diff --git a/.github/workflows/book.yml b/.github/workflows/book.yml index db458a3dbf..031a88b03c 100644 --- a/.github/workflows/book.yml +++ b/.github/workflows/book.yml @@ -13,7 +13,7 @@ jobs: build-and-upload-to-s3: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@master + - uses: actions/checkout@v4 - name: Setup mdBook uses: peaceiris/actions-mdbook@v1 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index bdd7b62653..54b355e631 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -71,7 +71,7 @@ jobs: VERSION_SUFFIX: ${{ needs.extract-version.outputs.VERSION_SUFFIX }} FEATURE_SUFFIX: ${{ matrix.features.version_suffix }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Update Rust if: env.SELF_HOSTED_RUNNERS == 'false' run: rustup update stable @@ -106,10 +106,10 @@ jobs: - name: Set up Docker Buildx if: env.SELF_HOSTED_RUNNERS == 'false' - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Build and push - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: file: ./Dockerfile.cross context: . @@ -129,7 +129,7 @@ jobs: VERSION_SUFFIX: ${{ needs.extract-version.outputs.VERSION_SUFFIX }} steps: - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Dockerhub login run: | @@ -148,7 +148,7 @@ jobs: VERSION: ${{ needs.extract-version.outputs.VERSION }} VERSION_SUFFIX: ${{ needs.extract-version.outputs.VERSION_SUFFIX }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Dockerhub login run: | echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin diff --git a/.github/workflows/linkcheck.yml b/.github/workflows/linkcheck.yml index 7f5d3e0b60..7e8d9135dd 100644 --- a/.github/workflows/linkcheck.yml +++ b/.github/workflows/linkcheck.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run mdbook server run: | diff --git a/.github/workflows/local-testnet.yml b/.github/workflows/local-testnet.yml index 75a81ce0e7..42293d38a7 100644 --- a/.github/workflows/local-testnet.yml +++ b/.github/workflows/local-testnet.yml @@ -24,7 +24,7 @@ jobs: # Enable portable to prevent issues with caching `blst` for the wrong CPU type FEATURES: portable,jemalloc steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get latest version of stable Rust run: rustup update stable @@ -46,7 +46,7 @@ jobs: echo "$(brew --prefix)/opt/gnu-sed/libexec/gnubin" >> $GITHUB_PATH echo "$(brew --prefix)/opt/grep/libexec/gnubin" >> $GITHUB_PATH # https://github.com/actions/cache/blob/main/examples.md#rust---cargo - - uses: actions/cache@v3 + - uses: actions/cache@v4 id: cache-cargo with: path: | @@ -95,6 +95,6 @@ jobs: runs-on: ubuntu-latest needs: ["run-local-testnet"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Check that success job is dependent on all others run: ./scripts/ci/check-success-job.sh ./.github/workflows/local-testnet.yml local-testnet-success diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 30ac52b6ef..3d23b4110e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -68,7 +68,7 @@ jobs: needs: extract-version steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Get latest version of stable Rust if: env.SELF_HOSTED_RUNNERS == 'false' run: rustup update stable @@ -80,7 +80,7 @@ jobs: - uses: KyleMayes/install-llvm-action@v1 if: env.SELF_HOSTED_RUNNERS == 'false' && startsWith(matrix.arch, 'x86_64-windows') with: - version: "15.0" + version: "16.0" directory: ${{ runner.temp }}/llvm - name: Set LIBCLANG_PATH if: startsWith(matrix.arch, 'x86_64-windows') @@ -172,17 +172,19 @@ jobs: # This is required to share artifacts between different jobs # ======================================================================= - - name: Upload artifact - uses: actions/upload-artifact@v3 + - name: Upload artifact + uses: actions/upload-artifact@v4 with: name: lighthouse-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.arch }}.tar.gz path: lighthouse-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.arch }}.tar.gz + compression-level: 0 - name: Upload signature - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: lighthouse-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.arch }}.tar.gz.asc path: lighthouse-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.arch }}.tar.gz.asc + compression-level: 0 draft-release: name: Draft Release @@ -193,7 +195,7 @@ jobs: steps: # This is necessary for generating the changelog. It has to come before "Download Artifacts" or else it deletes the artifacts. - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 @@ -202,7 +204,7 @@ jobs: # ============================== - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 # ============================== # Create release draft diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 87dc746ff9..377a7a6a86 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -41,7 +41,7 @@ jobs: # Use self-hosted runners only on the sigp repo. runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "large"]') || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get latest version of stable Rust if: env.SELF_HOSTED_RUNNERS == 'false' uses: moonrepo/setup-rust@v1 @@ -65,7 +65,7 @@ jobs: name: release-tests-windows runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "windows", "CI"]') || 'windows-2019' }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get latest version of stable Rust if: env.SELF_HOSTED_RUNNERS == 'false' uses: moonrepo/setup-rust@v1 @@ -86,7 +86,7 @@ jobs: # - uses: KyleMayes/install-llvm-action@v1 # if: env.SELF_HOSTED_RUNNERS == 'false' # with: -# version: "15.0" +# version: "16.0" # directory: ${{ runner.temp }}/llvm - name: Set LIBCLANG_PATH run: echo "LIBCLANG_PATH=$((gcm clang).source -replace "clang.exe")" >> $env:GITHUB_ENV @@ -102,7 +102,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get latest version of stable Rust if: env.SELF_HOSTED_RUNNERS == 'false' uses: moonrepo/setup-rust@v1 @@ -121,7 +121,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get latest version of stable Rust uses: moonrepo/setup-rust@v1 with: @@ -136,7 +136,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get latest version of stable Rust uses: moonrepo/setup-rust@v1 with: @@ -151,7 +151,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get latest version of stable Rust uses: moonrepo/setup-rust@v1 with: @@ -167,7 +167,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get latest version of stable Rust if: env.SELF_HOSTED_RUNNERS == 'false' uses: moonrepo/setup-rust@v1 @@ -188,7 +188,7 @@ jobs: name: state-transition-vectors-ubuntu runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get latest version of stable Rust uses: moonrepo/setup-rust@v1 with: @@ -203,7 +203,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get latest version of stable Rust if: env.SELF_HOSTED_RUNNERS == 'false' uses: moonrepo/setup-rust@v1 @@ -211,7 +211,7 @@ jobs: channel: stable cache-target: release bins: cargo-nextest - - name: Run consensus-spec-tests with blst, milagro and fake_crypto + - name: Run consensus-spec-tests with blst and fake_crypto run: make nextest-ef - name: Show cache stats if: env.SELF_HOSTED_RUNNERS == 'true' @@ -220,7 +220,7 @@ jobs: name: dockerfile-ubuntu runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Build the root Dockerfile run: docker build --build-arg FEATURES=portable -t lighthouse:local . - name: Test the built image @@ -229,7 +229,7 @@ jobs: name: eth1-simulator-ubuntu runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get latest version of stable Rust uses: moonrepo/setup-rust@v1 with: @@ -245,7 +245,7 @@ jobs: name: merge-transition-ubuntu runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get latest version of stable Rust uses: moonrepo/setup-rust@v1 with: @@ -261,7 +261,7 @@ jobs: name: no-eth1-simulator-ubuntu runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get latest version of stable Rust uses: moonrepo/setup-rust@v1 with: @@ -289,7 +289,7 @@ jobs: name: syncing-simulator-ubuntu runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get latest version of stable Rust uses: moonrepo/setup-rust@v1 with: @@ -308,7 +308,7 @@ jobs: # Enable portable to prevent issues with caching `blst` for the wrong CPU type FEATURES: jemalloc,portable steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get latest version of stable Rust if: env.SELF_HOSTED_RUNNERS == 'false' uses: moonrepo/setup-rust@v1 @@ -325,7 +325,9 @@ jobs: run: | make - name: Install lcli - if: env.SELF_HOSTED_RUNNERS == 'false' + # TODO: uncomment after the version of lcli in https://github.com/sigp/lighthouse/pull/5137 + # is installed on the runners + # if: env.SELF_HOSTED_RUNNERS == 'false' run: make install-lcli - name: Run the doppelganger protection failure test script run: | @@ -339,7 +341,7 @@ jobs: name: execution-engine-integration-ubuntu runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "small"]') || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get latest version of stable Rust if: env.SELF_HOSTED_RUNNERS == 'false' uses: moonrepo/setup-rust@v1 @@ -360,7 +362,7 @@ jobs: env: CARGO_INCREMENTAL: 1 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get latest version of stable Rust uses: moonrepo/setup-rust@v1 with: @@ -386,7 +388,7 @@ jobs: name: check-msrv runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Rust at Minimum Supported Rust Version (MSRV) run: | metadata=$(cargo metadata --no-deps --format-version 1) @@ -398,7 +400,7 @@ jobs: name: cargo-udeps runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get latest version of nightly Rust uses: moonrepo/setup-rust@v1 with: @@ -420,9 +422,9 @@ jobs: name: compile-with-beta-compiler runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install dependencies - run: sudo apt install -y git gcc g++ make cmake pkg-config llvm-dev libclang-dev clang + run: sudo apt update && 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 @@ -431,7 +433,7 @@ jobs: name: cli-check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get latest version of stable Rust uses: moonrepo/setup-rust@v1 with: @@ -469,6 +471,6 @@ jobs: 'cli-check', ] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Check that success job is dependent on all others run: ./scripts/ci/check-success-job.sh ./.github/workflows/test-suite.yml test-suite-success diff --git a/Cargo.lock b/Cargo.lock index 0b15faeb86..1f42e4b2d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,9 +115,9 @@ dependencies = [ [[package]] name = "aes" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher 0.4.4", @@ -126,14 +126,14 @@ dependencies = [ [[package]] name = "aes-gcm" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc3be92e19a7ef47457b8e6f90707e12b6ac5d20c6f3866584fa3be0787d839f" +checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6" dependencies = [ "aead 0.4.3", "aes 0.7.5", "cipher 0.3.0", - "ctr 0.7.0", + "ctr 0.8.0", "ghash 0.4.4", "subtle", ] @@ -145,7 +145,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" dependencies = [ "aead 0.5.2", - "aes 0.8.3", + "aes 0.8.4", "cipher 0.4.4", "ctr 0.9.2", "ghash 0.5.0", @@ -154,9 +154,9 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.7" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" +checksum = "8b79b82693f705137f8fb9b37871d99e4f9a7df12b917eed79c3d3954830a60b" dependencies = [ "cfg-if", "once_cell", @@ -180,9 +180,93 @@ 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.5.1#d3fc0a40cfe8b72ccda46ba050ee6786a59ce753" +name = "alloy-consensus" +version = "0.1.0" +source = "git+https://github.com/alloy-rs/alloy.git?rev=974d488bab5e21e9f17452a39a4bfa56677367b2#974d488bab5e21e9f17452a39a4bfa56677367b2" +dependencies = [ + "alloy-eips", + "alloy-network", + "alloy-primitives", + "alloy-rlp", +] + +[[package]] +name = "alloy-eips" +version = "0.1.0" +source = "git+https://github.com/alloy-rs/alloy.git?rev=974d488bab5e21e9f17452a39a4bfa56677367b2#974d488bab5e21e9f17452a39a4bfa56677367b2" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "serde", + "thiserror", +] + +[[package]] +name = "alloy-json-rpc" +version = "0.1.0" +source = "git+https://github.com/alloy-rs/alloy.git?rev=974d488bab5e21e9f17452a39a4bfa56677367b2#974d488bab5e21e9f17452a39a4bfa56677367b2" +dependencies = [ + "alloy-primitives", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "alloy-network" +version = "0.1.0" +source = "git+https://github.com/alloy-rs/alloy.git?rev=974d488bab5e21e9f17452a39a4bfa56677367b2#974d488bab5e21e9f17452a39a4bfa56677367b2" +dependencies = [ + "alloy-eips", + "alloy-json-rpc", + "alloy-primitives", + "alloy-rlp", + "serde", +] + +[[package]] +name = "alloy-primitives" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "600d34d8de81e23b6d909c094e23b3d357e01ca36b78a8c5424c501eedbe86f0" +dependencies = [ + "alloy-rlp", + "bytes", + "cfg-if", + "const-hex", + "derive_more", + "hex-literal", + "itoa", + "k256 0.13.3", + "keccak-asm", + "proptest", + "rand", + "ruint", + "serde", + "tiny-keccak", +] + +[[package]] +name = "alloy-rlp" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d58d9f5da7b40e9bfff0b7e7816700be4019db97d4b6359fe7f94a9e22e42ac" +dependencies = [ + "alloy-rlp-derive", + "arrayvec", + "bytes", +] + +[[package]] +name = "alloy-rlp-derive" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a047897373be4bbb0224c1afdabca92648dc57a9c9ef6e7b0be3aff7a859c83" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] [[package]] name = "android-tzdata" @@ -210,9 +294,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.79" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" +checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" [[package]] name = "arbitrary" @@ -229,6 +313,130 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" +[[package]] +name = "ark-ff" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" +dependencies = [ + "ark-ff-asm 0.3.0", + "ark-ff-macros 0.3.0", + "ark-serialize 0.3.0", + "ark-std 0.3.0", + "derivative", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.3.3", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm 0.4.2", + "ark-ff-macros 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "digest 0.10.7", + "itertools", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.4.0", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" +dependencies = [ + "num-bigint", + "num-traits", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-serialize" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" +dependencies = [ + "ark-std 0.3.0", + "digest 0.9.0", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-std 0.4.0", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-std" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand", +] + [[package]] name = "arrayref" version = "0.3.7" @@ -299,9 +507,9 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb41eb19024a91746eba0773aa5e16036045bbf45733766661099e182ea6a744" +checksum = "8f97ab0c5b00a7cdbe5a371b9a782ee7be1316095885c8a4ea1daf490eb0ef65" dependencies = [ "async-lock", "cfg-if", @@ -310,7 +518,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 0.38.30", + "rustix 0.38.31", "slab", "tracing", "windows-sys 0.52.0", @@ -335,7 +543,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -346,7 +554,20 @@ checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" dependencies = [ "futures", "pharos", - "rustc_version", + "rustc_version 0.4.0", +] + +[[package]] +name = "asynchronous-codec" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057f2c32adbb2fc158e22fb38433c8e9bbf76b75a4732c7c0cbaf695fb65568" +dependencies = [ + "bytes", + "futures-sink", + "futures-util", + "memchr", + "pin-project-lite", ] [[package]] @@ -362,18 +583,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "attohttpc" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb8867f378f33f78a811a8eb9bf108ad99430d7aad43315dd9319c827ef6247" -dependencies = [ - "http 0.2.11", - "log", - "url", - "wildmatch", -] - [[package]] name = "attohttpc" version = "0.24.1" @@ -398,14 +607,13 @@ dependencies = [ [[package]] name = "auto_impl" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fee3da8ef1276b0bee5dd1c7258010d8fffd31801447323115a25560e1327b89" +checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42" dependencies = [ - "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.52", ] [[package]] @@ -427,7 +635,7 @@ dependencies = [ "http 1.0.0", "http-body 1.0.0", "http-body-util", - "hyper 1.1.0", + "hyper 1.2.0", "hyper-util", "itoa", "matchit", @@ -536,7 +744,6 @@ dependencies = [ "ethereum_ssz", "ethereum_ssz_derive", "execution_layer", - "exit-future", "fork_choice", "futures", "genesis", @@ -583,7 +790,7 @@ dependencies = [ [[package]] name = "beacon_node" -version = "4.6.0" +version = "5.1.3" dependencies = [ "beacon_chain", "clap", @@ -599,7 +806,7 @@ dependencies = [ "genesis", "hex", "http_api", - "hyper 1.1.0", + "hyper 1.2.0", "lighthouse_network", "lighthouse_version", "monitoring_api", @@ -671,28 +878,20 @@ dependencies = [ ] [[package]] -name = "bindgen" -version = "0.66.1" +name = "bit-set" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b84e06fc203107bfbad243f4aba2af864eb7db3b1cf46ea0a023b0b433d2a7" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ - "bitflags 2.4.2", - "cexpr", - "clang-sys", - "lazy_static", - "lazycell", - "log", - "peeking_take_while", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 2.0.48", - "which", + "bit-vec", ] +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -774,7 +973,6 @@ dependencies = [ "ethereum_serde_utils", "ethereum_ssz", "hex", - "milagro_bls", "rand", "serde", "tree_hash", @@ -805,7 +1003,7 @@ dependencies = [ [[package]] name = "boot_node" -version = "4.6.0" +version = "5.1.3" dependencies = [ "beacon_node", "clap", @@ -857,9 +1055,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" [[package]] name = "byte-slice-cast" @@ -905,10 +1103,10 @@ dependencies = [ [[package]] name = "c-kzg" -version = "0.1.0" -source = "git+https://github.com/ethereum/c-kzg-4844?rev=748283cced543c486145d5f3f38684becdfe3e1b#748283cced543c486145d5f3f38684becdfe3e1b" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3130f3d8717cc02e668a896af24984d5d5d4e8bf12e278e982e0f1bd88a0f9af" dependencies = [ - "bindgen 0.66.1", "blst", "cc", "glob", @@ -943,9 +1141,9 @@ dependencies = [ [[package]] name = "cargo-platform" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceed8ef69d8518a5dda55c07425450b58a4e1946f4951eab6d7191ee86c2443d" +checksum = "694c8807f2ae16faecc43dc17d74b3eb042482789fd0eb64b39a2e04e087053f" dependencies = [ "serde", ] @@ -958,7 +1156,7 @@ checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" dependencies = [ "camino", "cargo-platform", - "semver", + "semver 1.0.22", "serde", "serde_json", "thiserror", @@ -972,9 +1170,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.83" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" dependencies = [ "jobserver", "libc", @@ -1021,14 +1219,14 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.32" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41daef31d7a747c5c847246f36de49ced6f7403b4cdabc807a97b5cc184cda7a" +checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", - "windows-targets 0.52.0", + "windows-targets 0.52.4", ] [[package]] @@ -1106,6 +1304,7 @@ dependencies = [ "eth1", "eth2", "eth2_config", + "ethereum_ssz", "execution_layer", "futures", "genesis", @@ -1170,6 +1369,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-hex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbd12d49ab0eaf8193ba9175e45f56bbc2e4b27d57b8cfe62aa47942a46b9a9" +dependencies = [ + "cfg-if", + "cpufeatures", + "hex", + "proptest", + "serde", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -1224,9 +1436,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" dependencies = [ "cfg-if", ] @@ -1269,9 +1481,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" dependencies = [ "crossbeam-utils", ] @@ -1354,9 +1566,9 @@ dependencies = [ [[package]] name = "crypto-mac" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" dependencies = [ "generic-array", "subtle", @@ -1383,15 +1595,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "ctr" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a232f92a03f37dd7d7dd2adc67166c77e9cd88de5b019b9a9eecfaeaf7bfd481" -dependencies = [ - "cipher 0.3.0", -] - [[package]] name = "ctr" version = "0.8.0" @@ -1422,9 +1625,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.1.1" +version = "4.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" +checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348" dependencies = [ "cfg-if", "cpufeatures", @@ -1432,7 +1635,7 @@ dependencies = [ "digest 0.10.7", "fiat-crypto", "platforms 3.3.0", - "rustc_version", + "rustc_version 0.4.0", "subtle", "zeroize", ] @@ -1445,7 +1648,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -1503,6 +1706,12 @@ dependencies = [ "libc", ] +[[package]] +name = "dary_heap" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7762d17f1241643615821a8455a0b2c3e803784b058693d990b11f2dce25a0ca" + [[package]] name = "data-encoding" version = "2.5.0" @@ -1641,7 +1850,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -1653,7 +1862,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "rustc_version", + "rustc_version 0.4.0", "syn 1.0.109", ] @@ -1680,7 +1889,7 @@ dependencies = [ "diesel_table_macro_syntax", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -1700,7 +1909,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" dependencies = [ - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -1776,11 +1985,12 @@ dependencies = [ [[package]] name = "discv5" -version = "0.4.0" -source = "git+https://github.com/sigp/discv5?rev=e30a2c31b7ac0c57876458b971164654dfa4513b#e30a2c31b7ac0c57876458b971164654dfa4513b" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bac33cb3f99889a57e56a8c6ccb77aaf0cfc7787602b7af09783f736d77314e1" dependencies = [ "aes 0.7.5", - "aes-gcm 0.9.2", + "aes-gcm 0.9.4", "arrayvec", "delay_map", "enr", @@ -1812,7 +2022,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -1865,9 +2075,9 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f628eaec48bfd21b865dc2950cfa014450c01d2fa2b69a86c2fd5844ec523c0" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", @@ -1915,9 +2125,9 @@ dependencies = [ [[package]] name = "either" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "elliptic-curve" @@ -1995,7 +2205,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -2025,10 +2235,10 @@ dependencies = [ name = "environment" version = "0.1.2" dependencies = [ + "async-channel", "ctrlc", "eth2_config", "eth2_network_config", - "exit-future", "futures", "logging", "serde", @@ -2553,12 +2763,12 @@ dependencies = [ name = "execution_engine_integration" version = "0.1.0" dependencies = [ + "async-channel", "deposit_contract", "environment", "ethers-core", "ethers-providers", "execution_layer", - "exit-future", "fork_choice", "futures", "hex", @@ -2577,6 +2787,8 @@ dependencies = [ name = "execution_layer" version = "0.1.0" dependencies = [ + "alloy-consensus", + "alloy-rlp", "arc-swap", "async-trait", "builder_client", @@ -2587,7 +2799,6 @@ dependencies = [ "ethereum_serde_utils", "ethereum_ssz", "ethers-core", - "exit-future", "fork_choice", "futures", "hash-db", @@ -2624,20 +2835,11 @@ dependencies = [ "zeroize", ] -[[package]] -name = "exit-future" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e43f2f1833d64e33f15592464d6fdd70f349dda7b1a53088eb83cd94014008c5" -dependencies = [ - "futures", -] - [[package]] name = "eyre" -version = "0.6.11" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6267a1fa6f59179ea4afc8e50fd8612a3cc60bc858f786ff877a4a8cb042799" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" dependencies = [ "indenter", "once_cell", @@ -2661,6 +2863,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "fastrlp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + [[package]] name = "ff" version = "0.12.1" @@ -2689,9 +2902,9 @@ checksum = "ec54ac60a7f2ee9a97cad9946f9bf629a3bc6a7ae59e68983dc9318f5a54b81a" [[package]] name = "fiat-crypto" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" +checksum = "1676f435fc1dadde4d03e43f5d62b259e1ce5f40bd4ffb21db2b42ebe59c1382" [[package]] name = "field-offset" @@ -2700,7 +2913,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" dependencies = [ "memoffset", - "rustc_version", + "rustc_version 0.4.0", ] [[package]] @@ -2838,7 +3051,8 @@ dependencies = [ [[package]] name = "futures-bounded" version = "0.2.3" -source = "git+https://github.com/sigp/rust-libp2p/?rev=cfa3275ca17e502799ed56e555b6c0611752e369#cfa3275ca17e502799ed56e555b6c0611752e369" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e2774cc104e198ef3d3e1ff4ab40f86fa3245d6cb6a3a46174f21463cee173" dependencies = [ "futures-timer", "futures-util", @@ -2896,7 +3110,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -2906,7 +3120,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35bd3cf68c183738046838e300353e4716c674dc5e56890de4826801a6622a28" dependencies = [ "futures-io", - "rustls 0.21.10", + "rustls", ] [[package]] @@ -2934,9 +3148,9 @@ dependencies = [ [[package]] name = "futures-timer" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" @@ -3053,7 +3267,7 @@ checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -3062,6 +3276,37 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "gossipsub" +version = "0.5.0" +dependencies = [ + "async-channel", + "asynchronous-codec 0.7.0", + "base64 0.21.7", + "byteorder", + "bytes", + "either", + "fnv", + "futures", + "futures-ticker", + "futures-timer", + "getrandom", + "hex_fmt", + "instant", + "libp2p", + "prometheus-client", + "quick-protobuf", + "quick-protobuf-codec 0.3.1", + "quickcheck", + "rand", + "regex", + "serde", + "sha2 0.10.8", + "smallvec", + "tracing", + "void", +] + [[package]] name = "group" version = "0.12.1" @@ -3086,9 +3331,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.24" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", @@ -3096,7 +3341,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.11", - "indexmap 2.1.0", + "indexmap 2.2.5", "slab", "tokio", "tokio-util 0.7.10", @@ -3105,9 +3350,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943" +checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069" dependencies = [ "bytes", "fnv", @@ -3115,7 +3360,7 @@ dependencies = [ "futures-sink", "futures-util", "http 1.0.0", - "indexmap 2.1.0", + "indexmap 2.2.5", "slab", "tokio", "tokio-util 0.7.10", @@ -3124,9 +3369,9 @@ dependencies = [ [[package]] name = "half" -version = "1.8.2" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" [[package]] name = "hash-db" @@ -3149,6 +3394,15 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.14.3" @@ -3218,15 +3472,24 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.4" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" [[package]] name = "hex_fmt" @@ -3251,7 +3514,7 @@ dependencies = [ "ipnet", "once_cell", "rand", - "socket2 0.5.5", + "socket2 0.5.6", "thiserror", "tinyvec", "tokio", @@ -3305,7 +3568,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" dependencies = [ - "crypto-mac 0.11.0", + "crypto-mac 0.11.1", "digest 0.9.0", ] @@ -3329,15 +3592,6 @@ dependencies = [ "hmac 0.8.1", ] -[[package]] -name = "home" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "hostname" version = "0.3.1" @@ -3501,14 +3755,14 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.3.24", + "h2 0.3.26", "http 0.2.11", "http-body 0.4.6", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.5", + "socket2 0.5.6", "tokio", "tower-service", "tracing", @@ -3517,20 +3771,21 @@ dependencies = [ [[package]] name = "hyper" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5aa53871fc917b1a9ed87b683a5d86db645e23acb32c2e0785a353e522fb75" +checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.2", + "h2 0.4.4", "http 1.0.0", "http-body 1.0.0", "httparse", "httpdate", "itoa", "pin-project-lite", + "smallvec", "tokio", ] @@ -3543,9 +3798,9 @@ dependencies = [ "futures-util", "http 0.2.11", "hyper 0.14.28", - "rustls 0.21.10", + "rustls", "tokio", - "tokio-rustls 0.24.1", + "tokio-rustls", ] [[package]] @@ -3563,27 +3818,25 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdea9aac0dbe5a9240d68cfd9501e2db94222c6dc06843e06640b9e07f0fdc67" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" dependencies = [ "bytes", - "futures-channel", "futures-util", "http 1.0.0", "http-body 1.0.0", - "hyper 1.1.0", + "hyper 1.2.0", "pin-project-lite", - "socket2 0.5.5", + "socket2 0.5.6", "tokio", - "tracing", ] [[package]] name = "iana-time-zone" -version = "0.1.59" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -3628,17 +3881,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "if-addrs" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2273e421f7c4f0fc99e1934fe4776f59d8df2972f4199d703fc0da9f2a9f73de" -dependencies = [ - "if-addrs-sys", - "libc", - "winapi", -] - [[package]] name = "if-addrs" version = "0.10.2" @@ -3649,16 +3891,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "if-addrs-sys" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de74b9dd780476e837e5eb5ab7c88b49ed304126e412030a0adba99c8efe79ea" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "if-watch" version = "3.2.0" @@ -3669,7 +3901,7 @@ dependencies = [ "core-foundation", "fnv", "futures", - "if-addrs 0.10.2", + "if-addrs", "ipnet", "log", "rtnetlink", @@ -3678,19 +3910,6 @@ dependencies = [ "windows", ] -[[package]] -name = "igd" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556b5a75cd4adb7c4ea21c64af1c48cefb2ce7d43dc4352c720a1fe47c21f355" -dependencies = [ - "attohttpc 0.16.3", - "log", - "rand", - "url", - "xmltree", -] - [[package]] name = "igd-next" version = "0.14.3" @@ -3698,7 +3917,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064d90fec10d541084e7b39ead8875a5a80d9114a2b18791565253bae25f49e4" dependencies = [ "async-trait", - "attohttpc 0.24.1", + "attohttpc", "bytes", "futures", "http 0.2.11", @@ -3784,9 +4003,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" dependencies = [ "equivalent", "hashbrown 0.14.3", @@ -3837,7 +4056,7 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi 0.3.4", + "hermit-abi 0.3.9", "libc", "windows-sys 0.48.0", ] @@ -3848,7 +4067,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2 0.5.5", + "socket2 0.5.6", "widestring 1.0.2", "windows-sys 0.48.0", "winreg", @@ -3860,6 +4079,17 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "is-terminal" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "itertools" version = "0.10.5" @@ -3908,18 +4138,18 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.67" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" +checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" dependencies = [ "wasm-bindgen", ] @@ -3974,6 +4204,16 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "keccak-asm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb8515fff80ed850aea4a1595f2e519c003e2a00a82fe168ebf5269196caf444" +dependencies = [ + "digest 0.10.7", + "sha3-asm", +] + [[package]] name = "keccak-hash" version = "0.10.0" @@ -4017,7 +4257,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lcli" -version = "4.6.0" +version = "5.1.3" dependencies = [ "account_utils", "beacon_chain", @@ -4080,38 +4320,42 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.152" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libflate" -version = "1.4.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ff4ae71b685bbad2f2f391fe74f6b7659a34871c08b210fdc039e43bee07d18" +checksum = "9f7d5654ae1795afc7ff76f4365c2c8791b0feb18e8996a96adad8ffd7c3b2bf" dependencies = [ "adler32", + "core2", "crc32fast", + "dary_heap", "libflate_lz77", ] [[package]] name = "libflate_lz77" -version = "1.2.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a52d3a8bfc85f250440e4424db7d857e241a3aebbbe301f3eb606ab15c39acbf" +checksum = "be5f52fb8c451576ec6b79d3f4deb327398bc05bbdbd99021a6e77a4c855d524" dependencies = [ + "core2", + "hashbrown 0.13.2", "rle-decode-fast", ] [[package]] name = "libloading" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" +checksum = "2caa5afb8bf9f3a2652760ce7d4f62d21c4d5a423e68466fca30df82f2330164" dependencies = [ "cfg-if", - "windows-sys 0.48.0", + "windows-targets 0.52.4", ] [[package]] @@ -4137,8 +4381,9 @@ dependencies = [ [[package]] name = "libp2p" -version = "0.54.0" -source = "git+https://github.com/sigp/rust-libp2p/?rev=cfa3275ca17e502799ed56e555b6c0611752e369#cfa3275ca17e502799ed56e555b6c0611752e369" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "681fb3f183edfbedd7a57d32ebe5dcdc0b9f94061185acf3c30249349cc6fc99" dependencies = [ "bytes", "either", @@ -4150,7 +4395,6 @@ dependencies = [ "libp2p-connection-limits", "libp2p-core", "libp2p-dns", - "libp2p-gossipsub", "libp2p-identify", "libp2p-identity", "libp2p-mdns", @@ -4171,7 +4415,8 @@ dependencies = [ [[package]] name = "libp2p-allow-block-list" version = "0.3.0" -source = "git+https://github.com/sigp/rust-libp2p/?rev=cfa3275ca17e502799ed56e555b6c0611752e369#cfa3275ca17e502799ed56e555b6c0611752e369" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "107b238b794cb83ab53b74ad5dcf7cca3200899b72fe662840cfb52f5b0a32e6" dependencies = [ "libp2p-core", "libp2p-identity", @@ -4182,7 +4427,8 @@ dependencies = [ [[package]] name = "libp2p-connection-limits" version = "0.3.1" -source = "git+https://github.com/sigp/rust-libp2p/?rev=cfa3275ca17e502799ed56e555b6c0611752e369#cfa3275ca17e502799ed56e555b6c0611752e369" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7cd50a78ccfada14de94cbacd3ce4b0138157f376870f13d3a8422cd075b4fd" dependencies = [ "libp2p-core", "libp2p-identity", @@ -4193,7 +4439,8 @@ dependencies = [ [[package]] name = "libp2p-core" version = "0.41.2" -source = "git+https://github.com/sigp/rust-libp2p/?rev=cfa3275ca17e502799ed56e555b6c0611752e369#cfa3275ca17e502799ed56e555b6c0611752e369" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8130a8269e65a2554d55131c770bdf4bcd94d2b8d4efb24ca23699be65066c05" dependencies = [ "either", "fnv", @@ -4220,7 +4467,8 @@ dependencies = [ [[package]] name = "libp2p-dns" version = "0.41.1" -source = "git+https://github.com/sigp/rust-libp2p/?rev=cfa3275ca17e502799ed56e555b6c0611752e369#cfa3275ca17e502799ed56e555b6c0611752e369" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d17cbcf7160ff35c3e8e560de4a068fe9d6cb777ea72840e48eb76ff9576c4b6" dependencies = [ "async-trait", "futures", @@ -4232,44 +4480,13 @@ dependencies = [ "tracing", ] -[[package]] -name = "libp2p-gossipsub" -version = "0.46.1" -source = "git+https://github.com/sigp/rust-libp2p/?rev=cfa3275ca17e502799ed56e555b6c0611752e369#cfa3275ca17e502799ed56e555b6c0611752e369" -dependencies = [ - "async-channel", - "asynchronous-codec", - "base64 0.21.7", - "byteorder", - "bytes", - "either", - "fnv", - "futures", - "futures-ticker", - "futures-timer", - "getrandom", - "hex_fmt", - "instant", - "libp2p-core", - "libp2p-identity", - "libp2p-swarm", - "prometheus-client", - "quick-protobuf", - "quick-protobuf-codec", - "rand", - "regex", - "sha2 0.10.8", - "smallvec", - "tracing", - "void", -] - [[package]] name = "libp2p-identify" version = "0.44.1" -source = "git+https://github.com/sigp/rust-libp2p/?rev=cfa3275ca17e502799ed56e555b6c0611752e369#cfa3275ca17e502799ed56e555b6c0611752e369" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20499a945d2f0221fdc6269b3848892c0f370d2ee3e19c7f65a29d8f860f6126" dependencies = [ - "asynchronous-codec", + "asynchronous-codec 0.7.0", "either", "futures", "futures-bounded", @@ -4279,7 +4496,7 @@ dependencies = [ "libp2p-swarm", "lru", "quick-protobuf", - "quick-protobuf-codec", + "quick-protobuf-codec 0.3.1", "smallvec", "thiserror", "tracing", @@ -4312,7 +4529,8 @@ dependencies = [ [[package]] name = "libp2p-mdns" version = "0.45.1" -source = "git+https://github.com/sigp/rust-libp2p/?rev=cfa3275ca17e502799ed56e555b6c0611752e369#cfa3275ca17e502799ed56e555b6c0611752e369" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49007d9a339b3e1d7eeebc4d67c05dbf23d300b7d091193ec2d3f26802d7faf2" dependencies = [ "data-encoding", "futures", @@ -4323,7 +4541,7 @@ dependencies = [ "libp2p-swarm", "rand", "smallvec", - "socket2 0.5.5", + "socket2 0.5.6", "tokio", "tracing", "void", @@ -4332,12 +4550,12 @@ dependencies = [ [[package]] name = "libp2p-metrics" version = "0.14.1" -source = "git+https://github.com/sigp/rust-libp2p/?rev=cfa3275ca17e502799ed56e555b6c0611752e369#cfa3275ca17e502799ed56e555b6c0611752e369" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdac91ae4f291046a3b2660c039a2830c931f84df2ee227989af92f7692d3357" dependencies = [ "futures", "instant", "libp2p-core", - "libp2p-gossipsub", "libp2p-identify", "libp2p-identity", "libp2p-swarm", @@ -4348,9 +4566,10 @@ dependencies = [ [[package]] name = "libp2p-mplex" version = "0.41.0" -source = "git+https://github.com/sigp/rust-libp2p/?rev=cfa3275ca17e502799ed56e555b6c0611752e369#cfa3275ca17e502799ed56e555b6c0611752e369" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e895765e27e30217b25f7cb7ac4686dad1ff80bf2fdeffd1d898566900a924" dependencies = [ - "asynchronous-codec", + "asynchronous-codec 0.6.2", "bytes", "futures", "libp2p-core", @@ -4360,15 +4579,16 @@ dependencies = [ "rand", "smallvec", "tracing", - "unsigned-varint 0.8.0", + "unsigned-varint 0.7.2", ] [[package]] name = "libp2p-noise" version = "0.44.0" -source = "git+https://github.com/sigp/rust-libp2p/?rev=cfa3275ca17e502799ed56e555b6c0611752e369#cfa3275ca17e502799ed56e555b6c0611752e369" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecd0545ce077f6ea5434bcb76e8d0fe942693b4380aaad0d34a358c2bd05793" dependencies = [ - "asynchronous-codec", + "asynchronous-codec 0.7.0", "bytes", "curve25519-dalek", "futures", @@ -4391,22 +4611,24 @@ dependencies = [ [[package]] name = "libp2p-plaintext" version = "0.41.0" -source = "git+https://github.com/sigp/rust-libp2p/?rev=cfa3275ca17e502799ed56e555b6c0611752e369#cfa3275ca17e502799ed56e555b6c0611752e369" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67330af40b67217e746d42551913cfb7ad04c74fa300fb329660a56318590b3f" dependencies = [ - "asynchronous-codec", + "asynchronous-codec 0.6.2", "bytes", "futures", "libp2p-core", "libp2p-identity", "quick-protobuf", - "quick-protobuf-codec", + "quick-protobuf-codec 0.2.0", "tracing", ] [[package]] name = "libp2p-quic" version = "0.10.2" -source = "git+https://github.com/sigp/rust-libp2p/?rev=cfa3275ca17e502799ed56e555b6c0611752e369#cfa3275ca17e502799ed56e555b6c0611752e369" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0375cdfee57b47b313ef1f0fdb625b78aed770d33a40cf1c294a371ff5e6666" dependencies = [ "bytes", "futures", @@ -4419,8 +4641,8 @@ dependencies = [ "quinn", "rand", "ring 0.16.20", - "rustls 0.21.10", - "socket2 0.5.5", + "rustls", + "socket2 0.5.6", "thiserror", "tokio", "tracing", @@ -4428,8 +4650,9 @@ dependencies = [ [[package]] name = "libp2p-swarm" -version = "0.45.0" -source = "git+https://github.com/sigp/rust-libp2p/?rev=cfa3275ca17e502799ed56e555b6c0611752e369#cfa3275ca17e502799ed56e555b6c0611752e369" +version = "0.44.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e92532fc3c4fb292ae30c371815c9b10103718777726ea5497abc268a4761866" dependencies = [ "either", "fnv", @@ -4451,18 +4674,20 @@ dependencies = [ [[package]] name = "libp2p-swarm-derive" version = "0.34.1" -source = "git+https://github.com/sigp/rust-libp2p/?rev=cfa3275ca17e502799ed56e555b6c0611752e369#cfa3275ca17e502799ed56e555b6c0611752e369" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b644268b4acfdaa6a6100b31226ee7a36d96ab4c43287d113bfd2308607d8b6f" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] name = "libp2p-tcp" version = "0.41.0" -source = "git+https://github.com/sigp/rust-libp2p/?rev=cfa3275ca17e502799ed56e555b6c0611752e369#cfa3275ca17e502799ed56e555b6c0611752e369" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2460fc2748919adff99ecbc1aab296e4579e41f374fb164149bd2c9e529d4c" dependencies = [ "futures", "futures-timer", @@ -4470,7 +4695,7 @@ dependencies = [ "libc", "libp2p-core", "libp2p-identity", - "socket2 0.5.5", + "socket2 0.5.6", "tokio", "tracing", ] @@ -4478,7 +4703,8 @@ dependencies = [ [[package]] name = "libp2p-tls" version = "0.3.0" -source = "git+https://github.com/sigp/rust-libp2p/?rev=cfa3275ca17e502799ed56e555b6c0611752e369#cfa3275ca17e502799ed56e555b6c0611752e369" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ce7e3c2e7569d685d08ec795157981722ff96e9e9f9eae75df3c29d02b07a5" dependencies = [ "futures", "futures-rustls", @@ -4486,8 +4712,8 @@ dependencies = [ "libp2p-identity", "rcgen", "ring 0.16.20", - "rustls 0.21.10", - "rustls-webpki 0.101.7", + "rustls", + "rustls-webpki", "thiserror", "x509-parser", "yasna", @@ -4495,8 +4721,9 @@ dependencies = [ [[package]] name = "libp2p-upnp" -version = "0.2.0" -source = "git+https://github.com/sigp/rust-libp2p/?rev=cfa3275ca17e502799ed56e555b6c0611752e369#cfa3275ca17e502799ed56e555b6c0611752e369" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49cc89949bf0e06869297cd4fe2c132358c23fe93e76ad43950453df4da3d35" dependencies = [ "futures", "futures-timer", @@ -4511,7 +4738,8 @@ dependencies = [ [[package]] name = "libp2p-yamux" version = "0.45.1" -source = "git+https://github.com/sigp/rust-libp2p/?rev=cfa3275ca17e502799ed56e555b6c0611752e369#cfa3275ca17e502799ed56e555b6c0611752e369" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200cbe50349a44760927d50b431d77bed79b9c0a3959de1af8d24a63434b71e5" dependencies = [ "either", "futures", @@ -4594,9 +4822,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.14" +version = "1.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "295c17e837573c8c821dbaeb3cceb3d745ad082f7572191409e69cbc1b3fd050" +checksum = "037731f5d3aaa87a5675e895b63ddff1a87624bc29f77004ea829809654e48f6" dependencies = [ "cc", "pkg-config", @@ -4605,7 +4833,7 @@ dependencies = [ [[package]] name = "lighthouse" -version = "4.6.0" +version = "5.1.3" dependencies = [ "account_manager", "account_utils", @@ -4659,17 +4887,26 @@ dependencies = [ name = "lighthouse_network" version = "0.2.0" dependencies = [ + "async-channel", + "base64 0.21.7", + "byteorder", + "bytes", "delay_map", "directory", "dirs", "discv5", + "either", "error-chain", "ethereum_ssz", "ethereum_ssz_derive", - "exit-future", "fnv", "futures", + "futures-ticker", + "getrandom", + "gossipsub", "hex", + "hex_fmt", + "instant", "lazy_static", "libp2p", "libp2p-mplex", @@ -4699,6 +4936,7 @@ dependencies = [ "tokio", "tokio-io-timeout", "tokio-util 0.6.10", + "tracing", "tree_hash", "tree_hash_derive", "types", @@ -4775,9 +5013,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "logging" @@ -4804,9 +5042,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2994eeba8ed550fd9b47a0b38f0242bc3344e496483c6180b69139cc2fa5d1d7" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" dependencies = [ "hashbrown 0.14.3", ] @@ -4897,7 +5135,7 @@ name = "mdbx-sys" version = "0.11.6-4" source = "git+https://github.com/sigp/libmdbx-rs?tag=v0.1.4#096da80a83d14343f8df833006483f48075cd135" dependencies = [ - "bindgen 0.59.2", + "bindgen", "cc", "cmake", "libc", @@ -4905,9 +5143,9 @@ dependencies = [ [[package]] name = "mediatype" -version = "0.19.17" +version = "0.19.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a018c36a54f4e12c30464bbc59311f85d3f6f4d6c1b4fa4ea9db2b174ddefc" +checksum = "8878cd8d1b3c8c8ae4b2ba0a36652b7cf192f618a599a7fbdfa25cffd4ea72dd" [[package]] name = "memchr" @@ -4980,18 +5218,6 @@ dependencies = [ "quote", ] -[[package]] -name = "milagro_bls" -version = "1.5.1" -source = "git+https://github.com/sigp/milagro_bls?tag=v1.5.1#d3fc0a40cfe8b72ccda46ba050ee6786a59ce753" -dependencies = [ - "amcl", - "hex", - "lazy_static", - "rand", - "zeroize", -] - [[package]] name = "mime" version = "0.3.17" @@ -5016,18 +5242,18 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi", @@ -5036,9 +5262,9 @@ dependencies = [ [[package]] name = "mock_instant" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c1a54de846c4006b88b1516731cc1f6026eb5dc4bcb186aa071ef66d40524ec" +checksum = "9366861eb2a2c436c20b12c8dbec5f798cea6b47ad99216be0282942e2c81ea0" [[package]] name = "monitoring_api" @@ -5108,14 +5334,15 @@ dependencies = [ [[package]] name = "multistream-select" version = "0.13.0" -source = "git+https://github.com/sigp/rust-libp2p/?rev=cfa3275ca17e502799ed56e555b6c0611752e369#cfa3275ca17e502799ed56e555b6c0611752e369" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0df8e5eec2298a62b326ee4f0d7fe1a6b90a09dfcf9df37b38f947a8c42f19" dependencies = [ "bytes", "futures", + "log", "pin-project", "smallvec", - "tracing", - "unsigned-varint 0.8.0", + "unsigned-varint 0.7.2", ] [[package]] @@ -5206,6 +5433,8 @@ dependencies = [ name = "network" version = "0.2.0" dependencies = [ + "anyhow", + "async-channel", "beacon_chain", "beacon_processor", "delay_map", @@ -5216,13 +5445,12 @@ dependencies = [ "ethereum-types 0.14.1", "ethereum_ssz", "execution_layer", - "exit-future", "fnv", "futures", "genesis", + "gossipsub", "hex", - "if-addrs 0.6.7", - "igd", + "igd-next", "itertools", "lazy_static", "lighthouse_metrics", @@ -5355,20 +5583,25 @@ dependencies = [ ] [[package]] -name = "num-integer" -version = "0.1.45" +name = "num-conv" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] [[package]] name = "num-iter" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" dependencies = [ "autocfg", "num-integer", @@ -5377,11 +5610,12 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -5390,16 +5624,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.3.4", - "libc", -] - -[[package]] -name = "num_threads" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" -dependencies = [ + "hermit-abi 0.3.9", "libc", ] @@ -5442,9 +5667,9 @@ checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] name = "opaque-debug" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "open-fastrlp" @@ -5473,9 +5698,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.63" +version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ "bitflags 2.4.2", "cfg-if", @@ -5494,7 +5719,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -5505,18 +5730,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.2.1+3.2.0" +version = "300.2.3+3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fe476c29791a5ca0d1273c697e96085bbabbbea2ef7afd5617e78a4b40332d3" +checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.99" +version = "0.9.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" +checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" dependencies = [ "cc", "libc", @@ -5695,7 +5920,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d95f5254224e617595d2cc3cc73ff0a5eaf2637519e25f03388154e9378b6ffa" dependencies = [ - "crypto-mac 0.11.0", + "crypto-mac 0.11.1", ] [[package]] @@ -5750,6 +5975,17 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f8023d0fb78c8e03784ea1c7f3fa36e68a723138990b8d5a47d916b651e7a8" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + [[package]] name = "pharos" version = "0.5.3" @@ -5757,7 +5993,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" dependencies = [ "futures", - "rustc_version", + "rustc_version 0.4.0", ] [[package]] @@ -5780,22 +6016,22 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -5832,9 +6068,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "platforms" @@ -5878,14 +6114,14 @@ dependencies = [ [[package]] name = "polling" -version = "3.3.2" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "545c980a3880efd47b2e262f6a4bb6daad6555cf3367aa9c4e52895f69537a41" +checksum = "24f040dee2588b4963afb4e420540439d126f73fdacf4a9c486a96d840bac3c9" dependencies = [ "cfg-if", "concurrent-queue", "pin-project-lite", - "rustix 0.38.30", + "rustix 0.38.31", "tracing", "windows-sys 0.52.0", ] @@ -5910,7 +6146,7 @@ dependencies = [ "cfg-if", "cpufeatures", "opaque-debug", - "universal-hash 0.4.0", + "universal-hash 0.4.1", ] [[package]] @@ -5983,16 +6219,6 @@ dependencies = [ "sensitive_url", ] -[[package]] -name = "prettyplease" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" -dependencies = [ - "proc-macro2", - "syn 2.0.48", -] - [[package]] name = "primeorder" version = "0.13.6" @@ -6048,30 +6274,6 @@ dependencies = [ "toml_edit 0.20.7", ] -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro2" version = "1.0.78" @@ -6113,9 +6315,9 @@ dependencies = [ [[package]] name = "prometheus-client" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "510c4f1c9d81d556458f94c98f857748130ea9737bbd6053da497503b26ea63c" +checksum = "6f87c10af16e0af74010d2a123d202e8363c04db5acfa91d8747f64a8524da3a" dependencies = [ "dtoa", "itoa", @@ -6131,7 +6333,27 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", +] + +[[package]] +name = "proptest" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.4.2", + "lazy_static", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax 0.8.2", + "rusty-fork", + "tempfile", + "unarray", ] [[package]] @@ -6189,10 +6411,24 @@ dependencies = [ [[package]] name = "quick-protobuf-codec" -version = "0.3.1" -source = "git+https://github.com/sigp/rust-libp2p/?rev=cfa3275ca17e502799ed56e555b6c0611752e369#cfa3275ca17e502799ed56e555b6c0611752e369" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ededb1cd78531627244d51dd0c7139fbe736c7d57af0092a76f0ffb2f56e98" dependencies = [ - "asynchronous-codec", + "asynchronous-codec 0.6.2", + "bytes", + "quick-protobuf", + "thiserror", + "unsigned-varint 0.7.2", +] + +[[package]] +name = "quick-protobuf-codec" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15a0580ab32b169745d7a39db2ba969226ca16738931be152a3209b409de2474" +dependencies = [ + "asynchronous-codec 0.7.0", "bytes", "quick-protobuf", "thiserror", @@ -6233,7 +6469,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.21.10", + "rustls", "thiserror", "tokio", "tracing", @@ -6249,7 +6485,7 @@ dependencies = [ "rand", "ring 0.16.20", "rustc-hash", - "rustls 0.21.10", + "rustls", "slab", "thiserror", "tinyvec", @@ -6264,7 +6500,7 @@ checksum = "055b4e778e8feb9f93c4e439f71dc2156ef13360b432b799e179a8c4cdf0b1d7" dependencies = [ "bytes", "libc", - "socket2 0.5.5", + "socket2 0.5.6", "tracing", "windows-sys 0.48.0", ] @@ -6352,9 +6588,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" +checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" dependencies = [ "either", "rayon-core", @@ -6419,7 +6655,7 @@ checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.4", + "regex-automata 0.4.5", "regex-syntax 0.8.2", ] @@ -6434,9 +6670,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ "aho-corasick", "memchr", @@ -6457,16 +6693,16 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.11.23" +version = "0.11.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" dependencies = [ "base64 0.21.7", "bytes", "encoding_rs", "futures-core", "futures-util", - "h2 0.3.24", + "h2 0.3.26", "http 0.2.11", "http-body 0.4.6", "hyper 0.14.28", @@ -6480,15 +6716,16 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.21.10", - "rustls-pemfile 1.0.4", + "rustls", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", + "sync_wrapper", "system-configuration", "tokio", "tokio-native-tls", - "tokio-rustls 0.24.1", + "tokio-rustls", "tokio-util 0.7.10", "tower-service", "url", @@ -6548,16 +6785,17 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.7" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", + "cfg-if", "getrandom", "libc", "spin 0.9.8", "untrusted 0.9.0", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -6612,6 +6850,36 @@ dependencies = [ "tokio", ] +[[package]] +name = "ruint" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b1d9521f889713d1221270fdd63370feca7e5c71a18745343402fa86e4f04f" +dependencies = [ + "alloy-rlp", + "ark-ff 0.3.0", + "ark-ff 0.4.2", + "bytes", + "fastrlp", + "num-bigint", + "num-traits", + "parity-scale-codec 3.6.9", + "primitive-types 0.12.2", + "proptest", + "rand", + "rlp", + "ruint-macro", + "serde", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86854cf50259291520509879a5c294c3c9a4c334e9ff65071c51e42ef1e2343" + [[package]] name = "rusqlite" version = "0.28.0" @@ -6644,13 +6912,22 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver 0.11.0", +] + [[package]] name = "rustc_version" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver", + "semver 1.0.22", ] [[package]] @@ -6678,9 +6955,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.30" +version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ "bitflags 2.4.2", "errno", @@ -6696,25 +6973,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", - "ring 0.17.7", - "rustls-webpki 0.101.7", + "ring 0.17.8", + "rustls-webpki", "sct", ] -[[package]] -name = "rustls" -version = "0.22.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" -dependencies = [ - "log", - "ring 0.17.7", - "rustls-pki-types", - "rustls-webpki 0.102.1", - "subtle", - "zeroize", -] - [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -6724,40 +6987,13 @@ dependencies = [ "base64 0.21.7", ] -[[package]] -name = "rustls-pemfile" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e4980fa29e4c4b212ffb3db068a564cbf560e51d3944b7c88bd8bf5bec64f4" -dependencies = [ - "base64 0.21.7", - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e9d979b3ce68192e42760c7810125eb6cf2ea10efae545a156063e61f314e2a" - [[package]] name = "rustls-webpki" version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring 0.17.7", - "untrusted 0.9.0", -] - -[[package]] -name = "rustls-webpki" -version = "0.102.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4ca26037c909dedb327b48c3327d0ba91d3dd3c4e05dad328f210ffb68e95b" -dependencies = [ - "ring 0.17.7", - "rustls-pki-types", + "ring 0.17.8", "untrusted 0.9.0", ] @@ -6767,10 +7003,23 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "rw-stream-sink" version = "0.4.0" -source = "git+https://github.com/sigp/rust-libp2p/?rev=cfa3275ca17e502799ed56e555b6c0611752e369#cfa3275ca17e502799ed56e555b6c0611752e369" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c9026ff5d2f23da5e45bbc283f156383001bfb09c4e44256d02c1a685fe9a1" dependencies = [ "futures", "pin-project", @@ -6779,9 +7028,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "safe_arith" @@ -6877,7 +7126,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring 0.17.7", + "ring 0.17.8", "untrusted 0.9.0", ] @@ -6934,13 +7183,31 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.21" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" dependencies = [ "serde", ] +[[package]] +name = "semver-parser" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" +dependencies = [ + "pest", +] + [[package]] name = "send_wrapper" version = "0.6.0" @@ -6957,9 +7224,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.195" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] @@ -6986,20 +7253,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.195" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] name = "serde_json" -version = "1.0.111" +version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" dependencies = [ "itoa", "ryu", @@ -7024,7 +7291,7 @@ checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -7072,11 +7339,11 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.9.30" +version = "0.9.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1bf28c79a99f70ee1f1d83d10c875d2e70618417fda01ad1785e027579d9d38" +checksum = "8fd075d994154d4a774f95b51fb96bdc2832b0ea48425c92546073816cda1f2f" dependencies = [ - "indexmap 2.1.0", + "indexmap 2.2.5", "itoa", "ryu", "serde", @@ -7140,6 +7407,16 @@ dependencies = [ "keccak", ] +[[package]] +name = "sha3-asm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bac61da6b35ad76b195eb4771210f947734321a8d81d7738e1580d953bc7a15e" +dependencies = [ + "cc", + "cfg-if", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -7363,11 +7640,11 @@ dependencies = [ [[package]] name = "slog-term" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87d29185c55b7b258b4f120eab00f48557d4d9bc814f41713f449d35b0f8977c" +checksum = "b6e022d0b998abfe5c3782c1f03551a596269450ccd677ea51c56f8b214610e8" dependencies = [ - "atty", + "is-terminal", "slog", "term", "thread_local", @@ -7376,9 +7653,9 @@ dependencies = [ [[package]] name = "sloggers" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0a4d8569a69ee56f277bffc2f6eee637b98ed468448e8a5a84fa63efe4de9d" +checksum = "75062c2738b82cd45ae633623caae3393f43eb00aada1dc2d3ebe88db6b0db9b" dependencies = [ "chrono", "libc", @@ -7422,17 +7699,17 @@ checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" [[package]] name = "snow" -version = "0.9.4" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58021967fd0a5eeeb23b08df6cc244a4d4a5b4aec1d27c9e02fad1a58b4cd74e" +checksum = "850948bee068e713b8ab860fe1adc4d109676ab4c3b621fd8147f06b261f2f85" dependencies = [ "aes-gcm 0.10.3", "blake2", "chacha20poly1305", "curve25519-dalek", "rand_core", - "ring 0.17.7", - "rustc_version", + "ring 0.17.8", + "rustc_version 0.4.0", "sha2 0.10.8", "subtle", ] @@ -7449,12 +7726,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -7623,9 +7900,9 @@ dependencies = [ [[package]] name = "subtle" -version = "2.5.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "superstruct" @@ -7663,9 +7940,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.48" +version = "2.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" dependencies = [ "proc-macro2", "quote", @@ -7767,7 +8044,7 @@ checksum = "c63f48baada5c52e65a29eef93ab4f8982681b67f9e8d29c7b05abcfec2b9ffe" name = "task_executor" version = "0.1.0" dependencies = [ - "exit-future", + "async-channel", "futures", "lazy_static", "lighthouse_metrics", @@ -7778,14 +8055,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.9.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", - "redox_syscall 0.4.1", - "rustix 0.38.30", + "rustix 0.38.31", "windows-sys 0.52.0", ] @@ -7853,29 +8129,29 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.56" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.56" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] name = "thread_local" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", @@ -7892,14 +8168,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ "deranged", "itoa", - "libc", - "num_threads", + "num-conv", "powerfmt", "serde", "time-core", @@ -7914,10 +8189,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" dependencies = [ + "num-conv", "time-core", ] @@ -7987,9 +8263,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.1" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", @@ -7998,7 +8274,7 @@ dependencies = [ "num_cpus", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.5", + "socket2 0.5.6", "tokio-macros", "windows-sys 0.48.0", ] @@ -8021,7 +8297,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -8054,7 +8330,7 @@ dependencies = [ "postgres-protocol", "postgres-types", "rand", - "socket2 0.5.5", + "socket2 0.5.6", "tokio", "tokio-util 0.7.10", "whoami", @@ -8066,18 +8342,7 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls 0.21.10", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" -dependencies = [ - "rustls 0.22.2", - "rustls-pki-types", + "rustls", "tokio", ] @@ -8160,7 +8425,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.1.0", + "indexmap 2.2.5", "serde", "serde_spanned", "toml_datetime", @@ -8173,7 +8438,7 @@ version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ - "indexmap 2.1.0", + "indexmap 2.2.5", "toml_datetime", "winnow", ] @@ -8238,7 +8503,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -8405,6 +8670,12 @@ dependencies = [ "tree_hash_derive", ] +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + [[package]] name = "uint" version = "0.9.5" @@ -8418,6 +8689,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unescape" version = "0.1.0" @@ -8447,9 +8724,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] @@ -8468,9 +8745,9 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "universal-hash" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" dependencies = [ "generic-array", "subtle", @@ -8507,16 +8784,16 @@ name = "unsigned-varint" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105" +dependencies = [ + "asynchronous-codec 0.6.2", + "bytes", +] [[package]] name = "unsigned-varint" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" -dependencies = [ - "asynchronous-codec", - "bytes", -] [[package]] name = "untrusted" @@ -8576,11 +8853,10 @@ dependencies = [ "eth2", "eth2_keystore", "ethereum_serde_utils", - "exit-future", "filesystem", "futures", "hex", - "hyper 1.1.0", + "hyper 1.2.0", "itertools", "lazy_static", "libsecp256k1", @@ -8690,10 +8966,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" [[package]] -name = "walkdir" -version = "2.4.0" +name = "wait-timeout" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -8711,7 +8996,8 @@ dependencies = [ [[package]] name = "warp" version = "0.3.6" -source = "git+https://github.com/seanmonstar/warp.git#7b07043cee0ca24e912155db4e8f6d9ab7c049ed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e92e22e03ff1230c03a1a8ee37d2f89cd489e2e541b7550d6afad96faed169" dependencies = [ "bytes", "futures-channel", @@ -8724,13 +9010,14 @@ dependencies = [ "mime_guess", "percent-encoding", "pin-project", - "rustls-pemfile 2.0.0", + "rustls-pemfile", "scoped-tls", "serde", "serde_json", "serde_urlencoded", "tokio", - "tokio-rustls 0.25.0", + "tokio-rustls", + "tokio-stream", "tokio-util 0.7.10", "tower-service", "tracing", @@ -8763,10 +9050,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "wasm-bindgen" -version = "0.2.90" +name = "wasite" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -8774,24 +9067,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" +checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" +checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97" dependencies = [ "cfg-if", "js-sys", @@ -8801,9 +9094,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" +checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -8811,28 +9104,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" +checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" +checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" [[package]] name = "wasm-streams" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" dependencies = [ "futures-util", "js-sys", @@ -8872,7 +9165,7 @@ dependencies = [ "eth2", "hex", "http_api", - "hyper 1.1.0", + "hyper 1.2.0", "log", "logging", "network", @@ -8893,9 +9186,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.67" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" +checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" dependencies = [ "js-sys", "wasm-bindgen", @@ -8906,10 +9199,10 @@ name = "web3signer_tests" version = "0.1.0" dependencies = [ "account_utils", + "async-channel", "environment", "eth2_keystore", "eth2_network_config", - "exit-future", "futures", "lazy_static", "parking_lot 0.12.1", @@ -8929,29 +9222,18 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.3" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" - -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.30", -] +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "whoami" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" +checksum = "0fec781d48b41f8163426ed18e8fc2864c12937df9ce54c88ede7bd47270893e" dependencies = [ - "wasm-bindgen", + "redox_syscall 0.4.1", + "wasite", "web-sys", ] @@ -8967,12 +9249,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" -[[package]] -name = "wildmatch" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f44b95f62d34113cf558c93511ac93027e03e9c29a60dd0fd70e6e025c7270a" - [[package]] name = "winapi" version = "0.3.9" @@ -9041,7 +9317,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.0", + "windows-targets 0.52.4", ] [[package]] @@ -9068,7 +9344,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.0", + "windows-targets 0.52.4", ] [[package]] @@ -9103,17 +9379,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", ] [[package]] @@ -9130,9 +9406,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" [[package]] name = "windows_aarch64_msvc" @@ -9148,9 +9424,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" [[package]] name = "windows_i686_gnu" @@ -9166,9 +9442,9 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" [[package]] name = "windows_i686_msvc" @@ -9184,9 +9460,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" [[package]] name = "windows_x86_64_gnu" @@ -9202,9 +9478,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" [[package]] name = "windows_x86_64_gnullvm" @@ -9220,9 +9496,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" [[package]] name = "windows_x86_64_msvc" @@ -9238,15 +9514,15 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "winnow" -version = "0.5.34" +version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] @@ -9272,7 +9548,7 @@ dependencies = [ "js-sys", "log", "pharos", - "rustc_version", + "rustc_version 0.4.0", "send_wrapper", "thiserror", "wasm-bindgen", @@ -9297,9 +9573,9 @@ dependencies = [ [[package]] name = "x25519-dalek" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ "curve25519-dalek", "rand_core", @@ -9366,8 +9642,7 @@ dependencies = [ [[package]] name = "yamux" version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1d0148b89300047e72994bee99ecdabd15a9166a7b70c8b8c37c314dcc9002" +source = "git+https://github.com/sigp/rust-yamux.git#12a23aa0e34b7807c0c5f87f06b3438f7d6c2ed0" dependencies = [ "futures", "instant", @@ -9405,7 +9680,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -9425,7 +9700,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -9434,7 +9709,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" dependencies = [ - "aes 0.8.3", + "aes 0.8.4", "byteorder", "bzip2", "constant_time_eq", diff --git a/Cargo.toml b/Cargo.toml index ca55d00d4b..24775b728d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "beacon_node/client", "beacon_node/eth1", "beacon_node/lighthouse_network", + "beacon_node/lighthouse_network/gossipsub", "beacon_node/execution_layer", "beacon_node/http_api", "beacon_node/http_metrics", @@ -94,23 +95,27 @@ resolver = "2" edition = "2021" [workspace.dependencies] +anyhow = "1" arbitrary = { version = "1", features = ["derive"] } +async-channel = "1.9.0" bincode = "1" bitvec = "1" byteorder = "1" bytes = "1" +c-kzg = "1" clap = "2" compare_fields_derive = { path = "common/compare_fields_derive" } criterion = "0.3" delay_map = "0.3" derivative = "2" dirs = "3" -discv5 = { git="https://github.com/sigp/discv5", rev="e30a2c31b7ac0c57876458b971164654dfa4513b", features = ["libp2p"] } +either = "1.9" +discv5 = { version = "0.4.1", 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_serde_utils = "0.5.2" ethereum_ssz = "0.5" ethereum_ssz_derive = "0.5" ethers-core = "1" @@ -160,6 +165,7 @@ tempfile = "3" tokio = { version = "1", features = ["rt-multi-thread", "sync", "signal"] } tokio-stream = { version = "0.1", features = ["sync"] } tokio-util = { version = "0.6", features = ["codec", "compat", "time"] } +tracing = "0.1.40" tracing-appender = "0.2" tracing-core = "0.1" tracing-log = "0.2" @@ -168,8 +174,7 @@ 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"] } +warp = { version = "0.3.6", default-features = false, features = ["tls"] } zeroize = { version = "1", features = ["zeroize_derive"] } zip = "0.6" @@ -197,6 +202,7 @@ execution_layer = { path = "beacon_node/execution_layer" } filesystem = { path = "common/filesystem" } fork_choice = { path = "consensus/fork_choice" } genesis = { path = "beacon_node/genesis" } +gossipsub = { path = "beacon_node/lighthouse_network/gossipsub/" } http_api = { path = "beacon_node/http_api" } int_to_bytes = { path = "consensus/int_to_bytes" } kzg = { path = "crypto/kzg" } @@ -228,6 +234,9 @@ validator_client = { path = "validator_client" } validator_dir = { path = "common/validator_dir" } warp_utils = { path = "common/warp_utils" } +[patch.crates-io] +yamux = { git = "https://github.com/sigp/rust-yamux.git" } + [profile.maxperf] inherits = "release" lto = "fat" diff --git a/Dockerfile b/Dockerfile index a8dadf2ad5..901c1b83d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.73.0-bullseye AS builder +FROM rust:1.75.0-bullseye AS builder RUN apt-get update && apt-get -y upgrade && apt-get install -y cmake libclang-dev COPY . lighthouse ARG FEATURES diff --git a/Makefile b/Makefile index 8392d00170..4b2d0f6c5d 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ PROFILE ?= release # List of all hard forks. This list is used to set env variables for several tests so that # they run for different forks. -FORKS=phase0 altair merge capella deneb +FORKS=phase0 altair merge capella deneb electra # Extra flags for Cargo CARGO_INSTALL_EXTRA_FLAGS?= @@ -143,7 +143,6 @@ run-ef-tests: rm -rf $(EF_TESTS)/.accessed_file_log.txt cargo test --release -p ef_tests --features "ef_tests,$(EF_TEST_FEATURES)" cargo test --release -p ef_tests --features "ef_tests,$(EF_TEST_FEATURES),fake_crypto" - cargo test --release -p ef_tests --features "ef_tests,$(EF_TEST_FEATURES),milagro" ./$(EF_TESTS)/check_all_files_accessed.py $(EF_TESTS)/.accessed_file_log.txt $(EF_TESTS)/consensus-spec-tests # Runs EF test vectors with nextest @@ -151,7 +150,6 @@ nextest-run-ef-tests: rm -rf $(EF_TESTS)/.accessed_file_log.txt cargo nextest run --release -p ef_tests --features "ef_tests,$(EF_TEST_FEATURES)" cargo nextest run --release -p ef_tests --features "ef_tests,$(EF_TEST_FEATURES),fake_crypto" - cargo nextest run --release -p ef_tests --features "ef_tests,$(EF_TEST_FEATURES),milagro" ./$(EF_TESTS)/check_all_files_accessed.py $(EF_TESTS)/.accessed_file_log.txt $(EF_TESTS)/consensus-spec-tests # Run the tests in the `beacon_chain` crate for all known forks. diff --git a/README.md b/README.md index ade3bc2aba..11a87b81fe 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ as the canonical staking deposit contract address. The [Lighthouse Book](https://lighthouse-book.sigmaprime.io) contains information for users and developers. -The Lighthouse team maintains a blog at [lighthouse-blog.sigmaprime.io][blog] which contains periodical +The Lighthouse team maintains a blog at [lighthouse-blog.sigmaprime.io][blog] which contains periodic progress updates, roadmap insights and interesting findings. ## Branches diff --git a/account_manager/README.md b/account_manager/README.md index 6762b937fc..cd303718ad 100644 --- a/account_manager/README.md +++ b/account_manager/README.md @@ -29,6 +29,6 @@ Simply run `./account_manager generate` to generate a new random private key, which will be automatically saved to the correct directory. If you prefer to use our "deterministic" keys for testing purposes, simply -run `./accounts_manager generate_deterministic -i `, where `index` is +run `./account_manager generate_deterministic -i `, where `index` is the validator index for the key. This will reliably produce the same key each time -and save it to the directory. \ No newline at end of file +and save it to the directory. diff --git a/account_manager/src/lib.rs b/account_manager/src/lib.rs index a032a85f71..ce7e8a42c2 100644 --- a/account_manager/src/lib.rs +++ b/account_manager/src/lib.rs @@ -22,7 +22,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { } /// Run the account manager, returning an error if the operation did not succeed. -pub fn run(matches: &ArgMatches<'_>, env: Environment) -> Result<(), String> { +pub fn run(matches: &ArgMatches<'_>, env: Environment) -> Result<(), String> { match matches.subcommand() { (wallet::CMD, Some(matches)) => wallet::cli_run(matches)?, (validator::CMD, Some(matches)) => validator::cli_run(matches, env)?, diff --git a/account_manager/src/validator/create.rs b/account_manager/src/validator/create.rs index da01121055..8da32531a8 100644 --- a/account_manager/src/validator/create.rs +++ b/account_manager/src/validator/create.rs @@ -112,9 +112,9 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { ) } -pub fn cli_run( +pub fn cli_run( matches: &ArgMatches, - env: Environment, + env: Environment, validator_dir: PathBuf, ) -> Result<(), String> { let spec = env.core_context().eth2_config.spec; diff --git a/account_manager/src/validator/mod.rs b/account_manager/src/validator/mod.rs index 4f1bde0795..af977dcf03 100644 --- a/account_manager/src/validator/mod.rs +++ b/account_manager/src/validator/mod.rs @@ -39,7 +39,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .subcommand(exit::cli_app()) } -pub fn cli_run(matches: &ArgMatches, env: Environment) -> Result<(), String> { +pub fn cli_run(matches: &ArgMatches, env: Environment) -> Result<(), String> { let validator_base_dir = if matches.value_of("datadir").is_some() { let path: PathBuf = clap_utils::parse_required(matches, "datadir")?; path.join(DEFAULT_VALIDATOR_DIR) @@ -49,7 +49,7 @@ pub fn cli_run(matches: &ArgMatches, env: Environment) -> Result< eprintln!("validator-dir path: {:?}", validator_base_dir); match matches.subcommand() { - (create::CMD, Some(matches)) => create::cli_run::(matches, env, validator_base_dir), + (create::CMD, Some(matches)) => create::cli_run::(matches, env, validator_base_dir), (modify::CMD, Some(matches)) => modify::cli_run(matches, validator_base_dir), (import::CMD, Some(matches)) => import::cli_run(matches, validator_base_dir), (list::CMD, Some(_)) => list::cli_run(validator_base_dir), diff --git a/account_manager/src/validator/slashing_protection.rs b/account_manager/src/validator/slashing_protection.rs index 0a98a452b8..ff2eeb9cbf 100644 --- a/account_manager/src/validator/slashing_protection.rs +++ b/account_manager/src/validator/slashing_protection.rs @@ -53,9 +53,9 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { ) } -pub fn cli_run( +pub fn cli_run( matches: &ArgMatches<'_>, - env: Environment, + env: Environment, validator_base_dir: PathBuf, ) -> Result<(), String> { let slashing_protection_db_path = validator_base_dir.join(SLASHING_PROTECTION_FILENAME); @@ -64,7 +64,7 @@ pub fn cli_run( .ok_or("Unable to get testnet configuration from the environment")?; let genesis_validators_root = eth2_network_config - .genesis_validators_root::()? + .genesis_validators_root::()? .ok_or_else(|| "Unable to get genesis state, has genesis occurred?".to_string())?; match matches.subcommand() { diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index 8428a30a3b..7cc6e2b6ae 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "beacon_node" -version = "4.6.0" +version = "5.1.3" authors = [ "Paul Hauner ", "Age Manning BeaconChain { pub fn compute_attestation_rewards( @@ -57,7 +56,8 @@ impl BeaconChain { BeaconState::Altair(_) | BeaconState::Merge(_) | BeaconState::Capella(_) - | BeaconState::Deneb(_) => self.compute_attestation_rewards_altair(state, validators), + | BeaconState::Deneb(_) + | BeaconState::Electra(_) => self.compute_attestation_rewards_altair(state, validators), } } @@ -133,11 +133,16 @@ impl BeaconChain { ) -> Result { let spec = &self.spec; + // Build required caches. + initialize_epoch_cache(&mut state, spec)?; + initialize_progressive_balances_cache(&mut state, spec)?; + state.build_exit_cache(spec)?; + state.build_committee_cache(RelativeEpoch::Previous, spec)?; + state.build_committee_cache(RelativeEpoch::Current, spec)?; + // Calculate ideal_rewards - let participation_cache = ParticipationCache::new(&state, spec)?; - process_justification_and_finalization(&state, &participation_cache)? - .apply_changes_to_state(&mut state); - process_inactivity_updates(&mut state, &participation_cache, spec)?; + process_justification_and_finalization(&state)?.apply_changes_to_state(&mut state); + process_inactivity_updates_slow(&mut state, spec)?; let previous_epoch = state.previous_epoch(); @@ -147,18 +152,14 @@ impl BeaconChain { let weight = get_flag_weight(flag_index) .map_err(|_| BeaconChainError::AttestationRewardsError)?; - let unslashed_participating_indices = participation_cache - .get_unslashed_participating_indices(flag_index, previous_epoch)?; - - let unslashed_participating_balance = - unslashed_participating_indices - .total_balance() - .map_err(|_| BeaconChainError::AttestationRewardsError)?; + let unslashed_participating_balance = state + .progressive_balances_cache() + .previous_epoch_flag_attesting_balance(flag_index)?; let unslashed_participating_increments = unslashed_participating_balance.safe_div(spec.effective_balance_increment)?; - let total_active_balance = participation_cache.current_epoch_total_active_balance(); + let total_active_balance = state.get_total_active_balance()?; let active_increments = total_active_balance.safe_div(spec.effective_balance_increment)?; @@ -194,30 +195,49 @@ impl BeaconChain { let mut total_rewards: Vec = Vec::new(); let validators = if validators.is_empty() { - participation_cache.eligible_validator_indices().to_vec() + Self::all_eligible_validator_indices(&state, previous_epoch)? } else { Self::validators_ids_to_indices(&mut state, validators)? }; - for validator_index in &validators { - let eligible = state.is_eligible_validator(previous_epoch, *validator_index)?; + for &validator_index in &validators { + // Return 0s for unknown/inactive validator indices. + let Ok(validator) = state.get_validator(validator_index) else { + debug!( + self.log, + "No rewards for inactive/unknown validator"; + "index" => validator_index, + "epoch" => previous_epoch + ); + total_rewards.push(TotalAttestationRewards { + validator_index: validator_index as u64, + head: 0, + target: 0, + source: 0, + inclusion_delay: None, + inactivity: 0, + }); + continue; + }; + let previous_epoch_participation_flags = state + .previous_epoch_participation()? + .get(validator_index) + .ok_or(BeaconChainError::AttestationRewardsError)?; + let eligible = state.is_eligible_validator(previous_epoch, validator)?; let mut head_reward = 0i64; let mut target_reward = 0i64; let mut source_reward = 0i64; let mut inactivity_penalty = 0i64; if eligible { - let effective_balance = state.get_effective_balance(*validator_index)?; + let effective_balance = validator.effective_balance; for flag_index in 0..PARTICIPATION_FLAG_WEIGHTS.len() { let (ideal_reward, penalty) = ideal_rewards_hashmap .get(&(flag_index, effective_balance)) .ok_or(BeaconChainError::AttestationRewardsError)?; - let voted_correctly = participation_cache - .get_unslashed_participating_indices(flag_index, previous_epoch) - .map_err(|_| BeaconChainError::AttestationRewardsError)? - .contains(*validator_index) - .map_err(|_| BeaconChainError::AttestationRewardsError)?; + let voted_correctly = !validator.slashed + && previous_epoch_participation_flags.has_flag(flag_index)?; if voted_correctly { if flag_index == TIMELY_HEAD_FLAG_INDEX { head_reward += *ideal_reward as i64; @@ -232,10 +252,10 @@ impl BeaconChain { target_reward = *penalty; let penalty_numerator = effective_balance - .safe_mul(state.get_inactivity_score(*validator_index)?)?; - let penalty_denominator = spec - .inactivity_score_bias - .safe_mul(spec.inactivity_penalty_quotient_for_state(&state))?; + .safe_mul(state.get_inactivity_score(validator_index)?)?; + let penalty_denominator = spec.inactivity_score_bias.safe_mul( + spec.inactivity_penalty_quotient_for_fork(state.fork_name_unchecked()), + )?; inactivity_penalty = -(penalty_numerator.safe_div(penalty_denominator)? as i64); } else if flag_index == TIMELY_SOURCE_FLAG_INDEX { @@ -244,7 +264,7 @@ impl BeaconChain { } } total_rewards.push(TotalAttestationRewards { - validator_index: *validator_index as u64, + validator_index: validator_index as u64, head: head_reward, target: target_reward, source: source_reward, @@ -301,6 +321,24 @@ impl BeaconChain { Ok(max_steps) } + fn all_eligible_validator_indices( + state: &BeaconState, + previous_epoch: Epoch, + ) -> Result, BeaconChainError> { + state + .validators() + .iter() + .enumerate() + .filter_map(|(i, validator)| { + state + .is_eligible_validator(previous_epoch, validator) + .map(|eligible| eligible.then_some(i)) + .map_err(BeaconChainError::BeaconStateError) + .transpose() + }) + .collect() + } + fn validators_ids_to_indices( state: &mut BeaconState, validators: Vec, @@ -339,15 +377,12 @@ impl BeaconChain { }; let mut ideal_attestation_rewards_list = Vec::new(); - + let sqrt_total_active_balance = SqrtTotalActiveBalance::new(total_balances.current_epoch()); 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, - )?; + let base_reward = + base::get_base_reward(effective_balance, sqrt_total_active_balance, spec)?; // compute ideal head rewards let head = get_attestation_component_delta( diff --git a/beacon_node/beacon_chain/src/attestation_verification.rs b/beacon_node/beacon_chain/src/attestation_verification.rs index d7a8bca4d0..f3bde8678e 100644 --- a/beacon_node/beacon_chain/src/attestation_verification.rs +++ b/beacon_node/beacon_chain/src/attestation_verification.rs @@ -539,8 +539,8 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { Err(e) => return Err(SignatureNotChecked(&signed_aggregate.message.aggregate, e)), }; - let indexed_attestation = - match map_attestation_committee(chain, attestation, |(committee, _)| { + let get_indexed_attestation_with_committee = + |(committee, _): (BeaconCommittee, CommitteesPerSlot)| { // Note: this clones the signature which is known to be a relatively slow operation. // // Future optimizations should remove this clone. @@ -561,11 +561,17 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { get_indexed_attestation(committee.committee, attestation) .map_err(|e| BeaconChainError::from(e).into()) - }) { - Ok(indexed_attestation) => indexed_attestation, - Err(e) => return Err(SignatureNotChecked(&signed_aggregate.message.aggregate, e)), }; + let indexed_attestation = match map_attestation_committee( + chain, + attestation, + get_indexed_attestation_with_committee, + ) { + Ok(indexed_attestation) => indexed_attestation, + Err(e) => return Err(SignatureNotChecked(&signed_aggregate.message.aggregate, e)), + }; + Ok(IndexedAggregatedAttestation { signed_aggregate, indexed_attestation, @@ -1059,7 +1065,7 @@ pub fn verify_propagation_slot_range( let earliest_permissible_slot = match current_fork { ForkName::Base | ForkName::Altair | ForkName::Merge | ForkName::Capella => one_epoch_prior, // EIP-7045 - ForkName::Deneb => one_epoch_prior + ForkName::Deneb | ForkName::Electra => one_epoch_prior .epoch(E::slots_per_epoch()) .start_slot(E::slots_per_epoch()), }; @@ -1115,13 +1121,13 @@ pub fn verify_attestation_signature( /// Verifies that the `attestation.data.target.root` is indeed the target root of the block at /// `attestation.data.beacon_block_root`. -pub fn verify_attestation_target_root( +pub fn verify_attestation_target_root( head_block: &ProtoBlock, - attestation: &Attestation, + attestation: &Attestation, ) -> Result<(), Error> { // Check the attestation target root. - let head_block_epoch = head_block.slot.epoch(T::slots_per_epoch()); - let attestation_epoch = attestation.data.slot.epoch(T::slots_per_epoch()); + let head_block_epoch = head_block.slot.epoch(E::slots_per_epoch()); + let attestation_epoch = attestation.data.slot.epoch(E::slots_per_epoch()); if head_block_epoch > attestation_epoch { // The epoch references an invalid head block from a future epoch. // diff --git a/beacon_node/beacon_chain/src/attester_cache.rs b/beacon_node/beacon_chain/src/attester_cache.rs index 24963a125d..2e07cd32ed 100644 --- a/beacon_node/beacon_chain/src/attester_cache.rs +++ b/beacon_node/beacon_chain/src/attester_cache.rs @@ -84,7 +84,7 @@ pub struct CommitteeLengths { impl CommitteeLengths { /// Instantiate `Self` using `state.current_epoch()`. - pub fn new(state: &BeaconState, spec: &ChainSpec) -> Result { + pub fn new(state: &BeaconState, spec: &ChainSpec) -> Result { let active_validator_indices_len = if let Ok(committee_cache) = state.committee_cache(RelativeEpoch::Current) { @@ -102,21 +102,21 @@ impl CommitteeLengths { } /// Get the count of committees per each slot of `self.epoch`. - pub fn get_committee_count_per_slot( + pub fn get_committee_count_per_slot( &self, spec: &ChainSpec, ) -> Result { - T::get_committee_count_per_slot(self.active_validator_indices_len, spec).map_err(Into::into) + E::get_committee_count_per_slot(self.active_validator_indices_len, spec).map_err(Into::into) } /// Get the length of the committee at the given `slot` and `committee_index`. - pub fn get_committee_length( + pub fn get_committee_length( &self, slot: Slot, committee_index: CommitteeIndex, spec: &ChainSpec, ) -> Result { - let slots_per_epoch = T::slots_per_epoch(); + let slots_per_epoch = E::slots_per_epoch(); let request_epoch = slot.epoch(slots_per_epoch); // Sanity check. @@ -128,7 +128,7 @@ impl CommitteeLengths { } let slots_per_epoch = slots_per_epoch as usize; - let committees_per_slot = self.get_committee_count_per_slot::(spec)?; + let committees_per_slot = self.get_committee_count_per_slot::(spec)?; let index_in_epoch = compute_committee_index_in_epoch( slot, slots_per_epoch, @@ -162,7 +162,7 @@ pub struct AttesterCacheValue { impl AttesterCacheValue { /// Instantiate `Self` using `state.current_epoch()`. - pub fn new(state: &BeaconState, spec: &ChainSpec) -> Result { + pub fn new(state: &BeaconState, spec: &ChainSpec) -> Result { let current_justified_checkpoint = state.current_justified_checkpoint(); let committee_lengths = CommitteeLengths::new(state, spec)?; Ok(Self { @@ -172,14 +172,14 @@ impl AttesterCacheValue { } /// Get the justified checkpoint and committee length for some `slot` and `committee_index`. - fn get( + fn get( &self, slot: Slot, committee_index: CommitteeIndex, spec: &ChainSpec, ) -> Result<(JustifiedCheckpoint, CommitteeLength), Error> { self.committee_lengths - .get_committee_length::(slot, committee_index, spec) + .get_committee_length::(slot, committee_index, spec) .map(|committee_length| (self.current_justified_checkpoint, committee_length)) } } @@ -216,12 +216,12 @@ impl AttesterCacheKey { /// ## Errors /// /// May error if `epoch` is out of the range of `state.block_roots`. - pub fn new( + pub fn new( epoch: Epoch, - state: &BeaconState, + state: &BeaconState, latest_block_root: Hash256, ) -> Result { - let slots_per_epoch = T::slots_per_epoch(); + let slots_per_epoch = E::slots_per_epoch(); let decision_slot = epoch.start_slot(slots_per_epoch).saturating_sub(1_u64); let decision_root = if decision_slot.epoch(slots_per_epoch) == epoch { @@ -255,7 +255,7 @@ pub struct AttesterCache { impl AttesterCache { /// Get the justified checkpoint and committee length for the `slot` and `committee_index` in /// the state identified by the cache `key`. - pub fn get( + pub fn get( &self, key: &AttesterCacheKey, slot: Slot, @@ -265,14 +265,14 @@ impl AttesterCache { self.cache .read() .get(key) - .map(|cache_item| cache_item.get::(slot, committee_index, spec)) + .map(|cache_item| cache_item.get::(slot, committee_index, spec)) .transpose() } /// Cache the `state.current_epoch()` values if they are not already present in the state. - pub fn maybe_cache_state( + pub fn maybe_cache_state( &self, - state: &BeaconState, + state: &BeaconState, latest_block_root: Hash256, spec: &ChainSpec, ) -> Result<(), Error> { diff --git a/beacon_node/beacon_chain/src/beacon_block_reward.rs b/beacon_node/beacon_chain/src/beacon_block_reward.rs index d05f7cb4ff..5b70215d22 100644 --- a/beacon_node/beacon_chain/src/beacon_block_reward.rs +++ b/beacon_node/beacon_chain/src/beacon_block_reward.rs @@ -4,9 +4,8 @@ use operation_pool::RewardCache; use safe_arith::SafeArith; use slog::error; use state_processing::{ - common::{ - altair, get_attestation_participation_flag_indices, get_attesting_indices_from_state, - }, + common::{get_attestation_participation_flag_indices, get_attesting_indices_from_state}, + epoch_cache::initialize_epoch_cache, per_block_processing::{ altair::sync_committee::compute_sync_aggregate_rewards, get_slashable_indices, }, @@ -32,6 +31,7 @@ impl BeaconChain { state.build_committee_cache(RelativeEpoch::Previous, &self.spec)?; state.build_committee_cache(RelativeEpoch::Current, &self.spec)?; + initialize_epoch_cache(state, &self.spec)?; self.compute_beacon_block_reward_with_cache(block, block_root, state) } @@ -191,10 +191,6 @@ impl BeaconChain { block: BeaconBlockRef<'_, T::EthSpec, Payload>, state: &BeaconState, ) -> Result { - let total_active_balance = state.get_total_active_balance()?; - let base_reward_per_increment = - altair::BaseRewardPerIncrement::new(total_active_balance, &self.spec)?; - let mut total_proposer_reward = 0; let proposer_reward_denominator = WEIGHT_DENOMINATOR @@ -235,15 +231,8 @@ impl BeaconChain { && !validator_participation.has_flag(flag_index)? { validator_participation.add_flag(flag_index)?; - proposer_reward_numerator.safe_add_assign( - altair::get_base_reward( - state, - index, - base_reward_per_increment, - &self.spec, - )? - .safe_mul(weight)?, - )?; + proposer_reward_numerator + .safe_add_assign(state.get_base_reward(index)?.safe_mul(weight)?)?; } } } diff --git a/beacon_node/beacon_chain/src/beacon_block_streamer.rs b/beacon_node/beacon_chain/src/beacon_block_streamer.rs index 9312d4511d..bbd5bfcac9 100644 --- a/beacon_node/beacon_chain/src/beacon_block_streamer.rs +++ b/beacon_node/beacon_chain/src/beacon_block_streamer.rs @@ -1,4 +1,4 @@ -use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; +use crate::{metrics, BeaconChain, BeaconChainError, BeaconChainTypes}; use execution_layer::{ExecutionLayer, ExecutionPayloadBodyV1}; use slog::{crit, debug, Logger}; use std::collections::HashMap; @@ -15,11 +15,12 @@ use types::{ SignedBlindedBeaconBlock, Slot, }; use types::{ - ExecutionPayload, ExecutionPayloadCapella, ExecutionPayloadHeader, ExecutionPayloadMerge, + ExecutionPayload, ExecutionPayloadCapella, ExecutionPayloadElectra, ExecutionPayloadHeader, + ExecutionPayloadMerge, }; #[derive(PartialEq)] -pub enum CheckEarlyAttesterCache { +pub enum CheckCaches { Yes, No, } @@ -98,6 +99,7 @@ fn reconstruct_default_header_block( ForkName::Merge => ExecutionPayloadMerge::default().into(), ForkName::Capella => ExecutionPayloadCapella::default().into(), ForkName::Deneb => ExecutionPayloadDeneb::default().into(), + ForkName::Electra => ExecutionPayloadElectra::default().into(), ForkName::Base | ForkName::Altair => { return Err(Error::PayloadReconstruction(format!( "Block with fork variant {} has execution payload", @@ -385,14 +387,14 @@ impl EngineRequest { pub struct BeaconBlockStreamer { execution_layer: ExecutionLayer, - check_early_attester_cache: CheckEarlyAttesterCache, + check_caches: CheckCaches, beacon_chain: Arc>, } impl BeaconBlockStreamer { pub fn new( beacon_chain: &Arc>, - check_early_attester_cache: CheckEarlyAttesterCache, + check_caches: CheckCaches, ) -> Result { let execution_layer = beacon_chain .execution_layer @@ -402,17 +404,22 @@ impl BeaconBlockStreamer { Ok(Self { execution_layer, - check_early_attester_cache, + check_caches, beacon_chain: beacon_chain.clone(), }) } - fn check_early_attester_cache( - &self, - root: Hash256, - ) -> Option>> { - if self.check_early_attester_cache == CheckEarlyAttesterCache::Yes { - self.beacon_chain.early_attester_cache.get_block(root) + fn check_caches(&self, root: Hash256) -> Option>> { + if self.check_caches == CheckCaches::Yes { + self.beacon_chain + .reqresp_pre_import_cache + .read() + .get(&root) + .map(|block| { + metrics::inc_counter(&metrics::BEACON_REQRESP_PRE_IMPORT_CACHE_HITS); + block.clone() + }) + .or(self.beacon_chain.early_attester_cache.get_block(root)) } else { None } @@ -422,10 +429,7 @@ impl BeaconBlockStreamer { let mut db_blocks = Vec::new(); for root in block_roots { - if let Some(cached_block) = self - .check_early_attester_cache(root) - .map(LoadedBeaconBlock::Full) - { + if let Some(cached_block) = self.check_caches(root).map(LoadedBeaconBlock::Full) { db_blocks.push((root, Ok(Some(cached_block)))); continue; } @@ -554,7 +558,7 @@ impl BeaconBlockStreamer { "Using slower fallback method of eth_getBlockByHash()" ); for root in block_roots { - let cached_block = self.check_early_attester_cache(root); + let cached_block = self.check_caches(root); let block_result = if cached_block.is_some() { Ok(cached_block) } else { @@ -682,7 +686,7 @@ impl From for BeaconChainError { #[cfg(test)] mod tests { - use crate::beacon_block_streamer::{BeaconBlockStreamer, CheckEarlyAttesterCache}; + use crate::beacon_block_streamer::{BeaconBlockStreamer, CheckCaches}; use crate::test_utils::{test_spec, BeaconChainHarness, EphemeralHarnessType}; use execution_layer::test_utils::{Block, DEFAULT_ENGINE_CAPABILITIES}; use execution_layer::EngineCapabilities; @@ -715,12 +719,13 @@ mod tests { } #[tokio::test] - async fn check_all_blocks_from_altair_to_deneb() { + async fn check_all_blocks_from_altair_to_electra() { let slots_per_epoch = MinimalEthSpec::slots_per_epoch() as usize; - let num_epochs = 8; + let num_epochs = 10; let bellatrix_fork_epoch = 2usize; let capella_fork_epoch = 4usize; let deneb_fork_epoch = 6usize; + let electra_fork_epoch = 8usize; let num_blocks_produced = num_epochs * slots_per_epoch; let mut spec = test_spec::(); @@ -728,6 +733,7 @@ mod tests { spec.bellatrix_fork_epoch = Some(Epoch::new(bellatrix_fork_epoch as u64)); spec.capella_fork_epoch = Some(Epoch::new(capella_fork_epoch as u64)); spec.deneb_fork_epoch = Some(Epoch::new(deneb_fork_epoch as u64)); + spec.electra_fork_epoch = Some(Epoch::new(electra_fork_epoch as u64)); let harness = get_harness(VALIDATOR_COUNT, spec.clone()); // go to bellatrix fork @@ -804,7 +810,7 @@ mod tests { let start = epoch * slots_per_epoch; let mut epoch_roots = vec![Hash256::zero(); slots_per_epoch]; epoch_roots[..].clone_from_slice(&block_roots[start..(start + slots_per_epoch)]); - let streamer = BeaconBlockStreamer::new(&harness.chain, CheckEarlyAttesterCache::No) + let streamer = BeaconBlockStreamer::new(&harness.chain, CheckCaches::No) .expect("should create streamer"); let (block_tx, mut block_rx) = mpsc::unbounded_channel(); streamer.stream(epoch_roots.clone(), block_tx).await; @@ -836,12 +842,13 @@ mod tests { } #[tokio::test] - async fn check_fallback_altair_to_deneb() { + async fn check_fallback_altair_to_electra() { let slots_per_epoch = MinimalEthSpec::slots_per_epoch() as usize; - let num_epochs = 8; + let num_epochs = 10; let bellatrix_fork_epoch = 2usize; let capella_fork_epoch = 4usize; let deneb_fork_epoch = 6usize; + let electra_fork_epoch = 8usize; let num_blocks_produced = num_epochs * slots_per_epoch; let mut spec = test_spec::(); @@ -849,6 +856,7 @@ mod tests { spec.bellatrix_fork_epoch = Some(Epoch::new(bellatrix_fork_epoch as u64)); spec.capella_fork_epoch = Some(Epoch::new(capella_fork_epoch as u64)); spec.deneb_fork_epoch = Some(Epoch::new(deneb_fork_epoch as u64)); + spec.electra_fork_epoch = Some(Epoch::new(electra_fork_epoch as u64)); let harness = get_harness(VALIDATOR_COUNT, spec); @@ -945,7 +953,7 @@ mod tests { let start = epoch * slots_per_epoch; let mut epoch_roots = vec![Hash256::zero(); slots_per_epoch]; epoch_roots[..].clone_from_slice(&block_roots[start..(start + slots_per_epoch)]); - let streamer = BeaconBlockStreamer::new(&harness.chain, CheckEarlyAttesterCache::No) + let streamer = BeaconBlockStreamer::new(&harness.chain, CheckCaches::No) .expect("should create streamer"); let (block_tx, mut block_rx) = mpsc::unbounded_channel(); streamer.stream(epoch_roots.clone(), block_tx).await; diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index b05ba01ee7..7c497e7458 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -4,7 +4,7 @@ use crate::attestation_verification::{ VerifiedUnaggregatedAttestation, }; use crate::attester_cache::{AttesterCache, AttesterCacheKey}; -use crate::beacon_block_streamer::{BeaconBlockStreamer, CheckEarlyAttesterCache}; +use crate::beacon_block_streamer::{BeaconBlockStreamer, CheckCaches}; use crate::beacon_proposer_cache::compute_proposer_duties_from_head; use crate::beacon_proposer_cache::BeaconProposerCache; use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob}; @@ -72,7 +72,7 @@ use crate::{ kzg_utils, metrics, AvailabilityPendingExecutedBlock, BeaconChainError, BeaconForkChoiceStore, BeaconSnapshot, CachedHead, }; -use eth2::types::{EventKind, SseBlobSidecar, SseBlock, SseExtendedPayloadAttributes, SyncDuty}; +use eth2::types::{EventKind, SseBlobSidecar, SseBlock, SseExtendedPayloadAttributes}; use execution_layer::{ BlockProposalContents, BlockProposalContentsType, BuilderParams, ChainHealth, ExecutionLayer, FailedCondition, PayloadAttributes, PayloadStatus, @@ -95,6 +95,7 @@ use slot_clock::SlotClock; use ssz::Encode; use state_processing::{ common::get_attesting_indices_from_state, + epoch_cache::initialize_epoch_cache, per_block_processing, per_block_processing::{ errors::AttestationValidationError, get_expected_withdrawals, @@ -120,8 +121,7 @@ use store::{ use task_executor::{ShutdownReason, TaskExecutor}; use tokio_stream::Stream; use tree_hash::TreeHash; -use types::beacon_state::CloneConfig; -use types::blob_sidecar::{BlobSidecarList, FixedBlobSidecarList}; +use types::blob_sidecar::FixedBlobSidecarList; use types::payload::BlockProductionVersion; use types::*; @@ -216,14 +216,14 @@ impl TryInto for AvailabilityProcessingStatus { } /// The result of a chain segment processing. -pub enum ChainSegmentResult { +pub enum ChainSegmentResult { /// Processing this chain segment finished successfully. Successful { imported_blocks: usize }, /// There was an error processing this chain segment. Before the error, some blocks could /// have been imported. Failed { imported_blocks: usize, - error: BlockError, + error: BlockError, }, } @@ -359,6 +359,9 @@ pub type BeaconStore = Arc< >, >; +/// Cache gossip verified blocks to serve over ReqResp before they are imported +type ReqRespPreImportCache = HashMap>>; + /// Represents the "Beacon Chain" component of Ethereum 2.0. Allows import of blocks and block /// operations and chooses a canonical head. pub struct BeaconChain { @@ -414,14 +417,14 @@ pub struct BeaconChain { /// Maintains a record of slashable message seen over the gossip network or RPC. pub observed_slashable: RwLock>, /// Maintains a record of which validators have submitted voluntary exits. - pub(crate) observed_voluntary_exits: Mutex>, + pub observed_voluntary_exits: Mutex>, /// Maintains a record of which validators we've seen proposer slashings for. - pub(crate) observed_proposer_slashings: Mutex>, + pub observed_proposer_slashings: Mutex>, /// Maintains a record of which validators we've seen attester slashings for. - pub(crate) observed_attester_slashings: + pub observed_attester_slashings: Mutex, T::EthSpec>>, /// Maintains a record of which validators we've seen BLS to execution changes for. - pub(crate) observed_bls_to_execution_changes: + pub observed_bls_to_execution_changes: Mutex>, /// Provides information from the Ethereum 1 (PoW) chain. pub eth1_chain: Option>, @@ -461,6 +464,8 @@ pub struct BeaconChain { pub(crate) attester_cache: Arc, /// A cache used when producing attestations whilst the head block is still being imported. pub early_attester_cache: EarlyAttesterCache, + /// Cache gossip verified blocks to serve over ReqResp before they are imported + pub reqresp_pre_import_cache: Arc>>, /// A cache used to keep track of various block timings. pub block_times_cache: Arc>, /// A cache used to track pre-finalization block roots for quick rejection. @@ -494,9 +499,9 @@ pub struct BeaconChain { pub block_production_state: Arc)>>>, } -pub enum BeaconBlockResponseWrapper { - Full(BeaconBlockResponse>), - Blinded(BeaconBlockResponse>), +pub enum BeaconBlockResponseWrapper { + Full(BeaconBlockResponse>), + Blinded(BeaconBlockResponse>), } impl BeaconBlockResponseWrapper { @@ -531,13 +536,13 @@ impl BeaconBlockResponseWrapper { } /// The components produced when the local beacon node creates a new block to extend the chain -pub struct BeaconBlockResponse> { +pub struct BeaconBlockResponse> { /// The newly produced beacon block - pub block: BeaconBlock, + pub block: BeaconBlock, /// The post-state after applying the new block - pub state: BeaconState, + pub state: BeaconState, /// The Blobs / Proofs associated with the new block - pub blob_items: Option<(KzgProofs, BlobsList)>, + pub blob_items: Option<(KzgProofs, BlobsList)>, /// The execution layer reward for the block pub execution_payload_value: Uint256, /// The consensus layer reward to the proposer @@ -1131,7 +1136,7 @@ impl BeaconChain { /// ## Errors /// /// May return a database error. - pub fn get_blocks_checking_early_attester_cache( + pub fn get_blocks_checking_caches( self: &Arc, block_roots: Vec, executor: &TaskExecutor, @@ -1144,10 +1149,8 @@ impl BeaconChain { >, Error, > { - Ok( - BeaconBlockStreamer::::new(self, CheckEarlyAttesterCache::Yes)? - .launch_stream(block_roots, executor), - ) + Ok(BeaconBlockStreamer::::new(self, CheckCaches::Yes)? + .launch_stream(block_roots, executor)) } pub fn get_blocks( @@ -1163,10 +1166,8 @@ impl BeaconChain { >, Error, > { - Ok( - BeaconBlockStreamer::::new(self, CheckEarlyAttesterCache::No)? - .launch_stream(block_roots, executor), - ) + Ok(BeaconBlockStreamer::::new(self, CheckCaches::No)? + .launch_stream(block_roots, executor)) } pub fn get_blobs_checking_early_attester_cache( @@ -1352,11 +1353,12 @@ impl BeaconChain { (parent_root, slot, sync_aggregate): LightClientProducerEvent, ) -> Result<(), Error> { self.light_client_server_cache.recompute_and_cache_updates( - &self.log, self.store.clone(), &parent_root, slot, &sync_aggregate, + &self.log, + &self.spec, ) } @@ -2571,7 +2573,7 @@ impl BeaconChain { &self, epoch: Epoch, validator_indices: &[u64], - ) -> Result>, Error> { + ) -> Result, BeaconStateError>>, Error> { self.with_head(move |head| { head.beacon_state .get_sync_committee_duties(epoch, validator_indices, &self.spec) @@ -2656,7 +2658,7 @@ impl BeaconChain { // If the block is relevant, add it to the filtered chain segment. Ok(_) => filtered_chain_segment.push((block_root, block)), // If the block is already known, simply ignore this block. - Err(BlockError::BlockIsAlreadyKnown) => continue, + Err(BlockError::BlockIsAlreadyKnown(_)) => continue, // If the block is the genesis block, simply ignore this block. Err(BlockError::GenesisBlock) => continue, // If the block is is for a finalized slot, simply ignore this block. @@ -2800,6 +2802,12 @@ impl BeaconChain { } } } + Err(BlockError::BlockIsAlreadyKnown(block_root)) => { + debug!(self.log, + "Ignoring already known blocks while processing chain segment"; + "block_root" => ?block_root); + continue; + } Err(error) => { return ChainSegmentResult::Failed { imported_blocks, @@ -2884,7 +2892,7 @@ impl BeaconChain { .fork_choice_read_lock() .contains_block(&block_root) { - return Err(BlockError::BlockIsAlreadyKnown); + return Err(BlockError::BlockIsAlreadyKnown(blob.block_root())); } if let Some(event_handler) = self.event_handler.as_ref() { @@ -2895,8 +2903,6 @@ impl BeaconChain { } } - self.data_availability_checker - .notify_gossip_blob(blob.slot(), block_root, &blob); let r = self.check_gossip_blob_availability_and_import(blob).await; self.remove_notified(&block_root, r) } @@ -2916,7 +2922,7 @@ impl BeaconChain { .fork_choice_read_lock() .contains_block(&block_root) { - return Err(BlockError::BlockIsAlreadyKnown); + return Err(BlockError::BlockIsAlreadyKnown(block_root)); } if let Some(event_handler) = self.event_handler.as_ref() { @@ -2929,8 +2935,6 @@ impl BeaconChain { } } - self.data_availability_checker - .notify_rpc_blobs(slot, block_root, &blobs); let r = self .check_rpc_blob_availability_and_import(slot, block_root, blobs) .await; @@ -2947,7 +2951,7 @@ impl BeaconChain { let has_missing_components = matches!(r, Ok(AvailabilityProcessingStatus::MissingComponents(_, _))); if !has_missing_components { - self.data_availability_checker.remove_notified(block_root); + self.reqresp_pre_import_cache.write().remove(block_root); } r } @@ -2960,18 +2964,10 @@ impl BeaconChain { unverified_block: B, notify_execution_layer: NotifyExecutionLayer, ) -> Result> { - if let Ok(commitments) = unverified_block - .block() - .message() - .body() - .blob_kzg_commitments() - { - self.data_availability_checker.notify_block_commitments( - unverified_block.block().slot(), - block_root, - commitments.clone(), - ); - }; + self.reqresp_pre_import_cache + .write() + .insert(block_root, unverified_block.block_cloned()); + let r = self .process_block(block_root, unverified_block, notify_execution_layer, || { Ok(()) @@ -3046,7 +3042,7 @@ impl BeaconChain { match import_block.await { // The block was successfully verified and imported. Yay. Ok(status @ AvailabilityProcessingStatus::Imported(block_root)) => { - trace!( + debug!( self.log, "Beacon block imported"; "block_root" => ?block_root, @@ -3059,7 +3055,7 @@ impl BeaconChain { Ok(status) } Ok(status @ AvailabilityProcessingStatus::MissingComponents(slot, block_root)) => { - trace!( + debug!( self.log, "Beacon block awaiting blobs"; "block_root" => ?block_root, @@ -3368,9 +3364,7 @@ impl BeaconChain { block_delay, &state, payload_verification_status, - self.config.progressive_balances_mode, &self.spec, - &self.log, ) .map_err(|e| BlockError::BeaconChainError(e.into()))?; } @@ -4180,21 +4174,14 @@ impl BeaconChain { (re_org_state.pre_state, re_org_state.state_root) } // Normal case: proposing a block atop the current head using the cache. - else if let Some((_, cached_state)) = self - .block_production_state - .lock() - .take() - .filter(|(cached_block_root, _)| *cached_block_root == head_block_root) + else if let Some((_, cached_state)) = + self.get_state_from_block_production_cache(head_block_root) { (cached_state.pre_state, cached_state.state_root) } // Fall back to a direct read of the snapshot cache. - else if let Some(pre_state) = self - .snapshot_cache - .try_read_for(BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT) - .and_then(|snapshot_cache| { - snapshot_cache.get_state_for_block_production(head_block_root) - }) + else if let Some(pre_state) = + self.get_state_from_snapshot_cache_for_block_production(head_block_root) { warn!( self.log, @@ -4235,6 +4222,40 @@ impl BeaconChain { Ok((state, state_root_opt)) } + /// Get the state cached for block production *if* it matches `head_block_root`. + /// + /// This will clear the cache regardless of whether the block root matches, so only call this if + /// you think the `head_block_root` is likely to match! + fn get_state_from_block_production_cache( + &self, + head_block_root: Hash256, + ) -> Option<(Hash256, BlockProductionPreState)> { + // Take care to drop the lock as quickly as possible. + let mut lock = self.block_production_state.lock(); + let result = lock + .take() + .filter(|(cached_block_root, _)| *cached_block_root == head_block_root); + drop(lock); + result + } + + /// Get a state for block production from the snapshot cache. + fn get_state_from_snapshot_cache_for_block_production( + &self, + head_block_root: Hash256, + ) -> Option> { + if let Some(lock) = self + .snapshot_cache + .try_read_for(BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT) + { + let result = lock.get_state_for_block_production(head_block_root); + drop(lock); + result + } else { + None + } + } + /// Fetch the beacon state to use for producing a block if a 1-slot proposer re-org is viable. /// /// This function will return `None` if proposer re-orgs are disabled. @@ -4244,7 +4265,8 @@ impl BeaconChain { head_slot: Slot, canonical_head: Hash256, ) -> Option> { - let re_org_threshold = self.config.re_org_threshold?; + let re_org_head_threshold = self.config.re_org_head_threshold?; + let re_org_parent_threshold = self.config.re_org_parent_threshold?; if self.spec.proposer_score_boost.is_none() { warn!( @@ -4301,7 +4323,8 @@ impl BeaconChain { .get_proposer_head( slot, canonical_head, - re_org_threshold, + re_org_head_threshold, + re_org_parent_threshold, &self.config.re_org_disallowed_offsets, self.config.re_org_max_epochs_since_finalization, ) @@ -4327,12 +4350,8 @@ impl BeaconChain { // Only attempt a re-org if we hit the block production cache or snapshot cache. let pre_state = self - .block_production_state - .lock() - .take() - .and_then(|(cached_block_root, state)| { - (cached_block_root == re_org_parent_block).then_some(state) - }) + .get_state_from_block_production_cache(re_org_parent_block) + .map(|(_, state)| state) .or_else(|| { warn!( self.log, @@ -4341,11 +4360,7 @@ impl BeaconChain { "slot" => slot, "block_root" => ?re_org_parent_block ); - self.snapshot_cache - .try_read_for(BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT) - .and_then(|snapshot_cache| { - snapshot_cache.get_state_for_block_production(re_org_parent_block) - }) + self.get_state_from_snapshot_cache_for_block_production(re_org_parent_block) }) .or_else(|| { debug!( @@ -4363,7 +4378,7 @@ impl BeaconChain { "weak_head" => ?canonical_head, "parent" => ?re_org_parent_block, "head_weight" => proposer_head.head_node.weight, - "threshold_weight" => proposer_head.re_org_weight_threshold + "threshold_weight" => proposer_head.re_org_head_weight_threshold ); Some(pre_state) @@ -4583,9 +4598,14 @@ impl BeaconChain { let _timer = metrics::start_timer(&metrics::FORK_CHOICE_OVERRIDE_FCU_TIMES); // Never override if proposer re-orgs are disabled. - let re_org_threshold = self + let re_org_head_threshold = self .config - .re_org_threshold + .re_org_head_threshold + .ok_or(DoNotReOrg::ReOrgsDisabled)?; + + let re_org_parent_threshold = self + .config + .re_org_parent_threshold .ok_or(DoNotReOrg::ReOrgsDisabled)?; let head_block_root = canonical_forkchoice_params.head_root; @@ -4596,7 +4616,8 @@ impl BeaconChain { .fork_choice_read_lock() .get_preliminary_proposer_head( head_block_root, - re_org_threshold, + re_org_head_threshold, + re_org_parent_threshold, &self.config.re_org_disallowed_offsets, self.config.re_org_max_epochs_since_finalization, ) @@ -4664,16 +4685,27 @@ impl BeaconChain { } // If the current slot is already equal to the proposal slot (or we are in the tail end of - // the prior slot), then check the actual weight of the head against the re-org threshold. - let head_weak = if fork_choice_slot == re_org_block_slot { - info.head_node.weight < info.re_org_weight_threshold + // the prior slot), then check the actual weight of the head against the head re-org threshold + // and the actual weight of the parent against the parent re-org threshold. + let (head_weak, parent_strong) = if fork_choice_slot == re_org_block_slot { + ( + info.head_node.weight < info.re_org_head_weight_threshold, + info.parent_node.weight > info.re_org_parent_weight_threshold, + ) } else { - true + (true, true) }; if !head_weak { return Err(DoNotReOrg::HeadNotWeak { head_weight: info.head_node.weight, - re_org_weight_threshold: info.re_org_weight_threshold, + re_org_head_weight_threshold: info.re_org_head_weight_threshold, + } + .into()); + } + if !parent_strong { + return Err(DoNotReOrg::ParentNotStrong { + parent_weight: info.parent_node.weight, + re_org_parent_weight_threshold: info.re_org_parent_weight_threshold, } .into()); } @@ -4912,7 +4944,10 @@ impl BeaconChain { // allows it to run concurrently with things like attestation packing. let prepare_payload_handle = match &state { BeaconState::Base(_) | BeaconState::Altair(_) => None, - BeaconState::Merge(_) | BeaconState::Capella(_) | BeaconState::Deneb(_) => { + BeaconState::Merge(_) + | BeaconState::Capella(_) + | BeaconState::Deneb(_) + | BeaconState::Electra(_) => { let prepare_payload_handle = get_execution_payload( self.clone(), &state, @@ -4967,6 +5002,10 @@ impl BeaconChain { let attestation_packing_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_ATTESTATION_TIMES); + // Epoch cache and total balance cache are required for op pool packing. + state.build_total_active_balance_cache(&self.spec)?; + initialize_epoch_cache(&mut state, &self.spec)?; + let mut prev_filter_cache = HashMap::new(); let prev_attestation_filter = |att: &AttestationRef| { self.filter_op_pool_attestation(&mut prev_filter_cache, att, &state) @@ -5273,6 +5312,41 @@ impl BeaconChain { execution_payload_value, ) } + BeaconState::Electra(_) => { + let (payload, kzg_commitments, maybe_blobs_and_proofs, execution_payload_value) = + block_contents + .ok_or(BlockProductionError::MissingExecutionPayload)? + .deconstruct(); + + ( + BeaconBlock::Electra(BeaconBlockElectra { + slot, + proposer_index, + parent_root, + state_root: Hash256::zero(), + body: BeaconBlockBodyElectra { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings: proposer_slashings.into(), + attester_slashings: attester_slashings.into(), + attestations: attestations.into(), + deposits: deposits.into(), + voluntary_exits: voluntary_exits.into(), + sync_aggregate: sync_aggregate + .ok_or(BlockProductionError::MissingSyncAggregate)?, + execution_payload: payload + .try_into() + .map_err(|_| BlockProductionError::InvalidPayloadFork)?, + bls_to_execution_changes: bls_to_execution_changes.into(), + blob_kzg_commitments: kzg_commitments + .ok_or(BlockProductionError::InvalidPayloadFork)?, + }, + }), + maybe_blobs_and_proofs, + execution_payload_value, + ) + } }; let block = SignedBeaconBlock::from_block( @@ -5596,7 +5670,7 @@ impl BeaconChain { let prepare_slot_fork = self.spec.fork_name_at_slot::(prepare_slot); let withdrawals = match prepare_slot_fork { ForkName::Base | ForkName::Altair | ForkName::Merge => None, - ForkName::Capella | ForkName::Deneb => { + ForkName::Capella | ForkName::Deneb | ForkName::Electra => { let chain = self.clone(); self.spawn_blocking_handle( move || { @@ -5611,7 +5685,9 @@ impl BeaconChain { let parent_beacon_block_root = match prepare_slot_fork { ForkName::Base | ForkName::Altair | ForkName::Merge | ForkName::Capella => None, - ForkName::Deneb => Some(pre_payload_attributes.parent_beacon_block_root), + ForkName::Deneb | ForkName::Electra => { + Some(pre_payload_attributes.parent_beacon_block_root) + } }; let payload_attributes = PayloadAttributes::new( @@ -6631,13 +6707,17 @@ impl BeaconChain { &self, block_root: &Hash256, ) -> Result, ForkName)>, Error> { - let Some((state_root, slot)) = self - .get_blinded_block(block_root)? - .map(|block| (block.state_root(), block.slot())) - else { + let handle = self + .task_executor + .handle() + .ok_or(BeaconChainError::RuntimeShutdown)?; + + let Some(block) = handle.block_on(async { self.get_block(block_root).await })? else { return Ok(None); }; + let (state_root, slot) = (block.state_root(), block.slot()); + let Some(mut state) = self.get_state(&state_root, Some(slot))? else { return Ok(None); }; @@ -6647,12 +6727,16 @@ impl BeaconChain { .map_err(Error::InconsistentFork)?; match fork_name { - ForkName::Altair | ForkName::Merge => { - LightClientBootstrap::from_beacon_state(&mut state) + ForkName::Altair + | ForkName::Merge + | ForkName::Capella + | ForkName::Deneb + | ForkName::Electra => { + LightClientBootstrap::from_beacon_state(&mut state, &block, &self.spec) .map(|bootstrap| Some((bootstrap, fork_name))) .map_err(Error::LightClientError) } - ForkName::Base | ForkName::Capella | ForkName::Deneb => Err(Error::UnsupportedFork), + ForkName::Base => Err(Error::UnsupportedFork), } } } @@ -6699,8 +6783,8 @@ impl From for Error { } } -impl ChainSegmentResult { - pub fn into_block_error(self) -> Result<(), BlockError> { +impl ChainSegmentResult { + pub fn into_block_error(self) -> Result<(), BlockError> { match self { ChainSegmentResult::Failed { error, .. } => Err(error), ChainSegmentResult::Successful { .. } => Ok(()), diff --git a/beacon_node/beacon_chain/src/beacon_proposer_cache.rs b/beacon_node/beacon_chain/src/beacon_proposer_cache.rs index fa6c93a3ee..ca390712b1 100644 --- a/beacon_node/beacon_chain/src/beacon_proposer_cache.rs +++ b/beacon_node/beacon_chain/src/beacon_proposer_cache.rs @@ -68,19 +68,19 @@ impl Default for BeaconProposerCache { impl BeaconProposerCache { /// If it is cached, returns the proposer for the block at `slot` where the block has the /// ancestor block root of `shuffling_decision_block` at `end_slot(slot.epoch() - 1)`. - pub fn get_slot( + pub fn get_slot( &mut self, shuffling_decision_block: Hash256, slot: Slot, ) -> Option { - let epoch = slot.epoch(T::slots_per_epoch()); + let epoch = slot.epoch(E::slots_per_epoch()); let key = (epoch, shuffling_decision_block); if let Some(cache) = self.cache.get(&key) { // This `if` statement is likely unnecessary, but it feels like good practice. if epoch == cache.epoch { cache .proposers - .get(slot.as_usize() % T::SlotsPerEpoch::to_usize()) + .get(slot.as_usize() % E::SlotsPerEpoch::to_usize()) .map(|&index| Proposer { index, fork: cache.fork, @@ -98,7 +98,7 @@ impl BeaconProposerCache { /// The nth slot in the returned `SmallVec` will be equal to the nth slot in the given `epoch`. /// E.g., if `epoch == 1` then `smallvec[0]` refers to slot 32 (assuming `SLOTS_PER_EPOCH == /// 32`). - pub fn get_epoch( + pub fn get_epoch( &mut self, shuffling_decision_block: Hash256, epoch: Epoch, diff --git a/beacon_node/beacon_chain/src/blob_verification.rs b/beacon_node/beacon_chain/src/blob_verification.rs index f2d150d72b..1fb6170200 100644 --- a/beacon_node/beacon_chain/src/blob_verification.rs +++ b/beacon_node/beacon_chain/src/blob_verification.rs @@ -22,7 +22,7 @@ use types::{ /// An error occurred while validating a gossip blob. #[derive(Debug)] -pub enum GossipBlobError { +pub enum GossipBlobError { /// The blob sidecar is from a slot that is later than the current slot (with respect to the /// gossip clock disparity). /// @@ -95,7 +95,7 @@ pub enum GossipBlobError { /// ## Peer scoring /// /// We cannot process the blob without validating its parent, the peer isn't necessarily faulty. - BlobParentUnknown(Arc>), + BlobParentUnknown(Arc>), /// Invalid kzg commitment inclusion proof /// ## Peer scoring @@ -152,7 +152,7 @@ pub enum GossipBlobError { NotFinalizedDescendant { block_parent_root: Hash256 }, } -impl std::fmt::Display for GossipBlobError { +impl std::fmt::Display for GossipBlobError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { GossipBlobError::BlobParentUnknown(blob_sidecar) => { @@ -167,13 +167,13 @@ impl std::fmt::Display for GossipBlobError { } } -impl From for GossipBlobError { +impl From for GossipBlobError { fn from(e: BeaconChainError) -> Self { GossipBlobError::BeaconChainError(e) } } -impl From for GossipBlobError { +impl From for GossipBlobError { fn from(e: BeaconStateError) -> Self { GossipBlobError::BeaconChainError(BeaconChainError::BeaconStateError(e)) } @@ -258,34 +258,37 @@ impl GossipVerifiedBlob { #[derive(Debug, Derivative, Clone, Encode, Decode)] #[derivative(PartialEq, Eq)] #[ssz(struct_behaviour = "transparent")] -pub struct KzgVerifiedBlob { - blob: Arc>, +pub struct KzgVerifiedBlob { + blob: Arc>, } -impl PartialOrd for KzgVerifiedBlob { +impl PartialOrd for KzgVerifiedBlob { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl Ord for KzgVerifiedBlob { +impl Ord for KzgVerifiedBlob { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.blob.cmp(&other.blob) } } -impl KzgVerifiedBlob { - pub fn new(blob: Arc>, kzg: &Kzg) -> Result { +impl KzgVerifiedBlob { + pub fn new(blob: Arc>, kzg: &Kzg) -> Result { verify_kzg_for_blob(blob, kzg) } - pub fn to_blob(self) -> Arc> { + pub fn to_blob(self) -> Arc> { self.blob } - pub fn as_blob(&self) -> &BlobSidecar { + pub fn as_blob(&self) -> &BlobSidecar { &self.blob } + pub fn get_commitment(&self) -> &KzgCommitment { + &self.blob.kzg_commitment + } /// This is cheap as we're calling clone on an Arc - pub fn clone_blob(&self) -> Arc> { + pub fn clone_blob(&self) -> Arc> { self.blob.clone() } pub fn blob_index(&self) -> u64 { @@ -295,7 +298,7 @@ impl KzgVerifiedBlob { /// /// This should ONLY be used for testing. #[cfg(test)] - pub fn __assumed_valid(blob: Arc>) -> Self { + pub fn __assumed_valid(blob: Arc>) -> Self { Self { blob } } } @@ -303,11 +306,11 @@ impl KzgVerifiedBlob { /// Complete kzg verification for a `BlobSidecar`. /// /// Returns an error if the kzg verification check fails. -pub fn verify_kzg_for_blob( - blob: Arc>, +pub fn verify_kzg_for_blob( + blob: Arc>, kzg: &Kzg, -) -> Result, KzgError> { - validate_blob::(kzg, &blob.blob, blob.kzg_commitment, blob.kzg_proof)?; +) -> Result, KzgError> { + validate_blob::(kzg, &blob.blob, blob.kzg_commitment, blob.kzg_proof)?; Ok(KzgVerifiedBlob { blob }) } @@ -345,17 +348,17 @@ impl IntoIterator for KzgVerifiedBlobList { /// /// Note: This function should be preferred over calling `verify_kzg_for_blob` /// in a loop since this function kzg verifies a list of blobs more efficiently. -pub fn verify_kzg_for_blob_list<'a, T: EthSpec, I>( +pub fn verify_kzg_for_blob_list<'a, E: EthSpec, I>( blob_iter: I, kzg: &'a Kzg, ) -> Result<(), KzgError> where - I: Iterator>>, + I: Iterator>>, { let (blobs, (commitments, proofs)): (Vec<_>, (Vec<_>, Vec<_>)) = blob_iter .map(|blob| (&blob.blob, (blob.kzg_commitment, blob.kzg_proof))) .unzip(); - validate_blobs::(kzg, commitments.as_slice(), blobs, proofs.as_slice()) + validate_blobs::(kzg, commitments.as_slice(), blobs, proofs.as_slice()) } pub fn validate_blob_sidecar_for_gossip( diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index e8df5b811e..3cd8a7f259 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -143,14 +143,14 @@ const WRITE_BLOCK_PROCESSING_SSZ: bool = cfg!(feature = "write_ssz_files"); /// - The block is malformed/invalid (indicated by all results other than `BeaconChainError`. /// - We encountered an error whilst trying to verify the block (a `BeaconChainError`). #[derive(Debug)] -pub enum BlockError { +pub enum BlockError { /// The parent block was unknown. /// /// ## Peer scoring /// /// It's unclear if this block is valid, but it cannot be processed without already knowing /// its parent. - ParentUnknown(RpcBlock), + ParentUnknown(RpcBlock), /// The block slot is greater than the present slot. /// /// ## Peer scoring @@ -190,7 +190,7 @@ pub enum BlockError { /// ## Peer scoring /// /// The block is valid and we have already imported a block with this hash. - BlockIsAlreadyKnown, + BlockIsAlreadyKnown(Hash256), /// The block slot exceeds the MAXIMUM_BLOCK_SLOT_NUMBER. /// /// ## Peer scoring @@ -310,7 +310,7 @@ pub enum BlockError { AvailabilityCheck(AvailabilityCheckError), } -impl From for BlockError { +impl From for BlockError { fn from(e: AvailabilityCheckError) -> Self { Self::AvailabilityCheck(e) } @@ -418,19 +418,19 @@ impl From for ExecutionPayloadError { } } -impl From for BlockError { +impl From for BlockError { fn from(e: ExecutionPayloadError) -> Self { BlockError::ExecutionPayloadError(e) } } -impl From for BlockError { +impl From for BlockError { fn from(e: InconsistentFork) -> Self { BlockError::InconsistentFork(e) } } -impl std::fmt::Display for BlockError { +impl std::fmt::Display for BlockError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { BlockError::ParentUnknown(block) => { @@ -441,7 +441,7 @@ impl std::fmt::Display for BlockError { } } -impl From for BlockError { +impl From for BlockError { fn from(e: BlockSignatureVerifierError) -> Self { match e { // Make a special distinction for `IncorrectBlockProposer` since it indicates an @@ -458,31 +458,31 @@ impl From for BlockError { } } -impl From for BlockError { +impl From for BlockError { fn from(e: BeaconChainError) -> Self { BlockError::BeaconChainError(e) } } -impl From for BlockError { +impl From for BlockError { fn from(e: BeaconStateError) -> Self { BlockError::BeaconChainError(BeaconChainError::BeaconStateError(e)) } } -impl From for BlockError { +impl From for BlockError { fn from(e: SlotProcessingError) -> Self { BlockError::BeaconChainError(BeaconChainError::SlotProcessingError(e)) } } -impl From for BlockError { +impl From for BlockError { fn from(e: DBError) -> Self { BlockError::BeaconChainError(BeaconChainError::DBError(e)) } } -impl From for BlockError { +impl From for BlockError { fn from(e: ArithError) -> Self { BlockError::BeaconChainError(BeaconChainError::ArithError(e)) } @@ -764,6 +764,7 @@ pub trait IntoExecutionPendingBlock: Sized { ) -> Result, BlockSlashInfo>>; fn block(&self) -> &SignedBeaconBlock; + fn block_cloned(&self) -> Arc>; } impl GossipVerifiedBlock { @@ -831,7 +832,7 @@ impl GossipVerifiedBlock { // already know this block. 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); + return Err(BlockError::BlockIsAlreadyKnown(block_root)); } // Do not process a block that doesn't descend from the finalized root. @@ -965,7 +966,7 @@ impl GossipVerifiedBlock { SeenBlock::Slashable => { return Err(BlockError::Slashable); } - SeenBlock::Duplicate => return Err(BlockError::BlockIsAlreadyKnown), + SeenBlock::Duplicate => return Err(BlockError::BlockIsAlreadyKnown(block_root)), SeenBlock::UniqueNonSlashable => {} }; @@ -1017,6 +1018,10 @@ impl IntoExecutionPendingBlock for GossipVerifiedBlock &SignedBeaconBlock { self.block.as_block() } + + fn block_cloned(&self) -> Arc> { + self.block.clone() + } } impl SignatureVerifiedBlock { @@ -1168,6 +1173,10 @@ impl IntoExecutionPendingBlock for SignatureVerifiedBloc fn block(&self) -> &SignedBeaconBlock { self.block.as_block() } + + fn block_cloned(&self) -> Arc> { + self.block.block_cloned() + } } impl IntoExecutionPendingBlock for Arc> { @@ -1198,6 +1207,10 @@ impl IntoExecutionPendingBlock for Arc &SignedBeaconBlock { self } + + fn block_cloned(&self) -> Arc> { + self.clone() + } } impl IntoExecutionPendingBlock for RpcBlock { @@ -1228,6 +1241,10 @@ impl IntoExecutionPendingBlock for RpcBlock fn block(&self) -> &SignedBeaconBlock { self.as_block() } + + fn block_cloned(&self) -> Arc> { + self.block_cloned() + } } impl ExecutionPendingBlock { @@ -1767,7 +1784,7 @@ pub fn check_block_relevancy( .fork_choice_read_lock() .contains_block(&block_root) { - return Err(BlockError::BlockIsAlreadyKnown); + return Err(BlockError::BlockIsAlreadyKnown(block_root)); } Ok(block_root) @@ -2099,7 +2116,7 @@ pub fn verify_header_signature( } } -fn write_state(prefix: &str, state: &BeaconState, log: &Logger) { +fn write_state(prefix: &str, state: &BeaconState, log: &Logger) { if WRITE_BLOCK_PROCESSING_SSZ { let root = state.tree_hash_root(); let filename = format!("{}_slot_{}_root_{}.ssz", prefix, state.slot(), root); @@ -2121,7 +2138,7 @@ fn write_state(prefix: &str, state: &BeaconState, log: &Logger) { } } -fn write_block(block: &SignedBeaconBlock, root: Hash256, log: &Logger) { +fn write_block(block: &SignedBeaconBlock, root: Hash256, log: &Logger) { if WRITE_BLOCK_PROCESSING_SSZ { let filename = format!("block_slot_{}_root{}.ssz", block.slot(), root); let mut path = std::env::temp_dir().join("lighthouse"); diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index a6840ed764..fb0e0c965f 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -7,6 +7,7 @@ use crate::{get_block_root, GossipVerifiedBlock, PayloadVerificationOutcome}; use derivative::Derivative; use ssz_types::VariableList; use state_processing::ConsensusContext; +use std::fmt::{Debug, Formatter}; use std::sync::Arc; use types::blob_sidecar::{BlobIdentifier, BlobSidecarError, FixedBlobSidecarList}; use types::{ @@ -27,13 +28,19 @@ use types::{ /// Note: We make a distinction over blocks received over gossip because /// in a post-deneb world, the blobs corresponding to a given block that are received /// over rpc do not contain the proposer signature for dos resistance. -#[derive(Debug, Clone, Derivative)] +#[derive(Clone, Derivative)] #[derivative(Hash(bound = "E: EthSpec"))] pub struct RpcBlock { block_root: Hash256, block: RpcBlockInner, } +impl Debug for RpcBlock { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "RpcBlock({:?})", self.block_root) + } +} + impl RpcBlock { pub fn block_root(&self) -> Hash256 { self.block_root @@ -46,6 +53,13 @@ impl RpcBlock { } } + pub fn block_cloned(&self) -> Arc> { + match &self.block { + RpcBlockInner::Block(block) => block.clone(), + RpcBlockInner::BlockAndBlobs(block, _) => block.clone(), + } + } + pub fn blobs(&self) -> Option<&BlobSidecarList> { match &self.block { RpcBlockInner::Block(_) => None, @@ -295,29 +309,29 @@ pub struct BlockImportData { pub consensus_context: ConsensusContext, } -pub type GossipVerifiedBlockContents = - (GossipVerifiedBlock, Option>); +pub type GossipVerifiedBlockContents = + (GossipVerifiedBlock, Option>); #[derive(Debug)] -pub enum BlockContentsError { - BlockError(BlockError), - BlobError(GossipBlobError), +pub enum BlockContentsError { + BlockError(BlockError), + BlobError(GossipBlobError), SidecarError(BlobSidecarError), } -impl From> for BlockContentsError { - fn from(value: BlockError) -> Self { +impl From> for BlockContentsError { + fn from(value: BlockError) -> Self { Self::BlockError(value) } } -impl From> for BlockContentsError { - fn from(value: GossipBlobError) -> Self { +impl From> for BlockContentsError { + fn from(value: GossipBlobError) -> Self { Self::BlobError(value) } } -impl std::fmt::Display for BlockContentsError { +impl std::fmt::Display for BlockContentsError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { BlockContentsError::BlockError(err) => { diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 5e06692b8d..56381a7806 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -12,7 +12,7 @@ use crate::light_client_server_cache::LightClientServerCache; use crate::migrate::{BackgroundMigrator, MigratorConfig}; use crate::persisted_beacon_chain::PersistedBeaconChain; use crate::shuffling_cache::{BlockShufflingIds, ShufflingCache}; -use crate::snapshot_cache::{SnapshotCache, DEFAULT_SNAPSHOT_CACHE_SIZE}; +use crate::snapshot_cache::SnapshotCache; use crate::timeout_rw_lock::TimeoutRwLock; use crate::validator_monitor::{ValidatorMonitor, ValidatorMonitorConfig}; use crate::validator_pubkey_cache::ValidatorPubkeyCache; @@ -25,7 +25,7 @@ use eth1::Config as Eth1Config; use execution_layer::ExecutionLayer; use fork_choice::{ForkChoice, ResetPayloadStatuses}; use futures::channel::mpsc::Sender; -use kzg::{Kzg, TrustedSetup}; +use kzg::Kzg; use operation_pool::{OperationPool, PersistedOperationPool}; use parking_lot::{Mutex, RwLock}; use proto_array::{DisallowedReOrgOffsets, ReOrgThreshold}; @@ -39,30 +39,30 @@ use std::time::Duration; use store::{Error as StoreError, HotColdDB, ItemStore, KeyValueStoreOp}; use task_executor::{ShutdownReason, TaskExecutor}; use types::{ - BeaconBlock, BeaconState, ChainSpec, Checkpoint, Epoch, EthSpec, Graffiti, Hash256, Signature, - SignedBeaconBlock, Slot, + BeaconBlock, BeaconState, BlobSidecarList, ChainSpec, Checkpoint, Epoch, EthSpec, Graffiti, + Hash256, Signature, SignedBeaconBlock, Slot, }; /// An empty struct used to "witness" all the `BeaconChainTypes` traits. It has no user-facing /// functionality and only exists to satisfy the type system. -pub struct Witness( - PhantomData<(TSlotClock, TEth1Backend, TEthSpec, THotStore, TColdStore)>, +pub struct Witness( + PhantomData<(TSlotClock, TEth1Backend, E, THotStore, TColdStore)>, ); -impl BeaconChainTypes - for Witness +impl BeaconChainTypes + for Witness where - THotStore: ItemStore + 'static, - TColdStore: ItemStore + 'static, + THotStore: ItemStore + 'static, + TColdStore: ItemStore + 'static, TSlotClock: SlotClock + 'static, - TEth1Backend: Eth1ChainBackend + 'static, - TEthSpec: EthSpec + 'static, + TEth1Backend: Eth1ChainBackend + 'static, + E: EthSpec + 'static, { type HotStore = THotStore; type ColdStore = TColdStore; type SlotClock = TSlotClock; type Eth1Chain = TEth1Backend; - type EthSpec = TEthSpec; + type EthSpec = E; } /// Builds a `BeaconChain` by either creating anew from genesis, or, resuming from an existing chain @@ -101,25 +101,25 @@ pub struct BeaconChainBuilder { // Pending I/O batch that is constructed during building and should be executed atomically // alongside `PersistedBeaconChain` storage when `BeaconChainBuilder::build` is called. pending_io_batch: Vec, - trusted_setup: Option, + kzg: Option>, task_executor: Option, validator_monitor_config: Option, } -impl - BeaconChainBuilder> +impl + BeaconChainBuilder> where - THotStore: ItemStore + 'static, - TColdStore: ItemStore + 'static, + THotStore: ItemStore + 'static, + TColdStore: ItemStore + 'static, TSlotClock: SlotClock + 'static, - TEth1Backend: Eth1ChainBackend + 'static, - TEthSpec: EthSpec + 'static, + TEth1Backend: Eth1ChainBackend + 'static, + E: EthSpec + 'static, { /// Returns a new builder. /// - /// The `_eth_spec_instance` parameter is only supplied to make concrete the `TEthSpec` trait. + /// The `_eth_spec_instance` parameter is only supplied to make concrete the `E` trait. /// This should generally be either the `MinimalEthSpec` or `MainnetEthSpec` types. - pub fn new(_eth_spec_instance: TEthSpec) -> Self { + pub fn new(_eth_spec_instance: E) -> Self { Self { store: None, store_migrator_config: None, @@ -136,19 +136,19 @@ where light_client_server_tx: None, head_tracker: None, validator_pubkey_cache: None, - spec: TEthSpec::default_spec(), + spec: E::default_spec(), chain_config: ChainConfig::default(), log: None, graffiti: Graffiti::default(), slasher: None, pending_io_batch: vec![], - trusted_setup: None, + kzg: None, task_executor: None, validator_monitor_config: None, } } - /// Override the default spec (as defined by `TEthSpec`). + /// Override the default spec (as defined by `E`). /// /// This method should generally be called immediately after `Self::new` to ensure components /// are started with a consistent spec. @@ -172,8 +172,8 @@ where } /// Sets the proposer re-org threshold. - pub fn proposer_re_org_threshold(mut self, threshold: Option) -> Self { - self.chain_config.re_org_threshold = threshold; + pub fn proposer_re_org_head_threshold(mut self, threshold: Option) -> Self { + self.chain_config.re_org_head_threshold = threshold; self } @@ -198,7 +198,7 @@ where /// Sets the store (database). /// /// Should generally be called early in the build chain. - pub fn store(mut self, store: Arc>) -> Self { + pub fn store(mut self, store: Arc>) -> Self { self.store = Some(store); self } @@ -210,7 +210,7 @@ where } /// Sets the slasher. - pub fn slasher(mut self, slasher: Arc>) -> Self { + pub fn slasher(mut self, slasher: Arc>) -> Self { self.slasher = Some(slasher); self } @@ -304,7 +304,7 @@ where self.op_pool = Some( store - .get_item::>(&OP_POOL_DB_KEY) + .get_item::>(&OP_POOL_DB_KEY) .map_err(|e| format!("DB error whilst reading persisted op pool: {:?}", e))? .map(PersistedOperationPool::into_operation_pool) .transpose() @@ -339,8 +339,8 @@ where /// Return the `BeaconSnapshot` representing genesis as well as the mutated builder. fn set_genesis_state( mut self, - mut beacon_state: BeaconState, - ) -> Result<(BeaconSnapshot, Self), String> { + mut beacon_state: BeaconState, + ) -> Result<(BeaconSnapshot, Self), String> { let store = self .store .clone() @@ -387,7 +387,7 @@ where } /// Starts a new chain from a genesis state. - pub fn genesis_state(mut self, beacon_state: BeaconState) -> Result { + pub fn genesis_state(mut self, beacon_state: BeaconState) -> Result { let store = self.store.clone().ok_or("genesis_state requires a store")?; let (genesis, updated_builder) = self.set_genesis_state(beacon_state)?; @@ -430,9 +430,10 @@ where /// Start the chain from a weak subjectivity state. pub fn weak_subjectivity_state( mut self, - mut weak_subj_state: BeaconState, - weak_subj_block: SignedBeaconBlock, - genesis_state: BeaconState, + mut weak_subj_state: BeaconState, + weak_subj_block: SignedBeaconBlock, + weak_subj_blobs: Option>, + genesis_state: BeaconState, ) -> Result { let store = self .store @@ -444,7 +445,7 @@ where .ok_or("weak_subjectivity_state requires a log")?; // Ensure the state is advanced to an epoch boundary. - let slots_per_epoch = TEthSpec::slots_per_epoch(); + let slots_per_epoch = E::slots_per_epoch(); if weak_subj_state.slot() % slots_per_epoch != 0 { debug!( log, @@ -490,6 +491,29 @@ where )); } + // Verify that blobs (if provided) match the block. + if let Some(blobs) = &weak_subj_blobs { + let commitments = weak_subj_block + .message() + .body() + .blob_kzg_commitments() + .map_err(|e| format!("Blobs provided but block does not reference them: {e:?}"))?; + if blobs.len() != commitments.len() { + return Err(format!( + "Wrong number of blobs, expected: {}, got: {}", + commitments.len(), + blobs.len() + )); + } + if commitments + .iter() + .zip(blobs.iter()) + .any(|(commitment, blob)| *commitment != blob.kzg_commitment) + { + return Err("Checkpoint blob does not match block commitment".into()); + } + } + // 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, weak_subj_block_root); @@ -511,14 +535,19 @@ where .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 + // Write the state, block and blobs non-atomically, it doesn't matter if they're forgotten // about on a crash restart. store .put_state(&weak_subj_state_root, &weak_subj_state) - .map_err(|e| format!("Failed to store weak subjectivity state: {:?}", e))?; + .map_err(|e| format!("Failed to store weak subjectivity state: {e:?}"))?; store .put_block(&weak_subj_block_root, weak_subj_block.clone()) - .map_err(|e| format!("Failed to store weak subjectivity block: {:?}", e))?; + .map_err(|e| format!("Failed to store weak subjectivity block: {e:?}"))?; + if let Some(blobs) = weak_subj_blobs { + store + .put_blobs(&weak_subj_block_root, blobs) + .map_err(|e| format!("Failed to store weak subjectivity blobs: {e:?}"))?; + } // 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 @@ -540,7 +569,7 @@ where self.pending_io_batch .push(store.pruning_checkpoint_store_op(Checkpoint { root: weak_subj_block_root, - epoch: weak_subj_state.slot().epoch(TEthSpec::slots_per_epoch()), + epoch: weak_subj_state.slot().epoch(E::slots_per_epoch()), })); let snapshot = BeaconSnapshot { @@ -574,7 +603,7 @@ where } /// Sets the `BeaconChain` execution layer. - pub fn execution_layer(mut self, execution_layer: Option>) -> Self { + pub fn execution_layer(mut self, execution_layer: Option>) -> Self { self.execution_layer = execution_layer; self } @@ -582,7 +611,7 @@ where /// Sets the `BeaconChain` event handler backend. /// /// For example, provide `ServerSentEventHandler` as a `handler`. - pub fn event_handler(mut self, handler: Option>) -> Self { + pub fn event_handler(mut self, handler: Option>) -> Self { self.event_handler = handler; self } @@ -609,10 +638,7 @@ where } /// Sets a `Sender` to allow the beacon chain to trigger light_client update production. - pub fn light_client_server_tx( - mut self, - sender: Sender>, - ) -> Self { + pub fn light_client_server_tx(mut self, sender: Sender>) -> Self { self.light_client_server_tx = Some(sender); self } @@ -643,8 +669,8 @@ where self } - pub fn trusted_setup(mut self, trusted_setup: TrustedSetup) -> Self { - self.trusted_setup = Some(trusted_setup); + pub fn kzg(mut self, kzg: Option>) -> Self { + self.kzg = kzg; self } @@ -657,10 +683,8 @@ where #[allow(clippy::type_complexity)] // I think there's nothing to be gained here from a type alias. pub fn build( mut self, - ) -> Result< - BeaconChain>, - String, - > { + ) -> Result>, String> + { let log = self.log.ok_or("Cannot build without a logger")?; let slot_clock = self .slot_clock @@ -694,15 +718,6 @@ where slot_clock.now().ok_or("Unable to read slot")? }; - let kzg = if let Some(trusted_setup) = self.trusted_setup { - let kzg = Kzg::new_from_trusted_setup(trusted_setup) - .map_err(|e| format!("Failed to load trusted setup: {:?}", e))?; - let kzg_arc = Arc::new(kzg); - Some(kzg_arc) - } else { - None - }; - let initial_head_block_root = fork_choice .get_head(current_slot, &self.spec) .map_err(|e| format!("Unable to get fork choice head: {:?}", e))?; @@ -751,8 +766,6 @@ where store.clone(), Some(current_slot), &self.spec, - self.chain_config.progressive_balances_mode, - &log, )?; } @@ -798,7 +811,7 @@ where if let Some(slot) = slot_clock.now() { validator_monitor.process_valid_state( - slot.epoch(TEthSpec::slots_per_epoch()), + slot.epoch(E::slots_per_epoch()), &head_snapshot.beacon_state, &self.spec, ); @@ -821,12 +834,12 @@ where // doesn't write a `PersistedBeaconChain` without the rest of the batch. let head_tracker_reader = head_tracker.0.read(); self.pending_io_batch.push(BeaconChain::< - Witness, + Witness, >::persist_head_in_batch_standalone( genesis_block_root, &head_tracker_reader )); self.pending_io_batch.push(BeaconChain::< - Witness, + Witness, >::persist_fork_choice_in_batch_standalone( &fork_choice )); @@ -841,21 +854,26 @@ where let head_for_snapshot_cache = head_snapshot.clone(); let canonical_head = CanonicalHead::new(fork_choice, Arc::new(head_snapshot)); let shuffling_cache_size = self.chain_config.shuffling_cache_size; + let snapshot_cache_size = self.chain_config.snapshot_cache_size; // Calculate the weak subjectivity point in which to backfill blocks to. let genesis_backfill_slot = if self.chain_config.genesis_backfill { Slot::new(0) } else { - let backfill_epoch_range = (self.spec.min_validator_withdrawability_delay - + self.spec.churn_limit_quotient) - .as_u64() - / 2; + let backfill_epoch_range = if cfg!(feature = "test_backfill") { + 3 + } else { + (self.spec.min_validator_withdrawability_delay + self.spec.churn_limit_quotient) + .as_u64() + / 2 + }; + match slot_clock.now() { Some(current_slot) => { let genesis_backfill_epoch = current_slot - .epoch(TEthSpec::slots_per_epoch()) + .epoch(E::slots_per_epoch()) .saturating_sub(backfill_epoch_range); - genesis_backfill_epoch.start_slot(TEthSpec::slots_per_epoch()) + genesis_backfill_epoch.start_slot(E::slots_per_epoch()) } None => { // The slot clock cannot derive the current slot. We therefore assume we are @@ -913,7 +931,7 @@ where event_handler: self.event_handler, head_tracker, snapshot_cache: TimeoutRwLock::new(SnapshotCache::new( - DEFAULT_SNAPSHOT_CACHE_SIZE, + snapshot_cache_size, head_for_snapshot_cache, )), shuffling_cache: TimeoutRwLock::new(ShufflingCache::new( @@ -928,6 +946,7 @@ where validator_pubkey_cache: TimeoutRwLock::new(validator_pubkey_cache), attester_cache: <_>::default(), early_attester_cache: <_>::default(), + reqresp_pre_import_cache: <_>::default(), light_client_server_cache: LightClientServerCache::new(), light_client_server_tx: self.light_client_server_tx, shutdown_sender: self @@ -939,10 +958,10 @@ where validator_monitor: RwLock::new(validator_monitor), genesis_backfill_slot, data_availability_checker: Arc::new( - DataAvailabilityChecker::new(slot_clock, kzg.clone(), store, &log, self.spec) + DataAvailabilityChecker::new(slot_clock, self.kzg.clone(), store, &log, self.spec) .map_err(|e| format!("Error initializing DataAvailabiltyChecker: {:?}", e))?, ), - kzg, + kzg: self.kzg.clone(), block_production_state: Arc::new(Mutex::new(None)), }; @@ -1017,15 +1036,13 @@ where } } -impl - BeaconChainBuilder< - Witness, TEthSpec, THotStore, TColdStore>, - > +impl + BeaconChainBuilder, E, THotStore, TColdStore>> where - THotStore: ItemStore + 'static, - TColdStore: ItemStore + 'static, + THotStore: ItemStore + 'static, + TColdStore: ItemStore + 'static, TSlotClock: SlotClock + 'static, - TEthSpec: EthSpec + 'static, + E: EthSpec + 'static, { /// Do not use any eth1 backend. The client will not be able to produce beacon blocks. pub fn no_eth1_backend(self) -> Self { @@ -1048,13 +1065,13 @@ where } } -impl - BeaconChainBuilder> +impl + BeaconChainBuilder> where - THotStore: ItemStore + 'static, - TColdStore: ItemStore + 'static, - TEth1Backend: Eth1ChainBackend + 'static, - TEthSpec: EthSpec + 'static, + THotStore: ItemStore + 'static, + TColdStore: ItemStore + 'static, + TEth1Backend: Eth1ChainBackend + 'static, + E: EthSpec + 'static, { /// Sets the `BeaconChain` slot clock to `TestingSlotClock`. /// @@ -1074,10 +1091,10 @@ where } } -fn genesis_block( - genesis_state: &mut BeaconState, +fn genesis_block( + genesis_state: &mut BeaconState, spec: &ChainSpec, -) -> Result, String> { +) -> Result, String> { let mut genesis_block = BeaconBlock::empty(spec); *genesis_block.state_root_mut() = genesis_state .update_tree_hash_cache() diff --git a/beacon_node/beacon_chain/src/chain_config.rs b/beacon_node/beacon_chain/src/chain_config.rs index 23e17a6efa..0772aff671 100644 --- a/beacon_node/beacon_chain/src/chain_config.rs +++ b/beacon_node/beacon_chain/src/chain_config.rs @@ -1,9 +1,10 @@ pub use proto_array::{DisallowedReOrgOffsets, ReOrgThreshold}; use serde::{Deserialize, Serialize}; use std::time::Duration; -use types::{Checkpoint, Epoch, ProgressiveBalancesMode}; +use types::{Checkpoint, Epoch}; -pub const DEFAULT_RE_ORG_THRESHOLD: ReOrgThreshold = ReOrgThreshold(20); +pub const DEFAULT_RE_ORG_HEAD_THRESHOLD: ReOrgThreshold = ReOrgThreshold(20); +pub const DEFAULT_RE_ORG_PARENT_THRESHOLD: ReOrgThreshold = ReOrgThreshold(160); pub const DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION: Epoch = Epoch::new(2); /// Default to 1/12th of the slot, which is 1 second on mainnet. pub const DEFAULT_RE_ORG_CUTOFF_DENOMINATOR: u32 = 12; @@ -31,8 +32,10 @@ pub struct ChainConfig { pub enable_lock_timeouts: bool, /// The max size of a message that can be sent over the network. pub max_network_size: usize, - /// Maximum percentage of committee weight at which to attempt re-orging the canonical head. - pub re_org_threshold: Option, + /// Maximum percentage of the head committee weight at which to attempt re-orging the canonical head. + pub re_org_head_threshold: Option, + /// Minimum percentage of the parent committee weight at which to attempt re-orging the canonical head. + pub re_org_parent_threshold: Option, /// Maximum number of epochs since finalization for attempting a proposer re-org. pub re_org_max_epochs_since_finalization: Epoch, /// Maximum delay after the start of the slot at which to propose a reorging block. @@ -72,6 +75,8 @@ pub struct ChainConfig { pub optimistic_finalized_sync: bool, /// The size of the shuffling cache, pub shuffling_cache_size: usize, + /// The size of the snapshot cache. + pub snapshot_cache_size: usize, /// If using a weak-subjectivity sync, whether we should download blocks all the way back to /// genesis. pub genesis_backfill: bool, @@ -79,8 +84,6 @@ pub struct ChainConfig { /// /// This is useful for block builders and testing. pub always_prepare_payload: 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, /// When set to true Light client server computes and caches state proofs for serving updates @@ -95,7 +98,8 @@ impl Default for ChainConfig { reconstruct_historic_states: false, enable_lock_timeouts: true, max_network_size: 10 * 1_048_576, // 10M - re_org_threshold: Some(DEFAULT_RE_ORG_THRESHOLD), + re_org_head_threshold: Some(DEFAULT_RE_ORG_HEAD_THRESHOLD), + re_org_parent_threshold: Some(DEFAULT_RE_ORG_PARENT_THRESHOLD), re_org_max_epochs_since_finalization: DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, re_org_cutoff_millis: None, re_org_disallowed_offsets: DisallowedReOrgOffsets::default(), @@ -112,9 +116,9 @@ impl Default for ChainConfig { // This value isn't actually read except in tests. optimistic_finalized_sync: true, shuffling_cache_size: crate::shuffling_cache::DEFAULT_CACHE_SIZE, + snapshot_cache_size: crate::snapshot_cache::DEFAULT_SNAPSHOT_CACHE_SIZE, genesis_backfill: false, always_prepare_payload: false, - progressive_balances_mode: ProgressiveBalancesMode::Fast, epochs_per_migration: crate::migrate::DEFAULT_EPOCHS_PER_MIGRATION, enable_light_client_server: false, } diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 48d505e9e7..3ef105c6d3 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -2,33 +2,25 @@ use crate::blob_verification::{verify_kzg_for_blob_list, GossipVerifiedBlob, Kzg use crate::block_verification_types::{ AvailabilityPendingExecutedBlock, AvailableExecutedBlock, RpcBlock, }; -pub use crate::data_availability_checker::availability_view::{ - AvailabilityView, GetCommitment, GetCommitments, -}; pub use crate::data_availability_checker::child_components::ChildComponents; use crate::data_availability_checker::overflow_lru_cache::OverflowLRUCache; -use crate::data_availability_checker::processing_cache::ProcessingCache; use crate::{BeaconChain, BeaconChainTypes, BeaconStore}; use kzg::Kzg; -use parking_lot::RwLock; -pub use processing_cache::ProcessingComponents; use slasher::test_utils::E; use slog::{debug, error, Logger}; use slot_clock::SlotClock; +use ssz_types::FixedVector; use std::fmt; use std::fmt::Debug; use std::num::NonZeroUsize; use std::sync::Arc; use task_executor::TaskExecutor; -use types::beacon_block_body::{KzgCommitmentOpts, KzgCommitments}; use types::blob_sidecar::{BlobIdentifier, BlobSidecar, FixedBlobSidecarList}; -use types::{BlobSidecarList, ChainSpec, Epoch, EthSpec, Hash256, SignedBeaconBlock, Slot}; +use types::{BlobSidecarList, ChainSpec, Epoch, EthSpec, Hash256, SignedBeaconBlock}; -mod availability_view; mod child_components; mod error; mod overflow_lru_cache; -mod processing_cache; mod state_lru_cache; pub use error::{Error as AvailabilityCheckError, ErrorCategory as AvailabilityCheckErrorCategory}; @@ -49,7 +41,6 @@ pub const STATE_LRU_CAPACITY: usize = STATE_LRU_CAPACITY_NON_ZERO.get(); /// `DataAvailabilityChecker` is responsible for KZG verification of block components as well as /// checking whether a "availability check" is required at all. pub struct DataAvailabilityChecker { - processing_cache: RwLock>, availability_cache: Arc>, slot_clock: T::SlotClock, kzg: Option>, @@ -62,12 +53,12 @@ pub struct DataAvailabilityChecker { /// Indicates if the block is fully `Available` or if we need blobs or blocks /// to "complete" the requirements for an `AvailableBlock`. #[derive(PartialEq)] -pub enum Availability { +pub enum Availability { MissingComponents(Hash256), - Available(Box>), + Available(Box>), } -impl Debug for Availability { +impl Debug for Availability { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::MissingComponents(block_root) => { @@ -88,7 +79,6 @@ impl DataAvailabilityChecker { ) -> Result { let overflow_cache = OverflowLRUCache::new(OVERFLOW_LRU_CAPACITY, store, spec.clone())?; Ok(Self { - processing_cache: <_>::default(), availability_cache: Arc::new(overflow_cache), slot_clock, log: log.clone(), @@ -97,27 +87,34 @@ impl DataAvailabilityChecker { }) } - /// Checks if the given block root is cached. + /// Checks if the block root is currenlty in the availability cache awaiting processing because + /// of missing components. pub fn has_block(&self, block_root: &Hash256) -> bool { - self.processing_cache.read().has_block(block_root) + self.availability_cache.has_block(block_root) } - /// Get the processing info for a block. - pub fn get_processing_components( - &self, - block_root: Hash256, - ) -> Option> { - self.processing_cache.read().get(&block_root).cloned() + pub fn get_missing_blob_ids_with(&self, block_root: Hash256) -> MissingBlobs { + self.availability_cache + .with_pending_components(&block_root, |pending_components| match pending_components { + Some(pending_components) => self.get_missing_blob_ids( + block_root, + pending_components + .get_cached_block() + .as_ref() + .map(|b| b.as_block()), + &pending_components.verified_blobs, + ), + None => MissingBlobs::new_without_block(block_root, self.is_deneb()), + }) } - /// A `None` indicates blobs are not required. - /// /// If there's no block, all possible ids will be returned that don't exist in the given blobs. /// If there no blobs, all possible ids will be returned. - pub fn get_missing_blob_ids>( + pub fn get_missing_blob_ids( &self, block_root: Hash256, - availability_view: &V, + block: Option<&SignedBeaconBlock>, + blobs: &FixedVector, ::MaxBlobsPerBlock>, ) -> MissingBlobs { let Some(current_slot) = self.slot_clock.now_or_genesis() else { error!( @@ -130,49 +127,25 @@ impl DataAvailabilityChecker { let current_epoch = current_slot.epoch(T::EthSpec::slots_per_epoch()); if self.da_check_required_for_epoch(current_epoch) { - match availability_view.get_cached_block() { + match block { Some(cached_block) => { - let block_commitments = cached_block.get_commitments(); - let blob_commitments = availability_view.get_cached_blobs(); - - let num_blobs_expected = block_commitments.len(); - let mut blob_ids = Vec::with_capacity(num_blobs_expected); - - // Zip here will always limit the number of iterations to the size of - // `block_commitment` because `blob_commitments` will always be populated - // with `Option` values up to `MAX_BLOBS_PER_BLOCK`. - for (index, (block_commitment, blob_commitment_opt)) in block_commitments - .into_iter() - .zip(blob_commitments.iter()) + let block_commitments_len = cached_block + .message() + .body() + .blob_kzg_commitments() + .map(|v| v.len()) + .unwrap_or(0); + let blob_ids = blobs + .iter() + .take(block_commitments_len) .enumerate() - { - // Always add a missing blob. - let Some(blob_commitment) = blob_commitment_opt else { - blob_ids.push(BlobIdentifier { + .filter_map(|(index, blob_commitment_opt)| { + blob_commitment_opt.is_none().then_some(BlobIdentifier { block_root, index: index as u64, - }); - continue; - }; - - let blob_commitment = *blob_commitment.get_commitment(); - - // Check for consistency, but this shouldn't happen, an availability view - // should guaruntee consistency. - if blob_commitment != block_commitment { - error!(self.log, - "Inconsistent availability view"; - "block_root" => ?block_root, - "block_commitment" => ?block_commitment, - "blob_commitment" => ?blob_commitment, - "index" => index - ); - blob_ids.push(BlobIdentifier { - block_root, - index: index as u64, - }); - } - } + }) + }) + .collect(); MissingBlobs::KnownMissing(blob_ids) } None => { @@ -344,75 +317,6 @@ impl DataAvailabilityChecker { block.num_expected_blobs() > 0 && self.da_check_required_for_epoch(block.epoch()) } - /// Adds block commitments to the processing cache. These commitments are unverified but caching - /// them here is useful to avoid duplicate downloads of blocks, as well as understanding - /// our blob download requirements. - pub fn notify_block_commitments( - &self, - slot: Slot, - block_root: Hash256, - commitments: KzgCommitments, - ) { - self.processing_cache - .write() - .entry(block_root) - .or_insert_with(|| ProcessingComponents::new(slot)) - .merge_block(commitments); - } - - /// Add a single blob commitment to the processing cache. This commitment is unverified but caching - /// them here is useful to avoid duplicate downloads of blobs, as well as understanding - /// our block and blob download requirements. - pub fn notify_gossip_blob( - &self, - slot: Slot, - block_root: Hash256, - blob: &GossipVerifiedBlob, - ) { - let index = blob.index(); - let commitment = blob.kzg_commitment(); - self.processing_cache - .write() - .entry(block_root) - .or_insert_with(|| ProcessingComponents::new(slot)) - .merge_single_blob(index as usize, commitment); - } - - /// Adds blob commitments to the processing cache. These commitments are unverified but caching - /// them here is useful to avoid duplicate downloads of blobs, as well as understanding - /// our block and blob download requirements. - pub fn notify_rpc_blobs( - &self, - slot: Slot, - block_root: Hash256, - blobs: &FixedBlobSidecarList, - ) { - let mut commitments = KzgCommitmentOpts::::default(); - for blob in blobs.iter().flatten() { - if let Some(commitment) = commitments.get_mut(blob.index as usize) { - *commitment = Some(blob.kzg_commitment); - } - } - self.processing_cache - .write() - .entry(block_root) - .or_insert_with(|| ProcessingComponents::new(slot)) - .merge_blobs(commitments); - } - - /// Clears the block and all blobs from the processing cache for a give root if they exist. - pub fn remove_notified(&self, block_root: &Hash256) { - self.processing_cache.write().remove(block_root) - } - - /// Gather all block roots for which we are not currently processing all components for the - /// given slot. - pub fn incomplete_processing_components(&self, slot: Slot) -> Vec { - self.processing_cache - .read() - .incomplete_processing_components(slot) - } - /// The epoch at which we require a data availability check in block processing. /// `None` if the `Deneb` fork is disabled. pub fn data_availability_boundary(&self) -> Option { @@ -450,6 +354,22 @@ impl DataAvailabilityChecker { pub fn persist_all(&self) -> Result<(), AvailabilityCheckError> { self.availability_cache.write_all_to_disk() } + + /// Collects metrics from the data availability checker. + pub fn metrics(&self) -> DataAvailabilityCheckerMetrics { + DataAvailabilityCheckerMetrics { + num_store_entries: self.availability_cache.num_store_entries(), + state_cache_size: self.availability_cache.state_cache_size(), + block_cache_size: self.availability_cache.block_cache_size(), + } + } +} + +/// Helper struct to group data availability checker metrics. +pub struct DataAvailabilityCheckerMetrics { + pub num_store_entries: usize, + pub state_cache_size: usize, + pub block_cache_size: usize, } pub fn start_availability_cache_maintenance_service( @@ -597,6 +517,15 @@ pub enum MaybeAvailableBlock { }, } +impl MaybeAvailableBlock { + pub fn block_cloned(&self) -> Arc> { + match self { + Self::Available(block) => block.block_cloned(), + Self::AvailabilityPending { block, .. } => block.clone(), + } + } +} + #[derive(Debug, Clone)] pub enum MissingBlobs { /// We know for certain these blobs are missing. diff --git a/beacon_node/beacon_chain/src/data_availability_checker/availability_view.rs b/beacon_node/beacon_chain/src/data_availability_checker/availability_view.rs deleted file mode 100644 index 776f81ee54..0000000000 --- a/beacon_node/beacon_chain/src/data_availability_checker/availability_view.rs +++ /dev/null @@ -1,553 +0,0 @@ -use super::child_components::ChildComponents; -use super::state_lru_cache::DietAvailabilityPendingExecutedBlock; -use crate::blob_verification::KzgVerifiedBlob; -use crate::block_verification_types::AsBlock; -use crate::data_availability_checker::overflow_lru_cache::PendingComponents; -use crate::data_availability_checker::ProcessingComponents; -use kzg::KzgCommitment; -use ssz_types::FixedVector; -use std::sync::Arc; -use types::beacon_block_body::KzgCommitments; -use types::{BlobSidecar, EthSpec, SignedBeaconBlock}; - -/// Defines an interface for managing data availability with two key invariants: -/// -/// 1. If we haven't seen a block yet, we will insert the first blob for a given (block_root, index) -/// but we won't insert subsequent blobs for the same (block_root, index) if they have a different -/// commitment. -/// 2. On block insertion, any non-matching blob commitments are evicted. -/// -/// Types implementing this trait can be used for validating and managing availability -/// of blocks and blobs in a cache-like data structure. -pub trait AvailabilityView { - /// The type representing a block in the implementation. - type BlockType: GetCommitments; - - /// The type representing a blob in the implementation. Must implement `Clone`. - type BlobType: Clone + GetCommitment; - - /// Returns an immutable reference to the cached block. - fn get_cached_block(&self) -> &Option; - - /// Returns an immutable reference to the fixed vector of cached blobs. - fn get_cached_blobs(&self) -> &FixedVector, E::MaxBlobsPerBlock>; - - /// Returns a mutable reference to the cached block. - fn get_cached_block_mut(&mut self) -> &mut Option; - - /// Returns a mutable reference to the fixed vector of cached blobs. - fn get_cached_blobs_mut( - &mut self, - ) -> &mut FixedVector, E::MaxBlobsPerBlock>; - - /// Checks if a block exists in the cache. - /// - /// Returns: - /// - `true` if a block exists. - /// - `false` otherwise. - fn block_exists(&self) -> bool { - self.get_cached_block().is_some() - } - - /// Checks if a blob exists at the given index in the cache. - /// - /// Returns: - /// - `true` if a blob exists at the given index. - /// - `false` otherwise. - fn blob_exists(&self, blob_index: usize) -> bool { - self.get_cached_blobs() - .get(blob_index) - .map(|b| b.is_some()) - .unwrap_or(false) - } - - /// Returns the number of blobs that are expected to be present. Returns `None` if we don't have a - /// block. - /// - /// This corresponds to the number of commitments that are present in a block. - fn num_expected_blobs(&self) -> Option { - self.get_cached_block() - .as_ref() - .map(|b| b.get_commitments().len()) - } - - /// Returns the number of blobs that have been received and are stored in the cache. - fn num_received_blobs(&self) -> usize { - self.get_cached_blobs().iter().flatten().count() - } - - /// Inserts a block into the cache. - fn insert_block(&mut self, block: Self::BlockType) { - *self.get_cached_block_mut() = Some(block) - } - - /// Inserts a blob at a specific index in the cache. - /// - /// Existing blob at the index will be replaced. - fn insert_blob_at_index(&mut self, blob_index: usize, blob: Self::BlobType) { - if let Some(b) = self.get_cached_blobs_mut().get_mut(blob_index) { - *b = Some(blob); - } - } - - /// Merges a given set of blobs into the cache. - /// - /// Blobs are only inserted if: - /// 1. The blob entry at the index is empty and no block exists. - /// 2. The block exists and its commitment matches the blob's commitment. - fn merge_blobs(&mut self, blobs: FixedVector, E::MaxBlobsPerBlock>) { - for (index, blob) in blobs.iter().cloned().enumerate() { - let Some(blob) = blob else { continue }; - self.merge_single_blob(index, blob); - } - } - - /// Merges a single blob into the cache. - /// - /// Blobs are only inserted if: - /// 1. The blob entry at the index is empty and no block exists, or - /// 2. The block exists and its commitment matches the blob's commitment. - fn merge_single_blob(&mut self, index: usize, blob: Self::BlobType) { - let commitment = *blob.get_commitment(); - if let Some(cached_block) = self.get_cached_block() { - let block_commitment_opt = cached_block.get_commitments().get(index).copied(); - if let Some(block_commitment) = block_commitment_opt { - if block_commitment == commitment { - self.insert_blob_at_index(index, blob) - } - } - } else if !self.blob_exists(index) { - self.insert_blob_at_index(index, blob) - } - } - - /// Inserts a new block and revalidates the existing blobs against it. - /// - /// Blobs that don't match the new block's commitments are evicted. - fn merge_block(&mut self, block: Self::BlockType) { - self.insert_block(block); - let reinsert = std::mem::take(self.get_cached_blobs_mut()); - self.merge_blobs(reinsert); - } - - /// Checks if the block and all of its expected blobs are available in the cache. - /// - /// Returns `true` if both the block exists and the number of received blobs matches the number - /// of expected blobs. - fn is_available(&self) -> bool { - if let Some(num_expected_blobs) = self.num_expected_blobs() { - num_expected_blobs == self.num_received_blobs() - } else { - false - } - } -} - -/// Implements the `AvailabilityView` trait for a given struct. -/// -/// - `$struct_name`: The name of the struct for which to implement `AvailabilityView`. -/// - `$block_type`: The type to use for `BlockType` in the `AvailabilityView` trait. -/// - `$blob_type`: The type to use for `BlobType` in the `AvailabilityView` trait. -/// - `$block_field`: The field name in the struct that holds the cached block. -/// - `$blob_field`: The field name in the struct that holds the cached blobs. -#[macro_export] -macro_rules! impl_availability_view { - ($struct_name:ident, $block_type:ty, $blob_type:ty, $block_field:ident, $blob_field:ident) => { - impl AvailabilityView for $struct_name { - type BlockType = $block_type; - type BlobType = $blob_type; - - fn get_cached_block(&self) -> &Option { - &self.$block_field - } - - fn get_cached_blobs( - &self, - ) -> &FixedVector, E::MaxBlobsPerBlock> { - &self.$blob_field - } - - fn get_cached_block_mut(&mut self) -> &mut Option { - &mut self.$block_field - } - - fn get_cached_blobs_mut( - &mut self, - ) -> &mut FixedVector, E::MaxBlobsPerBlock> { - &mut self.$blob_field - } - } - }; -} - -impl_availability_view!( - ProcessingComponents, - KzgCommitments, - KzgCommitment, - block_commitments, - blob_commitments -); - -impl_availability_view!( - PendingComponents, - DietAvailabilityPendingExecutedBlock, - KzgVerifiedBlob, - executed_block, - verified_blobs -); - -impl_availability_view!( - ChildComponents, - Arc>, - Arc>, - downloaded_block, - downloaded_blobs -); - -pub trait GetCommitments { - fn get_commitments(&self) -> KzgCommitments; -} - -pub trait GetCommitment { - fn get_commitment(&self) -> &KzgCommitment; -} - -// These implementations are required to implement `AvailabilityView` for `ProcessingView`. -impl GetCommitments for KzgCommitments { - fn get_commitments(&self) -> KzgCommitments { - self.clone() - } -} -impl GetCommitment for KzgCommitment { - fn get_commitment(&self) -> &KzgCommitment { - self - } -} - -// These implementations are required to implement `AvailabilityView` for `PendingComponents`. -impl GetCommitments for DietAvailabilityPendingExecutedBlock { - fn get_commitments(&self) -> KzgCommitments { - self.as_block() - .message() - .body() - .blob_kzg_commitments() - .cloned() - .unwrap_or_default() - } -} - -impl GetCommitment for KzgVerifiedBlob { - fn get_commitment(&self) -> &KzgCommitment { - &self.as_blob().kzg_commitment - } -} - -// These implementations are required to implement `AvailabilityView` for `ChildComponents`. -impl GetCommitments for Arc> { - fn get_commitments(&self) -> KzgCommitments { - self.message() - .body() - .blob_kzg_commitments() - .ok() - .cloned() - .unwrap_or_default() - } -} -impl GetCommitment for Arc> { - fn get_commitment(&self) -> &KzgCommitment { - &self.kzg_commitment - } -} - -#[cfg(test)] -pub mod tests { - use super::*; - use crate::block_verification_types::BlockImportData; - use crate::eth1_finalization_cache::Eth1FinalizationData; - use crate::test_utils::{generate_rand_block_and_blobs, NumBlobs}; - use crate::AvailabilityPendingExecutedBlock; - use crate::PayloadVerificationOutcome; - use fork_choice::PayloadVerificationStatus; - use rand::rngs::StdRng; - use rand::SeedableRng; - use state_processing::ConsensusContext; - use types::test_utils::TestRandom; - use types::{BeaconState, ChainSpec, ForkName, MainnetEthSpec, Slot}; - - type E = MainnetEthSpec; - - type Setup = ( - SignedBeaconBlock, - FixedVector>>, ::MaxBlobsPerBlock>, - FixedVector>>, ::MaxBlobsPerBlock>, - ); - - pub fn pre_setup() -> Setup { - let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); - let (block, blobs_vec) = - generate_rand_block_and_blobs::(ForkName::Deneb, NumBlobs::Random, &mut rng); - let mut blobs: FixedVector<_, ::MaxBlobsPerBlock> = FixedVector::default(); - - for blob in blobs_vec { - if let Some(b) = blobs.get_mut(blob.index as usize) { - *b = Some(Arc::new(blob)); - } - } - - let mut invalid_blobs: FixedVector< - Option>>, - ::MaxBlobsPerBlock, - > = FixedVector::default(); - for (index, blob) in blobs.iter().enumerate() { - if let Some(invalid_blob) = blob { - let mut blob_copy = invalid_blob.as_ref().clone(); - blob_copy.kzg_commitment = KzgCommitment::random_for_test(&mut rng); - *invalid_blobs.get_mut(index).unwrap() = Some(Arc::new(blob_copy)); - } - } - - (block, blobs, invalid_blobs) - } - - type ProcessingViewSetup = ( - KzgCommitments, - FixedVector, ::MaxBlobsPerBlock>, - FixedVector, ::MaxBlobsPerBlock>, - ); - - pub fn setup_processing_components( - block: SignedBeaconBlock, - valid_blobs: FixedVector>>, ::MaxBlobsPerBlock>, - invalid_blobs: FixedVector>>, ::MaxBlobsPerBlock>, - ) -> ProcessingViewSetup { - let commitments = block - .message() - .body() - .blob_kzg_commitments() - .unwrap() - .clone(); - let blobs = FixedVector::from( - valid_blobs - .iter() - .map(|blob_opt| blob_opt.as_ref().map(|blob| blob.kzg_commitment)) - .collect::>(), - ); - let invalid_blobs = FixedVector::from( - invalid_blobs - .iter() - .map(|blob_opt| blob_opt.as_ref().map(|blob| blob.kzg_commitment)) - .collect::>(), - ); - (commitments, blobs, invalid_blobs) - } - - type PendingComponentsSetup = ( - DietAvailabilityPendingExecutedBlock, - FixedVector>, ::MaxBlobsPerBlock>, - FixedVector>, ::MaxBlobsPerBlock>, - ); - - pub fn setup_pending_components( - block: SignedBeaconBlock, - valid_blobs: FixedVector>>, ::MaxBlobsPerBlock>, - invalid_blobs: FixedVector>>, ::MaxBlobsPerBlock>, - ) -> PendingComponentsSetup { - let blobs = FixedVector::from( - valid_blobs - .iter() - .map(|blob_opt| { - blob_opt - .as_ref() - .map(|blob| KzgVerifiedBlob::__assumed_valid(blob.clone())) - }) - .collect::>(), - ); - let invalid_blobs = FixedVector::from( - invalid_blobs - .iter() - .map(|blob_opt| { - blob_opt - .as_ref() - .map(|blob| KzgVerifiedBlob::__assumed_valid(blob.clone())) - }) - .collect::>(), - ); - let dummy_parent = block.clone_as_blinded(); - let block = AvailabilityPendingExecutedBlock { - block: Arc::new(block), - import_data: BlockImportData { - block_root: Default::default(), - state: BeaconState::new(0, Default::default(), &ChainSpec::minimal()), - parent_block: dummy_parent, - parent_eth1_finalization_data: Eth1FinalizationData { - eth1_data: Default::default(), - eth1_deposit_index: 0, - }, - confirmed_state_roots: vec![], - consensus_context: ConsensusContext::new(Slot::new(0)), - }, - payload_verification_outcome: PayloadVerificationOutcome { - payload_verification_status: PayloadVerificationStatus::Verified, - is_valid_merge_transition_block: false, - }, - }; - (block.into(), blobs, invalid_blobs) - } - - type ChildComponentsSetup = ( - Arc>, - FixedVector>>, ::MaxBlobsPerBlock>, - FixedVector>>, ::MaxBlobsPerBlock>, - ); - - pub fn setup_child_components( - block: SignedBeaconBlock, - valid_blobs: FixedVector>>, ::MaxBlobsPerBlock>, - invalid_blobs: FixedVector>>, ::MaxBlobsPerBlock>, - ) -> ChildComponentsSetup { - let blobs = FixedVector::from(valid_blobs.into_iter().cloned().collect::>()); - let invalid_blobs = - FixedVector::from(invalid_blobs.into_iter().cloned().collect::>()); - (Arc::new(block), blobs, invalid_blobs) - } - - pub fn assert_cache_consistent>(cache: V) { - if let Some(cached_block) = cache.get_cached_block() { - let cached_block_commitments = cached_block.get_commitments(); - for index in 0..E::max_blobs_per_block() { - let block_commitment = cached_block_commitments.get(index).copied(); - let blob_commitment_opt = cache.get_cached_blobs().get(index).unwrap(); - let blob_commitment = blob_commitment_opt.as_ref().map(|b| *b.get_commitment()); - assert_eq!(block_commitment, blob_commitment); - } - } else { - panic!("No cached block") - } - } - - pub fn assert_empty_blob_cache>(cache: V) { - for blob in cache.get_cached_blobs().iter() { - assert!(blob.is_none()); - } - } - - #[macro_export] - macro_rules! generate_tests { - ($module_name:ident, $type_name:ty, $block_field:ident, $blob_field:ident, $setup_fn:ident) => { - mod $module_name { - use super::*; - use types::Hash256; - - #[test] - fn valid_block_invalid_blobs_valid_blobs() { - let (block_commitments, blobs, random_blobs) = pre_setup(); - let (block_commitments, blobs, random_blobs) = - $setup_fn(block_commitments, blobs, random_blobs); - let block_root = Hash256::zero(); - let mut cache = <$type_name>::empty(block_root); - cache.merge_block(block_commitments); - cache.merge_blobs(random_blobs); - cache.merge_blobs(blobs); - - assert_cache_consistent(cache); - } - - #[test] - fn invalid_blobs_block_valid_blobs() { - let (block_commitments, blobs, random_blobs) = pre_setup(); - let (block_commitments, blobs, random_blobs) = - $setup_fn(block_commitments, blobs, random_blobs); - let block_root = Hash256::zero(); - let mut cache = <$type_name>::empty(block_root); - cache.merge_blobs(random_blobs); - cache.merge_block(block_commitments); - cache.merge_blobs(blobs); - - assert_cache_consistent(cache); - } - - #[test] - fn invalid_blobs_valid_blobs_block() { - let (block_commitments, blobs, random_blobs) = pre_setup(); - let (block_commitments, blobs, random_blobs) = - $setup_fn(block_commitments, blobs, random_blobs); - - let block_root = Hash256::zero(); - let mut cache = <$type_name>::empty(block_root); - cache.merge_blobs(random_blobs); - cache.merge_blobs(blobs); - cache.merge_block(block_commitments); - - assert_empty_blob_cache(cache); - } - - #[test] - fn block_valid_blobs_invalid_blobs() { - let (block_commitments, blobs, random_blobs) = pre_setup(); - let (block_commitments, blobs, random_blobs) = - $setup_fn(block_commitments, blobs, random_blobs); - - let block_root = Hash256::zero(); - let mut cache = <$type_name>::empty(block_root); - cache.merge_block(block_commitments); - cache.merge_blobs(blobs); - cache.merge_blobs(random_blobs); - - assert_cache_consistent(cache); - } - - #[test] - fn valid_blobs_block_invalid_blobs() { - let (block_commitments, blobs, random_blobs) = pre_setup(); - let (block_commitments, blobs, random_blobs) = - $setup_fn(block_commitments, blobs, random_blobs); - - let block_root = Hash256::zero(); - let mut cache = <$type_name>::empty(block_root); - cache.merge_blobs(blobs); - cache.merge_block(block_commitments); - cache.merge_blobs(random_blobs); - - assert_cache_consistent(cache); - } - - #[test] - fn valid_blobs_invalid_blobs_block() { - let (block_commitments, blobs, random_blobs) = pre_setup(); - let (block_commitments, blobs, random_blobs) = - $setup_fn(block_commitments, blobs, random_blobs); - - let block_root = Hash256::zero(); - let mut cache = <$type_name>::empty(block_root); - cache.merge_blobs(blobs); - cache.merge_blobs(random_blobs); - cache.merge_block(block_commitments); - - assert_cache_consistent(cache); - } - } - }; - } - - generate_tests!( - processing_components_tests, - ProcessingComponents::, - kzg_commitments, - processing_blobs, - setup_processing_components - ); - generate_tests!( - pending_components_tests, - PendingComponents, - executed_block, - verified_blobs, - setup_pending_components - ); - generate_tests!( - child_component_tests, - ChildComponents::, - downloaded_block, - downloaded_blobs, - setup_child_components - ); -} diff --git a/beacon_node/beacon_chain/src/data_availability_checker/child_components.rs b/beacon_node/beacon_chain/src/data_availability_checker/child_components.rs index 028bf9d67c..184dfc4500 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/child_components.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/child_components.rs @@ -1,9 +1,8 @@ use crate::block_verification_types::RpcBlock; -use crate::data_availability_checker::AvailabilityView; use bls::Hash256; use std::sync::Arc; use types::blob_sidecar::FixedBlobSidecarList; -use types::{EthSpec, SignedBeaconBlock}; +use types::{BlobSidecar, EthSpec, SignedBeaconBlock}; /// For requests triggered by an `UnknownBlockParent` or `UnknownBlobParent`, this struct /// is used to cache components as they are sent to the network service. We can't use the @@ -48,6 +47,22 @@ impl ChildComponents { cache } + pub fn merge_block(&mut self, block: Arc>) { + self.downloaded_block = Some(block); + } + + pub fn merge_blob(&mut self, blob: Arc>) { + if let Some(blob_ref) = self.downloaded_blobs.get_mut(blob.index as usize) { + *blob_ref = Some(blob); + } + } + + pub fn merge_blobs(&mut self, blobs: FixedBlobSidecarList) { + for blob in blobs.iter().flatten() { + self.merge_blob(blob.clone()); + } + } + pub fn clear_blobs(&mut self) { self.downloaded_blobs = FixedBlobSidecarList::default(); } diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index 34c9bc76f6..edd981e6dd 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -33,7 +33,6 @@ use crate::blob_verification::KzgVerifiedBlob; use crate::block_verification_types::{ AvailabilityPendingExecutedBlock, AvailableBlock, AvailableExecutedBlock, }; -use crate::data_availability_checker::availability_view::AvailabilityView; use crate::data_availability_checker::{Availability, AvailabilityCheckError}; use crate::store::{DBColumn, KeyValueStore}; use crate::BeaconChainTypes; @@ -52,13 +51,133 @@ use types::{BlobSidecar, ChainSpec, Epoch, EthSpec, Hash256}; /// The blobs are all gossip and kzg verified. /// The block has completed all verifications except the availability check. #[derive(Encode, Decode, Clone)] -pub struct PendingComponents { +pub struct PendingComponents { pub block_root: Hash256, - pub verified_blobs: FixedVector>, T::MaxBlobsPerBlock>, - pub executed_block: Option>, + pub verified_blobs: FixedVector>, E::MaxBlobsPerBlock>, + pub executed_block: Option>, } -impl PendingComponents { +impl PendingComponents { + /// Returns an immutable reference to the cached block. + pub fn get_cached_block(&self) -> &Option> { + &self.executed_block + } + + /// Returns an immutable reference to the fixed vector of cached blobs. + pub fn get_cached_blobs( + &self, + ) -> &FixedVector>, E::MaxBlobsPerBlock> { + &self.verified_blobs + } + + /// Returns a mutable reference to the cached block. + pub fn get_cached_block_mut(&mut self) -> &mut Option> { + &mut self.executed_block + } + + /// Returns a mutable reference to the fixed vector of cached blobs. + pub fn get_cached_blobs_mut( + &mut self, + ) -> &mut FixedVector>, E::MaxBlobsPerBlock> { + &mut self.verified_blobs + } + + /// Checks if a blob exists at the given index in the cache. + /// + /// Returns: + /// - `true` if a blob exists at the given index. + /// - `false` otherwise. + pub fn blob_exists(&self, blob_index: usize) -> bool { + self.get_cached_blobs() + .get(blob_index) + .map(|b| b.is_some()) + .unwrap_or(false) + } + + /// Returns the number of blobs that are expected to be present. Returns `None` if we don't have a + /// block. + /// + /// This corresponds to the number of commitments that are present in a block. + pub fn num_expected_blobs(&self) -> Option { + self.get_cached_block() + .as_ref() + .map(|b| b.get_commitments().len()) + } + + /// Returns the number of blobs that have been received and are stored in the cache. + pub fn num_received_blobs(&self) -> usize { + self.get_cached_blobs().iter().flatten().count() + } + + /// Inserts a block into the cache. + pub fn insert_block(&mut self, block: DietAvailabilityPendingExecutedBlock) { + *self.get_cached_block_mut() = Some(block) + } + + /// Inserts a blob at a specific index in the cache. + /// + /// Existing blob at the index will be replaced. + pub fn insert_blob_at_index(&mut self, blob_index: usize, blob: KzgVerifiedBlob) { + if let Some(b) = self.get_cached_blobs_mut().get_mut(blob_index) { + *b = Some(blob); + } + } + + /// Merges a given set of blobs into the cache. + /// + /// Blobs are only inserted if: + /// 1. The blob entry at the index is empty and no block exists. + /// 2. The block exists and its commitment matches the blob's commitment. + pub fn merge_blobs( + &mut self, + blobs: FixedVector>, E::MaxBlobsPerBlock>, + ) { + for (index, blob) in blobs.iter().cloned().enumerate() { + let Some(blob) = blob else { continue }; + self.merge_single_blob(index, blob); + } + } + + /// Merges a single blob into the cache. + /// + /// Blobs are only inserted if: + /// 1. The blob entry at the index is empty and no block exists, or + /// 2. The block exists and its commitment matches the blob's commitment. + pub fn merge_single_blob(&mut self, index: usize, blob: KzgVerifiedBlob) { + if let Some(cached_block) = self.get_cached_block() { + let block_commitment_opt = cached_block.get_commitments().get(index).copied(); + if let Some(block_commitment) = block_commitment_opt { + if block_commitment == *blob.get_commitment() { + self.insert_blob_at_index(index, blob) + } + } + } else if !self.blob_exists(index) { + self.insert_blob_at_index(index, blob) + } + } + + /// Inserts a new block and revalidates the existing blobs against it. + /// + /// Blobs that don't match the new block's commitments are evicted. + pub fn merge_block(&mut self, block: DietAvailabilityPendingExecutedBlock) { + self.insert_block(block); + let reinsert = std::mem::take(self.get_cached_blobs_mut()); + self.merge_blobs(reinsert); + } + + /// Checks if the block and all of its expected blobs are available in the cache. + /// + /// Returns `true` if both the block exists and the number of received blobs matches the number + /// of expected blobs. + pub fn is_available(&self) -> bool { + if let Some(num_expected_blobs) = self.num_expected_blobs() { + num_expected_blobs == self.num_received_blobs() + } else { + false + } + } + + /// Returns an empty `PendingComponents` object with the given block root. pub fn empty(block_root: Hash256) -> Self { Self { block_root, @@ -73,11 +192,11 @@ impl PendingComponents { /// /// WARNING: This function can potentially take a lot of time if the state needs to be /// reconstructed from disk. Ensure you are not holding any write locks while calling this. - pub fn make_available(self, recover: R) -> Result, AvailabilityCheckError> + pub fn make_available(self, recover: R) -> Result, AvailabilityCheckError> where R: FnOnce( - DietAvailabilityPendingExecutedBlock, - ) -> Result, AvailabilityCheckError>, + DietAvailabilityPendingExecutedBlock, + ) -> Result, AvailabilityCheckError>, { let Self { block_root, @@ -118,6 +237,7 @@ impl PendingComponents { ))) } + /// Returns the epoch of the block if it is cached, otherwise returns the epoch of the first blob. pub fn epoch(&self) -> Option { self.executed_block .as_ref() @@ -129,7 +249,7 @@ impl PendingComponents { kzg_verified_blob .as_blob() .slot() - .epoch(T::slots_per_epoch()) + .epoch(E::slots_per_epoch()) }); } } @@ -305,6 +425,11 @@ impl Critical { Ok(()) } + /// Returns true if the block root is known, without altering the LRU ordering + pub fn has_block(&self, block_root: &Hash256) -> bool { + self.in_memory.peek(block_root).is_some() || self.store_keys.contains(block_root) + } + /// This only checks for the blobs in memory pub fn peek_blob( &self, @@ -322,6 +447,13 @@ impl Critical { } } + pub fn peek_pending_components( + &self, + block_root: &Hash256, + ) -> Option<&PendingComponents> { + self.in_memory.peek(block_root) + } + /// Puts the pending components in the LRU cache. If the cache /// is at capacity, the LRU entry is written to the store first pub fn put_pending_components( @@ -363,6 +495,16 @@ impl Critical { } } } + + /// Returns the number of pending component entries in memory. + pub fn num_blocks(&self) -> usize { + self.in_memory.len() + } + + /// Returns the number of entries that have overflowed to disk. + pub fn num_store_entries(&self) -> usize { + self.store_keys.len() + } } /// This is the main struct for this module. Outside methods should @@ -399,6 +541,11 @@ impl OverflowLRUCache { }) } + /// Returns true if the block root is known, without altering the LRU ordering + pub fn has_block(&self, block_root: &Hash256) -> bool { + self.critical.read().has_block(block_root) + } + /// Fetch a blob from the cache without affecting the LRU ordering pub fn peek_blob( &self, @@ -415,6 +562,14 @@ impl OverflowLRUCache { } } + pub fn with_pending_components>) -> R>( + &self, + block_root: &Hash256, + f: F, + ) -> R { + f(self.critical.read().peek_pending_components(block_root)) + } + pub fn put_kzg_verified_blobs>>( &self, block_root: Hash256, @@ -671,6 +826,21 @@ impl OverflowLRUCache { pub fn state_lru_cache(&self) -> &StateLRUCache { &self.state_cache } + + /// Number of states stored in memory in the cache. + pub fn state_cache_size(&self) -> usize { + self.state_cache.lru_cache().read().len() + } + + /// Number of pending component entries in memory in the cache. + pub fn block_cache_size(&self) -> usize { + self.critical.read().num_blocks() + } + + /// Returns the number of entries in the cache that have overflowed to disk. + pub fn num_store_entries(&self) -> usize { + self.critical.read().num_store_entries() + } } impl ssz::Encode for OverflowKey { @@ -755,7 +925,7 @@ mod test { use store::{HotColdDB, ItemStore, LevelDB, StoreConfig}; use tempfile::{tempdir, TempDir}; use types::non_zero_usize::new_non_zero_usize; - use types::{ChainSpec, ExecPayload, MinimalEthSpec}; + use types::{ExecPayload, MinimalEthSpec}; const LOW_VALIDATOR_COUNT: usize = 32; @@ -1653,3 +1823,215 @@ mod test { ); } } + +#[cfg(test)] +mod pending_components_tests { + use super::*; + use crate::block_verification_types::BlockImportData; + use crate::eth1_finalization_cache::Eth1FinalizationData; + use crate::test_utils::{generate_rand_block_and_blobs, NumBlobs}; + use crate::PayloadVerificationOutcome; + use fork_choice::PayloadVerificationStatus; + use kzg::KzgCommitment; + use rand::rngs::StdRng; + use rand::SeedableRng; + use state_processing::ConsensusContext; + use types::test_utils::TestRandom; + use types::{BeaconState, ForkName, MainnetEthSpec, SignedBeaconBlock, Slot}; + + type E = MainnetEthSpec; + + type Setup = ( + SignedBeaconBlock, + FixedVector>>, ::MaxBlobsPerBlock>, + FixedVector>>, ::MaxBlobsPerBlock>, + ); + + pub fn pre_setup() -> Setup { + let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); + let (block, blobs_vec) = + generate_rand_block_and_blobs::(ForkName::Deneb, NumBlobs::Random, &mut rng); + let mut blobs: FixedVector<_, ::MaxBlobsPerBlock> = FixedVector::default(); + + for blob in blobs_vec { + if let Some(b) = blobs.get_mut(blob.index as usize) { + *b = Some(Arc::new(blob)); + } + } + + let mut invalid_blobs: FixedVector< + Option>>, + ::MaxBlobsPerBlock, + > = FixedVector::default(); + for (index, blob) in blobs.iter().enumerate() { + if let Some(invalid_blob) = blob { + let mut blob_copy = invalid_blob.as_ref().clone(); + blob_copy.kzg_commitment = KzgCommitment::random_for_test(&mut rng); + *invalid_blobs.get_mut(index).unwrap() = Some(Arc::new(blob_copy)); + } + } + + (block, blobs, invalid_blobs) + } + + type PendingComponentsSetup = ( + DietAvailabilityPendingExecutedBlock, + FixedVector>, ::MaxBlobsPerBlock>, + FixedVector>, ::MaxBlobsPerBlock>, + ); + + pub fn setup_pending_components( + block: SignedBeaconBlock, + valid_blobs: FixedVector>>, ::MaxBlobsPerBlock>, + invalid_blobs: FixedVector>>, ::MaxBlobsPerBlock>, + ) -> PendingComponentsSetup { + let blobs = FixedVector::from( + valid_blobs + .iter() + .map(|blob_opt| { + blob_opt + .as_ref() + .map(|blob| KzgVerifiedBlob::__assumed_valid(blob.clone())) + }) + .collect::>(), + ); + let invalid_blobs = FixedVector::from( + invalid_blobs + .iter() + .map(|blob_opt| { + blob_opt + .as_ref() + .map(|blob| KzgVerifiedBlob::__assumed_valid(blob.clone())) + }) + .collect::>(), + ); + let dummy_parent = block.clone_as_blinded(); + let block = AvailabilityPendingExecutedBlock { + block: Arc::new(block), + import_data: BlockImportData { + block_root: Default::default(), + state: BeaconState::new(0, Default::default(), &ChainSpec::minimal()), + parent_block: dummy_parent, + parent_eth1_finalization_data: Eth1FinalizationData { + eth1_data: Default::default(), + eth1_deposit_index: 0, + }, + confirmed_state_roots: vec![], + consensus_context: ConsensusContext::new(Slot::new(0)), + }, + payload_verification_outcome: PayloadVerificationOutcome { + payload_verification_status: PayloadVerificationStatus::Verified, + is_valid_merge_transition_block: false, + }, + }; + (block.into(), blobs, invalid_blobs) + } + + pub fn assert_cache_consistent(cache: PendingComponents) { + if let Some(cached_block) = cache.get_cached_block() { + let cached_block_commitments = cached_block.get_commitments(); + for index in 0..E::max_blobs_per_block() { + let block_commitment = cached_block_commitments.get(index).copied(); + let blob_commitment_opt = cache.get_cached_blobs().get(index).unwrap(); + let blob_commitment = blob_commitment_opt.as_ref().map(|b| *b.get_commitment()); + assert_eq!(block_commitment, blob_commitment); + } + } else { + panic!("No cached block") + } + } + + pub fn assert_empty_blob_cache(cache: PendingComponents) { + for blob in cache.get_cached_blobs().iter() { + assert!(blob.is_none()); + } + } + + #[test] + fn valid_block_invalid_blobs_valid_blobs() { + let (block_commitments, blobs, random_blobs) = pre_setup(); + let (block_commitments, blobs, random_blobs) = + setup_pending_components(block_commitments, blobs, random_blobs); + let block_root = Hash256::zero(); + let mut cache = >::empty(block_root); + cache.merge_block(block_commitments); + cache.merge_blobs(random_blobs); + cache.merge_blobs(blobs); + + assert_cache_consistent(cache); + } + + #[test] + fn invalid_blobs_block_valid_blobs() { + let (block_commitments, blobs, random_blobs) = pre_setup(); + let (block_commitments, blobs, random_blobs) = + setup_pending_components(block_commitments, blobs, random_blobs); + let block_root = Hash256::zero(); + let mut cache = >::empty(block_root); + cache.merge_blobs(random_blobs); + cache.merge_block(block_commitments); + cache.merge_blobs(blobs); + + assert_cache_consistent(cache); + } + + #[test] + fn invalid_blobs_valid_blobs_block() { + let (block_commitments, blobs, random_blobs) = pre_setup(); + let (block_commitments, blobs, random_blobs) = + setup_pending_components(block_commitments, blobs, random_blobs); + + let block_root = Hash256::zero(); + let mut cache = >::empty(block_root); + cache.merge_blobs(random_blobs); + cache.merge_blobs(blobs); + cache.merge_block(block_commitments); + + assert_empty_blob_cache(cache); + } + + #[test] + fn block_valid_blobs_invalid_blobs() { + let (block_commitments, blobs, random_blobs) = pre_setup(); + let (block_commitments, blobs, random_blobs) = + setup_pending_components(block_commitments, blobs, random_blobs); + + let block_root = Hash256::zero(); + let mut cache = >::empty(block_root); + cache.merge_block(block_commitments); + cache.merge_blobs(blobs); + cache.merge_blobs(random_blobs); + + assert_cache_consistent(cache); + } + + #[test] + fn valid_blobs_block_invalid_blobs() { + let (block_commitments, blobs, random_blobs) = pre_setup(); + let (block_commitments, blobs, random_blobs) = + setup_pending_components(block_commitments, blobs, random_blobs); + + let block_root = Hash256::zero(); + let mut cache = >::empty(block_root); + cache.merge_blobs(blobs); + cache.merge_block(block_commitments); + cache.merge_blobs(random_blobs); + + assert_cache_consistent(cache); + } + + #[test] + fn valid_blobs_invalid_blobs_block() { + let (block_commitments, blobs, random_blobs) = pre_setup(); + let (block_commitments, blobs, random_blobs) = + setup_pending_components(block_commitments, blobs, random_blobs); + + let block_root = Hash256::zero(); + let mut cache = >::empty(block_root); + cache.merge_blobs(blobs); + cache.merge_blobs(random_blobs); + cache.merge_block(block_commitments); + + assert_cache_consistent(cache); + } +} diff --git a/beacon_node/beacon_chain/src/data_availability_checker/processing_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/processing_cache.rs deleted file mode 100644 index 969034c657..0000000000 --- a/beacon_node/beacon_chain/src/data_availability_checker/processing_cache.rs +++ /dev/null @@ -1,74 +0,0 @@ -use crate::data_availability_checker::AvailabilityView; -use std::collections::hash_map::Entry; -use std::collections::HashMap; -use types::beacon_block_body::{KzgCommitmentOpts, KzgCommitments}; -use types::{EthSpec, Hash256, Slot}; - -/// This cache is used only for gossip blocks/blobs and single block/blob lookups, to give req/resp -/// a view of what we have and what we require. This cache serves a slightly different purpose than -/// gossip caches because it allows us to process duplicate blobs that are valid in gossip. -/// See `AvailabilityView`'s trait definition. -#[derive(Default)] -pub struct ProcessingCache { - processing_cache: HashMap>, -} - -impl ProcessingCache { - pub fn get(&self, block_root: &Hash256) -> Option<&ProcessingComponents> { - self.processing_cache.get(block_root) - } - pub fn entry(&mut self, block_root: Hash256) -> Entry<'_, Hash256, ProcessingComponents> { - self.processing_cache.entry(block_root) - } - pub fn remove(&mut self, block_root: &Hash256) { - self.processing_cache.remove(block_root); - } - pub fn has_block(&self, block_root: &Hash256) -> bool { - self.processing_cache - .get(block_root) - .map_or(false, |b| b.block_exists()) - } - pub fn incomplete_processing_components(&self, slot: Slot) -> Vec { - let mut roots_missing_components = vec![]; - for (&block_root, info) in self.processing_cache.iter() { - if info.slot == slot && !info.is_available() { - roots_missing_components.push(block_root); - } - } - roots_missing_components - } -} - -#[derive(Debug, Clone)] -pub struct ProcessingComponents { - slot: Slot, - /// Blobs required for a block can only be known if we have seen the block. So `Some` here - /// means we've seen it, a `None` means we haven't. The `kzg_commitments` value helps us figure - /// out whether incoming blobs actually match the block. - pub block_commitments: Option>, - /// `KzgCommitments` for blobs are always known, even if we haven't seen the block. See - /// `AvailabilityView`'s trait definition for more details. - pub blob_commitments: KzgCommitmentOpts, -} - -impl ProcessingComponents { - pub fn new(slot: Slot) -> Self { - Self { - slot, - block_commitments: None, - blob_commitments: KzgCommitmentOpts::::default(), - } - } -} - -// Not safe for use outside of tests as this always required a slot. -#[cfg(test)] -impl ProcessingComponents { - pub fn empty(_block_root: Hash256) -> Self { - Self { - slot: Slot::new(0), - block_commitments: None, - blob_commitments: KzgCommitmentOpts::::default(), - } - } -} diff --git a/beacon_node/beacon_chain/src/data_availability_checker/state_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/state_lru_cache.rs index bd125a7f42..c3492b53bd 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/state_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/state_lru_cache.rs @@ -10,6 +10,7 @@ use parking_lot::RwLock; use ssz_derive::{Decode, Encode}; use state_processing::{BlockReplayer, ConsensusContext, StateProcessingStrategy}; use std::sync::Arc; +use types::beacon_block_body::KzgCommitments; use types::{ssz_tagged_signed_beacon_block, ssz_tagged_signed_beacon_block_arc}; use types::{BeaconState, BlindedPayload, ChainSpec, Epoch, EthSpec, Hash256, SignedBeaconBlock}; @@ -42,6 +43,15 @@ impl DietAvailabilityPendingExecutedBlock { .blob_kzg_commitments() .map_or(0, |commitments| commitments.len()) } + + pub fn get_commitments(&self) -> KzgCommitments { + self.as_block() + .message() + .body() + .blob_kzg_commitments() + .cloned() + .unwrap_or_default() + } } /// This LRU cache holds BeaconStates used for block import. If the cache overflows, @@ -190,8 +200,7 @@ impl StateLRUCache { }) } - /// returns the state cache for inspection in tests - #[cfg(test)] + /// returns the state cache for inspection pub fn lru_cache(&self) -> &RwLock>> { &self.states } diff --git a/beacon_node/beacon_chain/src/early_attester_cache.rs b/beacon_node/beacon_chain/src/early_attester_cache.rs index da3c2c8a1e..79d732f51b 100644 --- a/beacon_node/beacon_chain/src/early_attester_cache.rs +++ b/beacon_node/beacon_chain/src/early_attester_cache.rs @@ -6,7 +6,6 @@ use crate::{ use parking_lot::RwLock; use proto_array::Block as ProtoBlock; use std::sync::Arc; -use types::blob_sidecar::BlobSidecarList; use types::*; pub struct CacheItem { diff --git a/beacon_node/beacon_chain/src/electra_readiness.rs b/beacon_node/beacon_chain/src/electra_readiness.rs new file mode 100644 index 0000000000..0e911bf37e --- /dev/null +++ b/beacon_node/beacon_chain/src/electra_readiness.rs @@ -0,0 +1,123 @@ +//! Provides tools for checking if a node is ready for the Electra upgrade and following merge +//! transition. + +use crate::{BeaconChain, BeaconChainTypes}; +use execution_layer::http::{ + ENGINE_FORKCHOICE_UPDATED_V3, ENGINE_GET_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V3, +}; +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::time::Duration; +use types::*; + +/// The time before the Electra fork when we will start issuing warnings about preparation. +use super::merge_readiness::SECONDS_IN_A_WEEK; +pub const ELECTRA_READINESS_PREPARATION_SECONDS: u64 = SECONDS_IN_A_WEEK * 2; +pub const ENGINE_CAPABILITIES_REFRESH_INTERVAL: u64 = 300; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[serde(tag = "type")] +pub enum ElectraReadiness { + /// The execution engine is electra-enabled (as far as we can tell) + Ready, + /// We are connected to an execution engine which doesn't support the V3 engine api methods + V3MethodsNotSupported { error: String }, + /// The transition configuration with the EL failed, there might be a problem with + /// connectivity, authentication or a difference in configuration. + ExchangeCapabilitiesFailed { error: String }, + /// The user has not configured an execution endpoint + NoExecutionEndpoint, +} + +impl fmt::Display for ElectraReadiness { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ElectraReadiness::Ready => { + write!(f, "This node appears ready for Electra.") + } + ElectraReadiness::ExchangeCapabilitiesFailed { error } => write!( + f, + "Could not exchange capabilities with the \ + execution endpoint: {}", + error + ), + ElectraReadiness::NoExecutionEndpoint => write!( + f, + "The --execution-endpoint flag is not specified, this is a \ + requirement post-merge" + ), + ElectraReadiness::V3MethodsNotSupported { error } => write!( + f, + "Execution endpoint does not support Electra methods: {}", + error + ), + } + } +} + +impl BeaconChain { + /// Returns `true` if electra epoch is set and Electra fork has occurred or will + /// occur within `ELECTRA_READINESS_PREPARATION_SECONDS` + pub fn is_time_to_prepare_for_electra(&self, current_slot: Slot) -> bool { + if let Some(electra_epoch) = self.spec.electra_fork_epoch { + let electra_slot = electra_epoch.start_slot(T::EthSpec::slots_per_epoch()); + let electra_readiness_preparation_slots = + ELECTRA_READINESS_PREPARATION_SECONDS / self.spec.seconds_per_slot; + // Return `true` if Electra has happened or is within the preparation time. + current_slot + electra_readiness_preparation_slots > electra_slot + } else { + // The Electra fork epoch has not been defined yet, no need to prepare. + false + } + } + + /// Attempts to connect to the EL and confirm that it is ready for electra. + pub async fn check_electra_readiness(&self) -> ElectraReadiness { + if let Some(el) = self.execution_layer.as_ref() { + match el + .get_engine_capabilities(Some(Duration::from_secs( + ENGINE_CAPABILITIES_REFRESH_INTERVAL, + ))) + .await + { + Err(e) => { + // The EL was either unreachable or responded with an error + ElectraReadiness::ExchangeCapabilitiesFailed { + error: format!("{:?}", e), + } + } + Ok(capabilities) => { + // TODO(electra): Update in the event we get V4s. + let mut missing_methods = String::from("Required Methods Unsupported:"); + let mut all_good = true; + if !capabilities.get_payload_v3 { + missing_methods.push(' '); + missing_methods.push_str(ENGINE_GET_PAYLOAD_V3); + all_good = false; + } + if !capabilities.forkchoice_updated_v3 { + missing_methods.push(' '); + missing_methods.push_str(ENGINE_FORKCHOICE_UPDATED_V3); + all_good = false; + } + if !capabilities.new_payload_v3 { + missing_methods.push(' '); + missing_methods.push_str(ENGINE_NEW_PAYLOAD_V3); + all_good = false; + } + + if all_good { + ElectraReadiness::Ready + } else { + ElectraReadiness::V3MethodsNotSupported { + error: missing_methods, + } + } + } + } + } else { + ElectraReadiness::NoExecutionEndpoint + } + } +} diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 9c1ba06f85..9c82e964cc 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -55,6 +55,7 @@ pub enum BeaconChainError { SlotClockDidNotStart, NoStateForSlot(Slot), BeaconStateError(BeaconStateError), + EpochCacheError(EpochCacheError), DBInconsistent(String), DBError(store::Error), ForkChoiceError(ForkChoiceError), @@ -250,6 +251,8 @@ easy_from_to!(StateAdvanceError, BeaconChainError); easy_from_to!(BlockReplayError, BeaconChainError); easy_from_to!(InconsistentFork, BeaconChainError); easy_from_to!(AvailabilityCheckError, BeaconChainError); +easy_from_to!(EpochCacheError, BeaconChainError); +easy_from_to!(LightClientError, BeaconChainError); #[derive(Debug)] pub enum BlockProductionError { @@ -258,6 +261,7 @@ pub enum BlockProductionError { UnableToProduceAtSlot(Slot), SlotProcessingError(SlotProcessingError), BlockProcessingError(BlockProcessingError), + EpochCacheError(EpochCacheError), ForkChoiceError(ForkChoiceError), Eth1ChainError(Eth1ChainError), BeaconStateError(BeaconStateError), @@ -297,3 +301,4 @@ easy_from_to!(SlotProcessingError, BlockProductionError); easy_from_to!(Eth1ChainError, BlockProductionError); easy_from_to!(StateAdvanceError, BlockProductionError); easy_from_to!(ForkChoiceError, BlockProductionError); +easy_from_to!(EpochCacheError, BlockProductionError); diff --git a/beacon_node/beacon_chain/src/eth1_chain.rs b/beacon_node/beacon_chain/src/eth1_chain.rs index 8b6c6b3740..3ec39f9d19 100644 --- a/beacon_node/beacon_chain/src/eth1_chain.rs +++ b/beacon_node/beacon_chain/src/eth1_chain.rs @@ -9,7 +9,6 @@ use ssz_derive::{Decode, Encode}; use state_processing::per_block_processing::get_new_eth1_data; use std::cmp::Ordering; use std::collections::HashMap; -use std::iter::DoubleEndedIterator; use std::marker::PhantomData; use std::time::{SystemTime, UNIX_EPOCH}; use store::{DBColumn, Error as StoreError, StoreItem}; @@ -67,7 +66,7 @@ impl From for Error { /// - `genesis_time`: beacon chain genesis time. /// - `current_slot`: current beacon chain slot. /// - `spec`: current beacon chain specification. -fn get_sync_status( +fn get_sync_status( latest_cached_block: Option<&Eth1Block>, head_block: Option<&Eth1Block>, genesis_time: u64, @@ -85,7 +84,7 @@ fn get_sync_status( // that are *before* genesis, so that we can indicate to users that we're actually adequately // cached for where they are in time. let voting_target_timestamp = if let Some(current_slot) = current_slot { - let period = T::SlotsPerEth1VotingPeriod::to_u64(); + let period = E::SlotsPerEth1VotingPeriod::to_u64(); let voting_period_start_slot = (current_slot / period) * period; let period_start = slot_start_seconds( @@ -98,7 +97,7 @@ fn get_sync_status( } else { // The number of seconds in an eth1 voting period. let voting_period_duration = - T::slots_per_eth1_voting_period() as u64 * spec.seconds_per_slot; + E::slots_per_eth1_voting_period() as u64 * spec.seconds_per_slot; let now = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs(); @@ -316,10 +315,10 @@ where } } -pub trait Eth1ChainBackend: Sized + Send + Sync { +pub trait Eth1ChainBackend: Sized + Send + Sync { /// Returns the `Eth1Data` that should be included in a block being produced for the given /// `state`. - fn eth1_data(&self, beacon_state: &BeaconState, spec: &ChainSpec) + fn eth1_data(&self, beacon_state: &BeaconState, spec: &ChainSpec) -> Result; /// Returns all `Deposits` between `state.eth1_deposit_index` and @@ -331,7 +330,7 @@ pub trait Eth1ChainBackend: Sized + Send + Sync { /// be more than `MAX_DEPOSIT_COUNT` or the churn may be too high. fn queued_deposits( &self, - beacon_state: &BeaconState, + beacon_state: &BeaconState, eth1_data_vote: &Eth1Data, spec: &ChainSpec, ) -> Result, Error>; @@ -365,13 +364,13 @@ pub trait Eth1ChainBackend: Sized + Send + Sync { /// Never creates deposits, therefore the validator set is static. /// /// This was used in the 2019 Canada interop workshops. -pub struct DummyEth1ChainBackend(PhantomData); +pub struct DummyEth1ChainBackend(PhantomData); -impl Eth1ChainBackend for DummyEth1ChainBackend { +impl Eth1ChainBackend for DummyEth1ChainBackend { /// Produce some deterministic junk based upon the current epoch. - fn eth1_data(&self, state: &BeaconState, _spec: &ChainSpec) -> Result { + fn eth1_data(&self, state: &BeaconState, _spec: &ChainSpec) -> Result { let current_epoch = state.current_epoch(); - let slots_per_voting_period = T::slots_per_eth1_voting_period() as u64; + let slots_per_voting_period = E::slots_per_eth1_voting_period() as u64; let current_voting_period: u64 = current_epoch.as_u64() / slots_per_voting_period; let deposit_root = hash(&int_to_bytes32(current_voting_period)); @@ -387,7 +386,7 @@ impl Eth1ChainBackend for DummyEth1ChainBackend { /// The dummy back-end never produces deposits. fn queued_deposits( &self, - _: &BeaconState, + _: &BeaconState, _: &Eth1Data, _: &ChainSpec, ) -> Result, Error> { @@ -420,7 +419,7 @@ impl Eth1ChainBackend for DummyEth1ChainBackend { } } -impl Default for DummyEth1ChainBackend { +impl Default for DummyEth1ChainBackend { fn default() -> Self { Self(PhantomData) } @@ -432,13 +431,13 @@ impl Default for DummyEth1ChainBackend { /// The `core` connects to some external eth1 client (e.g., Parity/Geth) and polls it for /// information. #[derive(Clone)] -pub struct CachingEth1Backend { +pub struct CachingEth1Backend { pub core: HttpService, log: Logger, - _phantom: PhantomData, + _phantom: PhantomData, } -impl CachingEth1Backend { +impl CachingEth1Backend { /// Instantiates `self` with empty caches. /// /// Does not connect to the eth1 node or start any tasks to keep the cache updated. @@ -466,9 +465,9 @@ impl CachingEth1Backend { } } -impl Eth1ChainBackend for CachingEth1Backend { - fn eth1_data(&self, state: &BeaconState, spec: &ChainSpec) -> Result { - let period = T::SlotsPerEth1VotingPeriod::to_u64(); +impl Eth1ChainBackend for CachingEth1Backend { + fn eth1_data(&self, state: &BeaconState, spec: &ChainSpec) -> Result { + let period = E::SlotsPerEth1VotingPeriod::to_u64(); let voting_period_start_slot = (state.slot() / period) * period; let voting_period_start_seconds = slot_start_seconds( state.genesis_time(), @@ -536,7 +535,7 @@ impl Eth1ChainBackend for CachingEth1Backend { fn queued_deposits( &self, - state: &BeaconState, + state: &BeaconState, eth1_data_vote: &Eth1Data, _spec: &ChainSpec, ) -> Result, Error> { @@ -552,7 +551,7 @@ impl Eth1ChainBackend for CachingEth1Backend { Ordering::Equal => Ok(vec![]), Ordering::Less => { let next = deposit_index; - let last = std::cmp::min(deposit_count, next + T::MaxDeposits::to_u64()); + let last = std::cmp::min(deposit_count, next + E::MaxDeposits::to_u64()); self.core .deposits() @@ -627,8 +626,8 @@ where /// Collect all valid votes that are cast during the current voting period. /// Return hashmap with count of each vote cast. -fn collect_valid_votes( - state: &BeaconState, +fn collect_valid_votes( + state: &BeaconState, votes_to_consider: &HashMap, ) -> Eth1DataVoteCount { let mut valid_votes = HashMap::new(); @@ -736,7 +735,7 @@ mod test { mod eth1_chain_json_backend { use super::*; use eth1::DepositLog; - use types::{test_utils::generate_deterministic_keypair, EthSpec, MainnetEthSpec}; + use types::{test_utils::generate_deterministic_keypair, MainnetEthSpec}; fn get_eth1_chain() -> Eth1Chain, E> { let eth1_config = Eth1Config { @@ -967,7 +966,7 @@ mod test { let spec = &E::default_spec(); let state: BeaconState = BeaconState::new(0, get_eth1_data(0), spec); - let blocks = vec![]; + let blocks = []; assert_eq!( get_votes_to_consider( diff --git a/beacon_node/beacon_chain/src/events.rs b/beacon_node/beacon_chain/src/events.rs index 0e5dfc8059..1fdcfdf8d0 100644 --- a/beacon_node/beacon_chain/src/events.rs +++ b/beacon_node/beacon_chain/src/events.rs @@ -6,24 +6,24 @@ use types::EthSpec; const DEFAULT_CHANNEL_CAPACITY: usize = 16; -pub struct ServerSentEventHandler { - attestation_tx: Sender>, - block_tx: Sender>, - blob_sidecar_tx: Sender>, - finalized_tx: Sender>, - head_tx: Sender>, - exit_tx: Sender>, - chain_reorg_tx: Sender>, - contribution_tx: Sender>, - payload_attributes_tx: Sender>, - late_head: Sender>, - light_client_finality_update_tx: Sender>, - light_client_optimistic_update_tx: Sender>, - block_reward_tx: Sender>, +pub struct ServerSentEventHandler { + attestation_tx: Sender>, + block_tx: Sender>, + blob_sidecar_tx: Sender>, + finalized_tx: Sender>, + head_tx: Sender>, + exit_tx: Sender>, + chain_reorg_tx: Sender>, + contribution_tx: Sender>, + payload_attributes_tx: Sender>, + late_head: Sender>, + light_client_finality_update_tx: Sender>, + light_client_optimistic_update_tx: Sender>, + block_reward_tx: Sender>, log: Logger, } -impl ServerSentEventHandler { +impl ServerSentEventHandler { pub fn new(log: Logger, capacity_multiplier: usize) -> Self { Self::new_with_capacity( log, @@ -64,7 +64,7 @@ impl ServerSentEventHandler { } } - pub fn register(&self, kind: EventKind) { + pub fn register(&self, kind: EventKind) { let log_count = |name, count| { trace!( self.log, @@ -132,55 +132,55 @@ impl ServerSentEventHandler { } } - pub fn subscribe_attestation(&self) -> Receiver> { + pub fn subscribe_attestation(&self) -> Receiver> { self.attestation_tx.subscribe() } - pub fn subscribe_block(&self) -> Receiver> { + pub fn subscribe_block(&self) -> Receiver> { self.block_tx.subscribe() } - pub fn subscribe_blob_sidecar(&self) -> Receiver> { + pub fn subscribe_blob_sidecar(&self) -> Receiver> { self.blob_sidecar_tx.subscribe() } - pub fn subscribe_finalized(&self) -> Receiver> { + pub fn subscribe_finalized(&self) -> Receiver> { self.finalized_tx.subscribe() } - pub fn subscribe_head(&self) -> Receiver> { + pub fn subscribe_head(&self) -> Receiver> { self.head_tx.subscribe() } - pub fn subscribe_exit(&self) -> Receiver> { + pub fn subscribe_exit(&self) -> Receiver> { self.exit_tx.subscribe() } - pub fn subscribe_reorgs(&self) -> Receiver> { + pub fn subscribe_reorgs(&self) -> Receiver> { self.chain_reorg_tx.subscribe() } - pub fn subscribe_contributions(&self) -> Receiver> { + pub fn subscribe_contributions(&self) -> Receiver> { self.contribution_tx.subscribe() } - pub fn subscribe_payload_attributes(&self) -> Receiver> { + pub fn subscribe_payload_attributes(&self) -> Receiver> { self.payload_attributes_tx.subscribe() } - pub fn subscribe_late_head(&self) -> Receiver> { + pub fn subscribe_late_head(&self) -> Receiver> { self.late_head.subscribe() } - pub fn subscribe_light_client_finality_update(&self) -> Receiver> { + pub fn subscribe_light_client_finality_update(&self) -> Receiver> { self.light_client_finality_update_tx.subscribe() } - pub fn subscribe_light_client_optimistic_update(&self) -> Receiver> { + pub fn subscribe_light_client_optimistic_update(&self) -> Receiver> { self.light_client_optimistic_update_tx.subscribe() } - pub fn subscribe_block_reward(&self) -> Receiver> { + pub fn subscribe_block_reward(&self) -> Receiver> { self.block_reward_tx.subscribe() } diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index e25976c2a5..b3804f0d23 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -81,14 +81,10 @@ impl PayloadNotifier { match notify_execution_layer { NotifyExecutionLayer::No if chain.config.optimistic_finalized_sync => { - // Verify the block hash here in Lighthouse and immediately mark the block as - // optimistically imported. This saves a lot of roundtrips to the EL. - let execution_layer = chain - .execution_layer - .as_ref() - .ok_or(ExecutionPayloadError::NoExecutionConnection)?; - - if let Err(e) = execution_layer.verify_payload_block_hash(block_message) { + // Create a NewPayloadRequest (no clones required) and check optimistic sync verifications + let new_payload_request: NewPayloadRequest = + block_message.try_into()?; + if let Err(e) = new_payload_request.perform_optimistic_sync_verifications() { warn!( chain.log, "Falling back to slow block hash verification"; @@ -143,11 +139,8 @@ async fn notify_new_payload<'a, T: BeaconChainTypes>( .as_ref() .ok_or(ExecutionPayloadError::NoExecutionConnection)?; - let new_payload_request: NewPayloadRequest = block.try_into()?; - let execution_block_hash = new_payload_request.block_hash(); - let new_payload_response = execution_layer - .notify_new_payload(new_payload_request) - .await; + let execution_block_hash = block.execution_payload()?.block_hash(); + let new_payload_response = execution_layer.notify_new_payload(block.try_into()?).await; match new_payload_response { Ok(status) => match status { @@ -419,7 +412,7 @@ pub fn get_execution_payload( let latest_execution_payload_header_block_hash = state.latest_execution_payload_header()?.block_hash(); let withdrawals = match state { - &BeaconState::Capella(_) | &BeaconState::Deneb(_) => { + &BeaconState::Capella(_) | &BeaconState::Deneb(_) | &BeaconState::Electra(_) => { Some(get_expected_withdrawals(state, spec)?.into()) } &BeaconState::Merge(_) => None, @@ -427,7 +420,7 @@ pub fn get_execution_payload( &BeaconState::Base(_) | &BeaconState::Altair(_) => None, }; let parent_beacon_block_root = match state { - BeaconState::Deneb(_) => Some(parent_block_root), + BeaconState::Deneb(_) | BeaconState::Electra(_) => Some(parent_block_root), BeaconState::Merge(_) | BeaconState::Capella(_) => None, // These shouldn't happen but they're here to make the pattern irrefutable BeaconState::Base(_) | BeaconState::Altair(_) => None, @@ -567,9 +560,6 @@ where parent_beacon_block_root, ); - // Note: the suggested_fee_recipient is stored in the `execution_layer`, it will add this parameter. - // - // This future is not executed here, it's up to the caller to await it. let block_contents = execution_layer .get_payload( parent_hash, diff --git a/beacon_node/beacon_chain/src/fork_revert.rs b/beacon_node/beacon_chain/src/fork_revert.rs index dc0e34277c..084ae95e09 100644 --- a/beacon_node/beacon_chain/src/fork_revert.rs +++ b/beacon_node/beacon_chain/src/fork_revert.rs @@ -10,10 +10,7 @@ use state_processing::{ use std::sync::Arc; use std::time::Duration; use store::{iter::ParentRootBlockIterator, HotColdDB, ItemStore}; -use types::{ - BeaconState, ChainSpec, EthSpec, ForkName, Hash256, ProgressiveBalancesMode, SignedBeaconBlock, - Slot, -}; +use types::{BeaconState, ChainSpec, EthSpec, ForkName, Hash256, 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."; @@ -103,8 +100,6 @@ pub fn reset_fork_choice_to_finalization, Cold: It store: Arc>, current_slot: Option, spec: &ChainSpec, - progressive_balances_mode: ProgressiveBalancesMode, - log: &Logger, ) -> Result, E>, String> { // Fetch finalized block. let finalized_checkpoint = head_state.finalized_checkpoint(); @@ -202,9 +197,7 @@ pub fn reset_fork_choice_to_finalization, Cold: It Duration::from_secs(0), &state, payload_verification_status, - progressive_balances_mode, spec, - log, ) .map_err(|e| format!("Error applying replayed block to fork choice: {:?}", e))?; } diff --git a/beacon_node/beacon_chain/src/kzg_utils.rs b/beacon_node/beacon_chain/src/kzg_utils.rs index 924cc26520..b554133875 100644 --- a/beacon_node/beacon_chain/src/kzg_utils.rs +++ b/beacon_node/beacon_chain/src/kzg_utils.rs @@ -3,71 +3,71 @@ use types::{Blob, EthSpec, Hash256, KzgCommitment, KzgProof}; /// Converts a blob ssz List object to an array to be used with the kzg /// crypto library. -fn ssz_blob_to_crypto_blob(blob: &Blob) -> Result { +fn ssz_blob_to_crypto_blob(blob: &Blob) -> Result { KzgBlob::from_bytes(blob.as_ref()).map_err(Into::into) } /// Validate a single blob-commitment-proof triplet from a `BlobSidecar`. -pub fn validate_blob( +pub fn validate_blob( kzg: &Kzg, - blob: &Blob, + blob: &Blob, kzg_commitment: KzgCommitment, kzg_proof: KzgProof, ) -> Result<(), KzgError> { let _timer = crate::metrics::start_timer(&crate::metrics::KZG_VERIFICATION_SINGLE_TIMES); - let kzg_blob = ssz_blob_to_crypto_blob::(blob)?; + let kzg_blob = ssz_blob_to_crypto_blob::(blob)?; kzg.verify_blob_kzg_proof(&kzg_blob, kzg_commitment, kzg_proof) } /// Validate a batch of blob-commitment-proof triplets from multiple `BlobSidecars`. -pub fn validate_blobs( +pub fn validate_blobs( kzg: &Kzg, expected_kzg_commitments: &[KzgCommitment], - blobs: Vec<&Blob>, + blobs: Vec<&Blob>, kzg_proofs: &[KzgProof], ) -> Result<(), KzgError> { let _timer = crate::metrics::start_timer(&crate::metrics::KZG_VERIFICATION_BATCH_TIMES); let blobs = blobs .into_iter() - .map(|blob| ssz_blob_to_crypto_blob::(blob)) + .map(|blob| ssz_blob_to_crypto_blob::(blob)) .collect::, KzgError>>()?; kzg.verify_blob_kzg_proof_batch(&blobs, expected_kzg_commitments, kzg_proofs) } /// Compute the kzg proof given an ssz blob and its kzg commitment. -pub fn compute_blob_kzg_proof( +pub fn compute_blob_kzg_proof( kzg: &Kzg, - blob: &Blob, + blob: &Blob, kzg_commitment: KzgCommitment, ) -> Result { - let kzg_blob = ssz_blob_to_crypto_blob::(blob)?; + let kzg_blob = ssz_blob_to_crypto_blob::(blob)?; kzg.compute_blob_kzg_proof(&kzg_blob, kzg_commitment) } /// Compute the kzg commitment for a given blob. -pub fn blob_to_kzg_commitment( +pub fn blob_to_kzg_commitment( kzg: &Kzg, - blob: &Blob, + blob: &Blob, ) -> Result { - let kzg_blob = ssz_blob_to_crypto_blob::(blob)?; + let kzg_blob = ssz_blob_to_crypto_blob::(blob)?; kzg.blob_to_kzg_commitment(&kzg_blob) } /// Compute the kzg proof for a given blob and an evaluation point z. -pub fn compute_kzg_proof( +pub fn compute_kzg_proof( kzg: &Kzg, - blob: &Blob, + blob: &Blob, z: Hash256, ) -> Result<(KzgProof, Hash256), KzgError> { let z = z.0.into(); - let kzg_blob = ssz_blob_to_crypto_blob::(blob)?; + let kzg_blob = ssz_blob_to_crypto_blob::(blob)?; kzg.compute_kzg_proof(&kzg_blob, &z) .map(|(proof, z)| (proof, Hash256::from_slice(&z.to_vec()))) } /// Verify a `kzg_proof` for a `kzg_commitment` that evaluating a polynomial at `z` results in `y` -pub fn verify_kzg_proof( +pub fn verify_kzg_proof( kzg: &Kzg, kzg_commitment: KzgCommitment, kzg_proof: KzgProof, diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 522009b1b2..71c96d0fd5 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -20,6 +20,7 @@ pub mod chain_config; pub mod data_availability_checker; pub mod deneb_readiness; mod early_attester_cache; +pub mod electra_readiness; mod errors; pub mod eth1_chain; mod eth1_finalization_cache; @@ -50,7 +51,7 @@ mod pre_finalization_cache; pub mod proposer_prep_service; pub mod schema_change; pub mod shuffling_cache; -mod snapshot_cache; +pub mod snapshot_cache; pub mod state_advance_timer; pub mod sync_committee_rewards; pub mod sync_committee_verification; @@ -85,7 +86,7 @@ pub use events::ServerSentEventHandler; pub use execution_layer::EngineState; pub use execution_payload::NotifyExecutionLayer; pub use fork_choice::{ExecutionStatus, ForkchoiceUpdateParameters}; -pub use kzg::TrustedSetup; +pub use kzg::{Kzg, TrustedSetup}; pub use metrics::scrape_for_metrics; pub use migrate::MigratorConfig; pub use parking_lot; 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 35863aa05f..879fa02f7d 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 @@ -48,7 +48,7 @@ impl VerifiedLightClientFinalityUpdate { // verify that enough time has passed for the block to have been propagated let start_time = chain .slot_clock - .start_of(rcv_finality_update.signature_slot) + .start_of(*rcv_finality_update.signature_slot()) .ok_or(Error::SigSlotStartIsNone)?; let one_third_slot_duration = Duration::new(chain.spec.seconds_per_slot / 3, 0); if seen_timestamp + chain.spec.maximum_gossip_clock_disparity() 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 813b112db5..5665adc3ed 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 @@ -52,7 +52,7 @@ impl VerifiedLightClientOptimisticUpdate { // verify that enough time has passed for the block to have been propagated let start_time = chain .slot_clock - .start_of(rcv_optimistic_update.signature_slot) + .start_of(*rcv_optimistic_update.signature_slot()) .ok_or(Error::SigSlotStartIsNone)?; let one_third_slot_duration = Duration::new(chain.spec.seconds_per_slot / 3, 0); if seen_timestamp + chain.spec.maximum_gossip_clock_disparity() @@ -65,10 +65,7 @@ impl VerifiedLightClientOptimisticUpdate { let head_block = &head.snapshot.beacon_block; // check if we can process the optimistic update immediately // otherwise queue - let canonical_root = rcv_optimistic_update - .attested_header - .beacon - .canonical_root(); + let canonical_root = rcv_optimistic_update.get_canonical_root(); if canonical_root != head_block.message().parent_root() { return Err(Error::UnknownBlockParentRoot(canonical_root)); @@ -84,7 +81,7 @@ impl VerifiedLightClientOptimisticUpdate { return Err(Error::InvalidLightClientOptimisticUpdate); } - let parent_root = rcv_optimistic_update.attested_header.beacon.parent_root; + let parent_root = rcv_optimistic_update.get_parent_root(); Ok(Self { light_client_optimistic_update: rcv_optimistic_update, parent_root, diff --git a/beacon_node/beacon_chain/src/light_client_server_cache.rs b/beacon_node/beacon_chain/src/light_client_server_cache.rs index 1397e3fc9d..ca02905737 100644 --- a/beacon_node/beacon_chain/src/light_client_server_cache.rs +++ b/beacon_node/beacon_chain/src/light_client_server_cache.rs @@ -8,7 +8,7 @@ use types::light_client_update::{FinalizedRootProofLen, FINALIZED_ROOT_INDEX}; use types::non_zero_usize::new_non_zero_usize; use types::{ BeaconBlockRef, BeaconState, ChainSpec, EthSpec, ForkName, Hash256, LightClientFinalityUpdate, - LightClientHeader, LightClientOptimisticUpdate, Slot, SyncAggregate, + LightClientOptimisticUpdate, Slot, SyncAggregate, }; /// A prev block cache miss requires to re-generate the state of the post-parent block. Items in the @@ -71,11 +71,12 @@ impl LightClientServerCache { /// results are cached either on disk or memory to be served via p2p and rest API pub fn recompute_and_cache_updates( &self, - log: &Logger, store: BeaconStore, block_parent_root: &Hash256, block_slot: Slot, sync_aggregate: &SyncAggregate, + log: &Logger, + chain_spec: &ChainSpec, ) -> Result<(), BeaconChainError> { let _timer = metrics::start_timer(&metrics::LIGHT_CLIENT_SERVER_CACHE_RECOMPUTE_UPDATES_TIMES); @@ -83,12 +84,13 @@ impl LightClientServerCache { let signature_slot = block_slot; let attested_block_root = block_parent_root; - let attested_block = store.get_blinded_block(attested_block_root)?.ok_or( - BeaconChainError::DBInconsistent(format!( - "Block not available {:?}", - attested_block_root - )), - )?; + let attested_block = + store + .get_full_block(attested_block_root)? + .ok_or(BeaconChainError::DBInconsistent(format!( + "Block not available {:?}", + attested_block_root + )))?; let cached_parts = self.get_or_compute_prev_block_cache( store.clone(), @@ -109,11 +111,12 @@ impl LightClientServerCache { }; if is_latest_optimistic { // can create an optimistic update, that is more recent - *self.latest_optimistic_update.write() = Some(LightClientOptimisticUpdate { - attested_header: block_to_light_client_header(attested_block.message()), - sync_aggregate: sync_aggregate.clone(), + *self.latest_optimistic_update.write() = Some(LightClientOptimisticUpdate::new( + &attested_block, + sync_aggregate.clone(), signature_slot, - }); + chain_spec, + )?); }; // Spec: Full nodes SHOULD provide the LightClientFinalityUpdate with the highest @@ -127,17 +130,16 @@ impl LightClientServerCache { if is_latest_finality & !cached_parts.finalized_block_root.is_zero() { // Immediately after checkpoint sync the finalized block may not be available yet. if let Some(finalized_block) = - store.get_blinded_block(&cached_parts.finalized_block_root)? + store.get_full_block(&cached_parts.finalized_block_root)? { - *self.latest_finality_update.write() = Some(LightClientFinalityUpdate { - // TODO: may want to cache this result from latest_optimistic_update if producing a - // light_client header becomes expensive - attested_header: block_to_light_client_header(attested_block.message()), - finalized_header: block_to_light_client_header(finalized_block.message()), - finality_branch: cached_parts.finality_branch.clone(), - sync_aggregate: sync_aggregate.clone(), + *self.latest_finality_update.write() = Some(LightClientFinalityUpdate::new( + &attested_block, + &finalized_block, + cached_parts.finality_branch.clone(), + sync_aggregate.clone(), signature_slot, - }); + chain_spec, + )?); } else { debug!( log, @@ -206,7 +208,7 @@ struct LightClientCachedData { } impl LightClientCachedData { - fn from_state(state: &mut BeaconState) -> Result { + fn from_state(state: &mut BeaconState) -> Result { Ok(Self { finality_branch: state.compute_merkle_proof(FINALIZED_ROOT_INDEX)?.into(), finalized_block_root: state.finalized_checkpoint().root, @@ -214,43 +216,36 @@ impl LightClientCachedData { } } -// Implements spec priorization rules: +// Implements spec prioritization rules: // > Full nodes SHOULD provide the LightClientFinalityUpdate with the highest attested_header.beacon.slot (if multiple, highest signature_slot) // // ref: https://github.com/ethereum/consensus-specs/blob/113c58f9bf9c08867f6f5f633c4d98e0364d612a/specs/altair/light-client/full-node.md#create_light_client_finality_update -fn is_latest_finality_update( - prev: &LightClientFinalityUpdate, +fn is_latest_finality_update( + prev: &LightClientFinalityUpdate, attested_slot: Slot, signature_slot: Slot, ) -> bool { - if attested_slot > prev.attested_header.beacon.slot { + let prev_slot = prev.get_attested_header_slot(); + if attested_slot > prev_slot { true } else { - attested_slot == prev.attested_header.beacon.slot && signature_slot > prev.signature_slot + attested_slot == prev_slot && signature_slot > *prev.signature_slot() } } -// Implements spec priorization rules: +// Implements spec prioritization rules: // > Full nodes SHOULD provide the LightClientOptimisticUpdate with the highest attested_header.beacon.slot (if multiple, highest signature_slot) // // ref: https://github.com/ethereum/consensus-specs/blob/113c58f9bf9c08867f6f5f633c4d98e0364d612a/specs/altair/light-client/full-node.md#create_light_client_optimistic_update -fn is_latest_optimistic_update( - prev: &LightClientOptimisticUpdate, +fn is_latest_optimistic_update( + prev: &LightClientOptimisticUpdate, attested_slot: Slot, signature_slot: Slot, ) -> bool { - if attested_slot > prev.attested_header.beacon.slot { + let prev_slot = prev.get_slot(); + if attested_slot > prev_slot { true } else { - attested_slot == prev.attested_header.beacon.slot && signature_slot > prev.signature_slot - } -} - -fn block_to_light_client_header( - block: BeaconBlockRef>, -) -> LightClientHeader { - // TODO: make fork aware - LightClientHeader { - beacon: block.block_header(), + attested_slot == prev_slot && signature_slot > *prev.signature_slot() } } diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index 24c05e01f2..6cb0b6fd76 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -304,6 +304,14 @@ lazy_static! { "Count of times the early attester cache returns an attestation" ); + pub static ref BEACON_REQRESP_PRE_IMPORT_CACHE_SIZE: Result = try_create_int_gauge( + "beacon_reqresp_pre_import_cache_size", + "Current count of items of the reqresp pre import cache" + ); + pub static ref BEACON_REQRESP_PRE_IMPORT_CACHE_HITS: Result = try_create_int_counter( + "beacon_reqresp_pre_import_cache_hits", + "Count of times the reqresp pre import cache returns an item" + ); } // Second lazy-static block is used to account for macro recursion limit. @@ -1129,6 +1137,25 @@ lazy_static! { Ok(vec![0.1, 0.2, 0.3,0.4,0.5,0.75,1.0,1.25,1.5,1.75,2.0,2.5,3.0,3.5,4.0,5.0,6.0,7.0,8.0,9.0,10.0,15.0,20.0]) ); + /* + * Data Availability cache metrics + */ + pub static ref DATA_AVAILABILITY_OVERFLOW_MEMORY_BLOCK_CACHE_SIZE: Result = + try_create_int_gauge( + "data_availability_overflow_memory_block_cache_size", + "Number of entries in the data availability overflow block memory cache." + ); + pub static ref DATA_AVAILABILITY_OVERFLOW_MEMORY_STATE_CACHE_SIZE: Result = + try_create_int_gauge( + "data_availability_overflow_memory_state_cache_size", + "Number of entries in the data availability overflow state memory cache." + ); + pub static ref DATA_AVAILABILITY_OVERFLOW_STORE_CACHE_SIZE: Result = + try_create_int_gauge( + "data_availability_overflow_store_cache_size", + "Number of entries in the data availability overflow store cache." + ); + /* * light_client server metrics */ @@ -1171,6 +1198,25 @@ pub fn scrape_for_metrics(beacon_chain: &BeaconChain) { ) } + set_gauge_by_usize( + &BEACON_REQRESP_PRE_IMPORT_CACHE_SIZE, + beacon_chain.reqresp_pre_import_cache.read().len(), + ); + + let da_checker_metrics = beacon_chain.data_availability_checker.metrics(); + set_gauge_by_usize( + &DATA_AVAILABILITY_OVERFLOW_MEMORY_BLOCK_CACHE_SIZE, + da_checker_metrics.block_cache_size, + ); + set_gauge_by_usize( + &DATA_AVAILABILITY_OVERFLOW_MEMORY_STATE_CACHE_SIZE, + da_checker_metrics.state_cache_size, + ); + set_gauge_by_usize( + &DATA_AVAILABILITY_OVERFLOW_STORE_CACHE_SIZE, + da_checker_metrics.num_store_entries, + ); + if let Some((size, num_lookups)) = beacon_chain.pre_finalization_block_cache.metrics() { set_gauge_by_usize(&PRE_FINALIZATION_BLOCK_CACHE_SIZE, size); set_gauge_by_usize(&PRE_FINALIZATION_BLOCK_LOOKUP_COUNT, num_lookups); @@ -1212,7 +1258,7 @@ pub fn scrape_for_metrics(beacon_chain: &BeaconChain) { } /// Scrape the given `state` assuming it's the head state, updating the `DEFAULT_REGISTRY`. -fn scrape_head_state(state: &BeaconState, state_root: Hash256) { +fn scrape_head_state(state: &BeaconState, state_root: Hash256) { set_gauge_by_slot(&HEAD_STATE_SLOT, state.slot()); set_gauge_by_slot(&HEAD_STATE_SLOT_INTEROP, state.slot()); set_gauge_by_hash(&HEAD_STATE_ROOT, state_root); diff --git a/beacon_node/beacon_chain/src/observed_aggregates.rs b/beacon_node/beacon_chain/src/observed_aggregates.rs index aa513da547..ab00aefcd3 100644 --- a/beacon_node/beacon_chain/src/observed_aggregates.rs +++ b/beacon_node/beacon_chain/src/observed_aggregates.rs @@ -35,7 +35,7 @@ pub trait Consts { fn max_per_slot_capacity() -> usize; } -impl Consts for Attestation { +impl Consts for Attestation { /// Use 128 as it's the target committee size for the mainnet spec. This is perhaps a little /// wasteful for the minimal spec, but considering it's approx. 128 * 32 bytes we're not wasting /// much. @@ -43,7 +43,7 @@ impl Consts for Attestation { /// We need to keep attestations for each slot of the current epoch. fn max_slot_capacity() -> usize { - 2 * T::slots_per_epoch() as usize + 2 * E::slots_per_epoch() as usize } /// As a DoS protection measure, the maximum number of distinct `Attestations` or @@ -62,7 +62,7 @@ impl Consts for Attestation { } } -impl Consts for SyncCommitteeContribution { +impl Consts for SyncCommitteeContribution { /// Set to `TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE * SYNC_COMMITTEE_SUBNET_COUNT`. This is the /// expected number of aggregators per slot across all subcommittees. const DEFAULT_PER_SLOT_CAPACITY: usize = @@ -75,7 +75,7 @@ impl Consts for SyncCommitteeContribution { /// We should never receive more aggregates than there are sync committee participants. fn max_per_slot_capacity() -> usize { - T::sync_committee_size() + E::sync_committee_size() } } @@ -102,8 +102,8 @@ pub trait SubsetItem { fn root(&self) -> Hash256; } -impl SubsetItem for Attestation { - type Item = BitList; +impl SubsetItem for Attestation { + type Item = BitList; fn is_subset(&self, other: &Self::Item) -> bool { self.aggregation_bits.is_subset(other) } @@ -123,8 +123,8 @@ impl SubsetItem for Attestation { } } -impl SubsetItem for SyncCommitteeContribution { - type Item = BitVector; +impl SubsetItem for SyncCommitteeContribution { + type Item = BitVector; fn is_subset(&self, other: &Self::Item) -> bool { self.aggregation_bits.is_subset(other) } diff --git a/beacon_node/beacon_chain/src/observed_blob_sidecars.rs b/beacon_node/beacon_chain/src/observed_blob_sidecars.rs index 148d85befb..7d7f490ebb 100644 --- a/beacon_node/beacon_chain/src/observed_blob_sidecars.rs +++ b/beacon_node/beacon_chain/src/observed_blob_sidecars.rs @@ -27,11 +27,11 @@ pub enum Error { /// /// Note: To prevent DoS attacks, this cache must include only items that have received some DoS resistance /// like checking the proposer signature. -pub struct ObservedBlobSidecars { +pub struct ObservedBlobSidecars { finalized_slot: Slot, /// Stores all received blob indices for a given `(ValidatorIndex, Slot)` tuple. items: HashMap>, - _phantom: PhantomData, + _phantom: PhantomData, } impl Default for ObservedBlobSidecars { @@ -45,12 +45,12 @@ impl Default for ObservedBlobSidecars { } } -impl ObservedBlobSidecars { +impl ObservedBlobSidecars { /// Observe the `blob_sidecar` at (`blob_sidecar.block_proposer_index, blob_sidecar.slot`). /// This will update `self` so future calls to it indicate that this `blob_sidecar` is known. /// /// The supplied `blob_sidecar` **MUST** have completed proposer signature verification. - pub fn observe_sidecar(&mut self, blob_sidecar: &BlobSidecar) -> Result { + pub fn observe_sidecar(&mut self, blob_sidecar: &BlobSidecar) -> Result { self.sanitize_blob_sidecar(blob_sidecar)?; let blob_indices = self @@ -59,14 +59,14 @@ impl ObservedBlobSidecars { slot: blob_sidecar.slot(), proposer: blob_sidecar.block_proposer_index(), }) - .or_insert_with(|| HashSet::with_capacity(T::max_blobs_per_block())); + .or_insert_with(|| HashSet::with_capacity(E::max_blobs_per_block())); let did_not_exist = blob_indices.insert(blob_sidecar.index); Ok(!did_not_exist) } /// Returns `true` if the `blob_sidecar` has already been observed in the cache within the prune window. - pub fn proposer_is_known(&self, blob_sidecar: &BlobSidecar) -> Result { + pub fn proposer_is_known(&self, blob_sidecar: &BlobSidecar) -> Result { self.sanitize_blob_sidecar(blob_sidecar)?; let is_known = self .items @@ -80,8 +80,8 @@ impl ObservedBlobSidecars { Ok(is_known) } - fn sanitize_blob_sidecar(&self, blob_sidecar: &BlobSidecar) -> Result<(), Error> { - if blob_sidecar.index >= T::max_blobs_per_block() as u64 { + fn sanitize_blob_sidecar(&self, blob_sidecar: &BlobSidecar) -> Result<(), Error> { + if blob_sidecar.index >= E::max_blobs_per_block() as u64 { return Err(Error::InvalidBlobIndex(blob_sidecar.index)); } let finalized_slot = self.finalized_slot; @@ -111,7 +111,7 @@ mod tests { use super::*; use bls::Hash256; use std::sync::Arc; - use types::{BlobSidecar, MainnetEthSpec}; + use types::MainnetEthSpec; type E = MainnetEthSpec; diff --git a/beacon_node/beacon_chain/src/observed_operations.rs b/beacon_node/beacon_chain/src/observed_operations.rs index 4121111b3e..04861fbe31 100644 --- a/beacon_node/beacon_chain/src/observed_operations.rs +++ b/beacon_node/beacon_chain/src/observed_operations.rs @@ -153,6 +153,11 @@ impl, E: EthSpec> ObservedOperations { self.current_fork = head_fork; } } + + /// Reset the cache. MUST ONLY BE USED IN TESTS. + pub fn __reset_for_testing_only(&mut self) { + self.observed_validator_indices.clear(); + } } impl + VerifyOperationAt, E: EthSpec> ObservedOperations { diff --git a/beacon_node/beacon_chain/src/shuffling_cache.rs b/beacon_node/beacon_chain/src/shuffling_cache.rs index 086e1c0949..b3de6f91c9 100644 --- a/beacon_node/beacon_chain/src/shuffling_cache.rs +++ b/beacon_node/beacon_chain/src/shuffling_cache.rs @@ -268,9 +268,9 @@ impl BlockShufflingIds { } } - pub fn try_from_head( + pub fn try_from_head( head_block_root: Hash256, - head_state: &BeaconState, + head_state: &BeaconState, ) -> Result { let get_shuffling_id = |relative_epoch| { AttestationShufflingId::new(head_block_root, head_state, relative_epoch).map_err(|e| { diff --git a/beacon_node/beacon_chain/src/snapshot_cache.rs b/beacon_node/beacon_chain/src/snapshot_cache.rs index d2846c0856..ac4e71d3d5 100644 --- a/beacon_node/beacon_chain/src/snapshot_cache.rs +++ b/beacon_node/beacon_chain/src/snapshot_cache.rs @@ -9,7 +9,7 @@ use types::{ }; /// The default size of the cache. -pub const DEFAULT_SNAPSHOT_CACHE_SIZE: usize = 4; +pub const DEFAULT_SNAPSHOT_CACHE_SIZE: usize = 3; /// The minimum block delay to clone the state in the cache instead of removing it. /// This helps keep block processing fast during re-orgs from late blocks. @@ -20,19 +20,19 @@ fn minimum_block_delay_for_clone(seconds_per_slot: u64) -> Duration { /// This snapshot is to be used for verifying a child of `self.beacon_block`. #[derive(Debug)] -pub struct PreProcessingSnapshot { +pub struct PreProcessingSnapshot { /// This state is equivalent to the `self.beacon_block.state_root()` state that has been /// advanced forward one slot using `per_slot_processing`. This state is "primed and ready" for /// the application of another block. - pub pre_state: BeaconState, + pub pre_state: BeaconState, /// This value is only set to `Some` if the `pre_state` was *not* advanced forward. pub beacon_state_root: Option, - pub beacon_block: SignedBeaconBlock>, + pub beacon_block: SignedBeaconBlock>, pub beacon_block_root: Hash256, } -impl From> for PreProcessingSnapshot { - fn from(snapshot: BeaconSnapshot) -> Self { +impl From> for PreProcessingSnapshot { + fn from(snapshot: BeaconSnapshot) -> Self { let beacon_state_root = Some(snapshot.beacon_state_root()); Self { pre_state: snapshot.beacon_state, @@ -43,8 +43,8 @@ impl From> for PreProcessingSnapshot { } } -impl CacheItem { - pub fn new_without_pre_state(snapshot: BeaconSnapshot) -> Self { +impl CacheItem { + pub fn new_without_pre_state(snapshot: BeaconSnapshot) -> Self { Self { beacon_block: snapshot.beacon_block, beacon_block_root: snapshot.beacon_block_root, @@ -53,7 +53,7 @@ impl CacheItem { } } - fn clone_to_snapshot_with(&self, clone_config: CloneConfig) -> BeaconSnapshot { + fn clone_to_snapshot_with(&self, clone_config: CloneConfig) -> BeaconSnapshot { BeaconSnapshot { beacon_state: self.beacon_state.clone_with(clone_config), beacon_block: self.beacon_block.clone(), @@ -61,7 +61,7 @@ impl CacheItem { } } - pub fn into_pre_state(self) -> PreProcessingSnapshot { + pub fn into_pre_state(self) -> PreProcessingSnapshot { // Do not include the beacon state root if the state has been advanced. let beacon_state_root = Some(self.beacon_block.state_root()).filter(|_| self.pre_state.is_none()); @@ -74,7 +74,7 @@ impl CacheItem { } } - pub fn clone_as_pre_state(&self) -> PreProcessingSnapshot { + pub fn clone_as_pre_state(&self) -> PreProcessingSnapshot { // Do not include the beacon state root if the state has been advanced. let beacon_state_root = Some(self.beacon_block.state_root()).filter(|_| self.pre_state.is_none()); @@ -92,11 +92,11 @@ impl CacheItem { } /// The information required for block production. -pub struct BlockProductionPreState { +pub struct BlockProductionPreState { /// This state may or may not have been advanced forward a single slot. /// /// See the documentation in the `crate::state_advance_timer` module for more information. - pub pre_state: BeaconState, + pub pre_state: BeaconState, /// This value will only be `Some` if `self.pre_state` was **not** advanced forward a single /// slot. /// @@ -105,32 +105,32 @@ pub struct BlockProductionPreState { pub state_root: Option, } -pub enum StateAdvance { +pub enum StateAdvance { /// The cache does not contain the supplied block root. BlockNotFound, /// The cache contains the supplied block root but the state has already been advanced. AlreadyAdvanced, /// The cache contains the supplied block root and the state has not yet been advanced. State { - state: Box>, + state: Box>, state_root: Hash256, block_slot: Slot, }, } /// The item stored in the `SnapshotCache`. -pub struct CacheItem { - beacon_block: Arc>, +pub struct CacheItem { + beacon_block: Arc>, beacon_block_root: Hash256, /// This state is equivalent to `self.beacon_block.state_root()`. - beacon_state: BeaconState, + beacon_state: BeaconState, /// This state is equivalent to `self.beacon_state` that has had `per_slot_processing` applied /// to it. This state assists in optimizing block processing. - pre_state: Option>, + pre_state: Option>, } -impl Into> for CacheItem { - fn into(self) -> BeaconSnapshot { +impl Into> for CacheItem { + fn into(self) -> BeaconSnapshot { BeaconSnapshot { beacon_state: self.beacon_state, beacon_block: self.beacon_block, @@ -151,17 +151,17 @@ impl Into> for CacheItem { /// /// - Never be the `head_block_root`. /// - Be the snapshot with the lowest `state.slot` (ties broken arbitrarily). -pub struct SnapshotCache { +pub struct SnapshotCache { max_len: usize, head_block_root: Hash256, - snapshots: Vec>, + snapshots: Vec>, } -impl SnapshotCache { +impl SnapshotCache { /// Instantiate a new cache which contains the `head` snapshot. /// /// Setting `max_len = 0` is equivalent to setting `max_len = 1`. - pub fn new(max_len: usize, head: BeaconSnapshot) -> Self { + pub fn new(max_len: usize, head: BeaconSnapshot) -> Self { Self { max_len: cmp::max(max_len, 1), head_block_root: head.beacon_block_root, @@ -174,6 +174,7 @@ impl SnapshotCache { self.snapshots.iter().map(|s| s.beacon_block_root).collect() } + #[allow(clippy::len_without_is_empty)] /// The number of snapshots contained in `self`. pub fn len(&self) -> usize { self.snapshots.len() @@ -183,8 +184,8 @@ impl SnapshotCache { /// struct-level documentation for more info). pub fn insert( &mut self, - snapshot: BeaconSnapshot, - pre_state: Option>, + snapshot: BeaconSnapshot, + pre_state: Option>, spec: &ChainSpec, ) { let parent_root = snapshot.beacon_block.message().parent_root(); @@ -251,7 +252,7 @@ impl SnapshotCache { block_slot: Slot, block_delay: Option, spec: &ChainSpec, - ) -> Option<(PreProcessingSnapshot, bool)> { + ) -> Option<(PreProcessingSnapshot, bool)> { self.snapshots .iter() .position(|snapshot| snapshot.beacon_block_root == block_root) @@ -282,7 +283,7 @@ impl SnapshotCache { pub fn get_state_for_block_production( &self, block_root: Hash256, - ) -> Option> { + ) -> Option> { self.snapshots .iter() .find(|snapshot| snapshot.beacon_block_root == block_root) @@ -306,14 +307,14 @@ impl SnapshotCache { &self, block_root: Hash256, clone_config: CloneConfig, - ) -> Option> { + ) -> Option> { self.snapshots .iter() .find(|snapshot| snapshot.beacon_block_root == block_root) .map(|snapshot| snapshot.clone_to_snapshot_with(clone_config)) } - pub fn get_for_state_advance(&mut self, block_root: Hash256) -> StateAdvance { + pub fn get_for_state_advance(&mut self, block_root: Hash256) -> StateAdvance { if let Some(snapshot) = self .snapshots .iter_mut() @@ -337,7 +338,7 @@ impl SnapshotCache { } } - pub fn update_pre_state(&mut self, block_root: Hash256, state: BeaconState) -> Option<()> { + pub fn update_pre_state(&mut self, block_root: Hash256, state: BeaconState) -> Option<()> { self.snapshots .iter_mut() .find(|snapshot| snapshot.beacon_block_root == block_root) @@ -349,7 +350,7 @@ impl SnapshotCache { /// Removes all snapshots from the queue that are less than or equal to the finalized epoch. pub fn prune(&mut self, finalized_epoch: Epoch) { self.snapshots.retain(|snapshot| { - snapshot.beacon_state.slot() > finalized_epoch.start_slot(T::slots_per_epoch()) + snapshot.beacon_state.slot() > finalized_epoch.start_slot(E::slots_per_epoch()) }) } @@ -366,10 +367,7 @@ impl SnapshotCache { mod test { use super::*; use crate::test_utils::{BeaconChainHarness, EphemeralHarnessType}; - use types::{ - test_utils::generate_deterministic_keypair, BeaconBlock, Epoch, MainnetEthSpec, - SignedBeaconBlock, Slot, - }; + use types::{test_utils::generate_deterministic_keypair, BeaconBlock, MainnetEthSpec}; fn get_harness() -> BeaconChainHarness> { let harness = BeaconChainHarness::builder(MainnetEthSpec) diff --git a/beacon_node/beacon_chain/src/state_advance_timer.rs b/beacon_node/beacon_chain/src/state_advance_timer.rs index c04815ebc1..39d35f8111 100644 --- a/beacon_node/beacon_chain/src/state_advance_timer.rs +++ b/beacon_node/beacon_chain/src/state_advance_timer.rs @@ -51,7 +51,8 @@ const MAX_BLOCK_PRODUCTION_CACHE_DISTANCE: u64 = 4; #[derive(Debug)] enum Error { BeaconChain(BeaconChainError), - HeadMissingFromSnapshotCache(Hash256), + // We don't use the inner value directly, but it's used in the Debug impl. + HeadMissingFromSnapshotCache(#[allow(dead_code)] Hash256), MaxDistanceExceeded { current_slot: Slot, head_slot: Slot, diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 6b85c8e493..542262487a 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -31,6 +31,7 @@ use futures::channel::mpsc::Receiver; pub use genesis::{interop_genesis_state_with_eth1, DEFAULT_ETH1_BLOCK_HASH}; use int_to_bytes::int_to_bytes32; use kzg::{Kzg, TrustedSetup}; +use lazy_static::lazy_static; use merkle_proof::MerkleTree; use operation_pool::ReceivedPreCapella; use parking_lot::Mutex; @@ -61,7 +62,6 @@ use task_executor::TaskExecutor; use task_executor::{test_utils::TestRuntime, ShutdownReason}; use tree_hash::TreeHash; use types::payload::BlockProductionVersion; -use types::sync_selection_proof::SyncSelectionProof; pub use types::test_utils::generate_deterministic_keypairs; use types::test_utils::TestRandom; use types::{typenum::U4294967296, *}; @@ -77,8 +77,18 @@ pub const FORK_NAME_ENV_VAR: &str = "FORK_NAME"; // a different value. pub const DEFAULT_TARGET_AGGREGATORS: u64 = u64::MAX; -pub type BaseHarnessType = - Witness, TEthSpec, THotStore, TColdStore>; +lazy_static! { + pub static ref KZG: Arc = { + let trusted_setup: TrustedSetup = serde_json::from_reader(TRUSTED_SETUP_BYTES) + .map_err(|e| format!("Unable to read trusted setup file: {}", e)) + .expect("should have trusted setup"); + let kzg = Kzg::new_from_trusted_setup(trusted_setup).expect("should create kzg"); + Arc::new(kzg) + }; +} + +pub type BaseHarnessType = + Witness, E, THotStore, TColdStore>; pub type DiskHarnessType = BaseHarnessType, LevelDB>; pub type EphemeralHarnessType = BaseHarnessType, MemoryStore>; @@ -459,6 +469,10 @@ where mock.server.execution_block_generator().cancun_time = spec.deneb_fork_epoch.map(|epoch| { genesis_time + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() }); + mock.server.execution_block_generator().prague_time = + spec.electra_fork_epoch.map(|epoch| { + genesis_time + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() + }); self } @@ -502,16 +516,14 @@ where let validator_keypairs = self .validator_keypairs .expect("cannot build without validator keypairs"); - let trusted_setup: TrustedSetup = serde_json::from_reader(TRUSTED_SETUP_BYTES) - .map_err(|e| format!("Unable to read trusted setup file: {}", e)) - .unwrap(); + let kzg = spec.deneb_fork_epoch.map(|_| KZG.clone()); let validator_monitor_config = self.validator_monitor_config.unwrap_or_default(); let chain_config = self.chain_config.unwrap_or_default(); let mut builder = BeaconChainBuilder::new(self.eth_spec_instance) .logger(log.clone()) - .custom_spec(spec) + .custom_spec(spec.clone()) .store(self.store.expect("cannot build without store")) .store_migrator_config( MigratorConfig::default() @@ -529,7 +541,7 @@ where 5, ))) .validator_monitor_config(validator_monitor_config) - .trusted_setup(trusted_setup); + .kzg(kzg); builder = if let Some(mutator) = self.initial_mutator { mutator(builder) @@ -570,30 +582,31 @@ where } } -pub fn mock_execution_layer_from_parts( +pub fn mock_execution_layer_from_parts( spec: &ChainSpec, task_executor: TaskExecutor, -) -> MockExecutionLayer { +) -> MockExecutionLayer { let shanghai_time = spec.capella_fork_epoch.map(|epoch| { - HARNESS_GENESIS_TIME + spec.seconds_per_slot * T::slots_per_epoch() * epoch.as_u64() + HARNESS_GENESIS_TIME + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() }); let cancun_time = spec.deneb_fork_epoch.map(|epoch| { - HARNESS_GENESIS_TIME + spec.seconds_per_slot * T::slots_per_epoch() * epoch.as_u64() + HARNESS_GENESIS_TIME + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() + }); + let prague_time = spec.electra_fork_epoch.map(|epoch| { + HARNESS_GENESIS_TIME + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() }); - let trusted_setup: TrustedSetup = serde_json::from_reader(TRUSTED_SETUP_BYTES) - .map_err(|e| format!("Unable to read trusted setup file: {}", e)) - .expect("should have trusted setup"); - let kzg = Kzg::new_from_trusted_setup(trusted_setup).expect("should create kzg"); + let kzg_opt = spec.deneb_fork_epoch.map(|_| KZG.clone()); MockExecutionLayer::new( task_executor, DEFAULT_TERMINAL_BLOCK, shanghai_time, cancun_time, + prague_time, Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()), spec.clone(), - Some(kzg), + kzg_opt, ) } @@ -878,7 +891,9 @@ where | SignedBeaconBlock::Altair(_) | SignedBeaconBlock::Merge(_) | SignedBeaconBlock::Capella(_) => (signed_block, None), - SignedBeaconBlock::Deneb(_) => (signed_block, block_response.blob_items), + SignedBeaconBlock::Deneb(_) | SignedBeaconBlock::Electra(_) => { + (signed_block, block_response.blob_items) + } }; (block_contents, block_response.state) @@ -940,7 +955,9 @@ where | SignedBeaconBlock::Altair(_) | SignedBeaconBlock::Merge(_) | SignedBeaconBlock::Capella(_) => (signed_block, None), - SignedBeaconBlock::Deneb(_) => (signed_block, block_response.blob_items), + SignedBeaconBlock::Deneb(_) | SignedBeaconBlock::Electra(_) => { + (signed_block, block_response.blob_items) + } }; (block_contents, pre_state) } @@ -2487,6 +2504,7 @@ pub fn build_log(level: slog::Level, enabled: bool) -> Logger { pub enum NumBlobs { Random, + Number(usize), None, } @@ -2498,48 +2516,75 @@ pub fn generate_rand_block_and_blobs( let inner = map_fork_name!(fork_name, BeaconBlock, <_>::random_for_test(rng)); let mut block = SignedBeaconBlock::from_block(inner, types::Signature::random_for_test(rng)); let mut blob_sidecars = vec![]; - if let Ok(message) = block.message_deneb_mut() { - // Get either zero blobs or a random number of blobs between 1 and Max Blobs. - let payload: &mut FullPayloadDeneb = &mut message.body.execution_payload; - let num_blobs = match num_blobs { - NumBlobs::Random => rng.gen_range(1..=E::max_blobs_per_block()), - NumBlobs::None => 0, - }; - let (bundle, transactions) = - execution_layer::test_utils::generate_blobs::(num_blobs).unwrap(); - payload.execution_payload.transactions = <_>::default(); - for tx in Vec::from(transactions) { - payload.execution_payload.transactions.push(tx).unwrap(); + let bundle = match block { + SignedBeaconBlock::Deneb(SignedBeaconBlockDeneb { + ref mut message, .. + }) => { + // Get either zero blobs or a random number of blobs between 1 and Max Blobs. + let payload: &mut FullPayloadDeneb = &mut message.body.execution_payload; + let num_blobs = match num_blobs { + NumBlobs::Random => rng.gen_range(1..=E::max_blobs_per_block()), + NumBlobs::Number(n) => n, + NumBlobs::None => 0, + }; + let (bundle, transactions) = + execution_layer::test_utils::generate_blobs::(num_blobs).unwrap(); + + payload.execution_payload.transactions = <_>::default(); + for tx in Vec::from(transactions) { + payload.execution_payload.transactions.push(tx).unwrap(); + } + message.body.blob_kzg_commitments = bundle.commitments.clone(); + bundle } - message.body.blob_kzg_commitments = bundle.commitments.clone(); + SignedBeaconBlock::Electra(SignedBeaconBlockElectra { + ref mut message, .. + }) => { + // Get either zero blobs or a random number of blobs between 1 and Max Blobs. + let payload: &mut FullPayloadElectra = &mut message.body.execution_payload; + let num_blobs = match num_blobs { + NumBlobs::Random => rng.gen_range(1..=E::max_blobs_per_block()), + NumBlobs::Number(n) => n, + NumBlobs::None => 0, + }; + let (bundle, transactions) = + execution_layer::test_utils::generate_blobs::(num_blobs).unwrap(); - let eth2::types::BlobsBundle { - commitments, - proofs, - blobs, - } = bundle; - - for (index, ((blob, kzg_commitment), kzg_proof)) in blobs - .into_iter() - .zip(commitments.into_iter()) - .zip(proofs.into_iter()) - .enumerate() - { - blob_sidecars.push(BlobSidecar { - index: index as u64, - blob: blob.clone(), - kzg_commitment, - kzg_proof, - signed_block_header: block.signed_block_header(), - kzg_commitment_inclusion_proof: block - .message() - .body() - .kzg_commitment_merkle_proof(index) - .unwrap(), - }); + payload.execution_payload.transactions = <_>::default(); + for tx in Vec::from(transactions) { + payload.execution_payload.transactions.push(tx).unwrap(); + } + message.body.blob_kzg_commitments = bundle.commitments.clone(); + bundle } + _ => return (block, blob_sidecars), + }; + + let eth2::types::BlobsBundle { + commitments, + proofs, + blobs, + } = bundle; + + for (index, ((blob, kzg_commitment), kzg_proof)) in blobs + .into_iter() + .zip(commitments.into_iter()) + .zip(proofs.into_iter()) + .enumerate() + { + blob_sidecars.push(BlobSidecar { + index: index as u64, + blob: blob.clone(), + kzg_commitment, + kzg_proof, + signed_block_header: block.signed_block_header(), + kzg_commitment_inclusion_proof: block + .message() + .body() + .kzg_commitment_merkle_proof(index) + .unwrap(), + }); } - (block, blob_sidecars) } diff --git a/beacon_node/beacon_chain/src/validator_monitor.rs b/beacon_node/beacon_chain/src/validator_monitor.rs index 49a555816b..a63940074b 100644 --- a/beacon_node/beacon_chain/src/validator_monitor.rs +++ b/beacon_node/beacon_chain/src/validator_monitor.rs @@ -15,7 +15,6 @@ use state_processing::per_epoch_processing::{ errors::EpochProcessingError, EpochProcessingSummary, }; use std::collections::{HashMap, HashSet}; -use std::convert::TryFrom; use std::io; use std::marker::PhantomData; use std::str::Utf8Error; @@ -383,7 +382,7 @@ struct MissedBlock { /// /// The intention of this struct is to provide users with more logging and Prometheus metrics around /// validators that they are interested in. -pub struct ValidatorMonitor { +pub struct ValidatorMonitor { /// The validators that require additional monitoring. validators: HashMap, /// A map of validator index (state.validators) to a validator public key. @@ -400,12 +399,12 @@ pub struct ValidatorMonitor { // A beacon proposer cache beacon_proposer_cache: Arc>, // Unaggregated attestations generated by the committee index at each slot. - unaggregated_attestations: HashMap>, + unaggregated_attestations: HashMap>, log: Logger, - _phantom: PhantomData, + _phantom: PhantomData, } -impl ValidatorMonitor { +impl ValidatorMonitor { pub fn new( config: ValidatorMonitorConfig, beacon_proposer_cache: Arc>, @@ -461,7 +460,7 @@ impl ValidatorMonitor { } /// Add an unaggregated attestation - pub fn set_unaggregated_attestation(&mut self, attestation: Attestation) { + pub fn set_unaggregated_attestation(&mut self, attestation: Attestation) { let unaggregated_attestations = &mut self.unaggregated_attestations; // Pruning, this removes the oldest key/pair of the hashmap if it's greater than MAX_UNAGGREGATED_ATTESTATION_HASHMAP_LENGTH @@ -474,7 +473,7 @@ impl ValidatorMonitor { self.unaggregated_attestations.insert(slot, attestation); } - pub fn get_unaggregated_attestation(&self, slot: Slot) -> Option<&Attestation> { + pub fn get_unaggregated_attestation(&self, slot: Slot) -> Option<&Attestation> { self.unaggregated_attestations.get(&slot) } @@ -483,7 +482,7 @@ impl ValidatorMonitor { pub fn process_valid_state( &mut self, current_epoch: Epoch, - state: &BeaconState, + state: &BeaconState, spec: &ChainSpec, ) { // Add any new validator indices. @@ -586,19 +585,19 @@ impl ValidatorMonitor { // Prune missed blocks that are prior to last finalized epochs - MISSED_BLOCK_LOOKBACK_EPOCHS let finalized_epoch = state.finalized_checkpoint().epoch; self.missed_blocks.retain(|missed_block| { - let epoch = missed_block.slot.epoch(T::slots_per_epoch()); + let epoch = missed_block.slot.epoch(E::slots_per_epoch()); epoch + Epoch::new(MISSED_BLOCK_LOOKBACK_EPOCHS) >= finalized_epoch }); } /// Add missed non-finalized blocks for the monitored validators - fn add_validators_missed_blocks(&mut self, state: &BeaconState) { + fn add_validators_missed_blocks(&mut self, state: &BeaconState) { // Define range variables let current_slot = state.slot(); - let current_epoch = current_slot.epoch(T::slots_per_epoch()); + let current_epoch = current_slot.epoch(E::slots_per_epoch()); // start_slot needs to be coherent with what can be retrieved from the beacon_proposer_cache - let start_slot = current_epoch.start_slot(T::slots_per_epoch()) - - Slot::new(MISSED_BLOCK_LOOKBACK_EPOCHS * T::slots_per_epoch()); + let start_slot = current_epoch.start_slot(E::slots_per_epoch()) + - Slot::new(MISSED_BLOCK_LOOKBACK_EPOCHS * E::slots_per_epoch()); let end_slot = current_slot.saturating_sub(MISSED_BLOCK_LAG_SLOTS).as_u64(); @@ -618,7 +617,7 @@ impl ValidatorMonitor { { // Found missed block if block_root == prev_block_root { - let slot_epoch = slot.epoch(T::slots_per_epoch()); + let slot_epoch = slot.epoch(E::slots_per_epoch()); if let Ok(shuffling_decision_block) = state.proposer_shuffling_decision_root_at_epoch(slot_epoch, *block_root) @@ -639,7 +638,7 @@ impl ValidatorMonitor { } // Only add missed blocks for the proposer if it's in the list of monitored validators - let slot_in_epoch = slot % T::slots_per_epoch(); + let slot_in_epoch = slot % E::slots_per_epoch(); if let Some(proposer_index) = proposers_per_epoch .as_ref() .and_then(|(proposers, _)| proposers.get(slot_in_epoch.as_usize())) @@ -698,13 +697,13 @@ impl ValidatorMonitor { ) -> Option> { let mut cache = self.beacon_proposer_cache.lock(); cache - .get_epoch::(shuffling_decision_block, epoch) + .get_epoch::(shuffling_decision_block, epoch) .cloned() } /// Process the unaggregated attestations generated by the service `attestation_simulator_service` /// and check if the attestation qualifies for a reward matching the flags source/target/head - fn process_unaggregated_attestations(&mut self, state: &BeaconState, spec: &ChainSpec) { + fn process_unaggregated_attestations(&mut self, state: &BeaconState, spec: &ChainSpec) { let current_slot = state.slot(); // Ensures that we process attestation when there have been skipped slots between blocks @@ -722,7 +721,7 @@ impl ValidatorMonitor { for slot in attested_slots { if let Some(unaggregated_attestation) = unaggregated_attestations.remove(&slot) { // Don't process this attestation, it's too old to be processed by this state. - if slot.epoch(T::slots_per_epoch()) < state.previous_epoch() { + if slot.epoch(E::slots_per_epoch()) < state.previous_epoch() { continue; } @@ -791,7 +790,7 @@ impl ValidatorMonitor { pub fn process_validator_statuses( &self, epoch: Epoch, - summary: &EpochProcessingSummary, + summary: &EpochProcessingSummary, spec: &ChainSpec, ) -> Result<(), EpochProcessingError> { let mut attestation_success = Vec::new(); @@ -1006,7 +1005,7 @@ impl ValidatorMonitor { self.log, "Current epoch sync signatures"; "included" => summary.sync_signature_block_inclusions, - "expected" => T::slots_per_epoch(), + "expected" => E::slots_per_epoch(), "epoch" => current_epoch, "validator" => id, ); @@ -1140,7 +1139,7 @@ impl ValidatorMonitor { pub fn register_gossip_block( &self, seen_timestamp: Duration, - block: BeaconBlockRef<'_, T>, + block: BeaconBlockRef<'_, E>, block_root: Hash256, slot_clock: &S, ) { @@ -1151,7 +1150,7 @@ impl ValidatorMonitor { pub fn register_api_block( &self, seen_timestamp: Duration, - block: BeaconBlockRef<'_, T>, + block: BeaconBlockRef<'_, E>, block_root: Hash256, slot_clock: &S, ) { @@ -1162,11 +1161,11 @@ impl ValidatorMonitor { &self, src: &str, seen_timestamp: Duration, - block: BeaconBlockRef<'_, T>, + block: BeaconBlockRef<'_, E>, block_root: Hash256, slot_clock: &S, ) { - let epoch = block.slot().epoch(T::slots_per_epoch()); + let epoch = block.slot().epoch(E::slots_per_epoch()); if let Some(validator) = self.get_validator(block.proposer_index()) { let id = &validator.id; let delay = get_block_delay_ms(seen_timestamp, block, slot_clock); @@ -1201,7 +1200,7 @@ impl ValidatorMonitor { pub fn register_gossip_unaggregated_attestation( &self, seen_timestamp: Duration, - indexed_attestation: &IndexedAttestation, + indexed_attestation: &IndexedAttestation, slot_clock: &S, ) { self.register_unaggregated_attestation( @@ -1216,7 +1215,7 @@ impl ValidatorMonitor { pub fn register_api_unaggregated_attestation( &self, seen_timestamp: Duration, - indexed_attestation: &IndexedAttestation, + indexed_attestation: &IndexedAttestation, slot_clock: &S, ) { self.register_unaggregated_attestation( @@ -1231,11 +1230,11 @@ impl ValidatorMonitor { &self, src: &str, seen_timestamp: Duration, - indexed_attestation: &IndexedAttestation, + indexed_attestation: &IndexedAttestation, slot_clock: &S, ) { let data = &indexed_attestation.data; - let epoch = data.slot.epoch(T::slots_per_epoch()); + let epoch = data.slot.epoch(E::slots_per_epoch()); let delay = get_message_delay_ms( seen_timestamp, data.slot, @@ -1284,8 +1283,8 @@ impl ValidatorMonitor { pub fn register_gossip_aggregated_attestation( &self, seen_timestamp: Duration, - signed_aggregate_and_proof: &SignedAggregateAndProof, - indexed_attestation: &IndexedAttestation, + signed_aggregate_and_proof: &SignedAggregateAndProof, + indexed_attestation: &IndexedAttestation, slot_clock: &S, ) { self.register_aggregated_attestation( @@ -1301,8 +1300,8 @@ impl ValidatorMonitor { pub fn register_api_aggregated_attestation( &self, seen_timestamp: Duration, - signed_aggregate_and_proof: &SignedAggregateAndProof, - indexed_attestation: &IndexedAttestation, + signed_aggregate_and_proof: &SignedAggregateAndProof, + indexed_attestation: &IndexedAttestation, slot_clock: &S, ) { self.register_aggregated_attestation( @@ -1318,12 +1317,12 @@ impl ValidatorMonitor { &self, src: &str, seen_timestamp: Duration, - signed_aggregate_and_proof: &SignedAggregateAndProof, - indexed_attestation: &IndexedAttestation, + signed_aggregate_and_proof: &SignedAggregateAndProof, + indexed_attestation: &IndexedAttestation, slot_clock: &S, ) { let data = &indexed_attestation.data; - let epoch = data.slot.epoch(T::slots_per_epoch()); + let epoch = data.slot.epoch(E::slots_per_epoch()); let delay = get_message_delay_ms( seen_timestamp, data.slot, @@ -1411,7 +1410,7 @@ impl ValidatorMonitor { /// Note: Blocks that get orphaned will skew the inclusion distance calculation. pub fn register_attestation_in_block( &self, - indexed_attestation: &IndexedAttestation, + indexed_attestation: &IndexedAttestation, parent_slot: Slot, spec: &ChainSpec, ) { @@ -1422,7 +1421,7 @@ impl ValidatorMonitor { let inclusion_distance = parent_slot.saturating_sub(data.slot) + 1; let delay = inclusion_distance - spec.min_attestation_inclusion_delay; - let epoch = data.slot.epoch(T::slots_per_epoch()); + let epoch = data.slot.epoch(E::slots_per_epoch()); indexed_attestation.attesting_indices.iter().for_each(|i| { if let Some(validator) = self.get_validator(*i) { @@ -1502,7 +1501,7 @@ impl ValidatorMonitor { if let Some(validator) = self.get_validator(sync_committee_message.validator_index) { let id = &validator.id; - let epoch = sync_committee_message.slot.epoch(T::slots_per_epoch()); + let epoch = sync_committee_message.slot.epoch(E::slots_per_epoch()); let delay = get_message_delay_ms( seen_timestamp, sync_committee_message.slot, @@ -1545,7 +1544,7 @@ impl ValidatorMonitor { pub fn register_gossip_sync_committee_contribution( &self, seen_timestamp: Duration, - sync_contribution: &SignedContributionAndProof, + sync_contribution: &SignedContributionAndProof, participant_pubkeys: &[PublicKeyBytes], slot_clock: &S, ) { @@ -1562,7 +1561,7 @@ impl ValidatorMonitor { pub fn register_api_sync_committee_contribution( &self, seen_timestamp: Duration, - sync_contribution: &SignedContributionAndProof, + sync_contribution: &SignedContributionAndProof, participant_pubkeys: &[PublicKeyBytes], slot_clock: &S, ) { @@ -1580,12 +1579,12 @@ impl ValidatorMonitor { &self, src: &str, seen_timestamp: Duration, - sync_contribution: &SignedContributionAndProof, + sync_contribution: &SignedContributionAndProof, participant_pubkeys: &[PublicKeyBytes], slot_clock: &S, ) { let slot = sync_contribution.message.contribution.slot; - let epoch = slot.epoch(T::slots_per_epoch()); + let epoch = slot.epoch(E::slots_per_epoch()); let beacon_block_root = sync_contribution.message.contribution.beacon_block_root; let delay = get_message_delay_ms( seen_timestamp, @@ -1666,7 +1665,7 @@ impl ValidatorMonitor { beacon_block_root: Hash256, participant_pubkeys: Vec<&PublicKeyBytes>, ) { - let epoch = slot.epoch(T::slots_per_epoch()); + let epoch = slot.epoch(E::slots_per_epoch()); for validator_pubkey in participant_pubkeys { if let Some(validator) = self.validators.get(validator_pubkey) { @@ -1753,7 +1752,7 @@ impl ValidatorMonitor { fn register_proposer_slashing(&self, src: &str, slashing: &ProposerSlashing) { let proposer = slashing.signed_header_1.message.proposer_index; let slot = slashing.signed_header_1.message.slot; - let epoch = slot.epoch(T::slots_per_epoch()); + let epoch = slot.epoch(E::slots_per_epoch()); let root_1 = slashing.signed_header_1.message.canonical_root(); let root_2 = slashing.signed_header_2.message.canonical_root(); @@ -1784,21 +1783,21 @@ impl ValidatorMonitor { } /// Register an attester slashing from the gossip network. - pub fn register_gossip_attester_slashing(&self, slashing: &AttesterSlashing) { + pub fn register_gossip_attester_slashing(&self, slashing: &AttesterSlashing) { self.register_attester_slashing("gossip", slashing) } /// Register an attester slashing from the HTTP API. - pub fn register_api_attester_slashing(&self, slashing: &AttesterSlashing) { + pub fn register_api_attester_slashing(&self, slashing: &AttesterSlashing) { self.register_attester_slashing("api", slashing) } /// Register an attester slashing included in a *valid* `BeaconBlock`. - pub fn register_block_attester_slashing(&self, slashing: &AttesterSlashing) { + pub fn register_block_attester_slashing(&self, slashing: &AttesterSlashing) { self.register_attester_slashing("block", slashing) } - fn register_attester_slashing(&self, src: &str, slashing: &AttesterSlashing) { + fn register_attester_slashing(&self, src: &str, slashing: &AttesterSlashing) { let data = &slashing.attestation_1.data; let attestation_1_indices: HashSet = slashing .attestation_1 @@ -1815,7 +1814,7 @@ impl ValidatorMonitor { .filter_map(|index| self.get_validator(*index)) .for_each(|validator| { let id = &validator.id; - let epoch = data.slot.epoch(T::slots_per_epoch()); + let epoch = data.slot.epoch(E::slots_per_epoch()); self.aggregatable_metric(id, |label| { metrics::inc_counter_vec( @@ -1849,8 +1848,8 @@ impl ValidatorMonitor { ); if let Some(slot) = slot_clock.now() { - let epoch = slot.epoch(T::slots_per_epoch()); - let slot_in_epoch = slot % T::slots_per_epoch(); + let epoch = slot.epoch(E::slots_per_epoch()); + let slot_in_epoch = slot % E::slots_per_epoch(); // Only start to report on the current epoch once we've progressed past the point where // all attestation should be included in a block. @@ -2073,9 +2072,9 @@ fn u64_to_i64(n: impl Into) -> i64 { } /// Returns the delay between the start of `block.slot` and `seen_timestamp`. -pub fn get_block_delay_ms>( +pub fn get_block_delay_ms>( seen_timestamp: Duration, - block: BeaconBlockRef<'_, T, P>, + block: BeaconBlockRef<'_, E, P>, slot_clock: &S, ) -> Duration { get_slot_delay_ms::(seen_timestamp, block.slot(), slot_clock) diff --git a/beacon_node/beacon_chain/src/validator_pubkey_cache.rs b/beacon_node/beacon_chain/src/validator_pubkey_cache.rs index 00140dd6ec..2cf0c32615 100644 --- a/beacon_node/beacon_chain/src/validator_pubkey_cache.rs +++ b/beacon_node/beacon_chain/src/validator_pubkey_cache.rs @@ -2,7 +2,6 @@ use crate::errors::BeaconChainError; use crate::{BeaconChainTypes, BeaconStore}; use ssz::{Decode, Encode}; use std::collections::HashMap; -use std::convert::TryInto; use std::marker::PhantomData; use store::{DBColumn, Error as StoreError, StoreItem, StoreOp}; use types::{BeaconState, Hash256, PublicKey, PublicKeyBytes}; @@ -195,7 +194,7 @@ mod test { use logging::test_logger; use std::sync::Arc; use store::HotColdDB; - use types::{BeaconState, EthSpec, Keypair, MainnetEthSpec}; + use types::{EthSpec, Keypair, MainnetEthSpec}; type E = MainnetEthSpec; type T = EphemeralHarnessType; diff --git a/beacon_node/beacon_chain/tests/attestation_verification.rs b/beacon_node/beacon_chain/tests/attestation_verification.rs index 2501768c78..3432604cc9 100644 --- a/beacon_node/beacon_chain/tests/attestation_verification.rs +++ b/beacon_node/beacon_chain/tests/attestation_verification.rs @@ -342,7 +342,7 @@ impl GossipTester { E::slots_per_epoch() + 1 } // EIP-7045 - ForkName::Deneb => { + ForkName::Deneb | ForkName::Electra => { let epoch_slot_offset = (self.slot() % E::slots_per_epoch()).as_u64(); if epoch_slot_offset != 0 { E::slots_per_epoch() + epoch_slot_offset @@ -1235,7 +1235,7 @@ async fn attestation_to_finalized_block() { .chain .verify_unaggregated_attestation_for_gossip(&attestation, Some(subnet_id)); assert!( - matches!(res, Err(AttnError:: HeadBlockFinalized { beacon_block_root }) + matches!(res, Err(AttnError::HeadBlockFinalized { beacon_block_root }) if beacon_block_root == earlier_block_root ) ); diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 541e97436d..4d37557f0d 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -1,4 +1,4 @@ -// #![cfg(not(debug_assertions))] +#![cfg(not(debug_assertions))] use beacon_chain::block_verification_types::{AsBlock, ExecutedBlock, RpcBlock}; use beacon_chain::{ @@ -840,7 +840,7 @@ async fn invalid_signature_exit() { } } -fn unwrap_err(result: Result) -> E { +fn unwrap_err(result: Result) -> U { match result { Ok(_) => panic!("called unwrap_err on Ok"), Err(e) => e, @@ -1087,7 +1087,7 @@ async fn block_gossip_verification() { assert!( matches!( unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(block.clone())).await), - BlockError::BlockIsAlreadyKnown, + BlockError::BlockIsAlreadyKnown(_), ), "should register any valid signature against the proposer, even if the block failed later verification" ); @@ -1115,7 +1115,7 @@ async fn block_gossip_verification() { .verify_block_for_gossip(block.clone()) .await .expect_err("should error when processing known block"), - BlockError::BlockIsAlreadyKnown + BlockError::BlockIsAlreadyKnown(_) ), "the second proposal by this validator should be rejected" ); diff --git a/beacon_node/beacon_chain/tests/capella.rs b/beacon_node/beacon_chain/tests/capella.rs index f0b799ec9f..dc40280f53 100644 --- a/beacon_node/beacon_chain/tests/capella.rs +++ b/beacon_node/beacon_chain/tests/capella.rs @@ -7,8 +7,8 @@ use types::*; const VALIDATOR_COUNT: usize = 32; type E = MainnetEthSpec; -fn verify_execution_payload_chain(chain: &[FullPayload]) { - let mut prev_ep: Option> = None; +fn verify_execution_payload_chain(chain: &[FullPayload]) { + let mut prev_ep: Option> = None; for ep in chain { assert!(!ep.is_default_with_empty_roots()); diff --git a/beacon_node/beacon_chain/tests/merge.rs b/beacon_node/beacon_chain/tests/merge.rs index 1e0112a495..bff5c4523d 100644 --- a/beacon_node/beacon_chain/tests/merge.rs +++ b/beacon_node/beacon_chain/tests/merge.rs @@ -8,8 +8,8 @@ const VALIDATOR_COUNT: usize = 32; type E = MainnetEthSpec; -fn verify_execution_payload_chain(chain: &[FullPayload]) { - let mut prev_ep: Option> = None; +fn verify_execution_payload_chain(chain: &[FullPayload]) { + let mut prev_ep: Option> = None; for ep in chain { assert!(!ep.is_default_with_empty_roots()); diff --git a/beacon_node/beacon_chain/tests/op_verification.rs b/beacon_node/beacon_chain/tests/op_verification.rs index f6cf40a396..40910b9b9f 100644 --- a/beacon_node/beacon_chain/tests/op_verification.rs +++ b/beacon_node/beacon_chain/tests/op_verification.rs @@ -2,12 +2,18 @@ #![cfg(not(debug_assertions))] -use beacon_chain::observed_operations::ObservationOutcome; -use beacon_chain::test_utils::{ - test_spec, AttestationStrategy, BeaconChainHarness, BlockStrategy, DiskHarnessType, +use beacon_chain::{ + observed_operations::ObservationOutcome, + test_utils::{ + test_spec, AttestationStrategy, BeaconChainHarness, BlockStrategy, DiskHarnessType, + }, + BeaconChainError, }; use lazy_static::lazy_static; use sloggers::{null::NullLoggerBuilder, Build}; +use state_processing::per_block_processing::errors::{ + AttesterSlashingInvalid, BlockOperationError, ExitInvalid, ProposerSlashingInvalid, +}; use std::sync::Arc; use store::{LevelDB, StoreConfig}; use tempfile::{tempdir, TempDir}; @@ -119,6 +125,75 @@ async fn voluntary_exit() { )); } +#[tokio::test] +async fn voluntary_exit_duplicate_in_state() { + let db_path = tempdir().unwrap(); + let store = get_store(&db_path); + let harness = get_harness(store.clone(), VALIDATOR_COUNT); + let spec = &harness.chain.spec; + + harness + .extend_chain( + (E::slots_per_epoch() * (spec.shard_committee_period + 1)) as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + harness.advance_slot(); + + // Exit a validator. + let exited_validator = 0; + let exit = + harness.make_voluntary_exit(exited_validator, Epoch::new(spec.shard_committee_period)); + let ObservationOutcome::New(verified_exit) = harness + .chain + .verify_voluntary_exit_for_gossip(exit.clone()) + .unwrap() + else { + panic!("exit should verify"); + }; + harness.chain.import_voluntary_exit(verified_exit); + + // Make a new block to include the exit. + harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Verify validator is actually exited. + assert_ne!( + harness + .get_current_state() + .validators() + .get(exited_validator as usize) + .unwrap() + .exit_epoch, + spec.far_future_epoch + ); + + // Clear the in-memory gossip cache & try to verify the same exit on gossip. + // It should still fail because gossip verification should check the validator's `exit_epoch` + // field in the head state. + harness + .chain + .observed_voluntary_exits + .lock() + .__reset_for_testing_only(); + + assert!(matches!( + harness + .chain + .verify_voluntary_exit_for_gossip(exit) + .unwrap_err(), + BeaconChainError::ExitValidationError(BlockOperationError::Invalid( + ExitInvalid::AlreadyExited(index) + )) if index == exited_validator + )); +} + #[test] fn proposer_slashing() { let db_path = tempdir().unwrap(); @@ -171,6 +246,63 @@ fn proposer_slashing() { )); } +#[tokio::test] +async fn proposer_slashing_duplicate_in_state() { + let db_path = tempdir().unwrap(); + let store = get_store(&db_path); + let harness = get_harness(store.clone(), VALIDATOR_COUNT); + + // Slash a validator. + let slashed_validator = 0; + let slashing = harness.make_proposer_slashing(slashed_validator); + let ObservationOutcome::New(verified_slashing) = harness + .chain + .verify_proposer_slashing_for_gossip(slashing.clone()) + .unwrap() + else { + panic!("slashing should verify"); + }; + harness.chain.import_proposer_slashing(verified_slashing); + + // Make a new block to include the slashing. + harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Verify validator is actually slashed. + assert!( + harness + .get_current_state() + .validators() + .get(slashed_validator as usize) + .unwrap() + .slashed + ); + + // Clear the in-memory gossip cache & try to verify the same slashing on gossip. + // It should still fail because gossip verification should check the validator's `slashed` field + // in the head state. + harness + .chain + .observed_proposer_slashings + .lock() + .__reset_for_testing_only(); + + assert!(matches!( + harness + .chain + .verify_proposer_slashing_for_gossip(slashing) + .unwrap_err(), + BeaconChainError::ProposerSlashingValidationError(BlockOperationError::Invalid( + ProposerSlashingInvalid::ProposerNotSlashable(index) + )) if index == slashed_validator + )); +} + #[test] fn attester_slashing() { let db_path = tempdir().unwrap(); @@ -241,3 +373,60 @@ fn attester_slashing() { ObservationOutcome::AlreadyKnown )); } + +#[tokio::test] +async fn attester_slashing_duplicate_in_state() { + let db_path = tempdir().unwrap(); + let store = get_store(&db_path); + let harness = get_harness(store.clone(), VALIDATOR_COUNT); + + // Slash a validator. + let slashed_validator = 0; + let slashing = harness.make_attester_slashing(vec![slashed_validator]); + let ObservationOutcome::New(verified_slashing) = harness + .chain + .verify_attester_slashing_for_gossip(slashing.clone()) + .unwrap() + else { + panic!("slashing should verify"); + }; + harness.chain.import_attester_slashing(verified_slashing); + + // Make a new block to include the slashing. + harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Verify validator is actually slashed. + assert!( + harness + .get_current_state() + .validators() + .get(slashed_validator as usize) + .unwrap() + .slashed + ); + + // Clear the in-memory gossip cache & try to verify the same slashing on gossip. + // It should still fail because gossip verification should check the validator's `slashed` field + // in the head state. + harness + .chain + .observed_attester_slashings + .lock() + .__reset_for_testing_only(); + + assert!(matches!( + harness + .chain + .verify_attester_slashing_for_gossip(slashing) + .unwrap_err(), + BeaconChainError::AttesterSlashingValidationError(BlockOperationError::Invalid( + AttesterSlashingInvalid::NoSlashableIndices + )) + )); +} diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index a0b7fbd365..f1262596f7 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -1077,9 +1077,7 @@ async fn invalid_parent() { Duration::from_secs(0), &state, PayloadVerificationStatus::Optimistic, - rig.harness.chain.config.progressive_balances_mode, &rig.harness.chain.spec, - rig.harness.logger() ), Err(ForkChoiceError::ProtoArrayStringError(message)) if message.contains(&format!( @@ -1820,81 +1818,94 @@ struct InvalidHeadSetup { } impl InvalidHeadSetup { + /// This function aims to produce two things: + /// + /// 1. A chain where the only viable head block has an invalid execution payload. + /// 2. A block (`fork_block`) which will become the head of the chain when + /// it is imported. async fn new() -> InvalidHeadSetup { + let slots_per_epoch = E::slots_per_epoch(); let mut rig = InvalidPayloadRig::new().enable_attestations(); rig.move_to_terminal_block(); rig.import_block(Payload::Valid).await; // Import a valid transition block. - // Import blocks until the first time the chain finalizes. + // Import blocks until the first time the chain finalizes. This avoids + // some edge-cases around genesis. while rig.cached_head().finalized_checkpoint().epoch == 0 { rig.import_block(Payload::Syncing).await; } - let slots_per_epoch = E::slots_per_epoch(); - let start_slot = rig.cached_head().head_slot() + 1; - let mut opt_fork_block = None; + // Define a helper function. + let chain = rig.harness.chain.clone(); + let get_unrealized_justified_epoch = move || { + chain + .canonical_head + .fork_choice_read_lock() + .unrealized_justified_checkpoint() + .epoch + }; - assert_eq!(start_slot % slots_per_epoch, 1); - for i in 0..slots_per_epoch - 1 { - let slot = start_slot + i; - let slot_offset = slot.as_u64() % slots_per_epoch; - - rig.harness.set_current_slot(slot); - - if slot_offset == slots_per_epoch - 1 { - // Optimistic head block right before epoch boundary. - let is_valid = Payload::Syncing; - rig.import_block_parametric(is_valid, is_valid, Some(slot), |error| { - matches!( - error, - BlockError::ExecutionPayloadError( - ExecutionPayloadError::RejectedByExecutionEngine { .. } - ) - ) - }) - .await; - } else if 3 * slot_offset < 2 * slots_per_epoch { - // Valid block in previous epoch. - rig.import_block(Payload::Valid).await; - } else if slot_offset == slots_per_epoch - 2 { - // Fork block one slot prior to invalid head, not applied immediately. - let parent_state = rig - .harness - .chain - .state_at_slot(slot - 1, StateSkipConfig::WithStateRoots) - .unwrap(); - let (fork_block_tuple, _) = rig.harness.make_block(parent_state, slot).await; - opt_fork_block = Some(fork_block_tuple.0); - } else { - // Skipped slot. - }; + // Import more blocks until there is a new and higher unrealized + // justified checkpoint. + // + // The result will be a single chain where the head block has a higher + // unrealized justified checkpoint than all other blocks in the chain. + let initial_unrealized_justified = get_unrealized_justified_epoch(); + while get_unrealized_justified_epoch() == initial_unrealized_justified { + rig.import_block(Payload::Syncing).await; } - let invalid_head = rig.cached_head(); - assert_eq!( - invalid_head.head_slot() % slots_per_epoch, - slots_per_epoch - 1 - ); + // Create a forked block that competes with the head block. Both the + // head block and this fork block will share the same parent. + // + // The fork block and head block will both have an unrealized justified + // checkpoint at epoch `N` whilst their parent is at `N - 1`. + let head_slot = rig.cached_head().head_slot(); + let parent_slot = head_slot - 1; + let fork_block_slot = head_slot + 1; + let parent_state = rig + .harness + .chain + .state_at_slot(parent_slot, StateSkipConfig::WithStateRoots) + .unwrap(); + let (fork_block_tuple, _) = rig.harness.make_block(parent_state, fork_block_slot).await; + let fork_block = fork_block_tuple.0; - // Advance clock to new epoch to realize the justification of soon-to-be-invalid head block. - rig.harness.set_current_slot(invalid_head.head_slot() + 1); + let invalid_head = rig.cached_head(); + + // Advance the chain forward two epochs past the current head block. + // + // This ensures that `voting_source.epoch + 2 >= current_epoch` is + // `false` in the `node_is_viable_for_head` function. In effect, this + // ensures that no other block but the current head block is viable as a + // head block. + let invalid_head_epoch = invalid_head.head_slot().epoch(slots_per_epoch); + let new_wall_clock_epoch = invalid_head_epoch + 2; + rig.harness + .set_current_slot(new_wall_clock_epoch.start_slot(slots_per_epoch)); // Invalidate the head block. rig.invalidate_manually(invalid_head.head_block_root()) .await; + // Since our setup ensures that there is only a single, invalid block + // that's viable for head (according to FFG filtering), setting the + // head block as invalid should not result in another head being chosen. + // Rather, it should fail to run fork choice and leave the invalid block as + // the head. assert!(rig .canonical_head() .head_execution_status() .unwrap() .is_invalid()); - // Finding a new head should fail since the only possible head is not valid. + // Ensure that we're getting the correct error when trying to find a new + // head. rig.assert_get_head_error_contains("InvalidBestNode"); Self { rig, - fork_block: opt_fork_block.unwrap(), + fork_block, invalid_head, } } diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index ffd1b843a1..51d62cf8a1 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -7,15 +7,13 @@ use beacon_chain::data_availability_checker::AvailableBlock; use beacon_chain::schema_change::migrate_schema; use beacon_chain::test_utils::{ mock_execution_layer_from_parts, test_spec, AttestationStrategy, BeaconChainHarness, - BlockStrategy, DiskHarnessType, + BlockStrategy, DiskHarnessType, KZG, }; use beacon_chain::{ data_availability_checker::MaybeAvailableBlock, historical_blocks::HistoricalBlockError, migrate::MigratorConfig, BeaconChain, BeaconChainError, BeaconChainTypes, BeaconSnapshot, BlockError, ChainConfig, NotifyExecutionLayer, ServerSentEventHandler, WhenSlotSkipped, }; -use eth2_network_config::TRUSTED_SETUP_BYTES; -use kzg::TrustedSetup; use lazy_static::lazy_static; use logging::test_logger; use maplit::hashset; @@ -2396,6 +2394,7 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { .get_full_block(&wss_block_root) .unwrap() .unwrap(); + let wss_blobs_opt = harness.chain.store.get_blobs(&wss_block_root).unwrap(); let wss_state = full_store .get_state(&wss_state_root, Some(checkpoint_slot)) .unwrap() @@ -2417,9 +2416,7 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { let store = get_store(&temp2); let spec = test_spec::(); let seconds_per_slot = spec.seconds_per_slot; - let trusted_setup: TrustedSetup = serde_json::from_reader(TRUSTED_SETUP_BYTES) - .map_err(|e| println!("Unable to read trusted setup file: {}", e)) - .unwrap(); + let kzg = spec.deneb_fork_epoch.map(|_| KZG.clone()); let mock = mock_execution_layer_from_parts(&harness.spec, harness.runtime.task_executor.clone()); @@ -2438,7 +2435,12 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { .custom_spec(test_spec::()) .task_executor(harness.chain.task_executor.clone()) .logger(log.clone()) - .weak_subjectivity_state(wss_state, wss_block.clone(), genesis_state) + .weak_subjectivity_state( + wss_state, + wss_block.clone(), + wss_blobs_opt.clone(), + genesis_state, + ) .unwrap() .store_migrator_config(MigratorConfig::default().blocking()) .dummy_eth1_backend() @@ -2451,11 +2453,22 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { 1, ))) .execution_layer(Some(mock.el)) - .trusted_setup(trusted_setup) + .kzg(kzg) .build() .expect("should build"); let beacon_chain = Arc::new(beacon_chain); + let wss_block_root = wss_block.canonical_root(); + let store_wss_block = harness + .chain + .get_block(&wss_block_root) + .await + .unwrap() + .unwrap(); + let store_wss_blobs_opt = beacon_chain.store.get_blobs(&wss_block_root).unwrap(); + + assert_eq!(store_wss_block, wss_block); + assert_eq!(store_wss_blobs_opt, wss_blobs_opt); // Apply blocks forward to reach head. let chain_dump = harness.chain.chain_dump().unwrap(); @@ -3518,13 +3531,12 @@ fn assert_chains_pretty_much_the_same(a: &BeaconChain, b a_head.beacon_block, b_head.beacon_block, "head blocks should be equal" ); - // Clone with committee caches only to prevent other caches from messing with the equality - // check. - assert_eq!( - a_head.beacon_state.clone_with_only_committee_caches(), - b_head.beacon_state.clone_with_only_committee_caches(), - "head states should be equal" - ); + // Drop all caches to prevent them messing with the equality check. + let mut a_head_state = a_head.beacon_state.clone(); + a_head_state.drop_all_caches().unwrap(); + let mut b_head_state = b_head.beacon_state.clone(); + b_head_state.drop_all_caches().unwrap(); + assert_eq!(a_head_state, b_head_state, "head states should be equal"); assert_eq!(a.heads(), b.heads(), "heads() should be equal"); assert_eq!( a.genesis_block_root, b.genesis_block_root, diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs index 4334f90836..e27180a002 100644 --- a/beacon_node/beacon_chain/tests/tests.rs +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -10,9 +10,7 @@ use beacon_chain::{ }; use lazy_static::lazy_static; use operation_pool::PersistedOperationPool; -use state_processing::{ - per_slot_processing, per_slot_processing::Error as SlotProcessingError, EpochProcessingError, -}; +use state_processing::{per_slot_processing, per_slot_processing::Error as SlotProcessingError}; use types::{ BeaconState, BeaconStateError, EthSpec, Hash256, Keypair, MinimalEthSpec, RelativeEpoch, Slot, }; @@ -59,9 +57,7 @@ fn massive_skips() { assert!(state.slot() > 1, "the state should skip at least one slot"); assert_eq!( error, - SlotProcessingError::EpochProcessingError(EpochProcessingError::BeaconStateError( - BeaconStateError::InsufficientValidators - )), + SlotProcessingError::BeaconStateError(BeaconStateError::InsufficientValidators), "should return error indicating that validators have been slashed out" ) } diff --git a/beacon_node/beacon_chain/tests/validator_monitor.rs b/beacon_node/beacon_chain/tests/validator_monitor.rs index d9ff57b1b0..f595e5037e 100644 --- a/beacon_node/beacon_chain/tests/validator_monitor.rs +++ b/beacon_node/beacon_chain/tests/validator_monitor.rs @@ -203,8 +203,12 @@ async fn produces_missed_blocks() { // making sure that the cache reloads when the epoch changes // in that scenario the slot that missed a block is the first slot of the epoch validator_index_to_monitor = 7; - // We are adding other validators to monitor as thoses one will miss a block depending on - // the fork name specified when running the test as the proposer cache differs depending on the fork name (cf. seed) + // We are adding other validators to monitor as these ones will miss a block depending on + // the fork name specified when running the test as the proposer cache differs depending on + // the fork name (cf. seed) + // + // If you are adding a new fork and seeing errors, print + // `validator_indexes[slot_in_epoch.as_usize()]` and add it below. let validator_index_to_monitor_altair = 2; // Same as above but for the merge upgrade let validator_index_to_monitor_merge = 4; @@ -212,6 +216,9 @@ async fn produces_missed_blocks() { let validator_index_to_monitor_capella = 11; // Same as above but for the deneb upgrade let validator_index_to_monitor_deneb = 3; + // Same as above but for the electra upgrade + let validator_index_to_monitor_electra = 6; + let harness2 = get_harness( validator_count, vec![ @@ -220,6 +227,7 @@ async fn produces_missed_blocks() { validator_index_to_monitor_merge, validator_index_to_monitor_capella, validator_index_to_monitor_deneb, + validator_index_to_monitor_electra, ], ); let advance_slot_by = 9; @@ -243,6 +251,10 @@ async fn produces_missed_blocks() { duplicate_block_root = *_state2.block_roots().get(idx as usize).unwrap(); validator_indexes = _state2.get_beacon_proposer_indices(&harness2.spec).unwrap(); validator_index = validator_indexes[slot_in_epoch.as_usize()]; + // If you are adding a new fork and seeing errors, it means the fork seed has changed the + // validator_index. Uncomment this line, run the test again and add the resulting index to the + // list above. + //eprintln!("new index which needs to be added => {:?}", validator_index); let beacon_proposer_cache = harness2 .chain diff --git a/beacon_node/beacon_processor/src/lib.rs b/beacon_node/beacon_processor/src/lib.rs index 045b06a1e7..9b83e9cacb 100644 --- a/beacon_node/beacon_processor/src/lib.rs +++ b/beacon_node/beacon_processor/src/lib.rs @@ -187,6 +187,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 queued `LightClientOptimisticUpdateRequest` objects received from the network RPC that +/// will be stored before we start dropping them. +const MAX_LIGHT_CLIENT_OPTIMISTIC_UPDATE_QUEUE_LEN: usize = 512; + +/// The maximum number of queued `LightClientFinalityUpdateRequest` objects received from the network RPC that +/// will be stored before we start dropping them. +const MAX_LIGHT_CLIENT_FINALITY_UPDATE_QUEUE_LEN: usize = 512; + /// 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; @@ -243,6 +251,8 @@ pub const BLOCKS_BY_ROOTS_REQUEST: &str = "blocks_by_roots_request"; pub const BLOBS_BY_RANGE_REQUEST: &str = "blobs_by_range_request"; pub const BLOBS_BY_ROOTS_REQUEST: &str = "blobs_by_roots_request"; pub const LIGHT_CLIENT_BOOTSTRAP_REQUEST: &str = "light_client_bootstrap"; +pub const LIGHT_CLIENT_FINALITY_UPDATE_REQUEST: &str = "light_client_finality_update_request"; +pub const LIGHT_CLIENT_OPTIMISTIC_UPDATE_REQUEST: &str = "light_client_optimistic_update_request"; 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"; @@ -284,7 +294,7 @@ pub struct BeaconProcessorChannels { impl BeaconProcessorChannels { pub fn new(config: &BeaconProcessorConfig) -> Self { let (beacon_processor_tx, beacon_processor_rx) = - mpsc::channel(config.max_scheduled_work_queue_len); + mpsc::channel(config.max_work_event_queue_len); let (work_reprocessing_tx, work_reprocessing_rx) = mpsc::channel(config.max_scheduled_work_queue_len); @@ -561,7 +571,7 @@ pub enum BlockingOrAsync { /// queuing specifics. pub enum Work { GossipAttestation { - attestation: GossipAttestationPackage, + attestation: Box>, process_individual: Box) + Send + Sync>, process_batch: Box>) + Send + Sync>, }, @@ -573,7 +583,7 @@ pub enum Work { process_batch: Box>) + Send + Sync>, }, GossipAggregate { - aggregate: GossipAggregatePackage, + aggregate: Box>, process_individual: Box) + Send + Sync>, process_batch: Box>) + Send + Sync>, }, @@ -614,12 +624,14 @@ pub enum Work { ChainSegment(AsyncFn), ChainSegmentBackfill(AsyncFn), Status(BlockingFn), - BlocksByRangeRequest(BlockingFnWithManualSendOnIdle), - BlocksByRootsRequest(BlockingFnWithManualSendOnIdle), + BlocksByRangeRequest(AsyncFn), + BlocksByRootsRequest(AsyncFn), BlobsByRangeRequest(BlockingFn), BlobsByRootsRequest(BlockingFn), GossipBlsToExecutionChange(BlockingFn), LightClientBootstrapRequest(BlockingFn), + LightClientOptimisticUpdateRequest(BlockingFn), + LightClientFinalityUpdateRequest(BlockingFn), ApiRequestP0(BlockingOrAsync), ApiRequestP1(BlockingOrAsync), } @@ -659,6 +671,8 @@ impl Work { Work::BlobsByRangeRequest(_) => BLOBS_BY_RANGE_REQUEST, Work::BlobsByRootsRequest(_) => BLOBS_BY_ROOTS_REQUEST, Work::LightClientBootstrapRequest(_) => LIGHT_CLIENT_BOOTSTRAP_REQUEST, + Work::LightClientOptimisticUpdateRequest(_) => LIGHT_CLIENT_OPTIMISTIC_UPDATE_REQUEST, + Work::LightClientFinalityUpdateRequest(_) => LIGHT_CLIENT_FINALITY_UPDATE_REQUEST, Work::UnknownBlockAttestation { .. } => UNKNOWN_BLOCK_ATTESTATION, Work::UnknownBlockAggregate { .. } => UNKNOWN_BLOCK_AGGREGATE, Work::GossipBlsToExecutionChange(_) => GOSSIP_BLS_TO_EXECUTION_CHANGE, @@ -820,7 +834,11 @@ impl BeaconProcessor { let mut gossip_bls_to_execution_change_queue = FifoQueue::new(MAX_BLS_TO_EXECUTION_CHANGE_QUEUE_LEN); - let mut lcbootstrap_queue = FifoQueue::new(MAX_LIGHT_CLIENT_BOOTSTRAP_QUEUE_LEN); + let mut lc_bootstrap_queue = FifoQueue::new(MAX_LIGHT_CLIENT_BOOTSTRAP_QUEUE_LEN); + let mut lc_optimistic_update_queue = + FifoQueue::new(MAX_LIGHT_CLIENT_OPTIMISTIC_UPDATE_QUEUE_LEN); + let mut lc_finality_update_queue = + FifoQueue::new(MAX_LIGHT_CLIENT_FINALITY_UPDATE_QUEUE_LEN); 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); @@ -997,7 +1015,7 @@ impl BeaconProcessor { process_individual: _, process_batch, } => { - aggregates.push(aggregate); + aggregates.push(*aggregate); if process_batch_opt.is_none() { process_batch_opt = Some(process_batch); } @@ -1057,7 +1075,7 @@ impl BeaconProcessor { process_individual: _, process_batch, } => { - attestations.push(attestation); + attestations.push(*attestation); if process_batch_opt.is_none() { process_batch_opt = Some(process_batch); } @@ -1137,9 +1155,14 @@ impl BeaconProcessor { // Handle backfill sync chain segments. } else if let Some(item) = backfill_chain_segment.pop() { self.spawn_worker(item, idle_tx); - // This statement should always be the final else statement. - } else if let Some(item) = lcbootstrap_queue.pop() { + // Handle light client requests. + } else if let Some(item) = lc_bootstrap_queue.pop() { self.spawn_worker(item, idle_tx); + } else if let Some(item) = lc_optimistic_update_queue.pop() { + self.spawn_worker(item, idle_tx); + } else if let Some(item) = lc_finality_update_queue.pop() { + self.spawn_worker(item, idle_tx); + // This statement should always be the final else statement. } else { // Let the journal know that a worker is freed and there's nothing else // for it to do. @@ -1249,7 +1272,13 @@ impl BeaconProcessor { blbrange_queue.push(work, work_id, &self.log) } Work::LightClientBootstrapRequest { .. } => { - lcbootstrap_queue.push(work, work_id, &self.log) + lc_bootstrap_queue.push(work, work_id, &self.log) + } + Work::LightClientOptimisticUpdateRequest { .. } => { + lc_optimistic_update_queue.push(work, work_id, &self.log) + } + Work::LightClientFinalityUpdateRequest { .. } => { + lc_finality_update_queue.push(work, work_id, &self.log) } Work::UnknownBlockAttestation { .. } => { unknown_block_attestation_queue.push(work) @@ -1416,7 +1445,7 @@ impl BeaconProcessor { process_individual, process_batch: _, } => task_spawner.spawn_blocking(move || { - process_individual(attestation); + process_individual(*attestation); }), Work::GossipAttestationBatch { attestations, @@ -1429,7 +1458,7 @@ impl BeaconProcessor { process_individual, process_batch: _, } => task_spawner.spawn_blocking(move || { - process_individual(aggregate); + process_individual(*aggregate); }), Work::GossipAggregateBatch { aggregates, @@ -1464,7 +1493,7 @@ impl BeaconProcessor { task_spawner.spawn_blocking(process_fn) } Work::BlocksByRangeRequest(work) | Work::BlocksByRootsRequest(work) => { - task_spawner.spawn_blocking_with_manual_send_idle(work) + task_spawner.spawn_async(work) } Work::ChainSegmentBackfill(process_fn) => task_spawner.spawn_async(process_fn), Work::ApiRequestP0(process_fn) | Work::ApiRequestP1(process_fn) => match process_fn { @@ -1480,7 +1509,9 @@ impl BeaconProcessor { | Work::GossipLightClientOptimisticUpdate(process_fn) | Work::Status(process_fn) | Work::GossipBlsToExecutionChange(process_fn) - | Work::LightClientBootstrapRequest(process_fn) => { + | Work::LightClientBootstrapRequest(process_fn) + | Work::LightClientOptimisticUpdateRequest(process_fn) + | Work::LightClientFinalityUpdateRequest(process_fn) => { task_spawner.spawn_blocking(process_fn) } }; @@ -1524,23 +1555,6 @@ impl TaskSpawner { WORKER_TASK_NAME, ) } - - /// Spawn a blocking task, passing the `SendOnDrop` into the task. - /// - /// ## Notes - /// - /// Users must ensure the `SendOnDrop` is dropped at the appropriate time! - pub fn spawn_blocking_with_manual_send_idle(self, task: F) - where - F: FnOnce(SendOnDrop) + Send + 'static, - { - self.executor.spawn_blocking( - || { - task(self.send_idle_on_drop); - }, - WORKER_TASK_NAME, - ) - } } /// This struct will send a message on `self.tx` when it is dropped. An error will be logged on diff --git a/beacon_node/beacon_processor/src/work_reprocessing_queue.rs b/beacon_node/beacon_processor/src/work_reprocessing_queue.rs index 20f3e21d08..c9be28444c 100644 --- a/beacon_node/beacon_processor/src/work_reprocessing_queue.rs +++ b/beacon_node/beacon_processor/src/work_reprocessing_queue.rs @@ -159,10 +159,10 @@ pub struct IgnoredRpcBlock { /// A backfill batch work that has been queued for processing later. pub struct QueuedBackfillBatch(pub AsyncFn); -impl TryFrom> for QueuedBackfillBatch { - type Error = WorkEvent; +impl TryFrom> for QueuedBackfillBatch { + type Error = WorkEvent; - fn try_from(event: WorkEvent) -> Result> { + fn try_from(event: WorkEvent) -> Result> { match event { WorkEvent { work: Work::ChainSegmentBackfill(process_fn), @@ -173,8 +173,8 @@ impl TryFrom> for QueuedBackfillBatch { } } -impl From for WorkEvent { - fn from(queued_backfill_batch: QueuedBackfillBatch) -> WorkEvent { +impl From for WorkEvent { + fn from(queued_backfill_batch: QueuedBackfillBatch) -> WorkEvent { WorkEvent { drop_during_sync: false, work: Work::ChainSegmentBackfill(queued_backfill_batch.0), @@ -964,7 +964,6 @@ impl ReprocessQueue { mod tests { use super::*; use slot_clock::TestingSlotClock; - use types::Slot; #[test] fn backfill_processing_schedule_calculation() { diff --git a/beacon_node/builder_client/src/lib.rs b/beacon_node/builder_client/src/lib.rs index 934ef059d5..2b373292f3 100644 --- a/beacon_node/builder_client/src/lib.rs +++ b/beacon_node/builder_client/src/lib.rs @@ -5,7 +5,8 @@ use eth2::types::{ }; use eth2::types::{FullPayloadContents, SignedBlindedBeaconBlock}; pub use eth2::Error; -use eth2::{ok_or_error, StatusCode}; +use eth2::{ok_or_error, StatusCode, CONSENSUS_VERSION_HEADER}; +use reqwest::header::{HeaderMap, HeaderValue}; use reqwest::{IntoUrl, Response}; use sensitive_url::SensitiveUrl; use serde::de::DeserializeOwned; @@ -108,13 +109,20 @@ impl BuilderHttpClient { &self, url: U, body: &T, + headers: HeaderMap, timeout: Option, ) -> Result { let mut builder = self.client.post(url); if let Some(timeout) = timeout { builder = builder.timeout(timeout); } - let response = builder.json(body).send().await.map_err(Error::from)?; + + let response = builder + .headers(headers) + .json(body) + .send() + .await + .map_err(Error::from)?; ok_or_error(response).await } @@ -151,10 +159,16 @@ impl BuilderHttpClient { .push("builder") .push("blinded_blocks"); + let mut headers = HeaderMap::new(); + if let Ok(value) = HeaderValue::from_str(&blinded_block.fork_name_unchecked().to_string()) { + headers.insert(CONSENSUS_VERSION_HEADER, value); + } + Ok(self .post_with_raw_response( path, &blinded_block, + headers, Some(self.timeouts.post_blinded_blocks), ) .await? diff --git a/beacon_node/client/Cargo.toml b/beacon_node/client/Cargo.toml index 0160cad4b2..03cbcc9ff7 100644 --- a/beacon_node/client/Cargo.toml +++ b/beacon_node/client/Cargo.toml @@ -45,3 +45,4 @@ monitoring_api = { workspace = true } execution_layer = { workspace = true } beacon_processor = { workspace = true } num_cpus = { workspace = true } +ethereum_ssz = { workspace = true } diff --git a/beacon_node/client/src/address_change_broadcast.rs b/beacon_node/client/src/address_change_broadcast.rs deleted file mode 100644 index 69614159fe..0000000000 --- a/beacon_node/client/src/address_change_broadcast.rs +++ /dev/null @@ -1,322 +0,0 @@ -use crate::*; -use lighthouse_network::PubsubMessage; -use network::NetworkMessage; -use slog::{debug, info, warn, Logger}; -use slot_clock::SlotClock; -use std::cmp; -use std::collections::HashSet; -use std::mem; -use std::time::Duration; -use tokio::sync::mpsc::UnboundedSender; -use tokio::time::sleep; -use types::EthSpec; - -/// The size of each chunk of addresses changes to be broadcast at the Capella -/// fork. -const BROADCAST_CHUNK_SIZE: usize = 128; -/// The delay between broadcasting each chunk. -const BROADCAST_CHUNK_DELAY: Duration = Duration::from_millis(500); - -/// If the Capella fork has already been reached, `broadcast_address_changes` is -/// called immediately. -/// -/// If the Capella fork has not been reached, waits until the start of the fork -/// epoch and then calls `broadcast_address_changes`. -pub async fn broadcast_address_changes_at_capella( - chain: &BeaconChain, - network_send: UnboundedSender>, - log: &Logger, -) { - let spec = &chain.spec; - let slot_clock = &chain.slot_clock; - - let capella_fork_slot = if let Some(epoch) = spec.capella_fork_epoch { - epoch.start_slot(T::EthSpec::slots_per_epoch()) - } else { - // Exit now if Capella is not defined. - return; - }; - - // Wait until the Capella fork epoch. - while chain.slot().map_or(true, |slot| slot < capella_fork_slot) { - match slot_clock.duration_to_slot(capella_fork_slot) { - Some(duration) => { - // Sleep until the Capella fork. - sleep(duration).await; - break; - } - None => { - // We were unable to read the slot clock wait another slot - // and then try again. - sleep(slot_clock.slot_duration()).await; - } - } - } - - // The following function will be called in two scenarios: - // - // 1. The node has been running for some time and the Capella fork has just - // been reached. - // 2. The node has just started and it is *after* the Capella fork. - broadcast_address_changes(chain, network_send, log).await -} - -/// Broadcasts any address changes that are flagged for broadcasting at the -/// Capella fork epoch. -/// -/// Address changes are published in chunks, with a delay between each chunk. -/// This helps reduce the load on the P2P network and also helps prevent us from -/// clogging our `network_send` channel and being late to publish -/// blocks, attestations, etc. -pub async fn broadcast_address_changes( - chain: &BeaconChain, - network_send: UnboundedSender>, - log: &Logger, -) { - let head = chain.head_snapshot(); - let mut changes = chain - .op_pool - .get_bls_to_execution_changes_received_pre_capella(&head.beacon_state, &chain.spec); - - while !changes.is_empty() { - // This `split_off` approach is to allow us to have owned chunks of the - // `changes` vec. The `std::slice::Chunks` method uses references and - // the `itertools` iterator that achives this isn't `Send` so it doesn't - // work well with the `sleep` at the end of the loop. - let tail = changes.split_off(cmp::min(BROADCAST_CHUNK_SIZE, changes.len())); - let chunk = mem::replace(&mut changes, tail); - - let mut published_indices = HashSet::with_capacity(BROADCAST_CHUNK_SIZE); - let mut num_ok = 0; - let mut num_err = 0; - - // Publish each individual address change. - for address_change in chunk { - let validator_index = address_change.message.validator_index; - - let pubsub_message = PubsubMessage::BlsToExecutionChange(Box::new(address_change)); - let message = NetworkMessage::Publish { - messages: vec![pubsub_message], - }; - // It seems highly unlikely that this unbounded send will fail, but - // we handle the result nonetheless. - if let Err(e) = network_send.send(message) { - debug!( - log, - "Failed to publish change message"; - "error" => ?e, - "validator_index" => validator_index - ); - num_err += 1; - } else { - debug!( - log, - "Published address change message"; - "validator_index" => validator_index - ); - num_ok += 1; - published_indices.insert(validator_index); - } - } - - // Remove any published indices from the list of indices that need to be - // published. - chain - .op_pool - .register_indices_broadcasted_at_capella(&published_indices); - - info!( - log, - "Published address change messages"; - "num_published" => num_ok, - ); - - if num_err > 0 { - warn!( - log, - "Failed to publish address changes"; - "info" => "failed messages will be retried", - "num_unable_to_publish" => num_err, - ); - } - - sleep(BROADCAST_CHUNK_DELAY).await; - } - - debug!( - log, - "Address change routine complete"; - ); -} - -#[cfg(not(debug_assertions))] // Tests run too slow in debug. -#[cfg(test)] -mod tests { - use super::*; - use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; - use operation_pool::ReceivedPreCapella; - use state_processing::{SigVerifiedOp, VerifyOperation}; - use std::collections::HashSet; - use tokio::sync::mpsc; - use types::*; - - type E = MainnetEthSpec; - - pub const VALIDATOR_COUNT: usize = BROADCAST_CHUNK_SIZE * 3; - pub const EXECUTION_ADDRESS: Address = Address::repeat_byte(42); - - struct Tester { - harness: BeaconChainHarness>, - /// Changes which should be broadcast at the Capella fork. - received_pre_capella_changes: Vec>, - /// Changes which should *not* be broadcast at the Capella fork. - not_received_pre_capella_changes: Vec>, - } - - impl Tester { - fn new() -> Self { - let altair_fork_epoch = Epoch::new(0); - let bellatrix_fork_epoch = Epoch::new(0); - let capella_fork_epoch = Epoch::new(2); - - let mut spec = E::default_spec(); - spec.altair_fork_epoch = Some(altair_fork_epoch); - spec.bellatrix_fork_epoch = Some(bellatrix_fork_epoch); - spec.capella_fork_epoch = Some(capella_fork_epoch); - - let harness = BeaconChainHarness::builder(E::default()) - .spec(spec) - .logger(logging::test_logger()) - .deterministic_keypairs(VALIDATOR_COUNT) - .deterministic_withdrawal_keypairs(VALIDATOR_COUNT) - .fresh_ephemeral_store() - .mock_execution_layer() - .build(); - - Self { - harness, - received_pre_capella_changes: <_>::default(), - not_received_pre_capella_changes: <_>::default(), - } - } - - fn produce_verified_address_change( - &self, - validator_index: u64, - ) -> SigVerifiedOp { - let change = self - .harness - .make_bls_to_execution_change(validator_index, EXECUTION_ADDRESS); - let head = self.harness.chain.head_snapshot(); - - change - .validate(&head.beacon_state, &self.harness.spec) - .unwrap() - } - - fn produce_received_pre_capella_changes(mut self, indices: Vec) -> Self { - for validator_index in indices { - self.received_pre_capella_changes - .push(self.produce_verified_address_change(validator_index)); - } - self - } - - fn produce_not_received_pre_capella_changes(mut self, indices: Vec) -> Self { - for validator_index in indices { - self.not_received_pre_capella_changes - .push(self.produce_verified_address_change(validator_index)); - } - self - } - - async fn run(self) { - let harness = self.harness; - let chain = harness.chain.clone(); - - let mut broadcast_indices = HashSet::new(); - for change in self.received_pre_capella_changes { - broadcast_indices.insert(change.as_inner().message.validator_index); - chain - .op_pool - .insert_bls_to_execution_change(change, ReceivedPreCapella::Yes); - } - - let mut non_broadcast_indices = HashSet::new(); - for change in self.not_received_pre_capella_changes { - non_broadcast_indices.insert(change.as_inner().message.validator_index); - chain - .op_pool - .insert_bls_to_execution_change(change, ReceivedPreCapella::No); - } - - harness.set_current_slot( - chain - .spec - .capella_fork_epoch - .unwrap() - .start_slot(E::slots_per_epoch()), - ); - - let (sender, mut receiver) = mpsc::unbounded_channel(); - - broadcast_address_changes_at_capella(&chain, sender, &logging::test_logger()).await; - - let mut broadcasted_changes = vec![]; - while let Some(NetworkMessage::Publish { mut messages }) = receiver.recv().await { - match messages.pop().unwrap() { - PubsubMessage::BlsToExecutionChange(change) => broadcasted_changes.push(change), - _ => panic!("unexpected message"), - } - } - - assert_eq!( - broadcasted_changes.len(), - broadcast_indices.len(), - "all expected changes should have been broadcast" - ); - - for broadcasted in &broadcasted_changes { - assert!( - !non_broadcast_indices.contains(&broadcasted.message.validator_index), - "messages not flagged for broadcast should not have been broadcast" - ); - } - - let head = chain.head_snapshot(); - assert!( - chain - .op_pool - .get_bls_to_execution_changes_received_pre_capella( - &head.beacon_state, - &chain.spec, - ) - .is_empty(), - "there shouldn't be any capella broadcast changes left in the op pool" - ); - } - } - - // Useful for generating even-numbered indices. Required since only even - // numbered genesis validators have BLS credentials. - fn even_indices(start: u64, count: usize) -> Vec { - (start..).filter(|i| i % 2 == 0).take(count).collect() - } - - #[tokio::test] - async fn one_chunk() { - Tester::new() - .produce_received_pre_capella_changes(even_indices(0, 4)) - .produce_not_received_pre_capella_changes(even_indices(10, 4)) - .run() - .await; - } - - #[tokio::test] - async fn multiple_chunks() { - Tester::new() - .produce_received_pre_capella_changes(even_indices(0, BROADCAST_CHUNK_SIZE * 3 / 2)) - .run() - .await; - } -} diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index 444a277509..6c50575154 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -1,4 +1,3 @@ -use crate::address_change_broadcast::broadcast_address_changes_at_capella; use crate::compute_light_client_updates::{ compute_light_client_updates, LIGHT_CLIENT_SERVER_CHANNEL_CAPACITY, }; @@ -36,6 +35,7 @@ use network::{NetworkConfig, NetworkSenders, NetworkService}; use slasher::Slasher; use slasher_service::SlasherService; use slog::{debug, info, warn, Logger}; +use ssz::Decode; use std::net::TcpListener; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -44,7 +44,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use timer::spawn_timer; use tokio::sync::oneshot; use types::{ - test_utils::generate_deterministic_keypairs, BeaconState, ChainSpec, EthSpec, + test_utils::generate_deterministic_keypairs, BeaconState, BlobSidecarList, ChainSpec, EthSpec, ExecutionBlockHash, Hash256, SignedBeaconBlock, }; @@ -92,19 +92,19 @@ pub struct ClientBuilder { eth_spec_instance: T::EthSpec, } -impl - ClientBuilder> +impl + ClientBuilder> where TSlotClock: SlotClock + Clone + 'static, - TEth1Backend: Eth1ChainBackend + 'static, - TEthSpec: EthSpec + 'static, - THotStore: ItemStore + 'static, - TColdStore: ItemStore + 'static, + TEth1Backend: Eth1ChainBackend + 'static, + E: EthSpec + 'static, + THotStore: ItemStore + 'static, + TColdStore: ItemStore + 'static, { /// Instantiates a new, empty builder. /// - /// The `eth_spec_instance` parameter is used to concretize `TEthSpec`. - pub fn new(eth_spec_instance: TEthSpec) -> Self { + /// The `eth_spec_instance` parameter is used to concretize `E`. + pub fn new(eth_spec_instance: E) -> Self { Self { slot_clock: None, store: None, @@ -129,7 +129,7 @@ where } /// Specifies the runtime context (tokio executor, logger, etc) for client services. - pub fn runtime_context(mut self, context: RuntimeContext) -> Self { + pub fn runtime_context(mut self, context: RuntimeContext) -> Self { self.runtime_context = Some(context); self } @@ -146,7 +146,7 @@ where self } - pub fn slasher(mut self, slasher: Arc>) -> Self { + pub fn slasher(mut self, slasher: Arc>) -> Self { self.slasher = Some(slasher); self } @@ -214,7 +214,7 @@ where }; let builder = if config.network.enable_light_client_server { - let (tx, rv) = futures::channel::mpsc::channel::>( + let (tx, rv) = futures::channel::mpsc::channel::>( LIGHT_CLIENT_SERVER_CHANNEL_CAPACITY, ); self.light_client_server_rv = Some(rv); @@ -299,7 +299,7 @@ where .min_epochs_for_blob_sidecars_requests .saturating_sub(BLOB_AVAILABILITY_REDUCTION_EPOCHS); let blob_availability_window = reduced_p2p_availability_epochs - * TEthSpec::slots_per_epoch() + * E::slots_per_epoch() * spec.seconds_per_slot; if now > deneb_time + blob_availability_window { @@ -319,6 +319,7 @@ where ClientGenesis::WeakSubjSszBytes { anchor_state_bytes, anchor_block_bytes, + anchor_blobs_bytes, } => { info!(context.log(), "Starting checkpoint sync"); if config.chain.genesis_backfill { @@ -332,10 +333,25 @@ where .map_err(|e| format!("Unable to parse weak subj state SSZ: {:?}", e))?; let anchor_block = SignedBeaconBlock::from_ssz_bytes(&anchor_block_bytes, &spec) .map_err(|e| format!("Unable to parse weak subj block SSZ: {:?}", e))?; + let anchor_blobs = if anchor_block.message().body().has_blobs() { + let anchor_blobs_bytes = anchor_blobs_bytes + .ok_or("Blobs for checkpoint must be provided using --checkpoint-blobs")?; + Some( + BlobSidecarList::from_ssz_bytes(&anchor_blobs_bytes) + .map_err(|e| format!("Unable to parse weak subj blobs SSZ: {e:?}"))?, + ) + } else { + None + }; let genesis_state = genesis_state(&runtime_context, &config, log).await?; builder - .weak_subjectivity_state(anchor_state, anchor_block, genesis_state) + .weak_subjectivity_state( + anchor_state, + anchor_block, + anchor_blobs, + genesis_state, + ) .map(|v| (v, None))? } ClientGenesis::CheckpointSyncUrl { url } => { @@ -408,7 +424,7 @@ where "Downloading finalized state"; ); let state = remote - .get_debug_beacon_states_ssz::(StateId::Finalized, &spec) + .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())?; @@ -419,7 +435,7 @@ where debug!(context.log(), "Downloading finalized block"; "block_slot" => ?finalized_block_slot); let block = remote - .get_beacon_blocks_ssz::(BlockId::Slot(finalized_block_slot), &spec) + .get_beacon_blocks_ssz::(BlockId::Slot(finalized_block_slot), &spec) .await .map_err(|e| match e { ApiError::InvalidSsz(e) => format!( @@ -430,9 +446,33 @@ where e => format!("Error fetching finalized block from remote: {:?}", e), })? .ok_or("Finalized block missing from remote, it returned 404")?; + let block_root = block.canonical_root(); debug!(context.log(), "Downloaded finalized block"); + let blobs = if block.message().body().has_blobs() { + debug!(context.log(), "Downloading finalized blobs"); + if let Some(response) = remote + .get_blobs::(BlockId::Root(block_root), None) + .await + .map_err(|e| format!("Error fetching finalized blobs from remote: {e:?}"))? + { + debug!(context.log(), "Downloaded finalized blobs"); + Some(response.data) + } else { + warn!( + context.log(), + "Checkpoint server is missing blobs"; + "block_root" => %block_root, + "hint" => "use a different URL or ask the provider to update", + "impact" => "db will be slightly corrupt until these blobs are pruned", + ); + None + } + } else { + None + }; + let genesis_state = genesis_state(&runtime_context, &config, log).await?; info!( @@ -440,7 +480,7 @@ where "Loaded checkpoint block and state"; "block_slot" => block.slot(), "state_slot" => state.slot(), - "block_root" => ?block.canonical_root(), + "block_root" => ?block_root, ); let service = @@ -468,7 +508,7 @@ where }); builder - .weak_subjectivity_state(state, block, genesis_state) + .weak_subjectivity_state(state, block, blobs, genesis_state) .map(|v| (v, service))? } ClientGenesis::DepositContract => { @@ -497,7 +537,7 @@ where #[allow(clippy::type_complexity)] let ctx: Arc< http_api::Context< - Witness, + Witness, >, > = Arc::new(http_api::Context { config: self.http_api_config.clone(), @@ -505,6 +545,7 @@ where network_senders: None, network_globals: None, beacon_processor_send: None, + beacon_processor_reprocess_send: None, eth1_service: Some(genesis_service.eth1_service.clone()), log: context.log().clone(), sse_logging_components: runtime_context.sse_logging_components.clone(), @@ -565,7 +606,12 @@ where }; let beacon_chain_builder = if let Some(trusted_setup) = config.trusted_setup { - beacon_chain_builder.trusted_setup(trusted_setup) + let kzg = trusted_setup + .try_into() + .map(Arc::new) + .map(Some) + .map_err(|e| format!("Failed to load trusted setup: {:?}", e))?; + beacon_chain_builder.kzg(kzg) } else { beacon_chain_builder }; @@ -723,8 +769,7 @@ where #[allow(clippy::type_complexity)] pub fn build( mut self, - ) -> Result>, String> - { + ) -> Result>, String> { let runtime_context = self .runtime_context .as_ref() @@ -747,6 +792,9 @@ where network_globals: self.network_globals.clone(), eth1_service: self.eth1_service.clone(), beacon_processor_send: Some(beacon_processor_channels.beacon_processor_tx.clone()), + beacon_processor_reprocess_send: Some( + beacon_processor_channels.work_reprocessing_tx.clone(), + ), sse_logging_components: runtime_context.sse_logging_components.clone(), log: log.clone(), }); @@ -875,25 +923,6 @@ where beacon_chain.slot_clock.clone(), ); } - - // Spawn a service to publish BLS to execution changes at the Capella fork. - if let Some(network_senders) = self.network_senders.clone() { - let inner_chain = beacon_chain.clone(); - let broadcast_context = - runtime_context.service_context("addr_bcast".to_string()); - let log = broadcast_context.log().clone(); - broadcast_context.executor.spawn( - async move { - broadcast_address_changes_at_capella( - &inner_chain, - network_senders.network_send(), - &log, - ) - .await - }, - "addr_broadcast", - ); - } } // Spawn service to publish light_client updates at some interval into the slot. @@ -937,14 +966,14 @@ where } } -impl - ClientBuilder> +impl + ClientBuilder> where TSlotClock: SlotClock + Clone + 'static, - TEth1Backend: Eth1ChainBackend + 'static, - TEthSpec: EthSpec + 'static, - THotStore: ItemStore + 'static, - TColdStore: ItemStore + 'static, + TEth1Backend: Eth1ChainBackend + 'static, + E: EthSpec + 'static, + THotStore: ItemStore + 'static, + TColdStore: ItemStore + 'static, { /// Consumes the internal `BeaconChainBuilder`, attaching the resulting `BeaconChain` to self. pub fn build_beacon_chain(mut self) -> Result { @@ -974,12 +1003,12 @@ where } } -impl - ClientBuilder, LevelDB>> +impl + ClientBuilder, LevelDB>> where TSlotClock: SlotClock + 'static, - TEth1Backend: Eth1ChainBackend + 'static, - TEthSpec: EthSpec + 'static, + TEth1Backend: Eth1ChainBackend + 'static, + E: EthSpec + 'static, { /// Specifies that the `Client` should use a `HotColdDB` database. pub fn disk_store( @@ -1036,15 +1065,13 @@ where } } -impl - ClientBuilder< - Witness, TEthSpec, THotStore, TColdStore>, - > +impl + ClientBuilder, E, THotStore, TColdStore>> where TSlotClock: SlotClock + 'static, - TEthSpec: EthSpec + 'static, - THotStore: ItemStore + 'static, - TColdStore: ItemStore + 'static, + E: EthSpec + 'static, + THotStore: ItemStore + 'static, + TColdStore: ItemStore + 'static, { /// Specifies that the `BeaconChain` should cache eth1 blocks/logs from a remote eth1 node /// (e.g., Parity/Geth) and refer to that cache when collecting deposits or eth1 votes during @@ -1137,13 +1164,13 @@ where } } -impl - ClientBuilder> +impl + ClientBuilder> where - TEth1Backend: Eth1ChainBackend + 'static, - TEthSpec: EthSpec + 'static, - THotStore: ItemStore + 'static, - TColdStore: ItemStore + 'static, + TEth1Backend: Eth1ChainBackend + 'static, + E: EthSpec + 'static, + THotStore: ItemStore + 'static, + TColdStore: ItemStore + 'static, { /// Specifies that the slot clock should read the time from the computers system clock. pub fn system_time_slot_clock(mut self) -> Result { @@ -1173,17 +1200,17 @@ where } /// Obtain the genesis state from the `eth2_network_config` in `context`. -async fn genesis_state( - context: &RuntimeContext, +async fn genesis_state( + context: &RuntimeContext, config: &ClientConfig, log: &Logger, -) -> Result, String> { +) -> 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::( + .genesis_state::( config.genesis_state_url.as_deref(), config.genesis_state_url_timeout, log, diff --git a/beacon_node/client/src/config.rs b/beacon_node/client/src/config.rs index 275f999864..48ad77abc5 100644 --- a/beacon_node/client/src/config.rs +++ b/beacon_node/client/src/config.rs @@ -35,6 +35,7 @@ pub enum ClientGenesis { WeakSubjSszBytes { anchor_state_bytes: Vec, anchor_block_bytes: Vec, + anchor_blobs_bytes: Option>, }, CheckpointSyncUrl { url: SensitiveUrl, @@ -117,7 +118,7 @@ impl Default for Config { impl Config { /// Updates the data directory for the Client. pub fn set_data_dir(&mut self, data_dir: PathBuf) { - self.data_dir = data_dir.clone(); + self.data_dir.clone_from(&data_dir); self.http_api.data_dir = data_dir; } diff --git a/beacon_node/client/src/lib.rs b/beacon_node/client/src/lib.rs index 2f14d87efc..fd92c28255 100644 --- a/beacon_node/client/src/lib.rs +++ b/beacon_node/client/src/lib.rs @@ -1,6 +1,5 @@ extern crate slog; -mod address_change_broadcast; mod compute_light_client_updates; pub mod config; mod metrics; diff --git a/beacon_node/client/src/notifier.rs b/beacon_node/client/src/notifier.rs index 8a0e5ce223..912babdae3 100644 --- a/beacon_node/client/src/notifier.rs +++ b/beacon_node/client/src/notifier.rs @@ -2,6 +2,7 @@ use crate::metrics; use beacon_chain::{ capella_readiness::CapellaReadiness, deneb_readiness::DenebReadiness, + electra_readiness::ElectraReadiness, merge_readiness::{GenesisExecutionPayloadStatus, MergeConfig, MergeReadiness}, BeaconChain, BeaconChainTypes, ExecutionStatus, }; @@ -321,6 +322,7 @@ pub fn spawn_notifier( merge_readiness_logging(current_slot, &beacon_chain, &log).await; capella_readiness_logging(current_slot, &beacon_chain, &log).await; deneb_readiness_logging(current_slot, &beacon_chain, &log).await; + electra_readiness_logging(current_slot, &beacon_chain, &log).await; } }; @@ -512,8 +514,7 @@ async fn deneb_readiness_logging( error!( log, "Execution endpoint required"; - "info" => "you need a Deneb enabled execution engine to validate blocks, see: \ - https://lighthouse-book.sigmaprime.io/merge-migration.html" + "info" => "you need a Deneb enabled execution engine to validate blocks." ); return; } @@ -542,6 +543,66 @@ async fn deneb_readiness_logging( ), } } +/// Provides some helpful logging to users to indicate if their node is ready for Electra. +async fn electra_readiness_logging( + current_slot: Slot, + beacon_chain: &BeaconChain, + log: &Logger, +) { + // TODO(electra): Once Electra has features, this code can be swapped back. + let electra_completed = false; + //let electra_completed = beacon_chain + // .canonical_head + // .cached_head() + // .snapshot + // .beacon_block + // .message() + // .body() + // .execution_payload() + // .map_or(false, |payload| payload.electra_placeholder().is_ok()); + + let has_execution_layer = beacon_chain.execution_layer.is_some(); + + if electra_completed && has_execution_layer + || !beacon_chain.is_time_to_prepare_for_electra(current_slot) + { + return; + } + + if electra_completed && !has_execution_layer { + // When adding a new fork, add a check for the next fork readiness here. + error!( + log, + "Execution endpoint required"; + "info" => "you need a Electra enabled execution engine to validate blocks." + ); + return; + } + + match beacon_chain.check_electra_readiness().await { + ElectraReadiness::Ready => { + info!( + log, + "Ready for Electra"; + "info" => "ensure the execution endpoint is updated to the latest Electra/Prague release" + ) + } + readiness @ ElectraReadiness::ExchangeCapabilitiesFailed { error: _ } => { + error!( + log, + "Not ready for Electra"; + "hint" => "the execution endpoint may be offline", + "info" => %readiness, + ) + } + readiness => warn!( + log, + "Not ready for Electra"; + "hint" => "try updating the execution endpoint", + "info" => %readiness, + ), + } +} async fn genesis_execution_payload_logging( beacon_chain: &BeaconChain, diff --git a/beacon_node/eth1/src/block_cache.rs b/beacon_node/eth1/src/block_cache.rs index e676d17ab9..399634a9fa 100644 --- a/beacon_node/eth1/src/block_cache.rs +++ b/beacon_node/eth1/src/block_cache.rs @@ -196,7 +196,6 @@ impl BlockCache { #[cfg(test)] mod tests { use super::*; - use types::Hash256; fn get_block(i: u64, interval_secs: u64) -> Eth1Block { Eth1Block { diff --git a/beacon_node/eth1/tests/test.rs b/beacon_node/eth1/tests/test.rs index 505e4a4796..0479ea7c58 100644 --- a/beacon_node/eth1/tests/test.rs +++ b/beacon_node/eth1/tests/test.rs @@ -99,7 +99,6 @@ async fn new_anvil_instance() -> Result { mod eth1_cache { use super::*; - use types::{EthSpec, MainnetEthSpec}; #[tokio::test] async fn simple_scenario() { diff --git a/beacon_node/execution_layer/Cargo.toml b/beacon_node/execution_layer/Cargo.toml index 7f65268980..ace8e24a8e 100644 --- a/beacon_node/execution_layer/Cargo.toml +++ b/beacon_node/execution_layer/Cargo.toml @@ -29,7 +29,6 @@ kzg = { 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 } @@ -51,3 +50,5 @@ hash-db = "0.15.2" pretty_reqwest_error = { workspace = true } arc-swap = "1.6.0" eth2_network_config = { workspace = true } +alloy-rlp = "0.3" +alloy-consensus = { git = "https://github.com/alloy-rs/alloy.git", rev = "974d488bab5e21e9f17452a39a4bfa56677367b2" } diff --git a/beacon_node/execution_layer/src/block_hash.rs b/beacon_node/execution_layer/src/block_hash.rs index 5ba61beafc..1f8c29f6b2 100644 --- a/beacon_node/execution_layer/src/block_hash.rs +++ b/beacon_node/execution_layer/src/block_hash.rs @@ -1,92 +1,61 @@ use crate::{ json_structures::JsonWithdrawal, keccak::{keccak256, KeccakHasher}, - metrics, Error, ExecutionLayer, }; use ethers_core::utils::rlp::RlpStream; use keccak_hash::KECCAK_EMPTY_LIST_RLP; use triehash::ordered_trie_root; use types::{ - map_execution_block_header_fields_base, Address, BeaconBlockRef, EthSpec, ExecutionBlockHash, + map_execution_block_header_fields_base, Address, EthSpec, ExecutionBlockHash, ExecutionBlockHeader, ExecutionPayloadRef, Hash256, Hash64, Uint256, }; -impl ExecutionLayer { - /// Calculate the block hash of an execution block. - /// - /// Return `(block_hash, transactions_root)`, where `transactions_root` is the root of the RLP - /// transactions. - pub fn calculate_execution_block_hash( - payload: ExecutionPayloadRef, - parent_beacon_block_root: Hash256, - ) -> (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. - let rlp_transactions_root = ordered_trie_root::( - payload.transactions().iter().map(|txn_bytes| &**txn_bytes), - ); +/// Calculate the block hash of an execution block. +/// +/// Return `(block_hash, transactions_root)`, where `transactions_root` is the root of the RLP +/// transactions. +pub fn calculate_execution_block_hash( + payload: ExecutionPayloadRef, + parent_beacon_block_root: Option, +) -> (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. + let rlp_transactions_root = ordered_trie_root::( + payload.transactions().iter().map(|txn_bytes| &**txn_bytes), + ); - // Calculate withdrawals root (post-Capella). - let rlp_withdrawals_root = if let Ok(withdrawals) = payload.withdrawals() { - Some(ordered_trie_root::( - withdrawals.iter().map(|withdrawal| { - rlp_encode_withdrawal(&JsonWithdrawal::from(withdrawal.clone())) - }), - )) - } else { - None - }; + // Calculate withdrawals root (post-Capella). + let rlp_withdrawals_root = if let Ok(withdrawals) = payload.withdrawals() { + Some(ordered_trie_root::( + withdrawals + .iter() + .map(|withdrawal| rlp_encode_withdrawal(&JsonWithdrawal::from(withdrawal.clone()))), + )) + } else { + None + }; - let rlp_blob_gas_used = payload.blob_gas_used().ok(); - let rlp_excess_blob_gas = payload.excess_blob_gas().ok(); + let rlp_blob_gas_used = payload.blob_gas_used().ok(); + let rlp_excess_blob_gas = payload.excess_blob_gas().ok(); - // Calculate parent beacon block root (post-Deneb). - let rlp_parent_beacon_block_root = rlp_excess_blob_gas - .as_ref() - .map(|_| parent_beacon_block_root); + // Construct the block header. + let exec_block_header = ExecutionBlockHeader::from_payload( + payload, + KECCAK_EMPTY_LIST_RLP.as_fixed_bytes().into(), + rlp_transactions_root, + rlp_withdrawals_root, + rlp_blob_gas_used, + rlp_excess_blob_gas, + parent_beacon_block_root, + ); - // Construct the block header. - let exec_block_header = ExecutionBlockHeader::from_payload( - payload, - KECCAK_EMPTY_LIST_RLP.as_fixed_bytes().into(), - rlp_transactions_root, - rlp_withdrawals_root, - rlp_blob_gas_used, - rlp_excess_blob_gas, - rlp_parent_beacon_block_root, - ); - - // Hash the RLP encoding of the block header. - let rlp_block_header = rlp_encode_block_header(&exec_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, block: BeaconBlockRef) -> Result<(), Error> { - let payload = block.execution_payload()?.execution_payload_ref(); - let parent_beacon_block_root = block.parent_root(); - - let _timer = metrics::start_timer(&metrics::EXECUTION_LAYER_VERIFY_BLOCK_HASH); - - let (header_hash, rlp_transactions_root) = - Self::calculate_execution_block_hash(payload, parent_beacon_block_root); - - if header_hash != payload.block_hash() { - return Err(Error::BlockHashMismatch { - computed: header_hash, - payload: payload.block_hash(), - transactions_root: rlp_transactions_root, - }); - } - - Ok(()) - } + // Hash the RLP encoding of the block header. + let rlp_block_header = rlp_encode_block_header(&exec_block_header); + ( + ExecutionBlockHash::from_root(keccak256(&rlp_block_header)), + rlp_transactions_root, + ) } /// RLP encode a withdrawal. @@ -267,4 +236,34 @@ mod test { .unwrap(); test_rlp_encoding(&header, None, expected_hash); } + + #[test] + fn test_rlp_encode_block_electra() { + let header = ExecutionBlockHeader { + parent_hash: Hash256::from_str("172864416698b842f4c92f7b476be294b4ef720202779df194cd225f531053ab").unwrap(), + ommers_hash: Hash256::from_str("1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347").unwrap(), + beneficiary: Address::from_str("878705ba3f8bc32fcf7f4caa1a35e72af65cf766").unwrap(), + state_root: Hash256::from_str("c6457d0df85c84c62d1c68f68138b6e796e8a44fb44de221386fb2d5611c41e0").unwrap(), + transactions_root: Hash256::from_str("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").unwrap(), + receipts_root: Hash256::from_str("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").unwrap(), + logs_bloom:<[u8; 256]>::from_hex("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").unwrap().into(), + difficulty: 0.into(), + number: 97.into(), + gas_limit: 27482534.into(), + gas_used: 0.into(), + timestamp: 1692132829u64, + extra_data: hex::decode("d883010d00846765746888676f312e32302e37856c696e7578").unwrap(), + mix_hash: Hash256::from_str("0b493c22d2ad4ca76c77ae6ad916af429b42b1dc98fdcb8e5ddbd049bbc5d623").unwrap(), + nonce: Hash64::zero(), + base_fee_per_gas: 2374u64.into(), + withdrawals_root: Some(Hash256::from_str("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").unwrap()), + blob_gas_used: Some(0x0u64), + excess_blob_gas: Some(0x0u64), + parent_beacon_block_root: Some(Hash256::from_str("f7d327d2c04e4f12e9cdd492e53d39a1d390f8b1571e3b2a22ac6e1e170e5b1a").unwrap()), + }; + let expected_hash = + Hash256::from_str("a7448e600ead0a23d16f96aa46e8dea9eef8a7c5669a5f0a5ff32709afe9c408") + .unwrap(); + test_rlp_encoding(&header, None, expected_hash); + } } diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index 19b9a58eb6..a91f5d6a44 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -17,8 +17,6 @@ pub use json_structures::{JsonWithdrawal, TransitionConfigurationV1}; use pretty_reqwest_error::PrettyReqwestError; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; -use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash; -use std::convert::TryFrom; use strum::IntoStaticStr; use superstruct::superstruct; pub use types::{ @@ -26,14 +24,21 @@ pub use types::{ ExecutionPayloadRef, FixedVector, ForkName, Hash256, Transactions, Uint256, VariableList, Withdrawal, Withdrawals, }; + use types::{ - BeaconStateError, ExecutionPayloadCapella, ExecutionPayloadDeneb, ExecutionPayloadMerge, - KzgProofs, VersionedHash, + ExecutionPayloadCapella, ExecutionPayloadDeneb, ExecutionPayloadElectra, ExecutionPayloadMerge, + KzgProofs, }; pub mod auth; pub mod http; pub mod json_structures; +mod new_payload_request; + +pub use new_payload_request::{ + NewPayloadRequest, NewPayloadRequestCapella, NewPayloadRequestDeneb, NewPayloadRequestElectra, + NewPayloadRequestMerge, +}; pub const LATEST_TAG: &str = "latest"; @@ -150,24 +155,24 @@ pub struct ExecutionBlock { /// Representation of an execution block with enough detail to reconstruct a payload. #[superstruct( - variants(Merge, Capella, Deneb), + variants(Merge, Capella, Deneb, Electra), variant_attributes( derive(Clone, Debug, PartialEq, Serialize, Deserialize,), - serde(bound = "T: EthSpec", rename_all = "camelCase"), + serde(bound = "E: EthSpec", rename_all = "camelCase"), ), cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"), partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant") )] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(bound = "T: EthSpec", rename_all = "camelCase", untagged)] -pub struct ExecutionBlockWithTransactions { +#[serde(bound = "E: EthSpec", rename_all = "camelCase", untagged)] +pub struct ExecutionBlockWithTransactions { pub parent_hash: ExecutionBlockHash, #[serde(alias = "miner")] pub fee_recipient: Address, pub state_root: Hash256, pub receipts_root: Hash256, #[serde(with = "ssz_types::serde_utils::hex_fixed_vec")] - pub logs_bloom: FixedVector, + pub logs_bloom: FixedVector, #[serde(alias = "mixHash")] pub prev_randao: Hash256, #[serde(rename = "number", with = "serde_utils::u64_hex_be")] @@ -179,25 +184,25 @@ pub struct ExecutionBlockWithTransactions { #[serde(with = "serde_utils::u64_hex_be")] pub timestamp: u64, #[serde(with = "ssz_types::serde_utils::hex_var_list")] - pub extra_data: VariableList, + pub extra_data: VariableList, pub base_fee_per_gas: Uint256, #[serde(rename = "hash")] pub block_hash: ExecutionBlockHash, pub transactions: Vec, - #[superstruct(only(Capella, Deneb))] + #[superstruct(only(Capella, Deneb, Electra))] pub withdrawals: Vec, - #[superstruct(only(Deneb))] + #[superstruct(only(Deneb, Electra))] #[serde(with = "serde_utils::u64_hex_be")] pub blob_gas_used: u64, - #[superstruct(only(Deneb))] + #[superstruct(only(Deneb, Electra))] #[serde(with = "serde_utils::u64_hex_be")] pub excess_blob_gas: u64, } -impl TryFrom> for ExecutionBlockWithTransactions { +impl TryFrom> for ExecutionBlockWithTransactions { type Error = Error; - fn try_from(payload: ExecutionPayload) -> Result { + fn try_from(payload: ExecutionPayload) -> Result { let json_payload = match payload { ExecutionPayload::Merge(block) => Self::Merge(ExecutionBlockWithTransactionsMerge { parent_hash: block.parent_hash, @@ -271,6 +276,34 @@ impl TryFrom> for ExecutionBlockWithTransactions blob_gas_used: block.blob_gas_used, excess_blob_gas: block.excess_blob_gas, }), + ExecutionPayload::Electra(block) => { + Self::Electra(ExecutionBlockWithTransactionsElectra { + parent_hash: block.parent_hash, + fee_recipient: block.fee_recipient, + state_root: block.state_root, + receipts_root: block.receipts_root, + logs_bloom: block.logs_bloom, + prev_randao: block.prev_randao, + block_number: block.block_number, + gas_limit: block.gas_limit, + gas_used: block.gas_used, + timestamp: block.timestamp, + extra_data: block.extra_data, + base_fee_per_gas: block.base_fee_per_gas, + block_hash: block.block_hash, + transactions: block + .transactions + .iter() + .map(|tx| Transaction::decode(&Rlp::new(tx))) + .collect::, _>>()?, + withdrawals: Vec::from(block.withdrawals) + .into_iter() + .map(|withdrawal| withdrawal.into()) + .collect(), + blob_gas_used: block.blob_gas_used, + excess_blob_gas: block.excess_blob_gas, + }) + } }; Ok(json_payload) } @@ -390,7 +423,7 @@ pub struct ProposeBlindedBlockResponse { } #[superstruct( - variants(Merge, Capella, Deneb), + variants(Merge, Capella, Deneb, Electra), variant_attributes(derive(Clone, Debug, PartialEq),), map_into(ExecutionPayload), map_ref_into(ExecutionPayloadRef), @@ -398,17 +431,19 @@ pub struct ProposeBlindedBlockResponse { partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant") )] #[derive(Clone, Debug, PartialEq)] -pub struct GetPayloadResponse { +pub struct GetPayloadResponse { #[superstruct(only(Merge), partial_getter(rename = "execution_payload_merge"))] - pub execution_payload: ExecutionPayloadMerge, + pub execution_payload: ExecutionPayloadMerge, #[superstruct(only(Capella), partial_getter(rename = "execution_payload_capella"))] - pub execution_payload: ExecutionPayloadCapella, + pub execution_payload: ExecutionPayloadCapella, #[superstruct(only(Deneb), partial_getter(rename = "execution_payload_deneb"))] - pub execution_payload: ExecutionPayloadDeneb, + pub execution_payload: ExecutionPayloadDeneb, + #[superstruct(only(Electra), partial_getter(rename = "execution_payload_electra"))] + pub execution_payload: ExecutionPayloadElectra, pub block_value: Uint256, - #[superstruct(only(Deneb))] - pub blobs_bundle: BlobsBundle, - #[superstruct(only(Deneb), partial_getter(copy))] + #[superstruct(only(Deneb, Electra))] + pub blobs_bundle: BlobsBundle, + #[superstruct(only(Deneb, Electra), partial_getter(copy))] pub should_override_builder: bool, } @@ -426,26 +461,26 @@ impl GetPayloadResponse { } } -impl<'a, T: EthSpec> From> for ExecutionPayloadRef<'a, T> { - fn from(response: GetPayloadResponseRef<'a, T>) -> Self { +impl<'a, E: EthSpec> From> for ExecutionPayloadRef<'a, E> { + fn from(response: GetPayloadResponseRef<'a, E>) -> Self { map_get_payload_response_ref_into_execution_payload_ref!(&'a _, response, |inner, cons| { cons(&inner.execution_payload) }) } } -impl From> for ExecutionPayload { - fn from(response: GetPayloadResponse) -> Self { +impl From> for ExecutionPayload { + fn from(response: GetPayloadResponse) -> Self { map_get_payload_response_into_execution_payload!(response, |inner, cons| { cons(inner.execution_payload) }) } } -impl From> - for (ExecutionPayload, Uint256, Option>) +impl From> + for (ExecutionPayload, Uint256, Option>) { - fn from(response: GetPayloadResponse) -> Self { + fn from(response: GetPayloadResponse) -> Self { match response { GetPayloadResponse::Merge(inner) => ( ExecutionPayload::Merge(inner.execution_payload), @@ -462,6 +497,11 @@ impl From> inner.block_value, Some(inner.blobs_bundle), ), + GetPayloadResponse::Electra(inner) => ( + ExecutionPayload::Electra(inner.execution_payload), + inner.block_value, + Some(inner.blobs_bundle), + ), } } } @@ -471,8 +511,8 @@ pub enum GetPayloadResponseType { Blinded(GetPayloadResponse), } -impl GetPayloadResponse { - pub fn execution_payload_ref(&self) -> ExecutionPayloadRef { +impl GetPayloadResponse { + pub fn execution_payload_ref(&self) -> ExecutionPayloadRef { self.to_ref().into() } } @@ -562,115 +602,39 @@ impl ExecutionPayloadBodyV1 { })) } else { Err(format!( - "block {} is post capella but payload body doesn't have withdrawals", + "block {} is post-capella but payload body doesn't have withdrawals", header.block_hash )) } } - } - } -} - -#[superstruct( - variants(Merge, Capella, Deneb), - variant_attributes(derive(Clone, Debug, PartialEq),), - map_into(ExecutionPayload), - map_ref_into(ExecutionPayloadRef), - cast_error( - ty = "BeaconStateError", - expr = "BeaconStateError::IncorrectStateVariant" - ), - partial_getter_error( - ty = "BeaconStateError", - expr = "BeaconStateError::IncorrectStateVariant" - ) -)] -#[derive(Clone, Debug, PartialEq)] -pub struct NewPayloadRequest { - #[superstruct(only(Merge), partial_getter(rename = "execution_payload_merge"))] - pub execution_payload: ExecutionPayloadMerge, - #[superstruct(only(Capella), partial_getter(rename = "execution_payload_capella"))] - pub execution_payload: ExecutionPayloadCapella, - #[superstruct(only(Deneb), partial_getter(rename = "execution_payload_deneb"))] - pub execution_payload: ExecutionPayloadDeneb, - #[superstruct(only(Deneb))] - pub versioned_hashes: Vec, - #[superstruct(only(Deneb))] - pub parent_beacon_block_root: Hash256, -} - -impl NewPayloadRequest { - pub fn parent_hash(&self) -> ExecutionBlockHash { - match self { - Self::Merge(payload) => payload.execution_payload.parent_hash, - Self::Capella(payload) => payload.execution_payload.parent_hash, - Self::Deneb(payload) => payload.execution_payload.parent_hash, - } - } - - pub fn block_hash(&self) -> ExecutionBlockHash { - match self { - Self::Merge(payload) => payload.execution_payload.block_hash, - Self::Capella(payload) => payload.execution_payload.block_hash, - Self::Deneb(payload) => payload.execution_payload.block_hash, - } - } - - pub fn block_number(&self) -> u64 { - match self { - Self::Merge(payload) => payload.execution_payload.block_number, - Self::Capella(payload) => payload.execution_payload.block_number, - Self::Deneb(payload) => payload.execution_payload.block_number, - } - } - - pub fn into_execution_payload(self) -> ExecutionPayload { - map_new_payload_request_into_execution_payload!(self, |request, cons| { - cons(request.execution_payload) - }) - } -} - -impl<'a, E: EthSpec> TryFrom> for NewPayloadRequest { - type Error = BeaconStateError; - - fn try_from(block: BeaconBlockRef<'a, E>) -> Result { - match block { - BeaconBlockRef::Base(_) | BeaconBlockRef::Altair(_) => { - Err(Self::Error::IncorrectStateVariant) + ExecutionPayloadHeader::Electra(header) => { + if let Some(withdrawals) = self.withdrawals { + Ok(ExecutionPayload::Electra(ExecutionPayloadElectra { + parent_hash: header.parent_hash, + fee_recipient: header.fee_recipient, + state_root: header.state_root, + receipts_root: header.receipts_root, + logs_bloom: header.logs_bloom, + prev_randao: header.prev_randao, + block_number: header.block_number, + gas_limit: header.gas_limit, + gas_used: header.gas_used, + timestamp: header.timestamp, + extra_data: header.extra_data, + base_fee_per_gas: header.base_fee_per_gas, + block_hash: header.block_hash, + transactions: self.transactions, + withdrawals, + blob_gas_used: header.blob_gas_used, + excess_blob_gas: header.excess_blob_gas, + })) + } else { + Err(format!( + "block {} is post-capella but payload body doesn't have withdrawals", + header.block_hash + )) + } } - BeaconBlockRef::Merge(block_ref) => Ok(Self::Merge(NewPayloadRequestMerge { - execution_payload: block_ref.body.execution_payload.execution_payload.clone(), - })), - BeaconBlockRef::Capella(block_ref) => Ok(Self::Capella(NewPayloadRequestCapella { - execution_payload: block_ref.body.execution_payload.execution_payload.clone(), - })), - BeaconBlockRef::Deneb(block_ref) => Ok(Self::Deneb(NewPayloadRequestDeneb { - execution_payload: block_ref.body.execution_payload.execution_payload.clone(), - versioned_hashes: block_ref - .body - .blob_kzg_commitments - .iter() - .map(kzg_commitment_to_versioned_hash) - .collect(), - parent_beacon_block_root: block_ref.parent_root, - })), - } - } -} - -impl TryFrom> for NewPayloadRequest { - type Error = BeaconStateError; - - fn try_from(payload: ExecutionPayload) -> Result { - match payload { - ExecutionPayload::Merge(payload) => Ok(Self::Merge(NewPayloadRequestMerge { - execution_payload: payload, - })), - ExecutionPayload::Capella(payload) => Ok(Self::Capella(NewPayloadRequestCapella { - execution_payload: payload, - })), - ExecutionPayload::Deneb(_) => Err(Self::Error::IncorrectStateVariant), } } } diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index ac7dfa57e9..ebd6ebeba2 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -11,7 +11,6 @@ use std::collections::HashSet; use tokio::sync::Mutex; use std::time::{Duration, Instant}; -use types::EthSpec; pub use deposit_log::{DepositLog, Log}; pub use reqwest::Client; @@ -72,23 +71,6 @@ pub static LIGHTHOUSE_CAPABILITIES: &[&str] = &[ ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, ]; -/// This is necessary because a user might run a capella-enabled version of -/// lighthouse before they update to a capella-enabled execution engine. -// TODO (mark): rip this out once we are post-capella on mainnet -pub static PRE_CAPELLA_ENGINE_CAPABILITIES: EngineCapabilities = EngineCapabilities { - new_payload_v1: true, - new_payload_v2: false, - new_payload_v3: false, - forkchoice_updated_v1: true, - forkchoice_updated_v2: false, - forkchoice_updated_v3: false, - get_payload_bodies_by_hash_v1: false, - get_payload_bodies_by_range_v1: false, - get_payload_v1: true, - get_payload_v2: false, - get_payload_v3: false, -}; - /// Contains methods to convert arbitrary bytes to an ETH2 deposit contract object. pub mod deposit_log { use ssz::Decode; @@ -727,11 +709,11 @@ impl HttpJsonRpc { .await } - pub async fn get_block_by_hash_with_txns( + pub async fn get_block_by_hash_with_txns( &self, block_hash: ExecutionBlockHash, fork: ForkName, - ) -> Result>, Error> { + ) -> Result>, Error> { let params = json!([block_hash, true]); Ok(Some(match fork { ForkName::Merge => ExecutionBlockWithTransactions::Merge( @@ -758,6 +740,14 @@ impl HttpJsonRpc { ) .await?, ), + ForkName::Electra => ExecutionBlockWithTransactions::Electra( + self.rpc_request( + ETH_GET_BLOCK_BY_HASH, + params, + ETH_GET_BLOCK_BY_HASH_TIMEOUT * self.execution_timeout_multiplier, + ) + .await?, + ), ForkName::Base | ForkName::Altair => { return Err(Error::UnsupportedForkVariant(format!( "called get_block_by_hash_with_txns with fork {:?}", @@ -767,9 +757,9 @@ impl HttpJsonRpc { })) } - pub async fn new_payload_v1( + pub async fn new_payload_v1( &self, - execution_payload: ExecutionPayload, + execution_payload: ExecutionPayload, ) -> Result { let params = json!([JsonExecutionPayload::from(execution_payload)]); @@ -784,9 +774,9 @@ impl HttpJsonRpc { Ok(response.into()) } - pub async fn new_payload_v2( + pub async fn new_payload_v2( &self, - execution_payload: ExecutionPayload, + execution_payload: ExecutionPayload, ) -> Result { let params = json!([JsonExecutionPayload::from(execution_payload)]); @@ -801,12 +791,12 @@ impl HttpJsonRpc { Ok(response.into()) } - pub async fn new_payload_v3( + pub async fn new_payload_v3_deneb( &self, - new_payload_request_deneb: NewPayloadRequestDeneb, + new_payload_request_deneb: NewPayloadRequestDeneb<'_, E>, ) -> Result { let params = json!([ - JsonExecutionPayload::V3(new_payload_request_deneb.execution_payload.into()), + JsonExecutionPayload::V3(new_payload_request_deneb.execution_payload.clone().into()), new_payload_request_deneb.versioned_hashes, new_payload_request_deneb.parent_beacon_block_root, ]); @@ -822,13 +812,34 @@ impl HttpJsonRpc { Ok(response.into()) } - pub async fn get_payload_v1( + pub async fn new_payload_v3_electra( + &self, + new_payload_request_electra: NewPayloadRequestElectra<'_, E>, + ) -> Result { + let params = json!([ + JsonExecutionPayload::V4(new_payload_request_electra.execution_payload.clone().into()), + new_payload_request_electra.versioned_hashes, + new_payload_request_electra.parent_beacon_block_root, + ]); + + let response: JsonPayloadStatusV1 = self + .rpc_request( + ENGINE_NEW_PAYLOAD_V3, + params, + ENGINE_NEW_PAYLOAD_TIMEOUT * self.execution_timeout_multiplier, + ) + .await?; + + Ok(response.into()) + } + + pub async fn get_payload_v1( &self, payload_id: PayloadId, - ) -> Result, Error> { + ) -> Result, Error> { let params = json!([JsonPayloadIdRequest::from(payload_id)]); - let payload_v1: JsonExecutionPayloadV1 = self + let payload_v1: JsonExecutionPayloadV1 = self .rpc_request( ENGINE_GET_PAYLOAD_V1, params, @@ -845,16 +856,16 @@ impl HttpJsonRpc { })) } - pub async fn get_payload_v2( + pub async fn get_payload_v2( &self, fork_name: ForkName, payload_id: PayloadId, - ) -> Result, Error> { + ) -> Result, Error> { let params = json!([JsonPayloadIdRequest::from(payload_id)]); match fork_name { ForkName::Merge => { - let response: JsonGetPayloadResponseV1 = self + let response: JsonGetPayloadResponseV1 = self .rpc_request( ENGINE_GET_PAYLOAD_V2, params, @@ -864,7 +875,7 @@ impl HttpJsonRpc { Ok(JsonGetPayloadResponse::V1(response).into()) } ForkName::Capella => { - let response: JsonGetPayloadResponseV2 = self + let response: JsonGetPayloadResponseV2 = self .rpc_request( ENGINE_GET_PAYLOAD_V2, params, @@ -873,22 +884,22 @@ impl HttpJsonRpc { .await?; Ok(JsonGetPayloadResponse::V2(response).into()) } - ForkName::Base | ForkName::Altair | ForkName::Deneb => Err( + ForkName::Base | ForkName::Altair | ForkName::Deneb | ForkName::Electra => Err( Error::UnsupportedForkVariant(format!("called get_payload_v2 with {}", fork_name)), ), } } - pub async fn get_payload_v3( + pub async fn get_payload_v3( &self, fork_name: ForkName, payload_id: PayloadId, - ) -> Result, Error> { + ) -> Result, Error> { let params = json!([JsonPayloadIdRequest::from(payload_id)]); match fork_name { ForkName::Deneb => { - let response: JsonGetPayloadResponseV3 = self + let response: JsonGetPayloadResponseV3 = self .rpc_request( ENGINE_GET_PAYLOAD_V3, params, @@ -897,6 +908,16 @@ impl HttpJsonRpc { .await?; Ok(JsonGetPayloadResponse::V3(response).into()) } + ForkName::Electra => { + let response: JsonGetPayloadResponseV4 = self + .rpc_request( + ENGINE_GET_PAYLOAD_V3, + params, + ENGINE_GET_PAYLOAD_TIMEOUT * self.execution_timeout_multiplier, + ) + .await?; + Ok(JsonGetPayloadResponse::V4(response).into()) + } ForkName::Base | ForkName::Altair | ForkName::Merge | ForkName::Capella => Err( Error::UnsupportedForkVariant(format!("called get_payload_v3 with {}", fork_name)), ), @@ -1013,38 +1034,29 @@ impl HttpJsonRpc { pub async fn exchange_capabilities(&self) -> Result { let params = json!([LIGHTHOUSE_CAPABILITIES]); - let response: Result, _> = self + let capabilities: HashSet = self .rpc_request( ENGINE_EXCHANGE_CAPABILITIES, params, ENGINE_EXCHANGE_CAPABILITIES_TIMEOUT * self.execution_timeout_multiplier, ) - .await; + .await?; - match response { - // TODO (mark): rip this out once we are post capella on mainnet - Err(error) => match error { - Error::ServerMessage { code, message: _ } if code == METHOD_NOT_FOUND_CODE => { - Ok(PRE_CAPELLA_ENGINE_CAPABILITIES) - } - _ => Err(error), - }, - Ok(capabilities) => Ok(EngineCapabilities { - new_payload_v1: capabilities.contains(ENGINE_NEW_PAYLOAD_V1), - new_payload_v2: capabilities.contains(ENGINE_NEW_PAYLOAD_V2), - new_payload_v3: capabilities.contains(ENGINE_NEW_PAYLOAD_V3), - forkchoice_updated_v1: capabilities.contains(ENGINE_FORKCHOICE_UPDATED_V1), - forkchoice_updated_v2: capabilities.contains(ENGINE_FORKCHOICE_UPDATED_V2), - forkchoice_updated_v3: capabilities.contains(ENGINE_FORKCHOICE_UPDATED_V3), - get_payload_bodies_by_hash_v1: capabilities - .contains(ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1), - get_payload_bodies_by_range_v1: capabilities - .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), - get_payload_v3: capabilities.contains(ENGINE_GET_PAYLOAD_V3), - }), - } + Ok(EngineCapabilities { + new_payload_v1: capabilities.contains(ENGINE_NEW_PAYLOAD_V1), + new_payload_v2: capabilities.contains(ENGINE_NEW_PAYLOAD_V2), + new_payload_v3: capabilities.contains(ENGINE_NEW_PAYLOAD_V3), + forkchoice_updated_v1: capabilities.contains(ENGINE_FORKCHOICE_UPDATED_V1), + forkchoice_updated_v2: capabilities.contains(ENGINE_FORKCHOICE_UPDATED_V2), + forkchoice_updated_v3: capabilities.contains(ENGINE_FORKCHOICE_UPDATED_V3), + get_payload_bodies_by_hash_v1: capabilities + .contains(ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1), + get_payload_bodies_by_range_v1: capabilities + .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), + get_payload_v3: capabilities.contains(ENGINE_GET_PAYLOAD_V3), + }) } pub async fn clear_exchange_capabilties_cache(&self) { @@ -1077,9 +1089,9 @@ impl HttpJsonRpc { // automatically selects the latest version of // new_payload that the execution engine supports - pub async fn new_payload( + pub async fn new_payload( &self, - new_payload_request: NewPayloadRequest, + new_payload_request: NewPayloadRequest<'_, E>, ) -> Result { let engine_capabilities = self.get_engine_capabilities(None).await?; match new_payload_request { @@ -1096,7 +1108,15 @@ impl HttpJsonRpc { } NewPayloadRequest::Deneb(new_payload_request_deneb) => { if engine_capabilities.new_payload_v3 { - self.new_payload_v3(new_payload_request_deneb).await + self.new_payload_v3_deneb(new_payload_request_deneb).await + } else { + Err(Error::RequiredMethodUnsupported("engine_newPayloadV3")) + } + } + NewPayloadRequest::Electra(new_payload_request_electra) => { + if engine_capabilities.new_payload_v3 { + self.new_payload_v3_electra(new_payload_request_electra) + .await } else { Err(Error::RequiredMethodUnsupported("engine_newPayloadV3")) } @@ -1106,11 +1126,11 @@ impl HttpJsonRpc { // automatically selects the latest version of // get_payload that the execution engine supports - pub async fn get_payload( + pub async fn get_payload( &self, fork_name: ForkName, payload_id: PayloadId, - ) -> Result, Error> { + ) -> Result, Error> { let engine_capabilities = self.get_engine_capabilities(None).await?; match fork_name { ForkName::Merge | ForkName::Capella => { @@ -1122,7 +1142,7 @@ impl HttpJsonRpc { Err(Error::RequiredMethodUnsupported("engine_getPayload")) } } - ForkName::Deneb => { + ForkName::Deneb | ForkName::Electra => { if engine_capabilities.get_payload_v3 { self.get_payload_v3(fork_name, payload_id).await } else { @@ -1191,7 +1211,7 @@ mod test { use std::future::Future; use std::str::FromStr; use std::sync::Arc; - use types::{ExecutionPayloadMerge, MainnetEthSpec, Transactions, Unsigned, VariableList}; + use types::{MainnetEthSpec, Unsigned}; struct Tester { server: MockServer, @@ -1802,7 +1822,7 @@ mod test { "extraData":"0x", "baseFeePerGas":"0x7", "blockHash":"0x6359b8381a370e2f54072a5784ddd78b6ed024991558c511d4452eb4f6ac898c", - "transactions":[] + "transactions":[], } })], |client| async move { @@ -1826,7 +1846,7 @@ mod test { extra_data: vec![].into(), base_fee_per_gas: Uint256::from(7), block_hash: ExecutionBlockHash::from_str("0x6359b8381a370e2f54072a5784ddd78b6ed024991558c511d4452eb4f6ac898c").unwrap(), - transactions: vec![].into(), + transactions: vec![].into(), }); assert_eq!(payload, expected); @@ -1873,7 +1893,7 @@ mod test { "extraData":"0x", "baseFeePerGas":"0x7", "blockHash":"0x3559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858", - "transactions":[] + "transactions":[], }], }) ) diff --git a/beacon_node/execution_layer/src/engine_api/json_structures.rs b/beacon_node/execution_layer/src/engine_api/json_structures.rs index e8641be795..9f2387ae31 100644 --- a/beacon_node/execution_layer/src/engine_api/json_structures.rs +++ b/beacon_node/execution_layer/src/engine_api/json_structures.rs @@ -4,10 +4,7 @@ use strum::EnumString; use superstruct::superstruct; use types::beacon_block_body::KzgCommitments; use types::blob_sidecar::BlobsList; -use types::{ - EthSpec, ExecutionBlockHash, ExecutionPayload, ExecutionPayloadCapella, ExecutionPayloadDeneb, - ExecutionPayloadMerge, FixedVector, Transactions, Unsigned, VariableList, Withdrawal, -}; +use types::{FixedVector, Unsigned}; #[derive(Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -63,23 +60,23 @@ pub struct JsonPayloadIdResponse { } #[superstruct( - variants(V1, V2, V3), + variants(V1, V2, V3, V4), variant_attributes( derive(Debug, PartialEq, Default, Serialize, Deserialize,), - serde(bound = "T: EthSpec", rename_all = "camelCase"), + serde(bound = "E: EthSpec", rename_all = "camelCase"), ), cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"), partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant") )] #[derive(Debug, PartialEq, Serialize, Deserialize)] -#[serde(bound = "T: EthSpec", rename_all = "camelCase", untagged)] -pub struct JsonExecutionPayload { +#[serde(bound = "E: EthSpec", rename_all = "camelCase", untagged)] +pub struct JsonExecutionPayload { pub parent_hash: ExecutionBlockHash, pub fee_recipient: Address, pub state_root: Hash256, pub receipts_root: Hash256, #[serde(with = "serde_logs_bloom")] - pub logs_bloom: FixedVector, + pub logs_bloom: FixedVector, pub prev_randao: Hash256, #[serde(with = "serde_utils::u64_hex_be")] pub block_number: u64, @@ -90,24 +87,24 @@ pub struct JsonExecutionPayload { #[serde(with = "serde_utils::u64_hex_be")] pub timestamp: u64, #[serde(with = "ssz_types::serde_utils::hex_var_list")] - pub extra_data: VariableList, + pub extra_data: VariableList, #[serde(with = "serde_utils::u256_hex_be")] pub base_fee_per_gas: Uint256, pub block_hash: ExecutionBlockHash, #[serde(with = "ssz_types::serde_utils::list_of_hex_var_list")] - pub transactions: Transactions, - #[superstruct(only(V2, V3))] - pub withdrawals: VariableList, - #[superstruct(only(V3))] + pub transactions: Transactions, + #[superstruct(only(V2, V3, V4))] + pub withdrawals: VariableList, + #[superstruct(only(V3, V4))] #[serde(with = "serde_utils::u64_hex_be")] pub blob_gas_used: u64, - #[superstruct(only(V3))] + #[superstruct(only(V3, V4))] #[serde(with = "serde_utils::u64_hex_be")] pub excess_blob_gas: u64, } -impl From> for JsonExecutionPayloadV1 { - fn from(payload: ExecutionPayloadMerge) -> Self { +impl From> for JsonExecutionPayloadV1 { + fn from(payload: ExecutionPayloadMerge) -> Self { JsonExecutionPayloadV1 { parent_hash: payload.parent_hash, fee_recipient: payload.fee_recipient, @@ -126,8 +123,8 @@ impl From> for JsonExecutionPayloadV1 { } } } -impl From> for JsonExecutionPayloadV2 { - fn from(payload: ExecutionPayloadCapella) -> Self { +impl From> for JsonExecutionPayloadV2 { + fn from(payload: ExecutionPayloadCapella) -> Self { JsonExecutionPayloadV2 { parent_hash: payload.parent_hash, fee_recipient: payload.fee_recipient, @@ -152,8 +149,8 @@ impl From> for JsonExecutionPayloadV2 } } } -impl From> for JsonExecutionPayloadV3 { - fn from(payload: ExecutionPayloadDeneb) -> Self { +impl From> for JsonExecutionPayloadV3 { + fn from(payload: ExecutionPayloadDeneb) -> Self { JsonExecutionPayloadV3 { parent_hash: payload.parent_hash, fee_recipient: payload.fee_recipient, @@ -181,18 +178,48 @@ impl From> for JsonExecutionPayloadV3 { } } -impl From> for JsonExecutionPayload { - fn from(execution_payload: ExecutionPayload) -> Self { - match execution_payload { - ExecutionPayload::Merge(payload) => JsonExecutionPayload::V1(payload.into()), - ExecutionPayload::Capella(payload) => JsonExecutionPayload::V2(payload.into()), - ExecutionPayload::Deneb(payload) => JsonExecutionPayload::V3(payload.into()), +impl From> for JsonExecutionPayloadV4 { + fn from(payload: ExecutionPayloadElectra) -> Self { + JsonExecutionPayloadV4 { + parent_hash: payload.parent_hash, + fee_recipient: payload.fee_recipient, + state_root: payload.state_root, + receipts_root: payload.receipts_root, + logs_bloom: payload.logs_bloom, + prev_randao: payload.prev_randao, + block_number: payload.block_number, + gas_limit: payload.gas_limit, + gas_used: payload.gas_used, + timestamp: payload.timestamp, + extra_data: payload.extra_data, + base_fee_per_gas: payload.base_fee_per_gas, + block_hash: payload.block_hash, + transactions: payload.transactions, + withdrawals: payload + .withdrawals + .into_iter() + .map(Into::into) + .collect::>() + .into(), + blob_gas_used: payload.blob_gas_used, + excess_blob_gas: payload.excess_blob_gas, } } } -impl From> for ExecutionPayloadMerge { - fn from(payload: JsonExecutionPayloadV1) -> Self { +impl From> for JsonExecutionPayload { + fn from(execution_payload: ExecutionPayload) -> Self { + match execution_payload { + ExecutionPayload::Merge(payload) => JsonExecutionPayload::V1(payload.into()), + ExecutionPayload::Capella(payload) => JsonExecutionPayload::V2(payload.into()), + ExecutionPayload::Deneb(payload) => JsonExecutionPayload::V3(payload.into()), + ExecutionPayload::Electra(payload) => JsonExecutionPayload::V4(payload.into()), + } + } +} + +impl From> for ExecutionPayloadMerge { + fn from(payload: JsonExecutionPayloadV1) -> Self { ExecutionPayloadMerge { parent_hash: payload.parent_hash, fee_recipient: payload.fee_recipient, @@ -211,8 +238,8 @@ impl From> for ExecutionPayloadMerge { } } } -impl From> for ExecutionPayloadCapella { - fn from(payload: JsonExecutionPayloadV2) -> Self { +impl From> for ExecutionPayloadCapella { + fn from(payload: JsonExecutionPayloadV2) -> Self { ExecutionPayloadCapella { parent_hash: payload.parent_hash, fee_recipient: payload.fee_recipient, @@ -237,8 +264,9 @@ impl From> for ExecutionPayloadCapella } } } -impl From> for ExecutionPayloadDeneb { - fn from(payload: JsonExecutionPayloadV3) -> Self { + +impl From> for ExecutionPayloadDeneb { + fn from(payload: JsonExecutionPayloadV3) -> Self { ExecutionPayloadDeneb { parent_hash: payload.parent_hash, fee_recipient: payload.fee_recipient, @@ -266,44 +294,76 @@ impl From> for ExecutionPayloadDeneb { } } -impl From> for ExecutionPayload { - fn from(json_execution_payload: JsonExecutionPayload) -> Self { +impl From> for ExecutionPayloadElectra { + fn from(payload: JsonExecutionPayloadV4) -> Self { + ExecutionPayloadElectra { + parent_hash: payload.parent_hash, + fee_recipient: payload.fee_recipient, + state_root: payload.state_root, + receipts_root: payload.receipts_root, + logs_bloom: payload.logs_bloom, + prev_randao: payload.prev_randao, + block_number: payload.block_number, + gas_limit: payload.gas_limit, + gas_used: payload.gas_used, + timestamp: payload.timestamp, + extra_data: payload.extra_data, + base_fee_per_gas: payload.base_fee_per_gas, + block_hash: payload.block_hash, + transactions: payload.transactions, + withdrawals: payload + .withdrawals + .into_iter() + .map(Into::into) + .collect::>() + .into(), + blob_gas_used: payload.blob_gas_used, + excess_blob_gas: payload.excess_blob_gas, + } + } +} + +impl From> for ExecutionPayload { + fn from(json_execution_payload: JsonExecutionPayload) -> Self { match json_execution_payload { JsonExecutionPayload::V1(payload) => ExecutionPayload::Merge(payload.into()), JsonExecutionPayload::V2(payload) => ExecutionPayload::Capella(payload.into()), JsonExecutionPayload::V3(payload) => ExecutionPayload::Deneb(payload.into()), + JsonExecutionPayload::V4(payload) => ExecutionPayload::Electra(payload.into()), } } } #[superstruct( - variants(V1, V2, V3), + variants(V1, V2, V3, V4), variant_attributes( derive(Debug, PartialEq, Serialize, Deserialize), - serde(bound = "T: EthSpec", rename_all = "camelCase") + serde(bound = "E: EthSpec", rename_all = "camelCase") ), cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"), partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant") )] #[derive(Debug, PartialEq, Serialize, Deserialize)] #[serde(untagged)] -pub struct JsonGetPayloadResponse { +pub struct JsonGetPayloadResponse { #[superstruct(only(V1), partial_getter(rename = "execution_payload_v1"))] - pub execution_payload: JsonExecutionPayloadV1, + pub execution_payload: JsonExecutionPayloadV1, #[superstruct(only(V2), partial_getter(rename = "execution_payload_v2"))] - pub execution_payload: JsonExecutionPayloadV2, + pub execution_payload: JsonExecutionPayloadV2, #[superstruct(only(V3), partial_getter(rename = "execution_payload_v3"))] - pub execution_payload: JsonExecutionPayloadV3, + pub execution_payload: JsonExecutionPayloadV3, + #[superstruct(only(V4), partial_getter(rename = "execution_payload_v4"))] + pub execution_payload: JsonExecutionPayloadV4, #[serde(with = "serde_utils::u256_hex_be")] pub block_value: Uint256, - #[superstruct(only(V3))] - pub blobs_bundle: JsonBlobsBundleV1, - #[superstruct(only(V3))] + #[superstruct(only(V3, V4))] + pub blobs_bundle: JsonBlobsBundleV1, + #[superstruct(only(V3, V4))] pub should_override_builder: bool, } -impl From> for GetPayloadResponse { - fn from(json_get_payload_response: JsonGetPayloadResponse) -> Self { +impl From> for GetPayloadResponse { + fn from(json_get_payload_response: JsonGetPayloadResponse) -> Self { match json_get_payload_response { JsonGetPayloadResponse::V1(response) => { GetPayloadResponse::Merge(GetPayloadResponseMerge { @@ -325,6 +385,14 @@ impl From> for GetPayloadResponse { should_override_builder: response.should_override_builder, }) } + JsonGetPayloadResponse::V4(response) => { + GetPayloadResponse::Electra(GetPayloadResponseElectra { + execution_payload: response.execution_payload.into(), + block_value: response.block_value, + blobs_bundle: response.blobs_bundle.into(), + should_override_builder: response.should_override_builder, + }) + } } } } diff --git a/beacon_node/execution_layer/src/engine_api/new_payload_request.rs b/beacon_node/execution_layer/src/engine_api/new_payload_request.rs new file mode 100644 index 0000000000..6b6df13b70 --- /dev/null +++ b/beacon_node/execution_layer/src/engine_api/new_payload_request.rs @@ -0,0 +1,352 @@ +use crate::{block_hash::calculate_execution_block_hash, metrics, Error}; + +use crate::versioned_hashes::verify_versioned_hashes; +use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash; +use superstruct::superstruct; +use types::{ + BeaconBlockRef, BeaconStateError, EthSpec, ExecutionBlockHash, ExecutionPayload, + ExecutionPayloadRef, Hash256, VersionedHash, +}; +use types::{ + ExecutionPayloadCapella, ExecutionPayloadDeneb, ExecutionPayloadElectra, ExecutionPayloadMerge, +}; + +#[superstruct( + variants(Merge, Capella, Deneb, Electra), + variant_attributes(derive(Clone, Debug, PartialEq),), + map_into(ExecutionPayload), + map_ref_into(ExecutionPayloadRef), + cast_error( + ty = "BeaconStateError", + expr = "BeaconStateError::IncorrectStateVariant" + ), + partial_getter_error( + ty = "BeaconStateError", + expr = "BeaconStateError::IncorrectStateVariant" + ) +)] +#[derive(Clone, Debug, PartialEq)] +pub struct NewPayloadRequest<'block, E: EthSpec> { + #[superstruct(only(Merge), partial_getter(rename = "execution_payload_merge"))] + pub execution_payload: &'block ExecutionPayloadMerge, + #[superstruct(only(Capella), partial_getter(rename = "execution_payload_capella"))] + pub execution_payload: &'block ExecutionPayloadCapella, + #[superstruct(only(Deneb), partial_getter(rename = "execution_payload_deneb"))] + pub execution_payload: &'block ExecutionPayloadDeneb, + #[superstruct(only(Electra), partial_getter(rename = "execution_payload_electra"))] + pub execution_payload: &'block ExecutionPayloadElectra, + #[superstruct(only(Deneb, Electra))] + pub versioned_hashes: Vec, + #[superstruct(only(Deneb, Electra))] + pub parent_beacon_block_root: Hash256, +} + +impl<'block, E: EthSpec> NewPayloadRequest<'block, E> { + pub fn parent_hash(&self) -> ExecutionBlockHash { + match self { + Self::Merge(payload) => payload.execution_payload.parent_hash, + Self::Capella(payload) => payload.execution_payload.parent_hash, + Self::Deneb(payload) => payload.execution_payload.parent_hash, + Self::Electra(payload) => payload.execution_payload.parent_hash, + } + } + + pub fn block_hash(&self) -> ExecutionBlockHash { + match self { + Self::Merge(payload) => payload.execution_payload.block_hash, + Self::Capella(payload) => payload.execution_payload.block_hash, + Self::Deneb(payload) => payload.execution_payload.block_hash, + Self::Electra(payload) => payload.execution_payload.block_hash, + } + } + + pub fn block_number(&self) -> u64 { + match self { + Self::Merge(payload) => payload.execution_payload.block_number, + Self::Capella(payload) => payload.execution_payload.block_number, + Self::Deneb(payload) => payload.execution_payload.block_number, + Self::Electra(payload) => payload.execution_payload.block_number, + } + } + + pub fn execution_payload_ref(&self) -> ExecutionPayloadRef<'block, E> { + match self { + Self::Merge(request) => ExecutionPayloadRef::Merge(request.execution_payload), + Self::Capella(request) => ExecutionPayloadRef::Capella(request.execution_payload), + Self::Deneb(request) => ExecutionPayloadRef::Deneb(request.execution_payload), + Self::Electra(request) => ExecutionPayloadRef::Electra(request.execution_payload), + } + } + + pub fn into_execution_payload(self) -> ExecutionPayload { + match self { + Self::Merge(request) => ExecutionPayload::Merge(request.execution_payload.clone()), + Self::Capella(request) => ExecutionPayload::Capella(request.execution_payload.clone()), + Self::Deneb(request) => ExecutionPayload::Deneb(request.execution_payload.clone()), + Self::Electra(request) => ExecutionPayload::Electra(request.execution_payload.clone()), + } + } + + /// Performs the required verifications of the payload when the chain is optimistically syncing. + /// + /// ## Specification + /// + /// Performs the verifications in the `verify_and_notify_new_payload` function: + /// + /// https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.2/specs/deneb/beacon-chain.md#modified-verify_and_notify_new_payload + pub fn perform_optimistic_sync_verifications(&self) -> Result<(), Error> { + self.verify_payload_block_hash()?; + self.verify_versioned_hashes()?; + + Ok(()) + } + + /// Verify the block hash is consistent locally within Lighthouse. + /// + /// ## Specification + /// + /// Equivalent to `is_valid_block_hash` in the spec: + /// https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.2/specs/deneb/beacon-chain.md#is_valid_block_hash + pub fn verify_payload_block_hash(&self) -> Result<(), Error> { + let payload = self.execution_payload_ref(); + let parent_beacon_block_root = self.parent_beacon_block_root().ok().cloned(); + + let _timer = metrics::start_timer(&metrics::EXECUTION_LAYER_VERIFY_BLOCK_HASH); + + let (header_hash, rlp_transactions_root) = + calculate_execution_block_hash(payload, parent_beacon_block_root); + + if header_hash != self.block_hash() { + return Err(Error::BlockHashMismatch { + computed: header_hash, + payload: payload.block_hash(), + transactions_root: rlp_transactions_root, + }); + } + + Ok(()) + } + + /// Verify the versioned hashes computed by the blob transactions match the versioned hashes computed from the commitments. + /// + /// ## Specification + /// + /// Equivalent to `is_valid_versioned_hashes` in the spec: + /// https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.2/specs/deneb/beacon-chain.md#is_valid_versioned_hashes + pub fn verify_versioned_hashes(&self) -> Result<(), Error> { + if let Ok(versioned_hashes) = self.versioned_hashes() { + verify_versioned_hashes(self.execution_payload_ref(), versioned_hashes) + .map_err(Error::VerifyingVersionedHashes)?; + } + Ok(()) + } +} + +impl<'a, E: EthSpec> TryFrom> for NewPayloadRequest<'a, E> { + type Error = BeaconStateError; + + fn try_from(block: BeaconBlockRef<'a, E>) -> Result { + match block { + BeaconBlockRef::Base(_) | BeaconBlockRef::Altair(_) => { + Err(Self::Error::IncorrectStateVariant) + } + BeaconBlockRef::Merge(block_ref) => Ok(Self::Merge(NewPayloadRequestMerge { + execution_payload: &block_ref.body.execution_payload.execution_payload, + })), + BeaconBlockRef::Capella(block_ref) => Ok(Self::Capella(NewPayloadRequestCapella { + execution_payload: &block_ref.body.execution_payload.execution_payload, + })), + BeaconBlockRef::Deneb(block_ref) => Ok(Self::Deneb(NewPayloadRequestDeneb { + execution_payload: &block_ref.body.execution_payload.execution_payload, + versioned_hashes: block_ref + .body + .blob_kzg_commitments + .iter() + .map(kzg_commitment_to_versioned_hash) + .collect(), + parent_beacon_block_root: block_ref.parent_root, + })), + BeaconBlockRef::Electra(block_ref) => Ok(Self::Electra(NewPayloadRequestElectra { + execution_payload: &block_ref.body.execution_payload.execution_payload, + versioned_hashes: block_ref + .body + .blob_kzg_commitments + .iter() + .map(kzg_commitment_to_versioned_hash) + .collect(), + parent_beacon_block_root: block_ref.parent_root, + })), + } + } +} + +impl<'a, E: EthSpec> TryFrom> for NewPayloadRequest<'a, E> { + type Error = BeaconStateError; + + fn try_from(payload: ExecutionPayloadRef<'a, E>) -> Result { + match payload { + ExecutionPayloadRef::Merge(payload) => Ok(Self::Merge(NewPayloadRequestMerge { + execution_payload: payload, + })), + ExecutionPayloadRef::Capella(payload) => Ok(Self::Capella(NewPayloadRequestCapella { + execution_payload: payload, + })), + ExecutionPayloadRef::Deneb(_) => Err(Self::Error::IncorrectStateVariant), + ExecutionPayloadRef::Electra(_) => Err(Self::Error::IncorrectStateVariant), + } + } +} + +#[cfg(test)] +mod test { + use crate::versioned_hashes::Error as VersionedHashError; + use crate::{Error, NewPayloadRequest}; + use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash; + use types::{BeaconBlock, ExecPayload, ExecutionBlockHash, Hash256, MainnetEthSpec}; + + #[test] + fn test_optimistic_sync_verifications_valid_block() { + let beacon_block = get_valid_beacon_block(); + let new_payload_request = NewPayloadRequest::try_from(beacon_block.to_ref()) + .expect("should create new payload request"); + + assert!( + new_payload_request + .perform_optimistic_sync_verifications() + .is_ok(), + "validations should pass" + ); + } + + #[test] + fn test_optimistic_sync_verifications_bad_block_hash() { + let mut beacon_block = get_valid_beacon_block(); + let correct_block_hash = beacon_block + .body() + .execution_payload() + .expect("should get payload") + .block_hash(); + let invalid_block_hash = ExecutionBlockHash(Hash256::repeat_byte(0x42)); + + // now mutate the block hash + beacon_block + .body_mut() + .execution_payload_deneb_mut() + .expect("should get payload") + .execution_payload + .block_hash = invalid_block_hash; + + let new_payload_request = NewPayloadRequest::try_from(beacon_block.to_ref()) + .expect("should create new payload request"); + let verification_result = new_payload_request.perform_optimistic_sync_verifications(); + println!("verification_result: {:?}", verification_result); + let got_expected_result = match verification_result { + Err(Error::BlockHashMismatch { + computed, payload, .. + }) => computed == correct_block_hash && payload == invalid_block_hash, + _ => false, + }; + assert!(got_expected_result, "should return expected error"); + } + + #[test] + fn test_optimistic_sync_verifications_bad_versioned_hashes() { + let mut beacon_block = get_valid_beacon_block(); + + let mut commitments: Vec<_> = beacon_block + .body() + .blob_kzg_commitments() + .expect("should get commitments") + .clone() + .into(); + + let correct_versioned_hash = kzg_commitment_to_versioned_hash( + commitments.last().expect("should get last commitment"), + ); + + // mutate the last commitment + commitments + .last_mut() + .expect("should get last commitment") + .0[0] = 0x42; + + // calculate versioned hash from mutated commitment + let bad_versioned_hash = kzg_commitment_to_versioned_hash( + commitments.last().expect("should get last commitment"), + ); + + *beacon_block + .body_mut() + .blob_kzg_commitments_mut() + .expect("should get commitments") = commitments.into(); + + let new_payload_request = NewPayloadRequest::try_from(beacon_block.to_ref()) + .expect("should create new payload request"); + let verification_result = new_payload_request.perform_optimistic_sync_verifications(); + println!("verification_result: {:?}", verification_result); + + let got_expected_result = match verification_result { + Err(Error::VerifyingVersionedHashes(VersionedHashError::VersionHashMismatch { + expected, + found, + })) => expected == bad_versioned_hash && found == correct_versioned_hash, + _ => false, + }; + assert!(got_expected_result, "should return expected error"); + } + + fn get_valid_beacon_block() -> BeaconBlock { + BeaconBlock::Deneb(serde_json::from_str(r#"{ + "slot": "88160", + "proposer_index": "583", + "parent_root": "0x60770cd86a497ca3aa2e91f1687aa3ebafac87af52c30a920b5f40bd9e930eb6", + "state_root": "0x4a0e0abbcbcf576f2cb7387c4289ab13b8a128e32127642f056143d6164941a6", + "body": { + "randao_reveal": "0xb5253d5739496abc4f67c7c92e39e46cca452c2fdfc5275e3e0426a012aa62df82f47f7dece348e28db4bb212f0e793d187120bbd47b8031ed79344116eb4128f0ce0b05ba18cd615bb13966c1bd7d89e23cc769c8e4d8e4a63755f623ac3bed", + "eth1_data": { + "deposit_root": "0xe4785ac914d8673797f886e3151ce2647f81ae070c7ddb6845e65fd1c47d1222", + "deposit_count": "1181", + "block_hash": "0x010671bdfbfce6b0071984a06a7ded6deef13b4f8fdbae402c606a7a0c8780d1" + }, + "graffiti": "0x6c6f6465737461722f6765746800000000000000000000000000000000000000", + "proposer_slashings": [], + "attester_slashings": [], + "attestations": [], + "deposits": [], + "voluntary_exits": [], + "sync_aggregate": { + "sync_committee_bits": "0xfebffffffebfff7fff7f7fffbbefffff6affffffffbfffffefffebfffdbf77fff7fd77ffffefffdff7ffffeffffffe7e5ffffffdefffff7ffbffff7fffffffff", + "sync_committee_signature": "0x91939b5baf2a6f52d405b6dd396f5346ec435eca7d25912c91cc6a2f7030d870d68bebe4f2b21872a06929ff4cf3e5e9191053cb43eb24ebe34b9a75fb88a3acd06baf329c87f68bd664b49891260c698d7bca0f5365870b5b2b3a76f582156c" + }, + "execution_payload": { + "parent_hash": "0xa6f3ed782a992f79ad38da2af91b3e8923c71b801c50bc9033bb35a2e1da885f", + "fee_recipient": "0xf97e180c050e5ab072211ad2c213eb5aee4df134", + "state_root": "0x3bfd1a7f309ed35048c349a8daf01815bdc09a6d5df86ea77d1056f248ba2017", + "receipts_root": "0xcb5b8ffea57cd0fa87194d49bc8bb7fad08c93c9934b886489503c328d15fd36", + "logs_bloom": "0x002000000000000000000000800000000000000000001040000000000000000000000001000000000000000000000000000000000000100000000020000c0800000000000000008000000008000000200000800000000000000000000000000000000000000000008000000000008000000000000000000002000010000000000000000000000000000000000000000000000000000000080000004000000000800000000000000000000100000000000000000000000000000000000800000000000102000000000000000000000000000000080000001000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "prev_randao": "0xb2693020177d99ffbd4c267023be172d759e7306ff51b0e7d677d3148fbd7f1d", + "block_number": "74807", + "gas_limit": "30000000", + "gas_used": "128393", + "timestamp": "1697039520", + "extra_data": "0xd883010d03846765746888676f312e32312e31856c696e7578", + "base_fee_per_gas": "7", + "block_hash": "0xc64f3a43c64aeb98518a237f6279fa03095b9f95ca673c860ad7f16fb9340062", + "transactions": [ + "0x02f9017a8501a1f0ff4382317585012a05f2008512a05f2000830249f094c1b0bc605e2c808aa0867bfc98e51a1fe3e9867f80b901040cc7326300000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000036e534e16b8920d000000000000000000000000fb3e9c7cb92443931ee6b5b9728598d4eb9618c1000000000000000000000000fc7360b3b28cf4204268a8354dbec60720d155d2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000009a054a063f0fe7b9c68de8df91aaa5e96c15ab540000000000000000000000000c8d41b8fcc066cdabaf074d78e5153e8ce018a9c080a07dd9be0d014ffcd5b6883d0917c66b74ba51f0d976c8fc5674af192af6fa9450a02dad2c660974c125f5f22b1e6e8862a292e08cc2b4cafda35af650ee62868a43", + "0x03f8db8501a1f0ff430d84773594008504a817c8008252089454e594b6de0aa4b0188cd1549dd7ba715a455d078080c08504a817c800f863a001253ce00f525e3495cffa0b865eadb90a4c5ee812185cc796af74b6ec0a5dd7a0010720372a4d7dcab84413ed0cfc164fb91fb6ef1562ec2f7a82e912a1d9e129a0015a73e97950397896ed2c47dcab7c0360220bcfb413a8f210a7b6e6264e698880a04402cb0f13c17ef41dca106b1e1520c7aadcbe62984d81171e29914f587d67c1a02db62a8edb581917958e4a3884e7eececbaec114c5ee496e238033e896f997ac" + ], + "withdrawals": [], + "blob_gas_used": "393216", + "excess_blob_gas": "58720256" + }, + "bls_to_execution_changes": [], + "blob_kzg_commitments": [ + "0xa7accb7a25224a8c2e0cee9cd569fc1798665bfbfe780e08945fa9098ec61da4061f5b04e750a88d3340a801850a54fa", + "0xac7b47f99836510ae9076dc5f5da1f370679dea1d47073307a14cbb125cdc7822ae619637135777cb40e13d897fd00a7", + "0x997794110b9655833a88ad5a4ec40a3dc7964877bfbeb04ca1abe1d51bdc43e20e4c5757028896d298d7da954a6f14a1" + ] + } + }"#).expect("should decode")) + } +} diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 868d819446..30930318ef 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -7,6 +7,7 @@ use crate::payload_cache::PayloadCache; use arc_swap::ArcSwapOption; use auth::{strip_prefix, Auth, JwtKey}; +pub use block_hash::calculate_execution_block_hash; use builder_client::BuilderHttpClient; pub use engine_api::EngineCapabilities; use engine_api::Error as ApiError; @@ -50,7 +51,8 @@ use types::{ }; use types::{ BeaconStateError, BlindedPayload, ChainSpec, Epoch, ExecPayload, ExecutionPayloadCapella, - ExecutionPayloadMerge, FullPayload, ProposerPreparationData, PublicKeyBytes, Signature, Slot, + ExecutionPayloadElectra, ExecutionPayloadMerge, FullPayload, ProposerPreparationData, + PublicKeyBytes, Signature, Slot, }; mod block_hash; @@ -61,6 +63,7 @@ mod metrics; pub mod payload_cache; mod payload_status; pub mod test_utils; +mod versioned_hashes; /// Indicates the default jwt authenticated execution endpoint. pub const DEFAULT_EXECUTION_ENDPOINT: &str = "http://localhost:8551/"; @@ -109,6 +112,12 @@ impl TryFrom> for ProvenancedPayload BlockProposalContents::PayloadAndBlobs { + payload: ExecutionPayloadHeader::Electra(builder_bid.header).into(), + block_value: builder_bid.value, + kzg_commitments: builder_bid.blob_kzg_commitments, + blobs_and_proofs: None, + }, }; Ok(ProvenancedPayload::Builder( BlockProposalContentsType::Blinded(block_proposal_contents), @@ -141,6 +150,7 @@ pub enum Error { InvalidBlobConversion(String), BeaconStateError(BeaconStateError), PayloadTypeMismatch, + VerifyingVersionedHashes(versioned_hashes::Error), } impl From for Error { @@ -160,7 +170,7 @@ pub enum BlockProposalContentsType { Blinded(BlockProposalContents>), } -pub enum BlockProposalContents> { +pub enum BlockProposalContents> { Payload { payload: Payload, block_value: Uint256, @@ -168,16 +178,16 @@ pub enum BlockProposalContents> { PayloadAndBlobs { payload: Payload, block_value: Uint256, - kzg_commitments: KzgCommitments, + kzg_commitments: KzgCommitments, /// `None` for blinded `PayloadAndBlobs`. - blobs_and_proofs: Option<(BlobsList, KzgProofs)>, + blobs_and_proofs: Option<(BlobsList, KzgProofs)>, }, } -impl From>> - for BlockProposalContents> +impl From>> + for BlockProposalContents> { - fn from(item: BlockProposalContents>) -> Self { + fn from(item: BlockProposalContents>) -> Self { match item { BlockProposalContents::Payload { payload, @@ -235,13 +245,13 @@ impl TryFrom> for BlockProposalContentsTyp } #[allow(clippy::type_complexity)] -impl> BlockProposalContents { +impl> BlockProposalContents { pub fn deconstruct( self, ) -> ( Payload, - Option>, - Option<(BlobsList, KzgProofs)>, + Option>, + Option<(BlobsList, KzgProofs)>, Uint256, ) { match self { @@ -323,7 +333,7 @@ pub enum FailedCondition { EpochsSinceFinalization, } -type PayloadContentsRefTuple<'a, T> = (ExecutionPayloadRef<'a, T>, Option<&'a BlobsBundle>); +type PayloadContentsRefTuple<'a, E> = (ExecutionPayloadRef<'a, E>, Option<&'a BlobsBundle>); struct Inner { engine: Arc, @@ -368,11 +378,11 @@ pub struct Config { /// Provides access to one execution engine and provides a neat interface for consumption by the /// `BeaconChain`. #[derive(Clone)] -pub struct ExecutionLayer { - inner: Arc>, +pub struct ExecutionLayer { + inner: Arc>, } -impl ExecutionLayer { +impl ExecutionLayer { /// Instantiate `Self` with an Execution engine specified in `Config`, using JSON-RPC via HTTP. pub fn from_config(config: Config, executor: TaskExecutor, log: Logger) -> Result { let Config { @@ -492,8 +502,8 @@ impl ExecutionLayer { /// Cache a full payload, keyed on the `tree_hash_root` of the payload fn cache_payload( &self, - payload_and_blobs: PayloadContentsRefTuple, - ) -> Option> { + payload_and_blobs: PayloadContentsRefTuple, + ) -> Option> { let (payload_ref, maybe_json_blobs_bundle) = payload_and_blobs; let payload = payload_ref.clone_from_ref(); @@ -511,7 +521,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> { + pub fn get_payload_by_root(&self, root: &Hash256) -> Option> { self.inner.payload_cache.get(root) } @@ -573,7 +583,7 @@ impl ExecutionLayer { /// Spawns a routine which attempts to keep the execution engine online. pub fn spawn_watchdog_routine(&self, slot_clock: S) { - let watchdog = |el: ExecutionLayer| async move { + let watchdog = |el: ExecutionLayer| async move { // Run one task immediately. el.watchdog_task().await; @@ -597,18 +607,18 @@ impl ExecutionLayer { /// Spawns a routine which cleans the cached proposer data periodically. pub fn spawn_clean_proposer_caches_routine(&self, slot_clock: S) { - let preparation_cleaner = |el: ExecutionLayer| async move { + let preparation_cleaner = |el: ExecutionLayer| async move { // Start the loop to periodically clean proposer preparation cache. loop { if let Some(duration_to_next_epoch) = - slot_clock.duration_to_next_epoch(T::slots_per_epoch()) + slot_clock.duration_to_next_epoch(E::slots_per_epoch()) { // Wait for next epoch sleep(duration_to_next_epoch).await; match slot_clock .now() - .map(|slot| slot.epoch(T::slots_per_epoch())) + .map(|slot| slot.epoch(E::slots_per_epoch())) { Some(current_epoch) => el .clean_proposer_caches(current_epoch) @@ -711,7 +721,7 @@ impl ExecutionLayer { }); drop(proposer_preparation_data); - let retain_slot = retain_epoch.start_slot(T::slots_per_epoch()); + let retain_slot = retain_epoch.start_slot(E::slots_per_epoch()); self.proposers() .write() .await @@ -790,7 +800,7 @@ impl ExecutionLayer { spec: &ChainSpec, builder_boost_factor: Option, block_production_version: BlockProductionVersion, - ) -> Result, Error> { + ) -> Result, Error> { let payload_result_type = match block_production_version { BlockProductionVersion::V3 => match self .determine_and_fetch_payload( @@ -889,8 +899,8 @@ impl ExecutionLayer { forkchoice_update_params: ForkchoiceUpdateParameters, current_fork: ForkName, ) -> ( - Result>>, builder_client::Error>, - Result, Error>, + Result>>, builder_client::Error>, + Result, Error>, ) { let slot = builder_params.slot; let pubkey = &builder_params.pubkey; @@ -907,7 +917,7 @@ impl ExecutionLayer { let ((relay_result, relay_duration), (local_result, local_duration)) = tokio::join!( timed_future(metrics::GET_BLINDED_PAYLOAD_BUILDER, async { builder - .get_builder_header::(slot, parent_hash, pubkey) + .get_builder_header::(slot, parent_hash, pubkey) .await }), timed_future(metrics::GET_BLINDED_PAYLOAD_LOCAL, async { @@ -955,7 +965,7 @@ impl ExecutionLayer { current_fork: ForkName, builder_boost_factor: Option, spec: &ChainSpec, - ) -> Result>, Error> { + ) -> Result>, Error> { let Some(builder) = self.builder() else { // no builder.. return local payload return self @@ -1200,7 +1210,7 @@ impl ExecutionLayer { payload_attributes: &PayloadAttributes, forkchoice_update_params: ForkchoiceUpdateParameters, current_fork: ForkName, - ) -> Result, Error> { + ) -> Result, Error> { self.get_full_payload_with( parent_hash, payload_attributes, @@ -1218,10 +1228,10 @@ impl ExecutionLayer { forkchoice_update_params: ForkchoiceUpdateParameters, current_fork: ForkName, cache_fn: fn( - &ExecutionLayer, - PayloadContentsRefTuple, - ) -> Option>, - ) -> Result, Error> { + &ExecutionLayer, + PayloadContentsRefTuple, + ) -> Option>, + ) -> Result, Error> { self.engine() .request(move |engine| async move { let payload_id = if let Some(id) = engine @@ -1287,7 +1297,7 @@ impl ExecutionLayer { &metrics::EXECUTION_LAYER_REQUEST_TIMES, &[metrics::GET_PAYLOAD], ); - engine.api.get_payload::(current_fork, payload_id).await + engine.api.get_payload::(current_fork, payload_id).await }.await?; if payload_response.execution_payload_ref().fee_recipient() != payload_attributes.suggested_fee_recipient() { @@ -1321,7 +1331,7 @@ impl ExecutionLayer { /// Maps to the `engine_newPayload` JSON-RPC call. pub async fn notify_new_payload( &self, - new_payload_request: NewPayloadRequest, + new_payload_request: NewPayloadRequest<'_, E>, ) -> Result { let _timer = metrics::start_timer_vec( &metrics::EXECUTION_LAYER_REQUEST_TIMES, @@ -1723,7 +1733,7 @@ impl ExecutionLayer { pub async fn get_payload_bodies_by_hash( &self, hashes: Vec, - ) -> Result>>, Error> { + ) -> Result>>, Error> { self.engine() .request(|engine: &Engine| async move { engine.api.get_payload_bodies_by_hash_v1(hashes).await @@ -1737,7 +1747,7 @@ impl ExecutionLayer { &self, start: u64, count: u64, - ) -> Result>>, Error> { + ) -> Result>>, Error> { let _timer = metrics::start_timer(&metrics::EXECUTION_LAYER_GET_PAYLOAD_BODIES_BY_RANGE); self.engine() .request(|engine: &Engine| async move { @@ -1756,9 +1766,9 @@ impl ExecutionLayer { /// This will fail if the payload is not from the finalized portion of the chain. pub async fn get_payload_for_header( &self, - header: &ExecutionPayloadHeader, + header: &ExecutionPayloadHeader, fork: ForkName, - ) -> Result>, Error> { + ) -> Result>, Error> { let hash = header.block_hash(); let block_number = header.block_number(); @@ -1768,6 +1778,7 @@ impl ExecutionLayer { ForkName::Merge => ExecutionPayloadMerge::default().into(), ForkName::Capella => ExecutionPayloadCapella::default().into(), ForkName::Deneb => ExecutionPayloadDeneb::default().into(), + ForkName::Electra => ExecutionPayloadElectra::default().into(), ForkName::Base | ForkName::Altair => { return Err(Error::InvalidForkForPayload); } @@ -1812,7 +1823,7 @@ impl ExecutionLayer { &self, hash: ExecutionBlockHash, fork: ForkName, - ) -> Result>, Error> { + ) -> Result>, Error> { self.engine() .request(|engine| async move { self.get_payload_by_hash_from_engine(engine, hash, fork) @@ -1828,7 +1839,7 @@ impl ExecutionLayer { engine: &Engine, hash: ExecutionBlockHash, fork: ForkName, - ) -> Result>, ApiError> { + ) -> Result>, ApiError> { let _timer = metrics::start_timer(&metrics::EXECUTION_LAYER_GET_PAYLOAD_BY_BLOCK_HASH); if hash == ExecutionBlockHash::zero() { @@ -1836,6 +1847,7 @@ impl ExecutionLayer { ForkName::Merge => Ok(Some(ExecutionPayloadMerge::default().into())), ForkName::Capella => Ok(Some(ExecutionPayloadCapella::default().into())), ForkName::Deneb => Ok(Some(ExecutionPayloadDeneb::default().into())), + ForkName::Electra => Ok(Some(ExecutionPayloadElectra::default().into())), ForkName::Base | ForkName::Altair => Err(ApiError::UnsupportedForkVariant( format!("called get_payload_by_hash_from_engine with {}", fork), )), @@ -1844,7 +1856,7 @@ impl ExecutionLayer { let Some(block) = engine .api - .get_block_by_hash_with_txns::(hash, fork) + .get_block_by_hash_with_txns::(hash, fork) .await? else { return Ok(None); @@ -1935,6 +1947,35 @@ impl ExecutionLayer { excess_blob_gas: deneb_block.excess_blob_gas, }) } + ExecutionBlockWithTransactions::Electra(electra_block) => { + let withdrawals = VariableList::new( + electra_block + .withdrawals + .into_iter() + .map(Into::into) + .collect(), + ) + .map_err(ApiError::DeserializeWithdrawals)?; + ExecutionPayload::Electra(ExecutionPayloadElectra { + parent_hash: electra_block.parent_hash, + fee_recipient: electra_block.fee_recipient, + state_root: electra_block.state_root, + receipts_root: electra_block.receipts_root, + logs_bloom: electra_block.logs_bloom, + prev_randao: electra_block.prev_randao, + block_number: electra_block.block_number, + gas_limit: electra_block.gas_limit, + gas_used: electra_block.gas_used, + timestamp: electra_block.timestamp, + extra_data: electra_block.extra_data, + base_fee_per_gas: electra_block.base_fee_per_gas, + block_hash: electra_block.block_hash, + transactions: convert_transactions(electra_block.transactions)?, + withdrawals, + blob_gas_used: electra_block.blob_gas_used, + excess_blob_gas: electra_block.excess_blob_gas, + }) + } }; Ok(Some(payload)) @@ -1943,8 +1984,8 @@ impl ExecutionLayer { pub async fn propose_blinded_beacon_block( &self, block_root: Hash256, - block: &SignedBlindedBeaconBlock, - ) -> Result, Error> { + block: &SignedBlindedBeaconBlock, + ) -> Result, Error> { debug!( self.log(), "Sending block to builder"; @@ -2081,8 +2122,8 @@ impl fmt::Display for InvalidBuilderPayload { } /// Perform some cursory, non-exhaustive validation of the bid returned from the builder. -fn verify_builder_bid( - bid: &ForkVersionedResponse>, +fn verify_builder_bid( + bid: &ForkVersionedResponse>, parent_hash: ExecutionBlockHash, payload_attributes: &PayloadAttributes, block_number: Option, @@ -2106,7 +2147,7 @@ fn verify_builder_bid( .withdrawals() .ok() .cloned() - .map(|withdrawals| Withdrawals::::from(withdrawals).tree_hash_root()); + .map(|withdrawals| Withdrawals::::from(withdrawals).tree_hash_root()); let payload_withdrawals_root = header.withdrawals_root().ok().copied(); if header.parent_hash() != parent_hash { @@ -2167,10 +2208,10 @@ fn timestamp_now() -> u64 { .as_secs() } -fn noop( - _: &ExecutionLayer, - _: PayloadContentsRefTuple, -) -> Option> { +fn noop( + _: &ExecutionLayer, + _: PayloadContentsRefTuple, +) -> Option> { None } diff --git a/beacon_node/execution_layer/src/payload_cache.rs b/beacon_node/execution_layer/src/payload_cache.rs index 1a2864c194..26ae89b5cb 100644 --- a/beacon_node/execution_layer/src/payload_cache.rs +++ b/beacon_node/execution_layer/src/payload_cache.rs @@ -9,14 +9,14 @@ use types::{EthSpec, Hash256}; pub const DEFAULT_PAYLOAD_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(10); /// A cache mapping execution payloads by tree hash roots. -pub struct PayloadCache { - payloads: Mutex>>, +pub struct PayloadCache { + payloads: Mutex>>, } #[derive(Hash, PartialEq, Eq)] struct PayloadCacheId(Hash256); -impl Default for PayloadCache { +impl Default for PayloadCache { fn default() -> Self { PayloadCache { payloads: Mutex::new(LruCache::new(DEFAULT_PAYLOAD_CACHE_SIZE)), @@ -24,17 +24,17 @@ impl Default for PayloadCache { } } -impl PayloadCache { - pub fn put(&self, payload: FullPayloadContents) -> Option> { +impl PayloadCache { + pub fn put(&self, payload: FullPayloadContents) -> Option> { let root = payload.payload_ref().tree_hash_root(); self.payloads.lock().put(PayloadCacheId(root), payload) } - pub fn pop(&self, root: &Hash256) -> Option> { + pub fn pop(&self, root: &Hash256) -> Option> { self.payloads.lock().pop(&PayloadCacheId(*root)) } - pub fn get(&self, hash: &Hash256) -> Option> { + pub fn get(&self, hash: &Hash256) -> Option> { self.payloads.lock().get(&PayloadCacheId(*hash)).cloned() } } diff --git a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs index 182cad50fa..08f8ca6843 100644 --- a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs +++ b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs @@ -22,8 +22,8 @@ use tree_hash::TreeHash; use tree_hash_derive::TreeHash; use types::{ Blob, ChainSpec, EthSpec, ExecutionBlockHash, ExecutionPayload, ExecutionPayloadCapella, - ExecutionPayloadDeneb, ExecutionPayloadHeader, ExecutionPayloadMerge, ForkName, Hash256, - Transaction, Transactions, Uint256, + ExecutionPayloadDeneb, ExecutionPayloadElectra, ExecutionPayloadHeader, ExecutionPayloadMerge, + ForkName, Hash256, Transaction, Transactions, Uint256, }; use super::DEFAULT_TERMINAL_BLOCK; @@ -35,12 +35,12 @@ const GAS_USED: u64 = GAS_LIMIT - 1; #[derive(Clone, Debug, PartialEq)] #[allow(clippy::large_enum_variant)] // This struct is only for testing. -pub enum Block { +pub enum Block { PoW(PoWBlock), - PoS(ExecutionPayload), + PoS(ExecutionPayload), } -impl Block { +impl Block { pub fn block_number(&self) -> u64 { match self { Block::PoW(block) => block.block_number, @@ -88,7 +88,7 @@ impl Block { } } - pub fn as_execution_block_with_tx(&self) -> Option> { + pub fn as_execution_block_with_tx(&self) -> Option> { match self { Block::PoS(payload) => Some(payload.clone().try_into().unwrap()), Block::PoW(_) => None, @@ -107,13 +107,13 @@ pub struct PoWBlock { } #[derive(Debug, Clone)] -pub struct ExecutionBlockGenerator { +pub struct ExecutionBlockGenerator { /* * Common database */ - head_block: Option>, + head_block: Option>, finalized_block_hash: Option, - blocks: HashMap>, + blocks: HashMap>, block_hashes: HashMap>, /* * PoW block parameters @@ -124,18 +124,19 @@ pub struct ExecutionBlockGenerator { /* * PoS block parameters */ - pub pending_payloads: HashMap>, + pub pending_payloads: HashMap>, pub next_payload_id: u64, - pub payload_ids: HashMap>, + pub payload_ids: HashMap>, /* * Post-merge fork triggers */ - pub shanghai_time: Option, // withdrawals + pub shanghai_time: Option, // capella pub cancun_time: Option, // deneb + pub prague_time: Option, // electra /* * deneb stuff */ - pub blobs_bundles: HashMap>, + pub blobs_bundles: HashMap>, pub kzg: Option>, rng: Arc>, } @@ -146,14 +147,15 @@ fn make_rng() -> Arc> { Arc::new(Mutex::new(StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64))) } -impl ExecutionBlockGenerator { +impl ExecutionBlockGenerator { pub fn new( terminal_total_difficulty: Uint256, terminal_block_number: u64, terminal_block_hash: ExecutionBlockHash, shanghai_time: Option, cancun_time: Option, - kzg: Option, + prague_time: Option, + kzg: Option>, ) -> Self { let mut gen = Self { head_block: <_>::default(), @@ -168,8 +170,9 @@ impl ExecutionBlockGenerator { payload_ids: <_>::default(), shanghai_time, cancun_time, + prague_time, blobs_bundles: <_>::default(), - kzg: kzg.map(Arc::new), + kzg, rng: make_rng(), }; @@ -178,7 +181,7 @@ impl ExecutionBlockGenerator { gen } - pub fn latest_block(&self) -> Option> { + pub fn latest_block(&self) -> Option> { self.head_block.clone() } @@ -187,7 +190,7 @@ impl ExecutionBlockGenerator { .map(|block| block.as_execution_block(self.terminal_total_difficulty)) } - pub fn block_by_number(&self, number: u64) -> Option> { + pub fn block_by_number(&self, number: u64) -> Option> { // Get the latest canonical head block let mut latest_block = self.latest_block()?; loop { @@ -203,11 +206,14 @@ impl ExecutionBlockGenerator { } pub fn get_fork_at_timestamp(&self, timestamp: u64) -> ForkName { - match self.cancun_time { - Some(fork_time) if timestamp >= fork_time => ForkName::Deneb, - _ => match self.shanghai_time { - Some(fork_time) if timestamp >= fork_time => ForkName::Capella, - _ => ForkName::Merge, + match self.prague_time { + Some(fork_time) if timestamp >= fork_time => ForkName::Electra, + _ => match self.cancun_time { + Some(fork_time) if timestamp >= fork_time => ForkName::Deneb, + _ => match self.shanghai_time { + Some(fork_time) if timestamp >= fork_time => ForkName::Capella, + _ => ForkName::Merge, + }, }, } } @@ -217,7 +223,7 @@ impl ExecutionBlockGenerator { .map(|block| block.as_execution_block(self.terminal_total_difficulty)) } - pub fn block_by_hash(&self, hash: ExecutionBlockHash) -> Option> { + pub fn block_by_hash(&self, hash: ExecutionBlockHash) -> Option> { self.blocks.get(&hash).cloned() } @@ -229,7 +235,7 @@ impl ExecutionBlockGenerator { pub fn execution_block_with_txs_by_hash( &self, hash: ExecutionBlockHash, - ) -> Option> { + ) -> Option> { self.block_by_hash(hash) .and_then(|block| block.as_execution_block_with_tx()) } @@ -237,7 +243,7 @@ impl ExecutionBlockGenerator { pub fn execution_block_with_txs_by_number( &self, number: u64, - ) -> Option> { + ) -> Option> { self.block_by_number(number) .and_then(|block| block.as_execution_block_with_tx()) } @@ -362,7 +368,7 @@ impl ExecutionBlockGenerator { // This does not reject duplicate blocks inserted. This lets us re-use the same execution // block generator for multiple beacon chains which is useful in testing. - pub fn insert_block(&mut self, block: Block) -> Result { + pub fn insert_block(&mut self, block: Block) -> Result { if block.parent_hash() != ExecutionBlockHash::zero() && !self.blocks.contains_key(&block.parent_hash()) { @@ -372,7 +378,7 @@ impl ExecutionBlockGenerator { Ok(self.insert_block_without_checks(block)) } - pub fn insert_block_without_checks(&mut self, block: Block) -> ExecutionBlockHash { + pub fn insert_block_without_checks(&mut self, block: Block) -> ExecutionBlockHash { let block_hash = block.block_hash(); self.block_hashes .entry(block.block_number()) @@ -383,7 +389,7 @@ impl ExecutionBlockGenerator { block_hash } - pub fn modify_last_block(&mut self, block_modifier: impl FnOnce(&mut Block)) { + pub fn modify_last_block(&mut self, block_modifier: impl FnOnce(&mut Block)) { if let Some(last_block_hash) = self .block_hashes .iter_mut() @@ -417,15 +423,15 @@ impl ExecutionBlockGenerator { } } - pub fn get_payload(&mut self, id: &PayloadId) -> Option> { + pub fn get_payload(&mut self, id: &PayloadId) -> Option> { self.payload_ids.get(id).cloned() } - pub fn get_blobs_bundle(&mut self, id: &PayloadId) -> Option> { + pub fn get_blobs_bundle(&mut self, id: &PayloadId) -> Option> { self.blobs_bundles.get(id).cloned() } - pub fn new_payload(&mut self, payload: ExecutionPayload) -> PayloadStatusV1 { + pub fn new_payload(&mut self, payload: ExecutionPayload) -> PayloadStatusV1 { let Some(parent) = self.blocks.get(&payload.parent_hash()) else { return PayloadStatusV1 { status: PayloadStatusV1Status::Syncing, @@ -514,6 +520,7 @@ impl ExecutionBlockGenerator { let execution_payload = self.build_new_execution_payload(head_block_hash, &parent, id, &attributes)?; + self.payload_ids.insert(id, execution_payload); Some(id) @@ -544,10 +551,10 @@ impl ExecutionBlockGenerator { pub fn build_new_execution_payload( &mut self, head_block_hash: ExecutionBlockHash, - parent: &Block, + parent: &Block, id: PayloadId, attributes: &PayloadAttributes, - ) -> Result, String> { + ) -> Result, String> { let mut execution_payload = match attributes { PayloadAttributes::V1(pa) => ExecutionPayload::Merge(ExecutionPayloadMerge { parent_hash: head_block_hash, @@ -601,33 +608,55 @@ impl ExecutionBlockGenerator { }), _ => unreachable!(), }, - PayloadAttributes::V3(pa) => ExecutionPayload::Deneb(ExecutionPayloadDeneb { - parent_hash: head_block_hash, - fee_recipient: pa.suggested_fee_recipient, - receipts_root: Hash256::repeat_byte(42), - state_root: Hash256::repeat_byte(43), - logs_bloom: vec![0; 256].into(), - prev_randao: pa.prev_randao, - block_number: parent.block_number() + 1, - gas_limit: GAS_LIMIT, - gas_used: GAS_USED, - timestamp: pa.timestamp, - extra_data: "block gen was here".as_bytes().to_vec().into(), - base_fee_per_gas: Uint256::one(), - block_hash: ExecutionBlockHash::zero(), - transactions: vec![].into(), - withdrawals: pa.withdrawals.clone().into(), - blob_gas_used: 0, - excess_blob_gas: 0, - }), + PayloadAttributes::V3(pa) => match self.get_fork_at_timestamp(pa.timestamp) { + ForkName::Deneb => ExecutionPayload::Deneb(ExecutionPayloadDeneb { + parent_hash: head_block_hash, + fee_recipient: pa.suggested_fee_recipient, + receipts_root: Hash256::repeat_byte(42), + state_root: Hash256::repeat_byte(43), + logs_bloom: vec![0; 256].into(), + prev_randao: pa.prev_randao, + block_number: parent.block_number() + 1, + gas_limit: GAS_LIMIT, + gas_used: GAS_USED, + timestamp: pa.timestamp, + extra_data: "block gen was here".as_bytes().to_vec().into(), + base_fee_per_gas: Uint256::one(), + block_hash: ExecutionBlockHash::zero(), + transactions: vec![].into(), + withdrawals: pa.withdrawals.clone().into(), + blob_gas_used: 0, + excess_blob_gas: 0, + }), + ForkName::Electra => ExecutionPayload::Electra(ExecutionPayloadElectra { + parent_hash: head_block_hash, + fee_recipient: pa.suggested_fee_recipient, + receipts_root: Hash256::repeat_byte(42), + state_root: Hash256::repeat_byte(43), + logs_bloom: vec![0; 256].into(), + prev_randao: pa.prev_randao, + block_number: parent.block_number() + 1, + gas_limit: GAS_LIMIT, + gas_used: GAS_USED, + timestamp: pa.timestamp, + extra_data: "block gen was here".as_bytes().to_vec().into(), + base_fee_per_gas: Uint256::one(), + block_hash: ExecutionBlockHash::zero(), + transactions: vec![].into(), + withdrawals: pa.withdrawals.clone().into(), + blob_gas_used: 0, + excess_blob_gas: 0, + }), + _ => unreachable!(), + }, }; match execution_payload.fork_name() { ForkName::Base | ForkName::Altair | ForkName::Merge | ForkName::Capella => {} - ForkName::Deneb => { + ForkName::Deneb | ForkName::Electra => { // get random number between 0 and Max Blobs let mut rng = self.rng.lock(); - let num_blobs = rng.gen::() % (T::max_blobs_per_block() + 1); + let num_blobs = rng.gen::() % (E::max_blobs_per_block() + 1); let (bundle, transactions) = generate_blobs(num_blobs)?; for tx in Vec::from(transactions) { execution_payload @@ -699,7 +728,7 @@ pub fn generate_blobs( Ok((bundle, transactions.into())) } -fn static_valid_tx() -> Result, String> { +pub fn static_valid_tx() -> Result, String> { // This is a real transaction hex encoded, but we don't care about the contents of the transaction. let transaction: EthersTransaction = serde_json::from_str( r#"{ @@ -728,11 +757,11 @@ fn payload_id_from_u64(n: u64) -> PayloadId { n.to_le_bytes() } -pub fn generate_genesis_header( +pub fn generate_genesis_header( spec: &ChainSpec, post_transition_merge: bool, -) -> Option> { - let genesis_fork = spec.fork_name_at_slot::(spec.genesis_slot); +) -> Option> { + let genesis_fork = spec.fork_name_at_slot::(spec.genesis_slot); let genesis_block_hash = generate_genesis_block(spec.terminal_total_difficulty, DEFAULT_TERMINAL_BLOCK) .ok() @@ -745,7 +774,7 @@ pub fn generate_genesis_header( *header.block_hash_mut() = genesis_block_hash.unwrap_or_default(); Some(header) } else { - Some(ExecutionPayloadHeader::::Merge(<_>::default())) + Some(ExecutionPayloadHeader::::Merge(<_>::default())) } } ForkName::Capella => { @@ -758,6 +787,11 @@ pub fn generate_genesis_header( *header.block_hash_mut() = genesis_block_hash.unwrap_or_default(); Some(header) } + ForkName::Electra => { + let mut header = ExecutionPayloadHeader::Electra(<_>::default()); + *header.block_hash_mut() = genesis_block_hash.unwrap_or_default(); + Some(header) + } } } @@ -830,6 +864,7 @@ mod test { None, None, None, + None, ); for i in 0..=TERMINAL_BLOCK { 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 9dff1ac008..77d972ab88 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -5,16 +5,15 @@ use crate::test_utils::DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI; use serde::{de::DeserializeOwned, Deserialize}; use serde_json::Value as JsonValue; use std::sync::Arc; -use types::{EthSpec, ForkName}; pub const GENERIC_ERROR_CODE: i64 = -1234; pub const BAD_PARAMS_ERROR_CODE: i64 = -32602; pub const UNKNOWN_PAYLOAD_ERROR_CODE: i64 = -38001; pub const FORK_REQUEST_MISMATCH_ERROR_CODE: i64 = -32000; -pub async fn handle_rpc( +pub async fn handle_rpc( body: JsonValue, - ctx: Arc>, + ctx: Arc>, ) -> Result { *ctx.previous_request.lock() = Some(body.clone()); @@ -96,24 +95,28 @@ pub async fn handle_rpc( ENGINE_NEW_PAYLOAD_V1 | ENGINE_NEW_PAYLOAD_V2 | ENGINE_NEW_PAYLOAD_V3 => { let request = match method { ENGINE_NEW_PAYLOAD_V1 => JsonExecutionPayload::V1( - get_param::>(params, 0) + get_param::>(params, 0) .map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?, ), - ENGINE_NEW_PAYLOAD_V2 => get_param::>(params, 0) + ENGINE_NEW_PAYLOAD_V2 => get_param::>(params, 0) .map(|jep| JsonExecutionPayload::V2(jep)) .or_else(|_| { - get_param::>(params, 0) + get_param::>(params, 0) .map(|jep| JsonExecutionPayload::V1(jep)) }) .map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?, - ENGINE_NEW_PAYLOAD_V3 => get_param::>(params, 0) - .map(|jep| JsonExecutionPayload::V3(jep)) + ENGINE_NEW_PAYLOAD_V3 => get_param::>(params, 0) + .map(|jep| JsonExecutionPayload::V4(jep)) .or_else(|_| { - get_param::>(params, 0) - .map(|jep| JsonExecutionPayload::V2(jep)) + get_param::>(params, 0) + .map(|jep| JsonExecutionPayload::V3(jep)) .or_else(|_| { - get_param::>(params, 0) - .map(|jep| JsonExecutionPayload::V1(jep)) + get_param::>(params, 0) + .map(|jep| JsonExecutionPayload::V2(jep)) + .or_else(|_| { + get_param::>(params, 0) + .map(|jep| JsonExecutionPayload::V1(jep)) + }) }) }) .map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?, @@ -124,7 +127,7 @@ pub async fn handle_rpc( .execution_block_generator .read() .get_fork_at_timestamp(*request.timestamp()); - // validate method called correctly according to shanghai fork time + // validate method called correctly according to fork time match fork { ForkName::Merge => { if matches!(request, JsonExecutionPayload::V2(_)) { @@ -157,14 +160,14 @@ pub async fn handle_rpc( ForkName::Deneb => { if method == ENGINE_NEW_PAYLOAD_V1 || method == ENGINE_NEW_PAYLOAD_V2 { return Err(( - format!("{} called after deneb fork!", method), + format!("{} called after Deneb fork!", method), GENERIC_ERROR_CODE, )); } if matches!(request, JsonExecutionPayload::V1(_)) { return Err(( format!( - "{} called with `ExecutionPayloadV1` after deneb fork!", + "{} called with `ExecutionPayloadV1` after Deneb fork!", method ), GENERIC_ERROR_CODE, @@ -173,7 +176,42 @@ pub async fn handle_rpc( if matches!(request, JsonExecutionPayload::V2(_)) { return Err(( format!( - "{} called with `ExecutionPayloadV2` after deneb fork!", + "{} called with `ExecutionPayloadV2` after Deneb fork!", + method + ), + GENERIC_ERROR_CODE, + )); + } + } + ForkName::Electra => { + if method == ENGINE_NEW_PAYLOAD_V1 || method == ENGINE_NEW_PAYLOAD_V2 { + return Err(( + format!("{} called after Electra fork!", method), + GENERIC_ERROR_CODE, + )); + } + if matches!(request, JsonExecutionPayload::V1(_)) { + return Err(( + format!( + "{} called with `ExecutionPayloadV1` after Electra fork!", + method + ), + GENERIC_ERROR_CODE, + )); + } + if matches!(request, JsonExecutionPayload::V2(_)) { + return Err(( + format!( + "{} called with `ExecutionPayloadV2` after Electra fork!", + method + ), + GENERIC_ERROR_CODE, + )); + } + if matches!(request, JsonExecutionPayload::V3(_)) { + return Err(( + format!( + "{} called with `ExecutionPayloadV3` after Electra fork!", method ), GENERIC_ERROR_CODE, @@ -246,7 +284,7 @@ pub async fn handle_rpc( FORK_REQUEST_MISMATCH_ERROR_CODE, )); } - // validate method called correctly according to deneb fork time + // validate method called correctly according to cancun fork time if ctx .execution_block_generator .read() @@ -255,7 +293,20 @@ pub async fn handle_rpc( && (method == ENGINE_GET_PAYLOAD_V1 || method == ENGINE_GET_PAYLOAD_V2) { return Err(( - format!("{} called after deneb fork!", method), + format!("{} called after Deneb fork!", method), + FORK_REQUEST_MISMATCH_ERROR_CODE, + )); + } + // validate method called correctly according to prague fork time + if ctx + .execution_block_generator + .read() + .get_fork_at_timestamp(response.timestamp()) + == ForkName::Electra + && method == ENGINE_GET_PAYLOAD_V1 + { + return Err(( + format!("{} called after Electra fork!", method), FORK_REQUEST_MISMATCH_ERROR_CODE, )); } @@ -296,6 +347,20 @@ pub async fn handle_rpc( }) .unwrap() } + JsonExecutionPayload::V4(execution_payload) => { + serde_json::to_value(JsonGetPayloadResponseV4 { + execution_payload, + block_value: DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI.into(), + blobs_bundle: maybe_blobs + .ok_or(( + "No blobs returned despite V4 Payload".to_string(), + GENERIC_ERROR_CODE, + ))? + .into(), + should_override_builder: false, + }) + .unwrap() + } _ => unreachable!(), }), _ => unreachable!(), @@ -329,7 +394,7 @@ pub async fn handle_rpc( .map(|opt| opt.map(JsonPayloadAttributes::V1)) .transpose() } - ForkName::Capella | ForkName::Deneb => { + ForkName::Capella | ForkName::Deneb | ForkName::Electra => { get_param::>(params, 1) .map(|opt| opt.map(JsonPayloadAttributes::V2)) .transpose() @@ -393,7 +458,7 @@ pub async fn handle_rpc( )); } } - ForkName::Deneb => { + ForkName::Deneb | ForkName::Electra => { if method == ENGINE_FORKCHOICE_UPDATED_V1 { return Err(( format!("{} called after Deneb fork!", method), @@ -478,7 +543,7 @@ pub async fn handle_rpc( match maybe_block { Some(block) => { - let transactions = Transactions::::new( + let transactions = Transactions::::new( block .transactions() .iter() @@ -498,7 +563,7 @@ pub async fn handle_rpc( ) })?; - response.push(Some(JsonExecutionPayloadBodyV1:: { + response.push(Some(JsonExecutionPayloadBodyV1:: { transactions, withdrawals: block .withdrawals() 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 3d4ea51f4b..b12e26a3d6 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_builder.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_builder.rs @@ -1,7 +1,7 @@ use crate::test_utils::{DEFAULT_BUILDER_PAYLOAD_VALUE_WEI, DEFAULT_JWT_SECRET}; use crate::{Config, ExecutionLayer, PayloadAttributes}; use eth2::types::{BlobsBundle, BlockId, StateId, ValidatorId}; -use eth2::{BeaconNodeHttpClient, Timeouts}; +use eth2::{BeaconNodeHttpClient, Timeouts, CONSENSUS_VERSION_HEADER}; use fork_choice::ForkchoiceUpdateParameters; use parking_lot::RwLock; use sensitive_url::SensitiveUrl; @@ -15,7 +15,8 @@ use task_executor::TaskExecutor; use tempfile::NamedTempFile; use tree_hash::TreeHash; use types::builder_bid::{ - BuilderBid, BuilderBidCapella, BuilderBidDeneb, BuilderBidMerge, SignedBuilderBid, + BuilderBid, BuilderBidCapella, BuilderBidDeneb, BuilderBidElectra, BuilderBidMerge, + SignedBuilderBid, }; use types::{ Address, BeaconState, ChainSpec, EthSpec, ExecPayload, ExecutionPayload, @@ -54,7 +55,8 @@ impl Operation { } #[derive(Debug)] -struct Custom(String); +// We don't use the string value directly, but it's used in the Debug impl which is required by `warp::reject::Reject`. +struct Custom(#[allow(dead_code)] String); impl warp::reject::Reject for Custom {} @@ -70,8 +72,6 @@ pub trait BidStuff { fn set_withdrawals_root(&mut self, withdrawals_root: Hash256); fn sign_builder_message(&mut self, sk: &SecretKey, spec: &ChainSpec) -> Signature; - - fn to_signed_bid(self, signature: Signature) -> SignedBuilderBid; } impl BidStuff for BuilderBid { @@ -86,6 +86,9 @@ impl BidStuff for BuilderBid { ExecutionPayloadHeaderRefMut::Deneb(header) => { header.fee_recipient = fee_recipient; } + ExecutionPayloadHeaderRefMut::Electra(header) => { + header.fee_recipient = fee_recipient; + } } } @@ -100,6 +103,9 @@ impl BidStuff for BuilderBid { ExecutionPayloadHeaderRefMut::Deneb(header) => { header.gas_limit = gas_limit; } + ExecutionPayloadHeaderRefMut::Electra(header) => { + header.gas_limit = gas_limit; + } } } @@ -118,6 +124,9 @@ impl BidStuff for BuilderBid { ExecutionPayloadHeaderRefMut::Deneb(header) => { header.parent_hash = ExecutionBlockHash::from_root(parent_hash); } + ExecutionPayloadHeaderRefMut::Electra(header) => { + header.parent_hash = ExecutionBlockHash::from_root(parent_hash); + } } } @@ -132,6 +141,9 @@ impl BidStuff for BuilderBid { ExecutionPayloadHeaderRefMut::Deneb(header) => { header.prev_randao = prev_randao; } + ExecutionPayloadHeaderRefMut::Electra(header) => { + header.prev_randao = prev_randao; + } } } @@ -146,6 +158,9 @@ impl BidStuff for BuilderBid { ExecutionPayloadHeaderRefMut::Deneb(header) => { header.block_number = block_number; } + ExecutionPayloadHeaderRefMut::Electra(header) => { + header.block_number = block_number; + } } } @@ -160,6 +175,9 @@ impl BidStuff for BuilderBid { ExecutionPayloadHeaderRefMut::Deneb(header) => { header.timestamp = timestamp; } + ExecutionPayloadHeaderRefMut::Electra(header) => { + header.timestamp = timestamp; + } } } @@ -174,6 +192,9 @@ impl BidStuff for BuilderBid { ExecutionPayloadHeaderRefMut::Deneb(header) => { header.withdrawals_root = withdrawals_root; } + ExecutionPayloadHeaderRefMut::Electra(header) => { + header.withdrawals_root = withdrawals_root; + } } } @@ -182,13 +203,6 @@ impl BidStuff for BuilderBid { let message = self.signing_root(domain); sk.sign(message) } - - fn to_signed_bid(self, signature: Signature) -> SignedBuilderBid { - SignedBuilderBid { - message: self, - signature, - } - } } #[derive(Clone)] @@ -307,53 +321,57 @@ pub fn serve( }, ); - let blinded_block = prefix - .and(warp::path("blinded_blocks")) - .and(warp::body::json()) - .and(warp::path::end()) - .and(ctx_filter.clone()) - .and_then( - |block: SignedBlindedBeaconBlock, builder: MockBuilder| async move { - let slot = block.slot(); - let root = match block { - SignedBlindedBeaconBlock::Base(_) | types::SignedBeaconBlock::Altair(_) => { - return Err(reject("invalid fork")); - } - SignedBlindedBeaconBlock::Merge(block) => { - block.message.body.execution_payload.tree_hash_root() - } - SignedBlindedBeaconBlock::Capella(block) => { - block.message.body.execution_payload.tree_hash_root() - } - SignedBlindedBeaconBlock::Deneb(block) => { - block.message.body.execution_payload.tree_hash_root() - } - }; + let blinded_block = + prefix + .and(warp::path("blinded_blocks")) + .and(warp::body::json()) + .and(warp::header::header::(CONSENSUS_VERSION_HEADER)) + .and(warp::path::end()) + .and(ctx_filter.clone()) + .and_then( + |block: SignedBlindedBeaconBlock, + fork_name: ForkName, + builder: MockBuilder| async move { + let root = match block { + SignedBlindedBeaconBlock::Base(_) | types::SignedBeaconBlock::Altair(_) => { + return Err(reject("invalid fork")); + } + SignedBlindedBeaconBlock::Merge(block) => { + block.message.body.execution_payload.tree_hash_root() + } + SignedBlindedBeaconBlock::Capella(block) => { + block.message.body.execution_payload.tree_hash_root() + } + SignedBlindedBeaconBlock::Deneb(block) => { + block.message.body.execution_payload.tree_hash_root() + } + SignedBlindedBeaconBlock::Electra(block) => { + block.message.body.execution_payload.tree_hash_root() + } + }; + let payload = builder + .el + .get_payload_by_root(&root) + .ok_or_else(|| reject("missing payload for tx root"))?; + let resp: ForkVersionedResponse<_> = ForkVersionedResponse { + version: Some(fork_name), + metadata: Default::default(), + data: payload, + }; - let fork_name = builder.spec.fork_name_at_slot::(slot); - let payload = builder - .el - .get_payload_by_root(&root) - .ok_or_else(|| reject("missing payload for tx root"))?; - let resp: ForkVersionedResponse<_> = ForkVersionedResponse { - version: Some(fork_name), - metadata: Default::default(), - data: payload, - }; - - let json_payload = serde_json::to_string(&resp) - .map_err(|_| reject("coudn't serialize response"))?; - Ok::<_, warp::reject::Rejection>( - warp::http::Response::builder() - .status(200) - .body( - serde_json::to_string(&json_payload) - .map_err(|_| reject("nvalid JSON"))?, - ) - .unwrap(), - ) - }, - ); + let json_payload = serde_json::to_string(&resp) + .map_err(|_| reject("coudn't serialize response"))?; + Ok::<_, warp::reject::Rejection>( + warp::http::Response::builder() + .status(200) + .body( + serde_json::to_string(&json_payload) + .map_err(|_| reject("invalid JSON"))?, + ) + .unwrap(), + ) + }, + ); let status = prefix .and(warp::path("status")) @@ -463,7 +481,7 @@ pub fn serve( .map_err(|_| reject("couldn't get prev randao"))?; let expected_withdrawals = match fork { ForkName::Base | ForkName::Altair | ForkName::Merge => None, - ForkName::Capella | ForkName::Deneb => Some( + ForkName::Capella | ForkName::Deneb | ForkName::Electra => Some( builder .beacon_client .get_expected_withdrawals(&StateId::Head) @@ -485,7 +503,7 @@ pub fn serve( expected_withdrawals, None, ), - ForkName::Deneb => PayloadAttributes::new( + ForkName::Deneb | ForkName::Electra => PayloadAttributes::new( timestamp, *prev_randao, fee_recipient, @@ -529,6 +547,17 @@ pub fn serve( ) = payload_response.into(); match fork { + ForkName::Electra => BuilderBid::Electra(BuilderBidElectra { + header: payload + .as_electra() + .map_err(|_| reject("incorrect payload variant"))? + .into(), + blob_kzg_commitments: maybe_blobs_bundle + .map(|b| b.commitments) + .unwrap_or_default(), + value: Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI), + pubkey: builder.builder_sk.public_key().compress(), + }), ForkName::Deneb => BuilderBid::Deneb(BuilderBidDeneb { header: payload .as_deneb() @@ -568,6 +597,17 @@ pub fn serve( Option>, ) = payload_response.into(); match fork { + ForkName::Electra => BuilderBid::Electra(BuilderBidElectra { + header: payload + .as_electra() + .map_err(|_| reject("incorrect payload variant"))? + .into(), + blob_kzg_commitments: maybe_blobs_bundle + .map(|b| b.commitments) + .unwrap_or_default(), + value: Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI), + pubkey: builder.builder_sk.public_key().compress(), + }), ForkName::Deneb => BuilderBid::Deneb(BuilderBidDeneb { header: payload .as_deneb() 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 7afeafc321..f76edfa90b 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 @@ -2,23 +2,21 @@ use crate::{ test_utils::{ MockServer, DEFAULT_JWT_SECRET, DEFAULT_TERMINAL_BLOCK, DEFAULT_TERMINAL_DIFFICULTY, }, - Config, *, + *, }; use keccak_hash::H256; use kzg::Kzg; -use sensitive_url::SensitiveUrl; -use task_executor::TaskExecutor; use tempfile::NamedTempFile; -use types::{Address, ChainSpec, Epoch, EthSpec, Hash256, MainnetEthSpec}; +use types::MainnetEthSpec; -pub struct MockExecutionLayer { - pub server: MockServer, - pub el: ExecutionLayer, +pub struct MockExecutionLayer { + pub server: MockServer, + pub el: ExecutionLayer, pub executor: TaskExecutor, pub spec: ChainSpec, } -impl MockExecutionLayer { +impl MockExecutionLayer { pub fn default_params(executor: TaskExecutor) -> Self { let mut spec = MainnetEthSpec::default_spec(); spec.terminal_total_difficulty = DEFAULT_TERMINAL_DIFFICULTY.into(); @@ -29,6 +27,7 @@ impl MockExecutionLayer { DEFAULT_TERMINAL_BLOCK, None, None, + None, Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()), spec, None, @@ -41,9 +40,10 @@ impl MockExecutionLayer { terminal_block: u64, shanghai_time: Option, cancun_time: Option, + prague_time: Option, jwt_key: Option, spec: ChainSpec, - kzg: Option, + kzg: Option>, ) -> Self { let handle = executor.handle().unwrap(); @@ -56,6 +56,7 @@ impl MockExecutionLayer { spec.terminal_block_hash, shanghai_time, cancun_time, + prague_time, kzg, ); @@ -145,7 +146,7 @@ impl MockExecutionLayer { .await .unwrap(); - let payload: ExecutionPayload = match block_proposal_content_type { + let payload: ExecutionPayload = match block_proposal_content_type { BlockProposalContentsType::Full(block) => block.to_payload().into(), BlockProposalContentsType::Blinded(_) => panic!("Should always be a full payload"), }; @@ -218,9 +219,9 @@ impl MockExecutionLayer { } #[allow(clippy::too_many_arguments)] - pub async fn assert_valid_execution_payload_on_head>( + pub async fn assert_valid_execution_payload_on_head>( &self, - payload: ExecutionPayload, + payload: ExecutionPayload, payload_header: Payload, block_hash: ExecutionBlockHash, parent_hash: ExecutionBlockHash, @@ -244,7 +245,7 @@ impl MockExecutionLayer { // TODO: again consider forks let status = self .el - .notify_new_payload(payload.try_into().unwrap()) + .notify_new_payload(payload.to_ref().try_into().unwrap()) .await .unwrap(); assert_eq!(status, PayloadStatus::Valid); @@ -306,7 +307,7 @@ impl MockExecutionLayer { pub async fn with_terminal_block<'a, U, V>(self, func: U) -> Self where - U: Fn(ChainSpec, ExecutionLayer, Option) -> V, + U: Fn(ChainSpec, ExecutionLayer, Option) -> V, V: Future, { let terminal_block_number = self diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index f0be511147..eeb449b992 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -25,8 +25,8 @@ use warp::{http::StatusCode, Filter, Rejection}; use crate::EngineCapabilities; pub use execution_block_generator::{ - generate_blobs, generate_genesis_block, generate_genesis_header, generate_pow_block, Block, - ExecutionBlockGenerator, + generate_blobs, generate_genesis_block, generate_genesis_header, generate_pow_block, + static_valid_tx, Block, ExecutionBlockGenerator, }; pub use hook::Hook; pub use mock_builder::{MockBuilder, Operation}; @@ -66,6 +66,7 @@ pub struct MockExecutionConfig { pub terminal_block_hash: ExecutionBlockHash, pub shanghai_time: Option, pub cancun_time: Option, + pub prague_time: Option, } impl Default for MockExecutionConfig { @@ -78,18 +79,19 @@ impl Default for MockExecutionConfig { server_config: Config::default(), shanghai_time: None, cancun_time: None, + prague_time: None, } } } -pub struct MockServer { +pub struct MockServer { _shutdown_tx: oneshot::Sender<()>, listen_socket_addr: SocketAddr, last_echo_request: Arc>>, - pub ctx: Arc>, + pub ctx: Arc>, } -impl MockServer { +impl MockServer { pub fn unit_testing() -> Self { Self::new( &runtime::Handle::current(), @@ -99,14 +101,15 @@ impl MockServer { ExecutionBlockHash::zero(), None, // FIXME(capella): should this be the default? None, // FIXME(deneb): should this be the default? - None, // FIXME(deneb): should this be the default? + None, // FIXME(electra): should this be the default? + None, ) } pub fn new_with_config( handle: &runtime::Handle, config: MockExecutionConfig, - kzg: Option, + kzg: Option>, ) -> Self { let MockExecutionConfig { jwt_key, @@ -116,6 +119,7 @@ impl MockServer { server_config, shanghai_time, cancun_time, + prague_time, } = config; let last_echo_request = Arc::new(RwLock::new(None)); let preloaded_responses = Arc::new(Mutex::new(vec![])); @@ -125,10 +129,11 @@ impl MockServer { terminal_block_hash, shanghai_time, cancun_time, + prague_time, kzg, ); - let ctx: Arc> = Arc::new(Context { + let ctx: Arc> = Arc::new(Context { config: server_config, jwt_key, log: null_logger().unwrap(), @@ -187,7 +192,8 @@ impl MockServer { terminal_block_hash: ExecutionBlockHash, shanghai_time: Option, cancun_time: Option, - kzg: Option, + prague_time: Option, + kzg: Option>, ) -> Self { Self::new_with_config( handle, @@ -199,12 +205,13 @@ impl MockServer { terminal_block_hash, shanghai_time, cancun_time, + prague_time, }, kzg, ) } - pub fn execution_block_generator(&self) -> RwLockWriteGuard<'_, ExecutionBlockGenerator> { + pub fn execution_block_generator(&self) -> RwLockWriteGuard<'_, ExecutionBlockGenerator> { self.ctx.execution_block_generator.write() } @@ -416,7 +423,7 @@ impl MockServer { .insert_block_without_checks(block); } - pub fn get_block(&self, block_hash: ExecutionBlockHash) -> Option> { + pub fn get_block(&self, block_hash: ExecutionBlockHash) -> Option> { self.ctx .execution_block_generator .read() @@ -494,12 +501,12 @@ impl warp::reject::Reject for AuthError {} /// A wrapper around all the items required to spawn the HTTP server. /// /// The server will gracefully handle the case where any fields are `None`. -pub struct Context { +pub struct Context { pub config: Config, pub jwt_key: JwtKey, pub log: Logger, pub last_echo_request: Arc>>, - pub execution_block_generator: RwLock>, + pub execution_block_generator: RwLock>, pub preloaded_responses: Arc>>, pub previous_request: Arc>>, pub static_new_payload_response: Arc>>, @@ -518,10 +525,10 @@ pub struct Context { pub syncing_response: Arc>>, pub engine_capabilities: Arc>, - pub _phantom: PhantomData, + pub _phantom: PhantomData, } -impl Context { +impl Context { pub fn get_new_payload_status( &self, block_hash: &ExecutionBlockHash, @@ -599,8 +606,8 @@ async fn handle_rejection(err: Rejection) -> Result() { - message = format!("Authorization error: {:?}", e); + if let Some(AuthError(e)) = err.find::() { + message = format!("Authorization error: {}", e); code = StatusCode::UNAUTHORIZED; } else { message = "BAD_REQUEST".to_string(); @@ -630,8 +637,8 @@ async fn handle_rejection(err: Rejection) -> Result( - ctx: Arc>, +pub fn serve( + ctx: Arc>, shutdown: impl Future + Send + Sync + 'static, ) -> Result<(SocketAddr, impl Future), Error> { let config = &ctx.config; @@ -646,7 +653,7 @@ pub fn serve( let root = warp::path::end() .and(warp::body::json()) .and(ctx_filter.clone()) - .and_then(|body: serde_json::Value, ctx: Arc>| async move { + .and_then(|body: serde_json::Value, ctx: Arc>| async move { let id = body .get("id") .and_then(serde_json::Value::as_u64) @@ -693,7 +700,7 @@ pub fn serve( let echo = warp::path("echo") .and(warp::body::bytes()) .and(ctx_filter) - .and_then(|bytes: Bytes, ctx: Arc>| async move { + .and_then(|bytes: Bytes, ctx: Arc>| async move { *ctx.last_echo_request.write() = Some(bytes.clone()); Ok::<_, warp::reject::Rejection>( warp::http::Response::builder().status(200).body(bytes), diff --git a/beacon_node/execution_layer/src/versioned_hashes.rs b/beacon_node/execution_layer/src/versioned_hashes.rs new file mode 100644 index 0000000000..37bd35646d --- /dev/null +++ b/beacon_node/execution_layer/src/versioned_hashes.rs @@ -0,0 +1,135 @@ +extern crate alloy_consensus; +extern crate alloy_rlp; +use alloy_consensus::TxEnvelope; +use alloy_rlp::Decodable; +use types::{EthSpec, ExecutionPayloadRef, Hash256, Unsigned, VersionedHash}; + +#[derive(Debug)] +pub enum Error { + DecodingTransaction(String), + LengthMismatch { expected: usize, found: usize }, + VersionHashMismatch { expected: Hash256, found: Hash256 }, +} + +pub fn verify_versioned_hashes( + execution_payload: ExecutionPayloadRef, + expected_versioned_hashes: &[VersionedHash], +) -> Result<(), Error> { + let versioned_hashes = + extract_versioned_hashes_from_transactions::(execution_payload.transactions())?; + if versioned_hashes.len() != expected_versioned_hashes.len() { + return Err(Error::LengthMismatch { + expected: expected_versioned_hashes.len(), + found: versioned_hashes.len(), + }); + } + for (found, expected) in versioned_hashes + .iter() + .zip(expected_versioned_hashes.iter()) + { + if found != expected { + return Err(Error::VersionHashMismatch { + expected: *expected, + found: *found, + }); + } + } + + Ok(()) +} + +pub fn extract_versioned_hashes_from_transactions( + transactions: &types::Transactions, +) -> Result, Error> { + let mut versioned_hashes = Vec::new(); + + for tx in transactions { + match beacon_tx_to_tx_envelope(tx)? { + TxEnvelope::Eip4844(signed_tx_eip4844) => { + versioned_hashes.extend( + signed_tx_eip4844 + .tx() + .blob_versioned_hashes + .iter() + .map(|fb| Hash256::from(fb.0)), + ); + } + // enumerating all variants explicitly to make pattern irrefutable + // in case new types are added in the future which also have blobs + TxEnvelope::Legacy(_) + | TxEnvelope::TaggedLegacy(_) + | TxEnvelope::Eip2930(_) + | TxEnvelope::Eip1559(_) => {} + } + } + + Ok(versioned_hashes) +} + +pub fn beacon_tx_to_tx_envelope( + tx: &types::Transaction, +) -> Result { + let tx_bytes = Vec::from(tx.clone()); + TxEnvelope::decode(&mut tx_bytes.as_slice()) + .map_err(|e| Error::DecodingTransaction(e.to_string())) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::test_utils::static_valid_tx; + use alloy_consensus::{TxKind, TxLegacy}; + + type E = types::MainnetEthSpec; + + #[test] + fn test_decode_static_transaction() { + let valid_tx = static_valid_tx::().expect("should give me known valid transaction"); + let tx_envelope = beacon_tx_to_tx_envelope(&valid_tx).expect("should decode tx"); + let TxEnvelope::Legacy(signed_tx) = tx_envelope else { + panic!("should decode to legacy transaction"); + }; + + assert!(matches!( + signed_tx.tx(), + TxLegacy { + chain_id: Some(0x01), + nonce: 0x15, + gas_price: 0x4a817c800, + to: TxKind::Call(..), + .. + } + )); + } + + #[test] + fn test_extract_versioned_hashes() { + use serde::Deserialize; + + #[derive(Deserialize)] + #[serde(transparent)] + struct TestTransactions( + #[serde(with = "ssz_types::serde_utils::list_of_hex_var_list")] types::Transactions, + ); + + let TestTransactions(raw_transactions): TestTransactions = serde_json::from_str(r#"[ + "0x03f901388501a1f0ff430f843b9aca00843b9aca0082520894e7249813d8ccf6fa95a2203f46a64166073d58878080c002f8c6a0012e98362c814f1724262c0d211a1463418a5f6382a8d457b37a2698afbe7b5ea00100ef985761395dfa8ed5ce91f3f2180b612401909e4cb8f33b90c8a454d9baa0013d45411623b90d90f916e4025ada74b453dd4ca093c017c838367c9de0f801a001753e2af0b1e70e7ef80541355b2a035cc9b2c177418bb2a4402a9b346cf84da0011789b520a8068094a92aa0b04db8d8ef1c6c9818947c5210821732b8744049a0011c4c4f95597305daa5f62bf5f690e37fa11f5de05a95d05cac4e2119e394db80a0ccd86a742af0e042d08cbb35d910ddc24bbc6538f9e53be6620d4b6e1bb77662a01a8bacbc614940ac2f5c23ffc00a122c9f085046883de65c88ab0edb859acb99", + "0x02f9017a8501a1f0ff4382363485012a05f2008512a05f2000830249f094c1b0bc605e2c808aa0867bfc98e51a1fe3e9867f80b901040cc7326300000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000009445a285baa43e00000000000000000000000000c500931f24edb821cef6e28f7adb33b38578c82000000000000000000000000fc7360b3b28cf4204268a8354dbec60720d155d2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000009a054a063f0fe7b9c68de8df91aaa5e96c15ab540000000000000000000000000c8d41b8fcc066cdabaf074d78e5153e8ce018a9c080a008e14475c1173cd9f5740c24c08b793f9e16c36c08fa73769db95050e31e3396a019767dcdda26c4a774ca28c9df15d0c20e43bd07bd33ee0f84d6096cb5a1ebed" + ]"#).expect("should get raw transactions"); + let expected_versioned_hashes = vec![ + "0x012e98362c814f1724262c0d211a1463418a5f6382a8d457b37a2698afbe7b5e", + "0x0100ef985761395dfa8ed5ce91f3f2180b612401909e4cb8f33b90c8a454d9ba", + "0x013d45411623b90d90f916e4025ada74b453dd4ca093c017c838367c9de0f801", + "0x01753e2af0b1e70e7ef80541355b2a035cc9b2c177418bb2a4402a9b346cf84d", + "0x011789b520a8068094a92aa0b04db8d8ef1c6c9818947c5210821732b8744049", + "0x011c4c4f95597305daa5f62bf5f690e37fa11f5de05a95d05cac4e2119e394db", + ] + .into_iter() + .map(|tx| Hash256::from_slice(&hex::decode(&tx[2..]).expect("should decode hex"))) + .collect::>(); + + let versioned_hashes = extract_versioned_hashes_from_transactions::(&raw_transactions) + .expect("should get versioned hashes"); + assert_eq!(versioned_hashes, expected_versioned_hashes); + } +} diff --git a/beacon_node/genesis/src/interop.rs b/beacon_node/genesis/src/interop.rs index d012983430..b4753e92f1 100644 --- a/beacon_node/genesis/src/interop.rs +++ b/beacon_node/genesis/src/interop.rs @@ -28,18 +28,18 @@ fn eth1_withdrawal_credentials(pubkey: &PublicKey, spec: &ChainSpec) -> Hash256 /// /// Reference: /// https://github.com/ethereum/eth2.0-pm/tree/6e41fcf383ebeb5125938850d8e9b4e9888389b4/interop/mocked_start -pub fn interop_genesis_state( +pub fn interop_genesis_state( keypairs: &[Keypair], genesis_time: u64, eth1_block_hash: Hash256, - execution_payload_header: Option>, + execution_payload_header: Option>, spec: &ChainSpec, -) -> Result, String> { +) -> Result, String> { let withdrawal_credentials = keypairs .iter() .map(|keypair| bls_withdrawal_credentials(&keypair.pk, spec)) .collect::>(); - interop_genesis_state_with_withdrawal_credentials::( + interop_genesis_state_with_withdrawal_credentials::( keypairs, &withdrawal_credentials, genesis_time, @@ -51,13 +51,13 @@ pub fn interop_genesis_state( // returns an interop genesis state except every other // validator has eth1 withdrawal credentials -pub fn interop_genesis_state_with_eth1( +pub fn interop_genesis_state_with_eth1( keypairs: &[Keypair], genesis_time: u64, eth1_block_hash: Hash256, - execution_payload_header: Option>, + execution_payload_header: Option>, spec: &ChainSpec, -) -> Result, String> { +) -> Result, String> { let withdrawal_credentials = keypairs .iter() .enumerate() @@ -69,7 +69,7 @@ pub fn interop_genesis_state_with_eth1( } }) .collect::>(); - interop_genesis_state_with_withdrawal_credentials::( + interop_genesis_state_with_withdrawal_credentials::( keypairs, &withdrawal_credentials, genesis_time, @@ -79,14 +79,14 @@ pub fn interop_genesis_state_with_eth1( ) } -pub fn interop_genesis_state_with_withdrawal_credentials( +pub fn interop_genesis_state_with_withdrawal_credentials( keypairs: &[Keypair], withdrawal_credentials: &[Hash256], genesis_time: u64, eth1_block_hash: Hash256, - execution_payload_header: Option>, + execution_payload_header: Option>, spec: &ChainSpec, -) -> Result, String> { +) -> Result, String> { if keypairs.len() != withdrawal_credentials.len() { return Err(format!( "wrong number of withdrawal credentials, expected: {}, got: {}", @@ -137,7 +137,7 @@ pub fn interop_genesis_state_with_withdrawal_credentials( #[cfg(test)] mod test { use super::*; - use types::{test_utils::generate_deterministic_keypairs, EthSpec, MinimalEthSpec}; + use types::{test_utils::generate_deterministic_keypairs, MinimalEthSpec}; type TestEthSpec = MinimalEthSpec; diff --git a/beacon_node/http_api/src/attestation_performance.rs b/beacon_node/http_api/src/attestation_performance.rs index 3e7d8d5e31..d4f9916814 100644 --- a/beacon_node/http_api/src/attestation_performance.rs +++ b/beacon_node/http_api/src/attestation_performance.rs @@ -3,7 +3,6 @@ use eth2::lighthouse::{ AttestationPerformance, AttestationPerformanceQuery, AttestationPerformanceStatistics, }; use state_processing::{ - per_epoch_processing::altair::participation_cache::Error as ParticipationCacheError, per_epoch_processing::EpochProcessingSummary, BlockReplayError, BlockReplayer, }; use std::sync::Arc; @@ -14,11 +13,11 @@ const MAX_REQUEST_RANGE_EPOCHS: usize = 100; const BLOCK_ROOT_CHUNK_SIZE: usize = 100; #[derive(Debug)] +// We don't use the inner values directly, but they're used in the Debug impl. enum AttestationPerformanceError { - BlockReplay(BlockReplayError), - BeaconState(BeaconStateError), - ParticipationCache(ParticipationCacheError), - UnableToFindValidator(usize), + BlockReplay(#[allow(dead_code)] BlockReplayError), + BeaconState(#[allow(dead_code)] BeaconStateError), + UnableToFindValidator(#[allow(dead_code)] usize), } impl From for AttestationPerformanceError { @@ -33,12 +32,6 @@ impl From for AttestationPerformanceError { } } -impl From for AttestationPerformanceError { - fn from(e: ParticipationCacheError) -> Self { - Self::ParticipationCache(e) - } -} - pub fn get_attestation_performance( target: String, query: AttestationPerformanceQuery, diff --git a/beacon_node/http_api/src/block_packing_efficiency.rs b/beacon_node/http_api/src/block_packing_efficiency.rs index e099e130a8..d78f1f7c66 100644 --- a/beacon_node/http_api/src/block_packing_efficiency.rs +++ b/beacon_node/http_api/src/block_packing_efficiency.rs @@ -19,10 +19,11 @@ use warp_utils::reject::{beacon_chain_error, custom_bad_request, custom_server_e const BLOCK_ROOT_CHUNK_SIZE: usize = 100; #[derive(Debug)] +// We don't use the inner values directly, but they're used in the Debug impl. enum PackingEfficiencyError { - BlockReplay(BlockReplayError), - BeaconState(BeaconStateError), - CommitteeStoreError(Slot), + BlockReplay(#[allow(dead_code)] BlockReplayError), + BeaconState(#[allow(dead_code)] BeaconStateError), + CommitteeStoreError(#[allow(dead_code)] Slot), InvalidAttestationError, } @@ -52,24 +53,24 @@ impl CommitteeStore { } } -struct PackingEfficiencyHandler { +struct PackingEfficiencyHandler { current_slot: Slot, current_epoch: Epoch, prior_skip_slots: u64, available_attestations: HashSet, included_attestations: HashMap, committee_store: CommitteeStore, - _phantom: PhantomData, + _phantom: PhantomData, } -impl PackingEfficiencyHandler { +impl PackingEfficiencyHandler { fn new( start_epoch: Epoch, - starting_state: BeaconState, + starting_state: BeaconState, spec: &ChainSpec, ) -> Result { let mut handler = PackingEfficiencyHandler { - current_slot: start_epoch.start_slot(T::slots_per_epoch()), + current_slot: start_epoch.start_slot(E::slots_per_epoch()), current_epoch: start_epoch, prior_skip_slots: 0, available_attestations: HashSet::new(), @@ -84,27 +85,27 @@ impl PackingEfficiencyHandler { fn update_slot(&mut self, slot: Slot) { self.current_slot = slot; - if slot % T::slots_per_epoch() == 0 { - self.current_epoch = Epoch::new(slot.as_u64() / T::slots_per_epoch()); + if slot % E::slots_per_epoch() == 0 { + self.current_epoch = Epoch::new(slot.as_u64() / E::slots_per_epoch()); } } fn prune_included_attestations(&mut self) { let epoch = self.current_epoch; self.included_attestations.retain(|x, _| { - x.slot >= Epoch::new(epoch.as_u64().saturating_sub(2)).start_slot(T::slots_per_epoch()) + x.slot >= Epoch::new(epoch.as_u64().saturating_sub(2)).start_slot(E::slots_per_epoch()) }); } fn prune_available_attestations(&mut self) { let slot = self.current_slot; self.available_attestations - .retain(|x| x.slot >= (slot.as_u64().saturating_sub(T::slots_per_epoch()))); + .retain(|x| x.slot >= (slot.as_u64().saturating_sub(E::slots_per_epoch()))); } fn apply_block( &mut self, - block: &SignedBeaconBlock>, + block: &SignedBeaconBlock>, ) -> Result { let block_body = block.message().body(); let attestations = block_body.attestations(); @@ -131,7 +132,7 @@ impl PackingEfficiencyHandler { } // Remove duplicate attestations as these yield no reward. - attestations_in_block.retain(|x, _| self.included_attestations.get(x).is_none()); + attestations_in_block.retain(|x, _| !self.included_attestations.contains_key(x)); self.included_attestations .extend(attestations_in_block.clone()); @@ -157,7 +158,7 @@ impl PackingEfficiencyHandler { fn compute_epoch( &mut self, epoch: Epoch, - state: &BeaconState, + state: &BeaconState, spec: &ChainSpec, ) -> Result<(), PackingEfficiencyError> { // Free some memory by pruning old attestations from the included set. @@ -178,8 +179,9 @@ impl PackingEfficiencyHandler { .collect::>() }; - self.committee_store.previous_epoch_committees = - self.committee_store.current_epoch_committees.clone(); + self.committee_store + .previous_epoch_committees + .clone_from(&self.committee_store.current_epoch_committees); self.committee_store.current_epoch_committees = new_committees; diff --git a/beacon_node/http_api/src/build_block_contents.rs b/beacon_node/http_api/src/build_block_contents.rs index 37b4049c0c..7e3778b3fb 100644 --- a/beacon_node/http_api/src/build_block_contents.rs +++ b/beacon_node/http_api/src/build_block_contents.rs @@ -15,7 +15,7 @@ pub fn build_block_contents( ForkName::Base | ForkName::Altair | ForkName::Merge | ForkName::Capella => Ok( ProduceBlockV3Response::Full(FullBlockContents::Block(block.block)), ), - ForkName::Deneb => { + ForkName::Deneb | ForkName::Electra => { let BeaconBlockResponse { block, state: _, diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index fe01f3c524..cc117c3fb9 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -16,6 +16,7 @@ mod database; mod metrics; mod produce_block; mod proposer_duties; +mod publish_attestations; mod publish_blocks; mod standard_block_rewards; mod state_id; @@ -35,7 +36,7 @@ use beacon_chain::{ validator_monitor::timestamp_now, AttestationError as AttnError, BeaconChain, BeaconChainError, BeaconChainTypes, WhenSlotSkipped, }; -use beacon_processor::BeaconProcessorSend; +use beacon_processor::{work_reprocessing_queue::ReprocessQueueMessage, BeaconProcessorSend}; pub use block_id::BlockId; use builder_states::get_next_withdrawals; use bytes::Bytes; @@ -67,7 +68,7 @@ use std::path::PathBuf; use std::pin::Pin; use std::sync::Arc; use sysinfo::{System, SystemExt}; -use system_health::observe_system_health_bn; +use system_health::{observe_nat, observe_system_health_bn}; use task_spawner::{Priority, TaskSpawner}; use tokio::sync::{ mpsc::{Sender, UnboundedSender}, @@ -95,7 +96,7 @@ use warp::http::StatusCode; use warp::hyper::Body; use warp::sse::Event; use warp::Reply; -use warp::{http::Response, Filter}; +use warp::{http::Response, Filter, Rejection}; use warp_utils::{query::multi_key_query, uor::UnifyingOrFilter}; const API_PREFIX: &str = "eth"; @@ -129,6 +130,7 @@ pub struct Context { pub network_senders: Option>, pub network_globals: Option>>, pub beacon_processor_send: Option>, + pub beacon_processor_reprocess_send: Option>, pub eth1_service: Option, pub sse_logging_components: Option, pub log: Logger, @@ -142,7 +144,6 @@ pub struct Config { pub listen_port: u16, pub allow_origin: Option, pub tls_config: Option, - pub allow_sync_stalled: bool, pub spec_fork_name: Option, pub data_dir: PathBuf, pub sse_capacity_multiplier: usize, @@ -160,7 +161,6 @@ impl Default for Config { listen_port: 5052, allow_origin: None, tls_config: None, - allow_sync_stalled: false, spec_fork_name: None, data_dir: PathBuf::from(DEFAULT_ROOT_DIR), sse_capacity_multiplier: 1, @@ -319,7 +319,6 @@ pub fn serve( shutdown: impl Future + Send + Sync + 'static, ) -> Result { let config = ctx.config.clone(); - let allow_sync_stalled = config.allow_sync_stalled; let log = ctx.log.clone(); // Configure CORS. @@ -451,7 +450,7 @@ pub fn serve( warp::any() .and(network_globals.clone()) .and(chain_filter.clone()) - .and_then( + .then( move |network_globals: Arc>, chain: Arc>| async move { match *network_globals.sync_state.read() { @@ -480,14 +479,10 @@ pub fn serve( | SyncState::SyncTransition | SyncState::BackFillSyncing { .. } => Ok(()), SyncState::Synced => Ok(()), - SyncState::Stalled if allow_sync_stalled => Ok(()), - SyncState::Stalled => Err(warp_utils::reject::not_synced( - "sync is stalled".to_string(), - )), + SyncState::Stalled => Ok(()), } }, - ) - .untuple_one(); + ); // Create a `warp` filter that provides access to the logger. let inner_ctx = ctx.clone(); @@ -534,6 +529,11 @@ pub fn serve( .filter(|_| config.enable_beacon_processor); let task_spawner_filter = warp::any().map(move || TaskSpawner::new(beacon_processor_send.clone())); + let beacon_processor_reprocess_send = ctx + .beacon_processor_reprocess_send + .clone() + .filter(|_| config.enable_beacon_processor); + let reprocess_send_filter = warp::any().map(move || beacon_processor_reprocess_send.clone()); let duplicate_block_status_code = ctx.config.duplicate_block_status_code; @@ -1019,7 +1019,7 @@ pub fn serve( Ok(( state .get_built_sync_committee(epoch, &chain.spec) - .map(|committee| committee.clone()) + .cloned() .map_err(|e| match e { BeaconStateError::SyncCommitteeNotKnown { .. } => { warp_utils::reject::custom_bad_request(format!( @@ -1756,140 +1756,26 @@ pub fn serve( .and(warp::path::end()) .and(warp_utils::json::json()) .and(network_tx_filter.clone()) + .and(reprocess_send_filter) .and(log_filter.clone()) .then( |task_spawner: TaskSpawner, chain: Arc>, attestations: Vec>, network_tx: UnboundedSender>, - log: Logger| { - task_spawner.blocking_json_task(Priority::P0, move || { - let seen_timestamp = timestamp_now(); - let mut failures = Vec::new(); - let mut num_already_known = 0; - - for (index, attestation) in attestations.as_slice().iter().enumerate() { - let attestation = match chain - .verify_unaggregated_attestation_for_gossip(attestation, None) - { - Ok(attestation) => attestation, - Err(AttnError::PriorAttestationKnown { .. }) => { - num_already_known += 1; - - // Skip to the next attestation since an attestation for this - // validator is already known in this epoch. - // - // There's little value for the network in validating a second - // attestation for another validator since it is either: - // - // 1. A duplicate. - // 2. Slashable. - // 3. Invalid. - // - // We are likely to get duplicates in the case where a VC is using - // fallback BNs. If the first BN actually publishes some/all of a - // batch of attestations but fails to respond in a timely fashion, - // the VC is likely to try publishing the attestations on another - // BN. That second BN may have already seen the attestations from - // the first BN and therefore indicate that the attestations are - // "already seen". An attestation that has already been seen has - // been published on the network so there's no actual error from - // the perspective of the user. - // - // It's better to prevent slashable attestations from ever - // appearing on the network than trying to slash validators, - // especially those validators connected to the local API. - // - // There might be *some* value in determining that this attestation - // is invalid, but since a valid attestation already it exists it - // appears that this validator is capable of producing valid - // attestations and there's no immediate cause for concern. - continue; - } - Err(e) => { - error!(log, - "Failure verifying attestation for gossip"; - "error" => ?e, - "request_index" => index, - "committee_index" => attestation.data.index, - "attestation_slot" => attestation.data.slot, - ); - failures.push(api_types::Failure::new( - index, - format!("Verification: {:?}", e), - )); - // skip to the next attestation so we do not publish this one to gossip - continue; - } - }; - - // Notify the validator monitor. - chain - .validator_monitor - .read() - .register_api_unaggregated_attestation( - seen_timestamp, - attestation.indexed_attestation(), - &chain.slot_clock, - ); - - publish_pubsub_message( - &network_tx, - PubsubMessage::Attestation(Box::new(( - attestation.subnet_id(), - attestation.attestation().clone(), - ))), - )?; - - let committee_index = attestation.attestation().data.index; - let slot = attestation.attestation().data.slot; - - if let Err(e) = chain.apply_attestation_to_fork_choice(&attestation) { - error!(log, - "Failure applying verified attestation to fork choice"; - "error" => ?e, - "request_index" => index, - "committee_index" => committee_index, - "slot" => slot, - ); - failures.push(api_types::Failure::new( - index, - format!("Fork choice: {:?}", e), - )); - }; - - if let Err(e) = chain.add_to_naive_aggregation_pool(&attestation) { - error!(log, - "Failure adding verified attestation to the naive aggregation pool"; - "error" => ?e, - "request_index" => index, - "committee_index" => committee_index, - "slot" => slot, - ); - failures.push(api_types::Failure::new( - index, - format!("Naive aggregation pool: {:?}", e), - )); - } - } - - if num_already_known > 0 { - debug!( - log, - "Some unagg attestations already known"; - "count" => num_already_known - ); - } - - if failures.is_empty() { - Ok(()) - } else { - Err(warp_utils::reject::indexed_bad_request( - "error processing attestations".to_string(), - failures, - )) - } - }) + reprocess_tx: Option>, + log: Logger| async move { + let result = crate::publish_attestations::publish_attestations( + task_spawner, + chain, + attestations, + network_tx, + reprocess_tx, + log, + ) + .await + .map(|()| warp::reply::json(&())); + task_spawner::convert_rejection(result).await }, ); @@ -2444,7 +2330,7 @@ pub fn serve( let fork_name = chain .spec - .fork_name_at_slot::(update.signature_slot); + .fork_name_at_slot::(*update.signature_slot()); match accept_header { Some(api_types::Accept::Ssz) => Response::builder() .status(200) @@ -2491,7 +2377,7 @@ pub fn serve( let fork_name = chain .spec - .fork_name_at_slot::(update.signature_slot); + .fork_name_at_slot::(*update.signature_slot()); match accept_header { Some(api_types::Accept::Ssz) => Response::builder() .status(200) @@ -2858,7 +2744,7 @@ pub fn serve( hex::encode( meta_data .syncnets() - .map(|x| x.clone()) + .cloned() .unwrap_or_default() .into_bytes() ) @@ -3165,10 +3051,12 @@ pub fn serve( .and(log_filter.clone()) .then( |epoch: Epoch, + not_synced_filter: Result<(), Rejection>, task_spawner: TaskSpawner, chain: Arc>, log: Logger| { task_spawner.blocking_json_task(Priority::P0, move || { + not_synced_filter?; proposer_duties::proposer_duties(epoch, &chain, &log) }) }, @@ -3194,6 +3082,7 @@ pub fn serve( |endpoint_version: EndpointVersion, slot: Slot, accept_header: Option, + not_synced_filter: Result<(), Rejection>, query: api_types::ValidatorBlocksQuery, task_spawner: TaskSpawner, chain: Arc>, @@ -3205,6 +3094,8 @@ pub fn serve( "slot" => slot ); + not_synced_filter?; + if endpoint_version == V3 { produce_block_v3(accept_header, chain, slot, query).await } else { @@ -3231,11 +3122,13 @@ pub fn serve( .and(chain_filter.clone()) .then( |slot: Slot, + not_synced_filter: Result<(), Rejection>, query: api_types::ValidatorBlocksQuery, accept_header: Option, task_spawner: TaskSpawner, chain: Arc>| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { + not_synced_filter?; produce_blinded_block_v2(EndpointVersion(2), accept_header, chain, slot, query) .await }) @@ -3253,9 +3146,12 @@ pub fn serve( .and(chain_filter.clone()) .then( |query: api_types::ValidatorAttestationDataQuery, + not_synced_filter: Result<(), Rejection>, task_spawner: TaskSpawner, chain: Arc>| { task_spawner.blocking_json_task(Priority::P0, move || { + not_synced_filter?; + let current_slot = chain .slot() .map_err(warp_utils::reject::beacon_chain_error)?; @@ -3288,9 +3184,11 @@ pub fn serve( .and(chain_filter.clone()) .then( |query: api_types::ValidatorAggregateAttestationQuery, + not_synced_filter: Result<(), Rejection>, task_spawner: TaskSpawner, chain: Arc>| { task_spawner.blocking_json_task(Priority::P0, move || { + not_synced_filter?; chain .get_aggregated_attestation_by_slot_and_root( query.slot, @@ -3329,10 +3227,12 @@ pub fn serve( .and(chain_filter.clone()) .then( |epoch: Epoch, + not_synced_filter: Result<(), Rejection>, indices: api_types::ValidatorIndexData, task_spawner: TaskSpawner, chain: Arc>| { task_spawner.blocking_json_task(Priority::P0, move || { + not_synced_filter?; attester_duties::attester_duties(epoch, &indices.0, &chain) }) }, @@ -3355,10 +3255,12 @@ pub fn serve( .and(chain_filter.clone()) .then( |epoch: Epoch, + not_synced_filter: Result<(), Rejection>, indices: api_types::ValidatorIndexData, task_spawner: TaskSpawner, chain: Arc>| { task_spawner.blocking_json_task(Priority::P0, move || { + not_synced_filter?; sync_committees::sync_committee_duties(epoch, &indices.0, &chain) }) }, @@ -3375,9 +3277,11 @@ pub fn serve( .and(chain_filter.clone()) .then( |sync_committee_data: SyncContributionData, + not_synced_filter: Result<(), Rejection>, task_spawner: TaskSpawner, chain: Arc>| { task_spawner.blocking_json_task(Priority::P0, move || { + not_synced_filter?; chain .get_aggregated_sync_committee_contribution(&sync_committee_data) .map_err(|e| { @@ -3408,11 +3312,13 @@ pub fn serve( .and(network_tx_filter.clone()) .and(log_filter.clone()) .then( - |task_spawner: TaskSpawner, + |not_synced_filter: Result<(), Rejection>, + task_spawner: TaskSpawner, chain: Arc>, aggregates: Vec>, network_tx: UnboundedSender>, log: Logger| { task_spawner.blocking_json_task(Priority::P0, move || { + not_synced_filter?; let seen_timestamp = timestamp_now(); let mut verified_aggregates = Vec::with_capacity(aggregates.len()); let mut messages = Vec::with_capacity(aggregates.len()); @@ -3521,12 +3427,14 @@ pub fn serve( .and(network_tx_filter) .and(log_filter.clone()) .then( - |task_spawner: TaskSpawner, + |not_synced_filter: Result<(), Rejection>, + task_spawner: TaskSpawner, chain: Arc>, contributions: Vec>, network_tx: UnboundedSender>, log: Logger| { task_spawner.blocking_json_task(Priority::P0, move || { + not_synced_filter?; sync_committees::process_signed_contribution_and_proofs( contributions, network_tx, @@ -3555,34 +3463,34 @@ pub fn serve( chain: Arc>, log: Logger| { task_spawner.blocking_json_task(Priority::P0, move || { - for subscription in &subscriptions { - chain - .validator_monitor - .write() - .auto_register_local_validator(subscription.validator_index); - - let validator_subscription = api_types::ValidatorSubscription { - validator_index: subscription.validator_index, - attestation_committee_index: subscription.committee_index, - slot: subscription.slot, - committee_count_at_slot: subscription.committees_at_slot, - is_aggregator: subscription.is_aggregator, - }; - - let message = ValidatorSubscriptionMessage::AttestationSubscribe { - subscriptions: vec![validator_subscription], - }; - if let Err(e) = validator_subscription_tx.try_send(message) { - warn!( - log, - "Unable to process committee subscriptions"; - "info" => "the host may be overloaded or resource-constrained", - "error" => ?e, - ); - return Err(warp_utils::reject::custom_server_error( - "unable to queue subscription, host may be overloaded or shutting down".to_string(), - )); - } + let subscriptions: std::collections::BTreeSet<_> = subscriptions + .iter() + .map(|subscription| { + chain + .validator_monitor + .write() + .auto_register_local_validator(subscription.validator_index); + api_types::ValidatorSubscription { + attestation_committee_index: subscription.committee_index, + slot: subscription.slot, + committee_count_at_slot: subscription.committees_at_slot, + is_aggregator: subscription.is_aggregator, + } + }) + .collect(); + let message = + ValidatorSubscriptionMessage::AttestationSubscribe { subscriptions }; + if let Err(e) = validator_subscription_tx.try_send(message) { + warn!( + log, + "Unable to process committee subscriptions"; + "info" => "the host may be overloaded or resource-constrained", + "error" => ?e, + ); + return Err(warp_utils::reject::custom_server_error( + "unable to queue subscription, host may be overloaded or shutting down" + .to_string(), + )); } Ok(()) @@ -3601,11 +3509,13 @@ pub fn serve( .and(log_filter.clone()) .and(warp_utils::json::json()) .then( - |task_spawner: TaskSpawner, + |not_synced_filter: Result<(), Rejection>, + task_spawner: TaskSpawner, chain: Arc>, log: Logger, preparation_data: Vec| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { + not_synced_filter?; let execution_layer = chain .execution_layer .as_ref() @@ -3864,7 +3774,7 @@ pub fn serve( }, ); - // POST vaidator/liveness/{epoch} + // POST validator/liveness/{epoch} let post_validator_liveness_epoch = eth_v1 .and(warp::path("validator")) .and(warp::path("liveness")) @@ -3875,7 +3785,7 @@ pub fn serve( .and(chain_filter.clone()) .then( |epoch: Epoch, - indices: Vec, + indices: api_types::ValidatorIndexData, task_spawner: TaskSpawner, chain: Arc>| { task_spawner.blocking_json_task(Priority::P0, move || { @@ -3894,6 +3804,7 @@ pub fn serve( } let liveness: Vec = indices + .0 .iter() .cloned() .map(|index| { @@ -4071,13 +3982,7 @@ pub fn serve( .and(warp::path::end()) .then(|task_spawner: TaskSpawner| { task_spawner.blocking_json_task(Priority::P1, move || { - Ok(api_types::GenericResponse::from( - lighthouse_network::metrics::NAT_OPEN - .as_ref() - .map(|v| v.get()) - .unwrap_or(0) - != 0, - )) + Ok(api_types::GenericResponse::from(observe_nat())) }) }); @@ -4309,8 +4214,11 @@ pub fn serve( .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .then( - |task_spawner: TaskSpawner, chain: Arc>| { + |not_synced_filter: Result<(), Rejection>, + task_spawner: TaskSpawner, + chain: Arc>| { task_spawner.blocking_json_task(Priority::P1, move || { + not_synced_filter?; chain.store_migrator.process_reconstruction(); Ok("success") }) @@ -4706,9 +4614,9 @@ pub fn serve( } /// Publish a message to the libp2p pubsub network. -fn publish_pubsub_message( - network_tx: &UnboundedSender>, - message: PubsubMessage, +fn publish_pubsub_message( + network_tx: &UnboundedSender>, + message: PubsubMessage, ) -> Result<(), warp::Rejection> { publish_network_message( network_tx, @@ -4719,17 +4627,17 @@ fn publish_pubsub_message( } /// Publish a message to the libp2p pubsub network. -fn publish_pubsub_messages( - network_tx: &UnboundedSender>, - messages: Vec>, +fn publish_pubsub_messages( + network_tx: &UnboundedSender>, + messages: Vec>, ) -> Result<(), warp::Rejection> { publish_network_message(network_tx, NetworkMessage::Publish { messages }) } /// Publish a message to the libp2p network. -fn publish_network_message( - network_tx: &UnboundedSender>, - message: NetworkMessage, +fn publish_network_message( + network_tx: &UnboundedSender>, + message: NetworkMessage, ) -> Result<(), warp::Rejection> { network_tx.send(message).map_err(|e| { warp_utils::reject::custom_server_error(format!( diff --git a/beacon_node/http_api/src/produce_block.rs b/beacon_node/http_api/src/produce_block.rs index 0da3bdc7aa..ed30da7362 100644 --- a/beacon_node/http_api/src/produce_block.rs +++ b/beacon_node/http_api/src/produce_block.rs @@ -20,6 +20,10 @@ use warp::{ Reply, }; +/// If default boost factor is provided in validator/blocks v3 request, we will skip the calculation +/// to keep the precision. +const DEFAULT_BOOST_FACTOR: u64 = 100; + pub fn get_randao_verification( query: &api_types::ValidatorBlocksQuery, randao_reveal_infinity: bool, @@ -52,6 +56,11 @@ pub async fn produce_block_v3( })?; let randao_verification = get_randao_verification(&query, randao_reveal.is_infinity())?; + let builder_boost_factor = if query.builder_boost_factor == Some(DEFAULT_BOOST_FACTOR) { + None + } else { + query.builder_boost_factor + }; let block_response_type = chain .produce_block_with_verification( @@ -59,7 +68,7 @@ pub async fn produce_block_v3( slot, query.graffiti, randao_verification, - query.builder_boost_factor, + builder_boost_factor, BlockProductionVersion::V3, ) .await diff --git a/beacon_node/http_api/src/publish_attestations.rs b/beacon_node/http_api/src/publish_attestations.rs new file mode 100644 index 0000000000..ed7f1ed17c --- /dev/null +++ b/beacon_node/http_api/src/publish_attestations.rs @@ -0,0 +1,319 @@ +//! Import attestations and publish them to the network. +//! +//! This module gracefully handles attestations to unknown blocks by requeuing them and then +//! efficiently waiting for them to finish reprocessing (using an async yield). +//! +//! The following comments relate to the handling of duplicate attestations (relocated here during +//! refactoring): +//! +//! Skip to the next attestation since an attestation for this +//! validator is already known in this epoch. +//! +//! There's little value for the network in validating a second +//! attestation for another validator since it is either: +//! +//! 1. A duplicate. +//! 2. Slashable. +//! 3. Invalid. +//! +//! We are likely to get duplicates in the case where a VC is using +//! fallback BNs. If the first BN actually publishes some/all of a +//! batch of attestations but fails to respond in a timely fashion, +//! the VC is likely to try publishing the attestations on another +//! BN. That second BN may have already seen the attestations from +//! the first BN and therefore indicate that the attestations are +//! "already seen". An attestation that has already been seen has +//! been published on the network so there's no actual error from +//! the perspective of the user. +//! +//! It's better to prevent slashable attestations from ever +//! appearing on the network than trying to slash validators, +//! especially those validators connected to the local API. +//! +//! There might be *some* value in determining that this attestation +//! is invalid, but since a valid attestation already it exists it +//! appears that this validator is capable of producing valid +//! attestations and there's no immediate cause for concern. +use crate::task_spawner::{Priority, TaskSpawner}; +use beacon_chain::{ + validator_monitor::timestamp_now, AttestationError, BeaconChain, BeaconChainError, + BeaconChainTypes, +}; +use beacon_processor::work_reprocessing_queue::{QueuedUnaggregate, ReprocessQueueMessage}; +use eth2::types::Failure; +use lighthouse_network::PubsubMessage; +use network::NetworkMessage; +use slog::{debug, error, warn, Logger}; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::{ + mpsc::{Sender, UnboundedSender}, + oneshot, +}; +use types::Attestation; + +// Error variants are only used in `Debug` and considered `dead_code` by the compiler. +#[derive(Debug)] +enum Error { + Validation(AttestationError), + Publication, + ForkChoice(#[allow(dead_code)] BeaconChainError), + AggregationPool(#[allow(dead_code)] AttestationError), + ReprocessDisabled, + ReprocessFull, + ReprocessTimeout, +} + +enum PublishAttestationResult { + Success, + AlreadyKnown, + Reprocessing(oneshot::Receiver>), + Failure(Error), +} + +fn verify_and_publish_attestation( + chain: &Arc>, + attestation: &Attestation, + seen_timestamp: Duration, + network_tx: &UnboundedSender>, + log: &Logger, +) -> Result<(), Error> { + let attestation = chain + .verify_unaggregated_attestation_for_gossip(attestation, None) + .map_err(Error::Validation)?; + + // Publish. + network_tx + .send(NetworkMessage::Publish { + messages: vec![PubsubMessage::Attestation(Box::new(( + attestation.subnet_id(), + attestation.attestation().clone(), + )))], + }) + .map_err(|_| Error::Publication)?; + + // Notify the validator monitor. + chain + .validator_monitor + .read() + .register_api_unaggregated_attestation( + seen_timestamp, + attestation.indexed_attestation(), + &chain.slot_clock, + ); + + let fc_result = chain.apply_attestation_to_fork_choice(&attestation); + let naive_aggregation_result = chain.add_to_naive_aggregation_pool(&attestation); + + if let Err(e) = &fc_result { + warn!( + log, + "Attestation invalid for fork choice"; + "err" => ?e, + ); + } + if let Err(e) = &naive_aggregation_result { + warn!( + log, + "Attestation invalid for aggregation"; + "err" => ?e + ); + } + + if let Err(e) = fc_result { + Err(Error::ForkChoice(e)) + } else if let Err(e) = naive_aggregation_result { + Err(Error::AggregationPool(e)) + } else { + Ok(()) + } +} + +pub async fn publish_attestations( + task_spawner: TaskSpawner, + chain: Arc>, + attestations: Vec>, + network_tx: UnboundedSender>, + reprocess_send: Option>, + log: Logger, +) -> Result<(), warp::Rejection> { + // Collect metadata about attestations which we'll use to report failures. We need to + // move the `attestations` vec into the blocking task, so this small overhead is unavoidable. + let attestation_metadata = attestations + .iter() + .map(|att| (att.data.slot, att.data.index)) + .collect::>(); + + // Gossip validate and publish attestations that can be immediately processed. + let seen_timestamp = timestamp_now(); + let inner_log = log.clone(); + let mut prelim_results = task_spawner + .blocking_task(Priority::P0, move || { + Ok(attestations + .into_iter() + .map(|attestation| { + match verify_and_publish_attestation( + &chain, + &attestation, + seen_timestamp, + &network_tx, + &inner_log, + ) { + Ok(()) => PublishAttestationResult::Success, + Err(Error::Validation(AttestationError::UnknownHeadBlock { + beacon_block_root, + })) => { + let Some(reprocess_tx) = &reprocess_send else { + return PublishAttestationResult::Failure(Error::ReprocessDisabled); + }; + // Re-process. + let (tx, rx) = oneshot::channel(); + let reprocess_chain = chain.clone(); + let reprocess_network_tx = network_tx.clone(); + let reprocess_log = inner_log.clone(); + let reprocess_fn = move || { + let result = verify_and_publish_attestation( + &reprocess_chain, + &attestation, + seen_timestamp, + &reprocess_network_tx, + &reprocess_log, + ); + // Ignore failure on the oneshot that reports the result. This + // shouldn't happen unless some catastrophe befalls the waiting + // thread which causes it to drop. + let _ = tx.send(result); + }; + let reprocess_msg = + ReprocessQueueMessage::UnknownBlockUnaggregate(QueuedUnaggregate { + beacon_block_root, + process_fn: Box::new(reprocess_fn), + }); + if reprocess_tx.try_send(reprocess_msg).is_err() { + PublishAttestationResult::Failure(Error::ReprocessFull) + } else { + PublishAttestationResult::Reprocessing(rx) + } + } + Err(Error::Validation(AttestationError::PriorAttestationKnown { + .. + })) => PublishAttestationResult::AlreadyKnown, + Err(e) => PublishAttestationResult::Failure(e), + } + }) + .map(Some) + .collect::>()) + }) + .await?; + + // Asynchronously wait for re-processing of attestations to unknown blocks. This avoids blocking + // any of the beacon processor workers while we wait for reprocessing. + let (reprocess_indices, reprocess_futures): (Vec<_>, Vec<_>) = prelim_results + .iter_mut() + .enumerate() + .filter_map(|(i, opt_result)| { + if let Some(PublishAttestationResult::Reprocessing(..)) = &opt_result { + let PublishAttestationResult::Reprocessing(rx) = opt_result.take()? else { + // Unreachable. + return None; + }; + Some((i, rx)) + } else { + None + } + }) + .unzip(); + let reprocess_results = futures::future::join_all(reprocess_futures).await; + + // Join everything back together and construct a response. + // This part should be quick so we just stay in the Tokio executor's async task. + for (i, reprocess_result) in reprocess_indices.into_iter().zip(reprocess_results) { + let Some(result_entry) = prelim_results.get_mut(i) else { + error!( + log, + "Unreachable case in attestation publishing"; + "case" => "prelim out of bounds", + "request_index" => i, + ); + continue; + }; + *result_entry = Some(match reprocess_result { + Ok(Ok(())) => PublishAttestationResult::Success, + // Attestation failed processing on re-process. + Ok(Err(Error::Validation(AttestationError::PriorAttestationKnown { .. }))) => { + PublishAttestationResult::AlreadyKnown + } + Ok(Err(e)) => PublishAttestationResult::Failure(e), + // Oneshot was dropped, indicating that the attestation either timed out in the + // reprocess queue or was dropped due to some error. + Err(_) => PublishAttestationResult::Failure(Error::ReprocessTimeout), + }); + } + + // Construct the response. + let mut failures = vec![]; + let mut num_already_known = 0; + + for (index, result) in prelim_results.iter().enumerate() { + match result { + Some(PublishAttestationResult::Success) => {} + Some(PublishAttestationResult::AlreadyKnown) => num_already_known += 1, + Some(PublishAttestationResult::Failure(e)) => { + if let Some((slot, committee_index)) = attestation_metadata.get(index) { + error!( + log, + "Failure verifying attestation for gossip"; + "error" => ?e, + "request_index" => index, + "committee_index" => committee_index, + "attestation_slot" => slot, + ); + failures.push(Failure::new(index, format!("{e:?}"))); + } else { + error!( + log, + "Unreachable case in attestation publishing"; + "case" => "out of bounds", + "request_index" => index + ); + failures.push(Failure::new(index, "metadata logic error".into())); + } + } + Some(PublishAttestationResult::Reprocessing(_)) => { + error!( + log, + "Unreachable case in attestation publishing"; + "case" => "reprocessing", + "request_index" => index + ); + failures.push(Failure::new(index, "reprocess logic error".into())); + } + None => { + error!( + log, + "Unreachable case in attestation publishing"; + "case" => "result is None", + "request_index" => index + ); + failures.push(Failure::new(index, "result logic error".into())); + } + } + } + + if num_already_known > 0 { + debug!( + log, + "Some unagg attestations already known"; + "count" => num_already_known + ); + } + + if failures.is_empty() { + Ok(()) + } else { + Err(warp_utils::reject::indexed_bad_request( + "error processing attestations".to_string(), + failures, + )) + } +} diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 8b85c2ac95..52e77753ce 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -82,11 +82,11 @@ pub async fn publish_block { - crate::publish_pubsub_message(&sender, PubsubMessage::BeaconBlock(block.clone())) + crate::publish_pubsub_message(&sender, PubsubMessage::BeaconBlock(block)) .map_err(|_| BlockError::BeaconChainError(BeaconChainError::UnableToPublish))?; } - SignedBeaconBlock::Deneb(_) => { - let mut pubsub_messages = vec![PubsubMessage::BeaconBlock(block.clone())]; + SignedBeaconBlock::Deneb(_) | SignedBeaconBlock::Electra(_) => { + let mut pubsub_messages = vec![PubsubMessage::BeaconBlock(block)]; if let Some(blob_sidecars) = blobs_opt { for (blob_index, blob) in blob_sidecars.into_iter().enumerate() { pubsub_messages.push(PubsubMessage::BlobSidecar(Box::new(( @@ -113,7 +113,7 @@ pub async fn publish_block b, - Err(BlockContentsError::BlockError(BlockError::BlockIsAlreadyKnown)) + Err(BlockContentsError::BlockError(BlockError::BlockIsAlreadyKnown(_))) | Err(BlockContentsError::BlobError( beacon_chain::blob_verification::GossipBlobError::RepeatBlob { .. }, )) => { @@ -133,7 +133,7 @@ pub async fn publish_block slot, - "error" => ?e + "error" => %e ); return Err(warp_utils::reject::custom_bad_request(e.to_string())); } diff --git a/beacon_node/http_api/src/sync_committees.rs b/beacon_node/http_api/src/sync_committees.rs index 8b0c7dc0ef..3e5b1dc524 100644 --- a/beacon_node/http_api/src/sync_committees.rs +++ b/beacon_node/http_api/src/sync_committees.rs @@ -45,7 +45,12 @@ pub fn sync_committee_duties( // the vast majority of requests. Rather than checking if we think the request will succeed in a // way prone to data races, we attempt the request immediately and check the error code. match chain.sync_committee_duties_from_head(request_epoch, request_indices) { - Ok(duties) => return Ok(convert_to_response(duties, execution_optimistic)), + Ok(duties) => { + return Ok(convert_to_response( + verify_unknown_validators(duties, request_epoch, chain)?, + execution_optimistic, + )) + } Err(BeaconChainError::SyncDutiesError(BeaconStateError::SyncCommitteeNotKnown { .. })) @@ -64,7 +69,10 @@ pub fn sync_committee_duties( )), e => warp_utils::reject::beacon_chain_error(e), })?; - Ok(convert_to_response(duties, execution_optimistic)) + Ok(convert_to_response( + verify_unknown_validators(duties, request_epoch, chain)?, + execution_optimistic, + )) } /// Slow path for duties: load a state and use it to compute the duties. @@ -73,7 +81,7 @@ fn duties_from_state_load( request_indices: &[u64], altair_fork_epoch: Epoch, chain: &BeaconChain, -) -> Result>, BeaconChainError> { +) -> Result, BeaconStateError>>, BeaconChainError> { // Determine what the current epoch would be if we fast-forward our system clock by // `MAXIMUM_GOSSIP_CLOCK_DISPARITY`. // @@ -121,6 +129,45 @@ fn duties_from_state_load( } } +fn verify_unknown_validators( + duties: Vec, BeaconStateError>>, + request_epoch: Epoch, + chain: &BeaconChain, +) -> Result>, warp::reject::Rejection> { + // Lazily load the request_epoch_state, as it is only needed if there are any UnknownValidator + let mut request_epoch_state = None; + + duties + .into_iter() + .map(|res| { + res.or_else(|err| { + // Make sure the validator is really unknown w.r.t. the request_epoch + if let BeaconStateError::UnknownValidator(idx) = err { + let request_epoch_state = match &mut request_epoch_state { + Some(state) => state, + None => request_epoch_state.insert(chain.state_at_slot( + request_epoch.start_slot(T::EthSpec::slots_per_epoch()), + StateSkipConfig::WithoutStateRoots, + )?), + }; + request_epoch_state + .get_validator(idx) + .map_err(BeaconChainError::SyncDutiesError) + .map(|_| None) + } else { + Err(BeaconChainError::SyncDutiesError(err)) + } + }) + }) + .collect::, _>>() + .map_err(|err| match err { + BeaconChainError::SyncDutiesError(BeaconStateError::UnknownValidator(idx)) => { + warp_utils::reject::custom_bad_request(format!("invalid validator index: {idx}")) + } + e => warp_utils::reject::beacon_chain_error(e), + }) +} + fn convert_to_response(duties: Vec>, execution_optimistic: bool) -> SyncDuties { api_types::GenericResponse::from(duties.into_iter().flatten().collect::>()) .add_execution_optimistic(execution_optimistic) diff --git a/beacon_node/http_api/src/task_spawner.rs b/beacon_node/http_api/src/task_spawner.rs index 8768e057da..cfee5e01ca 100644 --- a/beacon_node/http_api/src/task_spawner.rs +++ b/beacon_node/http_api/src/task_spawner.rs @@ -60,11 +60,15 @@ impl TaskSpawner { } } - /// Executes a "blocking" (non-async) task which returns a `Response`. - pub async fn blocking_response_task(self, priority: Priority, func: F) -> Response + /// Executes a "blocking" (non-async) task which returns an arbitrary value. + pub async fn blocking_task( + self, + priority: Priority, + func: F, + ) -> Result where F: FnOnce() -> Result + Send + Sync + 'static, - T: Reply + Send + 'static, + T: Send + 'static, { if let Some(beacon_processor_send) = &self.beacon_processor_send { // Create a closure that will execute `func` and send the result to @@ -79,22 +83,31 @@ impl TaskSpawner { }; // Send the function to the beacon processor for execution at some arbitrary time. - let result = send_to_beacon_processor( + send_to_beacon_processor( beacon_processor_send, priority, BlockingOrAsync::Blocking(Box::new(process_fn)), rx, ) .await - .and_then(|x| x); - convert_rejection(result).await + .and_then(|x| x) } 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 + warp_utils::task::blocking_task(func).await } } + /// 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, + { + let result = self.blocking_task(priority, func).await; + convert_rejection(result).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 diff --git a/beacon_node/http_api/src/test_utils.rs b/beacon_node/http_api/src/test_utils.rs index b87fdf6088..c1313168bc 100644 --- a/beacon_node/http_api/src/test_utils.rs +++ b/beacon_node/http_api/src/test_utils.rs @@ -35,6 +35,7 @@ pub const EXTERNAL_ADDR: &str = "/ip4/0.0.0.0/tcp/9000"; /// HTTP API tester that allows interaction with the underlying beacon chain harness. pub struct InteractiveTester { + pub ctx: Arc>>, pub harness: BeaconChainHarness>, pub client: BeaconNodeHttpClient, pub network_rx: NetworkReceivers, @@ -43,10 +44,11 @@ pub struct InteractiveTester { /// The result of calling `create_api_server`. /// /// Glue-type between `tests::ApiTester` and `InteractiveTester`. -pub struct ApiServer> { +pub struct ApiServer> { + pub ctx: Arc>, pub server: SFut, pub listening_socket: SocketAddr, - pub network_rx: NetworkReceivers, + pub network_rx: NetworkReceivers, pub local_enr: Enr, pub external_peer_id: PeerId, } @@ -90,6 +92,7 @@ impl InteractiveTester { let harness = harness_builder.build(); let ApiServer { + ctx, server, listening_socket, network_rx, @@ -114,6 +117,7 @@ impl InteractiveTester { ); Self { + ctx, harness, client, network_rx, @@ -125,7 +129,7 @@ pub async fn create_api_server( chain: Arc>, test_runtime: &TestRuntime, log: Logger, -) -> ApiServer> { +) -> ApiServer> { // Use port 0 to allocate a new unused port. let port = 0; @@ -187,6 +191,7 @@ pub async fn create_api_server( } = BeaconProcessorChannels::new(&beacon_processor_config); let beacon_processor_send = beacon_processor_tx; + let reprocess_send = work_reprocessing_tx.clone(); BeaconProcessor { network_globals: network_globals.clone(), executor: test_runtime.task_executor.clone(), @@ -216,14 +221,17 @@ pub async fn create_api_server( network_senders: Some(network_senders), network_globals: Some(network_globals), beacon_processor_send: Some(beacon_processor_send), + beacon_processor_reprocess_send: Some(reprocess_send), eth1_service: Some(eth1_service), sse_logging_components: None, log, }); - let (listening_socket, server) = crate::serve(ctx, test_runtime.task_executor.exit()).unwrap(); + let (listening_socket, server) = + crate::serve(ctx.clone(), test_runtime.task_executor.exit()).unwrap(); ApiServer { + ctx, server, listening_socket, network_rx: network_receivers, diff --git a/beacon_node/http_api/src/validator_inclusion.rs b/beacon_node/http_api/src/validator_inclusion.rs index f22ced1e69..dd4e137ce6 100644 --- a/beacon_node/http_api/src/validator_inclusion.rs +++ b/beacon_node/http_api/src/validator_inclusion.rs @@ -4,11 +4,8 @@ use eth2::{ lighthouse::{GlobalValidatorInclusionData, ValidatorInclusionData}, types::ValidatorId, }; -use state_processing::per_epoch_processing::{ - altair::participation_cache::Error as ParticipationCacheError, process_epoch, - EpochProcessingSummary, -}; -use types::{BeaconState, ChainSpec, Epoch, EthSpec}; +use state_processing::per_epoch_processing::{process_epoch, EpochProcessingSummary}; +use types::{BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec}; /// Returns the state in the last slot of `epoch`. fn end_of_epoch_state( @@ -27,15 +24,15 @@ fn end_of_epoch_state( /// ## Notes /// /// Will mutate `state`, transitioning it to the next epoch. -fn get_epoch_processing_summary( - state: &mut BeaconState, +fn get_epoch_processing_summary( + state: &mut BeaconState, spec: &ChainSpec, -) -> Result, warp::reject::Rejection> { +) -> Result, warp::reject::Rejection> { process_epoch(state, spec) .map_err(|e| warp_utils::reject::custom_server_error(format!("{:?}", e))) } -fn convert_cache_error(error: ParticipationCacheError) -> warp::reject::Rejection { +fn convert_cache_error(error: BeaconStateError) -> warp::reject::Rejection { warp_utils::reject::custom_server_error(format!("{:?}", error)) } @@ -50,7 +47,6 @@ pub fn global_validator_inclusion_data( Ok(GlobalValidatorInclusionData { current_epoch_active_gwei: summary.current_epoch_total_active_balance(), - previous_epoch_active_gwei: summary.previous_epoch_total_active_balance(), current_epoch_target_attesting_gwei: summary .current_epoch_target_attesting_balance() .map_err(convert_cache_error)?, diff --git a/beacon_node/http_api/src/validators.rs b/beacon_node/http_api/src/validators.rs index 20af7a680d..93e63953ef 100644 --- a/beacon_node/http_api/src/validators.rs +++ b/beacon_node/http_api/src/validators.rs @@ -4,7 +4,7 @@ use eth2::types::{ self as api_types, ExecutionOptimisticFinalizedResponse, ValidatorBalanceData, ValidatorData, ValidatorId, ValidatorStatus, }; -use std::sync::Arc; +use std::{collections::HashSet, sync::Arc}; pub fn get_beacon_state_validators( state_id: StateId, @@ -18,6 +18,8 @@ pub fn get_beacon_state_validators( |state, execution_optimistic, finalized| { let epoch = state.current_epoch(); let far_future_epoch = chain.spec.far_future_epoch; + let ids_filter_set: Option> = + query_ids.as_ref().map(HashSet::from_iter); Ok(( state @@ -27,13 +29,9 @@ pub fn get_beacon_state_validators( .enumerate() // filter by validator id(s) if provided .filter(|(index, (validator, _))| { - query_ids.as_ref().map_or(true, |ids| { - ids.iter().any(|id| match id { - ValidatorId::PublicKey(pubkey) => &validator.pubkey == pubkey, - ValidatorId::Index(param_index) => { - *param_index == *index as u64 - } - }) + ids_filter_set.as_ref().map_or(true, |ids_set| { + ids_set.contains(&ValidatorId::PublicKey(validator.pubkey)) + || ids_set.contains(&ValidatorId::Index(*index as u64)) }) }) // filter by status(es) if provided and map the result @@ -83,6 +81,9 @@ pub fn get_beacon_state_validator_balances( .map_state_and_execution_optimistic_and_finalized( &chain, |state, execution_optimistic, finalized| { + let ids_filter_set: Option> = + optional_ids.map(|f| HashSet::from_iter(f.iter())); + Ok(( state .validators() @@ -91,13 +92,9 @@ pub fn get_beacon_state_validator_balances( .enumerate() // filter by validator id(s) if provided .filter(|(index, (validator, _))| { - optional_ids.map_or(true, |ids| { - ids.iter().any(|id| match id { - ValidatorId::PublicKey(pubkey) => &validator.pubkey == pubkey, - ValidatorId::Index(param_index) => { - *param_index == *index as u64 - } - }) + ids_filter_set.as_ref().map_or(true, |ids_set| { + ids_set.contains(&ValidatorId::PublicKey(validator.pubkey)) + || ids_set.contains(&ValidatorId::Index(*index as u64)) }) }) .map(|(index, (_, balance))| ValidatorBalanceData { diff --git a/beacon_node/http_api/tests/interactive_tests.rs b/beacon_node/http_api/tests/interactive_tests.rs index 6fb197b41a..529dc852e9 100644 --- a/beacon_node/http_api/tests/interactive_tests.rs +++ b/beacon_node/http_api/tests/interactive_tests.rs @@ -4,6 +4,7 @@ use beacon_chain::{ test_utils::{AttestationStrategy, BlockStrategy, SyncCommitteeStrategy}, ChainConfig, }; +use beacon_processor::work_reprocessing_queue::ReprocessQueueMessage; use eth2::types::ProduceBlockV3Response; use eth2::types::{DepositContractData, StateId}; use execution_layer::{ForkchoiceState, PayloadAttributes}; @@ -418,7 +419,7 @@ pub async fn proposer_boost_re_org_test( None, Some(Box::new(move |builder| { builder - .proposer_re_org_threshold(Some(ReOrgThreshold(re_org_threshold))) + .proposer_re_org_head_threshold(Some(ReOrgThreshold(re_org_threshold))) .proposer_re_org_max_epochs_since_finalization(Epoch::new( max_epochs_since_finalization, )) @@ -840,3 +841,78 @@ pub async fn fork_choice_before_proposal() { // D's parent is B. assert_eq!(block_d.parent_root(), block_root_b.into()); } + +// Test that attestations to unknown blocks are requeued and processed when their block arrives. +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn queue_attestations_from_http() { + let validator_count = 128; + let all_validators = (0..validator_count).collect::>(); + + let tester = InteractiveTester::::new(None, validator_count).await; + let harness = &tester.harness; + let client = tester.client.clone(); + + let num_initial = 5; + + // Slot of the block attested to. + let attestation_slot = Slot::new(num_initial) + 1; + + // Make some initial blocks. + harness.advance_slot(); + harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + harness.advance_slot(); + assert_eq!(harness.get_current_slot(), attestation_slot); + + // Make the attested-to block without applying it. + let pre_state = harness.get_current_state(); + let (block, post_state) = harness.make_block(pre_state, attestation_slot).await; + let block_root = block.0.canonical_root(); + + // Make attestations to the block and POST them to the beacon node on a background thread. + let attestations = harness + .make_unaggregated_attestations( + &all_validators, + &post_state, + block.0.state_root(), + block_root.into(), + attestation_slot, + ) + .into_iter() + .flat_map(|attestations| attestations.into_iter().map(|(att, _subnet)| att)) + .collect::>(); + + let attestation_future = tokio::spawn(async move { + client + .post_beacon_pool_attestations(&attestations) + .await + .expect("attestations should be processed successfully") + }); + + // In parallel, apply the block. We need to manually notify the reprocess queue, because the + // `beacon_chain` does not know about the queue and will not update it for us. + let parent_root = block.0.parent_root(); + harness + .process_block(attestation_slot, block_root, block) + .await + .unwrap(); + tester + .ctx + .beacon_processor_reprocess_send + .as_ref() + .unwrap() + .send(ReprocessQueueMessage::BlockImported { + block_root, + parent_root, + }) + .await + .unwrap(); + + attestation_future.await.unwrap(); +} diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 2d946f3092..e4580e4ffd 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -248,6 +248,7 @@ impl ApiTester { let log = null_logger().unwrap(); let ApiServer { + ctx: _, server, listening_socket, network_rx, @@ -341,6 +342,7 @@ impl ApiTester { let log = null_logger().unwrap(); let ApiServer { + ctx: _, server, listening_socket, network_rx, @@ -626,7 +628,7 @@ impl ApiTester { self } - pub async fn test_beacon_blocks_finalized(self) -> Self { + pub async fn test_beacon_blocks_finalized(self) -> Self { for block_id in self.interesting_block_ids() { let block_root = block_id.root(&self.chain); let block = block_id.full_block(&self.chain).await; @@ -663,7 +665,7 @@ impl ApiTester { self } - pub async fn test_beacon_blinded_blocks_finalized(self) -> Self { + pub async fn test_beacon_blinded_blocks_finalized(self) -> Self { for block_id in self.interesting_block_ids() { let block_root = block_id.root(&self.chain); let block = block_id.full_block(&self.chain).await; @@ -1715,7 +1717,7 @@ impl ApiTester { }; let expected = block.slot(); - assert_eq!(result.header.beacon.slot, expected); + assert_eq!(result.get_slot(), expected); self } @@ -1929,9 +1931,9 @@ impl ApiTester { pub async fn test_get_config_spec(self) -> Self { let result = self .client - .get_config_spec::() + .get_config_spec::() .await - .map(|res| ConfigAndPreset::Deneb(res.data)) + .map(|res| ConfigAndPreset::Electra(res.data)) .unwrap(); let expected = ConfigAndPreset::from_chain_spec::(&self.chain.spec, None); @@ -2719,6 +2721,31 @@ impl ApiTester { self } + /// Check that the metadata from the headers & JSON response body are consistent, and that the + /// consensus block value is non-zero. + fn check_block_v3_metadata( + metadata: &ProduceBlockV3Metadata, + response: &JsonProduceBlockV3Response, + ) { + // Compare fork name to ForkVersionedResponse rather than metadata consensus_version, which + // is deserialized to a dummy value. + assert_eq!(Some(metadata.consensus_version), response.version); + assert_eq!(ForkName::Base, response.metadata.consensus_version); + assert_eq!( + metadata.execution_payload_blinded, + response.metadata.execution_payload_blinded + ); + assert_eq!( + metadata.execution_payload_value, + response.metadata.execution_payload_value + ); + assert_eq!( + metadata.consensus_block_value, + response.metadata.consensus_block_value + ); + assert!(!metadata.consensus_block_value.is_zero()); + } + pub async fn test_block_production_v3_ssz(self) -> Self { let fork = self.chain.canonical_head.cached_head().head_fork(); let genesis_validators_root = self.chain.genesis_validators_root; @@ -3580,11 +3607,12 @@ impl ApiTester { let (proposer_index, randao_reveal) = self.get_test_randao(slot, epoch).await; - let (payload_type, _) = self + let (payload_type, metadata) = self .client .get_validator_blocks_v3::(slot, &randao_reveal, None, None) .await .unwrap(); + Self::check_block_v3_metadata(&metadata, &payload_type); let payload: BlindedPayload = match payload_type.data { ProduceBlockV3Response::Blinded(payload) => { @@ -3606,11 +3634,12 @@ impl ApiTester { let (proposer_index, randao_reveal) = self.get_test_randao(slot, epoch).await; - let (payload_type, _) = self + let (payload_type, metadata) = self .client .get_validator_blocks_v3::(slot, &randao_reveal, None, Some(0)) .await .unwrap(); + Self::check_block_v3_metadata(&metadata, &payload_type); let payload: FullPayload = match payload_type.data { ProduceBlockV3Response::Full(payload) => { @@ -3632,11 +3661,12 @@ impl ApiTester { let (proposer_index, randao_reveal) = self.get_test_randao(slot, epoch).await; - let (payload_type, _) = self + let (payload_type, metadata) = self .client .get_validator_blocks_v3::(slot, &randao_reveal, None, Some(u64::MAX)) .await .unwrap(); + Self::check_block_v3_metadata(&metadata, &payload_type); let payload: BlindedPayload = match payload_type.data { ProduceBlockV3Response::Blinded(payload) => { @@ -3736,11 +3766,12 @@ impl ApiTester { let (proposer_index, randao_reveal) = self.get_test_randao(slot, epoch).await; - let (payload_type, _) = self + let (payload_type, metadata) = self .client .get_validator_blocks_v3::(slot, &randao_reveal, None, None) .await .unwrap(); + Self::check_block_v3_metadata(&metadata, &payload_type); let payload: BlindedPayload = match payload_type.data { ProduceBlockV3Response::Blinded(payload) => { @@ -3812,11 +3843,12 @@ impl ApiTester { let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; - let (payload_type, _) = self + let (payload_type, metadata) = self .client .get_validator_blocks_v3::(slot, &randao_reveal, None, None) .await .unwrap(); + Self::check_block_v3_metadata(&metadata, &payload_type); let payload: BlindedPayload = match payload_type.data { ProduceBlockV3Response::Blinded(payload) => { @@ -3902,11 +3934,12 @@ impl ApiTester { let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; - let (payload_type, _) = self + let (payload_type, metadata) = self .client .get_validator_blocks_v3::(slot, &randao_reveal, None, None) .await .unwrap(); + Self::check_block_v3_metadata(&metadata, &payload_type); let payload: FullPayload = match payload_type.data { ProduceBlockV3Response::Full(payload) => { @@ -3988,11 +4021,12 @@ impl ApiTester { .unwrap(); let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; - let (payload_type, _) = self + let (payload_type, metadata) = self .client .get_validator_blocks_v3::(slot, &randao_reveal, None, None) .await .unwrap(); + Self::check_block_v3_metadata(&metadata, &payload_type); let payload: FullPayload = match payload_type.data { ProduceBlockV3Response::Full(payload) => { @@ -4074,11 +4108,12 @@ impl ApiTester { let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; - let (payload_type, _) = self + let (payload_type, metadata) = self .client .get_validator_blocks_v3::(slot, &randao_reveal, None, None) .await .unwrap(); + Self::check_block_v3_metadata(&metadata, &payload_type); let payload: FullPayload = match payload_type.data { ProduceBlockV3Response::Full(payload) => { @@ -4158,11 +4193,12 @@ impl ApiTester { let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; - let (payload_type, _) = self + let (payload_type, metadata) = self .client .get_validator_blocks_v3::(slot, &randao_reveal, None, None) .await .unwrap(); + Self::check_block_v3_metadata(&metadata, &payload_type); let payload: FullPayload = match payload_type.data { ProduceBlockV3Response::Full(payload) => { @@ -4214,11 +4250,12 @@ impl ApiTester { let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; - let (payload_type, _) = self + let (payload_type, metadata) = self .client .get_validator_blocks_v3::(slot, &randao_reveal, None, None) .await .unwrap(); + Self::check_block_v3_metadata(&metadata, &payload_type); match payload_type.data { ProduceBlockV3Response::Full(_) => (), @@ -4280,11 +4317,12 @@ impl ApiTester { let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; - let (payload_type, _) = self + let (payload_type, metadata) = self .client .get_validator_blocks_v3::(slot, &randao_reveal, None, None) .await .unwrap(); + Self::check_block_v3_metadata(&metadata, &payload_type); match payload_type.data { ProduceBlockV3Response::Full(_) => (), @@ -4388,11 +4426,12 @@ impl ApiTester { .get_test_randao(next_slot, next_slot.epoch(E::slots_per_epoch())) .await; - let (payload_type, _) = self + let (payload_type, metadata) = self .client .get_validator_blocks_v3::(next_slot, &randao_reveal, None, None) .await .unwrap(); + Self::check_block_v3_metadata(&metadata, &payload_type); match payload_type.data { ProduceBlockV3Response::Blinded(_) => (), @@ -4408,11 +4447,12 @@ impl ApiTester { .get_test_randao(next_slot, next_slot.epoch(E::slots_per_epoch())) .await; - let (payload_type, _) = self + let (payload_type, metadata) = self .client .get_validator_blocks_v3::(next_slot, &randao_reveal, None, None) .await .unwrap(); + Self::check_block_v3_metadata(&metadata, &payload_type); match payload_type.data { ProduceBlockV3Response::Full(_) => (), @@ -4536,11 +4576,12 @@ impl ApiTester { .get_test_randao(next_slot, next_slot.epoch(E::slots_per_epoch())) .await; - let (payload_type, _) = self + let (payload_type, metadata) = self .client .get_validator_blocks_v3::(next_slot, &randao_reveal, None, None) .await .unwrap(); + Self::check_block_v3_metadata(&metadata, &payload_type); match payload_type.data { ProduceBlockV3Response::Full(_) => (), @@ -4566,11 +4607,12 @@ impl ApiTester { .get_test_randao(next_slot, next_slot.epoch(E::slots_per_epoch())) .await; - let (payload_type, _) = self + let (payload_type, metadata) = self .client .get_validator_blocks_v3::(next_slot, &randao_reveal, None, None) .await .unwrap(); + Self::check_block_v3_metadata(&metadata, &payload_type); match payload_type.data { ProduceBlockV3Response::Blinded(_) => (), @@ -4646,11 +4688,12 @@ impl ApiTester { let (proposer_index, randao_reveal) = self.get_test_randao(slot, epoch).await; - let (payload_type, _) = self + let (payload_type, metadata) = self .client .get_validator_blocks_v3::(slot, &randao_reveal, None, None) .await .unwrap(); + Self::check_block_v3_metadata(&metadata, &payload_type); let payload: FullPayload = match payload_type.data { ProduceBlockV3Response::Full(payload) => { @@ -4715,11 +4758,12 @@ impl ApiTester { let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; - let (payload_type, _) = self + let (payload_type, metadata) = self .client .get_validator_blocks_v3::(slot, &randao_reveal, None, None) .await .unwrap(); + Self::check_block_v3_metadata(&metadata, &payload_type); match payload_type.data { ProduceBlockV3Response::Blinded(_) => (), @@ -4779,11 +4823,12 @@ impl ApiTester { let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; - let (payload_type, _) = self + let (payload_type, metadata) = self .client .get_validator_blocks_v3::(slot, &randao_reveal, None, None) .await .unwrap(); + Self::check_block_v3_metadata(&metadata, &payload_type); match payload_type.data { ProduceBlockV3Response::Full(_) => (), @@ -4843,11 +4888,12 @@ impl ApiTester { let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; - let (payload_type, _) = self + let (payload_type, metadata) = self .client .get_validator_blocks_v3::(slot, &randao_reveal, None, None) .await .unwrap(); + Self::check_block_v3_metadata(&metadata, &payload_type); match payload_type.data { ProduceBlockV3Response::Full(_) => (), @@ -4905,11 +4951,12 @@ impl ApiTester { let epoch = self.chain.epoch().unwrap(); let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; - let (payload_type, _) = self + let (payload_type, metadata) = self .client .get_validator_blocks_v3::(slot, &randao_reveal, None, None) .await .unwrap(); + Self::check_block_v3_metadata(&metadata, &payload_type); let _block_contents = match payload_type.data { ProduceBlockV3Response::Blinded(payload) => payload, @@ -4977,11 +5024,12 @@ impl ApiTester { let epoch = self.chain.epoch().unwrap(); let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; - let (payload_type, _) = self + let (payload_type, metadata) = self .client .get_validator_blocks_v3::(slot, &randao_reveal, None, None) .await .unwrap(); + Self::check_block_v3_metadata(&metadata, &payload_type); match payload_type.data { ProduceBlockV3Response::Full(_) => (), @@ -5480,11 +5528,11 @@ impl ApiTester { } } -async fn poll_events, eth2::Error>> + Unpin, T: EthSpec>( +async fn poll_events, eth2::Error>> + Unpin, E: EthSpec>( stream: &mut S, num_events: usize, timeout: Duration, -) -> Vec> { +) -> Vec> { let mut events = Vec::new(); let collect_stream_fut = async { diff --git a/beacon_node/lighthouse_network/Cargo.toml b/beacon_node/lighthouse_network/Cargo.toml index 46acdeaded..1617c0bd6c 100644 --- a/beacon_node/lighthouse_network/Cargo.toml +++ b/beacon_node/lighthouse_network/Cargo.toml @@ -6,6 +6,7 @@ edition = { workspace = true } [dependencies] discv5 = { workspace = true } +gossipsub = { workspace = true } unsigned-varint = { version = "0.6", features = ["codec"] } ssz_types = { workspace = true } types = { workspace = true } @@ -42,22 +43,32 @@ superstruct = { workspace = true } prometheus-client = "0.22.0" unused_port = { workspace = true } delay_map = { workspace = true } -void = "1" -libp2p-mplex = { git = "https://github.com/sigp/rust-libp2p/", rev = "cfa3275ca17e502799ed56e555b6c0611752e369" } +tracing = { workspace = true } +byteorder = { workspace = true } +bytes = { workspace = true } +either = { workspace = true } + +# Local dependencies +futures-ticker = "0.0.3" +getrandom = "0.2.11" +hex_fmt = "0.3.0" +instant = "0.1.12" +void = "1.0.2" +base64 = "0.21.5" +libp2p-mplex = "0.41" [dependencies.libp2p] -git = "https://github.com/sigp/rust-libp2p/" -rev = "cfa3275ca17e502799ed56e555b6c0611752e369" +version = "0.53" default-features = false -features = ["identify", "yamux", "noise", "gossipsub", "dns", "tcp", "tokio", "plaintext", "secp256k1", "macros", "ecdsa", "metrics", "quic"] +features = ["identify", "yamux", "noise", "dns", "tcp", "tokio", "plaintext", "secp256k1", "macros", "ecdsa", "metrics", "quic", "upnp"] [dev-dependencies] slog-term = { workspace = true } slog-async = { workspace = true } tempfile = { workspace = true } -exit-future = { workspace = true } quickcheck = { workspace = true } quickcheck_macros = { workspace = true } +async-channel = { workspace = true } [features] libp2p-websocket = [] diff --git a/beacon_node/lighthouse_network/gossipsub/CHANGELOG.md b/beacon_node/lighthouse_network/gossipsub/CHANGELOG.md new file mode 100644 index 0000000000..448e224cb6 --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/CHANGELOG.md @@ -0,0 +1,378 @@ +## 0.5 Sigma Prime fork + +- Attempt to publish to at least mesh_n peers when publishing a message when flood publish is disabled. + See [PR 5357](https://github.com/sigp/lighthouse/pull/5357). +- Drop `Publish` and `Forward` gossipsub stale messages when polling ConnectionHandler. + See [PR 5175](https://github.com/sigp/lighthouse/pull/5175). +- Apply back pressure by setting a limit in the ConnectionHandler message queue. + See [PR 5066](https://github.com/sigp/lighthouse/pull/5066). + +## 0.46.1 + +- Deprecate `Rpc` in preparation for removing it from the public API because it is an internal type. + See [PR 4833](https://github.com/libp2p/rust-libp2p/pull/4833). + +## 0.46.0 + +- Remove `fast_message_id_fn` mechanism from `Config`. + See [PR 4285](https://github.com/libp2p/rust-libp2p/pull/4285). +- Remove deprecated `gossipsub::Config::idle_timeout` in favor of `SwarmBuilder::idle_connection_timeout`. + See [PR 4642](https://github.com/libp2p/rust-libp2p/pull/4642). +- Return typed error from config builder. + See [PR 4445](https://github.com/libp2p/rust-libp2p/pull/4445). +- Process outbound stream before inbound stream in `EnabledHandler::poll(..)`. + See [PR 4778](https://github.com/libp2p/rust-libp2p/pull/4778). + +## 0.45.2 + +- Deprecate `gossipsub::Config::idle_timeout` in favor of `SwarmBuilder::idle_connection_timeout`. + See [PR 4648]. + + + +[PR 4648]: (https://github.com/libp2p/rust-libp2p/pull/4648) + + + +## 0.45.1 + +- Add getter function to o btain `TopicScoreParams`. + See [PR 4231]. + +[PR 4231]: https://github.com/libp2p/rust-libp2p/pull/4231 + +## 0.45.0 + +- Raise MSRV to 1.65. + See [PR 3715]. +- Remove deprecated items. See [PR 3862]. + +[PR 3715]: https://github.com/libp2p/rust-libp2p/pull/3715 +[PR 3862]: https://github.com/libp2p/rust-libp2p/pull/3862 + +## 0.44.4 + +- Deprecate `metrics`, `protocol`, `subscription_filter`, `time_cache` modules to make them private. See [PR 3777]. +- Honor the `gossipsub::Config::support_floodsub` in all cases. + Previously, it was ignored when a custom protocol id was set via `gossipsub::Config::protocol_id`. + See [PR 3837]. + +[PR 3777]: https://github.com/libp2p/rust-libp2p/pull/3777 +[PR 3837]: https://github.com/libp2p/rust-libp2p/pull/3837 + +## 0.44.3 + +- Fix erroneously duplicate message IDs. See [PR 3716]. + +- Gracefully disable handler on stream errors. Deprecate a few variants of `HandlerError`. + See [PR 3625]. + +[PR 3716]: https://github.com/libp2p/rust-libp2p/pull/3716 +[PR 3625]: https://github.com/libp2p/rust-libp2p/pull/3325 + +## 0.44.2 + +- Signed messages now use sequential integers in the sequence number field. + See [PR 3551]. + +[PR 3551]: https://github.com/libp2p/rust-libp2p/pull/3551 + +## 0.44.1 + +- Migrate from `prost` to `quick-protobuf`. This removes `protoc` dependency. See [PR 3312]. + +[PR 3312]: https://github.com/libp2p/rust-libp2p/pull/3312 + +## 0.44.0 + +- Update to `prometheus-client` `v0.19.0`. See [PR 3207]. + +- Update to `libp2p-core` `v0.39.0`. + +- Update to `libp2p-swarm` `v0.42.0`. + +- Initialize `ProtocolConfig` via `GossipsubConfig`. See [PR 3381]. + +- Rename types as per [discussion 2174]. + `Gossipsub` has been renamed to `Behaviour`. + The `Gossipsub` prefix has been removed from various types like `GossipsubConfig` or `GossipsubMessage`. + It is preferred to import the gossipsub protocol as a module (`use libp2p::gossipsub;`), and refer to its types via `gossipsub::`. + For example: `gossipsub::Behaviour` or `gossipsub::RawMessage`. See [PR 3303]. + +[PR 3207]: https://github.com/libp2p/rust-libp2p/pull/3207/ +[PR 3303]: https://github.com/libp2p/rust-libp2p/pull/3303/ +[PR 3381]: https://github.com/libp2p/rust-libp2p/pull/3381/ +[discussion 2174]: https://github.com/libp2p/rust-libp2p/discussions/2174 + +## 0.43.0 + +- Update to `libp2p-core` `v0.38.0`. + +- Update to `libp2p-swarm` `v0.41.0`. + +- Update to `prost-codec` `v0.3.0`. + +- Refactoring GossipsubCodec to use common protobuf Codec. See [PR 3070]. + +- Replace `Gossipsub`'s `NetworkBehaviour` implementation `inject_*` methods with the new `on_*` methods. + See [PR 3011]. + +- Replace `GossipsubHandler`'s `ConnectionHandler` implementation `inject_*` methods with the new `on_*` methods. + See [PR 3085]. + +- Update `rust-version` to reflect the actual MSRV: 1.62.0. See [PR 3090]. + +[PR 3085]: https://github.com/libp2p/rust-libp2p/pull/3085 +[PR 3070]: https://github.com/libp2p/rust-libp2p/pull/3070 +[PR 3011]: https://github.com/libp2p/rust-libp2p/pull/3011 +[PR 3090]: https://github.com/libp2p/rust-libp2p/pull/3090 + +## 0.42.0 + +- Bump rand to 0.8 and quickcheck to 1. See [PR 2857]. + +- Update to `libp2p-core` `v0.37.0`. + +- Update to `libp2p-swarm` `v0.40.0`. + +[PR 2857]: https://github.com/libp2p/rust-libp2p/pull/2857 + +## 0.41.0 + +- Update to `libp2p-swarm` `v0.39.0`. + +- Update to `libp2p-core` `v0.36.0`. + +- Allow publishing with any `impl Into` as a topic. See [PR 2862]. + +[PR 2862]: https://github.com/libp2p/rust-libp2p/pull/2862 + +## 0.40.0 + +- Update prost requirement from 0.10 to 0.11 which no longer installs the protoc Protobuf compiler. + Thus you will need protoc installed locally. See [PR 2788]. + +- Update to `libp2p-swarm` `v0.38.0`. + +- Update to `libp2p-core` `v0.35.0`. + +- Update to `prometheus-client` `v0.18.0`. See [PR 2822]. + +[PR 2822]: https://github.com/libp2p/rust-libp2p/pull/2761/ +[PR 2788]: https://github.com/libp2p/rust-libp2p/pull/2788 + +## 0.39.0 + +- Update to `libp2p-core` `v0.34.0`. + +- Update to `libp2p-swarm` `v0.37.0`. + +- Allow for custom protocol ID via `GossipsubConfigBuilder::protocol_id()`. See [PR 2718]. + +[PR 2718]: https://github.com/libp2p/rust-libp2p/pull/2718/ + +## 0.38.1 + +- Fix duplicate connection id. See [PR 2702]. + +[PR 2702]: https://github.com/libp2p/rust-libp2p/pull/2702 + +## 0.38.0 + +- Update to `libp2p-core` `v0.33.0`. + +- Update to `libp2p-swarm` `v0.36.0`. + +- changed `TimeCache::contains_key` and `DuplicateCache::contains` to immutable methods. See [PR 2620]. + +- Update to `prometheus-client` `v0.16.0`. See [PR 2631]. + +[PR 2620]: https://github.com/libp2p/rust-libp2p/pull/2620 +[PR 2631]: https://github.com/libp2p/rust-libp2p/pull/2631 + +## 0.37.0 + +- Update to `libp2p-swarm` `v0.35.0`. + +- Fix gossipsub metric (see [PR 2558]). + +- Allow the user to set the buckets for the score histogram, and to adjust them from the score thresholds. See [PR 2595]. + +[PR 2558]: https://github.com/libp2p/rust-libp2p/pull/2558 +[PR 2595]: https://github.com/libp2p/rust-libp2p/pull/2595 + +## 0.36.0 [2022-02-22] + +- Update to `libp2p-core` `v0.32.0`. + +- Update to `libp2p-swarm` `v0.34.0`. + +- Move from `open-metrics-client` to `prometheus-client` (see [PR 2442]). + +- Emit gossip of all non empty topics (see [PR 2481]). + +- Merge NetworkBehaviour's inject_\* paired methods (see [PR 2445]). + +- Revert to wasm-timer (see [PR 2506]). + +- Do not overwrite msg's peers if put again into mcache (see [PR 2493]). + +[PR 2442]: https://github.com/libp2p/rust-libp2p/pull/2442 +[PR 2481]: https://github.com/libp2p/rust-libp2p/pull/2481 +[PR 2445]: https://github.com/libp2p/rust-libp2p/pull/2445 +[PR 2506]: https://github.com/libp2p/rust-libp2p/pull/2506 +[PR 2493]: https://github.com/libp2p/rust-libp2p/pull/2493 + +## 0.35.0 [2022-01-27] + +- Update dependencies. + +- Migrate to Rust edition 2021 (see [PR 2339]). + +- Add metrics for network and configuration performance analysis (see [PR 2346]). + +- Improve bandwidth performance by tracking IWANTs and reducing duplicate sends + (see [PR 2327]). + +- Implement `Serialize` and `Deserialize` for `MessageId` and `FastMessageId` (see [PR 2408]) + +- Fix `GossipsubConfigBuilder::build()` requiring `&self` to live for `'static` (see [PR 2409]) + +- Implement Unsubscribe backoff as per [libp2p specs PR 383] (see [PR 2403]). + +[PR 2346]: https://github.com/libp2p/rust-libp2p/pull/2346 +[PR 2339]: https://github.com/libp2p/rust-libp2p/pull/2339 +[PR 2327]: https://github.com/libp2p/rust-libp2p/pull/2327 +[PR 2408]: https://github.com/libp2p/rust-libp2p/pull/2408 +[PR 2409]: https://github.com/libp2p/rust-libp2p/pull/2409 +[PR 2403]: https://github.com/libp2p/rust-libp2p/pull/2403 +[libp2p specs PR 383]: https://github.com/libp2p/specs/pull/383 + +## 0.34.0 [2021-11-16] + +- Add topic and mesh metrics (see [PR 2316]). + +- Fix bug in internal peer's topics tracking (see [PR 2325]). + +- Use `instant` and `futures-timer` instead of `wasm-timer` (see [PR 2245]). + +- Update dependencies. + +[PR 2245]: https://github.com/libp2p/rust-libp2p/pull/2245 +[PR 2325]: https://github.com/libp2p/rust-libp2p/pull/2325 +[PR 2316]: https://github.com/libp2p/rust-libp2p/pull/2316 + +## 0.33.0 [2021-11-01] + +- Add an event to register peers that do not support the gossipsub protocol + [PR 2241](https://github.com/libp2p/rust-libp2p/pull/2241) + +- Make default features of `libp2p-core` optional. + [PR 2181](https://github.com/libp2p/rust-libp2p/pull/2181) + +- Improve internal peer tracking. + [PR 2175](https://github.com/libp2p/rust-libp2p/pull/2175) + +- Update dependencies. + +- Allow `message_id_fn`s to accept closures that capture variables. + [PR 2103](https://github.com/libp2p/rust-libp2p/pull/2103) + +- Implement std::error::Error for error types. + [PR 2254](https://github.com/libp2p/rust-libp2p/pull/2254) + +## 0.32.0 [2021-07-12] + +- Update dependencies. + +- Reduce log levels across the crate to lessen noisiness of libp2p-gossipsub (see [PR 2101]). + +[PR 2101]: https://github.com/libp2p/rust-libp2p/pull/2101 + +## 0.31.0 [2021-05-17] + +- Keep connections to peers in a mesh alive. Allow closing idle connections to peers not in a mesh + [PR-2043]. + +[PR-2043]: https://github.com/libp2p/rust-libp2p/pull/2043https://github.com/libp2p/rust-libp2p/pull/2043 + +## 0.30.1 [2021-04-27] + +- Remove `regex-filter` feature flag thus always enabling `regex::RegexSubscriptionFilter` [PR + 2056](https://github.com/libp2p/rust-libp2p/pull/2056). + +## 0.30.0 [2021-04-13] + +- Update `libp2p-swarm`. + +- Update dependencies. + +## 0.29.0 [2021-03-17] + +- Update `libp2p-swarm`. + +- Update dependencies. + +## 0.28.0 [2021-02-15] + +- Prevent non-published messages being added to caches. + [PR 1930](https://github.com/libp2p/rust-libp2p/pull/1930) + +- Update dependencies. + +## 0.27.0 [2021-01-12] + +- Update dependencies. + +- Implement Gossipsub v1.1 specification. + [PR 1720](https://github.com/libp2p/rust-libp2p/pull/1720) + +## 0.26.0 [2020-12-17] + +- Update `libp2p-swarm` and `libp2p-core`. + +## 0.25.0 [2020-11-25] + +- Update `libp2p-swarm` and `libp2p-core`. + +## 0.24.0 [2020-11-09] + +- Update dependencies. + +## 0.23.0 [2020-10-16] + +- Update dependencies. + +## 0.22.0 [2020-09-09] + +- Update `libp2p-swarm` and `libp2p-core`. + +## 0.21.0 [2020-08-18] + +- Add public API to list topics and peers. [PR 1677](https://github.com/libp2p/rust-libp2p/pull/1677). + +- Add message signing and extended privacy/validation configurations. [PR 1583](https://github.com/libp2p/rust-libp2p/pull/1583). + +- `Debug` instance for `Gossipsub`. [PR 1673](https://github.com/libp2p/rust-libp2p/pull/1673). + +- Bump `libp2p-core` and `libp2p-swarm` dependency. + +## 0.20.0 [2020-07-01] + +- Updated dependencies. + +## 0.19.3 [2020-06-23] + +- Maintenance release fixing linter warnings. + +## 0.19.2 [2020-06-22] + +- Updated dependencies. diff --git a/beacon_node/lighthouse_network/gossipsub/Cargo.toml b/beacon_node/lighthouse_network/gossipsub/Cargo.toml new file mode 100644 index 0000000000..871955c059 --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "gossipsub" +edition = "2021" +description = "Sigma prime's version of Gossipsub protocol for libp2p" +version = "0.5.0" +authors = ["Age Manning "] +license = "MIT" +repository = "https://github.com/sigp/lighthouse/" +keywords = ["peer-to-peer", "libp2p", "networking"] +categories = ["network-programming", "asynchronous"] + +[features] +wasm-bindgen = ["getrandom/js", "instant/wasm-bindgen"] + +[dependencies] +async-channel = { workspace = true } +asynchronous-codec = "0.7.0" +base64 = "0.21.7" +byteorder = "1.5.0" +bytes = "1.5" +either = "1.9" +fnv = "1.0.7" +futures = "0.3.30" +futures-ticker = "0.0.3" +futures-timer = "3.0.2" +getrandom = "0.2.12" +hex_fmt = "0.3.0" +instant = "0.1.12" +libp2p = { version = "0.53", default-features = false } +quick-protobuf = "0.8" +quick-protobuf-codec = "0.3" +rand = "0.8" +regex = "1.10.3" +serde = { version = "1", optional = true, features = ["derive"] } +sha2 = "0.10.8" +smallvec = "1.13.1" +tracing = "0.1.37" +void = "1.0.2" + +prometheus-client = "0.22.0" + +[dev-dependencies] +quickcheck = { workspace = true } + +# Passing arguments to the docsrs builder in order to properly document cfg's. +# More information: https://docs.rs/about/builds#cross-compiling +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] +rustc-args = ["--cfg", "docsrs"] diff --git a/beacon_node/lighthouse_network/gossipsub/src/backoff.rs b/beacon_node/lighthouse_network/gossipsub/src/backoff.rs new file mode 100644 index 0000000000..2567a3691e --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/backoff.rs @@ -0,0 +1,175 @@ +// Copyright 2020 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! Data structure for efficiently storing known back-off's when pruning peers. +use crate::topic::TopicHash; +use instant::Instant; +use libp2p::identity::PeerId; +use std::collections::{ + hash_map::{Entry, HashMap}, + HashSet, +}; +use std::time::Duration; + +#[derive(Copy, Clone)] +struct HeartbeatIndex(usize); + +/// Stores backoffs in an efficient manner. +pub(crate) struct BackoffStorage { + /// Stores backoffs and the index in backoffs_by_heartbeat per peer per topic. + backoffs: HashMap>, + /// Stores peer topic pairs per heartbeat (this is cyclic the current index is + /// heartbeat_index). + backoffs_by_heartbeat: Vec>, + /// The index in the backoffs_by_heartbeat vector corresponding to the current heartbeat. + heartbeat_index: HeartbeatIndex, + /// The heartbeat interval duration from the config. + heartbeat_interval: Duration, + /// Backoff slack from the config. + backoff_slack: u32, +} + +impl BackoffStorage { + fn heartbeats(d: &Duration, heartbeat_interval: &Duration) -> usize { + ((d.as_nanos() + heartbeat_interval.as_nanos() - 1) / heartbeat_interval.as_nanos()) + as usize + } + + pub(crate) fn new( + prune_backoff: &Duration, + heartbeat_interval: Duration, + backoff_slack: u32, + ) -> BackoffStorage { + // We add one additional slot for partial heartbeat + let max_heartbeats = + Self::heartbeats(prune_backoff, &heartbeat_interval) + backoff_slack as usize + 1; + BackoffStorage { + backoffs: HashMap::new(), + backoffs_by_heartbeat: vec![HashSet::new(); max_heartbeats], + heartbeat_index: HeartbeatIndex(0), + heartbeat_interval, + backoff_slack, + } + } + + /// Updates the backoff for a peer (if there is already a more restrictive backoff then this call + /// doesn't change anything). + pub(crate) fn update_backoff(&mut self, topic: &TopicHash, peer: &PeerId, time: Duration) { + let instant = Instant::now() + time; + let insert_into_backoffs_by_heartbeat = + |heartbeat_index: HeartbeatIndex, + backoffs_by_heartbeat: &mut Vec>, + heartbeat_interval, + backoff_slack| { + let pair = (topic.clone(), *peer); + let index = (heartbeat_index.0 + + Self::heartbeats(&time, heartbeat_interval) + + backoff_slack as usize) + % backoffs_by_heartbeat.len(); + backoffs_by_heartbeat[index].insert(pair); + HeartbeatIndex(index) + }; + match self.backoffs.entry(topic.clone()).or_default().entry(*peer) { + Entry::Occupied(mut o) => { + let (backoff, index) = o.get(); + if backoff < &instant { + let pair = (topic.clone(), *peer); + if let Some(s) = self.backoffs_by_heartbeat.get_mut(index.0) { + s.remove(&pair); + } + let index = insert_into_backoffs_by_heartbeat( + self.heartbeat_index, + &mut self.backoffs_by_heartbeat, + &self.heartbeat_interval, + self.backoff_slack, + ); + o.insert((instant, index)); + } + } + Entry::Vacant(v) => { + let index = insert_into_backoffs_by_heartbeat( + self.heartbeat_index, + &mut self.backoffs_by_heartbeat, + &self.heartbeat_interval, + self.backoff_slack, + ); + v.insert((instant, index)); + } + }; + } + + /// Checks if a given peer is backoffed for the given topic. This method respects the + /// configured BACKOFF_SLACK and may return true even if the backup is already over. + /// It is guaranteed to return false if the backoff is not over and eventually if enough time + /// passed true if the backoff is over. + /// + /// This method should be used for deciding if we can already send a GRAFT to a previously + /// backoffed peer. + pub(crate) fn is_backoff_with_slack(&self, topic: &TopicHash, peer: &PeerId) -> bool { + self.backoffs + .get(topic) + .map_or(false, |m| m.contains_key(peer)) + } + + pub(crate) fn get_backoff_time(&self, topic: &TopicHash, peer: &PeerId) -> Option { + Self::get_backoff_time_from_backoffs(&self.backoffs, topic, peer) + } + + fn get_backoff_time_from_backoffs( + backoffs: &HashMap>, + topic: &TopicHash, + peer: &PeerId, + ) -> Option { + backoffs + .get(topic) + .and_then(|m| m.get(peer).map(|(i, _)| *i)) + } + + /// Applies a heartbeat. That should be called regularly in intervals of length + /// `heartbeat_interval`. + pub(crate) fn heartbeat(&mut self) { + // Clean up backoffs_by_heartbeat + if let Some(s) = self.backoffs_by_heartbeat.get_mut(self.heartbeat_index.0) { + let backoffs = &mut self.backoffs; + let slack = self.heartbeat_interval * self.backoff_slack; + let now = Instant::now(); + s.retain(|(topic, peer)| { + let keep = match Self::get_backoff_time_from_backoffs(backoffs, topic, peer) { + Some(backoff_time) => backoff_time + slack > now, + None => false, + }; + if !keep { + //remove from backoffs + if let Entry::Occupied(mut m) = backoffs.entry(topic.clone()) { + if m.get_mut().remove(peer).is_some() && m.get().is_empty() { + m.remove(); + } + } + } + + keep + }); + } + + // Increase heartbeat index + self.heartbeat_index = + HeartbeatIndex((self.heartbeat_index.0 + 1) % self.backoffs_by_heartbeat.len()); + } +} diff --git a/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs b/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs new file mode 100644 index 0000000000..ce0437342e --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs @@ -0,0 +1,3488 @@ +// Copyright 2020 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +use std::{ + cmp::{max, Ordering}, + collections::HashSet, + collections::VecDeque, + collections::{BTreeSet, HashMap}, + fmt, + net::IpAddr, + task::{Context, Poll}, + time::Duration, +}; + +use futures::StreamExt; +use futures_ticker::Ticker; +use prometheus_client::registry::Registry; +use rand::{seq::SliceRandom, thread_rng}; + +use instant::Instant; +use libp2p::core::{multiaddr::Protocol::Ip4, multiaddr::Protocol::Ip6, Endpoint, Multiaddr}; +use libp2p::identity::Keypair; +use libp2p::identity::PeerId; +use libp2p::swarm::{ + behaviour::{AddressChange, ConnectionClosed, ConnectionEstablished, FromSwarm}, + dial_opts::DialOpts, + ConnectionDenied, ConnectionId, NetworkBehaviour, NotifyHandler, THandler, THandlerInEvent, + THandlerOutEvent, ToSwarm, +}; + +use super::gossip_promises::GossipPromises; +use super::handler::{Handler, HandlerEvent, HandlerIn}; +use super::mcache::MessageCache; +use super::metrics::{Churn, Config as MetricsConfig, Inclusion, Metrics, Penalty}; +use super::peer_score::{PeerScore, PeerScoreParams, PeerScoreThresholds, RejectReason}; +use super::protocol::SIGNING_PREFIX; +use super::rpc_proto::proto; +use super::subscription_filter::{AllowAllSubscriptionFilter, TopicSubscriptionFilter}; +use super::time_cache::DuplicateCache; +use super::topic::{Hasher, Topic, TopicHash}; +use super::transform::{DataTransform, IdentityTransform}; +use super::types::{ + ControlAction, FailedMessages, Message, MessageAcceptance, MessageId, PeerInfo, RawMessage, + Subscription, SubscriptionAction, +}; +use super::types::{Graft, IHave, IWant, PeerConnections, PeerKind, Prune}; +use super::{backoff::BackoffStorage, types::RpcSender}; +use super::{ + config::{Config, ValidationMode}, + types::RpcOut, +}; +use super::{PublishError, SubscriptionError, TopicScoreParams, ValidationError}; +use instant::SystemTime; +use quick_protobuf::{MessageWrite, Writer}; +use std::{cmp::Ordering::Equal, fmt::Debug}; + +#[cfg(test)] +mod tests; + +/// Determines if published messages should be signed or not. +/// +/// Without signing, a number of privacy preserving modes can be selected. +/// +/// NOTE: The default validation settings are to require signatures. The [`ValidationMode`] +/// should be updated in the [`Config`] to allow for unsigned messages. +#[derive(Clone)] +pub enum MessageAuthenticity { + /// Message signing is enabled. The author will be the owner of the key and the sequence number + /// will be linearly increasing. + Signed(Keypair), + /// Message signing is disabled. + /// + /// The specified [`PeerId`] will be used as the author of all published messages. The sequence + /// number will be randomized. + Author(PeerId), + /// Message signing is disabled. + /// + /// A random [`PeerId`] will be used when publishing each message. The sequence number will be + /// randomized. + RandomAuthor, + /// Message signing is disabled. + /// + /// The author of the message and the sequence numbers are excluded from the message. + /// + /// NOTE: Excluding these fields may make these messages invalid by other nodes who + /// enforce validation of these fields. See [`ValidationMode`] in the [`Config`] + /// for how to customise this for rust-libp2p gossipsub. A custom `message_id` + /// function will need to be set to prevent all messages from a peer being filtered + /// as duplicates. + Anonymous, +} + +impl MessageAuthenticity { + /// Returns true if signing is enabled. + pub fn is_signing(&self) -> bool { + matches!(self, MessageAuthenticity::Signed(_)) + } + + pub fn is_anonymous(&self) -> bool { + matches!(self, MessageAuthenticity::Anonymous) + } +} + +/// Event that can be emitted by the gossipsub behaviour. +#[derive(Debug)] +pub enum Event { + /// A message has been received. + Message { + /// The peer that forwarded us this message. + propagation_source: PeerId, + /// The [`MessageId`] of the message. This should be referenced by the application when + /// validating a message (if required). + message_id: MessageId, + /// The decompressed message itself. + message: Message, + }, + /// A remote subscribed to a topic. + Subscribed { + /// Remote that has subscribed. + peer_id: PeerId, + /// The topic it has subscribed to. + topic: TopicHash, + }, + /// A remote unsubscribed from a topic. + Unsubscribed { + /// Remote that has unsubscribed. + peer_id: PeerId, + /// The topic it has subscribed from. + topic: TopicHash, + }, + /// A peer that does not support gossipsub has connected. + GossipsubNotSupported { peer_id: PeerId }, + /// A peer is not able to download messages in time. + SlowPeer { + /// The peer_id + peer_id: PeerId, + /// The types and amounts of failed messages that are occurring for this peer. + failed_messages: FailedMessages, + }, +} + +/// A data structure for storing configuration for publishing messages. See [`MessageAuthenticity`] +/// for further details. +#[allow(clippy::large_enum_variant)] +enum PublishConfig { + Signing { + keypair: Keypair, + author: PeerId, + inline_key: Option>, + last_seq_no: SequenceNumber, + }, + Author(PeerId), + RandomAuthor, + Anonymous, +} + +/// A strictly linearly increasing sequence number. +/// +/// We start from the current time as unix timestamp in milliseconds. +#[derive(Debug)] +struct SequenceNumber(u64); + +impl SequenceNumber { + fn new() -> Self { + let unix_timestamp = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("time to be linear") + .as_nanos(); + + Self(unix_timestamp as u64) + } + + fn next(&mut self) -> u64 { + self.0 = self + .0 + .checked_add(1) + .expect("to not exhaust u64 space for sequence numbers"); + + self.0 + } +} + +impl PublishConfig { + pub(crate) fn get_own_id(&self) -> Option<&PeerId> { + match self { + Self::Signing { author, .. } => Some(author), + Self::Author(author) => Some(author), + _ => None, + } + } +} + +impl From for PublishConfig { + fn from(authenticity: MessageAuthenticity) -> Self { + match authenticity { + MessageAuthenticity::Signed(keypair) => { + let public_key = keypair.public(); + let key_enc = public_key.encode_protobuf(); + let key = if key_enc.len() <= 42 { + // The public key can be inlined in [`rpc_proto::proto::::Message::from`], so we don't include it + // specifically in the [`rpc_proto::proto::Message::key`] field. + None + } else { + // Include the protobuf encoding of the public key in the message. + Some(key_enc) + }; + + PublishConfig::Signing { + keypair, + author: public_key.to_peer_id(), + inline_key: key, + last_seq_no: SequenceNumber::new(), + } + } + MessageAuthenticity::Author(peer_id) => PublishConfig::Author(peer_id), + MessageAuthenticity::RandomAuthor => PublishConfig::RandomAuthor, + MessageAuthenticity::Anonymous => PublishConfig::Anonymous, + } + } +} + +/// Network behaviour that handles the gossipsub protocol. +/// +/// NOTE: Initialisation requires a [`MessageAuthenticity`] and [`Config`] instance. If +/// message signing is disabled, the [`ValidationMode`] in the config should be adjusted to an +/// appropriate level to accept unsigned messages. +/// +/// The DataTransform trait allows applications to optionally add extra encoding/decoding +/// functionality to the underlying messages. This is intended for custom compression algorithms. +/// +/// The TopicSubscriptionFilter allows applications to implement specific filters on topics to +/// prevent unwanted messages being propagated and evaluated. +pub struct Behaviour { + /// Configuration providing gossipsub performance parameters. + config: Config, + + /// Events that need to be yielded to the outside when polling. + events: VecDeque>, + + /// Information used for publishing messages. + publish_config: PublishConfig, + + /// An LRU Time cache for storing seen messages (based on their ID). This cache prevents + /// duplicates from being propagated to the application and on the network. + duplicate_cache: DuplicateCache, + + /// A set of connected peers, indexed by their [`PeerId`] tracking both the [`PeerKind`] and + /// the set of [`ConnectionId`]s. + connected_peers: HashMap, + + /// A set of all explicit peers. These are peers that remain connected and we unconditionally + /// forward messages to, outside of the scoring system. + explicit_peers: HashSet, + + /// A list of peers that have been blacklisted by the user. + /// Messages are not sent to and are rejected from these peers. + blacklisted_peers: HashSet, + + /// Overlay network of connected peers - Maps topics to connected gossipsub peers. + mesh: HashMap>, + + /// Map of topics to list of peers that we publish to, but don't subscribe to. + fanout: HashMap>, + + /// The last publish time for fanout topics. + fanout_last_pub: HashMap, + + ///Storage for backoffs + backoffs: BackoffStorage, + + /// Message cache for the last few heartbeats. + mcache: MessageCache, + + /// Heartbeat interval stream. + heartbeat: Ticker, + + /// Number of heartbeats since the beginning of time; this allows us to amortize some resource + /// clean up -- eg backoff clean up. + heartbeat_ticks: u64, + + /// We remember all peers we found through peer exchange, since those peers are not considered + /// as safe as randomly discovered outbound peers. This behaviour diverges from the go + /// implementation to avoid possible love bombing attacks in PX. When disconnecting peers will + /// be removed from this list which may result in a true outbound rediscovery. + px_peers: HashSet, + + /// Set of connected outbound peers (we only consider true outbound peers found through + /// discovery and not by PX). + outbound_peers: HashSet, + + /// Stores optional peer score data together with thresholds, decay interval and gossip + /// promises. + peer_score: Option<(PeerScore, PeerScoreThresholds, Ticker, GossipPromises)>, + + /// Counts the number of `IHAVE` received from each peer since the last heartbeat. + count_received_ihave: HashMap, + + /// Counts the number of `IWANT` that we sent the each peer since the last heartbeat. + count_sent_iwant: HashMap, + + /// Short term cache for published message ids. This is used for penalizing peers sending + /// our own messages back if the messages are anonymous or use a random author. + published_message_ids: DuplicateCache, + + /// The filter used to handle message subscriptions. + subscription_filter: F, + + /// A general transformation function that can be applied to data received from the wire before + /// calculating the message-id and sending to the application. This is designed to allow the + /// user to implement arbitrary topic-based compression algorithms. + data_transform: D, + + /// Keep track of a set of internal metrics relating to gossipsub. + metrics: Option, + + /// Tracks the numbers of failed messages per peer-id. + failed_messages: HashMap, +} + +impl Behaviour +where + D: DataTransform + Default, + F: TopicSubscriptionFilter + Default, +{ + /// Creates a Gossipsub [`Behaviour`] struct given a set of parameters specified via a + /// [`Config`]. This has no subscription filter and uses no compression. + pub fn new(privacy: MessageAuthenticity, config: Config) -> Result { + Self::new_with_subscription_filter_and_transform( + privacy, + config, + None, + F::default(), + D::default(), + ) + } + + /// Creates a Gossipsub [`Behaviour`] struct given a set of parameters specified via a + /// [`Config`]. This has no subscription filter and uses no compression. + /// Metrics can be evaluated by passing a reference to a [`Registry`]. + pub fn new_with_metrics( + privacy: MessageAuthenticity, + config: Config, + metrics_registry: &mut Registry, + metrics_config: MetricsConfig, + ) -> Result { + Self::new_with_subscription_filter_and_transform( + privacy, + config, + Some((metrics_registry, metrics_config)), + F::default(), + D::default(), + ) + } +} + +impl Behaviour +where + D: DataTransform + Default, + F: TopicSubscriptionFilter, +{ + /// Creates a Gossipsub [`Behaviour`] struct given a set of parameters specified via a + /// [`Config`] and a custom subscription filter. + pub fn new_with_subscription_filter( + privacy: MessageAuthenticity, + config: Config, + metrics: Option<(&mut Registry, MetricsConfig)>, + subscription_filter: F, + ) -> Result { + Self::new_with_subscription_filter_and_transform( + privacy, + config, + metrics, + subscription_filter, + D::default(), + ) + } +} + +impl Behaviour +where + D: DataTransform, + F: TopicSubscriptionFilter + Default, +{ + /// Creates a Gossipsub [`Behaviour`] struct given a set of parameters specified via a + /// [`Config`] and a custom data transform. + pub fn new_with_transform( + privacy: MessageAuthenticity, + config: Config, + metrics: Option<(&mut Registry, MetricsConfig)>, + data_transform: D, + ) -> Result { + Self::new_with_subscription_filter_and_transform( + privacy, + config, + metrics, + F::default(), + data_transform, + ) + } +} + +impl Behaviour +where + D: DataTransform, + F: TopicSubscriptionFilter, +{ + /// Creates a Gossipsub [`Behaviour`] struct given a set of parameters specified via a + /// [`Config`] and a custom subscription filter and data transform. + pub fn new_with_subscription_filter_and_transform( + privacy: MessageAuthenticity, + config: Config, + metrics: Option<(&mut Registry, MetricsConfig)>, + subscription_filter: F, + data_transform: D, + ) -> Result { + // Set up the router given the configuration settings. + + // We do not allow configurations where a published message would also be rejected if it + // were received locally. + validate_config(&privacy, config.validation_mode())?; + + Ok(Behaviour { + metrics: metrics.map(|(registry, cfg)| Metrics::new(registry, cfg)), + events: VecDeque::new(), + publish_config: privacy.into(), + duplicate_cache: DuplicateCache::new(config.duplicate_cache_time()), + explicit_peers: HashSet::new(), + blacklisted_peers: HashSet::new(), + mesh: HashMap::new(), + fanout: HashMap::new(), + fanout_last_pub: HashMap::new(), + backoffs: BackoffStorage::new( + &config.prune_backoff(), + config.heartbeat_interval(), + config.backoff_slack(), + ), + mcache: MessageCache::new(config.history_gossip(), config.history_length()), + heartbeat: Ticker::new_with_next( + config.heartbeat_interval(), + config.heartbeat_initial_delay(), + ), + heartbeat_ticks: 0, + px_peers: HashSet::new(), + outbound_peers: HashSet::new(), + peer_score: None, + count_received_ihave: HashMap::new(), + count_sent_iwant: HashMap::new(), + connected_peers: HashMap::new(), + published_message_ids: DuplicateCache::new(config.published_message_ids_cache_time()), + config, + subscription_filter, + data_transform, + failed_messages: Default::default(), + }) + } +} + +impl Behaviour +where + D: DataTransform + Send + 'static, + F: TopicSubscriptionFilter + Send + 'static, +{ + /// Lists the hashes of the topics we are currently subscribed to. + pub fn topics(&self) -> impl Iterator { + self.mesh.keys() + } + + /// Lists all mesh peers for a certain topic hash. + pub fn mesh_peers(&self, topic_hash: &TopicHash) -> impl Iterator { + self.mesh.get(topic_hash).into_iter().flat_map(|x| x.iter()) + } + + pub fn all_mesh_peers(&self) -> impl Iterator { + let mut res = BTreeSet::new(); + for peers in self.mesh.values() { + res.extend(peers); + } + res.into_iter() + } + + /// Lists all known peers and their associated subscribed topics. + pub fn all_peers(&self) -> impl Iterator)> { + self.connected_peers + .iter() + .map(|(peer_id, peer)| (peer_id, peer.topics.iter().collect())) + } + + /// Lists all known peers and their associated protocol. + pub fn peer_protocol(&self) -> impl Iterator { + self.connected_peers.iter().map(|(k, v)| (k, &v.kind)) + } + + /// Returns the gossipsub score for a given peer, if one exists. + pub fn peer_score(&self, peer_id: &PeerId) -> Option { + self.peer_score + .as_ref() + .map(|(score, ..)| score.score(peer_id)) + } + + /// Subscribe to a topic. + /// + /// Returns [`Ok(true)`] if the subscription worked. Returns [`Ok(false)`] if we were already + /// subscribed. + pub fn subscribe(&mut self, topic: &Topic) -> Result { + tracing::debug!(%topic, "Subscribing to topic"); + let topic_hash = topic.hash(); + if !self.subscription_filter.can_subscribe(&topic_hash) { + return Err(SubscriptionError::NotAllowed); + } + + if self.mesh.contains_key(&topic_hash) { + tracing::debug!(%topic, "Topic is already in the mesh"); + return Ok(false); + } + + // send subscription request to all peers + for (peer_id, peer) in self.connected_peers.iter_mut() { + tracing::debug!(%peer_id, "Sending SUBSCRIBE to peer"); + + peer.sender.subscribe(topic_hash.clone()); + } + + // call JOIN(topic) + // this will add new peers to the mesh for the topic + self.join(&topic_hash); + tracing::debug!(%topic, "Subscribed to topic"); + Ok(true) + } + + /// Unsubscribes from a topic. + /// + /// Returns [`Ok(true)`] if we were subscribed to this topic. + pub fn unsubscribe(&mut self, topic: &Topic) -> Result { + tracing::debug!(%topic, "Unsubscribing from topic"); + let topic_hash = topic.hash(); + + if !self.mesh.contains_key(&topic_hash) { + tracing::debug!(topic=%topic_hash, "Already unsubscribed from topic"); + // we are not subscribed + return Ok(false); + } + + // announce to all peers + for (peer_id, peer) in self.connected_peers.iter_mut() { + tracing::debug!(%peer_id, "Sending UNSUBSCRIBE to peer"); + peer.sender.unsubscribe(topic_hash.clone()); + } + + // call LEAVE(topic) + // this will remove the topic from the mesh + self.leave(&topic_hash); + + tracing::debug!(topic=%topic_hash, "Unsubscribed from topic"); + Ok(true) + } + + /// Publishes a message with multiple topics to the network. + pub fn publish( + &mut self, + topic: impl Into, + data: impl Into>, + ) -> Result { + let data = data.into(); + let topic = topic.into(); + + // Transform the data before building a raw_message. + let transformed_data = self + .data_transform + .outbound_transform(&topic, data.clone())?; + + let raw_message = self.build_raw_message(topic, transformed_data)?; + + // calculate the message id from the un-transformed data + let msg_id = self.config.message_id(&Message { + source: raw_message.source, + data, // the uncompressed form + sequence_number: raw_message.sequence_number, + topic: raw_message.topic.clone(), + }); + + // check that the size doesn't exceed the max transmission size + if raw_message.raw_protobuf_len() > self.config.max_transmit_size() { + return Err(PublishError::MessageTooLarge); + } + + // Check the if the message has been published before + if self.duplicate_cache.contains(&msg_id) { + // This message has already been seen. We don't re-publish messages that have already + // been published on the network. + tracing::warn!( + message=%msg_id, + "Not publishing a message that has already been published" + ); + return Err(PublishError::Duplicate); + } + + tracing::trace!(message=%msg_id, "Publishing message"); + + let topic_hash = raw_message.topic.clone(); + + let mut peers_on_topic = self + .connected_peers + .iter() + .filter(|(_, p)| p.topics.contains(&topic_hash)) + .map(|(peer_id, _)| peer_id) + .peekable(); + + if peers_on_topic.peek().is_none() { + return Err(PublishError::InsufficientPeers); + } + + let mut recipient_peers = HashSet::new(); + + if self.config.flood_publish() { + // Forward to all peers above score and all explicit peers + recipient_peers.extend(peers_on_topic.filter(|p| { + self.explicit_peers.contains(*p) + || !self.score_below_threshold(p, |ts| ts.publish_threshold).0 + })); + } else { + match self.mesh.get(&topic_hash) { + // Mesh peers + Some(mesh_peers) => { + // We have a mesh set. We want to make sure to publish to at least `mesh_n` + // peers (if possible). + let needed_extra_peers = self.config.mesh_n().saturating_sub(mesh_peers.len()); + + if needed_extra_peers > 0 { + // We don't have `mesh_n` peers in our mesh, we will randomly select extras + // and publish to them. + + // Get a random set of peers that are appropriate to send messages too. + let peer_list = get_random_peers( + &self.connected_peers, + &topic_hash, + needed_extra_peers, + |peer| { + !mesh_peers.contains(peer) + && !self.explicit_peers.contains(peer) + && !self + .score_below_threshold(peer, |pst| pst.publish_threshold) + .0 + }, + ); + recipient_peers.extend(peer_list); + } + + recipient_peers.extend(mesh_peers); + } + // Gossipsub peers + None => { + tracing::debug!(topic=%topic_hash, "Topic not in the mesh"); + // If we have fanout peers add them to the map. + if self.fanout.contains_key(&topic_hash) { + for peer in self.fanout.get(&topic_hash).expect("Topic must exist") { + recipient_peers.insert(*peer); + } + } else { + // We have no fanout peers, select mesh_n of them and add them to the fanout + let mesh_n = self.config.mesh_n(); + let new_peers = + get_random_peers(&self.connected_peers, &topic_hash, mesh_n, { + |p| { + !self.explicit_peers.contains(p) + && !self + .score_below_threshold(p, |pst| pst.publish_threshold) + .0 + } + }); + // Add the new peers to the fanout and recipient peers + self.fanout.insert(topic_hash.clone(), new_peers.clone()); + for peer in new_peers { + tracing::debug!(%peer, "Peer added to fanout"); + recipient_peers.insert(peer); + } + } + // We are publishing to fanout peers - update the time we published + self.fanout_last_pub + .insert(topic_hash.clone(), Instant::now()); + } + } + + // Explicit peers that are part of the topic + recipient_peers + .extend(peers_on_topic.filter(|peer_id| self.explicit_peers.contains(peer_id))); + + // Floodsub peers + for (peer, connections) in &self.connected_peers { + if connections.kind == PeerKind::Floodsub + && !self + .score_below_threshold(peer, |ts| ts.publish_threshold) + .0 + { + recipient_peers.insert(*peer); + } + } + } + + // If the message isn't a duplicate and we have sent it to some peers add it to the + // duplicate cache and memcache. + self.duplicate_cache.insert(msg_id.clone()); + self.mcache.put(&msg_id, raw_message.clone()); + + // If the message is anonymous or has a random author add it to the published message ids + // cache. + if let PublishConfig::RandomAuthor | PublishConfig::Anonymous = self.publish_config { + if !self.config.allow_self_origin() { + self.published_message_ids.insert(msg_id.clone()); + } + } + + // Send to peers we know are subscribed to the topic. + let mut publish_failed = true; + for peer_id in recipient_peers.iter() { + if let Some(peer) = self.connected_peers.get_mut(peer_id) { + tracing::trace!(peer=%peer_id, "Sending message to peer"); + match peer.sender.publish( + raw_message.clone(), + self.config.publish_queue_duration(), + self.metrics.as_mut(), + ) { + Ok(_) => publish_failed = false, + Err(_) => { + self.failed_messages.entry(*peer_id).or_default().priority += 1; + + tracing::warn!(peer_id=%peer_id, "Publish queue full. Could not publish to peer"); + // Downscore the peer due to failed message. + if let Some((peer_score, ..)) = &mut self.peer_score { + peer_score.failed_message_slow_peer(peer_id); + } + } + } + } else { + tracing::error!(peer_id = %peer_id, + "Could not PUBLISH, peer doesn't exist in connected peer list"); + } + } + + if recipient_peers.is_empty() { + return Err(PublishError::InsufficientPeers); + } + + if publish_failed { + return Err(PublishError::AllQueuesFull(recipient_peers.len())); + } + + tracing::debug!(message=%msg_id, "Published message"); + + if let Some(metrics) = self.metrics.as_mut() { + metrics.register_published_message(&topic_hash); + } + + Ok(msg_id) + } + + /// This function should be called when [`Config::validate_messages()`] is `true` after + /// the message got validated by the caller. Messages are stored in the ['Memcache'] and + /// validation is expected to be fast enough that the messages should still exist in the cache. + /// There are three possible validation outcomes and the outcome is given in acceptance. + /// + /// If acceptance = [`MessageAcceptance::Accept`] the message will get propagated to the + /// network. The `propagation_source` parameter indicates who the message was received by and + /// will not be forwarded back to that peer. + /// + /// If acceptance = [`MessageAcceptance::Reject`] the message will be deleted from the memcache + /// and the Pâ‚„ penalty will be applied to the `propagation_source`. + // + /// If acceptance = [`MessageAcceptance::Ignore`] the message will be deleted from the memcache + /// but no Pâ‚„ penalty will be applied. + /// + /// This function will return true if the message was found in the cache and false if was not + /// in the cache anymore. + /// + /// This should only be called once per message. + pub fn report_message_validation_result( + &mut self, + msg_id: &MessageId, + propagation_source: &PeerId, + acceptance: MessageAcceptance, + ) -> Result { + let reject_reason = match acceptance { + MessageAcceptance::Accept => { + let (raw_message, originating_peers) = match self.mcache.validate(msg_id) { + Some((raw_message, originating_peers)) => { + (raw_message.clone(), originating_peers) + } + None => { + tracing::warn!( + message=%msg_id, + "Message not in cache. Ignoring forwarding" + ); + if let Some(metrics) = self.metrics.as_mut() { + metrics.memcache_miss(); + } + return Ok(false); + } + }; + + if let Some(metrics) = self.metrics.as_mut() { + metrics.register_msg_validation(&raw_message.topic, &acceptance); + } + + self.forward_msg( + msg_id, + raw_message, + Some(propagation_source), + originating_peers, + )?; + return Ok(true); + } + MessageAcceptance::Reject => RejectReason::ValidationFailed, + MessageAcceptance::Ignore => RejectReason::ValidationIgnored, + }; + + if let Some((raw_message, originating_peers)) = self.mcache.remove(msg_id) { + if let Some(metrics) = self.metrics.as_mut() { + metrics.register_msg_validation(&raw_message.topic, &acceptance); + } + + // Tell peer_score about reject + // Reject the original source, and any duplicates we've seen from other peers. + if let Some((peer_score, ..)) = &mut self.peer_score { + peer_score.reject_message( + propagation_source, + msg_id, + &raw_message.topic, + reject_reason, + ); + for peer in originating_peers.iter() { + peer_score.reject_message(peer, msg_id, &raw_message.topic, reject_reason); + } + } + Ok(true) + } else { + tracing::warn!(message=%msg_id, "Rejected message not in cache"); + Ok(false) + } + } + + /// Register topics to ensure metrics are recorded correctly for these topics. + pub fn register_topics_for_metrics(&mut self, topics: Vec) { + if let Some(metrics) = &mut self.metrics { + metrics.register_allowed_topics(topics); + } + } + + /// Adds a new peer to the list of explicitly connected peers. + pub fn add_explicit_peer(&mut self, peer_id: &PeerId) { + tracing::debug!(peer=%peer_id, "Adding explicit peer"); + + self.explicit_peers.insert(*peer_id); + + self.check_explicit_peer_connection(peer_id); + } + + /// This removes the peer from explicitly connected peers, note that this does not disconnect + /// the peer. + pub fn remove_explicit_peer(&mut self, peer_id: &PeerId) { + tracing::debug!(peer=%peer_id, "Removing explicit peer"); + self.explicit_peers.remove(peer_id); + } + + /// Blacklists a peer. All messages from this peer will be rejected and any message that was + /// created by this peer will be rejected. + pub fn blacklist_peer(&mut self, peer_id: &PeerId) { + if self.blacklisted_peers.insert(*peer_id) { + tracing::debug!(peer=%peer_id, "Peer has been blacklisted"); + } + } + + /// Removes a peer from the blacklist if it has previously been blacklisted. + pub fn remove_blacklisted_peer(&mut self, peer_id: &PeerId) { + if self.blacklisted_peers.remove(peer_id) { + tracing::debug!(peer=%peer_id, "Peer has been removed from the blacklist"); + } + } + + /// Activates the peer scoring system with the given parameters. This will reset all scores + /// if there was already another peer scoring system activated. Returns an error if the + /// params are not valid or if they got already set. + pub fn with_peer_score( + &mut self, + params: PeerScoreParams, + threshold: PeerScoreThresholds, + ) -> Result<(), String> { + self.with_peer_score_and_message_delivery_time_callback(params, threshold, None) + } + + /// Activates the peer scoring system with the given parameters and a message delivery time + /// callback. Returns an error if the parameters got already set. + pub fn with_peer_score_and_message_delivery_time_callback( + &mut self, + params: PeerScoreParams, + threshold: PeerScoreThresholds, + callback: Option, + ) -> Result<(), String> { + params.validate()?; + threshold.validate()?; + + if self.peer_score.is_some() { + return Err("Peer score set twice".into()); + } + + let interval = Ticker::new(params.decay_interval); + let peer_score = PeerScore::new_with_message_delivery_time_callback(params, callback); + self.peer_score = Some((peer_score, threshold, interval, GossipPromises::default())); + Ok(()) + } + + /// Sets scoring parameters for a topic. + /// + /// The [`Self::with_peer_score()`] must first be called to initialise peer scoring. + pub fn set_topic_params( + &mut self, + topic: Topic, + params: TopicScoreParams, + ) -> Result<(), &'static str> { + if let Some((peer_score, ..)) = &mut self.peer_score { + peer_score.set_topic_params(topic.hash(), params); + Ok(()) + } else { + Err("Peer score must be initialised with `with_peer_score()`") + } + } + + /// Returns a scoring parameters for a topic if existent. + pub fn get_topic_params(&self, topic: &Topic) -> Option<&TopicScoreParams> { + self.peer_score.as_ref()?.0.get_topic_params(&topic.hash()) + } + + /// Sets the application specific score for a peer. Returns true if scoring is active and + /// the peer is connected or if the score of the peer is not yet expired, false otherwise. + pub fn set_application_score(&mut self, peer_id: &PeerId, new_score: f64) -> bool { + if let Some((peer_score, ..)) = &mut self.peer_score { + peer_score.set_application_score(peer_id, new_score) + } else { + false + } + } + + /// Gossipsub JOIN(topic) - adds topic peers to mesh and sends them GRAFT messages. + fn join(&mut self, topic_hash: &TopicHash) { + tracing::debug!(topic=%topic_hash, "Running JOIN for topic"); + + // if we are already in the mesh, return + if self.mesh.contains_key(topic_hash) { + tracing::debug!(topic=%topic_hash, "JOIN: The topic is already in the mesh, ignoring JOIN"); + return; + } + + let mut added_peers = HashSet::new(); + + if let Some(m) = self.metrics.as_mut() { + m.joined(topic_hash) + } + + // check if we have mesh_n peers in fanout[topic] and add them to the mesh if we do, + // removing the fanout entry. + if let Some((_, mut peers)) = self.fanout.remove_entry(topic_hash) { + tracing::debug!( + topic=%topic_hash, + "JOIN: Removing peers from the fanout for topic" + ); + + // remove explicit peers, peers with negative scores, and backoffed peers + peers.retain(|p| { + !self.explicit_peers.contains(p) + && !self.score_below_threshold(p, |_| 0.0).0 + && !self.backoffs.is_backoff_with_slack(topic_hash, p) + }); + + // Add up to mesh_n of them them to the mesh + // NOTE: These aren't randomly added, currently FIFO + let add_peers = std::cmp::min(peers.len(), self.config.mesh_n()); + tracing::debug!( + topic=%topic_hash, + "JOIN: Adding {:?} peers from the fanout for topic", + add_peers + ); + added_peers.extend(peers.iter().take(add_peers)); + + self.mesh.insert( + topic_hash.clone(), + peers.into_iter().take(add_peers).collect(), + ); + + // remove the last published time + self.fanout_last_pub.remove(topic_hash); + } + + let fanaout_added = added_peers.len(); + if let Some(m) = self.metrics.as_mut() { + m.peers_included(topic_hash, Inclusion::Fanout, fanaout_added) + } + + // check if we need to get more peers, which we randomly select + if added_peers.len() < self.config.mesh_n() { + // get the peers + let new_peers = get_random_peers( + &self.connected_peers, + topic_hash, + self.config.mesh_n() - added_peers.len(), + |peer| { + !added_peers.contains(peer) + && !self.explicit_peers.contains(peer) + && !self.score_below_threshold(peer, |_| 0.0).0 + && !self.backoffs.is_backoff_with_slack(topic_hash, peer) + }, + ); + added_peers.extend(new_peers.clone()); + // add them to the mesh + tracing::debug!( + "JOIN: Inserting {:?} random peers into the mesh", + new_peers.len() + ); + let mesh_peers = self.mesh.entry(topic_hash.clone()).or_default(); + mesh_peers.extend(new_peers); + } + + let random_added = added_peers.len() - fanaout_added; + if let Some(m) = self.metrics.as_mut() { + m.peers_included(topic_hash, Inclusion::Random, random_added) + } + + for peer_id in added_peers { + // Send a GRAFT control message + if let Some((peer_score, ..)) = &mut self.peer_score { + peer_score.graft(&peer_id, topic_hash.clone()); + } + if let Some(peer) = &mut self.connected_peers.get_mut(&peer_id) { + tracing::debug!(peer=%peer_id, "JOIN: Sending Graft message to peer"); + peer.sender.graft(Graft { + topic_hash: topic_hash.clone(), + }); + } else { + tracing::error!(peer = %peer_id, + "Could not GRAFT, peer doesn't exist in connected peer list"); + } + + // If the peer did not previously exist in any mesh, inform the handler + peer_added_to_mesh( + peer_id, + vec![topic_hash], + &self.mesh, + &mut self.events, + &self.connected_peers, + ); + } + + let mesh_peers = self.mesh_peers(topic_hash).count(); + if let Some(m) = self.metrics.as_mut() { + m.set_mesh_peers(topic_hash, mesh_peers) + } + + tracing::debug!(topic=%topic_hash, "Completed JOIN for topic"); + } + + /// Creates a PRUNE gossipsub action. + fn make_prune( + &mut self, + topic_hash: &TopicHash, + peer: &PeerId, + do_px: bool, + on_unsubscribe: bool, + ) -> Prune { + if let Some((peer_score, ..)) = &mut self.peer_score { + peer_score.prune(peer, topic_hash.clone()); + } + + match self.connected_peers.get(peer).map(|v| &v.kind) { + Some(PeerKind::Floodsub) => { + tracing::error!("Attempted to prune a Floodsub peer"); + } + Some(PeerKind::Gossipsub) => { + // GossipSub v1.0 -- no peer exchange, the peer won't be able to parse it anyway + return Prune { + topic_hash: topic_hash.clone(), + peers: Vec::new(), + backoff: None, + }; + } + None => { + tracing::error!("Attempted to Prune an unknown peer"); + } + _ => {} // Gossipsub 1.1 peer perform the `Prune` + } + + // Select peers for peer exchange + let peers = if do_px { + get_random_peers( + &self.connected_peers, + topic_hash, + self.config.prune_peers(), + |p| p != peer && !self.score_below_threshold(p, |_| 0.0).0, + ) + .into_iter() + .map(|p| PeerInfo { peer_id: Some(p) }) + .collect() + } else { + Vec::new() + }; + + let backoff = if on_unsubscribe { + self.config.unsubscribe_backoff() + } else { + self.config.prune_backoff() + }; + + // update backoff + self.backoffs.update_backoff(topic_hash, peer, backoff); + + Prune { + topic_hash: topic_hash.clone(), + peers, + backoff: Some(backoff.as_secs()), + } + } + + /// Gossipsub LEAVE(topic) - Notifies mesh\[topic\] peers with PRUNE messages. + fn leave(&mut self, topic_hash: &TopicHash) { + tracing::debug!(topic=%topic_hash, "Running LEAVE for topic"); + + // If our mesh contains the topic, send prune to peers and delete it from the mesh + if let Some((_, peers)) = self.mesh.remove_entry(topic_hash) { + if let Some(m) = self.metrics.as_mut() { + m.left(topic_hash) + } + for peer_id in peers { + // Send a PRUNE control message + let prune = self.make_prune(topic_hash, &peer_id, self.config.do_px(), true); + if let Some(peer) = &mut self.connected_peers.get_mut(&peer_id) { + tracing::debug!(%peer_id, "LEAVE: Sending PRUNE to peer"); + peer.sender.prune(prune); + } else { + tracing::error!(peer = %peer_id, + "Could not PRUNE, peer doesn't exist in connected peer list"); + } + + // If the peer did not previously exist in any mesh, inform the handler + peer_removed_from_mesh( + peer_id, + topic_hash, + &self.mesh, + &mut self.events, + &self.connected_peers, + ); + } + } + tracing::debug!(topic=%topic_hash, "Completed LEAVE for topic"); + } + + /// Checks if the given peer is still connected and if not dials the peer again. + fn check_explicit_peer_connection(&mut self, peer_id: &PeerId) { + if !self.connected_peers.contains_key(peer_id) { + // Connect to peer + tracing::debug!(peer=%peer_id, "Connecting to explicit peer"); + self.events.push_back(ToSwarm::Dial { + opts: DialOpts::peer_id(*peer_id).build(), + }); + } + } + + /// Determines if a peer's score is below a given `PeerScoreThreshold` chosen via the + /// `threshold` parameter. + fn score_below_threshold( + &self, + peer_id: &PeerId, + threshold: impl Fn(&PeerScoreThresholds) -> f64, + ) -> (bool, f64) { + Self::score_below_threshold_from_scores(&self.peer_score, peer_id, threshold) + } + + fn score_below_threshold_from_scores( + peer_score: &Option<(PeerScore, PeerScoreThresholds, Ticker, GossipPromises)>, + peer_id: &PeerId, + threshold: impl Fn(&PeerScoreThresholds) -> f64, + ) -> (bool, f64) { + if let Some((peer_score, thresholds, ..)) = peer_score { + let score = peer_score.score(peer_id); + if score < threshold(thresholds) { + return (true, score); + } + (false, score) + } else { + (false, 0.0) + } + } + + /// Handles an IHAVE control message. Checks our cache of messages. If the message is unknown, + /// requests it with an IWANT control message. + fn handle_ihave(&mut self, peer_id: &PeerId, ihave_msgs: Vec<(TopicHash, Vec)>) { + // We ignore IHAVE gossip from any peer whose score is below the gossip threshold + if let (true, score) = self.score_below_threshold(peer_id, |pst| pst.gossip_threshold) { + tracing::debug!( + peer=%peer_id, + %score, + "IHAVE: ignoring peer with score below threshold" + ); + return; + } + + // IHAVE flood protection + let peer_have = self.count_received_ihave.entry(*peer_id).or_insert(0); + *peer_have += 1; + if *peer_have > self.config.max_ihave_messages() { + tracing::debug!( + peer=%peer_id, + "IHAVE: peer has advertised too many times ({}) within this heartbeat \ + interval; ignoring", + *peer_have + ); + return; + } + + if let Some(iasked) = self.count_sent_iwant.get(peer_id) { + if *iasked >= self.config.max_ihave_length() { + tracing::debug!( + peer=%peer_id, + "IHAVE: peer has already advertised too many messages ({}); ignoring", + *iasked + ); + return; + } + } + + tracing::trace!(peer=%peer_id, "Handling IHAVE for peer"); + + let mut iwant_ids = HashSet::new(); + + let want_message = |id: &MessageId| { + if self.duplicate_cache.contains(id) { + return false; + } + + self.peer_score + .as_ref() + .map(|(_, _, _, promises)| !promises.contains(id)) + .unwrap_or(true) + }; + + for (topic, ids) in ihave_msgs { + // only process the message if we are subscribed + if !self.mesh.contains_key(&topic) { + tracing::debug!( + %topic, + "IHAVE: Ignoring IHAVE - Not subscribed to topic" + ); + continue; + } + + for id in ids.into_iter().filter(want_message) { + // have not seen this message and are not currently requesting it + if iwant_ids.insert(id) { + // Register the IWANT metric + if let Some(metrics) = self.metrics.as_mut() { + metrics.register_iwant(&topic); + } + } + } + } + + if !iwant_ids.is_empty() { + let iasked = self.count_sent_iwant.entry(*peer_id).or_insert(0); + let mut iask = iwant_ids.len(); + if *iasked + iask > self.config.max_ihave_length() { + iask = self.config.max_ihave_length().saturating_sub(*iasked); + } + + // Send the list of IWANT control messages + tracing::debug!( + peer=%peer_id, + "IHAVE: Asking for {} out of {} messages from peer", + iask, + iwant_ids.len() + ); + + // Ask in random order + let mut iwant_ids_vec: Vec<_> = iwant_ids.into_iter().collect(); + let mut rng = thread_rng(); + iwant_ids_vec.partial_shuffle(&mut rng, iask); + + iwant_ids_vec.truncate(iask); + *iasked += iask; + + if let Some((_, _, _, gossip_promises)) = &mut self.peer_score { + gossip_promises.add_promise( + *peer_id, + &iwant_ids_vec, + Instant::now() + self.config.iwant_followup_time(), + ); + } + + if let Some(peer) = &mut self.connected_peers.get_mut(peer_id) { + tracing::trace!( + peer=%peer_id, + "IHAVE: Asking for the following messages from peer: {:?}", + iwant_ids_vec + ); + + if peer + .sender + .iwant(IWant { + message_ids: iwant_ids_vec, + }) + .is_err() + { + tracing::warn!(peer=%peer_id, "Send Queue full. Could not send IWANT"); + + if let Some((peer_score, ..)) = &mut self.peer_score { + peer_score.failed_message_slow_peer(peer_id); + } + // Increment failed message count + self.failed_messages + .entry(*peer_id) + .or_default() + .non_priority += 1; + } + } else { + tracing::error!(peer = %peer_id, + "Could not IWANT, peer doesn't exist in connected peer list"); + } + } + tracing::trace!(peer=%peer_id, "Completed IHAVE handling for peer"); + } + + /// Handles an IWANT control message. Checks our cache of messages. If the message exists it is + /// forwarded to the requesting peer. + fn handle_iwant(&mut self, peer_id: &PeerId, iwant_msgs: Vec) { + // We ignore IWANT gossip from any peer whose score is below the gossip threshold + if let (true, score) = self.score_below_threshold(peer_id, |pst| pst.gossip_threshold) { + tracing::debug!( + peer=%peer_id, + "IWANT: ignoring peer with score below threshold [score = {}]", + score + ); + return; + } + + tracing::debug!(peer=%peer_id, "Handling IWANT for peer"); + + for id in iwant_msgs { + // If we have it and the IHAVE count is not above the threshold, + // foward the message. + if let Some((msg, count)) = self + .mcache + .get_with_iwant_counts(&id, peer_id) + .map(|(msg, count)| (msg.clone(), count)) + { + if count > self.config.gossip_retransimission() { + tracing::debug!( + peer=%peer_id, + message=%id, + "IWANT: Peer has asked for message too many times; ignoring request" + ); + } else if let Some(peer) = &mut self.connected_peers.get_mut(peer_id) { + tracing::debug!(peer=%peer_id, "IWANT: Sending cached messages to peer"); + if peer + .sender + .forward( + msg, + self.config.forward_queue_duration(), + self.metrics.as_mut(), + ) + .is_err() + { + // Downscore the peer + if let Some((peer_score, ..)) = &mut self.peer_score { + peer_score.failed_message_slow_peer(peer_id); + } + // Increment the failed message count + self.failed_messages + .entry(*peer_id) + .or_default() + .non_priority += 1; + } + } else { + tracing::error!(peer = %peer_id, + "Could not IWANT, peer doesn't exist in connected peer list"); + } + } + } + tracing::debug!(peer=%peer_id, "Completed IWANT handling for peer"); + } + + /// Handles GRAFT control messages. If subscribed to the topic, adds the peer to mesh, if not, + /// responds with PRUNE messages. + fn handle_graft(&mut self, peer_id: &PeerId, topics: Vec) { + tracing::debug!(peer=%peer_id, "Handling GRAFT message for peer"); + + let mut to_prune_topics = HashSet::new(); + + let mut do_px = self.config.do_px(); + + // For each topic, if a peer has grafted us, then we necessarily must be in their mesh + // and they must be subscribed to the topic. Ensure we have recorded the mapping. + for topic in &topics { + let Some(connected_peer) = self.connected_peers.get_mut(peer_id) else { + tracing::error!(peer_id = %peer_id, "Peer non-existent when handling graft"); + return; + }; + if connected_peer.topics.insert(topic.clone()) { + if let Some(m) = self.metrics.as_mut() { + m.inc_topic_peers(topic); + } + } + } + + // we don't GRAFT to/from explicit peers; complain loudly if this happens + if self.explicit_peers.contains(peer_id) { + tracing::warn!(peer=%peer_id, "GRAFT: ignoring request from direct peer"); + // this is possibly a bug from non-reciprocal configuration; send a PRUNE for all topics + to_prune_topics = topics.into_iter().collect(); + // but don't PX + do_px = false + } else { + let (below_zero, score) = self.score_below_threshold(peer_id, |_| 0.0); + let now = Instant::now(); + for topic_hash in topics { + if let Some(peers) = self.mesh.get_mut(&topic_hash) { + // if the peer is already in the mesh ignore the graft + if peers.contains(peer_id) { + tracing::debug!( + peer=%peer_id, + topic=%&topic_hash, + "GRAFT: Received graft for peer that is already in topic" + ); + continue; + } + + // make sure we are not backing off that peer + if let Some(backoff_time) = self.backoffs.get_backoff_time(&topic_hash, peer_id) + { + if backoff_time > now { + tracing::warn!( + peer=%peer_id, + "[Penalty] Peer attempted graft within backoff time, penalizing" + ); + // add behavioural penalty + if let Some((peer_score, ..)) = &mut self.peer_score { + if let Some(metrics) = self.metrics.as_mut() { + metrics.register_score_penalty(Penalty::GraftBackoff); + } + peer_score.add_penalty(peer_id, 1); + + // check the flood cutoff + // See: https://github.com/rust-lang/rust-clippy/issues/10061 + #[allow(unknown_lints, clippy::unchecked_duration_subtraction)] + let flood_cutoff = (backoff_time + + self.config.graft_flood_threshold()) + - self.config.prune_backoff(); + if flood_cutoff > now { + //extra penalty + peer_score.add_penalty(peer_id, 1); + } + } + // no PX + do_px = false; + + to_prune_topics.insert(topic_hash.clone()); + continue; + } + } + + // check the score + if below_zero { + // we don't GRAFT peers with negative score + tracing::debug!( + peer=%peer_id, + %score, + topic=%topic_hash, + "GRAFT: ignoring peer with negative score" + ); + // we do send them PRUNE however, because it's a matter of protocol correctness + to_prune_topics.insert(topic_hash.clone()); + // but we won't PX to them + do_px = false; + continue; + } + + // check mesh upper bound and only allow graft if the upper bound is not reached or + // if it is an outbound peer + if peers.len() >= self.config.mesh_n_high() + && !self.outbound_peers.contains(peer_id) + { + to_prune_topics.insert(topic_hash.clone()); + continue; + } + + // add peer to the mesh + tracing::debug!( + peer=%peer_id, + topic=%topic_hash, + "GRAFT: Mesh link added for peer in topic" + ); + + if peers.insert(*peer_id) { + if let Some(m) = self.metrics.as_mut() { + m.peers_included(&topic_hash, Inclusion::Subscribed, 1) + } + } + + // If the peer did not previously exist in any mesh, inform the handler + peer_added_to_mesh( + *peer_id, + vec![&topic_hash], + &self.mesh, + &mut self.events, + &self.connected_peers, + ); + + if let Some((peer_score, ..)) = &mut self.peer_score { + peer_score.graft(peer_id, topic_hash); + } + } else { + // don't do PX when there is an unknown topic to avoid leaking our peers + do_px = false; + tracing::debug!( + peer=%peer_id, + topic=%topic_hash, + "GRAFT: Received graft for unknown topic from peer" + ); + // spam hardening: ignore GRAFTs for unknown topics + continue; + } + } + } + + if !to_prune_topics.is_empty() { + // build the prune messages to send + let on_unsubscribe = false; + + let mut sender = match self.connected_peers.get_mut(peer_id) { + Some(connected_peer) => connected_peer.sender.clone(), + None => { + tracing::error!(peer_id = %peer_id, "Peer non-existent when handling graft and obtaining a sender"); + return; + } + }; + + for prune in to_prune_topics + .iter() + .map(|t| self.make_prune(t, peer_id, do_px, on_unsubscribe)) + { + sender.prune(prune); + } + // Send the prune messages to the peer + tracing::debug!( + peer=%peer_id, + "GRAFT: Not subscribed to topics - Sending PRUNE to peer" + ); + } + tracing::debug!(peer=%peer_id, "Completed GRAFT handling for peer"); + } + + fn remove_peer_from_mesh( + &mut self, + peer_id: &PeerId, + topic_hash: &TopicHash, + backoff: Option, + always_update_backoff: bool, + reason: Churn, + ) { + let mut update_backoff = always_update_backoff; + if let Some(peers) = self.mesh.get_mut(topic_hash) { + // remove the peer if it exists in the mesh + if peers.remove(peer_id) { + tracing::debug!( + peer=%peer_id, + topic=%topic_hash, + "PRUNE: Removing peer from the mesh for topic" + ); + if let Some(m) = self.metrics.as_mut() { + m.peers_removed(topic_hash, reason, 1) + } + + if let Some((peer_score, ..)) = &mut self.peer_score { + peer_score.prune(peer_id, topic_hash.clone()); + } + + update_backoff = true; + + // inform the handler + peer_removed_from_mesh( + *peer_id, + topic_hash, + &self.mesh, + &mut self.events, + &self.connected_peers, + ); + } + } + if update_backoff { + let time = if let Some(backoff) = backoff { + Duration::from_secs(backoff) + } else { + self.config.prune_backoff() + }; + // is there a backoff specified by the peer? if so obey it. + self.backoffs.update_backoff(topic_hash, peer_id, time); + } + } + + /// Handles PRUNE control messages. Removes peer from the mesh. + fn handle_prune( + &mut self, + peer_id: &PeerId, + prune_data: Vec<(TopicHash, Vec, Option)>, + ) { + tracing::debug!(peer=%peer_id, "Handling PRUNE message for peer"); + let (below_threshold, score) = + self.score_below_threshold(peer_id, |pst| pst.accept_px_threshold); + for (topic_hash, px, backoff) in prune_data { + self.remove_peer_from_mesh(peer_id, &topic_hash, backoff, true, Churn::Prune); + + if self.mesh.contains_key(&topic_hash) { + //connect to px peers + if !px.is_empty() { + // we ignore PX from peers with insufficient score + if below_threshold { + tracing::debug!( + peer=%peer_id, + %score, + topic=%topic_hash, + "PRUNE: ignoring PX from peer with insufficient score" + ); + continue; + } + + // NOTE: We cannot dial any peers from PX currently as we typically will not + // know their multiaddr. Until SignedRecords are spec'd this + // remains a stub. By default `config.prune_peers()` is set to zero and + // this is skipped. If the user modifies this, this will only be able to + // dial already known peers (from an external discovery mechanism for + // example). + if self.config.prune_peers() > 0 { + self.px_connect(px); + } + } + } + } + tracing::debug!(peer=%peer_id, "Completed PRUNE handling for peer"); + } + + fn px_connect(&mut self, mut px: Vec) { + let n = self.config.prune_peers(); + // Ignore peerInfo with no ID + // + //TODO: Once signed records are spec'd: Can we use peerInfo without any IDs if they have a + // signed peer record? + px.retain(|p| p.peer_id.is_some()); + if px.len() > n { + // only use at most prune_peers many random peers + let mut rng = thread_rng(); + px.partial_shuffle(&mut rng, n); + px = px.into_iter().take(n).collect(); + } + + for p in px { + // TODO: Once signed records are spec'd: extract signed peer record if given and handle + // it, see https://github.com/libp2p/specs/pull/217 + if let Some(peer_id) = p.peer_id { + // mark as px peer + self.px_peers.insert(peer_id); + + // dial peer + self.events.push_back(ToSwarm::Dial { + opts: DialOpts::peer_id(peer_id).build(), + }); + } + } + } + + /// Applies some basic checks to whether this message is valid. Does not apply user validation + /// checks. + fn message_is_valid( + &mut self, + msg_id: &MessageId, + raw_message: &mut RawMessage, + propagation_source: &PeerId, + ) -> bool { + tracing::debug!( + peer=%propagation_source, + message=%msg_id, + "Handling message from peer" + ); + + // Reject any message from a blacklisted peer + if self.blacklisted_peers.contains(propagation_source) { + tracing::debug!( + peer=%propagation_source, + "Rejecting message from blacklisted peer" + ); + if let Some((peer_score, .., gossip_promises)) = &mut self.peer_score { + peer_score.reject_message( + propagation_source, + msg_id, + &raw_message.topic, + RejectReason::BlackListedPeer, + ); + gossip_promises.reject_message(msg_id, &RejectReason::BlackListedPeer); + } + return false; + } + + // Also reject any message that originated from a blacklisted peer + if let Some(source) = raw_message.source.as_ref() { + if self.blacklisted_peers.contains(source) { + tracing::debug!( + peer=%propagation_source, + %source, + "Rejecting message from peer because of blacklisted source" + ); + self.handle_invalid_message( + propagation_source, + raw_message, + RejectReason::BlackListedSource, + ); + return false; + } + } + + // If we are not validating messages, assume this message is validated + // This will allow the message to be gossiped without explicitly calling + // `validate_message`. + if !self.config.validate_messages() { + raw_message.validated = true; + } + + // reject messages claiming to be from ourselves but not locally published + let self_published = !self.config.allow_self_origin() + && if let Some(own_id) = self.publish_config.get_own_id() { + own_id != propagation_source + && raw_message.source.as_ref().map_or(false, |s| s == own_id) + } else { + self.published_message_ids.contains(msg_id) + }; + + if self_published { + tracing::debug!( + message=%msg_id, + source=%propagation_source, + "Dropping message claiming to be from self but forwarded from source" + ); + self.handle_invalid_message(propagation_source, raw_message, RejectReason::SelfOrigin); + return false; + } + + true + } + + /// Handles a newly received [`RawMessage`]. + /// + /// Forwards the message to all peers in the mesh. + fn handle_received_message( + &mut self, + mut raw_message: RawMessage, + propagation_source: &PeerId, + ) { + // Record the received metric + if let Some(metrics) = self.metrics.as_mut() { + metrics.msg_recvd_unfiltered(&raw_message.topic, raw_message.raw_protobuf_len()); + } + + // Try and perform the data transform to the message. If it fails, consider it invalid. + let message = match self.data_transform.inbound_transform(raw_message.clone()) { + Ok(message) => message, + Err(e) => { + tracing::debug!("Invalid message. Transform error: {:?}", e); + // Reject the message and return + self.handle_invalid_message( + propagation_source, + &raw_message, + RejectReason::ValidationError(ValidationError::TransformFailed), + ); + return; + } + }; + + // Calculate the message id on the transformed data. + let msg_id = self.config.message_id(&message); + + // Check the validity of the message + // Peers get penalized if this message is invalid. We don't add it to the duplicate cache + // and instead continually penalize peers that repeatedly send this message. + if !self.message_is_valid(&msg_id, &mut raw_message, propagation_source) { + return; + } + + if !self.duplicate_cache.insert(msg_id.clone()) { + tracing::debug!(message=%msg_id, "Message already received, ignoring"); + if let Some((peer_score, ..)) = &mut self.peer_score { + peer_score.duplicated_message(propagation_source, &msg_id, &message.topic); + } + self.mcache.observe_duplicate(&msg_id, propagation_source); + return; + } + tracing::debug!( + message=%msg_id, + "Put message in duplicate_cache and resolve promises" + ); + + // Record the received message with the metrics + if let Some(metrics) = self.metrics.as_mut() { + metrics.msg_recvd(&message.topic); + } + + // Tells score that message arrived (but is maybe not fully validated yet). + // Consider the message as delivered for gossip promises. + if let Some((peer_score, .., gossip_promises)) = &mut self.peer_score { + peer_score.validate_message(propagation_source, &msg_id, &message.topic); + gossip_promises.message_delivered(&msg_id); + } + + // Add the message to our memcache + self.mcache.put(&msg_id, raw_message.clone()); + + // Dispatch the message to the user if we are subscribed to any of the topics + if self.mesh.contains_key(&message.topic) { + tracing::debug!("Sending received message to user"); + self.events + .push_back(ToSwarm::GenerateEvent(Event::Message { + propagation_source: *propagation_source, + message_id: msg_id.clone(), + message, + })); + } else { + tracing::debug!( + topic=%message.topic, + "Received message on a topic we are not subscribed to" + ); + return; + } + + // forward the message to mesh peers, if no validation is required + if !self.config.validate_messages() { + if self + .forward_msg( + &msg_id, + raw_message, + Some(propagation_source), + HashSet::new(), + ) + .is_err() + { + tracing::error!("Failed to forward message. Too large"); + } + tracing::debug!(message=%msg_id, "Completed message handling for message"); + } + } + + // Handles invalid messages received. + fn handle_invalid_message( + &mut self, + propagation_source: &PeerId, + raw_message: &RawMessage, + reject_reason: RejectReason, + ) { + if let Some((peer_score, .., gossip_promises)) = &mut self.peer_score { + if let Some(metrics) = self.metrics.as_mut() { + metrics.register_invalid_message(&raw_message.topic); + } + + if let Ok(message) = self.data_transform.inbound_transform(raw_message.clone()) { + let message_id = self.config.message_id(&message); + + peer_score.reject_message( + propagation_source, + &message_id, + &message.topic, + reject_reason, + ); + + gossip_promises.reject_message(&message_id, &reject_reason); + } else { + // The message is invalid, we reject it ignoring any gossip promises. If a peer is + // advertising this message via an IHAVE and it's invalid it will be double + // penalized, one for sending us an invalid and again for breaking a promise. + peer_score.reject_invalid_message(propagation_source, &raw_message.topic); + } + } + } + + /// Handles received subscriptions. + fn handle_received_subscriptions( + &mut self, + subscriptions: &[Subscription], + propagation_source: &PeerId, + ) { + tracing::debug!( + source=%propagation_source, + "Handling subscriptions: {:?}", + subscriptions, + ); + + let mut unsubscribed_peers = Vec::new(); + + let Some(peer) = self.connected_peers.get_mut(propagation_source) else { + tracing::error!( + peer=%propagation_source, + "Subscription by unknown peer" + ); + return; + }; + + // Collect potential graft topics for the peer. + let mut topics_to_graft = Vec::new(); + + // Notify the application about the subscription, after the grafts are sent. + let mut application_event = Vec::new(); + + let filtered_topics = match self + .subscription_filter + .filter_incoming_subscriptions(subscriptions, &peer.topics) + { + Ok(topics) => topics, + Err(s) => { + tracing::error!( + peer=%propagation_source, + "Subscription filter error: {}; ignoring RPC from peer", + s + ); + return; + } + }; + + for subscription in filtered_topics { + // get the peers from the mapping, or insert empty lists if the topic doesn't exist + let topic_hash = &subscription.topic_hash; + + match subscription.action { + SubscriptionAction::Subscribe => { + // add to the peer_topics mapping + if peer.topics.insert(topic_hash.clone()) { + tracing::debug!( + peer=%propagation_source, + topic=%topic_hash, + "SUBSCRIPTION: Adding gossip peer to topic" + ); + + if let Some(m) = self.metrics.as_mut() { + m.inc_topic_peers(topic_hash); + } + } + // if the mesh needs peers add the peer to the mesh + if !self.explicit_peers.contains(propagation_source) + && matches!(peer.kind, PeerKind::Gossipsubv1_1 | PeerKind::Gossipsub) + && !Self::score_below_threshold_from_scores( + &self.peer_score, + propagation_source, + |_| 0.0, + ) + .0 + && !self + .backoffs + .is_backoff_with_slack(topic_hash, propagation_source) + { + if let Some(peers) = self.mesh.get_mut(topic_hash) { + if peers.len() < self.config.mesh_n_low() + && peers.insert(*propagation_source) + { + tracing::debug!( + peer=%propagation_source, + topic=%topic_hash, + "SUBSCRIPTION: Adding peer to the mesh for topic" + ); + if let Some(m) = self.metrics.as_mut() { + m.peers_included(topic_hash, Inclusion::Subscribed, 1) + } + // send graft to the peer + tracing::debug!( + peer=%propagation_source, + topic=%topic_hash, + "Sending GRAFT to peer for topic" + ); + if let Some((peer_score, ..)) = &mut self.peer_score { + peer_score.graft(propagation_source, topic_hash.clone()); + } + topics_to_graft.push(topic_hash.clone()); + } + } + } + // generates a subscription event to be polled + application_event.push(ToSwarm::GenerateEvent(Event::Subscribed { + peer_id: *propagation_source, + topic: topic_hash.clone(), + })); + } + SubscriptionAction::Unsubscribe => { + // remove topic from the peer_topics mapping + if peer.topics.remove(topic_hash) { + tracing::debug!( + peer=%propagation_source, + topic=%topic_hash, + "SUBSCRIPTION: Removing gossip peer from topic" + ); + + if let Some(m) = self.metrics.as_mut() { + m.dec_topic_peers(topic_hash); + } + } + + unsubscribed_peers.push((*propagation_source, topic_hash.clone())); + // generate an unsubscribe event to be polled + application_event.push(ToSwarm::GenerateEvent(Event::Unsubscribed { + peer_id: *propagation_source, + topic: topic_hash.clone(), + })); + } + } + } + + // remove unsubscribed peers from the mesh if it exists + for (peer_id, topic_hash) in unsubscribed_peers { + self.remove_peer_from_mesh(&peer_id, &topic_hash, None, false, Churn::Unsub); + } + + // Potentially inform the handler if we have added this peer to a mesh for the first time. + let topics_joined = topics_to_graft.iter().collect::>(); + if !topics_joined.is_empty() { + peer_added_to_mesh( + *propagation_source, + topics_joined, + &self.mesh, + &mut self.events, + &self.connected_peers, + ); + } + + // If we need to send grafts to peer, do so immediately, rather than waiting for the + // heartbeat. + if let Some(peer) = &mut self.connected_peers.get_mut(propagation_source) { + for topic_hash in topics_to_graft.into_iter() { + peer.sender.graft(Graft { topic_hash }); + } + } else { + tracing::error!(peer = %propagation_source, + "Could not GRAFT, peer doesn't exist in connected peer list"); + } + + // Notify the application of the subscriptions + for event in application_event { + self.events.push_back(event); + } + + tracing::trace!( + source=%propagation_source, + "Completed handling subscriptions from source" + ); + } + + /// Applies penalties to peers that did not respond to our IWANT requests. + fn apply_iwant_penalties(&mut self) { + if let Some((peer_score, .., gossip_promises)) = &mut self.peer_score { + for (peer, count) in gossip_promises.get_broken_promises() { + peer_score.add_penalty(&peer, count); + if let Some(metrics) = self.metrics.as_mut() { + metrics.register_score_penalty(Penalty::BrokenPromise); + } + } + } + } + + /// Heartbeat function which shifts the memcache and updates the mesh. + fn heartbeat(&mut self) { + tracing::debug!("Starting heartbeat"); + let start = Instant::now(); + + // Every heartbeat we sample the send queues to add to our metrics. We do this intentionally + // before we add all the gossip from this heartbeat in order to gain a true measure of + // steady-state size of the queues. + if let Some(m) = &mut self.metrics { + for sender_queue in self.connected_peers.values_mut().map(|v| &v.sender) { + m.observe_priority_queue_size(sender_queue.priority_len()); + m.observe_non_priority_queue_size(sender_queue.non_priority_len()); + } + } + + self.heartbeat_ticks += 1; + + let mut to_graft = HashMap::new(); + let mut to_prune = HashMap::new(); + let mut no_px = HashSet::new(); + + // clean up expired backoffs + self.backoffs.heartbeat(); + + // clean up ihave counters + self.count_sent_iwant.clear(); + self.count_received_ihave.clear(); + + // apply iwant penalties + self.apply_iwant_penalties(); + + // check connections to explicit peers + if self.heartbeat_ticks % self.config.check_explicit_peers_ticks() == 0 { + for p in self.explicit_peers.clone() { + self.check_explicit_peer_connection(&p); + } + } + + // Cache the scores of all connected peers, and record metrics for current penalties. + let mut scores = HashMap::with_capacity(self.connected_peers.len()); + if let Some((peer_score, ..)) = &self.peer_score { + for peer_id in self.connected_peers.keys() { + scores + .entry(peer_id) + .or_insert_with(|| peer_score.metric_score(peer_id, self.metrics.as_mut())); + } + } + + // maintain the mesh for each topic + for (topic_hash, peers) in self.mesh.iter_mut() { + let explicit_peers = &self.explicit_peers; + let backoffs = &self.backoffs; + let outbound_peers = &self.outbound_peers; + + // drop all peers with negative score, without PX + // if there is at some point a stable retain method for BTreeSet the following can be + // written more efficiently with retain. + let mut to_remove_peers = Vec::new(); + for peer_id in peers.iter() { + let peer_score = *scores.get(peer_id).unwrap_or(&0.0); + + // Record the score per mesh + if let Some(metrics) = self.metrics.as_mut() { + metrics.observe_mesh_peers_score(topic_hash, peer_score); + } + + if peer_score < 0.0 { + tracing::debug!( + peer=%peer_id, + score=%peer_score, + topic=%topic_hash, + "HEARTBEAT: Prune peer with negative score" + ); + + let current_topic = to_prune.entry(*peer_id).or_insert_with(Vec::new); + current_topic.push(topic_hash.clone()); + no_px.insert(*peer_id); + to_remove_peers.push(*peer_id); + } + } + + if let Some(m) = self.metrics.as_mut() { + m.peers_removed(topic_hash, Churn::BadScore, to_remove_peers.len()) + } + + for peer_id in to_remove_peers { + peers.remove(&peer_id); + } + + // too little peers - add some + if peers.len() < self.config.mesh_n_low() { + tracing::debug!( + topic=%topic_hash, + "HEARTBEAT: Mesh low. Topic contains: {} needs: {}", + peers.len(), + self.config.mesh_n_low() + ); + // not enough peers - get mesh_n - current_length more + let desired_peers = self.config.mesh_n() - peers.len(); + let peer_list = + get_random_peers(&self.connected_peers, topic_hash, desired_peers, |peer| { + !peers.contains(peer) + && !explicit_peers.contains(peer) + && !backoffs.is_backoff_with_slack(topic_hash, peer) + && *scores.get(peer).unwrap_or(&0.0) >= 0.0 + }); + for peer in &peer_list { + let current_topic = to_graft.entry(*peer).or_insert_with(Vec::new); + current_topic.push(topic_hash.clone()); + } + // update the mesh + tracing::debug!("Updating mesh, new mesh: {:?}", peer_list); + if let Some(m) = self.metrics.as_mut() { + m.peers_included(topic_hash, Inclusion::Random, peer_list.len()) + } + peers.extend(peer_list); + } + + // too many peers - remove some + if peers.len() > self.config.mesh_n_high() { + tracing::debug!( + topic=%topic_hash, + "HEARTBEAT: Mesh high. Topic contains: {} needs: {}", + peers.len(), + self.config.mesh_n_high() + ); + let excess_peer_no = peers.len() - self.config.mesh_n(); + + // shuffle the peers and then sort by score ascending beginning with the worst + let mut rng = thread_rng(); + let mut shuffled = peers.iter().copied().collect::>(); + shuffled.shuffle(&mut rng); + shuffled.sort_by(|p1, p2| { + let score_p1 = *scores.get(p1).unwrap_or(&0.0); + let score_p2 = *scores.get(p2).unwrap_or(&0.0); + + score_p1.partial_cmp(&score_p2).unwrap_or(Ordering::Equal) + }); + // shuffle everything except the last retain_scores many peers (the best ones) + shuffled[..peers.len() - self.config.retain_scores()].shuffle(&mut rng); + + // count total number of outbound peers + let mut outbound = { + let outbound_peers = &self.outbound_peers; + shuffled + .iter() + .filter(|p| outbound_peers.contains(*p)) + .count() + }; + + // remove the first excess_peer_no allowed (by outbound restrictions) peers adding + // them to to_prune + let mut removed = 0; + for peer in shuffled { + if removed == excess_peer_no { + break; + } + if self.outbound_peers.contains(&peer) { + if outbound <= self.config.mesh_outbound_min() { + // do not remove anymore outbound peers + continue; + } + // an outbound peer gets removed + outbound -= 1; + } + + // remove the peer + peers.remove(&peer); + let current_topic = to_prune.entry(peer).or_insert_with(Vec::new); + current_topic.push(topic_hash.clone()); + removed += 1; + } + + if let Some(m) = self.metrics.as_mut() { + m.peers_removed(topic_hash, Churn::Excess, removed) + } + } + + // do we have enough outbound peers? + if peers.len() >= self.config.mesh_n_low() { + // count number of outbound peers we have + let outbound = { peers.iter().filter(|p| outbound_peers.contains(*p)).count() }; + + // if we have not enough outbound peers, graft to some new outbound peers + if outbound < self.config.mesh_outbound_min() { + let needed = self.config.mesh_outbound_min() - outbound; + let peer_list = + get_random_peers(&self.connected_peers, topic_hash, needed, |peer| { + !peers.contains(peer) + && !explicit_peers.contains(peer) + && !backoffs.is_backoff_with_slack(topic_hash, peer) + && *scores.get(peer).unwrap_or(&0.0) >= 0.0 + && outbound_peers.contains(peer) + }); + for peer in &peer_list { + let current_topic = to_graft.entry(*peer).or_insert_with(Vec::new); + current_topic.push(topic_hash.clone()); + } + // update the mesh + tracing::debug!("Updating mesh, new mesh: {:?}", peer_list); + if let Some(m) = self.metrics.as_mut() { + m.peers_included(topic_hash, Inclusion::Outbound, peer_list.len()) + } + peers.extend(peer_list); + } + } + + // should we try to improve the mesh with opportunistic grafting? + if self.heartbeat_ticks % self.config.opportunistic_graft_ticks() == 0 + && peers.len() > 1 + && self.peer_score.is_some() + { + if let Some((_, thresholds, _, _)) = &self.peer_score { + // Opportunistic grafting works as follows: we check the median score of peers + // in the mesh; if this score is below the opportunisticGraftThreshold, we + // select a few peers at random with score over the median. + // The intention is to (slowly) improve an underperforming mesh by introducing + // good scoring peers that may have been gossiping at us. This allows us to + // get out of sticky situations where we are stuck with poor peers and also + // recover from churn of good peers. + + // now compute the median peer score in the mesh + let mut peers_by_score: Vec<_> = peers.iter().collect(); + peers_by_score.sort_by(|p1, p2| { + let p1_score = *scores.get(p1).unwrap_or(&0.0); + let p2_score = *scores.get(p2).unwrap_or(&0.0); + p1_score.partial_cmp(&p2_score).unwrap_or(Equal) + }); + + let middle = peers_by_score.len() / 2; + let median = if peers_by_score.len() % 2 == 0 { + let sub_middle_peer = *peers_by_score + .get(middle - 1) + .expect("middle < vector length and middle > 0 since peers.len() > 0"); + let sub_middle_score = *scores.get(sub_middle_peer).unwrap_or(&0.0); + let middle_peer = + *peers_by_score.get(middle).expect("middle < vector length"); + let middle_score = *scores.get(middle_peer).unwrap_or(&0.0); + + (sub_middle_score + middle_score) * 0.5 + } else { + *scores + .get(*peers_by_score.get(middle).expect("middle < vector length")) + .unwrap_or(&0.0) + }; + + // if the median score is below the threshold, select a better peer (if any) and + // GRAFT + if median < thresholds.opportunistic_graft_threshold { + let peer_list = get_random_peers( + &self.connected_peers, + topic_hash, + self.config.opportunistic_graft_peers(), + |peer_id| { + !peers.contains(peer_id) + && !explicit_peers.contains(peer_id) + && !backoffs.is_backoff_with_slack(topic_hash, peer_id) + && *scores.get(peer_id).unwrap_or(&0.0) > median + }, + ); + for peer in &peer_list { + let current_topic = to_graft.entry(*peer).or_insert_with(Vec::new); + current_topic.push(topic_hash.clone()); + } + // update the mesh + tracing::debug!( + topic=%topic_hash, + "Opportunistically graft in topic with peers {:?}", + peer_list + ); + if let Some(m) = self.metrics.as_mut() { + m.peers_included(topic_hash, Inclusion::Random, peer_list.len()) + } + peers.extend(peer_list); + } + } + } + // Register the final count of peers in the mesh + if let Some(m) = self.metrics.as_mut() { + m.set_mesh_peers(topic_hash, peers.len()) + } + } + + // remove expired fanout topics + { + let fanout = &mut self.fanout; // help the borrow checker + let fanout_ttl = self.config.fanout_ttl(); + self.fanout_last_pub.retain(|topic_hash, last_pub_time| { + if *last_pub_time + fanout_ttl < Instant::now() { + tracing::debug!( + topic=%topic_hash, + "HEARTBEAT: Fanout topic removed due to timeout" + ); + fanout.remove(topic_hash); + return false; + } + true + }); + } + + // maintain fanout + // check if our peers are still a part of the topic + for (topic_hash, peers) in self.fanout.iter_mut() { + let mut to_remove_peers = Vec::new(); + let publish_threshold = match &self.peer_score { + Some((_, thresholds, _, _)) => thresholds.publish_threshold, + _ => 0.0, + }; + for peer_id in peers.iter() { + // is the peer still subscribed to the topic? + let peer_score = *scores.get(peer_id).unwrap_or(&0.0); + match self.connected_peers.get(peer_id) { + Some(peer) => { + if !peer.topics.contains(topic_hash) || peer_score < publish_threshold { + tracing::debug!( + topic=%topic_hash, + "HEARTBEAT: Peer removed from fanout for topic" + ); + to_remove_peers.push(*peer_id); + } + } + None => { + // remove if the peer has disconnected + to_remove_peers.push(*peer_id); + } + } + } + for to_remove in to_remove_peers { + peers.remove(&to_remove); + } + + // not enough peers + if peers.len() < self.config.mesh_n() { + tracing::debug!( + "HEARTBEAT: Fanout low. Contains: {:?} needs: {:?}", + peers.len(), + self.config.mesh_n() + ); + let needed_peers = self.config.mesh_n() - peers.len(); + let explicit_peers = &self.explicit_peers; + let new_peers = + get_random_peers(&self.connected_peers, topic_hash, needed_peers, |peer_id| { + !peers.contains(peer_id) + && !explicit_peers.contains(peer_id) + && *scores.get(peer_id).unwrap_or(&0.0) < publish_threshold + }); + peers.extend(new_peers); + } + } + + if self.peer_score.is_some() { + tracing::trace!("Mesh message deliveries: {:?}", { + self.mesh + .iter() + .map(|(t, peers)| { + ( + t.clone(), + peers + .iter() + .map(|p| { + ( + *p, + self.peer_score + .as_ref() + .expect("peer_score.is_some()") + .0 + .mesh_message_deliveries(p, t) + .unwrap_or(0.0), + ) + }) + .collect::>(), + ) + }) + .collect::>>() + }) + } + + self.emit_gossip(); + + // send graft/prunes + if !to_graft.is_empty() | !to_prune.is_empty() { + self.send_graft_prune(to_graft, to_prune, no_px); + } + + // shift the memcache + self.mcache.shift(); + + // Report expired messages + for (peer_id, failed_messages) in self.failed_messages.drain() { + tracing::debug!("Peer couldn't consume messages: {:?}", failed_messages); + self.events + .push_back(ToSwarm::GenerateEvent(Event::SlowPeer { + peer_id, + failed_messages, + })); + } + self.failed_messages.shrink_to_fit(); + + tracing::debug!("Completed Heartbeat"); + if let Some(metrics) = self.metrics.as_mut() { + let duration = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX); + metrics.observe_heartbeat_duration(duration); + } + } + + /// Emits gossip - Send IHAVE messages to a random set of gossip peers. This is applied to mesh + /// and fanout peers + fn emit_gossip(&mut self) { + let mut rng = thread_rng(); + for (topic_hash, peers) in self.mesh.iter().chain(self.fanout.iter()) { + let mut message_ids = self.mcache.get_gossip_message_ids(topic_hash); + if message_ids.is_empty() { + continue; + } + + // if we are emitting more than GossipSubMaxIHaveLength message_ids, truncate the list + if message_ids.len() > self.config.max_ihave_length() { + // we do the truncation (with shuffling) per peer below + tracing::debug!( + "too many messages for gossip; will truncate IHAVE list ({} messages)", + message_ids.len() + ); + } else { + // shuffle to emit in random order + message_ids.shuffle(&mut rng); + } + + // dynamic number of peers to gossip based on `gossip_factor` with minimum `gossip_lazy` + let n_map = |m| { + max( + self.config.gossip_lazy(), + (self.config.gossip_factor() * m as f64) as usize, + ) + }; + // get gossip_lazy random peers + let to_msg_peers = + get_random_peers_dynamic(&self.connected_peers, topic_hash, n_map, |peer| { + !peers.contains(peer) + && !self.explicit_peers.contains(peer) + && !self.score_below_threshold(peer, |ts| ts.gossip_threshold).0 + }); + + tracing::debug!("Gossiping IHAVE to {} peers", to_msg_peers.len()); + + for peer_id in to_msg_peers { + let mut peer_message_ids = message_ids.clone(); + + if peer_message_ids.len() > self.config.max_ihave_length() { + // We do this per peer so that we emit a different set for each peer. + // we have enough redundancy in the system that this will significantly increase + // the message coverage when we do truncate. + peer_message_ids.partial_shuffle(&mut rng, self.config.max_ihave_length()); + peer_message_ids.truncate(self.config.max_ihave_length()); + } + + // send an IHAVE message + if let Some(peer) = &mut self.connected_peers.get_mut(&peer_id) { + if peer + .sender + .ihave(IHave { + topic_hash: topic_hash.clone(), + message_ids: peer_message_ids, + }) + .is_err() + { + tracing::warn!(peer=%peer_id, "Send Queue full. Could not send IHAVE"); + + if let Some((peer_score, ..)) = &mut self.peer_score { + peer_score.failed_message_slow_peer(&peer_id); + } + // Increment failed message count + self.failed_messages + .entry(peer_id) + .or_default() + .non_priority += 1; + } + } else { + tracing::error!(peer = %peer_id, + "Could not IHAVE, peer doesn't exist in connected peer list"); + } + } + } + } + + /// Handles multiple GRAFT/PRUNE messages and coalesces them into chunked gossip control + /// messages. + fn send_graft_prune( + &mut self, + to_graft: HashMap>, + mut to_prune: HashMap>, + no_px: HashSet, + ) { + // handle the grafts and overlapping prunes per peer + for (peer_id, topics) in to_graft.into_iter() { + for topic in &topics { + // inform scoring of graft + if let Some((peer_score, ..)) = &mut self.peer_score { + peer_score.graft(&peer_id, topic.clone()); + } + + // inform the handler of the peer being added to the mesh + // If the peer did not previously exist in any mesh, inform the handler + peer_added_to_mesh( + peer_id, + vec![topic], + &self.mesh, + &mut self.events, + &self.connected_peers, + ); + } + + // If there are prunes associated with the same peer add them. + // NOTE: In this case a peer has been added to a topic mesh, and removed from another. + // It therefore must be in at least one mesh and we do not need to inform the handler + // of its removal from another. + + // send the control messages + let mut sender = match self.connected_peers.get_mut(&peer_id) { + Some(connected_peer) => connected_peer.sender.clone(), + None => { + tracing::error!(peer_id = %peer_id, "Peer non-existent when sending graft/prune"); + return; + } + }; + + // The following prunes are not due to unsubscribing. + let prunes = to_prune + .remove(&peer_id) + .into_iter() + .flatten() + .map(|topic_hash| { + self.make_prune( + &topic_hash, + &peer_id, + self.config.do_px() && !no_px.contains(&peer_id), + false, + ) + }); + + for topic_hash in topics { + sender.graft(Graft { + topic_hash: topic_hash.clone(), + }); + } + + for prune in prunes { + sender.prune(prune); + } + } + + // handle the remaining prunes + // The following prunes are not due to unsubscribing. + for (peer_id, topics) in to_prune.iter() { + for topic_hash in topics { + let prune = self.make_prune( + topic_hash, + peer_id, + self.config.do_px() && !no_px.contains(peer_id), + false, + ); + if let Some(peer) = self.connected_peers.get_mut(peer_id) { + peer.sender.prune(prune); + } else { + tracing::error!(peer = %peer_id, + "Could not PRUNE, peer doesn't exist in connected peer list"); + } + + // inform the handler + peer_removed_from_mesh( + *peer_id, + topic_hash, + &self.mesh, + &mut self.events, + &self.connected_peers, + ); + } + } + } + + /// Helper function which forwards a message to mesh\[topic\] peers. + /// + /// Returns true if at least one peer was messaged. + fn forward_msg( + &mut self, + msg_id: &MessageId, + message: RawMessage, + propagation_source: Option<&PeerId>, + originating_peers: HashSet, + ) -> Result { + // message is fully validated inform peer_score + if let Some((peer_score, ..)) = &mut self.peer_score { + if let Some(peer) = propagation_source { + peer_score.deliver_message(peer, msg_id, &message.topic); + } + } + + tracing::debug!(message=%msg_id, "Forwarding message"); + let mut recipient_peers = HashSet::new(); + + // Populate the recipient peers mapping + + // Add explicit peers + for peer_id in &self.explicit_peers { + if let Some(peer) = self.connected_peers.get(peer_id) { + if Some(peer_id) != propagation_source + && !originating_peers.contains(peer_id) + && Some(peer_id) != message.source.as_ref() + && peer.topics.contains(&message.topic) + { + recipient_peers.insert(*peer_id); + } + } + } + + // add mesh peers + let topic = &message.topic; + // mesh + if let Some(mesh_peers) = self.mesh.get(topic) { + for peer_id in mesh_peers { + if Some(peer_id) != propagation_source + && !originating_peers.contains(peer_id) + && Some(peer_id) != message.source.as_ref() + { + recipient_peers.insert(*peer_id); + } + } + } + + // forward the message to peers + if !recipient_peers.is_empty() { + for peer_id in recipient_peers.iter() { + if let Some(peer) = self.connected_peers.get_mut(peer_id) { + tracing::debug!(%peer_id, message=%msg_id, "Sending message to peer"); + if peer + .sender + .forward( + message.clone(), + self.config.forward_queue_duration(), + self.metrics.as_mut(), + ) + .is_err() + { + // Downscore the peer + if let Some((peer_score, ..)) = &mut self.peer_score { + peer_score.failed_message_slow_peer(peer_id); + } + // Increment the failed message count + self.failed_messages + .entry(*peer_id) + .or_default() + .non_priority += 1; + } + } else { + tracing::error!(peer = %peer_id, + "Could not FORWARD, peer doesn't exist in connected peer list"); + } + } + tracing::debug!("Completed forwarding message"); + Ok(true) + } else { + Ok(false) + } + } + + /// Constructs a [`RawMessage`] performing message signing if required. + pub(crate) fn build_raw_message( + &mut self, + topic: TopicHash, + data: Vec, + ) -> Result { + match &mut self.publish_config { + PublishConfig::Signing { + ref keypair, + author, + inline_key, + last_seq_no, + } => { + let sequence_number = last_seq_no.next(); + + let signature = { + let message = proto::Message { + from: Some(author.to_bytes()), + data: Some(data.clone()), + seqno: Some(sequence_number.to_be_bytes().to_vec()), + topic: topic.clone().into_string(), + signature: None, + key: None, + }; + + let mut buf = Vec::with_capacity(message.get_size()); + let mut writer = Writer::new(&mut buf); + + message + .write_message(&mut writer) + .expect("Encoding to succeed"); + + // the signature is over the bytes "libp2p-pubsub:" + let mut signature_bytes = SIGNING_PREFIX.to_vec(); + signature_bytes.extend_from_slice(&buf); + Some(keypair.sign(&signature_bytes)?) + }; + + Ok(RawMessage { + source: Some(*author), + data, + // To be interoperable with the go-implementation this is treated as a 64-bit + // big-endian uint. + sequence_number: Some(sequence_number), + topic, + signature, + key: inline_key.clone(), + validated: true, // all published messages are valid + }) + } + PublishConfig::Author(peer_id) => { + Ok(RawMessage { + source: Some(*peer_id), + data, + // To be interoperable with the go-implementation this is treated as a 64-bit + // big-endian uint. + sequence_number: Some(rand::random()), + topic, + signature: None, + key: None, + validated: true, // all published messages are valid + }) + } + PublishConfig::RandomAuthor => { + Ok(RawMessage { + source: Some(PeerId::random()), + data, + // To be interoperable with the go-implementation this is treated as a 64-bit + // big-endian uint. + sequence_number: Some(rand::random()), + topic, + signature: None, + key: None, + validated: true, // all published messages are valid + }) + } + PublishConfig::Anonymous => { + Ok(RawMessage { + source: None, + data, + // To be interoperable with the go-implementation this is treated as a 64-bit + // big-endian uint. + sequence_number: None, + topic, + signature: None, + key: None, + validated: true, // all published messages are valid + }) + } + } + } + + fn on_connection_established( + &mut self, + ConnectionEstablished { + peer_id, + endpoint, + other_established, + .. + }: ConnectionEstablished, + ) { + // Diverging from the go implementation we only want to consider a peer as outbound peer + // if its first connection is outbound. + + if endpoint.is_dialer() && other_established == 0 && !self.px_peers.contains(&peer_id) { + // The first connection is outbound and it is not a peer from peer exchange => mark + // it as outbound peer + self.outbound_peers.insert(peer_id); + } + + // Add the IP to the peer scoring system + if let Some((peer_score, ..)) = &mut self.peer_score { + if let Some(ip) = get_ip_addr(endpoint.get_remote_address()) { + peer_score.add_ip(&peer_id, ip); + } else { + tracing::trace!( + peer=%peer_id, + "Couldn't extract ip from endpoint of peer with endpoint {:?}", + endpoint + ) + } + } + + if other_established > 0 { + return; // Not our first connection to this peer, hence nothing to do. + } + + if let Some((peer_score, ..)) = &mut self.peer_score { + peer_score.add_peer(peer_id); + } + + // Ignore connections from blacklisted peers. + if self.blacklisted_peers.contains(&peer_id) { + tracing::debug!(peer=%peer_id, "Ignoring connection from blacklisted peer"); + return; + } + + tracing::debug!(peer=%peer_id, "New peer connected"); + // We need to send our subscriptions to the newly-connected node. + if let Some(peer) = self.connected_peers.get_mut(&peer_id) { + for topic_hash in self.mesh.clone().into_keys() { + peer.sender.subscribe(topic_hash); + } + } else { + tracing::error!(peer = %peer_id, + "Could not SUBSCRIBE, peer doesn't exist in connected peer list"); + } + } + + fn on_connection_closed( + &mut self, + ConnectionClosed { + peer_id, + connection_id, + endpoint, + remaining_established, + .. + }: ConnectionClosed, + ) { + // Remove IP from peer scoring system + if let Some((peer_score, ..)) = &mut self.peer_score { + if let Some(ip) = get_ip_addr(endpoint.get_remote_address()) { + peer_score.remove_ip(&peer_id, &ip); + } else { + tracing::trace!( + peer=%peer_id, + "Couldn't extract ip from endpoint of peer with endpoint {:?}", + endpoint + ) + } + } + + if remaining_established != 0 { + // Remove the connection from the list + if let Some(peer) = self.connected_peers.get_mut(&peer_id) { + let index = peer + .connections + .iter() + .position(|v| v == &connection_id) + .expect("Previously established connection to peer must be present"); + peer.connections.remove(index); + + // If there are more connections and this peer is in a mesh, inform the first connection + // handler. + if !peer.connections.is_empty() { + for topic in &peer.topics { + if let Some(mesh_peers) = self.mesh.get(topic) { + if mesh_peers.contains(&peer_id) { + self.events.push_back(ToSwarm::NotifyHandler { + peer_id, + event: HandlerIn::JoinedMesh, + handler: NotifyHandler::One(peer.connections[0]), + }); + break; + } + } + } + } + } + } else { + // remove from mesh, topic_peers, peer_topic and the fanout + tracing::debug!(peer=%peer_id, "Peer disconnected"); + + let Some(connected_peer) = self.connected_peers.get(&peer_id) else { + tracing::error!(peer_id = %peer_id, "Peer non-existent when handling disconnection"); + return; + }; + + // remove peer from all mappings + for topic in &connected_peer.topics { + // check the mesh for the topic + if let Some(mesh_peers) = self.mesh.get_mut(topic) { + // check if the peer is in the mesh and remove it + if mesh_peers.remove(&peer_id) { + if let Some(m) = self.metrics.as_mut() { + m.peers_removed(topic, Churn::Dc, 1); + m.set_mesh_peers(topic, mesh_peers.len()); + } + }; + } + + if let Some(m) = self.metrics.as_mut() { + m.dec_topic_peers(topic); + } + + // remove from fanout + self.fanout + .get_mut(topic) + .map(|peers| peers.remove(&peer_id)); + } + + // Forget px and outbound status for this peer + self.px_peers.remove(&peer_id); + self.outbound_peers.remove(&peer_id); + + // If metrics are enabled, register the disconnection of a peer based on its protocol. + if let Some(metrics) = self.metrics.as_mut() { + metrics.peer_protocol_disconnected(connected_peer.kind.clone()); + } + + self.connected_peers.remove(&peer_id); + + if let Some((peer_score, ..)) = &mut self.peer_score { + peer_score.remove_peer(&peer_id); + } + } + } + + fn on_address_change( + &mut self, + AddressChange { + peer_id, + old: endpoint_old, + new: endpoint_new, + .. + }: AddressChange, + ) { + // Exchange IP in peer scoring system + if let Some((peer_score, ..)) = &mut self.peer_score { + if let Some(ip) = get_ip_addr(endpoint_old.get_remote_address()) { + peer_score.remove_ip(&peer_id, &ip); + } else { + tracing::trace!( + peer=%&peer_id, + "Couldn't extract ip from endpoint of peer with endpoint {:?}", + endpoint_old + ) + } + if let Some(ip) = get_ip_addr(endpoint_new.get_remote_address()) { + peer_score.add_ip(&peer_id, ip); + } else { + tracing::trace!( + peer=%peer_id, + "Couldn't extract ip from endpoint of peer with endpoint {:?}", + endpoint_new + ) + } + } + } +} + +fn get_ip_addr(addr: &Multiaddr) -> Option { + addr.iter().find_map(|p| match p { + Ip4(addr) => Some(IpAddr::V4(addr)), + Ip6(addr) => Some(IpAddr::V6(addr)), + _ => None, + }) +} + +impl NetworkBehaviour for Behaviour +where + C: Send + 'static + DataTransform, + F: Send + 'static + TopicSubscriptionFilter, +{ + type ConnectionHandler = Handler; + type ToSwarm = Event; + + fn handle_established_inbound_connection( + &mut self, + connection_id: ConnectionId, + peer_id: PeerId, + _: &Multiaddr, + _: &Multiaddr, + ) -> Result, ConnectionDenied> { + // By default we assume a peer is only a floodsub peer. + // + // The protocol negotiation occurs once a message is sent/received. Once this happens we + // update the type of peer that this is in order to determine which kind of routing should + // occur. + let connected_peer = self + .connected_peers + .entry(peer_id) + .or_insert(PeerConnections { + kind: PeerKind::Floodsub, + connections: vec![], + sender: RpcSender::new(self.config.connection_handler_queue_len()), + topics: Default::default(), + }); + // Add the new connection + connected_peer.connections.push(connection_id); + + Ok(Handler::new( + self.config.protocol_config(), + connected_peer.sender.new_receiver(), + )) + } + + fn handle_established_outbound_connection( + &mut self, + connection_id: ConnectionId, + peer_id: PeerId, + _: &Multiaddr, + _: Endpoint, + ) -> Result, ConnectionDenied> { + // By default we assume a peer is only a floodsub peer. + // + // The protocol negotiation occurs once a message is sent/received. Once this happens we + // update the type of peer that this is in order to determine which kind of routing should + // occur. + let connected_peer = self + .connected_peers + .entry(peer_id) + .or_insert(PeerConnections { + kind: PeerKind::Floodsub, + connections: vec![], + sender: RpcSender::new(self.config.connection_handler_queue_len()), + topics: Default::default(), + }); + // Add the new connection + connected_peer.connections.push(connection_id); + + Ok(Handler::new( + self.config.protocol_config(), + connected_peer.sender.new_receiver(), + )) + } + + fn on_connection_handler_event( + &mut self, + propagation_source: PeerId, + _connection_id: ConnectionId, + handler_event: THandlerOutEvent, + ) { + match handler_event { + HandlerEvent::PeerKind(kind) => { + // We have identified the protocol this peer is using + + if let Some(metrics) = self.metrics.as_mut() { + metrics.peer_protocol_connected(kind.clone()); + } + + if let PeerKind::NotSupported = kind { + tracing::debug!( + peer=%propagation_source, + "Peer does not support gossipsub protocols" + ); + self.events + .push_back(ToSwarm::GenerateEvent(Event::GossipsubNotSupported { + peer_id: propagation_source, + })); + } else if let Some(conn) = self.connected_peers.get_mut(&propagation_source) { + // Only change the value if the old value is Floodsub (the default set in + // `NetworkBehaviour::on_event` with FromSwarm::ConnectionEstablished). + // All other PeerKind changes are ignored. + tracing::debug!( + peer=%propagation_source, + peer_type=%kind, + "New peer type found for peer" + ); + if let PeerKind::Floodsub = conn.kind { + conn.kind = kind; + } + } + } + HandlerEvent::MessageDropped(rpc) => { + // Account for this in the scoring logic + if let Some((peer_score, _, _, _)) = &mut self.peer_score { + peer_score.failed_message_slow_peer(&propagation_source); + } + + // Keep track of expired messages for the application layer. + match rpc { + RpcOut::Publish { .. } => { + self.failed_messages + .entry(propagation_source) + .or_default() + .publish += 1; + } + RpcOut::Forward { .. } => { + self.failed_messages + .entry(propagation_source) + .or_default() + .forward += 1; + } + _ => {} // + } + + // Record metrics on the failure. + if let Some(metrics) = self.metrics.as_mut() { + match rpc { + RpcOut::Publish { message, .. } => { + metrics.publish_msg_dropped(&message.topic); + } + RpcOut::Forward { message, .. } => { + metrics.forward_msg_dropped(&message.topic); + } + _ => {} + } + } + } + HandlerEvent::Message { + rpc, + invalid_messages, + } => { + // Handle the gossipsub RPC + + // Handle subscriptions + // Update connected peers topics + if !rpc.subscriptions.is_empty() { + self.handle_received_subscriptions(&rpc.subscriptions, &propagation_source); + } + + // Check if peer is graylisted in which case we ignore the event + if let (true, _) = + self.score_below_threshold(&propagation_source, |pst| pst.graylist_threshold) + { + tracing::debug!(peer=%propagation_source, "RPC Dropped from greylisted peer"); + return; + } + + // Handle any invalid messages from this peer + if self.peer_score.is_some() { + for (raw_message, validation_error) in invalid_messages { + self.handle_invalid_message( + &propagation_source, + &raw_message, + RejectReason::ValidationError(validation_error), + ) + } + } else { + // log the invalid messages + for (message, validation_error) in invalid_messages { + tracing::warn!( + peer=%propagation_source, + source=?message.source, + "Invalid message from peer. Reason: {:?}", + validation_error, + ); + } + } + + // Handle messages + for (count, raw_message) in rpc.messages.into_iter().enumerate() { + // Only process the amount of messages the configuration allows. + if self.config.max_messages_per_rpc().is_some() + && Some(count) >= self.config.max_messages_per_rpc() + { + tracing::warn!("Received more messages than permitted. Ignoring further messages. Processed: {}", count); + break; + } + self.handle_received_message(raw_message, &propagation_source); + } + + // Handle control messages + // group some control messages, this minimises SendEvents (code is simplified to handle each event at a time however) + let mut ihave_msgs = vec![]; + let mut graft_msgs = vec![]; + let mut prune_msgs = vec![]; + for control_msg in rpc.control_msgs { + match control_msg { + ControlAction::IHave(IHave { + topic_hash, + message_ids, + }) => { + ihave_msgs.push((topic_hash, message_ids)); + } + ControlAction::IWant(IWant { message_ids }) => { + self.handle_iwant(&propagation_source, message_ids) + } + ControlAction::Graft(Graft { topic_hash }) => graft_msgs.push(topic_hash), + ControlAction::Prune(Prune { + topic_hash, + peers, + backoff, + }) => prune_msgs.push((topic_hash, peers, backoff)), + } + } + if !ihave_msgs.is_empty() { + self.handle_ihave(&propagation_source, ihave_msgs); + } + if !graft_msgs.is_empty() { + self.handle_graft(&propagation_source, graft_msgs); + } + if !prune_msgs.is_empty() { + self.handle_prune(&propagation_source, prune_msgs); + } + } + } + } + + #[tracing::instrument(level = "trace", name = "NetworkBehaviour::poll", skip(self, cx))] + fn poll( + &mut self, + cx: &mut Context<'_>, + ) -> Poll>> { + if let Some(event) = self.events.pop_front() { + return Poll::Ready(event); + } + + // update scores + if let Some((peer_score, _, interval, _)) = &mut self.peer_score { + while let Poll::Ready(Some(_)) = interval.poll_next_unpin(cx) { + peer_score.refresh_scores(); + } + } + + while let Poll::Ready(Some(_)) = self.heartbeat.poll_next_unpin(cx) { + self.heartbeat(); + } + + Poll::Pending + } + + fn on_swarm_event(&mut self, event: FromSwarm) { + match event { + FromSwarm::ConnectionEstablished(connection_established) => { + self.on_connection_established(connection_established) + } + FromSwarm::ConnectionClosed(connection_closed) => { + self.on_connection_closed(connection_closed) + } + FromSwarm::AddressChange(address_change) => self.on_address_change(address_change), + _ => {} + } + } +} + +/// This is called when peers are added to any mesh. It checks if the peer existed +/// in any other mesh. If this is the first mesh they have joined, it queues a message to notify +/// the appropriate connection handler to maintain a connection. +fn peer_added_to_mesh( + peer_id: PeerId, + new_topics: Vec<&TopicHash>, + mesh: &HashMap>, + events: &mut VecDeque>, + connections: &HashMap, +) { + // Ensure there is an active connection + let connection_id = match connections.get(&peer_id) { + Some(p) => p + .connections + .first() + .expect("There should be at least one connection to a peer."), + None => { + tracing::error!(peer_id=%peer_id, "Peer not existent when added to the mesh"); + return; + } + }; + + if let Some(peer) = connections.get(&peer_id) { + for topic in &peer.topics { + if !new_topics.contains(&topic) { + if let Some(mesh_peers) = mesh.get(topic) { + if mesh_peers.contains(&peer_id) { + // the peer is already in a mesh for another topic + return; + } + } + } + } + } + // This is the first mesh the peer has joined, inform the handler + events.push_back(ToSwarm::NotifyHandler { + peer_id, + event: HandlerIn::JoinedMesh, + handler: NotifyHandler::One(*connection_id), + }); +} + +/// This is called when peers are removed from a mesh. It checks if the peer exists +/// in any other mesh. If this is the last mesh they have joined, we return true, in order to +/// notify the handler to no longer maintain a connection. +fn peer_removed_from_mesh( + peer_id: PeerId, + old_topic: &TopicHash, + mesh: &HashMap>, + events: &mut VecDeque>, + connections: &HashMap, +) { + // Ensure there is an active connection + let connection_id = match connections.get(&peer_id) { + Some(p) => p + .connections + .first() + .expect("There should be at least one connection to a peer."), + None => { + tracing::error!(peer_id=%peer_id, "Peer not existent when removed from mesh"); + return; + } + }; + + if let Some(peer) = connections.get(&peer_id) { + for topic in &peer.topics { + if topic != old_topic { + if let Some(mesh_peers) = mesh.get(topic) { + if mesh_peers.contains(&peer_id) { + // the peer exists in another mesh still + return; + } + } + } + } + } + // The peer is not in any other mesh, inform the handler + events.push_back(ToSwarm::NotifyHandler { + peer_id, + event: HandlerIn::LeftMesh, + handler: NotifyHandler::One(*connection_id), + }); +} + +/// Helper function to get a subset of random gossipsub peers for a `topic_hash` +/// filtered by the function `f`. The number of peers to get equals the output of `n_map` +/// that gets as input the number of filtered peers. +fn get_random_peers_dynamic( + connected_peers: &HashMap, + topic_hash: &TopicHash, + // maps the number of total peers to the number of selected peers + n_map: impl Fn(usize) -> usize, + mut f: impl FnMut(&PeerId) -> bool, +) -> BTreeSet { + let mut gossip_peers = connected_peers + .iter() + .filter(|(_, p)| p.topics.contains(topic_hash)) + .filter(|(peer_id, _)| f(peer_id)) + .filter(|(_, p)| p.kind == PeerKind::Gossipsub || p.kind == PeerKind::Gossipsubv1_1) + .map(|(peer_id, _)| *peer_id) + .collect::>(); + + // if we have less than needed, return them + let n = n_map(gossip_peers.len()); + if gossip_peers.len() <= n { + tracing::debug!("RANDOM PEERS: Got {:?} peers", gossip_peers.len()); + return gossip_peers.into_iter().collect(); + } + + // we have more peers than needed, shuffle them and return n of them + let mut rng = thread_rng(); + gossip_peers.partial_shuffle(&mut rng, n); + + tracing::debug!("RANDOM PEERS: Got {:?} peers", n); + + gossip_peers.into_iter().take(n).collect() +} + +/// Helper function to get a set of `n` random gossipsub peers for a `topic_hash` +/// filtered by the function `f`. +fn get_random_peers( + connected_peers: &HashMap, + topic_hash: &TopicHash, + n: usize, + f: impl FnMut(&PeerId) -> bool, +) -> BTreeSet { + get_random_peers_dynamic(connected_peers, topic_hash, |_| n, f) +} + +/// Validates the combination of signing, privacy and message validation to ensure the +/// configuration will not reject published messages. +fn validate_config( + authenticity: &MessageAuthenticity, + validation_mode: &ValidationMode, +) -> Result<(), &'static str> { + match validation_mode { + ValidationMode::Anonymous => { + if authenticity.is_signing() { + return Err("Cannot enable message signing with an Anonymous validation mode. Consider changing either the ValidationMode or MessageAuthenticity"); + } + + if !authenticity.is_anonymous() { + return Err("Published messages contain an author but incoming messages with an author will be rejected. Consider adjusting the validation or privacy settings in the config"); + } + } + ValidationMode::Strict => { + if !authenticity.is_signing() { + return Err( + "Messages will be + published unsigned and incoming unsigned messages will be rejected. Consider adjusting + the validation or privacy settings in the config" + ); + } + } + _ => {} + } + Ok(()) +} + +impl fmt::Debug for Behaviour { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Behaviour") + .field("config", &self.config) + .field("events", &self.events.len()) + .field("publish_config", &self.publish_config) + .field("mesh", &self.mesh) + .field("fanout", &self.fanout) + .field("fanout_last_pub", &self.fanout_last_pub) + .field("mcache", &self.mcache) + .field("heartbeat", &self.heartbeat) + .finish() + } +} + +impl fmt::Debug for PublishConfig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PublishConfig::Signing { author, .. } => { + f.write_fmt(format_args!("PublishConfig::Signing({author})")) + } + PublishConfig::Author(author) => { + f.write_fmt(format_args!("PublishConfig::Author({author})")) + } + PublishConfig::RandomAuthor => f.write_fmt(format_args!("PublishConfig::RandomAuthor")), + PublishConfig::Anonymous => f.write_fmt(format_args!("PublishConfig::Anonymous")), + } + } +} diff --git a/beacon_node/lighthouse_network/gossipsub/src/behaviour/tests.rs b/beacon_node/lighthouse_network/gossipsub/src/behaviour/tests.rs new file mode 100644 index 0000000000..2af0199ec9 --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/behaviour/tests.rs @@ -0,0 +1,5238 @@ +// Copyright 2020 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Collection of tests for the gossipsub network behaviour + +use super::*; +use crate::subscription_filter::WhitelistSubscriptionFilter; +use crate::types::RpcReceiver; +use crate::{config::ConfigBuilder, types::Rpc, IdentTopic as Topic}; +use byteorder::{BigEndian, ByteOrder}; +use libp2p::core::ConnectedPoint; +use rand::Rng; +use std::net::Ipv4Addr; +use std::thread::sleep; + +#[derive(Default, Debug)] +struct InjectNodes +// TODO: remove trait bound Default when this issue is fixed: +// https://github.com/colin-kiegel/rust-derive-builder/issues/93 +where + D: DataTransform + Default + Clone + Send + 'static, + F: TopicSubscriptionFilter + Clone + Default + Send + 'static, +{ + peer_no: usize, + topics: Vec, + to_subscribe: bool, + gs_config: Config, + explicit: usize, + outbound: usize, + scoring: Option<(PeerScoreParams, PeerScoreThresholds)>, + data_transform: D, + subscription_filter: F, +} + +impl InjectNodes +where + D: DataTransform + Default + Clone + Send + 'static, + F: TopicSubscriptionFilter + Clone + Default + Send + 'static, +{ + #[allow(clippy::type_complexity)] + pub(crate) fn create_network( + self, + ) -> ( + Behaviour, + Vec, + HashMap, + Vec, + ) { + let keypair = libp2p::identity::Keypair::generate_ed25519(); + // create a gossipsub struct + let mut gs: Behaviour = Behaviour::new_with_subscription_filter_and_transform( + MessageAuthenticity::Signed(keypair), + self.gs_config, + None, + self.subscription_filter, + self.data_transform, + ) + .unwrap(); + + if let Some((scoring_params, scoring_thresholds)) = self.scoring { + gs.with_peer_score(scoring_params, scoring_thresholds) + .unwrap(); + } + + let mut topic_hashes = vec![]; + + // subscribe to the topics + for t in self.topics { + let topic = Topic::new(t); + gs.subscribe(&topic).unwrap(); + topic_hashes.push(topic.hash().clone()); + } + + // build and connect peer_no random peers + let mut peers = vec![]; + let mut receivers = HashMap::new(); + + let empty = vec![]; + for i in 0..self.peer_no { + let (peer, receiver) = add_peer( + &mut gs, + if self.to_subscribe { + &topic_hashes + } else { + &empty + }, + i < self.outbound, + i < self.explicit, + ); + peers.push(peer); + receivers.insert(peer, receiver); + } + + (gs, peers, receivers, topic_hashes) + } + + fn peer_no(mut self, peer_no: usize) -> Self { + self.peer_no = peer_no; + self + } + + fn topics(mut self, topics: Vec) -> Self { + self.topics = topics; + self + } + + #[allow(clippy::wrong_self_convention)] + fn to_subscribe(mut self, to_subscribe: bool) -> Self { + self.to_subscribe = to_subscribe; + self + } + + fn gs_config(mut self, gs_config: Config) -> Self { + self.gs_config = gs_config; + self + } + + fn explicit(mut self, explicit: usize) -> Self { + self.explicit = explicit; + self + } + + fn outbound(mut self, outbound: usize) -> Self { + self.outbound = outbound; + self + } + + fn scoring(mut self, scoring: Option<(PeerScoreParams, PeerScoreThresholds)>) -> Self { + self.scoring = scoring; + self + } + + fn subscription_filter(mut self, subscription_filter: F) -> Self { + self.subscription_filter = subscription_filter; + self + } +} + +fn inject_nodes() -> InjectNodes +where + D: DataTransform + Default + Clone + Send + 'static, + F: TopicSubscriptionFilter + Clone + Default + Send + 'static, +{ + InjectNodes::default() +} + +fn inject_nodes1() -> InjectNodes { + InjectNodes::::default() +} + +// helper functions for testing + +fn add_peer( + gs: &mut Behaviour, + topic_hashes: &[TopicHash], + outbound: bool, + explicit: bool, +) -> (PeerId, RpcReceiver) +where + D: DataTransform + Default + Clone + Send + 'static, + F: TopicSubscriptionFilter + Clone + Default + Send + 'static, +{ + add_peer_with_addr(gs, topic_hashes, outbound, explicit, Multiaddr::empty()) +} + +fn add_peer_with_addr( + gs: &mut Behaviour, + topic_hashes: &[TopicHash], + outbound: bool, + explicit: bool, + address: Multiaddr, +) -> (PeerId, RpcReceiver) +where + D: DataTransform + Default + Clone + Send + 'static, + F: TopicSubscriptionFilter + Clone + Default + Send + 'static, +{ + add_peer_with_addr_and_kind( + gs, + topic_hashes, + outbound, + explicit, + address, + Some(PeerKind::Gossipsubv1_1), + ) +} + +fn add_peer_with_addr_and_kind( + gs: &mut Behaviour, + topic_hashes: &[TopicHash], + outbound: bool, + explicit: bool, + address: Multiaddr, + kind: Option, +) -> (PeerId, RpcReceiver) +where + D: DataTransform + Default + Clone + Send + 'static, + F: TopicSubscriptionFilter + Clone + Default + Send + 'static, +{ + let peer = PeerId::random(); + let endpoint = if outbound { + ConnectedPoint::Dialer { + address, + role_override: Endpoint::Dialer, + } + } else { + ConnectedPoint::Listener { + local_addr: Multiaddr::empty(), + send_back_addr: address, + } + }; + + let sender = RpcSender::new(gs.config.connection_handler_queue_len()); + let receiver = sender.new_receiver(); + let connection_id = ConnectionId::new_unchecked(0); + gs.connected_peers.insert( + peer, + PeerConnections { + kind: kind.clone().unwrap_or(PeerKind::Floodsub), + connections: vec![connection_id], + topics: Default::default(), + sender, + }, + ); + + gs.on_swarm_event(FromSwarm::ConnectionEstablished(ConnectionEstablished { + peer_id: peer, + connection_id, + endpoint: &endpoint, + failed_addresses: &[], + other_established: 0, // first connection + })); + if let Some(kind) = kind { + gs.on_connection_handler_event( + peer, + ConnectionId::new_unchecked(0), + HandlerEvent::PeerKind(kind), + ); + } + if explicit { + gs.add_explicit_peer(&peer); + } + if !topic_hashes.is_empty() { + gs.handle_received_subscriptions( + &topic_hashes + .iter() + .cloned() + .map(|t| Subscription { + action: SubscriptionAction::Subscribe, + topic_hash: t, + }) + .collect::>(), + &peer, + ); + } + (peer, receiver) +} + +fn disconnect_peer(gs: &mut Behaviour, peer_id: &PeerId) +where + D: DataTransform + Default + Clone + Send + 'static, + F: TopicSubscriptionFilter + Clone + Default + Send + 'static, +{ + if let Some(peer_connections) = gs.connected_peers.get(peer_id) { + let fake_endpoint = ConnectedPoint::Dialer { + address: Multiaddr::empty(), + role_override: Endpoint::Dialer, + }; // this is not relevant + // peer_connections.connections should never be empty. + + let mut active_connections = peer_connections.connections.len(); + for connection_id in peer_connections.connections.clone() { + active_connections = active_connections.checked_sub(1).unwrap(); + + gs.on_swarm_event(FromSwarm::ConnectionClosed(ConnectionClosed { + peer_id: *peer_id, + connection_id, + endpoint: &fake_endpoint, + remaining_established: active_connections, + })); + } + } +} + +// Converts a protobuf message into a gossipsub message for reading the Gossipsub event queue. +fn proto_to_message(rpc: &proto::RPC) -> Rpc { + // Store valid messages. + let mut messages = Vec::with_capacity(rpc.publish.len()); + let rpc = rpc.clone(); + for message in rpc.publish.into_iter() { + messages.push(RawMessage { + source: message.from.map(|x| PeerId::from_bytes(&x).unwrap()), + data: message.data.unwrap_or_default(), + sequence_number: message.seqno.map(|x| BigEndian::read_u64(&x)), // don't inform the application + topic: TopicHash::from_raw(message.topic), + signature: message.signature, // don't inform the application + key: None, + validated: false, + }); + } + let mut control_msgs = Vec::new(); + if let Some(rpc_control) = rpc.control { + // Collect the gossipsub control messages + let ihave_msgs: Vec = rpc_control + .ihave + .into_iter() + .map(|ihave| { + ControlAction::IHave(IHave { + topic_hash: TopicHash::from_raw(ihave.topic_id.unwrap_or_default()), + message_ids: ihave + .message_ids + .into_iter() + .map(MessageId::from) + .collect::>(), + }) + }) + .collect(); + + let iwant_msgs: Vec = rpc_control + .iwant + .into_iter() + .map(|iwant| { + ControlAction::IWant(IWant { + message_ids: iwant + .message_ids + .into_iter() + .map(MessageId::from) + .collect::>(), + }) + }) + .collect(); + + let graft_msgs: Vec = rpc_control + .graft + .into_iter() + .map(|graft| { + ControlAction::Graft(Graft { + topic_hash: TopicHash::from_raw(graft.topic_id.unwrap_or_default()), + }) + }) + .collect(); + + let mut prune_msgs = Vec::new(); + + for prune in rpc_control.prune { + // filter out invalid peers + let peers = prune + .peers + .into_iter() + .filter_map(|info| { + info.peer_id + .and_then(|id| PeerId::from_bytes(&id).ok()) + .map(|peer_id| + //TODO signedPeerRecord, see https://github.com/libp2p/specs/pull/217 + PeerInfo { + peer_id: Some(peer_id), + }) + }) + .collect::>(); + + let topic_hash = TopicHash::from_raw(prune.topic_id.unwrap_or_default()); + prune_msgs.push(ControlAction::Prune(Prune { + topic_hash, + peers, + backoff: prune.backoff, + })); + } + + control_msgs.extend(ihave_msgs); + control_msgs.extend(iwant_msgs); + control_msgs.extend(graft_msgs); + control_msgs.extend(prune_msgs); + } + + Rpc { + messages, + subscriptions: rpc + .subscriptions + .into_iter() + .map(|sub| Subscription { + action: if Some(true) == sub.subscribe { + SubscriptionAction::Subscribe + } else { + SubscriptionAction::Unsubscribe + }, + topic_hash: TopicHash::from_raw(sub.topic_id.unwrap_or_default()), + }) + .collect(), + control_msgs, + } +} + +#[test] +/// Test local node subscribing to a topic +fn test_subscribe() { + // The node should: + // - Create an empty vector in mesh[topic] + // - Send subscription request to all peers + // - run JOIN(topic) + + let subscribe_topic = vec![String::from("test_subscribe")]; + let (gs, _, receivers, topic_hashes) = inject_nodes1() + .peer_no(20) + .topics(subscribe_topic) + .to_subscribe(true) + .create_network(); + + assert!( + gs.mesh.contains_key(&topic_hashes[0]), + "Subscribe should add a new entry to the mesh[topic] hashmap" + ); + + // collect all the subscriptions + let subscriptions = receivers + .into_values() + .fold(0, |mut collected_subscriptions, c| { + let priority = c.priority.into_inner(); + while !priority.is_empty() { + if let Ok(RpcOut::Subscribe(_)) = priority.try_recv() { + collected_subscriptions += 1 + } + } + collected_subscriptions + }); + + // we sent a subscribe to all known peers + assert_eq!(subscriptions, 20); +} + +/// Test unsubscribe. +#[test] +fn test_unsubscribe() { + // Unsubscribe should: + // - Remove the mesh entry for topic + // - Send UNSUBSCRIBE to all known peers + // - Call Leave + + let topic_strings = vec![String::from("topic1"), String::from("topic2")]; + let topics = topic_strings + .iter() + .map(|t| Topic::new(t.clone())) + .collect::>(); + + // subscribe to topic_strings + let (mut gs, _, receivers, topic_hashes) = inject_nodes1() + .peer_no(20) + .topics(topic_strings) + .to_subscribe(true) + .create_network(); + + for topic_hash in &topic_hashes { + assert!( + gs.connected_peers + .values() + .any(|p| p.topics.contains(topic_hash)), + "Topic_peers contain a topic entry" + ); + assert!( + gs.mesh.contains_key(topic_hash), + "mesh should contain a topic entry" + ); + } + + // unsubscribe from both topics + assert!( + gs.unsubscribe(&topics[0]).unwrap(), + "should be able to unsubscribe successfully from each topic", + ); + assert!( + gs.unsubscribe(&topics[1]).unwrap(), + "should be able to unsubscribe successfully from each topic", + ); + + // collect all the subscriptions + let subscriptions = receivers + .into_values() + .fold(0, |mut collected_subscriptions, c| { + let priority = c.priority.into_inner(); + while !priority.is_empty() { + if let Ok(RpcOut::Subscribe(_)) = priority.try_recv() { + collected_subscriptions += 1 + } + } + collected_subscriptions + }); + + // we sent a unsubscribe to all known peers, for two topics + assert_eq!(subscriptions, 40); + + // check we clean up internal structures + for topic_hash in &topic_hashes { + assert!( + !gs.mesh.contains_key(topic_hash), + "All topics should have been removed from the mesh" + ); + } +} + +/// Test JOIN(topic) functionality. +#[test] +fn test_join() { + // The Join function should: + // - Remove peers from fanout[topic] + // - Add any fanout[topic] peers to the mesh (up to mesh_n) + // - Fill up to mesh_n peers from known gossipsub peers in the topic + // - Send GRAFT messages to all nodes added to the mesh + + // This test is not an isolated unit test, rather it uses higher level, + // subscribe/unsubscribe to perform the test. + + let topic_strings = vec![String::from("topic1"), String::from("topic2")]; + let topics = topic_strings + .iter() + .map(|t| Topic::new(t.clone())) + .collect::>(); + + let (mut gs, _, mut receivers, topic_hashes) = inject_nodes1() + .peer_no(20) + .topics(topic_strings) + .to_subscribe(true) + .create_network(); + + // Flush previous GRAFT messages. + receivers = flush_events(&mut gs, receivers); + + // unsubscribe, then call join to invoke functionality + assert!( + gs.unsubscribe(&topics[0]).unwrap(), + "should be able to unsubscribe successfully" + ); + assert!( + gs.unsubscribe(&topics[1]).unwrap(), + "should be able to unsubscribe successfully" + ); + + // re-subscribe - there should be peers associated with the topic + assert!( + gs.subscribe(&topics[0]).unwrap(), + "should be able to subscribe successfully" + ); + + // should have added mesh_n nodes to the mesh + assert!( + gs.mesh.get(&topic_hashes[0]).unwrap().len() == 6, + "Should have added 6 nodes to the mesh" + ); + + fn count_grafts( + receivers: HashMap, + ) -> (usize, HashMap) { + let mut new_receivers = HashMap::new(); + let mut acc = 0; + + for (peer_id, c) in receivers.into_iter() { + let priority = c.priority.into_inner(); + while !priority.is_empty() { + if let Ok(RpcOut::Graft(_)) = priority.try_recv() { + acc += 1; + } + } + new_receivers.insert( + peer_id, + RpcReceiver { + priority_len: c.priority_len, + priority: priority.peekable(), + non_priority: c.non_priority, + }, + ); + } + (acc, new_receivers) + } + + // there should be mesh_n GRAFT messages. + let (graft_messages, mut receivers) = count_grafts(receivers); + + assert_eq!( + graft_messages, 6, + "There should be 6 grafts messages sent to peers" + ); + + // verify fanout nodes + // add 3 random peers to the fanout[topic1] + gs.fanout + .insert(topic_hashes[1].clone(), Default::default()); + let mut new_peers: Vec = vec![]; + + for _ in 0..3 { + let random_peer = PeerId::random(); + // inform the behaviour of a new peer + let address = "/ip4/127.0.0.1".parse::().unwrap(); + gs.handle_established_inbound_connection( + ConnectionId::new_unchecked(0), + random_peer, + &address, + &address, + ) + .unwrap(); + let sender = RpcSender::new(gs.config.connection_handler_queue_len()); + let receiver = sender.new_receiver(); + let connection_id = ConnectionId::new_unchecked(0); + gs.connected_peers.insert( + random_peer, + PeerConnections { + kind: PeerKind::Floodsub, + connections: vec![connection_id], + topics: Default::default(), + sender, + }, + ); + receivers.insert(random_peer, receiver); + + gs.on_swarm_event(FromSwarm::ConnectionEstablished(ConnectionEstablished { + peer_id: random_peer, + connection_id, + endpoint: &ConnectedPoint::Dialer { + address, + role_override: Endpoint::Dialer, + }, + failed_addresses: &[], + other_established: 0, + })); + + // add the new peer to the fanout + let fanout_peers = gs.fanout.get_mut(&topic_hashes[1]).unwrap(); + fanout_peers.insert(random_peer); + new_peers.push(random_peer); + } + + // subscribe to topic1 + gs.subscribe(&topics[1]).unwrap(); + + // the three new peers should have been added, along with 3 more from the pool. + assert!( + gs.mesh.get(&topic_hashes[1]).unwrap().len() == 6, + "Should have added 6 nodes to the mesh" + ); + let mesh_peers = gs.mesh.get(&topic_hashes[1]).unwrap(); + for new_peer in new_peers { + assert!( + mesh_peers.contains(&new_peer), + "Fanout peer should be included in the mesh" + ); + } + + // there should now 6 graft messages to be sent + let (graft_messages, _) = count_grafts(receivers); + + assert_eq!( + graft_messages, 6, + "There should be 6 grafts messages sent to peers" + ); +} + +/// Test local node publish to subscribed topic +#[test] +fn test_publish_without_flood_publishing() { + // node should: + // - Send publish message to all peers + // - Insert message into gs.mcache and gs.received + + //turn off flood publish to test old behaviour + let config = ConfigBuilder::default() + .flood_publish(false) + .build() + .unwrap(); + + let publish_topic = String::from("test_publish"); + let (mut gs, _, receivers, topic_hashes) = inject_nodes1() + .peer_no(20) + .topics(vec![publish_topic.clone()]) + .to_subscribe(true) + .gs_config(config) + .create_network(); + + assert!( + gs.mesh.contains_key(&topic_hashes[0]), + "Subscribe should add a new entry to the mesh[topic] hashmap" + ); + + // all peers should be subscribed to the topic + assert_eq!( + gs.connected_peers + .values() + .filter(|p| p.topics.contains(&topic_hashes[0])) + .count(), + 20, + "Peers should be subscribed to the topic" + ); + + // publish on topic + let publish_data = vec![0; 42]; + gs.publish(Topic::new(publish_topic), publish_data).unwrap(); + + // Collect all publish messages + let publishes = receivers + .into_values() + .fold(vec![], |mut collected_publish, c| { + let priority = c.priority.into_inner(); + while !priority.is_empty() { + if let Ok(RpcOut::Publish { message, .. }) = priority.try_recv() { + collected_publish.push(message); + } + } + collected_publish + }); + + // Transform the inbound message + let message = &gs + .data_transform + .inbound_transform( + publishes + .first() + .expect("Should contain > 0 entries") + .clone(), + ) + .unwrap(); + + let msg_id = gs.config.message_id(message); + + let config: Config = Config::default(); + assert_eq!( + publishes.len(), + config.mesh_n(), + "Should send a publish message to at least mesh_n peers" + ); + + assert!( + gs.mcache.get(&msg_id).is_some(), + "Message cache should contain published message" + ); +} + +/// Test local node publish to unsubscribed topic +#[test] +fn test_fanout() { + // node should: + // - Populate fanout peers + // - Send publish message to fanout peers + // - Insert message into gs.mcache and gs.received + + //turn off flood publish to test fanout behaviour + let config = ConfigBuilder::default() + .flood_publish(false) + .build() + .unwrap(); + + let fanout_topic = String::from("test_fanout"); + let (mut gs, _, receivers, topic_hashes) = inject_nodes1() + .peer_no(20) + .topics(vec![fanout_topic.clone()]) + .to_subscribe(true) + .gs_config(config) + .create_network(); + + assert!( + gs.mesh.contains_key(&topic_hashes[0]), + "Subscribe should add a new entry to the mesh[topic] hashmap" + ); + // Unsubscribe from topic + assert!( + gs.unsubscribe(&Topic::new(fanout_topic.clone())).unwrap(), + "should be able to unsubscribe successfully from topic" + ); + + // Publish on unsubscribed topic + let publish_data = vec![0; 42]; + gs.publish(Topic::new(fanout_topic.clone()), publish_data) + .unwrap(); + + assert_eq!( + gs.fanout + .get(&TopicHash::from_raw(fanout_topic)) + .unwrap() + .len(), + gs.config.mesh_n(), + "Fanout should contain `mesh_n` peers for fanout topic" + ); + + // Collect all publish messages + let publishes = receivers + .into_values() + .fold(vec![], |mut collected_publish, c| { + let priority = c.priority.into_inner(); + while !priority.is_empty() { + if let Ok(RpcOut::Publish { message, .. }) = priority.try_recv() { + collected_publish.push(message); + } + } + collected_publish + }); + + // Transform the inbound message + let message = &gs + .data_transform + .inbound_transform( + publishes + .first() + .expect("Should contain > 0 entries") + .clone(), + ) + .unwrap(); + + let msg_id = gs.config.message_id(message); + + assert_eq!( + publishes.len(), + gs.config.mesh_n(), + "Should send a publish message to `mesh_n` fanout peers" + ); + + assert!( + gs.mcache.get(&msg_id).is_some(), + "Message cache should contain published message" + ); +} + +/// Test the gossipsub NetworkBehaviour peer connection logic. +#[test] +fn test_inject_connected() { + let (gs, peers, receivers, topic_hashes) = inject_nodes1() + .peer_no(20) + .topics(vec![String::from("topic1"), String::from("topic2")]) + .to_subscribe(true) + .create_network(); + + // check that our subscriptions are sent to each of the peers + // collect all the SendEvents + let subscriptions = receivers.into_iter().fold( + HashMap::>::new(), + |mut collected_subscriptions, (peer, c)| { + let priority = c.priority.into_inner(); + while !priority.is_empty() { + if let Ok(RpcOut::Subscribe(topic)) = priority.try_recv() { + let mut peer_subs = collected_subscriptions.remove(&peer).unwrap_or_default(); + peer_subs.push(topic.into_string()); + collected_subscriptions.insert(peer, peer_subs); + } + } + collected_subscriptions + }, + ); + + // check that there are two subscriptions sent to each peer + for peer_subs in subscriptions.values() { + assert!(peer_subs.contains(&String::from("topic1"))); + assert!(peer_subs.contains(&String::from("topic2"))); + assert_eq!(peer_subs.len(), 2); + } + + // check that there are 20 send events created + assert_eq!(subscriptions.len(), 20); + + // should add the new peers to `peer_topics` with an empty vec as a gossipsub node + for peer in peers { + let peer = gs.connected_peers.get(&peer).unwrap(); + assert!( + peer.topics == topic_hashes.iter().cloned().collect(), + "The topics for each node should all topics" + ); + } +} + +/// Test subscription handling +#[test] +fn test_handle_received_subscriptions() { + // For every subscription: + // SUBSCRIBE: - Add subscribed topic to peer_topics for peer. + // - Add peer to topics_peer. + // UNSUBSCRIBE - Remove topic from peer_topics for peer. + // - Remove peer from topic_peers. + + let topics = ["topic1", "topic2", "topic3", "topic4"] + .iter() + .map(|&t| String::from(t)) + .collect(); + let (mut gs, peers, _receivers, topic_hashes) = inject_nodes1() + .peer_no(20) + .topics(topics) + .to_subscribe(false) + .create_network(); + + // The first peer sends 3 subscriptions and 1 unsubscription + let mut subscriptions = topic_hashes[..3] + .iter() + .map(|topic_hash| Subscription { + action: SubscriptionAction::Subscribe, + topic_hash: topic_hash.clone(), + }) + .collect::>(); + + subscriptions.push(Subscription { + action: SubscriptionAction::Unsubscribe, + topic_hash: topic_hashes[topic_hashes.len() - 1].clone(), + }); + + let unknown_peer = PeerId::random(); + // process the subscriptions + // first and second peers send subscriptions + gs.handle_received_subscriptions(&subscriptions, &peers[0]); + gs.handle_received_subscriptions(&subscriptions, &peers[1]); + // unknown peer sends the same subscriptions + gs.handle_received_subscriptions(&subscriptions, &unknown_peer); + + // verify the result + + let peer = gs.connected_peers.get(&peers[0]).unwrap(); + assert!( + peer.topics + == topic_hashes + .iter() + .take(3) + .cloned() + .collect::>(), + "First peer should be subscribed to three topics" + ); + let peer1 = gs.connected_peers.get(&peers[1]).unwrap(); + assert!( + peer1.topics + == topic_hashes + .iter() + .take(3) + .cloned() + .collect::>(), + "Second peer should be subscribed to three topics" + ); + + assert!( + !gs.connected_peers.contains_key(&unknown_peer), + "Unknown peer should not have been added" + ); + + for topic_hash in topic_hashes[..3].iter() { + let topic_peers = gs + .connected_peers + .iter() + .filter(|(_, p)| p.topics.contains(topic_hash)) + .map(|(peer_id, _)| *peer_id) + .collect::>(); + assert!( + topic_peers == peers[..2].iter().cloned().collect(), + "Two peers should be added to the first three topics" + ); + } + + // Peer 0 unsubscribes from the first topic + + gs.handle_received_subscriptions( + &[Subscription { + action: SubscriptionAction::Unsubscribe, + topic_hash: topic_hashes[0].clone(), + }], + &peers[0], + ); + + let peer = gs.connected_peers.get(&peers[0]).unwrap(); + assert!( + peer.topics == topic_hashes[1..3].iter().cloned().collect::>(), + "Peer should be subscribed to two topics" + ); + + // only gossipsub at the moment + let topic_peers = gs + .connected_peers + .iter() + .filter(|(_, p)| p.topics.contains(&topic_hashes[0])) + .map(|(peer_id, _)| *peer_id) + .collect::>(); + + assert!( + topic_peers == peers[1..2].iter().cloned().collect(), + "Only the second peers should be in the first topic" + ); +} + +/// Test Gossipsub.get_random_peers() function +#[test] +fn test_get_random_peers() { + // generate a default Config + let gs_config = ConfigBuilder::default() + .validation_mode(ValidationMode::Anonymous) + .build() + .unwrap(); + // create a gossipsub struct + let mut gs: Behaviour = Behaviour::new(MessageAuthenticity::Anonymous, gs_config).unwrap(); + + // create a topic and fill it with some peers + let topic_hash = Topic::new("Test").hash(); + let mut peers = vec![]; + let mut topics = BTreeSet::new(); + topics.insert(topic_hash.clone()); + + for _ in 0..20 { + let peer_id = PeerId::random(); + peers.push(peer_id); + gs.connected_peers.insert( + peer_id, + PeerConnections { + kind: PeerKind::Gossipsubv1_1, + connections: vec![ConnectionId::new_unchecked(0)], + topics: topics.clone(), + sender: RpcSender::new(gs.config.connection_handler_queue_len()), + }, + ); + } + + let random_peers = get_random_peers(&gs.connected_peers, &topic_hash, 5, |_| true); + assert_eq!(random_peers.len(), 5, "Expected 5 peers to be returned"); + let random_peers = get_random_peers(&gs.connected_peers, &topic_hash, 30, |_| true); + assert!(random_peers.len() == 20, "Expected 20 peers to be returned"); + assert!( + random_peers == peers.iter().cloned().collect(), + "Expected no shuffling" + ); + let random_peers = get_random_peers(&gs.connected_peers, &topic_hash, 20, |_| true); + assert!(random_peers.len() == 20, "Expected 20 peers to be returned"); + assert!( + random_peers == peers.iter().cloned().collect(), + "Expected no shuffling" + ); + let random_peers = get_random_peers(&gs.connected_peers, &topic_hash, 0, |_| true); + assert!(random_peers.is_empty(), "Expected 0 peers to be returned"); + // test the filter + let random_peers = get_random_peers(&gs.connected_peers, &topic_hash, 5, |_| false); + assert!(random_peers.is_empty(), "Expected 0 peers to be returned"); + let random_peers = get_random_peers(&gs.connected_peers, &topic_hash, 10, { + |peer| peers.contains(peer) + }); + assert!(random_peers.len() == 10, "Expected 10 peers to be returned"); +} + +/// Tests that the correct message is sent when a peer asks for a message in our cache. +#[test] +fn test_handle_iwant_msg_cached() { + let (mut gs, peers, receivers, _) = inject_nodes1() + .peer_no(20) + .topics(Vec::new()) + .to_subscribe(true) + .create_network(); + + let raw_message = RawMessage { + source: Some(peers[11]), + data: vec![1, 2, 3, 4], + sequence_number: Some(1u64), + topic: TopicHash::from_raw("topic"), + signature: None, + key: None, + validated: true, + }; + + // Transform the inbound message + let message = &gs + .data_transform + .inbound_transform(raw_message.clone()) + .unwrap(); + + let msg_id = gs.config.message_id(message); + gs.mcache.put(&msg_id, raw_message); + + gs.handle_iwant(&peers[7], vec![msg_id.clone()]); + + // the messages we are sending + let sent_messages = receivers + .into_values() + .fold(vec![], |mut collected_messages, c| { + let non_priority = c.non_priority.into_inner(); + while !non_priority.is_empty() { + if let Ok(RpcOut::Forward { message, .. }) = non_priority.try_recv() { + collected_messages.push(message) + } + } + collected_messages + }); + + assert!( + sent_messages + .iter() + .map(|msg| gs.data_transform.inbound_transform(msg.clone()).unwrap()) + .any(|msg| gs.config.message_id(&msg) == msg_id), + "Expected the cached message to be sent to an IWANT peer" + ); +} + +/// Tests that messages are sent correctly depending on the shifting of the message cache. +#[test] +fn test_handle_iwant_msg_cached_shifted() { + let (mut gs, peers, mut receivers, _) = inject_nodes1() + .peer_no(20) + .topics(Vec::new()) + .to_subscribe(true) + .create_network(); + + // perform 10 memshifts and check that it leaves the cache + for shift in 1..10 { + let raw_message = RawMessage { + source: Some(peers[11]), + data: vec![1, 2, 3, 4], + sequence_number: Some(shift), + topic: TopicHash::from_raw("topic"), + signature: None, + key: None, + validated: true, + }; + + // Transform the inbound message + let message = &gs + .data_transform + .inbound_transform(raw_message.clone()) + .unwrap(); + + let msg_id = gs.config.message_id(message); + gs.mcache.put(&msg_id, raw_message); + for _ in 0..shift { + gs.mcache.shift(); + } + + gs.handle_iwant(&peers[7], vec![msg_id.clone()]); + + // is the message is being sent? + let mut message_exists = false; + receivers = receivers.into_iter().map(|(peer_id, c)| { + let non_priority = c.non_priority.into_inner(); + while !non_priority.is_empty() { + if matches!(non_priority.try_recv(), Ok(RpcOut::Forward{message, timeout: _ }) if + gs.config.message_id( + &gs.data_transform + .inbound_transform(message.clone()) + .unwrap(), + ) == msg_id) + { + message_exists = true; + } + } + ( + peer_id, + RpcReceiver { + priority_len: c.priority_len, + priority: c.priority, + non_priority: non_priority.peekable(), + }, + ) + }).collect(); + // default history_length is 5, expect no messages after shift > 5 + if shift < 5 { + assert!( + message_exists, + "Expected the cached message to be sent to an IWANT peer before 5 shifts" + ); + } else { + assert!( + !message_exists, + "Expected the cached message to not be sent to an IWANT peer after 5 shifts" + ); + } + } +} + +/// tests that an event is not created when a peers asks for a message not in our cache +#[test] +fn test_handle_iwant_msg_not_cached() { + let (mut gs, peers, _, _) = inject_nodes1() + .peer_no(20) + .topics(Vec::new()) + .to_subscribe(true) + .create_network(); + + let events_before = gs.events.len(); + gs.handle_iwant(&peers[7], vec![MessageId::new(b"unknown id")]); + let events_after = gs.events.len(); + + assert_eq!( + events_before, events_after, + "Expected event count to stay the same" + ); +} + +/// tests that an event is created when a peer shares that it has a message we want +#[test] +fn test_handle_ihave_subscribed_and_msg_not_cached() { + let (mut gs, peers, mut receivers, topic_hashes) = inject_nodes1() + .peer_no(20) + .topics(vec![String::from("topic1")]) + .to_subscribe(true) + .create_network(); + + gs.handle_ihave( + &peers[7], + vec![(topic_hashes[0].clone(), vec![MessageId::new(b"unknown id")])], + ); + + // check that we sent an IWANT request for `unknown id` + let mut iwant_exists = false; + let receiver = receivers.remove(&peers[7]).unwrap(); + let non_priority = receiver.non_priority.into_inner(); + while !non_priority.is_empty() { + if let Ok(RpcOut::IWant(IWant { message_ids })) = non_priority.try_recv() { + if message_ids + .iter() + .any(|m| *m == MessageId::new(b"unknown id")) + { + iwant_exists = true; + break; + } + } + } + + assert!( + iwant_exists, + "Expected to send an IWANT control message for unkown message id" + ); +} + +/// tests that an event is not created when a peer shares that it has a message that +/// we already have +#[test] +fn test_handle_ihave_subscribed_and_msg_cached() { + let (mut gs, peers, _, topic_hashes) = inject_nodes1() + .peer_no(20) + .topics(vec![String::from("topic1")]) + .to_subscribe(true) + .create_network(); + + let msg_id = MessageId::new(b"known id"); + + let events_before = gs.events.len(); + gs.handle_ihave(&peers[7], vec![(topic_hashes[0].clone(), vec![msg_id])]); + let events_after = gs.events.len(); + + assert_eq!( + events_before, events_after, + "Expected event count to stay the same" + ) +} + +/// test that an event is not created when a peer shares that it has a message in +/// a topic that we are not subscribed to +#[test] +fn test_handle_ihave_not_subscribed() { + let (mut gs, peers, _, _) = inject_nodes1() + .peer_no(20) + .topics(vec![]) + .to_subscribe(true) + .create_network(); + + let events_before = gs.events.len(); + gs.handle_ihave( + &peers[7], + vec![( + TopicHash::from_raw(String::from("unsubscribed topic")), + vec![MessageId::new(b"irrelevant id")], + )], + ); + let events_after = gs.events.len(); + + assert_eq!( + events_before, events_after, + "Expected event count to stay the same" + ) +} + +/// tests that a peer is added to our mesh when we are both subscribed +/// to the same topic +#[test] +fn test_handle_graft_is_subscribed() { + let (mut gs, peers, _, topic_hashes) = inject_nodes1() + .peer_no(20) + .topics(vec![String::from("topic1")]) + .to_subscribe(true) + .create_network(); + + gs.handle_graft(&peers[7], topic_hashes.clone()); + + assert!( + gs.mesh.get(&topic_hashes[0]).unwrap().contains(&peers[7]), + "Expected peer to have been added to mesh" + ); +} + +/// tests that a peer is not added to our mesh when they are subscribed to +/// a topic that we are not +#[test] +fn test_handle_graft_is_not_subscribed() { + let (mut gs, peers, _, topic_hashes) = inject_nodes1() + .peer_no(20) + .topics(vec![String::from("topic1")]) + .to_subscribe(true) + .create_network(); + + gs.handle_graft( + &peers[7], + vec![TopicHash::from_raw(String::from("unsubscribed topic"))], + ); + + assert!( + !gs.mesh.get(&topic_hashes[0]).unwrap().contains(&peers[7]), + "Expected peer to have been added to mesh" + ); +} + +/// tests multiple topics in a single graft message +#[test] +fn test_handle_graft_multiple_topics() { + let topics: Vec = ["topic1", "topic2", "topic3", "topic4"] + .iter() + .map(|&t| String::from(t)) + .collect(); + + let (mut gs, peers, _, topic_hashes) = inject_nodes1() + .peer_no(20) + .topics(topics) + .to_subscribe(true) + .create_network(); + + let mut their_topics = topic_hashes.clone(); + // their_topics = [topic1, topic2, topic3] + // our_topics = [topic1, topic2, topic4] + their_topics.pop(); + gs.leave(&their_topics[2]); + + gs.handle_graft(&peers[7], their_topics.clone()); + + for hash in topic_hashes.iter().take(2) { + assert!( + gs.mesh.get(hash).unwrap().contains(&peers[7]), + "Expected peer to be in the mesh for the first 2 topics" + ); + } + + assert!( + !gs.mesh.contains_key(&topic_hashes[2]), + "Expected the second topic to not be in the mesh" + ); +} + +/// tests that a peer is removed from our mesh +#[test] +fn test_handle_prune_peer_in_mesh() { + let (mut gs, peers, _, topic_hashes) = inject_nodes1() + .peer_no(20) + .topics(vec![String::from("topic1")]) + .to_subscribe(true) + .create_network(); + + // insert peer into our mesh for 'topic1' + gs.mesh + .insert(topic_hashes[0].clone(), peers.iter().cloned().collect()); + assert!( + gs.mesh.get(&topic_hashes[0]).unwrap().contains(&peers[7]), + "Expected peer to be in mesh" + ); + + gs.handle_prune( + &peers[7], + topic_hashes + .iter() + .map(|h| (h.clone(), vec![], None)) + .collect(), + ); + assert!( + !gs.mesh.get(&topic_hashes[0]).unwrap().contains(&peers[7]), + "Expected peer to be removed from mesh" + ); +} + +fn count_control_msgs( + receivers: HashMap, + mut filter: impl FnMut(&PeerId, &RpcOut) -> bool, +) -> (usize, HashMap) { + let mut new_receivers = HashMap::new(); + let mut collected_messages = 0; + for (peer_id, c) in receivers.into_iter() { + let priority = c.priority.into_inner(); + let non_priority = c.non_priority.into_inner(); + while !priority.is_empty() || !non_priority.is_empty() { + if let Ok(rpc) = priority.try_recv() { + if filter(&peer_id, &rpc) { + collected_messages += 1; + } + } + if let Ok(rpc) = non_priority.try_recv() { + if filter(&peer_id, &rpc) { + collected_messages += 1; + } + } + } + new_receivers.insert( + peer_id, + RpcReceiver { + priority_len: c.priority_len, + priority: priority.peekable(), + non_priority: non_priority.peekable(), + }, + ); + } + (collected_messages, new_receivers) +} + +fn flush_events( + gs: &mut Behaviour, + receivers: HashMap, +) -> HashMap { + gs.events.clear(); + let mut new_receivers = HashMap::new(); + for (peer_id, c) in receivers.into_iter() { + let priority = c.priority.into_inner(); + let non_priority = c.non_priority.into_inner(); + while !priority.is_empty() || !non_priority.is_empty() { + let _ = priority.try_recv(); + let _ = non_priority.try_recv(); + } + new_receivers.insert( + peer_id, + RpcReceiver { + priority_len: c.priority_len, + priority: priority.peekable(), + non_priority: non_priority.peekable(), + }, + ); + } + new_receivers +} + +/// tests that a peer added as explicit peer gets connected to +#[test] +fn test_explicit_peer_gets_connected() { + let (mut gs, _, _, _) = inject_nodes1() + .peer_no(0) + .topics(Vec::new()) + .to_subscribe(true) + .create_network(); + + //create new peer + let peer = PeerId::random(); + + //add peer as explicit peer + gs.add_explicit_peer(&peer); + + let num_events = gs + .events + .iter() + .filter(|e| match e { + ToSwarm::Dial { opts } => opts.get_peer_id() == Some(peer), + _ => false, + }) + .count(); + + assert_eq!( + num_events, 1, + "There was no dial peer event for the explicit peer" + ); +} + +#[test] +fn test_explicit_peer_reconnects() { + let config = ConfigBuilder::default() + .check_explicit_peers_ticks(2) + .build() + .unwrap(); + let (mut gs, others, receivers, _) = inject_nodes1() + .peer_no(1) + .topics(Vec::new()) + .to_subscribe(true) + .gs_config(config) + .create_network(); + + let peer = others.first().unwrap(); + + //add peer as explicit peer + gs.add_explicit_peer(peer); + + flush_events(&mut gs, receivers); + + //disconnect peer + disconnect_peer(&mut gs, peer); + + gs.heartbeat(); + + //check that no reconnect after first heartbeat since `explicit_peer_ticks == 2` + assert_eq!( + gs.events + .iter() + .filter(|e| match e { + ToSwarm::Dial { opts } => opts.get_peer_id() == Some(*peer), + _ => false, + }) + .count(), + 0, + "There was a dial peer event before explicit_peer_ticks heartbeats" + ); + + gs.heartbeat(); + + //check that there is a reconnect after second heartbeat + assert!( + gs.events + .iter() + .filter(|e| match e { + ToSwarm::Dial { opts } => opts.get_peer_id() == Some(*peer), + _ => false, + }) + .count() + >= 1, + "There was no dial peer event for the explicit peer" + ); +} + +#[test] +fn test_handle_graft_explicit_peer() { + let (mut gs, peers, receivers, topic_hashes) = inject_nodes1() + .peer_no(1) + .topics(vec![String::from("topic1"), String::from("topic2")]) + .to_subscribe(true) + .gs_config(Config::default()) + .explicit(1) + .create_network(); + + let peer = peers.first().unwrap(); + + gs.handle_graft(peer, topic_hashes.clone()); + + //peer got not added to mesh + assert!(gs.mesh[&topic_hashes[0]].is_empty()); + assert!(gs.mesh[&topic_hashes[1]].is_empty()); + + //check prunes + let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { + peer_id == peer + && match m { + RpcOut::Prune(Prune { topic_hash, .. }) => { + topic_hash == &topic_hashes[0] || topic_hash == &topic_hashes[1] + } + _ => false, + } + }); + assert!( + control_msgs >= 2, + "Not enough prunes sent when grafting from explicit peer" + ); +} + +#[test] +fn explicit_peers_not_added_to_mesh_on_receiving_subscription() { + let (gs, peers, receivers, topic_hashes) = inject_nodes1() + .peer_no(2) + .topics(vec![String::from("topic1")]) + .to_subscribe(true) + .gs_config(Config::default()) + .explicit(1) + .create_network(); + + //only peer 1 is in the mesh not peer 0 (which is an explicit peer) + assert_eq!( + gs.mesh[&topic_hashes[0]], + vec![peers[1]].into_iter().collect() + ); + + //assert that graft gets created to non-explicit peer + let (control_msgs, receivers) = count_control_msgs(receivers, |peer_id, m| { + peer_id == &peers[1] && matches!(m, RpcOut::Graft { .. }) + }); + assert!( + control_msgs >= 1, + "No graft message got created to non-explicit peer" + ); + + //assert that no graft gets created to explicit peer + let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { + peer_id == &peers[0] && matches!(m, RpcOut::Graft { .. }) + }); + assert_eq!( + control_msgs, 0, + "A graft message got created to an explicit peer" + ); +} + +#[test] +fn do_not_graft_explicit_peer() { + let (mut gs, others, receivers, topic_hashes) = inject_nodes1() + .peer_no(1) + .topics(vec![String::from("topic")]) + .to_subscribe(true) + .gs_config(Config::default()) + .explicit(1) + .create_network(); + + gs.heartbeat(); + + //mesh stays empty + assert_eq!(gs.mesh[&topic_hashes[0]], BTreeSet::new()); + + //assert that no graft gets created to explicit peer + let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { + peer_id == &others[0] && matches!(m, RpcOut::Graft { .. }) + }); + assert_eq!( + control_msgs, 0, + "A graft message got created to an explicit peer" + ); +} + +#[test] +fn do_forward_messages_to_explicit_peers() { + let (mut gs, peers, receivers, topic_hashes) = inject_nodes1() + .peer_no(2) + .topics(vec![String::from("topic1"), String::from("topic2")]) + .to_subscribe(true) + .gs_config(Config::default()) + .explicit(1) + .create_network(); + + let local_id = PeerId::random(); + + let message = RawMessage { + source: Some(peers[1]), + data: vec![12], + sequence_number: Some(0), + topic: topic_hashes[0].clone(), + signature: None, + key: None, + validated: true, + }; + gs.handle_received_message(message.clone(), &local_id); + assert_eq!( + receivers.into_iter().fold(0, |mut fwds, (peer_id, c)| { + let non_priority = c.non_priority.into_inner(); + while !non_priority.is_empty() { + if matches!(non_priority.try_recv(), Ok(RpcOut::Forward{message: m, timeout: _}) if peer_id == peers[0] && m.data == message.data) { + fwds +=1; + } + } + fwds + }), + 1, + "The message did not get forwarded to the explicit peer" + ); +} + +#[test] +fn explicit_peers_not_added_to_mesh_on_subscribe() { + let (mut gs, peers, receivers, _) = inject_nodes1() + .peer_no(2) + .topics(Vec::new()) + .to_subscribe(true) + .gs_config(Config::default()) + .explicit(1) + .create_network(); + + //create new topic, both peers subscribing to it but we do not subscribe to it + let topic = Topic::new(String::from("t")); + let topic_hash = topic.hash(); + for peer in peers.iter().take(2) { + gs.handle_received_subscriptions( + &[Subscription { + action: SubscriptionAction::Subscribe, + topic_hash: topic_hash.clone(), + }], + peer, + ); + } + + //subscribe now to topic + gs.subscribe(&topic).unwrap(); + + //only peer 1 is in the mesh not peer 0 (which is an explicit peer) + assert_eq!(gs.mesh[&topic_hash], vec![peers[1]].into_iter().collect()); + + //assert that graft gets created to non-explicit peer + let (control_msgs, receivers) = count_control_msgs(receivers, |peer_id, m| { + peer_id == &peers[1] && matches!(m, RpcOut::Graft { .. }) + }); + assert!( + control_msgs > 0, + "No graft message got created to non-explicit peer" + ); + + //assert that no graft gets created to explicit peer + let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { + peer_id == &peers[0] && matches!(m, RpcOut::Graft { .. }) + }); + assert_eq!( + control_msgs, 0, + "A graft message got created to an explicit peer" + ); +} + +#[test] +fn explicit_peers_not_added_to_mesh_from_fanout_on_subscribe() { + let (mut gs, peers, receivers, _) = inject_nodes1() + .peer_no(2) + .topics(Vec::new()) + .to_subscribe(true) + .gs_config(Config::default()) + .explicit(1) + .create_network(); + + //create new topic, both peers subscribing to it but we do not subscribe to it + let topic = Topic::new(String::from("t")); + let topic_hash = topic.hash(); + for peer in peers.iter().take(2) { + gs.handle_received_subscriptions( + &[Subscription { + action: SubscriptionAction::Subscribe, + topic_hash: topic_hash.clone(), + }], + peer, + ); + } + + //we send a message for this topic => this will initialize the fanout + gs.publish(topic.clone(), vec![1, 2, 3]).unwrap(); + + //subscribe now to topic + gs.subscribe(&topic).unwrap(); + + //only peer 1 is in the mesh not peer 0 (which is an explicit peer) + assert_eq!(gs.mesh[&topic_hash], vec![peers[1]].into_iter().collect()); + + //assert that graft gets created to non-explicit peer + let (control_msgs, receivers) = count_control_msgs(receivers, |peer_id, m| { + peer_id == &peers[1] && matches!(m, RpcOut::Graft { .. }) + }); + assert!( + control_msgs >= 1, + "No graft message got created to non-explicit peer" + ); + + //assert that no graft gets created to explicit peer + let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { + peer_id == &peers[0] && matches!(m, RpcOut::Graft { .. }) + }); + assert_eq!( + control_msgs, 0, + "A graft message got created to an explicit peer" + ); +} + +#[test] +fn no_gossip_gets_sent_to_explicit_peers() { + let (mut gs, peers, mut receivers, topic_hashes) = inject_nodes1() + .peer_no(2) + .topics(vec![String::from("topic1"), String::from("topic2")]) + .to_subscribe(true) + .gs_config(Config::default()) + .explicit(1) + .create_network(); + + let local_id = PeerId::random(); + + let message = RawMessage { + source: Some(peers[1]), + data: vec![], + sequence_number: Some(0), + topic: topic_hashes[0].clone(), + signature: None, + key: None, + validated: true, + }; + + //forward the message + gs.handle_received_message(message, &local_id); + + //simulate multiple gossip calls (for randomness) + for _ in 0..3 { + gs.emit_gossip(); + } + + //assert that no gossip gets sent to explicit peer + let receiver = receivers.remove(&peers[0]).unwrap(); + let mut gossips = 0; + let non_priority = receiver.non_priority.into_inner(); + while !non_priority.is_empty() { + if let Ok(RpcOut::IHave(_)) = non_priority.try_recv() { + gossips += 1; + } + } + assert_eq!(gossips, 0, "Gossip got emitted to explicit peer"); +} + +/// Tests the mesh maintenance addition +#[test] +fn test_mesh_addition() { + let config: Config = Config::default(); + + // Adds mesh_low peers and PRUNE 2 giving us a deficit. + let (mut gs, peers, _receivers, topics) = inject_nodes1() + .peer_no(config.mesh_n() + 1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .create_network(); + + let to_remove_peers = config.mesh_n() + 1 - config.mesh_n_low() - 1; + + for peer in peers.iter().take(to_remove_peers) { + gs.handle_prune( + peer, + topics.iter().map(|h| (h.clone(), vec![], None)).collect(), + ); + } + + // Verify the pruned peers are removed from the mesh. + assert_eq!( + gs.mesh.get(&topics[0]).unwrap().len(), + config.mesh_n_low() - 1 + ); + + // run a heartbeat + gs.heartbeat(); + + // Peers should be added to reach mesh_n + assert_eq!(gs.mesh.get(&topics[0]).unwrap().len(), config.mesh_n()); +} + +/// Tests the mesh maintenance subtraction +#[test] +fn test_mesh_subtraction() { + let config = Config::default(); + + // Adds mesh_low peers and PRUNE 2 giving us a deficit. + let n = config.mesh_n_high() + 10; + //make all outbound connections so that we allow grafting to all + let (mut gs, peers, _receivers, topics) = inject_nodes1() + .peer_no(n) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config.clone()) + .outbound(n) + .create_network(); + + // graft all the peers + for peer in peers { + gs.handle_graft(&peer, topics.clone()); + } + + // run a heartbeat + gs.heartbeat(); + + // Peers should be removed to reach mesh_n + assert_eq!(gs.mesh.get(&topics[0]).unwrap().len(), config.mesh_n()); +} + +#[test] +fn test_connect_to_px_peers_on_handle_prune() { + let config: Config = Config::default(); + + let (mut gs, peers, _, topics) = inject_nodes1() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .create_network(); + + //handle prune from single peer with px peers + + let mut px = Vec::new(); + //propose more px peers than config.prune_peers() + for _ in 0..config.prune_peers() + 5 { + px.push(PeerInfo { + peer_id: Some(PeerId::random()), + }); + } + + gs.handle_prune( + &peers[0], + vec![( + topics[0].clone(), + px.clone(), + Some(config.prune_backoff().as_secs()), + )], + ); + + //Check DialPeer events for px peers + let dials: Vec<_> = gs + .events + .iter() + .filter_map(|e| match e { + ToSwarm::Dial { opts } => opts.get_peer_id(), + _ => None, + }) + .collect(); + + // Exactly config.prune_peers() many random peers should be dialled + assert_eq!(dials.len(), config.prune_peers()); + + let dials_set: HashSet<_> = dials.into_iter().collect(); + + // No duplicates + assert_eq!(dials_set.len(), config.prune_peers()); + + //all dial peers must be in px + assert!(dials_set.is_subset( + &px.iter() + .map(|i| *i.peer_id.as_ref().unwrap()) + .collect::>() + )); +} + +#[test] +fn test_send_px_and_backoff_in_prune() { + let config: Config = Config::default(); + + //build mesh with enough peers for px + let (mut gs, peers, receivers, topics) = inject_nodes1() + .peer_no(config.prune_peers() + 1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .create_network(); + + //send prune to peer + gs.send_graft_prune( + HashMap::new(), + vec![(peers[0], vec![topics[0].clone()])] + .into_iter() + .collect(), + HashSet::new(), + ); + + //check prune message + let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { + peer_id == &peers[0] + && match m { + RpcOut::Prune(Prune { + topic_hash, + peers, + backoff, + }) => { + topic_hash == &topics[0] && + peers.len() == config.prune_peers() && + //all peers are different + peers.iter().collect::>().len() == + config.prune_peers() && + backoff.unwrap() == config.prune_backoff().as_secs() + } + _ => false, + } + }); + assert_eq!(control_msgs, 1); +} + +#[test] +fn test_prune_backoffed_peer_on_graft() { + let config: Config = Config::default(); + + //build mesh with enough peers for px + let (mut gs, peers, receivers, topics) = inject_nodes1() + .peer_no(config.prune_peers() + 1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .create_network(); + + //remove peer from mesh and send prune to peer => this adds a backoff for this peer + gs.mesh.get_mut(&topics[0]).unwrap().remove(&peers[0]); + gs.send_graft_prune( + HashMap::new(), + vec![(peers[0], vec![topics[0].clone()])] + .into_iter() + .collect(), + HashSet::new(), + ); + + //ignore all messages until now + let receivers = flush_events(&mut gs, receivers); + + //handle graft + gs.handle_graft(&peers[0], vec![topics[0].clone()]); + + //check prune message + let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { + peer_id == &peers[0] + && match m { + RpcOut::Prune(Prune { + topic_hash, + peers, + backoff, + }) => { + topic_hash == &topics[0] && + //no px in this case + peers.is_empty() && + backoff.unwrap() == config.prune_backoff().as_secs() + } + _ => false, + } + }); + assert_eq!(control_msgs, 1); +} + +#[test] +fn test_do_not_graft_within_backoff_period() { + let config = ConfigBuilder::default() + .backoff_slack(1) + .heartbeat_interval(Duration::from_millis(100)) + .build() + .unwrap(); + //only one peer => mesh too small and will try to regraft as early as possible + let (mut gs, peers, receivers, topics) = inject_nodes1() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .create_network(); + + //handle prune from peer with backoff of one second + gs.handle_prune(&peers[0], vec![(topics[0].clone(), Vec::new(), Some(1))]); + + //forget all events until now + let receivers = flush_events(&mut gs, receivers); + + //call heartbeat + gs.heartbeat(); + + //Sleep for one second and apply 10 regular heartbeats (interval = 100ms). + for _ in 0..10 { + sleep(Duration::from_millis(100)); + gs.heartbeat(); + } + + //Check that no graft got created (we have backoff_slack = 1 therefore one more heartbeat + // is needed). + let (control_msgs, receivers) = + count_control_msgs(receivers, |_, m| matches!(m, RpcOut::Graft { .. })); + assert_eq!( + control_msgs, 0, + "Graft message created too early within backoff period" + ); + + //Heartbeat one more time this should graft now + sleep(Duration::from_millis(100)); + gs.heartbeat(); + + //check that graft got created + let (control_msgs, _) = count_control_msgs(receivers, |_, m| matches!(m, RpcOut::Graft { .. })); + assert!( + control_msgs > 0, + "No graft message was created after backoff period" + ); +} + +#[test] +fn test_do_not_graft_within_default_backoff_period_after_receiving_prune_without_backoff() { + //set default backoff period to 1 second + let config = ConfigBuilder::default() + .prune_backoff(Duration::from_millis(90)) + .backoff_slack(1) + .heartbeat_interval(Duration::from_millis(100)) + .build() + .unwrap(); + //only one peer => mesh too small and will try to regraft as early as possible + let (mut gs, peers, receivers, topics) = inject_nodes1() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .create_network(); + + //handle prune from peer without a specified backoff + gs.handle_prune(&peers[0], vec![(topics[0].clone(), Vec::new(), None)]); + + //forget all events until now + let receivers = flush_events(&mut gs, receivers); + + //call heartbeat + gs.heartbeat(); + + //Apply one more heartbeat + sleep(Duration::from_millis(100)); + gs.heartbeat(); + + //Check that no graft got created (we have backoff_slack = 1 therefore one more heartbeat + // is needed). + let (control_msgs, receivers) = + count_control_msgs(receivers, |_, m| matches!(m, RpcOut::Graft { .. })); + assert_eq!( + control_msgs, 0, + "Graft message created too early within backoff period" + ); + + //Heartbeat one more time this should graft now + sleep(Duration::from_millis(100)); + gs.heartbeat(); + + //check that graft got created + let (control_msgs, _) = count_control_msgs(receivers, |_, m| matches!(m, RpcOut::Graft { .. })); + assert!( + control_msgs > 0, + "No graft message was created after backoff period" + ); +} + +#[test] +fn test_unsubscribe_backoff() { + const HEARTBEAT_INTERVAL: Duration = Duration::from_millis(100); + let config = ConfigBuilder::default() + .backoff_slack(1) + // ensure a prune_backoff > unsubscribe_backoff + .prune_backoff(Duration::from_secs(5)) + .unsubscribe_backoff(1) + .heartbeat_interval(HEARTBEAT_INTERVAL) + .build() + .unwrap(); + + let topic = String::from("test"); + // only one peer => mesh too small and will try to regraft as early as possible + let (mut gs, _, receivers, topics) = inject_nodes1() + .peer_no(1) + .topics(vec![topic.clone()]) + .to_subscribe(true) + .gs_config(config) + .create_network(); + + let _ = gs.unsubscribe(&Topic::new(topic)); + + let (control_msgs, receivers) = count_control_msgs(receivers, |_, m| match m { + RpcOut::Prune(Prune { backoff, .. }) => backoff == &Some(1), + _ => false, + }); + assert_eq!( + control_msgs, 1, + "Peer should be pruned with `unsubscribe_backoff`." + ); + + let _ = gs.subscribe(&Topic::new(topics[0].to_string())); + + // forget all events until now + let receivers = flush_events(&mut gs, receivers); + + // call heartbeat + gs.heartbeat(); + + // Sleep for one second and apply 10 regular heartbeats (interval = 100ms). + for _ in 0..10 { + sleep(HEARTBEAT_INTERVAL); + gs.heartbeat(); + } + + // Check that no graft got created (we have backoff_slack = 1 therefore one more heartbeat + // is needed). + let (control_msgs, receivers) = + count_control_msgs(receivers, |_, m| matches!(m, RpcOut::Graft { .. })); + assert_eq!( + control_msgs, 0, + "Graft message created too early within backoff period" + ); + + // Heartbeat one more time this should graft now + sleep(HEARTBEAT_INTERVAL); + gs.heartbeat(); + + // check that graft got created + let (control_msgs, _) = count_control_msgs(receivers, |_, m| matches!(m, RpcOut::Graft { .. })); + assert!( + control_msgs > 0, + "No graft message was created after backoff period" + ); +} + +#[test] +fn test_flood_publish() { + let config: Config = Config::default(); + + let topic = "test"; + // Adds more peers than mesh can hold to test flood publishing + let (mut gs, _, receivers, _) = inject_nodes1() + .peer_no(config.mesh_n_high() + 10) + .topics(vec![topic.into()]) + .to_subscribe(true) + .create_network(); + + //publish message + let publish_data = vec![0; 42]; + gs.publish(Topic::new(topic), publish_data).unwrap(); + + // Collect all publish messages + let publishes = receivers + .into_values() + .fold(vec![], |mut collected_publish, c| { + let priority = c.priority.into_inner(); + while !priority.is_empty() { + if let Ok(RpcOut::Publish { message, .. }) = priority.try_recv() { + collected_publish.push(message); + } + } + collected_publish + }); + + // Transform the inbound message + let message = &gs + .data_transform + .inbound_transform( + publishes + .first() + .expect("Should contain > 0 entries") + .clone(), + ) + .unwrap(); + + let msg_id = gs.config.message_id(message); + + let config: Config = Config::default(); + assert_eq!( + publishes.len(), + config.mesh_n_high() + 10, + "Should send a publish message to all known peers" + ); + + assert!( + gs.mcache.get(&msg_id).is_some(), + "Message cache should contain published message" + ); +} + +#[test] +fn test_gossip_to_at_least_gossip_lazy_peers() { + let config: Config = Config::default(); + + //add more peers than in mesh to test gossipping + //by default only mesh_n_low peers will get added to mesh + let (mut gs, _, receivers, topic_hashes) = inject_nodes1() + .peer_no(config.mesh_n_low() + config.gossip_lazy() + 1) + .topics(vec!["topic".into()]) + .to_subscribe(true) + .create_network(); + + //receive message + let raw_message = RawMessage { + source: Some(PeerId::random()), + data: vec![], + sequence_number: Some(0), + topic: topic_hashes[0].clone(), + signature: None, + key: None, + validated: true, + }; + gs.handle_received_message(raw_message.clone(), &PeerId::random()); + + //emit gossip + gs.emit_gossip(); + + // Transform the inbound message + let message = &gs.data_transform.inbound_transform(raw_message).unwrap(); + + let msg_id = gs.config.message_id(message); + + //check that exactly config.gossip_lazy() many gossip messages were sent. + let (control_msgs, _) = count_control_msgs(receivers, |_, action| match action { + RpcOut::IHave(IHave { + topic_hash, + message_ids, + }) => topic_hash == &topic_hashes[0] && message_ids.iter().any(|id| id == &msg_id), + _ => false, + }); + assert_eq!(control_msgs, config.gossip_lazy()); +} + +#[test] +fn test_gossip_to_at_most_gossip_factor_peers() { + let config: Config = Config::default(); + + //add a lot of peers + let m = config.mesh_n_low() + config.gossip_lazy() * (2.0 / config.gossip_factor()) as usize; + let (mut gs, _, receivers, topic_hashes) = inject_nodes1() + .peer_no(m) + .topics(vec!["topic".into()]) + .to_subscribe(true) + .create_network(); + + //receive message + let raw_message = RawMessage { + source: Some(PeerId::random()), + data: vec![], + sequence_number: Some(0), + topic: topic_hashes[0].clone(), + signature: None, + key: None, + validated: true, + }; + gs.handle_received_message(raw_message.clone(), &PeerId::random()); + + //emit gossip + gs.emit_gossip(); + + // Transform the inbound message + let message = &gs.data_transform.inbound_transform(raw_message).unwrap(); + + let msg_id = gs.config.message_id(message); + //check that exactly config.gossip_lazy() many gossip messages were sent. + let (control_msgs, _) = count_control_msgs(receivers, |_, action| match action { + RpcOut::IHave(IHave { + topic_hash, + message_ids, + }) => topic_hash == &topic_hashes[0] && message_ids.iter().any(|id| id == &msg_id), + _ => false, + }); + assert_eq!( + control_msgs, + ((m - config.mesh_n_low()) as f64 * config.gossip_factor()) as usize + ); +} + +#[test] +fn test_accept_only_outbound_peer_grafts_when_mesh_full() { + let config: Config = Config::default(); + + //enough peers to fill the mesh + let (mut gs, peers, _, topics) = inject_nodes1() + .peer_no(config.mesh_n_high()) + .topics(vec!["test".into()]) + .to_subscribe(true) + .create_network(); + + // graft all the peers => this will fill the mesh + for peer in peers { + gs.handle_graft(&peer, topics.clone()); + } + + //assert current mesh size + assert_eq!(gs.mesh[&topics[0]].len(), config.mesh_n_high()); + + //create an outbound and an inbound peer + let (inbound, _in_reciver) = add_peer(&mut gs, &topics, false, false); + let (outbound, _out_receiver) = add_peer(&mut gs, &topics, true, false); + + //send grafts + gs.handle_graft(&inbound, vec![topics[0].clone()]); + gs.handle_graft(&outbound, vec![topics[0].clone()]); + + //assert mesh size + assert_eq!(gs.mesh[&topics[0]].len(), config.mesh_n_high() + 1); + + //inbound is not in mesh + assert!(!gs.mesh[&topics[0]].contains(&inbound)); + + //outbound is in mesh + assert!(gs.mesh[&topics[0]].contains(&outbound)); +} + +#[test] +fn test_do_not_remove_too_many_outbound_peers() { + //use an extreme case to catch errors with high probability + let m = 50; + let n = 2 * m; + let config = ConfigBuilder::default() + .mesh_n_high(n) + .mesh_n(n) + .mesh_n_low(n) + .mesh_outbound_min(m) + .build() + .unwrap(); + + //fill the mesh with inbound connections + let (mut gs, peers, _receivers, topics) = inject_nodes1() + .peer_no(n) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .create_network(); + + // graft all the peers + for peer in peers { + gs.handle_graft(&peer, topics.clone()); + } + + //create m outbound connections and graft (we will accept the graft) + let mut outbound = HashSet::new(); + for _ in 0..m { + let (peer, _) = add_peer(&mut gs, &topics, true, false); + outbound.insert(peer); + gs.handle_graft(&peer, topics.clone()); + } + + //mesh is overly full + assert_eq!(gs.mesh.get(&topics[0]).unwrap().len(), n + m); + + // run a heartbeat + gs.heartbeat(); + + // Peers should be removed to reach n + assert_eq!(gs.mesh.get(&topics[0]).unwrap().len(), n); + + //all outbound peers are still in the mesh + assert!(outbound.iter().all(|p| gs.mesh[&topics[0]].contains(p))); +} + +#[test] +fn test_add_outbound_peers_if_min_is_not_satisfied() { + let config: Config = Config::default(); + + // Fill full mesh with inbound peers + let (mut gs, peers, _, topics) = inject_nodes1() + .peer_no(config.mesh_n_high()) + .topics(vec!["test".into()]) + .to_subscribe(true) + .create_network(); + + // graft all the peers + for peer in peers { + gs.handle_graft(&peer, topics.clone()); + } + + //create config.mesh_outbound_min() many outbound connections without grafting + let mut peers = vec![]; + for _ in 0..config.mesh_outbound_min() { + peers.push(add_peer(&mut gs, &topics, true, false)); + } + + // Nothing changed in the mesh yet + assert_eq!(gs.mesh[&topics[0]].len(), config.mesh_n_high()); + + // run a heartbeat + gs.heartbeat(); + + // The outbound peers got additionally added + assert_eq!( + gs.mesh[&topics[0]].len(), + config.mesh_n_high() + config.mesh_outbound_min() + ); +} + +#[test] +fn test_prune_negative_scored_peers() { + let config = Config::default(); + + //build mesh with one peer + let (mut gs, peers, receivers, topics) = inject_nodes1() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config.clone()) + .explicit(0) + .outbound(0) + .scoring(Some(( + PeerScoreParams::default(), + PeerScoreThresholds::default(), + ))) + .create_network(); + + //add penalty to peer + gs.peer_score.as_mut().unwrap().0.add_penalty(&peers[0], 1); + + //execute heartbeat + gs.heartbeat(); + + //peer should not be in mesh anymore + assert!(gs.mesh[&topics[0]].is_empty()); + + //check prune message + let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { + peer_id == &peers[0] + && match m { + RpcOut::Prune(Prune { + topic_hash, + peers, + backoff, + }) => { + topic_hash == &topics[0] && + //no px in this case + peers.is_empty() && + backoff.unwrap() == config.prune_backoff().as_secs() + } + _ => false, + } + }); + assert_eq!(control_msgs, 1); +} + +#[test] +fn test_dont_graft_to_negative_scored_peers() { + let config = Config::default(); + //init full mesh + let (mut gs, peers, _, topics) = inject_nodes1() + .peer_no(config.mesh_n_high()) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .scoring(Some(( + PeerScoreParams::default(), + PeerScoreThresholds::default(), + ))) + .create_network(); + + //add two additional peers that will not be part of the mesh + let (p1, _receiver1) = add_peer(&mut gs, &topics, false, false); + let (p2, _receiver2) = add_peer(&mut gs, &topics, false, false); + + //reduce score of p1 to negative + gs.peer_score.as_mut().unwrap().0.add_penalty(&p1, 1); + + //handle prunes of all other peers + for p in peers { + gs.handle_prune(&p, vec![(topics[0].clone(), Vec::new(), None)]); + } + + //heartbeat + gs.heartbeat(); + + //assert that mesh only contains p2 + assert_eq!(gs.mesh.get(&topics[0]).unwrap().len(), 1); + assert!(gs.mesh.get(&topics[0]).unwrap().contains(&p2)); +} + +///Note that in this test also without a penalty the px would be ignored because of the +/// acceptPXThreshold, but the spec still explicitely states the rule that px from negative +/// peers should get ignored, therefore we test it here. +#[test] +fn test_ignore_px_from_negative_scored_peer() { + let config = Config::default(); + + //build mesh with one peer + let (mut gs, peers, _, topics) = inject_nodes1() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config.clone()) + .scoring(Some(( + PeerScoreParams::default(), + PeerScoreThresholds::default(), + ))) + .create_network(); + + //penalize peer + gs.peer_score.as_mut().unwrap().0.add_penalty(&peers[0], 1); + + //handle prune from single peer with px peers + let px = vec![PeerInfo { + peer_id: Some(PeerId::random()), + }]; + + gs.handle_prune( + &peers[0], + vec![( + topics[0].clone(), + px, + Some(config.prune_backoff().as_secs()), + )], + ); + + //assert no dials + assert_eq!( + gs.events + .iter() + .filter(|e| matches!(e, ToSwarm::Dial { .. })) + .count(), + 0 + ); +} + +#[test] +fn test_only_send_nonnegative_scoring_peers_in_px() { + let config = ConfigBuilder::default() + .prune_peers(16) + .do_px() + .build() + .unwrap(); + + // Build mesh with three peer + let (mut gs, peers, receivers, topics) = inject_nodes1() + .peer_no(3) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .explicit(0) + .outbound(0) + .scoring(Some(( + PeerScoreParams::default(), + PeerScoreThresholds::default(), + ))) + .create_network(); + + // Penalize first peer + gs.peer_score.as_mut().unwrap().0.add_penalty(&peers[0], 1); + + // Prune second peer + gs.send_graft_prune( + HashMap::new(), + vec![(peers[1], vec![topics[0].clone()])] + .into_iter() + .collect(), + HashSet::new(), + ); + + // Check that px in prune message only contains third peer + let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { + peer_id == &peers[1] + && match m { + RpcOut::Prune(Prune { + topic_hash, + peers: px, + .. + }) => { + topic_hash == &topics[0] + && px.len() == 1 + && px[0].peer_id.as_ref().unwrap() == &peers[2] + } + _ => false, + } + }); + assert_eq!(control_msgs, 1); +} + +#[test] +fn test_do_not_gossip_to_peers_below_gossip_threshold() { + let config = Config::default(); + let peer_score_params = PeerScoreParams::default(); + let peer_score_thresholds = PeerScoreThresholds { + gossip_threshold: 3.0 * peer_score_params.behaviour_penalty_weight, + ..PeerScoreThresholds::default() + }; + + // Build full mesh + let (mut gs, peers, mut receivers, topics) = inject_nodes1() + .peer_no(config.mesh_n_high()) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + // Graft all the peer + for peer in peers { + gs.handle_graft(&peer, topics.clone()); + } + + // Add two additional peers that will not be part of the mesh + let (p1, receiver1) = add_peer(&mut gs, &topics, false, false); + receivers.insert(p1, receiver1); + let (p2, receiver2) = add_peer(&mut gs, &topics, false, false); + receivers.insert(p2, receiver2); + + // Reduce score of p1 below peer_score_thresholds.gossip_threshold + // note that penalties get squared so two penalties means a score of + // 4 * peer_score_params.behaviour_penalty_weight. + gs.peer_score.as_mut().unwrap().0.add_penalty(&p1, 2); + + // Reduce score of p2 below 0 but not below peer_score_thresholds.gossip_threshold + gs.peer_score.as_mut().unwrap().0.add_penalty(&p2, 1); + + // Receive message + let raw_message = RawMessage { + source: Some(PeerId::random()), + data: vec![], + sequence_number: Some(0), + topic: topics[0].clone(), + signature: None, + key: None, + validated: true, + }; + gs.handle_received_message(raw_message.clone(), &PeerId::random()); + + // Transform the inbound message + let message = &gs.data_transform.inbound_transform(raw_message).unwrap(); + + let msg_id = gs.config.message_id(message); + + // Emit gossip + gs.emit_gossip(); + + // Check that exactly one gossip messages got sent and it got sent to p2 + let (control_msgs, _) = count_control_msgs(receivers, |peer, action| match action { + RpcOut::IHave(IHave { + topic_hash, + message_ids, + }) => { + if topic_hash == &topics[0] && message_ids.iter().any(|id| id == &msg_id) { + assert_eq!(peer, &p2); + true + } else { + false + } + } + _ => false, + }); + assert_eq!(control_msgs, 1); +} + +#[test] +fn test_iwant_msg_from_peer_below_gossip_threshold_gets_ignored() { + let config = Config::default(); + let peer_score_params = PeerScoreParams::default(); + let peer_score_thresholds = PeerScoreThresholds { + gossip_threshold: 3.0 * peer_score_params.behaviour_penalty_weight, + ..PeerScoreThresholds::default() + }; + + // Build full mesh + let (mut gs, peers, mut receivers, topics) = inject_nodes1() + .peer_no(config.mesh_n_high()) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + // Graft all the peer + for peer in peers { + gs.handle_graft(&peer, topics.clone()); + } + + // Add two additional peers that will not be part of the mesh + let (p1, receiver1) = add_peer(&mut gs, &topics, false, false); + receivers.insert(p1, receiver1); + let (p2, receiver2) = add_peer(&mut gs, &topics, false, false); + receivers.insert(p2, receiver2); + + // Reduce score of p1 below peer_score_thresholds.gossip_threshold + // note that penalties get squared so two penalties means a score of + // 4 * peer_score_params.behaviour_penalty_weight. + gs.peer_score.as_mut().unwrap().0.add_penalty(&p1, 2); + + // Reduce score of p2 below 0 but not below peer_score_thresholds.gossip_threshold + gs.peer_score.as_mut().unwrap().0.add_penalty(&p2, 1); + + // Receive message + let raw_message = RawMessage { + source: Some(PeerId::random()), + data: vec![], + sequence_number: Some(0), + topic: topics[0].clone(), + signature: None, + key: None, + validated: true, + }; + gs.handle_received_message(raw_message.clone(), &PeerId::random()); + + // Transform the inbound message + let message = &gs.data_transform.inbound_transform(raw_message).unwrap(); + + let msg_id = gs.config.message_id(message); + + gs.handle_iwant(&p1, vec![msg_id.clone()]); + gs.handle_iwant(&p2, vec![msg_id.clone()]); + + // the messages we are sending + let sent_messages = + receivers + .into_iter() + .fold(vec![], |mut collected_messages, (peer_id, c)| { + let non_priority = c.non_priority.into_inner(); + while !non_priority.is_empty() { + if let Ok(RpcOut::Forward { message, .. }) = non_priority.try_recv() { + collected_messages.push((peer_id, message)); + } + } + collected_messages + }); + + //the message got sent to p2 + assert!(sent_messages + .iter() + .map(|(peer_id, msg)| ( + peer_id, + gs.data_transform.inbound_transform(msg.clone()).unwrap() + )) + .any(|(peer_id, msg)| peer_id == &p2 && gs.config.message_id(&msg) == msg_id)); + //the message got not sent to p1 + assert!(sent_messages + .iter() + .map(|(peer_id, msg)| ( + peer_id, + gs.data_transform.inbound_transform(msg.clone()).unwrap() + )) + .all(|(peer_id, msg)| !(peer_id == &p1 && gs.config.message_id(&msg) == msg_id))); +} + +#[test] +fn test_ihave_msg_from_peer_below_gossip_threshold_gets_ignored() { + let config = Config::default(); + let peer_score_params = PeerScoreParams::default(); + let peer_score_thresholds = PeerScoreThresholds { + gossip_threshold: 3.0 * peer_score_params.behaviour_penalty_weight, + ..PeerScoreThresholds::default() + }; + //build full mesh + let (mut gs, peers, mut receivers, topics) = inject_nodes1() + .peer_no(config.mesh_n_high()) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + // graft all the peer + for peer in peers { + gs.handle_graft(&peer, topics.clone()); + } + + //add two additional peers that will not be part of the mesh + let (p1, receiver1) = add_peer(&mut gs, &topics, false, false); + receivers.insert(p1, receiver1); + let (p2, receiver2) = add_peer(&mut gs, &topics, false, false); + receivers.insert(p2, receiver2); + + //reduce score of p1 below peer_score_thresholds.gossip_threshold + //note that penalties get squared so two penalties means a score of + // 4 * peer_score_params.behaviour_penalty_weight. + gs.peer_score.as_mut().unwrap().0.add_penalty(&p1, 2); + + //reduce score of p2 below 0 but not below peer_score_thresholds.gossip_threshold + gs.peer_score.as_mut().unwrap().0.add_penalty(&p2, 1); + + //message that other peers have + let raw_message = RawMessage { + source: Some(PeerId::random()), + data: vec![], + sequence_number: Some(0), + topic: topics[0].clone(), + signature: None, + key: None, + validated: true, + }; + + // Transform the inbound message + let message = &gs.data_transform.inbound_transform(raw_message).unwrap(); + + let msg_id = gs.config.message_id(message); + + gs.handle_ihave(&p1, vec![(topics[0].clone(), vec![msg_id.clone()])]); + gs.handle_ihave(&p2, vec![(topics[0].clone(), vec![msg_id.clone()])]); + + // check that we sent exactly one IWANT request to p2 + let (control_msgs, _) = count_control_msgs(receivers, |peer, c| match c { + RpcOut::IWant(IWant { message_ids }) => { + if message_ids.iter().any(|m| m == &msg_id) { + assert_eq!(peer, &p2); + true + } else { + false + } + } + _ => false, + }); + assert_eq!(control_msgs, 1); +} + +#[test] +fn test_do_not_publish_to_peer_below_publish_threshold() { + let config = ConfigBuilder::default() + .flood_publish(false) + .build() + .unwrap(); + let peer_score_params = PeerScoreParams::default(); + let peer_score_thresholds = PeerScoreThresholds { + gossip_threshold: 0.5 * peer_score_params.behaviour_penalty_weight, + publish_threshold: 3.0 * peer_score_params.behaviour_penalty_weight, + ..PeerScoreThresholds::default() + }; + + //build mesh with no peers and no subscribed topics + let (mut gs, _, mut receivers, _) = inject_nodes1() + .gs_config(config) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + //create a new topic for which we are not subscribed + let topic = Topic::new("test"); + let topics = vec![topic.hash()]; + + //add two additional peers that will be added to the mesh + let (p1, receiver1) = add_peer(&mut gs, &topics, false, false); + receivers.insert(p1, receiver1); + let (p2, receiver2) = add_peer(&mut gs, &topics, false, false); + receivers.insert(p2, receiver2); + + //reduce score of p1 below peer_score_thresholds.publish_threshold + //note that penalties get squared so two penalties means a score of + // 4 * peer_score_params.behaviour_penalty_weight. + gs.peer_score.as_mut().unwrap().0.add_penalty(&p1, 2); + + //reduce score of p2 below 0 but not below peer_score_thresholds.publish_threshold + gs.peer_score.as_mut().unwrap().0.add_penalty(&p2, 1); + + //a heartbeat will remove the peers from the mesh + gs.heartbeat(); + + // publish on topic + let publish_data = vec![0; 42]; + gs.publish(topic, publish_data).unwrap(); + + // Collect all publish messages + let publishes = receivers + .into_iter() + .fold(vec![], |mut collected_publish, (peer_id, c)| { + let priority = c.priority.into_inner(); + while !priority.is_empty() { + if let Ok(RpcOut::Publish { message, .. }) = priority.try_recv() { + collected_publish.push((peer_id, message)); + } + } + collected_publish + }); + + //assert only published to p2 + assert_eq!(publishes.len(), 1); + assert_eq!(publishes[0].0, p2); +} + +#[test] +fn test_do_not_flood_publish_to_peer_below_publish_threshold() { + let config = Config::default(); + let peer_score_params = PeerScoreParams::default(); + let peer_score_thresholds = PeerScoreThresholds { + gossip_threshold: 0.5 * peer_score_params.behaviour_penalty_weight, + publish_threshold: 3.0 * peer_score_params.behaviour_penalty_weight, + ..PeerScoreThresholds::default() + }; + //build mesh with no peers + let (mut gs, _, mut receivers, topics) = inject_nodes1() + .topics(vec!["test".into()]) + .gs_config(config) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + //add two additional peers that will be added to the mesh + let (p1, receiver1) = add_peer(&mut gs, &topics, false, false); + receivers.insert(p1, receiver1); + let (p2, receiver2) = add_peer(&mut gs, &topics, false, false); + receivers.insert(p2, receiver2); + + //reduce score of p1 below peer_score_thresholds.publish_threshold + //note that penalties get squared so two penalties means a score of + // 4 * peer_score_params.behaviour_penalty_weight. + gs.peer_score.as_mut().unwrap().0.add_penalty(&p1, 2); + + //reduce score of p2 below 0 but not below peer_score_thresholds.publish_threshold + gs.peer_score.as_mut().unwrap().0.add_penalty(&p2, 1); + + //a heartbeat will remove the peers from the mesh + gs.heartbeat(); + + // publish on topic + let publish_data = vec![0; 42]; + gs.publish(Topic::new("test"), publish_data).unwrap(); + + // Collect all publish messages + let publishes = receivers + .into_iter() + .fold(vec![], |mut collected_publish, (peer_id, c)| { + let priority = c.priority.into_inner(); + while !priority.is_empty() { + if let Ok(RpcOut::Publish { message, .. }) = priority.try_recv() { + collected_publish.push((peer_id, message)) + } + } + collected_publish + }); + + //assert only published to p2 + assert_eq!(publishes.len(), 1); + assert!(publishes[0].0 == p2); +} + +#[test] +fn test_ignore_rpc_from_peers_below_graylist_threshold() { + let config = Config::default(); + let peer_score_params = PeerScoreParams::default(); + let peer_score_thresholds = PeerScoreThresholds { + gossip_threshold: 0.5 * peer_score_params.behaviour_penalty_weight, + publish_threshold: 0.5 * peer_score_params.behaviour_penalty_weight, + graylist_threshold: 3.0 * peer_score_params.behaviour_penalty_weight, + ..PeerScoreThresholds::default() + }; + + //build mesh with no peers + let (mut gs, _, _, topics) = inject_nodes1() + .topics(vec!["test".into()]) + .gs_config(config.clone()) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + //add two additional peers that will be added to the mesh + let (p1, _receiver1) = add_peer(&mut gs, &topics, false, false); + let (p2, _receiver2) = add_peer(&mut gs, &topics, false, false); + + //reduce score of p1 below peer_score_thresholds.graylist_threshold + //note that penalties get squared so two penalties means a score of + // 4 * peer_score_params.behaviour_penalty_weight. + gs.peer_score.as_mut().unwrap().0.add_penalty(&p1, 2); + + //reduce score of p2 below publish_threshold but not below graylist_threshold + gs.peer_score.as_mut().unwrap().0.add_penalty(&p2, 1); + + let raw_message1 = RawMessage { + source: Some(PeerId::random()), + data: vec![1, 2, 3, 4], + sequence_number: Some(1u64), + topic: topics[0].clone(), + signature: None, + key: None, + validated: true, + }; + + let raw_message2 = RawMessage { + source: Some(PeerId::random()), + data: vec![1, 2, 3, 4, 5], + sequence_number: Some(2u64), + topic: topics[0].clone(), + signature: None, + key: None, + validated: true, + }; + + let raw_message3 = RawMessage { + source: Some(PeerId::random()), + data: vec![1, 2, 3, 4, 5, 6], + sequence_number: Some(3u64), + topic: topics[0].clone(), + signature: None, + key: None, + validated: true, + }; + + let raw_message4 = RawMessage { + source: Some(PeerId::random()), + data: vec![1, 2, 3, 4, 5, 6, 7], + sequence_number: Some(4u64), + topic: topics[0].clone(), + signature: None, + key: None, + validated: true, + }; + + // Transform the inbound message + let message2 = &gs.data_transform.inbound_transform(raw_message2).unwrap(); + + // Transform the inbound message + let message4 = &gs.data_transform.inbound_transform(raw_message4).unwrap(); + + let subscription = Subscription { + action: SubscriptionAction::Subscribe, + topic_hash: topics[0].clone(), + }; + + let control_action = ControlAction::IHave(IHave { + topic_hash: topics[0].clone(), + message_ids: vec![config.message_id(message2)], + }); + + //clear events + gs.events.clear(); + + //receive from p1 + gs.on_connection_handler_event( + p1, + ConnectionId::new_unchecked(0), + HandlerEvent::Message { + rpc: Rpc { + messages: vec![raw_message1], + subscriptions: vec![subscription.clone()], + control_msgs: vec![control_action], + }, + invalid_messages: Vec::new(), + }, + ); + + //only the subscription event gets processed, the rest is dropped + assert_eq!(gs.events.len(), 1); + assert!(matches!( + gs.events[0], + ToSwarm::GenerateEvent(Event::Subscribed { .. }) + )); + + let control_action = ControlAction::IHave(IHave { + topic_hash: topics[0].clone(), + message_ids: vec![config.message_id(message4)], + }); + + //receive from p2 + gs.on_connection_handler_event( + p2, + ConnectionId::new_unchecked(0), + HandlerEvent::Message { + rpc: Rpc { + messages: vec![raw_message3], + subscriptions: vec![subscription], + control_msgs: vec![control_action], + }, + invalid_messages: Vec::new(), + }, + ); + + //events got processed + assert!(gs.events.len() > 1); +} + +#[test] +fn test_ignore_px_from_peers_below_accept_px_threshold() { + let config = ConfigBuilder::default().prune_peers(16).build().unwrap(); + let peer_score_params = PeerScoreParams::default(); + let peer_score_thresholds = PeerScoreThresholds { + accept_px_threshold: peer_score_params.app_specific_weight, + ..PeerScoreThresholds::default() + }; + // Build mesh with two peers + let (mut gs, peers, _, topics) = inject_nodes1() + .peer_no(2) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config.clone()) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + // Decrease score of first peer to less than accept_px_threshold + gs.set_application_score(&peers[0], 0.99); + + // Increase score of second peer to accept_px_threshold + gs.set_application_score(&peers[1], 1.0); + + // Handle prune from peer peers[0] with px peers + let px = vec![PeerInfo { + peer_id: Some(PeerId::random()), + }]; + gs.handle_prune( + &peers[0], + vec![( + topics[0].clone(), + px, + Some(config.prune_backoff().as_secs()), + )], + ); + + // Assert no dials + assert_eq!( + gs.events + .iter() + .filter(|e| matches!(e, ToSwarm::Dial { .. })) + .count(), + 0 + ); + + //handle prune from peer peers[1] with px peers + let px = vec![PeerInfo { + peer_id: Some(PeerId::random()), + }]; + gs.handle_prune( + &peers[1], + vec![( + topics[0].clone(), + px, + Some(config.prune_backoff().as_secs()), + )], + ); + + //assert there are dials now + assert!( + gs.events + .iter() + .filter(|e| matches!(e, ToSwarm::Dial { .. })) + .count() + > 0 + ); +} + +#[test] +fn test_keep_best_scoring_peers_on_oversubscription() { + let config = ConfigBuilder::default() + .mesh_n_low(15) + .mesh_n(30) + .mesh_n_high(60) + .retain_scores(29) + .build() + .unwrap(); + + //build mesh with more peers than mesh can hold + let n = config.mesh_n_high() + 1; + let (mut gs, peers, _receivers, topics) = inject_nodes1() + .peer_no(n) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config.clone()) + .explicit(0) + .outbound(n) + .scoring(Some(( + PeerScoreParams::default(), + PeerScoreThresholds::default(), + ))) + .create_network(); + + // graft all, will be accepted since the are outbound + for peer in &peers { + gs.handle_graft(peer, topics.clone()); + } + + //assign scores to peers equalling their index + + //set random positive scores + for (index, peer) in peers.iter().enumerate() { + gs.set_application_score(peer, index as f64); + } + + assert_eq!(gs.mesh[&topics[0]].len(), n); + + //heartbeat to prune some peers + gs.heartbeat(); + + assert_eq!(gs.mesh[&topics[0]].len(), config.mesh_n()); + + //mesh contains retain_scores best peers + assert!(gs.mesh[&topics[0]].is_superset( + &peers[(n - config.retain_scores())..] + .iter() + .cloned() + .collect() + )); +} + +#[test] +fn test_scoring_p1() { + let config = Config::default(); + let mut peer_score_params = PeerScoreParams::default(); + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let topic_params = TopicScoreParams { + time_in_mesh_weight: 2.0, + time_in_mesh_quantum: Duration::from_millis(50), + time_in_mesh_cap: 10.0, + topic_weight: 0.7, + ..TopicScoreParams::default() + }; + peer_score_params + .topics + .insert(topic_hash, topic_params.clone()); + let peer_score_thresholds = PeerScoreThresholds::default(); + + //build mesh with one peer + let (mut gs, peers, _, _) = inject_nodes1() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + //sleep for 2 times the mesh_quantum + sleep(topic_params.time_in_mesh_quantum * 2); + //refresh scores + gs.peer_score.as_mut().unwrap().0.refresh_scores(); + assert!( + gs.peer_score.as_ref().unwrap().0.score(&peers[0]) + >= 2.0 * topic_params.time_in_mesh_weight * topic_params.topic_weight, + "score should be at least 2 * time_in_mesh_weight * topic_weight" + ); + assert!( + gs.peer_score.as_ref().unwrap().0.score(&peers[0]) + < 3.0 * topic_params.time_in_mesh_weight * topic_params.topic_weight, + "score should be less than 3 * time_in_mesh_weight * topic_weight" + ); + + //sleep again for 2 times the mesh_quantum + sleep(topic_params.time_in_mesh_quantum * 2); + //refresh scores + gs.peer_score.as_mut().unwrap().0.refresh_scores(); + assert!( + gs.peer_score.as_ref().unwrap().0.score(&peers[0]) + >= 2.0 * topic_params.time_in_mesh_weight * topic_params.topic_weight, + "score should be at least 4 * time_in_mesh_weight * topic_weight" + ); + + //sleep for enough periods to reach maximum + sleep(topic_params.time_in_mesh_quantum * (topic_params.time_in_mesh_cap - 3.0) as u32); + //refresh scores + gs.peer_score.as_mut().unwrap().0.refresh_scores(); + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&peers[0]), + topic_params.time_in_mesh_cap + * topic_params.time_in_mesh_weight + * topic_params.topic_weight, + "score should be exactly time_in_mesh_cap * time_in_mesh_weight * topic_weight" + ); +} + +fn random_message(seq: &mut u64, topics: &[TopicHash]) -> RawMessage { + let mut rng = rand::thread_rng(); + *seq += 1; + RawMessage { + source: Some(PeerId::random()), + data: (0..rng.gen_range(10..30)).map(|_| rng.gen()).collect(), + sequence_number: Some(*seq), + topic: topics[rng.gen_range(0..topics.len())].clone(), + signature: None, + key: None, + validated: true, + } +} + +#[test] +fn test_scoring_p2() { + let config = Config::default(); + let mut peer_score_params = PeerScoreParams::default(); + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let topic_params = TopicScoreParams { + time_in_mesh_weight: 0.0, //deactivate time in mesh + first_message_deliveries_weight: 2.0, + first_message_deliveries_cap: 10.0, + first_message_deliveries_decay: 0.9, + topic_weight: 0.7, + ..TopicScoreParams::default() + }; + peer_score_params + .topics + .insert(topic_hash, topic_params.clone()); + let peer_score_thresholds = PeerScoreThresholds::default(); + + //build mesh with one peer + let (mut gs, peers, _, topics) = inject_nodes1() + .peer_no(2) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + let mut seq = 0; + let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { + gs.handle_received_message(msg, &peers[index]); + }; + + let m1 = random_message(&mut seq, &topics); + //peer 0 delivers message first + deliver_message(&mut gs, 0, m1.clone()); + //peer 1 delivers message second + deliver_message(&mut gs, 1, m1); + + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&peers[0]), + 1.0 * topic_params.first_message_deliveries_weight * topic_params.topic_weight, + "score should be exactly first_message_deliveries_weight * topic_weight" + ); + + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&peers[1]), + 0.0, + "there should be no score for second message deliveries * topic_weight" + ); + + //peer 2 delivers two new messages + deliver_message(&mut gs, 1, random_message(&mut seq, &topics)); + deliver_message(&mut gs, 1, random_message(&mut seq, &topics)); + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&peers[1]), + 2.0 * topic_params.first_message_deliveries_weight * topic_params.topic_weight, + "score should be exactly 2 * first_message_deliveries_weight * topic_weight" + ); + + //test decaying + gs.peer_score.as_mut().unwrap().0.refresh_scores(); + + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&peers[0]), + 1.0 * topic_params.first_message_deliveries_decay + * topic_params.first_message_deliveries_weight + * topic_params.topic_weight, + "score should be exactly first_message_deliveries_decay * \ + first_message_deliveries_weight * topic_weight" + ); + + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&peers[1]), + 2.0 * topic_params.first_message_deliveries_decay + * topic_params.first_message_deliveries_weight + * topic_params.topic_weight, + "score should be exactly 2 * first_message_deliveries_decay * \ + first_message_deliveries_weight * topic_weight" + ); + + //test cap + for _ in 0..topic_params.first_message_deliveries_cap as u64 { + deliver_message(&mut gs, 1, random_message(&mut seq, &topics)); + } + + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&peers[1]), + topic_params.first_message_deliveries_cap + * topic_params.first_message_deliveries_weight + * topic_params.topic_weight, + "score should be exactly first_message_deliveries_cap * \ + first_message_deliveries_weight * topic_weight" + ); +} + +#[test] +fn test_scoring_p3() { + let config = Config::default(); + let mut peer_score_params = PeerScoreParams::default(); + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let topic_params = TopicScoreParams { + time_in_mesh_weight: 0.0, //deactivate time in mesh + first_message_deliveries_weight: 0.0, //deactivate first time deliveries + mesh_message_deliveries_weight: -2.0, + mesh_message_deliveries_decay: 0.9, + mesh_message_deliveries_cap: 10.0, + mesh_message_deliveries_threshold: 5.0, + mesh_message_deliveries_activation: Duration::from_secs(1), + mesh_message_deliveries_window: Duration::from_millis(100), + topic_weight: 0.7, + ..TopicScoreParams::default() + }; + peer_score_params.topics.insert(topic_hash, topic_params); + let peer_score_thresholds = PeerScoreThresholds::default(); + + //build mesh with two peers + let (mut gs, peers, _, topics) = inject_nodes1() + .peer_no(2) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + let mut seq = 0; + let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { + gs.handle_received_message(msg, &peers[index]); + }; + + let mut expected_message_deliveries = 0.0; + + //messages used to test window + let m1 = random_message(&mut seq, &topics); + let m2 = random_message(&mut seq, &topics); + + //peer 1 delivers m1 + deliver_message(&mut gs, 1, m1.clone()); + + //peer 0 delivers two message + deliver_message(&mut gs, 0, random_message(&mut seq, &topics)); + deliver_message(&mut gs, 0, random_message(&mut seq, &topics)); + expected_message_deliveries += 2.0; + + sleep(Duration::from_millis(60)); + + //peer 1 delivers m2 + deliver_message(&mut gs, 1, m2.clone()); + + sleep(Duration::from_millis(70)); + //peer 0 delivers m1 and m2 only m2 gets counted + deliver_message(&mut gs, 0, m1); + deliver_message(&mut gs, 0, m2); + expected_message_deliveries += 1.0; + + sleep(Duration::from_millis(900)); + + //message deliveries penalties get activated, peer 0 has only delivered 3 messages and + // therefore gets a penalty + gs.peer_score.as_mut().unwrap().0.refresh_scores(); + expected_message_deliveries *= 0.9; //decay + + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&peers[0]), + (5f64 - expected_message_deliveries).powi(2) * -2.0 * 0.7 + ); + + // peer 0 delivers a lot of messages => message_deliveries should be capped at 10 + for _ in 0..20 { + deliver_message(&mut gs, 0, random_message(&mut seq, &topics)); + } + + expected_message_deliveries = 10.0; + + assert_eq!(gs.peer_score.as_ref().unwrap().0.score(&peers[0]), 0.0); + + //apply 10 decays + for _ in 0..10 { + gs.peer_score.as_mut().unwrap().0.refresh_scores(); + expected_message_deliveries *= 0.9; //decay + } + + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&peers[0]), + (5f64 - expected_message_deliveries).powi(2) * -2.0 * 0.7 + ); +} + +#[test] +fn test_scoring_p3b() { + let config = ConfigBuilder::default() + .prune_backoff(Duration::from_millis(100)) + .build() + .unwrap(); + let mut peer_score_params = PeerScoreParams::default(); + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let topic_params = TopicScoreParams { + time_in_mesh_weight: 0.0, //deactivate time in mesh + first_message_deliveries_weight: 0.0, //deactivate first time deliveries + mesh_message_deliveries_weight: -2.0, + mesh_message_deliveries_decay: 0.9, + mesh_message_deliveries_cap: 10.0, + mesh_message_deliveries_threshold: 5.0, + mesh_message_deliveries_activation: Duration::from_secs(1), + mesh_message_deliveries_window: Duration::from_millis(100), + mesh_failure_penalty_weight: -3.0, + mesh_failure_penalty_decay: 0.95, + topic_weight: 0.7, + ..Default::default() + }; + peer_score_params.topics.insert(topic_hash, topic_params); + peer_score_params.app_specific_weight = 1.0; + let peer_score_thresholds = PeerScoreThresholds::default(); + + //build mesh with one peer + let (mut gs, peers, _, topics) = inject_nodes1() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + let mut seq = 0; + let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { + gs.handle_received_message(msg, &peers[index]); + }; + + let mut expected_message_deliveries = 0.0; + + //add some positive score + gs.peer_score + .as_mut() + .unwrap() + .0 + .set_application_score(&peers[0], 100.0); + + //peer 0 delivers two message + deliver_message(&mut gs, 0, random_message(&mut seq, &topics)); + deliver_message(&mut gs, 0, random_message(&mut seq, &topics)); + expected_message_deliveries += 2.0; + + sleep(Duration::from_millis(1050)); + + //activation kicks in + gs.peer_score.as_mut().unwrap().0.refresh_scores(); + expected_message_deliveries *= 0.9; //decay + + //prune peer + gs.handle_prune(&peers[0], vec![(topics[0].clone(), vec![], None)]); + + //wait backoff + sleep(Duration::from_millis(130)); + + //regraft peer + gs.handle_graft(&peers[0], topics.clone()); + + //the score should now consider p3b + let mut expected_b3 = (5f64 - expected_message_deliveries).powi(2); + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&peers[0]), + 100.0 + expected_b3 * -3.0 * 0.7 + ); + + //we can also add a new p3 to the score + + //peer 0 delivers one message + deliver_message(&mut gs, 0, random_message(&mut seq, &topics)); + expected_message_deliveries += 1.0; + + sleep(Duration::from_millis(1050)); + gs.peer_score.as_mut().unwrap().0.refresh_scores(); + expected_message_deliveries *= 0.9; //decay + expected_b3 *= 0.95; + + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&peers[0]), + 100.0 + (expected_b3 * -3.0 + (5f64 - expected_message_deliveries).powi(2) * -2.0) * 0.7 + ); +} + +#[test] +fn test_scoring_p4_valid_message() { + let config = ConfigBuilder::default() + .validate_messages() + .build() + .unwrap(); + let mut peer_score_params = PeerScoreParams::default(); + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let topic_params = TopicScoreParams { + time_in_mesh_weight: 0.0, //deactivate time in mesh + first_message_deliveries_weight: 0.0, //deactivate first time deliveries + mesh_message_deliveries_weight: 0.0, //deactivate message deliveries + mesh_failure_penalty_weight: 0.0, //deactivate mesh failure penalties + invalid_message_deliveries_weight: -2.0, + invalid_message_deliveries_decay: 0.9, + topic_weight: 0.7, + ..Default::default() + }; + peer_score_params.topics.insert(topic_hash, topic_params); + peer_score_params.app_specific_weight = 1.0; + let peer_score_thresholds = PeerScoreThresholds::default(); + + //build mesh with two peers + let (mut gs, peers, _, topics) = inject_nodes1() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config.clone()) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + let mut seq = 0; + let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { + gs.handle_received_message(msg, &peers[index]); + }; + + //peer 0 delivers valid message + let m1 = random_message(&mut seq, &topics); + deliver_message(&mut gs, 0, m1.clone()); + + // Transform the inbound message + let message1 = &gs.data_transform.inbound_transform(m1).unwrap(); + + assert_eq!(gs.peer_score.as_ref().unwrap().0.score(&peers[0]), 0.0); + + //message m1 gets validated + gs.report_message_validation_result( + &config.message_id(message1), + &peers[0], + MessageAcceptance::Accept, + ) + .unwrap(); + + assert_eq!(gs.peer_score.as_ref().unwrap().0.score(&peers[0]), 0.0); +} + +#[test] +fn test_scoring_p4_invalid_signature() { + let config = ConfigBuilder::default() + .validate_messages() + .build() + .unwrap(); + let mut peer_score_params = PeerScoreParams::default(); + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let topic_params = TopicScoreParams { + time_in_mesh_weight: 0.0, //deactivate time in mesh + first_message_deliveries_weight: 0.0, //deactivate first time deliveries + mesh_message_deliveries_weight: 0.0, //deactivate message deliveries + mesh_failure_penalty_weight: 0.0, //deactivate mesh failure penalties + invalid_message_deliveries_weight: -2.0, + invalid_message_deliveries_decay: 0.9, + topic_weight: 0.7, + ..Default::default() + }; + peer_score_params.topics.insert(topic_hash, topic_params); + peer_score_params.app_specific_weight = 1.0; + let peer_score_thresholds = PeerScoreThresholds::default(); + + //build mesh with one peer + let (mut gs, peers, _, topics) = inject_nodes1() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + let mut seq = 0; + + //peer 0 delivers message with invalid signature + let m = random_message(&mut seq, &topics); + + gs.on_connection_handler_event( + peers[0], + ConnectionId::new_unchecked(0), + HandlerEvent::Message { + rpc: Rpc { + messages: vec![], + subscriptions: vec![], + control_msgs: vec![], + }, + invalid_messages: vec![(m, ValidationError::InvalidSignature)], + }, + ); + + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&peers[0]), + -2.0 * 0.7 + ); +} + +#[test] +fn test_scoring_p4_message_from_self() { + let config = ConfigBuilder::default() + .validate_messages() + .build() + .unwrap(); + let mut peer_score_params = PeerScoreParams::default(); + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let topic_params = TopicScoreParams { + time_in_mesh_weight: 0.0, //deactivate time in mesh + first_message_deliveries_weight: 0.0, //deactivate first time deliveries + mesh_message_deliveries_weight: 0.0, //deactivate message deliveries + mesh_failure_penalty_weight: 0.0, //deactivate mesh failure penalties + invalid_message_deliveries_weight: -2.0, + invalid_message_deliveries_decay: 0.9, + topic_weight: 0.7, + ..Default::default() + }; + peer_score_params.topics.insert(topic_hash, topic_params); + peer_score_params.app_specific_weight = 1.0; + let peer_score_thresholds = PeerScoreThresholds::default(); + + //build mesh with two peers + let (mut gs, peers, _, topics) = inject_nodes1() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + let mut seq = 0; + let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { + gs.handle_received_message(msg, &peers[index]); + }; + + //peer 0 delivers invalid message from self + let mut m = random_message(&mut seq, &topics); + m.source = Some(*gs.publish_config.get_own_id().unwrap()); + + deliver_message(&mut gs, 0, m); + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&peers[0]), + -2.0 * 0.7 + ); +} + +#[test] +fn test_scoring_p4_ignored_message() { + let config = ConfigBuilder::default() + .validate_messages() + .build() + .unwrap(); + let mut peer_score_params = PeerScoreParams::default(); + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let topic_params = TopicScoreParams { + time_in_mesh_weight: 0.0, //deactivate time in mesh + first_message_deliveries_weight: 0.0, //deactivate first time deliveries + mesh_message_deliveries_weight: 0.0, //deactivate message deliveries + mesh_failure_penalty_weight: 0.0, //deactivate mesh failure penalties + invalid_message_deliveries_weight: -2.0, + invalid_message_deliveries_decay: 0.9, + topic_weight: 0.7, + ..Default::default() + }; + peer_score_params.topics.insert(topic_hash, topic_params); + peer_score_params.app_specific_weight = 1.0; + let peer_score_thresholds = PeerScoreThresholds::default(); + + //build mesh with two peers + let (mut gs, peers, _, topics) = inject_nodes1() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config.clone()) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + let mut seq = 0; + let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { + gs.handle_received_message(msg, &peers[index]); + }; + + //peer 0 delivers ignored message + let m1 = random_message(&mut seq, &topics); + deliver_message(&mut gs, 0, m1.clone()); + + assert_eq!(gs.peer_score.as_ref().unwrap().0.score(&peers[0]), 0.0); + + // Transform the inbound message + let message1 = &gs.data_transform.inbound_transform(m1).unwrap(); + + //message m1 gets ignored + gs.report_message_validation_result( + &config.message_id(message1), + &peers[0], + MessageAcceptance::Ignore, + ) + .unwrap(); + + assert_eq!(gs.peer_score.as_ref().unwrap().0.score(&peers[0]), 0.0); +} + +#[test] +fn test_scoring_p4_application_invalidated_message() { + let config = ConfigBuilder::default() + .validate_messages() + .build() + .unwrap(); + let mut peer_score_params = PeerScoreParams::default(); + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let topic_params = TopicScoreParams { + time_in_mesh_weight: 0.0, //deactivate time in mesh + first_message_deliveries_weight: 0.0, //deactivate first time deliveries + mesh_message_deliveries_weight: 0.0, //deactivate message deliveries + mesh_failure_penalty_weight: 0.0, //deactivate mesh failure penalties + invalid_message_deliveries_weight: -2.0, + invalid_message_deliveries_decay: 0.9, + topic_weight: 0.7, + ..Default::default() + }; + peer_score_params.topics.insert(topic_hash, topic_params); + peer_score_params.app_specific_weight = 1.0; + let peer_score_thresholds = PeerScoreThresholds::default(); + + //build mesh with two peers + let (mut gs, peers, _, topics) = inject_nodes1() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config.clone()) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + let mut seq = 0; + let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { + gs.handle_received_message(msg, &peers[index]); + }; + + //peer 0 delivers invalid message + let m1 = random_message(&mut seq, &topics); + deliver_message(&mut gs, 0, m1.clone()); + + assert_eq!(gs.peer_score.as_ref().unwrap().0.score(&peers[0]), 0.0); + + // Transform the inbound message + let message1 = &gs.data_transform.inbound_transform(m1).unwrap(); + + //message m1 gets rejected + gs.report_message_validation_result( + &config.message_id(message1), + &peers[0], + MessageAcceptance::Reject, + ) + .unwrap(); + + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&peers[0]), + -2.0 * 0.7 + ); +} + +#[test] +fn test_scoring_p4_application_invalid_message_from_two_peers() { + let config = ConfigBuilder::default() + .validate_messages() + .build() + .unwrap(); + let mut peer_score_params = PeerScoreParams::default(); + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let topic_params = TopicScoreParams { + time_in_mesh_weight: 0.0, //deactivate time in mesh + first_message_deliveries_weight: 0.0, //deactivate first time deliveries + mesh_message_deliveries_weight: 0.0, //deactivate message deliveries + mesh_failure_penalty_weight: 0.0, //deactivate mesh failure penalties + invalid_message_deliveries_weight: -2.0, + invalid_message_deliveries_decay: 0.9, + topic_weight: 0.7, + ..Default::default() + }; + peer_score_params.topics.insert(topic_hash, topic_params); + peer_score_params.app_specific_weight = 1.0; + let peer_score_thresholds = PeerScoreThresholds::default(); + + //build mesh with two peers + let (mut gs, peers, _, topics) = inject_nodes1() + .peer_no(2) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config.clone()) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + let mut seq = 0; + let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { + gs.handle_received_message(msg, &peers[index]); + }; + + //peer 0 delivers invalid message + let m1 = random_message(&mut seq, &topics); + deliver_message(&mut gs, 0, m1.clone()); + + // Transform the inbound message + let message1 = &gs.data_transform.inbound_transform(m1.clone()).unwrap(); + + //peer 1 delivers same message + deliver_message(&mut gs, 1, m1); + + assert_eq!(gs.peer_score.as_ref().unwrap().0.score(&peers[0]), 0.0); + assert_eq!(gs.peer_score.as_ref().unwrap().0.score(&peers[1]), 0.0); + + //message m1 gets rejected + gs.report_message_validation_result( + &config.message_id(message1), + &peers[0], + MessageAcceptance::Reject, + ) + .unwrap(); + + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&peers[0]), + -2.0 * 0.7 + ); + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&peers[1]), + -2.0 * 0.7 + ); +} + +#[test] +fn test_scoring_p4_three_application_invalid_messages() { + let config = ConfigBuilder::default() + .validate_messages() + .build() + .unwrap(); + let mut peer_score_params = PeerScoreParams::default(); + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let topic_params = TopicScoreParams { + time_in_mesh_weight: 0.0, //deactivate time in mesh + first_message_deliveries_weight: 0.0, //deactivate first time deliveries + mesh_message_deliveries_weight: 0.0, //deactivate message deliveries + mesh_failure_penalty_weight: 0.0, //deactivate mesh failure penalties + invalid_message_deliveries_weight: -2.0, + invalid_message_deliveries_decay: 0.9, + topic_weight: 0.7, + ..Default::default() + }; + peer_score_params.topics.insert(topic_hash, topic_params); + peer_score_params.app_specific_weight = 1.0; + let peer_score_thresholds = PeerScoreThresholds::default(); + + //build mesh with one peer + let (mut gs, peers, _, topics) = inject_nodes1() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config.clone()) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + let mut seq = 0; + let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { + gs.handle_received_message(msg, &peers[index]); + }; + + //peer 0 delivers two invalid message + let m1 = random_message(&mut seq, &topics); + let m2 = random_message(&mut seq, &topics); + let m3 = random_message(&mut seq, &topics); + deliver_message(&mut gs, 0, m1.clone()); + deliver_message(&mut gs, 0, m2.clone()); + deliver_message(&mut gs, 0, m3.clone()); + + // Transform the inbound message + let message1 = &gs.data_transform.inbound_transform(m1).unwrap(); + + // Transform the inbound message + let message2 = &gs.data_transform.inbound_transform(m2).unwrap(); + // Transform the inbound message + let message3 = &gs.data_transform.inbound_transform(m3).unwrap(); + + assert_eq!(gs.peer_score.as_ref().unwrap().0.score(&peers[0]), 0.0); + + //messages gets rejected + gs.report_message_validation_result( + &config.message_id(message1), + &peers[0], + MessageAcceptance::Reject, + ) + .unwrap(); + gs.report_message_validation_result( + &config.message_id(message2), + &peers[0], + MessageAcceptance::Reject, + ) + .unwrap(); + gs.report_message_validation_result( + &config.message_id(message3), + &peers[0], + MessageAcceptance::Reject, + ) + .unwrap(); + + //number of invalid messages gets squared + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&peers[0]), + 9.0 * -2.0 * 0.7 + ); +} + +#[test] +fn test_scoring_p4_decay() { + let config = ConfigBuilder::default() + .validate_messages() + .build() + .unwrap(); + let mut peer_score_params = PeerScoreParams::default(); + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let topic_params = TopicScoreParams { + time_in_mesh_weight: 0.0, //deactivate time in mesh + first_message_deliveries_weight: 0.0, //deactivate first time deliveries + mesh_message_deliveries_weight: 0.0, //deactivate message deliveries + mesh_failure_penalty_weight: 0.0, //deactivate mesh failure penalties + invalid_message_deliveries_weight: -2.0, + invalid_message_deliveries_decay: 0.9, + topic_weight: 0.7, + ..Default::default() + }; + peer_score_params.topics.insert(topic_hash, topic_params); + peer_score_params.app_specific_weight = 1.0; + let peer_score_thresholds = PeerScoreThresholds::default(); + + //build mesh with one peer + let (mut gs, peers, _, topics) = inject_nodes1() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config.clone()) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + let mut seq = 0; + let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { + gs.handle_received_message(msg, &peers[index]); + }; + + //peer 0 delivers invalid message + let m1 = random_message(&mut seq, &topics); + deliver_message(&mut gs, 0, m1.clone()); + + // Transform the inbound message + let message1 = &gs.data_transform.inbound_transform(m1).unwrap(); + assert_eq!(gs.peer_score.as_ref().unwrap().0.score(&peers[0]), 0.0); + + //message m1 gets rejected + gs.report_message_validation_result( + &config.message_id(message1), + &peers[0], + MessageAcceptance::Reject, + ) + .unwrap(); + + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&peers[0]), + -2.0 * 0.7 + ); + + //we decay + gs.peer_score.as_mut().unwrap().0.refresh_scores(); + + // the number of invalids gets decayed to 0.9 and then squared in the score + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&peers[0]), + 0.9 * 0.9 * -2.0 * 0.7 + ); +} + +#[test] +fn test_scoring_p5() { + let peer_score_params = PeerScoreParams { + app_specific_weight: 2.0, + ..PeerScoreParams::default() + }; + + //build mesh with one peer + let (mut gs, peers, _, _) = inject_nodes1() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(Config::default()) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, PeerScoreThresholds::default()))) + .create_network(); + + gs.set_application_score(&peers[0], 1.1); + + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&peers[0]), + 1.1 * 2.0 + ); +} + +#[test] +fn test_scoring_p6() { + let peer_score_params = PeerScoreParams { + ip_colocation_factor_threshold: 5.0, + ip_colocation_factor_weight: -2.0, + ..Default::default() + }; + + let (mut gs, _, _, _) = inject_nodes1() + .peer_no(0) + .topics(vec![]) + .to_subscribe(false) + .gs_config(Config::default()) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, PeerScoreThresholds::default()))) + .create_network(); + + //create 5 peers with the same ip + let addr = Multiaddr::from(Ipv4Addr::new(10, 1, 2, 3)); + let peers = vec![ + add_peer_with_addr(&mut gs, &[], false, false, addr.clone()).0, + add_peer_with_addr(&mut gs, &[], false, false, addr.clone()).0, + add_peer_with_addr(&mut gs, &[], true, false, addr.clone()).0, + add_peer_with_addr(&mut gs, &[], true, false, addr.clone()).0, + add_peer_with_addr(&mut gs, &[], true, true, addr.clone()).0, + ]; + + //create 4 other peers with other ip + let addr2 = Multiaddr::from(Ipv4Addr::new(10, 1, 2, 4)); + let others = vec![ + add_peer_with_addr(&mut gs, &[], false, false, addr2.clone()).0, + add_peer_with_addr(&mut gs, &[], false, false, addr2.clone()).0, + add_peer_with_addr(&mut gs, &[], true, false, addr2.clone()).0, + add_peer_with_addr(&mut gs, &[], true, false, addr2.clone()).0, + ]; + + //no penalties yet + for peer in peers.iter().chain(others.iter()) { + assert_eq!(gs.peer_score.as_ref().unwrap().0.score(peer), 0.0); + } + + //add additional connection for 3 others with addr + for id in others.iter().take(3) { + gs.on_swarm_event(FromSwarm::ConnectionEstablished(ConnectionEstablished { + peer_id: *id, + connection_id: ConnectionId::new_unchecked(0), + endpoint: &ConnectedPoint::Dialer { + address: addr.clone(), + role_override: Endpoint::Dialer, + }, + failed_addresses: &[], + other_established: 0, + })); + } + + //penalties apply squared + for peer in peers.iter().chain(others.iter().take(3)) { + assert_eq!(gs.peer_score.as_ref().unwrap().0.score(peer), 9.0 * -2.0); + } + //fourth other peer still no penalty + assert_eq!(gs.peer_score.as_ref().unwrap().0.score(&others[3]), 0.0); + + //add additional connection for 3 of the peers to addr2 + for peer in peers.iter().take(3) { + gs.on_swarm_event(FromSwarm::ConnectionEstablished(ConnectionEstablished { + peer_id: *peer, + connection_id: ConnectionId::new_unchecked(0), + endpoint: &ConnectedPoint::Dialer { + address: addr2.clone(), + role_override: Endpoint::Dialer, + }, + failed_addresses: &[], + other_established: 1, + })); + } + + //double penalties for the first three of each + for peer in peers.iter().take(3).chain(others.iter().take(3)) { + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(peer), + (9.0 + 4.0) * -2.0 + ); + } + + //single penalties for the rest + for peer in peers.iter().skip(3) { + assert_eq!(gs.peer_score.as_ref().unwrap().0.score(peer), 9.0 * -2.0); + } + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&others[3]), + 4.0 * -2.0 + ); + + //two times same ip doesn't count twice + gs.on_swarm_event(FromSwarm::ConnectionEstablished(ConnectionEstablished { + peer_id: peers[0], + connection_id: ConnectionId::new_unchecked(0), + endpoint: &ConnectedPoint::Dialer { + address: addr, + role_override: Endpoint::Dialer, + }, + failed_addresses: &[], + other_established: 2, + })); + + //nothing changed + //double penalties for the first three of each + for peer in peers.iter().take(3).chain(others.iter().take(3)) { + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(peer), + (9.0 + 4.0) * -2.0 + ); + } + + //single penalties for the rest + for peer in peers.iter().skip(3) { + assert_eq!(gs.peer_score.as_ref().unwrap().0.score(peer), 9.0 * -2.0); + } + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&others[3]), + 4.0 * -2.0 + ); +} + +#[test] +fn test_scoring_p7_grafts_before_backoff() { + let config = ConfigBuilder::default() + .prune_backoff(Duration::from_millis(200)) + .graft_flood_threshold(Duration::from_millis(100)) + .build() + .unwrap(); + let peer_score_params = PeerScoreParams { + behaviour_penalty_weight: -2.0, + behaviour_penalty_decay: 0.9, + ..Default::default() + }; + + let (mut gs, peers, _receivers, topics) = inject_nodes1() + .peer_no(2) + .topics(vec!["test".into()]) + .to_subscribe(false) + .gs_config(config) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, PeerScoreThresholds::default()))) + .create_network(); + + //remove peers from mesh and send prune to them => this adds a backoff for the peers + for peer in peers.iter().take(2) { + gs.mesh.get_mut(&topics[0]).unwrap().remove(peer); + gs.send_graft_prune( + HashMap::new(), + HashMap::from([(*peer, vec![topics[0].clone()])]), + HashSet::new(), + ); + } + + //wait 50 millisecs + sleep(Duration::from_millis(50)); + + //first peer tries to graft + gs.handle_graft(&peers[0], vec![topics[0].clone()]); + + //double behaviour penalty for first peer (squared) + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&peers[0]), + 4.0 * -2.0 + ); + + //wait 100 millisecs + sleep(Duration::from_millis(100)); + + //second peer tries to graft + gs.handle_graft(&peers[1], vec![topics[0].clone()]); + + //single behaviour penalty for second peer + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&peers[1]), + 1.0 * -2.0 + ); + + //test decay + gs.peer_score.as_mut().unwrap().0.refresh_scores(); + + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&peers[0]), + 4.0 * 0.9 * 0.9 * -2.0 + ); + assert_eq!( + gs.peer_score.as_ref().unwrap().0.score(&peers[1]), + 1.0 * 0.9 * 0.9 * -2.0 + ); +} + +#[test] +fn test_opportunistic_grafting() { + let config = ConfigBuilder::default() + .mesh_n_low(3) + .mesh_n(5) + .mesh_n_high(7) + .mesh_outbound_min(0) //deactivate outbound handling + .opportunistic_graft_ticks(2) + .opportunistic_graft_peers(2) + .build() + .unwrap(); + let peer_score_params = PeerScoreParams { + app_specific_weight: 1.0, + ..Default::default() + }; + let thresholds = PeerScoreThresholds { + opportunistic_graft_threshold: 2.0, + ..Default::default() + }; + + let (mut gs, peers, _receivers, topics) = inject_nodes1() + .peer_no(5) + .topics(vec!["test".into()]) + .to_subscribe(false) + .gs_config(config) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, thresholds))) + .create_network(); + + //fill mesh with 5 peers + for peer in &peers { + gs.handle_graft(peer, topics.clone()); + } + + //add additional 5 peers + let others: Vec<_> = (0..5) + .map(|_| add_peer(&mut gs, &topics, false, false)) + .collect(); + + //currently mesh equals peers + assert_eq!(gs.mesh[&topics[0]], peers.iter().cloned().collect()); + + //give others high scores (but the first two have not high enough scores) + for (i, peer) in peers.iter().enumerate().take(5) { + gs.set_application_score(peer, 0.0 + i as f64); + } + + //set scores for peers in the mesh + for (i, (peer, _receiver)) in others.iter().enumerate().take(5) { + gs.set_application_score(peer, 0.0 + i as f64); + } + + //this gives a median of exactly 2.0 => should not apply opportunistic grafting + gs.heartbeat(); + gs.heartbeat(); + + assert_eq!( + gs.mesh[&topics[0]].len(), + 5, + "should not apply opportunistic grafting" + ); + + //reduce middle score to 1.0 giving a median of 1.0 + gs.set_application_score(&peers[2], 1.0); + + //opportunistic grafting after two heartbeats + + gs.heartbeat(); + assert_eq!( + gs.mesh[&topics[0]].len(), + 5, + "should not apply opportunistic grafting after first tick" + ); + + gs.heartbeat(); + + assert_eq!( + gs.mesh[&topics[0]].len(), + 7, + "opportunistic grafting should have added 2 peers" + ); + + assert!( + gs.mesh[&topics[0]].is_superset(&peers.iter().cloned().collect()), + "old peers are still part of the mesh" + ); + + assert!( + gs.mesh[&topics[0]].is_disjoint(&others.iter().map(|(p, _)| p).cloned().take(2).collect()), + "peers below or equal to median should not be added in opportunistic grafting" + ); +} + +#[test] +fn test_ignore_graft_from_unknown_topic() { + //build gossipsub without subscribing to any topics + let (mut gs, peers, receivers, _) = inject_nodes1() + .peer_no(1) + .topics(vec![]) + .to_subscribe(false) + .create_network(); + + //handle an incoming graft for some topic + gs.handle_graft(&peers[0], vec![Topic::new("test").hash()]); + + //assert that no prune got created + let (control_msgs, _) = count_control_msgs(receivers, |_, a| matches!(a, RpcOut::Prune { .. })); + assert_eq!( + control_msgs, 0, + "we should not prune after graft in unknown topic" + ); +} + +#[test] +fn test_ignore_too_many_iwants_from_same_peer_for_same_message() { + let config = Config::default(); + //build gossipsub with full mesh + let (mut gs, _, mut receivers, topics) = inject_nodes1() + .peer_no(config.mesh_n_high()) + .topics(vec!["test".into()]) + .to_subscribe(false) + .create_network(); + + //add another peer not in the mesh + let (peer, receiver) = add_peer(&mut gs, &topics, false, false); + receivers.insert(peer, receiver); + + //receive a message + let mut seq = 0; + let m1 = random_message(&mut seq, &topics); + + // Transform the inbound message + let message1 = &gs.data_transform.inbound_transform(m1.clone()).unwrap(); + + let id = config.message_id(message1); + + gs.handle_received_message(m1, &PeerId::random()); + + //clear events + let receivers = flush_events(&mut gs, receivers); + + //the first gossip_retransimission many iwants return the valid message, all others are + // ignored. + for _ in 0..(2 * config.gossip_retransimission() + 10) { + gs.handle_iwant(&peer, vec![id.clone()]); + } + + assert_eq!( + receivers.into_values().fold(0, |mut fwds, c| { + let non_priority = c.non_priority.into_inner(); + while !non_priority.is_empty() { + if let Ok(RpcOut::Forward { .. }) = non_priority.try_recv() { + fwds += 1; + } + } + fwds + }), + config.gossip_retransimission() as usize, + "not more then gossip_retransmission many messages get sent back" + ); +} + +#[test] +fn test_ignore_too_many_ihaves() { + let config = ConfigBuilder::default() + .max_ihave_messages(10) + .build() + .unwrap(); + //build gossipsub with full mesh + let (mut gs, _, mut receivers, topics) = inject_nodes1() + .peer_no(config.mesh_n_high()) + .topics(vec!["test".into()]) + .to_subscribe(false) + .gs_config(config.clone()) + .create_network(); + + //add another peer not in the mesh + let (peer, receiver) = add_peer(&mut gs, &topics, false, false); + receivers.insert(peer, receiver); + + //peer has 20 messages + let mut seq = 0; + let messages: Vec<_> = (0..20).map(|_| random_message(&mut seq, &topics)).collect(); + + //peer sends us one ihave for each message in order + for raw_message in &messages { + // Transform the inbound message + let message = &gs + .data_transform + .inbound_transform(raw_message.clone()) + .unwrap(); + + gs.handle_ihave( + &peer, + vec![(topics[0].clone(), vec![config.message_id(message)])], + ); + } + + let first_ten: HashSet<_> = messages + .iter() + .take(10) + .map(|msg| gs.data_transform.inbound_transform(msg.clone()).unwrap()) + .map(|m| config.message_id(&m)) + .collect(); + + //we send iwant only for the first 10 messages + let (control_msgs, receivers) = count_control_msgs(receivers, |p, action| { + p == &peer + && matches!(action, RpcOut::IWant(IWant { message_ids }) if message_ids.len() == 1 && first_ten.contains(&message_ids[0])) + }); + assert_eq!( + control_msgs, 10, + "exactly the first ten ihaves should be processed and one iwant for each created" + ); + + //after a heartbeat everything is forgotten + gs.heartbeat(); + + for raw_message in messages[10..].iter() { + // Transform the inbound message + let message = &gs + .data_transform + .inbound_transform(raw_message.clone()) + .unwrap(); + + gs.handle_ihave( + &peer, + vec![(topics[0].clone(), vec![config.message_id(message)])], + ); + } + + //we sent iwant for all 10 messages + let (control_msgs, _) = count_control_msgs(receivers, |p, action| { + p == &peer + && matches!(action, RpcOut::IWant(IWant { message_ids }) if message_ids.len() == 1) + }); + assert_eq!(control_msgs, 10, "all 20 should get sent"); +} + +#[test] +fn test_ignore_too_many_messages_in_ihave() { + let config = ConfigBuilder::default() + .max_ihave_messages(10) + .max_ihave_length(10) + .build() + .unwrap(); + //build gossipsub with full mesh + let (mut gs, _, mut receivers, topics) = inject_nodes1() + .peer_no(config.mesh_n_high()) + .topics(vec!["test".into()]) + .to_subscribe(false) + .gs_config(config.clone()) + .create_network(); + + //add another peer not in the mesh + let (peer, receiver) = add_peer(&mut gs, &topics, false, false); + receivers.insert(peer, receiver); + + //peer has 20 messages + let mut seq = 0; + let message_ids: Vec<_> = (0..20) + .map(|_| random_message(&mut seq, &topics)) + .map(|msg| gs.data_transform.inbound_transform(msg).unwrap()) + .map(|msg| config.message_id(&msg)) + .collect(); + + //peer sends us three ihaves + gs.handle_ihave(&peer, vec![(topics[0].clone(), message_ids[0..8].to_vec())]); + gs.handle_ihave( + &peer, + vec![(topics[0].clone(), message_ids[0..12].to_vec())], + ); + gs.handle_ihave( + &peer, + vec![(topics[0].clone(), message_ids[0..20].to_vec())], + ); + + let first_twelve: HashSet<_> = message_ids.iter().take(12).collect(); + + //we send iwant only for the first 10 messages + let mut sum = 0; + let (control_msgs, receivers) = count_control_msgs(receivers, |p, rpc| match rpc { + RpcOut::IWant(IWant { message_ids }) => { + p == &peer && { + assert!(first_twelve.is_superset(&message_ids.iter().collect())); + sum += message_ids.len(); + true + } + } + _ => false, + }); + assert_eq!( + control_msgs, 2, + "the third ihave should get ignored and no iwant sent" + ); + + assert_eq!(sum, 10, "exactly the first ten ihaves should be processed"); + + //after a heartbeat everything is forgotten + gs.heartbeat(); + gs.handle_ihave( + &peer, + vec![(topics[0].clone(), message_ids[10..20].to_vec())], + ); + + //we sent 10 iwant messages ids via a IWANT rpc. + let mut sum = 0; + let (control_msgs, _) = count_control_msgs(receivers, |p, rpc| match rpc { + RpcOut::IWant(IWant { message_ids }) => { + p == &peer && { + sum += message_ids.len(); + true + } + } + _ => false, + }); + assert_eq!(control_msgs, 1); + assert_eq!(sum, 10, "exactly 20 iwants should get sent"); +} + +#[test] +fn test_limit_number_of_message_ids_inside_ihave() { + let config = ConfigBuilder::default() + .max_ihave_messages(10) + .max_ihave_length(100) + .build() + .unwrap(); + //build gossipsub with full mesh + let (mut gs, peers, mut receivers, topics) = inject_nodes1() + .peer_no(config.mesh_n_high()) + .topics(vec!["test".into()]) + .to_subscribe(false) + .gs_config(config) + .create_network(); + + //graft to all peers to really fill the mesh with all the peers + for peer in peers { + gs.handle_graft(&peer, topics.clone()); + } + + //add two other peers not in the mesh + let (p1, receiver1) = add_peer(&mut gs, &topics, false, false); + receivers.insert(p1, receiver1); + let (p2, receiver2) = add_peer(&mut gs, &topics, false, false); + receivers.insert(p2, receiver2); + + //receive 200 messages from another peer + let mut seq = 0; + for _ in 0..200 { + gs.handle_received_message(random_message(&mut seq, &topics), &PeerId::random()); + } + + //emit gossip + gs.emit_gossip(); + + // both peers should have gotten 100 random ihave messages, to asser the randomness, we + // assert that both have not gotten the same set of messages, but have an intersection + // (which is the case with very high probability, the probabiltity of failure is < 10^-58). + + let mut ihaves1 = HashSet::new(); + let mut ihaves2 = HashSet::new(); + + let (control_msgs, _) = count_control_msgs(receivers, |p, action| match action { + RpcOut::IHave(IHave { message_ids, .. }) => { + if p == &p1 { + ihaves1 = message_ids.iter().cloned().collect(); + true + } else if p == &p2 { + ihaves2 = message_ids.iter().cloned().collect(); + true + } else { + false + } + } + _ => false, + }); + assert_eq!( + control_msgs, 2, + "should have emitted one ihave to p1 and one to p2" + ); + + assert_eq!( + ihaves1.len(), + 100, + "should have sent 100 message ids in ihave to p1" + ); + assert_eq!( + ihaves2.len(), + 100, + "should have sent 100 message ids in ihave to p2" + ); + assert!( + ihaves1 != ihaves2, + "should have sent different random messages to p1 and p2 \ + (this may fail with a probability < 10^-58" + ); + assert!( + ihaves1.intersection(&ihaves2).count() > 0, + "should have sent random messages with some common messages to p1 and p2 \ + (this may fail with a probability < 10^-58" + ); +} + +#[test] +fn test_iwant_penalties() { + /* + use tracing_subscriber::EnvFilter; + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + */ + let config = ConfigBuilder::default() + .iwant_followup_time(Duration::from_secs(4)) + .build() + .unwrap(); + let peer_score_params = PeerScoreParams { + behaviour_penalty_weight: -1.0, + ..Default::default() + }; + + // fill the mesh + let (mut gs, peers, _, topics) = inject_nodes1() + .peer_no(2) + .topics(vec!["test".into()]) + .to_subscribe(false) + .gs_config(config.clone()) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, PeerScoreThresholds::default()))) + .create_network(); + + // graft to all peers to really fill the mesh with all the peers + for peer in peers { + gs.handle_graft(&peer, topics.clone()); + } + + // add 100 more peers + let other_peers: Vec<_> = (0..100) + .map(|_| add_peer(&mut gs, &topics, false, false)) + .collect(); + + // each peer sends us an ihave containing each two message ids + let mut first_messages = Vec::new(); + let mut second_messages = Vec::new(); + let mut seq = 0; + for (peer, _receiver) in &other_peers { + let msg1 = random_message(&mut seq, &topics); + let msg2 = random_message(&mut seq, &topics); + + // Decompress the raw message and calculate the message id. + // Transform the inbound message + let message1 = &gs.data_transform.inbound_transform(msg1.clone()).unwrap(); + + // Transform the inbound message + let message2 = &gs.data_transform.inbound_transform(msg2.clone()).unwrap(); + + first_messages.push(msg1.clone()); + second_messages.push(msg2.clone()); + gs.handle_ihave( + peer, + vec![( + topics[0].clone(), + vec![config.message_id(message1), config.message_id(message2)], + )], + ); + } + + // the peers send us all the first message ids in time + for (index, (peer, _receiver)) in other_peers.iter().enumerate() { + gs.handle_received_message(first_messages[index].clone(), peer); + } + + // now we do a heartbeat no penalization should have been applied yet + gs.heartbeat(); + + for (peer, _receiver) in &other_peers { + assert_eq!(gs.peer_score.as_ref().unwrap().0.score(peer), 0.0); + } + + // receive the first twenty of the other peers then send their response + for (index, (peer, _receiver)) in other_peers.iter().enumerate().take(20) { + gs.handle_received_message(second_messages[index].clone(), peer); + } + + // sleep for the promise duration + sleep(Duration::from_secs(4)); + + // now we do a heartbeat to apply penalization + gs.heartbeat(); + + // now we get the second messages from the last 80 peers. + for (index, (peer, _receiver)) in other_peers.iter().enumerate() { + if index > 19 { + gs.handle_received_message(second_messages[index].clone(), peer); + } + } + + // no further penalizations should get applied + gs.heartbeat(); + + // Only the last 80 peers should be penalized for not responding in time + let mut not_penalized = 0; + let mut single_penalized = 0; + let mut double_penalized = 0; + + for (i, (peer, _receiver)) in other_peers.iter().enumerate() { + let score = gs.peer_score.as_ref().unwrap().0.score(peer); + if score == 0.0 { + not_penalized += 1; + } else if score == -1.0 { + assert!(i > 9); + single_penalized += 1; + } else if score == -4.0 { + assert!(i > 9); + double_penalized += 1 + } else { + println!("{peer}"); + println!("{score}"); + panic!("Invalid score of peer"); + } + } + + assert_eq!(not_penalized, 20); + assert_eq!(single_penalized, 80); + assert_eq!(double_penalized, 0); +} + +#[test] +fn test_publish_to_floodsub_peers_without_flood_publish() { + let config = ConfigBuilder::default() + .flood_publish(false) + .build() + .unwrap(); + let (mut gs, _, mut receivers, topics) = inject_nodes1() + .peer_no(config.mesh_n_low() - 1) + .topics(vec!["test".into()]) + .to_subscribe(false) + .gs_config(config) + .create_network(); + + //add two floodsub peer, one explicit, one implicit + let (p1, receiver1) = add_peer_with_addr_and_kind( + &mut gs, + &topics, + false, + false, + Multiaddr::empty(), + Some(PeerKind::Floodsub), + ); + receivers.insert(p1, receiver1); + + let (p2, receiver2) = + add_peer_with_addr_and_kind(&mut gs, &topics, false, false, Multiaddr::empty(), None); + receivers.insert(p2, receiver2); + + //p1 and p2 are not in the mesh + assert!(!gs.mesh[&topics[0]].contains(&p1) && !gs.mesh[&topics[0]].contains(&p2)); + + //publish a message + let publish_data = vec![0; 42]; + gs.publish(Topic::new("test"), publish_data).unwrap(); + + // Collect publish messages to floodsub peers + let publishes = receivers + .into_iter() + .fold(0, |mut collected_publish, (peer_id, c)| { + let priority = c.priority.into_inner(); + while !priority.is_empty() { + if matches!(priority.try_recv(), + Ok(RpcOut::Publish{..}) if peer_id == p1 || peer_id == p2) + { + collected_publish += 1; + } + } + collected_publish + }); + + assert_eq!( + publishes, 2, + "Should send a publish message to all floodsub peers" + ); +} + +#[test] +fn test_do_not_use_floodsub_in_fanout() { + let config = ConfigBuilder::default() + .flood_publish(false) + .build() + .unwrap(); + let (mut gs, _, mut receivers, _) = inject_nodes1() + .peer_no(config.mesh_n_low() - 1) + .topics(Vec::new()) + .to_subscribe(false) + .gs_config(config) + .create_network(); + + let topic = Topic::new("test"); + let topics = vec![topic.hash()]; + + //add two floodsub peer, one explicit, one implicit + let (p1, receiver1) = add_peer_with_addr_and_kind( + &mut gs, + &topics, + false, + false, + Multiaddr::empty(), + Some(PeerKind::Floodsub), + ); + + receivers.insert(p1, receiver1); + let (p2, receiver2) = + add_peer_with_addr_and_kind(&mut gs, &topics, false, false, Multiaddr::empty(), None); + + receivers.insert(p2, receiver2); + //publish a message + let publish_data = vec![0; 42]; + gs.publish(Topic::new("test"), publish_data).unwrap(); + + // Collect publish messages to floodsub peers + let publishes = receivers + .into_iter() + .fold(0, |mut collected_publish, (peer_id, c)| { + let priority = c.priority.into_inner(); + while !priority.is_empty() { + if matches!(priority.try_recv(), + Ok(RpcOut::Publish{..}) if peer_id == p1 || peer_id == p2) + { + collected_publish += 1; + } + } + collected_publish + }); + + assert_eq!( + publishes, 2, + "Should send a publish message to all floodsub peers" + ); + + assert!( + !gs.fanout[&topics[0]].contains(&p1) && !gs.fanout[&topics[0]].contains(&p2), + "Floodsub peers are not allowed in fanout" + ); +} + +#[test] +fn test_dont_add_floodsub_peers_to_mesh_on_join() { + let (mut gs, _, _, _) = inject_nodes1() + .peer_no(0) + .topics(Vec::new()) + .to_subscribe(false) + .create_network(); + + let topic = Topic::new("test"); + let topics = vec![topic.hash()]; + + //add two floodsub peer, one explicit, one implicit + let _p1 = add_peer_with_addr_and_kind( + &mut gs, + &topics, + false, + false, + Multiaddr::empty(), + Some(PeerKind::Floodsub), + ); + let _p2 = add_peer_with_addr_and_kind(&mut gs, &topics, false, false, Multiaddr::empty(), None); + + gs.join(&topics[0]); + + assert!( + gs.mesh[&topics[0]].is_empty(), + "Floodsub peers should not get added to mesh" + ); +} + +#[test] +fn test_dont_send_px_to_old_gossipsub_peers() { + let (mut gs, _, receivers, topics) = inject_nodes1() + .peer_no(0) + .topics(vec!["test".into()]) + .to_subscribe(false) + .create_network(); + + //add an old gossipsub peer + let (p1, _receiver1) = add_peer_with_addr_and_kind( + &mut gs, + &topics, + false, + false, + Multiaddr::empty(), + Some(PeerKind::Gossipsub), + ); + + //prune the peer + gs.send_graft_prune( + HashMap::new(), + vec![(p1, topics.clone())].into_iter().collect(), + HashSet::new(), + ); + + //check that prune does not contain px + let (control_msgs, _) = count_control_msgs(receivers, |_, m| match m { + RpcOut::Prune(Prune { peers: px, .. }) => !px.is_empty(), + _ => false, + }); + assert_eq!(control_msgs, 0, "Should not send px to floodsub peers"); +} + +#[test] +fn test_dont_send_floodsub_peers_in_px() { + //build mesh with one peer + let (mut gs, peers, receivers, topics) = inject_nodes1() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .create_network(); + + //add two floodsub peers + let _p1 = add_peer_with_addr_and_kind( + &mut gs, + &topics, + false, + false, + Multiaddr::empty(), + Some(PeerKind::Floodsub), + ); + let _p2 = add_peer_with_addr_and_kind(&mut gs, &topics, false, false, Multiaddr::empty(), None); + + //prune only mesh node + gs.send_graft_prune( + HashMap::new(), + vec![(peers[0], topics.clone())].into_iter().collect(), + HashSet::new(), + ); + + //check that px in prune message is empty + let (control_msgs, _) = count_control_msgs(receivers, |_, m| match m { + RpcOut::Prune(Prune { peers: px, .. }) => !px.is_empty(), + _ => false, + }); + assert_eq!(control_msgs, 0, "Should not include floodsub peers in px"); +} + +#[test] +fn test_dont_add_floodsub_peers_to_mesh_in_heartbeat() { + let (mut gs, _, _, topics) = inject_nodes1() + .peer_no(0) + .topics(vec!["test".into()]) + .to_subscribe(false) + .create_network(); + + //add two floodsub peer, one explicit, one implicit + let _p1 = add_peer_with_addr_and_kind( + &mut gs, + &topics, + true, + false, + Multiaddr::empty(), + Some(PeerKind::Floodsub), + ); + let _p2 = add_peer_with_addr_and_kind(&mut gs, &topics, true, false, Multiaddr::empty(), None); + + gs.heartbeat(); + + assert!( + gs.mesh[&topics[0]].is_empty(), + "Floodsub peers should not get added to mesh" + ); +} + +// Some very basic test of public api methods. +#[test] +fn test_public_api() { + let (gs, peers, _, topic_hashes) = inject_nodes1() + .peer_no(4) + .topics(vec![String::from("topic1")]) + .to_subscribe(true) + .create_network(); + let peers = peers.into_iter().collect::>(); + + assert_eq!( + gs.topics().cloned().collect::>(), + topic_hashes, + "Expected topics to match registered topic." + ); + + assert_eq!( + gs.mesh_peers(&TopicHash::from_raw("topic1")) + .cloned() + .collect::>(), + peers, + "Expected peers for a registered topic to contain all peers." + ); + + assert_eq!( + gs.all_mesh_peers().cloned().collect::>(), + peers, + "Expected all_peers to contain all peers." + ); +} + +#[test] +fn test_subscribe_to_invalid_topic() { + let t1 = Topic::new("t1"); + let t2 = Topic::new("t2"); + let (mut gs, _, _, _) = inject_nodes::() + .subscription_filter(WhitelistSubscriptionFilter( + vec![t1.hash()].into_iter().collect(), + )) + .to_subscribe(false) + .create_network(); + + assert!(gs.subscribe(&t1).is_ok()); + assert!(gs.subscribe(&t2).is_err()); +} + +#[test] +fn test_subscribe_and_graft_with_negative_score() { + //simulate a communication between two gossipsub instances + let (mut gs1, _, _, topic_hashes) = inject_nodes1() + .topics(vec!["test".into()]) + .scoring(Some(( + PeerScoreParams::default(), + PeerScoreThresholds::default(), + ))) + .create_network(); + + let (mut gs2, _, receivers, _) = inject_nodes1().create_network(); + + let connection_id = ConnectionId::new_unchecked(0); + + let topic = Topic::new("test"); + + let (p2, _receiver1) = add_peer(&mut gs1, &Vec::new(), true, false); + let (p1, _receiver2) = add_peer(&mut gs2, &topic_hashes, false, false); + + //add penalty to peer p2 + gs1.peer_score.as_mut().unwrap().0.add_penalty(&p2, 1); + + let original_score = gs1.peer_score.as_ref().unwrap().0.score(&p2); + + //subscribe to topic in gs2 + gs2.subscribe(&topic).unwrap(); + + let forward_messages_to_p1 = |gs1: &mut Behaviour<_, _>, + p1: PeerId, + p2: PeerId, + connection_id: ConnectionId, + receivers: HashMap| + -> HashMap { + let new_receivers = HashMap::new(); + for (peer_id, receiver) in receivers.into_iter() { + let non_priority = receiver.non_priority.into_inner(); + match non_priority.try_recv() { + Ok(rpc) if peer_id == p1 => { + gs1.on_connection_handler_event( + p2, + connection_id, + HandlerEvent::Message { + rpc: proto_to_message(&rpc.into_protobuf()), + invalid_messages: vec![], + }, + ); + } + _ => {} + } + } + new_receivers + }; + + //forward the subscribe message + let receivers = forward_messages_to_p1(&mut gs1, p1, p2, connection_id, receivers); + + //heartbeats on both + gs1.heartbeat(); + gs2.heartbeat(); + + //forward messages again + forward_messages_to_p1(&mut gs1, p1, p2, connection_id, receivers); + + //nobody got penalized + assert!(gs1.peer_score.as_ref().unwrap().0.score(&p2) >= original_score); +} + +#[test] +/// Test nodes that send grafts without subscriptions. +fn test_graft_without_subscribe() { + // The node should: + // - Create an empty vector in mesh[topic] + // - Send subscription request to all peers + // - run JOIN(topic) + + let topic = String::from("test_subscribe"); + let subscribe_topic = vec![topic.clone()]; + let subscribe_topic_hash = vec![Topic::new(topic.clone()).hash()]; + let (mut gs, peers, _, topic_hashes) = inject_nodes1() + .peer_no(1) + .topics(subscribe_topic) + .to_subscribe(false) + .create_network(); + + assert!( + gs.mesh.contains_key(&topic_hashes[0]), + "Subscribe should add a new entry to the mesh[topic] hashmap" + ); + + // The node sends a graft for the subscribe topic. + gs.handle_graft(&peers[0], subscribe_topic_hash); + + // The node disconnects + disconnect_peer(&mut gs, &peers[0]); + + // We unsubscribe from the topic. + let _ = gs.unsubscribe(&Topic::new(topic)); +} diff --git a/beacon_node/lighthouse_network/gossipsub/src/config.rs b/beacon_node/lighthouse_network/gossipsub/src/config.rs new file mode 100644 index 0000000000..1296e614c8 --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/config.rs @@ -0,0 +1,1024 @@ +// Copyright 2020 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +use std::borrow::Cow; +use std::sync::Arc; +use std::time::Duration; + +use super::error::ConfigBuilderError; +use super::protocol::{ProtocolConfig, ProtocolId, FLOODSUB_PROTOCOL}; +use super::types::{Message, MessageId, PeerKind}; + +use libp2p::identity::PeerId; +use libp2p::swarm::StreamProtocol; + +/// The types of message validation that can be employed by gossipsub. +#[derive(Debug, Clone)] +pub enum ValidationMode { + /// This is the default setting. This requires the message author to be a valid [`PeerId`] and to + /// be present as well as the sequence number. All messages must have valid signatures. + /// + /// NOTE: This setting will reject messages from nodes using + /// [`crate::behaviour::MessageAuthenticity::Anonymous`] and all messages that do not have + /// signatures. + Strict, + /// This setting permits messages that have no author, sequence number or signature. If any of + /// these fields exist in the message these are validated. + Permissive, + /// This setting requires the author, sequence number and signature fields of a message to be + /// empty. Any message that contains these fields is considered invalid. + Anonymous, + /// This setting does not check the author, sequence number or signature fields of incoming + /// messages. If these fields contain data, they are simply ignored. + /// + /// NOTE: This setting will consider messages with invalid signatures as valid messages. + None, +} + +/// Selector for custom Protocol Id +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Version { + V1_0, + V1_1, +} + +/// Configuration parameters that define the performance of the gossipsub network. +#[derive(Clone)] +pub struct Config { + protocol: ProtocolConfig, + history_length: usize, + history_gossip: usize, + mesh_n: usize, + mesh_n_low: usize, + mesh_n_high: usize, + retain_scores: usize, + gossip_lazy: usize, + gossip_factor: f64, + heartbeat_initial_delay: Duration, + heartbeat_interval: Duration, + fanout_ttl: Duration, + check_explicit_peers_ticks: u64, + duplicate_cache_time: Duration, + validate_messages: bool, + message_id_fn: Arc MessageId + Send + Sync + 'static>, + allow_self_origin: bool, + do_px: bool, + prune_peers: usize, + prune_backoff: Duration, + unsubscribe_backoff: Duration, + backoff_slack: u32, + flood_publish: bool, + graft_flood_threshold: Duration, + mesh_outbound_min: usize, + opportunistic_graft_ticks: u64, + opportunistic_graft_peers: usize, + gossip_retransimission: u32, + max_messages_per_rpc: Option, + max_ihave_length: usize, + max_ihave_messages: usize, + iwant_followup_time: Duration, + published_message_ids_cache_time: Duration, + connection_handler_queue_len: usize, + connection_handler_publish_duration: Duration, + connection_handler_forward_duration: Duration, +} + +impl Config { + pub(crate) fn protocol_config(&self) -> ProtocolConfig { + self.protocol.clone() + } + + // Overlay network parameters. + /// Number of heartbeats to keep in the `memcache` (default is 5). + pub fn history_length(&self) -> usize { + self.history_length + } + + /// Number of past heartbeats to gossip about (default is 3). + pub fn history_gossip(&self) -> usize { + self.history_gossip + } + + /// Target number of peers for the mesh network (D in the spec, default is 6). + pub fn mesh_n(&self) -> usize { + self.mesh_n + } + + /// Minimum number of peers in mesh network before adding more (D_lo in the spec, default is 5). + pub fn mesh_n_low(&self) -> usize { + self.mesh_n_low + } + + /// Maximum number of peers in mesh network before removing some (D_high in the spec, default + /// is 12). + pub fn mesh_n_high(&self) -> usize { + self.mesh_n_high + } + + /// Affects how peers are selected when pruning a mesh due to over subscription. + /// + /// At least `retain_scores` of the retained peers will be high-scoring, while the remainder are + /// chosen randomly (D_score in the spec, default is 4). + pub fn retain_scores(&self) -> usize { + self.retain_scores + } + + /// Minimum number of peers to emit gossip to during a heartbeat (D_lazy in the spec, + /// default is 6). + pub fn gossip_lazy(&self) -> usize { + self.gossip_lazy + } + + /// Affects how many peers we will emit gossip to at each heartbeat. + /// + /// We will send gossip to `gossip_factor * (total number of non-mesh peers)`, or + /// `gossip_lazy`, whichever is greater. The default is 0.25. + pub fn gossip_factor(&self) -> f64 { + self.gossip_factor + } + + /// Initial delay in each heartbeat (default is 5 seconds). + pub fn heartbeat_initial_delay(&self) -> Duration { + self.heartbeat_initial_delay + } + + /// Time between each heartbeat (default is 1 second). + pub fn heartbeat_interval(&self) -> Duration { + self.heartbeat_interval + } + + /// Time to live for fanout peers (default is 60 seconds). + pub fn fanout_ttl(&self) -> Duration { + self.fanout_ttl + } + + /// The number of heartbeat ticks until we recheck the connection to explicit peers and + /// reconnecting if necessary (default 300). + pub fn check_explicit_peers_ticks(&self) -> u64 { + self.check_explicit_peers_ticks + } + + /// The maximum byte size for each gossipsub RPC (default is 65536 bytes). + /// + /// This represents the maximum size of the entire protobuf payload. It must be at least + /// large enough to support basic control messages. If Peer eXchange is enabled, this + /// must be large enough to transmit the desired peer information on pruning. It must be at + /// least 100 bytes. Default is 65536 bytes. + pub fn max_transmit_size(&self) -> usize { + self.protocol.max_transmit_size + } + + /// Duplicates are prevented by storing message id's of known messages in an LRU time cache. + /// This settings sets the time period that messages are stored in the cache. Duplicates can be + /// received if duplicate messages are sent at a time greater than this setting apart. The + /// default is 1 minute. + pub fn duplicate_cache_time(&self) -> Duration { + self.duplicate_cache_time + } + + /// When set to `true`, prevents automatic forwarding of all received messages. This setting + /// allows a user to validate the messages before propagating them to their peers. If set to + /// true, the user must manually call [`crate::Behaviour::report_message_validation_result()`] + /// on the behaviour to forward message once validated (default is `false`). + /// The default is `false`. + pub fn validate_messages(&self) -> bool { + self.validate_messages + } + + /// Determines the level of validation used when receiving messages. See [`ValidationMode`] + /// for the available types. The default is ValidationMode::Strict. + pub fn validation_mode(&self) -> &ValidationMode { + &self.protocol.validation_mode + } + + /// A user-defined function allowing the user to specify the message id of a gossipsub message. + /// The default value is to concatenate the source peer id with a sequence number. Setting this + /// parameter allows the user to address packets arbitrarily. One example is content based + /// addressing, where this function may be set to `hash(message)`. This would prevent messages + /// of the same content from being duplicated. + /// + /// The function takes a [`Message`] as input and outputs a String to be interpreted as + /// the message id. + pub fn message_id(&self, message: &Message) -> MessageId { + (self.message_id_fn)(message) + } + + /// By default, gossipsub will reject messages that are sent to us that have the same message + /// source as we have specified locally. Enabling this, allows these messages and prevents + /// penalizing the peer that sent us the message. Default is false. + pub fn allow_self_origin(&self) -> bool { + self.allow_self_origin + } + + /// Whether Peer eXchange is enabled; this should be enabled in bootstrappers and other well + /// connected/trusted nodes. The default is false. + /// + /// Note: Peer exchange is not implemented today, see + /// . + pub fn do_px(&self) -> bool { + self.do_px + } + + /// Controls the number of peers to include in prune Peer eXchange. + /// When we prune a peer that's eligible for PX (has a good score, etc), we will try to + /// send them signed peer records for up to `prune_peers` other peers that we + /// know of. It is recommended that this value is larger than `mesh_n_high` so that the pruned + /// peer can reliably form a full mesh. The default is typically 16 however until signed + /// records are spec'd this is disabled and set to 0. + pub fn prune_peers(&self) -> usize { + self.prune_peers + } + + /// Controls the backoff time for pruned peers. This is how long + /// a peer must wait before attempting to graft into our mesh again after being pruned. + /// When pruning a peer, we send them our value of `prune_backoff` so they know + /// the minimum time to wait. Peers running older versions may not send a backoff time, + /// so if we receive a prune message without one, we will wait at least `prune_backoff` + /// before attempting to re-graft. The default is one minute. + pub fn prune_backoff(&self) -> Duration { + self.prune_backoff + } + + /// Controls the backoff time when unsubscribing from a topic. + /// + /// This is how long to wait before resubscribing to the topic. A short backoff period in case + /// of an unsubscribe event allows reaching a healthy mesh in a more timely manner. The default + /// is 10 seconds. + pub fn unsubscribe_backoff(&self) -> Duration { + self.unsubscribe_backoff + } + + /// Number of heartbeat slots considered as slack for backoffs. This gurantees that we wait + /// at least backoff_slack heartbeats after a backoff is over before we try to graft. This + /// solves problems occuring through high latencies. In particular if + /// `backoff_slack * heartbeat_interval` is longer than any latencies between processing + /// prunes on our side and processing prunes on the receiving side this guarantees that we + /// get not punished for too early grafting. The default is 1. + pub fn backoff_slack(&self) -> u32 { + self.backoff_slack + } + + /// Whether to do flood publishing or not. If enabled newly created messages will always be + /// sent to all peers that are subscribed to the topic and have a good enough score. + /// The default is true. + pub fn flood_publish(&self) -> bool { + self.flood_publish + } + + /// If a GRAFT comes before `graft_flood_threshold` has elapsed since the last PRUNE, + /// then there is an extra score penalty applied to the peer through P7. + pub fn graft_flood_threshold(&self) -> Duration { + self.graft_flood_threshold + } + + /// Minimum number of outbound peers in the mesh network before adding more (D_out in the spec). + /// This value must be smaller or equal than `mesh_n / 2` and smaller than `mesh_n_low`. + /// The default is 2. + pub fn mesh_outbound_min(&self) -> usize { + self.mesh_outbound_min + } + + /// Number of heartbeat ticks that specifcy the interval in which opportunistic grafting is + /// applied. Every `opportunistic_graft_ticks` we will attempt to select some high-scoring mesh + /// peers to replace lower-scoring ones, if the median score of our mesh peers falls below a + /// threshold (see ). + /// The default is 60. + pub fn opportunistic_graft_ticks(&self) -> u64 { + self.opportunistic_graft_ticks + } + + /// Controls how many times we will allow a peer to request the same message id through IWANT + /// gossip before we start ignoring them. This is designed to prevent peers from spamming us + /// with requests and wasting our resources. The default is 3. + pub fn gossip_retransimission(&self) -> u32 { + self.gossip_retransimission + } + + /// The maximum number of new peers to graft to during opportunistic grafting. The default is 2. + pub fn opportunistic_graft_peers(&self) -> usize { + self.opportunistic_graft_peers + } + + /// The maximum number of messages we will process in a given RPC. If this is unset, there is + /// no limit. The default is None. + pub fn max_messages_per_rpc(&self) -> Option { + self.max_messages_per_rpc + } + + /// The maximum number of messages to include in an IHAVE message. + /// Also controls the maximum number of IHAVE ids we will accept and request with IWANT from a + /// peer within a heartbeat, to protect from IHAVE floods. You should adjust this value from the + /// default if your system is pushing more than 5000 messages in GossipSubHistoryGossip + /// heartbeats; with the defaults this is 1666 messages/s. The default is 5000. + pub fn max_ihave_length(&self) -> usize { + self.max_ihave_length + } + + /// GossipSubMaxIHaveMessages is the maximum number of IHAVE messages to accept from a peer + /// within a heartbeat. + pub fn max_ihave_messages(&self) -> usize { + self.max_ihave_messages + } + + /// Time to wait for a message requested through IWANT following an IHAVE advertisement. + /// If the message is not received within this window, a broken promise is declared and + /// the router may apply behavioural penalties. The default is 3 seconds. + pub fn iwant_followup_time(&self) -> Duration { + self.iwant_followup_time + } + + /// Enable support for flooodsub peers. Default false. + pub fn support_floodsub(&self) -> bool { + self.protocol.protocol_ids.contains(&FLOODSUB_PROTOCOL) + } + + /// Published message ids time cache duration. The default is 10 seconds. + pub fn published_message_ids_cache_time(&self) -> Duration { + self.published_message_ids_cache_time + } + + /// The max number of messages a `ConnectionHandler` can buffer. The default is 5000. + pub fn connection_handler_queue_len(&self) -> usize { + self.connection_handler_queue_len + } + + /// The duration a message to be published can wait to be sent before it is abandoned. The + /// default is 5 seconds. + pub fn publish_queue_duration(&self) -> Duration { + self.connection_handler_publish_duration + } + + /// The duration a message to be forwarded can wait to be sent before it is abandoned. The + /// default is 1s. + pub fn forward_queue_duration(&self) -> Duration { + self.connection_handler_forward_duration + } +} + +impl Default for Config { + fn default() -> Self { + // use ConfigBuilder to also validate defaults + ConfigBuilder::default() + .build() + .expect("Default config parameters should be valid parameters") + } +} + +/// The builder struct for constructing a gossipsub configuration. +pub struct ConfigBuilder { + config: Config, + invalid_protocol: bool, // This is a bit of a hack to only expose one error to the user. +} + +impl Default for ConfigBuilder { + fn default() -> Self { + ConfigBuilder { + config: Config { + protocol: ProtocolConfig::default(), + history_length: 5, + history_gossip: 3, + mesh_n: 6, + mesh_n_low: 5, + mesh_n_high: 12, + retain_scores: 4, + gossip_lazy: 6, // default to mesh_n + gossip_factor: 0.25, + heartbeat_initial_delay: Duration::from_secs(5), + heartbeat_interval: Duration::from_secs(1), + fanout_ttl: Duration::from_secs(60), + check_explicit_peers_ticks: 300, + duplicate_cache_time: Duration::from_secs(60), + validate_messages: false, + message_id_fn: Arc::new(|message| { + // default message id is: source + sequence number + // NOTE: If either the peer_id or source is not provided, we set to 0; + let mut source_string = if let Some(peer_id) = message.source.as_ref() { + peer_id.to_base58() + } else { + PeerId::from_bytes(&[0, 1, 0]) + .expect("Valid peer id") + .to_base58() + }; + source_string + .push_str(&message.sequence_number.unwrap_or_default().to_string()); + MessageId::from(source_string) + }), + allow_self_origin: false, + do_px: false, + prune_peers: 0, // NOTE: Increasing this currently has little effect until Signed records are implemented. + prune_backoff: Duration::from_secs(60), + unsubscribe_backoff: Duration::from_secs(10), + backoff_slack: 1, + flood_publish: true, + graft_flood_threshold: Duration::from_secs(10), + mesh_outbound_min: 2, + opportunistic_graft_ticks: 60, + opportunistic_graft_peers: 2, + gossip_retransimission: 3, + max_messages_per_rpc: None, + max_ihave_length: 5000, + max_ihave_messages: 10, + iwant_followup_time: Duration::from_secs(3), + published_message_ids_cache_time: Duration::from_secs(10), + connection_handler_queue_len: 5000, + connection_handler_publish_duration: Duration::from_secs(5), + connection_handler_forward_duration: Duration::from_millis(1000), + }, + invalid_protocol: false, + } + } +} + +impl From for ConfigBuilder { + fn from(config: Config) -> Self { + ConfigBuilder { + config, + invalid_protocol: false, + } + } +} + +impl ConfigBuilder { + /// The protocol id prefix to negotiate this protocol (default is `/meshsub/1.1.0` and `/meshsub/1.0.0`). + pub fn protocol_id_prefix( + &mut self, + protocol_id_prefix: impl Into>, + ) -> &mut Self { + let cow = protocol_id_prefix.into(); + + match ( + StreamProtocol::try_from_owned(format!("{}/1.1.0", cow)), + StreamProtocol::try_from_owned(format!("{}/1.0.0", cow)), + ) { + (Ok(p1), Ok(p2)) => { + self.config.protocol.protocol_ids = vec![ + ProtocolId { + protocol: p1, + kind: PeerKind::Gossipsubv1_1, + }, + ProtocolId { + protocol: p2, + kind: PeerKind::Gossipsub, + }, + ] + } + _ => { + self.invalid_protocol = true; + } + } + + self + } + + /// The full protocol id to negotiate this protocol (does not append `/1.0.0` or `/1.1.0`). + pub fn protocol_id( + &mut self, + protocol_id: impl Into>, + custom_id_version: Version, + ) -> &mut Self { + let cow = protocol_id.into(); + + match StreamProtocol::try_from_owned(cow.to_string()) { + Ok(protocol) => { + self.config.protocol.protocol_ids = vec![ProtocolId { + protocol, + kind: match custom_id_version { + Version::V1_1 => PeerKind::Gossipsubv1_1, + Version::V1_0 => PeerKind::Gossipsub, + }, + }] + } + _ => { + self.invalid_protocol = true; + } + } + + self + } + + /// Number of heartbeats to keep in the `memcache` (default is 5). + pub fn history_length(&mut self, history_length: usize) -> &mut Self { + self.config.history_length = history_length; + self + } + + /// Number of past heartbeats to gossip about (default is 3). + pub fn history_gossip(&mut self, history_gossip: usize) -> &mut Self { + self.config.history_gossip = history_gossip; + self + } + + /// Target number of peers for the mesh network (D in the spec, default is 6). + pub fn mesh_n(&mut self, mesh_n: usize) -> &mut Self { + self.config.mesh_n = mesh_n; + self + } + + /// Minimum number of peers in mesh network before adding more (D_lo in the spec, default is 4). + pub fn mesh_n_low(&mut self, mesh_n_low: usize) -> &mut Self { + self.config.mesh_n_low = mesh_n_low; + self + } + + /// Maximum number of peers in mesh network before removing some (D_high in the spec, default + /// is 12). + pub fn mesh_n_high(&mut self, mesh_n_high: usize) -> &mut Self { + self.config.mesh_n_high = mesh_n_high; + self + } + + /// Affects how peers are selected when pruning a mesh due to over subscription. + /// + /// At least [`Self::retain_scores`] of the retained peers will be high-scoring, while the remainder are + /// chosen randomly (D_score in the spec, default is 4). + pub fn retain_scores(&mut self, retain_scores: usize) -> &mut Self { + self.config.retain_scores = retain_scores; + self + } + + /// Minimum number of peers to emit gossip to during a heartbeat (D_lazy in the spec, + /// default is 6). + pub fn gossip_lazy(&mut self, gossip_lazy: usize) -> &mut Self { + self.config.gossip_lazy = gossip_lazy; + self + } + + /// Affects how many peers we will emit gossip to at each heartbeat. + /// + /// We will send gossip to `gossip_factor * (total number of non-mesh peers)`, or + /// `gossip_lazy`, whichever is greater. The default is 0.25. + pub fn gossip_factor(&mut self, gossip_factor: f64) -> &mut Self { + self.config.gossip_factor = gossip_factor; + self + } + + /// Initial delay in each heartbeat (default is 5 seconds). + pub fn heartbeat_initial_delay(&mut self, heartbeat_initial_delay: Duration) -> &mut Self { + self.config.heartbeat_initial_delay = heartbeat_initial_delay; + self + } + + /// Time between each heartbeat (default is 1 second). + pub fn heartbeat_interval(&mut self, heartbeat_interval: Duration) -> &mut Self { + self.config.heartbeat_interval = heartbeat_interval; + self + } + + /// The number of heartbeat ticks until we recheck the connection to explicit peers and + /// reconnecting if necessary (default 300). + pub fn check_explicit_peers_ticks(&mut self, check_explicit_peers_ticks: u64) -> &mut Self { + self.config.check_explicit_peers_ticks = check_explicit_peers_ticks; + self + } + + /// Time to live for fanout peers (default is 60 seconds). + pub fn fanout_ttl(&mut self, fanout_ttl: Duration) -> &mut Self { + self.config.fanout_ttl = fanout_ttl; + self + } + + /// The maximum byte size for each gossip (default is 2048 bytes). + pub fn max_transmit_size(&mut self, max_transmit_size: usize) -> &mut Self { + self.config.protocol.max_transmit_size = max_transmit_size; + self + } + + /// Duplicates are prevented by storing message id's of known messages in an LRU time cache. + /// This settings sets the time period that messages are stored in the cache. Duplicates can be + /// received if duplicate messages are sent at a time greater than this setting apart. The + /// default is 1 minute. + pub fn duplicate_cache_time(&mut self, cache_size: Duration) -> &mut Self { + self.config.duplicate_cache_time = cache_size; + self + } + + /// When set, prevents automatic forwarding of all received messages. This setting + /// allows a user to validate the messages before propagating them to their peers. If set, + /// the user must manually call [`crate::Behaviour::report_message_validation_result()`] on the + /// behaviour to forward a message once validated. + pub fn validate_messages(&mut self) -> &mut Self { + self.config.validate_messages = true; + self + } + + /// Determines the level of validation used when receiving messages. See [`ValidationMode`] + /// for the available types. The default is ValidationMode::Strict. + pub fn validation_mode(&mut self, validation_mode: ValidationMode) -> &mut Self { + self.config.protocol.validation_mode = validation_mode; + self + } + + /// A user-defined function allowing the user to specify the message id of a gossipsub message. + /// The default value is to concatenate the source peer id with a sequence number. Setting this + /// parameter allows the user to address packets arbitrarily. One example is content based + /// addressing, where this function may be set to `hash(message)`. This would prevent messages + /// of the same content from being duplicated. + /// + /// The function takes a [`Message`] as input and outputs a String to be + /// interpreted as the message id. + pub fn message_id_fn(&mut self, id_fn: F) -> &mut Self + where + F: Fn(&Message) -> MessageId + Send + Sync + 'static, + { + self.config.message_id_fn = Arc::new(id_fn); + self + } + + /// Enables Peer eXchange. This should be enabled in bootstrappers and other well + /// connected/trusted nodes. The default is false. + /// + /// Note: Peer exchange is not implemented today, see + /// . + pub fn do_px(&mut self) -> &mut Self { + self.config.do_px = true; + self + } + + /// Controls the number of peers to include in prune Peer eXchange. + /// + /// When we prune a peer that's eligible for PX (has a good score, etc), we will try to + /// send them signed peer records for up to [`Self::prune_peers] other peers that we + /// know of. It is recommended that this value is larger than [`Self::mesh_n_high`] so that the + /// pruned peer can reliably form a full mesh. The default is 16. + pub fn prune_peers(&mut self, prune_peers: usize) -> &mut Self { + self.config.prune_peers = prune_peers; + self + } + + /// Controls the backoff time for pruned peers. This is how long + /// a peer must wait before attempting to graft into our mesh again after being pruned. + /// When pruning a peer, we send them our value of [`Self::prune_backoff`] so they know + /// the minimum time to wait. Peers running older versions may not send a backoff time, + /// so if we receive a prune message without one, we will wait at least [`Self::prune_backoff`] + /// before attempting to re-graft. The default is one minute. + pub fn prune_backoff(&mut self, prune_backoff: Duration) -> &mut Self { + self.config.prune_backoff = prune_backoff; + self + } + + /// Controls the backoff time when unsubscribing from a topic. + /// + /// This is how long to wait before resubscribing to the topic. A short backoff period in case + /// of an unsubscribe event allows reaching a healthy mesh in a more timely manner. The default + /// is 10 seconds. + pub fn unsubscribe_backoff(&mut self, unsubscribe_backoff: u64) -> &mut Self { + self.config.unsubscribe_backoff = Duration::from_secs(unsubscribe_backoff); + self + } + + /// Number of heartbeat slots considered as slack for backoffs. This gurantees that we wait + /// at least backoff_slack heartbeats after a backoff is over before we try to graft. This + /// solves problems occuring through high latencies. In particular if + /// `backoff_slack * heartbeat_interval` is longer than any latencies between processing + /// prunes on our side and processing prunes on the receiving side this guarantees that we + /// get not punished for too early grafting. The default is 1. + pub fn backoff_slack(&mut self, backoff_slack: u32) -> &mut Self { + self.config.backoff_slack = backoff_slack; + self + } + + /// Whether to do flood publishing or not. If enabled newly created messages will always be + /// sent to all peers that are subscribed to the topic and have a good enough score. + /// The default is true. + pub fn flood_publish(&mut self, flood_publish: bool) -> &mut Self { + self.config.flood_publish = flood_publish; + self + } + + /// If a GRAFT comes before `graft_flood_threshold` has elapsed since the last PRUNE, + /// then there is an extra score penalty applied to the peer through P7. + pub fn graft_flood_threshold(&mut self, graft_flood_threshold: Duration) -> &mut Self { + self.config.graft_flood_threshold = graft_flood_threshold; + self + } + + /// Minimum number of outbound peers in the mesh network before adding more (D_out in the spec). + /// This value must be smaller or equal than `mesh_n / 2` and smaller than `mesh_n_low`. + /// The default is 2. + pub fn mesh_outbound_min(&mut self, mesh_outbound_min: usize) -> &mut Self { + self.config.mesh_outbound_min = mesh_outbound_min; + self + } + + /// Number of heartbeat ticks that specifcy the interval in which opportunistic grafting is + /// applied. Every `opportunistic_graft_ticks` we will attempt to select some high-scoring mesh + /// peers to replace lower-scoring ones, if the median score of our mesh peers falls below a + /// threshold (see ). + /// The default is 60. + pub fn opportunistic_graft_ticks(&mut self, opportunistic_graft_ticks: u64) -> &mut Self { + self.config.opportunistic_graft_ticks = opportunistic_graft_ticks; + self + } + + /// Controls how many times we will allow a peer to request the same message id through IWANT + /// gossip before we start ignoring them. This is designed to prevent peers from spamming us + /// with requests and wasting our resources. + pub fn gossip_retransimission(&mut self, gossip_retransimission: u32) -> &mut Self { + self.config.gossip_retransimission = gossip_retransimission; + self + } + + /// The maximum number of new peers to graft to during opportunistic grafting. The default is 2. + pub fn opportunistic_graft_peers(&mut self, opportunistic_graft_peers: usize) -> &mut Self { + self.config.opportunistic_graft_peers = opportunistic_graft_peers; + self + } + + /// The maximum number of messages we will process in a given RPC. If this is unset, there is + /// no limit. The default is None. + pub fn max_messages_per_rpc(&mut self, max: Option) -> &mut Self { + self.config.max_messages_per_rpc = max; + self + } + + /// The maximum number of messages to include in an IHAVE message. + /// Also controls the maximum number of IHAVE ids we will accept and request with IWANT from a + /// peer within a heartbeat, to protect from IHAVE floods. You should adjust this value from the + /// default if your system is pushing more than 5000 messages in GossipSubHistoryGossip + /// heartbeats; with the defaults this is 1666 messages/s. The default is 5000. + pub fn max_ihave_length(&mut self, max_ihave_length: usize) -> &mut Self { + self.config.max_ihave_length = max_ihave_length; + self + } + + /// GossipSubMaxIHaveMessages is the maximum number of IHAVE messages to accept from a peer + /// within a heartbeat. + pub fn max_ihave_messages(&mut self, max_ihave_messages: usize) -> &mut Self { + self.config.max_ihave_messages = max_ihave_messages; + self + } + + /// By default, gossipsub will reject messages that are sent to us that has the same message + /// source as we have specified locally. Enabling this, allows these messages and prevents + /// penalizing the peer that sent us the message. Default is false. + pub fn allow_self_origin(&mut self, allow_self_origin: bool) -> &mut Self { + self.config.allow_self_origin = allow_self_origin; + self + } + + /// Time to wait for a message requested through IWANT following an IHAVE advertisement. + /// If the message is not received within this window, a broken promise is declared and + /// the router may apply behavioural penalties. The default is 3 seconds. + pub fn iwant_followup_time(&mut self, iwant_followup_time: Duration) -> &mut Self { + self.config.iwant_followup_time = iwant_followup_time; + self + } + + /// Enable support for flooodsub peers. + pub fn support_floodsub(&mut self) -> &mut Self { + if self + .config + .protocol + .protocol_ids + .contains(&FLOODSUB_PROTOCOL) + { + return self; + } + + self.config.protocol.protocol_ids.push(FLOODSUB_PROTOCOL); + self + } + + /// Published message ids time cache duration. The default is 10 seconds. + pub fn published_message_ids_cache_time( + &mut self, + published_message_ids_cache_time: Duration, + ) -> &mut Self { + self.config.published_message_ids_cache_time = published_message_ids_cache_time; + self + } + + /// The max number of messages a `ConnectionHandler` can buffer. The default is 5000. + pub fn connection_handler_queue_len(&mut self, len: usize) -> &mut Self { + self.config.connection_handler_queue_len = len; + self + } + + /// The duration a message to be published can wait to be sent before it is abandoned. The + /// default is 5 seconds. + pub fn publish_queue_duration(&mut self, duration: Duration) -> &mut Self { + self.config.connection_handler_publish_duration = duration; + self + } + + /// The duration a message to be forwarded can wait to be sent before it is abandoned. The + /// default is 1s. + pub fn forward_queue_duration(&mut self, duration: Duration) -> &mut Self { + self.config.connection_handler_forward_duration = duration; + self + } + + /// Constructs a [`Config`] from the given configuration and validates the settings. + pub fn build(&self) -> Result { + // check all constraints on config + + if self.config.protocol.max_transmit_size < 100 { + return Err(ConfigBuilderError::MaxTransmissionSizeTooSmall); + } + + if self.config.history_length < self.config.history_gossip { + return Err(ConfigBuilderError::HistoryLengthTooSmall); + } + + if !(self.config.mesh_outbound_min <= self.config.mesh_n_low + && self.config.mesh_n_low <= self.config.mesh_n + && self.config.mesh_n <= self.config.mesh_n_high) + { + return Err(ConfigBuilderError::MeshParametersInvalid); + } + + if self.config.mesh_outbound_min * 2 > self.config.mesh_n { + return Err(ConfigBuilderError::MeshOutboundInvalid); + } + + if self.config.unsubscribe_backoff.as_millis() == 0 { + return Err(ConfigBuilderError::UnsubscribeBackoffIsZero); + } + + if self.invalid_protocol { + return Err(ConfigBuilderError::InvalidProtocol); + } + + Ok(self.config.clone()) + } +} + +impl std::fmt::Debug for Config { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut builder = f.debug_struct("GossipsubConfig"); + let _ = builder.field("protocol", &self.protocol); + let _ = builder.field("history_length", &self.history_length); + let _ = builder.field("history_gossip", &self.history_gossip); + let _ = builder.field("mesh_n", &self.mesh_n); + let _ = builder.field("mesh_n_low", &self.mesh_n_low); + let _ = builder.field("mesh_n_high", &self.mesh_n_high); + let _ = builder.field("retain_scores", &self.retain_scores); + let _ = builder.field("gossip_lazy", &self.gossip_lazy); + let _ = builder.field("gossip_factor", &self.gossip_factor); + let _ = builder.field("heartbeat_initial_delay", &self.heartbeat_initial_delay); + let _ = builder.field("heartbeat_interval", &self.heartbeat_interval); + let _ = builder.field("fanout_ttl", &self.fanout_ttl); + let _ = builder.field("duplicate_cache_time", &self.duplicate_cache_time); + let _ = builder.field("validate_messages", &self.validate_messages); + let _ = builder.field("allow_self_origin", &self.allow_self_origin); + let _ = builder.field("do_px", &self.do_px); + let _ = builder.field("prune_peers", &self.prune_peers); + let _ = builder.field("prune_backoff", &self.prune_backoff); + let _ = builder.field("backoff_slack", &self.backoff_slack); + let _ = builder.field("flood_publish", &self.flood_publish); + let _ = builder.field("graft_flood_threshold", &self.graft_flood_threshold); + let _ = builder.field("mesh_outbound_min", &self.mesh_outbound_min); + let _ = builder.field("opportunistic_graft_ticks", &self.opportunistic_graft_ticks); + let _ = builder.field("opportunistic_graft_peers", &self.opportunistic_graft_peers); + let _ = builder.field("max_messages_per_rpc", &self.max_messages_per_rpc); + let _ = builder.field("max_ihave_length", &self.max_ihave_length); + let _ = builder.field("max_ihave_messages", &self.max_ihave_messages); + let _ = builder.field("iwant_followup_time", &self.iwant_followup_time); + let _ = builder.field( + "published_message_ids_cache_time", + &self.published_message_ids_cache_time, + ); + builder.finish() + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::topic::IdentityHash; + use crate::Topic; + use libp2p::core::UpgradeInfo; + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + + #[test] + fn create_config_with_message_id_as_plain_function() { + let config = ConfigBuilder::default() + .message_id_fn(message_id_plain_function) + .build() + .unwrap(); + + let result = config.message_id(&get_gossipsub_message()); + + assert_eq!(result, get_expected_message_id()); + } + + #[test] + fn create_config_with_message_id_as_closure() { + let config = ConfigBuilder::default() + .message_id_fn(|message: &Message| { + let mut s = DefaultHasher::new(); + message.data.hash(&mut s); + let mut v = s.finish().to_string(); + v.push('e'); + MessageId::from(v) + }) + .build() + .unwrap(); + + let result = config.message_id(&get_gossipsub_message()); + + assert_eq!(result, get_expected_message_id()); + } + + #[test] + fn create_config_with_message_id_as_closure_with_variable_capture() { + let captured: char = 'e'; + + let config = ConfigBuilder::default() + .message_id_fn(move |message: &Message| { + let mut s = DefaultHasher::new(); + message.data.hash(&mut s); + let mut v = s.finish().to_string(); + v.push(captured); + MessageId::from(v) + }) + .build() + .unwrap(); + + let result = config.message_id(&get_gossipsub_message()); + + assert_eq!(result, get_expected_message_id()); + } + + #[test] + fn create_config_with_protocol_id_prefix() { + let protocol_config = ConfigBuilder::default() + .protocol_id_prefix("/purple") + .build() + .unwrap() + .protocol_config(); + + let protocol_ids = protocol_config.protocol_info(); + + assert_eq!(protocol_ids.len(), 2); + + assert_eq!( + protocol_ids[0].protocol, + StreamProtocol::new("/purple/1.1.0") + ); + assert_eq!(protocol_ids[0].kind, PeerKind::Gossipsubv1_1); + + assert_eq!( + protocol_ids[1].protocol, + StreamProtocol::new("/purple/1.0.0") + ); + assert_eq!(protocol_ids[1].kind, PeerKind::Gossipsub); + } + + #[test] + fn create_config_with_custom_protocol_id() { + let protocol_config = ConfigBuilder::default() + .protocol_id("/purple", Version::V1_0) + .build() + .unwrap() + .protocol_config(); + + let protocol_ids = protocol_config.protocol_info(); + + assert_eq!(protocol_ids.len(), 1); + + assert_eq!(protocol_ids[0].protocol, "/purple"); + assert_eq!(protocol_ids[0].kind, PeerKind::Gossipsub); + } + + fn get_gossipsub_message() -> Message { + Message { + source: None, + data: vec![12, 34, 56], + sequence_number: None, + topic: Topic::::new("test").hash(), + } + } + + fn get_expected_message_id() -> MessageId { + MessageId::from([ + 49, 55, 56, 51, 56, 52, 49, 51, 52, 51, 52, 55, 51, 51, 53, 52, 54, 54, 52, 49, 101, + ]) + } + + fn message_id_plain_function(message: &Message) -> MessageId { + let mut s = DefaultHasher::new(); + message.data.hash(&mut s); + let mut v = s.finish().to_string(); + v.push('e'); + MessageId::from(v) + } +} diff --git a/beacon_node/lighthouse_network/gossipsub/src/error.rs b/beacon_node/lighthouse_network/gossipsub/src/error.rs new file mode 100644 index 0000000000..df3332bc92 --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/error.rs @@ -0,0 +1,159 @@ +// Copyright 2020 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! Error types that can result from gossipsub. + +use libp2p::identity::SigningError; + +/// Error associated with publishing a gossipsub message. +#[derive(Debug)] +pub enum PublishError { + /// This message has already been published. + Duplicate, + /// An error occurred whilst signing the message. + SigningError(SigningError), + /// There were no peers to send this message to. + InsufficientPeers, + /// The overall message was too large. This could be due to excessive topics or an excessive + /// message size. + MessageTooLarge, + /// The compression algorithm failed. + TransformFailed(std::io::Error), + /// Messages could not be sent because all queues for peers were full. The usize represents the + /// number of peers that have full queues. + AllQueuesFull(usize), +} + +impl std::fmt::Display for PublishError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{self:?}") + } +} + +impl std::error::Error for PublishError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::SigningError(err) => Some(err), + Self::TransformFailed(err) => Some(err), + _ => None, + } + } +} + +/// Error associated with subscribing to a topic. +#[derive(Debug)] +pub enum SubscriptionError { + /// Couldn't publish our subscription + PublishError(PublishError), + /// We are not allowed to subscribe to this topic by the subscription filter + NotAllowed, +} + +impl std::fmt::Display for SubscriptionError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{self:?}") + } +} + +impl std::error::Error for SubscriptionError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::PublishError(err) => Some(err), + _ => None, + } + } +} + +impl From for PublishError { + fn from(error: SigningError) -> Self { + PublishError::SigningError(error) + } +} + +#[derive(Debug, Clone, Copy)] +pub enum ValidationError { + /// The message has an invalid signature, + InvalidSignature, + /// The sequence number was empty, expected a value. + EmptySequenceNumber, + /// The sequence number was the incorrect size + InvalidSequenceNumber, + /// The PeerId was invalid + InvalidPeerId, + /// Signature existed when validation has been sent to + /// [`crate::behaviour::MessageAuthenticity::Anonymous`]. + SignaturePresent, + /// Sequence number existed when validation has been sent to + /// [`crate::behaviour::MessageAuthenticity::Anonymous`]. + SequenceNumberPresent, + /// Message source existed when validation has been sent to + /// [`crate::behaviour::MessageAuthenticity::Anonymous`]. + MessageSourcePresent, + /// The data transformation failed. + TransformFailed, +} + +impl std::fmt::Display for ValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{self:?}") + } +} + +impl std::error::Error for ValidationError {} + +impl From for PublishError { + fn from(error: std::io::Error) -> PublishError { + PublishError::TransformFailed(error) + } +} + +/// Error associated with Config building. +#[derive(Debug)] +pub enum ConfigBuilderError { + /// Maximum transmission size is too small. + MaxTransmissionSizeTooSmall, + /// Histroy length less than history gossip length. + HistoryLengthTooSmall, + /// The ineauality doesn't hold mesh_outbound_min <= mesh_n_low <= mesh_n <= mesh_n_high + MeshParametersInvalid, + /// The inequality doesn't hold mesh_outbound_min <= self.config.mesh_n / 2 + MeshOutboundInvalid, + /// unsubscribe_backoff is zero + UnsubscribeBackoffIsZero, + /// Invalid protocol + InvalidProtocol, +} + +impl std::error::Error for ConfigBuilderError {} + +impl std::fmt::Display for ConfigBuilderError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::MaxTransmissionSizeTooSmall => { + write!(f, "Maximum transmission size is too small") + } + Self::HistoryLengthTooSmall => write!(f, "Histroy length less than history gossip length"), + Self::MeshParametersInvalid => write!(f, "The ineauality doesn't hold mesh_outbound_min <= mesh_n_low <= mesh_n <= mesh_n_high"), + Self::MeshOutboundInvalid => write!(f, "The inequality doesn't hold mesh_outbound_min <= self.config.mesh_n / 2"), + Self::UnsubscribeBackoffIsZero => write!(f, "unsubscribe_backoff is zero"), + Self::InvalidProtocol => write!(f, "Invalid protocol"), + } + } +} diff --git a/beacon_node/lighthouse_network/gossipsub/src/generated/compat.proto b/beacon_node/lighthouse_network/gossipsub/src/generated/compat.proto new file mode 100644 index 0000000000..b2753bf7e4 --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/generated/compat.proto @@ -0,0 +1,12 @@ +syntax = "proto2"; + +package compat.pb; + +message Message { + optional bytes from = 1; + optional bytes data = 2; + optional bytes seqno = 3; + repeated string topic_ids = 4; + optional bytes signature = 5; + optional bytes key = 6; +} \ No newline at end of file diff --git a/beacon_node/lighthouse_network/gossipsub/src/generated/compat/mod.rs b/beacon_node/lighthouse_network/gossipsub/src/generated/compat/mod.rs new file mode 100644 index 0000000000..aec6164c7e --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/generated/compat/mod.rs @@ -0,0 +1,2 @@ +// Automatically generated mod.rs +pub mod pb; diff --git a/beacon_node/lighthouse_network/gossipsub/src/generated/compat/pb.rs b/beacon_node/lighthouse_network/gossipsub/src/generated/compat/pb.rs new file mode 100644 index 0000000000..fd59c38e2b --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/generated/compat/pb.rs @@ -0,0 +1,67 @@ +// Automatically generated rust module for 'compat.proto' file + +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(unused_imports)] +#![allow(unknown_lints)] +#![allow(clippy::all)] +#![cfg_attr(rustfmt, rustfmt_skip)] + + +use quick_protobuf::{MessageInfo, MessageRead, MessageWrite, BytesReader, Writer, WriterBackend, Result}; +use quick_protobuf::sizeofs::*; +use super::super::*; + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct Message { + pub from: Option>, + pub data: Option>, + pub seqno: Option>, + pub topic_ids: Vec, + pub signature: Option>, + pub key: Option>, +} + +impl<'a> MessageRead<'a> for Message { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.from = Some(r.read_bytes(bytes)?.to_owned()), + Ok(18) => msg.data = Some(r.read_bytes(bytes)?.to_owned()), + Ok(26) => msg.seqno = Some(r.read_bytes(bytes)?.to_owned()), + Ok(34) => msg.topic_ids.push(r.read_string(bytes)?.to_owned()), + Ok(42) => msg.signature = Some(r.read_bytes(bytes)?.to_owned()), + Ok(50) => msg.key = Some(r.read_bytes(bytes)?.to_owned()), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl MessageWrite for Message { + fn get_size(&self) -> usize { + 0 + + self.from.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) + + self.data.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) + + self.seqno.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) + + self.topic_ids.iter().map(|s| 1 + sizeof_len((s).len())).sum::() + + self.signature.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) + + self.key.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if let Some(ref s) = self.from { w.write_with_tag(10, |w| w.write_bytes(&**s))?; } + if let Some(ref s) = self.data { w.write_with_tag(18, |w| w.write_bytes(&**s))?; } + if let Some(ref s) = self.seqno { w.write_with_tag(26, |w| w.write_bytes(&**s))?; } + for s in &self.topic_ids { w.write_with_tag(34, |w| w.write_string(&**s))?; } + if let Some(ref s) = self.signature { w.write_with_tag(42, |w| w.write_bytes(&**s))?; } + if let Some(ref s) = self.key { w.write_with_tag(50, |w| w.write_bytes(&**s))?; } + Ok(()) + } +} + diff --git a/beacon_node/lighthouse_network/gossipsub/src/generated/gossipsub/mod.rs b/beacon_node/lighthouse_network/gossipsub/src/generated/gossipsub/mod.rs new file mode 100644 index 0000000000..aec6164c7e --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/generated/gossipsub/mod.rs @@ -0,0 +1,2 @@ +// Automatically generated mod.rs +pub mod pb; diff --git a/beacon_node/lighthouse_network/gossipsub/src/generated/gossipsub/pb.rs b/beacon_node/lighthouse_network/gossipsub/src/generated/gossipsub/pb.rs new file mode 100644 index 0000000000..9a074fd61f --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/generated/gossipsub/pb.rs @@ -0,0 +1,567 @@ +// Automatically generated rust module for 'rpc.proto' file + +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(unused_imports)] +#![allow(unknown_lints)] +#![allow(clippy::all)] +#![cfg_attr(rustfmt, rustfmt_skip)] + + +use quick_protobuf::{MessageInfo, MessageRead, MessageWrite, BytesReader, Writer, WriterBackend, Result}; +use quick_protobuf::sizeofs::*; +use super::super::*; + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct RPC { + pub subscriptions: Vec, + pub publish: Vec, + pub control: Option, +} + +impl<'a> MessageRead<'a> for RPC { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.subscriptions.push(r.read_message::(bytes)?), + Ok(18) => msg.publish.push(r.read_message::(bytes)?), + Ok(26) => msg.control = Some(r.read_message::(bytes)?), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl MessageWrite for RPC { + fn get_size(&self) -> usize { + 0 + + self.subscriptions.iter().map(|s| 1 + sizeof_len((s).get_size())).sum::() + + self.publish.iter().map(|s| 1 + sizeof_len((s).get_size())).sum::() + + self.control.as_ref().map_or(0, |m| 1 + sizeof_len((m).get_size())) + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + for s in &self.subscriptions { w.write_with_tag(10, |w| w.write_message(s))?; } + for s in &self.publish { w.write_with_tag(18, |w| w.write_message(s))?; } + if let Some(ref s) = self.control { w.write_with_tag(26, |w| w.write_message(s))?; } + Ok(()) + } +} + +pub mod mod_RPC { + +use super::*; + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct SubOpts { + pub subscribe: Option, + pub topic_id: Option, +} + +impl<'a> MessageRead<'a> for SubOpts { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(8) => msg.subscribe = Some(r.read_bool(bytes)?), + Ok(18) => msg.topic_id = Some(r.read_string(bytes)?.to_owned()), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl MessageWrite for SubOpts { + fn get_size(&self) -> usize { + 0 + + self.subscribe.as_ref().map_or(0, |m| 1 + sizeof_varint(*(m) as u64)) + + self.topic_id.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if let Some(ref s) = self.subscribe { w.write_with_tag(8, |w| w.write_bool(*s))?; } + if let Some(ref s) = self.topic_id { w.write_with_tag(18, |w| w.write_string(&**s))?; } + Ok(()) + } +} + +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct Message { + pub from: Option>, + pub data: Option>, + pub seqno: Option>, + pub topic: String, + pub signature: Option>, + pub key: Option>, +} + +impl<'a> MessageRead<'a> for Message { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.from = Some(r.read_bytes(bytes)?.to_owned()), + Ok(18) => msg.data = Some(r.read_bytes(bytes)?.to_owned()), + Ok(26) => msg.seqno = Some(r.read_bytes(bytes)?.to_owned()), + Ok(34) => msg.topic = r.read_string(bytes)?.to_owned(), + Ok(42) => msg.signature = Some(r.read_bytes(bytes)?.to_owned()), + Ok(50) => msg.key = Some(r.read_bytes(bytes)?.to_owned()), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl MessageWrite for Message { + fn get_size(&self) -> usize { + 0 + + self.from.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) + + self.data.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) + + self.seqno.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) + + 1 + sizeof_len((&self.topic).len()) + + self.signature.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) + + self.key.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if let Some(ref s) = self.from { w.write_with_tag(10, |w| w.write_bytes(&**s))?; } + if let Some(ref s) = self.data { w.write_with_tag(18, |w| w.write_bytes(&**s))?; } + if let Some(ref s) = self.seqno { w.write_with_tag(26, |w| w.write_bytes(&**s))?; } + w.write_with_tag(34, |w| w.write_string(&**&self.topic))?; + if let Some(ref s) = self.signature { w.write_with_tag(42, |w| w.write_bytes(&**s))?; } + if let Some(ref s) = self.key { w.write_with_tag(50, |w| w.write_bytes(&**s))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct ControlMessage { + pub ihave: Vec, + pub iwant: Vec, + pub graft: Vec, + pub prune: Vec, +} + +impl<'a> MessageRead<'a> for ControlMessage { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.ihave.push(r.read_message::(bytes)?), + Ok(18) => msg.iwant.push(r.read_message::(bytes)?), + Ok(26) => msg.graft.push(r.read_message::(bytes)?), + Ok(34) => msg.prune.push(r.read_message::(bytes)?), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl MessageWrite for ControlMessage { + fn get_size(&self) -> usize { + 0 + + self.ihave.iter().map(|s| 1 + sizeof_len((s).get_size())).sum::() + + self.iwant.iter().map(|s| 1 + sizeof_len((s).get_size())).sum::() + + self.graft.iter().map(|s| 1 + sizeof_len((s).get_size())).sum::() + + self.prune.iter().map(|s| 1 + sizeof_len((s).get_size())).sum::() + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + for s in &self.ihave { w.write_with_tag(10, |w| w.write_message(s))?; } + for s in &self.iwant { w.write_with_tag(18, |w| w.write_message(s))?; } + for s in &self.graft { w.write_with_tag(26, |w| w.write_message(s))?; } + for s in &self.prune { w.write_with_tag(34, |w| w.write_message(s))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct ControlIHave { + pub topic_id: Option, + pub message_ids: Vec>, +} + +impl<'a> MessageRead<'a> for ControlIHave { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.topic_id = Some(r.read_string(bytes)?.to_owned()), + Ok(18) => msg.message_ids.push(r.read_bytes(bytes)?.to_owned()), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl MessageWrite for ControlIHave { + fn get_size(&self) -> usize { + 0 + + self.topic_id.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) + + self.message_ids.iter().map(|s| 1 + sizeof_len((s).len())).sum::() + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if let Some(ref s) = self.topic_id { w.write_with_tag(10, |w| w.write_string(&**s))?; } + for s in &self.message_ids { w.write_with_tag(18, |w| w.write_bytes(&**s))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct ControlIWant { + pub message_ids: Vec>, +} + +impl<'a> MessageRead<'a> for ControlIWant { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.message_ids.push(r.read_bytes(bytes)?.to_owned()), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl MessageWrite for ControlIWant { + fn get_size(&self) -> usize { + 0 + + self.message_ids.iter().map(|s| 1 + sizeof_len((s).len())).sum::() + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + for s in &self.message_ids { w.write_with_tag(10, |w| w.write_bytes(&**s))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct ControlGraft { + pub topic_id: Option, +} + +impl<'a> MessageRead<'a> for ControlGraft { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.topic_id = Some(r.read_string(bytes)?.to_owned()), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl MessageWrite for ControlGraft { + fn get_size(&self) -> usize { + 0 + + self.topic_id.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if let Some(ref s) = self.topic_id { w.write_with_tag(10, |w| w.write_string(&**s))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct ControlPrune { + pub topic_id: Option, + pub peers: Vec, + pub backoff: Option, +} + +impl<'a> MessageRead<'a> for ControlPrune { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.topic_id = Some(r.read_string(bytes)?.to_owned()), + Ok(18) => msg.peers.push(r.read_message::(bytes)?), + Ok(24) => msg.backoff = Some(r.read_uint64(bytes)?), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl MessageWrite for ControlPrune { + fn get_size(&self) -> usize { + 0 + + self.topic_id.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) + + self.peers.iter().map(|s| 1 + sizeof_len((s).get_size())).sum::() + + self.backoff.as_ref().map_or(0, |m| 1 + sizeof_varint(*(m) as u64)) + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if let Some(ref s) = self.topic_id { w.write_with_tag(10, |w| w.write_string(&**s))?; } + for s in &self.peers { w.write_with_tag(18, |w| w.write_message(s))?; } + if let Some(ref s) = self.backoff { w.write_with_tag(24, |w| w.write_uint64(*s))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct PeerInfo { + pub peer_id: Option>, + pub signed_peer_record: Option>, +} + +impl<'a> MessageRead<'a> for PeerInfo { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.peer_id = Some(r.read_bytes(bytes)?.to_owned()), + Ok(18) => msg.signed_peer_record = Some(r.read_bytes(bytes)?.to_owned()), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl MessageWrite for PeerInfo { + fn get_size(&self) -> usize { + 0 + + self.peer_id.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) + + self.signed_peer_record.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if let Some(ref s) = self.peer_id { w.write_with_tag(10, |w| w.write_bytes(&**s))?; } + if let Some(ref s) = self.signed_peer_record { w.write_with_tag(18, |w| w.write_bytes(&**s))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct TopicDescriptor { + pub name: Option, + pub auth: Option, + pub enc: Option, +} + +impl<'a> MessageRead<'a> for TopicDescriptor { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.name = Some(r.read_string(bytes)?.to_owned()), + Ok(18) => msg.auth = Some(r.read_message::(bytes)?), + Ok(26) => msg.enc = Some(r.read_message::(bytes)?), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl MessageWrite for TopicDescriptor { + fn get_size(&self) -> usize { + 0 + + self.name.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) + + self.auth.as_ref().map_or(0, |m| 1 + sizeof_len((m).get_size())) + + self.enc.as_ref().map_or(0, |m| 1 + sizeof_len((m).get_size())) + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if let Some(ref s) = self.name { w.write_with_tag(10, |w| w.write_string(&**s))?; } + if let Some(ref s) = self.auth { w.write_with_tag(18, |w| w.write_message(s))?; } + if let Some(ref s) = self.enc { w.write_with_tag(26, |w| w.write_message(s))?; } + Ok(()) + } +} + +pub mod mod_TopicDescriptor { + +use super::*; + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct AuthOpts { + pub mode: Option, + pub keys: Vec>, +} + +impl<'a> MessageRead<'a> for AuthOpts { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(8) => msg.mode = Some(r.read_enum(bytes)?), + Ok(18) => msg.keys.push(r.read_bytes(bytes)?.to_owned()), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl MessageWrite for AuthOpts { + fn get_size(&self) -> usize { + 0 + + self.mode.as_ref().map_or(0, |m| 1 + sizeof_varint(*(m) as u64)) + + self.keys.iter().map(|s| 1 + sizeof_len((s).len())).sum::() + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if let Some(ref s) = self.mode { w.write_with_tag(8, |w| w.write_enum(*s as i32))?; } + for s in &self.keys { w.write_with_tag(18, |w| w.write_bytes(&**s))?; } + Ok(()) + } +} + +pub mod mod_AuthOpts { + + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum AuthMode { + NONE = 0, + KEY = 1, + WOT = 2, +} + +impl Default for AuthMode { + fn default() -> Self { + AuthMode::NONE + } +} + +impl From for AuthMode { + fn from(i: i32) -> Self { + match i { + 0 => AuthMode::NONE, + 1 => AuthMode::KEY, + 2 => AuthMode::WOT, + _ => Self::default(), + } + } +} + +impl<'a> From<&'a str> for AuthMode { + fn from(s: &'a str) -> Self { + match s { + "NONE" => AuthMode::NONE, + "KEY" => AuthMode::KEY, + "WOT" => AuthMode::WOT, + _ => Self::default(), + } + } +} + +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct EncOpts { + pub mode: Option, + pub key_hashes: Vec>, +} + +impl<'a> MessageRead<'a> for EncOpts { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(8) => msg.mode = Some(r.read_enum(bytes)?), + Ok(18) => msg.key_hashes.push(r.read_bytes(bytes)?.to_owned()), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl MessageWrite for EncOpts { + fn get_size(&self) -> usize { + 0 + + self.mode.as_ref().map_or(0, |m| 1 + sizeof_varint(*(m) as u64)) + + self.key_hashes.iter().map(|s| 1 + sizeof_len((s).len())).sum::() + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if let Some(ref s) = self.mode { w.write_with_tag(8, |w| w.write_enum(*s as i32))?; } + for s in &self.key_hashes { w.write_with_tag(18, |w| w.write_bytes(&**s))?; } + Ok(()) + } +} + +pub mod mod_EncOpts { + + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum EncMode { + NONE = 0, + SHAREDKEY = 1, + WOT = 2, +} + +impl Default for EncMode { + fn default() -> Self { + EncMode::NONE + } +} + +impl From for EncMode { + fn from(i: i32) -> Self { + match i { + 0 => EncMode::NONE, + 1 => EncMode::SHAREDKEY, + 2 => EncMode::WOT, + _ => Self::default(), + } + } +} + +impl<'a> From<&'a str> for EncMode { + fn from(s: &'a str) -> Self { + match s { + "NONE" => EncMode::NONE, + "SHAREDKEY" => EncMode::SHAREDKEY, + "WOT" => EncMode::WOT, + _ => Self::default(), + } + } +} + +} + +} + diff --git a/beacon_node/lighthouse_network/gossipsub/src/generated/mod.rs b/beacon_node/lighthouse_network/gossipsub/src/generated/mod.rs new file mode 100644 index 0000000000..7ac564f3c3 --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/generated/mod.rs @@ -0,0 +1,3 @@ +// Automatically generated mod.rs +pub mod compat; +pub mod gossipsub; diff --git a/beacon_node/lighthouse_network/gossipsub/src/generated/rpc.proto b/beacon_node/lighthouse_network/gossipsub/src/generated/rpc.proto new file mode 100644 index 0000000000..2ce12f3f37 --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/generated/rpc.proto @@ -0,0 +1,84 @@ +syntax = "proto2"; + +package gossipsub.pb; + +message RPC { + repeated SubOpts subscriptions = 1; + repeated Message publish = 2; + + message SubOpts { + optional bool subscribe = 1; // subscribe or unsubscribe + optional string topic_id = 2; + } + + optional ControlMessage control = 3; +} + +message Message { + optional bytes from = 1; + optional bytes data = 2; + optional bytes seqno = 3; + required string topic = 4; + optional bytes signature = 5; + optional bytes key = 6; +} + +message ControlMessage { + repeated ControlIHave ihave = 1; + repeated ControlIWant iwant = 2; + repeated ControlGraft graft = 3; + repeated ControlPrune prune = 4; +} + +message ControlIHave { + optional string topic_id = 1; + repeated bytes message_ids = 2; +} + +message ControlIWant { + repeated bytes message_ids= 1; +} + +message ControlGraft { + optional string topic_id = 1; +} + +message ControlPrune { + optional string topic_id = 1; + repeated PeerInfo peers = 2; // gossipsub v1.1 PX + optional uint64 backoff = 3; // gossipsub v1.1 backoff time (in seconds) +} + +message PeerInfo { + optional bytes peer_id = 1; + optional bytes signed_peer_record = 2; +} + +// topicID = hash(topicDescriptor); (not the topic.name) +message TopicDescriptor { + optional string name = 1; + optional AuthOpts auth = 2; + optional EncOpts enc = 3; + + message AuthOpts { + optional AuthMode mode = 1; + repeated bytes keys = 2; // root keys to trust + + enum AuthMode { + NONE = 0; // no authentication, anyone can publish + KEY = 1; // only messages signed by keys in the topic descriptor are accepted + WOT = 2; // web of trust, certificates can allow publisher set to grow + } + } + + message EncOpts { + optional EncMode mode = 1; + repeated bytes key_hashes = 2; // the hashes of the shared keys used (salted) + + enum EncMode { + NONE = 0; // no encryption, anyone can read + SHAREDKEY = 1; // messages are encrypted with shared key + WOT = 2; // web of trust, certificates can allow publisher set to grow + } + } +} diff --git a/beacon_node/lighthouse_network/gossipsub/src/gossip_promises.rs b/beacon_node/lighthouse_network/gossipsub/src/gossip_promises.rs new file mode 100644 index 0000000000..43ca178556 --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/gossip_promises.rs @@ -0,0 +1,101 @@ +// Copyright 2020 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +use super::peer_score::RejectReason; +use super::MessageId; +use super::ValidationError; +use instant::Instant; +use libp2p::identity::PeerId; +use std::collections::HashMap; + +/// Tracks recently sent `IWANT` messages and checks if peers respond to them. +#[derive(Default)] +pub(crate) struct GossipPromises { + /// Stores for each tracked message id and peer the instant when this promise expires. + /// + /// If the peer didn't respond until then we consider the promise as broken and penalize the + /// peer. + promises: HashMap>, +} + +impl GossipPromises { + /// Returns true if the message id exists in the promises. + pub(crate) fn contains(&self, message: &MessageId) -> bool { + self.promises.contains_key(message) + } + + /// Track a promise to deliver a message from a list of [`MessageId`]s we are requesting. + pub(crate) fn add_promise(&mut self, peer: PeerId, messages: &[MessageId], expires: Instant) { + for message_id in messages { + // If a promise for this message id and peer already exists we don't update the expiry! + self.promises + .entry(message_id.clone()) + .or_default() + .entry(peer) + .or_insert(expires); + } + } + + pub(crate) fn message_delivered(&mut self, message_id: &MessageId) { + // Someone delivered a message, we can stop tracking all promises for it. + self.promises.remove(message_id); + } + + pub(crate) fn reject_message(&mut self, message_id: &MessageId, reason: &RejectReason) { + // A message got rejected, so we can stop tracking promises and let the score penalty apply + // from invalid message delivery. + // We do take exception and apply promise penalty regardless in the following cases, where + // the peer delivered an obviously invalid message. + match reason { + RejectReason::ValidationError(ValidationError::InvalidSignature) => (), + RejectReason::SelfOrigin => (), + _ => { + self.promises.remove(message_id); + } + }; + } + + /// Returns the number of broken promises for each peer who didn't follow up on an IWANT + /// request. + /// This should be called not too often relative to the expire times, since it iterates over + /// the whole stored data. + pub(crate) fn get_broken_promises(&mut self) -> HashMap { + let now = Instant::now(); + let mut result = HashMap::new(); + self.promises.retain(|msg, peers| { + peers.retain(|peer_id, expires| { + if *expires < now { + let count = result.entry(*peer_id).or_insert(0); + *count += 1; + tracing::debug!( + peer=%peer_id, + message=%msg, + "[Penalty] The peer broke the promise to deliver message in time!" + ); + false + } else { + true + } + }); + !peers.is_empty() + }); + result + } +} diff --git a/beacon_node/lighthouse_network/gossipsub/src/handler.rs b/beacon_node/lighthouse_network/gossipsub/src/handler.rs new file mode 100644 index 0000000000..298570955f --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/handler.rs @@ -0,0 +1,562 @@ +// Copyright 2020 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +use super::protocol::{GossipsubCodec, ProtocolConfig}; +use super::rpc_proto::proto; +use super::types::{PeerKind, RawMessage, Rpc, RpcOut, RpcReceiver}; +use super::ValidationError; +use asynchronous_codec::Framed; +use futures::future::Either; +use futures::prelude::*; +use futures::StreamExt; +use instant::Instant; +use libp2p::core::upgrade::DeniedUpgrade; +use libp2p::swarm::handler::{ + ConnectionEvent, ConnectionHandler, ConnectionHandlerEvent, DialUpgradeError, + FullyNegotiatedInbound, FullyNegotiatedOutbound, StreamUpgradeError, SubstreamProtocol, +}; +use libp2p::swarm::Stream; +use std::{ + pin::Pin, + task::{Context, Poll}, +}; + +/// The event emitted by the Handler. This informs the behaviour of various events created +/// by the handler. +#[derive(Debug)] +pub enum HandlerEvent { + /// A GossipsubRPC message has been received. This also contains a list of invalid messages (if + /// any) that were received. + Message { + /// The GossipsubRPC message excluding any invalid messages. + rpc: Rpc, + /// Any invalid messages that were received in the RPC, along with the associated + /// validation error. + invalid_messages: Vec<(RawMessage, ValidationError)>, + }, + /// An inbound or outbound substream has been established with the peer and this informs over + /// which protocol. This message only occurs once per connection. + PeerKind(PeerKind), + /// A message to be published was dropped because it could not be sent in time. + MessageDropped(RpcOut), +} + +/// A message sent from the behaviour to the handler. +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +pub enum HandlerIn { + /// The peer has joined the mesh. + JoinedMesh, + /// The peer has left the mesh. + LeftMesh, +} + +/// The maximum number of inbound or outbound substreams attempts we allow. +/// +/// Gossipsub is supposed to have a single long-lived inbound and outbound substream. On failure we +/// attempt to recreate these. This imposes an upper bound of new substreams before we consider the +/// connection faulty and disable the handler. This also prevents against potential substream +/// creation loops. +const MAX_SUBSTREAM_ATTEMPTS: usize = 5; + +#[allow(clippy::large_enum_variant)] +pub enum Handler { + Enabled(EnabledHandler), + Disabled(DisabledHandler), +} + +/// Protocol Handler that manages a single long-lived substream with a peer. +pub struct EnabledHandler { + /// Upgrade configuration for the gossipsub protocol. + listen_protocol: ProtocolConfig, + + /// The single long-lived outbound substream. + outbound_substream: Option, + + /// The single long-lived inbound substream. + inbound_substream: Option, + + /// Queue of values that we want to send to the remote + send_queue: RpcReceiver, + + /// Flag indicating that an outbound substream is being established to prevent duplicate + /// requests. + outbound_substream_establishing: bool, + + /// The number of outbound substreams we have requested. + outbound_substream_attempts: usize, + + /// The number of inbound substreams that have been created by the peer. + inbound_substream_attempts: usize, + + /// The type of peer this handler is associated to. + peer_kind: Option, + + /// Keeps track on whether we have sent the peer kind to the behaviour. + // + // NOTE: Use this flag rather than checking the substream count each poll. + peer_kind_sent: bool, + + last_io_activity: Instant, + + /// Keeps track of whether this connection is for a peer in the mesh. This is used to make + /// decisions about the keep alive state for this connection. + in_mesh: bool, +} + +pub enum DisabledHandler { + /// If the peer doesn't support the gossipsub protocol we do not immediately disconnect. + /// Rather, we disable the handler and prevent any incoming or outgoing substreams from being + /// established. + ProtocolUnsupported { + /// Keeps track on whether we have sent the peer kind to the behaviour. + peer_kind_sent: bool, + }, + /// The maximum number of inbound or outbound substream attempts have happened and thereby the + /// handler has been disabled. + MaxSubstreamAttempts, +} + +/// State of the inbound substream, opened either by us or by the remote. +enum InboundSubstreamState { + /// Waiting for a message from the remote. The idle state for an inbound substream. + WaitingInput(Framed), + /// The substream is being closed. + Closing(Framed), + /// An error occurred during processing. + Poisoned, +} + +/// State of the outbound substream, opened either by us or by the remote. +enum OutboundSubstreamState { + /// Waiting for the user to send a message. The idle state for an outbound substream. + WaitingOutput(Framed), + /// Waiting to send a message to the remote. + PendingSend(Framed, proto::RPC), + /// Waiting to flush the substream so that the data arrives to the remote. + PendingFlush(Framed), + /// An error occurred during processing. + Poisoned, +} + +impl Handler { + /// Builds a new [`Handler`]. + pub fn new(protocol_config: ProtocolConfig, message_queue: RpcReceiver) -> Self { + Handler::Enabled(EnabledHandler { + listen_protocol: protocol_config, + inbound_substream: None, + outbound_substream: None, + outbound_substream_establishing: false, + outbound_substream_attempts: 0, + inbound_substream_attempts: 0, + peer_kind: None, + peer_kind_sent: false, + last_io_activity: Instant::now(), + in_mesh: false, + send_queue: message_queue, + }) + } +} + +impl EnabledHandler { + fn on_fully_negotiated_inbound( + &mut self, + (substream, peer_kind): (Framed, PeerKind), + ) { + // update the known kind of peer + if self.peer_kind.is_none() { + self.peer_kind = Some(peer_kind); + } + + // new inbound substream. Replace the current one, if it exists. + tracing::trace!("New inbound substream request"); + self.inbound_substream = Some(InboundSubstreamState::WaitingInput(substream)); + } + + fn on_fully_negotiated_outbound( + &mut self, + FullyNegotiatedOutbound { protocol, .. }: FullyNegotiatedOutbound< + ::OutboundProtocol, + ::OutboundOpenInfo, + >, + ) { + let (substream, peer_kind) = protocol; + + // update the known kind of peer + if self.peer_kind.is_none() { + self.peer_kind = Some(peer_kind); + } + + assert!( + self.outbound_substream.is_none(), + "Established an outbound substream with one already available" + ); + self.outbound_substream = Some(OutboundSubstreamState::WaitingOutput(substream)); + } + + fn poll( + &mut self, + cx: &mut Context<'_>, + ) -> Poll< + ConnectionHandlerEvent< + ::OutboundProtocol, + ::OutboundOpenInfo, + ::ToBehaviour, + >, + > { + if !self.peer_kind_sent { + if let Some(peer_kind) = self.peer_kind.as_ref() { + self.peer_kind_sent = true; + return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour( + HandlerEvent::PeerKind(peer_kind.clone()), + )); + } + } + + // determine if we need to create the outbound stream + if !self.send_queue.poll_is_empty(cx) + && self.outbound_substream.is_none() + && !self.outbound_substream_establishing + { + self.outbound_substream_establishing = true; + return Poll::Ready(ConnectionHandlerEvent::OutboundSubstreamRequest { + protocol: SubstreamProtocol::new(self.listen_protocol.clone(), ()), + }); + } + + // process outbound stream + loop { + match std::mem::replace( + &mut self.outbound_substream, + Some(OutboundSubstreamState::Poisoned), + ) { + // outbound idle state + Some(OutboundSubstreamState::WaitingOutput(substream)) => { + if let Poll::Ready(Some(mut message)) = self.send_queue.poll_next_unpin(cx) { + match message { + RpcOut::Publish { + message: _, + ref mut timeout, + } + | RpcOut::Forward { + message: _, + ref mut timeout, + } => { + if Pin::new(timeout).poll(cx).is_ready() { + // Inform the behaviour and end the poll. + self.outbound_substream = + Some(OutboundSubstreamState::WaitingOutput(substream)); + return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour( + HandlerEvent::MessageDropped(message), + )); + } + } + _ => {} // All other messages are not time-bound. + } + self.outbound_substream = Some(OutboundSubstreamState::PendingSend( + substream, + message.into_protobuf(), + )); + continue; + } + + self.outbound_substream = + Some(OutboundSubstreamState::WaitingOutput(substream)); + break; + } + Some(OutboundSubstreamState::PendingSend(mut substream, message)) => { + match Sink::poll_ready(Pin::new(&mut substream), cx) { + Poll::Ready(Ok(())) => { + match Sink::start_send(Pin::new(&mut substream), message) { + Ok(()) => { + self.outbound_substream = + Some(OutboundSubstreamState::PendingFlush(substream)) + } + Err(e) => { + tracing::debug!( + "Failed to send message on outbound stream: {e}" + ); + self.outbound_substream = None; + break; + } + } + } + Poll::Ready(Err(e)) => { + tracing::debug!("Failed to send message on outbound stream: {e}"); + self.outbound_substream = None; + break; + } + Poll::Pending => { + self.outbound_substream = + Some(OutboundSubstreamState::PendingSend(substream, message)); + break; + } + } + } + Some(OutboundSubstreamState::PendingFlush(mut substream)) => { + match Sink::poll_flush(Pin::new(&mut substream), cx) { + Poll::Ready(Ok(())) => { + self.last_io_activity = Instant::now(); + self.outbound_substream = + Some(OutboundSubstreamState::WaitingOutput(substream)) + } + Poll::Ready(Err(e)) => { + tracing::debug!("Failed to flush outbound stream: {e}"); + self.outbound_substream = None; + break; + } + Poll::Pending => { + self.outbound_substream = + Some(OutboundSubstreamState::PendingFlush(substream)); + break; + } + } + } + None => { + self.outbound_substream = None; + break; + } + Some(OutboundSubstreamState::Poisoned) => { + unreachable!("Error occurred during outbound stream processing") + } + } + } + + // Handle inbound messages. + loop { + match std::mem::replace( + &mut self.inbound_substream, + Some(InboundSubstreamState::Poisoned), + ) { + // inbound idle state + Some(InboundSubstreamState::WaitingInput(mut substream)) => { + match substream.poll_next_unpin(cx) { + Poll::Ready(Some(Ok(message))) => { + self.last_io_activity = Instant::now(); + self.inbound_substream = + Some(InboundSubstreamState::WaitingInput(substream)); + return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour(message)); + } + Poll::Ready(Some(Err(error))) => { + tracing::debug!("Failed to read from inbound stream: {error}"); + // Close this side of the stream. If the + // peer is still around, they will re-establish their + // outbound stream i.e. our inbound stream. + self.inbound_substream = + Some(InboundSubstreamState::Closing(substream)); + } + // peer closed the stream + Poll::Ready(None) => { + tracing::debug!("Inbound stream closed by remote"); + self.inbound_substream = + Some(InboundSubstreamState::Closing(substream)); + } + Poll::Pending => { + self.inbound_substream = + Some(InboundSubstreamState::WaitingInput(substream)); + break; + } + } + } + Some(InboundSubstreamState::Closing(mut substream)) => { + match Sink::poll_close(Pin::new(&mut substream), cx) { + Poll::Ready(res) => { + if let Err(e) = res { + // Don't close the connection but just drop the inbound substream. + // In case the remote has more to send, they will open up a new + // substream. + tracing::debug!("Inbound substream error while closing: {e}"); + } + self.inbound_substream = None; + break; + } + Poll::Pending => { + self.inbound_substream = + Some(InboundSubstreamState::Closing(substream)); + break; + } + } + } + None => { + self.inbound_substream = None; + break; + } + Some(InboundSubstreamState::Poisoned) => { + unreachable!("Error occurred during inbound stream processing") + } + } + } + + // Drop the next message in queue if it's stale. + if let Poll::Ready(Some(rpc)) = self.send_queue.poll_stale(cx) { + return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour( + HandlerEvent::MessageDropped(rpc), + )); + } + + Poll::Pending + } +} + +impl ConnectionHandler for Handler { + type FromBehaviour = HandlerIn; + type ToBehaviour = HandlerEvent; + type InboundOpenInfo = (); + type InboundProtocol = either::Either; + type OutboundOpenInfo = (); + type OutboundProtocol = ProtocolConfig; + + fn listen_protocol(&self) -> SubstreamProtocol { + match self { + Handler::Enabled(handler) => { + SubstreamProtocol::new(either::Either::Left(handler.listen_protocol.clone()), ()) + } + Handler::Disabled(_) => { + SubstreamProtocol::new(either::Either::Right(DeniedUpgrade), ()) + } + } + } + + fn on_behaviour_event(&mut self, message: HandlerIn) { + match self { + Handler::Enabled(handler) => match message { + HandlerIn::JoinedMesh => { + handler.in_mesh = true; + } + HandlerIn::LeftMesh => { + handler.in_mesh = false; + } + }, + Handler::Disabled(_) => { + tracing::debug!(?message, "Handler is disabled. Dropping message"); + } + } + } + + fn connection_keep_alive(&self) -> bool { + matches!(self, Handler::Enabled(h) if h.in_mesh) + } + + #[tracing::instrument(level = "trace", name = "ConnectionHandler::poll", skip(self, cx))] + fn poll( + &mut self, + cx: &mut Context<'_>, + ) -> Poll< + ConnectionHandlerEvent, + > { + match self { + Handler::Enabled(handler) => handler.poll(cx), + Handler::Disabled(DisabledHandler::ProtocolUnsupported { peer_kind_sent }) => { + if !*peer_kind_sent { + *peer_kind_sent = true; + return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour( + HandlerEvent::PeerKind(PeerKind::NotSupported), + )); + } + + Poll::Pending + } + Handler::Disabled(DisabledHandler::MaxSubstreamAttempts) => Poll::Pending, + } + } + + fn on_connection_event( + &mut self, + event: ConnectionEvent< + Self::InboundProtocol, + Self::OutboundProtocol, + Self::InboundOpenInfo, + Self::OutboundOpenInfo, + >, + ) { + match self { + Handler::Enabled(handler) => { + if event.is_inbound() { + handler.inbound_substream_attempts += 1; + + if handler.inbound_substream_attempts == MAX_SUBSTREAM_ATTEMPTS { + tracing::warn!( + "The maximum number of inbound substreams attempts has been exceeded" + ); + *self = Handler::Disabled(DisabledHandler::MaxSubstreamAttempts); + return; + } + } + + if event.is_outbound() { + handler.outbound_substream_establishing = false; + + handler.outbound_substream_attempts += 1; + + if handler.outbound_substream_attempts == MAX_SUBSTREAM_ATTEMPTS { + tracing::warn!( + "The maximum number of outbound substream attempts has been exceeded" + ); + *self = Handler::Disabled(DisabledHandler::MaxSubstreamAttempts); + return; + } + } + + match event { + ConnectionEvent::FullyNegotiatedInbound(FullyNegotiatedInbound { + protocol, + .. + }) => match protocol { + Either::Left(protocol) => handler.on_fully_negotiated_inbound(protocol), + Either::Right(v) => void::unreachable(v), + }, + ConnectionEvent::FullyNegotiatedOutbound(fully_negotiated_outbound) => { + handler.on_fully_negotiated_outbound(fully_negotiated_outbound) + } + ConnectionEvent::DialUpgradeError(DialUpgradeError { + error: StreamUpgradeError::Timeout, + .. + }) => { + tracing::debug!("Dial upgrade error: Protocol negotiation timeout"); + } + ConnectionEvent::DialUpgradeError(DialUpgradeError { + error: StreamUpgradeError::Apply(e), + .. + }) => void::unreachable(e), + ConnectionEvent::DialUpgradeError(DialUpgradeError { + error: StreamUpgradeError::NegotiationFailed, + .. + }) => { + // The protocol is not supported + tracing::debug!( + "The remote peer does not support gossipsub on this connection" + ); + *self = Handler::Disabled(DisabledHandler::ProtocolUnsupported { + peer_kind_sent: false, + }); + } + ConnectionEvent::DialUpgradeError(DialUpgradeError { + error: StreamUpgradeError::Io(e), + .. + }) => { + tracing::debug!("Protocol negotiation failed: {e}") + } + _ => {} + } + } + Handler::Disabled(_) => {} + } + } +} diff --git a/beacon_node/lighthouse_network/gossipsub/src/lib.rs b/beacon_node/lighthouse_network/gossipsub/src/lib.rs new file mode 100644 index 0000000000..e825024cc7 --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/lib.rs @@ -0,0 +1,134 @@ +// Copyright 2020 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! Implementation of the [Gossipsub](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/README.md) protocol. +//! +//! Gossipsub is a P2P pubsub (publish/subscription) routing layer designed to extend upon +//! floodsub and meshsub routing protocols. +//! +//! # Overview +//! +//! *Note: The gossipsub protocol specifications +//! () provide an outline for the +//! routing protocol. They should be consulted for further detail.* +//! +//! Gossipsub is a blend of meshsub for data and randomsub for mesh metadata. It provides bounded +//! degree and amplification factor with the meshsub construction and augments it using gossip +//! propagation of metadata with the randomsub technique. +//! +//! The router maintains an overlay mesh network of peers on which to efficiently send messages and +//! metadata. Peers use control messages to broadcast and request known messages and +//! subscribe/unsubscribe from topics in the mesh network. +//! +//! # Important Discrepancies +//! +//! This section outlines the current implementation's potential discrepancies from that of other +//! implementations, due to undefined elements in the current specification. +//! +//! - **Topics** - In gossipsub, topics configurable by the `hash_topics` configuration parameter. +//! Topics are of type [`TopicHash`]. The current go implementation uses raw utf-8 strings, and this +//! is default configuration in rust-libp2p. Topics can be hashed (SHA256 hashed then base64 +//! encoded) by setting the `hash_topics` configuration parameter to true. +//! +//! - **Sequence Numbers** - A message on the gossipsub network is identified by the source +//! [`PeerId`](libp2p_identity::PeerId) and a nonce (sequence number) of the message. The sequence numbers in +//! this implementation are sent as raw bytes across the wire. They are 64-bit big-endian unsigned +//! integers. When messages are signed, they are monotonically increasing integers starting from a +//! random value and wrapping around u64::MAX. When messages are unsigned, they are chosen at random. +//! NOTE: These numbers are sequential in the current go implementation. +//! +//! # Peer Discovery +//! +//! Gossipsub does not provide peer discovery by itself. Peer discovery is the process by which +//! peers in a p2p network exchange information about each other among other reasons to become resistant +//! against the failure or replacement of the +//! [boot nodes](https://docs.libp2p.io/reference/glossary/#boot-node) of the network. +//! +//! Peer +//! discovery can e.g. be implemented with the help of the [Kademlia](https://github.com/libp2p/specs/blob/master/kad-dht/README.md) protocol +//! in combination with the [Identify](https://github.com/libp2p/specs/tree/master/identify) protocol. See the +//! Kademlia implementation documentation for more information. +//! +//! # Using Gossipsub +//! +//! ## Gossipsub Config +//! +//! The [`Config`] struct specifies various network performance/tuning configuration +//! parameters. Specifically it specifies: +//! +//! [`Config`]: struct.Config.html +//! +//! This struct implements the [`Default`] trait and can be initialised via +//! [`Config::default()`]. +//! +//! +//! ## Behaviour +//! +//! The [`Behaviour`] struct implements the [`libp2p_swarm::NetworkBehaviour`] trait allowing it to +//! act as the routing behaviour in a [`libp2p_swarm::Swarm`]. This struct requires an instance of +//! [`PeerId`](libp2p_identity::PeerId) and [`Config`]. +//! +//! [`Behaviour`]: struct.Behaviour.html + +//! ## Example +//! +//! For an example on how to use gossipsub, see the [chat-example](https://github.com/libp2p/rust-libp2p/tree/master/examples/chat). + +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +mod backoff; +mod behaviour; +mod config; +mod error; +mod gossip_promises; +mod handler; +mod mcache; +mod metrics; +mod peer_score; +mod protocol; +mod rpc_proto; +mod subscription_filter; +mod time_cache; +mod topic; +mod transform; +mod types; + +pub use self::behaviour::{Behaviour, Event, MessageAuthenticity}; +pub use self::config::{Config, ConfigBuilder, ValidationMode, Version}; +pub use self::error::{ConfigBuilderError, PublishError, SubscriptionError, ValidationError}; +pub use self::metrics::Config as MetricsConfig; +pub use self::peer_score::{ + score_parameter_decay, score_parameter_decay_with_base, PeerScoreParams, PeerScoreThresholds, + TopicScoreParams, +}; +pub use self::subscription_filter::{ + AllowAllSubscriptionFilter, CallbackSubscriptionFilter, CombinedSubscriptionFilters, + MaxCountSubscriptionFilter, RegexSubscriptionFilter, TopicSubscriptionFilter, + WhitelistSubscriptionFilter, +}; +pub use self::topic::{Hasher, Topic, TopicHash}; +pub use self::transform::{DataTransform, IdentityTransform}; +pub use self::types::{FailedMessages, Message, MessageAcceptance, MessageId, RawMessage}; + +#[deprecated(note = "Will be removed from the public API.")] +pub type Rpc = self::types::Rpc; + +pub type IdentTopic = Topic; +pub type Sha256Topic = Topic; diff --git a/beacon_node/lighthouse_network/gossipsub/src/mcache.rs b/beacon_node/lighthouse_network/gossipsub/src/mcache.rs new file mode 100644 index 0000000000..eced0456d6 --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/mcache.rs @@ -0,0 +1,385 @@ +// Copyright 2020 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +use super::topic::TopicHash; +use super::types::{MessageId, RawMessage}; +use libp2p::identity::PeerId; +use std::collections::hash_map::Entry; +use std::fmt::Debug; +use std::{ + collections::{HashMap, HashSet}, + fmt, +}; + +/// CacheEntry stored in the history. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct CacheEntry { + mid: MessageId, + topic: TopicHash, +} + +/// MessageCache struct holding history of messages. +#[derive(Clone)] +pub(crate) struct MessageCache { + msgs: HashMap)>, + /// For every message and peer the number of times this peer asked for the message + iwant_counts: HashMap>, + history: Vec>, + /// The number of indices in the cache history used for gossiping. That means that a message + /// won't get gossiped anymore when shift got called `gossip` many times after inserting the + /// message in the cache. + gossip: usize, +} + +impl fmt::Debug for MessageCache { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MessageCache") + .field("msgs", &self.msgs) + .field("history", &self.history) + .field("gossip", &self.gossip) + .finish() + } +} + +/// Implementation of the MessageCache. +impl MessageCache { + pub(crate) fn new(gossip: usize, history_capacity: usize) -> Self { + MessageCache { + gossip, + msgs: HashMap::default(), + iwant_counts: HashMap::default(), + history: vec![Vec::new(); history_capacity], + } + } + + /// Put a message into the memory cache. + /// + /// Returns true if the message didn't already exist in the cache. + pub(crate) fn put(&mut self, message_id: &MessageId, msg: RawMessage) -> bool { + match self.msgs.entry(message_id.clone()) { + Entry::Occupied(_) => { + // Don't add duplicate entries to the cache. + false + } + Entry::Vacant(entry) => { + let cache_entry = CacheEntry { + mid: message_id.clone(), + topic: msg.topic.clone(), + }; + entry.insert((msg, HashSet::default())); + self.history[0].push(cache_entry); + + tracing::trace!(message=?message_id, "Put message in mcache"); + true + } + } + } + + /// Keeps track of peers we know have received the message to prevent forwarding to said peers. + pub(crate) fn observe_duplicate(&mut self, message_id: &MessageId, source: &PeerId) { + if let Some((message, originating_peers)) = self.msgs.get_mut(message_id) { + // if the message is already validated, we don't need to store extra peers sending us + // duplicates as the message has already been forwarded + if message.validated { + return; + } + + originating_peers.insert(*source); + } + } + + /// Get a message with `message_id` + #[cfg(test)] + pub(crate) fn get(&self, message_id: &MessageId) -> Option<&RawMessage> { + self.msgs.get(message_id).map(|(message, _)| message) + } + + /// Increases the iwant count for the given message by one and returns the message together + /// with the iwant if the message exists. + pub(crate) fn get_with_iwant_counts( + &mut self, + message_id: &MessageId, + peer: &PeerId, + ) -> Option<(&RawMessage, u32)> { + let iwant_counts = &mut self.iwant_counts; + self.msgs.get(message_id).and_then(|(message, _)| { + if !message.validated { + None + } else { + Some((message, { + let count = iwant_counts + .entry(message_id.clone()) + .or_default() + .entry(*peer) + .or_default(); + *count += 1; + *count + })) + } + }) + } + + /// Gets a message with [`MessageId`] and tags it as validated. + /// This function also returns the known peers that have sent us this message. This is used to + /// prevent us sending redundant messages to peers who have already propagated it. + pub(crate) fn validate( + &mut self, + message_id: &MessageId, + ) -> Option<(&RawMessage, HashSet)> { + self.msgs.get_mut(message_id).map(|(message, known_peers)| { + message.validated = true; + // Clear the known peers list (after a message is validated, it is forwarded and we no + // longer need to store the originating peers). + let originating_peers = std::mem::take(known_peers); + (&*message, originating_peers) + }) + } + + /// Get a list of [`MessageId`]s for a given topic. + pub(crate) fn get_gossip_message_ids(&self, topic: &TopicHash) -> Vec { + self.history[..self.gossip] + .iter() + .fold(vec![], |mut current_entries, entries| { + // search for entries with desired topic + let mut found_entries: Vec = entries + .iter() + .filter_map(|entry| { + if &entry.topic == topic { + let mid = &entry.mid; + // Only gossip validated messages + if let Some(true) = self.msgs.get(mid).map(|(msg, _)| msg.validated) { + Some(mid.clone()) + } else { + None + } + } else { + None + } + }) + .collect(); + + // generate the list + current_entries.append(&mut found_entries); + current_entries + }) + } + + /// Shift the history array down one and delete messages associated with the + /// last entry. + pub(crate) fn shift(&mut self) { + for entry in self.history.pop().expect("history is always > 1") { + if let Some((msg, _)) = self.msgs.remove(&entry.mid) { + if !msg.validated { + // If GossipsubConfig::validate_messages is true, the implementing + // application has to ensure that Gossipsub::validate_message gets called for + // each received message within the cache timeout time." + tracing::debug!( + message=%&entry.mid, + "The message got removed from the cache without being validated." + ); + } + } + tracing::trace!(message=%&entry.mid, "Remove message from the cache"); + + self.iwant_counts.remove(&entry.mid); + } + + // Insert an empty vec in position 0 + self.history.insert(0, Vec::new()); + } + + /// Removes a message from the cache and returns it if existent + pub(crate) fn remove( + &mut self, + message_id: &MessageId, + ) -> Option<(RawMessage, HashSet)> { + //We only remove the message from msgs and iwant_count and keep the message_id in the + // history vector. Zhe id in the history vector will simply be ignored on popping. + + self.iwant_counts.remove(message_id); + self.msgs.remove(message_id) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::IdentTopic as Topic; + + fn gen_testm(x: u64, topic: TopicHash) -> (MessageId, RawMessage) { + let default_id = |message: &RawMessage| { + // default message id is: source + sequence number + let mut source_string = message.source.as_ref().unwrap().to_base58(); + source_string.push_str(&message.sequence_number.unwrap().to_string()); + MessageId::from(source_string) + }; + let u8x: u8 = x as u8; + let source = Some(PeerId::random()); + let data: Vec = vec![u8x]; + let sequence_number = Some(x); + + let m = RawMessage { + source, + data, + sequence_number, + topic, + signature: None, + key: None, + validated: false, + }; + + let id = default_id(&m); + (id, m) + } + + fn new_cache(gossip_size: usize, history: usize) -> MessageCache { + MessageCache::new(gossip_size, history) + } + + #[test] + /// Test that the message cache can be created. + fn test_new_cache() { + let x: usize = 3; + let mc = new_cache(x, 5); + + assert_eq!(mc.gossip, x); + } + + #[test] + /// Test you can put one message and get one. + fn test_put_get_one() { + let mut mc = new_cache(10, 15); + + let topic1_hash = Topic::new("topic1").hash(); + let (id, m) = gen_testm(10, topic1_hash); + + mc.put(&id, m.clone()); + + assert_eq!(mc.history[0].len(), 1); + + let fetched = mc.get(&id); + + assert_eq!(fetched.unwrap(), &m); + } + + #[test] + /// Test attempting to 'get' with a wrong id. + fn test_get_wrong() { + let mut mc = new_cache(10, 15); + + let topic1_hash = Topic::new("topic1").hash(); + let (id, m) = gen_testm(10, topic1_hash); + + mc.put(&id, m); + + // Try to get an incorrect ID + let wrong_id = MessageId::new(b"wrongid"); + let fetched = mc.get(&wrong_id); + assert!(fetched.is_none()); + } + + #[test] + /// Test attempting to 'get' empty message cache. + fn test_get_empty() { + let mc = new_cache(10, 15); + + // Try to get an incorrect ID + let wrong_string = MessageId::new(b"imempty"); + let fetched = mc.get(&wrong_string); + assert!(fetched.is_none()); + } + + #[test] + /// Test shift mechanism. + fn test_shift() { + let mut mc = new_cache(1, 5); + + let topic1_hash = Topic::new("topic1").hash(); + + // Build the message + for i in 0..10 { + let (id, m) = gen_testm(i, topic1_hash.clone()); + mc.put(&id, m.clone()); + } + + mc.shift(); + + // Ensure the shift occurred + assert!(mc.history[0].is_empty()); + assert!(mc.history[1].len() == 10); + + // Make sure no messages deleted + assert!(mc.msgs.len() == 10); + } + + #[test] + /// Test Shift with no additions. + fn test_empty_shift() { + let mut mc = new_cache(1, 5); + + let topic1_hash = Topic::new("topic1").hash(); + + // Build the message + for i in 0..10 { + let (id, m) = gen_testm(i, topic1_hash.clone()); + mc.put(&id, m.clone()); + } + + mc.shift(); + + // Ensure the shift occurred + assert!(mc.history[0].is_empty()); + assert!(mc.history[1].len() == 10); + + mc.shift(); + + assert!(mc.history[2].len() == 10); + assert!(mc.history[1].is_empty()); + assert!(mc.history[0].is_empty()); + } + + #[test] + /// Test shift to see if the last history messages are removed. + fn test_remove_last_from_shift() { + let mut mc = new_cache(4, 5); + + let topic1_hash = Topic::new("topic1").hash(); + + // Build the message + for i in 0..10 { + let (id, m) = gen_testm(i, topic1_hash.clone()); + mc.put(&id, m.clone()); + } + + // Shift right until deleting messages + mc.shift(); + mc.shift(); + mc.shift(); + mc.shift(); + + assert_eq!(mc.history[mc.history.len() - 1].len(), 10); + + // Shift and delete the messages + mc.shift(); + assert_eq!(mc.history[mc.history.len() - 1].len(), 0); + assert_eq!(mc.history[0].len(), 0); + assert_eq!(mc.msgs.len(), 0); + } +} diff --git a/beacon_node/lighthouse_network/gossipsub/src/metrics.rs b/beacon_node/lighthouse_network/gossipsub/src/metrics.rs new file mode 100644 index 0000000000..91bcd5f54b --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/metrics.rs @@ -0,0 +1,680 @@ +// Copyright 2020 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! A set of metrics used to help track and diagnose the network behaviour of the gossipsub +//! protocol. + +use std::collections::HashMap; + +use prometheus_client::encoding::{EncodeLabelSet, EncodeLabelValue}; +use prometheus_client::metrics::counter::Counter; +use prometheus_client::metrics::family::{Family, MetricConstructor}; +use prometheus_client::metrics::gauge::Gauge; +use prometheus_client::metrics::histogram::{linear_buckets, Histogram}; +use prometheus_client::registry::Registry; + +use super::topic::TopicHash; +use super::types::{MessageAcceptance, PeerKind}; + +// Default value that limits for how many topics do we store metrics. +const DEFAULT_MAX_TOPICS: usize = 300; + +// Default value that limits how many topics for which there has never been a subscription do we +// store metrics. +const DEFAULT_MAX_NEVER_SUBSCRIBED_TOPICS: usize = 100; + +#[derive(Debug, Clone)] +pub struct Config { + /// This provides an upper bound to the number of mesh topics we create metrics for. It + /// prevents unbounded labels being created in the metrics. + pub max_topics: usize, + /// Mesh topics are controlled by the user via subscriptions whereas non-mesh topics are + /// determined by users on the network. This limit permits a fixed amount of topics to allow, + /// in-addition to the mesh topics. + pub max_never_subscribed_topics: usize, + /// Buckets used for the score histograms. + pub score_buckets: Vec, +} + +impl Config { + /// Create buckets for the score histograms based on score thresholds. + pub fn buckets_using_scoring_thresholds(&mut self, params: &super::PeerScoreThresholds) { + self.score_buckets = vec![ + params.graylist_threshold, + params.publish_threshold, + params.gossip_threshold, + params.gossip_threshold / 2.0, + params.gossip_threshold / 4.0, + 0.0, + 1.0, + 10.0, + 100.0, + ]; + } +} + +impl Default for Config { + fn default() -> Self { + // Some sensible defaults + let gossip_threshold = -4000.0; + let publish_threshold = -8000.0; + let graylist_threshold = -16000.0; + let score_buckets: Vec = vec![ + graylist_threshold, + publish_threshold, + gossip_threshold, + gossip_threshold / 2.0, + gossip_threshold / 4.0, + 0.0, + 1.0, + 10.0, + 100.0, + ]; + Config { + max_topics: DEFAULT_MAX_TOPICS, + max_never_subscribed_topics: DEFAULT_MAX_NEVER_SUBSCRIBED_TOPICS, + score_buckets, + } + } +} + +/// Whether we have ever been subscribed to this topic. +type EverSubscribed = bool; + +/// A collection of metrics used throughout the Gossipsub behaviour. +pub(crate) struct Metrics { + /* Configuration parameters */ + /// Maximum number of topics for which we store metrics. This helps keep the metrics bounded. + max_topics: usize, + /// Maximum number of topics for which we store metrics, where the topic in not one to which we + /// have subscribed at some point. This helps keep the metrics bounded, since these topics come + /// from received messages and not explicit application subscriptions. + max_never_subscribed_topics: usize, + + /* Auxiliary variables */ + /// Information needed to decide if a topic is allowed or not. + topic_info: HashMap, + + /* Metrics per known topic */ + /// Status of our subscription to this topic. This metric allows analyzing other topic metrics + /// filtered by our current subscription status. + topic_subscription_status: Family, + /// Number of peers subscribed to each topic. This allows us to analyze a topic's behaviour + /// regardless of our subscription status. + topic_peers_count: Family, + /// The number of invalid messages received for a given topic. + invalid_messages: Family, + /// The number of messages accepted by the application (validation result). + accepted_messages: Family, + /// The number of messages ignored by the application (validation result). + ignored_messages: Family, + /// The number of messages rejected by the application (validation result). + rejected_messages: Family, + /// The number of publish messages dropped by the sender. + publish_messages_dropped: Family, + /// The number of forward messages dropped by the sender. + forward_messages_dropped: Family, + + /* Metrics regarding mesh state */ + /// Number of peers in our mesh. This metric should be updated with the count of peers for a + /// topic in the mesh regardless of inclusion and churn events. + mesh_peer_counts: Family, + /// Number of times we include peers in a topic mesh for different reasons. + mesh_peer_inclusion_events: Family, + /// Number of times we remove peers in a topic mesh for different reasons. + mesh_peer_churn_events: Family, + + /* Metrics regarding messages sent/received */ + /// Number of gossip messages sent to each topic. + topic_msg_sent_counts: Family, + /// Bytes from gossip messages sent to each topic. + topic_msg_sent_bytes: Family, + /// Number of gossipsub messages published to each topic. + topic_msg_published: Family, + + /// Number of gossipsub messages received on each topic (without filtering duplicates). + topic_msg_recv_counts_unfiltered: Family, + /// Number of gossipsub messages received on each topic (after filtering duplicates). + topic_msg_recv_counts: Family, + /// Bytes received from gossip messages for each topic. + topic_msg_recv_bytes: Family, + + /* Metrics related to scoring */ + /// Histogram of the scores for each mesh topic. + score_per_mesh: Family, + /// A counter of the kind of penalties being applied to peers. + scoring_penalties: Family, + + /* General Metrics */ + /// Gossipsub supports floodsub, gossipsub v1.0 and gossipsub v1.1. Peers are classified based + /// on which protocol they support. This metric keeps track of the number of peers that are + /// connected of each type. + peers_per_protocol: Family, + /// The time it takes to complete one iteration of the heartbeat. + heartbeat_duration: Histogram, + + /* Performance metrics */ + /// When the user validates a message, it tries to re propagate it to its mesh peers. If the + /// message expires from the memcache before it can be validated, we count this a cache miss + /// and it is an indicator that the memcache size should be increased. + memcache_misses: Counter, + /// The number of times we have decided that an IWANT control message is required for this + /// topic. A very high metric might indicate an underperforming network. + topic_iwant_msgs: Family, + + /// The size of the priority queue. + priority_queue_size: Histogram, + /// The size of the non-priority queue. + non_priority_queue_size: Histogram, +} + +impl Metrics { + pub(crate) fn new(registry: &mut Registry, config: Config) -> Self { + // Destructure the config to be sure everything is used. + let Config { + max_topics, + max_never_subscribed_topics, + score_buckets, + } = config; + + macro_rules! register_family { + ($name:expr, $help:expr) => {{ + let fam = Family::default(); + registry.register($name, $help, fam.clone()); + fam + }}; + } + + let topic_subscription_status = register_family!( + "topic_subscription_status", + "Subscription status per known topic" + ); + let topic_peers_count = register_family!( + "topic_peers_counts", + "Number of peers subscribed to each topic" + ); + + let invalid_messages = register_family!( + "invalid_messages_per_topic", + "Number of invalid messages received for each topic" + ); + + let accepted_messages = register_family!( + "accepted_messages_per_topic", + "Number of accepted messages received for each topic" + ); + + let ignored_messages = register_family!( + "ignored_messages_per_topic", + "Number of ignored messages received for each topic" + ); + + let rejected_messages = register_family!( + "rejected_messages_per_topic", + "Number of rejected messages received for each topic" + ); + + let publish_messages_dropped = register_family!( + "publish_messages_dropped_per_topic", + "Number of publish messages dropped per topic" + ); + + let forward_messages_dropped = register_family!( + "forward_messages_dropped_per_topic", + "Number of forward messages dropped per topic" + ); + + let mesh_peer_counts = register_family!( + "mesh_peer_counts", + "Number of peers in each topic in our mesh" + ); + let mesh_peer_inclusion_events = register_family!( + "mesh_peer_inclusion_events", + "Number of times a peer gets added to our mesh for different reasons" + ); + let mesh_peer_churn_events = register_family!( + "mesh_peer_churn_events", + "Number of times a peer gets removed from our mesh for different reasons" + ); + let topic_msg_sent_counts = register_family!( + "topic_msg_sent_counts", + "Number of gossip messages sent to each topic" + ); + let topic_msg_published = register_family!( + "topic_msg_published", + "Number of gossip messages published to each topic" + ); + let topic_msg_sent_bytes = register_family!( + "topic_msg_sent_bytes", + "Bytes from gossip messages sent to each topic" + ); + + let topic_msg_recv_counts_unfiltered = register_family!( + "topic_msg_recv_counts_unfiltered", + "Number of gossip messages received on each topic (without duplicates being filtered)" + ); + + let topic_msg_recv_counts = register_family!( + "topic_msg_recv_counts", + "Number of gossip messages received on each topic (after duplicates have been filtered)" + ); + let topic_msg_recv_bytes = register_family!( + "topic_msg_recv_bytes", + "Bytes received from gossip messages for each topic" + ); + + let hist_builder = HistBuilder { + buckets: score_buckets, + }; + + let score_per_mesh: Family<_, _, HistBuilder> = Family::new_with_constructor(hist_builder); + registry.register( + "score_per_mesh", + "Histogram of scores per mesh topic", + score_per_mesh.clone(), + ); + + let scoring_penalties = register_family!( + "scoring_penalties", + "Counter of types of scoring penalties given to peers" + ); + let peers_per_protocol = register_family!( + "peers_per_protocol", + "Number of connected peers by protocol type" + ); + + let heartbeat_duration = Histogram::new(linear_buckets(0.0, 50.0, 10)); + registry.register( + "heartbeat_duration", + "Histogram of observed heartbeat durations", + heartbeat_duration.clone(), + ); + + let topic_iwant_msgs = register_family!( + "topic_iwant_msgs", + "Number of times we have decided an IWANT is required for this topic" + ); + let memcache_misses = { + let metric = Counter::default(); + registry.register( + "memcache_misses", + "Number of times a message is not found in the duplicate cache when validating", + metric.clone(), + ); + metric + }; + + let priority_queue_size = Histogram::new(linear_buckets(0.0, 25.0, 100)); + registry.register( + "priority_queue_size", + "Histogram of observed priority queue sizes", + priority_queue_size.clone(), + ); + + let non_priority_queue_size = Histogram::new(linear_buckets(0.0, 25.0, 100)); + registry.register( + "non_priority_queue_size", + "Histogram of observed non-priority queue sizes", + non_priority_queue_size.clone(), + ); + + Self { + max_topics, + max_never_subscribed_topics, + topic_info: HashMap::default(), + topic_subscription_status, + topic_peers_count, + invalid_messages, + accepted_messages, + ignored_messages, + rejected_messages, + publish_messages_dropped, + forward_messages_dropped, + mesh_peer_counts, + mesh_peer_inclusion_events, + mesh_peer_churn_events, + topic_msg_sent_counts, + topic_msg_sent_bytes, + topic_msg_published, + topic_msg_recv_counts_unfiltered, + topic_msg_recv_counts, + topic_msg_recv_bytes, + score_per_mesh, + scoring_penalties, + peers_per_protocol, + heartbeat_duration, + memcache_misses, + topic_iwant_msgs, + priority_queue_size, + non_priority_queue_size, + } + } + + fn non_subscription_topics_count(&self) -> usize { + self.topic_info + .values() + .filter(|&ever_subscribed| !ever_subscribed) + .count() + } + + /// Registers a topic if not already known and if the bounds allow it. + fn register_topic(&mut self, topic: &TopicHash) -> Result<(), ()> { + if self.topic_info.contains_key(topic) { + Ok(()) + } else if self.topic_info.len() < self.max_topics + && self.non_subscription_topics_count() < self.max_never_subscribed_topics + { + // This is a topic without an explicit subscription and we register it if we are within + // the configured bounds. + self.topic_info.entry(topic.clone()).or_insert(false); + self.topic_subscription_status.get_or_create(topic).set(0); + Ok(()) + } else { + // We don't know this topic and there is no space left to store it + Err(()) + } + } + + /// Registers a set of topics that we want to store calculate metrics for. + pub(crate) fn register_allowed_topics(&mut self, topics: Vec) { + for topic_hash in topics { + self.topic_info.insert(topic_hash, true); + } + } + + /// Increase the number of peers that are subscribed to this topic. + pub(crate) fn inc_topic_peers(&mut self, topic: &TopicHash) { + if self.register_topic(topic).is_ok() { + self.topic_peers_count.get_or_create(topic).inc(); + } + } + + /// Decrease the number of peers that are subscribed to this topic. + pub(crate) fn dec_topic_peers(&mut self, topic: &TopicHash) { + if self.register_topic(topic).is_ok() { + self.topic_peers_count.get_or_create(topic).dec(); + } + } + + /* Mesh related methods */ + + /// Registers the subscription to a topic if the configured limits allow it. + /// Sets the registered number of peers in the mesh to 0. + pub(crate) fn joined(&mut self, topic: &TopicHash) { + if self.topic_info.contains_key(topic) || self.topic_info.len() < self.max_topics { + self.topic_info.insert(topic.clone(), true); + let was_subscribed = self.topic_subscription_status.get_or_create(topic).set(1); + debug_assert_eq!(was_subscribed, 0); + self.mesh_peer_counts.get_or_create(topic).set(0); + } + } + + /// Registers the unsubscription to a topic if the topic was previously allowed. + /// Sets the registered number of peers in the mesh to 0. + pub(crate) fn left(&mut self, topic: &TopicHash) { + if self.topic_info.contains_key(topic) { + // Depending on the configured topic bounds we could miss a mesh topic. + // So, check first if the topic was previously allowed. + let was_subscribed = self.topic_subscription_status.get_or_create(topic).set(0); + debug_assert_eq!(was_subscribed, 1); + self.mesh_peer_counts.get_or_create(topic).set(0); + } + } + + /// Register the inclusion of peers in our mesh due to some reason. + pub(crate) fn peers_included(&mut self, topic: &TopicHash, reason: Inclusion, count: usize) { + if self.register_topic(topic).is_ok() { + self.mesh_peer_inclusion_events + .get_or_create(&InclusionLabel { + hash: topic.to_string(), + reason, + }) + .inc_by(count as u64); + } + } + + /// Register the removal of peers in our mesh due to some reason. + pub(crate) fn peers_removed(&mut self, topic: &TopicHash, reason: Churn, count: usize) { + if self.register_topic(topic).is_ok() { + self.mesh_peer_churn_events + .get_or_create(&ChurnLabel { + hash: topic.to_string(), + reason, + }) + .inc_by(count as u64); + } + } + + /// Register the current number of peers in our mesh for this topic. + pub(crate) fn set_mesh_peers(&mut self, topic: &TopicHash, count: usize) { + if self.register_topic(topic).is_ok() { + // Due to limits, this topic could have not been allowed, so we check. + self.mesh_peer_counts.get_or_create(topic).set(count as i64); + } + } + + /// Register that an invalid message was received on a specific topic. + pub(crate) fn register_invalid_message(&mut self, topic: &TopicHash) { + if self.register_topic(topic).is_ok() { + self.invalid_messages.get_or_create(topic).inc(); + } + } + + /// Register a score penalty. + pub(crate) fn register_score_penalty(&mut self, penalty: Penalty) { + self.scoring_penalties + .get_or_create(&PenaltyLabel { penalty }) + .inc(); + } + + /// Registers that a message was published on a specific topic. + pub(crate) fn register_published_message(&mut self, topic: &TopicHash) { + if self.register_topic(topic).is_ok() { + self.topic_msg_published.get_or_create(topic).inc(); + } + } + + /// Register sending a message over a topic. + pub(crate) fn msg_sent(&mut self, topic: &TopicHash, bytes: usize) { + if self.register_topic(topic).is_ok() { + self.topic_msg_sent_counts.get_or_create(topic).inc(); + self.topic_msg_sent_bytes + .get_or_create(topic) + .inc_by(bytes as u64); + } + } + + /// Register sending a message over a topic. + pub(crate) fn publish_msg_dropped(&mut self, topic: &TopicHash) { + if self.register_topic(topic).is_ok() { + self.publish_messages_dropped.get_or_create(topic).inc(); + } + } + + /// Register dropping a message over a topic. + pub(crate) fn forward_msg_dropped(&mut self, topic: &TopicHash) { + if self.register_topic(topic).is_ok() { + self.forward_messages_dropped.get_or_create(topic).inc(); + } + } + + /// Register that a message was received (and was not a duplicate). + pub(crate) fn msg_recvd(&mut self, topic: &TopicHash) { + if self.register_topic(topic).is_ok() { + self.topic_msg_recv_counts.get_or_create(topic).inc(); + } + } + + /// Register that a message was received (could have been a duplicate). + pub(crate) fn msg_recvd_unfiltered(&mut self, topic: &TopicHash, bytes: usize) { + if self.register_topic(topic).is_ok() { + self.topic_msg_recv_counts_unfiltered + .get_or_create(topic) + .inc(); + self.topic_msg_recv_bytes + .get_or_create(topic) + .inc_by(bytes as u64); + } + } + + pub(crate) fn register_msg_validation( + &mut self, + topic: &TopicHash, + validation: &MessageAcceptance, + ) { + if self.register_topic(topic).is_ok() { + match validation { + MessageAcceptance::Accept => self.accepted_messages.get_or_create(topic).inc(), + MessageAcceptance::Ignore => self.ignored_messages.get_or_create(topic).inc(), + MessageAcceptance::Reject => self.rejected_messages.get_or_create(topic).inc(), + }; + } + } + + /// Register a memcache miss. + pub(crate) fn memcache_miss(&mut self) { + self.memcache_misses.inc(); + } + + /// Register sending an IWANT msg for this topic. + pub(crate) fn register_iwant(&mut self, topic: &TopicHash) { + if self.register_topic(topic).is_ok() { + self.topic_iwant_msgs.get_or_create(topic).inc(); + } + } + + /// Observes a heartbeat duration. + pub(crate) fn observe_heartbeat_duration(&mut self, millis: u64) { + self.heartbeat_duration.observe(millis as f64); + } + + /// Observes a priority queue size. + pub(crate) fn observe_priority_queue_size(&mut self, len: usize) { + self.priority_queue_size.observe(len as f64); + } + + /// Observes a non-priority queue size. + pub(crate) fn observe_non_priority_queue_size(&mut self, len: usize) { + self.non_priority_queue_size.observe(len as f64); + } + + /// Observe a score of a mesh peer. + pub(crate) fn observe_mesh_peers_score(&mut self, topic: &TopicHash, score: f64) { + if self.register_topic(topic).is_ok() { + self.score_per_mesh.get_or_create(topic).observe(score); + } + } + + /// Register a new peers connection based on its protocol. + pub(crate) fn peer_protocol_connected(&mut self, kind: PeerKind) { + self.peers_per_protocol + .get_or_create(&ProtocolLabel { protocol: kind }) + .inc(); + } + + /// Removes a peer from the counter based on its protocol when it disconnects. + pub(crate) fn peer_protocol_disconnected(&mut self, kind: PeerKind) { + let metric = self + .peers_per_protocol + .get_or_create(&ProtocolLabel { protocol: kind }); + if metric.get() != 0 { + // decrement the counter + metric.set(metric.get() - 1); + } + } +} + +/// Reasons why a peer was included in the mesh. +#[derive(PartialEq, Eq, Hash, EncodeLabelValue, Clone, Debug)] +pub(crate) enum Inclusion { + /// Peer was a fanaout peer. + Fanout, + /// Included from random selection. + Random, + /// Peer subscribed. + Subscribed, + /// Peer was included to fill the outbound quota. + Outbound, +} + +/// Reasons why a peer was removed from the mesh. +#[derive(PartialEq, Eq, Hash, EncodeLabelValue, Clone, Debug)] +pub(crate) enum Churn { + /// Peer disconnected. + Dc, + /// Peer had a bad score. + BadScore, + /// Peer sent a PRUNE. + Prune, + /// Peer unsubscribed. + Unsub, + /// Too many peers. + Excess, +} + +/// Kinds of reasons a peer's score has been penalized +#[derive(PartialEq, Eq, Hash, EncodeLabelValue, Clone, Debug)] +pub(crate) enum Penalty { + /// A peer grafted before waiting the back-off time. + GraftBackoff, + /// A Peer did not respond to an IWANT request in time. + BrokenPromise, + /// A Peer did not send enough messages as expected. + MessageDeficit, + /// Too many peers under one IP address. + IPColocation, +} + +/// Label for the mesh inclusion event metrics. +#[derive(PartialEq, Eq, Hash, EncodeLabelSet, Clone, Debug)] +struct InclusionLabel { + hash: String, + reason: Inclusion, +} + +/// Label for the mesh churn event metrics. +#[derive(PartialEq, Eq, Hash, EncodeLabelSet, Clone, Debug)] +struct ChurnLabel { + hash: String, + reason: Churn, +} + +/// Label for the kinds of protocols peers can connect as. +#[derive(PartialEq, Eq, Hash, EncodeLabelSet, Clone, Debug)] +struct ProtocolLabel { + protocol: PeerKind, +} + +/// Label for the kinds of scoring penalties that can occur +#[derive(PartialEq, Eq, Hash, EncodeLabelSet, Clone, Debug)] +struct PenaltyLabel { + penalty: Penalty, +} + +#[derive(Clone)] +struct HistBuilder { + buckets: Vec, +} + +impl MetricConstructor for HistBuilder { + fn new_metric(&self) -> Histogram { + Histogram::new(self.buckets.clone().into_iter()) + } +} diff --git a/beacon_node/lighthouse_network/gossipsub/src/mod.rs b/beacon_node/lighthouse_network/gossipsub/src/mod.rs new file mode 100644 index 0000000000..8ccdc32cdd --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/mod.rs @@ -0,0 +1,111 @@ +//! Implementation of the [Gossipsub](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/README.md) protocol. +//! +//! Gossipsub is a P2P pubsub (publish/subscription) routing layer designed to extend upon +//! floodsub and meshsub routing protocols. +//! +//! # Overview +//! +//! *Note: The gossipsub protocol specifications +//! () provide an outline for the +//! routing protocol. They should be consulted for further detail.* +//! +//! Gossipsub is a blend of meshsub for data and randomsub for mesh metadata. It provides bounded +//! degree and amplification factor with the meshsub construction and augments it using gossip +//! propagation of metadata with the randomsub technique. +//! +//! The router maintains an overlay mesh network of peers on which to efficiently send messages and +//! metadata. Peers use control messages to broadcast and request known messages and +//! subscribe/unsubscribe from topics in the mesh network. +//! +//! # Important Discrepancies +//! +//! This section outlines the current implementation's potential discrepancies from that of other +//! implementations, due to undefined elements in the current specification. +//! +//! - **Topics** - In gossipsub, topics configurable by the `hash_topics` configuration parameter. +//! Topics are of type [`TopicHash`]. The current go implementation uses raw utf-8 strings, and this +//! is default configuration in rust-libp2p. Topics can be hashed (SHA256 hashed then base64 +//! encoded) by setting the `hash_topics` configuration parameter to true. +//! +//! - **Sequence Numbers** - A message on the gossipsub network is identified by the source +//! [`PeerId`](libp2p_identity::PeerId) and a nonce (sequence number) of the message. The sequence numbers in +//! this implementation are sent as raw bytes across the wire. They are 64-bit big-endian unsigned +//! integers. When messages are signed, they are monotonically increasing integers starting from a +//! random value and wrapping around u64::MAX. When messages are unsigned, they are chosen at random. +//! NOTE: These numbers are sequential in the current go implementation. +//! +//! # Peer Discovery +//! +//! Gossipsub does not provide peer discovery by itself. Peer discovery is the process by which +//! peers in a p2p network exchange information about each other among other reasons to become resistant +//! against the failure or replacement of the +//! [boot nodes](https://docs.libp2p.io/reference/glossary/#boot-node) of the network. +//! +//! Peer +//! discovery can e.g. be implemented with the help of the [Kademlia](https://github.com/libp2p/specs/blob/master/kad-dht/README.md) protocol +//! in combination with the [Identify](https://github.com/libp2p/specs/tree/master/identify) protocol. See the +//! Kademlia implementation documentation for more information. +//! +//! # Using Gossipsub +//! +//! ## Gossipsub Config +//! +//! The [`Config`] struct specifies various network performance/tuning configuration +//! parameters. Specifically it specifies: +//! +//! [`Config`]: struct.Config.html +//! +//! This struct implements the [`Default`] trait and can be initialised via +//! [`Config::default()`]. +//! +//! +//! ## Behaviour +//! +//! The [`Behaviour`] struct implements the [`libp2p_swarm::NetworkBehaviour`] trait allowing it to +//! act as the routing behaviour in a [`libp2p_swarm::Swarm`]. This struct requires an instance of +//! [`PeerId`](libp2p_identity::PeerId) and [`Config`]. +//! +//! [`Behaviour`]: struct.Behaviour.html + +//! ## Example +//! +//! For an example on how to use gossipsub, see the [chat-example](https://github.com/libp2p/rust-libp2p/tree/master/examples/chat). + +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +mod backoff; +mod behaviour; +mod config; +mod error; +mod gossip_promises; +mod handler; +mod mcache; +mod metrics; +mod peer_score; +mod protocol; +mod rpc_proto; +mod subscription_filter; +mod time_cache; +mod topic; +mod transform; +mod types; + +pub use self::behaviour::{Behaviour, Event, MessageAuthenticity}; +pub use self::config::{Config, ConfigBuilder, ValidationMode, Version}; +pub use self::error::{ConfigBuilderError, PublishError, SubscriptionError, ValidationError}; +pub use self::metrics::Config as MetricsConfig; +pub use self::peer_score::{ + score_parameter_decay, score_parameter_decay_with_base, PeerScoreParams, PeerScoreThresholds, + TopicScoreParams, +}; +pub use self::subscription_filter::{ + AllowAllSubscriptionFilter, CallbackSubscriptionFilter, CombinedSubscriptionFilters, + MaxCountSubscriptionFilter, RegexSubscriptionFilter, TopicSubscriptionFilter, + WhitelistSubscriptionFilter, +}; +pub use self::topic::{Hasher, Topic, TopicHash}; +pub use self::transform::{DataTransform, IdentityTransform}; +pub use self::types::{Message, MessageAcceptance, MessageId, RawMessage}; +pub type IdentTopic = Topic; +pub type Sha256Topic = Topic; +pub use self::types::FailedMessages; diff --git a/beacon_node/lighthouse_network/gossipsub/src/peer_score.rs b/beacon_node/lighthouse_network/gossipsub/src/peer_score.rs new file mode 100644 index 0000000000..4d609434f1 --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/peer_score.rs @@ -0,0 +1,937 @@ +// Copyright 2020 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! +//! Manages and stores the Scoring logic of a particular peer on the gossipsub behaviour. + +use super::metrics::{Metrics, Penalty}; +use super::time_cache::TimeCache; +use super::{MessageId, TopicHash}; +use instant::Instant; +use libp2p::identity::PeerId; +use std::collections::{hash_map, HashMap, HashSet}; +use std::net::IpAddr; +use std::time::Duration; + +mod params; +use super::ValidationError; +pub use params::{ + score_parameter_decay, score_parameter_decay_with_base, PeerScoreParams, PeerScoreThresholds, + TopicScoreParams, +}; + +#[cfg(test)] +mod tests; + +/// The number of seconds delivery messages are stored in the cache. +const TIME_CACHE_DURATION: u64 = 120; + +pub(crate) struct PeerScore { + params: PeerScoreParams, + /// The score parameters. + peer_stats: HashMap, + /// Tracking peers per IP. + peer_ips: HashMap>, + /// Message delivery tracking. This is a time-cache of [`DeliveryRecord`]s. + deliveries: TimeCache, + /// callback for monitoring message delivery times + message_delivery_time_callback: Option, +} + +/// General statistics for a given gossipsub peer. +struct PeerStats { + /// Connection status of the peer. + status: ConnectionStatus, + /// Stats per topic. + topics: HashMap, + /// IP tracking for individual peers. + known_ips: HashSet, + /// Behaviour penalty that is applied to the peer, assigned by the behaviour. + behaviour_penalty: f64, + /// Application specific score. Can be manipulated by calling PeerScore::set_application_score + application_score: f64, + /// Scoring based on how whether this peer consumes messages fast enough or not. + slow_peer_penalty: f64, +} + +enum ConnectionStatus { + /// The peer is connected. + Connected, + /// The peer is disconnected + Disconnected { + /// Expiration time of the score state for disconnected peers. + expire: Instant, + }, +} + +impl Default for PeerStats { + fn default() -> Self { + PeerStats { + status: ConnectionStatus::Connected, + topics: HashMap::new(), + known_ips: HashSet::new(), + behaviour_penalty: 0f64, + application_score: 0f64, + slow_peer_penalty: 0f64, + } + } +} + +impl PeerStats { + /// Returns a mutable reference to topic stats if they exist, otherwise if the supplied parameters score the + /// topic, inserts the default stats and returns a reference to those. If neither apply, returns None. + pub(crate) fn stats_or_default_mut( + &mut self, + topic_hash: TopicHash, + params: &PeerScoreParams, + ) -> Option<&mut TopicStats> { + if params.topics.contains_key(&topic_hash) { + Some(self.topics.entry(topic_hash).or_default()) + } else { + self.topics.get_mut(&topic_hash) + } + } +} + +/// Stats assigned to peer for each topic. +struct TopicStats { + mesh_status: MeshStatus, + /// Number of first message deliveries. + first_message_deliveries: f64, + /// True if the peer has been in the mesh for enough time to activate mesh message deliveries. + mesh_message_deliveries_active: bool, + /// Number of message deliveries from the mesh. + mesh_message_deliveries: f64, + /// Mesh rate failure penalty. + mesh_failure_penalty: f64, + /// Invalid message counter. + invalid_message_deliveries: f64, +} + +impl TopicStats { + /// Returns true if the peer is in the `mesh`. + pub(crate) fn in_mesh(&self) -> bool { + matches!(self.mesh_status, MeshStatus::Active { .. }) + } +} + +/// Status defining a peer's inclusion in the mesh and associated parameters. +enum MeshStatus { + Active { + /// The time the peer was last GRAFTed; + graft_time: Instant, + /// The time the peer has been in the mesh. + mesh_time: Duration, + }, + InActive, +} + +impl MeshStatus { + /// Initialises a new [`MeshStatus::Active`] mesh status. + pub(crate) fn new_active() -> Self { + MeshStatus::Active { + graft_time: Instant::now(), + mesh_time: Duration::from_secs(0), + } + } +} + +impl Default for TopicStats { + fn default() -> Self { + TopicStats { + mesh_status: MeshStatus::InActive, + first_message_deliveries: Default::default(), + mesh_message_deliveries_active: Default::default(), + mesh_message_deliveries: Default::default(), + mesh_failure_penalty: Default::default(), + invalid_message_deliveries: Default::default(), + } + } +} + +#[derive(PartialEq, Debug)] +struct DeliveryRecord { + status: DeliveryStatus, + first_seen: Instant, + peers: HashSet, +} + +#[derive(PartialEq, Debug)] +enum DeliveryStatus { + /// Don't know (yet) if the message is valid. + Unknown, + /// The message is valid together with the validated time. + Valid(Instant), + /// The message is invalid. + Invalid, + /// Instructed by the validator to ignore the message. + Ignored, +} + +impl Default for DeliveryRecord { + fn default() -> Self { + DeliveryRecord { + status: DeliveryStatus::Unknown, + first_seen: Instant::now(), + peers: HashSet::new(), + } + } +} + +impl PeerScore { + /// Creates a new [`PeerScore`] using a given set of peer scoring parameters. + #[allow(dead_code)] + pub(crate) fn new(params: PeerScoreParams) -> Self { + Self::new_with_message_delivery_time_callback(params, None) + } + + pub(crate) fn new_with_message_delivery_time_callback( + params: PeerScoreParams, + callback: Option, + ) -> Self { + PeerScore { + params, + peer_stats: HashMap::new(), + peer_ips: HashMap::new(), + deliveries: TimeCache::new(Duration::from_secs(TIME_CACHE_DURATION)), + message_delivery_time_callback: callback, + } + } + + /// Returns the score for a peer + pub(crate) fn score(&self, peer_id: &PeerId) -> f64 { + self.metric_score(peer_id, None) + } + + /// Returns the score for a peer, logging metrics. This is called from the heartbeat and + /// increments the metric counts for penalties. + pub(crate) fn metric_score(&self, peer_id: &PeerId, mut metrics: Option<&mut Metrics>) -> f64 { + let Some(peer_stats) = self.peer_stats.get(peer_id) else { + return 0.0; + }; + let mut score = 0.0; + + // topic scores + for (topic, topic_stats) in peer_stats.topics.iter() { + // topic parameters + if let Some(topic_params) = self.params.topics.get(topic) { + // we are tracking the topic + + // the topic score + let mut topic_score = 0.0; + + // P1: time in mesh + if let MeshStatus::Active { mesh_time, .. } = topic_stats.mesh_status { + let p1 = { + let v = mesh_time.as_secs_f64() + / topic_params.time_in_mesh_quantum.as_secs_f64(); + if v < topic_params.time_in_mesh_cap { + v + } else { + topic_params.time_in_mesh_cap + } + }; + topic_score += p1 * topic_params.time_in_mesh_weight; + } + + // P2: first message deliveries + let p2 = { + let v = topic_stats.first_message_deliveries; + if v < topic_params.first_message_deliveries_cap { + v + } else { + topic_params.first_message_deliveries_cap + } + }; + topic_score += p2 * topic_params.first_message_deliveries_weight; + + // P3: mesh message deliveries + if topic_stats.mesh_message_deliveries_active + && topic_stats.mesh_message_deliveries + < topic_params.mesh_message_deliveries_threshold + { + let deficit = topic_params.mesh_message_deliveries_threshold + - topic_stats.mesh_message_deliveries; + let p3 = deficit * deficit; + topic_score += p3 * topic_params.mesh_message_deliveries_weight; + if let Some(metrics) = metrics.as_mut() { + metrics.register_score_penalty(Penalty::MessageDeficit); + } + tracing::debug!( + peer=%peer_id, + %topic, + %deficit, + penalty=%topic_score, + "[Penalty] The peer has a mesh deliveries deficit and will be penalized" + ); + } + + // P3b: + // NOTE: the weight of P3b is negative (validated in TopicScoreParams.validate), so this detracts. + let p3b = topic_stats.mesh_failure_penalty; + topic_score += p3b * topic_params.mesh_failure_penalty_weight; + + // P4: invalid messages + // NOTE: the weight of P4 is negative (validated in TopicScoreParams.validate), so this detracts. + let p4 = + topic_stats.invalid_message_deliveries * topic_stats.invalid_message_deliveries; + topic_score += p4 * topic_params.invalid_message_deliveries_weight; + + // update score, mixing with topic weight + score += topic_score * topic_params.topic_weight; + } + } + + // apply the topic score cap, if any + if self.params.topic_score_cap > 0f64 && score > self.params.topic_score_cap { + score = self.params.topic_score_cap; + } + + // P5: application-specific score + let p5 = peer_stats.application_score; + score += p5 * self.params.app_specific_weight; + + // P6: IP collocation factor + for ip in peer_stats.known_ips.iter() { + if self.params.ip_colocation_factor_whitelist.contains(ip) { + continue; + } + + // P6 has a cliff (ip_colocation_factor_threshold); it's only applied iff + // at least that many peers are connected to us from that source IP + // addr. It is quadratic, and the weight is negative (validated by + // peer_score_params.validate()). + if let Some(peers_in_ip) = self.peer_ips.get(ip).map(|peers| peers.len()) { + if (peers_in_ip as f64) > self.params.ip_colocation_factor_threshold { + let surplus = (peers_in_ip as f64) - self.params.ip_colocation_factor_threshold; + let p6 = surplus * surplus; + if let Some(metrics) = metrics.as_mut() { + metrics.register_score_penalty(Penalty::IPColocation); + } + tracing::debug!( + peer=%peer_id, + surplus_ip=%ip, + surplus=%surplus, + "[Penalty] The peer gets penalized because of too many peers with the same ip" + ); + score += p6 * self.params.ip_colocation_factor_weight; + } + } + } + + // P7: behavioural pattern penalty + if peer_stats.behaviour_penalty > self.params.behaviour_penalty_threshold { + let excess = peer_stats.behaviour_penalty - self.params.behaviour_penalty_threshold; + let p7 = excess * excess; + score += p7 * self.params.behaviour_penalty_weight; + } + + // Slow peer weighting + if peer_stats.slow_peer_penalty > self.params.slow_peer_threshold { + let excess = peer_stats.slow_peer_penalty - self.params.slow_peer_threshold; + score += excess * self.params.slow_peer_weight; + } + + score + } + + pub(crate) fn add_penalty(&mut self, peer_id: &PeerId, count: usize) { + if let Some(peer_stats) = self.peer_stats.get_mut(peer_id) { + tracing::debug!( + peer=%peer_id, + %count, + "[Penalty] Behavioral penalty for peer" + ); + peer_stats.behaviour_penalty += count as f64; + } + } + + fn remove_ips_for_peer( + peer_stats: &PeerStats, + peer_ips: &mut HashMap>, + peer_id: &PeerId, + ) { + for ip in peer_stats.known_ips.iter() { + if let Some(peer_set) = peer_ips.get_mut(ip) { + peer_set.remove(peer_id); + } + } + } + + pub(crate) fn refresh_scores(&mut self) { + let now = Instant::now(); + let params_ref = &self.params; + let peer_ips_ref = &mut self.peer_ips; + self.peer_stats.retain(|peer_id, peer_stats| { + if let ConnectionStatus::Disconnected { expire } = peer_stats.status { + // has the retention period expired? + if now > expire { + // yes, throw it away (but clean up the IP tracking first) + Self::remove_ips_for_peer(peer_stats, peer_ips_ref, peer_id); + // re address this, use retain or entry + return false; + } + + // we don't decay retained scores, as the peer is not active. + // this way the peer cannot reset a negative score by simply disconnecting and reconnecting, + // unless the retention period has elapsed. + // similarly, a well behaved peer does not lose its score by getting disconnected. + return true; + } + + for (topic, topic_stats) in peer_stats.topics.iter_mut() { + // the topic parameters + if let Some(topic_params) = params_ref.topics.get(topic) { + // decay counters + topic_stats.first_message_deliveries *= + topic_params.first_message_deliveries_decay; + if topic_stats.first_message_deliveries < params_ref.decay_to_zero { + topic_stats.first_message_deliveries = 0.0; + } + topic_stats.mesh_message_deliveries *= + topic_params.mesh_message_deliveries_decay; + if topic_stats.mesh_message_deliveries < params_ref.decay_to_zero { + topic_stats.mesh_message_deliveries = 0.0; + } + topic_stats.mesh_failure_penalty *= topic_params.mesh_failure_penalty_decay; + if topic_stats.mesh_failure_penalty < params_ref.decay_to_zero { + topic_stats.mesh_failure_penalty = 0.0; + } + topic_stats.invalid_message_deliveries *= + topic_params.invalid_message_deliveries_decay; + if topic_stats.invalid_message_deliveries < params_ref.decay_to_zero { + topic_stats.invalid_message_deliveries = 0.0; + } + // update mesh time and activate mesh message delivery parameter if need be + if let MeshStatus::Active { + ref mut mesh_time, + ref mut graft_time, + } = topic_stats.mesh_status + { + *mesh_time = now.duration_since(*graft_time); + if *mesh_time > topic_params.mesh_message_deliveries_activation { + topic_stats.mesh_message_deliveries_active = true; + } + } + } + } + + // decay P7 counter + peer_stats.behaviour_penalty *= params_ref.behaviour_penalty_decay; + if peer_stats.behaviour_penalty < params_ref.decay_to_zero { + peer_stats.behaviour_penalty = 0.0; + } + + // decay slow peer score + peer_stats.slow_peer_penalty *= params_ref.slow_peer_decay; + if peer_stats.slow_peer_penalty < params_ref.decay_to_zero { + peer_stats.slow_peer_penalty = 0.0; + } + + true + }); + } + + /// Adds a connected peer to [`PeerScore`], initialising with empty ips (ips get added later + /// through add_ip. + pub(crate) fn add_peer(&mut self, peer_id: PeerId) { + let peer_stats = self.peer_stats.entry(peer_id).or_default(); + + // mark the peer as connected + peer_stats.status = ConnectionStatus::Connected; + } + + /// Adds a new ip to a peer, if the peer is not yet known creates a new peer_stats entry for it + pub(crate) fn add_ip(&mut self, peer_id: &PeerId, ip: IpAddr) { + tracing::trace!(peer=%peer_id, %ip, "Add ip for peer"); + let peer_stats = self.peer_stats.entry(*peer_id).or_default(); + + // Mark the peer as connected (currently the default is connected, but we don't want to + // rely on the default). + peer_stats.status = ConnectionStatus::Connected; + + // Insert the ip + peer_stats.known_ips.insert(ip); + self.peer_ips.entry(ip).or_default().insert(*peer_id); + } + + /// Indicate that a peer has been too slow to consume a message. + pub(crate) fn failed_message_slow_peer(&mut self, peer_id: &PeerId) { + if let Some(peer_stats) = self.peer_stats.get_mut(peer_id) { + peer_stats.slow_peer_penalty += 1.0; + tracing::debug!(peer=%peer_id, %peer_stats.slow_peer_penalty, "[Penalty] Expired message penalty."); + } + } + + /// Removes an ip from a peer + pub(crate) fn remove_ip(&mut self, peer_id: &PeerId, ip: &IpAddr) { + if let Some(peer_stats) = self.peer_stats.get_mut(peer_id) { + peer_stats.known_ips.remove(ip); + if let Some(peer_ids) = self.peer_ips.get_mut(ip) { + tracing::trace!(peer=%peer_id, %ip, "Remove ip for peer"); + peer_ids.remove(peer_id); + } else { + tracing::trace!( + peer=%peer_id, + %ip, + "No entry in peer_ips for ip which should get removed for peer" + ); + } + } else { + tracing::trace!( + peer=%peer_id, + %ip, + "No peer_stats for peer which should remove the ip" + ); + } + } + + /// Removes a peer from the score table. This retains peer statistics if their score is + /// non-positive. + pub(crate) fn remove_peer(&mut self, peer_id: &PeerId) { + // we only retain non-positive scores of peers + if self.score(peer_id) > 0f64 { + if let hash_map::Entry::Occupied(entry) = self.peer_stats.entry(*peer_id) { + Self::remove_ips_for_peer(entry.get(), &mut self.peer_ips, peer_id); + entry.remove(); + } + return; + } + + // if the peer is retained (including it's score) the `first_message_delivery` counters + // are reset to 0 and mesh delivery penalties applied. + if let Some(peer_stats) = self.peer_stats.get_mut(peer_id) { + for (topic, topic_stats) in peer_stats.topics.iter_mut() { + topic_stats.first_message_deliveries = 0f64; + + if let Some(threshold) = self + .params + .topics + .get(topic) + .map(|param| param.mesh_message_deliveries_threshold) + { + if topic_stats.in_mesh() + && topic_stats.mesh_message_deliveries_active + && topic_stats.mesh_message_deliveries < threshold + { + let deficit = threshold - topic_stats.mesh_message_deliveries; + topic_stats.mesh_failure_penalty += deficit * deficit; + } + } + + topic_stats.mesh_status = MeshStatus::InActive; + topic_stats.mesh_message_deliveries_active = false; + } + + peer_stats.status = ConnectionStatus::Disconnected { + expire: Instant::now() + self.params.retain_score, + }; + } + } + + /// Handles scoring functionality as a peer GRAFTs to a topic. + pub(crate) fn graft(&mut self, peer_id: &PeerId, topic: impl Into) { + let topic = topic.into(); + if let Some(peer_stats) = self.peer_stats.get_mut(peer_id) { + // if we are scoring the topic, update the mesh status. + if let Some(topic_stats) = peer_stats.stats_or_default_mut(topic, &self.params) { + topic_stats.mesh_status = MeshStatus::new_active(); + topic_stats.mesh_message_deliveries_active = false; + } + } + } + + /// Handles scoring functionality as a peer PRUNEs from a topic. + pub(crate) fn prune(&mut self, peer_id: &PeerId, topic: TopicHash) { + if let Some(peer_stats) = self.peer_stats.get_mut(peer_id) { + // if we are scoring the topic, update the mesh status. + if let Some(topic_stats) = peer_stats.stats_or_default_mut(topic.clone(), &self.params) + { + // sticky mesh delivery rate failure penalty + let threshold = self + .params + .topics + .get(&topic) + .expect("Topic must exist in order for there to be topic stats") + .mesh_message_deliveries_threshold; + if topic_stats.mesh_message_deliveries_active + && topic_stats.mesh_message_deliveries < threshold + { + let deficit = threshold - topic_stats.mesh_message_deliveries; + topic_stats.mesh_failure_penalty += deficit * deficit; + } + topic_stats.mesh_message_deliveries_active = false; + topic_stats.mesh_status = MeshStatus::InActive; + } + } + } + + pub(crate) fn validate_message( + &mut self, + from: &PeerId, + msg_id: &MessageId, + topic_hash: &TopicHash, + ) { + // adds an empty record with the message id + self.deliveries.entry(msg_id.clone()).or_default(); + + if let Some(callback) = self.message_delivery_time_callback { + if self + .peer_stats + .get(from) + .and_then(|s| s.topics.get(topic_hash)) + .map(|ts| ts.in_mesh()) + .unwrap_or(false) + { + callback(from, topic_hash, 0.0); + } + } + } + + pub(crate) fn deliver_message( + &mut self, + from: &PeerId, + msg_id: &MessageId, + topic_hash: &TopicHash, + ) { + self.mark_first_message_delivery(from, topic_hash); + + let record = self.deliveries.entry(msg_id.clone()).or_default(); + + // this should be the first delivery trace + if record.status != DeliveryStatus::Unknown { + tracing::warn!( + peer=%from, + status=?record.status, + first_seen=?record.first_seen.elapsed().as_secs(), + "Unexpected delivery trace" + ); + return; + } + + // mark the message as valid and reward mesh peers that have already forwarded it to us + record.status = DeliveryStatus::Valid(Instant::now()); + for peer in record.peers.iter().cloned().collect::>() { + // this check is to make sure a peer can't send us a message twice and get a double + // count if it is a first delivery + if &peer != from { + self.mark_duplicate_message_delivery(&peer, topic_hash, None); + } + } + } + + /// Similar to `reject_message` except does not require the message id or reason for an invalid message. + pub(crate) fn reject_invalid_message(&mut self, from: &PeerId, topic_hash: &TopicHash) { + tracing::debug!( + peer=%from, + "[Penalty] Message from peer rejected because of ValidationError or SelfOrigin" + ); + + self.mark_invalid_message_delivery(from, topic_hash); + } + + // Reject a message. + pub(crate) fn reject_message( + &mut self, + from: &PeerId, + msg_id: &MessageId, + topic_hash: &TopicHash, + reason: RejectReason, + ) { + match reason { + // these messages are not tracked, but the peer is penalized as they are invalid + RejectReason::ValidationError(_) | RejectReason::SelfOrigin => { + self.reject_invalid_message(from, topic_hash); + return; + } + // we ignore those messages, so do nothing. + RejectReason::BlackListedPeer | RejectReason::BlackListedSource => { + return; + } + _ => {} // the rest are handled after record creation + } + + let peers: Vec<_> = { + let record = self.deliveries.entry(msg_id.clone()).or_default(); + + // Multiple peers can now reject the same message as we track which peers send us the + // message. If we have already updated the status, return. + if record.status != DeliveryStatus::Unknown { + return; + } + + if let RejectReason::ValidationIgnored = reason { + // we were explicitly instructed by the validator to ignore the message but not penalize + // the peer + record.status = DeliveryStatus::Ignored; + record.peers.clear(); + return; + } + + // mark the message as invalid and penalize peers that have already forwarded it. + record.status = DeliveryStatus::Invalid; + // release the delivery time tracking map to free some memory early + record.peers.drain().collect() + }; + + self.mark_invalid_message_delivery(from, topic_hash); + for peer_id in peers.iter() { + self.mark_invalid_message_delivery(peer_id, topic_hash) + } + } + + pub(crate) fn duplicated_message( + &mut self, + from: &PeerId, + msg_id: &MessageId, + topic_hash: &TopicHash, + ) { + let record = self.deliveries.entry(msg_id.clone()).or_default(); + + if record.peers.contains(from) { + // we have already seen this duplicate! + return; + } + + if let Some(callback) = self.message_delivery_time_callback { + let time = if let DeliveryStatus::Valid(validated) = record.status { + validated.elapsed().as_secs_f64() + } else { + 0.0 + }; + if self + .peer_stats + .get(from) + .and_then(|s| s.topics.get(topic_hash)) + .map(|ts| ts.in_mesh()) + .unwrap_or(false) + { + callback(from, topic_hash, time); + } + } + + match record.status { + DeliveryStatus::Unknown => { + // the message is being validated; track the peer delivery and wait for + // the Deliver/Reject notification. + record.peers.insert(*from); + } + DeliveryStatus::Valid(validated) => { + // mark the peer delivery time to only count a duplicate delivery once. + record.peers.insert(*from); + self.mark_duplicate_message_delivery(from, topic_hash, Some(validated)); + } + DeliveryStatus::Invalid => { + // we no longer track delivery time + self.mark_invalid_message_delivery(from, topic_hash); + } + DeliveryStatus::Ignored => { + // the message was ignored; do nothing (we don't know if it was valid) + } + } + } + + /// Sets the application specific score for a peer. Returns true if the peer is the peer is + /// connected or if the score of the peer is not yet expired and false otherwise. + pub(crate) fn set_application_score(&mut self, peer_id: &PeerId, new_score: f64) -> bool { + if let Some(peer_stats) = self.peer_stats.get_mut(peer_id) { + peer_stats.application_score = new_score; + true + } else { + false + } + } + + /// Sets scoring parameters for a topic. + pub(crate) fn set_topic_params(&mut self, topic_hash: TopicHash, params: TopicScoreParams) { + use hash_map::Entry::*; + match self.params.topics.entry(topic_hash.clone()) { + Occupied(mut entry) => { + let first_message_deliveries_cap = params.first_message_deliveries_cap; + let mesh_message_deliveries_cap = params.mesh_message_deliveries_cap; + let old_params = entry.insert(params); + + if old_params.first_message_deliveries_cap > first_message_deliveries_cap { + for stats in &mut self.peer_stats.values_mut() { + if let Some(tstats) = stats.topics.get_mut(&topic_hash) { + if tstats.first_message_deliveries > first_message_deliveries_cap { + tstats.first_message_deliveries = first_message_deliveries_cap; + } + } + } + } + + if old_params.mesh_message_deliveries_cap > mesh_message_deliveries_cap { + for stats in self.peer_stats.values_mut() { + if let Some(tstats) = stats.topics.get_mut(&topic_hash) { + if tstats.mesh_message_deliveries > mesh_message_deliveries_cap { + tstats.mesh_message_deliveries = mesh_message_deliveries_cap; + } + } + } + } + } + Vacant(entry) => { + entry.insert(params); + } + } + } + + /// Returns a scoring parameters for a topic if existent. + pub(crate) fn get_topic_params(&self, topic_hash: &TopicHash) -> Option<&TopicScoreParams> { + self.params.topics.get(topic_hash) + } + + /// Increments the "invalid message deliveries" counter for all scored topics the message + /// is published in. + fn mark_invalid_message_delivery(&mut self, peer_id: &PeerId, topic_hash: &TopicHash) { + if let Some(peer_stats) = self.peer_stats.get_mut(peer_id) { + if let Some(topic_stats) = + peer_stats.stats_or_default_mut(topic_hash.clone(), &self.params) + { + tracing::debug!( + peer=%peer_id, + topic=%topic_hash, + "[Penalty] Peer delivered an invalid message in topic and gets penalized \ + for it", + ); + topic_stats.invalid_message_deliveries += 1f64; + } + } + } + + /// Increments the "first message deliveries" counter for all scored topics the message is + /// published in, as well as the "mesh message deliveries" counter, if the peer is in the + /// mesh for the topic. + fn mark_first_message_delivery(&mut self, peer_id: &PeerId, topic_hash: &TopicHash) { + if let Some(peer_stats) = self.peer_stats.get_mut(peer_id) { + if let Some(topic_stats) = + peer_stats.stats_or_default_mut(topic_hash.clone(), &self.params) + { + let cap = self + .params + .topics + .get(topic_hash) + .expect("Topic must exist if there are known topic_stats") + .first_message_deliveries_cap; + topic_stats.first_message_deliveries = + if topic_stats.first_message_deliveries + 1f64 > cap { + cap + } else { + topic_stats.first_message_deliveries + 1f64 + }; + + if let MeshStatus::Active { .. } = topic_stats.mesh_status { + let cap = self + .params + .topics + .get(topic_hash) + .expect("Topic must exist if there are known topic_stats") + .mesh_message_deliveries_cap; + + topic_stats.mesh_message_deliveries = + if topic_stats.mesh_message_deliveries + 1f64 > cap { + cap + } else { + topic_stats.mesh_message_deliveries + 1f64 + }; + } + } + } + } + + /// Increments the "mesh message deliveries" counter for messages we've seen before, as long the + /// message was received within the P3 window. + fn mark_duplicate_message_delivery( + &mut self, + peer_id: &PeerId, + topic_hash: &TopicHash, + validated_time: Option, + ) { + if let Some(peer_stats) = self.peer_stats.get_mut(peer_id) { + let now = if validated_time.is_some() { + Some(Instant::now()) + } else { + None + }; + if let Some(topic_stats) = + peer_stats.stats_or_default_mut(topic_hash.clone(), &self.params) + { + if let MeshStatus::Active { .. } = topic_stats.mesh_status { + let topic_params = self + .params + .topics + .get(topic_hash) + .expect("Topic must exist if there are known topic_stats"); + + // check against the mesh delivery window -- if the validated time is passed as 0, then + // the message was received before we finished validation and thus falls within the mesh + // delivery window. + let mut falls_in_mesh_deliver_window = true; + if let Some(validated_time) = validated_time { + if let Some(now) = &now { + //should always be true + let window_time = validated_time + .checked_add(topic_params.mesh_message_deliveries_window) + .unwrap_or(*now); + if now > &window_time { + falls_in_mesh_deliver_window = false; + } + } + } + + if falls_in_mesh_deliver_window { + let cap = topic_params.mesh_message_deliveries_cap; + topic_stats.mesh_message_deliveries = + if topic_stats.mesh_message_deliveries + 1f64 > cap { + cap + } else { + topic_stats.mesh_message_deliveries + 1f64 + }; + } + } + } + } + } + + pub(crate) fn mesh_message_deliveries(&self, peer: &PeerId, topic: &TopicHash) -> Option { + self.peer_stats + .get(peer) + .and_then(|s| s.topics.get(topic)) + .map(|t| t.mesh_message_deliveries) + } +} + +/// The reason a Gossipsub message has been rejected. +#[derive(Clone, Copy)] +pub(crate) enum RejectReason { + /// The message failed the configured validation during decoding. + ValidationError(ValidationError), + /// The message source is us. + SelfOrigin, + /// The peer that sent the message was blacklisted. + BlackListedPeer, + /// The source (from field) of the message was blacklisted. + BlackListedSource, + /// The validation was ignored. + ValidationIgnored, + /// The validation failed. + ValidationFailed, +} diff --git a/beacon_node/lighthouse_network/gossipsub/src/peer_score/params.rs b/beacon_node/lighthouse_network/gossipsub/src/peer_score/params.rs new file mode 100644 index 0000000000..a5ac1b63b5 --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/peer_score/params.rs @@ -0,0 +1,404 @@ +// Copyright 2020 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +use crate::TopicHash; +use std::collections::{HashMap, HashSet}; +use std::net::IpAddr; +use std::time::Duration; + +/// The default number of seconds for a decay interval. +const DEFAULT_DECAY_INTERVAL: u64 = 1; +/// The default rate to decay to 0. +const DEFAULT_DECAY_TO_ZERO: f64 = 0.1; + +/// Computes the decay factor for a parameter, assuming the `decay_interval` is 1s +/// and that the value decays to zero if it drops below 0.01. +pub fn score_parameter_decay(decay: Duration) -> f64 { + score_parameter_decay_with_base( + decay, + Duration::from_secs(DEFAULT_DECAY_INTERVAL), + DEFAULT_DECAY_TO_ZERO, + ) +} + +/// Computes the decay factor for a parameter using base as the `decay_interval`. +pub fn score_parameter_decay_with_base(decay: Duration, base: Duration, decay_to_zero: f64) -> f64 { + // the decay is linear, so after n ticks the value is factor^n + // so factor^n = decay_to_zero => factor = decay_to_zero^(1/n) + let ticks = decay.as_secs_f64() / base.as_secs_f64(); + decay_to_zero.powf(1f64 / ticks) +} + +#[derive(Debug, Clone)] +pub struct PeerScoreThresholds { + /// The score threshold below which gossip propagation is suppressed; + /// should be negative. + pub gossip_threshold: f64, + + /// The score threshold below which we shouldn't publish when using flood + /// publishing (also applies to fanout peers); should be negative and <= `gossip_threshold`. + pub publish_threshold: f64, + + /// The score threshold below which message processing is suppressed altogether, + /// implementing an effective graylist according to peer score; should be negative and + /// <= `publish_threshold`. + pub graylist_threshold: f64, + + /// The score threshold below which px will be ignored; this should be positive + /// and limited to scores attainable by bootstrappers and other trusted nodes. + pub accept_px_threshold: f64, + + /// The median mesh score threshold before triggering opportunistic + /// grafting; this should have a small positive value. + pub opportunistic_graft_threshold: f64, +} + +impl Default for PeerScoreThresholds { + fn default() -> Self { + PeerScoreThresholds { + gossip_threshold: -10.0, + publish_threshold: -50.0, + graylist_threshold: -80.0, + accept_px_threshold: 10.0, + opportunistic_graft_threshold: 20.0, + } + } +} + +impl PeerScoreThresholds { + pub fn validate(&self) -> Result<(), &'static str> { + if self.gossip_threshold > 0f64 { + return Err("invalid gossip threshold; it must be <= 0"); + } + if self.publish_threshold > 0f64 || self.publish_threshold > self.gossip_threshold { + return Err("Invalid publish threshold; it must be <= 0 and <= gossip threshold"); + } + if self.graylist_threshold > 0f64 || self.graylist_threshold > self.publish_threshold { + return Err("Invalid graylist threshold; it must be <= 0 and <= publish threshold"); + } + if self.accept_px_threshold < 0f64 { + return Err("Invalid accept px threshold; it must be >= 0"); + } + if self.opportunistic_graft_threshold < 0f64 { + return Err("Invalid opportunistic grafting threshold; it must be >= 0"); + } + Ok(()) + } +} + +#[derive(Debug, Clone)] +pub struct PeerScoreParams { + /// Score parameters per topic. + pub topics: HashMap, + + /// Aggregate topic score cap; this limits the total contribution of topics towards a positive + /// score. It must be positive (or 0 for no cap). + pub topic_score_cap: f64, + + /// P5: Application-specific peer scoring + pub app_specific_weight: f64, + + /// P6: IP-colocation factor. + /// The parameter has an associated counter which counts the number of peers with the same IP. + /// If the number of peers in the same IP exceeds `ip_colocation_factor_threshold, then the value + /// is the square of the difference, ie `(peers_in_same_ip - ip_colocation_threshold)^2`. + /// If the number of peers in the same IP is less than the threshold, then the value is 0. + /// The weight of the parameter MUST be negative, unless you want to disable for testing. + /// Note: In order to simulate many IPs in a manageable manner when testing, you can set the weight to 0 + /// thus disabling the IP colocation penalty. + pub ip_colocation_factor_weight: f64, + pub ip_colocation_factor_threshold: f64, + pub ip_colocation_factor_whitelist: HashSet, + + /// P7: behavioural pattern penalties. + /// This parameter has an associated counter which tracks misbehaviour as detected by the + /// router. The router currently applies penalties for the following behaviors: + /// - attempting to re-graft before the prune backoff time has elapsed. + /// - not following up in IWANT requests for messages advertised with IHAVE. + /// + /// The value of the parameter is the square of the counter over the threshold, which decays + /// with BehaviourPenaltyDecay. + /// The weight of the parameter MUST be negative (or zero to disable). + pub behaviour_penalty_weight: f64, + pub behaviour_penalty_threshold: f64, + pub behaviour_penalty_decay: f64, + + /// The decay interval for parameter counters. + pub decay_interval: Duration, + + /// Counter value below which it is considered 0. + pub decay_to_zero: f64, + + /// Time to remember counters for a disconnected peer. + pub retain_score: Duration, + + /// Slow peer penalty conditions + pub slow_peer_weight: f64, + pub slow_peer_threshold: f64, + pub slow_peer_decay: f64, +} + +impl Default for PeerScoreParams { + fn default() -> Self { + PeerScoreParams { + topics: HashMap::new(), + topic_score_cap: 3600.0, + app_specific_weight: 10.0, + ip_colocation_factor_weight: -5.0, + ip_colocation_factor_threshold: 10.0, + ip_colocation_factor_whitelist: HashSet::new(), + behaviour_penalty_weight: -10.0, + behaviour_penalty_threshold: 0.0, + behaviour_penalty_decay: 0.2, + decay_interval: Duration::from_secs(DEFAULT_DECAY_INTERVAL), + decay_to_zero: DEFAULT_DECAY_TO_ZERO, + retain_score: Duration::from_secs(3600), + slow_peer_weight: -0.2, + slow_peer_threshold: 0.0, + slow_peer_decay: 0.2, + } + } +} + +/// Peer score parameter validation +impl PeerScoreParams { + pub fn validate(&self) -> Result<(), String> { + for (topic, params) in self.topics.iter() { + if let Err(e) = params.validate() { + return Err(format!("Invalid score parameters for topic {topic}: {e}")); + } + } + + // check that the topic score is 0 or something positive + if self.topic_score_cap < 0f64 { + return Err("Invalid topic score cap; must be positive (or 0 for no cap)".into()); + } + + // check the IP colocation factor + if self.ip_colocation_factor_weight > 0f64 { + return Err( + "Invalid ip_colocation_factor_weight; must be negative (or 0 to disable)".into(), + ); + } + if self.ip_colocation_factor_weight != 0f64 && self.ip_colocation_factor_threshold < 1f64 { + return Err("Invalid ip_colocation_factor_threshold; must be at least 1".into()); + } + + // check the behaviour penalty + if self.behaviour_penalty_weight > 0f64 { + return Err( + "Invalid behaviour_penalty_weight; must be negative (or 0 to disable)".into(), + ); + } + if self.behaviour_penalty_weight != 0f64 + && (self.behaviour_penalty_decay <= 0f64 || self.behaviour_penalty_decay >= 1f64) + { + return Err("invalid behaviour_penalty_decay; must be between 0 and 1".into()); + } + + if self.behaviour_penalty_threshold < 0f64 { + return Err("invalid behaviour_penalty_threshold; must be >= 0".into()); + } + + // check the decay parameters + if self.decay_interval < Duration::from_secs(1) { + return Err("Invalid decay_interval; must be at least 1s".into()); + } + if self.decay_to_zero <= 0f64 || self.decay_to_zero >= 1f64 { + return Err("Invalid decay_to_zero; must be between 0 and 1".into()); + } + + // no need to check the score retention; a value of 0 means that we don't retain scores + Ok(()) + } +} + +#[derive(Debug, Clone)] +pub struct TopicScoreParams { + /// The weight of the topic. + pub topic_weight: f64, + + /// P1: time in the mesh + /// This is the time the peer has been grafted in the mesh. + /// The value of of the parameter is the `time/time_in_mesh_quantum`, capped by `time_in_mesh_cap` + /// The weight of the parameter must be positive (or zero to disable). + pub time_in_mesh_weight: f64, + pub time_in_mesh_quantum: Duration, + pub time_in_mesh_cap: f64, + + /// P2: first message deliveries + /// This is the number of message deliveries in the topic. + /// The value of the parameter is a counter, decaying with `first_message_deliveries_decay`, and capped + /// by `first_message_deliveries_cap`. + /// The weight of the parameter MUST be positive (or zero to disable). + pub first_message_deliveries_weight: f64, + pub first_message_deliveries_decay: f64, + pub first_message_deliveries_cap: f64, + + /// P3: mesh message deliveries + /// This is the number of message deliveries in the mesh, within the + /// `mesh_message_deliveries_window` of message validation; deliveries during validation also + /// count and are retroactively applied when validation succeeds. + /// This window accounts for the minimum time before a hostile mesh peer trying to game the + /// score could replay back a valid message we just sent them. + /// It effectively tracks first and near-first deliveries, ie a message seen from a mesh peer + /// before we have forwarded it to them. + /// The parameter has an associated counter, decaying with `mesh_message_deliveries_decay`. + /// If the counter exceeds the threshold, its value is 0. + /// If the counter is below the `mesh_message_deliveries_threshold`, the value is the square of + /// the deficit, ie (`message_deliveries_threshold - counter)^2` + /// The penalty is only activated after `mesh_message_deliveries_activation` time in the mesh. + /// The weight of the parameter MUST be negative (or zero to disable). + pub mesh_message_deliveries_weight: f64, + pub mesh_message_deliveries_decay: f64, + pub mesh_message_deliveries_cap: f64, + pub mesh_message_deliveries_threshold: f64, + pub mesh_message_deliveries_window: Duration, + pub mesh_message_deliveries_activation: Duration, + + /// P3b: sticky mesh propagation failures + /// This is a sticky penalty that applies when a peer gets pruned from the mesh with an active + /// mesh message delivery penalty. + /// The weight of the parameter MUST be negative (or zero to disable) + pub mesh_failure_penalty_weight: f64, + pub mesh_failure_penalty_decay: f64, + + /// P4: invalid messages + /// This is the number of invalid messages in the topic. + /// The value of the parameter is the square of the counter, decaying with + /// `invalid_message_deliveries_decay`. + /// The weight of the parameter MUST be negative (or zero to disable). + pub invalid_message_deliveries_weight: f64, + pub invalid_message_deliveries_decay: f64, +} + +/// NOTE: The topic score parameters are very network specific. +/// For any production system, these values should be manually set. +impl Default for TopicScoreParams { + fn default() -> Self { + TopicScoreParams { + topic_weight: 0.5, + // P1 + time_in_mesh_weight: 1.0, + time_in_mesh_quantum: Duration::from_millis(1), + time_in_mesh_cap: 3600.0, + // P2 + first_message_deliveries_weight: 1.0, + first_message_deliveries_decay: 0.5, + first_message_deliveries_cap: 2000.0, + // P3 + mesh_message_deliveries_weight: -1.0, + mesh_message_deliveries_decay: 0.5, + mesh_message_deliveries_cap: 100.0, + mesh_message_deliveries_threshold: 20.0, + mesh_message_deliveries_window: Duration::from_millis(10), + mesh_message_deliveries_activation: Duration::from_secs(5), + // P3b + mesh_failure_penalty_weight: -1.0, + mesh_failure_penalty_decay: 0.5, + // P4 + invalid_message_deliveries_weight: -1.0, + invalid_message_deliveries_decay: 0.3, + } + } +} + +impl TopicScoreParams { + pub fn validate(&self) -> Result<(), &'static str> { + // make sure we have a sane topic weight + if self.topic_weight < 0f64 { + return Err("invalid topic weight; must be >= 0"); + } + + if self.time_in_mesh_quantum == Duration::from_secs(0) { + return Err("Invalid time_in_mesh_quantum; must be non zero"); + } + if self.time_in_mesh_weight < 0f64 { + return Err("Invalid time_in_mesh_weight; must be positive (or 0 to disable)"); + } + if self.time_in_mesh_weight != 0f64 && self.time_in_mesh_cap <= 0f64 { + return Err("Invalid time_in_mesh_cap must be positive"); + } + + if self.first_message_deliveries_weight < 0f64 { + return Err( + "Invalid first_message_deliveries_weight; must be positive (or 0 to disable)", + ); + } + if self.first_message_deliveries_weight != 0f64 + && (self.first_message_deliveries_decay <= 0f64 + || self.first_message_deliveries_decay >= 1f64) + { + return Err("Invalid first_message_deliveries_decay; must be between 0 and 1"); + } + if self.first_message_deliveries_weight != 0f64 && self.first_message_deliveries_cap <= 0f64 + { + return Err("Invalid first_message_deliveries_cap must be positive"); + } + + if self.mesh_message_deliveries_weight > 0f64 { + return Err( + "Invalid mesh_message_deliveries_weight; must be negative (or 0 to disable)", + ); + } + if self.mesh_message_deliveries_weight != 0f64 + && (self.mesh_message_deliveries_decay <= 0f64 + || self.mesh_message_deliveries_decay >= 1f64) + { + return Err("Invalid mesh_message_deliveries_decay; must be between 0 and 1"); + } + if self.mesh_message_deliveries_weight != 0f64 && self.mesh_message_deliveries_cap <= 0f64 { + return Err("Invalid mesh_message_deliveries_cap must be positive"); + } + if self.mesh_message_deliveries_weight != 0f64 + && self.mesh_message_deliveries_threshold <= 0f64 + { + return Err("Invalid mesh_message_deliveries_threshold; must be positive"); + } + if self.mesh_message_deliveries_weight != 0f64 + && self.mesh_message_deliveries_activation < Duration::from_secs(1) + { + return Err("Invalid mesh_message_deliveries_activation; must be at least 1s"); + } + + // check P3b + if self.mesh_failure_penalty_weight > 0f64 { + return Err("Invalid mesh_failure_penalty_weight; must be negative (or 0 to disable)"); + } + if self.mesh_failure_penalty_weight != 0f64 + && (self.mesh_failure_penalty_decay <= 0f64 || self.mesh_failure_penalty_decay >= 1f64) + { + return Err("Invalid mesh_failure_penalty_decay; must be between 0 and 1"); + } + + // check P4 + if self.invalid_message_deliveries_weight > 0f64 { + return Err( + "Invalid invalid_message_deliveries_weight; must be negative (or 0 to disable)", + ); + } + if self.invalid_message_deliveries_decay <= 0f64 + || self.invalid_message_deliveries_decay >= 1f64 + { + return Err("Invalid invalid_message_deliveries_decay; must be between 0 and 1"); + } + Ok(()) + } +} diff --git a/beacon_node/lighthouse_network/gossipsub/src/peer_score/tests.rs b/beacon_node/lighthouse_network/gossipsub/src/peer_score/tests.rs new file mode 100644 index 0000000000..064e277eed --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/peer_score/tests.rs @@ -0,0 +1,978 @@ +// Copyright 2020 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +/// A collection of unit tests mostly ported from the go implementation. +use super::*; + +use crate::types::RawMessage; +use crate::{IdentTopic as Topic, Message}; + +// estimates a value within variance +fn within_variance(value: f64, expected: f64, variance: f64) -> bool { + if expected >= 0.0 { + return value > expected * (1.0 - variance) && value < expected * (1.0 + variance); + } + value > expected * (1.0 + variance) && value < expected * (1.0 - variance) +} + +// generates a random gossipsub message with sequence number i +fn make_test_message(seq: u64) -> (MessageId, RawMessage) { + let raw_message = RawMessage { + source: Some(PeerId::random()), + data: vec![12, 34, 56], + sequence_number: Some(seq), + topic: Topic::new("test").hash(), + signature: None, + key: None, + validated: true, + }; + + let message = Message { + source: raw_message.source, + data: raw_message.data.clone(), + sequence_number: raw_message.sequence_number, + topic: raw_message.topic.clone(), + }; + + let id = default_message_id()(&message); + (id, raw_message) +} + +fn default_message_id() -> fn(&Message) -> MessageId { + |message| { + // default message id is: source + sequence number + // NOTE: If either the peer_id or source is not provided, we set to 0; + let mut source_string = if let Some(peer_id) = message.source.as_ref() { + peer_id.to_base58() + } else { + PeerId::from_bytes(&[0, 1, 0]) + .expect("Valid peer id") + .to_base58() + }; + source_string.push_str(&message.sequence_number.unwrap_or_default().to_string()); + MessageId::from(source_string) + } +} + +#[test] +fn test_score_time_in_mesh() { + // Create parameters with reasonable default values + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let mut params = PeerScoreParams { + topic_score_cap: 1000.0, + ..Default::default() + }; + + let topic_params = TopicScoreParams { + topic_weight: 0.5, + time_in_mesh_weight: 1.0, + time_in_mesh_quantum: Duration::from_millis(1), + time_in_mesh_cap: 3600.0, + ..Default::default() + }; + + params.topics.insert(topic_hash, topic_params.clone()); + + let peer_id = PeerId::random(); + + let mut peer_score = PeerScore::new(params); + // Peer score should start at 0 + peer_score.add_peer(peer_id); + + let score = peer_score.score(&peer_id); + assert!( + score == 0.0, + "expected score to start at zero. Score found: {score}" + ); + + // The time in mesh depends on how long the peer has been grafted + peer_score.graft(&peer_id, topic); + let elapsed = topic_params.time_in_mesh_quantum * 200; + std::thread::sleep(elapsed); + peer_score.refresh_scores(); + + let score = peer_score.score(&peer_id); + let expected = topic_params.topic_weight + * topic_params.time_in_mesh_weight + * (elapsed.as_millis() / topic_params.time_in_mesh_quantum.as_millis()) as f64; + assert!( + score >= expected, + "The score: {score} should be greater than or equal to: {expected}" + ); +} + +#[test] +fn test_score_time_in_mesh_cap() { + // Create parameters with reasonable default values + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let mut params = PeerScoreParams::default(); + + let topic_params = TopicScoreParams { + topic_weight: 0.5, + time_in_mesh_weight: 1.0, + time_in_mesh_quantum: Duration::from_millis(1), + time_in_mesh_cap: 10.0, + ..Default::default() + }; + + params.topics.insert(topic_hash, topic_params.clone()); + + let peer_id = PeerId::random(); + + let mut peer_score = PeerScore::new(params); + // Peer score should start at 0 + peer_score.add_peer(peer_id); + + let score = peer_score.score(&peer_id); + assert!( + score == 0.0, + "expected score to start at zero. Score found: {score}" + ); + + // The time in mesh depends on how long the peer has been grafted + peer_score.graft(&peer_id, topic); + let elapsed = topic_params.time_in_mesh_quantum * 40; + std::thread::sleep(elapsed); + peer_score.refresh_scores(); + + let score = peer_score.score(&peer_id); + let expected = topic_params.topic_weight + * topic_params.time_in_mesh_weight + * topic_params.time_in_mesh_cap; + let variance = 0.5; + assert!( + within_variance(score, expected, variance), + "The score: {} should be within {} of {}", + score, + score * variance, + expected + ); +} + +#[test] +fn test_score_first_message_deliveries() { + // Create parameters with reasonable default values + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let mut params = PeerScoreParams::default(); + + let topic_params = TopicScoreParams { + topic_weight: 1.0, + first_message_deliveries_weight: 1.0, + first_message_deliveries_decay: 1.0, + first_message_deliveries_cap: 2000.0, + time_in_mesh_weight: 0.0, + ..Default::default() + }; + + params.topics.insert(topic_hash, topic_params.clone()); + + let peer_id = PeerId::random(); + + let mut peer_score = PeerScore::new(params); + // Peer score should start at 0 + peer_score.add_peer(peer_id); + peer_score.graft(&peer_id, topic); + + // deliver a bunch of messages from the peer + let messages = 100; + for seq in 0..messages { + let (id, msg) = make_test_message(seq); + peer_score.validate_message(&peer_id, &id, &msg.topic); + peer_score.deliver_message(&peer_id, &id, &msg.topic); + } + + peer_score.refresh_scores(); + + let score = peer_score.score(&peer_id); + let expected = + topic_params.topic_weight * topic_params.first_message_deliveries_weight * messages as f64; + assert!(score == expected, "The score: {score} should be {expected}"); +} + +#[test] +fn test_score_first_message_deliveries_cap() { + // Create parameters with reasonable default values + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let mut params = PeerScoreParams::default(); + + let topic_params = TopicScoreParams { + topic_weight: 1.0, + first_message_deliveries_weight: 1.0, + first_message_deliveries_decay: 1.0, // test without decay + first_message_deliveries_cap: 50.0, + time_in_mesh_weight: 0.0, + ..Default::default() + }; + + params.topics.insert(topic_hash, topic_params.clone()); + + let peer_id = PeerId::random(); + + let mut peer_score = PeerScore::new(params); + // Peer score should start at 0 + peer_score.add_peer(peer_id); + peer_score.graft(&peer_id, topic); + + // deliver a bunch of messages from the peer + let messages = 100; + for seq in 0..messages { + let (id, msg) = make_test_message(seq); + peer_score.validate_message(&peer_id, &id, &msg.topic); + peer_score.deliver_message(&peer_id, &id, &msg.topic); + } + + peer_score.refresh_scores(); + let score = peer_score.score(&peer_id); + let expected = topic_params.topic_weight + * topic_params.first_message_deliveries_weight + * topic_params.first_message_deliveries_cap; + assert!(score == expected, "The score: {score} should be {expected}"); +} + +#[test] +fn test_score_first_message_deliveries_decay() { + // Create parameters with reasonable default values + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let mut params = PeerScoreParams::default(); + + let topic_params = TopicScoreParams { + topic_weight: 1.0, + first_message_deliveries_weight: 1.0, + first_message_deliveries_decay: 0.9, // decay 10% per decay interval + first_message_deliveries_cap: 2000.0, + time_in_mesh_weight: 0.0, + ..Default::default() + }; + + params.topics.insert(topic_hash, topic_params.clone()); + let peer_id = PeerId::random(); + let mut peer_score = PeerScore::new(params); + peer_score.add_peer(peer_id); + peer_score.graft(&peer_id, topic); + + // deliver a bunch of messages from the peer + let messages = 100; + for seq in 0..messages { + let (id, msg) = make_test_message(seq); + peer_score.validate_message(&peer_id, &id, &msg.topic); + peer_score.deliver_message(&peer_id, &id, &msg.topic); + } + + peer_score.refresh_scores(); + let score = peer_score.score(&peer_id); + let mut expected = topic_params.topic_weight + * topic_params.first_message_deliveries_weight + * topic_params.first_message_deliveries_decay + * messages as f64; + assert!(score == expected, "The score: {score} should be {expected}"); + + // refreshing the scores applies the decay param + let decay_intervals = 10; + for _ in 0..decay_intervals { + peer_score.refresh_scores(); + expected *= topic_params.first_message_deliveries_decay; + } + let score = peer_score.score(&peer_id); + assert!(score == expected, "The score: {score} should be {expected}"); +} + +#[test] +fn test_score_mesh_message_deliveries() { + // Create parameters with reasonable default values + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let mut params = PeerScoreParams::default(); + + let topic_params = TopicScoreParams { + topic_weight: 1.0, + mesh_message_deliveries_weight: -1.0, + mesh_message_deliveries_activation: Duration::from_secs(1), + mesh_message_deliveries_window: Duration::from_millis(10), + mesh_message_deliveries_threshold: 20.0, + mesh_message_deliveries_cap: 100.0, + mesh_message_deliveries_decay: 1.0, + first_message_deliveries_weight: 0.0, + time_in_mesh_weight: 0.0, + mesh_failure_penalty_weight: 0.0, + ..Default::default() + }; + + params.topics.insert(topic_hash, topic_params.clone()); + let mut peer_score = PeerScore::new(params); + + // peer A always delivers the message first. + // peer B delivers next (within the delivery window). + // peer C delivers outside the delivery window. + // we expect peers A and B to have a score of zero, since all other parameter weights are zero. + // Peer C should have a negative score. + let peer_id_a = PeerId::random(); + let peer_id_b = PeerId::random(); + let peer_id_c = PeerId::random(); + + let peers = vec![peer_id_a, peer_id_b, peer_id_c]; + + for peer_id in &peers { + peer_score.add_peer(*peer_id); + peer_score.graft(peer_id, topic.clone()); + } + + // assert that nobody has been penalized yet for not delivering messages before activation time + peer_score.refresh_scores(); + for peer_id in &peers { + let score = peer_score.score(peer_id); + assert!( + score >= 0.0, + "expected no mesh delivery penalty before activation time, got score {score}" + ); + } + + // wait for the activation time to kick in + std::thread::sleep(topic_params.mesh_message_deliveries_activation); + + // deliver a bunch of messages from peer A, with duplicates within the window from peer B, + // and duplicates outside the window from peer C. + let messages = 100; + let mut messages_to_send = Vec::new(); + for seq in 0..messages { + let (id, msg) = make_test_message(seq); + peer_score.validate_message(&peer_id_a, &id, &msg.topic); + peer_score.deliver_message(&peer_id_a, &id, &msg.topic); + + peer_score.duplicated_message(&peer_id_b, &id, &msg.topic); + messages_to_send.push((id, msg)); + } + + std::thread::sleep(topic_params.mesh_message_deliveries_window + Duration::from_millis(20)); + + for (id, msg) in messages_to_send { + peer_score.duplicated_message(&peer_id_c, &id, &msg.topic); + } + + peer_score.refresh_scores(); + let score_a = peer_score.score(&peer_id_a); + let score_b = peer_score.score(&peer_id_b); + let score_c = peer_score.score(&peer_id_c); + + assert!( + score_a >= 0.0, + "expected non-negative score for Peer A, got score {score_a}" + ); + assert!( + score_b >= 0.0, + "expected non-negative score for Peer B, got score {score_b}" + ); + + // the penalty is the difference between the threshold and the actual mesh deliveries, squared. + // since we didn't deliver anything, this is just the value of the threshold + let penalty = topic_params.mesh_message_deliveries_threshold + * topic_params.mesh_message_deliveries_threshold; + let expected = + topic_params.topic_weight * topic_params.mesh_message_deliveries_weight * penalty; + + assert!(score_c == expected, "Score: {score_c}. Expected {expected}"); +} + +#[test] +fn test_score_mesh_message_deliveries_decay() { + // Create parameters with reasonable default values + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let mut params = PeerScoreParams::default(); + + let topic_params = TopicScoreParams { + topic_weight: 1.0, + mesh_message_deliveries_weight: -1.0, + mesh_message_deliveries_activation: Duration::from_secs(0), + mesh_message_deliveries_window: Duration::from_millis(10), + mesh_message_deliveries_threshold: 20.0, + mesh_message_deliveries_cap: 100.0, + mesh_message_deliveries_decay: 0.9, + first_message_deliveries_weight: 0.0, + time_in_mesh_weight: 0.0, + time_in_mesh_quantum: Duration::from_secs(1), + mesh_failure_penalty_weight: 0.0, + ..Default::default() + }; + + params.topics.insert(topic_hash, topic_params.clone()); + let mut peer_score = PeerScore::new(params); + + let peer_id_a = PeerId::random(); + peer_score.add_peer(peer_id_a); + peer_score.graft(&peer_id_a, topic); + + // deliver a bunch of messages from peer A + let messages = 100; + for seq in 0..messages { + let (id, msg) = make_test_message(seq); + peer_score.validate_message(&peer_id_a, &id, &msg.topic); + peer_score.deliver_message(&peer_id_a, &id, &msg.topic); + } + + // we should have a positive score, since we delivered more messages than the threshold + peer_score.refresh_scores(); + + let score_a = peer_score.score(&peer_id_a); + assert!( + score_a >= 0.0, + "expected non-negative score for Peer A, got score {score_a}" + ); + + let mut decayed_delivery_count = (messages as f64) * topic_params.mesh_message_deliveries_decay; + for _ in 0..20 { + peer_score.refresh_scores(); + decayed_delivery_count *= topic_params.mesh_message_deliveries_decay; + } + + let score_a = peer_score.score(&peer_id_a); + // the penalty is the difference between the threshold and the (decayed) mesh deliveries, squared. + let deficit = topic_params.mesh_message_deliveries_threshold - decayed_delivery_count; + let penalty = deficit * deficit; + let expected = + topic_params.topic_weight * topic_params.mesh_message_deliveries_weight * penalty; + + assert_eq!(score_a, expected, "Invalid score"); +} + +#[test] +fn test_score_mesh_failure_penalty() { + // Create parameters with reasonable default values + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let mut params = PeerScoreParams::default(); + + let topic_params = TopicScoreParams { + // the mesh failure penalty is applied when a peer is pruned while their + // mesh deliveries are under the threshold. + // for this test, we set the mesh delivery threshold, but set + // mesh_message_deliveries to zero, so the only affect on the score + // is from the mesh failure penalty + topic_weight: 1.0, + mesh_message_deliveries_weight: 0.0, + mesh_message_deliveries_activation: Duration::from_secs(0), + mesh_message_deliveries_window: Duration::from_millis(10), + mesh_message_deliveries_threshold: 20.0, + mesh_message_deliveries_cap: 100.0, + mesh_message_deliveries_decay: 1.0, + first_message_deliveries_weight: 0.0, + time_in_mesh_weight: 0.0, + time_in_mesh_quantum: Duration::from_secs(1), + mesh_failure_penalty_weight: -1.0, + mesh_failure_penalty_decay: 1.0, + ..Default::default() + }; + + params.topics.insert(topic_hash, topic_params.clone()); + let mut peer_score = PeerScore::new(params); + + let peer_id_a = PeerId::random(); + let peer_id_b = PeerId::random(); + + let peers = vec![peer_id_a, peer_id_b]; + + for peer_id in &peers { + peer_score.add_peer(*peer_id); + peer_score.graft(peer_id, topic.clone()); + } + + // deliver a bunch of messages from peer A + let messages = 100; + for seq in 0..messages { + let (id, msg) = make_test_message(seq); + + peer_score.validate_message(&peer_id_a, &id, &msg.topic); + peer_score.deliver_message(&peer_id_a, &id, &msg.topic); + } + + // peers A and B should both have zero scores, since the failure penalty hasn't been applied yet + peer_score.refresh_scores(); + let score_a = peer_score.score(&peer_id_a); + let score_b = peer_score.score(&peer_id_b); + assert!( + score_a >= 0.0, + "expected non-negative score for Peer A, got score {score_a}" + ); + assert!( + score_b >= 0.0, + "expected non-negative score for Peer B, got score {score_b}" + ); + + // prune peer B to apply the penalty + peer_score.prune(&peer_id_b, topic.hash()); + peer_score.refresh_scores(); + let score_a = peer_score.score(&peer_id_a); + + assert_eq!(score_a, 0.0, "expected Peer A to have a 0"); + + // penalty calculation is the same as for mesh_message_deliveries, but multiplied by + // mesh_failure_penalty_weigh + // instead of mesh_message_deliveries_weight + let penalty = topic_params.mesh_message_deliveries_threshold + * topic_params.mesh_message_deliveries_threshold; + let expected = topic_params.topic_weight * topic_params.mesh_failure_penalty_weight * penalty; + + let score_b = peer_score.score(&peer_id_b); + + assert_eq!(score_b, expected, "Peer B should have expected score",); +} + +#[test] +fn test_score_invalid_message_deliveries() { + // Create parameters with reasonable default values + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let mut params = PeerScoreParams::default(); + + let topic_params = TopicScoreParams { + topic_weight: 1.0, + mesh_message_deliveries_weight: 0.0, + mesh_message_deliveries_activation: Duration::from_secs(1), + mesh_message_deliveries_window: Duration::from_millis(10), + mesh_message_deliveries_threshold: 20.0, + mesh_message_deliveries_cap: 100.0, + mesh_message_deliveries_decay: 1.0, + first_message_deliveries_weight: 0.0, + time_in_mesh_weight: 0.0, + mesh_failure_penalty_weight: 0.0, + invalid_message_deliveries_weight: -1.0, + invalid_message_deliveries_decay: 1.0, + ..Default::default() + }; + + params.topics.insert(topic_hash, topic_params.clone()); + let mut peer_score = PeerScore::new(params); + + let peer_id_a = PeerId::random(); + peer_score.add_peer(peer_id_a); + peer_score.graft(&peer_id_a, topic); + + // reject a bunch of messages from peer A + let messages = 100; + for seq in 0..messages { + let (id, msg) = make_test_message(seq); + peer_score.reject_message(&peer_id_a, &id, &msg.topic, RejectReason::ValidationFailed); + } + + peer_score.refresh_scores(); + let score_a = peer_score.score(&peer_id_a); + + let expected = topic_params.topic_weight + * topic_params.invalid_message_deliveries_weight + * (messages * messages) as f64; + + assert_eq!(score_a, expected, "Peer has unexpected score",); +} + +#[test] +fn test_score_invalid_message_deliveris_decay() { + // Create parameters with reasonable default values + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let mut params = PeerScoreParams::default(); + + let topic_params = TopicScoreParams { + topic_weight: 1.0, + mesh_message_deliveries_weight: 0.0, + mesh_message_deliveries_activation: Duration::from_secs(1), + mesh_message_deliveries_window: Duration::from_millis(10), + mesh_message_deliveries_threshold: 20.0, + mesh_message_deliveries_cap: 100.0, + mesh_message_deliveries_decay: 1.0, + first_message_deliveries_weight: 0.0, + time_in_mesh_weight: 0.0, + mesh_failure_penalty_weight: 0.0, + invalid_message_deliveries_weight: -1.0, + invalid_message_deliveries_decay: 0.9, + ..Default::default() + }; + + params.topics.insert(topic_hash, topic_params.clone()); + let mut peer_score = PeerScore::new(params); + + let peer_id_a = PeerId::random(); + peer_score.add_peer(peer_id_a); + peer_score.graft(&peer_id_a, topic); + + // reject a bunch of messages from peer A + let messages = 100; + for seq in 0..messages { + let (id, msg) = make_test_message(seq); + peer_score.reject_message(&peer_id_a, &id, &msg.topic, RejectReason::ValidationFailed); + } + + peer_score.refresh_scores(); + + let decay = topic_params.invalid_message_deliveries_decay * messages as f64; + + let mut expected = + topic_params.topic_weight * topic_params.invalid_message_deliveries_weight * decay * decay; + + let score_a = peer_score.score(&peer_id_a); + assert_eq!(score_a, expected, "Peer has unexpected score"); + + // refresh scores a few times to apply decay + for _ in 0..10 { + peer_score.refresh_scores(); + expected *= topic_params.invalid_message_deliveries_decay + * topic_params.invalid_message_deliveries_decay; + } + + let score_a = peer_score.score(&peer_id_a); + assert_eq!(score_a, expected, "Peer has unexpected score"); +} + +#[test] +fn test_score_reject_message_deliveries() { + // This tests adds coverage for the dark corners of rejection tracing + + // Create parameters with reasonable default values + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let mut params = PeerScoreParams::default(); + + let topic_params = TopicScoreParams { + topic_weight: 1.0, + mesh_message_deliveries_weight: 0.0, + first_message_deliveries_weight: 0.0, + mesh_failure_penalty_weight: 0.0, + time_in_mesh_weight: 0.0, + time_in_mesh_quantum: Duration::from_secs(1), + invalid_message_deliveries_weight: -1.0, + invalid_message_deliveries_decay: 1.0, + ..Default::default() + }; + + params.topics.insert(topic_hash, topic_params); + let mut peer_score = PeerScore::new(params); + + let peer_id_a = PeerId::random(); + let peer_id_b = PeerId::random(); + + let peers = vec![peer_id_a, peer_id_b]; + + for peer_id in &peers { + peer_score.add_peer(*peer_id); + } + + let (id, msg) = make_test_message(1); + + // these should have no effect in the score + peer_score.reject_message(&peer_id_a, &id, &msg.topic, RejectReason::BlackListedPeer); + peer_score.reject_message(&peer_id_a, &id, &msg.topic, RejectReason::BlackListedSource); + peer_score.reject_message(&peer_id_a, &id, &msg.topic, RejectReason::ValidationIgnored); + + peer_score.refresh_scores(); + let score_a = peer_score.score(&peer_id_a); + let score_b = peer_score.score(&peer_id_b); + + assert_eq!(score_a, 0.0, "Should have no effect on the score"); + assert_eq!(score_b, 0.0, "Should have no effect on the score"); + + // insert a record in the message deliveries + peer_score.validate_message(&peer_id_a, &id, &msg.topic); + + // this should have no effect in the score, and subsequent duplicate messages should have no + // effect either + peer_score.reject_message(&peer_id_a, &id, &msg.topic, RejectReason::ValidationIgnored); + peer_score.duplicated_message(&peer_id_b, &id, &msg.topic); + + peer_score.refresh_scores(); + let score_a = peer_score.score(&peer_id_a); + let score_b = peer_score.score(&peer_id_b); + + assert_eq!(score_a, 0.0, "Should have no effect on the score"); + assert_eq!(score_b, 0.0, "Should have no effect on the score"); + + // now clear the delivery record + peer_score.deliveries.clear(); + + // insert a record in the message deliveries + peer_score.validate_message(&peer_id_a, &id, &msg.topic); + + // this should have no effect in the score, and subsequent duplicate messages should have no + // effect either + peer_score.reject_message(&peer_id_a, &id, &msg.topic, RejectReason::ValidationIgnored); + peer_score.duplicated_message(&peer_id_b, &id, &msg.topic); + + peer_score.refresh_scores(); + let score_a = peer_score.score(&peer_id_a); + let score_b = peer_score.score(&peer_id_b); + + assert_eq!(score_a, 0.0, "Should have no effect on the score"); + assert_eq!(score_b, 0.0, "Should have no effect on the score"); + + // now clear the delivery record + peer_score.deliveries.clear(); + + // insert a new record in the message deliveries + peer_score.validate_message(&peer_id_a, &id, &msg.topic); + + // and reject the message to make sure duplicates are also penalized + peer_score.reject_message(&peer_id_a, &id, &msg.topic, RejectReason::ValidationFailed); + peer_score.duplicated_message(&peer_id_b, &id, &msg.topic); + + peer_score.refresh_scores(); + let score_a = peer_score.score(&peer_id_a); + let score_b = peer_score.score(&peer_id_b); + + assert_eq!(score_a, -1.0, "Score should be effected"); + assert_eq!(score_b, -1.0, "Score should be effected"); + + // now clear the delivery record again + peer_score.deliveries.clear(); + + // insert a new record in the message deliveries + peer_score.validate_message(&peer_id_a, &id, &msg.topic); + + // and reject the message after a duplicate has arrived + peer_score.duplicated_message(&peer_id_b, &id, &msg.topic); + peer_score.reject_message(&peer_id_a, &id, &msg.topic, RejectReason::ValidationFailed); + + peer_score.refresh_scores(); + let score_a = peer_score.score(&peer_id_a); + let score_b = peer_score.score(&peer_id_b); + + assert_eq!(score_a, -4.0, "Score should be effected"); + assert_eq!(score_b, -4.0, "Score should be effected"); +} + +#[test] +fn test_application_score() { + // Create parameters with reasonable default values + let app_specific_weight = 0.5; + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let mut params = PeerScoreParams { + app_specific_weight, + ..Default::default() + }; + + let topic_params = TopicScoreParams { + topic_weight: 1.0, + mesh_message_deliveries_weight: 0.0, + first_message_deliveries_weight: 0.0, + mesh_failure_penalty_weight: 0.0, + time_in_mesh_weight: 0.0, + time_in_mesh_quantum: Duration::from_secs(1), + invalid_message_deliveries_weight: 0.0, + invalid_message_deliveries_decay: 1.0, + ..Default::default() + }; + + params.topics.insert(topic_hash, topic_params); + let mut peer_score = PeerScore::new(params); + + let peer_id_a = PeerId::random(); + peer_score.add_peer(peer_id_a); + peer_score.graft(&peer_id_a, topic); + + let messages = 100; + for i in -100..messages { + let app_score_value = i as f64; + peer_score.set_application_score(&peer_id_a, app_score_value); + peer_score.refresh_scores(); + let score_a = peer_score.score(&peer_id_a); + let expected = (i as f64) * app_specific_weight; + assert_eq!(score_a, expected, "Peer has unexpected score"); + } +} + +#[test] +fn test_score_ip_colocation() { + // Create parameters with reasonable default values + let ip_colocation_factor_weight = -1.0; + let ip_colocation_factor_threshold = 1.0; + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let mut params = PeerScoreParams { + ip_colocation_factor_weight, + ip_colocation_factor_threshold, + ..Default::default() + }; + + let topic_params = TopicScoreParams { + topic_weight: 1.0, + mesh_message_deliveries_weight: 0.0, + first_message_deliveries_weight: 0.0, + mesh_failure_penalty_weight: 0.0, + time_in_mesh_weight: 0.0, + time_in_mesh_quantum: Duration::from_secs(1), + invalid_message_deliveries_weight: 0.0, + ..Default::default() + }; + + params.topics.insert(topic_hash, topic_params); + let mut peer_score = PeerScore::new(params); + + let peer_id_a = PeerId::random(); + let peer_id_b = PeerId::random(); + let peer_id_c = PeerId::random(); + let peer_id_d = PeerId::random(); + + let peers = vec![peer_id_a, peer_id_b, peer_id_c, peer_id_d]; + for peer_id in &peers { + peer_score.add_peer(*peer_id); + peer_score.graft(peer_id, topic.clone()); + } + + // peerA should have no penalty, but B, C, and D should be penalized for sharing an IP + peer_score.add_ip(&peer_id_a, "1.2.3.4".parse().unwrap()); + peer_score.add_ip(&peer_id_b, "2.3.4.5".parse().unwrap()); + peer_score.add_ip(&peer_id_c, "2.3.4.5".parse().unwrap()); + peer_score.add_ip(&peer_id_c, "3.4.5.6".parse().unwrap()); + peer_score.add_ip(&peer_id_d, "2.3.4.5".parse().unwrap()); + + peer_score.refresh_scores(); + let score_a = peer_score.score(&peer_id_a); + let score_b = peer_score.score(&peer_id_b); + let score_c = peer_score.score(&peer_id_c); + let score_d = peer_score.score(&peer_id_d); + + assert_eq!(score_a, 0.0, "Peer A should be unaffected"); + + let n_shared = 3.0; + let ip_surplus = n_shared - ip_colocation_factor_threshold; + let penalty = ip_surplus * ip_surplus; + let expected = ip_colocation_factor_weight * penalty; + + assert_eq!(score_b, expected, "Peer B should have expected score"); + assert_eq!(score_c, expected, "Peer C should have expected score"); + assert_eq!(score_d, expected, "Peer D should have expected score"); +} + +#[test] +fn test_score_behaviour_penality() { + // Create parameters with reasonable default values + let behaviour_penalty_weight = -1.0; + let behaviour_penalty_decay = 0.99; + + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let mut params = PeerScoreParams { + behaviour_penalty_decay, + behaviour_penalty_weight, + ..Default::default() + }; + + let topic_params = TopicScoreParams { + topic_weight: 1.0, + mesh_message_deliveries_weight: 0.0, + first_message_deliveries_weight: 0.0, + mesh_failure_penalty_weight: 0.0, + time_in_mesh_weight: 0.0, + time_in_mesh_quantum: Duration::from_secs(1), + invalid_message_deliveries_weight: 0.0, + ..Default::default() + }; + + params.topics.insert(topic_hash, topic_params); + let mut peer_score = PeerScore::new(params); + + let peer_id_a = PeerId::random(); + + // add a penalty to a non-existent peer. + peer_score.add_penalty(&peer_id_a, 1); + + let score_a = peer_score.score(&peer_id_a); + assert_eq!(score_a, 0.0, "Peer A should be unaffected"); + + // add the peer and test penalties + peer_score.add_peer(peer_id_a); + assert_eq!(score_a, 0.0, "Peer A should be unaffected"); + + peer_score.add_penalty(&peer_id_a, 1); + + let score_a = peer_score.score(&peer_id_a); + assert_eq!(score_a, -1.0, "Peer A should have been penalized"); + + peer_score.add_penalty(&peer_id_a, 1); + let score_a = peer_score.score(&peer_id_a); + assert_eq!(score_a, -4.0, "Peer A should have been penalized"); + + peer_score.refresh_scores(); + + let score_a = peer_score.score(&peer_id_a); + assert_eq!(score_a, -3.9204, "Peer A should have been penalized"); +} + +#[test] +fn test_score_retention() { + // Create parameters with reasonable default values + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let app_specific_weight = 1.0; + let app_score_value = -1000.0; + let retain_score = Duration::from_secs(1); + let mut params = PeerScoreParams { + app_specific_weight, + retain_score, + ..Default::default() + }; + + let topic_params = TopicScoreParams { + topic_weight: 0.0, + mesh_message_deliveries_weight: 0.0, + mesh_message_deliveries_activation: Duration::from_secs(0), + first_message_deliveries_weight: 0.0, + time_in_mesh_weight: 0.0, + ..Default::default() + }; + + params.topics.insert(topic_hash, topic_params); + let mut peer_score = PeerScore::new(params); + + let peer_id_a = PeerId::random(); + peer_score.add_peer(peer_id_a); + peer_score.graft(&peer_id_a, topic); + + peer_score.set_application_score(&peer_id_a, app_score_value); + + // score should equal -1000 (app specific score) + peer_score.refresh_scores(); + let score_a = peer_score.score(&peer_id_a); + assert_eq!( + score_a, app_score_value, + "Score should be the application specific score" + ); + + // disconnect & wait half of RetainScore time. Should still have negative score + peer_score.remove_peer(&peer_id_a); + std::thread::sleep(retain_score / 2); + peer_score.refresh_scores(); + let score_a = peer_score.score(&peer_id_a); + assert_eq!( + score_a, app_score_value, + "Score should be the application specific score" + ); + + // wait remaining time (plus a little slop) and the score should reset to zero + std::thread::sleep(retain_score / 2 + Duration::from_millis(50)); + peer_score.refresh_scores(); + let score_a = peer_score.score(&peer_id_a); + assert_eq!( + score_a, 0.0, + "Score should be the application specific score" + ); +} diff --git a/beacon_node/lighthouse_network/gossipsub/src/protocol.rs b/beacon_node/lighthouse_network/gossipsub/src/protocol.rs new file mode 100644 index 0000000000..ba84ae0aa7 --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/protocol.rs @@ -0,0 +1,623 @@ +// Copyright 2020 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +use super::config::ValidationMode; +use super::handler::HandlerEvent; +use super::rpc_proto::proto; +use super::topic::TopicHash; +use super::types::{ + ControlAction, Graft, IHave, IWant, MessageId, PeerInfo, PeerKind, Prune, RawMessage, Rpc, + Subscription, SubscriptionAction, +}; +use super::ValidationError; +use asynchronous_codec::{Decoder, Encoder, Framed}; +use byteorder::{BigEndian, ByteOrder}; +use bytes::BytesMut; +use futures::prelude::*; +use libp2p::core::{InboundUpgrade, OutboundUpgrade, UpgradeInfo}; +use libp2p::identity::{PeerId, PublicKey}; +use libp2p::swarm::StreamProtocol; +use quick_protobuf::Writer; +use std::pin::Pin; +use void::Void; + +pub(crate) const SIGNING_PREFIX: &[u8] = b"libp2p-pubsub:"; + +pub(crate) const GOSSIPSUB_1_1_0_PROTOCOL: ProtocolId = ProtocolId { + protocol: StreamProtocol::new("/meshsub/1.1.0"), + kind: PeerKind::Gossipsubv1_1, +}; +pub(crate) const GOSSIPSUB_1_0_0_PROTOCOL: ProtocolId = ProtocolId { + protocol: StreamProtocol::new("/meshsub/1.0.0"), + kind: PeerKind::Gossipsub, +}; +pub(crate) const FLOODSUB_PROTOCOL: ProtocolId = ProtocolId { + protocol: StreamProtocol::new("/floodsub/1.0.0"), + kind: PeerKind::Floodsub, +}; + +/// Implementation of [`InboundUpgrade`] and [`OutboundUpgrade`] for the Gossipsub protocol. +#[derive(Debug, Clone)] +pub struct ProtocolConfig { + /// The Gossipsub protocol id to listen on. + pub(crate) protocol_ids: Vec, + /// The maximum transmit size for a packet. + pub(crate) max_transmit_size: usize, + /// Determines the level of validation to be done on incoming messages. + pub(crate) validation_mode: ValidationMode, +} + +impl Default for ProtocolConfig { + fn default() -> Self { + Self { + max_transmit_size: 65536, + validation_mode: ValidationMode::Strict, + protocol_ids: vec![GOSSIPSUB_1_1_0_PROTOCOL, GOSSIPSUB_1_0_0_PROTOCOL], + } + } +} + +/// The protocol ID +#[derive(Clone, Debug, PartialEq)] +pub struct ProtocolId { + /// The RPC message type/name. + pub protocol: StreamProtocol, + /// The type of protocol we support + pub kind: PeerKind, +} + +impl AsRef for ProtocolId { + fn as_ref(&self) -> &str { + self.protocol.as_ref() + } +} + +impl UpgradeInfo for ProtocolConfig { + type Info = ProtocolId; + type InfoIter = Vec; + + fn protocol_info(&self) -> Self::InfoIter { + self.protocol_ids.clone() + } +} + +impl InboundUpgrade for ProtocolConfig +where + TSocket: AsyncRead + AsyncWrite + Unpin + Send + 'static, +{ + type Output = (Framed, PeerKind); + type Error = Void; + type Future = Pin> + Send>>; + + fn upgrade_inbound(self, socket: TSocket, protocol_id: Self::Info) -> Self::Future { + Box::pin(future::ok(( + Framed::new( + socket, + GossipsubCodec::new(self.max_transmit_size, self.validation_mode), + ), + protocol_id.kind, + ))) + } +} + +impl OutboundUpgrade for ProtocolConfig +where + TSocket: AsyncWrite + AsyncRead + Unpin + Send + 'static, +{ + type Output = (Framed, PeerKind); + type Error = Void; + type Future = Pin> + Send>>; + + fn upgrade_outbound(self, socket: TSocket, protocol_id: Self::Info) -> Self::Future { + Box::pin(future::ok(( + Framed::new( + socket, + GossipsubCodec::new(self.max_transmit_size, self.validation_mode), + ), + protocol_id.kind, + ))) + } +} + +/* Gossip codec for the framing */ + +pub struct GossipsubCodec { + /// Determines the level of validation performed on incoming messages. + validation_mode: ValidationMode, + /// The codec to handle common encoding/decoding of protobuf messages + codec: quick_protobuf_codec::Codec, +} + +impl GossipsubCodec { + pub fn new(max_length: usize, validation_mode: ValidationMode) -> GossipsubCodec { + let codec = quick_protobuf_codec::Codec::new(max_length); + GossipsubCodec { + validation_mode, + codec, + } + } + + /// Verifies a gossipsub message. This returns either a success or failure. All errors + /// are logged, which prevents error handling in the codec and handler. We simply drop invalid + /// messages and log warnings, rather than propagating errors through the codec. + fn verify_signature(message: &proto::Message) -> bool { + use quick_protobuf::MessageWrite; + + let Some(from) = message.from.as_ref() else { + tracing::debug!("Signature verification failed: No source id given"); + return false; + }; + + let Ok(source) = PeerId::from_bytes(from) else { + tracing::debug!("Signature verification failed: Invalid Peer Id"); + return false; + }; + + let Some(signature) = message.signature.as_ref() else { + tracing::debug!("Signature verification failed: No signature provided"); + return false; + }; + + // If there is a key value in the protobuf, use that key otherwise the key must be + // obtained from the inlined source peer_id. + let public_key = match message.key.as_deref().map(PublicKey::try_decode_protobuf) { + Some(Ok(key)) => key, + _ => match PublicKey::try_decode_protobuf(&source.to_bytes()[2..]) { + Ok(v) => v, + Err(_) => { + tracing::warn!("Signature verification failed: No valid public key supplied"); + return false; + } + }, + }; + + // The key must match the peer_id + if source != public_key.to_peer_id() { + tracing::warn!( + "Signature verification failed: Public key doesn't match source peer id" + ); + return false; + } + + // Construct the signature bytes + let mut message_sig = message.clone(); + message_sig.signature = None; + message_sig.key = None; + let mut buf = Vec::with_capacity(message_sig.get_size()); + let mut writer = Writer::new(&mut buf); + message_sig + .write_message(&mut writer) + .expect("Encoding to succeed"); + let mut signature_bytes = SIGNING_PREFIX.to_vec(); + signature_bytes.extend_from_slice(&buf); + public_key.verify(&signature_bytes, signature) + } +} + +impl Encoder for GossipsubCodec { + type Item<'a> = proto::RPC; + type Error = quick_protobuf_codec::Error; + + fn encode(&mut self, item: Self::Item<'_>, dst: &mut BytesMut) -> Result<(), Self::Error> { + self.codec.encode(item, dst) + } +} + +impl Decoder for GossipsubCodec { + type Item = HandlerEvent; + type Error = quick_protobuf_codec::Error; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + let Some(rpc) = self.codec.decode(src)? else { + return Ok(None); + }; + // Store valid messages. + let mut messages = Vec::with_capacity(rpc.publish.len()); + // Store any invalid messages. + let mut invalid_messages = Vec::new(); + + for message in rpc.publish.into_iter() { + // Keep track of the type of invalid message. + let mut invalid_kind = None; + let mut verify_signature = false; + let mut verify_sequence_no = false; + let mut verify_source = false; + + match self.validation_mode { + ValidationMode::Strict => { + // Validate everything + verify_signature = true; + verify_sequence_no = true; + verify_source = true; + } + ValidationMode::Permissive => { + // If the fields exist, validate them + if message.signature.is_some() { + verify_signature = true; + } + if message.seqno.is_some() { + verify_sequence_no = true; + } + if message.from.is_some() { + verify_source = true; + } + } + ValidationMode::Anonymous => { + if message.signature.is_some() { + tracing::warn!( + "Signature field was non-empty and anonymous validation mode is set" + ); + invalid_kind = Some(ValidationError::SignaturePresent); + } else if message.seqno.is_some() { + tracing::warn!( + "Sequence number was non-empty and anonymous validation mode is set" + ); + invalid_kind = Some(ValidationError::SequenceNumberPresent); + } else if message.from.is_some() { + tracing::warn!("Message dropped. Message source was non-empty and anonymous validation mode is set"); + invalid_kind = Some(ValidationError::MessageSourcePresent); + } + } + ValidationMode::None => {} + } + + // If the initial validation logic failed, add the message to invalid messages and + // continue processing the others. + if let Some(validation_error) = invalid_kind.take() { + let message = RawMessage { + source: None, // don't bother inform the application + data: message.data.unwrap_or_default(), + sequence_number: None, // don't inform the application + topic: TopicHash::from_raw(message.topic), + signature: None, // don't inform the application + key: message.key, + validated: false, + }; + invalid_messages.push((message, validation_error)); + // proceed to the next message + continue; + } + + // verify message signatures if required + if verify_signature && !GossipsubCodec::verify_signature(&message) { + tracing::warn!("Invalid signature for received message"); + + // Build the invalid message (ignoring further validation of sequence number + // and source) + let message = RawMessage { + source: None, // don't bother inform the application + data: message.data.unwrap_or_default(), + sequence_number: None, // don't inform the application + topic: TopicHash::from_raw(message.topic), + signature: None, // don't inform the application + key: message.key, + validated: false, + }; + invalid_messages.push((message, ValidationError::InvalidSignature)); + // proceed to the next message + continue; + } + + // ensure the sequence number is a u64 + let sequence_number = if verify_sequence_no { + if let Some(seq_no) = message.seqno { + if seq_no.is_empty() { + None + } else if seq_no.len() != 8 { + tracing::debug!( + sequence_number=?seq_no, + sequence_length=%seq_no.len(), + "Invalid sequence number length for received message" + ); + let message = RawMessage { + source: None, // don't bother inform the application + data: message.data.unwrap_or_default(), + sequence_number: None, // don't inform the application + topic: TopicHash::from_raw(message.topic), + signature: message.signature, // don't inform the application + key: message.key, + validated: false, + }; + invalid_messages.push((message, ValidationError::InvalidSequenceNumber)); + // proceed to the next message + continue; + } else { + // valid sequence number + Some(BigEndian::read_u64(&seq_no)) + } + } else { + // sequence number was not present + tracing::debug!("Sequence number not present but expected"); + let message = RawMessage { + source: None, // don't bother inform the application + data: message.data.unwrap_or_default(), + sequence_number: None, // don't inform the application + topic: TopicHash::from_raw(message.topic), + signature: message.signature, // don't inform the application + key: message.key, + validated: false, + }; + invalid_messages.push((message, ValidationError::EmptySequenceNumber)); + continue; + } + } else { + // Do not verify the sequence number, consider it empty + None + }; + + // Verify the message source if required + let source = if verify_source { + if let Some(bytes) = message.from { + if !bytes.is_empty() { + match PeerId::from_bytes(&bytes) { + Ok(peer_id) => Some(peer_id), // valid peer id + Err(_) => { + // invalid peer id, add to invalid messages + tracing::debug!("Message source has an invalid PeerId"); + let message = RawMessage { + source: None, // don't bother inform the application + data: message.data.unwrap_or_default(), + sequence_number, + topic: TopicHash::from_raw(message.topic), + signature: message.signature, // don't inform the application + key: message.key, + validated: false, + }; + invalid_messages.push((message, ValidationError::InvalidPeerId)); + continue; + } + } + } else { + None + } + } else { + None + } + } else { + None + }; + + // This message has passed all validation, add it to the validated messages. + messages.push(RawMessage { + source, + data: message.data.unwrap_or_default(), + sequence_number, + topic: TopicHash::from_raw(message.topic), + signature: message.signature, + key: message.key, + validated: false, + }); + } + + let mut control_msgs = Vec::new(); + + if let Some(rpc_control) = rpc.control { + // Collect the gossipsub control messages + let ihave_msgs: Vec = rpc_control + .ihave + .into_iter() + .map(|ihave| { + ControlAction::IHave(IHave { + topic_hash: TopicHash::from_raw(ihave.topic_id.unwrap_or_default()), + message_ids: ihave + .message_ids + .into_iter() + .map(MessageId::from) + .collect::>(), + }) + }) + .collect(); + + let iwant_msgs: Vec = rpc_control + .iwant + .into_iter() + .map(|iwant| { + ControlAction::IWant(IWant { + message_ids: iwant + .message_ids + .into_iter() + .map(MessageId::from) + .collect::>(), + }) + }) + .collect(); + + let graft_msgs: Vec = rpc_control + .graft + .into_iter() + .map(|graft| { + ControlAction::Graft(Graft { + topic_hash: TopicHash::from_raw(graft.topic_id.unwrap_or_default()), + }) + }) + .collect(); + + let mut prune_msgs = Vec::new(); + + for prune in rpc_control.prune { + // filter out invalid peers + let peers = prune + .peers + .into_iter() + .filter_map(|info| { + info.peer_id + .as_ref() + .and_then(|id| PeerId::from_bytes(id).ok()) + .map(|peer_id| + //TODO signedPeerRecord, see https://github.com/libp2p/specs/pull/217 + PeerInfo { + peer_id: Some(peer_id), + }) + }) + .collect::>(); + + let topic_hash = TopicHash::from_raw(prune.topic_id.unwrap_or_default()); + prune_msgs.push(ControlAction::Prune(Prune { + topic_hash, + peers, + backoff: prune.backoff, + })); + } + + control_msgs.extend(ihave_msgs); + control_msgs.extend(iwant_msgs); + control_msgs.extend(graft_msgs); + control_msgs.extend(prune_msgs); + } + + Ok(Some(HandlerEvent::Message { + rpc: Rpc { + messages, + subscriptions: rpc + .subscriptions + .into_iter() + .map(|sub| Subscription { + action: if Some(true) == sub.subscribe { + SubscriptionAction::Subscribe + } else { + SubscriptionAction::Unsubscribe + }, + topic_hash: TopicHash::from_raw(sub.topic_id.unwrap_or_default()), + }) + .collect(), + control_msgs, + }, + invalid_messages, + })) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::Config; + use crate::{Behaviour, ConfigBuilder, MessageAuthenticity}; + use crate::{IdentTopic as Topic, Version}; + use libp2p::identity::Keypair; + use quickcheck::*; + + #[derive(Clone, Debug)] + struct Message(RawMessage); + + impl Arbitrary for Message { + fn arbitrary(g: &mut Gen) -> Self { + let keypair = TestKeypair::arbitrary(g); + + // generate an arbitrary GossipsubMessage using the behaviour signing functionality + let config = Config::default(); + let mut gs: Behaviour = + Behaviour::new(MessageAuthenticity::Signed(keypair.0), config).unwrap(); + let mut data_g = quickcheck::Gen::new(10024); + let data = (0..u8::arbitrary(&mut data_g)) + .map(|_| u8::arbitrary(g)) + .collect::>(); + let topic_id = TopicId::arbitrary(g).0; + Message(gs.build_raw_message(topic_id, data).unwrap()) + } + } + + #[derive(Clone, Debug)] + struct TopicId(TopicHash); + + impl Arbitrary for TopicId { + fn arbitrary(g: &mut Gen) -> Self { + let mut data_g = quickcheck::Gen::new(1024); + let topic_string: String = (0..u8::arbitrary(&mut data_g)) + .map(|_| char::arbitrary(g)) + .collect::(); + TopicId(Topic::new(topic_string).into()) + } + } + + #[derive(Clone)] + struct TestKeypair(Keypair); + + impl Arbitrary for TestKeypair { + #[cfg(feature = "rsa")] + fn arbitrary(g: &mut Gen) -> Self { + let keypair = if bool::arbitrary(g) { + // Small enough to be inlined. + Keypair::generate_ed25519() + } else { + // Too large to be inlined. + let mut rsa_key = hex::decode("308204bd020100300d06092a864886f70d0101010500048204a7308204a30201000282010100ef930f41a71288b643c1cbecbf5f72ab53992249e2b00835bf07390b6745419f3848cbcc5b030faa127bc88cdcda1c1d6f3ff699f0524c15ab9d2c9d8015f5d4bd09881069aad4e9f91b8b0d2964d215cdbbae83ddd31a7622a8228acee07079f6e501aea95508fa26c6122816ef7b00ac526d422bd12aed347c37fff6c1c307f3ba57bb28a7f28609e0bdcc839da4eedca39f5d2fa855ba4b0f9c763e9764937db929a1839054642175312a3de2d3405c9d27bdf6505ef471ce85c5e015eee85bf7874b3d512f715de58d0794fd8afe021c197fbd385bb88a930342fac8da31c27166e2edab00fa55dc1c3814448ba38363077f4e8fe2bdea1c081f85f1aa6f02030100010282010028ff427a1aac1a470e7b4879601a6656193d3857ea79f33db74df61e14730e92bf9ffd78200efb0c40937c3356cbe049cd32e5f15be5c96d5febcaa9bd3484d7fded76a25062d282a3856a1b3b7d2c525cdd8434beae147628e21adf241dd64198d5819f310d033743915ba40ea0b6acdbd0533022ad6daa1ff42de51885f9e8bab2306c6ef1181902d1cd7709006eba1ab0587842b724e0519f295c24f6d848907f772ae9a0953fc931f4af16a07df450fb8bfa94572562437056613647818c238a6ff3f606cffa0533e4b8755da33418dfbc64a85110b1a036623c947400a536bb8df65e5ebe46f2dfd0cfc86e7aeeddd7574c253e8fbf755562b3669525d902818100f9fff30c6677b78dd31ec7a634361438457e80be7a7faf390903067ea8355faa78a1204a82b6e99cb7d9058d23c1ecf6cfe4a900137a00cecc0113fd68c5931602980267ea9a95d182d48ba0a6b4d5dd32fdac685cb2e5d8b42509b2eb59c9579ea6a67ccc7547427e2bd1fb1f23b0ccb4dd6ba7d206c8dd93253d70a451701302818100f5530dfef678d73ce6a401ae47043af10a2e3f224c71ae933035ecd68ccbc4df52d72bc6ca2b17e8faf3e548b483a2506c0369ab80df3b137b54d53fac98f95547c2bc245b416e650ce617e0d29db36066f1335a9ba02ad3e0edf9dc3d58fd835835042663edebce81803972696c789012847cb1f854ab2ac0a1bd3867ac7fb502818029c53010d456105f2bf52a9a8482bca2224a5eac74bf3cc1a4d5d291fafcdffd15a6a6448cce8efdd661f6617ca5fc37c8c885cc3374e109ac6049bcbf72b37eabf44602a2da2d4a1237fd145c863e6d75059976de762d9d258c42b0984e2a2befa01c95217c3ee9c736ff209c355466ff99375194eff943bc402ea1d172a1ed02818027175bf493bbbfb8719c12b47d967bf9eac061c90a5b5711172e9095c38bb8cc493c063abffe4bea110b0a2f22ac9311b3947ba31b7ef6bfecf8209eebd6d86c316a2366bbafda7279b2b47d5bb24b6202254f249205dcad347b574433f6593733b806f84316276c1990a016ce1bbdbe5f650325acc7791aefe515ecc60063bd02818100b6a2077f4adcf15a17092d9c4a346d6022ac48f3861b73cf714f84c440a07419a7ce75a73b9cbff4597c53c128bf81e87b272d70428a272d99f90cd9b9ea1033298e108f919c6477400145a102df3fb5601ffc4588203cf710002517bfa24e6ad32f4d09c6b1a995fa28a3104131bedd9072f3b4fb4a5c2056232643d310453f").unwrap(); + Keypair::rsa_from_pkcs8(&mut rsa_key).unwrap() + }; + TestKeypair(keypair) + } + + #[cfg(not(feature = "rsa"))] + fn arbitrary(_g: &mut Gen) -> Self { + // Small enough to be inlined. + TestKeypair(Keypair::generate_ed25519()) + } + } + + impl std::fmt::Debug for TestKeypair { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TestKeypair") + .field("public", &self.0.public()) + .finish() + } + } + + #[test] + /// Test that RPC messages can be encoded and decoded successfully. + fn encode_decode() { + fn prop(message: Message) { + let message = message.0; + + let rpc = crate::types::Rpc { + messages: vec![message.clone()], + subscriptions: vec![], + control_msgs: vec![], + }; + + let mut codec = GossipsubCodec::new(u32::MAX as usize, ValidationMode::Strict); + let mut buf = BytesMut::new(); + codec.encode(rpc.into_protobuf(), &mut buf).unwrap(); + let decoded_rpc = codec.decode(&mut buf).unwrap().unwrap(); + // mark as validated as its a published message + match decoded_rpc { + HandlerEvent::Message { mut rpc, .. } => { + rpc.messages[0].validated = true; + + assert_eq!(vec![message], rpc.messages); + } + _ => panic!("Must decode a message"), + } + } + + QuickCheck::new().quickcheck(prop as fn(_) -> _) + } + + #[test] + fn support_floodsub_with_custom_protocol() { + let protocol_config = ConfigBuilder::default() + .protocol_id("/foosub", Version::V1_1) + .support_floodsub() + .build() + .unwrap() + .protocol_config(); + + assert_eq!(protocol_config.protocol_ids[0].protocol, "/foosub"); + assert_eq!(protocol_config.protocol_ids[1].protocol, "/floodsub/1.0.0"); + } +} diff --git a/beacon_node/lighthouse_network/gossipsub/src/rpc_proto.rs b/beacon_node/lighthouse_network/gossipsub/src/rpc_proto.rs new file mode 100644 index 0000000000..f653779ba2 --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/rpc_proto.rs @@ -0,0 +1,92 @@ +// Copyright 2020 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +pub(crate) mod proto { + #![allow(unreachable_pub)] + include!("generated/mod.rs"); + pub use self::gossipsub::pb::{mod_RPC::SubOpts, *}; +} + +#[cfg(test)] +mod test { + use crate::rpc_proto::proto::compat; + use crate::IdentTopic as Topic; + use libp2p::identity::PeerId; + use quick_protobuf::{BytesReader, MessageRead, MessageWrite, Writer}; + use rand::Rng; + + #[test] + fn test_multi_topic_message_compatibility() { + let topic1 = Topic::new("t1").hash(); + let topic2 = Topic::new("t2").hash(); + + let new_message1 = super::proto::Message { + from: Some(PeerId::random().to_bytes()), + data: Some(rand::thread_rng().gen::<[u8; 32]>().to_vec()), + seqno: Some(rand::thread_rng().gen::<[u8; 8]>().to_vec()), + topic: topic1.clone().into_string(), + signature: Some(rand::thread_rng().gen::<[u8; 32]>().to_vec()), + key: Some(rand::thread_rng().gen::<[u8; 32]>().to_vec()), + }; + let old_message1 = compat::pb::Message { + from: Some(PeerId::random().to_bytes()), + data: Some(rand::thread_rng().gen::<[u8; 32]>().to_vec()), + seqno: Some(rand::thread_rng().gen::<[u8; 8]>().to_vec()), + topic_ids: vec![topic1.clone().into_string()], + signature: Some(rand::thread_rng().gen::<[u8; 32]>().to_vec()), + key: Some(rand::thread_rng().gen::<[u8; 32]>().to_vec()), + }; + let old_message2 = compat::pb::Message { + from: Some(PeerId::random().to_bytes()), + data: Some(rand::thread_rng().gen::<[u8; 32]>().to_vec()), + seqno: Some(rand::thread_rng().gen::<[u8; 8]>().to_vec()), + topic_ids: vec![topic1.clone().into_string(), topic2.clone().into_string()], + signature: Some(rand::thread_rng().gen::<[u8; 32]>().to_vec()), + key: Some(rand::thread_rng().gen::<[u8; 32]>().to_vec()), + }; + + let mut new_message1b = Vec::with_capacity(new_message1.get_size()); + let mut writer = Writer::new(&mut new_message1b); + new_message1.write_message(&mut writer).unwrap(); + + let mut old_message1b = Vec::with_capacity(old_message1.get_size()); + let mut writer = Writer::new(&mut old_message1b); + old_message1.write_message(&mut writer).unwrap(); + + let mut old_message2b = Vec::with_capacity(old_message2.get_size()); + let mut writer = Writer::new(&mut old_message2b); + old_message2.write_message(&mut writer).unwrap(); + + let mut reader = BytesReader::from_bytes(&old_message1b[..]); + let new_message = + super::proto::Message::from_reader(&mut reader, &old_message1b[..]).unwrap(); + assert_eq!(new_message.topic, topic1.clone().into_string()); + + let mut reader = BytesReader::from_bytes(&old_message2b[..]); + let new_message = + super::proto::Message::from_reader(&mut reader, &old_message2b[..]).unwrap(); + assert_eq!(new_message.topic, topic2.into_string()); + + let mut reader = BytesReader::from_bytes(&new_message1b[..]); + let old_message = + compat::pb::Message::from_reader(&mut reader, &new_message1b[..]).unwrap(); + assert_eq!(old_message.topic_ids, vec![topic1.into_string()]); + } +} diff --git a/beacon_node/lighthouse_network/gossipsub/src/subscription_filter.rs b/beacon_node/lighthouse_network/gossipsub/src/subscription_filter.rs new file mode 100644 index 0000000000..02bb9b4eab --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/subscription_filter.rs @@ -0,0 +1,435 @@ +// Copyright 2020 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +use crate::types::Subscription; +use crate::TopicHash; +use std::collections::{BTreeSet, HashMap, HashSet}; + +pub trait TopicSubscriptionFilter { + /// Returns true iff the topic is of interest and we can subscribe to it. + fn can_subscribe(&mut self, topic_hash: &TopicHash) -> bool; + + /// Filters a list of incoming subscriptions and returns a filtered set + /// By default this deduplicates the subscriptions and calls + /// [`Self::filter_incoming_subscription_set`] on the filtered set. + fn filter_incoming_subscriptions<'a>( + &mut self, + subscriptions: &'a [Subscription], + currently_subscribed_topics: &BTreeSet, + ) -> Result, String> { + let mut filtered_subscriptions: HashMap = HashMap::new(); + for subscription in subscriptions { + use std::collections::hash_map::Entry::*; + match filtered_subscriptions.entry(subscription.topic_hash.clone()) { + Occupied(entry) => { + if entry.get().action != subscription.action { + entry.remove(); + } + } + Vacant(entry) => { + entry.insert(subscription); + } + } + } + self.filter_incoming_subscription_set( + filtered_subscriptions.into_values().collect(), + currently_subscribed_topics, + ) + } + + /// Filters a set of deduplicated subscriptions + /// By default this filters the elements based on [`Self::allow_incoming_subscription`]. + fn filter_incoming_subscription_set<'a>( + &mut self, + mut subscriptions: HashSet<&'a Subscription>, + _currently_subscribed_topics: &BTreeSet, + ) -> Result, String> { + subscriptions.retain(|s| { + if self.allow_incoming_subscription(s) { + true + } else { + tracing::debug!(subscription=?s, "Filtered incoming subscription"); + false + } + }); + Ok(subscriptions) + } + + /// Returns true iff we allow an incoming subscription. + /// This is used by the default implementation of filter_incoming_subscription_set to decide + /// whether to filter out a subscription or not. + /// By default this uses can_subscribe to decide the same for incoming subscriptions as for + /// outgoing ones. + fn allow_incoming_subscription(&mut self, subscription: &Subscription) -> bool { + self.can_subscribe(&subscription.topic_hash) + } +} + +//some useful implementers + +/// Allows all subscriptions +#[derive(Default, Clone)] +pub struct AllowAllSubscriptionFilter {} + +impl TopicSubscriptionFilter for AllowAllSubscriptionFilter { + fn can_subscribe(&mut self, _: &TopicHash) -> bool { + true + } +} + +/// Allows only whitelisted subscriptions +#[derive(Default, Clone)] +pub struct WhitelistSubscriptionFilter(pub HashSet); + +impl TopicSubscriptionFilter for WhitelistSubscriptionFilter { + fn can_subscribe(&mut self, topic_hash: &TopicHash) -> bool { + self.0.contains(topic_hash) + } +} + +/// Adds a max count to a given subscription filter +pub struct MaxCountSubscriptionFilter { + pub filter: T, + pub max_subscribed_topics: usize, + pub max_subscriptions_per_request: usize, +} + +impl TopicSubscriptionFilter for MaxCountSubscriptionFilter { + fn can_subscribe(&mut self, topic_hash: &TopicHash) -> bool { + self.filter.can_subscribe(topic_hash) + } + + fn filter_incoming_subscriptions<'a>( + &mut self, + subscriptions: &'a [Subscription], + currently_subscribed_topics: &BTreeSet, + ) -> Result, String> { + if subscriptions.len() > self.max_subscriptions_per_request { + return Err("too many subscriptions per request".into()); + } + let result = self + .filter + .filter_incoming_subscriptions(subscriptions, currently_subscribed_topics)?; + + use crate::types::SubscriptionAction::*; + + let mut unsubscribed = 0; + let mut new_subscribed = 0; + for s in &result { + let currently_contained = currently_subscribed_topics.contains(&s.topic_hash); + match s.action { + Unsubscribe => { + if currently_contained { + unsubscribed += 1; + } + } + Subscribe => { + if !currently_contained { + new_subscribed += 1; + } + } + } + } + + if new_subscribed + currently_subscribed_topics.len() + > self.max_subscribed_topics + unsubscribed + { + return Err("too many subscribed topics".into()); + } + + Ok(result) + } +} + +/// Combines two subscription filters +pub struct CombinedSubscriptionFilters { + pub filter1: T, + pub filter2: S, +} + +impl TopicSubscriptionFilter for CombinedSubscriptionFilters +where + T: TopicSubscriptionFilter, + S: TopicSubscriptionFilter, +{ + fn can_subscribe(&mut self, topic_hash: &TopicHash) -> bool { + self.filter1.can_subscribe(topic_hash) && self.filter2.can_subscribe(topic_hash) + } + + fn filter_incoming_subscription_set<'a>( + &mut self, + subscriptions: HashSet<&'a Subscription>, + currently_subscribed_topics: &BTreeSet, + ) -> Result, String> { + let intermediate = self + .filter1 + .filter_incoming_subscription_set(subscriptions, currently_subscribed_topics)?; + self.filter2 + .filter_incoming_subscription_set(intermediate, currently_subscribed_topics) + } +} + +pub struct CallbackSubscriptionFilter(pub T) +where + T: FnMut(&TopicHash) -> bool; + +impl TopicSubscriptionFilter for CallbackSubscriptionFilter +where + T: FnMut(&TopicHash) -> bool, +{ + fn can_subscribe(&mut self, topic_hash: &TopicHash) -> bool { + (self.0)(topic_hash) + } +} + +///A subscription filter that filters topics based on a regular expression. +pub struct RegexSubscriptionFilter(pub regex::Regex); + +impl TopicSubscriptionFilter for RegexSubscriptionFilter { + fn can_subscribe(&mut self, topic_hash: &TopicHash) -> bool { + self.0.is_match(topic_hash.as_str()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::types::SubscriptionAction::*; + + #[test] + fn test_filter_incoming_allow_all_with_duplicates() { + let mut filter = AllowAllSubscriptionFilter {}; + + let t1 = TopicHash::from_raw("t1"); + let t2 = TopicHash::from_raw("t2"); + + let old = BTreeSet::from_iter(vec![t1.clone()]); + let subscriptions = vec![ + Subscription { + action: Unsubscribe, + topic_hash: t1.clone(), + }, + Subscription { + action: Unsubscribe, + topic_hash: t2.clone(), + }, + Subscription { + action: Subscribe, + topic_hash: t2, + }, + Subscription { + action: Subscribe, + topic_hash: t1.clone(), + }, + Subscription { + action: Unsubscribe, + topic_hash: t1, + }, + ]; + + let result = filter + .filter_incoming_subscriptions(&subscriptions, &old) + .unwrap(); + assert_eq!(result, vec![&subscriptions[4]].into_iter().collect()); + } + + #[test] + fn test_filter_incoming_whitelist() { + let t1 = TopicHash::from_raw("t1"); + let t2 = TopicHash::from_raw("t2"); + + let mut filter = WhitelistSubscriptionFilter(HashSet::from_iter(vec![t1.clone()])); + + let old = Default::default(); + let subscriptions = vec![ + Subscription { + action: Subscribe, + topic_hash: t1, + }, + Subscription { + action: Subscribe, + topic_hash: t2, + }, + ]; + + let result = filter + .filter_incoming_subscriptions(&subscriptions, &old) + .unwrap(); + assert_eq!(result, vec![&subscriptions[0]].into_iter().collect()); + } + + #[test] + fn test_filter_incoming_too_many_subscriptions_per_request() { + let t1 = TopicHash::from_raw("t1"); + + let mut filter = MaxCountSubscriptionFilter { + filter: AllowAllSubscriptionFilter {}, + max_subscribed_topics: 100, + max_subscriptions_per_request: 2, + }; + + let old = Default::default(); + + let subscriptions = vec![ + Subscription { + action: Subscribe, + topic_hash: t1.clone(), + }, + Subscription { + action: Unsubscribe, + topic_hash: t1.clone(), + }, + Subscription { + action: Subscribe, + topic_hash: t1, + }, + ]; + + let result = filter.filter_incoming_subscriptions(&subscriptions, &old); + assert_eq!(result, Err("too many subscriptions per request".into())); + } + + #[test] + fn test_filter_incoming_too_many_subscriptions() { + let t: Vec<_> = (0..4) + .map(|i| TopicHash::from_raw(format!("t{i}"))) + .collect(); + + let mut filter = MaxCountSubscriptionFilter { + filter: AllowAllSubscriptionFilter {}, + max_subscribed_topics: 3, + max_subscriptions_per_request: 2, + }; + + let old = t[0..2].iter().cloned().collect(); + + let subscriptions = vec![ + Subscription { + action: Subscribe, + topic_hash: t[2].clone(), + }, + Subscription { + action: Subscribe, + topic_hash: t[3].clone(), + }, + ]; + + let result = filter.filter_incoming_subscriptions(&subscriptions, &old); + assert_eq!(result, Err("too many subscribed topics".into())); + } + + #[test] + fn test_filter_incoming_max_subscribed_valid() { + let t: Vec<_> = (0..5) + .map(|i| TopicHash::from_raw(format!("t{i}"))) + .collect(); + + let mut filter = MaxCountSubscriptionFilter { + filter: WhitelistSubscriptionFilter(t.iter().take(4).cloned().collect()), + max_subscribed_topics: 2, + max_subscriptions_per_request: 5, + }; + + let old = t[0..2].iter().cloned().collect(); + + let subscriptions = vec![ + Subscription { + action: Subscribe, + topic_hash: t[4].clone(), + }, + Subscription { + action: Subscribe, + topic_hash: t[2].clone(), + }, + Subscription { + action: Subscribe, + topic_hash: t[3].clone(), + }, + Subscription { + action: Unsubscribe, + topic_hash: t[0].clone(), + }, + Subscription { + action: Unsubscribe, + topic_hash: t[1].clone(), + }, + ]; + + let result = filter + .filter_incoming_subscriptions(&subscriptions, &old) + .unwrap(); + assert_eq!(result, subscriptions[1..].iter().collect()); + } + + #[test] + fn test_callback_filter() { + let t1 = TopicHash::from_raw("t1"); + let t2 = TopicHash::from_raw("t2"); + + let mut filter = CallbackSubscriptionFilter(|h| h.as_str() == "t1"); + + let old = Default::default(); + let subscriptions = vec![ + Subscription { + action: Subscribe, + topic_hash: t1, + }, + Subscription { + action: Subscribe, + topic_hash: t2, + }, + ]; + + let result = filter + .filter_incoming_subscriptions(&subscriptions, &old) + .unwrap(); + assert_eq!(result, vec![&subscriptions[0]].into_iter().collect()); + } + + #[test] + fn test_regex_subscription_filter() { + let t1 = TopicHash::from_raw("tt"); + let t2 = TopicHash::from_raw("et3t3te"); + let t3 = TopicHash::from_raw("abcdefghijklmnopqrsuvwxyz"); + + let mut filter = RegexSubscriptionFilter(regex::Regex::new("t.*t").unwrap()); + + let old = Default::default(); + let subscriptions = vec![ + Subscription { + action: Subscribe, + topic_hash: t1, + }, + Subscription { + action: Subscribe, + topic_hash: t2, + }, + Subscription { + action: Subscribe, + topic_hash: t3, + }, + ]; + + let result = filter + .filter_incoming_subscriptions(&subscriptions, &old) + .unwrap(); + assert_eq!(result, subscriptions[..2].iter().collect()); + } +} diff --git a/beacon_node/lighthouse_network/gossipsub/src/time_cache.rs b/beacon_node/lighthouse_network/gossipsub/src/time_cache.rs new file mode 100644 index 0000000000..89fd4afee0 --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/time_cache.rs @@ -0,0 +1,219 @@ +// Copyright 2020 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! This implements a time-based LRU cache for checking gossipsub message duplicates. + +use fnv::FnvHashMap; +use instant::Instant; +use std::collections::hash_map::{ + self, + Entry::{Occupied, Vacant}, +}; +use std::collections::VecDeque; +use std::time::Duration; + +struct ExpiringElement { + /// The element that expires + element: Element, + /// The expire time. + expires: Instant, +} + +pub(crate) struct TimeCache { + /// Mapping a key to its value together with its latest expire time (can be updated through + /// reinserts). + map: FnvHashMap>, + /// An ordered list of keys by expires time. + list: VecDeque>, + /// The time elements remain in the cache. + ttl: Duration, +} + +pub(crate) struct OccupiedEntry<'a, K, V> { + entry: hash_map::OccupiedEntry<'a, K, ExpiringElement>, +} + +impl<'a, K, V> OccupiedEntry<'a, K, V> +where + K: Eq + std::hash::Hash + Clone, +{ + pub(crate) fn into_mut(self) -> &'a mut V { + &mut self.entry.into_mut().element + } +} + +pub(crate) struct VacantEntry<'a, K, V> { + expiration: Instant, + entry: hash_map::VacantEntry<'a, K, ExpiringElement>, + list: &'a mut VecDeque>, +} + +impl<'a, K, V> VacantEntry<'a, K, V> +where + K: Eq + std::hash::Hash + Clone, +{ + pub(crate) fn insert(self, value: V) -> &'a mut V { + self.list.push_back(ExpiringElement { + element: self.entry.key().clone(), + expires: self.expiration, + }); + &mut self + .entry + .insert(ExpiringElement { + element: value, + expires: self.expiration, + }) + .element + } +} + +pub(crate) enum Entry<'a, K: 'a, V: 'a> { + Occupied(OccupiedEntry<'a, K, V>), + Vacant(VacantEntry<'a, K, V>), +} + +impl<'a, K: 'a, V: 'a> Entry<'a, K, V> +where + K: Eq + std::hash::Hash + Clone, +{ + pub(crate) fn or_default(self) -> &'a mut V + where + V: Default, + { + match self { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => entry.insert(V::default()), + } + } +} + +impl TimeCache +where + Key: Eq + std::hash::Hash + Clone, +{ + pub(crate) fn new(ttl: Duration) -> Self { + TimeCache { + map: FnvHashMap::default(), + list: VecDeque::new(), + ttl, + } + } + + fn remove_expired_keys(&mut self, now: Instant) { + while let Some(element) = self.list.pop_front() { + if element.expires > now { + self.list.push_front(element); + break; + } + if let Occupied(entry) = self.map.entry(element.element.clone()) { + if entry.get().expires <= now { + entry.remove(); + } + } + } + } + + pub(crate) fn entry(&mut self, key: Key) -> Entry { + let now = Instant::now(); + self.remove_expired_keys(now); + match self.map.entry(key) { + Occupied(entry) => Entry::Occupied(OccupiedEntry { entry }), + Vacant(entry) => Entry::Vacant(VacantEntry { + expiration: now + self.ttl, + entry, + list: &mut self.list, + }), + } + } + + /// Empties the entire cache. + #[cfg(test)] + pub(crate) fn clear(&mut self) { + self.map.clear(); + self.list.clear(); + } + + pub(crate) fn contains_key(&self, key: &Key) -> bool { + self.map.contains_key(key) + } +} + +pub(crate) struct DuplicateCache(TimeCache); + +impl DuplicateCache +where + Key: Eq + std::hash::Hash + Clone, +{ + pub(crate) fn new(ttl: Duration) -> Self { + Self(TimeCache::new(ttl)) + } + + // Inserts new elements and removes any expired elements. + // + // If the key was not present this returns `true`. If the value was already present this + // returns `false`. + pub(crate) fn insert(&mut self, key: Key) -> bool { + if let Entry::Vacant(entry) = self.0.entry(key) { + entry.insert(()); + true + } else { + false + } + } + + pub(crate) fn contains(&self, key: &Key) -> bool { + self.0.contains_key(key) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn cache_added_entries_exist() { + let mut cache = DuplicateCache::new(Duration::from_secs(10)); + + cache.insert("t"); + cache.insert("e"); + + // Should report that 't' and 't' already exists + assert!(!cache.insert("t")); + assert!(!cache.insert("e")); + } + + #[test] + fn cache_entries_expire() { + let mut cache = DuplicateCache::new(Duration::from_millis(100)); + + cache.insert("t"); + assert!(!cache.insert("t")); + cache.insert("e"); + //assert!(!cache.insert("t")); + assert!(!cache.insert("e")); + // sleep until cache expiry + std::thread::sleep(Duration::from_millis(101)); + // add another element to clear previous cache + cache.insert("s"); + + // should be removed from the cache + assert!(cache.insert("t")); + } +} diff --git a/beacon_node/lighthouse_network/gossipsub/src/topic.rs b/beacon_node/lighthouse_network/gossipsub/src/topic.rs new file mode 100644 index 0000000000..a73496b53f --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/topic.rs @@ -0,0 +1,123 @@ +// Copyright 2020 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +use crate::rpc_proto::proto; +use base64::prelude::*; +use prometheus_client::encoding::EncodeLabelSet; +use quick_protobuf::Writer; +use sha2::{Digest, Sha256}; +use std::fmt; + +/// A generic trait that can be extended for various hashing types for a topic. +pub trait Hasher { + /// The function that takes a topic string and creates a topic hash. + fn hash(topic_string: String) -> TopicHash; +} + +/// A type for representing topics who use the identity hash. +#[derive(Debug, Clone)] +pub struct IdentityHash {} +impl Hasher for IdentityHash { + /// Creates a [`TopicHash`] as a raw string. + fn hash(topic_string: String) -> TopicHash { + TopicHash { hash: topic_string } + } +} + +#[derive(Debug, Clone)] +pub struct Sha256Hash {} +impl Hasher for Sha256Hash { + /// Creates a [`TopicHash`] by SHA256 hashing the topic then base64 encoding the + /// hash. + fn hash(topic_string: String) -> TopicHash { + use quick_protobuf::MessageWrite; + + let topic_descripter = proto::TopicDescriptor { + name: Some(topic_string), + auth: None, + enc: None, + }; + let mut bytes = Vec::with_capacity(topic_descripter.get_size()); + let mut writer = Writer::new(&mut bytes); + topic_descripter + .write_message(&mut writer) + .expect("Encoding to succeed"); + let hash = BASE64_STANDARD.encode(Sha256::digest(&bytes)); + TopicHash { hash } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, EncodeLabelSet)] +pub struct TopicHash { + /// The topic hash. Stored as a string to align with the protobuf API. + hash: String, +} + +impl TopicHash { + pub fn from_raw(hash: impl Into) -> TopicHash { + TopicHash { hash: hash.into() } + } + + pub fn into_string(self) -> String { + self.hash + } + + pub fn as_str(&self) -> &str { + &self.hash + } +} + +/// A gossipsub topic. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Topic { + topic: String, + phantom_data: std::marker::PhantomData, +} + +impl From> for TopicHash { + fn from(topic: Topic) -> TopicHash { + topic.hash() + } +} + +impl Topic { + pub fn new(topic: impl Into) -> Self { + Topic { + topic: topic.into(), + phantom_data: std::marker::PhantomData, + } + } + + pub fn hash(&self) -> TopicHash { + H::hash(self.topic.clone()) + } +} + +impl fmt::Display for Topic { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.topic) + } +} + +impl fmt::Display for TopicHash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.hash) + } +} diff --git a/beacon_node/lighthouse_network/gossipsub/src/transform.rs b/beacon_node/lighthouse_network/gossipsub/src/transform.rs new file mode 100644 index 0000000000..6f57d9fc46 --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/transform.rs @@ -0,0 +1,72 @@ +// Copyright 2020 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! This trait allows of extended user-level decoding that can apply to message-data before a +//! message-id is calculated. +//! +//! This is primarily designed to allow applications to implement their own custom compression +//! algorithms that can be topic-specific. Once the raw data is transformed the message-id is then +//! calculated, allowing for applications to employ message-id functions post compression. + +use crate::{Message, RawMessage, TopicHash}; + +/// A general trait of transforming a [`RawMessage`] into a [`Message`]. The +/// [`RawMessage`] is obtained from the wire and the [`Message`] is used to +/// calculate the [`crate::MessageId`] of the message and is what is sent to the application. +/// +/// The inbound/outbound transforms must be inverses. Applying the inbound transform and then the +/// outbound transform MUST leave the underlying data un-modified. +/// +/// By default, this is the identity transform for all fields in [`Message`]. +pub trait DataTransform { + /// Takes a [`RawMessage`] received and converts it to a [`Message`]. + fn inbound_transform(&self, raw_message: RawMessage) -> Result; + + /// Takes the data to be published (a topic and associated data) transforms the data. The + /// transformed data will then be used to create a [`crate::RawMessage`] to be sent to peers. + fn outbound_transform( + &self, + topic: &TopicHash, + data: Vec, + ) -> Result, std::io::Error>; +} + +/// The default transform, the raw data is propagated as is to the application layer gossipsub. +#[derive(Default, Clone)] +pub struct IdentityTransform; + +impl DataTransform for IdentityTransform { + fn inbound_transform(&self, raw_message: RawMessage) -> Result { + Ok(Message { + source: raw_message.source, + data: raw_message.data, + sequence_number: raw_message.sequence_number, + topic: raw_message.topic, + }) + } + + fn outbound_transform( + &self, + _topic: &TopicHash, + data: Vec, + ) -> Result, std::io::Error> { + Ok(data) + } +} diff --git a/beacon_node/lighthouse_network/gossipsub/src/types.rs b/beacon_node/lighthouse_network/gossipsub/src/types.rs new file mode 100644 index 0000000000..712698b42a --- /dev/null +++ b/beacon_node/lighthouse_network/gossipsub/src/types.rs @@ -0,0 +1,818 @@ +// Copyright 2020 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! A collection of types using the Gossipsub system. +use crate::metrics::Metrics; +use crate::TopicHash; +use async_channel::{Receiver, Sender}; +use futures::stream::Peekable; +use futures::{Future, Stream, StreamExt}; +use futures_timer::Delay; +use instant::Duration; +use libp2p::identity::PeerId; +use libp2p::swarm::ConnectionId; +use prometheus_client::encoding::EncodeLabelValue; +use quick_protobuf::MessageWrite; +use std::collections::BTreeSet; +use std::fmt::Debug; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; +use std::task::{Context, Poll}; +use std::{fmt, pin::Pin}; + +use crate::rpc_proto::proto; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// The type of messages that have expired while attempting to send to a peer. +#[derive(Clone, Debug, Default)] +pub struct FailedMessages { + /// The number of publish messages that failed to be published in a heartbeat. + pub publish: usize, + /// The number of forward messages that failed to be published in a heartbeat. + pub forward: usize, + /// The number of messages that were failed to be sent to the priority queue as it was full. + pub priority: usize, + /// The number of messages that were failed to be sent to the non-priority queue as it was full. + pub non_priority: usize, +} + +impl FailedMessages { + /// The total number of messages that expired due a timeout. + pub fn total_timeout(&self) -> usize { + self.publish + self.forward + } + + /// The total number of messages that failed due to the queue being full. + pub fn total_queue_full(&self) -> usize { + self.priority + self.non_priority + } + + /// The total failed messages in a heartbeat. + pub fn total(&self) -> usize { + self.total_timeout() + self.total_queue_full() + } +} + +#[derive(Debug)] +/// Validation kinds from the application for received messages. +pub enum MessageAcceptance { + /// The message is considered valid, and it should be delivered and forwarded to the network. + Accept, + /// The message is considered invalid, and it should be rejected and trigger the Pâ‚„ penalty. + Reject, + /// The message is neither delivered nor forwarded to the network, but the router does not + /// trigger the Pâ‚„ penalty. + Ignore, +} + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct MessageId(pub Vec); + +impl MessageId { + pub fn new(value: &[u8]) -> Self { + Self(value.to_vec()) + } +} + +impl>> From for MessageId { + fn from(value: T) -> Self { + Self(value.into()) + } +} + +impl std::fmt::Display for MessageId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex_fmt::HexFmt(&self.0)) + } +} + +impl std::fmt::Debug for MessageId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "MessageId({})", hex_fmt::HexFmt(&self.0)) + } +} + +#[derive(Debug, Clone)] +pub(crate) struct PeerConnections { + /// The kind of protocol the peer supports. + pub(crate) kind: PeerKind, + /// Its current connections. + pub(crate) connections: Vec, + /// The rpc sender to the peer. + pub(crate) sender: RpcSender, + /// Subscribed topics. + pub(crate) topics: BTreeSet, +} + +/// Describes the types of peers that can exist in the gossipsub context. +#[derive(Debug, Clone, PartialEq, Hash, EncodeLabelValue, Eq)] +pub enum PeerKind { + /// A gossipsub 1.1 peer. + Gossipsubv1_1, + /// A gossipsub 1.0 peer. + Gossipsub, + /// A floodsub peer. + Floodsub, + /// The peer doesn't support any of the protocols. + NotSupported, +} + +/// A message received by the gossipsub system and stored locally in caches.. +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +pub struct RawMessage { + /// Id of the peer that published this message. + pub source: Option, + + /// Content of the message. Its meaning is out of scope of this library. + pub data: Vec, + + /// A random sequence number. + pub sequence_number: Option, + + /// The topic this message belongs to + pub topic: TopicHash, + + /// The signature of the message if it's signed. + pub signature: Option>, + + /// The public key of the message if it is signed and the source [`PeerId`] cannot be inlined. + pub key: Option>, + + /// Flag indicating if this message has been validated by the application or not. + pub validated: bool, +} + +impl RawMessage { + /// Calculates the encoded length of this message (used for calculating metrics). + pub fn raw_protobuf_len(&self) -> usize { + let message = proto::Message { + from: self.source.map(|m| m.to_bytes()), + data: Some(self.data.clone()), + seqno: self.sequence_number.map(|s| s.to_be_bytes().to_vec()), + topic: TopicHash::into_string(self.topic.clone()), + signature: self.signature.clone(), + key: self.key.clone(), + }; + message.get_size() + } +} + +impl From for proto::Message { + fn from(raw: RawMessage) -> Self { + proto::Message { + from: raw.source.map(|m| m.to_bytes()), + data: Some(raw.data), + seqno: raw.sequence_number.map(|s| s.to_be_bytes().to_vec()), + topic: TopicHash::into_string(raw.topic), + signature: raw.signature, + key: raw.key, + } + } +} + +/// The message sent to the user after a [`RawMessage`] has been transformed by a +/// [`crate::DataTransform`]. +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct Message { + /// Id of the peer that published this message. + pub source: Option, + + /// Content of the message. + pub data: Vec, + + /// A random sequence number. + pub sequence_number: Option, + + /// The topic this message belongs to + pub topic: TopicHash, +} + +impl fmt::Debug for Message { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Message") + .field( + "data", + &format_args!("{:<20}", &hex_fmt::HexFmt(&self.data)), + ) + .field("source", &self.source) + .field("sequence_number", &self.sequence_number) + .field("topic", &self.topic) + .finish() + } +} + +/// A subscription received by the gossipsub system. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Subscription { + /// Action to perform. + pub action: SubscriptionAction, + /// The topic from which to subscribe or unsubscribe. + pub topic_hash: TopicHash, +} + +/// Action that a subscription wants to perform. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum SubscriptionAction { + /// The remote wants to subscribe to the given topic. + Subscribe, + /// The remote wants to unsubscribe from the given topic. + Unsubscribe, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct PeerInfo { + pub(crate) peer_id: Option, + //TODO add this when RFC: Signed Address Records got added to the spec (see pull request + // https://github.com/libp2p/specs/pull/217) + //pub signed_peer_record: ?, +} + +/// A Control message received by the gossipsub system. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ControlAction { + /// Node broadcasts known messages per topic - IHave control message. + IHave(IHave), + /// The node requests specific message ids (peer_id + sequence _number) - IWant control message. + IWant(IWant), + /// The node has been added to the mesh - Graft control message. + Graft(Graft), + /// The node has been removed from the mesh - Prune control message. + Prune(Prune), +} + +/// Node broadcasts known messages per topic - IHave control message. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct IHave { + /// The topic of the messages. + pub(crate) topic_hash: TopicHash, + /// A list of known message ids (peer_id + sequence _number) as a string. + pub(crate) message_ids: Vec, +} + +/// The node requests specific message ids (peer_id + sequence _number) - IWant control message. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct IWant { + /// A list of known message ids (peer_id + sequence _number) as a string. + pub(crate) message_ids: Vec, +} + +/// The node has been added to the mesh - Graft control message. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Graft { + /// The mesh topic the peer should be added to. + pub(crate) topic_hash: TopicHash, +} + +/// The node has been removed from the mesh - Prune control message. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Prune { + /// The mesh topic the peer should be removed from. + pub(crate) topic_hash: TopicHash, + /// A list of peers to be proposed to the removed peer as peer exchange + pub(crate) peers: Vec, + /// The backoff time in seconds before we allow to reconnect + pub(crate) backoff: Option, +} + +/// A Gossipsub RPC message sent. +#[derive(Debug)] +pub enum RpcOut { + /// Publish a Gossipsub message on network. The [`Delay`] tags the time we attempted to + /// send it. + Publish { message: RawMessage, timeout: Delay }, + /// Forward a Gossipsub message to the network. The [`Delay`] tags the time we attempted to + /// send it. + Forward { message: RawMessage, timeout: Delay }, + /// Subscribe a topic. + Subscribe(TopicHash), + /// Unsubscribe a topic. + Unsubscribe(TopicHash), + /// Send a GRAFT control message. + Graft(Graft), + /// Send a PRUNE control message. + Prune(Prune), + /// Send a IHave control message. + IHave(IHave), + /// Send a IWant control message. + IWant(IWant), +} + +impl RpcOut { + /// Converts the GossipsubRPC into its protobuf format. + // A convenience function to avoid explicitly specifying types. + pub fn into_protobuf(self) -> proto::RPC { + self.into() + } +} + +impl From for proto::RPC { + /// Converts the RPC into protobuf format. + fn from(rpc: RpcOut) -> Self { + match rpc { + RpcOut::Publish { + message, + timeout: _, + } => proto::RPC { + subscriptions: Vec::new(), + publish: vec![message.into()], + control: None, + }, + RpcOut::Forward { + message, + timeout: _, + } => proto::RPC { + publish: vec![message.into()], + subscriptions: Vec::new(), + control: None, + }, + RpcOut::Subscribe(topic) => proto::RPC { + publish: Vec::new(), + subscriptions: vec![proto::SubOpts { + subscribe: Some(true), + topic_id: Some(topic.into_string()), + }], + control: None, + }, + RpcOut::Unsubscribe(topic) => proto::RPC { + publish: Vec::new(), + subscriptions: vec![proto::SubOpts { + subscribe: Some(false), + topic_id: Some(topic.into_string()), + }], + control: None, + }, + RpcOut::IHave(IHave { + topic_hash, + message_ids, + }) => proto::RPC { + publish: Vec::new(), + subscriptions: Vec::new(), + control: Some(proto::ControlMessage { + ihave: vec![proto::ControlIHave { + topic_id: Some(topic_hash.into_string()), + message_ids: message_ids.into_iter().map(|msg_id| msg_id.0).collect(), + }], + iwant: vec![], + graft: vec![], + prune: vec![], + }), + }, + RpcOut::IWant(IWant { message_ids }) => proto::RPC { + publish: Vec::new(), + subscriptions: Vec::new(), + control: Some(proto::ControlMessage { + ihave: vec![], + iwant: vec![proto::ControlIWant { + message_ids: message_ids.into_iter().map(|msg_id| msg_id.0).collect(), + }], + graft: vec![], + prune: vec![], + }), + }, + RpcOut::Graft(Graft { topic_hash }) => proto::RPC { + publish: Vec::new(), + subscriptions: vec![], + control: Some(proto::ControlMessage { + ihave: vec![], + iwant: vec![], + graft: vec![proto::ControlGraft { + topic_id: Some(topic_hash.into_string()), + }], + prune: vec![], + }), + }, + RpcOut::Prune(Prune { + topic_hash, + peers, + backoff, + }) => { + proto::RPC { + publish: Vec::new(), + subscriptions: vec![], + control: Some(proto::ControlMessage { + ihave: vec![], + iwant: vec![], + graft: vec![], + prune: vec![proto::ControlPrune { + topic_id: Some(topic_hash.into_string()), + peers: peers + .into_iter() + .map(|info| proto::PeerInfo { + peer_id: info.peer_id.map(|id| id.to_bytes()), + // TODO, see https://github.com/libp2p/specs/pull/217 + signed_peer_record: None, + }) + .collect(), + backoff, + }], + }), + } + } + } + } +} + +/// An RPC received/sent. +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct Rpc { + /// List of messages that were part of this RPC query. + pub messages: Vec, + /// List of subscriptions. + pub subscriptions: Vec, + /// List of Gossipsub control messages. + pub control_msgs: Vec, +} + +impl Rpc { + /// Converts the GossipsubRPC into its protobuf format. + // A convenience function to avoid explicitly specifying types. + pub fn into_protobuf(self) -> proto::RPC { + self.into() + } +} + +impl From for proto::RPC { + /// Converts the RPC into protobuf format. + fn from(rpc: Rpc) -> Self { + // Messages + let mut publish = Vec::new(); + + for message in rpc.messages.into_iter() { + let message = proto::Message { + from: message.source.map(|m| m.to_bytes()), + data: Some(message.data), + seqno: message.sequence_number.map(|s| s.to_be_bytes().to_vec()), + topic: TopicHash::into_string(message.topic), + signature: message.signature, + key: message.key, + }; + + publish.push(message); + } + + // subscriptions + let subscriptions = rpc + .subscriptions + .into_iter() + .map(|sub| proto::SubOpts { + subscribe: Some(sub.action == SubscriptionAction::Subscribe), + topic_id: Some(sub.topic_hash.into_string()), + }) + .collect::>(); + + // control messages + let mut control = proto::ControlMessage { + ihave: Vec::new(), + iwant: Vec::new(), + graft: Vec::new(), + prune: Vec::new(), + }; + + let empty_control_msg = rpc.control_msgs.is_empty(); + + for action in rpc.control_msgs { + match action { + // collect all ihave messages + ControlAction::IHave(IHave { + topic_hash, + message_ids, + }) => { + let rpc_ihave = proto::ControlIHave { + topic_id: Some(topic_hash.into_string()), + message_ids: message_ids.into_iter().map(|msg_id| msg_id.0).collect(), + }; + control.ihave.push(rpc_ihave); + } + ControlAction::IWant(IWant { message_ids }) => { + let rpc_iwant = proto::ControlIWant { + message_ids: message_ids.into_iter().map(|msg_id| msg_id.0).collect(), + }; + control.iwant.push(rpc_iwant); + } + ControlAction::Graft(Graft { topic_hash }) => { + let rpc_graft = proto::ControlGraft { + topic_id: Some(topic_hash.into_string()), + }; + control.graft.push(rpc_graft); + } + ControlAction::Prune(Prune { + topic_hash, + peers, + backoff, + }) => { + let rpc_prune = proto::ControlPrune { + topic_id: Some(topic_hash.into_string()), + peers: peers + .into_iter() + .map(|info| proto::PeerInfo { + peer_id: info.peer_id.map(|id| id.to_bytes()), + // TODO, see https://github.com/libp2p/specs/pull/217 + signed_peer_record: None, + }) + .collect(), + backoff, + }; + control.prune.push(rpc_prune); + } + } + } + + proto::RPC { + subscriptions, + publish, + control: if empty_control_msg { + None + } else { + Some(control) + }, + } + } +} + +impl fmt::Debug for Rpc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut b = f.debug_struct("GossipsubRpc"); + if !self.messages.is_empty() { + b.field("messages", &self.messages); + } + if !self.subscriptions.is_empty() { + b.field("subscriptions", &self.subscriptions); + } + if !self.control_msgs.is_empty() { + b.field("control_msgs", &self.control_msgs); + } + b.finish() + } +} + +impl PeerKind { + pub fn as_static_ref(&self) -> &'static str { + match self { + Self::NotSupported => "Not Supported", + Self::Floodsub => "Floodsub", + Self::Gossipsub => "Gossipsub v1.0", + Self::Gossipsubv1_1 => "Gossipsub v1.1", + } + } +} + +impl AsRef for PeerKind { + fn as_ref(&self) -> &str { + self.as_static_ref() + } +} + +impl fmt::Display for PeerKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_ref()) + } +} + +/// `RpcOut` sender that is priority aware. +#[derive(Debug, Clone)] +pub(crate) struct RpcSender { + cap: usize, + len: Arc, + pub(crate) priority_sender: Sender, + pub(crate) non_priority_sender: Sender, + priority_receiver: Receiver, + non_priority_receiver: Receiver, +} + +impl RpcSender { + /// Create a RpcSender. + pub(crate) fn new(cap: usize) -> RpcSender { + let (priority_sender, priority_receiver) = async_channel::unbounded(); + let (non_priority_sender, non_priority_receiver) = async_channel::bounded(cap / 2); + let len = Arc::new(AtomicUsize::new(0)); + RpcSender { + cap: cap / 2, + len, + priority_sender, + non_priority_sender, + priority_receiver, + non_priority_receiver, + } + } + + /// Create a new Receiver to the sender. + pub(crate) fn new_receiver(&self) -> RpcReceiver { + RpcReceiver { + priority_len: self.len.clone(), + priority: self.priority_receiver.clone().peekable(), + non_priority: self.non_priority_receiver.clone().peekable(), + } + } + + /// Send a `RpcOut::Graft` message to the `RpcReceiver` + /// this is high priority. + pub(crate) fn graft(&mut self, graft: Graft) { + self.priority_sender + .try_send(RpcOut::Graft(graft)) + .expect("Channel is unbounded and should always be open"); + } + + /// Send a `RpcOut::Prune` message to the `RpcReceiver` + /// this is high priority. + pub(crate) fn prune(&mut self, prune: Prune) { + self.priority_sender + .try_send(RpcOut::Prune(prune)) + .expect("Channel is unbounded and should always be open"); + } + + /// Send a `RpcOut::IHave` message to the `RpcReceiver` + /// this is low priority, if the queue is full an Err is returned. + #[allow(clippy::result_large_err)] + pub(crate) fn ihave(&mut self, ihave: IHave) -> Result<(), RpcOut> { + self.non_priority_sender + .try_send(RpcOut::IHave(ihave)) + .map_err(|err| err.into_inner()) + } + + /// Send a `RpcOut::IHave` message to the `RpcReceiver` + /// this is low priority, if the queue is full an Err is returned. + #[allow(clippy::result_large_err)] + pub(crate) fn iwant(&mut self, iwant: IWant) -> Result<(), RpcOut> { + self.non_priority_sender + .try_send(RpcOut::IWant(iwant)) + .map_err(|err| err.into_inner()) + } + + /// Send a `RpcOut::Subscribe` message to the `RpcReceiver` + /// this is high priority. + pub(crate) fn subscribe(&mut self, topic: TopicHash) { + self.priority_sender + .try_send(RpcOut::Subscribe(topic)) + .expect("Channel is unbounded and should always be open"); + } + + /// Send a `RpcOut::Unsubscribe` message to the `RpcReceiver` + /// this is high priority. + pub(crate) fn unsubscribe(&mut self, topic: TopicHash) { + self.priority_sender + .try_send(RpcOut::Unsubscribe(topic)) + .expect("Channel is unbounded and should always be open"); + } + + /// Send a `RpcOut::Publish` message to the `RpcReceiver` + /// this is high priority. If message sending fails, an `Err` is returned. + pub(crate) fn publish( + &mut self, + message: RawMessage, + timeout: Duration, + metrics: Option<&mut Metrics>, + ) -> Result<(), ()> { + if self.len.load(Ordering::Relaxed) >= self.cap { + return Err(()); + } + self.priority_sender + .try_send(RpcOut::Publish { + message: message.clone(), + timeout: Delay::new(timeout), + }) + .expect("Channel is unbounded and should always be open"); + self.len.fetch_add(1, Ordering::Relaxed); + + if let Some(m) = metrics { + m.msg_sent(&message.topic, message.raw_protobuf_len()); + } + + Ok(()) + } + + /// Send a `RpcOut::Forward` message to the `RpcReceiver` + /// this is high priority. If the queue is full the message is discarded. + pub(crate) fn forward( + &mut self, + message: RawMessage, + timeout: Duration, + metrics: Option<&mut Metrics>, + ) -> Result<(), ()> { + self.non_priority_sender + .try_send(RpcOut::Forward { + message: message.clone(), + timeout: Delay::new(timeout), + }) + .map_err(|_| ())?; + + if let Some(m) = metrics { + m.msg_sent(&message.topic, message.raw_protobuf_len()); + } + + Ok(()) + } + + /// Returns the current size of the priority queue. + pub(crate) fn priority_len(&self) -> usize { + self.len.load(Ordering::Relaxed) + } + + /// Returns the current size of the non-priority queue. + pub(crate) fn non_priority_len(&self) -> usize { + self.non_priority_sender.len() + } +} + +/// `RpcOut` sender that is priority aware. +#[derive(Debug)] +pub struct RpcReceiver { + /// The maximum length of the priority queue. + pub(crate) priority_len: Arc, + /// The priority queue receiver. + pub(crate) priority: Peekable>, + /// The non priority queue receiver. + pub(crate) non_priority: Peekable>, +} + +impl RpcReceiver { + // Peek the next message in the queues and return it if its timeout has elapsed. + // Returns `None` if there aren't any more messages on the stream or none is stale. + pub(crate) fn poll_stale(&mut self, cx: &mut Context<'_>) -> Poll> { + // Peek priority queue. + let priority = match Pin::new(&mut self.priority).poll_peek_mut(cx) { + Poll::Ready(Some(RpcOut::Publish { + message: _, + ref mut timeout, + })) => { + if Pin::new(timeout).poll(cx).is_ready() { + // Return the message. + let dropped = futures::ready!(self.priority.poll_next_unpin(cx)) + .expect("There should be a message"); + return Poll::Ready(Some(dropped)); + } + Poll::Ready(None) + } + poll => poll, + }; + + let non_priority = match Pin::new(&mut self.non_priority).poll_peek_mut(cx) { + Poll::Ready(Some(RpcOut::Forward { + message: _, + ref mut timeout, + })) => { + if Pin::new(timeout).poll(cx).is_ready() { + // Return the message. + let dropped = futures::ready!(self.non_priority.poll_next_unpin(cx)) + .expect("There should be a message"); + return Poll::Ready(Some(dropped)); + } + Poll::Ready(None) + } + poll => poll, + }; + + match (priority, non_priority) { + (Poll::Ready(None), Poll::Ready(None)) => Poll::Ready(None), + _ => Poll::Pending, + } + } + + /// Poll queues and return true if both are empty. + pub(crate) fn poll_is_empty(&mut self, cx: &mut Context<'_>) -> bool { + matches!( + ( + Pin::new(&mut self.priority).poll_peek(cx), + Pin::new(&mut self.non_priority).poll_peek(cx), + ), + (Poll::Ready(None), Poll::Ready(None)) + ) + } +} + +impl Stream for RpcReceiver { + type Item = RpcOut; + + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + // The priority queue is first polled. + if let Poll::Ready(rpc) = Pin::new(&mut self.priority).poll_next(cx) { + if let Some(RpcOut::Publish { .. }) = rpc { + self.priority_len.fetch_sub(1, Ordering::Relaxed); + } + return Poll::Ready(rpc); + } + // Then we poll the non priority. + Pin::new(&mut self.non_priority).poll_next(cx) + } +} diff --git a/beacon_node/lighthouse_network/src/config.rs b/beacon_node/lighthouse_network/src/config.rs index 169a061d20..03f530db4d 100644 --- a/beacon_node/lighthouse_network/src/config.rs +++ b/beacon_node/lighthouse_network/src/config.rs @@ -5,7 +5,6 @@ use crate::{Enr, PeerIdSerialized}; use directory::{ DEFAULT_BEACON_NODE_DIR, DEFAULT_HARDCODED_NETWORK, DEFAULT_NETWORK_DIR, DEFAULT_ROOT_DIR, }; -use libp2p::gossipsub; use libp2p::Multiaddr; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -21,20 +20,6 @@ pub const DEFAULT_TCP_PORT: u16 = 9000u16; pub const DEFAULT_DISC_PORT: u16 = 9000u16; pub const DEFAULT_QUIC_PORT: u16 = 9001u16; -/// 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: -/// -/// ```ignore -/// ATTESTATION_PROPAGATION_SLOT_RANGE = 32 -/// attestation.data.slot + ATTESTATION_PROPAGATION_SLOT_RANGE >= current_slot >= attestation.data.slot -/// ``` -/// -/// Therefore, we must accept attestations across a span of 33 slots (where each slot is 12 -/// seconds). We add an additional second to account for the 500ms gossip clock disparity, and -/// another 500ms for "fudge factor". -pub const DUPLICATE_CACHE_TIME: Duration = Duration::from_secs(33 * 12 + 1); - /// The maximum size of gossip messages. pub fn gossip_max_size(is_merge_enabled: bool, gossip_max_size: usize) -> usize { if is_merge_enabled { @@ -84,10 +69,6 @@ pub struct Config { /// Target number of connected peers. pub target_peers: usize, - /// Gossipsub configuration parameters. - #[serde(skip)] - pub gs_config: gossipsub::Config, - /// Discv5 configuration parameters. #[serde(skip)] pub discv5_config: discv5::Config, @@ -101,7 +82,7 @@ pub struct Config { /// List of libp2p nodes to initially connect to. pub libp2p_nodes: Vec, - /// List of trusted libp2p nodes which are not scored. + /// List of trusted libp2p nodes which are not scored and marked as explicit. pub trusted_peers: Vec, /// Disables peer scoring altogether. @@ -157,10 +138,6 @@ pub struct Config { /// Configuration for the inbound rate limiter (requests received by this node). pub inbound_rate_limiter_config: Option, - - /// Whether to disable logging duplicate gossip messages as WARN. If set to true, duplicate - /// errors will be logged at DEBUG level. - pub disable_duplicate_warn_logs: bool, } impl Config { @@ -297,12 +274,6 @@ impl Default for Config { .join(DEFAULT_BEACON_NODE_DIR) .join(DEFAULT_NETWORK_DIR); - // Note: Using the default config here. Use `gossipsub_config` function for getting - // Lighthouse specific configuration for gossipsub. - let gs_config = gossipsub::ConfigBuilder::default() - .build() - .expect("valid gossipsub configuration"); - // Discv5 Unsolicited Packet Rate Limiter let filter_rate_limiter = Some( discv5::RateLimiterBuilder::new() @@ -354,8 +325,7 @@ impl Default for Config { enr_udp6_port: None, enr_quic6_port: None, enr_tcp6_port: None, - target_peers: 50, - gs_config, + target_peers: 100, discv5_config, boot_nodes_enr: vec![], boot_nodes_multiaddr: vec![], @@ -378,7 +348,6 @@ impl Default for Config { outbound_rate_limiter_config: None, invalid_block_storage: None, inbound_rate_limiter_config: None, - disable_duplicate_warn_logs: false, } } } @@ -458,6 +427,8 @@ pub fn gossipsub_config( network_load: u8, fork_context: Arc, gossipsub_config_params: GossipsubConfigParams, + seconds_per_slot: u64, + slots_per_epoch: u64, ) -> gossipsub::Config { fn prefix( prefix: [u8; 4], @@ -466,7 +437,11 @@ pub fn gossipsub_config( ) -> Vec { let topic_bytes = message.topic.as_str().as_bytes(); match fork_context.current_fork() { - ForkName::Altair | ForkName::Merge | ForkName::Capella | ForkName::Deneb => { + ForkName::Altair + | ForkName::Merge + | ForkName::Capella + | ForkName::Deneb + | ForkName::Electra => { let topic_len_bytes = topic_bytes.len().to_le_bytes(); let mut vec = Vec::with_capacity( prefix.len() + topic_len_bytes.len() + topic_bytes.len() + message.data.len(), @@ -497,6 +472,13 @@ pub fn gossipsub_config( let load = NetworkLoad::from(network_load); + // Since EIP 7045 (activated at the deneb fork), we allow attestations that are + // 2 epochs old to be circulated around the p2p network. + // To accommodate the increase, we should increase the duplicate cache time to filter older seen messages. + // 2 epochs is quite sane for pre-deneb network parameters as well. + // Hence we keep the same parameters for pre-deneb networks as well to avoid switching at the fork. + let duplicate_cache_time = Duration::from_secs(slots_per_epoch * seconds_per_slot * 2); + gossipsub::ConfigBuilder::default() .max_transmit_size(gossip_max_size( is_merge_enabled, @@ -515,7 +497,7 @@ pub fn gossipsub_config( .history_gossip(load.history_gossip) .validate_messages() // require validation before propagation .validation_mode(gossipsub::ValidationMode::Anonymous) - .duplicate_cache_time(DUPLICATE_CACHE_TIME) + .duplicate_cache_time(duplicate_cache_time) .message_id_fn(gossip_message_id) .allow_self_origin(true) .build() diff --git a/beacon_node/lighthouse_network/src/discovery/enr.rs b/beacon_node/lighthouse_network/src/discovery/enr.rs index b0e0a01eec..51e50808e1 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr.rs +++ b/beacon_node/lighthouse_network/src/discovery/enr.rs @@ -28,38 +28,34 @@ pub const SYNC_COMMITTEE_BITFIELD_ENR_KEY: &str = "syncnets"; /// Extension trait for ENR's within Eth2. pub trait Eth2Enr { /// The attestation subnet bitfield associated with the ENR. - fn attestation_bitfield( - &self, - ) -> Result, &'static str>; + fn attestation_bitfield(&self) -> Result, &'static str>; /// The sync committee subnet bitfield associated with the ENR. - fn sync_committee_bitfield( + fn sync_committee_bitfield( &self, - ) -> Result, &'static str>; + ) -> Result, &'static str>; fn eth2(&self) -> Result; } impl Eth2Enr for Enr { - fn attestation_bitfield( - &self, - ) -> Result, &'static str> { + fn attestation_bitfield(&self) -> Result, &'static str> { let bitfield_bytes = self .get(ATTESTATION_BITFIELD_ENR_KEY) .ok_or("ENR attestation bitfield non-existent")?; - BitVector::::from_ssz_bytes(bitfield_bytes) + BitVector::::from_ssz_bytes(bitfield_bytes) .map_err(|_| "Could not decode the ENR attnets bitfield") } - fn sync_committee_bitfield( + fn sync_committee_bitfield( &self, - ) -> Result, &'static str> { + ) -> Result, &'static str> { let bitfield_bytes = self .get(SYNC_COMMITTEE_BITFIELD_ENR_KEY) .ok_or("ENR sync committee bitfield non-existent")?; - BitVector::::from_ssz_bytes(bitfield_bytes) + BitVector::::from_ssz_bytes(bitfield_bytes) .map_err(|_| "Could not decode the ENR syncnets bitfield") } @@ -125,7 +121,7 @@ pub fn use_or_load_enr( /// /// If an ENR exists, with the same NodeId, this function checks to see if the loaded ENR from /// disk is suitable to use, otherwise we increment our newly generated ENR's sequence number. -pub fn build_or_load_enr( +pub fn build_or_load_enr( local_key: Keypair, config: &NetworkConfig, enr_fork_id: &EnrForkId, @@ -135,14 +131,14 @@ pub fn build_or_load_enr( // Note: Discovery should update the ENR record's IP to the external IP as seen by the // majority of our peers, if the CLI doesn't expressly forbid it. let enr_key = CombinedKey::from_libp2p(local_key)?; - let mut local_enr = build_enr::(&enr_key, config, enr_fork_id)?; + let mut local_enr = build_enr::(&enr_key, config, enr_fork_id)?; use_or_load_enr(&enr_key, &mut local_enr, config, log)?; Ok(local_enr) } /// Builds a lighthouse ENR given a `NetworkConfig`. -pub fn build_enr( +pub fn build_enr( enr_key: &CombinedKey, config: &NetworkConfig, enr_fork_id: &EnrForkId, @@ -216,12 +212,12 @@ pub fn build_enr( builder.add_value(ETH2_ENR_KEY, &enr_fork_id.as_ssz_bytes()); // set the "attnets" field on our ENR - let bitfield = BitVector::::new(); + let bitfield = BitVector::::new(); builder.add_value(ATTESTATION_BITFIELD_ENR_KEY, &bitfield.as_ssz_bytes()); // set the "syncnets" field on our ENR - let bitfield = BitVector::::new(); + let bitfield = BitVector::::new(); builder.add_value(SYNC_COMMITTEE_BITFIELD_ENR_KEY, &bitfield.as_ssz_bytes()); diff --git a/beacon_node/lighthouse_network/src/discovery/mod.rs b/beacon_node/lighthouse_network/src/discovery/mod.rs index 829124e123..8cc2ea86c0 100644 --- a/beacon_node/lighthouse_network/src/discovery/mod.rs +++ b/beacon_node/lighthouse_network/src/discovery/mod.rs @@ -59,7 +59,7 @@ const MAX_DISCOVERY_RETRY: usize = 3; /// Note: we always allow a single FindPeers query, so we would be /// running a maximum of `MAX_CONCURRENT_SUBNET_QUERIES + 1` /// discovery queries at a time. -const MAX_CONCURRENT_SUBNET_QUERIES: usize = 2; +const MAX_CONCURRENT_SUBNET_QUERIES: usize = 4; /// The max number of subnets to search for in a single subnet discovery query. const MAX_SUBNETS_IN_QUERY: usize = 3; /// The number of closest peers to search for when doing a regular peer search. @@ -154,7 +154,7 @@ enum EventStream { /// The main discovery service. This can be disabled via CLI arguements. When disabled the /// underlying processes are not started, but this struct still maintains our current ENR. -pub struct Discovery { +pub struct Discovery { /// A collection of seen live ENRs for quick lookup and to map peer-id's to ENRs. cached_enrs: LruCache, @@ -168,7 +168,7 @@ pub struct Discovery { discv5: Discv5, /// A collection of network constants that can be read from other threads. - network_globals: Arc>, + network_globals: Arc>, /// Indicates if we are actively searching for peers. We only allow a single FindPeers query at /// a time, regardless of the query concurrency. @@ -194,12 +194,12 @@ pub struct Discovery { log: slog::Logger, } -impl Discovery { +impl Discovery { /// NOTE: Creating discovery requires running within a tokio execution environment. pub async fn new( local_key: Keypair, config: &NetworkConfig, - network_globals: Arc>, + network_globals: Arc>, log: &slog::Logger, ) -> error::Result { let log = log.clone(); @@ -448,7 +448,7 @@ impl Discovery { match subnet { Subnet::Attestation(id) => { let id = *id as usize; - let mut current_bitfield = local_enr.attestation_bitfield::()?; + let mut current_bitfield = local_enr.attestation_bitfield::()?; if id >= current_bitfield.len() { return Err(format!( "Subnet id: {} is outside the ENR bitfield length: {}", @@ -481,7 +481,7 @@ impl Discovery { } Subnet::SyncCommittee(id) => { let id = *id as usize; - let mut current_bitfield = local_enr.sync_committee_bitfield::()?; + let mut current_bitfield = local_enr.sync_committee_bitfield::()?; if id >= current_bitfield.len() { return Err(format!( @@ -718,7 +718,7 @@ impl Discovery { // Only start a discovery query if we have a subnet to look for. if !filtered_subnet_queries.is_empty() { // build the subnet predicate as a combination of the eth2_fork_predicate and the subnet predicate - let subnet_predicate = subnet_predicate::(filtered_subnets, &self.log); + let subnet_predicate = subnet_predicate::(filtered_subnets, &self.log); debug!( self.log, @@ -845,7 +845,7 @@ impl Discovery { // Check the specific subnet against the enr let subnet_predicate = - subnet_predicate::(vec![query.subnet], &self.log); + subnet_predicate::(vec![query.subnet], &self.log); r.clone() .into_iter() @@ -916,7 +916,7 @@ impl Discovery { /* NetworkBehaviour Implementation */ -impl NetworkBehaviour for Discovery { +impl NetworkBehaviour for Discovery { // Discovery is not a real NetworkBehaviour... type ConnectionHandler = ConnectionHandler; type ToSwarm = DiscoveredPeers; @@ -1004,7 +1004,10 @@ impl NetworkBehaviour for Discovery { discv5::Event::SocketUpdated(socket_addr) => { info!(self.log, "Address updated"; "ip" => %socket_addr.ip(), "udp_port" => %socket_addr.port()); metrics::inc_counter(&metrics::ADDRESS_UPDATE_COUNT); - metrics::check_nat(); + // We have SOCKET_UPDATED messages. This occurs when discovery has a majority of + // users reporting an external port and our ENR gets updated. + // Which means we are able to do NAT traversal. + metrics::set_gauge_vec(&metrics::NAT_OPEN, &["discv5"], 1); // Discv5 will have updated our local ENR. We save the updated version // to disk. @@ -1113,7 +1116,7 @@ impl NetworkBehaviour for Discovery { } } -impl Discovery { +impl Discovery { fn on_dial_failure(&mut self, peer_id: Option, error: &DialError) { if let Some(peer_id) = peer_id { match error { diff --git a/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs b/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs index f79ff8daf6..1aabf12b72 100644 --- a/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs +++ b/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs @@ -5,26 +5,23 @@ use slog::trace; use std::ops::Deref; /// Returns the predicate for a given subnet. -pub fn subnet_predicate( - subnets: Vec, - log: &slog::Logger, -) -> impl Fn(&Enr) -> bool + Send +pub fn subnet_predicate(subnets: Vec, log: &slog::Logger) -> impl Fn(&Enr) -> bool + Send where - TSpec: EthSpec, + E: EthSpec, { let log_clone = log.clone(); move |enr: &Enr| { - let attestation_bitfield: EnrAttestationBitfield = - match enr.attestation_bitfield::() { - Ok(b) => b, - Err(_e) => return false, - }; + let attestation_bitfield: EnrAttestationBitfield = match enr.attestation_bitfield::() + { + Ok(b) => b, + Err(_e) => return false, + }; // Pre-fork/fork-boundary enrs may not contain a syncnets field. // Don't return early here - let sync_committee_bitfield: Result, _> = - enr.sync_committee_bitfield::(); + let sync_committee_bitfield: Result, _> = + enr.sync_committee_bitfield::(); let predicate = subnets.iter().any(|subnet| match subnet { Subnet::Attestation(s) => attestation_bitfield diff --git a/beacon_node/lighthouse_network/src/lib.rs b/beacon_node/lighthouse_network/src/lib.rs index ea1ab07e3e..264795844a 100644 --- a/beacon_node/lighthouse_network/src/lib.rs +++ b/beacon_node/lighthouse_network/src/lib.rs @@ -114,8 +114,8 @@ pub use prometheus_client; pub use config::Config as NetworkConfig; pub use discovery::{CombinedKeyExt, EnrExt, Eth2Enr}; pub use discv5; +pub use gossipsub::{IdentTopic, MessageAcceptance, MessageId, Topic, TopicHash}; pub use libp2p; -pub use libp2p::gossipsub::{IdentTopic, MessageAcceptance, MessageId, Topic, TopicHash}; pub use libp2p::{core::ConnectedPoint, PeerId, Swarm}; pub use libp2p::{multiaddr, Multiaddr}; pub use metrics::scrape_discovery_metrics; diff --git a/beacon_node/lighthouse_network/src/metrics.rs b/beacon_node/lighthouse_network/src/metrics.rs index ae02b689d8..fc441f2533 100644 --- a/beacon_node/lighthouse_network/src/metrics.rs +++ b/beacon_node/lighthouse_network/src/metrics.rs @@ -1,9 +1,10 @@ pub use lighthouse_metrics::*; lazy_static! { - pub static ref NAT_OPEN: Result = try_create_int_counter( + pub static ref NAT_OPEN: Result = try_create_int_gauge_vec( "nat_open", - "An estimate indicating if the local node is exposed to the internet." + "An estimate indicating if the local node is reachable from external nodes", + &["protocol"] ); pub static ref ADDRESS_UPDATE_COUNT: Result = try_create_int_counter( "libp2p_address_update_total", @@ -14,6 +15,9 @@ lazy_static! { "Count of libp2p peers currently connected" ); + pub static ref PEERS_CONNECTED_MULTI: Result = + try_create_int_gauge_vec("libp2p_peers_multi", "Count of libp2p peers currently connected", &["direction", "transport"]); + pub static ref TCP_PEERS_CONNECTED: Result = try_create_int_gauge( "libp2p_tcp_peers", "Count of libp2p peers currently connected via TCP" @@ -32,13 +36,10 @@ lazy_static! { "libp2p_peer_disconnect_event_total", "Count of libp2p peer disconnect events" ); - pub static ref DISCOVERY_SENT_BYTES: Result = try_create_int_gauge( - "discovery_sent_bytes", - "The number of bytes sent in discovery" - ); - pub static ref DISCOVERY_RECV_BYTES: Result = try_create_int_gauge( - "discovery_recv_bytes", - "The number of bytes received in discovery" + pub static ref DISCOVERY_BYTES: Result = try_create_int_gauge_vec( + "discovery_bytes", + "The number of bytes sent and received in discovery", + &["direction"] ); pub static ref DISCOVERY_QUEUE: Result = try_create_int_gauge( "discovery_queue_size", @@ -135,17 +136,6 @@ lazy_static! { &["type"] ); - /* - * Inbound/Outbound peers - */ - /// The number of peers that dialed us. - pub static ref NETWORK_INBOUND_PEERS: Result = - try_create_int_gauge("network_inbound_peers","The number of peers that are currently connected that have dialed us."); - - /// The number of peers that we dialed us. - pub static ref NETWORK_OUTBOUND_PEERS: Result = - try_create_int_gauge("network_outbound_peers","The number of peers that are currently connected that we dialed."); - /* * Peer Reporting */ @@ -156,31 +146,11 @@ lazy_static! { ); } -/// Checks if we consider the NAT open. -/// -/// Conditions for an open NAT: -/// 1. We have 1 or more SOCKET_UPDATED messages. This occurs when discovery has a majority of -/// users reporting an external port and our ENR gets updated. -/// 2. We have 0 SOCKET_UPDATED messages (can be true if the port was correct on boot), then we -/// rely on whether we have any inbound messages. If we have no socket update messages, but -/// manage to get at least one inbound peer, we are exposed correctly. -pub fn check_nat() { - // NAT is already deemed open. - if NAT_OPEN.as_ref().map(|v| v.get()).unwrap_or(0) != 0 { - return; - } - if ADDRESS_UPDATE_COUNT.as_ref().map(|v| v.get()).unwrap_or(0) != 0 - || NETWORK_INBOUND_PEERS.as_ref().map(|v| v.get()).unwrap_or(0) != 0_i64 - { - inc_counter(&NAT_OPEN); - } -} - pub fn scrape_discovery_metrics() { let metrics = discv5::metrics::Metrics::from(discv5::Discv5::::raw_metrics()); set_float_gauge(&DISCOVERY_REQS, metrics.unsolicited_requests_per_second); set_gauge(&DISCOVERY_SESSIONS, metrics.active_sessions as i64); - set_gauge(&DISCOVERY_SENT_BYTES, metrics.bytes_sent as i64); - set_gauge(&DISCOVERY_RECV_BYTES, metrics.bytes_recv as i64); + set_gauge_vec(&DISCOVERY_BYTES, &["inbound"], metrics.bytes_recv as i64); + set_gauge_vec(&DISCOVERY_BYTES, &["outbound"], metrics.bytes_sent as i64); } diff --git a/beacon_node/lighthouse_network/src/peer_manager/config.rs b/beacon_node/lighthouse_network/src/peer_manager/config.rs index 6c5523de45..d2fc7a8abd 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/config.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/config.rs @@ -18,6 +18,8 @@ pub struct Config { pub discovery_enabled: bool, /// Whether metrics are enabled. pub metrics_enabled: bool, + /// Whether quic is enabled. + pub quic_enabled: bool, /// Target number of peers to connect to. pub target_peer_count: usize, @@ -37,6 +39,7 @@ impl Default for Config { Config { discovery_enabled: true, metrics_enabled: false, + quic_enabled: true, target_peer_count: DEFAULT_TARGET_PEERS, status_interval: DEFAULT_STATUS_INTERVAL, ping_interval_inbound: DEFAULT_PING_INTERVAL_INBOUND, diff --git a/beacon_node/lighthouse_network/src/peer_manager/mod.rs b/beacon_node/lighthouse_network/src/peer_manager/mod.rs index 4316c0d07e..0d9a7c60dd 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/mod.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/mod.rs @@ -10,7 +10,7 @@ use delay_map::HashSetDelay; use discv5::Enr; use libp2p::identify::Info as IdentifyInfo; use lru_cache::LRUTimeCache; -use peerdb::{client::ClientKind, BanOperation, BanResult, ScoreUpdateResult}; +use peerdb::{BanOperation, BanResult, ScoreUpdateResult}; use rand::seq::SliceRandom; use slog::{debug, error, trace, warn}; use smallvec::SmallVec; @@ -18,7 +18,6 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use strum::IntoEnumIterator; use types::{EthSpec, SyncSubnetId}; pub use libp2p::core::Multiaddr; @@ -27,6 +26,8 @@ pub use libp2p::identity::Keypair; #[allow(clippy::mutable_key_type)] // PeerId in hashmaps are no longer permitted by clippy pub mod peerdb; +use crate::peer_manager::peerdb::client::ClientKind; +use libp2p::multiaddr; pub use peerdb::peer_info::{ ConnectionDirection, PeerConnectionStatus, PeerConnectionStatus::*, PeerInfo, }; @@ -34,6 +35,8 @@ use peerdb::score::{PeerAction, ReportSource}; pub use peerdb::sync_status::{SyncInfo, SyncStatus}; use std::collections::{hash_map::Entry, HashMap}; use std::net::IpAddr; +use strum::IntoEnumIterator; + pub mod config; mod network_behaviour; @@ -64,9 +67,9 @@ pub const MIN_OUTBOUND_ONLY_FACTOR: f32 = 0.2; pub const PRIORITY_PEER_EXCESS: f32 = 0.2; /// The main struct that handles peer's reputation and connection status. -pub struct PeerManager { +pub struct PeerManager { /// Storage of network globals to access the `PeerDB`. - network_globals: Arc>, + network_globals: Arc>, /// A queue of events that the `PeerManager` is waiting to produce. events: SmallVec<[PeerManagerEvent; 16]>, /// A collection of inbound-connected peers awaiting to be Ping'd. @@ -104,6 +107,8 @@ pub struct PeerManager { discovery_enabled: bool, /// Keeps track if the current instance is reporting metrics or not. metrics_enabled: bool, + /// Keeps track of whether the QUIC protocol is enabled or not. + quic_enabled: bool, /// The logger associated with the `PeerManager`. log: slog::Logger, } @@ -135,11 +140,11 @@ pub enum PeerManagerEvent { DiscoverSubnetPeers(Vec), } -impl PeerManager { +impl PeerManager { // NOTE: Must be run inside a tokio executor. pub fn new( cfg: config::Config, - network_globals: Arc>, + network_globals: Arc>, log: &slog::Logger, ) -> error::Result { let config::Config { @@ -149,6 +154,7 @@ impl PeerManager { status_interval, ping_interval_inbound, ping_interval_outbound, + quic_enabled, } = cfg; // Set up the peer manager heartbeat interval @@ -167,6 +173,7 @@ impl PeerManager { heartbeat, discovery_enabled, metrics_enabled, + quic_enabled, log: log.clone(), }) } @@ -461,19 +468,6 @@ impl PeerManager { "observed_address" => ?info.observed_addr, "protocols" => ?info.protocols ); - - // update the peer client kind metric if the peer is connected - if matches!( - peer_info.connection_status(), - PeerConnectionStatus::Connected { .. } - | PeerConnectionStatus::Disconnecting { .. } - ) { - metrics::inc_gauge_vec( - &metrics::PEERS_PER_CLIENT, - &[peer_info.client().kind.as_ref()], - ); - metrics::dec_gauge_vec(&metrics::PEERS_PER_CLIENT, &[previous_kind.as_ref()]); - } } } else { error!(self.log, "Received an Identify response from an unknown peer"; "peer_id" => peer_id.to_string()); @@ -559,7 +553,11 @@ impl PeerManager { Protocol::BlocksByRange => PeerAction::MidToleranceError, Protocol::BlocksByRoot => PeerAction::MidToleranceError, Protocol::BlobsByRange => PeerAction::MidToleranceError, - Protocol::LightClientBootstrap => PeerAction::LowToleranceError, + // Lighthouse does not currently make light client requests; therefore, this + // is an unexpected scenario. We do not ban the peer for rate limiting. + Protocol::LightClientBootstrap => return, + Protocol::LightClientOptimisticUpdate => return, + Protocol::LightClientFinalityUpdate => return, Protocol::BlobsByRoot => PeerAction::MidToleranceError, Protocol::Goodbye => PeerAction::LowToleranceError, Protocol::MetaData => PeerAction::LowToleranceError, @@ -581,6 +579,8 @@ impl PeerManager { Protocol::BlobsByRoot => return, Protocol::Goodbye => return, Protocol::LightClientBootstrap => return, + Protocol::LightClientOptimisticUpdate => return, + Protocol::LightClientFinalityUpdate => return, Protocol::MetaData => PeerAction::Fatal, Protocol::Status => PeerAction::Fatal, } @@ -598,6 +598,8 @@ impl PeerManager { Protocol::BlobsByRange => PeerAction::MidToleranceError, Protocol::BlobsByRoot => PeerAction::MidToleranceError, Protocol::LightClientBootstrap => return, + Protocol::LightClientOptimisticUpdate => return, + Protocol::LightClientFinalityUpdate => return, Protocol::Goodbye => return, Protocol::MetaData => return, Protocol::Status => return, @@ -678,7 +680,7 @@ impl PeerManager { } /// Received a metadata response from a peer. - pub fn meta_data_response(&mut self, peer_id: &PeerId, meta_data: MetaData) { + pub fn meta_data_response(&mut self, peer_id: &PeerId, meta_data: MetaData) { if let Some(peer_info) = self.network_globals.peers.write().peer_info_mut(peer_id) { if let Some(known_meta_data) = &peer_info.meta_data() { if *known_meta_data.seq_number() < *meta_data.seq_number() { @@ -715,46 +717,6 @@ impl PeerManager { } } - // This function updates metrics for all connected peers. - fn update_connected_peer_metrics(&self) { - // Do nothing if we don't have metrics enabled. - if !self.metrics_enabled { - return; - } - - let mut connected_peer_count = 0; - let mut inbound_connected_peers = 0; - let mut outbound_connected_peers = 0; - let mut clients_per_peer = HashMap::new(); - - for (_peer, peer_info) in self.network_globals.peers.read().connected_peers() { - connected_peer_count += 1; - if let PeerConnectionStatus::Connected { n_in, .. } = peer_info.connection_status() { - if *n_in > 0 { - inbound_connected_peers += 1; - } else { - outbound_connected_peers += 1; - } - } - *clients_per_peer - .entry(peer_info.client().kind.to_string()) - .or_default() += 1; - } - - metrics::set_gauge(&metrics::PEERS_CONNECTED, connected_peer_count); - metrics::set_gauge(&metrics::NETWORK_INBOUND_PEERS, inbound_connected_peers); - metrics::set_gauge(&metrics::NETWORK_OUTBOUND_PEERS, outbound_connected_peers); - - for client_kind in ClientKind::iter() { - let value = clients_per_peer.get(&client_kind.to_string()).unwrap_or(&0); - metrics::set_gauge_vec( - &metrics::PEERS_PER_CLIENT, - &[client_kind.as_ref()], - *value as i64, - ); - } - } - /* Internal functions */ /// Sets a peer as connected as long as their reputation allows it @@ -849,12 +811,6 @@ impl PeerManager { // start a ping and status timer for the peer self.status_peers.insert(*peer_id); - let connected_peers = self.network_globals.connected_peers() as i64; - - // increment prometheus metrics - metrics::inc_counter(&metrics::PEER_CONNECT_EVENT_COUNT); - metrics::set_gauge(&metrics::PEERS_CONNECTED, connected_peers); - true } @@ -917,8 +873,7 @@ impl PeerManager { let outbound_only_peer_count = self.network_globals.connected_outbound_only_peers(); let wanted_peers = if peer_count < self.target_peers.saturating_sub(dialing_peers) { // We need more peers in general. - // Note: The maximum discovery query is bounded by `Discovery`. - self.target_peers.saturating_sub(dialing_peers) - peer_count + self.max_peers().saturating_sub(dialing_peers) - peer_count } else if outbound_only_peer_count < self.min_outbound_only_peers() && peer_count < self.max_outbound_dialing_peers() { @@ -1026,20 +981,19 @@ impl PeerManager { } // 1. Look through peers that have the worst score (ignoring non-penalized scored peers). - prune_peers!(|info: &PeerInfo| { info.score().score() < 0.0 }); + prune_peers!(|info: &PeerInfo| { info.score().score() < 0.0 }); // 2. Attempt to remove peers that are not subscribed to a subnet, if we still need to // prune more. if peers_to_prune.len() < connected_peer_count.saturating_sub(self.target_peers) { - prune_peers!(|info: &PeerInfo| { !info.has_long_lived_subnet() }); + prune_peers!(|info: &PeerInfo| { !info.has_long_lived_subnet() }); } // 3. and 4. Remove peers that are too grouped on any given subnet. If all subnets are // uniformly distributed, remove random peers. if peers_to_prune.len() < connected_peer_count.saturating_sub(self.target_peers) { // Of our connected peers, build a map from subnet_id -> Vec<(PeerId, PeerInfo)> - let mut subnet_to_peer: HashMap)>> = - HashMap::new(); + let mut subnet_to_peer: HashMap)>> = HashMap::new(); // These variables are used to track if a peer is in a long-lived sync-committee as we // may wish to retain this peer over others when pruning. let mut sync_committee_peer_count: HashMap = HashMap::new(); @@ -1305,6 +1259,70 @@ impl PeerManager { ); } } + + // Update peer count related metrics. + fn update_peer_count_metrics(&self) { + let mut peers_connected = 0; + let mut clients_per_peer = HashMap::new(); + let mut peers_connected_mutli: HashMap<(&str, &str), i32> = HashMap::new(); + + for (_, peer_info) in self.network_globals.peers.read().connected_peers() { + peers_connected += 1; + + *clients_per_peer + .entry(peer_info.client().kind.to_string()) + .or_default() += 1; + + let direction = match peer_info.connection_direction() { + Some(ConnectionDirection::Incoming) => "inbound", + Some(ConnectionDirection::Outgoing) => "outbound", + None => "none", + }; + // Note: the `transport` is set to `unknown` if the `listening_addresses` list is empty. + // This situation occurs when the peer is initially registered in PeerDB, but the peer + // info has not yet been updated at `PeerManager::identify`. + let transport = peer_info + .listening_addresses() + .iter() + .find_map(|addr| { + addr.iter().find_map(|proto| match proto { + multiaddr::Protocol::QuicV1 => Some("quic"), + multiaddr::Protocol::Tcp(_) => Some("tcp"), + _ => None, + }) + }) + .unwrap_or("unknown"); + *peers_connected_mutli + .entry((direction, transport)) + .or_default() += 1; + } + + // PEERS_CONNECTED + metrics::set_gauge(&metrics::PEERS_CONNECTED, peers_connected); + + // PEERS_PER_CLIENT + for client_kind in ClientKind::iter() { + let value = clients_per_peer.get(&client_kind.to_string()).unwrap_or(&0); + metrics::set_gauge_vec( + &metrics::PEERS_PER_CLIENT, + &[client_kind.as_ref()], + *value as i64, + ); + } + + // PEERS_CONNECTED_MULTI + for direction in ["inbound", "outbound", "none"] { + for transport in ["quic", "tcp", "unknown"] { + metrics::set_gauge_vec( + &metrics::PEERS_CONNECTED_MULTI, + &[direction, transport], + *peers_connected_mutli + .get(&(direction, transport)) + .unwrap_or(&0) as i64, + ); + } + } + } } enum ConnectingType { 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 da205d169a..b776347ad0 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs @@ -4,7 +4,7 @@ use std::net::IpAddr; use std::task::{Context, Poll}; use futures::StreamExt; -use libp2p::core::{multiaddr, ConnectedPoint}; +use libp2p::core::ConnectedPoint; use libp2p::identity::PeerId; use libp2p::swarm::behaviour::{ConnectionClosed, ConnectionEstablished, DialFailure, FromSwarm}; use libp2p::swarm::dial_opts::{DialOpts, PeerCondition}; @@ -21,7 +21,7 @@ use crate::{metrics, ClearDialError}; use super::{ConnectingType, PeerManager, PeerManagerEvent}; -impl NetworkBehaviour for PeerManager { +impl NetworkBehaviour for PeerManager { type ConnectionHandler = ConnectionHandler; type ToSwarm = PeerManagerEvent; @@ -96,10 +96,16 @@ impl NetworkBehaviour for PeerManager { 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); - } + + let quic_multiaddrs = if self.quic_enabled { + 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); + } + quic_multiaddrs + } else { + Vec::new() + }; // Prioritize Quic connections over Tcp ones. let multiaddrs = quic_multiaddrs @@ -148,8 +154,8 @@ impl NetworkBehaviour for PeerManager { self.on_dial_failure(peer_id); } FromSwarm::ExternalAddrConfirmed(_) => { - // TODO: we likely want to check this against our assumed external tcp - // address + // We have an external address confirmed, means we are able to do NAT traversal. + metrics::set_gauge_vec(&metrics::NAT_OPEN, &["libp2p"], 1); } _ => { // NOTE: FromSwarm is a non exhaustive enum so updates should be based on release @@ -221,7 +227,7 @@ impl NetworkBehaviour for PeerManager { } } -impl PeerManager { +impl PeerManager { fn on_connection_established( &mut self, peer_id: PeerId, @@ -237,34 +243,11 @@ impl PeerManager { self.events.push(PeerManagerEvent::MetaData(peer_id)); } - // Check NAT if metrics are enabled - if self.network_globals.local_enr.read().udp4().is_some() { - metrics::check_nat(); - } - - // increment prometheus metrics + // Update the prometheus metrics if self.metrics_enabled { - let remote_addr = endpoint.get_remote_address(); - 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); + + self.update_peer_count_metrics(); } // Count dialing peers in the limit if the peer dialed us. @@ -302,7 +285,7 @@ impl PeerManager { fn on_connection_closed( &mut self, peer_id: PeerId, - endpoint: &ConnectedPoint, + _endpoint: &ConnectedPoint, remaining_established: usize, ) { if remaining_established > 0 { @@ -330,26 +313,12 @@ impl PeerManager { // reference so that peer manager can track this peer. self.inject_disconnect(&peer_id); - let remote_addr = endpoint.get_remote_address(); // Update the prometheus metrics 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(); + // Legacy standard metrics. metrics::inc_counter(&metrics::PEER_DISCONNECT_EVENT_COUNT); + + self.update_peer_count_metrics(); } } diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs index a6bf3ffecc..c3e77ae225 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs @@ -1,3 +1,4 @@ +use crate::discovery::CombinedKey; use crate::{metrics, multiaddr::Multiaddr, types::Subnet, Enr, Gossipsub, PeerId}; use peer_info::{ConnectionDirection, PeerConnectionStatus, PeerInfo}; use rand::seq::SliceRandom; @@ -32,9 +33,9 @@ const ALLOWED_NEGATIVE_GOSSIPSUB_FACTOR: f32 = 0.1; const DIAL_TIMEOUT: u64 = 15; /// Storage of known peers, their reputation and information -pub struct PeerDB { +pub struct PeerDB { /// The collection of known connected peers, their status and reputation - peers: HashMap>, + peers: HashMap>, /// The number of disconnected nodes in the database. disconnected_peers: usize, /// Counts banned peers in total and per ip @@ -45,7 +46,7 @@ pub struct PeerDB { log: slog::Logger, } -impl PeerDB { +impl PeerDB { pub fn new(trusted_peers: Vec, disable_peer_scoring: bool, log: &slog::Logger) -> Self { // Initialize the peers hashmap with trusted peers let peers = trusted_peers @@ -72,7 +73,7 @@ impl PeerDB { } /// Returns an iterator over all peers in the db. - pub fn peers(&self) -> impl Iterator)> { + pub fn peers(&self) -> impl Iterator)> { self.peers.iter() } @@ -82,14 +83,14 @@ impl PeerDB { } /// Returns a peer's info, if known. - pub fn peer_info(&self, peer_id: &PeerId) -> Option<&PeerInfo> { + pub fn peer_info(&self, peer_id: &PeerId) -> Option<&PeerInfo> { self.peers.get(peer_id) } /// Returns a mutable reference to a peer's info if known. // VISIBILITY: The peer manager is able to modify some elements of the peer info, such as sync // status. - pub(super) fn peer_info_mut(&mut self, peer_id: &PeerId) -> Option<&mut PeerInfo> { + pub(super) fn peer_info_mut(&mut self, peer_id: &PeerId) -> Option<&mut PeerInfo> { self.peers.get_mut(peer_id) } @@ -154,7 +155,7 @@ impl PeerDB { } /// Checks if the peer's known addresses are currently banned. - fn ip_is_banned(&self, peer: &PeerInfo) -> Option { + fn ip_is_banned(&self, peer: &PeerInfo) -> Option { peer.seen_ip_addresses() .find(|ip| self.banned_peers_count.ip_is_banned(ip)) } @@ -168,7 +169,7 @@ impl PeerDB { fn score_state_banned_or_disconnected(&self, peer_id: &PeerId) -> bool { if let Some(peer) = self.peers.get(peer_id) { match peer.score_state() { - ScoreState::Banned | ScoreState::Disconnected => true, + ScoreState::Banned | ScoreState::ForcedDisconnect => true, _ => self.ip_is_banned(peer).is_some(), } } else { @@ -177,7 +178,7 @@ impl PeerDB { } /// Gives the ids and info of all known connected peers. - pub fn connected_peers(&self) -> impl Iterator)> { + pub fn connected_peers(&self) -> impl Iterator)> { self.peers.iter().filter(|(_, info)| info.is_connected()) } @@ -271,7 +272,7 @@ impl PeerDB { /// Returns a vector of all connected peers sorted by score beginning with the worst scores. /// Ties get broken randomly. - pub fn worst_connected_peers(&self) -> Vec<(&PeerId, &PeerInfo)> { + pub fn worst_connected_peers(&self) -> Vec<(&PeerId, &PeerInfo)> { let mut connected = self .peers .iter() @@ -285,9 +286,9 @@ impl PeerDB { /// Returns a vector containing peers (their ids and info), sorted by /// score from highest to lowest, and filtered using `is_status` - pub fn best_peers_by_status(&self, is_status: F) -> Vec<(&PeerId, &PeerInfo)> + pub fn best_peers_by_status(&self, is_status: F) -> Vec<(&PeerId, &PeerInfo)> where - F: Fn(&PeerInfo) -> bool, + F: Fn(&PeerInfo) -> bool, { let mut by_status = self .peers @@ -301,7 +302,7 @@ impl PeerDB { /// Returns the peer with highest reputation that satisfies `is_status` pub fn best_by_status(&self, is_status: F) -> Option<&PeerId> where - F: Fn(&PeerInfo) -> bool, + F: Fn(&PeerInfo) -> bool, { self.peers .iter() @@ -671,6 +672,20 @@ impl PeerDB { ); } + /// Updates the connection state. MUST ONLY BE USED IN TESTS. + pub fn __add_connected_peer_testing_only(&mut self, peer_id: &PeerId) -> Option { + let enr_key = CombinedKey::generate_secp256k1(); + let enr = Enr::builder().build(&enr_key).unwrap(); + self.update_connection_state( + peer_id, + NewConnectionState::Connected { + enr: Some(enr), + seen_address: Multiaddr::empty(), + direction: ConnectionDirection::Outgoing, + }, + ) + } + /// The connection state of the peer has been changed. Modify the peer in the db to ensure all /// variables are in sync with libp2p. /// Updating the state can lead to a `BanOperation` which needs to be processed via the peer @@ -756,8 +771,8 @@ impl PeerDB { // Update the connection state match direction { - ConnectionDirection::Incoming => info.connect_ingoing(Some(seen_address)), - ConnectionDirection::Outgoing => info.connect_outgoing(Some(seen_address)), + ConnectionDirection::Incoming => info.connect_ingoing(seen_address), + ConnectionDirection::Outgoing => info.connect_outgoing(seen_address), } } @@ -1058,16 +1073,16 @@ impl PeerDB { fn handle_score_transition( previous_state: ScoreState, peer_id: &PeerId, - info: &PeerInfo, + info: &PeerInfo, log: &slog::Logger, ) -> ScoreTransitionResult { match (info.score_state(), previous_state) { - (ScoreState::Banned, ScoreState::Healthy | ScoreState::Disconnected) => { + (ScoreState::Banned, ScoreState::Healthy | ScoreState::ForcedDisconnect) => { debug!(log, "Peer has been banned"; "peer_id" => %peer_id, "score" => %info.score()); ScoreTransitionResult::Banned } - (ScoreState::Disconnected, ScoreState::Banned | ScoreState::Healthy) => { - debug!(log, "Peer transitioned to disconnect state"; "peer_id" => %peer_id, "score" => %info.score(), "past_state" => %previous_state); + (ScoreState::ForcedDisconnect, ScoreState::Banned | ScoreState::Healthy) => { + debug!(log, "Peer transitioned to forced disconnect score state"; "peer_id" => %peer_id, "score" => %info.score(), "past_score_state" => %previous_state); // disconnect the peer if it's currently connected or dialing if info.is_connected_or_dialing() { ScoreTransitionResult::Disconnected @@ -1079,18 +1094,20 @@ impl PeerDB { ScoreTransitionResult::NoAction } } - (ScoreState::Healthy, ScoreState::Disconnected) => { - debug!(log, "Peer transitioned to healthy state"; "peer_id" => %peer_id, "score" => %info.score(), "past_state" => %previous_state); + (ScoreState::Healthy, ScoreState::ForcedDisconnect) => { + debug!(log, "Peer transitioned to healthy score state"; "peer_id" => %peer_id, "score" => %info.score(), "past_score_state" => %previous_state); ScoreTransitionResult::NoAction } (ScoreState::Healthy, ScoreState::Banned) => { - debug!(log, "Peer transitioned to healthy state"; "peer_id" => %peer_id, "score" => %info.score(), "past_state" => %previous_state); + debug!(log, "Peer transitioned to healthy score state"; "peer_id" => %peer_id, "score" => %info.score(), "past_score_state" => %previous_state); // unban the peer if it was previously banned. ScoreTransitionResult::Unbanned } // Explicitly ignore states that haven't transitioned. (ScoreState::Healthy, ScoreState::Healthy) => ScoreTransitionResult::NoAction, - (ScoreState::Disconnected, ScoreState::Disconnected) => ScoreTransitionResult::NoAction, + (ScoreState::ForcedDisconnect, ScoreState::ForcedDisconnect) => { + ScoreTransitionResult::NoAction + } (ScoreState::Banned, ScoreState::Banned) => ScoreTransitionResult::NoAction, } @@ -1249,7 +1266,6 @@ impl BannedPeersCount { mod tests { use super::*; use libp2p::core::multiaddr::Protocol; - use libp2p::core::Multiaddr; use slog::{o, Drain}; use std::net::{Ipv4Addr, Ipv6Addr}; use types::MinimalEthSpec; @@ -1268,13 +1284,13 @@ mod tests { } } - fn add_score(db: &mut PeerDB, peer_id: &PeerId, score: f64) { + fn add_score(db: &mut PeerDB, peer_id: &PeerId, score: f64) { if let Some(info) = db.peer_info_mut(peer_id) { info.add_to_score(score); } } - fn reset_score(db: &mut PeerDB, peer_id: &PeerId) { + fn reset_score(db: &mut PeerDB, peer_id: &PeerId) { if let Some(info) = db.peer_info_mut(peer_id) { info.reset_score(); } diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb/client.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb/client.rs index 1178dbcb9c..9450584d6f 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb/client.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb/client.rs @@ -33,6 +33,8 @@ pub enum ClientKind { Prysm, /// A lodestar node. Lodestar, + /// A Caplin node. + Caplin, /// An unknown client. Unknown, } @@ -88,6 +90,7 @@ impl std::fmt::Display for Client { self.version, self.os_version ), ClientKind::Lodestar => write!(f, "Lodestar: version: {}", self.version), + ClientKind::Caplin => write!(f, "Caplin"), ClientKind::Unknown => { if let Some(agent_string) = &self.agent_string { write!(f, "Unknown: {}", agent_string) @@ -109,11 +112,11 @@ impl std::fmt::Display for ClientKind { // kind and it's associated version and the OS kind. fn client_from_agent_version(agent_version: &str) -> (ClientKind, String, String) { let mut agent_split = agent_version.split('/'); + let mut version = String::from("unknown"); + let mut os_version = String::from("unknown"); match agent_split.next() { Some("Lighthouse") => { let kind = ClientKind::Lighthouse; - let mut version = String::from("unknown"); - let mut os_version = version.clone(); if let Some(agent_version) = agent_split.next() { version = agent_version.into(); if let Some(agent_os_version) = agent_split.next() { @@ -124,8 +127,6 @@ fn client_from_agent_version(agent_version: &str) -> (ClientKind, String, String } Some("teku") => { let kind = ClientKind::Teku; - let mut version = String::from("unknown"); - let mut os_version = version.clone(); if agent_split.next().is_some() { if let Some(agent_version) = agent_split.next() { version = agent_version.into(); @@ -138,13 +139,10 @@ fn client_from_agent_version(agent_version: &str) -> (ClientKind, String, String } Some("github.com") => { let kind = ClientKind::Prysm; - let unknown = String::from("unknown"); - (kind, unknown.clone(), unknown) + (kind, version, os_version) } Some("Prysm") => { let kind = ClientKind::Prysm; - let mut version = String::from("unknown"); - let mut os_version = version.clone(); if agent_split.next().is_some() { if let Some(agent_version) = agent_split.next() { version = agent_version.into(); @@ -157,8 +155,6 @@ fn client_from_agent_version(agent_version: &str) -> (ClientKind, String, String } Some("nimbus") => { let kind = ClientKind::Nimbus; - let mut version = String::from("unknown"); - let mut os_version = version.clone(); if agent_split.next().is_some() { if let Some(agent_version) = agent_split.next() { version = agent_version.into(); @@ -171,8 +167,6 @@ fn client_from_agent_version(agent_version: &str) -> (ClientKind, String, String } Some("nim-libp2p") => { let kind = ClientKind::Nimbus; - let mut version = String::from("unknown"); - let mut os_version = version.clone(); if let Some(agent_version) = agent_split.next() { version = agent_version.into(); if let Some(agent_os_version) = agent_split.next() { @@ -183,8 +177,6 @@ fn client_from_agent_version(agent_version: &str) -> (ClientKind, String, String } Some("js-libp2p") | Some("lodestar") => { let kind = ClientKind::Lodestar; - let mut version = String::from("unknown"); - let mut os_version = version.clone(); if let Some(agent_version) = agent_split.next() { version = agent_version.into(); if let Some(agent_os_version) = agent_split.next() { @@ -193,6 +185,14 @@ fn client_from_agent_version(agent_version: &str) -> (ClientKind, String, String } (kind, version, os_version) } + Some("erigon") => { + let client_kind = if let Some("caplin") = agent_split.next() { + ClientKind::Caplin + } else { + ClientKind::Unknown + }; + (client_kind, version, os_version) + } _ => { let unknown = String::from("unknown"); (ClientKind::Unknown, unknown.clone(), unknown) 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 44c54511dd..ab113a1a04 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 @@ -18,8 +18,8 @@ use PeerConnectionStatus::*; /// Information about a given connected peer. #[derive(Clone, Debug, Serialize)] -#[serde(bound = "T: EthSpec")] -pub struct PeerInfo { +#[serde(bound = "E: EthSpec")] +pub struct PeerInfo { /// The peers reputation score: Score, /// Client managing this peer @@ -37,7 +37,7 @@ pub struct PeerInfo { sync_status: SyncStatus, /// The ENR subnet bitfield of the peer. This may be determined after it's initial /// connection. - meta_data: Option>, + meta_data: Option>, /// Subnets the peer is connected to. subnets: HashSet, /// The time we would like to retain this peer. After this time, the peer is no longer @@ -53,8 +53,8 @@ pub struct PeerInfo { enr: Option, } -impl Default for PeerInfo { - fn default() -> PeerInfo { +impl Default for PeerInfo { + fn default() -> PeerInfo { PeerInfo { score: Score::default(), client: Client::default(), @@ -72,7 +72,7 @@ impl Default for PeerInfo { } } -impl PeerInfo { +impl PeerInfo { /// Return a PeerInfo struct for a trusted peer. pub fn trusted_peer_info() -> Self { PeerInfo { @@ -120,7 +120,7 @@ impl PeerInfo { } /// Returns the metadata for the peer if currently known. - pub fn meta_data(&self) -> Option<&MetaData> { + pub fn meta_data(&self) -> Option<&MetaData> { self.meta_data.as_ref() } @@ -151,7 +151,7 @@ impl PeerInfo { if let Some(meta_data) = self.meta_data.as_ref() { return meta_data.attnets().num_set_bits(); } else if let Some(enr) = self.enr.as_ref() { - if let Ok(attnets) = enr.attestation_bitfield::() { + if let Ok(attnets) = enr.attestation_bitfield::() { return attnets.num_set_bits(); } } @@ -177,7 +177,7 @@ impl PeerInfo { } } } else if let Some(enr) = self.enr.as_ref() { - if let Ok(attnets) = enr.attestation_bitfield::() { + if let Ok(attnets) = enr.attestation_bitfield::() { for subnet in 0..=attnets.highest_set_bit().unwrap_or(0) { if attnets.get(subnet).unwrap_or(false) { long_lived_subnets.push(Subnet::Attestation((subnet as u64).into())); @@ -185,7 +185,7 @@ impl PeerInfo { } } - if let Ok(syncnets) = enr.sync_committee_bitfield::() { + if let Ok(syncnets) = enr.sync_committee_bitfield::() { for subnet in 0..=syncnets.highest_set_bit().unwrap_or(0) { if syncnets.get(subnet).unwrap_or(false) { long_lived_subnets.push(Subnet::SyncCommittee((subnet as u64).into())); @@ -217,7 +217,7 @@ impl PeerInfo { // We may not have the metadata but may have an ENR. Lets check that if let Some(enr) = self.enr.as_ref() { - if let Ok(attnets) = enr.attestation_bitfield::() { + if let Ok(attnets) = enr.attestation_bitfield::() { if !attnets.is_zero() && !self.subnets.is_empty() { return true; } @@ -307,13 +307,13 @@ impl PeerInfo { /// Checks if the peer is outbound-only pub fn is_outbound_only(&self) -> bool { - matches!(self.connection_status, Connected {n_in, n_out} if n_in == 0 && n_out > 0) + matches!(self.connection_status, Connected {n_in, n_out, ..} if n_in == 0 && n_out > 0) } /// Returns the number of connections with this peer. pub fn connections(&self) -> (u8, u8) { match self.connection_status { - Connected { n_in, n_out } => (n_in, n_out), + Connected { n_in, n_out, .. } => (n_in, n_out), _ => (0, 0), } } @@ -344,7 +344,7 @@ impl PeerInfo { /// Sets an explicit value for the meta data. // VISIBILITY: The peer manager is able to adjust the meta_data - pub(in crate::peer_manager) fn set_meta_data(&mut self, meta_data: MetaData) { + pub(in crate::peer_manager) fn set_meta_data(&mut self, meta_data: MetaData) { self.meta_data = Some(meta_data) } @@ -421,7 +421,9 @@ impl PeerInfo { /// Modifies the status to Connected and increases the number of ingoing /// connections by one - pub(super) fn connect_ingoing(&mut self, seen_multiaddr: Option) { + pub(super) fn connect_ingoing(&mut self, multiaddr: Multiaddr) { + self.seen_multiaddrs.insert(multiaddr.clone()); + match &mut self.connection_status { Connected { n_in, .. } => *n_in += 1, Disconnected { .. } @@ -429,19 +431,20 @@ impl PeerInfo { | Dialing { .. } | Disconnecting { .. } | Unknown => { - self.connection_status = Connected { n_in: 1, n_out: 0 }; + self.connection_status = Connected { + n_in: 1, + n_out: 0, + multiaddr, + }; self.connection_direction = Some(ConnectionDirection::Incoming); } } - - 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_multiaddr: Option) { + pub(super) fn connect_outgoing(&mut self, multiaddr: Multiaddr) { + self.seen_multiaddrs.insert(multiaddr.clone()); match &mut self.connection_status { Connected { n_out, .. } => *n_out += 1, Disconnected { .. } @@ -449,13 +452,14 @@ impl PeerInfo { | Dialing { .. } | Disconnecting { .. } | Unknown => { - self.connection_status = Connected { n_in: 0, n_out: 1 }; + self.connection_status = Connected { + n_in: 0, + n_out: 1, + multiaddr, + }; self.connection_direction = Some(ConnectionDirection::Outgoing); } } - if let Some(multiaddr) = seen_multiaddr { - self.seen_multiaddrs.insert(multiaddr); - } } #[cfg(test)] @@ -487,6 +491,8 @@ pub enum ConnectionDirection { pub enum PeerConnectionStatus { /// The peer is connected. Connected { + /// The multiaddr that we are connected via. + multiaddr: Multiaddr, /// number of ingoing connections. n_in: u8, /// number of outgoing connections. @@ -522,7 +528,12 @@ impl Serialize for PeerConnectionStatus { fn serialize(&self, serializer: S) -> Result { let mut s = serializer.serialize_struct("connection_status", 6)?; match self { - Connected { n_in, n_out } => { + Connected { + n_in, + n_out, + multiaddr, + } => { + s.serialize_field("multiaddr", multiaddr)?; s.serialize_field("status", "connected")?; s.serialize_field("connections_in", n_in)?; s.serialize_field("connections_out", n_out)?; diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb/score.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb/score.rs index 877d725812..ba9bd31472 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb/score.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb/score.rs @@ -104,7 +104,7 @@ pub(crate) enum ScoreState { /// We are content with the peers performance. We permit connections and messages. Healthy, /// The peer should be disconnected. We allow re-connections if the peer is persistent. - Disconnected, + ForcedDisconnect, /// The peer is banned. We disallow new connections until it's score has decayed into a /// tolerable threshold. Banned, @@ -115,7 +115,7 @@ impl std::fmt::Display for ScoreState { match self { ScoreState::Healthy => write!(f, "Healthy"), ScoreState::Banned => write!(f, "Banned"), - ScoreState::Disconnected => write!(f, "Disconnected"), + ScoreState::ForcedDisconnect => write!(f, "Disconnected"), } } } @@ -313,7 +313,7 @@ impl Score { pub(crate) fn state(&self) -> ScoreState { match self.score() { x if x <= MIN_SCORE_BEFORE_BAN => ScoreState::Banned, - x if x <= MIN_SCORE_BEFORE_DISCONNECT => ScoreState::Disconnected, + x if x <= MIN_SCORE_BEFORE_DISCONNECT => ScoreState::ForcedDisconnect, _ => ScoreState::Healthy, } } @@ -407,7 +407,7 @@ mod tests { assert!(score.score() < 0.0); assert_eq!(score.state(), ScoreState::Healthy); score.test_add(-1.0001); - assert_eq!(score.state(), ScoreState::Disconnected); + assert_eq!(score.state(), ScoreState::ForcedDisconnect); } #[test] diff --git a/beacon_node/lighthouse_network/src/rpc/codec/base.rs b/beacon_node/lighthouse_network/src/rpc/codec/base.rs index 4085ac17b7..287f0a3f5f 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec/base.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec/base.rs @@ -20,20 +20,20 @@ pub trait OutboundCodec: Encoder + Decoder { /* Global Inbound Codec */ // This deals with Decoding RPC Requests from other peers and encoding our responses -pub struct BaseInboundCodec +pub struct BaseInboundCodec where - TCodec: Encoder> + Decoder, - TSpec: EthSpec, + TCodec: Encoder> + Decoder, + E: EthSpec, { /// Inner codec for handling various encodings inner: TCodec, - phantom: PhantomData, + phantom: PhantomData, } -impl BaseInboundCodec +impl BaseInboundCodec where - TCodec: Encoder> + Decoder, - TSpec: EthSpec, + TCodec: Encoder> + Decoder, + E: EthSpec, { pub fn new(codec: TCodec) -> Self { BaseInboundCodec { @@ -45,22 +45,22 @@ where /* Global Outbound Codec */ // This deals with Decoding RPC Responses from other peers and encoding our requests -pub struct BaseOutboundCodec +pub struct BaseOutboundCodec where - TOutboundCodec: OutboundCodec>, - TSpec: EthSpec, + TOutboundCodec: OutboundCodec>, + E: EthSpec, { /// Inner codec for handling various encodings. inner: TOutboundCodec, /// Keeps track of the current response code for a chunk. current_response_code: Option, - phantom: PhantomData, + phantom: PhantomData, } -impl BaseOutboundCodec +impl BaseOutboundCodec where - TSpec: EthSpec, - TOutboundCodec: OutboundCodec>, + E: EthSpec, + TOutboundCodec: OutboundCodec>, { pub fn new(codec: TOutboundCodec) -> Self { BaseOutboundCodec { @@ -76,18 +76,14 @@ where /* Base Inbound Codec */ // This Encodes RPC Responses sent to external peers -impl Encoder> for BaseInboundCodec +impl Encoder> for BaseInboundCodec where - TSpec: EthSpec, - TCodec: Decoder + Encoder>, + E: EthSpec, + TCodec: Decoder + Encoder>, { - type Error = >>::Error; + type Error = >>::Error; - fn encode( - &mut self, - item: RPCCodedResponse, - dst: &mut BytesMut, - ) -> Result<(), Self::Error> { + fn encode(&mut self, item: RPCCodedResponse, dst: &mut BytesMut) -> Result<(), Self::Error> { dst.clear(); dst.reserve(1); dst.put_u8( @@ -99,12 +95,12 @@ where } // This Decodes RPC Requests from external peers -impl Decoder for BaseInboundCodec +impl Decoder for BaseInboundCodec where - TSpec: EthSpec, - TCodec: Encoder> + Decoder>, + E: EthSpec, + TCodec: Encoder> + Decoder>, { - type Item = InboundRequest; + type Item = InboundRequest; type Error = ::Error; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { @@ -115,30 +111,26 @@ where /* Base Outbound Codec */ // This Encodes RPC Requests sent to external peers -impl Encoder> for BaseOutboundCodec +impl Encoder> for BaseOutboundCodec where - TSpec: EthSpec, - TCodec: OutboundCodec> + Encoder>, + E: EthSpec, + TCodec: OutboundCodec> + Encoder>, { - type Error = >>::Error; + type Error = >>::Error; - fn encode( - &mut self, - item: OutboundRequest, - dst: &mut BytesMut, - ) -> Result<(), Self::Error> { + fn encode(&mut self, item: OutboundRequest, dst: &mut BytesMut) -> Result<(), Self::Error> { self.inner.encode(item, dst) } } // This decodes RPC Responses received from external peers -impl Decoder for BaseOutboundCodec +impl Decoder for BaseOutboundCodec where - TSpec: EthSpec, - TCodec: OutboundCodec, CodecErrorType = ErrorType> - + Decoder>, + E: EthSpec, + TCodec: OutboundCodec, CodecErrorType = ErrorType> + + Decoder>, { - type Item = RPCCodedResponse; + type Item = RPCCodedResponse; type Error = ::Error; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { @@ -154,7 +146,7 @@ where }); let inner_result = { - if RPCCodedResponse::::is_response(response_code) { + if RPCCodedResponse::::is_response(response_code) { // decode an actual response and mutates the buffer if enough bytes have been read // returning the result. self.inner @@ -195,11 +187,13 @@ mod tests { let merge_fork_epoch = Epoch::new(2); let capella_fork_epoch = Epoch::new(3); let deneb_fork_epoch = Epoch::new(4); + let electra_fork_epoch = Epoch::new(5); chain_spec.altair_fork_epoch = Some(altair_fork_epoch); chain_spec.bellatrix_fork_epoch = Some(merge_fork_epoch); chain_spec.capella_fork_epoch = Some(capella_fork_epoch); chain_spec.deneb_fork_epoch = Some(deneb_fork_epoch); + chain_spec.electra_fork_epoch = Some(electra_fork_epoch); let current_slot = match fork_name { ForkName::Base => Slot::new(0), @@ -207,6 +201,7 @@ mod tests { ForkName::Merge => merge_fork_epoch.start_slot(Spec::slots_per_epoch()), ForkName::Capella => capella_fork_epoch.start_slot(Spec::slots_per_epoch()), ForkName::Deneb => deneb_fork_epoch.start_slot(Spec::slots_per_epoch()), + ForkName::Electra => electra_fork_epoch.start_slot(Spec::slots_per_epoch()), }; ForkContext::new::(current_slot, Hash256::zero(), &chain_spec) } diff --git a/beacon_node/lighthouse_network/src/rpc/codec/mod.rs b/beacon_node/lighthouse_network/src/rpc/codec/mod.rs index 05de328857..dbe99af5bf 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec/mod.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec/mod.rs @@ -10,26 +10,26 @@ use tokio_util::codec::{Decoder, Encoder}; use types::EthSpec; // Known types of codecs -pub enum InboundCodec { - SSZSnappy(BaseInboundCodec, TSpec>), +pub enum InboundCodec { + SSZSnappy(BaseInboundCodec, E>), } -pub enum OutboundCodec { - SSZSnappy(BaseOutboundCodec, TSpec>), +pub enum OutboundCodec { + SSZSnappy(BaseOutboundCodec, E>), } -impl Encoder> for InboundCodec { +impl Encoder> for InboundCodec { type Error = RPCError; - fn encode(&mut self, item: RPCCodedResponse, dst: &mut BytesMut) -> Result<(), Self::Error> { + fn encode(&mut self, item: RPCCodedResponse, dst: &mut BytesMut) -> Result<(), Self::Error> { match self { InboundCodec::SSZSnappy(codec) => codec.encode(item, dst), } } } -impl Decoder for InboundCodec { - type Item = InboundRequest; +impl Decoder for InboundCodec { + type Item = InboundRequest; type Error = RPCError; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { @@ -39,22 +39,18 @@ impl Decoder for InboundCodec { } } -impl Encoder> for OutboundCodec { +impl Encoder> for OutboundCodec { type Error = RPCError; - fn encode( - &mut self, - item: OutboundRequest, - dst: &mut BytesMut, - ) -> Result<(), Self::Error> { + fn encode(&mut self, item: OutboundRequest, dst: &mut BytesMut) -> Result<(), Self::Error> { match self { OutboundCodec::SSZSnappy(codec) => codec.encode(item, dst), } } } -impl Decoder for OutboundCodec { - type Item = RPCCodedResponse; +impl Decoder for OutboundCodec { + type Item = RPCCodedResponse; type Error = RPCError; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { 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 7a7f2969f1..4cbb658258 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs @@ -3,7 +3,7 @@ use crate::rpc::{ codec::base::OutboundCodec, protocol::{Encoding, ProtocolId, RPCError, SupportedProtocol, ERROR_TYPE_MAX, ERROR_TYPE_MIN}, }; -use crate::rpc::{InboundRequest, OutboundRequest, RPCCodedResponse, RPCResponse}; +use crate::rpc::{InboundRequest, OutboundRequest}; use libp2p::bytes::BytesMut; use snap::read::FrameDecoder; use snap::write::FrameEncoder; @@ -15,11 +15,11 @@ use std::io::{Read, Write}; use std::marker::PhantomData; use std::sync::Arc; use tokio_util::codec::{Decoder, Encoder}; -use types::ChainSpec; use types::{ - BlobSidecar, EthSpec, ForkContext, ForkName, Hash256, LightClientBootstrap, - RuntimeVariableList, SignedBeaconBlock, SignedBeaconBlockAltair, SignedBeaconBlockBase, - SignedBeaconBlockCapella, SignedBeaconBlockDeneb, SignedBeaconBlockMerge, + BlobSidecar, ChainSpec, EthSpec, ForkContext, ForkName, Hash256, LightClientBootstrap, + LightClientFinalityUpdate, LightClientOptimisticUpdate, RuntimeVariableList, SignedBeaconBlock, + SignedBeaconBlockAltair, SignedBeaconBlockBase, SignedBeaconBlockCapella, + SignedBeaconBlockDeneb, SignedBeaconBlockElectra, SignedBeaconBlockMerge, }; use unsigned_varint::codec::Uvi; @@ -27,17 +27,17 @@ const CONTEXT_BYTES_LEN: usize = 4; /* Inbound Codec */ -pub struct SSZSnappyInboundCodec { +pub struct SSZSnappyInboundCodec { protocol: ProtocolId, inner: Uvi, len: Option, /// Maximum bytes that can be sent in one req/resp chunked responses. max_packet_size: usize, fork_context: Arc, - phantom: PhantomData, + phantom: PhantomData, } -impl SSZSnappyInboundCodec { +impl SSZSnappyInboundCodec { pub fn new( protocol: ProtocolId, max_packet_size: usize, @@ -59,14 +59,10 @@ impl SSZSnappyInboundCodec { } // Encoder for inbound streams: Encodes RPC Responses sent to peers. -impl Encoder> for SSZSnappyInboundCodec { +impl Encoder> for SSZSnappyInboundCodec { type Error = RPCError; - fn encode( - &mut self, - item: RPCCodedResponse, - dst: &mut BytesMut, - ) -> Result<(), Self::Error> { + fn encode(&mut self, item: RPCCodedResponse, dst: &mut BytesMut) -> Result<(), Self::Error> { let bytes = match &item { RPCCodedResponse::Success(resp) => match &resp { RPCResponse::Status(res) => res.as_ssz_bytes(), @@ -75,6 +71,8 @@ impl Encoder> for SSZSnappyInboundCodec< RPCResponse::BlobsByRange(res) => res.as_ssz_bytes(), RPCResponse::BlobsByRoot(res) => res.as_ssz_bytes(), RPCResponse::LightClientBootstrap(res) => res.as_ssz_bytes(), + RPCResponse::LightClientOptimisticUpdate(res) => res.as_ssz_bytes(), + RPCResponse::LightClientFinalityUpdate(res) => res.as_ssz_bytes(), RPCResponse::Pong(res) => res.data.as_ssz_bytes(), RPCResponse::MetaData(res) => // Encode the correct version of the MetaData response based on the negotiated version. @@ -124,8 +122,8 @@ impl Encoder> for SSZSnappyInboundCodec< } // Decoder for inbound streams: Decodes RPC requests from peers -impl Decoder for SSZSnappyInboundCodec { - type Item = InboundRequest; +impl Decoder for SSZSnappyInboundCodec { + type Item = InboundRequest; type Error = RPCError; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { @@ -174,7 +172,7 @@ impl Decoder for SSZSnappyInboundCodec { } /* Outbound Codec: Codec for initiating RPC requests */ -pub struct SSZSnappyOutboundCodec { +pub struct SSZSnappyOutboundCodec { inner: Uvi, len: Option, protocol: ProtocolId, @@ -183,10 +181,10 @@ pub struct SSZSnappyOutboundCodec { /// The fork name corresponding to the received context bytes. fork_name: Option, fork_context: Arc, - phantom: PhantomData, + phantom: PhantomData, } -impl SSZSnappyOutboundCodec { +impl SSZSnappyOutboundCodec { pub fn new( protocol: ProtocolId, max_packet_size: usize, @@ -209,14 +207,10 @@ impl SSZSnappyOutboundCodec { } // Encoder for outbound streams: Encodes RPC Requests to peers -impl Encoder> for SSZSnappyOutboundCodec { +impl Encoder> for SSZSnappyOutboundCodec { type Error = RPCError; - fn encode( - &mut self, - item: OutboundRequest, - dst: &mut BytesMut, - ) -> Result<(), Self::Error> { + fn encode(&mut self, item: OutboundRequest, dst: &mut BytesMut) -> Result<(), Self::Error> { let bytes = match item { OutboundRequest::Status(req) => req.as_ssz_bytes(), OutboundRequest::Goodbye(req) => req.as_ssz_bytes(), @@ -261,8 +255,8 @@ impl Encoder> for SSZSnappyOutboundCodec< // The majority of the decoding has now been pushed upstream due to the changing specification. // We prefer to decode blocks and attestations with extra knowledge about the chain to perform // faster verification checks before decoding entire blocks/attestations. -impl Decoder for SSZSnappyOutboundCodec { - type Item = RPCResponse; +impl Decoder for SSZSnappyOutboundCodec { + type Item = RPCResponse; type Error = RPCError; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { @@ -286,9 +280,7 @@ impl Decoder for SSZSnappyOutboundCodec { // Should not attempt to decode rpc chunks with `length > max_packet_size` or not within bounds of // packet size for ssz container corresponding to `self.protocol`. - let ssz_limits = self - .protocol - .rpc_response_limits::(&self.fork_context); + let ssz_limits = self.protocol.rpc_response_limits::(&self.fork_context); if ssz_limits.is_out_of_bounds(length, self.max_packet_size) { return Err(RPCError::InvalidData(format!( "RPC response length is out of bounds, length {}, max {}, min {}", @@ -319,7 +311,7 @@ impl Decoder for SSZSnappyOutboundCodec { } } -impl OutboundCodec> for SSZSnappyOutboundCodec { +impl OutboundCodec> for SSZSnappyOutboundCodec { type CodecErrorType = ErrorType; fn decode_error( @@ -388,37 +380,59 @@ fn handle_error( /// Returns `Some(context_bytes)` for encoding RPC responses that require context bytes. /// Returns `None` when context bytes are not required. -fn context_bytes( +fn context_bytes( protocol: &ProtocolId, fork_context: &ForkContext, - resp: &RPCCodedResponse, + resp: &RPCCodedResponse, ) -> Option<[u8; CONTEXT_BYTES_LEN]> { // Add the context bytes if required if protocol.has_context_bytes() { if let RPCCodedResponse::Success(rpc_variant) = resp { - if let RPCResponse::BlocksByRange(ref_box_block) - | RPCResponse::BlocksByRoot(ref_box_block) = rpc_variant - { - return match **ref_box_block { - // NOTE: If you are adding another fork type here, be sure to modify the - // `fork_context.to_context_bytes()` function to support it as well! - SignedBeaconBlock::Deneb { .. } => { - fork_context.to_context_bytes(ForkName::Deneb) - } - SignedBeaconBlock::Capella { .. } => { - fork_context.to_context_bytes(ForkName::Capella) - } - SignedBeaconBlock::Merge { .. } => { - fork_context.to_context_bytes(ForkName::Merge) - } - SignedBeaconBlock::Altair { .. } => { - fork_context.to_context_bytes(ForkName::Altair) - } - SignedBeaconBlock::Base { .. } => Some(fork_context.genesis_context_bytes()), - }; - } - if let RPCResponse::BlobsByRange(_) | RPCResponse::BlobsByRoot(_) = rpc_variant { - return fork_context.to_context_bytes(ForkName::Deneb); + match rpc_variant { + RPCResponse::BlocksByRange(ref_box_block) + | RPCResponse::BlocksByRoot(ref_box_block) => { + return match **ref_box_block { + // NOTE: If you are adding another fork type here, be sure to modify the + // `fork_context.to_context_bytes()` function to support it as well! + SignedBeaconBlock::Electra { .. } => { + fork_context.to_context_bytes(ForkName::Electra) + } + SignedBeaconBlock::Deneb { .. } => { + fork_context.to_context_bytes(ForkName::Deneb) + } + SignedBeaconBlock::Capella { .. } => { + fork_context.to_context_bytes(ForkName::Capella) + } + SignedBeaconBlock::Merge { .. } => { + fork_context.to_context_bytes(ForkName::Merge) + } + SignedBeaconBlock::Altair { .. } => { + fork_context.to_context_bytes(ForkName::Altair) + } + SignedBeaconBlock::Base { .. } => { + Some(fork_context.genesis_context_bytes()) + } + }; + } + RPCResponse::BlobsByRange(_) | RPCResponse::BlobsByRoot(_) => { + return fork_context.to_context_bytes(ForkName::Deneb); + } + RPCResponse::LightClientBootstrap(lc_bootstrap) => { + return lc_bootstrap + .map_with_fork_name(|fork_name| fork_context.to_context_bytes(fork_name)); + } + RPCResponse::LightClientOptimisticUpdate(lc_optimistic_update) => { + return lc_optimistic_update + .map_with_fork_name(|fork_name| fork_context.to_context_bytes(fork_name)); + } + RPCResponse::LightClientFinalityUpdate(lc_finality_update) => { + return lc_finality_update + .map_with_fork_name(|fork_name| fork_context.to_context_bytes(fork_name)); + } + // These will not pass the has_context_bytes() check + RPCResponse::Status(_) | RPCResponse::Pong(_) | RPCResponse::MetaData(_) => { + return None; + } } } } @@ -453,11 +467,11 @@ fn handle_length( /// 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_rpc_request( +fn handle_rpc_request( versioned_protocol: SupportedProtocol, decoded_buffer: &[u8], spec: &ChainSpec, -) -> Result>, RPCError> { +) -> Result>, RPCError> { match versioned_protocol { SupportedProtocol::StatusV1 => Ok(Some(InboundRequest::Status( StatusMessage::from_ssz_bytes(decoded_buffer)?, @@ -506,6 +520,12 @@ fn handle_rpc_request( root: Hash256::from_ssz_bytes(decoded_buffer)?, }), )), + SupportedProtocol::LightClientOptimisticUpdateV1 => { + Ok(Some(InboundRequest::LightClientOptimisticUpdate)) + } + SupportedProtocol::LightClientFinalityUpdateV1 => { + Ok(Some(InboundRequest::LightClientFinalityUpdate)) + } // MetaData requests return early from InboundUpgrade and do not reach the decoder. // Handle this case just for completeness. SupportedProtocol::MetaDataV2 => { @@ -533,11 +553,11 @@ fn handle_rpc_request( /// /// For BlocksByRange/BlocksByRoot reponses, decodes the appropriate response /// according to the received `ForkName`. -fn handle_rpc_response( +fn handle_rpc_response( versioned_protocol: SupportedProtocol, decoded_buffer: &[u8], fork_name: Option, -) -> Result>, RPCError> { +) -> Result>, RPCError> { match versioned_protocol { SupportedProtocol::StatusV1 => Ok(Some(RPCResponse::Status( StatusMessage::from_ssz_bytes(decoded_buffer)?, @@ -590,9 +610,42 @@ fn handle_rpc_response( 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)?, - ))), + SupportedProtocol::LightClientBootstrapV1 => match fork_name { + Some(fork_name) => Ok(Some(RPCResponse::LightClientBootstrap(Arc::new( + LightClientBootstrap::from_ssz_bytes(decoded_buffer, fork_name)?, + )))), + None => Err(RPCError::ErrorResponse( + RPCResponseErrorCode::InvalidRequest, + format!( + "No context bytes provided for {:?} response", + versioned_protocol + ), + )), + }, + SupportedProtocol::LightClientOptimisticUpdateV1 => match fork_name { + Some(fork_name) => Ok(Some(RPCResponse::LightClientOptimisticUpdate(Arc::new( + LightClientOptimisticUpdate::from_ssz_bytes(decoded_buffer, fork_name)?, + )))), + None => Err(RPCError::ErrorResponse( + RPCResponseErrorCode::InvalidRequest, + format!( + "No context bytes provided for {:?} response", + versioned_protocol + ), + )), + }, + SupportedProtocol::LightClientFinalityUpdateV1 => match fork_name { + Some(fork_name) => Ok(Some(RPCResponse::LightClientFinalityUpdate(Arc::new( + LightClientFinalityUpdate::from_ssz_bytes(decoded_buffer, fork_name)?, + )))), + None => Err(RPCError::ErrorResponse( + RPCResponseErrorCode::InvalidRequest, + format!( + "No context bytes provided for {:?} response", + versioned_protocol + ), + )), + }, // 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)?, @@ -616,6 +669,11 @@ fn handle_rpc_response( Some(ForkName::Deneb) => Ok(Some(RPCResponse::BlocksByRange(Arc::new( SignedBeaconBlock::Deneb(SignedBeaconBlockDeneb::from_ssz_bytes(decoded_buffer)?), )))), + Some(ForkName::Electra) => Ok(Some(RPCResponse::BlocksByRange(Arc::new( + SignedBeaconBlock::Electra(SignedBeaconBlockElectra::from_ssz_bytes( + decoded_buffer, + )?), + )))), None => Err(RPCError::ErrorResponse( RPCResponseErrorCode::InvalidRequest, format!( @@ -642,6 +700,11 @@ fn handle_rpc_response( Some(ForkName::Deneb) => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( SignedBeaconBlock::Deneb(SignedBeaconBlockDeneb::from_ssz_bytes(decoded_buffer)?), )))), + Some(ForkName::Electra) => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( + SignedBeaconBlock::Electra(SignedBeaconBlockElectra::from_ssz_bytes( + decoded_buffer, + )?), + )))), None => Err(RPCError::ErrorResponse( RPCResponseErrorCode::InvalidRequest, format!( @@ -676,22 +739,13 @@ fn context_bytes_to_fork_name( mod tests { use super::*; - use crate::rpc::{protocol::*, MetaData}; - use crate::{ - rpc::{methods::StatusMessage, Ping, RPCResponseErrorCode}, - types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield}, - }; - use std::sync::Arc; + use crate::rpc::protocol::*; + use crate::types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield}; use types::{ blob_sidecar::BlobIdentifier, BeaconBlock, BeaconBlockAltair, BeaconBlockBase, - BeaconBlockMerge, ChainSpec, EmptyBlock, Epoch, ForkContext, FullPayload, Hash256, - Signature, SignedBeaconBlock, Slot, + BeaconBlockMerge, EmptyBlock, Epoch, FullPayload, Signature, Slot, }; - use snap::write::FrameEncoder; - use ssz::Encode; - use std::io::Write; - type Spec = types::MainnetEthSpec; fn fork_context(fork_name: ForkName) -> ForkContext { @@ -700,11 +754,13 @@ mod tests { let merge_fork_epoch = Epoch::new(2); let capella_fork_epoch = Epoch::new(3); let deneb_fork_epoch = Epoch::new(4); + let electra_fork_epoch = Epoch::new(5); chain_spec.altair_fork_epoch = Some(altair_fork_epoch); chain_spec.bellatrix_fork_epoch = Some(merge_fork_epoch); chain_spec.capella_fork_epoch = Some(capella_fork_epoch); chain_spec.deneb_fork_epoch = Some(deneb_fork_epoch); + chain_spec.electra_fork_epoch = Some(electra_fork_epoch); let current_slot = match fork_name { ForkName::Base => Slot::new(0), @@ -712,6 +768,7 @@ mod tests { ForkName::Merge => merge_fork_epoch.start_slot(Spec::slots_per_epoch()), ForkName::Capella => capella_fork_epoch.start_slot(Spec::slots_per_epoch()), ForkName::Deneb => deneb_fork_epoch.start_slot(Spec::slots_per_epoch()), + ForkName::Electra => electra_fork_epoch.start_slot(Spec::slots_per_epoch()), }; ForkContext::new::(current_slot, Hash256::zero(), &chain_spec) } diff --git a/beacon_node/lighthouse_network/src/rpc/config.rs b/beacon_node/lighthouse_network/src/rpc/config.rs index 9895149198..08b81c7eae 100644 --- a/beacon_node/lighthouse_network/src/rpc/config.rs +++ b/beacon_node/lighthouse_network/src/rpc/config.rs @@ -92,6 +92,8 @@ pub struct RateLimiterConfig { pub(super) blobs_by_range_quota: Quota, pub(super) blobs_by_root_quota: Quota, pub(super) light_client_bootstrap_quota: Quota, + pub(super) light_client_optimistic_update_quota: Quota, + pub(super) light_client_finality_update_quota: Quota, } impl RateLimiterConfig { @@ -104,6 +106,8 @@ impl RateLimiterConfig { pub const DEFAULT_BLOBS_BY_RANGE_QUOTA: Quota = Quota::n_every(768, 10); pub const DEFAULT_BLOBS_BY_ROOT_QUOTA: Quota = Quota::n_every(128, 10); pub const DEFAULT_LIGHT_CLIENT_BOOTSTRAP_QUOTA: Quota = Quota::one_every(10); + pub const DEFAULT_LIGHT_CLIENT_OPTIMISTIC_UPDATE_QUOTA: Quota = Quota::one_every(10); + pub const DEFAULT_LIGHT_CLIENT_FINALITY_UPDATE_QUOTA: Quota = Quota::one_every(10); } impl Default for RateLimiterConfig { @@ -118,6 +122,9 @@ impl Default for RateLimiterConfig { blobs_by_range_quota: Self::DEFAULT_BLOBS_BY_RANGE_QUOTA, blobs_by_root_quota: Self::DEFAULT_BLOBS_BY_ROOT_QUOTA, light_client_bootstrap_quota: Self::DEFAULT_LIGHT_CLIENT_BOOTSTRAP_QUOTA, + light_client_optimistic_update_quota: + Self::DEFAULT_LIGHT_CLIENT_OPTIMISTIC_UPDATE_QUOTA, + light_client_finality_update_quota: Self::DEFAULT_LIGHT_CLIENT_FINALITY_UPDATE_QUOTA, } } } @@ -164,6 +171,8 @@ impl FromStr for RateLimiterConfig { let mut blobs_by_range_quota = None; let mut blobs_by_root_quota = None; let mut light_client_bootstrap_quota = None; + let mut light_client_optimistic_update_quota = None; + let mut light_client_finality_update_quota = None; for proto_def in s.split(';') { let ProtocolQuota { protocol, quota } = proto_def.parse()?; @@ -180,6 +189,14 @@ impl FromStr for RateLimiterConfig { Protocol::LightClientBootstrap => { light_client_bootstrap_quota = light_client_bootstrap_quota.or(quota) } + Protocol::LightClientOptimisticUpdate => { + light_client_optimistic_update_quota = + light_client_optimistic_update_quota.or(quota) + } + Protocol::LightClientFinalityUpdate => { + light_client_finality_update_quota = + light_client_finality_update_quota.or(quota) + } } } Ok(RateLimiterConfig { @@ -196,6 +213,10 @@ impl FromStr for RateLimiterConfig { blobs_by_root_quota: blobs_by_root_quota.unwrap_or(Self::DEFAULT_BLOBS_BY_ROOT_QUOTA), light_client_bootstrap_quota: light_client_bootstrap_quota .unwrap_or(Self::DEFAULT_LIGHT_CLIENT_BOOTSTRAP_QUOTA), + light_client_optimistic_update_quota: light_client_optimistic_update_quota + .unwrap_or(Self::DEFAULT_LIGHT_CLIENT_OPTIMISTIC_UPDATE_QUOTA), + light_client_finality_update_quota: light_client_finality_update_quota + .unwrap_or(Self::DEFAULT_LIGHT_CLIENT_FINALITY_UPDATE_QUOTA), }) } } diff --git a/beacon_node/lighthouse_network/src/rpc/handler.rs b/beacon_node/lighthouse_network/src/rpc/handler.rs index 03f4761ff0..df5bbba99c 100644 --- a/beacon_node/lighthouse_network/src/rpc/handler.rs +++ b/beacon_node/lighthouse_network/src/rpc/handler.rs @@ -9,7 +9,7 @@ use crate::rpc::outbound::{OutboundFramed, OutboundRequest}; use crate::rpc::protocol::InboundFramed; use fnv::FnvHashMap; use futures::prelude::*; -use futures::{Sink, SinkExt}; +use futures::SinkExt; use libp2p::swarm::handler::{ ConnectionEvent, ConnectionHandler, ConnectionHandlerEvent, DialUpgradeError, FullyNegotiatedInbound, FullyNegotiatedOutbound, StreamUpgradeError, SubstreamProtocol, @@ -24,7 +24,7 @@ use std::{ task::{Context, Poll}, time::{Duration, Instant}, }; -use tokio::time::{sleep_until, Instant as TInstant, Sleep}; +use tokio::time::{sleep, Sleep}; use tokio_util::time::{delay_queue, DelayQueue}; use types::{EthSpec, ForkContext}; @@ -32,7 +32,7 @@ use types::{EthSpec, ForkContext}; const IO_ERROR_RETRIES: u8 = 3; /// Maximum time given to the handler to perform shutdown operations. -const SHUTDOWN_TIMEOUT_SECS: u8 = 15; +const SHUTDOWN_TIMEOUT_SECS: u64 = 15; /// Maximum number of simultaneous inbound substreams we keep for this peer. const MAX_INBOUND_SUBSTREAMS: usize = 32; @@ -47,12 +47,12 @@ impl SubstreamId { } } -type InboundSubstream = InboundFramed; +type InboundSubstream = InboundFramed; /// Events the handler emits to the behaviour. #[derive(Debug)] -pub enum HandlerEvent { - Ok(RPCReceived), +pub enum HandlerEvent { + Ok(RPCReceived), Err(HandlerErr), Close(RPCError), } @@ -84,30 +84,30 @@ pub enum HandlerErr { } /// Implementation of `ConnectionHandler` for the RPC protocol. -pub struct RPCHandler +pub struct RPCHandler where - TSpec: EthSpec, + E: EthSpec, { /// The upgrade for inbound substreams. - listen_protocol: SubstreamProtocol, ()>, + listen_protocol: SubstreamProtocol, ()>, /// Queue of events to produce in `poll()`. - events_out: SmallVec<[HandlerEvent; 4]>, + events_out: SmallVec<[HandlerEvent; 4]>, /// Queue of outbound substreams to open. - dial_queue: SmallVec<[(Id, OutboundRequest); 4]>, + dial_queue: SmallVec<[(Id, OutboundRequest); 4]>, /// Current number of concurrent outbound substreams being opened. dial_negotiated: u32, /// Current inbound substreams awaiting processing. - inbound_substreams: FnvHashMap>, + inbound_substreams: FnvHashMap>, /// Inbound substream `DelayQueue` which keeps track of when an inbound substream will timeout. inbound_substreams_delay: DelayQueue, /// Map of outbound substreams that need to be driven to completion. - outbound_substreams: FnvHashMap>, + outbound_substreams: FnvHashMap>, /// Inbound substream `DelayQueue` which keeps track of when an inbound substream will timeout. outbound_substreams_delay: DelayQueue, @@ -155,15 +155,15 @@ enum HandlerState { } /// Contains the information the handler keeps on established inbound substreams. -struct InboundInfo { +struct InboundInfo { /// State of the substream. - state: InboundState, + state: InboundState, /// Responses queued for sending. - pending_items: VecDeque>, + pending_items: VecDeque>, /// Protocol of the original request we received from the peer. protocol: Protocol, /// Responses that the peer is still expecting from us. - remaining_chunks: u64, + max_remaining_chunks: u64, /// Useful to timing how long each request took to process. Currently only used by /// BlocksByRange. request_start_time: Instant, @@ -172,53 +172,53 @@ struct InboundInfo { } /// Contains the information the handler keeps on established outbound substreams. -struct OutboundInfo { +struct OutboundInfo { /// State of the substream. - state: OutboundSubstreamState, + state: OutboundSubstreamState, /// Key to keep track of the substream's timeout via `self.outbound_substreams_delay`. delay_key: delay_queue::Key, /// Info over the protocol this substream is handling. proto: Protocol, /// Number of chunks to be seen from the peer's response. - remaining_chunks: Option, + max_remaining_chunks: Option, /// `Id` as given by the application that sent the request. req_id: Id, } /// State of an inbound substream connection. -enum InboundState { +enum InboundState { /// The underlying substream is not being used. - Idle(InboundSubstream), + Idle(InboundSubstream), /// The underlying substream is processing responses. /// The return value of the future is (substream, stream_was_closed). The stream_was_closed boolean /// indicates if the stream was closed due to an error or successfully completing a response. - Busy(Pin, bool), RPCError>> + Send>>), + Busy(Pin, bool), RPCError>> + Send>>), /// Temporary state during processing Poisoned, } /// State of an outbound substream. Either waiting for a response, or in the process of sending. -pub enum OutboundSubstreamState { +pub enum OutboundSubstreamState { /// A request has been sent, and we are awaiting a response. This future is driven in the /// 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, + request: OutboundRequest, }, /// Closing an outbound substream> - Closing(Box>), + Closing(Box>), /// Temporary state during processing Poisoned, } -impl RPCHandler +impl RPCHandler where - TSpec: EthSpec, + E: EthSpec, { pub fn new( - listen_protocol: SubstreamProtocol, ()>, + listen_protocol: SubstreamProtocol, ()>, fork_context: Arc, log: &slog::Logger, resp_timeout: Duration, @@ -266,14 +266,14 @@ where self.dial_queue.push((id, OutboundRequest::Goodbye(reason))); } - self.state = HandlerState::ShuttingDown(Box::pin(sleep_until( - TInstant::now() + Duration::from_secs(SHUTDOWN_TIMEOUT_SECS as u64), - ))); + self.state = HandlerState::ShuttingDown(Box::pin(sleep(Duration::from_secs( + SHUTDOWN_TIMEOUT_SECS, + )))); } } /// Opens an outbound substream with a request. - fn send_request(&mut self, id: Id, req: OutboundRequest) { + fn send_request(&mut self, id: Id, req: OutboundRequest) { match self.state { HandlerState::Active => { self.dial_queue.push((id, req)); @@ -291,7 +291,7 @@ where /// Sends a response to a peer's request. // NOTE: If the substream has closed due to inactivity, or the substream is in the // wrong state a response will fail silently. - fn send_response(&mut self, inbound_id: SubstreamId, response: RPCCodedResponse) { + fn send_response(&mut self, inbound_id: SubstreamId, response: RPCCodedResponse) { // check if the stream matching the response still exists let Some(inbound_info) = self.inbound_substreams.get_mut(&inbound_id) else { if !matches!(response, RPCCodedResponse::StreamTermination(..)) { @@ -320,16 +320,16 @@ where } } -impl ConnectionHandler for RPCHandler +impl ConnectionHandler for RPCHandler where - TSpec: EthSpec, + E: EthSpec, Id: ReqId, { - type FromBehaviour = RPCSend; - type ToBehaviour = HandlerEvent; - type InboundProtocol = RPCProtocol; - type OutboundProtocol = OutboundRequestContainer; - type OutboundOpenInfo = (Id, OutboundRequest); // Keep track of the id and the request + type FromBehaviour = RPCSend; + type ToBehaviour = HandlerEvent; + type InboundProtocol = RPCProtocol; + type OutboundProtocol = OutboundRequestContainer; + type OutboundOpenInfo = (Id, OutboundRequest); // Keep track of the id and the request type InboundOpenInfo = (); fn listen_protocol(&self) -> SubstreamProtocol { @@ -349,23 +349,7 @@ where } fn connection_keep_alive(&self) -> bool { - // Check that we don't have outbound items pending for dialing, nor dialing, nor - // established. Also check that there are no established inbound substreams. - // Errors and events need to be reported back, so check those too. - match self.state { - HandlerState::ShuttingDown(_) => { - !self.dial_queue.is_empty() - || !self.outbound_substreams.is_empty() - || !self.inbound_substreams.is_empty() - || !self.events_out.is_empty() - || !self.dial_negotiated != 0 - } - HandlerState::Deactivated => { - // Regardless of events, the timeout has expired. Force the disconnect. - false - } - _ => true, - } + !matches!(self.state, HandlerState::Deactivated) } fn poll( @@ -395,7 +379,7 @@ where match delay.as_mut().poll(cx) { Poll::Ready(_) => { self.state = HandlerState::Deactivated; - debug!(self.log, "Handler deactivated"); + debug!(self.log, "Shutdown timeout elapsed, Handler deactivated"); return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour( HandlerEvent::Close(RPCError::Disconnected), )); @@ -487,7 +471,7 @@ where // Process one more message if one exists. if let Some(message) = info.pending_items.pop_front() { // If this is the last chunk, terminate the stream. - let last_chunk = info.remaining_chunks <= 1; + let last_chunk = info.max_remaining_chunks <= 1; let fut = send_message_to_inbound_substream(substream, message, last_chunk) .boxed(); @@ -553,7 +537,8 @@ where { // The substream is still active, decrement the remaining // chunks expected. - info.remaining_chunks = info.remaining_chunks.saturating_sub(1); + info.max_remaining_chunks = + info.max_remaining_chunks.saturating_sub(1); // If this substream has not ended, we reset the timer. // Each chunk is allowed RESPONSE_TIMEOUT to be sent. @@ -568,7 +553,7 @@ where // Process one more message if one exists. if let Some(message) = info.pending_items.pop_front() { // If this is the last chunk, terminate the stream. - let last_chunk = info.remaining_chunks <= 1; + let last_chunk = info.max_remaining_chunks <= 1; let fut = send_message_to_inbound_substream( substream, message, last_chunk, ) @@ -680,15 +665,19 @@ where request, } => match substream.poll_next_unpin(cx) { Poll::Ready(Some(Ok(response))) => { - if request.expected_responses() > 1 && !response.close_after() { + if request.expect_exactly_one_response() || response.close_after() { + // either this is a single response request or this response closes the + // stream + entry.get_mut().state = OutboundSubstreamState::Closing(substream); + } else { let substream_entry = entry.get_mut(); let delay_key = &substream_entry.delay_key; // chunks left after this one - let remaining_chunks = substream_entry - .remaining_chunks + let max_remaining_chunks = substream_entry + .max_remaining_chunks .map(|count| count.saturating_sub(1)) .unwrap_or_else(|| 0); - if remaining_chunks == 0 { + if max_remaining_chunks == 0 { // this is the last expected message, close the stream as all expected chunks have been received substream_entry.state = OutboundSubstreamState::Closing(substream); } else { @@ -698,14 +687,10 @@ where substream, request, }; - substream_entry.remaining_chunks = Some(remaining_chunks); + substream_entry.max_remaining_chunks = Some(max_remaining_chunks); self.outbound_substreams_delay .reset(delay_key, self.resp_timeout); } - } else { - // either this is a single response request or this response closes the - // stream - entry.get_mut().state = OutboundSubstreamState::Closing(substream); } // Check what type of response we got and report it accordingly @@ -741,7 +726,16 @@ where self.outbound_substreams_delay.remove(delay_key); entry.remove_entry(); // notify the application error - if request.expected_responses() > 1 { + if request.expect_exactly_one_response() { + // return an error, stream should not have closed early. + return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour( + HandlerEvent::Err(HandlerErr::Outbound { + id: request_id, + proto: request.versioned_protocol().protocol(), + error: RPCError::IncompleteStream, + }), + )); + } else { // return an end of stream result return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour( HandlerEvent::Ok(RPCReceived::EndOfStream( @@ -750,16 +744,6 @@ where )), )); } - - // else we return an error, stream should not have closed early. - let outbound_err = HandlerErr::Outbound { - id: request_id, - proto: request.versioned_protocol().protocol(), - error: RPCError::IncompleteStream, - }; - return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour( - HandlerEvent::Err(outbound_err), - )); } Poll::Pending => { entry.get_mut().state = @@ -844,6 +828,8 @@ where && self.events_out.is_empty() && self.dial_negotiated == 0 { + debug!(self.log, "Goodbye sent, Handler deactivated"); + self.state = HandlerState::Deactivated; return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour( HandlerEvent::Close(RPCError::Disconnected), )); @@ -882,22 +868,22 @@ where } } -impl RPCHandler +impl RPCHandler where Id: ReqId, - TSpec: EthSpec, + E: EthSpec, { - fn on_fully_negotiated_inbound(&mut self, substream: InboundOutput) { + 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(); + let max_responses = req.max_responses(); // store requests that expect responses - if expected_responses > 0 { + if max_responses > 0 { if self.inbound_substreams.len() < MAX_INBOUND_SUBSTREAMS { // Store the stream and tag the output. let delay_key = self @@ -908,14 +894,13 @@ where self.current_inbound_substream_id, InboundInfo { state: awaiting_stream, - pending_items: VecDeque::with_capacity(std::cmp::min( - expected_responses, - 128, - ) as usize), + pending_items: VecDeque::with_capacity( + std::cmp::min(max_responses, 128) as usize + ), delay_key: Some(delay_key), protocol: req.versioned_protocol().protocol(), request_start_time: Instant::now(), - remaining_chunks: expected_responses, + max_remaining_chunks: max_responses, }, ); } else { @@ -942,8 +927,8 @@ where fn on_fully_negotiated_outbound( &mut self, - substream: OutboundFramed, - (id, request): (Id, OutboundRequest), + substream: OutboundFramed, + (id, request): (Id, OutboundRequest), ) { self.dial_negotiated -= 1; // Reset any io-retries counter. @@ -962,8 +947,14 @@ where } // add the stream to substreams if we expect a response, otherwise drop the stream. - let expected_responses = request.expected_responses(); - if expected_responses > 0 { + let max_responses = request.max_responses(); + if max_responses > 0 { + let max_remaining_chunks = if request.expect_exactly_one_response() { + // Currently enforced only for multiple responses + None + } else { + Some(max_responses) + }; // new outbound request. Store the stream and tag the output. let delay_key = self .outbound_substreams_delay @@ -972,12 +963,6 @@ where 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( @@ -986,7 +971,7 @@ where state: awaiting_stream, delay_key, proto, - remaining_chunks: expected_responses, + max_remaining_chunks, req_id: id, }, ) @@ -999,7 +984,7 @@ where } fn on_dial_upgrade_error( &mut self, - request_info: (Id, OutboundRequest), + request_info: (Id, OutboundRequest), error: StreamUpgradeError, ) { let (id, req) = request_info; @@ -1055,11 +1040,11 @@ impl slog::Value for SubstreamId { /// /// This function returns the given substream, along with whether it has been closed or not. Any /// error that occurred with sending a message is reported also. -async fn send_message_to_inbound_substream( - mut substream: InboundSubstream, - message: RPCCodedResponse, +async fn send_message_to_inbound_substream( + mut substream: InboundSubstream, + message: RPCCodedResponse, last_chunk: bool, -) -> Result<(InboundSubstream, bool), RPCError> { +) -> Result<(InboundSubstream, bool), RPCError> { if matches!(message, RPCCodedResponse::StreamTermination(_)) { substream.close().await.map(|_| (substream, true)) } else { diff --git a/beacon_node/lighthouse_network/src/rpc/methods.rs b/beacon_node/lighthouse_network/src/rpc/methods.rs index cd3579ad6e..1b0486ff77 100644 --- a/beacon_node/lighthouse_network/src/rpc/methods.rs +++ b/beacon_node/lighthouse_network/src/rpc/methods.rs @@ -6,6 +6,7 @@ use serde::Serialize; use ssz::Encode; use ssz_derive::{Decode, Encode}; use ssz_types::{typenum::U256, VariableList}; +use std::fmt::Display; use std::marker::PhantomData; use std::ops::Deref; use std::sync::Arc; @@ -14,7 +15,8 @@ use superstruct::superstruct; use types::blob_sidecar::BlobIdentifier; use types::{ blob_sidecar::BlobSidecar, ChainSpec, Epoch, EthSpec, Hash256, LightClientBootstrap, - RuntimeVariableList, SignedBeaconBlock, Slot, + LightClientFinalityUpdate, LightClientOptimisticUpdate, RuntimeVariableList, SignedBeaconBlock, + Slot, }; /// Maximum length of error message. @@ -44,11 +46,13 @@ impl Deref for ErrorType { } } -impl ToString for ErrorType { - fn to_string(&self) -> String { +impl Display for ErrorType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { #[allow(clippy::invalid_regex)] let re = Regex::new("\\p{C}").expect("Regex is valid"); - String::from_utf8_lossy(&re.replace_all(self.0.deref(), &b""[..])).to_string() + let error_type_str = + String::from_utf8_lossy(&re.replace_all(self.0.deref(), &b""[..])).to_string(); + write!(f, "{}", error_type_str) } } @@ -88,11 +92,11 @@ pub struct Ping { variant_attributes(derive(Clone, Debug, PartialEq, Serialize),) )] #[derive(Clone, Debug, PartialEq)] -pub struct MetadataRequest { - _phantom_data: PhantomData, +pub struct MetadataRequest { + _phantom_data: PhantomData, } -impl MetadataRequest { +impl MetadataRequest { pub fn new_v1() -> Self { Self::V1(MetadataRequestV1 { _phantom_data: PhantomData, @@ -111,22 +115,22 @@ impl MetadataRequest { variants(V1, V2), variant_attributes( derive(Encode, Decode, Clone, Debug, PartialEq, Serialize), - serde(bound = "T: EthSpec", deny_unknown_fields), + serde(bound = "E: EthSpec", deny_unknown_fields), ) )] #[derive(Clone, Debug, PartialEq, Serialize)] -#[serde(bound = "T: EthSpec")] -pub struct MetaData { +#[serde(bound = "E: EthSpec")] +pub struct MetaData { /// A sequential counter indicating when data gets modified. pub seq_number: u64, /// The persistent attestation subnet bitfield. - pub attnets: EnrAttestationBitfield, + pub attnets: EnrAttestationBitfield, /// The persistent sync committee bitfield. #[superstruct(only(V2))] - pub syncnets: EnrSyncCommitteeBitfield, + pub syncnets: EnrSyncCommitteeBitfield, } -impl MetaData { +impl MetaData { /// Returns a V1 MetaData response from self. pub fn metadata_v1(&self) -> Self { match self { @@ -370,31 +374,37 @@ impl BlobsByRootRequest { // Collection of enums and structs used by the Codecs to encode/decode RPC messages #[derive(Debug, Clone, PartialEq)] -pub enum RPCResponse { +pub enum RPCResponse { /// A HELLO message. Status(StatusMessage), /// A response to a get BLOCKS_BY_RANGE request. A None response signifies the end of the /// batch. - BlocksByRange(Arc>), + BlocksByRange(Arc>), /// A response to a get BLOCKS_BY_ROOT request. - BlocksByRoot(Arc>), + BlocksByRoot(Arc>), /// A response to a get BLOBS_BY_RANGE request - BlobsByRange(Arc>), + BlobsByRange(Arc>), /// A response to a get LIGHT_CLIENT_BOOTSTRAP request. - LightClientBootstrap(LightClientBootstrap), + LightClientBootstrap(Arc>), + + /// A response to a get LIGHT_CLIENT_OPTIMISTIC_UPDATE request. + LightClientOptimisticUpdate(Arc>), + + /// A response to a get LIGHT_CLIENT_FINALITY_UPDATE request. + LightClientFinalityUpdate(Arc>), /// A response to a get BLOBS_BY_ROOT request. - BlobsByRoot(Arc>), + BlobsByRoot(Arc>), /// A PONG response to a PING request. Pong(Ping), /// A response to a META_DATA request. - MetaData(MetaData), + MetaData(MetaData), } /// Indicates which response is being terminated by a stream termination response. @@ -416,9 +426,9 @@ pub enum ResponseTermination { /// The structured response containing a result/code indicating success or failure /// and the contents of the response #[derive(Debug, Clone)] -pub enum RPCCodedResponse { +pub enum RPCCodedResponse { /// The response is a successful. - Success(RPCResponse), + Success(RPCResponse), Error(RPCResponseErrorCode, ErrorType), @@ -445,7 +455,7 @@ pub enum RPCResponseErrorCode { Unknown, } -impl RPCCodedResponse { +impl RPCCodedResponse { /// Used to encode the response in the codec. pub fn as_u8(&self) -> Option { match self { @@ -473,25 +483,6 @@ impl RPCCodedResponse { RPCCodedResponse::Error(code, err) } - /// Specifies which response allows for multiple chunks for the stream handler. - pub fn multiple_responses(&self) -> bool { - match self { - RPCCodedResponse::Success(resp) => match resp { - RPCResponse::Status(_) => false, - RPCResponse::BlocksByRange(_) => true, - RPCResponse::BlocksByRoot(_) => true, - RPCResponse::BlobsByRange(_) => true, - RPCResponse::BlobsByRoot(_) => true, - RPCResponse::Pong(_) => false, - RPCResponse::MetaData(_) => false, - RPCResponse::LightClientBootstrap(_) => false, - }, - RPCCodedResponse::Error(_, _) => true, - // Stream terminations are part of responses that have chunks - RPCCodedResponse::StreamTermination(_) => true, - } - } - /// Returns true if this response always terminates the stream. pub fn close_after(&self) -> bool { !matches!(self, RPCCodedResponse::Success(_)) @@ -512,7 +503,7 @@ impl RPCResponseErrorCode { } use super::Protocol; -impl RPCResponse { +impl RPCResponse { pub fn protocol(&self) -> Protocol { match self { RPCResponse::Status(_) => Protocol::Status, @@ -523,6 +514,8 @@ impl RPCResponse { RPCResponse::Pong(_) => Protocol::Ping, RPCResponse::MetaData(_) => Protocol::MetaData, RPCResponse::LightClientBootstrap(_) => Protocol::LightClientBootstrap, + RPCResponse::LightClientOptimisticUpdate(_) => Protocol::LightClientOptimisticUpdate, + RPCResponse::LightClientFinalityUpdate(_) => Protocol::LightClientFinalityUpdate, } } } @@ -547,7 +540,7 @@ impl std::fmt::Display for StatusMessage { } } -impl std::fmt::Display for RPCResponse { +impl std::fmt::Display for RPCResponse { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { RPCResponse::Status(status) => write!(f, "{}", status), @@ -566,21 +559,31 @@ impl std::fmt::Display for RPCResponse { RPCResponse::Pong(ping) => write!(f, "Pong: {}", ping.data), RPCResponse::MetaData(metadata) => write!(f, "Metadata: {}", metadata.seq_number()), RPCResponse::LightClientBootstrap(bootstrap) => { + write!(f, "LightClientBootstrap Slot: {}", bootstrap.get_slot()) + } + RPCResponse::LightClientOptimisticUpdate(update) => { write!( f, - "LightClientBootstrap Slot: {}", - bootstrap.header.beacon.slot + "LightClientOptimisticUpdate Slot: {}", + update.signature_slot() + ) + } + RPCResponse::LightClientFinalityUpdate(update) => { + write!( + f, + "LightClientFinalityUpdate Slot: {}", + update.signature_slot() ) } } } } -impl std::fmt::Display for RPCCodedResponse { +impl std::fmt::Display for RPCCodedResponse { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { RPCCodedResponse::Success(res) => write!(f, "{}", res), - RPCCodedResponse::Error(code, err) => write!(f, "{}: {}", code, err.to_string()), + RPCCodedResponse::Error(code, err) => write!(f, "{}: {}", code, err), RPCCodedResponse::StreamTermination(_) => write!(f, "Stream Termination"), } } diff --git a/beacon_node/lighthouse_network/src/rpc/mod.rs b/beacon_node/lighthouse_network/src/rpc/mod.rs index 3606438fb9..a91c9e44b2 100644 --- a/beacon_node/lighthouse_network/src/rpc/mod.rs +++ b/beacon_node/lighthouse_network/src/rpc/mod.rs @@ -51,41 +51,41 @@ impl ReqId for T where T: Send + 'static + std::fmt::Debug + Copy + Clone {} /// RPC events sent from Lighthouse. #[derive(Debug, Clone)] -pub enum RPCSend { +pub enum RPCSend { /// A request sent from Lighthouse. /// /// The `Id` is given by the application making the request. These /// go over *outbound* connections. - Request(Id, OutboundRequest), + Request(Id, OutboundRequest), /// A response sent from Lighthouse. /// /// The `SubstreamId` must correspond to the RPC-given ID of the original request received from the /// peer. The second parameter is a single chunk of a response. These go over *inbound* /// connections. - Response(SubstreamId, RPCCodedResponse), + Response(SubstreamId, RPCCodedResponse), /// Lighthouse has requested to terminate the connection with a goodbye message. Shutdown(Id, GoodbyeReason), } /// RPC events received from outside Lighthouse. #[derive(Debug, Clone)] -pub enum RPCReceived { +pub enum RPCReceived { /// A request received from the outside. /// /// The `SubstreamId` is given by the `RPCHandler` as it identifies this request with the /// *inbound* substream over which it is managed. - Request(SubstreamId, InboundRequest), + Request(SubstreamId, InboundRequest), /// A response received from the outside. /// /// The `Id` corresponds to the application given ID of the original request sent to the /// peer. The second parameter is a single chunk of a response. These go over *outbound* /// connections. - Response(Id, RPCResponse), + Response(Id, RPCResponse), /// Marks a request as completed EndOfStream(Id, ResponseTermination), } -impl std::fmt::Display for RPCSend { +impl std::fmt::Display for RPCSend { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { RPCSend::Request(id, req) => write!(f, "RPC Request(id: {:?}, {})", id, req), @@ -97,16 +97,16 @@ impl std::fmt::Display for RPCSend { /// Messages sent to the user from the RPC protocol. #[derive(Debug)] -pub struct RPCMessage { +pub struct RPCMessage { /// The peer that sent the message. pub peer_id: PeerId, /// Handler managing this message. pub conn_id: ConnectionId, /// The message that was sent. - pub event: HandlerEvent, + pub event: HandlerEvent, } -type BehaviourAction = ToSwarm, RPCSend>; +type BehaviourAction = ToSwarm, RPCSend>; pub struct NetworkParams { pub max_chunk_size: usize, @@ -116,13 +116,13 @@ pub struct NetworkParams { /// Implements the libp2p `NetworkBehaviour` trait and therefore manages network-level /// logic. -pub struct RPC { +pub struct RPC { /// Rate limiter limiter: Option, /// Rate limiter for our own requests. - self_limiter: Option>, + self_limiter: Option>, /// Queue of events to be processed. - events: Vec>, + events: Vec>, fork_context: Arc, enable_light_client_server: bool, /// Slog logger for RPC behaviour. @@ -131,7 +131,7 @@ pub struct RPC { network_params: NetworkParams, } -impl RPC { +impl RPC { pub fn new( fork_context: Arc, enable_light_client_server: bool, @@ -170,7 +170,7 @@ impl RPC { &mut self, peer_id: PeerId, id: (ConnectionId, SubstreamId), - event: RPCCodedResponse, + event: RPCCodedResponse, ) { self.events.push(ToSwarm::NotifyHandler { peer_id, @@ -182,7 +182,7 @@ impl RPC { /// Submits an RPC request. /// /// The peer must be connected for this to succeed. - pub fn send_request(&mut self, peer_id: PeerId, request_id: Id, req: OutboundRequest) { + pub fn send_request(&mut self, peer_id: PeerId, request_id: Id, req: OutboundRequest) { let event = if let Some(self_limiter) = self.self_limiter.as_mut() { match self_limiter.allows(peer_id, request_id, req) { Ok(event) => event, @@ -213,17 +213,17 @@ impl RPC { } } -impl NetworkBehaviour for RPC +impl NetworkBehaviour for RPC where - TSpec: EthSpec, + E: EthSpec, Id: ReqId, { - type ConnectionHandler = RPCHandler; - type ToSwarm = RPCMessage; + type ConnectionHandler = RPCHandler; + type ToSwarm = RPCMessage; fn handle_established_inbound_connection( &mut self, - _connection_id: ConnectionId, + connection_id: ConnectionId, peer_id: PeerId, _local_addr: &libp2p::Multiaddr, _remote_addr: &libp2p::Multiaddr, @@ -238,9 +238,9 @@ where }, (), ); - // 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 log = self + .log + .new(slog::o!("peer_id" => peer_id.to_string(), "connection_id" => connection_id.to_string())); let handler = RPCHandler::new( protocol, self.fork_context.clone(), @@ -253,7 +253,7 @@ where fn handle_established_outbound_connection( &mut self, - _connection_id: ConnectionId, + connection_id: ConnectionId, peer_id: PeerId, _addr: &libp2p::Multiaddr, _role_override: libp2p::core::Endpoint, @@ -269,9 +269,10 @@ where (), ); - // 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 log = self + .log + .new(slog::o!("peer_id" => peer_id.to_string(), "connection_id" => connection_id.to_string())); + let handler = RPCHandler::new( protocol, self.fork_context.clone(), @@ -309,11 +310,16 @@ where 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) - || matches!(protocol, Protocol::BlobsByRange) - { - debug!(self.log, "By range request will never be processed"; "request" => %req, "protocol" => %protocol); + if matches!( + protocol, + Protocol::BlocksByRange + | Protocol::BlobsByRange + | Protocol::BlocksByRoot + | Protocol::BlobsByRoot + ) { + debug!(self.log, "Request too large to process"; "request" => %req, "protocol" => %protocol); } else { + // Other protocols shouldn't be sending large messages, we should flag the peer kind crit!(self.log, "Request size too large to ever be processed"; "protocol" => %protocol); } // send an error code to the peer. @@ -388,9 +394,9 @@ where } } -impl slog::KV for RPCMessage +impl slog::KV for RPCMessage where - TSpec: EthSpec, + E: EthSpec, Id: ReqId, { fn serialize( diff --git a/beacon_node/lighthouse_network/src/rpc/outbound.rs b/beacon_node/lighthouse_network/src/rpc/outbound.rs index 713e9e0ec9..8ea7b84bc9 100644 --- a/beacon_node/lighthouse_network/src/rpc/outbound.rs +++ b/beacon_node/lighthouse_network/src/rpc/outbound.rs @@ -2,11 +2,10 @@ use super::methods::*; use super::protocol::ProtocolId; use super::protocol::SupportedProtocol; use super::RPCError; -use crate::rpc::protocol::Encoding; -use crate::rpc::{ - codec::{base::BaseOutboundCodec, ssz_snappy::SSZSnappyOutboundCodec, OutboundCodec}, - methods::ResponseTermination, +use crate::rpc::codec::{ + base::BaseOutboundCodec, ssz_snappy::SSZSnappyOutboundCodec, OutboundCodec, }; +use crate::rpc::protocol::Encoding; use futures::future::BoxFuture; use futures::prelude::{AsyncRead, AsyncWrite}; use futures::{FutureExt, SinkExt}; @@ -23,14 +22,14 @@ use types::{EthSpec, ForkContext}; // `OutboundUpgrade` #[derive(Debug, Clone)] -pub struct OutboundRequestContainer { - pub req: OutboundRequest, +pub struct OutboundRequestContainer { + pub req: OutboundRequest, pub fork_context: Arc, pub max_rpc_size: usize, } #[derive(Debug, Clone, PartialEq)] -pub enum OutboundRequest { +pub enum OutboundRequest { Status(StatusMessage), Goodbye(GoodbyeReason), BlocksByRange(OldBlocksByRangeRequest), @@ -38,10 +37,10 @@ pub enum OutboundRequest { BlobsByRange(BlobsByRangeRequest), BlobsByRoot(BlobsByRootRequest), Ping(Ping), - MetaData(MetadataRequest), + MetaData(MetadataRequest), } -impl UpgradeInfo for OutboundRequestContainer { +impl UpgradeInfo for OutboundRequestContainer { type Info = ProtocolId; type InfoIter = Vec; @@ -52,7 +51,7 @@ impl UpgradeInfo for OutboundRequestContainer { } /// Implements the encoding per supported protocol for `RPCRequest`. -impl OutboundRequest { +impl OutboundRequest { pub fn supported_protocols(&self) -> Vec { match self { // add more protocols when versions/encodings are supported @@ -92,20 +91,33 @@ impl OutboundRequest { } /* These functions are used in the handler for stream management */ - /// Number of responses expected for this request. - pub fn expected_responses(&self) -> u64 { + /// Maximum number of responses expected for this request. + pub fn max_responses(&self) -> u64 { match self { OutboundRequest::Status(_) => 1, OutboundRequest::Goodbye(_) => 0, OutboundRequest::BlocksByRange(req) => *req.count(), OutboundRequest::BlocksByRoot(req) => req.block_roots().len() as u64, - OutboundRequest::BlobsByRange(req) => req.max_blobs_requested::(), + OutboundRequest::BlobsByRange(req) => req.max_blobs_requested::(), OutboundRequest::BlobsByRoot(req) => req.blob_ids.len() as u64, OutboundRequest::Ping(_) => 1, OutboundRequest::MetaData(_) => 1, } } + pub fn expect_exactly_one_response(&self) -> bool { + match self { + OutboundRequest::Status(_) => true, + OutboundRequest::Goodbye(_) => false, + OutboundRequest::BlocksByRange(_) => false, + OutboundRequest::BlocksByRoot(_) => false, + OutboundRequest::BlobsByRange(_) => false, + OutboundRequest::BlobsByRoot(_) => false, + OutboundRequest::Ping(_) => true, + OutboundRequest::MetaData(_) => true, + } + } + /// Gives the corresponding `SupportedProtocol` to this request. pub fn versioned_protocol(&self) -> SupportedProtocol { match self { @@ -151,14 +163,14 @@ impl OutboundRequest { /* Outbound upgrades */ -pub type OutboundFramed = Framed, OutboundCodec>; +pub type OutboundFramed = Framed, OutboundCodec>; -impl OutboundUpgrade for OutboundRequestContainer +impl OutboundUpgrade for OutboundRequestContainer where - TSpec: EthSpec + Send + 'static, + E: EthSpec + Send + 'static, TSocket: AsyncRead + AsyncWrite + Unpin + Send + 'static, { - type Output = OutboundFramed; + type Output = OutboundFramed; type Error = RPCError; type Future = BoxFuture<'static, Result>; @@ -187,7 +199,7 @@ where } } -impl std::fmt::Display for OutboundRequest { +impl std::fmt::Display for OutboundRequest { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { OutboundRequest::Status(status) => write!(f, "Status Message: {}", status), diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index 9c174b8e42..f65586087c 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -1,8 +1,5 @@ use super::methods::*; -use crate::rpc::{ - codec::{base::BaseInboundCodec, ssz_snappy::SSZSnappyInboundCodec, InboundCodec}, - methods::{MaxErrorLen, ResponseTermination, MAX_ERROR_LEN}, -}; +use crate::rpc::codec::{base::BaseInboundCodec, ssz_snappy::SSZSnappyInboundCodec, InboundCodec}; use futures::future::BoxFuture; use futures::prelude::{AsyncRead, AsyncWrite}; use futures::{FutureExt, StreamExt}; @@ -20,9 +17,11 @@ use tokio_util::{ compat::{Compat, FuturesAsyncReadCompatExt}, }; use types::{ - BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BeaconBlockCapella, BeaconBlockMerge, - BlobSidecar, ChainSpec, EmptyBlock, EthSpec, ForkContext, ForkName, MainnetEthSpec, Signature, - SignedBeaconBlock, + BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BeaconBlockCapella, BeaconBlockElectra, + BeaconBlockMerge, BlobSidecar, ChainSpec, EmptyBlock, EthSpec, ForkContext, ForkName, + LightClientBootstrap, LightClientBootstrapAltair, LightClientFinalityUpdate, + LightClientFinalityUpdateAltair, LightClientOptimisticUpdate, + LightClientOptimisticUpdateAltair, MainnetEthSpec, Signature, SignedBeaconBlock, }; lazy_static! { @@ -68,6 +67,13 @@ lazy_static! { .as_ssz_bytes() .len(); + pub static ref SIGNED_BEACON_BLOCK_ELECTRA_MAX_WITHOUT_PAYLOAD: usize = SignedBeaconBlock::::from_block( + BeaconBlock::Electra(BeaconBlockElectra::full(&MainnetEthSpec::default_spec())), + Signature::empty(), + ) + .as_ssz_bytes() + .len(); + /// 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 @@ -86,6 +92,12 @@ lazy_static! { + types::ExecutionPayload::::max_execution_payload_deneb_size() // adding max size of execution payload (~16gb) + ssz::BYTES_PER_LENGTH_OFFSET // Adding the additional offsets for the `ExecutionPayload` + (::ssz_fixed_len() * ::max_blobs_per_block()) + + ssz::BYTES_PER_LENGTH_OFFSET; // Length offset for the blob commitments field. + // + pub static ref SIGNED_BEACON_BLOCK_ELECTRA_MAX: usize = *SIGNED_BEACON_BLOCK_ELECTRA_MAX_WITHOUT_PAYLOAD + + types::ExecutionPayload::::max_execution_payload_electra_size() // adding max size of execution payload (~16gb) + + ssz::BYTES_PER_LENGTH_OFFSET // Adding the additional ssz offset for the `ExecutionPayload` field + + (::ssz_fixed_len() * ::max_blobs_per_block()) + ssz::BYTES_PER_LENGTH_OFFSET; // Length offset for the blob commitments field. pub static ref ERROR_TYPE_MIN: usize = @@ -100,6 +112,16 @@ lazy_static! { ]) .as_ssz_bytes() .len(); + + pub static ref LIGHT_CLIENT_FINALITY_UPDATE_CAPELLA_MAX: usize = LightClientFinalityUpdate::::ssz_max_len_for_fork(ForkName::Capella); + pub static ref LIGHT_CLIENT_FINALITY_UPDATE_DENEB_MAX: usize = LightClientFinalityUpdate::::ssz_max_len_for_fork(ForkName::Deneb); + pub static ref LIGHT_CLIENT_FINALITY_UPDATE_ELECTRA_MAX: usize = LightClientFinalityUpdate::::ssz_max_len_for_fork(ForkName::Electra); + pub static ref LIGHT_CLIENT_OPTIMISTIC_UPDATE_CAPELLA_MAX: usize = LightClientOptimisticUpdate::::ssz_max_len_for_fork(ForkName::Capella); + pub static ref LIGHT_CLIENT_OPTIMISTIC_UPDATE_DENEB_MAX: usize = LightClientOptimisticUpdate::::ssz_max_len_for_fork(ForkName::Deneb); + pub static ref LIGHT_CLIENT_OPTIMISTIC_UPDATE_ELECTRA_MAX: usize = LightClientOptimisticUpdate::::ssz_max_len_for_fork(ForkName::Electra); + pub static ref LIGHT_CLIENT_BOOTSTRAP_CAPELLA_MAX: usize = LightClientBootstrap::::ssz_max_len_for_fork(ForkName::Capella); + pub static ref LIGHT_CLIENT_BOOTSTRAP_DENEB_MAX: usize = LightClientBootstrap::::ssz_max_len_for_fork(ForkName::Deneb); + pub static ref LIGHT_CLIENT_BOOTSTRAP_ELECTRA_MAX: usize = LightClientBootstrap::::ssz_max_len_for_fork(ForkName::Electra); } /// The protocol prefix the RPC protocol id. @@ -115,6 +137,7 @@ pub fn max_rpc_size(fork_context: &ForkContext, max_chunk_size: usize) -> usize ForkName::Merge => max_chunk_size, ForkName::Capella => max_chunk_size, ForkName::Deneb => max_chunk_size, + ForkName::Electra => max_chunk_size, } } @@ -141,8 +164,62 @@ pub fn rpc_block_limits_by_fork(current_fork: ForkName) -> RpcLimits { ), ForkName::Deneb => RpcLimits::new( *SIGNED_BEACON_BLOCK_BASE_MIN, // Base block is smaller than altair and merge blocks - *SIGNED_BEACON_BLOCK_DENEB_MAX, // EIP 4844 block is larger than all prior fork blocks + *SIGNED_BEACON_BLOCK_DENEB_MAX, // Deneb block is larger than all prior fork blocks ), + ForkName::Electra => RpcLimits::new( + *SIGNED_BEACON_BLOCK_BASE_MIN, // Base block is smaller than altair and merge blocks + *SIGNED_BEACON_BLOCK_ELECTRA_MAX, // Electra block is larger than Deneb block + ), + } +} + +fn rpc_light_client_finality_update_limits_by_fork(current_fork: ForkName) -> RpcLimits { + let altair_fixed_len = LightClientFinalityUpdateAltair::::ssz_fixed_len(); + + match ¤t_fork { + ForkName::Base => RpcLimits::new(0, 0), + ForkName::Altair | ForkName::Merge => RpcLimits::new(altair_fixed_len, altair_fixed_len), + ForkName::Capella => { + RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_FINALITY_UPDATE_CAPELLA_MAX) + } + ForkName::Deneb => { + RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_FINALITY_UPDATE_DENEB_MAX) + } + ForkName::Electra => { + RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_FINALITY_UPDATE_ELECTRA_MAX) + } + } +} + +fn rpc_light_client_optimistic_update_limits_by_fork(current_fork: ForkName) -> RpcLimits { + let altair_fixed_len = LightClientOptimisticUpdateAltair::::ssz_fixed_len(); + + match ¤t_fork { + ForkName::Base => RpcLimits::new(0, 0), + ForkName::Altair | ForkName::Merge => RpcLimits::new(altair_fixed_len, altair_fixed_len), + ForkName::Capella => RpcLimits::new( + altair_fixed_len, + *LIGHT_CLIENT_OPTIMISTIC_UPDATE_CAPELLA_MAX, + ), + ForkName::Deneb => { + RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_OPTIMISTIC_UPDATE_DENEB_MAX) + } + ForkName::Electra => RpcLimits::new( + altair_fixed_len, + *LIGHT_CLIENT_OPTIMISTIC_UPDATE_ELECTRA_MAX, + ), + } +} + +fn rpc_light_client_bootstrap_limits_by_fork(current_fork: ForkName) -> RpcLimits { + let altair_fixed_len = LightClientBootstrapAltair::::ssz_fixed_len(); + + match ¤t_fork { + ForkName::Base => RpcLimits::new(0, 0), + ForkName::Altair | ForkName::Merge => RpcLimits::new(altair_fixed_len, altair_fixed_len), + ForkName::Capella => RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_BOOTSTRAP_CAPELLA_MAX), + ForkName::Deneb => RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_BOOTSTRAP_DENEB_MAX), + ForkName::Electra => RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_BOOTSTRAP_ELECTRA_MAX), } } @@ -174,6 +251,12 @@ pub enum Protocol { /// The `LightClientBootstrap` protocol name. #[strum(serialize = "light_client_bootstrap")] LightClientBootstrap, + /// The `LightClientOptimisticUpdate` protocol name. + #[strum(serialize = "light_client_optimistic_update")] + LightClientOptimisticUpdate, + /// The `LightClientFinalityUpdate` protocol name. + #[strum(serialize = "light_client_finality_update")] + LightClientFinalityUpdate, } impl Protocol { @@ -188,6 +271,8 @@ impl Protocol { Protocol::Ping => None, Protocol::MetaData => None, Protocol::LightClientBootstrap => None, + Protocol::LightClientOptimisticUpdate => None, + Protocol::LightClientFinalityUpdate => None, } } } @@ -213,6 +298,8 @@ pub enum SupportedProtocol { MetaDataV1, MetaDataV2, LightClientBootstrapV1, + LightClientOptimisticUpdateV1, + LightClientFinalityUpdateV1, } impl SupportedProtocol { @@ -230,6 +317,8 @@ impl SupportedProtocol { SupportedProtocol::MetaDataV1 => "1", SupportedProtocol::MetaDataV2 => "2", SupportedProtocol::LightClientBootstrapV1 => "1", + SupportedProtocol::LightClientOptimisticUpdateV1 => "1", + SupportedProtocol::LightClientFinalityUpdateV1 => "1", } } @@ -247,6 +336,10 @@ impl SupportedProtocol { SupportedProtocol::MetaDataV1 => Protocol::MetaData, SupportedProtocol::MetaDataV2 => Protocol::MetaData, SupportedProtocol::LightClientBootstrapV1 => Protocol::LightClientBootstrap, + SupportedProtocol::LightClientOptimisticUpdateV1 => { + Protocol::LightClientOptimisticUpdate + } + SupportedProtocol::LightClientFinalityUpdateV1 => Protocol::LightClientFinalityUpdate, } } @@ -283,15 +376,15 @@ impl std::fmt::Display for Encoding { } #[derive(Debug, Clone)] -pub struct RPCProtocol { +pub struct RPCProtocol { pub fork_context: Arc, pub max_rpc_size: usize, pub enable_light_client_server: bool, - pub phantom: PhantomData, + pub phantom: PhantomData, pub ttfb_timeout: Duration, } -impl UpgradeInfo for RPCProtocol { +impl UpgradeInfo for RPCProtocol { type Info = ProtocolId; type InfoIter = Vec; @@ -303,6 +396,14 @@ impl UpgradeInfo for RPCProtocol { SupportedProtocol::LightClientBootstrapV1, Encoding::SSZSnappy, )); + supported_protocols.push(ProtocolId::new( + SupportedProtocol::LightClientOptimisticUpdateV1, + Encoding::SSZSnappy, + )); + supported_protocols.push(ProtocolId::new( + SupportedProtocol::LightClientFinalityUpdateV1, + Encoding::SSZSnappy, + )); } supported_protocols } @@ -377,12 +478,14 @@ impl ProtocolId { ::ssz_fixed_len(), ::ssz_fixed_len(), ), + Protocol::LightClientOptimisticUpdate => RpcLimits::new(0, 0), + Protocol::LightClientFinalityUpdate => RpcLimits::new(0, 0), Protocol::MetaData => RpcLimits::new(0, 0), // Metadata requests are empty } } /// Returns min and max size for messages of given protocol id responses. - pub fn rpc_response_limits(&self, fork_context: &ForkContext) -> RpcLimits { + pub fn rpc_response_limits(&self, fork_context: &ForkContext) -> RpcLimits { match self.versioned_protocol.protocol() { Protocol::Status => RpcLimits::new( ::ssz_fixed_len(), @@ -391,20 +494,25 @@ impl ProtocolId { Protocol::Goodbye => RpcLimits::new(0, 0), // Goodbye request has no response Protocol::BlocksByRange => rpc_block_limits_by_fork(fork_context.current_fork()), Protocol::BlocksByRoot => rpc_block_limits_by_fork(fork_context.current_fork()), - Protocol::BlobsByRange => rpc_blob_limits::(), - Protocol::BlobsByRoot => rpc_blob_limits::(), + Protocol::BlobsByRange => rpc_blob_limits::(), + Protocol::BlobsByRoot => rpc_blob_limits::(), Protocol::Ping => RpcLimits::new( ::ssz_fixed_len(), ::ssz_fixed_len(), ), Protocol::MetaData => RpcLimits::new( - as Encode>::ssz_fixed_len(), - as Encode>::ssz_fixed_len(), - ), - Protocol::LightClientBootstrap => RpcLimits::new( - ::ssz_fixed_len(), - ::ssz_fixed_len(), + as Encode>::ssz_fixed_len(), + as Encode>::ssz_fixed_len(), ), + Protocol::LightClientBootstrap => { + rpc_light_client_bootstrap_limits_by_fork(fork_context.current_fork()) + } + Protocol::LightClientOptimisticUpdate => { + rpc_light_client_optimistic_update_limits_by_fork(fork_context.current_fork()) + } + Protocol::LightClientFinalityUpdate => { + rpc_light_client_finality_update_limits_by_fork(fork_context.current_fork()) + } } } @@ -416,7 +524,9 @@ impl ProtocolId { | SupportedProtocol::BlocksByRootV2 | SupportedProtocol::BlobsByRangeV1 | SupportedProtocol::BlobsByRootV1 - | SupportedProtocol::LightClientBootstrapV1 => true, + | SupportedProtocol::LightClientBootstrapV1 + | SupportedProtocol::LightClientOptimisticUpdateV1 + | SupportedProtocol::LightClientFinalityUpdateV1 => true, SupportedProtocol::StatusV1 | SupportedProtocol::BlocksByRootV1 | SupportedProtocol::BlocksByRangeV1 @@ -447,10 +557,10 @@ impl ProtocolId { } } -pub fn rpc_blob_limits() -> RpcLimits { +pub fn rpc_blob_limits() -> RpcLimits { RpcLimits::new( - BlobSidecar::::empty().as_ssz_bytes().len(), - BlobSidecar::::max_size(), + BlobSidecar::::empty().as_ssz_bytes().len(), + BlobSidecar::::max_size(), ) } @@ -459,16 +569,16 @@ pub fn rpc_blob_limits() -> RpcLimits { // The inbound protocol reads the request, decodes it and returns the stream to the protocol // handler to respond to once ready. -pub type InboundOutput = (InboundRequest, InboundFramed); -pub type InboundFramed = - Framed>>>, InboundCodec>; +pub type InboundOutput = (InboundRequest, InboundFramed); +pub type InboundFramed = + Framed>>>, InboundCodec>; -impl InboundUpgrade for RPCProtocol +impl InboundUpgrade for RPCProtocol where TSocket: AsyncRead + AsyncWrite + Unpin + Send + 'static, - TSpec: EthSpec, + E: EthSpec, { - type Output = InboundOutput; + type Output = InboundOutput; type Error = RPCError; type Future = BoxFuture<'static, Result>; @@ -500,6 +610,12 @@ where SupportedProtocol::MetaDataV2 => { Ok((InboundRequest::MetaData(MetadataRequest::new_v2()), socket)) } + SupportedProtocol::LightClientOptimisticUpdateV1 => { + Ok((InboundRequest::LightClientOptimisticUpdate, socket)) + } + SupportedProtocol::LightClientFinalityUpdateV1 => { + Ok((InboundRequest::LightClientFinalityUpdate, socket)) + } _ => { match tokio::time::timeout( Duration::from_secs(REQUEST_TIMEOUT), @@ -520,7 +636,7 @@ where } #[derive(Debug, Clone, PartialEq)] -pub enum InboundRequest { +pub enum InboundRequest { Status(StatusMessage), Goodbye(GoodbyeReason), BlocksByRange(OldBlocksByRangeRequest), @@ -528,26 +644,30 @@ pub enum InboundRequest { BlobsByRange(BlobsByRangeRequest), BlobsByRoot(BlobsByRootRequest), LightClientBootstrap(LightClientBootstrapRequest), + LightClientOptimisticUpdate, + LightClientFinalityUpdate, Ping(Ping), - MetaData(MetadataRequest), + MetaData(MetadataRequest), } /// Implements the encoding per supported protocol for `RPCRequest`. -impl InboundRequest { +impl InboundRequest { /* These functions are used in the handler for stream management */ - /// Number of responses expected for this request. - pub fn expected_responses(&self) -> u64 { + /// Maximum number of responses expected for this request. + pub fn max_responses(&self) -> u64 { match self { InboundRequest::Status(_) => 1, InboundRequest::Goodbye(_) => 0, InboundRequest::BlocksByRange(req) => *req.count(), InboundRequest::BlocksByRoot(req) => req.block_roots().len() as u64, - InboundRequest::BlobsByRange(req) => req.max_blobs_requested::(), + InboundRequest::BlobsByRange(req) => req.max_blobs_requested::(), InboundRequest::BlobsByRoot(req) => req.blob_ids.len() as u64, InboundRequest::Ping(_) => 1, InboundRequest::MetaData(_) => 1, InboundRequest::LightClientBootstrap(_) => 1, + InboundRequest::LightClientOptimisticUpdate => 1, + InboundRequest::LightClientFinalityUpdate => 1, } } @@ -572,6 +692,12 @@ impl InboundRequest { MetadataRequest::V2(_) => SupportedProtocol::MetaDataV2, }, InboundRequest::LightClientBootstrap(_) => SupportedProtocol::LightClientBootstrapV1, + InboundRequest::LightClientOptimisticUpdate => { + SupportedProtocol::LightClientOptimisticUpdateV1 + } + InboundRequest::LightClientFinalityUpdate => { + SupportedProtocol::LightClientFinalityUpdateV1 + } } } @@ -590,6 +716,8 @@ impl InboundRequest { InboundRequest::Ping(_) => unreachable!(), InboundRequest::MetaData(_) => unreachable!(), InboundRequest::LightClientBootstrap(_) => unreachable!(), + InboundRequest::LightClientFinalityUpdate => unreachable!(), + InboundRequest::LightClientOptimisticUpdate => unreachable!(), } } } @@ -684,7 +812,7 @@ impl std::error::Error for RPCError { } } -impl std::fmt::Display for InboundRequest { +impl std::fmt::Display for InboundRequest { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { InboundRequest::Status(status) => write!(f, "Status Message: {}", status), @@ -696,7 +824,13 @@ impl std::fmt::Display for InboundRequest { InboundRequest::Ping(ping) => write!(f, "Ping: {}", ping.data), InboundRequest::MetaData(_) => write!(f, "MetaData request"), InboundRequest::LightClientBootstrap(bootstrap) => { - write!(f, "LightClientBootstrap: {}", bootstrap.root) + write!(f, "Light client boostrap: {}", bootstrap.root) + } + InboundRequest::LightClientOptimisticUpdate => { + write!(f, "Light client optimistic update request") + } + InboundRequest::LightClientFinalityUpdate => { + write!(f, "Light client finality update request") } } } diff --git a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs index 0b57374e8b..b304eb546d 100644 --- a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs @@ -3,7 +3,6 @@ use crate::rpc::Protocol; use fnv::FnvHashMap; use libp2p::PeerId; use serde::{Deserialize, Serialize}; -use std::convert::TryInto; use std::future::Future; use std::hash::Hash; use std::pin::Pin; @@ -99,7 +98,11 @@ pub struct RPCRateLimiter { /// BlobsByRoot rate limiter. blbroot_rl: Limiter, /// LightClientBootstrap rate limiter. - lcbootstrap_rl: Limiter, + lc_bootstrap_rl: Limiter, + /// LightClientOptimisticUpdate rate limiter. + lc_optimistic_update_rl: Limiter, + /// LightClientFinalityUpdate rate limiter. + lc_finality_update_rl: Limiter, } /// Error type for non conformant requests @@ -132,6 +135,10 @@ pub struct RPCRateLimiterBuilder { blbroot_quota: Option, /// Quota for the LightClientBootstrap protocol. lcbootstrap_quota: Option, + /// Quota for the LightClientOptimisticUpdate protocol. + lc_optimistic_update_quota: Option, + /// Quota for the LightClientOptimisticUpdate protocol. + lc_finality_update_quota: Option, } impl RPCRateLimiterBuilder { @@ -148,6 +155,8 @@ impl RPCRateLimiterBuilder { Protocol::BlobsByRange => self.blbrange_quota = q, Protocol::BlobsByRoot => self.blbroot_quota = q, Protocol::LightClientBootstrap => self.lcbootstrap_quota = q, + Protocol::LightClientOptimisticUpdate => self.lc_optimistic_update_quota = q, + Protocol::LightClientFinalityUpdate => self.lc_finality_update_quota = q, } self } @@ -164,9 +173,15 @@ impl RPCRateLimiterBuilder { let bbrange_quota = self .bbrange_quota .ok_or("BlocksByRange quota not specified")?; - let lcbootstrap_quote = self + let lc_bootstrap_quota = self .lcbootstrap_quota .ok_or("LightClientBootstrap quota not specified")?; + let lc_optimistic_update_quota = self + .lc_optimistic_update_quota + .ok_or("LightClientOptimisticUpdate quota not specified")?; + let lc_finality_update_quota = self + .lc_finality_update_quota + .ok_or("LightClientFinalityUpdate quota not specified")?; let blbrange_quota = self .blbrange_quota @@ -185,7 +200,9 @@ impl RPCRateLimiterBuilder { let bbrange_rl = Limiter::from_quota(bbrange_quota)?; let blbrange_rl = Limiter::from_quota(blbrange_quota)?; let blbroot_rl = Limiter::from_quota(blbroots_quota)?; - let lcbootstrap_rl = Limiter::from_quota(lcbootstrap_quote)?; + let lc_bootstrap_rl = Limiter::from_quota(lc_bootstrap_quota)?; + let lc_optimistic_update_rl = Limiter::from_quota(lc_optimistic_update_quota)?; + let lc_finality_update_rl = Limiter::from_quota(lc_finality_update_quota)?; // check for peers to prune every 30 seconds, starting in 30 seconds let prune_every = tokio::time::Duration::from_secs(30); @@ -201,7 +218,9 @@ impl RPCRateLimiterBuilder { bbrange_rl, blbrange_rl, blbroot_rl, - lcbootstrap_rl, + lc_bootstrap_rl, + lc_optimistic_update_rl, + lc_finality_update_rl, init_time: Instant::now(), }) } @@ -209,26 +228,26 @@ impl RPCRateLimiterBuilder { pub trait RateLimiterItem { fn protocol(&self) -> Protocol; - fn expected_responses(&self) -> u64; + fn max_responses(&self) -> u64; } -impl RateLimiterItem for super::InboundRequest { +impl RateLimiterItem for super::InboundRequest { fn protocol(&self) -> Protocol { self.versioned_protocol().protocol() } - fn expected_responses(&self) -> u64 { - self.expected_responses() + fn max_responses(&self) -> u64 { + self.max_responses() } } -impl RateLimiterItem for super::OutboundRequest { +impl RateLimiterItem for super::OutboundRequest { fn protocol(&self) -> Protocol { self.versioned_protocol().protocol() } - fn expected_responses(&self) -> u64 { - self.expected_responses() + fn max_responses(&self) -> u64 { + self.max_responses() } } impl RPCRateLimiter { @@ -244,6 +263,8 @@ impl RPCRateLimiter { blobs_by_range_quota, blobs_by_root_quota, light_client_bootstrap_quota, + light_client_optimistic_update_quota, + light_client_finality_update_quota, } = config; Self::builder() @@ -256,6 +277,14 @@ impl RPCRateLimiter { .set_quota(Protocol::BlobsByRange, blobs_by_range_quota) .set_quota(Protocol::BlobsByRoot, blobs_by_root_quota) .set_quota(Protocol::LightClientBootstrap, light_client_bootstrap_quota) + .set_quota( + Protocol::LightClientOptimisticUpdate, + light_client_optimistic_update_quota, + ) + .set_quota( + Protocol::LightClientFinalityUpdate, + light_client_finality_update_quota, + ) .build() } @@ -270,7 +299,7 @@ impl RPCRateLimiter { request: &Item, ) -> Result<(), RateLimitedErr> { let time_since_start = self.init_time.elapsed(); - let tokens = request.expected_responses().max(1); + let tokens = request.max_responses().max(1); let check = |limiter: &mut Limiter| limiter.allows(time_since_start, peer_id, tokens); @@ -283,7 +312,9 @@ impl RPCRateLimiter { Protocol::BlocksByRoot => &mut self.bbroots_rl, Protocol::BlobsByRange => &mut self.blbrange_rl, Protocol::BlobsByRoot => &mut self.blbroot_rl, - Protocol::LightClientBootstrap => &mut self.lcbootstrap_rl, + Protocol::LightClientBootstrap => &mut self.lc_bootstrap_rl, + Protocol::LightClientOptimisticUpdate => &mut self.lc_optimistic_update_rl, + Protocol::LightClientFinalityUpdate => &mut self.lc_finality_update_rl, }; check(limiter) } diff --git a/beacon_node/lighthouse_network/src/rpc/self_limiter.rs b/beacon_node/lighthouse_network/src/rpc/self_limiter.rs index 4348c1ec6d..e845a775cb 100644 --- a/beacon_node/lighthouse_network/src/rpc/self_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/self_limiter.rs @@ -19,22 +19,22 @@ use super::{ /// A request that was rate limited or waiting on rate limited requests for the same peer and /// protocol. -struct QueuedRequest { - req: OutboundRequest, +struct QueuedRequest { + req: OutboundRequest, request_id: Id, } -pub(crate) struct SelfRateLimiter { +pub(crate) struct SelfRateLimiter { /// Requests queued for sending per peer. This requests are stored when the self rate /// limiter rejects them. Rate limiting is based on a Peer and Protocol basis, therefore /// are stored in the same way. - delayed_requests: HashMap<(PeerId, Protocol), VecDeque>>, + delayed_requests: HashMap<(PeerId, Protocol), VecDeque>>, /// The delay required to allow a peer's outbound request per protocol. next_peer_request: DelayQueue<(PeerId, Protocol)>, /// Rate limiter for our own requests. limiter: RateLimiter, /// Requests that are ready to be sent. - ready_requests: SmallVec<[BehaviourAction; 3]>, + ready_requests: SmallVec<[BehaviourAction; 3]>, /// Slog logger. log: Logger, } @@ -48,7 +48,7 @@ pub enum Error { RateLimited, } -impl SelfRateLimiter { +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); @@ -70,8 +70,8 @@ impl SelfRateLimiter { &mut self, peer_id: PeerId, request_id: Id, - req: OutboundRequest, - ) -> Result, Error> { + req: OutboundRequest, + ) -> Result, Error> { 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)) { @@ -101,9 +101,9 @@ impl SelfRateLimiter { limiter: &mut RateLimiter, peer_id: PeerId, request_id: Id, - req: OutboundRequest, + req: OutboundRequest, log: &Logger, - ) -> Result, (QueuedRequest, Duration)> { + ) -> Result, (QueuedRequest, Duration)> { match limiter.allows(&peer_id, &req) { Ok(()) => Ok(BehaviourAction::NotifyHandler { peer_id, @@ -160,7 +160,7 @@ impl SelfRateLimiter { } } - pub fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + pub fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { // First check the requests that were self rate limited, since those might add events to // the queue. Also do this this before rate limiter prunning to avoid removing and // immediately adding rate limiting keys. diff --git a/beacon_node/lighthouse_network/src/service/api_types.rs b/beacon_node/lighthouse_network/src/service/api_types.rs index 96c9d28332..2ea4150248 100644 --- a/beacon_node/lighthouse_network/src/service/api_types.rs +++ b/beacon_node/lighthouse_network/src/service/api_types.rs @@ -1,7 +1,10 @@ use std::sync::Arc; use libp2p::swarm::ConnectionId; -use types::{BlobSidecar, EthSpec, LightClientBootstrap, SignedBeaconBlock}; +use types::{ + BlobSidecar, EthSpec, LightClientBootstrap, LightClientFinalityUpdate, + LightClientOptimisticUpdate, SignedBeaconBlock, +}; use crate::rpc::methods::{BlobsByRangeRequest, BlobsByRootRequest}; use crate::rpc::{ @@ -40,12 +43,16 @@ pub enum Request { BlocksByRoot(BlocksByRootRequest), // light client bootstrap request LightClientBootstrap(LightClientBootstrapRequest), + // light client optimistic update request + LightClientOptimisticUpdate, + // light client finality update request + LightClientFinalityUpdate, /// A request blobs root request. BlobsByRoot(BlobsByRootRequest), } -impl std::convert::From for OutboundRequest { - fn from(req: Request) -> OutboundRequest { +impl std::convert::From for OutboundRequest { + fn from(req: Request) -> OutboundRequest { match req { Request::BlocksByRoot(r) => OutboundRequest::BlocksByRoot(r), Request::BlocksByRange(r) => match r { @@ -64,7 +71,9 @@ impl std::convert::From for OutboundRequest { }), ), }, - Request::LightClientBootstrap(_) => { + Request::LightClientBootstrap(_) + | Request::LightClientOptimisticUpdate + | Request::LightClientFinalityUpdate => { unreachable!("Lighthouse never makes an outbound light client request") } Request::BlobsByRange(r) => OutboundRequest::BlobsByRange(r), @@ -81,23 +90,27 @@ impl std::convert::From for OutboundRequest { // Behaviour. For all protocol reponses managed by RPC see `RPCResponse` and // `RPCCodedResponse`. #[derive(Debug, Clone, PartialEq)] -pub enum Response { +pub enum Response { /// A Status message. Status(StatusMessage), /// A response to a get BLOCKS_BY_RANGE request. A None response signals the end of the batch. - BlocksByRange(Option>>), + BlocksByRange(Option>>), /// A response to a get BLOBS_BY_RANGE request. A None response signals the end of the batch. - BlobsByRange(Option>>), + BlobsByRange(Option>>), /// A response to a get BLOCKS_BY_ROOT request. - BlocksByRoot(Option>>), + BlocksByRoot(Option>>), /// A response to a get BLOBS_BY_ROOT request. - BlobsByRoot(Option>>), + BlobsByRoot(Option>>), /// A response to a LightClientUpdate request. - LightClientBootstrap(LightClientBootstrap), + LightClientBootstrap(Arc>), + /// A response to a LightClientOptimisticUpdate request. + LightClientOptimisticUpdate(Arc>), + /// A response to a LightClientFinalityUpdate request. + LightClientFinalityUpdate(Arc>), } -impl std::convert::From> for RPCCodedResponse { - fn from(resp: Response) -> RPCCodedResponse { +impl std::convert::From> for RPCCodedResponse { + fn from(resp: Response) -> RPCCodedResponse { match resp { Response::BlocksByRoot(r) => match r { Some(b) => RPCCodedResponse::Success(RPCResponse::BlocksByRoot(b)), @@ -119,6 +132,12 @@ impl std::convert::From> for RPCCodedResponse { RPCCodedResponse::Success(RPCResponse::LightClientBootstrap(b)) } + Response::LightClientOptimisticUpdate(o) => { + RPCCodedResponse::Success(RPCResponse::LightClientOptimisticUpdate(o)) + } + Response::LightClientFinalityUpdate(f) => { + RPCCodedResponse::Success(RPCResponse::LightClientFinalityUpdate(f)) + } } } } diff --git a/beacon_node/lighthouse_network/src/service/behaviour.rs b/beacon_node/lighthouse_network/src/service/behaviour.rs index 8dd750429c..90121ffbfb 100644 --- a/beacon_node/lighthouse_network/src/service/behaviour.rs +++ b/beacon_node/lighthouse_network/src/service/behaviour.rs @@ -3,9 +3,10 @@ use crate::peer_manager::PeerManager; use crate::rpc::{ReqId, RPC}; use crate::types::SnappyTransform; -use libp2p::gossipsub; use libp2p::identify; +use libp2p::swarm::behaviour::toggle::Toggle; use libp2p::swarm::NetworkBehaviour; +use libp2p::upnp::tokio::Behaviour as Upnp; use types::EthSpec; use super::api_types::RequestId; @@ -15,23 +16,25 @@ pub type SubscriptionFilter = pub type Gossipsub = gossipsub::Behaviour; #[derive(NetworkBehaviour)] -pub(crate) struct Behaviour +pub(crate) struct Behaviour where AppReqId: ReqId, - TSpec: EthSpec, + E: EthSpec, { /// 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 peer manager that keeps track of peer's reputation and status. + pub peer_manager: PeerManager, /// The Eth2 RPC specified in the wire-0 protocol. - pub eth2_rpc: RPC, TSpec>, + pub eth2_rpc: RPC, E>, /// Discv5 Discovery protocol. - pub discovery: Discovery, + pub discovery: Discovery, /// 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::Behaviour, - /// The peer manager that keeps track of peer's reputation and status. - pub peer_manager: PeerManager, + /// Libp2p UPnP port mapping. + pub upnp: Toggle, + /// The routing pub-sub mechanism for eth2. + pub gossipsub: Gossipsub, } diff --git a/beacon_node/lighthouse_network/src/service/gossip_cache.rs b/beacon_node/lighthouse_network/src/service/gossip_cache.rs index 5dc0d29ff5..225b4ef8dd 100644 --- a/beacon_node/lighthouse_network/src/service/gossip_cache.rs +++ b/beacon_node/lighthouse_network/src/service/gossip_cache.rs @@ -268,8 +268,6 @@ impl futures::stream::Stream for GossipCache { #[cfg(test)] mod tests { - use crate::types::GossipKind; - use super::*; use futures::stream::StreamExt; 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 47c2c9e56a..c6a764bb0e 100644 --- a/beacon_node/lighthouse_network/src/service/gossipsub_scoring_parameters.rs +++ b/beacon_node/lighthouse_network/src/service/gossipsub_scoring_parameters.rs @@ -1,9 +1,6 @@ use crate::types::{GossipEncoding, GossipKind, GossipTopic}; use crate::{error, TopicHash}; -use libp2p::gossipsub::{ - Config as GossipsubConfig, IdentTopic as Topic, PeerScoreParams, PeerScoreThresholds, - TopicScoreParams, -}; +use gossipsub::{IdentTopic as Topic, PeerScoreParams, PeerScoreThresholds, TopicScoreParams}; use std::cmp::max; use std::collections::HashMap; use std::marker::PhantomData; @@ -35,7 +32,7 @@ pub fn lighthouse_gossip_thresholds() -> PeerScoreThresholds { } } -pub struct PeerScoreSettings { +pub struct PeerScoreSettings { slot: Duration, epoch: Duration, @@ -50,11 +47,11 @@ pub struct PeerScoreSettings { target_committee_size: usize, target_aggregators_per_committee: usize, attestation_subnet_count: u64, - phantom: PhantomData, + phantom: PhantomData, } -impl PeerScoreSettings { - pub fn new(chain_spec: &ChainSpec, gs_config: &GossipsubConfig) -> PeerScoreSettings { +impl PeerScoreSettings { + pub fn new(chain_spec: &ChainSpec, mesh_n: usize) -> PeerScoreSettings { let slot = Duration::from_secs(chain_spec.seconds_per_slot); let beacon_attestation_subnet_weight = 1.0 / chain_spec.attestation_subnet_count as f64; let max_positive_score = (MAX_IN_MESH_SCORE + MAX_FIRST_MESSAGE_DELIVERIES_SCORE) @@ -67,12 +64,12 @@ impl PeerScoreSettings { PeerScoreSettings { slot, - epoch: slot * TSpec::slots_per_epoch() as u32, + epoch: slot * E::slots_per_epoch() as u32, beacon_attestation_subnet_weight, max_positive_score, decay_interval: max(Duration::from_secs(1), slot), decay_to_zero: 0.01, - mesh_n: gs_config.mesh_n(), + mesh_n, max_committees_per_slot: chain_spec.max_committees_per_slot, target_committee_size: chain_spec.target_committee_size, target_aggregators_per_committee: chain_spec.target_aggregators_per_committee as usize, @@ -104,7 +101,7 @@ impl PeerScoreSettings { let target_value = Self::decay_convergence( params.behaviour_penalty_decay, - 10.0 / TSpec::slots_per_epoch() as f64, + 10.0 / E::slots_per_epoch() as f64, ) - params.behaviour_penalty_threshold; params.behaviour_penalty_weight = thresholds.gossip_threshold / target_value.powi(2); @@ -125,7 +122,7 @@ impl PeerScoreSettings { Self::get_topic_params( self, VOLUNTARY_EXIT_WEIGHT, - 4.0 / TSpec::slots_per_epoch() as f64, + 4.0 / E::slots_per_epoch() as f64, self.epoch * 100, None, ), @@ -135,7 +132,7 @@ impl PeerScoreSettings { Self::get_topic_params( self, ATTESTER_SLASHING_WEIGHT, - 1.0 / 5.0 / TSpec::slots_per_epoch() as f64, + 1.0 / 5.0 / E::slots_per_epoch() as f64, self.epoch * 100, None, ), @@ -145,7 +142,7 @@ impl PeerScoreSettings { Self::get_topic_params( self, PROPOSER_SLASHING_WEIGHT, - 1.0 / 5.0 / TSpec::slots_per_epoch() as f64, + 1.0 / 5.0 / E::slots_per_epoch() as f64, self.epoch * 100, None, ), @@ -181,15 +178,15 @@ impl PeerScoreSettings { ) -> error::Result<(TopicScoreParams, TopicScoreParams, TopicScoreParams)> { let (aggregators_per_slot, committees_per_slot) = self.expected_aggregator_count_per_slot(active_validators)?; - let multiple_bursts_per_subnet_per_epoch = committees_per_slot as u64 - >= 2 * self.attestation_subnet_count / TSpec::slots_per_epoch(); + let multiple_bursts_per_subnet_per_epoch = + committees_per_slot as u64 >= 2 * self.attestation_subnet_count / E::slots_per_epoch(); let beacon_block_params = Self::get_topic_params( self, BEACON_BLOCK_WEIGHT, 1.0, self.epoch * 20, - Some((TSpec::slots_per_epoch() * 5, 3.0, self.epoch, current_slot)), + Some((E::slots_per_epoch() * 5, 3.0, self.epoch, current_slot)), ); let beacon_aggregate_proof_params = Self::get_topic_params( @@ -197,14 +194,14 @@ impl PeerScoreSettings { BEACON_AGGREGATE_PROOF_WEIGHT, aggregators_per_slot, self.epoch, - Some((TSpec::slots_per_epoch() * 2, 4.0, self.epoch, current_slot)), + Some((E::slots_per_epoch() * 2, 4.0, self.epoch, current_slot)), ); let beacon_attestation_subnet_params = Self::get_topic_params( self, self.beacon_attestation_subnet_weight, active_validators as f64 / self.attestation_subnet_count as f64 - / TSpec::slots_per_epoch() as f64, + / E::slots_per_epoch() as f64, self.epoch * (if multiple_bursts_per_subnet_per_epoch { 1 @@ -212,7 +209,7 @@ impl PeerScoreSettings { 4 }), Some(( - TSpec::slots_per_epoch() + E::slots_per_epoch() * (if multiple_bursts_per_subnet_per_epoch { 4 } else { @@ -220,7 +217,7 @@ impl PeerScoreSettings { }), 16.0, if multiple_bursts_per_subnet_per_epoch { - self.slot * (TSpec::slots_per_epoch() as u32 / 2 + 1) + self.slot * (E::slots_per_epoch() as u32 / 2 + 1) } else { self.epoch * 3 }, @@ -260,14 +257,14 @@ impl PeerScoreSettings { &self, active_validators: usize, ) -> error::Result<(f64, usize)> { - let committees_per_slot = TSpec::get_committee_count_per_slot_with( + let committees_per_slot = E::get_committee_count_per_slot_with( active_validators, self.max_committees_per_slot, self.target_committee_size, ) .map_err(|e| format!("Could not get committee count from spec: {:?}", e))?; - let committees = committees_per_slot * TSpec::slots_per_epoch() as usize; + let committees = committees_per_slot * E::slots_per_epoch() as usize; let smaller_committee_size = active_validators / committees; let num_larger_committees = active_validators - smaller_committee_size * committees; @@ -286,7 +283,7 @@ impl PeerScoreSettings { / modulo_smaller as f64 + (num_larger_committees * (smaller_committee_size + 1)) as f64 / modulo_larger as f64) - / TSpec::slots_per_epoch() as f64, + / E::slots_per_epoch() as f64, committees_per_slot, )) } diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index 2b20c76cf4..f91a5b471a 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -14,24 +14,24 @@ use crate::rpc::*; use crate::service::behaviour::BehaviourEvent; pub use crate::service::behaviour::Gossipsub; use crate::types::{ - fork_core_topics, subnet_from_topic_hash, GossipEncoding, GossipKind, GossipTopic, - SnappyTransform, Subnet, SubnetDiscovery, ALTAIR_CORE_TOPICS, BASE_CORE_TOPICS, - CAPELLA_CORE_TOPICS, DENEB_CORE_TOPICS, LIGHT_CLIENT_GOSSIP_TOPICS, + attestation_sync_committee_topics, fork_core_topics, subnet_from_topic_hash, GossipEncoding, + GossipKind, GossipTopic, SnappyTransform, Subnet, SubnetDiscovery, ALTAIR_CORE_TOPICS, + BASE_CORE_TOPICS, CAPELLA_CORE_TOPICS, DENEB_CORE_TOPICS, LIGHT_CLIENT_GOSSIP_TOPICS, }; use crate::EnrExt; use crate::Eth2Enr; use crate::{error, metrics, Enr, NetworkGlobals, PubsubMessage, TopicHash}; use api_types::{PeerRequestId, Request, RequestId, Response}; use futures::stream::StreamExt; -use gossipsub_scoring_parameters::{lighthouse_gossip_thresholds, PeerScoreSettings}; -use libp2p::gossipsub::{ - self, IdentTopic as Topic, MessageAcceptance, MessageAuthenticity, MessageId, PublishError, +use gossipsub::{ + IdentTopic as Topic, MessageAcceptance, MessageAuthenticity, MessageId, PublishError, TopicScoreParams, }; -use libp2p::multiaddr::{Multiaddr, Protocol as MProtocol}; +use gossipsub_scoring_parameters::{lighthouse_gossip_thresholds, PeerScoreSettings}; +use libp2p::multiaddr::{self, Multiaddr, Protocol as MProtocol}; +use libp2p::swarm::behaviour::toggle::Toggle; use libp2p::swarm::{Swarm, SwarmEvent}; -use libp2p::PeerId; -use libp2p::{identify, SwarmBuilder}; +use libp2p::{identify, PeerId, SwarmBuilder}; use slog::{crit, debug, info, o, trace, warn}; use std::path::PathBuf; use std::pin::Pin; @@ -51,13 +51,13 @@ mod gossip_cache; pub mod gossipsub_scoring_parameters; pub mod utils; /// The number of peers we target per subnet for discovery queries. -pub const TARGET_SUBNET_PEERS: usize = 6; +pub const TARGET_SUBNET_PEERS: usize = 3; const MAX_IDENTIFY_ADDRESSES: usize = 10; /// The types of events than can be obtained from polling the behaviour. #[derive(Debug)] -pub enum NetworkEvent { +pub enum NetworkEvent { /// We have successfully dialed and connected to a peer. PeerConnectedOutgoing(PeerId), /// A peer has successfully dialed and connected to us. @@ -87,7 +87,7 @@ pub enum NetworkEvent { /// Id of the request to which the peer is responding. id: AppReqId, /// Response the peer sent. - response: Response, + response: Response, }, PubsubMessage { /// The gossipsub message id. Used when propagating blocks after validation. @@ -97,7 +97,7 @@ pub enum NetworkEvent { /// The topic that this message was sent on. topic: TopicHash, /// The message itself. - message: PubsubMessage, + message: PubsubMessage, }, /// Inform the network to send a Status to this peer. StatusPeer(PeerId), @@ -108,11 +108,11 @@ pub enum NetworkEvent { /// Builds the network behaviour that manages the core protocols of eth2. /// This core behaviour is managed by `Behaviour` which adds peer management to all core /// behaviours. -pub struct Network { - swarm: libp2p::swarm::Swarm>, +pub struct Network { + swarm: libp2p::swarm::Swarm>, /* Auxiliary Fields */ /// A collections of variables accessible outside the network service. - network_globals: Arc>, + network_globals: Arc>, /// Keeps track of the current EnrForkId for upgrading gossipsub topics. // NOTE: This can be accessed via the network_globals ENR. However we keep it here for quick // lookups for every gossipsub message send. @@ -121,36 +121,42 @@ pub struct Network { network_dir: PathBuf, fork_context: Arc, /// Gossipsub score parameters. - score_settings: PeerScoreSettings, + score_settings: PeerScoreSettings, /// The interval for updating gossipsub scores update_gossipsub_scores: tokio::time::Interval, gossip_cache: GossipCache, /// This node's PeerId. pub local_peer_id: PeerId, - /// Flag to disable warning logs for duplicate gossip messages and log at DEBUG level instead. - pub disable_duplicate_warn_logs: bool, /// Logger for behaviour actions. log: slog::Logger, } /// Implements the combined behaviour for the libp2p service. -impl Network { +impl Network { pub async fn new( executor: task_executor::TaskExecutor, mut ctx: ServiceContext<'_>, log: &slog::Logger, - ) -> error::Result<(Self, Arc>)> { + ) -> error::Result<(Self, Arc>)> { let log = log.new(o!("service"=> "libp2p")); - let mut config = ctx.config.clone(); + let config = ctx.config.clone(); trace!(log, "Libp2p Service starting"); // initialise the node's ID let local_keypair = utils::load_private_key(&config, &log); + // Trusted peers will also be marked as explicit in GossipSub. + // Cfr. https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.1.md#explicit-peering-agreements + let trusted_peers: Vec = config + .trusted_peers + .iter() + .map(|x| PeerId::from(x.clone())) + .collect(); + // set up a collection of variables accessible outside of the network crate let network_globals = { // Create an ENR or load from disk if appropriate - let enr = crate::discovery::enr::build_or_load_enr::( + let enr = crate::discovery::enr::build_or_load_enr::( local_keypair.clone(), &config, &ctx.enr_fork_id, @@ -161,11 +167,7 @@ impl Network { let globals = NetworkGlobals::new( enr, meta_data, - config - .trusted_peers - .iter() - .map(|x| PeerId::from(x.clone())) - .collect(), + trusted_peers, config.disable_peer_scoring, &log, ); @@ -178,12 +180,24 @@ impl Network { .eth2() .expect("Local ENR must have a fork id"); - let score_settings = PeerScoreSettings::new(ctx.chain_spec, &config.gs_config); + 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, + }; + let gs_config = gossipsub_config( + config.network_load, + ctx.fork_context.clone(), + gossipsub_config_params, + ctx.chain_spec.seconds_per_slot, + E::slots_per_epoch(), + ); + + let score_settings = PeerScoreSettings::new(ctx.chain_spec, gs_config.mesh_n()); let gossip_cache = { let slot_duration = std::time::Duration::from_secs(ctx.chain_spec.seconds_per_slot); let half_epoch = std::time::Duration::from_secs( - ctx.chain_spec.seconds_per_slot * TSpec::slots_per_epoch() / 2, + ctx.chain_spec.seconds_per_slot * E::slots_per_epoch() / 2, ); GossipCache::builder() @@ -208,7 +222,7 @@ impl Network { let params = { // Construct a set of gossipsub peer scoring parameters // We don't know the number of active validators and the current slot yet - let active_validators = TSpec::minimum_validator_count(); + let active_validators = E::minimum_validator_count(); let current_slot = Slot::new(0); score_settings.get_peer_score_params( active_validators, @@ -246,16 +260,6 @@ impl Network { max_subscriptions_per_request: max_topics * 2, }; - 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 libp2p build the configuration let gossipsub_metrics = ctx.libp2p_registry.as_mut().map(|registry| { ( @@ -264,10 +268,10 @@ impl Network { ) }); - let snappy_transform = SnappyTransform::new(config.gs_config.max_transmit_size()); + let snappy_transform = SnappyTransform::new(gs_config.max_transmit_size()); let mut gossipsub = Gossipsub::new_with_subscription_filter_and_transform( MessageAuthenticity::Anonymous, - config.gs_config.clone(), + gs_config.clone(), gossipsub_metrics, filter, snappy_transform, @@ -278,6 +282,27 @@ impl Network { .with_peer_score(params, thresholds) .expect("Valid score params and thresholds"); + // Mark trusted peers as explicit. + for explicit_peer in config.trusted_peers.iter() { + gossipsub.add_explicit_peer(&PeerId::from(explicit_peer.clone())); + } + + // If we are using metrics, then register which topics we want to make sure to keep + // track of + if ctx.libp2p_registry.is_some() { + let topics_to_keep_metrics_for = attestation_sync_committee_topics::() + .map(|gossip_kind| { + Topic::from(GossipTopic::new( + gossip_kind, + GossipEncoding::default(), + enr_fork_id.fork_digest, + )) + .into() + }) + .collect::>(); + gossipsub.register_topics_for_metrics(topics_to_keep_metrics_for); + } + (gossipsub, update_gossipsub_scores) }; @@ -328,6 +353,7 @@ impl Network { let peer_manager = { let peer_manager_cfg = PeerManagerCfg { discovery_enabled: !config.disable_discovery, + quic_enabled: !config.disable_quic_support, metrics_enabled: config.metrics_enabled, target_peer_count: config.target_peers, ..Default::default() @@ -356,6 +382,11 @@ impl Network { libp2p::connection_limits::Behaviour::new(limits) }; + let upnp = Toggle::from( + config + .upnp_enabled + .then_some(libp2p::upnp::tokio::Behaviour::default()), + ); let behaviour = { Behaviour { gossipsub, @@ -364,6 +395,7 @@ impl Network { identify, peer_manager, connection_limits, + upnp, } }; @@ -426,7 +458,6 @@ impl Network { update_gossipsub_scores, gossip_cache, local_peer_id, - disable_duplicate_warn_logs: config.disable_duplicate_warn_logs, log, }; @@ -561,11 +592,11 @@ impl Network { &mut self.swarm.behaviour_mut().gossipsub } /// The Eth2 RPC specified in the wire-0 protocol. - pub fn eth2_rpc_mut(&mut self) -> &mut RPC, TSpec> { + pub fn eth2_rpc_mut(&mut self) -> &mut RPC, E> { &mut self.swarm.behaviour_mut().eth2_rpc } /// Discv5 Discovery protocol. - pub fn discovery_mut(&mut self) -> &mut Discovery { + pub fn discovery_mut(&mut self) -> &mut Discovery { &mut self.swarm.behaviour_mut().discovery } /// Provides IP addresses and peer information. @@ -573,7 +604,7 @@ impl Network { &mut self.swarm.behaviour_mut().identify } /// The peer manager that keeps track of peer's reputation and status. - pub fn peer_manager_mut(&mut self) -> &mut PeerManager { + pub fn peer_manager_mut(&mut self) -> &mut PeerManager { &mut self.swarm.behaviour_mut().peer_manager } @@ -582,11 +613,11 @@ impl Network { &self.swarm.behaviour().gossipsub } /// The Eth2 RPC specified in the wire-0 protocol. - pub fn eth2_rpc(&self) -> &RPC, TSpec> { + pub fn eth2_rpc(&self) -> &RPC, E> { &self.swarm.behaviour().eth2_rpc } /// Discv5 Discovery protocol. - pub fn discovery(&self) -> &Discovery { + pub fn discovery(&self) -> &Discovery { &self.swarm.behaviour().discovery } /// Provides IP addresses and peer information. @@ -594,7 +625,7 @@ impl Network { &self.swarm.behaviour().identify } /// The peer manager that keeps track of peer's reputation and status. - pub fn peer_manager(&self) -> &PeerManager { + pub fn peer_manager(&self) -> &PeerManager { &self.swarm.behaviour().peer_manager } @@ -638,10 +669,24 @@ impl Network { } // Subscribe to core topics for the new fork - for kind in fork_core_topics::(&new_fork, &self.fork_context.spec) { + for kind in fork_core_topics::(&new_fork, &self.fork_context.spec) { let topic = GossipTopic::new(kind, GossipEncoding::default(), new_fork_digest); self.subscribe(topic); } + + // Register the new topics for metrics + let topics_to_keep_metrics_for = attestation_sync_committee_topics::() + .map(|gossip_kind| { + Topic::from(GossipTopic::new( + gossip_kind, + GossipEncoding::default(), + new_fork_digest, + )) + .into() + }) + .collect::>(); + self.gossipsub_mut() + .register_topics_for_metrics(topics_to_keep_metrics_for); } /// Unsubscribe from all topics that doesn't have the given fork_digest @@ -737,7 +782,7 @@ impl Network { } /// Publishes a list of messages on the pubsub (gossipsub) behaviour, choosing the encoding. - pub fn publish(&mut self, messages: Vec>) { + pub fn publish(&mut self, messages: Vec>) { for message in messages { for topic in message.topics(GossipEncoding::default(), self.enr_fork_id.fork_digest) { let message_data = message.encode(GossipEncoding::default()); @@ -745,21 +790,23 @@ impl Network { .gossipsub_mut() .publish(Topic::from(topic.clone()), message_data.clone()) { - if self.disable_duplicate_warn_logs && matches!(e, PublishError::Duplicate) { - debug!( - self.log, - "Could not publish message"; - "error" => ?e, - "kind" => %topic.kind(), - ); - } else { - warn!( - self.log, - "Could not publish message"; - "error" => ?e, - "kind" => %topic.kind(), - ); - }; + match e { + PublishError::Duplicate => { + debug!( + self.log, + "Attempted to publish duplicate message"; + "kind" => %topic.kind(), + ); + } + ref e => { + warn!( + self.log, + "Could not publish message"; + "error" => ?e, + "kind" => %topic.kind(), + ); + } + } // add to metrics match topic.kind() { @@ -879,7 +926,7 @@ impl Network { } /// Send a successful response to a peer over RPC. - pub fn send_response(&mut self, peer_id: PeerId, id: PeerRequestId, response: Response) { + pub fn send_response(&mut self, peer_id: PeerId, id: PeerRequestId, response: Response) { self.eth2_rpc_mut() .send_response(peer_id, id, response.into()) } @@ -1016,13 +1063,13 @@ impl Network { let local_attnets = self .discovery_mut() .local_enr() - .attestation_bitfield::() + .attestation_bitfield::() .expect("Local discovery must have attestation bitfield"); let local_syncnets = self .discovery_mut() .local_enr() - .sync_committee_bitfield::() + .sync_committee_bitfield::() .expect("Local discovery must have sync committee bitfield"); { @@ -1075,7 +1122,7 @@ impl Network { /// Sends a METADATA response to a peer. fn send_meta_data_response( &mut self, - req: MetadataRequest, + req: MetadataRequest, id: PeerRequestId, peer_id: PeerId, ) { @@ -1095,8 +1142,8 @@ impl Network { &mut self, id: RequestId, peer_id: PeerId, - response: Response, - ) -> Option> { + response: Response, + ) -> Option> { match id { RequestId::Application(id) => Some(NetworkEvent::ResponseReceived { peer_id, @@ -1114,7 +1161,7 @@ impl Network { id: PeerRequestId, peer_id: PeerId, request: Request, - ) -> NetworkEvent { + ) -> NetworkEvent { // Increment metrics match &request { Request::Status(_) => { @@ -1123,6 +1170,14 @@ impl Network { Request::LightClientBootstrap(_) => { metrics::inc_counter_vec(&metrics::TOTAL_RPC_REQUESTS, &["light_client_bootstrap"]) } + Request::LightClientOptimisticUpdate => metrics::inc_counter_vec( + &metrics::TOTAL_RPC_REQUESTS, + &["light_client_optimistic_update"], + ), + Request::LightClientFinalityUpdate => metrics::inc_counter_vec( + &metrics::TOTAL_RPC_REQUESTS, + &["light_client_finality_update"], + ), Request::BlocksByRange { .. } => { metrics::inc_counter_vec(&metrics::TOTAL_RPC_REQUESTS, &["blocks_by_range"]) } @@ -1146,7 +1201,7 @@ impl Network { /// 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 predicate = subnet_predicate::(vec![subnet], &self.log); let peers_to_dial: Vec = self .discovery() .cached_enrs() @@ -1172,10 +1227,7 @@ impl Network { /* Sub-behaviour event handling functions */ /// Handle a gossipsub event. - fn inject_gs_event( - &mut self, - event: gossipsub::Event, - ) -> Option> { + fn inject_gs_event(&mut self, event: gossipsub::Event) -> Option> { match event { gossipsub::Event::Message { propagation_source, @@ -1226,22 +1278,39 @@ impl Network { .publish(Topic::from(topic.clone()), data) { Ok(_) => { - warn!(self.log, "Gossip message published on retry"; "topic" => topic_str); - if let Some(v) = metrics::get_int_counter( + debug!( + self.log, + "Gossip message published on retry"; + "topic" => topic_str + ); + metrics::inc_counter_vec( &metrics::GOSSIP_LATE_PUBLISH_PER_TOPIC_KIND, &[topic_str], - ) { - v.inc() - }; + ); } - Err(e) => { - warn!(self.log, "Gossip message publish failed on retry"; "topic" => topic_str, "error" => %e); - if let Some(v) = metrics::get_int_counter( + Err(PublishError::Duplicate) => { + debug!( + self.log, + "Gossip message publish ignored on retry"; + "reason" => "duplicate", + "topic" => topic_str + ); + metrics::inc_counter_vec( &metrics::GOSSIP_FAILED_LATE_PUBLISH_PER_TOPIC_KIND, &[topic_str], - ) { - v.inc() - }; + ); + } + Err(e) => { + warn!( + self.log, + "Gossip message publish failed on retry"; + "topic" => topic_str, + "error" => %e + ); + metrics::inc_counter_vec( + &metrics::GOSSIP_FAILED_LATE_PUBLISH_PER_TOPIC_KIND, + &[topic_str], + ); } } } @@ -1299,8 +1368,8 @@ impl Network { /// Handle an RPC event. fn inject_rpc_event( &mut self, - event: RPCMessage, TSpec>, - ) -> Option> { + event: RPCMessage, E>, + ) -> Option> { let peer_id = event.peer_id; if !self.peer_manager().is_connected(&peer_id) { @@ -1447,6 +1516,22 @@ impl Network { ); Some(event) } + InboundRequest::LightClientOptimisticUpdate => { + let event = self.build_request( + peer_request_id, + peer_id, + Request::LightClientOptimisticUpdate, + ); + Some(event) + } + InboundRequest::LightClientFinalityUpdate => { + let event = self.build_request( + peer_request_id, + peer_id, + Request::LightClientFinalityUpdate, + ); + Some(event) + } } } HandlerEvent::Ok(RPCReceived::Response(id, resp)) => { @@ -1484,6 +1569,16 @@ impl Network { RPCResponse::LightClientBootstrap(bootstrap) => { self.build_response(id, peer_id, Response::LightClientBootstrap(bootstrap)) } + RPCResponse::LightClientOptimisticUpdate(update) => self.build_response( + id, + peer_id, + Response::LightClientOptimisticUpdate(update), + ), + RPCResponse::LightClientFinalityUpdate(update) => self.build_response( + id, + peer_id, + Response::LightClientFinalityUpdate(update), + ), } } HandlerEvent::Ok(RPCReceived::EndOfStream(id, termination)) => { @@ -1506,7 +1601,7 @@ impl Network { fn inject_identify_event( &mut self, event: identify::Event, - ) -> Option> { + ) -> Option> { match event { identify::Event::Received { peer_id, mut info } => { if info.listen_addrs.len() > MAX_IDENTIFY_ADDRESSES { @@ -1527,10 +1622,7 @@ impl Network { } /// Handle a peer manager event. - fn inject_pm_event( - &mut self, - event: PeerManagerEvent, - ) -> Option> { + fn inject_pm_event(&mut self, event: PeerManagerEvent) -> Option> { match event { PeerManagerEvent::PeerConnectedIncoming(peer_id) => { Some(NetworkEvent::PeerConnectedIncoming(peer_id)) @@ -1584,12 +1676,53 @@ impl Network { } } + fn inject_upnp_event(&mut self, event: libp2p::upnp::Event) { + match event { + libp2p::upnp::Event::NewExternalAddr(addr) => { + info!(self.log, "UPnP route established"; "addr" => %addr); + let mut iter = addr.iter(); + // Skip Ip address. + iter.next(); + match iter.next() { + Some(multiaddr::Protocol::Udp(udp_port)) => match iter.next() { + Some(multiaddr::Protocol::QuicV1) => { + if let Err(e) = self.discovery_mut().update_enr_quic_port(udp_port) { + warn!(self.log, "Failed to update ENR"; "error" => e); + } + } + _ => { + trace!(self.log, "UPnP address mapped multiaddr from unknown transport"; "addr" => %addr) + } + }, + Some(multiaddr::Protocol::Tcp(tcp_port)) => { + if let Err(e) = self.discovery_mut().update_enr_tcp_port(tcp_port) { + warn!(self.log, "Failed to update ENR"; "error" => e); + } + } + _ => { + trace!(self.log, "UPnP address mapped multiaddr from unknown transport"; "addr" => %addr); + } + } + } + libp2p::upnp::Event::ExpiredExternalAddr(_) => {} + libp2p::upnp::Event::GatewayNotFound => { + info!(self.log, "UPnP not available"); + } + libp2p::upnp::Event::NonRoutableGateway => { + info!( + self.log, + "UPnP is available but gateway is not exposed to public network" + ); + } + } + } + /* Networking polling */ /// Poll the p2p networking stack. /// /// This will poll the swarm and do maintenance routines. - pub fn poll_network(&mut self, cx: &mut Context) -> Poll> { + pub fn poll_network(&mut self, cx: &mut Context) -> Poll> { while let Poll::Ready(Some(swarm_event)) = self.swarm.poll_next_unpin(cx) { let maybe_event = match swarm_event { SwarmEvent::Behaviour(behaviour_event) => match behaviour_event { @@ -1606,6 +1739,10 @@ impl Network { } BehaviourEvent::Identify(ie) => self.inject_identify_event(ie), BehaviourEvent::PeerManager(pe) => self.inject_pm_event(pe), + BehaviourEvent::Upnp(e) => { + self.inject_upnp_event(e); + None + } BehaviourEvent::ConnectionLimits(le) => void::unreachable(le), }, SwarmEvent::ConnectionEstablished { .. } => None, @@ -1727,7 +1864,7 @@ impl Network { Poll::Pending } - pub async fn next_event(&mut self) -> NetworkEvent { + pub async fn next_event(&mut self) -> NetworkEvent { futures::future::poll_fn(|cx| self.poll_network(cx)).await } } diff --git a/beacon_node/lighthouse_network/src/service/utils.rs b/beacon_node/lighthouse_network/src/service/utils.rs index 34dec1ca6c..c6dbee1d2e 100644 --- a/beacon_node/lighthouse_network/src/service/utils.rs +++ b/beacon_node/lighthouse_network/src/service/utils.rs @@ -6,9 +6,7 @@ use crate::types::{ use crate::{GossipTopic, NetworkConfig}; use futures::future::Either; use libp2p::core::{multiaddr::Multiaddr, muxing::StreamMuxerBox, transport::Boxed}; -use libp2p::gossipsub; use libp2p::identity::{secp256k1, Keypair}; -use libp2p::quic; use libp2p::{core, noise, yamux, PeerId, Transport}; use prometheus_client::registry::Registry; use slog::{debug, warn}; @@ -63,8 +61,8 @@ pub fn build_transport( let transport = if quic_support { // Enables Quic // The default quic configuration suits us for now. - let quic_config = quic::Config::new(&local_private_key); - let quic = quic::tokio::Transport::new(quic_config); + let quic_config = libp2p::quic::Config::new(&local_private_key); + let quic = libp2p::quic::tokio::Transport::new(quic_config); let transport = tcp .or_transport(quic) .map(|either_output, _| match either_output { diff --git a/beacon_node/lighthouse_network/src/types/globals.rs b/beacon_node/lighthouse_network/src/types/globals.rs index 84a581d56d..f9ed2c9f74 100644 --- a/beacon_node/lighthouse_network/src/types/globals.rs +++ b/beacon_node/lighthouse_network/src/types/globals.rs @@ -9,7 +9,7 @@ use parking_lot::RwLock; use std::collections::HashSet; use types::EthSpec; -pub struct NetworkGlobals { +pub struct NetworkGlobals { /// The current local ENR. pub local_enr: RwLock, /// The local peer_id. @@ -17,9 +17,9 @@ pub struct NetworkGlobals { /// Listening multiaddrs. pub listen_multiaddrs: RwLock>, /// The collection of known peers. - pub peers: RwLock>, + pub peers: RwLock>, // The local meta data of our node. - pub local_metadata: RwLock>, + pub local_metadata: RwLock>, /// The current gossipsub topic subscriptions. pub gossipsub_subscriptions: RwLock>, /// The current sync status of the node. @@ -28,10 +28,10 @@ pub struct NetworkGlobals { pub backfill_state: RwLock, } -impl NetworkGlobals { +impl NetworkGlobals { pub fn new( enr: Enr, - local_metadata: MetaData, + local_metadata: MetaData, trusted_peers: Vec, disable_peer_scoring: bool, log: &slog::Logger, @@ -111,10 +111,7 @@ impl NetworkGlobals { } /// TESTING ONLY. Build a dummy NetworkGlobals instance. - pub fn new_test_globals( - trusted_peers: Vec, - log: &slog::Logger, - ) -> NetworkGlobals { + pub fn new_test_globals(trusted_peers: Vec, log: &slog::Logger) -> NetworkGlobals { use crate::CombinedKeyExt; let keypair = libp2p::identity::secp256k1::Keypair::generate(); let enr_key: discv5::enr::CombinedKey = discv5::enr::CombinedKey::from_secp256k1(&keypair); diff --git a/beacon_node/lighthouse_network/src/types/mod.rs b/beacon_node/lighthouse_network/src/types/mod.rs index af9e9ef45d..82558f6c97 100644 --- a/beacon_node/lighthouse_network/src/types/mod.rs +++ b/beacon_node/lighthouse_network/src/types/mod.rs @@ -7,8 +7,8 @@ mod topics; use types::{BitVector, EthSpec}; -pub type EnrAttestationBitfield = BitVector<::SubnetBitfieldLength>; -pub type EnrSyncCommitteeBitfield = BitVector<::SyncCommitteeSubnetCount>; +pub type EnrAttestationBitfield = BitVector<::SubnetBitfieldLength>; +pub type EnrSyncCommitteeBitfield = BitVector<::SyncCommitteeSubnetCount>; pub type Enr = discv5::enr::Enr; @@ -17,7 +17,7 @@ pub use pubsub::{PubsubMessage, SnappyTransform}; pub use subnet::{Subnet, SubnetDiscovery}; pub use sync_state::{BackFillState, SyncState}; pub use topics::{ - core_topics_to_subscribe, fork_core_topics, subnet_from_topic_hash, GossipEncoding, GossipKind, - GossipTopic, ALTAIR_CORE_TOPICS, BASE_CORE_TOPICS, CAPELLA_CORE_TOPICS, DENEB_CORE_TOPICS, - LIGHT_CLIENT_GOSSIP_TOPICS, + attestation_sync_committee_topics, core_topics_to_subscribe, fork_core_topics, + subnet_from_topic_hash, GossipEncoding, GossipKind, GossipTopic, ALTAIR_CORE_TOPICS, + BASE_CORE_TOPICS, CAPELLA_CORE_TOPICS, DENEB_CORE_TOPICS, LIGHT_CLIENT_GOSSIP_TOPICS, }; diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index 60fe374826..a5cf03412d 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -2,47 +2,45 @@ use crate::types::{GossipEncoding, GossipKind, GossipTopic}; use crate::TopicHash; -use libp2p::gossipsub; use snap::raw::{decompress_len, Decoder, Encoder}; use ssz::{Decode, Encode}; -use std::boxed::Box; use std::io::{Error, ErrorKind}; use std::sync::Arc; use types::{ Attestation, AttesterSlashing, BlobSidecar, EthSpec, ForkContext, ForkName, LightClientFinalityUpdate, LightClientOptimisticUpdate, ProposerSlashing, SignedAggregateAndProof, SignedBeaconBlock, SignedBeaconBlockAltair, SignedBeaconBlockBase, - SignedBeaconBlockCapella, SignedBeaconBlockDeneb, SignedBeaconBlockMerge, - SignedBlsToExecutionChange, SignedContributionAndProof, SignedVoluntaryExit, SubnetId, - SyncCommitteeMessage, SyncSubnetId, + SignedBeaconBlockCapella, SignedBeaconBlockDeneb, SignedBeaconBlockElectra, + SignedBeaconBlockMerge, SignedBlsToExecutionChange, SignedContributionAndProof, + SignedVoluntaryExit, SubnetId, SyncCommitteeMessage, SyncSubnetId, }; #[derive(Debug, Clone, PartialEq)] -pub enum PubsubMessage { +pub enum PubsubMessage { /// Gossipsub message providing notification of a new block. - BeaconBlock(Arc>), + BeaconBlock(Arc>), /// Gossipsub message providing notification of a [`BlobSidecar`] along with the subnet id where it was received. - BlobSidecar(Box<(u64, Arc>)>), + BlobSidecar(Box<(u64, Arc>)>), /// Gossipsub message providing notification of a Aggregate attestation and associated proof. - AggregateAndProofAttestation(Box>), + AggregateAndProofAttestation(Box>), /// Gossipsub message providing notification of a raw un-aggregated attestation with its shard id. - Attestation(Box<(SubnetId, Attestation)>), + Attestation(Box<(SubnetId, Attestation)>), /// Gossipsub message providing notification of a voluntary exit. VoluntaryExit(Box), /// Gossipsub message providing notification of a new proposer slashing. ProposerSlashing(Box), /// Gossipsub message providing notification of a new attester slashing. - AttesterSlashing(Box>), + AttesterSlashing(Box>), /// Gossipsub message providing notification of partially aggregated sync committee signatures. - SignedContributionAndProof(Box>), + SignedContributionAndProof(Box>), /// Gossipsub message providing notification of unaggregated sync committee signatures with its subnet id. SyncCommitteeMessage(Box<(SyncSubnetId, SyncCommitteeMessage)>), /// Gossipsub message for BLS to execution change messages. BlsToExecutionChange(Box), /// Gossipsub message providing notification of a light client finality update. - LightClientFinalityUpdate(Box>), + LightClientFinalityUpdate(Box>), /// Gossipsub message providing notification of a light client optimistic update. - LightClientOptimisticUpdate(Box>), + LightClientOptimisticUpdate(Box>), } // Implements the `DataTransform` trait of gossipsub to employ snappy compression @@ -105,7 +103,7 @@ impl gossipsub::DataTransform for SnappyTransform { } } -impl PubsubMessage { +impl PubsubMessage { /// Returns the topics that each pubsub message will be sent across, given a supported /// gossipsub encoding and fork version. pub fn topics(&self, encoding: GossipEncoding, fork_version: [u8; 4]) -> Vec { @@ -173,26 +171,30 @@ impl PubsubMessage { GossipKind::BeaconBlock => { let beacon_block = match fork_context.from_context_bytes(gossip_topic.fork_digest) { - Some(ForkName::Base) => SignedBeaconBlock::::Base( + Some(ForkName::Base) => SignedBeaconBlock::::Base( SignedBeaconBlockBase::from_ssz_bytes(data) .map_err(|e| format!("{:?}", e))?, ), - Some(ForkName::Altair) => SignedBeaconBlock::::Altair( + Some(ForkName::Altair) => SignedBeaconBlock::::Altair( SignedBeaconBlockAltair::from_ssz_bytes(data) .map_err(|e| format!("{:?}", e))?, ), - Some(ForkName::Merge) => SignedBeaconBlock::::Merge( + Some(ForkName::Merge) => SignedBeaconBlock::::Merge( SignedBeaconBlockMerge::from_ssz_bytes(data) .map_err(|e| format!("{:?}", e))?, ), - Some(ForkName::Capella) => SignedBeaconBlock::::Capella( + Some(ForkName::Capella) => SignedBeaconBlock::::Capella( SignedBeaconBlockCapella::from_ssz_bytes(data) .map_err(|e| format!("{:?}", e))?, ), - Some(ForkName::Deneb) => SignedBeaconBlock::::Deneb( + Some(ForkName::Deneb) => SignedBeaconBlock::::Deneb( SignedBeaconBlockDeneb::from_ssz_bytes(data) .map_err(|e| format!("{:?}", e))?, ), + Some(ForkName::Electra) => SignedBeaconBlock::::Electra( + SignedBeaconBlockElectra::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?, + ), None => { return Err(format!( "Unknown gossipsub fork digest: {:?}", @@ -204,7 +206,7 @@ impl PubsubMessage { } GossipKind::BlobSidecar(blob_index) => { match fork_context.from_context_bytes(gossip_topic.fork_digest) { - Some(ForkName::Deneb) => { + Some(ForkName::Deneb | ForkName::Electra) => { let blob_sidecar = Arc::new( BlobSidecar::from_ssz_bytes(data) .map_err(|e| format!("{:?}", e))?, @@ -265,17 +267,31 @@ impl PubsubMessage { ))) } GossipKind::LightClientFinalityUpdate => { - let light_client_finality_update = - LightClientFinalityUpdate::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?; + let light_client_finality_update = match fork_context.from_context_bytes(gossip_topic.fork_digest) { + Some(&fork_name) => { + LightClientFinalityUpdate::from_ssz_bytes(data, fork_name) + .map_err(|e| format!("{:?}", e))? + }, + None => return Err(format!( + "light_client_finality_update topic invalid for given fork digest {:?}", + gossip_topic.fork_digest + )), + }; Ok(PubsubMessage::LightClientFinalityUpdate(Box::new( light_client_finality_update, ))) } GossipKind::LightClientOptimisticUpdate => { - let light_client_optimistic_update = - LightClientOptimisticUpdate::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?; + let light_client_optimistic_update = match fork_context.from_context_bytes(gossip_topic.fork_digest) { + Some(&fork_name) => { + LightClientOptimisticUpdate::from_ssz_bytes(data, fork_name) + .map_err(|e| format!("{:?}", e))? + }, + None => return Err(format!( + "light_client_optimistic_update topic invalid for given fork digest {:?}", + gossip_topic.fork_digest + )), + }; Ok(PubsubMessage::LightClientOptimisticUpdate(Box::new( light_client_optimistic_update, ))) @@ -309,7 +325,7 @@ impl PubsubMessage { } } -impl std::fmt::Display for PubsubMessage { +impl std::fmt::Display for PubsubMessage { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { PubsubMessage::BeaconBlock(block) => write!( diff --git a/beacon_node/lighthouse_network/src/types/topics.rs b/beacon_node/lighthouse_network/src/types/topics.rs index b774905174..7cbb5bac57 100644 --- a/beacon_node/lighthouse_network/src/types/topics.rs +++ b/beacon_node/lighthouse_network/src/types/topics.rs @@ -1,7 +1,7 @@ -use libp2p::gossipsub::{IdentTopic as Topic, TopicHash}; +use gossipsub::{IdentTopic as Topic, TopicHash}; use serde::{Deserialize, Serialize}; use strum::AsRefStr; -use types::{ChainSpec, EthSpec, ForkName, SubnetId, SyncSubnetId}; +use types::{ChainSpec, EthSpec, ForkName, SubnetId, SyncSubnetId, Unsigned}; use crate::Subnet; @@ -43,7 +43,7 @@ pub const LIGHT_CLIENT_GOSSIP_TOPICS: [GossipKind; 2] = [ pub const DENEB_CORE_TOPICS: [GossipKind; 0] = []; /// Returns the core topics associated with each fork that are new to the previous fork -pub fn fork_core_topics(fork_name: &ForkName, spec: &ChainSpec) -> Vec { +pub fn fork_core_topics(fork_name: &ForkName, spec: &ChainSpec) -> Vec { match fork_name { ForkName::Base => BASE_CORE_TOPICS.to_vec(), ForkName::Altair => ALTAIR_CORE_TOPICS.to_vec(), @@ -59,18 +59,30 @@ pub fn fork_core_topics(fork_name: &ForkName, spec: &ChainSpec) -> V deneb_topics.append(&mut deneb_blob_topics); deneb_topics } + ForkName::Electra => vec![], } } +/// Returns all the attestation and sync committee topics, for a given fork. +pub fn attestation_sync_committee_topics() -> impl Iterator { + (0..E::SubnetBitfieldLength::to_usize()) + .map(|subnet_id| GossipKind::Attestation(SubnetId::new(subnet_id as u64))) + .chain( + (0..E::SyncCommitteeSubnetCount::to_usize()).map(|sync_committee_id| { + GossipKind::SyncCommitteeMessage(SyncSubnetId::new(sync_committee_id as u64)) + }), + ) +} + /// Returns all the topics that we need to subscribe to for a given fork /// including topics from older forks and new topics for the current fork. -pub fn core_topics_to_subscribe( +pub fn core_topics_to_subscribe( mut current_fork: ForkName, spec: &ChainSpec, ) -> Vec { - let mut topics = fork_core_topics::(¤t_fork, spec); + let mut topics = fork_core_topics::(¤t_fork, spec); while let Some(previous_fork) = current_fork.previous_fork() { - let previous_fork_topics = fork_core_topics::(&previous_fork, spec); + let previous_fork_topics = fork_core_topics::(&previous_fork, spec); topics.extend(previous_fork_topics); current_fork = previous_fork; } diff --git a/beacon_node/lighthouse_network/tests/common.rs b/beacon_node/lighthouse_network/tests/common.rs index 9585dcf5af..40ae341bd2 100644 --- a/beacon_node/lighthouse_network/tests/common.rs +++ b/beacon_node/lighthouse_network/tests/common.rs @@ -1,5 +1,4 @@ #![cfg(test)] -use libp2p::gossipsub; use lighthouse_network::service::Network as LibP2PService; use lighthouse_network::Enr; use lighthouse_network::EnrExt; @@ -8,7 +7,6 @@ use lighthouse_network::{NetworkConfig, NetworkEvent}; use slog::{debug, error, o, Drain}; use std::sync::Arc; use std::sync::Weak; -use std::time::Duration; use tokio::runtime::Runtime; use types::{ ChainSpec, EnrForkId, Epoch, EthSpec, ForkContext, ForkName, Hash256, MinimalEthSpec, Slot, @@ -26,11 +24,13 @@ pub fn fork_context(fork_name: ForkName) -> ForkContext { let merge_fork_epoch = Epoch::new(2); let capella_fork_epoch = Epoch::new(3); let deneb_fork_epoch = Epoch::new(4); + let electra_fork_epoch = Epoch::new(5); chain_spec.altair_fork_epoch = Some(altair_fork_epoch); chain_spec.bellatrix_fork_epoch = Some(merge_fork_epoch); chain_spec.capella_fork_epoch = Some(capella_fork_epoch); chain_spec.deneb_fork_epoch = Some(deneb_fork_epoch); + chain_spec.electra_fork_epoch = Some(electra_fork_epoch); let current_slot = match fork_name { ForkName::Base => Slot::new(0), @@ -38,11 +38,17 @@ pub fn fork_context(fork_name: ForkName) -> ForkContext { ForkName::Merge => merge_fork_epoch.start_slot(E::slots_per_epoch()), ForkName::Capella => capella_fork_epoch.start_slot(E::slots_per_epoch()), ForkName::Deneb => deneb_fork_epoch.start_slot(E::slots_per_epoch()), + ForkName::Electra => electra_fork_epoch.start_slot(E::slots_per_epoch()), }; ForkContext::new::(current_slot, Hash256::zero(), &chain_spec) } -pub struct Libp2pInstance(LibP2PService, exit_future::Signal); +pub struct Libp2pInstance( + LibP2PService, + #[allow(dead_code)] + // This field is managed for lifetime purposes may not be used directly, hence the `#[allow(dead_code)]` attribute. + async_channel::Sender<()>, +); impl std::ops::Deref for Libp2pInstance { type Target = LibP2PService; @@ -86,12 +92,6 @@ pub fn build_config(mut boot_nodes: Vec) -> NetworkConfig { 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 = gossipsub::ConfigBuilder::from(config.gs_config) - .heartbeat_initial_delay(Duration::from_millis(500)) - .heartbeat_interval(Duration::from_millis(500)) - .build() - .unwrap(); config } @@ -105,7 +105,7 @@ pub async fn build_libp2p_instance( let config = build_config(boot_nodes); // launch libp2p service - let (signal, exit) = exit_future::signal(); + let (signal, exit) = async_channel::bounded(1); let (shutdown_tx, _) = futures::channel::mpsc::channel(1); let executor = task_executor::TaskExecutor::new(rt, exit, log.clone(), shutdown_tx); let libp2p_context = lighthouse_network::Context { diff --git a/beacon_node/lighthouse_network/tests/rpc_tests.rs b/beacon_node/lighthouse_network/tests/rpc_tests.rs index 643c1231a0..5a21b462d4 100644 --- a/beacon_node/lighthouse_network/tests/rpc_tests.rs +++ b/beacon_node/lighthouse_network/tests/rpc_tests.rs @@ -1056,7 +1056,7 @@ fn goodbye_test(log_level: Level, enable_logging: bool, protocol: Protocol) { 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; + let enable_logging = false; goodbye_test(log_level, enable_logging, Protocol::Tcp); } @@ -1066,6 +1066,6 @@ fn tcp_test_goodbye_rpc() { 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; + let enable_logging = false; goodbye_test(log_level, enable_logging, Protocol::Quic); } diff --git a/beacon_node/network/Cargo.toml b/beacon_node/network/Cargo.toml index bbd2af2174..406015360e 100644 --- a/beacon_node/network/Cargo.toml +++ b/beacon_node/network/Cargo.toml @@ -8,12 +8,14 @@ edition = { workspace = true } sloggers = { workspace = true } genesis = { workspace = true } matches = "0.1.8" -exit-future = { workspace = true } slog-term = { workspace = true } slog-async = { workspace = true } eth2 = { workspace = true } +gossipsub = { workspace = true } [dependencies] +async-channel = { workspace = true } +anyhow = { workspace = true } beacon_chain = { workspace = true } store = { workspace = true } lighthouse_network = { workspace = true } @@ -35,11 +37,10 @@ lazy_static = { workspace = true } lighthouse_metrics = { workspace = true } logging = { workspace = true } task_executor = { workspace = true } -igd = "0.12.1" +igd-next = "0.14" itertools = { workspace = true } num_cpus = { workspace = true } lru_cache = { workspace = true } -if-addrs = "0.6.4" lru = { workspace = true } strum = { workspace = true } tokio-util = { workspace = true } @@ -56,4 +57,5 @@ environment = { workspace = true } # NOTE: This can be run via cargo build --bin lighthouse --features network/disable-backfill disable-backfill = [] fork_from_env = ["beacon_chain/fork_from_env"] -portable = ["beacon_chain/portable"] \ No newline at end of file +portable = ["beacon_chain/portable"] +test_logger = [] diff --git a/beacon_node/network/src/metrics.rs b/beacon_node/network/src/metrics.rs index 0509ed1ea7..d19a41a28f 100644 --- a/beacon_node/network/src/metrics.rs +++ b/beacon_node/network/src/metrics.rs @@ -337,9 +337,9 @@ pub fn register_sync_committee_error(error: &SyncCommitteeError) { inc_counter_vec(&GOSSIP_SYNC_COMMITTEE_ERRORS_PER_TYPE, &[error.as_ref()]); } -pub fn update_gossip_metrics( +pub fn update_gossip_metrics( gossipsub: &Gossipsub, - network_globals: &Arc>, + network_globals: &Arc>, ) { // Mesh peers per client // Reset the gauges @@ -398,7 +398,7 @@ pub fn update_gossip_metrics( } } -pub fn update_sync_metrics(network_globals: &Arc>) { +pub fn update_sync_metrics(network_globals: &Arc>) { // reset the counts if PEERS_PER_SYNC_TYPE .as_ref() diff --git a/beacon_node/network/src/nat.rs b/beacon_node/network/src/nat.rs index d011ac42e8..e63ff55039 100644 --- a/beacon_node/network/src/nat.rs +++ b/beacon_node/network/src/nat.rs @@ -3,225 +3,58 @@ //! Currently supported strategies: //! - UPnP -use crate::{NetworkConfig, NetworkMessage}; -use if_addrs::get_if_addrs; -use slog::{debug, info}; -use std::net::{IpAddr, SocketAddr, SocketAddrV4}; -use tokio::sync::mpsc; -use types::EthSpec; +use anyhow::{bail, Context, Error}; +use igd_next::{aio::tokio as igd, PortMappingProtocol}; +use slog::debug; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::time::Duration; +use tokio::time::sleep; -/// Configuration required to construct the UPnP port mappings. -pub struct UPnPConfig { - /// The local TCP port. - tcp_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, -} +/// The duration in seconds of a port mapping on the gateway. +const MAPPING_DURATION: u32 = 3600; -/// 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, -} +/// Renew the Mapping every half of `MAPPING_DURATION` to avoid the port being unmapped. +const MAPPING_TIMEOUT: u64 = MAPPING_DURATION as u64 / 2; -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, - disc_port: v4_addr.disc_port, - quic_port: v4_addr.quic_port, - disable_discovery: config.disable_discovery, - disable_quic_support: config.disable_quic_support, - }) - } -} - -/// Attempts to construct external port mappings with UPnP. -pub fn construct_upnp_mappings( - config: UPnPConfig, - network_send: mpsc::UnboundedSender>, +/// Attempts to map Discovery external port mappings with UPnP. +pub async fn construct_upnp_mappings( + addr: Ipv4Addr, + port: u16, log: slog::Logger, -) { - info!(log, "UPnP Attempting to initialise routes"); - match igd::search_gateway(Default::default()) { - Err(e) => info!(log, "UPnP not available"; "error" => %e), - Ok(gateway) => { - // Need to find the local listening address matched with the router subnet - let interfaces = match get_if_addrs() { - Ok(v) => v, - Err(e) => { - info!(log, "UPnP failed to get local interfaces"; "error" => %e); - return; - } - }; - let local_ip = interfaces.iter().find_map(|interface| { - // Just use the first IP of the first interface that is not a loopback and not an - // ipv6 address. - if !interface.is_loopback() { - interface.ip().is_ipv4().then(|| interface.ip()) - } else { - None - } - }); +) -> Result<(), Error> { + let gateway = igd::search_gateway(Default::default()) + .await + .context("Gateway does not support UPnP")?; - let local_ip = match local_ip { - None => { - info!(log, "UPnP failed to find local IP address"); - return; - } - Some(v) => v, - }; + let external_address = gateway + .get_external_ip() + .await + .context("Could not access gateway's external ip")?; - 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); - let external_ip = gateway.get_external_ip(); - // We add specific port mappings rather than getting the router to arbitrary assign - // 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. - mappings.tcp_port = add_port_mapping( - &gateway, - igd::PortMappingProtocol::TCP, - libp2p_socket, - "tcp", - &log, - ).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)); - config.tcp_port - }).ok(); - - let set_udp_mapping = |udp_port| { - let udp_socket = SocketAddrV4::new(address, udp_port); - add_port_mapping( - &gateway, - igd::PortMappingProtocol::UDP, - udp_socket, - "udp", - &log, - ).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. - 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"), - } - } + let is_private = match external_address { + IpAddr::V4(ipv4) => ipv4.is_private(), + IpAddr::V6(ipv6) => ipv6.is_loopback() || ipv6.is_unspecified(), }; -} -/// Sets up a port mapping for a protocol returning the mapped port if successful. -fn add_port_mapping( - gateway: &igd::Gateway, - protocol: igd::PortMappingProtocol, - socket: SocketAddrV4, - protocol_string: &'static str, - log: &slog::Logger, -) -> Result<(), ()> { - // We add specific port mappings rather than getting the router to arbitrary assign - // 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 mapping_string = &format!("lighthouse-{}", protocol_string); - for _ in 0..2 { - match gateway.add_port(protocol, socket.port(), socket, 0, mapping_string) { - Err(e) => { - match e { - igd::AddPortError::PortInUse => { - // Try and remove and re-create - debug!(log, "UPnP port in use, attempting to remap"; "protocol" => protocol_string, "port" => socket.port()); - match gateway.remove_port(protocol, socket.port()) { - Ok(()) => { - debug!(log, "UPnP Removed port mapping"; "protocol" => protocol_string, "port" => socket.port()) - } - Err(e) => { - debug!(log, "UPnP Port remove failure"; "protocol" => protocol_string, "port" => socket.port(), "error" => %e); - return Err(()); - } - } - } - e => { - info!(log, "UPnP TCP route not set"; "error" => %e); - return Err(()); - } - } - } - Ok(_) => { - return Ok(()); - } - } + if is_private { + bail!( + "Gateway's external address is a private address: {}", + external_address + ); } - Err(()) -} -/// Removes the specified TCP and UDP port mappings. -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) = 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) => { - debug!(log, "UPnP Failed to remove TCP port mapping"; "port" => tcp_port, "error" => %e) - } - } - } - 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) - } - } - } - } - Err(e) => debug!(log, "UPnP failed to remove mappings"; "error" => %e), - } + loop { + gateway + .add_port( + PortMappingProtocol::UDP, + port, + SocketAddr::new(IpAddr::V4(addr), port), + MAPPING_DURATION, + "Lighthouse Discovery port", + ) + .await + .with_context(|| format!("Could not UPnP map port: {} on the gateway", port))?; + debug!(log, "Discovery UPnP port mapped"; "port" => %port); + sleep(Duration::from_secs(MAPPING_TIMEOUT)).await; } } diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index 07fc06bc37..f7bba90037 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -78,8 +78,8 @@ impl VerifiedAttestation for VerifiedUnaggregate { } /// An attestation that failed validation by the `BeaconChain`. -struct RejectedUnaggregate { - attestation: Box>, +struct RejectedUnaggregate { + attestation: Box>, error: AttnError, } @@ -112,26 +112,26 @@ impl VerifiedAttestation for VerifiedAggregate { } /// An attestation that failed validation by the `BeaconChain`. -struct RejectedAggregate { - signed_aggregate: Box>, +struct RejectedAggregate { + signed_aggregate: Box>, error: AttnError, } /// Data for an aggregated or unaggregated attestation that failed verification. -enum FailedAtt { +enum FailedAtt { Unaggregate { - attestation: Box>, + attestation: Box>, subnet_id: SubnetId, should_import: bool, seen_timestamp: Duration, }, Aggregate { - attestation: Box>, + attestation: Box>, seen_timestamp: Duration, }, } -impl FailedAtt { +impl FailedAtt { pub fn beacon_block_root(&self) -> &Hash256 { &self.attestation().data.beacon_block_root } @@ -143,7 +143,7 @@ impl FailedAtt { } } - pub fn attestation(&self) -> &Attestation { + pub fn attestation(&self) -> &Attestation { match self { FailedAtt::Unaggregate { attestation, .. } => attestation, FailedAtt::Aggregate { attestation, .. } => &attestation.message.aggregate, @@ -963,7 +963,7 @@ impl NetworkBeaconProcessor { self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); return None; } - Err(BlockError::BlockIsAlreadyKnown) => { + Err(BlockError::BlockIsAlreadyKnown(_)) => { debug!( self.log, "Gossip block is already known"; diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index e7d3a7ce21..27b9e676da 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -3,16 +3,13 @@ use crate::{ sync::{manager::BlockProcessType, SyncMessage}, }; use beacon_chain::block_verification_types::RpcBlock; -use beacon_chain::{ - builder::Witness, eth1_chain::CachingEth1Backend, test_utils::BeaconChainHarness, BeaconChain, -}; +use beacon_chain::{builder::Witness, eth1_chain::CachingEth1Backend, 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::methods::{BlobsByRangeRequest, BlobsByRootRequest}; use lighthouse_network::{ rpc::{BlocksByRangeRequest, BlocksByRootRequest, LightClientBootstrapRequest, StatusMessage}, @@ -24,7 +21,6 @@ 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::*; @@ -106,14 +102,14 @@ impl NetworkBeaconProcessor { self.try_send(BeaconWorkEvent { drop_during_sync: true, work: Work::GossipAttestation { - attestation: GossipAttestationPackage { + attestation: Box::new(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), }, @@ -152,13 +148,13 @@ impl NetworkBeaconProcessor { self.try_send(BeaconWorkEvent { drop_during_sync: true, work: Work::GossipAggregate { - aggregate: GossipAggregatePackage { + aggregate: Box::new(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), }, @@ -512,20 +508,16 @@ impl NetworkBeaconProcessor { request: BlocksByRangeRequest, ) -> Result<(), Error> { let processor = self.clone(); - let process_fn = move |send_idle_on_drop| { + let process_fn = async move { let executor = processor.executor.clone(); - processor.handle_blocks_by_range_request( - executor, - send_idle_on_drop, - peer_id, - request_id, - request, - ) + processor + .handle_blocks_by_range_request(executor, peer_id, request_id, request) + .await; }; self.try_send(BeaconWorkEvent { drop_during_sync: false, - work: Work::BlocksByRangeRequest(Box::new(process_fn)), + work: Work::BlocksByRangeRequest(Box::pin(process_fn)), }) } @@ -537,20 +529,16 @@ impl NetworkBeaconProcessor { request: BlocksByRootRequest, ) -> Result<(), Error> { let processor = self.clone(); - let process_fn = move |send_idle_on_drop| { + let process_fn = async move { let executor = processor.executor.clone(); - processor.handle_blocks_by_root_request( - executor, - send_idle_on_drop, - peer_id, - request_id, - request, - ) + processor + .handle_blocks_by_root_request(executor, peer_id, request_id, request) + .await; }; self.try_send(BeaconWorkEvent { drop_during_sync: false, - work: Work::BlocksByRootsRequest(Box::new(process_fn)), + work: Work::BlocksByRootsRequest(Box::pin(process_fn)), }) } @@ -605,6 +593,37 @@ impl NetworkBeaconProcessor { }) } + /// Create a new work event to process a `LightClientOptimisticUpdate` request from the RPC network. + pub fn send_light_client_optimistic_update_request( + self: &Arc, + peer_id: PeerId, + request_id: PeerRequestId, + ) -> Result<(), Error> { + let processor = self.clone(); + let process_fn = + move || processor.handle_light_client_optimistic_update(peer_id, request_id); + + self.try_send(BeaconWorkEvent { + drop_during_sync: true, + work: Work::LightClientOptimisticUpdateRequest(Box::new(process_fn)), + }) + } + + /// Create a new work event to process a `LightClientFinalityUpdate` request from the RPC network. + pub fn send_light_client_finality_update_request( + self: &Arc, + peer_id: PeerId, + request_id: PeerRequestId, + ) -> Result<(), Error> { + let processor = self.clone(); + let process_fn = move || processor.handle_light_client_finality_update(peer_id, request_id); + + self.try_send(BeaconWorkEvent { + drop_during_sync: true, + work: Work::LightClientFinalityUpdateRequest(Box::new(process_fn)), + }) + } + /// Send a message to `sync_tx`. /// /// Creates a log if there is an internal error. @@ -636,6 +655,9 @@ impl NetworkBeaconProcessor> { // processor (but not much else). pub fn null_for_testing( network_globals: Arc>, + chain: Arc>>, + executor: TaskExecutor, + log: Logger, ) -> (Self, mpsc::Receiver>) { let BeaconProcessorChannels { beacon_processor_tx, @@ -646,27 +668,17 @@ impl NetworkBeaconProcessor> { 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, + chain, network_tx, sync_tx, reprocess_tx: work_reprocessing_tx, network_globals, invalid_block_storage: InvalidBlockStorage::Disabled, - executor: runtime.task_executor.clone(), + executor, log, }; diff --git a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs index a731dea7c1..81c8f662ee 100644 --- a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs @@ -3,10 +3,8 @@ 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::methods::{BlobsByRangeRequest, BlobsByRootRequest}; -use lighthouse_network::rpc::StatusMessage; use lighthouse_network::rpc::*; use lighthouse_network::{PeerId, PeerRequestId, ReportSource, Response, SyncInfo}; use slog::{debug, error, warn}; @@ -129,10 +127,9 @@ impl NetworkBeaconProcessor { } /// Handle a `BlocksByRoot` request from the peer. - pub fn handle_blocks_by_root_request( + pub async fn handle_blocks_by_root_request( self: Arc, executor: TaskExecutor, - send_on_drop: SendOnDrop, peer_id: PeerId, request_id: PeerRequestId, request: BlocksByRootRequest, @@ -140,79 +137,69 @@ impl NetworkBeaconProcessor { let requested_blocks = request.block_roots().len(); let mut block_stream = match self .chain - .get_blocks_checking_early_attester_cache(request.block_roots().to_vec(), &executor) + .get_blocks_checking_caches(request.block_roots().to_vec(), &executor) { Ok(block_stream) => block_stream, Err(e) => return error!(self.log, "Error getting block stream"; "error" => ?e), }; // Fetching blocks is async because it may have to hit the execution layer for payloads. - executor.spawn( - async move { - let mut send_block_count = 0; - let mut send_response = true; - while let Some((root, result)) = block_stream.next().await { - match result.as_ref() { - Ok(Some(block)) => { - self.send_response( - peer_id, - Response::BlocksByRoot(Some(block.clone())), - request_id, - ); - send_block_count += 1; - } - Ok(None) => { - debug!( - self.log, - "Peer requested unknown block"; - "peer" => %peer_id, - "request_root" => ?root - ); - } - Err(BeaconChainError::BlockHashMissingFromExecutionLayer(_)) => { - debug!( - self.log, - "Failed to fetch execution payload for blocks by root request"; - "block_root" => ?root, - "reason" => "execution layer not synced", - ); - // send the stream terminator - self.send_error_response( - peer_id, - RPCResponseErrorCode::ResourceUnavailable, - "Execution layer not synced".into(), - request_id, - ); - send_response = false; - break; - } - Err(e) => { - debug!( - self.log, - "Error fetching block for peer"; - "peer" => %peer_id, - "request_root" => ?root, - "error" => ?e, - ); - } - } + let mut send_block_count = 0; + while let Some((root, result)) = block_stream.next().await { + match result.as_ref() { + Ok(Some(block)) => { + self.send_response( + peer_id, + Response::BlocksByRoot(Some(block.clone())), + request_id, + ); + send_block_count += 1; } - debug!( - self.log, - "Received BlocksByRoot Request"; - "peer" => %peer_id, - "requested" => requested_blocks, - "returned" => %send_block_count - ); + Ok(None) => { + debug!( + self.log, + "Peer requested unknown block"; + "peer" => %peer_id, + "request_root" => ?root + ); + } + Err(BeaconChainError::BlockHashMissingFromExecutionLayer(_)) => { + debug!( + self.log, + "Failed to fetch execution payload for blocks by root request"; + "block_root" => ?root, + "reason" => "execution layer not synced", + ); + // send the stream terminator + return self.send_error_response( + peer_id, + RPCResponseErrorCode::ResourceUnavailable, + "Execution layer not synced".into(), + request_id, + ); + } + Err(e) => { + debug!( + self.log, + "Error fetching block for peer"; + "peer" => %peer_id, + "request_root" => ?root, + "error" => ?e, + ); + } + } + } + debug!( + self.log, + "Received BlocksByRoot Request"; + "peer" => %peer_id, + "requested" => requested_blocks, + "returned" => %send_block_count + ); - // send stream termination - if send_response { - self.send_response(peer_id, Response::BlocksByRoot(None), request_id); - } - drop(send_on_drop); - }, - "load_blocks_by_root_blocks", - ) + // send stream termination + self.send_response(peer_id, Response::BlocksByRoot(None), request_id); } + /// Handle a `BlobsByRoot` request from the peer. pub fn handle_blobs_by_root_request( self: Arc, @@ -220,10 +207,25 @@ impl NetworkBeaconProcessor { request_id: PeerRequestId, request: BlobsByRootRequest, ) { + self.terminate_response_stream( + peer_id, + request_id, + self.handle_blobs_by_root_request_inner(peer_id, request_id, request), + Response::BlobsByRoot, + ); + } + + /// Handle a `BlobsByRoot` request from the peer. + pub fn handle_blobs_by_root_request_inner( + &self, + peer_id: PeerId, + request_id: PeerRequestId, + request: BlobsByRootRequest, + ) -> Result<(), (RPCResponseErrorCode, &'static str)> { let Some(requested_root) = request.blob_ids.as_slice().first().map(|id| id.block_root) else { // No blob ids requested. - return; + return Ok(()); }; let requested_indices = request .blob_ids @@ -232,7 +234,6 @@ impl NetworkBeaconProcessor { .map(|id| id.index) .collect::>(); let mut send_blob_count = 0; - let send_response = true; let mut blob_list_results = HashMap::new(); for id in request.blob_ids.as_slice() { @@ -288,53 +289,93 @@ impl NetworkBeaconProcessor { "returned" => send_blob_count ); - // send stream termination - if send_response { - self.send_response(peer_id, Response::BlobsByRoot(None), request_id); - } + Ok(()) } - /// Handle a `BlocksByRoot` request from the peer. + /// Handle a `LightClientBootstrap` request from the peer. pub fn handle_light_client_bootstrap( self: &Arc, peer_id: PeerId, request_id: PeerRequestId, request: LightClientBootstrapRequest, ) { - let block_root = request.root; - match self.chain.get_light_client_bootstrap(&block_root) { - Ok(Some((bootstrap, _))) => self.send_response( - peer_id, - Response::LightClientBootstrap(bootstrap), - request_id, - ), - Ok(None) => self.send_error_response( - peer_id, - RPCResponseErrorCode::ResourceUnavailable, - "Bootstrap not available".into(), - request_id, - ), - Err(e) => { - self.send_error_response( - peer_id, + self.terminate_response_single_item( + peer_id, + request_id, + match self.chain.get_light_client_bootstrap(&request.root) { + Ok(Some((bootstrap, _))) => Ok(Arc::new(bootstrap)), + Ok(None) => Err(( RPCResponseErrorCode::ResourceUnavailable, - "Bootstrap not available".into(), - request_id, - ); - error!(self.log, "Error getting LightClientBootstrap instance"; - "block_root" => ?block_root, - "peer" => %peer_id, - "error" => ?e - ) - } - }; + "Bootstrap not available", + )), + Err(e) => { + error!(self.log, "Error getting LightClientBootstrap instance"; + "block_root" => ?request.root, + "peer" => %peer_id, + "error" => ?e + ); + Err(( + RPCResponseErrorCode::ResourceUnavailable, + "Bootstrap not available", + )) + } + }, + Response::LightClientBootstrap, + ); + } + + /// Handle a `LightClientOptimisticUpdate` request from the peer. + pub fn handle_light_client_optimistic_update( + self: &Arc, + peer_id: PeerId, + request_id: PeerRequestId, + ) { + self.terminate_response_single_item( + peer_id, + request_id, + match self + .chain + .light_client_server_cache + .get_latest_optimistic_update() + { + Some(update) => Ok(Arc::new(update)), + None => Err(( + RPCResponseErrorCode::ResourceUnavailable, + "Latest optimistic update not available", + )), + }, + Response::LightClientOptimisticUpdate, + ); + } + + /// Handle a `LightClientFinalityUpdate` request from the peer. + pub fn handle_light_client_finality_update( + self: &Arc, + peer_id: PeerId, + request_id: PeerRequestId, + ) { + self.terminate_response_single_item( + peer_id, + request_id, + match self + .chain + .light_client_server_cache + .get_latest_finality_update() + { + Some(update) => Ok(Arc::new(update)), + None => Err(( + RPCResponseErrorCode::ResourceUnavailable, + "Latest finality update not available", + )), + }, + Response::LightClientFinalityUpdate, + ); } /// Handle a `BlocksByRange` request from the peer. - pub fn handle_blocks_by_range_request( + pub async fn handle_blocks_by_range_request( self: Arc, executor: TaskExecutor, - send_on_drop: SendOnDrop, peer_id: PeerId, request_id: PeerRequestId, req: BlocksByRangeRequest, @@ -351,7 +392,9 @@ impl NetworkBeaconProcessor { .epoch() .map_or(self.chain.spec.max_request_blocks, |epoch| { match self.chain.spec.fork_name_at_epoch(epoch) { - ForkName::Deneb => self.chain.spec.max_request_blocks_deneb, + ForkName::Deneb | ForkName::Electra => { + self.chain.spec.max_request_blocks_deneb + } ForkName::Base | ForkName::Altair | ForkName::Merge | ForkName::Capella => { self.chain.spec.max_request_blocks } @@ -442,137 +485,121 @@ impl NetworkBeaconProcessor { }; // Fetching blocks is async because it may have to hit the execution layer for payloads. - executor.spawn( - async move { - let mut blocks_sent = 0; - let mut send_response = true; + let mut blocks_sent = 0; - while let Some((root, result)) = block_stream.next().await { - match result.as_ref() { - 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() - { - blocks_sent += 1; - self.send_network_message(NetworkMessage::SendResponse { - peer_id, - response: Response::BlocksByRange(Some(block.clone())), - id: request_id, - }); - } - } - Ok(None) => { - error!( - self.log, - "Block in the chain is not in the store"; - "request" => ?req, - "peer" => %peer_id, - "request_root" => ?root - ); - self.send_error_response( - peer_id, - RPCResponseErrorCode::ServerError, - "Database inconsistency".into(), - request_id, - ); - send_response = false; - break; - } - Err(BeaconChainError::BlockHashMissingFromExecutionLayer(_)) => { - debug!( - self.log, - "Failed to fetch execution payload for blocks by range request"; - "block_root" => ?root, - "reason" => "execution layer not synced", - ); - // send the stream terminator - self.send_error_response( - peer_id, - RPCResponseErrorCode::ResourceUnavailable, - "Execution layer not synced".into(), - request_id, - ); - send_response = false; - break; - } - Err(e) => { - if matches!( - e, - BeaconChainError::ExecutionLayerErrorPayloadReconstruction(_block_hash, ref boxed_error) - if matches!(**boxed_error, execution_layer::Error::EngineError(_)) - ) { - warn!( - self.log, - "Error rebuilding payload for peer"; - "info" => "this may occur occasionally when the EE is busy", - "block_root" => ?root, - "error" => ?e, - ); - } else { - error!( - self.log, - "Error fetching block for peer"; - "block_root" => ?root, - "error" => ?e - ); - } - - // send the stream terminator - self.send_error_response( - peer_id, - RPCResponseErrorCode::ServerError, - "Failed fetching blocks".into(), - request_id, - ); - send_response = false; - break; - } + while let Some((root, result)) = block_stream.next().await { + match result.as_ref() { + 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() + { + blocks_sent += 1; + self.send_network_message(NetworkMessage::SendResponse { + peer_id, + response: Response::BlocksByRange(Some(block.clone())), + id: request_id, + }); } } - - let current_slot = self - .chain - .slot() - .unwrap_or_else(|_| self.chain.slot_clock.genesis_slot()); - - if blocks_sent < (*req.count() as usize) { - debug!( + Ok(None) => { + error!( self.log, - "BlocksByRange outgoing response processed"; + "Block in the chain is not in the store"; + "request" => ?req, "peer" => %peer_id, - "msg" => "Failed to return all requested blocks", - "start_slot" => req.start_slot(), - "current_slot" => current_slot, - "requested" => req.count(), - "returned" => blocks_sent + "request_root" => ?root ); - } else { - debug!( - self.log, - "BlocksByRange outgoing response processed"; - "peer" => %peer_id, - "start_slot" => req.start_slot(), - "current_slot" => current_slot, - "requested" => req.count(), - "returned" => blocks_sent - ); - } - - if send_response { - // send the stream terminator - self.send_network_message(NetworkMessage::SendResponse { + return self.send_error_response( peer_id, - response: Response::BlocksByRange(None), - id: request_id, - }); + RPCResponseErrorCode::ServerError, + "Database inconsistency".into(), + request_id, + ); } + Err(BeaconChainError::BlockHashMissingFromExecutionLayer(_)) => { + debug!( + self.log, + "Failed to fetch execution payload for blocks by range request"; + "block_root" => ?root, + "reason" => "execution layer not synced", + ); + // send the stream terminator + return self.send_error_response( + peer_id, + RPCResponseErrorCode::ResourceUnavailable, + "Execution layer not synced".into(), + request_id, + ); + } + Err(e) => { + if matches!( + e, + BeaconChainError::ExecutionLayerErrorPayloadReconstruction(_block_hash, ref boxed_error) + if matches!(**boxed_error, execution_layer::Error::EngineError(_)) + ) { + warn!( + self.log, + "Error rebuilding payload for peer"; + "info" => "this may occur occasionally when the EE is busy", + "block_root" => ?root, + "error" => ?e, + ); + } else { + error!( + self.log, + "Error fetching block for peer"; + "block_root" => ?root, + "error" => ?e + ); + } - drop(send_on_drop); - }, - "load_blocks_by_range_blocks", - ); + // send the stream terminator + return self.send_error_response( + peer_id, + RPCResponseErrorCode::ServerError, + "Failed fetching blocks".into(), + request_id, + ); + } + } + } + + let current_slot = self + .chain + .slot() + .unwrap_or_else(|_| self.chain.slot_clock.genesis_slot()); + + 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(), + "current_slot" => current_slot, + "requested" => req.count(), + "returned" => blocks_sent + ); + } else { + debug!( + self.log, + "BlocksByRange outgoing response processed"; + "peer" => %peer_id, + "start_slot" => req.start_slot(), + "current_slot" => current_slot, + "requested" => req.count(), + "returned" => blocks_sent + ); + } + + // send the stream terminator + self.send_network_message(NetworkMessage::SendResponse { + peer_id, + response: Response::BlocksByRange(None), + id: request_id, + }); } /// Handle a `BlobsByRange` request from the peer. @@ -582,6 +609,21 @@ impl NetworkBeaconProcessor { request_id: PeerRequestId, req: BlobsByRangeRequest, ) { + self.terminate_response_stream( + peer_id, + request_id, + self.handle_blobs_by_range_request_inner(peer_id, request_id, req), + Response::BlobsByRange, + ); + } + + /// Handle a `BlobsByRange` request from the peer. + fn handle_blobs_by_range_request_inner( + &self, + peer_id: PeerId, + request_id: PeerRequestId, + req: BlobsByRangeRequest, + ) -> Result<(), (RPCResponseErrorCode, &'static str)> { debug!(self.log, "Received BlobsByRange Request"; "peer_id" => %peer_id, "count" => req.count, @@ -590,12 +632,10 @@ impl NetworkBeaconProcessor { // Should not send more than max request blocks if req.max_blobs_requested::() > self.chain.spec.max_request_blob_sidecars { - return self.send_error_response( - peer_id, + return Err(( RPCResponseErrorCode::InvalidRequest, - "Request exceeded `MAX_REQUEST_BLOBS_SIDECARS`".into(), - request_id, - ); + "Request exceeded `MAX_REQUEST_BLOBS_SIDECARS`", + )); } let request_start_slot = Slot::from(req.start_slot); @@ -604,13 +644,10 @@ impl NetworkBeaconProcessor { Some(boundary) => boundary.start_slot(T::EthSpec::slots_per_epoch()), None => { debug!(self.log, "Deneb fork is disabled"); - self.send_error_response( - peer_id, + return Err(( RPCResponseErrorCode::InvalidRequest, - "Deneb fork is disabled".into(), - request_id, - ); - return; + "Deneb fork is disabled", + )); } }; @@ -630,19 +667,15 @@ impl NetworkBeaconProcessor { ); return if data_availability_boundary_slot < oldest_blob_slot { - self.send_error_response( - peer_id, + Err(( RPCResponseErrorCode::ResourceUnavailable, - "blobs pruned within boundary".into(), - request_id, - ) + "blobs pruned within boundary", + )) } else { - self.send_error_response( - peer_id, + Err(( RPCResponseErrorCode::InvalidRequest, - "Req outside availability period".into(), - request_id, - ) + "Req outside availability period", + )) }; } @@ -659,25 +692,15 @@ impl NetworkBeaconProcessor { "requested_slot" => slot, "oldest_known_slot" => oldest_block_slot ); - return self.send_error_response( - peer_id, - RPCResponseErrorCode::ResourceUnavailable, - "Backfilling".into(), - request_id, - ); + return Err((RPCResponseErrorCode::ResourceUnavailable, "Backfilling")); } Err(e) => { - self.send_error_response( - peer_id, - RPCResponseErrorCode::ServerError, - "Database error".into(), - request_id, - ); - return error!(self.log, "Unable to obtain root iter"; + error!(self.log, "Unable to obtain root iter"; "request" => ?req, "peer" => %peer_id, "error" => ?e ); + return Err((RPCResponseErrorCode::ServerError, "Database error")); } }; @@ -709,11 +732,12 @@ impl NetworkBeaconProcessor { let block_roots = match maybe_block_roots { Ok(block_roots) => block_roots, Err(e) => { - return error!(self.log, "Error during iteration over blocks"; + error!(self.log, "Error during iteration over blocks"; "request" => ?req, "peer" => %peer_id, "error" => ?e - ) + ); + return Err((RPCResponseErrorCode::ServerError, "Database error")); } }; @@ -721,7 +745,6 @@ impl NetworkBeaconProcessor { let block_roots = block_roots.into_iter().flatten(); let mut blobs_sent = 0; - let mut send_response = true; for root in block_roots { match self.chain.get_blobs(&root) { @@ -744,14 +767,10 @@ impl NetworkBeaconProcessor { "block_root" => ?root, "error" => ?e ); - self.send_error_response( - peer_id, + return Err(( RPCResponseErrorCode::ServerError, - "No blobs and failed fetching corresponding block".into(), - request_id, - ); - send_response = false; - break; + "No blobs and failed fetching corresponding block", + )); } } } @@ -771,13 +790,53 @@ impl NetworkBeaconProcessor { "returned" => blobs_sent ); - if send_response { - // send the stream terminator - self.send_network_message(NetworkMessage::SendResponse { + Ok(()) + } + + /// Helper function to ensure single item protocol always end with either a single chunk or an + /// error + fn terminate_response_single_item Response>( + &self, + peer_id: PeerId, + request_id: PeerRequestId, + result: Result, + into_response: F, + ) { + match result { + Ok(resp) => { + // Not necessary to explicitly send a termination message if this InboundRequest + // returns <= 1 for InboundRequest::expected_responses + // https://github.com/sigp/lighthouse/blob/3058b96f2560f1da04ada4f9d8ba8e5651794ff6/beacon_node/lighthouse_network/src/rpc/handler.rs#L555-L558 + self.send_network_message(NetworkMessage::SendResponse { + peer_id, + response: into_response(resp), + id: request_id, + }); + } + Err((error_code, reason)) => { + self.send_error_response(peer_id, error_code, reason.into(), request_id); + } + } + } + + /// Helper function to ensure streamed protocols with multiple responses always end with either + /// a stream termination or an error + fn terminate_response_stream) -> Response>( + &self, + peer_id: PeerId, + request_id: PeerRequestId, + result: Result<(), (RPCResponseErrorCode, &'static str)>, + into_response: F, + ) { + match result { + Ok(_) => self.send_network_message(NetworkMessage::SendResponse { peer_id, - response: Response::BlobsByRange(None), + response: into_response(None), id: request_id, - }); + }), + Err((error_code, reason)) => { + self.send_error_response(peer_id, error_code, reason.into(), request_id); + } } } } diff --git a/beacon_node/network/src/network_beacon_processor/sync_methods.rs b/beacon_node/network/src/network_beacon_processor/sync_methods.rs index 8894d5d9fd..887974c6e0 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -117,6 +117,7 @@ impl NetworkBeaconProcessor { "Gossip block is being processed"; "action" => "sending rpc block to reprocessing queue", "block_root" => %block_root, + "process_type" => ?process_type, ); // Send message to work reprocess queue to retry the block @@ -149,6 +150,7 @@ impl NetworkBeaconProcessor { "proposer" => block.message().proposer_index(), "slot" => block.slot(), "commitments" => commitments_formatted, + "process_type" => ?process_type, ); let result = self @@ -267,6 +269,7 @@ impl NetworkBeaconProcessor { "slot" => %slot, "block_hash" => %hash, ); + self.chain.recompute_head_at_current_slot().await; } Ok(AvailabilityProcessingStatus::MissingComponents(_, _)) => { debug!( @@ -276,7 +279,7 @@ impl NetworkBeaconProcessor { "slot" => %slot, ); } - Err(BlockError::BlockIsAlreadyKnown) => { + Err(BlockError::BlockIsAlreadyKnown(_)) => { debug!( self.log, "Blobs have already been imported"; @@ -417,7 +420,11 @@ impl NetworkBeaconProcessor { } } (imported_blocks, Ok(_)) => { - debug!(self.log, "Parent lookup processed successfully"); + debug!( + self.log, "Parent lookup processed successfully"; + "chain_hash" => %chain_head, + "imported_blocks" => imported_blocks + ); BatchProcessResult::Success { was_non_empty: imported_blocks > 0, } @@ -639,7 +646,7 @@ impl NetworkBeaconProcessor { peer_action: Some(PeerAction::LowToleranceError), }) } - BlockError::BlockIsAlreadyKnown => { + BlockError::BlockIsAlreadyKnown(_) => { // This can happen for many reasons. Head sync's can download multiples and parent // lookups can download blocks before range sync Ok(()) diff --git a/beacon_node/network/src/persisted_dht.rs b/beacon_node/network/src/persisted_dht.rs index e69230c50c..289bf14335 100644 --- a/beacon_node/network/src/persisted_dht.rs +++ b/beacon_node/network/src/persisted_dht.rs @@ -60,11 +60,10 @@ impl StoreItem for PersistedDht { #[cfg(test)] mod tests { use super::*; - use lighthouse_network::Enr; use sloggers::{null::NullLoggerBuilder, Build}; use std::str::FromStr; use store::config::StoreConfig; - use store::{HotColdDB, MemoryStore}; + use store::MemoryStore; use types::{ChainSpec, MinimalEthSpec}; #[test] fn test_persisted_dht() { diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index a774c0e16f..1937fc11cf 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -49,7 +49,7 @@ pub struct Router { /// Types of messages the router can receive. #[derive(Debug)] -pub enum RouterMessage { +pub enum RouterMessage { /// Peer has disconnected. PeerDisconnected(PeerId), /// An RPC request has been received. @@ -62,7 +62,7 @@ pub enum RouterMessage { RPCResponseReceived { peer_id: PeerId, request_id: RequestId, - response: Response, + response: Response, }, /// An RPC request failed RPCFailed { @@ -73,7 +73,7 @@ pub enum RouterMessage { /// A gossip message has been received. The fields are: message id, the peer that sent us this /// message, the message itself and a bool which indicates if the message should be processed /// by the beacon chain after successful verification. - PubsubMessage(MessageId, PeerId, PubsubMessage, bool), + PubsubMessage(MessageId, PeerId, PubsubMessage, bool), /// The peer manager has requested we re-status a peer. StatusPeer(PeerId), } @@ -220,6 +220,14 @@ impl Router { self.network_beacon_processor .send_light_client_bootstrap_request(peer_id, request_id, request), ), + Request::LightClientOptimisticUpdate => self.handle_beacon_processor_send_result( + self.network_beacon_processor + .send_light_client_optimistic_update_request(peer_id, request_id), + ), + Request::LightClientFinalityUpdate => self.handle_beacon_processor_send_result( + self.network_beacon_processor + .send_light_client_finality_update_request(peer_id, request_id), + ), } } @@ -250,7 +258,10 @@ impl Router { Response::BlobsByRoot(blob) => { self.on_blobs_by_root_response(peer_id, request_id, blob); } - Response::LightClientBootstrap(_) => unreachable!(), + // Light client responses should not be received + Response::LightClientBootstrap(_) + | Response::LightClientOptimisticUpdate(_) + | Response::LightClientFinalityUpdate(_) => unreachable!(), } } @@ -482,17 +493,11 @@ impl Router { ) { let request_id = match request_id { RequestId::Sync(sync_id) => match sync_id { - SyncId::SingleBlock { .. } - | SyncId::SingleBlob { .. } - | SyncId::ParentLookup { .. } - | SyncId::ParentLookupBlob { .. } => { + SyncId::SingleBlock { .. } | SyncId::SingleBlob { .. } => { crit!(self.log, "Block lookups do not request BBRange requests"; "peer_id" => %peer_id); return; } - id @ (SyncId::BackFillBlocks { .. } - | SyncId::RangeBlocks { .. } - | SyncId::BackFillBlockAndBlobs { .. } - | SyncId::RangeBlockAndBlobs { .. }) => id, + id @ SyncId::RangeBlockAndBlobs { .. } => id, }, RequestId::Router => { crit!(self.log, "All BBRange requests belong to sync"; "peer_id" => %peer_id); @@ -550,15 +555,12 @@ impl Router { ) { let request_id = match request_id { RequestId::Sync(sync_id) => match sync_id { - id @ (SyncId::SingleBlock { .. } | SyncId::ParentLookup { .. }) => id, - SyncId::BackFillBlocks { .. } - | SyncId::RangeBlocks { .. } - | SyncId::RangeBlockAndBlobs { .. } - | SyncId::BackFillBlockAndBlobs { .. } => { + id @ SyncId::SingleBlock { .. } => id, + SyncId::RangeBlockAndBlobs { .. } => { crit!(self.log, "Batch syncing do not request BBRoot requests"; "peer_id" => %peer_id); return; } - SyncId::SingleBlob { .. } | SyncId::ParentLookupBlob { .. } => { + SyncId::SingleBlob { .. } => { crit!(self.log, "Blob response to block by roots request"; "peer_id" => %peer_id); return; } @@ -591,15 +593,12 @@ impl Router { ) { let request_id = match request_id { RequestId::Sync(sync_id) => match sync_id { - id @ (SyncId::SingleBlob { .. } | SyncId::ParentLookupBlob { .. }) => id, - SyncId::SingleBlock { .. } | SyncId::ParentLookup { .. } => { + id @ SyncId::SingleBlob { .. } => id, + SyncId::SingleBlock { .. } => { crit!(self.log, "Block response to blobs by roots request"; "peer_id" => %peer_id); return; } - SyncId::BackFillBlocks { .. } - | SyncId::RangeBlocks { .. } - | SyncId::RangeBlockAndBlobs { .. } - | SyncId::BackFillBlockAndBlobs { .. } => { + SyncId::RangeBlockAndBlobs { .. } => { crit!(self.log, "Batch syncing does not request BBRoot requests"; "peer_id" => %peer_id); return; } @@ -645,20 +644,20 @@ impl Router { /// Wraps a Network Channel to employ various RPC related network functionality for the /// processor. #[derive(Clone)] -pub struct HandlerNetworkContext { +pub struct HandlerNetworkContext { /// The network channel to relay messages to the Network service. - network_send: mpsc::UnboundedSender>, + network_send: mpsc::UnboundedSender>, /// Logger for the `NetworkContext`. log: slog::Logger, } -impl HandlerNetworkContext { - pub fn new(network_send: mpsc::UnboundedSender>, log: slog::Logger) -> Self { +impl HandlerNetworkContext { + pub fn new(network_send: mpsc::UnboundedSender>, log: slog::Logger) -> Self { Self { network_send, log } } /// Sends a message to the network task. - fn inform_network(&mut self, msg: NetworkMessage) { + fn inform_network(&mut self, msg: NetworkMessage) { self.network_send.send(msg).unwrap_or_else( |e| warn!(self.log, "Could not send message to the network service"; "error" => %e), ) @@ -674,7 +673,7 @@ impl HandlerNetworkContext { } /// Sends a response to the network task. - pub fn send_response(&mut self, peer_id: PeerId, response: Response, id: PeerRequestId) { + pub fn send_response(&mut self, peer_id: PeerId, response: Response, id: PeerRequestId) { self.inform_network(NetworkMessage::SendResponse { peer_id, id, diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index 01a7e1f989..34ed3edcf9 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -1,5 +1,5 @@ use super::sync::manager::RequestId as SyncId; -use crate::nat::EstablishedUPnPMappings; +use crate::nat; use crate::network_beacon_processor::InvalidBlockStorage; use crate::persisted_dht::{clear_dht, load_dht, persist_dht}; use crate::router::{Router, RouterMessage}; @@ -27,6 +27,7 @@ use lighthouse_network::{ MessageId, NetworkEvent, NetworkGlobals, PeerId, }; use slog::{crit, debug, error, info, o, trace, warn}; +use std::collections::BTreeSet; use std::{collections::HashSet, pin::Pin, sync::Arc, time::Duration}; use store::HotColdDB; use strum::IntoStaticStr; @@ -60,7 +61,7 @@ pub enum RequestId { /// Types of messages that the network service can receive. #[derive(Debug, IntoStaticStr)] #[strum(serialize_all = "snake_case")] -pub enum NetworkMessage { +pub enum NetworkMessage { /// Subscribes the beacon node to the core gossipsub topics. We do this when we are either /// synced or close to the head slot. SubscribeCoreTopics, @@ -73,7 +74,7 @@ pub enum NetworkMessage { /// Send a successful Response to the libp2p service. SendResponse { peer_id: PeerId, - response: Response, + response: Response, id: PeerRequestId, }, /// Sends an error response to an RPC request. @@ -84,7 +85,7 @@ pub enum NetworkMessage { id: PeerRequestId, }, /// Publish a list of messages to the gossipsub protocol. - Publish { messages: Vec> }, + Publish { messages: Vec> }, /// Validates a received gossipsub message. This will propagate the message on the network. ValidationResult { /// The peer that sent us the message. We don't send back to this peer. @@ -94,11 +95,6 @@ pub enum NetworkMessage { /// The result of the validation validation_result: MessageAcceptance, }, - /// Called if UPnP managed to establish an external port mapping. - UPnPMappingEstablished { - /// The mappings that were established. - mappings: EstablishedUPnPMappings, - }, /// Reports a peer to the peer manager for performing an action. ReportPeer { peer_id: PeerId, @@ -124,7 +120,7 @@ pub enum NetworkMessage { pub enum ValidatorSubscriptionMessage { /// Subscribes a list of validators to specific slots for attestation duties. AttestationSubscribe { - subscriptions: Vec, + subscriptions: BTreeSet, }, SyncCommitteeSubscribe { subscriptions: Vec, @@ -188,9 +184,6 @@ pub struct NetworkService { store: Arc>, /// 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 - /// 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. @@ -237,22 +230,24 @@ impl NetworkService { "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")); - let upnp_network_send = network_senders.network_send(); - if config.upnp_enabled { - executor.spawn_blocking( - move || { - crate::nat::construct_upnp_mappings( - upnp_config, - upnp_network_send, - upnp_log, - ) - }, - "UPnP", - ); - } + if let (true, false, Some(v4)) = ( + config.upnp_enabled, + config.disable_discovery, + config.listen_addrs().v4(), + ) { + let nw = network_log.clone(); + let v4 = v4.clone(); + executor.spawn( + async move { + info!(nw, "UPnP Attempting to initialise routes"); + if let Err(e) = + nat::construct_upnp_mappings(v4.addr, v4.disc_port, nw.clone()).await + { + info!(nw, "Could not UPnP map Discovery port"; "error" => %e); + } + }, + "UPnP", + ); } // get a reference to the beacon chain store @@ -358,7 +353,6 @@ impl NetworkService { router_send, store, network_globals: network_globals.clone(), - upnp_mappings: EstablishedUPnPMappings::default(), next_fork_update, next_fork_subscriptions, next_unsubscribe, @@ -636,21 +630,6 @@ impl NetworkService { } => { self.libp2p.send_error_response(peer_id, id, error, reason); } - NetworkMessage::UPnPMappingEstablished { mappings } => { - self.upnp_mappings = mappings; - // If there is an external TCP port update, modify our local ENR. - 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 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); - } - } - } NetworkMessage::ValidationResult { propagation_source, message_id, @@ -805,7 +784,7 @@ impl NetworkService { ValidatorSubscriptionMessage::AttestationSubscribe { subscriptions } => { if let Err(e) = self .attestation_service - .validator_subscriptions(subscriptions) + .validator_subscriptions(subscriptions.into_iter()) { warn!(self.log, "Attestation validator subscription failed"; "error" => e); } @@ -1009,10 +988,6 @@ impl Drop for NetworkService { "Saved DHT state"; ), } - - // attempt to remove port mappings - 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 35a7f1eab7..b573187696 100644 --- a/beacon_node/network/src/service/tests.rs +++ b/beacon_node/network/src/service/tests.rs @@ -17,10 +17,7 @@ mod tests { use types::{Epoch, EthSpec, ForkName, MinimalEthSpec, SubnetId}; impl NetworkService { - fn get_topic_params( - &self, - topic: GossipTopic, - ) -> Option<&lighthouse_network::libp2p::gossipsub::TopicScoreParams> { + fn get_topic_params(&self, topic: GossipTopic) -> Option<&gossipsub::TopicScoreParams> { self.libp2p.get_topic_params(topic) } } @@ -62,7 +59,7 @@ mod tests { let runtime = Arc::new(Runtime::new().unwrap()); - let (signal, exit) = exit_future::signal(); + let (signal, exit) = async_channel::bounded(1); let (shutdown_tx, _) = futures::channel::mpsc::channel(1); let executor = task_executor::TaskExecutor::new( Arc::downgrade(&runtime), @@ -139,7 +136,7 @@ mod tests { // Build network service. let (mut network_service, network_globals, _network_senders) = runtime.block_on(async { - let (_, exit) = exit_future::signal(); + let (_, exit) = async_channel::bounded(1); let (shutdown_tx, _) = futures::channel::mpsc::channel(1); let executor = task_executor::TaskExecutor::new( Arc::downgrade(&runtime), diff --git a/beacon_node/network/src/subnet_service/attestation_subnets.rs b/beacon_node/network/src/subnet_service/attestation_subnets.rs index 1cae6299e1..ab9ffb95a6 100644 --- a/beacon_node/network/src/subnet_service/attestation_subnets.rs +++ b/beacon_node/network/src/subnet_service/attestation_subnets.rs @@ -196,7 +196,7 @@ impl AttestationService { /// safely dropped. pub fn validator_subscriptions( &mut self, - subscriptions: Vec, + subscriptions: impl Iterator, ) -> Result<(), String> { // If the node is in a proposer-only state, we ignore all subnet subscriptions. if self.proposer_only { @@ -227,7 +227,6 @@ impl AttestationService { warn!(self.log, "Failed to compute subnet id for validator subscription"; "error" => ?e, - "validator_index" => subscription.validator_index ); continue; } @@ -257,13 +256,11 @@ impl AttestationService { warn!(self.log, "Subscription to subnet error"; "error" => e, - "validator_index" => subscription.validator_index, ); } else { trace!(self.log, "Subscribed to subnet for aggregator duties"; "exact_subnet" => ?exact_subnet, - "validator_index" => subscription.validator_index ); } } diff --git a/beacon_node/network/src/subnet_service/tests/mod.rs b/beacon_node/network/src/subnet_service/tests/mod.rs index 769775a62c..74f3f59df3 100644 --- a/beacon_node/network/src/subnet_service/tests/mod.rs +++ b/beacon_node/network/src/subnet_service/tests/mod.rs @@ -180,14 +180,12 @@ mod attestation_service { use super::*; fn get_subscription( - validator_index: u64, attestation_committee_index: CommitteeIndex, slot: Slot, committee_count_at_slot: u64, is_aggregator: bool, ) -> ValidatorSubscription { ValidatorSubscription { - validator_index, attestation_committee_index, slot, committee_count_at_slot, @@ -204,7 +202,6 @@ mod attestation_service { (0..validator_count) .map(|validator_index| { get_subscription( - validator_index, validator_index, slot, committee_count_at_slot, @@ -217,7 +214,6 @@ mod attestation_service { #[tokio::test] async fn subscribe_current_slot_wait_for_unsubscribe() { // subscription config - let validator_index = 1; let committee_index = 1; // Keep a low subscription slot so that there are no additional subnet discovery events. let subscription_slot = 0; @@ -233,7 +229,6 @@ mod attestation_service { .expect("Could not get current slot"); let subscriptions = vec![get_subscription( - validator_index, committee_index, current_slot + Slot::new(subscription_slot), committee_count, @@ -242,7 +237,7 @@ mod attestation_service { // submit the subscriptions attestation_service - .validator_subscriptions(subscriptions) + .validator_subscriptions(subscriptions.into_iter()) .unwrap(); // not enough time for peer discovery, just subscribe, unsubscribe @@ -253,7 +248,7 @@ mod attestation_service { &attestation_service.beacon_chain.spec, ) .unwrap(); - let expected = vec![ + let expected = [ SubnetServiceMessage::Subscribe(Subnet::Attestation(subnet_id)), SubnetServiceMessage::Unsubscribe(Subnet::Attestation(subnet_id)), ]; @@ -293,7 +288,6 @@ mod attestation_service { #[tokio::test] async fn test_same_subnet_unsubscription() { // subscription config - let validator_index = 1; let committee_count = 1; let subnets_per_node = MainnetEthSpec::default_spec().subnets_per_node as usize; @@ -313,7 +307,6 @@ mod attestation_service { .expect("Could not get current slot"); let sub1 = get_subscription( - validator_index, com1, current_slot + Slot::new(subscription_slot1), committee_count, @@ -321,7 +314,6 @@ mod attestation_service { ); let sub2 = get_subscription( - validator_index, com2, current_slot + Slot::new(subscription_slot2), committee_count, @@ -350,7 +342,7 @@ mod attestation_service { // submit the subscriptions attestation_service - .validator_subscriptions(vec![sub1, sub2]) + .validator_subscriptions(vec![sub1, sub2].into_iter()) .unwrap(); // Unsubscription event should happen at slot 2 (since subnet id's are the same, unsubscription event should be at higher slot + 1) @@ -431,7 +423,7 @@ mod attestation_service { // submit the subscriptions attestation_service - .validator_subscriptions(subscriptions) + .validator_subscriptions(subscriptions.into_iter()) .unwrap(); let events = get_events(&mut attestation_service, Some(131), 10).await; @@ -501,7 +493,7 @@ mod attestation_service { // submit the subscriptions attestation_service - .validator_subscriptions(subscriptions) + .validator_subscriptions(subscriptions.into_iter()) .unwrap(); let events = get_events(&mut attestation_service, None, 3).await; @@ -538,7 +530,6 @@ mod attestation_service { #[tokio::test] async fn test_subscribe_same_subnet_several_slots_apart() { // subscription config - let validator_index = 1; let committee_count = 1; let subnets_per_node = MainnetEthSpec::default_spec().subnets_per_node as usize; @@ -558,7 +549,6 @@ mod attestation_service { .expect("Could not get current slot"); let sub1 = get_subscription( - validator_index, com1, current_slot + Slot::new(subscription_slot1), committee_count, @@ -566,7 +556,6 @@ mod attestation_service { ); let sub2 = get_subscription( - validator_index, com2, current_slot + Slot::new(subscription_slot2), committee_count, @@ -595,7 +584,7 @@ mod attestation_service { // submit the subscriptions attestation_service - .validator_subscriptions(vec![sub1, sub2]) + .validator_subscriptions(vec![sub1, sub2].into_iter()) .unwrap(); // Unsubscription event should happen at the end of the slot. @@ -668,7 +657,7 @@ mod attestation_service { // submit the subscriptions attestation_service - .validator_subscriptions(subscriptions) + .validator_subscriptions(subscriptions.into_iter()) .unwrap(); // There should only be the same subscriptions as there are in the specification, diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index 0d7e7c16c3..67fe871acc 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -10,6 +10,7 @@ use crate::network_beacon_processor::ChainSegmentProcessId; use crate::sync::manager::{BatchProcessResult, Id}; +use crate::sync::network_context::RangeRequestId; use crate::sync::network_context::SyncNetworkContext; use crate::sync::range_sync::{ BatchConfig, BatchId, BatchInfo, BatchOperationOutcome, BatchProcessingResult, BatchState, @@ -55,7 +56,7 @@ impl BatchConfig for BackFillBatchConfig { fn max_batch_processing_attempts() -> u8 { MAX_BATCH_PROCESSING_ATTEMPTS } - fn batch_attempt_hash(blocks: &[RpcBlock]) -> u64 { + fn batch_attempt_hash(blocks: &[RpcBlock]) -> u64 { use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; let mut hasher = DefaultHasher::new(); @@ -332,7 +333,16 @@ impl BackFillSync { } // If we have run out of peers in which to retry this batch, the backfill state // transitions to a paused state. - self.retry_batch_download(network, id)?; + // We still need to reset the state for all the affected batches, so we should not + // short circuit early + if self.retry_batch_download(network, id).is_err() { + debug!( + self.log, + "Batch could not be retried"; + "batch_id" => id, + "error" => "no synced peers" + ); + } } else { debug!(self.log, "Batch not found while removing peer"; "peer" => %peer_id, "batch" => id) @@ -952,7 +962,12 @@ impl BackFillSync { ) -> Result<(), BackFillError> { if let Some(batch) = self.batches.get_mut(&batch_id) { let (request, is_blob_batch) = batch.to_blocks_by_range_request(); - match network.backfill_blocks_by_range_request(peer, is_blob_batch, request, batch_id) { + match network.blocks_and_blobs_by_range_request( + peer, + is_blob_batch, + request, + RangeRequestId::BackfillSync { batch_id }, + ) { Ok(request_id) => { // inform the batch about the new request if let Err(e) = batch.start_downloading_from_peer(peer, request_id) { diff --git a/beacon_node/network/src/sync/block_lookups/common.rs b/beacon_node/network/src/sync/block_lookups/common.rs index d989fbb336..a46e7d0937 100644 --- a/beacon_node/network/src/sync/block_lookups/common.rs +++ b/beacon_node/network/src/sync/block_lookups/common.rs @@ -8,7 +8,7 @@ use crate::sync::block_lookups::{ use crate::sync::manager::{BlockProcessType, Id, SingleLookupReqId}; use crate::sync::network_context::SyncNetworkContext; use beacon_chain::block_verification_types::RpcBlock; -use beacon_chain::data_availability_checker::{AvailabilityView, ChildComponents}; +use beacon_chain::data_availability_checker::ChildComponents; use beacon_chain::{get_block_root, BeaconChainTypes}; use lighthouse_network::rpc::methods::BlobsByRootRequest; use lighthouse_network::rpc::BlocksByRootRequest; @@ -17,7 +17,7 @@ use std::ops::IndexMut; use std::sync::Arc; use std::time::Duration; use types::blob_sidecar::{BlobIdentifier, FixedBlobSidecarList}; -use types::{BlobSidecar, ChainSpec, EthSpec, Hash256, SignedBeaconBlock}; +use types::{BlobSidecar, ChainSpec, Hash256, SignedBeaconBlock}; #[derive(Debug, Copy, Clone)] pub enum ResponseType { @@ -25,7 +25,7 @@ pub enum ResponseType { Blob, } -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] pub enum LookupType { Current, Parent, @@ -119,6 +119,7 @@ pub trait RequestState { let id = SingleLookupReqId { id, req_counter: self.get_state().req_counter, + lookup_type: L::lookup_type(), }; Self::make_request(id, peer_id, request, cx) } @@ -265,7 +266,7 @@ impl RequestState for BlockRequestState request: Self::RequestType, cx: &SyncNetworkContext, ) -> Result<(), LookupRequestError> { - cx.block_lookup_request(id, peer_id, request, L::lookup_type()) + cx.block_lookup_request(id, peer_id, request) .map_err(LookupRequestError::SendFailed) } @@ -365,33 +366,41 @@ impl RequestState for BlobRequestState, ) -> Result<(), LookupRequestError> { - cx.blob_lookup_request(id, peer_id, request, L::lookup_type()) + cx.blob_lookup_request(id, peer_id, request) .map_err(LookupRequestError::SendFailed) } fn verify_response_inner( &mut self, - _expected_block_root: Hash256, + expected_block_root: Hash256, blob: Option, peer_id: PeerId, ) -> Result>, LookupVerifyError> { match blob { Some(blob) => { let received_id = blob.id(); - if !self.requested_ids.contains(&received_id) { - self.state.register_failure_downloading(); - Err(LookupVerifyError::UnrequestedBlobId) - } else { - // State should remain downloading until we receive the stream terminator. - self.requested_ids.remove(&received_id); - let blob_index = blob.index; - if blob_index >= T::EthSpec::max_blobs_per_block() as u64 { - return Err(LookupVerifyError::InvalidIndex(blob.index)); - } - *self.blob_download_queue.index_mut(blob_index as usize) = Some(blob); - Ok(None) + if !self.requested_ids.contains(&received_id) { + Err(LookupVerifyError::UnrequestedBlobId(received_id)) + } else if !blob.verify_blob_sidecar_inclusion_proof().unwrap_or(false) { + Err(LookupVerifyError::InvalidInclusionProof) + } else if blob.block_root() != expected_block_root { + Err(LookupVerifyError::UnrequestedHeader) + } else { + Ok(()) } + .map_err(|e| { + self.state.register_failure_downloading(); + e + })?; + + // State should remain downloading until we receive the stream terminator. + self.requested_ids.remove(&received_id); + + // The inclusion proof check above ensures `blob.index` is < MAX_BLOBS_PER_BLOCK + let blob_index = blob.index; + *self.blob_download_queue.index_mut(blob_index as usize) = Some(blob); + Ok(None) } None => { self.state.state = State::Processing { peer_id }; diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index 62cdc4fa22..949e1762ac 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -40,7 +40,7 @@ mod single_block_lookup; #[cfg(test)] mod tests; -pub type DownloadedBlock = (Hash256, RpcBlock); +pub type DownloadedBlock = (Hash256, RpcBlock); const FAILED_CHAINS_CACHE_EXPIRY_SECONDS: u64 = 60; pub const SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS: u8 = 3; @@ -76,6 +76,24 @@ impl BlockLookups { } } + #[cfg(test)] + pub(crate) fn active_single_lookups(&self) -> Vec { + self.single_block_lookups.keys().cloned().collect() + } + + #[cfg(test)] + pub(crate) fn active_parent_lookups(&self) -> Vec { + self.parent_lookups + .iter() + .map(|r| r.chain_hash()) + .collect::>() + } + + #[cfg(test)] + pub(crate) fn failed_chains_contains(&mut self, chain_hash: &Hash256) -> bool { + self.failed_chains.contains(chain_hash) + } + /* Lookup requests */ /// Creates a lookup for the block with the given `block_root` and immediately triggers it. @@ -162,6 +180,7 @@ impl BlockLookups { // If the block was already downloaded, or is being downloaded in this moment, do not // request it. + trace!(self.log, "Already searching for block in a parent lookup request"; "block_root" => ?block_root); return; } @@ -171,6 +190,7 @@ impl BlockLookups { .any(|(hashes, _last_parent_request)| hashes.contains(&block_root)) { // we are already processing this block, ignore it. + trace!(self.log, "Already processing block in a parent request"; "block_root" => ?block_root); return; } @@ -217,19 +237,27 @@ impl BlockLookups { // Make sure this block is not already downloaded, and that neither it or its parent is // being searched for. if let Some(parent_lookup) = self.parent_lookups.iter_mut().find(|parent_req| { - parent_req.contains_block(&block_root) || parent_req.is_for_block(block_root) + parent_req.contains_block(&parent_root) || parent_req.is_for_block(parent_root) }) { parent_lookup.add_peer(peer_id); // we are already searching for this block, ignore it + debug!(self.log, "Already searching for parent block"; + "block_root" => ?block_root, "parent_root" => ?parent_root); return; } if self .processing_parent_lookups - .values() - .any(|(hashes, _peers)| hashes.contains(&block_root) || hashes.contains(&parent_root)) + .iter() + .any(|(chain_hash, (hashes, _peers))| { + chain_hash == &block_root + || hashes.contains(&block_root) + || hashes.contains(&parent_root) + }) { // we are already processing this block, ignore it. + debug!(self.log, "Already processing parent block"; + "block_root" => ?block_root, "parent_root" => ?parent_root); return; } let parent_lookup = ParentLookup::new( @@ -239,6 +267,9 @@ impl BlockLookups { self.da_checker.clone(), cx, ); + + debug!(self.log, "Created new parent lookup"; "block_root" => ?block_root, "parent_root" => ?parent_root); + self.request_parent(parent_lookup, cx); } @@ -298,6 +329,15 @@ impl BlockLookups { }; let expected_block_root = lookup.block_root(); + if response.is_some() { + debug!(self.log, + "Peer returned response for single lookup"; + "peer_id" => %peer_id , + "id" => ?id, + "block_root" => ?expected_block_root, + "response_type" => ?response_type, + ); + } match self.single_lookup_response_inner::(peer_id, response, seen_timestamp, cx, lookup) { @@ -478,6 +518,16 @@ impl BlockLookups { return; }; + if response.is_some() { + debug!(self.log, + "Peer returned response for parent lookup"; + "peer_id" => %peer_id , + "id" => ?id, + "block_root" => ?parent_lookup.current_parent_request.block_request_state.requested_block_root, + "response_type" => ?R::response_type(), + ); + } + match self.parent_lookup_response_inner::( peer_id, response, @@ -540,11 +590,13 @@ impl BlockLookups { | ParentVerifyError::NoBlockReturned | ParentVerifyError::NotEnoughBlobsReturned | ParentVerifyError::ExtraBlocksReturned - | ParentVerifyError::UnrequestedBlobId + | ParentVerifyError::UnrequestedBlobId(_) + | ParentVerifyError::InvalidInclusionProof + | ParentVerifyError::UnrequestedHeader | ParentVerifyError::ExtraBlobsReturned | ParentVerifyError::InvalidIndex(_) => { let e = e.into(); - warn!(self.log, "Peer sent invalid response to parent request."; + warn!(self.log, "Peer sent invalid response to parent request"; "peer_id" => %peer_id, "reason" => %e); // We do not tolerate these kinds of errors. We will accept a few but these are signs @@ -630,7 +682,7 @@ impl BlockLookups { .position(|req| req.check_peer_disconnected(peer_id).is_err()) { let parent_lookup = self.parent_lookups.remove(pos); - trace!(self.log, "Parent lookup's peer disconnected"; &parent_lookup); + debug!(self.log, "Dropping parent lookup after peer disconnected"; &parent_lookup); self.request_parent(parent_lookup, cx); } } @@ -639,7 +691,7 @@ impl BlockLookups { pub fn parent_lookup_failed>( &mut self, id: SingleLookupReqId, - peer_id: PeerId, + peer_id: &PeerId, cx: &SyncNetworkContext, error: RPCError, ) { @@ -714,20 +766,27 @@ impl BlockLookups { cx: &mut SyncNetworkContext, ) { let Some(mut lookup) = self.single_block_lookups.remove(&target_id) else { + debug!(self.log, "Unknown single block lookup"; "target_id" => target_id); return; }; let root = lookup.block_root(); let request_state = R::request_state_mut(&mut lookup); - let Ok(peer_id) = request_state.get_state().processing_peer() else { - return; + let peer_id = match request_state.get_state().processing_peer() { + Ok(peer_id) => peer_id, + Err(e) => { + debug!(self.log, "Attempting to process single block lookup in bad state"; "id" => target_id, "response_type" => ?R::response_type(), "error" => e); + return; + } }; debug!( self.log, "Block component processed for lookup"; "response_type" => ?R::response_type(), "block_root" => ?root, + "result" => ?result, + "id" => target_id, ); match result { @@ -811,7 +870,7 @@ impl BlockLookups { let root = lookup.block_root(); trace!(self.log, "Single block processing failed"; "block" => %root, "error" => %e); match e { - BlockError::BlockIsAlreadyKnown => { + BlockError::BlockIsAlreadyKnown(_) => { // No error here return Ok(None); } @@ -898,17 +957,17 @@ impl BlockLookups { match &result { BlockProcessingResult::Ok(status) => match status { AvailabilityProcessingStatus::Imported(block_root) => { - trace!(self.log, "Parent block processing succeeded"; &parent_lookup, "block_root" => ?block_root) + debug!(self.log, "Parent block processing succeeded"; &parent_lookup, "block_root" => ?block_root) } AvailabilityProcessingStatus::MissingComponents(_, block_root) => { - trace!(self.log, "Parent missing parts, triggering single block lookup "; &parent_lookup,"block_root" => ?block_root) + debug!(self.log, "Parent missing parts, triggering single block lookup"; &parent_lookup,"block_root" => ?block_root) } }, BlockProcessingResult::Err(e) => { - trace!(self.log, "Parent block processing failed"; &parent_lookup, "error" => %e) + debug!(self.log, "Parent block processing failed"; &parent_lookup, "error" => %e) } BlockProcessingResult::Ignored => { - trace!( + debug!( self.log, "Parent block processing job was ignored"; "action" => "re-requesting block", @@ -954,7 +1013,7 @@ impl BlockLookups { self.request_parent(parent_lookup, cx); } BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(_)) - | BlockProcessingResult::Err(BlockError::BlockIsAlreadyKnown { .. }) => { + | BlockProcessingResult::Err(BlockError::BlockIsAlreadyKnown(_)) => { // Check if the beacon processor is available let Some(beacon_processor) = cx.beacon_processor_if_enabled() else { return trace!( @@ -1223,7 +1282,7 @@ impl BlockLookups { ) -> Result<(), LookupRequestError> { match cx.beacon_processor_if_enabled() { Some(beacon_processor) => { - trace!(self.log, "Sending block for processing"; "block" => ?block_root, "process" => ?process_type); + debug!(self.log, "Sending block for processing"; "block" => ?block_root, "process" => ?process_type); if let Err(e) = beacon_processor.send_rpc_beacon_block( block_root, block, diff --git a/beacon_node/network/src/sync/block_lookups/parent_lookup.rs b/beacon_node/network/src/sync/block_lookups/parent_lookup.rs index 5c2e90b48c..a3cdfd7b00 100644 --- a/beacon_node/network/src/sync/block_lookups/parent_lookup.rs +++ b/beacon_node/network/src/sync/block_lookups/parent_lookup.rs @@ -12,6 +12,7 @@ use std::collections::VecDeque; use std::sync::Arc; use store::Hash256; use strum::IntoStaticStr; +use types::blob_sidecar::BlobIdentifier; /// How many attempts we try to find a parent of a block before we give up trying. pub(crate) const PARENT_FAIL_TOLERANCE: u8 = 5; @@ -36,7 +37,9 @@ pub enum ParentVerifyError { NoBlockReturned, NotEnoughBlobsReturned, ExtraBlocksReturned, - UnrequestedBlobId, + UnrequestedBlobId(BlobIdentifier), + InvalidInclusionProof, + UnrequestedHeader, ExtraBlobsReturned, InvalidIndex(u64), PreviousFailure { parent_root: Hash256 }, @@ -124,14 +127,14 @@ impl ParentLookup { .update_requested_parent_block(next_parent) } - pub fn block_processing_peer(&self) -> Result { + pub fn block_processing_peer(&self) -> Result { self.current_parent_request .block_request_state .state .processing_peer() } - pub fn blob_processing_peer(&self) -> Result { + pub fn blob_processing_peer(&self) -> Result { self.current_parent_request .blob_request_state .state @@ -242,7 +245,9 @@ impl From for ParentVerifyError { E::RootMismatch => ParentVerifyError::RootMismatch, E::NoBlockReturned => ParentVerifyError::NoBlockReturned, E::ExtraBlocksReturned => ParentVerifyError::ExtraBlocksReturned, - E::UnrequestedBlobId => ParentVerifyError::UnrequestedBlobId, + E::UnrequestedBlobId(blob_id) => ParentVerifyError::UnrequestedBlobId(blob_id), + E::InvalidInclusionProof => ParentVerifyError::InvalidInclusionProof, + E::UnrequestedHeader => ParentVerifyError::UnrequestedHeader, E::ExtraBlobsReturned => ParentVerifyError::ExtraBlobsReturned, E::InvalidIndex(index) => ParentVerifyError::InvalidIndex(index), E::NotEnoughBlobsReturned => ParentVerifyError::NotEnoughBlobsReturned, 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 8c60621f1c..a312f6e970 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 @@ -3,10 +3,10 @@ use crate::sync::block_lookups::common::{Lookup, RequestState}; use crate::sync::block_lookups::Id; use crate::sync::network_context::SyncNetworkContext; use beacon_chain::block_verification_types::RpcBlock; +use beacon_chain::data_availability_checker::ChildComponents; use beacon_chain::data_availability_checker::{ AvailabilityCheckError, DataAvailabilityChecker, MissingBlobs, }; -use beacon_chain::data_availability_checker::{AvailabilityView, ChildComponents}; use beacon_chain::BeaconChainTypes; use lighthouse_network::PeerAction; use slog::{trace, Logger}; @@ -16,7 +16,7 @@ use std::marker::PhantomData; use std::sync::Arc; use store::Hash256; use strum::IntoStaticStr; -use types::blob_sidecar::FixedBlobSidecarList; +use types::blob_sidecar::{BlobIdentifier, FixedBlobSidecarList}; use types::EthSpec; #[derive(Debug, PartialEq, Eq)] @@ -31,7 +31,9 @@ pub enum LookupVerifyError { RootMismatch, NoBlockReturned, ExtraBlocksReturned, - UnrequestedBlobId, + UnrequestedBlobId(BlobIdentifier), + InvalidInclusionProof, + UnrequestedHeader, ExtraBlobsReturned, NotEnoughBlobsReturned, InvalidIndex(u64), @@ -247,7 +249,7 @@ impl SingleBlockLookup { /// Returns `true` if the block has already been downloaded. pub(crate) fn block_already_downloaded(&self) -> bool { if let Some(components) = self.child_components.as_ref() { - components.block_exists() + components.downloaded_block.is_some() } else { self.da_checker.has_block(&self.block_root()) } @@ -257,7 +259,9 @@ impl SingleBlockLookup { /// of which blobs still need to be requested. Returns `true` if there are no more blobs to /// request. pub(crate) fn blobs_already_downloaded(&mut self) -> bool { - self.update_blobs_request(); + if matches!(self.blob_request_state.state.state, State::AwaitingDownload) { + self.update_blobs_request(); + } self.blob_request_state.requested_ids.is_empty() } @@ -272,19 +276,17 @@ impl SingleBlockLookup { pub(crate) fn missing_blob_ids(&self) -> MissingBlobs { let block_root = self.block_root(); if let Some(components) = self.child_components.as_ref() { - self.da_checker.get_missing_blob_ids(block_root, components) + self.da_checker.get_missing_blob_ids( + block_root, + components.downloaded_block.as_ref().map(|b| b.as_ref()), + &components.downloaded_blobs, + ) } else { - let Some(processing_availability_view) = - self.da_checker.get_processing_components(block_root) - else { - return MissingBlobs::new_without_block(block_root, self.da_checker.is_deneb()); - }; - self.da_checker - .get_missing_blob_ids(block_root, &processing_availability_view) + self.da_checker.get_missing_blob_ids_with(block_root) } } - /// Penalizes a blob peer if it should have blobs but didn't return them to us. + /// Penalizes a blob peer if it should have blobs but didn't return them to us. pub fn penalize_blob_peer(&mut self, cx: &SyncNetworkContext) { if let Ok(blob_peer) = self.blob_request_state.state.processing_peer() { cx.report_peer( @@ -317,13 +319,13 @@ impl SingleBlockLookup { } /// The state of the blob request component of a `SingleBlockLookup`. -pub struct BlobRequestState { +pub struct BlobRequestState { /// The latest picture of which blobs still need to be requested. This includes information /// from both block/blobs downloaded in the network layer and any blocks/blobs that exist in /// the data availability checker. pub requested_ids: MissingBlobs, /// Where we store blobs until we receive the stream terminator. - pub blob_download_queue: FixedBlobSidecarList, + pub blob_download_queue: FixedBlobSidecarList, pub state: SingleLookupRequestState, _phantom: PhantomData, } @@ -455,11 +457,10 @@ impl SingleLookupRequestState { /// Returns the id peer we downloaded from if we have downloaded a verified block, otherwise /// returns an error. - pub fn processing_peer(&self) -> Result { - if let State::Processing { peer_id } = &self.state { - Ok(*peer_id) - } else { - Err(()) + pub fn processing_peer(&self) -> Result { + match &self.state { + State::Processing { peer_id } => Ok(*peer_id), + other => Err(format!("not in processing state: {}", other).to_string()), } } } @@ -515,11 +516,20 @@ impl slog::Value for SingleLookupRequestState { } } +impl std::fmt::Display for State { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + State::AwaitingDownload => write!(f, "AwaitingDownload"), + State::Downloading { .. } => write!(f, "Downloading"), + State::Processing { .. } => write!(f, "Processing"), + } + } +} + #[cfg(test)] mod tests { use super::*; use crate::sync::block_lookups::common::LookupType; - use crate::sync::block_lookups::common::{Lookup, RequestState}; use beacon_chain::builder::Witness; use beacon_chain::eth1_chain::CachingEth1Backend; use sloggers::null::NullLoggerBuilder; @@ -529,7 +539,7 @@ mod tests { use store::{HotColdDB, MemoryStore, StoreConfig}; use types::{ test_utils::{SeedableRng, TestRandom, XorShiftRng}, - ChainSpec, EthSpec, MinimalEthSpec as E, SignedBeaconBlock, Slot, + ChainSpec, MinimalEthSpec as E, SignedBeaconBlock, Slot, }; fn rand_block() -> SignedBeaconBlock { diff --git a/beacon_node/network/src/sync/block_lookups/tests.rs b/beacon_node/network/src/sync/block_lookups/tests.rs index c506696b9d..6d50fe6320 100644 --- a/beacon_node/network/src/sync/block_lookups/tests.rs +++ b/beacon_node/network/src/sync/block_lookups/tests.rs @@ -1,7 +1,8 @@ use crate::network_beacon_processor::NetworkBeaconProcessor; use crate::service::RequestId; -use crate::sync::manager::{RequestId as SyncId, SingleLookupReqId}; +use crate::sync::manager::{RequestId as SyncRequestId, SingleLookupReqId, SyncManager}; +use crate::sync::SyncMessage; use crate::NetworkMessage; use std::sync::Arc; @@ -15,29 +16,65 @@ use beacon_chain::test_utils::{ }; use beacon_processor::WorkEvent; use lighthouse_network::rpc::RPCResponseErrorCode; +use lighthouse_network::types::SyncState; use lighthouse_network::{NetworkGlobals, Request}; use slot_clock::{ManualSlotClock, SlotClock, TestingSlotClock}; use store::MemoryStore; use tokio::sync::mpsc; use types::{ test_utils::{SeedableRng, XorShiftRng}, - BlobSidecar, EthSpec, ForkName, MinimalEthSpec as E, SignedBeaconBlock, + BlobSidecar, ForkName, MinimalEthSpec as E, SignedBeaconBlock, }; type T = Witness, E, MemoryStore, MemoryStore>; +/// This test utility enables integration testing of Lighthouse sync components. +/// +/// It covers the following: +/// 1. Sending `SyncMessage` to `SyncManager` to trigger `RangeSync`, `BackFillSync` and `BlockLookups` behaviours. +/// 2. Making assertions on `WorkEvent`s received from sync +/// 3. Making assertion on `NetworkMessage` received from sync (Outgoing RPC requests). +/// +/// The test utility covers testing the interactions from and to `SyncManager`. In diagram form: +/// +-----------------+ +/// | BeaconProcessor | +/// +---------+-------+ +/// ^ | +/// | | +/// WorkEvent | | SyncMsg +/// | | (Result) +/// | v +/// +--------+ +-----+-----------+ +----------------+ +/// | Router +----------->| SyncManager +------------>| NetworkService | +/// +--------+ SyncMsg +-----------------+ NetworkMsg +----------------+ +/// (RPC resp) | - RangeSync | (RPC req) +/// +-----------------+ +/// | - BackFillSync | +/// +-----------------+ +/// | - BlockLookups | +/// +-----------------+ struct TestRig { + /// Receiver for `BeaconProcessor` events (e.g. block processing results). beacon_processor_rx: mpsc::Receiver>, + /// Receiver for `NetworkMessage` (e.g. outgoing RPC requests from sync) network_rx: mpsc::UnboundedReceiver>, + /// Stores all `NetworkMessage`s received from `network_recv`. (e.g. outgoing RPC requests) + network_rx_queue: Vec>, + /// To send `SyncMessage`. For sending RPC responses or block processing results to sync. + sync_manager: SyncManager, + /// To manipulate sync state and peer connection status + network_globals: Arc>, + /// `rng` for generating test blocks and blobs. rng: XorShiftRng, - harness: BeaconChainHarness, + fork_name: ForkName, } const D: Duration = Duration::new(0, 0); impl TestRig { - fn test_setup(enable_log: bool) -> (BlockLookups, SyncNetworkContext, Self) { - let log = build_log(slog::Level::Debug, enable_log); + fn test_setup() -> Self { + let enable_log = cfg!(feature = "test_logger"); + let log = build_log(slog::Level::Trace, enable_log); // Initialise a new beacon chain let harness = BeaconChainHarness::>::builder(E) @@ -56,91 +93,409 @@ impl TestRig { 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 (beacon_processor, beacon_processor_rx) = NetworkBeaconProcessor::null_for_testing( + globals, + chain.clone(), + harness.runtime.task_executor.clone(), + log.clone(), + ); + + let (_sync_send, sync_recv) = mpsc::unbounded_channel::>(); + + let fork_name = chain.spec.fork_name_at_slot::(chain.slot().unwrap()); + + // All current tests expect synced and EL online state + beacon_processor + .network_globals + .set_sync_state(SyncState::Synced); + let rng = XorShiftRng::from_seed([42; 16]); - let rig = TestRig { + TestRig { beacon_processor_rx, network_rx, + network_rx_queue: vec![], rng, - harness, - }; - - let bl = BlockLookups::new( - chain.data_availability_checker.clone(), - log.new(slog::o!("component" => "block_lookups")), - ); - let cx = { - SyncNetworkContext::new( - network_tx, - Arc::new(network_beacon_processor), + network_globals: beacon_processor.network_globals.clone(), + sync_manager: SyncManager::new( chain, - log.new(slog::o!("component" => "network_context")), - ) - }; - - (bl, cx, rig) + network_tx, + beacon_processor.into(), + sync_recv, + log.clone(), + ), + fork_name, + } } - fn rand_block(&mut self, fork_name: ForkName) -> SignedBeaconBlock { - self.rand_block_and_blobs(fork_name, NumBlobs::None).0 + fn test_setup_after_deneb() -> Option { + let r = Self::test_setup(); + if r.after_deneb() { + Some(r) + } else { + None + } + } + + fn after_deneb(&self) -> bool { + matches!(self.fork_name, ForkName::Deneb | ForkName::Electra) + } + + fn trigger_unknown_parent_block(&mut self, peer_id: PeerId, block: Arc>) { + let block_root = block.canonical_root(); + self.send_sync_message(SyncMessage::UnknownParentBlock( + peer_id, + RpcBlock::new_without_blobs(Some(block_root), block), + block_root, + )) + } + + fn trigger_unknown_parent_blob(&mut self, peer_id: PeerId, blob: BlobSidecar) { + self.send_sync_message(SyncMessage::UnknownParentBlob(peer_id, blob.into())); + } + + fn trigger_unknown_block_from_attestation(&mut self, block_root: Hash256, peer_id: PeerId) { + self.send_sync_message(SyncMessage::UnknownBlockHashFromAttestation( + peer_id, block_root, + )); + } + + fn rand_block(&mut self) -> SignedBeaconBlock { + self.rand_block_and_blobs(NumBlobs::None).0 } fn rand_block_and_blobs( &mut self, - fork_name: ForkName, num_blobs: NumBlobs, ) -> (SignedBeaconBlock, Vec>) { + let fork_name = self.fork_name; let rng = &mut self.rng; generate_rand_block_and_blobs::(fork_name, num_blobs, rng) } + pub fn rand_block_and_parent( + &mut self, + ) -> (SignedBeaconBlock, SignedBeaconBlock, Hash256, Hash256) { + let parent = self.rand_block(); + let parent_root = parent.canonical_root(); + let mut block = self.rand_block(); + *block.message_mut().parent_root_mut() = parent_root; + let block_root = block.canonical_root(); + (parent, block, parent_root, block_root) + } + + fn send_sync_message(&mut self, sync_message: SyncMessage) { + self.sync_manager.handle_message(sync_message); + } + + fn active_single_lookups_count(&self) -> usize { + self.sync_manager.active_single_lookups().len() + } + + fn active_parent_lookups(&self) -> Vec { + self.sync_manager.active_parent_lookups() + } + + fn active_parent_lookups_count(&self) -> usize { + self.sync_manager.active_parent_lookups().len() + } + + fn failed_chains_contains(&mut self, chain_hash: &Hash256) -> bool { + self.sync_manager.failed_chains_contains(chain_hash) + } + #[track_caller] - fn expect_lookup_request(&mut self, response_type: ResponseType) -> SingleLookupReqId { - match response_type { - ResponseType::Block => match self.network_rx.try_recv() { - Ok(NetworkMessage::SendRequest { - peer_id: _, - request: Request::BlocksByRoot(_request), - request_id: RequestId::Sync(SyncId::SingleBlock { id }), - }) => id, - other => { - panic!("Expected block request, found {:?}", other); - } - }, - ResponseType::Blob => match self.network_rx.try_recv() { - Ok(NetworkMessage::SendRequest { - peer_id: _, - request: Request::BlobsByRoot(_request), - request_id: RequestId::Sync(SyncId::SingleBlob { id }), - }) => id, - other => { - panic!("Expected blob request, found {:?}", other); - } + fn assert_parent_lookups_consistency(&self) { + let hashes = self.active_parent_lookups(); + let expected = hashes.len(); + assert_eq!( + expected, + hashes + .into_iter() + .collect::>() + .len(), + "duplicated chain hashes in parent queue" + ) + } + + fn new_connected_peer(&mut self) -> PeerId { + let peer_id = PeerId::random(); + self.network_globals + .peers + .write() + .__add_connected_peer_testing_only(&peer_id); + peer_id + } + + fn parent_chain_processed(&mut self, chain_hash: Hash256, result: BatchProcessResult) { + self.send_sync_message(SyncMessage::BatchProcessed { + sync_type: ChainSegmentProcessId::ParentLookup(chain_hash), + result, + }) + } + + fn parent_chain_processed_success(&mut self, chain_hash: Hash256) { + self.parent_chain_processed( + chain_hash, + BatchProcessResult::Success { + was_non_empty: true, }, + ) + } + + fn parent_block_processed(&mut self, chain_hash: Hash256, result: BlockProcessingResult) { + self.send_sync_message(SyncMessage::BlockComponentProcessed { + process_type: BlockProcessType::ParentLookup { chain_hash }, + result, + }); + } + + fn parent_block_processed_imported(&mut self, chain_hash: Hash256) { + self.parent_block_processed( + chain_hash, + BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(chain_hash)), + ); + } + + fn single_block_component_processed( + &mut self, + id: SingleLookupReqId, + result: BlockProcessingResult, + ) { + self.send_sync_message(SyncMessage::BlockComponentProcessed { + process_type: BlockProcessType::SingleBlock { id: id.id }, + result, + }) + } + + fn single_block_component_processed_imported( + &mut self, + id: SingleLookupReqId, + block_root: Hash256, + ) { + self.single_block_component_processed( + id, + BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(block_root)), + ) + } + + fn single_blob_component_processed( + &mut self, + id: SingleLookupReqId, + result: BlockProcessingResult, + ) { + self.send_sync_message(SyncMessage::BlockComponentProcessed { + process_type: BlockProcessType::SingleBlob { id: id.id }, + result, + }) + } + + fn parent_lookup_block_response( + &mut self, + id: SingleLookupReqId, + peer_id: PeerId, + beacon_block: Option>>, + ) { + self.send_sync_message(SyncMessage::RpcBlock { + request_id: SyncRequestId::SingleBlock { id }, + peer_id, + beacon_block, + seen_timestamp: D, + }); + } + + fn single_lookup_block_response( + &mut self, + id: SingleLookupReqId, + peer_id: PeerId, + beacon_block: Option>>, + ) { + self.send_sync_message(SyncMessage::RpcBlock { + request_id: SyncRequestId::SingleBlock { id }, + peer_id, + beacon_block, + seen_timestamp: D, + }); + } + + fn parent_lookup_blob_response( + &mut self, + id: SingleLookupReqId, + peer_id: PeerId, + blob_sidecar: Option>>, + ) { + self.send_sync_message(SyncMessage::RpcBlob { + request_id: SyncRequestId::SingleBlob { id }, + peer_id, + blob_sidecar, + seen_timestamp: D, + }); + } + + fn single_lookup_blob_response( + &mut self, + id: SingleLookupReqId, + peer_id: PeerId, + blob_sidecar: Option>>, + ) { + self.send_sync_message(SyncMessage::RpcBlob { + request_id: SyncRequestId::SingleBlob { id }, + peer_id, + blob_sidecar, + seen_timestamp: D, + }); + } + + fn parent_lookup_failed(&mut self, id: SingleLookupReqId, peer_id: PeerId, error: RPCError) { + self.send_sync_message(SyncMessage::RpcError { + peer_id, + request_id: SyncRequestId::SingleBlock { id }, + error, + }) + } + + fn parent_lookup_failed_unavailable(&mut self, id: SingleLookupReqId, peer_id: PeerId) { + self.parent_lookup_failed( + id, + peer_id, + RPCError::ErrorResponse( + RPCResponseErrorCode::ResourceUnavailable, + "older than deneb".into(), + ), + ); + } + + fn single_lookup_failed(&mut self, id: SingleLookupReqId, peer_id: PeerId, error: RPCError) { + self.send_sync_message(SyncMessage::RpcError { + peer_id, + request_id: SyncRequestId::SingleBlock { id }, + error, + }) + } + + fn peer_disconnected(&mut self, peer_id: PeerId) { + self.send_sync_message(SyncMessage::Disconnect(peer_id)); + } + + fn drain_network_rx(&mut self) { + while let Ok(event) = self.network_rx.try_recv() { + self.network_rx_queue.push(event); + } + } + + fn pop_received_network_event) -> Option>( + &mut self, + predicate_transform: F, + ) -> Result { + self.drain_network_rx(); + + if let Some(index) = self + .network_rx_queue + .iter() + .position(|x| predicate_transform(x).is_some()) + { + // Transform the item, knowing that it won't be None because we checked it in the position predicate. + let transformed = predicate_transform(&self.network_rx_queue[index]).unwrap(); + self.network_rx_queue.remove(index); + Ok(transformed) + } else { + Err(format!("current network messages {:?}", self.network_rx_queue).to_string()) } } #[track_caller] - fn expect_parent_request(&mut self, response_type: ResponseType) -> SingleLookupReqId { - match response_type { - ResponseType::Block => match self.network_rx.try_recv() { - Ok(NetworkMessage::SendRequest { - peer_id: _, - request: Request::BlocksByRoot(_request), - request_id: RequestId::Sync(SyncId::ParentLookup { id }), - }) => id, - other => panic!("Expected parent request, found {:?}", other), - }, - ResponseType::Blob => match self.network_rx.try_recv() { - Ok(NetworkMessage::SendRequest { - peer_id: _, - request: Request::BlobsByRoot(_request), - request_id: RequestId::Sync(SyncId::ParentLookupBlob { id }), - }) => id, - other => panic!("Expected parent blobs request, found {:?}", other), - }, + fn expect_block_lookup_request(&mut self, for_block: Hash256) -> SingleLookupReqId { + self.pop_received_network_event(|ev| match ev { + NetworkMessage::SendRequest { + peer_id: _, + request: Request::BlocksByRoot(request), + request_id: RequestId::Sync(SyncRequestId::SingleBlock { id }), + } if id.lookup_type == LookupType::Current + && request.block_roots().to_vec().contains(&for_block) => + { + Some(*id) + } + _ => None, + }) + .unwrap_or_else(|e| panic!("Expected block request for {for_block:?}: {e}")) + } + + #[track_caller] + fn expect_blob_lookup_request(&mut self, for_block: Hash256) -> SingleLookupReqId { + self.pop_received_network_event(|ev| match ev { + NetworkMessage::SendRequest { + peer_id: _, + request: Request::BlobsByRoot(request), + request_id: RequestId::Sync(SyncRequestId::SingleBlob { id }), + } if id.lookup_type == LookupType::Current + && request + .blob_ids + .to_vec() + .iter() + .any(|r| r.block_root == for_block) => + { + Some(*id) + } + _ => None, + }) + .unwrap_or_else(|e| panic!("Expected blob request for {for_block:?}: {e}")) + } + + #[track_caller] + fn expect_block_parent_request(&mut self, for_block: Hash256) -> SingleLookupReqId { + self.pop_received_network_event(|ev| match ev { + NetworkMessage::SendRequest { + peer_id: _, + request: Request::BlocksByRoot(request), + request_id: RequestId::Sync(SyncRequestId::SingleBlock { id }), + } if id.lookup_type == LookupType::Parent + && request.block_roots().to_vec().contains(&for_block) => + { + Some(*id) + } + _ => None, + }) + .unwrap_or_else(|e| panic!("Expected block parent request for {for_block:?}: {e}")) + } + + #[track_caller] + fn expect_blob_parent_request(&mut self, for_block: Hash256) -> SingleLookupReqId { + self.pop_received_network_event(|ev| match ev { + NetworkMessage::SendRequest { + peer_id: _, + request: Request::BlobsByRoot(request), + request_id: RequestId::Sync(SyncRequestId::SingleBlob { id }), + } if id.lookup_type == LookupType::Parent + && request + .blob_ids + .to_vec() + .iter() + .all(|r| r.block_root == for_block) => + { + Some(*id) + } + _ => None, + }) + .unwrap_or_else(|e| panic!("Expected blob parent request for {for_block:?}: {e}")) + } + + fn expect_lookup_request_block_and_blobs(&mut self, block_root: Hash256) -> SingleLookupReqId { + let id = self.expect_block_lookup_request(block_root); + // If we're in deneb, a blob request should have been triggered as well, + // we don't require a response because we're generateing 0-blob blocks in this test. + if self.after_deneb() { + let _ = self.expect_blob_lookup_request(block_root); } + id + } + + fn expect_parent_request_block_and_blobs(&mut self, block_root: Hash256) -> SingleLookupReqId { + let id = self.expect_block_parent_request(block_root); + // If we're in deneb, a blob request should have been triggered as well, + // we don't require a response because we're generateing 0-blob blocks in this test. + if self.after_deneb() { + let _ = self.expect_blob_parent_request(block_root); + } + id } #[track_caller] @@ -161,6 +516,23 @@ impl TestRig { } } + fn expect_no_penalty_for(&mut self, peer_id: PeerId) { + self.drain_network_rx(); + let downscore_events = self + .network_rx_queue + .iter() + .filter_map(|ev| match ev { + NetworkMessage::ReportPeer { + peer_id: p_id, msg, .. + } if p_id == &peer_id => Some(msg), + _ => None, + }) + .collect::>(); + if !downscore_events.is_empty() { + panic!("Some downscore events for {peer_id}: {downscore_events:?}"); + } + } + #[track_caller] fn expect_parent_chain_process(&mut self) { match self.beacon_processor_rx.try_recv() { @@ -173,10 +545,10 @@ impl TestRig { #[track_caller] fn expect_empty_network(&mut self) { - assert_eq!( - self.network_rx.try_recv().expect_err("must err"), - mpsc::error::TryRecvError::Empty - ); + self.drain_network_rx(); + if !self.network_rx_queue.is_empty() { + panic!("expected no network events: {:#?}", self.network_rx_queue); + } } #[track_caller] @@ -188,982 +560,537 @@ impl TestRig { } #[track_caller] - pub fn expect_penalty(&mut self) { - match self.network_rx.try_recv() { - Ok(NetworkMessage::ReportPeer { .. }) => {} - other => panic!("Expected peer penalty, found {:?}", other), - } - } - - pub fn block_with_parent( - &mut self, - parent_root: Hash256, - fork_name: ForkName, - ) -> SignedBeaconBlock { - let mut block = self.rand_block(fork_name); - *block.message_mut().parent_root_mut() = parent_root; - block + pub fn expect_penalty(&mut self, peer_id: PeerId) { + self.pop_received_network_event(|ev| match ev { + NetworkMessage::ReportPeer { peer_id: p_id, .. } if p_id == &peer_id => Some(()), + _ => None, + }) + .unwrap_or_else(|_| { + panic!( + "Expected peer penalty for {peer_id}: {:#?}", + self.network_rx_queue + ) + }) } pub fn block_with_parent_and_blobs( &mut self, parent_root: Hash256, - fork_name: ForkName, num_blobs: NumBlobs, ) -> (SignedBeaconBlock, Vec>) { - let (mut block, mut blobs) = self.rand_block_and_blobs(fork_name, num_blobs); + let (mut block, mut blobs) = self.rand_block_and_blobs(num_blobs); *block.message_mut().parent_root_mut() = parent_root; blobs.iter_mut().for_each(|blob| { blob.signed_block_header = block.signed_block_header(); }); (block, blobs) } + + pub fn rand_blockchain(&mut self, depth: usize) -> Vec>> { + let mut blocks = Vec::>>::with_capacity(depth); + while blocks.len() < depth { + let parent = blocks + .last() + .map(|b| b.canonical_root()) + .unwrap_or_else(Hash256::random); + let mut block = self.rand_block(); + *block.message_mut().parent_root_mut() = parent; + blocks.push(block.into()); + } + blocks + } } #[test] fn test_single_block_lookup_happy_path() { - let response_type = ResponseType::Block; - let (mut bl, mut cx, mut rig) = TestRig::test_setup(false); - let fork_name = rig - .harness - .spec - .fork_name_at_slot::(rig.harness.chain.slot().unwrap()); - - let block = rig.rand_block(fork_name); - let peer_id = PeerId::random(); + let mut rig = TestRig::test_setup(); + let block = rig.rand_block(); + let peer_id = rig.new_connected_peer(); let block_root = block.canonical_root(); // Trigger the request - bl.search_block(block_root, &[peer_id], &mut cx); - let id = rig.expect_lookup_request(response_type); - // If we're in deneb, a blob request should have been triggered as well, - // we don't require a response because we're generateing 0-blob blocks in this test. - if matches!(fork_name, ForkName::Deneb) { - let _ = rig.expect_lookup_request(ResponseType::Blob); - } + rig.trigger_unknown_block_from_attestation(block_root, peer_id); + let id = rig.expect_lookup_request_block_and_blobs(block_root); // The peer provides the correct block, should not be penalized. Now the block should be sent // for processing. - bl.single_lookup_response::>( - id, - peer_id, - Some(block.into()), - D, - &cx, - ); + rig.single_lookup_block_response(id, peer_id, Some(block.into())); rig.expect_empty_network(); - rig.expect_block_process(response_type); + rig.expect_block_process(ResponseType::Block); // The request should still be active. - assert_eq!(bl.single_block_lookups.len(), 1); + assert_eq!(rig.active_single_lookups_count(), 1); // Send the stream termination. Peer should have not been penalized, and the request removed // after processing. - bl.single_lookup_response::>(id, peer_id, None, D, &cx); - bl.single_block_component_processed::>( - id.id, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(block_root)), - &mut cx, - ); + rig.single_lookup_block_response(id, peer_id, None); + rig.single_block_component_processed_imported(id, block_root); rig.expect_empty_network(); - assert_eq!(bl.single_block_lookups.len(), 0); + assert_eq!(rig.active_single_lookups_count(), 0); } #[test] fn test_single_block_lookup_empty_response() { - let response_type = ResponseType::Block; - let (mut bl, mut cx, mut rig) = TestRig::test_setup(false); - let fork_name = rig - .harness - .spec - .fork_name_at_slot::(rig.harness.chain.slot().unwrap()); + let mut rig = TestRig::test_setup(); let block_hash = Hash256::random(); - let peer_id = PeerId::random(); + let peer_id = rig.new_connected_peer(); // Trigger the request - bl.search_block(block_hash, &[peer_id], &mut cx); - let id = rig.expect_lookup_request(response_type); - // If we're in deneb, a blob request should have been triggered as well, - // we don't require a response because we're generateing 0-blob blocks in this test. - if matches!(fork_name, ForkName::Deneb) { - let _ = rig.expect_lookup_request(ResponseType::Blob); - } + rig.trigger_unknown_block_from_attestation(block_hash, peer_id); + let id = rig.expect_lookup_request_block_and_blobs(block_hash); // The peer does not have the block. It should be penalized. - bl.single_lookup_response::>(id, peer_id, None, D, &cx); - rig.expect_penalty(); + rig.single_lookup_block_response(id, peer_id, None); + rig.expect_penalty(peer_id); - rig.expect_lookup_request(response_type); // it should be retried + rig.expect_block_lookup_request(block_hash); // it should be retried } #[test] fn test_single_block_lookup_wrong_response() { - let response_type = ResponseType::Block; - let (mut bl, mut cx, mut rig) = TestRig::test_setup(false); + let mut rig = TestRig::test_setup(); - let fork_name = rig - .harness - .spec - .fork_name_at_slot::(rig.harness.chain.slot().unwrap()); let block_hash = Hash256::random(); - let peer_id = PeerId::random(); + let peer_id = rig.new_connected_peer(); // Trigger the request - bl.search_block(block_hash, &[peer_id], &mut cx); - let id = rig.expect_lookup_request(response_type); - // If we're in deneb, a blob request should have been triggered as well, - // we don't require a response because we're generateing 0-blob blocks in this test. - if matches!(fork_name, ForkName::Deneb) { - let _ = rig.expect_lookup_request(ResponseType::Blob); - } + rig.trigger_unknown_block_from_attestation(block_hash, peer_id); + let id = rig.expect_lookup_request_block_and_blobs(block_hash); // Peer sends something else. It should be penalized. - let bad_block = rig.rand_block(fork_name); - bl.single_lookup_response::>( - id, - peer_id, - Some(bad_block.into()), - D, - &cx, - ); - rig.expect_penalty(); - rig.expect_lookup_request(response_type); // should be retried + let bad_block = rig.rand_block(); + rig.single_lookup_block_response(id, peer_id, Some(bad_block.into())); + rig.expect_penalty(peer_id); + rig.expect_block_lookup_request(block_hash); // should be retried // Send the stream termination. This should not produce an additional penalty. - bl.single_lookup_response::>(id, peer_id, None, D, &cx); + rig.single_lookup_block_response(id, peer_id, None); rig.expect_empty_network(); } #[test] fn test_single_block_lookup_failure() { - let response_type = ResponseType::Block; - let (mut bl, mut cx, mut rig) = TestRig::test_setup(false); + let mut rig = TestRig::test_setup(); - let fork_name = rig - .harness - .spec - .fork_name_at_slot::(rig.harness.chain.slot().unwrap()); let block_hash = Hash256::random(); - let peer_id = PeerId::random(); + let peer_id = rig.new_connected_peer(); // Trigger the request - bl.search_block(block_hash, &[peer_id], &mut cx); - let id = rig.expect_lookup_request(response_type); - // If we're in deneb, a blob request should have been triggered as well, - // we don't require a response because we're generateing 0-blob blocks in this test. - if matches!(fork_name, ForkName::Deneb) { - let _ = rig.expect_lookup_request(ResponseType::Blob); - } + rig.trigger_unknown_block_from_attestation(block_hash, peer_id); + let id = rig.expect_lookup_request_block_and_blobs(block_hash); // The request fails. RPC failures are handled elsewhere so we should not penalize the peer. - bl.single_block_lookup_failed::>( - id, - &peer_id, - &cx, - RPCError::UnsupportedProtocol, - ); - rig.expect_lookup_request(response_type); + rig.single_lookup_failed(id, peer_id, RPCError::UnsupportedProtocol); + rig.expect_block_lookup_request(block_hash); rig.expect_empty_network(); } #[test] fn test_single_block_lookup_becomes_parent_request() { - let response_type = ResponseType::Block; - let (mut bl, mut cx, mut rig) = TestRig::test_setup(false); + let mut rig = TestRig::test_setup(); - let fork_name = rig - .harness - .spec - .fork_name_at_slot::(rig.harness.chain.slot().unwrap()); - let block = Arc::new(rig.rand_block(fork_name)); - let peer_id = PeerId::random(); + let block = Arc::new(rig.rand_block()); + let block_root = block.canonical_root(); + let parent_root = block.parent_root(); + let peer_id = rig.new_connected_peer(); // Trigger the request - bl.search_block(block.canonical_root(), &[peer_id], &mut cx); - let id = rig.expect_lookup_request(response_type); - // If we're in deneb, a blob request should have been triggered as well, - // we don't require a response because we're generateing 0-blob blocks in this test. - if matches!(fork_name, ForkName::Deneb) { - let _ = rig.expect_lookup_request(ResponseType::Blob); - } + rig.trigger_unknown_block_from_attestation(block.canonical_root(), peer_id); + let id = rig.expect_lookup_request_block_and_blobs(block_root); // The peer provides the correct block, should not be penalized. Now the block should be sent // for processing. - bl.single_lookup_response::>( - id, - peer_id, - Some(block.clone()), - D, - &cx, - ); + rig.single_lookup_block_response(id, peer_id, Some(block.clone())); rig.expect_empty_network(); - rig.expect_block_process(response_type); + rig.expect_block_process(ResponseType::Block); // The request should still be active. - assert_eq!(bl.single_block_lookups.len(), 1); + assert_eq!(rig.active_single_lookups_count(), 1); // Send the stream termination. Peer should have not been penalized, and the request moved to a // parent request after processing. - bl.single_block_component_processed::>( - id.id, + rig.single_block_component_processed( + id, BlockError::ParentUnknown(RpcBlock::new_without_blobs(None, block)).into(), - &mut cx, ); - assert_eq!(bl.single_block_lookups.len(), 1); - rig.expect_parent_request(response_type); - // If we're in deneb, a blob request should have been triggered as well, - // we don't require a response because we're generateing 0-blob blocks in this test. - if matches!(fork_name, ForkName::Deneb) { - let _ = rig.expect_parent_request(ResponseType::Blob); - } + assert_eq!(rig.active_single_lookups_count(), 1); + rig.expect_parent_request_block_and_blobs(parent_root); rig.expect_empty_network(); - assert_eq!(bl.parent_lookups.len(), 1); + assert_eq!(rig.active_parent_lookups_count(), 1); } #[test] fn test_parent_lookup_happy_path() { - let response_type = ResponseType::Block; - let (mut bl, mut cx, mut rig) = TestRig::test_setup(false); + let mut rig = TestRig::test_setup(); - let fork_name = rig - .harness - .spec - .fork_name_at_slot::(rig.harness.chain.slot().unwrap()); - let parent = rig.rand_block(fork_name); - let block = rig.block_with_parent(parent.canonical_root(), fork_name); - let chain_hash = block.canonical_root(); - let peer_id = PeerId::random(); - let block_root = block.canonical_root(); - let parent_root = block.parent_root(); - let slot = block.slot(); + let (parent, block, parent_root, block_root) = rig.rand_block_and_parent(); + let peer_id = rig.new_connected_peer(); // Trigger the request - bl.search_parent(slot, block_root, parent_root, peer_id, &mut cx); - let id = rig.expect_parent_request(response_type); - // If we're in deneb, a blob request should have been triggered as well, - // we don't require a response because we're generateing 0-blob blocks in this test. - if matches!(fork_name, ForkName::Deneb) { - let _ = rig.expect_parent_request(ResponseType::Blob); - } + rig.trigger_unknown_parent_block(peer_id, block.into()); + let id = rig.expect_parent_request_block_and_blobs(parent_root); // Peer sends the right block, it should be sent for processing. Peer should not be penalized. - bl.parent_lookup_response::>( - id, - peer_id, - Some(parent.into()), - D, - &cx, - ); - rig.expect_block_process(response_type); + rig.parent_lookup_block_response(id, peer_id, Some(parent.into())); + rig.expect_block_process(ResponseType::Block); rig.expect_empty_network(); // Processing succeeds, now the rest of the chain should be sent for processing. - bl.parent_block_processed(chain_hash, BlockError::BlockIsAlreadyKnown.into(), &mut cx); + rig.parent_block_processed( + block_root, + BlockError::BlockIsAlreadyKnown(block_root).into(), + ); rig.expect_parent_chain_process(); - let process_result = BatchProcessResult::Success { - was_non_empty: true, - }; - bl.parent_chain_processed(chain_hash, process_result, &cx); - assert_eq!(bl.parent_lookups.len(), 0); + rig.parent_chain_processed_success(block_root); + assert_eq!(rig.active_parent_lookups_count(), 0); } #[test] fn test_parent_lookup_wrong_response() { - let response_type = ResponseType::Block; - let (mut bl, mut cx, mut rig) = TestRig::test_setup(false); + let mut rig = TestRig::test_setup(); - let fork_name = rig - .harness - .spec - .fork_name_at_slot::(rig.harness.chain.slot().unwrap()); - let parent = rig.rand_block(fork_name); - let block = rig.block_with_parent(parent.canonical_root(), fork_name); - let chain_hash = block.canonical_root(); - let peer_id = PeerId::random(); - let block_root = block.canonical_root(); - let parent_root = block.parent_root(); - let slot = block.slot(); + let (parent, block, parent_root, block_root) = rig.rand_block_and_parent(); + let peer_id = rig.new_connected_peer(); // Trigger the request - bl.search_parent(slot, block_root, parent_root, peer_id, &mut cx); - let id1 = rig.expect_parent_request(response_type); - // If we're in deneb, a blob request should have been triggered as well, - // we don't require a response because we're generateing 0-blob blocks in this test. - if matches!(fork_name, ForkName::Deneb) { - let _ = rig.expect_parent_request(ResponseType::Blob); - } + rig.trigger_unknown_parent_block(peer_id, block.into()); + let id1 = rig.expect_parent_request_block_and_blobs(parent_root); // Peer sends the wrong block, peer should be penalized and the block re-requested. - let bad_block = rig.rand_block(fork_name); - bl.parent_lookup_response::>( - id1, - peer_id, - Some(bad_block.into()), - D, - &cx, - ); - rig.expect_penalty(); - let id2 = rig.expect_parent_request(response_type); + let bad_block = rig.rand_block(); + rig.parent_lookup_block_response(id1, peer_id, Some(bad_block.into())); + rig.expect_penalty(peer_id); + let id2 = rig.expect_block_parent_request(parent_root); // Send the stream termination for the first request. This should not produce extra penalties. - bl.parent_lookup_response::>(id1, peer_id, None, D, &cx); + rig.parent_lookup_block_response(id1, peer_id, None); rig.expect_empty_network(); // Send the right block this time. - bl.parent_lookup_response::>( - id2, - peer_id, - Some(parent.into()), - D, - &cx, - ); - rig.expect_block_process(response_type); + rig.parent_lookup_block_response(id2, peer_id, Some(parent.into())); + rig.expect_block_process(ResponseType::Block); // Processing succeeds, now the rest of the chain should be sent for processing. - bl.parent_block_processed( - chain_hash, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(block_root)), - &mut cx, - ); + rig.parent_block_processed_imported(block_root); rig.expect_parent_chain_process(); - let process_result = BatchProcessResult::Success { - was_non_empty: true, - }; - bl.parent_chain_processed(chain_hash, process_result, &cx); - assert_eq!(bl.parent_lookups.len(), 0); + rig.parent_chain_processed_success(block_root); + assert_eq!(rig.active_parent_lookups_count(), 0); } #[test] fn test_parent_lookup_empty_response() { - let response_type = ResponseType::Block; - let (mut bl, mut cx, mut rig) = TestRig::test_setup(false); + let mut rig = TestRig::test_setup(); - let fork_name = rig - .harness - .spec - .fork_name_at_slot::(rig.harness.chain.slot().unwrap()); - let parent = rig.rand_block(fork_name); - let block = rig.block_with_parent(parent.canonical_root(), fork_name); - let chain_hash = block.canonical_root(); - let peer_id = PeerId::random(); - let block_root = block.canonical_root(); - let parent_root = block.parent_root(); - let slot = block.slot(); + let (parent, block, parent_root, block_root) = rig.rand_block_and_parent(); + let peer_id = rig.new_connected_peer(); // Trigger the request - bl.search_parent(slot, block_root, parent_root, peer_id, &mut cx); - let id1 = rig.expect_parent_request(response_type); - // If we're in deneb, a blob request should have been triggered as well, - // we don't require a response because we're generateing 0-blob blocks in this test. - if matches!(fork_name, ForkName::Deneb) { - let _ = rig.expect_parent_request(ResponseType::Blob); - } + rig.trigger_unknown_parent_block(peer_id, block.into()); + let id1 = rig.expect_parent_request_block_and_blobs(parent_root); // Peer sends an empty response, peer should be penalized and the block re-requested. - bl.parent_lookup_response::>(id1, peer_id, None, D, &cx); - rig.expect_penalty(); - let id2 = rig.expect_parent_request(response_type); + rig.parent_lookup_block_response(id1, peer_id, None); + rig.expect_penalty(peer_id); + let id2 = rig.expect_block_parent_request(parent_root); // Send the right block this time. - bl.parent_lookup_response::>( - id2, - peer_id, - Some(parent.into()), - D, - &cx, - ); - rig.expect_block_process(response_type); + rig.parent_lookup_block_response(id2, peer_id, Some(parent.into())); + rig.expect_block_process(ResponseType::Block); // Processing succeeds, now the rest of the chain should be sent for processing. - bl.parent_block_processed( - chain_hash, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(block_root)), - &mut cx, - ); + rig.parent_block_processed_imported(block_root); rig.expect_parent_chain_process(); - let process_result = BatchProcessResult::Success { - was_non_empty: true, - }; - bl.parent_chain_processed(chain_hash, process_result, &cx); - assert_eq!(bl.parent_lookups.len(), 0); + rig.parent_chain_processed_success(block_root); + assert_eq!(rig.active_parent_lookups_count(), 0); } #[test] fn test_parent_lookup_rpc_failure() { - let response_type = ResponseType::Block; - let (mut bl, mut cx, mut rig) = TestRig::test_setup(false); + let mut rig = TestRig::test_setup(); - let fork_name = rig - .harness - .spec - .fork_name_at_slot::(rig.harness.chain.slot().unwrap()); - let parent = rig.rand_block(fork_name); - let block = rig.block_with_parent(parent.canonical_root(), fork_name); - let chain_hash = block.canonical_root(); - let peer_id = PeerId::random(); - let block_root = block.canonical_root(); - let parent_root = block.parent_root(); - let slot = block.slot(); + let (parent, block, parent_root, block_root) = rig.rand_block_and_parent(); + let peer_id = rig.new_connected_peer(); // Trigger the request - bl.search_parent(slot, block_root, parent_root, peer_id, &mut cx); - let id1 = rig.expect_parent_request(response_type); - // If we're in deneb, a blob request should have been triggered as well, - // we don't require a response because we're generateing 0-blob blocks in this test. - if matches!(fork_name, ForkName::Deneb) { - let _ = rig.expect_parent_request(ResponseType::Blob); - } + rig.trigger_unknown_parent_block(peer_id, block.into()); + let id1 = rig.expect_parent_request_block_and_blobs(parent_root); // The request fails. It should be tried again. - bl.parent_lookup_failed::>( - id1, - peer_id, - &cx, - RPCError::ErrorResponse( - RPCResponseErrorCode::ResourceUnavailable, - "older than deneb".into(), - ), - ); - let id2 = rig.expect_parent_request(response_type); + rig.parent_lookup_failed_unavailable(id1, peer_id); + let id2 = rig.expect_block_parent_request(parent_root); // Send the right block this time. - bl.parent_lookup_response::>( - id2, - peer_id, - Some(parent.into()), - D, - &cx, - ); - rig.expect_block_process(response_type); + rig.parent_lookup_block_response(id2, peer_id, Some(parent.into())); + rig.expect_block_process(ResponseType::Block); // Processing succeeds, now the rest of the chain should be sent for processing. - bl.parent_block_processed( - chain_hash, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(block_root)), - &mut cx, - ); + rig.parent_block_processed_imported(block_root); rig.expect_parent_chain_process(); - let process_result = BatchProcessResult::Success { - was_non_empty: true, - }; - bl.parent_chain_processed(chain_hash, process_result, &cx); - assert_eq!(bl.parent_lookups.len(), 0); + rig.parent_chain_processed_success(block_root); + assert_eq!(rig.active_parent_lookups_count(), 0); } #[test] fn test_parent_lookup_too_many_attempts() { - let response_type = ResponseType::Block; - let (mut bl, mut cx, mut rig) = TestRig::test_setup(false); + let mut rig = TestRig::test_setup(); - let fork_name = rig - .harness - .spec - .fork_name_at_slot::(rig.harness.chain.slot().unwrap()); - let parent = rig.rand_block(fork_name); - let block = rig.block_with_parent(parent.canonical_root(), fork_name); - let peer_id = PeerId::random(); - let block_root = block.canonical_root(); + let block = rig.rand_block(); let parent_root = block.parent_root(); - let slot = block.slot(); + let peer_id = rig.new_connected_peer(); // Trigger the request - bl.search_parent(slot, block_root, parent_root, peer_id, &mut cx); + rig.trigger_unknown_parent_block(peer_id, block.into()); for i in 1..=parent_lookup::PARENT_FAIL_TOLERANCE { - let id = rig.expect_parent_request(response_type); - // If we're in deneb, a blob request should have been triggered as well, - // we don't require a response because we're generateing 0-blob blocks in this test. - if matches!(fork_name, ForkName::Deneb) && i == 1 { - let _ = rig.expect_parent_request(ResponseType::Blob); + let id = rig.expect_block_parent_request(parent_root); + // Blobs are only requested in the first iteration as this test only retries blocks + if rig.after_deneb() && i == 1 { + let _ = rig.expect_blob_parent_request(parent_root); } - match i % 2 { - // make sure every error is accounted for - 0 => { - // The request fails. It should be tried again. - bl.parent_lookup_failed::>( - id, - peer_id, - &cx, - RPCError::ErrorResponse( - RPCResponseErrorCode::ResourceUnavailable, - "older than deneb".into(), - ), - ); - } - _ => { - // Send a bad block this time. It should be tried again. - let bad_block = rig.rand_block(fork_name); - bl.parent_lookup_response::>( - id, - peer_id, - Some(bad_block.into()), - D, - &cx, - ); - // Send the stream termination - // Note, previously we would send the same lookup id with a stream terminator, - // we'd ignore it because we'd intrepret it as an unrequested response, since - // we already got one response for the block. I'm not sure what the intent is - // for having this stream terminator line in this test at all. Receiving an invalid - // block and a stream terminator with the same Id now results in two failed attempts, - // I'm unsure if this is how it should behave? - // - bl.parent_lookup_response::>(id, peer_id, None, D, &cx); - rig.expect_penalty(); - } - } - if i < parent_lookup::PARENT_FAIL_TOLERANCE { - assert_eq!( - bl.parent_lookups[0] - .current_parent_request - .block_request_state - .state - .failed_attempts(), - dbg!(i) - ); + if i % 2 == 0 { + // make sure every error is accounted for + // The request fails. It should be tried again. + rig.parent_lookup_failed_unavailable(id, peer_id); + } else { + // Send a bad block this time. It should be tried again. + let bad_block = rig.rand_block(); + rig.parent_lookup_block_response(id, peer_id, Some(bad_block.into())); + // Send the stream termination + + // Note, previously we would send the same lookup id with a stream terminator, + // we'd ignore it because we'd intrepret it as an unrequested response, since + // we already got one response for the block. I'm not sure what the intent is + // for having this stream terminator line in this test at all. Receiving an invalid + // block and a stream terminator with the same Id now results in two failed attempts, + // I'm unsure if this is how it should behave? + // + rig.parent_lookup_block_response(id, peer_id, None); + rig.expect_penalty(peer_id); } } - assert_eq!(bl.parent_lookups.len(), 0); + assert_eq!(rig.active_parent_lookups_count(), 0); } #[test] fn test_parent_lookup_too_many_download_attempts_no_blacklist() { - let response_type = ResponseType::Block; - let (mut bl, mut cx, mut rig) = TestRig::test_setup(false); + let mut rig = TestRig::test_setup(); - let fork_name = rig - .harness - .spec - .fork_name_at_slot::(rig.harness.chain.slot().unwrap()); - let parent = rig.rand_block(fork_name); - let block = rig.block_with_parent(parent.canonical_root(), fork_name); - let block_hash = block.canonical_root(); - let peer_id = PeerId::random(); - let block_root = block.canonical_root(); - let parent_root = block.parent_root(); - let slot = block.slot(); + let (parent, block, parent_root, block_root) = rig.rand_block_and_parent(); + let peer_id = rig.new_connected_peer(); // Trigger the request - bl.search_parent(slot, block_root, parent_root, peer_id, &mut cx); + rig.trigger_unknown_parent_block(peer_id, block.into()); for i in 1..=parent_lookup::PARENT_FAIL_TOLERANCE { - assert!(!bl.failed_chains.contains(&block_hash)); - let id = rig.expect_parent_request(response_type); - // If we're in deneb, a blob request should have been triggered as well, - // we don't require a response because we're generateing 0-blob blocks in this test. - if matches!(fork_name, ForkName::Deneb) && i == 1 { - let _ = rig.expect_parent_request(ResponseType::Blob); + assert!(!rig.failed_chains_contains(&block_root)); + let id = rig.expect_block_parent_request(parent_root); + // Blobs are only requested in the first iteration as this test only retries blocks + if rig.after_deneb() && i == 1 { + let _ = rig.expect_blob_parent_request(parent_root); } if i % 2 != 0 { // The request fails. It should be tried again. - bl.parent_lookup_failed::>( - id, - peer_id, - &cx, - RPCError::ErrorResponse( - RPCResponseErrorCode::ResourceUnavailable, - "older than deneb".into(), - ), - ); + rig.parent_lookup_failed_unavailable(id, peer_id); } else { // Send a bad block this time. It should be tried again. - let bad_block = rig.rand_block(fork_name); - bl.parent_lookup_response::>( - id, - peer_id, - Some(bad_block.into()), - D, - &cx, - ); - rig.expect_penalty(); - } - if i < parent_lookup::PARENT_FAIL_TOLERANCE { - assert_eq!( - bl.parent_lookups[0] - .current_parent_request - .block_request_state - .state - .failed_attempts(), - dbg!(i) - ); + let bad_block = rig.rand_block(); + rig.parent_lookup_block_response(id, peer_id, Some(bad_block.into())); + rig.expect_penalty(peer_id); } } - assert_eq!(bl.parent_lookups.len(), 0); - assert!(!bl.failed_chains.contains(&block_hash)); - assert!(!bl.failed_chains.contains(&parent.canonical_root())); + assert_eq!(rig.active_parent_lookups_count(), 0); + assert!(!rig.failed_chains_contains(&block_root)); + assert!(!rig.failed_chains_contains(&parent.canonical_root())); } #[test] fn test_parent_lookup_too_many_processing_attempts_must_blacklist() { - let response_type = ResponseType::Block; const PROCESSING_FAILURES: u8 = parent_lookup::PARENT_FAIL_TOLERANCE / 2 + 1; - let (mut bl, mut cx, mut rig) = TestRig::test_setup(false); - let fork_name = rig - .harness - .spec - .fork_name_at_slot::(rig.harness.chain.slot().unwrap()); - - let parent = Arc::new(rig.rand_block(fork_name)); - let block = rig.block_with_parent(parent.canonical_root(), fork_name); - let peer_id = PeerId::random(); - let block_root = block.canonical_root(); - let parent_root = block.parent_root(); - let slot = block.slot(); + let mut rig = TestRig::test_setup(); + let (parent, block, parent_root, block_root) = rig.rand_block_and_parent(); + let peer_id = rig.new_connected_peer(); // Trigger the request - bl.search_parent(slot, block_root, parent_root, peer_id, &mut cx); + rig.trigger_unknown_parent_block(peer_id, block.into()); // Fail downloading the block for i in 0..(parent_lookup::PARENT_FAIL_TOLERANCE - PROCESSING_FAILURES) { - let id = rig.expect_parent_request(response_type); - // If we're in deneb, a blob request should have been triggered as well, - // we don't require a response because we're generateing 0-blob blocks in this test. - if matches!(fork_name, ForkName::Deneb) && i == 0 { - let _ = rig.expect_parent_request(ResponseType::Blob); + let id = rig.expect_block_parent_request(parent_root); + // Blobs are only requested in the first iteration as this test only retries blocks + if rig.after_deneb() && i == 0 { + let _ = rig.expect_blob_parent_request(parent_root); } // The request fails. It should be tried again. - bl.parent_lookup_failed::>( - id, - peer_id, - &cx, - RPCError::ErrorResponse( - RPCResponseErrorCode::ResourceUnavailable, - "older than deneb".into(), - ), - ); + rig.parent_lookup_failed_unavailable(id, peer_id); } // Now fail processing a block in the parent request for i in 0..PROCESSING_FAILURES { - let id = dbg!(rig.expect_parent_request(response_type)); - if matches!(fork_name, ForkName::Deneb) && i != 0 { - let _ = rig.expect_parent_request(ResponseType::Blob); + let id = rig.expect_block_parent_request(parent_root); + // Blobs are only requested in the first iteration as this test only retries blocks + if rig.after_deneb() && i != 0 { + let _ = rig.expect_blob_parent_request(parent_root); } - // If we're in deneb, a blob request should have been triggered as well, - // we don't require a response because we're generateing 0-blob blocks in this test. - assert!(!bl.failed_chains.contains(&block_root)); + assert!(!rig.failed_chains_contains(&block_root)); // send the right parent but fail processing - bl.parent_lookup_response::>( - id, - peer_id, - Some(parent.clone()), - D, - &cx, - ); - bl.parent_block_processed(block_root, BlockError::InvalidSignature.into(), &mut cx); - bl.parent_lookup_response::>(id, peer_id, None, D, &cx); - rig.expect_penalty(); + rig.parent_lookup_block_response(id, peer_id, Some(parent.clone().into())); + rig.parent_block_processed(block_root, BlockError::InvalidSignature.into()); + rig.parent_lookup_block_response(id, peer_id, None); + rig.expect_penalty(peer_id); } - assert!(bl.failed_chains.contains(&block_root)); - assert_eq!(bl.parent_lookups.len(), 0); + assert!(rig.failed_chains_contains(&block_root)); + assert_eq!(rig.active_parent_lookups_count(), 0); } #[test] fn test_parent_lookup_too_deep() { - let response_type = ResponseType::Block; - let (mut bl, mut cx, mut rig) = TestRig::test_setup(false); - let fork_name = rig - .harness - .spec - .fork_name_at_slot::(rig.harness.chain.slot().unwrap()); - let mut blocks = - Vec::>>::with_capacity(parent_lookup::PARENT_DEPTH_TOLERANCE); - while blocks.len() < parent_lookup::PARENT_DEPTH_TOLERANCE { - let parent = blocks - .last() - .map(|b| b.canonical_root()) - .unwrap_or_else(Hash256::random); - let block = Arc::new(rig.block_with_parent(parent, fork_name)); - blocks.push(block); - } + let mut rig = TestRig::test_setup(); + let mut blocks = rig.rand_blockchain(parent_lookup::PARENT_DEPTH_TOLERANCE); - let peer_id = PeerId::random(); + let peer_id = rig.new_connected_peer(); let trigger_block = blocks.pop().unwrap(); let chain_hash = trigger_block.canonical_root(); - let trigger_block_root = trigger_block.canonical_root(); - let trigger_parent_root = trigger_block.parent_root(); - let trigger_slot = trigger_block.slot(); - bl.search_parent( - trigger_slot, - trigger_block_root, - trigger_parent_root, - peer_id, - &mut cx, - ); + rig.trigger_unknown_parent_block(peer_id, trigger_block); for block in blocks.into_iter().rev() { - let id = rig.expect_parent_request(response_type); - // If we're in deneb, a blob request should have been triggered as well, - // we don't require a response because we're generateing 0-blob blocks in this test. - if matches!(fork_name, ForkName::Deneb) { - let _ = rig.expect_parent_request(ResponseType::Blob); - } + let id = rig.expect_parent_request_block_and_blobs(block.canonical_root()); // the block - bl.parent_lookup_response::>( - id, - peer_id, - Some(block.clone()), - D, - &cx, - ); + rig.parent_lookup_block_response(id, peer_id, Some(block.clone())); // the stream termination - bl.parent_lookup_response::>(id, peer_id, None, D, &cx); + rig.parent_lookup_block_response(id, peer_id, None); // the processing request - rig.expect_block_process(response_type); + rig.expect_block_process(ResponseType::Block); // the processing result - bl.parent_block_processed( + rig.parent_block_processed( chain_hash, BlockError::ParentUnknown(RpcBlock::new_without_blobs(None, block)).into(), - &mut cx, ) } - rig.expect_penalty(); - assert!(bl.failed_chains.contains(&chain_hash)); + rig.expect_penalty(peer_id); + assert!(rig.failed_chains_contains(&chain_hash)); } #[test] fn test_parent_lookup_disconnection() { - let (mut bl, mut cx, mut rig) = TestRig::test_setup(false); - let peer_id = PeerId::random(); - let fork_name = rig - .harness - .spec - .fork_name_at_slot::(rig.harness.chain.slot().unwrap()); - let trigger_block = rig.rand_block(fork_name); - let trigger_block_root = trigger_block.canonical_root(); - let trigger_parent_root = trigger_block.parent_root(); - let trigger_slot = trigger_block.slot(); - bl.search_parent( - trigger_slot, - trigger_block_root, - trigger_parent_root, - peer_id, - &mut cx, - ); + let mut rig = TestRig::test_setup(); + let peer_id = rig.new_connected_peer(); + let trigger_block = rig.rand_block(); + rig.trigger_unknown_parent_block(peer_id, trigger_block.into()); - bl.peer_disconnected(&peer_id, &mut cx); - assert!(bl.parent_lookups.is_empty()); + rig.peer_disconnected(peer_id); + assert_eq!(rig.active_parent_lookups_count(), 0); } #[test] fn test_single_block_lookup_ignored_response() { - let response_type = ResponseType::Block; - let (mut bl, mut cx, mut rig) = TestRig::test_setup(false); + let mut rig = TestRig::test_setup(); - let fork_name = rig - .harness - .spec - .fork_name_at_slot::(rig.harness.chain.slot().unwrap()); - let block = rig.rand_block(fork_name); - let peer_id = PeerId::random(); + let block = rig.rand_block(); + let peer_id = rig.new_connected_peer(); // Trigger the request - bl.search_block(block.canonical_root(), &[peer_id], &mut cx); - let id = rig.expect_lookup_request(response_type); - // If we're in deneb, a blob request should have been triggered as well, - // we don't require a response because we're generateing 0-blob blocks in this test. - if matches!(fork_name, ForkName::Deneb) { - let _ = rig.expect_lookup_request(ResponseType::Blob); - } + rig.trigger_unknown_block_from_attestation(block.canonical_root(), peer_id); + let id = rig.expect_lookup_request_block_and_blobs(block.canonical_root()); // The peer provides the correct block, should not be penalized. Now the block should be sent // for processing. - bl.single_lookup_response::>( - id, - peer_id, - Some(block.into()), - D, - &cx, - ); + rig.single_lookup_block_response(id, peer_id, Some(block.into())); rig.expect_empty_network(); - rig.expect_block_process(response_type); + rig.expect_block_process(ResponseType::Block); // The request should still be active. - assert_eq!(bl.single_block_lookups.len(), 1); + assert_eq!(rig.active_single_lookups_count(), 1); // Send the stream termination. Peer should have not been penalized, and the request removed // after processing. - bl.single_lookup_response::>(id, peer_id, None, D, &cx); + rig.single_lookup_block_response(id, peer_id, None); // Send an Ignored response, the request should be dropped - bl.single_block_component_processed::>( - id.id, - BlockProcessingResult::Ignored, - &mut cx, - ); + rig.single_block_component_processed(id, BlockProcessingResult::Ignored); rig.expect_empty_network(); - assert_eq!(bl.single_block_lookups.len(), 0); + assert_eq!(rig.active_single_lookups_count(), 0); } #[test] fn test_parent_lookup_ignored_response() { - let response_type = ResponseType::Block; - let (mut bl, mut cx, mut rig) = TestRig::test_setup(false); + let mut rig = TestRig::test_setup(); - let fork_name = rig - .harness - .spec - .fork_name_at_slot::(rig.harness.chain.slot().unwrap()); - let parent = rig.rand_block(fork_name); - let block = rig.block_with_parent(parent.canonical_root(), fork_name); - let chain_hash = block.canonical_root(); - let peer_id = PeerId::random(); - let block_root = block.canonical_root(); - let parent_root = block.parent_root(); - let slot = block.slot(); + let (parent, block, parent_root, block_root) = rig.rand_block_and_parent(); + let peer_id = rig.new_connected_peer(); // Trigger the request - bl.search_parent(slot, block_root, parent_root, peer_id, &mut cx); - let id = rig.expect_parent_request(response_type); - - // If we're in deneb, a blob request should have been triggered as well, - // we don't require a response because we're generateing 0-blob blocks in this test. - if matches!(fork_name, ForkName::Deneb) { - let _ = rig.expect_parent_request(ResponseType::Blob); - } + rig.trigger_unknown_parent_block(peer_id, block.into()); + let id = rig.expect_parent_request_block_and_blobs(parent_root); // Peer sends the right block, it should be sent for processing. Peer should not be penalized. - bl.parent_lookup_response::>( - id, - peer_id, - Some(parent.into()), - D, - &cx, - ); - rig.expect_block_process(response_type); + rig.parent_lookup_block_response(id, peer_id, Some(parent.into())); + rig.expect_block_process(ResponseType::Block); rig.expect_empty_network(); // Return an Ignored result. The request should be dropped - bl.parent_block_processed(chain_hash, BlockProcessingResult::Ignored, &mut cx); + rig.parent_block_processed(block_root, BlockProcessingResult::Ignored); rig.expect_empty_network(); - assert_eq!(bl.parent_lookups.len(), 0); + assert_eq!(rig.active_parent_lookups_count(), 0); } /// This is a regression test. #[test] fn test_same_chain_race_condition() { - let response_type = ResponseType::Block; - let (mut bl, mut cx, mut rig) = TestRig::test_setup(true); + let mut rig = TestRig::test_setup(); - let fork_name = rig - .harness - .spec - .fork_name_at_slot::(rig.harness.chain.slot().unwrap()); - #[track_caller] - fn parent_lookups_consistency(bl: &BlockLookups) { - let hashes: Vec<_> = bl - .parent_lookups - .iter() - .map(|req| req.chain_hash()) - .collect(); - let expected = hashes.len(); - assert_eq!( - expected, - hashes - .into_iter() - .collect::>() - .len(), - "duplicated chain hashes in parent queue" - ) - } // if we use one or two blocks it will match on the hash or the parent hash, so make a longer // chain. let depth = 4; - let mut blocks = Vec::>>::with_capacity(depth); - while blocks.len() < depth { - let parent = blocks - .last() - .map(|b| b.canonical_root()) - .unwrap_or_else(Hash256::random); - let block = Arc::new(rig.block_with_parent(parent, fork_name)); - blocks.push(block); - } - - let peer_id = PeerId::random(); + let mut blocks = rig.rand_blockchain(depth); + let peer_id = rig.new_connected_peer(); let trigger_block = blocks.pop().unwrap(); let chain_hash = trigger_block.canonical_root(); - let trigger_block_root = trigger_block.canonical_root(); - let trigger_parent_root = trigger_block.parent_root(); - let trigger_slot = trigger_block.slot(); - bl.search_parent( - trigger_slot, - trigger_block_root, - trigger_parent_root, - peer_id, - &mut cx, - ); + rig.trigger_unknown_parent_block(peer_id, trigger_block.clone()); for (i, block) in blocks.into_iter().rev().enumerate() { - let id = rig.expect_parent_request(response_type); - // If we're in deneb, a blob request should have been triggered as well, - // we don't require a response because we're generateing 0-blob blocks in this test. - if matches!(fork_name, ForkName::Deneb) { - let _ = rig.expect_parent_request(ResponseType::Blob); - } + let id = rig.expect_parent_request_block_and_blobs(block.canonical_root()); // the block - bl.parent_lookup_response::>( - id, - peer_id, - Some(block.clone()), - D, - &cx, - ); + rig.parent_lookup_block_response(id, peer_id, Some(block.clone())); // the stream termination - bl.parent_lookup_response::>(id, peer_id, None, D, &cx); + rig.parent_lookup_block_response(id, peer_id, None); // the processing request - rig.expect_block_process(response_type); + rig.expect_block_process(ResponseType::Block); // the processing result if i + 2 == depth { // one block was removed - bl.parent_block_processed(chain_hash, BlockError::BlockIsAlreadyKnown.into(), &mut cx) + rig.parent_block_processed( + chain_hash, + BlockError::BlockIsAlreadyKnown(block.canonical_root()).into(), + ) } else { - bl.parent_block_processed( + rig.parent_block_processed( chain_hash, BlockError::ParentUnknown(RpcBlock::new_without_blobs(None, block)).into(), - &mut cx, ) } - parent_lookups_consistency(&bl) + rig.assert_parent_lookups_consistency(); } // Processing succeeds, now the rest of the chain should be sent for processing. rig.expect_parent_chain_process(); // Try to get this block again while the chain is being processed. We should not request it again. - let peer_id = PeerId::random(); - let trigger_block_root = trigger_block.canonical_root(); - let trigger_parent_root = trigger_block.parent_root(); - let trigger_slot = trigger_block.slot(); - bl.search_parent( - trigger_slot, - trigger_block_root, - trigger_parent_root, - peer_id, - &mut cx, - ); - parent_lookups_consistency(&bl); + let peer_id = rig.new_connected_peer(); + rig.trigger_unknown_parent_block(peer_id, trigger_block); + rig.assert_parent_lookups_consistency(); - let process_result = BatchProcessResult::Success { - was_non_empty: true, - }; - bl.parent_chain_processed(chain_hash, process_result, &cx); - assert_eq!(bl.parent_lookups.len(), 0); + rig.parent_chain_processed_success(chain_hash); + assert_eq!(rig.active_parent_lookups_count(), 0); } mod deneb_only { use super::*; - use crate::sync::block_lookups::common::ResponseType; use beacon_chain::data_availability_checker::AvailabilityCheckError; - use beacon_chain::test_utils::NumBlobs; use ssz_types::VariableList; - use std::ops::IndexMut; - use std::str::FromStr; struct DenebTester { - bl: BlockLookups, - cx: SyncNetworkContext, rig: TestRig, block: Arc>, blobs: Vec>>, @@ -1198,15 +1125,10 @@ mod deneb_only { impl DenebTester { fn new(request_trigger: RequestTrigger) -> Option { - let fork_name = get_fork_name(); - if !matches!(fork_name, ForkName::Deneb) { + let Some(mut rig) = TestRig::test_setup_after_deneb() else { return None; - } - let (mut bl, mut cx, mut rig) = TestRig::test_setup(false); - rig.harness.chain.slot_clock.set_slot( - E::slots_per_epoch() * rig.harness.spec.deneb_fork_epoch.unwrap().as_u64(), - ); - let (block, blobs) = rig.rand_block_and_blobs(fork_name, NumBlobs::Random); + }; + let (block, blobs) = rig.rand_block_and_blobs(NumBlobs::Random); let mut block = Arc::new(block); let mut blobs = blobs.into_iter().map(Arc::new).collect::>(); let slot = block.slot(); @@ -1224,7 +1146,7 @@ mod deneb_only { // Create the next block. let (child_block, child_blobs) = - rig.block_with_parent_and_blobs(parent_root, get_fork_name(), NumBlobs::Random); + rig.block_with_parent_and_blobs(parent_root, NumBlobs::Random); let mut child_block = Arc::new(child_block); let mut child_blobs = child_blobs.into_iter().map(Arc::new).collect::>(); @@ -1233,32 +1155,32 @@ mod deneb_only { std::mem::swap(&mut child_blobs, &mut blobs); } let block_root = block.canonical_root(); - let parent_root = block.parent_root(); - let peer_id = PeerId::random(); + let peer_id = rig.new_connected_peer(); // Trigger the request let (block_req_id, blob_req_id, parent_block_req_id, parent_blob_req_id) = match request_trigger { RequestTrigger::AttestationUnknownBlock => { - bl.search_block(block_root, &[peer_id], &mut cx); - let block_req_id = rig.expect_lookup_request(ResponseType::Block); - let blob_req_id = rig.expect_lookup_request(ResponseType::Blob); + rig.send_sync_message(SyncMessage::UnknownBlockHashFromAttestation( + peer_id, block_root, + )); + let block_req_id = rig.expect_block_lookup_request(block_root); + let blob_req_id = rig.expect_blob_lookup_request(block_root); (Some(block_req_id), Some(blob_req_id), None, None) } RequestTrigger::GossipUnknownParentBlock { .. } => { - bl.search_child_block( + rig.send_sync_message(SyncMessage::UnknownParentBlock( + peer_id, + RpcBlock::new_without_blobs(Some(block_root), block.clone()), block_root, - ChildComponents::new(block_root, Some(block.clone()), None), - &[peer_id], - &mut cx, - ); + )); - let blob_req_id = rig.expect_lookup_request(ResponseType::Blob); - rig.expect_empty_network(); // expect no block request - bl.search_parent(slot, block_root, parent_root, peer_id, &mut cx); - let parent_block_req_id = rig.expect_parent_request(ResponseType::Block); - let parent_blob_req_id = rig.expect_parent_request(ResponseType::Blob); + let parent_root = block.parent_root(); + let blob_req_id = rig.expect_blob_lookup_request(block_root); + let parent_block_req_id = rig.expect_block_parent_request(parent_root); + let parent_blob_req_id = rig.expect_blob_parent_request(parent_root); + rig.expect_empty_network(); // expect no more requests ( None, Some(blob_req_id), @@ -1268,23 +1190,14 @@ mod deneb_only { } RequestTrigger::GossipUnknownParentBlob { .. } => { let single_blob = blobs.first().cloned().unwrap(); - let child_root = single_blob.block_root(); + let parent_root = single_blob.block_parent_root(); + rig.send_sync_message(SyncMessage::UnknownParentBlob(peer_id, single_blob)); - let mut lookup_blobs = FixedBlobSidecarList::default(); - *lookup_blobs.index_mut(0) = Some(single_blob); - bl.search_child_block( - child_root, - ChildComponents::new(child_root, None, Some(lookup_blobs)), - &[peer_id], - &mut cx, - ); - - let block_req_id = rig.expect_lookup_request(ResponseType::Block); - let blobs_req_id = rig.expect_lookup_request(ResponseType::Blob); - rig.expect_empty_network(); // expect no block request - bl.search_parent(slot, child_root, parent_root, peer_id, &mut cx); - let parent_block_req_id = rig.expect_parent_request(ResponseType::Block); - let parent_blob_req_id = rig.expect_parent_request(ResponseType::Blob); + let block_req_id = rig.expect_block_lookup_request(block_root); + let blobs_req_id = rig.expect_blob_lookup_request(block_root); + let parent_block_req_id = rig.expect_block_parent_request(parent_root); + let parent_blob_req_id = rig.expect_blob_parent_request(parent_root); + rig.expect_empty_network(); // expect no more requests ( Some(block_req_id), Some(blobs_req_id), @@ -1295,8 +1208,6 @@ mod deneb_only { }; Some(Self { - bl, - cx, rig, block, blobs, @@ -1318,15 +1229,13 @@ mod deneb_only { self.rig.expect_empty_network(); let block = self.parent_block.pop_front().unwrap().clone(); let _ = self.unknown_parent_block.insert(block.clone()); - self.bl.parent_lookup_response::>( + self.rig.parent_lookup_block_response( self.parent_block_req_id.expect("parent request id"), self.peer_id, Some(block), - D, - &self.cx, ); - assert_eq!(self.bl.parent_lookups.len(), 1); + assert_eq!(self.rig.active_parent_lookups_count(), 1); self } @@ -1334,24 +1243,18 @@ mod deneb_only { let blobs = self.parent_blobs.pop_front().unwrap(); let _ = self.unknown_parent_blobs.insert(blobs.clone()); for blob in &blobs { - self.bl - .parent_lookup_response::>( - self.parent_blob_req_id.expect("parent blob request id"), - self.peer_id, - Some(blob.clone()), - D, - &self.cx, - ); - assert_eq!(self.bl.parent_lookups.len(), 1); - } - self.bl - .parent_lookup_response::>( - self.parent_blob_req_id.expect("blob request id"), + self.rig.parent_lookup_blob_response( + self.parent_blob_req_id.expect("parent blob request id"), self.peer_id, - None, - D, - &self.cx, + Some(blob.clone()), ); + assert_eq!(self.rig.active_parent_lookups_count(), 1); + } + self.rig.parent_lookup_blob_response( + self.parent_blob_req_id.expect("blob request id"), + self.peer_id, + None, + ); self } @@ -1361,48 +1264,39 @@ mod deneb_only { me.rig.expect_block_process(ResponseType::Block); // The request should still be active. - assert_eq!(me.bl.single_block_lookups.len(), 1); + assert_eq!(me.rig.active_single_lookups_count(), 1); me } fn block_response(mut self) -> Self { // The peer provides the correct block, should not be penalized. Now the block should be sent // for processing. - self.bl - .single_lookup_response::>( - self.block_req_id.expect("block request id"), - self.peer_id, - Some(self.block.clone()), - D, - &self.cx, - ); + self.rig.single_lookup_block_response( + self.block_req_id.expect("block request id"), + self.peer_id, + Some(self.block.clone()), + ); self.rig.expect_empty_network(); // The request should still be active. - assert_eq!(self.bl.single_block_lookups.len(), 1); + assert_eq!(self.rig.active_single_lookups_count(), 1); self } fn blobs_response(mut self) -> Self { for blob in &self.blobs { - self.bl - .single_lookup_response::>( - self.blob_req_id.expect("blob request id"), - self.peer_id, - Some(blob.clone()), - D, - &self.cx, - ); - assert_eq!(self.bl.single_block_lookups.len(), 1); - } - self.bl - .single_lookup_response::>( + self.rig.single_lookup_blob_response( self.blob_req_id.expect("blob request id"), self.peer_id, - None, - D, - &self.cx, + Some(blob.clone()), ); + assert_eq!(self.rig.active_single_lookups_count(), 1); + } + self.rig.single_lookup_blob_response( + self.blob_req_id.expect("blob request id"), + self.peer_id, + None, + ); self } @@ -1420,183 +1314,171 @@ mod deneb_only { } fn empty_block_response(mut self) -> Self { - self.bl - .single_lookup_response::>( - self.block_req_id.expect("block request id"), - self.peer_id, - None, - D, - &self.cx, - ); + self.rig.single_lookup_block_response( + self.block_req_id.expect("block request id"), + self.peer_id, + None, + ); self } fn empty_blobs_response(mut self) -> Self { - self.bl - .single_lookup_response::>( - self.blob_req_id.expect("blob request id"), - self.peer_id, - None, - D, - &self.cx, - ); + self.rig.single_lookup_blob_response( + self.blob_req_id.expect("blob request id"), + self.peer_id, + None, + ); self } fn empty_parent_block_response(mut self) -> Self { - self.bl.parent_lookup_response::>( + self.rig.parent_lookup_block_response( self.parent_block_req_id.expect("block request id"), self.peer_id, None, - D, - &self.cx, ); self } fn empty_parent_blobs_response(mut self) -> Self { - self.bl - .parent_lookup_response::>( - self.parent_blob_req_id.expect("blob request id"), - self.peer_id, - None, - D, - &self.cx, - ); + self.rig.parent_lookup_blob_response( + self.parent_blob_req_id.expect("blob request id"), + self.peer_id, + None, + ); self } fn block_imported(mut self) -> Self { // Missing blobs should be the request is not removed, the outstanding blobs request should // mean we do not send a new request. - self.bl - .single_block_component_processed::>( - self.block_req_id.expect("block request id").id, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported( - self.block_root, - )), - &mut self.cx, - ); + self.rig.single_block_component_processed( + self.block_req_id.expect("block request id"), + BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(self.block_root)), + ); self.rig.expect_empty_network(); - assert_eq!(self.bl.single_block_lookups.len(), 0); + assert_eq!(self.rig.active_single_lookups_count(), 0); self } fn parent_block_imported(mut self) -> Self { - self.bl.parent_block_processed( + self.rig.parent_block_processed( self.block_root, BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(self.block_root)), - &mut self.cx, ); self.rig.expect_empty_network(); - assert_eq!(self.bl.parent_lookups.len(), 0); + assert_eq!(self.rig.active_parent_lookups_count(), 0); self } fn parent_block_unknown_parent(mut self) -> Self { let block = self.unknown_parent_block.take().unwrap(); + // Now this block is the one we expect requests from + self.block = block.clone(); let block = RpcBlock::new( Some(block.canonical_root()), block, self.unknown_parent_blobs.take().map(VariableList::from), ) .unwrap(); - self.bl.parent_block_processed( + self.rig.parent_block_processed( self.block_root, BlockProcessingResult::Err(BlockError::ParentUnknown(block)), - &mut self.cx, ); - assert_eq!(self.bl.parent_lookups.len(), 1); + assert_eq!(self.rig.active_parent_lookups_count(), 1); self } fn invalid_parent_processed(mut self) -> Self { - self.bl.parent_block_processed( + self.rig.parent_block_processed( self.block_root, BlockProcessingResult::Err(BlockError::ProposalSignatureInvalid), - &mut self.cx, ); - assert_eq!(self.bl.parent_lookups.len(), 1); + assert_eq!(self.rig.active_parent_lookups_count(), 1); self } fn invalid_block_processed(mut self) -> Self { - self.bl - .single_block_component_processed::>( - self.block_req_id.expect("block request id").id, - BlockProcessingResult::Err(BlockError::ProposalSignatureInvalid), - &mut self.cx, - ); - assert_eq!(self.bl.single_block_lookups.len(), 1); + self.rig.single_block_component_processed( + self.block_req_id.expect("block request id"), + BlockProcessingResult::Err(BlockError::ProposalSignatureInvalid), + ); + assert_eq!(self.rig.active_single_lookups_count(), 1); self } fn invalid_blob_processed(mut self) -> Self { - self.bl - .single_block_component_processed::>( - self.blob_req_id.expect("blob request id").id, - BlockProcessingResult::Err(BlockError::AvailabilityCheck( - AvailabilityCheckError::KzgVerificationFailed, - )), - &mut self.cx, - ); - assert_eq!(self.bl.single_block_lookups.len(), 1); + self.rig.single_block_component_processed( + self.blob_req_id.expect("blob request id"), + BlockProcessingResult::Err(BlockError::AvailabilityCheck( + AvailabilityCheckError::KzgVerificationFailed, + )), + ); + assert_eq!(self.rig.active_single_lookups_count(), 1); self } fn missing_components_from_block_request(mut self) -> Self { - self.bl - .single_block_component_processed::>( - self.block_req_id.expect("block request id").id, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents( - self.slot, - self.block_root, - )), - &mut self.cx, - ); - assert_eq!(self.bl.single_block_lookups.len(), 1); + self.rig.single_block_component_processed( + self.block_req_id.expect("block request id"), + BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents( + self.slot, + self.block_root, + )), + ); + assert_eq!(self.rig.active_single_lookups_count(), 1); self } fn missing_components_from_blob_request(mut self) -> Self { - self.bl - .single_block_component_processed::>( - self.blob_req_id.expect("blob request id").id, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents( - self.slot, - self.block_root, - )), - &mut self.cx, - ); - assert_eq!(self.bl.single_block_lookups.len(), 1); + self.rig.single_blob_component_processed( + self.blob_req_id.expect("blob request id"), + BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents( + self.slot, + self.block_root, + )), + ); + assert_eq!(self.rig.active_single_lookups_count(), 1); self } fn expect_penalty(mut self) -> Self { - self.rig.expect_penalty(); + self.rig.expect_penalty(self.peer_id); self } fn expect_no_penalty(mut self) -> Self { self.rig.expect_empty_network(); self } + fn expect_no_penalty_and_no_requests(mut self) -> Self { + self.rig.expect_empty_network(); + self + } fn expect_block_request(mut self) -> Self { - let id = self.rig.expect_lookup_request(ResponseType::Block); + let id = self + .rig + .expect_block_lookup_request(self.block.canonical_root()); self.block_req_id = Some(id); self } fn expect_blobs_request(mut self) -> Self { - let id = self.rig.expect_lookup_request(ResponseType::Blob); + let id = self + .rig + .expect_blob_lookup_request(self.block.canonical_root()); self.blob_req_id = Some(id); self } fn expect_parent_block_request(mut self) -> Self { - let id = self.rig.expect_parent_request(ResponseType::Block); + let id = self + .rig + .expect_block_parent_request(self.block.parent_root()); self.parent_block_req_id = Some(id); self } fn expect_parent_blobs_request(mut self) -> Self { - let id = self.rig.expect_parent_request(ResponseType::Blob); + let id = self + .rig + .expect_blob_parent_request(self.block.parent_root()); self.parent_blob_req_id = Some(id); self } @@ -1625,19 +1507,11 @@ mod deneb_only { self.rig.expect_block_process(ResponseType::Block); self } - } - - fn get_fork_name() -> ForkName { - ForkName::from_str( - &std::env::var(beacon_chain::test_utils::FORK_NAME_ENV_VAR).unwrap_or_else(|e| { - panic!( - "{} env var must be defined when using fork_from_env: {:?}", - beacon_chain::test_utils::FORK_NAME_ENV_VAR, - e - ) - }), - ) - .unwrap() + fn search_parent_dup(mut self) -> Self { + self.rig + .trigger_unknown_parent_block(self.peer_id, self.block.clone()); + self + } } #[test] @@ -1711,9 +1585,7 @@ mod deneb_only { tester .blobs_response() .blobs_response_was_valid() - .expect_no_penalty() - .expect_no_block_request() - .expect_no_blobs_request() + .expect_no_penalty_and_no_requests() .missing_components_from_blob_request() .empty_block_response() .expect_penalty() @@ -1735,9 +1607,7 @@ mod deneb_only { .expect_no_blobs_request() .blobs_response() .missing_components_from_blob_request() - .expect_no_penalty() - .expect_no_block_request() - .expect_no_block_request(); + .expect_no_penalty_and_no_requests(); } #[test] @@ -1787,6 +1657,7 @@ mod deneb_only { .expect_blobs_request() .expect_no_block_request(); } + #[test] fn too_few_blobs_response_then_block_response_attestation() { let Some(tester) = DenebTester::new(RequestTrigger::AttestationUnknownBlock) else { @@ -1797,9 +1668,7 @@ mod deneb_only { .invalidate_blobs_too_few() .blobs_response() .blobs_response_was_valid() - .expect_no_penalty() - .expect_no_blobs_request() - .expect_no_block_request() + .expect_no_penalty_and_no_requests() .block_response_triggering_process(); } @@ -1886,9 +1755,7 @@ mod deneb_only { tester .blobs_response() - .expect_no_penalty() - .expect_no_block_request() - .expect_no_blobs_request() + .expect_no_penalty_and_no_requests() .parent_block_response() .parent_blob_response() .expect_block_process() @@ -1929,9 +1796,7 @@ mod deneb_only { tester .blobs_response() .empty_parent_blobs_response() - .expect_no_penalty() - .expect_no_blobs_request() - .expect_no_block_request() + .expect_no_penalty_and_no_requests() .parent_block_response() .expect_penalty() .expect_parent_blobs_request() @@ -2009,9 +1874,7 @@ mod deneb_only { tester .block_response() - .expect_no_penalty() - .expect_no_block_request() - .expect_no_blobs_request() + .expect_no_penalty_and_no_requests() .parent_block_response() .parent_blob_response() .expect_block_process() @@ -2052,9 +1915,7 @@ mod deneb_only { tester .block_response() .empty_parent_blobs_response() - .expect_no_penalty() - .expect_no_blobs_request() - .expect_no_block_request() + .expect_no_penalty_and_no_requests() .parent_block_response() .expect_penalty() .expect_parent_blobs_request() @@ -2088,4 +1949,57 @@ mod deneb_only { .expect_no_penalty() .expect_block_process(); } + + #[test] + fn unknown_parent_block_dup() { + let Some(tester) = + DenebTester::new(RequestTrigger::GossipUnknownParentBlock { num_parents: 1 }) + else { + return; + }; + + tester + .search_parent_dup() + .expect_no_blobs_request() + .expect_no_block_request(); + } + + #[test] + fn unknown_parent_blob_dup() { + let Some(tester) = + DenebTester::new(RequestTrigger::GossipUnknownParentBlob { num_parents: 1 }) + else { + return; + }; + + tester + .search_parent_dup() + .expect_no_blobs_request() + .expect_no_block_request(); + } + + #[test] + fn no_peer_penalty_when_rpc_response_already_known_from_gossip() { + let Some(mut r) = TestRig::test_setup_after_deneb() else { + return; + }; + let (block, blobs) = r.rand_block_and_blobs(NumBlobs::Number(2)); + let block_root = block.canonical_root(); + let blob_0 = blobs[0].clone(); + let blob_1 = blobs[1].clone(); + let peer_a = r.new_connected_peer(); + let peer_b = r.new_connected_peer(); + // Send unknown parent block lookup + r.trigger_unknown_parent_block(peer_a, block.into()); + // Expect network request for blobs + let id = r.expect_blob_lookup_request(block_root); + // Peer responses with blob 0 + r.single_lookup_blob_response(id, peer_a, Some(blob_0.into())); + // Blob 1 is received via gossip unknown parent blob from a different peer + r.trigger_unknown_parent_blob(peer_b, blob_1.clone()); + // Original peer sends blob 1 via RPC + r.single_lookup_blob_response(id, peer_a, Some(blob_1.into())); + // Assert no downscore event for original peer + r.expect_no_penalty_for(peer_a); + } } diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index f9ed45fcd8..6a3b568c1c 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -3,34 +3,52 @@ use ssz_types::VariableList; use std::{collections::VecDeque, sync::Arc}; use types::{BlobSidecar, EthSpec, SignedBeaconBlock}; -#[derive(Debug, Default)] -pub struct BlocksAndBlobsRequestInfo { +use super::range_sync::ByRangeRequestType; + +#[derive(Debug)] +pub struct BlocksAndBlobsRequestInfo { /// Blocks we have received awaiting for their corresponding sidecar. - accumulated_blocks: VecDeque>>, + accumulated_blocks: VecDeque>>, /// Sidecars we have received awaiting for their corresponding block. - accumulated_sidecars: VecDeque>>, + accumulated_sidecars: VecDeque>>, /// Whether the individual RPC request for blocks is finished or not. is_blocks_stream_terminated: bool, /// Whether the individual RPC request for sidecars is finished or not. is_sidecars_stream_terminated: bool, + /// Used to determine if this accumulator should wait for a sidecars stream termination + request_type: ByRangeRequestType, } -impl BlocksAndBlobsRequestInfo { - pub fn add_block_response(&mut self, block_opt: Option>>) { +impl BlocksAndBlobsRequestInfo { + pub fn new(request_type: ByRangeRequestType) -> Self { + Self { + accumulated_blocks: <_>::default(), + accumulated_sidecars: <_>::default(), + is_blocks_stream_terminated: <_>::default(), + is_sidecars_stream_terminated: <_>::default(), + request_type, + } + } + + pub fn get_request_type(&self) -> ByRangeRequestType { + self.request_type + } + + pub fn add_block_response(&mut self, block_opt: Option>>) { match block_opt { Some(block) => self.accumulated_blocks.push_back(block), None => self.is_blocks_stream_terminated = true, } } - pub fn add_sidecar_response(&mut self, sidecar_opt: Option>>) { + pub fn add_sidecar_response(&mut self, sidecar_opt: Option>>) { match sidecar_opt { Some(sidecar) => self.accumulated_sidecars.push_back(sidecar), None => self.is_sidecars_stream_terminated = true, } } - pub fn into_responses(self) -> Result>, String> { + pub fn into_responses(self) -> Result>, String> { let BlocksAndBlobsRequestInfo { accumulated_blocks, accumulated_sidecars, @@ -42,7 +60,7 @@ impl BlocksAndBlobsRequestInfo { let mut responses = Vec::with_capacity(accumulated_blocks.len()); let mut blob_iter = accumulated_sidecars.into_iter().peekable(); for block in accumulated_blocks.into_iter() { - let mut blob_list = Vec::with_capacity(T::max_blobs_per_block()); + let mut blob_list = Vec::with_capacity(E::max_blobs_per_block()); while { let pair_next_blob = blob_iter .peek() @@ -53,7 +71,7 @@ impl BlocksAndBlobsRequestInfo { blob_list.push(blob_iter.next().ok_or("Missing next blob".to_string())?); } - let mut blobs_buffer = vec![None; T::max_blobs_per_block()]; + let mut blobs_buffer = vec![None; E::max_blobs_per_block()]; for blob in blob_list { let blob_index = blob.index as usize; let Some(blob_opt) = blobs_buffer.get_mut(blob_index) else { @@ -78,6 +96,38 @@ impl BlocksAndBlobsRequestInfo { } pub fn is_finished(&self) -> bool { - self.is_blocks_stream_terminated && self.is_sidecars_stream_terminated + let blobs_requested = match self.request_type { + ByRangeRequestType::Blocks => false, + ByRangeRequestType::BlocksAndBlobs => true, + }; + self.is_blocks_stream_terminated && (!blobs_requested || self.is_sidecars_stream_terminated) + } +} + +#[cfg(test)] +mod tests { + use super::BlocksAndBlobsRequestInfo; + use crate::sync::range_sync::ByRangeRequestType; + use beacon_chain::test_utils::{generate_rand_block_and_blobs, NumBlobs}; + use rand::SeedableRng; + use types::{test_utils::XorShiftRng, ForkName, MinimalEthSpec as E}; + + #[test] + fn no_blobs_into_responses() { + let mut info = BlocksAndBlobsRequestInfo::::new(ByRangeRequestType::Blocks); + let mut rng = XorShiftRng::from_seed([42; 16]); + let blocks = (0..4) + .map(|_| generate_rand_block_and_blobs::(ForkName::Base, NumBlobs::None, &mut rng).0) + .collect::>(); + + // Send blocks and complete terminate response + for block in blocks { + info.add_block_response(Some(block.into())); + } + info.add_block_response(None); + + // Assert response is finished and RpcBlocks can be constructed + assert!(info.is_finished()); + info.into_responses().unwrap(); } } diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index acb735ea44..15595ecde5 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -34,8 +34,9 @@ //! search for the block and subsequently search for parents if needed. use super::backfill_sync::{BackFillSync, ProcessResult, SyncStart}; +use super::block_lookups::common::LookupType; use super::block_lookups::BlockLookups; -use super::network_context::{BlockOrBlob, SyncNetworkContext}; +use super::network_context::{BlockOrBlob, RangeRequestId, SyncNetworkContext}; use super::peer_sync_info::{remote_sync_type, PeerSyncType}; use super::range_sync::{RangeSync, RangeSyncType, EPOCHS_PER_BATCH}; use crate::network_beacon_processor::{ChainSegmentProcessId, NetworkBeaconProcessor}; @@ -43,8 +44,7 @@ use crate::service::NetworkMessage; use crate::status::ToStatusMessage; use crate::sync::block_lookups::common::{Current, Parent}; use crate::sync::block_lookups::{BlobRequestState, BlockRequestState}; -use crate::sync::network_context::BlocksAndBlobsByRangeRequest; -use crate::sync::range_sync::ByRangeRequestType; +use crate::sync::block_sidecar_coupling::BlocksAndBlobsRequestInfo; use beacon_chain::block_verification_types::AsBlock; use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::data_availability_checker::ChildComponents; @@ -57,7 +57,6 @@ use lighthouse_network::types::{NetworkGlobals, SyncState}; use lighthouse_network::SyncInfo; use lighthouse_network::{PeerAction, PeerId}; use slog::{crit, debug, error, info, trace, warn, Logger}; -use std::boxed::Box; use std::ops::IndexMut; use std::ops::Sub; use std::sync::Arc; @@ -81,6 +80,7 @@ pub type Id = u32; pub struct SingleLookupReqId { pub id: Id, pub req_counter: Id, + pub lookup_type: LookupType, } /// Id of rpc requests sent by sync to the network. @@ -90,25 +90,13 @@ pub enum RequestId { SingleBlock { id: SingleLookupReqId }, /// Request searching for a set of blobs given a hash. SingleBlob { id: SingleLookupReqId }, - /// Request searching for a block's parent. The id is the chain, share with the corresponding - /// blob id. - ParentLookup { id: SingleLookupReqId }, - /// Request searching for a block's parent blobs. The id is the chain, shared with the corresponding - /// block id. - ParentLookupBlob { id: SingleLookupReqId }, - /// Request was from the backfill sync algorithm. - BackFillBlocks { id: Id }, - /// Backfill request that is composed by both a block range request and a blob range request. - BackFillBlockAndBlobs { id: Id }, - /// The request was from a chain in the range sync algorithm. - RangeBlocks { id: Id }, /// Range request that is composed by both a block range request and a blob range request. RangeBlockAndBlobs { id: Id }, } #[derive(Debug)] /// A message that can be sent to the sync manager thread. -pub enum SyncMessage { +pub enum SyncMessage { /// A useful peer has been discovered. AddPeer(PeerId, SyncInfo), @@ -116,7 +104,7 @@ pub enum SyncMessage { RpcBlock { request_id: RequestId, peer_id: PeerId, - beacon_block: Option>>, + beacon_block: Option>>, seen_timestamp: Duration, }, @@ -124,15 +112,15 @@ pub enum SyncMessage { RpcBlob { request_id: RequestId, peer_id: PeerId, - blob_sidecar: Option>>, + blob_sidecar: Option>>, seen_timestamp: Duration, }, /// A block with an unknown parent has been received. - UnknownParentBlock(PeerId, RpcBlock, Hash256), + UnknownParentBlock(PeerId, RpcBlock, Hash256), /// A blob with an unknown parent has been received. - UnknownParentBlob(PeerId, Arc>), + UnknownParentBlob(PeerId, Arc>), /// A peer has sent an attestation that references a block that is unknown. This triggers the /// manager to attempt to find the block matching the unknown hash. @@ -157,7 +145,7 @@ pub enum SyncMessage { /// Block processed BlockComponentProcessed { process_type: BlockProcessType, - result: BlockProcessingResult, + result: BlockProcessingResult, }, } @@ -170,9 +158,9 @@ pub enum BlockProcessType { } #[derive(Debug)] -pub enum BlockProcessingResult { +pub enum BlockProcessingResult { Ok(AvailabilityProcessingStatus), - Err(BlockError), + Err(BlockError), Ignored, } @@ -234,24 +222,13 @@ pub fn spawn( ); // create an instance of the SyncManager - let network_globals = beacon_processor.network_globals.clone(); - let mut sync_manager = SyncManager { - chain: beacon_chain.clone(), - input_channel: sync_recv, - network: SyncNetworkContext::new( - network_send, - beacon_processor.clone(), - beacon_chain.clone(), - log.clone(), - ), - range_sync: RangeSync::new(beacon_chain.clone(), log.clone()), - backfill_sync: BackFillSync::new(beacon_chain.clone(), network_globals, log.clone()), - block_lookups: BlockLookups::new( - beacon_chain.data_availability_checker.clone(), - log.clone(), - ), - log: log.clone(), - }; + let mut sync_manager = SyncManager::new( + beacon_chain, + network_send, + beacon_processor, + sync_recv, + log.clone(), + ); // spawn the sync manager thread debug!(log, "Sync Manager started"); @@ -259,6 +236,48 @@ pub fn spawn( } impl SyncManager { + pub(crate) fn new( + beacon_chain: Arc>, + network_send: mpsc::UnboundedSender>, + beacon_processor: Arc>, + sync_recv: mpsc::UnboundedReceiver>, + log: slog::Logger, + ) -> Self { + let network_globals = beacon_processor.network_globals.clone(); + Self { + chain: beacon_chain.clone(), + input_channel: sync_recv, + network: SyncNetworkContext::new( + network_send, + beacon_processor.clone(), + beacon_chain.clone(), + log.clone(), + ), + range_sync: RangeSync::new(beacon_chain.clone(), log.clone()), + backfill_sync: BackFillSync::new(beacon_chain.clone(), network_globals, log.clone()), + block_lookups: BlockLookups::new( + beacon_chain.data_availability_checker.clone(), + log.clone(), + ), + log: log.clone(), + } + } + + #[cfg(test)] + pub(crate) fn active_single_lookups(&self) -> Vec { + self.block_lookups.active_single_lookups() + } + + #[cfg(test)] + pub(crate) fn active_parent_lookups(&self) -> Vec { + self.block_lookups.active_parent_lookups() + } + + #[cfg(test)] + pub(crate) fn failed_chains_contains(&mut self, chain_hash: &Hash256) -> bool { + self.block_lookups.failed_chains_contains(chain_hash) + } + fn network_globals(&self) -> &NetworkGlobals { self.network.network_globals() } @@ -301,99 +320,63 @@ impl SyncManager { fn inject_error(&mut self, peer_id: PeerId, request_id: RequestId, error: RPCError) { trace!(self.log, "Sync manager received a failed RPC"); match request_id { - RequestId::SingleBlock { id } => { - self.block_lookups + RequestId::SingleBlock { id } => match id.lookup_type { + LookupType::Current => self + .block_lookups .single_block_lookup_failed::>( id, &peer_id, &self.network, error, - ); - } - RequestId::SingleBlob { id } => { - self.block_lookups + ), + LookupType::Parent => self + .block_lookups + .parent_lookup_failed::>( + id, + &peer_id, + &self.network, + error, + ), + }, + RequestId::SingleBlob { id } => match id.lookup_type { + LookupType::Current => self + .block_lookups .single_block_lookup_failed::>( id, &peer_id, &self.network, error, - ); - } - RequestId::ParentLookup { id } => { - self.block_lookups - .parent_lookup_failed::>( - id, - peer_id, - &self.network, - error, - ); - } - RequestId::ParentLookupBlob { id } => { - self.block_lookups + ), + LookupType::Parent => self + .block_lookups .parent_lookup_failed::>( id, - peer_id, + &peer_id, &self.network, error, - ); - } - RequestId::BackFillBlocks { id } => { - if let Some(batch_id) = self - .network - .backfill_request_failed(id, ByRangeRequestType::Blocks) - { - match self - .backfill_sync - .inject_error(&mut self.network, batch_id, &peer_id, id) - { - Ok(_) => {} - Err(_) => self.update_sync_state(), - } - } - } - - RequestId::BackFillBlockAndBlobs { id } => { - if let Some(batch_id) = self - .network - .backfill_request_failed(id, ByRangeRequestType::BlocksAndBlobs) - { - match self - .backfill_sync - .inject_error(&mut self.network, batch_id, &peer_id, id) - { - Ok(_) => {} - Err(_) => self.update_sync_state(), - } - } - } - RequestId::RangeBlocks { id } => { - if let Some((chain_id, batch_id)) = self - .network - .range_sync_request_failed(id, ByRangeRequestType::Blocks) - { - self.range_sync.inject_error( - &mut self.network, - peer_id, - batch_id, - chain_id, - id, - ); - self.update_sync_state() - } - } + ), + }, RequestId::RangeBlockAndBlobs { id } => { - if let Some((chain_id, batch_id)) = self - .network - .range_sync_request_failed(id, ByRangeRequestType::BlocksAndBlobs) - { - self.range_sync.inject_error( - &mut self.network, - peer_id, - batch_id, - chain_id, - id, - ); - self.update_sync_state() + if let Some(sender_id) = self.network.range_request_failed(id) { + match sender_id { + RangeRequestId::RangeSync { chain_id, batch_id } => { + self.range_sync.inject_error( + &mut self.network, + peer_id, + batch_id, + chain_id, + id, + ); + self.update_sync_state(); + } + RangeRequestId::BackfillSync { batch_id } => match self + .backfill_sync + .inject_error(&mut self.network, batch_id, &peer_id, id) + { + Ok(_) => {} + Err(_) => self.update_sync_state(), + }, + } } } } @@ -598,7 +581,7 @@ impl SyncManager { } } - fn handle_message(&mut self, sync_message: SyncMessage) { + pub(crate) fn handle_message(&mut self, sync_message: SyncMessage) { match sync_message { SyncMessage::AddPeer(peer_id, info) => { self.add_peer(peer_id, info); @@ -620,6 +603,7 @@ impl SyncManager { SyncMessage::UnknownParentBlock(peer_id, block, block_root) => { let block_slot = block.slot(); let parent_root = block.parent_root(); + debug!(self.log, "Received unknown parent block message"; "block_root" => %block_root, "parent_root" => %parent_root); self.handle_unknown_parent( peer_id, block_root, @@ -639,6 +623,7 @@ impl SyncManager { } let mut blobs = FixedBlobSidecarList::default(); *blobs.index_mut(blob_index as usize) = Some(blob); + debug!(self.log, "Received unknown parent blob message"; "block_root" => %block_root, "parent_root" => %parent_root); self.handle_unknown_parent( peer_id, block_root, @@ -647,12 +632,9 @@ impl SyncManager { ChildComponents::new(block_root, None, Some(blobs)), ); } - SyncMessage::UnknownBlockHashFromAttestation(peer_id, block_hash) => { - // If we are not synced, ignore this block. - if self.synced_and_connected(&peer_id) { - self.block_lookups - .search_block(block_hash, &[peer_id], &mut self.network); - } + SyncMessage::UnknownBlockHashFromAttestation(peer_id, block_root) => { + debug!(self.log, "Received unknown block hash message"; "block_root" => %block_root); + self.handle_unknown_block_root(peer_id, block_root); } SyncMessage::Disconnect(peer_id) => { self.peer_disconnect(&peer_id); @@ -724,25 +706,50 @@ impl SyncManager { slot: Slot, child_components: ChildComponents, ) { - if self.should_search_for_block(slot, &peer_id) { - self.block_lookups.search_parent( - slot, - block_root, - parent_root, - peer_id, - &mut self.network, - ); - self.block_lookups.search_child_block( - block_root, - child_components, - &[peer_id], - &mut self.network, - ); + match self.should_search_for_block(Some(slot), &peer_id) { + Ok(_) => { + self.block_lookups.search_parent( + slot, + block_root, + parent_root, + peer_id, + &mut self.network, + ); + self.block_lookups.search_child_block( + block_root, + child_components, + &[peer_id], + &mut self.network, + ); + } + Err(reason) => { + debug!(self.log, "Ignoring unknown parent request"; "block_root" => %block_root, "parent_root" => %parent_root, "reason" => reason); + } } } - fn should_search_for_block(&mut self, block_slot: Slot, peer_id: &PeerId) -> bool { + fn handle_unknown_block_root(&mut self, peer_id: PeerId, block_root: Hash256) { + match self.should_search_for_block(None, &peer_id) { + Ok(_) => { + self.block_lookups + .search_block(block_root, &[peer_id], &mut self.network); + } + Err(reason) => { + debug!(self.log, "Ignoring unknown block request"; "block_root" => %block_root, "reason" => reason); + } + } + } + + fn should_search_for_block( + &mut self, + block_slot: Option, + peer_id: &PeerId, + ) -> Result<(), &'static str> { if !self.network_globals().sync_state.read().is_synced() { + let Some(block_slot) = block_slot else { + return Err("not synced"); + }; + let head_slot = self.chain.canonical_head.cached_head().head_slot(); // if the block is far in the future, ignore it. If its within the slot tolerance of @@ -752,21 +759,17 @@ impl SyncManager { || (head_slot < block_slot && block_slot.sub(head_slot).as_usize() > SLOT_IMPORT_TOLERANCE) { - return false; + return Err("not synced"); } } - self.network_globals().peers.read().is_connected(peer_id) - && self.network.is_execution_engine_online() - } - - fn synced(&mut self) -> bool { - self.network_globals().sync_state.read().is_synced() - && self.network.is_execution_engine_online() - } - - fn synced_and_connected(&mut self, peer_id: &PeerId) -> bool { - self.synced() && self.network_globals().peers.read().is_connected(peer_id) + if !self.network_globals().peers.read().is_connected(peer_id) { + return Err("peer not connected"); + } + if !self.network.is_execution_engine_online() { + return Err("execution engine offline"); + } + Ok(()) } fn handle_new_execution_engine_state(&mut self, engine_state: EngineState) { @@ -832,73 +835,29 @@ impl SyncManager { seen_timestamp: Duration, ) { match request_id { - RequestId::SingleBlock { id } => self - .block_lookups - .single_lookup_response::>( - id, - peer_id, - block, - seen_timestamp, - &self.network, - ), + RequestId::SingleBlock { id } => match id.lookup_type { + LookupType::Current => self + .block_lookups + .single_lookup_response::>( + id, + peer_id, + block, + seen_timestamp, + &self.network, + ), + LookupType::Parent => self + .block_lookups + .parent_lookup_response::>( + id, + peer_id, + block, + seen_timestamp, + &self.network, + ), + }, RequestId::SingleBlob { .. } => { crit!(self.log, "Block received during blob request"; "peer_id" => %peer_id ); } - RequestId::ParentLookup { id } => self - .block_lookups - .parent_lookup_response::>( - id, - peer_id, - block, - seen_timestamp, - &self.network, - ), - RequestId::ParentLookupBlob { id: _ } => { - crit!(self.log, "Block received during parent blob request"; "peer_id" => %peer_id ); - } - RequestId::BackFillBlocks { id } => { - let is_stream_terminator = block.is_none(); - if let Some(batch_id) = self - .network - .backfill_sync_only_blocks_response(id, is_stream_terminator) - { - match self.backfill_sync.on_block_response( - &mut self.network, - batch_id, - &peer_id, - id, - block.map(|b| RpcBlock::new_without_blobs(None, b)), - ) { - Ok(ProcessResult::SyncCompleted) => self.update_sync_state(), - Ok(ProcessResult::Successful) => {} - Err(_error) => { - // The backfill sync has failed, errors are reported - // within. - self.update_sync_state(); - } - } - } - } - RequestId::RangeBlocks { id } => { - let is_stream_terminator = block.is_none(); - if let Some((chain_id, batch_id)) = self - .network - .range_sync_block_only_response(id, is_stream_terminator) - { - self.range_sync.blocks_by_range_response( - &mut self.network, - peer_id, - chain_id, - batch_id, - id, - block.map(|b| RpcBlock::new_without_blobs(None, b)), - ); - self.update_sync_state(); - } - } - RequestId::BackFillBlockAndBlobs { id } => { - self.backfill_block_and_blobs_response(id, peer_id, block.into()) - } RequestId::RangeBlockAndBlobs { id } => { self.range_block_and_blobs_response(id, peer_id, block.into()) } @@ -916,53 +875,26 @@ impl SyncManager { RequestId::SingleBlock { .. } => { crit!(self.log, "Single blob received during block request"; "peer_id" => %peer_id ); } - RequestId::SingleBlob { id } => { - if let Some(blob) = blob.as_ref() { - debug!(self.log, - "Peer returned blob for single lookup"; - "peer_id" => %peer_id , - "blob_id" =>?blob.id() - ); - } - self.block_lookups + RequestId::SingleBlob { id } => match id.lookup_type { + LookupType::Current => self + .block_lookups .single_lookup_response::>( id, peer_id, blob, seen_timestamp, &self.network, - ) - } - - RequestId::ParentLookup { id: _ } => { - crit!(self.log, "Single blob received during parent block request"; "peer_id" => %peer_id ); - } - RequestId::ParentLookupBlob { id } => { - if let Some(blob) = blob.as_ref() { - debug!(self.log, - "Peer returned blob for parent lookup"; - "peer_id" => %peer_id , - "blob_id" =>?blob.id() - ); - } - self.block_lookups + ), + LookupType::Parent => self + .block_lookups .parent_lookup_response::>( id, peer_id, blob, seen_timestamp, &self.network, - ) - } - RequestId::BackFillBlocks { id: _ } => { - crit!(self.log, "Blob received during backfill block request"; "peer_id" => %peer_id ); - } - RequestId::RangeBlocks { id: _ } => { - crit!(self.log, "Blob received during range block request"; "peer_id" => %peer_id ); - } - RequestId::BackFillBlockAndBlobs { id } => { - self.backfill_block_and_blobs_response(id, peer_id, blob.into()) - } + ), + }, RequestId::RangeBlockAndBlobs { id } => { self.range_block_and_blobs_response(id, peer_id, blob.into()) } @@ -977,9 +909,9 @@ impl SyncManager { peer_id: PeerId, block_or_blob: BlockOrBlob, ) { - if let Some((chain_id, resp)) = self + if let Some(resp) = self .network - .range_sync_block_and_blob_response(id, block_or_blob) + .range_block_and_blob_response(id, block_or_blob) { match resp.responses { Ok(blocks) => { @@ -989,33 +921,52 @@ impl SyncManager { // chain the stream terminator .chain(vec![None]) { - self.range_sync.blocks_by_range_response( - &mut self.network, - peer_id, - chain_id, - resp.batch_id, - id, - block, - ); - self.update_sync_state(); + match resp.sender_id { + RangeRequestId::RangeSync { chain_id, batch_id } => { + self.range_sync.blocks_by_range_response( + &mut self.network, + peer_id, + chain_id, + batch_id, + id, + block, + ); + self.update_sync_state(); + } + RangeRequestId::BackfillSync { batch_id } => { + match self.backfill_sync.on_block_response( + &mut self.network, + batch_id, + &peer_id, + id, + block, + ) { + Ok(ProcessResult::SyncCompleted) => self.update_sync_state(), + Ok(ProcessResult::Successful) => {} + Err(_error) => { + // The backfill sync has failed, errors are reported + // within. + self.update_sync_state(); + } + } + } + } } } Err(e) => { // Re-insert the request so we can retry - let new_req = BlocksAndBlobsByRangeRequest { - chain_id, - batch_id: resp.batch_id, - block_blob_info: <_>::default(), - }; - self.network - .insert_range_blocks_and_blobs_request(id, new_req); + self.network.insert_range_blocks_and_blobs_request( + id, + resp.sender_id, + BlocksAndBlobsRequestInfo::new(resp.request_type), + ); // inform range that the request needs to be treated as failed // With time we will want to downgrade this log warn!( self.log, "Blocks and blobs request for range received invalid data"; "peer_id" => %peer_id, - "batch_id" => resp.batch_id, + "sender_id" => ?resp.sender_id, "error" => e.clone() ); let id = RequestId::RangeBlockAndBlobs { id }; @@ -1029,75 +980,12 @@ impl SyncManager { } } } - - /// Handles receiving a response for a Backfill sync request that should have both blocks and - /// blobs. - fn backfill_block_and_blobs_response( - &mut self, - id: Id, - peer_id: PeerId, - block_or_blob: BlockOrBlob, - ) { - if let Some(resp) = self - .network - .backfill_sync_block_and_blob_response(id, block_or_blob) - { - match resp.responses { - Ok(blocks) => { - for block in blocks - .into_iter() - .map(Some) - // chain the stream terminator - .chain(vec![None]) - { - match self.backfill_sync.on_block_response( - &mut self.network, - resp.batch_id, - &peer_id, - id, - block, - ) { - Ok(ProcessResult::SyncCompleted) => self.update_sync_state(), - Ok(ProcessResult::Successful) => {} - Err(_error) => { - // The backfill sync has failed, errors are reported - // within. - self.update_sync_state(); - } - } - } - } - Err(e) => { - // Re-insert the request so we can retry - self.network.insert_backfill_blocks_and_blobs_requests( - id, - resp.batch_id, - <_>::default(), - ); - - // inform backfill that the request needs to be treated as failed - // With time we will want to downgrade this log - warn!( - self.log, "Blocks and blobs request for backfill received invalid data"; - "peer_id" => %peer_id, "batch_id" => resp.batch_id, "error" => e.clone() - ); - let id = RequestId::BackFillBlockAndBlobs { id }; - self.network.report_peer( - peer_id, - PeerAction::MidToleranceError, - "block_blob_faulty_backfill_batch", - ); - self.inject_error(peer_id, id, RPCError::InvalidData(e)) - } - } - } - } } -impl From>> - for BlockProcessingResult +impl From>> + for BlockProcessingResult { - fn from(result: Result>) -> Self { + fn from(result: Result>) -> Self { match result { Ok(status) => BlockProcessingResult::Ok(status), Err(e) => BlockProcessingResult::Err(e), @@ -1105,8 +993,8 @@ impl From>> } } -impl From> for BlockProcessingResult { - fn from(e: BlockError) -> Self { +impl From> for BlockProcessingResult { + fn from(e: BlockError) -> Self { BlockProcessingResult::Err(e) } } diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 04feb8fdc2..96f8de46fb 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -7,7 +7,6 @@ use super::range_sync::{BatchId, ByRangeRequestType, ChainId}; use crate::network_beacon_processor::NetworkBeaconProcessor; use crate::service::{NetworkMessage, RequestId}; use crate::status::ToStatusMessage; -use crate::sync::block_lookups::common::LookupType; use crate::sync::manager::SingleLookupReqId; use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::{BeaconChain, BeaconChainTypes, EngineState}; @@ -21,15 +20,21 @@ use std::sync::Arc; use tokio::sync::mpsc; use types::{BlobSidecar, EthSpec, SignedBeaconBlock}; -pub struct BlocksAndBlobsByRangeResponse { - pub batch_id: BatchId, - pub responses: Result>, String>, +pub struct BlocksAndBlobsByRangeResponse { + pub sender_id: RangeRequestId, + pub responses: Result>, String>, + pub request_type: ByRangeRequestType, } -pub struct BlocksAndBlobsByRangeRequest { - pub chain_id: ChainId, - pub batch_id: BatchId, - pub block_blob_info: BlocksAndBlobsRequestInfo, +#[derive(Debug, Clone, Copy)] +pub enum RangeRequestId { + RangeSync { + chain_id: ChainId, + batch_id: BatchId, + }, + BackfillSync { + batch_id: BatchId, + }, } /// Wraps a Network channel to employ various RPC related network functionality for the Sync manager. This includes management of a global RPC request Id. @@ -40,18 +45,9 @@ pub struct SyncNetworkContext { /// A sequential ID for all RPC requests. request_id: Id, - /// BlocksByRange requests made by the range syncing algorithm. - range_requests: FnvHashMap, - - /// BlocksByRange requests made by backfill syncing. - backfill_requests: FnvHashMap, - - /// BlocksByRange requests paired with BlobsByRange requests made by the range. - range_blocks_and_blobs_requests: FnvHashMap>, - - /// BlocksByRange requests paired with BlobsByRange requests made by the backfill sync. - backfill_blocks_and_blobs_requests: - FnvHashMap)>, + /// BlocksByRange requests paired with BlobsByRange + range_blocks_and_blobs_requests: + FnvHashMap)>, /// Whether the ee is online. If it's not, we don't allow access to the /// `beacon_processor_send`. @@ -67,19 +63,19 @@ pub struct SyncNetworkContext { } /// Small enumeration to make dealing with block and blob requests easier. -pub enum BlockOrBlob { - Block(Option>>), - Blob(Option>>), +pub enum BlockOrBlob { + Block(Option>>), + Blob(Option>>), } -impl From>>> for BlockOrBlob { - fn from(block: Option>>) -> Self { +impl From>>> for BlockOrBlob { + fn from(block: Option>>) -> Self { BlockOrBlob::Block(block) } } -impl From>>> for BlockOrBlob { - fn from(blob: Option>>) -> Self { +impl From>>> for BlockOrBlob { + fn from(blob: Option>>) -> Self { BlockOrBlob::Blob(blob) } } @@ -95,10 +91,7 @@ impl SyncNetworkContext { network_send, execution_engine_state: EngineState::Online, // always assume `Online` at the start request_id: 1, - range_requests: FnvHashMap::default(), - backfill_requests: FnvHashMap::default(), range_blocks_and_blobs_requests: FnvHashMap::default(), - backfill_blocks_and_blobs_requests: FnvHashMap::default(), network_beacon_processor, chain, log, @@ -149,266 +142,85 @@ impl SyncNetworkContext { peer_id: PeerId, batch_type: ByRangeRequestType, request: BlocksByRangeRequest, - chain_id: ChainId, - batch_id: BatchId, ) -> Result { - match batch_type { - ByRangeRequestType::Blocks => { - trace!( - self.log, - "Sending BlocksByRange request"; - "method" => "BlocksByRange", - "count" => request.count(), - "peer" => %peer_id, - ); - let request = Request::BlocksByRange(request); - let id = self.next_id(); - let request_id = RequestId::Sync(SyncRequestId::RangeBlocks { id }); - self.send_network_msg(NetworkMessage::SendRequest { - peer_id, - request, - request_id, - })?; - self.range_requests.insert(id, (chain_id, batch_id)); - Ok(id) - } - ByRangeRequestType::BlocksAndBlobs => { - debug!( - self.log, - "Sending BlocksByRange and BlobsByRange requests"; - "method" => "Mixed by range request", - "count" => request.count(), - "peer" => %peer_id, - ); + let id = self.next_id(); + trace!( + self.log, + "Sending BlocksByRange request"; + "method" => "BlocksByRange", + "count" => request.count(), + "peer" => %peer_id, + ); + self.send_network_msg(NetworkMessage::SendRequest { + peer_id, + request: Request::BlocksByRange(request.clone()), + request_id: RequestId::Sync(SyncRequestId::RangeBlockAndBlobs { id }), + })?; - // create the shared request id. This is fine since the rpc handles substream ids. - let id = self.next_id(); - let request_id = RequestId::Sync(SyncRequestId::RangeBlockAndBlobs { id }); + if matches!(batch_type, ByRangeRequestType::BlocksAndBlobs) { + debug!( + self.log, + "Sending BlobsByRange requests"; + "method" => "BlobsByRange", + "count" => request.count(), + "peer" => %peer_id, + ); - // Create the blob request based on the blob request. - let blobs_request = Request::BlobsByRange(BlobsByRangeRequest { + // Create the blob request based on the blocks request. + self.send_network_msg(NetworkMessage::SendRequest { + peer_id, + request: Request::BlobsByRange(BlobsByRangeRequest { start_slot: *request.start_slot(), count: *request.count(), - }); - let blocks_request = Request::BlocksByRange(request); - - // Send both requests. Make sure both can be sent. - self.send_network_msg(NetworkMessage::SendRequest { - peer_id, - request: blocks_request, - request_id, - })?; - self.send_network_msg(NetworkMessage::SendRequest { - peer_id, - request: blobs_request, - request_id, - })?; - let block_blob_info = BlocksAndBlobsRequestInfo::default(); - self.range_blocks_and_blobs_requests.insert( - id, - BlocksAndBlobsByRangeRequest { - chain_id, - batch_id, - block_blob_info, - }, - ); - Ok(id) - } + }), + request_id: RequestId::Sync(SyncRequestId::RangeBlockAndBlobs { id }), + })?; } + + Ok(id) } - /// A blocks by range request sent by the backfill sync algorithm - pub fn backfill_blocks_by_range_request( + /// A blocks by range request sent by the range sync algorithm + pub fn blocks_and_blobs_by_range_request( &mut self, peer_id: PeerId, batch_type: ByRangeRequestType, request: BlocksByRangeRequest, - batch_id: BatchId, + sender_id: RangeRequestId, ) -> Result { - match batch_type { - ByRangeRequestType::Blocks => { - trace!( - self.log, - "Sending backfill BlocksByRange request"; - "method" => "BlocksByRange", - "count" => request.count(), - "peer" => %peer_id, - ); - let request = Request::BlocksByRange(request); - let id = self.next_id(); - let request_id = RequestId::Sync(SyncRequestId::BackFillBlocks { id }); - self.send_network_msg(NetworkMessage::SendRequest { - peer_id, - request, - request_id, - })?; - self.backfill_requests.insert(id, batch_id); - Ok(id) - } - ByRangeRequestType::BlocksAndBlobs => { - debug!( - self.log, - "Sending backfill BlocksByRange and BlobsByRange requests"; - "method" => "Mixed by range request", - "count" => request.count(), - "peer" => %peer_id, - ); - - // create the shared request id. This is fine since the rpc handles substream ids. - let id = self.next_id(); - let request_id = RequestId::Sync(SyncRequestId::BackFillBlockAndBlobs { id }); - - // Create the blob request based on the blob request. - let blobs_request = Request::BlobsByRange(BlobsByRangeRequest { - start_slot: *request.start_slot(), - count: *request.count(), - }); - let blocks_request = Request::BlocksByRange(request); - - // Send both requests. Make sure both can be sent. - self.send_network_msg(NetworkMessage::SendRequest { - peer_id, - request: blocks_request, - request_id, - })?; - self.send_network_msg(NetworkMessage::SendRequest { - peer_id, - request: blobs_request, - request_id, - })?; - let block_blob_info = BlocksAndBlobsRequestInfo::default(); - self.backfill_blocks_and_blobs_requests - .insert(id, (batch_id, block_blob_info)); - Ok(id) - } - } + let id = self.blocks_by_range_request(peer_id, batch_type, request)?; + self.range_blocks_and_blobs_requests + .insert(id, (sender_id, BlocksAndBlobsRequestInfo::new(batch_type))); + Ok(id) } - /// Response for a request that is only for blocks. - pub fn range_sync_block_only_response( - &mut self, - request_id: Id, - is_stream_terminator: bool, - ) -> Option<(ChainId, BatchId)> { - if is_stream_terminator { - self.range_requests.remove(&request_id) - } else { - self.range_requests.get(&request_id).copied() - } - } - - /// Received a blocks by range response for a request that couples blocks and blobs. - pub fn range_sync_block_and_blob_response( - &mut self, - request_id: Id, - block_or_blob: BlockOrBlob, - ) -> Option<(ChainId, BlocksAndBlobsByRangeResponse)> { - match self.range_blocks_and_blobs_requests.entry(request_id) { - Entry::Occupied(mut entry) => { - let req = entry.get_mut(); - let info = &mut req.block_blob_info; - match block_or_blob { - BlockOrBlob::Block(maybe_block) => info.add_block_response(maybe_block), - BlockOrBlob::Blob(maybe_sidecar) => info.add_sidecar_response(maybe_sidecar), - } - if info.is_finished() { - // If the request is finished, dequeue everything - let BlocksAndBlobsByRangeRequest { - chain_id, - batch_id, - block_blob_info, - } = entry.remove(); - Some(( - chain_id, - BlocksAndBlobsByRangeResponse { - batch_id, - responses: block_blob_info.into_responses(), - }, - )) - } else { - None - } - } - Entry::Vacant(_) => None, - } - } - - pub fn range_sync_request_failed( - &mut self, - request_id: Id, - batch_type: ByRangeRequestType, - ) -> Option<(ChainId, BatchId)> { - let req = match batch_type { - ByRangeRequestType::BlocksAndBlobs => self - .range_blocks_and_blobs_requests - .remove(&request_id) - .map(|req| (req.chain_id, req.batch_id)), - ByRangeRequestType::Blocks => self.range_requests.remove(&request_id), - }; - if let Some(req) = req { + pub fn range_request_failed(&mut self, request_id: Id) -> Option { + let sender_id = self + .range_blocks_and_blobs_requests + .remove(&request_id) + .map(|(sender_id, _info)| sender_id); + if let Some(sender_id) = sender_id { debug!( self.log, - "Range sync request failed"; + "Sync range request failed"; "request_id" => request_id, - "batch_type" => ?batch_type, - "chain_id" => ?req.0, - "batch_id" => ?req.1 + "sender_id" => ?sender_id ); - Some(req) + Some(sender_id) } else { - debug!(self.log, "Range sync request failed"; "request_id" => request_id, "batch_type" => ?batch_type); + debug!(self.log, "Sync range request failed"; "request_id" => request_id); None } } - pub fn backfill_request_failed( - &mut self, - request_id: Id, - batch_type: ByRangeRequestType, - ) -> Option { - let batch_id = match batch_type { - ByRangeRequestType::BlocksAndBlobs => self - .backfill_blocks_and_blobs_requests - .remove(&request_id) - .map(|(batch_id, _info)| batch_id), - ByRangeRequestType::Blocks => self.backfill_requests.remove(&request_id), - }; - if let Some(batch_id) = batch_id { - debug!( - self.log, - "Backfill sync request failed"; - "request_id" => request_id, - "batch_type" => ?batch_type, - "batch_id" => ?batch_id - ); - Some(batch_id) - } else { - debug!(self.log, "Backfill sync request failed"; "request_id" => request_id, "batch_type" => ?batch_type); - None - } - } - - /// Response for a request that is only for blocks. - pub fn backfill_sync_only_blocks_response( - &mut self, - request_id: Id, - is_stream_terminator: bool, - ) -> Option { - if is_stream_terminator { - self.backfill_requests.remove(&request_id) - } else { - self.backfill_requests.get(&request_id).copied() - } - } - /// Received a blocks by range or blobs by range response for a request that couples blocks ' /// and blobs. - pub fn backfill_sync_block_and_blob_response( + pub fn range_block_and_blob_response( &mut self, request_id: Id, block_or_blob: BlockOrBlob, ) -> Option> { - match self.backfill_blocks_and_blobs_requests.entry(request_id) { + match self.range_blocks_and_blobs_requests.entry(request_id) { Entry::Occupied(mut entry) => { let (_, info) = entry.get_mut(); match block_or_blob { @@ -417,12 +229,12 @@ impl SyncNetworkContext { } if info.is_finished() { // If the request is finished, dequeue everything - let (batch_id, info) = entry.remove(); - - let responses = info.into_responses(); + let (sender_id, info) = entry.remove(); + let request_type = info.get_request_type(); Some(BlocksAndBlobsByRangeResponse { - batch_id, - responses, + sender_id, + request_type, + responses: info.into_responses(), }) } else { None @@ -437,27 +249,20 @@ impl SyncNetworkContext { id: SingleLookupReqId, peer_id: PeerId, request: BlocksByRootRequest, - lookup_type: LookupType, ) -> Result<(), &'static str> { - let sync_id = match lookup_type { - LookupType::Current => SyncRequestId::SingleBlock { id }, - LookupType::Parent => SyncRequestId::ParentLookup { id }, - }; - let request_id = RequestId::Sync(sync_id); - debug!( self.log, "Sending BlocksByRoot Request"; "method" => "BlocksByRoot", "block_roots" => ?request.block_roots().to_vec(), "peer" => %peer_id, - "lookup_type" => ?lookup_type + "id" => ?id ); self.send_network_msg(NetworkMessage::SendRequest { peer_id, request: Request::BlocksByRoot(request), - request_id, + request_id: RequestId::Sync(SyncRequestId::SingleBlock { id }), })?; Ok(()) } @@ -467,14 +272,7 @@ impl SyncNetworkContext { id: SingleLookupReqId, blob_peer_id: PeerId, blob_request: BlobsByRootRequest, - lookup_type: LookupType, ) -> Result<(), &'static str> { - let sync_id = match lookup_type { - LookupType::Current => SyncRequestId::SingleBlob { id }, - LookupType::Parent => SyncRequestId::ParentLookupBlob { id }, - }; - let request_id = RequestId::Sync(sync_id); - if let Some(block_root) = blob_request .blob_ids .as_slice() @@ -494,13 +292,13 @@ impl SyncNetworkContext { "block_root" => ?block_root, "blob_indices" => ?indices, "peer" => %blob_peer_id, - "lookup_type" => ?lookup_type + "id" => ?id ); self.send_network_msg(NetworkMessage::SendRequest { peer_id: blob_peer_id, request: Request::BlobsByRoot(blob_request), - request_id, + request_id: RequestId::Sync(SyncRequestId::SingleBlob { id }), })?; } Ok(()) @@ -601,18 +399,10 @@ impl SyncNetworkContext { pub fn insert_range_blocks_and_blobs_request( &mut self, id: Id, - request: BlocksAndBlobsByRangeRequest, + sender_id: RangeRequestId, + info: BlocksAndBlobsRequestInfo, ) { - self.range_blocks_and_blobs_requests.insert(id, request); - } - - pub fn insert_backfill_blocks_and_blobs_requests( - &mut self, - id: Id, - batch_id: BatchId, - request: BlocksAndBlobsRequestInfo, - ) { - self.backfill_blocks_and_blobs_requests - .insert(id, (batch_id, request)); + self.range_blocks_and_blobs_requests + .insert(id, (sender_id, info)); } } diff --git a/beacon_node/network/src/sync/range_sync/batch.rs b/beacon_node/network/src/sync/range_sync/batch.rs index f5c320cb88..75cb49d176 100644 --- a/beacon_node/network/src/sync/range_sync/batch.rs +++ b/beacon_node/network/src/sync/range_sync/batch.rs @@ -56,7 +56,7 @@ pub trait BatchConfig { /// Note that simpler hashing functions considered in the past (hash of first block, hash of last /// block, number of received blocks) are not good enough to differentiate attempts. For this /// reason, we hash the complete set of blocks both in RangeSync and BackFillSync. - fn batch_attempt_hash(blocks: &[RpcBlock]) -> u64; + fn batch_attempt_hash(blocks: &[RpcBlock]) -> u64; } pub struct RangeSyncBatchConfig {} @@ -68,7 +68,7 @@ impl BatchConfig for RangeSyncBatchConfig { fn max_batch_processing_attempts() -> u8 { MAX_BATCH_PROCESSING_ATTEMPTS } - fn batch_attempt_hash(blocks: &[RpcBlock]) -> u64 { + fn batch_attempt_hash(blocks: &[RpcBlock]) -> u64 { let mut hasher = std::collections::hash_map::DefaultHasher::new(); blocks.hash(&mut hasher); hasher.finish() @@ -92,7 +92,7 @@ pub enum BatchProcessingResult { } /// A segment of a chain. -pub struct BatchInfo { +pub struct BatchInfo { /// Start slot of the batch. start_slot: Slot, /// End slot of the batch. @@ -104,7 +104,7 @@ pub struct BatchInfo { /// The number of download retries this batch has undergone due to a failed request. failed_download_attempts: Vec, /// State of the batch. - state: BatchState, + state: BatchState, /// Whether this batch contains all blocks or all blocks and blobs. batch_type: ByRangeRequestType, /// Pin the generic @@ -112,13 +112,13 @@ pub struct BatchInfo { } /// Current state of a batch -pub enum BatchState { +pub enum BatchState { /// The batch has failed either downloading or processing, but can be requested again. AwaitingDownload, /// The batch is being downloaded. - Downloading(PeerId, Vec>, Id), + Downloading(PeerId, Vec>, Id), /// The batch has been completely downloaded and is ready for processing. - AwaitingProcessing(PeerId, Vec>), + AwaitingProcessing(PeerId, Vec>), /// The batch is being processed. Processing(Attempt), /// The batch was successfully processed and is waiting to be validated. @@ -134,14 +134,14 @@ pub enum BatchState { Failed, } -impl BatchState { +impl BatchState { /// Helper function for poisoning a state. - pub fn poison(&mut self) -> BatchState { + pub fn poison(&mut self) -> BatchState { std::mem::replace(self, BatchState::Poisoned) } } -impl BatchInfo { +impl BatchInfo { /// Batches are downloaded excluding the first block of the epoch assuming it has already been /// downloaded. /// @@ -156,8 +156,8 @@ impl BatchInfo { /// deal with this for now. /// This means finalization might be slower in deneb pub fn new(start_epoch: &Epoch, num_of_epochs: u64, batch_type: ByRangeRequestType) -> Self { - let start_slot = start_epoch.start_slot(T::slots_per_epoch()); - let end_slot = start_slot + num_of_epochs * T::slots_per_epoch(); + let start_slot = start_epoch.start_slot(E::slots_per_epoch()); + let end_slot = start_slot + num_of_epochs * E::slots_per_epoch(); BatchInfo { start_slot, end_slot, @@ -242,7 +242,7 @@ impl BatchInfo { } } - pub fn state(&self) -> &BatchState { + pub fn state(&self) -> &BatchState { &self.state } @@ -251,7 +251,7 @@ impl BatchInfo { } /// Adds a block to a downloading batch. - pub fn add_block(&mut self, block: RpcBlock) -> Result<(), WrongState> { + pub fn add_block(&mut self, block: RpcBlock) -> Result<(), WrongState> { match self.state.poison() { BatchState::Downloading(peer, mut blocks, req_id) => { blocks.push(block); @@ -383,10 +383,10 @@ impl BatchInfo { } } - pub fn start_processing(&mut self) -> Result>, WrongState> { + pub fn start_processing(&mut self) -> Result>, WrongState> { match self.state.poison() { BatchState::AwaitingProcessing(peer, blocks) => { - self.state = BatchState::Processing(Attempt::new::(peer, &blocks)); + self.state = BatchState::Processing(Attempt::new::(peer, &blocks)); Ok(blocks) } BatchState::Poisoned => unreachable!("Poisoned batch"), @@ -481,13 +481,13 @@ pub struct Attempt { } impl Attempt { - fn new(peer_id: PeerId, blocks: &[RpcBlock]) -> Self { + fn new(peer_id: PeerId, blocks: &[RpcBlock]) -> Self { let hash = B::batch_attempt_hash(blocks); Attempt { peer_id, hash } } } -impl slog::KV for &mut BatchInfo { +impl slog::KV for &mut BatchInfo { fn serialize( &self, record: &slog::Record, @@ -497,7 +497,7 @@ impl slog::KV for &mut BatchInfo { } } -impl slog::KV for BatchInfo { +impl slog::KV for BatchInfo { fn serialize( &self, record: &slog::Record, @@ -520,7 +520,7 @@ impl slog::KV for BatchInfo { } } -impl std::fmt::Debug for BatchState { +impl std::fmt::Debug for BatchState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { BatchState::Processing(Attempt { diff --git a/beacon_node/network/src/sync/range_sync/chain.rs b/beacon_node/network/src/sync/range_sync/chain.rs index 5a77340e3b..c60cdb2cc9 100644 --- a/beacon_node/network/src/sync/range_sync/chain.rs +++ b/beacon_node/network/src/sync/range_sync/chain.rs @@ -1,5 +1,6 @@ use super::batch::{BatchInfo, BatchProcessingResult, BatchState}; use crate::network_beacon_processor::ChainSegmentProcessId; +use crate::sync::network_context::RangeRequestId; use crate::sync::{ manager::Id, network_context::SyncNetworkContext, BatchOperationOutcome, BatchProcessResult, }; @@ -905,7 +906,15 @@ impl SyncingChain { ) -> ProcessingResult { if let Some(batch) = self.batches.get_mut(&batch_id) { let (request, batch_type) = batch.to_blocks_by_range_request(); - match network.blocks_by_range_request(peer, batch_type, request, self.id, batch_id) { + match network.blocks_and_blobs_by_range_request( + peer, + batch_type, + request, + RangeRequestId::RangeSync { + chain_id: self.id, + batch_id, + }, + ) { Ok(request_id) => { // inform the batch about the new request batch.start_downloading_from_peer(peer, request_id)?; diff --git a/beacon_node/network/src/sync/range_sync/range.rs b/beacon_node/network/src/sync/range_sync/range.rs index e42fd936e6..c8e8266684 100644 --- a/beacon_node/network/src/sync/range_sync/range.rs +++ b/beacon_node/network/src/sync/range_sync/range.rs @@ -384,7 +384,7 @@ mod tests { use crate::NetworkMessage; use super::*; - use crate::sync::network_context::BlockOrBlob; + use crate::sync::network_context::{BlockOrBlob, RangeRequestId}; use beacon_chain::builder::Witness; use beacon_chain::eth1_chain::CachingEth1Backend; use beacon_chain::parking_lot::RwLock; @@ -395,10 +395,9 @@ mod tests { use slog::{o, Drain}; use slot_clock::TestingSlotClock; use std::collections::HashSet; - use std::sync::Arc; use store::MemoryStore; use tokio::sync::mpsc; - use types::{ForkName, Hash256, MinimalEthSpec as E}; + use types::{ForkName, MinimalEthSpec as E}; #[derive(Debug)] struct FakeStorage { @@ -531,7 +530,7 @@ mod tests { panic!("Should have sent a batch request to the peer") }; let blob_req_id = match fork_name { - ForkName::Deneb => { + ForkName::Deneb | ForkName::Electra => { if let Ok(NetworkMessage::SendRequest { peer_id, request: _, @@ -549,6 +548,51 @@ mod tests { (block_req_id, blob_req_id) } + fn complete_range_block_and_blobs_response( + &mut self, + block_req: RequestId, + blob_req_opt: Option, + ) -> (ChainId, BatchId, Id) { + if blob_req_opt.is_some() { + match block_req { + RequestId::Sync(crate::sync::manager::RequestId::RangeBlockAndBlobs { id }) => { + let _ = self + .cx + .range_block_and_blob_response(id, BlockOrBlob::Block(None)); + let response = self + .cx + .range_block_and_blob_response(id, BlockOrBlob::Blob(None)) + .unwrap(); + let (chain_id, batch_id) = + TestRig::unwrap_range_request_id(response.sender_id); + (chain_id, batch_id, id) + } + other => panic!("unexpected request {:?}", other), + } + } else { + match block_req { + RequestId::Sync(crate::sync::manager::RequestId::RangeBlockAndBlobs { id }) => { + let response = self + .cx + .range_block_and_blob_response(id, BlockOrBlob::Block(None)) + .unwrap(); + let (chain_id, batch_id) = + TestRig::unwrap_range_request_id(response.sender_id); + (chain_id, batch_id, id) + } + other => panic!("unexpected request {:?}", other), + } + } + } + + fn unwrap_range_request_id(sender_id: RangeRequestId) -> (ChainId, BatchId) { + if let RangeRequestId::RangeSync { chain_id, batch_id } = sender_id { + (chain_id, batch_id) + } else { + panic!("expected RangeSync request: {:?}", sender_id) + } + } + /// Produce a head peer fn head_peer( &self, @@ -638,7 +682,12 @@ mod tests { 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()); + NetworkBeaconProcessor::null_for_testing( + globals.clone(), + chain.clone(), + harness.runtime.task_executor.clone(), + log.clone(), + ); let cx = SyncNetworkContext::new( network_tx, Arc::new(network_beacon_processor), @@ -740,29 +789,8 @@ mod tests { range.add_peer(&mut rig.cx, local_info, peer1, head_info); let (block_req, blob_req_opt) = rig.grab_request(&peer1, fork); - let (chain1, batch1, id1) = if blob_req_opt.is_some() { - match block_req { - RequestId::Sync(crate::sync::manager::RequestId::RangeBlockAndBlobs { id }) => { - let _ = rig - .cx - .range_sync_block_and_blob_response(id, BlockOrBlob::Block(None)); - let (chain1, response) = rig - .cx - .range_sync_block_and_blob_response(id, BlockOrBlob::Blob(None)) - .unwrap(); - (chain1, response.batch_id, id) - } - other => panic!("unexpected request {:?}", other), - } - } else { - match block_req { - RequestId::Sync(crate::sync::manager::RequestId::RangeBlocks { id }) => { - let (chain, batch) = rig.cx.range_sync_block_only_response(id, true).unwrap(); - (chain, batch, id) - } - other => panic!("unexpected request {:?}", other), - } - }; + let (chain1, batch1, id1) = + rig.complete_range_block_and_blobs_response(block_req, blob_req_opt); // make the ee offline rig.cx.update_execution_engine_state(EngineState::Offline); @@ -778,29 +806,8 @@ mod tests { range.add_peer(&mut rig.cx, local_info, peer2, finalized_info); let (block_req, blob_req_opt) = rig.grab_request(&peer2, fork); - let (chain2, batch2, id2) = if blob_req_opt.is_some() { - match block_req { - RequestId::Sync(crate::sync::manager::RequestId::RangeBlockAndBlobs { id }) => { - let _ = rig - .cx - .range_sync_block_and_blob_response(id, BlockOrBlob::Block(None)); - let (chain2, response) = rig - .cx - .range_sync_block_and_blob_response(id, BlockOrBlob::Blob(None)) - .unwrap(); - (chain2, response.batch_id, id) - } - other => panic!("unexpected request {:?}", other), - } - } else { - match block_req { - RequestId::Sync(crate::sync::manager::RequestId::RangeBlocks { id }) => { - let (chain, batch) = rig.cx.range_sync_block_only_response(id, true).unwrap(); - (chain, batch, id) - } - other => panic!("unexpected request {:?}", other), - } - }; + let (chain2, batch2, id2) = + rig.complete_range_block_and_blobs_response(block_req, blob_req_opt); // send the response to the request range.blocks_by_range_response(&mut rig.cx, peer2, chain2, batch2, id2, None); diff --git a/beacon_node/operation_pool/src/attestation.rs b/beacon_node/operation_pool/src/attestation.rs index 97c291aa85..5c6f684e72 100644 --- a/beacon_node/operation_pool/src/attestation.rs +++ b/beacon_node/operation_pool/src/attestation.rs @@ -2,7 +2,7 @@ use crate::attestation_storage::AttestationRef; use crate::max_cover::MaxCover; use crate::reward_cache::RewardCache; use state_processing::common::{ - altair, base, get_attestation_participation_flag_indices, get_attesting_indices, + base, get_attestation_participation_flag_indices, get_attesting_indices, }; use std::collections::HashMap; use types::{ @@ -12,17 +12,17 @@ use types::{ }; #[derive(Debug, Clone)] -pub struct AttMaxCover<'a, T: EthSpec> { +pub struct AttMaxCover<'a, E: EthSpec> { /// Underlying attestation. - pub att: AttestationRef<'a, T>, + pub att: AttestationRef<'a, E>, /// Mapping of validator indices and their rewards. pub fresh_validators_rewards: HashMap, } -impl<'a, T: EthSpec> AttMaxCover<'a, T> { +impl<'a, E: EthSpec> AttMaxCover<'a, E> { pub fn new( - att: AttestationRef<'a, T>, - state: &BeaconState, + att: AttestationRef<'a, E>, + state: &BeaconState, reward_cache: &'a RewardCache, total_active_balance: u64, spec: &ChainSpec, @@ -30,15 +30,15 @@ impl<'a, T: EthSpec> AttMaxCover<'a, T> { if let BeaconState::Base(ref base_state) = state { Self::new_for_base(att, state, base_state, total_active_balance, spec) } else { - Self::new_for_altair_deneb(att, state, reward_cache, total_active_balance, spec) + Self::new_for_altair_deneb(att, state, reward_cache, spec) } } /// Initialise an attestation cover object for base/phase0 hard fork. pub fn new_for_base( - att: AttestationRef<'a, T>, - state: &BeaconState, - base_state: &BeaconStateBase, + att: AttestationRef<'a, E>, + state: &BeaconState, + base_state: &BeaconStateBase, total_active_balance: u64, spec: &ChainSpec, ) -> Option { @@ -46,19 +46,18 @@ impl<'a, T: EthSpec> AttMaxCover<'a, T> { let committee = state .get_beacon_committee(att.data.slot, att.data.index) .ok()?; - let indices = get_attesting_indices::(committee.committee, &fresh_validators).ok()?; + let indices = get_attesting_indices::(committee.committee, &fresh_validators).ok()?; + let sqrt_total_active_balance = base::SqrtTotalActiveBalance::new(total_active_balance); let fresh_validators_rewards: HashMap = indices .iter() .copied() .flat_map(|validator_index| { - let reward = base::get_base_reward( - state, - validator_index as usize, - total_active_balance, - spec, - ) - .ok()? - .checked_div(spec.proposer_reward_quotient)?; + let effective_balance = + state.get_effective_balance(validator_index as usize).ok()?; + let reward = + base::get_base_reward(effective_balance, sqrt_total_active_balance, spec) + .ok()? + .checked_div(spec.proposer_reward_quotient)?; Some((validator_index, reward)) }) .collect(); @@ -70,10 +69,9 @@ impl<'a, T: EthSpec> AttMaxCover<'a, T> { /// Initialise an attestation cover object for Altair or later. pub fn new_for_altair_deneb( - att: AttestationRef<'a, T>, - state: &BeaconState, + att: AttestationRef<'a, E>, + state: &BeaconState, reward_cache: &'a RewardCache, - total_active_balance: u64, spec: &ChainSpec, ) -> Option { let att_data = att.attestation_data(); @@ -82,8 +80,6 @@ impl<'a, T: EthSpec> AttMaxCover<'a, T> { let att_participation_flags = get_attestation_participation_flag_indices(state, &att_data, inclusion_delay, spec) .ok()?; - let base_reward_per_increment = - altair::BaseRewardPerIncrement::new(total_active_balance, spec).ok()?; let fresh_validators_rewards = att .indexed @@ -99,9 +95,7 @@ impl<'a, T: EthSpec> AttMaxCover<'a, T> { let mut proposer_reward_numerator = 0; - let base_reward = - altair::get_base_reward(state, index as usize, base_reward_per_increment, spec) - .ok()?; + let base_reward = state.get_base_reward(index as usize).ok()?; for (flag_index, weight) in PARTICIPATION_FLAG_WEIGHTS.iter().enumerate() { if att_participation_flags.contains(&flag_index) { @@ -123,16 +117,16 @@ impl<'a, T: EthSpec> AttMaxCover<'a, T> { } } -impl<'a, T: EthSpec> MaxCover for AttMaxCover<'a, T> { - type Object = Attestation; - type Intermediate = AttestationRef<'a, T>; +impl<'a, E: EthSpec> MaxCover for AttMaxCover<'a, E> { + type Object = Attestation; + type Intermediate = AttestationRef<'a, E>; type Set = HashMap; - fn intermediate(&self) -> &AttestationRef<'a, T> { + fn intermediate(&self) -> &AttestationRef<'a, E> { &self.att } - fn convert_to_object(att_ref: &AttestationRef<'a, T>) -> Attestation { + fn convert_to_object(att_ref: &AttestationRef<'a, E>) -> Attestation { att_ref.clone_as_attestation() } @@ -152,7 +146,7 @@ impl<'a, T: EthSpec> MaxCover for AttMaxCover<'a, T> { /// of slashable voting, which is rare. fn update_covering_set( &mut self, - best_att: &AttestationRef<'a, T>, + best_att: &AttestationRef<'a, E>, covered_validators: &HashMap, ) { if self.att.data.slot == best_att.data.slot && self.att.data.index == best_att.data.index { @@ -175,11 +169,11 @@ impl<'a, T: EthSpec> MaxCover for AttMaxCover<'a, T> { /// removed from the `aggregation_bits` before returning it. /// /// This isn't optimal, but with the Altair fork this code is obsolete and not worth upgrading. -pub fn earliest_attestation_validators( - attestation: &AttestationRef, - state: &BeaconState, - base_state: &BeaconStateBase, -) -> BitList { +pub fn earliest_attestation_validators( + attestation: &AttestationRef, + state: &BeaconState, + base_state: &BeaconStateBase, +) -> BitList { // Bitfield of validators whose attestations are new/fresh. let mut new_validators = attestation.indexed.aggregation_bits.clone(); diff --git a/beacon_node/operation_pool/src/attestation_storage.rs b/beacon_node/operation_pool/src/attestation_storage.rs index dac5e25b34..43fdf3923b 100644 --- a/beacon_node/operation_pool/src/attestation_storage.rs +++ b/beacon_node/operation_pool/src/attestation_storage.rs @@ -21,38 +21,38 @@ pub struct CompactAttestationData { } #[derive(Debug, PartialEq)] -pub struct CompactIndexedAttestation { +pub struct CompactIndexedAttestation { pub attesting_indices: Vec, - pub aggregation_bits: BitList, + pub aggregation_bits: BitList, pub signature: AggregateSignature, } #[derive(Debug)] -pub struct SplitAttestation { +pub struct SplitAttestation { pub checkpoint: CheckpointKey, pub data: CompactAttestationData, - pub indexed: CompactIndexedAttestation, + pub indexed: CompactIndexedAttestation, } #[derive(Debug, Clone)] -pub struct AttestationRef<'a, T: EthSpec> { +pub struct AttestationRef<'a, E: EthSpec> { pub checkpoint: &'a CheckpointKey, pub data: &'a CompactAttestationData, - pub indexed: &'a CompactIndexedAttestation, + pub indexed: &'a CompactIndexedAttestation, } #[derive(Debug, Default, PartialEq)] -pub struct AttestationMap { - checkpoint_map: HashMap>, +pub struct AttestationMap { + checkpoint_map: HashMap>, } #[derive(Debug, Default, PartialEq)] -pub struct AttestationDataMap { - attestations: HashMap>>, +pub struct AttestationDataMap { + attestations: HashMap>>, } -impl SplitAttestation { - pub fn new(attestation: Attestation, attesting_indices: Vec) -> Self { +impl SplitAttestation { + pub fn new(attestation: Attestation, attesting_indices: Vec) -> Self { let checkpoint = CheckpointKey { source: attestation.data.source, target_epoch: attestation.data.target.epoch, @@ -75,7 +75,7 @@ impl SplitAttestation { } } - pub fn as_ref(&self) -> AttestationRef { + pub fn as_ref(&self) -> AttestationRef { AttestationRef { checkpoint: &self.checkpoint, data: &self.data, @@ -84,7 +84,7 @@ impl SplitAttestation { } } -impl<'a, T: EthSpec> AttestationRef<'a, T> { +impl<'a, E: EthSpec> AttestationRef<'a, E> { pub fn attestation_data(&self) -> AttestationData { AttestationData { slot: self.data.slot, @@ -98,7 +98,7 @@ impl<'a, T: EthSpec> AttestationRef<'a, T> { } } - pub fn clone_as_attestation(&self) -> Attestation { + pub fn clone_as_attestation(&self) -> Attestation { Attestation { aggregation_bits: self.indexed.aggregation_bits.clone(), data: self.attestation_data(), @@ -110,7 +110,7 @@ impl<'a, T: EthSpec> AttestationRef<'a, T> { impl CheckpointKey { /// Return two checkpoint keys: `(previous, current)` for the previous and current epochs of /// the `state`. - pub fn keys_for_state(state: &BeaconState) -> (Self, Self) { + pub fn keys_for_state(state: &BeaconState) -> (Self, Self) { ( CheckpointKey { source: state.previous_justified_checkpoint(), @@ -124,7 +124,7 @@ impl CheckpointKey { } } -impl CompactIndexedAttestation { +impl CompactIndexedAttestation { pub fn signers_disjoint_from(&self, other: &Self) -> bool { self.aggregation_bits .intersection(&other.aggregation_bits) @@ -143,8 +143,8 @@ impl CompactIndexedAttestation { } } -impl AttestationMap { - pub fn insert(&mut self, attestation: Attestation, attesting_indices: Vec) { +impl AttestationMap { + pub fn insert(&mut self, attestation: Attestation, attesting_indices: Vec) { let SplitAttestation { checkpoint, data, @@ -176,7 +176,7 @@ impl AttestationMap { pub fn get_attestations<'a>( &'a self, checkpoint_key: &'a CheckpointKey, - ) -> impl Iterator> + 'a { + ) -> impl Iterator> + 'a { self.checkpoint_map .get(checkpoint_key) .into_iter() @@ -184,7 +184,7 @@ impl AttestationMap { } /// Iterate all attestations in the map. - pub fn iter(&self) -> impl Iterator> { + pub fn iter(&self) -> impl Iterator> { self.checkpoint_map .iter() .flat_map(|(checkpoint_key, attestation_map)| attestation_map.iter(checkpoint_key)) @@ -211,11 +211,11 @@ impl AttestationMap { } } -impl AttestationDataMap { +impl AttestationDataMap { pub fn iter<'a>( &'a self, checkpoint_key: &'a CheckpointKey, - ) -> impl Iterator> + 'a { + ) -> impl Iterator> + 'a { self.attestations.iter().flat_map(|(data, vec_indexed)| { vec_indexed.iter().map(|indexed| AttestationRef { checkpoint: checkpoint_key, diff --git a/beacon_node/operation_pool/src/attester_slashing.rs b/beacon_node/operation_pool/src/attester_slashing.rs index f5916384d4..725d4d2a85 100644 --- a/beacon_node/operation_pool/src/attester_slashing.rs +++ b/beacon_node/operation_pool/src/attester_slashing.rs @@ -4,16 +4,16 @@ use std::collections::{HashMap, HashSet}; use types::{AttesterSlashing, BeaconState, EthSpec}; #[derive(Debug, Clone)] -pub struct AttesterSlashingMaxCover<'a, T: EthSpec> { - slashing: &'a AttesterSlashing, +pub struct AttesterSlashingMaxCover<'a, E: EthSpec> { + slashing: &'a AttesterSlashing, effective_balances: HashMap, } -impl<'a, T: EthSpec> AttesterSlashingMaxCover<'a, T> { +impl<'a, E: EthSpec> AttesterSlashingMaxCover<'a, E> { pub fn new( - slashing: &'a AttesterSlashing, + slashing: &'a AttesterSlashing, proposer_slashing_indices: &HashSet, - state: &BeaconState, + state: &BeaconState, ) -> Option { let mut effective_balances: HashMap = HashMap::new(); let epoch = state.current_epoch(); @@ -36,18 +36,18 @@ impl<'a, T: EthSpec> AttesterSlashingMaxCover<'a, T> { } } -impl<'a, T: EthSpec> MaxCover for AttesterSlashingMaxCover<'a, T> { +impl<'a, E: EthSpec> MaxCover for AttesterSlashingMaxCover<'a, E> { /// The result type, of which we would eventually like a collection of maximal quality. - type Object = AttesterSlashing; - type Intermediate = AttesterSlashing; + type Object = AttesterSlashing; + type Intermediate = AttesterSlashing; /// The type used to represent sets. type Set = HashMap; - fn intermediate(&self) -> &AttesterSlashing { + fn intermediate(&self) -> &AttesterSlashing { self.slashing } - fn convert_to_object(slashing: &AttesterSlashing) -> AttesterSlashing { + fn convert_to_object(slashing: &AttesterSlashing) -> AttesterSlashing { slashing.clone() } @@ -58,7 +58,7 @@ impl<'a, T: EthSpec> MaxCover for AttesterSlashingMaxCover<'a, T> { /// Update the set of items covered, for the inclusion of some object in the solution. fn update_covering_set( &mut self, - _best_slashing: &AttesterSlashing, + _best_slashing: &AttesterSlashing, covered_validator_indices: &HashMap, ) { self.effective_balances diff --git a/beacon_node/operation_pool/src/bls_to_execution_changes.rs b/beacon_node/operation_pool/src/bls_to_execution_changes.rs index c73666e145..07fd72f02c 100644 --- a/beacon_node/operation_pool/src/bls_to_execution_changes.rs +++ b/beacon_node/operation_pool/src/bls_to_execution_changes.rs @@ -20,17 +20,17 @@ pub enum ReceivedPreCapella { /// Using the LIFO queue for block production disincentivises spam on P2P at the Capella fork, /// and is less-relevant after that. #[derive(Debug, Default)] -pub struct BlsToExecutionChanges { +pub struct BlsToExecutionChanges { /// Map from validator index to BLS to execution change. - by_validator_index: HashMap>>, + by_validator_index: HashMap>>, /// Last-in-first-out (LIFO) queue of verified messages. - queue: Vec>>, + queue: Vec>>, /// Contains a set of validator indices which need to have their changes /// broadcast at the capella epoch. received_pre_capella_indices: HashSet, } -impl BlsToExecutionChanges { +impl BlsToExecutionChanges { pub fn existing_change_equals( &self, address_change: &SignedBlsToExecutionChange, @@ -42,7 +42,7 @@ impl BlsToExecutionChanges { pub fn insert( &mut self, - verified_change: SigVerifiedOp, + verified_change: SigVerifiedOp, received_pre_capella: ReceivedPreCapella, ) -> bool { let validator_index = verified_change.as_inner().message.validator_index; @@ -64,14 +64,14 @@ impl BlsToExecutionChanges { /// FIFO ordering, used for persistence to disk. pub fn iter_fifo( &self, - ) -> impl Iterator>> { + ) -> impl Iterator>> { self.queue.iter() } /// LIFO ordering, used for block packing. pub fn iter_lifo( &self, - ) -> impl Iterator>> { + ) -> impl Iterator>> { self.queue.iter().rev() } @@ -80,7 +80,7 @@ impl BlsToExecutionChanges { /// the caller. pub fn iter_received_pre_capella( &self, - ) -> impl Iterator>> { + ) -> impl Iterator>> { self.queue.iter().filter(|address_change| { self.received_pre_capella_indices .contains(&address_change.as_inner().message.validator_index) @@ -99,10 +99,10 @@ impl BlsToExecutionChanges { /// address changes during re-orgs. This is isn't *perfect* so some address changes could /// still get stuck if there are gnarly re-orgs and the changes can't be widely republished /// due to the gossip duplicate rules. - pub fn prune>( + pub fn prune>( &mut self, - head_block: &SignedBeaconBlock, - head_state: &BeaconState, + head_block: &SignedBeaconBlock, + head_state: &BeaconState, spec: &ChainSpec, ) { let mut validator_indices_pruned = vec![]; diff --git a/beacon_node/operation_pool/src/lib.rs b/beacon_node/operation_pool/src/lib.rs index 7e1ddb1fd2..7cd40a4bb6 100644 --- a/beacon_node/operation_pool/src/lib.rs +++ b/beacon_node/operation_pool/src/lib.rs @@ -18,6 +18,8 @@ pub use persistence::{ PersistedOperationPoolV15, PersistedOperationPoolV5, }; pub use reward_cache::RewardCache; +use state_processing::epoch_cache::is_epoch_cache_initialized; +use types::EpochCacheError; use crate::attestation_storage::{AttestationMap, CheckpointKey}; use crate::bls_to_execution_changes::BlsToExecutionChanges; @@ -42,25 +44,25 @@ use types::{ SignedVoluntaryExit, Slot, SyncAggregate, SyncCommitteeContribution, Validator, }; -type SyncContributions = RwLock>>>; +type SyncContributions = RwLock>>>; #[derive(Default, Debug)] -pub struct OperationPool { +pub struct OperationPool { /// Map from attestation ID (see below) to vectors of attestations. - attestations: RwLock>, + attestations: RwLock>, /// Map from sync aggregate ID to the best `SyncCommitteeContribution`s seen for that ID. - sync_contributions: SyncContributions, + sync_contributions: SyncContributions, /// Set of attester slashings, and the fork version they were verified against. - attester_slashings: RwLock, T>>>, + attester_slashings: RwLock, E>>>, /// Map from proposer index to slashing. - proposer_slashings: RwLock>>, + proposer_slashings: RwLock>>, /// Map from exiting validator to their exit data. - voluntary_exits: RwLock>>, + voluntary_exits: RwLock>>, /// Map from credential changing validator to their position in the queue. - bls_to_execution_changes: RwLock>, + bls_to_execution_changes: RwLock>, /// Reward cache for accelerating attestation packing. reward_cache: RwLock, - _phantom: PhantomData, + _phantom: PhantomData, } #[derive(Debug, PartialEq)] @@ -75,6 +77,8 @@ pub enum OpPoolError { RewardCacheValidatorUnknown(BeaconStateError), RewardCacheOutOfBounds, IncorrectOpPoolVariant, + EpochCacheNotInitialized, + EpochCacheError(EpochCacheError), } #[derive(Default)] @@ -93,7 +97,7 @@ impl From for OpPoolError { } } -impl OperationPool { +impl OperationPool { /// Create a new operation pool. pub fn new() -> Self { Self::default() @@ -107,7 +111,7 @@ impl OperationPool { /// This function assumes the given `contribution` is valid. pub fn insert_sync_contribution( &self, - contribution: SyncCommitteeContribution, + contribution: SyncCommitteeContribution, ) -> Result<(), OpPoolError> { let aggregate_id = SyncAggregateId::new(contribution.slot, contribution.beacon_block_root); let mut contributions = self.sync_contributions.write(); @@ -153,8 +157,8 @@ impl OperationPool { /// contributions exist at this slot, or else `None`. pub fn get_sync_aggregate( &self, - state: &BeaconState, - ) -> Result>, OpPoolError> { + state: &BeaconState, + ) -> Result>, OpPoolError> { // Sync aggregates are formed from the contributions from the previous slot. let slot = state.slot().saturating_sub(1u64); let block_root = *state @@ -197,7 +201,7 @@ impl OperationPool { /// This function assumes the given `attestation` is valid. pub fn insert_attestation( &self, - attestation: Attestation, + attestation: Attestation, attesting_indices: Vec, ) -> Result<(), AttestationValidationError> { self.attestations @@ -220,18 +224,18 @@ impl OperationPool { fn get_valid_attestations_for_epoch<'a>( &'a self, checkpoint_key: &'a CheckpointKey, - all_attestations: &'a AttestationMap, - state: &'a BeaconState, + all_attestations: &'a AttestationMap, + state: &'a BeaconState, reward_cache: &'a RewardCache, total_active_balance: u64, - validity_filter: impl FnMut(&AttestationRef<'a, T>) -> bool + Send, + validity_filter: impl FnMut(&AttestationRef<'a, E>) -> bool + Send, spec: &'a ChainSpec, - ) -> impl Iterator> + Send { + ) -> impl Iterator> + Send { all_attestations .get_attestations(checkpoint_key) .filter(|att| { att.data.slot + spec.min_attestation_inclusion_delay <= state.slot() - && state.slot() <= att.data.slot + T::slots_per_epoch() + && state.slot() <= att.data.slot + E::slots_per_epoch() }) .filter(validity_filter) .filter_map(move |att| { @@ -247,11 +251,20 @@ impl OperationPool { /// in the operation pool. pub fn get_attestations( &self, - state: &BeaconState, - prev_epoch_validity_filter: impl for<'a> FnMut(&AttestationRef<'a, T>) -> bool + Send, - curr_epoch_validity_filter: impl for<'a> FnMut(&AttestationRef<'a, T>) -> bool + Send, + state: &BeaconState, + prev_epoch_validity_filter: impl for<'a> FnMut(&AttestationRef<'a, E>) -> bool + Send, + curr_epoch_validity_filter: impl for<'a> FnMut(&AttestationRef<'a, E>) -> bool + Send, spec: &ChainSpec, - ) -> Result>, OpPoolError> { + ) -> Result>, OpPoolError> { + if !matches!(state, BeaconState::Base(_)) { + // Epoch cache must be initialized to fetch base reward values in the max cover `score` + // function. Currently max cover ignores items on errors. If epoch cache is not + // initialized, this function returns an error. + if !is_epoch_cache_initialized(state).map_err(OpPoolError::EpochCacheError)? { + return Err(OpPoolError::EpochCacheNotInitialized); + } + } + // Attestations for the current fork, which may be from the current or previous epoch. let (prev_epoch_key, curr_epoch_key) = CheckpointKey::keys_for_state(state); let all_attestations = self.attestations.read(); @@ -296,12 +309,12 @@ impl OperationPool { let prev_epoch_limit = if let BeaconState::Base(base_state) = state { std::cmp::min( - T::MaxPendingAttestations::to_usize() + E::MaxPendingAttestations::to_usize() .saturating_sub(base_state.previous_epoch_attestations.len()), - T::MaxAttestations::to_usize(), + E::MaxAttestations::to_usize(), ) } else { - T::MaxAttestations::to_usize() + E::MaxAttestations::to_usize() }; let (prev_cover, curr_cover) = rayon::join( @@ -318,7 +331,7 @@ impl OperationPool { let _timer = metrics::start_timer(&metrics::ATTESTATION_CURR_EPOCH_PACKING_TIME); maximum_cover( curr_epoch_att, - T::MaxAttestations::to_usize(), + E::MaxAttestations::to_usize(), "curr_epoch_attestations", ) }, @@ -330,7 +343,7 @@ impl OperationPool { Ok(max_cover::merge_solutions( curr_cover, prev_cover, - T::MaxAttestations::to_usize(), + E::MaxAttestations::to_usize(), )) } @@ -342,7 +355,7 @@ impl OperationPool { /// Insert a proposer slashing into the pool. pub fn insert_proposer_slashing( &self, - verified_proposer_slashing: SigVerifiedOp, + verified_proposer_slashing: SigVerifiedOp, ) { self.proposer_slashings.write().insert( verified_proposer_slashing.as_inner().proposer_index(), @@ -353,7 +366,7 @@ impl OperationPool { /// Insert an attester slashing into the pool. pub fn insert_attester_slashing( &self, - verified_slashing: SigVerifiedOp, T>, + verified_slashing: SigVerifiedOp, E>, ) { self.attester_slashings.write().insert(verified_slashing); } @@ -365,11 +378,11 @@ impl OperationPool { /// earlier in the block. pub fn get_slashings_and_exits( &self, - state: &BeaconState, + state: &BeaconState, spec: &ChainSpec, ) -> ( Vec, - Vec>, + Vec>, Vec, ) { let proposer_slashings = filter_limit_operations( @@ -382,7 +395,7 @@ impl OperationPool { .map_or(false, |validator| !validator.slashed) }, |slashing| slashing.as_inner().clone(), - T::MaxProposerSlashings::to_usize(), + E::MaxProposerSlashings::to_usize(), ); // Set of validators to be slashed, so we don't attempt to construct invalid attester @@ -408,9 +421,9 @@ impl OperationPool { /// This function *must* remain private. fn get_attester_slashings( &self, - state: &BeaconState, + state: &BeaconState, to_be_slashed: &mut HashSet, - ) -> Vec> { + ) -> Vec> { let reader = self.attester_slashings.read(); let relevant_attester_slashings = reader.iter().flat_map(|slashing| { @@ -423,7 +436,7 @@ impl OperationPool { maximum_cover( relevant_attester_slashings, - T::MaxAttesterSlashings::to_usize(), + E::MaxAttesterSlashings::to_usize(), "attester_slashings", ) .into_iter() @@ -435,7 +448,7 @@ impl OperationPool { } /// Prune proposer slashings for validators which are exited in the finalized epoch. - pub fn prune_proposer_slashings(&self, head_state: &BeaconState) { + pub fn prune_proposer_slashings(&self, head_state: &BeaconState) { prune_validator_hash_map( &mut self.proposer_slashings.write(), |_, validator| validator.exit_epoch <= head_state.finalized_checkpoint().epoch, @@ -445,7 +458,7 @@ impl OperationPool { /// Prune attester slashings for all slashed or withdrawn validators, or attestations on another /// fork. - pub fn prune_attester_slashings(&self, head_state: &BeaconState) { + pub fn prune_attester_slashings(&self, head_state: &BeaconState) { self.attester_slashings.write().retain(|slashing| { // Check that the attestation's signature is still valid wrt the fork version. let signature_ok = slashing.signature_is_still_valid(&head_state.fork()); @@ -476,7 +489,7 @@ impl OperationPool { } /// Insert a voluntary exit that has previously been checked elsewhere. - pub fn insert_voluntary_exit(&self, exit: SigVerifiedOp) { + pub fn insert_voluntary_exit(&self, exit: SigVerifiedOp) { self.voluntary_exits .write() .insert(exit.as_inner().message.validator_index, exit); @@ -485,7 +498,7 @@ impl OperationPool { /// Get a list of voluntary exits for inclusion in a block. fn get_voluntary_exits( &self, - state: &BeaconState, + state: &BeaconState, filter: F, spec: &ChainSpec, ) -> Vec @@ -501,12 +514,12 @@ impl OperationPool { .is_ok() }, |exit| exit.as_inner().clone(), - T::MaxVoluntaryExits::to_usize(), + E::MaxVoluntaryExits::to_usize(), ) } /// Prune if validator has already exited at or before the finalized checkpoint of the head. - pub fn prune_voluntary_exits(&self, head_state: &BeaconState) { + pub fn prune_voluntary_exits(&self, head_state: &BeaconState) { prune_validator_hash_map( &mut self.voluntary_exits.write(), // This condition is slightly too loose, since there will be some finalized exits that @@ -536,7 +549,7 @@ impl OperationPool { /// Return `true` if the change was inserted. pub fn insert_bls_to_execution_change( &self, - verified_change: SigVerifiedOp, + verified_change: SigVerifiedOp, received_pre_capella: ReceivedPreCapella, ) -> bool { self.bls_to_execution_changes @@ -549,7 +562,7 @@ impl OperationPool { /// They're in random `HashMap` order, which isn't exactly fair, but isn't unfair either. pub fn get_bls_to_execution_changes( &self, - state: &BeaconState, + state: &BeaconState, spec: &ChainSpec, ) -> Vec { filter_limit_operations( @@ -563,7 +576,7 @@ impl OperationPool { }) }, |address_change| address_change.as_inner().clone(), - T::MaxBlsToExecutionChanges::to_usize(), + E::MaxBlsToExecutionChanges::to_usize(), ) } @@ -573,7 +586,7 @@ impl OperationPool { /// broadcast of messages. pub fn get_bls_to_execution_changes_received_pre_capella( &self, - state: &BeaconState, + state: &BeaconState, spec: &ChainSpec, ) -> Vec { let mut changes = filter_limit_operations( @@ -604,10 +617,10 @@ impl OperationPool { } /// Prune BLS to execution changes that have been applied to the state more than 1 block ago. - pub fn prune_bls_to_execution_changes>( + pub fn prune_bls_to_execution_changes>( &self, - head_block: &SignedBeaconBlock, - head_state: &BeaconState, + head_block: &SignedBeaconBlock, + head_state: &BeaconState, spec: &ChainSpec, ) { self.bls_to_execution_changes @@ -616,10 +629,10 @@ impl OperationPool { } /// Prune all types of transactions given the latest head state and head fork. - pub fn prune_all>( + pub fn prune_all>( &self, - head_block: &SignedBeaconBlock, - head_state: &BeaconState, + head_block: &SignedBeaconBlock, + head_state: &BeaconState, current_epoch: Epoch, spec: &ChainSpec, ) { @@ -639,7 +652,7 @@ impl OperationPool { /// Returns all known `Attestation` objects. /// /// This method may return objects that are invalid for block inclusion. - pub fn get_all_attestations(&self) -> Vec> { + pub fn get_all_attestations(&self) -> Vec> { self.attestations .read() .iter() @@ -650,7 +663,7 @@ impl OperationPool { /// Returns all known `Attestation` objects that pass the provided filter. /// /// This method may return objects that are invalid for block inclusion. - pub fn get_filtered_attestations(&self, filter: F) -> Vec> + pub fn get_filtered_attestations(&self, filter: F) -> Vec> where F: Fn(&AttestationData) -> bool, { @@ -665,7 +678,7 @@ impl OperationPool { /// Returns all known `AttesterSlashing` objects. /// /// This method may return objects that are invalid for block inclusion. - pub fn get_all_attester_slashings(&self) -> Vec> { + pub fn get_all_attester_slashings(&self) -> Vec> { self.attester_slashings .read() .iter() @@ -708,7 +721,7 @@ impl OperationPool { } /// Filter up to a maximum number of operations out of an iterator. -fn filter_limit_operations<'a, T: 'a, V: 'a, I, F, G>( +fn filter_limit_operations<'a, T, V: 'a, I, F, G>( operations: I, filter: F, mapping: G, @@ -718,7 +731,7 @@ where I: IntoIterator, F: Fn(&T) -> bool, G: Fn(&T) -> V, - T: Clone, + T: Clone + 'a, { operations .into_iter() @@ -751,7 +764,7 @@ fn prune_validator_hash_map( } /// Compare two operation pools. -impl PartialEq for OperationPool { +impl PartialEq for OperationPool { fn eq(&self, other: &Self) -> bool { if ptr::eq(self, other) { return true; @@ -773,6 +786,7 @@ mod release_tests { }; use lazy_static::lazy_static; use maplit::hashset; + use state_processing::epoch_cache::initialize_epoch_cache; use state_processing::{common::get_attesting_indices_from_state, VerifyOperation}; use std::collections::BTreeSet; use types::consts::altair::SYNC_COMMITTEE_SUBNET_COUNT; @@ -814,6 +828,15 @@ mod release_tests { (harness, spec) } + fn get_current_state_initialize_epoch_cache( + harness: &BeaconChainHarness>, + spec: &ChainSpec, + ) -> BeaconState { + let mut state = harness.get_current_state(); + initialize_epoch_cache(&mut state, spec).unwrap(); + state + } + /// Test state for sync contribution-related tests. async fn sync_contribution_test_state( num_committees: usize, @@ -847,7 +870,7 @@ mod release_tests { return; } - let mut state = harness.get_current_state(); + let mut state = get_current_state_initialize_epoch_cache(&harness, &spec); let slot = state.slot(); let committees = state .get_beacon_committees_at_slot(slot) @@ -929,7 +952,7 @@ mod release_tests { let (harness, ref spec) = attestation_test_state::(1); let op_pool = OperationPool::::new(); - let mut state = harness.get_current_state(); + let mut state = get_current_state_initialize_epoch_cache(&harness, &spec); let slot = state.slot(); let committees = state @@ -1004,7 +1027,7 @@ mod release_tests { fn attestation_duplicate() { let (harness, ref spec) = attestation_test_state::(1); - let state = harness.get_current_state(); + let state = get_current_state_initialize_epoch_cache(&harness, &spec); let op_pool = OperationPool::::new(); @@ -1044,7 +1067,7 @@ mod release_tests { fn attestation_pairwise_overlapping() { let (harness, ref spec) = attestation_test_state::(1); - let state = harness.get_current_state(); + let state = get_current_state_initialize_epoch_cache(&harness, &spec); let op_pool = OperationPool::::new(); @@ -1142,7 +1165,7 @@ mod release_tests { let (harness, ref spec) = attestation_test_state::(num_committees); - let mut state = harness.get_current_state(); + let mut state = get_current_state_initialize_epoch_cache(&harness, &spec); let op_pool = OperationPool::::new(); @@ -1232,7 +1255,7 @@ mod release_tests { let (harness, ref spec) = attestation_test_state::(num_committees); - let mut state = harness.get_current_state(); + let mut state = get_current_state_initialize_epoch_cache(&harness, &spec); let op_pool = OperationPool::::new(); let slot = state.slot(); diff --git a/beacon_node/operation_pool/src/max_cover.rs b/beacon_node/operation_pool/src/max_cover.rs index 2e629f786b..b4a95b1de0 100644 --- a/beacon_node/operation_pool/src/max_cover.rs +++ b/beacon_node/operation_pool/src/max_cover.rs @@ -118,7 +118,6 @@ where #[cfg(test)] mod test { use super::*; - use std::iter::FromIterator; use std::{collections::HashSet, hash::Hash}; impl MaxCover for HashSet diff --git a/beacon_node/operation_pool/src/persistence.rs b/beacon_node/operation_pool/src/persistence.rs index 35d2b4ce7e..ef749a220d 100644 --- a/beacon_node/operation_pool/src/persistence.rs +++ b/beacon_node/operation_pool/src/persistence.rs @@ -14,7 +14,7 @@ use std::mem; use store::{DBColumn, Error as StoreError, StoreItem}; use types::*; -type PersistedSyncContributions = Vec<(SyncAggregateId, Vec>)>; +type PersistedSyncContributions = Vec<(SyncAggregateId, Vec>)>; /// SSZ-serializable version of `OperationPool`. /// @@ -30,45 +30,45 @@ type PersistedSyncContributions = Vec<(SyncAggregateId, Vec { +pub struct PersistedOperationPool { /// [DEPRECATED] Mapping from attestation ID to attestation mappings. #[superstruct(only(V5))] - pub attestations_v5: Vec<(AttestationId, Vec>)>, + pub attestations_v5: Vec<(AttestationId, Vec>)>, /// Attestations and their attesting indices. #[superstruct(only(V12, V14, V15))] - pub attestations: Vec<(Attestation, Vec)>, + pub attestations: Vec<(Attestation, Vec)>, /// Mapping from sync contribution ID to sync contributions and aggregate. - pub sync_contributions: PersistedSyncContributions, + pub sync_contributions: PersistedSyncContributions, /// [DEPRECATED] Attester slashings. #[superstruct(only(V5))] - pub attester_slashings_v5: Vec<(AttesterSlashing, ForkVersion)>, + pub attester_slashings_v5: Vec<(AttesterSlashing, ForkVersion)>, /// Attester slashings. #[superstruct(only(V12, V14, V15))] - pub attester_slashings: Vec, T>>, + pub attester_slashings: Vec, E>>, /// [DEPRECATED] Proposer slashings. #[superstruct(only(V5))] pub proposer_slashings_v5: Vec, /// Proposer slashings with fork information. #[superstruct(only(V12, V14, V15))] - pub proposer_slashings: Vec>, + pub proposer_slashings: Vec>, /// [DEPRECATED] Voluntary exits. #[superstruct(only(V5))] pub voluntary_exits_v5: Vec, /// Voluntary exits with fork information. #[superstruct(only(V12, V14, V15))] - pub voluntary_exits: Vec>, + pub voluntary_exits: Vec>, /// BLS to Execution Changes #[superstruct(only(V14, V15))] - pub bls_to_execution_changes: Vec>, + pub bls_to_execution_changes: Vec>, /// Validator indices with BLS to Execution Changes to be broadcast at the /// Capella fork. #[superstruct(only(V15))] pub capella_bls_change_broadcast_indices: Vec, } -impl PersistedOperationPool { +impl PersistedOperationPool { /// Convert an `OperationPool` into serializable form. - pub fn from_operation_pool(operation_pool: &OperationPool) -> Self { + pub fn from_operation_pool(operation_pool: &OperationPool) -> Self { let attestations = operation_pool .attestations .read() @@ -135,7 +135,7 @@ impl PersistedOperationPool { } /// Reconstruct an `OperationPool`. - pub fn into_operation_pool(mut self) -> Result, OpPoolError> { + pub fn into_operation_pool(mut self) -> Result, OpPoolError> { let attester_slashings = RwLock::new(self.attester_slashings()?.iter().cloned().collect()); let proposer_slashings = RwLock::new( self.proposer_slashings()? @@ -200,7 +200,7 @@ impl PersistedOperationPool { } } -impl StoreItem for PersistedOperationPoolV5 { +impl StoreItem for PersistedOperationPoolV5 { fn db_column() -> DBColumn { DBColumn::OpPool } @@ -214,7 +214,7 @@ impl StoreItem for PersistedOperationPoolV5 { } } -impl StoreItem for PersistedOperationPoolV12 { +impl StoreItem for PersistedOperationPoolV12 { fn db_column() -> DBColumn { DBColumn::OpPool } @@ -228,7 +228,7 @@ impl StoreItem for PersistedOperationPoolV12 { } } -impl StoreItem for PersistedOperationPoolV14 { +impl StoreItem for PersistedOperationPoolV14 { fn db_column() -> DBColumn { DBColumn::OpPool } @@ -242,7 +242,7 @@ impl StoreItem for PersistedOperationPoolV14 { } } -impl StoreItem for PersistedOperationPoolV15 { +impl StoreItem for PersistedOperationPoolV15 { fn db_column() -> DBColumn { DBColumn::OpPool } @@ -257,7 +257,7 @@ impl StoreItem for PersistedOperationPoolV15 { } /// Deserialization for `PersistedOperationPool` defaults to `PersistedOperationPool::V12`. -impl StoreItem for PersistedOperationPool { +impl StoreItem for PersistedOperationPool { fn db_column() -> DBColumn { DBColumn::OpPool } diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 002bb344a3..818cdbd460 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -1,6 +1,5 @@ use clap::{App, Arg, ArgGroup}; use strum::VariantNames; -use types::ProgressiveBalancesMode; pub fn cli_app<'a, 'b>() -> App<'a, 'b> { App::new("beacon_node") @@ -393,9 +392,9 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .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.") + .help("This flag is deprecated and has no effect.") .takes_value(true) + .hidden(true) ) .arg( Arg::with_name("http-enable-tls") @@ -426,9 +425,8 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { 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.") + .help("This flag is deprecated and has no effect.") + .hidden(true) ) .arg( Arg::with_name("http-sse-capacity-multiplier") @@ -622,6 +620,13 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .help("Specifies how many states from the freezer database should cache in memory [default: 1]") .takes_value(true) ) + .arg( + Arg::with_name("state-cache-size") + .long("state-cache-size") + .value_name("STATE_CACHE_SIZE") + .help("Specifies the size of the snapshot cache [default: 3]") + .takes_value(true) + ) /* * Execution Layer Integration */ @@ -939,6 +944,15 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .takes_value(true) .requires("checkpoint-state") ) + .arg( + Arg::with_name("checkpoint-blobs") + .long("checkpoint-blobs") + .help("Set the checkpoint blobs to start syncing from. Must be aligned and match \ + --checkpoint-block. Using --checkpoint-sync-url instead is recommended.") + .value_name("BLOBS_SSZ") + .takes_value(true) + .requires("checkpoint-block") + ) .arg( Arg::with_name("checkpoint-sync-url") .long("checkpoint-sync-url") @@ -1025,10 +1039,18 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { Arg::with_name("proposer-reorg-threshold") .long("proposer-reorg-threshold") .value_name("PERCENT") - .help("Percentage of vote weight below which to attempt a proposer reorg. \ + .help("Percentage of head vote weight below which to attempt a proposer reorg. \ Default: 20%") .conflicts_with("disable-proposer-reorgs") ) + .arg( + Arg::with_name("proposer-reorg-parent-threshold") + .long("proposer-reorg-parent-threshold") + .value_name("PERCENT") + .help("Percentage of parent vote weight above which to attempt a proposer reorg. \ + Default: 160%") + .conflicts_with("disable-proposer-reorgs") + ) .arg( Arg::with_name("proposer-reorg-epochs-since-finalization") .long("proposer-reorg-epochs-since-finalization") @@ -1208,14 +1230,9 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { Arg::with_name("progressive-balances") .long("progressive-balances") .value_name("MODE") - .help("Control the progressive balances cache mode. The default `fast` mode uses \ - the cache to speed up fork choice. A more conservative `checked` mode \ - compares the cache's results against results without the cache. If \ - there is a mismatch, it falls back to the cache-free result. Using the \ - default `fast` mode is recommended unless advised otherwise by the \ - Lighthouse team.") + .help("Deprecated. This optimisation is now the default and cannot be disabled.") .takes_value(true) - .possible_values(ProgressiveBalancesMode::VARIANTS) + .possible_values(&["fast", "disabled", "checked", "strict"]) ) .arg( Arg::with_name("beacon-processor-max-workers") @@ -1225,6 +1242,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { 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.") + .hidden(true) .takes_value(true) ) .arg( @@ -1235,6 +1253,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { Higher values may prevent messages from being dropped while lower values \ may help protect the node from becoming overwhelmed.") .default_value("16384") + .hidden(true) .takes_value(true) ) .arg( @@ -1244,6 +1263,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .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.") + .hidden(true) .default_value("12288") .takes_value(true) ) @@ -1254,6 +1274,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .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.") + .hidden(true) .default_value("64") .takes_value(true) ) @@ -1265,17 +1286,14 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { 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.") + .hidden(true) .default_value("64") .takes_value(true) ) .arg( Arg::with_name("disable-duplicate-warn-logs") .long("disable-duplicate-warn-logs") - .help("Disable warning logs for duplicate gossip messages. The WARN level log is \ - useful for detecting a duplicate validator key running elsewhere. However, this may \ - result in excessive warning logs if the validator is broadcasting messages to \ - multiple beacon nodes via the validator client --broadcast flag. In this case, \ - disabling these warn logs may be useful.") + .help("This flag is deprecated and has no effect.") .takes_value(false) ) .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 fff384e195..5a27b148c9 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -1,6 +1,7 @@ use beacon_chain::chain_config::{ DisallowedReOrgOffsets, ReOrgThreshold, DEFAULT_PREPARE_PAYLOAD_LOOKAHEAD_FACTOR, - DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, DEFAULT_RE_ORG_THRESHOLD, + DEFAULT_RE_ORG_HEAD_THRESHOLD, DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, + DEFAULT_RE_ORG_PARENT_THRESHOLD, }; use beacon_chain::TrustedSetup; use clap::ArgMatches; @@ -127,8 +128,12 @@ pub fn get_config( client_config.http_api.allow_origin = Some(allow_origin.to_string()); } - 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-spec-fork") { + warn!( + log, + "Ignoring --http-spec-fork"; + "info" => "this flag is deprecated and will be removed" + ); } if cli_args.is_present("http-enable-tls") { @@ -147,7 +152,11 @@ pub fn get_config( } if cli_args.is_present("http-allow-sync-stalled") { - client_config.http_api.allow_sync_stalled = true; + warn!( + log, + "Ignoring --http-allow-sync-stalled"; + "info" => "this flag is deprecated and will be removed" + ); } client_config.http_api.sse_capacity_multiplier = @@ -170,6 +179,9 @@ pub fn get_config( if let Some(cache_size) = clap_utils::parse_optional(cli_args, "shuffling-cache-size")? { client_config.chain.shuffling_cache_size = cache_size; } + if let Some(cache_size) = clap_utils::parse_optional(cli_args, "state-cache-size")? { + client_config.chain.snapshot_cache_size = cache_size; + } /* * Prometheus metrics HTTP server @@ -342,7 +354,9 @@ pub fn get_config( clap_utils::parse_optional(cli_args, "suggested-fee-recipient")?; el_config.jwt_id = clap_utils::parse_optional(cli_args, "execution-jwt-id")?; el_config.jwt_version = clap_utils::parse_optional(cli_args, "execution-jwt-version")?; - el_config.default_datadir = client_config.data_dir().clone(); + el_config + .default_datadir + .clone_from(client_config.data_dir()); let execution_timeout_multiplier = clap_utils::parse_required(cli_args, "execution-timeout-multiplier")?; el_config.execution_timeout_multiplier = Some(execution_timeout_multiplier); @@ -512,9 +526,10 @@ pub fn get_config( 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)) = ( + if let (Some(initial_state_path), Some(initial_block_path), opt_initial_blobs_path) = ( cli_args.value_of("checkpoint-state"), cli_args.value_of("checkpoint-block"), + cli_args.value_of("checkpoint-blobs"), ) { let read = |path: &str| { use std::fs::File; @@ -530,10 +545,12 @@ pub fn get_config( let anchor_state_bytes = read(initial_state_path)?; let anchor_block_bytes = read(initial_block_path)?; + let anchor_blobs_bytes = opt_initial_blobs_path.map(read).transpose()?; ClientGenesis::WeakSubjSszBytes { anchor_state_bytes, anchor_block_bytes, + anchor_blobs_bytes, } } else if let Some(remote_bn_url) = cli_args.value_of("checkpoint-sync-url") { let url = SensitiveUrl::parse(remote_bn_url) @@ -741,12 +758,13 @@ pub fn get_config( } if cli_args.is_present("disable-proposer-reorgs") { - client_config.chain.re_org_threshold = None; + client_config.chain.re_org_head_threshold = None; + client_config.chain.re_org_parent_threshold = None; } else { - client_config.chain.re_org_threshold = Some( + client_config.chain.re_org_head_threshold = Some( clap_utils::parse_optional(cli_args, "proposer-reorg-threshold")? .map(ReOrgThreshold) - .unwrap_or(DEFAULT_RE_ORG_THRESHOLD), + .unwrap_or(DEFAULT_RE_ORG_HEAD_THRESHOLD), ); client_config.chain.re_org_max_epochs_since_finalization = clap_utils::parse_optional(cli_args, "proposer-reorg-epochs-since-finalization")? @@ -754,6 +772,12 @@ pub fn get_config( client_config.chain.re_org_cutoff_millis = clap_utils::parse_optional(cli_args, "proposer-reorg-cutoff")?; + client_config.chain.re_org_parent_threshold = Some( + clap_utils::parse_optional(cli_args, "proposer-reorg-parent-threshold")? + .map(ReOrgThreshold) + .unwrap_or(DEFAULT_RE_ORG_PARENT_THRESHOLD), + ); + if let Some(disallowed_offsets_str) = clap_utils::parse_optional::(cli_args, "proposer-reorg-disallowed-offsets")? { @@ -833,10 +857,12 @@ 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 cli_args.is_present("progressive-balances") { + warn!( + log, + "Progressive balances mode is deprecated"; + "info" => "please remove --progressive-balances" + ); } if let Some(max_workers) = clap_utils::parse_optional(cli_args, "beacon-processor-max-workers")? @@ -1122,8 +1148,6 @@ pub fn set_network_config( config.target_peers = target_peers_str .parse::() .map_err(|_| format!("Invalid number of target peers: {}", target_peers_str))?; - } else { - config.target_peers = 80; // default value } if let Some(value) = cli_args.value_of("network-load") { @@ -1425,9 +1449,6 @@ pub fn set_network_config( Some(config_str.parse()?) } }; - - config.disable_duplicate_warn_logs = cli_args.is_present("disable-duplicate-warn-logs"); - Ok(()) } @@ -1473,15 +1494,15 @@ pub fn get_slots_per_restore_point( /// Parses the `cli_value` as a comma-separated string of values to be parsed with `parser`. /// /// If there is more than one value, log a warning. If there are no values, return an error. -pub fn parse_only_one_value( +pub fn parse_only_one_value( cli_value: &str, parser: F, flag_name: &str, log: &Logger, ) -> Result where - F: Fn(&str) -> Result, - E: Debug, + F: Fn(&str) -> Result, + U: Debug, { let values = cli_value .split(',') diff --git a/beacon_node/store/src/chunked_vector.rs b/beacon_node/store/src/chunked_vector.rs index 537614f281..a0c50e5a2b 100644 --- a/beacon_node/store/src/chunked_vector.rs +++ b/beacon_node/store/src/chunked_vector.rs @@ -17,7 +17,6 @@ use self::UpdatePattern::*; use crate::*; use ssz::{Decode, Encode}; -use typenum::Unsigned; use types::historical_summary::HistoricalSummary; /// Description of how a `BeaconState` field is updated during state processing. @@ -287,9 +286,9 @@ macro_rules! field { #[derive(Clone, Copy)] pub struct $struct_name; - impl Field for $struct_name + impl Field for $struct_name where - T: EthSpec, + E: EthSpec, { type Value = $value_ty; type Length = $length_ty; @@ -304,7 +303,7 @@ macro_rules! field { } fn get_value( - state: &BeaconState, + state: &BeaconState, vindex: u64, spec: &ChainSpec, ) -> Result { @@ -325,7 +324,7 @@ field!( BlockRoots, FixedLengthField, Hash256, - T::SlotsPerHistoricalRoot, + E::SlotsPerHistoricalRoot, DBColumn::BeaconBlockRoots, |_| OncePerNSlots { n: 1, @@ -339,7 +338,7 @@ field!( StateRoots, FixedLengthField, Hash256, - T::SlotsPerHistoricalRoot, + E::SlotsPerHistoricalRoot, DBColumn::BeaconStateRoots, |_| OncePerNSlots { n: 1, @@ -353,14 +352,14 @@ field!( HistoricalRoots, VariableLengthField, Hash256, - T::HistoricalRootsLimit, + E::HistoricalRootsLimit, DBColumn::BeaconHistoricalRoots, |spec: &ChainSpec| OncePerNSlots { - n: T::SlotsPerHistoricalRoot::to_u64(), + n: E::SlotsPerHistoricalRoot::to_u64(), activation_slot: Some(Slot::new(0)), deactivation_slot: spec .capella_fork_epoch - .map(|fork_epoch| fork_epoch.start_slot(T::slots_per_epoch())), + .map(|fork_epoch| fork_epoch.start_slot(E::slots_per_epoch())), }, |state: &BeaconState<_>, index, _| safe_modulo_index(state.historical_roots(), index) ); @@ -369,7 +368,7 @@ field!( RandaoMixes, FixedLengthField, Hash256, - T::EpochsPerHistoricalVector, + E::EpochsPerHistoricalVector, DBColumn::BeaconRandaoMixes, |_| OncePerEpoch { lag: 1 }, |state: &BeaconState<_>, index, _| safe_modulo_index(state.randao_mixes(), index) @@ -379,13 +378,13 @@ field!( HistoricalSummaries, VariableLengthField, HistoricalSummary, - T::HistoricalRootsLimit, + E::HistoricalRootsLimit, DBColumn::BeaconHistoricalSummaries, |spec: &ChainSpec| OncePerNSlots { - n: T::SlotsPerHistoricalRoot::to_u64(), + n: E::SlotsPerHistoricalRoot::to_u64(), activation_slot: spec .capella_fork_epoch - .map(|fork_epoch| fork_epoch.start_slot(T::slots_per_epoch())), + .map(|fork_epoch| fork_epoch.start_slot(E::slots_per_epoch())), deactivation_slot: None, }, |state: &BeaconState<_>, index, _| safe_modulo_index( diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 63cd8e67d5..70e02164e0 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -33,13 +33,11 @@ use state_processing::{ BlockProcessingError, BlockReplayer, SlotProcessingError, StateProcessingStrategy, }; use std::cmp::min; -use std::convert::TryInto; use std::marker::PhantomData; use std::num::NonZeroUsize; use std::path::Path; use std::sync::Arc; use std::time::Duration; -use types::blob_sidecar::BlobSidecarList; use types::*; /// On-disk database that stores finalized states efficiently. @@ -1344,7 +1342,7 @@ impl, Cold: ItemStore> HotColdDB high_restore_point .get_block_root(slot) .or_else(|_| high_restore_point.get_oldest_block_root()) - .map(|x| *x) + .copied() .map_err(HotColdDBError::RestorePointBlockHashError) } @@ -2376,6 +2374,9 @@ impl, Cold: ItemStore> HotColdDB self.cold_db.do_atomically(cold_ops)?; } + // In order to reclaim space, we need to compact the freezer DB as well. + self.cold_db.compact()?; + Ok(()) } } diff --git a/beacon_node/store/src/impls/beacon_state.rs b/beacon_node/store/src/impls/beacon_state.rs index 88d1d2d7a1..d08bf56492 100644 --- a/beacon_node/store/src/impls/beacon_state.rs +++ b/beacon_node/store/src/impls/beacon_state.rs @@ -1,8 +1,6 @@ use crate::*; use ssz::{DecodeError, Encode}; use ssz_derive::Encode; -use std::convert::TryInto; -use types::beacon_state::{CloneConfig, CommitteeCache, CACHED_EPOCHS}; pub fn store_full_state( state_root: &Hash256, @@ -46,14 +44,14 @@ pub fn get_full_state, E: EthSpec>( /// A container for storing `BeaconState` components. // TODO: would be more space efficient with the caches stored separately and referenced by hash #[derive(Encode)] -pub struct StorageContainer { - state: BeaconState, +pub struct StorageContainer { + state: BeaconState, committee_caches: Vec, } -impl StorageContainer { +impl StorageContainer { /// Create a new instance for storing a `BeaconState`. - pub fn new(state: &BeaconState) -> Self { + pub fn new(state: &BeaconState) -> Self { Self { state: state.clone_with(CloneConfig::none()), committee_caches: state.committee_caches().to_vec(), @@ -80,10 +78,10 @@ impl StorageContainer { } } -impl TryInto> for StorageContainer { +impl TryInto> for StorageContainer { type Error = Error; - fn try_into(mut self) -> Result, Error> { + fn try_into(mut self) -> Result, Error> { let mut state = self.state; for i in (0..CACHED_EPOCHS).rev() { diff --git a/beacon_node/store/src/impls/execution_payload.rs b/beacon_node/store/src/impls/execution_payload.rs index 6445dad388..a874031ca2 100644 --- a/beacon_node/store/src/impls/execution_payload.rs +++ b/beacon_node/store/src/impls/execution_payload.rs @@ -2,7 +2,7 @@ use crate::{DBColumn, Error, StoreItem}; use ssz::{Decode, Encode}; use types::{ BlobSidecarList, EthSpec, ExecutionPayload, ExecutionPayloadCapella, ExecutionPayloadDeneb, - ExecutionPayloadMerge, + ExecutionPayloadElectra, ExecutionPayloadMerge, }; macro_rules! impl_store_item { @@ -25,6 +25,7 @@ macro_rules! impl_store_item { impl_store_item!(ExecutionPayloadMerge); impl_store_item!(ExecutionPayloadCapella); impl_store_item!(ExecutionPayloadDeneb); +impl_store_item!(ExecutionPayloadElectra); impl_store_item!(BlobSidecarList); /// This fork-agnostic implementation should be only used for writing. @@ -41,12 +42,18 @@ impl StoreItem for ExecutionPayload { } fn from_store_bytes(bytes: &[u8]) -> Result { - ExecutionPayloadDeneb::from_ssz_bytes(bytes) - .map(Self::Deneb) + ExecutionPayloadElectra::from_ssz_bytes(bytes) + .map(Self::Electra) .or_else(|_| { - ExecutionPayloadCapella::from_ssz_bytes(bytes) - .map(Self::Capella) - .or_else(|_| ExecutionPayloadMerge::from_ssz_bytes(bytes).map(Self::Merge)) + ExecutionPayloadDeneb::from_ssz_bytes(bytes) + .map(Self::Deneb) + .or_else(|_| { + ExecutionPayloadCapella::from_ssz_bytes(bytes) + .map(Self::Capella) + .or_else(|_| { + ExecutionPayloadMerge::from_ssz_bytes(bytes).map(Self::Merge) + }) + }) }) .map_err(Into::into) } diff --git a/beacon_node/store/src/iter.rs b/beacon_node/store/src/iter.rs index 07c99e5a4e..e459c1c357 100644 --- a/beacon_node/store/src/iter.rs +++ b/beacon_node/store/src/iter.rs @@ -49,12 +49,12 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> } } -pub struct StateRootsIterator<'a, T: EthSpec, Hot: ItemStore, Cold: ItemStore> { - inner: RootsIterator<'a, T, Hot, Cold>, +pub struct StateRootsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { + inner: RootsIterator<'a, E, Hot, Cold>, } -impl<'a, T: EthSpec, Hot: ItemStore, Cold: ItemStore> Clone - for StateRootsIterator<'a, T, Hot, Cold> +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> Clone + for StateRootsIterator<'a, E, Hot, Cold> { fn clone(&self) -> Self { Self { @@ -63,22 +63,22 @@ impl<'a, T: EthSpec, Hot: ItemStore, Cold: ItemStore> Clone } } -impl<'a, T: EthSpec, Hot: ItemStore, Cold: ItemStore> StateRootsIterator<'a, T, Hot, Cold> { - pub fn new(store: &'a HotColdDB, beacon_state: &'a BeaconState) -> Self { +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> StateRootsIterator<'a, E, Hot, Cold> { + pub fn new(store: &'a HotColdDB, beacon_state: &'a BeaconState) -> Self { Self { inner: RootsIterator::new(store, beacon_state), } } - pub fn owned(store: &'a HotColdDB, beacon_state: BeaconState) -> Self { + pub fn owned(store: &'a HotColdDB, beacon_state: BeaconState) -> Self { Self { inner: RootsIterator::owned(store, beacon_state), } } } -impl<'a, T: EthSpec, Hot: ItemStore, Cold: ItemStore> Iterator - for StateRootsIterator<'a, T, Hot, Cold> +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> Iterator + for StateRootsIterator<'a, E, Hot, Cold> { type Item = Result<(Hash256, Slot), Error>; @@ -97,12 +97,12 @@ impl<'a, T: EthSpec, Hot: ItemStore, Cold: ItemStore> Iterator /// exhausted. /// /// Returns `None` for roots prior to genesis or when there is an error reading from `Store`. -pub struct BlockRootsIterator<'a, T: EthSpec, Hot: ItemStore, Cold: ItemStore> { - inner: RootsIterator<'a, T, Hot, Cold>, +pub struct BlockRootsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { + inner: RootsIterator<'a, E, Hot, Cold>, } -impl<'a, T: EthSpec, Hot: ItemStore, Cold: ItemStore> Clone - for BlockRootsIterator<'a, T, Hot, Cold> +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> Clone + for BlockRootsIterator<'a, E, Hot, Cold> { fn clone(&self) -> Self { Self { @@ -111,23 +111,23 @@ impl<'a, T: EthSpec, Hot: ItemStore, Cold: ItemStore> Clone } } -impl<'a, T: EthSpec, Hot: ItemStore, Cold: ItemStore> BlockRootsIterator<'a, T, Hot, Cold> { +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> BlockRootsIterator<'a, E, Hot, Cold> { /// Create a new iterator over all block roots in the given `beacon_state` and prior states. - pub fn new(store: &'a HotColdDB, beacon_state: &'a BeaconState) -> Self { + pub fn new(store: &'a HotColdDB, beacon_state: &'a BeaconState) -> Self { Self { inner: RootsIterator::new(store, beacon_state), } } /// Create a new iterator over all block roots in the given `beacon_state` and prior states. - pub fn owned(store: &'a HotColdDB, beacon_state: BeaconState) -> Self { + pub fn owned(store: &'a HotColdDB, beacon_state: BeaconState) -> Self { Self { inner: RootsIterator::owned(store, beacon_state), } } pub fn from_block( - store: &'a HotColdDB, + store: &'a HotColdDB, block_hash: Hash256, ) -> Result { Ok(Self { @@ -136,8 +136,8 @@ impl<'a, T: EthSpec, Hot: ItemStore, Cold: ItemStore> BlockRootsIterator<' } } -impl<'a, T: EthSpec, Hot: ItemStore, Cold: ItemStore> Iterator - for BlockRootsIterator<'a, T, Hot, Cold> +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> Iterator + for BlockRootsIterator<'a, E, Hot, Cold> { type Item = Result<(Hash256, Slot), Error>; @@ -149,14 +149,14 @@ impl<'a, T: EthSpec, Hot: ItemStore, Cold: ItemStore> Iterator } /// Iterator over state and block roots that backtracks using the vectors from a `BeaconState`. -pub struct RootsIterator<'a, T: EthSpec, Hot: ItemStore, Cold: ItemStore> { - store: &'a HotColdDB, - beacon_state: Cow<'a, BeaconState>, +pub struct RootsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { + store: &'a HotColdDB, + beacon_state: Cow<'a, BeaconState>, slot: Slot, } -impl<'a, T: EthSpec, Hot: ItemStore, Cold: ItemStore> Clone - for RootsIterator<'a, T, Hot, Cold> +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> Clone + for RootsIterator<'a, E, Hot, Cold> { fn clone(&self) -> Self { Self { @@ -167,8 +167,8 @@ impl<'a, T: EthSpec, Hot: ItemStore, Cold: ItemStore> Clone } } -impl<'a, T: EthSpec, Hot: ItemStore, Cold: ItemStore> RootsIterator<'a, T, Hot, Cold> { - pub fn new(store: &'a HotColdDB, beacon_state: &'a BeaconState) -> Self { +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> RootsIterator<'a, E, Hot, Cold> { + pub fn new(store: &'a HotColdDB, beacon_state: &'a BeaconState) -> Self { Self { store, slot: beacon_state.slot(), @@ -176,7 +176,7 @@ impl<'a, T: EthSpec, Hot: ItemStore, Cold: ItemStore> RootsIterator<'a, T, } } - pub fn owned(store: &'a HotColdDB, beacon_state: BeaconState) -> Self { + pub fn owned(store: &'a HotColdDB, beacon_state: BeaconState) -> Self { Self { store, slot: beacon_state.slot(), @@ -185,7 +185,7 @@ impl<'a, T: EthSpec, Hot: ItemStore, Cold: ItemStore> RootsIterator<'a, T, } pub fn from_block( - store: &'a HotColdDB, + store: &'a HotColdDB, block_hash: Hash256, ) -> Result { let block = store @@ -232,8 +232,8 @@ impl<'a, T: EthSpec, Hot: ItemStore, Cold: ItemStore> RootsIterator<'a, T, } } -impl<'a, T: EthSpec, Hot: ItemStore, Cold: ItemStore> Iterator - for RootsIterator<'a, T, Hot, Cold> +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> Iterator + for RootsIterator<'a, E, Hot, Cold> { /// (block_root, state_root, slot) type Item = Result<(Hash256, Hash256, Slot), Error>; @@ -307,26 +307,26 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> Iterator #[derive(Clone)] /// Extends `BlockRootsIterator`, returning `SignedBeaconBlock` instances, instead of their roots. -pub struct BlockIterator<'a, T: EthSpec, Hot: ItemStore, Cold: ItemStore> { - roots: BlockRootsIterator<'a, T, Hot, Cold>, +pub struct BlockIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { + roots: BlockRootsIterator<'a, E, Hot, Cold>, } -impl<'a, T: EthSpec, Hot: ItemStore, Cold: ItemStore> BlockIterator<'a, T, Hot, Cold> { +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> BlockIterator<'a, E, Hot, Cold> { /// Create a new iterator over all blocks in the given `beacon_state` and prior states. - pub fn new(store: &'a HotColdDB, beacon_state: &'a BeaconState) -> Self { + pub fn new(store: &'a HotColdDB, beacon_state: &'a BeaconState) -> Self { Self { roots: BlockRootsIterator::new(store, beacon_state), } } /// Create a new iterator over all blocks in the given `beacon_state` and prior states. - pub fn owned(store: &'a HotColdDB, beacon_state: BeaconState) -> Self { + pub fn owned(store: &'a HotColdDB, beacon_state: BeaconState) -> Self { Self { roots: BlockRootsIterator::owned(store, beacon_state), } } - fn do_next(&mut self) -> Result>>, Error> { + fn do_next(&mut self) -> Result>>, Error> { if let Some(result) = self.roots.next() { let (root, _slot) = result?; self.roots.inner.store.get_blinded_block(&root) @@ -336,10 +336,10 @@ impl<'a, T: EthSpec, Hot: ItemStore, Cold: ItemStore> BlockIterator<'a, T, } } -impl<'a, T: EthSpec, Hot: ItemStore, Cold: ItemStore> Iterator - for BlockIterator<'a, T, Hot, Cold> +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> Iterator + for BlockIterator<'a, E, Hot, Cold> { - type Item = Result>, Error>; + type Item = Result>, Error>; fn next(&mut self) -> Option { self.do_next().transpose() @@ -381,14 +381,13 @@ fn slot_of_prev_restore_point(current_slot: Slot) -> Slot { #[cfg(test)] mod test { use super::*; - use crate::HotColdDB; use crate::StoreConfig as Config; use beacon_chain::test_utils::BeaconChainHarness; use beacon_chain::types::{ChainSpec, MainnetEthSpec}; use sloggers::{null::NullLoggerBuilder, Build}; - fn get_state() -> BeaconState { - let harness = BeaconChainHarness::builder(T::default()) + fn get_state() -> BeaconState { + let harness = BeaconChainHarness::builder(E::default()) .default_spec() .deterministic_keypairs(1) .fresh_ephemeral_store() diff --git a/beacon_node/store/src/leveldb_store.rs b/beacon_node/store/src/leveldb_store.rs index 62619dd2c7..ffd55c16a0 100644 --- a/beacon_node/store/src/leveldb_store.rs +++ b/beacon_node/store/src/leveldb_store.rs @@ -1,6 +1,5 @@ use super::*; use crate::hot_cold_store::HotColdDBError; -use crate::metrics; use leveldb::compaction::Compaction; use leveldb::database::batch::{Batch, Writebatch}; use leveldb::database::kv::KV; @@ -8,7 +7,7 @@ use leveldb::database::Database; use leveldb::error::Error as LevelDBError; use leveldb::iterator::{Iterable, KeyIterator, LevelDBIterator}; use leveldb::options::{Options, ReadOptions, WriteOptions}; -use parking_lot::{Mutex, MutexGuard}; +use parking_lot::Mutex; use std::marker::PhantomData; use std::path::Path; @@ -154,25 +153,15 @@ impl KeyValueStore for LevelDB { self.transaction_mutex.lock() } - /// Compact all values in the states and states flag columns. - fn compact(&self) -> Result<(), Error> { - let endpoints = |column: DBColumn| { - ( - BytesKey::from_vec(get_key_for_col(column.as_str(), Hash256::zero().as_bytes())), - BytesKey::from_vec(get_key_for_col( - column.as_str(), - Hash256::repeat_byte(0xff).as_bytes(), - )), - ) - }; - - for (start_key, end_key) in [ - endpoints(DBColumn::BeaconStateTemporary), - endpoints(DBColumn::BeaconState), - endpoints(DBColumn::BeaconStateSummary), - ] { - self.db.compact(&start_key, &end_key); - } + fn compact_column(&self, column: DBColumn) -> Result<(), Error> { + // Use key-size-agnostic keys [] and 0xff..ff with a minimum of 32 bytes to account for + // columns that may change size between sub-databases or schema versions. + let start_key = BytesKey::from_vec(get_key_for_col(column.as_str(), &[])); + let end_key = BytesKey::from_vec(get_key_for_col( + column.as_str(), + &vec![0xff; std::cmp::max(column.key_size(), 32)], + )); + self.db.compact(&start_key, &end_key); Ok(()) } diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index eacd28d2db..e86689b0cf 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -80,8 +80,22 @@ pub trait KeyValueStore: Sync + Send + Sized + 'static { /// this method. In future we may implement a safer mandatory locking scheme. fn begin_rw_transaction(&self) -> MutexGuard<()>; - /// Compact the database, freeing space used by deleted items. - fn compact(&self) -> Result<(), Error>; + /// Compact a single column in the database, freeing space used by deleted items. + fn compact_column(&self, column: DBColumn) -> Result<(), Error>; + + /// Compact a default set of columns that are likely to free substantial space. + fn compact(&self) -> Result<(), Error> { + // Compact state and block related columns as they are likely to have the most churn, + // i.e. entries being created and deleted. + for column in [ + DBColumn::BeaconState, + DBColumn::BeaconStateSummary, + DBColumn::BeaconBlock, + ] { + self.compact_column(column)?; + } + Ok(()) + } /// Iterate through all keys and values in a particular column. fn iter_column(&self, column: DBColumn) -> ColumnIter { diff --git a/beacon_node/store/src/memory_store.rs b/beacon_node/store/src/memory_store.rs index c2e494dce6..302d2c2add 100644 --- a/beacon_node/store/src/memory_store.rs +++ b/beacon_node/store/src/memory_store.rs @@ -108,7 +108,7 @@ impl KeyValueStore for MemoryStore { self.transaction_mutex.lock() } - fn compact(&self) -> Result<(), Error> { + fn compact_column(&self, _column: DBColumn) -> Result<(), Error> { Ok(()) } } diff --git a/beacon_node/store/src/partial_beacon_state.rs b/beacon_node/store/src/partial_beacon_state.rs index 1fb5751a0a..4e5a2b8e64 100644 --- a/beacon_node/store/src/partial_beacon_state.rs +++ b/beacon_node/store/src/partial_beacon_state.rs @@ -5,7 +5,6 @@ use crate::chunked_vector::{ use crate::{get_key_for_col, DBColumn, Error, KeyValueStore, KeyValueStoreOp}; use ssz::{Decode, DecodeError, Encode}; use ssz_derive::{Decode, Encode}; -use std::convert::TryInto; use std::sync::Arc; use types::historical_summary::HistoricalSummary; use types::superstruct; @@ -15,14 +14,14 @@ use types::*; /// /// Utilises lazy-loading from separate storage for its vector fields. #[superstruct( - variants(Base, Altair, Merge, Capella, Deneb), + variants(Base, Altair, Merge, Capella, Deneb, Electra), variant_attributes(derive(Debug, PartialEq, Clone, Encode, Decode)) )] #[derive(Debug, PartialEq, Clone, Encode)] #[ssz(enum_behaviour = "transparent")] -pub struct PartialBeaconState +pub struct PartialBeaconState where - T: EthSpec, + E: EthSpec, { // Versioning pub genesis_time: u64, @@ -35,85 +34,90 @@ where pub latest_block_header: BeaconBlockHeader, #[ssz(skip_serializing, skip_deserializing)] - pub block_roots: Option>, + pub block_roots: Option>, #[ssz(skip_serializing, skip_deserializing)] - pub state_roots: Option>, + pub state_roots: Option>, #[ssz(skip_serializing, skip_deserializing)] - pub historical_roots: Option>, + pub historical_roots: Option>, // Ethereum 1.0 chain data pub eth1_data: Eth1Data, - pub eth1_data_votes: VariableList, + pub eth1_data_votes: VariableList, pub eth1_deposit_index: u64, // Registry - pub validators: VariableList, - pub balances: VariableList, + pub validators: VariableList, + pub balances: VariableList, // Shuffling /// Randao value from the current slot, for patching into the per-epoch randao vector. pub latest_randao_value: Hash256, #[ssz(skip_serializing, skip_deserializing)] - pub randao_mixes: Option>, + pub randao_mixes: Option>, // Slashings - slashings: FixedVector, + slashings: FixedVector, // Attestations (genesis fork only) #[superstruct(only(Base))] - pub previous_epoch_attestations: VariableList, T::MaxPendingAttestations>, + pub previous_epoch_attestations: VariableList, E::MaxPendingAttestations>, #[superstruct(only(Base))] - pub current_epoch_attestations: VariableList, T::MaxPendingAttestations>, + pub current_epoch_attestations: VariableList, E::MaxPendingAttestations>, // Participation (Altair and later) - #[superstruct(only(Altair, Merge, Capella, Deneb))] - pub previous_epoch_participation: VariableList, - #[superstruct(only(Altair, Merge, Capella, Deneb))] - pub current_epoch_participation: VariableList, + #[superstruct(only(Altair, Merge, Capella, Deneb, Electra))] + pub previous_epoch_participation: VariableList, + #[superstruct(only(Altair, Merge, Capella, Deneb, Electra))] + pub current_epoch_participation: VariableList, // Finality - pub justification_bits: BitVector, + pub justification_bits: BitVector, pub previous_justified_checkpoint: Checkpoint, pub current_justified_checkpoint: Checkpoint, pub finalized_checkpoint: Checkpoint, // Inactivity - #[superstruct(only(Altair, Merge, Capella, Deneb))] - pub inactivity_scores: VariableList, + #[superstruct(only(Altair, Merge, Capella, Deneb, Electra))] + pub inactivity_scores: VariableList, // Light-client sync committees - #[superstruct(only(Altair, Merge, Capella, Deneb))] - pub current_sync_committee: Arc>, - #[superstruct(only(Altair, Merge, Capella, Deneb))] - pub next_sync_committee: Arc>, + #[superstruct(only(Altair, Merge, Capella, Deneb, Electra))] + pub current_sync_committee: Arc>, + #[superstruct(only(Altair, Merge, Capella, Deneb, Electra))] + pub next_sync_committee: Arc>, // Execution #[superstruct( only(Merge), partial_getter(rename = "latest_execution_payload_header_merge") )] - pub latest_execution_payload_header: ExecutionPayloadHeaderMerge, + pub latest_execution_payload_header: ExecutionPayloadHeaderMerge, #[superstruct( only(Capella), partial_getter(rename = "latest_execution_payload_header_capella") )] - pub latest_execution_payload_header: ExecutionPayloadHeaderCapella, + pub latest_execution_payload_header: ExecutionPayloadHeaderCapella, #[superstruct( only(Deneb), partial_getter(rename = "latest_execution_payload_header_deneb") )] - pub latest_execution_payload_header: ExecutionPayloadHeaderDeneb, + pub latest_execution_payload_header: ExecutionPayloadHeaderDeneb, + #[superstruct( + only(Electra), + partial_getter(rename = "latest_execution_payload_header_electra") + )] + pub latest_execution_payload_header: ExecutionPayloadHeaderElectra, // Capella - #[superstruct(only(Capella, Deneb))] + #[superstruct(only(Capella, Deneb, Electra))] pub next_withdrawal_index: u64, - #[superstruct(only(Capella, Deneb))] + #[superstruct(only(Capella, Deneb, Electra))] pub next_withdrawal_validator_index: u64, #[ssz(skip_serializing, skip_deserializing)] - #[superstruct(only(Capella, Deneb))] - pub historical_summaries: Option>, + #[superstruct(only(Capella, Deneb, Electra))] + pub historical_summaries: Option>, } /// Implement the conversion function from BeaconState -> PartialBeaconState. @@ -169,9 +173,9 @@ macro_rules! impl_from_state_forgetful { } } -impl PartialBeaconState { +impl PartialBeaconState { /// Convert a `BeaconState` to a `PartialBeaconState`, while dropping the optional fields. - pub fn from_state_forgetful(outer: &BeaconState) -> Self { + pub fn from_state_forgetful(outer: &BeaconState) -> Self { match outer { BeaconState::Base(s) => impl_from_state_forgetful!( s, @@ -244,6 +248,23 @@ impl PartialBeaconState { ], [historical_summaries] ), + BeaconState::Electra(s) => impl_from_state_forgetful!( + s, + outer, + Electra, + PartialBeaconStateElectra, + [ + previous_epoch_participation, + current_epoch_participation, + current_sync_committee, + next_sync_committee, + inactivity_scores, + latest_execution_payload_header, + next_withdrawal_index, + next_withdrawal_validator_index + ], + [historical_summaries] + ), } } @@ -260,7 +281,7 @@ impl PartialBeaconState { )?; let slot = Slot::from_ssz_bytes(slot_bytes)?; - let fork_at_slot = spec.fork_name_at_slot::(slot); + let fork_at_slot = spec.fork_name_at_slot::(slot); Ok(map_fork_name!( fork_at_slot, @@ -275,13 +296,13 @@ impl PartialBeaconState { KeyValueStoreOp::PutKeyValue(db_key, self.as_ssz_bytes()) } - pub fn load_block_roots>( + pub fn load_block_roots>( &mut self, store: &S, spec: &ChainSpec, ) -> Result<(), Error> { if self.block_roots().is_none() { - *self.block_roots_mut() = Some(load_vector_from_db::( + *self.block_roots_mut() = Some(load_vector_from_db::( store, self.slot(), spec, @@ -290,13 +311,13 @@ impl PartialBeaconState { Ok(()) } - pub fn load_state_roots>( + pub fn load_state_roots>( &mut self, store: &S, spec: &ChainSpec, ) -> Result<(), Error> { if self.state_roots().is_none() { - *self.state_roots_mut() = Some(load_vector_from_db::( + *self.state_roots_mut() = Some(load_vector_from_db::( store, self.slot(), spec, @@ -305,20 +326,20 @@ impl PartialBeaconState { Ok(()) } - pub fn load_historical_roots>( + pub fn load_historical_roots>( &mut self, store: &S, spec: &ChainSpec, ) -> Result<(), Error> { if self.historical_roots().is_none() { *self.historical_roots_mut() = Some( - load_variable_list_from_db::(store, self.slot(), spec)?, + load_variable_list_from_db::(store, self.slot(), spec)?, ); } Ok(()) } - pub fn load_historical_summaries>( + pub fn load_historical_summaries>( &mut self, store: &S, spec: &ChainSpec, @@ -327,7 +348,7 @@ impl PartialBeaconState { if let Ok(historical_summaries) = self.historical_summaries_mut() { if historical_summaries.is_none() { *historical_summaries = - Some(load_variable_list_from_db::( + Some(load_variable_list_from_db::( store, slot, spec, )?); } @@ -335,7 +356,7 @@ impl PartialBeaconState { Ok(()) } - pub fn load_randao_mixes>( + pub fn load_randao_mixes>( &mut self, store: &S, spec: &ChainSpec, @@ -343,10 +364,10 @@ impl PartialBeaconState { if self.randao_mixes().is_none() { // Load the per-epoch values from the database let mut randao_mixes = - load_vector_from_db::(store, self.slot(), spec)?; + load_vector_from_db::(store, self.slot(), spec)?; // Patch the value for the current slot into the index for the current epoch - let current_epoch = self.slot().epoch(T::slots_per_epoch()); + let current_epoch = self.slot().epoch(E::slots_per_epoch()); let len = randao_mixes.len(); randao_mixes[current_epoch.as_usize() % len] = *self.latest_randao_value(); @@ -399,6 +420,8 @@ macro_rules! impl_try_into_beacon_state { committee_caches: <_>::default(), pubkey_cache: <_>::default(), exit_cache: <_>::default(), + slashings_cache: <_>::default(), + epoch_cache: <_>::default(), tree_hash_cache: <_>::default(), // Variant-specific fields @@ -489,6 +512,22 @@ impl TryInto> for PartialBeaconState { ], [historical_summaries] ), + PartialBeaconState::Electra(inner) => impl_try_into_beacon_state!( + inner, + Electra, + BeaconStateElectra, + [ + previous_epoch_participation, + current_epoch_participation, + current_sync_committee, + next_sync_committee, + inactivity_scores, + latest_execution_payload_header, + next_withdrawal_index, + next_withdrawal_validator_index + ], + [historical_summaries] + ), }; Ok(state) } diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index e323659109..1a35d9d139 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -53,6 +53,7 @@ * [MEV](./builders.md) * [Merge Migration](./merge-migration.md) * [Late Block Re-orgs](./late-block-re-orgs.md) + * [Blobs](./advanced-blobs.md) * [Built-In Documentation](./help_general.md) * [Beacon Node](./help_bn.md) * [Validator Client](./help_vc.md) diff --git a/book/src/advanced-blobs.md b/book/src/advanced-blobs.md new file mode 100644 index 0000000000..eee404a9be --- /dev/null +++ b/book/src/advanced-blobs.md @@ -0,0 +1,42 @@ +# Blobs + +In the Deneb network upgrade, one of the changes is the implementation of EIP-4844, also known as [Proto-danksharding](https://blog.ethereum.org/2024/02/27/dencun-mainnet-announcement). Alongside with this, a new term named `blob` (binary large object) is introduced. Blobs are "side-cars" carrying transaction data in a block. They are mainly used by Ethereum layer 2 operators. As far as stakers are concerned, the main difference with the introduction of blobs is the increased storage requirement. + +### FAQ + +1. What is the storage requirement for blobs? + + We expect an additional increase of ~50 GB of storage requirement for blobs (on top of what is required by the consensus and execution clients database). The calculation is as below: + + One blob is 128 KB in size. Each block can carry a maximum of 6 blobs. Blobs will be kept for 4096 epochs and pruned afterwards. This means that the maximum increase in storage requirement will be: + + ``` + 2**17 bytes / blob * 6 blobs / block * 32 blocks / epoch * 4096 epochs = 96 GB + ``` + + However, the blob base fee targets 3 blobs per block and it works similarly to how EIP-1559 operates in the Ethereum gas fee. Therefore, practically it is very likely to average to 3 blobs per blocks, which translates to a storage requirement of 48 GB. + + +1. Do I have to add any flags for blobs? + + No, you can use the default values for blob-related flags, which means you do not need add or remove any flags. + +1. What if I want to keep all blobs? + + Use the flag `--prune-blobs false` in the beacon node. The storage requirement will be: + + ``` + 2**17 bytes * 3 blobs / block * 7200 blocks / day * 30 days = 79GB / month or 948GB / year + ``` + + To keep blobs for a custom period, you may use the flag `--blob-prune-margin-epochs ` which keeps blobs for 4096+EPOCHS specified in the flag. + +1. How to see the info of the blobs database? + + We can call the API: + + ```bash + curl "http://localhost:5052/lighthouse/database/info" | jq + ``` + + Refer to [Lighthouse API](./api-lighthouse.md#lighthousedatabaseinfo) for an example response. \ No newline at end of file diff --git a/book/src/advanced-release-candidates.md b/book/src/advanced-release-candidates.md index b2ff021365..a539aa489c 100644 --- a/book/src/advanced-release-candidates.md +++ b/book/src/advanced-release-candidates.md @@ -40,5 +40,5 @@ There can also be a scenario that a bug has been found and requires an urgent fi ## When *not* to use a release candidate -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). +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., Holesky). diff --git a/book/src/advanced.md b/book/src/advanced.md index 51416a3b73..21e732afa1 100644 --- a/book/src/advanced.md +++ b/book/src/advanced.md @@ -21,3 +21,4 @@ tips about how things work under the hood. * [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. +* [Blobs](./advanced-blobs.md): information about blobs in Deneb upgrade diff --git a/book/src/advanced_database.md b/book/src/advanced_database.md index 20c5d7443b..f65fb10415 100644 --- a/book/src/advanced_database.md +++ b/book/src/advanced_database.md @@ -23,15 +23,13 @@ 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 | +| 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 | +| Research | 32 | more than 10 TB | 155 ms | +| Enthusiast (prev. default) | 2048 | hundreds of GB | 10.2 s | +| Validator only (default) | 8192 | tens of GB | 41 s | -*Last update: May 2023. +*Last update: Dec 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 @@ -41,12 +39,12 @@ The default value is 8192 for databases synced from scratch using Lighthouse v2. 2048 for prior versions. Please see the section on [Defaults](#defaults) below. 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 +`BeaconState` consumes around 145MB of disk space, and each block replayed takes around 5ms. The **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. +To run a full archival node with fast access to beacon states and a SPRP of 32, the disk usage will be more than 10 TB per year, which is impractical for many users. As such, users may consider running the [tree-states](https://github.com/sigp/lighthouse/releases/tag/v5.0.111-exp) release, which only uses less than 200 GB for a full archival node. The caveat is that it is currently experimental and in alpha release (as of Dec 2023), thus not recommended for running mainnet validators. Nevertheless, it is suitable to be used for analysis purposes, and if you encounter any issues in tree-states, we do appreciate any feedback. We plan to have a stable release of tree-states in 1H 2024. ### Defaults diff --git a/book/src/advanced_networking.md b/book/src/advanced_networking.md index b1f05450c4..5fabf57d56 100644 --- a/book/src/advanced_networking.md +++ b/book/src/advanced_networking.md @@ -40,7 +40,7 @@ drastically and use the (recommended) default. ### NAT Traversal (Port Forwarding) -Lighthouse, by default, uses port 9000 for both TCP and UDP. Lighthouse will +Lighthouse, by default, uses port 9000 for both TCP and UDP. Since v4.5.0, Lighthouse will also attempt to make QUIC connections via UDP port 9001 by default. Lighthouse will still function if it is behind a NAT without any port mappings. Although Lighthouse still functions, we recommend that some mechanism is used to ensure that your Lighthouse node is publicly accessible. This will typically improve @@ -50,8 +50,8 @@ 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 to manually set up port mappings to both of Lighthouse's -TCP and UDP ports (9000 by default). +enabled, we recommend you to manually set up port mappings to Lighthouse's +TCP and UDP ports (9000 TCP/UDP, and 9001 UDP 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. @@ -66,7 +66,7 @@ TCP and UDP ports (9000 by default). 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 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. @@ -74,16 +74,22 @@ The steps to do port forwarding depends on the router, but the general steps are 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". +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 +- 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. +Since V4.5.0 port 9001/UDP is also used for QUIC support. +- Protocol: select `UDP` +- External port: `9001` +- Internal port: `9001` +- IP address: Choose the device that is running Lighthouse. + +5. To check that you have successfully opened 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. Note: this will only confirm if port 9000/TCP is open. You will need to ensure you have correctly setup port forwarding for the UDP ports (`9000` and `9001` by default). ### ENR Configuration @@ -125,6 +131,9 @@ IPv4 only: TCP and UDP. - `--listen-address :: --port 9909 --discovery-port 9999` will listen over IPv6 using port `9909` for TCP and port `9999` for UDP. +- By default, QUIC listens for UDP connections using a port number that is one greater than the specified port. + If the specified port is 9909, QUIC will use port 9910 for IPv6 UDP connections. + This can be configured with `--quic-port`. To listen over both IPv4 and IPv6: - Set two listening addresses using the `--listen-address` flag twice ensuring @@ -133,18 +142,38 @@ To listen over both IPv4 and IPv6: 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 +- 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. +- If necessary, set the `--quic-port6` flag to configure the port used by QUIC for + UDP over IPv6. This will default to the value given to `--port6` + 1. 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`. +> When using `--listen-address :: --listen-address 0.0.0.0 --port 9909`, listening will be set up as follows: +> +> **IPv4**: +> +> It listens on port `9909` for both TCP and UDP. +> QUIC will use the next sequential port `9910` for UDP. +> +> **IPv6**: +> +> It listens on the default value of --port6 (`9090`) for both UDP and TCP. +> QUIC will use port `9091` for UDP, which is the default `--port6` value (`9090`) + 1. + +> When using `--listen-address :: --listen-address --port 9909 --discovery-port6 9999`, listening will be set up as follows: +> +> **IPv4**: +> +> It listens on port `9909` for both TCP and UDP. +> QUIC will use the next sequential port `9910` for UDP. +> +> **IPv6**: +> +> It listens on the default value of `--port6` (`9090`) for TCP, and port `9999` for UDP. +> QUIC will use port `9091` for UDP, which is the default `--port6` value (`9090`) + 1. #### Configuring Lighthouse to advertise IPv6 reachable addresses Lighthouse supports IPv6 to connect to other nodes both over IPv6 exclusively, diff --git a/book/src/api-bn.md b/book/src/api-bn.md index 519ce57055..3e57edd8db 100644 --- a/book/src/api-bn.md +++ b/book/src/api-bn.md @@ -100,7 +100,7 @@ The `jq` tool is used to format the JSON data properly. If it returns `jq: comma 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" +curl -X GET "http://localhost:5052/eth/v1/beacon/states/head/validators/1" -H "accept: application/json" | jq ``` ```json diff --git a/book/src/api-vc-endpoints.md b/book/src/api-vc-endpoints.md index d3b2edafe9..cf52454c2d 100644 --- a/book/src/api-vc-endpoints.md +++ b/book/src/api-vc-endpoints.md @@ -16,6 +16,7 @@ HTTP Path | Description | [`POST /lighthouse/validators/keystore`](#post-lighthousevalidatorskeystore) | Import a keystore. [`POST /lighthouse/validators/mnemonic`](#post-lighthousevalidatorsmnemonic) | Create a new validator from an existing mnemonic. [`POST /lighthouse/validators/web3signer`](#post-lighthousevalidatorsweb3signer) | Add web3signer validators. +[`GET /lighthouse/logs`](#get-lighthouselogs) | Get logs The query to Lighthouse API endpoints requires authorization, see [Authorization Header](./api-vc-auth-header.md). @@ -745,19 +746,19 @@ Create any number of new validators, all of which will refer to a "graffiti": "Mr F was here", "suggested_fee_recipient": "0xa2e334e71511686bcfe38bb3ee1ad8f6babcc03d", "voting_public_key": "0xa062f95fee747144d5e511940624bc6546509eeaeae9383257a9c43e7ddc58c17c2bab4ae62053122184c381b90db380", + "builder_proposals": true, "url": "http://path-to-web3signer.com", - "root_certificate_path": "/path/on/vc/filesystem/to/certificate.pem", + "root_certificate_path": "/path/to/certificate.pem", + "client_identity_path": "/path/to/identity.p12", + "client_identity_password": "pass", "request_timeout_ms": 12000 } ] + ``` -The following fields may be omitted or nullified to obtain default values: +Some of the fields above may be omitted or nullified to obtain default values (e.g., `graffiti`, `request_timeout_ms`). -- `graffiti` -- `suggested_fee_recipient` -- `root_certificate_path` -- `request_timeout_ms` Command: ```bash @@ -765,7 +766,7 @@ 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}]" +-d "[{\"enable\":true,\"description\":\"validator_one\",\"graffiti\":\"Mr F was here\",\"suggested_fee_recipient\":\"0xa2e334e71511686bcfe38bb3ee1ad8f6babcc03d\",\"voting_public_key\":\"0xa062f95fee747144d5e511940624bc6546509eeaeae9383257a9c43e7ddc58c17c2bab4ae62053122184c381b90db380\",\"builder_proposals\":true,\"url\":\"http://path-to-web3signer.com\",\"root_certificate_path\":\"/path/to/certificate.pem\",\"client_identity_path\":\"/path/to/identity.p12\",\"client_identity_password\":\"pass\",\"request_timeout_ms\":12000}]" ``` diff --git a/book/src/checkpoint-sync.md b/book/src/checkpoint-sync.md index 00afea1567..37677c00ad 100644 --- a/book/src/checkpoint-sync.md +++ b/book/src/checkpoint-sync.md @@ -1,6 +1,6 @@ # Checkpoint Sync -Lighthouse supports syncing from a recent finalized checkpoint. This is substantially faster than syncing from genesis, while still providing all the same features. Checkpoint sync is also safer as it protects the node from long-range attacks. Since 4.6.0, checkpoint sync is required by default and genesis sync will no longer work without the use of `--allow-insecure-genesis-sync`. +Lighthouse supports syncing from a recent finalized checkpoint. This is substantially faster than syncing from genesis, while still providing all the same features. Checkpoint sync is also safer as it protects the node from long-range attacks. Since [v4.6.0](https://github.com/sigp/lighthouse/releases/tag/v4.6.0), checkpoint sync is required by default and genesis sync will no longer work without the use of `--allow-insecure-genesis-sync`. To quickly get started with checkpoint sync, read the sections below on: diff --git a/book/src/database-migrations.md b/book/src/database-migrations.md index a4d28452d4..1e8e134436 100644 --- a/book/src/database-migrations.md +++ b/book/src/database-migrations.md @@ -16,6 +16,8 @@ validator client or the slasher**. | Lighthouse version | Release date | Schema version | Downgrade available? | |--------------------|--------------|----------------|----------------------| +| v5.1.0 | Mar 2024 | v19 | yes before Deneb | +| v5.0.0 | Feb 2024 | v19 | yes before Deneb | | v4.6.0 | Dec 2023 | v19 | yes before Deneb | | v4.6.0-rc.0 | Dec 2023 | v18 | yes before Deneb | | v4.5.0 | Sep 2023 | v17 | yes | @@ -126,7 +128,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`, `goerli` or `sepolia`. +4. The `--network` flag must be set to the correct network, e.g. `mainnet`, `holesky` or `sepolia`. The general form for a `lighthouse db` command is: @@ -158,8 +160,7 @@ lighthouse db version --network mainnet Pruning historic states helps in managing the disk space used by the Lighthouse beacon node by removing old beacon states from the freezer database. This can be especially useful when the database has accumulated a significant amount -of historic data. This command is intended for nodes synced before 4.4.1, as newly synced node no longer store -historic states by default. +of historic data. This command is intended for nodes synced before 4.4.1, as newly synced nodes no longer store historic states by default. Here are the steps to prune historic states: @@ -175,14 +176,27 @@ Here are the steps to prune historic states: sudo -u "$LH_USER" lighthouse db prune-states --datadir "$LH_DATADIR" --network "$NET" ``` + If pruning is available, Lighthouse will log: + + ``` + INFO Ready to prune states + WARN Pruning states is irreversible + WARN Re-run this command with --confirm to commit to state deletion + INFO Nothing has been pruned on this run + ``` + 3. If you are ready to prune the states irreversibly, add the `--confirm` flag to commit the changes: ```bash sudo -u "$LH_USER" lighthouse db prune-states --confirm --datadir "$LH_DATADIR" --network "$NET" ``` - The `--confirm` flag ensures that you are aware the action is irreversible, and historic states will be permanently removed. + The `--confirm` flag ensures that you are aware the action is irreversible, and historic states will be permanently removed. Lighthouse will log: + ``` + INFO Historic states pruned successfully + ``` + 4. After successfully pruning the historic states, you can restart the Lighthouse beacon node: ```bash diff --git a/book/src/docker.md b/book/src/docker.md index defa89517e..2c410877e5 100644 --- a/book/src/docker.md +++ b/book/src/docker.md @@ -112,10 +112,10 @@ docker run lighthouse:local lighthouse --help You can run a Docker beacon node with the following command: ```bash -docker run -p 9000:9000/tcp -p 9000:9000/udp -p 127.0.0.1:5052:5052 -v $HOME/.lighthouse:/root/.lighthouse sigp/lighthouse lighthouse --network mainnet beacon --http --http-address 0.0.0.0 +docker run -p 9000:9000/tcp -p 9000:9000/udp -p 9001:9001/udp -p 127.0.0.1:5052:5052 -v $HOME/.lighthouse:/root/.lighthouse sigp/lighthouse lighthouse --network mainnet beacon --http --http-address 0.0.0.0 ``` -> To join the Goerli testnet, use `--network goerli` instead. +> To join the Holesky testnet, use `--network holesky` instead. > The `-v` (Volumes) and `-p` (Ports) and values are described below. @@ -135,18 +135,18 @@ docker run -v $HOME/.lighthouse:/root/.lighthouse sigp/lighthouse lighthouse bea ### Ports -In order to be a good peer and serve other peers you should expose port `9000` for both TCP and UDP. +In order to be a good peer and serve other peers you should expose port `9000` for both TCP and UDP, and port `9001` for UDP. Use the `-p` flag to do this: ```bash -docker run -p 9000:9000/tcp -p 9000:9000/udp sigp/lighthouse lighthouse beacon +docker run -p 9000:9000/tcp -p 9000:9000/udp -p 9001:9001/udp sigp/lighthouse lighthouse beacon ``` If you use the `--http` flag you may also want to expose the HTTP port with `-p 127.0.0.1:5052:5052`. ```bash -docker run -p 9000:9000/tcp -p 9000:9000/udp -p 127.0.0.1:5052:5052 sigp/lighthouse lighthouse beacon --http --http-address 0.0.0.0 +docker run -p 9000:9000/tcp -p 9000:9000/udp -p 9001:9001/udp -p 127.0.0.1:5052:5052 sigp/lighthouse lighthouse beacon --http --http-address 0.0.0.0 ``` [docker_hub]: https://hub.docker.com/repository/docker/sigp/lighthouse/ diff --git a/book/src/faq.md b/book/src/faq.md index b3f3d55edf..9cc695c442 100644 --- a/book/src/faq.md +++ b/book/src/faq.md @@ -3,6 +3,7 @@ ## [Beacon Node](#beacon-node-1) - [I see a warning about "Syncing deposit contract block cache" or an error about "updating deposit contract cache", what should I do?](#bn-deposit-contract) - [I see beacon logs showing `WARN: Execution engine called failed`, what should I do?](#bn-ee) +- [I see beacon logs showing `Error during execution engine upcheck`, what should I do?](#bn-upcheck) - [My beacon node is stuck at downloading historical block using checkpoint sync. What should I do?](#bn-download-historical) - [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) @@ -12,6 +13,7 @@ - [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) +- [My beacon node logs `WARN Failed to finalize deposit cache`, what should I do?](#bn-deposit-cache) ## [Validator](#validator-1) - [Why does it take so long for a validator to be activated?](#vc-activation) @@ -22,7 +24,7 @@ - [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) - +- [How can I delete my validator once it is imported?](#vc-delete) ## [Network, Monitoring and Maintenance](#network-monitoring-and-maintenance-1) - [I have a low peer count and it is not increasing](#net-peer) @@ -33,6 +35,7 @@ - [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) +- [How to know how many of my peers are connected through QUIC?](#net-quic) ## [Miscellaneous](#miscellaneous-1) - [What should I do if I lose my slashing protection database?](#misc-slashing) @@ -41,11 +44,10 @@ - [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) +- [My hard disk is full and my validator is down. What should I do? ](#misc-full) ## 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: @@ -75,17 +77,27 @@ If this log continues appearing during operation, it means your execution client 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` +`error: HttpClient(url: http://127.0.0.1:8551/, kind: timeout, detail: operation timed out), 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 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. +- 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. To confirm that the error is due to oom, run `sudo dmesg -T | grep killed` to look for killed processes. 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. + +### I see beacon logs showing `Error during execution engine upcheck`, what should I do? + +An example of the full error is: + +`ERRO Error during execution engine upcheck error: HttpClient(url: http://127.0.0.1:8551/, kind: request, detail: error trying to connect: tcp connect error: Connection refused (os error 111)), service: exec` + +Connection refused means the beacon node cannot reach the execution client. This could be due to the execution client is offline or the configuration is wrong. If the execution client is offline, run the execution engine and the error will disappear. + +If it is a configuration issue, ensure that the execution engine can be reached. The standard endpoint to connect to the execution client is `--execution-endpoint http://localhost:8551`. If the execution client is on a different host, the endpoint to connect to it will change, e.g., `--execution-endpoint http://IP_address:8551` where `IP_address` is the IP of the execution client node (you may also need additional flags to be set). If it is using another port, the endpoint link needs to be changed accordingly. Once the execution client/beacon node is configured correctly, the error will disappear. ### My beacon node is stuck at downloading historical block using checkpoint sync. What should I do? @@ -96,8 +108,8 @@ INFO Downloading historical blocks est_time: --, distance: 4524545 slo ``` 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. + + - Check the number of peers you are connected to. If you have low peers (less than 50), try to do port forwarding on the ports 9000 TCP/UDP and 9001 UDP to increase peer count. - Restart the beacon node. @@ -108,7 +120,7 @@ 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`. +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. @@ -122,7 +134,7 @@ WARN Head is optimistic execution_block_hash: 0x47e7555f1d4215d1ad409b1ac1 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? +### 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: @@ -131,7 +143,7 @@ CRIT Beacon block processing error error: ValidatorPubkeyCacheLockTime 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. +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? @@ -141,7 +153,7 @@ 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. +`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? @@ -171,7 +183,7 @@ The error is `503 Service Unavailable`. This means that the beacon node is still 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. +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? @@ -193,6 +205,9 @@ ERRO Aggregate attestation queue full, queue_len: 4096, msg: the system has insu 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 `WARN Failed to finalize deposit cache`, what should I do? + +This is a known [bug](https://github.com/sigp/lighthouse/issues/3707) that will fix by itself. ## Validator @@ -266,24 +281,24 @@ repeats until the queue is cleared. The churn limit is summarised in the table b
-| 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 | +| 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. +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! @@ -296,14 +311,14 @@ duplicate your JSON keystores and don't run `lighthouse vc` twice). This will le 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? +### 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). +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`): @@ -311,14 +326,14 @@ Another cause for missing attestations is delays during block processing. When t 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. +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. +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. @@ -333,7 +348,7 @@ If you would like to still use Lighthouse to submit the message, you will need t ### 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. +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. ### I want to add new validators. Do I have to reimport the existing keys? @@ -345,6 +360,13 @@ 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. + +### How can I delete my validator once it is imported? + +Lighthouse supports the [KeyManager API](https://ethereum.github.io/keymanager-APIs/#/Local%20Key%20Manager/deleteKeys) to delete validators and remove them from the `validator_definitions.yml` file. To do so, start the validator client with the flag `--http` and call the API. + +If you are looking to delete the validators in one node and import it to another, you can use the [validator-manager](./validator-manager-move.md) to move the validators across nodes without the hassle of deleting and importing the keys. + ## Network, Monitoring and Maintenance ### I have a low peer count and it is not increasing @@ -354,7 +376,7 @@ 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 +`Holesky` 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). @@ -379,7 +401,7 @@ If the ports are open, you should have incoming peers. To check that you have in 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. +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. @@ -426,8 +448,8 @@ 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/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. +Lighthouse uses port 9000 for both TCP and UDP, and optionally 9001 UDP for QUIC support. +Opening these ports will make your Lighthouse node maximally contactable. ### How can I monitor my validators? @@ -440,7 +462,7 @@ Monitoring](./validator-monitoring.md) for more information. Lighthouse has also 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. @@ -463,7 +485,7 @@ The setting on the beacon node is the same for both cases below. In the beacon n 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 ``` @@ -476,16 +498,35 @@ It is also worth noting that the `--beacon-nodes` flag can also be used for redu 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```. +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 9100`. +Since V4.5.0, Lighthouse supports QUIC and by default will use the value of `--port` + 1 to listen via UDP (default `9001`). +This can be configured by using the flag `--quic-port`. Refer to [Advanced Networking](./advanced_networking.md#nat-traversal-port-forwarding) for more information. ### 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. +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. -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. +### How to know how many of my peers are connected via QUIC? + +With `--metrics` enabled in the beacon node, you can find the number of peers connected via QUIC using: + +```bash + curl -s "http://localhost:5054/metrics" | grep libp2p_quic_peers +``` + +A response example is: + +``` +# HELP libp2p_quic_peers Count of libp2p peers currently connected via QUIC +# TYPE libp2p_quic_peers gauge +libp2p_quic_peers 4 +``` +which shows that there are 4 peers connected via QUIC. + ## Miscellaneous ### What should I do if I lose my slashing protection database? @@ -523,7 +564,7 @@ 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. +Yes, Lighthouse supports [state pruning](./database-migrations.md#how-to-prune-historic-states) which can help to save disk space. ### Can I use a HDD for the freezer database and only have the hot db on SSD? @@ -531,11 +572,11 @@ Yes, you can do so by using the flag `--freezer-dir /path/to/freezer_db` in the ### 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 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. + +### My hard disk is full and my validator is down. What should I do? + +A quick way to get the validator back online is by removing the Lighthouse beacon node database and resync Lighthouse using checkpoint sync. A guide to do this can be found in the [Lighthouse Discord server](https://discord.com/channels/605577013327167508/605577013331361793/1019755522985050142). With some free space left, you will then be able to prune the execution client database to free up more space. diff --git a/book/src/help_bn.md b/book/src/help_bn.md index dff2ab6876..e437925a0e 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -32,11 +32,7 @@ FLAGS: --disable-deposit-contract-sync Explicitly disables syncing of deposit logs from the execution node. This overrides any previous option that depends on it. Useful if you intend to run a non-validating beacon node. - --disable-duplicate-warn-logs Disable warning logs for duplicate gossip messages. The WARN level log is - useful for detecting a duplicate validator key running elsewhere. - However, this may result in excessive warning logs if the validator is - broadcasting messages to multiple beacon nodes via the validator client - --broadcast flag. In this case, disabling these warn logs may be useful. + --disable-duplicate-warn-logs This flag is deprecated and has no effect. -x, --disable-enr-auto-update Discovery automatically updates the nodes local ENR with an external IP address and port as seen by other peers on the network. This disables this feature, fixing the ENR's IP/PORT to those specified on boot. @@ -74,9 +70,6 @@ FLAGS: enables --http and --validator-monitor-auto and enables SSE logging. -h, --help Prints help information --http Enable the RESTful HTTP API server. Disabled by default. - --http-allow-sync-stalled 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. --http-enable-tls Serves the RESTful HTTP API server over TLS. This feature is currently experimental. --import-all-attestations Import and aggregate all attestations, regardless of validator @@ -129,25 +122,6 @@ OPTIONS: --auto-compact-db Enable or disable automatic compaction of the database on finalization. [default: true] - --beacon-processor-aggregate-batch-size - 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: 64] - --beacon-processor-attestation-batch-size - 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: 64] - --beacon-processor-max-workers - 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. - --beacon-processor-reprocess-queue-len - 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: 12288] - --beacon-processor-work-queue-len - 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: 16384] --blob-prune-margin-epochs The margin for blob pruning in epochs. The oldest blobs are pruned up until data_availability_boundary - blob_prune_margin_epochs. [default: 0] @@ -183,6 +157,9 @@ OPTIONS: --builder-user-agent The HTTP user agent to send alongside requests to the builder URL. The default is Lighthouse's version string. + --checkpoint-blobs + Set the checkpoint blobs to start syncing from. Must be aligned and match --checkpoint-block. Using + --checkpoint-sync-url instead is recommended. --checkpoint-block Set a checkpoint block to start syncing from. Must be aligned and match --checkpoint-state. Using --checkpoint-sync-url instead is recommended. @@ -300,9 +277,6 @@ OPTIONS: --http-port Set the listen TCP port for the RESTful HTTP API server. - --http-spec-fork - Serve the spec for a specific hard fork on /eth/v1/config/spec. It should not be necessary to set this flag. - --http-sse-capacity-multiplier Multiplier to apply to the length of HTTP server-sent-event (SSE) channels. Increasing this value can prevent messages from being dropped. @@ -389,10 +363,8 @@ OPTIONS: useful for execution nodes which don't improve their payload after the first call, and high values are useful for ensuring the EL is given ample notice. Default: 1/3 of a slot. --progressive-balances - Control the progressive balances cache mode. The default `fast` mode uses the cache to speed up fork choice. - A more conservative `checked` mode compares the cache's results against results without the cache. If there - is a mismatch, it falls back to the cache-free result. Using the default `fast` mode is recommended unless - advised otherwise by the Lighthouse team. [possible values: disabled, checked, strict, fast] + Deprecated. This optimisation is now the default and cannot be disabled. [possible values: fast, disabled, + checked, strict] --proposer-reorg-cutoff Maximum delay after the start of the slot at which to propose a reorging block. Lower values can prevent failed reorgs by ensuring the block has ample time to propagate and be processed by the network. The default @@ -405,8 +377,11 @@ OPTIONS: --proposer-reorg-epochs-since-finalization Maximum number of epochs since finalization at which proposer reorgs are allowed. Default: 2 + --proposer-reorg-parent-threshold + Percentage of parent vote weight above which to attempt a proposer reorg. Default: 160% + --proposer-reorg-threshold - Percentage of vote weight below which to attempt a proposer reorg. Default: 20% + Percentage of head vote weight below which to attempt a proposer reorg. Default: 20% --prune-blobs Prune blobs from Lighthouse's database when they are older than the data data availability boundary relative @@ -462,6 +437,9 @@ OPTIONS: --slots-per-restore-point Specifies how often a freezer DB restore point should be stored. Cannot be changed after initialization. [default: 8192 (mainnet) or 64 (minimal)] + --state-cache-size + Specifies the size of the snapshot cache [default: 3] + --suggested-fee-recipient Emergency fallback fee recipient for use in case the validator client does not have one configured. You should set this flag on the validator client instead of (or in addition to) setting it here. @@ -507,4 +485,5 @@ OPTIONS: Specify a weak subjectivity checkpoint in `block_root:epoch` format to verify the node's sync against. The block root should be 0x-prefixed. Note that this flag is for verification only, to perform a checkpoint sync from a recent state use --checkpoint-sync-url. -``` \ No newline at end of file +``` + diff --git a/book/src/help_general.md b/book/src/help_general.md index fbe05693e7..551f93e2bf 100644 --- a/book/src/help_general.md +++ b/book/src/help_general.md @@ -104,4 +104,5 @@ SUBCOMMANDS: blocks and attestations). [aliases: v, vc, validator] validator_manager Utilities for managing a Lighthouse validator client via the HTTP API. [aliases: vm, validator-manager, validator_manager] -``` \ No newline at end of file +``` + diff --git a/book/src/help_vc.md b/book/src/help_vc.md index bc6deec1e9..fb963f87cc 100644 --- a/book/src/help_vc.md +++ b/book/src/help_vc.md @@ -13,7 +13,7 @@ FLAGS: --disable-auto-discover If present, do not attempt to discover new validators in the validators-dir. Validators will need to be manually added to the validator_definitions.yml file. - --disable-log-timestamp If present, do not include timestamps in logging output. + --disable-log-timestamp If present, do not include timestamps in logging output. --disable-malloc-tuning If present, do not configure the system allocator. Providing this flag will generally increase memory usage, it should only be provided when debugging specific memory allocation issues. @@ -21,6 +21,14 @@ FLAGS: DEPRECATED. Use --broadcast. By default, Lighthouse publishes attestation, sync committee subscriptions and proposer preparation messages to all beacon nodes provided in the `--beacon-nodes flag`. This option changes that behaviour such that these api calls only go out to the first available and synced beacon node + --disable-slashing-protection-web3signer + Disable Lighthouse's slashing protection for all web3signer keys. This can reduce the I/O burden on the VC + but is only safe if slashing protection is enabled on the remote signer and is implemented correctly. DO NOT + ENABLE THIS FLAG UNLESS YOU ARE CERTAIN THAT SLASHING PROTECTION IS ENABLED ON THE REMOTE SIGNER. YOU WILL + GET SLASHED IF YOU USE THIS FLAG WITHOUT ENABLING WEB3SIGNER'S SLASHING PROTECTION. + --distributed + Enables functionality required for running the validator in a distributed validator cluster. + --enable-doppelganger-protection If this flag is set, Lighthouse will delay startup for three epochs and monitor for messages on the network by any of the validators managed by this client. This will result in three (possibly four) epochs worth of @@ -32,8 +40,8 @@ FLAGS: Enable per validator metrics for > 64 validators. Note: This flag is automatically enabled for <= 64 validators. Enabling this metric for higher validator counts will lead to higher volume of prometheus metrics being collected. - -h, --help Prints help information - --http Enable the RESTful HTTP API server. Disabled by default. + -h, --help Prints help information + --http Enable the RESTful HTTP API server. Disabled by default. --http-allow-keystore-export 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 @@ -47,7 +55,7 @@ FLAGS: flag unless you're certain that a new slashing protection database is required. Usually, your database will have been initialized when you imported your validator keys. If you misplace your database and then run with this flag you risk being slashed. - --log-color Force outputting colors when emitting logs to the terminal. + --log-color Force outputting colors when emitting logs to the terminal. --logfile-compress If present, compress old log files. This can help reduce the space needed to store old logs. @@ -55,7 +63,7 @@ FLAGS: If present, log files will be generated as world-readable meaning they can be read by any user on the machine. Note that logs can often contain sensitive information about your validator and so this flag should be used with caution. For Windows users, the log file permissions will be inherited from the parent folder. - --metrics Enable the Prometheus metrics HTTP server. Disabled by default. + --metrics Enable the Prometheus metrics HTTP server. Disabled by default. --prefer-builder-proposals If this flag is set, Lighthouse will always prefer blocks constructed by builders, regardless of payload value. @@ -69,7 +77,7 @@ FLAGS: --use-long-timeouts If present, the validator client will use longer timeouts for requests made to the beacon node. This flag is generally not recommended, longer timeouts can cause missed duties when fallbacks are used. - -V, --version Prints version information + -V, --version Prints version information OPTIONS: --beacon-nodes @@ -209,4 +217,10 @@ OPTIONS: --validators-dir The directory which contains the validator keystores, deposit data for each validator along with the common slashing protection database and the validator_definitions.yml -``` \ No newline at end of file + --web3-signer-keep-alive-timeout + Keep-alive timeout for each web3signer connection. Set to 'null' to never timeout [default: 90000] + + --web3-signer-max-idle-connections + Maximum number of idle connections to maintain per web3signer host. Default is unlimited. +``` + diff --git a/book/src/help_vm.md b/book/src/help_vm.md index fa08aa4f65..db01164a92 100644 --- a/book/src/help_vm.md +++ b/book/src/help_vm.md @@ -94,4 +94,5 @@ SUBCOMMANDS: move 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). -``` \ No newline at end of file +``` + diff --git a/book/src/help_vm_create.md b/book/src/help_vm_create.md index 71db3cc599..2fa54265ab 100644 --- a/book/src/help_vm_create.md +++ b/book/src/help_vm_create.md @@ -134,4 +134,5 @@ OPTIONS: -t, --testnet-dir Path to directory containing eth2_testnet specs. Defaults to a hard-coded Lighthouse testnet. Only effective if there is no existing database. -``` \ No newline at end of file +``` + diff --git a/book/src/help_vm_import.md b/book/src/help_vm_import.md index 3960a55f1a..e6ff351dac 100644 --- a/book/src/help_vm_import.md +++ b/book/src/help_vm_import.md @@ -98,4 +98,5 @@ OPTIONS: --vc-url 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: http://localhost:5062] -``` \ No newline at end of file +``` + diff --git a/book/src/help_vm_move.md b/book/src/help_vm_move.md index a89af437a9..fe1d4c5ae9 100644 --- a/book/src/help_vm_move.md +++ b/book/src/help_vm_move.md @@ -115,4 +115,5 @@ OPTIONS: if there is no existing database. --validators The validators to be moved. Either a list of 0x-prefixed validator pubkeys or the keyword "all". -``` \ No newline at end of file +``` + diff --git a/book/src/http.md b/book/src/http.md deleted file mode 100644 index 82a688586b..0000000000 --- a/book/src/http.md +++ /dev/null @@ -1,33 +0,0 @@ -# HTTP API - -[OpenAPI Specification](https://ethereum.github.io/beacon-APIs/) - -## Beacon Node - -A Lighthouse beacon node can be configured to expose a HTTP server by supplying the `--http` flag. The default listen address is `localhost:5052`. - -The following CLI flags control the HTTP server: - -- `--http`: enable the HTTP server (required even if the following flags are - provided). -- `--http-port`: specify the listen port of the server. -- `--http-address`: specify the listen address of the server. - -The schema of the API aligns with the standard Ethereum Beacon Node API as defined -at [github.com/ethereum/beacon-APIs](https://github.com/ethereum/beacon-APIs). -It is an easy-to-use RESTful HTTP/JSON API. An interactive specification is -available [here](https://ethereum.github.io/beacon-APIs/). - -## Troubleshooting - -### HTTP API is unavailable or refusing connections - -Ensure the `--http` flag has been supplied at the CLI. - -You can quickly check that the HTTP endpoint is up using `curl`: - -``` -curl "localhost:5052/beacon/head" - -{"slot":37934,"block_root":"0x4d3ae7ebe8c6ef042db05958ec76e8f7be9d412a67a0defa6420a677249afdc7","state_root":"0x1c86b13ffc70a41e410eccce20d33f1fe59d148585ea27c2afb4060f75fe6be2","finalized_slot":37856,"finalized_block_root":"0xbdae152b62acef1e5c332697567d2b89e358628790b8273729096da670b23e86","justified_slot":37888,"justified_block_root":"0x01c2f516a407d8fdda23cad4ed4381e4ab8913d638f935a2fe9bd00d6ced5ec4","previous_justified_slot":37856,"previous_justified_block_root":"0xbdae152b62acef1e5c332697567d2b89e358628790b8273729096da670b23e86"} -``` diff --git a/book/src/installation-source.md b/book/src/installation-source.md index 58e6917eca..c2f5861576 100644 --- a/book/src/installation-source.md +++ b/book/src/installation-source.md @@ -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 +sudo apt update && sudo apt install -y git gcc g++ make cmake pkg-config llvm-dev libclang-dev clang ``` > Tips: @@ -42,6 +42,16 @@ sudo apt install -y git gcc g++ make cmake pkg-config llvm-dev libclang-dev clan After this, you are ready to [build Lighthouse](#build-lighthouse). +#### Fedora/RHEL/CentOS + +Install the following packages: + +```bash +yum -y install git make perl clang cmake +``` + +After this, you are ready to [build Lighthouse](#build-lighthouse). + #### macOS 1. Install the [Homebrew][] package manager. diff --git a/book/src/installation.md b/book/src/installation.md index 4adaf8da76..e8caf5c457 100644 --- a/book/src/installation.md +++ b/book/src/installation.md @@ -10,7 +10,7 @@ There are three core methods to obtain the Lighthouse application: Additionally, there are two extra guides for specific uses: -- [Raspberry Pi 4 guide](./pi.md). +- [Raspberry Pi 4 guide](./pi.md). (Archived) - [Cross-compiling guide for developers](./cross-compiling.md). There are also community-maintained installation methods: diff --git a/book/src/js/deposit.js b/book/src/js/deposit.js deleted file mode 100644 index 58895854f6..0000000000 --- a/book/src/js/deposit.js +++ /dev/null @@ -1,127 +0,0 @@ -const NETWORK = "5"; -const NETWORK_NAME = "Goerli Test Network"; -const DEPOSIT_CONTRACT = "0x07b39F4fDE4A38bACe212b546dAc87C58DfE3fDC"; -const DEPOSIT_AMOUNT_ETH = "32"; -const GAS_LIMIT = "4000000"; -const DEPOSIT_DATA_BYTES = 420; - -let PREVIOUS_NON_ERROR_STATE = ""; - -$(document).ready(function(){ - if (typeof window.ethereum !== 'undefined') { - ethereum.on('networkChanged', function (accounts) { - checkNetwork() - }) - - PREVIOUS_NON_ERROR_STATE = "upload"; - checkNetwork() - } else { - console.error("No metamask detected!") - triggerError("Metamask is not installed.
Get Metamask.") - } - - $("#fileInput").change(function() { - openFile(this.files[0]) - }); - - $("#uploadButton").on("click", function() { - $("#fileInput").trigger("click"); - }); -}); - -function checkNetwork() { - if (window.ethereum.networkVersion === NETWORK) { - setUiState(PREVIOUS_NON_ERROR_STATE) - } else { - triggerError("Please set Metamask to use " + NETWORK_NAME + ".") - } -} - -function doDeposit(deposit_data) { - const ethereum = window.ethereum; - const utils = ethers.utils; - - let wei = utils.parseEther(DEPOSIT_AMOUNT_ETH); - let gasLimit = utils.bigNumberify(GAS_LIMIT); - - ethereum.enable() - .then(function (accounts) { - let params = [{ - "from": accounts[0], - "to": DEPOSIT_CONTRACT, - "gas": utils.hexlify(gasLimit), - "value": utils.hexlify(wei), - "data": deposit_data - }] - - ethereum.sendAsync({ - method: 'eth_sendTransaction', - params: params, - from: accounts[0], // Provide the user's account to use. - }, function (err, result) { - if (err !== null) { - triggerError("

" + err.message + "

Reload the window to try again.

") - } else { - let tx_hash = result.result; - $("#txLink").attr("href", "https://goerli.etherscan.io/tx/" + tx_hash); - setUiState("waiting"); - } - }) - }) - .catch(function (error) { - triggerError("Unable to get Metamask accounts.
Reload page to try again.") - }) - -} - -function openFile(file) { - var reader = new FileReader(); - - reader.onload = function () { - let data = reader.result; - if (data.startsWith("0x")) { - if (data.length === DEPOSIT_DATA_BYTES * 2 + 2) { - doDeposit(data) - } else { - triggerError("Invalid eth1_deposit_file. Bad length.") - } - } else { - triggerError("Invalid eth1_deposit_file. Did not start with 0x.") - } - } - - reader.readAsBinaryString(file); -} - -function triggerError(text) { - $("#errorText").html(text); - setUiState("error"); -} - -function setUiState(state) { - if (state === "upload") { - $('#uploadDiv').show(); - $('#depositDiv').hide(); - $('#waitingDiv').hide(); - $('#errorDiv').hide(); - } else if (state == "deposit") { - $('#uploadDiv').hide(); - $('#depositDiv').show(); - $('#waitingDiv').hide(); - $('#errorDiv').hide(); - } else if (state == "error") { - $('#uploadDiv').hide(); - $('#depositDiv').hide(); - $('#waitingDiv').hide(); - $('#errorDiv').show(); - } else if (state == "waiting") { - $('#uploadDiv').hide(); - $('#depositDiv').hide(); - $('#waitingDiv').show(); - $('#errorDiv').hide(); - } - - if (state !== "error") { - PREVIOUS_NON_ERROR_STATE = state; - } -} diff --git a/book/src/lighthouse-ui.md b/book/src/lighthouse-ui.md index 4182314da1..81098715f3 100644 --- a/book/src/lighthouse-ui.md +++ b/book/src/lighthouse-ui.md @@ -13,7 +13,7 @@ Siren is a user interface built for Lighthouse that connects to a Lighthouse Bea a Lighthouse Validator Client to monitor performance and display key validator metrics. -The UI is currently in active development. Its resides in the +The UI is currently in active development. It resides in the [Siren](https://github.com/sigp/siren) repository. ## Topics @@ -30,5 +30,5 @@ information: ## Contributing -If you find and issue or bug or would otherwise like to help out with the +If you find an issue or bug or would otherwise like to help out with the development of the Siren project, please submit issues and PRs to the [Siren](https://github.com/sigp/siren) repository. diff --git a/book/src/mainnet-validator.md b/book/src/mainnet-validator.md index 377e5ebaa4..942ca09b8e 100644 --- a/book/src/mainnet-validator.md +++ b/book/src/mainnet-validator.md @@ -13,7 +13,7 @@ managing servers. You'll also need at least 32 ETH! Being educated is critical to a validator's success. Before submitting your mainnet deposit, we recommend: -- Thoroughly exploring the [Staking Launchpad][launchpad] website, try running through the deposit process using a testnet launchpad such as the [Goerli staking launchpad](https://goerli.launchpad.ethereum.org/en/). +- Thoroughly exploring the [Staking Launchpad][launchpad] website, try running through the deposit process using a testnet launchpad such as the [Holesky staking launchpad](https://holesky.launchpad.ethereum.org/en/). - Running a testnet validator. - Reading through this documentation, especially the [Slashing Protection][slashing] section. - Performing a web search and doing your own research. @@ -41,10 +41,7 @@ There are five primary steps to become a validator: > **Important note**: The guide below contains both mainnet and testnet instructions. We highly recommend *all* users to **run a testnet validator** prior to staking mainnet ETH. By far, the best technical learning experience is to run a testnet validator. You can get hands-on experience with all the tools and it's a great way to test your staking hardware. 32 ETH is a significant outlay and joining a testnet is a great way to "try before you buy". - - - -> **Never use real ETH to join a testnet!** Testnet such as the Goerli testnet uses Goerli ETH which is worthless. This allows experimentation without real-world costs. +> **Never use real ETH to join a testnet!** Testnet such as the Holesky testnet uses Holesky ETH which is worthless. This allows experimentation without real-world costs. ### Step 1. Create validator keys @@ -52,7 +49,7 @@ The Ethereum Foundation provides the [staking-deposit-cli](https://github.com/et ```bash ./deposit new-mnemonic ``` -and follow the instructions to generate the keys. When prompted for a network, select `mainnet` if you want to run a mainnet validator, or select `goerli` if you want to run a Goerli testnet validator. A new mnemonic will be generated in the process. +and follow the instructions to generate the keys. When prompted for a network, select `mainnet` if you want to run a mainnet validator, or select `holesky` if you want to run a Holesky testnet validator. A new mnemonic will be generated in the process. > **Important note:** A mnemonic (or seed phrase) is a 24-word string randomly generated in the process. It is highly recommended to write down the mnemonic and keep it safe offline. It is important to ensure that the mnemonic is never stored in any digital form (computers, mobile phones, etc) connected to the internet. Please also make one or more backups of the mnemonic to ensure your ETH is not lost in the case of data loss. It is very important to keep your mnemonic private as it represents the ultimate control of your ETH. @@ -75,9 +72,9 @@ Mainnet: lighthouse --network mainnet account validator import --directory $HOME/staking-deposit-cli/validator_keys ``` -Goerli testnet: +Holesky testnet: ```bash -lighthouse --network goerli account validator import --directory $HOME/staking-deposit-cli/validator_keys +lighthouse --network holesky account validator import --directory $HOME/staking-deposit-cli/validator_keys ``` > Note: The user must specify the consensus client network that they are importing the keys by using the `--network` flag. @@ -137,9 +134,9 @@ Mainnet: lighthouse vc --network mainnet --suggested-fee-recipient YourFeeRecipientAddress ``` -Goerli testnet: +Holesky testnet: ```bash -lighthouse vc --network goerli --suggested-fee-recipient YourFeeRecipientAddress +lighthouse vc --network holesky --suggested-fee-recipient YourFeeRecipientAddress ``` The `validator client` manages validators using data obtained from the beacon node via a HTTP API. You are highly recommended to enter a fee-recipient by changing `YourFeeRecipientAddress` to an Ethereum address under your control. @@ -157,7 +154,7 @@ by the protocol. ### Step 5: Submit deposit (32ETH per validator) -After you have successfully run and synced the execution client, beacon node and validator client, you can now proceed to submit the deposit. Go to the mainnet [Staking launchpad](https://launchpad.ethereum.org/en/) (or [Goerli staking launchpad](https://goerli.launchpad.ethereum.org/en/) for testnet validator) and carefully go through the steps to becoming a validator. Once you are ready, you can submit the deposit by sending 32ETH per validator to the deposit contract. Upload the `deposit_data-*.json` file generated in [Step 1](#step-1-create-validator-keys) to the Staking launchpad. +After you have successfully run and synced the execution client, beacon node and validator client, you can now proceed to submit the deposit. Go to the mainnet [Staking launchpad](https://launchpad.ethereum.org/en/) (or [Holesky staking launchpad](https://holesky.launchpad.ethereum.org/en/) for testnet validator) and carefully go through the steps to becoming a validator. Once you are ready, you can submit the deposit by sending 32ETH per validator to the deposit contract. Upload the `deposit_data-*.json` file generated in [Step 1](#step-1-create-validator-keys) to the Staking launchpad. > **Important note:** Double check that the deposit contract for mainnet is `0x00000000219ab540356cBB839Cbe05303d7705Fa` before you confirm the transaction. diff --git a/book/src/merge-migration.md b/book/src/merge-migration.md index e2dab9652f..a5769162b0 100644 --- a/book/src/merge-migration.md +++ b/book/src/merge-migration.md @@ -25,14 +25,14 @@ All networks (**Mainnet**, **Goerli (Prater)**, **Ropsten**, **Sepolia**, **Kiln
-| Network | Bellatrix | The Merge | Remark | -|-------------------|--------------------------------------------|----|----| -| Ropsten | 2nd June 2022 | 8th June 2022 | Deprecated -| Sepolia | 20th June 2022 | 6th July 2022 | | -| Goerli | 4th August 2022 | 10th August 2022 | Previously named `Prater`| -| Mainnet | 6th September 2022 | 15th September 2022 | -| Chiado | 10th October 2022 | 4th November 2022 | -| Gnosis| 30th November 2022 | 8th December 2022 +| Network | Bellatrix | The Merge | Remark | +|---------|-------------------------------|-------------------------------| -----------| +| Ropsten | 2nd June 2022 | 8th June 2022 | Deprecated | +| Sepolia | 20th June 2022 | 6th July 2022 | | +| Goerli | 4th August 2022 | 10th August 2022 | Previously named `Prater`| +| Mainnet | 6th September 2022| 15th September 2022| | +| Chiado | 10th October 2022 | 4th November 2022 | | +| Gnosis | 30th November 2022| 8th December 2022 | |
diff --git a/book/src/pi.md b/book/src/pi.md index 550415240b..2fea91ad17 100644 --- a/book/src/pi.md +++ b/book/src/pi.md @@ -1,5 +1,7 @@ # Raspberry Pi 4 Installation +> Note: This page is left here for archival purposes. As the number of validators on mainnet has increased significantly, so does the requirement for hardware (e.g., RAM). Running Ethereum mainnet on a Raspberry Pi 4 is no longer recommended. + Tested on: - Raspberry Pi 4 Model B (4GB) @@ -22,7 +24,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 +sudo apt update && 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 11b9845658..bd1976f950 100644 --- a/book/src/redundancy.md +++ b/book/src/redundancy.md @@ -101,10 +101,6 @@ from this list: - `none`: Disable all broadcasting. This option only has an effect when provided alone, otherwise it is ignored. Not recommended except for expert tweakers. -Broadcasting attestation, blocks and sync committee messages may result in excessive warning logs in the beacon node -due to duplicate gossip messages. In this case, it may be desirable to disable warning logs for duplicates using the -beacon node `--disable-duplicate-warn-logs` flag. - The default is `--broadcast subscriptions`. To also broadcast blocks for example, use `--broadcast subscriptions,blocks`. diff --git a/book/src/run_a_node.md b/book/src/run_a_node.md index 1ea1427335..ab42c0c10a 100644 --- a/book/src/run_a_node.md +++ b/book/src/run_a_node.md @@ -56,7 +56,7 @@ Notable flags: - `--network` flag, which selects a network: - `lighthouse` (no flag): Mainnet. - `lighthouse --network mainnet`: Mainnet. - - `lighthouse --network goerli`: Goerli (testnet). + - `lighthouse --network holesky`: Holesky (testnet). - `lighthouse --network sepolia`: Sepolia (testnet). - `lighthouse --network chiado`: Chiado (testnet). - `lighthouse --network gnosis`: Gnosis chain. diff --git a/book/src/setup.md b/book/src/setup.md index 87f431f9ba..c678b4387a 100644 --- a/book/src/setup.md +++ b/book/src/setup.md @@ -16,7 +16,7 @@ The additional requirements for developers are: some dependencies. See [`Installation Guide`](./installation.md) for more info. - [`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-dev`](https://www.postgresql.org/docs/devel/libpq.html). Also known as `libpq-devel` on some systems. - [`docker`](https://www.docker.com/). Some tests need docker installed and **running**. diff --git a/book/src/slasher.md b/book/src/slasher.md index c8506922b4..79a2d1f8eb 100644 --- a/book/src/slasher.md +++ b/book/src/slasher.md @@ -17,7 +17,7 @@ of the immaturity of the slasher UX and the extra resources required. The slasher runs inside the same process as the beacon node, when enabled via the `--slasher` flag: ``` -lighthouse bn --slasher --debug-level debug +lighthouse bn --slasher ``` The slasher hooks into Lighthouse's block and attestation processing, and pushes messages into an @@ -26,9 +26,6 @@ verifies the signatures of otherwise invalid messages. When a slasher batch upda messages are filtered for relevancy, and all relevant messages are checked for slashings and written 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 developers should you encounter any bugs. - ## Configuration The slasher has several configuration options that control its functioning. diff --git a/book/src/slashing-protection.md b/book/src/slashing-protection.md index 6e2ca65b41..38348d2094 100644 --- a/book/src/slashing-protection.md +++ b/book/src/slashing-protection.md @@ -101,18 +101,6 @@ update the low watermarks for blocks and attestations. It will store only the ma for each validator, and the maximum source/target attestation. This is faster than importing all data while also being more resilient to repeated imports & stale data. -### Minification - -The exporter can be configured to minify (shrink) the data it exports by keeping only the -maximum-slot and maximum-epoch messages. Provide the `--minify=true` flag: - -``` -lighthouse account validator slashing-protection export --minify=true -``` - -This may make the file faster to import into other clients, but is unnecessary for Lighthouse to -Lighthouse transfers since v1.5.0. - ## Troubleshooting ### Misplaced Slashing Database diff --git a/book/src/ui-configuration.md b/book/src/ui-configuration.md index 1a5daa5441..31951c3c92 100644 --- a/book/src/ui-configuration.md +++ b/book/src/ui-configuration.md @@ -18,7 +18,7 @@ To enable the HTTP API for the beacon node, utilize the `--gui` CLI flag. This a 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`. +> 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`. When this is set, the validator client requires the flag `--beacon-nodes http://192.168.0.200:5052` to connect to the beacon node. 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. diff --git a/book/src/ui-faqs.md b/book/src/ui-faqs.md index 92e06270c2..77821788f6 100644 --- a/book/src/ui-faqs.md +++ b/book/src/ui-faqs.md @@ -10,7 +10,7 @@ The required Api token may be found in the default data directory of the validat 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 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`. +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`. When this is set, the validator client requires the flag `--beacon-nodes http://192.168.0.200:5052` to connect to the beacon node. 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. How can I use Siren to monitor my validators remotely when I am not at home? @@ -22,7 +22,7 @@ Most contemporary home routers provide options for VPN access in various ways. A 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: +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 @@ -55,7 +55,7 @@ If you have separate address setups for your Validator Client and Beacon Node re ## 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. +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/validator-inclusion.md b/book/src/validator-inclusion.md index cd31d78d62..f31d729449 100644 --- a/book/src/validator-inclusion.md +++ b/book/src/validator-inclusion.md @@ -56,7 +56,6 @@ The following fields are returned: able to vote) during the current epoch. - `current_epoch_target_attesting_gwei`: the total staked gwei that attested to the majority-elected Casper FFG target epoch during the current epoch. -- `previous_epoch_active_gwei`: as per `current_epoch_active_gwei`, but during the previous epoch. - `previous_epoch_target_attesting_gwei`: see `current_epoch_target_attesting_gwei`. - `previous_epoch_head_attesting_gwei`: the total staked gwei that attested to a head beacon block that is in the canonical chain. @@ -65,7 +64,7 @@ From this data you can calculate: #### Justification/Finalization Rate -`previous_epoch_target_attesting_gwei / previous_epoch_active_gwei` +`previous_epoch_target_attesting_gwei / current_epoch_active_gwei` When this value is greater than or equal to `2/3` it is possible that the beacon chain may justify and/or finalize the epoch. @@ -80,7 +79,6 @@ curl -X GET "http://localhost:5052/lighthouse/validator_inclusion/0/global" -H { "data": { "current_epoch_active_gwei": 642688000000000, - "previous_epoch_active_gwei": 642688000000000, "current_epoch_target_attesting_gwei": 366208000000000, "previous_epoch_target_attesting_gwei": 1000000000, "previous_epoch_head_attesting_gwei": 1000000000 diff --git a/book/src/validator-manager-create.md b/book/src/validator-manager-create.md index 6ba894a43c..98202d3b52 100644 --- a/book/src/validator-manager-create.md +++ b/book/src/validator-manager-create.md @@ -201,4 +201,4 @@ Duplicate validators are ignored, ignoring 0xab6e29f1b98fedfca878edce2b471f1b5ee Re-uploaded keystore 1 of 6 to the VC ``` -The guide is complete. \ No newline at end of file +The guide is complete. diff --git a/book/src/validator-manager-move.md b/book/src/validator-manager-move.md index 15089d65c5..5009e6407e 100644 --- a/book/src/validator-manager-move.md +++ b/book/src/validator-manager-move.md @@ -182,6 +182,13 @@ lighthouse \ --validators 0x9096aab771e44da149bd7c9926d6f7bb96ef465c0eeb4918be5178cd23a1deb4aec232c61d85ff329b54ed4a3bdfff3a,0x90fc4f72d898a8f01ab71242e36f4545aaf87e3887be81632bb8ba4b2ae8fb70753a62f866344d7905e9a07f5a9cdda1 ``` +> Note: If you have the `validator-monitor-auto` turned on, the source beacon node may still be reporting the attestation status of the validators that have been moved: +``` +INFO Previous epoch attestation(s) success validators: ["validator_index"], epoch: 100000, service: val_mon, service: beacon +``` +> This is fine as the validator monitor does not know that the validators have been moved (it *does not* mean that the validators have attested twice for the same slot). A restart of the beacon node will resolve this. + + 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-monitoring.md b/book/src/validator-monitoring.md index 893ec90bdd..532bd50065 100644 --- a/book/src/validator-monitoring.md +++ b/book/src/validator-monitoring.md @@ -57,6 +57,7 @@ Monitor the mainnet validators at indices `0` and `1`: ``` lighthouse bn --validator-monitor-pubkeys 0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95,0xa1d1ad0714035353258038e964ae9675dc0252ee22cea896825c01458e1807bfad2f9969338798548d9858a571f7425c ``` +> Note: The validator monitoring will stop collecting per-validator Prometheus metrics and issuing per-validator logs when the number of validators reaches 64. To continue collecting metrics and logging, use the flag `--validator-monitor-individual-tracking-threshold N` where `N` is a number greater than the number of validators to monitor. ## Observing Monitoring @@ -95,4 +96,60 @@ Jan 18 11:21:09.808 INFO Attestation included in block validator: 1, s The [`ValidatorMonitor`](https://github.com/sigp/lighthouse-metrics/blob/master/dashboards/ValidatorMonitor.json) -dashboard contains all/most of the metrics exposed via the validator monitor. +dashboard contains most of the metrics exposed via the validator monitor. + +### Attestation Simulator Metrics + +Lighthouse v4.6.0 introduces a new feature to track the performance of a beacon node. This feature internally simulates an attestation for each slot, and outputs a hit or miss for the head, target and source votes. The attestation simulator is turned on automatically (even when there are no validators) and prints logs in the debug level. + +> Note: The simulated attestations are never published to the network, so the simulator does not reflect the attestation performance of a validator. + +The attestation simulation prints the following logs when simulating an attestation: + +``` +DEBG Simulating unagg. attestation production, service: beacon, module: beacon_chain::attestation_simulator:39 +DEBG Produce unagg. attestation, attestation_target: 0x59fc…1a67, attestation_source: 0xc4c5…d414, service: beacon, module: beacon_chain::attestation_simulator:87 +``` + +When the simulated attestation has completed, it prints a log that specifies if the head, target and source votes are hit. An example of a log when all head, target and source are hit: + +``` +DEBG Simulated attestation evaluated, head_hit: true, target_hit: true, source_hit: true, attestation_slot: Slot(1132616), attestation_head: 0x61367335c30b0f114582fe298724b75b56ae9372bdc6e7ce5d735db68efbdd5f, attestation_target: 0xaab25a6d01748cf4528e952666558317b35874074632550c37d935ca2ec63c23, attestation_source: 0x13ccbf8978896c43027013972427ee7ce02b2bb9b898dbb264b870df9288c1e7, service: val_mon, service: beacon, module: beacon_chain::validator_monitor:2051 +``` + +An example of a log when the head is missed: +``` +DEBG Simulated attestation evaluated, head_hit: false, target_hit: true, source_hit: true, attestation_slot: Slot(1132623), attestation_head: 0x1c0e53c6ace8d0ff57f4a963e4460fe1c030b37bf1c76f19e40928dc2e214c59, attestation_target: 0xaab25a6d01748cf4528e952666558317b35874074632550c37d935ca2ec63c23, attestation_source: 0x13ccbf8978896c43027013972427ee7ce02b2bb9b898dbb264b870df9288c1e7, service: val_mon, service: beacon, module: beacon_chain::validator_monitor:2051 +``` + + +With `--metrics` enabled on the beacon node, the following metrics will be recorded: + +``` +validator_monitor_attestation_simulator_head_attester_hit_total +validator_monitor_attestation_simulator_head_attester_miss_total +validator_monitor_attestation_simulator_target_attester_hit_total +validator_monitor_attestation_simulator_target_attester_miss_total +validator_monitor_attestation_simulator_source_attester_hit_total +validator_monitor_attestation_simulator_source_attester_miss_total +``` + +A grafana dashboard to view the metrics for attestation simulator is available [here](https://github.com/sigp/lighthouse-metrics/blob/master/dashboards/AttestationSimulator.json). + +The attestation simulator provides an insight into the attestation performance of a beacon node. It can be used as an indication of how expediently the beacon node has completed importing blocks within the 4s time frame for an attestation to be made. + +The attestation simulator *does not* consider: +- the latency between the beacon node and the validator client +- the potential delays when publishing the attestation to the network + +which are critical factors to consider when evaluating the attestation performance of a validator. + +Assuming the above factors are ignored (no delays between beacon node and validator client, and in publishing the attestation to the network): + +1. If the attestation simulator says that all votes are hit, it means that if the beacon node were to publish the attestation for this slot, the validator should receive the rewards for the head, target and source votes. + +1. If the attestation simulator says that the one or more votes are missed, it means that there is a delay in importing the block. The delay could be due to slowness in processing the block (e.g., due to a slow CPU) or that the block is arriving late (e.g., the proposer publishes the block late). If the beacon node were to publish the attestation for this slot, the validator will miss one or more votes (e.g., the head vote). + + + + diff --git a/book/src/validator-web3signer.md b/book/src/validator-web3signer.md index 00ef9a6b59..6a518af3cf 100644 --- a/book/src/validator-web3signer.md +++ b/book/src/validator-web3signer.md @@ -30,8 +30,7 @@ or effectiveness. ## Usage A remote signing validator is added to Lighthouse in much the same way as one that uses a local -keystore, via the [`validator_definitions.yml`](./validator-management.md) file or via the `POST -/lighthouse/validators/web3signer` API endpoint. +keystore, via the [`validator_definitions.yml`](./validator-management.md) file or via the [`POST /lighthouse/validators/web3signer`](./api-vc-endpoints.md#post-lighthousevalidatorsweb3signer) API endpoint. Here is an example of a `validator_definitions.yml` file containing one validator which uses a remote signer: diff --git a/book/src/voluntary-exit.md b/book/src/voluntary-exit.md index 8d61c1770d..4ec4837fea 100644 --- a/book/src/voluntary-exit.md +++ b/book/src/voluntary-exit.md @@ -16,7 +16,7 @@ In order to initiate an exit, users can use the `lighthouse account validator ex - The `--keystore` flag is used to specify the path to the EIP-2335 voting keystore for the validator. The path should point directly to the validator key `.json` file, _not_ the folder containing the `.json` file. -- The `--beacon-node` flag is used to specify a beacon chain HTTP endpoint that confirms to the [Beacon Node API](https://ethereum.github.io/beacon-APIs/) specifications. That beacon node will be used to validate and propagate the voluntary exit. The default value for this flag is `http://localhost:5052`. +- The `--beacon-node` flag is used to specify a beacon chain HTTP endpoint that conforms to the [Beacon Node API](https://ethereum.github.io/beacon-APIs/) specifications. That beacon node will be used to validate and propagate the voluntary exit. The default value for this flag is `http://localhost:5052`. - The `--network` flag is used to specify the network (default is `mainnet`). @@ -30,13 +30,13 @@ The exit phrase is the following: -Below is an example for initiating a voluntary exit on the Goerli testnet. +Below is an example for initiating a voluntary exit on the Holesky testnet. ``` -$ lighthouse --network goerli account validator exit --keystore /path/to/keystore --beacon-node http://localhost:5052 +$ lighthouse --network holesky account validator exit --keystore /path/to/keystore --beacon-node http://localhost:5052 Running account manager for Prater network -validator-dir path: ~/.lighthouse/goerli/validators +validator-dir path: ~/.lighthouse/holesky/validators Enter the keystore password for validator in 0xabcd diff --git a/boot_node/Cargo.toml b/boot_node/Cargo.toml index bdf01914ad..6cf62e0430 100644 --- a/boot_node/Cargo.toml +++ b/boot_node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "boot_node" -version = "4.6.0" +version = "5.1.3" authors = ["Sigma Prime "] edition = { workspace = true } diff --git a/boot_node/src/config.rs b/boot_node/src/config.rs index 6fb1ea9bf5..a9c8950532 100644 --- a/boot_node/src/config.rs +++ b/boot_node/src/config.rs @@ -14,16 +14,16 @@ 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 struct BootNodeConfig { // TODO: Generalise to multiaddr pub boot_nodes: Vec, pub local_enr: Enr, pub local_key: CombinedKey, pub discv5_config: discv5::Config, - phantom: PhantomData, + phantom: PhantomData, } -impl BootNodeConfig { +impl BootNodeConfig { pub async fn new( matches: &ArgMatches<'_>, eth2_network_config: &Eth2NetworkConfig, @@ -94,7 +94,7 @@ impl BootNodeConfig { } else { // build the enr_fork_id and add it to the local_enr if it exists let enr_fork = { - let spec = eth2_network_config.chain_spec::()?; + let spec = eth2_network_config.chain_spec::()?; let genesis_state_url: Option = clap_utils::parse_optional(matches, "genesis-state-url")?; @@ -104,14 +104,14 @@ impl BootNodeConfig { 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? + .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::( + let enr_fork = spec.enr_fork_id::( types::Slot::from(0u64), genesis_state.genesis_validators_root(), ); @@ -188,7 +188,7 @@ pub struct BootNodeConfigSerialization { impl BootNodeConfigSerialization { /// Returns a `BootNodeConfigSerialization` obtained from copying resp. cloning the /// relevant fields of `config` - pub fn from_config_ref(config: &BootNodeConfig) -> Self { + pub fn from_config_ref(config: &BootNodeConfig) -> Self { let BootNodeConfig { boot_nodes, local_enr, diff --git a/boot_node/src/lib.rs b/boot_node/src/lib.rs index 0421ce2684..e707dc14f7 100644 --- a/boot_node/src/lib.rs +++ b/boot_node/src/lib.rs @@ -66,7 +66,7 @@ pub fn run( } } -fn main( +fn main( lh_matches: &ArgMatches<'_>, bn_matches: &ArgMatches<'_>, eth2_network_config: &Eth2NetworkConfig, @@ -79,7 +79,7 @@ fn main( .map_err(|e| format!("Failed to build runtime: {}", e))?; // Run the boot node - runtime.block_on(server::run::( + runtime.block_on(server::run::( lh_matches, bn_matches, eth2_network_config, diff --git a/boot_node/src/server.rs b/boot_node/src/server.rs index 8260038a0b..b6bdd148f4 100644 --- a/boot_node/src/server.rs +++ b/boot_node/src/server.rs @@ -11,21 +11,21 @@ use lighthouse_network::{ use slog::info; use types::EthSpec; -pub async fn run( +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?; + 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>( + clap_utils::check_dump_configs::<_, E>( lh_matches, &config_sz, - ð2_network_config.chain_spec::()?, + ð2_network_config.chain_spec::()?, )?; if lh_matches.is_present("immediate-shutdown") { diff --git a/common/deposit_contract/src/lib.rs b/common/deposit_contract/src/lib.rs index 2a9f985d5f..785b952213 100644 --- a/common/deposit_contract/src/lib.rs +++ b/common/deposit_contract/src/lib.rs @@ -86,8 +86,8 @@ pub fn decode_eth1_tx_data( mod tests { use super::*; use types::{ - test_utils::generate_deterministic_keypair, ChainSpec, EthSpec, Hash256, Keypair, - MinimalEthSpec, Signature, + test_utils::generate_deterministic_keypair, ChainSpec, EthSpec, Keypair, MinimalEthSpec, + Signature, }; type E = MinimalEthSpec; diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index f79bb7462a..cd53aa7300 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -30,10 +30,8 @@ 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::future::Future; -use std::iter::Iterator; use std::path::PathBuf; use std::time::Duration; use store::fork_versioned_response::ExecutionOptimisticFinalizedForkVersionedResponse; @@ -841,9 +839,9 @@ impl BeaconNodeHttpClient { /// `POST beacon/blocks` /// /// Returns `Ok(None)` on a 404 error. - pub async fn post_beacon_blocks( + pub async fn post_beacon_blocks( &self, - block_contents: &PublishBlockRequest, + block_contents: &PublishBlockRequest, ) -> Result<(), Error> { let mut path = self.eth_path(V1)?; @@ -861,9 +859,9 @@ impl BeaconNodeHttpClient { /// `POST beacon/blocks` /// /// Returns `Ok(None)` on a 404 error. - pub async fn post_beacon_blocks_ssz( + pub async fn post_beacon_blocks_ssz( &self, - block_contents: &PublishBlockRequest, + block_contents: &PublishBlockRequest, ) -> Result<(), Error> { let mut path = self.eth_path(V1)?; @@ -886,9 +884,9 @@ impl BeaconNodeHttpClient { /// `POST beacon/blinded_blocks` /// /// Returns `Ok(None)` on a 404 error. - pub async fn post_beacon_blinded_blocks( + pub async fn post_beacon_blinded_blocks( &self, - block: &SignedBlindedBeaconBlock, + block: &SignedBlindedBeaconBlock, ) -> Result<(), Error> { let mut path = self.eth_path(V1)?; @@ -906,9 +904,9 @@ impl BeaconNodeHttpClient { /// `POST beacon/blinded_blocks` /// /// Returns `Ok(None)` on a 404 error. - pub async fn post_beacon_blinded_blocks_ssz( + pub async fn post_beacon_blinded_blocks_ssz( &self, - block: &SignedBlindedBeaconBlock, + block: &SignedBlindedBeaconBlock, ) -> Result<(), Error> { let mut path = self.eth_path(V1)?; @@ -965,9 +963,9 @@ impl BeaconNodeHttpClient { } /// `POST v2/beacon/blocks` - pub async fn post_beacon_blocks_v2( + pub async fn post_beacon_blocks_v2( &self, - block_contents: &PublishBlockRequest, + block_contents: &PublishBlockRequest, validation_level: Option, ) -> Result<(), Error> { self.post_generic_with_consensus_version( @@ -982,9 +980,9 @@ impl BeaconNodeHttpClient { } /// `POST v2/beacon/blocks` - pub async fn post_beacon_blocks_v2_ssz( + pub async fn post_beacon_blocks_v2_ssz( &self, - block_contents: &PublishBlockRequest, + block_contents: &PublishBlockRequest, validation_level: Option, ) -> Result<(), Error> { self.post_generic_with_consensus_version_and_ssz_body( @@ -999,9 +997,9 @@ impl BeaconNodeHttpClient { } /// `POST v2/beacon/blinded_blocks` - pub async fn post_beacon_blinded_blocks_v2( + pub async fn post_beacon_blinded_blocks_v2( &self, - signed_block: &SignedBlindedBeaconBlock, + signed_block: &SignedBlindedBeaconBlock, validation_level: Option, ) -> Result<(), Error> { self.post_generic_with_consensus_version( @@ -1016,9 +1014,9 @@ impl BeaconNodeHttpClient { } /// `POST v2/beacon/blinded_blocks` - pub async fn post_beacon_blinded_blocks_v2_ssz( + pub async fn post_beacon_blinded_blocks_v2_ssz( &self, - signed_block: &SignedBlindedBeaconBlock, + signed_block: &SignedBlindedBeaconBlock, validation_level: Option, ) -> Result<(), Error> { self.post_generic_with_consensus_version_and_ssz_body( @@ -1068,11 +1066,11 @@ impl BeaconNodeHttpClient { /// `GET v2/beacon/blocks` /// /// Returns `Ok(None)` on a 404 error. - pub async fn get_beacon_blocks( + pub async fn get_beacon_blocks( &self, block_id: BlockId, ) -> Result< - Option>>, + Option>>, Error, > { let path = self.get_beacon_blocks_path(block_id)?; @@ -1086,11 +1084,11 @@ impl BeaconNodeHttpClient { /// `GET v1/beacon/blob_sidecars/{block_id}` /// /// Returns `Ok(None)` on a 404 error. - pub async fn get_blobs( + pub async fn get_blobs( &self, block_id: BlockId, indices: Option<&[u64]>, - ) -> Result>>, Error> { + ) -> Result>>, Error> { let mut path = self.get_blobs_path(block_id)?; if let Some(indices) = indices { let indices_string = indices @@ -1101,6 +1099,7 @@ impl BeaconNodeHttpClient { path.query_pairs_mut() .append_pair("indices", &indices_string); } + let Some(response) = self.get_response(path, |b| b).await.optional()? else { return Ok(None); }; @@ -1111,11 +1110,11 @@ impl BeaconNodeHttpClient { /// `GET v1/beacon/blinded_blocks/{block_id}` /// /// Returns `Ok(None)` on a 404 error. - pub async fn get_beacon_blinded_blocks( + pub async fn get_beacon_blinded_blocks( &self, block_id: BlockId, ) -> Result< - Option>>, + Option>>, Error, > { let path = self.get_beacon_blinded_blocks_path(block_id)?; @@ -1129,10 +1128,10 @@ impl BeaconNodeHttpClient { /// `GET v1/beacon/blocks` (LEGACY) /// /// Returns `Ok(None)` on a 404 error. - pub async fn get_beacon_blocks_v1( + pub async fn get_beacon_blocks_v1( &self, block_id: BlockId, - ) -> Result>>, Error> { + ) -> Result>>, Error> { let mut path = self.eth_path(V1)?; path.path_segments_mut() @@ -1147,11 +1146,11 @@ impl BeaconNodeHttpClient { /// `GET beacon/blocks` as SSZ /// /// Returns `Ok(None)` on a 404 error. - pub async fn get_beacon_blocks_ssz( + pub async fn get_beacon_blocks_ssz( &self, block_id: BlockId, spec: &ChainSpec, - ) -> Result>, Error> { + ) -> Result>, Error> { let path = self.get_beacon_blocks_path(block_id)?; self.get_bytes_opt_accept_header(path, Accept::Ssz, self.timeouts.get_beacon_blocks_ssz) @@ -1163,11 +1162,11 @@ impl BeaconNodeHttpClient { /// `GET beacon/blinded_blocks/{block_id}` as SSZ /// /// Returns `Ok(None)` on a 404 error. - pub async fn get_beacon_blinded_blocks_ssz( + pub async fn get_beacon_blinded_blocks_ssz( &self, block_id: BlockId, spec: &ChainSpec, - ) -> Result>, Error> { + ) -> Result>, Error> { let path = self.get_beacon_blinded_blocks_path(block_id)?; self.get_bytes_opt_accept_header(path, Accept::Ssz, self.timeouts.get_beacon_blocks_ssz) @@ -1200,10 +1199,10 @@ impl BeaconNodeHttpClient { /// `GET beacon/blocks/{block_id}/attestations` /// /// Returns `Ok(None)` on a 404 error. - pub async fn get_beacon_blocks_attestations( + pub async fn get_beacon_blocks_attestations( &self, block_id: BlockId, - ) -> Result>>>, Error> { + ) -> Result>>>, Error> { let mut path = self.eth_path(V1)?; path.path_segments_mut() @@ -1217,9 +1216,9 @@ impl BeaconNodeHttpClient { } /// `POST beacon/pool/attestations` - pub async fn post_beacon_pool_attestations( + pub async fn post_beacon_pool_attestations( &self, - attestations: &[Attestation], + attestations: &[Attestation], ) -> Result<(), Error> { let mut path = self.eth_path(V1)?; @@ -1236,11 +1235,11 @@ impl BeaconNodeHttpClient { } /// `GET beacon/pool/attestations?slot,committee_index` - pub async fn get_beacon_pool_attestations( + pub async fn get_beacon_pool_attestations( &self, slot: Option, committee_index: Option, - ) -> Result>>, Error> { + ) -> Result>>, Error> { let mut path = self.eth_path(V1)?; path.path_segments_mut() @@ -1263,9 +1262,9 @@ impl BeaconNodeHttpClient { } /// `POST beacon/pool/attester_slashings` - pub async fn post_beacon_pool_attester_slashings( + pub async fn post_beacon_pool_attester_slashings( &self, - slashing: &AttesterSlashing, + slashing: &AttesterSlashing, ) -> Result<(), Error> { let mut path = self.eth_path(V1)?; @@ -1282,9 +1281,9 @@ impl BeaconNodeHttpClient { } /// `GET beacon/pool/attester_slashings` - pub async fn get_beacon_pool_attester_slashings( + pub async fn get_beacon_pool_attester_slashings( &self, - ) -> Result>>, Error> { + ) -> Result>>, Error> { let mut path = self.eth_path(V1)?; path.path_segments_mut() @@ -1480,9 +1479,9 @@ impl BeaconNodeHttpClient { } /// `POST validator/contribution_and_proofs` - pub async fn post_validator_contribution_and_proofs( + pub async fn post_validator_contribution_and_proofs( &self, - signed_contributions: &[SignedContributionAndProof], + signed_contributions: &[SignedContributionAndProof], ) -> Result<(), Error> { let mut path = self.eth_path(V1)?; @@ -1704,10 +1703,10 @@ impl BeaconNodeHttpClient { } /// `GET v2/debug/beacon/states/{state_id}` - pub async fn get_debug_beacon_states( + pub async fn get_debug_beacon_states( &self, state_id: StateId, - ) -> Result>>, Error> + ) -> Result>>, Error> { let path = self.get_debug_beacon_states_path(state_id)?; self.get_opt(path).await @@ -1715,11 +1714,11 @@ impl BeaconNodeHttpClient { /// `GET debug/beacon/states/{state_id}` /// `-H "accept: application/octet-stream"` - pub async fn get_debug_beacon_states_ssz( + pub async fn get_debug_beacon_states_ssz( &self, state_id: StateId, spec: &ChainSpec, - ) -> Result>, Error> { + ) -> Result>, Error> { let path = self.get_debug_beacon_states_path(state_id)?; self.get_bytes_opt_accept_header(path, Accept::Ssz, self.timeouts.get_debug_beacon_states) @@ -1789,33 +1788,33 @@ impl BeaconNodeHttpClient { } /// `GET v2/validator/blocks/{slot}` - pub async fn get_validator_blocks( + pub async fn get_validator_blocks( &self, slot: Slot, randao_reveal: &SignatureBytes, graffiti: Option<&Graffiti>, - ) -> Result>, Error> { + ) -> Result>, Error> { self.get_validator_blocks_modular(slot, randao_reveal, graffiti, SkipRandaoVerification::No) .await } /// `GET v2/validator/blocks/{slot}` - pub async fn get_validator_blocks_modular( + pub async fn get_validator_blocks_modular( &self, slot: Slot, randao_reveal: &SignatureBytes, graffiti: Option<&Graffiti>, skip_randao_verification: SkipRandaoVerification, - ) -> Result>, Error> { + ) -> Result>, Error> { let path = self - .get_validator_blocks_path::(slot, randao_reveal, graffiti, skip_randao_verification) + .get_validator_blocks_path::(slot, randao_reveal, graffiti, skip_randao_verification) .await?; self.get(path).await } /// returns `GET v2/validator/blocks/{slot}` URL path - pub async fn get_validator_blocks_path( + pub async fn get_validator_blocks_path( &self, slot: Slot, randao_reveal: &SignatureBytes, @@ -1885,13 +1884,13 @@ impl BeaconNodeHttpClient { } /// `GET v3/validator/blocks/{slot}` - pub async fn get_validator_blocks_v3( + pub async fn get_validator_blocks_v3( &self, slot: Slot, randao_reveal: &SignatureBytes, graffiti: Option<&Graffiti>, builder_booster_factor: Option, - ) -> Result<(JsonProduceBlockV3Response, ProduceBlockV3Metadata), Error> { + ) -> Result<(JsonProduceBlockV3Response, ProduceBlockV3Metadata), Error> { self.get_validator_blocks_v3_modular( slot, randao_reveal, @@ -1903,14 +1902,14 @@ impl BeaconNodeHttpClient { } /// `GET v3/validator/blocks/{slot}` - pub async fn get_validator_blocks_v3_modular( + pub async fn get_validator_blocks_v3_modular( &self, slot: Slot, randao_reveal: &SignatureBytes, graffiti: Option<&Graffiti>, skip_randao_verification: SkipRandaoVerification, builder_booster_factor: Option, - ) -> Result<(JsonProduceBlockV3Response, ProduceBlockV3Metadata), Error> { + ) -> Result<(JsonProduceBlockV3Response, ProduceBlockV3Metadata), Error> { let path = self .get_validator_blocks_v3_path( slot, @@ -1931,14 +1930,14 @@ impl BeaconNodeHttpClient { .map_err(Error::InvalidHeaders)?; if header_metadata.execution_payload_blinded { let blinded_response = response - .json::, + .json::, ProduceBlockV3Metadata>>() .await? .map_data(ProduceBlockV3Response::Blinded); Ok((blinded_response, header_metadata)) } else { let full_block_response= response - .json::, + .json::, ProduceBlockV3Metadata>>() .await? .map_data(ProduceBlockV3Response::Full); @@ -1954,14 +1953,14 @@ impl BeaconNodeHttpClient { } /// `GET v3/validator/blocks/{slot}` in ssz format - pub async fn get_validator_blocks_v3_ssz( + pub async fn get_validator_blocks_v3_ssz( &self, slot: Slot, randao_reveal: &SignatureBytes, graffiti: Option<&Graffiti>, builder_booster_factor: Option, - ) -> Result<(ProduceBlockV3Response, ProduceBlockV3Metadata), Error> { - self.get_validator_blocks_v3_modular_ssz::( + ) -> Result<(ProduceBlockV3Response, ProduceBlockV3Metadata), Error> { + self.get_validator_blocks_v3_modular_ssz::( slot, randao_reveal, graffiti, @@ -1972,14 +1971,14 @@ impl BeaconNodeHttpClient { } /// `GET v3/validator/blocks/{slot}` in ssz format - pub async fn get_validator_blocks_v3_modular_ssz( + pub async fn get_validator_blocks_v3_modular_ssz( &self, slot: Slot, randao_reveal: &SignatureBytes, graffiti: Option<&Graffiti>, skip_randao_verification: SkipRandaoVerification, builder_booster_factor: Option, - ) -> Result<(ProduceBlockV3Response, ProduceBlockV3Metadata), Error> { + ) -> Result<(ProduceBlockV3Response, ProduceBlockV3Metadata), Error> { let path = self .get_validator_blocks_v3_path( slot, @@ -2030,13 +2029,13 @@ impl BeaconNodeHttpClient { } /// `GET v2/validator/blocks/{slot}` in ssz format - pub async fn get_validator_blocks_ssz( + pub async fn get_validator_blocks_ssz( &self, slot: Slot, randao_reveal: &SignatureBytes, graffiti: Option<&Graffiti>, ) -> Result>, Error> { - self.get_validator_blocks_modular_ssz::( + self.get_validator_blocks_modular_ssz::( slot, randao_reveal, graffiti, @@ -2046,7 +2045,7 @@ impl BeaconNodeHttpClient { } /// `GET v2/validator/blocks/{slot}` in ssz format - pub async fn get_validator_blocks_modular_ssz( + pub async fn get_validator_blocks_modular_ssz( &self, slot: Slot, randao_reveal: &SignatureBytes, @@ -2054,7 +2053,7 @@ impl BeaconNodeHttpClient { skip_randao_verification: SkipRandaoVerification, ) -> Result>, Error> { let path = self - .get_validator_blocks_path::(slot, randao_reveal, graffiti, skip_randao_verification) + .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) @@ -2062,12 +2061,12 @@ impl BeaconNodeHttpClient { } /// `GET v2/validator/blinded_blocks/{slot}` - pub async fn get_validator_blinded_blocks( + pub async fn get_validator_blinded_blocks( &self, slot: Slot, randao_reveal: &SignatureBytes, graffiti: Option<&Graffiti>, - ) -> Result>, Error> { + ) -> Result>, Error> { self.get_validator_blinded_blocks_modular( slot, randao_reveal, @@ -2078,7 +2077,7 @@ impl BeaconNodeHttpClient { } /// returns `GET v1/validator/blinded_blocks/{slot}` URL path - pub async fn get_validator_blinded_blocks_path( + pub async fn get_validator_blinded_blocks_path( &self, slot: Slot, randao_reveal: &SignatureBytes, @@ -2110,15 +2109,15 @@ impl BeaconNodeHttpClient { } /// `GET v1/validator/blinded_blocks/{slot}` - pub async fn get_validator_blinded_blocks_modular( + pub async fn get_validator_blinded_blocks_modular( &self, slot: Slot, randao_reveal: &SignatureBytes, graffiti: Option<&Graffiti>, skip_randao_verification: SkipRandaoVerification, - ) -> Result>, Error> { + ) -> Result>, Error> { let path = self - .get_validator_blinded_blocks_path::( + .get_validator_blinded_blocks_path::( slot, randao_reveal, graffiti, @@ -2130,13 +2129,13 @@ impl BeaconNodeHttpClient { } /// `GET v2/validator/blinded_blocks/{slot}` in ssz format - pub async fn get_validator_blinded_blocks_ssz( + 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::( + self.get_validator_blinded_blocks_modular_ssz::( slot, randao_reveal, graffiti, @@ -2145,7 +2144,7 @@ impl BeaconNodeHttpClient { .await } - pub async fn get_validator_blinded_blocks_modular_ssz( + pub async fn get_validator_blinded_blocks_modular_ssz( &self, slot: Slot, randao_reveal: &SignatureBytes, @@ -2153,7 +2152,7 @@ impl BeaconNodeHttpClient { skip_randao_verification: SkipRandaoVerification, ) -> Result>, Error> { let path = self - .get_validator_blinded_blocks_path::( + .get_validator_blinded_blocks_path::( slot, randao_reveal, graffiti, @@ -2186,11 +2185,11 @@ impl BeaconNodeHttpClient { } /// `GET validator/aggregate_attestation?slot,attestation_data_root` - pub async fn get_validator_aggregate_attestation( + pub async fn get_validator_aggregate_attestation( &self, slot: Slot, attestation_data_root: Hash256, - ) -> Result>>, Error> { + ) -> Result>>, Error> { let mut path = self.eth_path(V1)?; path.path_segments_mut() @@ -2210,10 +2209,10 @@ impl BeaconNodeHttpClient { } /// `GET validator/sync_committee_contribution` - pub async fn get_validator_sync_committee_contribution( + pub async fn get_validator_sync_committee_contribution( &self, sync_committee_data: &SyncContributionData, - ) -> Result>>, Error> { + ) -> Result>>, Error> { let mut path = self.eth_path(V1)?; path.path_segments_mut() @@ -2263,7 +2262,7 @@ impl BeaconNodeHttpClient { pub async fn post_validator_liveness_epoch( &self, epoch: Epoch, - indices: &Vec, + indices: &[u64], ) -> Result>, Error> { let mut path = self.eth_path(V1)?; @@ -2273,8 +2272,12 @@ impl BeaconNodeHttpClient { .push("liveness") .push(&epoch.to_string()); - self.post_with_timeout_and_response(path, indices, self.timeouts.liveness) - .await + self.post_with_timeout_and_response( + path, + &ValidatorIndexDataRef(indices), + self.timeouts.liveness, + ) + .await } /// `POST validator/duties/attester/{epoch}` @@ -2301,9 +2304,9 @@ impl BeaconNodeHttpClient { } /// `POST validator/aggregate_and_proofs` - pub async fn post_validator_aggregate_and_proof( + pub async fn post_validator_aggregate_and_proof( &self, - aggregates: &[SignedAggregateAndProof], + aggregates: &[SignedAggregateAndProof], ) -> Result<(), Error> { let mut path = self.eth_path(V1)?; @@ -2353,10 +2356,10 @@ impl BeaconNodeHttpClient { } /// `GET events?topics` - pub async fn get_events( + pub async fn get_events( &self, topic: &[EventTopic], - ) -> Result, Error>>, Error> { + ) -> Result, Error>>, Error> { let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? diff --git a/common/eth2/src/lighthouse.rs b/common/eth2/src/lighthouse.rs index 538f1a42d1..e978d92245 100644 --- a/common/eth2/src/lighthouse.rs +++ b/common/eth2/src/lighthouse.rs @@ -39,12 +39,12 @@ four_byte_option_impl!(four_byte_option_hash256, Hash256); /// Information returned by `peers` and `connected_peers`. // TODO: this should be deserializable.. #[derive(Debug, Clone, Serialize)] -#[serde(bound = "T: EthSpec")] -pub struct Peer { +#[serde(bound = "E: EthSpec")] +pub struct Peer { /// The Peer's ID pub peer_id: String, /// The PeerInfo associated with the peer. - pub peer_info: PeerInfo, + pub peer_info: PeerInfo, } /// The results of validators voting during an epoch. @@ -54,8 +54,6 @@ pub struct Peer { pub struct GlobalValidatorInclusionData { /// The total effective balance of all active validators during the _current_ epoch. pub current_epoch_active_gwei: u64, - /// The total effective balance of all active validators during the _previous_ epoch. - pub previous_epoch_active_gwei: u64, /// The total effective balance of all validators who attested during the _current_ epoch and /// agreed with the state about the beacon block at the first slot of the _current_ epoch. pub current_epoch_target_attesting_gwei: u64, diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index a301055f34..feff1d391a 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -12,7 +12,6 @@ use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; use ssz::{Decode, DecodeError}; use ssz_derive::{Decode, Encode}; -use std::convert::TryFrom; use std::fmt::{self, Display}; use std::str::{from_utf8, FromStr}; use std::sync::Arc; @@ -282,7 +281,7 @@ pub struct FinalityCheckpointsData { pub finalized: Checkpoint, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] #[serde(into = "String")] #[serde(try_from = "std::borrow::Cow")] pub enum ValidatorId { @@ -1033,6 +1032,9 @@ impl ForkVersionDeserialize for SsePayloadAttributes { ForkName::Deneb => serde_json::from_value(value) .map(Self::V3) .map_err(serde::de::Error::custom), + ForkName::Electra => serde_json::from_value(value) + .map(Self::V3) + .map_err(serde::de::Error::custom), ForkName::Base | ForkName::Altair => Err(serde::de::Error::custom(format!( "SsePayloadAttributes deserialization for {fork_name} not implemented" ))), @@ -1062,25 +1064,25 @@ impl ForkVersionDeserialize for SseExtendedPayloadAttributes { } #[derive(PartialEq, Debug, Serialize, Clone)] -#[serde(bound = "T: EthSpec", untagged)] -pub enum EventKind { - Attestation(Box>), +#[serde(bound = "E: EthSpec", untagged)] +pub enum EventKind { + Attestation(Box>), Block(SseBlock), BlobSidecar(SseBlobSidecar), FinalizedCheckpoint(SseFinalizedCheckpoint), Head(SseHead), VoluntaryExit(SignedVoluntaryExit), ChainReorg(SseChainReorg), - ContributionAndProof(Box>), + ContributionAndProof(Box>), LateHead(SseLateHead), - LightClientFinalityUpdate(Box>), - LightClientOptimisticUpdate(Box>), + LightClientFinalityUpdate(Box>), + LightClientOptimisticUpdate(Box>), #[cfg(feature = "lighthouse")] BlockReward(BlockReward), PayloadAttributes(VersionedSsePayloadAttributes), } -impl EventKind { +impl EventKind { pub fn topic_name(&self) -> &str { match self { EventKind::Head(_) => "head", @@ -1531,16 +1533,16 @@ pub type JsonProduceBlockV3Response = /// A wrapper over a [`BeaconBlock`] or a [`BlockContents`]. #[derive(Debug, Encode, Serialize, Deserialize)] #[serde(untagged)] -#[serde(bound = "T: EthSpec")] +#[serde(bound = "E: EthSpec")] #[ssz(enum_behaviour = "transparent")] -pub enum FullBlockContents { +pub enum FullBlockContents { /// This is a full deneb variant with block and blobs. - BlockContents(BlockContents), + BlockContents(BlockContents), /// This variant is for all pre-deneb full blocks. - Block(BeaconBlock), + Block(BeaconBlock), } -pub type BlockContentsTuple = (BeaconBlock, Option<(KzgProofs, BlobsList)>); +pub type BlockContentsTuple = (BeaconBlock, Option<(KzgProofs, BlobsList)>); // This value should never be used fn dummy_consensus_version() -> ForkName { @@ -1564,8 +1566,8 @@ pub struct ProduceBlockV3Metadata { pub consensus_block_value: Uint256, } -impl FullBlockContents { - pub fn new(block: BeaconBlock, blob_data: Option<(KzgProofs, BlobsList)>) -> Self { +impl FullBlockContents { + pub fn new(block: BeaconBlock, blob_data: Option<(KzgProofs, BlobsList)>) -> Self { match blob_data { Some((kzg_proofs, blobs)) => Self::BlockContents(BlockContents { block, @@ -1586,7 +1588,7 @@ impl FullBlockContents { expected: slot_len, })?; let slot = Slot::from_ssz_bytes(slot_bytes)?; - let fork_at_slot = spec.fork_name_at_slot::(slot); + let fork_at_slot = spec.fork_name_at_slot::(slot); Self::from_ssz_bytes_for_fork(bytes, fork_at_slot) } @@ -1600,12 +1602,12 @@ impl FullBlockContents { BeaconBlock::from_ssz_bytes_for_fork(bytes, fork_name) .map(|block| FullBlockContents::Block(block)) } - ForkName::Deneb => { + ForkName::Deneb | ForkName::Electra => { let mut builder = ssz::SszDecoderBuilder::new(bytes); builder.register_anonymous_variable_length_item()?; - builder.register_type::>()?; - builder.register_type::>()?; + builder.register_type::>()?; + builder.register_type::>()?; let mut decoder = builder.build()?; let block = decoder.decode_next_with(|bytes| { @@ -1619,14 +1621,14 @@ impl FullBlockContents { } } - pub fn block(&self) -> &BeaconBlock { + pub fn block(&self) -> &BeaconBlock { match self { FullBlockContents::BlockContents(block_and_sidecars) => &block_and_sidecars.block, FullBlockContents::Block(block) => block, } } - pub fn deconstruct(self) -> BlockContentsTuple { + pub fn deconstruct(self) -> BlockContentsTuple { match self { FullBlockContents::BlockContents(block_and_sidecars) => ( block_and_sidecars.block, @@ -1643,14 +1645,14 @@ impl FullBlockContents { fork: &Fork, genesis_validators_root: Hash256, spec: &ChainSpec, - ) -> PublishBlockRequest { + ) -> PublishBlockRequest { let (block, maybe_blobs) = self.deconstruct(); let signed_block = block.sign(secret_key, fork, genesis_validators_root, spec); PublishBlockRequest::new(Arc::new(signed_block), maybe_blobs) } } -impl ForkVersionDeserialize for FullBlockContents { +impl ForkVersionDeserialize for FullBlockContents { fn deserialize_by_fork<'de, D: serde::Deserializer<'de>>( value: serde_json::value::Value, fork_name: ForkName, @@ -1661,15 +1663,15 @@ impl ForkVersionDeserialize for FullBlockContents { BeaconBlock::deserialize_by_fork::<'de, D>(value, fork_name)?, )) } - ForkName::Deneb => Ok(FullBlockContents::BlockContents( + ForkName::Deneb | ForkName::Electra => Ok(FullBlockContents::BlockContents( BlockContents::deserialize_by_fork::<'de, D>(value, fork_name)?, )), } } } -impl Into> for FullBlockContents { - fn into(self) -> BeaconBlock { +impl Into> for FullBlockContents { + fn into(self) -> BeaconBlock { match self { Self::BlockContents(block_and_sidecars) => block_and_sidecars.block, Self::Block(block) => block, @@ -1677,9 +1679,9 @@ impl Into> for FullBlockContents { } } -pub type SignedBlockContentsTuple = ( - Arc>, - Option<(KzgProofs, BlobsList)>, +pub type SignedBlockContentsTuple = ( + Arc>, + Option<(KzgProofs, BlobsList)>, ); fn parse_required_header( @@ -1710,12 +1712,12 @@ impl TryFrom<&HeaderMap> for ProduceBlockV3Metadata { })?; let execution_payload_value = parse_required_header(headers, EXECUTION_PAYLOAD_VALUE_HEADER, |s| { - s.parse::() + Uint256::from_dec_str(s) .map_err(|e| format!("invalid {EXECUTION_PAYLOAD_VALUE_HEADER}: {e:?}")) })?; let consensus_block_value = parse_required_header(headers, CONSENSUS_BLOCK_VALUE_HEADER, |s| { - s.parse::() + Uint256::from_dec_str(s) .map_err(|e| format!("invalid {CONSENSUS_BLOCK_VALUE_HEADER}: {e:?}")) })?; @@ -1731,17 +1733,17 @@ impl TryFrom<&HeaderMap> for ProduceBlockV3Metadata { /// A wrapper over a [`SignedBeaconBlock`] or a [`SignedBlockContents`]. #[derive(Clone, Debug, Encode, Serialize, Deserialize)] #[serde(untagged)] -#[serde(bound = "T: EthSpec")] +#[serde(bound = "E: EthSpec")] #[ssz(enum_behaviour = "transparent")] -pub enum PublishBlockRequest { - BlockContents(SignedBlockContents), - Block(Arc>), +pub enum PublishBlockRequest { + BlockContents(SignedBlockContents), + Block(Arc>), } -impl PublishBlockRequest { +impl PublishBlockRequest { pub fn new( - block: Arc>, - blob_items: Option<(KzgProofs, BlobsList)>, + block: Arc>, + blob_items: Option<(KzgProofs, BlobsList)>, ) -> Self { match blob_items { Some((kzg_proofs, blobs)) => Self::BlockContents(SignedBlockContents { @@ -1760,11 +1762,11 @@ impl PublishBlockRequest { SignedBeaconBlock::from_ssz_bytes_for_fork(bytes, fork_name) .map(|block| PublishBlockRequest::Block(Arc::new(block))) } - ForkName::Deneb => { + ForkName::Deneb | ForkName::Electra => { let mut builder = ssz::SszDecoderBuilder::new(bytes); builder.register_anonymous_variable_length_item()?; - builder.register_type::>()?; - builder.register_type::>()?; + builder.register_type::>()?; + builder.register_type::>()?; let mut decoder = builder.build()?; let block = decoder.decode_next_with(|bytes| { @@ -1780,7 +1782,7 @@ impl PublishBlockRequest { } } - pub fn signed_block(&self) -> &Arc> { + pub fn signed_block(&self) -> &Arc> { match self { PublishBlockRequest::BlockContents(block_and_sidecars) => { &block_and_sidecars.signed_block @@ -1789,7 +1791,7 @@ impl PublishBlockRequest { } } - pub fn deconstruct(self) -> SignedBlockContentsTuple { + pub fn deconstruct(self) -> SignedBlockContentsTuple { match self { PublishBlockRequest::BlockContents(block_and_sidecars) => ( block_and_sidecars.signed_block, @@ -1801,10 +1803,10 @@ impl PublishBlockRequest { } /// Converting from a `SignedBlindedBeaconBlock` into a full `SignedBlockContents`. -pub fn into_full_block_and_blobs( - blinded_block: SignedBlindedBeaconBlock, - maybe_full_payload_contents: Option>, -) -> Result, String> { +pub fn into_full_block_and_blobs( + blinded_block: SignedBlindedBeaconBlock, + maybe_full_payload_contents: Option>, +) -> Result, String> { match maybe_full_payload_contents { None => { let signed_block = blinded_block @@ -1836,59 +1838,59 @@ pub fn into_full_block_and_blobs( } } -impl TryFrom>> for PublishBlockRequest { +impl TryFrom>> for PublishBlockRequest { type Error = &'static str; - fn try_from(block: Arc>) -> Result { + fn try_from(block: Arc>) -> Result { match *block { SignedBeaconBlock::Base(_) | SignedBeaconBlock::Altair(_) | SignedBeaconBlock::Merge(_) | SignedBeaconBlock::Capella(_) => Ok(PublishBlockRequest::Block(block)), - SignedBeaconBlock::Deneb(_) => { - Err("deneb block contents cannot be fully constructed from just the signed block") - } + SignedBeaconBlock::Deneb(_) | SignedBeaconBlock::Electra(_) => Err( + "post-Deneb block contents cannot be fully constructed from just the signed block", + ), } } } -impl From> for PublishBlockRequest { - fn from(block_contents_tuple: SignedBlockContentsTuple) -> Self { +impl From> for PublishBlockRequest { + fn from(block_contents_tuple: SignedBlockContentsTuple) -> Self { PublishBlockRequest::new(block_contents_tuple.0, block_contents_tuple.1) } } #[derive(Debug, Clone, Serialize, Deserialize, Encode)] -#[serde(bound = "T: EthSpec")] -pub struct SignedBlockContents { - pub signed_block: Arc>, - pub kzg_proofs: KzgProofs, +#[serde(bound = "E: EthSpec")] +pub struct SignedBlockContents { + pub signed_block: Arc>, + pub kzg_proofs: KzgProofs, #[serde(with = "ssz_types::serde_utils::list_of_hex_fixed_vec")] - pub blobs: BlobsList, + pub blobs: BlobsList, } #[derive(Debug, Clone, Serialize, Deserialize, Encode)] -#[serde(bound = "T: EthSpec")] -pub struct BlockContents { - pub block: BeaconBlock, - pub kzg_proofs: KzgProofs, +#[serde(bound = "E: EthSpec")] +pub struct BlockContents { + pub block: BeaconBlock, + pub kzg_proofs: KzgProofs, #[serde(with = "ssz_types::serde_utils::list_of_hex_fixed_vec")] - pub blobs: BlobsList, + pub blobs: BlobsList, } -impl ForkVersionDeserialize for BlockContents { +impl ForkVersionDeserialize for BlockContents { fn deserialize_by_fork<'de, D: serde::Deserializer<'de>>( value: serde_json::value::Value, fork_name: ForkName, ) -> Result { #[derive(Deserialize)] - #[serde(bound = "T: EthSpec")] - struct Helper { + #[serde(bound = "E: EthSpec")] + struct Helper { block: serde_json::Value, - kzg_proofs: KzgProofs, + kzg_proofs: KzgProofs, #[serde(with = "ssz_types::serde_utils::list_of_hex_fixed_vec")] - blobs: BlobsList, + blobs: BlobsList, } - let helper: Helper = serde_json::from_value(value).map_err(serde::de::Error::custom)?; + let helper: Helper = serde_json::from_value(value).map_err(serde::de::Error::custom)?; Ok(Self { block: BeaconBlock::deserialize_by_fork::<'de, D>(helper.block, fork_name)?, @@ -1954,7 +1956,7 @@ impl ForkVersionDeserialize for FullPayloadContents { ForkName::Merge | ForkName::Capella => serde_json::from_value(value) .map(Self::Payload) .map_err(serde::de::Error::custom), - ForkName::Deneb => serde_json::from_value(value) + ForkName::Deneb | ForkName::Electra => serde_json::from_value(value) .map(Self::PayloadAndBlobs) .map_err(serde::de::Error::custom), ForkName::Base | ForkName::Altair => Err(serde::de::Error::custom(format!( diff --git a/common/eth2_interop_keypairs/src/lib.rs b/common/eth2_interop_keypairs/src/lib.rs index 3d4ff02c38..3031e1c4dc 100644 --- a/common/eth2_interop_keypairs/src/lib.rs +++ b/common/eth2_interop_keypairs/src/lib.rs @@ -23,7 +23,6 @@ use bls::{Keypair, PublicKey, SecretKey}; use ethereum_hashing::hash; use num_bigint::BigUint; use serde::{Deserialize, Serialize}; -use std::convert::TryInto; use std::fs::File; use std::path::PathBuf; 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 index 96baffde6f..eb079bfc50 100644 --- 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 @@ -1,3 +1,11 @@ +# chiado-lighthouse-0 +- "enr:-L64QOijsdi9aVIawMb5h5PWueaPM9Ai6P17GNPFlHzz7MGJQ8tFMdYrEx8WQitNKLG924g2Q9cCdzg54M0UtKa3QIKCMxaHYXR0bmV0c4j__________4RldGgykDE2cEMCAABv__________-CaWSCdjSCaXCEi5AaWYlzZWNwMjU2azGhA8CjTkD4m1s8FbKCN18LgqlYcE65jrT148vFtwd9U62SiHN5bmNuZXRzD4N0Y3CCIyiDdWRwgiMo" +# chiado-lighthouse-1 +- "enr:-L64QKYKGQj5ybkfBxyFU5IEVzP7oJkGHJlie4W8BCGAYEi4P0mmMksaasiYF789mVW_AxYVNVFUjg9CyzmdvpyWQ1KCMlmHYXR0bmV0c4j__________4RldGgykDE2cEMCAABv__________-CaWSCdjSCaXCEi5CtNolzZWNwMjU2azGhAuA7BAwIijy1z81AO9nz_MOukA1ER68rGA67PYQ5pF1qiHN5bmNuZXRzD4N0Y3CCIyiDdWRwgiMo" +# chiado-lodestar-0 +- "enr:-Ly4QJJUnV9BxP_rw2Bv7E9iyw4sYS2b4OQZIf4Mu_cA6FljJvOeSTQiCUpbZhZjR4R0VseBhdTzrLrlHrAuu_OeZqgJh2F0dG5ldHOI__________-EZXRoMpAxNnBDAgAAb___________gmlkgnY0gmlwhIuQGnOJc2VjcDI1NmsxoQPT_u3IjDtB2r-nveH5DhUmlM8F2IgLyxhmwmqW4L5k3ohzeW5jbmV0cw-DdGNwgiMog3VkcIIjKA" +# chiado-prysm-0 +- "enr:-MK4QCkOyqOTPX1_-F-5XVFjPclDUc0fj3EeR8FJ5-hZjv6ARuGlFspM0DtioHn1r6YPUXkOg2g3x6EbeeKdsrvVBYmGAYQKrixeh2F0dG5ldHOIAAAAAAAAAACEZXRoMpAxNnBDAgAAb___________gmlkgnY0gmlwhIuQGlWJc2VjcDI1NmsxoQKdW3-DgLExBkpLGMRtuM88wW_gZkC7Yeg0stYDTrlynYhzeW5jbmV0cwCDdGNwgiMog3VkcIIjKA" # chiado-teku-0 - "enr:-Ly4QLYLNqrjvSxD3lpAPBUNlxa6cIbe79JqLZLFcZZjWoCjZcw-85agLUErHiygG2weRSCLnd5V460qTbLbwJQsfZkoh2F0dG5ldHOI__________-EZXRoMpAxNnBDAgAAb___________gmlkgnY0gmlwhKq7mu-Jc2VjcDI1NmsxoQP900YAYa9kdvzlSKGjVo-F3XVzATjOYp3BsjLjSophO4hzeW5jbmV0cw-DdGNwgiMog3VkcIIjKA" # chiado-teku-1 diff --git a/common/eth2_network_config/built_in_network_configs/chiado/config.yaml b/common/eth2_network_config/built_in_network_configs/chiado/config.yaml index 8064ea5556..c869d9cfc8 100644 --- a/common/eth2_network_config/built_in_network_configs/chiado/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/chiado/config.yaml @@ -46,7 +46,9 @@ CAPELLA_FORK_EPOCH: 244224 # Wed May 24 2023 13:12:00 GMT+0000 # Deneb DENEB_FORK_VERSION: 0x0400006f DENEB_FORK_EPOCH: 516608 # Wed Jan 31 2024 18:15:40 GMT+0000 - +# Electra +ELECTRA_FORK_VERSION: 0x0500006f +ELECTRA_FORK_EPOCH: 18446744073709551615 # Time parameters # --------------------------------------------------------------- diff --git a/common/eth2_network_config/built_in_network_configs/gnosis/boot_enr.yaml b/common/eth2_network_config/built_in_network_configs/gnosis/boot_enr.yaml index e5896988c9..7b64e66fee 100644 --- a/common/eth2_network_config/built_in_network_configs/gnosis/boot_enr.yaml +++ b/common/eth2_network_config/built_in_network_configs/gnosis/boot_enr.yaml @@ -7,3 +7,9 @@ - enr:-Ly4QBbaKRSX4SncCOxTTL611Kxlz-zYFrIn-k_63jGIPK_wbvFghVUHJICPCxufgTX5h79jvgfPr-2hEEQEdziGQ5MCh2F0dG5ldHOIAAAAAAAAAACEZXRoMpCCS-QxAgAAZP__________gmlkgnY0gmlwhAMazo6Jc2VjcDI1NmsxoQKt-kbM9isuWp8djhyEq6-4MLv1Sy7dOXeMOMdPgwu9LohzeW5jbmV0cwCDdGNwgiMog3VkcIIjKA - enr:-Ly4QKJ5BzgFyJ6BaTlGY0C8ROzl508U3GA6qxdG5Gn2hxdke6nQO187pYlLvhp82Dez4PQn436Fts1F0WAm-_5l2LACh2F0dG5ldHOIAAAAAAAAAACEZXRoMpCCS-QxAgAAZP__________gmlkgnY0gmlwhA-YLVKJc2VjcDI1NmsxoQI8_Lvr6p_TkcAu8KorKacfUEnoOon0tdO0qWhriPdBP4hzeW5jbmV0cwCDdGNwgiMog3VkcIIjKA - enr:-Ly4QJMtoiX2bPnVbiQOJCLbtUlqdqZk7kCJQln_W1bp1vOHcxWowE-iMXkKC4_uOb0o73wAW71WYi80Dlsg-7a5wiICh2F0dG5ldHOIAAAAAAAAAACEZXRoMpCCS-QxAgAAZP__________gmlkgnY0gmlwhDbP3KmJc2VjcDI1NmsxoQNvcfKYUqcemLFlpKxl7JcQJwQ3L9unYL44gY2aEiRnI4hzeW5jbmV0cwCDdGNwgiMog3VkcIIjKA +- enr:-Ly4QIAhiTHk6JdVhCdiLwT83wAolUFo5J4nI5HrF7-zJO_QEw3cmEGxC1jvqNNUN64Vu-xxqDKSM528vKRNCehZAfEBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpCCS-QxAgAAZP__________gmlkgnY0gmlwhEFtZ5SJc2VjcDI1NmsxoQJwgL5C-30E8RJmW8gCb7sfwWvvfre7wGcCeV4X1G2wJYhzeW5jbmV0cwCDdGNwgiMog3VkcIIjKA +- enr:-Ly4QDhEjlkf8fwO5uWAadexy88GXZneTuUCIPHhv98v8ZfXMtC0S1S_8soiT0CMEgoeLe9Db01dtkFQUnA9YcnYC_8Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpCCS-QxAgAAZP__________gmlkgnY0gmlwhEFtZ5WJc2VjcDI1NmsxoQMRSho89q2GKx_l2FZhR1RmnSiQr6o_9hfXfQUuW6bjMohzeW5jbmV0cwCDdGNwgiMog3VkcIIjKA +- enr:-Ly4QLKgv5M2D4DYJgo6s4NG_K4zu4sk5HOLCfGCdtgoezsbfRbfGpQ4iSd31M88ec3DHA5FWVbkgIas9EaJeXia0nwBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpCCS-QxAgAAZP__________gmlkgnY0gmlwhI1eYRaJc2VjcDI1NmsxoQLpK_A47iNBkVjka9Mde1F-Kie-R0sq97MCNKCxt2HwOIhzeW5jbmV0cwCDdGNwgiMog3VkcIIjKA +- enr:-Ly4QF_0qvji6xqXrhQEhwJR1W9h5dXV7ZjVCN_NlosKxcgZW6emAfB_KXxEiPgKr_-CZG8CWvTiojEohG1ewF7P368Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpCCS-QxAgAAZP__________gmlkgnY0gmlwhI1eYUqJc2VjcDI1NmsxoQIpNRUT6llrXqEbjkAodsZOyWv8fxQkyQtSvH4sg2D7n4hzeW5jbmV0cwCDdGNwgiMog3VkcIIjKA +- enr:-Ly4QCD5D99p36WafgTSxB6kY7D2V1ca71C49J4VWI2c8UZCCPYBvNRWiv0-HxOcbpuUdwPVhyWQCYm1yq2ZH0ukCbQBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpCCS-QxAgAAZP__________gmlkgnY0gmlwhI1eYVSJc2VjcDI1NmsxoQJJMSV8iSZ8zvkgbi8cjIGEUVJeekLqT0LQha_co-siT4hzeW5jbmV0cwCDdGNwgiMog3VkcIIjKA +- enr:-KK4QKXJq1QOVWuJAGige4uaT8LRPQGCVRf3lH3pxjaVScMRUfFW1eiiaz8RwOAYvw33D4EX-uASGJ5QVqVCqwccxa-Bi4RldGgykCGm-DYDAABk__________-CaWSCdjSCaXCEM0QnzolzZWNwMjU2azGhAhNvrRkpuK4MWTf3WqiOXSOePL8Zc-wKVpZ9FQx_BDadg3RjcIIjKIN1ZHCCIyg \ No newline at end of file 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 940fad3615..27fb81a513 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 @@ -41,13 +41,10 @@ CAPELLA_FORK_VERSION: 0x03000064 CAPELLA_FORK_EPOCH: 648704 # Deneb DENEB_FORK_VERSION: 0x04000064 -DENEB_FORK_EPOCH: 18446744073709551615 -# Sharding -SHARDING_FORK_VERSION: 0x03000064 -SHARDING_FORK_EPOCH: 18446744073709551615 - -# TBD, 2**32 is a placeholder. Merge transition approach is in active R&D. -TRANSITION_TOTAL_DIFFICULTY: 4294967296 +DENEB_FORK_EPOCH: 889856 # 2024-03-11T18:30:20.000Z +# Electra +ELECTRA_FORK_VERSION: 0x05000064 +ELECTRA_FORK_EPOCH: 18446744073709551615 # Time parameters @@ -75,7 +72,7 @@ EJECTION_BALANCE: 16000000000 # 2**2 (= 4) MIN_PER_EPOCH_CHURN_LIMIT: 4 # 2**3 (= 8) -MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: 8 +MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: 2 # 2**12 (= 4096) CHURN_LIMIT_QUOTIENT: 4096 @@ -84,6 +81,12 @@ CHURN_LIMIT_QUOTIENT: 4096 # --------------------------------------------------------------- # 40% PROPOSER_SCORE_BOOST: 40 +# 20% +REORG_HEAD_WEIGHT_THRESHOLD: 20 +# 160% +REORG_PARENT_WEIGHT_THRESHOLD: 160 +# `2` epochs +REORG_MAX_EPOCHS_SINCE_FINALIZATION: 2 # Deposit contract # --------------------------------------------------------------- @@ -105,3 +108,14 @@ MESSAGE_DOMAIN_VALID_SNAPPY: 0x01000000 ATTESTATION_SUBNET_COUNT: 64 ATTESTATION_SUBNET_EXTRA_BITS: 0 ATTESTATION_SUBNET_PREFIX_BITS: 6 +ATTESTATION_SUBNET_SHUFFLING_PREFIX_BITS: 3 + +# Deneb +# `2**7` (=128) +MAX_REQUEST_BLOCKS_DENEB: 128 +# MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK +MAX_REQUEST_BLOB_SIDECARS: 768 +# `2**14` (= 16384 epochs, ~15 days) +MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 16384 +# `6` +BLOB_SIDECAR_SUBNET_COUNT: 6 diff --git a/common/eth2_network_config/built_in_network_configs/holesky/config.yaml b/common/eth2_network_config/built_in_network_configs/holesky/config.yaml index 0bb72ebd88..bd384cfe49 100644 --- a/common/eth2_network_config/built_in_network_configs/holesky/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/holesky/config.yaml @@ -28,14 +28,15 @@ 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 - # Deneb DENEB_FORK_VERSION: 0x05017000 DENEB_FORK_EPOCH: 29696 +# Electra +ELECTRA_FORK_VERSION: 0x06017000 +ELECTRA_FORK_EPOCH: 18446744073709551615 # Time parameters # --------------------------------------------------------------- @@ -70,6 +71,12 @@ MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: 8 # --------------------------------------------------------------- # 40% PROPOSER_SCORE_BOOST: 40 +# 20% +REORG_HEAD_WEIGHT_THRESHOLD: 20 +# 160% +REORG_PARENT_WEIGHT_THRESHOLD: 160 +# `2` epochs +REORG_MAX_EPOCHS_SINCE_FINALIZATION: 2 # Deposit contract # --------------------------------------------------------------- @@ -105,6 +112,7 @@ ATTESTATION_SUBNET_COUNT: 64 ATTESTATION_SUBNET_EXTRA_BITS: 0 # ceillog2(ATTESTATION_SUBNET_COUNT) + ATTESTATION_SUBNET_EXTRA_BITS ATTESTATION_SUBNET_PREFIX_BITS: 6 +ATTESTATION_SUBNET_SHUFFLING_PREFIX_BITS: 3 # Deneb # `2**7` (=128) diff --git a/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml b/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml index ed96df2913..c8695123ab 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 @@ -1,9 +1,15 @@ # Mainnet config # Extends the mainnet preset -CONFIG_NAME: 'mainnet' PRESET_BASE: 'mainnet' +# Free-form short name of the network that this configuration applies to - known +# canonical network names include: +# * 'mainnet' - there can be only one +# * 'prater' - testnet +# Must match the regex: [a-z0-9\-] +CONFIG_NAME: 'mainnet' + # Transition # --------------------------------------------------------------- # Estimated on Sept 15, 2022 @@ -12,6 +18,8 @@ TERMINAL_TOTAL_DIFFICULTY: 58750000000000000000000 TERMINAL_BLOCK_HASH: 0x0000000000000000000000000000000000000000000000000000000000000000 TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH: 18446744073709551615 + + # Genesis # --------------------------------------------------------------- # `2**14` (= 16,384) @@ -32,22 +40,19 @@ GENESIS_DELAY: 604800 # Altair ALTAIR_FORK_VERSION: 0x01000000 -ALTAIR_FORK_EPOCH: 74240 -# Merge +ALTAIR_FORK_EPOCH: 74240 # Oct 27, 2021, 10:56:23am UTC +# Bellatrix BELLATRIX_FORK_VERSION: 0x02000000 -BELLATRIX_FORK_EPOCH: 144896 # Sept 6, 2022, 11:34:47am UTC +BELLATRIX_FORK_EPOCH: 144896 # Sept 6, 2022, 11:34:47am UTC # Capella CAPELLA_FORK_VERSION: 0x03000000 -CAPELLA_FORK_EPOCH: 194048 # April 12, 2023, 10:27:35pm UTC +CAPELLA_FORK_EPOCH: 194048 # April 12, 2023, 10:27:35pm UTC # Deneb DENEB_FORK_VERSION: 0x04000000 -DENEB_FORK_EPOCH: 18446744073709551615 -# Sharding -SHARDING_FORK_VERSION: 0x03000000 -SHARDING_FORK_EPOCH: 18446744073709551615 - -# TBD, 2**32 is a placeholder. Merge transition approach is in active R&D. -TRANSITION_TOTAL_DIFFICULTY: 4294967296 +DENEB_FORK_EPOCH: 269568 # March 13, 2024, 01:55:35pm UTC +# Electra +ELECTRA_FORK_VERSION: 0x05000000 +ELECTRA_FORK_EPOCH: 18446744073709551615 # Time parameters @@ -74,16 +79,22 @@ INACTIVITY_SCORE_RECOVERY_RATE: 16 EJECTION_BALANCE: 16000000000 # 2**2 (= 4) MIN_PER_EPOCH_CHURN_LIMIT: 4 -# 2**3 (= 8) -MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: 8 # 2**16 (= 65,536) CHURN_LIMIT_QUOTIENT: 65536 - +# [New in Deneb:EIP7514] 2**3 (= 8) +MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: 8 # Fork choice # --------------------------------------------------------------- # 40% PROPOSER_SCORE_BOOST: 40 +# 20% +REORG_HEAD_WEIGHT_THRESHOLD: 20 +# 160% +REORG_PARENT_WEIGHT_THRESHOLD: 160 +# `2` epochs +REORG_MAX_EPOCHS_SINCE_FINALIZATION: 2 + # Deposit contract # --------------------------------------------------------------- @@ -92,16 +103,43 @@ DEPOSIT_CHAIN_ID: 1 DEPOSIT_NETWORK_ID: 1 DEPOSIT_CONTRACT_ADDRESS: 0x00000000219ab540356cBB839Cbe05303d7705Fa -# Network + +# Networking # --------------------------------------------------------------- -SUBNETS_PER_NODE: 2 +# `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 +ATTESTATION_SUBNET_SHUFFLING_PREFIX_BITS: 3 + +# 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 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 1928aeb309..f474b172c5 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 @@ -78,6 +78,12 @@ MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: 8 # --------------------------------------------------------------- # 40% PROPOSER_SCORE_BOOST: 40 +# 20% +REORG_HEAD_WEIGHT_THRESHOLD: 20 +# 160% +REORG_PARENT_WEIGHT_THRESHOLD: 160 +# `2` epochs +REORG_MAX_EPOCHS_SINCE_FINALIZATION: 2 # Deposit contract # --------------------------------------------------------------- @@ -115,6 +121,7 @@ ATTESTATION_SUBNET_COUNT: 64 ATTESTATION_SUBNET_EXTRA_BITS: 0 # ceillog2(ATTESTATION_SUBNET_COUNT) + ATTESTATION_SUBNET_EXTRA_BITS ATTESTATION_SUBNET_PREFIX_BITS: 6 +ATTESTATION_SUBNET_SHUFFLING_PREFIX_BITS: 3 # Deneb # `2**7` (=128) 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 33a5ccb3fe..2df40798c1 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 @@ -69,6 +69,10 @@ MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: 8 # --------------------------------------------------------------- # 40% PROPOSER_SCORE_BOOST: 40 +# 20% +REORG_HEAD_WEIGHT_THRESHOLD: 20 +# 160% +REORG_PARENT_WEIGHT_THRESHOLD: 160 # Deposit contract # --------------------------------------------------------------- @@ -104,6 +108,7 @@ ATTESTATION_SUBNET_COUNT: 64 ATTESTATION_SUBNET_EXTRA_BITS: 0 # ceillog2(ATTESTATION_SUBNET_COUNT) + ATTESTATION_SUBNET_EXTRA_BITS ATTESTATION_SUBNET_PREFIX_BITS: 6 +ATTESTATION_SUBNET_SHUFFLING_PREFIX_BITS: 3 # Deneb # `2**7` (=128) diff --git a/common/eth2_network_config/src/lib.rs b/common/eth2_network_config/src/lib.rs index 565b8d7890..1ead9a6bde 100644 --- a/common/eth2_network_config/src/lib.rs +++ b/common/eth2_network_config/src/lib.rs @@ -462,7 +462,7 @@ mod tests { use super::*; use ssz::Encode; use tempfile::Builder as TempBuilder; - use types::{Config, Eth1Data, GnosisEthSpec, Hash256, MainnetEthSpec}; + use types::{Eth1Data, GnosisEthSpec, MainnetEthSpec}; type E = MainnetEthSpec; @@ -583,6 +583,8 @@ mod tests { } else { GenesisStateSource::Unknown }; + // With Deneb enabled by default we must set a trusted setup here. + let kzg_trusted_setup = get_trusted_setup_from_config(&config).unwrap(); let testnet = Eth2NetworkConfig { deposit_contract_deploy_block, @@ -593,7 +595,7 @@ mod tests { .map(Encode::as_ssz_bytes) .map(Into::into), config, - kzg_trusted_setup: None, + kzg_trusted_setup: Some(kzg_trusted_setup), }; testnet diff --git a/common/lighthouse_version/src/lib.rs b/common/lighthouse_version/src/lib.rs index af11723487..5387d322e9 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.6.0-", - fallback = "Lighthouse/v4.6.0" + prefix = "Lighthouse/v5.1.3-", + fallback = "Lighthouse/v5.1.3" ); /// Returns `VERSION`, but with platform information appended to the end. diff --git a/common/logging/src/lib.rs b/common/logging/src/lib.rs index caf3e1d2fb..3a5a5209b0 100644 --- a/common/logging/src/lib.rs +++ b/common/logging/src/lib.rs @@ -223,7 +223,7 @@ impl TimeLatch { } } -pub fn create_tracing_layer(base_tracing_log_path: PathBuf, turn_on_terminal_logs: bool) { +pub fn create_tracing_layer(base_tracing_log_path: PathBuf) { let filter_layer = match tracing_subscriber::EnvFilter::try_from_default_env() .or_else(|_| tracing_subscriber::EnvFilter::try_new("warn")) { @@ -268,11 +268,7 @@ pub fn create_tracing_layer(base_tracing_log_path: PathBuf, turn_on_terminal_log if let Err(e) = tracing_subscriber::fmt() .with_env_filter(filter_layer) - .with_writer(move || { - tracing_subscriber::fmt::writer::OptionalWriter::::from( - turn_on_terminal_logs.then(std::io::stdout), - ) - }) + .with_writer(std::io::sink) .finish() .with(MetricsLayer) .with(custom_layer) diff --git a/common/logging/src/tracing_logging_layer.rs b/common/logging/src/tracing_logging_layer.rs index e7d9109beb..aabb6ddd0c 100644 --- a/common/logging/src/tracing_logging_layer.rs +++ b/common/logging/src/tracing_logging_layer.rs @@ -27,7 +27,7 @@ where }; let mut writer = match target { - "libp2p_gossipsub" => self.libp2p_non_blocking_writer.clone(), + "gossipsub" => self.libp2p_non_blocking_writer.clone(), "discv5" => self.discv5_non_blocking_writer.clone(), _ => return, }; diff --git a/common/slot_clock/src/manual_slot_clock.rs b/common/slot_clock/src/manual_slot_clock.rs index 61299f74ac..7b42fa9062 100644 --- a/common/slot_clock/src/manual_slot_clock.rs +++ b/common/slot_clock/src/manual_slot_clock.rs @@ -1,6 +1,5 @@ use super::SlotClock; use parking_lot::RwLock; -use std::convert::TryInto; use std::sync::Arc; use std::time::Duration; use types::Slot; diff --git a/common/slot_clock/src/metrics.rs b/common/slot_clock/src/metrics.rs index 7d0042102f..23a793b203 100644 --- a/common/slot_clock/src/metrics.rs +++ b/common/slot_clock/src/metrics.rs @@ -16,7 +16,7 @@ lazy_static! { } /// Update the global metrics `DEFAULT_REGISTRY` with info from the slot clock. -pub fn scrape_for_metrics(clock: &U) { +pub fn scrape_for_metrics(clock: &U) { let present_slot = match clock.now() { Some(slot) => slot, _ => Slot::new(0), @@ -25,8 +25,8 @@ pub fn scrape_for_metrics(clock: &U) { set_gauge(&PRESENT_SLOT, present_slot.as_u64() as i64); set_gauge( &PRESENT_EPOCH, - present_slot.epoch(T::slots_per_epoch()).as_u64() as i64, + present_slot.epoch(E::slots_per_epoch()).as_u64() as i64, ); - set_gauge(&SLOTS_PER_EPOCH, T::slots_per_epoch() as i64); + set_gauge(&SLOTS_PER_EPOCH, E::slots_per_epoch() as i64); set_gauge(&SECONDS_PER_SLOT, clock.slot_duration().as_secs() as i64); } diff --git a/common/system_health/src/lib.rs b/common/system_health/src/lib.rs index d10540e506..feec08af84 100644 --- a/common/system_health/src/lib.rs +++ b/common/system_health/src/lib.rs @@ -198,12 +198,31 @@ pub fn observe_system_health_vc( } } +/// Observes if NAT traversal is possible. +pub fn observe_nat() -> bool { + let discv5_nat = lighthouse_network::metrics::get_int_gauge( + &lighthouse_network::metrics::NAT_OPEN, + &["discv5"], + ) + .map(|g| g.get() == 1) + .unwrap_or_default(); + + let libp2p_nat = lighthouse_network::metrics::get_int_gauge( + &lighthouse_network::metrics::NAT_OPEN, + &["libp2p"], + ) + .map(|g| g.get() == 1) + .unwrap_or_default(); + + discv5_nat || libp2p_nat +} + /// Observes the Beacon Node system health. -pub fn observe_system_health_bn( +pub fn observe_system_health_bn( sysinfo: Arc>, data_dir: PathBuf, app_uptime: u64, - network_globals: Arc>, + network_globals: Arc>, ) -> SystemHealthBN { let system_health = observe_system_health(sysinfo.clone(), data_dir, app_uptime); @@ -223,11 +242,7 @@ pub fn observe_system_health_bn( .unwrap_or_else(|| (String::from("None"), 0, 0)); // Determine if the NAT is open or not. - let nat_open = lighthouse_network::metrics::NAT_OPEN - .as_ref() - .map(|v| v.get()) - .unwrap_or(0) - != 0; + let nat_open = observe_nat(); SystemHealthBN { system_health, diff --git a/common/task_executor/Cargo.toml b/common/task_executor/Cargo.toml index 38f4eca369..b3d58fa5ea 100644 --- a/common/task_executor/Cargo.toml +++ b/common/task_executor/Cargo.toml @@ -5,10 +5,10 @@ authors = ["Sigma Prime "] edition = { workspace = true } [dependencies] +async-channel = { workspace = true } 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/task_executor/src/lib.rs b/common/task_executor/src/lib.rs index 6bf4cc8e08..d6edfd3121 100644 --- a/common/task_executor/src/lib.rs +++ b/common/task_executor/src/lib.rs @@ -3,7 +3,7 @@ pub mod test_utils; use futures::channel::mpsc::Sender; use futures::prelude::*; -use slog::{crit, debug, o, trace}; +use slog::{debug, o, trace}; use std::sync::Weak; use tokio::runtime::{Handle, Runtime}; @@ -73,7 +73,7 @@ pub struct TaskExecutor { /// The handle to the runtime on which tasks are spawned handle_provider: HandleProvider, /// The receiver exit future which on receiving shuts down the task - exit: exit_future::Exit, + exit: async_channel::Receiver<()>, /// Sender given to tasks, so that if they encounter a state in which execution cannot /// continue they can request that everything shuts down. /// @@ -93,7 +93,7 @@ impl TaskExecutor { /// crate). pub fn new>( handle: T, - exit: exit_future::Exit, + exit: async_channel::Receiver<()>, log: slog::Logger, signal_tx: Sender, ) -> Self { @@ -138,23 +138,11 @@ impl TaskExecutor { name: &'static str, ) { let mut shutdown_sender = self.shutdown_sender(); - let log = self.log.clone(); - if let Some(handle) = self.handle() { handle.spawn(async move { let timer = metrics::start_timer_vec(&metrics::TASKS_HISTOGRAM, &[name]); if let Err(join_error) = task_handle.await { - if let Ok(panic) = join_error.try_into_panic() { - let message = panic.downcast_ref::<&str>().unwrap_or(&""); - - crit!( - log, - "Task panic. This is a bug!"; - "task_name" => name, - "message" => message, - "advice" => "Please check above for a backtrace and notify \ - the developers" - ); + if let Ok(_panic) = join_error.try_into_panic() { let _ = shutdown_sender .try_send(ShutdownReason::Failure("Panic (fatal error)")); } @@ -171,8 +159,8 @@ impl TaskExecutor { /// Spawn a future on the tokio runtime. /// - /// The future is wrapped in an `exit_future::Exit`. The task is cancelled when the corresponding - /// exit_future `Signal` is fired/dropped. + /// The future is wrapped in an `async-channel::Receiver`. The task is cancelled when the corresponding + /// Sender is dropped. /// /// The future is monitored via another spawned future to ensure that it doesn't panic. In case /// of a panic, the executor will be shut down via `self.signal_tx`. @@ -184,9 +172,9 @@ impl TaskExecutor { } } - /// Spawn a future on the tokio runtime. This function does not wrap the task in an `exit_future::Exit` + /// Spawn a future on the tokio runtime. This function does not wrap the task in an `async-channel::Receiver` /// like [spawn](#method.spawn). - /// The caller of this function is responsible for wrapping up the task with an `exit_future::Exit` to + /// The caller of this function is responsible for wrapping up the task with an `async-channel::Receiver` to /// ensure that the task gets canceled appropriately. /// This function generates prometheus metrics on number of tasks and task duration. /// @@ -225,9 +213,9 @@ impl TaskExecutor { } } - /// Spawn a future on the tokio runtime wrapped in an `exit_future::Exit` returning an optional + /// Spawn a future on the tokio runtime wrapped in an `async-channel::Receiver` returning an optional /// join handle to the future. - /// The task is canceled when the corresponding exit_future `Signal` is fired/dropped. + /// The task is canceled when the corresponding async-channel is dropped. /// /// This function generates prometheus metrics on number of tasks and task duration. pub fn spawn_handle( @@ -235,30 +223,29 @@ impl TaskExecutor { task: impl Future + Send + 'static, name: &'static str, ) -> Option>> { - let exit = self.exit.clone(); + let exit = self.exit(); let log = self.log.clone(); if let Some(int_gauge) = metrics::get_int_gauge(&metrics::ASYNC_TASKS_COUNT, &[name]) { // Task is shutdown before it completes if `exit` receives let int_gauge_1 = int_gauge.clone(); - let future = future::select(Box::pin(task), exit).then(move |either| { - let result = match either { - future::Either::Left((value, _)) => { - trace!(log, "Async task completed"; "task" => name); - Some(value) - } - future::Either::Right(_) => { - debug!(log, "Async task shutdown, exit received"; "task" => name); - None - } - }; - int_gauge_1.dec(); - futures::future::ready(result) - }); - int_gauge.inc(); if let Some(handle) = self.handle() { - Some(handle.spawn(future)) + Some(handle.spawn(async move { + futures::pin_mut!(exit); + let result = match future::select(Box::pin(task), exit).await { + future::Either::Left((value, _)) => { + trace!(log, "Async task completed"; "task" => name); + Some(value) + } + future::Either::Right(_) => { + debug!(log, "Async task shutdown, exit received"; "task" => name); + None + } + }; + int_gauge_1.dec(); + result + })) } else { debug!(self.log, "Couldn't spawn task. Runtime shutting down"); None @@ -336,7 +323,7 @@ impl TaskExecutor { metrics::inc_gauge_vec(&metrics::BLOCK_ON_TASKS_COUNT, &[name]); let log = self.log.clone(); let handle = self.handle()?; - let exit = self.exit.clone(); + let exit = self.exit(); debug!( log, @@ -374,9 +361,13 @@ impl TaskExecutor { self.handle_provider.handle() } - /// Returns a copy of the `exit_future::Exit`. - pub fn exit(&self) -> exit_future::Exit { - self.exit.clone() + /// Returns a future that completes when `async-channel::Sender` is dropped or () is sent, + /// which translates to the exit signal being triggered. + pub fn exit(&self) -> impl Future { + let exit = self.exit.clone(); + async move { + let _ = exit.recv().await; + } } /// Get a channel to request shutting down. diff --git a/common/task_executor/src/test_utils.rs b/common/task_executor/src/test_utils.rs index c6e5ad01e6..6e372d9757 100644 --- a/common/task_executor/src/test_utils.rs +++ b/common/task_executor/src/test_utils.rs @@ -14,7 +14,7 @@ use tokio::runtime; /// This struct should never be used in production, only testing. pub struct TestRuntime { runtime: Option>, - _runtime_shutdown: exit_future::Signal, + _runtime_shutdown: async_channel::Sender<()>, pub task_executor: TaskExecutor, pub log: Logger, } @@ -24,7 +24,7 @@ impl Default for TestRuntime { /// called *outside* any existing runtime, create a new `Runtime` and keep it alive until the /// `Self` is dropped. fn default() -> Self { - let (runtime_shutdown, exit) = exit_future::signal(); + let (runtime_shutdown, exit) = async_channel::bounded(1); let (shutdown_tx, _) = futures::channel::mpsc::channel(1); let log = null_logger().unwrap(); diff --git a/common/validator_dir/src/builder.rs b/common/validator_dir/src/builder.rs index bccf9086ac..3d5d149608 100644 --- a/common/validator_dir/src/builder.rs +++ b/common/validator_dir/src/builder.rs @@ -214,6 +214,7 @@ impl<'a> Builder<'a> { .write(true) .read(true) .create(true) + .truncate(true) .open(path) .map_err(Error::UnableToSaveDepositData)? .write_all(hex.as_bytes()) @@ -231,6 +232,7 @@ impl<'a> Builder<'a> { .write(true) .read(true) .create(true) + .truncate(true) .open(path) .map_err(Error::UnableToSaveDepositAmount)? .write_all(format!("{}", amount).as_bytes()) diff --git a/consensus/cached_tree_hash/src/cache.rs b/consensus/cached_tree_hash/src/cache.rs index 3b4878503e..450128f15e 100644 --- a/consensus/cached_tree_hash/src/cache.rs +++ b/consensus/cached_tree_hash/src/cache.rs @@ -50,7 +50,7 @@ impl TreeHashCache { pub fn recalculate_merkle_root( &mut self, arena: &mut CacheArena, - leaves: impl Iterator + ExactSizeIterator, + leaves: impl ExactSizeIterator, ) -> Result { let dirty_indices = self.update_leaves(arena, leaves)?; self.update_merkle_root(arena, dirty_indices) @@ -60,7 +60,7 @@ impl TreeHashCache { pub fn update_leaves( &mut self, arena: &mut CacheArena, - mut leaves: impl Iterator + ExactSizeIterator, + mut leaves: impl ExactSizeIterator, ) -> Result, Error> { let new_leaf_count = leaves.len(); diff --git a/consensus/cached_tree_hash/src/impls.rs b/consensus/cached_tree_hash/src/impls.rs index 0624bd2014..efdba32b59 100644 --- a/consensus/cached_tree_hash/src/impls.rs +++ b/consensus/cached_tree_hash/src/impls.rs @@ -26,13 +26,11 @@ pub fn u64_leaf_count(len: usize) -> usize { pub fn hash256_iter( values: &[Hash256], -) -> impl Iterator + ExactSizeIterator + '_ { +) -> impl ExactSizeIterator + '_ { values.iter().copied().map(Hash256::to_fixed_bytes) } -pub fn u64_iter( - values: &[u64], -) -> impl Iterator + ExactSizeIterator + '_ { +pub fn u64_iter(values: &[u64]) -> impl ExactSizeIterator + '_ { let type_size = size_of::(); let vals_per_chunk = BYTES_PER_CHUNK / type_size; values.chunks(vals_per_chunk).map(move |xs| { diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 865a5affbb..6e3f6717ed 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -1,15 +1,10 @@ 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, error, warn, Logger}; +use slog::{crit, debug, 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, }; @@ -23,7 +18,6 @@ use types::{ EthSpec, ExecPayload, ExecutionBlockHash, Hash256, IndexedAttestation, RelativeEpoch, SignedBeaconBlock, Slot, }; -use types::{ProgressiveBalancesCache, ProgressiveBalancesMode}; #[derive(Debug)] pub enum Error { @@ -77,10 +71,7 @@ pub enum Error { proposer_boost_root: Hash256, }, UnrealizedVoteProcessing(state_processing::EpochProcessingError), - ParticipationCacheBuild(BeaconStateError), - ParticipationCacheError(ParticipationCacheError), ValidatorStatuses(BeaconStateError), - ProgressiveBalancesCacheCheckFailed(String), } impl From for Error { @@ -107,12 +98,6 @@ impl From for Error { } } -impl From for Error { - 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 { @@ -532,7 +517,8 @@ where &self, current_slot: Slot, canonical_head: Hash256, - re_org_threshold: ReOrgThreshold, + re_org_head_threshold: ReOrgThreshold, + re_org_parent_threshold: ReOrgThreshold, disallowed_offsets: &DisallowedReOrgOffsets, max_epochs_since_finalization: Epoch, ) -> Result>> { @@ -564,7 +550,8 @@ where current_slot, canonical_head, self.fc_store.justified_balances(), - re_org_threshold, + re_org_head_threshold, + re_org_parent_threshold, disallowed_offsets, max_epochs_since_finalization, ) @@ -574,7 +561,8 @@ where pub fn get_preliminary_proposer_head( &self, canonical_head: Hash256, - re_org_threshold: ReOrgThreshold, + re_org_head_threshold: ReOrgThreshold, + re_org_parent_threshold: ReOrgThreshold, disallowed_offsets: &DisallowedReOrgOffsets, max_epochs_since_finalization: Epoch, ) -> Result>> { @@ -584,7 +572,8 @@ where current_slot, canonical_head, self.fc_store.justified_balances(), - re_org_threshold, + re_org_head_threshold, + re_org_parent_threshold, disallowed_offsets, max_epochs_since_finalization, ) @@ -658,9 +647,7 @@ where block_delay: Duration, state: &BeaconState, payload_verification_status: PayloadVerificationStatus, - progressive_balances_mode: ProgressiveBalancesMode, spec: &ChainSpec, - log: &Logger, ) -> Result<(), 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 @@ -723,6 +710,7 @@ where // Add proposer score boost if the block is timely. let is_before_attesting_interval = block_delay < Duration::from_secs(spec.seconds_per_slot / INTERVALS_PER_SLOT); + let is_first_block = self.fc_store.proposer_boost_root().is_zero(); if current_slot == block.slot() && is_before_attesting_interval && is_first_block { self.fc_store.set_proposer_boost_root(block_root); @@ -755,85 +743,44 @@ where 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::Deneb(_) - | 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( + 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::Electra(_) + | BeaconBlockRef::Deneb(_) + | BeaconBlockRef::Capella(_) + | BeaconBlockRef::Merge(_) + | BeaconBlockRef::Altair(_) => { + // NOTE: Processing justification & finalization requires the progressive + // balances cache, but we cannot initialize it here as we only have an + // immutable reference. The state *should* have come straight from block + // processing, which initialises the cache, but if we add other `on_block` + // calls in future it could be worth passing a mutable reference. + per_epoch_processing::altair::process_justification_and_finalization(state)? + } + 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, - &participation_cache, + &validator_statuses.total_balances, + spec, )? } - 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()?; + }; - process_justification_and_finalization_from_progressive_cache::( - state, - 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, - )? - } + ( + justification_and_finalization_state.current_justified_checkpoint(), + justification_and_finalization_state.finalized_checkpoint(), + ) }; - ( - 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 @@ -1558,92 +1505,6 @@ 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( - state: &BeaconState, - maybe_participation_cache: Option<&ParticipationCache>, -) -> Result, Error> -where - E: EthSpec, - T: ForkChoiceStore, -{ - 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::( - 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( - state: &BeaconState, - participation_cache: &ParticipationCache, - cached_previous_target_balance: u64, - cached_current_target_balance: u64, - cached_total_active_balance: u64, -) -> Result<(), Error> -where - E: EthSpec, - T: ForkChoiceStore, -{ - 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. @@ -1655,7 +1516,7 @@ pub struct PersistedForkChoice { #[cfg(test)] mod tests { - use types::{EthSpec, MainnetEthSpec}; + use types::MainnetEthSpec; use super::*; diff --git a/consensus/fork_choice/src/fork_choice_store.rs b/consensus/fork_choice/src/fork_choice_store.rs index 320f10141d..27f3d34dbc 100644 --- a/consensus/fork_choice/src/fork_choice_store.rs +++ b/consensus/fork_choice/src/fork_choice_store.rs @@ -19,7 +19,7 @@ use types::{AbstractExecPayload, BeaconBlockRef, BeaconState, Checkpoint, EthSpe /// The primary motivation for defining this as a trait to be implemented upstream rather than a /// concrete struct is to allow this crate to be free from "impure" on-disk database logic, /// hopefully making auditing easier. -pub trait ForkChoiceStore: Sized { +pub trait ForkChoiceStore: Sized { type Error: Debug; /// Returns the last value passed to `Self::set_current_slot`. @@ -34,11 +34,11 @@ pub trait ForkChoiceStore: Sized { /// Called whenever `ForkChoice::on_block` has verified a block, but not yet added it to fork /// choice. Allows the implementer to performing caching or other housekeeping duties. - fn on_verified_block>( + fn on_verified_block>( &mut self, - block: BeaconBlockRef, + block: BeaconBlockRef, block_root: Hash256, - state: &BeaconState, + state: &BeaconState, ) -> Result<(), Self::Error>; /// Returns the `justified_checkpoint`. diff --git a/consensus/fork_choice/tests/tests.rs b/consensus/fork_choice/tests/tests.rs index 649fbcc555..3153275fb7 100644 --- a/consensus/fork_choice/tests/tests.rs +++ b/consensus/fork_choice/tests/tests.rs @@ -16,8 +16,8 @@ use std::time::Duration; use store::MemoryStore; use types::{ test_utils::generate_deterministic_keypair, BeaconBlockRef, BeaconState, ChainSpec, Checkpoint, - Epoch, EthSpec, ForkName, Hash256, IndexedAttestation, MainnetEthSpec, ProgressiveBalancesMode, - RelativeEpoch, SignedBeaconBlock, Slot, SubnetId, + Epoch, EthSpec, ForkName, Hash256, IndexedAttestation, MainnetEthSpec, RelativeEpoch, + SignedBeaconBlock, Slot, SubnetId, }; pub type E = MainnetEthSpec; @@ -47,37 +47,16 @@ impl fmt::Debug for ForkChoiceTest { impl ForkChoiceTest { /// Creates a new tester. pub fn new() -> Self { - let harness = BeaconChainHarness::builder(MainnetEthSpec) - .default_spec() - .deterministic_keypairs(VALIDATOR_COUNT) - .fresh_ephemeral_store() - .build(); - - Self { harness } + Self::new_with_chain_config(ChainConfig::default()) } /// Creates a new tester with a custom chain config. pub fn new_with_chain_config(chain_config: ChainConfig) -> Self { - let harness = BeaconChainHarness::builder(MainnetEthSpec) - .default_spec() - .chain_config(chain_config) - .deterministic_keypairs(VALIDATOR_COUNT) - .fresh_ephemeral_store() - .build(); - - 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) + // Run fork choice tests against the latest fork. 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() - }) + .chain_config(chain_config) .deterministic_keypairs(VALIDATOR_COUNT) .fresh_ephemeral_store() .mock_execution_layer() @@ -338,9 +317,7 @@ impl ForkChoiceTest { Duration::from_secs(0), &state, PayloadVerificationStatus::Verified, - self.harness.chain.config.progressive_balances_mode, &self.harness.chain.spec, - self.harness.logger(), ) .unwrap(); self @@ -383,9 +360,7 @@ impl ForkChoiceTest { Duration::from_secs(0), &state, PayloadVerificationStatus::Verified, - self.harness.chain.config.progressive_balances_mode, &self.harness.chain.spec, - self.harness.logger(), ) .expect_err("on_block did not return an error"); comparison_func(err); @@ -1348,7 +1323,7 @@ async fn weak_subjectivity_check_epoch_boundary_is_skip_slot_failure() { /// 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) + ForkChoiceTest::new() // first two epochs .apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0) .await @@ -1379,18 +1354,18 @@ async fn progressive_balances_cache_attester_slashing() { /// 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) + ForkChoiceTest::new() // first two epochs .apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0) .await .unwrap() // Note: This test may fail if the shuffling used changes, right now it re-runs with - // deterministic shuffling. A shuffling change my cause the slashed proposer to propose + // deterministic shuffling. A shuffling change may cause the slashed proposer to propose // again in the next epoch, which results in a block processing failure // (`HeaderInvalid::ProposerSlashed`). The harness should be re-worked to successfully skip // the slot in this scenario rather than panic-ing. The same applies to // `progressive_balances_cache_attester_slashing`. - .apply_blocks(1) + .apply_blocks(2) .await .add_previous_epoch_proposer_slashing(MainnetEthSpec::slots_per_epoch()) .await diff --git a/consensus/proto_array/src/fork_choice_test_definition/ffg_updates.rs b/consensus/proto_array/src/fork_choice_test_definition/ffg_updates.rs index 77211a86a7..3b31616145 100644 --- a/consensus/proto_array/src/fork_choice_test_definition/ffg_updates.rs +++ b/consensus/proto_array/src/fork_choice_test_definition/ffg_updates.rs @@ -59,7 +59,7 @@ pub fn get_ffg_case_01_test_definition() -> ForkChoiceTestDefinition { expected_head: get_root(3), }); - // Ensure that with justified epoch 1 we find 2 + // Ensure that with justified epoch 1 we find 3 // // 0 // | @@ -68,11 +68,15 @@ pub fn get_ffg_case_01_test_definition() -> ForkChoiceTestDefinition { // 2 <- start // | // 3 <- head + // + // Since https://github.com/ethereum/consensus-specs/pull/3431 it is valid + // to elect head blocks that have a higher justified checkpoint than the + // store. ops.push(Operation::FindHead { justified_checkpoint: get_checkpoint(1), finalized_checkpoint: get_checkpoint(0), justified_state_balances: balances.clone(), - expected_head: get_root(2), + expected_head: get_root(3), }); // Ensure that with justified epoch 2 we find 3 @@ -247,14 +251,19 @@ pub fn get_ffg_case_02_test_definition() -> ForkChoiceTestDefinition { justified_state_balances: balances.clone(), expected_head: get_root(10), }); - // Same as above, but with justified epoch 3 (should be invalid). - ops.push(Operation::InvalidFindHead { + // Same as above, but with justified epoch 3. + // + // Since https://github.com/ethereum/consensus-specs/pull/3431 it is valid + // to elect head blocks that have a higher justified checkpoint than the + // store. + ops.push(Operation::FindHead { justified_checkpoint: Checkpoint { epoch: Epoch::new(3), root: get_root(6), }, finalized_checkpoint: get_checkpoint(0), justified_state_balances: balances.clone(), + expected_head: get_root(10), }); // Add a vote to 1. @@ -305,14 +314,19 @@ pub fn get_ffg_case_02_test_definition() -> ForkChoiceTestDefinition { justified_state_balances: balances.clone(), expected_head: get_root(9), }); - // Save as above but justified epoch 3 (should fail). - ops.push(Operation::InvalidFindHead { + // Save as above but justified epoch 3. + // + // Since https://github.com/ethereum/consensus-specs/pull/3431 it is valid + // to elect head blocks that have a higher justified checkpoint than the + // store. + ops.push(Operation::FindHead { justified_checkpoint: Checkpoint { epoch: Epoch::new(3), root: get_root(5), }, finalized_checkpoint: get_checkpoint(0), justified_state_balances: balances.clone(), + expected_head: get_root(9), }); // Add a vote to 2. @@ -363,14 +377,19 @@ pub fn get_ffg_case_02_test_definition() -> ForkChoiceTestDefinition { justified_state_balances: balances.clone(), expected_head: get_root(10), }); - // Same as above but justified epoch 3 (should fail). - ops.push(Operation::InvalidFindHead { + // Same as above but justified epoch 3. + // + // Since https://github.com/ethereum/consensus-specs/pull/3431 it is valid + // to elect head blocks that have a higher justified checkpoint than the + // store. + ops.push(Operation::FindHead { justified_checkpoint: Checkpoint { epoch: Epoch::new(3), root: get_root(6), }, finalized_checkpoint: get_checkpoint(0), justified_state_balances: balances.clone(), + expected_head: get_root(10), }); // Ensure that if we start at 1 we find 9 (just: 0, fin: 0). @@ -405,14 +424,19 @@ pub fn get_ffg_case_02_test_definition() -> ForkChoiceTestDefinition { justified_state_balances: balances.clone(), expected_head: get_root(9), }); - // Same as above but justified epoch 3 (should fail). - ops.push(Operation::InvalidFindHead { + // Same as above but justified epoch 3. + // + // Since https://github.com/ethereum/consensus-specs/pull/3431 it is valid + // to elect head blocks that have a higher justified checkpoint than the + // store. + ops.push(Operation::FindHead { justified_checkpoint: Checkpoint { epoch: Epoch::new(3), root: get_root(5), }, finalized_checkpoint: get_checkpoint(0), justified_state_balances: balances.clone(), + expected_head: get_root(9), }); // Ensure that if we start at 2 we find 10 (just: 0, fin: 0). @@ -444,14 +468,19 @@ pub fn get_ffg_case_02_test_definition() -> ForkChoiceTestDefinition { justified_state_balances: balances.clone(), expected_head: get_root(10), }); - // Same as above but justified epoch 3 (should fail). - ops.push(Operation::InvalidFindHead { + // Same as above but justified epoch 3. + // + // Since https://github.com/ethereum/consensus-specs/pull/3431 it is valid + // to elect head blocks that have a higher justified checkpoint than the + // store. + ops.push(Operation::FindHead { justified_checkpoint: Checkpoint { epoch: Epoch::new(3), root: get_root(6), }, finalized_checkpoint: get_checkpoint(0), justified_state_balances: balances, + expected_head: get_root(10), }); // END OF TESTS diff --git a/consensus/proto_array/src/fork_choice_test_definition/no_votes.rs b/consensus/proto_array/src/fork_choice_test_definition/no_votes.rs index a60b3e6b36..27a7969e49 100644 --- a/consensus/proto_array/src/fork_choice_test_definition/no_votes.rs +++ b/consensus/proto_array/src/fork_choice_test_definition/no_votes.rs @@ -184,7 +184,7 @@ pub fn get_no_votes_test_definition() -> ForkChoiceTestDefinition { root: Hash256::zero(), }, }, - // Ensure the head is still 4 whilst the justified epoch is 0. + // Ensure the head is now 5 whilst the justified epoch is 0. // // 0 // / \ @@ -203,9 +203,10 @@ pub fn get_no_votes_test_definition() -> ForkChoiceTestDefinition { root: Hash256::zero(), }, justified_state_balances: balances.clone(), - expected_head: get_root(4), + expected_head: get_root(5), }, - // Ensure there is an error when starting from a block that has the wrong justified epoch. + // Ensure there is no error when starting from a block that has the + // wrong justified epoch. // // 0 // / \ @@ -214,7 +215,11 @@ pub fn get_no_votes_test_definition() -> ForkChoiceTestDefinition { // 4 3 // | // 5 <- starting from 5 with justified epoch 0 should error. - Operation::InvalidFindHead { + // + // Since https://github.com/ethereum/consensus-specs/pull/3431 it is valid + // to elect head blocks that have a higher justified checkpoint than the + // store. + Operation::FindHead { justified_checkpoint: Checkpoint { epoch: Epoch::new(1), root: get_root(5), @@ -224,6 +229,7 @@ pub fn get_no_votes_test_definition() -> ForkChoiceTestDefinition { root: Hash256::zero(), }, justified_state_balances: balances.clone(), + expected_head: get_root(5), }, // Set the justified epoch to 2 and the start block to 5 and ensure 5 is the head. // diff --git a/consensus/proto_array/src/justified_balances.rs b/consensus/proto_array/src/justified_balances.rs index c8787817f1..e08c8443ee 100644 --- a/consensus/proto_array/src/justified_balances.rs +++ b/consensus/proto_array/src/justified_balances.rs @@ -15,7 +15,7 @@ pub struct JustifiedBalances { } impl JustifiedBalances { - pub fn from_justified_state(state: &BeaconState) -> Result { + pub fn from_justified_state(state: &BeaconState) -> Result { let current_epoch = state.current_epoch(); let mut total_effective_balance = 0u64; let mut num_active_validators = 0u64; diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index 4726715a16..7c2ecfe26a 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -961,16 +961,9 @@ impl ProtoArray { node_justified_checkpoint }; - let mut correct_justified = self.justified_checkpoint.epoch == genesis_epoch - || voting_source.epoch == self.justified_checkpoint.epoch; - - if let Some(node_unrealized_justified_checkpoint) = node.unrealized_justified_checkpoint { - if !correct_justified && self.justified_checkpoint.epoch + 1 == current_epoch { - correct_justified = node_unrealized_justified_checkpoint.epoch - >= self.justified_checkpoint.epoch - && voting_source.epoch + 2 >= current_epoch; - } - } + let correct_justified = self.justified_checkpoint.epoch == genesis_epoch + || voting_source.epoch == self.justified_checkpoint.epoch + || voting_source.epoch + 2 >= current_epoch; let correct_finalized = self.finalized_checkpoint.epoch == genesis_epoch || self.is_finalized_checkpoint_or_descendant::(node.root); diff --git a/consensus/proto_array/src/proto_array_fork_choice.rs b/consensus/proto_array/src/proto_array_fork_choice.rs index 1c41b1855b..e1faba369f 100644 --- a/consensus/proto_array/src/proto_array_fork_choice.rs +++ b/consensus/proto_array/src/proto_array_fork_choice.rs @@ -195,8 +195,10 @@ pub struct ProposerHeadInfo { /// Information about the parent of the current head, which should be selected as the parent /// for a new proposal *if* a re-org is decided on. pub parent_node: ProtoNode, - /// The computed fraction of the active committee balance below which we can re-org. - pub re_org_weight_threshold: u64, + /// The computed fraction of the active head committee balance below which we can re-org. + pub re_org_head_weight_threshold: u64, + /// The computed fraction of the active parent committee balance above which we can re-org. + pub re_org_parent_weight_threshold: u64, /// The current slot from fork choice's point of view, may lead the wall-clock slot by upto /// 500ms. pub current_slot: Slot, @@ -207,13 +209,13 @@ pub struct ProposerHeadInfo { /// This type intentionally does not implement `Debug` so that callers are forced to handle the /// enum. #[derive(Debug, Clone, PartialEq)] -pub enum ProposerHeadError { +pub enum ProposerHeadError { DoNotReOrg(DoNotReOrg), - Error(E), + Error(T), } -impl From for ProposerHeadError { - fn from(e: DoNotReOrg) -> ProposerHeadError { +impl From for ProposerHeadError { + fn from(e: DoNotReOrg) -> ProposerHeadError { Self::DoNotReOrg(e) } } @@ -224,15 +226,15 @@ impl From for ProposerHeadError { } } -impl ProposerHeadError { - pub fn convert_inner_error(self) -> ProposerHeadError +impl ProposerHeadError { + pub fn convert_inner_error(self) -> ProposerHeadError where - E2: From, + T2: From, { - self.map_inner_error(E2::from) + self.map_inner_error(T2::from) } - pub fn map_inner_error(self, f: impl FnOnce(E1) -> E2) -> ProposerHeadError { + pub fn map_inner_error(self, f: impl FnOnce(T1) -> T2) -> ProposerHeadError { match self { ProposerHeadError::DoNotReOrg(reason) => ProposerHeadError::DoNotReOrg(reason), ProposerHeadError::Error(error) => ProposerHeadError::Error(f(error)), @@ -259,7 +261,11 @@ pub enum DoNotReOrg { }, HeadNotWeak { head_weight: u64, - re_org_weight_threshold: u64, + re_org_head_weight_threshold: u64, + }, + ParentNotStrong { + parent_weight: u64, + re_org_parent_weight_threshold: u64, }, HeadNotLate, NotProposing, @@ -288,9 +294,21 @@ impl std::fmt::Display for DoNotReOrg { ), Self::HeadNotWeak { head_weight, - re_org_weight_threshold, + re_org_head_weight_threshold, } => { - write!(f, "head not weak ({head_weight}/{re_org_weight_threshold})") + write!( + f, + "head not weak ({head_weight}/{re_org_head_weight_threshold})" + ) + } + Self::ParentNotStrong { + parent_weight, + re_org_parent_weight_threshold, + } => { + write!( + f, + "parent not strong ({parent_weight}/{re_org_parent_weight_threshold})" + ) } Self::HeadNotLate => { write!(f, "head arrived on time") @@ -486,12 +504,14 @@ impl ProtoArrayForkChoice { /// Get the block to propose on during `current_slot`. /// /// This function returns a *definitive* result which should be acted on. + #[allow(clippy::too_many_arguments)] pub fn get_proposer_head( &self, current_slot: Slot, canonical_head: Hash256, justified_balances: &JustifiedBalances, - re_org_threshold: ReOrgThreshold, + re_org_head_threshold: ReOrgThreshold, + re_org_parent_threshold: ReOrgThreshold, disallowed_offsets: &DisallowedReOrgOffsets, max_epochs_since_finalization: Epoch, ) -> Result> { @@ -499,7 +519,8 @@ impl ProtoArrayForkChoice { current_slot, canonical_head, justified_balances, - re_org_threshold, + re_org_head_threshold, + re_org_parent_threshold, disallowed_offsets, max_epochs_since_finalization, )?; @@ -510,14 +531,26 @@ impl ProtoArrayForkChoice { return Err(DoNotReOrg::HeadDistance.into()); } - // Only re-org if the head's weight is less than the configured committee fraction. + // Only re-org if the head's weight is less than the heads configured committee fraction. let head_weight = info.head_node.weight; - let re_org_weight_threshold = info.re_org_weight_threshold; - let weak_head = head_weight < re_org_weight_threshold; + let re_org_head_weight_threshold = info.re_org_head_weight_threshold; + let weak_head = head_weight < re_org_head_weight_threshold; if !weak_head { return Err(DoNotReOrg::HeadNotWeak { head_weight, - re_org_weight_threshold, + re_org_head_weight_threshold, + } + .into()); + } + + // Only re-org if the parent's weight is greater than the parents configured committee fraction. + let parent_weight = info.parent_node.weight; + let re_org_parent_weight_threshold = info.re_org_parent_weight_threshold; + let parent_strong = parent_weight > re_org_parent_weight_threshold; + if !parent_strong { + return Err(DoNotReOrg::ParentNotStrong { + parent_weight, + re_org_parent_weight_threshold, } .into()); } @@ -529,12 +562,14 @@ impl ProtoArrayForkChoice { /// Get information about the block to propose on during `current_slot`. /// /// This function returns a *partial* result which must be processed further. + #[allow(clippy::too_many_arguments)] pub fn get_proposer_head_info( &self, current_slot: Slot, canonical_head: Hash256, justified_balances: &JustifiedBalances, - re_org_threshold: ReOrgThreshold, + re_org_head_threshold: ReOrgThreshold, + re_org_parent_threshold: ReOrgThreshold, disallowed_offsets: &DisallowedReOrgOffsets, max_epochs_since_finalization: Epoch, ) -> Result> { @@ -595,15 +630,20 @@ impl ProtoArrayForkChoice { return Err(DoNotReOrg::JustificationAndFinalizationNotCompetitive.into()); } - // Compute re-org weight threshold. - let re_org_weight_threshold = - calculate_committee_fraction::(justified_balances, re_org_threshold.0) + // Compute re-org weight thresholds for head and parent. + let re_org_head_weight_threshold = + calculate_committee_fraction::(justified_balances, re_org_head_threshold.0) + .ok_or(Error::ReOrgThresholdOverflow)?; + + let re_org_parent_weight_threshold = + calculate_committee_fraction::(justified_balances, re_org_parent_threshold.0) .ok_or(Error::ReOrgThresholdOverflow)?; Ok(ProposerHeadInfo { head_node, parent_node, - re_org_weight_threshold, + re_org_head_weight_threshold, + re_org_parent_weight_threshold, current_slot, }) } diff --git a/consensus/proto_array/src/ssz_container.rs b/consensus/proto_array/src/ssz_container.rs index de7fa70d6a..3208584dc4 100644 --- a/consensus/proto_array/src/ssz_container.rs +++ b/consensus/proto_array/src/ssz_container.rs @@ -7,7 +7,6 @@ use crate::{ use ssz::{four_byte_option_impl, Encode}; use ssz_derive::{Decode, Encode}; use std::collections::HashMap; -use std::convert::TryFrom; use superstruct::superstruct; use types::{Checkpoint, Hash256}; diff --git a/consensus/state_processing/Cargo.toml b/consensus/state_processing/Cargo.toml index 7279fd28fa..be5367eb08 100644 --- a/consensus/state_processing/Cargo.toml +++ b/consensus/state_processing/Cargo.toml @@ -40,4 +40,4 @@ arbitrary-fuzz = [ "ssz_types/arbitrary", "tree_hash/arbitrary", ] -portable = ["bls/supranational-portable"] \ No newline at end of file +portable = ["bls/supranational-portable"] diff --git a/consensus/state_processing/src/all_caches.rs b/consensus/state_processing/src/all_caches.rs new file mode 100644 index 0000000000..106692c63a --- /dev/null +++ b/consensus/state_processing/src/all_caches.rs @@ -0,0 +1,52 @@ +use crate::common::update_progressive_balances_cache::initialize_progressive_balances_cache; +use crate::epoch_cache::initialize_epoch_cache; +use types::{BeaconState, ChainSpec, EpochCacheError, EthSpec, Hash256, RelativeEpoch}; + +/// Mixin trait for the beacon state that provides operations on *all* caches. +/// +/// The reason this trait exists here away from `BeaconState` itself is that some caches are +/// computed by functions in `state_processing`. +pub trait AllCaches { + /// Build all caches. + /// + /// Note that this excludes the tree-hash cache. That needs to be managed separately. + fn build_all_caches(&mut self, spec: &ChainSpec) -> Result<(), EpochCacheError>; + + /// Return true if all caches are built. + /// + /// Note that this excludes the tree-hash cache. That needs to be managed separately. + fn all_caches_built(&self) -> bool; +} + +impl AllCaches for BeaconState { + fn build_all_caches(&mut self, spec: &ChainSpec) -> Result<(), EpochCacheError> { + self.build_caches(spec)?; + initialize_epoch_cache(self, spec)?; + initialize_progressive_balances_cache(self, spec)?; + Ok(()) + } + + fn all_caches_built(&self) -> bool { + let current_epoch = self.current_epoch(); + let Ok(epoch_cache_decision_block_root) = + self.proposer_shuffling_decision_root(Hash256::zero()) + else { + return false; + }; + self.get_total_active_balance_at_epoch(current_epoch) + .is_ok() + && self.committee_cache_is_initialized(RelativeEpoch::Previous) + && self.committee_cache_is_initialized(RelativeEpoch::Current) + && self.committee_cache_is_initialized(RelativeEpoch::Next) + && self + .progressive_balances_cache() + .is_initialized_at(current_epoch) + && self.pubkey_cache().len() == self.validators().len() + && self.exit_cache().check_initialized().is_ok() + && self.slashings_cache_is_initialized() + && self + .epoch_cache() + .check_validity(current_epoch, epoch_cache_decision_block_root) + .is_ok() + } +} diff --git a/consensus/state_processing/src/common/altair.rs b/consensus/state_processing/src/common/altair.rs index 8943ef2f40..4380154133 100644 --- a/consensus/state_processing/src/common/altair.rs +++ b/consensus/state_processing/src/common/altair.rs @@ -24,14 +24,12 @@ impl BaseRewardPerIncrement { /// shown to be a significant optimisation. /// /// Spec v1.1.0 -pub fn get_base_reward( - state: &BeaconState, - index: usize, +pub fn get_base_reward( + validator_effective_balance: u64, base_reward_per_increment: BaseRewardPerIncrement, spec: &ChainSpec, ) -> Result { - state - .get_effective_balance(index)? + validator_effective_balance .safe_div(spec.effective_balance_increment)? .safe_mul(base_reward_per_increment.as_u64()) .map_err(Into::into) diff --git a/consensus/state_processing/src/common/base.rs b/consensus/state_processing/src/common/base.rs index a8d04ad6cd..d582e0bea2 100644 --- a/consensus/state_processing/src/common/base.rs +++ b/consensus/state_processing/src/common/base.rs @@ -1,31 +1,30 @@ use integer_sqrt::IntegerSquareRoot; -use safe_arith::SafeArith; +use safe_arith::{ArithError, SafeArith}; use types::*; -/// Returns the base reward for some validator. -pub fn get_base_reward( - state: &BeaconState, - index: usize, - // Should be == get_total_active_balance(state, spec) - total_active_balance: u64, - spec: &ChainSpec, -) -> Result { - state - .get_effective_balance(index)? - .safe_mul(spec.base_reward_factor)? - .safe_div(total_active_balance.integer_sqrt())? - .safe_div(spec.base_rewards_per_epoch) - .map_err(Into::into) +/// This type exists to avoid confusing `total_active_balance` with `sqrt_total_active_balance`, +/// since they are used in close proximity and have the same type (`u64`). +#[derive(Copy, Clone)] +pub struct SqrtTotalActiveBalance(u64); + +impl SqrtTotalActiveBalance { + pub fn new(total_active_balance: u64) -> Self { + Self(total_active_balance.integer_sqrt()) + } + + pub fn as_u64(&self) -> u64 { + self.0 + } } -pub fn get_base_reward_from_effective_balance( - effective_balance: u64, - total_active_balance: u64, +/// Returns the base reward for some validator. +pub fn get_base_reward( + validator_effective_balance: u64, + sqrt_total_active_balance: SqrtTotalActiveBalance, spec: &ChainSpec, -) -> Result { - effective_balance +) -> Result { + validator_effective_balance .safe_mul(spec.base_reward_factor)? - .safe_div(total_active_balance.integer_sqrt())? + .safe_div(sqrt_total_active_balance.as_u64())? .safe_div(spec.base_rewards_per_epoch) - .map_err(Into::into) } diff --git a/consensus/state_processing/src/common/get_attestation_participation.rs b/consensus/state_processing/src/common/get_attestation_participation.rs index e4e30230af..d27a00c382 100644 --- a/consensus/state_processing/src/common/get_attestation_participation.rs +++ b/consensus/state_processing/src/common/get_attestation_participation.rs @@ -16,8 +16,8 @@ use types::{AttestationData, BeaconState, ChainSpec, EthSpec}; /// /// This function will return an error if the source of the attestation doesn't match the /// state's relevant justified checkpoint. -pub fn get_attestation_participation_flag_indices( - state: &BeaconState, +pub fn get_attestation_participation_flag_indices( + state: &BeaconState, data: &AttestationData, inclusion_delay: u64, spec: &ChainSpec, @@ -41,7 +41,7 @@ pub fn get_attestation_participation_flag_indices( // Participation flag indices let mut participation_flag_indices = SmallVec::new(); - if is_matching_source && inclusion_delay <= T::slots_per_epoch().integer_sqrt() { + if is_matching_source && inclusion_delay <= E::slots_per_epoch().integer_sqrt() { participation_flag_indices.push(TIMELY_SOURCE_FLAG_INDEX); } match state { @@ -49,11 +49,11 @@ pub fn get_attestation_participation_flag_indices( | &BeaconState::Altair(_) | &BeaconState::Merge(_) | &BeaconState::Capella(_) => { - if is_matching_target && inclusion_delay <= T::slots_per_epoch() { + if is_matching_target && inclusion_delay <= E::slots_per_epoch() { participation_flag_indices.push(TIMELY_TARGET_FLAG_INDEX); } } - &BeaconState::Deneb(_) => { + &BeaconState::Deneb(_) | &BeaconState::Electra(_) => { if is_matching_target { // [Modified in Deneb:EIP7045] participation_flag_indices.push(TIMELY_TARGET_FLAG_INDEX); diff --git a/consensus/state_processing/src/common/get_attesting_indices.rs b/consensus/state_processing/src/common/get_attesting_indices.rs index d7d02c3601..a89b71ff2b 100644 --- a/consensus/state_processing/src/common/get_attesting_indices.rs +++ b/consensus/state_processing/src/common/get_attesting_indices.rs @@ -1,9 +1,9 @@ use types::*; /// Returns validator indices which participated in the attestation, sorted by increasing index. -pub fn get_attesting_indices( +pub fn get_attesting_indices( committee: &[usize], - bitlist: &BitList, + bitlist: &BitList, ) -> Result, BeaconStateError> { if bitlist.len() != committee.len() { return Err(BeaconStateError::InvalidBitfield); @@ -23,10 +23,10 @@ pub fn get_attesting_indices( } /// Shortcut for getting the attesting indices while fetching the committee from the state's cache. -pub fn get_attesting_indices_from_state( - state: &BeaconState, - att: &Attestation, +pub fn get_attesting_indices_from_state( + state: &BeaconState, + att: &Attestation, ) -> Result, BeaconStateError> { let committee = state.get_beacon_committee(att.data.slot, att.data.index)?; - get_attesting_indices::(committee.committee, &att.aggregation_bits) + get_attesting_indices::(committee.committee, &att.aggregation_bits) } diff --git a/consensus/state_processing/src/common/get_indexed_attestation.rs b/consensus/state_processing/src/common/get_indexed_attestation.rs index 63f63698e4..9cf689df40 100644 --- a/consensus/state_processing/src/common/get_indexed_attestation.rs +++ b/consensus/state_processing/src/common/get_indexed_attestation.rs @@ -7,11 +7,11 @@ type Result = std::result::Result>; /// Convert `attestation` to (almost) indexed-verifiable form. /// /// Spec v0.12.1 -pub fn get_indexed_attestation( +pub fn get_indexed_attestation( committee: &[usize], - attestation: &Attestation, -) -> Result> { - let attesting_indices = get_attesting_indices::(committee, &attestation.aggregation_bits)?; + attestation: &Attestation, +) -> Result> { + let attesting_indices = get_attesting_indices::(committee, &attestation.aggregation_bits)?; Ok(IndexedAttestation { attesting_indices: VariableList::new(attesting_indices)?, diff --git a/consensus/state_processing/src/common/initiate_validator_exit.rs b/consensus/state_processing/src/common/initiate_validator_exit.rs index 85e5e1df1d..c527807df8 100644 --- a/consensus/state_processing/src/common/initiate_validator_exit.rs +++ b/consensus/state_processing/src/common/initiate_validator_exit.rs @@ -3,15 +3,17 @@ use std::cmp::max; use types::{BeaconStateError as Error, *}; /// Initiate the exit of the validator of the given `index`. -pub fn initiate_validator_exit( - state: &mut BeaconState, +pub fn initiate_validator_exit( + state: &mut BeaconState, index: usize, spec: &ChainSpec, ) -> Result<(), Error> { - // Return if the validator already initiated exit - if state.get_validator(index)?.exit_epoch != spec.far_future_epoch { - return Ok(()); - } + // We do things in a slightly different order to the spec here. Instead of immediately checking + // whether the validator has already exited, we instead prepare the exit cache and compute the + // cheap-to-calculate values from that. *Then* we look up the validator a single time in the + // validator tree (expensive), make the check and mutate as appropriate. Compared to the spec + // ordering, this saves us from looking up the validator in the validator registry multiple + // times. // Ensure the exit cache is built. state.build_exit_cache(spec)?; @@ -28,12 +30,20 @@ pub fn initiate_validator_exit( exit_queue_epoch.safe_add_assign(1)?; } + let validator = state.get_validator_mut(index)?; + + // Return if the validator already initiated exit + if validator.exit_epoch != spec.far_future_epoch { + return Ok(()); + } + + validator.exit_epoch = exit_queue_epoch; + validator.withdrawable_epoch = + exit_queue_epoch.safe_add(spec.min_validator_withdrawability_delay)?; + state .exit_cache_mut() .record_validator_exit(exit_queue_epoch)?; - state.get_validator_mut(index)?.exit_epoch = exit_queue_epoch; - state.get_validator_mut(index)?.withdrawable_epoch = - exit_queue_epoch.safe_add(spec.min_validator_withdrawability_delay)?; Ok(()) } diff --git a/consensus/state_processing/src/common/mod.rs b/consensus/state_processing/src/common/mod.rs index ffe8be3a04..cefc47b023 100644 --- a/consensus/state_processing/src/common/mod.rs +++ b/consensus/state_processing/src/common/mod.rs @@ -25,8 +25,7 @@ pub fn increase_balance( index: usize, delta: u64, ) -> Result<(), BeaconStateError> { - state.get_balance_mut(index)?.safe_add_assign(delta)?; - Ok(()) + increase_balance_directly(state.get_balance_mut(index)?, delta) } /// Decrease the balance of a validator, saturating upon overflow, as per the spec. @@ -35,7 +34,17 @@ pub fn decrease_balance( index: usize, delta: u64, ) -> Result<(), BeaconStateError> { - let balance = state.get_balance_mut(index)?; + decrease_balance_directly(state.get_balance_mut(index)?, delta) +} + +/// Increase the balance of a validator, erroring upon overflow, as per the spec. +pub fn increase_balance_directly(balance: &mut u64, delta: u64) -> Result<(), BeaconStateError> { + balance.safe_add_assign(delta)?; + Ok(()) +} + +/// Decrease the balance of a validator, saturating upon overflow, as per the spec. +pub fn decrease_balance_directly(balance: &mut u64, delta: u64) -> Result<(), BeaconStateError> { *balance = balance.saturating_sub(delta); Ok(()) } diff --git a/consensus/state_processing/src/common/slash_validator.rs b/consensus/state_processing/src/common/slash_validator.rs index d8b1c1a107..16b4e74ece 100644 --- a/consensus/state_processing/src/common/slash_validator.rs +++ b/consensus/state_processing/src/common/slash_validator.rs @@ -12,14 +12,15 @@ use types::{ }; /// Slash the validator with index `slashed_index`. -pub fn slash_validator( - state: &mut BeaconState, +pub fn slash_validator( + state: &mut BeaconState, slashed_index: usize, opt_whistleblower_index: Option, - ctxt: &mut ConsensusContext, + ctxt: &mut ConsensusContext, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { let epoch = state.current_epoch(); + let latest_block_slot = state.latest_block_header().slot; initiate_validator_exit(state, slashed_index, spec)?; @@ -27,7 +28,7 @@ pub fn slash_validator( validator.slashed = true; validator.withdrawable_epoch = cmp::max( validator.withdrawable_epoch, - epoch.safe_add(T::EpochsPerSlashingsVector::to_u64())?, + epoch.safe_add(E::EpochsPerSlashingsVector::to_u64())?, ); let validator_effective_balance = validator.effective_balance; state.set_slashings( @@ -44,7 +45,10 @@ pub fn slash_validator( .safe_div(spec.min_slashing_penalty_quotient_for_state(state))?, )?; - update_progressive_balances_on_slashing(state, slashed_index)?; + update_progressive_balances_on_slashing(state, slashed_index, validator_effective_balance)?; + state + .slashings_cache_mut() + .record_validator_slashing(latest_block_slot, slashed_index)?; // Apply proposer and whistleblower rewards let proposer_index = ctxt.get_proposer_index(state, spec)? as usize; @@ -56,7 +60,8 @@ pub fn slash_validator( BeaconState::Altair(_) | BeaconState::Merge(_) | BeaconState::Capella(_) - | BeaconState::Deneb(_) => whistleblower_reward + | BeaconState::Deneb(_) + | BeaconState::Electra(_) => whistleblower_reward .safe_mul(PROPOSER_WEIGHT)? .safe_div(WEIGHT_DENOMINATOR)?, }; diff --git a/consensus/state_processing/src/common/update_progressive_balances_cache.rs b/consensus/state_processing/src/common/update_progressive_balances_cache.rs index 45b5d657a6..af843b3acb 100644 --- a/consensus/state_processing/src/common/update_progressive_balances_cache.rs +++ b/consensus/state_processing/src/common/update_progressive_balances_cache.rs @@ -3,23 +3,16 @@ 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, + is_progressive_balances_enabled, BeaconState, BeaconStateError, ChainSpec, Epoch, + EpochTotalBalances, EthSpec, ParticipationFlags, ProgressiveBalancesCache, Validator, }; -/// Initializes the `ProgressiveBalancesCache` cache using balance values from the -/// `ParticipationCache`. If the optional `&ParticipationCache` is not supplied, it will be computed -/// from the `BeaconState`. +/// Initializes the `ProgressiveBalancesCache` if it is unbuilt. pub fn initialize_progressive_balances_cache( state: &mut BeaconState, - maybe_participation_cache: Option<&ParticipationCache>, spec: &ChainSpec, ) -> Result<(), BeaconStateError> { if !is_progressive_balances_enabled(state) @@ -28,24 +21,42 @@ pub fn initialize_progressive_balances_cache( 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:?}")))?; - + // Calculate the total flag balances for previous & current epoch in a single iteration. + // This calculates `get_total_balance(unslashed_participating_indices(..))` for each flag in + // the current and previous epoch. let current_epoch = state.current_epoch(); + let previous_epoch = state.previous_epoch(); + let mut previous_epoch_cache = EpochTotalBalances::new(spec); + let mut current_epoch_cache = EpochTotalBalances::new(spec); + for ((validator, current_epoch_flags), previous_epoch_flags) in state + .validators() + .iter() + .zip(state.current_epoch_participation()?) + .zip(state.previous_epoch_participation()?) + { + // Exclude slashed validators. We are calculating *unslashed* participating totals. + if validator.slashed { + continue; + } + + // Update current epoch flag balances. + if validator.is_active_at(current_epoch) { + update_flag_total_balances(&mut current_epoch_cache, *current_epoch_flags, validator)?; + } + // Update previous epoch flag balances. + if validator.is_active_at(previous_epoch) { + update_flag_total_balances( + &mut previous_epoch_cache, + *previous_epoch_flags, + validator, + )?; + } + } + state.progressive_balances_cache_mut().initialize( current_epoch, - previous_epoch_target_attesting_balance, - current_epoch_target_attesting_balance, + previous_epoch_cache, + current_epoch_cache, ); update_progressive_balances_metrics(state.progressive_balances_cache())?; @@ -53,43 +64,65 @@ pub fn initialize_progressive_balances_cache( Ok(()) } -/// Updates the `ProgressiveBalancesCache` when a new target attestation has been processed. -pub fn update_progressive_balances_on_attestation( - state: &mut BeaconState, - 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)?; +/// During the initialization of the progressive balances for a single epoch, add +/// `validator.effective_balance` to the flag total, for each flag present in `participation_flags`. +/// +/// Pre-conditions: +/// +/// - `validator` must not be slashed +/// - the `participation_flags` must be for `validator` in the same epoch as the `total_balances` +fn update_flag_total_balances( + total_balances: &mut EpochTotalBalances, + participation_flags: ParticipationFlags, + validator: &Validator, +) -> Result<(), BeaconStateError> { + for (flag, balance) in total_balances.total_flag_balances.iter_mut().enumerate() { + if participation_flags.has_flag(flag)? { + balance.safe_add_assign(validator.effective_balance)?; } } Ok(()) } -/// Updates the `ProgressiveBalancesCache` when a target attester has been slashed. -pub fn update_progressive_balances_on_slashing( - state: &mut BeaconState, - validator_index: usize, +/// Updates the `ProgressiveBalancesCache` when a new target attestation has been processed. +pub fn update_progressive_balances_on_attestation( + state: &mut BeaconState, + epoch: Epoch, + flag_index: usize, + validator_effective_balance: u64, + validator_slashed: bool, ) -> 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::(previous_epoch_participation, validator_index)?; + state.progressive_balances_cache_mut().on_new_attestation( + epoch, + validator_slashed, + flag_index, + validator_effective_balance, + )?; + } + Ok(()) +} - let current_epoch_participation = state.current_epoch_participation()?; - let is_current_epoch_target_attester = - is_target_attester_in_epoch::(current_epoch_participation, validator_index)?; +/// Updates the `ProgressiveBalancesCache` when a target attester has been slashed. +pub fn update_progressive_balances_on_slashing( + state: &mut BeaconState, + validator_index: usize, + validator_effective_balance: u64, +) -> Result<(), BlockProcessingError> { + if is_progressive_balances_enabled(state) { + let previous_epoch_participation = *state + .previous_epoch_participation()? + .get(validator_index) + .ok_or(BeaconStateError::UnknownValidator(validator_index))?; - let validator_effective_balance = state.get_effective_balance(validator_index)?; + let current_epoch_participation = *state + .current_epoch_participation()? + .get(validator_index) + .ok_or(BeaconStateError::UnknownValidator(validator_index))?; state.progressive_balances_cache_mut().on_slashing( - is_previous_epoch_target_attester, - is_current_epoch_target_attester, + previous_epoch_participation, + current_epoch_participation, validator_effective_balance, )?; } @@ -98,8 +131,8 @@ pub fn update_progressive_balances_on_slashing( } /// Updates the `ProgressiveBalancesCache` on epoch transition. -pub fn update_progressive_balances_on_epoch_transition( - state: &mut BeaconState, +pub fn update_progressive_balances_on_epoch_transition( + state: &mut BeaconState, spec: &ChainSpec, ) -> Result<(), EpochProcessingError> { if is_progressive_balances_enabled(state) { @@ -128,15 +161,3 @@ pub fn update_progressive_balances_metrics( Ok(()) } - -fn is_target_attester_in_epoch( - epoch_participation: &VariableList, - validator_index: usize, -) -> Result { - 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/consensus_context.rs b/consensus/state_processing/src/consensus_context.rs index 8e49a0d498..263539fa42 100644 --- a/consensus/state_processing/src/consensus_context.rs +++ b/consensus/state_processing/src/consensus_context.rs @@ -1,5 +1,6 @@ use crate::common::get_indexed_attestation; use crate::per_block_processing::errors::{AttestationInvalid, BlockOperationError}; +use crate::EpochCacheError; use ssz_derive::{Decode, Encode}; use std::collections::{hash_map::Entry, HashMap}; use tree_hash::TreeHash; @@ -9,9 +10,13 @@ use types::{ }; #[derive(Debug, PartialEq, Clone, Encode, Decode)] -pub struct ConsensusContext { +pub struct ConsensusContext { /// Slot to act as an identifier/safeguard slot: Slot, + /// Previous epoch of the `slot` precomputed for optimization purpose. + pub(crate) previous_epoch: Epoch, + /// Current epoch of the `slot` precomputed for optimization purpose. + pub(crate) current_epoch: Epoch, /// Proposer index of the block at `slot`. proposer_index: Option, /// Block root of the block at `slot`. @@ -20,12 +25,13 @@ pub struct ConsensusContext { /// We can skip serializing / deserializing this as the cache will just be rebuilt #[ssz(skip_serializing, skip_deserializing)] indexed_attestations: - HashMap<(AttestationData, BitList), IndexedAttestation>, + HashMap<(AttestationData, BitList), IndexedAttestation>, } #[derive(Debug, PartialEq, Clone)] pub enum ContextError { BeaconState(BeaconStateError), + EpochCache(EpochCacheError), SlotMismatch { slot: Slot, expected: Slot }, EpochMismatch { epoch: Epoch, expected: Epoch }, } @@ -36,10 +42,20 @@ impl From for ContextError { } } -impl ConsensusContext { +impl From for ContextError { + fn from(e: EpochCacheError) -> Self { + Self::EpochCache(e) + } +} + +impl ConsensusContext { pub fn new(slot: Slot) -> Self { + let current_epoch = slot.epoch(E::slots_per_epoch()); + let previous_epoch = current_epoch.saturating_sub(1u64); Self { slot, + previous_epoch, + current_epoch, proposer_index: None, current_block_root: None, indexed_attestations: HashMap::new(), @@ -58,7 +74,7 @@ impl ConsensusContext { /// required. If the slot check is too restrictive, see `get_proposer_index_from_epoch_state`. pub fn get_proposer_index( &mut self, - state: &BeaconState, + state: &BeaconState, spec: &ChainSpec, ) -> Result { self.check_slot(state.slot())?; @@ -72,7 +88,7 @@ impl ConsensusContext { /// we want to extract the proposer index from a single state for every slot in the epoch. pub fn get_proposer_index_from_epoch_state( &mut self, - state: &BeaconState, + state: &BeaconState, spec: &ChainSpec, ) -> Result { self.check_epoch(state.current_epoch())?; @@ -81,7 +97,7 @@ impl ConsensusContext { fn get_proposer_index_no_checks( &mut self, - state: &BeaconState, + state: &BeaconState, spec: &ChainSpec, ) -> Result { if let Some(proposer_index) = self.proposer_index { @@ -98,9 +114,9 @@ impl ConsensusContext { self } - pub fn get_current_block_root>( + pub fn get_current_block_root>( &mut self, - block: &SignedBeaconBlock, + block: &SignedBeaconBlock, ) -> Result { self.check_slot(block.slot())?; @@ -125,7 +141,7 @@ impl ConsensusContext { } fn check_epoch(&self, epoch: Epoch) -> Result<(), ContextError> { - let expected = self.slot.epoch(T::slots_per_epoch()); + let expected = self.slot.epoch(E::slots_per_epoch()); if epoch == expected { Ok(()) } else { @@ -135,9 +151,9 @@ impl ConsensusContext { pub fn get_indexed_attestation( &mut self, - state: &BeaconState, - attestation: &Attestation, - ) -> Result<&IndexedAttestation, BlockOperationError> { + state: &BeaconState, + attestation: &Attestation, + ) -> Result<&IndexedAttestation, BlockOperationError> { let key = ( attestation.data.clone(), attestation.aggregation_bits.clone(), diff --git a/consensus/state_processing/src/epoch_cache.rs b/consensus/state_processing/src/epoch_cache.rs new file mode 100644 index 0000000000..b2f2d85407 --- /dev/null +++ b/consensus/state_processing/src/epoch_cache.rs @@ -0,0 +1,139 @@ +use crate::common::altair::BaseRewardPerIncrement; +use crate::common::base::SqrtTotalActiveBalance; +use crate::common::{altair, base}; +use safe_arith::SafeArith; +use types::epoch_cache::{EpochCache, EpochCacheError, EpochCacheKey}; +use types::{ActivationQueue, BeaconState, ChainSpec, EthSpec, ForkName, Hash256}; + +/// Precursor to an `EpochCache`. +pub struct PreEpochCache { + epoch_key: EpochCacheKey, + effective_balances: Vec, +} + +impl PreEpochCache { + pub fn new_for_next_epoch( + state: &mut BeaconState, + ) -> Result { + // The decision block root for the next epoch is the latest block root from this epoch. + let latest_block_header = state.latest_block_header(); + + let decision_block_root = if !latest_block_header.state_root.is_zero() { + latest_block_header.canonical_root() + } else { + // State root should already have been filled in by `process_slot`, except in the case + // of a `partial_state_advance`. Once we have tree-states this can be an error, and + // `self` can be immutable. + let state_root = state.update_tree_hash_cache()?; + state.get_latest_block_root(state_root) + }; + + let epoch_key = EpochCacheKey { + epoch: state.next_epoch()?, + decision_block_root, + }; + + Ok(Self { + epoch_key, + effective_balances: Vec::with_capacity(state.validators().len()), + }) + } + + pub fn push_effective_balance(&mut self, effective_balance: u64) { + self.effective_balances.push(effective_balance); + } + + pub fn into_epoch_cache( + self, + total_active_balance: u64, + activation_queue: ActivationQueue, + spec: &ChainSpec, + ) -> Result { + let epoch = self.epoch_key.epoch; + let sqrt_total_active_balance = SqrtTotalActiveBalance::new(total_active_balance); + let base_reward_per_increment = BaseRewardPerIncrement::new(total_active_balance, spec)?; + + let effective_balance_increment = spec.effective_balance_increment; + let max_effective_balance_eth = spec + .max_effective_balance + .safe_div(effective_balance_increment)?; + + let mut base_rewards = Vec::with_capacity(max_effective_balance_eth.safe_add(1)? as usize); + + for effective_balance_eth in 0..=max_effective_balance_eth { + let effective_balance = effective_balance_eth.safe_mul(effective_balance_increment)?; + let base_reward = if spec.fork_name_at_epoch(epoch) == ForkName::Base { + base::get_base_reward(effective_balance, sqrt_total_active_balance, spec)? + } else { + altair::get_base_reward(effective_balance, base_reward_per_increment, spec)? + }; + base_rewards.push(base_reward); + } + + Ok(EpochCache::new( + self.epoch_key, + self.effective_balances, + base_rewards, + activation_queue, + spec, + )) + } +} + +pub fn is_epoch_cache_initialized( + state: &BeaconState, +) -> Result { + let current_epoch = state.current_epoch(); + let epoch_cache: &EpochCache = state.epoch_cache(); + let decision_block_root = state + .proposer_shuffling_decision_root(Hash256::zero()) + .map_err(EpochCacheError::BeaconState)?; + + Ok(epoch_cache + .check_validity(current_epoch, decision_block_root) + .is_ok()) +} + +pub fn initialize_epoch_cache( + state: &mut BeaconState, + spec: &ChainSpec, +) -> Result<(), EpochCacheError> { + if is_epoch_cache_initialized(state)? { + // `EpochCache` has already been initialized and is valid, no need to initialize. + return Ok(()); + } + + let current_epoch = state.current_epoch(); + let next_epoch = state.next_epoch().map_err(EpochCacheError::BeaconState)?; + let decision_block_root = state + .proposer_shuffling_decision_root(Hash256::zero()) + .map_err(EpochCacheError::BeaconState)?; + + state.build_total_active_balance_cache(spec)?; + let total_active_balance = state.get_total_active_balance_at_epoch(current_epoch)?; + + // Collect effective balances and compute activation queue. + let mut effective_balances = Vec::with_capacity(state.validators().len()); + let mut activation_queue = ActivationQueue::default(); + + for (index, validator) in state.validators().iter().enumerate() { + effective_balances.push(validator.effective_balance); + + // Add to speculative activation queue. + activation_queue + .add_if_could_be_eligible_for_activation(index, validator, next_epoch, spec); + } + + // Compute base rewards. + let pre_epoch_cache = PreEpochCache { + epoch_key: EpochCacheKey { + epoch: current_epoch, + decision_block_root, + }, + effective_balances, + }; + *state.epoch_cache_mut() = + pre_epoch_cache.into_epoch_cache(total_active_balance, activation_queue, spec)?; + + Ok(()) +} diff --git a/consensus/state_processing/src/genesis.rs b/consensus/state_processing/src/genesis.rs index 284a7019f3..b225923b41 100644 --- a/consensus/state_processing/src/genesis.rs +++ b/consensus/state_processing/src/genesis.rs @@ -4,20 +4,20 @@ use super::per_block_processing::{ use crate::common::DepositDataTree; use crate::upgrade::{ upgrade_to_altair, upgrade_to_bellatrix, upgrade_to_capella, upgrade_to_deneb, + upgrade_to_electra, }; use safe_arith::{ArithError, SafeArith}; use tree_hash::TreeHash; -use types::DEPOSIT_TREE_DEPTH; use types::*; /// Initialize a `BeaconState` from genesis data. -pub fn initialize_beacon_state_from_eth1( +pub fn initialize_beacon_state_from_eth1( eth1_block_hash: Hash256, eth1_timestamp: u64, deposits: Vec, - execution_payload_header: Option>, + execution_payload_header: Option>, spec: &ChainSpec, -) -> Result, BlockProcessingError> { +) -> Result, BlockProcessingError> { let genesis_time = eth2_genesis_time(eth1_timestamp, spec)?; let eth1_data = Eth1Data { // Temporary deposit root @@ -51,7 +51,7 @@ pub fn initialize_beacon_state_from_eth1( // https://github.com/ethereum/eth2.0-specs/pull/2323 if spec .altair_fork_epoch - .map_or(false, |fork_epoch| fork_epoch == T::genesis_epoch()) + .map_or(false, |fork_epoch| fork_epoch == E::genesis_epoch()) { upgrade_to_altair(&mut state, spec)?; @@ -61,7 +61,7 @@ pub fn initialize_beacon_state_from_eth1( // Similarly, perform an upgrade to the merge if configured from genesis. if spec .bellatrix_fork_epoch - .map_or(false, |fork_epoch| fork_epoch == T::genesis_epoch()) + .map_or(false, |fork_epoch| fork_epoch == E::genesis_epoch()) { // this will set state.latest_execution_payload_header = ExecutionPayloadHeaderMerge::default() upgrade_to_bellatrix(&mut state, spec)?; @@ -79,7 +79,7 @@ pub fn initialize_beacon_state_from_eth1( // Upgrade to capella if configured from genesis if spec .capella_fork_epoch - .map_or(false, |fork_epoch| fork_epoch == T::genesis_epoch()) + .map_or(false, |fork_epoch| fork_epoch == E::genesis_epoch()) { upgrade_to_capella(&mut state, spec)?; @@ -96,7 +96,7 @@ pub fn initialize_beacon_state_from_eth1( // Upgrade to deneb if configured from genesis if spec .deneb_fork_epoch - .map_or(false, |fork_epoch| fork_epoch == T::genesis_epoch()) + .map_or(false, |fork_epoch| fork_epoch == E::genesis_epoch()) { upgrade_to_deneb(&mut state, spec)?; @@ -105,8 +105,25 @@ pub fn initialize_beacon_state_from_eth1( // Override latest execution payload header. // See https://github.com/ethereum/consensus-specs/blob/dev/specs/deneb/beacon-chain.md#testing - if let Some(ExecutionPayloadHeader::Deneb(header)) = execution_payload_header { - *state.latest_execution_payload_header_deneb_mut()? = header; + if let Some(ExecutionPayloadHeader::Deneb(ref header)) = execution_payload_header { + *state.latest_execution_payload_header_deneb_mut()? = header.clone(); + } + } + + // Upgrade to electra if configured from genesis. + if spec + .electra_fork_epoch + .map_or(false, |fork_epoch| fork_epoch == E::genesis_epoch()) + { + upgrade_to_electra(&mut state, spec)?; + + // Remove intermediate Deneb fork from `state.fork`. + state.fork_mut().previous_version = spec.electra_fork_version; + + // Override latest execution payload header. + // See https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#testing + if let Some(ExecutionPayloadHeader::Electra(header)) = execution_payload_header { + *state.latest_execution_payload_header_electra_mut()? = header.clone(); } } @@ -120,9 +137,9 @@ pub fn initialize_beacon_state_from_eth1( } /// Determine whether a candidate genesis state is suitable for starting the chain. -pub fn is_valid_genesis_state(state: &BeaconState, spec: &ChainSpec) -> bool { +pub fn is_valid_genesis_state(state: &BeaconState, spec: &ChainSpec) -> bool { state - .get_active_validator_indices(T::genesis_epoch(), spec) + .get_active_validator_indices(E::genesis_epoch(), spec) .map_or(false, |active_validators| { state.genesis_time() >= spec.min_genesis_time && active_validators.len() as u64 >= spec.min_genesis_active_validator_count @@ -130,8 +147,8 @@ pub fn is_valid_genesis_state(state: &BeaconState, spec: &ChainSp } /// Activate genesis validators, if their balance is acceptable. -pub fn process_activations( - state: &mut BeaconState, +pub fn process_activations( + state: &mut BeaconState, spec: &ChainSpec, ) -> Result<(), Error> { let (validators, balances, _) = state.validators_and_balances_and_progressive_balances_mut(); @@ -145,8 +162,8 @@ pub fn process_activations( spec.max_effective_balance, ); if validator.effective_balance == spec.max_effective_balance { - validator.activation_eligibility_epoch = T::genesis_epoch(); - validator.activation_epoch = T::genesis_epoch(); + validator.activation_eligibility_epoch = E::genesis_epoch(); + validator.activation_epoch = E::genesis_epoch(); } } Ok(()) diff --git a/consensus/state_processing/src/lib.rs b/consensus/state_processing/src/lib.rs index a3ee725406..7d84c426e8 100644 --- a/consensus/state_processing/src/lib.rs +++ b/consensus/state_processing/src/lib.rs @@ -16,9 +16,11 @@ mod macros; mod metrics; +pub mod all_caches; pub mod block_replayer; pub mod common; pub mod consensus_context; +pub mod epoch_cache; pub mod genesis; pub mod per_block_processing; pub mod per_epoch_processing; @@ -27,6 +29,7 @@ pub mod state_advance; pub mod upgrade; pub mod verify_operation; +pub use all_caches::AllCaches; pub use block_replayer::{BlockReplayError, BlockReplayer, StateProcessingStrategy}; pub use consensus_context::{ConsensusContext, ContextError}; pub use genesis::{ @@ -41,4 +44,5 @@ pub use per_epoch_processing::{ errors::EpochProcessingError, process_epoch as per_epoch_processing, }; pub use per_slot_processing::{per_slot_processing, Error as SlotProcessingError}; +pub use types::{EpochCache, EpochCacheError, EpochCacheKey}; pub use verify_operation::{SigVerifiedOp, VerifyOperation, VerifyOperationAt}; diff --git a/consensus/state_processing/src/metrics.rs b/consensus/state_processing/src/metrics.rs index d8a51135e8..e163f3b76b 100644 --- a/consensus/state_processing/src/metrics.rs +++ b/consensus/state_processing/src/metrics.rs @@ -17,9 +17,12 @@ lazy_static! { "beacon_participation_prev_epoch_source_attesting_gwei_total", "Total effective balance (gwei) of validators who attested to the source in the previous epoch" ); - pub static ref PARTICIPATION_PREV_EPOCH_ACTIVE_GWEI_TOTAL: Result = try_create_int_gauge( - "beacon_participation_prev_epoch_active_gwei_total", - "Total effective balance (gwei) of validators active in the previous epoch" + /* + * Processing metrics + */ + pub static ref PROCESS_EPOCH_TIME: Result = try_create_histogram( + "beacon_state_processing_process_epoch", + "Time required for process_epoch", ); /* * Participation Metrics (progressive balances) diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index b9a147a5ad..b370ec6216 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -45,6 +45,7 @@ use crate::StateProcessingStrategy; use crate::common::update_progressive_balances_cache::{ initialize_progressive_balances_cache, update_progressive_balances_metrics, }; +use crate::epoch_cache::initialize_epoch_cache; #[cfg(feature = "arbitrary-fuzz")] use arbitrary::Arbitrary; @@ -97,13 +98,13 @@ pub enum VerifyBlockRoot { /// re-calculating the root when it is already known. Note `block_root` should be equal to the /// tree hash root of the block, NOT the signing root of the block. This function takes /// care of mixing in the domain. -pub fn per_block_processing>( - state: &mut BeaconState, - signed_block: &SignedBeaconBlock, +pub fn per_block_processing>( + state: &mut BeaconState, + signed_block: &SignedBeaconBlock, block_signature_strategy: BlockSignatureStrategy, state_processing_strategy: StateProcessingStrategy, verify_block_root: VerifyBlockRoot, - ctxt: &mut ConsensusContext, + ctxt: &mut ConsensusContext, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { let block = signed_block.message(); @@ -118,7 +119,10 @@ pub fn per_block_processing>( .fork_name(spec) .map_err(BlockProcessingError::InconsistentStateFork)?; - initialize_progressive_balances_cache(state, None, spec)?; + // Build epoch cache if it hasn't already been built, or if it is no longer valid + initialize_epoch_cache(state, spec)?; + initialize_progressive_balances_cache(state, spec)?; + state.build_slashings_cache()?; let verify_signatures = match block_signature_strategy { BlockSignatureStrategy::VerifyBulk => { @@ -159,7 +163,7 @@ pub fn per_block_processing>( } else { verify_signatures }; - // Ensure the current and previous epoch caches are built. + // Ensure the current and previous epoch committee caches are built. state.build_committee_cache(RelativeEpoch::Previous, spec)?; state.build_committee_cache(RelativeEpoch::Current, spec)?; @@ -169,9 +173,9 @@ pub fn per_block_processing>( if is_execution_enabled(state, block.body()) { let body = block.body(); if state_processing_strategy == StateProcessingStrategy::Accurate { - process_withdrawals::(state, body.execution_payload()?, spec)?; + process_withdrawals::(state, body.execution_payload()?, spec)?; } - process_execution_payload::(state, body, spec)?; + process_execution_payload::(state, body, spec)?; } process_randao(state, block, verify_randao, ctxt, spec)?; @@ -196,11 +200,11 @@ pub fn per_block_processing>( } /// Processes the block header, returning the proposer index. -pub fn process_block_header( - state: &mut BeaconState, +pub fn process_block_header( + state: &mut BeaconState, block_header: BeaconBlockHeader, verify_block_root: VerifyBlockRoot, - ctxt: &mut ConsensusContext, + ctxt: &mut ConsensusContext, spec: &ChainSpec, ) -> Result> { // Verify that the slots match @@ -240,6 +244,9 @@ pub fn process_block_header( ); } + state + .slashings_cache_mut() + .update_latest_block_slot(block_header.slot); *state.latest_block_header_mut() = block_header; // Verify proposer is not slashed @@ -254,10 +261,10 @@ pub fn process_block_header( /// Verifies the signature of a block. /// /// Spec v0.12.1 -pub fn verify_block_signature>( - state: &BeaconState, - block: &SignedBeaconBlock, - ctxt: &mut ConsensusContext, +pub fn verify_block_signature>( + state: &BeaconState, + block: &SignedBeaconBlock, + ctxt: &mut ConsensusContext, spec: &ChainSpec, ) -> Result<(), BlockOperationError> { let block_root = Some(ctxt.get_current_block_root(block)?); @@ -280,11 +287,11 @@ pub fn verify_block_signature>( /// Verifies the `randao_reveal` against the block's proposer pubkey and updates /// `state.latest_randao_mixes`. -pub fn process_randao>( - state: &mut BeaconState, - block: BeaconBlockRef<'_, T, Payload>, +pub fn process_randao>( + state: &mut BeaconState, + block: BeaconBlockRef<'_, E, Payload>, verify_signatures: VerifySignatures, - ctxt: &mut ConsensusContext, + ctxt: &mut ConsensusContext, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { if verify_signatures.is_true() { @@ -310,8 +317,8 @@ pub fn process_randao>( } /// Update the `state.eth1_data_votes` based upon the `eth1_data` provided. -pub fn process_eth1_data( - state: &mut BeaconState, +pub fn process_eth1_data( + state: &mut BeaconState, eth1_data: &Eth1Data, ) -> Result<(), Error> { if let Some(new_eth1_data) = get_new_eth1_data(state, eth1_data)? { @@ -325,8 +332,8 @@ pub fn process_eth1_data( /// Returns `Ok(Some(eth1_data))` if adding the given `eth1_data` to `state.eth1_data_votes` would /// result in a change to `state.eth1_data`. -pub fn get_new_eth1_data( - state: &BeaconState, +pub fn get_new_eth1_data( + state: &BeaconState, eth1_data: &Eth1Data, ) -> Result, ArithError> { let num_votes = state @@ -336,7 +343,7 @@ pub fn get_new_eth1_data( .count(); // The +1 is to account for the `eth1_data` supplied to the function. - if num_votes.safe_add(1)?.safe_mul(2)? > T::SlotsPerEth1VotingPeriod::to_usize() { + if num_votes.safe_add(1)?.safe_mul(2)? > E::SlotsPerEth1VotingPeriod::to_usize() { Ok(Some(eth1_data.clone())) } else { Ok(None) @@ -353,10 +360,10 @@ pub fn get_new_eth1_data( /// Contains a partial set of checks from the `process_execution_payload` function: /// /// https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/merge/beacon-chain.md#process_execution_payload -pub fn partially_verify_execution_payload>( - state: &BeaconState, +pub fn partially_verify_execution_payload>( + state: &BeaconState, block_slot: Slot, - body: BeaconBlockBodyRef, + body: BeaconBlockBodyRef, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { let payload = body.execution_payload()?; @@ -389,9 +396,9 @@ pub fn partially_verify_execution_payload>( - state: &mut BeaconState, - body: BeaconBlockBodyRef, +pub fn process_execution_payload>( + state: &mut BeaconState, + body: BeaconBlockBodyRef, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { - partially_verify_execution_payload::(state, state.slot(), body, spec)?; + partially_verify_execution_payload::(state, state.slot(), body, spec)?; let payload = body.execution_payload()?; match state.latest_execution_payload_header_mut()? { ExecutionPayloadHeaderRefMut::Merge(header_mut) => { @@ -433,6 +440,12 @@ pub fn process_execution_payload>( _ => return Err(BlockProcessingError::IncorrectStateType), } } + ExecutionPayloadHeaderRefMut::Electra(header_mut) => { + match payload.to_execution_payload_header() { + ExecutionPayloadHeader::Electra(header) => *header_mut = header, + _ => return Err(BlockProcessingError::IncorrectStateType), + } + } } Ok(()) @@ -443,7 +456,7 @@ pub fn process_execution_payload>( /// errors from the `BeaconState` being an earlier variant than `BeaconStateMerge` as we'd have to /// repeatedly write code to treat these errors as false. /// https://github.com/ethereum/consensus-specs/blob/dev/specs/bellatrix/beacon-chain.md#is_merge_transition_complete -pub fn is_merge_transition_complete(state: &BeaconState) -> bool { +pub fn is_merge_transition_complete(state: &BeaconState) -> bool { match state { // We must check defaultness against the payload header with 0x0 roots, as that's what's meant // by `ExecutionPayloadHeader()` in the spec. @@ -451,14 +464,14 @@ pub fn is_merge_transition_complete(state: &BeaconState) -> bool .latest_execution_payload_header() .map(|header| !header.is_default_with_zero_roots()) .unwrap_or(false), - BeaconState::Deneb(_) | BeaconState::Capella(_) => true, + BeaconState::Electra(_) | BeaconState::Deneb(_) | BeaconState::Capella(_) => true, BeaconState::Base(_) | BeaconState::Altair(_) => false, } } /// https://github.com/ethereum/consensus-specs/blob/dev/specs/bellatrix/beacon-chain.md#is_merge_transition_block -pub fn is_merge_transition_block>( - state: &BeaconState, - body: BeaconBlockBodyRef, +pub fn is_merge_transition_block>( + state: &BeaconState, + body: BeaconBlockBodyRef, ) -> bool { // For execution payloads in blocks (which may be headers) we must check defaultness against // the payload with `transactions_root` equal to the tree hash of the empty list. @@ -469,16 +482,16 @@ pub fn is_merge_transition_block>( .unwrap_or(false) } /// https://github.com/ethereum/consensus-specs/blob/dev/specs/bellatrix/beacon-chain.md#is_execution_enabled -pub fn is_execution_enabled>( - state: &BeaconState, - body: BeaconBlockBodyRef, +pub fn is_execution_enabled>( + state: &BeaconState, + body: BeaconBlockBodyRef, ) -> bool { is_merge_transition_block(state, body) || is_merge_transition_complete(state) } /// https://github.com/ethereum/consensus-specs/blob/dev/specs/bellatrix/beacon-chain.md#compute_timestamp_at_slot -pub fn compute_timestamp_at_slot( - state: &BeaconState, +pub fn compute_timestamp_at_slot( + state: &BeaconState, block_slot: Slot, spec: &ChainSpec, ) -> Result { @@ -491,10 +504,10 @@ pub fn compute_timestamp_at_slot( /// Compute the next batch of withdrawals which should be included in a block. /// /// https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#new-get_expected_withdrawals -pub fn get_expected_withdrawals( - state: &BeaconState, +pub fn get_expected_withdrawals( + state: &BeaconState, spec: &ChainSpec, -) -> Result, BlockProcessingError> { +) -> Result, BlockProcessingError> { let epoch = state.current_epoch(); let mut withdrawal_index = state.next_withdrawal_index()?; let mut validator_index = state.next_withdrawal_validator_index()?; @@ -530,7 +543,7 @@ pub fn get_expected_withdrawals( }); withdrawal_index.safe_add_assign(1)?; } - if withdrawals.len() == T::max_withdrawals_per_payload() { + if withdrawals.len() == E::max_withdrawals_per_payload() { break; } validator_index = validator_index @@ -542,14 +555,14 @@ pub fn get_expected_withdrawals( } /// Apply withdrawals to the state. -pub fn process_withdrawals>( - state: &mut BeaconState, +pub fn process_withdrawals>( + state: &mut BeaconState, payload: Payload::Ref<'_>, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { match state { BeaconState::Merge(_) => Ok(()), - BeaconState::Capella(_) | BeaconState::Deneb(_) => { + BeaconState::Capella(_) | BeaconState::Deneb(_) | BeaconState::Electra(_) => { let expected_withdrawals = get_expected_withdrawals(state, spec)?; let expected_root = expected_withdrawals.tree_hash_root(); let withdrawals_root = payload.withdrawals_root()?; @@ -574,7 +587,7 @@ pub fn process_withdrawals>( *state.next_withdrawal_index_mut()? = latest_withdrawal.index.safe_add(1)?; // Update the next validator index to start the next withdrawal sweep - if expected_withdrawals.len() == T::max_withdrawals_per_payload() { + if expected_withdrawals.len() == E::max_withdrawals_per_payload() { // Next sweep starts after the latest withdrawal's validator index let next_validator_index = latest_withdrawal .validator_index @@ -585,7 +598,7 @@ pub fn process_withdrawals>( } // Advance sweep by the max length of the sweep if there was not a full set of withdrawals - if expected_withdrawals.len() != T::max_withdrawals_per_payload() { + if expected_withdrawals.len() != E::max_withdrawals_per_payload() { let next_validator_index = state .next_withdrawal_validator_index()? .safe_add(spec.max_validators_per_withdrawals_sweep)? diff --git a/consensus/state_processing/src/per_block_processing/altair/sync_committee.rs b/consensus/state_processing/src/per_block_processing/altair/sync_committee.rs index a5dcd6e0b6..210db4c9c1 100644 --- a/consensus/state_processing/src/per_block_processing/altair/sync_committee.rs +++ b/consensus/state_processing/src/per_block_processing/altair/sync_committee.rs @@ -4,11 +4,13 @@ use crate::{signature_sets::sync_aggregate_signature_set, VerifySignatures}; use safe_arith::SafeArith; use std::borrow::Cow; use types::consts::altair::{PROPOSER_WEIGHT, SYNC_REWARD_WEIGHT, WEIGHT_DENOMINATOR}; -use types::{BeaconState, ChainSpec, EthSpec, PublicKeyBytes, SyncAggregate, Unsigned}; +use types::{ + BeaconState, BeaconStateError, ChainSpec, EthSpec, PublicKeyBytes, SyncAggregate, Unsigned, +}; -pub fn process_sync_aggregate( - state: &mut BeaconState, - aggregate: &SyncAggregate, +pub fn process_sync_aggregate( + state: &mut BeaconState, + aggregate: &SyncAggregate, proposer_index: u64, verify_signatures: VerifySignatures, spec: &ChainSpec, @@ -47,26 +49,42 @@ pub fn process_sync_aggregate( // Apply participant and proposer rewards let committee_indices = state.get_sync_committee_indices(¤t_sync_committee)?; + let proposer_index = proposer_index as usize; + let mut proposer_balance = *state + .balances() + .get(proposer_index) + .ok_or(BeaconStateError::BalancesOutOfBounds(proposer_index))?; + for (participant_index, participation_bit) in committee_indices .into_iter() .zip(aggregate.sync_committee_bits.iter()) { if participation_bit { - increase_balance(state, participant_index, participant_reward)?; - increase_balance(state, proposer_index as usize, proposer_reward)?; + // Accumulate proposer rewards in a temp var in case the proposer has very low balance, is + // part of the sync committee, does not participate and its penalties saturate. + if participant_index == proposer_index { + proposer_balance.safe_add_assign(participant_reward)?; + } else { + increase_balance(state, participant_index, participant_reward)?; + } + proposer_balance.safe_add_assign(proposer_reward)?; + } else if participant_index == proposer_index { + proposer_balance = proposer_balance.saturating_sub(participant_reward); } else { decrease_balance(state, participant_index, participant_reward)?; } } + *state.get_balance_mut(proposer_index)? = proposer_balance; + Ok(()) } /// Compute the `(participant_reward, proposer_reward)` for a sync aggregate. /// /// The `state` should be the pre-state from the same slot as the block containing the aggregate. -pub fn compute_sync_aggregate_rewards( - state: &BeaconState, +pub fn compute_sync_aggregate_rewards( + state: &BeaconState, spec: &ChainSpec, ) -> Result<(u64, u64), BlockProcessingError> { let total_active_balance = state.get_total_active_balance()?; @@ -78,8 +96,8 @@ pub fn compute_sync_aggregate_rewards( let max_participant_rewards = total_base_rewards .safe_mul(SYNC_REWARD_WEIGHT)? .safe_div(WEIGHT_DENOMINATOR)? - .safe_div(T::slots_per_epoch())?; - let participant_reward = max_participant_rewards.safe_div(T::SyncCommitteeSize::to_u64())?; + .safe_div(E::slots_per_epoch())?; + let participant_reward = max_participant_rewards.safe_div(E::SyncCommitteeSize::to_u64())?; let proposer_reward = participant_reward .safe_mul(PROPOSER_WEIGHT)? .safe_div(WEIGHT_DENOMINATOR.safe_sub(PROPOSER_WEIGHT)?)?; 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 22309334fa..3b8a8ea52c 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 @@ -71,15 +71,15 @@ impl From> for Error { /// /// This allows for optimizations related to batch BLS operations (see the /// `Self::verify_entire_block(..)` function). -pub struct BlockSignatureVerifier<'a, T, F, D> +pub struct BlockSignatureVerifier<'a, E, F, D> where - T: EthSpec, + E: EthSpec, F: Fn(usize) -> Option> + Clone, D: Fn(&'a PublicKeyBytes) -> Option>, { get_pubkey: F, decompressor: D, - state: &'a BeaconState, + state: &'a BeaconState, spec: &'a ChainSpec, sets: ParallelSignatureSets<'a>, } @@ -95,16 +95,16 @@ impl<'a> From>> for ParallelSignatureSets<'a> { } } -impl<'a, T, F, D> BlockSignatureVerifier<'a, T, F, D> +impl<'a, E, F, D> BlockSignatureVerifier<'a, E, F, D> where - T: EthSpec, + E: EthSpec, F: Fn(usize) -> Option> + Clone, D: Fn(&'a PublicKeyBytes) -> Option>, { /// Create a new verifier without any included signatures. See the `include...` functions to /// add signatures, and the `verify` pub fn new( - state: &'a BeaconState, + state: &'a BeaconState, get_pubkey: F, decompressor: D, spec: &'a ChainSpec, @@ -125,12 +125,12 @@ where /// contains invalid signatures on deposits._ /// /// See `Self::verify` for more detail. - pub fn verify_entire_block>( - state: &'a BeaconState, + pub fn verify_entire_block>( + state: &'a BeaconState, get_pubkey: F, decompressor: D, - block: &'a SignedBeaconBlock, - ctxt: &mut ConsensusContext, + block: &'a SignedBeaconBlock, + ctxt: &mut ConsensusContext, spec: &'a ChainSpec, ) -> Result<()> { let mut verifier = Self::new(state, get_pubkey, decompressor, spec); @@ -139,10 +139,10 @@ where } /// Includes all signatures on the block (except the deposit signatures) for verification. - pub fn include_all_signatures>( + pub fn include_all_signatures>( &mut self, - block: &'a SignedBeaconBlock, - ctxt: &mut ConsensusContext, + block: &'a SignedBeaconBlock, + ctxt: &mut ConsensusContext, ) -> Result<()> { let block_root = Some(ctxt.get_current_block_root(block)?); let verified_proposer_index = @@ -156,10 +156,10 @@ where /// Includes all signatures on the block (except the deposit signatures and the proposal /// signature) for verification. - pub fn include_all_signatures_except_proposal>( + pub fn include_all_signatures_except_proposal>( &mut self, - block: &'a SignedBeaconBlock, - ctxt: &mut ConsensusContext, + block: &'a SignedBeaconBlock, + ctxt: &mut ConsensusContext, ) -> Result<()> { let verified_proposer_index = Some(ctxt.get_proposer_index_from_epoch_state(self.state, self.spec)?); @@ -176,9 +176,9 @@ where } /// Includes the block signature for `self.block` for verification. - pub fn include_block_proposal>( + pub fn include_block_proposal>( &mut self, - block: &'a SignedBeaconBlock, + block: &'a SignedBeaconBlock, block_root: Option, verified_proposer_index: Option, ) -> Result<()> { @@ -195,9 +195,9 @@ where } /// Includes the randao signature for `self.block` for verification. - pub fn include_randao_reveal>( + pub fn include_randao_reveal>( &mut self, - block: &'a SignedBeaconBlock, + block: &'a SignedBeaconBlock, verified_proposer_index: Option, ) -> Result<()> { let set = randao_signature_set( @@ -212,9 +212,9 @@ where } /// Includes all signatures in `self.block.body.proposer_slashings` for verification. - pub fn include_proposer_slashings>( + pub fn include_proposer_slashings>( &mut self, - block: &'a SignedBeaconBlock, + block: &'a SignedBeaconBlock, ) -> Result<()> { self.sets .sets @@ -241,9 +241,9 @@ where } /// Includes all signatures in `self.block.body.attester_slashings` for verification. - pub fn include_attester_slashings>( + pub fn include_attester_slashings>( &mut self, - block: &'a SignedBeaconBlock, + block: &'a SignedBeaconBlock, ) -> Result<()> { self.sets .sets @@ -270,10 +270,10 @@ where } /// Includes all signatures in `self.block.body.attestations` for verification. - pub fn include_attestations>( + pub fn include_attestations>( &mut self, - block: &'a SignedBeaconBlock, - ctxt: &mut ConsensusContext, + block: &'a SignedBeaconBlock, + ctxt: &mut ConsensusContext, ) -> Result<()> { self.sets .sets @@ -300,9 +300,9 @@ where } /// Includes all signatures in `self.block.body.voluntary_exits` for verification. - pub fn include_exits>( + pub fn include_exits>( &mut self, - block: &'a SignedBeaconBlock, + block: &'a SignedBeaconBlock, ) -> Result<()> { self.sets .sets @@ -324,9 +324,9 @@ where } /// Include the signature of the block's sync aggregate (if it exists) for verification. - pub fn include_sync_aggregate>( + pub fn include_sync_aggregate>( &mut self, - block: &'a SignedBeaconBlock, + block: &'a SignedBeaconBlock, ) -> Result<()> { if let Ok(sync_aggregate) = block.message().body().sync_aggregate() { if let Some(signature_set) = sync_aggregate_signature_set( @@ -344,9 +344,9 @@ where } /// Include the signature of the block's BLS to execution changes for verification. - pub fn include_bls_to_execution_changes>( + pub fn include_bls_to_execution_changes>( &mut self, - block: &'a SignedBeaconBlock, + block: &'a SignedBeaconBlock, ) -> Result<()> { // To improve performance we might want to decompress the withdrawal pubkeys in parallel. if let Ok(bls_to_execution_changes) = block.message().body().bls_to_execution_changes() { diff --git a/consensus/state_processing/src/per_block_processing/errors.rs b/consensus/state_processing/src/per_block_processing/errors.rs index de1c132951..28d36dbc51 100644 --- a/consensus/state_processing/src/per_block_processing/errors.rs +++ b/consensus/state_processing/src/per_block_processing/errors.rs @@ -1,8 +1,6 @@ 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::*; @@ -84,12 +82,12 @@ pub enum BlockProcessingError { }, ExecutionInvalid, ConsensusContext(ContextError), + EpochCacheError(EpochCacheError), WithdrawalsRootMismatch { expected: Hash256, found: Hash256, }, WithdrawalCredentialsInvalid, - ParticipationCacheError(ParticipationCacheError), } impl From for BlockProcessingError { @@ -134,6 +132,12 @@ impl From for BlockProcessingError { } } +impl From for BlockProcessingError { + fn from(e: EpochCacheError) -> Self { + BlockProcessingError::EpochCacheError(e) + } +} + impl From> for BlockProcessingError { fn from(e: BlockOperationError) -> BlockProcessingError { match e { @@ -147,12 +151,6 @@ impl From> for BlockProcessingError { } } -impl From 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 @@ -328,9 +326,11 @@ pub enum AttestationInvalid { /// /// `is_current` is `true` if the attestation was compared to the /// `state.current_justified_checkpoint`, `false` if compared to `state.previous_justified_checkpoint`. + /// + /// Checkpoints have been boxed to keep the error size down and prevent clippy failures. WrongJustifiedCheckpoint { - state: Checkpoint, - attestation: Checkpoint, + state: Box, + attestation: Box, is_current: bool, }, /// The aggregation bitfield length is not the smallest possible size to represent the committee. diff --git a/consensus/state_processing/src/per_block_processing/is_valid_indexed_attestation.rs b/consensus/state_processing/src/per_block_processing/is_valid_indexed_attestation.rs index c63cf52005..baccd1dbbd 100644 --- a/consensus/state_processing/src/per_block_processing/is_valid_indexed_attestation.rs +++ b/consensus/state_processing/src/per_block_processing/is_valid_indexed_attestation.rs @@ -11,9 +11,9 @@ fn error(reason: Invalid) -> BlockOperationError { } /// Verify an `IndexedAttestation`. -pub fn is_valid_indexed_attestation( - state: &BeaconState, - indexed_attestation: &IndexedAttestation, +pub fn is_valid_indexed_attestation( + state: &BeaconState, + indexed_attestation: &IndexedAttestation, verify_signatures: VerifySignatures, spec: &ChainSpec, ) -> Result<()> { 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 cb24a7ba7e..af9b793813 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -1,19 +1,17 @@ use super::*; use crate::common::{ - altair::{get_base_reward, BaseRewardPerIncrement}, get_attestation_participation_flag_indices, increase_balance, initiate_validator_exit, slash_validator, }; use crate::per_block_processing::errors::{BlockProcessingError, IntoWithIndex}; use crate::VerifySignatures; -use safe_arith::SafeArith; use types::consts::altair::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}; -pub fn process_operations>( - state: &mut BeaconState, - block_body: BeaconBlockBodyRef, +pub fn process_operations>( + state: &mut BeaconState, + block_body: BeaconBlockBodyRef, verify_signatures: VerifySignatures, - ctxt: &mut ConsensusContext, + ctxt: &mut ConsensusContext, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { process_proposer_slashings( @@ -48,15 +46,19 @@ pub mod base { /// /// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns /// an `Err` describing the invalid object or cause of failure. - pub fn process_attestations( - state: &mut BeaconState, - attestations: &[Attestation], + pub fn process_attestations( + state: &mut BeaconState, + attestations: &[Attestation], verify_signatures: VerifySignatures, - ctxt: &mut ConsensusContext, + ctxt: &mut ConsensusContext, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { - // Ensure the previous epoch cache exists. + // Ensure required caches are all built. These should be no-ops during regular operation. + state.build_committee_cache(RelativeEpoch::Current, spec)?; state.build_committee_cache(RelativeEpoch::Previous, spec)?; + initialize_epoch_cache(state, spec)?; + initialize_progressive_balances_cache(state, spec)?; + state.build_slashings_cache()?; let proposer_index = ctxt.get_proposer_index(state, spec)?; @@ -98,13 +100,12 @@ pub mod base { pub mod altair_deneb { 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( - state: &mut BeaconState, - attestations: &[Attestation], + pub fn process_attestations( + state: &mut BeaconState, + attestations: &[Attestation], verify_signatures: VerifySignatures, - ctxt: &mut ConsensusContext, + ctxt: &mut ConsensusContext, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { attestations @@ -115,18 +116,17 @@ pub mod altair_deneb { }) } - pub fn process_attestation( - state: &mut BeaconState, - attestation: &Attestation, + pub fn process_attestation( + state: &mut BeaconState, + attestation: &Attestation, att_index: usize, - ctxt: &mut ConsensusContext, + ctxt: &mut ConsensusContext, verify_signatures: VerifySignatures, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { - state.build_committee_cache(RelativeEpoch::Previous, spec)?; - state.build_committee_cache(RelativeEpoch::Current, spec)?; - let proposer_index = ctxt.get_proposer_index(state, spec)?; + let previous_epoch = ctxt.previous_epoch; + let current_epoch = ctxt.current_epoch; let attesting_indices = &verify_attestation_for_block_inclusion( state, @@ -145,32 +145,36 @@ pub mod altair_deneb { get_attestation_participation_flag_indices(state, data, inclusion_delay, spec)?; // Update epoch participation flags. - let total_active_balance = state.get_total_active_balance()?; - let base_reward_per_increment = BaseRewardPerIncrement::new(total_active_balance, spec)?; let mut proposer_reward_numerator = 0; for index in attesting_indices { let index = *index as usize; + let validator_effective_balance = state.epoch_cache().get_effective_balance(index)?; + let validator_slashed = state.slashings_cache().is_slashed(index); + for (flag_index, &weight) in PARTICIPATION_FLAG_WEIGHTS.iter().enumerate() { - let epoch_participation = state.get_epoch_participation_mut(data.target.epoch)?; - let validator_participation = epoch_participation - .get_mut(index) - .ok_or(BeaconStateError::ParticipationOutOfBounds(index))?; + let epoch_participation = state.get_epoch_participation_mut( + data.target.epoch, + previous_epoch, + current_epoch, + )?; - if participation_flag_indices.contains(&flag_index) - && !validator_participation.has_flag(flag_index)? - { - validator_participation.add_flag(flag_index)?; - proposer_reward_numerator.safe_add_assign( - get_base_reward(state, index, base_reward_per_increment, spec)? - .safe_mul(weight)?, - )?; + if participation_flag_indices.contains(&flag_index) { + let validator_participation = epoch_participation + .get_mut(index) + .ok_or(BeaconStateError::ParticipationOutOfBounds(index))?; + + if !validator_participation.has_flag(flag_index)? { + validator_participation.add_flag(flag_index)?; + proposer_reward_numerator + .safe_add_assign(state.get_base_reward(index)?.safe_mul(weight)?)?; - if flag_index == TIMELY_TARGET_FLAG_INDEX { update_progressive_balances_on_attestation( state, data.target.epoch, - index, + flag_index, + validator_effective_balance, + validator_slashed, )?; } } @@ -191,13 +195,15 @@ pub mod altair_deneb { /// /// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns /// an `Err` describing the invalid object or cause of failure. -pub fn process_proposer_slashings( - state: &mut BeaconState, +pub fn process_proposer_slashings( + state: &mut BeaconState, proposer_slashings: &[ProposerSlashing], verify_signatures: VerifySignatures, - ctxt: &mut ConsensusContext, + ctxt: &mut ConsensusContext, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { + state.build_slashings_cache()?; + // Verify and apply proposer slashings in series. // We have to verify in series because an invalid block may contain multiple slashings // for the same validator, and we need to correctly detect and reject that. @@ -224,19 +230,19 @@ pub fn process_proposer_slashings( /// /// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns /// an `Err` describing the invalid object or cause of failure. -pub fn process_attester_slashings( - state: &mut BeaconState, - attester_slashings: &[AttesterSlashing], +pub fn process_attester_slashings( + state: &mut BeaconState, + attester_slashings: &[AttesterSlashing], verify_signatures: VerifySignatures, - ctxt: &mut ConsensusContext, + ctxt: &mut ConsensusContext, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { - for (i, attester_slashing) in attester_slashings.iter().enumerate() { - verify_attester_slashing(state, attester_slashing, verify_signatures, spec) - .map_err(|e| e.into_with_index(i))?; + state.build_slashings_cache()?; + for (i, attester_slashing) in attester_slashings.iter().enumerate() { let slashable_indices = - get_slashable_indices(state, attester_slashing).map_err(|e| e.into_with_index(i))?; + verify_attester_slashing(state, attester_slashing, verify_signatures, spec) + .map_err(|e| e.into_with_index(i))?; for i in slashable_indices { slash_validator(state, i as usize, None, ctxt, spec)?; @@ -248,11 +254,11 @@ pub fn process_attester_slashings( /// Wrapper function to handle calling the correct version of `process_attestations` based on /// the fork. -pub fn process_attestations>( - state: &mut BeaconState, - block_body: BeaconBlockBodyRef, +pub fn process_attestations>( + state: &mut BeaconState, + block_body: BeaconBlockBodyRef, verify_signatures: VerifySignatures, - ctxt: &mut ConsensusContext, + ctxt: &mut ConsensusContext, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { match block_body { @@ -268,7 +274,8 @@ pub fn process_attestations>( BeaconBlockBodyRef::Altair(_) | BeaconBlockBodyRef::Merge(_) | BeaconBlockBodyRef::Capella(_) - | BeaconBlockBodyRef::Deneb(_) => { + | BeaconBlockBodyRef::Deneb(_) + | BeaconBlockBodyRef::Electra(_) => { altair_deneb::process_attestations( state, block_body.attestations(), @@ -285,8 +292,8 @@ pub fn process_attestations>( /// /// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns /// an `Err` describing the invalid object or cause of failure. -pub fn process_exits( - state: &mut BeaconState, +pub fn process_exits( + state: &mut BeaconState, voluntary_exits: &[SignedVoluntaryExit], verify_signatures: VerifySignatures, spec: &ChainSpec, @@ -306,8 +313,8 @@ pub fn process_exits( /// /// Returns `Ok(())` if the validation and state updates completed successfully. Otherwise returns /// an `Err` describing the invalid object or cause of failure. -pub fn process_bls_to_execution_changes( - state: &mut BeaconState, +pub fn process_bls_to_execution_changes( + state: &mut BeaconState, bls_to_execution_changes: &[SignedBlsToExecutionChange], verify_signatures: VerifySignatures, spec: &ChainSpec, @@ -331,13 +338,13 @@ pub fn process_bls_to_execution_changes( /// /// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns /// an `Err` describing the invalid object or cause of failure. -pub fn process_deposits( - state: &mut BeaconState, +pub fn process_deposits( + state: &mut BeaconState, deposits: &[Deposit], spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { let expected_deposit_len = std::cmp::min( - T::MaxDeposits::to_u64(), + E::MaxDeposits::to_u64(), state.get_outstanding_deposit_len()?, ); block_verify!( @@ -371,8 +378,8 @@ pub fn process_deposits( } /// Process a single deposit, optionally verifying its merkle proof. -pub fn process_deposit( - state: &mut BeaconState, +pub fn process_deposit( + state: &mut BeaconState, deposit: &Deposit, spec: &ChainSpec, verify_merkle_proof: bool, diff --git a/consensus/state_processing/src/per_block_processing/signature_sets.rs b/consensus/state_processing/src/per_block_processing/signature_sets.rs index fcd324e9eb..163b2cff7a 100644 --- a/consensus/state_processing/src/per_block_processing/signature_sets.rs +++ b/consensus/state_processing/src/per_block_processing/signature_sets.rs @@ -53,12 +53,12 @@ impl From for Error { } /// Helper function to get a public key from a `state`. -pub fn get_pubkey_from_state( - state: &BeaconState, +pub fn get_pubkey_from_state( + state: &BeaconState, validator_index: usize, ) -> Option> where - T: EthSpec, + E: EthSpec, { state .validators() @@ -71,16 +71,16 @@ where } /// A signature set that is valid if a block was signed by the expected block producer. -pub fn block_proposal_signature_set<'a, T, F, Payload: AbstractExecPayload>( - state: &'a BeaconState, +pub fn block_proposal_signature_set<'a, E, F, Payload: AbstractExecPayload>( + state: &'a BeaconState, get_pubkey: F, - signed_block: &'a SignedBeaconBlock, + signed_block: &'a SignedBeaconBlock, block_root: Option, verified_proposer_index: Option, spec: &'a ChainSpec, ) -> Result> where - T: EthSpec, + E: EthSpec, F: Fn(usize) -> Option>, { let block = signed_block.message(); @@ -113,8 +113,8 @@ where /// Unlike `block_proposal_signature_set` this does **not** check that the proposer index is /// correct according to the shuffling. It should only be used if no suitable `BeaconState` is /// available. -pub fn block_proposal_signature_set_from_parts<'a, T, F, Payload: AbstractExecPayload>( - signed_block: &'a SignedBeaconBlock, +pub fn block_proposal_signature_set_from_parts<'a, E, F, Payload: AbstractExecPayload>( + signed_block: &'a SignedBeaconBlock, block_root: Option, proposer_index: u64, fork: &Fork, @@ -123,7 +123,7 @@ pub fn block_proposal_signature_set_from_parts<'a, T, F, Payload: AbstractExecPa spec: &'a ChainSpec, ) -> Result> where - T: EthSpec, + E: EthSpec, F: Fn(usize) -> Option>, { // Verify that the `SignedBeaconBlock` instantiation matches the fork at `signed_block.slot()`. @@ -133,7 +133,7 @@ where let block = signed_block.message(); let domain = spec.get_domain( - block.slot().epoch(T::slots_per_epoch()), + block.slot().epoch(E::slots_per_epoch()), Domain::BeaconProposer, fork, genesis_validators_root, @@ -156,8 +156,8 @@ where )) } -pub fn bls_execution_change_signature_set<'a, T: EthSpec>( - state: &'a BeaconState, +pub fn bls_execution_change_signature_set<'a, E: EthSpec>( + state: &'a BeaconState, signed_address_change: &'a SignedBlsToExecutionChange, spec: &'a ChainSpec, ) -> Result> { @@ -183,15 +183,15 @@ pub fn bls_execution_change_signature_set<'a, T: EthSpec>( } /// A signature set that is valid if the block proposers randao reveal signature is correct. -pub fn randao_signature_set<'a, T, F, Payload: AbstractExecPayload>( - state: &'a BeaconState, +pub fn randao_signature_set<'a, E, F, Payload: AbstractExecPayload>( + state: &'a BeaconState, get_pubkey: F, - block: BeaconBlockRef<'a, T, Payload>, + block: BeaconBlockRef<'a, E, Payload>, verified_proposer_index: Option, spec: &'a ChainSpec, ) -> Result> where - T: EthSpec, + E: EthSpec, F: Fn(usize) -> Option>, { let proposer_index = if let Some(proposer_index) = verified_proposer_index { @@ -201,7 +201,7 @@ where }; let domain = spec.get_domain( - block.slot().epoch(T::slots_per_epoch()), + block.slot().epoch(E::slots_per_epoch()), Domain::Randao, &state.fork(), state.genesis_validators_root(), @@ -209,7 +209,7 @@ where let message = block .slot() - .epoch(T::slots_per_epoch()) + .epoch(E::slots_per_epoch()) .signing_root(domain); Ok(SignatureSet::single_pubkey( @@ -220,14 +220,14 @@ where } /// Returns two signature sets, one for each `BlockHeader` included in the `ProposerSlashing`. -pub fn proposer_slashing_signature_set<'a, T, F>( - state: &'a BeaconState, +pub fn proposer_slashing_signature_set<'a, E, F>( + state: &'a BeaconState, get_pubkey: F, proposer_slashing: &'a ProposerSlashing, spec: &'a ChainSpec, ) -> Result<(SignatureSet<'a>, SignatureSet<'a>)> where - T: EthSpec, + E: EthSpec, F: Fn(usize) -> Option>, { let proposer_index = proposer_slashing.signed_header_1.message.proposer_index as usize; @@ -249,14 +249,14 @@ where } /// Returns a signature set that is valid if the given `pubkey` signed the `header`. -fn block_header_signature_set<'a, T: EthSpec>( - state: &'a BeaconState, +fn block_header_signature_set<'a, E: EthSpec>( + state: &'a BeaconState, signed_header: &'a SignedBeaconBlockHeader, pubkey: Cow<'a, PublicKey>, spec: &'a ChainSpec, ) -> SignatureSet<'a> { let domain = spec.get_domain( - signed_header.message.slot.epoch(T::slots_per_epoch()), + signed_header.message.slot.epoch(E::slots_per_epoch()), Domain::BeaconProposer, &state.fork(), state.genesis_validators_root(), @@ -268,15 +268,15 @@ fn block_header_signature_set<'a, T: EthSpec>( } /// Returns the signature set for the given `indexed_attestation`. -pub fn indexed_attestation_signature_set<'a, 'b, T, F>( - state: &'a BeaconState, +pub fn indexed_attestation_signature_set<'a, 'b, E, F>( + state: &'a BeaconState, get_pubkey: F, signature: &'a AggregateSignature, - indexed_attestation: &'b IndexedAttestation, + indexed_attestation: &'b IndexedAttestation, spec: &'a ChainSpec, ) -> Result> where - T: EthSpec, + E: EthSpec, F: Fn(usize) -> Option>, { let mut pubkeys = Vec::with_capacity(indexed_attestation.attesting_indices.len()); @@ -300,16 +300,16 @@ where /// Returns the signature set for the given `indexed_attestation` but pubkeys are supplied directly /// instead of from the state. -pub fn indexed_attestation_signature_set_from_pubkeys<'a, 'b, T, F>( +pub fn indexed_attestation_signature_set_from_pubkeys<'a, 'b, E, F>( get_pubkey: F, signature: &'a AggregateSignature, - indexed_attestation: &'b IndexedAttestation, + indexed_attestation: &'b IndexedAttestation, fork: &Fork, genesis_validators_root: Hash256, spec: &'a ChainSpec, ) -> Result> where - T: EthSpec, + E: EthSpec, F: Fn(usize) -> Option>, { let mut pubkeys = Vec::with_capacity(indexed_attestation.attesting_indices.len()); @@ -332,14 +332,14 @@ where } /// Returns the signature set for the given `attester_slashing` and corresponding `pubkeys`. -pub fn attester_slashing_signature_sets<'a, T, F>( - state: &'a BeaconState, +pub fn attester_slashing_signature_sets<'a, E, F>( + state: &'a BeaconState, get_pubkey: F, - attester_slashing: &'a AttesterSlashing, + attester_slashing: &'a AttesterSlashing, spec: &'a ChainSpec, ) -> Result<(SignatureSet<'a>, SignatureSet<'a>)> where - T: EthSpec, + E: EthSpec, F: Fn(usize) -> Option> + Clone, { Ok(( @@ -374,14 +374,14 @@ pub fn deposit_pubkey_signature_message( /// Returns a signature set that is valid if the `SignedVoluntaryExit` was signed by the indicated /// validator. -pub fn exit_signature_set<'a, T, F>( - state: &'a BeaconState, +pub fn exit_signature_set<'a, E, F>( + state: &'a BeaconState, get_pubkey: F, signed_exit: &'a SignedVoluntaryExit, spec: &'a ChainSpec, ) -> Result> where - T: EthSpec, + E: EthSpec, F: Fn(usize) -> Option>, { let exit = &signed_exit.message; @@ -398,7 +398,7 @@ where state.genesis_validators_root(), ), // EIP-7044 - BeaconState::Deneb(_) => spec.compute_domain( + BeaconState::Deneb(_) | BeaconState::Electra(_) => spec.compute_domain( Domain::VoluntaryExit, spec.capella_fork_version, state.genesis_validators_root(), @@ -414,21 +414,21 @@ where )) } -pub fn signed_aggregate_selection_proof_signature_set<'a, T, F>( +pub fn signed_aggregate_selection_proof_signature_set<'a, E, F>( get_pubkey: F, - signed_aggregate_and_proof: &'a SignedAggregateAndProof, + signed_aggregate_and_proof: &'a SignedAggregateAndProof, fork: &Fork, genesis_validators_root: Hash256, spec: &'a ChainSpec, ) -> Result> where - T: EthSpec, + E: EthSpec, F: Fn(usize) -> Option>, { let slot = signed_aggregate_and_proof.message.aggregate.data.slot; let domain = spec.get_domain( - slot.epoch(T::slots_per_epoch()), + slot.epoch(E::slots_per_epoch()), Domain::SelectionProof, fork, genesis_validators_root, @@ -444,15 +444,15 @@ where )) } -pub fn signed_aggregate_signature_set<'a, T, F>( +pub fn signed_aggregate_signature_set<'a, E, F>( get_pubkey: F, - signed_aggregate_and_proof: &'a SignedAggregateAndProof, + signed_aggregate_and_proof: &'a SignedAggregateAndProof, fork: &Fork, genesis_validators_root: Hash256, spec: &'a ChainSpec, ) -> Result> where - T: EthSpec, + E: EthSpec, F: Fn(usize) -> Option>, { let target_epoch = signed_aggregate_and_proof @@ -479,21 +479,21 @@ where )) } -pub fn signed_sync_aggregate_selection_proof_signature_set<'a, T, F>( +pub fn signed_sync_aggregate_selection_proof_signature_set<'a, E, F>( get_pubkey: F, - signed_contribution_and_proof: &'a SignedContributionAndProof, + signed_contribution_and_proof: &'a SignedContributionAndProof, fork: &Fork, genesis_validators_root: Hash256, spec: &'a ChainSpec, ) -> Result> where - T: EthSpec, + E: EthSpec, F: Fn(usize) -> Option>, { let slot = signed_contribution_and_proof.message.contribution.slot; let domain = spec.get_domain( - slot.epoch(T::slots_per_epoch()), + slot.epoch(E::slots_per_epoch()), Domain::SyncCommitteeSelectionProof, fork, genesis_validators_root, @@ -516,22 +516,22 @@ where )) } -pub fn signed_sync_aggregate_signature_set<'a, T, F>( +pub fn signed_sync_aggregate_signature_set<'a, E, F>( get_pubkey: F, - signed_contribution_and_proof: &'a SignedContributionAndProof, + signed_contribution_and_proof: &'a SignedContributionAndProof, fork: &Fork, genesis_validators_root: Hash256, spec: &'a ChainSpec, ) -> Result> where - T: EthSpec, + E: EthSpec, F: Fn(usize) -> Option>, { let epoch = signed_contribution_and_proof .message .contribution .slot - .epoch(T::slots_per_epoch()); + .epoch(E::slots_per_epoch()); let domain = spec.get_domain( epoch, @@ -551,7 +551,7 @@ where } #[allow(clippy::too_many_arguments)] -pub fn sync_committee_contribution_signature_set_from_pubkeys<'a, T, F>( +pub fn sync_committee_contribution_signature_set_from_pubkeys<'a, E, F>( get_pubkey: F, pubkey_bytes: &[PublicKeyBytes], signature: &'a AggregateSignature, @@ -562,10 +562,10 @@ pub fn sync_committee_contribution_signature_set_from_pubkeys<'a, T, F>( spec: &'a ChainSpec, ) -> Result> where - T: EthSpec, + E: EthSpec, F: Fn(&PublicKeyBytes) -> Option>, { - let mut pubkeys = Vec::with_capacity(T::SyncSubcommitteeSize::to_usize()); + let mut pubkeys = Vec::with_capacity(E::SyncSubcommitteeSize::to_usize()); for pubkey in pubkey_bytes { pubkeys.push(get_pubkey(pubkey).ok_or(Error::ValidatorPubkeyUnknown(*pubkey))?); } @@ -577,7 +577,7 @@ where Ok(SignatureSet::multiple_pubkeys(signature, pubkeys, message)) } -pub fn sync_committee_message_set_from_pubkeys<'a, T>( +pub fn sync_committee_message_set_from_pubkeys<'a, E>( pubkey: Cow<'a, PublicKey>, signature: &'a AggregateSignature, epoch: Epoch, @@ -587,7 +587,7 @@ pub fn sync_committee_message_set_from_pubkeys<'a, T>( spec: &'a ChainSpec, ) -> Result> where - T: EthSpec, + E: EthSpec, { let domain = spec.get_domain(epoch, Domain::SyncCommittee, fork, genesis_validators_root); @@ -607,16 +607,16 @@ where /// uses a separate function `eth2_fast_aggregate_verify` for this, but we can equivalently /// check the exceptional case eagerly and do a `fast_aggregate_verify` in the case where the /// check fails (by returning `Some(signature_set)`). -pub fn sync_aggregate_signature_set<'a, T, D>( +pub fn sync_aggregate_signature_set<'a, E, D>( decompressor: D, - sync_aggregate: &'a SyncAggregate, + sync_aggregate: &'a SyncAggregate, slot: Slot, block_root: Hash256, - state: &'a BeaconState, + state: &'a BeaconState, spec: &ChainSpec, ) -> Result>> where - T: EthSpec, + E: EthSpec, D: Fn(&'a PublicKeyBytes) -> Option>, { // Allow the point at infinity to count as a signature for 0 validators as per @@ -628,7 +628,7 @@ where } let committee_pubkeys = &state - .get_built_sync_committee(slot.epoch(T::slots_per_epoch()), spec)? + .get_built_sync_committee(slot.epoch(E::slots_per_epoch()), spec)? .pubkeys; let participant_pubkeys = committee_pubkeys @@ -647,7 +647,7 @@ where let previous_slot = slot.saturating_sub(1u64); let domain = spec.get_domain( - previous_slot.epoch(T::slots_per_epoch()), + previous_slot.epoch(E::slots_per_epoch()), Domain::SyncCommittee, &state.fork(), state.genesis_validators_root(), diff --git a/consensus/state_processing/src/per_block_processing/tests.rs b/consensus/state_processing/src/per_block_processing/tests.rs index 83fd0f232c..2a2b67e30d 100644 --- a/consensus/state_processing/src/per_block_processing/tests.rs +++ b/consensus/state_processing/src/per_block_processing/tests.rs @@ -451,8 +451,8 @@ async fn invalid_attestation_wrong_justified_checkpoint() { Err(BlockProcessingError::AttestationInvalid { index: 0, reason: AttestationInvalid::WrongJustifiedCheckpoint { - state: old_justified_checkpoint, - attestation: new_justified_checkpoint, + state: Box::new(old_justified_checkpoint), + attestation: Box::new(new_justified_checkpoint), is_current: true, } }) diff --git a/consensus/state_processing/src/per_block_processing/verify_attestation.rs b/consensus/state_processing/src/per_block_processing/verify_attestation.rs index b7aa4643e4..73454559df 100644 --- a/consensus/state_processing/src/per_block_processing/verify_attestation.rs +++ b/consensus/state_processing/src/per_block_processing/verify_attestation.rs @@ -15,13 +15,13 @@ fn error(reason: Invalid) -> BlockOperationError { /// to `state`. Otherwise, returns a descriptive `Err`. /// /// Optionally verifies the aggregate signature, depending on `verify_signatures`. -pub fn verify_attestation_for_block_inclusion<'ctxt, T: EthSpec>( - state: &BeaconState, - attestation: &Attestation, - ctxt: &'ctxt mut ConsensusContext, +pub fn verify_attestation_for_block_inclusion<'ctxt, E: EthSpec>( + state: &BeaconState, + attestation: &Attestation, + ctxt: &'ctxt mut ConsensusContext, verify_signatures: VerifySignatures, spec: &ChainSpec, -) -> Result<&'ctxt IndexedAttestation> { +) -> Result<&'ctxt IndexedAttestation> { let data = &attestation.data; verify!( @@ -38,7 +38,7 @@ pub fn verify_attestation_for_block_inclusion<'ctxt, T: EthSpec>( | BeaconState::Merge(_) | BeaconState::Capella(_) => { verify!( - state.slot() <= data.slot.safe_add(T::slots_per_epoch())?, + state.slot() <= data.slot.safe_add(E::slots_per_epoch())?, Invalid::IncludedTooLate { state: state.slot(), attestation: data.slot, @@ -46,7 +46,7 @@ pub fn verify_attestation_for_block_inclusion<'ctxt, T: EthSpec>( ); } // [Modified in Deneb:EIP7045] - BeaconState::Deneb(_) => {} + BeaconState::Deneb(_) | BeaconState::Electra(_) => {} } verify_attestation_for_state(state, attestation, ctxt, verify_signatures, spec) @@ -59,13 +59,13 @@ pub fn verify_attestation_for_block_inclusion<'ctxt, T: EthSpec>( /// prior blocks in `state`. /// /// Spec v0.12.1 -pub fn verify_attestation_for_state<'ctxt, T: EthSpec>( - state: &BeaconState, - attestation: &Attestation, - ctxt: &'ctxt mut ConsensusContext, +pub fn verify_attestation_for_state<'ctxt, E: EthSpec>( + state: &BeaconState, + attestation: &Attestation, + ctxt: &'ctxt mut ConsensusContext, verify_signatures: VerifySignatures, spec: &ChainSpec, -) -> Result<&'ctxt IndexedAttestation> { +) -> Result<&'ctxt IndexedAttestation> { let data = &attestation.data; verify!( @@ -86,24 +86,24 @@ pub fn verify_attestation_for_state<'ctxt, T: EthSpec>( /// Check target epoch and source checkpoint. /// /// Spec v0.12.1 -fn verify_casper_ffg_vote( - attestation: &Attestation, - state: &BeaconState, +fn verify_casper_ffg_vote( + attestation: &Attestation, + state: &BeaconState, ) -> Result<()> { let data = &attestation.data; verify!( - data.target.epoch == data.slot.epoch(T::slots_per_epoch()), + data.target.epoch == data.slot.epoch(E::slots_per_epoch()), Invalid::TargetEpochSlotMismatch { target_epoch: data.target.epoch, - slot_epoch: data.slot.epoch(T::slots_per_epoch()), + slot_epoch: data.slot.epoch(E::slots_per_epoch()), } ); if data.target.epoch == state.current_epoch() { verify!( data.source == state.current_justified_checkpoint(), Invalid::WrongJustifiedCheckpoint { - state: state.current_justified_checkpoint(), - attestation: data.source, + state: Box::new(state.current_justified_checkpoint()), + attestation: Box::new(data.source), is_current: true, } ); @@ -112,8 +112,8 @@ fn verify_casper_ffg_vote( verify!( data.source == state.previous_justified_checkpoint(), Invalid::WrongJustifiedCheckpoint { - state: state.previous_justified_checkpoint(), - attestation: data.source, + state: Box::new(state.previous_justified_checkpoint()), + attestation: Box::new(data.source), is_current: false, } ); diff --git a/consensus/state_processing/src/per_block_processing/verify_attester_slashing.rs b/consensus/state_processing/src/per_block_processing/verify_attester_slashing.rs index 709d99ec1c..0cb215fe93 100644 --- a/consensus/state_processing/src/per_block_processing/verify_attester_slashing.rs +++ b/consensus/state_processing/src/per_block_processing/verify_attester_slashing.rs @@ -13,16 +13,15 @@ fn error(reason: Invalid) -> BlockOperationError { /// Indicates if an `AttesterSlashing` is valid to be included in a block in the current epoch of /// the given state. /// -/// Returns `Ok(())` if the `AttesterSlashing` is valid, otherwise indicates the reason for +/// Returns `Ok(indices)` with `indices` being a non-empty vec of validator indices in ascending +/// order if the `AttesterSlashing` is valid. Otherwise returns `Err(e)` with the reason for /// invalidity. -/// -/// Spec v0.12.1 -pub fn verify_attester_slashing( - state: &BeaconState, - attester_slashing: &AttesterSlashing, +pub fn verify_attester_slashing( + state: &BeaconState, + attester_slashing: &AttesterSlashing, verify_signatures: VerifySignatures, spec: &ChainSpec, -) -> Result<()> { +) -> Result> { let attestation_1 = &attester_slashing.attestation_1; let attestation_2 = &attester_slashing.attestation_2; @@ -38,17 +37,15 @@ pub fn verify_attester_slashing( is_valid_indexed_attestation(state, attestation_2, verify_signatures, spec) .map_err(|e| error(Invalid::IndexedAttestation2Invalid(e)))?; - Ok(()) + get_slashable_indices(state, attester_slashing) } /// For a given attester slashing, return the indices able to be slashed in ascending order. /// -/// Returns Ok(indices) if `indices.len() > 0`. -/// -/// Spec v0.12.1 -pub fn get_slashable_indices( - state: &BeaconState, - attester_slashing: &AttesterSlashing, +/// Returns Ok(indices) if `indices.len() > 0` +pub fn get_slashable_indices( + state: &BeaconState, + attester_slashing: &AttesterSlashing, ) -> Result> { get_slashable_indices_modular(state, attester_slashing, |_, validator| { validator.is_slashable_at(state.current_epoch()) @@ -57,9 +54,9 @@ pub fn get_slashable_indices( /// Same as `gather_attester_slashing_indices` but allows the caller to specify the criteria /// for determining whether a given validator should be considered slashable. -pub fn get_slashable_indices_modular( - state: &BeaconState, - attester_slashing: &AttesterSlashing, +pub fn get_slashable_indices_modular( + state: &BeaconState, + attester_slashing: &AttesterSlashing, is_slashable: F, ) -> Result> where diff --git a/consensus/state_processing/src/per_block_processing/verify_bls_to_execution_change.rs b/consensus/state_processing/src/per_block_processing/verify_bls_to_execution_change.rs index 731a82aa95..1e8f25ed10 100644 --- a/consensus/state_processing/src/per_block_processing/verify_bls_to_execution_change.rs +++ b/consensus/state_processing/src/per_block_processing/verify_bls_to_execution_change.rs @@ -14,8 +14,8 @@ fn error(reason: Invalid) -> BlockOperationError { /// where the block is being applied to the given `state`. /// /// Returns `Ok(())` if the `SignedBlsToExecutionChange` is valid, otherwise indicates the reason for invalidity. -pub fn verify_bls_to_execution_change( - state: &BeaconState, +pub fn verify_bls_to_execution_change( + state: &BeaconState, signed_address_change: &SignedBlsToExecutionChange, verify_signatures: VerifySignatures, spec: &ChainSpec, diff --git a/consensus/state_processing/src/per_block_processing/verify_deposit.rs b/consensus/state_processing/src/per_block_processing/verify_deposit.rs index 181b27ca1a..a964f3b574 100644 --- a/consensus/state_processing/src/per_block_processing/verify_deposit.rs +++ b/consensus/state_processing/src/per_block_processing/verify_deposit.rs @@ -30,8 +30,8 @@ pub fn verify_deposit_signature(deposit_data: &DepositData, spec: &ChainSpec) -> /// otherwise returns `None`. /// /// Builds the pubkey cache if it is not already built. -pub fn get_existing_validator_index( - state: &mut BeaconState, +pub fn get_existing_validator_index( + state: &mut BeaconState, pub_key: &PublicKeyBytes, ) -> Result> { let validator_index = state.get_validator_index(pub_key)?; @@ -44,8 +44,8 @@ pub fn get_existing_validator_index( /// before they're due to be processed, and in parallel. /// /// Spec v0.12.1 -pub fn verify_deposit_merkle_proof( - state: &BeaconState, +pub fn verify_deposit_merkle_proof( + state: &BeaconState, deposit: &Deposit, deposit_index: u64, spec: &ChainSpec, diff --git a/consensus/state_processing/src/per_block_processing/verify_exit.rs b/consensus/state_processing/src/per_block_processing/verify_exit.rs index 9e9282912d..fc258d3829 100644 --- a/consensus/state_processing/src/per_block_processing/verify_exit.rs +++ b/consensus/state_processing/src/per_block_processing/verify_exit.rs @@ -18,8 +18,8 @@ fn error(reason: ExitInvalid) -> BlockOperationError { /// Returns `Ok(())` if the `Exit` is valid, otherwise indicates the reason for invalidity. /// /// Spec v0.12.1 -pub fn verify_exit( - state: &BeaconState, +pub fn verify_exit( + state: &BeaconState, current_epoch: Option, signed_exit: &SignedVoluntaryExit, verify_signatures: VerifySignatures, diff --git a/consensus/state_processing/src/per_block_processing/verify_proposer_slashing.rs b/consensus/state_processing/src/per_block_processing/verify_proposer_slashing.rs index 9b290a47e1..b06ca3a3a6 100644 --- a/consensus/state_processing/src/per_block_processing/verify_proposer_slashing.rs +++ b/consensus/state_processing/src/per_block_processing/verify_proposer_slashing.rs @@ -15,9 +15,9 @@ fn error(reason: Invalid) -> BlockOperationError { /// Returns `Ok(())` if the `ProposerSlashing` is valid, otherwise indicates the reason for invalidity. /// /// Spec v0.12.1 -pub fn verify_proposer_slashing( +pub fn verify_proposer_slashing( proposer_slashing: &ProposerSlashing, - state: &BeaconState, + state: &BeaconState, verify_signatures: VerifySignatures, spec: &ChainSpec, ) -> Result<()> { diff --git a/consensus/state_processing/src/per_epoch_processing.rs b/consensus/state_processing/src/per_epoch_processing.rs index d5d06037cd..b51aa23f37 100644 --- a/consensus/state_processing/src/per_epoch_processing.rs +++ b/consensus/state_processing/src/per_epoch_processing.rs @@ -1,13 +1,14 @@ #![deny(clippy::wildcard_imports)] -pub use epoch_processing_summary::EpochProcessingSummary; +use crate::metrics; +pub use epoch_processing_summary::{EpochProcessingSummary, ParticipationEpochSummary}; use errors::EpochProcessingError as Error; pub use justification_and_finalization_state::JustificationAndFinalizationState; use safe_arith::SafeArith; use types::{BeaconState, ChainSpec, EthSpec}; -pub use registry_updates::process_registry_updates; -pub use slashings::process_slashings; +pub use registry_updates::{process_registry_updates, process_registry_updates_slow}; +pub use slashings::{process_slashings, process_slashings_slow}; pub use weigh_justification_and_finalization::weigh_justification_and_finalization; pub mod altair; @@ -20,6 +21,7 @@ pub mod historical_roots_update; pub mod justification_and_finalization_state; pub mod registry_updates; pub mod resets; +pub mod single_pass; pub mod slashings; pub mod tests; pub mod weigh_justification_and_finalization; @@ -28,10 +30,12 @@ pub mod weigh_justification_and_finalization; /// /// Mutates the given `BeaconState`, returning early if an error is encountered. If an error is /// returned, a state might be "half-processed" and therefore in an invalid state. -pub fn process_epoch( - state: &mut BeaconState, +pub fn process_epoch( + state: &mut BeaconState, spec: &ChainSpec, -) -> Result, Error> { +) -> Result, Error> { + let _timer = metrics::start_timer(&metrics::PROCESS_EPOCH_TIME); + // Verify that the `BeaconState` instantiation matches the fork at `state.slot()`. state .fork_name(spec) @@ -39,8 +43,11 @@ pub fn process_epoch( match state { BeaconState::Base(_) => base::process_epoch(state, spec), - BeaconState::Altair(_) | BeaconState::Merge(_) => altair::process_epoch(state, spec), - BeaconState::Capella(_) | BeaconState::Deneb(_) => capella::process_epoch(state, spec), + BeaconState::Altair(_) + | BeaconState::Merge(_) + | BeaconState::Capella(_) + | BeaconState::Deneb(_) + | BeaconState::Electra(_) => altair::process_epoch(state, spec), } } diff --git a/consensus/state_processing/src/per_epoch_processing/altair.rs b/consensus/state_processing/src/per_epoch_processing/altair.rs index 0abbd16a98..5fcd147b2e 100644 --- a/consensus/state_processing/src/per_epoch_processing/altair.rs +++ b/consensus/state_processing/src/per_epoch_processing/altair.rs @@ -1,75 +1,76 @@ -use super::{process_registry_updates, process_slashings, EpochProcessingSummary, Error}; +use super::{EpochProcessingSummary, Error}; use crate::common::update_progressive_balances_cache::{ initialize_progressive_balances_cache, update_progressive_balances_on_epoch_transition, }; +use crate::epoch_cache::initialize_epoch_cache; +use crate::per_epoch_processing::single_pass::{process_epoch_single_pass, SinglePassConfig}; use crate::per_epoch_processing::{ - effective_balance_updates::process_effective_balance_updates, + capella::process_historical_summaries_update, historical_roots_update::process_historical_roots_update, resets::{process_eth1_data_reset, process_randao_mixes_reset, process_slashings_reset}, }; -pub use inactivity_updates::process_inactivity_updates; +pub use inactivity_updates::process_inactivity_updates_slow; pub use justification_and_finalization::process_justification_and_finalization; -pub use participation_cache::ParticipationCache; pub use participation_flag_updates::process_participation_flag_updates; -pub use rewards_and_penalties::process_rewards_and_penalties; +pub use rewards_and_penalties::process_rewards_and_penalties_slow; pub use sync_committee_updates::process_sync_committee_updates; use types::{BeaconState, ChainSpec, EthSpec, RelativeEpoch}; pub mod inactivity_updates; pub mod justification_and_finalization; -pub mod participation_cache; pub mod participation_flag_updates; pub mod rewards_and_penalties; pub mod sync_committee_updates; -pub fn process_epoch( - state: &mut BeaconState, +pub fn process_epoch( + state: &mut BeaconState, spec: &ChainSpec, -) -> Result, Error> { - // Ensure the committee caches are built. +) -> Result, Error> { + // Ensure the required caches are built. state.build_committee_cache(RelativeEpoch::Previous, spec)?; state.build_committee_cache(RelativeEpoch::Current, spec)?; state.build_committee_cache(RelativeEpoch::Next, spec)?; + state.build_total_active_balance_cache(spec)?; + initialize_epoch_cache(state, spec)?; + initialize_progressive_balances_cache::(state, spec)?; - // 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 = - process_justification_and_finalization(state, &participation_cache)?; + let justification_and_finalization_state = process_justification_and_finalization(state)?; justification_and_finalization_state.apply_changes_to_state(state); - process_inactivity_updates(state, &participation_cache, spec)?; - - // Rewards and Penalties. - process_rewards_and_penalties(state, &participation_cache, spec)?; - - // Registry Updates. - process_registry_updates(state, spec)?; - - // Slashings. - process_slashings( - state, - participation_cache.current_epoch_total_active_balance(), - spec, - )?; + // In a single pass: + // - Inactivity updates + // - Rewards and penalties + // - Registry updates + // - Slashings + // - Effective balance updates + // + // The `process_eth1_data_reset` is not covered in the single pass, but happens afterwards + // without loss of correctness. + let current_epoch_progressive_balances = state.progressive_balances_cache().clone(); + let current_epoch_total_active_balance = state.get_total_active_balance()?; + let participation_summary = + process_epoch_single_pass(state, spec, SinglePassConfig::default())?; // Reset eth1 data votes. process_eth1_data_reset(state)?; - // Update effective balances with hysteresis (lag). - process_effective_balance_updates(state, Some(&participation_cache), spec)?; - // Reset slashings process_slashings_reset(state)?; // Set randao mix process_randao_mixes_reset(state)?; - // Set historical root accumulator - process_historical_roots_update(state)?; + // Set historical summaries accumulator + if state.historical_summaries().is_ok() { + // Post-Capella. + process_historical_summaries_update(state)?; + } else { + // Pre-Capella + process_historical_roots_update(state)?; + } // Rotate current/previous epoch participation process_participation_flag_updates(state)?; @@ -77,12 +78,13 @@ pub fn process_epoch( process_sync_committee_updates(state, spec)?; // Rotate the epoch caches to suit the epoch transition. - state.advance_caches(spec)?; - + state.advance_caches()?; update_progressive_balances_on_epoch_transition(state, spec)?; Ok(EpochProcessingSummary::Altair { - participation_cache, + progressive_balances: current_epoch_progressive_balances, + current_epoch_total_active_balance, + participation: participation_summary, 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 a895567d12..698e88b83f 100644 --- a/consensus/state_processing/src/per_epoch_processing/altair/inactivity_updates.rs +++ b/consensus/state_processing/src/per_epoch_processing/altair/inactivity_updates.rs @@ -1,44 +1,23 @@ -use super::ParticipationCache; +use crate::per_epoch_processing::single_pass::{process_epoch_single_pass, SinglePassConfig}; use crate::EpochProcessingError; -use core::result::Result; -use core::result::Result::Ok; -use safe_arith::SafeArith; -use std::cmp::min; use types::beacon_state::BeaconState; use types::chain_spec::ChainSpec; -use types::consts::altair::TIMELY_TARGET_FLAG_INDEX; use types::eth_spec::EthSpec; -pub fn process_inactivity_updates( - state: &mut BeaconState, - participation_cache: &ParticipationCache, +/// Slow version of `process_inactivity_updates` that runs a subset of single-pass processing. +/// +/// Should not be used for block processing, but is useful for testing & analytics. +pub fn process_inactivity_updates_slow( + state: &mut BeaconState, spec: &ChainSpec, ) -> Result<(), EpochProcessingError> { - let previous_epoch = state.previous_epoch(); - // Score updates based on previous epoch participation, skip genesis epoch - if state.current_epoch() == T::genesis_epoch() { - return Ok(()); - } - - let unslashed_indices = participation_cache - .get_unslashed_participating_indices(TIMELY_TARGET_FLAG_INDEX, state.previous_epoch())?; - - for &index in participation_cache.eligible_validator_indices() { - // Increase inactivity score of inactive validators - if unslashed_indices.contains(index)? { - let inactivity_score = state.get_inactivity_score_mut(index)?; - inactivity_score.safe_sub_assign(min(1, *inactivity_score))?; - } else { - state - .get_inactivity_score_mut(index)? - .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)? { - let inactivity_score = state.get_inactivity_score_mut(index)?; - inactivity_score - .safe_sub_assign(min(spec.inactivity_score_recovery_rate, *inactivity_score))?; - } - } + process_epoch_single_pass( + state, + spec, + SinglePassConfig { + inactivity_updates: true, + ..SinglePassConfig::disable_all() + }, + )?; Ok(()) } diff --git a/consensus/state_processing/src/per_epoch_processing/altair/justification_and_finalization.rs b/consensus/state_processing/src/per_epoch_processing/altair/justification_and_finalization.rs index 1f17cf56e0..61b5c1ed5a 100644 --- a/consensus/state_processing/src/per_epoch_processing/altair/justification_and_finalization.rs +++ b/consensus/state_processing/src/per_epoch_processing/altair/justification_and_finalization.rs @@ -1,32 +1,27 @@ -use super::ParticipationCache; use crate::per_epoch_processing::Error; use crate::per_epoch_processing::{ weigh_justification_and_finalization, JustificationAndFinalizationState, }; use safe_arith::SafeArith; -use types::consts::altair::TIMELY_TARGET_FLAG_INDEX; use types::{BeaconState, EthSpec}; -/// Update the justified and finalized checkpoints for matching target attestations. -pub fn process_justification_and_finalization( - state: &BeaconState, - participation_cache: &ParticipationCache, -) -> Result, Error> { +/// Process justification and finalization using the progressive balances cache. +pub fn process_justification_and_finalization( + state: &BeaconState, +) -> Result, Error> { let justification_and_finalization_state = JustificationAndFinalizationState::new(state); - - if state.current_epoch() <= T::genesis_epoch().safe_add(1)? { + if state.current_epoch() <= E::genesis_epoch().safe_add(1)? { return Ok(justification_and_finalization_state); } - let previous_epoch = state.previous_epoch(); - let current_epoch = state.current_epoch(); - let previous_indices = participation_cache - .get_unslashed_participating_indices(TIMELY_TARGET_FLAG_INDEX, previous_epoch)?; - let current_indices = participation_cache - .get_unslashed_participating_indices(TIMELY_TARGET_FLAG_INDEX, current_epoch)?; - let total_active_balance = participation_cache.current_epoch_total_active_balance(); - let previous_target_balance = previous_indices.total_balance()?; - let current_target_balance = current_indices.total_balance()?; + // Load cached balances + let progressive_balances_cache = 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()?; + weigh_justification_and_finalization( justification_and_finalization_state, total_active_balance, 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 deleted file mode 100644 index d67e7874cb..0000000000 --- a/consensus/state_processing/src/per_epoch_processing/altair/participation_cache.rs +++ /dev/null @@ -1,402 +0,0 @@ -//! Provides the `ParticipationCache`, a custom Lighthouse cache which attempts to reduce CPU and -//! memory usage by: -//! -//! - Caching a map of `validator_index -> participation_flags` for all active validators in the -//! previous and current epochs. -//! - Caching the total balances of: -//! - All active validators. -//! - All active validators matching each of the three "timely" flags. -//! - Caching the "eligible" validators. -//! -//! 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 types::{ - consts::altair::{ - NUM_FLAG_INDICES, TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX, - TIMELY_TARGET_FLAG_INDEX, - }, - Balance, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, ParticipationFlags, - RelativeEpoch, -}; - -#[derive(Debug, PartialEq, Clone)] -pub enum Error { - InvalidFlagIndex(usize), - InvalidValidatorIndex(usize), -} - -/// Caches the participation values for one epoch (either the previous or current). -#[derive(PartialEq, Debug, Clone)] -struct SingleEpochParticipationCache { - /// Maps an active validator index to their participation flags. - /// - /// To reiterate, only active and unslashed validator indices are stored in this map. - /// - /// ## Note - /// - /// It would be ideal to maintain a reference to the `BeaconState` here rather than copying the - /// `ParticipationFlags`, however that would cause us to run into mutable reference limitations - /// upstream. - unslashed_participating_indices: Vec>, - /// Stores the sum of the balances for all validators in `self.unslashed_participating_indices` - /// for all flags in `NUM_FLAG_INDICES`. - /// - /// A flag balance is only incremented if a validator is in that flag set. - total_flag_balances: [Balance; NUM_FLAG_INDICES], - /// Stores the sum of all balances of all validators in `self.unslashed_participating_indices` - /// (regardless of which flags are set). - total_active_balance: Balance, -} - -impl SingleEpochParticipationCache { - fn new(state: &BeaconState, spec: &ChainSpec) -> Self { - let num_validators = state.validators().len(); - let zero_balance = Balance::zero(spec.effective_balance_increment); - - Self { - unslashed_participating_indices: vec![None; num_validators], - total_flag_balances: [zero_balance; NUM_FLAG_INDICES], - total_active_balance: zero_balance, - } - } - - /// Returns the total balance of attesters who have `flag_index` set. - fn total_flag_balance(&self, flag_index: usize) -> Result { - self.total_flag_balances - .get(flag_index) - .map(Balance::get) - .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 { - 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 - /// - /// May return an error if `flag_index` is out-of-bounds. - fn has_flag(&self, val_index: usize, flag_index: usize) -> Result { - let participation_flags = self - .unslashed_participating_indices - .get(val_index) - .ok_or(Error::InvalidValidatorIndex(val_index))?; - if let Some(participation_flags) = participation_flags { - participation_flags - .has_flag(flag_index) - .map_err(|_| Error::InvalidFlagIndex(flag_index)) - } else { - Ok(false) - } - } - - /// Process an **active** validator, reading from the `state` with respect to the - /// `relative_epoch`. - /// - /// ## Errors - /// - /// - The provided `state` **must** be Altair. An error will be returned otherwise. - /// - An error will be returned if the `val_index` validator is inactive at the given - /// `relative_epoch`. - fn process_active_validator( - &mut self, - val_index: usize, - state: &BeaconState, - current_epoch: Epoch, - relative_epoch: RelativeEpoch, - ) -> Result<(), BeaconStateError> { - 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); - if !validator.is_active_at(epoch) { - return Err(BeaconStateError::ValidatorIsInactive { val_index }); - } - - let epoch_participation = match relative_epoch { - RelativeEpoch::Current => state.current_epoch_participation(), - RelativeEpoch::Previous => state.previous_epoch_participation(), - _ => Err(BeaconStateError::EpochOutOfBounds), - }? - .get(val_index) - .ok_or(BeaconStateError::ParticipationOutOfBounds(val_index))?; - - // All active validators increase the total active balance. - self.total_active_balance.safe_add_assign(val_balance)?; - - // Only unslashed validators may proceed. - if validator.slashed { - return Ok(()); - } - - // Add their `ParticipationFlags` to the map. - *self - .unslashed_participating_indices - .get_mut(val_index) - .ok_or(BeaconStateError::UnknownValidator(val_index))? = Some(*epoch_participation); - - // Iterate through all the flags and increment the total flag balances for whichever flags - // are set for `val_index`. - for (flag, balance) in self.total_flag_balances.iter_mut().enumerate() { - if epoch_participation.has_flag(flag)? { - balance.safe_add_assign(val_balance)?; - } - } - - Ok(()) - } -} - -/// Maintains a cache to be used during `altair::process_epoch`. -#[derive(PartialEq, Debug, Clone)] -pub struct ParticipationCache { - current_epoch: Epoch, - /// Caches information about active validators pertaining to `self.current_epoch`. - current_epoch_participation: SingleEpochParticipationCache, - previous_epoch: Epoch, - /// Caches information about active validators pertaining to `self.previous_epoch`. - previous_epoch_participation: SingleEpochParticipationCache, - /// Caches the result of the `get_eligible_validator_indices` function. - eligible_indices: Vec, -} - -impl ParticipationCache { - /// Instantiate `Self`, returning a fully initialized cache. - /// - /// ## Errors - /// - /// - The provided `state` **must** be an Altair state. An error will be returned otherwise. - pub fn new( - state: &BeaconState, - spec: &ChainSpec, - ) -> Result { - let current_epoch = state.current_epoch(); - let previous_epoch = state.previous_epoch(); - - // Both the current/previous epoch participations are set to a capacity that is slightly - // larger than required. The difference will be due slashed-but-active validators. - let mut current_epoch_participation = SingleEpochParticipationCache::new(state, spec); - let mut previous_epoch_participation = SingleEpochParticipationCache::new(state, spec); - // Contains the set of validators which are either: - // - // - Active in the previous epoch. - // - Slashed, but not yet withdrawable. - // - // Using the full length of `state.validators` is almost always overkill, but it ensures no - // reallocations. - let mut eligible_indices = Vec::with_capacity(state.validators().len()); - - // Iterate through all validators, updating: - // - // 1. Validator participation for current and previous epochs. - // 2. The "eligible indices". - // - // Care is taken to ensure that the ordering of `eligible_indices` is the same as the - // `get_eligible_validator_indices` function in the spec. - for (val_index, val) in state.validators().iter().enumerate() { - if val.is_active_at(current_epoch) { - current_epoch_participation.process_active_validator( - val_index, - state, - current_epoch, - RelativeEpoch::Current, - )?; - } - - if val.is_active_at(previous_epoch) { - previous_epoch_participation.process_active_validator( - val_index, - state, - current_epoch, - RelativeEpoch::Previous, - )?; - } - - // Note: a validator might still be "eligible" whilst returning `false` to - // `Validator::is_active_at`. - if state.is_eligible_validator(previous_epoch, val_index)? { - eligible_indices.push(val_index) - } - } - - Ok(Self { - current_epoch, - current_epoch_participation, - previous_epoch, - previous_epoch_participation, - eligible_indices, - }) - } - - /// Equivalent to the specification `get_eligible_validator_indices` function. - pub fn eligible_validator_indices(&self) -> &[usize] { - &self.eligible_indices - } - - /// Equivalent to the `get_unslashed_participating_indices` function in the specification. - pub fn get_unslashed_participating_indices( - &self, - flag_index: usize, - epoch: Epoch, - ) -> Result { - let participation = if epoch == self.current_epoch { - &self.current_epoch_participation - } else if epoch == self.previous_epoch { - &self.previous_epoch_participation - } else { - return Err(BeaconStateError::EpochOutOfBounds); - }; - - Ok(UnslashedParticipatingIndices { - participation, - flag_index, - }) - } - - /* - * Balances - */ - - pub fn current_epoch_total_active_balance(&self) -> u64 { - self.current_epoch_participation.total_active_balance.get() - } - - pub fn current_epoch_target_attesting_balance(&self) -> Result { - self.current_epoch_participation - .total_flag_balance(TIMELY_TARGET_FLAG_INDEX) - } - - pub fn current_epoch_target_attesting_balance_raw(&self) -> Result { - 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() - } - - pub fn previous_epoch_target_attesting_balance(&self) -> Result { - self.previous_epoch_participation - .total_flag_balance(TIMELY_TARGET_FLAG_INDEX) - } - - pub fn previous_epoch_target_attesting_balance_raw(&self) -> Result { - self.previous_epoch_participation - .total_flag_balance_raw(TIMELY_TARGET_FLAG_INDEX) - } - - pub fn previous_epoch_source_attesting_balance(&self) -> Result { - self.previous_epoch_participation - .total_flag_balance(TIMELY_SOURCE_FLAG_INDEX) - } - - pub fn previous_epoch_head_attesting_balance(&self) -> Result { - self.previous_epoch_participation - .total_flag_balance(TIMELY_HEAD_FLAG_INDEX) - } - - /* - * Active/Unslashed - */ - - /// Returns `None` for an unknown `val_index`. - pub fn is_active_unslashed_in_previous_epoch(&self, val_index: usize) -> Option { - self.previous_epoch_participation - .unslashed_participating_indices - .get(val_index) - .map(|flags| flags.is_some()) - } - - /// Returns `None` for an unknown `val_index`. - pub fn is_active_unslashed_in_current_epoch(&self, val_index: usize) -> Option { - self.current_epoch_participation - .unslashed_participating_indices - .get(val_index) - .map(|flags| flags.is_some()) - } - - /* - * Flags - */ - - /// Always returns false for a slashed validator. - pub fn is_previous_epoch_timely_source_attester( - &self, - val_index: usize, - ) -> Result { - self.previous_epoch_participation - .has_flag(val_index, TIMELY_SOURCE_FLAG_INDEX) - } - - /// Always returns false for a slashed validator. - pub fn is_previous_epoch_timely_target_attester( - &self, - val_index: usize, - ) -> Result { - self.previous_epoch_participation - .has_flag(val_index, TIMELY_TARGET_FLAG_INDEX) - } - - /// Always returns false for a slashed validator. - pub fn is_previous_epoch_timely_head_attester(&self, val_index: usize) -> Result { - self.previous_epoch_participation - .has_flag(val_index, TIMELY_HEAD_FLAG_INDEX) - } - - /// Always returns false for a slashed validator. - pub fn is_current_epoch_timely_source_attester(&self, val_index: usize) -> Result { - self.current_epoch_participation - .has_flag(val_index, TIMELY_SOURCE_FLAG_INDEX) - } - - /// Always returns false for a slashed validator. - pub fn is_current_epoch_timely_target_attester(&self, val_index: usize) -> Result { - self.current_epoch_participation - .has_flag(val_index, TIMELY_TARGET_FLAG_INDEX) - } - - /// Always returns false for a slashed validator. - pub fn is_current_epoch_timely_head_attester(&self, val_index: usize) -> Result { - self.current_epoch_participation - .has_flag(val_index, TIMELY_HEAD_FLAG_INDEX) - } -} - -/// Imitates the return value of the `get_unslashed_participating_indices` in the -/// specification. -/// -/// This struct exists to help make the Lighthouse code read more like the specification. -pub struct UnslashedParticipatingIndices<'a> { - participation: &'a SingleEpochParticipationCache, - flag_index: usize, -} - -impl<'a> UnslashedParticipatingIndices<'a> { - /// Returns `Ok(true)` if the given `val_index` is both: - /// - /// - An active validator. - /// - Has `self.flag_index` set. - pub fn contains(&self, val_index: usize) -> Result { - self.participation.has_flag(val_index, self.flag_index) - } - - /// Returns the sum of all balances of validators which have `self.flag_index` set. - /// - /// ## Notes - /// - /// Respects the `EFFECTIVE_BALANCE_INCREMENT` minimum. - pub fn total_balance(&self) -> Result { - self.participation - .total_flag_balances - .get(self.flag_index) - .ok_or(Error::InvalidFlagIndex(self.flag_index)) - .map(Balance::get) - } -} diff --git a/consensus/state_processing/src/per_epoch_processing/altair/participation_flag_updates.rs b/consensus/state_processing/src/per_epoch_processing/altair/participation_flag_updates.rs index 7162fa7f4a..dd1b2dfcd8 100644 --- a/consensus/state_processing/src/per_epoch_processing/altair/participation_flag_updates.rs +++ b/consensus/state_processing/src/per_epoch_processing/altair/participation_flag_updates.rs @@ -1,13 +1,11 @@ use crate::EpochProcessingError; -use core::result::Result; -use core::result::Result::Ok; use types::beacon_state::BeaconState; use types::eth_spec::EthSpec; use types::participation_flags::ParticipationFlags; use types::VariableList; -pub fn process_participation_flag_updates( - state: &mut BeaconState, +pub fn process_participation_flag_updates( + state: &mut BeaconState, ) -> Result<(), EpochProcessingError> { *state.previous_epoch_participation_mut()? = std::mem::take(state.current_epoch_participation_mut()?); 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 19d57130c9..c4059f94af 100644 --- a/consensus/state_processing/src/per_epoch_processing/altair/rewards_and_penalties.rs +++ b/consensus/state_processing/src/per_epoch_processing/altair/rewards_and_penalties.rs @@ -1,98 +1,25 @@ -use super::ParticipationCache; -use safe_arith::SafeArith; -use types::consts::altair::{ - PARTICIPATION_FLAG_WEIGHTS, TIMELY_HEAD_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX, - WEIGHT_DENOMINATOR, +use crate::per_epoch_processing::{ + single_pass::{process_epoch_single_pass, SinglePassConfig}, + Error, }; +use types::consts::altair::PARTICIPATION_FLAG_WEIGHTS; use types::{BeaconState, ChainSpec, EthSpec}; -use crate::common::{ - altair::{get_base_reward, BaseRewardPerIncrement}, - decrease_balance, increase_balance, -}; -use crate::per_epoch_processing::{Delta, Error}; - /// Apply attester and proposer rewards. /// -/// Spec v1.1.0 -pub fn process_rewards_and_penalties( - state: &mut BeaconState, - participation_cache: &ParticipationCache, +/// This function should only be used for testing. +pub fn process_rewards_and_penalties_slow( + state: &mut BeaconState, spec: &ChainSpec, ) -> Result<(), Error> { - if state.current_epoch() == T::genesis_epoch() { - return Ok(()); - } - - let mut deltas = vec![Delta::default(); state.validators().len()]; - - let total_active_balance = participation_cache.current_epoch_total_active_balance(); - - for flag_index in 0..PARTICIPATION_FLAG_WEIGHTS.len() { - get_flag_index_deltas( - &mut deltas, - state, - flag_index, - total_active_balance, - participation_cache, - spec, - )?; - } - - get_inactivity_penalty_deltas(&mut deltas, state, participation_cache, spec)?; - - // Apply the deltas, erroring on overflow above but not on overflow below (saturating at 0 - // instead). - for (i, delta) in deltas.into_iter().enumerate() { - increase_balance(state, i, delta.rewards)?; - decrease_balance(state, i, delta.penalties)?; - } - - Ok(()) -} - -/// Return the deltas for a given flag index by scanning through the participation flags. -/// -/// Spec v1.1.0 -pub fn get_flag_index_deltas( - deltas: &mut [Delta], - state: &BeaconState, - flag_index: usize, - total_active_balance: u64, - participation_cache: &ParticipationCache, - spec: &ChainSpec, -) -> Result<(), Error> { - let previous_epoch = state.previous_epoch(); - let unslashed_participating_indices = - participation_cache.get_unslashed_participating_indices(flag_index, previous_epoch)?; - let weight = get_flag_weight(flag_index)?; - let unslashed_participating_balance = unslashed_participating_indices.total_balance()?; - let unslashed_participating_increments = - unslashed_participating_balance.safe_div(spec.effective_balance_increment)?; - let active_increments = total_active_balance.safe_div(spec.effective_balance_increment)?; - let base_reward_per_increment = BaseRewardPerIncrement::new(total_active_balance, spec)?; - - for &index in participation_cache.eligible_validator_indices() { - let base_reward = get_base_reward(state, index, base_reward_per_increment, spec)?; - let mut delta = Delta::default(); - - if unslashed_participating_indices.contains(index)? { - if !state.is_in_inactivity_leak(previous_epoch, spec)? { - let reward_numerator = base_reward - .safe_mul(weight)? - .safe_mul(unslashed_participating_increments)?; - delta.reward( - reward_numerator.safe_div(active_increments.safe_mul(WEIGHT_DENOMINATOR)?)?, - )?; - } - } else if flag_index != TIMELY_HEAD_FLAG_INDEX { - delta.penalize(base_reward.safe_mul(weight)?.safe_div(WEIGHT_DENOMINATOR)?)?; - } - deltas - .get_mut(index) - .ok_or(Error::DeltaOutOfBounds(index))? - .combine(delta)?; - } + process_epoch_single_pass( + state, + spec, + SinglePassConfig { + rewards_and_penalties: true, + ..SinglePassConfig::disable_all() + }, + )?; Ok(()) } @@ -103,33 +30,3 @@ pub fn get_flag_weight(flag_index: usize) -> Result { .copied() .ok_or(Error::InvalidFlagIndex(flag_index)) } - -pub fn get_inactivity_penalty_deltas( - deltas: &mut [Delta], - state: &BeaconState, - participation_cache: &ParticipationCache, - spec: &ChainSpec, -) -> Result<(), Error> { - let previous_epoch = state.previous_epoch(); - let matching_target_indices = participation_cache - .get_unslashed_participating_indices(TIMELY_TARGET_FLAG_INDEX, previous_epoch)?; - for &index in participation_cache.eligible_validator_indices() { - let mut delta = Delta::default(); - - if !matching_target_indices.contains(index)? { - let penalty_numerator = state - .get_validator(index)? - .effective_balance - .safe_mul(state.get_inactivity_score(index)?)?; - let penalty_denominator = spec - .inactivity_score_bias - .safe_mul(spec.inactivity_penalty_quotient_for_state(state))?; - delta.penalize(penalty_numerator.safe_div(penalty_denominator)?)?; - } - deltas - .get_mut(index) - .ok_or(Error::DeltaOutOfBounds(index))? - .combine(delta)?; - } - Ok(()) -} diff --git a/consensus/state_processing/src/per_epoch_processing/altair/sync_committee_updates.rs b/consensus/state_processing/src/per_epoch_processing/altair/sync_committee_updates.rs index 294c05d1a4..3bb2ced982 100644 --- a/consensus/state_processing/src/per_epoch_processing/altair/sync_committee_updates.rs +++ b/consensus/state_processing/src/per_epoch_processing/altair/sync_committee_updates.rs @@ -5,8 +5,8 @@ use types::beacon_state::BeaconState; use types::chain_spec::ChainSpec; use types::eth_spec::EthSpec; -pub fn process_sync_committee_updates( - state: &mut BeaconState, +pub fn process_sync_committee_updates( + state: &mut BeaconState, spec: &ChainSpec, ) -> Result<(), EpochProcessingError> { let next_epoch = state.next_epoch()?; diff --git a/consensus/state_processing/src/per_epoch_processing/base.rs b/consensus/state_processing/src/per_epoch_processing/base.rs index c5864bd1ef..e468a8ddd6 100644 --- a/consensus/state_processing/src/per_epoch_processing/base.rs +++ b/consensus/state_processing/src/per_epoch_processing/base.rs @@ -1,4 +1,5 @@ use super::{process_registry_updates, process_slashings, EpochProcessingSummary, Error}; +use crate::epoch_cache::initialize_epoch_cache; use crate::per_epoch_processing::{ effective_balance_updates::process_effective_balance_updates, historical_roots_update::process_historical_roots_update, @@ -15,14 +16,16 @@ pub mod participation_record_updates; pub mod rewards_and_penalties; pub mod validator_statuses; -pub fn process_epoch( - state: &mut BeaconState, +pub fn process_epoch( + state: &mut BeaconState, spec: &ChainSpec, -) -> Result, Error> { +) -> Result, Error> { // Ensure the committee caches are built. state.build_committee_cache(RelativeEpoch::Previous, spec)?; state.build_committee_cache(RelativeEpoch::Current, spec)?; state.build_committee_cache(RelativeEpoch::Next, spec)?; + state.build_total_active_balance_cache(spec)?; + initialize_epoch_cache(state, spec)?; // Load the struct we use to assign validators into sets based on their participation. // @@ -52,7 +55,7 @@ pub fn process_epoch( process_eth1_data_reset(state)?; // Update effective balances with hysteresis (lag). - process_effective_balance_updates(state, None, spec)?; + process_effective_balance_updates(state, spec)?; // Reset slashings process_slashings_reset(state)?; @@ -67,7 +70,7 @@ pub fn process_epoch( process_participation_record_updates(state)?; // Rotate the epoch caches to suit the epoch transition. - state.advance_caches(spec)?; + state.advance_caches()?; Ok(EpochProcessingSummary::Base { total_balances: validator_statuses.total_balances, diff --git a/consensus/state_processing/src/per_epoch_processing/base/justification_and_finalization.rs b/consensus/state_processing/src/per_epoch_processing/base/justification_and_finalization.rs index 9792b54507..db64808a80 100644 --- a/consensus/state_processing/src/per_epoch_processing/base/justification_and_finalization.rs +++ b/consensus/state_processing/src/per_epoch_processing/base/justification_and_finalization.rs @@ -7,14 +7,14 @@ use safe_arith::SafeArith; use types::{BeaconState, ChainSpec, EthSpec}; /// Update the justified and finalized checkpoints for matching target attestations. -pub fn process_justification_and_finalization( - state: &BeaconState, +pub fn process_justification_and_finalization( + state: &BeaconState, total_balances: &TotalBalances, _spec: &ChainSpec, -) -> Result, Error> { +) -> Result, Error> { let justification_and_finalization_state = JustificationAndFinalizationState::new(state); - if state.current_epoch() <= T::genesis_epoch().safe_add(1)? { + if state.current_epoch() <= E::genesis_epoch().safe_add(1)? { return Ok(justification_and_finalization_state); } diff --git a/consensus/state_processing/src/per_epoch_processing/base/participation_record_updates.rs b/consensus/state_processing/src/per_epoch_processing/base/participation_record_updates.rs index 2cb82d187d..52646e2269 100644 --- a/consensus/state_processing/src/per_epoch_processing/base/participation_record_updates.rs +++ b/consensus/state_processing/src/per_epoch_processing/base/participation_record_updates.rs @@ -2,8 +2,8 @@ use crate::EpochProcessingError; use types::beacon_state::BeaconState; use types::eth_spec::EthSpec; -pub fn process_participation_record_updates( - state: &mut BeaconState, +pub fn process_participation_record_updates( + state: &mut BeaconState, ) -> Result<(), EpochProcessingError> { let base_state = state.as_base_mut()?; base_state.previous_epoch_attestations = 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 74c96d8aee..ecea0b554e 100644 --- a/consensus/state_processing/src/per_epoch_processing/base/rewards_and_penalties.rs +++ b/consensus/state_processing/src/per_epoch_processing/base/rewards_and_penalties.rs @@ -1,4 +1,7 @@ -use crate::common::{base::get_base_reward, decrease_balance, increase_balance}; +use crate::common::{ + base::{get_base_reward, SqrtTotalActiveBalance}, + decrease_balance, increase_balance, +}; use crate::per_epoch_processing::{ base::{TotalBalances, ValidatorStatus, ValidatorStatuses}, Delta, Error, @@ -43,12 +46,12 @@ impl AttestationDelta { } /// Apply attester and proposer rewards. -pub fn process_rewards_and_penalties( - state: &mut BeaconState, +pub fn process_rewards_and_penalties( + state: &mut BeaconState, validator_statuses: &ValidatorStatuses, spec: &ChainSpec, ) -> Result<(), Error> { - if state.current_epoch() == T::genesis_epoch() { + if state.current_epoch() == E::genesis_epoch() { return Ok(()); } @@ -73,8 +76,8 @@ pub fn process_rewards_and_penalties( } /// Apply rewards for participation in attestations during the previous epoch. -pub fn get_attestation_deltas_all( - state: &BeaconState, +pub fn get_attestation_deltas_all( + state: &BeaconState, validator_statuses: &ValidatorStatuses, spec: &ChainSpec, ) -> Result, Error> { @@ -83,8 +86,8 @@ pub fn get_attestation_deltas_all( /// 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( - state: &BeaconState, +pub fn get_attestation_deltas_subset( + state: &BeaconState, validator_statuses: &ValidatorStatuses, validators_subset: &Vec, spec: &ChainSpec, @@ -103,13 +106,12 @@ pub fn get_attestation_deltas_subset( /// returned, otherwise deltas for all validators are returned. /// /// Returns a vec of validator indices to `AttestationDelta`. -fn get_attestation_deltas( - state: &BeaconState, +fn get_attestation_deltas( + state: &BeaconState, validator_statuses: &ValidatorStatuses, maybe_validators_subset: Option<&Vec>, spec: &ChainSpec, ) -> Result, Error> { - let previous_epoch = state.previous_epoch(); let finality_delay = state .previous_epoch() .safe_sub(state.finalized_checkpoint().epoch)? @@ -118,6 +120,7 @@ fn get_attestation_deltas( let mut deltas = vec![AttestationDelta::default(); state.validators().len()]; let total_balances = &validator_statuses.total_balances; + let sqrt_total_active_balance = SqrtTotalActiveBalance::new(total_balances.current_epoch()); // 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() { @@ -131,11 +134,15 @@ fn get_attestation_deltas( // `get_inclusion_delay_deltas`. It's safe to do so here because any validator that is in // the unslashed indices of the matching source attestations is active, and therefore // eligible. - if !state.is_eligible_validator(previous_epoch, index)? { + if !validator.is_eligible { continue; } - let base_reward = get_base_reward(state, index, total_balances.current_epoch(), spec)?; + let base_reward = get_base_reward( + validator.current_epoch_effective_balance, + sqrt_total_active_balance, + spec, + )?; let (inclusion_delay_delta, proposer_delta) = get_inclusion_delay_delta(validator, base_reward, spec)?; diff --git a/consensus/state_processing/src/per_epoch_processing/base/validator_statuses.rs b/consensus/state_processing/src/per_epoch_processing/base/validator_statuses.rs index 26d2536e5f..7e24405803 100644 --- a/consensus/state_processing/src/per_epoch_processing/base/validator_statuses.rs +++ b/consensus/state_processing/src/per_epoch_processing/base/validator_statuses.rs @@ -53,6 +53,8 @@ impl InclusionInfo { pub struct ValidatorStatus { /// True if the validator has been slashed, ever. pub is_slashed: bool, + /// True if the validator is eligible. + pub is_eligible: bool, /// True if the validator can withdraw in the current epoch. pub is_withdrawable_in_current_epoch: bool, /// True if the validator was active in the state's _current_ epoch. @@ -92,6 +94,7 @@ impl ValidatorStatus { // Update all the bool fields, only updating `self` if `other` is true (never setting // `self` to false). set_self_if_other_is_true!(self, other, is_slashed); + set_self_if_other_is_true!(self, other, is_eligible); set_self_if_other_is_true!(self, other, is_withdrawable_in_current_epoch); set_self_if_other_is_true!(self, other, is_active_in_current_epoch); set_self_if_other_is_true!(self, other, is_active_in_previous_epoch); @@ -188,31 +191,34 @@ impl ValidatorStatuses { /// - Total balances for the current and previous epochs. /// /// Spec v0.12.1 - pub fn new( - state: &BeaconState, + pub fn new( + state: &BeaconState, spec: &ChainSpec, ) -> Result { let mut statuses = Vec::with_capacity(state.validators().len()); let mut total_balances = TotalBalances::new(spec); - for (i, validator) in state.validators().iter().enumerate() { - let effective_balance = state.get_effective_balance(i)?; + let current_epoch = state.current_epoch(); + let previous_epoch = state.previous_epoch(); + + for validator in state.validators().iter() { + let effective_balance = validator.effective_balance; let mut status = ValidatorStatus { is_slashed: validator.slashed, - is_withdrawable_in_current_epoch: validator - .is_withdrawable_at(state.current_epoch()), + is_eligible: state.is_eligible_validator(previous_epoch, validator)?, + is_withdrawable_in_current_epoch: validator.is_withdrawable_at(current_epoch), current_epoch_effective_balance: effective_balance, ..ValidatorStatus::default() }; - if validator.is_active_at(state.current_epoch()) { + if validator.is_active_at(current_epoch) { status.is_active_in_current_epoch = true; total_balances .current_epoch .safe_add_assign(effective_balance)?; } - if validator.is_active_at(state.previous_epoch()) { + if validator.is_active_at(previous_epoch) { status.is_active_in_previous_epoch = true; total_balances .previous_epoch @@ -232,9 +238,9 @@ impl ValidatorStatuses { /// `total_balances` fields. /// /// Spec v0.12.1 - pub fn process_attestations( + pub fn process_attestations( &mut self, - state: &BeaconState, + state: &BeaconState, ) -> Result<(), BeaconStateError> { let base_state = state.as_base()?; for a in base_state @@ -244,7 +250,7 @@ impl ValidatorStatuses { { let committee = state.get_beacon_committee(a.data.slot, a.data.index)?; let attesting_indices = - get_attesting_indices::(committee.committee, &a.aggregation_bits)?; + get_attesting_indices::(committee.committee, &a.aggregation_bits)?; let mut status = ValidatorStatus::default(); @@ -285,10 +291,10 @@ impl ValidatorStatuses { } // Compute the total balances - for (index, v) in self.statuses.iter().enumerate() { + for v in self.statuses.iter() { // According to the spec, we only count unslashed validators towards the totals. if !v.is_slashed { - let validator_balance = state.get_effective_balance(index)?; + let validator_balance = v.current_epoch_effective_balance; if v.is_current_epoch_attester { self.total_balances @@ -326,12 +332,12 @@ impl ValidatorStatuses { /// beacon block in the given `epoch`. /// /// Spec v0.12.1 -fn target_matches_epoch_start_block( - a: &PendingAttestation, - state: &BeaconState, +fn target_matches_epoch_start_block( + a: &PendingAttestation, + state: &BeaconState, epoch: Epoch, ) -> Result { - let slot = epoch.start_slot(T::slots_per_epoch()); + let slot = epoch.start_slot(E::slots_per_epoch()); let state_boundary_root = *state.get_block_root(slot)?; Ok(a.data.target.root == state_boundary_root) @@ -341,9 +347,9 @@ fn target_matches_epoch_start_block( /// the current slot of the `PendingAttestation`. /// /// Spec v0.12.1 -fn has_common_beacon_block_root( - a: &PendingAttestation, - state: &BeaconState, +fn has_common_beacon_block_root( + a: &PendingAttestation, + state: &BeaconState, ) -> Result { let state_block_root = *state.get_block_root(a.data.slot)?; diff --git a/consensus/state_processing/src/per_epoch_processing/capella.rs b/consensus/state_processing/src/per_epoch_processing/capella.rs index 911510ed0c..161bce5423 100644 --- a/consensus/state_processing/src/per_epoch_processing/capella.rs +++ b/consensus/state_processing/src/per_epoch_processing/capella.rs @@ -1,84 +1,3 @@ -use super::altair::inactivity_updates::process_inactivity_updates; -use super::altair::justification_and_finalization::process_justification_and_finalization; -use super::altair::participation_cache::ParticipationCache; -use super::altair::participation_flag_updates::process_participation_flag_updates; -use super::altair::rewards_and_penalties::process_rewards_and_penalties; -use super::altair::sync_committee_updates::process_sync_committee_updates; -use super::{process_registry_updates, process_slashings, EpochProcessingSummary, Error}; -use crate::per_epoch_processing::{ - effective_balance_updates::process_effective_balance_updates, - resets::{process_eth1_data_reset, process_randao_mixes_reset, process_slashings_reset}, -}; -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; - -pub fn process_epoch( - state: &mut BeaconState, - spec: &ChainSpec, -) -> Result, Error> { - // Ensure the committee caches are built. - state.build_committee_cache(RelativeEpoch::Previous, spec)?; - state.build_committee_cache(RelativeEpoch::Current, spec)?; - state.build_committee_cache(RelativeEpoch::Next, spec)?; - - // 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 = - process_justification_and_finalization(state, &participation_cache)?; - justification_and_finalization_state.apply_changes_to_state(state); - - process_inactivity_updates(state, &participation_cache, spec)?; - - // Rewards and Penalties. - process_rewards_and_penalties(state, &participation_cache, spec)?; - - // Registry Updates. - process_registry_updates(state, spec)?; - - // Slashings. - process_slashings( - state, - participation_cache.current_epoch_total_active_balance(), - spec, - )?; - - // Reset eth1 data votes. - process_eth1_data_reset(state)?; - - // Update effective balances with hysteresis (lag). - process_effective_balance_updates(state, Some(&participation_cache), spec)?; - - // Reset slashings - process_slashings_reset(state)?; - - // Set randao mix - process_randao_mixes_reset(state)?; - - // Set historical summaries accumulator - process_historical_summaries_update(state)?; - - // Rotate current/previous epoch participation - process_participation_flag_updates(state)?; - - process_sync_committee_updates(state, spec)?; - - // 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/capella/historical_summaries_update.rs b/consensus/state_processing/src/per_epoch_processing/capella/historical_summaries_update.rs index 9a87ceb605..7490f27656 100644 --- a/consensus/state_processing/src/per_epoch_processing/capella/historical_summaries_update.rs +++ b/consensus/state_processing/src/per_epoch_processing/capella/historical_summaries_update.rs @@ -3,14 +3,14 @@ use safe_arith::SafeArith; use types::historical_summary::HistoricalSummary; use types::{BeaconState, EthSpec}; -pub fn process_historical_summaries_update( - state: &mut BeaconState, +pub fn process_historical_summaries_update( + state: &mut BeaconState, ) -> Result<(), EpochProcessingError> { // Set historical block root accumulator. let next_epoch = state.next_epoch()?; if next_epoch .as_u64() - .safe_rem((T::slots_per_historical_root() as u64).safe_div(T::slots_per_epoch())?)? + .safe_rem((E::slots_per_historical_root() as u64).safe_div(E::slots_per_epoch())?)? == 0 { let summary = HistoricalSummary::new(state); 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 1759f7e140..7bd62c4081 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,68 +1,70 @@ use super::errors::EpochProcessingError; -use crate::per_epoch_processing::altair::ParticipationCache; +use crate::per_epoch_processing::single_pass::{process_epoch_single_pass, SinglePassConfig}; use safe_arith::SafeArith; use types::beacon_state::BeaconState; use types::chain_spec::ChainSpec; -use types::{BeaconStateError, EthSpec, ProgressiveBalancesCache}; +use types::{BeaconStateError, EthSpec}; -pub fn process_effective_balance_updates( - state: &mut BeaconState, - maybe_participation_cache: Option<&ParticipationCache>, +/// This implementation is now only used in phase0. Later hard forks use single-pass. +pub fn process_effective_balance_updates( + state: &mut BeaconState, spec: &ChainSpec, ) -> Result<(), EpochProcessingError> { + // Compute new total active balance for the next epoch as a side-effect of iterating the + // effective balances. + let next_epoch = state.next_epoch()?; + let mut new_total_active_balance = 0; + let hysteresis_increment = spec .effective_balance_increment .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, progressive_balances_cache) = - state.validators_and_balances_and_progressive_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) .copied() .ok_or(BeaconStateError::BalancesOutOfBounds(index))?; - if balance.safe_add(downward_threshold)? < validator.effective_balance + let new_effective_balance = if balance.safe_add(downward_threshold)? + < validator.effective_balance || validator.effective_balance.safe_add(upward_threshold)? < balance { - let old_effective_balance = validator.effective_balance; - let new_effective_balance = std::cmp::min( + std::cmp::min( balance.safe_sub(balance.safe_rem(spec.effective_balance_increment)?)?, spec.max_effective_balance, - ); + ) + } else { + validator.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, - )?; - } + if validator.is_active_at(next_epoch) { + new_total_active_balance.safe_add_assign(new_effective_balance)?; + } + if new_effective_balance != validator.effective_balance { validator.effective_balance = new_effective_balance; } } + + state.set_total_active_balance(next_epoch, new_total_active_balance, spec); + Ok(()) } -fn update_progressive_balances( - participation_cache: &ParticipationCache, - progressive_balances_cache: &mut ProgressiveBalancesCache, - index: usize, - old_effective_balance: u64, - new_effective_balance: u64, +/// Only used to test the effective balance part of single-pass in isolation. +pub fn process_effective_balance_updates_slow( + state: &mut BeaconState, + spec: &ChainSpec, ) -> 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, - )?; - } + process_epoch_single_pass( + state, + spec, + SinglePassConfig { + effective_balance_updates: true, + ..SinglePassConfig::disable_all() + }, + )?; 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 89bc4ab5a3..65a946e7bf 100644 --- a/consensus/state_processing/src/per_epoch_processing/epoch_processing_summary.rs +++ b/consensus/state_processing/src/per_epoch_processing/epoch_processing_summary.rs @@ -1,27 +1,93 @@ -use super::{ - altair::{participation_cache::Error as ParticipationCacheError, ParticipationCache}, - base::{validator_statuses::InclusionInfo, TotalBalances, ValidatorStatus}, -}; +use super::base::{validator_statuses::InclusionInfo, TotalBalances, ValidatorStatus}; use crate::metrics; use std::sync::Arc; -use types::{EthSpec, SyncCommittee}; +use types::{ + consts::altair::{TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX}, + BeaconStateError, Epoch, EthSpec, ParticipationFlags, ProgressiveBalancesCache, SyncCommittee, + Validator, VariableList, +}; /// Provides a summary of validator participation during the epoch. #[derive(PartialEq, Debug)] -pub enum EpochProcessingSummary { +pub enum EpochProcessingSummary { Base { total_balances: TotalBalances, statuses: Vec, }, Altair { - participation_cache: ParticipationCache, - sync_committee: Arc>, + progressive_balances: ProgressiveBalancesCache, + current_epoch_total_active_balance: u64, + participation: ParticipationEpochSummary, + sync_committee: Arc>, }, } -impl EpochProcessingSummary { +#[derive(PartialEq, Debug)] +pub struct ParticipationEpochSummary { + /// Copy of the validator registry prior to mutation. + validators: VariableList, + /// Copy of the participation flags for the previous epoch. + previous_epoch_participation: VariableList, + /// Copy of the participation flags for the current epoch. + current_epoch_participation: VariableList, + previous_epoch: Epoch, + current_epoch: Epoch, +} + +impl ParticipationEpochSummary { + pub fn new( + validators: VariableList, + previous_epoch_participation: VariableList, + current_epoch_participation: VariableList, + previous_epoch: Epoch, + current_epoch: Epoch, + ) -> Self { + Self { + validators, + previous_epoch_participation, + current_epoch_participation, + previous_epoch, + current_epoch, + } + } + + pub fn is_active_and_unslashed(&self, val_index: usize, epoch: Epoch) -> bool { + self.validators + .get(val_index) + .map(|validator| !validator.slashed && validator.is_active_at(epoch)) + .unwrap_or(false) + } + + pub fn is_previous_epoch_unslashed_participating_index( + &self, + val_index: usize, + flag_index: usize, + ) -> Result { + Ok(self.is_active_and_unslashed(val_index, self.previous_epoch) + && self + .previous_epoch_participation + .get(val_index) + .ok_or(BeaconStateError::UnknownValidator(val_index))? + .has_flag(flag_index)?) + } + + pub fn is_current_epoch_unslashed_participating_index( + &self, + val_index: usize, + flag_index: usize, + ) -> Result { + Ok(self.is_active_and_unslashed(val_index, self.current_epoch) + && self + .current_epoch_participation + .get(val_index) + .ok_or(BeaconStateError::UnknownValidator(val_index))? + .has_flag(flag_index)?) + } +} + +impl EpochProcessingSummary { /// Updates some Prometheus metrics with some values in `self`. - pub fn observe_metrics(&self) -> Result<(), ParticipationCacheError> { + pub fn observe_metrics(&self) -> Result<(), BeaconStateError> { metrics::set_gauge( &metrics::PARTICIPATION_PREV_EPOCH_HEAD_ATTESTING_GWEI_TOTAL, self.previous_epoch_head_attesting_balance()? as i64, @@ -34,16 +100,12 @@ impl EpochProcessingSummary { &metrics::PARTICIPATION_PREV_EPOCH_SOURCE_ATTESTING_GWEI_TOTAL, self.previous_epoch_source_attesting_balance()? as i64, ); - metrics::set_gauge( - &metrics::PARTICIPATION_PREV_EPOCH_ACTIVE_GWEI_TOTAL, - self.previous_epoch_total_active_balance() as i64, - ); Ok(()) } /// Returns the sync committee indices for the current epoch for altair. - pub fn sync_committee(&self) -> Option<&SyncCommittee> { + pub fn sync_committee(&self) -> Option<&SyncCommittee> { match self { EpochProcessingSummary::Altair { sync_committee, .. } => Some(sync_committee), EpochProcessingSummary::Base { .. } => None, @@ -55,34 +117,23 @@ impl EpochProcessingSummary { match self { EpochProcessingSummary::Base { total_balances, .. } => total_balances.current_epoch(), EpochProcessingSummary::Altair { - participation_cache, + current_epoch_total_active_balance, .. - } => participation_cache.current_epoch_total_active_balance(), + } => *current_epoch_total_active_balance, } } /// Returns the sum of the effective balance of all validators in the current epoch who /// included an attestation that matched the target. - pub fn current_epoch_target_attesting_balance(&self) -> Result { + pub fn current_epoch_target_attesting_balance(&self) -> Result { match self { EpochProcessingSummary::Base { total_balances, .. } => { Ok(total_balances.current_epoch_target_attesters()) } EpochProcessingSummary::Altair { - participation_cache, + progressive_balances, .. - } => participation_cache.current_epoch_target_attesting_balance(), - } - } - - /// Returns the sum of the effective balance of all validators in the previous epoch. - pub fn previous_epoch_total_active_balance(&self) -> u64 { - match self { - EpochProcessingSummary::Base { total_balances, .. } => total_balances.previous_epoch(), - EpochProcessingSummary::Altair { - participation_cache, - .. - } => participation_cache.previous_epoch_total_active_balance(), + } => progressive_balances.current_epoch_target_attesting_balance(), } } @@ -97,12 +148,9 @@ impl EpochProcessingSummary { EpochProcessingSummary::Base { statuses, .. } => statuses .get(val_index) .map_or(false, |s| s.is_active_in_current_epoch && !s.is_slashed), - EpochProcessingSummary::Altair { - participation_cache, - .. - } => participation_cache - .is_active_unslashed_in_current_epoch(val_index) - .unwrap_or(false), + EpochProcessingSummary::Altair { participation, .. } => { + participation.is_active_and_unslashed(val_index, participation.current_epoch) + } } } @@ -120,34 +168,30 @@ impl EpochProcessingSummary { pub fn is_current_epoch_target_attester( &self, val_index: usize, - ) -> Result { + ) -> Result { match self { EpochProcessingSummary::Base { statuses, .. } => Ok(statuses .get(val_index) .map_or(false, |s| s.is_current_epoch_target_attester)), - EpochProcessingSummary::Altair { - participation_cache, - .. - } => participation_cache - .is_current_epoch_timely_target_attester(val_index) - .or_else(|e| match e { - ParticipationCacheError::InvalidValidatorIndex(_) => Ok(false), - e => Err(e), - }), + EpochProcessingSummary::Altair { participation, .. } => participation + .is_current_epoch_unslashed_participating_index( + val_index, + TIMELY_TARGET_FLAG_INDEX, + ), } } /// Returns the sum of the effective balance of all validators in the previous epoch who /// included an attestation that matched the target. - pub fn previous_epoch_target_attesting_balance(&self) -> Result { + pub fn previous_epoch_target_attesting_balance(&self) -> Result { match self { EpochProcessingSummary::Base { total_balances, .. } => { Ok(total_balances.previous_epoch_target_attesters()) } EpochProcessingSummary::Altair { - participation_cache, + progressive_balances, .. - } => participation_cache.previous_epoch_target_attesting_balance(), + } => progressive_balances.previous_epoch_target_attesting_balance(), } } @@ -158,15 +202,15 @@ impl EpochProcessingSummary { /// /// - Base: any attestation can match the head. /// - Altair: only "timely" attestations can match the head. - pub fn previous_epoch_head_attesting_balance(&self) -> Result { + pub fn previous_epoch_head_attesting_balance(&self) -> Result { match self { EpochProcessingSummary::Base { total_balances, .. } => { Ok(total_balances.previous_epoch_head_attesters()) } EpochProcessingSummary::Altair { - participation_cache, + progressive_balances, .. - } => participation_cache.previous_epoch_head_attesting_balance(), + } => progressive_balances.previous_epoch_head_attesting_balance(), } } @@ -177,15 +221,15 @@ impl EpochProcessingSummary { /// /// - Base: any attestation can match the source. /// - Altair: only "timely" attestations can match the source. - pub fn previous_epoch_source_attesting_balance(&self) -> Result { + pub fn previous_epoch_source_attesting_balance(&self) -> Result { match self { EpochProcessingSummary::Base { total_balances, .. } => { Ok(total_balances.previous_epoch_attesters()) } EpochProcessingSummary::Altair { - participation_cache, + progressive_balances, .. - } => participation_cache.previous_epoch_source_attesting_balance(), + } => progressive_balances.previous_epoch_source_attesting_balance(), } } @@ -200,12 +244,9 @@ impl EpochProcessingSummary { EpochProcessingSummary::Base { statuses, .. } => statuses .get(val_index) .map_or(false, |s| s.is_active_in_previous_epoch && !s.is_slashed), - EpochProcessingSummary::Altair { - participation_cache, - .. - } => participation_cache - .is_active_unslashed_in_previous_epoch(val_index) - .unwrap_or(false), + EpochProcessingSummary::Altair { participation, .. } => { + participation.is_active_and_unslashed(val_index, participation.previous_epoch) + } } } @@ -218,20 +259,16 @@ impl EpochProcessingSummary { pub fn is_previous_epoch_target_attester( &self, val_index: usize, - ) -> Result { + ) -> Result { match self { EpochProcessingSummary::Base { statuses, .. } => Ok(statuses .get(val_index) .map_or(false, |s| s.is_previous_epoch_target_attester)), - EpochProcessingSummary::Altair { - participation_cache, - .. - } => participation_cache - .is_previous_epoch_timely_target_attester(val_index) - .or_else(|e| match e { - ParticipationCacheError::InvalidValidatorIndex(_) => Ok(false), - e => Err(e), - }), + EpochProcessingSummary::Altair { participation, .. } => participation + .is_previous_epoch_unslashed_participating_index( + val_index, + TIMELY_TARGET_FLAG_INDEX, + ), } } @@ -249,20 +286,13 @@ impl EpochProcessingSummary { pub fn is_previous_epoch_head_attester( &self, val_index: usize, - ) -> Result { + ) -> Result { match self { EpochProcessingSummary::Base { statuses, .. } => Ok(statuses .get(val_index) .map_or(false, |s| s.is_previous_epoch_head_attester)), - EpochProcessingSummary::Altair { - participation_cache, - .. - } => participation_cache - .is_previous_epoch_timely_head_attester(val_index) - .or_else(|e| match e { - ParticipationCacheError::InvalidValidatorIndex(_) => Ok(false), - e => Err(e), - }), + EpochProcessingSummary::Altair { participation, .. } => participation + .is_previous_epoch_unslashed_participating_index(val_index, TIMELY_HEAD_FLAG_INDEX), } } @@ -280,20 +310,16 @@ impl EpochProcessingSummary { pub fn is_previous_epoch_source_attester( &self, val_index: usize, - ) -> Result { + ) -> Result { match self { EpochProcessingSummary::Base { statuses, .. } => Ok(statuses .get(val_index) .map_or(false, |s| s.is_previous_epoch_attester)), - EpochProcessingSummary::Altair { - participation_cache, - .. - } => participation_cache - .is_previous_epoch_timely_source_attester(val_index) - .or_else(|e| match e { - ParticipationCacheError::InvalidValidatorIndex(_) => Ok(false), - e => Err(e), - }), + EpochProcessingSummary::Altair { participation, .. } => participation + .is_previous_epoch_unslashed_participating_index( + val_index, + TIMELY_SOURCE_FLAG_INDEX, + ), } } diff --git a/consensus/state_processing/src/per_epoch_processing/errors.rs b/consensus/state_processing/src/per_epoch_processing/errors.rs index 04797c5634..c18e1303b2 100644 --- a/consensus/state_processing/src/per_epoch_processing/errors.rs +++ b/consensus/state_processing/src/per_epoch_processing/errors.rs @@ -1,5 +1,4 @@ -use crate::per_epoch_processing::altair::participation_cache::Error as ParticipationCacheError; -use types::{BeaconStateError, InconsistentFork}; +use types::{BeaconStateError, EpochCacheError, InconsistentFork}; #[derive(Debug, PartialEq)] pub enum EpochProcessingError { @@ -24,7 +23,7 @@ pub enum EpochProcessingError { InconsistentStateFork(InconsistentFork), InvalidJustificationBit(ssz_types::Error), InvalidFlagIndex(usize), - ParticipationCache(ParticipationCacheError), + EpochCache(EpochCacheError), } impl From for EpochProcessingError { @@ -51,9 +50,9 @@ impl From for EpochProcessingError { } } -impl From for EpochProcessingError { - fn from(e: ParticipationCacheError) -> EpochProcessingError { - EpochProcessingError::ParticipationCache(e) +impl From for EpochProcessingError { + fn from(e: EpochCacheError) -> Self { + EpochProcessingError::EpochCache(e) } } diff --git a/consensus/state_processing/src/per_epoch_processing/historical_roots_update.rs b/consensus/state_processing/src/per_epoch_processing/historical_roots_update.rs index 8466104aa5..6d06b4d7ca 100644 --- a/consensus/state_processing/src/per_epoch_processing/historical_roots_update.rs +++ b/consensus/state_processing/src/per_epoch_processing/historical_roots_update.rs @@ -1,19 +1,17 @@ use super::errors::EpochProcessingError; -use core::result::Result; -use core::result::Result::Ok; use safe_arith::SafeArith; use tree_hash::TreeHash; use types::beacon_state::BeaconState; use types::eth_spec::EthSpec; use types::Unsigned; -pub fn process_historical_roots_update( - state: &mut BeaconState, +pub fn process_historical_roots_update( + state: &mut BeaconState, ) -> Result<(), EpochProcessingError> { let next_epoch = state.next_epoch()?; if next_epoch .as_u64() - .safe_rem(T::SlotsPerHistoricalRoot::to_u64().safe_div(T::slots_per_epoch())?)? + .safe_rem(E::SlotsPerHistoricalRoot::to_u64().safe_div(E::slots_per_epoch())?)? == 0 { let historical_batch = state.historical_batch(); diff --git a/consensus/state_processing/src/per_epoch_processing/justification_and_finalization_state.rs b/consensus/state_processing/src/per_epoch_processing/justification_and_finalization_state.rs index d8a641f464..66d68804e1 100644 --- a/consensus/state_processing/src/per_epoch_processing/justification_and_finalization_state.rs +++ b/consensus/state_processing/src/per_epoch_processing/justification_and_finalization_state.rs @@ -6,7 +6,7 @@ use types::{BeaconState, BeaconStateError, BitVector, Checkpoint, Epoch, EthSpec /// A `JustificationAndFinalizationState` can be created from a `BeaconState` to compute /// justification/finality changes and then applied to a `BeaconState` to enshrine those changes. #[must_use = "this value must be applied to a state or explicitly dropped"] -pub struct JustificationAndFinalizationState { +pub struct JustificationAndFinalizationState { /* * Immutable fields. */ @@ -20,11 +20,11 @@ pub struct JustificationAndFinalizationState { previous_justified_checkpoint: Checkpoint, current_justified_checkpoint: Checkpoint, finalized_checkpoint: Checkpoint, - justification_bits: BitVector, + justification_bits: BitVector, } -impl JustificationAndFinalizationState { - pub fn new(state: &BeaconState) -> Self { +impl JustificationAndFinalizationState { + pub fn new(state: &BeaconState) -> Self { let previous_epoch = state.previous_epoch(); let current_epoch = state.current_epoch(); Self { @@ -39,7 +39,7 @@ impl JustificationAndFinalizationState { } } - pub fn apply_changes_to_state(self, state: &mut BeaconState) { + pub fn apply_changes_to_state(self, state: &mut BeaconState) { let Self { /* * Immutable fields do not need to be used. @@ -105,11 +105,11 @@ impl JustificationAndFinalizationState { &mut self.finalized_checkpoint } - pub fn justification_bits(&self) -> &BitVector { + pub fn justification_bits(&self) -> &BitVector { &self.justification_bits } - pub fn justification_bits_mut(&mut self) -> &mut BitVector { + pub fn justification_bits_mut(&mut self) -> &mut BitVector { &mut self.justification_bits } } diff --git a/consensus/state_processing/src/per_epoch_processing/registry_updates.rs b/consensus/state_processing/src/per_epoch_processing/registry_updates.rs index 833be41387..4b2f940e5f 100644 --- a/consensus/state_processing/src/per_epoch_processing/registry_updates.rs +++ b/consensus/state_processing/src/per_epoch_processing/registry_updates.rs @@ -1,13 +1,13 @@ +use crate::per_epoch_processing::single_pass::{process_epoch_single_pass, SinglePassConfig}; use crate::{common::initiate_validator_exit, per_epoch_processing::Error}; -use itertools::Itertools; use safe_arith::SafeArith; use types::{BeaconState, ChainSpec, EthSpec, Validator}; /// Performs a validator registry update, if required. /// /// NOTE: unchanged in Altair -pub fn process_registry_updates( - state: &mut BeaconState, +pub fn process_registry_updates( + state: &mut BeaconState, spec: &ChainSpec, ) -> Result<(), Error> { // Process activation eligibility and ejections. @@ -40,21 +40,33 @@ pub fn process_registry_updates( } // Queue validators eligible for activation and not dequeued for activation prior to finalized epoch - let activation_queue = state - .validators() - .iter() - .enumerate() - .filter(|(_, validator)| validator.is_eligible_for_activation(state, spec)) - .sorted_by_key(|(index, validator)| (validator.activation_eligibility_epoch, *index)) - .map(|(index, _)| index) - .collect_vec(); - // Dequeue validators for activation up to churn limit - let activation_churn_limit = state.get_activation_churn_limit(spec)? as usize; + let churn_limit = state.get_activation_churn_limit(spec)? as usize; + + let epoch_cache = state.epoch_cache(); + let activation_queue = epoch_cache + .activation_queue()? + .get_validators_eligible_for_activation(state.finalized_checkpoint().epoch, churn_limit); + let delayed_activation_epoch = state.compute_activation_exit_epoch(current_epoch, spec)?; - for index in activation_queue.into_iter().take(activation_churn_limit) { + for index in activation_queue { state.get_validator_mut(index)?.activation_epoch = delayed_activation_epoch; } Ok(()) } + +pub fn process_registry_updates_slow( + state: &mut BeaconState, + spec: &ChainSpec, +) -> Result<(), Error> { + process_epoch_single_pass( + state, + spec, + SinglePassConfig { + registry_updates: true, + ..SinglePassConfig::disable_all() + }, + )?; + Ok(()) +} diff --git a/consensus/state_processing/src/per_epoch_processing/resets.rs b/consensus/state_processing/src/per_epoch_processing/resets.rs index dc3c9f07c0..d577c52e6a 100644 --- a/consensus/state_processing/src/per_epoch_processing/resets.rs +++ b/consensus/state_processing/src/per_epoch_processing/resets.rs @@ -1,18 +1,16 @@ use super::errors::EpochProcessingError; -use core::result::Result; -use core::result::Result::Ok; use safe_arith::SafeArith; use types::beacon_state::BeaconState; use types::eth_spec::EthSpec; use types::{Unsigned, VariableList}; -pub fn process_eth1_data_reset( - state: &mut BeaconState, +pub fn process_eth1_data_reset( + state: &mut BeaconState, ) -> Result<(), EpochProcessingError> { if state .slot() .safe_add(1)? - .safe_rem(T::SlotsPerEth1VotingPeriod::to_u64())? + .safe_rem(E::SlotsPerEth1VotingPeriod::to_u64())? == 0 { *state.eth1_data_votes_mut() = VariableList::empty(); @@ -20,16 +18,16 @@ pub fn process_eth1_data_reset( Ok(()) } -pub fn process_slashings_reset( - state: &mut BeaconState, +pub fn process_slashings_reset( + state: &mut BeaconState, ) -> Result<(), EpochProcessingError> { let next_epoch = state.next_epoch()?; state.set_slashings(next_epoch, 0)?; Ok(()) } -pub fn process_randao_mixes_reset( - state: &mut BeaconState, +pub fn process_randao_mixes_reset( + state: &mut BeaconState, ) -> Result<(), EpochProcessingError> { let current_epoch = state.current_epoch(); let next_epoch = state.next_epoch()?; diff --git a/consensus/state_processing/src/per_epoch_processing/single_pass.rs b/consensus/state_processing/src/per_epoch_processing/single_pass.rs new file mode 100644 index 0000000000..9319d2941b --- /dev/null +++ b/consensus/state_processing/src/per_epoch_processing/single_pass.rs @@ -0,0 +1,630 @@ +use crate::{ + common::update_progressive_balances_cache::initialize_progressive_balances_cache, + epoch_cache::{initialize_epoch_cache, PreEpochCache}, + per_epoch_processing::{Delta, Error, ParticipationEpochSummary}, +}; +use itertools::izip; +use safe_arith::{SafeArith, SafeArithIter}; +use std::cmp::{max, min}; +use std::collections::BTreeSet; +use types::{ + consts::altair::{ + NUM_FLAG_INDICES, PARTICIPATION_FLAG_WEIGHTS, TIMELY_HEAD_FLAG_INDEX, + TIMELY_TARGET_FLAG_INDEX, WEIGHT_DENOMINATOR, + }, + ActivationQueue, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, ExitCache, ForkName, + ParticipationFlags, ProgressiveBalancesCache, RelativeEpoch, Unsigned, Validator, +}; + +pub struct SinglePassConfig { + pub inactivity_updates: bool, + pub rewards_and_penalties: bool, + pub registry_updates: bool, + pub slashings: bool, + pub effective_balance_updates: bool, +} + +impl Default for SinglePassConfig { + fn default() -> SinglePassConfig { + Self::enable_all() + } +} + +impl SinglePassConfig { + pub fn enable_all() -> SinglePassConfig { + Self { + inactivity_updates: true, + rewards_and_penalties: true, + registry_updates: true, + slashings: true, + effective_balance_updates: true, + } + } + + pub fn disable_all() -> SinglePassConfig { + SinglePassConfig { + inactivity_updates: false, + rewards_and_penalties: false, + registry_updates: false, + slashings: false, + effective_balance_updates: false, + } + } +} + +/// Values from the state that are immutable throughout epoch processing. +struct StateContext { + current_epoch: Epoch, + next_epoch: Epoch, + is_in_inactivity_leak: bool, + total_active_balance: u64, + churn_limit: u64, + fork_name: ForkName, +} + +struct RewardsAndPenaltiesContext { + unslashed_participating_increments_array: [u64; NUM_FLAG_INDICES], + active_increments: u64, +} + +struct SlashingsContext { + adjusted_total_slashing_balance: u64, + target_withdrawable_epoch: Epoch, +} + +struct EffectiveBalancesContext { + downward_threshold: u64, + upward_threshold: u64, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct ValidatorInfo { + pub index: usize, + pub effective_balance: u64, + pub base_reward: u64, + pub is_eligible: bool, + pub is_slashed: bool, + pub is_active_current_epoch: bool, + pub is_active_previous_epoch: bool, + // Used for determining rewards. + pub previous_epoch_participation: ParticipationFlags, + // Used for updating the progressive balances cache for next epoch. + pub current_epoch_participation: ParticipationFlags, +} + +impl ValidatorInfo { + #[inline] + pub fn is_unslashed_participating_index(&self, flag_index: usize) -> Result { + Ok(self.is_active_previous_epoch + && !self.is_slashed + && self + .previous_epoch_participation + .has_flag(flag_index) + .map_err(|_| Error::InvalidFlagIndex(flag_index))?) + } +} + +pub fn process_epoch_single_pass( + state: &mut BeaconState, + spec: &ChainSpec, + conf: SinglePassConfig, +) -> Result, Error> { + initialize_epoch_cache(state, spec)?; + initialize_progressive_balances_cache(state, spec)?; + state.build_exit_cache(spec)?; + state.build_committee_cache(RelativeEpoch::Previous, spec)?; + state.build_committee_cache(RelativeEpoch::Current, spec)?; + + let previous_epoch = state.previous_epoch(); + let current_epoch = state.current_epoch(); + let next_epoch = state.next_epoch()?; + let is_in_inactivity_leak = state.is_in_inactivity_leak(previous_epoch, spec)?; + let total_active_balance = state.get_total_active_balance()?; + let churn_limit = state.get_churn_limit(spec)?; + let activation_churn_limit = state.get_activation_churn_limit(spec)?; + let finalized_checkpoint = state.finalized_checkpoint(); + let fork_name = state.fork_name_unchecked(); + + let state_ctxt = &StateContext { + current_epoch, + next_epoch, + is_in_inactivity_leak, + total_active_balance, + churn_limit, + fork_name, + }; + + // Contexts that require immutable access to `state`. + let slashings_ctxt = &SlashingsContext::new(state, state_ctxt, spec)?; + let mut next_epoch_cache = PreEpochCache::new_for_next_epoch(state)?; + + // Split the state into several disjoint mutable borrows. + let ( + validators, + balances, + previous_epoch_participation, + current_epoch_participation, + inactivity_scores, + progressive_balances, + exit_cache, + epoch_cache, + ) = state.mutable_validator_fields()?; + + let num_validators = validators.len(); + + // Take a snapshot of the validators and participation before mutating. This is used for + // informational purposes (e.g. by the validator monitor). + let summary = ParticipationEpochSummary::new( + validators.clone(), + previous_epoch_participation.clone(), + current_epoch_participation.clone(), + previous_epoch, + current_epoch, + ); + + // Compute shared values required for different parts of epoch processing. + let rewards_ctxt = &RewardsAndPenaltiesContext::new(progressive_balances, state_ctxt, spec)?; + let activation_queue = &epoch_cache + .activation_queue()? + .get_validators_eligible_for_activation( + finalized_checkpoint.epoch, + activation_churn_limit as usize, + ); + let effective_balances_ctxt = &EffectiveBalancesContext::new(spec)?; + + // Iterate over the validators and related fields in one pass. + let mut validators_iter = validators.iter_mut(); + let mut balances_iter = balances.iter_mut(); + let mut inactivity_scores_iter = inactivity_scores.iter_mut(); + + // Values computed for the next epoch transition. + let mut next_epoch_total_active_balance = 0; + let mut next_epoch_activation_queue = ActivationQueue::default(); + + for (index, &previous_epoch_participation, ¤t_epoch_participation) in izip!( + 0..num_validators, + previous_epoch_participation.iter(), + current_epoch_participation.iter(), + ) { + let validator = validators_iter + .next() + .ok_or(BeaconStateError::UnknownValidator(index))?; + let balance = balances_iter + .next() + .ok_or(BeaconStateError::UnknownValidator(index))?; + let inactivity_score = inactivity_scores_iter + .next() + .ok_or(BeaconStateError::UnknownValidator(index))?; + + let is_active_current_epoch = validator.is_active_at(current_epoch); + let is_active_previous_epoch = validator.is_active_at(previous_epoch); + let is_eligible = is_active_previous_epoch + || (validator.slashed && previous_epoch.safe_add(1)? < validator.withdrawable_epoch); + + let base_reward = if is_eligible { + epoch_cache.get_base_reward(index)? + } else { + 0 + }; + + let validator_info = &ValidatorInfo { + index, + effective_balance: validator.effective_balance, + base_reward, + is_eligible, + is_slashed: validator.slashed, + is_active_current_epoch, + is_active_previous_epoch, + previous_epoch_participation, + current_epoch_participation, + }; + + if current_epoch != E::genesis_epoch() { + // `process_inactivity_updates` + if conf.inactivity_updates { + process_single_inactivity_update( + inactivity_score, + validator_info, + state_ctxt, + spec, + )?; + } + + // `process_rewards_and_penalties` + if conf.rewards_and_penalties { + process_single_reward_and_penalty( + balance, + inactivity_score, + validator_info, + rewards_ctxt, + state_ctxt, + spec, + )?; + } + } + + // `process_registry_updates` + if conf.registry_updates { + process_single_registry_update( + validator, + validator_info, + exit_cache, + activation_queue, + &mut next_epoch_activation_queue, + state_ctxt, + spec, + )?; + } + + // `process_slashings` + if conf.slashings { + process_single_slashing(balance, validator, slashings_ctxt, state_ctxt, spec)?; + } + + // `process_effective_balance_updates` + if conf.effective_balance_updates { + process_single_effective_balance_update( + *balance, + validator, + validator_info, + &mut next_epoch_total_active_balance, + &mut next_epoch_cache, + progressive_balances, + effective_balances_ctxt, + state_ctxt, + spec, + )?; + } + } + + if conf.effective_balance_updates { + state.set_total_active_balance(next_epoch, next_epoch_total_active_balance, spec); + *state.epoch_cache_mut() = next_epoch_cache.into_epoch_cache( + next_epoch_total_active_balance, + next_epoch_activation_queue, + spec, + )?; + } + + Ok(summary) +} + +fn process_single_inactivity_update( + inactivity_score: &mut u64, + validator_info: &ValidatorInfo, + state_ctxt: &StateContext, + spec: &ChainSpec, +) -> Result<(), Error> { + if !validator_info.is_eligible { + return Ok(()); + } + + // Increase inactivity score of inactive validators + if validator_info.is_unslashed_participating_index(TIMELY_TARGET_FLAG_INDEX)? { + // Avoid mutating when the inactivity score is 0 and can't go any lower -- the common + // case. + if *inactivity_score == 0 { + return Ok(()); + } + inactivity_score.safe_sub_assign(1)?; + } else { + inactivity_score.safe_add_assign(spec.inactivity_score_bias)?; + } + + // Decrease the score of all validators for forgiveness when not during a leak + if !state_ctxt.is_in_inactivity_leak { + let deduction = min(spec.inactivity_score_recovery_rate, *inactivity_score); + inactivity_score.safe_sub_assign(deduction)?; + } + + Ok(()) +} + +fn process_single_reward_and_penalty( + balance: &mut u64, + inactivity_score: &u64, + validator_info: &ValidatorInfo, + rewards_ctxt: &RewardsAndPenaltiesContext, + state_ctxt: &StateContext, + spec: &ChainSpec, +) -> Result<(), Error> { + if !validator_info.is_eligible { + return Ok(()); + } + + let mut delta = Delta::default(); + for flag_index in 0..NUM_FLAG_INDICES { + get_flag_index_delta( + &mut delta, + validator_info, + flag_index, + rewards_ctxt, + state_ctxt, + )?; + } + get_inactivity_penalty_delta( + &mut delta, + validator_info, + inactivity_score, + state_ctxt, + spec, + )?; + + if delta.rewards != 0 || delta.penalties != 0 { + balance.safe_add_assign(delta.rewards)?; + *balance = balance.saturating_sub(delta.penalties); + } + + Ok(()) +} + +fn get_flag_index_delta( + delta: &mut Delta, + validator_info: &ValidatorInfo, + flag_index: usize, + rewards_ctxt: &RewardsAndPenaltiesContext, + state_ctxt: &StateContext, +) -> Result<(), Error> { + let base_reward = validator_info.base_reward; + let weight = get_flag_weight(flag_index)?; + let unslashed_participating_increments = + rewards_ctxt.get_unslashed_participating_increments(flag_index)?; + + if validator_info.is_unslashed_participating_index(flag_index)? { + if !state_ctxt.is_in_inactivity_leak { + let reward_numerator = base_reward + .safe_mul(weight)? + .safe_mul(unslashed_participating_increments)?; + delta.reward( + reward_numerator.safe_div( + rewards_ctxt + .active_increments + .safe_mul(WEIGHT_DENOMINATOR)?, + )?, + )?; + } + } else if flag_index != TIMELY_HEAD_FLAG_INDEX { + delta.penalize(base_reward.safe_mul(weight)?.safe_div(WEIGHT_DENOMINATOR)?)?; + } + Ok(()) +} + +/// Get the weight for a `flag_index` from the constant list of all weights. +fn get_flag_weight(flag_index: usize) -> Result { + PARTICIPATION_FLAG_WEIGHTS + .get(flag_index) + .copied() + .ok_or(Error::InvalidFlagIndex(flag_index)) +} + +fn get_inactivity_penalty_delta( + delta: &mut Delta, + validator_info: &ValidatorInfo, + inactivity_score: &u64, + state_ctxt: &StateContext, + spec: &ChainSpec, +) -> Result<(), Error> { + if !validator_info.is_unslashed_participating_index(TIMELY_TARGET_FLAG_INDEX)? { + let penalty_numerator = validator_info + .effective_balance + .safe_mul(*inactivity_score)?; + let penalty_denominator = spec + .inactivity_score_bias + .safe_mul(spec.inactivity_penalty_quotient_for_fork(state_ctxt.fork_name))?; + delta.penalize(penalty_numerator.safe_div(penalty_denominator)?)?; + } + Ok(()) +} + +impl RewardsAndPenaltiesContext { + fn new( + progressive_balances: &ProgressiveBalancesCache, + state_ctxt: &StateContext, + spec: &ChainSpec, + ) -> Result { + let mut unslashed_participating_increments_array = [0; NUM_FLAG_INDICES]; + for flag_index in 0..NUM_FLAG_INDICES { + let unslashed_participating_balance = + progressive_balances.previous_epoch_flag_attesting_balance(flag_index)?; + let unslashed_participating_increments = + unslashed_participating_balance.safe_div(spec.effective_balance_increment)?; + + *unslashed_participating_increments_array + .get_mut(flag_index) + .ok_or(Error::InvalidFlagIndex(flag_index))? = unslashed_participating_increments; + } + let active_increments = state_ctxt + .total_active_balance + .safe_div(spec.effective_balance_increment)?; + + Ok(Self { + unslashed_participating_increments_array, + active_increments, + }) + } + + fn get_unslashed_participating_increments(&self, flag_index: usize) -> Result { + self.unslashed_participating_increments_array + .get(flag_index) + .copied() + .ok_or(Error::InvalidFlagIndex(flag_index)) + } +} + +fn process_single_registry_update( + validator: &mut Validator, + validator_info: &ValidatorInfo, + exit_cache: &mut ExitCache, + activation_queue: &BTreeSet, + next_epoch_activation_queue: &mut ActivationQueue, + state_ctxt: &StateContext, + spec: &ChainSpec, +) -> Result<(), Error> { + let current_epoch = state_ctxt.current_epoch; + + if validator.is_eligible_for_activation_queue(spec) { + validator.activation_eligibility_epoch = current_epoch.safe_add(1)?; + } + + if validator.is_active_at(current_epoch) && validator.effective_balance <= spec.ejection_balance + { + initiate_validator_exit(validator, exit_cache, state_ctxt, spec)?; + } + + if activation_queue.contains(&validator_info.index) { + validator.activation_epoch = spec.compute_activation_exit_epoch(current_epoch)?; + } + + // Caching: add to speculative activation queue for next epoch. + next_epoch_activation_queue.add_if_could_be_eligible_for_activation( + validator_info.index, + validator, + state_ctxt.next_epoch, + spec, + ); + + Ok(()) +} + +fn initiate_validator_exit( + validator: &mut Validator, + exit_cache: &mut ExitCache, + state_ctxt: &StateContext, + spec: &ChainSpec, +) -> Result<(), Error> { + // Return if the validator already initiated exit + if validator.exit_epoch != spec.far_future_epoch { + return Ok(()); + } + + // Compute exit queue epoch + let delayed_epoch = spec.compute_activation_exit_epoch(state_ctxt.current_epoch)?; + let mut exit_queue_epoch = exit_cache + .max_epoch()? + .map_or(delayed_epoch, |epoch| max(epoch, delayed_epoch)); + let exit_queue_churn = exit_cache.get_churn_at(exit_queue_epoch)?; + + if exit_queue_churn >= state_ctxt.churn_limit { + exit_queue_epoch.safe_add_assign(1)?; + } + + validator.exit_epoch = exit_queue_epoch; + validator.withdrawable_epoch = + exit_queue_epoch.safe_add(spec.min_validator_withdrawability_delay)?; + + exit_cache.record_validator_exit(exit_queue_epoch)?; + Ok(()) +} + +impl SlashingsContext { + fn new( + state: &BeaconState, + state_ctxt: &StateContext, + spec: &ChainSpec, + ) -> Result { + let sum_slashings = state.get_all_slashings().iter().copied().safe_sum()?; + let adjusted_total_slashing_balance = min( + sum_slashings.safe_mul(spec.proportional_slashing_multiplier_for_state(state))?, + state_ctxt.total_active_balance, + ); + + let target_withdrawable_epoch = state_ctxt + .current_epoch + .safe_add(E::EpochsPerSlashingsVector::to_u64().safe_div(2)?)?; + + Ok(Self { + adjusted_total_slashing_balance, + target_withdrawable_epoch, + }) + } +} + +fn process_single_slashing( + balance: &mut u64, + validator: &Validator, + slashings_ctxt: &SlashingsContext, + state_ctxt: &StateContext, + spec: &ChainSpec, +) -> Result<(), Error> { + if validator.slashed && slashings_ctxt.target_withdrawable_epoch == validator.withdrawable_epoch + { + let increment = spec.effective_balance_increment; + let penalty_numerator = validator + .effective_balance + .safe_div(increment)? + .safe_mul(slashings_ctxt.adjusted_total_slashing_balance)?; + let penalty = penalty_numerator + .safe_div(state_ctxt.total_active_balance)? + .safe_mul(increment)?; + + *balance = balance.saturating_sub(penalty); + } + Ok(()) +} + +impl EffectiveBalancesContext { + fn new(spec: &ChainSpec) -> Result { + let hysteresis_increment = spec + .effective_balance_increment + .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)?; + + Ok(Self { + downward_threshold, + upward_threshold, + }) + } +} + +#[allow(clippy::too_many_arguments)] +fn process_single_effective_balance_update( + balance: u64, + validator: &mut Validator, + validator_info: &ValidatorInfo, + next_epoch_total_active_balance: &mut u64, + next_epoch_cache: &mut PreEpochCache, + progressive_balances: &mut ProgressiveBalancesCache, + eb_ctxt: &EffectiveBalancesContext, + state_ctxt: &StateContext, + spec: &ChainSpec, +) -> Result<(), Error> { + let old_effective_balance = validator.effective_balance; + let new_effective_balance = if balance.safe_add(eb_ctxt.downward_threshold)? + < validator.effective_balance + || validator + .effective_balance + .safe_add(eb_ctxt.upward_threshold)? + < balance + { + min( + balance.safe_sub(balance.safe_rem(spec.effective_balance_increment)?)?, + spec.max_effective_balance, + ) + } else { + validator.effective_balance + }; + + if validator.is_active_at(state_ctxt.next_epoch) { + next_epoch_total_active_balance.safe_add_assign(new_effective_balance)?; + } + + if new_effective_balance != old_effective_balance { + validator.effective_balance = new_effective_balance; + + // Update progressive balances cache for the *current* epoch, which will soon become the + // previous epoch once the epoch transition completes. + progressive_balances.on_effective_balance_change( + validator.slashed, + validator_info.current_epoch_participation, + old_effective_balance, + new_effective_balance, + )?; + } + + // Caching: update next epoch effective balances. + next_epoch_cache.push_effective_balance(new_effective_balance); + + Ok(()) +} diff --git a/consensus/state_processing/src/per_epoch_processing/slashings.rs b/consensus/state_processing/src/per_epoch_processing/slashings.rs index 2d595491c1..6104208ee6 100644 --- a/consensus/state_processing/src/per_epoch_processing/slashings.rs +++ b/consensus/state_processing/src/per_epoch_processing/slashings.rs @@ -1,10 +1,14 @@ -use crate::per_epoch_processing::Error; +use crate::common::decrease_balance; +use crate::per_epoch_processing::{ + single_pass::{process_epoch_single_pass, SinglePassConfig}, + Error, +}; use safe_arith::{SafeArith, SafeArithIter}; -use types::{BeaconState, BeaconStateError, ChainSpec, EthSpec, Unsigned}; +use types::{BeaconState, ChainSpec, EthSpec, Unsigned}; /// Process slashings. -pub fn process_slashings( - state: &mut BeaconState, +pub fn process_slashings( + state: &mut BeaconState, total_balance: u64, spec: &ChainSpec, ) -> Result<(), Error> { @@ -16,28 +20,44 @@ pub fn process_slashings( total_balance, ); - 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)?)? - == validator.withdrawable_epoch - { - let increment = spec.effective_balance_increment; - let penalty_numerator = validator - .effective_balance - .safe_div(increment)? - .safe_mul(adjusted_total_slashing_balance)?; - let penalty = penalty_numerator - .safe_div(total_balance)? - .safe_mul(increment)?; + let target_withdrawable_epoch = + epoch.safe_add(E::EpochsPerSlashingsVector::to_u64().safe_div(2)?)?; + let indices = state + .validators() + .iter() + .enumerate() + .filter(|(_, validator)| { + validator.slashed && target_withdrawable_epoch == validator.withdrawable_epoch + }) + .map(|(index, validator)| (index, validator.effective_balance)) + .collect::>(); - // Equivalent to `decrease_balance(state, index, penalty)`, but avoids borrowing `state`. - let balance = balances - .get_mut(index) - .ok_or(BeaconStateError::BalancesOutOfBounds(index))?; - *balance = balance.saturating_sub(penalty); - } + for (index, validator_effective_balance) in indices { + let increment = spec.effective_balance_increment; + let penalty_numerator = validator_effective_balance + .safe_div(increment)? + .safe_mul(adjusted_total_slashing_balance)?; + let penalty = penalty_numerator + .safe_div(total_balance)? + .safe_mul(increment)?; + + decrease_balance(state, index, penalty)?; } Ok(()) } + +pub fn process_slashings_slow( + state: &mut BeaconState, + spec: &ChainSpec, +) -> Result<(), Error> { + process_epoch_single_pass( + state, + spec, + SinglePassConfig { + slashings: true, + ..SinglePassConfig::disable_all() + }, + )?; + Ok(()) +} diff --git a/consensus/state_processing/src/per_epoch_processing/weigh_justification_and_finalization.rs b/consensus/state_processing/src/per_epoch_processing/weigh_justification_and_finalization.rs index 96f6a8ef14..9a047fbfdb 100644 --- a/consensus/state_processing/src/per_epoch_processing/weigh_justification_and_finalization.rs +++ b/consensus/state_processing/src/per_epoch_processing/weigh_justification_and_finalization.rs @@ -5,12 +5,12 @@ use types::{Checkpoint, EthSpec}; /// Update the justified and finalized checkpoints for matching target attestations. #[allow(clippy::if_same_then_else)] // For readability and consistency with spec. -pub fn weigh_justification_and_finalization( - mut state: JustificationAndFinalizationState, +pub fn weigh_justification_and_finalization( + mut state: JustificationAndFinalizationState, total_active_balance: u64, previous_target_balance: u64, current_target_balance: u64, -) -> Result, Error> { +) -> Result, Error> { let previous_epoch = state.previous_epoch(); let current_epoch = state.current_epoch(); diff --git a/consensus/state_processing/src/per_slot_processing.rs b/consensus/state_processing/src/per_slot_processing.rs index e89a78c4d8..cc28340962 100644 --- a/consensus/state_processing/src/per_slot_processing.rs +++ b/consensus/state_processing/src/per_slot_processing.rs @@ -1,5 +1,6 @@ use crate::upgrade::{ upgrade_to_altair, upgrade_to_bellatrix, upgrade_to_capella, upgrade_to_deneb, + upgrade_to_electra, }; use crate::{per_epoch_processing::EpochProcessingSummary, *}; use safe_arith::{ArithError, SafeArith}; @@ -24,11 +25,11 @@ impl From for Error { /// If the root of the supplied `state` is known, then it can be passed as `state_root`. If /// `state_root` is `None`, the root of `state` will be computed using a cached tree hash. /// Providing the `state_root` makes this function several orders of magnitude faster. -pub fn per_slot_processing( - state: &mut BeaconState, +pub fn per_slot_processing( + state: &mut BeaconState, state_root: Option, spec: &ChainSpec, -) -> Result>, Error> { +) -> Result>, Error> { // Verify that the `BeaconState` instantiation matches the fork at `state.slot()`. state .fork_name(spec) @@ -37,7 +38,7 @@ pub fn per_slot_processing( cache_state(state, state_root)?; let summary = if state.slot() > spec.genesis_slot - && state.slot().safe_add(1)?.safe_rem(T::slots_per_epoch())? == 0 + && state.slot().safe_add(1)?.safe_rem(E::slots_per_epoch())? == 0 { Some(per_epoch_processing(state, spec)?) } else { @@ -48,7 +49,7 @@ pub fn per_slot_processing( // Process fork upgrades here. Note that multiple upgrades can potentially run // in sequence if they are scheduled in the same Epoch (common in testnets) - if state.slot().safe_rem(T::slots_per_epoch())? == 0 { + if state.slot().safe_rem(E::slots_per_epoch())? == 0 { // If the Altair fork epoch is reached, perform an irregular state upgrade. if spec.altair_fork_epoch == Some(state.current_epoch()) { upgrade_to_altair(state, spec)?; @@ -61,17 +62,26 @@ pub fn per_slot_processing( if spec.capella_fork_epoch == Some(state.current_epoch()) { upgrade_to_capella(state, spec)?; } - // Deneb + // Deneb. if spec.deneb_fork_epoch == Some(state.current_epoch()) { upgrade_to_deneb(state, spec)?; } + // Electra. + if spec.electra_fork_epoch == Some(state.current_epoch()) { + upgrade_to_electra(state, spec)?; + } + + // Additionally build all caches so that all valid states that are advanced always have + // committee caches built, and we don't have to worry about initialising them at higher + // layers. + state.build_caches(spec)?; } Ok(summary) } -fn cache_state( - state: &mut BeaconState, +fn cache_state( + state: &mut BeaconState, state_root: Option, ) -> Result<(), Error> { let previous_state_root = if let Some(root) = state_root { diff --git a/consensus/state_processing/src/state_advance.rs b/consensus/state_processing/src/state_advance.rs index c3911be214..721907cac9 100644 --- a/consensus/state_processing/src/state_advance.rs +++ b/consensus/state_processing/src/state_advance.rs @@ -25,8 +25,8 @@ pub enum Error { /// /// This state advance method is "complete"; it outputs a perfectly valid `BeaconState` and doesn't /// do anything hacky like the "partial" method (see `partial_state_advance`). -pub fn complete_state_advance( - state: &mut BeaconState, +pub fn complete_state_advance( + state: &mut BeaconState, mut state_root_opt: Option, target_slot: Slot, spec: &ChainSpec, @@ -58,8 +58,8 @@ pub fn complete_state_advance( /// /// - If `state.slot > target_slot`, an error will be returned. /// - If `state_root_opt.is_none()` but the latest block header requires a state root. -pub fn partial_state_advance( - state: &mut BeaconState, +pub fn partial_state_advance( + state: &mut BeaconState, state_root_opt: Option, target_slot: Slot, spec: &ChainSpec, diff --git a/consensus/state_processing/src/upgrade.rs b/consensus/state_processing/src/upgrade.rs index 1509ee0e50..98602c66ba 100644 --- a/consensus/state_processing/src/upgrade.rs +++ b/consensus/state_processing/src/upgrade.rs @@ -1,9 +1,11 @@ pub mod altair; pub mod capella; pub mod deneb; +pub mod electra; pub mod merge; pub use altair::upgrade_to_altair; pub use capella::upgrade_to_capella; pub use deneb::upgrade_to_deneb; +pub use electra::upgrade_to_electra; pub use merge::upgrade_to_bellatrix; diff --git a/consensus/state_processing/src/upgrade/altair.rs b/consensus/state_processing/src/upgrade/altair.rs index 5bb4f0bd59..cfbc6eba9e 100644 --- a/consensus/state_processing/src/upgrade/altair.rs +++ b/consensus/state_processing/src/upgrade/altair.rs @@ -3,8 +3,8 @@ use crate::common::{get_attestation_participation_flag_indices, get_attesting_in use std::mem; use std::sync::Arc; use types::{ - BeaconState, BeaconStateAltair, BeaconStateError as Error, ChainSpec, EthSpec, Fork, - ParticipationFlags, PendingAttestation, RelativeEpoch, SyncCommittee, VariableList, + BeaconState, BeaconStateAltair, BeaconStateError as Error, ChainSpec, EpochCache, EthSpec, + Fork, ParticipationFlags, PendingAttestation, RelativeEpoch, SyncCommittee, VariableList, }; /// Translate the participation information from the epoch prior to the fork into Altair's format. @@ -106,13 +106,15 @@ pub fn upgrade_to_altair( committee_caches: mem::take(&mut pre.committee_caches), pubkey_cache: mem::take(&mut pre.pubkey_cache), exit_cache: mem::take(&mut pre.exit_cache), + slashings_cache: mem::take(&mut pre.slashings_cache), + epoch_cache: EpochCache::default(), tree_hash_cache: mem::take(&mut pre.tree_hash_cache), }); // 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)?; + initialize_progressive_balances_cache(&mut post, spec)?; // Fill in sync committees // Note: A duplicate committee is assigned for the current and next committee at the fork diff --git a/consensus/state_processing/src/upgrade/capella.rs b/consensus/state_processing/src/upgrade/capella.rs index 5153e35f44..87b40abebd 100644 --- a/consensus/state_processing/src/upgrade/capella.rs +++ b/consensus/state_processing/src/upgrade/capella.rs @@ -1,6 +1,8 @@ -use ssz_types::VariableList; use std::mem; -use types::{BeaconState, BeaconStateCapella, BeaconStateError as Error, ChainSpec, EthSpec, Fork}; +use types::{ + BeaconState, BeaconStateCapella, BeaconStateError as Error, ChainSpec, EpochCache, EthSpec, + Fork, VariableList, +}; /// Transform a `Merge` state into an `Capella` state. pub fn upgrade_to_capella( @@ -66,6 +68,8 @@ pub fn upgrade_to_capella( committee_caches: mem::take(&mut pre.committee_caches), pubkey_cache: mem::take(&mut pre.pubkey_cache), exit_cache: mem::take(&mut pre.exit_cache), + slashings_cache: mem::take(&mut pre.slashings_cache), + epoch_cache: EpochCache::default(), tree_hash_cache: mem::take(&mut pre.tree_hash_cache), }); diff --git a/consensus/state_processing/src/upgrade/deneb.rs b/consensus/state_processing/src/upgrade/deneb.rs index c253a8c162..43fe5d9dc3 100644 --- a/consensus/state_processing/src/upgrade/deneb.rs +++ b/consensus/state_processing/src/upgrade/deneb.rs @@ -1,5 +1,7 @@ use std::mem; -use types::{BeaconState, BeaconStateDeneb, BeaconStateError as Error, ChainSpec, EthSpec, Fork}; +use types::{ + BeaconState, BeaconStateDeneb, BeaconStateError as Error, ChainSpec, EpochCache, EthSpec, Fork, +}; /// Transform a `Capella` state into an `Deneb` state. pub fn upgrade_to_deneb( @@ -67,6 +69,8 @@ pub fn upgrade_to_deneb( committee_caches: mem::take(&mut pre.committee_caches), pubkey_cache: mem::take(&mut pre.pubkey_cache), exit_cache: mem::take(&mut pre.exit_cache), + slashings_cache: mem::take(&mut pre.slashings_cache), + epoch_cache: EpochCache::default(), tree_hash_cache: mem::take(&mut pre.tree_hash_cache), }); diff --git a/consensus/state_processing/src/upgrade/electra.rs b/consensus/state_processing/src/upgrade/electra.rs new file mode 100644 index 0000000000..a37d0fc3be --- /dev/null +++ b/consensus/state_processing/src/upgrade/electra.rs @@ -0,0 +1,79 @@ +use std::mem; +use types::{ + BeaconState, BeaconStateElectra, BeaconStateError as Error, ChainSpec, EpochCache, EthSpec, + Fork, +}; + +/// Transform a `Deneb` state into an `Electra` state. +pub fn upgrade_to_electra( + pre_state: &mut BeaconState, + spec: &ChainSpec, +) -> Result<(), Error> { + let epoch = pre_state.current_epoch(); + let pre = pre_state.as_deneb_mut()?; + + // Where possible, use something like `mem::take` to move fields from behind the &mut + // reference. For other fields that don't have a good default value, use `clone`. + // + // Fixed size vectors get cloned because replacing them would require the same size + // allocation as cloning. + let post = BeaconState::Electra(BeaconStateElectra { + // Versioning + genesis_time: pre.genesis_time, + genesis_validators_root: pre.genesis_validators_root, + slot: pre.slot, + fork: Fork { + previous_version: pre.fork.current_version, + current_version: spec.electra_fork_version, + epoch, + }, + // History + latest_block_header: pre.latest_block_header.clone(), + block_roots: pre.block_roots.clone(), + state_roots: pre.state_roots.clone(), + historical_roots: mem::take(&mut pre.historical_roots), + // Eth1 + eth1_data: pre.eth1_data.clone(), + eth1_data_votes: mem::take(&mut pre.eth1_data_votes), + eth1_deposit_index: pre.eth1_deposit_index, + // Registry + validators: mem::take(&mut pre.validators), + balances: mem::take(&mut pre.balances), + // Randomness + randao_mixes: pre.randao_mixes.clone(), + // Slashings + slashings: pre.slashings.clone(), + // `Participation + previous_epoch_participation: mem::take(&mut pre.previous_epoch_participation), + current_epoch_participation: mem::take(&mut pre.current_epoch_participation), + // Finality + justification_bits: pre.justification_bits.clone(), + previous_justified_checkpoint: pre.previous_justified_checkpoint, + current_justified_checkpoint: pre.current_justified_checkpoint, + finalized_checkpoint: pre.finalized_checkpoint, + // Inactivity + inactivity_scores: mem::take(&mut pre.inactivity_scores), + // Sync committees + current_sync_committee: pre.current_sync_committee.clone(), + next_sync_committee: pre.next_sync_committee.clone(), + // Execution + latest_execution_payload_header: pre.latest_execution_payload_header.upgrade_to_electra(), + // Capella + next_withdrawal_index: pre.next_withdrawal_index, + next_withdrawal_validator_index: pre.next_withdrawal_validator_index, + historical_summaries: pre.historical_summaries.clone(), + // Caches + total_active_balance: pre.total_active_balance, + progressive_balances_cache: mem::take(&mut pre.progressive_balances_cache), + committee_caches: mem::take(&mut pre.committee_caches), + pubkey_cache: mem::take(&mut pre.pubkey_cache), + exit_cache: mem::take(&mut pre.exit_cache), + slashings_cache: mem::take(&mut pre.slashings_cache), + epoch_cache: EpochCache::default(), + tree_hash_cache: mem::take(&mut pre.tree_hash_cache), + }); + + *pre_state = post; + + Ok(()) +} diff --git a/consensus/state_processing/src/upgrade/merge.rs b/consensus/state_processing/src/upgrade/merge.rs index eb74450107..147c97ac29 100644 --- a/consensus/state_processing/src/upgrade/merge.rs +++ b/consensus/state_processing/src/upgrade/merge.rs @@ -1,6 +1,6 @@ use std::mem; use types::{ - BeaconState, BeaconStateError as Error, BeaconStateMerge, ChainSpec, EthSpec, + BeaconState, BeaconStateError as Error, BeaconStateMerge, ChainSpec, EpochCache, EthSpec, ExecutionPayloadHeaderMerge, Fork, }; @@ -64,6 +64,8 @@ pub fn upgrade_to_bellatrix( committee_caches: mem::take(&mut pre.committee_caches), pubkey_cache: mem::take(&mut pre.pubkey_cache), exit_cache: mem::take(&mut pre.exit_cache), + slashings_cache: mem::take(&mut pre.slashings_cache), + epoch_cache: EpochCache::default(), tree_hash_cache: mem::take(&mut pre.tree_hash_cache), }); diff --git a/consensus/types/examples/tree_hash_state.rs b/consensus/types/examples/tree_hash_state.rs index a421a23ad5..26777b2591 100644 --- a/consensus/types/examples/tree_hash_state.rs +++ b/consensus/types/examples/tree_hash_state.rs @@ -49,7 +49,7 @@ fn main() { actual_thing::(&mut state); } -fn actual_thing(state: &mut BeaconState) { +fn actual_thing(state: &mut BeaconState) { for _ in 0..200_024 { let _ = state.update_tree_hash_cache().expect("should update cache"); } diff --git a/consensus/types/presets/gnosis/electra.yaml b/consensus/types/presets/gnosis/electra.yaml new file mode 100644 index 0000000000..cafdcbbf8d --- /dev/null +++ b/consensus/types/presets/gnosis/electra.yaml @@ -0,0 +1,3 @@ +# Gnosis preset - Electra + +ELECTRA_PLACEHOLDER: 0 diff --git a/consensus/types/presets/mainnet/deneb.yaml b/consensus/types/presets/mainnet/deneb.yaml index 6d2fb4abde..0f56b8bdfa 100644 --- a/consensus/types/presets/mainnet/deneb.yaml +++ b/consensus/types/presets/mainnet/deneb.yaml @@ -8,5 +8,5 @@ FIELD_ELEMENTS_PER_BLOB: 4096 MAX_BLOB_COMMITMENTS_PER_BLOCK: 4096 # `uint64(6)` MAX_BLOBS_PER_BLOCK: 6 -# `floorlog2(BLOB_KZG_COMMITMENTS_GINDEX) + 1 + ceillog2(MAX_BLOB_COMMITMENTS_PER_BLOCK)` = 4 + 1 + 12 = 17 +# `floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) + 1 + ceillog2(MAX_BLOB_COMMITMENTS_PER_BLOCK)` = 4 + 1 + 12 = 17 KZG_COMMITMENT_INCLUSION_PROOF_DEPTH: 17 diff --git a/consensus/types/presets/mainnet/electra.yaml b/consensus/types/presets/mainnet/electra.yaml new file mode 100644 index 0000000000..64d8b97b63 --- /dev/null +++ b/consensus/types/presets/mainnet/electra.yaml @@ -0,0 +1,3 @@ +# Mainnet preset - Electra + +ELECTRA_PLACEHOLDER: 0 diff --git a/consensus/types/presets/minimal/electra.yaml b/consensus/types/presets/minimal/electra.yaml new file mode 100644 index 0000000000..3baa7fa816 --- /dev/null +++ b/consensus/types/presets/minimal/electra.yaml @@ -0,0 +1,3 @@ +# Minimal preset - Electra + +ELECTRA_PLACEHOLDER: 0 diff --git a/consensus/types/src/activation_queue.rs b/consensus/types/src/activation_queue.rs new file mode 100644 index 0000000000..09ffa5b85e --- /dev/null +++ b/consensus/types/src/activation_queue.rs @@ -0,0 +1,44 @@ +use crate::{ChainSpec, Epoch, Validator}; +use std::collections::BTreeSet; + +/// Activation queue computed during epoch processing for use in the *next* epoch. +#[derive(Debug, PartialEq, Eq, Default, Clone, arbitrary::Arbitrary)] +pub struct ActivationQueue { + /// Validators represented by `(activation_eligibility_epoch, index)` in sorted order. + /// + /// These validators are not *necessarily* going to be activated. Their activation depends + /// on how finalization is updated, and the `churn_limit`. + queue: BTreeSet<(Epoch, usize)>, +} + +impl ActivationQueue { + /// Check if `validator` could be eligible for activation in the next epoch and add them to + /// the tentative activation queue if this is the case. + pub fn add_if_could_be_eligible_for_activation( + &mut self, + index: usize, + validator: &Validator, + next_epoch: Epoch, + spec: &ChainSpec, + ) { + if validator.could_be_eligible_for_activation_at(next_epoch, spec) { + self.queue + .insert((validator.activation_eligibility_epoch, index)); + } + } + + /// Determine the final activation queue after accounting for finalization & the churn limit. + pub fn get_validators_eligible_for_activation( + &self, + finalized_epoch: Epoch, + churn_limit: usize, + ) -> BTreeSet { + self.queue + .iter() + .filter_map(|&(eligibility_epoch, index)| { + (eligibility_epoch <= finalized_epoch).then_some(index) + }) + .take(churn_limit) + .collect() + } +} diff --git a/consensus/types/src/aggregate_and_proof.rs b/consensus/types/src/aggregate_and_proof.rs index ac31e78cb7..bfbf4d97af 100644 --- a/consensus/types/src/aggregate_and_proof.rs +++ b/consensus/types/src/aggregate_and_proof.rs @@ -23,27 +23,27 @@ use tree_hash_derive::TreeHash; TestRandom, TreeHash, )] -#[serde(bound = "T: EthSpec")] -#[arbitrary(bound = "T: EthSpec")] -pub struct AggregateAndProof { +#[serde(bound = "E: EthSpec")] +#[arbitrary(bound = "E: EthSpec")] +pub struct AggregateAndProof { /// The index of the validator that created the attestation. #[serde(with = "serde_utils::quoted_u64")] pub aggregator_index: u64, /// The aggregate attestation. - pub aggregate: Attestation, + pub aggregate: Attestation, /// A proof provided by the validator that permits them to publish on the /// `beacon_aggregate_and_proof` gossipsub topic. pub selection_proof: Signature, } -impl AggregateAndProof { +impl AggregateAndProof { /// Produces a new `AggregateAndProof` with a `selection_proof` generated by signing /// `aggregate.data.slot` with `secret_key`. /// /// If `selection_proof.is_none()` it will be computed locally. pub fn from_aggregate( aggregator_index: u64, - aggregate: Attestation, + aggregate: Attestation, selection_proof: Option, secret_key: &SecretKey, fork: &Fork, @@ -52,7 +52,7 @@ impl AggregateAndProof { ) -> Self { let selection_proof = selection_proof .unwrap_or_else(|| { - SelectionProof::new::( + SelectionProof::new::( aggregate.data.slot, secret_key, fork, @@ -77,7 +77,7 @@ impl AggregateAndProof { genesis_validators_root: Hash256, spec: &ChainSpec, ) -> bool { - let target_epoch = self.aggregate.data.slot.epoch(T::slots_per_epoch()); + let target_epoch = self.aggregate.data.slot.epoch(E::slots_per_epoch()); let domain = spec.get_domain( target_epoch, Domain::SelectionProof, @@ -89,4 +89,4 @@ impl AggregateAndProof { } } -impl SignedRoot for AggregateAndProof {} +impl SignedRoot for AggregateAndProof {} diff --git a/consensus/types/src/attestation.rs b/consensus/types/src/attestation.rs index ac4a583cbb..e43077d059 100644 --- a/consensus/types/src/attestation.rs +++ b/consensus/types/src/attestation.rs @@ -35,16 +35,16 @@ pub enum Error { TestRandom, Derivative, )] -#[derivative(PartialEq, Hash(bound = "T: EthSpec"))] -#[serde(bound = "T: EthSpec")] -#[arbitrary(bound = "T: EthSpec")] -pub struct Attestation { - pub aggregation_bits: BitList, +#[derivative(PartialEq, Hash(bound = "E: EthSpec"))] +#[serde(bound = "E: EthSpec")] +#[arbitrary(bound = "E: EthSpec")] +pub struct Attestation { + pub aggregation_bits: BitList, pub data: AttestationData, pub signature: AggregateSignature, } -impl Attestation { +impl Attestation { /// Are the aggregation bitfields of these attestations disjoint? pub fn signers_disjoint_from(&self, other: &Self) -> bool { self.aggregation_bits @@ -111,7 +111,7 @@ impl Attestation { } } -impl SlotData for Attestation { +impl SlotData for Attestation { fn get_slot(&self) -> Slot { self.data.slot } @@ -125,7 +125,7 @@ mod tests { // Check the in-memory size of an `Attestation`, which is useful for reasoning about memory // and preventing regressions. // - // This test will only pass with `blst`, if we run these tests with Milagro or another + // This test will only pass with `blst`, if we run these tests with another // BLS library in future we will have to make it generic. #[test] fn size_of() { diff --git a/consensus/types/src/attester_slashing.rs b/consensus/types/src/attester_slashing.rs index c2bbea637e..5ad5297d0c 100644 --- a/consensus/types/src/attester_slashing.rs +++ b/consensus/types/src/attester_slashing.rs @@ -21,12 +21,12 @@ use tree_hash_derive::TreeHash; TestRandom, arbitrary::Arbitrary, )] -#[derivative(PartialEq, Eq, Hash(bound = "T: EthSpec"))] -#[serde(bound = "T: EthSpec")] -#[arbitrary(bound = "T: EthSpec")] -pub struct AttesterSlashing { - pub attestation_1: IndexedAttestation, - pub attestation_2: IndexedAttestation, +#[derivative(PartialEq, Eq, Hash(bound = "E: EthSpec"))] +#[serde(bound = "E: EthSpec")] +#[arbitrary(bound = "E: EthSpec")] +pub struct AttesterSlashing { + pub attestation_1: IndexedAttestation, + pub attestation_2: IndexedAttestation, } #[cfg(test)] diff --git a/consensus/types/src/beacon_block.rs b/consensus/types/src/beacon_block.rs index 90dff84b39..14874f0204 100644 --- a/consensus/types/src/beacon_block.rs +++ b/consensus/types/src/beacon_block.rs @@ -1,10 +1,5 @@ -use crate::beacon_block_body::{ - BeaconBlockBodyAltair, BeaconBlockBodyBase, BeaconBlockBodyDeneb, BeaconBlockBodyMerge, - BeaconBlockBodyRef, BeaconBlockBodyRefMut, -}; use crate::test_utils::TestRandom; use crate::*; -use bls::Signature; use derivative::Derivative; use serde::{Deserialize, Serialize}; use ssz::{Decode, DecodeError}; @@ -17,7 +12,7 @@ use tree_hash_derive::TreeHash; /// A block of the `BeaconChain`. #[superstruct( - variants(Base, Altair, Merge, Capella, Deneb), + variants(Base, Altair, Merge, Capella, Deneb, Electra), variant_attributes( derive( Debug, @@ -31,12 +26,12 @@ use tree_hash_derive::TreeHash; Derivative, arbitrary::Arbitrary ), - derivative(PartialEq, Hash(bound = "T: EthSpec, Payload: AbstractExecPayload")), + derivative(PartialEq, Hash(bound = "E: EthSpec, Payload: AbstractExecPayload")), serde( - bound = "T: EthSpec, Payload: AbstractExecPayload", + bound = "E: EthSpec, Payload: AbstractExecPayload", deny_unknown_fields ), - arbitrary(bound = "T: EthSpec, Payload: AbstractExecPayload"), + arbitrary(bound = "E: EthSpec, Payload: AbstractExecPayload"), ), ref_attributes( derive(Debug, PartialEq, TreeHash), @@ -48,13 +43,13 @@ use tree_hash_derive::TreeHash; #[derive( Debug, Clone, Serialize, Deserialize, Encode, TreeHash, Derivative, arbitrary::Arbitrary, )] -#[derivative(PartialEq, Hash(bound = "T: EthSpec"))] +#[derivative(PartialEq, Hash(bound = "E: EthSpec"))] #[serde(untagged)] -#[serde(bound = "T: EthSpec, Payload: AbstractExecPayload")] -#[arbitrary(bound = "T: EthSpec, Payload: AbstractExecPayload")] +#[serde(bound = "E: EthSpec, Payload: AbstractExecPayload")] +#[arbitrary(bound = "E: EthSpec, Payload: AbstractExecPayload")] #[tree_hash(enum_behaviour = "transparent")] #[ssz(enum_behaviour = "transparent")] -pub struct BeaconBlock = FullPayload> { +pub struct BeaconBlock = FullPayload> { #[superstruct(getter(copy))] pub slot: Slot, #[superstruct(getter(copy))] @@ -65,22 +60,24 @@ pub struct BeaconBlock = FullPayload #[superstruct(getter(copy))] pub state_root: Hash256, #[superstruct(only(Base), partial_getter(rename = "body_base"))] - pub body: BeaconBlockBodyBase, + pub body: BeaconBlockBodyBase, #[superstruct(only(Altair), partial_getter(rename = "body_altair"))] - pub body: BeaconBlockBodyAltair, + pub body: BeaconBlockBodyAltair, #[superstruct(only(Merge), partial_getter(rename = "body_merge"))] - pub body: BeaconBlockBodyMerge, + pub body: BeaconBlockBodyMerge, #[superstruct(only(Capella), partial_getter(rename = "body_capella"))] - pub body: BeaconBlockBodyCapella, + pub body: BeaconBlockBodyCapella, #[superstruct(only(Deneb), partial_getter(rename = "body_deneb"))] - pub body: BeaconBlockBodyDeneb, + pub body: BeaconBlockBodyDeneb, + #[superstruct(only(Electra), partial_getter(rename = "body_electra"))] + pub body: BeaconBlockBodyElectra, } pub type BlindedBeaconBlock = BeaconBlock>; -impl> SignedRoot for BeaconBlock {} -impl<'a, T: EthSpec, Payload: AbstractExecPayload> SignedRoot - for BeaconBlockRef<'a, T, Payload> +impl> SignedRoot for BeaconBlock {} +impl<'a, E: EthSpec, Payload: AbstractExecPayload> SignedRoot + for BeaconBlockRef<'a, E, Payload> { } @@ -90,11 +87,11 @@ pub trait EmptyBlock { fn empty(spec: &ChainSpec) -> Self; } -impl> BeaconBlock { +impl> BeaconBlock { /// Returns an empty block to be used during genesis. pub fn empty(spec: &ChainSpec) -> Self { map_fork_name!( - spec.fork_name_at_epoch(T::genesis_epoch()), + spec.fork_name_at_epoch(E::genesis_epoch()), Self, EmptyBlock::empty(spec) ) @@ -111,7 +108,7 @@ impl> BeaconBlock { })?; let slot = Slot::from_ssz_bytes(slot_bytes)?; - let fork_at_slot = spec.fork_name_at_slot::(slot); + let fork_at_slot = spec.fork_name_at_slot::(slot); Self::from_ssz_bytes_for_fork(bytes, fork_at_slot) } @@ -129,8 +126,9 @@ impl> BeaconBlock { /// Usually it's better to prefer `from_ssz_bytes` which will decode the correct variant based /// on the fork slot. pub fn any_from_ssz_bytes(bytes: &[u8]) -> Result { - BeaconBlockDeneb::from_ssz_bytes(bytes) - .map(BeaconBlock::Deneb) + BeaconBlockElectra::from_ssz_bytes(bytes) + .map(BeaconBlock::Electra) + .or_else(|_| BeaconBlockDeneb::from_ssz_bytes(bytes).map(BeaconBlock::Deneb)) .or_else(|_| BeaconBlockCapella::from_ssz_bytes(bytes).map(BeaconBlock::Capella)) .or_else(|_| BeaconBlockMerge::from_ssz_bytes(bytes).map(BeaconBlock::Merge)) .or_else(|_| BeaconBlockAltair::from_ssz_bytes(bytes).map(BeaconBlock::Altair)) @@ -138,18 +136,18 @@ impl> BeaconBlock { } /// Convenience accessor for the `body` as a `BeaconBlockBodyRef`. - pub fn body(&self) -> BeaconBlockBodyRef<'_, T, Payload> { + pub fn body(&self) -> BeaconBlockBodyRef<'_, E, Payload> { self.to_ref().body() } /// Convenience accessor for the `body` as a `BeaconBlockBodyRefMut`. - pub fn body_mut(&mut self) -> BeaconBlockBodyRefMut<'_, T, Payload> { + pub fn body_mut(&mut self) -> BeaconBlockBodyRefMut<'_, E, Payload> { self.to_mut().body_mut() } /// Returns the epoch corresponding to `self.slot()`. pub fn epoch(&self) -> Epoch { - self.slot().epoch(T::slots_per_epoch()) + self.slot().epoch(E::slots_per_epoch()) } /// Returns the `tree_hash_root` of the block. @@ -184,7 +182,7 @@ impl> BeaconBlock { fork: &Fork, genesis_validators_root: Hash256, spec: &ChainSpec, - ) -> SignedBeaconBlock { + ) -> SignedBeaconBlock { let domain = spec.get_domain( self.epoch(), Domain::BeaconProposer, @@ -197,13 +195,13 @@ impl> BeaconBlock { } } -impl<'a, T: EthSpec, Payload: AbstractExecPayload> BeaconBlockRef<'a, T, Payload> { +impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockRef<'a, E, Payload> { /// Returns the name of the fork pertaining to `self`. /// /// Will return an `Err` if `self` has been instantiated to a variant conflicting with the fork /// dictated by `self.slot()`. pub fn fork_name(&self, spec: &ChainSpec) -> Result { - let fork_at_slot = spec.fork_name_at_slot::(self.slot()); + let fork_at_slot = spec.fork_name_at_slot::(self.slot()); let object_fork = self.fork_name_unchecked(); if fork_at_slot == object_fork { @@ -226,11 +224,12 @@ impl<'a, T: EthSpec, Payload: AbstractExecPayload> BeaconBlockRef<'a, T, Payl BeaconBlockRef::Merge { .. } => ForkName::Merge, BeaconBlockRef::Capella { .. } => ForkName::Capella, BeaconBlockRef::Deneb { .. } => ForkName::Deneb, + BeaconBlockRef::Electra { .. } => ForkName::Electra, } } /// Convenience accessor for the `body` as a `BeaconBlockBodyRef`. - pub fn body(&self) -> BeaconBlockBodyRef<'a, T, Payload> { + pub fn body(&self) -> BeaconBlockBodyRef<'a, E, Payload> { map_beacon_block_ref_into_beacon_block_body_ref!(&'a _, *self, |block, cons| cons( &block.body )) @@ -246,7 +245,7 @@ impl<'a, T: EthSpec, Payload: AbstractExecPayload> BeaconBlockRef<'a, T, Payl /// Returns the epoch corresponding to `self.slot()`. pub fn epoch(&self) -> Epoch { - self.slot().epoch(T::slots_per_epoch()) + self.slot().epoch(E::slots_per_epoch()) } /// Returns a full `BeaconBlockHeader` of this block. @@ -275,16 +274,16 @@ impl<'a, T: EthSpec, Payload: AbstractExecPayload> BeaconBlockRef<'a, T, Payl } } -impl<'a, T: EthSpec, Payload: AbstractExecPayload> BeaconBlockRefMut<'a, T, Payload> { +impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockRefMut<'a, E, Payload> { /// Convert a mutable reference to a beacon block to a mutable ref to its body. - pub fn body_mut(self) -> BeaconBlockBodyRefMut<'a, T, Payload> { + pub fn body_mut(self) -> BeaconBlockBodyRefMut<'a, E, Payload> { map_beacon_block_ref_mut_into_beacon_block_body_ref_mut!(&'a _, self, |block, cons| cons( &mut block.body )) } } -impl> EmptyBlock for BeaconBlockBase { +impl> EmptyBlock for BeaconBlockBase { fn empty(spec: &ChainSpec) -> Self { BeaconBlockBase { slot: spec.genesis_slot, @@ -310,7 +309,7 @@ impl> EmptyBlock for BeaconBlockBase } } -impl> BeaconBlockBase { +impl> BeaconBlockBase { /// Return a block where the block has maximum size. pub fn full(spec: &ChainSpec) -> Self { let header = BeaconBlockHeader { @@ -325,10 +324,10 @@ impl> BeaconBlockBase { message: header, signature: Signature::empty(), }; - let indexed_attestation: IndexedAttestation = IndexedAttestation { + let indexed_attestation: IndexedAttestation = IndexedAttestation { attesting_indices: VariableList::new(vec![ 0_u64; - T::MaxValidatorsPerCommittee::to_usize() + E::MaxValidatorsPerCommittee::to_usize() ]) .unwrap(), data: AttestationData::default(), @@ -351,8 +350,8 @@ impl> BeaconBlockBase { attestation_2: indexed_attestation, }; - let attestation: Attestation = Attestation { - aggregation_bits: BitList::with_capacity(T::MaxValidatorsPerCommittee::to_usize()) + let attestation: Attestation = Attestation { + aggregation_bits: BitList::with_capacity(E::MaxValidatorsPerCommittee::to_usize()) .unwrap(), data: AttestationData::default(), signature: AggregateSignature::empty(), @@ -373,25 +372,25 @@ impl> BeaconBlockBase { signature: Signature::empty(), }; - let mut block = BeaconBlockBase::::empty(spec); - for _ in 0..T::MaxProposerSlashings::to_usize() { + let mut block = BeaconBlockBase::::empty(spec); + for _ in 0..E::MaxProposerSlashings::to_usize() { block .body .proposer_slashings .push(proposer_slashing.clone()) .unwrap(); } - for _ in 0..T::MaxDeposits::to_usize() { + for _ in 0..E::MaxDeposits::to_usize() { block.body.deposits.push(deposit.clone()).unwrap(); } - for _ in 0..T::MaxVoluntaryExits::to_usize() { + for _ in 0..E::MaxVoluntaryExits::to_usize() { block .body .voluntary_exits .push(signed_voluntary_exit.clone()) .unwrap(); } - for _ in 0..T::MaxAttesterSlashings::to_usize() { + for _ in 0..E::MaxAttesterSlashings::to_usize() { block .body .attester_slashings @@ -399,14 +398,14 @@ impl> BeaconBlockBase { .unwrap(); } - for _ in 0..T::MaxAttestations::to_usize() { + for _ in 0..E::MaxAttestations::to_usize() { block.body.attestations.push(attestation.clone()).unwrap(); } block } } -impl> EmptyBlock for BeaconBlockAltair { +impl> EmptyBlock for BeaconBlockAltair { /// Returns an empty Altair block to be used during genesis. fn empty(spec: &ChainSpec) -> Self { BeaconBlockAltair { @@ -434,7 +433,7 @@ impl> EmptyBlock for BeaconBlockAlta } } -impl> BeaconBlockAltair { +impl> BeaconBlockAltair { /// Return an Altair block where the block has maximum size. pub fn full(spec: &ChainSpec) -> Self { let base_block: BeaconBlockBase<_, Payload> = BeaconBlockBase::full(spec); @@ -467,7 +466,7 @@ impl> BeaconBlockAltair } } -impl> EmptyBlock for BeaconBlockMerge { +impl> EmptyBlock for BeaconBlockMerge { /// Returns an empty Merge block to be used during genesis. fn empty(spec: &ChainSpec) -> Self { BeaconBlockMerge { @@ -495,7 +494,7 @@ impl> EmptyBlock for BeaconBlockMerg } } -impl> BeaconBlockCapella { +impl> BeaconBlockCapella { /// Return a Capella block where the block has maximum size. pub fn full(spec: &ChainSpec) -> Self { let base_block: BeaconBlockBase<_, Payload> = BeaconBlockBase::full(spec); @@ -508,7 +507,7 @@ impl> BeaconBlockCapella }, signature: Signature::empty() }; - T::max_bls_to_execution_changes() + E::max_bls_to_execution_changes() ] .into(); let sync_aggregate = SyncAggregate { @@ -541,7 +540,7 @@ impl> BeaconBlockCapella } } -impl> EmptyBlock for BeaconBlockCapella { +impl> EmptyBlock for BeaconBlockCapella { /// Returns an empty Capella block to be used during genesis. fn empty(spec: &ChainSpec) -> Self { BeaconBlockCapella { @@ -570,7 +569,7 @@ impl> EmptyBlock for BeaconBlockCape } } -impl> EmptyBlock for BeaconBlockDeneb { +impl> EmptyBlock for BeaconBlockDeneb { /// Returns an empty Deneb block to be used during genesis. fn empty(spec: &ChainSpec) -> Self { BeaconBlockDeneb { @@ -600,6 +599,83 @@ impl> EmptyBlock for BeaconBlockDene } } +impl> BeaconBlockElectra { + /// Return a Electra block where the block has maximum size. + pub fn full(spec: &ChainSpec) -> Self { + let base_block: BeaconBlockBase<_, Payload> = BeaconBlockBase::full(spec); + let bls_to_execution_changes = vec![ + SignedBlsToExecutionChange { + message: BlsToExecutionChange { + validator_index: 0, + from_bls_pubkey: PublicKeyBytes::empty(), + to_execution_address: Address::zero(), + }, + signature: Signature::empty() + }; + E::max_bls_to_execution_changes() + ] + .into(); + let sync_aggregate = SyncAggregate { + sync_committee_signature: AggregateSignature::empty(), + sync_committee_bits: BitVector::default(), + }; + BeaconBlockElectra { + slot: spec.genesis_slot, + proposer_index: 0, + parent_root: Hash256::zero(), + state_root: Hash256::zero(), + body: BeaconBlockBodyElectra { + proposer_slashings: base_block.body.proposer_slashings, + attester_slashings: base_block.body.attester_slashings, + attestations: base_block.body.attestations, + deposits: base_block.body.deposits, + voluntary_exits: base_block.body.voluntary_exits, + bls_to_execution_changes, + sync_aggregate, + randao_reveal: Signature::empty(), + eth1_data: Eth1Data { + deposit_root: Hash256::zero(), + block_hash: Hash256::zero(), + deposit_count: 0, + }, + graffiti: Graffiti::default(), + execution_payload: Payload::Electra::default(), + blob_kzg_commitments: VariableList::empty(), + }, + } + } +} + +impl> EmptyBlock for BeaconBlockElectra { + /// Returns an empty Electra block to be used during genesis. + fn empty(spec: &ChainSpec) -> Self { + BeaconBlockElectra { + slot: spec.genesis_slot, + proposer_index: 0, + parent_root: Hash256::zero(), + state_root: Hash256::zero(), + body: BeaconBlockBodyElectra { + randao_reveal: Signature::empty(), + eth1_data: Eth1Data { + deposit_root: Hash256::zero(), + block_hash: Hash256::zero(), + deposit_count: 0, + }, + graffiti: Graffiti::default(), + proposer_slashings: VariableList::empty(), + attester_slashings: VariableList::empty(), + attestations: VariableList::empty(), + deposits: VariableList::empty(), + voluntary_exits: VariableList::empty(), + sync_aggregate: SyncAggregate::empty(), + execution_payload: Payload::Electra::default(), + bls_to_execution_changes: VariableList::empty(), + blob_kzg_commitments: VariableList::empty(), + }, + } + } +} + // We can convert pre-Bellatrix blocks without payloads into blocks "with" payloads. impl From>> for BeaconBlockBase> @@ -680,6 +756,7 @@ impl_from!(BeaconBlockAltair, >, >, |body impl_from!(BeaconBlockMerge, >, >, |body: BeaconBlockBodyMerge<_, _>| body.into()); impl_from!(BeaconBlockCapella, >, >, |body: BeaconBlockBodyCapella<_, _>| body.into()); impl_from!(BeaconBlockDeneb, >, >, |body: BeaconBlockBodyDeneb<_, _>| body.into()); +impl_from!(BeaconBlockElectra, >, >, |body: BeaconBlockBodyElectra<_, _>| body.into()); // We can clone blocks with payloads to blocks without payloads, without cloning the payload. macro_rules! impl_clone_as_blinded { @@ -712,6 +789,7 @@ impl_clone_as_blinded!(BeaconBlockAltair, >, >, >); impl_clone_as_blinded!(BeaconBlockCapella, >, >); impl_clone_as_blinded!(BeaconBlockDeneb, >, >); +impl_clone_as_blinded!(BeaconBlockElectra, >, >); // A reference to a full beacon block can be cloned into a blinded beacon block, without cloning the // execution payload. @@ -741,8 +819,8 @@ impl From>> } } -impl> ForkVersionDeserialize - for BeaconBlock +impl> ForkVersionDeserialize + for BeaconBlock { fn deserialize_by_fork<'de, D: serde::Deserializer<'de>>( value: serde_json::value::Value, @@ -762,8 +840,7 @@ impl> ForkVersionDeserialize #[cfg(test)] mod tests { use super::*; - use crate::test_utils::{test_ssz_tree_hash_pair_with, SeedableRng, TestRandom, XorShiftRng}; - use crate::{ForkName, MainnetEthSpec}; + use crate::test_utils::{test_ssz_tree_hash_pair_with, SeedableRng, XorShiftRng}; use ssz::Encode; type BeaconBlock = super::BeaconBlock; @@ -828,7 +905,7 @@ mod tests { } #[test] - fn roundtrip_4844_block() { + fn roundtrip_deneb_block() { let rng = &mut XorShiftRng::from_seed([42; 16]); let spec = &ForkName::Deneb.make_genesis_spec(MainnetEthSpec::default_spec()); @@ -846,6 +923,26 @@ mod tests { }); } + #[test] + fn roundtrip_electra_block() { + let rng = &mut XorShiftRng::from_seed([42; 16]); + let spec = &ForkName::Electra.make_genesis_spec(MainnetEthSpec::default_spec()); + + let inner_block = BeaconBlockElectra { + slot: Slot::random_for_test(rng), + proposer_index: u64::random_for_test(rng), + parent_root: Hash256::random_for_test(rng), + state_root: Hash256::random_for_test(rng), + body: BeaconBlockBodyElectra::random_for_test(rng), + }; + + let block = BeaconBlock::Electra(inner_block.clone()); + + test_ssz_tree_hash_pair_with(&block, &inner_block, |bytes| { + BeaconBlock::from_ssz_bytes(bytes, spec) + }); + } + #[test] fn decode_base_and_altair() { type E = MainnetEthSpec; @@ -863,10 +960,13 @@ mod tests { let capella_slot = capella_epoch.start_slot(E::slots_per_epoch()); let deneb_epoch = capella_epoch + 1; let deneb_slot = deneb_epoch.start_slot(E::slots_per_epoch()); + let electra_epoch = deneb_epoch + 1; + let electra_slot = electra_epoch.start_slot(E::slots_per_epoch()); spec.altair_fork_epoch = Some(altair_epoch); spec.capella_fork_epoch = Some(capella_epoch); spec.deneb_fork_epoch = Some(deneb_epoch); + spec.electra_fork_epoch = Some(electra_epoch); // BeaconBlockBase { @@ -940,7 +1040,7 @@ mod tests { slot: deneb_slot, ..<_>::random_for_test(rng) }); - // It's invalid to have an Capella block with a epoch lower than the fork epoch. + // It's invalid to have a Deneb block with a epoch lower than the fork epoch. let bad_block = { let mut bad = good_block.clone(); *bad.slot_mut() = capella_slot; @@ -955,5 +1055,28 @@ mod tests { BeaconBlock::from_ssz_bytes(&bad_block.as_ssz_bytes(), &spec) .expect_err("bad deneb block cannot be decoded"); } + + // BeaconBlockElectra + { + let good_block = BeaconBlock::Electra(BeaconBlockElectra { + slot: electra_slot, + ..<_>::random_for_test(rng) + }); + // It's invalid to have an Electra block with a epoch lower than the fork epoch. + let bad_block = { + let mut bad = good_block.clone(); + *bad.slot_mut() = deneb_slot; + bad + }; + + assert_eq!( + BeaconBlock::from_ssz_bytes(&good_block.as_ssz_bytes(), &spec) + .expect("good electra block can be decoded"), + good_block + ); + // TODO(electra): once the Electra block is changed from Deneb, update this to match + // the other forks. + assert!(BeaconBlock::from_ssz_bytes(&bad_block.as_ssz_bytes(), &spec).is_ok()); + } } } diff --git a/consensus/types/src/beacon_block_body.rs b/consensus/types/src/beacon_block_body.rs index 146dff895c..a55c16b80d 100644 --- a/consensus/types/src/beacon_block_body.rs +++ b/consensus/types/src/beacon_block_body.rs @@ -4,18 +4,24 @@ use derivative::Derivative; use merkle_proof::{MerkleTree, MerkleTreeError}; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; -use ssz_types::VariableList; use std::marker::PhantomData; use superstruct::superstruct; use test_random_derive::TestRandom; use tree_hash::{TreeHash, BYTES_PER_CHUNK}; use tree_hash_derive::TreeHash; -pub type KzgCommitments = - VariableList::MaxBlobCommitmentsPerBlock>; -pub type KzgCommitmentOpts = - FixedVector, ::MaxBlobsPerBlock>; +pub type KzgCommitments = + VariableList::MaxBlobCommitmentsPerBlock>; +pub type KzgCommitmentOpts = + FixedVector, ::MaxBlobsPerBlock>; +/// The number of leaves (including padding) on the `BeaconBlockBody` Merkle tree. +/// +/// ## Note +/// +/// This constant is set with the assumption that there are `> 8` and `<= 16` fields on the +/// `BeaconBlockBody`. **Tree hashing will fail if this value is set incorrectly.** +pub const NUM_BEACON_BLOCK_BODY_HASH_TREE_ROOT_LEAVES: usize = 16; /// Index of the `blob_kzg_commitments` leaf in the `BeaconBlockBody` tree post-deneb. pub const BLOB_KZG_COMMITMENTS_INDEX: usize = 11; @@ -23,7 +29,7 @@ pub const BLOB_KZG_COMMITMENTS_INDEX: usize = 11; /// /// This *superstruct* abstracts over the hard-fork. #[superstruct( - variants(Base, Altair, Merge, Capella, Deneb), + variants(Base, Altair, Merge, Capella, Deneb, Electra), variant_attributes( derive( Debug, @@ -37,32 +43,32 @@ pub const BLOB_KZG_COMMITMENTS_INDEX: usize = 11; Derivative, arbitrary::Arbitrary ), - derivative(PartialEq, Hash(bound = "T: EthSpec, Payload: AbstractExecPayload")), + derivative(PartialEq, Hash(bound = "E: EthSpec, Payload: AbstractExecPayload")), serde( - bound = "T: EthSpec, Payload: AbstractExecPayload", + bound = "E: EthSpec, Payload: AbstractExecPayload", deny_unknown_fields ), - arbitrary(bound = "T: EthSpec, Payload: AbstractExecPayload"), + arbitrary(bound = "E: EthSpec, Payload: AbstractExecPayload"), ), cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"), partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant") )] #[derive(Debug, Clone, Serialize, Deserialize, Derivative, arbitrary::Arbitrary)] -#[derivative(PartialEq, Hash(bound = "T: EthSpec"))] +#[derivative(PartialEq, Hash(bound = "E: EthSpec"))] #[serde(untagged)] -#[serde(bound = "T: EthSpec, Payload: AbstractExecPayload")] -#[arbitrary(bound = "T: EthSpec, Payload: AbstractExecPayload")] -pub struct BeaconBlockBody = FullPayload> { +#[serde(bound = "E: EthSpec, Payload: AbstractExecPayload")] +#[arbitrary(bound = "E: EthSpec, Payload: AbstractExecPayload")] +pub struct BeaconBlockBody = FullPayload> { pub randao_reveal: Signature, pub eth1_data: Eth1Data, pub graffiti: Graffiti, - pub proposer_slashings: VariableList, - pub attester_slashings: VariableList, T::MaxAttesterSlashings>, - pub attestations: VariableList, T::MaxAttestations>, - pub deposits: VariableList, - pub voluntary_exits: VariableList, - #[superstruct(only(Altair, Merge, Capella, Deneb))] - pub sync_aggregate: SyncAggregate, + pub proposer_slashings: VariableList, + pub attester_slashings: VariableList, E::MaxAttesterSlashings>, + pub attestations: VariableList, E::MaxAttestations>, + pub deposits: VariableList, + pub voluntary_exits: VariableList, + #[superstruct(only(Altair, Merge, Capella, Deneb, Electra))] + pub sync_aggregate: SyncAggregate, // We flatten the execution payload so that serde can use the name of the inner type, // either `execution_payload` for full payloads, or `execution_payload_header` for blinded // payloads. @@ -75,11 +81,14 @@ pub struct BeaconBlockBody = FullPay #[superstruct(only(Deneb), partial_getter(rename = "execution_payload_deneb"))] #[serde(flatten)] pub execution_payload: Payload::Deneb, - #[superstruct(only(Capella, Deneb))] + #[superstruct(only(Electra), partial_getter(rename = "execution_payload_electra"))] + #[serde(flatten)] + pub execution_payload: Payload::Electra, + #[superstruct(only(Capella, Deneb, Electra))] pub bls_to_execution_changes: - VariableList, - #[superstruct(only(Deneb))] - pub blob_kzg_commitments: KzgCommitments, + VariableList, + #[superstruct(only(Deneb, Electra))] + pub blob_kzg_commitments: KzgCommitments, #[superstruct(only(Base, Altair))] #[ssz(skip_serializing, skip_deserializing)] #[tree_hash(skip_hashing)] @@ -88,19 +97,20 @@ pub struct BeaconBlockBody = FullPay pub _phantom: PhantomData, } -impl> BeaconBlockBody { +impl> BeaconBlockBody { pub fn execution_payload(&self) -> Result, Error> { self.to_ref().execution_payload() } } -impl<'a, T: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, T, Payload> { +impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, Payload> { pub fn execution_payload(&self) -> Result, Error> { match self { Self::Base(_) | Self::Altair(_) => Err(Error::IncorrectStateVariant), Self::Merge(body) => Ok(Payload::Ref::from(&body.execution_payload)), Self::Capella(body) => Ok(Payload::Ref::from(&body.execution_payload)), Self::Deneb(body) => Ok(Payload::Ref::from(&body.execution_payload)), + Self::Electra(body) => Ok(Payload::Ref::from(&body.execution_payload)), } } @@ -109,7 +119,7 @@ impl<'a, T: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, T, pub fn kzg_commitment_merkle_proof( &self, index: usize, - ) -> Result, Error> { + ) -> Result, Error> { match self { Self::Base(_) | Self::Altair(_) | Self::Merge(_) | Self::Capella(_) => { Err(Error::IncorrectStateVariant) @@ -123,7 +133,7 @@ impl<'a, T: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, T, // Part1 (Branches for the subtree rooted at `blob_kzg_commitments`) // // Branches for `blob_kzg_commitments` without length mix-in - let depth = T::max_blob_commitments_per_block() + let depth = E::max_blob_commitments_per_block() .next_power_of_two() .ilog2(); let leaves: Vec<_> = body @@ -171,14 +181,81 @@ impl<'a, T: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, T, // Join the proofs for the subtree and the main tree proof.append(&mut proof_body); - debug_assert_eq!(proof.len(), T::kzg_proof_inclusion_proof_depth()); + debug_assert_eq!(proof.len(), E::kzg_proof_inclusion_proof_depth()); + Ok(proof.into()) + } + // TODO(electra): De-duplicate proof computation. + Self::Electra(body) => { + // We compute the branches by generating 2 merkle trees: + // 1. Merkle tree for the `blob_kzg_commitments` List object + // 2. Merkle tree for the `BeaconBlockBody` container + // We then merge the branches for both the trees all the way up to the root. + + // Part1 (Branches for the subtree rooted at `blob_kzg_commitments`) + // + // Branches for `blob_kzg_commitments` without length mix-in + let depth = E::max_blob_commitments_per_block() + .next_power_of_two() + .ilog2(); + let leaves: Vec<_> = body + .blob_kzg_commitments + .iter() + .map(|commitment| commitment.tree_hash_root()) + .collect(); + let tree = MerkleTree::create(&leaves, depth as usize); + let (_, mut proof) = tree + .generate_proof(index, depth as usize) + .map_err(Error::MerkleTreeError)?; + + // Add the branch corresponding to the length mix-in. + let length = body.blob_kzg_commitments.len(); + let usize_len = std::mem::size_of::(); + let mut length_bytes = [0; BYTES_PER_CHUNK]; + length_bytes + .get_mut(0..usize_len) + .ok_or(Error::MerkleTreeError(MerkleTreeError::PleaseNotifyTheDevs))? + .copy_from_slice(&length.to_le_bytes()); + let length_root = Hash256::from_slice(length_bytes.as_slice()); + proof.push(length_root); + + // Part 2 + // Branches for `BeaconBlockBody` container + let leaves = [ + body.randao_reveal.tree_hash_root(), + body.eth1_data.tree_hash_root(), + body.graffiti.tree_hash_root(), + body.proposer_slashings.tree_hash_root(), + body.attester_slashings.tree_hash_root(), + body.attestations.tree_hash_root(), + body.deposits.tree_hash_root(), + body.voluntary_exits.tree_hash_root(), + body.sync_aggregate.tree_hash_root(), + body.execution_payload.tree_hash_root(), + body.bls_to_execution_changes.tree_hash_root(), + body.blob_kzg_commitments.tree_hash_root(), + ]; + let beacon_block_body_depth = leaves.len().next_power_of_two().ilog2() as usize; + let tree = MerkleTree::create(&leaves, beacon_block_body_depth); + let (_, mut proof_body) = tree + .generate_proof(BLOB_KZG_COMMITMENTS_INDEX, beacon_block_body_depth) + .map_err(Error::MerkleTreeError)?; + // Join the proofs for the subtree and the main tree + proof.append(&mut proof_body); + + debug_assert_eq!(proof.len(), E::kzg_proof_inclusion_proof_depth()); Ok(proof.into()) } } } + + /// Return `true` if this block body has a non-zero number of blobs. + pub fn has_blobs(self) -> bool { + self.blob_kzg_commitments() + .map_or(false, |blobs| !blobs.is_empty()) + } } -impl<'a, T: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, T, Payload> { +impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, Payload> { /// Get the fork_name of this object pub fn fork_name(self) -> ForkName { match self { @@ -187,6 +264,7 @@ impl<'a, T: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, T, BeaconBlockBodyRef::Merge { .. } => ForkName::Merge, BeaconBlockBodyRef::Capella { .. } => ForkName::Capella, BeaconBlockBodyRef::Deneb { .. } => ForkName::Deneb, + BeaconBlockBodyRef::Electra { .. } => ForkName::Electra, } } } @@ -455,6 +533,50 @@ impl From>> } } +impl From>> + for ( + BeaconBlockBodyElectra>, + Option>, + ) +{ + fn from(body: BeaconBlockBodyElectra>) -> Self { + let BeaconBlockBodyElectra { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + execution_payload: FullPayloadElectra { execution_payload }, + bls_to_execution_changes, + blob_kzg_commitments, + } = body; + + ( + BeaconBlockBodyElectra { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + execution_payload: BlindedPayloadElectra { + execution_payload_header: From::from(&execution_payload), + }, + bls_to_execution_changes, + blob_kzg_commitments: blob_kzg_commitments.clone(), + }, + Some(execution_payload), + ) + } +} + // We can clone a full block into a blinded block, without cloning the payload. impl BeaconBlockBodyBase> { pub fn clone_as_blinded(&self) -> BeaconBlockBodyBase> { @@ -572,6 +694,42 @@ impl BeaconBlockBodyDeneb> { } } +impl BeaconBlockBodyElectra> { + pub fn clone_as_blinded(&self) -> BeaconBlockBodyElectra> { + let BeaconBlockBodyElectra { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + execution_payload: FullPayloadElectra { execution_payload }, + bls_to_execution_changes, + blob_kzg_commitments, + } = self; + + BeaconBlockBodyElectra { + randao_reveal: randao_reveal.clone(), + eth1_data: eth1_data.clone(), + graffiti: *graffiti, + proposer_slashings: proposer_slashings.clone(), + attester_slashings: attester_slashings.clone(), + attestations: attestations.clone(), + deposits: deposits.clone(), + voluntary_exits: voluntary_exits.clone(), + sync_aggregate: sync_aggregate.clone(), + execution_payload: BlindedPayloadElectra { + execution_payload_header: execution_payload.into(), + }, + bls_to_execution_changes: bls_to_execution_changes.clone(), + blob_kzg_commitments: blob_kzg_commitments.clone(), + } + } +} + impl From>> for ( BeaconBlockBody>, @@ -586,6 +744,56 @@ impl From>> } } +impl BeaconBlockBody { + pub fn block_body_merkle_proof(&self, generalized_index: usize) -> Result, Error> { + let field_index = match generalized_index { + light_client_update::EXECUTION_PAYLOAD_INDEX => { + // Execution payload is a top-level field, subtract off the generalized indices + // for the internal nodes. Result should be 9, the field offset of the execution + // payload in the `BeaconBlockBody`: + // https://github.com/ethereum/consensus-specs/blob/dev/specs/deneb/beacon-chain.md#beaconblockbody + generalized_index + .checked_sub(NUM_BEACON_BLOCK_BODY_HASH_TREE_ROOT_LEAVES) + .ok_or(Error::IndexNotSupported(generalized_index))? + } + _ => return Err(Error::IndexNotSupported(generalized_index)), + }; + + let mut leaves = vec![ + self.randao_reveal().tree_hash_root(), + self.eth1_data().tree_hash_root(), + self.graffiti().tree_hash_root(), + self.proposer_slashings().tree_hash_root(), + self.attester_slashings().tree_hash_root(), + self.attestations().tree_hash_root(), + self.deposits().tree_hash_root(), + self.voluntary_exits().tree_hash_root(), + ]; + + if let Ok(sync_aggregate) = self.sync_aggregate() { + leaves.push(sync_aggregate.tree_hash_root()) + } + + if let Ok(execution_payload) = self.execution_payload() { + leaves.push(execution_payload.tree_hash_root()) + } + + if let Ok(bls_to_execution_changes) = self.bls_to_execution_changes() { + leaves.push(bls_to_execution_changes.tree_hash_root()) + } + + if let Ok(blob_kzg_commitments) = self.blob_kzg_commitments() { + leaves.push(blob_kzg_commitments.tree_hash_root()) + } + + let depth = light_client_update::EXECUTION_PAYLOAD_PROOF_LEN; + let tree = merkle_proof::MerkleTree::create(&leaves, depth); + let (_, proof) = tree.generate_proof(field_index, depth)?; + + Ok(proof) + } +} + /// Util method helpful for logging. pub fn format_kzg_commitments(commitments: &[KzgCommitment]) -> String { let commitment_strings: Vec = commitments.iter().map(|x| x.to_string()).collect(); diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index bdc3be9ece..ba11c9c4cc 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -1,5 +1,5 @@ use self::committee_cache::get_active_validator_indices; -use self::exit_cache::ExitCache; +use crate::historical_summary::HistoricalSummary; use crate::test_utils::TestRandom; use crate::*; use compare_fields::CompareFields; @@ -7,13 +7,11 @@ use compare_fields_derive::CompareFields; use derivative::Derivative; use ethereum_hashing::hash; use int_to_bytes::{int_to_bytes4, int_to_bytes8}; -use pubkey_cache::PubkeyCache; +pub use pubkey_cache::PubkeyCache; use safe_arith::{ArithError, SafeArith}; use serde::{Deserialize, Serialize}; use ssz::{ssz_encode, Decode, DecodeError, Encode}; use ssz_derive::{Decode, Encode}; -use ssz_types::{typenum::Unsigned, BitVector, FixedVector}; -use std::convert::TryInto; use std::hash::Hash; use std::{fmt, mem, sync::Arc}; use superstruct::superstruct; @@ -27,8 +25,9 @@ pub use self::committee_cache::{ CommitteeCache, }; pub use crate::beacon_state::balance::Balance; +pub use crate::beacon_state::exit_cache::ExitCache; pub use crate::beacon_state::progressive_balances_cache::*; -use crate::historical_summary::HistoricalSummary; +pub use crate::beacon_state::slashings_cache::SlashingsCache; pub use clone_config::CloneConfig; pub use eth_spec::*; pub use iter::BlockRootsIter; @@ -42,12 +41,16 @@ mod exit_cache; mod iter; mod progressive_balances_cache; mod pubkey_cache; +mod slashings_cache; mod tests; mod tree_hash_cache; pub const CACHED_EPOCHS: usize = 3; const MAX_RANDOM_BYTE: u64 = (1 << 8) - 1; +pub type Validators = VariableList::ValidatorRegistryLimit>; +pub type Balances = VariableList::ValidatorRegistryLimit>; + #[derive(Debug, PartialEq, Clone)] pub enum Error { /// A state for a different hard-fork was required -- a severe logic error. @@ -99,13 +102,20 @@ pub enum Error { }, RelativeEpochError(RelativeEpochError), ExitCacheUninitialized, + ExitCacheInvalidEpoch { + max_exit_epoch: Epoch, + request_epoch: Epoch, + }, + SlashingsCacheUninitialized { + initialized_slot: Option, + latest_block_slot: Slot, + }, CommitteeCacheUninitialized(Option), SyncCommitteeCacheUninitialized, BlsError(bls::Error), SszTypesError(ssz_types::Error), TreeHashCacheNotInitialized, NonLinearTreeHashCacheHistory, - ParticipationCacheError(String), ProgressiveBalancesCacheNotInitialized, ProgressiveBalancesCacheInconsistent, TreeHashCacheSkippedSlot { @@ -135,6 +145,7 @@ pub enum Error { epoch: Epoch, }, IndexNotSupported(usize), + InvalidFlagIndex(usize), MerkleTreeError(merkle_proof::MerkleTreeError), } @@ -183,7 +194,7 @@ impl From for Hash256 { /// The state of the `BeaconChain` at some slot. #[superstruct( - variants(Base, Altair, Merge, Capella, Deneb), + variants(Base, Altair, Merge, Capella, Deneb, Electra), variant_attributes( derive( Derivative, @@ -198,8 +209,8 @@ impl From for Hash256 { CompareFields, arbitrary::Arbitrary ), - serde(bound = "T: EthSpec", deny_unknown_fields), - arbitrary(bound = "T: EthSpec"), + serde(bound = "E: EthSpec", deny_unknown_fields), + arbitrary(bound = "E: EthSpec"), derivative(Clone), ), cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"), @@ -207,13 +218,13 @@ impl From for Hash256 { )] #[derive(Debug, PartialEq, Serialize, Deserialize, Encode, TreeHash, arbitrary::Arbitrary)] #[serde(untagged)] -#[serde(bound = "T: EthSpec")] -#[arbitrary(bound = "T: EthSpec")] +#[serde(bound = "E: EthSpec")] +#[arbitrary(bound = "E: EthSpec")] #[tree_hash(enum_behaviour = "transparent")] #[ssz(enum_behaviour = "transparent")] -pub struct BeaconState +pub struct BeaconState where - T: EthSpec, + E: EthSpec, { // Versioning #[superstruct(getter(copy))] @@ -229,48 +240,48 @@ where // History pub latest_block_header: BeaconBlockHeader, #[compare_fields(as_slice)] - pub block_roots: FixedVector, + pub block_roots: FixedVector, #[compare_fields(as_slice)] - pub state_roots: FixedVector, + pub state_roots: FixedVector, // Frozen in Capella, replaced by historical_summaries - pub historical_roots: VariableList, + pub historical_roots: VariableList, // Ethereum 1.0 chain data pub eth1_data: Eth1Data, - pub eth1_data_votes: VariableList, + pub eth1_data_votes: VariableList, #[superstruct(getter(copy))] #[serde(with = "serde_utils::quoted_u64")] pub eth1_deposit_index: u64, // Registry #[compare_fields(as_slice)] - pub validators: VariableList, + pub validators: VariableList, #[compare_fields(as_slice)] #[serde(with = "ssz_types::serde_utils::quoted_u64_var_list")] - pub balances: VariableList, + pub balances: VariableList, // Randomness - pub randao_mixes: FixedVector, + pub randao_mixes: FixedVector, // Slashings #[serde(with = "ssz_types::serde_utils::quoted_u64_fixed_vec")] - pub slashings: FixedVector, + pub slashings: FixedVector, // Attestations (genesis fork only) #[superstruct(only(Base))] - pub previous_epoch_attestations: VariableList, T::MaxPendingAttestations>, + pub previous_epoch_attestations: VariableList, E::MaxPendingAttestations>, #[superstruct(only(Base))] - pub current_epoch_attestations: VariableList, T::MaxPendingAttestations>, + pub current_epoch_attestations: VariableList, E::MaxPendingAttestations>, // Participation (Altair and later) - #[superstruct(only(Altair, Merge, Capella, Deneb))] - pub previous_epoch_participation: VariableList, - #[superstruct(only(Altair, Merge, Capella, Deneb))] - pub current_epoch_participation: VariableList, + #[superstruct(only(Altair, Merge, Capella, Deneb, Electra))] + pub previous_epoch_participation: VariableList, + #[superstruct(only(Altair, Merge, Capella, Deneb, Electra))] + pub current_epoch_participation: VariableList, // Finality #[test_random(default)] - pub justification_bits: BitVector, + pub justification_bits: BitVector, #[superstruct(getter(copy))] pub previous_justified_checkpoint: Checkpoint, #[superstruct(getter(copy))] @@ -280,42 +291,47 @@ where // Inactivity #[serde(with = "ssz_types::serde_utils::quoted_u64_var_list")] - #[superstruct(only(Altair, Merge, Capella, Deneb))] - pub inactivity_scores: VariableList, + #[superstruct(only(Altair, Merge, Capella, Deneb, Electra))] + pub inactivity_scores: VariableList, // Light-client sync committees - #[superstruct(only(Altair, Merge, Capella, Deneb))] - pub current_sync_committee: Arc>, - #[superstruct(only(Altair, Merge, Capella, Deneb))] - pub next_sync_committee: Arc>, + #[superstruct(only(Altair, Merge, Capella, Deneb, Electra))] + pub current_sync_committee: Arc>, + #[superstruct(only(Altair, Merge, Capella, Deneb, Electra))] + pub next_sync_committee: Arc>, // Execution #[superstruct( only(Merge), partial_getter(rename = "latest_execution_payload_header_merge") )] - pub latest_execution_payload_header: ExecutionPayloadHeaderMerge, + pub latest_execution_payload_header: ExecutionPayloadHeaderMerge, #[superstruct( only(Capella), partial_getter(rename = "latest_execution_payload_header_capella") )] - pub latest_execution_payload_header: ExecutionPayloadHeaderCapella, + pub latest_execution_payload_header: ExecutionPayloadHeaderCapella, #[superstruct( only(Deneb), partial_getter(rename = "latest_execution_payload_header_deneb") )] - pub latest_execution_payload_header: ExecutionPayloadHeaderDeneb, + pub latest_execution_payload_header: ExecutionPayloadHeaderDeneb, + #[superstruct( + only(Electra), + partial_getter(rename = "latest_execution_payload_header_electra") + )] + pub latest_execution_payload_header: ExecutionPayloadHeaderElectra, // Capella - #[superstruct(only(Capella, Deneb), partial_getter(copy))] + #[superstruct(only(Capella, Deneb, Electra), partial_getter(copy))] #[serde(with = "serde_utils::quoted_u64")] pub next_withdrawal_index: u64, - #[superstruct(only(Capella, Deneb), partial_getter(copy))] + #[superstruct(only(Capella, Deneb, Electra), partial_getter(copy))] #[serde(with = "serde_utils::quoted_u64")] pub next_withdrawal_validator_index: u64, // Deep history valid from Capella onwards. - #[superstruct(only(Capella, Deneb))] - pub historical_summaries: VariableList, + #[superstruct(only(Capella, Deneb, Electra))] + pub historical_summaries: VariableList, // Caching (not in the spec) #[serde(skip_serializing, skip_deserializing)] @@ -353,16 +369,28 @@ where #[tree_hash(skip_hashing)] #[test_random(default)] #[derivative(Clone(clone_with = "clone_default"))] - pub tree_hash_cache: BeaconTreeHashCache, + pub slashings_cache: SlashingsCache, + /// Epoch cache of values that are useful for block processing that are static over an epoch. + #[serde(skip_serializing, skip_deserializing)] + #[ssz(skip_serializing, skip_deserializing)] + #[tree_hash(skip_hashing)] + #[test_random(default)] + pub epoch_cache: EpochCache, + #[serde(skip_serializing, skip_deserializing)] + #[ssz(skip_serializing, skip_deserializing)] + #[tree_hash(skip_hashing)] + #[test_random(default)] + #[derivative(Clone(clone_with = "clone_default"))] + pub tree_hash_cache: BeaconTreeHashCache, } -impl Clone for BeaconState { +impl Clone for BeaconState { fn clone(&self) -> Self { self.clone_with(CloneConfig::all()) } } -impl BeaconState { +impl BeaconState { /// Create a new BeaconState suitable for genesis. /// /// Not a complete genesis state, see `initialize_beacon_state_from_eth1`. @@ -375,11 +403,11 @@ impl BeaconState { fork: Fork { previous_version: spec.genesis_fork_version, current_version: spec.genesis_fork_version, - epoch: T::genesis_epoch(), + epoch: E::genesis_epoch(), }, // History - latest_block_header: BeaconBlock::::empty(spec).temporary_block_header(), + latest_block_header: BeaconBlock::::empty(spec).temporary_block_header(), block_roots: FixedVector::from_elem(Hash256::zero()), state_roots: FixedVector::from_elem(Hash256::zero()), historical_roots: VariableList::empty(), @@ -419,6 +447,8 @@ impl BeaconState { ], pubkey_cache: PubkeyCache::default(), exit_cache: ExitCache::default(), + slashings_cache: SlashingsCache::default(), + epoch_cache: EpochCache::default(), tree_hash_cache: <_>::default(), }) } @@ -451,6 +481,7 @@ impl BeaconState { BeaconState::Merge { .. } => ForkName::Merge, BeaconState::Capella { .. } => ForkName::Capella, BeaconState::Deneb { .. } => ForkName::Deneb, + BeaconState::Electra { .. } => ForkName::Electra, } } @@ -469,7 +500,7 @@ impl BeaconState { })?; let slot = Slot::from_ssz_bytes(slot_bytes)?; - let fork_at_slot = spec.fork_name_at_slot::(slot); + let fork_at_slot = spec.fork_name_at_slot::(slot); Ok(map_fork_name!( fork_at_slot, @@ -485,7 +516,7 @@ impl BeaconState { Hash256::from_slice(&self.tree_hash_root()[..]) } - pub fn historical_batch(&self) -> HistoricalBatch { + pub fn historical_batch(&self) -> HistoricalBatch { HistoricalBatch { block_roots: self.block_roots().clone(), state_roots: self.state_roots().clone(), @@ -502,7 +533,7 @@ impl BeaconState { /// The epoch corresponding to `self.slot()`. pub fn current_epoch(&self) -> Epoch { - self.slot().epoch(T::slots_per_epoch()) + self.slot().epoch(E::slots_per_epoch()) } /// The epoch prior to `self.current_epoch()`. @@ -510,10 +541,8 @@ impl BeaconState { /// If the current epoch is the genesis epoch, the genesis_epoch is returned. pub fn previous_epoch(&self) -> Epoch { let current_epoch = self.current_epoch(); - if current_epoch > T::genesis_epoch() { - current_epoch - .safe_sub(1) - .expect("current epoch greater than genesis implies greater than 0") + if let Ok(prev_epoch) = current_epoch.safe_sub(1) { + prev_epoch } else { current_epoch } @@ -594,7 +623,7 @@ impl BeaconState { slot: Slot, index: CommitteeIndex, ) -> Result { - let epoch = slot.epoch(T::slots_per_epoch()); + let epoch = slot.epoch(E::slots_per_epoch()); let relative_epoch = RelativeEpoch::from_epoch(self.current_epoch(), epoch)?; let cache = self.committee_cache(relative_epoch)?; @@ -641,7 +670,7 @@ impl BeaconState { if self.slot() <= decision_slot { Ok(block_root) } else { - self.get_block_root(decision_slot).map(|root| *root) + self.get_block_root(decision_slot).copied() } } @@ -657,14 +686,14 @@ impl BeaconState { if self.slot() == decision_slot { Ok(block_root) } else { - self.get_block_root(decision_slot).map(|root| *root) + self.get_block_root(decision_slot).copied() } } /// Returns the slot at which the proposer shuffling was decided. The block root at this slot /// can be used to key the proposer shuffling for the given epoch. fn proposer_shuffling_decision_slot(&self, epoch: Epoch) -> Slot { - epoch.start_slot(T::slots_per_epoch()).saturating_sub(1_u64) + epoch.start_slot(E::slots_per_epoch()).saturating_sub(1_u64) } /// Returns the block root which decided the attester shuffling for the given `relative_epoch`. @@ -683,7 +712,7 @@ impl BeaconState { if self.slot() == decision_slot { Ok(block_root) } else { - self.get_block_root(decision_slot).map(|root| *root) + self.get_block_root(decision_slot).copied() } } @@ -695,7 +724,7 @@ impl BeaconState { RelativeEpoch::Current => self.previous_epoch(), RelativeEpoch::Previous => self.previous_epoch().saturating_sub(1_u64), } - .start_slot(T::slots_per_epoch()) + .start_slot(E::slots_per_epoch()) .saturating_sub(1_u64) } @@ -749,7 +778,7 @@ impl BeaconState { } /// Convenience accessor for the `execution_payload_header` as an `ExecutionPayloadHeaderRef`. - pub fn latest_execution_payload_header(&self) -> Result, Error> { + pub fn latest_execution_payload_header(&self) -> Result, Error> { match self { BeaconState::Base(_) | BeaconState::Altair(_) => Err(Error::IncorrectStateVariant), BeaconState::Merge(state) => Ok(ExecutionPayloadHeaderRef::Merge( @@ -761,12 +790,15 @@ impl BeaconState { BeaconState::Deneb(state) => Ok(ExecutionPayloadHeaderRef::Deneb( &state.latest_execution_payload_header, )), + BeaconState::Electra(state) => Ok(ExecutionPayloadHeaderRef::Electra( + &state.latest_execution_payload_header, + )), } } pub fn latest_execution_payload_header_mut( &mut self, - ) -> Result, Error> { + ) -> Result, Error> { match self { BeaconState::Base(_) | BeaconState::Altair(_) => Err(Error::IncorrectStateVariant), BeaconState::Merge(state) => Ok(ExecutionPayloadHeaderRefMut::Merge( @@ -778,6 +810,9 @@ impl BeaconState { BeaconState::Deneb(state) => Ok(ExecutionPayloadHeaderRefMut::Deneb( &mut state.latest_execution_payload_header, )), + BeaconState::Electra(state) => Ok(ExecutionPayloadHeaderRefMut::Electra( + &mut state.latest_execution_payload_header, + )), } } @@ -813,7 +848,7 @@ impl BeaconState { pub fn get_beacon_proposer_index(&self, slot: Slot, spec: &ChainSpec) -> Result { // Proposer indices are only known for the current epoch, due to the dependence on the // effective balances of validators, which change at every epoch transition. - let epoch = slot.epoch(T::slots_per_epoch()); + let epoch = slot.epoch(E::slots_per_epoch()); if epoch != self.current_epoch() { return Err(Error::SlotOutOfBounds); } @@ -834,7 +869,7 @@ impl BeaconState { let indices = self.get_active_validator_indices(self.current_epoch(), spec)?; self.current_epoch() - .slot_iter(T::slots_per_epoch()) + .slot_iter(E::slots_per_epoch()) .map(|slot| { let seed = self.get_beacon_proposer_seed(slot, spec)?; self.compute_proposer_index(&indices, &seed, spec) @@ -846,7 +881,7 @@ impl BeaconState { /// /// Spec v0.12.1 pub fn get_beacon_proposer_seed(&self, slot: Slot, spec: &ChainSpec) -> Result, Error> { - let epoch = slot.epoch(T::slots_per_epoch()); + let epoch = slot.epoch(E::slots_per_epoch()); let mut preimage = self .get_seed(epoch, Domain::BeaconProposer, spec)? .as_bytes() @@ -860,7 +895,7 @@ impl BeaconState { &self, epoch: Epoch, spec: &ChainSpec, - ) -> Result<&Arc>, Error> { + ) -> Result<&Arc>, Error> { let sync_committee_period = epoch.sync_committee_period(spec)?; let current_sync_committee_period = self.current_epoch().sync_committee_period(spec)?; let next_sync_committee_period = current_sync_committee_period.safe_add(1)?; @@ -880,16 +915,18 @@ impl BeaconState { /// Get the validator indices of all validators from `sync_committee`. pub fn get_sync_committee_indices( &mut self, - sync_committee: &SyncCommittee, + sync_committee: &SyncCommittee, ) -> Result, Error> { - let mut indices = Vec::with_capacity(sync_committee.pubkeys.len()); - for pubkey in sync_committee.pubkeys.iter() { - indices.push( - self.get_validator_index(pubkey)? - .ok_or(Error::PubkeyCacheInconsistent)?, - ) - } - Ok(indices) + self.update_pubkey_cache()?; + sync_committee + .pubkeys + .iter() + .map(|pubkey| { + self.pubkey_cache() + .get(pubkey) + .ok_or(Error::PubkeyCacheInconsistent) + }) + .collect() } /// Compute the sync committee indices for the next sync committee. @@ -902,8 +939,8 @@ impl BeaconState { let seed = self.get_seed(epoch, Domain::SyncCommittee, spec)?; let mut i = 0; - let mut sync_committee_indices = Vec::with_capacity(T::SyncCommitteeSize::to_usize()); - while sync_committee_indices.len() < T::SyncCommitteeSize::to_usize() { + let mut sync_committee_indices = Vec::with_capacity(E::SyncCommitteeSize::to_usize()); + while sync_committee_indices.len() < E::SyncCommitteeSize::to_usize() { let shuffled_index = compute_shuffled_index( i.safe_rem(active_validator_count)?, active_validator_count, @@ -929,7 +966,7 @@ impl BeaconState { } /// Compute the next sync committee. - pub fn get_next_sync_committee(&self, spec: &ChainSpec) -> Result, Error> { + pub fn get_next_sync_committee(&self, spec: &ChainSpec) -> Result, Error> { let sync_committee_indices = self.get_next_sync_committee_indices(spec)?; let pubkeys = sync_committee_indices @@ -962,10 +999,10 @@ impl BeaconState { epoch: Epoch, validator_indices: &[u64], spec: &ChainSpec, - ) -> Result>, Error> { + ) -> Result, Error>>, Error> { let sync_committee = self.get_built_sync_committee(epoch, spec)?; - validator_indices + Ok(validator_indices .iter() .map(|&validator_index| { let pubkey = self.get_validator(validator_index as usize)?.pubkey; @@ -976,7 +1013,7 @@ impl BeaconState { sync_committee, )) }) - .collect() + .collect()) } /// Get the canonical root of the `latest_block_header`, filling in its state root if necessary. @@ -1008,7 +1045,7 @@ impl BeaconState { /// Returns an iterator across the past block roots of `state` in descending slot-order. /// /// See the docs for `BlockRootsIter` for more detail. - pub fn rev_iter_block_roots<'a>(&'a self, spec: &ChainSpec) -> BlockRootsIter<'a, T> { + pub fn rev_iter_block_roots<'a>(&'a self, spec: &ChainSpec) -> BlockRootsIter<'a, E> { BlockRootsIter::new(self, spec.genesis_slot) } @@ -1024,7 +1061,7 @@ impl BeaconState { /// /// Note that the spec calls this `get_block_root`. pub fn get_block_root_at_epoch(&self, epoch: Epoch) -> Result<&Hash256, BeaconStateError> { - self.get_block_root(epoch.start_slot(T::slots_per_epoch())) + self.get_block_root(epoch.start_slot(E::slots_per_epoch())) } /// Sets the block root for some given slot. @@ -1055,7 +1092,7 @@ impl BeaconState { allow_next_epoch: AllowNextEpoch, ) -> Result { let current_epoch = self.current_epoch(); - let len = T::EpochsPerHistoricalVector::to_u64(); + let len = E::EpochsPerHistoricalVector::to_u64(); if current_epoch < epoch.safe_add(len)? && epoch <= allow_next_epoch.upper_bound_of(current_epoch)? @@ -1070,7 +1107,7 @@ impl BeaconState { pub fn min_randao_epoch(&self) -> Epoch { self.current_epoch() .saturating_add(1u64) - .saturating_sub(T::EpochsPerHistoricalVector::to_u64()) + .saturating_sub(E::EpochsPerHistoricalVector::to_u64()) } /// XOR-assigns the existing `epoch` randao mix with the hash of the `signature`. @@ -1081,7 +1118,7 @@ impl BeaconState { pub fn update_randao_mix(&mut self, epoch: Epoch, signature: &Signature) -> Result<(), Error> { let i = epoch .as_usize() - .safe_rem(T::EpochsPerHistoricalVector::to_usize())?; + .safe_rem(E::EpochsPerHistoricalVector::to_usize())?; let signature_hash = Hash256::from_slice(&hash(&ssz_encode(signature))); @@ -1164,12 +1201,12 @@ impl BeaconState { // We allow the slashings vector to be accessed at any cached epoch at or before // the current epoch, or the next epoch if `AllowNextEpoch::True` is passed. let current_epoch = self.current_epoch(); - if current_epoch < epoch.safe_add(T::EpochsPerSlashingsVector::to_u64())? + if current_epoch < epoch.safe_add(E::EpochsPerSlashingsVector::to_u64())? && epoch <= allow_next_epoch.upper_bound_of(current_epoch)? { Ok(epoch .as_usize() - .safe_rem(T::EpochsPerSlashingsVector::to_usize())?) + .safe_rem(E::EpochsPerSlashingsVector::to_usize())?) } else { Err(Error::EpochOutOfBounds) } @@ -1202,7 +1239,11 @@ impl BeaconState { /// Convenience accessor for validators and balances simultaneously. pub fn validators_and_balances_and_progressive_balances_mut( &mut self, - ) -> (&mut [Validator], &mut [u64], &mut ProgressiveBalancesCache) { + ) -> ( + &mut Validators, + &mut Balances, + &mut ProgressiveBalancesCache, + ) { match self { BeaconState::Base(state) => ( &mut state.validators, @@ -1229,6 +1270,82 @@ impl BeaconState { &mut state.balances, &mut state.progressive_balances_cache, ), + BeaconState::Electra(state) => ( + &mut state.validators, + &mut state.balances, + &mut state.progressive_balances_cache, + ), + } + } + + #[allow(clippy::type_complexity)] + pub fn mutable_validator_fields( + &mut self, + ) -> Result< + ( + &mut Validators, + &mut Balances, + &VariableList, + &VariableList, + &mut VariableList, + &mut ProgressiveBalancesCache, + &mut ExitCache, + &mut EpochCache, + ), + Error, + > { + match self { + BeaconState::Base(_) => Err(Error::IncorrectStateVariant), + BeaconState::Altair(state) => Ok(( + &mut state.validators, + &mut state.balances, + &state.previous_epoch_participation, + &state.current_epoch_participation, + &mut state.inactivity_scores, + &mut state.progressive_balances_cache, + &mut state.exit_cache, + &mut state.epoch_cache, + )), + BeaconState::Merge(state) => Ok(( + &mut state.validators, + &mut state.balances, + &state.previous_epoch_participation, + &state.current_epoch_participation, + &mut state.inactivity_scores, + &mut state.progressive_balances_cache, + &mut state.exit_cache, + &mut state.epoch_cache, + )), + BeaconState::Capella(state) => Ok(( + &mut state.validators, + &mut state.balances, + &state.previous_epoch_participation, + &state.current_epoch_participation, + &mut state.inactivity_scores, + &mut state.progressive_balances_cache, + &mut state.exit_cache, + &mut state.epoch_cache, + )), + BeaconState::Deneb(state) => Ok(( + &mut state.validators, + &mut state.balances, + &state.previous_epoch_participation, + &state.current_epoch_participation, + &mut state.inactivity_scores, + &mut state.progressive_balances_cache, + &mut state.exit_cache, + &mut state.epoch_cache, + )), + BeaconState::Electra(state) => Ok(( + &mut state.validators, + &mut state.balances, + &state.previous_epoch_participation, + &state.current_epoch_participation, + &mut state.inactivity_scores, + &mut state.progressive_balances_cache, + &mut state.exit_cache, + &mut state.epoch_cache, + )), } } @@ -1243,7 +1360,7 @@ impl BeaconState { // == 0`. let mix = { let i = epoch - .safe_add(T::EpochsPerHistoricalVector::to_u64())? + .safe_add(E::EpochsPerHistoricalVector::to_u64())? .safe_sub(spec.min_seed_lookahead)? .safe_sub(1)?; let i_mod = i.as_usize().safe_rem(self.randao_mixes().len())?; @@ -1321,14 +1438,12 @@ impl BeaconState { epoch: Epoch, spec: &ChainSpec, ) -> Result { - Ok(epoch.safe_add(1)?.safe_add(spec.max_seed_lookahead)?) + Ok(spec.compute_activation_exit_epoch(epoch)?) } /// Return the churn limit for the current epoch (number of validators who can leave per epoch). /// - /// Uses the epoch cache, and will error if it isn't initialized. - /// - /// Spec v0.12.1 + /// Uses the current epoch committee cache, and will error if it isn't initialized. pub fn get_churn_limit(&self, spec: &ChainSpec) -> Result { Ok(std::cmp::max( spec.min_per_epoch_churn_limit, @@ -1341,16 +1456,14 @@ impl BeaconState { /// Return the activation churn limit for the current epoch (number of validators who can enter per epoch). /// - /// Uses the epoch cache, and will error if it isn't initialized. - /// - /// Spec v1.4.0 + /// Uses the current epoch committee cache, and will error if it isn't initialized. pub fn get_activation_churn_limit(&self, spec: &ChainSpec) -> Result { Ok(match self { BeaconState::Base(_) | BeaconState::Altair(_) | BeaconState::Merge(_) | BeaconState::Capella(_) => self.get_churn_limit(spec)?, - BeaconState::Deneb(_) => std::cmp::min( + BeaconState::Deneb(_) | BeaconState::Electra(_) => std::cmp::min( spec.max_per_epoch_activation_churn_limit, self.get_churn_limit(spec)?, ), @@ -1373,20 +1486,22 @@ impl BeaconState { Ok(cache.get_attestation_duties(validator_index)) } - /// Implementation of `get_total_balance`, matching the spec. + /// Compute the total active balance cache from scratch. /// - /// Returns minimum `EFFECTIVE_BALANCE_INCREMENT`, to avoid div by 0. - pub fn get_total_balance<'a, I: IntoIterator>( - &'a self, - validator_indices: I, - spec: &ChainSpec, - ) -> Result { - let total_balance = validator_indices.into_iter().try_fold(0_u64, |acc, i| { - self.get_effective_balance(*i) - .and_then(|bal| Ok(acc.safe_add(bal)?)) - })?; + /// This method should rarely be invoked because single-pass epoch processing keeps the total + /// active balance cache up to date. + pub fn compute_total_active_balance_slow(&self, spec: &ChainSpec) -> Result { + let current_epoch = self.current_epoch(); + + let mut total_active_balance = 0; + + for validator in self.validators() { + if validator.is_active_at(current_epoch) { + total_active_balance.safe_add_assign(validator.effective_balance)?; + } + } Ok(std::cmp::max( - total_balance, + total_active_balance, spec.effective_balance_increment, )) } @@ -1398,33 +1513,54 @@ impl BeaconState { /// /// Returns minimum `EFFECTIVE_BALANCE_INCREMENT`, to avoid div by 0. pub fn get_total_active_balance(&self) -> Result { + self.get_total_active_balance_at_epoch(self.current_epoch()) + } + + /// Get the cached total active balance while checking that it is for the correct `epoch`. + pub fn get_total_active_balance_at_epoch(&self, epoch: Epoch) -> Result { let (initialized_epoch, balance) = self .total_active_balance() .ok_or(Error::TotalActiveBalanceCacheUninitialized)?; - let current_epoch = self.current_epoch(); - if initialized_epoch == current_epoch { + if initialized_epoch == epoch { Ok(balance) } else { Err(Error::TotalActiveBalanceCacheInconsistent { initialized_epoch, - current_epoch, + current_epoch: epoch, }) } } - /// Build the total active balance cache. + /// Manually set the total active balance. /// - /// This function requires the current committee cache to be already built. It is called - /// automatically when `build_committee_cache` is called for the current epoch. - fn build_total_active_balance_cache(&mut self, spec: &ChainSpec) -> Result<(), Error> { - // Order is irrelevant, so use the cached indices. - let current_epoch = self.current_epoch(); - let total_active_balance = self.get_total_balance( - self.get_cached_active_validator_indices(RelativeEpoch::Current)?, - spec, - )?; - *self.total_active_balance_mut() = Some((current_epoch, total_active_balance)); + /// This should only be called when the total active balance has been computed as part of + /// single-pass epoch processing (or `process_rewards_and_penalties` for phase0). + /// + /// This function will ensure the balance is never set to 0, thus conforming to the spec. + pub fn set_total_active_balance(&mut self, epoch: Epoch, balance: u64, spec: &ChainSpec) { + let safe_balance = std::cmp::max(balance, spec.effective_balance_increment); + *self.total_active_balance_mut() = Some((epoch, safe_balance)); + } + + /// Build the total active balance cache for the current epoch if it is not already built. + pub fn build_total_active_balance_cache(&mut self, spec: &ChainSpec) -> Result<(), Error> { + if self + .get_total_active_balance_at_epoch(self.current_epoch()) + .is_err() + { + self.force_build_total_active_balance_cache(spec)?; + } + Ok(()) + } + + /// Build the total active balance cache, even if it is already built. + pub fn force_build_total_active_balance_cache( + &mut self, + spec: &ChainSpec, + ) -> Result<(), Error> { + let total_active_balance = self.compute_total_active_balance_slow(spec)?; + *self.total_active_balance_mut() = Some((self.current_epoch(), total_active_balance)); Ok(()) } @@ -1437,22 +1573,26 @@ impl BeaconState { pub fn get_epoch_participation_mut( &mut self, epoch: Epoch, - ) -> Result<&mut VariableList, Error> { - if epoch == self.current_epoch() { + previous_epoch: Epoch, + current_epoch: Epoch, + ) -> Result<&mut VariableList, Error> { + if epoch == current_epoch { match self { BeaconState::Base(_) => Err(BeaconStateError::IncorrectStateVariant), BeaconState::Altair(state) => Ok(&mut state.current_epoch_participation), BeaconState::Merge(state) => Ok(&mut state.current_epoch_participation), BeaconState::Capella(state) => Ok(&mut state.current_epoch_participation), BeaconState::Deneb(state) => Ok(&mut state.current_epoch_participation), + BeaconState::Electra(state) => Ok(&mut state.current_epoch_participation), } - } else if epoch == self.previous_epoch() { + } else if epoch == previous_epoch { match self { BeaconState::Base(_) => Err(BeaconStateError::IncorrectStateVariant), BeaconState::Altair(state) => Ok(&mut state.previous_epoch_participation), BeaconState::Merge(state) => Ok(&mut state.previous_epoch_participation), BeaconState::Capella(state) => Ok(&mut state.previous_epoch_participation), BeaconState::Deneb(state) => Ok(&mut state.previous_epoch_participation), + BeaconState::Electra(state) => Ok(&mut state.previous_epoch_participation), } } else { Err(BeaconStateError::EpochOutOfBounds) @@ -1477,6 +1617,7 @@ impl BeaconState { self.build_all_committee_caches(spec)?; self.update_pubkey_cache()?; self.build_exit_cache(spec)?; + self.build_slashings_cache()?; Ok(()) } @@ -1497,6 +1638,20 @@ impl BeaconState { Ok(()) } + /// Build the slashings cache if it needs to be built. + pub fn build_slashings_cache(&mut self) -> Result<(), Error> { + let latest_block_slot = self.latest_block_header().slot; + if !self.slashings_cache().is_initialized(latest_block_slot) { + *self.slashings_cache_mut() = SlashingsCache::new(latest_block_slot, self.validators()); + } + Ok(()) + } + + pub fn slashings_cache_is_initialized(&self) -> bool { + let latest_block_slot = self.latest_block_header().slot; + self.slashings_cache().is_initialized(latest_block_slot) + } + /// Drop all caches on the state. pub fn drop_all_caches(&mut self) -> Result<(), Error> { self.drop_total_active_balance_cache(); @@ -1507,6 +1662,8 @@ impl BeaconState { self.drop_tree_hash_cache(); self.drop_progressive_balances_cache(); *self.exit_cache_mut() = ExitCache::default(); + *self.slashings_cache_mut() = SlashingsCache::default(); + *self.epoch_cache_mut() = EpochCache::default(); Ok(()) } @@ -1519,7 +1676,7 @@ impl BeaconState { }) } - /// Build an epoch cache, unless it is has already been built. + /// Build a committee cache, unless it is has already been built. pub fn build_committee_cache( &mut self, relative_epoch: RelativeEpoch, @@ -1540,7 +1697,7 @@ impl BeaconState { Ok(()) } - /// Always builds the previous epoch cache, even if it is already initialized. + /// Always builds the requested committee cache, even if it is already initialized. pub fn force_build_committee_cache( &mut self, relative_epoch: RelativeEpoch, @@ -1569,42 +1726,17 @@ impl BeaconState { /// /// This should be used if the `slot` of this state is advanced beyond an epoch boundary. /// - /// Note: this function will not build any new committee caches, but will build the total - /// balance cache if the (new) current epoch cache is initialized. - pub fn advance_caches(&mut self, spec: &ChainSpec) -> Result<(), Error> { + /// Note: this function will not build any new committee caches, nor will it update the total + /// active balance cache. The total active balance cache must be updated separately. + pub fn advance_caches(&mut self) -> Result<(), Error> { self.committee_caches_mut().rotate_left(1); - // Re-compute total active balance for current epoch. - // - // This can only be computed once the state's effective balances have been updated - // for the current epoch. I.e. it is not possible to know this value with the same - // lookahead as the committee shuffling. - let curr = Self::committee_cache_index(RelativeEpoch::Current); - let curr_cache = mem::take(self.committee_cache_at_index_mut(curr)?); - - // If current epoch cache is initialized, compute the total active balance from its - // indices. We check that the cache is initialized at the _next_ epoch because the slot has - // not yet been advanced. - let new_current_epoch = self.next_epoch()?; - if curr_cache.is_initialized_at(new_current_epoch) { - *self.total_active_balance_mut() = Some(( - new_current_epoch, - self.get_total_balance(curr_cache.active_validator_indices(), spec)?, - )); - } - // If the cache is not initialized, then the previous cached value for the total balance is - // wrong, so delete it. - else { - self.drop_total_active_balance_cache(); - } - *self.committee_cache_at_index_mut(curr)? = curr_cache; - let next = Self::committee_cache_index(RelativeEpoch::Next); *self.committee_cache_at_index_mut(next)? = CommitteeCache::default(); Ok(()) } - fn committee_cache_index(relative_epoch: RelativeEpoch) -> usize { + pub(crate) fn committee_cache_index(relative_epoch: RelativeEpoch) -> usize { match relative_epoch { RelativeEpoch::Previous => 0, RelativeEpoch::Current => 1, @@ -1616,7 +1748,7 @@ impl BeaconState { /// /// Return an error if the cache for the slot's epoch is not initialized. fn committee_cache_at_slot(&self, slot: Slot) -> Result<&CommitteeCache, Error> { - let epoch = slot.epoch(T::slots_per_epoch()); + let epoch = slot.epoch(E::slots_per_epoch()); let relative_epoch = RelativeEpoch::from_epoch(self.current_epoch(), epoch)?; self.committee_cache(relative_epoch) } @@ -1765,9 +1897,11 @@ impl BeaconState { BeaconState::Merge(inner) => BeaconState::Merge(inner.clone()), BeaconState::Capella(inner) => BeaconState::Capella(inner.clone()), BeaconState::Deneb(inner) => BeaconState::Deneb(inner.clone()), + BeaconState::Electra(inner) => BeaconState::Electra(inner.clone()), }; if config.committee_caches { - *res.committee_caches_mut() = self.committee_caches().clone(); + res.committee_caches_mut() + .clone_from(self.committee_caches()); *res.total_active_balance_mut() = *self.total_active_balance(); } if config.pubkey_cache { @@ -1776,6 +1910,9 @@ impl BeaconState { if config.exit_cache { *res.exit_cache_mut() = self.exit_cache().clone(); } + if config.slashings_cache { + *res.slashings_cache_mut() = self.slashings_cache().clone(); + } if config.tree_hash_cache { *res.tree_hash_cache_mut() = self.tree_hash_cache().clone(); } @@ -1794,9 +1931,8 @@ impl BeaconState { pub fn is_eligible_validator( &self, previous_epoch: Epoch, - val_index: usize, + val: &Validator, ) -> Result { - 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)) } @@ -1820,11 +1956,11 @@ impl BeaconState { pub fn get_sync_committee_for_next_slot( &self, spec: &ChainSpec, - ) -> Result>, Error> { + ) -> Result>, Error> { let next_slot_epoch = self .slot() .saturating_add(Slot::new(1)) - .epoch(T::slots_per_epoch()); + .epoch(E::slots_per_epoch()); let sync_committee = if self.current_epoch().sync_committee_period(spec) == next_slot_epoch.sync_committee_period(spec) @@ -1836,6 +1972,13 @@ impl BeaconState { Ok(sync_committee) } + /// Get the base reward for `validator_index` from the epoch cache. + /// + /// This function will error if the epoch cache is not initialized. + pub fn get_base_reward(&self, validator_index: usize) -> Result { + self.epoch_cache().get_base_reward(validator_index) + } + pub fn compute_merkle_proof( &mut self, generalized_index: usize, @@ -1936,7 +2079,7 @@ fn clone_default(_value: &T) -> T { T::default() } -impl CompareFields for BeaconState { +impl CompareFields for BeaconState { fn compare_fields(&self, other: &Self) -> Vec { match (self, other) { (BeaconState::Base(x), BeaconState::Base(y)) => x.compare_fields(y), @@ -1944,12 +2087,13 @@ impl CompareFields for BeaconState { (BeaconState::Merge(x), BeaconState::Merge(y)) => x.compare_fields(y), (BeaconState::Capella(x), BeaconState::Capella(y)) => x.compare_fields(y), (BeaconState::Deneb(x), BeaconState::Deneb(y)) => x.compare_fields(y), + (BeaconState::Electra(x), BeaconState::Electra(y)) => x.compare_fields(y), _ => panic!("compare_fields: mismatched state variants",), } } } -impl ForkVersionDeserialize for BeaconState { +impl ForkVersionDeserialize for BeaconState { fn deserialize_by_fork<'de, D: serde::Deserializer<'de>>( value: serde_json::value::Value, fork_name: ForkName, diff --git a/consensus/types/src/beacon_state/clone_config.rs b/consensus/types/src/beacon_state/clone_config.rs index c6e7f47421..27e066d5db 100644 --- a/consensus/types/src/beacon_state/clone_config.rs +++ b/consensus/types/src/beacon_state/clone_config.rs @@ -4,6 +4,7 @@ pub struct CloneConfig { pub committee_caches: bool, pub pubkey_cache: bool, pub exit_cache: bool, + pub slashings_cache: bool, pub tree_hash_cache: bool, pub progressive_balances_cache: bool, } @@ -14,6 +15,7 @@ impl CloneConfig { committee_caches: true, pubkey_cache: true, exit_cache: true, + slashings_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 8d29bc2217..a6b12cf5af 100644 --- a/consensus/types/src/beacon_state/committee_cache.rs +++ b/consensus/types/src/beacon_state/committee_cache.rs @@ -1,8 +1,8 @@ #![allow(clippy::arithmetic_side_effects)] -use super::BeaconState; use crate::*; use core::num::NonZeroUsize; +use derivative::Derivative; use safe_arith::SafeArith; use serde::{Deserialize, Serialize}; use ssz::{four_byte_option_impl, Decode, DecodeError, Encode}; @@ -19,22 +19,50 @@ four_byte_option_impl!(four_byte_option_non_zero_usize, NonZeroUsize); /// Computes and stores the shuffling for an epoch. Provides various getters to allow callers to /// read the committees for the given epoch. -#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, Encode, Decode)] +#[derive(Derivative, Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)] +#[derivative(PartialEq)] pub struct CommitteeCache { #[ssz(with = "four_byte_option_epoch")] initialized_epoch: Option, shuffling: Vec, + #[derivative(PartialEq(compare_with = "compare_shuffling_positions"))] shuffling_positions: Vec, committees_per_slot: u64, slots_per_epoch: u64, } +/// Equivalence function for `shuffling_positions` that ignores trailing `None` entries. +/// +/// It can happen that states from different epochs computing the same cache have different +/// numbers of validators in `state.validators()` due to recent deposits. These new validators +/// cannot be active however and will always be omitted from the shuffling. This function checks +/// that two lists of shuffling positions are equivalent by ensuring that they are identical on all +/// common entries, and that new entries at the end are all `None`. +/// +/// In practice this is only used in tests. +#[allow(clippy::indexing_slicing)] +fn compare_shuffling_positions(xs: &Vec, ys: &Vec) -> bool { + use std::cmp::Ordering; + + let (shorter, longer) = match xs.len().cmp(&ys.len()) { + Ordering::Equal => { + return xs == ys; + } + Ordering::Less => (xs, ys), + Ordering::Greater => (ys, xs), + }; + shorter == &longer[..shorter.len()] + && longer[shorter.len()..] + .iter() + .all(|new| *new == NonZeroUsizeOption(None)) +} + impl CommitteeCache { /// Return a new, fully initialized cache. /// /// Spec v0.12.1 - pub fn initialized( - state: &BeaconState, + pub fn initialized( + state: &BeaconState, epoch: Epoch, spec: &ChainSpec, ) -> Result { @@ -52,7 +80,7 @@ impl CommitteeCache { } // May cause divide-by-zero errors. - if T::slots_per_epoch() == 0 { + if E::slots_per_epoch() == 0 { return Err(Error::ZeroSlotsPerEpoch); } @@ -68,7 +96,7 @@ impl CommitteeCache { } let committees_per_slot = - T::get_committee_count_per_slot(active_validator_indices.len(), spec)? as u64; + E::get_committee_count_per_slot(active_validator_indices.len(), spec)? as u64; let seed = state.get_seed(epoch, Domain::BeaconAttester, spec)?; @@ -92,7 +120,7 @@ impl CommitteeCache { shuffling, shuffling_positions, committees_per_slot, - slots_per_epoch: T::slots_per_epoch(), + slots_per_epoch: E::slots_per_epoch(), }) } @@ -322,17 +350,21 @@ pub fn epoch_committee_count(committees_per_slot: usize, slots_per_epoch: usize) /// `epoch`. /// /// Spec v0.12.1 -pub fn get_active_validator_indices(validators: &[Validator], epoch: Epoch) -> Vec { - let mut active = Vec::with_capacity(validators.len()); +pub fn get_active_validator_indices<'a, V, I>(validators: V, epoch: Epoch) -> Vec +where + V: IntoIterator, + I: ExactSizeIterator + Iterator, +{ + let iter = validators.into_iter(); - for (index, validator) in validators.iter().enumerate() { + let mut active = Vec::with_capacity(iter.len()); + + for (index, validator) in iter.enumerate() { if validator.is_active_at(epoch) { active.push(index) } } - active.shrink_to_fit(); - active } diff --git a/consensus/types/src/beacon_state/committee_cache/tests.rs b/consensus/types/src/beacon_state/committee_cache/tests.rs index 11cc6095da..a5effb9363 100644 --- a/consensus/types/src/beacon_state/committee_cache/tests.rs +++ b/consensus/types/src/beacon_state/committee_cache/tests.rs @@ -34,7 +34,7 @@ fn default_values() { assert!(cache.get_beacon_committees_at_slot(Slot::new(0)).is_err()); } -async fn new_state(validator_count: usize, slot: Slot) -> BeaconState { +async fn new_state(validator_count: usize, slot: Slot) -> BeaconState { let harness = get_harness(validator_count); let head_state = harness.get_current_state(); if slot > Slot::new(0) { diff --git a/consensus/types/src/beacon_state/exit_cache.rs b/consensus/types/src/beacon_state/exit_cache.rs index cb96fba691..bda788e63b 100644 --- a/consensus/types/src/beacon_state/exit_cache.rs +++ b/consensus/types/src/beacon_state/exit_cache.rs @@ -1,13 +1,17 @@ use super::{BeaconStateError, ChainSpec, Epoch, Validator}; use safe_arith::SafeArith; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::cmp::Ordering; /// Map from exit epoch to the number of validators with that exit epoch. #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] pub struct ExitCache { + /// True if the cache has been initialized. initialized: bool, - exit_epoch_counts: HashMap, + /// Maximum `exit_epoch` of any validator. + max_exit_epoch: Epoch, + /// Number of validators known to be exiting at `max_exit_epoch`. + max_exit_epoch_churn: u64, } impl ExitCache { @@ -15,7 +19,8 @@ impl ExitCache { pub fn new(validators: &[Validator], spec: &ChainSpec) -> Result { let mut exit_cache = ExitCache { initialized: true, - ..ExitCache::default() + max_exit_epoch: Epoch::new(0), + max_exit_epoch_churn: 0, }; // Add all validators with a non-default exit epoch to the cache. validators @@ -37,27 +42,44 @@ impl ExitCache { /// Record the exit epoch of a validator. Must be called only once per exiting validator. pub fn record_validator_exit(&mut self, exit_epoch: Epoch) -> Result<(), BeaconStateError> { self.check_initialized()?; - self.exit_epoch_counts - .entry(exit_epoch) - .or_insert(0) - .safe_add_assign(1)?; + match exit_epoch.cmp(&self.max_exit_epoch) { + // Update churn for the current maximum epoch. + Ordering::Equal => { + self.max_exit_epoch_churn.safe_add_assign(1)?; + } + // Increase the max exit epoch, reset the churn to 1. + Ordering::Greater => { + self.max_exit_epoch = exit_epoch; + self.max_exit_epoch_churn = 1; + } + // Older exit epochs are not relevant. + Ordering::Less => (), + } Ok(()) } /// Get the largest exit epoch with a non-zero exit epoch count. pub fn max_epoch(&self) -> Result, BeaconStateError> { self.check_initialized()?; - Ok(self.exit_epoch_counts.keys().max().cloned()) + Ok((self.max_exit_epoch_churn > 0).then_some(self.max_exit_epoch)) } /// Get number of validators with the given exit epoch. (Return 0 for the default exit epoch.) pub fn get_churn_at(&self, exit_epoch: Epoch) -> Result { self.check_initialized()?; - Ok(self - .exit_epoch_counts - .get(&exit_epoch) - .cloned() - .unwrap_or(0)) + match exit_epoch.cmp(&self.max_exit_epoch) { + // Epochs are equal, we know the churn exactly. + Ordering::Equal => Ok(self.max_exit_epoch_churn), + // If exiting at an epoch later than the cached epoch then the churn is 0. This is a + // common case which happens when there are no exits for an epoch. + Ordering::Greater => Ok(0), + // Consensus code should never require the churn at an epoch prior to the cached epoch. + // That's a bug. + Ordering::Less => Err(BeaconStateError::ExitCacheInvalidEpoch { + max_exit_epoch: self.max_exit_epoch, + request_epoch: exit_epoch, + }), + } } } diff --git a/consensus/types/src/beacon_state/iter.rs b/consensus/types/src/beacon_state/iter.rs index 2c00913ce9..2d3ad02c83 100644 --- a/consensus/types/src/beacon_state/iter.rs +++ b/consensus/types/src/beacon_state/iter.rs @@ -8,17 +8,17 @@ use crate::*; /// - Will not return slots prior to the genesis_slot. /// - Each call to next will result in a slot one less than the prior one (or `None`). /// - Skipped slots will contain the block root from the prior non-skipped slot. -pub struct BlockRootsIter<'a, T: EthSpec> { - state: &'a BeaconState, +pub struct BlockRootsIter<'a, E: EthSpec> { + state: &'a BeaconState, genesis_slot: Slot, prev: Slot, } -impl<'a, T: EthSpec> BlockRootsIter<'a, T> { +impl<'a, E: EthSpec> BlockRootsIter<'a, E> { /// Instantiates a new iterator, returning roots for slots earlier that `state.slot`. /// /// See the struct-level documentation for more details. - pub fn new(state: &'a BeaconState, genesis_slot: Slot) -> Self { + pub fn new(state: &'a BeaconState, genesis_slot: Slot) -> Self { Self { state, genesis_slot, @@ -27,7 +27,7 @@ impl<'a, T: EthSpec> BlockRootsIter<'a, T> { } } -impl<'a, T: EthSpec> Iterator for BlockRootsIter<'a, T> { +impl<'a, E: EthSpec> Iterator for BlockRootsIter<'a, E> { type Item = Result<(Slot, Hash256), Error>; fn next(&mut self) -> Option { diff --git a/consensus/types/src/beacon_state/progressive_balances_cache.rs b/consensus/types/src/beacon_state/progressive_balances_cache.rs index 6c0682480b..523c94cf57 100644 --- a/consensus/types/src/beacon_state/progressive_balances_cache.rs +++ b/consensus/types/src/beacon_state/progressive_balances_cache.rs @@ -1,9 +1,13 @@ use crate::beacon_state::balance::Balance; -use crate::{BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec}; +use crate::{ + consts::altair::{ + NUM_FLAG_INDICES, TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX, + TIMELY_TARGET_FLAG_INDEX, + }, + BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, ParticipationFlags, +}; use arbitrary::Arbitrary; use safe_arith::SafeArith; -use serde::{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 @@ -17,21 +21,120 @@ pub struct ProgressiveBalancesCache { #[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, + pub previous_epoch_cache: EpochTotalBalances, + pub current_epoch_cache: EpochTotalBalances, +} + +/// Caches the participation values for one epoch (either the previous or current). +#[derive(PartialEq, Debug, Clone, Arbitrary)] +pub struct EpochTotalBalances { + /// Stores the sum of the balances for all validators in `self.unslashed_participating_indices` + /// for all flags in `NUM_FLAG_INDICES`. + /// + /// A flag balance is only incremented if a validator is in that flag set. + pub total_flag_balances: [Balance; NUM_FLAG_INDICES], +} + +impl EpochTotalBalances { + pub fn new(spec: &ChainSpec) -> Self { + let zero_balance = Balance::zero(spec.effective_balance_increment); + + Self { + total_flag_balances: [zero_balance; NUM_FLAG_INDICES], + } + } + + /// Returns the total balance of attesters who have `flag_index` set. + pub fn total_flag_balance(&self, flag_index: usize) -> Result { + self.total_flag_balances + .get(flag_index) + .map(Balance::get) + .ok_or(BeaconStateError::InvalidFlagIndex(flag_index)) + } + + /// Returns the raw total balance of attesters who have `flag_index` set. + pub fn total_flag_balance_raw(&self, flag_index: usize) -> Result { + self.total_flag_balances + .get(flag_index) + .copied() + .ok_or(BeaconStateError::InvalidFlagIndex(flag_index)) + } + + pub fn on_new_attestation( + &mut self, + is_slashed: bool, + flag_index: usize, + validator_effective_balance: u64, + ) -> Result<(), BeaconStateError> { + if is_slashed { + return Ok(()); + } + let balance = self + .total_flag_balances + .get_mut(flag_index) + .ok_or(BeaconStateError::InvalidFlagIndex(flag_index))?; + balance.safe_add_assign(validator_effective_balance)?; + Ok(()) + } + + pub fn on_slashing( + &mut self, + participation_flags: ParticipationFlags, + validator_effective_balance: u64, + ) -> Result<(), BeaconStateError> { + for flag_index in 0..NUM_FLAG_INDICES { + if participation_flags.has_flag(flag_index)? { + self.total_flag_balances + .get_mut(flag_index) + .ok_or(BeaconStateError::InvalidFlagIndex(flag_index))? + .safe_sub_assign(validator_effective_balance)?; + } + } + Ok(()) + } + + pub fn on_effective_balance_change( + &mut self, + is_slashed: bool, + current_epoch_participation_flags: ParticipationFlags, + old_effective_balance: u64, + new_effective_balance: u64, + ) -> Result<(), BeaconStateError> { + // If the validator is slashed then we should not update the effective balance, because this + // validator's effective balance has already been removed from the totals. + if is_slashed { + return Ok(()); + } + for flag_index in 0..NUM_FLAG_INDICES { + if current_epoch_participation_flags.has_flag(flag_index)? { + let total = self + .total_flag_balances + .get_mut(flag_index) + .ok_or(BeaconStateError::InvalidFlagIndex(flag_index))?; + if new_effective_balance > old_effective_balance { + total + .safe_add_assign(new_effective_balance.safe_sub(old_effective_balance)?)?; + } else { + total + .safe_sub_assign(old_effective_balance.safe_sub(new_effective_balance)?)?; + } + } + } + Ok(()) + } } impl ProgressiveBalancesCache { pub fn initialize( &mut self, current_epoch: Epoch, - previous_epoch_target_attesting_balance: Balance, - current_epoch_target_attesting_balance: Balance, + previous_epoch_cache: EpochTotalBalances, + current_epoch_cache: EpochTotalBalances, ) { self.inner = Some(Inner { current_epoch, - previous_epoch_target_attesting_balance, - current_epoch_target_attesting_balance, + previous_epoch_cache, + current_epoch_cache, }); } @@ -39,24 +142,36 @@ impl ProgressiveBalancesCache { self.inner.is_some() } + pub fn is_initialized_at(&self, epoch: Epoch) -> bool { + self.inner + .as_ref() + .map_or(false, |inner| inner.current_epoch == epoch) + } + /// 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( + pub fn on_new_attestation( &mut self, epoch: Epoch, + is_slashed: bool, + flag_index: usize, 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)?; + cache.current_epoch_cache.on_new_attestation( + is_slashed, + flag_index, + validator_effective_balance, + )?; } else if epoch.safe_add(1)? == cache.current_epoch { - cache - .previous_epoch_target_attesting_balance - .safe_add_assign(validator_effective_balance)?; + cache.previous_epoch_cache.on_new_attestation( + is_slashed, + flag_index, + validator_effective_balance, + )?; } else { return Err(BeaconStateError::ProgressiveBalancesCacheInconsistent); } @@ -68,21 +183,17 @@ impl ProgressiveBalancesCache { /// 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, + previous_epoch_participation: ParticipationFlags, + current_epoch_participation: ParticipationFlags, 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)?; - } + cache + .previous_epoch_cache + .on_slashing(previous_epoch_participation, effective_balance)?; + cache + .current_epoch_cache + .on_slashing(current_epoch_participation, effective_balance)?; Ok(()) } @@ -90,22 +201,18 @@ impl ProgressiveBalancesCache { /// its share of the target attesting balance in the cache. pub fn on_effective_balance_change( &mut self, - is_current_epoch_target_attester: bool, + is_slashed: bool, + current_epoch_participation: ParticipationFlags, 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)?)?; - } - } + cache.current_epoch_cache.on_effective_balance_change( + is_slashed, + current_epoch_participation, + old_effective_balance, + new_effective_balance, + )?; Ok(()) } @@ -114,25 +221,53 @@ impl ProgressiveBalancesCache { 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); + cache.previous_epoch_cache = std::mem::replace( + &mut cache.current_epoch_cache, + EpochTotalBalances::new(spec), + ); Ok(()) } + pub fn previous_epoch_flag_attesting_balance( + &self, + flag_index: usize, + ) -> Result { + self.get_inner()? + .previous_epoch_cache + .total_flag_balance(flag_index) + } + + pub fn current_epoch_flag_attesting_balance( + &self, + flag_index: usize, + ) -> Result { + self.get_inner()? + .current_epoch_cache + .total_flag_balance(flag_index) + } + + pub fn previous_epoch_source_attesting_balance(&self) -> Result { + self.previous_epoch_flag_attesting_balance(TIMELY_SOURCE_FLAG_INDEX) + } + pub fn previous_epoch_target_attesting_balance(&self) -> Result { - Ok(self - .get_inner()? - .previous_epoch_target_attesting_balance - .get()) + self.previous_epoch_flag_attesting_balance(TIMELY_TARGET_FLAG_INDEX) + } + + pub fn previous_epoch_head_attesting_balance(&self) -> Result { + self.previous_epoch_flag_attesting_balance(TIMELY_HEAD_FLAG_INDEX) + } + + pub fn current_epoch_source_attesting_balance(&self) -> Result { + self.current_epoch_flag_attesting_balance(TIMELY_SOURCE_FLAG_INDEX) } pub fn current_epoch_target_attesting_balance(&self) -> Result { - Ok(self - .get_inner()? - .current_epoch_target_attesting_balance - .get()) + self.current_epoch_flag_attesting_balance(TIMELY_TARGET_FLAG_INDEX) + } + + pub fn current_epoch_head_attesting_balance(&self) -> Result { + self.current_epoch_flag_attesting_balance(TIMELY_HEAD_FLAG_INDEX) } fn get_inner_mut(&mut self) -> Result<&mut Inner, BeaconStateError> { @@ -148,40 +283,14 @@ impl ProgressiveBalancesCache { } } -#[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`. +/// `ProgressiveBalancesCache` is only enabled from `Altair` as it uses Altair-specific logic. pub fn is_progressive_balances_enabled(state: &BeaconState) -> bool { match state { BeaconState::Base(_) => false, BeaconState::Altair(_) | BeaconState::Merge(_) | BeaconState::Capella(_) - | BeaconState::Deneb(_) => true, + | BeaconState::Deneb(_) + | BeaconState::Electra(_) => true, } } diff --git a/consensus/types/src/beacon_state/pubkey_cache.rs b/consensus/types/src/beacon_state/pubkey_cache.rs index c56c9077e1..0b61ea3c5f 100644 --- a/consensus/types/src/beacon_state/pubkey_cache.rs +++ b/consensus/types/src/beacon_state/pubkey_cache.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; type ValidatorIndex = usize; +#[allow(clippy::len_without_is_empty)] #[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)] pub struct PubkeyCache { /// Maintain the number of keys added to the map. It is not sufficient to just use the HashMap diff --git a/consensus/types/src/beacon_state/slashings_cache.rs b/consensus/types/src/beacon_state/slashings_cache.rs new file mode 100644 index 0000000000..cfdc349f86 --- /dev/null +++ b/consensus/types/src/beacon_state/slashings_cache.rs @@ -0,0 +1,63 @@ +use crate::{BeaconStateError, Slot, Validator}; +use arbitrary::Arbitrary; +use std::collections::HashSet; + +/// Persistent (cheap to clone) cache of all slashed validator indices. +#[derive(Debug, Default, Clone, PartialEq, Arbitrary)] +pub struct SlashingsCache { + latest_block_slot: Option, + #[arbitrary(default)] + slashed_validators: HashSet, +} + +impl SlashingsCache { + /// Initialize a new cache for the given list of validators. + pub fn new<'a, V, I>(latest_block_slot: Slot, validators: V) -> Self + where + V: IntoIterator, + I: ExactSizeIterator + Iterator, + { + let slashed_validators = validators + .into_iter() + .enumerate() + .filter_map(|(i, validator)| validator.slashed.then_some(i)) + .collect(); + Self { + latest_block_slot: Some(latest_block_slot), + slashed_validators, + } + } + + pub fn is_initialized(&self, slot: Slot) -> bool { + self.latest_block_slot == Some(slot) + } + + pub fn check_initialized(&self, latest_block_slot: Slot) -> Result<(), BeaconStateError> { + if self.is_initialized(latest_block_slot) { + Ok(()) + } else { + Err(BeaconStateError::SlashingsCacheUninitialized { + initialized_slot: self.latest_block_slot, + latest_block_slot, + }) + } + } + + pub fn record_validator_slashing( + &mut self, + block_slot: Slot, + validator_index: usize, + ) -> Result<(), BeaconStateError> { + self.check_initialized(block_slot)?; + self.slashed_validators.insert(validator_index); + Ok(()) + } + + pub fn is_slashed(&self, validator_index: usize) -> bool { + self.slashed_validators.contains(&validator_index) + } + + pub fn update_latest_block_slot(&mut self, latest_block_slot: Slot) { + self.latest_block_slot = Some(latest_block_slot); + } +} diff --git a/consensus/types/src/beacon_state/tests.rs b/consensus/types/src/beacon_state/tests.rs index 6cd9c1dbf8..00625a1788 100644 --- a/consensus/types/src/beacon_state/tests.rs +++ b/consensus/types/src/beacon_state/tests.rs @@ -1,6 +1,5 @@ #![cfg(test)] use crate::test_utils::*; -use crate::test_utils::{SeedableRng, XorShiftRng}; use beacon_chain::test_utils::{ interop_genesis_state_with_eth1, test_spec, BeaconChainHarness, EphemeralHarnessType, DEFAULT_ETH1_BLOCK_HASH, @@ -60,12 +59,12 @@ async fn build_state(validator_count: usize) -> BeaconState { .head_beacon_state_cloned() } -async fn test_beacon_proposer_index() { - let spec = T::default_spec(); +async fn test_beacon_proposer_index() { + let spec = E::default_spec(); // Get the i'th candidate proposer for the given state and slot - let ith_candidate = |state: &BeaconState, slot: Slot, i: usize, spec: &ChainSpec| { - let epoch = slot.epoch(T::slots_per_epoch()); + let ith_candidate = |state: &BeaconState, slot: Slot, i: usize, spec: &ChainSpec| { + let epoch = slot.epoch(E::slots_per_epoch()); let seed = state.get_beacon_proposer_seed(slot, spec).unwrap(); let active_validators = state.get_active_validator_indices(epoch, spec).unwrap(); active_validators[compute_shuffled_index( @@ -78,7 +77,7 @@ async fn test_beacon_proposer_index() { }; // Run a test on the state. - let test = |state: &BeaconState, slot: Slot, candidate_index: usize| { + let test = |state: &BeaconState, slot: Slot, candidate_index: usize| { assert_eq!( state.get_beacon_proposer_index(slot, &spec), Ok(ith_candidate(state, slot, candidate_index, &spec)) @@ -87,24 +86,24 @@ async fn test_beacon_proposer_index() { // Test where we have one validator per slot. // 0th candidate should be chosen every time. - let state = build_state(T::slots_per_epoch() as usize).await; - for i in 0..T::slots_per_epoch() { + let state = build_state(E::slots_per_epoch() as usize).await; + for i in 0..E::slots_per_epoch() { test(&state, Slot::from(i), 0); } // Test where we have two validators per slot. // 0th candidate should be chosen every time. - let state = build_state((T::slots_per_epoch() as usize).mul(2)).await; - for i in 0..T::slots_per_epoch() { + let state = build_state((E::slots_per_epoch() as usize).mul(2)).await; + for i in 0..E::slots_per_epoch() { test(&state, Slot::from(i), 0); } // Test with two validators per slot, first validator has zero balance. - let mut state = build_state::((T::slots_per_epoch() as usize).mul(2)).await; + let mut state = build_state::((E::slots_per_epoch() as usize).mul(2)).await; let slot0_candidate0 = ith_candidate(&state, Slot::new(0), 0, &spec); state.validators_mut()[slot0_candidate0].effective_balance = 0; test(&state, Slot::new(0), 1); - for i in 1..T::slots_per_epoch() { + for i in 1..E::slots_per_epoch() { test(&state, Slot::from(i), 0); } } @@ -119,14 +118,14 @@ async fn beacon_proposer_index() { /// 1. Using the cache before it's built fails. /// 2. Using the cache after it's build passes. /// 3. Using the cache after it's dropped fails. -fn test_cache_initialization( - state: &mut BeaconState, +fn test_cache_initialization( + state: &mut BeaconState, relative_epoch: RelativeEpoch, spec: &ChainSpec, ) { let slot = relative_epoch - .into_epoch(state.slot().epoch(T::slots_per_epoch())) - .start_slot(T::slots_per_epoch()); + .into_epoch(state.slot().epoch(E::slots_per_epoch())) + .start_slot(E::slots_per_epoch()); // Build the cache. state.build_committee_cache(relative_epoch, spec).unwrap(); @@ -224,13 +223,14 @@ async fn clone_config() { .update_tree_hash_cache() .expect("should update tree hash cache"); - let num_caches = 5; + let num_caches = 6; 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, + slashings_cache: ((i >> 3) & 1) != 0, + tree_hash_cache: ((i >> 4) & 1) != 0, + progressive_balances_cache: ((i >> 5) & 1) != 0, }); for config in all_configs { @@ -245,8 +245,8 @@ mod committees { use std::ops::{Add, Div}; use swap_or_not_shuffle::shuffle_list; - fn execute_committee_consistency_test( - state: BeaconState, + fn execute_committee_consistency_test( + state: BeaconState, epoch: Epoch, validator_count: usize, spec: &ChainSpec, @@ -271,7 +271,7 @@ mod committees { let mut expected_indices_iter = shuffling.iter(); // Loop through all slots in the epoch being tested. - for slot in epoch.slot_iter(T::slots_per_epoch()) { + for slot in epoch.slot_iter(E::slots_per_epoch()) { let beacon_committees = state.get_beacon_committees_at_slot(slot).unwrap(); // Assert that the number of committees in this slot is consistent with the reported number @@ -281,7 +281,7 @@ mod committees { state .get_epoch_committee_count(relative_epoch) .unwrap() - .div(T::slots_per_epoch()) + .div(E::slots_per_epoch()) ); for (committee_index, bc) in beacon_committees.iter().enumerate() { @@ -317,18 +317,18 @@ mod committees { assert!(expected_indices_iter.next().is_none()); } - async fn committee_consistency_test( + async fn committee_consistency_test( validator_count: usize, state_epoch: Epoch, cache_epoch: RelativeEpoch, ) { - let spec = &T::default_spec(); + let spec = &E::default_spec(); - let slot = state_epoch.start_slot(T::slots_per_epoch()); - let harness = get_harness::(validator_count, slot).await; + let slot = state_epoch.start_slot(E::slots_per_epoch()); + let harness = get_harness::(validator_count, slot).await; let mut new_head_state = harness.get_current_state(); - let distinct_hashes: Vec = (0..T::epochs_per_historical_vector()) + let distinct_hashes: Vec = (0..E::epochs_per_historical_vector()) .map(|i| Hash256::from_low_u64_be(i as u64)) .collect(); *new_head_state.randao_mixes_mut() = FixedVector::from(distinct_hashes); @@ -348,25 +348,25 @@ mod committees { execute_committee_consistency_test(new_head_state, cache_epoch, validator_count, spec); } - async fn committee_consistency_test_suite(cached_epoch: RelativeEpoch) { - let spec = T::default_spec(); + async fn committee_consistency_test_suite(cached_epoch: RelativeEpoch) { + let spec = E::default_spec(); let validator_count = spec .max_committees_per_slot - .mul(T::slots_per_epoch() as usize) + .mul(E::slots_per_epoch() as usize) .mul(spec.target_committee_size) .add(1); - committee_consistency_test::(validator_count, Epoch::new(0), cached_epoch).await; + committee_consistency_test::(validator_count, Epoch::new(0), cached_epoch).await; - committee_consistency_test::(validator_count, T::genesis_epoch() + 4, cached_epoch) + committee_consistency_test::(validator_count, E::genesis_epoch() + 4, cached_epoch) .await; - committee_consistency_test::( + committee_consistency_test::( validator_count, - T::genesis_epoch() - + (T::slots_per_historical_root() as u64) - .mul(T::slots_per_epoch()) + E::genesis_epoch() + + (E::slots_per_historical_root() as u64) + .mul(E::slots_per_epoch()) .mul(4), cached_epoch, ) diff --git a/consensus/types/src/beacon_state/tree_hash_cache.rs b/consensus/types/src/beacon_state/tree_hash_cache.rs index 69cd6fbb87..290020b1b3 100644 --- a/consensus/types/src/beacon_state/tree_hash_cache.rs +++ b/consensus/types/src/beacon_state/tree_hash_cache.rs @@ -10,7 +10,6 @@ use rayon::prelude::*; use ssz_derive::{Decode, Encode}; use ssz_types::VariableList; use std::cmp::Ordering; -use std::iter::ExactSizeIterator; use tree_hash::{mix_in_length, MerkleHasher, TreeHash}; /// The number of leaves (including padding) on the `BeaconState` Merkle tree. @@ -33,19 +32,19 @@ const NODES_PER_VALIDATOR: usize = 15; const VALIDATORS_PER_ARENA: usize = 4_096; #[derive(Debug, PartialEq, Clone, Encode, Decode)] -pub struct Eth1DataVotesTreeHashCache { +pub struct Eth1DataVotesTreeHashCache { arena: CacheArena, tree_hash_cache: TreeHashCache, voting_period: u64, - roots: VariableList, + roots: VariableList, } -impl Eth1DataVotesTreeHashCache { +impl Eth1DataVotesTreeHashCache { /// Instantiates a new cache. /// /// Allocates the necessary memory to store all of the cached Merkle trees. Only the leaves are /// hashed, leaving the internal nodes as all-zeros. - pub fn new(state: &BeaconState) -> Self { + pub fn new(state: &BeaconState) -> Self { let mut arena = CacheArena::default(); let roots: VariableList<_, _> = state .eth1_data_votes() @@ -64,10 +63,10 @@ impl Eth1DataVotesTreeHashCache { } fn voting_period(slot: Slot) -> u64 { - slot.as_u64() / T::SlotsPerEth1VotingPeriod::to_u64() + slot.as_u64() / E::SlotsPerEth1VotingPeriod::to_u64() } - pub fn recalculate_tree_hash_root(&mut self, state: &BeaconState) -> Result { + pub fn recalculate_tree_hash_root(&mut self, state: &BeaconState) -> Result { if state.eth1_data_votes().len() < self.roots.len() || Self::voting_period(state.slot()) != self.voting_period { @@ -90,12 +89,12 @@ impl Eth1DataVotesTreeHashCache { /// /// This type is a wrapper around the inner cache, which does all the work. #[derive(Debug, Default, PartialEq, Clone)] -pub struct BeaconTreeHashCache { - inner: Option>, +pub struct BeaconTreeHashCache { + inner: Option>, } -impl BeaconTreeHashCache { - pub fn new(state: &BeaconState) -> Self { +impl BeaconTreeHashCache { + pub fn new(state: &BeaconState) -> Self { Self { inner: Some(BeaconTreeHashCacheInner::new(state)), } @@ -106,12 +105,12 @@ impl BeaconTreeHashCache { } /// Move the inner cache out so that the containing `BeaconState` can be borrowed. - pub fn take(&mut self) -> Option> { + pub fn take(&mut self) -> Option> { self.inner.take() } /// Restore the inner cache after using `take`. - pub fn restore(&mut self, inner: BeaconTreeHashCacheInner) { + pub fn restore(&mut self, inner: BeaconTreeHashCacheInner) { self.inner = Some(inner); } @@ -129,7 +128,7 @@ impl BeaconTreeHashCache { } #[derive(Debug, PartialEq, Clone)] -pub struct BeaconTreeHashCacheInner { +pub struct BeaconTreeHashCacheInner { /// Tracks the previously generated state root to ensure the next state root provided descends /// directly from this state. previous_state: Option<(Hash256, Slot)>, @@ -147,19 +146,19 @@ pub struct BeaconTreeHashCacheInner { balances: TreeHashCache, randao_mixes: TreeHashCache, slashings: TreeHashCache, - eth1_data_votes: Eth1DataVotesTreeHashCache, + eth1_data_votes: Eth1DataVotesTreeHashCache, inactivity_scores: OptionalTreeHashCache, // Participation caches previous_epoch_participation: OptionalTreeHashCache, current_epoch_participation: OptionalTreeHashCache, } -impl BeaconTreeHashCacheInner { +impl BeaconTreeHashCacheInner { /// Instantiates a new cache. /// /// Allocates the necessary memory to store all of the cached Merkle trees. Only the leaves are /// hashed, leaving the internal nodes as all-zeros. - pub fn new(state: &BeaconState) -> Self { + pub fn new(state: &BeaconState) -> Self { let mut fixed_arena = CacheArena::default(); let block_roots = state.block_roots().new_tree_hash_cache(&mut fixed_arena); let state_roots = state.state_roots().new_tree_hash_cache(&mut fixed_arena); @@ -176,7 +175,7 @@ impl BeaconTreeHashCacheInner { let randao_mixes = state.randao_mixes().new_tree_hash_cache(&mut fixed_arena); - let validators = ValidatorsListTreeHashCache::new::(state.validators()); + let validators = ValidatorsListTreeHashCache::new::(state.validators()); let mut balances_arena = CacheArena::default(); let balances = state.balances().new_tree_hash_cache(&mut balances_arena); @@ -223,7 +222,7 @@ impl BeaconTreeHashCacheInner { pub fn recalculate_tree_hash_leaves( &mut self, - state: &BeaconState, + state: &BeaconState, ) -> Result, Error> { let mut leaves = vec![ // Genesis data leaves. @@ -329,7 +328,7 @@ impl BeaconTreeHashCacheInner { /// The provided `state` should be a descendant of the last `state` given to this function, or /// the `Self::new` function. If the state is more than `SLOTS_PER_HISTORICAL_ROOT` slots /// after `self.previous_state` then the whole cache will be re-initialized. - pub fn recalculate_tree_hash_root(&mut self, state: &BeaconState) -> Result { + pub fn recalculate_tree_hash_root(&mut self, state: &BeaconState) -> Result { // If this cache has previously produced a root, ensure that it is in the state root // history of this state. // @@ -600,7 +599,7 @@ impl OptionalTreeHashCacheInner { } } -impl arbitrary::Arbitrary<'_> for BeaconTreeHashCache { +impl arbitrary::Arbitrary<'_> for BeaconTreeHashCache { fn arbitrary(_u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { Ok(Self::default()) } diff --git a/consensus/types/src/blob_sidecar.rs b/consensus/types/src/blob_sidecar.rs index c249d8b4d8..31b1307aa7 100644 --- a/consensus/types/src/blob_sidecar.rs +++ b/consensus/types/src/blob_sidecar.rs @@ -70,27 +70,27 @@ impl Ord for BlobIdentifier { Derivative, arbitrary::Arbitrary, )] -#[serde(bound = "T: EthSpec")] -#[arbitrary(bound = "T: EthSpec")] -#[derivative(PartialEq, Eq, Hash(bound = "T: EthSpec"))] -pub struct BlobSidecar { +#[serde(bound = "E: EthSpec")] +#[arbitrary(bound = "E: EthSpec")] +#[derivative(PartialEq, Eq, Hash(bound = "E: EthSpec"))] +pub struct BlobSidecar { #[serde(with = "serde_utils::quoted_u64")] pub index: u64, #[serde(with = "ssz_types::serde_utils::hex_fixed_vec")] - pub blob: Blob, + pub blob: Blob, pub kzg_commitment: KzgCommitment, pub kzg_proof: KzgProof, pub signed_block_header: SignedBeaconBlockHeader, - pub kzg_commitment_inclusion_proof: FixedVector, + pub kzg_commitment_inclusion_proof: FixedVector, } -impl PartialOrd for BlobSidecar { +impl PartialOrd for BlobSidecar { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl Ord for BlobSidecar { +impl Ord for BlobSidecar { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.index.cmp(&other.index) } @@ -123,11 +123,11 @@ impl From for BlobSidecarError { } } -impl BlobSidecar { +impl BlobSidecar { pub fn new( index: usize, - blob: Blob, - signed_block: &SignedBeaconBlock, + blob: Blob, + signed_block: &SignedBeaconBlock, kzg_proof: KzgProof, ) -> Result { let expected_kzg_commitments = signed_block @@ -179,7 +179,7 @@ impl BlobSidecar { pub fn empty() -> Self { Self { index: 0, - blob: Blob::::default(), + blob: Blob::::default(), kzg_commitment: KzgCommitment::empty_for_testing(), kzg_proof: KzgProof::empty(), signed_block_header: SignedBeaconBlockHeader { @@ -194,7 +194,7 @@ impl BlobSidecar { pub fn verify_blob_sidecar_inclusion_proof(&self) -> Result { // Depth of the subtree rooted at `blob_kzg_commitments` in the `BeaconBlockBody` // is equal to depth of the ssz List max size + 1 for the length mixin - let kzg_commitments_tree_depth = (T::max_blob_commitments_per_block() + let kzg_commitments_tree_depth = (E::max_blob_commitments_per_block() .next_power_of_two() .ilog2() .safe_add(1))? as usize; @@ -212,9 +212,9 @@ impl BlobSidecar { Ok(verify_merkle_proof( blob_kzg_commitments_root, self.kzg_commitment_inclusion_proof - .get(kzg_commitments_tree_depth..T::kzg_proof_inclusion_proof_depth()) + .get(kzg_commitments_tree_depth..E::kzg_proof_inclusion_proof_depth()) .ok_or(MerkleTreeError::PleaseNotifyTheDevs)?, - T::kzg_proof_inclusion_proof_depth().safe_sub(kzg_commitments_tree_depth)?, + E::kzg_proof_inclusion_proof_depth().safe_sub(kzg_commitments_tree_depth)?, BLOB_KZG_COMMITMENTS_INDEX, self.signed_block_header.message.body_root, )) @@ -235,7 +235,7 @@ impl BlobSidecar { *byte = 0; } - let blob = Blob::::new(blob_bytes) + let blob = Blob::::new(blob_bytes) .map_err(|e| format!("error constructing random blob: {:?}", e))?; let kzg_blob = KzgBlob::from_bytes(&blob).unwrap(); @@ -262,10 +262,10 @@ impl BlobSidecar { } pub fn build_sidecars( - blobs: BlobsList, - block: &SignedBeaconBlock, - kzg_proofs: KzgProofs, - ) -> Result, BlobSidecarError> { + blobs: BlobsList, + block: &SignedBeaconBlock, + kzg_proofs: KzgProofs, + ) -> Result, BlobSidecarError> { let mut blob_sidecars = vec![]; for (i, (kzg_proof, blob)) in kzg_proofs.iter().zip(blobs).enumerate() { let blob_sidecar = BlobSidecar::new(i, blob, block, *kzg_proof)?; @@ -275,7 +275,7 @@ impl BlobSidecar { } } -pub type BlobSidecarList = VariableList>, ::MaxBlobsPerBlock>; -pub type FixedBlobSidecarList = - FixedVector>>, ::MaxBlobsPerBlock>; -pub type BlobsList = VariableList, ::MaxBlobCommitmentsPerBlock>; +pub type BlobSidecarList = VariableList>, ::MaxBlobsPerBlock>; +pub type FixedBlobSidecarList = + FixedVector>>, ::MaxBlobsPerBlock>; +pub type BlobsList = VariableList, ::MaxBlobCommitmentsPerBlock>; diff --git a/consensus/types/src/bls_to_execution_change.rs b/consensus/types/src/bls_to_execution_change.rs index baa65f5172..e6426e125f 100644 --- a/consensus/types/src/bls_to_execution_change.rs +++ b/consensus/types/src/bls_to_execution_change.rs @@ -1,6 +1,5 @@ use crate::test_utils::TestRandom; use crate::*; -use bls::PublicKeyBytes; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; diff --git a/consensus/types/src/builder_bid.rs b/consensus/types/src/builder_bid.rs index f43585000a..121d3f8427 100644 --- a/consensus/types/src/builder_bid.rs +++ b/consensus/types/src/builder_bid.rs @@ -1,8 +1,8 @@ use crate::beacon_block_body::KzgCommitments; use crate::{ ChainSpec, EthSpec, ExecutionPayloadHeaderCapella, ExecutionPayloadHeaderDeneb, - ExecutionPayloadHeaderMerge, ExecutionPayloadHeaderRef, ExecutionPayloadHeaderRefMut, ForkName, - ForkVersionDeserialize, SignedRoot, Uint256, + ExecutionPayloadHeaderElectra, ExecutionPayloadHeaderMerge, ExecutionPayloadHeaderRef, + ExecutionPayloadHeaderRefMut, ForkName, ForkVersionDeserialize, SignedRoot, Uint256, }; use bls::PublicKeyBytes; use bls::Signature; @@ -11,7 +11,7 @@ use superstruct::superstruct; use tree_hash_derive::TreeHash; #[superstruct( - variants(Merge, Capella, Deneb), + variants(Merge, Capella, Deneb, Electra), variant_attributes( derive(PartialEq, Debug, Serialize, Deserialize, TreeHash, Clone), serde(bound = "E: EthSpec", deny_unknown_fields) @@ -29,7 +29,9 @@ pub struct BuilderBid { pub header: ExecutionPayloadHeaderCapella, #[superstruct(only(Deneb), partial_getter(rename = "header_deneb"))] pub header: ExecutionPayloadHeaderDeneb, - #[superstruct(only(Deneb))] + #[superstruct(only(Electra), partial_getter(rename = "header_electra"))] + pub header: ExecutionPayloadHeaderElectra, + #[superstruct(only(Deneb, Electra))] pub blob_kzg_commitments: KzgCommitments, #[serde(with = "serde_utils::quoted_u256")] pub value: Uint256, @@ -68,7 +70,7 @@ pub struct SignedBuilderBid { pub signature: Signature, } -impl ForkVersionDeserialize for BuilderBid { +impl ForkVersionDeserialize for BuilderBid { fn deserialize_by_fork<'de, D: Deserializer<'de>>( value: serde_json::value::Value, fork_name: ForkName, @@ -80,6 +82,7 @@ impl ForkVersionDeserialize for BuilderBid { ForkName::Merge => Self::Merge(serde_json::from_value(value).map_err(convert_err)?), ForkName::Capella => Self::Capella(serde_json::from_value(value).map_err(convert_err)?), ForkName::Deneb => Self::Deneb(serde_json::from_value(value).map_err(convert_err)?), + ForkName::Electra => Self::Electra(serde_json::from_value(value).map_err(convert_err)?), ForkName::Base | ForkName::Altair => { return Err(serde::de::Error::custom(format!( "BuilderBid failed to deserialize: unsupported fork '{}'", @@ -90,7 +93,7 @@ impl ForkVersionDeserialize for BuilderBid { } } -impl ForkVersionDeserialize for SignedBuilderBid { +impl ForkVersionDeserialize for SignedBuilderBid { fn deserialize_by_fork<'de, D: Deserializer<'de>>( value: serde_json::value::Value, fork_name: ForkName, diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index b2120fb040..e9345ab14e 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -1,8 +1,9 @@ use crate::application_domain::{ApplicationDomain, APPLICATION_DOMAIN_BUILDER}; +use crate::blob_sidecar::BlobIdentifier; use crate::*; use int_to_bytes::int_to_bytes4; -use serde::Deserialize; -use serde::{Deserializer, Serialize, Serializer}; +use safe_arith::{ArithError, SafeArith}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_utils::quoted_u64::MaybeQuoted; use ssz::Encode; use std::fs::File; @@ -113,6 +114,8 @@ pub struct ChainSpec { */ pub safe_slots_to_update_justified: u64, pub proposer_score_boost: Option, + pub reorg_head_weight_threshold: Option, + pub reorg_parent_weight_threshold: Option, /* * Eth1 @@ -168,6 +171,13 @@ pub struct ChainSpec { pub deneb_fork_version: [u8; 4], pub deneb_fork_epoch: Option, + /* + * Electra hard fork params + */ + pub electra_fork_version: [u8; 4], + /// The Electra fork epoch is optional, with `None` representing "Electra never happens". + pub electra_fork_epoch: Option, + /* * Networking */ @@ -189,6 +199,7 @@ pub struct ChainSpec { pub attestation_subnet_count: u64, pub attestation_subnet_extra_bits: u8, pub attestation_subnet_prefix_bits: u8, + pub attestation_subnet_shuffling_prefix_bits: u8, /* * Networking Deneb @@ -220,22 +231,22 @@ pub struct ChainSpec { impl ChainSpec { /// Construct a `ChainSpec` from a standard config. - pub fn from_config(config: &Config) -> Option { - let spec = T::default_spec(); - config.apply_to_chain_spec::(&spec) + pub fn from_config(config: &Config) -> Option { + let spec = E::default_spec(); + config.apply_to_chain_spec::(&spec) } /// Returns an `EnrForkId` for the given `slot`. - pub fn enr_fork_id( + pub fn enr_fork_id( &self, slot: Slot, genesis_validators_root: Hash256, ) -> EnrForkId { EnrForkId { - fork_digest: self.fork_digest::(slot, genesis_validators_root), - next_fork_version: self.next_fork_version::(slot), + fork_digest: self.fork_digest::(slot, genesis_validators_root), + next_fork_version: self.next_fork_version::(slot), next_fork_epoch: self - .next_fork_epoch::(slot) + .next_fork_epoch::(slot) .map(|(_, e)| e) .unwrap_or(self.far_future_epoch), } @@ -245,8 +256,8 @@ impl ChainSpec { /// /// If `self.altair_fork_epoch == None`, then this function returns the genesis fork digest /// otherwise, returns the fork digest based on the slot. - pub fn fork_digest(&self, slot: Slot, genesis_validators_root: Hash256) -> [u8; 4] { - let fork_name = self.fork_name_at_slot::(slot); + pub fn fork_digest(&self, slot: Slot, genesis_validators_root: Hash256) -> [u8; 4] { + let fork_name = self.fork_name_at_slot::(slot); Self::compute_fork_digest( self.fork_version_for_name(fork_name), genesis_validators_root, @@ -266,8 +277,8 @@ impl ChainSpec { /// Returns the epoch of the next scheduled fork along with its corresponding `ForkName`. /// /// If no future forks are scheduled, this function returns `None`. - pub fn next_fork_epoch(&self, slot: Slot) -> Option<(ForkName, Epoch)> { - let current_fork_name = self.fork_name_at_slot::(slot); + pub fn next_fork_epoch(&self, slot: Slot) -> Option<(ForkName, Epoch)> { + let current_fork_name = self.fork_name_at_slot::(slot); let next_fork_name = current_fork_name.next_fork()?; let fork_epoch = self.fork_epoch(next_fork_name)?; Some((next_fork_name, fork_epoch)) @@ -280,15 +291,18 @@ impl ChainSpec { /// Returns the name of the fork which is active at `epoch`. pub fn fork_name_at_epoch(&self, epoch: Epoch) -> ForkName { - match self.deneb_fork_epoch { - Some(fork_epoch) if epoch >= fork_epoch => ForkName::Deneb, - _ => match self.capella_fork_epoch { - Some(fork_epoch) if epoch >= fork_epoch => ForkName::Capella, - _ => match self.bellatrix_fork_epoch { - Some(fork_epoch) if epoch >= fork_epoch => ForkName::Merge, - _ => match self.altair_fork_epoch { - Some(fork_epoch) if epoch >= fork_epoch => ForkName::Altair, - _ => ForkName::Base, + match self.electra_fork_epoch { + Some(fork_epoch) if epoch >= fork_epoch => ForkName::Electra, + _ => match self.deneb_fork_epoch { + Some(fork_epoch) if epoch >= fork_epoch => ForkName::Deneb, + _ => match self.capella_fork_epoch { + Some(fork_epoch) if epoch >= fork_epoch => ForkName::Capella, + _ => match self.bellatrix_fork_epoch { + Some(fork_epoch) if epoch >= fork_epoch => ForkName::Merge, + _ => match self.altair_fork_epoch { + Some(fork_epoch) if epoch >= fork_epoch => ForkName::Altair, + _ => ForkName::Base, + }, }, }, }, @@ -303,6 +317,7 @@ impl ChainSpec { ForkName::Merge => self.bellatrix_fork_version, ForkName::Capella => self.capella_fork_version, ForkName::Deneb => self.deneb_fork_version, + ForkName::Electra => self.electra_fork_version, } } @@ -314,45 +329,47 @@ impl ChainSpec { ForkName::Merge => self.bellatrix_fork_epoch, ForkName::Capella => self.capella_fork_epoch, ForkName::Deneb => self.deneb_fork_epoch, + ForkName::Electra => self.electra_fork_epoch, } } - /// For a given `BeaconState`, return the inactivity penalty quotient associated with its variant. - pub fn inactivity_penalty_quotient_for_state(&self, state: &BeaconState) -> u64 { - match state { - BeaconState::Base(_) => self.inactivity_penalty_quotient, - BeaconState::Altair(_) => self.inactivity_penalty_quotient_altair, - BeaconState::Merge(_) => self.inactivity_penalty_quotient_bellatrix, - BeaconState::Capella(_) => self.inactivity_penalty_quotient_bellatrix, - BeaconState::Deneb(_) => self.inactivity_penalty_quotient_bellatrix, + pub fn inactivity_penalty_quotient_for_fork(&self, fork_name: ForkName) -> u64 { + if fork_name >= ForkName::Merge { + self.inactivity_penalty_quotient_bellatrix + } else if fork_name >= ForkName::Altair { + self.inactivity_penalty_quotient_altair + } else { + self.inactivity_penalty_quotient } } /// For a given `BeaconState`, return the proportional slashing multiplier associated with its variant. - pub fn proportional_slashing_multiplier_for_state( + pub fn proportional_slashing_multiplier_for_state( &self, - state: &BeaconState, + state: &BeaconState, ) -> u64 { - match state { - BeaconState::Base(_) => self.proportional_slashing_multiplier, - BeaconState::Altair(_) => self.proportional_slashing_multiplier_altair, - BeaconState::Merge(_) => self.proportional_slashing_multiplier_bellatrix, - BeaconState::Capella(_) => self.proportional_slashing_multiplier_bellatrix, - BeaconState::Deneb(_) => self.proportional_slashing_multiplier_bellatrix, + let fork_name = state.fork_name_unchecked(); + if fork_name >= ForkName::Merge { + self.proportional_slashing_multiplier_bellatrix + } else if fork_name >= ForkName::Altair { + self.proportional_slashing_multiplier_altair + } else { + self.proportional_slashing_multiplier } } /// For a given `BeaconState`, return the minimum slashing penalty quotient associated with its variant. - pub fn min_slashing_penalty_quotient_for_state( + pub fn min_slashing_penalty_quotient_for_state( &self, - state: &BeaconState, + state: &BeaconState, ) -> u64 { - match state { - BeaconState::Base(_) => self.min_slashing_penalty_quotient, - BeaconState::Altair(_) => self.min_slashing_penalty_quotient_altair, - BeaconState::Merge(_) => self.min_slashing_penalty_quotient_bellatrix, - BeaconState::Capella(_) => self.min_slashing_penalty_quotient_bellatrix, - BeaconState::Deneb(_) => self.min_slashing_penalty_quotient_bellatrix, + let fork_name = state.fork_name_unchecked(); + if fork_name >= ForkName::Merge { + self.min_slashing_penalty_quotient_bellatrix + } else if fork_name >= ForkName::Altair { + self.min_slashing_penalty_quotient_altair + } else { + self.min_slashing_penalty_quotient } } @@ -494,6 +511,13 @@ impl ChainSpec { Hash256::from(domain) } + /// Compute the epoch used for activations prior to Deneb, and for exits under all forks. + /// + /// Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#compute_activation_exit_epoch + pub fn compute_activation_exit_epoch(&self, epoch: Epoch) -> Result { + epoch.safe_add(1)?.safe_add(self.max_seed_lookahead) + } + pub fn maximum_gossip_clock_disparity(&self) -> Duration { Duration::from_millis(self.maximum_gossip_clock_disparity_millis) } @@ -507,22 +531,19 @@ impl ChainSpec { } pub fn max_blocks_by_root_request(&self, fork_name: ForkName) -> usize { - match fork_name { - ForkName::Base | ForkName::Altair | ForkName::Merge | ForkName::Capella => { - self.max_blocks_by_root_request - } - ForkName::Deneb => self.max_blocks_by_root_request_deneb, + if fork_name >= ForkName::Deneb { + self.max_blocks_by_root_request_deneb + } else { + self.max_blocks_by_root_request } } pub fn max_request_blocks(&self, fork_name: ForkName) -> usize { - let max_request_blocks = match fork_name { - ForkName::Base | ForkName::Altair | ForkName::Merge | ForkName::Capella => { - self.max_request_blocks - } - ForkName::Deneb => self.max_request_blocks_deneb, - }; - max_request_blocks as usize + if fork_name >= ForkName::Deneb { + self.max_request_blocks_deneb as usize + } else { + self.max_request_blocks as usize + } } /// Returns a `ChainSpec` compatible with the Ethereum Foundation specification. @@ -620,6 +641,8 @@ impl ChainSpec { */ safe_slots_to_update_justified: 8, proposer_score_boost: Some(40), + reorg_head_weight_threshold: Some(20), + reorg_parent_weight_threshold: Some(160), /* * Eth1 @@ -679,7 +702,13 @@ impl ChainSpec { * Deneb hard fork params */ deneb_fork_version: [0x04, 0x00, 0x00, 0x00], - deneb_fork_epoch: None, + deneb_fork_epoch: Some(Epoch::new(269568)), + + /* + * Electra hard fork params + */ + electra_fork_version: [0x05, 00, 00, 00], + electra_fork_epoch: None, /* * Network specific @@ -701,6 +730,8 @@ impl ChainSpec { 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(), + attestation_subnet_shuffling_prefix_bits: + default_attestation_subnet_shuffling_prefix_bits(), max_request_blocks: default_max_request_blocks(), /* @@ -775,6 +806,9 @@ impl ChainSpec { // Deneb deneb_fork_version: [0x04, 0x00, 0x00, 0x01], deneb_fork_epoch: None, + // Electra + electra_fork_version: [0x05, 0x00, 0x00, 0x01], + electra_fork_epoch: None, // Other network_id: 2, // lighthouse testnet network id deposit_chain_id: 5, @@ -805,7 +839,7 @@ impl ChainSpec { max_committees_per_slot: 64, target_committee_size: 128, min_per_epoch_churn_limit: 4, - max_per_epoch_activation_churn_limit: 8, + max_per_epoch_activation_churn_limit: 2, churn_limit_quotient: 4_096, shuffle_round_count: 90, min_genesis_active_validator_count: 4_096, @@ -879,6 +913,8 @@ impl ChainSpec { */ safe_slots_to_update_justified: 8, proposer_score_boost: Some(40), + reorg_head_weight_threshold: Some(20), + reorg_parent_weight_threshold: Some(160), /* * Eth1 @@ -940,7 +976,13 @@ impl ChainSpec { * Deneb hard fork params */ deneb_fork_version: [0x04, 0x00, 0x00, 0x64], - deneb_fork_epoch: None, + deneb_fork_epoch: Some(Epoch::new(889856)), + + /* + * Electra hard fork params + */ + electra_fork_version: [0x05, 0x00, 0x00, 0x64], + electra_fork_epoch: None, /* * Network specific @@ -954,7 +996,7 @@ impl ChainSpec { target_aggregators_per_committee: 16, epochs_per_subnet_subscription: default_epochs_per_subnet_subscription(), gossip_max_size: default_gossip_max_size(), - min_epochs_for_block_requests: default_min_epochs_for_block_requests(), + min_epochs_for_block_requests: 33024, max_chunk_size: default_max_chunk_size(), ttfb_timeout: default_ttfb_timeout(), resp_timeout: default_resp_timeout(), @@ -962,6 +1004,8 @@ impl ChainSpec { 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(), + attestation_subnet_shuffling_prefix_bits: + default_attestation_subnet_shuffling_prefix_bits(), max_request_blocks: default_max_request_blocks(), /* @@ -969,7 +1013,7 @@ impl ChainSpec { */ max_request_blocks_deneb: default_max_request_blocks_deneb(), max_request_blob_sidecars: default_max_request_blob_sidecars(), - min_epochs_for_blob_sidecars_requests: default_min_epochs_for_blob_sidecars_requests(), + min_epochs_for_blob_sidecars_requests: 16384, blob_sidecar_subnet_count: default_blob_sidecar_subnet_count(), /* @@ -1063,6 +1107,14 @@ pub struct Config { #[serde(deserialize_with = "deserialize_fork_epoch")] pub deneb_fork_epoch: Option>, + #[serde(default = "default_electra_fork_version")] + #[serde(with = "serde_utils::bytes_4_hex")] + electra_fork_version: [u8; 4], + #[serde(default)] + #[serde(serialize_with = "serialize_fork_epoch")] + #[serde(deserialize_with = "deserialize_fork_epoch")] + pub electra_fork_epoch: Option>, + #[serde(with = "serde_utils::quoted_u64")] seconds_per_slot: u64, #[serde(with = "serde_utils::quoted_u64")] @@ -1139,6 +1191,9 @@ pub struct Config { #[serde(default = "default_attestation_subnet_prefix_bits")] #[serde(with = "serde_utils::quoted_u8")] attestation_subnet_prefix_bits: u8, + #[serde(default = "default_attestation_subnet_shuffling_prefix_bits")] + #[serde(with = "serde_utils::quoted_u8")] + attestation_subnet_shuffling_prefix_bits: u8, #[serde(default = "default_max_request_blocks_deneb")] #[serde(with = "serde_utils::quoted_u64")] max_request_blocks_deneb: u64, @@ -1168,6 +1223,11 @@ fn default_deneb_fork_version() -> [u8; 4] { [0xff, 0xff, 0xff, 0xff] } +fn default_electra_fork_version() -> [u8; 4] { + // This value shouldn't be used. + [0xff, 0xff, 0xff, 0xff] +} + /// Placeholder value: 2^256-2^10 (115792089237316195423570985008687907853269984665640564039457584007913129638912). /// /// Taken from https://github.com/ethereum/consensus-specs/blob/d5e4828aecafaf1c57ef67a5f23c4ae7b08c5137/configs/mainnet.yaml#L15-L16 @@ -1236,6 +1296,10 @@ const fn default_attestation_subnet_prefix_bits() -> u8 { 6 } +const fn default_attestation_subnet_shuffling_prefix_bits() -> u8 { + 3 +} + const fn default_max_request_blocks() -> u64 { 1024 } @@ -1280,8 +1344,13 @@ fn max_blocks_by_root_request_common(max_request_blocks: u64) -> usize { fn max_blobs_by_root_request_common(max_request_blob_sidecars: u64) -> usize { let max_request_blob_sidecars = max_request_blob_sidecars as usize; - RuntimeVariableList::::from_vec( - vec![Hash256::zero(); max_request_blob_sidecars], + let empty_blob_identifier = BlobIdentifier { + block_root: Hash256::zero(), + index: 0, + }; + + RuntimeVariableList::::from_vec( + vec![empty_blob_identifier; max_request_blob_sidecars], max_request_blob_sidecars, ) .as_ssz_bytes() @@ -1349,10 +1418,10 @@ impl Config { } } - pub fn from_chain_spec(spec: &ChainSpec) -> Self { + pub fn from_chain_spec(spec: &ChainSpec) -> Self { Self { config_name: spec.config_name.clone(), - preset_base: T::spec_name().to_string(), + preset_base: E::spec_name().to_string(), terminal_total_difficulty: spec.terminal_total_difficulty, terminal_block_hash: spec.terminal_block_hash, @@ -1368,19 +1437,27 @@ impl Config { altair_fork_epoch: spec .altair_fork_epoch .map(|epoch| MaybeQuoted { value: epoch }), + bellatrix_fork_version: spec.bellatrix_fork_version, bellatrix_fork_epoch: spec .bellatrix_fork_epoch .map(|epoch| MaybeQuoted { value: epoch }), + capella_fork_version: spec.capella_fork_version, capella_fork_epoch: spec .capella_fork_epoch .map(|epoch| MaybeQuoted { value: epoch }), + deneb_fork_version: spec.deneb_fork_version, deneb_fork_epoch: spec .deneb_fork_epoch .map(|epoch| MaybeQuoted { value: epoch }), + electra_fork_version: spec.electra_fork_version, + electra_fork_epoch: spec + .electra_fork_epoch + .map(|epoch| MaybeQuoted { value: epoch }), + seconds_per_slot: spec.seconds_per_slot, seconds_per_eth1_block: spec.seconds_per_eth1_block, min_validator_withdrawability_delay: spec.min_validator_withdrawability_delay, @@ -1414,6 +1491,7 @@ impl Config { 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, + attestation_subnet_shuffling_prefix_bits: spec.attestation_subnet_shuffling_prefix_bits, max_request_blocks_deneb: spec.max_request_blocks_deneb, max_request_blob_sidecars: spec.max_request_blob_sidecars, min_epochs_for_blob_sidecars_requests: spec.min_epochs_for_blob_sidecars_requests, @@ -1428,7 +1506,7 @@ impl Config { .map_err(|e| format!("Error parsing spec at {}: {:?}", filename.display(), e)) } - pub fn apply_to_chain_spec(&self, chain_spec: &ChainSpec) -> Option { + pub fn apply_to_chain_spec(&self, chain_spec: &ChainSpec) -> Option { // Pattern match here to avoid missing any fields. let &Config { ref config_name, @@ -1449,6 +1527,8 @@ impl Config { capella_fork_version, deneb_fork_epoch, deneb_fork_version, + electra_fork_epoch, + electra_fork_version, seconds_per_slot, seconds_per_eth1_block, min_validator_withdrawability_delay, @@ -1474,6 +1554,7 @@ impl Config { message_domain_valid_snappy, attestation_subnet_extra_bits, attestation_subnet_prefix_bits, + attestation_subnet_shuffling_prefix_bits, max_request_blocks, epochs_per_subnet_subscription, attestation_propagation_slot_range, @@ -1484,7 +1565,7 @@ impl Config { blob_sidecar_subnet_count, } = self; - if preset_base != T::spec_name().to_string().as_str() { + if preset_base != E::spec_name().to_string().as_str() { return None; } @@ -1502,6 +1583,8 @@ impl Config { capella_fork_version, deneb_fork_epoch: deneb_fork_epoch.map(|q| q.value), deneb_fork_version, + electra_fork_epoch: electra_fork_epoch.map(|q| q.value), + electra_fork_version, seconds_per_slot, seconds_per_eth1_block, min_validator_withdrawability_delay, @@ -1531,6 +1614,7 @@ impl Config { message_domain_valid_snappy, attestation_subnet_extra_bits, attestation_subnet_prefix_bits, + attestation_subnet_shuffling_prefix_bits, max_request_blocks, epochs_per_subnet_subscription, attestation_propagation_slot_range, @@ -1564,7 +1648,6 @@ where mod tests { use super::*; use itertools::Itertools; - use safe_arith::SafeArith; #[test] fn test_mainnet_spec_can_be_constructed() { @@ -1817,6 +1900,7 @@ mod yaml_tests { check_default!(message_domain_valid_snappy); check_default!(attestation_subnet_extra_bits); check_default!(attestation_subnet_prefix_bits); + check_default!(attestation_subnet_shuffling_prefix_bits); assert_eq!(chain_spec.bellatrix_fork_epoch, None); } diff --git a/consensus/types/src/config_and_preset.rs b/consensus/types/src/config_and_preset.rs index b651d34af3..f2a6e616da 100644 --- a/consensus/types/src/config_and_preset.rs +++ b/consensus/types/src/config_and_preset.rs @@ -1,6 +1,6 @@ use crate::{ consts::altair, AltairPreset, BasePreset, BellatrixPreset, CapellaPreset, ChainSpec, Config, - DenebPreset, EthSpec, ForkName, + DenebPreset, ElectraPreset, EthSpec, ForkName, }; use maplit::hashmap; use serde::{Deserialize, Serialize}; @@ -12,7 +12,7 @@ use superstruct::superstruct; /// /// Mostly useful for the API. #[superstruct( - variants(Capella, Deneb), + variants(Capella, Deneb, Electra), variant_attributes(derive(Serialize, Deserialize, Debug, PartialEq, Clone)) )] #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] @@ -29,28 +29,48 @@ pub struct ConfigAndPreset { pub bellatrix_preset: BellatrixPreset, #[serde(flatten)] pub capella_preset: CapellaPreset, - #[superstruct(only(Deneb))] + #[superstruct(only(Deneb, Electra))] #[serde(flatten)] pub deneb_preset: DenebPreset, + #[superstruct(only(Electra))] + #[serde(flatten)] + pub electra_preset: ElectraPreset, /// The `extra_fields` map allows us to gracefully decode fields intended for future hard forks. #[serde(flatten)] pub extra_fields: HashMap, } impl ConfigAndPreset { - pub fn from_chain_spec(spec: &ChainSpec, fork_name: Option) -> Self { - let config = Config::from_chain_spec::(spec); - let base_preset = BasePreset::from_chain_spec::(spec); - let altair_preset = AltairPreset::from_chain_spec::(spec); - let bellatrix_preset = BellatrixPreset::from_chain_spec::(spec); - let capella_preset = CapellaPreset::from_chain_spec::(spec); + pub fn from_chain_spec(spec: &ChainSpec, fork_name: Option) -> Self { + let config = Config::from_chain_spec::(spec); + let base_preset = BasePreset::from_chain_spec::(spec); + let altair_preset = AltairPreset::from_chain_spec::(spec); + let bellatrix_preset = BellatrixPreset::from_chain_spec::(spec); + let capella_preset = CapellaPreset::from_chain_spec::(spec); let extra_fields = get_extra_fields(spec); - if spec.deneb_fork_epoch.is_some() + if spec.electra_fork_epoch.is_some() + || fork_name.is_none() + || fork_name == Some(ForkName::Electra) + { + let deneb_preset = DenebPreset::from_chain_spec::(spec); + let electra_preset = ElectraPreset::from_chain_spec::(spec); + + ConfigAndPreset::Electra(ConfigAndPresetElectra { + config, + base_preset, + altair_preset, + bellatrix_preset, + capella_preset, + deneb_preset, + electra_preset, + extra_fields, + }) + } else if spec.deneb_fork_epoch.is_some() || fork_name.is_none() || fork_name == Some(ForkName::Deneb) { - let deneb_preset = DenebPreset::from_chain_spec::(spec); + let deneb_preset = DenebPreset::from_chain_spec::(spec); ConfigAndPreset::Deneb(ConfigAndPresetDeneb { config, base_preset, @@ -136,8 +156,8 @@ mod test { .write(false) .open(tmp_file.as_ref()) .expect("error while opening the file"); - let from: ConfigAndPresetDeneb = + let from: ConfigAndPresetElectra = serde_yaml::from_reader(reader).expect("error while deserializing"); - assert_eq!(ConfigAndPreset::Deneb(from), yamlconfig); + assert_eq!(ConfigAndPreset::Electra(from), yamlconfig); } } diff --git a/consensus/types/src/contribution_and_proof.rs b/consensus/types/src/contribution_and_proof.rs index aba98c92b7..321c12d220 100644 --- a/consensus/types/src/contribution_and_proof.rs +++ b/consensus/types/src/contribution_and_proof.rs @@ -21,27 +21,27 @@ use tree_hash_derive::TreeHash; TreeHash, arbitrary::Arbitrary, )] -#[serde(bound = "T: EthSpec")] -#[arbitrary(bound = "T: EthSpec")] -pub struct ContributionAndProof { +#[serde(bound = "E: EthSpec")] +#[arbitrary(bound = "E: EthSpec")] +pub struct ContributionAndProof { /// The index of the validator that created the sync contribution. #[serde(with = "serde_utils::quoted_u64")] pub aggregator_index: u64, /// The aggregate contribution. - pub contribution: SyncCommitteeContribution, + pub contribution: SyncCommitteeContribution, /// A proof provided by the validator that permits them to publish on the /// `sync_committee_contribution_and_proof` gossipsub topic. pub selection_proof: Signature, } -impl ContributionAndProof { +impl ContributionAndProof { /// Produces a new `ContributionAndProof` with a `selection_proof` generated by signing /// `SyncAggregatorSelectionData` with `secret_key`. /// /// If `selection_proof.is_none()` it will be computed locally. pub fn from_aggregate( aggregator_index: u64, - contribution: SyncCommitteeContribution, + contribution: SyncCommitteeContribution, selection_proof: Option, secret_key: &SecretKey, fork: &Fork, @@ -50,7 +50,7 @@ impl ContributionAndProof { ) -> Self { let selection_proof = selection_proof .unwrap_or_else(|| { - SyncSelectionProof::new::( + SyncSelectionProof::new::( contribution.slot, contribution.subcommittee_index, secret_key, @@ -69,4 +69,4 @@ impl ContributionAndProof { } } -impl SignedRoot for ContributionAndProof {} +impl SignedRoot for ContributionAndProof {} diff --git a/consensus/types/src/deposit_data.rs b/consensus/types/src/deposit_data.rs index e074ffdfaa..f62829e795 100644 --- a/consensus/types/src/deposit_data.rs +++ b/consensus/types/src/deposit_data.rs @@ -1,7 +1,6 @@ use crate::test_utils::TestRandom; use crate::*; -use bls::{PublicKeyBytes, SignatureBytes}; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; diff --git a/consensus/types/src/deposit_message.rs b/consensus/types/src/deposit_message.rs index e5c666df82..6184d0aeb3 100644 --- a/consensus/types/src/deposit_message.rs +++ b/consensus/types/src/deposit_message.rs @@ -1,7 +1,6 @@ use crate::test_utils::TestRandom; use crate::*; -use bls::PublicKeyBytes; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; diff --git a/consensus/types/src/deposit_tree_snapshot.rs b/consensus/types/src/deposit_tree_snapshot.rs index d4dcdb2eda..1793be1c7c 100644 --- a/consensus/types/src/deposit_tree_snapshot.rs +++ b/consensus/types/src/deposit_tree_snapshot.rs @@ -5,7 +5,6 @@ use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use test_utils::TestRandom; -use DEPOSIT_TREE_DEPTH; #[derive(Encode, Decode, Deserialize, Serialize, Clone, Debug, PartialEq, TestRandom)] pub struct FinalizedExecutionBlock { diff --git a/consensus/types/src/epoch_cache.rs b/consensus/types/src/epoch_cache.rs new file mode 100644 index 0000000000..b447e9b71e --- /dev/null +++ b/consensus/types/src/epoch_cache.rs @@ -0,0 +1,142 @@ +use crate::{ActivationQueue, BeaconStateError, ChainSpec, Epoch, Hash256, Slot}; +use safe_arith::{ArithError, SafeArith}; +use std::sync::Arc; + +/// Cache of values which are uniquely determined at the start of an epoch. +/// +/// The values are fixed with respect to the last block of the _prior_ epoch, which we refer +/// to as the "decision block". This cache is very similar to the `BeaconProposerCache` in that +/// beacon proposers are determined at exactly the same time as the values in this cache, so +/// the keys for the two caches are identical. +#[derive(Debug, PartialEq, Eq, Clone, Default, arbitrary::Arbitrary)] +pub struct EpochCache { + inner: Option>, +} + +#[derive(Debug, PartialEq, Eq, Clone, arbitrary::Arbitrary)] +struct Inner { + /// Unique identifier for this cache, which can be used to check its validity before use + /// with any `BeaconState`. + key: EpochCacheKey, + /// Effective balance for every validator in this epoch. + effective_balances: Vec, + /// Base rewards for every effective balance increment (currently 0..32 ETH). + /// + /// Keyed by `effective_balance / effective_balance_increment`. + base_rewards: Vec, + /// Validator activation queue. + activation_queue: ActivationQueue, + /// Effective balance increment. + effective_balance_increment: u64, +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, arbitrary::Arbitrary)] +pub struct EpochCacheKey { + pub epoch: Epoch, + pub decision_block_root: Hash256, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum EpochCacheError { + IncorrectEpoch { cache: Epoch, state: Epoch }, + IncorrectDecisionBlock { cache: Hash256, state: Hash256 }, + ValidatorIndexOutOfBounds { validator_index: usize }, + EffectiveBalanceOutOfBounds { effective_balance_eth: usize }, + InvalidSlot { slot: Slot }, + Arith(ArithError), + BeaconState(BeaconStateError), + CacheNotInitialized, +} + +impl From for EpochCacheError { + fn from(e: BeaconStateError) -> Self { + Self::BeaconState(e) + } +} + +impl From for EpochCacheError { + fn from(e: ArithError) -> Self { + Self::Arith(e) + } +} + +impl EpochCache { + pub fn new( + key: EpochCacheKey, + effective_balances: Vec, + base_rewards: Vec, + activation_queue: ActivationQueue, + spec: &ChainSpec, + ) -> EpochCache { + Self { + inner: Some(Arc::new(Inner { + key, + effective_balances, + base_rewards, + activation_queue, + effective_balance_increment: spec.effective_balance_increment, + })), + } + } + + pub fn check_validity( + &self, + current_epoch: Epoch, + state_decision_root: Hash256, + ) -> Result<(), EpochCacheError> { + let cache = self + .inner + .as_ref() + .ok_or(EpochCacheError::CacheNotInitialized)?; + if cache.key.epoch != current_epoch { + return Err(EpochCacheError::IncorrectEpoch { + cache: cache.key.epoch, + state: current_epoch, + }); + } + if cache.key.decision_block_root != state_decision_root { + return Err(EpochCacheError::IncorrectDecisionBlock { + cache: cache.key.decision_block_root, + state: state_decision_root, + }); + } + Ok(()) + } + + #[inline] + pub fn get_effective_balance(&self, validator_index: usize) -> Result { + self.inner + .as_ref() + .ok_or(EpochCacheError::CacheNotInitialized)? + .effective_balances + .get(validator_index) + .copied() + .ok_or(EpochCacheError::ValidatorIndexOutOfBounds { validator_index }) + } + + #[inline] + pub fn get_base_reward(&self, validator_index: usize) -> Result { + let inner = self + .inner + .as_ref() + .ok_or(EpochCacheError::CacheNotInitialized)?; + let effective_balance = self.get_effective_balance(validator_index)?; + let effective_balance_eth = + effective_balance.safe_div(inner.effective_balance_increment)? as usize; + inner + .base_rewards + .get(effective_balance_eth) + .copied() + .ok_or(EpochCacheError::EffectiveBalanceOutOfBounds { + effective_balance_eth, + }) + } + + pub fn activation_queue(&self) -> Result<&ActivationQueue, EpochCacheError> { + let inner = self + .inner + .as_ref() + .ok_or(EpochCacheError::CacheNotInitialized)?; + Ok(&inner.activation_queue) + } +} diff --git a/consensus/types/src/eth_spec.rs b/consensus/types/src/eth_spec.rs index 17baad9c4c..a2972b722a 100644 --- a/consensus/types/src/eth_spec.rs +++ b/consensus/types/src/eth_spec.rs @@ -3,8 +3,8 @@ use crate::*; use safe_arith::SafeArith; use serde::{Deserialize, Serialize}; use ssz_types::typenum::{ - bit::B0, UInt, Unsigned, U0, U1024, U1048576, U1073741824, U1099511627776, U128, U131072, U16, - U16777216, U2, U2048, U256, U32, U4, U4096, U512, U6, U625, U64, U65536, U8, U8192, + bit::B0, UInt, U0, U1024, U1048576, U1073741824, U1099511627776, U128, U131072, U16, U16777216, + U2, U2048, U256, U32, U4, U4096, U512, U6, U625, U64, U65536, U8, U8192, }; use ssz_types::typenum::{U17, U9}; use std::fmt::{self, Debug}; @@ -134,6 +134,11 @@ pub trait EthSpec: /// Must be set to `BytesPerFieldElement * FieldElementsPerBlob`. type BytesPerBlob: Unsigned + Clone + Sync + Send + Debug + PartialEq; + /* + * New in Electra + */ + type ElectraPlaceholder: Unsigned + Clone + Sync + Send + Debug + PartialEq; + fn default_spec() -> ChainSpec; fn spec_name() -> EthSpecId; @@ -273,10 +278,15 @@ pub trait EthSpec: fn bytes_per_blob() -> usize { Self::BytesPerBlob::to_usize() } + /// Returns the `KZG_COMMITMENT_INCLUSION_PROOF_DEPTH` preset for this specification. fn kzg_proof_inclusion_proof_depth() -> usize { Self::KzgCommitmentInclusionProofDepth::to_usize() } + + fn electra_placeholder() -> usize { + Self::ElectraPlaceholder::to_usize() + } } /// Macro to inherit some type values from another EthSpec. @@ -327,6 +337,7 @@ impl EthSpec for MainnetEthSpec { type SlotsPerEth1VotingPeriod = U2048; // 64 epochs * 32 slots per epoch type MaxBlsToExecutionChanges = U16; type MaxWithdrawalsPerPayload = U16; + type ElectraPlaceholder = U16; fn default_spec() -> ChainSpec { ChainSpec::mainnet() @@ -378,7 +389,8 @@ impl EthSpec for MinimalEthSpec { MaxExtraDataBytes, MaxBlsToExecutionChanges, MaxBlobsPerBlock, - BytesPerFieldElement + BytesPerFieldElement, + ElectraPlaceholder }); fn default_spec() -> ChainSpec { @@ -430,6 +442,7 @@ impl EthSpec for GnosisEthSpec { type BytesPerFieldElement = U32; type BytesPerBlob = U131072; type KzgCommitmentInclusionProofDepth = U17; + type ElectraPlaceholder = U16; fn default_spec() -> ChainSpec { ChainSpec::gnosis() diff --git a/consensus/types/src/execution_payload.rs b/consensus/types/src/execution_payload.rs index 1dc5951b25..27dc8cab0a 100644 --- a/consensus/types/src/execution_payload.rs +++ b/consensus/types/src/execution_payload.rs @@ -7,15 +7,15 @@ use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; pub type Transaction = VariableList; -pub type Transactions = VariableList< - Transaction<::MaxBytesPerTransaction>, - ::MaxTransactionsPerPayload, +pub type Transactions = VariableList< + Transaction<::MaxBytesPerTransaction>, + ::MaxTransactionsPerPayload, >; -pub type Withdrawals = VariableList::MaxWithdrawalsPerPayload>; +pub type Withdrawals = VariableList::MaxWithdrawalsPerPayload>; #[superstruct( - variants(Merge, Capella, Deneb), + variants(Merge, Capella, Deneb, Electra), variant_attributes( derive( Default, @@ -30,9 +30,9 @@ pub type Withdrawals = VariableList::MaxWithdrawal Derivative, arbitrary::Arbitrary ), - derivative(PartialEq, Hash(bound = "T: EthSpec")), - serde(bound = "T: EthSpec", deny_unknown_fields), - arbitrary(bound = "T: EthSpec") + derivative(PartialEq, Hash(bound = "E: EthSpec")), + serde(bound = "E: EthSpec", deny_unknown_fields), + arbitrary(bound = "E: EthSpec") ), cast_error(ty = "Error", expr = "BeaconStateError::IncorrectStateVariant"), partial_getter_error(ty = "Error", expr = "BeaconStateError::IncorrectStateVariant"), @@ -42,12 +42,12 @@ pub type Withdrawals = VariableList::MaxWithdrawal #[derive( Debug, Clone, Serialize, Encode, Deserialize, TreeHash, Derivative, arbitrary::Arbitrary, )] -#[derivative(PartialEq, Hash(bound = "T: EthSpec"))] -#[serde(bound = "T: EthSpec", untagged)] -#[arbitrary(bound = "T: EthSpec")] +#[derivative(PartialEq, Hash(bound = "E: EthSpec"))] +#[serde(bound = "E: EthSpec", untagged)] +#[arbitrary(bound = "E: EthSpec")] #[ssz(enum_behaviour = "transparent")] #[tree_hash(enum_behaviour = "transparent")] -pub struct ExecutionPayload { +pub struct ExecutionPayload { #[superstruct(getter(copy))] pub parent_hash: ExecutionBlockHash, #[superstruct(getter(copy))] @@ -57,7 +57,7 @@ pub struct ExecutionPayload { #[superstruct(getter(copy))] pub receipts_root: Hash256, #[serde(with = "ssz_types::serde_utils::hex_fixed_vec")] - pub logs_bloom: FixedVector, + pub logs_bloom: FixedVector, #[superstruct(getter(copy))] pub prev_randao: Hash256, #[serde(with = "serde_utils::quoted_u64")] @@ -73,27 +73,27 @@ pub struct ExecutionPayload { #[superstruct(getter(copy))] pub timestamp: u64, #[serde(with = "ssz_types::serde_utils::hex_var_list")] - pub extra_data: VariableList, + pub extra_data: VariableList, #[serde(with = "serde_utils::quoted_u256")] #[superstruct(getter(copy))] pub base_fee_per_gas: Uint256, #[superstruct(getter(copy))] pub block_hash: ExecutionBlockHash, #[serde(with = "ssz_types::serde_utils::list_of_hex_var_list")] - pub transactions: Transactions, - #[superstruct(only(Capella, Deneb))] - pub withdrawals: Withdrawals, - #[superstruct(only(Deneb), partial_getter(copy))] + pub transactions: Transactions, + #[superstruct(only(Capella, Deneb, Electra))] + pub withdrawals: Withdrawals, + #[superstruct(only(Deneb, Electra), partial_getter(copy))] #[serde(with = "serde_utils::quoted_u64")] pub blob_gas_used: u64, - #[superstruct(only(Deneb), partial_getter(copy))] + #[superstruct(only(Deneb, Electra), partial_getter(copy))] #[serde(with = "serde_utils::quoted_u64")] pub excess_blob_gas: u64, } -impl<'a, T: EthSpec> ExecutionPayloadRef<'a, T> { +impl<'a, E: EthSpec> ExecutionPayloadRef<'a, E> { // this emulates clone on a normal reference type - pub fn clone_from_ref(&self) -> ExecutionPayload { + pub fn clone_from_ref(&self) -> ExecutionPayload { map_execution_payload_ref!(&'a _, self, move |payload, cons| { cons(payload); payload.clone().into() @@ -101,7 +101,7 @@ impl<'a, T: EthSpec> ExecutionPayloadRef<'a, T> { } } -impl ExecutionPayload { +impl ExecutionPayload { pub fn from_ssz_bytes(bytes: &[u8], fork_name: ForkName) -> Result { match fork_name { ForkName::Base | ForkName::Altair => Err(ssz::DecodeError::BytesInvalid(format!( @@ -110,6 +110,7 @@ impl ExecutionPayload { ForkName::Merge => ExecutionPayloadMerge::from_ssz_bytes(bytes).map(Self::Merge), ForkName::Capella => ExecutionPayloadCapella::from_ssz_bytes(bytes).map(Self::Capella), ForkName::Deneb => ExecutionPayloadDeneb::from_ssz_bytes(bytes).map(Self::Deneb), + ForkName::Electra => ExecutionPayloadElectra::from_ssz_bytes(bytes).map(Self::Electra), } } @@ -117,41 +118,54 @@ impl ExecutionPayload { /// Returns the maximum size of an execution payload. pub fn max_execution_payload_merge_size() -> usize { // Fixed part - ExecutionPayloadMerge::::default().as_ssz_bytes().len() + ExecutionPayloadMerge::::default().as_ssz_bytes().len() // Max size of variable length `extra_data` field - + (T::max_extra_data_bytes() * ::ssz_fixed_len()) + + (E::max_extra_data_bytes() * ::ssz_fixed_len()) // Max size of variable length `transactions` field - + (T::max_transactions_per_payload() * (ssz::BYTES_PER_LENGTH_OFFSET + T::max_bytes_per_transaction())) + + (E::max_transactions_per_payload() * (ssz::BYTES_PER_LENGTH_OFFSET + E::max_bytes_per_transaction())) } #[allow(clippy::arithmetic_side_effects)] /// Returns the maximum size of an execution payload. pub fn max_execution_payload_capella_size() -> usize { // Fixed part - ExecutionPayloadCapella::::default().as_ssz_bytes().len() + ExecutionPayloadCapella::::default().as_ssz_bytes().len() // Max size of variable length `extra_data` field - + (T::max_extra_data_bytes() * ::ssz_fixed_len()) + + (E::max_extra_data_bytes() * ::ssz_fixed_len()) // Max size of variable length `transactions` field - + (T::max_transactions_per_payload() * (ssz::BYTES_PER_LENGTH_OFFSET + T::max_bytes_per_transaction())) + + (E::max_transactions_per_payload() * (ssz::BYTES_PER_LENGTH_OFFSET + E::max_bytes_per_transaction())) // Max size of variable length `withdrawals` field - + (T::max_withdrawals_per_payload() * ::ssz_fixed_len()) + + (E::max_withdrawals_per_payload() * ::ssz_fixed_len()) } #[allow(clippy::arithmetic_side_effects)] /// Returns the maximum size of an execution payload. pub fn max_execution_payload_deneb_size() -> usize { // Fixed part - ExecutionPayloadDeneb::::default().as_ssz_bytes().len() + ExecutionPayloadDeneb::::default().as_ssz_bytes().len() // Max size of variable length `extra_data` field - + (T::max_extra_data_bytes() * ::ssz_fixed_len()) + + (E::max_extra_data_bytes() * ::ssz_fixed_len()) // Max size of variable length `transactions` field - + (T::max_transactions_per_payload() * (ssz::BYTES_PER_LENGTH_OFFSET + T::max_bytes_per_transaction())) + + (E::max_transactions_per_payload() * (ssz::BYTES_PER_LENGTH_OFFSET + E::max_bytes_per_transaction())) // Max size of variable length `withdrawals` field - + (T::max_withdrawals_per_payload() * ::ssz_fixed_len()) + + (E::max_withdrawals_per_payload() * ::ssz_fixed_len()) + } + + #[allow(clippy::arithmetic_side_effects)] + /// Returns the maximum size of an execution payload. + pub fn max_execution_payload_electra_size() -> usize { + // Fixed part + ExecutionPayloadElectra::::default().as_ssz_bytes().len() + // Max size of variable length `extra_data` field + + (E::max_extra_data_bytes() * ::ssz_fixed_len()) + // Max size of variable length `transactions` field + + (E::max_transactions_per_payload() * (ssz::BYTES_PER_LENGTH_OFFSET + E::max_bytes_per_transaction())) + // Max size of variable length `withdrawals` field + + (E::max_withdrawals_per_payload() * ::ssz_fixed_len()) } } -impl ForkVersionDeserialize for ExecutionPayload { +impl ForkVersionDeserialize for ExecutionPayload { fn deserialize_by_fork<'de, D: serde::Deserializer<'de>>( value: serde_json::value::Value, fork_name: ForkName, @@ -164,6 +178,7 @@ impl ForkVersionDeserialize for ExecutionPayload { ForkName::Merge => Self::Merge(serde_json::from_value(value).map_err(convert_err)?), ForkName::Capella => Self::Capella(serde_json::from_value(value).map_err(convert_err)?), ForkName::Deneb => Self::Deneb(serde_json::from_value(value).map_err(convert_err)?), + ForkName::Electra => Self::Electra(serde_json::from_value(value).map_err(convert_err)?), ForkName::Base | ForkName::Altair => { return Err(serde::de::Error::custom(format!( "ExecutionPayload failed to deserialize: unsupported fork '{}'", @@ -174,12 +189,13 @@ impl ForkVersionDeserialize for ExecutionPayload { } } -impl ExecutionPayload { +impl ExecutionPayload { pub fn fork_name(&self) -> ForkName { match self { ExecutionPayload::Merge(_) => ForkName::Merge, ExecutionPayload::Capella(_) => ForkName::Capella, ExecutionPayload::Deneb(_) => ForkName::Deneb, + ExecutionPayload::Electra(_) => ForkName::Electra, } } } diff --git a/consensus/types/src/execution_payload_header.rs b/consensus/types/src/execution_payload_header.rs index e0859c0a1e..3d0b0aca41 100644 --- a/consensus/types/src/execution_payload_header.rs +++ b/consensus/types/src/execution_payload_header.rs @@ -1,15 +1,14 @@ use crate::{test_utils::TestRandom, *}; use derivative::Derivative; use serde::{Deserialize, Serialize}; -use ssz::Decode; +use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; -use BeaconStateError; #[superstruct( - variants(Merge, Capella, Deneb), + variants(Merge, Capella, Deneb, Electra), variant_attributes( derive( Default, @@ -24,9 +23,9 @@ use BeaconStateError; Derivative, arbitrary::Arbitrary ), - derivative(PartialEq, Hash(bound = "T: EthSpec")), - serde(bound = "T: EthSpec", deny_unknown_fields), - arbitrary(bound = "T: EthSpec") + derivative(PartialEq, Hash(bound = "E: EthSpec")), + serde(bound = "E: EthSpec", deny_unknown_fields), + arbitrary(bound = "E: EthSpec") ), ref_attributes( derive(PartialEq, TreeHash, Debug), @@ -38,12 +37,12 @@ use BeaconStateError; #[derive( Debug, Clone, Serialize, Deserialize, Encode, TreeHash, Derivative, arbitrary::Arbitrary, )] -#[derivative(PartialEq, Hash(bound = "T: EthSpec"))] -#[serde(bound = "T: EthSpec", untagged)] -#[arbitrary(bound = "T: EthSpec")] +#[derivative(PartialEq, Hash(bound = "E: EthSpec"))] +#[serde(bound = "E: EthSpec", untagged)] +#[arbitrary(bound = "E: EthSpec")] #[tree_hash(enum_behaviour = "transparent")] #[ssz(enum_behaviour = "transparent")] -pub struct ExecutionPayloadHeader { +pub struct ExecutionPayloadHeader { #[superstruct(getter(copy))] pub parent_hash: ExecutionBlockHash, #[superstruct(getter(copy))] @@ -53,7 +52,7 @@ pub struct ExecutionPayloadHeader { #[superstruct(getter(copy))] pub receipts_root: Hash256, #[serde(with = "ssz_types::serde_utils::hex_fixed_vec")] - pub logs_bloom: FixedVector, + pub logs_bloom: FixedVector, #[superstruct(getter(copy))] pub prev_randao: Hash256, #[serde(with = "serde_utils::quoted_u64")] @@ -69,7 +68,7 @@ pub struct ExecutionPayloadHeader { #[superstruct(getter(copy))] pub timestamp: u64, #[serde(with = "ssz_types::serde_utils::hex_var_list")] - pub extra_data: VariableList, + pub extra_data: VariableList, #[serde(with = "serde_utils::quoted_u256")] #[superstruct(getter(copy))] pub base_fee_per_gas: Uint256, @@ -77,21 +76,21 @@ pub struct ExecutionPayloadHeader { pub block_hash: ExecutionBlockHash, #[superstruct(getter(copy))] pub transactions_root: Hash256, - #[superstruct(only(Capella, Deneb))] + #[superstruct(only(Capella, Deneb, Electra))] #[superstruct(getter(copy))] pub withdrawals_root: Hash256, - #[superstruct(only(Deneb))] + #[superstruct(only(Deneb, Electra))] #[serde(with = "serde_utils::quoted_u64")] #[superstruct(getter(copy))] pub blob_gas_used: u64, - #[superstruct(only(Deneb))] + #[superstruct(only(Deneb, Electra))] #[serde(with = "serde_utils::quoted_u64")] #[superstruct(getter(copy))] pub excess_blob_gas: u64, } -impl ExecutionPayloadHeader { - pub fn transactions(&self) -> Option<&Transactions> { +impl ExecutionPayloadHeader { + pub fn transactions(&self) -> Option<&Transactions> { None } @@ -105,11 +104,31 @@ impl ExecutionPayloadHeader { ExecutionPayloadHeaderCapella::from_ssz_bytes(bytes).map(Self::Capella) } ForkName::Deneb => ExecutionPayloadHeaderDeneb::from_ssz_bytes(bytes).map(Self::Deneb), + ForkName::Electra => { + ExecutionPayloadHeaderElectra::from_ssz_bytes(bytes).map(Self::Electra) + } + } + } + + #[allow(clippy::arithmetic_side_effects)] + pub fn ssz_max_var_len_for_fork(fork_name: ForkName) -> usize { + // Matching here in case variable fields are added in future forks. + // TODO(electra): review electra changes + match fork_name { + ForkName::Base + | ForkName::Altair + | ForkName::Merge + | ForkName::Capella + | ForkName::Deneb + | ForkName::Electra => { + // Max size of variable length `extra_data` field + E::max_extra_data_bytes() * ::ssz_fixed_len() + } } } } -impl<'a, T: EthSpec> ExecutionPayloadHeaderRef<'a, T> { +impl<'a, E: EthSpec> ExecutionPayloadHeaderRef<'a, E> { pub fn is_default_with_zero_roots(self) -> bool { map_execution_payload_header_ref!(&'a _, self, |inner, cons| { cons(inner); @@ -118,8 +137,8 @@ impl<'a, T: EthSpec> ExecutionPayloadHeaderRef<'a, T> { } } -impl ExecutionPayloadHeaderMerge { - pub fn upgrade_to_capella(&self) -> ExecutionPayloadHeaderCapella { +impl ExecutionPayloadHeaderMerge { + pub fn upgrade_to_capella(&self) -> ExecutionPayloadHeaderCapella { ExecutionPayloadHeaderCapella { parent_hash: self.parent_hash, fee_recipient: self.fee_recipient, @@ -140,8 +159,8 @@ impl ExecutionPayloadHeaderMerge { } } -impl ExecutionPayloadHeaderCapella { - pub fn upgrade_to_deneb(&self) -> ExecutionPayloadHeaderDeneb { +impl ExecutionPayloadHeaderCapella { + pub fn upgrade_to_deneb(&self) -> ExecutionPayloadHeaderDeneb { ExecutionPayloadHeaderDeneb { parent_hash: self.parent_hash, fee_recipient: self.fee_recipient, @@ -164,8 +183,32 @@ impl ExecutionPayloadHeaderCapella { } } -impl<'a, T: EthSpec> From<&'a ExecutionPayloadMerge> for ExecutionPayloadHeaderMerge { - fn from(payload: &'a ExecutionPayloadMerge) -> Self { +impl ExecutionPayloadHeaderDeneb { + pub fn upgrade_to_electra(&self) -> ExecutionPayloadHeaderElectra { + ExecutionPayloadHeaderElectra { + parent_hash: self.parent_hash, + fee_recipient: self.fee_recipient, + state_root: self.state_root, + receipts_root: self.receipts_root, + logs_bloom: self.logs_bloom.clone(), + prev_randao: self.prev_randao, + block_number: self.block_number, + gas_limit: self.gas_limit, + gas_used: self.gas_used, + timestamp: self.timestamp, + extra_data: self.extra_data.clone(), + base_fee_per_gas: self.base_fee_per_gas, + block_hash: self.block_hash, + transactions_root: self.transactions_root, + withdrawals_root: self.withdrawals_root, + blob_gas_used: self.blob_gas_used, + excess_blob_gas: self.excess_blob_gas, + } + } +} + +impl<'a, E: EthSpec> From<&'a ExecutionPayloadMerge> for ExecutionPayloadHeaderMerge { + fn from(payload: &'a ExecutionPayloadMerge) -> Self { Self { parent_hash: payload.parent_hash, fee_recipient: payload.fee_recipient, @@ -184,8 +227,9 @@ impl<'a, T: EthSpec> From<&'a ExecutionPayloadMerge> for ExecutionPayloadHead } } } -impl<'a, T: EthSpec> From<&'a ExecutionPayloadCapella> for ExecutionPayloadHeaderCapella { - fn from(payload: &'a ExecutionPayloadCapella) -> Self { + +impl<'a, E: EthSpec> From<&'a ExecutionPayloadCapella> for ExecutionPayloadHeaderCapella { + fn from(payload: &'a ExecutionPayloadCapella) -> Self { Self { parent_hash: payload.parent_hash, fee_recipient: payload.fee_recipient, @@ -206,8 +250,32 @@ impl<'a, T: EthSpec> From<&'a ExecutionPayloadCapella> for ExecutionPayloadHe } } -impl<'a, T: EthSpec> From<&'a ExecutionPayloadDeneb> for ExecutionPayloadHeaderDeneb { - fn from(payload: &'a ExecutionPayloadDeneb) -> Self { +impl<'a, E: EthSpec> From<&'a ExecutionPayloadDeneb> for ExecutionPayloadHeaderDeneb { + fn from(payload: &'a ExecutionPayloadDeneb) -> Self { + Self { + parent_hash: payload.parent_hash, + fee_recipient: payload.fee_recipient, + state_root: payload.state_root, + receipts_root: payload.receipts_root, + logs_bloom: payload.logs_bloom.clone(), + prev_randao: payload.prev_randao, + block_number: payload.block_number, + gas_limit: payload.gas_limit, + gas_used: payload.gas_used, + timestamp: payload.timestamp, + extra_data: payload.extra_data.clone(), + base_fee_per_gas: payload.base_fee_per_gas, + block_hash: payload.block_hash, + transactions_root: payload.transactions.tree_hash_root(), + withdrawals_root: payload.withdrawals.tree_hash_root(), + blob_gas_used: payload.blob_gas_used, + excess_blob_gas: payload.excess_blob_gas, + } + } +} + +impl<'a, E: EthSpec> From<&'a ExecutionPayloadElectra> for ExecutionPayloadHeaderElectra { + fn from(payload: &'a ExecutionPayloadElectra) -> Self { Self { parent_hash: payload.parent_hash, fee_recipient: payload.fee_recipient, @@ -232,26 +300,32 @@ impl<'a, T: EthSpec> From<&'a ExecutionPayloadDeneb> for ExecutionPayloadHead // These impls are required to work around an inelegance in `to_execution_payload_header`. // They only clone headers so they should be relatively cheap. -impl<'a, T: EthSpec> From<&'a Self> for ExecutionPayloadHeaderMerge { +impl<'a, E: EthSpec> From<&'a Self> for ExecutionPayloadHeaderMerge { fn from(payload: &'a Self) -> Self { payload.clone() } } -impl<'a, T: EthSpec> From<&'a Self> for ExecutionPayloadHeaderCapella { +impl<'a, E: EthSpec> From<&'a Self> for ExecutionPayloadHeaderCapella { fn from(payload: &'a Self) -> Self { payload.clone() } } -impl<'a, T: EthSpec> From<&'a Self> for ExecutionPayloadHeaderDeneb { +impl<'a, E: EthSpec> From<&'a Self> for ExecutionPayloadHeaderDeneb { fn from(payload: &'a Self) -> Self { payload.clone() } } -impl<'a, T: EthSpec> From> for ExecutionPayloadHeader { - fn from(payload: ExecutionPayloadRef<'a, T>) -> Self { +impl<'a, E: EthSpec> From<&'a Self> for ExecutionPayloadHeaderElectra { + fn from(payload: &'a Self) -> Self { + payload.clone() + } +} + +impl<'a, E: EthSpec> From> for ExecutionPayloadHeader { + fn from(payload: ExecutionPayloadRef<'a, E>) -> Self { map_execution_payload_ref_into_execution_payload_header!( &'a _, payload, @@ -260,18 +334,18 @@ impl<'a, T: EthSpec> From> for ExecutionPayloadHeader } } -impl TryFrom> for ExecutionPayloadHeaderMerge { +impl TryFrom> for ExecutionPayloadHeaderMerge { type Error = BeaconStateError; - fn try_from(header: ExecutionPayloadHeader) -> Result { + fn try_from(header: ExecutionPayloadHeader) -> Result { match header { ExecutionPayloadHeader::Merge(execution_payload_header) => Ok(execution_payload_header), _ => Err(BeaconStateError::IncorrectStateVariant), } } } -impl TryFrom> for ExecutionPayloadHeaderCapella { +impl TryFrom> for ExecutionPayloadHeaderCapella { type Error = BeaconStateError; - fn try_from(header: ExecutionPayloadHeader) -> Result { + fn try_from(header: ExecutionPayloadHeader) -> Result { match header { ExecutionPayloadHeader::Capella(execution_payload_header) => { Ok(execution_payload_header) @@ -280,9 +354,9 @@ impl TryFrom> for ExecutionPayloadHeaderCa } } } -impl TryFrom> for ExecutionPayloadHeaderDeneb { +impl TryFrom> for ExecutionPayloadHeaderDeneb { type Error = BeaconStateError; - fn try_from(header: ExecutionPayloadHeader) -> Result { + fn try_from(header: ExecutionPayloadHeader) -> Result { match header { ExecutionPayloadHeader::Deneb(execution_payload_header) => Ok(execution_payload_header), _ => Err(BeaconStateError::IncorrectStateVariant), @@ -290,7 +364,19 @@ impl TryFrom> for ExecutionPayloadHeaderDe } } -impl ForkVersionDeserialize for ExecutionPayloadHeader { +impl TryFrom> for ExecutionPayloadHeaderElectra { + type Error = BeaconStateError; + fn try_from(header: ExecutionPayloadHeader) -> Result { + match header { + ExecutionPayloadHeader::Electra(execution_payload_header) => { + Ok(execution_payload_header) + } + _ => Err(BeaconStateError::IncorrectStateVariant), + } + } +} + +impl ForkVersionDeserialize for ExecutionPayloadHeader { fn deserialize_by_fork<'de, D: serde::Deserializer<'de>>( value: serde_json::value::Value, fork_name: ForkName, @@ -306,6 +392,7 @@ impl ForkVersionDeserialize for ExecutionPayloadHeader { ForkName::Merge => Self::Merge(serde_json::from_value(value).map_err(convert_err)?), ForkName::Capella => Self::Capella(serde_json::from_value(value).map_err(convert_err)?), ForkName::Deneb => Self::Deneb(serde_json::from_value(value).map_err(convert_err)?), + ForkName::Electra => Self::Electra(serde_json::from_value(value).map_err(convert_err)?), ForkName::Base | ForkName::Altair => { return Err(serde::de::Error::custom(format!( "ExecutionPayloadHeader failed to deserialize: unsupported fork '{}'", diff --git a/consensus/types/src/fork_context.rs b/consensus/types/src/fork_context.rs index 9992892714..6b052d8397 100644 --- a/consensus/types/src/fork_context.rs +++ b/consensus/types/src/fork_context.rs @@ -17,7 +17,7 @@ impl ForkContext { /// fork digest. /// /// A fork is disabled in the `ChainSpec` if the activation slot corresponding to that fork is `None`. - pub fn new( + pub fn new( current_slot: Slot, genesis_validators_root: Hash256, spec: &ChainSpec, @@ -62,6 +62,13 @@ impl ForkContext { )); } + if spec.electra_fork_epoch.is_some() { + fork_to_digest.push(( + ForkName::Electra, + ChainSpec::compute_fork_digest(spec.electra_fork_version, genesis_validators_root), + )); + } + let fork_to_digest: HashMap = fork_to_digest.into_iter().collect(); let digest_to_fork = fork_to_digest @@ -71,7 +78,7 @@ impl ForkContext { .collect(); Self { - current_fork: RwLock::new(spec.fork_name_at_slot::(current_slot)), + current_fork: RwLock::new(spec.fork_name_at_slot::(current_slot)), fork_to_digest, digest_to_fork, spec: spec.clone(), diff --git a/consensus/types/src/fork_name.rs b/consensus/types/src/fork_name.rs index 6523b2a678..f6bd5cbadf 100644 --- a/consensus/types/src/fork_name.rs +++ b/consensus/types/src/fork_name.rs @@ -1,11 +1,12 @@ use crate::{ChainSpec, Epoch}; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; -use std::convert::TryFrom; use std::fmt::{self, Display, Formatter}; use std::str::FromStr; -#[derive(Debug, Clone, Copy, Decode, Encode, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive( + Debug, Clone, Copy, Decode, Encode, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, +)] #[serde(try_from = "String")] #[serde(into = "String")] #[ssz(enum_behaviour = "tag")] @@ -15,6 +16,7 @@ pub enum ForkName { Merge, Capella, Deneb, + Electra, } impl ForkName { @@ -25,6 +27,7 @@ impl ForkName { ForkName::Merge, ForkName::Capella, ForkName::Deneb, + ForkName::Electra, ] } @@ -43,6 +46,7 @@ impl ForkName { spec.bellatrix_fork_epoch = None; spec.capella_fork_epoch = None; spec.deneb_fork_epoch = None; + spec.electra_fork_epoch = None; spec } ForkName::Altair => { @@ -50,6 +54,7 @@ impl ForkName { spec.bellatrix_fork_epoch = None; spec.capella_fork_epoch = None; spec.deneb_fork_epoch = None; + spec.electra_fork_epoch = None; spec } ForkName::Merge => { @@ -57,6 +62,7 @@ impl ForkName { spec.bellatrix_fork_epoch = Some(Epoch::new(0)); spec.capella_fork_epoch = None; spec.deneb_fork_epoch = None; + spec.electra_fork_epoch = None; spec } ForkName::Capella => { @@ -64,6 +70,7 @@ impl ForkName { spec.bellatrix_fork_epoch = Some(Epoch::new(0)); spec.capella_fork_epoch = Some(Epoch::new(0)); spec.deneb_fork_epoch = None; + spec.electra_fork_epoch = None; spec } ForkName::Deneb => { @@ -71,6 +78,15 @@ impl ForkName { spec.bellatrix_fork_epoch = Some(Epoch::new(0)); spec.capella_fork_epoch = Some(Epoch::new(0)); spec.deneb_fork_epoch = Some(Epoch::new(0)); + spec.electra_fork_epoch = None; + spec + } + ForkName::Electra => { + spec.altair_fork_epoch = Some(Epoch::new(0)); + spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + spec.capella_fork_epoch = Some(Epoch::new(0)); + spec.deneb_fork_epoch = Some(Epoch::new(0)); + spec.electra_fork_epoch = Some(Epoch::new(0)); spec } } @@ -86,6 +102,7 @@ impl ForkName { ForkName::Merge => Some(ForkName::Altair), ForkName::Capella => Some(ForkName::Merge), ForkName::Deneb => Some(ForkName::Capella), + ForkName::Electra => Some(ForkName::Deneb), } } @@ -98,7 +115,8 @@ impl ForkName { ForkName::Altair => Some(ForkName::Merge), ForkName::Merge => Some(ForkName::Capella), ForkName::Capella => Some(ForkName::Deneb), - ForkName::Deneb => None, + ForkName::Deneb => Some(ForkName::Electra), + ForkName::Electra => None, } } } @@ -148,6 +166,10 @@ macro_rules! map_fork_name_with { let (value, extra_data) = $body; ($t::Deneb(value), extra_data) } + ForkName::Electra => { + let (value, extra_data) = $body; + ($t::Electra(value), extra_data) + } } }; } @@ -162,6 +184,7 @@ impl FromStr for ForkName { "bellatrix" | "merge" => ForkName::Merge, "capella" => ForkName::Capella, "deneb" => ForkName::Deneb, + "electra" => ForkName::Electra, _ => return Err(format!("unknown fork name: {}", fork_name)), }) } @@ -175,6 +198,7 @@ impl Display for ForkName { ForkName::Merge => "bellatrix".fmt(f), ForkName::Capella => "capella".fmt(f), ForkName::Deneb => "deneb".fmt(f), + ForkName::Electra => "electra".fmt(f), } } } @@ -250,4 +274,13 @@ mod test { } assert_eq!(ForkName::latest(), fork); } + + #[test] + fn fork_ord_consistent() { + for (prev_fork, fork) in ForkName::list_all().into_iter().tuple_windows() { + assert_eq!(prev_fork.next_fork(), Some(fork)); + assert_eq!(fork.previous_fork(), Some(prev_fork)); + assert!(prev_fork < fork); + } + } } diff --git a/consensus/types/src/historical_batch.rs b/consensus/types/src/historical_batch.rs index e3e037fd63..1c565c0092 100644 --- a/consensus/types/src/historical_batch.rs +++ b/consensus/types/src/historical_batch.rs @@ -3,7 +3,6 @@ use crate::*; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; -use ssz_types::FixedVector; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; @@ -22,10 +21,10 @@ use tree_hash_derive::TreeHash; TestRandom, arbitrary::Arbitrary, )] -#[arbitrary(bound = "T: EthSpec")] -pub struct HistoricalBatch { - pub block_roots: FixedVector, - pub state_roots: FixedVector, +#[arbitrary(bound = "E: EthSpec")] +pub struct HistoricalBatch { + pub block_roots: FixedVector, + pub state_roots: FixedVector, } #[cfg(test)] diff --git a/consensus/types/src/historical_summary.rs b/consensus/types/src/historical_summary.rs index dcc387d3d6..95d015a0f7 100644 --- a/consensus/types/src/historical_summary.rs +++ b/consensus/types/src/historical_summary.rs @@ -37,7 +37,7 @@ pub struct HistoricalSummary { } impl HistoricalSummary { - pub fn new(state: &BeaconState) -> Self { + pub fn new(state: &BeaconState) -> Self { Self { block_summary_root: state.block_roots().tree_hash_root(), state_summary_root: state.state_roots().tree_hash_root(), @@ -81,7 +81,7 @@ impl<'a, N: Unsigned> CachedTreeHash for HistoricalSummaryCache<' pub fn leaf_iter( values: &[HistoricalSummary], -) -> impl Iterator + ExactSizeIterator + '_ { +) -> impl ExactSizeIterator + '_ { values .iter() .map(|value| value.tree_hash_root()) diff --git a/consensus/types/src/indexed_attestation.rs b/consensus/types/src/indexed_attestation.rs index c2d48d7242..d80b49d55a 100644 --- a/consensus/types/src/indexed_attestation.rs +++ b/consensus/types/src/indexed_attestation.rs @@ -25,17 +25,17 @@ use tree_hash_derive::TreeHash; arbitrary::Arbitrary, )] #[derivative(PartialEq, Eq)] // to satisfy Clippy's lint about `Hash` -#[serde(bound = "T: EthSpec")] -#[arbitrary(bound = "T: EthSpec")] -pub struct IndexedAttestation { +#[serde(bound = "E: EthSpec")] +#[arbitrary(bound = "E: EthSpec")] +pub struct IndexedAttestation { /// Lists validator registry indices, not committee indices. #[serde(with = "quoted_variable_list_u64")] - pub attesting_indices: VariableList, + pub attesting_indices: VariableList, pub data: AttestationData, pub signature: AggregateSignature, } -impl IndexedAttestation { +impl IndexedAttestation { /// Check if ``attestation_data_1`` and ``attestation_data_2`` have the same target. /// /// Spec v0.12.1 @@ -57,7 +57,7 @@ impl IndexedAttestation { /// Guarantees `att1 == att2 -> hash(att1) == hash(att2)`. /// /// Used in the operation pool. -impl Hash for IndexedAttestation { +impl Hash for IndexedAttestation { fn hash(&self, state: &mut H) { self.attesting_indices.hash(state); self.data.hash(state); @@ -106,7 +106,7 @@ mod quoted_variable_list_u64 { mod tests { use super::*; use crate::slot_epoch::Epoch; - use crate::test_utils::{SeedableRng, TestRandom, XorShiftRng}; + use crate::test_utils::{SeedableRng, XorShiftRng}; use crate::MainnetEthSpec; #[test] diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index b07b497a2a..6551ebc1dd 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -74,6 +74,7 @@ pub mod voluntary_exit; pub mod withdrawal_credentials; #[macro_use] pub mod slot_epoch_macros; +pub mod activation_queue; pub mod config_and_preset; pub mod execution_block_header; pub mod fork_context; @@ -94,6 +95,7 @@ mod tree_hash_impls; pub mod validator_registration_data; pub mod withdrawal; +pub mod epoch_cache; pub mod slot_data; #[cfg(feature = "sqlite")] pub mod sqlite; @@ -105,6 +107,7 @@ pub mod runtime_var_list; use ethereum_types::{H160, H256}; +pub use crate::activation_queue::ActivationQueue; pub use crate::aggregate_and_proof::AggregateAndProof; pub use crate::attestation::{Attestation, Error as AttestationError}; pub use crate::attestation_data::AttestationData; @@ -112,11 +115,13 @@ pub use crate::attestation_duty::AttestationDuty; pub use crate::attester_slashing::AttesterSlashing; pub use crate::beacon_block::{ BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BeaconBlockCapella, BeaconBlockDeneb, - BeaconBlockMerge, BeaconBlockRef, BeaconBlockRefMut, BlindedBeaconBlock, EmptyBlock, + BeaconBlockElectra, BeaconBlockMerge, BeaconBlockRef, BeaconBlockRefMut, BlindedBeaconBlock, + EmptyBlock, }; pub use crate::beacon_block_body::{ BeaconBlockBody, BeaconBlockBodyAltair, BeaconBlockBodyBase, BeaconBlockBodyCapella, - BeaconBlockBodyDeneb, BeaconBlockBodyMerge, BeaconBlockBodyRef, BeaconBlockBodyRefMut, + BeaconBlockBodyDeneb, BeaconBlockBodyElectra, BeaconBlockBodyMerge, BeaconBlockBodyRef, + BeaconBlockBodyRefMut, }; pub use crate::beacon_block_header::BeaconBlockHeader; pub use crate::beacon_committee::{BeaconCommittee, OwnedBeaconCommittee}; @@ -125,24 +130,28 @@ pub use crate::blob_sidecar::{BlobSidecar, BlobSidecarList, BlobsList}; pub use crate::bls_to_execution_change::BlsToExecutionChange; pub use crate::chain_spec::{ChainSpec, Config, Domain}; pub use crate::checkpoint::Checkpoint; -pub use crate::config_and_preset::{ConfigAndPreset, ConfigAndPresetCapella, ConfigAndPresetDeneb}; +pub use crate::config_and_preset::{ + ConfigAndPreset, ConfigAndPresetCapella, ConfigAndPresetDeneb, ConfigAndPresetElectra, +}; pub use crate::contribution_and_proof::ContributionAndProof; pub use crate::deposit::{Deposit, DEPOSIT_TREE_DEPTH}; pub use crate::deposit_data::DepositData; pub use crate::deposit_message::DepositMessage; pub use crate::deposit_tree_snapshot::{DepositTreeSnapshot, FinalizedExecutionBlock}; pub use crate::enr_fork_id::EnrForkId; +pub use crate::epoch_cache::{EpochCache, EpochCacheError, EpochCacheKey}; pub use crate::eth1_data::Eth1Data; pub use crate::eth_spec::EthSpecId; pub use crate::execution_block_hash::ExecutionBlockHash; pub use crate::execution_block_header::ExecutionBlockHeader; pub use crate::execution_payload::{ - ExecutionPayload, ExecutionPayloadCapella, ExecutionPayloadDeneb, ExecutionPayloadMerge, - ExecutionPayloadRef, Transaction, Transactions, Withdrawals, + ExecutionPayload, ExecutionPayloadCapella, ExecutionPayloadDeneb, ExecutionPayloadElectra, + ExecutionPayloadMerge, ExecutionPayloadRef, Transaction, Transactions, Withdrawals, }; pub use crate::execution_payload_header::{ ExecutionPayloadHeader, ExecutionPayloadHeaderCapella, ExecutionPayloadHeaderDeneb, - ExecutionPayloadHeaderMerge, ExecutionPayloadHeaderRef, ExecutionPayloadHeaderRefMut, + ExecutionPayloadHeaderElectra, ExecutionPayloadHeaderMerge, ExecutionPayloadHeaderRef, + ExecutionPayloadHeaderRefMut, }; pub use crate::fork::Fork; pub use crate::fork_context::ForkContext; @@ -152,20 +161,37 @@ pub use crate::fork_versioned_response::{ForkVersionDeserialize, ForkVersionedRe pub use crate::graffiti::{Graffiti, GRAFFITI_BYTES_LEN}; pub use crate::historical_batch::HistoricalBatch; pub use crate::indexed_attestation::IndexedAttestation; -pub use crate::light_client_bootstrap::LightClientBootstrap; -pub use crate::light_client_finality_update::LightClientFinalityUpdate; -pub use crate::light_client_header::LightClientHeader; -pub use crate::light_client_optimistic_update::LightClientOptimisticUpdate; -pub use crate::light_client_update::{Error as LightClientError, LightClientUpdate}; +pub use crate::light_client_bootstrap::{ + LightClientBootstrap, LightClientBootstrapAltair, LightClientBootstrapCapella, + LightClientBootstrapDeneb, +}; +pub use crate::light_client_finality_update::{ + LightClientFinalityUpdate, LightClientFinalityUpdateAltair, LightClientFinalityUpdateCapella, + LightClientFinalityUpdateDeneb, +}; +pub use crate::light_client_header::{ + LightClientHeader, LightClientHeaderAltair, LightClientHeaderCapella, LightClientHeaderDeneb, +}; +pub use crate::light_client_optimistic_update::{ + LightClientOptimisticUpdate, LightClientOptimisticUpdateAltair, + LightClientOptimisticUpdateCapella, LightClientOptimisticUpdateDeneb, +}; +pub use crate::light_client_update::{ + Error as LightClientError, LightClientUpdate, LightClientUpdateAltair, + LightClientUpdateCapella, LightClientUpdateDeneb, +}; pub use crate::participation_flags::ParticipationFlags; pub use crate::participation_list::ParticipationList; pub use crate::payload::{ AbstractExecPayload, BlindedPayload, BlindedPayloadCapella, BlindedPayloadDeneb, - BlindedPayloadMerge, BlindedPayloadRef, BlockType, ExecPayload, FullPayload, - FullPayloadCapella, FullPayloadDeneb, FullPayloadMerge, FullPayloadRef, OwnedExecPayload, + BlindedPayloadElectra, BlindedPayloadMerge, BlindedPayloadRef, BlockType, ExecPayload, + FullPayload, FullPayloadCapella, FullPayloadDeneb, FullPayloadElectra, FullPayloadMerge, + FullPayloadRef, OwnedExecPayload, }; pub use crate::pending_attestation::PendingAttestation; -pub use crate::preset::{AltairPreset, BasePreset, BellatrixPreset, CapellaPreset, DenebPreset}; +pub use crate::preset::{ + AltairPreset, BasePreset, BellatrixPreset, CapellaPreset, DenebPreset, ElectraPreset, +}; pub use crate::proposer_preparation_data::ProposerPreparationData; pub use crate::proposer_slashing::ProposerSlashing; pub use crate::relative_epoch::{Error as RelativeEpochError, RelativeEpoch}; @@ -176,8 +202,8 @@ pub use crate::signed_aggregate_and_proof::SignedAggregateAndProof; pub use crate::signed_beacon_block::{ ssz_tagged_signed_beacon_block, ssz_tagged_signed_beacon_block_arc, SignedBeaconBlock, SignedBeaconBlockAltair, SignedBeaconBlockBase, SignedBeaconBlockCapella, - SignedBeaconBlockDeneb, SignedBeaconBlockHash, SignedBeaconBlockMerge, - SignedBlindedBeaconBlock, + SignedBeaconBlockDeneb, SignedBeaconBlockElectra, SignedBeaconBlockHash, + SignedBeaconBlockMerge, SignedBlindedBeaconBlock, }; pub use crate::signed_beacon_block_header::SignedBeaconBlockHeader; pub use crate::signed_bls_to_execution_change::SignedBlsToExecutionChange; @@ -208,8 +234,8 @@ pub type Uint256 = ethereum_types::U256; pub type Address = H160; pub type ForkVersion = [u8; 4]; pub type BLSFieldElement = Uint256; -pub type Blob = FixedVector::BytesPerBlob>; -pub type KzgProofs = VariableList::MaxBlobCommitmentsPerBlock>; +pub type Blob = FixedVector::BytesPerBlob>; +pub type KzgProofs = VariableList::MaxBlobCommitmentsPerBlock>; pub type VersionedHash = Hash256; pub type Hash64 = ethereum_types::H64; diff --git a/consensus/types/src/light_client_bootstrap.rs b/consensus/types/src/light_client_bootstrap.rs index 6660783abd..33fbf214c8 100644 --- a/consensus/types/src/light_client_bootstrap.rs +++ b/consensus/types/src/light_client_bootstrap.rs @@ -1,68 +1,167 @@ -use super::{BeaconState, EthSpec, FixedVector, Hash256, SyncCommittee}; +use super::{BeaconState, EthSpec, FixedVector, Hash256, LightClientHeader, SyncCommittee}; use crate::{ - light_client_update::*, test_utils::TestRandom, ForkName, ForkVersionDeserialize, - LightClientHeader, + light_client_update::*, test_utils::TestRandom, ChainSpec, ForkName, ForkVersionDeserialize, + LightClientHeaderAltair, LightClientHeaderCapella, LightClientHeaderDeneb, SignedBeaconBlock, + Slot, }; +use derivative::Derivative; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; +use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use std::sync::Arc; +use superstruct::superstruct; use test_random_derive::TestRandom; +use tree_hash_derive::TreeHash; /// A LightClientBootstrap is the initializer we send over to light_client nodes /// that are trying to generate their basic storage when booting up. -#[derive( - Debug, - Clone, - PartialEq, - Serialize, - Deserialize, - Encode, - Decode, - TestRandom, - arbitrary::Arbitrary, +#[superstruct( + variants(Altair, Capella, Deneb), + variant_attributes( + derive( + Debug, + Clone, + PartialEq, + Serialize, + Deserialize, + Derivative, + Decode, + Encode, + TestRandom, + arbitrary::Arbitrary, + TreeHash, + ), + serde(bound = "E: EthSpec", deny_unknown_fields), + arbitrary(bound = "E: EthSpec"), + ) )] -#[serde(bound = "T: EthSpec")] -#[arbitrary(bound = "T: EthSpec")] -pub struct LightClientBootstrap { +#[derive( + Debug, Clone, Serialize, TreeHash, Encode, Deserialize, arbitrary::Arbitrary, PartialEq, +)] +#[serde(untagged)] +#[tree_hash(enum_behaviour = "transparent")] +#[ssz(enum_behaviour = "transparent")] +#[serde(bound = "E: EthSpec", deny_unknown_fields)] +#[arbitrary(bound = "E: EthSpec")] +pub struct LightClientBootstrap { /// The requested beacon block header. - pub header: LightClientHeader, + #[superstruct(only(Altair), partial_getter(rename = "header_altair"))] + pub header: LightClientHeaderAltair, + #[superstruct(only(Capella), partial_getter(rename = "header_capella"))] + pub header: LightClientHeaderCapella, + #[superstruct(only(Deneb), partial_getter(rename = "header_deneb"))] + pub header: LightClientHeaderDeneb, /// The `SyncCommittee` used in the requested period. - pub current_sync_committee: Arc>, + pub current_sync_committee: Arc>, /// Merkle proof for sync committee pub current_sync_committee_branch: FixedVector, } -impl LightClientBootstrap { - pub fn from_beacon_state(beacon_state: &mut BeaconState) -> Result { +impl LightClientBootstrap { + pub fn map_with_fork_name(&self, func: F) -> R + where + F: Fn(ForkName) -> R, + { + match self { + Self::Altair(_) => func(ForkName::Altair), + Self::Capella(_) => func(ForkName::Capella), + Self::Deneb(_) => func(ForkName::Deneb), + } + } + + pub fn get_slot<'a>(&'a self) -> Slot { + map_light_client_bootstrap_ref!(&'a _, self.to_ref(), |inner, cons| { + cons(inner); + inner.header.beacon.slot + }) + } + + pub fn from_ssz_bytes(bytes: &[u8], fork_name: ForkName) -> Result { + let bootstrap = match fork_name { + ForkName::Altair | ForkName::Merge => { + Self::Altair(LightClientBootstrapAltair::from_ssz_bytes(bytes)?) + } + ForkName::Capella => Self::Capella(LightClientBootstrapCapella::from_ssz_bytes(bytes)?), + ForkName::Deneb | ForkName::Electra => { + Self::Deneb(LightClientBootstrapDeneb::from_ssz_bytes(bytes)?) + } + ForkName::Base => { + return Err(ssz::DecodeError::BytesInvalid(format!( + "LightClientBootstrap decoding for {fork_name} not implemented" + ))) + } + }; + + Ok(bootstrap) + } + + #[allow(clippy::arithmetic_side_effects)] + pub fn ssz_max_len_for_fork(fork_name: ForkName) -> usize { + // TODO(electra): review electra changes + match fork_name { + ForkName::Base => 0, + ForkName::Altair + | ForkName::Merge + | ForkName::Capella + | ForkName::Deneb + | ForkName::Electra => { + as Encode>::ssz_fixed_len() + + LightClientHeader::::ssz_max_var_len_for_fork(fork_name) + } + } + } + + pub fn from_beacon_state( + beacon_state: &mut BeaconState, + block: &SignedBeaconBlock, + chain_spec: &ChainSpec, + ) -> Result { let mut header = beacon_state.latest_block_header().clone(); header.state_root = beacon_state.update_tree_hash_cache()?; let current_sync_committee_branch = - beacon_state.compute_merkle_proof(CURRENT_SYNC_COMMITTEE_INDEX)?; - Ok(LightClientBootstrap { - header: header.into(), - current_sync_committee: beacon_state.current_sync_committee()?.clone(), - current_sync_committee_branch: FixedVector::new(current_sync_committee_branch)?, - }) + FixedVector::new(beacon_state.compute_merkle_proof(CURRENT_SYNC_COMMITTEE_INDEX)?)?; + + let current_sync_committee = beacon_state.current_sync_committee()?.clone(); + + let light_client_bootstrap = match block + .fork_name(chain_spec) + .map_err(|_| Error::InconsistentFork)? + { + ForkName::Base => return Err(Error::AltairForkNotActive), + ForkName::Altair | ForkName::Merge => Self::Altair(LightClientBootstrapAltair { + header: LightClientHeaderAltair::block_to_light_client_header(block)?, + current_sync_committee, + current_sync_committee_branch, + }), + ForkName::Capella => Self::Capella(LightClientBootstrapCapella { + header: LightClientHeaderCapella::block_to_light_client_header(block)?, + current_sync_committee, + current_sync_committee_branch, + }), + ForkName::Deneb | ForkName::Electra => Self::Deneb(LightClientBootstrapDeneb { + header: LightClientHeaderDeneb::block_to_light_client_header(block)?, + current_sync_committee, + current_sync_committee_branch, + }), + }; + + Ok(light_client_bootstrap) } } -impl ForkVersionDeserialize for LightClientBootstrap { +impl ForkVersionDeserialize for LightClientBootstrap { fn deserialize_by_fork<'de, D: Deserializer<'de>>( value: Value, fork_name: ForkName, ) -> Result { match fork_name { - ForkName::Altair | ForkName::Merge => { - Ok(serde_json::from_value::>(value) - .map_err(serde::de::Error::custom))? - } - ForkName::Base | ForkName::Capella | ForkName::Deneb => { - Err(serde::de::Error::custom(format!( - "LightClientBootstrap failed to deserialize: unsupported fork '{}'", - fork_name - ))) - } + ForkName::Base => Err(serde::de::Error::custom(format!( + "LightClientBootstrap failed to deserialize: unsupported fork '{}'", + fork_name + ))), + _ => Ok(serde_json::from_value::>(value) + .map_err(serde::de::Error::custom))?, } } } @@ -72,5 +171,5 @@ mod tests { use super::*; use crate::MainnetEthSpec; - ssz_tests!(LightClientBootstrap); + ssz_tests!(LightClientBootstrapDeneb); } diff --git a/consensus/types/src/light_client_finality_update.rs b/consensus/types/src/light_client_finality_update.rs index 494e68b63f..c5c730e8b8 100644 --- a/consensus/types/src/light_client_finality_update.rs +++ b/consensus/types/src/light_client_finality_update.rs @@ -1,101 +1,201 @@ -use super::{ - EthSpec, FixedVector, Hash256, SignedBeaconBlock, SignedBlindedBeaconBlock, Slot, SyncAggregate, -}; +use super::{EthSpec, FixedVector, Hash256, LightClientHeader, Slot, SyncAggregate}; +use crate::ChainSpec; use crate::{ - light_client_update::*, test_utils::TestRandom, BeaconState, ChainSpec, ForkName, - ForkVersionDeserialize, LightClientHeader, + light_client_update::*, test_utils::TestRandom, ForkName, ForkVersionDeserialize, + LightClientHeaderAltair, LightClientHeaderCapella, LightClientHeaderDeneb, SignedBeaconBlock, }; +use derivative::Derivative; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; -use ssz_derive::{Decode, Encode}; +use ssz::{Decode, Encode}; +use ssz_derive::Decode; +use ssz_derive::Encode; +use superstruct::superstruct; use test_random_derive::TestRandom; -use tree_hash::TreeHash; +use tree_hash_derive::TreeHash; -/// A LightClientFinalityUpdate is the update light_client request or received by a gossip that -/// signal a new finalized beacon block header for the light client sync protocol. -#[derive( - Debug, - Clone, - PartialEq, - Serialize, - Deserialize, - Encode, - Decode, - TestRandom, - arbitrary::Arbitrary, +#[superstruct( + variants(Altair, Capella, Deneb), + variant_attributes( + derive( + Debug, + Clone, + PartialEq, + Serialize, + Deserialize, + Derivative, + Decode, + Encode, + TestRandom, + arbitrary::Arbitrary, + TreeHash, + ), + serde(bound = "E: EthSpec", deny_unknown_fields), + arbitrary(bound = "E: EthSpec"), + ) )] -#[serde(bound = "T: EthSpec")] -#[arbitrary(bound = "T: EthSpec")] -pub struct LightClientFinalityUpdate { +#[derive( + Debug, Clone, Serialize, Encode, TreeHash, Deserialize, arbitrary::Arbitrary, PartialEq, +)] +#[serde(untagged)] +#[tree_hash(enum_behaviour = "transparent")] +#[ssz(enum_behaviour = "transparent")] +#[serde(bound = "E: EthSpec", deny_unknown_fields)] +#[arbitrary(bound = "E: EthSpec")] +pub struct LightClientFinalityUpdate { /// The last `BeaconBlockHeader` from the last attested block by the sync committee. - pub attested_header: LightClientHeader, + #[superstruct(only(Altair), partial_getter(rename = "attested_header_altair"))] + pub attested_header: LightClientHeaderAltair, + #[superstruct(only(Capella), partial_getter(rename = "attested_header_capella"))] + pub attested_header: LightClientHeaderCapella, + #[superstruct(only(Deneb), partial_getter(rename = "attested_header_deneb"))] + pub attested_header: LightClientHeaderDeneb, /// The last `BeaconBlockHeader` from the last attested finalized block (end of epoch). - pub finalized_header: LightClientHeader, + #[superstruct(only(Altair), partial_getter(rename = "finalized_header_altair"))] + pub finalized_header: LightClientHeaderAltair, + #[superstruct(only(Capella), partial_getter(rename = "finalized_header_capella"))] + pub finalized_header: LightClientHeaderCapella, + #[superstruct(only(Deneb), partial_getter(rename = "finalized_header_deneb"))] + pub finalized_header: LightClientHeaderDeneb, /// Merkle proof attesting finalized header. pub finality_branch: FixedVector, - /// current sync aggreggate - pub sync_aggregate: SyncAggregate, - /// Slot of the sync aggregated singature + /// current sync aggregate + pub sync_aggregate: SyncAggregate, + /// Slot of the sync aggregated signature pub signature_slot: Slot, } -impl LightClientFinalityUpdate { +impl LightClientFinalityUpdate { pub fn new( + attested_block: &SignedBeaconBlock, + finalized_block: &SignedBeaconBlock, + finality_branch: FixedVector, + sync_aggregate: SyncAggregate, + signature_slot: Slot, chain_spec: &ChainSpec, - beacon_state: &BeaconState, - block: &SignedBeaconBlock, - attested_state: &mut BeaconState, - finalized_block: &SignedBlindedBeaconBlock, ) -> Result { - let altair_fork_epoch = chain_spec - .altair_fork_epoch - .ok_or(Error::AltairForkNotActive)?; - if beacon_state.slot().epoch(T::slots_per_epoch()) < altair_fork_epoch { - return Err(Error::AltairForkNotActive); + let finality_update = match attested_block + .fork_name(chain_spec) + .map_err(|_| Error::InconsistentFork)? + { + ForkName::Altair | ForkName::Merge => { + let finality_update = LightClientFinalityUpdateAltair { + attested_header: LightClientHeaderAltair::block_to_light_client_header( + attested_block, + )?, + finalized_header: LightClientHeaderAltair::block_to_light_client_header( + finalized_block, + )?, + finality_branch, + sync_aggregate, + signature_slot, + }; + Self::Altair(finality_update) + } + ForkName::Capella => { + let finality_update = LightClientFinalityUpdateCapella { + attested_header: LightClientHeaderCapella::block_to_light_client_header( + attested_block, + )?, + finalized_header: LightClientHeaderCapella::block_to_light_client_header( + finalized_block, + )?, + finality_branch, + sync_aggregate, + signature_slot, + }; + Self::Capella(finality_update) + } + ForkName::Deneb | ForkName::Electra => { + let finality_update = LightClientFinalityUpdateDeneb { + attested_header: LightClientHeaderDeneb::block_to_light_client_header( + attested_block, + )?, + finalized_header: LightClientHeaderDeneb::block_to_light_client_header( + finalized_block, + )?, + finality_branch, + sync_aggregate, + signature_slot, + }; + Self::Deneb(finality_update) + } + ForkName::Base => return Err(Error::AltairForkNotActive), + }; + + Ok(finality_update) + } + + pub fn map_with_fork_name(&self, func: F) -> R + where + F: Fn(ForkName) -> R, + { + match self { + Self::Altair(_) => func(ForkName::Altair), + Self::Capella(_) => func(ForkName::Capella), + Self::Deneb(_) => func(ForkName::Deneb), } + } - let sync_aggregate = block.message().body().sync_aggregate()?; - if sync_aggregate.num_set_bits() < chain_spec.min_sync_committee_participants as usize { - return Err(Error::NotEnoughSyncCommitteeParticipants); - } - - // Compute and validate attested header. - let mut attested_header = attested_state.latest_block_header().clone(); - attested_header.state_root = attested_state.update_tree_hash_cache()?; - // Build finalized header from finalized block - let finalized_header = finalized_block.message().block_header(); - - if finalized_header.tree_hash_root() != beacon_state.finalized_checkpoint().root { - return Err(Error::InvalidFinalizedBlock); - } - - let finality_branch = attested_state.compute_merkle_proof(FINALIZED_ROOT_INDEX)?; - Ok(Self { - attested_header: attested_header.into(), - finalized_header: finalized_header.into(), - finality_branch: FixedVector::new(finality_branch)?, - sync_aggregate: sync_aggregate.clone(), - signature_slot: block.slot(), + pub fn get_attested_header_slot<'a>(&'a self) -> Slot { + map_light_client_finality_update_ref!(&'a _, self.to_ref(), |inner, cons| { + cons(inner); + inner.attested_header.beacon.slot }) } + + pub fn from_ssz_bytes(bytes: &[u8], fork_name: ForkName) -> Result { + let finality_update = match fork_name { + ForkName::Altair | ForkName::Merge => { + Self::Altair(LightClientFinalityUpdateAltair::from_ssz_bytes(bytes)?) + } + ForkName::Capella => { + Self::Capella(LightClientFinalityUpdateCapella::from_ssz_bytes(bytes)?) + } + ForkName::Deneb | ForkName::Electra => { + Self::Deneb(LightClientFinalityUpdateDeneb::from_ssz_bytes(bytes)?) + } + ForkName::Base => { + return Err(ssz::DecodeError::BytesInvalid(format!( + "LightClientFinalityUpdate decoding for {fork_name} not implemented" + ))) + } + }; + + Ok(finality_update) + } + + #[allow(clippy::arithmetic_side_effects)] + pub fn ssz_max_len_for_fork(fork_name: ForkName) -> usize { + // TODO(electra): review electra changes + match fork_name { + ForkName::Base => 0, + ForkName::Altair + | ForkName::Merge + | ForkName::Capella + | ForkName::Deneb + | ForkName::Electra => { + as Encode>::ssz_fixed_len() + + 2 * LightClientHeader::::ssz_max_var_len_for_fork(fork_name) + } + } + } } -impl ForkVersionDeserialize for LightClientFinalityUpdate { +impl ForkVersionDeserialize for LightClientFinalityUpdate { fn deserialize_by_fork<'de, D: Deserializer<'de>>( value: Value, fork_name: ForkName, ) -> Result { match fork_name { - ForkName::Altair | ForkName::Merge => Ok(serde_json::from_value::< - LightClientFinalityUpdate, - >(value) - .map_err(serde::de::Error::custom))?, - ForkName::Base | ForkName::Capella | ForkName::Deneb => { - Err(serde::de::Error::custom(format!( - "LightClientFinalityUpdate failed to deserialize: unsupported fork '{}'", - fork_name - ))) - } + ForkName::Base => Err(serde::de::Error::custom(format!( + "LightClientFinalityUpdate failed to deserialize: unsupported fork '{}'", + fork_name + ))), + _ => Ok( + serde_json::from_value::>(value) + .map_err(serde::de::Error::custom), + )?, } } } @@ -105,5 +205,5 @@ mod tests { use super::*; use crate::MainnetEthSpec; - ssz_tests!(LightClientFinalityUpdate); + ssz_tests!(LightClientFinalityUpdateDeneb); } diff --git a/consensus/types/src/light_client_header.rs b/consensus/types/src/light_client_header.rs index 8fe31f7af8..647ece9949 100644 --- a/consensus/types/src/light_client_header.rs +++ b/consensus/types/src/light_client_header.rs @@ -1,26 +1,215 @@ -use crate::test_utils::TestRandom; -use crate::BeaconBlockHeader; +use crate::ChainSpec; +use crate::ForkName; +use crate::ForkVersionDeserialize; +use crate::{light_client_update::*, BeaconBlockBody}; +use crate::{ + test_utils::TestRandom, EthSpec, ExecutionPayloadHeaderCapella, ExecutionPayloadHeaderDeneb, + FixedVector, Hash256, SignedBeaconBlock, +}; +use crate::{BeaconBlockHeader, ExecutionPayloadHeader}; +use derivative::Derivative; use serde::{Deserialize, Serialize}; +use ssz::Decode; use ssz_derive::{Decode, Encode}; +use std::marker::PhantomData; +use superstruct::superstruct; use test_random_derive::TestRandom; +use tree_hash_derive::TreeHash; -#[derive( - Debug, - Clone, - PartialEq, - Serialize, - Deserialize, - Encode, - Decode, - TestRandom, - arbitrary::Arbitrary, +#[superstruct( + variants(Altair, Capella, Deneb), + variant_attributes( + derive( + Debug, + Clone, + PartialEq, + Serialize, + Deserialize, + Derivative, + Decode, + Encode, + TestRandom, + arbitrary::Arbitrary, + TreeHash, + ), + serde(bound = "E: EthSpec", deny_unknown_fields), + arbitrary(bound = "E: EthSpec"), + ) )] -pub struct LightClientHeader { +#[derive( + Debug, Clone, Serialize, TreeHash, Encode, Deserialize, arbitrary::Arbitrary, PartialEq, +)] +#[serde(untagged)] +#[tree_hash(enum_behaviour = "transparent")] +#[ssz(enum_behaviour = "transparent")] +#[serde(bound = "E: EthSpec", deny_unknown_fields)] +#[arbitrary(bound = "E: EthSpec")] +pub struct LightClientHeader { pub beacon: BeaconBlockHeader, + + #[superstruct( + only(Capella), + partial_getter(rename = "execution_payload_header_capella") + )] + pub execution: ExecutionPayloadHeaderCapella, + #[superstruct(only(Deneb), partial_getter(rename = "execution_payload_header_deneb"))] + pub execution: ExecutionPayloadHeaderDeneb, + + #[superstruct(only(Capella, Deneb))] + pub execution_branch: FixedVector, + + #[ssz(skip_serializing, skip_deserializing)] + #[tree_hash(skip_hashing)] + #[serde(skip)] + #[arbitrary(default)] + pub _phantom_data: PhantomData, } -impl From for LightClientHeader { - fn from(beacon: BeaconBlockHeader) -> Self { - LightClientHeader { beacon } +impl LightClientHeader { + pub fn block_to_light_client_header( + block: &SignedBeaconBlock, + chain_spec: &ChainSpec, + ) -> Result { + let header = match block + .fork_name(chain_spec) + .map_err(|_| Error::InconsistentFork)? + { + ForkName::Base => return Err(Error::AltairForkNotActive), + ForkName::Altair | ForkName::Merge => LightClientHeader::Altair( + LightClientHeaderAltair::block_to_light_client_header(block)?, + ), + ForkName::Capella => LightClientHeader::Capella( + LightClientHeaderCapella::block_to_light_client_header(block)?, + ), + ForkName::Deneb | ForkName::Electra => LightClientHeader::Deneb( + LightClientHeaderDeneb::block_to_light_client_header(block)?, + ), + }; + Ok(header) + } + + pub fn from_ssz_bytes(bytes: &[u8], fork_name: ForkName) -> Result { + let header = match fork_name { + ForkName::Altair | ForkName::Merge => { + LightClientHeader::Altair(LightClientHeaderAltair::from_ssz_bytes(bytes)?) + } + ForkName::Capella => { + LightClientHeader::Capella(LightClientHeaderCapella::from_ssz_bytes(bytes)?) + } + ForkName::Deneb | ForkName::Electra => { + LightClientHeader::Deneb(LightClientHeaderDeneb::from_ssz_bytes(bytes)?) + } + ForkName::Base => { + return Err(ssz::DecodeError::BytesInvalid(format!( + "LightClientHeader decoding for {fork_name} not implemented" + ))) + } + }; + + Ok(header) + } + + /// Custom SSZ decoder that takes a `ForkName` as context. + pub fn from_ssz_bytes_for_fork( + bytes: &[u8], + fork_name: ForkName, + ) -> Result { + Self::from_ssz_bytes(bytes, fork_name) + } + + pub fn ssz_max_var_len_for_fork(fork_name: ForkName) -> usize { + match fork_name { + ForkName::Base | ForkName::Altair | ForkName::Merge => 0, + ForkName::Capella | ForkName::Deneb | ForkName::Electra => { + ExecutionPayloadHeader::::ssz_max_var_len_for_fork(fork_name) + } + } + } +} + +impl LightClientHeaderAltair { + pub fn block_to_light_client_header(block: &SignedBeaconBlock) -> Result { + Ok(LightClientHeaderAltair { + beacon: block.message().block_header(), + _phantom_data: PhantomData, + }) + } +} + +impl LightClientHeaderCapella { + pub fn block_to_light_client_header(block: &SignedBeaconBlock) -> Result { + let payload = block + .message() + .execution_payload()? + .execution_payload_capella()?; + + let header = ExecutionPayloadHeaderCapella::from(payload); + let beacon_block_body = BeaconBlockBody::from( + block + .message() + .body_capella() + .map_err(|_| Error::BeaconBlockBodyError)? + .to_owned(), + ); + + let execution_branch = + beacon_block_body.block_body_merkle_proof(EXECUTION_PAYLOAD_INDEX)?; + + return Ok(LightClientHeaderCapella { + beacon: block.message().block_header(), + execution: header, + execution_branch: FixedVector::new(execution_branch)?, + _phantom_data: PhantomData, + }); + } +} + +impl LightClientHeaderDeneb { + pub fn block_to_light_client_header(block: &SignedBeaconBlock) -> Result { + let payload = block + .message() + .execution_payload()? + .execution_payload_deneb()?; + + let header = ExecutionPayloadHeaderDeneb::from(payload); + let beacon_block_body = BeaconBlockBody::from( + block + .message() + .body_deneb() + .map_err(|_| Error::BeaconBlockBodyError)? + .to_owned(), + ); + + let execution_branch = + beacon_block_body.block_body_merkle_proof(EXECUTION_PAYLOAD_INDEX)?; + + Ok(LightClientHeaderDeneb { + beacon: block.message().block_header(), + execution: header, + execution_branch: FixedVector::new(execution_branch)?, + _phantom_data: PhantomData, + }) + } +} + +impl ForkVersionDeserialize for LightClientHeader { + fn deserialize_by_fork<'de, D: serde::Deserializer<'de>>( + value: serde_json::value::Value, + fork_name: ForkName, + ) -> Result { + match fork_name { + ForkName::Altair | ForkName::Merge => serde_json::from_value(value) + .map(|light_client_header| Self::Altair(light_client_header)) + .map_err(serde::de::Error::custom), + ForkName::Capella => serde_json::from_value(value) + .map(|light_client_header| Self::Capella(light_client_header)) + .map_err(serde::de::Error::custom), + ForkName::Deneb | ForkName::Electra => serde_json::from_value(value) + .map(|light_client_header| Self::Deneb(light_client_header)) + .map_err(serde::de::Error::custom), + ForkName::Base => Err(serde::de::Error::custom(format!( + "LightClientHeader deserialization for {fork_name} not implemented" + ))), + } } } diff --git a/consensus/types/src/light_client_optimistic_update.rs b/consensus/types/src/light_client_optimistic_update.rs index d883d735f3..d22c4535f1 100644 --- a/consensus/types/src/light_client_optimistic_update.rs +++ b/consensus/types/src/light_client_optimistic_update.rs @@ -1,83 +1,186 @@ -use super::{EthSpec, ForkName, ForkVersionDeserialize, Slot, SyncAggregate}; -use crate::light_client_header::LightClientHeader; +use super::{EthSpec, ForkName, ForkVersionDeserialize, LightClientHeader, Slot, SyncAggregate}; +use crate::test_utils::TestRandom; use crate::{ - light_client_update::Error, test_utils::TestRandom, BeaconState, ChainSpec, SignedBeaconBlock, + light_client_update::*, ChainSpec, LightClientHeaderAltair, LightClientHeaderCapella, + LightClientHeaderDeneb, SignedBeaconBlock, }; +use derivative::Derivative; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; -use ssz_derive::{Decode, Encode}; +use ssz::{Decode, Encode}; +use ssz_derive::Decode; +use ssz_derive::Encode; +use superstruct::superstruct; use test_random_derive::TestRandom; -use tree_hash::TreeHash; +use tree_hash::Hash256; +use tree_hash_derive::TreeHash; /// A LightClientOptimisticUpdate is the update we send on each slot, /// it is based off the current unfinalized epoch is verified only against BLS signature. -#[derive( - Debug, - Clone, - PartialEq, - Serialize, - Deserialize, - Encode, - Decode, - TestRandom, - arbitrary::Arbitrary, +#[superstruct( + variants(Altair, Capella, Deneb), + variant_attributes( + derive( + Debug, + Clone, + PartialEq, + Serialize, + Deserialize, + Derivative, + Decode, + Encode, + TestRandom, + arbitrary::Arbitrary, + TreeHash, + ), + serde(bound = "E: EthSpec", deny_unknown_fields), + arbitrary(bound = "E: EthSpec"), + ) )] -#[serde(bound = "T: EthSpec")] -#[arbitrary(bound = "T: EthSpec")] -pub struct LightClientOptimisticUpdate { +#[derive( + Debug, Clone, Serialize, Encode, TreeHash, Deserialize, arbitrary::Arbitrary, PartialEq, +)] +#[serde(untagged)] +#[tree_hash(enum_behaviour = "transparent")] +#[ssz(enum_behaviour = "transparent")] +#[serde(bound = "E: EthSpec", deny_unknown_fields)] +#[arbitrary(bound = "E: EthSpec")] +pub struct LightClientOptimisticUpdate { /// The last `BeaconBlockHeader` from the last attested block by the sync committee. - pub attested_header: LightClientHeader, - /// current sync aggreggate - pub sync_aggregate: SyncAggregate, - /// Slot of the sync aggregated singature + #[superstruct(only(Altair), partial_getter(rename = "attested_header_altair"))] + pub attested_header: LightClientHeaderAltair, + #[superstruct(only(Capella), partial_getter(rename = "attested_header_capella"))] + pub attested_header: LightClientHeaderCapella, + #[superstruct(only(Deneb), partial_getter(rename = "attested_header_deneb"))] + pub attested_header: LightClientHeaderDeneb, + /// current sync aggregate + pub sync_aggregate: SyncAggregate, + /// Slot of the sync aggregated signature pub signature_slot: Slot, } -impl LightClientOptimisticUpdate { +impl LightClientOptimisticUpdate { pub fn new( + attested_block: &SignedBeaconBlock, + sync_aggregate: SyncAggregate, + signature_slot: Slot, chain_spec: &ChainSpec, - block: &SignedBeaconBlock, - attested_state: &BeaconState, ) -> Result { - let altair_fork_epoch = chain_spec - .altair_fork_epoch - .ok_or(Error::AltairForkNotActive)?; - if attested_state.slot().epoch(T::slots_per_epoch()) < altair_fork_epoch { - return Err(Error::AltairForkNotActive); - } + let optimistic_update = match attested_block + .fork_name(chain_spec) + .map_err(|_| Error::InconsistentFork)? + { + ForkName::Altair | ForkName::Merge => Self::Altair(LightClientOptimisticUpdateAltair { + attested_header: LightClientHeaderAltair::block_to_light_client_header( + attested_block, + )?, + sync_aggregate, + signature_slot, + }), + ForkName::Capella => Self::Capella(LightClientOptimisticUpdateCapella { + attested_header: LightClientHeaderCapella::block_to_light_client_header( + attested_block, + )?, + sync_aggregate, + signature_slot, + }), + ForkName::Deneb | ForkName::Electra => Self::Deneb(LightClientOptimisticUpdateDeneb { + attested_header: LightClientHeaderDeneb::block_to_light_client_header( + attested_block, + )?, + sync_aggregate, + signature_slot, + }), + ForkName::Base => return Err(Error::AltairForkNotActive), + }; - let sync_aggregate = block.message().body().sync_aggregate()?; - if sync_aggregate.num_set_bits() < chain_spec.min_sync_committee_participants as usize { - return Err(Error::NotEnoughSyncCommitteeParticipants); - } + Ok(optimistic_update) + } - // Compute and validate attested header. - let mut attested_header = attested_state.latest_block_header().clone(); - attested_header.state_root = attested_state.tree_hash_root(); - Ok(Self { - attested_header: attested_header.into(), - sync_aggregate: sync_aggregate.clone(), - signature_slot: block.slot(), + pub fn map_with_fork_name(&self, func: F) -> R + where + F: Fn(ForkName) -> R, + { + match self { + Self::Altair(_) => func(ForkName::Altair), + Self::Capella(_) => func(ForkName::Capella), + Self::Deneb(_) => func(ForkName::Deneb), + } + } + + pub fn get_slot<'a>(&'a self) -> Slot { + map_light_client_optimistic_update_ref!(&'a _, self.to_ref(), |inner, cons| { + cons(inner); + inner.attested_header.beacon.slot }) } + + pub fn get_canonical_root<'a>(&'a self) -> Hash256 { + map_light_client_optimistic_update_ref!(&'a _, self.to_ref(), |inner, cons| { + cons(inner); + inner.attested_header.beacon.canonical_root() + }) + } + + pub fn get_parent_root<'a>(&'a self) -> Hash256 { + map_light_client_optimistic_update_ref!(&'a _, self.to_ref(), |inner, cons| { + cons(inner); + inner.attested_header.beacon.parent_root + }) + } + + pub fn from_ssz_bytes(bytes: &[u8], fork_name: ForkName) -> Result { + let optimistic_update = match fork_name { + ForkName::Altair | ForkName::Merge => { + Self::Altair(LightClientOptimisticUpdateAltair::from_ssz_bytes(bytes)?) + } + ForkName::Capella => { + Self::Capella(LightClientOptimisticUpdateCapella::from_ssz_bytes(bytes)?) + } + ForkName::Deneb | ForkName::Electra => { + Self::Deneb(LightClientOptimisticUpdateDeneb::from_ssz_bytes(bytes)?) + } + ForkName::Base => { + return Err(ssz::DecodeError::BytesInvalid(format!( + "LightClientOptimisticUpdate decoding for {fork_name} not implemented" + ))) + } + }; + + Ok(optimistic_update) + } + + #[allow(clippy::arithmetic_side_effects)] + pub fn ssz_max_len_for_fork(fork_name: ForkName) -> usize { + // TODO(electra): review electra changes + match fork_name { + ForkName::Base => 0, + ForkName::Altair + | ForkName::Merge + | ForkName::Capella + | ForkName::Deneb + | ForkName::Electra => { + as Encode>::ssz_fixed_len() + + LightClientHeader::::ssz_max_var_len_for_fork(fork_name) + } + } + } } -impl ForkVersionDeserialize for LightClientOptimisticUpdate { +impl ForkVersionDeserialize for LightClientOptimisticUpdate { fn deserialize_by_fork<'de, D: Deserializer<'de>>( value: Value, fork_name: ForkName, ) -> Result { match fork_name { - ForkName::Altair | ForkName::Merge => Ok(serde_json::from_value::< - LightClientOptimisticUpdate, - >(value) - .map_err(serde::de::Error::custom))?, - ForkName::Base | ForkName::Capella | ForkName::Deneb => { - Err(serde::de::Error::custom(format!( - "LightClientOptimisticUpdate failed to deserialize: unsupported fork '{}'", - fork_name - ))) - } + ForkName::Base => Err(serde::de::Error::custom(format!( + "LightClientOptimisticUpdate failed to deserialize: unsupported fork '{}'", + fork_name + ))), + _ => Ok( + serde_json::from_value::>(value) + .map_err(serde::de::Error::custom), + )?, } } } @@ -87,5 +190,5 @@ mod tests { use super::*; use crate::MainnetEthSpec; - ssz_tests!(LightClientOptimisticUpdate); + ssz_tests!(LightClientOptimisticUpdateDeneb); } diff --git a/consensus/types/src/light_client_update.rs b/consensus/types/src/light_client_update.rs index 718cd7553f..af9cbc1661 100644 --- a/consensus/types/src/light_client_update.rs +++ b/consensus/types/src/light_client_update.rs @@ -1,28 +1,38 @@ -use super::{BeaconBlockHeader, EthSpec, FixedVector, Hash256, Slot, SyncAggregate, SyncCommittee}; +use super::{EthSpec, FixedVector, Hash256, Slot, SyncAggregate, SyncCommittee}; use crate::{ - beacon_state, test_utils::TestRandom, BeaconBlock, BeaconState, ChainSpec, ForkName, - ForkVersionDeserialize, LightClientHeader, + beacon_state, test_utils::TestRandom, BeaconBlock, BeaconBlockHeader, BeaconState, ChainSpec, + ForkName, ForkVersionDeserialize, LightClientHeaderAltair, LightClientHeaderCapella, + LightClientHeaderDeneb, SignedBeaconBlock, }; +use derivative::Derivative; use safe_arith::ArithError; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; -use ssz_derive::{Decode, Encode}; -use ssz_types::typenum::{U5, U6}; +use ssz::Decode; +use ssz_derive::Decode; +use ssz_derive::Encode; +use ssz_types::typenum::{U4, U5, U6}; use std::sync::Arc; +use superstruct::superstruct; use test_random_derive::TestRandom; use tree_hash::TreeHash; +use tree_hash_derive::TreeHash; pub const FINALIZED_ROOT_INDEX: usize = 105; pub const CURRENT_SYNC_COMMITTEE_INDEX: usize = 54; pub const NEXT_SYNC_COMMITTEE_INDEX: usize = 55; +pub const EXECUTION_PAYLOAD_INDEX: usize = 25; pub type FinalizedRootProofLen = U6; pub type CurrentSyncCommitteeProofLen = U5; +pub type ExecutionPayloadProofLen = U4; + pub type NextSyncCommitteeProofLen = U5; pub const FINALIZED_ROOT_PROOF_LEN: usize = 6; pub const CURRENT_SYNC_COMMITTEE_PROOF_LEN: usize = 5; pub const NEXT_SYNC_COMMITTEE_PROOF_LEN: usize = 5; +pub const EXECUTION_PAYLOAD_PROOF_LEN: usize = 4; #[derive(Debug, PartialEq, Clone)] pub enum Error { @@ -33,6 +43,8 @@ pub enum Error { NotEnoughSyncCommitteeParticipants, MismatchingPeriods, InvalidFinalizedBlock, + BeaconBlockBodyError, + InconsistentFork, } impl From for Error { @@ -53,77 +65,112 @@ impl From for Error { } } -/// A LightClientUpdate is the update we request solely to either complete the bootstraping process, +/// A LightClientUpdate is the update we request solely to either complete the bootstrapping process, /// or to sync up to the last committee period, we need to have one ready for each ALTAIR period /// we go over, note: there is no need to keep all of the updates from [ALTAIR_PERIOD, CURRENT_PERIOD]. -#[derive( - Debug, - Clone, - PartialEq, - Serialize, - Deserialize, - Encode, - Decode, - TestRandom, - arbitrary::Arbitrary, +#[superstruct( + variants(Altair, Capella, Deneb), + variant_attributes( + derive( + Debug, + Clone, + PartialEq, + Serialize, + Deserialize, + Derivative, + Decode, + Encode, + TestRandom, + arbitrary::Arbitrary, + TreeHash, + ), + serde(bound = "E: EthSpec", deny_unknown_fields), + arbitrary(bound = "E: EthSpec"), + ) )] -#[serde(bound = "T: EthSpec")] -#[arbitrary(bound = "T: EthSpec")] -pub struct LightClientUpdate { +#[derive( + Debug, Clone, Serialize, Encode, TreeHash, Deserialize, arbitrary::Arbitrary, PartialEq, +)] +#[serde(untagged)] +#[tree_hash(enum_behaviour = "transparent")] +#[ssz(enum_behaviour = "transparent")] +#[serde(bound = "E: EthSpec", deny_unknown_fields)] +#[arbitrary(bound = "E: EthSpec")] +pub struct LightClientUpdate { /// The last `BeaconBlockHeader` from the last attested block by the sync committee. - pub attested_header: LightClientHeader, + #[superstruct(only(Altair), partial_getter(rename = "attested_header_altair"))] + pub attested_header: LightClientHeaderAltair, + #[superstruct(only(Capella), partial_getter(rename = "attested_header_capella"))] + pub attested_header: LightClientHeaderCapella, + #[superstruct(only(Deneb), partial_getter(rename = "attested_header_deneb"))] + pub attested_header: LightClientHeaderDeneb, /// The `SyncCommittee` used in the next period. - pub next_sync_committee: Arc>, + pub next_sync_committee: Arc>, /// Merkle proof for next sync committee pub next_sync_committee_branch: FixedVector, /// The last `BeaconBlockHeader` from the last attested finalized block (end of epoch). - pub finalized_header: LightClientHeader, + #[superstruct(only(Altair), partial_getter(rename = "finalized_header_altair"))] + pub finalized_header: LightClientHeaderAltair, + #[superstruct(only(Capella), partial_getter(rename = "finalized_header_capella"))] + pub finalized_header: LightClientHeaderCapella, + #[superstruct(only(Deneb), partial_getter(rename = "finalized_header_deneb"))] + pub finalized_header: LightClientHeaderDeneb, /// Merkle proof attesting finalized header. pub finality_branch: FixedVector, /// current sync aggreggate - pub sync_aggregate: SyncAggregate, - /// Slot of the sync aggregated singature + pub sync_aggregate: SyncAggregate, + /// Slot of the sync aggregated signature pub signature_slot: Slot, } -impl LightClientUpdate { - pub fn new( - chain_spec: ChainSpec, - beacon_state: BeaconState, - block: BeaconBlock, - attested_state: &mut BeaconState, - finalized_block: BeaconBlock, - ) -> Result { - let altair_fork_epoch = chain_spec - .altair_fork_epoch - .ok_or(Error::AltairForkNotActive)?; - if attested_state.slot().epoch(T::slots_per_epoch()) < altair_fork_epoch { - return Err(Error::AltairForkNotActive); +impl ForkVersionDeserialize for LightClientUpdate { + fn deserialize_by_fork<'de, D: Deserializer<'de>>( + value: Value, + fork_name: ForkName, + ) -> Result { + match fork_name { + ForkName::Base => Err(serde::de::Error::custom(format!( + "LightClientUpdate failed to deserialize: unsupported fork '{}'", + fork_name + ))), + _ => Ok(serde_json::from_value::>(value) + .map_err(serde::de::Error::custom))?, } + } +} +impl LightClientUpdate { + pub fn new( + beacon_state: BeaconState, + block: BeaconBlock, + attested_state: &mut BeaconState, + attested_block: &SignedBeaconBlock, + finalized_block: &SignedBeaconBlock, + chain_spec: &ChainSpec, + ) -> Result { let sync_aggregate = block.body().sync_aggregate()?; if sync_aggregate.num_set_bits() < chain_spec.min_sync_committee_participants as usize { return Err(Error::NotEnoughSyncCommitteeParticipants); } - let signature_period = block.epoch().sync_committee_period(&chain_spec)?; + let signature_period = block.epoch().sync_committee_period(chain_spec)?; // Compute and validate attested header. let mut attested_header = attested_state.latest_block_header().clone(); attested_header.state_root = attested_state.tree_hash_root(); let attested_period = attested_header .slot - .epoch(T::slots_per_epoch()) - .sync_committee_period(&chain_spec)?; + .epoch(E::slots_per_epoch()) + .sync_committee_period(chain_spec)?; if attested_period != signature_period { return Err(Error::MismatchingPeriods); } // Build finalized header from finalized block let finalized_header = BeaconBlockHeader { slot: finalized_block.slot(), - proposer_index: finalized_block.proposer_index(), + proposer_index: finalized_block.message().proposer_index(), parent_root: finalized_block.parent_root(), state_root: finalized_block.state_root(), - body_root: finalized_block.body_root(), + body_root: finalized_block.message().body_root(), }; if finalized_header.tree_hash_root() != beacon_state.finalized_checkpoint().root { return Err(Error::InvalidFinalizedBlock); @@ -131,35 +178,79 @@ impl LightClientUpdate { let next_sync_committee_branch = attested_state.compute_merkle_proof(NEXT_SYNC_COMMITTEE_INDEX)?; let finality_branch = attested_state.compute_merkle_proof(FINALIZED_ROOT_INDEX)?; - Ok(Self { - attested_header: attested_header.into(), - next_sync_committee: attested_state.next_sync_committee()?.clone(), - next_sync_committee_branch: FixedVector::new(next_sync_committee_branch)?, - finalized_header: finalized_header.into(), - finality_branch: FixedVector::new(finality_branch)?, - sync_aggregate: sync_aggregate.clone(), - signature_slot: block.slot(), - }) - } -} -impl ForkVersionDeserialize for LightClientUpdate { - fn deserialize_by_fork<'de, D: Deserializer<'de>>( - value: Value, - fork_name: ForkName, - ) -> Result { - match fork_name { + let light_client_update = match attested_block + .fork_name(chain_spec) + .map_err(|_| Error::InconsistentFork)? + { + ForkName::Base => return Err(Error::AltairForkNotActive), ForkName::Altair | ForkName::Merge => { - Ok(serde_json::from_value::>(value) - .map_err(serde::de::Error::custom))? + let attested_header = + LightClientHeaderAltair::block_to_light_client_header(attested_block)?; + let finalized_header = + LightClientHeaderAltair::block_to_light_client_header(finalized_block)?; + Self::Altair(LightClientUpdateAltair { + attested_header, + next_sync_committee: attested_state.next_sync_committee()?.clone(), + next_sync_committee_branch: FixedVector::new(next_sync_committee_branch)?, + finalized_header, + finality_branch: FixedVector::new(finality_branch)?, + sync_aggregate: sync_aggregate.clone(), + signature_slot: block.slot(), + }) } - ForkName::Base | ForkName::Capella | ForkName::Deneb => { - Err(serde::de::Error::custom(format!( - "LightClientUpdate failed to deserialize: unsupported fork '{}'", - fork_name + ForkName::Capella => { + let attested_header = + LightClientHeaderCapella::block_to_light_client_header(attested_block)?; + let finalized_header = + LightClientHeaderCapella::block_to_light_client_header(finalized_block)?; + Self::Capella(LightClientUpdateCapella { + attested_header, + next_sync_committee: attested_state.next_sync_committee()?.clone(), + next_sync_committee_branch: FixedVector::new(next_sync_committee_branch)?, + finalized_header, + finality_branch: FixedVector::new(finality_branch)?, + sync_aggregate: sync_aggregate.clone(), + signature_slot: block.slot(), + }) + } + ForkName::Deneb | ForkName::Electra => { + let attested_header = + LightClientHeaderDeneb::block_to_light_client_header(attested_block)?; + let finalized_header = + LightClientHeaderDeneb::block_to_light_client_header(finalized_block)?; + Self::Deneb(LightClientUpdateDeneb { + attested_header, + next_sync_committee: attested_state.next_sync_committee()?.clone(), + next_sync_committee_branch: FixedVector::new(next_sync_committee_branch)?, + finalized_header, + finality_branch: FixedVector::new(finality_branch)?, + sync_aggregate: sync_aggregate.clone(), + signature_slot: block.slot(), + }) + } + }; + + Ok(light_client_update) + } + + pub fn from_ssz_bytes(bytes: &[u8], fork_name: ForkName) -> Result { + let update = match fork_name { + ForkName::Altair | ForkName::Merge => { + Self::Altair(LightClientUpdateAltair::from_ssz_bytes(bytes)?) + } + ForkName::Capella => Self::Capella(LightClientUpdateCapella::from_ssz_bytes(bytes)?), + ForkName::Deneb | ForkName::Electra => { + Self::Deneb(LightClientUpdateDeneb::from_ssz_bytes(bytes)?) + } + ForkName::Base => { + return Err(ssz::DecodeError::BytesInvalid(format!( + "LightClientUpdate decoding for {fork_name} not implemented" ))) } - } + }; + + Ok(update) } } @@ -169,7 +260,7 @@ mod tests { use crate::MainnetEthSpec; use ssz_types::typenum::Unsigned; - ssz_tests!(LightClientUpdate); + ssz_tests!(LightClientUpdateDeneb); #[test] fn finalized_root_params() { diff --git a/consensus/types/src/participation_list.rs b/consensus/types/src/participation_list.rs index be119fbef2..6e3d916dee 100644 --- a/consensus/types/src/participation_list.rs +++ b/consensus/types/src/participation_list.rs @@ -43,7 +43,7 @@ pub fn leaf_count(len: usize) -> usize { pub fn leaf_iter( values: &[ParticipationFlags], -) -> impl Iterator + ExactSizeIterator + '_ { +) -> impl ExactSizeIterator + '_ { values.chunks(BYTES_PER_CHUNK).map(|xs| { // Zero-pad chunks on the right. let mut chunk = [0u8; BYTES_PER_CHUNK]; diff --git a/consensus/types/src/payload.rs b/consensus/types/src/payload.rs index 2f7975161c..18b3199bd3 100644 --- a/consensus/types/src/payload.rs +++ b/consensus/types/src/payload.rs @@ -5,7 +5,6 @@ use serde::{Deserialize, Serialize}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use std::borrow::Cow; -use std::convert::TryFrom; use std::fmt::Debug; use std::hash::Hash; use test_random_derive::TestRandom; @@ -20,11 +19,11 @@ pub enum BlockType { /// A trait representing behavior of an `ExecutionPayload` that either has a full list of transactions /// or a transaction hash in it's place. -pub trait ExecPayload: Debug + Clone + PartialEq + Hash + TreeHash + Send { +pub trait ExecPayload: Debug + Clone + PartialEq + Hash + TreeHash + Send { fn block_type() -> BlockType; /// Convert the payload into a payload header. - fn to_execution_payload_header(&self) -> ExecutionPayloadHeader; + fn to_execution_payload_header(&self) -> ExecutionPayloadHeader; /// We provide a subset of field accessors, for the fields used in `consensus`. /// @@ -36,7 +35,7 @@ pub trait ExecPayload: Debug + Clone + PartialEq + Hash + TreeHash + fn block_hash(&self) -> ExecutionBlockHash; fn fee_recipient(&self) -> Address; fn gas_limit(&self) -> u64; - fn transactions(&self) -> Option<&Transactions>; + fn transactions(&self) -> Option<&Transactions>; /// fork-specific fields fn withdrawals_root(&self) -> Result; fn blob_gas_used(&self) -> Result; @@ -49,8 +48,8 @@ pub trait ExecPayload: Debug + Clone + PartialEq + Hash + TreeHash + } /// `ExecPayload` functionality the requires ownership. -pub trait OwnedExecPayload: - ExecPayload +pub trait OwnedExecPayload: + ExecPayload + Default + Serialize + DeserializeOwned @@ -62,8 +61,8 @@ pub trait OwnedExecPayload: { } -impl OwnedExecPayload for P where - P: ExecPayload +impl OwnedExecPayload for P where + P: ExecPayload + Default + Serialize + DeserializeOwned @@ -75,37 +74,43 @@ impl OwnedExecPayload for P where { } -pub trait AbstractExecPayload: - ExecPayload +pub trait AbstractExecPayload: + ExecPayload + Sized - + From> - + TryFrom> + + From> + + TryFrom> + TryInto + TryInto + TryInto + + TryInto { - type Ref<'a>: ExecPayload + type Ref<'a>: ExecPayload + Copy + From<&'a Self::Merge> + From<&'a Self::Capella> - + From<&'a Self::Deneb>; + + From<&'a Self::Deneb> + + From<&'a Self::Electra>; - type Merge: OwnedExecPayload + type Merge: OwnedExecPayload + Into - + for<'a> From>> - + TryFrom>; - type Capella: OwnedExecPayload + + for<'a> From>> + + TryFrom>; + type Capella: OwnedExecPayload + Into - + for<'a> From>> - + TryFrom>; - type Deneb: OwnedExecPayload + + for<'a> From>> + + TryFrom>; + type Deneb: OwnedExecPayload + Into - + for<'a> From>> - + TryFrom>; + + for<'a> From>> + + TryFrom>; + type Electra: OwnedExecPayload + + Into + + for<'a> From>> + + TryFrom>; } #[superstruct( - variants(Merge, Capella, Deneb), + variants(Merge, Capella, Deneb, Electra), variant_attributes( derive( Debug, @@ -119,14 +124,14 @@ pub trait AbstractExecPayload: Derivative, arbitrary::Arbitrary, ), - derivative(PartialEq, Hash(bound = "T: EthSpec")), - serde(bound = "T: EthSpec", deny_unknown_fields), - arbitrary(bound = "T: EthSpec"), + derivative(PartialEq, Hash(bound = "E: EthSpec")), + serde(bound = "E: EthSpec", deny_unknown_fields), + arbitrary(bound = "E: EthSpec"), ssz(struct_behaviour = "transparent"), ), ref_attributes( derive(Debug, Derivative, TreeHash), - derivative(PartialEq, Hash(bound = "T: EthSpec")), + derivative(PartialEq, Hash(bound = "E: EthSpec")), tree_hash(enum_behaviour = "transparent"), ), map_into(ExecutionPayload), @@ -135,29 +140,31 @@ pub trait AbstractExecPayload: partial_getter_error(ty = "Error", expr = "BeaconStateError::IncorrectStateVariant") )] #[derive(Debug, Clone, Serialize, Deserialize, TreeHash, Derivative, arbitrary::Arbitrary)] -#[derivative(PartialEq, Hash(bound = "T: EthSpec"))] -#[serde(bound = "T: EthSpec")] -#[arbitrary(bound = "T: EthSpec")] +#[derivative(PartialEq, Hash(bound = "E: EthSpec"))] +#[serde(bound = "E: EthSpec")] +#[arbitrary(bound = "E: EthSpec")] #[tree_hash(enum_behaviour = "transparent")] -pub struct FullPayload { +pub struct FullPayload { #[superstruct(only(Merge), partial_getter(rename = "execution_payload_merge"))] - pub execution_payload: ExecutionPayloadMerge, + pub execution_payload: ExecutionPayloadMerge, #[superstruct(only(Capella), partial_getter(rename = "execution_payload_capella"))] - pub execution_payload: ExecutionPayloadCapella, + pub execution_payload: ExecutionPayloadCapella, #[superstruct(only(Deneb), partial_getter(rename = "execution_payload_deneb"))] - pub execution_payload: ExecutionPayloadDeneb, + pub execution_payload: ExecutionPayloadDeneb, + #[superstruct(only(Electra), partial_getter(rename = "execution_payload_electra"))] + pub execution_payload: ExecutionPayloadElectra, } -impl From> for ExecutionPayload { - fn from(full_payload: FullPayload) -> Self { +impl From> for ExecutionPayload { + fn from(full_payload: FullPayload) -> Self { map_full_payload_into_execution_payload!(full_payload, move |payload, cons| { cons(payload.execution_payload) }) } } -impl<'a, T: EthSpec> From> for ExecutionPayload { - fn from(full_payload_ref: FullPayloadRef<'a, T>) -> Self { +impl<'a, E: EthSpec> From> for ExecutionPayload { + fn from(full_payload_ref: FullPayloadRef<'a, E>) -> Self { map_full_payload_ref!(&'a _, full_payload_ref, move |payload, cons| { cons(payload); payload.execution_payload.clone().into() @@ -165,8 +172,8 @@ impl<'a, T: EthSpec> From> for ExecutionPayload { } } -impl<'a, T: EthSpec> From> for FullPayload { - fn from(full_payload_ref: FullPayloadRef<'a, T>) -> Self { +impl<'a, E: EthSpec> From> for FullPayload { + fn from(full_payload_ref: FullPayloadRef<'a, E>) -> Self { map_full_payload_ref!(&'a _, full_payload_ref, move |payload, cons| { cons(payload); payload.clone().into() @@ -174,15 +181,15 @@ impl<'a, T: EthSpec> From> for FullPayload { } } -impl ExecPayload for FullPayload { +impl ExecPayload for FullPayload { fn block_type() -> BlockType { BlockType::Full } - fn to_execution_payload_header<'a>(&'a self) -> ExecutionPayloadHeader { + fn to_execution_payload_header<'a>(&'a self) -> ExecutionPayloadHeader { map_full_payload_ref!(&'a _, self.to_ref(), move |inner, cons| { cons(inner); - let exec_payload_ref: ExecutionPayloadRef<'a, T> = From::from(&inner.execution_payload); + let exec_payload_ref: ExecutionPayloadRef<'a, E> = From::from(&inner.execution_payload); ExecutionPayloadHeader::from(exec_payload_ref) }) } @@ -236,7 +243,7 @@ impl ExecPayload for FullPayload { }) } - fn transactions<'a>(&'a self) -> Option<&'a Transactions> { + fn transactions<'a>(&'a self) -> Option<&'a Transactions> { map_full_payload_ref!(&'a _, self.to_ref(), move |payload, cons| { cons(payload); Some(&payload.execution_payload.transactions) @@ -252,6 +259,9 @@ impl ExecPayload for FullPayload { FullPayload::Deneb(ref inner) => { Ok(inner.execution_payload.withdrawals.tree_hash_root()) } + FullPayload::Electra(ref inner) => { + Ok(inner.execution_payload.withdrawals.tree_hash_root()) + } } } @@ -259,6 +269,7 @@ impl ExecPayload for FullPayload { match self { FullPayload::Merge(_) | FullPayload::Capella(_) => Err(Error::IncorrectStateVariant), FullPayload::Deneb(ref inner) => Ok(inner.execution_payload.blob_gas_used), + FullPayload::Electra(ref inner) => Ok(inner.execution_payload.blob_gas_used), } } @@ -275,8 +286,8 @@ impl ExecPayload for FullPayload { } } -impl FullPayload { - pub fn execution_payload(self) -> ExecutionPayload { +impl FullPayload { + pub fn execution_payload(self) -> ExecutionPayload { map_full_payload_into_execution_payload!(self, |inner, cons| { cons(inner.execution_payload) }) @@ -288,24 +299,25 @@ impl FullPayload { ForkName::Merge => Ok(FullPayloadMerge::default().into()), ForkName::Capella => Ok(FullPayloadCapella::default().into()), ForkName::Deneb => Ok(FullPayloadDeneb::default().into()), + ForkName::Electra => Ok(FullPayloadElectra::default().into()), } } } -impl<'a, T: EthSpec> FullPayloadRef<'a, T> { - pub fn execution_payload_ref(self) -> ExecutionPayloadRef<'a, T> { +impl<'a, E: EthSpec> FullPayloadRef<'a, E> { + pub fn execution_payload_ref(self) -> ExecutionPayloadRef<'a, E> { map_full_payload_ref_into_execution_payload_ref!(&'a _, self, |inner, cons| { cons(&inner.execution_payload) }) } } -impl<'b, T: EthSpec> ExecPayload for FullPayloadRef<'b, T> { +impl<'b, E: EthSpec> ExecPayload for FullPayloadRef<'b, E> { fn block_type() -> BlockType { BlockType::Full } - fn to_execution_payload_header<'a>(&'a self) -> ExecutionPayloadHeader { + fn to_execution_payload_header<'a>(&'a self) -> ExecutionPayloadHeader { map_full_payload_ref!(&'a _, self, move |payload, cons| { cons(payload); payload.to_execution_payload_header() @@ -361,7 +373,7 @@ impl<'b, T: EthSpec> ExecPayload for FullPayloadRef<'b, T> { }) } - fn transactions<'a>(&'a self) -> Option<&'a Transactions> { + fn transactions<'a>(&'a self) -> Option<&'a Transactions> { map_full_payload_ref!(&'a _, self, move |payload, cons| { cons(payload); Some(&payload.execution_payload.transactions) @@ -377,6 +389,9 @@ impl<'b, T: EthSpec> ExecPayload for FullPayloadRef<'b, T> { FullPayloadRef::Deneb(inner) => { Ok(inner.execution_payload.withdrawals.tree_hash_root()) } + FullPayloadRef::Electra(inner) => { + Ok(inner.execution_payload.withdrawals.tree_hash_root()) + } } } @@ -386,6 +401,7 @@ impl<'b, T: EthSpec> ExecPayload for FullPayloadRef<'b, T> { Err(Error::IncorrectStateVariant) } FullPayloadRef::Deneb(inner) => Ok(inner.execution_payload.blob_gas_used), + FullPayloadRef::Electra(inner) => Ok(inner.execution_payload.blob_gas_used), } } @@ -402,30 +418,31 @@ impl<'b, T: EthSpec> ExecPayload for FullPayloadRef<'b, T> { } } -impl AbstractExecPayload for FullPayload { - type Ref<'a> = FullPayloadRef<'a, T>; - type Merge = FullPayloadMerge; - type Capella = FullPayloadCapella; - type Deneb = FullPayloadDeneb; +impl AbstractExecPayload for FullPayload { + type Ref<'a> = FullPayloadRef<'a, E>; + type Merge = FullPayloadMerge; + type Capella = FullPayloadCapella; + type Deneb = FullPayloadDeneb; + type Electra = FullPayloadElectra; } -impl From> for FullPayload { - fn from(execution_payload: ExecutionPayload) -> Self { +impl From> for FullPayload { + fn from(execution_payload: ExecutionPayload) -> Self { map_execution_payload_into_full_payload!(execution_payload, |inner, cons| { cons(inner.into()) }) } } -impl TryFrom> for FullPayload { +impl TryFrom> for FullPayload { type Error = (); - fn try_from(_: ExecutionPayloadHeader) -> Result { + fn try_from(_: ExecutionPayloadHeader) -> Result { Err(()) } } #[superstruct( - variants(Merge, Capella, Deneb), + variants(Merge, Capella, Deneb, Electra), variant_attributes( derive( Debug, @@ -439,14 +456,14 @@ impl TryFrom> for FullPayload { Derivative, arbitrary::Arbitrary ), - derivative(PartialEq, Hash(bound = "T: EthSpec")), - serde(bound = "T: EthSpec", deny_unknown_fields), - arbitrary(bound = "T: EthSpec"), + derivative(PartialEq, Hash(bound = "E: EthSpec")), + serde(bound = "E: EthSpec", deny_unknown_fields), + arbitrary(bound = "E: EthSpec"), ssz(struct_behaviour = "transparent"), ), ref_attributes( derive(Debug, Derivative, TreeHash), - derivative(PartialEq, Hash(bound = "T: EthSpec")), + derivative(PartialEq, Hash(bound = "E: EthSpec")), tree_hash(enum_behaviour = "transparent"), ), map_into(ExecutionPayloadHeader), @@ -454,21 +471,23 @@ impl TryFrom> for FullPayload { partial_getter_error(ty = "Error", expr = "BeaconStateError::IncorrectStateVariant") )] #[derive(Debug, Clone, Serialize, Deserialize, TreeHash, Derivative, arbitrary::Arbitrary)] -#[derivative(PartialEq, Hash(bound = "T: EthSpec"))] -#[serde(bound = "T: EthSpec")] -#[arbitrary(bound = "T: EthSpec")] +#[derivative(PartialEq, Hash(bound = "E: EthSpec"))] +#[serde(bound = "E: EthSpec")] +#[arbitrary(bound = "E: EthSpec")] #[tree_hash(enum_behaviour = "transparent")] -pub struct BlindedPayload { +pub struct BlindedPayload { #[superstruct(only(Merge), partial_getter(rename = "execution_payload_merge"))] - pub execution_payload_header: ExecutionPayloadHeaderMerge, + pub execution_payload_header: ExecutionPayloadHeaderMerge, #[superstruct(only(Capella), partial_getter(rename = "execution_payload_capella"))] - pub execution_payload_header: ExecutionPayloadHeaderCapella, + pub execution_payload_header: ExecutionPayloadHeaderCapella, #[superstruct(only(Deneb), partial_getter(rename = "execution_payload_deneb"))] - pub execution_payload_header: ExecutionPayloadHeaderDeneb, + pub execution_payload_header: ExecutionPayloadHeaderDeneb, + #[superstruct(only(Electra), partial_getter(rename = "execution_payload_electra"))] + pub execution_payload_header: ExecutionPayloadHeaderElectra, } -impl<'a, T: EthSpec> From> for BlindedPayload { - fn from(blinded_payload_ref: BlindedPayloadRef<'a, T>) -> Self { +impl<'a, E: EthSpec> From> for BlindedPayload { + fn from(blinded_payload_ref: BlindedPayloadRef<'a, E>) -> Self { map_blinded_payload_ref!(&'a _, blinded_payload_ref, move |payload, cons| { cons(payload); payload.clone().into() @@ -476,12 +495,12 @@ impl<'a, T: EthSpec> From> for BlindedPayload { } } -impl ExecPayload for BlindedPayload { +impl ExecPayload for BlindedPayload { fn block_type() -> BlockType { BlockType::Blinded } - fn to_execution_payload_header(&self) -> ExecutionPayloadHeader { + fn to_execution_payload_header(&self) -> ExecutionPayloadHeader { map_blinded_payload_into_execution_payload_header!(self.clone(), |inner, cons| { cons(inner.execution_payload_header) }) @@ -536,7 +555,7 @@ impl ExecPayload for BlindedPayload { }) } - fn transactions(&self) -> Option<&Transactions> { + fn transactions(&self) -> Option<&Transactions> { None } @@ -547,6 +566,9 @@ impl ExecPayload for BlindedPayload { Ok(inner.execution_payload_header.withdrawals_root) } BlindedPayload::Deneb(ref inner) => Ok(inner.execution_payload_header.withdrawals_root), + BlindedPayload::Electra(ref inner) => { + Ok(inner.execution_payload_header.withdrawals_root) + } } } @@ -556,6 +578,7 @@ impl ExecPayload for BlindedPayload { Err(Error::IncorrectStateVariant) } BlindedPayload::Deneb(ref inner) => Ok(inner.execution_payload_header.blob_gas_used), + BlindedPayload::Electra(ref inner) => Ok(inner.execution_payload_header.blob_gas_used), } } @@ -572,12 +595,12 @@ impl ExecPayload for BlindedPayload { } } -impl<'b, T: EthSpec> ExecPayload for BlindedPayloadRef<'b, T> { +impl<'b, E: EthSpec> ExecPayload for BlindedPayloadRef<'b, E> { fn block_type() -> BlockType { BlockType::Blinded } - fn to_execution_payload_header<'a>(&'a self) -> ExecutionPayloadHeader { + fn to_execution_payload_header<'a>(&'a self) -> ExecutionPayloadHeader { map_blinded_payload_ref!(&'a _, self, move |payload, cons| { cons(payload); payload.to_execution_payload_header() @@ -633,7 +656,7 @@ impl<'b, T: EthSpec> ExecPayload for BlindedPayloadRef<'b, T> { }) } - fn transactions(&self) -> Option<&Transactions> { + fn transactions(&self) -> Option<&Transactions> { None } @@ -644,6 +667,9 @@ impl<'b, T: EthSpec> ExecPayload for BlindedPayloadRef<'b, T> { Ok(inner.execution_payload_header.withdrawals_root) } BlindedPayloadRef::Deneb(inner) => Ok(inner.execution_payload_header.withdrawals_root), + BlindedPayloadRef::Electra(inner) => { + Ok(inner.execution_payload_header.withdrawals_root) + } } } @@ -653,6 +679,7 @@ impl<'b, T: EthSpec> ExecPayload for BlindedPayloadRef<'b, T> { Err(Error::IncorrectStateVariant) } BlindedPayloadRef::Deneb(inner) => Ok(inner.execution_payload_header.blob_gas_used), + BlindedPayloadRef::Electra(inner) => Ok(inner.execution_payload_header.blob_gas_used), } } @@ -683,12 +710,12 @@ macro_rules! impl_exec_payload_common { $f:block, $g:block, $h:block) => { - impl ExecPayload for $wrapper_type { + impl ExecPayload for $wrapper_type { fn block_type() -> BlockType { BlockType::$block_type_variant } - fn to_execution_payload_header(&self) -> ExecutionPayloadHeader { + fn to_execution_payload_header(&self) -> ExecutionPayloadHeader { ExecutionPayloadHeader::$fork_variant($wrapped_type_header::from( &self.$wrapped_field, )) @@ -731,7 +758,7 @@ macro_rules! impl_exec_payload_common { f(self) } - fn transactions(&self) -> Option<&Transactions> { + fn transactions(&self) -> Option<&Transactions> { let f = $f; f(self) } @@ -747,8 +774,8 @@ macro_rules! impl_exec_payload_common { } } - impl From<$wrapped_type> for $wrapper_type { - fn from($wrapped_field: $wrapped_type) -> Self { + impl From<$wrapped_type> for $wrapper_type { + fn from($wrapped_field: $wrapped_type) -> Self { Self { $wrapped_field } } } @@ -769,23 +796,23 @@ macro_rules! impl_exec_payload_for_fork { $fork_variant, // Merge Blinded, { - |wrapper: &$wrapper_type_header| { + |wrapper: &$wrapper_type_header| { wrapper.execution_payload_header == $wrapped_type_header::from(&$wrapped_type_full::default()) } }, { |_| { None } }, { - let c: for<'a> fn(&'a $wrapper_type_header) -> Result = - |payload: &$wrapper_type_header| { + let c: for<'a> fn(&'a $wrapper_type_header) -> Result = + |payload: &$wrapper_type_header| { let wrapper_ref_type = BlindedPayloadRef::$fork_variant(&payload); wrapper_ref_type.withdrawals_root() }; c }, { - let c: for<'a> fn(&'a $wrapper_type_header) -> Result = - |payload: &$wrapper_type_header| { + let c: for<'a> fn(&'a $wrapper_type_header) -> Result = + |payload: &$wrapper_type_header| { let wrapper_ref_type = BlindedPayloadRef::$fork_variant(&payload); wrapper_ref_type.blob_gas_used() }; @@ -793,10 +820,10 @@ macro_rules! impl_exec_payload_for_fork { } ); - impl TryInto<$wrapper_type_header> for BlindedPayload { + impl TryInto<$wrapper_type_header> for BlindedPayload { type Error = Error; - fn try_into(self) -> Result<$wrapper_type_header, Self::Error> { + fn try_into(self) -> Result<$wrapper_type_header, Self::Error> { match self { BlindedPayload::$fork_variant(payload) => Ok(payload), _ => Err(Error::IncorrectStateVariant), @@ -811,7 +838,7 @@ macro_rules! impl_exec_payload_for_fork { // The default `BlindedPayload` is therefore the payload header that results from blinding the // default `ExecutionPayload`, which differs from the default `ExecutionPayloadHeader` in that // its `transactions_root` is the hash of the empty list rather than 0x0. - impl Default for $wrapper_type_header { + impl Default for $wrapper_type_header { fn default() -> Self { Self { execution_payload_header: $wrapped_type_header::from( @@ -821,9 +848,9 @@ macro_rules! impl_exec_payload_for_fork { } } - impl TryFrom> for $wrapper_type_header { + impl TryFrom> for $wrapper_type_header { type Error = Error; - fn try_from(header: ExecutionPayloadHeader) -> Result { + fn try_from(header: ExecutionPayloadHeader) -> Result { match header { ExecutionPayloadHeader::$fork_variant(execution_payload_header) => { Ok(execution_payload_header.into()) @@ -834,8 +861,8 @@ macro_rules! impl_exec_payload_for_fork { } // BlindedPayload* from CoW reference to ExecutionPayload* (hopefully just a reference). - impl<'a, T: EthSpec> From>> for $wrapper_type_header { - fn from(execution_payload: Cow<'a, $wrapped_type_full>) -> Self { + impl<'a, E: EthSpec> From>> for $wrapper_type_header { + fn from(execution_payload: Cow<'a, $wrapped_type_full>) -> Self { Self { execution_payload_header: $wrapped_type_header::from(&*execution_payload), } @@ -853,26 +880,26 @@ macro_rules! impl_exec_payload_for_fork { $fork_variant, // Merge Full, { - |wrapper: &$wrapper_type_full| { + |wrapper: &$wrapper_type_full| { wrapper.execution_payload == $wrapped_type_full::default() } }, { - let c: for<'a> fn(&'a $wrapper_type_full) -> Option<&'a Transactions> = - |payload: &$wrapper_type_full| Some(&payload.execution_payload.transactions); + let c: for<'a> fn(&'a $wrapper_type_full) -> Option<&'a Transactions> = + |payload: &$wrapper_type_full| Some(&payload.execution_payload.transactions); c }, { - let c: for<'a> fn(&'a $wrapper_type_full) -> Result = - |payload: &$wrapper_type_full| { + let c: for<'a> fn(&'a $wrapper_type_full) -> Result = + |payload: &$wrapper_type_full| { let wrapper_ref_type = FullPayloadRef::$fork_variant(&payload); wrapper_ref_type.withdrawals_root() }; c }, { - let c: for<'a> fn(&'a $wrapper_type_full) -> Result = - |payload: &$wrapper_type_full| { + let c: for<'a> fn(&'a $wrapper_type_full) -> Result = + |payload: &$wrapper_type_full| { let wrapper_ref_type = FullPayloadRef::$fork_variant(&payload); wrapper_ref_type.blob_gas_used() }; @@ -880,7 +907,7 @@ macro_rules! impl_exec_payload_for_fork { } ); - impl Default for $wrapper_type_full { + impl Default for $wrapper_type_full { fn default() -> Self { Self { execution_payload: $wrapped_type_full::default(), @@ -889,32 +916,32 @@ macro_rules! impl_exec_payload_for_fork { } // FullPayload * from CoW reference to ExecutionPayload* (hopefully already owned). - impl<'a, T: EthSpec> From>> for $wrapper_type_full { - fn from(execution_payload: Cow<'a, $wrapped_type_full>) -> Self { + impl<'a, E: EthSpec> From>> for $wrapper_type_full { + fn from(execution_payload: Cow<'a, $wrapped_type_full>) -> Self { Self { execution_payload: $wrapped_type_full::from(execution_payload.into_owned()), } } } - impl TryFrom> for $wrapper_type_full { + impl TryFrom> for $wrapper_type_full { type Error = Error; - fn try_from(_: ExecutionPayloadHeader) -> Result { + fn try_from(_: ExecutionPayloadHeader) -> Result { Err(Error::PayloadConversionLogicFlaw) } } - impl TryFrom<$wrapped_type_header> for $wrapper_type_full { + impl TryFrom<$wrapped_type_header> for $wrapper_type_full { type Error = Error; - fn try_from(_: $wrapped_type_header) -> Result { + fn try_from(_: $wrapped_type_header) -> Result { Err(Error::PayloadConversionLogicFlaw) } } - impl TryInto<$wrapper_type_full> for FullPayload { + impl TryInto<$wrapper_type_full> for FullPayload { type Error = Error; - fn try_into(self) -> Result<$wrapper_type_full, Self::Error> { + fn try_into(self) -> Result<$wrapper_type_full, Self::Error> { match self { FullPayload::$fork_variant(payload) => Ok(payload), _ => Err(Error::PayloadConversionLogicFlaw), @@ -945,16 +972,24 @@ impl_exec_payload_for_fork!( ExecutionPayloadDeneb, Deneb ); +impl_exec_payload_for_fork!( + BlindedPayloadElectra, + FullPayloadElectra, + ExecutionPayloadHeaderElectra, + ExecutionPayloadElectra, + Electra +); -impl AbstractExecPayload for BlindedPayload { - type Ref<'a> = BlindedPayloadRef<'a, T>; - type Merge = BlindedPayloadMerge; - type Capella = BlindedPayloadCapella; - type Deneb = BlindedPayloadDeneb; +impl AbstractExecPayload for BlindedPayload { + type Ref<'a> = BlindedPayloadRef<'a, E>; + type Merge = BlindedPayloadMerge; + type Capella = BlindedPayloadCapella; + type Deneb = BlindedPayloadDeneb; + type Electra = BlindedPayloadElectra; } -impl From> for BlindedPayload { - fn from(payload: ExecutionPayload) -> Self { +impl From> for BlindedPayload { + fn from(payload: ExecutionPayload) -> Self { // This implementation is a bit wasteful in that it discards the payload body. // Required by the top-level constraint on AbstractExecPayload but could maybe be loosened // in future. @@ -964,8 +999,8 @@ impl From> for BlindedPayload { } } -impl From> for BlindedPayload { - fn from(execution_payload_header: ExecutionPayloadHeader) -> Self { +impl From> for BlindedPayload { + fn from(execution_payload_header: ExecutionPayloadHeader) -> Self { match execution_payload_header { ExecutionPayloadHeader::Merge(execution_payload_header) => { Self::Merge(BlindedPayloadMerge { @@ -982,12 +1017,17 @@ impl From> for BlindedPayload { execution_payload_header, }) } + ExecutionPayloadHeader::Electra(execution_payload_header) => { + Self::Electra(BlindedPayloadElectra { + execution_payload_header, + }) + } } } } -impl From> for ExecutionPayloadHeader { - fn from(blinded: BlindedPayload) -> Self { +impl From> for ExecutionPayloadHeader { + fn from(blinded: BlindedPayload) -> Self { match blinded { BlindedPayload::Merge(blinded_payload) => { ExecutionPayloadHeader::Merge(blinded_payload.execution_payload_header) @@ -998,6 +1038,9 @@ impl From> for ExecutionPayloadHeader { BlindedPayload::Deneb(blinded_payload) => { ExecutionPayloadHeader::Deneb(blinded_payload.execution_payload_header) } + BlindedPayload::Electra(blinded_payload) => { + ExecutionPayloadHeader::Electra(blinded_payload.execution_payload_header) + } } } } diff --git a/consensus/types/src/pending_attestation.rs b/consensus/types/src/pending_attestation.rs index d25a6987c0..0bccab5079 100644 --- a/consensus/types/src/pending_attestation.rs +++ b/consensus/types/src/pending_attestation.rs @@ -21,9 +21,9 @@ use tree_hash_derive::TreeHash; TestRandom, arbitrary::Arbitrary, )] -#[arbitrary(bound = "T: EthSpec")] -pub struct PendingAttestation { - pub aggregation_bits: BitList, +#[arbitrary(bound = "E: EthSpec")] +pub struct PendingAttestation { + pub aggregation_bits: BitList, pub data: AttestationData, #[serde(with = "serde_utils::quoted_u64")] pub inclusion_delay: u64, diff --git a/consensus/types/src/preset.rs b/consensus/types/src/preset.rs index 63a372ea1c..626a45d971 100644 --- a/consensus/types/src/preset.rs +++ b/consensus/types/src/preset.rs @@ -81,11 +81,11 @@ pub struct BasePreset { } impl BasePreset { - pub fn from_chain_spec(spec: &ChainSpec) -> Self { + pub fn from_chain_spec(spec: &ChainSpec) -> Self { Self { max_committees_per_slot: spec.max_committees_per_slot as u64, target_committee_size: spec.target_committee_size as u64, - max_validators_per_committee: T::MaxValidatorsPerCommittee::to_u64(), + max_validators_per_committee: E::MaxValidatorsPerCommittee::to_u64(), shuffle_round_count: spec.shuffle_round_count, hysteresis_quotient: spec.hysteresis_quotient, hysteresis_downward_multiplier: spec.hysteresis_downward_multiplier, @@ -95,27 +95,27 @@ impl BasePreset { max_effective_balance: spec.max_effective_balance, effective_balance_increment: spec.effective_balance_increment, min_attestation_inclusion_delay: spec.min_attestation_inclusion_delay, - slots_per_epoch: T::SlotsPerEpoch::to_u64(), + slots_per_epoch: E::SlotsPerEpoch::to_u64(), min_seed_lookahead: spec.min_seed_lookahead, max_seed_lookahead: spec.max_seed_lookahead, - epochs_per_eth1_voting_period: T::EpochsPerEth1VotingPeriod::to_u64(), - slots_per_historical_root: T::SlotsPerHistoricalRoot::to_u64(), + epochs_per_eth1_voting_period: E::EpochsPerEth1VotingPeriod::to_u64(), + slots_per_historical_root: E::SlotsPerHistoricalRoot::to_u64(), min_epochs_to_inactivity_penalty: spec.min_epochs_to_inactivity_penalty, - epochs_per_historical_vector: T::EpochsPerHistoricalVector::to_u64(), - epochs_per_slashings_vector: T::EpochsPerSlashingsVector::to_u64(), - historical_roots_limit: T::HistoricalRootsLimit::to_u64(), - validator_registry_limit: T::ValidatorRegistryLimit::to_u64(), + epochs_per_historical_vector: E::EpochsPerHistoricalVector::to_u64(), + epochs_per_slashings_vector: E::EpochsPerSlashingsVector::to_u64(), + historical_roots_limit: E::HistoricalRootsLimit::to_u64(), + validator_registry_limit: E::ValidatorRegistryLimit::to_u64(), base_reward_factor: spec.base_reward_factor, whistleblower_reward_quotient: spec.whistleblower_reward_quotient, proposer_reward_quotient: spec.proposer_reward_quotient, inactivity_penalty_quotient: spec.inactivity_penalty_quotient, min_slashing_penalty_quotient: spec.min_slashing_penalty_quotient, proportional_slashing_multiplier: spec.proportional_slashing_multiplier, - max_proposer_slashings: T::MaxProposerSlashings::to_u64(), - max_attester_slashings: T::MaxAttesterSlashings::to_u64(), - max_attestations: T::MaxAttestations::to_u64(), - max_deposits: T::MaxDeposits::to_u64(), - max_voluntary_exits: T::MaxVoluntaryExits::to_u64(), + max_proposer_slashings: E::MaxProposerSlashings::to_u64(), + max_attester_slashings: E::MaxAttesterSlashings::to_u64(), + max_attestations: E::MaxAttestations::to_u64(), + max_deposits: E::MaxDeposits::to_u64(), + max_voluntary_exits: E::MaxVoluntaryExits::to_u64(), } } } @@ -138,12 +138,12 @@ pub struct AltairPreset { } impl AltairPreset { - pub fn from_chain_spec(spec: &ChainSpec) -> Self { + pub fn from_chain_spec(spec: &ChainSpec) -> Self { Self { inactivity_penalty_quotient_altair: spec.inactivity_penalty_quotient_altair, min_slashing_penalty_quotient_altair: spec.min_slashing_penalty_quotient_altair, proportional_slashing_multiplier_altair: spec.proportional_slashing_multiplier_altair, - sync_committee_size: T::SyncCommitteeSize::to_u64(), + sync_committee_size: E::SyncCommitteeSize::to_u64(), epochs_per_sync_committee_period: spec.epochs_per_sync_committee_period, min_sync_committee_participants: spec.min_sync_committee_participants, } @@ -170,16 +170,16 @@ pub struct BellatrixPreset { } impl BellatrixPreset { - pub fn from_chain_spec(spec: &ChainSpec) -> Self { + pub fn from_chain_spec(spec: &ChainSpec) -> Self { Self { inactivity_penalty_quotient_bellatrix: spec.inactivity_penalty_quotient_bellatrix, min_slashing_penalty_quotient_bellatrix: spec.min_slashing_penalty_quotient_bellatrix, proportional_slashing_multiplier_bellatrix: spec .proportional_slashing_multiplier_bellatrix, - max_bytes_per_transaction: T::max_bytes_per_transaction() as u64, - max_transactions_per_payload: T::max_transactions_per_payload() as u64, - bytes_per_logs_bloom: T::bytes_per_logs_bloom() as u64, - max_extra_data_bytes: T::max_extra_data_bytes() as u64, + max_bytes_per_transaction: E::max_bytes_per_transaction() as u64, + max_transactions_per_payload: E::max_transactions_per_payload() as u64, + bytes_per_logs_bloom: E::bytes_per_logs_bloom() as u64, + max_extra_data_bytes: E::max_extra_data_bytes() as u64, } } } @@ -196,10 +196,10 @@ pub struct CapellaPreset { } impl CapellaPreset { - pub fn from_chain_spec(spec: &ChainSpec) -> Self { + pub fn from_chain_spec(spec: &ChainSpec) -> Self { Self { - max_bls_to_execution_changes: T::max_bls_to_execution_changes() as u64, - max_withdrawals_per_payload: T::max_withdrawals_per_payload() as u64, + max_bls_to_execution_changes: E::max_bls_to_execution_changes() as u64, + max_withdrawals_per_payload: E::max_withdrawals_per_payload() as u64, max_validators_per_withdrawals_sweep: spec.max_validators_per_withdrawals_sweep, } } @@ -217,11 +217,26 @@ pub struct DenebPreset { } impl DenebPreset { - pub fn from_chain_spec(_spec: &ChainSpec) -> Self { + pub fn from_chain_spec(_spec: &ChainSpec) -> Self { Self { - max_blobs_per_block: T::max_blobs_per_block() as u64, - max_blob_commitments_per_block: T::max_blob_commitments_per_block() as u64, - field_elements_per_blob: T::field_elements_per_blob() as u64, + max_blobs_per_block: E::max_blobs_per_block() as u64, + max_blob_commitments_per_block: E::max_blob_commitments_per_block() as u64, + field_elements_per_blob: E::field_elements_per_blob() as u64, + } + } +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub struct ElectraPreset { + #[serde(with = "serde_utils::quoted_u64")] + pub electra_placeholder: u64, +} + +impl ElectraPreset { + pub fn from_chain_spec(_spec: &ChainSpec) -> Self { + Self { + electra_placeholder: 0, } } } @@ -267,6 +282,9 @@ mod test { let deneb: DenebPreset = preset_from_file(&preset_name, "deneb.yaml"); assert_eq!(deneb, DenebPreset::from_chain_spec::(&spec)); + + let electra: ElectraPreset = preset_from_file(&preset_name, "electra.yaml"); + assert_eq!(electra, ElectraPreset::from_chain_spec::(&spec)); } #[test] diff --git a/consensus/types/src/selection_proof.rs b/consensus/types/src/selection_proof.rs index 2a404b3b96..cd9f9c06d0 100644 --- a/consensus/types/src/selection_proof.rs +++ b/consensus/types/src/selection_proof.rs @@ -5,13 +5,12 @@ use ethereum_hashing::hash; use safe_arith::{ArithError, SafeArith}; use ssz::Encode; use std::cmp; -use std::convert::TryInto; #[derive(arbitrary::Arbitrary, PartialEq, Debug, Clone)] pub struct SelectionProof(Signature); impl SelectionProof { - pub fn new( + pub fn new( slot: Slot, secret_key: &SecretKey, fork: &Fork, @@ -19,7 +18,7 @@ impl SelectionProof { spec: &ChainSpec, ) -> Self { let domain = spec.get_domain( - slot.epoch(T::slots_per_epoch()), + slot.epoch(E::slots_per_epoch()), Domain::SelectionProof, fork, genesis_validators_root, @@ -58,7 +57,7 @@ impl SelectionProof { signature_hash_int.safe_rem(modulo).map(|rem| rem == 0) } - pub fn verify( + pub fn verify( &self, slot: Slot, pubkey: &PublicKey, @@ -67,7 +66,7 @@ impl SelectionProof { spec: &ChainSpec, ) -> bool { let domain = spec.get_domain( - slot.epoch(T::slots_per_epoch()), + slot.epoch(E::slots_per_epoch()), Domain::SelectionProof, fork, genesis_validators_root, diff --git a/consensus/types/src/signed_aggregate_and_proof.rs b/consensus/types/src/signed_aggregate_and_proof.rs index 10010073e5..c31c50ea17 100644 --- a/consensus/types/src/signed_aggregate_and_proof.rs +++ b/consensus/types/src/signed_aggregate_and_proof.rs @@ -24,23 +24,23 @@ use tree_hash_derive::TreeHash; TreeHash, arbitrary::Arbitrary, )] -#[serde(bound = "T: EthSpec")] -#[arbitrary(bound = "T: EthSpec")] -pub struct SignedAggregateAndProof { +#[serde(bound = "E: EthSpec")] +#[arbitrary(bound = "E: EthSpec")] +pub struct SignedAggregateAndProof { /// The `AggregateAndProof` that was signed. - pub message: AggregateAndProof, + pub message: AggregateAndProof, /// The aggregate attestation. pub signature: Signature, } -impl SignedAggregateAndProof { +impl SignedAggregateAndProof { /// Produces a new `SignedAggregateAndProof` with a `selection_proof` generated by signing /// `aggregate.data.slot` with `secret_key`. /// /// If `selection_proof.is_none()` it will be computed locally. pub fn from_aggregate( aggregator_index: u64, - aggregate: Attestation, + aggregate: Attestation, selection_proof: Option, secret_key: &SecretKey, fork: &Fork, @@ -57,7 +57,7 @@ impl SignedAggregateAndProof { spec, ); - let target_epoch = message.aggregate.data.slot.epoch(T::slots_per_epoch()); + let target_epoch = message.aggregate.data.slot.epoch(E::slots_per_epoch()); let domain = spec.get_domain( target_epoch, Domain::AggregateAndProof, diff --git a/consensus/types/src/signed_beacon_block.rs b/consensus/types/src/signed_beacon_block.rs index 37304de1f1..da4ac39236 100644 --- a/consensus/types/src/signed_beacon_block.rs +++ b/consensus/types/src/signed_beacon_block.rs @@ -1,6 +1,5 @@ use crate::beacon_block_body::format_kzg_commitments; use crate::*; -use bls::Signature; use derivative::Derivative; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; @@ -38,7 +37,7 @@ impl From for Hash256 { /// A `BeaconBlock` and a signature from its proposer. #[superstruct( - variants(Base, Altair, Merge, Capella, Deneb), + variants(Base, Altair, Merge, Capella, Deneb, Electra), variant_attributes( derive( Debug, @@ -79,6 +78,8 @@ pub struct SignedBeaconBlock = FullP pub message: BeaconBlockCapella, #[superstruct(only(Deneb), partial_getter(rename = "message_deneb"))] pub message: BeaconBlockDeneb, + #[superstruct(only(Electra), partial_getter(rename = "message_electra"))] + pub message: BeaconBlockElectra, pub signature: Signature, } @@ -158,6 +159,9 @@ impl> SignedBeaconBlock BeaconBlock::Deneb(message) => { SignedBeaconBlock::Deneb(SignedBeaconBlockDeneb { message, signature }) } + BeaconBlock::Electra(message) => { + SignedBeaconBlock::Electra(SignedBeaconBlockElectra { message, signature }) + } } } @@ -468,6 +472,62 @@ impl SignedBeaconBlockDeneb> { } } +impl SignedBeaconBlockElectra> { + pub fn into_full_block( + self, + execution_payload: ExecutionPayloadElectra, + ) -> SignedBeaconBlockElectra> { + let SignedBeaconBlockElectra { + message: + BeaconBlockElectra { + slot, + proposer_index, + parent_root, + state_root, + body: + BeaconBlockBodyElectra { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + execution_payload: BlindedPayloadElectra { .. }, + bls_to_execution_changes, + blob_kzg_commitments, + }, + }, + signature, + } = self; + SignedBeaconBlockElectra { + message: BeaconBlockElectra { + slot, + proposer_index, + parent_root, + state_root, + body: BeaconBlockBodyElectra { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + execution_payload: FullPayloadElectra { execution_payload }, + bls_to_execution_changes, + blob_kzg_commitments, + }, + }, + signature, + } + } +} + impl SignedBeaconBlock> { pub fn try_into_full_block( self, @@ -485,11 +545,15 @@ impl SignedBeaconBlock> { (SignedBeaconBlock::Deneb(block), Some(ExecutionPayload::Deneb(payload))) => { SignedBeaconBlock::Deneb(block.into_full_block(payload)) } + (SignedBeaconBlock::Electra(block), Some(ExecutionPayload::Electra(payload))) => { + SignedBeaconBlock::Electra(block.into_full_block(payload)) + } // avoid wildcard matching forks so that compiler will // direct us here when a new fork has been added (SignedBeaconBlock::Merge(_), _) => return None, (SignedBeaconBlock::Capella(_), _) => return None, (SignedBeaconBlock::Deneb(_), _) => return None, + (SignedBeaconBlock::Electra(_), _) => return None, }; Some(full_block) } @@ -632,6 +696,9 @@ pub mod ssz_tagged_signed_beacon_block { ForkName::Deneb => Ok(SignedBeaconBlock::Deneb( SignedBeaconBlockDeneb::from_ssz_bytes(body)?, )), + ForkName::Electra => Ok(SignedBeaconBlock::Electra( + SignedBeaconBlockElectra::from_ssz_bytes(body)?, + )), } } } @@ -723,7 +790,14 @@ mod test { BeaconBlock::Capella(BeaconBlockCapella::empty(spec)), sig.clone(), ), - SignedBeaconBlock::from_block(BeaconBlock::Deneb(BeaconBlockDeneb::empty(spec)), sig), + SignedBeaconBlock::from_block( + BeaconBlock::Deneb(BeaconBlockDeneb::empty(spec)), + sig.clone(), + ), + SignedBeaconBlock::from_block( + BeaconBlock::Electra(BeaconBlockElectra::empty(spec)), + sig, + ), ]; for block in blocks { diff --git a/consensus/types/src/signed_bls_to_execution_change.rs b/consensus/types/src/signed_bls_to_execution_change.rs index 2a4ecdf438..a7bfd7c271 100644 --- a/consensus/types/src/signed_bls_to_execution_change.rs +++ b/consensus/types/src/signed_bls_to_execution_change.rs @@ -1,6 +1,5 @@ use crate::test_utils::TestRandom; use crate::*; -use bls::Signature; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; diff --git a/consensus/types/src/signed_contribution_and_proof.rs b/consensus/types/src/signed_contribution_and_proof.rs index 6cb45ac8e6..068fd980ae 100644 --- a/consensus/types/src/signed_contribution_and_proof.rs +++ b/consensus/types/src/signed_contribution_and_proof.rs @@ -22,23 +22,23 @@ use tree_hash_derive::TreeHash; TreeHash, arbitrary::Arbitrary, )] -#[serde(bound = "T: EthSpec")] -#[arbitrary(bound = "T: EthSpec")] -pub struct SignedContributionAndProof { +#[serde(bound = "E: EthSpec")] +#[arbitrary(bound = "E: EthSpec")] +pub struct SignedContributionAndProof { /// The `ContributionAndProof` that was signed. - pub message: ContributionAndProof, + pub message: ContributionAndProof, /// The validator's signature of `message`. pub signature: Signature, } -impl SignedContributionAndProof { +impl SignedContributionAndProof { /// Produces a new `SignedContributionAndProof` with a `selection_proof` generated by signing /// `aggregate.data.slot` with `secret_key`. /// /// If `selection_proof.is_none()` it will be computed locally. pub fn from_aggregate( aggregator_index: u64, - contribution: SyncCommitteeContribution, + contribution: SyncCommitteeContribution, selection_proof: Option, secret_key: &SecretKey, fork: &Fork, @@ -55,7 +55,7 @@ impl SignedContributionAndProof { spec, ); - let epoch = message.contribution.slot.epoch(T::slots_per_epoch()); + let epoch = message.contribution.slot.epoch(E::slots_per_epoch()); let domain = spec.get_domain( epoch, Domain::ContributionAndProof, diff --git a/consensus/types/src/slot_epoch.rs b/consensus/types/src/slot_epoch.rs index ec659d1dbb..79d0064911 100644 --- a/consensus/types/src/slot_epoch.rs +++ b/consensus/types/src/slot_epoch.rs @@ -19,7 +19,6 @@ use serde::{Deserialize, Serialize}; use ssz::{Decode, DecodeError, Encode}; use std::fmt; use std::hash::Hash; -use std::iter::Iterator; #[cfg(feature = "legacy-arith")] use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Rem, Sub, SubAssign}; diff --git a/consensus/types/src/sqlite.rs b/consensus/types/src/sqlite.rs index 194d14b23e..aa20666ae1 100644 --- a/consensus/types/src/sqlite.rs +++ b/consensus/types/src/sqlite.rs @@ -4,7 +4,6 @@ use rusqlite::{ types::{FromSql, FromSqlError, ToSql, ToSqlOutput, ValueRef}, Error, }; -use std::convert::TryFrom; macro_rules! impl_to_from_sql { ($type:ty) => { diff --git a/consensus/types/src/subnet_id.rs b/consensus/types/src/subnet_id.rs index 2752e31092..9b6a2e6a19 100644 --- a/consensus/types/src/subnet_id.rs +++ b/consensus/types/src/subnet_id.rs @@ -39,12 +39,12 @@ impl SubnetId { /// Compute the subnet for an attestation with `attestation_data` where each slot in the /// attestation epoch contains `committee_count_per_slot` committees. - pub fn compute_subnet_for_attestation_data( + pub fn compute_subnet_for_attestation_data( attestation_data: &AttestationData, committee_count_per_slot: u64, spec: &ChainSpec, ) -> Result { - Self::compute_subnet::( + Self::compute_subnet::( attestation_data.slot, attestation_data.index, committee_count_per_slot, @@ -55,13 +55,13 @@ impl SubnetId { /// Compute the subnet for an attestation with `attestation.data.slot == slot` and /// `attestation.data.index == committee_index` where each slot in the attestation epoch /// contains `committee_count_at_slot` committees. - pub fn compute_subnet( + pub fn compute_subnet( slot: Slot, committee_index: CommitteeIndex, committee_count_at_slot: u64, spec: &ChainSpec, ) -> Result { - let slots_since_epoch_start: u64 = slot.as_u64().safe_rem(T::slots_per_epoch())?; + let slots_since_epoch_start: u64 = slot.as_u64().safe_rem(E::slots_per_epoch())?; let committees_since_epoch_start = committee_count_at_slot.safe_mul(slots_since_epoch_start)?; @@ -72,36 +72,43 @@ impl SubnetId { .into()) } - #[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( + #[allow(clippy::arithmetic_side_effects)] + pub fn compute_subnets_for_epoch( node_id: ethereum_types::U256, epoch: Epoch, spec: &ChainSpec, ) -> Result<(impl Iterator, Epoch), &'static str> { - // Simplify the variable name + // simplify variable naming let subscription_duration = spec.epochs_per_subnet_subscription; + let prefix_bits = spec.attestation_subnet_prefix_bits as u64; + let shuffling_prefix_bits = spec.attestation_subnet_shuffling_prefix_bits as u64; - let node_id_prefix = - (node_id >> (256 - spec.attestation_subnet_prefix_bits as usize)).as_usize(); + // calculate the prefixes used to compute the subnet and shuffling + let node_id_prefix = (node_id >> (256 - prefix_bits)).as_u64(); + let shuffling_prefix = (node_id >> (256 - (prefix_bits + shuffling_prefix_bits))).as_u64(); - // 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(); + // number of groups the shuffling creates + let shuffling_groups = 1 << shuffling_prefix_bits; + // shuffling group for this node + let shuffling_bits = shuffling_prefix % shuffling_groups; + let epoch_transition = (node_id_prefix + + (shuffling_bits * (subscription_duration >> shuffling_prefix_bits))) + % subscription_duration; // 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); + .saturating_sub((epoch.as_u64() + epoch_transition) % subscription_duration); - let subscription_event_idx = (epoch.as_u64() + node_offset) / subscription_duration; + let subscription_event_idx = (epoch.as_u64() + epoch_transition) / 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 permutated_prefix = compute_shuffled_index( - node_id_prefix, + node_id_prefix as usize, num_subnets, &permutation_seed, spec.shuffle_round_count, @@ -180,38 +187,33 @@ mod tests { "60930578857433095740782970114409273483106482059893286066493409689627770333527", "103822458477361691467064888613019442068586830412598673713899771287914656699997", ] - .into_iter() - .map(|v| ethereum_types::U256::from_dec_str(v).unwrap()) - .collect::>(); + .map(|v| ethereum_types::U256::from_dec_str(v).unwrap()); let epochs = [ 54321u64, 1017090249, 1827566880, 846255942, 766597383, 1204990115, 1616209495, 1774367616, 1484598751, 3525502229, ] - .into_iter() - .map(Epoch::from) - .collect::>(); + .map(Epoch::from); // Test mainnet let spec = ChainSpec::mainnet(); // Calculated by hand - let expected_valid_time: Vec = [ - 54528, 1017090371, 1827567108, 846256076, 766597570, 1204990135, 1616209582, - 1774367723, 1484598953, 3525502371, - ] - .into(); + let expected_valid_time = [ + 54528u64, 1017090255, 1827567030, 846256049, 766597387, 1204990287, 1616209536, + 1774367857, 1484598847, 3525502311, + ]; // Calculated from pyspec - let expected_subnets = vec![ + let expected_subnets = [ vec![4u64, 5u64], - vec![61, 62], - vec![23, 24], + vec![31, 32], + vec![39, 40], vec![38, 39], vec![53, 54], - vec![39, 40], + vec![57, 58], vec![48, 49], - vec![39, 40], + vec![1, 2], vec![34, 35], vec![37, 38], ]; @@ -228,11 +230,11 @@ mod tests { >(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::>() ); + assert_eq!(Epoch::from(expected_valid_time[x]), valid_time); } } } diff --git a/consensus/types/src/sync_aggregate.rs b/consensus/types/src/sync_aggregate.rs index bb00c4aa20..43f72a3924 100644 --- a/consensus/types/src/sync_aggregate.rs +++ b/consensus/types/src/sync_aggregate.rs @@ -32,15 +32,15 @@ impl From for Error { Derivative, arbitrary::Arbitrary, )] -#[derivative(PartialEq, Hash(bound = "T: EthSpec"))] -#[serde(bound = "T: EthSpec")] -#[arbitrary(bound = "T: EthSpec")] -pub struct SyncAggregate { - pub sync_committee_bits: BitVector, +#[derivative(PartialEq, Hash(bound = "E: EthSpec"))] +#[serde(bound = "E: EthSpec")] +#[arbitrary(bound = "E: EthSpec")] +pub struct SyncAggregate { + pub sync_committee_bits: BitVector, pub sync_committee_signature: AggregateSignature, } -impl SyncAggregate { +impl SyncAggregate { /// New aggregate to be used as the seed for aggregating other signatures. #[allow(clippy::new_without_default)] pub fn new() -> Self { @@ -54,11 +54,11 @@ impl SyncAggregate { /// /// Equivalent to `process_sync_committee_contributions` from the spec. pub fn from_contributions( - contributions: &[SyncCommitteeContribution], - ) -> Result, Error> { + contributions: &[SyncCommitteeContribution], + ) -> Result, Error> { let mut sync_aggregate = Self::new(); let sync_subcommittee_size = - T::sync_committee_size().safe_div(SYNC_COMMITTEE_SUBNET_COUNT as usize)?; + E::sync_committee_size().safe_div(SYNC_COMMITTEE_SUBNET_COUNT as usize)?; for contribution in contributions { for (index, participated) in contribution.aggregation_bits.iter().enumerate() { if participated { diff --git a/consensus/types/src/sync_committee.rs b/consensus/types/src/sync_committee.rs index b42a000bb0..032f0d61f9 100644 --- a/consensus/types/src/sync_committee.rs +++ b/consensus/types/src/sync_committee.rs @@ -36,14 +36,14 @@ impl From for Error { TestRandom, arbitrary::Arbitrary, )] -#[serde(bound = "T: EthSpec")] -#[arbitrary(bound = "T: EthSpec")] -pub struct SyncCommittee { - pub pubkeys: FixedVector, +#[serde(bound = "E: EthSpec")] +#[arbitrary(bound = "E: EthSpec")] +pub struct SyncCommittee { + pub pubkeys: FixedVector, pub aggregate_pubkey: PublicKeyBytes, } -impl SyncCommittee { +impl SyncCommittee { /// Create a temporary sync committee that should *never* be included in a legitimate consensus object. pub fn temporary() -> Self { Self { @@ -57,9 +57,9 @@ impl SyncCommittee { &self, subcommittee_index: usize, ) -> Result, Error> { - let start_subcommittee_index = subcommittee_index.safe_mul(T::sync_subcommittee_size())?; + let start_subcommittee_index = subcommittee_index.safe_mul(E::sync_subcommittee_size())?; let end_subcommittee_index = - start_subcommittee_index.safe_add(T::sync_subcommittee_size())?; + start_subcommittee_index.safe_add(E::sync_subcommittee_size())?; self.pubkeys .get(start_subcommittee_index..end_subcommittee_index) .ok_or(Error::InvalidSubcommitteeRange { @@ -80,9 +80,9 @@ impl SyncCommittee { let mut subnet_positions = HashMap::new(); for (committee_index, validator_pubkey) in self.pubkeys.iter().enumerate() { if pubkey == validator_pubkey { - let subcommittee_index = committee_index.safe_div(T::sync_subcommittee_size())?; + let subcommittee_index = committee_index.safe_div(E::sync_subcommittee_size())?; let position_in_subcommittee = - committee_index.safe_rem(T::sync_subcommittee_size())?; + committee_index.safe_rem(E::sync_subcommittee_size())?; subnet_positions .entry(SyncSubnetId::new(subcommittee_index as u64)) .or_insert_with(Vec::new) diff --git a/consensus/types/src/sync_committee_contribution.rs b/consensus/types/src/sync_committee_contribution.rs index b8ee5c2e36..2c20daa970 100644 --- a/consensus/types/src/sync_committee_contribution.rs +++ b/consensus/types/src/sync_committee_contribution.rs @@ -27,18 +27,18 @@ pub enum Error { TestRandom, arbitrary::Arbitrary, )] -#[serde(bound = "T: EthSpec")] -#[arbitrary(bound = "T: EthSpec")] -pub struct SyncCommitteeContribution { +#[serde(bound = "E: EthSpec")] +#[arbitrary(bound = "E: EthSpec")] +pub struct SyncCommitteeContribution { pub slot: Slot, pub beacon_block_root: Hash256, #[serde(with = "serde_utils::quoted_u64")] pub subcommittee_index: u64, - pub aggregation_bits: BitVector, + pub aggregation_bits: BitVector, pub signature: AggregateSignature, } -impl SyncCommitteeContribution { +impl SyncCommitteeContribution { /// Create a `SyncCommitteeContribution` from: /// /// - `message`: A single `SyncCommitteeMessage`. @@ -95,7 +95,7 @@ pub struct SyncContributionData { } impl SyncContributionData { - pub fn from_contribution(signing_data: &SyncCommitteeContribution) -> Self { + pub fn from_contribution(signing_data: &SyncCommitteeContribution) -> Self { Self { slot: signing_data.slot, beacon_block_root: signing_data.beacon_block_root, @@ -104,7 +104,7 @@ impl SyncContributionData { } } -impl SlotData for SyncCommitteeContribution { +impl SlotData for SyncCommitteeContribution { fn get_slot(&self) -> Slot { self.slot } diff --git a/consensus/types/src/sync_duty.rs b/consensus/types/src/sync_duty.rs index 1058b9d3b4..59fbc960db 100644 --- a/consensus/types/src/sync_duty.rs +++ b/consensus/types/src/sync_duty.rs @@ -37,10 +37,10 @@ impl SyncDuty { /// Create a new `SyncDuty` from a `SyncCommittee`, which contains the pubkeys but not the /// indices. - pub fn from_sync_committee( + pub fn from_sync_committee( validator_index: u64, pubkey: PublicKeyBytes, - sync_committee: &SyncCommittee, + sync_committee: &SyncCommittee, ) -> Option { let validator_sync_committee_indices = sync_committee .pubkeys diff --git a/consensus/types/src/sync_selection_proof.rs b/consensus/types/src/sync_selection_proof.rs index 7cae3946c6..da7370a0c1 100644 --- a/consensus/types/src/sync_selection_proof.rs +++ b/consensus/types/src/sync_selection_proof.rs @@ -10,13 +10,12 @@ use safe_arith::{ArithError, SafeArith}; use ssz::Encode; use ssz_types::typenum::Unsigned; use std::cmp; -use std::convert::TryInto; #[derive(arbitrary::Arbitrary, PartialEq, Debug, Clone)] pub struct SyncSelectionProof(Signature); impl SyncSelectionProof { - pub fn new( + pub fn new( slot: Slot, subcommittee_index: u64, secret_key: &SecretKey, @@ -25,7 +24,7 @@ impl SyncSelectionProof { spec: &ChainSpec, ) -> Self { let domain = spec.get_domain( - slot.epoch(T::slots_per_epoch()), + slot.epoch(E::slots_per_epoch()), Domain::SyncCommitteeSelectionProof, fork, genesis_validators_root, @@ -40,17 +39,17 @@ impl SyncSelectionProof { } /// Returns the "modulo" used for determining if a `SyncSelectionProof` elects an aggregator. - pub fn modulo() -> Result { + pub fn modulo() -> Result { Ok(cmp::max( 1, - (T::SyncCommitteeSize::to_u64()) + (E::SyncCommitteeSize::to_u64()) .safe_div(SYNC_COMMITTEE_SUBNET_COUNT)? .safe_div(TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE)?, )) } - pub fn is_aggregator(&self) -> Result { - self.is_aggregator_from_modulo(Self::modulo::()?) + pub fn is_aggregator(&self) -> Result { + self.is_aggregator_from_modulo(Self::modulo::()?) } pub fn is_aggregator_from_modulo(&self, modulo: u64) -> Result { @@ -66,7 +65,7 @@ impl SyncSelectionProof { signature_hash_int.safe_rem(modulo).map(|rem| rem == 0) } - pub fn verify( + pub fn verify( &self, slot: Slot, subcommittee_index: u64, @@ -76,7 +75,7 @@ impl SyncSelectionProof { spec: &ChainSpec, ) -> bool { let domain = spec.get_domain( - slot.epoch(T::slots_per_epoch()), + slot.epoch(E::slots_per_epoch()), Domain::SyncCommitteeSelectionProof, fork, genesis_validators_root, diff --git a/consensus/types/src/sync_subnet_id.rs b/consensus/types/src/sync_subnet_id.rs index 5605482929..dd0807f21c 100644 --- a/consensus/types/src/sync_subnet_id.rs +++ b/consensus/types/src/sync_subnet_id.rs @@ -39,10 +39,10 @@ impl SyncSubnetId { } /// Compute required subnets to subscribe to given the sync committee indices. - pub fn compute_subnets_for_sync_committee( + pub fn compute_subnets_for_sync_committee( sync_committee_indices: &[u64], ) -> Result, ArithError> { - let subcommittee_size = T::SyncSubcommitteeSize::to_u64(); + let subcommittee_size = E::SyncSubcommitteeSize::to_u64(); sync_committee_indices .iter() diff --git a/consensus/types/src/test_utils/macros.rs b/consensus/types/src/test_utils/macros.rs index 1e275a5760..4fd7720689 100644 --- a/consensus/types/src/test_utils/macros.rs +++ b/consensus/types/src/test_utils/macros.rs @@ -20,7 +20,6 @@ macro_rules! ssz_tests { let original = <$type>::random_for_test(&mut rng); let bytes = ssz_encode(&original); - println!("bytes length: {}", bytes.len()); let decoded = <$type>::from_ssz_bytes(&bytes).unwrap(); assert_eq!(original, decoded); diff --git a/consensus/types/src/test_utils/test_random.rs b/consensus/types/src/test_utils/test_random.rs index f31df2ce1b..0adaf81bd7 100644 --- a/consensus/types/src/test_utils/test_random.rs +++ b/consensus/types/src/test_utils/test_random.rs @@ -2,7 +2,6 @@ use crate::*; use rand::RngCore; use rand::SeedableRng; use rand_xorshift::XorShiftRng; -use ssz_types::typenum::Unsigned; use std::marker::PhantomData; use std::sync::Arc; diff --git a/consensus/types/src/test_utils/test_random/address.rs b/consensus/types/src/test_utils/test_random/address.rs index 3aaad307e8..421801ce53 100644 --- a/consensus/types/src/test_utils/test_random/address.rs +++ b/consensus/types/src/test_utils/test_random/address.rs @@ -1,5 +1,4 @@ use super::*; -use crate::Address; impl TestRandom for Address { fn random_for_test(rng: &mut impl RngCore) -> Self { diff --git a/consensus/types/src/test_utils/test_random/aggregate_signature.rs b/consensus/types/src/test_utils/test_random/aggregate_signature.rs index 5d3c916b9a..772f284431 100644 --- a/consensus/types/src/test_utils/test_random/aggregate_signature.rs +++ b/consensus/types/src/test_utils/test_random/aggregate_signature.rs @@ -1,5 +1,4 @@ use super::*; -use bls::{AggregateSignature, Signature}; impl TestRandom for AggregateSignature { fn random_for_test(rng: &mut impl RngCore) -> Self { diff --git a/consensus/types/src/test_utils/test_random/bitfield.rs b/consensus/types/src/test_utils/test_random/bitfield.rs index 3992421e37..f73f7c18c5 100644 --- a/consensus/types/src/test_utils/test_random/bitfield.rs +++ b/consensus/types/src/test_utils/test_random/bitfield.rs @@ -1,5 +1,4 @@ use super::*; -use crate::{BitList, BitVector, Unsigned}; use smallvec::smallvec; impl TestRandom for BitList { diff --git a/consensus/types/src/test_utils/test_random/hash256.rs b/consensus/types/src/test_utils/test_random/hash256.rs index 8733f7de24..21d443c0e2 100644 --- a/consensus/types/src/test_utils/test_random/hash256.rs +++ b/consensus/types/src/test_utils/test_random/hash256.rs @@ -1,5 +1,4 @@ use super::*; -use crate::Hash256; impl TestRandom for Hash256 { fn random_for_test(rng: &mut impl RngCore) -> Self { diff --git a/consensus/types/src/test_utils/test_random/kzg_proof.rs b/consensus/types/src/test_utils/test_random/kzg_proof.rs index d6d8ed2d08..7e771ca566 100644 --- a/consensus/types/src/test_utils/test_random/kzg_proof.rs +++ b/consensus/types/src/test_utils/test_random/kzg_proof.rs @@ -1,5 +1,5 @@ use super::*; -use kzg::{KzgProof, BYTES_PER_COMMITMENT}; +use kzg::BYTES_PER_COMMITMENT; impl TestRandom for KzgProof { fn random_for_test(rng: &mut impl RngCore) -> Self { diff --git a/consensus/types/src/test_utils/test_random/public_key.rs b/consensus/types/src/test_utils/test_random/public_key.rs index 12821ee623..d33e9ac704 100644 --- a/consensus/types/src/test_utils/test_random/public_key.rs +++ b/consensus/types/src/test_utils/test_random/public_key.rs @@ -1,5 +1,4 @@ use super::*; -use bls::{PublicKey, SecretKey}; impl TestRandom for PublicKey { fn random_for_test(rng: &mut impl RngCore) -> Self { diff --git a/consensus/types/src/test_utils/test_random/public_key_bytes.rs b/consensus/types/src/test_utils/test_random/public_key_bytes.rs index f04bfc3bc8..6e5cafc4f0 100644 --- a/consensus/types/src/test_utils/test_random/public_key_bytes.rs +++ b/consensus/types/src/test_utils/test_random/public_key_bytes.rs @@ -1,6 +1,4 @@ -use std::convert::From; - -use bls::{PublicKeyBytes, PUBLIC_KEY_BYTES_LEN}; +use bls::PUBLIC_KEY_BYTES_LEN; use super::*; diff --git a/consensus/types/src/test_utils/test_random/secret_key.rs b/consensus/types/src/test_utils/test_random/secret_key.rs index 33fbcec560..3f3f6ed518 100644 --- a/consensus/types/src/test_utils/test_random/secret_key.rs +++ b/consensus/types/src/test_utils/test_random/secret_key.rs @@ -1,5 +1,4 @@ use super::*; -use bls::SecretKey; impl TestRandom for SecretKey { fn random_for_test(_rng: &mut impl RngCore) -> Self { diff --git a/consensus/types/src/test_utils/test_random/signature.rs b/consensus/types/src/test_utils/test_random/signature.rs index 119c81babb..5b952296b6 100644 --- a/consensus/types/src/test_utils/test_random/signature.rs +++ b/consensus/types/src/test_utils/test_random/signature.rs @@ -1,5 +1,4 @@ use super::*; -use bls::{SecretKey, Signature}; impl TestRandom for Signature { fn random_for_test(rng: &mut impl RngCore) -> Self { diff --git a/consensus/types/src/test_utils/test_random/signature_bytes.rs b/consensus/types/src/test_utils/test_random/signature_bytes.rs index a4ae772d89..2117a48232 100644 --- a/consensus/types/src/test_utils/test_random/signature_bytes.rs +++ b/consensus/types/src/test_utils/test_random/signature_bytes.rs @@ -1,7 +1,6 @@ -use bls::{SignatureBytes, SIGNATURE_BYTES_LEN}; +use bls::SIGNATURE_BYTES_LEN; use super::*; -use std::convert::From; impl TestRandom for SignatureBytes { fn random_for_test(rng: &mut impl RngCore) -> Self { diff --git a/consensus/types/src/test_utils/test_random/uint256.rs b/consensus/types/src/test_utils/test_random/uint256.rs index a74cc6b3d8..5eccc0a9fa 100644 --- a/consensus/types/src/test_utils/test_random/uint256.rs +++ b/consensus/types/src/test_utils/test_random/uint256.rs @@ -1,5 +1,4 @@ use super::*; -use crate::Uint256; impl TestRandom for Uint256 { fn random_for_test(rng: &mut impl RngCore) -> Self { diff --git a/consensus/types/src/tree_hash_impls.rs b/consensus/types/src/tree_hash_impls.rs index 34043c0e83..eb3660d466 100644 --- a/consensus/types/src/tree_hash_impls.rs +++ b/consensus/types/src/tree_hash_impls.rs @@ -101,7 +101,6 @@ fn process_bool_field(val: bool, leaf: &mut Hash256, force_update: bool) -> bool mod test { use super::*; use crate::test_utils::TestRandom; - use crate::Epoch; use rand::SeedableRng; use rand_xorshift::XorShiftRng; use tree_hash::TreeHash; diff --git a/consensus/types/src/validator.rs b/consensus/types/src/validator.rs index 8fbd9009ea..98567cd1e6 100644 --- a/consensus/types/src/validator.rs +++ b/consensus/types/src/validator.rs @@ -77,6 +77,22 @@ impl Validator { && self.activation_epoch == spec.far_future_epoch } + /// Returns `true` if the validator *could* be eligible for activation at `epoch`. + /// + /// Eligibility depends on finalization, so we assume best-possible finalization. This function + /// returning true is a necessary but *not sufficient* condition for a validator to activate in + /// the epoch transition at the end of `epoch`. + pub fn could_be_eligible_for_activation_at(&self, epoch: Epoch, spec: &ChainSpec) -> bool { + // Has not yet been activated + self.activation_epoch == spec.far_future_epoch + // Placement in queue could be finalized. + // + // NOTE: the epoch distance is 1 rather than 2 because we consider the activations that + // occur at the *end* of `epoch`, after `process_justification_and_finalization` has already + // updated the state's checkpoint. + && self.activation_eligibility_epoch < epoch + } + /// Returns `true` if the validator has eth1 withdrawal credential. pub fn has_eth1_withdrawal_credential(&self, spec: &ChainSpec) -> bool { self.withdrawal_credentials diff --git a/consensus/types/src/validator_subscription.rs b/consensus/types/src/validator_subscription.rs index fd48660c52..62932638ec 100644 --- a/consensus/types/src/validator_subscription.rs +++ b/consensus/types/src/validator_subscription.rs @@ -4,10 +4,8 @@ use ssz_derive::{Decode, Encode}; /// A validator subscription, created when a validator subscribes to a slot to perform optional aggregation /// duties. -#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Encode, Decode)] +#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Encode, Decode, Eq, PartialOrd, Ord)] pub struct ValidatorSubscription { - /// The validators index. - pub validator_index: u64, /// The index of the committee within `slot` of which the validator is a member. Used by the /// beacon node to quickly evaluate the associated `SubnetId`. pub attestation_committee_index: CommitteeIndex, diff --git a/consensus/types/src/voluntary_exit.rs b/consensus/types/src/voluntary_exit.rs index a24f7376a1..74175423e3 100644 --- a/consensus/types/src/voluntary_exit.rs +++ b/consensus/types/src/voluntary_exit.rs @@ -46,7 +46,7 @@ impl VoluntaryExit { spec.fork_version_for_name(fork_name) } // EIP-7044 - ForkName::Deneb => spec.fork_version_for_name(ForkName::Capella), + ForkName::Deneb | ForkName::Electra => spec.fork_version_for_name(ForkName::Capella), }; let domain = spec.compute_domain(Domain::VoluntaryExit, fork_version, genesis_validators_root); diff --git a/crypto/bls/Cargo.toml b/crypto/bls/Cargo.toml index 1216fc2a98..7aa8e02dca 100644 --- a/crypto/bls/Cargo.toml +++ b/crypto/bls/Cargo.toml @@ -7,7 +7,6 @@ edition = { workspace = true } [dependencies] 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 } ethereum_serde_utils = { workspace = true } @@ -22,7 +21,6 @@ blst = { version = "0.3.3", optional = true } arbitrary = [] default = ["supranational"] fake_crypto = [] -milagro = ["milagro_bls"] supranational = ["blst"] supranational-portable = ["supranational", "blst/portable"] supranational-force-adx = ["supranational", "blst/force-adx"] diff --git a/crypto/bls/src/generic_public_key_bytes.rs b/crypto/bls/src/generic_public_key_bytes.rs index 240568b4f6..985bff745c 100644 --- a/crypto/bls/src/generic_public_key_bytes.rs +++ b/crypto/bls/src/generic_public_key_bytes.rs @@ -6,7 +6,6 @@ use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; use serde_utils::hex::encode as hex_encode; use ssz::{Decode, Encode}; -use std::convert::TryInto; use std::fmt; use std::hash::{Hash, Hasher}; use std::marker::PhantomData; diff --git a/crypto/bls/src/generic_signature_bytes.rs b/crypto/bls/src/generic_signature_bytes.rs index 8f9f2a4d88..b291adb735 100644 --- a/crypto/bls/src/generic_signature_bytes.rs +++ b/crypto/bls/src/generic_signature_bytes.rs @@ -7,7 +7,6 @@ use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; use serde_utils::hex::encode as hex_encode; use ssz::{Decode, Encode}; -use std::convert::TryInto; use std::fmt; use std::hash::{Hash, Hasher}; use std::marker::PhantomData; diff --git a/crypto/bls/src/impls/blst.rs b/crypto/bls/src/impls/blst.rs index e831a175c7..0049d79cc5 100644 --- a/crypto/bls/src/impls/blst.rs +++ b/crypto/bls/src/impls/blst.rs @@ -9,7 +9,6 @@ use crate::{ pub use blst::min_pk as blst_core; use blst::{blst_scalar, BLST_ERROR}; use rand::Rng; -use std::iter::ExactSizeIterator; pub const DST: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"; pub const RAND_BITS: usize = 64; diff --git a/crypto/bls/src/impls/milagro.rs b/crypto/bls/src/impls/milagro.rs deleted file mode 100644 index eb4767d3c7..0000000000 --- a/crypto/bls/src/impls/milagro.rs +++ /dev/null @@ -1,194 +0,0 @@ -use crate::{ - generic_aggregate_public_key::TAggregatePublicKey, - generic_aggregate_signature::TAggregateSignature, - generic_public_key::{GenericPublicKey, TPublicKey, PUBLIC_KEY_BYTES_LEN}, - generic_secret_key::{TSecretKey, SECRET_KEY_BYTES_LEN}, - generic_signature::{TSignature, SIGNATURE_BYTES_LEN}, - Error, Hash256, ZeroizeHash, -}; -pub use milagro_bls as milagro; -use rand::thread_rng; -use std::iter::ExactSizeIterator; - -/// Provides the externally-facing, core BLS types. -pub mod types { - pub use super::milagro::AggregatePublicKey; - pub use super::milagro::AggregateSignature; - pub use super::milagro::PublicKey; - pub use super::milagro::SecretKey; - pub use super::milagro::Signature; - pub use super::verify_signature_sets; - pub use super::SignatureSet; -} - -pub type SignatureSet<'a> = crate::generic_signature_set::GenericSignatureSet< - 'a, - milagro::PublicKey, - milagro::AggregatePublicKey, - milagro::Signature, - milagro::AggregateSignature, ->; - -pub fn verify_signature_sets<'a>( - signature_sets: impl ExactSizeIterator>, -) -> bool { - if signature_sets.len() == 0 { - return false; - } - - signature_sets - .map(|signature_set| { - let mut aggregate = milagro::AggregatePublicKey::from_public_key( - signature_set.signing_keys.first().ok_or(())?.point(), - ); - - for signing_key in signature_set.signing_keys.iter().skip(1) { - aggregate.add(signing_key.point()) - } - - if signature_set.signature.point().is_none() { - return Err(()); - } - - Ok(( - signature_set.signature.as_ref(), - aggregate, - signature_set.message, - )) - }) - .collect::, ()>>() - .map(|aggregates| { - milagro::AggregateSignature::verify_multiple_aggregate_signatures( - &mut rand::thread_rng(), - aggregates.iter().map(|(signature, aggregate, message)| { - ( - signature - .point() - .expect("guarded against none by previous check"), - aggregate, - message.as_bytes(), - ) - }), - ) - }) - .unwrap_or(false) -} - -impl TPublicKey for milagro::PublicKey { - fn serialize(&self) -> [u8; PUBLIC_KEY_BYTES_LEN] { - let mut bytes = [0; PUBLIC_KEY_BYTES_LEN]; - bytes[..].copy_from_slice(&self.as_bytes()); - bytes - } - - fn deserialize(bytes: &[u8]) -> Result { - Self::from_bytes(bytes).map_err(Into::into) - } -} - -impl TAggregatePublicKey for milagro::AggregatePublicKey { - fn to_public_key(&self) -> GenericPublicKey { - GenericPublicKey::from_point(milagro::PublicKey { - point: self.point.clone(), - }) - } - - fn aggregate(pubkeys: &[GenericPublicKey]) -> Result { - let pubkey_refs = pubkeys.iter().map(|pk| pk.point()).collect::>(); - Ok(milagro::AggregatePublicKey::aggregate(&pubkey_refs)?) - } -} - -impl TSignature for milagro::Signature { - fn serialize(&self) -> [u8; SIGNATURE_BYTES_LEN] { - let mut bytes = [0; SIGNATURE_BYTES_LEN]; - - bytes[..].copy_from_slice(&self.as_bytes()); - - bytes - } - - fn deserialize(bytes: &[u8]) -> Result { - milagro::Signature::from_bytes(&bytes).map_err(Error::MilagroError) - } - - fn verify(&self, pubkey: &milagro::PublicKey, msg: Hash256) -> bool { - self.verify(msg.as_bytes(), pubkey) - } -} - -impl TAggregateSignature - for milagro::AggregateSignature -{ - fn infinity() -> Self { - milagro::AggregateSignature::new() - } - - fn add_assign(&mut self, other: &milagro::Signature) { - self.add(other) - } - - fn add_assign_aggregate(&mut self, other: &Self) { - self.add_aggregate(other) - } - - fn serialize(&self) -> [u8; SIGNATURE_BYTES_LEN] { - let mut bytes = [0; SIGNATURE_BYTES_LEN]; - - bytes[..].copy_from_slice(&self.as_bytes()); - - bytes - } - - fn deserialize(bytes: &[u8]) -> Result { - milagro::AggregateSignature::from_bytes(&bytes).map_err(Error::MilagroError) - } - - fn fast_aggregate_verify( - &self, - msg: Hash256, - pubkeys: &[&GenericPublicKey], - ) -> bool { - let pubkeys = pubkeys.iter().map(|pk| pk.point()).collect::>(); - self.fast_aggregate_verify(msg.as_bytes(), &pubkeys) - } - - fn aggregate_verify( - &self, - msgs: &[Hash256], - pubkeys: &[&GenericPublicKey], - ) -> bool { - let pubkeys = pubkeys.iter().map(|pk| pk.point()).collect::>(); - let msgs = msgs.iter().map(|hash| hash.as_bytes()).collect::>(); - self.aggregate_verify(&msgs, &pubkeys) - } -} - -impl TSecretKey for milagro::SecretKey { - fn random() -> Self { - Self::random(&mut thread_rng()) - } - - fn public_key(&self) -> milagro::PublicKey { - let point = milagro::PublicKey::from_secret_key(self).point; - milagro::PublicKey { point } - } - - fn sign(&self, msg: Hash256) -> milagro::Signature { - let point = milagro::Signature::new(msg.as_bytes(), self).point; - milagro::Signature { point } - } - - fn serialize(&self) -> ZeroizeHash { - let mut bytes = [0; SECRET_KEY_BYTES_LEN]; - - // Takes the right-hand 32 bytes from the secret key. - bytes[..].copy_from_slice(&self.as_bytes()); - - bytes.into() - } - - fn deserialize(bytes: &[u8]) -> Result { - Self::from_bytes(bytes).map_err(Into::into) - } -} diff --git a/crypto/bls/src/impls/mod.rs b/crypto/bls/src/impls/mod.rs index b3f2da77b1..d87c3b12ba 100644 --- a/crypto/bls/src/impls/mod.rs +++ b/crypto/bls/src/impls/mod.rs @@ -1,5 +1,3 @@ #[cfg(feature = "supranational")] pub mod blst; pub mod fake_crypto; -#[cfg(feature = "milagro")] -pub mod milagro; diff --git a/crypto/bls/src/lib.rs b/crypto/bls/src/lib.rs index 750e1bd5b8..fef9804b78 100644 --- a/crypto/bls/src/lib.rs +++ b/crypto/bls/src/lib.rs @@ -9,15 +9,13 @@ //! are supported via compile-time flags. There are three backends supported via features: //! //! - `supranational`: the pure-assembly, highly optimized version from the `blst` crate. -//! - `milagro`: the classic pure-Rust `milagro_bls` crate. //! - `fake_crypto`: an always-returns-valid implementation that is only useful for testing //! scenarios which intend to *ignore* real cryptography. //! //! This crate uses traits to reduce code-duplication between the two implementations. For example, //! the `GenericPublicKey` struct exported from this crate is generic across the `TPublicKey` trait //! (i.e., `PublicKey`). `TPublicKey` is implemented by all three backends (see the -//! `impls.rs` module). When compiling with the `milagro` feature, we export -//! `type PublicKey = GenericPublicKey`. +//! `impls.rs` module). #[macro_use] mod macros; @@ -43,16 +41,11 @@ pub use zeroize_hash::ZeroizeHash; #[cfg(feature = "supranational")] use blst::BLST_ERROR as BlstError; -#[cfg(feature = "milagro")] -use milagro_bls::AmclError; pub type Hash256 = ethereum_types::H256; #[derive(Clone, Debug, PartialEq)] pub enum Error { - /// An error was raised from the Milagro BLS library. - #[cfg(feature = "milagro")] - MilagroError(AmclError), /// An error was raised from the Supranational BLST BLS library. #[cfg(feature = "supranational")] BlstError(BlstError), @@ -66,13 +59,6 @@ pub enum Error { InvalidZeroSecretKey, } -#[cfg(feature = "milagro")] -impl From for Error { - fn from(e: AmclError) -> Error { - Error::MilagroError(e) - } -} - #[cfg(feature = "supranational")] impl From for Error { fn from(e: BlstError) -> Error { @@ -94,8 +80,7 @@ pub mod generics { } /// Defines all the fundamental BLS points which should be exported by this crate by making -/// concrete the generic type parameters using the points from some external BLS library (e.g., -/// Milagro, BLST). +/// concrete the generic type parameters using the points from some external BLS library (e.g.,BLST). macro_rules! define_mod { ($name: ident, $mod: path) => { pub mod $name { @@ -139,8 +124,6 @@ macro_rules! define_mod { }; } -#[cfg(feature = "milagro")] -define_mod!(milagro_implementations, crate::impls::milagro::types); #[cfg(feature = "supranational")] define_mod!(blst_implementations, crate::impls::blst::types); #[cfg(feature = "fake_crypto")] @@ -149,14 +132,7 @@ define_mod!( crate::impls::fake_crypto::types ); -#[cfg(all(feature = "milagro", not(feature = "fake_crypto"),))] -pub use milagro_implementations::*; - -#[cfg(all( - feature = "supranational", - not(feature = "fake_crypto"), - not(feature = "milagro") -))] +#[cfg(all(feature = "supranational", not(feature = "fake_crypto"),))] pub use blst_implementations::*; #[cfg(feature = "fake_crypto")] diff --git a/crypto/bls/tests/tests.rs b/crypto/bls/tests/tests.rs index ad498dbfa8..478c1b7dc2 100644 --- a/crypto/bls/tests/tests.rs +++ b/crypto/bls/tests/tests.rs @@ -509,8 +509,3 @@ macro_rules! test_suite { mod blst { test_suite!(blst_implementations); } - -#[cfg(all(feature = "milagro", not(debug_assertions)))] -mod milagro { - test_suite!(milagro_implementations); -} diff --git a/crypto/eth2_key_derivation/src/derived_key.rs b/crypto/eth2_key_derivation/src/derived_key.rs index b3373782ac..21f98796d4 100644 --- a/crypto/eth2_key_derivation/src/derived_key.rs +++ b/crypto/eth2_key_derivation/src/derived_key.rs @@ -2,7 +2,6 @@ use crate::{lamport_secret_key::LamportSecretKey, secret_bytes::SecretBytes, Zer use num_bigint_dig::BigUint; use ring::hkdf::{KeyType, Prk, Salt, HKDF_SHA256}; use sha2::{Digest, Sha256}; -use std::convert::TryFrom; use zeroize::Zeroize; /// The byte size of a SHA256 hash. diff --git a/crypto/eth2_key_derivation/src/lamport_secret_key.rs b/crypto/eth2_key_derivation/src/lamport_secret_key.rs index aa6dbb3932..c0c6eca4f1 100644 --- a/crypto/eth2_key_derivation/src/lamport_secret_key.rs +++ b/crypto/eth2_key_derivation/src/lamport_secret_key.rs @@ -1,5 +1,4 @@ use crate::derived_key::{HASH_SIZE, LAMPORT_ARRAY_SIZE}; -use std::iter::Iterator; use zeroize::Zeroize; /// A Lamport secret key as specified in [EIP-2333](https://eips.ethereum.org/EIPS/eip-2333). diff --git a/crypto/eth2_keystore/src/json_keystore/checksum_module.rs b/crypto/eth2_keystore/src/json_keystore/checksum_module.rs index bbcc418185..dbb21e4de1 100644 --- a/crypto/eth2_keystore/src/json_keystore/checksum_module.rs +++ b/crypto/eth2_keystore/src/json_keystore/checksum_module.rs @@ -6,7 +6,6 @@ use super::hex_bytes::HexBytes; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use std::convert::TryFrom; /// Used for ensuring that serde only decodes valid checksum functions. #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] diff --git a/crypto/eth2_keystore/src/json_keystore/cipher_module.rs b/crypto/eth2_keystore/src/json_keystore/cipher_module.rs index 5300b2f8b2..03a9d305a2 100644 --- a/crypto/eth2_keystore/src/json_keystore/cipher_module.rs +++ b/crypto/eth2_keystore/src/json_keystore/cipher_module.rs @@ -5,7 +5,6 @@ use super::hex_bytes::HexBytes; use serde::{Deserialize, Serialize}; -use std::convert::TryFrom; /// Used for ensuring that serde only decodes valid cipher functions. #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] diff --git a/crypto/eth2_keystore/src/json_keystore/hex_bytes.rs b/crypto/eth2_keystore/src/json_keystore/hex_bytes.rs index 67e156ff43..cc61f13d97 100644 --- a/crypto/eth2_keystore/src/json_keystore/hex_bytes.rs +++ b/crypto/eth2_keystore/src/json_keystore/hex_bytes.rs @@ -1,5 +1,4 @@ use serde::{Deserialize, Serialize}; -use std::convert::TryFrom; /// To allow serde to encode/decode byte arrays from HEX ASCII strings. #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] diff --git a/crypto/eth2_keystore/src/json_keystore/kdf_module.rs b/crypto/eth2_keystore/src/json_keystore/kdf_module.rs index 94aeab0682..a29b895c95 100644 --- a/crypto/eth2_keystore/src/json_keystore/kdf_module.rs +++ b/crypto/eth2_keystore/src/json_keystore/kdf_module.rs @@ -8,7 +8,6 @@ use crate::DKLEN; use hmac::{Hmac, Mac, NewMac}; use serde::{Deserialize, Serialize}; use sha2::Sha256; -use std::convert::TryFrom; /// KDF module representation. #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] diff --git a/crypto/eth2_keystore/src/keystore.rs b/crypto/eth2_keystore/src/keystore.rs index 2049518cd4..304ea3ecd6 100644 --- a/crypto/eth2_keystore/src/keystore.rs +++ b/crypto/eth2_keystore/src/keystore.rs @@ -23,7 +23,6 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::fs::File; use std::io::{Read, Write}; -use std::iter::FromIterator; use std::path::Path; use std::str; use unicode_normalization::UnicodeNormalization; diff --git a/crypto/eth2_wallet/src/json_wallet/mod.rs b/crypto/eth2_wallet/src/json_wallet/mod.rs index 834716fba2..d2092508a2 100644 --- a/crypto/eth2_wallet/src/json_wallet/mod.rs +++ b/crypto/eth2_wallet/src/json_wallet/mod.rs @@ -1,6 +1,5 @@ use serde::{Deserialize, Serialize}; use serde_repr::*; -use std::convert::TryFrom; pub use eth2_keystore::json_keystore::{ Aes128Ctr, ChecksumModule, Cipher, CipherModule, Crypto, EmptyMap, EmptyString, Kdf, KdfModule, diff --git a/crypto/eth2_wallet/src/validator_path.rs b/crypto/eth2_wallet/src/validator_path.rs index 3b4f7738da..db175aa5d2 100644 --- a/crypto/eth2_wallet/src/validator_path.rs +++ b/crypto/eth2_wallet/src/validator_path.rs @@ -1,5 +1,4 @@ use std::fmt; -use std::iter::Iterator; pub const PURPOSE: u32 = 12381; pub const COIN_TYPE: u32 = 3600; diff --git a/crypto/kzg/Cargo.toml b/crypto/kzg/Cargo.toml index 7b70166f92..d26dfe4992 100644 --- a/crypto/kzg/Cargo.toml +++ b/crypto/kzg/Cargo.toml @@ -16,4 +16,4 @@ serde = { workspace = true } ethereum_serde_utils = { workspace = true } hex = { workspace = true } ethereum_hashing = { workspace = true } -c-kzg = { git = "https://github.com/ethereum/c-kzg-4844", rev = "748283cced543c486145d5f3f38684becdfe3e1b"} \ No newline at end of file +c-kzg = { workspace = true } diff --git a/crypto/kzg/src/lib.rs b/crypto/kzg/src/lib.rs index 0e096ba55c..658dc9fe06 100644 --- a/crypto/kzg/src/lib.rs +++ b/crypto/kzg/src/lib.rs @@ -142,3 +142,11 @@ impl Kzg { .map_err(Into::into) } } + +impl TryFrom for Kzg { + type Error = Error; + + fn try_from(trusted_setup: TrustedSetup) -> Result { + Kzg::new_from_trusted_setup(trusted_setup) + } +} diff --git a/database_manager/src/lib.rs b/database_manager/src/lib.rs index 95af4d6382..617192abfe 100644 --- a/database_manager/src/lib.rs +++ b/database_manager/src/lib.rs @@ -77,7 +77,15 @@ pub fn inspect_cli_app<'a, 'b>() -> App<'a, 'b> { Arg::with_name("freezer") .long("freezer") .help("Inspect the freezer DB rather than the hot DB") - .takes_value(false), + .takes_value(false) + .conflicts_with("blobs-db"), + ) + .arg( + Arg::with_name("blobs-db") + .long("blobs-db") + .help("Inspect the blobs DB rather than the hot DB") + .takes_value(false) + .conflicts_with("freezer"), ) .arg( Arg::with_name("output-dir") @@ -88,6 +96,34 @@ pub fn inspect_cli_app<'a, 'b>() -> App<'a, 'b> { ) } +pub fn compact_cli_app<'a, 'b>() -> App<'a, 'b> { + App::new("compact") + .setting(clap::AppSettings::ColoredHelp) + .about("Compact database manually") + .arg( + Arg::with_name("column") + .long("column") + .value_name("TAG") + .help("3-byte column ID (see `DBColumn`)") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("freezer") + .long("freezer") + .help("Inspect the freezer DB rather than the hot DB") + .takes_value(false) + .conflicts_with("blobs-db"), + ) + .arg( + Arg::with_name("blobs-db") + .long("blobs-db") + .help("Inspect the blobs DB rather than the hot DB") + .takes_value(false) + .conflicts_with("freezer"), + ) +} + pub fn prune_payloads_app<'a, 'b>() -> App<'a, 'b> { App::new("prune-payloads") .alias("prune_payloads") @@ -162,6 +198,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .subcommand(migrate_cli_app()) .subcommand(version_cli_app()) .subcommand(inspect_cli_app()) + .subcommand(compact_cli_app()) .subcommand(prune_payloads_app()) .subcommand(prune_blobs_app()) .subcommand(prune_states_app()) @@ -251,6 +288,7 @@ pub struct InspectConfig { skip: Option, limit: Option, freezer: bool, + blobs_db: bool, /// Configures where the inspect output should be stored. output_dir: PathBuf, } @@ -261,6 +299,7 @@ fn parse_inspect_config(cli_args: &ArgMatches) -> Result let skip = clap_utils::parse_optional(cli_args, "skip")?; let limit = clap_utils::parse_optional(cli_args, "limit")?; let freezer = cli_args.is_present("freezer"); + let blobs_db = cli_args.is_present("blobs-db"); let output_dir: PathBuf = clap_utils::parse_optional(cli_args, "output-dir")?.unwrap_or_else(PathBuf::new); @@ -270,6 +309,7 @@ fn parse_inspect_config(cli_args: &ArgMatches) -> Result skip, limit, freezer, + blobs_db, output_dir, }) } @@ -277,32 +317,20 @@ fn parse_inspect_config(cli_args: &ArgMatches) -> Result pub fn inspect_db( inspect_config: InspectConfig, client_config: ClientConfig, - runtime_context: &RuntimeContext, - log: Logger, ) -> 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(); let blobs_path = client_config.get_blobs_db_path(); - let db = HotColdDB::, LevelDB>::open( - &hot_path, - &cold_path, - &blobs_path, - |_, _, _| Ok(()), - client_config.store, - spec, - log, - ) - .map_err(|e| format!("{:?}", e))?; - let mut total = 0; let mut num_keys = 0; let sub_db = if inspect_config.freezer { - &db.cold_db + LevelDB::::open(&cold_path).map_err(|e| format!("Unable to open freezer DB: {e:?}"))? + } else if inspect_config.blobs_db { + LevelDB::::open(&blobs_path).map_err(|e| format!("Unable to open blobs DB: {e:?}"))? } else { - &db.hot_db + LevelDB::::open(&hot_path).map_err(|e| format!("Unable to open hot DB: {e:?}"))? }; let skip = inspect_config.skip.unwrap_or(0); @@ -356,6 +384,7 @@ pub fn inspect_db( let write_result = fs::OpenOptions::new() .create(true) + .truncate(true) .write(true) .open(&file_path) .map_err(|e| format!("Failed to open file: {:?}", e)) @@ -384,6 +413,50 @@ pub fn inspect_db( Ok(()) } +pub struct CompactConfig { + column: DBColumn, + freezer: bool, + blobs_db: bool, +} + +fn parse_compact_config(cli_args: &ArgMatches) -> Result { + let column = clap_utils::parse_required(cli_args, "column")?; + let freezer = cli_args.is_present("freezer"); + let blobs_db = cli_args.is_present("blobs-db"); + Ok(CompactConfig { + column, + freezer, + blobs_db, + }) +} + +pub fn compact_db( + compact_config: CompactConfig, + client_config: ClientConfig, + log: Logger, +) -> Result<(), Error> { + let hot_path = client_config.get_db_path(); + let cold_path = client_config.get_freezer_db_path(); + let blobs_path = client_config.get_blobs_db_path(); + let column = compact_config.column; + + let (sub_db, db_name) = if compact_config.freezer { + (LevelDB::::open(&cold_path)?, "freezer_db") + } else if compact_config.blobs_db { + (LevelDB::::open(&blobs_path)?, "blobs_db") + } else { + (LevelDB::::open(&hot_path)?, "hot_db") + }; + info!( + log, + "Compacting database"; + "db" => db_name, + "column" => ?column + ); + sub_db.compact_column(column)?; + Ok(()) +} + pub struct MigrateConfig { to: SchemaVersion, } @@ -537,7 +610,10 @@ pub fn prune_states( // Check that the user has confirmed they want to proceed. if !prune_config.confirm { match db.get_anchor_info() { - Some(anchor_info) if anchor_info.state_upper_limit == STATE_UPPER_LIMIT_NO_RETAIN => { + Some(anchor_info) + if anchor_info.state_lower_limit == 0 + && anchor_info.state_upper_limit == STATE_UPPER_LIMIT_NO_RETAIN => + { info!(log, "States have already been pruned"); return Ok(()); } @@ -569,7 +645,7 @@ pub fn prune_states( } /// Run the database manager, returning an error string if the operation did not succeed. -pub fn run(cli_args: &ArgMatches<'_>, env: Environment) -> Result<(), String> { +pub fn run(cli_args: &ArgMatches<'_>, env: Environment) -> Result<(), String> { let client_config = parse_client_config(cli_args, &env)?; let context = env.core_context(); let log = context.log().clone(); @@ -585,7 +661,11 @@ pub fn run(cli_args: &ArgMatches<'_>, env: Environment) -> Result } ("inspect", Some(cli_args)) => { let inspect_config = parse_inspect_config(cli_args)?; - inspect_db(inspect_config, client_config, &context, log) + inspect_db::(inspect_config, client_config) + } + ("compact", Some(cli_args)) => { + let compact_config = parse_compact_config(cli_args)?; + compact_db::(compact_config, client_config, log).map_err(format_err) } ("prune-payloads", Some(_)) => { prune_payloads(client_config, &context, log).map_err(format_err) @@ -600,7 +680,7 @@ pub fn run(cli_args: &ArgMatches<'_>, env: Environment) -> Result let genesis_state = executor .block_on_dangerous( - network_config.genesis_state::( + network_config.genesis_state::( client_config.genesis_state_url.as_deref(), client_config.genesis_state_url_timeout, &log, diff --git a/lcli/Cargo.toml b/lcli/Cargo.toml index 1bba8242c5..2aba106e50 100644 --- a/lcli/Cargo.toml +++ b/lcli/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "lcli" description = "Lighthouse CLI (modeled after zcli)" -version = "4.6.0" +version = "5.1.3" authors = ["Paul Hauner "] edition = { workspace = true } diff --git a/lcli/Dockerfile b/lcli/Dockerfile index aed3628cf3..4f5c3f2972 100644 --- a/lcli/Dockerfile +++ b/lcli/Dockerfile @@ -1,7 +1,7 @@ # `lcli` requires the full project to be in scope, so this should be built either: # - from the `lighthouse` dir with the command: `docker build -f ./lcli/Dockerflie .` # - from the current directory with the command: `docker build -f ./Dockerfile ../` -FROM rust:1.73.0-bullseye AS builder +FROM rust:1.75.0-bullseye AS builder RUN apt-get update && apt-get -y upgrade && apt-get install -y cmake libclang-dev COPY . lighthouse ARG FEATURES diff --git a/lcli/src/block_root.rs b/lcli/src/block_root.rs index a4237d855b..0ee304c8a5 100644 --- a/lcli/src/block_root.rs +++ b/lcli/src/block_root.rs @@ -38,12 +38,12 @@ use types::{EthSpec, FullPayload, SignedBeaconBlock}; const HTTP_TIMEOUT: Duration = Duration::from_secs(5); -pub fn run( - env: Environment, +pub fn run( + env: Environment, network_config: Eth2NetworkConfig, matches: &ArgMatches, ) -> Result<(), String> { - let spec = &network_config.chain_spec::()?; + let spec = &network_config.chain_spec::()?; let executor = env.core_context().executor; /* @@ -54,14 +54,14 @@ pub fn run( let beacon_url: Option = parse_optional(matches, "beacon-url")?; let runs: usize = parse_required(matches, "runs")?; - info!("Using {} spec", T::spec_name()); + info!("Using {} spec", E::spec_name()); info!("Doing {} runs", runs); /* * Load the block and pre-state from disk or beaconAPI URL. */ - let block: SignedBeaconBlock> = match (block_path, beacon_url) { + let block: SignedBeaconBlock> = match (block_path, beacon_url) { (Some(block_path), None) => { info!("Block path: {:?}", block_path); load_from_ssz_with(&block_path, spec, SignedBeaconBlock::from_ssz_bytes)? diff --git a/lcli/src/change_genesis_time.rs b/lcli/src/change_genesis_time.rs index 6b7b812e87..f75652c768 100644 --- a/lcli/src/change_genesis_time.rs +++ b/lcli/src/change_genesis_time.rs @@ -6,7 +6,7 @@ use std::io::{Read, Write}; use std::path::PathBuf; use types::{BeaconState, EthSpec}; -pub fn run(testnet_dir: PathBuf, matches: &ArgMatches) -> Result<(), String> { +pub fn run(testnet_dir: PathBuf, matches: &ArgMatches) -> Result<(), String> { let path = matches .value_of("ssz-state") .ok_or("ssz-state not specified")? @@ -20,9 +20,9 @@ pub fn run(testnet_dir: PathBuf, matches: &ArgMatches) -> Result<(), .map_err(|e| format!("Unable to parse genesis-time: {}", e))?; let eth2_network_config = Eth2NetworkConfig::load(testnet_dir)?; - let spec = ð2_network_config.chain_spec::()?; + let spec = ð2_network_config.chain_spec::()?; - let mut state: BeaconState = { + let mut state: BeaconState = { let mut file = File::open(&path).map_err(|e| format!("Unable to open file: {}", e))?; let mut ssz = vec![]; diff --git a/lcli/src/create_payload_header.rs b/lcli/src/create_payload_header.rs index 5c96035851..7aa1ef7008 100644 --- a/lcli/src/create_payload_header.rs +++ b/lcli/src/create_payload_header.rs @@ -6,10 +6,10 @@ use std::io::Write; use std::time::{SystemTime, UNIX_EPOCH}; use types::{ EthSpec, ExecutionPayloadHeader, ExecutionPayloadHeaderCapella, ExecutionPayloadHeaderDeneb, - ExecutionPayloadHeaderMerge, ForkName, + ExecutionPayloadHeaderElectra, ExecutionPayloadHeaderMerge, ForkName, }; -pub fn run(matches: &ArgMatches) -> Result<(), String> { +pub fn run(matches: &ArgMatches) -> Result<(), String> { let eth1_block_hash = parse_required(matches, "execution-block-hash")?; let genesis_time = parse_optional(matches, "genesis-time")?.unwrap_or( SystemTime::now() @@ -22,7 +22,7 @@ pub fn run(matches: &ArgMatches) -> Result<(), String> { let file_name = matches.value_of("file").ok_or("No file supplied")?; let fork_name: ForkName = parse_optional(matches, "fork")?.unwrap_or(ForkName::Merge); - let execution_payload_header: ExecutionPayloadHeader = match fork_name { + let execution_payload_header: ExecutionPayloadHeader = match fork_name { ForkName::Base | ForkName::Altair => return Err("invalid fork name".to_string()), ForkName::Merge => ExecutionPayloadHeader::Merge(ExecutionPayloadHeaderMerge { gas_limit, @@ -48,6 +48,14 @@ pub fn run(matches: &ArgMatches) -> Result<(), String> { prev_randao: eth1_block_hash.into_root(), ..ExecutionPayloadHeaderDeneb::default() }), + ForkName::Electra => ExecutionPayloadHeader::Electra(ExecutionPayloadHeaderElectra { + gas_limit, + base_fee_per_gas, + timestamp: genesis_time, + block_hash: eth1_block_hash, + prev_randao: eth1_block_hash.into_root(), + ..ExecutionPayloadHeaderElectra::default() + }), }; let mut file = File::create(file_name).map_err(|_| "Unable to create file".to_string())?; diff --git a/lcli/src/deploy_deposit_contract.rs b/lcli/src/deploy_deposit_contract.rs index 8919ebdaf5..b920486c84 100644 --- a/lcli/src/deploy_deposit_contract.rs +++ b/lcli/src/deploy_deposit_contract.rs @@ -4,7 +4,7 @@ use types::EthSpec; use eth1_test_rig::{Http, Provider}; -pub fn run(env: Environment, matches: &ArgMatches<'_>) -> Result<(), String> { +pub fn run(env: Environment, matches: &ArgMatches<'_>) -> Result<(), String> { let eth1_http: String = clap_utils::parse_required(matches, "eth1-http")?; let confirmations: usize = clap_utils::parse_required(matches, "confirmations")?; let validator_count: Option = clap_utils::parse_optional(matches, "validator-count")?; @@ -24,7 +24,7 @@ pub fn run(env: Environment, matches: &ArgMatches<'_>) -> Result< let amount = env.eth2_config.spec.max_effective_balance; for i in 0..validator_count { println!("Submitting deposit for validator {}...", i); - contract.deposit_deterministic_async::(i, amount).await?; + contract.deposit_deterministic_async::(i, amount).await?; } } Ok(()) diff --git a/lcli/src/eth1_genesis.rs b/lcli/src/eth1_genesis.rs index bddd4baad8..635a36ef70 100644 --- a/lcli/src/eth1_genesis.rs +++ b/lcli/src/eth1_genesis.rs @@ -12,8 +12,8 @@ use types::EthSpec; /// Interval between polling the eth1 node for genesis information. pub const ETH1_GENESIS_UPDATE_INTERVAL: Duration = Duration::from_millis(7_000); -pub fn run( - env: Environment, +pub fn run( + env: Environment, testnet_dir: PathBuf, matches: &ArgMatches<'_>, ) -> Result<(), String> { @@ -27,7 +27,7 @@ pub fn run( let mut eth2_network_config = Eth2NetworkConfig::load(testnet_dir.clone())?; - let spec = eth2_network_config.chain_spec::()?; + let spec = eth2_network_config.chain_spec::()?; let mut config = Eth1Config::default(); if let Some(v) = endpoints.clone() { @@ -46,7 +46,7 @@ pub fn run( env.runtime().block_on(async { let _ = genesis_service - .wait_for_genesis_state::(ETH1_GENESIS_UPDATE_INTERVAL, spec) + .wait_for_genesis_state::(ETH1_GENESIS_UPDATE_INTERVAL, spec) .await .map(move |genesis_state| { eth2_network_config.genesis_state_bytes = Some(genesis_state.as_ssz_bytes().into()); diff --git a/lcli/src/generate_bootnode_enr.rs b/lcli/src/generate_bootnode_enr.rs index 1d41bedc88..52960b929d 100644 --- a/lcli/src/generate_bootnode_enr.rs +++ b/lcli/src/generate_bootnode_enr.rs @@ -10,7 +10,7 @@ use std::{fs, net::Ipv4Addr}; use std::{fs::File, num::NonZeroU16}; use types::{ChainSpec, EnrForkId, Epoch, EthSpec, Hash256}; -pub fn run(matches: &ArgMatches) -> Result<(), String> { +pub fn run(matches: &ArgMatches) -> Result<(), String> { let ip: Ipv4Addr = clap_utils::parse_required(matches, "ip")?; let udp_port: NonZeroU16 = clap_utils::parse_required(matches, "udp-port")?; let tcp_port: NonZeroU16 = clap_utils::parse_required(matches, "tcp-port")?; @@ -37,7 +37,7 @@ pub fn run(matches: &ArgMatches) -> Result<(), String> { next_fork_version: genesis_fork_version, next_fork_epoch: Epoch::max_value(), // FAR_FUTURE_EPOCH }; - let enr = build_enr::(&enr_key, &config, &enr_fork_id) + let enr = build_enr::(&enr_key, &config, &enr_fork_id) .map_err(|e| format!("Unable to create ENR: {:?}", e))?; fs::create_dir_all(&output_dir).map_err(|e| format!("Unable to create output-dir: {:?}", e))?; diff --git a/lcli/src/indexed_attestations.rs b/lcli/src/indexed_attestations.rs index 6e3bfa51d3..63f8cd9463 100644 --- a/lcli/src/indexed_attestations.rs +++ b/lcli/src/indexed_attestations.rs @@ -15,19 +15,19 @@ fn read_file_bytes(filename: &Path) -> Result, String> { Ok(bytes) } -pub fn run(matches: &ArgMatches) -> Result<(), String> { - let spec = &T::default_spec(); +pub fn run(matches: &ArgMatches) -> Result<(), String> { + let spec = &E::default_spec(); let state_file: PathBuf = parse_required(matches, "state")?; let attestations_file: PathBuf = parse_required(matches, "attestations")?; - let mut state = BeaconState::::from_ssz_bytes(&read_file_bytes(&state_file)?, spec) + let mut state = BeaconState::::from_ssz_bytes(&read_file_bytes(&state_file)?, spec) .map_err(|e| format!("Invalid state: {:?}", e))?; state .build_all_committee_caches(spec) .map_err(|e| format!("{:?}", e))?; - let attestations: Vec> = + let attestations: Vec> = serde_json::from_slice(&read_file_bytes(&attestations_file)?) .map_err(|e| format!("Invalid attestation list: {:?}", e))?; diff --git a/lcli/src/interop_genesis.rs b/lcli/src/interop_genesis.rs index 1a0b81fcb7..f44edffd46 100644 --- a/lcli/src/interop_genesis.rs +++ b/lcli/src/interop_genesis.rs @@ -7,7 +7,7 @@ use std::path::PathBuf; use std::time::{SystemTime, UNIX_EPOCH}; use types::{test_utils::generate_deterministic_keypairs, EthSpec, Hash256}; -pub fn run(testnet_dir: PathBuf, matches: &ArgMatches) -> Result<(), String> { +pub fn run(testnet_dir: PathBuf, matches: &ArgMatches) -> Result<(), String> { let validator_count = matches .value_of("validator-count") .ok_or("validator-count not specified")? @@ -27,14 +27,14 @@ pub fn run(testnet_dir: PathBuf, matches: &ArgMatches) -> Result<(), let mut eth2_network_config = Eth2NetworkConfig::load(testnet_dir.clone())?; - let mut spec = eth2_network_config.chain_spec::()?; + let mut spec = eth2_network_config.chain_spec::()?; if let Some(v) = parse_ssz_optional(matches, "genesis-fork-version")? { spec.genesis_fork_version = v; } let keypairs = generate_deterministic_keypairs(validator_count); - let genesis_state = interop_genesis_state::( + let genesis_state = interop_genesis_state::( &keypairs, genesis_time, Hash256::from_slice(DEFAULT_ETH1_BLOCK_HASH), diff --git a/lcli/src/main.rs b/lcli/src/main.rs index 17fafe6ec1..c374a8f4b3 100644 --- a/lcli/src/main.rs +++ b/lcli/src/main.rs @@ -433,7 +433,7 @@ fn main() { .takes_value(true) .default_value("bellatrix") .help("The fork for which the execution payload header should be created.") - .possible_values(&["merge", "bellatrix", "capella", "deneb"]) + .possible_values(&["merge", "bellatrix", "capella", "deneb", "electra"]) ) ) .subcommand( @@ -597,7 +597,7 @@ fn main() { .value_name("EPOCH") .takes_value(true) .help( - "The epoch at which to enable the Merge hard fork", + "The epoch at which to enable the Bellatrix hard fork", ), ) .arg( @@ -615,7 +615,16 @@ fn main() { .value_name("EPOCH") .takes_value(true) .help( - "The epoch at which to enable the deneb hard fork", + "The epoch at which to enable the Deneb hard fork", + ), + ) + .arg( + Arg::with_name("electra-fork-epoch") + .long("electra-fork-epoch") + .value_name("EPOCH") + .takes_value(true) + .help( + "The epoch at which to enable the Electra hard fork", ), ) .arg( @@ -946,6 +955,14 @@ fn main() { .help("The payload timestamp that enables Cancun. No default is provided \ until Cancun is triggered on mainnet.") ) + .arg( + Arg::with_name("prague-time") + .long("prague-time") + .value_name("UNIX_TIMESTAMP") + .takes_value(true) + .help("The payload timestamp that enables Prague. No default is provided \ + until Prague is triggered on mainnet.") + ) ) .get_matches(); @@ -968,8 +985,8 @@ fn main() { } } -fn run( - env_builder: EnvironmentBuilder, +fn run( + env_builder: EnvironmentBuilder, matches: &ArgMatches<'_>, ) -> Result<(), String> { let env = env_builder @@ -1024,71 +1041,71 @@ fn run( match matches.subcommand() { ("transition-blocks", Some(matches)) => { let network_config = get_network_config()?; - transition_blocks::run::(env, network_config, matches) + transition_blocks::run::(env, network_config, matches) .map_err(|e| format!("Failed to transition blocks: {}", e)) } ("skip-slots", Some(matches)) => { let network_config = get_network_config()?; - skip_slots::run::(env, network_config, matches) + skip_slots::run::(env, network_config, matches) .map_err(|e| format!("Failed to skip slots: {}", e)) } ("pretty-ssz", Some(matches)) => { let network_config = get_network_config()?; - run_parse_ssz::(network_config, matches) + run_parse_ssz::(network_config, matches) .map_err(|e| format!("Failed to pretty print hex: {}", e)) } ("deploy-deposit-contract", Some(matches)) => { - deploy_deposit_contract::run::(env, matches) + deploy_deposit_contract::run::(env, matches) .map_err(|e| format!("Failed to run deploy-deposit-contract command: {}", e)) } ("eth1-genesis", Some(matches)) => { let testnet_dir = get_testnet_dir()?; - eth1_genesis::run::(env, testnet_dir, matches) + eth1_genesis::run::(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::(testnet_dir, matches) + interop_genesis::run::(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::(testnet_dir, matches) + change_genesis_time::run::(testnet_dir, matches) .map_err(|e| format!("Failed to run change-genesis-time command: {}", e)) } - ("create-payload-header", Some(matches)) => create_payload_header::run::(matches) + ("create-payload-header", Some(matches)) => create_payload_header::run::(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::(testnet_dir, matches) + replace_state_pubkeys::run::(testnet_dir, matches) .map_err(|e| format!("Failed to run replace-state-pubkeys command: {}", e)) } ("new-testnet", Some(matches)) => { let testnet_dir = get_testnet_dir()?; - new_testnet::run::(testnet_dir, matches) + new_testnet::run::(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::(matches) + ("generate-bootnode-enr", Some(matches)) => generate_bootnode_enr::run::(matches) .map_err(|e| format!("Failed to run generate-bootnode-enr command: {}", e)), ("insecure-validators", Some(matches)) => insecure_validators::run(matches) .map_err(|e| format!("Failed to run insecure-validators command: {}", e)), ("mnemonic-validators", Some(matches)) => mnemonic_validators::run(matches) .map_err(|e| format!("Failed to run mnemonic-validators command: {}", e)), - ("indexed-attestations", Some(matches)) => indexed_attestations::run::(matches) + ("indexed-attestations", Some(matches)) => indexed_attestations::run::(matches) .map_err(|e| format!("Failed to run indexed-attestations command: {}", e)), ("block-root", Some(matches)) => { let network_config = get_network_config()?; - block_root::run::(env, network_config, matches) + block_root::run::(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::(env, network_config, matches) + state_root::run::(env, network_config, matches) .map_err(|e| format!("Failed to run state-root command: {}", e)) } - ("mock-el", Some(matches)) => mock_el::run::(env, matches) + ("mock-el", Some(matches)) => mock_el::run::(env, matches) .map_err(|e| format!("Failed to run mock-el command: {}", e)), (other, _) => Err(format!("Unknown subcommand {}. See --help.", other)), } diff --git a/lcli/src/mock_el.rs b/lcli/src/mock_el.rs index 094e23c3b4..8d3220b1df 100644 --- a/lcli/src/mock_el.rs +++ b/lcli/src/mock_el.rs @@ -11,16 +11,17 @@ use std::net::Ipv4Addr; use std::path::PathBuf; use types::*; -pub fn run(mut env: Environment, matches: &ArgMatches) -> Result<(), String> { +pub fn run(mut env: Environment, matches: &ArgMatches) -> Result<(), String> { let jwt_path: PathBuf = parse_required(matches, "jwt-output-path")?; let listen_addr: Ipv4Addr = parse_required(matches, "listen-address")?; let listen_port: u16 = parse_required(matches, "listen-port")?; let all_payloads_valid: bool = parse_required(matches, "all-payloads-valid")?; let shanghai_time = parse_required(matches, "shanghai-time")?; let cancun_time = parse_optional(matches, "cancun-time")?; + let prague_time = parse_optional(matches, "prague-time")?; let handle = env.core_context().executor.handle().unwrap(); - let spec = &T::default_spec(); + let spec = &E::default_spec(); let jwt_key = JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap(); std::fs::write(jwt_path, hex::encode(DEFAULT_JWT_SECRET)).unwrap(); @@ -35,9 +36,10 @@ pub fn run(mut env: Environment, matches: &ArgMatches) -> Result< terminal_block_hash: spec.terminal_block_hash, shanghai_time: Some(shanghai_time), cancun_time, + prague_time, }; let kzg = None; - let server: MockServer = MockServer::new_with_config(&handle, config, kzg); + let server: MockServer = MockServer::new_with_config(&handle, config, kzg); if all_payloads_valid { eprintln!( diff --git a/lcli/src/new_testnet.rs b/lcli/src/new_testnet.rs index 3a0c7a9f60..f9da3d2b3e 100644 --- a/lcli/src/new_testnet.rs +++ b/lcli/src/new_testnet.rs @@ -9,7 +9,10 @@ use ethereum_hashing::hash; use ssz::Decode; use ssz::Encode; use state_processing::process_activations; -use state_processing::upgrade::{upgrade_to_altair, upgrade_to_bellatrix}; +use state_processing::upgrade::{ + upgrade_to_altair, upgrade_to_bellatrix, upgrade_to_capella, upgrade_to_deneb, + upgrade_to_electra, +}; use std::fs::File; use std::io::Read; use std::path::PathBuf; @@ -19,11 +22,11 @@ use types::ExecutionBlockHash; use types::{ test_utils::generate_deterministic_keypairs, Address, BeaconState, ChainSpec, Config, Epoch, Eth1Data, EthSpec, ExecutionPayloadHeader, ExecutionPayloadHeaderCapella, - ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderMerge, ExecutionPayloadHeaderRefMut, + ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderElectra, ExecutionPayloadHeaderMerge, ForkName, Hash256, Keypair, PublicKey, Validator, }; -pub fn run(testnet_dir_path: PathBuf, matches: &ArgMatches) -> Result<(), String> { +pub fn run(testnet_dir_path: PathBuf, matches: &ArgMatches) -> Result<(), String> { let deposit_contract_address: Address = parse_required(matches, "deposit-contract-address")?; let deposit_contract_deploy_block = parse_required(matches, "deposit-contract-deploy-block")?; @@ -36,7 +39,7 @@ pub fn run(testnet_dir_path: PathBuf, matches: &ArgMatches) -> Resul )); } - let mut spec = T::default_spec(); + let mut spec = E::default_spec(); // Update the spec value if the flag was defined. Otherwise, leave it as the default. macro_rules! maybe_update { @@ -89,12 +92,16 @@ pub fn run(testnet_dir_path: PathBuf, matches: &ArgMatches) -> Resul spec.deneb_fork_epoch = Some(fork_epoch); } + if let Some(fork_epoch) = parse_optional(matches, "electra-fork-epoch")? { + spec.electra_fork_epoch = Some(fork_epoch); + } + if let Some(ttd) = parse_optional(matches, "ttd")? { spec.terminal_total_difficulty = ttd; } let validator_count = parse_required(matches, "validator-count")?; - let execution_payload_header: Option> = + let execution_payload_header: Option> = parse_optional(matches, "execution-payload-header")? .map(|filename: String| { let mut bytes = vec![]; @@ -108,17 +115,21 @@ pub fn run(testnet_dir_path: PathBuf, matches: &ArgMatches) -> Resul "genesis fork must be post-merge".to_string(), )), ForkName::Merge => { - ExecutionPayloadHeaderMerge::::from_ssz_bytes(bytes.as_slice()) + ExecutionPayloadHeaderMerge::::from_ssz_bytes(bytes.as_slice()) .map(ExecutionPayloadHeader::Merge) } ForkName::Capella => { - ExecutionPayloadHeaderCapella::::from_ssz_bytes(bytes.as_slice()) + ExecutionPayloadHeaderCapella::::from_ssz_bytes(bytes.as_slice()) .map(ExecutionPayloadHeader::Capella) } ForkName::Deneb => { - ExecutionPayloadHeaderDeneb::::from_ssz_bytes(bytes.as_slice()) + ExecutionPayloadHeaderDeneb::::from_ssz_bytes(bytes.as_slice()) .map(ExecutionPayloadHeader::Deneb) } + ForkName::Electra => { + ExecutionPayloadHeaderElectra::::from_ssz_bytes(bytes.as_slice()) + .map(ExecutionPayloadHeader::Electra) + } } .map_err(|e| format!("SSZ decode failed: {:?}", e)) }) @@ -147,7 +158,7 @@ pub fn run(testnet_dir_path: PathBuf, matches: &ArgMatches) -> Resul let keypairs = generate_deterministic_keypairs(validator_count); let keypairs: Vec<_> = keypairs.into_iter().map(|kp| (kp.clone(), kp)).collect(); - let genesis_state = initialize_state_with_validators::( + let genesis_state = initialize_state_with_validators::( &keypairs, genesis_time, eth1_block_hash.into_root(), @@ -183,7 +194,7 @@ pub fn run(testnet_dir_path: PathBuf, matches: &ArgMatches) -> Resul (voting_keypair, withdrawal_keypair) }) .collect::>(); - let genesis_state = initialize_state_with_validators::( + let genesis_state = initialize_state_with_validators::( &keypairs, genesis_time, eth1_block_hash.into_root(), @@ -210,7 +221,7 @@ pub fn run(testnet_dir_path: PathBuf, matches: &ArgMatches) -> Resul boot_enr: Some(vec![]), genesis_state_bytes: genesis_state_bytes.map(Into::into), genesis_state_source: GenesisStateSource::IncludedBytes, - config: Config::from_chain_spec::(&spec), + config: Config::from_chain_spec::(&spec), kzg_trusted_setup, }; @@ -226,15 +237,15 @@ pub fn run(testnet_dir_path: PathBuf, matches: &ArgMatches) -> Resul /// /// We need to ensure that `eth1_block_hash` is equal to the genesis block hash that is /// generated from the execution side `genesis.json`. -fn initialize_state_with_validators( +fn initialize_state_with_validators( keypairs: &[(Keypair, Keypair)], // Voting and Withdrawal keypairs genesis_time: u64, eth1_block_hash: Hash256, - execution_payload_header: Option>, + execution_payload_header: Option>, spec: &ChainSpec, -) -> Result, String> { +) -> Result, String> { // If no header is provided, then start from a Bellatrix state by default - let default_header: ExecutionPayloadHeader = + let default_header: ExecutionPayloadHeader = ExecutionPayloadHeader::Merge(ExecutionPayloadHeaderMerge { block_hash: ExecutionBlockHash::from_root(eth1_block_hash), parent_hash: ExecutionBlockHash::zero(), @@ -284,17 +295,17 @@ fn initialize_state_with_validators( if spec .altair_fork_epoch - .map_or(false, |fork_epoch| fork_epoch == T::genesis_epoch()) + .map_or(false, |fork_epoch| fork_epoch == E::genesis_epoch()) { upgrade_to_altair(&mut state, spec).unwrap(); state.fork_mut().previous_version = spec.altair_fork_version; } - // Similarly, perform an upgrade to the merge if configured from genesis. + // Similarly, perform an upgrade to Bellatrix if configured from genesis. if spec .bellatrix_fork_epoch - .map_or(false, |fork_epoch| fork_epoch == T::genesis_epoch()) + .map_or(false, |fork_epoch| fork_epoch == E::genesis_epoch()) { upgrade_to_bellatrix(&mut state, spec).unwrap(); @@ -302,26 +313,68 @@ fn initialize_state_with_validators( state.fork_mut().previous_version = spec.bellatrix_fork_version; // Override latest execution payload header. - // See https://github.com/ethereum/consensus-specs/blob/v1.1.0/specs/merge/beacon-chain.md#testing + // See https://github.com/ethereum/consensus-specs/blob/v1.1.0/specs/bellatrix/beacon-chain.md#testing + if let ExecutionPayloadHeader::Merge(ref header) = execution_payload_header { + *state + .latest_execution_payload_header_merge_mut() + .or(Err("mismatched fork".to_string()))? = header.clone(); + } + } - // Currently, we only support starting from a bellatrix state - match state - .latest_execution_payload_header_mut() - .map_err(|e| format!("Failed to get execution payload header: {:?}", e))? - { - ExecutionPayloadHeaderRefMut::Merge(header_mut) => { - if let ExecutionPayloadHeader::Merge(eph) = execution_payload_header { - *header_mut = eph; - } else { - return Err("Execution payload header must be a bellatrix header".to_string()); - } - } - ExecutionPayloadHeaderRefMut::Capella(_) => { - return Err("Cannot start genesis from a capella state".to_string()) - } - ExecutionPayloadHeaderRefMut::Deneb(_) => { - return Err("Cannot start genesis from a deneb state".to_string()) - } + // Similarly, perform an upgrade to Capella if configured from genesis. + if spec + .capella_fork_epoch + .map_or(false, |fork_epoch| fork_epoch == E::genesis_epoch()) + { + upgrade_to_capella(&mut state, spec).unwrap(); + + // Remove intermediate Bellatrix fork from `state.fork`. + state.fork_mut().previous_version = spec.capella_fork_version; + + // Override latest execution payload header. + // See https://github.com/ethereum/consensus-specs/blob/v1.1.0/specs/bellatrix/beacon-chain.md#testing + if let ExecutionPayloadHeader::Capella(ref header) = execution_payload_header { + *state + .latest_execution_payload_header_capella_mut() + .or(Err("mismatched fork".to_string()))? = header.clone(); + } + } + + // Similarly, perform an upgrade to Deneb if configured from genesis. + if spec + .deneb_fork_epoch + .map_or(false, |fork_epoch| fork_epoch == E::genesis_epoch()) + { + upgrade_to_deneb(&mut state, spec).unwrap(); + + // Remove intermediate Capella fork from `state.fork`. + state.fork_mut().previous_version = spec.deneb_fork_version; + + // Override latest execution payload header. + // See https://github.com/ethereum/consensus-specs/blob/v1.1.0/specs/bellatrix/beacon-chain.md#testing + if let ExecutionPayloadHeader::Deneb(ref header) = execution_payload_header { + *state + .latest_execution_payload_header_deneb_mut() + .or(Err("mismatched fork".to_string()))? = header.clone(); + } + } + + // Similarly, perform an upgrade to Electra if configured from genesis. + if spec + .electra_fork_epoch + .map_or(false, |fork_epoch| fork_epoch == E::genesis_epoch()) + { + upgrade_to_electra(&mut state, spec).unwrap(); + + // Remove intermediate Deneb fork from `state.fork`. + state.fork_mut().previous_version = spec.electra_fork_version; + + // Override latest execution payload header. + // See https://github.com/ethereum/consensus-specs/blob/v1.1.0/specs/bellatrix/beacon-chain.md#testing + if let ExecutionPayloadHeader::Electra(ref header) = execution_payload_header { + *state + .latest_execution_payload_header_electra_mut() + .or(Err("mismatched fork".to_string()))? = header.clone(); } } @@ -331,5 +384,10 @@ fn initialize_state_with_validators( // Set genesis validators root for domain separation and chain versioning *state.genesis_validators_root_mut() = state.update_validators_tree_hash_cache().unwrap(); + // Sanity check for state fork matching config fork. + state + .fork_name(spec) + .map_err(|e| format!("state fork mismatch: {e:?}"))?; + Ok(state) } diff --git a/lcli/src/parse_ssz.rs b/lcli/src/parse_ssz.rs index 453169cdc5..5b34fedfb9 100644 --- a/lcli/src/parse_ssz.rs +++ b/lcli/src/parse_ssz.rs @@ -27,7 +27,7 @@ impl FromStr for OutputFormat { } } -pub fn run_parse_ssz( +pub fn run_parse_ssz( network_config: Eth2NetworkConfig, matches: &ArgMatches, ) -> Result<(), String> { @@ -48,60 +48,68 @@ pub fn run_parse_ssz( bytes }; - let spec = &network_config.chain_spec::()?; + let spec = &network_config.chain_spec::()?; info!( "Using {} network config ({} preset)", spec.config_name.as_deref().unwrap_or("unknown"), - T::spec_name() + E::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 { - "SignedBeaconBlock" => decode_and_print::>( + "SignedBeaconBlock" => decode_and_print::>( &bytes, |bytes| SignedBeaconBlock::from_ssz_bytes(bytes, spec), format, )?, "SignedBeaconBlockBase" | "SignedBeaconBlockPhase0" => { - decode_and_print(&bytes, SignedBeaconBlockBase::::from_ssz_bytes, format)? + decode_and_print(&bytes, SignedBeaconBlockBase::::from_ssz_bytes, format)? } "SignedBeaconBlockAltair" => { - decode_and_print(&bytes, SignedBeaconBlockAltair::::from_ssz_bytes, format)? + decode_and_print(&bytes, SignedBeaconBlockAltair::::from_ssz_bytes, format)? } "SignedBeaconBlockMerge" | "SignedBeaconBlockBellatrix" => { - decode_and_print(&bytes, SignedBeaconBlockMerge::::from_ssz_bytes, format)? + decode_and_print(&bytes, SignedBeaconBlockMerge::::from_ssz_bytes, format)? } "SignedBeaconBlockCapella" => decode_and_print( &bytes, - SignedBeaconBlockCapella::::from_ssz_bytes, + SignedBeaconBlockCapella::::from_ssz_bytes, format, )?, "SignedBeaconBlockDeneb" => { - decode_and_print(&bytes, SignedBeaconBlockDeneb::::from_ssz_bytes, format)? + decode_and_print(&bytes, SignedBeaconBlockDeneb::::from_ssz_bytes, format)? } - "BeaconState" => decode_and_print::>( + "SignedBeaconBlockElectra" => decode_and_print( + &bytes, + SignedBeaconBlockElectra::::from_ssz_bytes, + format, + )?, + "BeaconState" => decode_and_print::>( &bytes, |bytes| BeaconState::from_ssz_bytes(bytes, spec), format, )?, "BeaconStateBase" | "BeaconStatePhase0" => { - decode_and_print(&bytes, BeaconStateBase::::from_ssz_bytes, format)? + decode_and_print(&bytes, BeaconStateBase::::from_ssz_bytes, format)? } "BeaconStateAltair" => { - decode_and_print(&bytes, BeaconStateAltair::::from_ssz_bytes, format)? + decode_and_print(&bytes, BeaconStateAltair::::from_ssz_bytes, format)? } "BeaconStateMerge" | "BeaconStateBellatrix" => { - decode_and_print(&bytes, BeaconStateMerge::::from_ssz_bytes, format)? + decode_and_print(&bytes, BeaconStateMerge::::from_ssz_bytes, format)? } "BeaconStateCapella" => { - decode_and_print(&bytes, BeaconStateCapella::::from_ssz_bytes, format)? + decode_and_print(&bytes, BeaconStateCapella::::from_ssz_bytes, format)? } "BeaconStateDeneb" => { - decode_and_print(&bytes, BeaconStateDeneb::::from_ssz_bytes, format)? + decode_and_print(&bytes, BeaconStateDeneb::::from_ssz_bytes, format)? } - "BlobSidecar" => decode_and_print(&bytes, BlobSidecar::::from_ssz_bytes, format)?, + "BeaconStateElectra" => { + decode_and_print(&bytes, BeaconStateElectra::::from_ssz_bytes, format)? + } + "BlobSidecar" => decode_and_print(&bytes, BlobSidecar::::from_ssz_bytes, format)?, other => return Err(format!("Unknown type: {}", other)), }; diff --git a/lcli/src/replace_state_pubkeys.rs b/lcli/src/replace_state_pubkeys.rs index e9e3388c06..0f9fac3aff 100644 --- a/lcli/src/replace_state_pubkeys.rs +++ b/lcli/src/replace_state_pubkeys.rs @@ -11,7 +11,7 @@ use std::path::PathBuf; use tree_hash::TreeHash; use types::{BeaconState, DepositData, EthSpec, Hash256, SignatureBytes, DEPOSIT_TREE_DEPTH}; -pub fn run(testnet_dir: PathBuf, matches: &ArgMatches) -> Result<(), String> { +pub fn run(testnet_dir: PathBuf, matches: &ArgMatches) -> Result<(), String> { let path = matches .value_of("ssz-state") .ok_or("ssz-state not specified")? @@ -23,9 +23,9 @@ pub fn run(testnet_dir: PathBuf, matches: &ArgMatches) -> Result<(), .ok_or("mnemonic not specified")?; let eth2_network_config = Eth2NetworkConfig::load(testnet_dir)?; - let spec = ð2_network_config.chain_spec::()?; + let spec = ð2_network_config.chain_spec::()?; - let mut state: BeaconState = { + let mut state: BeaconState = { let mut file = File::open(&path).map_err(|e| format!("Unable to open file: {}", e))?; let mut ssz = vec![]; diff --git a/lcli/src/skip_slots.rs b/lcli/src/skip_slots.rs index cdbacfe4d5..9e5da7709f 100644 --- a/lcli/src/skip_slots.rs +++ b/lcli/src/skip_slots.rs @@ -52,6 +52,7 @@ 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 state_processing::AllCaches; use std::fs::File; use std::io::prelude::*; use std::path::PathBuf; @@ -60,12 +61,12 @@ use types::{BeaconState, CloneConfig, EthSpec, Hash256}; const HTTP_TIMEOUT: Duration = Duration::from_secs(10); -pub fn run( - env: Environment, +pub fn run( + env: Environment, network_config: Eth2NetworkConfig, matches: &ArgMatches, ) -> Result<(), String> { - let spec = &network_config.chain_spec::()?; + let spec = &network_config.chain_spec::()?; let executor = env.core_context().executor; let output_path: Option = parse_optional(matches, "output-path")?; @@ -76,7 +77,7 @@ pub fn run( let cli_state_root: Option = parse_optional(matches, "state-root")?; let partial: bool = matches.is_present("partial-state-advance"); - info!("Using {} spec", T::spec_name()); + info!("Using {} spec", E::spec_name()); info!("Advancing {} slots", slots); info!("Doing {} runs", runs); @@ -94,7 +95,7 @@ pub fn run( .ok_or("shutdown in progress")? .block_on(async move { client - .get_debug_beacon_states::(state_id) + .get_debug_beacon_states::(state_id) .await .map_err(|e| format!("Failed to download state: {:?}", e)) }) @@ -115,7 +116,7 @@ pub fn run( let target_slot = initial_slot + slots; state - .build_caches(spec) + .build_all_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 index efcee2827a..06293b79b3 100644 --- a/lcli/src/state_root.rs +++ b/lcli/src/state_root.rs @@ -10,14 +10,14 @@ use types::{BeaconState, EthSpec}; const HTTP_TIMEOUT: Duration = Duration::from_secs(10); -pub fn run( - env: Environment, +pub fn run( + env: Environment, network_config: Eth2NetworkConfig, matches: &ArgMatches, ) -> Result<(), String> { let executor = env.core_context().executor; - let spec = &network_config.chain_spec::()?; + let spec = &network_config.chain_spec::()?; let state_path: Option = parse_optional(matches, "state-path")?; let beacon_url: Option = parse_optional(matches, "beacon-url")?; @@ -26,7 +26,7 @@ pub fn run( info!( "Using {} network ({} spec)", spec.config_name.as_deref().unwrap_or("unknown"), - T::spec_name() + E::spec_name() ); info!("Doing {} runs", runs); @@ -43,7 +43,7 @@ pub fn run( .ok_or("shutdown in progress")? .block_on(async move { client - .get_debug_beacon_states::(state_id) + .get_debug_beacon_states::(state_id) .await .map_err(|e| format!("Failed to download state: {:?}", e)) }) diff --git a/lcli/src/transition_blocks.rs b/lcli/src/transition_blocks.rs index 23b0ae2620..c72b41b1d4 100644 --- a/lcli/src/transition_blocks.rs +++ b/lcli/src/transition_blocks.rs @@ -75,8 +75,8 @@ 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, BlockSignatureStrategy, - ConsensusContext, StateProcessingStrategy, VerifyBlockRoot, + block_signature_verifier::BlockSignatureVerifier, per_block_processing, AllCaches, + BlockSignatureStrategy, ConsensusContext, StateProcessingStrategy, VerifyBlockRoot, }; use std::borrow::Cow; use std::fs::File; @@ -96,12 +96,12 @@ struct Config { exclude_post_block_thc: bool, } -pub fn run( - env: Environment, +pub fn run( + env: Environment, network_config: Eth2NetworkConfig, matches: &ArgMatches, ) -> Result<(), String> { - let spec = &network_config.chain_spec::()?; + let spec = &network_config.chain_spec::()?; let executor = env.core_context().executor; /* @@ -122,7 +122,7 @@ pub fn run( exclude_post_block_thc: matches.is_present("exclude-post-block-thc"), }; - info!("Using {} spec", T::spec_name()); + info!("Using {} spec", E::spec_name()); info!("Doing {} runs", runs); info!("{:?}", &config); @@ -157,7 +157,7 @@ pub fn run( return Err("Cannot run on the genesis block".to_string()); } - let parent_block: SignedBeaconBlock = client + let parent_block: SignedBeaconBlock = client .get_beacon_blocks(BlockId::Root(block.parent_root())) .await .map_err(|e| format!("Failed to download parent block: {:?}", e))? @@ -167,7 +167,7 @@ pub fn run( let state_root = parent_block.state_root(); let state_id = StateId::Root(state_root); let pre_state = client - .get_debug_beacon_states::(state_id) + .get_debug_beacon_states::(state_id) .await .map_err(|e| format!("Failed to download state: {:?}", e))? .ok_or_else(|| format!("Unable to locate state at {:?}", state_id))? @@ -211,7 +211,7 @@ pub fn run( if config.exclude_cache_builds { pre_state - .build_caches(spec) + .build_all_caches(spec) .map_err(|e| format!("Unable to build caches: {:?}", e))?; let state_root = pre_state .update_tree_hash_cache() @@ -232,6 +232,7 @@ pub fn run( */ let mut output_post_state = None; + let mut saved_ctxt = None; for i in 0..runs { let pre_state = pre_state.clone_with(CloneConfig::all()); let block = block.clone(); @@ -245,6 +246,7 @@ pub fn run( state_root_opt, &config, &validator_pubkey_cache, + &mut saved_ctxt, spec, )?; @@ -294,22 +296,26 @@ pub fn run( .map_err(|e| format!("Unable to write to output file: {:?}", e))?; } + drop(pre_state); + Ok(()) } -fn do_transition( - mut pre_state: BeaconState, +#[allow(clippy::too_many_arguments)] +fn do_transition( + mut pre_state: BeaconState, block_root: Hash256, - block: SignedBeaconBlock, + block: SignedBeaconBlock, mut state_root_opt: Option, config: &Config, - validator_pubkey_cache: &ValidatorPubkeyCache>, + validator_pubkey_cache: &ValidatorPubkeyCache>, + saved_ctxt: &mut Option>, spec: &ChainSpec, -) -> Result, String> { +) -> Result, String> { if !config.exclude_cache_builds { let t = Instant::now(); pre_state - .build_caches(spec) + .build_all_caches(spec) .map_err(|e| format!("Unable to build caches: {:?}", e))?; debug!("Build caches: {:?}", t.elapsed()); @@ -337,15 +343,23 @@ fn do_transition( .map_err(|e| format!("Unable to perform complete advance: {e:?}"))?; debug!("Slot processing: {:?}", t.elapsed()); + // Slot and epoch processing should keep the caches fully primed. + assert!(pre_state.all_caches_built()); + let t = Instant::now(); pre_state - .build_caches(spec) + .build_all_caches(spec) .map_err(|e| format!("Unable to build caches: {:?}", e))?; debug!("Build all caches (again): {:?}", t.elapsed()); - let mut ctxt = ConsensusContext::new(pre_state.slot()) - .set_current_block_root(block_root) - .set_proposer_index(block.message().proposer_index()); + let mut ctxt = if let Some(ctxt) = saved_ctxt { + ctxt.clone() + } else { + let ctxt = ConsensusContext::new(pre_state.slot()) + .set_current_block_root(block_root) + .set_proposer_index(block.message().proposer_index()); + ctxt + }; if !config.no_signature_verification { let get_pubkey = move |validator_index| { diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 8517c66c38..54faa03a31 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "lighthouse" -version = "4.6.0" +version = "5.1.3" authors = ["Sigma Prime "] edition = { workspace = true } autotests = false -rust-version = "1.73.0" +rust-version = "1.75.0" [features] default = ["slasher-lmdb"] @@ -14,8 +14,6 @@ write_ssz_files = ["beacon_node/write_ssz_files"] portable = ["bls/supranational-portable"] # Compiles BLST so that it always uses ADX instructions. modern = ["bls/supranational-force-adx"] -# Uses the slower Milagro BLS library, which is written in native Rust. -milagro = ["bls/milagro"] # Support minimal spec (used for testing only). spec-minimal = [] # Support Gnosis spec and Gnosis Beacon Chain. diff --git a/lighthouse/environment/Cargo.toml b/lighthouse/environment/Cargo.toml index b57e1e9dee..f95751392c 100644 --- a/lighthouse/environment/Cargo.toml +++ b/lighthouse/environment/Cargo.toml @@ -5,6 +5,7 @@ authors = ["Paul Hauner "] edition = { workspace = true } [dependencies] +async-channel = { workspace = true } tokio = { workspace = true } slog = { workspace = true } sloggers = { workspace = true } @@ -17,7 +18,6 @@ slog-term = { workspace = true } slog-async = { workspace = true } futures = { workspace = true } slog-json = "2.3.0" -exit-future = { workspace = true } serde = { workspace = true } [target.'cfg(not(target_family = "unix"))'.dependencies] diff --git a/lighthouse/environment/src/lib.rs b/lighthouse/environment/src/lib.rs index a1f6b26a95..e59b1d455a 100644 --- a/lighthouse/environment/src/lib.rs +++ b/lighthouse/environment/src/lib.rs @@ -343,7 +343,7 @@ impl EnvironmentBuilder { /// Consumes the builder, returning an `Environment`. pub fn build(self) -> Result, String> { - let (signal, exit) = exit_future::signal(); + let (signal, exit) = async_channel::bounded(1); let (signal_tx, signal_rx) = channel(1); Ok(Environment { runtime: self @@ -370,8 +370,8 @@ pub struct Environment { signal_rx: Option>, /// Sender to request shutting down. signal_tx: Sender, - signal: Option, - exit: exit_future::Exit, + signal: Option>, + exit: async_channel::Receiver<()>, log: Logger, sse_logging_components: Option, eth_spec_instance: E, @@ -434,7 +434,7 @@ impl Environment { async move { rx.next().await.ok_or("Internal shutdown channel exhausted") }; futures::pin_mut!(inner_shutdown); - match self.runtime().block_on(async { + let register_handlers = async { let mut handles = vec![]; // setup for handling SIGTERM @@ -465,7 +465,9 @@ impl Environment { } future::select(inner_shutdown, future::select_all(handles.into_iter())).await - }) { + }; + + match self.runtime().block_on(register_handlers) { future::Either::Left((Ok(reason), _)) => { info!(self.log, "Internal shutdown received"; "reason" => reason.message()); Ok(reason) @@ -541,7 +543,7 @@ impl Environment { /// Fire exit signal which shuts down all spawned services pub fn fire_signal(&mut self) { if let Some(signal) = self.signal.take() { - let _ = signal.fire(); + drop(signal); } } diff --git a/lighthouse/environment/tests/testnet_dir/config.yaml b/lighthouse/environment/tests/testnet_dir/config.yaml index dbb6819cd8..86f4dce239 100644 --- a/lighthouse/environment/tests/testnet_dir/config.yaml +++ b/lighthouse/environment/tests/testnet_dir/config.yaml @@ -97,3 +97,4 @@ MESSAGE_DOMAIN_VALID_SNAPPY: 0x01000000 ATTESTATION_SUBNET_COUNT: 64 ATTESTATION_SUBNET_EXTRA_BITS: 0 ATTESTATION_SUBNET_PREFIX_BITS: 6 +ATTESTATION_SUBNET_SHUFFLING_PREFIX_BITS: 3 diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index 06eb06fc08..932b125dc6 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -11,6 +11,7 @@ use futures::TryFutureExt; use lighthouse_version::VERSION; use malloc_utils::configure_memory_allocator; use slog::{crit, info}; +use std::backtrace::Backtrace; use std::path::PathBuf; use std::process::exit; use task_executor::ShutdownReason; @@ -22,8 +23,6 @@ fn bls_library_name() -> &'static str { "blst-portable" } else if cfg!(feature = "modern") { "blst-modern" - } else if cfg!(feature = "milagro") { - "milagro" } else { "blst" } @@ -528,6 +527,21 @@ fn run( let log = environment.core_context().log().clone(); + // Log panics properly. + { + let log = log.clone(); + std::panic::set_hook(Box::new(move |info| { + crit!( + log, + "Task panic. This is a bug!"; + "location" => info.location().map(ToString::to_string), + "message" => info.payload().downcast_ref::(), + "backtrace" => %Backtrace::capture(), + "advice" => "Please check above for a backtrace and notify the developers", + ); + })); + } + let mut tracing_log_path: Option = clap_utils::parse_optional(matches, "logfile")?; if tracing_log_path.is_none() { @@ -540,9 +554,7 @@ fn run( let path = tracing_log_path.clone().unwrap(); - let turn_on_terminal_logs = matches.is_present("env_log"); - - logging::create_tracing_layer(path, turn_on_terminal_logs); + logging::create_tracing_layer(path); // Allow Prometheus to export the time at which the process was started. metrics::expose_process_start_time(&log); diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 3efcb2d0e5..4a50126945 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -2,8 +2,8 @@ use beacon_node::ClientConfig as Config; use crate::exec::{CommandLineTestExec, CompletedTest}; use beacon_node::beacon_chain::chain_config::{ - DisallowedReOrgOffsets, DEFAULT_RE_ORG_CUTOFF_DENOMINATOR, - DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, DEFAULT_RE_ORG_THRESHOLD, + DisallowedReOrgOffsets, DEFAULT_RE_ORG_CUTOFF_DENOMINATOR, DEFAULT_RE_ORG_HEAD_THRESHOLD, + DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, }; use beacon_processor::BeaconProcessorConfig; use eth1::Eth1Endpoint; @@ -19,12 +19,12 @@ use std::string::ToString; use std::time::Duration; use tempfile::TempDir; use types::non_zero_usize::new_non_zero_usize; -use types::{ - Address, Checkpoint, Epoch, ExecutionBlockHash, ForkName, Hash256, MainnetEthSpec, - ProgressiveBalancesMode, -}; +use types::{Address, Checkpoint, Epoch, ExecutionBlockHash, Hash256, MainnetEthSpec}; +use unused_port::{unused_tcp4_port, unused_tcp6_port, unused_udp4_port, unused_udp6_port}; const DEFAULT_ETH1_ENDPOINT: &str = "http://localhost:8545/"; + +// These dummy ports should ONLY be used for `enr-xxx-port` flags that do not bind. const DUMMY_ENR_TCP_PORT: u16 = 7777; const DUMMY_ENR_UDP_PORT: u16 = 8888; const DUMMY_ENR_QUIC_PORT: u16 = 9999; @@ -55,6 +55,12 @@ impl CommandLineTest { } fn run_with_zero_port(&mut self) -> CompletedTest { + // Required since Deneb was enabled on mainnet. + self.cmd.arg("--allow-insecure-genesis-sync"); + self.run_with_zero_port_and_no_genesis_sync() + } + + fn run_with_zero_port_and_no_genesis_sync(&mut self) -> CompletedTest { self.cmd.arg("-z"); self.run() } @@ -93,16 +99,16 @@ fn staking_flag() { } #[test] -fn allow_insecure_genesis_sync() { - CommandLineTest::new() - .run_with_zero_port() - .with_config(|config| { - assert_eq!(config.allow_insecure_genesis_sync, false); - }); +#[should_panic] +fn allow_insecure_genesis_sync_default() { + CommandLineTest::new().run_with_zero_port_and_no_genesis_sync(); +} +#[test] +fn allow_insecure_genesis_sync_enabled() { CommandLineTest::new() .flag("allow-insecure-genesis-sync", None) - .run_with_zero_port() + .run_with_zero_port_and_no_genesis_sync() .with_config(|config| { assert_eq!(config.allow_insecure_genesis_sync, true); }); @@ -166,6 +172,26 @@ fn shuffling_cache_set() { .with_config(|config| assert_eq!(config.chain.shuffling_cache_size, 500)); } +#[test] +fn snapshot_cache_default() { + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| { + assert_eq!( + config.chain.snapshot_cache_size, + beacon_node::beacon_chain::snapshot_cache::DEFAULT_SNAPSHOT_CACHE_SIZE + ) + }); +} + +#[test] +fn snapshot_cache_set() { + CommandLineTest::new() + .flag("state-cache-size", Some("500")) + .run_with_zero_port() + .with_config(|config| assert_eq!(config.chain.snapshot_cache_size, 500)); +} + #[test] fn fork_choice_before_proposal_timeout_default() { CommandLineTest::new() @@ -851,6 +877,7 @@ fn network_port_flag_over_ipv4() { let port = 0; CommandLineTest::new() .flag("port", Some(port.to_string().as_str())) + .flag("allow-insecure-genesis-sync", None) .run() .with_config(|config| { assert_eq!( @@ -864,9 +891,10 @@ fn network_port_flag_over_ipv4() { ); }); - let port = 9000; + let port = unused_tcp4_port().expect("Unable to find unused port."); CommandLineTest::new() .flag("port", Some(port.to_string().as_str())) + .flag("allow-insecure-genesis-sync", None) .run() .with_config(|config| { assert_eq!( @@ -886,6 +914,7 @@ fn network_port_flag_over_ipv6() { CommandLineTest::new() .flag("listen-address", Some("::1")) .flag("port", Some(port.to_string().as_str())) + .flag("allow-insecure-genesis-sync", None) .run() .with_config(|config| { assert_eq!( @@ -899,10 +928,11 @@ fn network_port_flag_over_ipv6() { ); }); - let port = 9000; + let port = unused_tcp4_port().expect("Unable to find unused port."); CommandLineTest::new() .flag("listen-address", Some("::1")) .flag("port", Some(port.to_string().as_str())) + .flag("allow-insecure-genesis-sync", None) .run() .with_config(|config| { assert_eq!( @@ -925,6 +955,7 @@ fn network_port_flag_over_ipv4_and_ipv6() { .flag("listen-address", Some("::1")) .flag("port", Some(port.to_string().as_str())) .flag("port6", Some(port6.to_string().as_str())) + .flag("allow-insecure-genesis-sync", None) .run() .with_config(|config| { assert_eq!( @@ -947,13 +978,14 @@ fn network_port_flag_over_ipv4_and_ipv6() { ); }); - let port = 19000; - let port6 = 29000; + let port = unused_tcp4_port().expect("Unable to find unused port."); + let port6 = unused_tcp6_port().expect("Unable to find unused port."); CommandLineTest::new() .flag("listen-address", Some("127.0.0.1")) .flag("listen-address", Some("::1")) .flag("port", Some(port.to_string().as_str())) .flag("port6", Some(port6.to_string().as_str())) + .flag("allow-insecure-genesis-sync", None) .run() .with_config(|config| { assert_eq!( @@ -983,6 +1015,7 @@ fn network_port_and_discovery_port_flags_over_ipv4() { CommandLineTest::new() .flag("port", Some(tcp4_port.to_string().as_str())) .flag("discovery-port", Some(disc4_port.to_string().as_str())) + .flag("allow-insecure-genesis-sync", None) .run() .with_config(|config| { assert_eq!( @@ -1003,6 +1036,7 @@ fn network_port_and_discovery_port_flags_over_ipv6() { .flag("listen-address", Some("::1")) .flag("port", Some(tcp6_port.to_string().as_str())) .flag("discovery-port", Some(disc6_port.to_string().as_str())) + .flag("allow-insecure-genesis-sync", None) .run() .with_config(|config| { assert_eq!( @@ -1028,6 +1062,7 @@ fn network_port_and_discovery_port_flags_over_ipv4_and_ipv6() { .flag("discovery-port", Some(disc4_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("allow-insecure-genesis-sync", None) .run() .with_config(|config| { assert_eq!( @@ -1067,6 +1102,7 @@ fn network_port_discovery_quic_port_flags_over_ipv4_and_ipv6() { .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())) + .flag("allow-insecure-genesis-sync", None) .run() .with_config(|config| { assert_eq!( @@ -1284,15 +1320,15 @@ fn enr_tcp6_port_flag() { fn enr_match_flag_over_ipv4() { let addr = "127.0.0.2".parse::().unwrap(); - // the reason we use the ENR dummy values is because, due to the nature of the `--enr-match` flag, these will eventually become ENR ports (as well as listening ports). - let udp4_port = DUMMY_ENR_UDP_PORT; - let tcp4_port = DUMMY_ENR_TCP_PORT; + let udp4_port = unused_udp4_port().expect("Unable to find unused port."); + let tcp4_port = unused_tcp4_port().expect("Unable to find unused port."); CommandLineTest::new() .flag("enr-match", None) .flag("listen-address", Some("127.0.0.2")) .flag("discovery-port", Some(udp4_port.to_string().as_str())) .flag("port", Some(tcp4_port.to_string().as_str())) + .flag("allow-insecure-genesis-sync", None) .run() .with_config(|config| { assert_eq!( @@ -1315,15 +1351,15 @@ fn enr_match_flag_over_ipv6() { const ADDR: &str = "::1"; let addr = ADDR.parse::().unwrap(); - // the reason we use the ENR dummy values is because, due to the nature of the `--enr-match` flag, these will eventually become ENR ports (as well as listening ports). - let udp6_port = DUMMY_ENR_UDP_PORT; - let tcp6_port = DUMMY_ENR_TCP_PORT; + let udp6_port = unused_udp6_port().expect("Unable to find unused port."); + let tcp6_port = unused_tcp6_port().expect("Unable to find unused port."); CommandLineTest::new() .flag("enr-match", None) .flag("listen-address", Some(ADDR)) .flag("discovery-port", Some(udp6_port.to_string().as_str())) .flag("port", Some(tcp6_port.to_string().as_str())) + .flag("allow-insecure-genesis-sync", None) .run() .with_config(|config| { assert_eq!( @@ -1345,15 +1381,13 @@ fn enr_match_flag_over_ipv6() { fn enr_match_flag_over_ipv4_and_ipv6() { const IPV6_ADDR: &str = "::1"; - // the reason we use the ENR dummy values is because, due to the nature of the `--enr-match` flag, these will eventually become ENR ports (as well as listening ports). - let udp6_port = DUMMY_ENR_UDP_PORT; - let tcp6_port = DUMMY_ENR_TCP_PORT; + let udp6_port = unused_udp6_port().expect("Unable to find unused port."); + let tcp6_port = unused_tcp6_port().expect("Unable to find unused port."); let ipv6_addr = IPV6_ADDR.parse::().unwrap(); const IPV4_ADDR: &str = "127.0.0.1"; - // the reason we use the ENR dummy values is because, due to the nature of the `--enr-match` flag, these will eventually become ENR ports (as well as listening ports). - let udp4_port = DUMMY_ENR_UDP_PORT; - let tcp4_port = DUMMY_ENR_TCP_PORT; + let udp4_port = unused_udp4_port().expect("Unable to find unused port."); + let tcp4_port = unused_tcp4_port().expect("Unable to find unused port."); let ipv4_addr = IPV4_ADDR.parse::().unwrap(); CommandLineTest::new() @@ -1364,6 +1398,7 @@ fn enr_match_flag_over_ipv4_and_ipv6() { .flag("listen-address", Some(IPV6_ADDR)) .flag("discovery-port6", Some(udp6_port.to_string().as_str())) .flag("port6", Some(tcp6_port.to_string().as_str())) + .flag("allow-insecure-genesis-sync", None) .run() .with_config(|config| { assert_eq!( @@ -1490,6 +1525,7 @@ fn http_port_flag() { .flag("http", None) .flag("http-port", Some(port1.to_string().as_str())) .flag("port", Some(port2.to_string().as_str())) + .flag("allow-insecure-genesis-sync", None) .run() .with_config(|config| assert_eq!(config.http_api.listen_port, port1)); } @@ -1547,14 +1583,15 @@ fn http_allow_origin_all_flag() { .run_with_zero_port() .with_config(|config| assert_eq!(config.http_api.allow_origin, Some("*".to_string()))); } + #[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)); + .run_with_zero_port(); } + #[test] fn http_enable_beacon_processor() { CommandLineTest::new() @@ -1606,8 +1643,7 @@ 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))); + .run_with_zero_port(); } // Tests for Metrics flags. @@ -1647,6 +1683,7 @@ fn metrics_port_flag() { .flag("metrics", None) .flag("metrics-port", Some(port1.to_string().as_str())) .flag("port", Some(port2.to_string().as_str())) + .flag("allow-insecure-genesis-sync", None) .run() .with_config(|config| assert_eq!(config.http_metrics.listen_port, port1)); } @@ -2163,8 +2200,8 @@ fn enable_proposer_re_orgs_default() { .run_with_zero_port() .with_config(|config| { assert_eq!( - config.chain.re_org_threshold, - Some(DEFAULT_RE_ORG_THRESHOLD) + config.chain.re_org_head_threshold, + Some(DEFAULT_RE_ORG_HEAD_THRESHOLD) ); assert_eq!( config.chain.re_org_max_epochs_since_finalization, @@ -2182,15 +2219,26 @@ fn disable_proposer_re_orgs() { CommandLineTest::new() .flag("disable-proposer-reorgs", None) .run_with_zero_port() - .with_config(|config| assert_eq!(config.chain.re_org_threshold, None)); + .with_config(|config| { + assert_eq!(config.chain.re_org_head_threshold, None); + assert_eq!(config.chain.re_org_parent_threshold, None) + }); } #[test] -fn proposer_re_org_threshold() { +fn proposer_re_org_parent_threshold() { + CommandLineTest::new() + .flag("proposer-reorg-parent-threshold", Some("90")) + .run_with_zero_port() + .with_config(|config| assert_eq!(config.chain.re_org_parent_threshold.unwrap().0, 90)); +} + +#[test] +fn proposer_re_org_head_threshold() { CommandLineTest::new() .flag("proposer-reorg-threshold", Some("90")) .run_with_zero_port() - .with_config(|config| assert_eq!(config.chain.re_org_threshold.unwrap().0, 90)); + .with_config(|config| assert_eq!(config.chain.re_org_head_threshold.unwrap().0, 90)); } #[test] @@ -2462,29 +2510,12 @@ 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::Fast - ) - }); -} - #[test] fn progressive_balances_checked() { + // Flag is deprecated but supplying it should not crash until we remove it completely. CommandLineTest::new() .flag("progressive-balances", Some("checked")) - .run_with_zero_port() - .with_config(|config| { - assert_eq!( - config.chain.progressive_balances_mode, - ProgressiveBalancesMode::Checked - ) - }); + .run_with_zero_port(); } #[test] @@ -2584,22 +2615,3 @@ fn genesis_state_url_value() { assert_eq!(config.genesis_state_url_timeout, Duration::from_secs(42)); }); } - -#[test] -fn disable_duplicate_warn_logs_default() { - CommandLineTest::new() - .run_with_zero_port() - .with_config(|config| { - assert_eq!(config.network.disable_duplicate_warn_logs, false); - }); -} - -#[test] -fn disable_duplicate_warn_logs() { - CommandLineTest::new() - .flag("disable-duplicate-warn-logs", None) - .run_with_zero_port() - .with_config(|config| { - assert_eq!(config.network.disable_duplicate_warn_logs, true); - }); -} diff --git a/lighthouse/tests/validator_client.rs b/lighthouse/tests/validator_client.rs index 5087acf450..285f3596d9 100644 --- a/lighthouse/tests/validator_client.rs +++ b/lighthouse/tests/validator_client.rs @@ -662,3 +662,20 @@ fn validator_registration_batch_size_zero_value() { .flag("validator-registration-batch-size", Some("0")) .run(); } + +#[test] +fn validator_disable_web3_signer_slashing_protection_default() { + CommandLineTest::new().run().with_config(|config| { + assert!(config.enable_web3signer_slashing_protection); + }); +} + +#[test] +fn validator_disable_web3_signer_slashing_protection() { + CommandLineTest::new() + .flag("disable-slashing-protection-web3signer", None) + .run() + .with_config(|config| { + assert!(!config.enable_web3signer_slashing_protection); + }); +} diff --git a/scripts/cli.sh b/scripts/cli.sh index 7ba98d08ba..2767ed73c8 100755 --- a/scripts/cli.sh +++ b/scripts/cli.sh @@ -17,6 +17,9 @@ write_to_file() { # We need to add the header and the backticks to create the code block. printf "# %s\n\n\`\`\`\n%s\n\`\`\`" "$program" "$cmd" > "$file" + + # Adjust the width of the help text and append to the end of file + sed -i -e '$a\'$'\n''' "$file" } CMD=./target/release/lighthouse diff --git a/scripts/local_testnet/README.md b/scripts/local_testnet/README.md index 74dc4739b4..77c9d62c1c 100644 --- a/scripts/local_testnet/README.md +++ b/scripts/local_testnet/README.md @@ -78,7 +78,7 @@ To view the beacon, validator client and geth logs: ```bash tail -f ~/.lighthouse/local-testnet/testnet/beacon_node_1.log -taif -f ~/.lighthouse/local-testnet/testnet/validator_node_1.log +tail -f ~/.lighthouse/local-testnet/testnet/validator_node_1.log tail -f ~/.lighthouse/local-testnet/testnet/geth_1.log ``` @@ -198,4 +198,4 @@ Update the genesis time to now using: Some addresses in the local testnet are seeded with testnet ETH, allowing users to carry out transactions. To send a transaction, we first add the address to a wallet, such as [Metamask](https://metamask.io/). The private keys for the addresses are listed [here](https://github.com/sigp/lighthouse/blob/441fc1691b69f9edc4bbdc6665f3efab16265c9b/testing/execution_engine_integration/src/execution_engine.rs#L13-L14). -Next, we add the local testnet to Metamask, a brief guide can be found [here](https://support.metamask.io/hc/en-us/articles/360043227612-How-to-add-a-custom-network-RPC). If you start the local testnet with default settings, the network RPC is: http://localhost:6001 and the `Chain ID` is `4242`, as defined in [`vars.env`](https://github.com/sigp/lighthouse/blob/441fc1691b69f9edc4bbdc6665f3efab16265c9b/scripts/local_testnet/vars.env#L42). Once the network and account are added, you should see that the account contains testnet ETH which allow us to carry out transactions. \ No newline at end of file +Next, we add the local testnet to Metamask, a brief guide can be found [here](https://support.metamask.io/hc/en-us/articles/360043227612-How-to-add-a-custom-network-RPC). If you start the local testnet with default settings, the network RPC is: http://localhost:6001 and the `Chain ID` is `4242`, as defined in [`vars.env`](https://github.com/sigp/lighthouse/blob/441fc1691b69f9edc4bbdc6665f3efab16265c9b/scripts/local_testnet/vars.env#L42). Once the network and account are added, you should see that the account contains testnet ETH which allow us to carry out transactions. diff --git a/scripts/local_testnet/beacon_node.sh b/scripts/local_testnet/beacon_node.sh index 2660dfa3c0..940fe2b858 100755 --- a/scripts/local_testnet/beacon_node.sh +++ b/scripts/local_testnet/beacon_node.sh @@ -67,5 +67,4 @@ exec $lighthouse_binary \ --target-peers $((BN_COUNT - 1)) \ --execution-endpoint $execution_endpoint \ --execution-jwt $execution_jwt \ - --http-allow-sync-stalled \ $BN_ARGS diff --git a/scripts/local_testnet/genesis.json b/scripts/local_testnet/genesis.json index eda3b312f6..26003bed5d 100644 --- a/scripts/local_testnet/genesis.json +++ b/scripts/local_testnet/genesis.json @@ -14,6 +14,7 @@ "mergeNetsplitBlock": 0, "shanghaiTime": 0, "cancunTime": 0, + "pragueTime": 0, "terminalTotalDifficulty": 0, "terminalTotalDifficultyPassed": true }, diff --git a/scripts/local_testnet/setup.sh b/scripts/local_testnet/setup.sh index d7a6016aa8..419cba19ed 100755 --- a/scripts/local_testnet/setup.sh +++ b/scripts/local_testnet/setup.sh @@ -29,6 +29,7 @@ lcli \ --bellatrix-fork-epoch $BELLATRIX_FORK_EPOCH \ --capella-fork-epoch $CAPELLA_FORK_EPOCH \ --deneb-fork-epoch $DENEB_FORK_EPOCH \ + --electra-fork-epoch $ELECTRA_FORK_EPOCH \ --ttd $TTD \ --eth1-block-hash $ETH1_BLOCK_HASH \ --eth1-id $CHAIN_ID \ diff --git a/scripts/local_testnet/setup_time.sh b/scripts/local_testnet/setup_time.sh index 21a8ae7ac1..36f7fc4e99 100755 --- a/scripts/local_testnet/setup_time.sh +++ b/scripts/local_testnet/setup_time.sh @@ -28,5 +28,8 @@ sed -i 's/"shanghaiTime".*$/"shanghaiTime": '"$CAPELLA_TIME"',/g' $genesis_file CANCUN_TIME=$((GENESIS_TIME + (DENEB_FORK_EPOCH * $SLOT_PER_EPOCH * SECONDS_PER_SLOT))) echo $CANCUN_TIME sed -i 's/"cancunTime".*$/"cancunTime": '"$CANCUN_TIME"',/g' $genesis_file +PRAGUE_TIME=$((GENESIS_TIME + (ELECTRA_FORK_EPOCH * $SLOT_PER_EPOCH * SECONDS_PER_SLOT))) +echo $PRAGUE_TIME +sed -i 's/"pragueTime".*$/"pragueTime": '"$PRAGUE_TIME"',/g' $genesis_file cat $genesis_file diff --git a/scripts/local_testnet/start_local_testnet.sh b/scripts/local_testnet/start_local_testnet.sh index 512b1e98d1..7742209513 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 # Call setup_time.sh to update future hardforks time in the EL genesis file based on the CL genesis time -./setup_time.sh genesis.json +./setup_time.sh $genesis_file # Delay to let boot_enr.yaml to be created execute_command_add_PID bootnode.log ./bootnode.sh @@ -134,6 +134,7 @@ sleeping 20 # Reset the `genesis.json` config file fork times. sed -i 's/"shanghaiTime".*$/"shanghaiTime": 0,/g' $genesis_file sed -i 's/"cancunTime".*$/"cancunTime": 0,/g' $genesis_file +sed -i 's/"pragueTime".*$/"pragueTime": 0,/g' $genesis_file for (( bn=1; bn<=$BN_COUNT; bn++ )); do secret=$DATADIR/geth_datadir$bn/geth/jwtsecret diff --git a/scripts/local_testnet/vars.env b/scripts/local_testnet/vars.env index 31274d2c57..9bdec71ff7 100644 --- a/scripts/local_testnet/vars.env +++ b/scripts/local_testnet/vars.env @@ -44,8 +44,9 @@ CHAIN_ID=4242 # Hard fork configuration ALTAIR_FORK_EPOCH=0 BELLATRIX_FORK_EPOCH=0 -CAPELLA_FORK_EPOCH=1 -DENEB_FORK_EPOCH=2 +CAPELLA_FORK_EPOCH=0 +DENEB_FORK_EPOCH=1 +ELECTRA_FORK_EPOCH=9999999 TTD=0 diff --git a/scripts/tests/genesis.json b/scripts/tests/genesis.json index 83f45f1a01..bfbc08c81e 100644 --- a/scripts/tests/genesis.json +++ b/scripts/tests/genesis.json @@ -13,7 +13,7 @@ "londonBlock": 0, "mergeForkBlock": 0, "shanghaiTime": 0, - "shardingForkTime": 0, + "cancunTime": 0, "terminalTotalDifficulty": 0, "terminalTotalDifficultyPassed": true }, diff --git a/scripts/tests/vars.env b/scripts/tests/vars.env index 98ae08f074..4d8f9db64e 100644 --- a/scripts/tests/vars.env +++ b/scripts/tests/vars.env @@ -16,7 +16,7 @@ DEPOSIT_CONTRACT_ADDRESS=4242424242424242424242424242424242424242 GENESIS_FORK_VERSION=0x42424242 # Block hash generated from genesis.json in directory -ETH1_BLOCK_HASH=add7865f8346031c72287e2edc4a4952fd34fc0a8642403e8c1bce67f215c92b +ETH1_BLOCK_HASH=7a5c656343c3a66dcf75415958b500e8873f9dab0cd588e6cf0785b52a06dd34 VALIDATOR_COUNT=80 GENESIS_VALIDATOR_COUNT=80 @@ -41,8 +41,9 @@ CHAIN_ID=4242 # Hard fork configuration ALTAIR_FORK_EPOCH=0 BELLATRIX_FORK_EPOCH=0 -CAPELLA_FORK_EPOCH=1 -DENEB_FORK_EPOCH=18446744073709551615 +CAPELLA_FORK_EPOCH=0 +DENEB_FORK_EPOCH=0 +ELECTRA_FORK_EPOCH=18446744073709551615 TTD=0 @@ -62,4 +63,4 @@ PROPOSER_SCORE_BOOST=70 BN_ARGS="" # Enable doppelganger detection -VC_ARGS=" --enable-doppelganger-protection " \ No newline at end of file +VC_ARGS=" --enable-doppelganger-protection " diff --git a/slasher/src/array.rs b/slasher/src/array.rs index 91c8f373f4..b733b07c63 100644 --- a/slasher/src/array.rs +++ b/slasher/src/array.rs @@ -7,9 +7,7 @@ use flate2::bufread::{ZlibDecoder, ZlibEncoder}; use serde::{Deserialize, Serialize}; use std::borrow::Borrow; use std::collections::{btree_map::Entry, BTreeMap, HashSet}; -use std::convert::TryFrom; use std::io::Read; -use std::iter::Extend; use std::sync::Arc; use types::{AttesterSlashing, Epoch, EthSpec, IndexedAttestation}; diff --git a/slasher/src/attester_record.rs b/slasher/src/attester_record.rs index 498e8d49f0..56fdcb809f 100644 --- a/slasher/src/attester_record.rs +++ b/slasher/src/attester_record.rs @@ -79,16 +79,16 @@ impl IndexedAttesterRecord { } #[derive(Debug, Clone, Encode, Decode, TreeHash)] -struct IndexedAttestationHeader { - pub attesting_indices: VariableList, +struct IndexedAttestationHeader { + pub attesting_indices: VariableList, pub data_root: Hash256, pub signature: AggregateSignature, } -impl From> for AttesterRecord { - fn from(indexed_attestation: IndexedAttestation) -> AttesterRecord { +impl From> for AttesterRecord { + fn from(indexed_attestation: IndexedAttestation) -> AttesterRecord { let attestation_data_hash = indexed_attestation.data.tree_hash_root(); - let header = IndexedAttestationHeader:: { + let header = IndexedAttestationHeader:: { attesting_indices: indexed_attestation.attesting_indices, data_root: attestation_data_hash, signature: indexed_attestation.signature, diff --git a/slasher/src/database/lmdb_impl.rs b/slasher/src/database/lmdb_impl.rs index 98839fcc46..78deaf1767 100644 --- a/slasher/src/database/lmdb_impl.rs +++ b/slasher/src/database/lmdb_impl.rs @@ -3,15 +3,12 @@ use crate::{ config::MEGABYTE, database::{ - interface::{Key, OpenDatabases, Value}, + interface::{Key, Value}, *, }, - Config, Error, }; use lmdb::{Cursor as _, DatabaseFlags, Transaction, WriteFlags}; use lmdb_sys::{MDB_FIRST, MDB_GET_CURRENT, MDB_LAST, MDB_NEXT}; -use std::borrow::Cow; -use std::marker::PhantomData; use std::path::PathBuf; #[derive(Debug)] diff --git a/slasher/tests/random.rs b/slasher/tests/random.rs index 968a4dbb68..ce0e42df1d 100644 --- a/slasher/tests/random.rs +++ b/slasher/tests/random.rs @@ -2,7 +2,6 @@ use logging::test_logger; use rand::prelude::*; -use rand::{rngs::StdRng, thread_rng, Rng, SeedableRng}; use slasher::{ test_utils::{ block, indexed_att, slashed_validators_from_attestations, diff --git a/testing/ef_tests/Cargo.toml b/testing/ef_tests/Cargo.toml index 8bc36d008b..f3d00fa035 100644 --- a/testing/ef_tests/Cargo.toml +++ b/testing/ef_tests/Cargo.toml @@ -7,7 +7,6 @@ edition = { workspace = true } [features] # `ef_tests` feature must be enabled to actually run the tests ef_tests = [] -milagro = ["bls/milagro"] fake_crypto = ["bls/fake_crypto"] portable = ["beacon_chain/portable"] diff --git a/testing/ef_tests/Makefile b/testing/ef_tests/Makefile index e42db1801d..508c284275 100644 --- a/testing/ef_tests/Makefile +++ b/testing/ef_tests/Makefile @@ -1,4 +1,4 @@ -TESTS_TAG := v1.4.0-beta.4 +TESTS_TAG := v1.4.0-beta.6 TESTS = general minimal mainnet TARBALLS = $(patsubst %,%-$(TESTS_TAG).tar.gz,$(TESTS)) diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index a5ab897c33..7629d61827 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -29,18 +29,8 @@ excluded_paths = [ "tests/.*/.*/light_client", # LightClientStore "tests/.*/.*/ssz_static/LightClientStore", - # LightClientUpdate - "tests/.*/.*/ssz_static/LightClientUpdate", # LightClientSnapshot "tests/.*/.*/ssz_static/LightClientSnapshot", - # LightClientBootstrap - "tests/.*/.*/ssz_static/LightClientBootstrap", - # LightClientOptimistic - "tests/.*/.*/ssz_static/LightClientOptimistic", - # LightClientFinalityUpdate - "tests/.*/.*/ssz_static/LightClientFinalityUpdate", - # LightClientHeader - "tests/.*/.*/ssz_static/LightClientHeader", # One of the EF researchers likes to pack the tarballs on a Mac ".*\.DS_Store.*", # More Mac weirdness. @@ -51,7 +41,8 @@ excluded_paths = [ "bls12-381-tests/deserialization_G1", "bls12-381-tests/deserialization_G2", "bls12-381-tests/hash_to_G2", - "tests/.*/eip6110" + "tests/.*/eip6110", + "tests/.*/whisk" ] diff --git a/testing/ef_tests/src/case_result.rs b/testing/ef_tests/src/case_result.rs index 4982bf94c1..67ab9c51bb 100644 --- a/testing/ef_tests/src/case_result.rs +++ b/testing/ef_tests/src/case_result.rs @@ -32,9 +32,9 @@ impl CaseResult { /// Same as `compare_result_detailed`, however it drops the caches on both states before /// comparison. -pub fn compare_beacon_state_results_without_caches( - result: &mut Result, E>, - expected: &mut Option>, +pub fn compare_beacon_state_results_without_caches( + result: &mut Result, T>, + expected: &mut Option>, ) -> Result<(), Error> { if let (Ok(ref mut result), Some(ref mut expected)) = (result.as_mut(), expected.as_mut()) { result.drop_all_caches().unwrap(); @@ -46,13 +46,13 @@ pub fn compare_beacon_state_results_without_caches( /// Same as `compare_result`, however utilizes the `CompareFields` trait to give a list of /// mismatching fields when `Ok(result) != Some(expected)`. -pub fn compare_result_detailed( - result: &Result, +pub fn compare_result_detailed( + result: &Result, expected: &Option, ) -> Result<(), Error> where T: PartialEq + Debug + CompareFields, - E: Debug, + U: Debug, { match (result, expected) { (Ok(result), Some(expected)) => { @@ -84,10 +84,10 @@ where /// /// If `expected.is_none()` then `result` is expected to be `Err`. Otherwise, `T` in `result` and /// `expected` must be equal. -pub fn compare_result(result: &Result, expected: &Option) -> Result<(), Error> +pub fn compare_result(result: &Result, expected: &Option) -> Result<(), Error> where T: PartialEq + Debug, - E: Debug, + U: Debug, { match (result, expected) { // Pass: The should have failed and did fail. diff --git a/testing/ef_tests/src/cases/bls_eth_aggregate_pubkeys.rs b/testing/ef_tests/src/cases/bls_eth_aggregate_pubkeys.rs index 8783aa141e..2a9a393bfd 100644 --- a/testing/ef_tests/src/cases/bls_eth_aggregate_pubkeys.rs +++ b/testing/ef_tests/src/cases/bls_eth_aggregate_pubkeys.rs @@ -31,10 +31,6 @@ impl Case for BlsEthAggregatePubkeys { { return Ok(()); } - #[cfg(feature = "milagro")] - Err(bls::Error::MilagroError(_)) if self.output.is_none() => { - return Ok(()); - } Err(e) => return Err(Error::FailedToParseTest(format!("{:?}", e))), }; diff --git a/testing/ef_tests/src/cases/bls_eth_fast_aggregate_verify.rs b/testing/ef_tests/src/cases/bls_eth_fast_aggregate_verify.rs index 0fb3a026cf..88a161e974 100644 --- a/testing/ef_tests/src/cases/bls_eth_fast_aggregate_verify.rs +++ b/testing/ef_tests/src/cases/bls_eth_fast_aggregate_verify.rs @@ -3,7 +3,6 @@ use crate::case_result::compare_result; use crate::impl_bls_load_case; use bls::{AggregateSignature, PublicKeyBytes}; use serde::Deserialize; -use std::convert::TryInto; use types::Hash256; #[derive(Debug, Clone, Deserialize)] diff --git a/testing/ef_tests/src/cases/bls_fast_aggregate_verify.rs b/testing/ef_tests/src/cases/bls_fast_aggregate_verify.rs index dcdc1bd197..cec2edcfad 100644 --- a/testing/ef_tests/src/cases/bls_fast_aggregate_verify.rs +++ b/testing/ef_tests/src/cases/bls_fast_aggregate_verify.rs @@ -3,7 +3,6 @@ use crate::case_result::compare_result; use crate::impl_bls_load_case; use bls::{AggregateSignature, PublicKeyBytes}; use serde::Deserialize; -use std::convert::TryInto; use types::Hash256; #[derive(Debug, Clone, Deserialize)] diff --git a/testing/ef_tests/src/cases/bls_verify_msg.rs b/testing/ef_tests/src/cases/bls_verify_msg.rs index 24b62c5fa1..42ee459a60 100644 --- a/testing/ef_tests/src/cases/bls_verify_msg.rs +++ b/testing/ef_tests/src/cases/bls_verify_msg.rs @@ -3,7 +3,6 @@ use crate::case_result::compare_result; use crate::impl_bls_load_case; use bls::{PublicKeyBytes, Signature, SignatureBytes}; use serde::Deserialize; -use std::convert::TryInto; use types::Hash256; #[derive(Debug, Clone, Deserialize)] diff --git a/testing/ef_tests/src/cases/common.rs b/testing/ef_tests/src/cases/common.rs index 2a7c998758..342a48ba46 100644 --- a/testing/ef_tests/src/cases/common.rs +++ b/testing/ef_tests/src/cases/common.rs @@ -1,7 +1,6 @@ use serde::Deserialize; use ssz::Encode; use ssz_derive::{Decode, Encode}; -use std::convert::TryFrom; use std::fmt::Debug; use tree_hash::TreeHash; use types::ForkName; @@ -67,6 +66,7 @@ pub fn previous_fork(fork_name: ForkName) -> ForkName { ForkName::Merge => ForkName::Altair, ForkName::Capella => ForkName::Merge, ForkName::Deneb => ForkName::Capella, + ForkName::Electra => ForkName::Deneb, } } diff --git a/testing/ef_tests/src/cases/epoch_processing.rs b/testing/ef_tests/src/cases/epoch_processing.rs index cf182af2b2..a9c77c53c5 100644 --- a/testing/ef_tests/src/cases/epoch_processing.rs +++ b/testing/ef_tests/src/cases/epoch_processing.rs @@ -3,20 +3,23 @@ use crate::bls_setting::BlsSetting; use crate::case_result::compare_beacon_state_results_without_caches; use crate::decode::{ssz_decode_state, yaml_decode_file}; use crate::type_name; -use crate::type_name::TypeName; use serde::Deserialize; +use state_processing::common::update_progressive_balances_cache::initialize_progressive_balances_cache; +use state_processing::epoch_cache::initialize_epoch_cache; use state_processing::per_epoch_processing::capella::process_historical_summaries_update; -use state_processing::per_epoch_processing::effective_balance_updates::process_effective_balance_updates; +use state_processing::per_epoch_processing::effective_balance_updates::{ + process_effective_balance_updates, process_effective_balance_updates_slow, +}; use state_processing::per_epoch_processing::{ altair, base, historical_roots_update::process_historical_roots_update, - process_registry_updates, process_slashings, + process_registry_updates, process_registry_updates_slow, process_slashings, + process_slashings_slow, resets::{process_eth1_data_reset, process_randao_mixes_reset, process_slashings_reset}, }; use state_processing::EpochProcessingError; use std::marker::PhantomData; -use std::path::{Path, PathBuf}; -use types::{BeaconState, ChainSpec, EthSpec, ForkName}; +use types::BeaconState; #[derive(Debug, Clone, Default, Deserialize)] pub struct Metadata { @@ -104,12 +107,11 @@ impl EpochTransition for JustificationAndFinalization { BeaconState::Altair(_) | BeaconState::Merge(_) | BeaconState::Capella(_) - | BeaconState::Deneb(_) => { + | BeaconState::Deneb(_) + | BeaconState::Electra(_) => { + initialize_progressive_balances_cache(state, spec)?; let justification_and_finalization_state = - altair::process_justification_and_finalization( - state, - &altair::ParticipationCache::new(state, spec).unwrap(), - )?; + altair::process_justification_and_finalization(state)?; justification_and_finalization_state.apply_changes_to_state(state); Ok(()) } @@ -128,18 +130,21 @@ impl EpochTransition for RewardsAndPenalties { BeaconState::Altair(_) | BeaconState::Merge(_) | BeaconState::Capella(_) - | BeaconState::Deneb(_) => altair::process_rewards_and_penalties( - state, - &altair::ParticipationCache::new(state, spec).unwrap(), - spec, - ), + | BeaconState::Deneb(_) + | BeaconState::Electra(_) => altair::process_rewards_and_penalties_slow(state, spec), } } } impl EpochTransition for RegistryUpdates { fn run(state: &mut BeaconState, spec: &ChainSpec) -> Result<(), EpochProcessingError> { - process_registry_updates(state, spec) + initialize_epoch_cache(state, spec)?; + + if let BeaconState::Base(_) = state { + process_registry_updates(state, spec) + } else { + process_registry_updates_slow(state, spec) + } } } @@ -158,14 +163,9 @@ impl EpochTransition for Slashings { BeaconState::Altair(_) | BeaconState::Merge(_) | BeaconState::Capella(_) - | BeaconState::Deneb(_) => { - process_slashings( - state, - altair::ParticipationCache::new(state, spec) - .unwrap() - .current_epoch_total_active_balance(), - spec, - )?; + | BeaconState::Deneb(_) + | BeaconState::Electra(_) => { + process_slashings_slow(state, spec)?; } }; Ok(()) @@ -180,7 +180,11 @@ impl EpochTransition for Eth1DataReset { impl EpochTransition for EffectiveBalanceUpdates { fn run(state: &mut BeaconState, spec: &ChainSpec) -> Result<(), EpochProcessingError> { - process_effective_balance_updates(state, None, spec) + if let BeaconState::Base(_) = state { + process_effective_balance_updates(state, spec) + } else { + process_effective_balance_updates_slow(state, spec) + } } } @@ -210,7 +214,7 @@ impl EpochTransition for HistoricalRootsUpdate { impl EpochTransition for HistoricalSummariesUpdate { fn run(state: &mut BeaconState, _spec: &ChainSpec) -> Result<(), EpochProcessingError> { match state { - BeaconState::Capella(_) | BeaconState::Deneb(_) => { + BeaconState::Capella(_) | BeaconState::Deneb(_) | BeaconState::Electra(_) => { process_historical_summaries_update(state) } _ => Ok(()), @@ -235,7 +239,8 @@ impl EpochTransition for SyncCommitteeUpdates { BeaconState::Altair(_) | BeaconState::Merge(_) | BeaconState::Capella(_) - | BeaconState::Deneb(_) => altair::process_sync_committee_updates(state, spec), + | BeaconState::Deneb(_) + | BeaconState::Electra(_) => altair::process_sync_committee_updates(state, spec), } } } @@ -247,11 +252,8 @@ impl EpochTransition for InactivityUpdates { BeaconState::Altair(_) | BeaconState::Merge(_) | BeaconState::Capella(_) - | BeaconState::Deneb(_) => altair::process_inactivity_updates( - state, - &altair::ParticipationCache::new(state, spec).unwrap(), - spec, - ), + | BeaconState::Deneb(_) + | BeaconState::Electra(_) => altair::process_inactivity_updates_slow(state, spec), } } } @@ -263,7 +265,8 @@ impl EpochTransition for ParticipationFlagUpdates { BeaconState::Altair(_) | BeaconState::Merge(_) | BeaconState::Capella(_) - | BeaconState::Deneb(_) => altair::process_participation_flag_updates(state), + | BeaconState::Deneb(_) + | BeaconState::Electra(_) => altair::process_participation_flag_updates(state), } } } @@ -314,7 +317,7 @@ impl> Case for EpochProcessing { T::name() != "participation_record_updates" && T::name() != "historical_summaries_update" } - ForkName::Capella | ForkName::Deneb => { + ForkName::Capella | ForkName::Deneb | ForkName::Electra => { T::name() != "participation_record_updates" && T::name() != "historical_roots_update" } @@ -324,17 +327,20 @@ impl> Case for EpochProcessing { fn result(&self, _case_index: usize, fork_name: ForkName) -> Result<(), Error> { self.metadata.bls_setting.unwrap_or_default().check()?; - let mut state = self.pre.clone(); + let spec = &testing_spec::(fork_name); + let mut pre_state = self.pre.clone(); + + // Processing requires the committee caches. + pre_state.build_all_committee_caches(spec).unwrap(); + + let mut state = pre_state.clone(); let mut expected = self.post.clone(); - let spec = &testing_spec::(fork_name); + if let Some(post_state) = expected.as_mut() { + post_state.build_all_committee_caches(spec).unwrap(); + } - let mut result = (|| { - // Processing requires the committee caches. - state.build_all_committee_caches(spec)?; - - T::run(&mut state, spec).map(|_| state) - })(); + let mut result = T::run(&mut state, spec).map(|_| state); compare_beacon_state_results_without_caches(&mut result, &mut expected) } diff --git a/testing/ef_tests/src/cases/fork.rs b/testing/ef_tests/src/cases/fork.rs index bc340fa1cb..be8a344a35 100644 --- a/testing/ef_tests/src/cases/fork.rs +++ b/testing/ef_tests/src/cases/fork.rs @@ -5,8 +5,9 @@ use crate::decode::{ssz_decode_state, yaml_decode_file}; use serde::Deserialize; use state_processing::upgrade::{ upgrade_to_altair, upgrade_to_bellatrix, upgrade_to_capella, upgrade_to_deneb, + upgrade_to_electra, }; -use types::{BeaconState, ForkName}; +use types::BeaconState; #[derive(Debug, Clone, Default, Deserialize)] pub struct Metadata { @@ -65,6 +66,7 @@ impl Case for ForkTest { ForkName::Merge => upgrade_to_bellatrix(&mut result_state, spec).map(|_| result_state), ForkName::Capella => upgrade_to_capella(&mut result_state, spec).map(|_| result_state), ForkName::Deneb => upgrade_to_deneb(&mut result_state, spec).map(|_| result_state), + ForkName::Electra => upgrade_to_electra(&mut result_state, spec).map(|_| result_state), }; compare_beacon_state_results_without_caches(&mut result, &mut expected) diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 9884a709eb..f0749c3c7e 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -4,7 +4,8 @@ use ::fork_choice::{PayloadVerificationStatus, ProposerHeadError}; use beacon_chain::beacon_proposer_cache::compute_proposer_duties_from_head; use beacon_chain::blob_verification::GossipBlobError; use beacon_chain::chain_config::{ - DisallowedReOrgOffsets, DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, DEFAULT_RE_ORG_THRESHOLD, + DisallowedReOrgOffsets, DEFAULT_RE_ORG_HEAD_THRESHOLD, + DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, DEFAULT_RE_ORG_PARENT_THRESHOLD, }; use beacon_chain::slot_clock::SlotClock; use beacon_chain::{ @@ -24,8 +25,8 @@ use std::sync::Arc; use std::time::Duration; use types::{ Attestation, AttesterSlashing, BeaconBlock, BeaconState, BlobSidecar, BlobsList, Checkpoint, - EthSpec, ExecutionBlockHash, ForkName, Hash256, IndexedAttestation, KzgProof, - ProgressiveBalancesMode, ProposerPreparationData, SignedBeaconBlock, Slot, Uint256, + ExecutionBlockHash, Hash256, IndexedAttestation, KzgProof, ProposerPreparationData, + SignedBeaconBlock, Slot, Uint256, }; #[derive(Default, Debug, PartialEq, Clone, Deserialize, Decode)] @@ -557,9 +558,7 @@ impl Tester { block_delay, &state, PayloadVerificationStatus::Irrelevant, - ProgressiveBalancesMode::Strict, &self.harness.chain.spec, - self.harness.logger(), ); if result.is_ok() { @@ -748,7 +747,8 @@ impl Tester { let proposer_head_result = fc.get_proposer_head( slot, canonical_head, - DEFAULT_RE_ORG_THRESHOLD, + DEFAULT_RE_ORG_HEAD_THRESHOLD, + DEFAULT_RE_ORG_PARENT_THRESHOLD, &DisallowedReOrgOffsets::default(), DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, ); diff --git a/testing/ef_tests/src/cases/genesis_initialization.rs b/testing/ef_tests/src/cases/genesis_initialization.rs index 14fe7ef959..11402c75e6 100644 --- a/testing/ef_tests/src/cases/genesis_initialization.rs +++ b/testing/ef_tests/src/cases/genesis_initialization.rs @@ -3,8 +3,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 serde::Deserialize; use state_processing::initialize_beacon_state_from_eth1; -use std::path::PathBuf; -use types::{BeaconState, Deposit, EthSpec, ExecutionPayloadHeader, ForkName, Hash256}; +use types::{BeaconState, Deposit, ExecutionPayloadHeader, Hash256}; #[derive(Debug, Clone, Deserialize)] struct Metadata { diff --git a/testing/ef_tests/src/cases/genesis_validity.rs b/testing/ef_tests/src/cases/genesis_validity.rs index ec89e0f64b..e977fa3d63 100644 --- a/testing/ef_tests/src/cases/genesis_validity.rs +++ b/testing/ef_tests/src/cases/genesis_validity.rs @@ -2,8 +2,7 @@ use super::*; use crate::decode::{ssz_decode_state, yaml_decode_file}; use serde::Deserialize; use state_processing::is_valid_genesis_state; -use std::path::Path; -use types::{BeaconState, EthSpec, ForkName}; +use types::BeaconState; #[derive(Debug, Clone, Deserialize)] pub struct Metadata { diff --git a/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof.rs b/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof.rs index 04d1b8d5dc..f68f0fd7ed 100644 --- a/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof.rs +++ b/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof.rs @@ -4,7 +4,6 @@ use beacon_chain::kzg_utils::validate_blob; use eth2_network_config::TRUSTED_SETUP_BYTES; use kzg::{Error as KzgError, Kzg, KzgCommitment, KzgProof, TrustedSetup}; use serde::Deserialize; -use std::convert::TryInto; use std::marker::PhantomData; use types::Blob; diff --git a/testing/ef_tests/src/cases/merkle_proof_validity.rs b/testing/ef_tests/src/cases/merkle_proof_validity.rs index d9deda8123..a2e831ade5 100644 --- a/testing/ef_tests/src/cases/merkle_proof_validity.rs +++ b/testing/ef_tests/src/cases/merkle_proof_validity.rs @@ -1,9 +1,10 @@ use super::*; use crate::decode::{ssz_decode_file, ssz_decode_state, yaml_decode_file}; use serde::Deserialize; -use std::path::Path; use tree_hash::Hash256; -use types::{BeaconBlockBody, BeaconBlockBodyDeneb, BeaconState, EthSpec, ForkName}; +use types::{ + BeaconBlockBody, BeaconBlockBodyDeneb, BeaconBlockBodyElectra, BeaconState, FullPayload, +}; #[derive(Debug, Clone, Deserialize)] pub struct Metadata { @@ -93,7 +94,7 @@ pub struct KzgInclusionMerkleProofValidity { impl LoadCase for KzgInclusionMerkleProofValidity { fn load_from_dir(path: &Path, fork_name: ForkName) -> Result { - let block = match fork_name { + let block: BeaconBlockBody> = match fork_name { ForkName::Base | ForkName::Altair | ForkName::Merge | ForkName::Capella => { return Err(Error::InternalError(format!( "KZG inclusion merkle proof validity test skipped for {:?}", @@ -101,7 +102,11 @@ impl LoadCase for KzgInclusionMerkleProofValidity { ))) } ForkName::Deneb => { - ssz_decode_file::>(&path.join("object.ssz_snappy"))? + ssz_decode_file::>(&path.join("object.ssz_snappy"))?.into() + } + ForkName::Electra => { + ssz_decode_file::>(&path.join("object.ssz_snappy"))? + .into() } }; let merkle_proof = yaml_decode_file(&path.join("proof.yaml"))?; @@ -115,7 +120,7 @@ impl LoadCase for KzgInclusionMerkleProofValidity { Ok(Self { metadata, - block: block.into(), + block, merkle_proof, }) } diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index 4c02126d41..a2f50896a5 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -2,10 +2,10 @@ use super::*; use crate::bls_setting::BlsSetting; 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::Deserialize; use ssz::Decode; use state_processing::common::update_progressive_balances_cache::initialize_progressive_balances_cache; +use state_processing::epoch_cache::initialize_epoch_cache; use state_processing::{ per_block_processing::{ errors::BlockProcessingError, @@ -19,11 +19,10 @@ use state_processing::{ ConsensusContext, }; use std::fmt::Debug; -use std::path::Path; use types::{ Attestation, AttesterSlashing, BeaconBlock, BeaconBlockBody, BeaconBlockBodyCapella, - BeaconBlockBodyDeneb, BeaconBlockBodyMerge, BeaconState, BlindedPayload, ChainSpec, Deposit, - EthSpec, ExecutionPayload, ForkName, FullPayload, ProposerSlashing, SignedBlsToExecutionChange, + BeaconBlockBodyDeneb, BeaconBlockBodyMerge, BeaconState, BlindedPayload, Deposit, + ExecutionPayload, FullPayload, ProposerSlashing, SignedBlsToExecutionChange, SignedVoluntaryExit, SyncAggregate, }; @@ -40,8 +39,8 @@ struct ExecutionMetadata { /// Newtype for testing withdrawals. #[derive(Debug, Clone, Deserialize)] -pub struct WithdrawalsPayload { - payload: FullPayload, +pub struct WithdrawalsPayload { + payload: FullPayload, } #[derive(Debug, Clone)] @@ -89,6 +88,7 @@ impl Operation for Attestation { spec: &ChainSpec, _: &Operations, ) -> Result<(), BlockProcessingError> { + initialize_epoch_cache(state, spec)?; let mut ctxt = ConsensusContext::new(state.slot()); match state { BeaconState::Base(_) => base::process_attestations( @@ -101,8 +101,9 @@ impl Operation for Attestation { BeaconState::Altair(_) | BeaconState::Merge(_) | BeaconState::Capella(_) - | BeaconState::Deneb(_) => { - initialize_progressive_balances_cache(state, None, spec)?; + | BeaconState::Deneb(_) + | BeaconState::Electra(_) => { + initialize_progressive_balances_cache(state, spec)?; altair_deneb::process_attestation( state, self, @@ -132,7 +133,7 @@ impl Operation for AttesterSlashing { _: &Operations, ) -> Result<(), BlockProcessingError> { let mut ctxt = ConsensusContext::new(state.slot()); - initialize_progressive_balances_cache(state, None, spec)?; + initialize_progressive_balances_cache(state, spec)?; process_attester_slashings( state, &[self.clone()], @@ -183,7 +184,7 @@ impl Operation for ProposerSlashing { _: &Operations, ) -> Result<(), BlockProcessingError> { let mut ctxt = ConsensusContext::new(state.slot()); - initialize_progressive_balances_cache(state, None, spec)?; + initialize_progressive_balances_cache(state, spec)?; process_proposer_slashings( state, &[self.clone()], @@ -485,14 +486,22 @@ impl> Case for Operations { fn result(&self, _case_index: usize, fork_name: ForkName) -> Result<(), Error> { let spec = &testing_spec::(fork_name); - let mut state = self.pre.clone(); - let mut expected = self.post.clone(); + let mut pre_state = self.pre.clone(); // Processing requires the committee caches. // NOTE: some of the withdrawals tests have 0 active validators, do not try // to build the commitee cache in this case. if O::handler_name() != "withdrawals" { - state.build_all_committee_caches(spec).unwrap(); + pre_state.build_all_committee_caches(spec).unwrap(); + } + + let mut state = pre_state.clone(); + let mut expected = self.post.clone(); + + if O::handler_name() != "withdrawals" { + if let Some(post_state) = expected.as_mut() { + post_state.build_all_committee_caches(spec).unwrap(); + } } let mut result = self diff --git a/testing/ef_tests/src/cases/rewards.rs b/testing/ef_tests/src/cases/rewards.rs index bb41f6fe12..ea75c69c35 100644 --- a/testing/ef_tests/src/cases/rewards.rs +++ b/testing/ef_tests/src/cases/rewards.rs @@ -7,17 +7,13 @@ use ssz::four_byte_option_impl; use ssz_derive::{Decode, Encode}; use state_processing::{ per_epoch_processing::{ - altair::{self, rewards_and_penalties::get_flag_index_deltas, ParticipationCache}, + altair, base::{self, rewards_and_penalties::AttestationDelta, ValidatorStatuses}, Delta, }, EpochProcessingError, }; -use std::path::{Path, PathBuf}; -use types::{ - consts::altair::{TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX}, - BeaconState, EthSpec, ForkName, -}; +use types::BeaconState; #[derive(Debug, Clone, PartialEq, Decode, Encode, CompareFields)] pub struct Deltas { @@ -41,6 +37,11 @@ pub struct AllDeltas { inactivity_penalty_deltas: Deltas, } +#[derive(Debug, Clone, PartialEq, CompareFields)] +pub struct TotalDeltas { + deltas: Vec, +} + #[derive(Debug, Clone, Default, Deserialize)] pub struct Metadata { pub description: Option, @@ -110,11 +111,19 @@ impl Case for RewardsTest { let mut state = self.pre.clone(); let spec = &testing_spec::(fork_name); - let deltas: Result = (|| { - // Processing requires the committee caches. - state.build_all_committee_caches(spec)?; + // Single-pass epoch processing doesn't compute rewards in the genesis epoch because that's + // what the spec for `process_rewards_and_penalties` says to do. We skip these tests for now. + // + // See: https://github.com/ethereum/consensus-specs/issues/3593 + if fork_name != ForkName::Base && state.current_epoch() == 0 { + return Err(Error::SkippedKnownFailure); + } + + if let BeaconState::Base(_) = state { + let deltas: Result = (|| { + // Processing requires the committee caches. + state.build_all_committee_caches(spec)?; - if let BeaconState::Base(_) = state { let mut validator_statuses = ValidatorStatuses::new(&state, spec)?; validator_statuses.process_attestations(&state)?; @@ -125,39 +134,19 @@ impl Case for RewardsTest { )?; Ok(convert_all_base_deltas(&deltas)) - } else { - let total_active_balance = state.get_total_active_balance()?; + })(); + compare_result_detailed(&deltas, &Some(self.deltas.clone()))?; + } else { + let deltas: Result = (|| { + // Processing requires the committee caches. + state.build_all_committee_caches(spec)?; + compute_altair_deltas(&mut state, spec) + })(); - let source_deltas = compute_altair_flag_deltas( - &state, - TIMELY_SOURCE_FLAG_INDEX, - total_active_balance, - spec, - )?; - let target_deltas = compute_altair_flag_deltas( - &state, - TIMELY_TARGET_FLAG_INDEX, - total_active_balance, - spec, - )?; - let head_deltas = compute_altair_flag_deltas( - &state, - TIMELY_HEAD_FLAG_INDEX, - total_active_balance, - spec, - )?; - let inactivity_penalty_deltas = compute_altair_inactivity_deltas(&state, spec)?; - Ok(AllDeltas { - source_deltas, - target_deltas, - head_deltas, - inclusion_delay_deltas: None, - inactivity_penalty_deltas, - }) - } - })(); + let expected = all_deltas_to_total_deltas(&self.deltas); - compare_result_detailed(&deltas, &Some(self.deltas.clone()))?; + compare_result_detailed(&deltas, &Some(expected))?; + }; Ok(()) } @@ -182,39 +171,54 @@ fn convert_base_deltas(attestation_deltas: &[AttestationDelta], accessor: Access Deltas { rewards, penalties } } -fn compute_altair_flag_deltas( - state: &BeaconState, - flag_index: usize, - total_active_balance: u64, - spec: &ChainSpec, -) -> Result { - let mut deltas = vec![Delta::default(); state.validators().len()]; - get_flag_index_deltas( - &mut deltas, - state, - flag_index, - total_active_balance, - &ParticipationCache::new(state, spec).unwrap(), - spec, - )?; - Ok(convert_altair_deltas(deltas)) +fn deltas_to_total_deltas(d: &Deltas) -> impl Iterator + '_ { + d.rewards + .iter() + .zip(&d.penalties) + .map(|(&reward, &penalty)| reward as i64 - penalty as i64) } -fn compute_altair_inactivity_deltas( - state: &BeaconState, - spec: &ChainSpec, -) -> Result { - let mut deltas = vec![Delta::default(); state.validators().len()]; - altair::rewards_and_penalties::get_inactivity_penalty_deltas( - &mut deltas, - state, - &ParticipationCache::new(state, spec).unwrap(), - spec, - )?; - Ok(convert_altair_deltas(deltas)) +fn optional_deltas_to_total_deltas(d: &Option, len: usize) -> TotalDeltas { + let deltas = if let Some(d) = d { + deltas_to_total_deltas(d).collect() + } else { + vec![0i64; len] + }; + TotalDeltas { deltas } } -fn convert_altair_deltas(deltas: Vec) -> Deltas { - let (rewards, penalties) = deltas.into_iter().map(|d| (d.rewards, d.penalties)).unzip(); - Deltas { rewards, penalties } +fn all_deltas_to_total_deltas(d: &AllDeltas) -> TotalDeltas { + let len = d.source_deltas.rewards.len(); + let deltas = deltas_to_total_deltas(&d.source_deltas) + .zip(deltas_to_total_deltas(&d.target_deltas)) + .zip(deltas_to_total_deltas(&d.head_deltas)) + .zip(optional_deltas_to_total_deltas(&d.inclusion_delay_deltas, len).deltas) + .zip(deltas_to_total_deltas(&d.inactivity_penalty_deltas)) + .map( + |((((source, target), head), inclusion_delay), inactivity_penalty)| { + source + target + head + inclusion_delay + inactivity_penalty + }, + ) + .collect::>(); + TotalDeltas { deltas } +} + +fn compute_altair_deltas( + state: &mut BeaconState, + spec: &ChainSpec, +) -> Result { + // Initialise deltas to pre-state balances. + let mut deltas = state + .balances() + .iter() + .map(|x| *x as i64) + .collect::>(); + altair::process_rewards_and_penalties_slow(state, spec)?; + + for (delta, new_balance) in deltas.iter_mut().zip(state.balances()) { + let old_balance = *delta; + *delta = *new_balance as i64 - old_balance; + } + + Ok(TotalDeltas { deltas }) } diff --git a/testing/ef_tests/src/cases/sanity_blocks.rs b/testing/ef_tests/src/cases/sanity_blocks.rs index cf8e6b5b2f..b0902cb5b7 100644 --- a/testing/ef_tests/src/cases/sanity_blocks.rs +++ b/testing/ef_tests/src/cases/sanity_blocks.rs @@ -7,7 +7,7 @@ use state_processing::{ per_block_processing, per_slot_processing, BlockProcessingError, BlockSignatureStrategy, ConsensusContext, StateProcessingStrategy, VerifyBlockRoot, }; -use types::{BeaconState, EthSpec, ForkName, RelativeEpoch, SignedBeaconBlock}; +use types::{BeaconState, RelativeEpoch, SignedBeaconBlock}; #[derive(Debug, Clone, Deserialize)] pub struct Metadata { diff --git a/testing/ef_tests/src/cases/sanity_slots.rs b/testing/ef_tests/src/cases/sanity_slots.rs index 0da179d536..71c782c78f 100644 --- a/testing/ef_tests/src/cases/sanity_slots.rs +++ b/testing/ef_tests/src/cases/sanity_slots.rs @@ -4,7 +4,7 @@ use crate::case_result::compare_beacon_state_results_without_caches; use crate::decode::{ssz_decode_state, yaml_decode_file}; use serde::Deserialize; use state_processing::per_slot_processing; -use types::{BeaconState, EthSpec, ForkName}; +use types::BeaconState; #[derive(Debug, Clone, Default, Deserialize)] pub struct Metadata { diff --git a/testing/ef_tests/src/cases/shuffling.rs b/testing/ef_tests/src/cases/shuffling.rs index e05763c2d8..184b9e9d26 100644 --- a/testing/ef_tests/src/cases/shuffling.rs +++ b/testing/ef_tests/src/cases/shuffling.rs @@ -4,29 +4,28 @@ use crate::decode::yaml_decode_file; use serde::Deserialize; use std::marker::PhantomData; use swap_or_not_shuffle::{compute_shuffled_index, shuffle_list}; -use types::ForkName; #[derive(Debug, Clone, Deserialize)] -pub struct Shuffling { +pub struct Shuffling { pub seed: String, pub count: usize, pub mapping: Vec, #[serde(skip)] - _phantom: PhantomData, + _phantom: PhantomData, } -impl LoadCase for Shuffling { +impl LoadCase for Shuffling { fn load_from_dir(path: &Path, _fork_name: ForkName) -> Result { yaml_decode_file(&path.join("mapping.yaml")) } } -impl Case for Shuffling { +impl Case for Shuffling { fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { if self.count == 0 { compare_result::<_, Error>(&Ok(vec![]), &Some(self.mapping.clone()))?; } else { - let spec = T::default_spec(); + let spec = E::default_spec(); let seed = hex::decode(&self.seed[2..]) .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; diff --git a/testing/ef_tests/src/cases/ssz_generic.rs b/testing/ef_tests/src/cases/ssz_generic.rs index d6c764f52b..bb2465aae1 100644 --- a/testing/ef_tests/src/cases/ssz_generic.rs +++ b/testing/ef_tests/src/cases/ssz_generic.rs @@ -1,16 +1,14 @@ #![allow(non_snake_case)] use super::*; -use crate::cases::common::{SszStaticType, TestU128, TestU256}; -use crate::cases::ssz_static::{check_serialization, check_tree_hash}; +use crate::cases::common::{TestU128, TestU256}; use crate::decode::{snappy_decode_file, yaml_decode_file}; use serde::Deserialize; use serde::{de::Error as SerdeError, Deserializer}; use ssz_derive::{Decode, Encode}; -use std::path::{Path, PathBuf}; use tree_hash_derive::TreeHash; use types::typenum::*; -use types::{BitList, BitVector, FixedVector, ForkName, VariableList}; +use types::{BitList, BitVector, FixedVector, VariableList}; #[derive(Debug, Clone, Deserialize)] struct Metadata { diff --git a/testing/ef_tests/src/cases/ssz_static.rs b/testing/ef_tests/src/cases/ssz_static.rs index 423dc31528..e41c90c6e0 100644 --- a/testing/ef_tests/src/cases/ssz_static.rs +++ b/testing/ef_tests/src/cases/ssz_static.rs @@ -1,11 +1,10 @@ use super::*; use crate::case_result::compare_result; -use crate::cases::common::SszStaticType; use crate::decode::{snappy_decode_file, yaml_decode_file}; use serde::Deserialize; use ssz::Decode; use tree_hash::TreeHash; -use types::{BeaconBlock, BeaconState, ForkName, Hash256, SignedBeaconBlock}; +use types::{BeaconBlock, BeaconState, Hash256, SignedBeaconBlock}; #[derive(Debug, Clone, Deserialize)] struct SszStaticRoots { diff --git a/testing/ef_tests/src/cases/transition.rs b/testing/ef_tests/src/cases/transition.rs index c94ce3a23a..927589948a 100644 --- a/testing/ef_tests/src/cases/transition.rs +++ b/testing/ef_tests/src/cases/transition.rs @@ -7,7 +7,7 @@ use state_processing::{ ConsensusContext, StateProcessingStrategy, VerifyBlockRoot, }; use std::str::FromStr; -use types::{BeaconState, Epoch, ForkName, SignedBeaconBlock}; +use types::{BeaconState, Epoch, SignedBeaconBlock}; #[derive(Debug, Clone, Deserialize)] pub struct Metadata { @@ -53,6 +53,13 @@ impl LoadCase for TransitionTest { spec.capella_fork_epoch = Some(Epoch::new(0)); spec.deneb_fork_epoch = Some(metadata.fork_epoch); } + ForkName::Electra => { + spec.altair_fork_epoch = Some(Epoch::new(0)); + spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + spec.capella_fork_epoch = Some(Epoch::new(0)); + spec.deneb_fork_epoch = Some(Epoch::new(0)); + spec.electra_fork_epoch = Some(metadata.fork_epoch); + } } // Load blocks diff --git a/testing/ef_tests/src/decode.rs b/testing/ef_tests/src/decode.rs index b5c0da53a0..51ab682f3d 100644 --- a/testing/ef_tests/src/decode.rs +++ b/testing/ef_tests/src/decode.rs @@ -5,7 +5,7 @@ use std::fs::{self}; use std::io::Write; use std::path::Path; use std::path::PathBuf; -use types::{BeaconState, EthSpec}; +use types::BeaconState; /// See `log_file_access` for details. const ACCESSED_FILE_LOG_FILENAME: &str = ".accessed_file_log.txt"; @@ -71,9 +71,7 @@ where f(&bytes).map_err(|e| { match e { // NOTE: this is a bit hacky, but seemingly better than the alternatives - ssz::DecodeError::BytesInvalid(message) - if message.contains("Blst") || message.contains("Milagro") => - { + ssz::DecodeError::BytesInvalid(message) if message.contains("Blst") => { Error::InvalidBLSInput(message) } e => Error::FailedToParseTest(format!( diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index 0295ff1bd4..59b5cb6ba7 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -18,13 +18,20 @@ pub trait Handler { fn handler_name(&self) -> String; + // Add forks here to exclude them from EF spec testing. Helpful for adding future or + // unspecified forks. + // TODO(electra): Enable Electra once spec tests are available. + fn disabled_forks(&self) -> Vec { + vec![ForkName::Electra] + } + fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { Self::Case::is_enabled_for_fork(fork_name) } fn run(&self) { for fork_name in ForkName::list_all() { - if self.is_enabled_for_fork(fork_name) { + if !self.disabled_forks().contains(&fork_name) && self.is_enabled_for_fork(fork_name) { self.run_for_fork(fork_name) } } diff --git a/testing/ef_tests/src/type_name.rs b/testing/ef_tests/src/type_name.rs index 13121854ac..ef5d7eb001 100644 --- a/testing/ef_tests/src/type_name.rs +++ b/testing/ef_tests/src/type_name.rs @@ -73,6 +73,38 @@ type_name!(Fork); type_name!(ForkData); type_name_generic!(HistoricalBatch); type_name_generic!(IndexedAttestation); +type_name_generic!(LightClientBootstrap); +type_name_generic!(LightClientBootstrapAltair, "LightClientBootstrap"); +type_name_generic!(LightClientBootstrapCapella, "LightClientBootstrap"); +type_name_generic!(LightClientBootstrapDeneb, "LightClientBootstrap"); +type_name_generic!(LightClientFinalityUpdate); +type_name_generic!(LightClientFinalityUpdateAltair, "LightClientFinalityUpdate"); +type_name_generic!( + LightClientFinalityUpdateCapella, + "LightClientFinalityUpdate" +); +type_name_generic!(LightClientFinalityUpdateDeneb, "LightClientFinalityUpdate"); +type_name_generic!(LightClientHeader); +type_name_generic!(LightClientHeaderDeneb, "LightClientHeader"); +type_name_generic!(LightClientHeaderCapella, "LightClientHeader"); +type_name_generic!(LightClientHeaderAltair, "LightClientHeader"); +type_name_generic!(LightClientOptimisticUpdate); +type_name_generic!( + LightClientOptimisticUpdateAltair, + "LightClientOptimisticUpdate" +); +type_name_generic!( + LightClientOptimisticUpdateCapella, + "LightClientOptimisticUpdate" +); +type_name_generic!( + LightClientOptimisticUpdateDeneb, + "LightClientOptimisticUpdate" +); +type_name_generic!(LightClientUpdate); +type_name_generic!(LightClientUpdateAltair, "LightClientUpdate"); +type_name_generic!(LightClientUpdateCapella, "LightClientUpdate"); +type_name_generic!(LightClientUpdateDeneb, "LightClientUpdate"); type_name_generic!(PendingAttestation); type_name!(ProposerSlashing); type_name_generic!(SignedAggregateAndProof); diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index dd25dba8b6..3093239f7f 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -1,6 +1,6 @@ #![cfg(feature = "ef_tests")] -use ef_tests::{KzgInclusionMerkleProofValidityHandler, *}; +use ef_tests::*; use types::{MainnetEthSpec, MinimalEthSpec, *}; // Check that the hand-computed multiplications on EthSpec are correctly computed. @@ -217,7 +217,7 @@ mod ssz_static { use ef_tests::{Handler, SszStaticHandler, SszStaticTHCHandler, SszStaticWithSpecHandler}; use types::blob_sidecar::BlobIdentifier; use types::historical_summary::HistoricalSummary; - use types::*; + use types::{LightClientBootstrapAltair, *}; ssz_static_test!(aggregate_and_proof, AggregateAndProof<_>); ssz_static_test!(attestation, Attestation<_>); @@ -236,7 +236,6 @@ mod ssz_static { ssz_static_test!(fork_data, ForkData); ssz_static_test!(historical_batch, HistoricalBatch<_>); ssz_static_test!(indexed_attestation, IndexedAttestation<_>); - // NOTE: LightClient* intentionally omitted ssz_static_test!(pending_attestation, PendingAttestation<_>); ssz_static_test!(proposer_slashing, ProposerSlashing); ssz_static_test!(signed_aggregate_and_proof, SignedAggregateAndProof<_>); @@ -250,7 +249,6 @@ mod ssz_static { ssz_static_test!(signing_data, SigningData); ssz_static_test!(validator, Validator); ssz_static_test!(voluntary_exit, VoluntaryExit); - // BeaconBlockBody has no internal indicator of which fork it is for, so we test it separately. #[test] fn beacon_block_body() { @@ -285,6 +283,135 @@ mod ssz_static { .run(); } + // LightClientBootstrap has no internal indicator of which fork it is for, so we test it separately. + #[test] + fn light_client_bootstrap() { + SszStaticHandler::, MinimalEthSpec>::altair_only() + .run(); + SszStaticHandler::, MainnetEthSpec>::altair_only() + .run(); + SszStaticHandler::, MinimalEthSpec>::merge_only( + ) + .run(); + SszStaticHandler::, MainnetEthSpec>::merge_only( + ) + .run(); + SszStaticHandler::, MinimalEthSpec>::capella_only() + .run(); + SszStaticHandler::, MainnetEthSpec>::capella_only() + .run(); + SszStaticHandler::, MinimalEthSpec>::deneb_only() + .run(); + SszStaticHandler::, MainnetEthSpec>::deneb_only() + .run(); + } + + // LightClientHeader has no internal indicator of which fork it is for, so we test it separately. + #[test] + fn light_client_header() { + SszStaticHandler::, MinimalEthSpec>::altair_only() + .run(); + SszStaticHandler::, MainnetEthSpec>::altair_only() + .run(); + SszStaticHandler::, MinimalEthSpec>::merge_only() + .run(); + SszStaticHandler::, MainnetEthSpec>::merge_only() + .run(); + + SszStaticHandler::, MinimalEthSpec>::capella_only( + ) + .run(); + SszStaticHandler::, MainnetEthSpec>::capella_only( + ) + .run(); + + SszStaticHandler::, MinimalEthSpec>::deneb_only() + .run(); + SszStaticHandler::, MainnetEthSpec>::deneb_only() + .run(); + } + + // LightClientOptimisticUpdate has no internal indicator of which fork it is for, so we test it separately. + #[test] + fn light_client_optimistic_update() { + SszStaticHandler::, MinimalEthSpec>::altair_only( + ) + .run(); + SszStaticHandler::, MainnetEthSpec>::altair_only( + ) + .run(); + SszStaticHandler::, MinimalEthSpec>::merge_only( + ) + .run(); + SszStaticHandler::, MainnetEthSpec>::merge_only( + ) + .run(); + SszStaticHandler::, MinimalEthSpec>::capella_only( + ) + .run(); + SszStaticHandler::, MainnetEthSpec>::capella_only( + ) + .run(); + SszStaticHandler::, MinimalEthSpec>::deneb_only( + ) + .run(); + SszStaticHandler::, MainnetEthSpec>::deneb_only( + ) + .run(); + } + + // LightClientFinalityUpdate has no internal indicator of which fork it is for, so we test it separately. + #[test] + fn light_client_finality_update() { + SszStaticHandler::, MinimalEthSpec>::altair_only( + ) + .run(); + SszStaticHandler::, MainnetEthSpec>::altair_only( + ) + .run(); + SszStaticHandler::, MinimalEthSpec>::merge_only( + ) + .run(); + SszStaticHandler::, MainnetEthSpec>::merge_only( + ) + .run(); + SszStaticHandler::, MinimalEthSpec>::capella_only( + ) + .run(); + SszStaticHandler::, MainnetEthSpec>::capella_only( + ) + .run(); + SszStaticHandler::, MinimalEthSpec>::deneb_only( + ) + .run(); + SszStaticHandler::, MainnetEthSpec>::deneb_only( + ) + .run(); + } + + // LightClientUpdate has no internal indicator of which fork it is for, so we test it separately. + #[test] + fn light_client_update() { + SszStaticHandler::, MinimalEthSpec>::altair_only() + .run(); + SszStaticHandler::, MainnetEthSpec>::altair_only() + .run(); + SszStaticHandler::, MinimalEthSpec>::merge_only() + .run(); + SszStaticHandler::, MainnetEthSpec>::merge_only() + .run(); + SszStaticHandler::, MinimalEthSpec>::capella_only( + ) + .run(); + SszStaticHandler::, MainnetEthSpec>::capella_only( + ) + .run(); + SszStaticHandler::, MinimalEthSpec>::deneb_only() + .run(); + SszStaticHandler::, MainnetEthSpec>::deneb_only() + .run(); + } + #[test] fn signed_contribution_and_proof() { SszStaticHandler::, MinimalEthSpec>::altair_and_later().run(); @@ -605,6 +732,7 @@ fn merkle_proof_validity() { } #[test] +#[cfg(feature = "fake_crypto")] fn kzg_inclusion_merkle_proof_validity() { KzgInclusionMerkleProofValidityHandler::::default().run(); KzgInclusionMerkleProofValidityHandler::::default().run(); diff --git a/testing/eth1_test_rig/src/anvil.rs b/testing/eth1_test_rig/src/anvil.rs index 1b86711c2f..c6c37ae4a7 100644 --- a/testing/eth1_test_rig/src/anvil.rs +++ b/testing/eth1_test_rig/src/anvil.rs @@ -1,7 +1,6 @@ use ethers_core::utils::{Anvil, AnvilInstance}; use ethers_providers::{Http, Middleware, Provider}; use serde_json::json; -use std::convert::TryFrom; use unused_port::unused_tcp4_port; /// Provides a dedicated `anvil` instance. diff --git a/testing/execution_engine_integration/Cargo.toml b/testing/execution_engine_integration/Cargo.toml index 6de108fcb6..7f66658f0f 100644 --- a/testing/execution_engine_integration/Cargo.toml +++ b/testing/execution_engine_integration/Cargo.toml @@ -4,12 +4,12 @@ version = "0.1.0" edition = { workspace = true } [dependencies] +async-channel = { workspace = true } 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 } @@ -24,4 +24,4 @@ fork_choice = { workspace = true } logging = { workspace = true } [features] -portable = ["types/portable"] \ No newline at end of file +portable = ["types/portable"] diff --git a/testing/execution_engine_integration/src/test_rig.rs b/testing/execution_engine_integration/src/test_rig.rs index b0701e80a1..8f782c7e4e 100644 --- a/testing/execution_engine_integration/src/test_rig.rs +++ b/testing/execution_engine_integration/src/test_rig.rs @@ -24,25 +24,25 @@ const EXECUTION_ENGINE_START_TIMEOUT: Duration = Duration::from_secs(60); const TEST_FORK: ForkName = ForkName::Capella; -struct ExecutionPair { +struct ExecutionPair { /// The Lighthouse `ExecutionLayer` struct, connected to the `execution_engine` via HTTP. - execution_layer: ExecutionLayer, + execution_layer: ExecutionLayer, /// A handle to external EE process, once this is dropped the process will be killed. #[allow(dead_code)] - execution_engine: ExecutionEngine, + execution_engine: ExecutionEngine, } /// A rig that holds two EE processes for testing. /// /// There are two EEs held here so that we can test out-of-order application of payloads, and other /// edge-cases. -pub struct TestRig { +pub struct TestRig { #[allow(dead_code)] runtime: Arc, - ee_a: ExecutionPair, - ee_b: ExecutionPair, + ee_a: ExecutionPair, + ee_b: ExecutionPair, spec: ChainSpec, - _runtime_shutdown: exit_future::Signal, + _runtime_shutdown: async_channel::Sender<()>, } /// Import a private key into the execution engine and unlock it so that we can @@ -102,8 +102,8 @@ async fn import_and_unlock(http_url: SensitiveUrl, priv_keys: &[&str], password: } } -impl TestRig { - pub fn new(generic_engine: E) -> Self { +impl TestRig { + pub fn new(generic_engine: Engine) -> Self { let log = logging::test_logger(); let runtime = Arc::new( tokio::runtime::Builder::new_multi_thread() @@ -111,7 +111,7 @@ impl TestRig { .build() .unwrap(), ); - let (runtime_shutdown, exit) = exit_future::signal(); + let (runtime_shutdown, exit) = async_channel::bounded(1); let (shutdown_tx, _) = futures::channel::mpsc::channel(1); let executor = TaskExecutor::new(Arc::downgrade(&runtime), exit, log.clone(), shutdown_tx); let mut spec = TEST_FORK.make_genesis_spec(MainnetEthSpec::default_spec()); @@ -381,7 +381,7 @@ impl TestRig { let status = self .ee_a .execution_layer - .notify_new_payload(valid_payload.clone().try_into().unwrap()) + .notify_new_payload(valid_payload.to_ref().try_into().unwrap()) .await .unwrap(); assert_eq!(status, PayloadStatus::Valid); @@ -435,7 +435,7 @@ impl TestRig { let status = self .ee_a .execution_layer - .notify_new_payload(invalid_payload.try_into().unwrap()) + .notify_new_payload(invalid_payload.to_ref().try_into().unwrap()) .await .unwrap(); assert!(matches!( @@ -507,7 +507,7 @@ impl TestRig { let status = self .ee_a .execution_layer - .notify_new_payload(second_payload.clone().try_into().unwrap()) + .notify_new_payload(second_payload.to_ref().try_into().unwrap()) .await .unwrap(); assert_eq!(status, PayloadStatus::Valid); @@ -559,7 +559,7 @@ impl TestRig { let status = self .ee_b .execution_layer - .notify_new_payload(second_payload.clone().try_into().unwrap()) + .notify_new_payload(second_payload.to_ref().try_into().unwrap()) .await .unwrap(); assert!(matches!(status, PayloadStatus::Syncing)); @@ -597,7 +597,7 @@ impl TestRig { let status = self .ee_b .execution_layer - .notify_new_payload(valid_payload.clone().try_into().unwrap()) + .notify_new_payload(valid_payload.to_ref().try_into().unwrap()) .await .unwrap(); assert_eq!(status, PayloadStatus::Valid); @@ -611,7 +611,7 @@ impl TestRig { let status = self .ee_b .execution_layer - .notify_new_payload(second_payload.clone().try_into().unwrap()) + .notify_new_payload(second_payload.to_ref().try_into().unwrap()) .await .unwrap(); assert_eq!(status, PayloadStatus::Valid); diff --git a/testing/network_testing/README.md b/testing/network_testing/README.md index 3591514682..f97c3cff28 100644 --- a/testing/network_testing/README.md +++ b/testing/network_testing/README.md @@ -60,7 +60,7 @@ https://prater.checkpoint.sigp.io --execution-endpoint http://localhost:8551 Mainnet: ``` -$ lighthouse --network prater bn --execution-jwt /tmp/mockel.jwt --checkpoint-sync-url +$ lighthouse --network mainnet bn --execution-jwt /tmp/mockel.jwt --checkpoint-sync-url https://checkpoint.sigp.io --execution-endpoint http://localhost:8551 ``` diff --git a/testing/node_test_rig/src/lib.rs b/testing/node_test_rig/src/lib.rs index 6c9af707f5..3320898642 100644 --- a/testing/node_test_rig/src/lib.rs +++ b/testing/node_test_rig/src/lib.rs @@ -181,8 +181,8 @@ impl ValidatorFiles { /// is _local_ to this process). /// /// Intended for use in testing and simulation. Not for production. -pub struct LocalValidatorClient { - pub client: ProductionValidatorClient, +pub struct LocalValidatorClient { + pub client: ProductionValidatorClient, pub files: ValidatorFiles, } diff --git a/testing/simulator/src/checks.rs b/testing/simulator/src/checks.rs index 3f603af669..eab5fc425a 100644 --- a/testing/simulator/src/checks.rs +++ b/testing/simulator/src/checks.rs @@ -288,13 +288,13 @@ pub(crate) async fn verify_light_client_updates( } // Verify light client optimistic update. `signature_slot_distance` should be 1 in the ideal scenario. - let signature_slot = client + let signature_slot = *client .get_beacon_light_client_optimistic_update::() .await .map_err(|e| format!("Error while getting light client updates: {:?}", e))? .ok_or(format!("Light client optimistic update not found {slot:?}"))? .data - .signature_slot; + .signature_slot(); let signature_slot_distance = slot - signature_slot; if signature_slot_distance > light_client_update_slot_tolerance { return Err(format!("Existing optimistic update too old: signature slot {signature_slot}, current slot {slot:?}")); @@ -317,13 +317,13 @@ pub(crate) async fn verify_light_client_updates( } continue; } - let signature_slot = client + let signature_slot = *client .get_beacon_light_client_finality_update::() .await .map_err(|e| format!("Error while getting light client updates: {:?}", e))? .ok_or(format!("Light client finality update not found {slot:?}"))? .data - .signature_slot; + .signature_slot(); let signature_slot_distance = slot - signature_slot; if signature_slot_distance > light_client_update_slot_tolerance { return Err(format!( diff --git a/testing/simulator/src/retry.rs b/testing/simulator/src/retry.rs index a4eb52cea1..ad85b74236 100644 --- a/testing/simulator/src/retry.rs +++ b/testing/simulator/src/retry.rs @@ -4,10 +4,10 @@ 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(max_retries: usize, mut func: F) -> Result +pub async fn with_retry(max_retries: usize, mut func: F) -> Result where - F: FnMut() -> Pin>>>, - E: Debug, + F: FnMut() -> Pin>>>, + U: Debug, { let mut retry_count = 0; loop { diff --git a/testing/simulator/src/sync_sim.rs b/testing/simulator/src/sync_sim.rs index 78f7e1ee9f..ba4ea4530a 100644 --- a/testing/simulator/src/sync_sim.rs +++ b/testing/simulator/src/sync_sim.rs @@ -94,8 +94,6 @@ fn syncing_sim( beacon_config.dummy_eth1_backend = true; beacon_config.sync_eth1_chain = true; - beacon_config.http_api.allow_sync_stalled = true; - beacon_config.network.enr_address = (Some(Ipv4Addr::LOCALHOST), None); // Generate the directories and keystores required for the validator clients. diff --git a/testing/state_transition_vectors/src/exit.rs b/testing/state_transition_vectors/src/exit.rs index 50b98d3066..e3cd346da1 100644 --- a/testing/state_transition_vectors/src/exit.rs +++ b/testing/state_transition_vectors/src/exit.rs @@ -1,10 +1,9 @@ use super::*; -use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; use state_processing::{ per_block_processing, per_block_processing::errors::ExitInvalid, BlockProcessingError, BlockSignatureStrategy, ConsensusContext, StateProcessingStrategy, VerifyBlockRoot, }; -use types::{BeaconBlock, BeaconState, Epoch, EthSpec, SignedBeaconBlock}; +use types::{BeaconBlock, Epoch}; // Default validator index to exit. pub const VALIDATOR_INDEX: u64 = 0; diff --git a/testing/web3signer_tests/Cargo.toml b/testing/web3signer_tests/Cargo.toml index 38b775b392..1bdf62cd22 100644 --- a/testing/web3signer_tests/Cargo.toml +++ b/testing/web3signer_tests/Cargo.toml @@ -8,6 +8,7 @@ edition = { workspace = true } [dependencies] [dev-dependencies] +async-channel = { workspace = true } eth2_keystore = { workspace = true } types = { workspace = true } tempfile = { workspace = true } @@ -17,7 +18,6 @@ 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 } diff --git a/testing/web3signer_tests/src/lib.rs b/testing/web3signer_tests/src/lib.rs index 6f3536fe46..3090b4da55 100644 --- a/testing/web3signer_tests/src/lib.rs +++ b/testing/web3signer_tests/src/lib.rs @@ -45,7 +45,7 @@ mod tests { initialized_validators::{ load_pem_certificate, load_pkcs12_identity, InitializedValidators, }, - validator_store::ValidatorStore, + validator_store::{Error as ValidatorStoreError, ValidatorStore}, SlashingDatabase, SLASHING_PROTECTION_FILENAME, }; @@ -157,6 +157,18 @@ mod tests { } } + #[derive(Debug, Clone, Copy)] + struct SlashingProtectionConfig { + /// Whether to enable slashing protection for web3signer keys locally within Lighthouse. + local: bool, + } + + impl Default for SlashingProtectionConfig { + fn default() -> Self { + SlashingProtectionConfig { local: true } + } + } + impl Web3SignerRig { pub async fn new(network: &str, listen_address: &str, listen_port: u16) -> Self { GET_WEB3SIGNER_BIN @@ -231,6 +243,8 @@ mod tests { )) .arg("eth2") .arg(format!("--network={}", network)) + // Can't *easily* test `--slashing-protection-enabled=true` because web3signer + // requires a Postgres instance. .arg("--slashing-protection-enabled=false") .stdout(stdio()) .stderr(stdio()) @@ -293,18 +307,26 @@ mod tests { validator_store: Arc>, _validator_dir: TempDir, runtime: Arc, - _runtime_shutdown: exit_future::Signal, + _runtime_shutdown: async_channel::Sender<()>, + using_web3signer: bool, } impl ValidatorStoreRig { - pub async fn new(validator_definitions: Vec, spec: ChainSpec) -> Self { + pub async fn new( + validator_definitions: Vec, + slashing_protection_config: SlashingProtectionConfig, + using_web3signer: bool, + spec: ChainSpec, + ) -> Self { let log = environment::null_logger().unwrap(); let validator_dir = TempDir::new().unwrap(); + let config = validator_client::Config::default(); let validator_definitions = ValidatorDefinitions::from(validator_definitions); let initialized_validators = InitializedValidators::from_definitions( validator_definitions, validator_dir.path().into(), + config.clone(), log.clone(), ) .await @@ -318,7 +340,7 @@ mod tests { .build() .unwrap(), ); - let (runtime_shutdown, exit) = exit_future::signal(); + let (runtime_shutdown, exit) = async_channel::bounded(1); let (shutdown_tx, _) = futures::channel::mpsc::channel(1); let executor = TaskExecutor::new(Arc::downgrade(&runtime), exit, log.clone(), shutdown_tx); @@ -331,7 +353,10 @@ mod tests { let slot_clock = TestingSlotClock::new(Slot::new(0), Duration::from_secs(0), Duration::from_secs(1)); - let config = validator_client::Config::default(); + let config = validator_client::Config { + enable_web3signer_slashing_protection: slashing_protection_config.local, + ..Default::default() + }; let validator_store = ValidatorStore::<_, E>::new( initialized_validators, @@ -350,6 +375,7 @@ mod tests { _validator_dir: validator_dir, runtime, _runtime_shutdown: runtime_shutdown, + using_web3signer, } } @@ -378,7 +404,12 @@ mod tests { } impl TestingRig { - pub async fn new(network: &str, spec: ChainSpec, listen_port: u16) -> Self { + pub async fn new( + network: &str, + slashing_protection_config: SlashingProtectionConfig, + spec: ChainSpec, + listen_port: u16, + ) -> Self { let signer_rig = Web3SignerRig::new(network, WEB3SIGNER_LISTEN_ADDRESS, listen_port).await; let validator_pubkey = signer_rig.keypair.pk.clone(); @@ -400,7 +431,13 @@ mod tests { voting_keystore_password: Some(KEYSTORE_PASSWORD.to_string().into()), }, }; - ValidatorStoreRig::new(vec![validator_definition], spec.clone()).await + ValidatorStoreRig::new( + vec![validator_definition], + slashing_protection_config, + false, + spec.clone(), + ) + .await }; let remote_signer_validator_store = { @@ -422,7 +459,13 @@ mod tests { client_identity_password: Some(client_identity_password()), }), }; - ValidatorStoreRig::new(vec![validator_definition], spec).await + ValidatorStoreRig::new( + vec![validator_definition], + slashing_protection_config, + true, + spec, + ) + .await }; Self { @@ -465,6 +508,36 @@ mod tests { assert!(prev_signature.is_some(), "sanity check"); self } + + /// Assert that a slashable message fails to be signed locally and is either signed or not + /// by the web3signer rig depending on the value of `web3signer_should_sign`. + pub async fn assert_slashable_message_should_sign( + self, + case_name: &str, + generate_sig: F, + web3signer_should_sign: bool, + ) -> Self + where + F: Fn(PublicKeyBytes, Arc>) -> R, + R: Future>, + { + for validator_rig in &self.validator_rigs { + let result = + generate_sig(self.validator_pubkey, validator_rig.validator_store.clone()) + .await; + + if !validator_rig.using_web3signer || !web3signer_should_sign { + let err = result.unwrap_err(); + assert!( + matches!(err, ValidatorStoreError::Slashable(_)), + "should not sign slashable {case_name}" + ); + } else { + assert_eq!(result, Ok(()), "should sign slashable {case_name}"); + } + } + self + } } /// Get a generic, arbitrary attestation for signing. @@ -503,64 +576,69 @@ mod tests { let network_config = Eth2NetworkConfig::constant(network).unwrap().unwrap(); let spec = &network_config.chain_spec::().unwrap(); - TestingRig::new(network, spec.clone(), listen_port) - .await - .assert_signatures_match("randao_reveal", |pubkey, validator_store| async move { + TestingRig::new( + network, + SlashingProtectionConfig::default(), + spec.clone(), + listen_port, + ) + .await + .assert_signatures_match("randao_reveal", |pubkey, validator_store| async move { + validator_store + .randao_reveal(pubkey, Epoch::new(0)) + .await + .unwrap() + }) + .await + .assert_signatures_match("beacon_block_base", |pubkey, validator_store| async move { + let block = BeaconBlock::Base(BeaconBlockBase::empty(spec)); + let block_slot = block.slot(); + validator_store + .sign_block(pubkey, block, block_slot) + .await + .unwrap() + }) + .await + .assert_signatures_match("attestation", |pubkey, validator_store| async move { + let mut attestation = get_attestation(); + validator_store + .sign_attestation(pubkey, 0, &mut attestation, Epoch::new(0)) + .await + .unwrap(); + attestation + }) + .await + .assert_signatures_match("signed_aggregate", |pubkey, validator_store| async move { + let attestation = get_attestation(); + validator_store + .produce_signed_aggregate_and_proof( + pubkey, + 0, + attestation, + SelectionProof::from(Signature::empty()), + ) + .await + .unwrap() + }) + .await + .assert_signatures_match("selection_proof", |pubkey, validator_store| async move { + validator_store + .produce_selection_proof(pubkey, Slot::new(0)) + .await + .unwrap() + }) + .await + .assert_signatures_match( + "validator_registration", + |pubkey, validator_store| async move { + let val_reg_data = get_validator_registration(pubkey); validator_store - .randao_reveal(pubkey, Epoch::new(0)) + .sign_validator_registration_data(val_reg_data) .await .unwrap() - }) - .await - .assert_signatures_match("beacon_block_base", |pubkey, validator_store| async move { - let block = BeaconBlock::Base(BeaconBlockBase::empty(spec)); - let block_slot = block.slot(); - validator_store - .sign_block(pubkey, block, block_slot) - .await - .unwrap() - }) - .await - .assert_signatures_match("attestation", |pubkey, validator_store| async move { - let mut attestation = get_attestation(); - validator_store - .sign_attestation(pubkey, 0, &mut attestation, Epoch::new(0)) - .await - .unwrap(); - attestation - }) - .await - .assert_signatures_match("signed_aggregate", |pubkey, validator_store| async move { - let attestation = get_attestation(); - validator_store - .produce_signed_aggregate_and_proof( - pubkey, - 0, - attestation, - SelectionProof::from(Signature::empty()), - ) - .await - .unwrap() - }) - .await - .assert_signatures_match("selection_proof", |pubkey, validator_store| async move { - validator_store - .produce_selection_proof(pubkey, Slot::new(0)) - .await - .unwrap() - }) - .await - .assert_signatures_match( - "validator_registration", - |pubkey, validator_store| async move { - let val_reg_data = get_validator_registration(pubkey); - validator_store - .sign_validator_registration_data(val_reg_data) - .await - .unwrap() - }, - ) - .await; + }, + ) + .await; } /// Test all the Altair types. @@ -572,82 +650,78 @@ mod tests { .unwrap() .start_slot(E::slots_per_epoch()); - TestingRig::new(network, spec.clone(), listen_port) - .await - .assert_signatures_match( - "beacon_block_altair", - |pubkey, validator_store| async move { - let mut altair_block = BeaconBlockAltair::empty(spec); - altair_block.slot = altair_fork_slot; - validator_store - .sign_block(pubkey, BeaconBlock::Altair(altair_block), altair_fork_slot) - .await - .unwrap() - }, - ) - .await - .assert_signatures_match( - "sync_selection_proof", - |pubkey, validator_store| async move { - validator_store - .produce_sync_selection_proof( - &pubkey, - altair_fork_slot, - SyncSubnetId::from(0), - ) - .await - .unwrap() - }, - ) - .await - .assert_signatures_match( - "sync_committee_signature", - |pubkey, validator_store| async move { - validator_store - .produce_sync_committee_signature( - altair_fork_slot, - Hash256::zero(), - 0, - &pubkey, - ) - .await - .unwrap() - }, - ) - .await - .assert_signatures_match( - "signed_contribution_and_proof", - |pubkey, validator_store| async move { - let contribution = SyncCommitteeContribution { - slot: altair_fork_slot, - beacon_block_root: <_>::default(), - subcommittee_index: <_>::default(), - aggregation_bits: <_>::default(), - signature: AggregateSignature::empty(), - }; - validator_store - .produce_signed_contribution_and_proof( - 0, - pubkey, - contribution, - SyncSelectionProof::from(Signature::empty()), - ) - .await - .unwrap() - }, - ) - .await - .assert_signatures_match( - "validator_registration", - |pubkey, validator_store| async move { - let val_reg_data = get_validator_registration(pubkey); - validator_store - .sign_validator_registration_data(val_reg_data) - .await - .unwrap() - }, - ) - .await; + TestingRig::new( + network, + SlashingProtectionConfig::default(), + spec.clone(), + listen_port, + ) + .await + .assert_signatures_match( + "beacon_block_altair", + |pubkey, validator_store| async move { + let mut altair_block = BeaconBlockAltair::empty(spec); + altair_block.slot = altair_fork_slot; + validator_store + .sign_block(pubkey, BeaconBlock::Altair(altair_block), altair_fork_slot) + .await + .unwrap() + }, + ) + .await + .assert_signatures_match( + "sync_selection_proof", + |pubkey, validator_store| async move { + validator_store + .produce_sync_selection_proof(&pubkey, altair_fork_slot, SyncSubnetId::from(0)) + .await + .unwrap() + }, + ) + .await + .assert_signatures_match( + "sync_committee_signature", + |pubkey, validator_store| async move { + validator_store + .produce_sync_committee_signature(altair_fork_slot, Hash256::zero(), 0, &pubkey) + .await + .unwrap() + }, + ) + .await + .assert_signatures_match( + "signed_contribution_and_proof", + |pubkey, validator_store| async move { + let contribution = SyncCommitteeContribution { + slot: altair_fork_slot, + beacon_block_root: <_>::default(), + subcommittee_index: <_>::default(), + aggregation_bits: <_>::default(), + signature: AggregateSignature::empty(), + }; + validator_store + .produce_signed_contribution_and_proof( + 0, + pubkey, + contribution, + SyncSelectionProof::from(Signature::empty()), + ) + .await + .unwrap() + }, + ) + .await + .assert_signatures_match( + "validator_registration", + |pubkey, validator_store| async move { + let val_reg_data = get_validator_registration(pubkey); + validator_store + .sign_validator_registration_data(val_reg_data) + .await + .unwrap() + }, + ) + .await; } /// Test all the Merge types. @@ -659,17 +733,154 @@ mod tests { .unwrap() .start_slot(E::slots_per_epoch()); - TestingRig::new(network, spec.clone(), listen_port) - .await - .assert_signatures_match("beacon_block_merge", |pubkey, validator_store| async move { - let mut merge_block = BeaconBlockMerge::empty(spec); - merge_block.slot = merge_fork_slot; + TestingRig::new( + network, + SlashingProtectionConfig::default(), + spec.clone(), + listen_port, + ) + .await + .assert_signatures_match("beacon_block_merge", |pubkey, validator_store| async move { + let mut merge_block = BeaconBlockMerge::empty(spec); + merge_block.slot = merge_fork_slot; + validator_store + .sign_block(pubkey, BeaconBlock::Merge(merge_block), merge_fork_slot) + .await + .unwrap() + }) + .await; + } + + async fn test_lighthouse_slashing_protection( + slashing_protection_config: SlashingProtectionConfig, + listen_port: u16, + ) { + // Run these tests on mainnet. + let network = "mainnet"; + + let network_config = Eth2NetworkConfig::constant(network).unwrap().unwrap(); + let spec = &network_config.chain_spec::().unwrap(); + let merge_fork_slot = spec + .bellatrix_fork_epoch + .unwrap() + .start_slot(E::slots_per_epoch()); + + // The slashable message should only be signed by the web3signer validator if slashing + // protection is disabled in Lighthouse. + let slashable_message_should_sign = !slashing_protection_config.local; + + let first_attestation = || { + let mut attestation = get_attestation(); + attestation.data.source.epoch = Epoch::new(1); + attestation.data.target.epoch = Epoch::new(4); + attestation + }; + + let double_vote_attestation = || { + let mut attestation = first_attestation(); + attestation.data.beacon_block_root = Hash256::from_low_u64_be(1); + attestation + }; + + let surrounding_attestation = || { + let mut attestation = first_attestation(); + attestation.data.source.epoch = Epoch::new(0); + attestation.data.target.epoch = Epoch::new(5); + attestation + }; + + let surrounded_attestation = || { + let mut attestation = first_attestation(); + attestation.data.source.epoch = Epoch::new(2); + attestation.data.target.epoch = Epoch::new(3); + attestation + }; + + let first_block = || { + let mut merge_block = BeaconBlockMerge::empty(spec); + merge_block.slot = merge_fork_slot; + BeaconBlock::Merge(merge_block) + }; + + let double_vote_block = || { + let mut block = first_block(); + *block.state_root_mut() = Hash256::repeat_byte(0xff); + block + }; + + let current_epoch = Epoch::new(5); + + TestingRig::new( + network, + slashing_protection_config, + spec.clone(), + listen_port, + ) + .await + .assert_signatures_match("first_attestation", |pubkey, validator_store| async move { + let mut attestation = first_attestation(); + validator_store + .sign_attestation(pubkey, 0, &mut attestation, current_epoch) + .await + .unwrap(); + attestation + }) + .await + .assert_slashable_message_should_sign( + "double_vote_attestation", + move |pubkey, validator_store| async move { + let mut attestation = double_vote_attestation(); validator_store - .sign_block(pubkey, BeaconBlock::Merge(merge_block), merge_fork_slot) + .sign_attestation(pubkey, 0, &mut attestation, current_epoch) .await - .unwrap() - }) - .await; + }, + slashable_message_should_sign, + ) + .await + .assert_slashable_message_should_sign( + "surrounding_attestation", + move |pubkey, validator_store| async move { + let mut attestation = surrounding_attestation(); + validator_store + .sign_attestation(pubkey, 0, &mut attestation, current_epoch) + .await + }, + slashable_message_should_sign, + ) + .await + .assert_slashable_message_should_sign( + "surrounded_attestation", + move |pubkey, validator_store| async move { + let mut attestation = surrounded_attestation(); + validator_store + .sign_attestation(pubkey, 0, &mut attestation, current_epoch) + .await + }, + slashable_message_should_sign, + ) + .await + .assert_signatures_match("first_block", |pubkey, validator_store| async move { + let block = first_block(); + let slot = block.slot(); + validator_store + .sign_block(pubkey, block, slot) + .await + .unwrap() + }) + .await + .assert_slashable_message_should_sign( + "double_vote_block", + move |pubkey, validator_store| async move { + let block = double_vote_block(); + let slot = block.slot(); + validator_store + .sign_block(pubkey, block, slot) + .await + .map(|_| ()) + }, + slashable_message_should_sign, + ) + .await; } #[tokio::test] @@ -706,4 +917,14 @@ mod tests { async fn sepolia_merge_types() { test_merge_types("sepolia", 4252).await } + + #[tokio::test] + async fn slashing_protection_disabled_locally() { + test_lighthouse_slashing_protection(SlashingProtectionConfig { local: false }, 4253).await + } + + #[tokio::test] + async fn slashing_protection_enabled_locally() { + test_lighthouse_slashing_protection(SlashingProtectionConfig { local: true }, 4254).await + } } diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index 90a82b7e3b..011243e414 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -30,7 +30,6 @@ 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 } @@ -51,7 +50,6 @@ 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 } @@ -62,3 +60,4 @@ sysinfo = { workspace = true } system_health = { path = "../common/system_health" } logging = { workspace = true } strum = { workspace = true } +itertools = { workspace = true } diff --git a/validator_client/slashing_protection/src/attestation_tests.rs b/validator_client/slashing_protection/src/attestation_tests.rs index c66a67b70a..a162c4e150 100644 --- a/validator_client/slashing_protection/src/attestation_tests.rs +++ b/validator_client/slashing_protection/src/attestation_tests.rs @@ -2,7 +2,7 @@ use crate::test_utils::*; use crate::*; -use types::{AttestationData, Checkpoint, Epoch, Hash256, Slot}; +use types::{AttestationData, Checkpoint, Epoch, Slot}; pub fn build_checkpoint(epoch_num: u64) -> Checkpoint { Checkpoint { diff --git a/validator_client/slashing_protection/src/block_tests.rs b/validator_client/slashing_protection/src/block_tests.rs index a1f634ef07..abd452a0b6 100644 --- a/validator_client/slashing_protection/src/block_tests.rs +++ b/validator_client/slashing_protection/src/block_tests.rs @@ -2,7 +2,7 @@ use super::*; use crate::test_utils::*; -use types::{BeaconBlockHeader, Hash256, Slot}; +use types::{BeaconBlockHeader, Slot}; pub fn block(slot: u64) -> BeaconBlockHeader { BeaconBlockHeader { diff --git a/validator_client/slashing_protection/src/lib.rs b/validator_client/slashing_protection/src/lib.rs index 1610b52372..c4fa32b611 100644 --- a/validator_client/slashing_protection/src/lib.rs +++ b/validator_client/slashing_protection/src/lib.rs @@ -17,8 +17,8 @@ pub use crate::slashing_database::{ SUPPORTED_INTERCHANGE_FORMAT_VERSION, }; use rusqlite::Error as SQLError; +use std::fmt::Display; use std::io::{Error as IOError, ErrorKind}; -use std::string::ToString; use types::{Hash256, PublicKeyBytes}; /// The filename within the `validators` directory that contains the slashing protection DB. @@ -122,9 +122,9 @@ impl From for NotSafe { } } -impl ToString for NotSafe { - fn to_string(&self) -> String { - format!("{:?}", self) +impl Display for NotSafe { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) } } diff --git a/validator_client/slashing_protection/src/slashing_database.rs b/validator_client/slashing_protection/src/slashing_database.rs index 406913bfd1..b497abd7dd 100644 --- a/validator_client/slashing_protection/src/slashing_database.rs +++ b/validator_client/slashing_protection/src/slashing_database.rs @@ -175,10 +175,10 @@ impl SlashingDatabase { } /// Execute a database transaction as a closure, committing if `f` returns `Ok`. - pub fn with_transaction(&self, f: F) -> Result + pub fn with_transaction(&self, f: F) -> Result where - F: FnOnce(&Transaction) -> Result, - E: From, + F: FnOnce(&Transaction) -> Result, + U: From, { let mut conn = self.conn_pool.get().map_err(NotSafe::from)?; let txn = conn.transaction().map_err(NotSafe::from)?; diff --git a/validator_client/slashing_protection/src/test_utils.rs b/validator_client/slashing_protection/src/test_utils.rs index 3df892ecd9..efdeb9bc6b 100644 --- a/validator_client/slashing_protection/src/test_utils.rs +++ b/validator_client/slashing_protection/src/test_utils.rs @@ -1,9 +1,6 @@ use crate::*; use tempfile::{tempdir, TempDir}; -use types::{ - test_utils::generate_deterministic_keypair, AttestationData, BeaconBlockHeader, Hash256, - PublicKeyBytes, -}; +use types::{test_utils::generate_deterministic_keypair, AttestationData, BeaconBlockHeader}; pub const DEFAULT_VALIDATOR_INDEX: usize = 0; pub const DEFAULT_DOMAIN: Hash256 = Hash256::zero(); diff --git a/validator_client/src/attestation_service.rs b/validator_client/src/attestation_service.rs index 90ce52b156..9a5dc6bcc9 100644 --- a/validator_client/src/attestation_service.rs +++ b/validator_client/src/attestation_service.rs @@ -425,6 +425,11 @@ impl AttestationService { .flatten() .unzip(); + if attestations.is_empty() { + warn!(log, "No attestations were published"); + return Ok(None); + } + // Post the attestations to the BN. match self .beacon_nodes diff --git a/validator_client/src/beacon_node_fallback.rs b/validator_client/src/beacon_node_fallback.rs index 8e22c80c98..22e0ffa140 100644 --- a/validator_client/src/beacon_node_fallback.rs +++ b/validator_client/src/beacon_node_fallback.rs @@ -104,15 +104,15 @@ pub fn start_fallback_updater_service( } #[derive(Debug)] -pub enum Error { +pub enum Error { /// The node was unavailable and we didn't attempt to contact it. Unavailable(CandidateError), /// We attempted to contact the node but it failed. - RequestFailed(E), + RequestFailed(T), } -impl Error { - pub fn request_failure(&self) -> Option<&E> { +impl Error { + pub fn request_failure(&self) -> Option<&T> { match self { Error::RequestFailed(e) => Some(e), _ => None, @@ -121,9 +121,9 @@ impl Error { } /// The list of errors encountered whilst attempting to perform a query. -pub struct Errors(pub Vec<(String, Error)>); +pub struct Errors(pub Vec<(String, Error)>); -impl fmt::Display for Errors { +impl fmt::Display for Errors { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if !self.0.is_empty() { write!(f, "Some endpoints failed, num_failed: {}", self.0.len())?; @@ -332,6 +332,14 @@ impl CandidateBeaconNode { "endpoint_deneb_fork_epoch" => ?beacon_node_spec.deneb_fork_epoch, "hint" => UPDATE_REQUIRED_LOG_HINT, ); + } else if beacon_node_spec.electra_fork_epoch != spec.electra_fork_epoch { + warn!( + log, + "Beacon node has mismatched Electra fork epoch"; + "endpoint" => %self.beacon_node, + "endpoint_electra_fork_epoch" => ?beacon_node_spec.electra_fork_epoch, + "hint" => UPDATE_REQUIRED_LOG_HINT, + ); } Ok(()) diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index ee939de31f..94e81cf214 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -145,6 +145,12 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { future.") .takes_value(false) ) + .arg( + Arg::with_name("distributed") + .long("distributed") + .help("Enables functionality required for running the validator in a distributed validator cluster.") + .takes_value(false) + ) /* REST API related arguments */ .arg( Arg::with_name("http") @@ -392,4 +398,35 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { [default: 8,8,48]") .takes_value(true) ) + .arg( + Arg::with_name("disable-slashing-protection-web3signer") + .long("disable-slashing-protection-web3signer") + .help("Disable Lighthouse's slashing protection for all web3signer keys. This can \ + reduce the I/O burden on the VC but is only safe if slashing protection \ + is enabled on the remote signer and is implemented correctly. DO NOT ENABLE \ + THIS FLAG UNLESS YOU ARE CERTAIN THAT SLASHING PROTECTION IS ENABLED ON \ + THE REMOTE SIGNER. YOU WILL GET SLASHED IF YOU USE THIS FLAG WITHOUT \ + ENABLING WEB3SIGNER'S SLASHING PROTECTION.") + .takes_value(false) + ) + /* + * Experimental/development options. + */ + .arg( + Arg::with_name("web3-signer-keep-alive-timeout") + .long("web3-signer-keep-alive-timeout") + .value_name("MILLIS") + .default_value("90000") + .help("Keep-alive timeout for each web3signer connection. Set to 'null' to never \ + timeout") + .takes_value(true), + ) + .arg( + Arg::with_name("web3-signer-max-idle-connections") + .long("web3-signer-max-idle-connections") + .value_name("COUNT") + .help("Maximum number of idle connections to maintain per web3signer host. Default \ + is unlimited.") + .takes_value(true), + ) } diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index b79837f9c9..7ad2a5dd1a 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -17,6 +17,7 @@ use std::fs; use std::net::IpAddr; use std::path::PathBuf; use std::str::FromStr; +use std::time::Duration; use types::{Address, GRAFFITI_BYTES_LEN}; pub const DEFAULT_BEACON_NODE: &str = "http://localhost:5052/"; @@ -80,12 +81,18 @@ pub struct Config { 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, + /// Enable slashing protection even while using web3signer keys. + pub enable_web3signer_slashing_protection: bool, /// Enables block production via the block v3 endpoint. This configuration option can be removed post deneb. pub produce_block_v3: bool, /// Specifies the boost factor, a percentage multiplier to apply to the builder's payload value. pub builder_boost_factor: Option, /// If true, Lighthouse will prefer builder proposals, if available. pub prefer_builder_proposals: bool, + /// Whether we are running with distributed network support. + pub distributed: bool, + pub web3_signer_keep_alive_timeout: Option, + pub web3_signer_max_idle_connections: Option, } impl Default for Config { @@ -127,9 +134,13 @@ impl Default for Config { broadcast_topics: vec![ApiTopic::Subscriptions], enable_latency_measurement_service: true, validator_registration_batch_size: 500, + enable_web3signer_slashing_protection: true, produce_block_v3: false, builder_boost_factor: None, prefer_builder_proposals: false, + distributed: false, + web3_signer_keep_alive_timeout: Some(Duration::from_secs(90)), + web3_signer_max_idle_connections: None, } } } @@ -231,6 +242,10 @@ impl Config { config.beacon_nodes_tls_certs = Some(tls_certs.split(',').map(PathBuf::from).collect()); } + if cli_args.is_present("distributed") { + config.distributed = true; + } + if cli_args.is_present("disable-run-on-all") { warn!( log, @@ -264,6 +279,22 @@ impl Config { config.beacon_node_fallback.sync_tolerances = BeaconNodeSyncDistanceTiers::default(); } + /* + * Web3 signer + */ + if let Some(s) = parse_optional::(cli_args, "web3-signer-keep-alive-timeout")? { + config.web3_signer_keep_alive_timeout = if s == "null" { + None + } else { + Some(Duration::from_millis( + s.parse().map_err(|_| "invalid timeout value".to_string())?, + )) + } + } + if let Some(n) = parse_optional::(cli_args, "web3-signer-max-idle-connections")? { + config.web3_signer_max_idle_connections = Some(n); + } + /* * Http API server */ @@ -405,6 +436,19 @@ impl Config { return Err("validator-registration-batch-size cannot be 0".to_string()); } + config.enable_web3signer_slashing_protection = + if cli_args.is_present("disable-slashing-protection-web3signer") { + warn!( + log, + "Slashing protection for remote keys disabled"; + "info" => "ensure slashing protection on web3signer is enabled or you WILL \ + get slashed" + ); + false + } else { + true + }; + Ok(config) } } diff --git a/validator_client/src/doppelganger_service.rs b/validator_client/src/doppelganger_service.rs index 052decb89e..67ff73cc15 100644 --- a/validator_client/src/doppelganger_service.rs +++ b/validator_client/src/doppelganger_service.rs @@ -687,7 +687,6 @@ mod test { use environment::null_logger; use futures::executor::block_on; use slot_clock::TestingSlotClock; - use std::collections::HashSet; use std::future; use std::time::Duration; use types::{ diff --git a/validator_client/src/duties_service.rs b/validator_client/src/duties_service.rs index 863e85529d..2ffc9b9adc 100644 --- a/validator_client/src/duties_service.rs +++ b/validator_client/src/duties_service.rs @@ -6,7 +6,7 @@ //! The `DutiesService` is also responsible for sending events to the `BlockService` which trigger //! block production. -mod sync; +pub mod sync; use crate::beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; use crate::http_metrics::metrics::{get_int_gauge, set_int_gauge, ATTESTATION_DUTY}; @@ -42,6 +42,9 @@ const HISTORICAL_DUTIES_EPOCHS: u64 = 2; /// At start-up selection proofs will be computed with less lookahead out of necessity. const SELECTION_PROOF_SLOT_LOOKAHEAD: u64 = 8; +/// The attestation selection proof lookahead for those running with the --distributed flag. +const SELECTION_PROOF_SLOT_LOOKAHEAD_DVT: u64 = 1; + /// Fraction of a slot at which selection proof signing should happen (2 means half way). const SELECTION_PROOF_SCHEDULE_DENOM: u32 = 2; @@ -119,43 +122,37 @@ pub struct SubscriptionSlots { slots: Vec<(Slot, AtomicBool)>, } -impl DutyAndProof { - /// Instantiate `Self`, computing the selection proof as well. - pub async fn new_with_selection_proof( - duty: AttesterData, - validator_store: &ValidatorStore, - spec: &ChainSpec, - ) -> Result { - let selection_proof = validator_store - .produce_selection_proof(duty.pubkey, duty.slot) - .await - .map_err(Error::FailedToProduceSelectionProof)?; +/// Create a selection proof for `duty`. +/// +/// Return `Ok(None)` if the attesting validator is not an aggregator. +async fn make_selection_proof( + duty: &AttesterData, + validator_store: &ValidatorStore, + spec: &ChainSpec, +) -> Result, Error> { + let selection_proof = validator_store + .produce_selection_proof(duty.pubkey, duty.slot) + .await + .map_err(Error::FailedToProduceSelectionProof)?; - let selection_proof = selection_proof - .is_aggregator(duty.committee_length as usize, spec) - .map_err(Error::InvalidModulo) - .map(|is_aggregator| { - if is_aggregator { - Some(selection_proof) - } else { - // Don't bother storing the selection proof if the validator isn't an - // aggregator, we won't need it. - None - } - })?; - - let subscription_slots = SubscriptionSlots::new(duty.slot); - - Ok(Self { - duty, - selection_proof, - subscription_slots, + selection_proof + .is_aggregator(duty.committee_length as usize, spec) + .map_err(Error::InvalidModulo) + .map(|is_aggregator| { + if is_aggregator { + Some(selection_proof) + } else { + // Don't bother storing the selection proof if the validator isn't an + // aggregator, we won't need it. + None + } }) - } +} +impl DutyAndProof { /// Create a new `DutyAndProof` with the selection proof waiting to be filled in. - pub fn new_without_selection_proof(duty: AttesterData) -> Self { - let subscription_slots = SubscriptionSlots::new(duty.slot); + pub fn new_without_selection_proof(duty: AttesterData, current_slot: Slot) -> Self { + let subscription_slots = SubscriptionSlots::new(duty.slot, current_slot); Self { duty, selection_proof: None, @@ -165,10 +162,13 @@ impl DutyAndProof { } impl SubscriptionSlots { - fn new(duty_slot: Slot) -> Arc { + fn new(duty_slot: Slot, current_slot: Slot) -> Arc { let slots = ATTESTATION_SUBSCRIPTION_OFFSETS .into_iter() .filter_map(|offset| duty_slot.safe_sub(offset).ok()) + // Keep only scheduled slots that haven't happened yet. This avoids sending expired + // subscriptions. + .filter(|scheduled_slot| *scheduled_slot > current_slot) .map(|scheduled_slot| (scheduled_slot, AtomicBool::new(false))) .collect(); Arc::new(Self { slots }) @@ -211,16 +211,21 @@ pub struct DutiesService { /// proposals for any validators which are not registered locally. pub proposers: RwLock, /// Map from validator index to sync committee duties. - pub sync_duties: SyncDutiesMap, + pub sync_duties: SyncDutiesMap, /// Provides the canonical list of locally-managed validators. pub validator_store: Arc>, /// Tracks the current slot. pub slot_clock: T, /// Provides HTTP access to remote beacon nodes. pub beacon_nodes: Arc>, - pub enable_high_validator_count_metrics: bool, + /// The runtime for spawning tasks. pub context: RuntimeContext, + /// The current chain spec. pub spec: ChainSpec, + //// Whether we permit large validator counts in the metrics. + pub enable_high_validator_count_metrics: bool, + /// If this validator is running in distributed mode. + pub distributed: bool, } impl DutiesService { @@ -770,14 +775,14 @@ async fn poll_beacon_attesters_for_epoch( // request for extra data unless necessary in order to save on network bandwidth. let uninitialized_validators = get_uninitialized_validators(duties_service, &epoch, local_pubkeys); - let indices_to_request = if !uninitialized_validators.is_empty() { + let initial_indices_to_request = if !uninitialized_validators.is_empty() { uninitialized_validators.as_slice() } else { &local_indices[0..min(INITIAL_DUTIES_QUERY_SIZE, local_indices.len())] }; let response = - post_validator_duties_attester(duties_service, epoch, indices_to_request).await?; + post_validator_duties_attester(duties_service, epoch, initial_indices_to_request).await?; let dependent_root = response.dependent_root; // Find any validators which have conflicting (epoch, dependent_root) values or missing duties for the epoch. @@ -801,24 +806,29 @@ async fn poll_beacon_attesters_for_epoch( return Ok(()); } - // Filter out validators which have already been requested. - let initial_duties = &response.data; + // Make a request for all indices that require updating which we have not already made a request + // for. let indices_to_request = validators_to_update .iter() - .filter(|&&&pubkey| !initial_duties.iter().any(|duty| duty.pubkey == pubkey)) .filter_map(|pubkey| duties_service.validator_store.validator_index(pubkey)) + .filter(|validator_index| !initial_indices_to_request.contains(validator_index)) .collect::>(); - let new_duties = if !indices_to_request.is_empty() { + // Filter the initial duties by their relevance so that we don't hit the warning below about + // overwriting duties. There was previously a bug here. + let new_initial_duties = response + .data + .into_iter() + .filter(|duty| validators_to_update.contains(&&duty.pubkey)); + + let mut new_duties = if !indices_to_request.is_empty() { post_validator_duties_attester(duties_service, epoch, indices_to_request.as_slice()) .await? .data - .into_iter() - .chain(response.data) - .collect::>() } else { - response.data + vec![] }; + new_duties.extend(new_initial_duties); drop(fetch_timer); @@ -837,26 +847,53 @@ async fn poll_beacon_attesters_for_epoch( // Update the duties service with the new `DutyAndProof` messages. let mut attesters = duties_service.attesters.write(); let mut already_warned = Some(()); + let current_slot = duties_service + .slot_clock + .now_or_genesis() + .unwrap_or_default(); for duty in &new_duties { let attester_map = attesters.entry(duty.pubkey).or_default(); // Create initial entries in the map without selection proofs. We'll compute them in the // background later to avoid creating a thundering herd of signing threads whenever new // duties are computed. - let duty_and_proof = DutyAndProof::new_without_selection_proof(duty.clone()); + let duty_and_proof = DutyAndProof::new_without_selection_proof(duty.clone(), current_slot); - if let Some((prior_dependent_root, _)) = - attester_map.insert(epoch, (dependent_root, duty_and_proof)) - { - // Using `already_warned` avoids excessive logs. - if dependent_root != prior_dependent_root && already_warned.take().is_some() { - warn!( - log, - "Attester duties re-org"; - "prior_dependent_root" => %prior_dependent_root, - "dependent_root" => %dependent_root, - "msg" => "this may happen from time to time" - ) + match attester_map.entry(epoch) { + hash_map::Entry::Occupied(mut occupied) => { + let mut_value = occupied.get_mut(); + let (prior_dependent_root, prior_duty_and_proof) = &mut_value; + + // Guard against overwriting an existing value for the same duty. If we did + // overwrite we could lose a selection proof or information from + // `subscription_slots`. Hitting this branch should be prevented by our logic for + // fetching duties only for unknown indices. + if dependent_root == *prior_dependent_root + && prior_duty_and_proof.duty == duty_and_proof.duty + { + warn!( + log, + "Redundant attester duty update"; + "dependent_root" => %dependent_root, + "validator_index" => duty.validator_index, + ); + continue; + } + + // Using `already_warned` avoids excessive logs. + if dependent_root != *prior_dependent_root && already_warned.take().is_some() { + warn!( + log, + "Attester duties re-org"; + "prior_dependent_root" => %prior_dependent_root, + "dependent_root" => %dependent_root, + "msg" => "this may happen from time to time" + ) + } + *mut_value = (dependent_root, duty_and_proof); + } + hash_map::Entry::Vacant(vacant) => { + vacant.insert((dependent_root, duty_and_proof)); } } } @@ -984,7 +1021,13 @@ async fn fill_in_selection_proofs( continue; }; - let lookahead_slot = current_slot + SELECTION_PROOF_SLOT_LOOKAHEAD; + let selection_lookahead = if duties_service.distributed { + SELECTION_PROOF_SLOT_LOOKAHEAD_DVT + } else { + SELECTION_PROOF_SLOT_LOOKAHEAD + }; + + let lookahead_slot = current_slot + selection_lookahead; let mut relevant_duties = duties_by_slot.split_off(&lookahead_slot); std::mem::swap(&mut relevant_duties, &mut duties_by_slot); @@ -1003,12 +1046,13 @@ async fn fill_in_selection_proofs( // Sign selection proofs (serially). let duty_and_proof_results = stream::iter(relevant_duties.into_values().flatten()) .then(|duty| async { - DutyAndProof::new_with_selection_proof( - duty, + let opt_selection_proof = make_selection_proof( + &duty, &duties_service.validator_store, &duties_service.spec, ) - .await + .await?; + Ok((duty, opt_selection_proof)) }) .collect::>() .await; @@ -1016,7 +1060,7 @@ async fn fill_in_selection_proofs( // Add to attesters store. let mut attesters = duties_service.attesters.write(); for result in duty_and_proof_results { - let duty_and_proof = match result { + let (duty, selection_proof) = match result { Ok(duty_and_proof) => duty_and_proof, Err(Error::FailedToProduceSelectionProof( ValidatorStoreError::UnknownPubkey(pubkey), @@ -1044,12 +1088,12 @@ async fn fill_in_selection_proofs( } }; - let attester_map = attesters.entry(duty_and_proof.duty.pubkey).or_default(); - let epoch = duty_and_proof.duty.slot.epoch(E::slots_per_epoch()); + let attester_map = attesters.entry(duty.pubkey).or_default(); + let epoch = duty.slot.epoch(E::slots_per_epoch()); match attester_map.entry(epoch) { hash_map::Entry::Occupied(mut entry) => { // No need to update duties for which no proof was computed. - let Some(selection_proof) = duty_and_proof.selection_proof else { + let Some(selection_proof) = selection_proof else { continue; }; @@ -1070,6 +1114,14 @@ async fn fill_in_selection_proofs( } } hash_map::Entry::Vacant(entry) => { + // This probably shouldn't happen, but we have enough info to fill in the + // entry so we may as well. + let subscription_slots = SubscriptionSlots::new(duty.slot, current_slot); + let duty_and_proof = DutyAndProof { + duty, + selection_proof, + subscription_slots, + }; entry.insert((dependent_root, duty_and_proof)); } } @@ -1289,13 +1341,15 @@ mod test { #[test] fn subscription_slots_exact() { + // Set current slot in the past so no duties are considered expired. + let current_slot = Slot::new(0); for duty_slot in [ - Slot::new(32), + Slot::new(33), Slot::new(47), Slot::new(99), Slot::new(1002003), ] { - let subscription_slots = SubscriptionSlots::new(duty_slot); + let subscription_slots = SubscriptionSlots::new(duty_slot, current_slot); // Run twice to check idempotence (subscription slots shouldn't be marked as done until // we mark them manually). @@ -1329,8 +1383,9 @@ mod test { #[test] fn subscription_slots_mark_multiple() { for (i, offset) in ATTESTATION_SUBSCRIPTION_OFFSETS.into_iter().enumerate() { + let current_slot = Slot::new(0); let duty_slot = Slot::new(64); - let subscription_slots = SubscriptionSlots::new(duty_slot); + let subscription_slots = SubscriptionSlots::new(duty_slot, current_slot); subscription_slots.record_successful_subscription_at(duty_slot - offset); @@ -1345,4 +1400,22 @@ mod test { } } } + + /// Test the boundary condition where all subscription slots are *just* expired. + #[test] + fn subscription_slots_expired() { + let current_slot = Slot::new(100); + let duty_slot = current_slot + ATTESTATION_SUBSCRIPTION_OFFSETS[0]; + let subscription_slots = SubscriptionSlots::new(duty_slot, current_slot); + for offset in ATTESTATION_SUBSCRIPTION_OFFSETS.into_iter() { + let slot = duty_slot - offset; + assert!(!subscription_slots.should_send_subscription_at(slot)); + } + assert!(subscription_slots.slots.is_empty()); + + // If the duty slot is 1 later, we get a non-empty set of duties. + let subscription_slots = SubscriptionSlots::new(duty_slot + 1, current_slot); + assert_eq!(subscription_slots.slots.len(), 1); + assert!(subscription_slots.should_send_subscription_at(current_slot + 1),); + } } diff --git a/validator_client/src/duties_service/sync.rs b/validator_client/src/duties_service/sync.rs index 53e0ae3281..0bd99dc638 100644 --- a/validator_client/src/duties_service/sync.rs +++ b/validator_client/src/duties_service/sync.rs @@ -6,18 +6,18 @@ use crate::{ }; use futures::future::join_all; -use itertools::Itertools; use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; use slog::{crit, debug, info, warn}; use slot_clock::SlotClock; use std::collections::{HashMap, HashSet}; +use std::marker::PhantomData; use std::sync::Arc; -use types::{ - ChainSpec, Epoch, EthSpec, PublicKeyBytes, Slot, SyncDuty, SyncSelectionProof, SyncSubnetId, -}; +use types::{ChainSpec, EthSpec, PublicKeyBytes, Slot, SyncDuty, SyncSelectionProof, SyncSubnetId}; -/// Number of epochs in advance to compute selection proofs. +/// Number of epochs in advance to compute selection proofs when not in `distributed` mode. pub const AGGREGATION_PRE_COMPUTE_EPOCHS: u64 = 2; +/// Number of slots in advance to compute selection proofs when in `distributed` mode. +pub const AGGREGATION_PRE_COMPUTE_SLOTS_DISTRIBUTED: u64 = 1; /// Top-level data-structure containing sync duty information. /// @@ -31,9 +31,12 @@ pub const AGGREGATION_PRE_COMPUTE_EPOCHS: u64 = 2; /// 2. One-at-a-time locking. For the innermost locks on the aggregator duties, all of the functions /// in this file take care to only lock one validator at a time. We never hold a lock while /// trying to obtain another one (hence no lock ordering issues). -pub struct SyncDutiesMap { +pub struct SyncDutiesMap { /// Map from sync committee period to duties for members of that sync committee. committees: RwLock>, + /// Whether we are in `distributed` mode and using reduced lookahead for aggregate pre-compute. + distributed: bool, + _phantom: PhantomData, } /// Duties for a single sync committee period. @@ -58,8 +61,8 @@ pub struct ValidatorDuties { /// Aggregator duties for a single validator. pub struct AggregatorDuties { - /// The epoch up to which aggregation proofs have already been computed (inclusive). - pre_compute_epoch: RwLock>, + /// The slot up to which aggregation proofs have already been computed (inclusive). + pre_compute_slot: RwLock>, /// Map from slot & subnet ID to proof that this validator is an aggregator. /// /// The slot is the slot at which the signed contribution and proof should be broadcast, @@ -81,15 +84,15 @@ pub struct SlotDuties { pub aggregators: HashMap>, } -impl Default for SyncDutiesMap { - fn default() -> Self { +impl SyncDutiesMap { + pub fn new(distributed: bool) -> Self { Self { committees: RwLock::new(HashMap::new()), + distributed, + _phantom: PhantomData, } } -} -impl SyncDutiesMap { /// Check if duties are already known for all of the given validators for `committee_period`. fn all_duties_known(&self, committee_period: u64, validator_indices: &[u64]) -> bool { self.committees @@ -103,22 +106,34 @@ impl SyncDutiesMap { }) } + /// Number of slots in advance to compute selection proofs + fn aggregation_pre_compute_slots(&self) -> u64 { + if self.distributed { + AGGREGATION_PRE_COMPUTE_SLOTS_DISTRIBUTED + } else { + E::slots_per_epoch() * AGGREGATION_PRE_COMPUTE_EPOCHS + } + } + /// Prepare for pre-computation of selection proofs for `committee_period`. /// - /// Return the epoch up to which proofs should be pre-computed, as well as a vec of - /// `(previous_pre_compute_epoch, sync_duty)` pairs for all validators which need to have proofs + /// Return the slot up to which proofs should be pre-computed, as well as a vec of + /// `(previous_pre_compute_slot, sync_duty)` pairs for all validators which need to have proofs /// computed. See `fill_in_aggregation_proofs` for the actual calculation. fn prepare_for_aggregator_pre_compute( &self, committee_period: u64, - current_epoch: Epoch, + current_slot: Slot, spec: &ChainSpec, - ) -> (Epoch, Vec<(Epoch, SyncDuty)>) { - let default_start_epoch = - std::cmp::max(current_epoch, first_epoch_of_period(committee_period, spec)); - let pre_compute_epoch = std::cmp::min( - current_epoch + AGGREGATION_PRE_COMPUTE_EPOCHS, - last_epoch_of_period(committee_period, spec), + ) -> (Slot, Vec<(Slot, SyncDuty)>) { + let default_start_slot = std::cmp::max( + current_slot, + first_slot_of_period::(committee_period, spec), + ); + let pre_compute_lookahead_slots = self.aggregation_pre_compute_slots(); + let pre_compute_slot = std::cmp::min( + current_slot + pre_compute_lookahead_slots, + last_slot_of_period::(committee_period, spec), ); let pre_compute_duties = self.committees.read().get(&committee_period).map_or_else( @@ -129,18 +144,18 @@ impl SyncDutiesMap { .values() .filter_map(|maybe_duty| { let duty = maybe_duty.as_ref()?; - let old_pre_compute_epoch = duty + let old_pre_compute_slot = duty .aggregation_duties - .pre_compute_epoch + .pre_compute_slot .write() - .replace(pre_compute_epoch); + .replace(pre_compute_slot); - match old_pre_compute_epoch { + match old_pre_compute_slot { // No proofs pre-computed previously, compute all from the start of - // the period or the current epoch (whichever is later). - None => Some((default_start_epoch, duty.duty.clone())), + // the period or the current slot (whichever is later). + None => Some((default_start_slot, duty.duty.clone())), // Proofs computed up to `prev`, start from the subsequent epoch. - Some(prev) if prev < pre_compute_epoch => { + Some(prev) if prev < pre_compute_slot => { Some((prev + 1, duty.duty.clone())) } // Proofs already known, no need to compute. @@ -150,7 +165,7 @@ impl SyncDutiesMap { .collect() }, ); - (pre_compute_epoch, pre_compute_duties) + (pre_compute_slot, pre_compute_duties) } fn get_or_create_committee_duties<'a, 'b>( @@ -175,7 +190,7 @@ impl SyncDutiesMap { /// Get duties for all validators for the given `wall_clock_slot`. /// /// This is the entry-point for the sync committee service. - pub fn get_duties_for_slot( + pub fn get_duties_for_slot( &self, wall_clock_slot: Slot, spec: &ChainSpec, @@ -252,7 +267,7 @@ impl ValidatorDuties { Self { duty, aggregation_duties: AggregatorDuties { - pre_compute_epoch: RwLock::new(None), + pre_compute_slot: RwLock::new(None), proofs: RwLock::new(HashMap::new()), }, } @@ -264,12 +279,12 @@ fn epoch_offset(spec: &ChainSpec) -> u64 { spec.epochs_per_sync_committee_period.as_u64() / 2 } -fn first_epoch_of_period(sync_committee_period: u64, spec: &ChainSpec) -> Epoch { - spec.epochs_per_sync_committee_period * sync_committee_period +fn first_slot_of_period(sync_committee_period: u64, spec: &ChainSpec) -> Slot { + (spec.epochs_per_sync_committee_period * sync_committee_period).start_slot(E::slots_per_epoch()) } -fn last_epoch_of_period(sync_committee_period: u64, spec: &ChainSpec) -> Epoch { - first_epoch_of_period(sync_committee_period + 1, spec) - 1 +fn last_slot_of_period(sync_committee_period: u64, spec: &ChainSpec) -> Slot { + first_slot_of_period::(sync_committee_period + 1, spec) - 1 } pub async fn poll_sync_committee_duties( @@ -277,11 +292,11 @@ pub async fn poll_sync_committee_duties( ) -> Result<(), Error> { let sync_duties = &duties_service.sync_duties; let spec = &duties_service.spec; - let current_epoch = duties_service + let current_slot = duties_service .slot_clock .now() - .ok_or(Error::UnableToReadSlotClock)? - .epoch(E::slots_per_epoch()); + .ok_or(Error::UnableToReadSlotClock)?; + let current_epoch = current_slot.epoch(E::slots_per_epoch()); // If the Altair fork is yet to be activated, do not attempt to poll for duties. if spec @@ -329,8 +344,8 @@ pub async fn poll_sync_committee_duties( } // Pre-compute aggregator selection proofs for the current period. - let (current_pre_compute_epoch, new_pre_compute_duties) = sync_duties - .prepare_for_aggregator_pre_compute(current_sync_committee_period, current_epoch, spec); + let (current_pre_compute_slot, new_pre_compute_duties) = sync_duties + .prepare_for_aggregator_pre_compute(current_sync_committee_period, current_slot, spec); if !new_pre_compute_duties.is_empty() { let sub_duties_service = duties_service.clone(); @@ -340,8 +355,8 @@ pub async fn poll_sync_committee_duties( sub_duties_service, &new_pre_compute_duties, current_sync_committee_period, - current_epoch, - current_pre_compute_epoch, + current_slot, + current_pre_compute_slot, ) .await }, @@ -367,11 +382,14 @@ pub async fn poll_sync_committee_duties( } // Pre-compute aggregator selection proofs for the next period. - if (current_epoch + AGGREGATION_PRE_COMPUTE_EPOCHS).sync_committee_period(spec)? + let aggregate_pre_compute_lookahead_slots = sync_duties.aggregation_pre_compute_slots(); + if (current_slot + aggregate_pre_compute_lookahead_slots) + .epoch(E::slots_per_epoch()) + .sync_committee_period(spec)? == next_sync_committee_period { - let (pre_compute_epoch, new_pre_compute_duties) = sync_duties - .prepare_for_aggregator_pre_compute(next_sync_committee_period, current_epoch, spec); + let (pre_compute_slot, new_pre_compute_duties) = sync_duties + .prepare_for_aggregator_pre_compute(next_sync_committee_period, current_slot, spec); if !new_pre_compute_duties.is_empty() { let sub_duties_service = duties_service.clone(); @@ -381,8 +399,8 @@ pub async fn poll_sync_committee_duties( sub_duties_service, &new_pre_compute_duties, next_sync_committee_period, - current_epoch, - pre_compute_epoch, + current_slot, + pre_compute_slot, ) .await }, @@ -490,10 +508,10 @@ pub async fn poll_sync_committee_duties_for_period( duties_service: Arc>, - pre_compute_duties: &[(Epoch, SyncDuty)], + pre_compute_duties: &[(Slot, SyncDuty)], sync_committee_period: u64, - current_epoch: Epoch, - pre_compute_epoch: Epoch, + current_slot: Slot, + pre_compute_slot: Slot, ) { let log = duties_service.context.log(); @@ -501,16 +519,16 @@ pub async fn fill_in_aggregation_proofs( log, "Calculating sync selection proofs"; "period" => sync_committee_period, - "current_epoch" => current_epoch, - "pre_compute_epoch" => pre_compute_epoch + "current_slot" => current_slot, + "pre_compute_slot" => pre_compute_slot ); - // Generate selection proofs for each validator at each slot, one epoch at a time. - for epoch in (current_epoch.as_u64()..=pre_compute_epoch.as_u64()).map(Epoch::new) { + // Generate selection proofs for each validator at each slot, one slot at a time. + for slot in (current_slot.as_u64()..=pre_compute_slot.as_u64()).map(Slot::new) { let mut validator_proofs = vec![]; - for (validator_start_epoch, duty) in pre_compute_duties { - // Proofs are already known at this epoch for this validator. - if epoch < *validator_start_epoch { + for (validator_start_slot, duty) in pre_compute_duties { + // Proofs are already known at this slot for this validator. + if slot < *validator_start_slot { continue; } @@ -528,67 +546,64 @@ pub async fn fill_in_aggregation_proofs( // Create futures to produce proofs. let duties_service_ref = &duties_service; - let futures = epoch - .slot_iter(E::slots_per_epoch()) - .cartesian_product(&subnet_ids) - .map(|(duty_slot, subnet_id)| async move { - // Construct proof for prior slot. - let slot = duty_slot - 1; + let futures = subnet_ids.iter().map(|subnet_id| async move { + // Construct proof for prior slot. + let proof_slot = slot - 1; - let proof = match duties_service_ref - .validator_store - .produce_sync_selection_proof(&duty.pubkey, slot, *subnet_id) - .await - { - Ok(proof) => proof, - Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { - // A pubkey can be missing when a validator was recently - // removed via the API. - debug!( - log, - "Missing pubkey for sync selection proof"; - "pubkey" => ?pubkey, - "pubkey" => ?duty.pubkey, - "slot" => slot, - ); - return None; - } - Err(e) => { - warn!( - log, - "Unable to sign selection proof"; - "error" => ?e, - "pubkey" => ?duty.pubkey, - "slot" => slot, - ); - return None; - } - }; - - match proof.is_aggregator::() { - Ok(true) => { - debug!( - log, - "Validator is sync aggregator"; - "validator_index" => duty.validator_index, - "slot" => slot, - "subnet_id" => %subnet_id, - ); - Some(((slot, *subnet_id), proof)) - } - Ok(false) => None, - Err(e) => { - warn!( - log, - "Error determining is_aggregator"; - "pubkey" => ?duty.pubkey, - "slot" => slot, - "error" => ?e, - ); - None - } + let proof = match duties_service_ref + .validator_store + .produce_sync_selection_proof(&duty.pubkey, proof_slot, *subnet_id) + .await + { + Ok(proof) => proof, + Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { + // A pubkey can be missing when a validator was recently + // removed via the API. + debug!( + log, + "Missing pubkey for sync selection proof"; + "pubkey" => ?pubkey, + "pubkey" => ?duty.pubkey, + "slot" => proof_slot, + ); + return None; } - }); + Err(e) => { + warn!( + log, + "Unable to sign selection proof"; + "error" => ?e, + "pubkey" => ?duty.pubkey, + "slot" => proof_slot, + ); + return None; + } + }; + + match proof.is_aggregator::() { + Ok(true) => { + debug!( + log, + "Validator is sync aggregator"; + "validator_index" => duty.validator_index, + "slot" => proof_slot, + "subnet_id" => %subnet_id, + ); + Some(((proof_slot, *subnet_id), proof)) + } + Ok(false) => None, + Err(e) => { + warn!( + log, + "Error determining is_aggregator"; + "pubkey" => ?duty.pubkey, + "slot" => proof_slot, + "error" => ?e, + ); + None + } + } + }); // Execute all the futures in parallel, collecting any successful results. let proofs = join_all(futures) @@ -630,7 +645,7 @@ pub async fn fill_in_aggregation_proofs( debug!( log, "Finished computing sync selection proofs"; - "epoch" => epoch, + "slot" => slot, "updated_validators" => num_validators_updated, ); } diff --git a/validator_client/src/http_api/mod.rs b/validator_client/src/http_api/mod.rs index b7e7e790ed..4ffe6d25f3 100644 --- a/validator_client/src/http_api/mod.rs +++ b/validator_client/src/http_api/mod.rs @@ -728,7 +728,16 @@ pub fn serve( 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(); + let initialized_validators = initialized_validators_rw_lock.upgradable_read(); + + // Do not make any changes if all fields are identical or unchanged. + fn equal_or_none( + current_value: Option, + new_value: Option, + ) -> bool { + new_value.is_none() || current_value == new_value + } + match ( initialized_validators.is_enabled(&validator_pubkey), initialized_validators.validator(&validator_pubkey.compress()), @@ -737,32 +746,65 @@ pub fn serve( "no validator for {:?}", validator_pubkey ))), + // If all specified parameters match their existing settings, then this + // change is a no-op. (Some(is_enabled), Some(initialized_validator)) - if Some(is_enabled) == body.enabled - && initialized_validator.get_gas_limit() == body.gas_limit - && initialized_validator.get_builder_boost_factor() - == body.builder_boost_factor - && initialized_validator.get_builder_proposals() - == body.builder_proposals - && initialized_validator.get_prefer_builder_proposals() - == body.prefer_builder_proposals - && initialized_validator.get_graffiti() == maybe_graffiti => + if equal_or_none(Some(is_enabled), body.enabled) + && equal_or_none( + initialized_validator.get_gas_limit(), + body.gas_limit, + ) + && equal_or_none( + initialized_validator.get_builder_boost_factor(), + body.builder_boost_factor, + ) + && equal_or_none( + initialized_validator.get_builder_proposals(), + body.builder_proposals, + ) + && equal_or_none( + initialized_validator.get_prefer_builder_proposals(), + body.prefer_builder_proposals, + ) + && equal_or_none( + initialized_validator.get_graffiti(), + maybe_graffiti, + ) => + { + Ok(()) + } + // Disabling an already disabled validator *with no other changes* is a + // no-op. + (Some(false), None) + if body.enabled.map_or(true, |enabled| !enabled) + && body.gas_limit.is_none() + && body.builder_boost_factor.is_none() + && body.builder_proposals.is_none() + && body.prefer_builder_proposals.is_none() + && maybe_graffiti.is_none() => { Ok(()) } (Some(_), _) => { + // Upgrade read lock only in the case where a write is actually + // required. + let mut initialized_validators_write = + parking_lot::RwLockUpgradableReadGuard::upgrade( + initialized_validators, + ); if let Some(handle) = task_executor.handle() { handle .block_on( - initialized_validators.set_validator_definition_fields( - &validator_pubkey, - body.enabled, - body.gas_limit, - body.builder_proposals, - body.builder_boost_factor, - body.prefer_builder_proposals, - body.graffiti, - ), + initialized_validators_write + .set_validator_definition_fields( + &validator_pubkey, + body.enabled, + body.gas_limit, + body.builder_proposals, + body.builder_boost_factor, + body.prefer_builder_proposals, + body.graffiti, + ), ) .map_err(|e| { warp_utils::reject::custom_server_error(format!( diff --git a/validator_client/src/http_api/test_utils.rs b/validator_client/src/http_api/test_utils.rs index d61d88e84f..d135dfc272 100644 --- a/validator_client/src/http_api/test_utils.rs +++ b/validator_client/src/http_api/test_utils.rs @@ -80,6 +80,7 @@ impl ApiTester { let initialized_validators = InitializedValidators::from_definitions( validator_defs, validator_dir.path().into(), + Default::default(), log.clone(), ) .await @@ -250,9 +251,9 @@ impl ApiTester { pub async fn test_get_lighthouse_spec(self) -> Self { let result = self .client - .get_lighthouse_spec::() + .get_lighthouse_spec::() .await - .map(|res| ConfigAndPreset::Capella(res.data)) + .map(|res| ConfigAndPreset::Electra(res.data)) .unwrap(); let expected = ConfigAndPreset::from_chain_spec::(&E::default_spec(), None); diff --git a/validator_client/src/http_api/tests.rs b/validator_client/src/http_api/tests.rs index 7ac88bb499..f6da4f76e7 100644 --- a/validator_client/src/http_api/tests.rs +++ b/validator_client/src/http_api/tests.rs @@ -68,6 +68,7 @@ impl ApiTester { let initialized_validators = InitializedValidators::from_definitions( validator_defs, validator_dir.path().into(), + Config::default(), log.clone(), ) .await @@ -210,9 +211,9 @@ impl ApiTester { pub async fn test_get_lighthouse_spec(self) -> Self { let result = self .client - .get_lighthouse_spec::() + .get_lighthouse_spec::() .await - .map(|res| ConfigAndPreset::Deneb(res.data)) + .map(|res| ConfigAndPreset::Electra(res.data)) .unwrap(); let expected = ConfigAndPreset::from_chain_spec::(&E::default_spec(), None); diff --git a/validator_client/src/http_metrics/metrics.rs b/validator_client/src/http_metrics/metrics.rs index 52b52126bd..8284ca3e94 100644 --- a/validator_client/src/http_metrics/metrics.rs +++ b/validator_client/src/http_metrics/metrics.rs @@ -203,8 +203,8 @@ lazy_static::lazy_static! { ); } -pub fn gather_prometheus_metrics( - ctx: &Context, +pub fn gather_prometheus_metrics( + ctx: &Context, ) -> std::result::Result { let mut buffer = vec![]; let encoder = TextEncoder::new(); @@ -221,7 +221,7 @@ pub fn gather_prometheus_metrics( if let Some(duties_service) = &shared.duties_service { if let Some(slot) = duties_service.slot_clock.now() { - let current_epoch = slot.epoch(T::slots_per_epoch()); + let current_epoch = slot.epoch(E::slots_per_epoch()); let next_epoch = current_epoch + 1; set_int_gauge( diff --git a/validator_client/src/http_metrics/mod.rs b/validator_client/src/http_metrics/mod.rs index 31337491e8..de6c06437b 100644 --- a/validator_client/src/http_metrics/mod.rs +++ b/validator_client/src/http_metrics/mod.rs @@ -34,18 +34,18 @@ impl From for Error { } /// Contains objects which have shared access from inside/outside of the metrics server. -pub struct Shared { - pub validator_store: Option>>, - pub duties_service: Option>>, +pub struct Shared { + pub validator_store: Option>>, + pub duties_service: Option>>, pub genesis_time: Option, } /// A wrapper around all the items required to spawn the HTTP server. /// /// The server will gracefully handle the case where any fields are `None`. -pub struct Context { +pub struct Context { pub config: Config, - pub shared: RwLock>, + pub shared: RwLock>, pub log: Logger, } @@ -86,8 +86,8 @@ impl Default for Config { /// /// Returns an error if the server is unable to bind or there is another error during /// configuration. -pub fn serve( - ctx: Arc>, +pub fn serve( + ctx: Arc>, shutdown: impl Future + Send + Sync + 'static, ) -> Result<(SocketAddr, impl Future), Error> { let config = &ctx.config; @@ -118,7 +118,7 @@ pub fn serve( let routes = warp::get() .and(warp::path("metrics")) .map(move || inner_ctx.clone()) - .and_then(|ctx: Arc>| async move { + .and_then(|ctx: Arc>| async move { Ok::<_, warp::Rejection>( metrics::gather_prometheus_metrics(&ctx) .map(|body| { diff --git a/validator_client/src/initialized_validators.rs b/validator_client/src/initialized_validators.rs index 7e4331dc85..c94115e5ec 100644 --- a/validator_client/src/initialized_validators.rs +++ b/validator_client/src/initialized_validators.rs @@ -34,6 +34,7 @@ use validator_dir::Builder as ValidatorDirBuilder; use crate::key_cache; use crate::key_cache::KeyCache; +use crate::Config; /// Default timeout for a request to a remote signer for a signature. /// @@ -208,6 +209,7 @@ impl InitializedValidator { key_cache: &mut KeyCache, key_stores: &mut HashMap, web3_signer_client_map: &mut Option>, + config: &Config, ) -> Result { if !def.enabled { return Err(Error::UnableToInitializeDisabledValidator); @@ -311,6 +313,8 @@ impl InitializedValidator { web3_signer.client_identity_path.clone(), web3_signer.client_identity_password.clone(), request_timeout, + config.web3_signer_keep_alive_timeout, + config.web3_signer_max_idle_connections, )?; client_map.insert(web3_signer, client.clone()); client @@ -325,6 +329,8 @@ impl InitializedValidator { web3_signer.client_identity_path.clone(), web3_signer.client_identity_password.clone(), request_timeout, + config.web3_signer_keep_alive_timeout, + config.web3_signer_max_idle_connections, )?; new_web3_signer_client_map.insert(web3_signer, client.clone()); *web3_signer_client_map = Some(new_web3_signer_client_map); @@ -393,8 +399,13 @@ fn build_web3_signer_client( client_identity_path: Option, client_identity_password: Option, request_timeout: Duration, + keep_alive_timeout: Option, + max_idle_connections: Option, ) -> Result { - let builder = Client::builder().timeout(request_timeout); + let builder = Client::builder() + .timeout(request_timeout) + .pool_idle_timeout(keep_alive_timeout) + .pool_max_idle_per_host(max_idle_connections.unwrap_or(usize::MAX)); let builder = if let Some(path) = root_certificate_path { let certificate = load_pem_certificate(path)?; @@ -475,6 +486,7 @@ pub struct InitializedValidators { web3_signer_client_map: Option>, /// For logging via `slog`. log: Logger, + config: Config, } impl InitializedValidators { @@ -482,6 +494,7 @@ impl InitializedValidators { pub async fn from_definitions( definitions: ValidatorDefinitions, validators_dir: PathBuf, + config: Config, log: Logger, ) -> Result { let mut this = Self { @@ -489,6 +502,7 @@ impl InitializedValidators { definitions, validators: HashMap::default(), web3_signer_client_map: None, + config, log, }; this.update_validators().await?; @@ -1234,6 +1248,7 @@ impl InitializedValidators { &mut key_cache, &mut key_stores, &mut None, + &self.config, ) .await { @@ -1284,6 +1299,7 @@ impl InitializedValidators { &mut key_cache, &mut key_stores, &mut self.web3_signer_client_map, + &self.config, ) .await { diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 31046c10c6..595e58017c 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -40,7 +40,7 @@ use account_utils::validator_definitions::ValidatorDefinitions; use attestation_service::{AttestationService, AttestationServiceBuilder}; use block_service::{BlockService, BlockServiceBuilder}; use clap::ArgMatches; -use duties_service::DutiesService; +use duties_service::{sync::SyncDutiesMap, DutiesService}; use environment::RuntimeContext; use eth2::{reqwest::ClientBuilder, types::Graffiti, BeaconNodeHttpClient, StatusCode, Timeouts}; use http_api::ApiSecret; @@ -89,27 +89,27 @@ const HTTP_GET_VALIDATOR_BLOCK_TIMEOUT_QUOTIENT: u32 = 4; const DOPPELGANGER_SERVICE_NAME: &str = "doppelganger"; #[derive(Clone)] -pub struct ProductionValidatorClient { - context: RuntimeContext, - duties_service: Arc>, - block_service: BlockService, - attestation_service: AttestationService, - sync_committee_service: SyncCommitteeService, +pub struct ProductionValidatorClient { + context: RuntimeContext, + duties_service: Arc>, + block_service: BlockService, + attestation_service: AttestationService, + sync_committee_service: SyncCommitteeService, doppelganger_service: Option>, - preparation_service: PreparationService, - validator_store: Arc>, + preparation_service: PreparationService, + validator_store: Arc>, slot_clock: SystemTimeSlotClock, http_api_listen_addr: Option, config: Config, - beacon_nodes: Arc>, + beacon_nodes: Arc>, genesis_time: u64, } -impl ProductionValidatorClient { +impl ProductionValidatorClient { /// Instantiates the validator client, _without_ starting the timers to trigger block /// and attestation production. pub async fn new_from_cli( - context: RuntimeContext, + context: RuntimeContext, cli_args: &ArgMatches<'_>, ) -> Result { let config = Config::from_cli(cli_args, context.log()) @@ -119,7 +119,7 @@ impl ProductionValidatorClient { /// Instantiates the validator client, _without_ starting the timers to trigger block /// and attestation production. - pub async fn new(context: RuntimeContext, config: Config) -> Result { + pub async fn new(context: RuntimeContext, config: Config) -> Result { let log = context.log().clone(); info!( @@ -137,7 +137,7 @@ impl ProductionValidatorClient { duties_service: None, }; - let ctx: Arc> = Arc::new(http_metrics::Context { + let ctx: Arc> = Arc::new(http_metrics::Context { config: config.http_metrics.clone(), shared: RwLock::new(shared), log: log.clone(), @@ -193,6 +193,7 @@ impl ProductionValidatorClient { let validators = InitializedValidators::from_definitions( validator_defs, config.validator_dir.clone(), + config.clone(), log.clone(), ) .await @@ -374,7 +375,7 @@ impl ProductionValidatorClient { // Initialize the number of connected, avaliable beacon nodes to 0. set_gauge(&http_metrics::metrics::AVAILABLE_BEACON_NODES_COUNT, 0); - let mut beacon_nodes: BeaconNodeFallback<_, T> = BeaconNodeFallback::new( + let mut beacon_nodes: BeaconNodeFallback<_, E> = BeaconNodeFallback::new( candidates, config.beacon_node_fallback, config.broadcast_topics.clone(), @@ -382,7 +383,7 @@ impl ProductionValidatorClient { log.clone(), ); - let mut proposer_nodes: BeaconNodeFallback<_, T> = BeaconNodeFallback::new( + let mut proposer_nodes: BeaconNodeFallback<_, E> = BeaconNodeFallback::new( proposer_candidates, config.beacon_node_fallback, config.broadcast_topics.clone(), @@ -452,20 +453,21 @@ impl ProductionValidatorClient { // oversized from having not been pruned (by a prior version) we don't want to prune // concurrently, as it will hog the lock and cause the attestation service to spew CRITs. if let Some(slot) = slot_clock.now() { - validator_store.prune_slashing_protection_db(slot.epoch(T::slots_per_epoch()), true); + validator_store.prune_slashing_protection_db(slot.epoch(E::slots_per_epoch()), true); } let duties_context = context.service_context("duties".into()); let duties_service = Arc::new(DutiesService { attesters: <_>::default(), proposers: <_>::default(), - sync_duties: <_>::default(), + sync_duties: SyncDutiesMap::new(config.distributed), slot_clock: slot_clock.clone(), beacon_nodes: beacon_nodes.clone(), validator_store: validator_store.clone(), spec: context.eth2_config.spec.clone(), context: duties_context, enable_high_validator_count_metrics: config.enable_high_validator_count_metrics, + distributed: config.distributed, }); // Update the metrics server. @@ -535,7 +537,7 @@ impl ProductionValidatorClient { // We use `SLOTS_PER_EPOCH` as the capacity of the block notification channel, because // 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 channel_capacity = E::slots_per_epoch() as usize; let (block_service_tx, block_service_rx) = mpsc::channel(channel_capacity); let log = self.context.log(); diff --git a/validator_client/src/notifier.rs b/validator_client/src/notifier.rs index e23989f68e..1e524ceae8 100644 --- a/validator_client/src/notifier.rs +++ b/validator_client/src/notifier.rs @@ -7,7 +7,7 @@ use tokio::time::{sleep, Duration}; use types::EthSpec; /// Spawns a notifier service which periodically logs information about the node. -pub fn spawn_notifier(client: &ProductionValidatorClient) -> Result<(), String> { +pub fn spawn_notifier(client: &ProductionValidatorClient) -> Result<(), String> { let context = client.context.service_context("notifier".into()); let executor = context.executor.clone(); let duties_service = client.duties_service.clone(); @@ -58,7 +58,7 @@ async fn notify( ); if num_synced > 0 { let primary = candidate_info - .get(0) + .first() .map(|candidate| candidate.node.as_str()) .unwrap_or("None"); info!( diff --git a/validator_client/src/signing_method.rs b/validator_client/src/signing_method.rs index 0de2f2f54f..fe520e11f5 100644 --- a/validator_client/src/signing_method.rs +++ b/validator_client/src/signing_method.rs @@ -34,23 +34,23 @@ pub enum Error { } /// Enumerates all messages that can be signed by a validator. -pub enum SignableMessage<'a, T: EthSpec, Payload: AbstractExecPayload = FullPayload> { +pub enum SignableMessage<'a, E: EthSpec, Payload: AbstractExecPayload = FullPayload> { RandaoReveal(Epoch), - BeaconBlock(&'a BeaconBlock), + BeaconBlock(&'a BeaconBlock), AttestationData(&'a AttestationData), - SignedAggregateAndProof(&'a AggregateAndProof), + SignedAggregateAndProof(&'a AggregateAndProof), SelectionProof(Slot), SyncSelectionProof(&'a SyncAggregatorSelectionData), SyncCommitteeSignature { beacon_block_root: Hash256, slot: Slot, }, - SignedContributionAndProof(&'a ContributionAndProof), + SignedContributionAndProof(&'a ContributionAndProof), ValidatorRegistration(&'a ValidatorRegistrationData), VoluntaryExit(&'a VoluntaryExit), } -impl<'a, T: EthSpec, Payload: AbstractExecPayload> SignableMessage<'a, T, Payload> { +impl<'a, E: EthSpec, Payload: AbstractExecPayload> SignableMessage<'a, E, Payload> { /// Returns the `SignedRoot` for the contained message. /// /// The actual `SignedRoot` trait is not used since it also requires a `TreeHash` impl, which is @@ -117,10 +117,24 @@ impl SigningContext { } impl SigningMethod { - /// Return the signature of `signable_message`, with respect to the `signing_context`. - pub async fn get_signature>( + /// Return whether this signing method requires local slashing protection. + pub fn requires_local_slashing_protection( &self, - signable_message: SignableMessage<'_, T, Payload>, + enable_web3signer_slashing_protection: bool, + ) -> bool { + match self { + // Slashing protection is ALWAYS required for local keys. DO NOT TURN THIS OFF. + SigningMethod::LocalKeystore { .. } => true, + // Slashing protection is only required for remote signer keys when the configuration + // dictates that it is desired. + SigningMethod::Web3Signer { .. } => enable_web3signer_slashing_protection, + } + } + + /// Return the signature of `signable_message`, with respect to the `signing_context`. + pub async fn get_signature>( + &self, + signable_message: SignableMessage<'_, E, Payload>, signing_context: SigningContext, spec: &ChainSpec, executor: &TaskExecutor, @@ -143,9 +157,9 @@ impl SigningMethod { .await } - pub async fn get_signature_from_root>( + pub async fn get_signature_from_root>( &self, - signable_message: SignableMessage<'_, T, Payload>, + signable_message: SignableMessage<'_, E, Payload>, signing_root: Hash256, executor: &TaskExecutor, fork_info: Option, diff --git a/validator_client/src/signing_method/web3signer.rs b/validator_client/src/signing_method/web3signer.rs index d7d74c9448..7973cab2c1 100644 --- a/validator_client/src/signing_method/web3signer.rs +++ b/validator_client/src/signing_method/web3signer.rs @@ -28,6 +28,7 @@ pub enum ForkName { Bellatrix, Capella, Deneb, + Electra, } #[derive(Debug, PartialEq, Serialize)] @@ -37,17 +38,17 @@ pub struct ForkInfo { } #[derive(Debug, PartialEq, Serialize)] -#[serde(bound = "T: EthSpec", rename_all = "snake_case")] -pub enum Web3SignerObject<'a, T: EthSpec, Payload: AbstractExecPayload> { +#[serde(bound = "E: EthSpec", rename_all = "snake_case")] +pub enum Web3SignerObject<'a, E: EthSpec, Payload: AbstractExecPayload> { AggregationSlot { slot: Slot, }, - AggregateAndProof(&'a AggregateAndProof), + AggregateAndProof(&'a AggregateAndProof), Attestation(&'a AttestationData), BeaconBlock { version: ForkName, #[serde(skip_serializing_if = "Option::is_none")] - block: Option<&'a BeaconBlock>, + block: Option<&'a BeaconBlock>, #[serde(skip_serializing_if = "Option::is_none")] block_header: Option, }, @@ -69,12 +70,12 @@ pub enum Web3SignerObject<'a, T: EthSpec, Payload: AbstractExecPayload> { slot: Slot, }, SyncAggregatorSelectionData(&'a SyncAggregatorSelectionData), - ContributionAndProof(&'a ContributionAndProof), + ContributionAndProof(&'a ContributionAndProof), ValidatorRegistration(&'a ValidatorRegistrationData), } -impl<'a, T: EthSpec, Payload: AbstractExecPayload> Web3SignerObject<'a, T, Payload> { - pub fn beacon_block(block: &'a BeaconBlock) -> Result { +impl<'a, E: EthSpec, Payload: AbstractExecPayload> Web3SignerObject<'a, E, Payload> { + pub fn beacon_block(block: &'a BeaconBlock) -> Result { match block { BeaconBlock::Base(_) => Ok(Web3SignerObject::BeaconBlock { version: ForkName::Phase0, @@ -101,6 +102,11 @@ impl<'a, T: EthSpec, Payload: AbstractExecPayload> Web3SignerObject<'a, T, Pa block: None, block_header: Some(block.block_header()), }), + BeaconBlock::Electra(_) => Ok(Web3SignerObject::BeaconBlock { + version: ForkName::Electra, + block: None, + block_header: Some(block.block_header()), + }), } } @@ -126,8 +132,8 @@ impl<'a, T: EthSpec, Payload: AbstractExecPayload> Web3SignerObject<'a, T, Pa } #[derive(Debug, PartialEq, Serialize)] -#[serde(bound = "T: EthSpec")] -pub struct SigningRequest<'a, T: EthSpec, Payload: AbstractExecPayload> { +#[serde(bound = "E: EthSpec")] +pub struct SigningRequest<'a, E: EthSpec, Payload: AbstractExecPayload> { #[serde(rename = "type")] pub message_type: MessageType, #[serde(skip_serializing_if = "Option::is_none")] @@ -135,7 +141,7 @@ pub struct SigningRequest<'a, T: EthSpec, Payload: AbstractExecPayload> { #[serde(rename = "signingRoot")] pub signing_root: Hash256, #[serde(flatten)] - pub object: Web3SignerObject<'a, T, Payload>, + pub object: Web3SignerObject<'a, E, Payload>, } #[derive(Debug, PartialEq, Deserialize)] diff --git a/validator_client/src/sync_committee_service.rs b/validator_client/src/sync_committee_service.rs index 3f86054f44..5c02998e3f 100644 --- a/validator_client/src/sync_committee_service.rs +++ b/validator_client/src/sync_committee_service.rs @@ -160,7 +160,7 @@ impl SyncCommitteeService { let Some(slot_duties) = self .duties_service .sync_duties - .get_duties_for_slot::(slot, &self.duties_service.spec) + .get_duties_for_slot(slot, &self.duties_service.spec) else { debug!(log, "No duties known for slot {}", slot); return Ok(()); @@ -532,7 +532,7 @@ impl SyncCommitteeService { match self .duties_service .sync_duties - .get_duties_for_slot::(duty_slot, spec) + .get_duties_for_slot(duty_slot, spec) { Some(duties) => subscriptions.extend(subscriptions_from_sync_duties( duties.duties, diff --git a/validator_client/src/validator_store.rs b/validator_client/src/validator_store.rs index a2298d3038..0a00dad9be 100644 --- a/validator_client/src/validator_store.rs +++ b/validator_client/src/validator_store.rs @@ -12,7 +12,6 @@ use slashing_protection::{ }; use slog::{crit, error, info, warn, Logger}; use slot_clock::SlotClock; -use std::iter::FromIterator; use std::marker::PhantomData; use std::path::Path; use std::sync::Arc; @@ -20,13 +19,12 @@ use task_executor::TaskExecutor; use types::{ attestation::Error as AttestationError, graffiti::GraffitiString, AbstractExecPayload, Address, AggregateAndProof, Attestation, BeaconBlock, BlindedPayload, ChainSpec, ContributionAndProof, - Domain, Epoch, EthSpec, Fork, ForkName, Graffiti, Hash256, Keypair, PublicKeyBytes, - SelectionProof, Signature, SignedAggregateAndProof, SignedBeaconBlock, - SignedContributionAndProof, SignedRoot, SignedValidatorRegistrationData, SignedVoluntaryExit, - Slot, SyncAggregatorSelectionData, SyncCommitteeContribution, SyncCommitteeMessage, - SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, VoluntaryExit, + Domain, Epoch, EthSpec, Fork, ForkName, Graffiti, Hash256, PublicKeyBytes, SelectionProof, + Signature, SignedAggregateAndProof, SignedBeaconBlock, SignedContributionAndProof, SignedRoot, + SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, SyncAggregatorSelectionData, + SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, + ValidatorRegistrationData, VoluntaryExit, }; -use validator_dir::ValidatorDir; pub use crate::doppelganger_service::DoppelgangerStatus; use crate::preparation_service::ProposalData; @@ -60,31 +58,6 @@ const SLASHING_PROTECTION_HISTORY_EPOCHS: u64 = 512; /// https://github.com/ethereum/builder-specs/issues/17 pub const DEFAULT_GAS_LIMIT: u64 = 30_000_000; -struct LocalValidator { - validator_dir: ValidatorDir, - voting_keypair: Keypair, -} - -/// We derive our own `PartialEq` to avoid doing equality checks between secret keys. -/// -/// It's nice to avoid secret key comparisons from a security perspective, but it's also a little -/// risky when it comes to `HashMap` integrity (that's why we need `PartialEq`). -/// -/// Currently, we obtain keypairs from keystores where we derive the `PublicKey` from a `SecretKey` -/// via a hash function. In order to have two equal `PublicKey` with different `SecretKey` we would -/// need to have either: -/// -/// - A serious upstream integrity error. -/// - A hash collision. -/// -/// It seems reasonable to make these two assumptions in order to avoid the equality checks. -impl PartialEq for LocalValidator { - fn eq(&self, other: &Self) -> bool { - self.validator_dir == other.validator_dir - && self.voting_keypair.pk == other.voting_keypair.pk - } -} - pub struct ValidatorStore { validators: Arc>, slashing_protection: SlashingDatabase, @@ -97,6 +70,7 @@ pub struct ValidatorStore { fee_recipient_process: Option
, gas_limit: Option, builder_proposals: bool, + enable_web3signer_slashing_protection: bool, produce_block_v3: bool, prefer_builder_proposals: bool, builder_boost_factor: Option, @@ -131,6 +105,7 @@ impl ValidatorStore { fee_recipient_process: config.fee_recipient, gas_limit: config.gas_limit, builder_proposals: config.builder_proposals, + enable_web3signer_slashing_protection: config.enable_web3signer_slashing_protection, produce_block_v3: config.produce_block_v3, prefer_builder_proposals: config.prefer_builder_proposals, builder_boost_factor: config.builder_boost_factor, @@ -394,7 +369,7 @@ impl ValidatorStore { } } // EIP-7044 - ForkName::Deneb => SigningContext { + ForkName::Deneb | ForkName::Electra => SigningContext { domain, epoch: signing_epoch, fork: Fork { @@ -604,19 +579,26 @@ impl ValidatorStore { let signing_context = self.signing_context(Domain::BeaconProposer, signing_epoch); let domain_hash = signing_context.domain_hash(&self.spec); + let signing_method = self.doppelganger_checked_signing_method(validator_pubkey)?; + // Check for slashing conditions. - let slashing_status = self.slashing_protection.check_and_insert_block_proposal( - &validator_pubkey, - &block.block_header(), - domain_hash, - ); + let slashing_status = if signing_method + .requires_local_slashing_protection(self.enable_web3signer_slashing_protection) + { + self.slashing_protection.check_and_insert_block_proposal( + &validator_pubkey, + &block.block_header(), + domain_hash, + ) + } else { + Ok(Safe::Valid) + }; match slashing_status { // We can safely sign this block without slashing. Ok(Safe::Valid) => { metrics::inc_counter_vec(&metrics::SIGNED_BLOCKS_TOTAL, &[metrics::SUCCESS]); - let signing_method = self.doppelganger_checked_signing_method(validator_pubkey)?; let signature = signing_method .get_signature::( SignableMessage::BeaconBlock(&block), @@ -672,20 +654,28 @@ impl ValidatorStore { }); } + // Get the signing method and check doppelganger protection. + let signing_method = self.doppelganger_checked_signing_method(validator_pubkey)?; + // Checking for slashing conditions. let signing_epoch = attestation.data.target.epoch; let signing_context = self.signing_context(Domain::BeaconAttester, signing_epoch); let domain_hash = signing_context.domain_hash(&self.spec); - let slashing_status = self.slashing_protection.check_and_insert_attestation( - &validator_pubkey, - &attestation.data, - domain_hash, - ); + let slashing_status = if signing_method + .requires_local_slashing_protection(self.enable_web3signer_slashing_protection) + { + self.slashing_protection.check_and_insert_attestation( + &validator_pubkey, + &attestation.data, + domain_hash, + ) + } else { + Ok(Safe::Valid) + }; match slashing_status { // We can safely sign this attestation. Ok(Safe::Valid) => { - let signing_method = self.doppelganger_checked_signing_method(validator_pubkey)?; let signature = signing_method .get_signature::>( SignableMessage::AttestationData(&attestation.data), diff --git a/validator_manager/src/create_validators.rs b/validator_manager/src/create_validators.rs index 8ab3303d36..cd19bd0ae3 100644 --- a/validator_manager/src/create_validators.rs +++ b/validator_manager/src/create_validators.rs @@ -270,7 +270,7 @@ struct ValidatorsAndDeposits { } impl ValidatorsAndDeposits { - async fn new<'a, T: EthSpec>(config: CreateConfig, spec: &ChainSpec) -> Result { + async fn new<'a, E: EthSpec>(config: CreateConfig, spec: &ChainSpec) -> Result { let CreateConfig { // The output path is handled upstream. output_path: _, @@ -330,7 +330,7 @@ impl ValidatorsAndDeposits { eprintln!("Beacon node is on {} network", config_name) } let bn_spec = bn_config - .apply_to_chain_spec::(&T::default_spec()) + .apply_to_chain_spec::(&E::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 { @@ -516,7 +516,7 @@ impl ValidatorsAndDeposits { } } -pub async fn cli_run<'a, T: EthSpec>( +pub async fn cli_run<'a, E: EthSpec>( matches: &'a ArgMatches<'a>, spec: &ChainSpec, dump_config: DumpConfig, @@ -525,11 +525,11 @@ pub async fn cli_run<'a, T: EthSpec>( if dump_config.should_exit_early(&config)? { Ok(()) } else { - run::(config, spec).await + run::(config, spec).await } } -async fn run<'a, T: EthSpec>(config: CreateConfig, spec: &ChainSpec) -> Result<(), String> { +async fn run<'a, E: EthSpec>(config: CreateConfig, spec: &ChainSpec) -> Result<(), String> { let output_path = config.output_path.clone(); if !output_path.exists() { @@ -554,7 +554,7 @@ async fn run<'a, T: EthSpec>(config: CreateConfig, spec: &ChainSpec) -> Result<( )); } - let validators_and_deposits = ValidatorsAndDeposits::new::(config, spec).await?; + let validators_and_deposits = ValidatorsAndDeposits::new::(config, spec).await?; eprintln!("Keystore generation complete"); diff --git a/validator_manager/src/lib.rs b/validator_manager/src/lib.rs index 6889ee79d2..a9991d3272 100644 --- a/validator_manager/src/lib.rs +++ b/validator_manager/src/lib.rs @@ -48,7 +48,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { } /// 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) -> Result<(), String> { +pub fn run<'a, E: EthSpec>(matches: &'a ArgMatches<'a>, env: Environment) -> Result<(), String> { let context = env.core_context(); let spec = context.eth2_config.spec; let dump_config = clap_utils::parse_optional(matches, DUMP_CONFIGS_FLAG)? @@ -64,7 +64,7 @@ pub fn run<'a, T: EthSpec>(matches: &'a ArgMatches<'a>, env: Environment) -> async { match matches.subcommand() { (create_validators::CMD, Some(matches)) => { - create_validators::cli_run::(matches, &spec, dump_config).await + create_validators::cli_run::(matches, &spec, dump_config).await } (import_validators::CMD, Some(matches)) => { import_validators::cli_run(matches, dump_config).await diff --git a/watch/src/block_packing/updater.rs b/watch/src/block_packing/updater.rs index 215964901a..34847f6264 100644 --- a/watch/src/block_packing/updater.rs +++ b/watch/src/block_packing/updater.rs @@ -8,7 +8,7 @@ use log::{debug, error, warn}; const MAX_SIZE_SINGLE_REQUEST_BLOCK_PACKING: u64 = 50; -impl UpdateHandler { +impl UpdateHandler { /// Forward fills the `block_packing` table starting from the entry with the /// highest slot. /// diff --git a/watch/src/block_rewards/updater.rs b/watch/src/block_rewards/updater.rs index ad34b1f078..e2893ad0fe 100644 --- a/watch/src/block_rewards/updater.rs +++ b/watch/src/block_rewards/updater.rs @@ -8,7 +8,7 @@ use log::{debug, error, warn}; const MAX_SIZE_SINGLE_REQUEST_BLOCK_REWARDS: u64 = 1600; -impl UpdateHandler { +impl UpdateHandler { /// Forward fills the `block_rewards` table starting from the entry with the /// highest slot. /// diff --git a/watch/src/blockprint/updater.rs b/watch/src/blockprint/updater.rs index 28c3184556..7ec56dd9c8 100644 --- a/watch/src/blockprint/updater.rs +++ b/watch/src/blockprint/updater.rs @@ -6,7 +6,7 @@ use log::{debug, error, warn}; const MAX_SIZE_SINGLE_REQUEST_BLOCKPRINT: u64 = 1600; -impl UpdateHandler { +impl UpdateHandler { /// Forward fills the `blockprint` table starting from the entry with the /// highest slot. /// diff --git a/watch/src/database/compat.rs b/watch/src/database/compat.rs index b8cda0b216..e3e9e0df6f 100644 --- a/watch/src/database/compat.rs +++ b/watch/src/database/compat.rs @@ -5,8 +5,6 @@ use diesel::pg::{Pg, PgValue}; use diesel::serialize::{self, Output, ToSql}; use diesel::sql_types::{Binary, Integer}; -use std::convert::TryFrom; - macro_rules! impl_to_from_sql_int { ($type:ty) => { impl ToSql for $type diff --git a/watch/src/database/mod.rs b/watch/src/database/mod.rs index 841ebe5ee7..59547c303a 100644 --- a/watch/src/database/mod.rs +++ b/watch/src/database/mod.rs @@ -13,7 +13,6 @@ use self::schema::{ }; use diesel::dsl::max; -use diesel::pg::PgConnection; use diesel::prelude::*; use diesel::r2d2::{Builder, ConnectionManager, Pool, PooledConnection}; use diesel::upsert::excluded; @@ -128,9 +127,9 @@ pub fn insert_canonical_slot(conn: &mut PgConn, new_slot: WatchCanonicalSlot) -> Ok(()) } -pub fn insert_beacon_block( +pub fn insert_beacon_block( conn: &mut PgConn, - block: SignedBeaconBlock, + block: SignedBeaconBlock, root: WatchHash, ) -> Result<(), Error> { use self::canonical_slots::dsl::{beacon_block, slot as canonical_slot}; diff --git a/watch/src/database/utils.rs b/watch/src/database/utils.rs index 7e450f0cee..9134c3698f 100644 --- a/watch/src/database/utils.rs +++ b/watch/src/database/utils.rs @@ -1,6 +1,5 @@ #![allow(dead_code)] use crate::database::config::Config; -use diesel::pg::PgConnection; use diesel::prelude::*; use diesel_migrations::{FileBasedMigrations, MigrationHarness}; diff --git a/watch/src/suboptimal_attestations/updater.rs b/watch/src/suboptimal_attestations/updater.rs index aeabff2035..d8f6ec57d5 100644 --- a/watch/src/suboptimal_attestations/updater.rs +++ b/watch/src/suboptimal_attestations/updater.rs @@ -8,7 +8,7 @@ use log::{debug, error, warn}; const MAX_SIZE_SINGLE_REQUEST_ATTESTATIONS: u64 = 50; -impl UpdateHandler { +impl UpdateHandler { /// Forward fills the `suboptimal_attestations` table starting from the entry with the highest /// slot. /// diff --git a/watch/src/updater/handler.rs b/watch/src/updater/handler.rs index 1e1662bf74..a0bfc0b9a4 100644 --- a/watch/src/updater/handler.rs +++ b/watch/src/updater/handler.rs @@ -9,7 +9,6 @@ use eth2::{ }; use log::{debug, error, info, warn}; use std::collections::HashSet; -use std::iter::FromIterator; use types::{BeaconBlockHeader, EthSpec, Hash256, SignedBeaconBlock, Slot}; use crate::updater::{get_beacon_block, get_header, get_validators}; @@ -17,8 +16,8 @@ use crate::updater::{get_beacon_block, get_header, get_validators}; const MAX_EXPECTED_REORG_LENGTH: u64 = 32; /// Ensure the existing database is valid for this run. -pub async fn ensure_valid_database( - spec: &WatchSpec, +pub async fn ensure_valid_database( + spec: &WatchSpec, pool: &mut PgPool, ) -> Result<(), Error> { let mut conn = database::get_connection(pool)?; @@ -42,21 +41,21 @@ pub async fn ensure_valid_database( } } -pub struct UpdateHandler { +pub struct UpdateHandler { pub pool: PgPool, pub bn: BeaconNodeHttpClient, pub blockprint: Option, pub config: Config, pub slots_per_epoch: u64, - pub spec: WatchSpec, + pub spec: WatchSpec, } -impl UpdateHandler { +impl UpdateHandler { pub async fn new( bn: BeaconNodeHttpClient, - spec: WatchSpec, + spec: WatchSpec, config: FullConfig, - ) -> Result, Error> { + ) -> Result, Error> { let blockprint = if config.blockprint.enabled { if let Some(server) = config.blockprint.url { let blockprint_url = SensitiveUrl::parse(&server).map_err(Error::SensitiveUrl)?; @@ -100,7 +99,7 @@ impl UpdateHandler { let mut conn = database::get_connection(&self.pool)?; let roots = database::get_unknown_canonical_blocks(&mut conn)?; for root in roots { - let block_opt: Option> = + let block_opt: Option> = get_beacon_block(&self.bn, BlockId::Root(root.as_hash())).await?; if let Some(block) = block_opt { database::insert_beacon_block(&mut conn, block, root)?; diff --git a/watch/src/updater/mod.rs b/watch/src/updater/mod.rs index 1fbb0107ae..65e0a90a2b 100644 --- a/watch/src/updater/mod.rs +++ b/watch/src/updater/mod.rs @@ -24,14 +24,14 @@ const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5); const MAINNET: &str = "mainnet"; const GNOSIS: &str = "gnosis"; -pub struct WatchSpec { +pub struct WatchSpec { network: String, - spec: PhantomData, + spec: PhantomData, } -impl WatchSpec { +impl WatchSpec { fn slots_per_epoch(&self) -> u64 { - T::slots_per_epoch() + E::slots_per_epoch() } } @@ -87,9 +87,9 @@ pub async fn run_updater(config: FullConfig) -> Result<(), Error> { } } -pub async fn run_once( +pub async fn run_once( bn: BeaconNodeHttpClient, - spec: WatchSpec, + spec: WatchSpec, config: FullConfig, ) -> Result<(), Error> { let mut watch = UpdateHandler::new(bn, spec, config.clone()).await?; @@ -190,10 +190,10 @@ pub async fn get_header( Ok(None) } -pub async fn get_beacon_block( +pub async fn get_beacon_block( bn: &BeaconNodeHttpClient, block_id: BlockId, -) -> Result>, Error> { +) -> Result>, Error> { let block = bn.get_beacon_blocks(block_id).await?.map(|resp| resp.data); Ok(block)