diff --git a/.github/mergify.yml b/.github/mergify.yml index 4c4046cf67..73267904b8 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -1,3 +1,37 @@ +pull_request_rules: + - name: Ask to resolve conflict + conditions: + - conflict + - -author=dependabot[bot] + - or: + - -draft # Don't report conflicts on regular draft. + - and: # Do report conflicts on draft that are scheduled for the next major release. + - draft + - milestone~=v[0-9]\.[0-9]{2} + actions: + comment: + message: This pull request has merge conflicts. Could you please resolve them + @{{author}}? 🙏 + + - name: Approve trivial maintainer PRs + conditions: + - base!=stable + - label=trivial + - author=@sigp/lighthouse + - -conflict + actions: + review: + type: APPROVE + + - name: Add ready-to-merge labeled PRs to merge queue + conditions: + # All branch protection rules are implicit: https://docs.mergify.com/conditions/#about-branch-protection + - base!=stable + - label=ready-for-merge + - label!=do-not-merge + actions: + queue: + queue_rules: - name: default batch_size: 8 @@ -6,14 +40,16 @@ queue_rules: merge_method: squash commit_message_template: | {{ title }} (#{{ number }}) - - {% for commit in commits %} - * {{ commit.commit_message }} - {% endfor %} + + {{ body | get_section("## Issue Addressed", "") }} + + + {{ body | get_section("## Proposed Changes", "") }} queue_conditions: - "#approved-reviews-by >= 1" - "check-success=license/cla" - "check-success=target-branch-check" + - "label!=do-not-merge" merge_conditions: - "check-success=test-suite-success" - "check-success=local-testnet-success" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cfba601fad..de4fd29409 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,6 +33,7 @@ jobs: arch: [aarch64-unknown-linux-gnu, x86_64-unknown-linux-gnu, x86_64-apple-darwin, + aarch64-apple-darwin, x86_64-windows] include: - arch: aarch64-unknown-linux-gnu @@ -44,6 +45,9 @@ jobs: - arch: x86_64-apple-darwin runner: macos-13 profile: maxperf + - arch: aarch64-apple-darwin + runner: macos-14 + profile: maxperf - arch: x86_64-windows runner: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "windows", "release"]') || 'windows-2019' }} profile: maxperf @@ -94,6 +98,10 @@ jobs: if: matrix.arch == 'x86_64-apple-darwin' run: cargo install --path lighthouse --force --locked --features portable,gnosis --profile ${{ matrix.profile }} + - name: Build Lighthouse for aarch64-apple-darwin + if: matrix.arch == 'aarch64-apple-darwin' + run: cargo install --path lighthouse --force --locked --features portable,gnosis --profile ${{ matrix.profile }} + - name: Build Lighthouse for Windows if: matrix.arch == 'x86_64-windows' run: cargo install --path lighthouse --force --locked --features portable,gnosis --profile ${{ matrix.profile }} @@ -221,7 +229,7 @@ jobs: |Non-Staking Users| |---| *See [Update - Priorities](https://lighthouse-book.sigmaprime.io/installation-priorities.html) + Priorities](https://lighthouse-book.sigmaprime.io/installation_priorities.html) more information about this table.* ## All Changes @@ -230,19 +238,20 @@ jobs: ## Binaries - [See pre-built binaries documentation.](https://lighthouse-book.sigmaprime.io/installation-binaries.html) + [See pre-built binaries documentation.](https://lighthouse-book.sigmaprime.io/installation_binaries.html) The binaries are signed with Sigma Prime's PGP key: `15E66D941F697E28F49381F426416DC3F30674B0` | System | Architecture | Binary | PGP Signature | |:---:|:---:|:---:|:---| - | | x86_64 | [lighthouse-${{ env.VERSION }}-x86_64-apple-darwin.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-apple-darwin.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-apple-darwin.tar.gz.asc) | - | | x86_64 | [lighthouse-${{ env.VERSION }}-x86_64-unknown-linux-gnu.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-unknown-linux-gnu.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-unknown-linux-gnu.tar.gz.asc) | - | | aarch64 | [lighthouse-${{ env.VERSION }}-aarch64-unknown-linux-gnu.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-aarch64-unknown-linux-gnu.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-aarch64-unknown-linux-gnu.tar.gz.asc) | - | | x86_64 | [lighthouse-${{ env.VERSION }}-x86_64-windows.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-windows.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-windows.tar.gz.asc) | + | Apple logo | x86_64 | [lighthouse-${{ env.VERSION }}-x86_64-apple-darwin.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-apple-darwin.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-apple-darwin.tar.gz.asc) | + | Apple logo | aarch64 | [lighthouse-${{ env.VERSION }}-aarch64-apple-darwin.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-aarch64-apple-darwin.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-aarch64-apple-darwin.tar.gz.asc) | + | Linux logo | x86_64 | [lighthouse-${{ env.VERSION }}-x86_64-unknown-linux-gnu.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-unknown-linux-gnu.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-unknown-linux-gnu.tar.gz.asc) | + | Raspberrypi logo | aarch64 | [lighthouse-${{ env.VERSION }}-aarch64-unknown-linux-gnu.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-aarch64-unknown-linux-gnu.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-aarch64-unknown-linux-gnu.tar.gz.asc) | + | Windows logo | x86_64 | [lighthouse-${{ env.VERSION }}-x86_64-windows.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-windows.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-windows.tar.gz.asc) | | | | | | | **System** | **Option** | - | **Resource** | - | | Docker | [${{ env.VERSION }}](https://hub.docker.com/r/${{ env.IMAGE_NAME }}/tags?page=1&ordering=last_updated&name=${{ env.VERSION }}) | [${{ env.IMAGE_NAME }}](https://hub.docker.com/r/${{ env.IMAGE_NAME }}) | + | Docker logo | Docker | [${{ env.VERSION }}](https://hub.docker.com/r/${{ env.IMAGE_NAME }}/tags?page=1&ordering=last_updated&name=${{ env.VERSION }}) | [${{ env.IMAGE_NAME }}](https://hub.docker.com/r/${{ env.IMAGE_NAME }}) | ENDBODY ) assets=(./lighthouse-*.tar.gz*/lighthouse-*.tar.gz*) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 817fd9524d..a94a19900c 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -295,8 +295,15 @@ jobs: with: channel: stable cache-target: release - - name: Run a basic beacon chain sim that starts from Bellatrix - run: cargo run --release --bin simulator basic-sim + - name: Create log dir + run: mkdir ${{ runner.temp }}/basic_simulator_logs + - name: Run a basic beacon chain sim that starts from Deneb + run: cargo run --release --bin simulator basic-sim --disable-stdout-logging --log-dir ${{ runner.temp }}/basic_simulator_logs + - name: Upload logs + uses: actions/upload-artifact@v4 + with: + name: basic_simulator_logs + path: ${{ runner.temp }}/basic_simulator_logs fallback-simulator-ubuntu: name: fallback-simulator-ubuntu needs: [check-labels] @@ -309,8 +316,15 @@ jobs: with: channel: stable cache-target: release + - name: Create log dir + run: mkdir ${{ runner.temp }}/fallback_simulator_logs - name: Run a beacon chain sim which tests VC fallback behaviour - run: cargo run --release --bin simulator fallback-sim + run: cargo run --release --bin simulator fallback-sim --disable-stdout-logging --log-dir ${{ runner.temp }}/fallback_simulator_logs + - name: Upload logs + uses: actions/upload-artifact@v4 + with: + name: fallback_simulator_logs + path: ${{ runner.temp }}/fallback_simulator_logs execution-engine-integration-ubuntu: name: execution-engine-integration-ubuntu needs: [check-labels] diff --git a/Cargo.lock b/Cargo.lock index f66595f056..9ff9d62f5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,7 @@ dependencies = [ "filesystem", "safe_arith", "sensitive_url", + "serde_json", "slashing_protection", "slot_clock", "tempfile", @@ -79,7 +80,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ "crypto-common", - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -92,7 +93,7 @@ dependencies = [ "cipher 0.3.0", "cpufeatures", "ctr 0.8.0", - "opaque-debug", + "opaque-debug 0.3.1", ] [[package]] @@ -122,14 +123,14 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", "once_cell", "version_check", - "zerocopy 0.7.35", + "zerocopy", ] [[package]] @@ -193,14 +194,14 @@ dependencies = [ "derive_more 1.0.0", "once_cell", "serde", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] name = "alloy-primitives" -version = "0.8.22" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c66bb6715b7499ea755bde4c96223ae8eb74e05c014ab38b9db602879ffb825" +checksum = "8c77490fe91a0ce933a1f219029521f20fc28c2c0ca95d53fa4da9c00b8d9d4e" dependencies = [ "alloy-rlp", "arbitrary", @@ -210,9 +211,9 @@ dependencies = [ "derive_arbitrary", "derive_more 2.0.1", "foldhash", - "getrandom 0.2.15", - "hashbrown 0.15.2", - "indexmap 2.8.0", + "getrandom 0.2.16", + "hashbrown 0.15.3", + "indexmap 2.9.0", "itoa", "k256 0.13.4", "keccak-asm", @@ -246,7 +247,7 @@ checksum = "a40e1ef334153322fd878d07e86af7a529bcb86b2439525920a88eba87bcf943" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -322,9 +323,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "arbitrary" @@ -516,7 +517,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "synstructure", ] @@ -528,7 +529,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -608,18 +609,18 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] name = "async-trait" -version = "0.1.87" +version = "0.1.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -676,13 +677,25 @@ dependencies = [ [[package]] name = "auto_impl" -version = "1.2.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e12882f59de5360c748c4cbf569a042d5fb0eb515f7bea9c1f470b47f6ffbd73" +checksum = "7862e21c893d65a1650125d157eaeec691439379a1cee17ee49031b79236ada4" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "auto_impl" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -693,9 +706,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -724,6 +737,28 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base58" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5024ee8015f02155eee35c711107ddd9a9bf3cb689cf2a9089c97e79b6e1ae83" + +[[package]] +name = "base58check" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee2fe4c9a0c84515f136aaae2466744a721af6d63339c18689d9e995d74d99b" +dependencies = [ + "base58", + "sha2 0.8.2", +] + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + [[package]] name = "base64" version = "0.13.1" @@ -744,9 +779,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.7.1" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb97d56060ee67d285efb8001fec9d2a4c710c32efd2e14b5cbb5ba71930fc2d" +checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" [[package]] name = "beacon_chain" @@ -778,6 +813,7 @@ dependencies = [ "maplit", "merkle_proof", "metrics", + "once_cell", "oneshot_broadcast", "operation_pool", "parking_lot 0.12.3", @@ -808,7 +844,7 @@ dependencies = [ [[package]] name = "beacon_node" -version = "7.0.0-beta.5" +version = "7.1.0-beta.0" dependencies = [ "account_utils", "beacon_chain", @@ -843,14 +879,13 @@ name = "beacon_node_fallback" version = "0.1.0" dependencies = [ "clap", - "environment", "eth2", "futures", "itertools 0.10.5", - "logging", "serde", "slot_clock", "strum", + "task_executor", "tokio", "tracing", "types", @@ -880,6 +915,12 @@ dependencies = [ "types", ] +[[package]] +name = "bech32" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dabbe35f96fb9507f7330793dc490461b2962659ac5d427181e451a623751d1" + [[package]] name = "bincode" version = "1.3.3" @@ -908,7 +949,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.100", + "syn 2.0.101", "which", ] @@ -939,6 +980,16 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +[[package]] +name = "bitvec" +version = "0.17.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41262f11d771fd4a61aa3ce019fca363b4b6c282fca9da2a31186d3965a47a5c" +dependencies = [ + "either", + "radium 0.3.0", +] + [[package]] name = "bitvec" version = "0.20.4" @@ -972,14 +1023,26 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding 0.1.5", + "byte-tools", + "byteorder", + "generic-array 0.12.4", +] + [[package]] name = "block-buffer" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "block-padding", - "generic-array", + "block-padding 0.2.1", + "generic-array 0.14.7", ] [[package]] @@ -988,7 +1051,16 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array", + "generic-array 0.14.7", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", ] [[package]] @@ -1046,7 +1118,7 @@ dependencies = [ [[package]] name = "boot_node" -version = "7.0.0-beta.5" +version = "7.1.0-beta.0" dependencies = [ "beacon_node", "bytes", @@ -1105,6 +1177,12 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + [[package]] name = "byteorder" version = "1.5.0" @@ -1209,9 +1287,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.16" +version = "1.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0" dependencies = [ "jobserver", "libc", @@ -1265,9 +1343,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", @@ -1310,7 +1388,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -1337,9 +1415,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.32" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" +checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" dependencies = [ "clap_builder", "clap_derive", @@ -1347,9 +1425,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.32" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" +checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" dependencies = [ "anstream", "anstyle", @@ -1367,7 +1445,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1417,6 +1495,7 @@ dependencies = [ "monitoring_api", "network", "operation_pool", + "rand 0.8.5", "sensitive_url", "serde", "serde_json", @@ -1444,6 +1523,63 @@ dependencies = [ "cc", ] +[[package]] +name = "coins-bip32" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634c509653de24b439672164bbf56f5f582a2ab0e313d3b0f6af0b7345cf2560" +dependencies = [ + "bincode", + "bs58 0.4.0", + "coins-core", + "digest 0.10.7", + "getrandom 0.2.16", + "hmac 0.12.1", + "k256 0.11.6", + "lazy_static", + "serde", + "sha2 0.10.9", + "thiserror 1.0.69", +] + +[[package]] +name = "coins-bip39" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a11892bcac83b4c6e95ab84b5b06c76d9d70ad73548dd07418269c5c7977171" +dependencies = [ + "bitvec 0.17.4", + "coins-bip32", + "getrandom 0.2.16", + "hex", + "hmac 0.12.1", + "pbkdf2 0.11.0", + "rand 0.8.5", + "sha2 0.10.9", + "thiserror 1.0.69", +] + +[[package]] +name = "coins-core" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94090a6663f224feae66ab01e41a2555a8296ee07b5f20dab8888bdefc9f617" +dependencies = [ + "base58check", + "base64 0.12.3", + "bech32", + "blake2", + "digest 0.10.7", + "generic-array 0.14.7", + "hex", + "ripemd", + "serde", + "serde_derive", + "sha2 0.10.9", + "sha3 0.10.8", + "thiserror 1.0.69", +] + [[package]] name = "colorchoice" version = "1.0.3" @@ -1529,12 +1665,41 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "context_deserialize" +version = "0.1.0" +dependencies = [ + "milhouse", + "serde", + "ssz_types", +] + +[[package]] +name = "context_deserialize_derive" +version = "0.1.0" +dependencies = [ + "context_deserialize", + "quote", + "serde", + "serde_json", + "syn 1.0.109", +] + [[package]] name = "convert_case" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1618,7 +1783,7 @@ dependencies = [ "crate_crypto_internal_eth_kzg_maybe_rayon", "crate_crypto_internal_eth_kzg_polynomial", "hex", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] @@ -1668,9 +1833,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] @@ -1712,7 +1877,7 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" dependencies = [ - "generic-array", + "generic-array 0.14.7", "rand_core 0.6.4", "subtle", "zeroize", @@ -1724,7 +1889,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ - "generic-array", + "generic-array 0.14.7", "rand_core 0.6.4", "subtle", "zeroize", @@ -1736,28 +1901,18 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array", + "generic-array 0.14.7", "rand_core 0.6.4", "typenum", ] -[[package]] -name = "crypto-mac" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" -dependencies = [ - "generic-array", - "subtle", -] - [[package]] name = "crypto-mac" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" dependencies = [ - "generic-array", + "generic-array 0.14.7", "subtle", ] @@ -1781,9 +1936,9 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.4.5" +version = "3.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" +checksum = "697b5419f348fd5ae2478e8018cb016c00a5881c7f46c717de98ffd135a5651c" dependencies = [ "nix 0.29.0", "windows-sys 0.59.0", @@ -1813,7 +1968,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1828,12 +1983,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core 0.20.10", - "darling_macro 0.20.10", + "darling_core 0.20.11", + "darling_macro 0.20.11", ] [[package]] @@ -1852,16 +2007,16 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1877,13 +2032,13 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core 0.20.10", + "darling_core 0.20.11", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1908,15 +2063,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "data-encoding-macro" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f9724adfcf41f45bf652b3995837669d73c4d49a1b5ac1ff82905ac7d9b5558" +checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" dependencies = [ "data-encoding", "data-encoding-macro-internal", @@ -1924,12 +2079,12 @@ dependencies = [ [[package]] name = "data-encoding-macro-internal" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e4fdb82bd54a12e42fb58a800dcae6b9e13982238ce2296dc3570b92148e1f" +checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" dependencies = [ "data-encoding", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1957,9 +2112,9 @@ checksum = "b72465f46d518f6015d9cf07f7f3013a95dd6b9c2747c3d65ae0cce43929d14f" [[package]] name = "delay_map" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df941644b671f05f59433e481ba0d31ac10e3667de725236a4c0d587c496fba1" +checksum = "88e365f083a5cb5972d50ce8b1b2c9f125dc5ec0f50c0248cfb568ae59efcf0b" dependencies = [ "futures", "tokio", @@ -1992,9 +2147,9 @@ dependencies = [ [[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "pem-rfc7468", @@ -2017,9 +2172,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", ] @@ -2043,20 +2198,20 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] name = "derive_more" -version = "0.99.19" +version = "0.99.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da29a38df43d6f156149c9b43ded5e018ddff2a855cf2cfd62e8cd7d079c69f" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ - "convert_case", + "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version 0.4.1", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -2085,7 +2240,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -2096,17 +2251,26 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "unicode-xid", ] +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + [[package]] name = "digest" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -2191,7 +2355,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -2209,6 +2373,7 @@ dependencies = [ "tokio", "tracing", "types", + "validator_store", ] [[package]] @@ -2241,7 +2406,7 @@ version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der 0.7.9", + "der 0.7.10", "digest 0.10.7", "elliptic-curve 0.13.8", "rfc6979 0.4.0", @@ -2269,7 +2434,7 @@ dependencies = [ "ed25519", "rand_core 0.6.4", "serde", - "sha2 0.10.8", + "sha2 0.10.9", "subtle", "zeroize", ] @@ -2283,7 +2448,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -2335,8 +2500,9 @@ dependencies = [ "der 0.6.1", "digest 0.10.7", "ff 0.12.1", - "generic-array", + "generic-array 0.14.7", "group 0.12.1", + "pkcs8 0.9.0", "rand_core 0.6.4", "sec1 0.3.0", "subtle", @@ -2353,7 +2519,7 @@ dependencies = [ "crypto-bigint 0.5.5", "digest 0.10.7", "ff 0.13.1", - "generic-array", + "generic-array 0.14.7", "group 0.13.0", "pem-rfc7468", "pkcs8 0.10.2", @@ -2400,7 +2566,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -2420,7 +2586,7 @@ checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -2476,14 +2642,36 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", "windows-sys 0.59.0", ] +[[package]] +name = "eth-keystore" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fda3bf123be441da5260717e0661c25a2fd9cb2b2c1d20bf2e05580047158ab" +dependencies = [ + "aes 0.8.4", + "ctr 0.9.2", + "digest 0.10.7", + "hex", + "hmac 0.12.1", + "pbkdf2 0.11.0", + "rand 0.8.5", + "scrypt 0.10.0", + "serde", + "serde_json", + "sha2 0.10.9", + "sha3 0.10.8", + "thiserror 1.0.69", + "uuid 0.8.2", +] + [[package]] name = "eth1" version = "0.2.0" @@ -2544,6 +2732,7 @@ dependencies = [ "multiaddr", "pretty_reqwest_error", "proto_array", + "rand 0.8.5", "reqwest", "reqwest-eventsource", "sensitive_url", @@ -2551,6 +2740,7 @@ dependencies = [ "serde_json", "slashing_protection", "ssz_types", + "test_random_derive", "tokio", "types", "zeroize", @@ -2600,7 +2790,7 @@ dependencies = [ "hmac 0.11.0", "pbkdf2 0.8.0", "rand 0.8.5", - "scrypt", + "scrypt 0.7.0", "serde", "serde_json", "serde_repr", @@ -2756,7 +2946,7 @@ checksum = "c853bd72c9e5787f8aafc3df2907c2ed03cff3150c3acd94e2e53a98ab70a8ab" dependencies = [ "cpufeatures", "ring", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] @@ -2794,10 +2984,10 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d832a5c38eba0e7ad92592f7a22d693954637fbb332b4f669590d66a5c3183e5" dependencies = [ - "darling 0.20.10", + "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -2830,7 +3020,7 @@ dependencies = [ "dunce", "ethers-core", "eyre", - "getrandom 0.2.15", + "getrandom 0.2.16", "hex", "proc-macro2", "quote", @@ -2869,13 +3059,15 @@ dependencies = [ "bytes", "cargo_metadata 0.15.4", "chrono", + "convert_case 0.6.0", "elliptic-curve 0.12.3", "ethabi 18.0.0", - "generic-array", + "generic-array 0.14.7", "hex", "k256 0.11.6", "once_cell", "open-fastrlp", + "proc-macro2", "rand 0.8.5", "rlp", "rlp-derive", @@ -2888,6 +3080,49 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "ethers-etherscan" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9713f525348e5dde025d09b0a4217429f8074e8ff22c886263cc191e87d8216" +dependencies = [ + "ethers-core", + "getrandom 0.2.16", + "reqwest", + "semver 1.0.26", + "serde", + "serde-aux", + "serde_json", + "thiserror 1.0.69", + "tracing", +] + +[[package]] +name = "ethers-middleware" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e71df7391b0a9a51208ffb5c7f2d068900e99d6b3128d3a4849d138f194778b7" +dependencies = [ + "async-trait", + "auto_impl 0.5.0", + "ethers-contract", + "ethers-core", + "ethers-etherscan", + "ethers-providers", + "ethers-signers", + "futures-locks", + "futures-util", + "instant", + "reqwest", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tracing", + "tracing-futures", + "url", +] + [[package]] name = "ethers-providers" version = "1.0.2" @@ -2895,13 +3130,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1a9e0597aa6b2fdc810ff58bc95e4eeaa2c219b3e615ed025106ecb027407d8" dependencies = [ "async-trait", - "auto_impl", + "auto_impl 1.3.0", "base64 0.13.1", "ethers-core", "futures-core", "futures-timer", "futures-util", - "getrandom 0.2.15", + "getrandom 0.2.16", "hashers", "hex", "http 0.2.12", @@ -2923,6 +3158,24 @@ dependencies = [ "ws_stream_wasm", ] +[[package]] +name = "ethers-signers" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f41ced186867f64773db2e55ffdd92959e094072a1d09a5e5e831d443204f98" +dependencies = [ + "async-trait", + "coins-bip32", + "coins-bip39", + "elliptic-curve 0.12.3", + "eth-keystore", + "ethers-core", + "hex", + "rand 0.8.5", + "sha2 0.10.9", + "thiserror 1.0.69", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -2942,9 +3195,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ "event-listener 5.4.0", "pin-project-lite", @@ -2968,7 +3221,9 @@ dependencies = [ "async-channel 1.9.0", "deposit_contract", "ethers-core", + "ethers-middleware", "ethers-providers", + "ethers-signers", "execution_layer", "fork_choice", "futures", @@ -3046,6 +3301,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -3071,7 +3332,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" dependencies = [ "arrayvec", - "auto_impl", + "auto_impl 1.3.0", "bytes", ] @@ -3082,7 +3343,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" dependencies = [ "arrayvec", - "auto_impl", + "auto_impl 1.3.0", "bytes", ] @@ -3181,9 +3442,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", "libz-sys", @@ -3198,9 +3459,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "foreign-types" @@ -3334,6 +3595,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "futures-locks" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ec6fe3675af967e67c5536c0b9d44e34e6c52f86bedc4ea49c5317b8e94d06" +dependencies = [ + "futures-channel", + "futures-task", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -3342,7 +3613,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3352,7 +3623,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" dependencies = [ "futures-io", - "rustls 0.23.23", + "rustls 0.23.27", "rustls-pki-types", ] @@ -3414,6 +3685,15 @@ dependencies = [ "windows 0.58.0", ] +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -3449,9 +3729,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "js-sys", @@ -3462,14 +3742,16 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" dependencies = [ "cfg-if", + "js-sys", "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets 0.52.6", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -3478,7 +3760,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" dependencies = [ - "opaque-debug", + "opaque-debug 0.3.1", "polyval", ] @@ -3505,7 +3787,7 @@ checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3562,7 +3844,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.8.0", + "indexmap 2.9.0", "slab", "tokio", "tokio-util", @@ -3571,17 +3853,17 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.8" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.3.0", - "indexmap 2.8.0", + "http 1.3.1", + "indexmap 2.9.0", "slab", "tokio", "tokio-util", @@ -3590,9 +3872,9 @@ dependencies = [ [[package]] name = "half" -version = "2.4.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ "cfg-if", "crunchy", @@ -3631,9 +3913,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" dependencies = [ "allocator-api2", "equivalent", @@ -3737,9 +4019,9 @@ checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] name = "hermit-abi" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" +checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" [[package]] name = "hex" @@ -3773,7 +4055,7 @@ dependencies = [ "idna", "ipnet", "once_cell", - "rand 0.9.0", + "rand 0.9.1", "socket2", "thiserror 2.0.12", "tinyvec", @@ -3795,7 +4077,7 @@ dependencies = [ "moka", "once_cell", "parking_lot 0.12.3", - "rand 0.9.0", + "rand 0.9.1", "resolv-conf", "smallvec", "thiserror 2.0.12", @@ -3812,23 +4094,13 @@ dependencies = [ "hmac 0.12.1", ] -[[package]] -name = "hmac" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" -dependencies = [ - "crypto-mac 0.8.0", - "digest 0.9.0", -] - [[package]] name = "hmac" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" dependencies = [ - "crypto-mac 0.11.0", + "crypto-mac", "digest 0.9.0", ] @@ -3841,17 +4113,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "hmac-drbg" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" -dependencies = [ - "digest 0.9.0", - "generic-array", - "hmac 0.8.1", -] - [[package]] name = "home" version = "0.5.11" @@ -3861,17 +4122,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "hostname" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" -dependencies = [ - "libc", - "match_cfg", - "winapi", -] - [[package]] name = "http" version = "0.2.12" @@ -3885,9 +4135,9 @@ dependencies = [ [[package]] name = "http" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a761d192fbf18bdef69f5ceedd0d1333afcbda0ee23840373b8317570d23c65" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -3912,7 +4162,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.3.0", + "http 1.3.1", ] [[package]] @@ -3923,7 +4173,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.3.0", + "http 1.3.1", "http-body 1.0.1", "pin-project-lite", ] @@ -4012,9 +4262,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" [[package]] name = "hyper" @@ -4049,8 +4299,8 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.8", - "http 1.3.0", + "h2 0.4.10", + "http 1.3.1", "http-body 1.0.1", "httparse", "httpdate", @@ -4090,16 +4340,17 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.3.0", + "http 1.3.1", "http-body 1.0.1", "hyper 1.6.0", + "libc", "pin-project-lite", "socket2", "tokio", @@ -4109,16 +4360,17 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", - "windows-core 0.52.0", + "windows-core 0.61.0", ] [[package]] @@ -4171,9 +4423,9 @@ dependencies = [ [[package]] name = "icu_locid_transform_data" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" +checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" [[package]] name = "icu_normalizer" @@ -4195,9 +4447,9 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" [[package]] name = "icu_properties" @@ -4216,9 +4468,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" [[package]] name = "icu_provider" @@ -4245,7 +4497,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -4318,7 +4570,7 @@ dependencies = [ "attohttpc", "bytes", "futures", - "http 1.3.0", + "http 1.3.1", "http-body-util", "hyper 1.6.0", "hyper-util", @@ -4331,20 +4583,20 @@ dependencies = [ [[package]] name = "igd-next" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2830127baaaa55dae9aa5ee03158d5aa3687a9c2c11ce66870452580cc695df4" +checksum = "d06464e726471718db9ad3fefc020529fabcde03313a0fc3967510e2db5add12" dependencies = [ "async-trait", "attohttpc", "bytes", "futures", - "http 1.3.0", + "http 1.3.1", "http-body-util", "hyper 1.6.0", "hyper-util", "log", - "rand 0.8.5", + "rand 0.9.1", "tokio", "url", "xmltree", @@ -4403,7 +4655,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -4424,13 +4676,13 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "arbitrary", "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.15.3", "serde", ] @@ -4466,7 +4718,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -4517,7 +4769,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ "socket2", - "widestring 1.1.0", + "widestring 1.2.0", "windows-sys 0.48.0", "winreg", ] @@ -4534,7 +4786,7 @@ version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ - "hermit-abi 0.5.0", + "hermit-abi 0.5.1", "libc", "windows-sys 0.59.0", ] @@ -4580,10 +4832,11 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jobserver" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ + "getrandom 0.3.2", "libc", ] @@ -4621,7 +4874,7 @@ dependencies = [ "cfg-if", "ecdsa 0.14.8", "elliptic-curve 0.12.3", - "sha2 0.10.8", + "sha2 0.10.9", "sha3 0.10.8", ] @@ -4635,7 +4888,7 @@ dependencies = [ "ecdsa 0.16.9", "elliptic-curve 0.13.8", "once_cell", - "sha2 0.10.8", + "sha2 0.10.9", "signature 2.2.0", ] @@ -4704,7 +4957,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lcli" -version = "7.0.0-beta.5" +version = "7.1.0-beta.0" dependencies = [ "account_utils", "beacon_chain", @@ -4764,9 +5017,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.171" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libloading" @@ -4780,9 +5033,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libmdbx" @@ -4791,7 +5044,7 @@ source = "git+https://github.com/sigp/libmdbx-rs?rev=e6ff4b9377c1619bcf0bfdf52be dependencies = [ "bitflags 1.3.2", "byteorder", - "derive_more 0.99.19", + "derive_more 0.99.20", "indexmap 1.9.3", "libc", "mdbx-sys", @@ -4809,7 +5062,7 @@ dependencies = [ "either", "futures", "futures-timer", - "getrandom 0.2.15", + "getrandom 0.2.16", "libp2p-allow-block-list", "libp2p-connection-limits", "libp2p-core", @@ -4898,7 +5151,7 @@ dependencies = [ [[package]] name = "libp2p-gossipsub" version = "0.49.0" -source = "git+https://github.com/sigp/rust-libp2p.git?rev=7a36e4c#7a36e4cde83041f1bd5f2078c4d3934ccb16777e" +source = "git+https://github.com/sigp/rust-libp2p.git?rev=61b2820#61b2820de7a3fab5ae5e1362c4dfa93bd7c41e98" dependencies = [ "async-channel 2.3.1", "asynchronous-codec", @@ -4909,7 +5162,7 @@ dependencies = [ "fnv", "futures", "futures-timer", - "getrandom 0.2.15", + "getrandom 0.2.16", "hashlink 0.9.1", "hex_fmt", "libp2p-core", @@ -4920,7 +5173,7 @@ dependencies = [ "quick-protobuf-codec", "rand 0.8.5", "regex", - "sha2 0.10.8", + "sha2 0.10.9", "tracing", "web-time", ] @@ -4948,22 +5201,22 @@ dependencies = [ [[package]] name = "libp2p-identity" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "257b5621d159b32282eac446bed6670c39c7dc68a200a992d8f056afa0066f6d" +checksum = "fbb68ea10844211a59ce46230909fd0ea040e8a192454d4cc2ee0d53e12280eb" dependencies = [ "asn1_der", "bs58 0.5.1", "ed25519-dalek", "hkdf", - "libsecp256k1", + "k256 0.13.4", "multihash", "p256", "quick-protobuf", "rand 0.8.5", "sec1 0.7.3", - "sha2 0.10.8", - "thiserror 1.0.69", + "sha2 0.10.9", + "thiserror 2.0.12", "tracing", "zeroize", ] @@ -5077,7 +5330,7 @@ dependencies = [ "quinn", "rand 0.8.5", "ring", - "rustls 0.23.23", + "rustls 0.23.27", "socket2", "thiserror 2.0.12", "tokio", @@ -5116,7 +5369,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -5147,7 +5400,7 @@ dependencies = [ "libp2p-identity", "rcgen", "ring", - "rustls 0.23.23", + "rustls 0.23.27", "rustls-webpki 0.101.7", "thiserror 2.0.12", "x509-parser", @@ -5194,54 +5447,6 @@ dependencies = [ "libc", ] -[[package]] -name = "libsecp256k1" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b09eff1b35ed3b33b877ced3a691fc7a481919c7e29c53c906226fcf55e2a1" -dependencies = [ - "arrayref", - "base64 0.13.1", - "digest 0.9.0", - "hmac-drbg", - "libsecp256k1-core", - "libsecp256k1-gen-ecmult", - "libsecp256k1-gen-genmult", - "rand 0.8.5", - "serde", - "sha2 0.9.9", - "typenum", -] - -[[package]] -name = "libsecp256k1-core" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" -dependencies = [ - "crunchy", - "digest 0.9.0", - "subtle", -] - -[[package]] -name = "libsecp256k1-gen-ecmult" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" -dependencies = [ - "libsecp256k1-core", -] - -[[package]] -name = "libsecp256k1-gen-genmult" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" -dependencies = [ - "libsecp256k1-core", -] - [[package]] name = "libsqlite3-sys" version = "0.25.2" @@ -5255,9 +5460,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.21" +version = "1.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9b68e50e6e0b26f672573834882eb57759f6db9b3be2ea3c35c91188bb4eaa" +checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" dependencies = [ "cc", "pkg-config", @@ -5266,7 +5471,7 @@ dependencies = [ [[package]] name = "lighthouse" -version = "7.0.0-beta.5" +version = "7.1.0-beta.0" dependencies = [ "account_manager", "account_utils", @@ -5365,6 +5570,32 @@ dependencies = [ "unused_port", ] +[[package]] +name = "lighthouse_validator_store" +version = "0.1.0" +dependencies = [ + "account_utils", + "beacon_node_fallback", + "doppelganger_service", + "either", + "environment", + "eth2", + "futures", + "initialized_validators", + "logging", + "parking_lot 0.12.3", + "serde", + "signing_method", + "slashing_protection", + "slot_clock", + "task_executor", + "tokio", + "tracing", + "types", + "validator_metrics", + "validator_store", +] + [[package]] name = "lighthouse_version" version = "0.1.0" @@ -5388,9 +5619,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" @@ -5421,13 +5652,13 @@ dependencies = [ [[package]] name = "local-ip-address" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3669cf5561f8d27e8fc84cc15e58350e70f557d4d65f70e3154e54cd2f8e1782" +checksum = "656b3b27f8893f7bbf9485148ff9a65f019e3f33bd5cdc87c83cab16b3fd9ec8" dependencies = [ "libc", "neli", - "thiserror 1.0.69", + "thiserror 2.0.12", "windows-sys 0.59.0", ] @@ -5451,9 +5682,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.26" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "logging" @@ -5462,8 +5693,6 @@ dependencies = [ "chrono", "logroller", "metrics", - "once_cell", - "parking_lot 0.12.3", "serde", "serde_json", "tokio", @@ -5477,9 +5706,9 @@ dependencies = [ [[package]] name = "logroller" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8dd932139da44917b3cd5812ed9536d985aa67203778e0507347579499f49c" +checksum = "90536db32a1cb3672665cdf3269bf030b0f395fabee863895c27b75b9f7a8a7d" dependencies = [ "chrono", "flate2", @@ -5506,7 +5735,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.15.2", + "hashbrown 0.15.3", ] [[package]] @@ -5543,12 +5772,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" -[[package]] -name = "match_cfg" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" - [[package]] name = "matchers" version = "0.1.0" @@ -5685,9 +5908,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", ] @@ -5719,13 +5942,13 @@ dependencies = [ "bytes", "colored", "futures-util", - "http 1.3.0", + "http 1.3.1", "http-body 1.0.1", "http-body-util", "hyper 1.6.0", "hyper-util", "log", - "rand 0.9.0", + "rand 0.9.1", "regex", "serde_json", "serde_urlencoded", @@ -5749,7 +5972,7 @@ dependencies = [ "smallvec", "tagptr", "thiserror 1.0.69", - "uuid 1.15.1", + "uuid 1.16.0", ] [[package]] @@ -5958,7 +6181,7 @@ dependencies = [ "futures", "genesis", "hex", - "igd-next 0.16.0", + "igd-next 0.16.1", "itertools 0.10.5", "k256 0.13.4", "kzg", @@ -6166,9 +6389,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.0" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde51589ab56b20a6f686b2c68f7a0bd6add753d697abf720d63f8db3ab7b1ad" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "oneshot_broadcast" @@ -6183,6 +6406,12 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -6196,7 +6425,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "786393f80485445794f6043fd3138854dd109cc6c4bd1a6383db304c9ce9b9ce" dependencies = [ "arrayvec", - "auto_impl", + "auto_impl 1.3.0", "bytes", "ethereum-types 0.14.1", "open-fastrlp-derive", @@ -6216,9 +6445,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.71" +version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ "bitflags 2.9.0", "cfg-if", @@ -6237,7 +6466,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -6248,18 +6477,18 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-src" -version = "300.4.2+3.4.1" +version = "300.5.0+3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168ce4e058f975fe43e89d9ccf78ca668601887ae736090aacc23ae353c298e2" +checksum = "e8ce546f549326b0e6052b649198487d91320875da901e7bd11a06d1ee3f9c2f" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.106" +version = "0.9.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" +checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847" dependencies = [ "cc", "libc", @@ -6290,6 +6519,15 @@ dependencies = [ "types", ] +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "overload" version = "0.1.1" @@ -6305,7 +6543,7 @@ dependencies = [ "ecdsa 0.16.9", "elliptic-curve 0.13.8", "primeorder", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] @@ -6368,7 +6606,7 @@ dependencies = [ "proc-macro-crate 3.3.0", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -6420,7 +6658,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.10", + "redox_syscall 0.5.12", "smallvec", "windows-targets 0.52.6", ] @@ -6448,7 +6686,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", ] [[package]] @@ -6460,7 +6698,7 @@ dependencies = [ "digest 0.10.7", "hmac 0.12.1", "password-hash", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] @@ -6490,9 +6728,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.15" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" +checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" dependencies = [ "memchr", "thiserror 2.0.12", @@ -6526,7 +6764,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -6557,7 +6795,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der 0.7.9", + "der 0.7.10", "spki 0.7.3", ] @@ -6623,7 +6861,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ "cpufeatures", - "opaque-debug", + "opaque-debug 0.3.1", "universal-hash", ] @@ -6635,7 +6873,7 @@ checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", "cpufeatures", - "opaque-debug", + "opaque-debug 0.3.1", "universal-hash", ] @@ -6657,7 +6895,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.8.23", + "zerocopy", ] [[package]] @@ -6670,12 +6908,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.30" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1ccf34da56fc294e7d4ccf69a85992b7dfb826b7cf57bac6a70bba3494cc08a" +checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" dependencies = [ "proc-macro2", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -6730,14 +6968,38 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ - "toml_edit 0.22.24", + "toml_edit 0.22.26", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", ] [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -6791,7 +7053,7 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -6822,7 +7084,7 @@ checksum = "4ee1c9ac207483d5e7db4940700de86a9aae46ef90c48b57f99fe7edb8345e49" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -6847,7 +7109,7 @@ checksum = "5e617cc9058daa5e1fe5a0d23ed745773a5ee354111dad1ec0235b0cc16b6730" dependencies = [ "cfg-if", "darwin-libproc", - "derive_more 0.99.19", + "derive_more 0.99.20", "glob", "mach2", "nix 0.24.3", @@ -6898,46 +7160,48 @@ dependencies = [ [[package]] name = "quickcheck_macros" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b22a693222d716a9587786f37ac3f6b4faedb5b80c23914e7303ff5a1d8016e9" +checksum = "f71ee38b42f8459a88d3362be6f9b841ad2d5421844f61eb1c59c11bff3ac14a" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.101", ] [[package]] name = "quinn" -version = "0.11.6" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" +checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" dependencies = [ "bytes", + "cfg_aliases", "futures-io", "pin-project-lite", "quinn-proto", "quinn-udp", "rustc-hash 2.1.1", - "rustls 0.23.23", + "rustls 0.23.27", "socket2", "thiserror 2.0.12", "tokio", "tracing", + "web-time", ] [[package]] name = "quinn-proto" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" +checksum = "bcbafbbdbb0f638fe3f35f3c56739f77a8a1d070cb25603226c83339b391472b" dependencies = [ "bytes", - "getrandom 0.2.15", - "rand 0.8.5", + "getrandom 0.3.2", + "rand 0.9.1", "ring", "rustc-hash 2.1.1", - "rustls 0.23.23", + "rustls 0.23.27", "rustls-pki-types", "slab", "thiserror 2.0.12", @@ -6948,9 +7212,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.10" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944" +checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" dependencies = [ "cfg_aliases", "libc", @@ -6962,13 +7226,19 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "r2d2" version = "0.8.10" @@ -6990,6 +7260,12 @@ dependencies = [ "rusqlite", ] +[[package]] +name = "radium" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def50a86306165861203e7f84ecffbbdfdea79f0e51039b33de1e952358c47ac" + [[package]] name = "radium" version = "0.6.2" @@ -7016,13 +7292,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", - "zerocopy 0.8.23", ] [[package]] @@ -7051,7 +7326,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] @@ -7060,7 +7335,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.1", + "getrandom 0.3.2", ] [[package]] @@ -7107,9 +7382,9 @@ dependencies = [ [[package]] name = "redb" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0a72cd7140de9fc3e318823b883abf819c20d478ec89ce880466dc2ef263c6" +checksum = "34bc6763177194266fc3773e2b2bb3693f7b02fdf461e285aa33202e3164b74e" dependencies = [ "libc", ] @@ -7125,9 +7400,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.10" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ "bitflags 2.9.0", ] @@ -7138,7 +7413,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "libredox", "thiserror 1.0.69", ] @@ -7251,13 +7526,9 @@ dependencies = [ [[package]] name = "resolv-conf" -version = "0.7.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" -dependencies = [ - "hostname", - "quick-error", -] +checksum = "fc7c8f7f733062b66dc1c63f9db168ac0b97a9210e247fa90fdc9ad08f51b302" [[package]] name = "rfc6979" @@ -7288,12 +7559,21 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", ] +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "rlp" version = "0.5.2" @@ -7354,9 +7634,9 @@ dependencies = [ [[package]] name = "ruint" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "825df406ec217a8116bd7b06897c6cc8f65ffefc15d030ae2c9540acc9ed50b6" +checksum = "78a46eb779843b2c4f21fac5773e25d6d5b7c8f0922876c91541790d2ca27eef" dependencies = [ "alloy-rlp", "arbitrary", @@ -7372,6 +7652,7 @@ dependencies = [ "primitive-types 0.12.2", "proptest", "rand 0.8.5", + "rand 0.9.1", "rlp", "ruint-macro", "serde", @@ -7493,14 +7774,14 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.2" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ "bitflags 2.9.0", "errno", "libc", - "linux-raw-sys 0.9.2", + "linux-raw-sys 0.9.4", "windows-sys 0.59.0", ] @@ -7532,14 +7813,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.23" +version = "0.23.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.102.8", + "rustls-webpki 0.103.2", "subtle", "zeroize", ] @@ -7564,11 +7845,12 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ "web-time", + "zeroize", ] [[package]] @@ -7592,6 +7874,17 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustls-webpki" +version = "0.103.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7149975849f1abb3832b246010ef62ccc80d3a76169517ada7188252b9cfb437" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.20" @@ -7640,6 +7933,15 @@ dependencies = [ "cipher 0.3.0", ] +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher 0.4.4", +] + [[package]] name = "same-file" version = "1.0.6" @@ -7670,7 +7972,7 @@ dependencies = [ "proc-macro-crate 3.3.0", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -7711,10 +8013,22 @@ checksum = "879588d8f90906e73302547e20fffefdd240eb3e0e744e142321f5d49dea0518" dependencies = [ "hmac 0.11.0", "pbkdf2 0.8.0", - "salsa20", + "salsa20 0.8.1", "sha2 0.9.9", ] +[[package]] +name = "scrypt" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f9e24d2b632954ded8ab2ef9fea0a0c769ea56ea98bddbafbad22caeeadf45d" +dependencies = [ + "hmac 0.12.1", + "pbkdf2 0.11.0", + "salsa20 0.10.2", + "sha2 0.10.9", +] + [[package]] name = "sct" version = "0.7.1" @@ -7733,7 +8047,7 @@ checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" dependencies = [ "base16ct 0.1.1", "der 0.6.1", - "generic-array", + "generic-array 0.14.7", "pkcs8 0.9.0", "subtle", "zeroize", @@ -7746,8 +8060,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct 0.2.0", - "der 0.7.9", - "generic-array", + "der 0.7.10", + "generic-array 0.14.7", "pkcs8 0.10.2", "subtle", "zeroize", @@ -7826,6 +8140,27 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-aux" +version = "4.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "207f67b28fe90fb596503a9bf0bf1ea5e831e21307658e177c5dfcdfc3ab8a0a" +dependencies = [ + "serde", + "serde-value", + "serde_json", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde_array_query" version = "0.1.0" @@ -7844,7 +8179,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -7867,7 +8202,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -7888,7 +8223,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.8.0", + "indexmap 2.9.0", "itoa", "ryu", "serde", @@ -7906,6 +8241,18 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69" +dependencies = [ + "block-buffer 0.7.3", + "digest 0.8.1", + "fake-simd", + "opaque-debug 0.2.3", +] + [[package]] name = "sha2" version = "0.9.9" @@ -7916,14 +8263,14 @@ dependencies = [ "cfg-if", "cpufeatures", "digest 0.9.0", - "opaque-debug", + "opaque-debug 0.3.1", ] [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -7939,7 +8286,7 @@ dependencies = [ "block-buffer 0.9.0", "digest 0.9.0", "keccak", - "opaque-debug", + "opaque-debug 0.3.1", ] [[package]] @@ -7979,9 +8326,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] @@ -8058,6 +8405,7 @@ dependencies = [ "sensitive_url", "serde_json", "tokio", + "tracing", "tracing-subscriber", "types", ] @@ -8149,9 +8497,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" dependencies = [ "arbitrary", ] @@ -8175,15 +8523,15 @@ dependencies = [ "rand_core 0.6.4", "ring", "rustc_version 0.4.1", - "sha2 0.10.8", + "sha2 0.10.9", "subtle", ] [[package]] name = "socket2" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ "libc", "windows-sys 0.52.0", @@ -8212,7 +8560,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der 0.7.9", + "der 0.7.10", ] [[package]] @@ -8393,9 +8741,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.100" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -8410,13 +8758,13 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -8525,15 +8873,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.18.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ - "cfg-if", "fastrand", - "getrandom 0.3.1", + "getrandom 0.3.2", "once_cell", - "rustix 1.0.2", + "rustix 1.0.7", "windows-sys 0.59.0", ] @@ -8552,7 +8899,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" dependencies = [ - "rustix 1.0.2", + "rustix 1.0.7", "windows-sys 0.59.0", ] @@ -8590,7 +8937,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -8601,7 +8948,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -8656,9 +9003,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.39" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", @@ -8671,15 +9018,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.20" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", @@ -8708,7 +9055,7 @@ dependencies = [ "pbkdf2 0.11.0", "rand 0.8.5", "rustc-hash 1.1.0", - "sha2 0.10.8", + "sha2 0.10.9", "thiserror 1.0.69", "unicode-normalization", "wasm-bindgen", @@ -8761,9 +9108,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.44.0" +version = "1.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a" +checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" dependencies = [ "backtrace", "bytes", @@ -8795,7 +9142,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -8843,9 +9190,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.13" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", @@ -8867,9 +9214,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" [[package]] name = "toml_edit" @@ -8877,20 +9224,20 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.8.0", + "indexmap 2.9.0", "toml_datetime", "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ - "indexmap 2.8.0", + "indexmap 2.9.0", "toml_datetime", - "winnow 0.7.3", + "winnow 0.7.10", ] [[package]] @@ -8931,7 +9278,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -9015,10 +9362,10 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "699e7fb6b3fdfe0c809916f251cf5132d64966858601695c3736630a87e7166a" dependencies = [ - "darling 0.20.10", + "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -9064,6 +9411,8 @@ dependencies = [ "bls", "compare_fields", "compare_fields_derive", + "context_deserialize", + "context_deserialize_derive", "criterion", "derivative", "eth2_interop_keypairs", @@ -9168,6 +9517,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -9256,17 +9611,17 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "serde", ] [[package]] name = "uuid" -version = "1.15.1" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" dependencies = [ - "getrandom 0.3.1", + "getrandom 0.3.2", ] [[package]] @@ -9286,6 +9641,7 @@ dependencies = [ "graffiti_file", "hyper 1.6.0", "initialized_validators", + "lighthouse_validator_store", "metrics", "monitoring_api", "parking_lot 0.12.3", @@ -9341,6 +9697,7 @@ dependencies = [ "health_metrics", "initialized_validators", "itertools 0.10.5", + "lighthouse_validator_store", "lighthouse_version", "logging", "parking_lot 0.12.3", @@ -9373,6 +9730,7 @@ name = "validator_http_metrics" version = "0.1.0" dependencies = [ "health_metrics", + "lighthouse_validator_store", "lighthouse_version", "logging", "malloc_utils", @@ -9384,7 +9742,6 @@ dependencies = [ "types", "validator_metrics", "validator_services", - "validator_store", "warp", "warp_utils", ] @@ -9427,9 +9784,7 @@ version = "0.1.0" dependencies = [ "beacon_node_fallback", "bls", - "doppelganger_service", "either", - "environment", "eth2", "futures", "graffiti_file", @@ -9437,6 +9792,7 @@ dependencies = [ "parking_lot 0.12.3", "safe_arith", "slot_clock", + "task_executor", "tokio", "tracing", "tree_hash", @@ -9449,19 +9805,9 @@ dependencies = [ name = "validator_store" version = "0.1.0" dependencies = [ - "account_utils", - "doppelganger_service", - "initialized_validators", - "logging", - "parking_lot 0.12.3", - "serde", - "signing_method", + "eth2", "slashing_protection", - "slot_clock", - "task_executor", - "tracing", "types", - "validator_metrics", ] [[package]] @@ -9582,9 +9928,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" -version = "0.13.3+wasi-0.2.2" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] @@ -9611,7 +9957,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "wasm-bindgen-shared", ] @@ -9646,7 +9992,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -9715,10 +10061,12 @@ dependencies = [ "account_utils", "async-channel 1.9.0", "environment", + "eth2", "eth2_keystore", "eth2_network_config", "futures", "initialized_validators", + "lighthouse_validator_store", "logging", "parking_lot 0.12.3", "reqwest", @@ -9762,9 +10110,9 @@ checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" [[package]] name = "widestring" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" +checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" [[package]] name = "winapi" @@ -9829,15 +10177,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.53.0" @@ -9854,13 +10193,26 @@ version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.58.0", + "windows-interface 0.58.0", "windows-result 0.2.0", - "windows-strings", + "windows-strings 0.1.0", "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement 0.60.0", + "windows-interface 0.59.1", + "windows-link", + "windows-result 0.3.2", + "windows-strings 0.4.0", +] + [[package]] name = "windows-implement" version = "0.58.0" @@ -9869,7 +10221,18 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", ] [[package]] @@ -9880,14 +10243,25 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", ] [[package]] name = "windows-link" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] name = "windows-result" @@ -9907,6 +10281,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-strings" version = "0.1.0" @@ -9917,6 +10300,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -10142,9 +10534,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.3" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" dependencies = [ "memchr", ] @@ -10161,9 +10553,9 @@ dependencies = [ [[package]] name = "wit-bindgen-rt" -version = "0.33.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags 2.9.0", ] @@ -10267,9 +10659,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" +checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" [[package]] name = "xmltree" @@ -10351,48 +10743,28 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "synstructure", ] [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ - "zerocopy-derive 0.7.35", -] - -[[package]] -name = "zerocopy" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" -dependencies = [ - "zerocopy-derive 0.8.23", + "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -10412,7 +10784,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "synstructure", ] @@ -10434,7 +10806,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -10456,7 +10828,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -10494,7 +10866,7 @@ version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" dependencies = [ - "zstd-safe 7.2.3", + "zstd-safe 7.2.4", ] [[package]] @@ -10509,18 +10881,18 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "7.2.3" +version = "7.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3051792fbdc2e1e143244dc28c60f73d8470e93f3f9cbd0ead44da5ed802722" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.14+zstd.1.5.7" +version = "2.0.15+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb060d4926e4ac3a3ad15d864e99ceb5f343c6b34f5bd6d81ae6ed417311be5" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index de5d6b541e..86cca0a259 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,8 @@ members = [ "common/warp_utils", "common/workspace_members", + "consensus/context_deserialize", + "consensus/context_deserialize_derive", "consensus/fixed_bytes", "consensus/fork_choice", "consensus/int_to_bytes", @@ -96,11 +98,11 @@ members = [ "validator_client/http_api", "validator_client/http_metrics", "validator_client/initialized_validators", + "validator_client/lighthouse_validator_store", "validator_client/signing_method", "validator_client/slashing_protection", "validator_client/validator_metrics", "validator_client/validator_services", - "validator_client/validator_store", "validator_manager", ] @@ -127,6 +129,8 @@ clap = { version = "4.5.4", features = ["derive", "cargo", "wrap_help"] } # feature ourselves when desired. c-kzg = { version = "1", default-features = false } compare_fields_derive = { path = "common/compare_fields_derive" } +context_deserialize = { path = "consensus/context_deserialize" } +context_deserialize_derive = { path = "consensus/context_deserialize_derive" } criterion = "0.5" delay_map = "0.4" derivative = "2" @@ -141,12 +145,14 @@ ethereum_ssz = "0.8.2" ethereum_ssz_derive = "0.8.2" ethers-core = "1" ethers-providers = { version = "1", default-features = false } +ethers-signers = { version = "1", default-features = false } +ethers-middleware = { version = "1", default-features = false } exit-future = "0.2" fnv = "1" fs2 = "0.4" futures = "0.3" graffiti_file = { path = "validator_client/graffiti_file" } -gossipsub = { package = "libp2p-gossipsub", git = "https://github.com/sigp/rust-libp2p.git", rev = "7a36e4c" } +gossipsub = { package = "libp2p-gossipsub", git = "https://github.com/sigp/rust-libp2p.git", rev = "61b2820" } hex = "0.4" hashlink = "0.9.0" hyper = "1" @@ -159,6 +165,7 @@ maplit = "1" milhouse = "0.5" mockito = "1.5.0" num_cpus = "1" +once_cell = "1.17.1" parking_lot = "0.12" paste = "1" prometheus = { version = "0.13", default-features = false } @@ -225,7 +232,6 @@ compare_fields = { path = "common/compare_fields" } deposit_contract = { path = "common/deposit_contract" } directory = { path = "common/directory" } doppelganger_service = { path = "validator_client/doppelganger_service" } -validator_services = { path = "validator_client/validator_services" } environment = { path = "lighthouse/environment" } eth1 = { path = "beacon_node/eth1" } eth1_test_rig = { path = "testing/eth1_test_rig" } @@ -247,6 +253,7 @@ int_to_bytes = { path = "consensus/int_to_bytes" } kzg = { path = "crypto/kzg" } metrics = { path = "common/metrics" } lighthouse_network = { path = "beacon_node/lighthouse_network" } +lighthouse_validator_store = { path = "validator_client/lighthouse_validator_store" } lighthouse_version = { path = "common/lighthouse_version" } workspace_members = { path = "common/workspace_members" } lockfile = { path = "common/lockfile" } @@ -278,6 +285,7 @@ validator_dir = { path = "common/validator_dir" } validator_http_api = { path = "validator_client/http_api" } validator_http_metrics = { path = "validator_client/http_metrics" } validator_metrics = { path = "validator_client/validator_metrics" } +validator_services = { path = "validator_client/validator_services" } validator_store = { path = "validator_client/validator_store" } validator_test_rig = { path = "testing/validator_test_rig" } warp_utils = { path = "common/warp_utils" } @@ -290,5 +298,12 @@ lto = "fat" codegen-units = 1 incremental = false +[profile.reproducible] +inherits = "release" +debug = false +panic = "abort" +codegen-units = 1 +overflow-checks = true + [patch.crates-io] quick-protobuf = { git = "https://github.com/sigp/quick-protobuf.git", rev = "681f413312404ab6e51f0b46f39b0075c6f4ebfd" } diff --git a/Cross.toml b/Cross.toml index 8181967f32..391e8751c8 100644 --- a/Cross.toml +++ b/Cross.toml @@ -4,6 +4,11 @@ pre-build = ["apt-get install -y cmake clang-5.0"] [target.aarch64-unknown-linux-gnu] pre-build = ["apt-get install -y cmake clang-5.0"] +[target.riscv64gc-unknown-linux-gnu] +pre-build = ["apt-get install -y cmake clang"] +# Use the most recent Cross image for RISCV because the stable 0.2.5 image doesn't work +image = "ghcr.io/cross-rs/riscv64gc-unknown-linux-gnu:main" + # Allow setting page size limits for jemalloc at build time: # For certain architectures (like aarch64), we must compile # jemalloc with support for large page sizes, otherwise the host's diff --git a/Dockerfile.reproducible b/Dockerfile.reproducible new file mode 100644 index 0000000000..df57616874 --- /dev/null +++ b/Dockerfile.reproducible @@ -0,0 +1,44 @@ +# Define the Rust image as an argument with a default to x86_64 Rust 1.82 image based on Debian Bullseye +ARG RUST_IMAGE="rust:1.82-bullseye@sha256:ac7fe7b0c9429313c0fe87d3a8993998d1fe2be9e3e91b5e2ec05d3a09d87128" +FROM ${RUST_IMAGE} AS builder + +# Install specific version of the build dependencies +RUN apt-get update && apt-get install -y libclang-dev=1:11.0-51+nmu5 cmake=3.18.4-2+deb11u1 + +# Add target architecture argument with default value +ARG RUST_TARGET="x86_64-unknown-linux-gnu" + +# Copy the project to the container +COPY . /app +WORKDIR /app + +# Get the latest commit timestamp and set SOURCE_DATE_EPOCH (default it to 0 if not passed) +ARG SOURCE_DATE=0 + +# Set environment variables for reproducibility +ARG RUSTFLAGS="-C link-arg=-Wl,--build-id=none -C metadata='' --remap-path-prefix $(pwd)=." +ENV SOURCE_DATE_EPOCH=$SOURCE_DATE \ + CARGO_INCREMENTAL=0 \ + LC_ALL=C \ + TZ=UTC \ + RUSTFLAGS="${RUSTFLAGS}" + +# Set the default features if not provided +ARG FEATURES="gnosis,slasher-lmdb,slasher-mdbx,slasher-redb,jemalloc" + +# Set the default profile if not provided +ARG PROFILE="reproducible" + +# Build the project with the reproducible settings +RUN cargo build --bin lighthouse \ + --features "${FEATURES}" \ + --profile "${PROFILE}" \ + --locked \ + --target "${RUST_TARGET}" + +RUN mv /app/target/${RUST_TARGET}/${PROFILE}/lighthouse /lighthouse + +# Create a minimal final image with just the binary +FROM gcr.io/distroless/cc-debian12:nonroot-6755e21ccd99ddead6edc8106ba03888cbeed41a +COPY --from=builder /lighthouse /lighthouse +ENTRYPOINT [ "/lighthouse" ] diff --git a/Makefile b/Makefile index d58553fe88..d27e7edd13 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,8 @@ X86_64_TAG = "x86_64-unknown-linux-gnu" BUILD_PATH_X86_64 = "target/$(X86_64_TAG)/release" AARCH64_TAG = "aarch64-unknown-linux-gnu" BUILD_PATH_AARCH64 = "target/$(AARCH64_TAG)/release" +RISCV64_TAG = "riscv64gc-unknown-linux-gnu" +BUILD_PATH_RISCV64 = "target/$(RISCV64_TAG)/release" PINNED_NIGHTLY ?= nightly @@ -67,6 +69,8 @@ build-aarch64: # pages, which are commonly used by aarch64 systems. # See: https://github.com/sigp/lighthouse/issues/5244 JEMALLOC_SYS_WITH_LG_PAGE=16 cross build --bin lighthouse --target aarch64-unknown-linux-gnu --features "portable,$(CROSS_FEATURES)" --profile "$(CROSS_PROFILE)" --locked +build-riscv64: + cross build --bin lighthouse --target riscv64gc-unknown-linux-gnu --features "portable,$(CROSS_FEATURES)" --profile "$(CROSS_PROFILE)" --locked build-lcli-x86_64: cross build --bin lcli --target x86_64-unknown-linux-gnu --features "portable" --profile "$(CROSS_PROFILE)" --locked @@ -75,6 +79,39 @@ build-lcli-aarch64: # pages, which are commonly used by aarch64 systems. # See: https://github.com/sigp/lighthouse/issues/5244 JEMALLOC_SYS_WITH_LG_PAGE=16 cross build --bin lcli --target aarch64-unknown-linux-gnu --features "portable" --profile "$(CROSS_PROFILE)" --locked +build-lcli-riscv64: + cross build --bin lcli --target riscv64gc-unknown-linux-gnu --features "portable" --profile "$(CROSS_PROFILE)" --locked + +# extracts the current source date for reproducible builds +SOURCE_DATE := $(shell git log -1 --pretty=%ct) + +# Default image for x86_64 +RUST_IMAGE_AMD64 ?= rust:1.82-bullseye@sha256:ac7fe7b0c9429313c0fe87d3a8993998d1fe2be9e3e91b5e2ec05d3a09d87128 + +# Reproducible build for x86_64 +build-reproducible-x86_64: + DOCKER_BUILDKIT=1 docker build \ + --build-arg RUST_TARGET="x86_64-unknown-linux-gnu" \ + --build-arg RUST_IMAGE=$(RUST_IMAGE_AMD64) \ + --build-arg SOURCE_DATE=$(SOURCE_DATE) \ + -f Dockerfile.reproducible \ + -t lighthouse:reproducible-amd64 . + +# Default image for arm64 +RUST_IMAGE_ARM64 ?= rust:1.82-bullseye@sha256:3c1b8b6487513ad4e753d008b960260f5bcc81bf110883460f6ed3cd72bf439b + +# Reproducible build for aarch64 +build-reproducible-aarch64: + DOCKER_BUILDKIT=1 docker build \ + --platform linux/arm64 \ + --build-arg RUST_TARGET="aarch64-unknown-linux-gnu" \ + --build-arg RUST_IMAGE=$(RUST_IMAGE_ARM64) \ + --build-arg SOURCE_DATE=$(SOURCE_DATE) \ + -f Dockerfile.reproducible \ + -t lighthouse:reproducible-arm64 . + +# Build both architectures +build-reproducible-all: build-reproducible-x86_64 build-reproducible-aarch64 # Create a `.tar.gz` containing a binary for a specific target. define tarball_release_binary @@ -95,6 +132,9 @@ build-release-tarballs: $(call tarball_release_binary,$(BUILD_PATH_X86_64),$(X86_64_TAG),"") $(MAKE) build-aarch64 $(call tarball_release_binary,$(BUILD_PATH_AARCH64),$(AARCH64_TAG),"") + $(MAKE) build-riscv64 + $(call tarball_release_binary,$(BUILD_PATH_RISCV64),$(RISCV64_TAG),"") + # Runs the full workspace tests in **release**, without downloading any additional # test vectors. diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index a7752d621f..071e2681dd 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -22,6 +22,7 @@ eth2_wallet_manager = { path = "../common/eth2_wallet_manager" } filesystem = { workspace = true } safe_arith = { workspace = true } sensitive_url = { workspace = true } +serde_json = { workspace = true } slashing_protection = { workspace = true } slot_clock = { workspace = true } tokio = { workspace = true } diff --git a/account_manager/src/validator/exit.rs b/account_manager/src/validator/exit.rs index ea1a24da1f..1393d0f152 100644 --- a/account_manager/src/validator/exit.rs +++ b/account_manager/src/validator/exit.rs @@ -11,6 +11,7 @@ use eth2_keystore::Keystore; use eth2_network_config::Eth2NetworkConfig; use safe_arith::SafeArith; use sensitive_url::SensitiveUrl; +use serde_json; use slot_clock::{SlotClock, SystemTimeSlotClock}; use std::path::{Path, PathBuf}; use std::time::Duration; @@ -24,10 +25,11 @@ pub const BEACON_SERVER_FLAG: &str = "beacon-node"; pub const NO_WAIT: &str = "no-wait"; pub const NO_CONFIRMATION: &str = "no-confirmation"; pub const PASSWORD_PROMPT: &str = "Enter the keystore password"; +pub const PRESIGN: &str = "presign"; pub const DEFAULT_BEACON_NODE: &str = "http://localhost:5052/"; pub const CONFIRMATION_PHRASE: &str = "Exit my validator"; -pub const WEBSITE_URL: &str = "https://lighthouse-book.sigmaprime.io/voluntary-exit.html"; +pub const WEBSITE_URL: &str = "https://lighthouse-book.sigmaprime.io/validator_voluntary_exit.html"; pub fn cli_app() -> Command { Command::new("exit") @@ -74,6 +76,15 @@ pub fn cli_app() -> Command { .action(ArgAction::SetTrue) .help_heading(FLAG_HEADER) ) + .arg( + Arg::new(PRESIGN) + .long(PRESIGN) + .help("Only presign the voluntary exit message without publishing it") + .default_value("false") + .action(ArgAction::SetTrue) + .help_heading(FLAG_HEADER) + .display_order(0) + ) } pub fn cli_run(matches: &ArgMatches, env: Environment) -> Result<(), String> { @@ -84,6 +95,7 @@ pub fn cli_run(matches: &ArgMatches, env: Environment) -> Result< let stdin_inputs = cfg!(windows) || matches.get_flag(STDIN_INPUTS_FLAG); let no_wait = matches.get_flag(NO_WAIT); let no_confirmation = matches.get_flag(NO_CONFIRMATION); + let presign = matches.get_flag(PRESIGN); let spec = env.eth2_config().spec.clone(); let server_url: String = clap_utils::parse_required(matches, BEACON_SERVER_FLAG)?; @@ -107,6 +119,7 @@ pub fn cli_run(matches: &ArgMatches, env: Environment) -> Result< ð2_network_config, no_wait, no_confirmation, + presign, ))?; Ok(()) @@ -123,6 +136,7 @@ async fn publish_voluntary_exit( eth2_network_config: &Eth2NetworkConfig, no_wait: bool, no_confirmation: bool, + presign: bool, ) -> Result<(), String> { let genesis_data = get_geneisis_data(client).await?; let testnet_genesis_root = eth2_network_config @@ -154,6 +168,23 @@ async fn publish_voluntary_exit( validator_index, }; + // Sign the voluntary exit. We sign ahead of the prompt as that step is only important for the broadcast + let signed_voluntary_exit = + voluntary_exit.sign(&keypair.sk, genesis_data.genesis_validators_root, spec); + if presign { + eprintln!( + "Successfully pre-signed voluntary exit for validator {}. Not publishing.", + keypair.pk + ); + + // Convert to JSON and print + let string_output = serde_json::to_string_pretty(&signed_voluntary_exit) + .map_err(|e| format!("Unable to convert to JSON: {}", e))?; + + println!("{}", string_output); + return Ok(()); + } + eprintln!( "Publishing a voluntary exit for validator: {} \n", keypair.pk @@ -174,9 +205,7 @@ async fn publish_voluntary_exit( }; if confirmation == CONFIRMATION_PHRASE { - // Sign and publish the voluntary exit to network - let signed_voluntary_exit = - voluntary_exit.sign(&keypair.sk, genesis_data.genesis_validators_root, spec); + // Publish the voluntary exit to network client .post_beacon_pool_voluntary_exits(&signed_voluntary_exit) .await diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index cf963535c7..30d6846964 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "beacon_node" -version = "7.0.0-beta.5" +version = "7.1.0-beta.0" authors = [ "Paul Hauner ", "Age Manning ( num_of_blobs: usize, spec: &ChainSpec, -) -> (SignedBeaconBlock, BlobsList) { +) -> (SignedBeaconBlock, BlobsList, KzgProofs) { let mut block = BeaconBlock::Deneb(BeaconBlockDeneb::empty(spec)); let mut body = block.body_mut(); let blob_kzg_commitments = body.blob_kzg_commitments_mut().unwrap(); @@ -27,8 +27,9 @@ fn create_test_block_and_blobs( .map(|_| Blob::::default()) .collect::>() .into(); + let proofs = vec![KzgProof::empty(); num_of_blobs * spec.number_of_columns as usize].into(); - (signed_block, blobs) + (signed_block, blobs, proofs) } fn all_benches(c: &mut Criterion) { @@ -37,10 +38,11 @@ fn all_benches(c: &mut Criterion) { let kzg = get_kzg(&spec); for blob_count in [1, 2, 3, 6] { - let (signed_block, blobs) = create_test_block_and_blobs::(blob_count, &spec); + let (signed_block, blobs, proofs) = create_test_block_and_blobs::(blob_count, &spec); let column_sidecars = blobs_to_data_column_sidecars( &blobs.iter().collect::>(), + proofs.to_vec(), &signed_block, &kzg, &spec, diff --git a/beacon_node/beacon_chain/src/attestation_verification.rs b/beacon_node/beacon_chain/src/attestation_verification.rs index 6f1174c1ba..d69667f3de 100644 --- a/beacon_node/beacon_chain/src/attestation_verification.rs +++ b/beacon_node/beacon_chain/src/attestation_verification.rs @@ -272,12 +272,12 @@ pub enum Error { /// /// We were unable to process this attestation due to an internal error. It's unclear if the /// attestation is valid. - BeaconChainError(BeaconChainError), + BeaconChainError(Box), } impl From for Error { fn from(e: BeaconChainError) -> Self { - Self::BeaconChainError(e) + Self::BeaconChainError(Box::new(e)) } } @@ -525,7 +525,7 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { .observed_attestations .write() .is_known_subset(attestation, observed_attestation_key_root) - .map_err(|e| Error::BeaconChainError(e.into()))? + .map_err(|e| Error::BeaconChainError(Box::new(e.into())))? { metrics::inc_counter(&metrics::AGGREGATED_ATTESTATION_SUBSETS); return Err(Error::AttestationSupersetKnown( @@ -628,7 +628,7 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { if !SelectionProof::from(selection_proof) .is_aggregator(committee.committee.len(), &chain.spec) - .map_err(|e| Error::BeaconChainError(e.into()))? + .map_err(|e| Error::BeaconChainError(Box::new(e.into())))? { return Err(Error::InvalidSelectionProof { aggregator_index }); } @@ -698,7 +698,7 @@ impl<'a, T: BeaconChainTypes> VerifiedAggregatedAttestation<'a, T> { .observed_attestations .write() .observe_item(attestation, Some(observed_attestation_key_root)) - .map_err(|e| Error::BeaconChainError(e.into()))? + .map_err(|e| Error::BeaconChainError(Box::new(e.into())))? { metrics::inc_counter(&metrics::AGGREGATED_ATTESTATION_SUBSETS); return Err(Error::AttestationSupersetKnown( diff --git a/beacon_node/beacon_chain/src/beacon_block_reward.rs b/beacon_node/beacon_chain/src/beacon_block_reward.rs index 8808a3f121..ecaa4f45e7 100644 --- a/beacon_node/beacon_chain/src/beacon_block_reward.rs +++ b/beacon_node/beacon_chain/src/beacon_block_reward.rs @@ -135,7 +135,7 @@ impl BeaconChain { state .get_validator(proposer_slashing.proposer_index() as usize)? .effective_balance - .safe_div(self.spec.whistleblower_reward_quotient)?, + .safe_div(self.spec.whistleblower_reward_quotient_for_state(state))?, )?; } @@ -157,7 +157,7 @@ impl BeaconChain { state .get_validator(attester_index as usize)? .effective_balance - .safe_div(self.spec.whistleblower_reward_quotient)?, + .safe_div(self.spec.whistleblower_reward_quotient_for_state(state))?, )?; } } diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 0f376c0ec4..b342e4afd7 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -31,9 +31,9 @@ use crate::eth1_chain::{Eth1Chain, Eth1ChainBackend}; use crate::eth1_finalization_cache::{Eth1FinalizationCache, Eth1FinalizationData}; use crate::events::ServerSentEventHandler; use crate::execution_payload::{get_execution_payload, NotifyExecutionLayer, PreparePayloadHandle}; +use crate::fetch_blobs::EngineGetBlobsOutput; use crate::fork_choice_signal::{ForkChoiceSignalRx, ForkChoiceSignalTx, ForkChoiceWaitResult}; use crate::graffiti_calculator::GraffitiCalculator; -use crate::head_tracker::{HeadTracker, HeadTrackerReader, SszHeadTracker}; use crate::inclusion_list_verification::GossipInclusionListError; use crate::inclusion_list_verification::GossipVerifiedInclusionList; use crate::kzg_utils::reconstruct_blobs; @@ -59,7 +59,7 @@ use crate::observed_block_producers::ObservedBlockProducers; use crate::observed_data_sidecars::ObservedDataSidecars; use crate::observed_operations::{ObservationOutcome, ObservedOperations}; use crate::observed_slashable::ObservedSlashable; -use crate::persisted_beacon_chain::{PersistedBeaconChain, DUMMY_CANONICAL_HEAD_BLOCK_ROOT}; +use crate::persisted_beacon_chain::PersistedBeaconChain; use crate::persisted_fork_choice::PersistedForkChoice; use crate::pre_finalization_cache::PreFinalizationBlockCache; use crate::shuffling_cache::{BlockShufflingIds, ShufflingCache}; @@ -94,6 +94,7 @@ use operation_pool::{ }; use parking_lot::{Mutex, RwLock, RwLockWriteGuard}; use proto_array::{DoNotReOrg, ProposerHeadError}; +use rand::RngCore; use safe_arith::SafeArith; use slasher::Slasher; use slot_clock::SlotClock; @@ -124,12 +125,11 @@ use store::{ KeyValueStore, KeyValueStoreOp, StoreItem, StoreOp, }; use task_executor::{ShutdownReason, TaskExecutor}; -use tokio::sync::oneshot; use tokio_stream::Stream; use tracing::{debug, error, info, trace, warn}; use tree_hash::TreeHash; use types::blob_sidecar::FixedBlobSidecarList; -use types::data_column_sidecar::{ColumnIndex, DataColumnIdentifier}; +use types::data_column_sidecar::ColumnIndex; use types::payload::BlockProductionVersion; use types::*; @@ -456,8 +456,6 @@ pub struct BeaconChain { /// A handler for events generated by the beacon chain. This is only initialized when the /// HTTP server is enabled. pub event_handler: Option>, - /// Used to track the heads of the beacon chain. - pub(crate) head_tracker: Arc, /// Caches the attester shuffling for a given epoch and shuffling key root. pub shuffling_cache: RwLock, /// A cache of eth1 deposit data at epoch boundaries for deposit finalization @@ -498,6 +496,8 @@ pub struct BeaconChain { pub data_availability_checker: Arc>, /// The KZG trusted setup used by this chain. pub kzg: Arc, + /// RNG instance used by the chain. Currently used for shuffling column sidecars in block publishing. + pub rng: Arc>>, } pub enum BeaconBlockResponseWrapper { @@ -611,57 +611,13 @@ impl BeaconChain { }) } - /// Persists the head tracker and fork choice. + /// Return a database operation for writing the `PersistedBeaconChain` to disk. /// - /// We do it atomically even though no guarantees need to be made about blocks from - /// the head tracker also being present in fork choice. - pub fn persist_head_and_fork_choice(&self) -> Result<(), Error> { - let mut batch = vec![]; - - let _head_timer = metrics::start_timer(&metrics::PERSIST_HEAD); - - // Hold a lock to head_tracker until it has been persisted to disk. Otherwise there's a race - // condition with the pruning thread which can result in a block present in the head tracker - // but absent in the DB. This inconsistency halts pruning and dramastically increases disk - // size. Ref: https://github.com/sigp/lighthouse/issues/4773 - let head_tracker = self.head_tracker.0.read(); - batch.push(self.persist_head_in_batch(&head_tracker)); - - let _fork_choice_timer = metrics::start_timer(&metrics::PERSIST_FORK_CHOICE); - batch.push(self.persist_fork_choice_in_batch()); - - self.store.hot_db.do_atomically(batch)?; - drop(head_tracker); - - Ok(()) - } - - /// Return a `PersistedBeaconChain` without reference to a `BeaconChain`. - pub fn make_persisted_head( - genesis_block_root: Hash256, - head_tracker_reader: &HeadTrackerReader, - ) -> PersistedBeaconChain { - PersistedBeaconChain { - _canonical_head_block_root: DUMMY_CANONICAL_HEAD_BLOCK_ROOT, - genesis_block_root, - ssz_head_tracker: SszHeadTracker::from_map(head_tracker_reader), - } - } - - /// Return a database operation for writing the beacon chain head to disk. - pub fn persist_head_in_batch( - &self, - head_tracker_reader: &HeadTrackerReader, - ) -> KeyValueStoreOp { - Self::persist_head_in_batch_standalone(self.genesis_block_root, head_tracker_reader) - } - - pub fn persist_head_in_batch_standalone( - genesis_block_root: Hash256, - head_tracker_reader: &HeadTrackerReader, - ) -> KeyValueStoreOp { - Self::make_persisted_head(genesis_block_root, head_tracker_reader) - .as_kv_store_op(BEACON_CHAIN_DB_KEY) + /// These days the `PersistedBeaconChain` is only used to store the genesis block root, so it + /// should only ever be written once at startup. It used to be written more frequently, but + /// this is no longer necessary. + pub fn persist_head_in_batch_standalone(genesis_block_root: Hash256) -> KeyValueStoreOp { + PersistedBeaconChain { genesis_block_root }.as_kv_store_op(BEACON_CHAIN_DB_KEY) } /// Load fork choice from disk, returning `None` if it isn't found. @@ -1154,23 +1110,25 @@ impl BeaconChain { .map_or_else(|| self.get_blobs(block_root), Ok) } - pub fn get_data_column_checking_all_caches( + pub fn get_data_columns_checking_all_caches( &self, block_root: Hash256, - index: ColumnIndex, - ) -> Result>>, Error> { - if let Some(column) = self + indices: &[ColumnIndex], + ) -> Result, Error> { + let all_cached_columns_opt = self .data_availability_checker - .get_data_column(&DataColumnIdentifier { block_root, index })? - { - return Ok(Some(column)); - } + .get_data_columns(block_root) + .or_else(|| self.early_attester_cache.get_data_columns(block_root)); - if let Some(columns) = self.early_attester_cache.get_data_columns(block_root) { - return Ok(columns.iter().find(|c| c.index == index).cloned()); + if let Some(mut all_cached_columns) = all_cached_columns_opt { + all_cached_columns.retain(|col| indices.contains(&col.index)); + Ok(all_cached_columns) + } else { + indices + .iter() + .filter_map(|index| self.get_data_column(&block_root, index).transpose()) + .collect::>() } - - self.get_data_column(&block_root, &index) } /// Returns the block at the given root, if any. @@ -1454,12 +1412,13 @@ impl BeaconChain { /// /// Returns `(block_root, block_slot)`. pub fn heads(&self) -> Vec<(Hash256, Slot)> { - self.head_tracker.heads() - } - - /// Only used in tests. - pub fn knows_head(&self, block_hash: &SignedBeaconBlockHash) -> bool { - self.head_tracker.contains_head((*block_hash).into()) + self.canonical_head + .fork_choice_read_lock() + .proto_array() + .heads_descended_from_finalization::() + .iter() + .map(|node| (node.root, node.slot)) + .collect() } /// Returns the `BeaconState` at the given slot. @@ -1788,8 +1747,6 @@ impl BeaconChain { let notif = ManualFinalizationNotification { state_root: state_root.into(), checkpoint, - head_tracker: self.head_tracker.clone(), - genesis_block_root: self.genesis_block_root, }; self.store_migrator.process_manual_finalization(notif); @@ -2941,7 +2898,7 @@ impl BeaconChain { pub fn filter_chain_segment( self: &Arc, chain_segment: Vec>, - ) -> Result>, ChainSegmentResult> { + ) -> Result>, Box> { // This function will never import any blocks. let imported_blocks = vec![]; let mut filtered_chain_segment = Vec::with_capacity(chain_segment.len()); @@ -2958,10 +2915,10 @@ impl BeaconChain { for (i, block) in chain_segment.into_iter().enumerate() { // Ensure the block is the correct structure for the fork at `block.slot()`. if let Err(e) = block.as_block().fork_name(&self.spec) { - return Err(ChainSegmentResult::Failed { + return Err(Box::new(ChainSegmentResult::Failed { imported_blocks, error: BlockError::InconsistentFork(e), - }); + })); } let block_root = block.block_root(); @@ -2973,18 +2930,18 @@ impl BeaconChain { // Without this check it would be possible to have a block verified using the // incorrect shuffling. That would be bad, mmkay. if block_root != *child_parent_root { - return Err(ChainSegmentResult::Failed { + return Err(Box::new(ChainSegmentResult::Failed { imported_blocks, error: BlockError::NonLinearParentRoots, - }); + })); } // Ensure that the slots are strictly increasing throughout the chain segment. if *child_slot <= block.slot() { - return Err(ChainSegmentResult::Failed { + return Err(Box::new(ChainSegmentResult::Failed { imported_blocks, error: BlockError::NonLinearSlots, - }); + })); } } @@ -3015,18 +2972,18 @@ impl BeaconChain { // The block has a known parent that does not descend from the finalized block. // There is no need to process this block or any children. Err(BlockError::NotFinalizedDescendant { block_parent_root }) => { - return Err(ChainSegmentResult::Failed { + return Err(Box::new(ChainSegmentResult::Failed { imported_blocks, error: BlockError::NotFinalizedDescendant { block_parent_root }, - }); + })); } // If there was an error whilst determining if the block was invalid, return that // error. Err(BlockError::BeaconChainError(e)) => { - return Err(ChainSegmentResult::Failed { + return Err(Box::new(ChainSegmentResult::Failed { imported_blocks, error: BlockError::BeaconChainError(e), - }); + })); } // If the block was decided to be irrelevant for any other reason, don't include // this block or any of it's children in the filtered chain segment. @@ -3071,11 +3028,11 @@ impl BeaconChain { ); let mut filtered_chain_segment = match filtered_chain_segment_future.await { Ok(Ok(filtered_segment)) => filtered_segment, - Ok(Err(segment_result)) => return segment_result, + Ok(Err(segment_result)) => return *segment_result, Err(error) => { return ChainSegmentResult::Failed { imported_blocks, - error: BlockError::BeaconChainError(error), + error: BlockError::BeaconChainError(error.into()), } } }; @@ -3114,7 +3071,7 @@ impl BeaconChain { Err(error) => { return ChainSegmentResult::Failed { imported_blocks, - error: BlockError::BeaconChainError(error), + error: BlockError::BeaconChainError(error.into()), }; } }; @@ -3350,16 +3307,11 @@ impl BeaconChain { } /// Process blobs retrieved from the EL and returns the `AvailabilityProcessingStatus`. - /// - /// `data_column_recv`: An optional receiver for `DataColumnSidecarList`. - /// If PeerDAS is enabled, this receiver will be provided and used to send - /// the `DataColumnSidecar`s once they have been successfully computed. pub async fn process_engine_blobs( self: &Arc, slot: Slot, block_root: Hash256, - blobs: FixedBlobSidecarList, - data_column_recv: Option>>, + engine_get_blobs_output: EngineGetBlobsOutput, ) -> Result { // If this block has already been imported to forkchoice it must have been available, so // we don't need to process its blobs again. @@ -3373,15 +3325,12 @@ impl BeaconChain { // process_engine_blobs is called for both pre and post PeerDAS. However, post PeerDAS // consumers don't expect the blobs event to fire erratically. - if !self - .spec - .is_peer_das_enabled_for_epoch(slot.epoch(T::EthSpec::slots_per_epoch())) - { + if let EngineGetBlobsOutput::Blobs(blobs) = &engine_get_blobs_output { self.emit_sse_blob_sidecar_events(&block_root, blobs.iter().flatten().map(Arc::as_ref)); } let r = self - .check_engine_blob_availability_and_import(slot, block_root, blobs, data_column_recv) + .check_engine_blobs_availability_and_import(slot, block_root, engine_get_blobs_output) .await; self.remove_notified(&block_root, r) } @@ -3660,20 +3609,23 @@ impl BeaconChain { Ok(status) } - Err(e @ BlockError::BeaconChainError(BeaconChainError::TokioJoin(_))) => { - debug!( - error = ?e, - "Beacon block processing cancelled" - ); - Err(e) - } - // There was an error whilst attempting to verify and import the block. The block might - // be partially verified or partially imported. Err(BlockError::BeaconChainError(e)) => { - crit!( - error = ?e, - "Beacon block processing error" - ); + match e.as_ref() { + BeaconChainError::TokioJoin(e) => { + debug!( + error = ?e, + "Beacon block processing cancelled" + ); + } + _ => { + // There was an error whilst attempting to verify and import the block. The block might + // be partially verified or partially imported. + crit!( + error = ?e, + "Beacon block processing error" + ); + } + }; Err(BlockError::BeaconChainError(e)) } // The block failed verification. @@ -3805,7 +3757,7 @@ impl BeaconChain { header.message.proposer_index, block_root, ) - .map_err(|e| BlockError::BeaconChainError(e.into()))?; + .map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))?; if let Some(slasher) = self.slasher.as_ref() { slasher.accept_block_header(header); } @@ -3831,20 +3783,24 @@ impl BeaconChain { .await } - async fn check_engine_blob_availability_and_import( + async fn check_engine_blobs_availability_and_import( self: &Arc, slot: Slot, block_root: Hash256, - blobs: FixedBlobSidecarList, - data_column_recv: Option>>, + engine_get_blobs_output: EngineGetBlobsOutput, ) -> Result { - self.check_blobs_for_slashability(block_root, &blobs)?; - let availability = self.data_availability_checker.put_engine_blobs( - block_root, - slot.epoch(T::EthSpec::slots_per_epoch()), - blobs, - data_column_recv, - )?; + let availability = match engine_get_blobs_output { + EngineGetBlobsOutput::Blobs(blobs) => { + self.check_blobs_for_slashability(block_root, &blobs)?; + self.data_availability_checker + .put_engine_blobs(block_root, blobs)? + } + EngineGetBlobsOutput::CustodyColumns(data_columns) => { + self.check_columns_for_slashability(block_root, &data_columns)?; + self.data_availability_checker + .put_engine_data_columns(block_root, data_columns)? + } + }; self.process_availability(slot, availability, || Ok(())) .await @@ -3858,27 +3814,7 @@ impl BeaconChain { block_root: Hash256, custody_columns: DataColumnSidecarList, ) -> Result { - // Need to scope this to ensure the lock is dropped before calling `process_availability` - // Even an explicit drop is not enough to convince the borrow checker. - { - let mut slashable_cache = self.observed_slashable.write(); - // Assumes all items in custody_columns are for the same block_root - if let Some(column) = custody_columns.first() { - let header = &column.signed_block_header; - if verify_header_signature::(self, header).is_ok() { - slashable_cache - .observe_slashable( - header.message.slot, - header.message.proposer_index, - block_root, - ) - .map_err(|e| BlockError::BeaconChainError(e.into()))?; - if let Some(slasher) = self.slasher.as_ref() { - slasher.accept_block_header(header.clone()); - } - } - } - } + self.check_columns_for_slashability(block_root, &custody_columns)?; // This slot value is purely informative for the consumers of // `AvailabilityProcessingStatus::MissingComponents` to log an error with a slot. @@ -3890,6 +3826,31 @@ impl BeaconChain { .await } + fn check_columns_for_slashability( + self: &Arc, + block_root: Hash256, + custody_columns: &DataColumnSidecarList, + ) -> Result<(), BlockError> { + let mut slashable_cache = self.observed_slashable.write(); + // Assumes all items in custody_columns are for the same block_root + if let Some(column) = custody_columns.first() { + let header = &column.signed_block_header; + if verify_header_signature::(self, header).is_ok() { + slashable_cache + .observe_slashable( + header.message.slot, + header.message.proposer_index, + block_root, + ) + .map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))?; + if let Some(slasher) = self.slasher.as_ref() { + slasher.accept_block_header(header.clone()); + } + } + } + Ok(()) + } + /// Imports a fully available block. Otherwise, returns `AvailabilityProcessingStatus::MissingComponents` /// /// An error is returned if the block was unable to be imported. It may be partially imported @@ -3927,7 +3888,6 @@ impl BeaconChain { state, parent_block, parent_eth1_finalization_data, - confirmed_state_roots, consensus_context, } = import_data; @@ -3951,7 +3911,6 @@ impl BeaconChain { block, block_root, state, - confirmed_state_roots, payload_verification_outcome.payload_verification_status, parent_block, parent_eth1_finalization_data, @@ -3989,7 +3948,6 @@ impl BeaconChain { signed_block: AvailableBlock, block_root: Hash256, mut state: BeaconState, - confirmed_state_roots: Vec, payload_verification_status: PayloadVerificationStatus, parent_block: SignedBlindedBeaconBlock, parent_eth1_finalization_data: Eth1FinalizationData, @@ -4067,7 +4025,7 @@ impl BeaconChain { payload_verification_status, &self.spec, ) - .map_err(|e| BlockError::BeaconChainError(e.into()))?; + .map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))?; } // If the block is recent enough and it was not optimistically imported, check to see if it @@ -4177,11 +4135,6 @@ impl BeaconChain { let block = signed_block.message(); let db_write_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_DB_WRITE); - ops.extend( - confirmed_state_roots - .into_iter() - .map(StoreOp::DeleteStateTemporaryFlag), - ); ops.push(StoreOp::PutBlock(block_root, signed_block.clone())); ops.push(StoreOp::PutState(block.state_root(), &state)); @@ -4208,9 +4161,6 @@ impl BeaconChain { // about it. let block_time_imported = timestamp_now(); - let parent_root = block.parent_root(); - let slot = block.slot(); - let current_eth1_finalization_data = Eth1FinalizationData { eth1_data: state.eth1_data().clone(), eth1_deposit_index: state.eth1_deposit_index(), @@ -4227,13 +4177,10 @@ impl BeaconChain { &mut state, ) .unwrap_or_else(|e| { - error!("error caching light_client data {:?}", e); + debug!("error caching light_client data {:?}", e); }); } - self.head_tracker - .register_block(block_root, parent_root, slot); - metrics::stop_timer(db_write_timer); metrics::inc_counter(&metrics::BLOCK_PROCESSING_SUCCESSES); @@ -4291,7 +4238,7 @@ impl BeaconChain { warning = "The database is likely corrupt now, consider --purge-db", "No stored fork choice found to restore from" ); - Err(BlockError::BeaconChainError(e)) + Err(BlockError::BeaconChainError(Box::new(e))) } else { Ok(()) } @@ -4346,9 +4293,9 @@ impl BeaconChain { Provided block root is not a checkpoint.", )) .map_err(|err| { - BlockError::BeaconChainError( + BlockError::BeaconChainError(Box::new( BeaconChainError::WeakSubjectivtyShutdownError(err), - ) + )) })?; return Err(BlockError::WeakSubjectivityConflict); } @@ -5122,7 +5069,7 @@ impl BeaconChain { canonical_forkchoice_params: ForkchoiceUpdateParameters, ) -> Result { self.overridden_forkchoice_update_params_or_failure_reason(&canonical_forkchoice_params) - .or_else(|e| match e { + .or_else(|e| match *e { ProposerHeadError::DoNotReOrg(reason) => { trace!( %reason, @@ -5137,19 +5084,19 @@ impl BeaconChain { pub fn overridden_forkchoice_update_params_or_failure_reason( &self, canonical_forkchoice_params: &ForkchoiceUpdateParameters, - ) -> Result> { + ) -> Result>> { let _timer = metrics::start_timer(&metrics::FORK_CHOICE_OVERRIDE_FCU_TIMES); // Never override if proposer re-orgs are disabled. let re_org_head_threshold = self .config .re_org_head_threshold - .ok_or(DoNotReOrg::ReOrgsDisabled)?; + .ok_or(Box::new(DoNotReOrg::ReOrgsDisabled.into()))?; let re_org_parent_threshold = self .config .re_org_parent_threshold - .ok_or(DoNotReOrg::ReOrgsDisabled)?; + .ok_or(Box::new(DoNotReOrg::ReOrgsDisabled.into()))?; let head_block_root = canonical_forkchoice_params.head_root; @@ -5190,7 +5137,7 @@ impl BeaconChain { false }; if !current_slot_ok { - return Err(DoNotReOrg::HeadDistance.into()); + return Err(Box::new(DoNotReOrg::HeadDistance.into())); } // Only attempt a re-org if we have a proposer registered for the re-org slot. @@ -5213,7 +5160,7 @@ impl BeaconChain { decision_root = ?shuffling_decision_root, "Fork choice override proposer shuffling miss" ); - DoNotReOrg::NotProposing + Box::new(DoNotReOrg::NotProposing.into()) })? .index as u64; @@ -5223,7 +5170,7 @@ impl BeaconChain { .has_proposer_preparation_data_blocking(proposer_index) }; if !proposing_at_re_org_slot { - return Err(DoNotReOrg::NotProposing.into()); + return Err(Box::new(DoNotReOrg::NotProposing.into())); } // If the current slot is already equal to the proposal slot (or we are in the tail end of @@ -5238,18 +5185,22 @@ impl BeaconChain { (true, true) }; if !head_weak { - return Err(DoNotReOrg::HeadNotWeak { - head_weight: info.head_node.weight, - re_org_head_weight_threshold: info.re_org_head_weight_threshold, - } - .into()); + return Err(Box::new( + DoNotReOrg::HeadNotWeak { + head_weight: info.head_node.weight, + 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()); + return Err(Box::new( + DoNotReOrg::ParentNotStrong { + parent_weight: info.parent_node.weight, + re_org_parent_weight_threshold: info.re_org_parent_weight_threshold, + } + .into(), + )); } // Check that the head block arrived late and is vulnerable to a re-org. This check is only @@ -5260,7 +5211,7 @@ impl BeaconChain { let head_block_late = self.block_observed_after_attestation_deadline(head_block_root, head_slot); if !head_block_late { - return Err(DoNotReOrg::HeadNotLate.into()); + return Err(Box::new(DoNotReOrg::HeadNotLate.into())); } let parent_head_hash = info.parent_node.execution_status.block_hash(); @@ -5474,16 +5425,16 @@ impl BeaconChain { .validators() .get(proposer_index as usize) .map(|v| v.pubkey) - .ok_or(BlockProductionError::BeaconChain( + .ok_or(BlockProductionError::BeaconChain(Box::new( BeaconChainError::ValidatorIndexUnknown(proposer_index as usize), - ))?; + )))?; let builder_params = BuilderParams { pubkey, slot: state.slot(), chain_health: self .is_healthy(&parent_root) - .map_err(BlockProductionError::BeaconChain)?, + .map_err(|e| BlockProductionError::BeaconChain(Box::new(e)))?, }; // If required, start the process of loading an execution payload from the EL early. This @@ -6067,15 +6018,26 @@ impl BeaconChain { let kzg_proofs = Vec::from(proofs); let kzg = self.kzg.as_ref(); - - // TODO(fulu): we no longer need blob proofs from PeerDAS and could avoid computing. - kzg_utils::validate_blobs::( - kzg, - expected_kzg_commitments, - blobs.iter().collect(), - &kzg_proofs, - ) - .map_err(BlockProductionError::KzgError)?; + if self + .spec + .is_peer_das_enabled_for_epoch(slot.epoch(T::EthSpec::slots_per_epoch())) + { + kzg_utils::validate_blobs_and_cell_proofs::( + kzg, + blobs.iter().collect(), + &kzg_proofs, + expected_kzg_commitments, + ) + .map_err(BlockProductionError::KzgError)?; + } else { + kzg_utils::validate_blobs::( + kzg, + expected_kzg_commitments, + blobs.iter().collect(), + &kzg_proofs, + ) + .map_err(BlockProductionError::KzgError)?; + } Some((kzg_proofs.into(), blobs)) } @@ -6360,7 +6322,7 @@ impl BeaconChain { payload_attributes: payload_attributes.into(), }, metadata: Default::default(), - version: Some(self.spec.fork_name_at_slot::(prepare_slot)), + version: self.spec.fork_name_at_slot::(prepare_slot), })); } } @@ -7423,35 +7385,39 @@ impl BeaconChain { ); Ok(Some(StoreOp::PutDataColumns(block_root, data_columns))) } - AvailableBlockData::DataColumnsRecv(data_column_recv) => { - // Blobs were available from the EL, in this case we wait for the data columns to be computed (blocking). - let _column_recv_timer = - metrics::start_timer(&metrics::BLOCK_PROCESSING_DATA_COLUMNS_WAIT); - // Unable to receive data columns from sender, sender is either dropped or - // failed to compute data columns from blobs. We restore fork choice here and - // return to avoid inconsistency in database. - let computed_data_columns = data_column_recv - .blocking_recv() - .map_err(|e| format!("Did not receive data columns from sender: {e:?}"))?; - debug!( - %block_root, - count = computed_data_columns.len(), - "Writing data columns to store" - ); - // TODO(das): Store only this node's custody columns - Ok(Some(StoreOp::PutDataColumns( - block_root, - computed_data_columns, - ))) + } + } + + /// Retrieves block roots (in ascending slot order) within some slot range from fork choice. + pub fn block_roots_from_fork_choice(&self, start_slot: u64, count: u64) -> Vec { + let head_block_root = self.canonical_head.cached_head().head_block_root(); + let fork_choice_read_lock = self.canonical_head.fork_choice_read_lock(); + let block_roots_iter = fork_choice_read_lock + .proto_array() + .iter_block_roots(&head_block_root); + let end_slot = start_slot.saturating_add(count); + let mut roots = vec![]; + + for (root, slot) in block_roots_iter { + if slot < end_slot && slot >= start_slot { + roots.push(root); + } + if slot < start_slot { + break; } } + + drop(fork_choice_read_lock); + // return in ascending slot order + roots.reverse(); + roots } } impl Drop for BeaconChain { fn drop(&mut self) { let drop = || -> Result<(), Error> { - self.persist_head_and_fork_choice()?; + self.persist_fork_choice()?; self.persist_op_pool()?; self.persist_eth1_cache() }; diff --git a/beacon_node/beacon_chain/src/beacon_proposer_cache.rs b/beacon_node/beacon_chain/src/beacon_proposer_cache.rs index 567433caee..56b13b0b77 100644 --- a/beacon_node/beacon_chain/src/beacon_proposer_cache.rs +++ b/beacon_node/beacon_chain/src/beacon_proposer_cache.rs @@ -11,10 +11,12 @@ use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; use fork_choice::ExecutionStatus; use lru::LruCache; +use once_cell::sync::OnceCell; use smallvec::SmallVec; use state_processing::state_advance::partial_state_advance; use std::cmp::Ordering; use std::num::NonZeroUsize; +use std::sync::Arc; use types::non_zero_usize::new_non_zero_usize; use types::{ BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, Fork, Hash256, Slot, Unsigned, @@ -39,21 +41,21 @@ pub struct Proposer { /// their signatures. pub struct EpochBlockProposers { /// The epoch to which the proposers pertain. - epoch: Epoch, + pub(crate) epoch: Epoch, /// The fork that should be used to verify proposer signatures. - fork: Fork, + pub(crate) fork: Fork, /// A list of length `T::EthSpec::slots_per_epoch()`, representing the proposers for each slot /// in that epoch. /// /// E.g., if `self.epoch == 1`, then `self.proposers[0]` contains the proposer for slot `32`. - proposers: SmallVec<[usize; TYPICAL_SLOTS_PER_EPOCH]>, + pub(crate) proposers: SmallVec<[usize; TYPICAL_SLOTS_PER_EPOCH]>, } /// A cache to store the proposers for some epoch. /// /// See the module-level documentation for more information. pub struct BeaconProposerCache { - cache: LruCache<(Epoch, Hash256), EpochBlockProposers>, + cache: LruCache<(Epoch, Hash256), Arc>>, } impl Default for BeaconProposerCache { @@ -74,7 +76,8 @@ impl BeaconProposerCache { ) -> Option { let epoch = slot.epoch(E::slots_per_epoch()); let key = (epoch, shuffling_decision_block); - if let Some(cache) = self.cache.get(&key) { + let cache_opt = self.cache.get(&key).and_then(|cell| cell.get()); + if let Some(cache) = cache_opt { // This `if` statement is likely unnecessary, but it feels like good practice. if epoch == cache.epoch { cache @@ -103,7 +106,26 @@ impl BeaconProposerCache { epoch: Epoch, ) -> Option<&SmallVec<[usize; TYPICAL_SLOTS_PER_EPOCH]>> { let key = (epoch, shuffling_decision_block); - self.cache.get(&key).map(|cache| &cache.proposers) + self.cache + .get(&key) + .and_then(|cache_once_cell| cache_once_cell.get().map(|proposers| &proposers.proposers)) + } + + /// Returns the `OnceCell` for the given `(epoch, shuffling_decision_block)` key, + /// inserting an empty one if it doesn't exist. + /// + /// The returned `OnceCell` allows the caller to initialise the value externally + /// using `get_or_try_init`, enabling deferred computation without holding a mutable + /// reference to the cache. + pub fn get_or_insert_key( + &mut self, + epoch: Epoch, + shuffling_decision_block: Hash256, + ) -> Arc> { + let key = (epoch, shuffling_decision_block); + self.cache + .get_or_insert(key, || Arc::new(OnceCell::new())) + .clone() } /// Insert the proposers into the cache. @@ -120,14 +142,13 @@ impl BeaconProposerCache { ) -> Result<(), BeaconStateError> { let key = (epoch, shuffling_decision_block); if !self.cache.contains(&key) { - self.cache.put( - key, - EpochBlockProposers { - epoch, - fork, - proposers: proposers.into(), - }, - ); + let epoch_proposers = EpochBlockProposers { + epoch, + fork, + proposers: proposers.into(), + }; + self.cache + .put(key, Arc::new(OnceCell::with_value(epoch_proposers))); } Ok(()) diff --git a/beacon_node/beacon_chain/src/blob_verification.rs b/beacon_node/beacon_chain/src/blob_verification.rs index fe9d8c6bfc..6fe710f41a 100644 --- a/beacon_node/beacon_chain/src/blob_verification.rs +++ b/beacon_node/beacon_chain/src/blob_verification.rs @@ -42,7 +42,7 @@ pub enum GossipBlobError { /// /// We were unable to process this blob due to an internal error. It's /// unclear if the blob is valid. - BeaconChainError(BeaconChainError), + BeaconChainError(Box), /// The `BlobSidecar` was gossiped over an incorrect subnet. /// @@ -147,13 +147,13 @@ impl std::fmt::Display for GossipBlobError { impl From for GossipBlobError { fn from(e: BeaconChainError) -> Self { - GossipBlobError::BeaconChainError(e) + GossipBlobError::BeaconChainError(e.into()) } } impl From for GossipBlobError { fn from(e: BeaconStateError) -> Self { - GossipBlobError::BeaconChainError(BeaconChainError::BeaconStateError(e)) + GossipBlobError::BeaconChainError(BeaconChainError::BeaconStateError(e).into()) } } @@ -446,7 +446,7 @@ pub fn validate_blob_sidecar_for_gossip( .observed_blob_sidecars .write() .observe_sidecar(blob_sidecar) - .map_err(|e| GossipBlobError::BeaconChainError(e.into()))? + .map_err(|e| GossipBlobError::BeaconChainError(Box::new(e.into())))? { return Err(GossipBlobError::RepeatBlob { proposer: blob_sidecar.block_proposer_index(), diff --git a/beacon_node/beacon_chain/src/block_reward.rs b/beacon_node/beacon_chain/src/block_reward.rs index 69eecc89b8..0809ce34ef 100644 --- a/beacon_node/beacon_chain/src/block_reward.rs +++ b/beacon_node/beacon_chain/src/block_reward.rs @@ -1,6 +1,8 @@ use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; use eth2::lighthouse::{AttestationRewards, BlockReward, BlockRewardMeta}; -use operation_pool::{AttMaxCover, MaxCover, RewardCache, SplitAttestation}; +use operation_pool::{ + AttMaxCover, MaxCover, RewardCache, SplitAttestation, PROPOSER_REWARD_DENOMINATOR, +}; use state_processing::{ common::get_attesting_indices_from_state, per_block_processing::altair::sync_committee::compute_sync_aggregate_rewards, @@ -65,13 +67,10 @@ impl BeaconChain { let mut curr_epoch_total = 0; for cover in &per_attestation_rewards { - for &reward in cover.fresh_validators_rewards.values() { - if cover.att.data.slot.epoch(T::EthSpec::slots_per_epoch()) == state.current_epoch() - { - curr_epoch_total += reward; - } else { - prev_epoch_total += reward; - } + if cover.att.data.slot.epoch(T::EthSpec::slots_per_epoch()) == state.current_epoch() { + curr_epoch_total += cover.score() as u64; + } else { + prev_epoch_total += cover.score() as u64; } } @@ -80,7 +79,16 @@ impl BeaconChain { // Drop the covers. let per_attestation_rewards = per_attestation_rewards .into_iter() - .map(|cover| cover.fresh_validators_rewards) + .map(|cover| { + // Divide each reward numerator by the denominator. This can lead to the total being + // less than the sum of the individual rewards due to the fact that integer division + // does not distribute over addition. + let mut rewards = cover.fresh_validators_rewards; + rewards + .values_mut() + .for_each(|reward| *reward /= PROPOSER_REWARD_DENOMINATOR); + rewards + }) .collect(); // Add the attestation data if desired. diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 0a0ffab7fa..26bf872392 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -97,8 +97,8 @@ use tracing::{debug, error}; use types::{ data_column_sidecar::DataColumnSidecarError, BeaconBlockRef, BeaconState, BeaconStateError, BlobsList, ChainSpec, DataColumnSidecarList, Epoch, EthSpec, ExecutionBlockHash, FullPayload, - Hash256, InconsistentFork, PublicKey, PublicKeyBytes, RelativeEpoch, SignedBeaconBlock, - SignedBeaconBlockHeader, Slot, + Hash256, InconsistentFork, KzgProofs, PublicKey, PublicKeyBytes, RelativeEpoch, + SignedBeaconBlock, SignedBeaconBlockHeader, Slot, }; pub const POS_PANDA_BANNER: &str = r#" @@ -252,7 +252,7 @@ pub enum BlockError { /// /// We were unable to process this block due to an internal error. It's unclear if the block is /// valid. - BeaconChainError(BeaconChainError), + BeaconChainError(Box), /// There was an error whilst verifying weak subjectivity. This block conflicts with the /// configured weak subjectivity checkpoint and was not imported. /// @@ -475,38 +475,40 @@ impl From for BlockError { block, local_shuffling, }, - e => BlockError::BeaconChainError(BeaconChainError::BlockSignatureVerifierError(e)), + e => BlockError::BeaconChainError( + BeaconChainError::BlockSignatureVerifierError(e).into(), + ), } } } impl From for BlockError { fn from(e: BeaconChainError) -> Self { - BlockError::BeaconChainError(e) + BlockError::BeaconChainError(e.into()) } } impl From for BlockError { fn from(e: BeaconStateError) -> Self { - BlockError::BeaconChainError(BeaconChainError::BeaconStateError(e)) + BlockError::BeaconChainError(BeaconChainError::BeaconStateError(e).into()) } } impl From for BlockError { fn from(e: SlotProcessingError) -> Self { - BlockError::BeaconChainError(BeaconChainError::SlotProcessingError(e)) + BlockError::BeaconChainError(BeaconChainError::SlotProcessingError(e).into()) } } impl From for BlockError { fn from(e: DBError) -> Self { - BlockError::BeaconChainError(BeaconChainError::DBError(e)) + BlockError::BeaconChainError(BeaconChainError::DBError(e).into()) } } impl From for BlockError { fn from(e: ArithError) -> Self { - BlockError::BeaconChainError(BeaconChainError::ArithError(e)) + BlockError::BeaconChainError(BeaconChainError::ArithError(e).into()) } } @@ -755,6 +757,7 @@ pub fn build_blob_data_column_sidecars( chain: &BeaconChain, block: &SignedBeaconBlock>, blobs: BlobsList, + kzg_cell_proofs: KzgProofs, ) -> Result, DataColumnSidecarError> { // Only attempt to build data columns if blobs is non empty to avoid skewing the metrics. if blobs.is_empty() { @@ -766,8 +769,14 @@ pub fn build_blob_data_column_sidecars( &[&blobs.len().to_string()], ); let blob_refs = blobs.iter().collect::>(); - let sidecars = blobs_to_data_column_sidecars(&blob_refs, block, &chain.kzg, &chain.spec) - .discard_timer_on_break(&mut timer)?; + let sidecars = blobs_to_data_column_sidecars( + &blob_refs, + kzg_cell_proofs.to_vec(), + block, + &chain.kzg, + &chain.spec, + ) + .discard_timer_on_break(&mut timer)?; drop(timer); Ok(sidecars) } @@ -993,7 +1002,7 @@ impl GossipVerifiedBlock { .observed_slashable .write() .observe_slashable(block.slot(), block.message().proposer_index(), block_root) - .map_err(|e| BlockError::BeaconChainError(e.into()))?; + .map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))?; // Now the signature is valid, store the proposal so we don't accept another from this // validator and slot. // @@ -1003,7 +1012,7 @@ impl GossipVerifiedBlock { .observed_block_producers .write() .observe_proposal(block_root, block.message()) - .map_err(|e| BlockError::BeaconChainError(e.into()))? + .map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))? { SeenBlock::Slashable => { return Err(BlockError::Slashable); @@ -1260,40 +1269,6 @@ impl IntoExecutionPendingBlock for SignatureVerifiedBloc } } -impl IntoExecutionPendingBlock for Arc> { - /// Verifies the `SignedBeaconBlock` by first transforming it into a `SignatureVerifiedBlock` - /// and then using that implementation of `IntoExecutionPendingBlock` to complete verification. - fn into_execution_pending_block_slashable( - self, - block_root: Hash256, - chain: &Arc>, - notify_execution_layer: NotifyExecutionLayer, - ) -> Result, BlockSlashInfo> { - // Perform an early check to prevent wasting time on irrelevant blocks. - let block_root = check_block_relevancy(&self, block_root, chain) - .map_err(|e| BlockSlashInfo::SignatureNotChecked(self.signed_block_header(), e))?; - let maybe_available = chain - .data_availability_checker - .verify_kzg_for_rpc_block(RpcBlock::new_without_blobs(Some(block_root), self.clone())) - .map_err(|e| { - BlockSlashInfo::SignatureNotChecked( - self.signed_block_header(), - BlockError::AvailabilityCheck(e), - ) - })?; - SignatureVerifiedBlock::check_slashable(maybe_available, block_root, chain)? - .into_execution_pending_block_slashable(block_root, chain, notify_execution_layer) - } - - fn block(&self) -> &SignedBeaconBlock { - self - } - - fn block_cloned(&self) -> Arc> { - self.clone() - } -} - impl IntoExecutionPendingBlock for RpcBlock { /// Verifies the `SignedBeaconBlock` by first transforming it into a `SignatureVerifiedBlock` /// and then using that implementation of `IntoExecutionPendingBlock` to complete verification. @@ -1348,13 +1323,13 @@ impl ExecutionPendingBlock { .observed_slashable .write() .observe_slashable(block.slot(), block.message().proposer_index(), block_root) - .map_err(|e| BlockError::BeaconChainError(e.into()))?; + .map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))?; chain .observed_block_producers .write() .observe_proposal(block_root, block.message()) - .map_err(|e| BlockError::BeaconChainError(e.into()))?; + .map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))?; if let Some(parent) = chain .canonical_head @@ -1453,22 +1428,8 @@ impl ExecutionPendingBlock { let catchup_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_CATCHUP_STATE); - // Stage a batch of operations to be completed atomically if this block is imported - // successfully. If there is a skipped slot, we include the state root of the pre-state, - // which may be an advanced state that was stored in the DB with a `temporary` flag. let mut state = parent.pre_state; - let mut confirmed_state_roots = - if block.slot() > state.slot() && state.slot() > parent.beacon_block.slot() { - // Advanced pre-state. Delete its temporary flag. - let pre_state_root = state.update_tree_hash_cache()?; - vec![pre_state_root] - } else { - // Pre state is either unadvanced, or should not be stored long-term because there - // is no skipped slot between `parent` and `block`. - vec![] - }; - // The block must have a higher slot than its parent. if block.slot() <= parent.beacon_block.slot() { return Err(BlockError::BlockIsNotLaterThanParent { @@ -1515,38 +1476,29 @@ impl ExecutionPendingBlock { // processing, but we get early access to it. let state_root = state.update_tree_hash_cache()?; - // Store the state immediately, marking it as temporary, and staging the deletion - // of its temporary status as part of the larger atomic operation. + // Store the state immediately. let txn_lock = chain.store.hot_db.begin_rw_transaction(); let state_already_exists = chain.store.load_hot_state_summary(&state_root)?.is_some(); let state_batch = if state_already_exists { - // If the state exists, it could be temporary or permanent, but in neither case - // should we rewrite it or store a new temporary flag for it. We *will* stage - // the temporary flag for deletion because it's OK to double-delete the flag, - // and we don't mind if another thread gets there first. + // If the state exists, we do not need to re-write it. vec![] } else { - vec![ - if state.slot() % T::EthSpec::slots_per_epoch() == 0 { - StoreOp::PutState(state_root, &state) - } else { - StoreOp::PutStateSummary( - state_root, - HotStateSummary::new(&state_root, &state)?, - ) - }, - StoreOp::PutStateTemporaryFlag(state_root), - ] + vec![if state.slot() % T::EthSpec::slots_per_epoch() == 0 { + StoreOp::PutState(state_root, &state) + } else { + StoreOp::PutStateSummary( + state_root, + HotStateSummary::new(&state_root, &state)?, + ) + }] }; chain .store .do_atomically_with_block_and_blobs_cache(state_batch)?; drop(txn_lock); - confirmed_state_roots.push(state_root); - state_root }; @@ -1701,7 +1653,7 @@ impl ExecutionPendingBlock { // Ignore invalid attestations whilst importing attestations from a block. The // block might be very old and therefore the attestations useless to fork choice. Err(ForkChoiceError::InvalidAttestation(_)) => Ok(()), - Err(e) => Err(BlockError::BeaconChainError(e.into())), + Err(e) => Err(BlockError::BeaconChainError(Box::new(e.into()))), }?; } drop(fork_choice); @@ -1713,7 +1665,6 @@ impl ExecutionPendingBlock { state, parent_block: parent.beacon_block, parent_eth1_finalization_data, - confirmed_state_roots, consensus_context, }, payload_verification_handle, @@ -1794,7 +1745,7 @@ pub fn check_block_is_finalized_checkpoint_or_descendant< if chain .store .block_exists(&block.parent_root()) - .map_err(|e| BlockError::BeaconChainError(e.into()))? + .map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))? { Err(BlockError::NotFinalizedDescendant { block_parent_root: block.parent_root(), @@ -1939,7 +1890,7 @@ fn load_parent>( let root = block.parent_root(); let parent_block = chain .get_blinded_block(&block.parent_root()) - .map_err(BlockError::BeaconChainError)? + .map_err(|e| BlockError::BeaconChainError(Box::new(e)))? .ok_or_else(|| { // Return a `MissingBeaconBlock` error instead of a `ParentUnknown` error since // we've already checked fork choice for this block. diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index d3a6e93862..dab54dc823 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -103,14 +103,14 @@ impl RpcBlock { pub fn new_without_blobs( block_root: Option, block: Arc>, + custody_columns_count: usize, ) -> Self { let block_root = block_root.unwrap_or_else(|| get_block_root(&block)); Self { block_root, block: RpcBlockInner::Block(block), - // Block has zero columns - custody_columns_count: 0, + custody_columns_count, } } @@ -358,7 +358,6 @@ pub struct BlockImportData { pub state: BeaconState, pub parent_block: SignedBeaconBlock>, pub parent_eth1_finalization_data: Eth1FinalizationData, - pub confirmed_state_roots: Vec, pub consensus_context: ConsensusContext, } @@ -376,7 +375,6 @@ impl BlockImportData { eth1_data: <_>::default(), eth1_deposit_index: 0, }, - confirmed_state_roots: vec![], consensus_context: ConsensusContext::new(Slot::new(0)), } } diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 65f40aea3e..6f232400ed 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -8,8 +8,7 @@ use crate::eth1_finalization_cache::Eth1FinalizationCache; use crate::fork_choice_signal::ForkChoiceSignalTx; use crate::fork_revert::{reset_fork_choice_to_finalization, revert_to_fork_boundary}; use crate::graffiti_calculator::{GraffitiCalculator, GraffitiOrigin}; -use crate::head_tracker::HeadTracker; -use crate::kzg_utils::blobs_to_data_column_sidecars; +use crate::kzg_utils::build_data_column_sidecars; use crate::light_client_server_cache::LightClientServerCache; use crate::migrate::{BackgroundMigrator, MigratorConfig}; use crate::observed_data_sidecars::ObservedDataSidecars; @@ -31,6 +30,8 @@ use logging::crit; use operation_pool::{OperationPool, PersistedOperationPool}; use parking_lot::{Mutex, RwLock}; use proto_array::{DisallowedReOrgOffsets, ReOrgThreshold}; +use rand::RngCore; +use rayon::prelude::*; use slasher::Slasher; use slot_clock::{SlotClock, TestingSlotClock}; use state_processing::{per_slot_processing, AllCaches}; @@ -41,8 +42,8 @@ use store::{Error as StoreError, HotColdDB, ItemStore, KeyValueStoreOp}; use task_executor::{ShutdownReason, TaskExecutor}; use tracing::{debug, error, info}; use types::{ - BeaconBlock, BeaconState, BlobSidecarList, ChainSpec, Checkpoint, Epoch, EthSpec, - FixedBytesExtended, Hash256, Signature, SignedBeaconBlock, Slot, + BeaconBlock, BeaconState, BlobSidecarList, ChainSpec, Checkpoint, DataColumnSidecarList, Epoch, + EthSpec, FixedBytesExtended, Hash256, Signature, SignedBeaconBlock, Slot, }; /// An empty struct used to "witness" all the `BeaconChainTypes` traits. It has no user-facing @@ -93,7 +94,6 @@ pub struct BeaconChainBuilder { slot_clock: Option, shutdown_sender: Option>, light_client_server_tx: Option>>, - head_tracker: Option, validator_pubkey_cache: Option>, spec: Arc, chain_config: ChainConfig, @@ -106,6 +106,7 @@ pub struct BeaconChainBuilder { task_executor: Option, validator_monitor_config: Option, import_all_data_columns: bool, + rng: Option>, } impl @@ -136,7 +137,6 @@ where slot_clock: None, shutdown_sender: None, light_client_server_tx: None, - head_tracker: None, validator_pubkey_cache: None, spec: Arc::new(E::default_spec()), chain_config: ChainConfig::default(), @@ -147,6 +147,7 @@ where task_executor: None, validator_monitor_config: None, import_all_data_columns: false, + rng: None, } } @@ -314,10 +315,6 @@ where self.genesis_block_root = Some(chain.genesis_block_root); self.genesis_state_root = Some(genesis_block.state_root()); - self.head_tracker = Some( - HeadTracker::from_ssz_container(&chain.ssz_head_tracker) - .map_err(|e| format!("Failed to decode head tracker for database: {:?}", e))?, - ); self.validator_pubkey_cache = Some(pubkey_cache); self.fork_choice = Some(fork_choice); @@ -553,15 +550,8 @@ where { // After PeerDAS recompute columns from blobs to not force the checkpointz server // into exposing another route. - let blobs = blobs - .iter() - .map(|blob_sidecar| &blob_sidecar.blob) - .collect::>(); let data_columns = - blobs_to_data_column_sidecars(&blobs, &weak_subj_block, &self.kzg, &self.spec) - .map_err(|e| { - format!("Failed to compute weak subjectivity data_columns: {e:?}") - })?; + build_data_columns_from_blobs(&weak_subj_block, &blobs, &self.kzg, &self.spec)?; // TODO(das): only persist the columns under custody store .put_data_columns(&weak_subj_block_root, data_columns) @@ -704,6 +694,14 @@ where self } + /// Sets the `rng` field. + /// + /// Currently used for shuffling column sidecars in block publishing. + pub fn rng(mut self, rng: Box) -> Self { + self.rng = Some(rng); + self + } + /// Consumes `self`, returning a `BeaconChain` if all required parameters have been supplied. /// /// An error will be returned at runtime if all required parameters have not been configured. @@ -729,7 +727,7 @@ where .genesis_state_root .ok_or("Cannot build without a genesis state root")?; let validator_monitor_config = self.validator_monitor_config.unwrap_or_default(); - let head_tracker = Arc::new(self.head_tracker.unwrap_or_default()); + let rng = self.rng.ok_or("Cannot build without an RNG")?; let beacon_proposer_cache: Arc> = <_>::default(); let mut validator_monitor = @@ -769,8 +767,6 @@ where &self.spec, )?; - // Update head tracker. - head_tracker.register_block(block_root, block.parent_root(), block.slot()); (block_root, block, true) } Err(e) => return Err(descriptive_db_error("head block", &e)), @@ -846,8 +842,7 @@ where })?; let migrator_config = self.store_migrator_config.unwrap_or_default(); - let store_migrator = - BackgroundMigrator::new(store.clone(), migrator_config, genesis_block_root); + let store_migrator = BackgroundMigrator::new(store.clone(), migrator_config); if let Some(slot) = slot_clock.now() { validator_monitor.process_valid_state( @@ -872,11 +867,10 @@ where // // This *must* be stored before constructing the `BeaconChain`, so that its `Drop` instance // doesn't write a `PersistedBeaconChain` without the rest of the batch. - let head_tracker_reader = head_tracker.0.read(); self.pending_io_batch.push(BeaconChain::< Witness, >::persist_head_in_batch_standalone( - genesis_block_root, &head_tracker_reader + genesis_block_root )); self.pending_io_batch.push(BeaconChain::< Witness, @@ -887,7 +881,6 @@ where .hot_db .do_atomically(self.pending_io_batch) .map_err(|e| format!("Error writing chain & metadata to disk: {:?}", e))?; - drop(head_tracker_reader); let genesis_validators_root = head_snapshot.beacon_state.genesis_validators_root(); let genesis_time = head_snapshot.beacon_state.genesis_time(); @@ -968,7 +961,6 @@ where fork_choice_signal_tx, fork_choice_signal_rx, event_handler: self.event_handler, - head_tracker, shuffling_cache: RwLock::new(ShufflingCache::new( shuffling_cache_size, head_shuffling_ids, @@ -1000,6 +992,7 @@ where .map_err(|e| format!("Error initializing DataAvailabilityChecker: {:?}", e))?, ), kzg: self.kzg.clone(), + rng: Arc::new(Mutex::new(rng)), }; let head = beacon_chain.head_snapshot(); @@ -1153,6 +1146,49 @@ fn descriptive_db_error(item: &str, error: &StoreError) -> String { ) } +/// Build data columns and proofs from blobs. +fn build_data_columns_from_blobs( + block: &SignedBeaconBlock, + blobs: &BlobSidecarList, + kzg: &Kzg, + spec: &ChainSpec, +) -> Result, String> { + let blob_cells_and_proofs_vec = blobs + .into_par_iter() + .map(|blob_sidecar| { + let kzg_blob_ref = blob_sidecar + .blob + .as_ref() + .try_into() + .map_err(|e| format!("Failed to convert blob to kzg blob: {e:?}"))?; + let cells_and_proofs = kzg + .compute_cells_and_proofs(kzg_blob_ref) + .map_err(|e| format!("Failed to compute cell kzg proofs: {e:?}"))?; + Ok(cells_and_proofs) + }) + .collect::, String>>()?; + + let data_columns = { + let beacon_block_body = block.message().body(); + let kzg_commitments = beacon_block_body + .blob_kzg_commitments() + .cloned() + .map_err(|e| format!("Unexpected pre Deneb block: {e:?}"))?; + let kzg_commitments_inclusion_proof = beacon_block_body + .kzg_commitments_merkle_proof() + .map_err(|e| format!("Failed to compute kzg commitments merkle proof: {e:?}"))?; + build_data_column_sidecars( + kzg_commitments, + kzg_commitments_inclusion_proof, + block.signed_block_header(), + blob_cells_and_proofs_vec, + spec, + ) + .map_err(|e| format!("Failed to compute weak subjectivity data_columns: {e:?}"))? + }; + Ok(data_columns) +} + #[cfg(not(debug_assertions))] #[cfg(test)] mod test { @@ -1162,6 +1198,8 @@ mod test { use genesis::{ generate_deterministic_keypairs, interop_genesis_state, DEFAULT_ETH1_BLOCK_HASH, }; + use rand::rngs::StdRng; + use rand::SeedableRng; use ssz::Encode; use std::time::Duration; use store::config::StoreConfig; @@ -1208,6 +1246,7 @@ mod test { .testing_slot_clock(Duration::from_secs(1)) .expect("should configure testing slot clock") .shutdown_sender(shutdown_tx) + .rng(Box::new(StdRng::seed_from_u64(42))) .build() .expect("should build"); diff --git a/beacon_node/beacon_chain/src/canonical_head.rs b/beacon_node/beacon_chain/src/canonical_head.rs index a7efc8b70d..7c3d5b57d0 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -53,7 +53,7 @@ use slot_clock::SlotClock; use state_processing::AllCaches; use std::sync::Arc; use std::time::Duration; -use store::{iter::StateRootsIterator, KeyValueStoreOp, StoreItem}; +use store::{iter::StateRootsIterator, KeyValueStore, KeyValueStoreOp, StoreItem}; use task_executor::{JoinHandle, ShutdownReason}; use tracing::{debug, error, info, warn}; use types::*; @@ -845,7 +845,7 @@ impl BeaconChain { ); if is_epoch_transition || reorg_distance.is_some() { - self.persist_head_and_fork_choice()?; + self.persist_fork_choice()?; self.op_pool.prune_attestations(self.epoch()?); } @@ -988,7 +988,6 @@ impl BeaconChain { self.store_migrator.process_finalization( new_finalized_state_root.into(), new_view.finalized_checkpoint, - self.head_tracker.clone(), )?; // Prune blobs in the background. @@ -1003,6 +1002,14 @@ impl BeaconChain { Ok(()) } + /// Persist fork choice to disk, writing immediately. + pub fn persist_fork_choice(&self) -> Result<(), Error> { + let _fork_choice_timer = metrics::start_timer(&metrics::PERSIST_FORK_CHOICE); + let batch = vec![self.persist_fork_choice_in_batch()]; + self.store.hot_db.do_atomically(batch)?; + Ok(()) + } + /// Return a database operation for writing fork choice to disk. pub fn persist_fork_choice_in_batch(&self) -> KeyValueStoreOp { Self::persist_fork_choice_in_batch_standalone(&self.canonical_head.fork_choice_read_lock()) diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 2b7ae9e4d1..6f292f3551 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -14,12 +14,11 @@ use std::num::NonZeroUsize; use std::sync::Arc; use std::time::Duration; use task_executor::TaskExecutor; -use tokio::sync::oneshot; use tracing::{debug, error, info_span, Instrument}; use types::blob_sidecar::{BlobIdentifier, BlobSidecar, FixedBlobSidecarList}; use types::{ - BlobSidecarList, ChainSpec, DataColumnIdentifier, DataColumnSidecar, DataColumnSidecarList, - Epoch, EthSpec, Hash256, RuntimeVariableList, SignedBeaconBlock, + BlobSidecarList, ChainSpec, DataColumnSidecarList, Epoch, EthSpec, Hash256, + RuntimeVariableList, SignedBeaconBlock, }; mod error; @@ -164,12 +163,12 @@ impl DataAvailabilityChecker { self.availability_cache.peek_blob(blob_id) } - /// Get a data column from the availability cache. - pub fn get_data_column( + /// Get data columns for a block from the availability cache. + pub fn get_data_columns( &self, - data_column_id: &DataColumnIdentifier, - ) -> Result>>, AvailabilityCheckError> { - self.availability_cache.peek_data_column(data_column_id) + block_root: Hash256, + ) -> Option> { + self.availability_cache.peek_data_columns(block_root) } /// Put a list of blobs received via RPC into the availability cache. This performs KZG @@ -226,27 +225,45 @@ impl DataAvailabilityChecker { pub fn put_engine_blobs( &self, block_root: Hash256, - block_epoch: Epoch, blobs: FixedBlobSidecarList, - data_columns_recv: Option>>, ) -> Result, AvailabilityCheckError> { - // `data_columns_recv` is always Some if block_root is post-PeerDAS - if let Some(data_columns_recv) = data_columns_recv { - self.availability_cache.put_computed_data_columns_recv( - block_root, - block_epoch, - data_columns_recv, - ) - } else { - let seen_timestamp = self - .slot_clock - .now_duration() - .ok_or(AvailabilityCheckError::SlotClockError)?; - self.availability_cache.put_kzg_verified_blobs( - block_root, - KzgVerifiedBlobList::from_verified(blobs.iter().flatten().cloned(), seen_timestamp), - ) - } + let seen_timestamp = self + .slot_clock + .now_duration() + .ok_or(AvailabilityCheckError::SlotClockError)?; + self.availability_cache.put_kzg_verified_blobs( + block_root, + KzgVerifiedBlobList::from_verified(blobs.iter().flatten().cloned(), seen_timestamp), + ) + } + + /// Put a list of data columns computed from blobs received from the EL pool into the + /// availability cache. + /// + /// This DOES NOT perform KZG proof and inclusion proof verification because + /// - The KZG proofs should have been verified by the trusted EL. + /// - The KZG commitments inclusion proof should have been constructed immediately prior to + /// calling this function so they are assumed to be valid. + /// + /// This method is used if the EL already has the blobs and returns them via the `getBlobsV2` + /// engine method. + /// More details in [fetch_blobs.rs](https://github.com/sigp/lighthouse/blob/44f8add41ea2252769bb967864af95b3c13af8ca/beacon_node/beacon_chain/src/fetch_blobs.rs). + pub fn put_engine_data_columns( + &self, + block_root: Hash256, + data_columns: DataColumnSidecarList, + ) -> Result, AvailabilityCheckError> { + let kzg_verified_custody_columns = data_columns + .into_iter() + .map(|d| { + KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::from_verified(d), + ) + }) + .collect::>(); + + self.availability_cache + .put_kzg_verified_data_columns(block_root, kzg_verified_custody_columns) } /// Check if we've cached other blobs for this block. If it completes a set and we also @@ -704,9 +721,6 @@ pub enum AvailableBlockData { Blobs(BlobSidecarList), /// Block is post-PeerDAS and has more than zero blobs DataColumns(DataColumnSidecarList), - /// Block is post-PeerDAS, has more than zero blobs and we recomputed the columns from the EL's - /// mempool blobs - DataColumnsRecv(oneshot::Receiver>), } /// A fully available block that is ready to be imported into fork choice. @@ -756,7 +770,6 @@ impl AvailableBlock { AvailableBlockData::NoData => false, AvailableBlockData::Blobs(..) => true, AvailableBlockData::DataColumns(_) => false, - AvailableBlockData::DataColumnsRecv(_) => false, } } @@ -782,9 +795,6 @@ impl AvailableBlock { AvailableBlockData::DataColumns(data_columns) => { AvailableBlockData::DataColumns(data_columns.clone()) } - AvailableBlockData::DataColumnsRecv(_) => { - return Err("Can't clone DataColumnsRecv".to_owned()) - } }, blobs_available_timestamp: self.blobs_available_timestamp, spec: self.spec.clone(), 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 f38a3b8b9c..3478c183f3 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 @@ -13,13 +13,11 @@ use parking_lot::RwLock; use std::cmp::Ordering; use std::num::NonZeroUsize; use std::sync::Arc; -use tokio::sync::oneshot; use tracing::debug; use types::blob_sidecar::BlobIdentifier; use types::{ - BlobSidecar, ChainSpec, ColumnIndex, DataColumnIdentifier, DataColumnSidecar, - DataColumnSidecarList, Epoch, EthSpec, Hash256, RuntimeFixedVector, RuntimeVariableList, - SignedBeaconBlock, + BlobSidecar, ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, Epoch, EthSpec, + Hash256, RuntimeFixedVector, RuntimeVariableList, SignedBeaconBlock, }; /// This represents the components of a partially available block @@ -32,12 +30,6 @@ pub struct PendingComponents { pub verified_data_columns: Vec>, pub executed_block: Option>, pub reconstruction_started: bool, - /// Receiver for data columns that are computed asynchronously; - /// - /// If `data_column_recv` is `Some`, it means data column computation or reconstruction has been - /// started. This can happen either via engine blobs fetching or data column reconstruction - /// (triggered when >= 50% columns are received via gossip). - pub data_column_recv: Option>>, } impl PendingComponents { @@ -202,13 +194,8 @@ impl PendingComponents { Some(AvailableBlockData::DataColumns(data_columns)) } Ordering::Less => { - // The data_columns_recv is an infallible promise that we will receive all expected - // columns, so we consider the block available. - // We take the receiver as it can't be cloned, and make_available should never - // be called again once it returns `Some`. - self.data_column_recv - .take() - .map(AvailableBlockData::DataColumnsRecv) + // Not enough data columns received yet + None } } } else { @@ -261,7 +248,6 @@ impl PendingComponents { .max(), // TODO(das): To be fixed with https://github.com/sigp/lighthouse/pull/6850 AvailableBlockData::DataColumns(_) => None, - AvailableBlockData::DataColumnsRecv(_) => None, }; let AvailabilityPendingExecutedBlock { @@ -293,7 +279,6 @@ impl PendingComponents { verified_data_columns: vec![], executed_block: None, reconstruction_started: false, - data_column_recv: None, } } @@ -331,17 +316,11 @@ impl PendingComponents { } else { "?" }; - let data_column_recv_count = if self.data_column_recv.is_some() { - 1 - } else { - 0 - }; format!( - "block {} data_columns {}/{} data_columns_recv {}", + "block {} data_columns {}/{}", block_count, self.verified_data_columns.len(), custody_columns_count, - data_column_recv_count, ) } else { let num_expected_blobs = if let Some(block) = self.get_cached_block() { @@ -352,7 +331,7 @@ impl PendingComponents { format!( "block {} blobs {}/{}", block_count, - self.verified_blobs.len(), + self.verified_blobs.iter().flatten().count(), num_expected_blobs ) } @@ -425,20 +404,21 @@ impl DataAvailabilityCheckerInner { } } - /// Fetch a data column from the cache without affecting the LRU ordering - pub fn peek_data_column( + /// Fetch data columns of a given `block_root` from the cache without affecting the LRU ordering + pub fn peek_data_columns( &self, - data_column_id: &DataColumnIdentifier, - ) -> Result>>, AvailabilityCheckError> { - if let Some(pending_components) = self.critical.read().peek(&data_column_id.block_root) { - Ok(pending_components - .verified_data_columns - .iter() - .find(|data_column| data_column.as_data_column().index == data_column_id.index) - .map(|data_column| data_column.clone_arc())) - } else { - Ok(None) - } + block_root: Hash256, + ) -> Option> { + self.critical + .read() + .peek(&block_root) + .map(|pending_components| { + pending_components + .verified_data_columns + .iter() + .map(|col| col.clone_arc()) + .collect() + }) } pub fn peek_pending_components>) -> R>( @@ -498,7 +478,6 @@ impl DataAvailabilityCheckerInner { self.state_cache.recover_pending_executed_block(block) })? { // We keep the pending components in the availability cache during block import (#5845). - // `data_column_recv` is returned as part of the available block and is no longer needed here. write_lock.put(block_root, pending_components); drop(write_lock); Ok(Availability::Available(Box::new(available_block))) @@ -551,55 +530,6 @@ impl DataAvailabilityCheckerInner { self.state_cache.recover_pending_executed_block(block) })? { // We keep the pending components in the availability cache during block import (#5845). - // `data_column_recv` is returned as part of the available block and is no longer needed here. - write_lock.put(block_root, pending_components); - drop(write_lock); - Ok(Availability::Available(Box::new(available_block))) - } else { - write_lock.put(block_root, pending_components); - Ok(Availability::MissingComponents(block_root)) - } - } - - /// The `data_column_recv` parameter is a `Receiver` for data columns that are computed - /// asynchronously. This method is used if the EL already has the blobs and returns them via the - /// `getBlobsV1` engine method. More details in [fetch_blobs.rs](https://github.com/sigp/lighthouse/blob/44f8add41ea2252769bb967864af95b3c13af8ca/beacon_node/beacon_chain/src/fetch_blobs.rs). - pub fn put_computed_data_columns_recv( - &self, - block_root: Hash256, - block_epoch: Epoch, - data_column_recv: oneshot::Receiver>, - ) -> Result, AvailabilityCheckError> { - let mut write_lock = self.critical.write(); - - // Grab existing entry or create a new entry. - let mut pending_components = write_lock - .pop_entry(&block_root) - .map(|(_, v)| v) - .unwrap_or_else(|| { - PendingComponents::empty( - block_root, - self.spec.max_blobs_per_block(block_epoch) as usize, - ) - }); - - // We have all the blobs from engine, and have started computing data columns. We store the - // receiver in `PendingComponents` for later use when importing the block. - // TODO(das): Error or log if we overwrite a prior receiver https://github.com/sigp/lighthouse/issues/6764 - pending_components.data_column_recv = Some(data_column_recv); - - debug!( - component = "data_columns_recv", - ?block_root, - status = pending_components.status_str(block_epoch, &self.spec), - "Component added to data availability checker" - ); - - if let Some(available_block) = pending_components.make_available(&self.spec, |block| { - self.state_cache.recover_pending_executed_block(block) - })? { - // We keep the pending components in the availability cache during block import (#5845). - // `data_column_recv` is returned as part of the available block and is no longer needed here. write_lock.put(block_root, pending_components); drop(write_lock); Ok(Availability::Available(Box::new(available_block))) @@ -694,7 +624,6 @@ impl DataAvailabilityCheckerInner { self.state_cache.recover_pending_executed_block(block) })? { // We keep the pending components in the availability cache during block import (#5845). - // `data_column_recv` is returned as part of the available block and is no longer needed here. write_lock.put(block_root, pending_components); drop(write_lock); Ok(Availability::Available(Box::new(available_block))) @@ -920,7 +849,6 @@ mod test { state, parent_block, parent_eth1_finalization_data, - confirmed_state_roots: vec![], consensus_context, }; @@ -1305,7 +1233,6 @@ mod pending_components_tests { eth1_data: Default::default(), eth1_deposit_index: 0, }, - confirmed_state_roots: vec![], consensus_context: ConsensusContext::new(Slot::new(0)), }, payload_verification_outcome: PayloadVerificationOutcome { 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 09d0563a4a..5fe674f30c 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 @@ -7,26 +7,21 @@ use crate::{ }; use lru::LruCache; use parking_lot::RwLock; -use ssz_derive::{Decode, Encode}; use state_processing::BlockReplayer; use std::sync::Arc; use store::OnDiskConsensusContext; 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}; /// This mirrors everything in the `AvailabilityPendingExecutedBlock`, except /// that it is much smaller because it contains only a state root instead of /// a full `BeaconState`. -#[derive(Encode, Decode, Clone)] +#[derive(Clone)] pub struct DietAvailabilityPendingExecutedBlock { - #[ssz(with = "ssz_tagged_signed_beacon_block_arc")] block: Arc>, state_root: Hash256, - #[ssz(with = "ssz_tagged_signed_beacon_block")] parent_block: SignedBeaconBlock>, parent_eth1_finalization_data: Eth1FinalizationData, - confirmed_state_roots: Vec, consensus_context: OnDiskConsensusContext, payload_verification_outcome: PayloadVerificationOutcome, custody_columns_count: usize, @@ -108,7 +103,6 @@ impl StateLRUCache { state_root, parent_block: executed_block.import_data.parent_block, parent_eth1_finalization_data: executed_block.import_data.parent_eth1_finalization_data, - confirmed_state_roots: executed_block.import_data.confirmed_state_roots, consensus_context: OnDiskConsensusContext::from_consensus_context( executed_block.import_data.consensus_context, ), @@ -138,7 +132,6 @@ impl StateLRUCache { state, parent_block: diet_executed_block.parent_block, parent_eth1_finalization_data: diet_executed_block.parent_eth1_finalization_data, - confirmed_state_roots: diet_executed_block.confirmed_state_roots, consensus_context: diet_executed_block .consensus_context .into_consensus_context(), @@ -227,7 +220,6 @@ impl From> state_root: value.import_data.state.canonical_root().unwrap(), parent_block: value.import_data.parent_block, parent_eth1_finalization_data: value.import_data.parent_eth1_finalization_data, - confirmed_state_roots: value.import_data.confirmed_state_roots, consensus_context: OnDiskConsensusContext::from_consensus_context( value.import_data.consensus_context, ), diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index 2f95d834b5..b43b259cf6 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -1,3 +1,4 @@ +use crate::beacon_proposer_cache::EpochBlockProposers; use crate::block_verification::{ cheap_state_advance_to_obtain_committees, get_validator_pubkey_cache, process_block_slash_info, BlockSlashInfo, @@ -9,14 +10,13 @@ use derivative::Derivative; use fork_choice::ProtoBlock; use kzg::{Error as KzgError, Kzg}; use proto_array::Block; -use slasher::test_utils::E; use slot_clock::SlotClock; use ssz_derive::{Decode, Encode}; use std::iter; use std::marker::PhantomData; use std::sync::Arc; use tracing::debug; -use types::data_column_sidecar::{ColumnIndex, DataColumnIdentifier}; +use types::data_column_sidecar::ColumnIndex; use types::{ BeaconStateError, ChainSpec, DataColumnSidecar, DataColumnSubnetId, EthSpec, Hash256, RuntimeVariableList, SignedBeaconBlockHeader, Slot, @@ -32,7 +32,7 @@ pub enum GossipDataColumnError { /// /// We were unable to process this data column due to an internal error. It's /// unclear if the data column is valid. - BeaconChainError(BeaconChainError), + BeaconChainError(Box), /// The proposal signature in invalid. /// /// ## Peer scoring @@ -141,24 +141,34 @@ pub enum GossipDataColumnError { /// /// The column sidecar is invalid and the peer is faulty UnexpectedDataColumn, - /// The data column length must be equal to the number of commitments/proofs, otherwise the + /// The data column length must be equal to the number of commitments, otherwise the /// sidecar is invalid. /// /// ## Peer scoring /// /// The column sidecar is invalid and the peer is faulty - InconsistentCommitmentsOrProofLength, + InconsistentCommitmentsLength { + cells_len: usize, + commitments_len: usize, + }, + /// The data column length must be equal to the number of proofs, otherwise the + /// sidecar is invalid. + /// + /// ## Peer scoring + /// + /// The column sidecar is invalid and the peer is faulty + InconsistentProofsLength { cells_len: usize, proofs_len: usize }, } impl From for GossipDataColumnError { fn from(e: BeaconChainError) -> Self { - GossipDataColumnError::BeaconChainError(e) + GossipDataColumnError::BeaconChainError(e.into()) } } impl From for GossipDataColumnError { fn from(e: BeaconStateError) -> Self { - GossipDataColumnError::BeaconChainError(BeaconChainError::BeaconStateError(e)) + GossipDataColumnError::BeaconChainError(BeaconChainError::BeaconStateError(e).into()) } } @@ -190,13 +200,6 @@ impl GossipVerifiedDataColumn ) } - pub fn id(&self) -> DataColumnIdentifier { - DataColumnIdentifier { - block_root: self.block_root, - index: self.data_column.index(), - } - } - pub fn as_data_column(&self) -> &DataColumnSidecar { self.data_column.as_data_column() } @@ -240,6 +243,14 @@ impl KzgVerifiedDataColumn { verify_kzg_for_data_column(data_column, kzg) } + /// Create a `KzgVerifiedDataColumn` from `data_column` that are already KZG verified. + /// + /// This should be used with caution, as used incorrectly it could result in KZG verification + /// being skipped and invalid data_columns being deemed valid. + pub fn from_verified(data_column: Arc>) -> Self { + Self { data: data_column } + } + pub fn from_batch( data_columns: Vec>>, kzg: &Kzg, @@ -449,7 +460,7 @@ pub fn validate_data_column_sidecar_for_gossip( if data_column.kzg_commitments.is_empty() { return Err(GossipDataColumnError::UnexpectedDataColumn); } - if data_column.column.len() != data_column.kzg_commitments.len() - || data_column.column.len() != data_column.kzg_proofs.len() - { - return Err(GossipDataColumnError::InconsistentCommitmentsOrProofLength); + + let cells_len = data_column.column.len(); + let commitments_len = data_column.kzg_commitments.len(); + let proofs_len = data_column.kzg_proofs.len(); + + if cells_len != commitments_len { + return Err(GossipDataColumnError::InconsistentCommitmentsLength { + cells_len, + commitments_len, + }); + } + + if cells_len != proofs_len { + return Err(GossipDataColumnError::InconsistentProofsLength { + cells_len, + proofs_len, + }); } Ok(()) @@ -492,7 +516,7 @@ fn verify_is_first_sidecar( .observed_column_sidecars .read() .proposer_is_known(data_column) - .map_err(|e| GossipDataColumnError::BeaconChainError(e.into()))? + .map_err(|e| GossipDataColumnError::BeaconChainError(Box::new(e.into())))? { return Err(GossipDataColumnError::PriorKnown { proposer: data_column.block_proposer_index(), @@ -557,28 +581,33 @@ fn verify_proposer_and_signature( chain: &BeaconChain, ) -> Result<(), GossipDataColumnError> { let column_slot = data_column.slot(); - let column_epoch = column_slot.epoch(E::slots_per_epoch()); + let slots_per_epoch = T::EthSpec::slots_per_epoch(); + let column_epoch = column_slot.epoch(slots_per_epoch); let column_index = data_column.index; let block_root = data_column.block_root(); let block_parent_root = data_column.block_parent_root(); - let proposer_shuffling_root = - if parent_block.slot.epoch(T::EthSpec::slots_per_epoch()) == column_epoch { - parent_block - .next_epoch_shuffling_id - .shuffling_decision_block - } else { - parent_block.root - }; + let proposer_shuffling_root = if parent_block.slot.epoch(slots_per_epoch) == column_epoch { + parent_block + .next_epoch_shuffling_id + .shuffling_decision_block + } else { + parent_block.root + }; - let proposer_opt = chain + // We lock the cache briefly to get or insert a OnceCell, then drop the lock + // before doing proposer shuffling calculation via `OnceCell::get_or_try_init`. This avoids + // holding the lock during the computation, while still ensuring the result is cached and + // initialised only once. + // + // This approach exposes the cache internals (`OnceCell` & `EpochBlockProposers`) + // as a trade-off for avoiding lock contention. + let epoch_proposers_cell = chain .beacon_proposer_cache .lock() - .get_slot::(proposer_shuffling_root, column_slot); + .get_or_insert_key(column_epoch, proposer_shuffling_root); - let (proposer_index, fork) = if let Some(proposer) = proposer_opt { - (proposer.index, proposer.fork) - } else { + let epoch_proposers = epoch_proposers_cell.get_or_try_init(move || { debug!( %block_root, index = %column_index, @@ -587,7 +616,7 @@ fn verify_proposer_and_signature( let (parent_state_root, mut parent_state) = chain .store .get_advanced_hot_state(block_parent_root, column_slot, parent_block.state_root) - .map_err(|e| GossipDataColumnError::BeaconChainError(e.into()))? + .map_err(|e| GossipDataColumnError::BeaconChainError(Box::new(e.into())))? .ok_or_else(|| { BeaconChainError::DBInconsistent(format!( "Missing state for parent block {block_parent_root:?}", @@ -602,19 +631,20 @@ fn verify_proposer_and_signature( )?; let proposers = state.get_beacon_proposer_indices(&chain.spec)?; - let proposer_index = *proposers - .get(column_slot.as_usize() % T::EthSpec::slots_per_epoch() as usize) - .ok_or_else(|| BeaconChainError::NoProposerForSlot(column_slot))?; - // Prime the proposer shuffling cache with the newly-learned value. - chain.beacon_proposer_cache.lock().insert( - column_epoch, - proposer_shuffling_root, - proposers, - state.fork(), - )?; - (proposer_index, state.fork()) - }; + Ok::<_, GossipDataColumnError>(EpochBlockProposers { + epoch: column_epoch, + fork: state.fork(), + proposers: proposers.into(), + }) + })?; + + let proposer_index = *epoch_proposers + .proposers + .get(column_slot.as_usize() % slots_per_epoch as usize) + .ok_or_else(|| BeaconChainError::NoProposerForSlot(column_slot))?; + + let fork = epoch_proposers.fork; // Signature verify the signed block header. let signature_is_valid = { @@ -704,7 +734,7 @@ pub fn observe_gossip_data_column( chain: &BeaconChain, ) -> Result<(), GossipDataColumnError> { // Now the signature is valid, store the proposal so we don't accept another data column sidecar - // with the same `DataColumnIdentifier`. It's important to double-check that the proposer still + // with the same `ColumnIndex`. It's important to double-check that the proposer still // hasn't been observed so we don't have a race-condition when verifying two blocks // simultaneously. // @@ -718,7 +748,7 @@ pub fn observe_gossip_data_column( .observed_column_sidecars .write() .observe_sidecar(data_column_sidecar) - .map_err(|e| GossipDataColumnError::BeaconChainError(e.into()))? + .map_err(|e| GossipDataColumnError::BeaconChainError(Box::new(e.into())))? { return Err(GossipDataColumnError::PriorKnown { proposer: data_column_sidecar.block_proposer_index(), diff --git a/beacon_node/beacon_chain/src/early_attester_cache.rs b/beacon_node/beacon_chain/src/early_attester_cache.rs index f4810e7b4a..5665ef3775 100644 --- a/beacon_node/beacon_chain/src/early_attester_cache.rs +++ b/beacon_node/beacon_chain/src/early_attester_cache.rs @@ -74,10 +74,6 @@ impl EarlyAttesterCache { AvailableBlockData::NoData => (None, None), AvailableBlockData::Blobs(blobs) => (Some(blobs.clone()), None), AvailableBlockData::DataColumns(data_columns) => (None, Some(data_columns.clone())), - // TODO(das): Once the columns are received, they will not be available in - // the early attester cache. If someone does a query to us via RPC we - // will get downscored. - AvailableBlockData::DataColumnsRecv(_) => (None, None), }; let item = CacheItem { diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 323e371a9f..a296163adc 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -298,7 +298,7 @@ pub enum BlockProductionError { MissingExecutionPayload, MissingKzgCommitment(String), TokioJoin(JoinError), - BeaconChain(BeaconChainError), + BeaconChain(Box), InvalidPayloadFork, InvalidBlockVariant(String), KzgError(kzg::Error), diff --git a/beacon_node/beacon_chain/src/eth1_chain.rs b/beacon_node/beacon_chain/src/eth1_chain.rs index 43429b726c..8a79bff4c7 100644 --- a/beacon_node/beacon_chain/src/eth1_chain.rs +++ b/beacon_node/beacon_chain/src/eth1_chain.rs @@ -362,6 +362,12 @@ pub struct DummyEth1ChainBackend(PhantomData); impl Eth1ChainBackend for DummyEth1ChainBackend { /// Produce some deterministic junk based upon the current epoch. fn eth1_data(&self, state: &BeaconState, _spec: &ChainSpec) -> Result { + // [New in Electra:EIP6110] + if let Ok(deposit_requests_start_index) = state.deposit_requests_start_index() { + if state.eth1_deposit_index() == deposit_requests_start_index { + return Ok(state.eth1_data().clone()); + } + } let current_epoch = state.current_epoch(); let slots_per_voting_period = E::slots_per_eth1_voting_period() as u64; let current_voting_period: u64 = current_epoch.as_u64() / slots_per_voting_period; @@ -456,6 +462,12 @@ impl CachingEth1Backend { impl Eth1ChainBackend for CachingEth1Backend { fn eth1_data(&self, state: &BeaconState, spec: &ChainSpec) -> Result { + // [New in Electra:EIP6110] + if let Ok(deposit_requests_start_index) = state.deposit_requests_start_index() { + if state.eth1_deposit_index() == deposit_requests_start_index { + return Ok(state.eth1_data().clone()); + } + } let period = E::SlotsPerEth1VotingPeriod::to_u64(); let voting_period_start_slot = (state.slot() / period) * period; let voting_period_start_seconds = slot_start_seconds( diff --git a/beacon_node/beacon_chain/src/eth1_finalization_cache.rs b/beacon_node/beacon_chain/src/eth1_finalization_cache.rs index 0b9d19e156..8c3bb8c483 100644 --- a/beacon_node/beacon_chain/src/eth1_finalization_cache.rs +++ b/beacon_node/beacon_chain/src/eth1_finalization_cache.rs @@ -100,22 +100,13 @@ impl CheckpointMap { /// This cache stores `Eth1CacheData` that could potentially be finalized within 4 /// future epochs. +#[derive(Default)] pub struct Eth1FinalizationCache { by_checkpoint: CheckpointMap, pending_eth1: BTreeMap, last_finalized: Option, } -impl Default for Eth1FinalizationCache { - fn default() -> Self { - Self { - by_checkpoint: CheckpointMap::new(), - pending_eth1: BTreeMap::new(), - last_finalized: None, - } - } -} - /// Provides a cache of `Eth1CacheData` at epoch boundaries. This is used to /// finalize deposits when a new epoch is finalized. /// diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index 8328726b21..236e8c9cb4 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -374,9 +374,9 @@ pub fn validate_execution_payload_for_gossip( .slot_clock .start_of(block.slot()) .map(|d| d.as_secs()) - .ok_or(BlockError::BeaconChainError( + .ok_or(BlockError::BeaconChainError(Box::new( BeaconChainError::UnableToComputeTimeAtSlot, - ))?; + )))?; // The block's execution payload timestamp is correct with respect to the slot if execution_payload.timestamp() != expected_timestamp { @@ -559,7 +559,7 @@ where "prepare_execution_payload_forkchoice_update_params", ) .await - .map_err(BlockProductionError::BeaconChain)?; + .map_err(|e| BlockProductionError::BeaconChain(Box::new(e)))?; let suggested_fee_recipient = execution_layer .get_suggested_fee_recipient(proposer_index) diff --git a/beacon_node/beacon_chain/src/fetch_blobs.rs b/beacon_node/beacon_chain/src/fetch_blobs.rs index 3c28ac9a44..d91f103b9d 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs.rs @@ -7,34 +7,52 @@ //! on P2P gossip to the network. From PeerDAS onwards, together with the increase in blob count, //! broadcasting blobs requires a much higher bandwidth, and is only done by high capacity //! supernodes. + use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob}; use crate::kzg_utils::blobs_to_data_column_sidecars; use crate::observed_data_sidecars::DoNotObserve; -use crate::{metrics, AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError}; -use execution_layer::json_structures::BlobAndProofV1; +use crate::{ + metrics, AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, + BlockError, +}; +use execution_layer::json_structures::{BlobAndProofV1, BlobAndProofV2}; use execution_layer::Error as ExecutionLayerError; use metrics::{inc_counter, TryExt}; use ssz_types::FixedVector; use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash; +use std::collections::HashSet; use std::sync::Arc; -use tokio::sync::oneshot; -use tracing::{debug, error}; +use tracing::debug; use types::blob_sidecar::{BlobSidecarError, FixedBlobSidecarList}; +use types::data_column_sidecar::DataColumnSidecarError; use types::{ - BeaconStateError, BlobSidecar, ChainSpec, DataColumnSidecar, DataColumnSidecarList, EthSpec, - FullPayload, Hash256, SignedBeaconBlock, SignedBeaconBlockHeader, + BeaconStateError, Blob, BlobSidecar, ChainSpec, ColumnIndex, DataColumnSidecarList, EthSpec, + FullPayload, Hash256, KzgProofs, SignedBeaconBlock, SignedBeaconBlockHeader, VersionedHash, }; +/// Blobs or data column to be published to the gossip network. pub enum BlobsOrDataColumns { Blobs(Vec>), DataColumns(DataColumnSidecarList), } +/// Result from engine get blobs to be passed onto `DataAvailabilityChecker`. +/// +/// The blobs are retrieved from a trusted EL and columns are computed locally, therefore they are +/// considered valid without requiring extra validation. +pub enum EngineGetBlobsOutput { + Blobs(FixedBlobSidecarList), + /// A filtered list of custody data columns to be imported into the `DataAvailabilityChecker`. + CustodyColumns(DataColumnSidecarList), +} + #[derive(Debug)] pub enum FetchEngineBlobError { BeaconStateError(BeaconStateError), + BeaconChainError(Box), BlobProcessingError(BlockError), BlobSidecarError(BlobSidecarError), + DataColumnSidecarError(DataColumnSidecarError), ExecutionLayerMissing, InternalError(String), GossipBlob(GossipBlobError), @@ -48,6 +66,7 @@ pub async fn fetch_and_process_engine_blobs( chain: Arc>, block_root: Hash256, block: Arc>>, + custody_columns: HashSet, publish_fn: impl Fn(BlobsOrDataColumns) + Send + 'static, ) -> Result, FetchEngineBlobError> { let versioned_hashes = if let Some(kzg_commitments) = block @@ -66,8 +85,34 @@ pub async fn fetch_and_process_engine_blobs( return Ok(None); }; - let num_expected_blobs = versioned_hashes.len(); + debug!( + num_expected_blobs = versioned_hashes.len(), + "Fetching blobs from the EL" + ); + if chain.spec.is_peer_das_enabled_for_epoch(block.epoch()) { + fetch_and_process_blobs_v2( + chain, + block_root, + block, + versioned_hashes, + custody_columns, + publish_fn, + ) + .await + } else { + fetch_and_process_blobs_v1(chain, block_root, block, versioned_hashes, publish_fn).await + } +} + +async fn fetch_and_process_blobs_v1( + chain: Arc>, + block_root: Hash256, + block: Arc>, + versioned_hashes: Vec, + publish_fn: impl Fn(BlobsOrDataColumns) + Send + Sized, +) -> Result, FetchEngineBlobError> { + let num_expected_blobs = versioned_hashes.len(); let execution_layer = chain .execution_layer .as_ref() @@ -76,7 +121,7 @@ pub async fn fetch_and_process_engine_blobs( metrics::observe(&metrics::BLOBS_FROM_EL_EXPECTED, num_expected_blobs as f64); debug!(num_expected_blobs, "Fetching blobs from the EL"); let response = execution_layer - .get_blobs(versioned_hashes) + .get_blobs_v1(versioned_hashes) .await .inspect_err(|_| { inc_counter(&metrics::BLOBS_FROM_EL_ERROR_TOTAL); @@ -125,59 +170,9 @@ pub async fn fetch_and_process_engine_blobs( .collect::, _>>() .map_err(FetchEngineBlobError::GossipBlob)?; - let peer_das_enabled = chain.spec.is_peer_das_enabled_for_epoch(block.epoch()); - - let data_columns_receiver_opt = if peer_das_enabled { - // Partial blobs response isn't useful for PeerDAS, so we don't bother building and publishing data columns. - if num_fetched_blobs != num_expected_blobs { - debug!( - info = "Unable to compute data columns", - num_fetched_blobs, num_expected_blobs, "Not all blobs fetched from the EL" - ); - return Ok(None); - } - - if chain - .canonical_head - .fork_choice_read_lock() - .contains_block(&block_root) - { - // Avoid computing columns if block has already been imported. - debug!( - info = "block has already been imported", - "Ignoring EL blobs response" - ); - return Ok(None); - } - - if chain - .canonical_head - .fork_choice_read_lock() - .contains_block(&block_root) - { - // Avoid computing columns if block has already been imported. - debug!( - info = "block has already been imported", - "Ignoring EL blobs response" - ); - return Ok(None); - } - - let data_columns_receiver = spawn_compute_and_publish_data_columns_task( - &chain, - block.clone(), - fixed_blob_sidecar_list.clone(), - publish_fn, - ); - - Some(data_columns_receiver) - } else { - if !blobs_to_import_and_publish.is_empty() { - publish_fn(BlobsOrDataColumns::Blobs(blobs_to_import_and_publish)); - } - - None - }; + if !blobs_to_import_and_publish.is_empty() { + publish_fn(BlobsOrDataColumns::Blobs(blobs_to_import_and_publish)); + } debug!(num_fetched_blobs, "Processing engine blobs"); @@ -185,8 +180,7 @@ pub async fn fetch_and_process_engine_blobs( .process_engine_blobs( block.slot(), block_root, - fixed_blob_sidecar_list.clone(), - data_columns_receiver_opt, + EngineGetBlobsOutput::Blobs(fixed_blob_sidecar_list.clone()), ) .await .map_err(FetchEngineBlobError::BlobProcessingError)?; @@ -194,67 +188,140 @@ pub async fn fetch_and_process_engine_blobs( Ok(Some(availability_processing_status)) } -/// Spawn a blocking task here for long computation tasks, so it doesn't block processing, and it -/// allows blobs / data columns to propagate without waiting for processing. -/// -/// An `mpsc::Sender` is then used to send the produced data columns to the `beacon_chain` for it -/// to be persisted, **after** the block is made attestable. -/// -/// The reason for doing this is to make the block available and attestable as soon as possible, -/// while maintaining the invariant that block and data columns are persisted atomically. -fn spawn_compute_and_publish_data_columns_task( +async fn fetch_and_process_blobs_v2( + chain: Arc>, + block_root: Hash256, + block: Arc>, + versioned_hashes: Vec, + custody_columns_indices: HashSet, + publish_fn: impl Fn(BlobsOrDataColumns) + Send + 'static, +) -> Result, FetchEngineBlobError> { + let num_expected_blobs = versioned_hashes.len(); + let execution_layer = chain + .execution_layer + .as_ref() + .ok_or(FetchEngineBlobError::ExecutionLayerMissing)?; + + metrics::observe(&metrics::BLOBS_FROM_EL_EXPECTED, num_expected_blobs as f64); + debug!(num_expected_blobs, "Fetching blobs from the EL"); + let response = execution_layer + .get_blobs_v2(versioned_hashes) + .await + .inspect_err(|_| { + inc_counter(&metrics::BLOBS_FROM_EL_ERROR_TOTAL); + }) + .map_err(FetchEngineBlobError::RequestFailed)?; + + let (blobs, proofs): (Vec<_>, Vec<_>) = response + .into_iter() + .filter_map(|blob_and_proof_opt| { + blob_and_proof_opt.map(|blob_and_proof| { + let BlobAndProofV2 { blob, proofs } = blob_and_proof; + (blob, proofs) + }) + }) + .unzip(); + + let num_fetched_blobs = blobs.len(); + metrics::observe(&metrics::BLOBS_FROM_EL_RECEIVED, num_fetched_blobs as f64); + + // Partial blobs response isn't useful for PeerDAS, so we don't bother building and publishing data columns. + if num_fetched_blobs != num_expected_blobs { + debug!( + info = "Unable to compute data columns", + num_fetched_blobs, num_expected_blobs, "Not all blobs fetched from the EL" + ); + inc_counter(&metrics::BLOBS_FROM_EL_MISS_TOTAL); + return Ok(None); + } else { + inc_counter(&metrics::BLOBS_FROM_EL_HIT_TOTAL); + } + + if chain + .canonical_head + .fork_choice_read_lock() + .contains_block(&block_root) + { + // Avoid computing columns if block has already been imported. + debug!( + info = "block has already been imported", + "Ignoring EL blobs response" + ); + return Ok(None); + } + + let custody_columns = compute_and_publish_data_columns( + &chain, + block.clone(), + blobs, + proofs, + custody_columns_indices, + publish_fn, + ) + .await?; + + debug!(num_fetched_blobs, "Processing engine blobs"); + + let availability_processing_status = chain + .process_engine_blobs( + block.slot(), + block_root, + EngineGetBlobsOutput::CustodyColumns(custody_columns), + ) + .await + .map_err(FetchEngineBlobError::BlobProcessingError)?; + + Ok(Some(availability_processing_status)) +} + +/// Offload the data column computation to a blocking task to avoid holding up the async runtime. +async fn compute_and_publish_data_columns( chain: &Arc>, block: Arc>>, - blobs: FixedBlobSidecarList, + blobs: Vec>, + proofs: Vec>, + custody_columns_indices: HashSet, publish_fn: impl Fn(BlobsOrDataColumns) + Send + 'static, -) -> oneshot::Receiver>>> { +) -> Result, FetchEngineBlobError> { let chain_cloned = chain.clone(); - let (data_columns_sender, data_columns_receiver) = oneshot::channel(); + chain + .spawn_blocking_handle( + move || { + let mut timer = metrics::start_timer_vec( + &metrics::DATA_COLUMN_SIDECAR_COMPUTATION, + &[&blobs.len().to_string()], + ); - chain.task_executor.spawn_blocking( - move || { - let mut timer = metrics::start_timer_vec( - &metrics::DATA_COLUMN_SIDECAR_COMPUTATION, - &[&blobs.len().to_string()], - ); - let blob_refs = blobs - .iter() - .filter_map(|b| b.as_ref().map(|b| &b.blob)) - .collect::>(); - let data_columns_result = blobs_to_data_column_sidecars( - &blob_refs, - &block, - &chain_cloned.kzg, - &chain_cloned.spec, - ) - .discard_timer_on_break(&mut timer); - drop(timer); + let blob_refs = blobs.iter().collect::>(); + let cell_proofs = proofs.into_iter().flatten().collect(); + let data_columns_result = blobs_to_data_column_sidecars( + &blob_refs, + cell_proofs, + &block, + &chain_cloned.kzg, + &chain_cloned.spec, + ) + .discard_timer_on_break(&mut timer); + drop(timer); - let all_data_columns = match data_columns_result { - Ok(d) => d, - Err(e) => { - error!( - error = ?e, - "Failed to build data column sidecars from blobs" - ); - return; - } - }; + // This filtering ensures we only import and publish the custody columns. + // `DataAvailabilityChecker` requires a strict match on custody columns count to + // consider a block available. + let custody_columns = data_columns_result + .map(|mut data_columns| { + data_columns.retain(|col| custody_columns_indices.contains(&col.index)); + data_columns + }) + .map_err(FetchEngineBlobError::DataColumnSidecarError)?; - if data_columns_sender.send(all_data_columns.clone()).is_err() { - // Data column receiver have been dropped - block may have already been imported. - // This race condition exists because gossip columns may arrive and trigger block - // import during the computation. Here we just drop the computed columns. - debug!("Failed to send computed data columns"); - return; - }; - - publish_fn(BlobsOrDataColumns::DataColumns(all_data_columns)); - }, - "compute_and_publish_data_columns", - ); - - data_columns_receiver + publish_fn(BlobsOrDataColumns::DataColumns(custody_columns.clone())); + Ok(custody_columns) + }, + "compute_and_publish_data_columns", + ) + .await + .map_err(|e| FetchEngineBlobError::BeaconChainError(Box::new(e))) + .and_then(|r| r) } fn build_blob_sidecars( diff --git a/beacon_node/beacon_chain/src/fulu_readiness.rs b/beacon_node/beacon_chain/src/fulu_readiness.rs index 872fe58f2b..1107acad74 100644 --- a/beacon_node/beacon_chain/src/fulu_readiness.rs +++ b/beacon_node/beacon_chain/src/fulu_readiness.rs @@ -1,7 +1,7 @@ //! Provides tools for checking if a node is ready for the Fulu upgrade. use crate::{BeaconChain, BeaconChainTypes}; -use execution_layer::http::{ENGINE_GET_PAYLOAD_V4, ENGINE_NEW_PAYLOAD_V4}; +use execution_layer::http::{ENGINE_GET_PAYLOAD_V5, ENGINE_NEW_PAYLOAD_V4}; use serde::{Deserialize, Serialize}; use std::fmt; use std::time::Duration; @@ -87,12 +87,12 @@ impl BeaconChain { Ok(capabilities) => { let mut missing_methods = String::from("Required Methods Unsupported:"); let mut all_good = true; - // TODO(fulu) switch to v5 when the EL is ready - if !capabilities.get_payload_v4 { + if !capabilities.get_payload_v5 { missing_methods.push(' '); - missing_methods.push_str(ENGINE_GET_PAYLOAD_V4); + missing_methods.push_str(ENGINE_GET_PAYLOAD_V5); all_good = false; } + // TODO(fulu) switch to v5 when the EL is ready if !capabilities.new_payload_v4 { missing_methods.push(' '); missing_methods.push_str(ENGINE_NEW_PAYLOAD_V4); diff --git a/beacon_node/beacon_chain/src/head_tracker.rs b/beacon_node/beacon_chain/src/head_tracker.rs deleted file mode 100644 index 9c06ef33a1..0000000000 --- a/beacon_node/beacon_chain/src/head_tracker.rs +++ /dev/null @@ -1,214 +0,0 @@ -use parking_lot::{RwLock, RwLockReadGuard}; -use ssz_derive::{Decode, Encode}; -use std::collections::HashMap; -use types::{Hash256, Slot}; - -#[derive(Debug, PartialEq)] -pub enum Error { - MismatchingLengths { roots_len: usize, slots_len: usize }, -} - -/// Maintains a list of `BeaconChain` head block roots and slots. -/// -/// Each time a new block is imported, it should be applied to the `Self::register_block` function. -/// In order for this struct to be effective, every single block that is imported must be -/// registered here. -#[derive(Default, Debug)] -pub struct HeadTracker(pub RwLock>); - -pub type HeadTrackerReader<'a> = RwLockReadGuard<'a, HashMap>; - -impl HeadTracker { - /// Register a block with `Self`, so it may or may not be included in a `Self::heads` call. - /// - /// This function assumes that no block is imported without its parent having already been - /// imported. It cannot detect an error if this is not the case, it is the responsibility of - /// the upstream user. - pub fn register_block(&self, block_root: Hash256, parent_root: Hash256, slot: Slot) { - let mut map = self.0.write(); - map.remove(&parent_root); - map.insert(block_root, slot); - } - - /// Returns true iff `block_root` is a recognized head. - pub fn contains_head(&self, block_root: Hash256) -> bool { - self.0.read().contains_key(&block_root) - } - - /// Returns the list of heads in the chain. - pub fn heads(&self) -> Vec<(Hash256, Slot)> { - self.0 - .read() - .iter() - .map(|(root, slot)| (*root, *slot)) - .collect() - } - - /// Returns a `SszHeadTracker`, which contains all necessary information to restore the state - /// of `Self` at some later point. - /// - /// Should ONLY be used for tests, due to the potential for database races. - /// - /// See - #[cfg(test)] - pub fn to_ssz_container(&self) -> SszHeadTracker { - SszHeadTracker::from_map(&self.0.read()) - } - - /// Creates a new `Self` from the given `SszHeadTracker`, restoring `Self` to the same state of - /// the `Self` that created the `SszHeadTracker`. - pub fn from_ssz_container(ssz_container: &SszHeadTracker) -> Result { - let roots_len = ssz_container.roots.len(); - let slots_len = ssz_container.slots.len(); - - if roots_len != slots_len { - Err(Error::MismatchingLengths { - roots_len, - slots_len, - }) - } else { - let map = ssz_container - .roots - .iter() - .zip(ssz_container.slots.iter()) - .map(|(root, slot)| (*root, *slot)) - .collect::>(); - - Ok(Self(RwLock::new(map))) - } - } -} - -impl PartialEq for HeadTracker { - fn eq(&self, other: &HeadTracker) -> bool { - *self.0.read() == *other.0.read() - } -} - -/// Helper struct that is used to encode/decode the state of the `HeadTracker` as SSZ bytes. -/// -/// This is used when persisting the state of the `BeaconChain` to disk. -#[derive(Encode, Decode, Clone)] -pub struct SszHeadTracker { - roots: Vec, - slots: Vec, -} - -impl SszHeadTracker { - pub fn from_map(map: &HashMap) -> Self { - let (roots, slots) = map.iter().map(|(hash, slot)| (*hash, *slot)).unzip(); - SszHeadTracker { roots, slots } - } -} - -#[cfg(test)] -mod test { - use super::*; - use ssz::{Decode, Encode}; - use types::{BeaconBlock, EthSpec, FixedBytesExtended, MainnetEthSpec}; - - type E = MainnetEthSpec; - - #[test] - fn block_add() { - let spec = &E::default_spec(); - - let head_tracker = HeadTracker::default(); - - for i in 0..16 { - let mut block: BeaconBlock = BeaconBlock::empty(spec); - let block_root = Hash256::from_low_u64_be(i); - - *block.slot_mut() = Slot::new(i); - *block.parent_root_mut() = if i == 0 { - Hash256::random() - } else { - Hash256::from_low_u64_be(i - 1) - }; - - head_tracker.register_block(block_root, block.parent_root(), block.slot()); - } - - assert_eq!( - head_tracker.heads(), - vec![(Hash256::from_low_u64_be(15), Slot::new(15))], - "should only have one head" - ); - - let mut block: BeaconBlock = BeaconBlock::empty(spec); - let block_root = Hash256::from_low_u64_be(42); - *block.slot_mut() = Slot::new(15); - *block.parent_root_mut() = Hash256::from_low_u64_be(14); - head_tracker.register_block(block_root, block.parent_root(), block.slot()); - - let heads = head_tracker.heads(); - - assert_eq!(heads.len(), 2, "should only have two heads"); - assert!( - heads - .iter() - .any(|(root, slot)| *root == Hash256::from_low_u64_be(15) && *slot == Slot::new(15)), - "should contain first head" - ); - assert!( - heads - .iter() - .any(|(root, slot)| *root == Hash256::from_low_u64_be(42) && *slot == Slot::new(15)), - "should contain second head" - ); - } - - #[test] - fn empty_round_trip() { - let non_empty = HeadTracker::default(); - for i in 0..16 { - non_empty.0.write().insert(Hash256::random(), Slot::new(i)); - } - let bytes = non_empty.to_ssz_container().as_ssz_bytes(); - - assert_eq!( - HeadTracker::from_ssz_container( - &SszHeadTracker::from_ssz_bytes(&bytes).expect("should decode") - ), - Ok(non_empty), - "non_empty should pass round trip" - ); - } - - #[test] - fn non_empty_round_trip() { - let non_empty = HeadTracker::default(); - for i in 0..16 { - non_empty.0.write().insert(Hash256::random(), Slot::new(i)); - } - let bytes = non_empty.to_ssz_container().as_ssz_bytes(); - - assert_eq!( - HeadTracker::from_ssz_container( - &SszHeadTracker::from_ssz_bytes(&bytes).expect("should decode") - ), - Ok(non_empty), - "non_empty should pass round trip" - ); - } - - #[test] - fn bad_length() { - let container = SszHeadTracker { - roots: vec![Hash256::random()], - slots: vec![], - }; - let bytes = container.as_ssz_bytes(); - - assert_eq!( - HeadTracker::from_ssz_container( - &SszHeadTracker::from_ssz_bytes(&bytes).expect("should decode") - ), - Err(Error::MismatchingLengths { - roots_len: 1, - slots_len: 0 - }), - "should fail decoding with bad lengths" - ); - } -} diff --git a/beacon_node/beacon_chain/src/historical_blocks.rs b/beacon_node/beacon_chain/src/historical_blocks.rs index ee51964910..348e6d52a6 100644 --- a/beacon_node/beacon_chain/src/historical_blocks.rs +++ b/beacon_node/beacon_chain/src/historical_blocks.rs @@ -132,7 +132,7 @@ impl BeaconChain { AvailableBlockData::Blobs(..) => { new_oldest_blob_slot = Some(block.slot()); } - AvailableBlockData::DataColumns(_) | AvailableBlockData::DataColumnsRecv(_) => { + AvailableBlockData::DataColumns(_) => { new_oldest_data_column_slot = Some(block.slot()); } } diff --git a/beacon_node/beacon_chain/src/inclusion_list_verification.rs b/beacon_node/beacon_chain/src/inclusion_list_verification.rs index 28abb1325a..48096d91f7 100644 --- a/beacon_node/beacon_chain/src/inclusion_list_verification.rs +++ b/beacon_node/beacon_chain/src/inclusion_list_verification.rs @@ -14,14 +14,14 @@ pub enum GossipInclusionListError { ValidatorNotInCommittee, TooManyTransactions, InvalidSignature, - BeaconChainError(BeaconChainError), + BeaconChainError(Box), PriorInclusionListKnown, // TODO: equivocation e.g. PriorInclusionListKnown } impl From for GossipInclusionListError { fn from(value: BeaconChainError) -> Self { - Self::BeaconChainError(value) + Self::BeaconChainError(value.into()) } } @@ -80,9 +80,9 @@ impl GossipVerifiedInclusionList { let validator_index = signed_il.message.validator_index as usize; let pubkey = chain.validator_pubkey(validator_index)?; let Some(pubkey) = pubkey else { - return Err(GossipInclusionListError::BeaconChainError( + return Err(GossipInclusionListError::BeaconChainError(Box::new( BeaconChainError::ValidatorIndexUnknown(validator_index), - )); + ))); }; signed_il.signature.verify(&pubkey, message); diff --git a/beacon_node/beacon_chain/src/kzg_utils.rs b/beacon_node/beacon_chain/src/kzg_utils.rs index 06cce14144..704fb3663f 100644 --- a/beacon_node/beacon_chain/src/kzg_utils.rs +++ b/beacon_node/beacon_chain/src/kzg_utils.rs @@ -1,15 +1,16 @@ use kzg::{ - Blob as KzgBlob, Bytes48, CellRef as KzgCellRef, CellsAndKzgProofs, Error as KzgError, Kzg, + Blob as KzgBlob, Bytes48, Cell as KzgCell, CellRef as KzgCellRef, CellsAndKzgProofs, + Error as KzgError, Kzg, CELLS_PER_EXT_BLOB, }; use rayon::prelude::*; -use ssz_types::FixedVector; +use ssz_types::{FixedVector, VariableList}; use std::sync::Arc; use types::beacon_block_body::KzgCommitments; use types::data_column_sidecar::{Cell, DataColumn, DataColumnSidecarError}; use types::{ - Blob, BlobSidecar, BlobSidecarList, ChainSpec, ColumnIndex, DataColumnSidecar, - DataColumnSidecarList, EthSpec, Hash256, KzgCommitment, KzgProof, KzgProofs, SignedBeaconBlock, - SignedBeaconBlockHeader, SignedBlindedBeaconBlock, + Blob, BlobSidecar, BlobSidecarList, ChainSpec, DataColumnSidecar, DataColumnSidecarList, + EthSpec, Hash256, KzgCommitment, KzgProof, SignedBeaconBlock, SignedBeaconBlockHeader, + SignedBlindedBeaconBlock, }; /// Converts a blob ssz List object to an array to be used with the kzg @@ -43,6 +44,33 @@ pub fn validate_blob( kzg.verify_blob_kzg_proof(&kzg_blob, kzg_commitment, kzg_proof) } +/// Validates a list of blobs along with their corresponding KZG commitments and +/// cell proofs for the extended blobs. +pub fn validate_blobs_and_cell_proofs( + kzg: &Kzg, + blobs: Vec<&Blob>, + cell_proofs: &[KzgProof], + kzg_commitments: &KzgCommitments, +) -> Result<(), KzgError> { + let cells = compute_cells::(&blobs, kzg)?; + let cell_refs = cells.iter().map(|cell| cell.as_ref()).collect::>(); + let cell_indices = (0..blobs.len()) + .flat_map(|_| 0..CELLS_PER_EXT_BLOB as u64) + .collect::>(); + + let proofs = cell_proofs + .iter() + .map(|&proof| Bytes48::from(proof)) + .collect::>(); + + let commitments = kzg_commitments + .iter() + .flat_map(|&commitment| std::iter::repeat_n(Bytes48::from(commitment), CELLS_PER_EXT_BLOB)) + .collect::>(); + + kzg.verify_cell_proof_batch(&cell_refs, &proofs, cell_indices, &commitments) +} + /// Validate a batch of `DataColumnSidecar`. pub fn validate_data_columns<'a, E: EthSpec, I>( kzg: &Kzg, @@ -51,38 +79,27 @@ pub fn validate_data_columns<'a, E: EthSpec, I>( where I: Iterator>> + Clone, { - let cells = data_column_iter - .clone() - .flat_map(|data_column| data_column.column.iter().map(ssz_cell_to_crypto_cell::)) - .collect::, KzgError>>()?; + let mut cells = Vec::new(); + let mut proofs = Vec::new(); + let mut column_indices = Vec::new(); + let mut commitments = Vec::new(); - let proofs = data_column_iter - .clone() - .flat_map(|data_column| { - data_column - .kzg_proofs - .iter() - .map(|&proof| Bytes48::from(proof)) - }) - .collect::>(); + for data_column in data_column_iter { + let col_index = data_column.index; - let column_indices = data_column_iter - .clone() - .flat_map(|data_column| { - let col_index = data_column.index; - data_column.column.iter().map(move |_| col_index) - }) - .collect::>(); + for cell in &data_column.column { + cells.push(ssz_cell_to_crypto_cell::(cell)?); + column_indices.push(col_index); + } - let commitments = data_column_iter - .clone() - .flat_map(|data_column| { - data_column - .kzg_commitments - .iter() - .map(|&commitment| Bytes48::from(commitment)) - }) - .collect::>(); + for &proof in &data_column.kzg_proofs { + proofs.push(Bytes48::from(proof)); + } + + for &commitment in &data_column.kzg_commitments { + commitments.push(Bytes48::from(commitment)); + } + } kzg.verify_cell_proof_batch(&cells, &proofs, column_indices, &commitments) } @@ -148,6 +165,7 @@ pub fn verify_kzg_proof( /// Build data column sidecars from a signed beacon block and its blobs. pub fn blobs_to_data_column_sidecars( blobs: &[&Blob], + cell_proofs: Vec, block: &SignedBeaconBlock, kzg: &Kzg, spec: &ChainSpec, @@ -164,15 +182,28 @@ pub fn blobs_to_data_column_sidecars( let kzg_commitments_inclusion_proof = block.message().body().kzg_commitments_merkle_proof()?; let signed_block_header = block.signed_block_header(); + let proof_chunks = cell_proofs + .chunks_exact(spec.number_of_columns as usize) + .collect::>(); + // NOTE: assumes blob sidecars are ordered by index let blob_cells_and_proofs_vec = blobs .into_par_iter() - .map(|blob| { + .zip(proof_chunks.into_par_iter()) + .map(|(blob, proofs)| { let blob = blob .as_ref() .try_into() .expect("blob should have a guaranteed size due to FixedVector"); - kzg.compute_cells_and_proofs(blob) + + kzg.compute_cells(blob).map(|cells| { + ( + cells, + proofs + .try_into() + .expect("proof chunks should have exactly `number_of_columns` proofs"), + ) + }) }) .collect::, KzgError>>()?; @@ -186,6 +217,23 @@ pub fn blobs_to_data_column_sidecars( .map_err(DataColumnSidecarError::BuildSidecarFailed) } +pub fn compute_cells(blobs: &[&Blob], kzg: &Kzg) -> Result, KzgError> { + let cells_vec = blobs + .into_par_iter() + .map(|blob| { + let blob = blob + .as_ref() + .try_into() + .expect("blob should have a guaranteed size due to FixedVector"); + + kzg.compute_cells(blob) + }) + .collect::, KzgError>>()?; + + let cells_flattened: Vec = cells_vec.into_iter().flatten().collect(); + Ok(cells_flattened) +} + pub(crate) fn build_data_column_sidecars( kzg_commitments: KzgCommitments, kzg_commitments_inclusion_proof: FixedVector, @@ -236,7 +284,7 @@ pub(crate) fn build_data_column_sidecars( index: index as u64, column: DataColumn::::from(col), kzg_commitments: kzg_commitments.clone(), - kzg_proofs: KzgProofs::::from(proofs), + kzg_proofs: VariableList::from(proofs), signed_block_header: signed_block_header.clone(), kzg_commitments_inclusion_proof: kzg_commitments_inclusion_proof.clone(), }) @@ -300,12 +348,7 @@ pub fn reconstruct_blobs( .collect(); let blob = Blob::::new(blob_bytes).map_err(|e| format!("{e:?}"))?; - let kzg_commitment = first_data_column - .kzg_commitments - .get(row_index) - .ok_or(format!("Missing KZG commitment for blob {row_index}"))?; - let kzg_proof = compute_blob_kzg_proof::(kzg, &blob, *kzg_commitment) - .map_err(|e| format!("{e:?}"))?; + let kzg_proof = KzgProof::empty(); BlobSidecar::::new_with_existing_proof( row_index, @@ -373,14 +416,15 @@ pub fn reconstruct_data_columns( mod test { use crate::kzg_utils::{ blobs_to_data_column_sidecars, reconstruct_blobs, reconstruct_data_columns, + validate_blobs_and_cell_proofs, }; use bls::Signature; use eth2::types::BlobsBundle; use execution_layer::test_utils::generate_blobs; use kzg::{trusted_setup::get_trusted_setup, Kzg, KzgCommitment, TrustedSetup}; use types::{ - beacon_block_body::KzgCommitments, BeaconBlock, BeaconBlockDeneb, BlobsList, ChainSpec, - EmptyBlock, EthSpec, MainnetEthSpec, SignedBeaconBlock, + beacon_block_body::KzgCommitments, BeaconBlock, BeaconBlockFulu, BlobsList, ChainSpec, + EmptyBlock, EthSpec, ForkName, FullPayload, KzgProofs, MainnetEthSpec, SignedBeaconBlock, }; type E = MainnetEthSpec; @@ -389,32 +433,52 @@ mod test { // only load it once. #[test] fn test_build_data_columns_sidecars() { - let spec = E::default_spec(); + let spec = ForkName::Fulu.make_genesis_spec(E::default_spec()); let kzg = get_kzg(); test_build_data_columns_empty(&kzg, &spec); test_build_data_columns(&kzg, &spec); test_reconstruct_data_columns(&kzg, &spec); test_reconstruct_blobs_from_data_columns(&kzg, &spec); + test_verify_blob_and_cell_proofs(&kzg); + } + + #[track_caller] + fn test_verify_blob_and_cell_proofs(kzg: &Kzg) { + let (blobs_bundle, _) = generate_blobs::(3, ForkName::Fulu).unwrap(); + let BlobsBundle { + blobs, + commitments, + proofs, + } = blobs_bundle; + + let result = + validate_blobs_and_cell_proofs::(kzg, blobs.iter().collect(), &proofs, &commitments); + + assert!(result.is_ok()); } #[track_caller] fn test_build_data_columns_empty(kzg: &Kzg, spec: &ChainSpec) { let num_of_blobs = 0; - let (signed_block, blobs) = create_test_block_and_blobs::(num_of_blobs, spec); + let (signed_block, blobs, proofs) = + create_test_fulu_block_and_blobs::(num_of_blobs, spec); let blob_refs = blobs.iter().collect::>(); let column_sidecars = - blobs_to_data_column_sidecars(&blob_refs, &signed_block, kzg, spec).unwrap(); + blobs_to_data_column_sidecars(&blob_refs, proofs.to_vec(), &signed_block, kzg, spec) + .unwrap(); assert!(column_sidecars.is_empty()); } #[track_caller] fn test_build_data_columns(kzg: &Kzg, spec: &ChainSpec) { let num_of_blobs = 6; - let (signed_block, blobs) = create_test_block_and_blobs::(num_of_blobs, spec); + let (signed_block, blobs, proofs) = + create_test_fulu_block_and_blobs::(num_of_blobs, spec); let blob_refs = blobs.iter().collect::>(); let column_sidecars = - blobs_to_data_column_sidecars(&blob_refs, &signed_block, kzg, spec).unwrap(); + blobs_to_data_column_sidecars(&blob_refs, proofs.to_vec(), &signed_block, kzg, spec) + .unwrap(); let block_kzg_commitments = signed_block .message() @@ -448,10 +512,12 @@ mod test { #[track_caller] fn test_reconstruct_data_columns(kzg: &Kzg, spec: &ChainSpec) { let num_of_blobs = 6; - let (signed_block, blobs) = create_test_block_and_blobs::(num_of_blobs, spec); + let (signed_block, blobs, proofs) = + create_test_fulu_block_and_blobs::(num_of_blobs, spec); let blob_refs = blobs.iter().collect::>(); let column_sidecars = - blobs_to_data_column_sidecars(&blob_refs, &signed_block, kzg, spec).unwrap(); + blobs_to_data_column_sidecars(&blob_refs, proofs.to_vec(), &signed_block, kzg, spec) + .unwrap(); // Now reconstruct let reconstructed_columns = reconstruct_data_columns( @@ -469,10 +535,12 @@ mod test { #[track_caller] fn test_reconstruct_blobs_from_data_columns(kzg: &Kzg, spec: &ChainSpec) { let num_of_blobs = 6; - let (signed_block, blobs) = create_test_block_and_blobs::(num_of_blobs, spec); + let (signed_block, blobs, proofs) = + create_test_fulu_block_and_blobs::(num_of_blobs, spec); let blob_refs = blobs.iter().collect::>(); let column_sidecars = - blobs_to_data_column_sidecars(&blob_refs, &signed_block, kzg, spec).unwrap(); + blobs_to_data_column_sidecars(&blob_refs, proofs.to_vec(), &signed_block, kzg, spec) + .unwrap(); // Now reconstruct let signed_blinded_block = signed_block.into(); @@ -504,11 +572,15 @@ mod test { Kzg::new_from_trusted_setup_das_enabled(trusted_setup).expect("should create kzg") } - fn create_test_block_and_blobs( + fn create_test_fulu_block_and_blobs( num_of_blobs: usize, spec: &ChainSpec, - ) -> (SignedBeaconBlock, BlobsList) { - let mut block = BeaconBlock::Deneb(BeaconBlockDeneb::empty(spec)); + ) -> ( + SignedBeaconBlock>, + BlobsList, + KzgProofs, + ) { + let mut block = BeaconBlock::Fulu(BeaconBlockFulu::empty(spec)); let mut body = block.body_mut(); let blob_kzg_commitments = body.blob_kzg_commitments_mut().unwrap(); *blob_kzg_commitments = @@ -516,12 +588,12 @@ mod test { .unwrap(); let mut signed_block = SignedBeaconBlock::from_block(block, Signature::empty()); - - let (blobs_bundle, _) = generate_blobs::(num_of_blobs).unwrap(); + let fork = signed_block.fork_name_unchecked(); + let (blobs_bundle, _) = generate_blobs::(num_of_blobs, fork).unwrap(); let BlobsBundle { blobs, commitments, - proofs: _, + proofs, } = blobs_bundle; *signed_block @@ -530,6 +602,6 @@ mod test { .blob_kzg_commitments_mut() .unwrap() = commitments; - (signed_block, blobs) + (signed_block, blobs, proofs) } } diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 1e87677634..fb47996100 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -34,7 +34,6 @@ pub mod fork_choice_signal; pub mod fork_revert; pub mod fulu_readiness; pub mod graffiti_calculator; -mod head_tracker; pub mod historical_blocks; pub mod inclusion_list_verification; pub mod kzg_utils; @@ -58,6 +57,7 @@ pub mod schema_change; pub mod shuffling_cache; pub mod single_attestation; pub mod state_advance_timer; +pub mod summaries_dag; pub mod sync_committee_rewards; pub mod sync_committee_verification; pub mod test_utils; 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 8e29be9732..b7b6d1df18 100644 --- a/beacon_node/beacon_chain/src/light_client_server_cache.rs +++ b/beacon_node/beacon_chain/src/light_client_server_cache.rs @@ -421,18 +421,13 @@ struct LightClientCachedData { impl LightClientCachedData { fn from_state(state: &mut BeaconState) -> Result { - let (finality_branch, next_sync_committee_branch, current_sync_committee_branch) = ( - state.compute_finalized_root_proof()?, - state.compute_current_sync_committee_proof()?, - state.compute_next_sync_committee_proof()?, - ); Ok(Self { finalized_checkpoint: state.finalized_checkpoint(), - finality_branch, + finality_branch: state.compute_finalized_root_proof()?, next_sync_committee: state.next_sync_committee()?.clone(), current_sync_committee: state.current_sync_committee()?.clone(), - next_sync_committee_branch, - current_sync_committee_branch, + next_sync_committee_branch: state.compute_next_sync_committee_proof()?, + current_sync_committee_branch: state.compute_current_sync_committee_proof()?, finalized_block_root: state.finalized_checkpoint().root, }) } diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index 53915db4be..b030cd809c 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -625,12 +625,6 @@ pub static BALANCES_CACHE_MISSES: LazyLock> = LazyLock::new(| /* * Persisting BeaconChain components to disk */ -pub static PERSIST_HEAD: LazyLock> = LazyLock::new(|| { - try_create_histogram( - "beacon_persist_head", - "Time taken to persist the canonical head", - ) -}); pub static PERSIST_OP_POOL: LazyLock> = LazyLock::new(|| { try_create_histogram( "beacon_persist_op_pool", @@ -1693,7 +1687,7 @@ pub static BLOBS_FROM_EL_HIT_TOTAL: LazyLock> = LazyLock::new pub static BLOBS_FROM_EL_MISS_TOTAL: LazyLock> = LazyLock::new(|| { try_create_int_counter( "beacon_blobs_from_el_miss_total", - "Number of empty blob responses from the execution layer", + "Number of empty or incomplete blob responses from the execution layer", ) }); diff --git a/beacon_node/beacon_chain/src/migrate.rs b/beacon_node/beacon_chain/src/migrate.rs index cda5b34103..03c468a35e 100644 --- a/beacon_node/beacon_chain/src/migrate.rs +++ b/beacon_node/beacon_chain/src/migrate.rs @@ -1,22 +1,16 @@ -use crate::beacon_chain::BEACON_CHAIN_DB_KEY; use crate::errors::BeaconChainError; -use crate::head_tracker::{HeadTracker, SszHeadTracker}; -use crate::persisted_beacon_chain::{PersistedBeaconChain, DUMMY_CANONICAL_HEAD_BLOCK_ROOT}; +use crate::summaries_dag::{DAGStateSummaryV22, Error as SummariesDagError, StateSummariesDAG}; use parking_lot::Mutex; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::mem; use std::sync::{mpsc, Arc}; use std::thread; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use store::hot_cold_store::{migrate_database, HotColdDBError}; -use store::iter::RootsIterator; -use store::{Error, ItemStore, StoreItem, StoreOp}; +use store::{Error, ItemStore, StoreOp}; pub use store::{HotColdDB, MemoryStore}; use tracing::{debug, error, info, warn}; -use types::{ - BeaconState, BeaconStateError, BeaconStateHash, Checkpoint, Epoch, EthSpec, FixedBytesExtended, - Hash256, SignedBeaconBlockHash, Slot, -}; +use types::{BeaconState, BeaconStateHash, Checkpoint, Epoch, EthSpec, Hash256, Slot}; /// Compact at least this frequently, finalization permitting (7 days). const MAX_COMPACTION_PERIOD_SECONDS: u64 = 604800; @@ -42,8 +36,6 @@ pub struct BackgroundMigrator, Cold: ItemStore> prev_migration: Arc>, #[allow(clippy::type_complexity)] tx_thread: Option, thread::JoinHandle<()>)>>, - /// Genesis block root, for persisting the `PersistedBeaconChain`. - genesis_block_root: Hash256, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -89,7 +81,7 @@ pub struct PrevMigration { pub enum PruningOutcome { /// The pruning succeeded and updated the pruning checkpoint from `old_finalized_checkpoint`. Successful { - old_finalized_checkpoint: Checkpoint, + old_finalized_checkpoint_epoch: Epoch, }, /// The run was aborted because the new finalized checkpoint is older than the previous one. OutOfOrderFinalization { @@ -116,6 +108,11 @@ pub enum PruningError { }, UnexpectedEqualStateRoots, UnexpectedUnequalStateRoots, + MissingSummaryForFinalizedCheckpoint(Hash256), + MissingBlindedBlock(Hash256), + SummariesDagError(&'static str, SummariesDagError), + EmptyFinalizedStates, + EmptyFinalizedBlocks, } /// Message sent to the migration thread containing the information it needs to run. @@ -130,25 +127,17 @@ pub enum Notification { pub struct ManualFinalizationNotification { pub state_root: BeaconStateHash, pub checkpoint: Checkpoint, - pub head_tracker: Arc, - pub genesis_block_root: Hash256, } pub struct FinalizationNotification { pub finalized_state_root: BeaconStateHash, pub finalized_checkpoint: Checkpoint, - pub head_tracker: Arc, pub prev_migration: Arc>, - pub genesis_block_root: Hash256, } impl, Cold: ItemStore> BackgroundMigrator { /// Create a new `BackgroundMigrator` and spawn its thread if necessary. - pub fn new( - db: Arc>, - config: MigratorConfig, - genesis_block_root: Hash256, - ) -> Self { + pub fn new(db: Arc>, config: MigratorConfig) -> Self { // Estimate last migration run from DB split slot. let prev_migration = Arc::new(Mutex::new(PrevMigration { epoch: db.get_split_slot().epoch(E::slots_per_epoch()), @@ -163,7 +152,6 @@ impl, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator, ) -> Result<(), BeaconChainError> { let notif = FinalizationNotification { finalized_state_root, finalized_checkpoint, - head_tracker, prev_migration: self.prev_migration.clone(), - genesis_block_root: self.genesis_block_root, }; // Send to background thread if configured, otherwise run in foreground. @@ -314,9 +299,7 @@ impl, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator {} + Err(Error::HotColdDBError(HotColdDBError::FreezeSlotUnaligned(slot))) => { + debug!( + slot = slot.as_u64(), + "Database migration postponed, unaligned finalized block" + ); + } + Err(e) => { + warn!(error = ?e, "Database migration failed"); + return; + } + }; + + let old_finalized_checkpoint_epoch = match Self::prune_hot_db( + db.clone(), + finalized_state_root.into(), &finalized_state, notif.finalized_checkpoint, - notif.genesis_block_root, ) { Ok(PruningOutcome::Successful { - old_finalized_checkpoint, - }) => old_finalized_checkpoint, + old_finalized_checkpoint_epoch, + }) => old_finalized_checkpoint_epoch, Ok(PruningOutcome::DeferredConcurrentHeadTrackerMutation) => { warn!( message = "this is expected only very rarely!", @@ -391,26 +391,10 @@ impl, Cold: ItemStore> BackgroundMigrator { - warn!(error = ?e,"Block pruning failed"); - return; - } - }; - - match migrate_database( - db.clone(), - finalized_state_root.into(), - finalized_block_root, - &finalized_state, - ) { - Ok(()) => {} - Err(Error::HotColdDBError(HotColdDBError::FreezeSlotUnaligned(slot))) => { - debug!( - slot = slot.as_u64(), - "Database migration postponed, unaligned finalized block" + warn!( + error = ?e, + "Hot DB pruning failed" ); - } - Err(e) => { - warn!(error = ?e, "Database migration failed"); return; } }; @@ -418,7 +402,7 @@ impl, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator>, - head_tracker: Arc, - new_finalized_state_hash: BeaconStateHash, + new_finalized_state_root: Hash256, new_finalized_state: &BeaconState, new_finalized_checkpoint: Checkpoint, - genesis_block_root: Hash256, ) -> Result { - let old_finalized_checkpoint = - store - .load_pruning_checkpoint()? - .unwrap_or_else(|| Checkpoint { - epoch: Epoch::new(0), - root: Hash256::zero(), - }); - - let old_finalized_slot = old_finalized_checkpoint - .epoch - .start_slot(E::slots_per_epoch()); let new_finalized_slot = new_finalized_checkpoint .epoch .start_slot(E::slots_per_epoch()); - let new_finalized_block_hash = new_finalized_checkpoint.root.into(); // The finalized state must be for the epoch boundary slot, not the slot of the finalized // block. @@ -549,200 +518,221 @@ impl, Cold: ItemStore> BackgroundMigrator new_finalized_slot { - return Ok(PruningOutcome::OutOfOrderFinalization { - old_finalized_checkpoint, - new_finalized_checkpoint, - }); - } - debug!( - old_finalized_epoch = %old_finalized_checkpoint.epoch, - new_finalized_epoch = %new_finalized_checkpoint.epoch, + new_finalized_checkpoint = ?new_finalized_checkpoint, + new_finalized_state_root = %new_finalized_state_root, "Starting database pruning" ); - // For each slot between the new finalized checkpoint and the old finalized checkpoint, - // collect the beacon block root and state root of the canonical chain. - let newly_finalized_chain: HashMap = - std::iter::once(Ok(( - new_finalized_slot, - (new_finalized_block_hash, new_finalized_state_hash), - ))) - .chain(RootsIterator::new(&store, new_finalized_state).map(|res| { - res.map(|(block_root, state_root, slot)| { - (slot, (block_root.into(), state_root.into())) + + let state_summaries_dag = { + let state_summaries = store + .load_hot_state_summaries()? + .into_iter() + .map(|(state_root, summary)| { + let block_root = summary.latest_block_root; + // This error should never happen unless we break a DB invariant + let block = store + .get_blinded_block(&block_root)? + .ok_or(PruningError::MissingBlindedBlock(block_root))?; + Ok(( + state_root, + DAGStateSummaryV22 { + slot: summary.slot, + latest_block_root: summary.latest_block_root, + block_slot: block.slot(), + block_parent_root: block.parent_root(), + }, + )) }) - })) - .take_while(|res| { - res.as_ref() - .map_or(true, |(slot, _)| *slot >= old_finalized_slot) - }) - .collect::>()?; + .collect::, BeaconChainError>>()?; + + // De-duplicate block roots to reduce block reads below + let summary_block_roots = HashSet::::from_iter( + state_summaries + .iter() + .map(|(_, summary)| summary.latest_block_root), + ); + + // Sanity check, there is at least one summary with the new finalized block root + if !summary_block_roots.contains(&new_finalized_checkpoint.root) { + return Err(BeaconChainError::PruningError( + PruningError::MissingSummaryForFinalizedCheckpoint( + new_finalized_checkpoint.root, + ), + )); + } + + StateSummariesDAG::new_from_v22(state_summaries) + .map_err(|e| PruningError::SummariesDagError("new StateSumariesDAG", e))? + }; + + // To debug faulty trees log if we unexpectedly have more than one root. These trees may not + // result in an error, as they may not be queried in the codepaths below. + let state_summaries_dag_roots = state_summaries_dag.tree_roots(); + if state_summaries_dag_roots.len() > 1 { + warn!( + state_summaries_dag_roots = ?state_summaries_dag_roots, + error = "summaries dag found more than one root", + "Notify the devs your hot DB has some inconsistency. Pruning will fix it but devs want to know about it", + ); + } + + // `new_finalized_state_root` is the *state at the slot of the finalized epoch*, + // rather than the state of the latest finalized block. These two values will only + // differ when the first slot of the finalized epoch is a skip slot. + let finalized_and_descendant_state_roots_of_finalized_checkpoint = + HashSet::::from_iter( + std::iter::once(new_finalized_state_root).chain( + state_summaries_dag + .descendants_of(&new_finalized_state_root) + .map_err(|e| PruningError::SummariesDagError("descendants of", e))?, + ), + ); + + // Collect all `latest_block_roots` of the + // finalized_and_descendant_state_roots_of_finalized_checkpoint set. Includes the finalized + // block as `new_finalized_state_root` always has a latest block root equal to the finalized + // block. + let finalized_and_descendant_block_roots_of_finalized_checkpoint = + HashSet::::from_iter( + state_summaries_dag + .blocks_of_states( + finalized_and_descendant_state_roots_of_finalized_checkpoint.iter(), + ) + // should never error, we just constructed + // finalized_and_descendant_state_roots_of_finalized_checkpoint from the + // state_summaries_dag + .map_err(|e| PruningError::SummariesDagError("blocks of descendant", e))? + .into_iter() + .map(|(block_root, _)| block_root), + ); + + // Note: ancestors_of includes the finalized state root + let newly_finalized_state_summaries = state_summaries_dag + .ancestors_of(new_finalized_state_root) + .map_err(|e| PruningError::SummariesDagError("ancestors of", e))?; + let newly_finalized_state_roots = newly_finalized_state_summaries + .iter() + .map(|(root, _)| *root) + .collect::>(); + let newly_finalized_states_min_slot = *newly_finalized_state_summaries + .iter() + .map(|(_, slot)| slot) + .min() + .ok_or(PruningError::EmptyFinalizedStates)?; + + // Note: ancestors_of includes the finalized block + let newly_finalized_blocks = state_summaries_dag + .blocks_of_states(newly_finalized_state_roots.iter()) + .map_err(|e| PruningError::SummariesDagError("blocks of newly finalized", e))?; // We don't know which blocks are shared among abandoned chains, so we buffer and delete // everything in one fell swoop. - let mut abandoned_blocks: HashSet = HashSet::new(); - let mut abandoned_states: HashSet<(Slot, BeaconStateHash)> = HashSet::new(); - let mut abandoned_heads: HashSet = HashSet::new(); + let mut blocks_to_prune: HashSet = HashSet::new(); + let mut states_to_prune: HashSet<(Slot, Hash256)> = HashSet::new(); - let heads = head_tracker.heads(); - debug!( - old_finalized_root = ?old_finalized_checkpoint.root, - new_finalized_root = ?new_finalized_checkpoint.root, - head_count = heads.len(), - "Extra pruning information" - ); + // Consider the following block tree where we finalize block `[0]` at the checkpoint `(f)`. + // There's a block `[3]` that descendends from the finalized block but NOT from the + // finalized checkpoint. The block tree rooted in `[3]` conflicts with finality and must be + // pruned. Therefore we collect all state summaries descendant of `(f)`. + // + // finalize epoch boundary + // | /-------[2]----- + // [0]-------|--(f)--[1]---------- + // \---[3]--|-----------------[4] + // | - for (head_hash, head_slot) in heads { - // Load head block. If it fails with a decode error, it's likely a reverted block, - // so delete it from the head tracker but leave it and its states in the database - // This is suboptimal as it wastes disk space, but it's difficult to fix. A re-sync - // can be used to reclaim the space. - let head_state_root = match store.get_blinded_block(&head_hash) { - Ok(Some(block)) => block.state_root(), - Ok(None) => { - return Err(BeaconStateError::MissingBeaconBlock(head_hash.into()).into()) + for (_, summaries) in state_summaries_dag.summaries_by_slot_ascending() { + for (state_root, summary) in summaries { + let should_prune = if finalized_and_descendant_state_roots_of_finalized_checkpoint + .contains(&state_root) + { + // This state is a viable descendant of the finalized checkpoint, so does not + // conflict with finality and can be built on or become a head + false + } else { + // Everything else, prune + true + }; + + if should_prune { + // States are migrated into the cold DB in the migrate step. All hot states + // prior to finalized can be pruned from the hot DB columns + states_to_prune.insert((summary.slot, state_root)); } - Err(Error::SszDecodeError(e)) => { - warn!( - block_root = ?head_hash, - error = ?e, - "Forgetting invalid head block" - ); - abandoned_heads.insert(head_hash); - continue; - } - Err(e) => return Err(e.into()), + } + } + + for (block_root, slot) in state_summaries_dag.iter_blocks() { + // Blocks both finalized and unfinalized are in the same DB column. We must only + // prune blocks from abandoned forks. Note that block pruning and state pruning differ. + // The blocks DB column is shared for hot and cold data, while the states have different + // columns. Thus, we only prune unviable blocks or from abandoned forks. + let should_prune = if finalized_and_descendant_block_roots_of_finalized_checkpoint + .contains(&block_root) + { + // Keep unfinalized blocks descendant of finalized checkpoint + finalized block + // itself Note that we anchor this set on the finalized checkpoint instead of the + // finalized block. A diagram above shows a relevant example. + false + } else if newly_finalized_blocks.contains(&(block_root, slot)) { + // Keep recently finalized blocks + false + } else if slot < newly_finalized_states_min_slot { + // Keep recently finalized blocks that we know are canonical. Blocks with slots < + // that `newly_finalized_blocks_min_slot` we don't have canonical information so we + // assume they are part of the finalized pruned chain + // + // Pruning these would risk breaking the DB by deleting canonical blocks once the + // HDiff grid advances. If the pruning routine is correct this condition should + // never be hit. + false + } else { + // Everything else, prune + true }; - let mut potentially_abandoned_head = Some(head_hash); - let mut potentially_abandoned_blocks = vec![]; - - // Iterate backwards from this head, staging blocks and states for deletion. - let iter = std::iter::once(Ok((head_hash, head_state_root, head_slot))) - .chain(RootsIterator::from_block(&store, head_hash)?); - - for maybe_tuple in iter { - let (block_root, state_root, slot) = maybe_tuple?; - let block_root = SignedBeaconBlockHash::from(block_root); - let state_root = BeaconStateHash::from(state_root); - - match newly_finalized_chain.get(&slot) { - // If there's no information about a slot on the finalized chain, then - // it should be because it's ahead of the new finalized slot. Stage - // the fork's block and state for possible deletion. - None => { - if slot > new_finalized_slot { - potentially_abandoned_blocks.push(( - slot, - Some(block_root), - Some(state_root), - )); - } else if slot >= old_finalized_slot { - return Err(PruningError::MissingInfoForCanonicalChain { slot }.into()); - } else { - // We must assume here any candidate chains include the old finalized - // checkpoint, i.e. there aren't any forks starting at a block that is a - // strict ancestor of old_finalized_checkpoint. - warn!( - head_block_root = ?head_hash, - %head_slot, - "Found a chain that should already have been pruned" - ); - potentially_abandoned_head.take(); - break; - } - } - Some((finalized_block_root, finalized_state_root)) => { - // This fork descends from a newly finalized block, we can stop. - if block_root == *finalized_block_root { - // Sanity check: if the slot and block root match, then the - // state roots should match too. - if state_root != *finalized_state_root { - return Err(PruningError::UnexpectedUnequalStateRoots.into()); - } - - // If the fork descends from the whole finalized chain, - // do not prune it. Otherwise continue to delete all - // of the blocks and states that have been staged for - // deletion so far. - if slot == new_finalized_slot { - potentially_abandoned_blocks.clear(); - potentially_abandoned_head.take(); - } - // If there are skipped slots on the fork to be pruned, then - // we will have just staged the common block for deletion. - // Unstage it. - else { - for (_, block_root, _) in - potentially_abandoned_blocks.iter_mut().rev() - { - if block_root.as_ref() == Some(finalized_block_root) { - *block_root = None; - } else { - break; - } - } - } - break; - } else { - if state_root == *finalized_state_root { - return Err(PruningError::UnexpectedEqualStateRoots.into()); - } - potentially_abandoned_blocks.push(( - slot, - Some(block_root), - Some(state_root), - )); - } - } - } - } - - if let Some(abandoned_head) = potentially_abandoned_head { - debug!( - head_block_root = ?abandoned_head, - %head_slot, - "Pruning head" - ); - abandoned_heads.insert(abandoned_head); - abandoned_blocks.extend( - potentially_abandoned_blocks - .iter() - .filter_map(|(_, maybe_block_hash, _)| *maybe_block_hash), - ); - abandoned_states.extend(potentially_abandoned_blocks.iter().filter_map( - |(slot, _, maybe_state_hash)| maybe_state_hash.map(|sr| (*slot, sr)), - )); + if should_prune { + blocks_to_prune.insert(block_root); } } - // Update the head tracker before the database, so that we maintain the invariant - // that a block present in the head tracker is present in the database. - // See https://github.com/sigp/lighthouse/issues/1557 - let mut head_tracker_lock = head_tracker.0.write(); + // Sort states to prune to make it more readable + let mut states_to_prune = states_to_prune.into_iter().collect::>(); + states_to_prune.sort_by_key(|(slot, _)| *slot); - // Check that all the heads to be deleted are still present. The absence of any - // head indicates a race, that will likely resolve itself, so we defer pruning until - // later. - for head_hash in &abandoned_heads { - if !head_tracker_lock.contains_key(head_hash) { - return Ok(PruningOutcome::DeferredConcurrentHeadTrackerMutation); - } + debug!( + new_finalized_checkpoint = ?new_finalized_checkpoint, + newly_finalized_blocks = newly_finalized_blocks.len(), + newly_finalized_state_roots = newly_finalized_state_roots.len(), + newly_finalized_states_min_slot = %newly_finalized_states_min_slot, + state_summaries_count = state_summaries_dag.summaries_count(), + state_summaries_dag_roots = ?state_summaries_dag_roots, + finalized_and_descendant_state_roots_of_finalized_checkpoint = finalized_and_descendant_state_roots_of_finalized_checkpoint.len(), + finalized_and_descendant_state_roots_of_finalized_checkpoint = finalized_and_descendant_state_roots_of_finalized_checkpoint.len(), + blocks_to_prune = blocks_to_prune.len(), + states_to_prune = states_to_prune.len(), + "Extra pruning information" + ); + // Don't log the full `states_to_prune` in the log statement above as it can result in a + // single log line of +1Kb and break logging setups. + for block_root in &blocks_to_prune { + debug!( + block_root = ?block_root, + "Pruning block" + ); + } + for (slot, state_root) in &states_to_prune { + debug!( + ?state_root, + %slot, + "Pruning hot state" + ); } - // Then remove them for real. - for head_hash in abandoned_heads { - head_tracker_lock.remove(&head_hash); - } - - let mut batch: Vec> = abandoned_blocks + let mut batch: Vec> = blocks_to_prune .into_iter() - .map(Into::into) - .flat_map(|block_root: Hash256| { + .flat_map(|block_root| { [ StoreOp::DeleteBlock(block_root), StoreOp::DeleteExecutionPayload(block_root), @@ -750,43 +740,87 @@ impl, Cold: ItemStore> BackgroundMigrator>, + ) { + for (block_root, slot) in finalized_blocks { + // Delete the execution payload if payload pruning is enabled. At a skipped slot we may + // delete the payload for the finalized block itself, but that's OK as we only guarantee + // that payloads are present for slots >= the split slot. + if *slot < new_finalized_slot { + hot_db_ops.push(StoreOp::DeleteExecutionPayload(*block_root)); + } + } + } + + fn prune_non_checkpoint_sync_committee_branches( + finalized_blocks_desc: &[(Hash256, Slot)], + hot_db_ops: &mut Vec>, + ) { + let mut epoch_boundary_blocks = HashSet::new(); + let mut non_checkpoint_block_roots = HashSet::new(); + + // Then, iterate states in slot ascending order, as they are stored wrt previous states. + for (block_root, slot) in finalized_blocks_desc.iter().rev() { + // At a missed slot, `state_root_iter` will return the block root + // from the previous non-missed slot. This ensures that the block root at an + // epoch boundary is always a checkpoint block root. We keep track of block roots + // at epoch boundaries by storing them in the `epoch_boundary_blocks` hash set. + // We then ensure that block roots at the epoch boundary aren't included in the + // `non_checkpoint_block_roots` hash set. + if *slot % E::slots_per_epoch() == 0 { + epoch_boundary_blocks.insert(block_root); + } else { + non_checkpoint_block_roots.insert(block_root); + } + + if epoch_boundary_blocks.contains(&block_root) { + non_checkpoint_block_roots.remove(&block_root); + } + } + + // Prune sync committee branch data for all non checkpoint block roots. + // Note that `non_checkpoint_block_roots` should only contain non checkpoint block roots + // as long as `finalized_state.slot()` is at an epoch boundary. If this were not the case + // we risk the chance of pruning a `sync_committee_branch` for a checkpoint block root. + // E.g. if `current_split_slot` = (Epoch A slot 0) and `finalized_state.slot()` = (Epoch C slot 31) + // and (Epoch D slot 0) is a skipped slot, we will have pruned a `sync_committee_branch` + // for a checkpoint block root. + non_checkpoint_block_roots + .into_iter() + .for_each(|block_root| { + hot_db_ops.push(StoreOp::DeleteSyncCommitteeBranch(*block_root)); + }); + } + /// Compact the database if it has been more than `COMPACTION_PERIOD_SECONDS` since it /// was last compacted. pub fn run_compaction( diff --git a/beacon_node/beacon_chain/src/persisted_beacon_chain.rs b/beacon_node/beacon_chain/src/persisted_beacon_chain.rs index adb68def0d..83affb0dcd 100644 --- a/beacon_node/beacon_chain/src/persisted_beacon_chain.rs +++ b/beacon_node/beacon_chain/src/persisted_beacon_chain.rs @@ -1,24 +1,11 @@ -use crate::head_tracker::SszHeadTracker; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use store::{DBColumn, Error as StoreError, StoreItem}; use types::Hash256; -/// Dummy value to use for the canonical head block root, see below. -pub const DUMMY_CANONICAL_HEAD_BLOCK_ROOT: Hash256 = Hash256::repeat_byte(0xff); - #[derive(Clone, Encode, Decode)] pub struct PersistedBeaconChain { - /// This value is ignored to resolve the issue described here: - /// - /// https://github.com/sigp/lighthouse/pull/1639 - /// - /// Its removal is tracked here: - /// - /// https://github.com/sigp/lighthouse/issues/1784 - pub _canonical_head_block_root: Hash256, pub genesis_block_root: Hash256, - pub ssz_head_tracker: SszHeadTracker, } impl StoreItem for PersistedBeaconChain { diff --git a/beacon_node/beacon_chain/src/schema_change.rs b/beacon_node/beacon_chain/src/schema_change.rs index ccfae1b182..49aa116f6c 100644 --- a/beacon_node/beacon_chain/src/schema_change.rs +++ b/beacon_node/beacon_chain/src/schema_change.rs @@ -2,6 +2,7 @@ mod migration_schema_v20; mod migration_schema_v21; mod migration_schema_v22; +mod migration_schema_v23; use crate::beacon_chain::BeaconChainTypes; use std::sync::Arc; @@ -57,6 +58,14 @@ pub fn migrate_schema( // bumped inside the upgrade_to_v22 fn migration_schema_v22::upgrade_to_v22::(db.clone(), genesis_state_root) } + (SchemaVersion(22), SchemaVersion(23)) => { + let ops = migration_schema_v23::upgrade_to_v23::(db.clone())?; + db.store_schema_version_atomically(to, ops) + } + (SchemaVersion(23), SchemaVersion(22)) => { + let ops = migration_schema_v23::downgrade_from_v23::(db.clone())?; + db.store_schema_version_atomically(to, ops) + } // Anything else is an error. (_, _) => Err(HotColdDBError::UnsupportedSchemaVersion { target_version: to, diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs index 0b64fdbe08..a995f9d6b4 100644 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs @@ -41,7 +41,7 @@ pub fn upgrade_to_v22( db: Arc>, genesis_state_root: Option, ) -> Result<(), Error> { - info!("Upgrading from v21 to v22"); + info!("Upgrading DB schema from v21 to v22"); let old_anchor = db.get_anchor_info(); diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v23.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v23.rs new file mode 100644 index 0000000000..d0f8202679 --- /dev/null +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v23.rs @@ -0,0 +1,164 @@ +use crate::beacon_chain::BeaconChainTypes; +use crate::persisted_fork_choice::PersistedForkChoice; +use crate::schema_change::StoreError; +use crate::test_utils::{PersistedBeaconChain, BEACON_CHAIN_DB_KEY, FORK_CHOICE_DB_KEY}; +use crate::BeaconForkChoiceStore; +use fork_choice::{ForkChoice, ResetPayloadStatuses}; +use ssz::{Decode, Encode}; +use ssz_derive::{Decode, Encode}; +use std::sync::Arc; +use store::{DBColumn, Error, HotColdDB, KeyValueStore, KeyValueStoreOp, StoreItem}; +use tracing::{debug, info}; +use types::{Hash256, Slot}; + +/// Dummy value to use for the canonical head block root, see below. +pub const DUMMY_CANONICAL_HEAD_BLOCK_ROOT: Hash256 = Hash256::repeat_byte(0xff); + +pub fn upgrade_to_v23( + db: Arc>, +) -> Result, Error> { + info!("Upgrading DB schema from v22 to v23"); + + // 1) Set the head-tracker to empty + let Some(persisted_beacon_chain_v22) = + db.get_item::(&BEACON_CHAIN_DB_KEY)? + else { + return Err(Error::MigrationError( + "No persisted beacon chain found in DB. Datadir could be incorrect or DB could be corrupt".to_string() + )); + }; + + let persisted_beacon_chain = PersistedBeaconChain { + genesis_block_root: persisted_beacon_chain_v22.genesis_block_root, + }; + + let mut ops = vec![persisted_beacon_chain.as_kv_store_op(BEACON_CHAIN_DB_KEY)]; + + // 2) Wipe out all state temporary flags. While un-used in V23, if there's a rollback we could + // end-up with an inconsistent DB. + for state_root_result in db + .hot_db + .iter_column_keys::(DBColumn::BeaconStateTemporary) + { + let state_root = state_root_result?; + debug!( + ?state_root, + "Deleting temporary state flag on v23 schema migration" + ); + ops.push(KeyValueStoreOp::DeleteKey( + DBColumn::BeaconStateTemporary, + state_root.as_slice().to_vec(), + )); + // Here we SHOULD delete the items for key `state_root` in columns `BeaconState` and + // `BeaconStateSummary`. However, in the event we have dangling temporary states at the time + // of the migration, the first pruning routine will prune them. They will be a tree branch / + // root not part of the finalized tree and trigger a warning log once. + // + // We believe there may be race conditions concerning temporary flags where a necessary + // canonical state is marked as temporary. In current stable, a restart with that DB will + // corrupt the DB. In the unlikely case this happens we choose to leave the states and + // allow pruning to clean them. + } + + Ok(ops) +} + +pub fn downgrade_from_v23( + db: Arc>, +) -> Result, Error> { + let Some(persisted_beacon_chain) = db.get_item::(&BEACON_CHAIN_DB_KEY)? + else { + // The `PersistedBeaconChain` must exist if fork choice exists. + return Err(Error::MigrationError( + "No persisted beacon chain found in DB. Datadir could be incorrect or DB could be corrupt".to_string(), + )); + }; + + // Recreate head-tracker from fork choice. + let Some(persisted_fork_choice) = db.get_item::(&FORK_CHOICE_DB_KEY)? + else { + // Fork choice should exist if the database exists. + return Err(Error::MigrationError( + "No fork choice found in DB".to_string(), + )); + }; + + let fc_store = + BeaconForkChoiceStore::from_persisted(persisted_fork_choice.fork_choice_store, db.clone()) + .map_err(|e| { + Error::MigrationError(format!( + "Error loading fork choise store from persisted: {e:?}" + )) + })?; + + // Doesn't matter what policy we use for invalid payloads, as our head calculation just + // considers descent from finalization. + let reset_payload_statuses = ResetPayloadStatuses::OnlyWithInvalidPayload; + let fork_choice = ForkChoice::from_persisted( + persisted_fork_choice.fork_choice, + reset_payload_statuses, + fc_store, + &db.spec, + ) + .map_err(|e| { + Error::MigrationError(format!("Error loading fork choice from persisted: {e:?}")) + })?; + + let heads = fork_choice + .proto_array() + .heads_descended_from_finalization::(); + + let head_roots = heads.iter().map(|node| node.root).collect(); + let head_slots = heads.iter().map(|node| node.slot).collect(); + + let persisted_beacon_chain_v22 = PersistedBeaconChainV22 { + _canonical_head_block_root: DUMMY_CANONICAL_HEAD_BLOCK_ROOT, + genesis_block_root: persisted_beacon_chain.genesis_block_root, + ssz_head_tracker: SszHeadTracker { + roots: head_roots, + slots: head_slots, + }, + }; + + let ops = vec![persisted_beacon_chain_v22.as_kv_store_op(BEACON_CHAIN_DB_KEY)]; + + Ok(ops) +} + +/// Helper struct that is used to encode/decode the state of the `HeadTracker` as SSZ bytes. +/// +/// This is used when persisting the state of the `BeaconChain` to disk. +#[derive(Encode, Decode, Clone)] +pub struct SszHeadTracker { + roots: Vec, + slots: Vec, +} + +#[derive(Clone, Encode, Decode)] +pub struct PersistedBeaconChainV22 { + /// This value is ignored to resolve the issue described here: + /// + /// https://github.com/sigp/lighthouse/pull/1639 + /// + /// Its removal is tracked here: + /// + /// https://github.com/sigp/lighthouse/issues/1784 + pub _canonical_head_block_root: Hash256, + pub genesis_block_root: Hash256, + /// DEPRECATED + pub ssz_head_tracker: SszHeadTracker, +} + +impl StoreItem for PersistedBeaconChainV22 { + fn db_column() -> DBColumn { + DBColumn::BeaconChain + } + + fn as_store_bytes(&self) -> Vec { + self.as_ssz_bytes() + } + + fn from_store_bytes(bytes: &[u8]) -> Result { + Self::from_ssz_bytes(bytes).map_err(Into::into) + } +} diff --git a/beacon_node/beacon_chain/src/state_advance_timer.rs b/beacon_node/beacon_chain/src/state_advance_timer.rs index f4216ef76d..f206405f67 100644 --- a/beacon_node/beacon_chain/src/state_advance_timer.rs +++ b/beacon_node/beacon_chain/src/state_advance_timer.rs @@ -23,7 +23,6 @@ use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, }; -use store::KeyValueStore; use task_executor::TaskExecutor; use tokio::time::{sleep, sleep_until, Instant}; use tracing::{debug, error, warn}; @@ -45,7 +44,7 @@ const MAX_FORK_CHOICE_DISTANCE: u64 = 256; #[derive(Debug)] enum Error { - BeaconChain(BeaconChainError), + BeaconChain(Box), // We don't use the inner value directly, but it's used in the Debug impl. HeadMissingFromSnapshotCache(#[allow(dead_code)] Hash256), BeaconState(#[allow(dead_code)] BeaconStateError), @@ -65,7 +64,7 @@ enum Error { impl From for Error { fn from(e: BeaconChainError) -> Self { - Self::BeaconChain(e) + Self::BeaconChain(e.into()) } } @@ -297,7 +296,7 @@ fn advance_head(beacon_chain: &Arc>) -> Resu // Protect against advancing a state more than a single slot. // // Advancing more than one slot without storing the intermediate state would corrupt the - // database. Future works might store temporary, intermediate states inside this function. + // database. Future works might store intermediate states inside this function. match state.slot().cmp(&state.latest_block_header().slot) { std::cmp::Ordering::Equal => (), std::cmp::Ordering::Greater => { @@ -432,20 +431,13 @@ fn advance_head(beacon_chain: &Arc>) -> Resu ); } - // Write the advanced state to the database with a temporary flag that will be deleted when - // a block is imported on top of this state. We should delete this once we bring in the DB - // changes from tree-states that allow us to prune states without temporary flags. + // Write the advanced state to the database. + // We no longer use a transaction lock here when checking whether the state exists, because + // even if we race with the deletion of this state by the finalization pruning code, the worst + // case is we end up with a finalized state stored, that will get pruned the next time pruning + // runs. let advanced_state_root = state.update_tree_hash_cache()?; - let txn_lock = beacon_chain.store.hot_db.begin_rw_transaction(); - let state_already_exists = beacon_chain - .store - .load_hot_state_summary(&advanced_state_root)? - .is_some(); - let temporary = !state_already_exists; - beacon_chain - .store - .put_state_possibly_temporary(&advanced_state_root, &state, temporary)?; - drop(txn_lock); + beacon_chain.store.put_state(&advanced_state_root, &state)?; debug!( ?head_block_root, diff --git a/beacon_node/beacon_chain/src/summaries_dag.rs b/beacon_node/beacon_chain/src/summaries_dag.rs new file mode 100644 index 0000000000..8dff2ac7be --- /dev/null +++ b/beacon_node/beacon_chain/src/summaries_dag.rs @@ -0,0 +1,440 @@ +use itertools::Itertools; +use std::{ + cmp::Ordering, + collections::{btree_map::Entry, BTreeMap, HashMap}, +}; +use types::{Hash256, Slot}; + +#[derive(Debug, Clone, Copy)] +pub struct DAGStateSummary { + pub slot: Slot, + pub latest_block_root: Hash256, + pub latest_block_slot: Slot, + pub previous_state_root: Hash256, +} + +#[derive(Debug, Clone, Copy)] +pub struct DAGStateSummaryV22 { + pub slot: Slot, + pub latest_block_root: Hash256, + pub block_slot: Slot, + pub block_parent_root: Hash256, +} + +pub struct StateSummariesDAG { + // state_root -> state_summary + state_summaries_by_state_root: HashMap, + // block_root -> state slot -> (state_root, state summary) + state_summaries_by_block_root: HashMap>, + // parent_state_root -> Vec + // cached value to prevent having to recompute in each recursive call into `descendants_of` + child_state_roots: HashMap>, +} + +#[derive(Debug)] +pub enum Error { + DuplicateStateSummary { + block_root: Hash256, + existing_state_summary: Box<(Slot, Hash256)>, + new_state_summary: (Slot, Hash256), + }, + MissingStateSummary(Hash256), + MissingStateSummaryByBlockRoot { + state_root: Hash256, + latest_block_root: Hash256, + }, + MissingChildStateRoot(Hash256), + RequestedSlotAboveSummary { + starting_state_root: Hash256, + ancestor_slot: Slot, + state_root: Hash256, + state_slot: Slot, + }, + RootUnknownPreviousStateRoot(Slot, Hash256), + RootUnknownAncestorStateRoot { + starting_state_root: Hash256, + ancestor_slot: Slot, + root_state_root: Hash256, + root_state_slot: Slot, + }, +} + +impl StateSummariesDAG { + pub fn new(state_summaries: Vec<(Hash256, DAGStateSummary)>) -> Result { + // Group them by latest block root, and sorted state slot + let mut state_summaries_by_state_root = HashMap::new(); + let mut state_summaries_by_block_root = HashMap::<_, BTreeMap<_, _>>::new(); + let mut child_state_roots = HashMap::<_, Vec<_>>::new(); + + for (state_root, summary) in state_summaries.into_iter() { + let summaries = state_summaries_by_block_root + .entry(summary.latest_block_root) + .or_default(); + + // Sanity check to ensure no duplicate summaries for the tuple (block_root, state_slot) + match summaries.entry(summary.slot) { + Entry::Vacant(entry) => { + entry.insert((state_root, summary)); + } + Entry::Occupied(existing) => { + return Err(Error::DuplicateStateSummary { + block_root: summary.latest_block_root, + existing_state_summary: (summary.slot, state_root).into(), + new_state_summary: (*existing.key(), existing.get().0), + }) + } + } + + state_summaries_by_state_root.insert(state_root, summary); + + child_state_roots + .entry(summary.previous_state_root) + .or_default() + .push(state_root); + // Add empty entry for the child state + child_state_roots.entry(state_root).or_default(); + } + + Ok(Self { + state_summaries_by_state_root, + state_summaries_by_block_root, + child_state_roots, + }) + } + + /// Computes a DAG from a sequence of state summaries, including their parent block + /// relationships. + /// + /// - Expects summaries to be contiguous per slot: there must exist a summary at every slot + /// of each tree branch + /// - Maybe include multiple disjoint trees. The root of each tree will have a ZERO parent state + /// root, which will error later when calling `previous_state_root`. + pub fn new_from_v22( + state_summaries_v22: Vec<(Hash256, DAGStateSummaryV22)>, + ) -> Result { + // Group them by latest block root, and sorted state slot + let mut state_summaries_by_block_root = HashMap::<_, BTreeMap<_, _>>::new(); + for (state_root, summary) in state_summaries_v22.iter() { + let summaries = state_summaries_by_block_root + .entry(summary.latest_block_root) + .or_default(); + + // Sanity check to ensure no duplicate summaries for the tuple (block_root, state_slot) + match summaries.entry(summary.slot) { + Entry::Vacant(entry) => { + entry.insert((state_root, summary)); + } + Entry::Occupied(existing) => { + return Err(Error::DuplicateStateSummary { + block_root: summary.latest_block_root, + existing_state_summary: (summary.slot, *state_root).into(), + new_state_summary: (*existing.key(), *existing.get().0), + }) + } + } + } + + let state_summaries = state_summaries_v22 + .iter() + .map(|(state_root, summary)| { + let previous_state_root = if summary.slot == 0 { + Hash256::ZERO + } else { + let previous_slot = summary.slot - 1; + + // Check the set of states in the same state's block root + let same_block_root_summaries = state_summaries_by_block_root + .get(&summary.latest_block_root) + // Should never error: we construct the HashMap here and must have at least + // one entry per block root + .ok_or(Error::MissingStateSummaryByBlockRoot { + state_root: *state_root, + latest_block_root: summary.latest_block_root, + })?; + if let Some((state_root, _)) = same_block_root_summaries.get(&previous_slot) { + // Skipped slot: block root at previous slot is the same as latest block root. + **state_root + } else { + // Common case: not a skipped slot. + // + // If we can't find a state summmary for the parent block and previous slot, + // then there is some amount of disjointedness in the DAG. We set the parent + // state root to 0x0 in this case, and will prune any dangling states. + let parent_block_root = summary.block_parent_root; + state_summaries_by_block_root + .get(&parent_block_root) + .and_then(|parent_block_summaries| { + parent_block_summaries.get(&previous_slot) + }) + .map_or(Hash256::ZERO, |(parent_state_root, _)| **parent_state_root) + } + }; + + Ok(( + *state_root, + DAGStateSummary { + slot: summary.slot, + latest_block_root: summary.latest_block_root, + latest_block_slot: summary.block_slot, + previous_state_root, + }, + )) + }) + .collect::, _>>()?; + + Self::new(state_summaries) + } + + // Returns all non-unique latest block roots of a given set of states + pub fn blocks_of_states<'a, I: Iterator>( + &self, + state_roots: I, + ) -> Result, Error> { + state_roots + .map(|state_root| { + let summary = self + .state_summaries_by_state_root + .get(state_root) + .ok_or(Error::MissingStateSummary(*state_root))?; + Ok((summary.latest_block_root, summary.latest_block_slot)) + }) + .collect() + } + + // Returns all unique latest blocks of this DAG's summaries + pub fn iter_blocks(&self) -> impl Iterator + '_ { + self.state_summaries_by_state_root + .values() + .map(|summary| (summary.latest_block_root, summary.latest_block_slot)) + .unique() + } + + /// Returns a vec of state summaries that have an unknown parent when forming the DAG tree + pub fn tree_roots(&self) -> Vec<(Hash256, DAGStateSummary)> { + self.state_summaries_by_state_root + .iter() + .filter_map(|(state_root, summary)| { + if self + .state_summaries_by_state_root + .contains_key(&summary.previous_state_root) + { + // Summaries with a known parent are not roots + None + } else { + Some((*state_root, *summary)) + } + }) + .collect() + } + + pub fn summaries_count(&self) -> usize { + self.state_summaries_by_block_root + .values() + .map(|s| s.len()) + .sum() + } + + pub fn summaries_by_slot_ascending(&self) -> BTreeMap> { + let mut summaries = BTreeMap::>::new(); + for (state_root, summary) in self.state_summaries_by_state_root.iter() { + summaries + .entry(summary.slot) + .or_default() + .push((*state_root, *summary)); + } + summaries + } + + pub fn previous_state_root(&self, state_root: Hash256) -> Result { + let summary = self + .state_summaries_by_state_root + .get(&state_root) + .ok_or(Error::MissingStateSummary(state_root))?; + if summary.previous_state_root == Hash256::ZERO { + Err(Error::RootUnknownPreviousStateRoot( + summary.slot, + state_root, + )) + } else { + Ok(summary.previous_state_root) + } + } + + pub fn ancestor_state_root_at_slot( + &self, + starting_state_root: Hash256, + ancestor_slot: Slot, + ) -> Result { + let mut state_root = starting_state_root; + // Walk backwards until we reach the state at `ancestor_slot`. + loop { + let summary = self + .state_summaries_by_state_root + .get(&state_root) + .ok_or(Error::MissingStateSummary(state_root))?; + + // Assumes all summaries are contiguous + match summary.slot.cmp(&ancestor_slot) { + Ordering::Less => { + return Err(Error::RequestedSlotAboveSummary { + starting_state_root, + ancestor_slot, + state_root, + state_slot: summary.slot, + }) + } + Ordering::Equal => { + return Ok(state_root); + } + Ordering::Greater => { + if summary.previous_state_root == Hash256::ZERO { + return Err(Error::RootUnknownAncestorStateRoot { + starting_state_root, + ancestor_slot, + root_state_root: state_root, + root_state_slot: summary.slot, + }); + } else { + state_root = summary.previous_state_root; + } + } + } + } + } + + /// Returns all ancestors of `state_root` INCLUDING `state_root` until the next parent is not + /// known. + pub fn ancestors_of(&self, mut state_root: Hash256) -> Result, Error> { + // Sanity check that the first summary exists + if !self.state_summaries_by_state_root.contains_key(&state_root) { + return Err(Error::MissingStateSummary(state_root)); + } + + let mut ancestors = vec![]; + loop { + if let Some(summary) = self.state_summaries_by_state_root.get(&state_root) { + ancestors.push((state_root, summary.slot)); + state_root = summary.previous_state_root + } else { + return Ok(ancestors); + } + } + } + + /// Returns of the descendant state summaries roots given an initiail state root. + pub fn descendants_of(&self, query_state_root: &Hash256) -> Result, Error> { + let mut descendants = vec![]; + for child_root in self + .child_state_roots + .get(query_state_root) + .ok_or(Error::MissingChildStateRoot(*query_state_root))? + { + descendants.push(*child_root); + descendants.extend(self.descendants_of(child_root)?); + } + Ok(descendants) + } +} + +#[cfg(test)] +mod tests { + use super::{DAGStateSummaryV22, Error, StateSummariesDAG}; + use bls::FixedBytesExtended; + use types::{Hash256, Slot}; + + fn root(n: u64) -> Hash256 { + Hash256::from_low_u64_le(n) + } + + #[test] + fn new_from_v22_empty() { + StateSummariesDAG::new_from_v22(vec![]).unwrap(); + } + + fn assert_previous_state_root_is_zero(dag: &StateSummariesDAG, root: Hash256) { + assert!(matches!( + dag.previous_state_root(root).unwrap_err(), + Error::RootUnknownPreviousStateRoot { .. } + )); + } + + #[test] + fn new_from_v22_one_state() { + let root_a = root(0xa); + let root_1 = root(1); + let root_2 = root(2); + let summary_1 = DAGStateSummaryV22 { + slot: Slot::new(1), + latest_block_root: root_1, + block_parent_root: root_2, + block_slot: Slot::new(1), + }; + + let dag = StateSummariesDAG::new_from_v22(vec![(root_a, summary_1)]).unwrap(); + + // The parent of the root summary is ZERO + assert_previous_state_root_is_zero(&dag, root_a); + } + + #[test] + fn new_from_v22_multiple_states() { + let dag = StateSummariesDAG::new_from_v22(vec![ + ( + root(0xa), + DAGStateSummaryV22 { + slot: Slot::new(3), + latest_block_root: root(3), + block_parent_root: root(1), + block_slot: Slot::new(3), + }, + ), + ( + root(0xb), + DAGStateSummaryV22 { + slot: Slot::new(4), + latest_block_root: root(4), + block_parent_root: root(3), + block_slot: Slot::new(4), + }, + ), + // fork 1 + ( + root(0xc), + DAGStateSummaryV22 { + slot: Slot::new(5), + latest_block_root: root(5), + block_parent_root: root(4), + block_slot: Slot::new(5), + }, + ), + // fork 2 + // skipped slot + ( + root(0xd), + DAGStateSummaryV22 { + slot: Slot::new(5), + latest_block_root: root(4), + block_parent_root: root(3), + block_slot: Slot::new(4), + }, + ), + // normal slot + ( + root(0xe), + DAGStateSummaryV22 { + slot: Slot::new(6), + latest_block_root: root(6), + block_parent_root: root(4), + block_slot: Slot::new(6), + }, + ), + ]) + .unwrap(); + + // The parent of the root summary is ZERO + assert_previous_state_root_is_zero(&dag, root(0xa)); + assert_eq!(dag.previous_state_root(root(0xc)).unwrap(), root(0xb)); + assert_eq!(dag.previous_state_root(root(0xd)).unwrap(), root(0xb)); + assert_eq!(dag.previous_state_root(root(0xe)).unwrap(), root(0xd)); + } +} diff --git a/beacon_node/beacon_chain/src/sync_committee_verification.rs b/beacon_node/beacon_chain/src/sync_committee_verification.rs index e1a5de56d1..768c971f94 100644 --- a/beacon_node/beacon_chain/src/sync_committee_verification.rs +++ b/beacon_node/beacon_chain/src/sync_committee_verification.rs @@ -189,7 +189,7 @@ pub enum Error { /// /// We were unable to process this sync committee message due to an internal error. It's unclear if the /// sync committee message is valid. - BeaconChainError(BeaconChainError), + BeaconChainError(Box), /// There was an error whilst processing the sync contribution. It is not known if it is valid or invalid. /// /// ## Peer scoring @@ -232,7 +232,7 @@ pub enum Error { impl From for Error { fn from(e: BeaconChainError) -> Self { - Error::BeaconChainError(e) + Error::BeaconChainError(e.into()) } } @@ -334,7 +334,7 @@ impl VerifiedSyncContribution { .observed_sync_contributions .write() .is_known_subset(contribution, contribution_data_root) - .map_err(|e| Error::BeaconChainError(e.into()))? + .map_err(|e| Error::BeaconChainError(Box::new(e.into())))? { metrics::inc_counter(&metrics::SYNC_CONTRIBUTION_SUBSETS); return Err(Error::SyncContributionSupersetKnown(contribution_data_root)); @@ -363,7 +363,7 @@ impl VerifiedSyncContribution { if !selection_proof .is_aggregator::() - .map_err(|e| Error::BeaconChainError(e.into()))? + .map_err(|e| Error::BeaconChainError(Box::new(e.into())))? { return Err(Error::InvalidSelectionProof { aggregator_index }); } @@ -395,7 +395,7 @@ impl VerifiedSyncContribution { .observed_sync_contributions .write() .observe_item(contribution, Some(contribution_data_root)) - .map_err(|e| Error::BeaconChainError(e.into()))? + .map_err(|e| Error::BeaconChainError(Box::new(e.into())))? { metrics::inc_counter(&metrics::SYNC_CONTRIBUTION_SUBSETS); return Err(Error::SyncContributionSupersetKnown(contribution_data_root)); diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index fb44587fe9..bbf700c63b 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -38,8 +38,7 @@ use kzg::{Kzg, TrustedSetup}; use logging::create_test_tracing_subscriber; use merkle_proof::MerkleTree; use operation_pool::ReceivedPreCapella; -use parking_lot::Mutex; -use parking_lot::RwLockWriteGuard; +use parking_lot::{Mutex, RwLockWriteGuard}; use rand::rngs::StdRng; use rand::Rng; use rand::SeedableRng; @@ -592,7 +591,8 @@ where .chain_config(chain_config) .import_all_data_columns(self.import_all_data_columns) .event_handler(Some(ServerSentEventHandler::new_with_capacity(5))) - .validator_monitor_config(validator_monitor_config); + .validator_monitor_config(validator_monitor_config) + .rng(Box::new(StdRng::seed_from_u64(42))); builder = if let Some(mutator) = self.initial_mutator { mutator(builder) @@ -901,6 +901,28 @@ where state.get_block_root(slot).unwrap() == state.get_block_root(slot - 1).unwrap() } + pub fn knows_head(&self, block_hash: &SignedBeaconBlockHash) -> bool { + self.chain + .heads() + .iter() + .any(|(head, _)| *head == Hash256::from(*block_hash)) + } + + pub fn assert_knows_head(&self, head_block_root: Hash256) { + let heads = self.chain.heads(); + if !heads.iter().any(|(head, _)| *head == head_block_root) { + let fork_choice = self.chain.canonical_head.fork_choice_read_lock(); + if heads.is_empty() { + let nodes = &fork_choice.proto_array().core_proto_array().nodes; + panic!("Expected to know head block root {head_block_root:?}, but heads is empty. Nodes: {nodes:#?}"); + } else { + panic!( + "Expected to know head block root {head_block_root:?}, known heads {heads:#?}" + ); + } + } + } + pub async fn make_blinded_block( &self, state: BeaconState, @@ -2419,7 +2441,7 @@ where .blob_kzg_commitments() .is_ok_and(|c| !c.is_empty()); if !has_blobs { - return RpcBlock::new_without_blobs(Some(block_root), block); + return RpcBlock::new_without_blobs(Some(block_root), block, 0); } // Blobs are stored as data columns from Fulu (PeerDAS) @@ -2470,7 +2492,7 @@ where &self.spec, )? } else { - RpcBlock::new_without_blobs(Some(block_root), block) + RpcBlock::new_without_blobs(Some(block_root), block, 0) } } else { let blobs = blob_items @@ -3247,7 +3269,7 @@ pub fn generate_rand_block_and_blobs( NumBlobs::None => 0, }; let (bundle, transactions) = - execution_layer::test_utils::generate_blobs::(num_blobs).unwrap(); + execution_layer::test_utils::generate_blobs::(num_blobs, fork_name).unwrap(); payload.execution_payload.transactions = <_>::default(); for tx in Vec::from(transactions) { @@ -3267,7 +3289,7 @@ pub fn generate_rand_block_and_blobs( NumBlobs::None => 0, }; let (bundle, transactions) = - execution_layer::test_utils::generate_blobs::(num_blobs).unwrap(); + execution_layer::test_utils::generate_blobs::(num_blobs, fork_name).unwrap(); payload.execution_payload.transactions = <_>::default(); for tx in Vec::from(transactions) { payload.execution_payload.transactions.push(tx).unwrap(); @@ -3286,7 +3308,7 @@ pub fn generate_rand_block_and_blobs( NumBlobs::None => 0, }; let (bundle, transactions) = - execution_layer::test_utils::generate_blobs::(num_blobs).unwrap(); + execution_layer::test_utils::generate_blobs::(num_blobs, fork_name).unwrap(); payload.execution_payload.transactions = <_>::default(); for tx in Vec::from(transactions) { payload.execution_payload.transactions.push(tx).unwrap(); diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index a23aedde2e..1303978053 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -147,7 +147,7 @@ fn build_rpc_block( RpcBlock::new_with_custody_columns(None, block, columns.clone(), columns.len(), spec) .unwrap() } - None => RpcBlock::new_without_blobs(None, block), + None => RpcBlock::new_without_blobs(None, block, 0), } } @@ -370,6 +370,7 @@ async fn chain_segment_non_linear_parent_roots() { blocks[3] = RpcBlock::new_without_blobs( None, Arc::new(SignedBeaconBlock::from_block(block, signature)), + harness.sampling_column_count, ); assert!( @@ -407,6 +408,7 @@ async fn chain_segment_non_linear_slots() { blocks[3] = RpcBlock::new_without_blobs( None, Arc::new(SignedBeaconBlock::from_block(block, signature)), + harness.sampling_column_count, ); assert!( @@ -434,6 +436,7 @@ async fn chain_segment_non_linear_slots() { blocks[3] = RpcBlock::new_without_blobs( None, Arc::new(SignedBeaconBlock::from_block(block, signature)), + harness.sampling_column_count, ); assert!( @@ -575,11 +578,16 @@ async fn invalid_signature_gossip_block() { .into_block_error() .expect("should import all blocks prior to the one being tested"); let signed_block = SignedBeaconBlock::from_block(block, junk_signature()); + let rpc_block = RpcBlock::new_without_blobs( + None, + Arc::new(signed_block), + harness.sampling_column_count, + ); let process_res = harness .chain .process_block( - signed_block.canonical_root(), - Arc::new(signed_block), + rpc_block.block_root(), + rpc_block, NotifyExecutionLayer::Yes, BlockImportSource::Lookup, || Ok(()), @@ -1550,12 +1558,13 @@ async fn add_base_block_to_altair_chain() { )); // Ensure that it would be impossible to import via `BeaconChain::process_block`. + let base_rpc_block = RpcBlock::new_without_blobs(None, Arc::new(base_block.clone()), 0); assert!(matches!( harness .chain .process_block( - base_block.canonical_root(), - Arc::new(base_block.clone()), + base_rpc_block.block_root(), + base_rpc_block, NotifyExecutionLayer::Yes, BlockImportSource::Lookup, || Ok(()), @@ -1573,7 +1582,7 @@ async fn add_base_block_to_altair_chain() { harness .chain .process_chain_segment( - vec![RpcBlock::new_without_blobs(None, Arc::new(base_block))], + vec![RpcBlock::new_without_blobs(None, Arc::new(base_block), 0)], NotifyExecutionLayer::Yes, ) .await, @@ -1686,12 +1695,13 @@ async fn add_altair_block_to_base_chain() { )); // Ensure that it would be impossible to import via `BeaconChain::process_block`. + let altair_rpc_block = RpcBlock::new_without_blobs(None, Arc::new(altair_block.clone()), 0); assert!(matches!( harness .chain .process_block( - altair_block.canonical_root(), - Arc::new(altair_block.clone()), + altair_rpc_block.block_root(), + altair_rpc_block, NotifyExecutionLayer::Yes, BlockImportSource::Lookup, || Ok(()), @@ -1709,7 +1719,7 @@ async fn add_altair_block_to_base_chain() { harness .chain .process_chain_segment( - vec![RpcBlock::new_without_blobs(None, Arc::new(altair_block))], + vec![RpcBlock::new_without_blobs(None, Arc::new(altair_block), 0)], NotifyExecutionLayer::Yes ) .await, @@ -1770,11 +1780,16 @@ async fn import_duplicate_block_unrealized_justification() { // Create two verified variants of the block, representing the same block being processed in // parallel. let notify_execution_layer = NotifyExecutionLayer::Yes; - let verified_block1 = block + let rpc_block = RpcBlock::new_without_blobs( + Some(block_root), + block.clone(), + harness.sampling_column_count, + ); + let verified_block1 = rpc_block .clone() .into_execution_pending_block(block_root, chain, notify_execution_layer) .unwrap(); - let verified_block2 = block + let verified_block2 = rpc_block .into_execution_pending_block(block_root, chain, notify_execution_layer) .unwrap(); diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index ac7627b0b1..6b9ff9d6ed 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -1,5 +1,6 @@ #![cfg(not(debug_assertions))] +use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::{ canonical_head::{CachedHead, CanonicalHead}, test_utils::{BeaconChainHarness, EphemeralHarnessType}, @@ -507,13 +508,11 @@ async fn justified_checkpoint_becomes_invalid() { let is_valid = Payload::Invalid { latest_valid_hash: Some(parent_hash_of_justified), }; - rig.import_block_parametric(is_valid, is_valid, None, |error| { - matches!( - error, - // The block import should fail since the beacon chain knows the justified payload - // is invalid. - BlockError::BeaconChainError(BeaconChainError::JustifiedPayloadInvalid { .. }) - ) + rig.import_block_parametric(is_valid, is_valid, None, |error| match error { + BlockError::BeaconChainError(e) => { + matches!(e.as_ref(), BeaconChainError::JustifiedPayloadInvalid { .. }) + } + _ => false, }) .await; @@ -687,12 +686,14 @@ async fn invalidates_all_descendants() { assert_eq!(fork_parent_state.slot(), fork_parent_slot); let ((fork_block, _), _fork_post_state) = rig.harness.make_block(fork_parent_state, fork_slot).await; + let fork_rpc_block = + RpcBlock::new_without_blobs(None, fork_block.clone(), rig.harness.sampling_column_count); let fork_block_root = rig .harness .chain .process_block( - fork_block.canonical_root(), - fork_block, + fork_rpc_block.block_root(), + fork_rpc_block, NotifyExecutionLayer::Yes, BlockImportSource::Lookup, || Ok(()), @@ -788,12 +789,14 @@ async fn switches_heads() { let ((fork_block, _), _fork_post_state) = rig.harness.make_block(fork_parent_state, fork_slot).await; let fork_parent_root = fork_block.parent_root(); + let fork_rpc_block = + RpcBlock::new_without_blobs(None, fork_block.clone(), rig.harness.sampling_column_count); let fork_block_root = rig .harness .chain .process_block( - fork_block.canonical_root(), - fork_block, + fork_rpc_block.block_root(), + fork_rpc_block, NotifyExecutionLayer::Yes, BlockImportSource::Lookup, || Ok(()), @@ -1057,8 +1060,10 @@ async fn invalid_parent() { )); // Ensure the block built atop an invalid payload is invalid for import. + let rpc_block = + RpcBlock::new_without_blobs(None, block.clone(), rig.harness.sampling_column_count); assert!(matches!( - rig.harness.chain.process_block(block.canonical_root(), block.clone(), NotifyExecutionLayer::Yes, BlockImportSource::Lookup, + rig.harness.chain.process_block(rpc_block.block_root(), rpc_block, NotifyExecutionLayer::Yes, BlockImportSource::Lookup, || Ok(()), ).await, Err(BlockError::ParentExecutionPayloadInvalid { parent_root: invalid_root }) @@ -1380,11 +1385,13 @@ async fn recover_from_invalid_head_by_importing_blocks() { } = InvalidHeadSetup::new().await; // Import the fork block, it should become the head. + let fork_rpc_block = + RpcBlock::new_without_blobs(None, fork_block.clone(), rig.harness.sampling_column_count); rig.harness .chain .process_block( - fork_block.canonical_root(), - fork_block.clone(), + fork_rpc_block.block_root(), + fork_rpc_block, NotifyExecutionLayer::Yes, BlockImportSource::Lookup, || Ok(()), @@ -1419,8 +1426,8 @@ async fn recover_from_invalid_head_after_persist_and_reboot() { let slot_clock = rig.harness.chain.slot_clock.clone(); - // Forcefully persist the head and fork choice. - rig.harness.chain.persist_head_and_fork_choice().unwrap(); + // Forcefully persist fork choice. + rig.harness.chain.persist_fork_choice().unwrap(); let resumed = BeaconChainHarness::builder(MainnetEthSpec) .default_spec() diff --git a/beacon_node/beacon_chain/tests/rewards.rs b/beacon_node/beacon_chain/tests/rewards.rs index 6226ed39cb..fa2d028f22 100644 --- a/beacon_node/beacon_chain/tests/rewards.rs +++ b/beacon_node/beacon_chain/tests/rewards.rs @@ -254,6 +254,35 @@ async fn test_rewards_base_inactivity_leak_justification_epoch() { ); } +#[tokio::test] +async fn test_rewards_electra_slashings() { + let spec = ForkName::Electra.make_genesis_spec(E::default_spec()); + let harness = get_electra_harness(spec); + let state = harness.get_current_state(); + + harness.extend_slots(E::slots_per_epoch() as usize).await; + + let mut initial_balances = harness.get_current_state().balances().to_vec(); + + // add an attester slashing and calculate slashing penalties + harness.add_attester_slashing(vec![0]).unwrap(); + let slashed_balance_1 = initial_balances.get_mut(0).unwrap(); + let validator_1_effective_balance = state.get_effective_balance(0).unwrap(); + let delta_1 = validator_1_effective_balance + / harness.spec.min_slashing_penalty_quotient_for_state(&state); + *slashed_balance_1 -= delta_1; + + // add a proposer slashing and calculating slashing penalties + harness.add_proposer_slashing(1).unwrap(); + let slashed_balance_2 = initial_balances.get_mut(1).unwrap(); + let validator_2_effective_balance = state.get_effective_balance(1).unwrap(); + let delta_2 = validator_2_effective_balance + / harness.spec.min_slashing_penalty_quotient_for_state(&state); + *slashed_balance_2 -= delta_2; + + check_all_electra_rewards(&harness, initial_balances).await; +} + #[tokio::test] async fn test_rewards_base_slashings() { let spec = ForkName::Base.make_genesis_spec(E::default_spec()); @@ -693,6 +722,75 @@ async fn test_rewards_base_subset_only() { check_all_base_rewards_for_subset(&harness, initial_balances, validators_subset).await; } +async fn check_all_electra_rewards( + harness: &BeaconChainHarness>, + mut balances: Vec, +) { + let mut proposal_rewards_map = HashMap::new(); + let mut sync_committee_rewards_map = HashMap::new(); + for _ in 0..E::slots_per_epoch() { + let state = harness.get_current_state(); + let slot = state.slot() + Slot::new(1); + + // calculate beacon block rewards / penalties + let ((signed_block, _maybe_blob_sidecars), mut state) = + harness.make_block_return_pre_state(state, slot).await; + let beacon_block_reward = harness + .chain + .compute_beacon_block_reward(signed_block.message(), &mut state) + .unwrap(); + + let total_proposer_reward = proposal_rewards_map + .entry(beacon_block_reward.proposer_index) + .or_insert(0); + *total_proposer_reward += beacon_block_reward.total as i64; + + // calculate sync committee rewards / penalties + let reward_payload = harness + .chain + .compute_sync_committee_rewards(signed_block.message(), &mut state) + .unwrap(); + + for reward in reward_payload { + let total_sync_reward = sync_committee_rewards_map + .entry(reward.validator_index) + .or_insert(0); + *total_sync_reward += reward.reward; + } + + harness.extend_slots(1).await; + } + + // compute reward deltas for all validators in epoch 0 + let StandardAttestationRewards { + ideal_rewards, + total_rewards, + } = harness + .chain + .compute_attestation_rewards(Epoch::new(0), vec![]) + .unwrap(); + + // assert ideal rewards are greater than 0 + assert_eq!( + ideal_rewards.len() as u64, + harness.spec.max_effective_balance_electra / harness.spec.effective_balance_increment + ); + + assert!(ideal_rewards + .iter() + .all(|reward| reward.head > 0 && reward.target > 0 && reward.source > 0)); + + // apply attestation, proposal, and sync committee rewards and penalties to initial balances + apply_attestation_rewards(&mut balances, total_rewards); + apply_other_rewards(&mut balances, &proposal_rewards_map); + apply_other_rewards(&mut balances, &sync_committee_rewards_map); + + // verify expected balances against actual balances + let actual_balances: Vec = harness.get_current_state().balances().to_vec(); + + assert_eq!(balances, actual_balances); +} + async fn check_all_base_rewards( harness: &BeaconChainHarness>, balances: Vec, diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 9cf628433f..3343dc101b 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -1,6 +1,7 @@ #![cfg(not(debug_assertions))] use beacon_chain::attestation_verification::Error as AttnError; +use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::builder::BeaconChainBuilder; use beacon_chain::data_availability_checker::AvailableBlock; use beacon_chain::schema_change::migrate_schema; @@ -16,6 +17,7 @@ use beacon_chain::{ }; use logging::create_test_tracing_subscriber; use maplit::hashset; +use rand::rngs::StdRng; use rand::Rng; use slot_clock::{SlotClock, TestingSlotClock}; use state_processing::{state_advance::complete_state_advance, BlockReplayer}; @@ -31,7 +33,6 @@ use store::{ BlobInfo, DBColumn, HotColdDB, StoreConfig, }; use tempfile::{tempdir, TempDir}; -use tokio::time::sleep; use types::test_utils::{SeedableRng, XorShiftRng}; use types::*; @@ -120,6 +121,17 @@ fn get_harness_generic( harness } +fn count_states_descendant_of_block( + store: &HotColdDB, BeaconNodeBackend>, + block_root: Hash256, +) -> usize { + let summaries = store.load_hot_state_summaries().unwrap(); + summaries + .iter() + .filter(|(_, s)| s.latest_block_root == block_root) + .count() +} + #[tokio::test] async fn light_client_bootstrap_test() { let spec = test_spec::(); @@ -166,7 +178,6 @@ async fn light_client_bootstrap_test() { LightClientBootstrap::Capella(lc_bootstrap) => lc_bootstrap.header.beacon.slot, LightClientBootstrap::Deneb(lc_bootstrap) => lc_bootstrap.header.beacon.slot, LightClientBootstrap::Electra(lc_bootstrap) => lc_bootstrap.header.beacon.slot, - LightClientBootstrap::Eip7805(lc_bootstrap) => lc_bootstrap.header.beacon.slot, LightClientBootstrap::Fulu(lc_bootstrap) => lc_bootstrap.header.beacon.slot, }; @@ -1226,7 +1237,7 @@ async fn prunes_abandoned_fork_between_two_finalized_checkpoints() { assert_eq!(rig.get_finalized_checkpoints(), hashset! {}); - assert!(rig.chain.knows_head(&stray_head)); + rig.assert_knows_head(stray_head.into()); // Trigger finalization let finalization_slots: Vec = ((canonical_chain_slot + 1) @@ -1274,7 +1285,7 @@ async fn prunes_abandoned_fork_between_two_finalized_checkpoints() { ); } - assert!(!rig.chain.knows_head(&stray_head)); + assert!(!rig.knows_head(&stray_head)); } #[tokio::test] @@ -1400,7 +1411,7 @@ async fn pruning_does_not_touch_abandoned_block_shared_with_canonical_chain() { ); } - assert!(!rig.chain.knows_head(&stray_head)); + assert!(!rig.knows_head(&stray_head)); let chain_dump = rig.chain.chain_dump().unwrap(); assert!(get_blocks(&chain_dump).contains(&shared_head)); } @@ -1493,7 +1504,7 @@ async fn pruning_does_not_touch_blocks_prior_to_finalization() { ); } - assert!(rig.chain.knows_head(&stray_head)); + rig.assert_knows_head(stray_head.into()); } #[tokio::test] @@ -1577,7 +1588,7 @@ async fn prunes_fork_growing_past_youngest_finalized_checkpoint() { // Precondition: Nothing is finalized yet assert_eq!(rig.get_finalized_checkpoints(), hashset! {},); - assert!(rig.chain.knows_head(&stray_head)); + rig.assert_knows_head(stray_head.into()); // Trigger finalization let canonical_slots: Vec = (rig.epoch_start_slot(2)..=rig.epoch_start_slot(6)) @@ -1632,7 +1643,7 @@ async fn prunes_fork_growing_past_youngest_finalized_checkpoint() { ); } - assert!(!rig.chain.knows_head(&stray_head)); + assert!(!rig.knows_head(&stray_head)); } // This is to check if state outside of normal block processing are pruned correctly. @@ -2151,64 +2162,6 @@ async fn pruning_test( check_no_blocks_exist(&harness, stray_blocks.values()); } -#[tokio::test] -async fn garbage_collect_temp_states_from_failed_block_on_startup() { - let db_path = tempdir().unwrap(); - - // Wrap these functions to ensure the variables are dropped before we try to open another - // instance of the store. - let mut store = { - let store = get_store(&db_path); - let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); - - let slots_per_epoch = E::slots_per_epoch(); - - let genesis_state = harness.get_current_state(); - let block_slot = Slot::new(2 * slots_per_epoch); - let ((signed_block, _), state) = harness.make_block(genesis_state, block_slot).await; - - let (mut block, _) = (*signed_block).clone().deconstruct(); - - // Mutate the block to make it invalid, and re-sign it. - *block.state_root_mut() = Hash256::repeat_byte(0xff); - let proposer_index = block.proposer_index() as usize; - let block = Arc::new(block.sign( - &harness.validator_keypairs[proposer_index].sk, - &state.fork(), - state.genesis_validators_root(), - &harness.spec, - )); - - // The block should be rejected, but should store a bunch of temporary states. - harness.set_current_slot(block_slot); - harness - .process_block_result((block, None)) - .await - .unwrap_err(); - - assert_eq!( - store.iter_temporary_state_roots().count(), - block_slot.as_usize() - 1 - ); - store - }; - - // Wait until all the references to the store have been dropped, this helps ensure we can - // re-open the store later. - loop { - store = if let Err(store_arc) = Arc::try_unwrap(store) { - sleep(Duration::from_millis(500)).await; - store_arc - } else { - break; - } - } - - // On startup, the store should garbage collect all the temporary states. - let store = get_store(&db_path); - assert_eq!(store.iter_temporary_state_roots().count(), 0); -} - #[tokio::test] async fn garbage_collect_temp_states_from_failed_block_on_finalization() { let db_path = tempdir().unwrap(); @@ -2223,6 +2176,7 @@ async fn garbage_collect_temp_states_from_failed_block_on_finalization() { let ((signed_block, _), state) = harness.make_block(genesis_state, block_slot).await; let (mut block, _) = (*signed_block).clone().deconstruct(); + let bad_block_parent_root = block.parent_root(); // Mutate the block to make it invalid, and re-sign it. *block.state_root_mut() = Hash256::repeat_byte(0xff); @@ -2241,9 +2195,11 @@ async fn garbage_collect_temp_states_from_failed_block_on_finalization() { .await .unwrap_err(); + // The bad block parent root is the genesis block root. There's `block_slot - 1` temporary + // states to remove + the genesis state = block_slot. assert_eq!( - store.iter_temporary_state_roots().count(), - block_slot.as_usize() - 1 + count_states_descendant_of_block(&store, bad_block_parent_root), + block_slot.as_usize(), ); // Finalize the chain without the block, which should result in pruning of all temporary states. @@ -2260,8 +2216,12 @@ async fn garbage_collect_temp_states_from_failed_block_on_finalization() { // Check that the finalization migration ran. assert_ne!(store.get_split_slot(), 0); - // Check that temporary states have been pruned. - assert_eq!(store.iter_temporary_state_roots().count(), 0); + // Check that temporary states have been pruned. The genesis block is not a descendant of the + // latest finalized checkpoint, so all its states have been pruned from the hot DB, = 0. + assert_eq!( + count_states_descendant_of_block(&store, bad_block_parent_root), + 0 + ); } #[tokio::test] @@ -2415,6 +2375,7 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { .chain_config(ChainConfig::default()) .event_handler(Some(ServerSentEventHandler::new_with_capacity(1))) .execution_layer(Some(mock.el)) + .rng(Box::new(StdRng::seed_from_u64(42))) .build() .expect("should build"); @@ -2683,12 +2644,17 @@ async fn process_blocks_and_attestations_for_unaligned_checkpoint() { assert_eq!(split.block_root, valid_fork_block.parent_root()); assert_ne!(split.state_root, unadvanced_split_state_root); + let invalid_fork_rpc_block = RpcBlock::new_without_blobs( + None, + invalid_fork_block.clone(), + harness.sampling_column_count, + ); // Applying the invalid block should fail. let err = harness .chain .process_block( - invalid_fork_block.canonical_root(), - invalid_fork_block.clone(), + invalid_fork_rpc_block.block_root(), + invalid_fork_rpc_block, NotifyExecutionLayer::Yes, BlockImportSource::Lookup, || Ok(()), @@ -2698,11 +2664,16 @@ async fn process_blocks_and_attestations_for_unaligned_checkpoint() { assert!(matches!(err, BlockError::WouldRevertFinalizedSlot { .. })); // Applying the valid block should succeed, but it should not become head. + let valid_fork_rpc_block = RpcBlock::new_without_blobs( + None, + valid_fork_block.clone(), + harness.sampling_column_count, + ); harness .chain .process_block( - valid_fork_block.canonical_root(), - valid_fork_block.clone(), + valid_fork_rpc_block.block_root(), + valid_fork_rpc_block, NotifyExecutionLayer::Yes, BlockImportSource::Lookup, || Ok(()), @@ -2786,8 +2757,8 @@ async fn finalizes_after_resuming_from_db() { harness .chain - .persist_head_and_fork_choice() - .expect("should persist the head and fork choice"); + .persist_fork_choice() + .expect("should persist fork choice"); harness .chain .persist_op_pool() @@ -3000,11 +2971,13 @@ async fn revert_minority_fork_on_resume() { resumed_harness.chain.recompute_head_at_current_slot().await; assert_eq!(resumed_harness.head_slot(), fork_slot - 1); - // Head track should know the canonical head and the rogue head. - assert_eq!(resumed_harness.chain.heads().len(), 2); - assert!(resumed_harness - .chain - .knows_head(&resumed_harness.head_block_root().into())); + // Fork choice should only know the canonical head. When we reverted the head we also should + // have called `reset_fork_choice_to_finalization` which rebuilds fork choice from scratch + // without the reverted block. + assert_eq!( + resumed_harness.chain.heads(), + vec![(resumed_harness.head_block_root(), fork_slot - 1)] + ); // Apply blocks from the majority chain and trigger finalization. let initial_split_slot = resumed_harness.chain.store.get_split_slot(); diff --git a/beacon_node/beacon_processor/src/work_reprocessing_queue.rs b/beacon_node/beacon_processor/src/work_reprocessing_queue.rs index a4f539aea0..2b6e72ae0c 100644 --- a/beacon_node/beacon_processor/src/work_reprocessing_queue.rs +++ b/beacon_node/beacon_processor/src/work_reprocessing_queue.rs @@ -452,7 +452,7 @@ impl ReprocessQueue { if self.early_block_debounce.elapsed() { warn!( queue_size = MAXIMUM_QUEUED_BLOCKS, - msg = "check system clock", + msg = "system resources may be saturated", "Early blocks queue is full" ); } @@ -500,7 +500,7 @@ impl ReprocessQueue { if self.rpc_block_debounce.elapsed() { warn!( queue_size = MAXIMUM_QUEUED_BLOCKS, - msg = "check system clock", + msg = "system resources may be saturated", "RPC blocks queue is full" ); } @@ -540,7 +540,7 @@ impl ReprocessQueue { if self.attestation_delay_debounce.elapsed() { error!( queue_size = MAXIMUM_QUEUED_ATTESTATIONS, - msg = "check system clock", + msg = "system resources may be saturated", "Aggregate attestation delay queue is full" ); } @@ -572,7 +572,7 @@ impl ReprocessQueue { if self.attestation_delay_debounce.elapsed() { error!( queue_size = MAXIMUM_QUEUED_ATTESTATIONS, - msg = "check system clock", + msg = "system resources may be saturated", "Attestation delay queue is full" ); } @@ -606,7 +606,7 @@ impl ReprocessQueue { if self.lc_update_delay_debounce.elapsed() { error!( queue_size = MAXIMUM_QUEUED_LIGHT_CLIENT_UPDATES, - msg = "check system clock", + msg = "system resources may be saturated", "Light client updates delay queue is full" ); } diff --git a/beacon_node/builder_client/src/lib.rs b/beacon_node/builder_client/src/lib.rs index 6d82542cef..d193eaf1d8 100644 --- a/beacon_node/builder_client/src/lib.rs +++ b/beacon_node/builder_client/src/lib.rs @@ -1,7 +1,7 @@ +use eth2::types::beacon_response::EmptyMetadata; use eth2::types::builder_bid::SignedBuilderBid; -use eth2::types::fork_versioned_response::EmptyMetadata; use eth2::types::{ - ContentType, EthSpec, ExecutionBlockHash, ForkName, ForkVersionDecode, ForkVersionDeserialize, + ContentType, ContextDeserialize, EthSpec, ExecutionBlockHash, ForkName, ForkVersionDecode, ForkVersionedResponse, PublicKeyBytes, SignedValidatorRegistrationData, Slot, }; use eth2::types::{FullPayloadContents, SignedBlindedBeaconBlock}; @@ -119,7 +119,7 @@ impl BuilderHttpClient { } async fn get_with_header< - T: DeserializeOwned + ForkVersionDecode + ForkVersionDeserialize, + T: DeserializeOwned + ForkVersionDecode + for<'de> ContextDeserialize<'de, ForkName>, U: IntoUrl, >( &self, @@ -147,7 +147,7 @@ impl BuilderHttpClient { self.ssz_available.store(true, Ordering::SeqCst); T::from_ssz_bytes_by_fork(&response_bytes, fork_name) .map(|data| ForkVersionedResponse { - version: Some(fork_name), + version: fork_name, metadata: EmptyMetadata {}, data, }) @@ -155,7 +155,15 @@ impl BuilderHttpClient { } ContentType::Json => { self.ssz_available.store(false, Ordering::SeqCst); - serde_json::from_slice(&response_bytes).map_err(Error::InvalidJson) + let mut de = serde_json::Deserializer::from_slice(&response_bytes); + let data = + T::context_deserialize(&mut de, fork_name).map_err(Error::InvalidJson)?; + + Ok(ForkVersionedResponse { + version: fork_name, + metadata: EmptyMetadata {}, + data, + }) } } } diff --git a/beacon_node/client/Cargo.toml b/beacon_node/client/Cargo.toml index e11fc23072..195c53c4a0 100644 --- a/beacon_node/client/Cargo.toml +++ b/beacon_node/client/Cargo.toml @@ -31,6 +31,7 @@ logging = { workspace = true } metrics = { workspace = true } monitoring_api = { workspace = true } network = { workspace = true } +rand = { workspace = true } sensitive_url = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index c8ff6521c8..a581d5c128 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -33,6 +33,8 @@ use genesis::{interop_genesis_state, Eth1GenesisService, DEFAULT_ETH1_BLOCK_HASH use lighthouse_network::{prometheus_client::registry::Registry, NetworkGlobals}; use monitoring_api::{MonitoringHttpClient, ProcessType}; use network::{NetworkConfig, NetworkSenders, NetworkService}; +use rand::rngs::{OsRng, StdRng}; +use rand::SeedableRng; use slasher::Slasher; use slasher_service::SlasherService; use std::net::TcpListener; @@ -210,7 +212,10 @@ where .event_handler(event_handler) .execution_layer(execution_layer) .import_all_data_columns(config.network.subscribe_all_data_column_subnets) - .validator_monitor_config(config.validator_monitor.clone()); + .validator_monitor_config(config.validator_monitor.clone()) + .rng(Box::new( + StdRng::from_rng(OsRng).map_err(|e| format!("Failed to create RNG: {:?}", e))?, + )); let builder = if let Some(slasher) = self.slasher.clone() { builder.slasher(slasher) @@ -305,8 +310,10 @@ where .map_err(|e| format!("Unable to read system time: {e:}"))? .as_secs(); let genesis_time = genesis_state.genesis_time(); - let deneb_time = - genesis_time + (deneb_fork_epoch.as_u64() * spec.seconds_per_slot); + let deneb_time = genesis_time + + (deneb_fork_epoch.as_u64() + * E::slots_per_epoch() + * spec.seconds_per_slot); // Shrink the blob availability window so users don't start // a sync right before blobs start to disappear from the P2P @@ -456,12 +463,12 @@ where let blobs = if block.message().body().has_blobs() { debug!("Downloading finalized blobs"); if let Some(response) = remote - .get_blobs::(BlockId::Root(block_root), None) + .get_blobs::(BlockId::Root(block_root), None, &spec) .await .map_err(|e| format!("Error fetching finalized blobs from remote: {e:?}"))? { debug!("Downloaded finalized blobs"); - Some(response.data) + Some(response.into_data()) } else { warn!( block_root = %block_root, diff --git a/beacon_node/client/src/notifier.rs b/beacon_node/client/src/notifier.rs index ddfefdb970..d2f8c9eb2e 100644 --- a/beacon_node/client/src/notifier.rs +++ b/beacon_node/client/src/notifier.rs @@ -355,7 +355,7 @@ async fn bellatrix_readiness_logging( if !beacon_chain.is_time_to_prepare_for_capella(current_slot) { error!( info = "you need an execution engine to validate blocks, see: \ - https://lighthouse-book.sigmaprime.io/merge-migration.html", + https://lighthouse-book.sigmaprime.io/archived_merge_migration.html", "Execution endpoint required" ); } @@ -435,7 +435,7 @@ async fn capella_readiness_logging( if !beacon_chain.is_time_to_prepare_for_deneb(current_slot) { error!( info = "you need a Capella enabled execution engine to validate blocks, see: \ - https://lighthouse-book.sigmaprime.io/merge-migration.html", + https://lighthouse-book.sigmaprime.io/archived_merge_migration.html", "Execution endpoint required" ); } @@ -601,8 +601,7 @@ async fn eip7805_readiness_logging( match beacon_chain.check_eip7805_readiness().await { Eip7805Readiness::Ready => { info!( - info = - "ensure the execution endpoint is updated to the latest eip7805 release", + info = "ensure the execution endpoint is updated to the latest eip7805 release", "Ready for Eip7805" ) } diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index 11f62ee328..120ed7b776 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -1,11 +1,11 @@ use crate::engines::ForkchoiceState; use crate::http::{ ENGINE_FORKCHOICE_UPDATED_V1, ENGINE_FORKCHOICE_UPDATED_V2, ENGINE_FORKCHOICE_UPDATED_V3, - ENGINE_GET_BLOBS_V1, ENGINE_GET_CLIENT_VERSION_V1, ENGINE_GET_INCLUSION_LIST_V1, - ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, - ENGINE_GET_PAYLOAD_V1, ENGINE_GET_PAYLOAD_V2, ENGINE_GET_PAYLOAD_V3, ENGINE_GET_PAYLOAD_V4, - ENGINE_GET_PAYLOAD_V5, ENGINE_NEW_PAYLOAD_V1, ENGINE_NEW_PAYLOAD_V2, ENGINE_NEW_PAYLOAD_V3, - ENGINE_NEW_PAYLOAD_V4, ENGINE_NEW_PAYLOAD_V5, + ENGINE_GET_BLOBS_V1, ENGINE_GET_BLOBS_V2, ENGINE_GET_CLIENT_VERSION_V1, + ENGINE_GET_INCLUSION_LIST_V1, ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, + ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, ENGINE_GET_PAYLOAD_V1, ENGINE_GET_PAYLOAD_V2, + ENGINE_GET_PAYLOAD_V3, ENGINE_GET_PAYLOAD_V4, ENGINE_GET_PAYLOAD_V5, ENGINE_NEW_PAYLOAD_V1, + ENGINE_NEW_PAYLOAD_V2, ENGINE_NEW_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V4, ENGINE_NEW_PAYLOAD_V5, }; use eth2::types::{ BlobsBundle, SsePayloadAttributes, SsePayloadAttributesV1, SsePayloadAttributesV2, @@ -592,6 +592,7 @@ pub struct EngineCapabilities { pub get_payload_v5: bool, pub get_client_version_v1: bool, pub get_blobs_v1: bool, + pub get_blobs_v2: bool, pub get_inclusion_list_v1: bool, } @@ -649,6 +650,9 @@ impl EngineCapabilities { if self.get_blobs_v1 { response.push(ENGINE_GET_BLOBS_V1); } + if self.get_blobs_v2 { + response.push(ENGINE_GET_BLOBS_V2); + } if self.get_inclusion_list_v1 { response.push(ENGINE_GET_INCLUSION_LIST_V1); } diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index c848940502..01ef33c4b1 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -61,6 +61,7 @@ pub const ENGINE_GET_CLIENT_VERSION_V1: &str = "engine_getClientVersionV1"; pub const ENGINE_GET_CLIENT_VERSION_TIMEOUT: Duration = Duration::from_secs(1); pub const ENGINE_GET_BLOBS_V1: &str = "engine_getBlobsV1"; +pub const ENGINE_GET_BLOBS_V2: &str = "engine_getBlobsV2"; pub const ENGINE_GET_BLOBS_TIMEOUT: Duration = Duration::from_secs(1); pub const ENGINE_GET_INCLUSION_LIST_V1: &str = "engine_getInclusionListV1"; @@ -90,6 +91,7 @@ pub static LIGHTHOUSE_CAPABILITIES: &[&str] = &[ ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, ENGINE_GET_CLIENT_VERSION_V1, ENGINE_GET_BLOBS_V1, + ENGINE_GET_BLOBS_V2, ENGINE_GET_INCLUSION_LIST_V1, ]; @@ -712,7 +714,7 @@ impl HttpJsonRpc { } } - pub async fn get_blobs( + pub async fn get_blobs_v1( &self, versioned_hashes: Vec, ) -> Result>>, Error> { @@ -726,6 +728,20 @@ impl HttpJsonRpc { .await } + pub async fn get_blobs_v2( + &self, + versioned_hashes: Vec, + ) -> Result>>, Error> { + let params = json!([versioned_hashes]); + + self.rpc_request( + ENGINE_GET_BLOBS_V2, + params, + ENGINE_GET_BLOBS_TIMEOUT * self.execution_timeout_multiplier, + ) + .await + } + pub async fn update_payload_with_inclusion_list(&self) {} pub async fn get_inclusion_list( @@ -884,7 +900,7 @@ impl HttpJsonRpc { pub async fn new_payload_v5_fulu( &self, - new_payload_request_fulu: NewPayloadRequestFulu<'_, E>, + _new_payload_request_fulu: NewPayloadRequestFulu<'_, E>, ) -> Result { unreachable!("new payload fulu"); // // TODO(focil) clean this up? @@ -1025,19 +1041,6 @@ impl HttpJsonRpc { .try_into() .map_err(Error::BadResponse) } - // TODO(fulu): remove when v5 method is ready. - ForkName::Fulu => { - let response: JsonGetPayloadResponseV5 = self - .rpc_request( - ENGINE_GET_PAYLOAD_V4, - params, - ENGINE_GET_PAYLOAD_TIMEOUT * self.execution_timeout_multiplier, - ) - .await?; - JsonGetPayloadResponse::V5(response) - .try_into() - .map_err(Error::BadResponse) - } _ => Err(Error::UnsupportedForkVariant(format!( "called get_payload_v4 with {}", fork_name @@ -1210,6 +1213,7 @@ impl HttpJsonRpc { get_payload_v5: capabilities.contains(ENGINE_GET_PAYLOAD_V5), get_client_version_v1: capabilities.contains(ENGINE_GET_CLIENT_VERSION_V1), get_blobs_v1: capabilities.contains(ENGINE_GET_BLOBS_V1), + get_blobs_v2: capabilities.contains(ENGINE_GET_BLOBS_V2), get_inclusion_list_v1: capabilities.contains(ENGINE_GET_INCLUSION_LIST_V1), }) } @@ -1397,9 +1401,8 @@ impl HttpJsonRpc { } } ForkName::Fulu => { - // TODO(fulu): switch to v5 when the EL is ready - if engine_capabilities.get_payload_v4 { - self.get_payload_v4(fork_name, payload_id).await + if engine_capabilities.get_payload_v5 { + self.get_payload_v5(fork_name, payload_id).await } else { Err(Error::RequiredMethodUnsupported("engine_getPayloadv5")) } 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 705bbf6ae2..97305ed700 100644 --- a/beacon_node/execution_layer/src/engine_api/json_structures.rs +++ b/beacon_node/execution_layer/src/engine_api/json_structures.rs @@ -718,12 +718,23 @@ impl From> for BlobsBundle { } } +#[superstruct( + variants(V1, V2), + variant_attributes( + derive(Debug, Clone, PartialEq, Serialize, Deserialize), + serde(bound = "E: EthSpec", rename_all = "camelCase") + ) +)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(bound = "E: EthSpec", rename_all = "camelCase")] -pub struct BlobAndProofV1 { +pub struct BlobAndProof { #[serde(with = "ssz_types::serde_utils::hex_fixed_vec")] pub blob: Blob, + /// KZG proof for the blob (Deneb) + #[superstruct(only(V1))] pub proof: KzgProof, + /// KZG cell proofs for the extended blob (PeerDAS) + #[superstruct(only(V2))] + pub proofs: KzgProofs, } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] diff --git a/beacon_node/execution_layer/src/engines.rs b/beacon_node/execution_layer/src/engines.rs index b9e030703d..c46a94c5af 100644 --- a/beacon_node/execution_layer/src/engines.rs +++ b/beacon_node/execution_layer/src/engines.rs @@ -26,8 +26,8 @@ const CACHED_RESPONSE_AGE_LIMIT: Duration = Duration::from_secs(900); // 15 minu /// Stores the remembered state of a engine. #[derive(Copy, Clone, PartialEq, Debug, Eq, Default)] enum EngineStateInternal { - Synced, #[default] + Synced, Offline, Syncing, AuthFailed, @@ -403,12 +403,17 @@ mod tests { async fn test_state_notifier() { let mut state = State::default(); let initial_state: EngineState = state.state.into(); - assert_eq!(initial_state, EngineState::Offline); - state.update(EngineStateInternal::Synced); + // default state is online + assert_eq!(initial_state, EngineState::Online); // a watcher that arrives after the first update. let mut watcher = state.watch(); let new_state = watcher.next().await.expect("Last state is always present"); assert_eq!(new_state, EngineState::Online); + + // update to offline + state.update(EngineStateInternal::Offline); + let new_state = watcher.next().await.expect("Last state is always present"); + assert_eq!(new_state, EngineState::Offline); } } diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index e2b3b125bf..4ad453934f 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -4,7 +4,7 @@ //! This crate only provides useful functionality for "The Merge", it does not provide any of the //! deposit-contract functionality that the `beacon_node/eth1` crate already provides. -use crate::json_structures::BlobAndProofV1; +use crate::json_structures::{BlobAndProofV1, BlobAndProofV2}; use crate::payload_cache::PayloadCache; use arc_swap::ArcSwapOption; use auth::{strip_prefix, Auth, JwtKey}; @@ -16,8 +16,8 @@ pub use engine_api::*; pub use engine_api::{http, http::deposit_methods, http::HttpJsonRpc}; use engines::{Engine, EngineError}; pub use engines::{EngineState, ForkchoiceState}; -use eth2::types::FullPayloadContents; -use eth2::types::{builder_bid::SignedBuilderBid, BlobsBundle, ForkVersionedResponse}; +use eth2::types::{builder_bid::SignedBuilderBid, ForkVersionedResponse}; +use eth2::types::{BlobsBundle, FullPayloadContents}; use ethers_core::types::Transaction as EthersTransaction; use fixed_bytes::UintExtended; use fork_choice::ForkchoiceUpdateParameters; @@ -232,6 +232,7 @@ pub enum BlockProposalContents> { /// `None` for blinded `PayloadAndBlobs`. blobs_and_proofs: Option<(BlobsList, KzgProofs)>, // TODO(electra): this should probably be a separate variant/superstruct + // See: https://github.com/sigp/lighthouse/issues/6981 requests: Option>, }, } @@ -618,13 +619,7 @@ impl ExecutionLayer { let (payload_ref, maybe_json_blobs_bundle) = payload_and_blobs; let payload = payload_ref.clone_from_ref(); - let maybe_blobs_bundle = maybe_json_blobs_bundle - .cloned() - .map(|blobs_bundle| BlobsBundle { - commitments: blobs_bundle.commitments, - proofs: blobs_bundle.proofs, - blobs: blobs_bundle.blobs, - }); + let maybe_blobs_bundle = maybe_json_blobs_bundle.cloned(); self.inner .payload_cache @@ -1870,7 +1865,7 @@ impl ExecutionLayer { } } - pub async fn get_blobs( + pub async fn get_blobs_v1( &self, query: Vec, ) -> Result>>, Error> { @@ -1878,7 +1873,24 @@ impl ExecutionLayer { if capabilities.get_blobs_v1 { self.engine() - .request(|engine| async move { engine.api.get_blobs(query).await }) + .request(|engine| async move { engine.api.get_blobs_v1(query).await }) + .await + .map_err(Box::new) + .map_err(Error::EngineError) + } else { + Err(Error::GetBlobsNotSupported) + } + } + + pub async fn get_blobs_v2( + &self, + query: Vec, + ) -> Result>>, Error> { + let capabilities = self.get_engine_capabilities(None).await?; + + if capabilities.get_blobs_v2 { + self.engine() + .request(|engine| async move { engine.api.get_blobs_v2(query).await }) .await .map_err(Box::new) .map_err(Error::EngineError) @@ -2017,7 +2029,7 @@ enum InvalidBuilderPayload { expected: Option, }, Fork { - payload: Option, + payload: ForkName, expected: ForkName, }, Signature { @@ -2050,7 +2062,7 @@ impl fmt::Display for InvalidBuilderPayload { write!(f, "payload block number was {} not {:?}", payload, expected) } InvalidBuilderPayload::Fork { payload, expected } => { - write!(f, "payload fork was {:?} not {}", payload, expected) + write!(f, "payload fork was {} not {}", payload, expected) } InvalidBuilderPayload::Signature { signature, pubkey } => write!( f, @@ -2153,7 +2165,7 @@ fn verify_builder_bid( payload: header.block_number(), expected: block_number, })) - } else if bid.version != Some(current_fork) { + } else if bid.version != current_fork { Err(Box::new(InvalidBuilderPayload::Fork { payload: bid.version, expected: current_fork, 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 d0b3c12885..e3b6279e18 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 @@ -21,12 +21,13 @@ use types::{ Blob, ChainSpec, EthSpec, ExecutionBlockHash, ExecutionPayload, ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb, ExecutionPayloadEip7805, ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadHeader, FixedBytesExtended, - ForkName, Hash256, Transaction, Transactions, Uint256, + ForkName, Hash256, KzgProofs, Transaction, Transactions, Uint256, }; use super::DEFAULT_TERMINAL_BLOCK; const TEST_BLOB_BUNDLE: &[u8] = include_bytes!("fixtures/mainnet/test_blobs_bundle.ssz"); +const TEST_BLOB_BUNDLE_V2: &[u8] = include_bytes!("fixtures/mainnet/test_blobs_bundle_v2.ssz"); pub const DEFAULT_GAS_LIMIT: u64 = 30_000_000; const GAS_USED: u64 = DEFAULT_GAS_LIMIT - 1; @@ -722,15 +723,13 @@ impl ExecutionBlockGenerator { }, }; - if execution_payload.fork_name().deneb_enabled() { + let fork_name = execution_payload.fork_name(); + if fork_name.deneb_enabled() { // get random number between 0 and Max Blobs let mut rng = self.rng.lock(); - let max_blobs = self - .spec - .max_blobs_per_block_by_fork(execution_payload.fork_name()) - as usize; + let max_blobs = self.spec.max_blobs_per_block_by_fork(fork_name) as usize; let num_blobs = rng.gen::() % (max_blobs + 1); - let (bundle, transactions) = generate_blobs(num_blobs)?; + let (bundle, transactions) = generate_blobs(num_blobs, fork_name)?; for tx in Vec::from(transactions) { execution_payload .transactions_mut() @@ -746,7 +745,8 @@ impl ExecutionBlockGenerator { } } -pub fn load_test_blobs_bundle() -> Result<(KzgCommitment, KzgProof, Blob), String> { +pub fn load_test_blobs_bundle_v1() -> Result<(KzgCommitment, KzgProof, Blob), String> +{ let BlobsBundle:: { commitments, proofs, @@ -770,32 +770,56 @@ pub fn load_test_blobs_bundle() -> Result<(KzgCommitment, KzgProof, )) } +pub fn load_test_blobs_bundle_v2( +) -> Result<(KzgCommitment, KzgProofs, Blob), String> { + let BlobsBundle:: { + commitments, + proofs, + blobs, + } = BlobsBundle::from_ssz_bytes(TEST_BLOB_BUNDLE_V2) + .map_err(|e| format!("Unable to decode ssz: {:?}", e))?; + + Ok(( + commitments + .first() + .cloned() + .ok_or("commitment missing in test bundle")?, + // there's only one blob in the test bundle, hence we take all the cell proofs here. + proofs, + blobs + .first() + .cloned() + .ok_or("blob missing in test bundle")?, + )) +} + pub fn generate_blobs( n_blobs: usize, + fork_name: ForkName, ) -> Result<(BlobsBundle, Transactions), String> { - let (kzg_commitment, kzg_proof, blob) = load_test_blobs_bundle::()?; + let tx = static_valid_tx::() + .map_err(|e| format!("error creating valid tx SSZ bytes: {:?}", e))?; + let transactions = vec![tx; n_blobs]; - let mut bundle = BlobsBundle::::default(); - let mut transactions = vec![]; - - for blob_index in 0..n_blobs { - let tx = static_valid_tx::() - .map_err(|e| format!("error creating valid tx SSZ bytes: {:?}", e))?; - - transactions.push(tx); - bundle - .blobs - .push(blob.clone()) - .map_err(|_| format!("blobs are full, blob index: {:?}", blob_index))?; - bundle - .commitments - .push(kzg_commitment) - .map_err(|_| format!("blobs are full, blob index: {:?}", blob_index))?; - bundle - .proofs - .push(kzg_proof) - .map_err(|_| format!("blobs are full, blob index: {:?}", blob_index))?; - } + let bundle = if fork_name.fulu_enabled() { + let (kzg_commitment, kzg_proofs, blob) = load_test_blobs_bundle_v2::()?; + BlobsBundle { + commitments: vec![kzg_commitment; n_blobs].into(), + proofs: vec![kzg_proofs.to_vec(); n_blobs] + .into_iter() + .flatten() + .collect::>() + .into(), + blobs: vec![blob; n_blobs].into(), + } + } else { + let (kzg_commitment, kzg_proof, blob) = load_test_blobs_bundle_v1::()?; + BlobsBundle { + commitments: vec![kzg_commitment; n_blobs].into(), + proofs: vec![kzg_proof; n_blobs].into(), + blobs: vec![blob; n_blobs].into(), + } + }; Ok((bundle, transactions.into())) } @@ -936,7 +960,7 @@ pub fn generate_pow_block( #[cfg(test)] mod test { use super::*; - use kzg::{trusted_setup::get_trusted_setup, TrustedSetup}; + use kzg::{trusted_setup::get_trusted_setup, Bytes48, CellRef, KzgBlobRef, TrustedSetup}; use types::{MainnetEthSpec, MinimalEthSpec}; #[test] @@ -1006,20 +1030,28 @@ mod test { } #[test] - fn valid_test_blobs() { + fn valid_test_blobs_bundle_v1() { assert!( - validate_blob::().is_ok(), + validate_blob_bundle_v1::().is_ok(), "Mainnet preset test blobs bundle should contain valid proofs" ); assert!( - validate_blob::().is_ok(), + validate_blob_bundle_v1::().is_ok(), "Minimal preset test blobs bundle should contain valid proofs" ); } - fn validate_blob() -> Result<(), String> { + #[test] + fn valid_test_blobs_bundle_v2() { + validate_blob_bundle_v2::() + .expect("Mainnet preset test blobs bundle v2 should contain valid proofs"); + validate_blob_bundle_v2::() + .expect("Minimal preset test blobs bundle v2 should contain valid proofs"); + } + + fn validate_blob_bundle_v1() -> Result<(), String> { let kzg = load_kzg()?; - let (kzg_commitment, kzg_proof, blob) = load_test_blobs_bundle::()?; + let (kzg_commitment, kzg_proof, blob) = load_test_blobs_bundle_v1::()?; let kzg_blob = kzg::Blob::from_bytes(blob.as_ref()) .map(Box::new) .map_err(|e| format!("Error converting blob to kzg blob: {e:?}"))?; @@ -1027,6 +1059,26 @@ mod test { .map_err(|e| format!("Invalid blobs bundle: {e:?}")) } + fn validate_blob_bundle_v2() -> Result<(), String> { + let kzg = load_kzg()?; + let (kzg_commitments, kzg_proofs, cells) = + load_test_blobs_bundle_v2::().map(|(commitment, proofs, blob)| { + let kzg_blob: KzgBlobRef = blob.as_ref().try_into().unwrap(); + ( + vec![Bytes48::from(commitment); proofs.len()], + proofs.into_iter().map(|p| p.into()).collect::>(), + kzg.compute_cells(kzg_blob).unwrap(), + ) + })?; + let (cell_indices, cell_refs): (Vec, Vec) = cells + .iter() + .enumerate() + .map(|(cell_idx, cell)| (cell_idx as u64, CellRef::try_from(cell.as_ref()).unwrap())) + .unzip(); + kzg.verify_cell_proof_batch(&cell_refs, &kzg_proofs, cell_indices, &kzg_commitments) + .map_err(|e| format!("Invalid blobs bundle: {e:?}")) + } + fn load_kzg() -> Result { let trusted_setup: TrustedSetup = serde_json::from_reader(get_trusted_setup().as_slice()) diff --git a/beacon_node/execution_layer/src/test_utils/fixtures/mainnet/test_blobs_bundle_v2.ssz b/beacon_node/execution_layer/src/test_utils/fixtures/mainnet/test_blobs_bundle_v2.ssz new file mode 100644 index 0000000000..e57096c076 Binary files /dev/null and b/beacon_node/execution_layer/src/test_utils/fixtures/mainnet/test_blobs_bundle_v2.ssz differ 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 9b997afb0a..7255145c62 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -424,9 +424,8 @@ pub async fn handle_rpc( == ForkName::Fulu && (method == ENGINE_GET_PAYLOAD_V1 || method == ENGINE_GET_PAYLOAD_V2 - || method == ENGINE_GET_PAYLOAD_V3) - // TODO(fulu): Uncomment this once v5 method is ready for Fulu - // || method == ENGINE_GET_PAYLOAD_V4) + || method == ENGINE_GET_PAYLOAD_V3 + || method == ENGINE_GET_PAYLOAD_V4) { return Err(( format!("{} called after Fulu fork!", method), @@ -492,22 +491,6 @@ pub async fn handle_rpc( }) .unwrap() } - // TODO(fulu): remove this once we switch to v5 method - JsonExecutionPayload::V5(execution_payload) => { - serde_json::to_value(JsonGetPayloadResponseV5 { - execution_payload, - block_value: Uint256::from(DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI), - blobs_bundle: maybe_blobs - .ok_or(( - "No blobs returned despite V5 Payload".to_string(), - GENERIC_ERROR_CODE, - ))? - .into(), - should_override_builder: false, - execution_requests: Default::default(), - }) - .unwrap() - } _ => unreachable!(), }), ENGINE_GET_PAYLOAD_V5 => Ok(match JsonExecutionPayload::from(response) { 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 7a6298d6a9..3c62034744 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_builder.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_builder.rs @@ -574,7 +574,7 @@ impl MockBuilder { .map_err(|_| "incorrect payload variant".to_string())? .into(), blob_kzg_commitments: maybe_blobs_bundle - .map(|b| b.commitments) + .map(|b| b.commitments.clone()) .unwrap_or_default(), value: self.get_bid_value(value), pubkey: self.builder_sk.public_key().compress(), @@ -598,7 +598,7 @@ impl MockBuilder { .map_err(|_| "incorrect payload variant".to_string())? .into(), blob_kzg_commitments: maybe_blobs_bundle - .map(|b| b.commitments) + .map(|b| b.commitments.clone()) .unwrap_or_default(), value: self.get_bid_value(value), pubkey: self.builder_sk.public_key().compress(), @@ -610,7 +610,7 @@ impl MockBuilder { .map_err(|_| "incorrect payload variant".to_string())? .into(), blob_kzg_commitments: maybe_blobs_bundle - .map(|b| b.commitments) + .map(|b| b.commitments.clone()) .unwrap_or_default(), value: self.get_bid_value(value), pubkey: self.builder_sk.public_key().compress(), @@ -783,7 +783,7 @@ impl MockBuilder { .await .map_err(|_| "couldn't get head".to_string())? .ok_or_else(|| "missing head block".to_string())? - .data; + .into_data(); let head_block_root = head_block_root.unwrap_or(head.canonical_root()); @@ -801,7 +801,7 @@ impl MockBuilder { .await .map_err(|_| "couldn't get finalized block".to_string())? .ok_or_else(|| "missing finalized block".to_string())? - .data + .data() .message() .body() .execution_payload() @@ -814,7 +814,7 @@ impl MockBuilder { .await .map_err(|_| "couldn't get justified block".to_string())? .ok_or_else(|| "missing justified block".to_string())? - .data + .data() .message() .body() .execution_payload() @@ -855,7 +855,7 @@ impl MockBuilder { .await .map_err(|_| "couldn't get state".to_string())? .ok_or_else(|| "missing state".to_string())? - .data; + .into_data(); let prev_randao = head_state .get_randao_mix(head_state.current_epoch()) @@ -1022,7 +1022,7 @@ pub fn serve( .await .map_err(|e| warp::reject::custom(Custom(e)))?; let resp: ForkVersionedResponse<_> = ForkVersionedResponse { - version: Some(fork_name), + version: fork_name, metadata: Default::default(), data: payload, }; @@ -1082,7 +1082,7 @@ pub fn serve( ), eth2::types::Accept::Json | eth2::types::Accept::Any => { let resp: ForkVersionedResponse<_> = ForkVersionedResponse { - version: Some(fork_name), + version: fork_name, metadata: Default::default(), data: signed_bid, }; diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index 7978c60069..54aa7ec4ef 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -58,6 +58,7 @@ pub const DEFAULT_ENGINE_CAPABILITIES: EngineCapabilities = EngineCapabilities { get_payload_v5: true, get_client_version_v1: true, get_blobs_v1: true, + get_blobs_v2: true, get_inclusion_list_v1: true, }; diff --git a/beacon_node/http_api/Cargo.toml b/beacon_node/http_api/Cargo.toml index b13517f27e..afc68ad96d 100644 --- a/beacon_node/http_api/Cargo.toml +++ b/beacon_node/http_api/Cargo.toml @@ -28,6 +28,7 @@ metrics = { workspace = true } network = { workspace = true } operation_pool = { workspace = true } parking_lot = { workspace = true } +proto_array = { workspace = true } rand = { workspace = true } safe_arith = { workspace = true } sensitive_url = { workspace = true } diff --git a/beacon_node/http_api/src/aggregate_attestation.rs b/beacon_node/http_api/src/aggregate_attestation.rs index 23af5b0cb5..809f381139 100644 --- a/beacon_node/http_api/src/aggregate_attestation.rs +++ b/beacon_node/http_api/src/aggregate_attestation.rs @@ -4,7 +4,7 @@ use crate::version::{add_consensus_version_header, V1, V2}; use beacon_chain::{BeaconChain, BeaconChainTypes}; use eth2::types::{self, EndpointVersion, Hash256, Slot}; use std::sync::Arc; -use types::fork_versioned_response::EmptyMetadata; +use types::beacon_response::EmptyMetadata; use types::{CommitteeIndex, ForkVersionedResponse}; use warp::{ hyper::{Body, Response}, @@ -52,7 +52,7 @@ pub fn get_aggregate_attestation( if endpoint_version == V2 { let fork_versioned_response = ForkVersionedResponse { - version: Some(fork_name), + version: fork_name, metadata: EmptyMetadata {}, data: aggregate_attestation, }; diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index eb82c544e9..585686f090 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -36,7 +36,7 @@ mod validators; mod version; use crate::light_client::{get_light_client_bootstrap, get_light_client_updates}; use crate::produce_block::{produce_blinded_block_v2, produce_block_v2, produce_block_v3}; -use crate::version::fork_versioned_response; +use crate::version::beacon_response; use beacon_chain::{ attestation_verification::VerifiedAttestation, observed_operations::ObservationOutcome, validator_monitor::timestamp_now, AttestationError as AttnError, BeaconChain, BeaconChainError, @@ -49,9 +49,9 @@ use bytes::Bytes; use directory::DEFAULT_ROOT_DIR; use either::Either; use eth2::types::{ - self as api_types, BroadcastValidation, EndpointVersion, ForkChoice, ForkChoiceNode, - LightClientUpdatesQuery, PublishBlockRequest, ValidatorBalancesRequestBody, ValidatorId, - ValidatorStatus, ValidatorsRequestBody, + self as api_types, BroadcastValidation, ContextDeserialize, EndpointVersion, ForkChoice, + ForkChoiceNode, LightClientUpdatesQuery, PublishBlockRequest, ValidatorBalancesRequestBody, + ValidatorId, ValidatorStatus, ValidatorsRequestBody, }; use eth2::{CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER}; use health_metrics::observe::Observe; @@ -70,6 +70,7 @@ use serde_json::Value; use slot_clock::SlotClock; use ssz::Encode; pub use state_id::StateId; +use std::collections::HashSet; use std::future::Future; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; @@ -88,10 +89,10 @@ use tokio_stream::{ StreamExt, }; use tracing::{debug, error, info, warn}; +use types::AttestationData; use types::{ - fork_versioned_response::EmptyMetadata, Attestation, AttestationData, AttestationShufflingId, - AttesterSlashing, BeaconStateError, ChainSpec, Checkpoint, CommitteeCache, ConfigAndPreset, - Epoch, EthSpec, ForkName, ForkVersionedResponse, Hash256, ProposerPreparationData, + Attestation, AttestationShufflingId, AttesterSlashing, BeaconStateError, ChainSpec, Checkpoint, + CommitteeCache, ConfigAndPreset, Epoch, EthSpec, ForkName, Hash256, ProposerPreparationData, ProposerSlashing, RelativeEpoch, SignedAggregateAndProof, SignedBlindedBeaconBlock, SignedBlsToExecutionChange, SignedContributionAndProof, SignedInclusionList, SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, SyncCommitteeMessage, @@ -100,8 +101,8 @@ use types::{ use validator::pubkey_to_validator_index; use version::{ add_consensus_version_header, add_ssz_content_type_header, - execution_optimistic_finalized_fork_versioned_response, inconsistent_fork_rejection, - unsupported_version_rejection, V1, V2, V3, + execution_optimistic_finalized_beacon_response, inconsistent_fork_rejection, + unsupported_version_rejection, ResponseIncludesVersion, V1, V2, V3, }; use warp::http::StatusCode; use warp::hyper::Body; @@ -216,37 +217,67 @@ pub fn prometheus_metrics() -> warp::filters::log::Log warp::filters::log::Log warp::filters::log::Log { + warp::log::custom(move |info| { + let status = info.status(); + // Ensure elapsed time is in milliseconds. + let elapsed = info.elapsed().as_secs_f64() * 1000.0; + let path = info.path(); + let method = info.method().to_string(); + + if status == StatusCode::OK + || status == StatusCode::NOT_FOUND + || status == StatusCode::PARTIAL_CONTENT + { + debug!( + elapsed_ms = %elapsed, + status = %status, + path = %path, + method = %method, + "Processed HTTP API request" + ); + } else { + warn!( + elapsed_ms = %elapsed, + status = %status, + path = %path, + method = %method, + "Error processing HTTP API request" + ); + } + }) +} + /// Creates a server that will serve requests using information from `ctx`. /// /// The server will shut down gracefully when the `shutdown` future resolves. @@ -650,7 +713,7 @@ pub fn serve( .clone() .and(warp::path("validator_balances")) .and(warp::path::end()) - .and(warp_utils::json::json()) + .and(warp_utils::json::json_no_body()) .then( |state_id: StateId, task_spawner: TaskSpawner, @@ -1093,8 +1156,8 @@ pub fn serve( |state_id: StateId, task_spawner: TaskSpawner, chain: Arc>| { - task_spawner.blocking_json_task(Priority::P1, move || { - let (data, execution_optimistic, finalized) = state_id + task_spawner.blocking_response_task(Priority::P1, move || { + let (data, execution_optimistic, finalized, fork_name) = state_id .map_state_and_execution_optimistic_and_finalized( &chain, |state, execution_optimistic, finalized| { @@ -1104,15 +1167,23 @@ pub fn serve( )); }; - Ok((deposits.clone(), execution_optimistic, finalized)) + Ok(( + deposits.clone(), + execution_optimistic, + finalized, + state.fork_name_unchecked(), + )) }, )?; - Ok(api_types::ExecutionOptimisticFinalizedResponse { + execution_optimistic_finalized_beacon_response( + ResponseIncludesVersion::Yes(fork_name), + execution_optimistic, + finalized, data, - execution_optimistic: Some(execution_optimistic), - finalized: Some(finalized), - }) + ) + .map(|res| warp::reply::json(&res).into_response()) + .map(|resp| add_consensus_version_header(resp, fork_name)) }) }, ); @@ -1126,8 +1197,8 @@ pub fn serve( |state_id: StateId, task_spawner: TaskSpawner, chain: Arc>| { - task_spawner.blocking_json_task(Priority::P1, move || { - let (data, execution_optimistic, finalized) = state_id + task_spawner.blocking_response_task(Priority::P1, move || { + let (data, execution_optimistic, finalized, fork_name) = state_id .map_state_and_execution_optimistic_and_finalized( &chain, |state, execution_optimistic, finalized| { @@ -1137,7 +1208,48 @@ pub fn serve( )); }; - Ok((withdrawals.clone(), execution_optimistic, finalized)) + Ok(( + withdrawals.clone(), + execution_optimistic, + finalized, + state.fork_name_unchecked(), + )) + }, + )?; + + execution_optimistic_finalized_beacon_response( + ResponseIncludesVersion::Yes(fork_name), + execution_optimistic, + finalized, + data, + ) + .map(|res| warp::reply::json(&res).into_response()) + .map(|resp| add_consensus_version_header(resp, fork_name)) + }) + }, + ); + + // GET beacon/states/{state_id}/pending_consolidations + let get_beacon_state_pending_consolidations = beacon_states_path + .clone() + .and(warp::path("pending_consolidations")) + .and(warp::path::end()) + .then( + |state_id: StateId, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + let (data, execution_optimistic, finalized) = state_id + .map_state_and_execution_optimistic_and_finalized( + &chain, + |state, execution_optimistic, finalized| { + let Ok(consolidations) = state.pending_consolidations() else { + return Err(warp_utils::reject::custom_bad_request( + "Pending consolidations not found".to_string(), + )); + }; + + Ok((consolidations.clone(), execution_optimistic, finalized)) }, )?; @@ -1312,21 +1424,30 @@ pub fn serve( .and(warp::path("beacon")) .and(warp::path("blocks")) .and(warp::path::end()) - .and(warp_utils::json::json()) + .and(warp::body::json()) + .and(consensus_version_header_filter) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(network_tx_filter.clone()) .and(network_globals.clone()) .then( - move |block_contents: PublishBlockRequest, + move |value: serde_json::Value, + consensus_version: ForkName, task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>, network_globals: Arc>| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { + let request = PublishBlockRequest::::context_deserialize( + &value, + consensus_version, + ) + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!("invalid JSON: {e:?}")) + })?; publish_blocks::publish_block( None, - ProvenancedBlock::local_from_publish_request(block_contents), + ProvenancedBlock::local_from_publish_request(request), chain, &network_tx, BroadcastValidation::default(), @@ -1382,22 +1503,32 @@ pub fn serve( .and(warp::path("blocks")) .and(warp::query::()) .and(warp::path::end()) - .and(warp_utils::json::json()) + .and(warp::body::json()) + .and(consensus_version_header_filter) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(network_tx_filter.clone()) .and(network_globals.clone()) .then( move |validation_level: api_types::BroadcastValidationQuery, - block_contents: PublishBlockRequest, + value: serde_json::Value, + consensus_version: ForkName, task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>, network_globals: Arc>| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { + let request = PublishBlockRequest::::context_deserialize( + &value, + consensus_version, + ) + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!("invalid JSON: {e:?}")) + })?; + publish_blocks::publish_block( None, - ProvenancedBlock::local_from_publish_request(block_contents), + ProvenancedBlock::local_from_publish_request(request), chain, &network_tx, validation_level.broadcast_validation, @@ -1630,6 +1761,12 @@ pub fn serve( .fork_name(&chain.spec) .map_err(inconsistent_fork_rejection)?; + let require_version = match endpoint_version { + V1 => ResponseIncludesVersion::No, + V2 => ResponseIncludesVersion::Yes(fork_name), + _ => return Err(unsupported_version_rejection(endpoint_version)), + }; + match accept_header { Some(api_types::Accept::Ssz) => Response::builder() .status(200) @@ -1641,9 +1778,8 @@ pub fn serve( e )) }), - _ => execution_optimistic_finalized_fork_versioned_response( - endpoint_version, - fork_name, + _ => execution_optimistic_finalized_beacon_response( + require_version, execution_optimistic, finalized, block, @@ -1703,9 +1839,15 @@ pub fn serve( .attestations() .map(|att| att.clone_as_attestation()) .collect::>(); - let res = execution_optimistic_finalized_fork_versioned_response( - endpoint_version, - fork_name, + + let require_version = match endpoint_version { + V1 => ResponseIncludesVersion::No, + V2 => ResponseIncludesVersion::Yes(fork_name), + _ => return Err(unsupported_version_rejection(endpoint_version)), + }; + + let res = execution_optimistic_finalized_beacon_response( + require_version, execution_optimistic, finalized, &atts, @@ -1752,9 +1894,8 @@ pub fn serve( }), _ => { // Post as a V2 endpoint so we return the fork version. - execution_optimistic_finalized_fork_versioned_response( - V2, - fork_name, + execution_optimistic_finalized_beacon_response( + ResponseIncludesVersion::Yes(fork_name), execution_optimistic, finalized, block, @@ -1808,9 +1949,8 @@ pub fn serve( }), _ => { // Post as a V2 endpoint so we return the fork version. - let res = execution_optimistic_finalized_fork_versioned_response( - V2, - fork_name, + let res = execution_optimistic_finalized_beacon_response( + ResponseIncludesVersion::Yes(fork_name), execution_optimistic, finalized, &blob_sidecar_list_filtered, @@ -1932,11 +2072,11 @@ pub fn serve( chain: Arc>, query: api_types::AttestationPoolQuery| { task_spawner.blocking_response_task(Priority::P1, move || { - let query_filter = |data: &AttestationData| { + let query_filter = |data: &AttestationData, committee_indices: HashSet| { query.slot.is_none_or(|slot| slot == data.slot) && query .committee_index - .is_none_or(|index| index == data.index) + .is_none_or(|index| committee_indices.contains(&index)) }; let mut attestations = chain.op_pool.get_filtered_attestations(query_filter); @@ -1945,7 +2085,9 @@ pub fn serve( .naive_aggregation_pool .read() .iter() - .filter(|&att| query_filter(att.data())) + .filter(|&att| { + query_filter(att.data(), att.get_committee_indices_map()) + }) .cloned(), ); // Use the current slot to find the fork version, and convert all messages to the @@ -1968,7 +2110,13 @@ pub fn serve( }) .collect::>(); - let res = fork_versioned_response(endpoint_version, fork_name, &attestations)?; + let require_version = match endpoint_version { + V1 => ResponseIncludesVersion::No, + V2 => ResponseIncludesVersion::Yes(fork_name), + _ => return Err(unsupported_version_rejection(endpoint_version)), + }; + + let res = beacon_response(require_version, &attestations); Ok(add_consensus_version_header( warp::reply::json(&res).into_response(), fork_name, @@ -2057,7 +2205,13 @@ pub fn serve( }) .collect::>(); - let res = fork_versioned_response(endpoint_version, fork_name, &slashings)?; + let require_version = match endpoint_version { + V1 => ResponseIncludesVersion::No, + V2 => ResponseIncludesVersion::Yes(fork_name), + _ => return Err(unsupported_version_rejection(endpoint_version)), + }; + + let res = beacon_response(require_version, &slashings); Ok(add_consensus_version_header( warp::reply::json(&res).into_response(), fork_name, @@ -2521,7 +2675,7 @@ pub fn serve( let fork_name = chain .spec - .fork_name_at_slot::(*update.signature_slot()); + .fork_name_at_slot::(update.get_slot()); match accept_header { Some(api_types::Accept::Ssz) => Response::builder() .status(200) @@ -2533,11 +2687,10 @@ pub fn serve( e )) }), - _ => Ok(warp::reply::json(&ForkVersionedResponse { - version: Some(fork_name), - metadata: EmptyMetadata {}, - data: update, - }) + _ => Ok(warp::reply::json(&beacon_response( + ResponseIncludesVersion::Yes(fork_name), + update, + )) .into_response()), } .map(|resp| add_consensus_version_header(resp, fork_name)) @@ -2582,11 +2735,10 @@ pub fn serve( e )) }), - _ => Ok(warp::reply::json(&ForkVersionedResponse { - version: Some(fork_name), - metadata: EmptyMetadata {}, - data: update, - }) + _ => Ok(warp::reply::json(&beacon_response( + ResponseIncludesVersion::Yes(fork_name), + update, + )) .into_response()), } .map(|resp| add_consensus_version_header(resp, fork_name)) @@ -2778,7 +2930,7 @@ pub fn serve( .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .then( - |endpoint_version: EndpointVersion, + |_endpoint_version: EndpointVersion, state_id: StateId, accept_header: Option, task_spawner: TaskSpawner, @@ -2822,9 +2974,8 @@ pub fn serve( let fork_name = state .fork_name(&chain.spec) .map_err(inconsistent_fork_rejection)?; - let res = execution_optimistic_finalized_fork_versioned_response( - endpoint_version, - fork_name, + let res = execution_optimistic_finalized_beacon_response( + ResponseIncludesVersion::Yes(fork_name), execution_optimistic, finalized, &state, @@ -3174,13 +3325,14 @@ pub fn serve( let direction = dir.into(); let state = peer_info.connection_status().clone().into(); - let state_matches = query.state.as_ref().is_none_or(|states| { - states.iter().any(|state_param| *state_param == state) - }); - let direction_matches = - query.direction.as_ref().is_none_or(|directions| { - directions.iter().any(|dir_param| *dir_param == direction) - }); + let state_matches = query + .state + .as_ref() + .is_none_or(|states| states.contains(&state)); + let direction_matches = query + .direction + .as_ref() + .is_none_or(|directions| directions.contains(&direction)); if state_matches && direction_matches { peers.push(api_types::PeerData { @@ -3304,7 +3456,7 @@ pub fn serve( if endpoint_version == V3 { produce_block_v3(accept_header, chain, slot, query).await } else { - produce_block_v2(endpoint_version, accept_header, chain, slot, query).await + produce_block_v2(accept_header, chain, slot, query).await } }) }, @@ -3334,8 +3486,7 @@ pub fn serve( 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 + produce_blinded_block_v2(accept_header, chain, slot, query).await }) }, ); @@ -4848,6 +4999,7 @@ pub fn serve( .uor(get_beacon_state_randao) .uor(get_beacon_state_pending_deposits) .uor(get_beacon_state_pending_partial_withdrawals) + .uor(get_beacon_state_pending_consolidations) .uor(get_beacon_headers) .uor(get_beacon_headers_block_id) .uor(get_beacon_block) @@ -4960,6 +5112,7 @@ pub fn serve( ), ) .recover(warp_utils::reject::handle_rejection) + .with(tracing_logging()) .with(prometheus_metrics()) // Add a `Server` header. .map(|reply| warp::reply::with_header(reply, "Server", &version_with_platform())) diff --git a/beacon_node/http_api/src/light_client.rs b/beacon_node/http_api/src/light_client.rs index ac8c08581c..24b1338a72 100644 --- a/beacon_node/http_api/src/light_client.rs +++ b/beacon_node/http_api/src/light_client.rs @@ -1,14 +1,15 @@ use crate::version::{ - add_consensus_version_header, add_ssz_content_type_header, fork_versioned_response, V1, + add_consensus_version_header, add_ssz_content_type_header, beacon_response, + ResponseIncludesVersion, }; use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes}; use eth2::types::{ - self as api_types, ChainSpec, ForkVersionedResponse, LightClientUpdate, - LightClientUpdateResponseChunk, LightClientUpdateSszResponse, LightClientUpdatesQuery, + self as api_types, ChainSpec, LightClientUpdate, LightClientUpdateResponseChunk, + LightClientUpdateResponseChunkInner, LightClientUpdatesQuery, }; use ssz::Encode; use std::sync::Arc; -use types::{ForkName, Hash256, LightClientBootstrap}; +use types::{BeaconResponse, ForkName, Hash256, LightClientBootstrap}; use warp::{ hyper::{Body, Response}, reply::Reply, @@ -37,15 +38,9 @@ pub fn get_light_client_updates( .map(|update| map_light_client_update_to_ssz_chunk::(&chain, update)) .collect::>(); - let ssz_response = LightClientUpdateSszResponse { - response_chunk_len: (light_client_updates.len() as u64).to_le_bytes().to_vec(), - response_chunk: response_chunks.as_ssz_bytes(), - } - .as_ssz_bytes(); - Response::builder() .status(200) - .body(ssz_response) + .body(response_chunks.as_ssz_bytes()) .map(|res: Response>| add_ssz_content_type_header(res)) .map_err(|e| { warp_utils::reject::custom_server_error(format!( @@ -58,7 +53,7 @@ pub fn get_light_client_updates( let fork_versioned_response = light_client_updates .iter() .map(|update| map_light_client_update_to_json_response::(&chain, update.clone())) - .collect::>>, Rejection>>()?; + .collect::>>>(); Ok(warp::reply::json(&fork_versioned_response).into_response()) } } @@ -94,10 +89,8 @@ pub fn get_light_client_bootstrap( warp_utils::reject::custom_server_error(format!("failed to create response: {}", e)) }), _ => { - let fork_versioned_response = map_light_client_bootstrap_to_json_response::( - fork_name, - light_client_bootstrap, - )?; + let fork_versioned_response = + map_light_client_bootstrap_to_json_response::(fork_name, light_client_bootstrap); Ok(warp::reply::json(&fork_versioned_response).into_response()) } } @@ -159,33 +152,44 @@ fn map_light_client_update_to_ssz_chunk( ) -> LightClientUpdateResponseChunk { let fork_name = chain .spec - .fork_name_at_slot::(*light_client_update.signature_slot()); + .fork_name_at_slot::(light_client_update.attested_header_slot()); let fork_digest = ChainSpec::compute_fork_digest( chain.spec.fork_version_for_name(fork_name), chain.genesis_validators_root, ); - LightClientUpdateResponseChunk { + let payload = light_client_update.as_ssz_bytes(); + let response_chunk_len = fork_digest.len() + payload.len(); + + let response_chunk = LightClientUpdateResponseChunkInner { context: fork_digest, - payload: light_client_update.as_ssz_bytes(), + payload, + }; + + LightClientUpdateResponseChunk { + response_chunk_len: response_chunk_len as u64, + response_chunk, } } fn map_light_client_bootstrap_to_json_response( fork_name: ForkName, light_client_bootstrap: LightClientBootstrap, -) -> Result>, Rejection> { - fork_versioned_response(V1, fork_name, light_client_bootstrap) +) -> BeaconResponse> { + beacon_response( + ResponseIncludesVersion::Yes(fork_name), + light_client_bootstrap, + ) } fn map_light_client_update_to_json_response( chain: &BeaconChain, light_client_update: LightClientUpdate, -) -> Result>, Rejection> { +) -> BeaconResponse> { let fork_name = chain .spec .fork_name_at_slot::(*light_client_update.signature_slot()); - fork_versioned_response(V1, fork_name, light_client_update) + beacon_response(ResponseIncludesVersion::Yes(fork_name), light_client_update) } diff --git a/beacon_node/http_api/src/produce_block.rs b/beacon_node/http_api/src/produce_block.rs index 22d6f0e7ae..db82ff214c 100644 --- a/beacon_node/http_api/src/produce_block.rs +++ b/beacon_node/http_api/src/produce_block.rs @@ -3,15 +3,14 @@ use crate::{ version::{ add_consensus_block_value_header, add_consensus_version_header, add_execution_payload_blinded_header, add_execution_payload_value_header, - add_ssz_content_type_header, fork_versioned_response, inconsistent_fork_rejection, + add_ssz_content_type_header, beacon_response, inconsistent_fork_rejection, + ResponseIncludesVersion, }, }; use beacon_chain::{ BeaconBlockResponseWrapper, BeaconChain, BeaconChainTypes, ProduceBlockVerification, }; -use eth2::types::{ - self as api_types, EndpointVersion, ProduceBlockV3Metadata, SkipRandaoVerification, -}; +use eth2::types::{self as api_types, ProduceBlockV3Metadata, SkipRandaoVerification}; use ssz::Encode; use std::sync::Arc; use types::{payload::BlockProductionVersion, *}; @@ -115,7 +114,7 @@ pub fn build_response_v3( warp_utils::reject::custom_server_error(format!("failed to create response: {}", e)) }), _ => Ok(warp::reply::json(&ForkVersionedResponse { - version: Some(fork_name), + version: fork_name, metadata, data: block_contents, }) @@ -129,7 +128,6 @@ pub fn build_response_v3( } pub async fn produce_blinded_block_v2( - endpoint_version: EndpointVersion, accept_header: Option, chain: Arc>, slot: Slot, @@ -155,11 +153,10 @@ pub async fn produce_blinded_block_v2( .await .map_err(warp_utils::reject::unhandled_error)?; - build_response_v2(chain, block_response_type, endpoint_version, accept_header) + build_response_v2(chain, block_response_type, accept_header) } pub async fn produce_block_v2( - endpoint_version: EndpointVersion, accept_header: Option, chain: Arc>, slot: Slot, @@ -186,13 +183,12 @@ pub async fn produce_block_v2( .await .map_err(warp_utils::reject::unhandled_error)?; - build_response_v2(chain, block_response_type, endpoint_version, accept_header) + build_response_v2(chain, block_response_type, accept_header) } pub fn build_response_v2( chain: Arc>, block_response: BeaconBlockResponseWrapper, - endpoint_version: EndpointVersion, accept_header: Option, ) -> Result, warp::Rejection> { let fork_name = block_response @@ -210,8 +206,10 @@ pub fn build_response_v2( .map_err(|e| { warp_utils::reject::custom_server_error(format!("failed to create response: {}", e)) }), - _ => fork_versioned_response(endpoint_version, fork_name, block_contents) - .map(|response| warp::reply::json(&response).into_response()) - .map(|res| add_consensus_version_header(res, fork_name)), + _ => Ok(warp::reply::json(&beacon_response( + ResponseIncludesVersion::Yes(fork_name), + block_contents, + )) + .into_response()), } } diff --git a/beacon_node/http_api/src/publish_attestations.rs b/beacon_node/http_api/src/publish_attestations.rs index cd5e912bdf..db85b8f205 100644 --- a/beacon_node/http_api/src/publish_attestations.rs +++ b/beacon_node/http_api/src/publish_attestations.rs @@ -60,13 +60,13 @@ use types::{Attestation, EthSpec, ForkName, SingleAttestation}; pub enum Error { Validation(AttestationError), Publication, - ForkChoice(#[allow(dead_code)] BeaconChainError), + ForkChoice(#[allow(dead_code)] Box), AggregationPool(#[allow(dead_code)] AttestationError), ReprocessDisabled, ReprocessFull, ReprocessTimeout, InvalidJson(#[allow(dead_code)] serde_json::Error), - FailedConversion(#[allow(dead_code)] BeaconChainError), + FailedConversion(#[allow(dead_code)] Box), } enum PublishAttestationResult { @@ -164,7 +164,7 @@ fn verify_and_publish_attestation( } if let Err(e) = fc_result { - Err(Error::ForkChoice(e)) + Err(Error::ForkChoice(Box::new(e))) } else if let Err(e) = naive_aggregation_result { Err(Error::AggregationPool(e)) } else { @@ -213,7 +213,7 @@ fn convert_to_attestation<'a, T: BeaconChainTypes>( beacon_block_root, })) } - Err(e) => Err(Error::FailedConversion(e)), + Err(e) => Err(Error::FailedConversion(Box::new(e))), } } } diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index a5cd94536d..9b1a3f8677 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -2,7 +2,7 @@ use crate::metrics; use std::future::Future; use beacon_chain::blob_verification::{GossipBlobError, GossipVerifiedBlob}; -use beacon_chain::block_verification_types::AsBlock; +use beacon_chain::block_verification_types::{AsBlock, RpcBlock}; use beacon_chain::data_column_verification::{GossipDataColumnError, GossipVerifiedDataColumn}; use beacon_chain::validator_monitor::{get_block_delay_ms, timestamp_now}; use beacon_chain::{ @@ -123,8 +123,9 @@ pub async fn publish_block>( "Signed block published to network via HTTP API" ); - crate::publish_pubsub_message(&sender, PubsubMessage::BeaconBlock(block.clone())) - .map_err(|_| BlockError::BeaconChainError(BeaconChainError::UnableToPublish))?; + crate::publish_pubsub_message(&sender, PubsubMessage::BeaconBlock(block.clone())).map_err( + |_| BlockError::BeaconChainError(Box::new(BeaconChainError::UnableToPublish)), + )?; Ok(()) }; @@ -302,7 +303,11 @@ pub async fn publish_block>( ); let import_result = Box::pin(chain.process_block( block_root, - block.clone(), + RpcBlock::new_without_blobs( + Some(block_root), + block.clone(), + network_globals.custody_columns_count() as usize, + ), NotifyExecutionLayer::Yes, BlockImportSource::HttpApi, publish_fn, @@ -364,7 +369,7 @@ fn spawn_build_data_sidecar_task( } else { // Post PeerDAS: construct data columns. let gossip_verified_data_columns = - build_gossip_verified_data_columns(&chain, &block, blobs)?; + build_gossip_verified_data_columns(&chain, &block, blobs, kzg_proofs)?; Ok((vec![], gossip_verified_data_columns)) } }, @@ -383,10 +388,11 @@ fn build_gossip_verified_data_columns( chain: &BeaconChain, block: &SignedBeaconBlock>, blobs: BlobsList, + kzg_cell_proofs: KzgProofs, ) -> Result>>, Rejection> { let slot = block.slot(); let data_column_sidecars = - build_blob_data_column_sidecars(chain, block, blobs).map_err(|e| { + build_blob_data_column_sidecars(chain, block, blobs, kzg_cell_proofs).map_err(|e| { error!( error = ?e, %slot, @@ -501,7 +507,7 @@ fn publish_blob_sidecars( ) -> Result<(), BlockError> { let pubsub_message = PubsubMessage::BlobSidecar(Box::new((blob.index(), blob.clone_blob()))); crate::publish_pubsub_message(sender_clone, pubsub_message) - .map_err(|_| BlockError::BeaconChainError(BeaconChainError::UnableToPublish)) + .map_err(|_| BlockError::BeaconChainError(Box::new(BeaconChainError::UnableToPublish))) } fn publish_column_sidecars( @@ -520,7 +526,7 @@ fn publish_column_sidecars( .len() .saturating_sub(malicious_withhold_count); // Randomize columns before dropping the last malicious_withhold_count items - data_column_sidecars.shuffle(&mut rand::thread_rng()); + data_column_sidecars.shuffle(&mut **chain.rng.lock()); data_column_sidecars.truncate(columns_to_keep); } let pubsub_messages = data_column_sidecars @@ -531,7 +537,7 @@ fn publish_column_sidecars( }) .collect::>(); crate::publish_pubsub_messages(sender_clone, pubsub_messages) - .map_err(|_| BlockError::BeaconChainError(BeaconChainError::UnableToPublish)) + .map_err(|_| BlockError::BeaconChainError(Box::new(BeaconChainError::UnableToPublish))) } async fn post_block_import_logging_and_response( @@ -588,7 +594,9 @@ async fn post_block_import_logging_and_response( Err(warp_utils::reject::custom_bad_request(msg)) } } - Err(BlockError::BeaconChainError(BeaconChainError::UnableToPublish)) => { + Err(BlockError::BeaconChainError(e)) + if matches!(e.as_ref(), BeaconChainError::UnableToPublish) => + { Err(warp_utils::reject::custom_server_error( "unable to publish to network channel".to_string(), )) @@ -784,7 +792,7 @@ fn check_slashable( block_clone.message().proposer_index(), block_root, ) - .map_err(|e| BlockError::BeaconChainError(e.into()))? + .map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))? { warn!( slot = %block_clone.slot(), diff --git a/beacon_node/http_api/src/sync_committees.rs b/beacon_node/http_api/src/sync_committees.rs index 9ca1a2401a..aa126bbc82 100644 --- a/beacon_node/http_api/src/sync_committees.rs +++ b/beacon_node/http_api/src/sync_committees.rs @@ -59,7 +59,7 @@ pub fn sync_committee_duties( } let duties = duties_from_state_load(request_epoch, request_indices, altair_fork_epoch, chain) - .map_err(|e| match e { + .map_err(|e| match *e { BeaconChainError::SyncDutiesError(BeaconStateError::SyncCommitteeNotKnown { current_epoch, .. @@ -81,7 +81,7 @@ fn duties_from_state_load( request_indices: &[u64], altair_fork_epoch: Epoch, chain: &BeaconChain, -) -> Result, BeaconStateError>>, BeaconChainError> { +) -> Result, BeaconStateError>>, Box> { // Determine what the current epoch would be if we fast-forward our system clock by // `MAXIMUM_GOSSIP_CLOCK_DISPARITY`. // @@ -92,11 +92,17 @@ fn duties_from_state_load( let tolerant_current_epoch = chain .slot_clock .now_with_future_tolerance(chain.spec.maximum_gossip_clock_disparity()) - .ok_or(BeaconChainError::UnableToReadSlot)? + .ok_or(BeaconChainError::UnableToReadSlot) + .map_err(Box::new)? .epoch(T::EthSpec::slots_per_epoch()); - let max_sync_committee_period = tolerant_current_epoch.sync_committee_period(&chain.spec)? + 1; - let sync_committee_period = request_epoch.sync_committee_period(&chain.spec)?; + let max_sync_committee_period = tolerant_current_epoch + .sync_committee_period(&chain.spec) + .map_err(|e| Box::new(e.into()))? + + 1; + let sync_committee_period = request_epoch + .sync_committee_period(&chain.spec) + .map_err(|e| Box::new(e.into()))?; if tolerant_current_epoch < altair_fork_epoch { // Empty response if the epoch is pre-Altair. @@ -119,13 +125,14 @@ fn duties_from_state_load( state .get_sync_committee_duties(request_epoch, request_indices, &chain.spec) .map_err(BeaconChainError::SyncDutiesError) + .map_err(Box::new) } else { - Err(BeaconChainError::SyncDutiesError( + Err(Box::new(BeaconChainError::SyncDutiesError( BeaconStateError::SyncCommitteeNotKnown { current_epoch, epoch: request_epoch, }, - )) + ))) } } diff --git a/beacon_node/http_api/src/validator.rs b/beacon_node/http_api/src/validator.rs index baa41e33ed..25b0feb99e 100644 --- a/beacon_node/http_api/src/validator.rs +++ b/beacon_node/http_api/src/validator.rs @@ -7,9 +7,10 @@ pub fn pubkey_to_validator_index( chain: &BeaconChain, state: &BeaconState, pubkey: &PublicKeyBytes, -) -> Result, BeaconChainError> { +) -> Result, Box> { chain - .validator_index(pubkey)? + .validator_index(pubkey) + .map_err(Box::new)? .filter(|&index| { state .validators() diff --git a/beacon_node/http_api/src/validators.rs b/beacon_node/http_api/src/validators.rs index f3d78e6fcd..90ddd1ee8f 100644 --- a/beacon_node/http_api/src/validators.rs +++ b/beacon_node/http_api/src/validators.rs @@ -81,8 +81,13 @@ 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())); + let ids_filter_set: Option> = match optional_ids { + // if optional_ids (the request data body) is [], returns a `None`, so that later when calling .is_none_or() will return True + // Hence, all validators will pass through .filter(), and balances of all validators are returned, in accordance to the spec + Some([]) => None, + Some(ids) => Some(HashSet::from_iter(ids.iter())), + None => None, + }; Ok(( state diff --git a/beacon_node/http_api/src/version.rs b/beacon_node/http_api/src/version.rs index 59816cb897..361e8e78ea 100644 --- a/beacon_node/http_api/src/version.rs +++ b/beacon_node/http_api/src/version.rs @@ -5,10 +5,11 @@ use eth2::{ }; use serde::Serialize; use types::{ - fork_versioned_response::{ - ExecutionOptimisticFinalizedForkVersionedResponse, ExecutionOptimisticFinalizedMetadata, + beacon_response::{ + ExecutionOptimisticFinalizedBeaconResponse, ExecutionOptimisticFinalizedMetadata, }, - ForkName, ForkVersionedResponse, InconsistentFork, Uint256, + BeaconResponse, ForkName, ForkVersionedResponse, InconsistentFork, Uint256, + UnversionedResponse, }; use warp::reply::{self, Reply, Response}; @@ -16,47 +17,54 @@ pub const V1: EndpointVersion = EndpointVersion(1); pub const V2: EndpointVersion = EndpointVersion(2); pub const V3: EndpointVersion = EndpointVersion(3); -pub fn fork_versioned_response( - endpoint_version: EndpointVersion, - fork_name: ForkName, - data: T, -) -> Result, warp::reject::Rejection> { - let fork_name = if endpoint_version == V1 { - None - } else if endpoint_version == V2 || endpoint_version == V3 { - Some(fork_name) - } else { - return Err(unsupported_version_rejection(endpoint_version)); - }; - Ok(ForkVersionedResponse { - version: fork_name, - metadata: Default::default(), - data, - }) +#[derive(Debug, PartialEq, Clone, Serialize)] +pub enum ResponseIncludesVersion { + Yes(ForkName), + No, } -pub fn execution_optimistic_finalized_fork_versioned_response( - endpoint_version: EndpointVersion, - fork_name: ForkName, +pub fn beacon_response( + require_version: ResponseIncludesVersion, + data: T, +) -> BeaconResponse { + match require_version { + ResponseIncludesVersion::Yes(fork_name) => { + BeaconResponse::ForkVersioned(ForkVersionedResponse { + version: fork_name, + metadata: Default::default(), + data, + }) + } + ResponseIncludesVersion::No => BeaconResponse::Unversioned(UnversionedResponse { + metadata: Default::default(), + data, + }), + } +} + +pub fn execution_optimistic_finalized_beacon_response( + require_version: ResponseIncludesVersion, execution_optimistic: bool, finalized: bool, data: T, -) -> Result, warp::reject::Rejection> { - let fork_name = if endpoint_version == V1 { - None - } else if endpoint_version == V2 { - Some(fork_name) - } else { - return Err(unsupported_version_rejection(endpoint_version)); +) -> Result, warp::reject::Rejection> { + let metadata = ExecutionOptimisticFinalizedMetadata { + execution_optimistic: Some(execution_optimistic), + finalized: Some(finalized), }; - Ok(ExecutionOptimisticFinalizedForkVersionedResponse { - version: fork_name, - metadata: ExecutionOptimisticFinalizedMetadata { - execution_optimistic: Some(execution_optimistic), - finalized: Some(finalized), - }, - data, - }) + match require_version { + ResponseIncludesVersion::Yes(fork_name) => { + Ok(BeaconResponse::ForkVersioned(ForkVersionedResponse { + version: fork_name, + metadata, + data, + })) + } + ResponseIncludesVersion::No => Ok(BeaconResponse::Unversioned(UnversionedResponse { + metadata, + data, + })), + } } /// Add the 'Content-Type application/octet-stream` header to a response. diff --git a/beacon_node/http_api/tests/interactive_tests.rs b/beacon_node/http_api/tests/interactive_tests.rs index bb3086945b..4f3cd6c828 100644 --- a/beacon_node/http_api/tests/interactive_tests.rs +++ b/beacon_node/http_api/tests/interactive_tests.rs @@ -115,10 +115,10 @@ async fn state_by_root_pruned_from_fork_choice() { .unwrap() .unwrap(); - assert!(response.metadata.finalized.unwrap()); - assert!(!response.metadata.execution_optimistic.unwrap()); + assert!(response.metadata().finalized.unwrap()); + assert!(!response.metadata().execution_optimistic.unwrap()); - let mut state = response.data; + let mut state = response.into_data(); assert_eq!(state.update_tree_hash_cache().unwrap(), state_root); } } @@ -846,7 +846,7 @@ pub async fn fork_choice_before_proposal() { .get_validator_blocks::(slot_d, &randao_reveal, None) .await .unwrap() - .data + .into_data() .deconstruct() .0; diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 828b1613a7..c97fec5a06 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -27,6 +27,7 @@ use http_api::{ }; use lighthouse_network::{types::SyncState, Enr, EnrExt, PeerId}; use network::NetworkReceivers; +use operation_pool::attestation_storage::CheckpointKey; use proto_array::ExecutionStatus; use sensitive_url::SensitiveUrl; use slot_clock::SlotClock; @@ -681,7 +682,7 @@ impl ApiTester { .await .unwrap() .unwrap() - .metadata + .metadata() .finalized .unwrap(); @@ -718,7 +719,7 @@ impl ApiTester { .await .unwrap() .unwrap() - .metadata + .metadata() .finalized .unwrap(); @@ -756,7 +757,7 @@ impl ApiTester { .await .unwrap() .unwrap() - .metadata + .metadata() .finalized .unwrap(); @@ -926,18 +927,32 @@ impl ApiTester { .map(|res| res.data); let expected = state_opt.map(|(state, _execution_optimistic, _finalized)| { - let mut validators = Vec::with_capacity(validator_indices.len()); + // If validator_indices is empty, return balances for all validators + if validator_indices.is_empty() { + state + .balances() + .iter() + .enumerate() + .map(|(index, balance)| ValidatorBalanceData { + index: index as u64, + balance: *balance, + }) + .collect() + } else { + // Same behaviour as before for the else branch + let mut validators = Vec::with_capacity(validator_indices.len()); - for i in validator_indices { - if i < state.balances().len() as u64 { - validators.push(ValidatorBalanceData { - index: i, - balance: *state.balances().get(i as usize).unwrap(), - }); + for i in validator_indices { + if i < state.balances().len() as u64 { + validators.push(ValidatorBalanceData { + index: i, + balance: *state.balances().get(i as usize).unwrap(), + }); + } } - } - validators + validators + } }); assert_eq!(result_index_ids, expected, "{:?}", state_id); @@ -1241,6 +1256,33 @@ impl ApiTester { self } + pub async fn test_beacon_states_pending_consolidations(self) -> Self { + for state_id in self.interesting_state_ids() { + let mut state_opt = state_id + .state(&self.chain) + .ok() + .map(|(state, _execution_optimistic, _finalized)| state); + + let result = self + .client + .get_beacon_states_pending_consolidations(state_id.0) + .await + .unwrap() + .map(|res| res.data); + + if result.is_none() && state_opt.is_none() { + continue; + } + + let state = state_opt.as_mut().expect("result should be none"); + let expected = state.pending_consolidations().unwrap(); + + assert_eq!(result.unwrap(), expected.to_vec()); + } + + self + } + pub async fn test_beacon_headers_all_slots(self) -> Self { for slot in 0..CHAIN_LENGTH { let slot = Slot::from(slot); @@ -1569,9 +1611,9 @@ impl ApiTester { let json_result = self.client.get_beacon_blocks(block_id.0).await.unwrap(); if let (Some(json), Some(expected)) = (&json_result, &expected) { - assert_eq!(&json.data, expected.as_ref(), "{:?}", block_id); + assert_eq!(json.data(), expected.as_ref(), "{:?}", block_id); assert_eq!( - json.version, + json.version(), Some(expected.fork_name(&self.chain.spec).unwrap()) ); } else { @@ -1595,8 +1637,8 @@ impl ApiTester { // Check that the legacy v1 API still works but doesn't return a version field. let v1_result = self.client.get_beacon_blocks_v1(block_id.0).await.unwrap(); if let (Some(v1_result), Some(expected)) = (&v1_result, &expected) { - assert_eq!(v1_result.version, None); - assert_eq!(&v1_result.data, expected.as_ref()); + assert_eq!(v1_result.version(), None); + assert_eq!(v1_result.data(), expected.as_ref()); } else { assert_eq!(v1_result, None); assert_eq!(expected, None); @@ -1657,9 +1699,9 @@ impl ApiTester { .unwrap(); if let (Some(json), Some(expected)) = (&json_result, &expected) { - assert_eq!(&json.data, expected, "{:?}", block_id); + assert_eq!(json.data(), expected, "{:?}", block_id); assert_eq!( - json.version, + json.version(), Some(expected.fork_name(&self.chain.spec).unwrap()) ); } else { @@ -1722,10 +1764,14 @@ impl ApiTester { }; let result = match self .client - .get_blobs::(CoreBlockId::Root(block_root), blob_indices.as_deref()) + .get_blobs::( + CoreBlockId::Root(block_root), + blob_indices.as_deref(), + &self.chain.spec, + ) .await { - Ok(result) => result.unwrap().data, + Ok(result) => result.unwrap().into_data(), Err(e) => panic!("query failed incorrectly: {e:?}"), }; @@ -1778,13 +1824,13 @@ impl ApiTester { match self .client - .get_blobs::(CoreBlockId::Slot(test_slot), None) + .get_blobs::(CoreBlockId::Slot(test_slot), None, &self.chain.spec) .await { Ok(result) => { if zero_blobs { assert_eq!( - &result.unwrap().data[..], + &result.unwrap().into_data()[..], &[], "empty blobs are always available" ); @@ -1816,7 +1862,7 @@ impl ApiTester { match self .client - .get_blobs::(CoreBlockId::Slot(test_slot), None) + .get_blobs::(CoreBlockId::Slot(test_slot), None, &self.chain.spec) .await { Ok(result) => panic!("queries for pre-Deneb slots should fail. got: {result:?}"), @@ -1833,7 +1879,7 @@ impl ApiTester { .get_beacon_blocks_attestations_v2(block_id.0) .await .unwrap() - .map(|res| res.data); + .map(|res| res.into_data()); let expected = block_id.full_block(&self.chain).await.ok().map( |(block, _execution_optimistic, _finalized)| { @@ -2043,7 +2089,7 @@ impl ApiTester { .get_light_client_bootstrap(&self.chain.store, &block_root, 1u64, &self.chain.spec); assert!(expected.is_ok()); - assert_eq!(result.unwrap().data, expected.unwrap().unwrap().0); + assert_eq!(result.unwrap().data(), &expected.unwrap().unwrap().0); self } @@ -2055,7 +2101,7 @@ impl ApiTester { .get_beacon_light_client_optimistic_update::() .await { - Ok(result) => result.map(|res| res.data), + Ok(result) => result.map(|res| res.into_data()), Err(e) => panic!("query failed incorrectly: {e:?}"), }; @@ -2074,7 +2120,7 @@ impl ApiTester { .get_beacon_light_client_finality_update::() .await { - Ok(result) => result.map(|res| res.data), + Ok(result) => result.map(|res| res.into_data()), Err(e) => panic!("query failed incorrectly: {e:?}"), }; @@ -2087,7 +2133,7 @@ impl ApiTester { self } - pub async fn test_get_beacon_pool_attestations(self) -> Self { + pub async fn test_get_beacon_pool_attestations(self) { let result = self .client .get_beacon_pool_attestations_v1(None, None) @@ -2105,10 +2151,81 @@ impl ApiTester { .get_beacon_pool_attestations_v2(None, None) .await .unwrap() - .data; + .into_data(); + assert_eq!(result, expected); - self + let result_committee_index_filtered = self + .client + .get_beacon_pool_attestations_v1(None, Some(0)) + .await + .unwrap() + .data; + + let expected_committee_index_filtered = expected + .clone() + .into_iter() + .filter(|att| att.get_committee_indices_map().contains(&0)) + .collect::>(); + + assert_eq!( + result_committee_index_filtered, + expected_committee_index_filtered + ); + + let result_committee_index_filtered = self + .client + .get_beacon_pool_attestations_v1(None, Some(1)) + .await + .unwrap() + .data; + + let expected_committee_index_filtered = expected + .clone() + .into_iter() + .filter(|att| att.get_committee_indices_map().contains(&1)) + .collect::>(); + + assert_eq!( + result_committee_index_filtered, + expected_committee_index_filtered + ); + + let fork_name = self + .harness + .chain + .spec + .fork_name_at_slot::(self.harness.chain.slot().unwrap()); + + // aggregate electra attestations + if fork_name.electra_enabled() { + // Take and drop the lock in a block to avoid clippy complaining + // about taking locks across await points + { + let mut all_attestations = self.chain.op_pool.attestations.write(); + let (prev_epoch_key, curr_epoch_key) = + CheckpointKey::keys_for_state(&self.harness.get_current_state()); + all_attestations.aggregate_across_committees(prev_epoch_key); + all_attestations.aggregate_across_committees(curr_epoch_key); + } + let result_committee_index_filtered = self + .client + .get_beacon_pool_attestations_v2(None, Some(0)) + .await + .unwrap() + .into_data(); + let mut expected = self.chain.op_pool.get_all_attestations(); + expected.extend(self.chain.naive_aggregation_pool.read().iter().cloned()); + let expected_committee_index_filtered = expected + .clone() + .into_iter() + .filter(|att| att.get_committee_indices_map().contains(&0)) + .collect::>(); + assert_eq!( + result_committee_index_filtered, + expected_committee_index_filtered + ); + } } pub async fn test_post_beacon_pool_attester_slashings_valid_v1(mut self) -> Self { @@ -2212,7 +2329,7 @@ impl ApiTester { .get_beacon_pool_attester_slashings_v2() .await .unwrap() - .data; + .into_data(); assert_eq!(result, expected); self @@ -2376,7 +2493,7 @@ impl ApiTester { is_syncing: false, is_optimistic: false, // these tests run without the Bellatrix fork enabled - el_offline: true, + el_offline: false, head_slot, sync_distance, }; @@ -2440,11 +2557,11 @@ impl ApiTester { pub async fn test_get_node_health(self) -> Self { let status = self.client.get_node_health().await; match status { - Ok(_) => { - panic!("should return 503 error status code"); + Ok(status) => { + assert_eq!(status, 200); } - Err(e) => { - assert_eq!(e.status().unwrap(), 503); + Err(_) => { + panic!("should return valid status"); } } self @@ -2550,9 +2667,9 @@ impl ApiTester { expected.as_mut().map(|state| state.drop_all_caches()); if let (Some(json), Some(expected)) = (&result_json, &expected) { - assert_eq!(json.data, *expected, "{:?}", state_id); + assert_eq!(json.data(), expected, "{:?}", state_id); assert_eq!( - json.version, + json.version(), Some(expected.fork_name(&self.chain.spec).unwrap()) ); } else { @@ -3073,7 +3190,7 @@ impl ApiTester { .get_validator_blocks::(slot, &randao_reveal, None) .await .unwrap() - .data + .into_data() .deconstruct() .0; @@ -3170,7 +3287,7 @@ impl ApiTester { ) { // 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!(metadata.consensus_version, response.version); assert_eq!(ForkName::Base, response.metadata.consensus_version); assert_eq!( metadata.execution_payload_blinded, @@ -3296,7 +3413,7 @@ impl ApiTester { ) .await .unwrap() - .data + .into_data() .deconstruct() .0; assert_eq!(block.slot(), slot); @@ -3410,7 +3527,7 @@ impl ApiTester { .get_validator_blinded_blocks::(slot, &randao_reveal, None) .await .unwrap() - .data; + .into_data(); let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); @@ -3425,7 +3542,7 @@ impl ApiTester { .await .unwrap() .unwrap() - .data; + .into_data(); assert_eq!(head_block.clone_as_blinded(), signed_block); @@ -3498,7 +3615,7 @@ impl ApiTester { .await .unwrap() .unwrap() - .data; + .into_data(); let signed_block = signed_block_contents.signed_block(); assert_eq!(head_block, **signed_block); @@ -3521,7 +3638,7 @@ impl ApiTester { ) .await .unwrap() - .data; + .into_data(); assert_eq!(blinded_block.slot(), slot); self.chain.slot_clock.set_slot(slot.as_u64() + 1); } @@ -3665,7 +3782,7 @@ impl ApiTester { .await .unwrap() .unwrap() - .data; + .into_data(); let expected = attestation; assert_eq!(result, expected); @@ -4239,7 +4356,7 @@ impl ApiTester { .get_validator_blinded_blocks::(slot, &randao_reveal, None) .await .unwrap() - .data + .into_data() .body() .execution_payload() .unwrap() @@ -4285,7 +4402,7 @@ impl ApiTester { .get_validator_blinded_blocks::(slot, &randao_reveal, None) .await .unwrap() - .data + .into_data() .body() .execution_payload() .unwrap() @@ -4329,7 +4446,7 @@ impl ApiTester { .get_validator_blinded_blocks::(slot, &randao_reveal, None) .await .unwrap() - .data + .into_data() .body() .execution_payload() .unwrap() @@ -4403,7 +4520,7 @@ impl ApiTester { .get_validator_blinded_blocks::(slot, &randao_reveal, None) .await .unwrap() - .data + .into_data() .body() .execution_payload() .unwrap() @@ -4489,7 +4606,7 @@ impl ApiTester { .get_validator_blinded_blocks::(slot, &randao_reveal, None) .await .unwrap() - .data + .into_data() .body() .execution_payload() .unwrap() @@ -4581,7 +4698,7 @@ impl ApiTester { .get_validator_blinded_blocks::(slot, &randao_reveal, None) .await .unwrap() - .data + .into_data() .body() .execution_payload() .unwrap() @@ -4671,7 +4788,7 @@ impl ApiTester { .get_validator_blinded_blocks::(slot, &randao_reveal, None) .await .unwrap() - .data + .into_data() .body() .execution_payload() .unwrap() @@ -4760,7 +4877,7 @@ impl ApiTester { .get_validator_blinded_blocks::(slot, &randao_reveal, None) .await .unwrap() - .data + .into_data() .body() .execution_payload() .unwrap() @@ -4835,7 +4952,7 @@ impl ApiTester { .get_validator_blinded_blocks::(slot, &randao_reveal, None) .await .unwrap() - .data + .into_data() .body() .execution_payload() .unwrap() @@ -4898,7 +5015,7 @@ impl ApiTester { .get_validator_blinded_blocks::(slot, &randao_reveal, None) .await .unwrap() - .data + .into_data() .body() .execution_payload() .unwrap() @@ -4974,7 +5091,7 @@ impl ApiTester { .get_validator_blinded_blocks::(next_slot, &randao_reveal, None) .await .unwrap() - .data + .into_data() .body() .execution_payload() .unwrap() @@ -5005,7 +5122,7 @@ impl ApiTester { .get_validator_blinded_blocks::(next_slot, &randao_reveal, None) .await .unwrap() - .data + .into_data() .body() .execution_payload() .unwrap() @@ -5113,7 +5230,7 @@ impl ApiTester { .get_validator_blinded_blocks::(next_slot, &randao_reveal, None) .await .unwrap() - .data + .into_data() .body() .execution_payload() .unwrap() @@ -5154,7 +5271,7 @@ impl ApiTester { .get_validator_blinded_blocks::(next_slot, &randao_reveal, None) .await .unwrap() - .data + .into_data() .body() .execution_payload() .unwrap() @@ -5270,7 +5387,7 @@ impl ApiTester { .get_validator_blinded_blocks::(slot, &randao_reveal, None) .await .unwrap() - .data + .into_data() .body() .execution_payload() .unwrap() @@ -5351,7 +5468,7 @@ impl ApiTester { .get_validator_blinded_blocks::(slot, &randao_reveal, None) .await .unwrap() - .data + .into_data() .body() .execution_payload() .unwrap() @@ -5419,7 +5536,7 @@ impl ApiTester { .get_validator_blinded_blocks::(slot, &randao_reveal, None) .await .unwrap() - .data + .into_data() .body() .execution_payload() .unwrap() @@ -5487,7 +5604,7 @@ impl ApiTester { .get_validator_blinded_blocks::(slot, &randao_reveal, None) .await .unwrap() - .data + .into_data() .body() .execution_payload() .unwrap() @@ -5554,7 +5671,7 @@ impl ApiTester { .get_validator_blinded_blocks::(slot, &randao_reveal, None) .await .unwrap() - .data + .into_data() .body() .execution_payload() .unwrap() @@ -5625,7 +5742,7 @@ impl ApiTester { .get_validator_blinded_blocks::(slot, &randao_reveal, None) .await .unwrap() - .data + .into_data() .body() .execution_payload() .unwrap() @@ -6294,6 +6411,34 @@ impl ApiTester { assert_eq!(result.execution_optimistic, Some(true)); } + + async fn test_get_beacon_rewards_blocks_at_head(&self) -> StandardBlockReward { + self.client + .get_beacon_rewards_blocks(CoreBlockId::Head) + .await + .unwrap() + .data + } + + async fn test_beacon_block_rewards_electra(self) -> Self { + for _ in 0..E::slots_per_epoch() { + let state = self.harness.get_current_state(); + let slot = state.slot() + Slot::new(1); + // calculate beacon block rewards / penalties + let ((signed_block, _maybe_blob_sidecars), mut state) = + self.harness.make_block_return_pre_state(state, slot).await; + + let beacon_block_reward = self + .harness + .chain + .compute_beacon_block_reward(signed_block.message(), &mut state) + .unwrap(); + self.harness.extend_slots(1).await; + let api_beacon_block_reward = self.test_get_beacon_rewards_blocks_at_head().await; + assert_eq!(beacon_block_reward, api_beacon_block_reward); + } + self + } } async fn poll_events, eth2::Error>> + Unpin, E: EthSpec>( @@ -6416,6 +6561,8 @@ async fn beacon_get_state_info_electra() { .test_beacon_states_pending_deposits() .await .test_beacon_states_pending_partial_withdrawals() + .await + .test_beacon_states_pending_consolidations() .await; } @@ -6446,10 +6593,30 @@ async fn beacon_get_blocks() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn beacon_get_pools() { +async fn test_beacon_pool_attestations_electra() { + let mut config = ApiTesterConfig::default(); + config.spec.altair_fork_epoch = Some(Epoch::new(0)); + config.spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + config.spec.capella_fork_epoch = Some(Epoch::new(0)); + config.spec.deneb_fork_epoch = Some(Epoch::new(0)); + config.spec.electra_fork_epoch = Some(Epoch::new(0)); + ApiTester::new_from_config(config) + .await + .test_get_beacon_pool_attestations() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_beacon_pool_attestations_base() { ApiTester::new() .await .test_get_beacon_pool_attestations() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn beacon_get_pools() { + ApiTester::new() .await .test_get_beacon_pool_attester_slashings() .await @@ -7412,6 +7579,20 @@ async fn expected_withdrawals_valid_capella() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn get_beacon_rewards_blocks_electra() { + let mut config = ApiTesterConfig::default(); + config.spec.altair_fork_epoch = Some(Epoch::new(0)); + config.spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + config.spec.capella_fork_epoch = Some(Epoch::new(0)); + config.spec.deneb_fork_epoch = Some(Epoch::new(0)); + config.spec.electra_fork_epoch = Some(Epoch::new(0)); + ApiTester::new_from_config(config) + .await + .test_beacon_block_rewards_electra() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn create_signed_inclusion_lists() { let mut config = ApiTesterConfig::default(); diff --git a/beacon_node/lighthouse_network/src/config.rs b/beacon_node/lighthouse_network/src/config.rs index 5a6628439e..89d260569a 100644 --- a/beacon_node/lighthouse_network/src/config.rs +++ b/beacon_node/lighthouse_network/src/config.rs @@ -14,7 +14,7 @@ use std::num::NonZeroU16; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; -use types::{ForkContext, ForkName}; +use types::ForkContext; pub const DEFAULT_IPV4_ADDRESS: Ipv4Addr = Ipv4Addr::UNSPECIFIED; pub const DEFAULT_TCP_PORT: u16 = 9000u16; @@ -22,18 +22,9 @@ pub const DEFAULT_DISC_PORT: u16 = 9000u16; pub const DEFAULT_QUIC_PORT: u16 = 9001u16; pub const DEFAULT_IDONTWANT_MESSAGE_SIZE_THRESHOLD: usize = 1000usize; -/// The maximum size of gossip messages. -pub fn gossip_max_size(is_merge_enabled: bool, gossip_max_size: usize) -> usize { - if is_merge_enabled { - gossip_max_size - } else { - gossip_max_size / 10 - } -} - pub struct GossipsubConfigParams { pub message_domain_valid_snappy: [u8; 4], - pub gossip_max_size: usize, + pub gossipsub_max_transmit_size: usize, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -480,7 +471,6 @@ pub fn gossipsub_config( } } let message_domain_valid_snappy = gossipsub_config_params.message_domain_valid_snappy; - let is_bellatrix_enabled = fork_context.fork_exists(ForkName::Bellatrix); let gossip_message_id = move |message: &gossipsub::Message| { gossipsub::MessageId::from( &Sha256::digest( @@ -499,10 +489,7 @@ pub fn gossipsub_config( let duplicate_cache_time = Duration::from_secs(slots_per_epoch * seconds_per_slot * 2); gossipsub::ConfigBuilder::default() - .max_transmit_size(gossip_max_size( - is_bellatrix_enabled, - gossipsub_config_params.gossip_max_size, - )) + .max_transmit_size(gossipsub_config_params.gossipsub_max_transmit_size) .heartbeat_interval(load.heartbeat_interval) .mesh_n(load.mesh_n) .mesh_n_low(load.mesh_n_low) diff --git a/beacon_node/lighthouse_network/src/lib.rs b/beacon_node/lighthouse_network/src/lib.rs index dbeb0c2c2b..40fdd71b38 100644 --- a/beacon_node/lighthouse_network/src/lib.rs +++ b/beacon_node/lighthouse_network/src/lib.rs @@ -12,7 +12,6 @@ pub mod peer_manager; pub mod rpc; pub mod types; -pub use config::gossip_max_size; use libp2p::swarm::DialError; pub use listen_addr::*; diff --git a/beacon_node/lighthouse_network/src/metrics.rs b/beacon_node/lighthouse_network/src/metrics.rs index b36cb8075d..da986f2884 100644 --- a/beacon_node/lighthouse_network/src/metrics.rs +++ b/beacon_node/lighthouse_network/src/metrics.rs @@ -206,6 +206,20 @@ pub static REPORT_PEER_MSGS: LazyLock> = LazyLock::new(|| ) }); +pub static OUTBOUND_REQUEST_IDLING: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "outbound_request_idling_seconds", + "The time our own request remained idle in the self-limiter", + ) +}); + +pub static RESPONSE_IDLING: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "response_idling_seconds", + "The time our response remained idle in the response limiter", + ) +}); + pub fn scrape_discovery_metrics() { let metrics = discv5::metrics::Metrics::from(discv5::Discv5::::raw_metrics()); diff --git a/beacon_node/lighthouse_network/src/peer_manager/mod.rs b/beacon_node/lighthouse_network/src/peer_manager/mod.rs index c3a44d941a..01cc161105 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/mod.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/mod.rs @@ -712,8 +712,9 @@ 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) -> bool { let mut invalid_meta_data = false; + let mut updated_cgc = false; 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() { @@ -729,12 +730,16 @@ impl PeerManager { debug!(%peer_id, new_seq_no = meta_data.seq_number(), "Obtained peer's metadata"); } + let known_custody_group_count = peer_info + .meta_data() + .and_then(|meta_data| meta_data.custody_group_count().copied().ok()); + let custody_group_count_opt = meta_data.custody_group_count().copied().ok(); peer_info.set_meta_data(meta_data); if self.network_globals.spec.is_peer_das_scheduled() { - // Gracefully ignore metadata/v2 peers. Potentially downscore after PeerDAS to - // prioritize PeerDAS peers. + // Gracefully ignore metadata/v2 peers. + // We only send metadata v3 requests when PeerDAS is scheduled if let Some(custody_group_count) = custody_group_count_opt { match self.compute_peer_custody_groups(peer_id, custody_group_count) { Ok(custody_groups) => { @@ -755,6 +760,8 @@ impl PeerManager { }) .collect(); peer_info.set_custody_subnets(custody_subnets); + + updated_cgc = Some(custody_group_count) != known_custody_group_count; } Err(err) => { debug!( @@ -777,6 +784,8 @@ impl PeerManager { if invalid_meta_data { self.goodbye_peer(peer_id, GoodbyeReason::Fault, ReportSource::PeerManager) } + + updated_cgc } /// Updates the gossipsub scores for all known peers in gossipsub. @@ -1487,6 +1496,15 @@ impl PeerManager { pub fn remove_trusted_peer(&mut self, enr: Enr) { self.trusted_peers.remove(&enr); } + + #[cfg(test)] + fn custody_subnet_count_for_peer(&self, peer_id: &PeerId) -> Option { + self.network_globals + .peers + .read() + .peer_info(peer_id) + .map(|peer_info| peer_info.custody_subnets_iter().count()) + } } enum ConnectingType { @@ -1507,8 +1525,9 @@ enum ConnectingType { #[cfg(test)] mod tests { use super::*; + use crate::rpc::MetaDataV3; use crate::NetworkConfig; - use types::MainnetEthSpec as E; + use types::{ChainSpec, ForkName, MainnetEthSpec as E}; async fn build_peer_manager(target_peer_count: usize) -> PeerManager { build_peer_manager_with_trusted_peers(vec![], target_peer_count).await @@ -1517,6 +1536,15 @@ mod tests { async fn build_peer_manager_with_trusted_peers( trusted_peers: Vec, target_peer_count: usize, + ) -> PeerManager { + let spec = Arc::new(E::default_spec()); + build_peer_manager_with_opts(trusted_peers, target_peer_count, spec).await + } + + async fn build_peer_manager_with_opts( + trusted_peers: Vec, + target_peer_count: usize, + spec: Arc, ) -> PeerManager { let config = config::Config { target_peer_count, @@ -1527,7 +1555,6 @@ mod tests { target_peers: target_peer_count, ..Default::default() }); - let spec = Arc::new(E::default_spec()); let globals = NetworkGlobals::new_test_globals(trusted_peers, network_config, spec); PeerManager::new(config, Arc::new(globals)).unwrap() } @@ -1878,6 +1905,44 @@ mod tests { assert!(peers_should_have_removed.is_empty()); } + #[tokio::test] + /// Test a metadata response should update custody subnets + async fn test_peer_manager_update_custody_subnets() { + // PeerDAS is enabled from Fulu. + let spec = Arc::new(ForkName::Fulu.make_genesis_spec(E::default_spec())); + let mut peer_manager = build_peer_manager_with_opts(vec![], 1, spec).await; + let pubkey = Keypair::generate_secp256k1().public(); + let peer_id = PeerId::from_public_key(&pubkey); + peer_manager.inject_connect_ingoing( + &peer_id, + Multiaddr::empty().with_p2p(peer_id).unwrap(), + None, + ); + + // A newly connected peer should have no custody subnets before metadata is received. + let custody_subnet_count = peer_manager.custody_subnet_count_for_peer(&peer_id); + assert_eq!(custody_subnet_count, Some(0)); + + // Metadata should update the custody subnets. + let peer_cgc = 4; + let meta_data = MetaData::V3(MetaDataV3 { + seq_number: 0, + attnets: Default::default(), + syncnets: Default::default(), + custody_group_count: peer_cgc, + }); + let cgc_updated = peer_manager.meta_data_response(&peer_id, meta_data.clone()); + assert!(cgc_updated); + let custody_subnet_count = peer_manager.custody_subnet_count_for_peer(&peer_id); + assert_eq!(custody_subnet_count, Some(peer_cgc as usize)); + + // Make another update and assert that CGC is not updated. + let cgc_updated = peer_manager.meta_data_response(&peer_id, meta_data); + assert!(!cgc_updated); + let custody_subnet_count = peer_manager.custody_subnet_count_for_peer(&peer_id); + assert_eq!(custody_subnet_count, Some(peer_cgc as usize)); + } + #[tokio::test] /// Test the pruning logic to remove grouped subnet peers async fn test_peer_manager_prune_grouped_subnet_peers() { diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs index 083887046a..95a4e82fa2 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs @@ -1,6 +1,8 @@ use crate::discovery::enr::PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY; use crate::discovery::{peer_id_to_node_id, CombinedKey}; -use crate::{metrics, multiaddr::Multiaddr, types::Subnet, Enr, EnrExt, Gossipsub, PeerId}; +use crate::{ + metrics, multiaddr::Multiaddr, types::Subnet, Enr, EnrExt, Gossipsub, PeerId, SyncInfo, +}; use itertools::Itertools; use logging::crit; use peer_info::{ConnectionDirection, PeerConnectionStatus, PeerInfo}; @@ -15,7 +17,7 @@ use std::{ use sync_status::SyncStatus; use tracing::{debug, error, trace, warn}; use types::data_column_custody_group::compute_subnets_for_node; -use types::{ChainSpec, DataColumnSubnetId, EthSpec}; +use types::{ChainSpec, DataColumnSubnetId, Epoch, EthSpec, Hash256, Slot}; pub mod client; pub mod peer_info; @@ -735,6 +737,19 @@ impl PeerDB { }, ); + self.update_sync_status( + &peer_id, + SyncStatus::Synced { + // Fill in mock SyncInfo, only for the peer to return `is_synced() == true`. + info: SyncInfo { + head_slot: Slot::new(0), + head_root: Hash256::ZERO, + finalized_epoch: Epoch::new(0), + finalized_root: Hash256::ZERO, + }, + }, + ); + if supernode { let peer_info = self.peers.get_mut(&peer_id).expect("peer exists"); let all_subnets = (0..spec.data_column_sidecar_subnet_count) diff --git a/beacon_node/lighthouse_network/src/rpc/codec.rs b/beacon_node/lighthouse_network/src/rpc/codec.rs index 6e6c6a1dd3..cdf09bd89d 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec.rs @@ -16,12 +16,12 @@ use std::marker::PhantomData; use std::sync::Arc; use tokio_util::codec::{Decoder, Encoder}; use types::{ - BlobSidecar, ChainSpec, DataColumnSidecar, EthSpec, ForkContext, ForkName, Hash256, - LightClientBootstrap, LightClientFinalityUpdate, LightClientOptimisticUpdate, - LightClientUpdate, RuntimeVariableList, SignedBeaconBlock, SignedBeaconBlockAltair, - SignedBeaconBlockBase, SignedBeaconBlockBellatrix, SignedBeaconBlockCapella, - SignedBeaconBlockDeneb, SignedBeaconBlockEip7805, SignedBeaconBlockElectra, - SignedBeaconBlockFulu, + BlobSidecar, ChainSpec, DataColumnSidecar, DataColumnsByRootIdentifier, EthSpec, ForkContext, + ForkName, Hash256, LightClientBootstrap, LightClientFinalityUpdate, + LightClientOptimisticUpdate, LightClientUpdate, RuntimeVariableList, SignedBeaconBlock, + SignedBeaconBlockAltair, SignedBeaconBlockBase, SignedBeaconBlockBellatrix, + SignedBeaconBlockCapella, SignedBeaconBlockDeneb, SignedBeaconBlockEip7805, + SignedBeaconBlockElectra, SignedBeaconBlockFulu, }; use unsigned_varint::codec::Uvi; @@ -600,10 +600,12 @@ fn handle_rpc_request( ))), SupportedProtocol::DataColumnsByRootV1 => Ok(Some(RequestType::DataColumnsByRoot( DataColumnsByRootRequest { - data_column_ids: RuntimeVariableList::from_ssz_bytes( - decoded_buffer, - spec.max_request_data_column_sidecars as usize, - )?, + data_column_ids: + >::from_ssz_bytes_with_nested( + decoded_buffer, + spec.max_request_blocks(current_fork), + spec.number_of_columns as usize, + )?, }, ))), SupportedProtocol::PingV1 => Ok(Some(RequestType::Ping(Ping { @@ -949,8 +951,8 @@ mod tests { use crate::types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield}; use types::{ blob_sidecar::BlobIdentifier, data_column_sidecar::Cell, BeaconBlock, BeaconBlockAltair, - BeaconBlockBase, BeaconBlockBellatrix, BeaconBlockHeader, DataColumnIdentifier, EmptyBlock, - Epoch, FixedBytesExtended, FullPayload, KzgCommitment, KzgProof, Signature, + BeaconBlockBase, BeaconBlockBellatrix, BeaconBlockHeader, DataColumnsByRootIdentifier, + EmptyBlock, Epoch, FixedBytesExtended, FullPayload, KzgCommitment, KzgProof, Signature, SignedBeaconBlockHeader, Slot, }; @@ -1019,10 +1021,7 @@ mod tests { } /// Bellatrix block with length < max_rpc_size. - fn bellatrix_block_small( - fork_context: &ForkContext, - spec: &ChainSpec, - ) -> SignedBeaconBlock { + fn bellatrix_block_small(spec: &ChainSpec) -> SignedBeaconBlock { let mut block: BeaconBlockBellatrix<_, FullPayload> = BeaconBlockBellatrix::empty(&Spec::default_spec()); @@ -1032,17 +1031,14 @@ mod tests { block.body.execution_payload.execution_payload.transactions = txs; let block = BeaconBlock::Bellatrix(block); - assert!(block.ssz_bytes_len() <= max_rpc_size(fork_context, spec.max_chunk_size as usize)); + assert!(block.ssz_bytes_len() <= spec.max_payload_size as usize); SignedBeaconBlock::from_block(block, Signature::empty()) } /// Bellatrix block with length > MAX_RPC_SIZE. /// The max limit for a Bellatrix block is in the order of ~16GiB which wouldn't fit in memory. /// Hence, we generate a Bellatrix block just greater than `MAX_RPC_SIZE` to test rejection on the rpc layer. - fn bellatrix_block_large( - fork_context: &ForkContext, - spec: &ChainSpec, - ) -> SignedBeaconBlock { + fn bellatrix_block_large(spec: &ChainSpec) -> SignedBeaconBlock { let mut block: BeaconBlockBellatrix<_, FullPayload> = BeaconBlockBellatrix::empty(&Spec::default_spec()); @@ -1052,7 +1048,7 @@ mod tests { block.body.execution_payload.execution_payload.transactions = txs; let block = BeaconBlock::Bellatrix(block); - assert!(block.ssz_bytes_len() > max_rpc_size(fork_context, spec.max_chunk_size as usize)); + assert!(block.ssz_bytes_len() > spec.max_payload_size as usize); SignedBeaconBlock::from_block(block, Signature::empty()) } @@ -1089,14 +1085,15 @@ mod tests { } } - fn dcbroot_request(spec: &ChainSpec) -> DataColumnsByRootRequest { + fn dcbroot_request(spec: &ChainSpec, fork_name: ForkName) -> DataColumnsByRootRequest { + let number_of_columns = spec.number_of_columns as usize; DataColumnsByRootRequest { data_column_ids: RuntimeVariableList::new( - vec![DataColumnIdentifier { + vec![DataColumnsByRootIdentifier { block_root: Hash256::zero(), - index: 0, + columns: RuntimeVariableList::from_vec(vec![0, 1, 2], number_of_columns), }], - spec.max_request_data_column_sidecars as usize, + spec.max_request_blocks(fork_name), ) .unwrap(), } @@ -1160,7 +1157,7 @@ mod tests { ) -> Result { let snappy_protocol_id = ProtocolId::new(protocol, Encoding::SSZSnappy); let fork_context = Arc::new(fork_context(fork_name)); - let max_packet_size = max_rpc_size(&fork_context, spec.max_chunk_size as usize); + let max_packet_size = spec.max_payload_size as usize; let mut buf = BytesMut::new(); let mut snappy_inbound_codec = @@ -1207,7 +1204,7 @@ mod tests { ) -> Result>, RPCError> { let snappy_protocol_id = ProtocolId::new(protocol, Encoding::SSZSnappy); let fork_context = Arc::new(fork_context(fork_name)); - let max_packet_size = max_rpc_size(&fork_context, spec.max_chunk_size as usize); + let max_packet_size = spec.max_payload_size as usize; let mut snappy_outbound_codec = SSZSnappyOutboundCodec::::new(snappy_protocol_id, max_packet_size, fork_context); // decode message just as snappy message @@ -1228,7 +1225,7 @@ mod tests { /// Verifies that requests we send are encoded in a way that we would correctly decode too. fn encode_then_decode_request(req: RequestType, fork_name: ForkName, spec: &ChainSpec) { let fork_context = Arc::new(fork_context(fork_name)); - let max_packet_size = max_rpc_size(&fork_context, spec.max_chunk_size as usize); + let max_packet_size = spec.max_payload_size as usize; let protocol = ProtocolId::new(req.versioned_protocol(), Encoding::SSZSnappy); // Encode a request we send let mut buf = BytesMut::new(); @@ -1643,10 +1640,8 @@ mod tests { )))) ); - let bellatrix_block_small = - bellatrix_block_small(&fork_context(ForkName::Bellatrix), &chain_spec); - let bellatrix_block_large = - bellatrix_block_large(&fork_context(ForkName::Bellatrix), &chain_spec); + let bellatrix_block_small = bellatrix_block_small(&chain_spec); + let bellatrix_block_large = bellatrix_block_large(&chain_spec); assert_eq!( encode_then_decode_response( @@ -1967,7 +1962,6 @@ mod tests { RequestType::MetaData(MetadataRequest::new_v1()), RequestType::BlobsByRange(blbrange_request()), RequestType::DataColumnsByRange(dcbrange_request()), - RequestType::DataColumnsByRoot(dcbroot_request(&chain_spec)), RequestType::MetaData(MetadataRequest::new_v2()), ]; for req in requests.iter() { @@ -1983,6 +1977,7 @@ mod tests { RequestType::BlobsByRoot(blbroot_request(fork_name)), RequestType::BlocksByRoot(bbroot_request_v1(fork_name)), RequestType::BlocksByRoot(bbroot_request_v2(fork_name)), + RequestType::DataColumnsByRoot(dcbroot_request(&chain_spec, fork_name)), ] }; for fork_name in ForkName::list_all() { @@ -2146,7 +2141,7 @@ mod tests { // Insert length-prefix uvi_codec - .encode(chain_spec.max_chunk_size as usize + 1, &mut dst) + .encode(chain_spec.max_payload_size as usize + 1, &mut dst) .unwrap(); // Insert snappy stream identifier @@ -2184,7 +2179,7 @@ mod tests { let mut snappy_outbound_codec = SSZSnappyOutboundCodec::::new( snappy_protocol_id, - max_rpc_size(&fork_context, chain_spec.max_chunk_size as usize), + chain_spec.max_payload_size as usize, fork_context, ); @@ -2220,7 +2215,7 @@ mod tests { let mut snappy_outbound_codec = SSZSnappyOutboundCodec::::new( snappy_protocol_id, - max_rpc_size(&fork_context, chain_spec.max_chunk_size as usize), + chain_spec.max_payload_size as usize, fork_context, ); @@ -2249,7 +2244,7 @@ mod tests { let chain_spec = Spec::default_spec(); - let max_rpc_size = max_rpc_size(&fork_context, chain_spec.max_chunk_size as usize); + let max_rpc_size = chain_spec.max_payload_size as usize; let limit = protocol_id.rpc_response_limits::(&fork_context); let mut max = encode_len(limit.max + 1); let mut codec = SSZSnappyOutboundCodec::::new( diff --git a/beacon_node/lighthouse_network/src/rpc/handler.rs b/beacon_node/lighthouse_network/src/rpc/handler.rs index b86e2b3a6f..33c5521c3b 100644 --- a/beacon_node/lighthouse_network/src/rpc/handler.rs +++ b/beacon_node/lighthouse_network/src/rpc/handler.rs @@ -141,7 +141,7 @@ where /// Waker, to be sure the handler gets polled when needed. waker: Option, - /// Timeout that will me used for inbound and outbound responses. + /// Timeout that will be used for inbound and outbound responses. resp_timeout: Duration, } @@ -314,6 +314,7 @@ where } return; }; + // If the response we are sending is an error, report back for handling if let RpcResponse::Error(ref code, ref reason) = response { self.events_out.push(HandlerEvent::Err(HandlerErr::Inbound { @@ -331,6 +332,7 @@ where "Response not sent. Deactivated handler"); return; } + inbound_info.pending_items.push_back(response); } } diff --git a/beacon_node/lighthouse_network/src/rpc/methods.rs b/beacon_node/lighthouse_network/src/rpc/methods.rs index b748ab11c0..9fe2fef9e8 100644 --- a/beacon_node/lighthouse_network/src/rpc/methods.rs +++ b/beacon_node/lighthouse_network/src/rpc/methods.rs @@ -6,7 +6,6 @@ use serde::Serialize; use ssz::Encode; use ssz_derive::{Decode, Encode}; use ssz_types::{typenum::U256, VariableList}; -use std::collections::BTreeMap; use std::fmt::Display; use std::marker::PhantomData; use std::ops::Deref; @@ -16,9 +15,10 @@ use superstruct::superstruct; use types::blob_sidecar::BlobIdentifier; use types::light_client_update::MAX_REQUEST_LIGHT_CLIENT_UPDATES; use types::{ - blob_sidecar::BlobSidecar, ChainSpec, ColumnIndex, DataColumnIdentifier, DataColumnSidecar, - Epoch, EthSpec, Hash256, LightClientBootstrap, LightClientFinalityUpdate, - LightClientOptimisticUpdate, LightClientUpdate, RuntimeVariableList, SignedBeaconBlock, Slot, + blob_sidecar::BlobSidecar, ChainSpec, ColumnIndex, DataColumnSidecar, + DataColumnsByRootIdentifier, Epoch, EthSpec, Hash256, LightClientBootstrap, + LightClientFinalityUpdate, LightClientOptimisticUpdate, LightClientUpdate, RuntimeVariableList, + SignedBeaconBlock, Slot, }; use types::{ForkContext, ForkName}; @@ -479,31 +479,20 @@ impl BlobsByRootRequest { #[derive(Clone, Debug, PartialEq)] pub struct DataColumnsByRootRequest { /// The list of beacon block roots and column indices being requested. - pub data_column_ids: RuntimeVariableList, + pub data_column_ids: RuntimeVariableList, } impl DataColumnsByRootRequest { - pub fn new(data_column_ids: Vec, spec: &ChainSpec) -> Self { - let data_column_ids = RuntimeVariableList::from_vec( - data_column_ids, - spec.max_request_data_column_sidecars as usize, - ); + pub fn new( + data_column_ids: Vec, + max_request_blocks: usize, + ) -> Self { + let data_column_ids = RuntimeVariableList::from_vec(data_column_ids, max_request_blocks); Self { data_column_ids } } - pub fn new_single(block_root: Hash256, index: ColumnIndex, spec: &ChainSpec) -> Self { - Self::new(vec![DataColumnIdentifier { block_root, index }], spec) - } - - pub fn group_by_ordered_block_root(&self) -> Vec<(Hash256, Vec)> { - let mut column_indexes_by_block = BTreeMap::>::new(); - for request_id in self.data_column_ids.as_slice() { - column_indexes_by_block - .entry(request_id.block_root) - .or_default() - .push(request_id.index); - } - column_indexes_by_block.into_iter().collect() + pub fn max_requested(&self) -> usize { + self.data_column_ids.iter().map(|id| id.columns.len()).sum() } } @@ -606,6 +595,20 @@ pub enum ResponseTermination { LightClientUpdatesByRange, } +impl ResponseTermination { + pub fn as_protocol(&self) -> Protocol { + match self { + ResponseTermination::BlocksByRange => Protocol::BlocksByRange, + ResponseTermination::BlocksByRoot => Protocol::BlocksByRoot, + ResponseTermination::BlobsByRange => Protocol::BlobsByRange, + ResponseTermination::BlobsByRoot => Protocol::BlobsByRoot, + ResponseTermination::DataColumnsByRoot => Protocol::DataColumnsByRoot, + ResponseTermination::DataColumnsByRange => Protocol::DataColumnsByRange, + ResponseTermination::LightClientUpdatesByRange => Protocol::LightClientUpdatesByRange, + } + } +} + /// The structured response containing a result/code indicating success or failure /// and the contents of the response #[derive(Debug, Clone)] diff --git a/beacon_node/lighthouse_network/src/rpc/mod.rs b/beacon_node/lighthouse_network/src/rpc/mod.rs index 1156447d56..8cb720132a 100644 --- a/beacon_node/lighthouse_network/src/rpc/mod.rs +++ b/beacon_node/lighthouse_network/src/rpc/mod.rs @@ -4,7 +4,6 @@ //! direct peer-to-peer communication primarily for sending/receiving chain information for //! syncing. -use futures::future::FutureExt; use handler::RPCHandler; use libp2p::core::transport::PortUse; use libp2p::swarm::{ @@ -13,13 +12,12 @@ use libp2p::swarm::{ }; use libp2p::swarm::{ConnectionClosed, FromSwarm, SubstreamProtocol, THandlerInEvent}; use libp2p::PeerId; -use logging::crit; -use rate_limiter::{RPCRateLimiter as RateLimiter, RateLimitedErr}; +use std::collections::HashMap; use std::marker::PhantomData; use std::sync::Arc; use std::task::{Context, Poll}; use std::time::Duration; -use tracing::{debug, instrument, trace}; +use tracing::{debug, error, instrument, trace}; use types::{EthSpec, ForkContext}; pub(crate) use handler::{HandlerErr, HandlerEvent}; @@ -28,16 +26,17 @@ pub(crate) use methods::{ }; pub use protocol::RequestType; +use self::config::{InboundRateLimiterConfig, OutboundRateLimiterConfig}; +use self::protocol::RPCProtocol; +use self::self_limiter::SelfRateLimiter; +use crate::rpc::rate_limiter::RateLimiterItem; +use crate::rpc::response_limiter::ResponseLimiter; pub use handler::SubstreamId; pub use methods::{ BlocksByRangeRequest, BlocksByRootRequest, GoodbyeReason, LightClientBootstrapRequest, ResponseTermination, RpcErrorResponse, StatusMessage, }; -pub use protocol::{max_rpc_size, Protocol, RPCError}; - -use self::config::{InboundRateLimiterConfig, OutboundRateLimiterConfig}; -use self::protocol::RPCProtocol; -use self::self_limiter::SelfRateLimiter; +pub use protocol::{Protocol, RPCError}; pub(crate) mod codec; pub mod config; @@ -46,8 +45,12 @@ pub mod methods; mod outbound; mod protocol; mod rate_limiter; +mod response_limiter; mod self_limiter; +// Maximum number of concurrent requests per protocol ID that a client may issue. +const MAX_CONCURRENT_REQUESTS: usize = 2; + /// Composite trait for a request id. pub trait ReqId: Send + 'static + std::fmt::Debug + Copy + Clone {} impl ReqId for T where T: Send + 'static + std::fmt::Debug + Copy + Clone {} @@ -136,7 +139,7 @@ pub struct RPCMessage { type BehaviourAction = ToSwarm, RPCSend>; pub struct NetworkParams { - pub max_chunk_size: usize, + pub max_payload_size: usize, pub ttfb_timeout: Duration, pub resp_timeout: Duration, } @@ -144,10 +147,12 @@ pub struct NetworkParams { /// Implements the libp2p `NetworkBehaviour` trait and therefore manages network-level /// logic. pub struct RPC { - /// Rate limiter - limiter: Option, + /// Rate limiter for our responses. + response_limiter: Option>, /// Rate limiter for our own requests. - self_limiter: Option>, + outbound_request_limiter: SelfRateLimiter, + /// Active inbound requests that are awaiting a response. + active_inbound_requests: HashMap)>, /// Queue of events to be processed. events: Vec>, fork_context: Arc, @@ -173,20 +178,20 @@ impl RPC { network_params: NetworkParams, seq_number: u64, ) -> Self { - let inbound_limiter = inbound_rate_limiter_config.map(|config| { - debug!(?config, "Using inbound rate limiting params"); - RateLimiter::new_with_config(config.0, fork_context.clone()) + let response_limiter = inbound_rate_limiter_config.map(|config| { + debug!(?config, "Using response rate limiting params"); + ResponseLimiter::new(config, fork_context.clone()) .expect("Inbound limiter configuration parameters are valid") }); - let self_limiter = outbound_rate_limiter_config.map(|config| { - SelfRateLimiter::new(config, fork_context.clone()) - .expect("Configuration parameters are valid") - }); + let outbound_request_limiter: SelfRateLimiter = + SelfRateLimiter::new(outbound_rate_limiter_config, fork_context.clone()) + .expect("Outbound limiter configuration parameters are valid"); RPC { - limiter: inbound_limiter, - self_limiter, + response_limiter, + outbound_request_limiter, + active_inbound_requests: HashMap::new(), events: Vec::new(), fork_context, enable_light_client_server, @@ -210,6 +215,44 @@ impl RPC { request_id: InboundRequestId, response: RpcResponse, ) { + let Some((_peer_id, request_type)) = self.active_inbound_requests.remove(&request_id) + else { + error!(%peer_id, ?request_id, %response, "Request not found in active_inbound_requests. Response not sent"); + return; + }; + + // Add the request back to active requests if the response is `Success` and requires stream + // termination. + if request_type.protocol().terminator().is_some() + && matches!(response, RpcResponse::Success(_)) + { + self.active_inbound_requests + .insert(request_id, (peer_id, request_type.clone())); + } + + self.send_response_inner(peer_id, request_type.protocol(), request_id, response); + } + + fn send_response_inner( + &mut self, + peer_id: PeerId, + protocol: Protocol, + request_id: InboundRequestId, + response: RpcResponse, + ) { + if let Some(response_limiter) = self.response_limiter.as_mut() { + if !response_limiter.allows( + peer_id, + protocol, + request_id.connection_id, + request_id.substream_id, + response.clone(), + ) { + // Response is logged and queued internally in the response limiter. + return; + } + } + self.events.push(ToSwarm::NotifyHandler { peer_id, handler: NotifyHandler::One(request_id.connection_id), @@ -227,23 +270,19 @@ impl RPC { skip_all )] pub fn send_request(&mut self, peer_id: PeerId, request_id: Id, req: RequestType) { - let event = if let Some(self_limiter) = self.self_limiter.as_mut() { - match self_limiter.allows(peer_id, request_id, req) { - Ok(event) => event, - Err(_e) => { - // Request is logged and queued internally in the self rate limiter. - return; - } + match self + .outbound_request_limiter + .allows(peer_id, request_id, req) + { + Ok(event) => self.events.push(BehaviourAction::NotifyHandler { + peer_id, + handler: NotifyHandler::Any, + event, + }), + Err(_e) => { + // Request is logged and queued internally in the self rate limiter. } - } else { - RPCSend::Request(request_id, req) - }; - - self.events.push(BehaviourAction::NotifyHandler { - peer_id, - handler: NotifyHandler::Any, - event, - }); + } } /// Lighthouse wishes to disconnect from this peer by sending a Goodbye message. This @@ -306,7 +345,7 @@ where let protocol = SubstreamProtocol::new( RPCProtocol { fork_context: self.fork_context.clone(), - max_rpc_size: max_rpc_size(&self.fork_context, self.network_params.max_chunk_size), + max_rpc_size: self.fork_context.spec.max_payload_size as usize, enable_light_client_server: self.enable_light_client_server, phantom: PhantomData, ttfb_timeout: self.network_params.ttfb_timeout, @@ -336,7 +375,7 @@ where let protocol = SubstreamProtocol::new( RPCProtocol { fork_context: self.fork_context.clone(), - max_rpc_size: max_rpc_size(&self.fork_context, self.network_params.max_chunk_size), + max_rpc_size: self.fork_context.spec.max_payload_size as usize, enable_light_client_server: self.enable_light_client_server, phantom: PhantomData, ttfb_timeout: self.network_params.ttfb_timeout, @@ -373,20 +412,27 @@ where if remaining_established > 0 { return; } + // Get a list of pending requests from the self rate limiter - if let Some(limiter) = self.self_limiter.as_mut() { - for (id, proto) in limiter.peer_disconnected(peer_id) { - let error_msg = ToSwarm::GenerateEvent(RPCMessage { - peer_id, - connection_id, - message: Err(HandlerErr::Outbound { - id, - proto, - error: RPCError::Disconnected, - }), - }); - self.events.push(error_msg); - } + for (id, proto) in self.outbound_request_limiter.peer_disconnected(peer_id) { + let error_msg = ToSwarm::GenerateEvent(RPCMessage { + peer_id, + connection_id, + message: Err(HandlerErr::Outbound { + id, + proto, + error: RPCError::Disconnected, + }), + }); + self.events.push(error_msg); + } + + self.active_inbound_requests.retain( + |_inbound_request_id, (request_peer_id, _request_type)| *request_peer_id != peer_id, + ); + + if let Some(limiter) = self.response_limiter.as_mut() { + limiter.peer_disconnected(peer_id); } // Replace the pending Requests to the disconnected peer @@ -420,57 +466,39 @@ where ) { match event { HandlerEvent::Ok(RPCReceived::Request(request_id, request_type)) => { - if let Some(limiter) = self.limiter.as_mut() { - // check if the request is conformant to the quota - match limiter.allows(&peer_id, &request_type) { - Err(RateLimitedErr::TooLarge) => { - // we set the batch sizes, so this is a coding/config err for most protocols - let protocol = request_type.versioned_protocol().protocol(); - if matches!( - protocol, - Protocol::BlocksByRange - | Protocol::BlobsByRange - | Protocol::DataColumnsByRange - | Protocol::BlocksByRoot - | Protocol::BlobsByRoot - | Protocol::DataColumnsByRoot - ) { - debug!(request = %request_type, %protocol, "Request too large to process"); - } else { - // Other protocols shouldn't be sending large messages, we should flag the peer kind - crit!(%protocol, "Request size too large to ever be processed"); - } - // send an error code to the peer. - // the handler upon receiving the error code will send it back to the behaviour - self.send_response( - peer_id, - request_id, - RpcResponse::Error( - RpcErrorResponse::RateLimited, - "Rate limited. Request too large".into(), - ), - ); - return; - } - Err(RateLimitedErr::TooSoon(wait_time)) => { - debug!(request = %request_type, %peer_id, wait_time_ms = wait_time.as_millis(), "Request exceeds the rate limit"); - // send an error code to the peer. - // the handler upon receiving the error code will send it back to the behaviour - self.send_response( - peer_id, - request_id, - RpcResponse::Error( - RpcErrorResponse::RateLimited, - format!("Wait {:?}", wait_time).into(), - ), - ); - return; - } - // No rate limiting, continue. - Ok(()) => {} - } + let is_concurrent_request_limit_exceeded = self + .active_inbound_requests + .iter() + .filter( + |(_inbound_request_id, (request_peer_id, active_request_type))| { + *request_peer_id == peer_id + && active_request_type.protocol() == request_type.protocol() + }, + ) + .count() + >= MAX_CONCURRENT_REQUESTS; + + // Restricts more than MAX_CONCURRENT_REQUESTS inbound requests from running simultaneously on the same protocol per peer. + if is_concurrent_request_limit_exceeded { + // There is already an active request with the same protocol. Send an error code to the peer. + debug!(request = %request_type, protocol = %request_type.protocol(), %peer_id, "There is an active request with the same protocol"); + self.send_response_inner( + peer_id, + request_type.protocol(), + request_id, + RpcResponse::Error( + RpcErrorResponse::RateLimited, + format!("Rate limited. There are already {MAX_CONCURRENT_REQUESTS} active requests with the same protocol") + .into(), + ), + ); + return; } + // Requests that are below the limit on the number of simultaneous requests are added to the active inbound requests. + self.active_inbound_requests + .insert(request_id, (peer_id, request_type.clone())); + // If we received a Ping, we queue a Pong response. if let RequestType::Ping(_) = request_type { trace!(connection_id = %connection_id, %peer_id, "Received Ping, queueing Pong"); @@ -489,14 +517,38 @@ where message: Ok(RPCReceived::Request(request_id, request_type)), })); } - HandlerEvent::Ok(rpc) => { + HandlerEvent::Ok(RPCReceived::Response(id, response)) => { + if response.protocol().terminator().is_none() { + // Inform the limiter that a response has been received. + self.outbound_request_limiter + .request_completed(&peer_id, response.protocol()); + } + self.events.push(ToSwarm::GenerateEvent(RPCMessage { peer_id, connection_id, - message: Ok(rpc), + message: Ok(RPCReceived::Response(id, response)), + })); + } + HandlerEvent::Ok(RPCReceived::EndOfStream(id, response_termination)) => { + // Inform the limiter that a response has been received. + self.outbound_request_limiter + .request_completed(&peer_id, response_termination.as_protocol()); + + self.events.push(ToSwarm::GenerateEvent(RPCMessage { + peer_id, + connection_id, + message: Ok(RPCReceived::EndOfStream(id, response_termination)), })); } HandlerEvent::Err(err) => { + // Inform the limiter that the request has ended with an error. + let protocol = match err { + HandlerErr::Inbound { proto, .. } | HandlerErr::Outbound { proto, .. } => proto, + }; + self.outbound_request_limiter + .request_completed(&peer_id, protocol); + self.events.push(ToSwarm::GenerateEvent(RPCMessage { peer_id, connection_id, @@ -514,15 +566,20 @@ where } fn poll(&mut self, cx: &mut Context) -> Poll>> { - // let the rate limiter prune. - if let Some(limiter) = self.limiter.as_mut() { - let _ = limiter.poll_unpin(cx); + if let Some(response_limiter) = self.response_limiter.as_mut() { + if let Poll::Ready(responses) = response_limiter.poll_ready(cx) { + for response in responses { + self.events.push(ToSwarm::NotifyHandler { + peer_id: response.peer_id, + handler: NotifyHandler::One(response.connection_id), + event: RPCSend::Response(response.substream_id, response.response), + }); + } + } } - if let Some(self_limiter) = self.self_limiter.as_mut() { - if let Poll::Ready(event) = self_limiter.poll_ready(cx) { - self.events.push(event) - } + if let Poll::Ready(event) = self.outbound_request_limiter.poll_ready(cx) { + self.events.push(event) } if !self.events.is_empty() { diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index 524a639538..44d0a5b04d 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -57,7 +57,7 @@ pub static SIGNED_BEACON_BLOCK_ALTAIR_MAX: LazyLock = LazyLock::new(|| { /// The `BeaconBlockBellatrix` block has an `ExecutionPayload` field which has a max size ~16 GiB for future proofing. /// We calculate the value from its fields instead of constructing the block and checking the length. /// Note: This is only the theoretical upper bound. We further bound the max size we receive over the network -/// with `max_chunk_size`. +/// with `max_payload_size`. pub static SIGNED_BEACON_BLOCK_BELLATRIX_MAX: LazyLock = LazyLock::new(|| // Size of a full altair block *SIGNED_BEACON_BLOCK_ALTAIR_MAX @@ -122,15 +122,6 @@ const PROTOCOL_PREFIX: &str = "/eth2/beacon_chain/req"; /// established before the stream is terminated. const REQUEST_TIMEOUT: u64 = 15; -/// Returns the maximum bytes that can be sent across the RPC. -pub fn max_rpc_size(fork_context: &ForkContext, max_chunk_size: usize) -> usize { - if fork_context.current_fork().bellatrix_enabled() { - max_chunk_size - } else { - max_chunk_size / 10 - } -} - /// Returns the rpc limits for beacon_block_by_range and beacon_block_by_root responses. /// /// Note: This function should take care to return the min/max limits accounting for all @@ -749,7 +740,7 @@ impl RequestType { RequestType::BlocksByRoot(req) => req.block_roots().len() as u64, RequestType::BlobsByRange(req) => req.max_blobs_requested(current_fork, spec), RequestType::BlobsByRoot(req) => req.blob_ids.len() as u64, - RequestType::DataColumnsByRoot(req) => req.data_column_ids.len() as u64, + RequestType::DataColumnsByRoot(req) => req.max_requested() as u64, RequestType::DataColumnsByRange(req) => req.max_requested::(), RequestType::Ping(_) => 1, RequestType::MetaData(_) => 1, diff --git a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs index b9e82a5f1e..f666c30d52 100644 --- a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs @@ -149,7 +149,7 @@ pub struct RPCRateLimiterBuilder { lcbootstrap_quota: Option, /// Quota for the LightClientOptimisticUpdate protocol. lc_optimistic_update_quota: Option, - /// Quota for the LightClientOptimisticUpdate protocol. + /// Quota for the LightClientFinalityUpdate protocol. lc_finality_update_quota: Option, /// Quota for the LightClientUpdatesByRange protocol. lc_updates_by_range_quota: Option, @@ -275,6 +275,17 @@ impl RateLimiterItem for super::RequestType { } } +impl RateLimiterItem for (super::RpcResponse, Protocol) { + fn protocol(&self) -> Protocol { + self.1 + } + + fn max_responses(&self, _current_fork: ForkName, _spec: &ChainSpec) -> u64 { + // A response chunk consumes one token of the rate limiter. + 1 + } +} + impl RPCRateLimiter { pub fn new_with_config( config: RateLimiterConfig, diff --git a/beacon_node/lighthouse_network/src/rpc/response_limiter.rs b/beacon_node/lighthouse_network/src/rpc/response_limiter.rs new file mode 100644 index 0000000000..c583baaadd --- /dev/null +++ b/beacon_node/lighthouse_network/src/rpc/response_limiter.rs @@ -0,0 +1,177 @@ +use crate::rpc::config::InboundRateLimiterConfig; +use crate::rpc::rate_limiter::{RPCRateLimiter, RateLimitedErr}; +use crate::rpc::self_limiter::timestamp_now; +use crate::rpc::{Protocol, RpcResponse, SubstreamId}; +use crate::PeerId; +use futures::FutureExt; +use libp2p::swarm::ConnectionId; +use logging::crit; +use std::collections::hash_map::Entry; +use std::collections::{HashMap, VecDeque}; +use std::sync::Arc; +use std::task::{Context, Poll}; +use std::time::Duration; +use tokio_util::time::DelayQueue; +use tracing::debug; +use types::{EthSpec, ForkContext}; + +/// A response that was rate limited or waiting on rate limited responses for the same peer and +/// protocol. +#[derive(Clone)] +pub(super) struct QueuedResponse { + pub peer_id: PeerId, + pub connection_id: ConnectionId, + pub substream_id: SubstreamId, + pub response: RpcResponse, + pub protocol: Protocol, + pub queued_at: Duration, +} + +pub(super) struct ResponseLimiter { + /// Rate limiter for our responses. + limiter: RPCRateLimiter, + /// Responses queued for sending. These responses are stored when the response limiter rejects them. + delayed_responses: HashMap<(PeerId, Protocol), VecDeque>>, + /// The delay required to allow a peer's outbound response per protocol. + next_response: DelayQueue<(PeerId, Protocol)>, +} + +impl ResponseLimiter { + /// Creates a new [`ResponseLimiter`] based on configuration values. + pub fn new( + config: InboundRateLimiterConfig, + fork_context: Arc, + ) -> Result { + Ok(ResponseLimiter { + limiter: RPCRateLimiter::new_with_config(config.0, fork_context)?, + delayed_responses: HashMap::new(), + next_response: DelayQueue::new(), + }) + } + + /// Checks if the rate limiter allows the response. When not allowed, the response is delayed + /// until it can be sent. + pub fn allows( + &mut self, + peer_id: PeerId, + protocol: Protocol, + connection_id: ConnectionId, + substream_id: SubstreamId, + response: RpcResponse, + ) -> bool { + // First check that there are not already other responses waiting to be sent. + if let Some(queue) = self.delayed_responses.get_mut(&(peer_id, protocol)) { + debug!(%peer_id, %protocol, "Response rate limiting since there are already other responses waiting to be sent"); + queue.push_back(QueuedResponse { + peer_id, + connection_id, + substream_id, + response, + protocol, + queued_at: timestamp_now(), + }); + return false; + } + + if let Err(wait_time) = + Self::try_limiter(&mut self.limiter, peer_id, response.clone(), protocol) + { + self.delayed_responses + .entry((peer_id, protocol)) + .or_default() + .push_back(QueuedResponse { + peer_id, + connection_id, + substream_id, + response, + protocol, + queued_at: timestamp_now(), + }); + self.next_response.insert((peer_id, protocol), wait_time); + return false; + } + + true + } + + /// Checks if the limiter allows the response. If the response should be delayed, the duration + /// to wait is returned. + fn try_limiter( + limiter: &mut RPCRateLimiter, + peer_id: PeerId, + response: RpcResponse, + protocol: Protocol, + ) -> Result<(), Duration> { + match limiter.allows(&peer_id, &(response.clone(), protocol)) { + Ok(()) => Ok(()), + Err(e) => match e { + RateLimitedErr::TooLarge => { + // This should never happen with default parameters. Let's just send the response. + // Log a crit since this is a config issue. + crit!( + %protocol, + "Response rate limiting error for a batch that will never fit. Sending response anyway. Check configuration parameters." + ); + Ok(()) + } + RateLimitedErr::TooSoon(wait_time) => { + debug!(%peer_id, %protocol, wait_time_ms = wait_time.as_millis(), "Response rate limiting"); + Err(wait_time) + } + }, + } + } + + /// Informs the limiter that a peer has disconnected. This removes any pending responses. + pub fn peer_disconnected(&mut self, peer_id: PeerId) { + self.delayed_responses + .retain(|(map_peer_id, _protocol), _queue| map_peer_id != &peer_id); + } + + /// When a peer and protocol are allowed to send a next response, this function checks the + /// queued responses and attempts marking as ready as many as the limiter allows. + pub fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll>> { + let mut responses = vec![]; + while let Poll::Ready(Some(expired)) = self.next_response.poll_expired(cx) { + let (peer_id, protocol) = expired.into_inner(); + + if let Entry::Occupied(mut entry) = self.delayed_responses.entry((peer_id, protocol)) { + let queue = entry.get_mut(); + // Take delayed responses from the queue, as long as the limiter allows it. + while let Some(response) = queue.pop_front() { + match Self::try_limiter( + &mut self.limiter, + response.peer_id, + response.response.clone(), + response.protocol, + ) { + Ok(()) => { + metrics::observe_duration( + &crate::metrics::RESPONSE_IDLING, + timestamp_now().saturating_sub(response.queued_at), + ); + responses.push(response) + } + Err(wait_time) => { + // The response was taken from the queue, but the limiter didn't allow it. + queue.push_front(response); + self.next_response.insert((peer_id, protocol), wait_time); + break; + } + } + } + if queue.is_empty() { + entry.remove(); + } + } + } + + // Prune the rate limiter. + let _ = self.limiter.poll_unpin(cx); + + if !responses.is_empty() { + return Poll::Ready(responses); + } + Poll::Pending + } +} diff --git a/beacon_node/lighthouse_network/src/rpc/self_limiter.rs b/beacon_node/lighthouse_network/src/rpc/self_limiter.rs index e4af977a6c..e5b685676f 100644 --- a/beacon_node/lighthouse_network/src/rpc/self_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/self_limiter.rs @@ -1,3 +1,10 @@ +use super::{ + config::OutboundRateLimiterConfig, + rate_limiter::{RPCRateLimiter as RateLimiter, RateLimitedErr}, + BehaviourAction, Protocol, RPCSend, ReqId, RequestType, MAX_CONCURRENT_REQUESTS, +}; +use crate::rpc::rate_limiter::RateLimiterItem; +use std::time::{SystemTime, UNIX_EPOCH}; use std::{ collections::{hash_map::Entry, HashMap, VecDeque}, sync::Arc, @@ -13,30 +20,31 @@ use tokio_util::time::DelayQueue; use tracing::debug; use types::{EthSpec, ForkContext}; -use super::{ - config::OutboundRateLimiterConfig, - rate_limiter::{RPCRateLimiter as RateLimiter, RateLimitedErr}, - BehaviourAction, Protocol, RPCSend, ReqId, RequestType, -}; - /// A request that was rate limited or waiting on rate limited requests for the same peer and /// protocol. struct QueuedRequest { req: RequestType, request_id: Id, + queued_at: Duration, } +/// The number of milliseconds requests delayed due to the concurrent request limit stay in the queue. +const WAIT_TIME_DUE_TO_CONCURRENT_REQUESTS: u64 = 100; + +#[allow(clippy::type_complexity)] pub(crate) struct SelfRateLimiter { - /// Requests queued for sending per peer. This requests are stored when the self rate + /// Active requests that are awaiting a response. + active_requests: HashMap>, + /// Requests queued for sending per peer. These 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>>, /// 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, + rate_limiter: Option, /// Requests that are ready to be sent. - ready_requests: SmallVec<[(PeerId, RPCSend); 3]>, + ready_requests: SmallVec<[(PeerId, RPCSend, Duration); 3]>, } /// Error returned when the rate limiter does not accept a request. @@ -49,18 +57,23 @@ pub enum Error { } impl SelfRateLimiter { - /// Creates a new [`SelfRateLimiter`] based on configration values. + /// Creates a new [`SelfRateLimiter`] based on configuration values. pub fn new( - config: OutboundRateLimiterConfig, + config: Option, fork_context: Arc, ) -> Result { debug!(?config, "Using self rate limiting params"); - let limiter = RateLimiter::new_with_config(config.0, fork_context)?; + let rate_limiter = if let Some(c) = config { + Some(RateLimiter::new_with_config(c.0, fork_context)?) + } else { + None + }; Ok(SelfRateLimiter { + active_requests: Default::default(), delayed_requests: Default::default(), next_peer_request: Default::default(), - limiter, + rate_limiter, ready_requests: Default::default(), }) } @@ -77,11 +90,21 @@ impl SelfRateLimiter { let protocol = req.versioned_protocol().protocol(); // First check that there are not already other requests waiting to be sent. if let Some(queued_requests) = self.delayed_requests.get_mut(&(peer_id, protocol)) { - queued_requests.push_back(QueuedRequest { req, request_id }); - + debug!(%peer_id, protocol = %req.protocol(), "Self rate limiting since there are already other requests waiting to be sent"); + queued_requests.push_back(QueuedRequest { + req, + request_id, + queued_at: timestamp_now(), + }); return Err(Error::PendingRequests); } - match Self::try_send_request(&mut self.limiter, peer_id, request_id, req) { + match Self::try_send_request( + &mut self.active_requests, + &mut self.rate_limiter, + peer_id, + request_id, + req, + ) { Err((rate_limited_req, wait_time)) => { let key = (peer_id, protocol); self.next_peer_request.insert(key, wait_time); @@ -99,33 +122,71 @@ impl SelfRateLimiter { /// Auxiliary function to deal with self rate limiting outcomes. If the rate limiter allows the /// request, the [`ToSwarm`] that should be emitted is returned. If the request /// should be delayed, it's returned with the duration to wait. + #[allow(clippy::result_large_err)] fn try_send_request( - limiter: &mut RateLimiter, + active_requests: &mut HashMap>, + rate_limiter: &mut Option, peer_id: PeerId, request_id: Id, req: RequestType, ) -> Result, (QueuedRequest, Duration)> { - match limiter.allows(&peer_id, &req) { - Ok(()) => Ok(RPCSend::Request(request_id, req)), - Err(e) => { - let protocol = req.versioned_protocol(); - match e { - RateLimitedErr::TooLarge => { - // this should never happen with default parameters. Let's just send the request. - // Log a crit since this is a config issue. - crit!( - protocol = %req.versioned_protocol().protocol(), - "Self rate limiting error for a batch that will never fit. Sending request anyway. Check configuration parameters." - ); - Ok(RPCSend::Request(request_id, req)) - } - RateLimitedErr::TooSoon(wait_time) => { - debug!(protocol = %protocol.protocol(), wait_time_ms = wait_time.as_millis(), %peer_id, "Self rate limiting"); - Err((QueuedRequest { req, request_id }, wait_time)) + if let Some(active_request) = active_requests.get(&peer_id) { + if let Some(count) = active_request.get(&req.protocol()) { + if *count >= MAX_CONCURRENT_REQUESTS { + debug!( + %peer_id, + protocol = %req.protocol(), + "Self rate limiting due to the number of concurrent requests" + ); + return Err(( + QueuedRequest { + req, + request_id, + queued_at: timestamp_now(), + }, + Duration::from_millis(WAIT_TIME_DUE_TO_CONCURRENT_REQUESTS), + )); + } + } + } + + if let Some(limiter) = rate_limiter.as_mut() { + match limiter.allows(&peer_id, &req) { + Ok(()) => {} + Err(e) => { + let protocol = req.versioned_protocol(); + match e { + RateLimitedErr::TooLarge => { + // this should never happen with default parameters. Let's just send the request. + // Log a crit since this is a config issue. + crit!( + protocol = %req.versioned_protocol().protocol(), + "Self rate limiting error for a batch that will never fit. Sending request anyway. Check configuration parameters.", + ); + } + RateLimitedErr::TooSoon(wait_time) => { + debug!(protocol = %protocol.protocol(), wait_time_ms = wait_time.as_millis(), %peer_id, "Self rate limiting"); + return Err(( + QueuedRequest { + req, + request_id, + queued_at: timestamp_now(), + }, + wait_time, + )); + } } } } } + + *active_requests + .entry(peer_id) + .or_default() + .entry(req.protocol()) + .or_default() += 1; + + Ok(RPCSend::Request(request_id, req)) } /// When a peer and protocol are allowed to send a next request, this function checks the @@ -133,16 +194,32 @@ impl SelfRateLimiter { fn next_peer_request_ready(&mut self, peer_id: PeerId, protocol: Protocol) { if let Entry::Occupied(mut entry) = self.delayed_requests.entry((peer_id, protocol)) { let queued_requests = entry.get_mut(); - while let Some(QueuedRequest { req, request_id }) = queued_requests.pop_front() { - match Self::try_send_request(&mut self.limiter, peer_id, request_id, req) { - Err((rate_limited_req, wait_time)) => { + while let Some(QueuedRequest { + req, + request_id, + queued_at, + }) = queued_requests.pop_front() + { + match Self::try_send_request( + &mut self.active_requests, + &mut self.rate_limiter, + peer_id, + request_id, + req.clone(), + ) { + Err((_rate_limited_req, wait_time)) => { let key = (peer_id, protocol); self.next_peer_request.insert(key, wait_time); - queued_requests.push_front(rate_limited_req); + // Don't push `rate_limited_req` here to prevent `queued_at` from being updated. + queued_requests.push_front(QueuedRequest { + req, + request_id, + queued_at, + }); // If one fails just wait for the next window that allows sending requests. return; } - Ok(event) => self.ready_requests.push((peer_id, event)), + Ok(event) => self.ready_requests.push((peer_id, event, queued_at)), } } if queued_requests.is_empty() { @@ -156,6 +233,8 @@ impl SelfRateLimiter { /// Informs the limiter that a peer has disconnected. This removes any pending requests and /// returns their IDs. pub fn peer_disconnected(&mut self, peer_id: PeerId) -> Vec<(Id, Protocol)> { + self.active_requests.remove(&peer_id); + // It's not ideal to iterate this map, but the key is (PeerId, Protocol) and this map // should never really be large. So we iterate for simplicity let mut failed_requests = Vec::new(); @@ -177,19 +256,39 @@ impl SelfRateLimiter { failed_requests } + /// Informs the limiter that a response has been received. + pub fn request_completed(&mut self, peer_id: &PeerId, protocol: Protocol) { + if let Some(active_requests) = self.active_requests.get_mut(peer_id) { + if let Entry::Occupied(mut entry) = active_requests.entry(protocol) { + if *entry.get() > 1 { + *entry.get_mut() -= 1; + } else { + entry.remove(); + } + } + } + } + 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 + // the queue. Also do this before rate limiter pruning to avoid removing and // immediately adding rate limiting keys. if let Poll::Ready(Some(expired)) = self.next_peer_request.poll_expired(cx) { let (peer_id, protocol) = expired.into_inner(); self.next_peer_request_ready(peer_id, protocol); } + // Prune the rate limiter. - let _ = self.limiter.poll_unpin(cx); + if let Some(limiter) = self.rate_limiter.as_mut() { + let _ = limiter.poll_unpin(cx); + } // Finally return any queued events. - if let Some((peer_id, event)) = self.ready_requests.pop() { + if let Some((peer_id, event, queued_at)) = self.ready_requests.pop() { + metrics::observe_duration( + &crate::metrics::OUTBOUND_REQUEST_IDLING, + timestamp_now().saturating_sub(queued_at), + ); return Poll::Ready(BehaviourAction::NotifyHandler { peer_id, handler: NotifyHandler::Any, @@ -201,12 +300,19 @@ impl SelfRateLimiter { } } +/// Returns the duration since the unix epoch. +pub fn timestamp_now() -> Duration { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_else(|_| Duration::from_secs(0)) +} + #[cfg(test)] mod tests { use crate::rpc::config::{OutboundRateLimiterConfig, RateLimiterConfig}; use crate::rpc::rate_limiter::Quota; use crate::rpc::self_limiter::SelfRateLimiter; - use crate::rpc::{Ping, Protocol, RequestType}; + use crate::rpc::{Ping, Protocol, RPCSend, RequestType}; use crate::service::api_types::{AppRequestId, SingleLookupReqId, SyncRequestId}; use libp2p::PeerId; use logging::create_test_tracing_subscriber; @@ -227,7 +333,7 @@ mod tests { &MainnetEthSpec::default_spec(), )); let mut limiter: SelfRateLimiter = - SelfRateLimiter::new(config, fork_context).unwrap(); + SelfRateLimiter::new(Some(config), fork_context).unwrap(); let peer_id = PeerId::random(); let lookup_id = 0; @@ -290,4 +396,149 @@ mod tests { assert_eq!(limiter.ready_requests.len(), 1); } } + + /// Test that `next_peer_request_ready` correctly maintains the queue when using the self-limiter without rate limiting. + #[tokio::test] + async fn test_next_peer_request_ready_concurrent_requests() { + let fork_context = std::sync::Arc::new(ForkContext::new::( + Slot::new(0), + Hash256::ZERO, + &MainnetEthSpec::default_spec(), + )); + let mut limiter: SelfRateLimiter = + SelfRateLimiter::new(None, fork_context).unwrap(); + let peer_id = PeerId::random(); + + for i in 1..=5u32 { + let result = limiter.allows( + peer_id, + AppRequestId::Sync(SyncRequestId::SingleBlock { + id: SingleLookupReqId { + lookup_id: i, + req_id: i, + }, + }), + RequestType::Ping(Ping { data: i as u64 }), + ); + + // Check that the limiter allows the first two requests. + if i <= 2 { + assert!(result.is_ok()); + } else { + assert!(result.is_err()); + } + } + + let queue = limiter + .delayed_requests + .get(&(peer_id, Protocol::Ping)) + .unwrap(); + assert_eq!(3, queue.len()); + + // The delayed requests remain even after the next_peer_request_ready call because the responses have not been received. + limiter.next_peer_request_ready(peer_id, Protocol::Ping); + let queue = limiter + .delayed_requests + .get(&(peer_id, Protocol::Ping)) + .unwrap(); + assert_eq!(3, queue.len()); + + limiter.request_completed(&peer_id, Protocol::Ping); + limiter.next_peer_request_ready(peer_id, Protocol::Ping); + + let queue = limiter + .delayed_requests + .get(&(peer_id, Protocol::Ping)) + .unwrap(); + assert_eq!(2, queue.len()); + + limiter.request_completed(&peer_id, Protocol::Ping); + limiter.request_completed(&peer_id, Protocol::Ping); + limiter.next_peer_request_ready(peer_id, Protocol::Ping); + + let queue = limiter.delayed_requests.get(&(peer_id, Protocol::Ping)); + assert!(queue.is_none()); + + // Check that the three delayed requests have moved to ready_requests. + let mut it = limiter.ready_requests.iter(); + for i in 3..=5u32 { + let (_peer_id, RPCSend::Request(request_id, _), _) = it.next().unwrap() else { + unreachable!() + }; + + assert!(matches!( + request_id, + AppRequestId::Sync(SyncRequestId::SingleBlock { + id: SingleLookupReqId { req_id, .. }, + }) if *req_id == i + )); + } + } + + #[tokio::test] + async fn test_peer_disconnected() { + let fork_context = std::sync::Arc::new(ForkContext::new::( + Slot::new(0), + Hash256::ZERO, + &MainnetEthSpec::default_spec(), + )); + let mut limiter: SelfRateLimiter = + SelfRateLimiter::new(None, fork_context).unwrap(); + let peer1 = PeerId::random(); + let peer2 = PeerId::random(); + + for peer in [peer1, peer2] { + for i in 1..=5u32 { + let result = limiter.allows( + peer, + AppRequestId::Sync(SyncRequestId::SingleBlock { + id: SingleLookupReqId { + lookup_id: i, + req_id: i, + }, + }), + RequestType::Ping(Ping { data: i as u64 }), + ); + + // Check that the limiter allows the first two requests. + if i <= 2 { + assert!(result.is_ok()); + } else { + assert!(result.is_err()); + } + } + } + + assert!(limiter.active_requests.contains_key(&peer1)); + assert!(limiter + .delayed_requests + .contains_key(&(peer1, Protocol::Ping))); + assert!(limiter.active_requests.contains_key(&peer2)); + assert!(limiter + .delayed_requests + .contains_key(&(peer2, Protocol::Ping))); + + // Check that the limiter returns the IDs of pending requests and that the IDs are ordered correctly. + let mut failed_requests = limiter.peer_disconnected(peer1); + for i in 3..=5u32 { + let (request_id, _) = failed_requests.remove(0); + assert!(matches!( + request_id, + AppRequestId::Sync(SyncRequestId::SingleBlock { + id: SingleLookupReqId { req_id, .. }, + }) if req_id == i + )); + } + + // Check that peer1’s active and delayed requests have been removed. + assert!(!limiter.active_requests.contains_key(&peer1)); + assert!(!limiter + .delayed_requests + .contains_key(&(peer1, Protocol::Ping))); + + assert!(limiter.active_requests.contains_key(&peer2)); + assert!(limiter + .delayed_requests + .contains_key(&(peer2, Protocol::Ping))); + } } diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index bc9f2011f8..23060df9e6 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -103,6 +103,8 @@ pub enum NetworkEvent { StatusPeer(PeerId), NewListenAddr(Multiaddr), ZeroListeners, + /// A peer has an updated custody group count from MetaData. + PeerUpdatedCustodyGroupCount(PeerId), } pub type Gossipsub = gossipsub::Behaviour; @@ -223,7 +225,7 @@ impl Network { 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, + gossipsub_max_transmit_size: ctx.chain_spec.max_message_size(), }; let gs_config = gossipsub_config( config.network_load, @@ -334,7 +336,9 @@ impl Network { ) }); - let snappy_transform = SnappyTransform::new(gs_config.max_transmit_size()); + let spec = &ctx.chain_spec; + let snappy_transform = + SnappyTransform::new(spec.max_payload_size as usize, spec.max_compressed_len()); let mut gossipsub = Gossipsub::new_with_subscription_filter_and_transform( MessageAuthenticity::Anonymous, gs_config.clone(), @@ -365,7 +369,7 @@ impl Network { }; let network_params = NetworkParams { - max_chunk_size: ctx.chain_spec.max_chunk_size as usize, + max_payload_size: ctx.chain_spec.max_payload_size as usize, ttfb_timeout: ctx.chain_spec.ttfb_timeout(), resp_timeout: ctx.chain_spec.resp_timeout(), }; @@ -1653,7 +1657,7 @@ impl Network { return None; } - // The METADATA and PING RPC responses are handled within the behaviour and not propagated + // The PING RPC responses are handled within the behaviour and not propagated match event.message { Err(handler_err) => { match handler_err { @@ -1856,9 +1860,11 @@ impl Network { None } RpcSuccessResponse::MetaData(meta_data) => { - self.peer_manager_mut() + let updated_cgc = self + .peer_manager_mut() .meta_data_response(&peer_id, meta_data.as_ref().clone()); - None + // Send event after calling into peer_manager so the PeerDB is updated. + updated_cgc.then(|| NetworkEvent::PeerUpdatedCustodyGroupCount(peer_id)) } /* Network propagated protocols */ RpcSuccessResponse::Status(msg) => { diff --git a/beacon_node/lighthouse_network/src/types/globals.rs b/beacon_node/lighthouse_network/src/types/globals.rs index 3031a0dff7..fd99d93589 100644 --- a/beacon_node/lighthouse_network/src/types/globals.rs +++ b/beacon_node/lighthouse_network/src/types/globals.rs @@ -206,6 +206,20 @@ impl NetworkGlobals { .collect::>() } + /// Returns true if the peer is known and is a custodian of `column_index` + pub fn is_custody_peer_of(&self, column_index: ColumnIndex, peer_id: &PeerId) -> bool { + self.peers + .read() + .peer_info(peer_id) + .map(|info| { + info.is_assigned_to_custody_subnet(&DataColumnSubnetId::from_column_index( + column_index, + &self.spec, + )) + }) + .unwrap_or(false) + } + /// Returns the TopicConfig to compute the set of Gossip topics for a given fork pub fn as_topic_config(&self) -> TopicConfig { TopicConfig { diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index 57a22caba5..34ebe53113 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -55,13 +55,16 @@ pub enum PubsubMessage { // Implements the `DataTransform` trait of gossipsub to employ snappy compression pub struct SnappyTransform { /// Sets the maximum size we allow gossipsub messages to decompress to. - max_size_per_message: usize, + max_uncompressed_len: usize, + /// Sets the maximum size we allow for compressed gossipsub message data. + max_compressed_len: usize, } impl SnappyTransform { - pub fn new(max_size_per_message: usize) -> Self { + pub fn new(max_uncompressed_len: usize, max_compressed_len: usize) -> Self { SnappyTransform { - max_size_per_message, + max_uncompressed_len, + max_compressed_len, } } } @@ -72,12 +75,19 @@ impl gossipsub::DataTransform for SnappyTransform { &self, raw_message: gossipsub::RawMessage, ) -> Result { - // check the length of the raw bytes - let len = decompress_len(&raw_message.data)?; - if len > self.max_size_per_message { + // first check the size of the compressed payload + if raw_message.data.len() > self.max_compressed_len { return Err(Error::new( ErrorKind::InvalidData, - "ssz_snappy decoded data > GOSSIP_MAX_SIZE", + "ssz_snappy encoded data > max_compressed_len", + )); + } + // check the length of the uncompressed bytes + let len = decompress_len(&raw_message.data)?; + if len > self.max_uncompressed_len { + return Err(Error::new( + ErrorKind::InvalidData, + "ssz_snappy decoded data > MAX_PAYLOAD_SIZE", )); } @@ -101,10 +111,10 @@ impl gossipsub::DataTransform for SnappyTransform { ) -> Result, std::io::Error> { // Currently we are not employing topic-based compression. Everything is expected to be // snappy compressed. - if data.len() > self.max_size_per_message { + if data.len() > self.max_uncompressed_len { return Err(Error::new( ErrorKind::InvalidData, - "ssz_snappy Encoded data > GOSSIP_MAX_SIZE", + "ssz_snappy Encoded data > MAX_PAYLOAD_SIZE", )); } let mut encoder = Encoder::new(); diff --git a/beacon_node/lighthouse_network/tests/common.rs b/beacon_node/lighthouse_network/tests/common.rs index 42624c4731..332e8a73cd 100644 --- a/beacon_node/lighthouse_network/tests/common.rs +++ b/beacon_node/lighthouse_network/tests/common.rs @@ -16,6 +16,7 @@ use types::{ type E = MinimalEthSpec; +use lighthouse_network::rpc::config::InboundRateLimiterConfig; use tempfile::Builder as TempBuilder; /// Returns a dummy fork context @@ -80,7 +81,11 @@ pub fn build_tracing_subscriber(level: &str, enabled: bool) { } } -pub fn build_config(mut boot_nodes: Vec) -> Arc { +pub fn build_config( + mut boot_nodes: Vec, + disable_peer_scoring: bool, + inbound_rate_limiter: Option, +) -> Arc { let mut config = NetworkConfig::default(); // Find unused ports by using the 0 port. @@ -96,6 +101,8 @@ pub fn build_config(mut boot_nodes: Vec) -> Arc { config.enr_address = (Some(std::net::Ipv4Addr::LOCALHOST), None); config.boot_nodes_enr.append(&mut boot_nodes); config.network_dir = path.into_path(); + config.disable_peer_scoring = disable_peer_scoring; + config.inbound_rate_limiter_config = inbound_rate_limiter; Arc::new(config) } @@ -105,8 +112,10 @@ pub async fn build_libp2p_instance( fork_name: ForkName, chain_spec: Arc, service_name: String, + disable_peer_scoring: bool, + inbound_rate_limiter: Option, ) -> Libp2pInstance { - let config = build_config(boot_nodes); + let config = build_config(boot_nodes, disable_peer_scoring, inbound_rate_limiter); // launch libp2p service let (signal, exit) = async_channel::bounded(1); @@ -147,6 +156,8 @@ pub async fn build_node_pair( fork_name: ForkName, spec: Arc, protocol: Protocol, + disable_peer_scoring: bool, + inbound_rate_limiter: Option, ) -> (Libp2pInstance, Libp2pInstance) { let mut sender = build_libp2p_instance( rt.clone(), @@ -154,10 +165,20 @@ pub async fn build_node_pair( fork_name, spec.clone(), "sender".to_string(), + disable_peer_scoring, + inbound_rate_limiter.clone(), + ) + .await; + let mut receiver = build_libp2p_instance( + rt, + vec![], + fork_name, + spec.clone(), + "receiver".to_string(), + disable_peer_scoring, + inbound_rate_limiter, ) .await; - let mut receiver = - build_libp2p_instance(rt, vec![], fork_name, spec.clone(), "receiver".to_string()).await; // let the two nodes set up listeners let sender_fut = async { @@ -238,6 +259,8 @@ pub async fn build_linear( fork_name, spec.clone(), "linear".to_string(), + false, + None, ) .await, ); diff --git a/beacon_node/lighthouse_network/tests/rpc_tests.rs b/beacon_node/lighthouse_network/tests/rpc_tests.rs index 24565ad9c6..9b43e8b581 100644 --- a/beacon_node/lighthouse_network/tests/rpc_tests.rs +++ b/beacon_node/lighthouse_network/tests/rpc_tests.rs @@ -5,24 +5,24 @@ mod common; use common::{build_tracing_subscriber, Protocol}; use lighthouse_network::rpc::{methods::*, RequestType}; use lighthouse_network::service::api_types::AppRequestId; -use lighthouse_network::{rpc::max_rpc_size, NetworkEvent, ReportSource, Response}; +use lighthouse_network::{NetworkEvent, ReportSource, Response}; use ssz::Encode; use ssz_types::VariableList; use std::sync::Arc; -use std::time::Duration; +use std::time::{Duration, Instant}; use tokio::runtime::Runtime; use tokio::time::sleep; -use tracing::{debug, warn}; +use tracing::{debug, error, warn}; use types::{ BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BeaconBlockBellatrix, BlobSidecar, ChainSpec, - EmptyBlock, Epoch, EthSpec, FixedBytesExtended, ForkContext, ForkName, Hash256, MinimalEthSpec, + EmptyBlock, Epoch, EthSpec, FixedBytesExtended, ForkName, Hash256, MinimalEthSpec, RuntimeVariableList, Signature, SignedBeaconBlock, Slot, }; type E = MinimalEthSpec; /// Bellatrix block with length < max_rpc_size. -fn bellatrix_block_small(fork_context: &ForkContext, spec: &ChainSpec) -> BeaconBlock { +fn bellatrix_block_small(spec: &ChainSpec) -> BeaconBlock { let mut block = BeaconBlockBellatrix::::empty(spec); let tx = VariableList::from(vec![0; 1024]); let txs = VariableList::from(std::iter::repeat_n(tx, 5000).collect::>()); @@ -30,14 +30,14 @@ fn bellatrix_block_small(fork_context: &ForkContext, spec: &ChainSpec) -> Beacon block.body.execution_payload.execution_payload.transactions = txs; let block = BeaconBlock::Bellatrix(block); - assert!(block.ssz_bytes_len() <= max_rpc_size(fork_context, spec.max_chunk_size as usize)); + assert!(block.ssz_bytes_len() <= spec.max_payload_size as usize); block } /// Bellatrix block with length > MAX_RPC_SIZE. /// The max limit for a bellatrix block is in the order of ~16GiB which wouldn't fit in memory. /// Hence, we generate a bellatrix block just greater than `MAX_RPC_SIZE` to test rejection on the rpc layer. -fn bellatrix_block_large(fork_context: &ForkContext, spec: &ChainSpec) -> BeaconBlock { +fn bellatrix_block_large(spec: &ChainSpec) -> BeaconBlock { let mut block = BeaconBlockBellatrix::::empty(spec); let tx = VariableList::from(vec![0; 1024]); let txs = VariableList::from(std::iter::repeat_n(tx, 100000).collect::>()); @@ -45,7 +45,7 @@ fn bellatrix_block_large(fork_context: &ForkContext, spec: &ChainSpec) -> Beacon block.body.execution_payload.execution_payload.transactions = txs; let block = BeaconBlock::Bellatrix(block); - assert!(block.ssz_bytes_len() > max_rpc_size(fork_context, spec.max_chunk_size as usize)); + assert!(block.ssz_bytes_len() > spec.max_payload_size as usize); block } @@ -64,8 +64,15 @@ fn test_tcp_status_rpc() { rt.block_on(async { // get sender/receiver - let (mut sender, mut receiver) = - common::build_node_pair(Arc::downgrade(&rt), ForkName::Base, spec, Protocol::Tcp).await; + let (mut sender, mut receiver) = common::build_node_pair( + Arc::downgrade(&rt), + ForkName::Base, + spec, + Protocol::Tcp, + false, + None, + ) + .await; // Dummy STATUS RPC message let rpc_request = RequestType::Status(StatusMessage { @@ -168,6 +175,8 @@ fn test_tcp_blocks_by_range_chunked_rpc() { ForkName::Bellatrix, spec.clone(), Protocol::Tcp, + false, + None, ) .await; @@ -188,7 +197,7 @@ fn test_tcp_blocks_by_range_chunked_rpc() { let signed_full_block = SignedBeaconBlock::from_block(full_block, Signature::empty()); let rpc_response_altair = Response::BlocksByRange(Some(Arc::new(signed_full_block))); - let full_block = bellatrix_block_small(&common::fork_context(ForkName::Bellatrix), &spec); + let full_block = bellatrix_block_small(&spec); let signed_full_block = SignedBeaconBlock::from_block(full_block, Signature::empty()); let rpc_response_bellatrix_small = Response::BlocksByRange(Some(Arc::new(signed_full_block))); @@ -311,6 +320,8 @@ fn test_blobs_by_range_chunked_rpc() { ForkName::Deneb, spec.clone(), Protocol::Tcp, + false, + None, ) .await; @@ -430,6 +441,8 @@ fn test_tcp_blocks_by_range_over_limit() { ForkName::Bellatrix, spec.clone(), Protocol::Tcp, + false, + None, ) .await; @@ -442,7 +455,7 @@ fn test_tcp_blocks_by_range_over_limit() { })); // BlocksByRange Response - let full_block = bellatrix_block_large(&common::fork_context(ForkName::Bellatrix), &spec); + let full_block = bellatrix_block_large(&spec); let signed_full_block = SignedBeaconBlock::from_block(full_block, Signature::empty()); let rpc_response_bellatrix_large = Response::BlocksByRange(Some(Arc::new(signed_full_block))); @@ -533,6 +546,8 @@ fn test_tcp_blocks_by_range_chunked_rpc_terminates_correctly() { ForkName::Base, spec.clone(), Protocol::Tcp, + false, + None, ) .await; @@ -665,6 +680,8 @@ fn test_tcp_blocks_by_range_single_empty_rpc() { ForkName::Base, spec.clone(), Protocol::Tcp, + false, + None, ) .await; @@ -785,6 +802,8 @@ fn test_tcp_blocks_by_root_chunked_rpc() { ForkName::Bellatrix, spec.clone(), Protocol::Tcp, + false, + None, ) .await; @@ -813,7 +832,7 @@ fn test_tcp_blocks_by_root_chunked_rpc() { let signed_full_block = SignedBeaconBlock::from_block(full_block, Signature::empty()); let rpc_response_altair = Response::BlocksByRoot(Some(Arc::new(signed_full_block))); - let full_block = bellatrix_block_small(&common::fork_context(ForkName::Bellatrix), &spec); + let full_block = bellatrix_block_small(&spec); let signed_full_block = SignedBeaconBlock::from_block(full_block, Signature::empty()); let rpc_response_bellatrix_small = Response::BlocksByRoot(Some(Arc::new(signed_full_block))); @@ -929,6 +948,8 @@ fn test_tcp_blocks_by_root_chunked_rpc_terminates_correctly() { ForkName::Base, spec.clone(), Protocol::Tcp, + false, + None, ) .await; @@ -1065,8 +1086,15 @@ fn goodbye_test(log_level: &str, enable_logging: bool, protocol: Protocol) { // get sender/receiver rt.block_on(async { - let (mut sender, mut receiver) = - common::build_node_pair(Arc::downgrade(&rt), ForkName::Base, spec, protocol).await; + let (mut sender, mut receiver) = common::build_node_pair( + Arc::downgrade(&rt), + ForkName::Base, + spec, + protocol, + false, + None, + ) + .await; // build the sender future let sender_future = async { @@ -1127,3 +1155,239 @@ fn quic_test_goodbye_rpc() { let enabled_logging = false; goodbye_test(log_level, enabled_logging, Protocol::Quic); } + +// Test that the receiver delays the responses during response rate-limiting. +#[test] +fn test_delayed_rpc_response() { + let rt = Arc::new(Runtime::new().unwrap()); + let spec = Arc::new(E::default_spec()); + + // Allow 1 token to be use used every 3 seconds. + const QUOTA_SEC: u64 = 3; + + rt.block_on(async { + // get sender/receiver + let (mut sender, mut receiver) = common::build_node_pair( + Arc::downgrade(&rt), + ForkName::Base, + spec, + Protocol::Tcp, + false, + // Configure a quota for STATUS responses of 1 token every 3 seconds. + Some(format!("status:1/{QUOTA_SEC}").parse().unwrap()), + ) + .await; + + // Dummy STATUS RPC message + let rpc_request = RequestType::Status(StatusMessage { + fork_digest: [0; 4], + finalized_root: Hash256::from_low_u64_be(0), + finalized_epoch: Epoch::new(1), + head_root: Hash256::from_low_u64_be(0), + head_slot: Slot::new(1), + }); + + // Dummy STATUS RPC message + let rpc_response = Response::Status(StatusMessage { + fork_digest: [0; 4], + finalized_root: Hash256::from_low_u64_be(0), + finalized_epoch: Epoch::new(1), + head_root: Hash256::from_low_u64_be(0), + head_slot: Slot::new(1), + }); + + // build the sender future + let sender_future = async { + let mut request_id = 1; + let mut request_sent_at = Instant::now(); + loop { + match sender.next_event().await { + NetworkEvent::PeerConnectedOutgoing(peer_id) => { + debug!(%request_id, "Sending RPC request"); + sender + .send_request(peer_id, AppRequestId::Router, rpc_request.clone()) + .unwrap(); + request_sent_at = Instant::now(); + } + NetworkEvent::ResponseReceived { + peer_id, + app_request_id: _, + response, + } => { + debug!(%request_id, "Sender received"); + assert_eq!(response, rpc_response); + + match request_id { + 1 => { + // The first response is returned instantly. + assert!(request_sent_at.elapsed() < Duration::from_millis(100)); + } + 2..=5 => { + // The second and subsequent responses are delayed due to the response rate-limiter on the receiver side. + // Adding a slight margin to the elapsed time check to account for potential timing issues caused by system + // scheduling or execution delays during testing. + assert!( + request_sent_at.elapsed() + > (Duration::from_secs(QUOTA_SEC) + - Duration::from_millis(100)) + ); + if request_id == 5 { + // End the test + return; + } + } + _ => unreachable!(), + } + + request_id += 1; + debug!(%request_id, "Sending RPC request"); + sender + .send_request(peer_id, AppRequestId::Router, rpc_request.clone()) + .unwrap(); + request_sent_at = Instant::now(); + } + NetworkEvent::RPCFailed { + app_request_id: _, + peer_id: _, + error, + } => { + error!(?error, "RPC Failed"); + panic!("Rpc failed."); + } + _ => {} + } + } + }; + + // build the receiver future + let receiver_future = async { + loop { + if let NetworkEvent::RequestReceived { + peer_id, + inbound_request_id, + request_type, + } = receiver.next_event().await + { + assert_eq!(request_type, rpc_request); + debug!("Receiver received request"); + receiver.send_response(peer_id, inbound_request_id, rpc_response.clone()); + } + } + }; + + tokio::select! { + _ = sender_future => {} + _ = receiver_future => {} + _ = sleep(Duration::from_secs(30)) => { + panic!("Future timed out"); + } + } + }) +} + +// Test that a rate-limited error doesn't occur even if the sender attempts to send many requests at +// once, thanks to the self-limiter on the sender side. +#[test] +fn test_active_requests() { + let rt = Arc::new(Runtime::new().unwrap()); + let spec = Arc::new(E::default_spec()); + + rt.block_on(async { + // Get sender/receiver. + let (mut sender, mut receiver) = common::build_node_pair( + Arc::downgrade(&rt), + ForkName::Base, + spec, + Protocol::Tcp, + false, + None, + ) + .await; + + // Dummy STATUS RPC request. + let rpc_request = RequestType::Status(StatusMessage { + fork_digest: [0; 4], + finalized_root: Hash256::from_low_u64_be(0), + finalized_epoch: Epoch::new(1), + head_root: Hash256::from_low_u64_be(0), + head_slot: Slot::new(1), + }); + + // Dummy STATUS RPC response. + let rpc_response = Response::Status(StatusMessage { + fork_digest: [0; 4], + finalized_root: Hash256::zero(), + finalized_epoch: Epoch::new(1), + head_root: Hash256::zero(), + head_slot: Slot::new(1), + }); + + // Number of requests. + const REQUESTS: u8 = 10; + + // Build the sender future. + let sender_future = async { + let mut response_received = 0; + loop { + match sender.next_event().await { + NetworkEvent::PeerConnectedOutgoing(peer_id) => { + debug!("Sending RPC request"); + // Send requests in quick succession to intentionally trigger request queueing in the self-limiter. + for _ in 0..REQUESTS { + sender + .send_request(peer_id, AppRequestId::Router, rpc_request.clone()) + .unwrap(); + } + } + NetworkEvent::ResponseReceived { response, .. } => { + debug!(?response, "Sender received response"); + if matches!(response, Response::Status(_)) { + response_received += 1; + } + } + NetworkEvent::RPCFailed { + app_request_id: _, + peer_id: _, + error, + } => panic!("RPC failed: {:?}", error), + _ => {} + } + + if response_received == REQUESTS { + return; + } + } + }; + + // Build the receiver future. + let receiver_future = async { + let mut received_requests = vec![]; + loop { + tokio::select! { + event = receiver.next_event() => { + if let NetworkEvent::RequestReceived { peer_id, inbound_request_id, request_type } = event { + debug!(?request_type, "Receiver received request"); + if matches!(request_type, RequestType::Status(_)) { + received_requests.push((peer_id, inbound_request_id)); + } + } + } + // Introduce a delay in sending responses to trigger request queueing on the sender side. + _ = sleep(Duration::from_secs(3)) => { + for (peer_id, inbound_request_id) in received_requests.drain(..) { + receiver.send_response(peer_id, inbound_request_id, rpc_response.clone()); + } + } + } + } + }; + + tokio::select! { + _ = sender_future => {} + _ = receiver_future => {} + _ = sleep(Duration::from_secs(30)) => { + panic!("Future timed out"); + } + } + }) +} diff --git a/beacon_node/network/src/metrics.rs b/beacon_node/network/src/metrics.rs index 7c38ae9d75..b129b54841 100644 --- a/beacon_node/network/src/metrics.rs +++ b/beacon_node/network/src/metrics.rs @@ -88,6 +88,15 @@ pub static BEACON_PROCESSOR_IMPORT_ERRORS_PER_TYPE: LazyLock> = + LazyLock::new(|| { + try_create_histogram_vec_with_buckets( + "beacon_processor_get_block_roots_time_seconds", + "Time to complete get_block_roots when serving by_range requests", + decimal_buckets(-3, -1), + &["source"], + ) + }); /* * Gossip processor 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 55fa6e9e4e..89384bc39e 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -540,7 +540,7 @@ impl NetworkBeaconProcessor { attestation: single_attestation, }, None, - AttnError::BeaconChainError(error), + AttnError::BeaconChainError(Box::new(error)), seen_timestamp, ); } @@ -828,7 +828,8 @@ impl NetworkBeaconProcessor { | GossipDataColumnError::InvalidKzgProof { .. } | GossipDataColumnError::UnexpectedDataColumn | GossipDataColumnError::InvalidColumnIndex(_) - | GossipDataColumnError::InconsistentCommitmentsOrProofLength + | GossipDataColumnError::InconsistentCommitmentsLength { .. } + | GossipDataColumnError::InconsistentProofsLength { .. } | GossipDataColumnError::NotFinalizedDescendant { .. } => { debug!( error = ?err, @@ -1132,7 +1133,7 @@ impl NetworkBeaconProcessor { let processing_start_time = Instant::now(); let block_root = verified_data_column.block_root(); let data_column_slot = verified_data_column.slot(); - let data_column_index = verified_data_column.id().index; + let data_column_index = verified_data_column.index(); let result = self .chain @@ -1162,7 +1163,8 @@ impl NetworkBeaconProcessor { "Processed data column, waiting for other components" ); - self.attempt_data_column_reconstruction(block_root).await; + self.attempt_data_column_reconstruction(block_root, true) + .await; } }, Err(BlockError::DuplicateFullyImported(_)) => { @@ -2778,41 +2780,57 @@ impl NetworkBeaconProcessor { "attn_to_finalized_block", ); } - AttnError::BeaconChainError(BeaconChainError::DBError(Error::HotColdDBError( - HotColdDBError::FinalizedStateNotInHotDatabase { .. }, - ))) => { - debug!(%peer_id, "Attestation for finalized state"); - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); - } - e @ AttnError::BeaconChainError(BeaconChainError::MaxCommitteePromises(_)) => { - debug!( - target_root = ?failed_att.attestation_data().target.root, - ?beacon_block_root, - slot = ?failed_att.attestation_data().slot, - ?attestation_type, - error = ?e, - %peer_id, - "Dropping attestation" - ); - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); - } AttnError::BeaconChainError(e) => { - /* - * Lighthouse hit an unexpected error whilst processing the attestation. It - * should be impossible to trigger a `BeaconChainError` from the network, - * so we have a bug. - * - * It's not clear if the message is invalid/malicious. - */ - error!( - ?beacon_block_root, - slot = ?failed_att.attestation_data().slot, - ?attestation_type, - %peer_id, - error = ?e, - "Unable to validate attestation" - ); - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); + match e.as_ref() { + BeaconChainError::DBError(Error::HotColdDBError( + HotColdDBError::FinalizedStateNotInHotDatabase { .. }, + )) => { + debug!(%peer_id, "Attestation for finalized state"); + self.propagate_validation_result( + message_id, + peer_id, + MessageAcceptance::Ignore, + ); + } + BeaconChainError::MaxCommitteePromises(e) => { + debug!( + target_root = ?failed_att.attestation_data().target.root, + ?beacon_block_root, + slot = ?failed_att.attestation_data().slot, + ?attestation_type, + error = ?e, + %peer_id, + "Dropping attestation" + ); + self.propagate_validation_result( + message_id, + peer_id, + MessageAcceptance::Ignore, + ); + } + _ => { + /* + * Lighthouse hit an unexpected error whilst processing the attestation. It + * should be impossible to trigger a `BeaconChainError` from the network, + * so we have a bug. + * + * It's not clear if the message is invalid/malicious. + */ + error!( + ?beacon_block_root, + slot = ?failed_att.attestation_data().slot, + ?attestation_type, + %peer_id, + error = ?e, + "Unable to validate attestation" + ); + self.propagate_validation_result( + message_id, + peer_id, + MessageAcceptance::Ignore, + ); + } + } } } diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index a58949b2a8..a898420f37 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -867,14 +867,10 @@ impl NetworkBeaconProcessor { block_root: Hash256, publish_blobs: bool, ) { - let is_supernode = self.network_globals.is_supernode(); - + let custody_columns = self.network_globals.sampling_columns.clone(); let self_cloned = self.clone(); let publish_fn = move |blobs_or_data_column| { - // At the moment non supernodes are not required to publish any columns. - // TODO(das): we could experiment with having full nodes publish their custodied - // columns here. - if publish_blobs && is_supernode { + if publish_blobs { match blobs_or_data_column { BlobsOrDataColumns::Blobs(blobs) => { self_cloned.publish_blobs_gradually(blobs, block_root); @@ -890,6 +886,7 @@ impl NetworkBeaconProcessor { self.chain.clone(), block_root, block.clone(), + custody_columns, publish_fn, ) .instrument(tracing::info_span!( @@ -945,9 +942,13 @@ impl NetworkBeaconProcessor { /// /// Returns `Some(AvailabilityProcessingStatus)` if reconstruction is successfully performed, /// otherwise returns `None`. + /// + /// The `publish_columns` parameter controls whether reconstructed columns should be published + /// to the gossip network. async fn attempt_data_column_reconstruction( self: &Arc, block_root: Hash256, + publish_columns: bool, ) -> Option { // Only supernodes attempt reconstruction if !self.network_globals.is_supernode() { @@ -957,7 +958,9 @@ impl NetworkBeaconProcessor { let result = self.chain.reconstruct_data_columns(block_root).await; match result { Ok(Some((availability_processing_status, data_columns_to_publish))) => { - self.publish_data_columns_gradually(data_columns_to_publish, block_root); + if publish_columns { + self.publish_data_columns_gradually(data_columns_to_publish, block_root); + } match &availability_processing_status { AvailabilityProcessingStatus::Imported(hash) => { debug!( @@ -1079,7 +1082,7 @@ impl NetworkBeaconProcessor { /// /// This is an optimisation to reduce outbound bandwidth and ensures each column is published /// by some nodes on the network as soon as possible. Our hope is that some columns arrive from - /// other supernodes in the meantime, obviating the need for us to publish them. If no other + /// other nodes in the meantime, obviating the need for us to publish them. If no other /// publisher exists for a column, it will eventually get published here. fn publish_data_columns_gradually( self: &Arc, @@ -1104,9 +1107,9 @@ impl NetworkBeaconProcessor { }); }; - // If this node is a super node, permute the columns and split them into batches. + // Permute the columns and split them into batches. // The hope is that we won't need to publish some columns because we will receive them - // on gossip from other supernodes. + // on gossip from other nodes. data_columns_to_publish.shuffle(&mut rand::thread_rng()); let blob_publication_batch_interval = chain.config.blob_publication_batch_interval; @@ -1168,7 +1171,7 @@ use { }; #[cfg(test)] -type TestBeaconChainType = +pub(crate) type TestBeaconChainType = Witness, E, MemoryStore, MemoryStore>; #[cfg(test)] 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 4694c926c9..7c3c854ed8 100644 --- a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs @@ -1,9 +1,10 @@ +use crate::metrics; use crate::network_beacon_processor::{NetworkBeaconProcessor, FUTURE_SLOT_TOLERANCE}; use crate::service::NetworkMessage; use crate::status::ToStatusMessage; use crate::sync::SyncMessage; use beacon_chain::{BeaconChainError, BeaconChainTypes, WhenSlotSkipped}; -use itertools::process_results; +use itertools::{process_results, Itertools}; use lighthouse_network::rpc::methods::{ BlobsByRangeRequest, BlobsByRootRequest, DataColumnsByRangeRequest, DataColumnsByRootRequest, }; @@ -65,7 +66,7 @@ impl NetworkBeaconProcessor { fn check_peer_relevance( &self, remote: &StatusMessage, - ) -> Result, BeaconChainError> { + ) -> Result, Box> { let local = self.chain.status_message(); let start_slot = |epoch: Epoch| epoch.start_slot(T::EthSpec::slots_per_epoch()); @@ -111,7 +112,8 @@ impl NetworkBeaconProcessor { if self .chain .block_root_at_slot(remote_finalized_slot, WhenSlotSkipped::Prev) - .map(|root_opt| root_opt != Some(remote.finalized_root))? + .map(|root_opt| root_opt != Some(remote.finalized_root)) + .map_err(Box::new)? { Some("Different finalized chain".to_string()) } else { @@ -359,24 +361,25 @@ impl NetworkBeaconProcessor { ) -> Result<(), (RpcErrorResponse, &'static str)> { let mut send_data_column_count = 0; - for data_column_id in request.data_column_ids.as_slice() { - match self.chain.get_data_column_checking_all_caches( - data_column_id.block_root, - data_column_id.index, + for data_column_ids_by_root in request.data_column_ids.as_slice() { + match self.chain.get_data_columns_checking_all_caches( + data_column_ids_by_root.block_root, + data_column_ids_by_root.columns.as_slice(), ) { - Ok(Some(data_column)) => { - send_data_column_count += 1; - self.send_response( - peer_id, - inbound_request_id, - Response::DataColumnsByRoot(Some(data_column)), - ); + Ok(data_columns) => { + send_data_column_count += data_columns.len(); + for data_column in data_columns { + self.send_response( + peer_id, + inbound_request_id, + Response::DataColumnsByRoot(Some(data_column)), + ); + } } - Ok(None) => {} // no-op Err(e) => { // TODO(das): lower log level when feature is stabilized error!( - block_root = ?data_column_id.block_root, + block_root = ?data_column_ids_by_root.block_root, %peer_id, error = ?e, "Error getting data column" @@ -388,7 +391,7 @@ impl NetworkBeaconProcessor { debug!( %peer_id, - request = ?request.group_by_ordered_block_root(), + request = ?request.data_column_ids, returned = send_data_column_count, "Received DataColumnsByRoot Request" ); @@ -588,97 +591,57 @@ impl NetworkBeaconProcessor { inbound_request_id: InboundRequestId, req: BlocksByRangeRequest, ) -> Result<(), (RpcErrorResponse, &'static str)> { + let req_start_slot = *req.start_slot(); + let req_count = *req.count(); + debug!( %peer_id, - count = req.count(), - start_slot = %req.start_slot(), + count = req_count, + start_slot = %req_start_slot, "Received BlocksByRange Request" ); - let forwards_block_root_iter = match self - .chain - .forwards_iter_block_roots(Slot::from(*req.start_slot())) - { - Ok(iter) => iter, - Err(BeaconChainError::HistoricalBlockOutOfRange { - slot, - oldest_block_slot, - }) => { - debug!( - requested_slot = %slot, - oldest_known_slot = %oldest_block_slot, - "Range request failed during backfill" - ); - return Err((RpcErrorResponse::ResourceUnavailable, "Backfilling")); - } - Err(e) => { - error!( - request = ?req, - %peer_id, - error = ?e, - "Unable to obtain root iter" - ); - return Err((RpcErrorResponse::ServerError, "Database error")); - } - }; - - // Pick out the required blocks, ignoring skip-slots. - let mut last_block_root = None; - let maybe_block_roots = process_results(forwards_block_root_iter, |iter| { - iter.take_while(|(_, slot)| { - slot.as_u64() < req.start_slot().saturating_add(*req.count()) - }) - // map skip slots to None - .map(|(root, _)| { - let result = if Some(root) == last_block_root { - None - } else { - Some(root) - }; - last_block_root = Some(root); - result - }) - .collect::>>() - }); - - let block_roots = match maybe_block_roots { - Ok(block_roots) => block_roots, - Err(e) => { - error!( - request = ?req, - %peer_id, - error = ?e, - "Error during iteration over blocks" - ); - return Err((RpcErrorResponse::ServerError, "Iteration error")); - } - }; - - // remove all skip slots - let block_roots = block_roots.into_iter().flatten().collect::>(); + // Spawn a blocking handle since get_block_roots_for_slot_range takes a sync lock on the + // fork-choice. + let network_beacon_processor = self.clone(); + let block_roots = self + .executor + .spawn_blocking_handle( + move || { + network_beacon_processor.get_block_roots_for_slot_range( + req_start_slot, + req_count, + "BlocksByRange", + ) + }, + "get_block_roots_for_slot_range", + ) + .ok_or((RpcErrorResponse::ServerError, "shutting down"))? + .await + .map_err(|_| (RpcErrorResponse::ServerError, "tokio join"))??; let current_slot = self .chain .slot() .unwrap_or_else(|_| self.chain.slot_clock.genesis_slot()); - let log_results = |req: BlocksByRangeRequest, peer_id, blocks_sent| { - if blocks_sent < (*req.count() as usize) { + let log_results = |peer_id, blocks_sent| { + if blocks_sent < (req_count as usize) { debug!( %peer_id, msg = "Failed to return all requested blocks", - start_slot = %req.start_slot(), + start_slot = %req_start_slot, %current_slot, - requested = req.count(), + requested = req_count, returned = blocks_sent, "BlocksByRange outgoing response processed" ); } else { debug!( %peer_id, - start_slot = %req.start_slot(), + start_slot = %req_start_slot, %current_slot, - requested = req.count(), + requested = req_count, returned = blocks_sent, "BlocksByRange outgoing response processed" ); @@ -700,8 +663,7 @@ impl NetworkBeaconProcessor { Ok(Some(block)) => { // Due to skip slots, blocks could be out of the range, we ensure they // are in the range before sending - if block.slot() >= *req.start_slot() - && block.slot() < req.start_slot() + req.count() + if block.slot() >= req_start_slot && block.slot() < req_start_slot + req.count() { blocks_sent += 1; self.send_network_message(NetworkMessage::SendResponse { @@ -718,7 +680,7 @@ impl NetworkBeaconProcessor { request_root = ?root, "Block in the chain is not in the store" ); - log_results(req, peer_id, blocks_sent); + log_results(peer_id, blocks_sent); return Err((RpcErrorResponse::ServerError, "Database inconsistency")); } Err(BeaconChainError::BlockHashMissingFromExecutionLayer(_)) => { @@ -727,7 +689,7 @@ impl NetworkBeaconProcessor { reason = "execution layer not synced", "Failed to fetch execution payload for blocks by range request" ); - log_results(req, peer_id, blocks_sent); + log_results(peer_id, blocks_sent); // send the stream terminator return Err(( RpcErrorResponse::ResourceUnavailable, @@ -753,17 +715,144 @@ impl NetworkBeaconProcessor { "Error fetching block for peer" ); } - log_results(req, peer_id, blocks_sent); + log_results(peer_id, blocks_sent); // send the stream terminator return Err((RpcErrorResponse::ServerError, "Failed fetching blocks")); } } } - log_results(req, peer_id, blocks_sent); + log_results(peer_id, blocks_sent); Ok(()) } + fn get_block_roots_for_slot_range( + &self, + req_start_slot: u64, + req_count: u64, + req_type: &str, + ) -> Result, (RpcErrorResponse, &'static str)> { + let start_time = std::time::Instant::now(); + let finalized_slot = self + .chain + .canonical_head + .cached_head() + .finalized_checkpoint() + .epoch + .start_slot(T::EthSpec::slots_per_epoch()); + + let (block_roots, source) = if req_start_slot >= finalized_slot.as_u64() { + // If the entire requested range is after finalization, use fork_choice + ( + self.chain + .block_roots_from_fork_choice(req_start_slot, req_count), + "fork_choice", + ) + } else if req_start_slot + req_count <= finalized_slot.as_u64() { + // If the entire requested range is before finalization, use store + ( + self.get_block_roots_from_store(req_start_slot, req_count)?, + "store", + ) + } else { + // Split the request at the finalization boundary + let count_from_store = finalized_slot.as_u64() - req_start_slot; + let count_from_fork_choice = req_count - count_from_store; + let start_slot_fork_choice = finalized_slot.as_u64(); + + // Get roots from store (up to and including finalized slot) + let mut roots_from_store = + self.get_block_roots_from_store(req_start_slot, count_from_store)?; + + // Get roots from fork choice (after finalized slot) + let roots_from_fork_choice = self + .chain + .block_roots_from_fork_choice(start_slot_fork_choice, count_from_fork_choice); + + roots_from_store.extend(roots_from_fork_choice); + + (roots_from_store, "mixed") + }; + + let elapsed = start_time.elapsed(); + metrics::observe_timer_vec( + &metrics::BEACON_PROCESSOR_GET_BLOCK_ROOTS_TIME, + &[source], + elapsed, + ); + + debug!( + req_type, + start_slot = %req_start_slot, + req_count, + roots_count = block_roots.len(), + source, + elapsed = ?elapsed, + %finalized_slot, + "Range request block roots retrieved" + ); + + Ok(block_roots) + } + + /// Get block roots for a `BlocksByRangeRequest` from the store using roots iterator. + fn get_block_roots_from_store( + &self, + start_slot: u64, + count: u64, + ) -> Result, (RpcErrorResponse, &'static str)> { + let forwards_block_root_iter = + match self.chain.forwards_iter_block_roots(Slot::from(start_slot)) { + Ok(iter) => iter, + Err(BeaconChainError::HistoricalBlockOutOfRange { + slot, + oldest_block_slot, + }) => { + debug!( + requested_slot = %slot, + oldest_known_slot = %oldest_block_slot, + "Range request failed during backfill" + ); + return Err((RpcErrorResponse::ResourceUnavailable, "Backfilling")); + } + Err(e) => { + error!( + %start_slot, + count, + error = ?e, + "Unable to obtain root iter for range request" + ); + return Err((RpcErrorResponse::ServerError, "Database error")); + } + }; + + // Pick out the required blocks, ignoring skip-slots. + let maybe_block_roots = process_results(forwards_block_root_iter, |iter| { + iter.take_while(|(_, slot)| slot.as_u64() < start_slot.saturating_add(count)) + .collect::>() + }); + + let block_roots = match maybe_block_roots { + Ok(block_roots) => block_roots, + Err(e) => { + error!( + %start_slot, + count, + error = ?e, + "Error during iteration over blocks for range request" + ); + return Err((RpcErrorResponse::ServerError, "Iteration error")); + } + }; + + // remove all skip slots i.e. duplicated roots + Ok(block_roots + .into_iter() + .map(|(root, _)| root) + .unique() + .collect::>()) + } + /// Handle a `BlobsByRange` request from the peer. pub fn handle_blobs_by_range_request( self: Arc, @@ -830,68 +919,8 @@ impl NetworkBeaconProcessor { }; } - let forwards_block_root_iter = - match self.chain.forwards_iter_block_roots(request_start_slot) { - Ok(iter) => iter, - Err(BeaconChainError::HistoricalBlockOutOfRange { - slot, - oldest_block_slot, - }) => { - debug!( - requested_slot = %slot, - oldest_known_slot = %oldest_block_slot, - "Range request failed during backfill" - ); - return Err((RpcErrorResponse::ResourceUnavailable, "Backfilling")); - } - Err(e) => { - error!( - request = ?req, - %peer_id, - error = ?e, - "Unable to obtain root iter" - ); - return Err((RpcErrorResponse::ServerError, "Database error")); - } - }; - - // Use `WhenSlotSkipped::Prev` to get the most recent block root prior to - // `request_start_slot` in order to check whether the `request_start_slot` is a skip. - let mut last_block_root = req.start_slot.checked_sub(1).and_then(|prev_slot| { - self.chain - .block_root_at_slot(Slot::new(prev_slot), WhenSlotSkipped::Prev) - .ok() - .flatten() - }); - - // Pick out the required blocks, ignoring skip-slots. - let maybe_block_roots = process_results(forwards_block_root_iter, |iter| { - iter.take_while(|(_, slot)| slot.as_u64() < req.start_slot.saturating_add(req.count)) - // map skip slots to None - .map(|(root, _)| { - let result = if Some(root) == last_block_root { - None - } else { - Some(root) - }; - last_block_root = Some(root); - result - }) - .collect::>>() - }); - - let block_roots = match maybe_block_roots { - Ok(block_roots) => block_roots, - Err(e) => { - error!( - request = ?req, - %peer_id, - error = ?e, - "Error during iteration over blocks" - ); - return Err((RpcErrorResponse::ServerError, "Database error")); - } - }; + let block_roots = + self.get_block_roots_for_slot_range(req.start_slot, req.count, "BlobsByRange")?; let current_slot = self .chain @@ -909,8 +938,6 @@ impl NetworkBeaconProcessor { ); }; - // remove all skip slots - let block_roots = block_roots.into_iter().flatten(); let mut blobs_sent = 0; for root in block_roots { @@ -1022,71 +1049,8 @@ impl NetworkBeaconProcessor { }; } - let forwards_block_root_iter = - match self.chain.forwards_iter_block_roots(request_start_slot) { - Ok(iter) => iter, - Err(BeaconChainError::HistoricalBlockOutOfRange { - slot, - oldest_block_slot, - }) => { - debug!( - requested_slot = %slot, - oldest_known_slot = %oldest_block_slot, - "Range request failed during backfill" - ); - return Err((RpcErrorResponse::ResourceUnavailable, "Backfilling")); - } - Err(e) => { - error!( - request = ?req, - %peer_id, - error = ?e, - "Unable to obtain root iter" - ); - return Err((RpcErrorResponse::ServerError, "Database error")); - } - }; - - // Use `WhenSlotSkipped::Prev` to get the most recent block root prior to - // `request_start_slot` in order to check whether the `request_start_slot` is a skip. - let mut last_block_root = req.start_slot.checked_sub(1).and_then(|prev_slot| { - self.chain - .block_root_at_slot(Slot::new(prev_slot), WhenSlotSkipped::Prev) - .ok() - .flatten() - }); - - // Pick out the required blocks, ignoring skip-slots. - let maybe_block_roots = process_results(forwards_block_root_iter, |iter| { - iter.take_while(|(_, slot)| slot.as_u64() < req.start_slot.saturating_add(req.count)) - // map skip slots to None - .map(|(root, _)| { - let result = if Some(root) == last_block_root { - None - } else { - Some(root) - }; - last_block_root = Some(root); - result - }) - .collect::>>() - }); - - let block_roots = match maybe_block_roots { - Ok(block_roots) => block_roots, - Err(e) => { - error!( - request = ?req, - %peer_id, - error = ?e, - "Error during iteration over blocks" - ); - return Err((RpcErrorResponse::ServerError, "Database error")); - } - }; - - // remove all skip slots - let block_roots = block_roots.into_iter().flatten(); + let block_roots = + self.get_block_roots_for_slot_range(req.start_slot, req.count, "DataColumnsByRange")?; let mut data_columns_sent = 0; for root in block_roots { 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 48ae26c826..31b17a41a4 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -383,8 +383,12 @@ impl NetworkBeaconProcessor { ); // Attempt reconstruction here before notifying sync, to avoid sending out more requests // that we may no longer need. - if let Some(availability) = - self.attempt_data_column_reconstruction(block_root).await + // We don't publish columns reconstructed from rpc columns to the gossip network, + // as these are likely historic columns. + let publish_columns = false; + if let Some(availability) = self + .attempt_data_column_reconstruction(block_root, publish_columns) + .await { result = Ok(availability) } diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index aa5f54ac1f..292e894870 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -9,11 +9,14 @@ use crate::{ sync::{manager::BlockProcessType, SyncMessage}, }; use beacon_chain::block_verification_types::RpcBlock; +use beacon_chain::kzg_utils::blobs_to_data_column_sidecars; use beacon_chain::test_utils::{ - test_spec, AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, + get_kzg, test_spec, AttestationStrategy, BeaconChainHarness, BlockStrategy, + EphemeralHarnessType, }; use beacon_chain::{BeaconChain, WhenSlotSkipped}; use beacon_processor::{work_reprocessing_queue::*, *}; +use itertools::Itertools; use lighthouse_network::rpc::methods::{BlobsByRangeRequest, MetaDataV3}; use lighthouse_network::rpc::InboundRequestId; use lighthouse_network::{ @@ -29,9 +32,9 @@ use std::time::Duration; use tokio::sync::mpsc; use types::blob_sidecar::FixedBlobSidecarList; use types::{ - Attestation, AttesterSlashing, BlobSidecar, BlobSidecarList, Epoch, Hash256, MainnetEthSpec, - ProposerSlashing, SignedAggregateAndProof, SignedBeaconBlock, SignedVoluntaryExit, Slot, - SubnetId, + Attestation, AttesterSlashing, BlobSidecar, BlobSidecarList, DataColumnSidecarList, + DataColumnSubnetId, Epoch, Hash256, MainnetEthSpec, ProposerSlashing, SignedAggregateAndProof, + SignedBeaconBlock, SignedVoluntaryExit, Slot, SubnetId, }; type E = MainnetEthSpec; @@ -52,6 +55,7 @@ struct TestRig { chain: Arc>, next_block: Arc>, next_blobs: Option>, + next_data_columns: Option>, attestations: Vec<(Attestation, SubnetId)>, next_block_attestations: Vec<(Attestation, SubnetId)>, next_block_aggregate_attestations: Vec>, @@ -241,7 +245,7 @@ impl TestRig { let network_beacon_processor = Arc::new(network_beacon_processor); let beacon_processor = BeaconProcessor { - network_globals, + network_globals: network_globals.clone(), executor, current_workers: 0, config: beacon_processor_config, @@ -262,15 +266,36 @@ impl TestRig { assert!(beacon_processor.is_ok()); let block = next_block_tuple.0; - let blob_sidecars = if let Some((kzg_proofs, blobs)) = next_block_tuple.1 { - Some(BlobSidecar::build_sidecars(blobs, &block, kzg_proofs, &chain.spec).unwrap()) + let (blob_sidecars, data_columns) = if let Some((kzg_proofs, blobs)) = next_block_tuple.1 { + if chain.spec.is_peer_das_enabled_for_epoch(block.epoch()) { + let kzg = get_kzg(&chain.spec); + let custody_columns: DataColumnSidecarList = blobs_to_data_column_sidecars( + &blobs.iter().collect_vec(), + kzg_proofs.clone().into_iter().collect_vec(), + &block, + &kzg, + &chain.spec, + ) + .unwrap() + .into_iter() + .filter(|c| network_globals.sampling_columns.contains(&c.index)) + .collect::>(); + + (None, Some(custody_columns)) + } else { + let blob_sidecars = + BlobSidecar::build_sidecars(blobs, &block, kzg_proofs, &chain.spec).unwrap(); + (Some(blob_sidecars), None) + } } else { - None + (None, None) }; + Self { chain, next_block: block, next_blobs: blob_sidecars, + next_data_columns: data_columns, attestations, next_block_attestations, next_block_aggregate_attestations, @@ -323,12 +348,38 @@ impl TestRig { } } + pub fn enqueue_gossip_data_columns(&self, col_index: usize) { + if let Some(data_columns) = self.next_data_columns.as_ref() { + let data_column = data_columns.get(col_index).unwrap(); + self.network_beacon_processor + .send_gossip_data_column_sidecar( + junk_message_id(), + junk_peer_id(), + Client::default(), + DataColumnSubnetId::from_column_index(data_column.index, &self.chain.spec), + data_column.clone(), + Duration::from_secs(0), + ) + .unwrap(); + } + } + + pub fn custody_columns_count(&self) -> usize { + self.network_beacon_processor + .network_globals + .custody_columns_count() as usize + } + pub fn enqueue_rpc_block(&self) { let block_root = self.next_block.canonical_root(); self.network_beacon_processor .send_rpc_beacon_block( block_root, - RpcBlock::new_without_blobs(Some(block_root), self.next_block.clone()), + RpcBlock::new_without_blobs( + Some(block_root), + self.next_block.clone(), + self.custody_columns_count(), + ), std::time::Duration::default(), BlockProcessType::SingleBlock { id: 0 }, ) @@ -340,7 +391,11 @@ impl TestRig { self.network_beacon_processor .send_rpc_beacon_block( block_root, - RpcBlock::new_without_blobs(Some(block_root), self.next_block.clone()), + RpcBlock::new_without_blobs( + Some(block_root), + self.next_block.clone(), + self.custody_columns_count(), + ), std::time::Duration::default(), BlockProcessType::SingleBlock { id: 1 }, ) @@ -361,6 +416,19 @@ impl TestRig { } } + pub fn enqueue_single_lookup_rpc_data_columns(&self) { + if let Some(data_columns) = self.next_data_columns.clone() { + self.network_beacon_processor + .send_rpc_custody_columns( + self.next_block.canonical_root(), + data_columns, + Duration::default(), + BlockProcessType::SingleCustodyColumn(1), + ) + .unwrap(); + } + } + pub fn enqueue_blobs_by_range_request(&self, count: u64) { self.network_beacon_processor .send_blobs_by_range_request( @@ -618,6 +686,13 @@ async fn import_gossip_block_acceptably_early() { .await; } + let num_data_columns = rig.next_data_columns.as_ref().map(|c| c.len()).unwrap_or(0); + for i in 0..num_data_columns { + rig.enqueue_gossip_data_columns(i); + rig.assert_event_journal_completes(&[WorkType::GossipDataColumnSidecar]) + .await; + } + // Note: this section of the code is a bit race-y. We're assuming that we can set the slot clock // and check the head in the time between the block arrived early and when its due for // processing. @@ -694,19 +769,20 @@ async fn import_gossip_block_at_current_slot() { rig.assert_event_journal_completes(&[WorkType::GossipBlock]) .await; - let num_blobs = rig - .next_blobs - .as_ref() - .map(|blobs| blobs.len()) - .unwrap_or(0); - + let num_blobs = rig.next_blobs.as_ref().map(|b| b.len()).unwrap_or(0); for i in 0..num_blobs { rig.enqueue_gossip_blob(i); - rig.assert_event_journal_completes(&[WorkType::GossipBlobSidecar]) .await; } + let num_data_columns = rig.next_data_columns.as_ref().map(|c| c.len()).unwrap_or(0); + for i in 0..num_data_columns { + rig.enqueue_gossip_data_columns(i); + rig.assert_event_journal_completes(&[WorkType::GossipDataColumnSidecar]) + .await; + } + assert_eq!( rig.head_root(), rig.next_block.canonical_root(), @@ -759,11 +835,8 @@ async fn attestation_to_unknown_block_processed(import_method: BlockImportMethod ); // Send the block and ensure that the attestation is received back and imported. - let num_blobs = rig - .next_blobs - .as_ref() - .map(|blobs| blobs.len()) - .unwrap_or(0); + let num_blobs = rig.next_blobs.as_ref().map(|b| b.len()).unwrap_or(0); + let num_data_columns = rig.next_data_columns.as_ref().map(|c| c.len()).unwrap_or(0); let mut events = vec![]; match import_method { BlockImportMethod::Gossip => { @@ -773,6 +846,10 @@ async fn attestation_to_unknown_block_processed(import_method: BlockImportMethod rig.enqueue_gossip_blob(i); events.push(WorkType::GossipBlobSidecar); } + for i in 0..num_data_columns { + rig.enqueue_gossip_data_columns(i); + events.push(WorkType::GossipDataColumnSidecar); + } } BlockImportMethod::Rpc => { rig.enqueue_rpc_block(); @@ -781,6 +858,10 @@ async fn attestation_to_unknown_block_processed(import_method: BlockImportMethod rig.enqueue_single_lookup_rpc_blobs(); events.push(WorkType::RpcBlobs); } + if num_data_columns > 0 { + rig.enqueue_single_lookup_rpc_data_columns(); + events.push(WorkType::RpcCustodyColumn); + } } }; @@ -840,11 +921,8 @@ async fn aggregate_attestation_to_unknown_block(import_method: BlockImportMethod ); // Send the block and ensure that the attestation is received back and imported. - let num_blobs = rig - .next_blobs - .as_ref() - .map(|blobs| blobs.len()) - .unwrap_or(0); + let num_blobs = rig.next_blobs.as_ref().map(|b| b.len()).unwrap_or(0); + let num_data_columns = rig.next_data_columns.as_ref().map(|c| c.len()).unwrap_or(0); let mut events = vec![]; match import_method { BlockImportMethod::Gossip => { @@ -854,6 +932,10 @@ async fn aggregate_attestation_to_unknown_block(import_method: BlockImportMethod rig.enqueue_gossip_blob(i); events.push(WorkType::GossipBlobSidecar); } + for i in 0..num_data_columns { + rig.enqueue_gossip_data_columns(i); + events.push(WorkType::GossipDataColumnSidecar) + } } BlockImportMethod::Rpc => { rig.enqueue_rpc_block(); @@ -862,6 +944,10 @@ async fn aggregate_attestation_to_unknown_block(import_method: BlockImportMethod rig.enqueue_single_lookup_rpc_blobs(); events.push(WorkType::RpcBlobs); } + if num_data_columns > 0 { + rig.enqueue_single_lookup_rpc_data_columns(); + events.push(WorkType::RpcCustodyColumn); + } } }; @@ -1046,12 +1132,20 @@ async fn test_rpc_block_reprocessing() { rig.assert_event_journal_completes(&[WorkType::RpcBlock]) .await; - rig.enqueue_single_lookup_rpc_blobs(); - if rig.next_blobs.as_ref().map(|b| b.len()).unwrap_or(0) > 0 { + let num_blobs = rig.next_blobs.as_ref().map(|b| b.len()).unwrap_or(0); + if num_blobs > 0 { + rig.enqueue_single_lookup_rpc_blobs(); rig.assert_event_journal_completes(&[WorkType::RpcBlobs]) .await; } + let num_data_columns = rig.next_data_columns.as_ref().map(|c| c.len()).unwrap_or(0); + if num_data_columns > 0 { + rig.enqueue_single_lookup_rpc_data_columns(); + rig.assert_event_journal_completes(&[WorkType::RpcCustodyColumn]) + .await; + } + // next_block shouldn't be processed since it couldn't get the // duplicate cache handle assert_ne!(next_block_root, rig.head_root()); diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index 328d9349b8..fa50876e0d 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -73,6 +73,8 @@ pub enum RouterMessage { PubsubMessage(MessageId, PeerId, PubsubMessage, bool), /// The peer manager has requested we re-status a peer. StatusPeer(PeerId), + /// The peer has an updated custody group count from METADATA. + PeerUpdatedCustodyGroupCount(PeerId), } impl Router { @@ -155,6 +157,10 @@ impl Router { RouterMessage::PeerDisconnected(peer_id) => { self.send_to_sync(SyncMessage::Disconnect(peer_id)); } + // A peer has updated CGC + RouterMessage::PeerUpdatedCustodyGroupCount(peer_id) => { + self.send_to_sync(SyncMessage::UpdatedPeerCgc(peer_id)); + } RouterMessage::RPCRequestReceived { peer_id, inbound_request_id, diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index 7afd62ab2e..77204b455d 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -485,6 +485,9 @@ impl NetworkService { NetworkEvent::PeerDisconnected(peer_id) => { self.send_to_router(RouterMessage::PeerDisconnected(peer_id)); } + NetworkEvent::PeerUpdatedCustodyGroupCount(peer_id) => { + self.send_to_router(RouterMessage::PeerUpdatedCustodyGroupCount(peer_id)); + } NetworkEvent::RequestReceived { peer_id, inbound_request_id, diff --git a/beacon_node/network/src/subnet_service/tests/mod.rs b/beacon_node/network/src/subnet_service/tests/mod.rs index 6f9e8cd41a..7fdf9047fc 100644 --- a/beacon_node/network/src/subnet_service/tests/mod.rs +++ b/beacon_node/network/src/subnet_service/tests/mod.rs @@ -7,6 +7,8 @@ use beacon_chain::{ }; use genesis::{generate_deterministic_keypairs, interop_genesis_state, DEFAULT_ETH1_BLOCK_HASH}; use lighthouse_network::NetworkConfig; +use rand::rngs::StdRng; +use rand::SeedableRng; use slot_clock::{SlotClock, SystemTimeSlotClock}; use std::sync::{Arc, LazyLock}; use std::time::{Duration, SystemTime}; @@ -76,6 +78,7 @@ impl TestBeaconChain { Duration::from_millis(SLOT_DURATION_MILLIS), )) .shutdown_sender(shutdown_tx) + .rng(Box::new(StdRng::seed_from_u64(42))) .build() .expect("should build"), ); diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index 509caf7316..fcef06271f 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -10,7 +10,9 @@ use crate::network_beacon_processor::ChainSegmentProcessId; use crate::sync::manager::BatchProcessResult; -use crate::sync::network_context::{RangeRequestId, RpcResponseError, SyncNetworkContext}; +use crate::sync::network_context::{ + RangeRequestId, RpcRequestSendError, RpcResponseError, SyncNetworkContext, +}; use crate::sync::range_sync::{ BatchConfig, BatchId, BatchInfo, BatchOperationOutcome, BatchProcessingResult, BatchState, }; @@ -20,10 +22,9 @@ use lighthouse_network::service::api_types::Id; use lighthouse_network::types::{BackFillState, NetworkGlobals}; use lighthouse_network::{PeerAction, PeerId}; use logging::crit; -use rand::seq::SliceRandom; use std::collections::{ btree_map::{BTreeMap, Entry}, - HashMap, HashSet, + HashSet, }; use std::sync::Arc; use tracing::{debug, error, info, instrument, warn}; @@ -121,9 +122,6 @@ pub struct BackFillSync { /// Sorted map of batches undergoing some kind of processing. batches: BTreeMap>, - /// List of peers we are currently awaiting a response for. - active_requests: HashMap>, - /// The current processing batch, if any. current_processing_batch: Option, @@ -176,7 +174,6 @@ impl BackFillSync { let bfs = BackFillSync { batches: BTreeMap::new(), - active_requests: HashMap::new(), processing_target: current_start, current_start, last_batch_downloaded: false, @@ -314,45 +311,11 @@ impl BackFillSync { skip_all )] #[must_use = "A failure here indicates the backfill sync has failed and the global sync state should be updated"] - pub fn peer_disconnected( - &mut self, - peer_id: &PeerId, - network: &mut SyncNetworkContext, - ) -> Result<(), BackFillError> { + pub fn peer_disconnected(&mut self, peer_id: &PeerId) -> Result<(), BackFillError> { if matches!(self.state(), BackFillState::Failed) { return Ok(()); } - if let Some(batch_ids) = self.active_requests.remove(peer_id) { - // fail the batches. - for id in batch_ids { - if let Some(batch) = self.batches.get_mut(&id) { - match batch.download_failed(false) { - Ok(BatchOperationOutcome::Failed { blacklist: _ }) => { - self.fail_sync(BackFillError::BatchDownloadFailed(id))?; - } - Ok(BatchOperationOutcome::Continue) => {} - Err(e) => { - self.fail_sync(BackFillError::BatchInvalidState(id, e.0))?; - } - } - // If we have run out of peers in which to retry this batch, the backfill state - // transitions to a paused state. - // 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!( - batch_id = %id, - error = "no synced peers", - "Batch could not be retried" - ); - } - } else { - debug!(peer = %peer_id, batch = %id, "Batch not found while removing peer"); - } - } - } - // Remove the peer from the participation list self.participating_peers.remove(peer_id); Ok(()) @@ -386,15 +349,12 @@ impl BackFillSync { return Ok(()); } debug!(batch_epoch = %batch_id, error = ?err, "Batch download failed"); - if let Some(active_requests) = self.active_requests.get_mut(peer_id) { - active_requests.remove(&batch_id); - } - match batch.download_failed(true) { + match batch.download_failed(Some(*peer_id)) { Err(e) => self.fail_sync(BackFillError::BatchInvalidState(batch_id, e.0)), Ok(BatchOperationOutcome::Failed { blacklist: _ }) => { self.fail_sync(BackFillError::BatchDownloadFailed(batch_id)) } - Ok(BatchOperationOutcome::Continue) => self.retry_batch_download(network, batch_id), + Ok(BatchOperationOutcome::Continue) => self.send_batch(network, batch_id), } } else { // this could be an error for an old batch, removed when the chain advances @@ -435,19 +395,11 @@ impl BackFillSync { // sending an error /timeout) if the peer is removed from the chain for other // reasons. Check that this block belongs to the expected peer, and that the // request_id matches - // TODO(das): removed peer_id matching as the node may request a different peer for data - // columns. if !batch.is_expecting_block(&request_id) { return Ok(ProcessResult::Successful); } - // A stream termination has been sent. This batch has ended. Process a completed batch. - // Remove the request from the peer's active batches - self.active_requests - .get_mut(peer_id) - .map(|active_requests| active_requests.remove(&batch_id)); - - match batch.download_completed(blocks) { + match batch.download_completed(blocks, *peer_id) { Ok(received) => { let awaiting_batches = self.processing_target.saturating_sub(batch_id) / BACKFILL_EPOCHS_PER_BATCH; @@ -488,7 +440,6 @@ impl BackFillSync { self.set_state(BackFillState::Failed); // Remove all batches and active requests and participating peers. self.batches.clear(); - self.active_requests.clear(); self.participating_peers.clear(); self.restart_failed_sync = false; @@ -622,7 +573,7 @@ impl BackFillSync { } }; - let Some(peer) = batch.current_peer() else { + let Some(peer) = batch.processing_peer() else { self.fail_sync(BackFillError::BatchInvalidState( batch_id, String::from("Peer does not exist"), @@ -698,6 +649,8 @@ impl BackFillSync { ); for peer in self.participating_peers.drain() { + // TODO(das): `participating_peers` only includes block peers. Should we + // penalize the custody column peers too? network.report_peer(peer, *penalty, "backfill_batch_failed"); } self.fail_sync(BackFillError::BatchProcessingFailed(batch_id)) @@ -723,7 +676,7 @@ impl BackFillSync { { self.fail_sync(BackFillError::BatchInvalidState(batch_id, e.0))?; } - self.retry_batch_download(network, batch_id)?; + self.send_batch(network, batch_id)?; Ok(ProcessResult::Successful) } } @@ -864,12 +817,7 @@ impl BackFillSync { } } } - BatchState::Downloading(peer, ..) => { - // remove this batch from the peer's active requests - if let Some(active_requests) = self.active_requests.get_mut(peer) { - active_requests.remove(&id); - } - } + BatchState::Downloading(..) => {} BatchState::Failed | BatchState::Poisoned | BatchState::AwaitingDownload => { crit!("batch indicates inconsistent chain state while advancing chain") } @@ -951,57 +899,10 @@ impl BackFillSync { self.processing_target = self.current_start; for id in redownload_queue { - self.retry_batch_download(network, id)?; + self.send_batch(network, id)?; } // finally, re-request the failed batch. - self.retry_batch_download(network, batch_id) - } - - /// Sends and registers the request of a batch awaiting download. - #[instrument(parent = None, - level = "info", - fields(service = "backfill_sync"), - name = "backfill_sync", - skip_all - )] - fn retry_batch_download( - &mut self, - network: &mut SyncNetworkContext, - batch_id: BatchId, - ) -> Result<(), BackFillError> { - let Some(batch) = self.batches.get_mut(&batch_id) else { - return Ok(()); - }; - - // Find a peer to request the batch - let failed_peers = batch.failed_peers(); - - let new_peer = self - .network_globals - .peers - .read() - .synced_peers() - .map(|peer| { - ( - failed_peers.contains(peer), - self.active_requests.get(peer).map(|v| v.len()).unwrap_or(0), - rand::random::(), - *peer, - ) - }) - // Sort peers prioritizing unrelated peers with less active requests. - .min() - .map(|(_, _, _, peer)| peer); - - if let Some(peer) = new_peer { - self.participating_peers.insert(peer); - self.send_batch(network, batch_id, peer) - } else { - // If we are here the chain has no more synced peers - info!(reason = "insufficient_synced_peers", "Backfill sync paused"); - self.set_state(BackFillState::Paused); - Err(BackFillError::Paused) - } + self.send_batch(network, batch_id) } /// Requests the batch assigned to the given id from a given peer. @@ -1015,53 +916,65 @@ impl BackFillSync { &mut self, network: &mut SyncNetworkContext, batch_id: BatchId, - peer: PeerId, ) -> Result<(), BackFillError> { if let Some(batch) = self.batches.get_mut(&batch_id) { + let synced_peers = self + .network_globals + .peers + .read() + .synced_peers() + .cloned() + .collect::>(); + let (request, is_blob_batch) = batch.to_blocks_by_range_request(); + let failed_peers = batch.failed_peers(); match network.block_components_by_range_request( - peer, is_blob_batch, request, RangeRequestId::BackfillSync { batch_id }, + &synced_peers, + &failed_peers, ) { Ok(request_id) => { // inform the batch about the new request - if let Err(e) = batch.start_downloading_from_peer(peer, request_id) { + if let Err(e) = batch.start_downloading(request_id) { return self.fail_sync(BackFillError::BatchInvalidState(batch_id, e.0)); } debug!(epoch = %batch_id, %batch, "Requesting batch"); - // register the batch for this peer - self.active_requests - .entry(peer) - .or_default() - .insert(batch_id); return Ok(()); } - Err(e) => { - // NOTE: under normal conditions this shouldn't happen but we handle it anyway - warn!(%batch_id, error = ?e, %batch,"Could not send batch request"); - // register the failed download and check if the batch can be retried - if let Err(e) = batch.start_downloading_from_peer(peer, 1) { - return self.fail_sync(BackFillError::BatchInvalidState(batch_id, e.0)); + Err(e) => match e { + RpcRequestSendError::NoPeer(no_peer) => { + // If we are here the chain has no more synced peers + info!( + "reason" = format!("insufficient_synced_peers({no_peer:?})"), + "Backfill sync paused" + ); + self.set_state(BackFillState::Paused); + return Err(BackFillError::Paused); } - self.active_requests - .get_mut(&peer) - .map(|request| request.remove(&batch_id)); + RpcRequestSendError::InternalError(e) => { + // NOTE: under normal conditions this shouldn't happen but we handle it anyway + warn!(%batch_id, error = ?e, %batch,"Could not send batch request"); + // register the failed download and check if the batch can be retried + if let Err(e) = batch.start_downloading(1) { + return self.fail_sync(BackFillError::BatchInvalidState(batch_id, e.0)); + } - match batch.download_failed(true) { - Err(e) => { - self.fail_sync(BackFillError::BatchInvalidState(batch_id, e.0))? - } - Ok(BatchOperationOutcome::Failed { blacklist: _ }) => { - self.fail_sync(BackFillError::BatchDownloadFailed(batch_id))? - } - Ok(BatchOperationOutcome::Continue) => { - return self.retry_batch_download(network, batch_id) + match batch.download_failed(None) { + Err(e) => { + self.fail_sync(BackFillError::BatchInvalidState(batch_id, e.0))? + } + Ok(BatchOperationOutcome::Failed { blacklist: _ }) => { + self.fail_sync(BackFillError::BatchDownloadFailed(batch_id))? + } + Ok(BatchOperationOutcome::Continue) => { + return self.send_batch(network, batch_id) + } } } - } + }, } } @@ -1093,7 +1006,7 @@ impl BackFillSync { .collect::>(); for batch_id in batch_ids_to_retry { - self.retry_batch_download(network, batch_id)?; + self.send_batch(network, batch_id)?; } Ok(()) } @@ -1115,34 +1028,16 @@ impl BackFillSync { } // find the next pending batch and request it from the peer - - // randomize the peers for load balancing - let mut rng = rand::thread_rng(); - let mut idle_peers = self - .network_globals - .peers - .read() - .synced_peers() - .filter(|peer_id| { - self.active_requests - .get(peer_id) - .map(|requests| requests.is_empty()) - .unwrap_or(true) - }) - .cloned() - .collect::>(); - - idle_peers.shuffle(&mut rng); - - while let Some(peer) = idle_peers.pop() { - if let Some(batch_id) = self.include_next_batch(network) { - // send the batch - self.send_batch(network, batch_id, peer)?; - } else { - // No more batches, simply stop - return Ok(()); - } + // Note: for this function to not infinite loop we must: + // - If `include_next_batch` returns Some we MUST increase the count of batches that are + // accounted in the `BACKFILL_BATCH_BUFFER_SIZE` limit in the `matches!` statement of + // that function. + while let Some(batch_id) = self.include_next_batch(network) { + // send the batch + self.send_batch(network, batch_id)?; } + + // No more batches, simply stop Ok(()) } @@ -1296,3 +1191,73 @@ enum ResetEpochError { /// The chain has already completed. SyncCompleted, } + +#[cfg(test)] +mod tests { + use super::*; + use beacon_chain::test_utils::BeaconChainHarness; + use bls::Hash256; + use lighthouse_network::{NetworkConfig, SyncInfo, SyncStatus}; + use rand::prelude::StdRng; + use rand::SeedableRng; + use types::MinimalEthSpec; + + #[test] + fn request_batches_should_not_loop_infinitely() { + let harness = BeaconChainHarness::builder(MinimalEthSpec) + .default_spec() + .deterministic_keypairs(4) + .fresh_ephemeral_store() + .build(); + + let beacon_chain = harness.chain.clone(); + let slots_per_epoch = MinimalEthSpec::slots_per_epoch(); + + let network_globals = Arc::new(NetworkGlobals::new_test_globals( + vec![], + Arc::new(NetworkConfig::default()), + beacon_chain.spec.clone(), + )); + + { + let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); + let peer_id = network_globals + .peers + .write() + .__add_connected_peer_testing_only( + true, + &beacon_chain.spec, + k256::ecdsa::SigningKey::random(&mut rng).into(), + ); + + // Simulate finalized epoch and head being 2 epochs ahead + let finalized_epoch = Epoch::new(40); + let head_epoch = finalized_epoch + 2; + let head_slot = head_epoch.start_slot(slots_per_epoch) + 1; + + network_globals.peers.write().update_sync_status( + &peer_id, + SyncStatus::Synced { + info: SyncInfo { + head_slot, + head_root: Hash256::random(), + finalized_epoch, + finalized_root: Hash256::random(), + }, + }, + ); + } + + let mut network = SyncNetworkContext::new_for_testing( + beacon_chain.clone(), + network_globals.clone(), + harness.runtime.task_executor.clone(), + ); + + let mut backfill = BackFillSync::new(beacon_chain, network_globals); + backfill.set_state(BackFillState::Syncing); + + // if this ends up running into an infinite loop, the test will overflow the stack pretty quickly. + let _ = backfill.request_batches(&mut network); + } +} diff --git a/beacon_node/network/src/sync/block_lookups/common.rs b/beacon_node/network/src/sync/block_lookups/common.rs index 8eefb2d675..86b6894bac 100644 --- a/beacon_node/network/src/sync/block_lookups/common.rs +++ b/beacon_node/network/src/sync/block_lookups/common.rs @@ -6,7 +6,6 @@ use crate::sync::block_lookups::{ }; use crate::sync::manager::BlockProcessType; use crate::sync::network_context::{LookupRequestResult, SyncNetworkContext}; -use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::BeaconChainTypes; use lighthouse_network::service::api_types::Id; use parking_lot::RwLock; @@ -97,13 +96,8 @@ impl RequestState for BlockRequestState { seen_timestamp, .. } = download_result; - cx.send_block_for_processing( - id, - block_root, - RpcBlock::new_without_blobs(Some(block_root), value), - seen_timestamp, - ) - .map_err(LookupRequestError::SendFailedProcessor) + cx.send_block_for_processing(id, block_root, value, seen_timestamp) + .map_err(LookupRequestError::SendFailedProcessor) } fn response_type() -> ResponseType { diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index ef9285c8dc..99428b0c80 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -266,7 +266,8 @@ impl RangeBlockComponentsRequest { ) .map_err(|e| format!("{e:?}"))? } else { - RpcBlock::new_without_blobs(Some(block_root), block) + // Block has no data, expects zero columns + RpcBlock::new_without_blobs(Some(block_root), block, 0) }); } diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 84e492c04f..473881f182 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -106,6 +106,9 @@ pub enum SyncMessage { head_slot: Option, }, + /// Peer manager has received a MetaData of a peer with a new or updated CGC value. + UpdatedPeerCgc(PeerId), + /// A block has been received from the RPC. RpcBlock { sync_request_id: SyncRequestId, @@ -476,6 +479,16 @@ impl SyncManager { } } + fn updated_peer_cgc(&mut self, _peer_id: PeerId) { + // Try to make progress on custody requests that are waiting for peers + for (id, result) in self.network.continue_custody_by_root_requests() { + self.on_custody_by_root_result(id, result); + } + + // Attempt to resume range sync too + self.range_sync.resume(&mut self.network); + } + /// Handles RPC errors related to requests that were emitted from the sync manager. fn inject_error(&mut self, peer_id: PeerId, sync_request_id: SyncRequestId, error: RPCError) { trace!("Sync manager received a failed RPC"); @@ -515,9 +528,7 @@ impl SyncManager { // Remove peer from all data structures self.range_sync.peer_disconnect(&mut self.network, peer_id); - let _ = self - .backfill_sync - .peer_disconnected(peer_id, &mut self.network); + let _ = self.backfill_sync.peer_disconnected(peer_id); self.block_lookups.peer_disconnected(peer_id); // Regardless of the outcome, we update the sync status. @@ -750,6 +761,13 @@ impl SyncManager { } => { self.add_peers_force_range_sync(&peers, head_root, head_slot); } + SyncMessage::UpdatedPeerCgc(peer_id) => { + debug!( + peer_id = ?peer_id, + "Received updated peer CGC message" + ); + self.updated_peer_cgc(peer_id); + } SyncMessage::RpcBlock { sync_request_id, peer_id, diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 69b350f8cb..58641f8606 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -9,6 +9,8 @@ use super::range_sync::ByRangeRequestType; use super::SyncMessage; use crate::metrics; use crate::network_beacon_processor::NetworkBeaconProcessor; +#[cfg(test)] +use crate::network_beacon_processor::TestBeaconChainType; use crate::service::NetworkMessage; use crate::status::ToStatusMessage; use crate::sync::block_lookups::SingleLookupId; @@ -27,18 +29,20 @@ use lighthouse_network::service::api_types::{ }; use lighthouse_network::{Client, NetworkGlobals, PeerAction, PeerId, ReportSource}; use parking_lot::RwLock; -use rand::prelude::IteratorRandom; -use rand::thread_rng; pub use requests::LookupVerifyError; use requests::{ ActiveRequests, BlobsByRangeRequestItems, BlobsByRootRequestItems, BlocksByRangeRequestItems, BlocksByRootRequestItems, DataColumnsByRangeRequestItems, DataColumnsByRootRequestItems, }; +#[cfg(test)] +use slot_clock::SlotClock; use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet}; use std::fmt::Debug; use std::sync::Arc; use std::time::Duration; +#[cfg(test)] +use task_executor::TaskExecutor; use tokio::sync::mpsc; use tracing::{debug, error, span, warn, Level}; use types::blob_sidecar::FixedBlobSidecarList; @@ -82,24 +86,18 @@ pub enum RpcResponseError { #[derive(Debug, PartialEq, Eq)] pub enum RpcRequestSendError { - /// Network channel send failed - NetworkSendError, - NoCustodyPeers, - CustodyRequestError(custody::Error), - SlotClockError, + /// No peer available matching the required criteria + NoPeer(NoPeerError), + /// These errors should never happen, including unreachable custody errors or network send + /// errors. + InternalError(String), } -impl std::fmt::Display for RpcRequestSendError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - RpcRequestSendError::NetworkSendError => write!(f, "Network send error"), - RpcRequestSendError::NoCustodyPeers => write!(f, "No custody peers"), - RpcRequestSendError::CustodyRequestError(e) => { - write!(f, "Custody request error: {:?}", e) - } - RpcRequestSendError::SlotClockError => write!(f, "Slot clock error"), - } - } +/// Type of peer missing that caused a `RpcRequestSendError::NoPeers` +#[derive(Debug, PartialEq, Eq)] +pub enum NoPeerError { + BlockPeer, + CustodyPeer(ColumnIndex), } #[derive(Debug, PartialEq, Eq)] @@ -232,6 +230,35 @@ pub enum RangeBlockComponent { ), } +#[cfg(test)] +impl SyncNetworkContext> { + pub fn new_for_testing( + beacon_chain: Arc>>, + network_globals: Arc>, + task_executor: TaskExecutor, + ) -> Self { + let fork_context = Arc::new(ForkContext::new::( + beacon_chain.slot_clock.now().unwrap_or(Slot::new(0)), + beacon_chain.genesis_validators_root, + &beacon_chain.spec, + )); + let (network_tx, _network_rx) = mpsc::unbounded_channel(); + let (beacon_processor, _) = NetworkBeaconProcessor::null_for_testing( + network_globals, + mpsc::unbounded_channel().0, + beacon_chain.clone(), + task_executor, + ); + + SyncNetworkContext::new( + network_tx, + Arc::new(beacon_processor), + beacon_chain, + fork_context, + ) + } +} + impl SyncNetworkContext { pub fn new( network_send: mpsc::UnboundedSender>, @@ -331,12 +358,6 @@ impl SyncNetworkContext { .custody_peers_for_column(column_index) } - pub fn get_random_custodial_peer(&self, column_index: ColumnIndex) -> Option { - self.get_custodial_peers(column_index) - .into_iter() - .choose(&mut thread_rng()) - } - pub fn network_globals(&self) -> &NetworkGlobals { &self.network_beacon_processor.network_globals } @@ -381,34 +402,102 @@ impl SyncNetworkContext { } } + fn active_request_count_by_peer(&self) -> HashMap { + let Self { + network_send: _, + request_id: _, + blocks_by_root_requests, + blobs_by_root_requests, + data_columns_by_root_requests, + blocks_by_range_requests, + blobs_by_range_requests, + data_columns_by_range_requests, + // custody_by_root_requests is a meta request of data_columns_by_root_requests + custody_by_root_requests: _, + // components_by_range_requests is a meta request of various _by_range requests + components_by_range_requests: _, + execution_engine_state: _, + network_beacon_processor: _, + chain: _, + fork_context: _, + // Don't use a fallback match. We want to be sure that all requests are considered when + // adding new ones + } = self; + + let mut active_request_count_by_peer = HashMap::::new(); + + for peer_id in blocks_by_root_requests + .iter_request_peers() + .chain(blobs_by_root_requests.iter_request_peers()) + .chain(data_columns_by_root_requests.iter_request_peers()) + .chain(blocks_by_range_requests.iter_request_peers()) + .chain(blobs_by_range_requests.iter_request_peers()) + .chain(data_columns_by_range_requests.iter_request_peers()) + { + *active_request_count_by_peer.entry(peer_id).or_default() += 1; + } + + active_request_count_by_peer + } + /// A blocks by range request sent by the range sync algorithm pub fn block_components_by_range_request( &mut self, - peer_id: PeerId, batch_type: ByRangeRequestType, request: BlocksByRangeRequest, requester: RangeRequestId, + peers: &HashSet, + peers_to_deprioritize: &HashSet, ) -> Result { + let active_request_count_by_peer = self.active_request_count_by_peer(); + + let Some(block_peer) = peers + .iter() + .map(|peer| { + ( + // If contains -> 1 (order after), not contains -> 0 (order first) + peers_to_deprioritize.contains(peer), + // Prefer peers with less overall requests + active_request_count_by_peer.get(peer).copied().unwrap_or(0), + // Random factor to break ties, otherwise the PeerID breaks ties + rand::random::(), + peer, + ) + }) + .min() + .map(|(_, _, _, peer)| *peer) + else { + // Backfill and forward sync handle this condition gracefully. + // - Backfill sync: will pause waiting for more peers to join + // - Forward sync: can never happen as the chain is dropped when removing the last peer. + return Err(RpcRequestSendError::NoPeer(NoPeerError::BlockPeer)); + }; + + // Attempt to find all required custody peers before sending any request or creating an ID + let columns_by_range_peers_to_request = + if matches!(batch_type, ByRangeRequestType::BlocksAndColumns) { + let column_indexes = self.network_globals().sampling_columns.clone(); + Some(self.select_columns_by_range_peers_to_request( + &column_indexes, + peers, + active_request_count_by_peer, + peers_to_deprioritize, + )?) + } else { + None + }; + // Create the overall components_by_range request ID before its individual components let id = ComponentsByRangeRequestId { id: self.next_id(), requester, }; - // Compute custody column peers before sending the blocks_by_range request. If we don't have - // enough peers, error here. - let data_column_requests = if matches!(batch_type, ByRangeRequestType::BlocksAndColumns) { - let column_indexes = self.network_globals().sampling_columns.clone(); - Some(self.make_columns_by_range_requests(request.clone(), &column_indexes)?) - } else { - None - }; - - let blocks_req_id = self.send_blocks_by_range_request(peer_id, request.clone(), id)?; + let blocks_req_id = self.send_blocks_by_range_request(block_peer, request.clone(), id)?; let blobs_req_id = if matches!(batch_type, ByRangeRequestType::BlocksAndBlobs) { Some(self.send_blobs_by_range_request( - peer_id, + block_peer, BlobsByRangeRequest { start_slot: *request.start_slot(), count: *request.count(), @@ -419,64 +508,98 @@ impl SyncNetworkContext { None }; - let data_columns = if let Some(data_column_requests) = data_column_requests { - let data_column_requests = data_column_requests - .into_iter() - .map(|(peer_id, columns_by_range_request)| { - self.send_data_columns_by_range_request(peer_id, columns_by_range_request, id) - }) - .collect::, _>>()?; + let data_column_requests = columns_by_range_peers_to_request + .map(|columns_by_range_peers_to_request| { + columns_by_range_peers_to_request + .into_iter() + .map(|(peer_id, columns)| { + self.send_data_columns_by_range_request( + peer_id, + DataColumnsByRangeRequest { + start_slot: *request.start_slot(), + count: *request.count(), + columns, + }, + id, + ) + }) + .collect::, _>>() + }) + .transpose()?; - Some(( - data_column_requests, - self.network_globals() - .sampling_columns - .iter() - .cloned() - .collect::>(), - )) - } else { - None - }; - - let info = RangeBlockComponentsRequest::new(blocks_req_id, blobs_req_id, data_columns); + let info = RangeBlockComponentsRequest::new( + blocks_req_id, + blobs_req_id, + data_column_requests.map(|data_column_requests| { + ( + data_column_requests, + self.network_globals() + .sampling_columns + .clone() + .iter() + .copied() + .collect(), + ) + }), + ); self.components_by_range_requests.insert(id, info); Ok(id.id) } - fn make_columns_by_range_requests( + fn select_columns_by_range_peers_to_request( &self, - request: BlocksByRangeRequest, custody_indexes: &HashSet, - ) -> Result, RpcRequestSendError> { - let mut peer_id_to_request_map = HashMap::new(); + peers: &HashSet, + active_request_count_by_peer: HashMap, + peers_to_deprioritize: &HashSet, + ) -> Result>, RpcRequestSendError> { + let mut columns_to_request_by_peer = HashMap::>::new(); for column_index in custody_indexes { - // TODO(das): The peer selection logic here needs to be improved - we should probably - // avoid retrying from failed peers, however `BatchState` currently only tracks the peer - // serving the blocks. - let Some(custody_peer) = self.get_random_custodial_peer(*column_index) else { + // Strictly consider peers that are custodials of this column AND are part of this + // syncing chain. If the forward range sync chain has few peers, it's likely that this + // function will not be able to find peers on our custody columns. + let Some(custody_peer) = peers + .iter() + .filter(|peer| { + self.network_globals() + .is_custody_peer_of(*column_index, peer) + }) + .map(|peer| { + ( + // If contains -> 1 (order after), not contains -> 0 (order first) + peers_to_deprioritize.contains(peer), + // Prefer peers with less overall requests + // Also account for requests that are not yet issued tracked in peer_id_to_request_map + // We batch requests to the same peer, so count existance in the + // `columns_to_request_by_peer` as a single 1 request. + active_request_count_by_peer.get(peer).copied().unwrap_or(0) + + columns_to_request_by_peer.get(peer).map(|_| 1).unwrap_or(0), + // Random factor to break ties, otherwise the PeerID breaks ties + rand::random::(), + peer, + ) + }) + .min() + .map(|(_, _, _, peer)| *peer) + else { // TODO(das): this will be pretty bad UX. To improve we should: - // - Attempt to fetch custody requests first, before requesting blocks // - Handle the no peers case gracefully, maybe add some timeout and give a few // minutes / seconds to the peer manager to locate peers on this subnet before // abandoing progress on the chain completely. - return Err(RpcRequestSendError::NoCustodyPeers); + return Err(RpcRequestSendError::NoPeer(NoPeerError::CustodyPeer( + *column_index, + ))); }; - let columns_by_range_request = peer_id_to_request_map + columns_to_request_by_peer .entry(custody_peer) - .or_insert_with(|| DataColumnsByRangeRequest { - start_slot: *request.start_slot(), - count: *request.count(), - columns: vec![], - }); - - columns_by_range_request.columns.push(*column_index); + .or_default() + .push(*column_index); } - Ok(peer_id_to_request_map) + Ok(columns_to_request_by_peer) } /// Received a blocks by range or blobs by range response for a request that couples blocks ' @@ -536,11 +659,21 @@ impl SyncNetworkContext { lookup_peers: Arc>>, block_root: Hash256, ) -> Result { + let active_request_count_by_peer = self.active_request_count_by_peer(); let Some(peer_id) = lookup_peers .read() .iter() - .choose(&mut rand::thread_rng()) - .copied() + .map(|peer| { + ( + // Prefer peers with less overall requests + active_request_count_by_peer.get(peer).copied().unwrap_or(0), + // Random factor to break ties, otherwise the PeerID breaks ties + rand::random::(), + peer, + ) + }) + .min() + .map(|(_, _, peer)| *peer) else { // Allow lookup to not have any peers and do nothing. This is an optimization to not // lose progress of lookups created from a block with unknown parent before we receive @@ -597,7 +730,7 @@ impl SyncNetworkContext { request: RequestType::BlocksByRoot(request.into_request(&self.fork_context)), app_request_id: AppRequestId::Sync(SyncRequestId::SingleBlock { id }), }) - .map_err(|_| RpcRequestSendError::NetworkSendError)?; + .map_err(|_| RpcRequestSendError::InternalError("network send error".to_owned()))?; debug!( method = "BlocksByRoot", @@ -632,11 +765,21 @@ impl SyncNetworkContext { block_root: Hash256, expected_blobs: usize, ) -> Result { + let active_request_count_by_peer = self.active_request_count_by_peer(); let Some(peer_id) = lookup_peers .read() .iter() - .choose(&mut rand::thread_rng()) - .copied() + .map(|peer| { + ( + // Prefer peers with less overall requests + active_request_count_by_peer.get(peer).copied().unwrap_or(0), + // Random factor to break ties, otherwise the PeerID breaks ties + rand::random::(), + peer, + ) + }) + .min() + .map(|(_, _, peer)| *peer) else { // Allow lookup to not have any peers and do nothing. This is an optimization to not // lose progress of lookups created from a block with unknown parent before we receive @@ -686,7 +829,7 @@ impl SyncNetworkContext { request: RequestType::BlobsByRoot(request.clone().into_request(&self.fork_context)), app_request_id: AppRequestId::Sync(SyncRequestId::SingleBlob { id }), }) - .map_err(|_| RpcRequestSendError::NetworkSendError)?; + .map_err(|_| RpcRequestSendError::InternalError("network send error".to_owned()))?; debug!( method = "BlobsByRoot", @@ -732,7 +875,11 @@ impl SyncNetworkContext { self.send_network_msg(NetworkMessage::SendRequest { peer_id, - request: RequestType::DataColumnsByRoot(request.clone().into_request(&self.chain.spec)), + request: RequestType::DataColumnsByRoot( + request + .clone() + .try_into_request(self.fork_context.current_fork(), &self.chain.spec)?, + ), app_request_id: AppRequestId::Sync(SyncRequestId::DataColumnsByRoot(id)), })?; @@ -821,7 +968,25 @@ impl SyncNetworkContext { self.custody_by_root_requests.insert(requester, request); Ok(LookupRequestResult::RequestSent(id.req_id)) } - Err(e) => Err(RpcRequestSendError::CustodyRequestError(e)), + Err(e) => Err(match e { + CustodyRequestError::NoPeer(column_index) => { + RpcRequestSendError::NoPeer(NoPeerError::CustodyPeer(column_index)) + } + // - TooManyFailures: Should never happen, `request` has just been created, it's + // count of download_failures is 0 here + // - BadState: Should never happen, a bad state can only happen when handling a + // network response + // - UnexpectedRequestId: Never happens: this Err is only constructed handling a + // download or processing response + // - SendFailed: Should never happen unless in a bad drop sequence when shutting + // down the node + e @ (CustodyRequestError::TooManyFailures + | CustodyRequestError::BadState { .. } + | CustodyRequestError::UnexpectedRequestId { .. } + | CustodyRequestError::SendFailed { .. }) => { + RpcRequestSendError::InternalError(format!("{e:?}")) + } + }), } } @@ -841,7 +1006,7 @@ impl SyncNetworkContext { request: RequestType::BlocksByRange(request.clone().into()), app_request_id: AppRequestId::Sync(SyncRequestId::BlocksByRange(id)), }) - .map_err(|_| RpcRequestSendError::NetworkSendError)?; + .map_err(|_| RpcRequestSendError::InternalError("network send error".to_owned()))?; debug!( method = "BlocksByRange", @@ -882,7 +1047,7 @@ impl SyncNetworkContext { request: RequestType::BlobsByRange(request.clone()), app_request_id: AppRequestId::Sync(SyncRequestId::BlobsByRange(id)), }) - .map_err(|_| RpcRequestSendError::NetworkSendError)?; + .map_err(|_| RpcRequestSendError::InternalError("network send error".to_owned()))?; debug!( method = "BlobsByRange", @@ -921,7 +1086,7 @@ impl SyncNetworkContext { request: RequestType::DataColumnsByRange(request.clone()), app_request_id: AppRequestId::Sync(SyncRequestId::DataColumnsByRange(id)), }) - .map_err(|_| RpcRequestSendError::NetworkSendError)?; + .map_err(|_| RpcRequestSendError::InternalError("network send error".to_owned()))?; debug!( method = "DataColumnsByRange", @@ -1308,7 +1473,7 @@ impl SyncNetworkContext { &self, id: Id, block_root: Hash256, - block: RpcBlock, + block: Arc>, seen_timestamp: Duration, ) -> Result<(), SendErrorProcessor> { let span = span!( @@ -1322,6 +1487,12 @@ impl SyncNetworkContext { .beacon_processor_if_enabled() .ok_or(SendErrorProcessor::ProcessorNotAvailable)?; + let block = RpcBlock::new_without_blobs( + Some(block_root), + block, + self.network_globals().custody_columns_count() as usize, + ); + debug!(block = ?block_root, id, "Sending block for processing"); // Lookup sync event safety: If `beacon_processor.send_rpc_beacon_block` returns Ok() sync // must receive a single `SyncMessage::BlockComponentProcessed` with this process type diff --git a/beacon_node/network/src/sync/network_context/custody.rs b/beacon_node/network/src/sync/network_context/custody.rs index e7e6e62349..f4d010b881 100644 --- a/beacon_node/network/src/sync/network_context/custody.rs +++ b/beacon_node/network/src/sync/network_context/custody.rs @@ -45,7 +45,7 @@ pub enum Error { SendFailed(&'static str), TooManyFailures, BadState(String), - NoPeers(ColumnIndex), + NoPeer(ColumnIndex), /// Received a download result for a different request id than the in-flight request. /// There should only exist a single request at a time. Having multiple requests is a bug and /// can result in undefined state, so it's treated as a hard error and the lookup is dropped. @@ -56,7 +56,6 @@ pub enum Error { } struct ActiveBatchColumnsRequest { - peer_id: PeerId, indices: Vec, } @@ -220,6 +219,7 @@ impl ActiveCustodyRequest { return Ok(Some((columns, peer_group, max_seen_timestamp))); } + let active_request_count_by_peer = cx.active_request_count_by_peer(); let mut columns_to_request_by_peer = HashMap::>::new(); let lookup_peers = self.lookup_peers.read(); @@ -238,15 +238,11 @@ impl ActiveCustodyRequest { // only query the peers on that fork. Should this case be handled? How to handle it? let custodial_peers = cx.get_custodial_peers(*column_index); - // TODO(das): cache this computation in a OneCell or similar to prevent having to - // run it every loop - let mut active_requests_by_peer = HashMap::::new(); - for batch_request in self.active_batch_columns_requests.values() { - *active_requests_by_peer - .entry(batch_request.peer_id) - .or_default() += 1; - } - + // We draw from the total set of peers, but prioritize those peers who we have + // received an attestation / status / block message claiming to have imported the + // lookup. The frequency of those messages is low, so drawing only from lookup_peers + // could cause many lookups to take much longer or fail as they don't have enough + // custody peers on a given column let mut priorized_peers = custodial_peers .iter() .map(|peer| { @@ -256,9 +252,12 @@ impl ActiveCustodyRequest { // De-prioritize peers that have failed to successfully respond to // requests recently self.failed_peers.contains(peer), - // Prefer peers with less requests to load balance across peers - active_requests_by_peer.get(peer).copied().unwrap_or(0), - // Final random factor to give all peers a shot in each retry + // Prefer peers with fewer requests to load balance across peers. + // We batch requests to the same peer, so count existence in the + // `columns_to_request_by_peer` as a single 1 request. + active_request_count_by_peer.get(peer).copied().unwrap_or(0) + + columns_to_request_by_peer.get(peer).map(|_| 1).unwrap_or(0), + // Random factor to break ties, otherwise the PeerID breaks ties rand::thread_rng().gen::(), *peer, ) @@ -276,7 +275,7 @@ impl ActiveCustodyRequest { // `MAX_STALE_NO_PEERS_DURATION`, else error and drop the request. Note that // lookup will naturally retry when other peers send us attestations for // descendants of this un-available lookup. - return Err(Error::NoPeers(*column_index)); + return Err(Error::NoPeer(*column_index)); } else { // Do not issue requests if there is no custody peer on this column } @@ -306,13 +305,14 @@ impl ActiveCustodyRequest { let column_request = self .column_requests .get_mut(column_index) + // Should never happen: column_index is iterated from column_requests .ok_or(Error::BadState("unknown column_index".to_owned()))?; column_request.on_download_start(req_id)?; } self.active_batch_columns_requests - .insert(req_id, ActiveBatchColumnsRequest { indices, peer_id }); + .insert(req_id, ActiveBatchColumnsRequest { indices }); } LookupRequestResult::NoRequestNeeded(_) => unreachable!(), LookupRequestResult::Pending(_) => unreachable!(), diff --git a/beacon_node/network/src/sync/network_context/requests.rs b/beacon_node/network/src/sync/network_context/requests.rs index c9b85e47b6..963b633ed6 100644 --- a/beacon_node/network/src/sync/network_context/requests.rs +++ b/beacon_node/network/src/sync/network_context/requests.rs @@ -179,6 +179,10 @@ impl ActiveRequests { .collect() } + pub fn iter_request_peers(&self) -> impl Iterator + '_ { + self.requests.values().map(|request| request.peer_id) + } + pub fn len(&self) -> usize { self.requests.len() } diff --git a/beacon_node/network/src/sync/network_context/requests/data_columns_by_root.rs b/beacon_node/network/src/sync/network_context/requests/data_columns_by_root.rs index 4e02737f08..09d7f4b3b7 100644 --- a/beacon_node/network/src/sync/network_context/requests/data_columns_by_root.rs +++ b/beacon_node/network/src/sync/network_context/requests/data_columns_by_root.rs @@ -1,6 +1,9 @@ use lighthouse_network::rpc::methods::DataColumnsByRootRequest; use std::sync::Arc; -use types::{ChainSpec, DataColumnIdentifier, DataColumnSidecar, EthSpec, Hash256}; +use types::{ + ChainSpec, DataColumnSidecar, DataColumnsByRootIdentifier, EthSpec, ForkName, Hash256, + RuntimeVariableList, +}; use super::{ActiveRequestItems, LookupVerifyError}; @@ -11,17 +14,21 @@ pub struct DataColumnsByRootSingleBlockRequest { } impl DataColumnsByRootSingleBlockRequest { - pub fn into_request(self, spec: &ChainSpec) -> DataColumnsByRootRequest { - DataColumnsByRootRequest::new( - self.indices - .into_iter() - .map(|index| DataColumnIdentifier { - block_root: self.block_root, - index, - }) - .collect(), - spec, - ) + pub fn try_into_request( + self, + fork_name: ForkName, + spec: &ChainSpec, + ) -> Result { + let number_of_columns = spec.number_of_columns as usize; + let columns = RuntimeVariableList::new(self.indices, number_of_columns) + .map_err(|_| "Number of indices exceeds total number of columns")?; + Ok(DataColumnsByRootRequest::new( + vec![DataColumnsByRootIdentifier { + block_root: self.block_root, + columns, + }], + spec.max_request_blocks(fork_name), + )) } } diff --git a/beacon_node/network/src/sync/range_sync/batch.rs b/beacon_node/network/src/sync/range_sync/batch.rs index c1ad550376..264f83ee82 100644 --- a/beacon_node/network/src/sync/range_sync/batch.rs +++ b/beacon_node/network/src/sync/range_sync/batch.rs @@ -107,7 +107,7 @@ pub struct BatchInfo { /// Number of processing attempts that have failed but we do not count. non_faulty_processing_attempts: u8, /// The number of download retries this batch has undergone due to a failed request. - failed_download_attempts: Vec, + failed_download_attempts: Vec>, /// State of the batch. state: BatchState, /// Whether this batch contains all blocks or all blocks and blobs. @@ -132,7 +132,7 @@ pub enum BatchState { /// The batch has failed either downloading or processing, but can be requested again. AwaitingDownload, /// The batch is being downloaded. - Downloading(PeerId, Id), + Downloading(Id), /// The batch has been completely downloaded and is ready for processing. AwaitingProcessing(PeerId, Vec>, Instant), /// The batch is being processed. @@ -197,8 +197,8 @@ impl BatchInfo { peers.insert(attempt.peer_id); } - for download in &self.failed_download_attempts { - peers.insert(*download); + for peer in self.failed_download_attempts.iter().flatten() { + peers.insert(*peer); } peers @@ -206,18 +206,17 @@ impl BatchInfo { /// Verifies if an incoming block belongs to this batch. pub fn is_expecting_block(&self, request_id: &Id) -> bool { - if let BatchState::Downloading(_, expected_id) = &self.state { + if let BatchState::Downloading(expected_id) = &self.state { return expected_id == request_id; } false } /// Returns the peer that is currently responsible for progressing the state of the batch. - pub fn current_peer(&self) -> Option<&PeerId> { + pub fn processing_peer(&self) -> Option<&PeerId> { match &self.state { - BatchState::AwaitingDownload | BatchState::Failed => None, - BatchState::Downloading(peer_id, _) - | BatchState::AwaitingProcessing(peer_id, _, _) + BatchState::AwaitingDownload | BatchState::Failed | BatchState::Downloading(..) => None, + BatchState::AwaitingProcessing(peer_id, _, _) | BatchState::Processing(Attempt { peer_id, .. }) | BatchState::AwaitingValidation(Attempt { peer_id, .. }) => Some(peer_id), BatchState::Poisoned => unreachable!("Poisoned batch"), @@ -276,9 +275,10 @@ impl BatchInfo { pub fn download_completed( &mut self, blocks: Vec>, + peer: PeerId, ) -> Result { match self.state.poison() { - BatchState::Downloading(peer, _request_id) => { + BatchState::Downloading(_) => { let received = blocks.len(); self.state = BatchState::AwaitingProcessing(peer, blocks, Instant::now()); Ok(received) @@ -297,19 +297,18 @@ impl BatchInfo { /// Mark the batch as failed and return whether we can attempt a re-download. /// /// This can happen if a peer disconnects or some error occurred that was not the peers fault. - /// THe `mark_failed` parameter, when set to false, does not increment the failed attempts of + /// The `peer` parameter, when set to None, does not increment the failed attempts of /// this batch and register the peer, rather attempts a re-download. #[must_use = "Batch may have failed"] pub fn download_failed( &mut self, - mark_failed: bool, + peer: Option, ) -> Result { match self.state.poison() { - BatchState::Downloading(peer, _request_id) => { + BatchState::Downloading(_) => { // register the attempt and check if the batch can be tried again - if mark_failed { - self.failed_download_attempts.push(peer); - } + self.failed_download_attempts.push(peer); + self.state = if self.failed_download_attempts.len() >= B::max_batch_download_attempts() as usize { @@ -331,14 +330,10 @@ impl BatchInfo { } } - pub fn start_downloading_from_peer( - &mut self, - peer: PeerId, - request_id: Id, - ) -> Result<(), WrongState> { + pub fn start_downloading(&mut self, request_id: Id) -> Result<(), WrongState> { match self.state.poison() { BatchState::AwaitingDownload => { - self.state = BatchState::Downloading(peer, request_id); + self.state = BatchState::Downloading(request_id); Ok(()) } BatchState::Poisoned => unreachable!("Poisoned batch"), @@ -477,8 +472,8 @@ impl std::fmt::Debug for BatchState { BatchState::AwaitingProcessing(ref peer, ref blocks, _) => { write!(f, "AwaitingProcessing({}, {} blocks)", peer, blocks.len()) } - BatchState::Downloading(peer, request_id) => { - write!(f, "Downloading({}, {})", peer, request_id) + BatchState::Downloading(request_id) => { + write!(f, "Downloading({})", request_id) } BatchState::Poisoned => f.write_str("Poisoned"), } diff --git a/beacon_node/network/src/sync/range_sync/chain.rs b/beacon_node/network/src/sync/range_sync/chain.rs index 24045e901b..be01734417 100644 --- a/beacon_node/network/src/sync/range_sync/chain.rs +++ b/beacon_node/network/src/sync/range_sync/chain.rs @@ -2,18 +2,14 @@ use super::batch::{BatchInfo, BatchProcessingResult, BatchState}; use super::RangeSyncType; use crate::metrics; use crate::network_beacon_processor::ChainSegmentProcessId; -use crate::sync::network_context::{RangeRequestId, RpcResponseError}; +use crate::sync::network_context::{RangeRequestId, RpcRequestSendError, RpcResponseError}; use crate::sync::{network_context::SyncNetworkContext, BatchOperationOutcome, BatchProcessResult}; use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::BeaconChainTypes; -use fnv::FnvHashMap; use lighthouse_network::service::api_types::Id; use lighthouse_network::{PeerAction, PeerId}; use logging::crit; -use rand::seq::SliceRandom; -use rand::Rng; use std::collections::{btree_map::Entry, BTreeMap, HashSet}; -use std::fmt; use strum::IntoStaticStr; use tracing::{debug, instrument, warn}; use types::{Epoch, EthSpec, Hash256, Slot}; @@ -92,7 +88,7 @@ pub struct SyncingChain { /// The peers that agree on the `target_head_slot` and `target_head_root` as a canonical chain /// and thus available to download this chain from, as well as the batches we are currently /// requesting. - peers: FnvHashMap>, + peers: HashSet, /// Starting epoch of the next batch that needs to be downloaded. to_be_downloaded: BatchId, @@ -116,16 +112,6 @@ pub struct SyncingChain { current_processing_batch: Option, } -impl fmt::Display for SyncingChain { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.chain_type { - SyncingChainType::Head => write!(f, "Head"), - SyncingChainType::Finalized => write!(f, "Finalized"), - SyncingChainType::Backfill => write!(f, "Backfill"), - } - } -} - #[derive(PartialEq, Debug)] pub enum ChainSyncingState { /// The chain is not being synced. @@ -144,9 +130,6 @@ impl SyncingChain { peer_id: PeerId, chain_type: SyncingChainType, ) -> Self { - let mut peers = FnvHashMap::default(); - peers.insert(peer_id, Default::default()); - SyncingChain { id, chain_type, @@ -154,7 +137,7 @@ impl SyncingChain { target_head_slot, target_head_root, batches: BTreeMap::new(), - peers, + peers: HashSet::from_iter([peer_id]), to_be_downloaded: start_epoch, processing_target: start_epoch, optimistic_start: None, @@ -177,14 +160,14 @@ impl SyncingChain { /// Get the chain's id. #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] - pub fn get_id(&self) -> ChainId { + pub fn id(&self) -> ChainId { self.id } /// Peers currently syncing this chain. #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn peers(&self) -> impl Iterator + '_ { - self.peers.keys().cloned() + self.peers.iter().cloned() } /// Progress in epochs made by the chain @@ -207,29 +190,8 @@ impl SyncingChain { /// Removes a peer from the chain. /// If the peer has active batches, those are considered failed and re-requested. #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] - pub fn remove_peer( - &mut self, - peer_id: &PeerId, - network: &mut SyncNetworkContext, - ) -> ProcessingResult { - if let Some(batch_ids) = self.peers.remove(peer_id) { - // fail the batches. - for id in batch_ids { - if let Some(batch) = self.batches.get_mut(&id) { - if let BatchOperationOutcome::Failed { blacklist } = - batch.download_failed(true)? - { - return Err(RemoveChain::ChainFailed { - blacklist, - failing_batch: id, - }); - } - self.retry_batch_download(network, id)?; - } else { - debug!(%peer_id, batch = ?id, "Batch not found while removing peer") - } - } - } + pub fn remove_peer(&mut self, peer_id: &PeerId) -> ProcessingResult { + self.peers.remove(peer_id); if self.peers.is_empty() { Err(RemoveChain::EmptyPeerPool) @@ -281,11 +243,9 @@ impl SyncingChain { // A stream termination has been sent. This batch has ended. Process a completed batch. // Remove the request from the peer's active batches - self.peers - .get_mut(peer_id) - .map(|active_requests| active_requests.remove(&batch_id)); - let received = batch.download_completed(blocks)?; + // TODO(das): should use peer group here https://github.com/sigp/lighthouse/issues/6258 + let received = batch.download_completed(blocks, *peer_id)?; let awaiting_batches = batch_id .saturating_sub(self.optimistic_start.unwrap_or(self.processing_target)) / EPOCHS_PER_BATCH; @@ -487,7 +447,7 @@ impl SyncingChain { } }; - let peer = batch.current_peer().cloned().ok_or_else(|| { + let peer = batch.processing_peer().cloned().ok_or_else(|| { RemoveChain::WrongBatchState(format!( "Processing target is in wrong state: {:?}", batch.state(), @@ -593,7 +553,7 @@ impl SyncingChain { "Batch failed to download. Dropping chain scoring peers" ); - for (peer, _) in self.peers.drain() { + for peer in self.peers.drain() { network.report_peer(peer, *penalty, "faulty_chain"); } Err(RemoveChain::ChainFailed { @@ -606,7 +566,7 @@ impl SyncingChain { BatchProcessResult::NonFaultyFailure => { batch.processing_completed(BatchProcessingResult::NonFaultyFailure)?; // Simply redownload the batch. - self.retry_batch_download(network, batch_id) + self.send_batch(network, batch_id) } } } @@ -627,7 +587,7 @@ impl SyncingChain { debug!(%epoch, reason, "Rejected optimistic batch left for future use"); // this batch is now treated as any other batch, and re-requested for future use if redownload { - return self.retry_batch_download(network, epoch); + return self.send_batch(network, epoch); } } else { debug!(%epoch, reason, "Rejected optimistic batch"); @@ -707,12 +667,7 @@ impl SyncingChain { } } } - BatchState::Downloading(peer, ..) => { - // remove this batch from the peer's active requests - if let Some(active_batches) = self.peers.get_mut(peer) { - active_batches.remove(&id); - } - } + BatchState::Downloading(..) => {} BatchState::Failed | BatchState::Poisoned | BatchState::AwaitingDownload => { crit!("batch indicates inconsistent chain state while advancing chain") } @@ -801,10 +756,10 @@ impl SyncingChain { self.processing_target = self.start_epoch; for id in redownload_queue { - self.retry_batch_download(network, id)?; + self.send_batch(network, id)?; } // finally, re-request the failed batch. - self.retry_batch_download(network, batch_id) + self.send_batch(network, batch_id) } pub fn stop_syncing(&mut self) { @@ -860,13 +815,8 @@ impl SyncingChain { network: &mut SyncNetworkContext, peer_id: PeerId, ) -> ProcessingResult { - // add the peer without overwriting its active requests - if self.peers.entry(peer_id).or_default().is_empty() { - // Either new or not, this peer is idle, try to request more batches - self.request_batches(network) - } else { - Ok(KeepChain) - } + self.peers.insert(peer_id); + self.request_batches(network) } /// An RPC error has occurred. @@ -907,16 +857,15 @@ impl SyncingChain { %request_id, "Batch download error" ); - if let Some(active_requests) = self.peers.get_mut(peer_id) { - active_requests.remove(&batch_id); - } - if let BatchOperationOutcome::Failed { blacklist } = batch.download_failed(true)? { + if let BatchOperationOutcome::Failed { blacklist } = + batch.download_failed(Some(*peer_id))? + { return Err(RemoveChain::ChainFailed { blacklist, failing_batch: batch_id, }); } - self.retry_batch_download(network, batch_id) + self.send_batch(network, batch_id) } else { debug!( batch_epoch = %batch_id, @@ -930,66 +879,42 @@ impl SyncingChain { } } - /// Sends and registers the request of a batch awaiting download. - #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] - pub fn retry_batch_download( - &mut self, - network: &mut SyncNetworkContext, - batch_id: BatchId, - ) -> ProcessingResult { - let Some(batch) = self.batches.get_mut(&batch_id) else { - return Ok(KeepChain); - }; - - // Find a peer to request the batch - let failed_peers = batch.failed_peers(); - - let new_peer = self - .peers - .iter() - .map(|(peer, requests)| { - ( - failed_peers.contains(peer), - requests.len(), - rand::thread_rng().gen::(), - *peer, - ) - }) - // Sort peers prioritizing unrelated peers with less active requests. - .min() - .map(|(_, _, _, peer)| peer); - - if let Some(peer) = new_peer { - self.send_batch(network, batch_id, peer) - } else { - // If we are here the chain has no more peers - Err(RemoveChain::EmptyPeerPool) - } - } - /// Requests the batch assigned to the given id from a given peer. #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn send_batch( &mut self, network: &mut SyncNetworkContext, batch_id: BatchId, - peer: PeerId, ) -> ProcessingResult { let batch_state = self.visualize_batch_state(); if let Some(batch) = self.batches.get_mut(&batch_id) { let (request, batch_type) = batch.to_blocks_by_range_request(); + let failed_peers = batch.failed_peers(); + + // TODO(das): we should request only from peers that are part of this SyncingChain. + // However, then we hit the NoPeer error frequently which causes the batch to fail and + // the SyncingChain to be dropped. We need to handle this case more gracefully. + let synced_peers = network + .network_globals() + .peers + .read() + .synced_peers() + .cloned() + .collect::>(); + match network.block_components_by_range_request( - peer, batch_type, request, RangeRequestId::RangeSync { chain_id: self.id, batch_id, }, + &synced_peers, + &failed_peers, ) { Ok(request_id) => { // inform the batch about the new request - batch.start_downloading_from_peer(peer, request_id)?; + batch.start_downloading(request_id)?; if self .optimistic_start .map(|epoch| epoch == batch_id) @@ -999,41 +924,34 @@ impl SyncingChain { } else { debug!(epoch = %batch_id, %batch, %batch_state, "Requesting batch"); } - // register the batch for this peer - return self - .peers - .get_mut(&peer) - .map(|requests| { - requests.insert(batch_id); - Ok(KeepChain) - }) - .unwrap_or_else(|| { - Err(RemoveChain::WrongChainState(format!( - "Sending batch to a peer that is not in the chain: {}", - peer - ))) - }); + return Ok(KeepChain); } - Err(e) => { - // NOTE: under normal conditions this shouldn't happen but we handle it anyway - warn!(%batch_id, error = %e, %batch, "Could not send batch request"); - // register the failed download and check if the batch can be retried - batch.start_downloading_from_peer(peer, 1)?; // fake request_id is not relevant - self.peers - .get_mut(&peer) - .map(|request| request.remove(&batch_id)); - match batch.download_failed(true)? { - BatchOperationOutcome::Failed { blacklist } => { - return Err(RemoveChain::ChainFailed { - blacklist, - failing_batch: batch_id, - }) - } - BatchOperationOutcome::Continue => { - return self.retry_batch_download(network, batch_id) + Err(e) => match e { + // TODO(das): Handle the NoPeer case explicitly and don't drop the batch. For + // sync to work properly it must be okay to have "stalled" batches in + // AwaitingDownload state. Currently it will error with invalid state if + // that happens. Sync manager must periodicatlly prune stalled batches like + // we do for lookup sync. Then we can deprecate the redundant + // `good_peers_on_sampling_subnets` checks. + e + @ (RpcRequestSendError::NoPeer(_) | RpcRequestSendError::InternalError(_)) => { + // NOTE: under normal conditions this shouldn't happen but we handle it anyway + warn!(%batch_id, error = ?e, "batch_id" = %batch_id, %batch, "Could not send batch request"); + // register the failed download and check if the batch can be retried + batch.start_downloading(1)?; // fake request_id = 1 is not relevant + match batch.download_failed(None)? { + BatchOperationOutcome::Failed { blacklist } => { + return Err(RemoveChain::ChainFailed { + blacklist, + failing_batch: batch_id, + }) + } + BatchOperationOutcome::Continue => { + return self.send_batch(network, batch_id) + } } } - } + }, } } @@ -1072,21 +990,6 @@ impl SyncingChain { // find the next pending batch and request it from the peer - // randomize the peers for load balancing - let mut rng = rand::thread_rng(); - let mut idle_peers = self - .peers - .iter() - .filter_map(|(peer, requests)| { - if requests.is_empty() { - Some(*peer) - } else { - None - } - }) - .collect::>(); - idle_peers.shuffle(&mut rng); - // check if we have the batch for our optimistic start. If not, request it first. // We wait for this batch before requesting any other batches. if let Some(epoch) = self.optimistic_start { @@ -1096,26 +999,25 @@ impl SyncingChain { } if let Entry::Vacant(entry) = self.batches.entry(epoch) { - if let Some(peer) = idle_peers.pop() { - let batch_type = network.batch_type(epoch); - let optimistic_batch = BatchInfo::new(&epoch, EPOCHS_PER_BATCH, batch_type); - entry.insert(optimistic_batch); - self.send_batch(network, epoch, peer)?; - } + let batch_type = network.batch_type(epoch); + let optimistic_batch = BatchInfo::new(&epoch, EPOCHS_PER_BATCH, batch_type); + entry.insert(optimistic_batch); + self.send_batch(network, epoch)?; } return Ok(KeepChain); } - while let Some(peer) = idle_peers.pop() { - if let Some(batch_id) = self.include_next_batch(network) { - // send the batch - self.send_batch(network, batch_id, peer)?; - } else { - // No more batches, simply stop - return Ok(KeepChain); - } + // find the next pending batch and request it from the peer + // Note: for this function to not infinite loop we must: + // - If `include_next_batch` returns Some we MUST increase the count of batches that are + // accounted in the `BACKFILL_BATCH_BUFFER_SIZE` limit in the `matches!` statement of + // that function. + while let Some(batch_id) = self.include_next_batch(network) { + // send the batch + self.send_batch(network, batch_id)?; } + // No more batches, simply stop Ok(KeepChain) } @@ -1160,6 +1062,7 @@ impl SyncingChain { { return None; } + // only request batches up to the buffer size limit // NOTE: we don't count batches in the AwaitingValidation state, to prevent stalling sync // if the current processing window is contained in a long range of skip slots. @@ -1188,19 +1091,20 @@ impl SyncingChain { return None; } - let batch_id = self.to_be_downloaded; + // If no batch needs a retry, attempt to send the batch of the next epoch to download + let next_batch_id = self.to_be_downloaded; // this batch could have been included already being an optimistic batch - match self.batches.entry(batch_id) { + match self.batches.entry(next_batch_id) { Entry::Occupied(_) => { // this batch doesn't need downloading, let this same function decide the next batch self.to_be_downloaded += EPOCHS_PER_BATCH; self.include_next_batch(network) } Entry::Vacant(entry) => { - let batch_type = network.batch_type(batch_id); - entry.insert(BatchInfo::new(&batch_id, EPOCHS_PER_BATCH, batch_type)); + let batch_type = network.batch_type(next_batch_id); + entry.insert(BatchInfo::new(&next_batch_id, EPOCHS_PER_BATCH, batch_type)); self.to_be_downloaded += EPOCHS_PER_BATCH; - Some(batch_id) + Some(next_batch_id) } } } diff --git a/beacon_node/network/src/sync/range_sync/chain_collection.rs b/beacon_node/network/src/sync/range_sync/chain_collection.rs index c6be3de576..9f500c61e0 100644 --- a/beacon_node/network/src/sync/range_sync/chain_collection.rs +++ b/beacon_node/network/src/sync/range_sync/chain_collection.rs @@ -293,8 +293,8 @@ impl ChainCollection { .expect("Chain exists"); match old_id { - Some(Some(old_id)) => debug!(old_id, %chain, "Switching finalized chains"), - None => debug!(%chain, "Syncing new finalized chain"), + Some(Some(old_id)) => debug!(old_id, id = chain.id(), "Switching finalized chains"), + None => debug!(id = chain.id(), "Syncing new finalized chain"), Some(None) => { // this is the same chain. We try to advance it. } @@ -359,7 +359,7 @@ impl ChainCollection { if syncing_chains.len() < PARALLEL_HEAD_CHAINS { // start this chain if it's not already syncing if !chain.is_syncing() { - debug!(%chain, "New head chain started syncing"); + debug!(id = chain.id(), "New head chain started syncing"); } if let Err(remove_reason) = chain.start_syncing(network, local_epoch, local_head_epoch) @@ -421,7 +421,7 @@ impl ChainCollection { if is_outdated(&chain.target_head_slot, &chain.target_head_root) || chain.available_peers() == 0 { - debug!(%chain, "Purging out of finalized chain"); + debug!(id, "Purging out of finalized chain"); Some((*id, chain.is_syncing(), RangeSyncType::Finalized)) } else { None @@ -432,7 +432,7 @@ impl ChainCollection { if is_outdated(&chain.target_head_slot, &chain.target_head_root) || chain.available_peers() == 0 { - debug!(%chain, "Purging out of date head chain"); + debug!(id, "Purging out of date head chain"); Some((*id, chain.is_syncing(), RangeSyncType::Head)) } else { None @@ -478,9 +478,9 @@ impl ChainCollection { debug_assert_eq!(chain.target_head_slot, target_head_slot); if let Err(remove_reason) = chain.add_peer(network, peer) { if remove_reason.is_critical() { - crit!(chain = %id, reason = ?remove_reason, "Chain removed after adding peer"); + crit!(id, reason = ?remove_reason, "Chain removed after adding peer"); } else { - error!(chain = %id, reason = ?remove_reason, "Chain removed after adding peer"); + error!(id, reason = ?remove_reason, "Chain removed after adding peer"); } let is_syncing = chain.is_syncing(); collection.remove(&id); @@ -499,7 +499,15 @@ impl ChainCollection { sync_type.into(), ); - debug!(peer_id = peer_rpr, ?sync_type, %new_chain, "New chain added to sync"); + debug!( + peer_id = peer_rpr, + ?sync_type, + id, + %start_epoch, + %target_head_slot, + ?target_head_root, + "New chain added to sync" + ); collection.insert(id, new_chain); metrics::inc_counter_vec(&metrics::SYNCING_CHAINS_ADDED, &[sync_type.as_str()]); self.update_metrics(); diff --git a/beacon_node/network/src/sync/range_sync/range.rs b/beacon_node/network/src/sync/range_sync/range.rs index ab9a88e4ac..1ec1440991 100644 --- a/beacon_node/network/src/sync/range_sync/range.rs +++ b/beacon_node/network/src/sync/range_sync/range.rs @@ -317,9 +317,8 @@ where skip_all )] fn remove_peer(&mut self, network: &mut SyncNetworkContext, peer_id: &PeerId) { - for (removed_chain, sync_type, remove_reason) in self - .chains - .call_all(|chain| chain.remove_peer(peer_id, network)) + for (removed_chain, sync_type, remove_reason) in + self.chains.call_all(|chain| chain.remove_peer(peer_id)) { self.on_chain_removed( removed_chain, @@ -386,15 +385,15 @@ where op: &'static str, ) { if remove_reason.is_critical() { - crit!(?sync_type, %chain, reason = ?remove_reason,op, "Chain removed"); + crit!(id = chain.id(), ?sync_type, reason = ?remove_reason, op, "Chain removed"); } else { - debug!(?sync_type, %chain, reason = ?remove_reason,op, "Chain removed"); + debug!(id = chain.id(), ?sync_type, reason = ?remove_reason, op, "Chain removed"); } if let RemoveChain::ChainFailed { blacklist, .. } = remove_reason { if RangeSyncType::Finalized == sync_type && blacklist { warn!( - %chain, + id = chain.id(), "Chain failed! Syncing to its head won't be retried for at least the next {} seconds", FAILED_CHAINS_EXPIRY_SECONDS ); diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index 84c95b2a4c..5863091cf0 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -357,10 +357,13 @@ impl TestRig { pub fn new_connected_peer(&mut self) -> PeerId { let key = self.determinstic_key(); - self.network_globals + let peer_id = self + .network_globals .peers .write() - .__add_connected_peer_testing_only(false, &self.harness.spec, key) + .__add_connected_peer_testing_only(false, &self.harness.spec, key); + self.log(&format!("Added new peer for testing {peer_id:?}")); + peer_id } pub fn new_connected_supernode_peer(&mut self) -> PeerId { @@ -976,18 +979,13 @@ impl TestRig { request: RequestType::DataColumnsByRoot(request), app_request_id: AppRequestId::Sync(id @ SyncRequestId::DataColumnsByRoot { .. }), - } if request - .data_column_ids - .to_vec() - .iter() - .any(|r| r.block_root == block_root) => - { - let indices = request + } => { + let matching = request .data_column_ids - .to_vec() .iter() - .map(|cid| cid.index) - .collect::>(); + .find(|id| id.block_root == block_root)?; + + let indices = matching.columns.iter().copied().collect(); Some((*id, indices)) } _ => None, diff --git a/beacon_node/network/src/sync/tests/range.rs b/beacon_node/network/src/sync/tests/range.rs index 2871ea2a4d..932f485dd0 100644 --- a/beacon_node/network/src/sync/tests/range.rs +++ b/beacon_node/network/src/sync/tests/range.rs @@ -459,7 +459,8 @@ fn build_rpc_block( ) .unwrap() } - None => RpcBlock::new_without_blobs(None, block), + // Block has no data, expects zero columns + None => RpcBlock::new_without_blobs(None, block, 0), } } diff --git a/beacon_node/operation_pool/src/attestation.rs b/beacon_node/operation_pool/src/attestation.rs index 97d0583e34..78280278e0 100644 --- a/beacon_node/operation_pool/src/attestation.rs +++ b/beacon_node/operation_pool/src/attestation.rs @@ -7,15 +7,18 @@ use state_processing::common::{ use std::collections::HashMap; use types::{ beacon_state::BeaconStateBase, - consts::altair::{PARTICIPATION_FLAG_WEIGHTS, WEIGHT_DENOMINATOR}, + consts::altair::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}, Attestation, BeaconState, BitList, ChainSpec, EthSpec, }; +pub const PROPOSER_REWARD_DENOMINATOR: u64 = + (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT) * WEIGHT_DENOMINATOR / PROPOSER_WEIGHT; + #[derive(Debug, Clone)] pub struct AttMaxCover<'a, E: EthSpec> { /// Underlying attestation. pub att: CompactAttestationRef<'a, E>, - /// Mapping of validator indices and their rewards. + /// Mapping of validator indices and their reward *numerators*. pub fresh_validators_rewards: HashMap, } @@ -30,7 +33,7 @@ impl<'a, E: EthSpec> AttMaxCover<'a, E> { 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, spec) + Self::new_for_altair_or_later(att, state, reward_cache, spec) } } @@ -68,7 +71,7 @@ impl<'a, E: EthSpec> AttMaxCover<'a, E> { } /// Initialise an attestation cover object for Altair or later. - pub fn new_for_altair_deneb( + pub fn new_for_altair_or_later( att: CompactAttestationRef<'a, E>, state: &BeaconState, reward_cache: &'a RewardCache, @@ -103,10 +106,7 @@ impl<'a, E: EthSpec> AttMaxCover<'a, E> { } } - let proposer_reward = proposer_reward_numerator - .checked_div(WEIGHT_DENOMINATOR.checked_mul(spec.proposer_reward_quotient)?)?; - - Some((index, proposer_reward)).filter(|_| proposer_reward != 0) + Some((index, proposer_reward_numerator)).filter(|_| proposer_reward_numerator != 0) }) .collect(); @@ -163,7 +163,7 @@ impl<'a, E: EthSpec> MaxCover for AttMaxCover<'a, E> { } fn score(&self) -> usize { - self.fresh_validators_rewards.values().sum::() as usize + (self.fresh_validators_rewards.values().sum::() / PROPOSER_REWARD_DENOMINATOR) as usize } } diff --git a/beacon_node/operation_pool/src/attestation_storage.rs b/beacon_node/operation_pool/src/attestation_storage.rs index 49ef5c279c..67c24b9c7a 100644 --- a/beacon_node/operation_pool/src/attestation_storage.rs +++ b/beacon_node/operation_pool/src/attestation_storage.rs @@ -1,6 +1,6 @@ use crate::AttestationStats; use itertools::Itertools; -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, HashMap, HashSet}; use types::{ attestation::{AttestationBase, AttestationElectra}, superstruct, AggregateSignature, Attestation, AttestationData, BeaconState, BitList, BitVector, @@ -119,6 +119,18 @@ impl CompactAttestationRef<'_, E> { } } + pub fn get_committee_indices_map(&self) -> HashSet { + match self.indexed { + CompactIndexedAttestation::Base(_) => HashSet::from([self.data.index]), + CompactIndexedAttestation::Electra(indexed_att) => indexed_att + .committee_bits + .iter() + .enumerate() + .filter_map(|(index, bit)| if bit { Some(index as u64) } else { None }) + .collect(), + } + } + pub fn clone_as_attestation(&self) -> Attestation { match self.indexed { CompactIndexedAttestation::Base(indexed_att) => Attestation::Base(AttestationBase { @@ -268,7 +280,11 @@ impl CompactIndexedAttestationElectra { } pub fn committee_index(&self) -> Option { - self.get_committee_indices().first().copied() + self.committee_bits + .iter() + .enumerate() + .find(|&(_, bit)| bit) + .map(|(index, _)| index as u64) } pub fn get_committee_indices(&self) -> Vec { diff --git a/beacon_node/operation_pool/src/lib.rs b/beacon_node/operation_pool/src/lib.rs index 584a5f9f32..7481aa896a 100644 --- a/beacon_node/operation_pool/src/lib.rs +++ b/beacon_node/operation_pool/src/lib.rs @@ -1,5 +1,5 @@ mod attestation; -mod attestation_storage; +pub mod attestation_storage; mod attester_slashing; mod bls_to_execution_changes; mod max_cover; @@ -9,7 +9,7 @@ mod reward_cache; mod sync_aggregate_id; pub use crate::bls_to_execution_changes::ReceivedPreCapella; -pub use attestation::{earliest_attestation_validators, AttMaxCover}; +pub use attestation::{earliest_attestation_validators, AttMaxCover, PROPOSER_REWARD_DENOMINATOR}; pub use attestation_storage::{CompactAttestationRef, SplitAttestation}; pub use max_cover::MaxCover; pub use persistence::{ @@ -47,7 +47,7 @@ type SyncContributions = RwLock { /// Map from attestation ID (see below) to vectors of attestations. - attestations: RwLock>, + pub attestations: RwLock>, /// Map from sync aggregate ID to the best `SyncCommitteeContribution`s seen for that ID. sync_contributions: SyncContributions, /// Set of attester slashings, and the fork version they were verified against. @@ -673,12 +673,12 @@ impl OperationPool { /// This method may return objects that are invalid for block inclusion. pub fn get_filtered_attestations(&self, filter: F) -> Vec> where - F: Fn(&AttestationData) -> bool, + F: Fn(&AttestationData, HashSet) -> bool, { self.attestations .read() .iter() - .filter(|att| filter(&att.attestation_data())) + .filter(|att| filter(&att.attestation_data(), att.get_committee_indices_map())) .map(|att| att.clone_as_attestation()) .collect() } @@ -1402,7 +1402,8 @@ mod release_tests { .retain(|validator_index, _| !seen_indices.contains(validator_index)); // Check that rewards are in decreasing order - let rewards = fresh_validators_rewards.values().sum(); + let rewards = + fresh_validators_rewards.values().sum::() / PROPOSER_REWARD_DENOMINATOR; assert!(prev_reward >= rewards); prev_reward = rewards; seen_indices.extend(fresh_validators_rewards.keys()); diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 8723c2d708..e887aa9abc 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -661,10 +661,7 @@ pub fn get_config( }; } - client_config.chain.max_network_size = lighthouse_network::gossip_max_size( - spec.bellatrix_fork_epoch.is_some(), - spec.gossip_max_size as usize, - ); + client_config.chain.max_network_size = spec.max_payload_size as usize; if cli_args.get_flag("slasher") { let slasher_dir = if let Some(slasher_dir) = cli_args.get_one::("slasher-dir") { diff --git a/beacon_node/store/src/database/leveldb_impl.rs b/beacon_node/store/src/database/leveldb_impl.rs index 3d8bbe1473..81d6d1d4bd 100644 --- a/beacon_node/store/src/database/leveldb_impl.rs +++ b/beacon_node/store/src/database/leveldb_impl.rs @@ -195,7 +195,6 @@ impl LevelDB { }; for (start_key, end_key) in [ - endpoints(DBColumn::BeaconStateTemporary), endpoints(DBColumn::BeaconState), endpoints(DBColumn::BeaconStateSummary), ] { diff --git a/beacon_node/store/src/errors.rs b/beacon_node/store/src/errors.rs index 41fd17ef43..cff08bc655 100644 --- a/beacon_node/store/src/errors.rs +++ b/beacon_node/store/src/errors.rs @@ -25,7 +25,7 @@ pub enum Error { NoContinuationData, SplitPointModified(Slot, Slot), ConfigError(StoreConfigError), - SchemaMigrationError(String), + MigrationError(String), /// The store's `anchor_info` was mutated concurrently, the latest modification wasn't applied. AnchorInfoConcurrentMutation, /// The store's `blob_info` was mutated concurrently, the latest modification wasn't applied. @@ -57,7 +57,7 @@ pub enum Error { #[cfg(feature = "leveldb")] LevelDbError(LevelDBError), #[cfg(feature = "redb")] - RedbError(redb::Error), + RedbError(Box), CacheBuildError(EpochCacheError), RandaoMixOutOfBounds, MilhouseError(milhouse::Error), @@ -161,49 +161,49 @@ impl From for Error { #[cfg(feature = "redb")] impl From for Error { fn from(e: redb::Error) -> Self { - Error::RedbError(e) + Error::RedbError(Box::new(e)) } } #[cfg(feature = "redb")] impl From for Error { fn from(e: redb::TableError) -> Self { - Error::RedbError(e.into()) + Error::RedbError(Box::new(e.into())) } } #[cfg(feature = "redb")] impl From for Error { fn from(e: redb::TransactionError) -> Self { - Error::RedbError(e.into()) + Error::RedbError(Box::new(e.into())) } } #[cfg(feature = "redb")] impl From for Error { fn from(e: redb::DatabaseError) -> Self { - Error::RedbError(e.into()) + Error::RedbError(Box::new(e.into())) } } #[cfg(feature = "redb")] impl From for Error { fn from(e: redb::StorageError) -> Self { - Error::RedbError(e.into()) + Error::RedbError(Box::new(e.into())) } } #[cfg(feature = "redb")] impl From for Error { fn from(e: redb::CommitError) -> Self { - Error::RedbError(e.into()) + Error::RedbError(Box::new(e.into())) } } #[cfg(feature = "redb")] impl From for Error { fn from(e: redb::CompactionError) -> Self { - Error::RedbError(e.into()) + Error::RedbError(Box::new(e.into())) } } diff --git a/beacon_node/store/src/garbage_collection.rs b/beacon_node/store/src/garbage_collection.rs deleted file mode 100644 index 586db44c89..0000000000 --- a/beacon_node/store/src/garbage_collection.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! Garbage collection process that runs at start-up to clean up the database. -use crate::database::interface::BeaconNodeBackend; -use crate::hot_cold_store::HotColdDB; -use crate::{DBColumn, Error}; -use tracing::debug; -use types::EthSpec; - -impl HotColdDB, BeaconNodeBackend> -where - E: EthSpec, -{ - /// Clean up the database by performing one-off maintenance at start-up. - pub fn remove_garbage(&self) -> Result<(), Error> { - self.delete_temp_states()?; - Ok(()) - } - - /// Delete the temporary states that were leftover by failed block imports. - pub fn delete_temp_states(&self) -> Result<(), Error> { - let mut ops = vec![]; - self.iter_temporary_state_roots().for_each(|state_root| { - if let Ok(state_root) = state_root { - ops.push(state_root); - } - }); - if !ops.is_empty() { - debug!("Garbage collecting {} temporary states", ops.len()); - - self.delete_batch(DBColumn::BeaconState, ops.clone())?; - self.delete_batch(DBColumn::BeaconStateSummary, ops.clone())?; - self.delete_batch(DBColumn::BeaconStateTemporary, ops)?; - } - - Ok(()) - } -} diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 6a30d8a428..d4b68357b2 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -14,8 +14,8 @@ use crate::metadata::{ }; use crate::state_cache::{PutStateOutcome, StateCache}; use crate::{ - get_data_column_key, metrics, parse_data_column_key, BlobSidecarListFromRoot, ColumnKeyIter, - DBColumn, DatabaseBlock, Error, ItemStore, KeyValueStore, KeyValueStoreOp, StoreItem, StoreOp, + get_data_column_key, metrics, parse_data_column_key, BlobSidecarListFromRoot, DBColumn, + DatabaseBlock, Error, ItemStore, KeyValueStoreOp, StoreItem, StoreOp, }; use itertools::{process_results, Itertools}; use lru::LruCache; @@ -36,7 +36,7 @@ use std::num::NonZeroUsize; use std::path::Path; use std::sync::Arc; use std::time::Duration; -use tracing::{debug, error, info, trace, warn}; +use tracing::{debug, error, info, warn}; use types::data_column_sidecar::{ColumnIndex, DataColumnSidecar, DataColumnSidecarList}; use types::*; use zstd::{Decoder, Encoder}; @@ -80,7 +80,7 @@ pub struct HotColdDB, Cold: ItemStore> { /// HTTP API. historic_state_cache: Mutex>, /// Chain spec. - pub(crate) spec: Arc, + pub spec: Arc, /// Mere vessel for E. _phantom: PhantomData, } @@ -117,19 +117,16 @@ impl BlockCache { pub fn get_blobs<'a>(&'a mut self, block_root: &Hash256) -> Option<&'a BlobSidecarList> { self.blob_cache.get(block_root) } - pub fn get_data_columns(&mut self, block_root: &Hash256) -> Option> { - self.data_column_cache - .get(block_root) - .map(|map| map.values().cloned().collect::>()) - } - pub fn get_data_column<'a>( - &'a mut self, + // Note: data columns are all individually cached, hence there's no guarantee that + // `data_column_cache.get(block_root)` will return all custody columns. + pub fn get_data_column( + &mut self, block_root: &Hash256, column_index: &ColumnIndex, - ) -> Option<&'a Arc>> { + ) -> Option>> { self.data_column_cache .get(block_root) - .and_then(|map| map.get(column_index)) + .and_then(|map| map.get(column_index).cloned()) } pub fn delete_block(&mut self, block_root: &Hash256) { let _ = self.block_cache.pop(block_root); @@ -161,7 +158,7 @@ pub enum HotColdDBError { MissingRestorePoint(Hash256), MissingColdStateSummary(Hash256), MissingHotStateSummary(Hash256), - MissingEpochBoundaryState(Hash256), + MissingEpochBoundaryState(Hash256, Hash256), MissingPrevState(Hash256), MissingSplitState(Hash256, Slot), MissingStateDiff(Hash256), @@ -390,8 +387,11 @@ impl HotColdDB, BeaconNodeBackend> { } db.store_config()?; - // Run a garbage collection pass. - db.remove_garbage()?; + // TODO(tree-states): Here we can choose to prune advanced states to reclaim disk space. As + // it's a foreground task there's no risk of race condition that can corrupt the DB. + // Advanced states for invalid blocks that were never written to the DB, or descendants of + // heads can be safely pruned at the expense of potentially having to recompute them in the + // future. However this would require a new dedicated pruning routine. // If configured, run a foreground compaction pass. if db.config.compact_on_init { @@ -402,12 +402,6 @@ impl HotColdDB, BeaconNodeBackend> { Ok(db) } - - /// Return an iterator over the state roots of all temporary states. - pub fn iter_temporary_state_roots(&self) -> ColumnKeyIter { - self.hot_db - .iter_column_keys::(DBColumn::BeaconStateTemporary) - } } impl, Cold: ItemStore> HotColdDB { @@ -903,26 +897,11 @@ impl, Cold: ItemStore> HotColdDB /// Store a state in the store. pub fn put_state(&self, state_root: &Hash256, state: &BeaconState) -> Result<(), Error> { - self.put_state_possibly_temporary(state_root, state, false) - } - - /// Store a state in the store. - /// - /// The `temporary` flag indicates whether this state should be considered canonical. - pub fn put_state_possibly_temporary( - &self, - state_root: &Hash256, - state: &BeaconState, - temporary: bool, - ) -> Result<(), Error> { let mut ops: Vec = Vec::new(); if state.slot() < self.get_split_slot() { self.store_cold_state(state_root, state, &mut ops)?; self.cold_db.do_atomically(ops) } else { - if temporary { - ops.push(TemporaryFlag.as_kv_store_op(*state_root)); - } self.store_hot_state(state_root, state, &mut ops)?; self.hot_db.do_atomically(ops) } @@ -1138,6 +1117,7 @@ impl, Cold: ItemStore> HotColdDB .load_hot_state(&epoch_boundary_state_root, true)? .ok_or(HotColdDBError::MissingEpochBoundaryState( epoch_boundary_state_root, + *state_root, ))?; Ok(Some(state)) } else { @@ -1201,17 +1181,6 @@ impl, Cold: ItemStore> HotColdDB key_value_batch.push(summary.as_kv_store_op(state_root)); } - StoreOp::PutStateTemporaryFlag(state_root) => { - key_value_batch.push(TemporaryFlag.as_kv_store_op(state_root)); - } - - StoreOp::DeleteStateTemporaryFlag(state_root) => { - key_value_batch.push(KeyValueStoreOp::DeleteKey( - TemporaryFlag::db_column(), - state_root.as_slice().to_vec(), - )); - } - StoreOp::DeleteBlock(block_root) => { key_value_batch.push(KeyValueStoreOp::DeleteKey( DBColumn::BeaconBlock, @@ -1241,13 +1210,6 @@ impl, Cold: ItemStore> HotColdDB state_root.as_slice().to_vec(), )); - // Delete the state temporary flag (if any). Temporary flags are commonly - // created by the state advance routine. - key_value_batch.push(KeyValueStoreOp::DeleteKey( - DBColumn::BeaconStateTemporary, - state_root.as_slice().to_vec(), - )); - if slot.is_none_or(|slot| slot % E::slots_per_epoch() == 0) { key_value_batch.push(KeyValueStoreOp::DeleteKey( DBColumn::BeaconState, @@ -1408,10 +1370,6 @@ impl, Cold: ItemStore> HotColdDB StoreOp::PutStateSummary(_, _) => (), - StoreOp::PutStateTemporaryFlag(_) => (), - - StoreOp::DeleteStateTemporaryFlag(_) => (), - StoreOp::DeleteBlock(block_root) => { guard.delete_block(&block_root); self.state_cache.lock().delete_block_states(&block_root); @@ -1492,8 +1450,8 @@ impl, Cold: ItemStore> HotColdDB // On the epoch boundary, store the full state. if state.slot() % E::slots_per_epoch() == 0 { - trace!( - slot = %state.slot().as_u64(), + debug!( + slot = %state.slot(), ?state_root, "Storing full state on epoch boundary" ); @@ -1571,12 +1529,6 @@ impl, Cold: ItemStore> HotColdDB ) -> Result, Hash256)>, Error> { metrics::inc_counter(&metrics::BEACON_STATE_HOT_GET_COUNT); - // If the state is marked as temporary, do not return it. It will become visible - // only once its transaction commits and deletes its temporary flag. - if self.load_state_temporary_flag(state_root)?.is_some() { - return Ok(None); - } - if let Some(HotStateSummary { slot, latest_block_root, @@ -1585,7 +1537,10 @@ impl, Cold: ItemStore> HotColdDB { let mut boundary_state = get_full_state(&self.hot_db, &epoch_boundary_state_root, &self.spec)?.ok_or( - HotColdDBError::MissingEpochBoundaryState(epoch_boundary_state_root), + HotColdDBError::MissingEpochBoundaryState( + epoch_boundary_state_root, + *state_root, + ), )?; // Immediately rebase the state from disk on the finalized state so that we can reuse @@ -2073,38 +2028,19 @@ impl, Cold: ItemStore> HotColdDB }) } - /// Fetch columns for a given block from the store. + /// Fetch all columns for a given block from the store. pub fn get_data_columns( &self, block_root: &Hash256, ) -> Result>, Error> { - if let Some(columns) = self.block_cache.lock().get_data_columns(block_root) { - metrics::inc_counter(&metrics::BEACON_DATA_COLUMNS_CACHE_HIT_COUNT); - return Ok(Some(columns)); - } + let column_indices = self.get_data_column_keys(*block_root)?; - let columns = self - .blobs_db - .iter_column_from::>(DBColumn::BeaconDataColumn, block_root.as_slice()) - .take_while(|res| { - res.as_ref() - .is_ok_and(|(key, _)| key.starts_with(block_root.as_slice())) - }) - .map(|result| { - let (_key, value) = result?; - let column = DataColumnSidecar::::from_ssz_bytes(&value).map(Arc::new)?; - self.block_cache - .lock() - .put_data_column(*block_root, column.clone()); - Ok(column) - }) - .collect::, Error>>()?; + let columns: DataColumnSidecarList = column_indices + .into_iter() + .filter_map(|col_index| self.get_data_column(block_root, &col_index).transpose()) + .collect::>()?; - if columns.is_empty() { - Ok(None) - } else { - Ok(Some(columns)) - } + Ok((!columns.is_empty()).then_some(columns)) } /// Fetch blobs for a given block from the store. @@ -2169,7 +2105,7 @@ impl, Cold: ItemStore> HotColdDB .get_data_column(block_root, column_index) { metrics::inc_counter(&metrics::BEACON_DATA_COLUMNS_CACHE_HIT_COUNT); - return Ok(Some(data_column.clone())); + return Ok(Some(data_column)); } match self.blobs_db.get_bytes( @@ -2545,15 +2481,16 @@ impl, Cold: ItemStore> HotColdDB self.hot_db.get(state_root) } - /// Load the temporary flag for a state root, if one exists. - /// - /// Returns `Some` if the state is temporary, or `None` if the state is permanent or does not - /// exist -- you should call `load_hot_state_summary` to find out which. - pub fn load_state_temporary_flag( - &self, - state_root: &Hash256, - ) -> Result, Error> { - self.hot_db.get(state_root) + /// Load all hot state summaries present in the hot DB + pub fn load_hot_state_summaries(&self) -> Result, Error> { + self.hot_db + .iter_column::(DBColumn::BeaconStateSummary) + .map(|res| { + let (state_root, value) = res?; + let summary = HotStateSummary::from_ssz_bytes(&value)?; + Ok((state_root, summary)) + }) + .collect() } /// Run a compaction pass to free up space used by deleted states. @@ -2985,54 +2922,13 @@ impl, Cold: ItemStore> HotColdDB Ok(()) } - - /// Prune states from the hot database which are prior to the split. - /// - /// This routine is important for cleaning up advanced states which are stored in the database - /// with a temporary flag. - pub fn prune_old_hot_states(&self) -> Result<(), Error> { - let split = self.get_split_info(); - debug!( - %split.slot, - "Database state pruning started" - ); - let mut state_delete_batch = vec![]; - for res in self - .hot_db - .iter_column::(DBColumn::BeaconStateSummary) - { - let (state_root, summary_bytes) = res?; - let summary = HotStateSummary::from_ssz_bytes(&summary_bytes)?; - - if summary.slot <= split.slot { - let old = summary.slot < split.slot; - let non_canonical = summary.slot == split.slot - && state_root != split.state_root - && !split.state_root.is_zero(); - if old || non_canonical { - let reason = if old { - "old dangling state" - } else { - "non-canonical" - }; - debug!( - ?state_root, - slot = %summary.slot, - %reason, - "Deleting state" - ); - state_delete_batch.push(StoreOp::DeleteState(state_root, Some(summary.slot))); - } - } - } - let num_deleted_states = state_delete_batch.len(); - self.do_atomically_with_block_and_blobs_cache(state_delete_batch)?; - debug!(%num_deleted_states, "Database state pruning complete"); - Ok(()) - } } -/// Advance the split point of the store, moving new finalized states to the freezer. +/// Advance the split point of the store, copying new finalized states to the freezer. +/// +/// This function previously did a combination of freezer migration alongside pruning. Now it is +/// *just* responsible for copying relevant data to the freezer, while pruning is implemented +/// in `prune_hot_db`. pub fn migrate_database, Cold: ItemStore>( store: Arc>, finalized_state_root: Hash256, @@ -3064,29 +2960,17 @@ pub fn migrate_database, Cold: ItemStore>( return Err(HotColdDBError::FreezeSlotUnaligned(finalized_state.slot()).into()); } - let mut hot_db_ops = vec![]; let mut cold_db_block_ops = vec![]; - let mut epoch_boundary_blocks = HashSet::new(); - let mut non_checkpoint_block_roots = HashSet::new(); // Iterate in descending order until the current split slot - let state_roots = RootsIterator::new(&store, finalized_state) - .take_while(|result| match result { - Ok((_, _, slot)) => *slot >= current_split_slot, - Err(_) => true, - }) - .collect::, _>>()?; + let state_roots: Vec<_> = + process_results(RootsIterator::new(&store, finalized_state), |iter| { + iter.take_while(|(_, _, slot)| *slot >= current_split_slot) + .collect() + })?; // Then, iterate states in slot ascending order, as they are stored wrt previous states. for (block_root, state_root, slot) in state_roots.into_iter().rev() { - // Delete the execution payload if payload pruning is enabled. At a skipped slot we may - // delete the payload for the finalized block itself, but that's OK as we only guarantee - // that payloads are present for slots >= the split slot. The payload fetching code is also - // forgiving of missing payloads. - if store.config.prune_payloads { - hot_db_ops.push(StoreOp::DeleteExecutionPayload(block_root)); - } - // Store the slot to block root mapping. cold_db_block_ops.push(KeyValueStoreOp::PutKeyValue( DBColumn::BeaconBlockRoots, @@ -3094,44 +2978,27 @@ pub fn migrate_database, Cold: ItemStore>( block_root.as_slice().to_vec(), )); - // At a missed slot, `state_root_iter` will return the block root - // from the previous non-missed slot. This ensures that the block root at an - // epoch boundary is always a checkpoint block root. We keep track of block roots - // at epoch boundaries by storing them in the `epoch_boundary_blocks` hash set. - // We then ensure that block roots at the epoch boundary aren't included in the - // `non_checkpoint_block_roots` hash set. - if slot % E::slots_per_epoch() == 0 { - epoch_boundary_blocks.insert(block_root); - } else { - non_checkpoint_block_roots.insert(block_root); - } - - if epoch_boundary_blocks.contains(&block_root) { - non_checkpoint_block_roots.remove(&block_root); - } - - // Delete the old summary, and the full state if we lie on an epoch boundary. - hot_db_ops.push(StoreOp::DeleteState(state_root, Some(slot))); - // Do not try to store states if a restore point is yet to be stored, or will never be // stored (see `STATE_UPPER_LIMIT_NO_RETAIN`). Make an exception for the genesis state // which always needs to be copied from the hot DB to the freezer and should not be deleted. if slot != 0 && slot < anchor_info.state_upper_limit { - debug!(%slot, "Pruning finalized state"); continue; } - let mut cold_db_ops = vec![]; + let mut cold_db_state_ops = vec![]; // Only store the cold state if it's on a diff boundary. // Calling `store_cold_state_summary` instead of `store_cold_state` for those allows us // to skip loading many hot states. - if matches!( - store.hierarchy.storage_strategy(slot)?, - StorageStrategy::ReplayFrom(..) - ) { + if let StorageStrategy::ReplayFrom(from) = store.hierarchy.storage_strategy(slot)? { // Store slot -> state_root and state_root -> slot mappings. - store.store_cold_state_summary(&state_root, slot, &mut cold_db_ops)?; + debug!( + strategy = "replay", + from_slot = %from, + %slot, + "Storing cold state" + ); + store.store_cold_state_summary(&state_root, slot, &mut cold_db_state_ops)?; } else { // This is some state that we want to migrate to the freezer db. // There is no reason to cache this state. @@ -3139,36 +3006,22 @@ pub fn migrate_database, Cold: ItemStore>( .get_hot_state(&state_root, false)? .ok_or(HotColdDBError::MissingStateToFreeze(state_root))?; - store.store_cold_state(&state_root, &state, &mut cold_db_ops)?; + store.store_cold_state(&state_root, &state, &mut cold_db_state_ops)?; } // Cold states are diffed with respect to each other, so we need to finish writing previous // states before storing new ones. - store.cold_db.do_atomically(cold_db_ops)?; + store.cold_db.do_atomically(cold_db_state_ops)?; } - // Prune sync committee branch data for all non checkpoint block roots. - // Note that `non_checkpoint_block_roots` should only contain non checkpoint block roots - // as long as `finalized_state.slot()` is at an epoch boundary. If this were not the case - // we risk the chance of pruning a `sync_committee_branch` for a checkpoint block root. - // E.g. if `current_split_slot` = (Epoch A slot 0) and `finalized_state.slot()` = (Epoch C slot 31) - // and (Epoch D slot 0) is a skipped slot, we will have pruned a `sync_committee_branch` - // for a checkpoint block root. - non_checkpoint_block_roots - .into_iter() - .for_each(|block_root| { - hot_db_ops.push(StoreOp::DeleteSyncCommitteeBranch(block_root)); - }); - - // Warning: Critical section. We have to take care not to put any of the two databases in an + // Warning: Critical section. We have to take care not to put any of the two databases in an // inconsistent state if the OS process dies at any point during the freezing // procedure. // // Since it is pretty much impossible to be atomic across more than one database, we trade - // losing track of states to delete, for consistency. In other words: We should be safe to die - // at any point below but it may happen that some states won't be deleted from the hot database - // and will remain there forever. Since dying in these particular few lines should be an - // exceedingly rare event, this should be an acceptable tradeoff. + // potentially re-doing the migration to copy data to the freezer, for consistency. If we crash + // after writing all new block & state data to the freezer but before updating the split, then + // in the worst case we will restart with the old split and re-run the migration. store.cold_db.do_atomically(cold_db_block_ops)?; store.cold_db.sync()?; { @@ -3181,7 +3034,7 @@ pub fn migrate_database, Cold: ItemStore>( error!( previous_split_slot = %current_split_slot, current_split_slot = %latest_split_slot, - "Race condition detected: Split point changed while moving states to the freezer" + "Race condition detected: Split point changed while copying states to the freezer" ); // Assume the freezing procedure will be retried in case this happens. @@ -3206,9 +3059,6 @@ pub fn migrate_database, Cold: ItemStore>( *split_guard = split; } - // Delete the blocks and states from the hot database if we got this far. - store.do_atomically_with_block_and_blobs_cache(hot_db_ops)?; - // Update the cache's view of the finalized state. store.update_finalized_state( finalized_state_root, @@ -3325,23 +3175,6 @@ impl StoreItem for ColdStateSummary { } } -#[derive(Debug, Clone, Copy, Default)] -pub struct TemporaryFlag; - -impl StoreItem for TemporaryFlag { - fn db_column() -> DBColumn { - DBColumn::BeaconStateTemporary - } - - fn as_store_bytes(&self) -> Vec { - vec![] - } - - fn from_store_bytes(_: &[u8]) -> Result { - Ok(TemporaryFlag) - } -} - #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct BytesKey { pub key: Vec, diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index 2b5be03489..5b30971fd8 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -14,7 +14,6 @@ pub mod config; pub mod consensus_context; pub mod errors; mod forwards_iter; -mod garbage_collection; pub mod hdiff; pub mod historic_state_cache; pub mod hot_cold_store; @@ -241,8 +240,6 @@ pub enum StoreOp<'a, E: EthSpec> { PutBlobs(Hash256, BlobSidecarList), PutDataColumns(Hash256, DataColumnSidecarList), PutStateSummary(Hash256, HotStateSummary), - PutStateTemporaryFlag(Hash256), - DeleteStateTemporaryFlag(Hash256), DeleteBlock(Hash256), DeleteBlobs(Hash256), DeleteDataColumns(Hash256, Vec), @@ -287,8 +284,10 @@ pub enum DBColumn { /// Mapping from state root to `ColdStateSummary` in the cold DB. #[strum(serialize = "bcs")] BeaconColdStateSummary, - /// For the list of temporary states stored during block import, - /// and then made non-temporary by the deletion of their state root from this column. + /// DEPRECATED. + /// + /// Previously used for the list of temporary states stored during block import, and then made + /// non-temporary by the deletion of their state root from this column. #[strum(serialize = "bst")] BeaconStateTemporary, /// Execution payloads for blocks more recent than the finalized checkpoint. diff --git a/beacon_node/store/src/metadata.rs b/beacon_node/store/src/metadata.rs index 1d70e105b9..55c64bf850 100644 --- a/beacon_node/store/src/metadata.rs +++ b/beacon_node/store/src/metadata.rs @@ -4,7 +4,7 @@ use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use types::{Checkpoint, Hash256, Slot}; -pub const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(22); +pub const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(23); // All the keys that get stored under the `BeaconMeta` column. // diff --git a/beacon_node/tests/test.rs b/beacon_node/tests/test.rs index 0d448e6c06..ab78b65ae9 100644 --- a/beacon_node/tests/test.rs +++ b/beacon_node/tests/test.rs @@ -41,7 +41,7 @@ fn http_server_genesis_state() { .block_on(remote_node.get_debug_beacon_states(StateId::Slot(Slot::new(0)))) .expect("should fetch state from http api") .unwrap() - .data; + .into_data(); let mut db_state = node .client diff --git a/book/.markdownlint.yml b/book/.markdownlint.yml index 4f7d113364..a40a2f5dbd 100644 --- a/book/.markdownlint.yml +++ b/book/.markdownlint.yml @@ -25,4 +25,8 @@ MD036: false # MD040 code blocks should have a language specified: https://github.com/DavidAnson/markdownlint/blob/main/doc/md040.md # Set to false as the help_x.md files are code blocks without a language specified, which is fine and does not need to change -MD040: false \ No newline at end of file +MD040: false + +# MD059 Link text should be descriptive: https://github.com/DavidAnson/markdownlint/blob/main/doc/md059.md +# Set to false because it is too strict +MD059: false diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 3d09e3a6a5..feecd4b689 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -22,6 +22,7 @@ * [Doppelganger Protection](./validator_doppelganger.md) * [Suggested Fee Recipient](./validator_fee_recipient.md) * [Validator Graffiti](./validator_graffiti.md) + * [Consolidation](./validator_consolidation.md) * [APIs](./api.md) * [Beacon Node API](./api_bn.md) * [Lighthouse API](./api_lighthouse.md) @@ -31,7 +32,7 @@ * [Authorization Header](./api_vc_auth_header.md) * [Prometheus Metrics](./api_metrics.md) * [Lighthouse UI (Siren)](./ui.md) - * [Configuration](./ui_configuration.md) + * [Installation](./ui_installation.md) * [Authentication](./ui_authentication.md) * [Usage](./ui_usage.md) * [FAQs](./ui_faqs.md) @@ -61,6 +62,7 @@ * [Development Environment](./contributing_setup.md) * [FAQs](./faq.md) * [Protocol Developers](./developers.md) + * [Lighthouse Architecture](./developers_architecture.md) * [Security Researchers](./security.md) * [Archived](./archived.md) * [Merge Migration](./archived_merge_migration.md) diff --git a/book/src/advanced_blobs.md b/book/src/advanced_blobs.md index aa995b8e1d..524f70219f 100644 --- a/book/src/advanced_blobs.md +++ b/book/src/advanced_blobs.md @@ -6,7 +6,7 @@ In the Deneb network upgrade, one of the changes is the implementation of EIP-48 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: + After Deneb, 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: @@ -16,6 +16,8 @@ In the Deneb network upgrade, one of the changes is the implementation of EIP-48 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. + After Electra, the target blobs is increased to 6 blobs per block. This means blobs storage is expected to use ~100GB of disk space. + 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. @@ -25,7 +27,7 @@ In the Deneb network upgrade, one of the changes is the implementation of EIP-48 Use the flag `--prune-blobs false` in the beacon node. The storage requirement will be: ```text - 2**17 bytes * 3 blobs / block * 7200 blocks / day * 30 days = 79GB / month or 948GB / year + 2**17 bytes * 6 blobs / block * 7200 blocks / day * 30 days = 158GB / month or 1896GB / 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. diff --git a/book/src/advanced_database_migrations.md b/book/src/advanced_database_migrations.md index a9bfb00ccd..e9954e2ad9 100644 --- a/book/src/advanced_database_migrations.md +++ b/book/src/advanced_database_migrations.md @@ -7,7 +7,8 @@ been applied automatically and in a _backwards compatible_ way. However, backwards compatibility does not imply the ability to _downgrade_ to a prior version of Lighthouse after upgrading. To facilitate smooth downgrades, Lighthouse v2.3.0 and above includes a -command for applying database downgrades. +command for applying database downgrades. If a downgrade is available _from_ a schema version, +it is listed in the table below under the "Downgrade available?" header. **Everything on this page applies to the Lighthouse _beacon node_, not to the validator client or the slasher**. @@ -16,12 +17,8 @@ validator client or the slasher**. | Lighthouse version | Release date | Schema version | Downgrade available? | |--------------------|--------------|----------------|----------------------| +| v7.0.0 | Apr 2025 | v22 | no | | v6.0.0 | Nov 2024 | v22 | no | -| v5.3.0 | Aug 2024 | v21 | yes | -| v5.2.0 | Jun 2024 | v19 | no | -| v5.1.0 | Mar 2024 | v19 | no | -| v5.0.0 | Feb 2024 | v19 | no | -| v4.6.0 | Dec 2023 | v19 | no | > **Note**: All point releases (e.g. v4.4.1) are schema-compatible with the prior minor release > (e.g. v4.4.0). @@ -128,7 +125,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`, `holesky` or `sepolia`. +4. The `--network` flag must be set to the correct network, e.g. `mainnet`, `hoodi` or `sepolia`. The general form for a `lighthouse db` command is: @@ -209,8 +206,9 @@ Here are the steps to prune historic states: | Lighthouse version | Release date | Schema version | Downgrade available? | |--------------------|--------------|----------------|-------------------------------------| +| v7.0.0 | Apr 2025 | v22 | no | | v6.0.0 | Nov 2024 | v22 | no | -| v5.3.0 | Aug 2024 | v21 | yes | +| v5.3.0 | Aug 2024 | v21 | yes before Electra using <= v7.0.0 | | v5.2.0 | Jun 2024 | v19 | yes before Deneb using <= v5.2.1 | | v5.1.0 | Mar 2024 | v19 | yes before Deneb using <= v5.2.1 | | v5.0.0 | Feb 2024 | v19 | yes before Deneb using <= v5.2.1 | diff --git a/book/src/advanced_redundancy.md b/book/src/advanced_redundancy.md index 4582866657..4c231ed6ab 100644 --- a/book/src/advanced_redundancy.md +++ b/book/src/advanced_redundancy.md @@ -39,9 +39,6 @@ There are a few interesting properties about the list of `--beacon-nodes`: earlier in the list. - *Synced is preferred*: the validator client prefers a synced beacon node over one that is still syncing. -- *Failure is sticky*: if a beacon node fails, it will be flagged as offline - and won't be retried again for the rest of the slot (12 seconds). This helps prevent the impact - of time-outs and other lengthy errors. > Note: When supplying multiple beacon nodes the `http://localhost:5052` address must be explicitly > provided (if it is desired). It will only be used as default if no `--beacon-nodes` flag is @@ -76,6 +73,22 @@ Prior to v3.2.0 fallback beacon nodes also required the `--subscribe-all-subnets now broadcast subscriptions to all connected beacon nodes by default. This broadcast behaviour can be disabled using the `--broadcast none` flag for `lighthouse vc`. +### Fallback Health + +Since v6.0.0, the validator client will be more aggressive in switching to a fallback node. To do this, +it uses the concept of "Health". Every slot, the validator client checks each connected beacon node +to determine which node is the "Healthiest". In general, the validator client will prefer nodes +which are synced, have synced execution layers and which are not currently optimistically +syncing. + +Sync distance is separated out into 4 tiers: "Synced", "Small", "Medium", "Large". Nodes are then +sorted into tiers based onto sync distance and execution layer status. You can use the +`--beacon-nodes-sync-tolerances` to change how many slots wide each tier is. In the case where +multiple nodes fall into the same tier, user order is used to tie-break. + +To see health information for each connected node, you can use the +[`/lighthouse/beacon/health` API endpoint](./api_vc_endpoints.md#get-lighthousebeaconhealth). + ### Broadcast modes Since v4.6.0, the Lighthouse VC can be configured to broadcast messages to all configured beacon diff --git a/book/src/advanced_release_candidates.md b/book/src/advanced_release_candidates.md index 9f00da9ae9..f5aee05ede 100644 --- a/book/src/advanced_release_candidates.md +++ b/book/src/advanced_release_candidates.md @@ -40,4 +40,4 @@ 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., Holesky). +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., Hoodi). diff --git a/book/src/advanced_web3signer.md b/book/src/advanced_web3signer.md index 6145fd4a71..4280d58500 100644 --- a/book/src/advanced_web3signer.md +++ b/book/src/advanced_web3signer.md @@ -56,3 +56,11 @@ SSL client authentication with the "self-signed" certificate in `/home/paul/my-k > with a new timeout in milliseconds. This is the timeout before requests to Web3Signer are > considered to be failures. Setting a value that is too long may create contention and late duties > in the VC. Setting it too short will result in failed signatures and therefore missed duties. + +## Slashing protection database + +Web3signer can be configured with its own slashing protection database. This makes the local slashing protection database by Lighthouse redundant. To disable Lighthouse slashing protection database for web3signer keys, use the flag `--disable-slashing-protection-web3signer` on the validator client. + +> Note: DO NOT use this flag unless you are certain that slashing protection is enabled on web3signer. + +The `--init-slashing-protection` flag is also required to initialize the slashing protection database locally. diff --git a/book/src/api_vc_auth_header.md b/book/src/api_vc_auth_header.md index f792ee870e..3e536cf3c8 100644 --- a/book/src/api_vc_auth_header.md +++ b/book/src/api_vc_auth_header.md @@ -32,7 +32,7 @@ When starting the validator client it will output a log message containing the p to the file containing the api token. ```text -Sep 28 19:17:52.615 INFO HTTP API started api_token_file: "$HOME/holesky/validators/api-token.txt", listen_address: 127.0.0.1:5062 +Sep 28 19:17:52.615 INFO HTTP API started api_token_file: "$HOME/hoodi/validators/api-token.txt", listen_address: 127.0.0.1:5062 ``` The _path_ to the API token may also be fetched from the HTTP API itself (this endpoint is the only @@ -46,7 +46,7 @@ Response: ```json { - "token_path": "/home/karlm/.lighthouse/holesky/validators/api-token.txt" + "token_path": "/home/karlm/.lighthouse/hoodi/validators/api-token.txt" } ``` diff --git a/book/src/api_vc_endpoints.md b/book/src/api_vc_endpoints.md index a7c6f0ad5e..87c9a517a5 100644 --- a/book/src/api_vc_endpoints.md +++ b/book/src/api_vc_endpoints.md @@ -18,6 +18,7 @@ | [`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 | +| [`GET /lighthouse/beacon/health`](#get-lighthousebeaconhealth) | Get health information for each connected beacon node. | The query to Lighthouse API endpoints requires authorization, see [Authorization Header](./api_vc_auth_header.md). @@ -225,26 +226,33 @@ Example Response Body ```json { "data": { - "CONFIG_NAME": "holesky", + "CONFIG_NAME": "hoodi", "PRESET_BASE": "mainnet", - "TERMINAL_TOTAL_DIFFICULTY": "10790000", + "TERMINAL_TOTAL_DIFFICULTY": "0", "TERMINAL_BLOCK_HASH": "0x0000000000000000000000000000000000000000000000000000000000000000", "TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH": "18446744073709551615", "MIN_GENESIS_ACTIVE_VALIDATOR_COUNT": "16384", - "MIN_GENESIS_TIME": "1614588812", - "GENESIS_FORK_VERSION": "0x00001020", - "GENESIS_DELAY": "1919188", - "ALTAIR_FORK_VERSION": "0x01001020", - "ALTAIR_FORK_EPOCH": "36660", - "BELLATRIX_FORK_VERSION": "0x02001020", - "BELLATRIX_FORK_EPOCH": "112260", - "CAPELLA_FORK_VERSION": "0x03001020", - "CAPELLA_FORK_EPOCH": "162304", + "MIN_GENESIS_TIME": "1742212800", + "GENESIS_FORK_VERSION": "0x10000910", + "GENESIS_DELAY": "600", + "ALTAIR_FORK_VERSION": "0x20000910", + "ALTAIR_FORK_EPOCH": "0", + "BELLATRIX_FORK_VERSION": "0x30000910", + "BELLATRIX_FORK_EPOCH": "0", + "CAPELLA_FORK_VERSION": "0x40000910", + "CAPELLA_FORK_EPOCH": "0", + "DENEB_FORK_VERSION": "0x50000910", + "DENEB_FORK_EPOCH": "0", + "ELECTRA_FORK_VERSION": "0x60000910", + "ELECTRA_FORK_EPOCH": "2048", + "FULU_FORK_VERSION": "0x70000910", + "FULU_FORK_EPOCH": "18446744073709551615", "SECONDS_PER_SLOT": "12", - "SECONDS_PER_ETH1_BLOCK": "14", + "SECONDS_PER_ETH1_BLOCK": "12", "MIN_VALIDATOR_WITHDRAWABILITY_DELAY": "256", "SHARD_COMMITTEE_PERIOD": "256", "ETH1_FOLLOW_DISTANCE": "2048", + "SUBNETS_PER_NODE": "2", "INACTIVITY_SCORE_BIAS": "4", "INACTIVITY_SCORE_RECOVERY_RATE": "16", "EJECTION_BALANCE": "16000000000", @@ -252,9 +260,36 @@ Example Response Body "MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT": "8", "CHURN_LIMIT_QUOTIENT": "65536", "PROPOSER_SCORE_BOOST": "40", - "DEPOSIT_CHAIN_ID": "5", - "DEPOSIT_NETWORK_ID": "5", - "DEPOSIT_CONTRACT_ADDRESS": "0xff50ed3d0ec03ac01d4c79aad74928bff48a7b2b", + "DEPOSIT_CHAIN_ID": "560048", + "DEPOSIT_NETWORK_ID": "560048", + "DEPOSIT_CONTRACT_ADDRESS": "0x00000000219ab540356cbb839cbe05303d7705fa", + "GAS_LIMIT_ADJUSTMENT_FACTOR": "1024", + "MAX_PAYLOAD_SIZE": "10485760", + "MAX_REQUEST_BLOCKS": "1024", + "MIN_EPOCHS_FOR_BLOCK_REQUESTS": "33024", + "TTFB_TIMEOUT": "5", + "RESP_TIMEOUT": "10", + "ATTESTATION_PROPAGATION_SLOT_RANGE": "32", + "MAXIMUM_GOSSIP_CLOCK_DISPARITY_MILLIS": "500", + "MESSAGE_DOMAIN_INVALID_SNAPPY": "0x00000000", + "MESSAGE_DOMAIN_VALID_SNAPPY": "0x01000000", + "ATTESTATION_SUBNET_PREFIX_BITS": "6", + "MAX_REQUEST_BLOCKS_DENEB": "128", + "MAX_REQUEST_BLOB_SIDECARS": "768", + "MAX_REQUEST_DATA_COLUMN_SIDECARS": "16384", + "MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS": "4096", + "BLOB_SIDECAR_SUBNET_COUNT": "6", + "MAX_BLOBS_PER_BLOCK": "6", + "MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA": "128000000000", + "MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT": "256000000000", + "MAX_BLOBS_PER_BLOCK_ELECTRA": "9", + "BLOB_SIDECAR_SUBNET_COUNT_ELECTRA": "9", + "MAX_REQUEST_BLOB_SIDECARS_ELECTRA": "1152", + "NUMBER_OF_COLUMNS": "128", + "NUMBER_OF_CUSTODY_GROUPS": "128", + "DATA_COLUMN_SIDECAR_SUBNET_COUNT": "128", + "SAMPLES_PER_SLOT": "8", + "CUSTODY_REQUIREMENT": "4", "MAX_COMMITTEES_PER_SLOT": "64", "TARGET_COMMITTEE_SIZE": "128", "MAX_VALIDATORS_PER_COMMITTEE": "2048", @@ -303,23 +338,45 @@ Example Response Body "MAX_BLS_TO_EXECUTION_CHANGES": "16", "MAX_WITHDRAWALS_PER_PAYLOAD": "16", "MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP": "16384", - "DOMAIN_DEPOSIT": "0x03000000", - "BLS_WITHDRAWAL_PREFIX": "0x00", - "RANDOM_SUBNETS_PER_VALIDATOR": "1", - "DOMAIN_SYNC_COMMITTEE": "0x07000000", - "TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE": "16", - "DOMAIN_BEACON_ATTESTER": "0x01000000", - "DOMAIN_VOLUNTARY_EXIT": "0x04000000", - "DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF": "0x08000000", - "DOMAIN_CONTRIBUTION_AND_PROOF": "0x09000000", - "EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION": "256", - "TARGET_AGGREGATORS_PER_COMMITTEE": "16", - "DOMAIN_APPLICATION_MASK": "0x00000001", - "DOMAIN_AGGREGATE_AND_PROOF": "0x06000000", - "DOMAIN_RANDAO": "0x02000000", - "DOMAIN_SELECTION_PROOF": "0x05000000", + "MAX_BLOB_COMMITMENTS_PER_BLOCK": "4096", + "FIELD_ELEMENTS_PER_BLOB": "4096", + "MIN_ACTIVATION_BALANCE": "32000000000", + "MAX_EFFECTIVE_BALANCE_ELECTRA": "2048000000000", + "MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA": "4096", + "WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA": "4096", + "PENDING_DEPOSITS_LIMIT": "134217728", + "PENDING_PARTIAL_WITHDRAWALS_LIMIT": "134217728", + "PENDING_CONSOLIDATIONS_LIMIT": "262144", + "MAX_ATTESTER_SLASHINGS_ELECTRA": "1", + "MAX_ATTESTATIONS_ELECTRA": "8", + "MAX_DEPOSIT_REQUESTS_PER_PAYLOAD": "8192", + "MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD": "16", + "MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD": "2", + "MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP": "8", + "MAX_PENDING_DEPOSITS_PER_EPOCH": "16", + "FIELD_ELEMENTS_PER_CELL": "64", + "FIELD_ELEMENTS_PER_EXT_BLOB": "8192", + "KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH": "4", "DOMAIN_BEACON_PROPOSER": "0x00000000", - "SYNC_COMMITTEE_SUBNET_COUNT": "4" + "DOMAIN_CONTRIBUTION_AND_PROOF": "0x09000000", + "DOMAIN_DEPOSIT": "0x03000000", + "DOMAIN_SELECTION_PROOF": "0x05000000", + "VERSIONED_HASH_VERSION_KZG": "1", + "TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE": "16", + "DOMAIN_VOLUNTARY_EXIT": "0x04000000", + "BLS_WITHDRAWAL_PREFIX": "0x00", + "DOMAIN_APPLICATION_MASK": "0x00000001", + "DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF": "0x08000000", + "DOMAIN_SYNC_COMMITTEE": "0x07000000", + "COMPOUNDING_WITHDRAWAL_PREFIX": "0x02", + "TARGET_AGGREGATORS_PER_COMMITTEE": "16", + "SYNC_COMMITTEE_SUBNET_COUNT": "4", + "DOMAIN_BEACON_ATTESTER": "0x01000000", + "UNSET_DEPOSIT_REQUESTS_START_INDEX": "18446744073709551615", + "FULL_EXIT_REQUEST_AMOUNT": "0", + "DOMAIN_AGGREGATE_AND_PROOF": "0x06000000", + "ETH1_ADDRESS_WITHDRAWAL_PREFIX": "0x01", + "DOMAIN_RANDAO": "0x02000000" } } ``` @@ -351,7 +408,7 @@ Example Response Body ```json { - "token_path": "/home/karlm/.lighthouse/holesky/validators/api-token.txt" + "token_path": "/home/karlm/.lighthouse/hoodi/validators/api-token.txt" } ``` @@ -816,3 +873,56 @@ logs emitted are INFO level or higher. } } ``` + +## `GET /lighthouse/beacon/health` + +Provides information about the sync status and execution layer health of each connected beacon node. +For more information about how to interpret the beacon node health, see [Fallback Health](./advanced_redundancy.md#fallback-health). + +### HTTP Specification + +| Property | Specification | +|-------------------|--------------------------------------------| +| Path | `/lighthouse/beacon/health` | +| Method | GET | +| Required Headers | [`Authorization`](./api_vc_auth_header.md) | +| Typical Responses | 200, 400 | + +Command: + +```bash +DATADIR=/var/lib/lighthouse +curl -X GET http://localhost:5062/lighthouse/beacon/health \ + -H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" | jq + ``` + +### Example Response Body + +```json +{ + "data": { + "beacon_nodes": [ + { + "index": 0, + "endpoint": "http://localhost:5052", + "health": { + "user_index": 0, + "head": 10500000, + "optimistic_status": "No", + "execution_status": "Healthy", + "health_tier": { + "tier": 1, + "sync_distance": 0, + "distance_tier": "Synced" + } + } + }, + { + "index": 1, + "endpoint": "http://fallbacks-r.us", + "health": "Offline" + } + ] + } +} +``` diff --git a/book/src/archived.md b/book/src/archived.md index 7b6e4b7e8e..d37cd9aa15 100644 --- a/book/src/archived.md +++ b/book/src/archived.md @@ -1,3 +1,3 @@ # Archived -This section keeps the topics that are deprecated or less applicable for archived purposes. +This section keeps the topics that are deprecated. Documentation in this section is for informational purposes only and will not be maintained. diff --git a/book/src/archived-key-management.md b/book/src/archived_key_management.md similarity index 88% rename from book/src/archived-key-management.md rename to book/src/archived_key_management.md index 3f600794e0..d8b00e8352 100644 --- a/book/src/archived-key-management.md +++ b/book/src/archived_key_management.md @@ -75,21 +75,21 @@ mnemonic is encrypted with a password. It is the responsibility of the user to define a strong password. The password is only required for interacting with the wallet, it is not required for recovering keys from a mnemonic. -To create a wallet, use the `lighthouse account wallet` command. For example, if we wish to create a new wallet for the Holesky testnet named `wally` and saves it in `~/.lighthouse/holesky/wallets` with a randomly generated password saved +To create a wallet, use the `lighthouse account wallet` command. For example, if we wish to create a new wallet for the Hoodi testnet named `wally` and saves it in `~/.lighthouse/hoodi/wallets` with a randomly generated password saved to `./wallet.pass`: ```bash -lighthouse --network holesky account wallet create --name wally --password-file wally.pass +lighthouse --network hoodi account wallet create --name wally --password-file wally.pass ``` -Using the above command, a wallet will be created in `~/.lighthouse/holesky/wallets` with the name +Using the above command, a wallet will be created in `~/.lighthouse/hoodi/wallets` with the name `wally`. It is encrypted using the password defined in the `wally.pass` file. During the wallet creation process, a 24-word mnemonic will be displayed. Record the mnemonic because it allows you to recreate the files in the case of data loss. > Notes: > -> - When navigating to the directory `~/.lighthouse/holesky/wallets`, one will not see the wallet name `wally`, but a hexadecimal folder containing the wallet file. However, when interacting with `lighthouse` in the CLI, the name `wally` will be used. +> - When navigating to the directory `~/.lighthouse/hoodi/wallets`, one will not see the wallet name `wally`, but a hexadecimal folder containing the wallet file. However, when interacting with `lighthouse` in the CLI, the name `wally` will be used. > - The password is not `wally.pass`, it is the _content_ of the > `wally.pass` file. > - If `wally.pass` already exists, the wallet password will be set to the content @@ -100,18 +100,18 @@ During the wallet creation process, a 24-word mnemonic will be displayed. Record Validators are fundamentally represented by a BLS keypair. In Lighthouse, we use a wallet to generate these keypairs. Once a wallet exists, the `lighthouse account validator create` command can be used to generate the BLS keypair and all necessary information to submit a validator deposit. With the `wally` wallet created in [Step 1](#step-1-create-a-wallet-and-record-the-mnemonic), we can create a validator with the command: ```bash -lighthouse --network holesky account validator create --wallet-name wally --wallet-password wally.pass --count 1 +lighthouse --network hoodi account validator create --wallet-name wally --wallet-password wally.pass --count 1 ``` This command will: -- Derive a single new BLS keypair from wallet `wally` in `~/.lighthouse/holesky/wallets`, updating it so that it generates a new key next time. -- Create a new directory `~/.lighthouse/holesky/validators` containing: +- Derive a single new BLS keypair from wallet `wally` in `~/.lighthouse/hoodi/wallets`, updating it so that it generates a new key next time. +- Create a new directory `~/.lighthouse/hoodi/validators` containing: - An encrypted keystore file `voting-keystore.json` containing the validator's voting keypair. - An `eth1_deposit_data.rlp` assuming the default deposit amount (`32 ETH`) which can be submitted to the deposit contract for the Goerli testnet. Other networks can be set via the `--network` parameter. -- Create a new directory `~/.lighthouse/holesky/secrets` which stores a password to the validator's voting keypair. +- Create a new directory `~/.lighthouse/hoodi/secrets` which stores a password to the validator's voting keypair. If you want to create another validator in the future, repeat [Step 2](#step-2-create-a-validator). The wallet keeps track of how many validators it has generated and ensures that a new validator is generated each time. The important thing is to keep the 24-word mnemonic safe so that it can be used to generate new validator keys if needed. diff --git a/book/src/archived-merge-migration.md b/book/src/archived_merge_migration.md similarity index 100% rename from book/src/archived-merge-migration.md rename to book/src/archived_merge_migration.md diff --git a/book/src/developers_architecture.md b/book/src/developers_architecture.md new file mode 100644 index 0000000000..1150525512 --- /dev/null +++ b/book/src/developers_architecture.md @@ -0,0 +1,5 @@ +# Lighthouse architecture + +A technical walkthrough of Lighthouse's architecture can be found at: [Lighthouse technical walkthrough](https://www.youtube.com/watch?v=pLHhTh_vGZ0) + +![Lighthouse architecture](imgs/developers_architecture.svg) diff --git a/book/src/faq.md b/book/src/faq.md index a741834501..b97a82fcca 100644 --- a/book/src/faq.md +++ b/book/src/faq.md @@ -17,7 +17,6 @@ ## [Validator](#validator-1) -- [Why does it take so long for a validator to be activated?](#vc-activation) - [Can I use redundancy in my staking setup?](#vc-redundancy) - [I am missing attestations. Why?](#vc-missed-attestations) - [Sometimes I miss the attestation head vote, resulting in penalty. Is this normal?](#vc-head-vote) @@ -112,10 +111,7 @@ After checkpoint forwards sync completes, the beacon node will start to download INFO Downloading historical blocks est_time: --, distance: 4524545 slots (89 weeks 5 days), service: slot_notifier ``` -If the same log appears every minute and you do not see progress in downloading historical blocks, you can try one of the followings: - -- Check the number of peers you are connected to. If you have low peers (less than 50), try to do port forwarding on the ports 9000 TCP/UDP and 9001 UDP to increase peer count. -- Restart the beacon node. +If the same log appears every minute and you do not see progress in downloading historical blocks, 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. ### I proposed a block but the beacon node shows `could not publish message` with error `duplicate` as below, should I be worried? @@ -154,29 +150,13 @@ This is a normal behaviour. Since [v4.1.0](https://github.com/sigp/lighthouse/re ### My beacon node logs `WARN Error processing HTTP API request`, what should I do? -This warning usually comes with an http error code. Some examples are given below: +An example of the log is shown below -1. The log shows: +```text +WARN Error processing HTTP API request method: GET, path: /eth/v1/validator/attestation_data, status: 500 Internal Server Error, elapsed: 305.65µs +``` - ```text - WARN Error processing HTTP API request method: GET, path: /eth/v1/validator/attestation_data, status: 500 Internal Server Error, elapsed: 305.65µs - ``` - - The error is `500 Internal Server Error`. This suggests that the execution client is not synced. Once the execution client is synced, the error will disappear. - -1. The log shows: - - ```text - WARN Error processing HTTP API request method: POST, path: /eth/v1/validator/duties/attester/199565, status: 503 Service Unavailable, elapsed: 96.787µs - ``` - - The error is `503 Service Unavailable`. This means that the beacon node is still syncing. When this happens, the validator client will log: - - ```text - 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 warning usually happens when the validator client sends a request to the beacon node, but the beacon node is unable to fulfil the request. This can be due to the execution client is not synced/is syncing and/or the beacon node is syncing. The error show go away when the node is synced. ### My beacon node logs `WARN Error signalling fork choice waiter`, what should I do? @@ -190,13 +170,21 @@ This suggests that the computer resources are being overwhelmed. It could be due ### My beacon node logs `ERRO Aggregate attestation queue full`, what should I do? -An example of the full log is shown below: +Some examples of the full log is shown below: ```text ERRO Aggregate attestation queue full, queue_len: 4096, msg: the system has insufficient resources for load, module: network::beacon_processor:1542 +ERRO Attestation delay queue is full msg: system resources may be saturated, queue_size: 16384, service: bproc ``` -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. +This suggests that the computer resources are being overwhelmed. It could be due to high CPU usage or high disk I/O usage. Some common reasons are: + +- when the beacon node is downloading historical blocks +- the execution client is syncing +- disk IO is being overwhelmed +- parallel API queries to the beacon node + +If the node is syncing or downloading historical blocks, the error should 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? @@ -204,77 +192,6 @@ This is a known [bug](https://github.com/sigp/lighthouse/issues/3707) that will ## Validator -### Why does it take so long for a validator to be activated? - -After validators create their execution layer deposit transaction there are two waiting -periods before they can start producing blocks and attestations: - -1. Waiting for the beacon chain to recognise the execution layer block containing the - deposit (generally takes ~13.6 hours). -1. Waiting in the queue for validator activation. - -Detailed answers below: - -#### 1. Waiting for the beacon chain to detect the execution layer deposit - -Since the beacon chain uses the execution layer for validator on-boarding, beacon chain -validators must listen to event logs from the deposit contract. Since the -latest blocks of the execution chain are vulnerable to re-orgs due to minor network -partitions, beacon nodes follow the execution chain at a distance of 2048 blocks -(~6.8 hours) (see -[`ETH1_FOLLOW_DISTANCE`](https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/validator.md#process-deposit)). -This follow distance protects the beacon chain from on-boarding validators that -are likely to be removed due to an execution chain re-org. - -Now we know there's a 6.8 hours delay before the beacon nodes even _consider_ an -execution layer block. Once they _are_ considering these blocks, there's a voting period -where beacon validators vote on which execution block hash to include in the beacon chain. This -period is defined as 64 epochs (~6.8 hours, see -[`ETH1_VOTING_PERIOD`](https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/beacon-chain.md#time-parameters)). -During this voting period, each beacon block producer includes an -[`Eth1Data`](https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/beacon-chain.md#eth1data) -in their block which counts as a vote towards what that validator considers to -be the head of the execution chain at the start of the voting period (with respect -to `ETH1_FOLLOW_DISTANCE`, of course). You can see the exact voting logic -[here](https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/validator.md#eth1-data). - -These two delays combined represent the time between an execution layer deposit being -included in an execution data vote and that validator appearing in the beacon chain. -The `ETH1_FOLLOW_DISTANCE` delay causes a minimum delay of ~6.8 hours and -`ETH1_VOTING_PERIOD` means that if a validator deposit happens just _before_ -the start of a new voting period then they might not notice this delay at all. -However, if the validator deposit happens just _after_ the start of the new -voting period the validator might have to wait ~6.8 hours for next voting -period. In times of very severe network issues, the network may even fail -to vote in new execution layer blocks, thus stopping all new validator deposits and causing the wait to be longer. - -#### 2. Waiting for a validator to be activated - -If a validator has provided an invalid public key or signature, they will -_never_ be activated. -They will simply be forgotten by the beacon chain! But, if those parameters were -correct, once the execution layer delays have elapsed and the validator appears in the -beacon chain, there's _another_ delay before the validator becomes "active" -(canonical definition -[here](https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/beacon-chain.md#is_active_validator)) and can start producing blocks and attestations. - -Firstly, the validator won't become active until their beacon chain balance is -equal to or greater than -[`MAX_EFFECTIVE_BALANCE`](https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/beacon-chain.md#gwei-values) -(32 ETH on mainnet, usually 3.2 ETH on testnets). Once this balance is reached, -the validator must wait until the start of the next epoch (up to 6.4 minutes) -for the -[`process_registry_updates`](https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/beacon-chain.md#registry-updates) -routine to run. This routine activates validators with respect to a [churn -limit](https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/beacon-chain.md#get_validator_churn_limit); -it will only allow the number of validators to increase (churn) by a certain -amount. If a new validator isn't within the churn limit from the front of the queue, -they will need to wait another epoch (6.4 minutes) for their next chance. This -repeats until the queue is cleared. The churn limit for validators joining the beacon chain is capped at 8 per epoch or 1800 per day. 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! - ### Can I use redundancy in my staking setup? You should **never** use duplicate/redundant validator keypairs or validator clients (i.e., don't @@ -299,15 +216,15 @@ Another cause for missing attestations is the block arriving late, or there are An example of the log: (debug logs can be found under `$datadir/beacon/logs`): ```text -Delayed head block, set_as_head_time_ms: 27, imported_time_ms: 168, attestable_delay_ms: 4209, available_delay_ms: 4186, execution_time_ms: 201, blob_delay_ms: 3815, observed_delay_ms: 3984, total_delay_ms: 4381, slot: 1886014, proposer_index: 733, block_root: 0xa7390baac88d50f1cbb5ad81691915f6402385a12521a670bbbd4cd5f8bf3934, service: beacon, module: beacon_chain::canonical_head:1441 +DEBG Delayed head block, set_as_head_time_ms: 37, imported_time_ms: 1824, attestable_delay_ms: 3660, available_delay_ms: 3491, execution_time_ms: 78, consensus_time_ms: 161, blob_delay_ms: 3291, observed_delay_ms: 3250, total_delay_ms: 5352, slot: 11429888, proposer_index: 778696, block_root: 0x34cc0675ad5fd052699af2ff37b858c3eb8186c5b29fdadb1dabd246caf79e43, service: beacon, module: beacon_chain::canonical_head:1440 ``` -The field to look for is `attestable_delay`, which defines the time when a block is ready for the validator to attest. If the `attestable_delay` is greater than 4s which has past the window of attestation, the attestation will fail. In the above example, the delay is mostly caused by late block observed by the node, as shown in `observed_delay`. 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). Ideally, `observed_delay` should be less than 3 seconds. In this example, the validator failed to attest the block due to the block arriving late. +The field to look for is `attestable_delay`, which defines the time when a block is ready for the validator to attest. If the `attestable_delay` is greater than 4s then it has missed the window for attestation, and the attestation will fail. In the above example, the delay is mostly caused by a late block observed by the node, as shown in `observed_delay`. 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). Ideally, `observed_delay` should be less than 3 seconds. In this example, the validator failed to attest to the block due to the block arriving late. Another example of log: ``` -DEBG Delayed head block, set_as_head_time_ms: 22, imported_time_ms: 312, attestable_delay_ms: 7052, available_delay_ms: 6874, execution_time_ms: 4694, blob_delay_ms: 2159, observed_delay_ms: 2179, total_delay_ms: 7209, slot: 1885922, proposer_index: 606896, block_root: 0x9966df24d24e722d7133068186f0caa098428696e9f441ac416d0aca70cc0a23, service: beacon, module: beacon_chain::canonical_head:1441 +DEBG Delayed head block, set_as_head_time_ms: 22, imported_time_ms: 312, attestable_delay_ms: 7052, available_delay_ms: 6874, execution_time_ms: 4694, consensus_time_ms: 232, blob_delay_ms: 2159, observed_delay_ms: 2179, total_delay_ms: 7209, slot: 1885922, proposer_index: 606896, block_root: 0x9966df24d24e722d7133068186f0caa098428696e9f441ac416d0aca70cc0a23, service: beacon, module: beacon_chain::canonical_head:1441 /159.69.68.247/tcp/9000, service: libp2p, module: lighthouse_network::service:1811 ``` @@ -323,7 +240,7 @@ Another possible reason for missing the head vote is due to a chain "reorg". A r ### Can I submit a voluntary exit message without running a beacon node? -Yes. Beaconcha.in provides the tool to broadcast the message. You can create the voluntary exit message file with [ethdo](https://github.com/wealdtech/ethdo/releases/tag/v1.30.0) and submit the message via the [beaconcha.in](https://beaconcha.in/tools/broadcast) website. A guide on how to use `ethdo` to perform voluntary exit can be found [here](https://github.com/eth-educators/ethstaker-guides/blob/main/docs/voluntary-exit.md). +Yes. Beaconcha.in provides the tool to broadcast the message. You can create the voluntary exit message file with [ethdo](https://github.com/wealdtech/ethdo/releases) and submit the message via the [beaconcha.in](https://beaconcha.in/tools/broadcast) website. A guide on how to use `ethdo` to perform voluntary exit can be found [here](https://github.com/eth-educators/ethstaker-guides/blob/main/docs/voluntary-exit.md). It is also noted that you can submit your BLS-to-execution-change message to update your withdrawal credentials from type `0x00` to `0x01` using the same link. @@ -345,7 +262,7 @@ If you do not want to stop `lighthouse vc`, you can use the [key manager API](./ ### 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. +You can use the `lighthouse vm delete` command to delete validator keys, see [validator manager delete](./validator_manager_api.md#delete). 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. @@ -358,7 +275,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 -`Holesky` testnet and are now trying to join a new network but using the same +`Hoodi` 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). diff --git a/book/src/help_vc.md b/book/src/help_vc.md index c32104b17a..15b5c209a7 100644 --- a/book/src/help_vc.md +++ b/book/src/help_vc.md @@ -40,7 +40,7 @@ Options: The gas limit to be used in all builder proposals for all validators managed by this validator client. Note this will not necessarily be used if the gas limit set here moves too far from the previous block's - gas limit. [default: 30000000] + gas limit. [default: 36000000] --genesis-state-url A URL of a beacon-API compatible server from which to download the genesis state. Checkpoint sync server URLs can generally be used with diff --git a/book/src/imgs/consolidation-source.png b/book/src/imgs/consolidation-source.png new file mode 100644 index 0000000000..955cc1a443 Binary files /dev/null and b/book/src/imgs/consolidation-source.png differ diff --git a/book/src/imgs/consolidation-target.png b/book/src/imgs/consolidation-target.png new file mode 100644 index 0000000000..853a069562 Binary files /dev/null and b/book/src/imgs/consolidation-target.png differ diff --git a/book/src/imgs/deposit-funds.png b/book/src/imgs/deposit-funds.png new file mode 100644 index 0000000000..ab5e207ad1 Binary files /dev/null and b/book/src/imgs/deposit-funds.png differ diff --git a/book/src/imgs/developers_architecture.svg b/book/src/imgs/developers_architecture.svg new file mode 100644 index 0000000000..66c9c0ec89 --- /dev/null +++ b/book/src/imgs/developers_architecture.svg @@ -0,0 +1,4 @@ + + + +
p2p network
p2p network
rust-libp2p
rust-libp2p
lighthouse_network
lighthouse_network
gossipsub
gossipsub
http_api
http_api
validator client
validator client
crypto
crypto
bls
bls
blst
blst
kzg
kzg
ckzg
ckzg
discv5
discv5
slasher
slasher
store
store
execution_layer
execution_layer
execution client
execution client
operation_pool
operation_pool
mev-boost
mev-boost
builder_client
builder_client
beacon_processor
beacon_processor
tokio
tokio
network
network
gossip_methods
gossip_methods
rpc_methods
rpc_methods
sync
sync
beacon_chain
beacon_chain
block_verification
block_verification
attestation_verification
attestation_verificati...
blob_verification
blob_verification
blob_verification
blob_verification
light_client_*
light_client_*
block_verification
block_verification
import_block
import_block
produce_block
produce_block
Linux/macOS/Windows
Linux/macOS/Windows
Legend
Legend
= internal crate
= internal crate
= external crate
= external crate
= file
= file
= function/method
= function/method
= external service/component
= external service/compone...
consensus
consensus
types
types
state_processing
state_processing
ethereum_ssz
ethereum_ssz
tree_hash
tree_hash
milhouse
milhouse
fork_choice
fork_choice
merkle_proof
merkle_proof
sha2
sha2
leveldb
leveldb
\ No newline at end of file diff --git a/book/src/imgs/partial-withdrawal-siren.png b/book/src/imgs/partial-withdrawal-siren.png new file mode 100644 index 0000000000..76e140f7ae Binary files /dev/null and b/book/src/imgs/partial-withdrawal-siren.png differ diff --git a/book/src/imgs/ui-dep-1.png b/book/src/imgs/ui-dep-1.png new file mode 100644 index 0000000000..c70e1d8337 Binary files /dev/null and b/book/src/imgs/ui-dep-1.png differ diff --git a/book/src/imgs/ui-dep-2.png b/book/src/imgs/ui-dep-2.png new file mode 100644 index 0000000000..cf3f301892 Binary files /dev/null and b/book/src/imgs/ui-dep-2.png differ diff --git a/book/src/imgs/ui-dep-3.png b/book/src/imgs/ui-dep-3.png new file mode 100644 index 0000000000..a7b129a1ba Binary files /dev/null and b/book/src/imgs/ui-dep-3.png differ diff --git a/book/src/imgs/ui-dep-4.png b/book/src/imgs/ui-dep-4.png new file mode 100644 index 0000000000..6458bc2807 Binary files /dev/null and b/book/src/imgs/ui-dep-4.png differ diff --git a/book/src/imgs/ui-dep-5.png b/book/src/imgs/ui-dep-5.png new file mode 100644 index 0000000000..ebed72effc Binary files /dev/null and b/book/src/imgs/ui-dep-5.png differ diff --git a/book/src/imgs/ui-dep-6.png b/book/src/imgs/ui-dep-6.png new file mode 100644 index 0000000000..2b8ec2827c Binary files /dev/null and b/book/src/imgs/ui-dep-6.png differ diff --git a/book/src/imgs/ui-session.png b/book/src/imgs/ui-session.png index b950e164f7..68becc0f53 100644 Binary files a/book/src/imgs/ui-session.png and b/book/src/imgs/ui-session.png differ diff --git a/book/src/imgs/ui-val-modal.png b/book/src/imgs/ui-val-modal.png index 09c2287591..ae3424f30f 100644 Binary files a/book/src/imgs/ui-val-modal.png and b/book/src/imgs/ui-val-modal.png differ diff --git a/book/src/installation_cross_compiling.md b/book/src/installation_cross_compiling.md index 4f6ba9af38..59fa3762c2 100644 --- a/book/src/installation_cross_compiling.md +++ b/book/src/installation_cross_compiling.md @@ -18,7 +18,8 @@ project. The `Makefile` in the project contains two targets for cross-compiling: - `build-x86_64`: builds an optimized version for x86_64 processors (suitable for most users). -- `build-aarch64`: builds an optimized version for 64-bit ARM processors (suitable for Raspberry Pi 4). +- `build-aarch64`: builds an optimized version for 64-bit ARM processors (suitable for Raspberry Pi 4/5). +- `build-riscv64`: builds an optimized version for 64-bit RISC-V processors. ### Example diff --git a/book/src/installation_docker.md b/book/src/installation_docker.md index 8ee0c56bb4..12ce4f690c 100644 --- a/book/src/installation_docker.md +++ b/book/src/installation_docker.md @@ -99,7 +99,7 @@ You can run a Docker beacon node with the following command: 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 Holesky testnet, use `--network holesky` instead. +> To join the Hoodi testnet, use `--network hoodi` instead. > The `-v` (Volumes) and `-p` (Ports) and values are described below. diff --git a/book/src/mainnet_validator.md b/book/src/mainnet_validator.md index d21d49f0c9..8da8b98f89 100644 --- a/book/src/mainnet_validator.md +++ b/book/src/mainnet_validator.md @@ -12,7 +12,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 [Holesky staking launchpad](https://holesky.launchpad.ethereum.org/en/). +- Thoroughly exploring the [Staking Launchpad][launchpad] website, try running through the deposit process using a testnet launchpad such as the [Hoodi staking launchpad](https://hoodi.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. @@ -33,12 +33,12 @@ There are five primary steps to become a validator: 1. [Start an execution client and Lighthouse beacon node](#step-2-start-an-execution-client-and-lighthouse-beacon-node) 1. [Import validator keys into Lighthouse](#step-3-import-validator-keys-to-lighthouse) 1. [Start Lighthouse validator client](#step-4-start-lighthouse-validator-client) -1. [Submit deposit](#step-5-submit-deposit-32eth-per-validator) +1. [Submit deposit](#step-5-submit-deposit-a-minimum-of-32eth-to-activate-one-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 Holesky testnet uses Holesky ETH which is worthless. This allows experimentation without real-world costs. +> **Never use real ETH to join a testnet!** Testnet such as the Hoodi testnet uses Hoodi ETH which is worthless. This allows experimentation without real-world costs. ### Step 1. Create validator keys @@ -48,7 +48,7 @@ The Ethereum Foundation provides the [staking-deposit-cli](https://github.com/et ./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 `holesky` if you want to run a Holesky 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 `hoodi` if you want to run a Hoodi 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. @@ -71,10 +71,10 @@ Mainnet: lighthouse --network mainnet account validator import --directory $HOME/staking-deposit-cli/validator_keys ``` -Holesky testnet: +Hoodi testnet: ```bash -lighthouse --network holesky account validator import --directory $HOME/staking-deposit-cli/validator_keys +lighthouse --network hoodi 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. @@ -132,10 +132,10 @@ Mainnet: lighthouse vc --network mainnet --suggested-fee-recipient YourFeeRecipientAddress ``` -Holesky testnet: +Hoodi testnet: ```bash -lighthouse vc --network holesky --suggested-fee-recipient YourFeeRecipientAddress +lighthouse vc --network hoodi --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. @@ -151,13 +151,13 @@ Once this log appears (and there are no errors) the `lighthouse vc` application will ensure that the validator starts performing its duties and being rewarded by the protocol. -### Step 5: Submit deposit (32ETH per validator) +### Step 5: Submit deposit (a minimum of 32ETH to activate one 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 [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. +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 [Hoodi staking launchpad](https://hoodi.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 ETH 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. -Once the deposit transaction is confirmed, it will take a minimum of ~16 hours to a few days/weeks for the beacon chain to process and activate your validator, depending on the queue. Refer to our [FAQ - Why does it take so long for a validator to be activated](./faq.md#why-does-it-take-so-long-for-a-validator-to-be-activated) for more info. +Once the deposit transaction is confirmed, it will take a minimum of ~13 minutes to a few days to activate your validator, depending on the queue. Once your validator is activated, the validator client will start to publish attestations each epoch: diff --git a/book/src/run_a_node.md b/book/src/run_a_node.md index 15567497e5..6c43ef5e32 100644 --- a/book/src/run_a_node.md +++ b/book/src/run_a_node.md @@ -54,7 +54,7 @@ Notable flags: - `--network` flag, which selects a network: - `lighthouse` (no flag): Mainnet. - `lighthouse --network mainnet`: Mainnet. - - `lighthouse --network holesky`: Holesky (testnet). + - `lighthouse --network hoodi`: Hoodi (testnet). - `lighthouse --network sepolia`: Sepolia (testnet). - `lighthouse --network chiado`: Chiado (testnet). - `lighthouse --network gnosis`: Gnosis chain. diff --git a/book/src/ui.md b/book/src/ui.md index e980e90268..4cf645714e 100644 --- a/book/src/ui.md +++ b/book/src/ui.md @@ -21,7 +21,7 @@ The UI is currently in active development. It resides in the See the following Siren specific topics for more context-specific information: -- [Configuration Guide](./ui_configuration.md) - Explanation of how to setup +- [Installation Guide](./ui_installation.md) - Explanation of how to setup and configure Siren. - [Authentication Guide](./ui_authentication.md) - Explanation of how Siren authentication works and protects validator actions. - [Usage](./ui_usage.md) - Details various Siren components. diff --git a/book/src/ui_authentication.md b/book/src/ui_authentication.md index 36e3835e3b..92cefd6901 100644 --- a/book/src/ui_authentication.md +++ b/book/src/ui_authentication.md @@ -2,12 +2,12 @@ ## Siren Session -For enhanced security, Siren will require users to authenticate with their session password to access the dashboard. This is crucial because Siren now includes features that can permanently alter the status of the user's validators. The session password must be set during the [configuration](./ui_configuration.md) process before running the Docker or local build, either in an `.env` file or via Docker flags. +For enhanced security, Siren will require users to authenticate with their session password to access the dashboard. This is crucial because Siren now includes features that can permanently alter the status of the user's validators. The session password must be set during the [installation](./ui_installation.md) process before running the Docker or local build, either in an `.env` file or via Docker flags. ![exit](imgs/ui-session.png) ## Protected Actions -Prior to executing any sensitive validator action, Siren will request authentication of the session password. If you wish to update your password please refer to the Siren [configuration process](./ui_configuration.md). +Prior to executing any sensitive validator action, Siren will request authentication of the session password. If you wish to update your password please refer to the Siren [installation process](./ui_installation.md). ![exit](imgs/ui-auth.png) diff --git a/book/src/ui_faqs.md b/book/src/ui_faqs.md index db365e2fa0..cbfaa2c430 100644 --- a/book/src/ui_faqs.md +++ b/book/src/ui_faqs.md @@ -10,22 +10,30 @@ The required API token may be found in the default data directory of the validat ## 3. How do I fix the Node Network Errors? -If you receive a red notification with a BEACON or VALIDATOR NODE NETWORK ERROR you can refer to the lighthouse ui [`configuration`](./ui_configuration.md#configuration). +If you receive a red notification with a BEACON or VALIDATOR NODE NETWORK ERROR you can refer to the lighthouse ui [`installation`](./ui_installation.md#configuration). ## 4. How do I connect Siren to Lighthouse from a different computer on the same network? -Siren is a webapp, you can access it like any other website. We don't recommend exposing it to the internet; if you require remote access a VPN or (authenticated) reverse proxy is highly recommended. +Siren is a webapp, you can access it like any other website. We don't recommend exposing it to the internet; if you require remote access a VPN or (authenticated) reverse proxy is highly recommended. That being said, it is entirely possible to have it published over the internet, how to do that goes well beyond the scope of this document but we want to emphasize once more the need for *at least* SSL encryption if you choose to do so. ## 5. How can I use Siren to monitor my validators remotely when I am not at home? -Most contemporary home routers provide options for VPN access in various ways. A VPN permits a remote computer to establish a connection with internal computers within a home network. With a VPN configuration in place, connecting to the VPN enables you to treat your computer as if it is part of your local home network. The connection process involves following the setup steps for connecting via another machine on the same network on the Siren configuration page and [`configuration`](./ui_configuration.md#configuration). +Most contemporary home routers provide options for VPN access in various ways. A VPN permits a remote computer to establish a connection with internal computers within a home network. With a VPN configuration in place, connecting to the VPN enables you to treat your computer as if it is part of your local home network. The connection process involves following the setup steps for connecting via another machine on the same network on the Siren configuration page and [`installation`](./ui_installation.md#configuration). ## 6. Does Siren support reverse proxy or DNS named addresses? -Yes, if you need to access your beacon or validator from an address such as `https://merp-server:9909/eth2-vc` you should configure Siren as follows: +Yes, if you need to access your beacon or validator from an address such as `https://merp-server:9909/eth2-vc` you should configure Siren as follows: `VALIDATOR_URL=https://merp-server:9909/eth2-vc` ## 7. 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. + +## 8. How can I connect to Siren using Wallet Connect? + +Depending on your configuration, building with Docker or Local, you will need to include the `NEXT_PUBLIC_WALLET_CONNECT_ID` variable in your `.env` file. To obtain your Wallet Connect project ID, please follow the instructions on their [website](https://cloud.walletconnect.com/sign-in). After providing a valid project ID, the Wallet Connect option should appear in the wallet connector dropdown. + +## 9. I can't log in to Siren even with correct credentials? + +When you deploy Siren via Docker, `NODE_ENV` defaults to `production`, which enforces HTTPS‑only access. If you access the dashboard over HTTP, the authentication cookie can’t be set and login will fail. To allow HTTP access, unset `NODE_ENV` or set it to development. diff --git a/book/src/ui_configuration.md b/book/src/ui_installation.md similarity index 61% rename from book/src/ui_configuration.md rename to book/src/ui_installation.md index 64b293372b..df0522f07a 100644 --- a/book/src/ui_configuration.md +++ b/book/src/ui_installation.md @@ -8,19 +8,55 @@ To ensure proper functionality, the Siren app requires Lighthouse v4.3.0 or high ## Configuration -Siren requires a connection to both a Lighthouse Validator Client and a Lighthouse Beacon Node. +Siren requires a connection to both a Lighthouse Validator Client and a Lighthouse Beacon Node. -Both the Beacon node and the Validator client need to have their HTTP APIs enabled. +Both the Beacon node and the Validator client need to have their HTTP APIs enabled. These ports should be accessible from Siren. This means adding the flag `--http` on both beacon node and validator client. To enable the HTTP API for the beacon node, utilize the `--gui` CLI flag. This action ensures that the HTTP API can be accessed by other software on the same machine. > The Beacon Node must be run with the `--gui` flag set. -## Running the Docker container (Recommended) +## Running Siren with Docker Compose (Recommended) We recommend running Siren's container next to your beacon node (on the same server), as it's essentially a webapp that you can access with any browser. + 1. Clone the Siren repository: + + ``` + git clone https://github.com/sigp/siren + cd siren + ``` + + 1. Copy the example `.env.example` file to `.env`: + + ``` + cp .env.example .env + ``` + + 1. Edit the `.env` file filling in the required fields. A beacon node and validator url needs to be + specified as well as the validator clients `API_TOKEN`, which can be obtained from the [`Validator Client Authorization Header`](./api_vc_auth_header.md). + Note that the HTTP API ports must be accessible from within docker and cannot just be listening + on localhost. This means using the + `--http-address 0.0.0.0` flag on the beacon node and, and both `--http-address 0.0.0.0` and `--unencrypted-http-transport` flags on the validator client. + + 1. Run the containers with docker compose + + ``` + docker compose up -d + ``` + + 1. You should now be able to access siren at the url (provided SSL is enabled): + + ``` + https://localhost + ``` + +> Note: If running on a remote host and the port is exposed, you can access Siren remotely via +`https://` + +## Running Siren in Docker + 1. Create a directory to run Siren: ```bash @@ -29,9 +65,9 @@ We recommend running Siren's container next to your beacon node (on the same ser cd Siren ``` - 1. Create a configuration file in the `Siren` directory: `nano .env` and insert the following fields to the `.env` file. The field values are given here as an example, modify the fields as necessary. For example, the `API_TOKEN` can be obtained from [`Validator Client Authorization Header`](./api_vc_auth_header.md) + 1. Create a configuration file in the `Siren` directory: `nano .env` and insert the following fields to the `.env` file. The field values are given here as an example, modify the fields as necessary. For example, the `API_TOKEN` can be obtained from [`Validator Client Authorization Header`](./api_vc_auth_header.md). - A full example with all possible configuration options can be found [here](https://github.com/sigp/siren/blob/stable/.env.example). + A full example with all possible configuration options can be found [here](https://github.com/sigp/siren/blob/stable/.env.example). ``` BEACON_URL=http://localhost:5052 @@ -43,32 +79,45 @@ We recommend running Siren's container next to your beacon node (on the same ser 1. You can now start Siren with: ```bash - docker run --rm -ti --name siren --env-file $PWD/.env --net host sigp/siren + docker run -ti --name siren --env-file $PWD/.env -p 443:443 sigp/siren ``` - Note that, due to the `--net=host` flag, this will expose Siren on ports 3000, 80, and 443. Preferably, only the latter should be accessible. Adjust your firewall and/or skip the flag wherever possible. +> Note: If you have only exposed your HTTP API ports on the Beacon Node and Validator client to +localhost, i.e via --http-address 127.0.0.1, you must add +`--add-host=host.docker.internal:host-gateway` to the docker command to allow docker to access the +hosts localhost. Alternatively, you should expose the HTTP API to the IP address of the host or +`0.0.0.0` - If it fails to start, an error message will be shown. For example, the error + 1. Siren should be accessible at the url: ``` - http://localhost:5062 unreachable, check settings and connection + https://localhost ``` - means that the validator client is not running, or the `--http` flag is not provided, or otherwise inaccessible from within the container. Another common error is: +> Note: If running on a remote host and the port is exposed, you can access Siren remotely via +`https://` - ``` - validator api issue, server response: 403 - ``` +## Possible Docker Errors - which means that the API token is incorrect. Check that you have provided the correct token in the field `API_TOKEN` in `.env`. +Note that when use SSL, you will get an SSL warning. Advanced users can mount their own certificates or disable SSL altogether, see the `SSL Certificates` section below. This error is safe to ignore. - When Siren has successfully started, you should see the log `LOG [NestApplication] Nest application successfully started +118ms`, indicating that Siren has started. +If it fails to start, an error message will be shown. For example, the error - 1. Siren is now accessible at `https://` (when used with `--net=host`). You will get a warning about an invalid certificate, this can be safely ignored. +``` +http://localhost:5062 unreachable, check settings and connection +``` - > Note: We recommend setting a strong password when running Siren to protect it from unauthorized access. +means that the validator client is not running, or the `--http` flag is not provided, or otherwise inaccessible from within the container. Another common error is: -Advanced users can mount their own certificates or disable SSL altogether, see the `SSL Certificates` section below. +``` +validator api issue, server response: 403 +``` + +which means that the API token is incorrect. Check that you have provided the correct token in the field `API_TOKEN` in `.env`. + +When Siren has successfully started, you should see the log `LOG [NestApplication] Nest application successfully started +118ms`, indicating that Siren has started (in the docker logs). + +> Note: We recommend setting a strong password when running Siren to protect it from unauthorized access. ## Building From Source @@ -101,7 +150,7 @@ By default, internally, Siren is running on port 80 (plain, behind nginx), port [mkcert](https://github.com/FiloSottile/mkcert) is a tool that makes it super easy to generate a self-signed certificate that is trusted by your browser. -To use it for `siren`, install it following the instructions. Then, run `mkdir certs; mkcert -cert-file certs/cert.pem -key-file certs/key.pem 127.0.0.1 localhost` (add or replace any IP or hostname that you would use to access it at the end of this command). +To use it for `siren`, install it following the instructions. Then, run `mkdir certs; mkcert -cert-file certs/cert.pem -key-file certs/key.pem 127.0.0.1 localhost` (add or replace any IP or hostname that you would use to access it at the end of this command). To use these generated certificates, add this to to your `docker run` command: `-v $PWD/certs:/certs` The nginx SSL config inside Siren's container expects 3 files: `/certs/cert.pem` `/certs/key.pem` `/certs/key.pass`. If `/certs/cert.pem` does not exist, it will generate a self-signed certificate as mentioned above. If `/certs/cert.pem` does exist, it will attempt to use your provided or persisted certificates. diff --git a/book/src/ui_usage.md b/book/src/ui_usage.md index f52a655c4e..1ee011f29a 100644 --- a/book/src/ui_usage.md +++ b/book/src/ui_usage.md @@ -62,7 +62,7 @@ Siren's validator management view provides a detailed overview of all validators Clicking the validator icon activates a detailed validator modal component. This component also allows users to trigger validator actions and as well to view and update validator graffiti. Each modal contains the validator total income with hourly, daily and weekly earnings estimates. -ui-validator-modal +![bls-execution](imgs/ui-val-modal.png) ### Validator BLS Withdrawal Credentials @@ -77,7 +77,7 @@ If you wish to convert your withdrawal address, Siren will prompt you to provide ### Validator Edit Siren makes it possible to edit your validator's display name by clicking the edit icon in the validator table. Note: This does not change the validator name, but gives it an alias you can use to identify each validator easily. -These settings are stored in your browser's `localStorage` +These settings are stored in your browser's `localStorage` ![edit](imgs/ui-val-edit.png) @@ -87,6 +87,82 @@ Siren provides the ability to exit/withdraw your validators via the validator ma ![exit](imgs/ui-val-exit.png) +### Deposit and Import new Validators + +Siren's deposit flow aims to create a smooth and easy process for depositing and importing a new Lighthouse validator. The process is separated into 6 main steps: + +#### Validator Setup + +- First, select the number of validators you wish to create, ensuring you connect a wallet with sufficient funds to cover each validator deposit. For each validator candidate, you can set a custom name and optionally enable the `0x02` withdrawal credential flag, which indicates to the deposit contract that the validator will compound and have an increased `MAX_EFFECTIVE_BALANCE`. + +![deposit-step-1](imgs/ui-dep-1.png) + +#### Phrase Verification + +- Enter a valid mnemonic phrase to generate corresponding deposit JSON and keystore objects. This is a sensitive step; copying and pasting your mnemonic phrase is not recommended. This information is never stored or transmitted through any communication channel. + +![deposit-step-2](imgs/ui-dep-2.png) + +#### Mnemonic Indexing + +- The mnemonic index is as important as the mnemonic phrase; reusing existing or previously exited indices directs deposits to existing validators and may require additional steps to recover those funds. Each index combined with the mnemonic phrase generates a deterministic public key, which Siren validates by checking against the Beacon Node. Since newly submitted deposits may not immediately appear on the Beacon Node, Siren provides [Beaconcha.in](https://beaconcha.in) links for secondary confirmation. + +![deposit-step-3](imgs/ui-dep-3.png) + +#### Withdrawal Credentials + +- Next, set the withdrawal and suggested fee recipient addresses. In the basic view, you can conveniently set both values to the same address, or switch to the advanced view to specify them separately. You may apply these settings uniformly to all validators or individually per candidate. Each value can be verified by connecting the relevant wallet and signing a valid message. Skipping verification is not recommended, as the withdrawal address will receive the staked validator funds and cannot be changed later. + +![deposit-step-4](imgs/ui-dep-4.png) + +#### Keystore Authentication + +- To securely import your validator post-deposit, set a strong keystore password. You may apply the same password across all candidates or individually assign passwords for each. + +![deposit-step-5](imgs/ui-dep-5.png) + +#### Sign and Deposit + +- Finally, complete each deposit by connecting a wallet with sufficient funds to Siren and signing the transaction. Upon successful inclusion of the deposit in the next block, Siren automatically imports the validator using the provided keystore credentials. Once imported, your validator will appear in Siren when the Beacon Node processes the transaction and enters the deposit queue. Processing time may vary depending on the queue length, potentially taking several days. Siren maintains a record of the deposit transaction for your review during this period. + +![deposit-step-6](imgs/ui-dep-6.png) + +### Consolidate Validator + +`EIP-7251` increases the `MAX_EFFECTIVE_BALANCE` limit up to `2048 ETH`, allowing validators with `0x02` withdrawal credentials to consolidate funds from multiple exited validators. Siren facilitates requests to a consolidation contract, enabling validators to upgrade their withdrawal credentials and merge several validators into one compounding target validator. + +![consolidation-target](imgs/consolidation-target.png) + +#### Eligibility requirements for consolidation + +- Validators must have at least `0x01` withdrawal credentials. Validators with `0x00` credentials must first perform a [BLS Execution Change](./ui_usage.md#validator-bls-withdrawal-credentials). + +- Target validators with `0x01` withdrawal credentials must initiate a self-consolidation request to upgrade credentials to `0x02`, enabling them to accept funds and benefit from the increased balance cap. + +- Source validators must first have been active long enough to become eligible for exit and must not have any pending withdrawal requests. + +![consolidation-source](imgs/consolidation-source.png) + +#### Post-consolidation + +- All source validators will exit automatically, and their funds will be transferred to the target validator. + +- Validators consolidated under the new credentials (`0x02`) will no longer participate in automatic partial withdrawal sweeps. Instead, withdrawal requests must be explicitly submitted to the withdrawal contract as defined in `EIP-7002`. + +### Partial Validator Withdrawal + +`EIP-7002` enables partial withdrawals from validators with `0x02` withdrawal credentials and balances exceeding the `MIN_ACTIVATION_BALANCE`. Additionally, validators with upgraded `0x02` credentials will no longer participate in the automatic withdrawal sweeps, making this tool very valuable for Lighthouse validators. + +In order to request a partial withdrawal you must have access to the wallet set in the validator's withdrawal credentials and enough ETH to cover the withdrawal request and gas fees. Connect this wallet to the Siren dashboard to start withdrawing funds. All pending withdrawals will be visible in the same view for your convenience. + +![partial-withdrawal](imgs/partial-withdrawal-siren.png) + +### Partial Validator Top-ups + +If your validator's `EFFECTIVE_BALANCE` drops, or you've upgraded to `0x02` compounding withdrawal credentials, you can add additional funds. Simply connect any wallet to Siren and enter the desired amount to deposit to your validator. Once prompted sign the deposit transaction and your funds will enter the deposit queue and processed by the Beacon Node. + +![deposit-funds](imgs/deposit-funds.png) + ________________________________________________________________________________________________________________________________ ## Validator and Beacon Logs diff --git a/book/src/validator_consolidation.md b/book/src/validator_consolidation.md new file mode 100644 index 0000000000..3c9860a514 --- /dev/null +++ b/book/src/validator_consolidation.md @@ -0,0 +1,30 @@ +# Consolidation + +With the [Pectra](https://ethereum.org/en/history/#pectra) upgrade, a validator can hold a stake of up to 2048 ETH. This is done by updating the validator withdrawal credentials to type 0x02. With 0x02 withdrawal credentials, it is possible to consolidate two or more validators into a single validator with a higher stake. + +Let's take a look at an example: Initially, validators A and B are both with 0x01 withdrawal credentials with 32 ETH. Let's say we want to consolidate the balance of validator B to validator A, so that the balance of validator A becomes 64 ETH. These are the steps: + +1. Update the withdrawal credentials of validator A to 0x02. You can do this using [Siren](./ui.md) or the [staking launchpad](https://launchpad.ethereum.org/en/). Select: + - source validator: validator A + - target validator: validator A + > Note: After the update, the withdrawal credential type 0x02 cannot be reverted to 0x01, unless the validator exits and makes a fresh deposit. + +2. Perform consolidation by selecting: + - source validator: validator B + - target validator: validator A + + and then execute the transaction. + + Depending on the exit queue and pending consolidations, the process could take from a day to weeks. The outcome is: + - validator A has 64 ETH + - validator B has 0 ETH (i.e., validator B has exited the beacon chain) + +The consolidation process can be repeated to consolidate more validators into validator A. The request is made by signing a transaction using the **withdrawal address** of the source validator. The withdrawal credential of the target validator can be different from the source validator. + +It is important to note that there are some conditions required to perform consolidation, a few common ones are: + +- both source and target validator **must be active** (i.e., not exiting or slashed). +- the _target validator_ **must** have a withdrawal credential **type 0x02**. The source validator could have a 0x01 or 0x02 withdrawal credential. +- the source validator must be active for at least 256 epochs to be able to perform consolidation. + +Note that if a user were to send a consolidation transaction that does not meet the conditions, the transaction can still be accepted by the execution layer. However, the consolidation will fail once it reaches the consensus layer (where the checks are performed). Therefore, it is recommended to check that the conditions are fulfilled before sending a consolidation transaction. diff --git a/book/src/validator_management.md b/book/src/validator_management.md index 18abfb1538..3bfac37ac6 100644 --- a/book/src/validator_management.md +++ b/book/src/validator_management.md @@ -151,7 +151,7 @@ ensure their `secrets-dir` is organised as below: ### Manual configuration The automatic validator discovery process works out-of-the-box with validators -that are created using the `lighthouse account validator new` command. The +that are created using the `lighthouse account validator create` command. The details of this process are only interesting to those who are using keystores generated with another tool or have a non-standard requirements. diff --git a/book/src/validator_sweep.md b/book/src/validator_sweep.md index b707988e84..0755c06d51 100644 --- a/book/src/validator_sweep.md +++ b/book/src/validator_sweep.md @@ -5,6 +5,10 @@ After the [Capella](https://ethereum.org/en/history/#capella) upgrade on 12 - if a validator has a withdrawal credential type `0x00`, the rewards will continue to accumulate and will be locked in the beacon chain. - if a validator has a withdrawal credential type `0x01`, any rewards above 32ETH will be periodically withdrawn to the withdrawal address. This is also known as the "validator sweep", i.e., once the "validator sweep" reaches your validator's index, your rewards will be withdrawn to the withdrawal address. The validator sweep is automatic and it does not incur any fees to withdraw. +## Partial withdrawals via the execution layer + +With the [Pectra](https://ethereum.org/en/history/#pectra) upgrade, validators with 0x02 withdrawal credentials can partially withdraw staked funds via the execution layer by sending a transaction using the withdrawal address. You can withdraw down to a validator balance of 32 ETH. For example, if the validator balance is 40 ETH, you can withdraw up to 8 ETH. You can use [Siren](./ui.md) or the [staking launchpad](https://launchpad.ethereum.org/en/) to execute partial withdrawals. + ## FAQ 1. How to know if I have the withdrawal credentials type `0x00` or `0x01`? diff --git a/book/src/validator_voluntary_exit.md b/book/src/validator_voluntary_exit.md index 6261f2e267..d5d1722d59 100644 --- a/book/src/validator_voluntary_exit.md +++ b/book/src/validator_voluntary_exit.md @@ -27,13 +27,13 @@ After validating the password, the user will be prompted to enter a special exit The exit phrase is the following: > Exit my validator -Below is an example for initiating a voluntary exit on the Holesky testnet. +Below is an example for initiating a voluntary exit on the Hoodi testnet. ``` -$ lighthouse --network holesky account validator exit --keystore /path/to/keystore --beacon-node http://localhost:5052 +$ lighthouse --network hoodi account validator exit --keystore /path/to/keystore --beacon-node http://localhost:5052 -Running account manager for Holesky network -validator-dir path: ~/.lighthouse/holesky/validators +Running account manager for Hoodi network +validator-dir path: ~/.lighthouse/hoodi/validators Enter the keystore password for validator in 0xabcd @@ -45,7 +45,7 @@ WARNING: WARNING: THIS IS AN IRREVERSIBLE OPERATION -PLEASE VISIT https://lighthouse-book.sigmaprime.io/voluntary-exit.html +PLEASE VISIT https://lighthouse-book.sigmaprime.io/validator_voluntary_exit.html TO MAKE SURE YOU UNDERSTAND THE IMPLICATIONS OF A VOLUNTARY EXIT. Enter the exit phrase from the above URL to confirm the voluntary exit: @@ -58,6 +58,31 @@ Please keep your validator running till exit epoch Exit epoch in approximately 1920 secs ``` +## Generate pre-signed exit message without broadcasting + +You can also generate a pre-signed exit message without broadcasting it to the network. To do so, use the `--presign` flag: + +```bash +lighthouse account validator exit --network hoodi --keystore /path/to/keystore --presign +``` + +It will prompt for the keystore password, which, upon entering the correct password, will generate a pre-signed exit message: + +``` +Successfully pre-signed voluntary exit for validator 0x[redacted]. Not publishing. +{ + "message": { + "epoch": "12959", + "validator_index": "123456" + }, + "signature": "0x97deafb740cd56eaf55b671efb35d0ce15cd1835cbcc52e20ee9cdc11e1f4ab8a5f228c378730437eb544ae70e1987cd0d2f925aa3babe686b66df823c90ac4027ef7a06d12c56d536d9bcd3a1d15f02917b170c0aa97ab102d67602a586333f" +} +``` + +## Exit via the execution layer + +The voluntary exit above is via the consensus layer. With the [Pectra](https://ethereum.org/en/history/#pectra) upgrade, validators with 0x01 and 0x02 withdrawal credentials can also exit their validators via the execution layer by sending a transaction using the withdrawal address. You can use [Siren](./ui.md) or the [staking launchpad](https://launchpad.ethereum.org/en/) to send an exit transaction. + ## Full withdrawal of staked fund After the [Capella](https://ethereum.org/en/history/#capella) upgrade on 12th April 2023, if a user initiates a voluntary exit, they will receive the full staked funds to the withdrawal address, provided that the validator has withdrawal credentials of type `0x01`. For more information on how fund withdrawal works, please visit [Ethereum.org](https://ethereum.org/en/staking/withdrawals/#how-do-withdrawals-work) website. @@ -93,7 +118,7 @@ There are two types of withdrawal credentials, `0x00` and `0x01`. To check which - A fixed waiting period of 256 epochs (27.3 hours) for the validator's status to become withdrawable. -- A varying time of "validator sweep" that can take up to _n_ days with _n_ listed in the table below. The "validator sweep" is the process of skimming through all eligible validators by index number for withdrawals (those with type `0x01` and balance above 32ETH). Once the "validator sweep" reaches your validator's index, your staked fund will be fully withdrawn to the withdrawal address set. +- A varying time of "validator sweep" that can take up to _n_ days with _n_ listed in the table below. The "validator sweep" is the process of skimming through all eligible validators by index number for withdrawals (those with type `0x01` and balance above 32ETH). Once the "validator sweep" reaches your validator's index, your staked fund will be fully withdrawn to the withdrawal address set.
diff --git a/boot_node/Cargo.toml b/boot_node/Cargo.toml index 5638be0564..d1b059f3b2 100644 --- a/boot_node/Cargo.toml +++ b/boot_node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "boot_node" -version = "7.0.0-beta.5" +version = "7.1.0-beta.0" authors = ["Sigma Prime "] edition = { workspace = true } diff --git a/common/eth2/Cargo.toml b/common/eth2/Cargo.toml index 35dd806fb3..5d0ad1f45e 100644 --- a/common/eth2/Cargo.toml +++ b/common/eth2/Cargo.toml @@ -19,6 +19,7 @@ mediatype = "0.19.13" multiaddr = "0.18.2" pretty_reqwest_error = { workspace = true } proto_array = { workspace = true } +rand = { workspace = true } reqwest = { workspace = true } reqwest-eventsource = "0.5.0" sensitive_url = { workspace = true } @@ -26,6 +27,7 @@ serde = { workspace = true } serde_json = { workspace = true } slashing_protection = { workspace = true } ssz_types = { workspace = true } +test_random_derive = { path = "../../common/test_random_derive" } types = { workspace = true } zeroize = { workspace = true } diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 9d1b565df0..c1ec41d19c 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -16,7 +16,7 @@ pub mod types; use self::mixin::{RequestAccept, ResponseOptional}; use self::types::{Error as ResponseError, *}; -use ::types::fork_versioned_response::ExecutionOptimisticFinalizedForkVersionedResponse; +use ::types::beacon_response::ExecutionOptimisticFinalizedBeaconResponse; use derivative::Derivative; use either::Either; use futures::Stream; @@ -56,7 +56,7 @@ pub enum Error { /// The `reqwest` client raised an error. HttpClient(PrettyReqwestError), /// The `reqwest_eventsource` client raised an error. - SseClient(reqwest_eventsource::Error), + SseClient(Box), /// The server returned an error message where the body was able to be parsed. ServerMessage(ErrorMessage), /// The server returned an error message with an array of errors. @@ -99,7 +99,7 @@ impl Error { match self { Error::HttpClient(error) => error.inner().status(), Error::SseClient(error) => { - if let reqwest_eventsource::Error::InvalidStatusCode(status, _) = error { + if let reqwest_eventsource::Error::InvalidStatusCode(status, _) = error.as_ref() { Some(*status) } else { None @@ -146,6 +146,7 @@ pub struct Timeouts { pub get_debug_beacon_states: Duration, pub get_deposit_snapshot: Duration, pub get_validator_block: Duration, + pub default: Duration, } impl Timeouts { @@ -165,6 +166,7 @@ impl Timeouts { get_debug_beacon_states: timeout, get_deposit_snapshot: timeout, get_validator_block: timeout, + default: timeout, } } } @@ -239,7 +241,9 @@ impl BeaconNodeHttpClient { url: U, builder: impl FnOnce(RequestBuilder) -> RequestBuilder, ) -> Result { - let response = builder(self.client.get(url)).send().await?; + let response = builder(self.client.get(url).timeout(self.timeouts.default)) + .send() + .await?; ok_or_error(response).await } @@ -283,6 +287,54 @@ impl BeaconNodeHttpClient { } } + pub async fn get_fork_contextual( + &self, + url: U, + ctx_constructor: impl Fn(ForkName) -> Ctx, + ) -> Result>, Error> + where + U: IntoUrl, + T: ContextDeserialize<'static, Ctx>, + Meta: DeserializeOwned, + Ctx: Clone, + { + let response = self + .get_response(url, |b| b.accept(Accept::Json)) + .await + .optional()?; + + let Some(resp) = response else { + return Ok(None); + }; + + let bytes = resp.bytes().await?; + + #[derive(serde::Deserialize)] + struct Helper { + // TODO: remove this default once checkpointz follows the spec + #[serde(default = "ForkName::latest_stable")] + version: ForkName, + #[serde(flatten)] + metadata: serde_json::Value, + data: serde_json::Value, + } + + let helper: Helper = serde_json::from_slice(&bytes).map_err(Error::InvalidJson)?; + + let metadata: Meta = serde_json::from_value(helper.metadata).map_err(Error::InvalidJson)?; + + let ctx = ctx_constructor(helper.version); + + let data: T = ContextDeserialize::context_deserialize(helper.data, ctx) + .map_err(Error::InvalidJson)?; + + Ok(Some(ForkVersionedResponse { + version: helper.version, + metadata, + data, + })) + } + /// Perform a HTTP GET request using an 'accept' header, returning `None` on a 404 error. pub async fn get_bytes_opt_accept_header( &self, @@ -402,11 +454,10 @@ impl BeaconNodeHttpClient { body: &T, timeout: Option, ) -> Result { - let mut builder = self.client.post(url); - if let Some(timeout) = timeout { - builder = builder.timeout(timeout); - } - + let builder = self + .client + .post(url) + .timeout(timeout.unwrap_or(self.timeouts.default)); let response = builder.json(body).send().await?; ok_or_error(response).await } @@ -419,10 +470,10 @@ impl BeaconNodeHttpClient { timeout: Option, fork: ForkName, ) -> Result { - let mut builder = self.client.post(url); - if let Some(timeout) = timeout { - builder = builder.timeout(timeout); - } + let builder = self + .client + .post(url) + .timeout(timeout.unwrap_or(self.timeouts.default)); let response = builder .header(CONSENSUS_VERSION_HEADER, fork.to_string()) .json(body) @@ -437,7 +488,7 @@ impl BeaconNodeHttpClient { url: U, body: &T, ) -> Result { - let builder = self.client.post(url); + let builder = self.client.post(url).timeout(self.timeouts.default); let mut headers = HeaderMap::new(); headers.insert( @@ -456,10 +507,10 @@ impl BeaconNodeHttpClient { timeout: Option, fork: ForkName, ) -> Result { - let mut builder = self.client.post(url); - if let Some(timeout) = timeout { - builder = builder.timeout(timeout); - } + let builder = self + .client + .post(url) + .timeout(timeout.unwrap_or(self.timeouts.default)); let mut headers = HeaderMap::new(); headers.insert( CONSENSUS_VERSION_HEADER, @@ -824,6 +875,26 @@ impl BeaconNodeHttpClient { self.get_opt(path).await } + /// `GET beacon/states/{state_id}/pending_consolidations` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn get_beacon_states_pending_consolidations( + &self, + state_id: StateId, + ) -> Result>>, Error> + { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("states") + .push(&state_id.to_string()) + .push("pending_consolidations"); + + self.get_opt(path).await + } + /// `GET beacon/light_client/updates` /// /// Returns `Ok(None)` on a 404 error. @@ -831,7 +902,7 @@ impl BeaconNodeHttpClient { &self, start_period: u64, count: u64, - ) -> Result>>>, Error> { + ) -> Result>>>, Error> { let mut path = self.eth_path(V1)?; path.path_segments_mut() @@ -846,7 +917,14 @@ impl BeaconNodeHttpClient { path.query_pairs_mut() .append_pair("count", &count.to_string()); - self.get_opt(path).await + self.get_opt(path).await.map(|opt| { + opt.map(|updates: Vec<_>| { + updates + .into_iter() + .map(BeaconResponse::ForkVersioned) + .collect() + }) + }) } /// `GET beacon/light_client/bootstrap` @@ -855,7 +933,7 @@ impl BeaconNodeHttpClient { pub async fn get_light_client_bootstrap( &self, block_root: Hash256, - ) -> Result>>, Error> { + ) -> Result>>, Error> { let mut path = self.eth_path(V1)?; path.path_segments_mut() @@ -865,7 +943,9 @@ impl BeaconNodeHttpClient { .push("bootstrap") .push(&format!("{:?}", block_root)); - self.get_opt(path).await + self.get_opt(path) + .await + .map(|opt| opt.map(BeaconResponse::ForkVersioned)) } /// `GET beacon/light_client/optimistic_update` @@ -873,7 +953,7 @@ impl BeaconNodeHttpClient { /// Returns `Ok(None)` on a 404 error. pub async fn get_beacon_light_client_optimistic_update( &self, - ) -> Result>>, Error> { + ) -> Result>>, Error> { let mut path = self.eth_path(V1)?; path.path_segments_mut() @@ -882,7 +962,9 @@ impl BeaconNodeHttpClient { .push("light_client") .push("optimistic_update"); - self.get_opt(path).await + self.get_opt(path) + .await + .map(|opt| opt.map(BeaconResponse::ForkVersioned)) } /// `GET beacon/light_client/finality_update` @@ -890,7 +972,7 @@ impl BeaconNodeHttpClient { /// Returns `Ok(None)` on a 404 error. pub async fn get_beacon_light_client_finality_update( &self, - ) -> Result>>, Error> { + ) -> Result>>, Error> { let mut path = self.eth_path(V1)?; path.path_segments_mut() @@ -899,7 +981,9 @@ impl BeaconNodeHttpClient { .push("light_client") .push("finality_update"); - self.get_opt(path).await + self.get_opt(path) + .await + .map(|opt| opt.map(BeaconResponse::ForkVersioned)) } /// `GET beacon/headers?slot,parent_root` @@ -962,8 +1046,14 @@ impl BeaconNodeHttpClient { .push("beacon") .push("blocks"); - self.post_with_timeout(path, block_contents, self.timeouts.proposal) - .await?; + let fork_name = block_contents.signed_block().fork_name_unchecked(); + self.post_generic_with_consensus_version( + path, + block_contents, + Some(self.timeouts.proposal), + fork_name, + ) + .await?; Ok(()) } @@ -1181,16 +1271,12 @@ impl BeaconNodeHttpClient { pub async fn get_beacon_blocks( &self, block_id: BlockId, - ) -> Result< - Option>>, - Error, - > { + ) -> Result>>, Error> + { let path = self.get_beacon_blocks_path(block_id)?; - let Some(response) = self.get_response(path, |b| b).await.optional()? else { - return Ok(None); - }; - - Ok(Some(response.json().await?)) + self.get_opt(path) + .await + .map(|opt| opt.map(BeaconResponse::ForkVersioned)) } /// `GET v1/beacon/blob_sidecars/{block_id}` @@ -1200,8 +1286,8 @@ impl BeaconNodeHttpClient { &self, block_id: BlockId, indices: Option<&[u64]>, - ) -> Result>>, Error> - { + spec: &ChainSpec, + ) -> Result>>, Error> { let mut path = self.get_blobs_path(block_id)?; if let Some(indices) = indices { let indices_string = indices @@ -1213,11 +1299,11 @@ impl BeaconNodeHttpClient { .append_pair("indices", &indices_string); } - let Some(response) = self.get_response(path, |b| b).await.optional()? else { - return Ok(None); - }; - - Ok(Some(response.json().await?)) + self.get_fork_contextual(path, |fork| { + (fork, spec.max_blobs_per_block_by_fork(fork) as usize) + }) + .await + .map(|opt| opt.map(BeaconResponse::ForkVersioned)) } /// `GET v1/beacon/blinded_blocks/{block_id}` @@ -1227,15 +1313,13 @@ impl BeaconNodeHttpClient { &self, block_id: BlockId, ) -> Result< - Option>>, + Option>>, Error, > { let path = self.get_beacon_blinded_blocks_path(block_id)?; - let Some(response) = self.get_response(path, |b| b).await.optional()? else { - return Ok(None); - }; - - Ok(Some(response.json().await?)) + self.get_opt(path) + .await + .map(|opt| opt.map(BeaconResponse::ForkVersioned)) } /// `GET v1/beacon/blocks` (LEGACY) @@ -1244,7 +1328,7 @@ impl BeaconNodeHttpClient { 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() @@ -1253,7 +1337,9 @@ impl BeaconNodeHttpClient { .push("blocks") .push(&block_id.to_string()); - self.get_opt(path).await + self.get_opt(path) + .await + .map(|opt| opt.map(BeaconResponse::Unversioned)) } /// `GET beacon/blocks` as SSZ @@ -1334,7 +1420,7 @@ impl BeaconNodeHttpClient { pub async fn get_beacon_blocks_attestations_v2( &self, block_id: BlockId, - ) -> Result>>>, Error> + ) -> Result>>>, Error> { let mut path = self.eth_path(V2)?; @@ -1345,7 +1431,9 @@ impl BeaconNodeHttpClient { .push(&block_id.to_string()) .push("attestations"); - self.get_opt(path).await + self.get_opt(path) + .await + .map(|opt| opt.map(BeaconResponse::ForkVersioned)) } /// `POST v1/beacon/pool/attestations` @@ -1437,7 +1525,7 @@ impl BeaconNodeHttpClient { &self, slot: Option, committee_index: Option, - ) -> Result>>, Error> { + ) -> Result>>, Error> { let mut path = self.eth_path(V2)?; path.path_segments_mut() @@ -1456,7 +1544,7 @@ impl BeaconNodeHttpClient { .append_pair("committee_index", &index.to_string()); } - self.get(path).await + self.get(path).await.map(BeaconResponse::ForkVersioned) } /// `POST v1/beacon/pool/attester_slashings` @@ -1515,7 +1603,7 @@ impl BeaconNodeHttpClient { /// `GET v2/beacon/pool/attester_slashings` pub async fn get_beacon_pool_attester_slashings_v2( &self, - ) -> Result>>, Error> { + ) -> Result>>, Error> { let mut path = self.eth_path(V2)?; path.path_segments_mut() @@ -1524,7 +1612,7 @@ impl BeaconNodeHttpClient { .push("pool") .push("attester_slashings"); - self.get(path).await + self.get(path).await.map(BeaconResponse::ForkVersioned) } /// `POST beacon/pool/proposer_slashings` @@ -1871,7 +1959,13 @@ impl BeaconNodeHttpClient { .push("node") .push("health"); - let status = self.client.get(path).send().await?.status(); + let status = self + .client + .get(path) + .timeout(self.timeouts.default) + .send() + .await? + .status(); if status == StatusCode::OK || status == StatusCode::PARTIAL_CONTENT { Ok(status) } else { @@ -1958,10 +2052,11 @@ impl BeaconNodeHttpClient { 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 + self.get_opt(path) + .await + .map(|opt| opt.map(BeaconResponse::ForkVersioned)) } /// `GET debug/beacon/states/{state_id}` @@ -2045,7 +2140,7 @@ impl BeaconNodeHttpClient { slot: Slot, randao_reveal: &SignatureBytes, graffiti: Option<&Graffiti>, - ) -> Result>, Error> { + ) -> Result>, Error> { self.get_validator_blocks_modular(slot, randao_reveal, graffiti, SkipRandaoVerification::No) .await } @@ -2057,12 +2152,12 @@ impl BeaconNodeHttpClient { 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) .await?; - self.get(path).await + self.get(path).await.map(BeaconResponse::ForkVersioned) } /// returns `GET v2/validator/blocks/{slot}` URL path @@ -2318,7 +2413,7 @@ impl BeaconNodeHttpClient { slot: Slot, randao_reveal: &SignatureBytes, graffiti: Option<&Graffiti>, - ) -> Result>, Error> { + ) -> Result>, Error> { self.get_validator_blinded_blocks_modular( slot, randao_reveal, @@ -2367,7 +2462,7 @@ impl BeaconNodeHttpClient { randao_reveal: &SignatureBytes, graffiti: Option<&Graffiti>, skip_randao_verification: SkipRandaoVerification, - ) -> Result>, Error> { + ) -> Result>, Error> { let path = self .get_validator_blinded_blocks_path::( slot, @@ -2377,7 +2472,7 @@ impl BeaconNodeHttpClient { ) .await?; - self.get(path).await + self.get(path).await.map(BeaconResponse::ForkVersioned) } /// `GET v2/validator/blinded_blocks/{slot}` in ssz format @@ -2466,7 +2561,7 @@ impl BeaconNodeHttpClient { slot: Slot, attestation_data_root: Hash256, committee_index: CommitteeIndex, - ) -> Result>>, Error> { + ) -> Result>>, Error> { let mut path = self.eth_path(V2)?; path.path_segments_mut() @@ -2484,6 +2579,7 @@ impl BeaconNodeHttpClient { self.get_opt_with_timeout(path, self.timeouts.attestation) .await + .map(|opt| opt.map(BeaconResponse::ForkVersioned)) } /// `GET validator/sync_committee_contribution` @@ -2729,7 +2825,7 @@ impl BeaconNodeHttpClient { while let Some(event) = es.next().await { match event { Ok(Event::Open) => break, - Err(err) => return Err(Error::SseClient(err)), + Err(err) => return Err(Error::SseClient(err.into())), // This should never happen as we are guaranteed to get the // Open event before any message starts coming through. Ok(Event::Message(_)) => continue, @@ -2741,7 +2837,7 @@ impl BeaconNodeHttpClient { Ok(Event::Message(message)) => { Some(EventKind::from_sse_bytes(&message.event, &message.data)) } - Err(err) => Some(Err(Error::SseClient(err))), + Err(err) => Some(Err(Error::SseClient(err.into()))), } }))) } diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index 4cffbb776c..db850bdd84 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -10,7 +10,6 @@ use mediatype::{names, MediaType, MediaTypeList}; use multiaddr::Multiaddr; use reqwest::header::HeaderMap; use serde::{Deserialize, Deserializer, Serialize}; -use serde_json::Value; use serde_utils::quoted_u64::Quoted; use ssz::{Decode, DecodeError}; use ssz_derive::{Decode, Encode}; @@ -18,7 +17,9 @@ use std::fmt::{self, Display}; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; +use test_random_derive::TestRandom; use types::beacon_block_body::KzgCommitments; +use types::test_utils::TestRandom; pub use types::*; #[cfg(feature = "lighthouse")] @@ -687,7 +688,7 @@ pub struct ValidatorBalancesQuery { pub id: Option>, } -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Default, Serialize, Deserialize)] #[serde(transparent)] pub struct ValidatorBalancesRequestBody { pub ids: Vec, @@ -807,13 +808,13 @@ pub struct LightClientUpdatesQuery { } #[derive(Encode, Decode)] -pub struct LightClientUpdateSszResponse { - pub response_chunk_len: Vec, - pub response_chunk: Vec, +pub struct LightClientUpdateResponseChunk { + pub response_chunk_len: u64, + pub response_chunk: LightClientUpdateResponseChunkInner, } #[derive(Encode, Decode)] -pub struct LightClientUpdateResponseChunk { +pub struct LightClientUpdateResponseChunkInner { pub context: [u8; 4], pub payload: Vec, } @@ -1053,54 +1054,56 @@ pub struct SseExtendedPayloadAttributesGeneric { pub type SseExtendedPayloadAttributes = SseExtendedPayloadAttributesGeneric; pub type VersionedSsePayloadAttributes = ForkVersionedResponse; -impl ForkVersionDeserialize for SsePayloadAttributes { - fn deserialize_by_fork<'de, D: serde::Deserializer<'de>>( - value: serde_json::value::Value, - fork_name: ForkName, - ) -> Result { - match fork_name { - ForkName::Bellatrix => serde_json::from_value(value) - .map(Self::V1) - .map_err(serde::de::Error::custom), - ForkName::Capella => serde_json::from_value(value) - .map(Self::V2) - .map_err(serde::de::Error::custom), - 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::Eip7805 => serde_json::from_value(value) - .map(Self::V3) - .map_err(serde::de::Error::custom), - ForkName::Fulu => 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" - ))), - } +impl<'de> ContextDeserialize<'de, ForkName> for SsePayloadAttributes { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { + let convert_err = |e| { + serde::de::Error::custom(format!( + "SsePayloadAttributes failed to deserialize: {:?}", + e + )) + }; + Ok(match context { + ForkName::Base | ForkName::Altair => { + return Err(serde::de::Error::custom(format!( + "SsePayloadAttributes failed to deserialize: unsupported fork '{}'", + context + ))) + } + ForkName::Bellatrix => { + Self::V1(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Capella => { + Self::V2(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Deneb | ForkName::Electra | ForkName::Eip7805 | ForkName::Fulu => { + Self::V3(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + }) } } -impl ForkVersionDeserialize for SseExtendedPayloadAttributes { - fn deserialize_by_fork<'de, D: serde::Deserializer<'de>>( - value: serde_json::value::Value, - fork_name: ForkName, - ) -> Result { - let helper: SseExtendedPayloadAttributesGeneric = - serde_json::from_value(value).map_err(serde::de::Error::custom)?; +impl<'de> ContextDeserialize<'de, ForkName> for SseExtendedPayloadAttributes { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { + let helper = + SseExtendedPayloadAttributesGeneric::::deserialize(deserializer)?; + Ok(Self { proposal_slot: helper.proposal_slot, proposer_index: helper.proposer_index, parent_block_root: helper.parent_block_root, parent_block_number: helper.parent_block_number, parent_block_hash: helper.parent_block_hash, - payload_attributes: SsePayloadAttributes::deserialize_by_fork::( + payload_attributes: SsePayloadAttributes::context_deserialize( helper.payload_attributes, - fork_name, - )?, + context, + ) + .map_err(serde::de::Error::custom)?, }) } } @@ -1118,8 +1121,8 @@ pub enum EventKind { ChainReorg(SseChainReorg), ContributionAndProof(Box>), LateHead(SseLateHead), - LightClientFinalityUpdate(Box>), - LightClientOptimisticUpdate(Box>), + LightClientFinalityUpdate(Box>>), + LightClientOptimisticUpdate(Box>>), #[cfg(feature = "lighthouse")] BlockReward(BlockReward), PayloadAttributes(VersionedSsePayloadAttributes), @@ -1201,22 +1204,24 @@ impl EventKind { ServerError::InvalidServerSentEvent(format!("Payload Attributes: {:?}", e)) })?, )), - "light_client_finality_update" => Ok(EventKind::LightClientFinalityUpdate( - serde_json::from_str(data).map_err(|e| { + "light_client_finality_update" => Ok(EventKind::LightClientFinalityUpdate(Box::new( + BeaconResponse::ForkVersioned(serde_json::from_str(data).map_err(|e| { ServerError::InvalidServerSentEvent(format!( "Light Client Finality Update: {:?}", e )) - })?, - )), - "light_client_optimistic_update" => Ok(EventKind::LightClientOptimisticUpdate( - serde_json::from_str(data).map_err(|e| { - ServerError::InvalidServerSentEvent(format!( - "Light Client Optimistic Update: {:?}", - e - )) - })?, - )), + })?), + ))), + "light_client_optimistic_update" => { + Ok(EventKind::LightClientOptimisticUpdate(Box::new( + BeaconResponse::ForkVersioned(serde_json::from_str(data).map_err(|e| { + ServerError::InvalidServerSentEvent(format!( + "Light Client Optimistic Update: {:?}", + e + )) + })?), + ))) + } #[cfg(feature = "lighthouse")] "block_reward" => Ok(EventKind::BlockReward(serde_json::from_str(data).map_err( |e| ServerError::InvalidServerSentEvent(format!("Block Reward: {:?}", e)), @@ -1613,7 +1618,7 @@ mod tests { } } -#[derive(Debug, Encode, Serialize, Deserialize)] +#[derive(Debug, Encode, Serialize)] #[serde(untagged)] #[serde(bound = "E: EthSpec")] #[ssz(enum_behaviour = "transparent")] @@ -1626,7 +1631,7 @@ pub type JsonProduceBlockV3Response = ForkVersionedResponse, ProduceBlockV3Metadata>; /// A wrapper over a [`BeaconBlock`] or a [`BlockContents`]. -#[derive(Debug, Encode, Serialize, Deserialize)] +#[derive(Debug, Encode, Serialize)] #[serde(untagged)] #[serde(bound = "E: EthSpec")] #[ssz(enum_behaviour = "transparent")] @@ -1740,18 +1745,18 @@ impl FullBlockContents { } } -impl ForkVersionDeserialize for FullBlockContents { - fn deserialize_by_fork<'de, D: serde::Deserializer<'de>>( - value: serde_json::value::Value, - fork_name: ForkName, - ) -> Result { - if fork_name.deneb_enabled() { +impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for FullBlockContents { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { + if context.deneb_enabled() { Ok(FullBlockContents::BlockContents( - BlockContents::deserialize_by_fork::<'de, D>(value, fork_name)?, + BlockContents::context_deserialize::(deserializer, context)?, )) } else { Ok(FullBlockContents::Block( - BeaconBlock::deserialize_by_fork::<'de, D>(value, fork_name)?, + BeaconBlock::context_deserialize::(deserializer, context)?, )) } } @@ -1818,7 +1823,7 @@ impl TryFrom<&HeaderMap> for ProduceBlockV3Metadata { } /// A wrapper over a [`SignedBeaconBlock`] or a [`SignedBlockContents`]. -#[derive(Clone, Debug, Encode, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Encode, Serialize)] #[serde(untagged)] #[serde(bound = "E: EthSpec")] #[ssz(enum_behaviour = "transparent")] @@ -1827,6 +1832,26 @@ pub enum PublishBlockRequest { Block(Arc>), } +impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for PublishBlockRequest { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { + let value = + serde_json::Value::deserialize(deserializer).map_err(serde::de::Error::custom)?; + + SignedBlockContents::::context_deserialize(&value, context) + .map(PublishBlockRequest::BlockContents) + .or_else(|_| { + Arc::>::context_deserialize(&value, context) + .map(PublishBlockRequest::Block) + }) + .map_err(|_| { + serde::de::Error::custom("could not match any variant of PublishBlockRequest") + }) + } +} + impl PublishBlockRequest { pub fn new( block: Arc>, @@ -1903,7 +1928,7 @@ impl From> for PublishBlockRequest { } } -#[derive(Debug, Clone, Serialize, Deserialize, Encode)] +#[derive(Debug, Clone, PartialEq, Serialize, Encode)] #[serde(bound = "E: EthSpec")] pub struct SignedBlockContents { pub signed_block: Arc>, @@ -1912,7 +1937,33 @@ pub struct SignedBlockContents { pub blobs: BlobsList, } -#[derive(Debug, Clone, Serialize, Deserialize, Encode)] +impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for SignedBlockContents { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(bound = "E: EthSpec")] + struct Helper { + signed_block: serde_json::Value, + kzg_proofs: KzgProofs, + #[serde(with = "ssz_types::serde_utils::list_of_hex_fixed_vec")] + blobs: BlobsList, + } + let helper = Helper::::deserialize(deserializer).map_err(serde::de::Error::custom)?; + + let block = SignedBeaconBlock::context_deserialize(helper.signed_block, context) + .map_err(serde::de::Error::custom)?; + + Ok(Self { + signed_block: Arc::new(block), + kzg_proofs: helper.kzg_proofs, + blobs: helper.blobs, + }) + } +} + +#[derive(Debug, Clone, Serialize, Encode)] #[serde(bound = "E: EthSpec")] pub struct BlockContents { pub block: BeaconBlock, @@ -1921,11 +1972,11 @@ pub struct BlockContents { pub blobs: BlobsList, } -impl ForkVersionDeserialize for BlockContents { - fn deserialize_by_fork<'de, D: serde::Deserializer<'de>>( - value: serde_json::value::Value, - fork_name: ForkName, - ) -> Result { +impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for BlockContents { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { #[derive(Deserialize)] #[serde(bound = "E: EthSpec")] struct Helper { @@ -1934,10 +1985,13 @@ impl ForkVersionDeserialize for BlockContents { #[serde(with = "ssz_types::serde_utils::list_of_hex_fixed_vec")] blobs: BlobsList, } - let helper: Helper = serde_json::from_value(value).map_err(serde::de::Error::custom)?; + let helper = Helper::::deserialize(deserializer).map_err(serde::de::Error::custom)?; + + let block = BeaconBlock::context_deserialize(helper.block, context) + .map_err(serde::de::Error::custom)?; Ok(Self { - block: BeaconBlock::deserialize_by_fork::<'de, D>(helper.block, fork_name)?, + block, kzg_proofs: helper.kzg_proofs, blobs: helper.blobs, }) @@ -2009,22 +2063,22 @@ impl FullPayloadContents { } } -impl ForkVersionDeserialize for FullPayloadContents { - fn deserialize_by_fork<'de, D: Deserializer<'de>>( - value: Value, - fork_name: ForkName, - ) -> Result { - if fork_name.deneb_enabled() { - serde_json::from_value(value) +impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for FullPayloadContents { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { + if context.deneb_enabled() { + ExecutionPayloadAndBlobs::context_deserialize::(deserializer, context) .map(Self::PayloadAndBlobs) .map_err(serde::de::Error::custom) - } else if fork_name.bellatrix_enabled() { - serde_json::from_value(value) + } else if context.bellatrix_enabled() { + ExecutionPayload::context_deserialize::(deserializer, context) .map(Self::Payload) .map_err(serde::de::Error::custom) } else { Err(serde::de::Error::custom(format!( - "FullPayloadContents deserialization for {fork_name} not implemented" + "FullPayloadContents deserialization for {context} not implemented" ))) } } @@ -2037,6 +2091,30 @@ pub struct ExecutionPayloadAndBlobs { pub blobs_bundle: BlobsBundle, } +impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for ExecutionPayloadAndBlobs { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(bound = "E: EthSpec")] + struct Helper { + execution_payload: serde_json::Value, + blobs_bundle: BlobsBundle, + } + let helper = Helper::::deserialize(deserializer).map_err(serde::de::Error::custom)?; + + Ok(Self { + execution_payload: ExecutionPayload::context_deserialize( + helper.execution_payload, + context, + ) + .map_err(serde::de::Error::custom)?, + blobs_bundle: helper.blobs_bundle, + }) + } +} + impl ForkVersionDecode for ExecutionPayloadAndBlobs { fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result { let mut builder = ssz::SszDecoderBuilder::new(bytes); @@ -2067,7 +2145,7 @@ pub enum ContentType { Ssz, } -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Encode, Decode)] +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Encode, Decode, TestRandom)] #[serde(bound = "E: EthSpec")] pub struct BlobsBundle { pub commitments: KzgCommitments, @@ -2162,6 +2240,10 @@ pub struct StandardAttestationRewards { #[cfg(test)] mod test { + use std::fmt::Debug; + + use types::test_utils::{SeedableRng, TestRandom, XorShiftRng}; + use super::*; #[test] @@ -2175,4 +2257,173 @@ mod test { let y: ValidatorId = serde_json::from_str(pubkey_str).unwrap(); assert_eq!(serde_json::to_string(&y).unwrap(), pubkey_str); } + + #[test] + fn test_publish_block_request_context_deserialize() { + let round_trip_test = |request: PublishBlockRequest| { + let fork_name = request.signed_block().fork_name_unchecked(); + let json_str = serde_json::to_string(&request).unwrap(); + let mut de = serde_json::Deserializer::from_str(&json_str); + let deserialized_request = + PublishBlockRequest::::context_deserialize(&mut de, fork_name) + .unwrap(); + assert_eq!(request, deserialized_request); + }; + + let rng = &mut XorShiftRng::from_seed([42; 16]); + for fork_name in ForkName::list_all() { + let signed_beacon_block = + map_fork_name!(fork_name, SignedBeaconBlock, <_>::random_for_test(rng)); + let request = if fork_name.deneb_enabled() { + let kzg_proofs = KzgProofs::::random_for_test(rng); + let blobs = BlobsList::::random_for_test(rng); + let block_contents = SignedBlockContents { + signed_block: Arc::new(signed_beacon_block), + kzg_proofs, + blobs, + }; + PublishBlockRequest::BlockContents(block_contents) + } else { + PublishBlockRequest::Block(Arc::new(signed_beacon_block)) + }; + round_trip_test(request); + println!("fork_name: {:?} PASSED", fork_name); + } + } + + #[test] + fn test_signed_block_contents_context_deserialize() { + let round_trip_test = |contents: SignedBlockContents| { + let fork_name = contents.signed_block.fork_name_unchecked(); + let json_str = serde_json::to_string(&contents).unwrap(); + let mut de = serde_json::Deserializer::from_str(&json_str); + let deserialized_contents = + SignedBlockContents::::context_deserialize(&mut de, fork_name) + .unwrap(); + assert_eq!(contents, deserialized_contents); + }; + + let mut fork_name = ForkName::Deneb; + let rng = &mut XorShiftRng::from_seed([42; 16]); + loop { + let signed_beacon_block = + map_fork_name!(fork_name, SignedBeaconBlock, <_>::random_for_test(rng)); + let kzg_proofs = KzgProofs::::random_for_test(rng); + let blobs = BlobsList::::random_for_test(rng); + let block_contents = SignedBlockContents { + signed_block: Arc::new(signed_beacon_block), + kzg_proofs, + blobs, + }; + round_trip_test(block_contents); + println!("fork_name: {:?} PASSED", fork_name); + if let Some(next_fork_name) = fork_name.next_fork() { + fork_name = next_fork_name; + } else { + break; + } + } + } + + #[test] + fn test_execution_payload_execution_payload_deserialize_by_fork() { + let rng = &mut XorShiftRng::from_seed([42; 16]); + + let payloads = [ + ExecutionPayload::Bellatrix( + ExecutionPayloadBellatrix::::random_for_test(rng), + ), + ExecutionPayload::Capella(ExecutionPayloadCapella::::random_for_test( + rng, + )), + ExecutionPayload::Deneb(ExecutionPayloadDeneb::::random_for_test( + rng, + )), + ExecutionPayload::Electra(ExecutionPayloadElectra::::random_for_test( + rng, + )), + ExecutionPayload::Fulu(ExecutionPayloadFulu::::random_for_test(rng)), + ]; + let merged_forks = &ForkName::list_all()[2..]; + assert_eq!( + payloads.len(), + merged_forks.len(), + "we should test every known fork; add new fork variant to payloads above" + ); + + for (payload, &fork_name) in payloads.into_iter().zip(merged_forks) { + assert_eq!(payload.fork_name(), fork_name); + let payload_str = serde_json::to_string(&payload).unwrap(); + let mut de = serde_json::Deserializer::from_str(&payload_str); + generic_deserialize_by_fork(&mut de, payload, fork_name); + } + } + + #[test] + fn test_execution_payload_and_blobs_deserialize_by_fork() { + let rng = &mut XorShiftRng::from_seed([42; 16]); + + let payloads = [ + { + let execution_payload = + ExecutionPayload::Deneb( + ExecutionPayloadDeneb::::random_for_test(rng), + ); + let blobs_bundle = BlobsBundle::random_for_test(rng); + ExecutionPayloadAndBlobs { + execution_payload, + blobs_bundle, + } + }, + { + let execution_payload = + ExecutionPayload::Electra( + ExecutionPayloadElectra::::random_for_test(rng), + ); + let blobs_bundle = BlobsBundle::random_for_test(rng); + ExecutionPayloadAndBlobs { + execution_payload, + blobs_bundle, + } + }, + { + let execution_payload = + ExecutionPayload::Fulu( + ExecutionPayloadFulu::::random_for_test(rng), + ); + let blobs_bundle = BlobsBundle::random_for_test(rng); + ExecutionPayloadAndBlobs { + execution_payload, + blobs_bundle, + } + }, + ]; + let blob_forks = &ForkName::list_all()[4..]; + + assert_eq!( + payloads.len(), + blob_forks.len(), + "we should test every known fork; add new fork variant to payloads above" + ); + + for (payload, &fork_name) in payloads.into_iter().zip(blob_forks) { + assert_eq!(payload.execution_payload.fork_name(), fork_name); + let payload_str = serde_json::to_string(&payload).unwrap(); + let mut de = serde_json::Deserializer::from_str(&payload_str); + generic_deserialize_by_fork(&mut de, payload, fork_name); + } + } + + fn generic_deserialize_by_fork< + 'de, + D: Deserializer<'de>, + O: ContextDeserialize<'de, ForkName> + PartialEq + Debug, + >( + deserializer: D, + original: O, + fork_name: ForkName, + ) { + let roundtrip = O::context_deserialize::(deserializer, fork_name).unwrap(); + assert_eq!(original, roundtrip); + } } diff --git a/common/eth2_config/src/lib.rs b/common/eth2_config/src/lib.rs index 017bdf288d..544138f0fa 100644 --- a/common/eth2_config/src/lib.rs +++ b/common/eth2_config/src/lib.rs @@ -212,7 +212,7 @@ macro_rules! define_net { "../", "deposit_contract_block.txt" ), - boot_enr: $this_crate::$include_file!($this_crate, "../", "boot_enr.yaml"), + boot_enr: $this_crate::$include_file!($this_crate, "../", "bootstrap_nodes.yaml"), genesis_state_bytes: $this_crate::$include_file!($this_crate, "../", "genesis.ssz"), } }}; 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/bootstrap_nodes.yaml similarity index 100% rename from common/eth2_network_config/built_in_network_configs/chiado/boot_enr.yaml rename to common/eth2_network_config/built_in_network_configs/chiado/bootstrap_nodes.yaml 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 1455ec5f63..4d4ccdf717 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 @@ -100,15 +100,13 @@ DEPOSIT_CONTRACT_ADDRESS: 0xb97036A26259B7147018913bD58a774cf91acf25 # Networking # --------------------------------------------------------------- # `10 * 2**20` (= 10485760, 10 MiB) -GOSSIP_MAX_SIZE: 10485760 +MAX_PAYLOAD_SIZE: 10485760 # `2**10` (= 1024) MAX_REQUEST_BLOCKS: 1024 # `2**8` (= 256) EPOCHS_PER_SUBNET_SUBSCRIPTION: 256 # 33024, ~31 days MIN_EPOCHS_FOR_BLOCK_REQUESTS: 33024 -# `10 * 2**20` (=10485760, 10 MiB) -MAX_CHUNK_SIZE: 10485760 # 5s TTFB_TIMEOUT: 5 # 10s @@ -150,9 +148,10 @@ MAX_BLOBS_PER_BLOCK_ELECTRA: 2 # MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK_ELECTRA MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 256 -# DAS +# Fulu NUMBER_OF_COLUMNS: 128 NUMBER_OF_CUSTODY_GROUPS: 128 DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 SAMPLES_PER_SLOT: 8 CUSTODY_REQUIREMENT: 4 +MAX_BLOBS_PER_BLOCK_FULU: 12 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/bootstrap_nodes.yaml similarity index 100% rename from common/eth2_network_config/built_in_network_configs/gnosis/boot_enr.yaml rename to common/eth2_network_config/built_in_network_configs/gnosis/bootstrap_nodes.yaml 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 c986b1f496..b02ecf2c49 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 @@ -43,7 +43,7 @@ DENEB_FORK_VERSION: 0x04000064 DENEB_FORK_EPOCH: 889856 # 2024-03-11T18:30:20.000Z # Electra ELECTRA_FORK_VERSION: 0x05000064 -ELECTRA_FORK_EPOCH: 18446744073709551615 +ELECTRA_FORK_EPOCH: 1337856 # 2025-04-30T14:03:40.000Z # eip7805 EIP7805_FORK_VERSION: 0x06000000 EIP7805_FORK_EPOCH: 18446744073709551615 @@ -100,9 +100,8 @@ DEPOSIT_CONTRACT_ADDRESS: 0x0B98057eA310F4d31F2a452B414647007d1645d9 # Network # --------------------------------------------------------------- SUBNETS_PER_NODE: 4 -GOSSIP_MAX_SIZE: 10485760 +MAX_PAYLOAD_SIZE: 10485760 MIN_EPOCHS_FOR_BLOCK_REQUESTS: 33024 -MAX_CHUNK_SIZE: 10485760 TTFB_TIMEOUT: 5 RESP_TIMEOUT: 10 MESSAGE_DOMAIN_INVALID_SNAPPY: 0x00000000 @@ -121,12 +120,25 @@ MAX_REQUEST_BLOB_SIDECARS: 768 MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 16384 # `6` BLOB_SIDECAR_SUBNET_COUNT: 6 -# `uint64(6)` -MAX_BLOBS_PER_BLOCK: 6 +# `uint64(2)` +MAX_BLOBS_PER_BLOCK: 2 -# DAS +# Electra +# 2**7 * 10**9 (= 128,000,000,000) +MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA: 128000000000 +# 2**6 * 10**9 (= 64,000,000,000) +MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT: 64000000000 +# `2` +BLOB_SIDECAR_SUBNET_COUNT_ELECTRA: 2 +# `uint64(2)` +MAX_BLOBS_PER_BLOCK_ELECTRA: 2 +# MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK_ELECTRA +MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 256 + +# Fulu NUMBER_OF_COLUMNS: 128 NUMBER_OF_CUSTODY_GROUPS: 128 DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 SAMPLES_PER_SLOT: 8 CUSTODY_REQUIREMENT: 4 +MAX_BLOBS_PER_BLOCK_FULU: 12 diff --git a/common/eth2_network_config/built_in_network_configs/holesky/boot_enr.yaml b/common/eth2_network_config/built_in_network_configs/holesky/bootstrap_nodes.yaml similarity index 100% rename from common/eth2_network_config/built_in_network_configs/holesky/boot_enr.yaml rename to common/eth2_network_config/built_in_network_configs/holesky/bootstrap_nodes.yaml 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 e5f38b8c9b..19a3f79cc0 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 @@ -88,15 +88,13 @@ DEPOSIT_CONTRACT_ADDRESS: 0x4242424242424242424242424242424242424242 # Networking # --------------------------------------------------------------- # `10 * 2**20` (= 10485760, 10 MiB) -GOSSIP_MAX_SIZE: 10485760 +MAX_PAYLOAD_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 @@ -139,9 +137,10 @@ MAX_BLOBS_PER_BLOCK_ELECTRA: 9 # MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK_ELECTRA MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 1152 -# DAS +# Fulu NUMBER_OF_COLUMNS: 128 NUMBER_OF_CUSTODY_GROUPS: 128 DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 SAMPLES_PER_SLOT: 8 CUSTODY_REQUIREMENT: 4 +MAX_BLOBS_PER_BLOCK_FULU: 12 diff --git a/common/eth2_network_config/built_in_network_configs/hoodi/boot_enr.yaml b/common/eth2_network_config/built_in_network_configs/hoodi/bootstrap_nodes.yaml similarity index 100% rename from common/eth2_network_config/built_in_network_configs/hoodi/boot_enr.yaml rename to common/eth2_network_config/built_in_network_configs/hoodi/bootstrap_nodes.yaml diff --git a/common/eth2_network_config/built_in_network_configs/hoodi/config.yaml b/common/eth2_network_config/built_in_network_configs/hoodi/config.yaml index 19d7797424..5cca1cd037 100644 --- a/common/eth2_network_config/built_in_network_configs/hoodi/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/hoodi/config.yaml @@ -93,15 +93,13 @@ DEPOSIT_CONTRACT_ADDRESS: 0x00000000219ab540356cBB839Cbe05303d7705Fa # Networking # --------------------------------------------------------------- # `10 * 2**20` (= 10485760, 10 MiB) -GOSSIP_MAX_SIZE: 10485760 +MAX_PAYLOAD_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 diff --git a/common/eth2_network_config/built_in_network_configs/mainnet/boot_enr.yaml b/common/eth2_network_config/built_in_network_configs/mainnet/boot_enr.yaml deleted file mode 100644 index 1ae519387a..0000000000 --- a/common/eth2_network_config/built_in_network_configs/mainnet/boot_enr.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Lighthouse Team (Sigma Prime) -- enr:-Le4QPUXJS2BTORXxyx2Ia-9ae4YqA_JWX3ssj4E_J-3z1A-HmFGrU8BpvpqhNabayXeOZ2Nq_sbeDgtzMJpLLnXFgAChGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISsaa0Zg2lwNpAkAIkHAAAAAPA8kv_-awoTiXNlY3AyNTZrMaEDHAD2JKYevx89W0CcFJFiskdcEzkH_Wdv9iW42qLK79ODdWRwgiMohHVkcDaCI4I -- enr:-Le4QLHZDSvkLfqgEo8IWGG96h6mxwe_PsggC20CL3neLBjfXLGAQFOPSltZ7oP6ol54OvaNqO02Rnvb8YmDR274uq8ChGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISLosQxg2lwNpAqAX4AAAAAAPA8kv_-ax65iXNlY3AyNTZrMaEDBJj7_dLFACaxBfaI8KZTh_SSJUjhyAyfshimvSqo22WDdWRwgiMohHVkcDaCI4I -- enr:-Le4QH6LQrusDbAHPjU_HcKOuMeXfdEB5NJyXgHWFadfHgiySqeDyusQMvfphdYWOzuSZO9Uq2AMRJR5O4ip7OvVma8BhGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISLY9ncg2lwNpAkAh8AgQIBAAAAAAAAAAmXiXNlY3AyNTZrMaECDYCZTZEksF-kmgPholqgVt8IXr-8L7Nu7YrZ7HUpgxmDdWRwgiMohHVkcDaCI4I -- enr:-Le4QIqLuWybHNONr933Lk0dcMmAB5WgvGKRyDihy1wHDIVlNuuztX62W51voT4I8qD34GcTEOTmag1bcdZ_8aaT4NUBhGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISLY04ng2lwNpAkAh8AgAIBAAAAAAAAAA-fiXNlY3AyNTZrMaEDscnRV6n1m-D9ID5UsURk0jsoKNXt1TIrj8uKOGW6iluDdWRwgiMohHVkcDaCI4I -# EF Team -- enr:-Ku4QHqVeJ8PPICcWk1vSn_XcSkjOkNiTg6Fmii5j6vUQgvzMc9L1goFnLKgXqBJspJjIsB91LTOleFmyWWrFVATGngBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhAMRHkWJc2VjcDI1NmsxoQKLVXFOhp2uX6jeT0DvvDpPcU8FWMjQdR4wMuORMhpX24N1ZHCCIyg -- enr:-Ku4QG-2_Md3sZIAUebGYT6g0SMskIml77l6yR-M_JXc-UdNHCmHQeOiMLbylPejyJsdAPsTHJyjJB2sYGDLe0dn8uYBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhBLY-NyJc2VjcDI1NmsxoQORcM6e19T1T9gi7jxEZjk_sjVLGFscUNqAY9obgZaxbIN1ZHCCIyg -- enr:-Ku4QPn5eVhcoF1opaFEvg1b6JNFD2rqVkHQ8HApOKK61OIcIXD127bKWgAtbwI7pnxx6cDyk_nI88TrZKQaGMZj0q0Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhDayLMaJc2VjcDI1NmsxoQK2sBOLGcUb4AwuYzFuAVCaNHA-dy24UuEKkeFNgCVCsIN1ZHCCIyg -- enr:-Ku4QEWzdnVtXc2Q0ZVigfCGggOVB2Vc1ZCPEc6j21NIFLODSJbvNaef1g4PxhPwl_3kax86YPheFUSLXPRs98vvYsoBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhDZBrP2Jc2VjcDI1NmsxoQM6jr8Rb1ktLEsVcKAPa08wCsKUmvoQ8khiOl_SLozf9IN1ZHCCIyg -# Teku team (Consensys) -- enr:-KG4QNTx85fjxABbSq_Rta9wy56nQ1fHK0PewJbGjLm1M4bMGx5-3Qq4ZX2-iFJ0pys_O90sVXNNOxp2E7afBsGsBrgDhGV0aDKQu6TalgMAAAD__________4JpZIJ2NIJpcIQEnfA2iXNlY3AyNTZrMaECGXWQ-rQ2KZKRH1aOW4IlPDBkY4XDphxg9pxKytFCkayDdGNwgiMog3VkcIIjKA -- enr:-KG4QF4B5WrlFcRhUU6dZETwY5ZzAXnA0vGC__L1Kdw602nDZwXSTs5RFXFIFUnbQJmhNGVU6OIX7KVrCSTODsz1tK4DhGV0aDKQu6TalgMAAAD__________4JpZIJ2NIJpcIQExNYEiXNlY3AyNTZrMaECQmM9vp7KhaXhI-nqL_R0ovULLCFSFTa9CPPSdb1zPX6DdGNwgiMog3VkcIIjKA -# Prysm team (Prysmatic Labs) -- enr:-Ku4QImhMc1z8yCiNJ1TyUxdcfNucje3BGwEHzodEZUan8PherEo4sF7pPHPSIB1NNuSg5fZy7qFsjmUKs2ea1Whi0EBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQOVphkDqal4QzPMksc5wnpuC3gvSC8AfbFOnZY_On34wIN1ZHCCIyg -- enr:-Ku4QP2xDnEtUXIjzJ_DhlCRN9SN99RYQPJL92TMlSv7U5C1YnYLjwOQHgZIUXw6c-BvRg2Yc2QsZxxoS_pPRVe0yK8Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQMeFF5GrS7UZpAH2Ly84aLK-TyvH-dRo0JM1i8yygH50YN1ZHCCJxA -- enr:-Ku4QPp9z1W4tAO8Ber_NQierYaOStqhDqQdOPY3bB3jDgkjcbk6YrEnVYIiCBbTxuar3CzS528d2iE7TdJsrL-dEKoBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQMw5fqqkw2hHC4F5HZZDPsNmPdB1Gi8JPQK7pRc9XHh-oN1ZHCCKvg -# Nimbus team -- enr:-LK4QA8FfhaAjlb_BXsXxSfiysR7R52Nhi9JBt4F8SPssu8hdE1BXQQEtVDC3qStCW60LSO7hEsVHv5zm8_6Vnjhcn0Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhAN4aBKJc2VjcDI1NmsxoQJerDhsJ-KxZ8sHySMOCmTO6sHM3iCFQ6VMvLTe948MyYN0Y3CCI4yDdWRwgiOM -- enr:-LK4QKWrXTpV9T78hNG6s8AM6IO4XH9kFT91uZtFg1GcsJ6dKovDOr1jtAAFPnS2lvNltkOGA9k29BUN7lFh_sjuc9QBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhANAdd-Jc2VjcDI1NmsxoQLQa6ai7y9PMN5hpLe5HmiJSlYzMuzP7ZhwRiwHvqNXdoN0Y3CCI4yDdWRwgiOM diff --git a/common/eth2_network_config/built_in_network_configs/mainnet/bootstrap_nodes.yaml b/common/eth2_network_config/built_in_network_configs/mainnet/bootstrap_nodes.yaml new file mode 100644 index 0000000000..70aeaac9c5 --- /dev/null +++ b/common/eth2_network_config/built_in_network_configs/mainnet/bootstrap_nodes.yaml @@ -0,0 +1,34 @@ +# Eth mainnet consensus layer bootnodes +# --------------------------------------- +# 1. Tag nodes with maintainer +# 2. Keep nodes updated +# 3. Review PRs: check ENR duplicates, fork-digest, connection. + +# Teku team's bootnodes +- enr:-Iu4QLm7bZGdAt9NSeJG0cEnJohWcQTQaI9wFLu3Q7eHIDfrI4cwtzvEW3F3VbG9XdFXlrHyFGeXPn9snTCQJ9bnMRABgmlkgnY0gmlwhAOTJQCJc2VjcDI1NmsxoQIZdZD6tDYpkpEfVo5bgiU8MGRjhcOmHGD2nErK0UKRrIN0Y3CCIyiDdWRwgiMo # 3.147.37.0 | aws-us-east-2-ohio +- enr:-Iu4QEDJ4Wa_UQNbK8Ay1hFEkXvd8psolVK6OhfTL9irqz3nbXxxWyKwEplPfkju4zduVQj6mMhUCm9R2Lc4YM5jPcIBgmlkgnY0gmlwhANrfESJc2VjcDI1NmsxoQJCYz2-nsqFpeEj6eov9HSi9QssIVIVNr0I89J1vXM9foN0Y3CCIyiDdWRwgiMo # 3.107.124.68 | aws-ap-southeast-2-sydney + +# Prylab team's bootnodes +- enr:-Ku4QImhMc1z8yCiNJ1TyUxdcfNucje3BGwEHzodEZUan8PherEo4sF7pPHPSIB1NNuSg5fZy7qFsjmUKs2ea1Whi0EBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQOVphkDqal4QzPMksc5wnpuC3gvSC8AfbFOnZY_On34wIN1ZHCCIyg # 18.223.219.100 | aws-us-east-2-ohio +- enr:-Ku4QP2xDnEtUXIjzJ_DhlCRN9SN99RYQPJL92TMlSv7U5C1YnYLjwOQHgZIUXw6c-BvRg2Yc2QsZxxoS_pPRVe0yK8Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQMeFF5GrS7UZpAH2Ly84aLK-TyvH-dRo0JM1i8yygH50YN1ZHCCJxA # 18.223.219.100 | aws-us-east-2-ohio +- enr:-Ku4QPp9z1W4tAO8Ber_NQierYaOStqhDqQdOPY3bB3jDgkjcbk6YrEnVYIiCBbTxuar3CzS528d2iE7TdJsrL-dEKoBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQMw5fqqkw2hHC4F5HZZDPsNmPdB1Gi8JPQK7pRc9XHh-oN1ZHCCKvg # 18.223.219.100 | aws-us-east-2-ohio + +# Lighthouse team (Sigma Prime) +- enr:-Le4QPUXJS2BTORXxyx2Ia-9ae4YqA_JWX3ssj4E_J-3z1A-HmFGrU8BpvpqhNabayXeOZ2Nq_sbeDgtzMJpLLnXFgAChGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISsaa0Zg2lwNpAkAIkHAAAAAPA8kv_-awoTiXNlY3AyNTZrMaEDHAD2JKYevx89W0CcFJFiskdcEzkH_Wdv9iW42qLK79ODdWRwgiMohHVkcDaCI4I # 172.105.173.25 | linode-au-sydney +- enr:-Le4QLHZDSvkLfqgEo8IWGG96h6mxwe_PsggC20CL3neLBjfXLGAQFOPSltZ7oP6ol54OvaNqO02Rnvb8YmDR274uq8ChGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISLosQxg2lwNpAqAX4AAAAAAPA8kv_-ax65iXNlY3AyNTZrMaEDBJj7_dLFACaxBfaI8KZTh_SSJUjhyAyfshimvSqo22WDdWRwgiMohHVkcDaCI4I # 139.162.196.49 | linode-uk-london +- enr:-Le4QH6LQrusDbAHPjU_HcKOuMeXfdEB5NJyXgHWFadfHgiySqeDyusQMvfphdYWOzuSZO9Uq2AMRJR5O4ip7OvVma8BhGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISLY9ncg2lwNpAkAh8AgQIBAAAAAAAAAAmXiXNlY3AyNTZrMaECDYCZTZEksF-kmgPholqgVt8IXr-8L7Nu7YrZ7HUpgxmDdWRwgiMohHVkcDaCI4I # 139.99.217.220 | ovh-au-sydney +- enr:-Le4QIqLuWybHNONr933Lk0dcMmAB5WgvGKRyDihy1wHDIVlNuuztX62W51voT4I8qD34GcTEOTmag1bcdZ_8aaT4NUBhGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISLY04ng2lwNpAkAh8AgAIBAAAAAAAAAA-fiXNlY3AyNTZrMaEDscnRV6n1m-D9ID5UsURk0jsoKNXt1TIrj8uKOGW6iluDdWRwgiMohHVkcDaCI4I # 139.99.78.39 | ovh-singapore + +# EF bootnodes +- enr:-Ku4QHqVeJ8PPICcWk1vSn_XcSkjOkNiTg6Fmii5j6vUQgvzMc9L1goFnLKgXqBJspJjIsB91LTOleFmyWWrFVATGngBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhAMRHkWJc2VjcDI1NmsxoQKLVXFOhp2uX6jeT0DvvDpPcU8FWMjQdR4wMuORMhpX24N1ZHCCIyg # 3.17.30.69 | aws-us-east-2-ohio +- enr:-Ku4QG-2_Md3sZIAUebGYT6g0SMskIml77l6yR-M_JXc-UdNHCmHQeOiMLbylPejyJsdAPsTHJyjJB2sYGDLe0dn8uYBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhBLY-NyJc2VjcDI1NmsxoQORcM6e19T1T9gi7jxEZjk_sjVLGFscUNqAY9obgZaxbIN1ZHCCIyg # 18.216.248.220 | aws-us-east-2-ohio +- enr:-Ku4QPn5eVhcoF1opaFEvg1b6JNFD2rqVkHQ8HApOKK61OIcIXD127bKWgAtbwI7pnxx6cDyk_nI88TrZKQaGMZj0q0Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhDayLMaJc2VjcDI1NmsxoQK2sBOLGcUb4AwuYzFuAVCaNHA-dy24UuEKkeFNgCVCsIN1ZHCCIyg # 54.178.44.198 | aws-ap-northeast-1-tokyo +- enr:-Ku4QEWzdnVtXc2Q0ZVigfCGggOVB2Vc1ZCPEc6j21NIFLODSJbvNaef1g4PxhPwl_3kax86YPheFUSLXPRs98vvYsoBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhDZBrP2Jc2VjcDI1NmsxoQM6jr8Rb1ktLEsVcKAPa08wCsKUmvoQ8khiOl_SLozf9IN1ZHCCIyg # 54.65.172.253 | aws-ap-northeast-1-tokyo + +# Nimbus team's bootnodes +- enr:-LK4QA8FfhaAjlb_BXsXxSfiysR7R52Nhi9JBt4F8SPssu8hdE1BXQQEtVDC3qStCW60LSO7hEsVHv5zm8_6Vnjhcn0Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhAN4aBKJc2VjcDI1NmsxoQJerDhsJ-KxZ8sHySMOCmTO6sHM3iCFQ6VMvLTe948MyYN0Y3CCI4yDdWRwgiOM # 3.120.104.18 | aws-eu-central-1-frankfurt +- enr:-LK4QKWrXTpV9T78hNG6s8AM6IO4XH9kFT91uZtFg1GcsJ6dKovDOr1jtAAFPnS2lvNltkOGA9k29BUN7lFh_sjuc9QBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhANAdd-Jc2VjcDI1NmsxoQLQa6ai7y9PMN5hpLe5HmiJSlYzMuzP7ZhwRiwHvqNXdoN0Y3CCI4yDdWRwgiOM # 3.64.117.223 | aws-eu-central-1-frankfurt + +# Lodestar team's bootnodes +- enr:-IS4QPi-onjNsT5xAIAenhCGTDl4z-4UOR25Uq-3TmG4V3kwB9ljLTb_Kp1wdjHNj-H8VVLRBSSWVZo3GUe3z6k0E-IBgmlkgnY0gmlwhKB3_qGJc2VjcDI1NmsxoQMvAfgB4cJXvvXeM6WbCG86CstbSxbQBSGx31FAwVtOTYN1ZHCCIyg # 160.119.254.161 | hostafrica-southafrica +- enr:-KG4QCb8NC3gEM3I0okStV5BPX7Bg6ZXTYCzzbYyEXUPGcZtHmvQtiJH4C4F2jG7azTcb9pN3JlgpfxAnRVFzJ3-LykBgmlkgnY0gmlwhFPlR9KDaXA2kP6AAAAAAAAAAlBW__4my5iJc2VjcDI1NmsxoQLdUv9Eo9sxCt0tc_CheLOWnX59yHJtkBSOL7kpxdJ6GYN1ZHCCIyiEdWRwNoIjKA # 83.229.71.210 | kamatera-telaviv-israel \ No newline at end of file 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 5f25bb936a..85a662bc07 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 @@ -49,7 +49,7 @@ DENEB_FORK_VERSION: 0x04000000 DENEB_FORK_EPOCH: 269568 # March 13, 2024, 01:55:35pm UTC # Electra ELECTRA_FORK_VERSION: 0x05000000 -ELECTRA_FORK_EPOCH: 18446744073709551615 +ELECTRA_FORK_EPOCH: 364032 # May 7, 2025, 10:05:11am UTC # eip7805 EIP7805_FORK_VERSION: 0x06000000 EIP7805_FORK_EPOCH: 18446744073709551615 @@ -106,15 +106,13 @@ DEPOSIT_CONTRACT_ADDRESS: 0x00000000219ab540356cBB839Cbe05303d7705Fa # Networking # --------------------------------------------------------------- # `10 * 2**20` (= 10485760, 10 MiB) -GOSSIP_MAX_SIZE: 10485760 +MAX_PAYLOAD_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 @@ -145,9 +143,22 @@ BLOB_SIDECAR_SUBNET_COUNT: 6 # `uint64(6)` MAX_BLOBS_PER_BLOCK: 6 -# DAS +# Electra +# 2**7 * 10**9 (= 128,000,000,000) +MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA: 128000000000 +# 2**8 * 10**9 (= 256,000,000,000) +MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT: 256000000000 +# `9` +BLOB_SIDECAR_SUBNET_COUNT_ELECTRA: 9 +# `uint64(9)` +MAX_BLOBS_PER_BLOCK_ELECTRA: 9 +# MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK_ELECTRA +MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 1152 + +# Fulu NUMBER_OF_COLUMNS: 128 NUMBER_OF_CUSTODY_GROUPS: 128 DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 SAMPLES_PER_SLOT: 8 CUSTODY_REQUIREMENT: 4 +MAX_BLOBS_PER_BLOCK_FULU: 12 diff --git a/common/eth2_network_config/built_in_network_configs/sepolia/boot_enr.yaml b/common/eth2_network_config/built_in_network_configs/sepolia/bootstrap_nodes.yaml similarity index 100% rename from common/eth2_network_config/built_in_network_configs/sepolia/boot_enr.yaml rename to common/eth2_network_config/built_in_network_configs/sepolia/bootstrap_nodes.yaml 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 af78332205..10be107263 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 @@ -89,15 +89,13 @@ DEPOSIT_CONTRACT_ADDRESS: 0x7f02C3E3c98b133055B8B348B2Ac625669Ed295D # Networking # --------------------------------------------------------------- # `10 * 2**20` (= 10485760, 10 MiB) -GOSSIP_MAX_SIZE: 10485760 +MAX_PAYLOAD_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 @@ -140,9 +138,10 @@ MAX_BLOBS_PER_BLOCK_ELECTRA: 9 # MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK_ELECTRA MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 1152 -# DAS +# Fulu NUMBER_OF_COLUMNS: 128 NUMBER_OF_CUSTODY_GROUPS: 128 DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 SAMPLES_PER_SLOT: 8 CUSTODY_REQUIREMENT: 4 +MAX_BLOBS_PER_BLOCK_FULU: 12 diff --git a/common/eth2_network_config/src/lib.rs b/common/eth2_network_config/src/lib.rs index 0bb12c4187..ac488ed2a3 100644 --- a/common/eth2_network_config/src/lib.rs +++ b/common/eth2_network_config/src/lib.rs @@ -31,7 +31,7 @@ use url::Url; pub use eth2_config::GenesisStateSource; pub const DEPLOY_BLOCK_FILE: &str = "deposit_contract_block.txt"; -pub const BOOT_ENR_FILE: &str = "boot_enr.yaml"; +pub const BOOT_ENR_FILE: &str = "bootstrap_nodes.yaml"; pub const GENESIS_STATE_FILE: &str = "genesis.ssz"; pub const BASE_CONFIG_FILE: &str = "config.yaml"; diff --git a/common/lighthouse_version/src/lib.rs b/common/lighthouse_version/src/lib.rs index bd5e31e3ab..b20708e7b0 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/v7.0.0-beta.5-", - fallback = "Lighthouse/v7.0.0-beta.5" + prefix = "Lighthouse/v7.1.0-beta.0-", + fallback = "Lighthouse/v7.1.0-beta.0" ); /// Returns the first eight characters of the latest commit hash for this build. @@ -54,7 +54,7 @@ pub fn version_with_platform() -> String { /// /// `1.5.1` pub fn version() -> &'static str { - "7.0.0-beta.5" + "7.1.0-beta.0" } /// Returns the name of the current client running. diff --git a/common/logging/Cargo.toml b/common/logging/Cargo.toml index 6975e04505..41c82dbd61 100644 --- a/common/logging/Cargo.toml +++ b/common/logging/Cargo.toml @@ -11,8 +11,6 @@ test_logger = [] # Print log output to stderr when running tests instead of drop chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } logroller = { workspace = true } metrics = { workspace = true } -once_cell = "1.17.1" -parking_lot = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true, features = [ "time" ] } diff --git a/common/logging/src/sse_logging_components.rs b/common/logging/src/sse_logging_components.rs index a25b5be6c5..d526f2b040 100644 --- a/common/logging/src/sse_logging_components.rs +++ b/common/logging/src/sse_logging_components.rs @@ -1,5 +1,4 @@ -// TODO(tracing) fix the comments below and remove reference of slog::Drain -//! This module provides an implementation of `slog::Drain` that optionally writes to a channel if +//! This module provides an implementation of `tracing_subscriber::layer::Layer` that optionally writes to a channel if //! there are subscribers to a HTTP SSE stream. use serde_json::json; diff --git a/common/logging/src/tracing_logging_layer.rs b/common/logging/src/tracing_logging_layer.rs index 810f7e960e..c3784a8f62 100644 --- a/common/logging/src/tracing_logging_layer.rs +++ b/common/logging/src/tracing_logging_layer.rs @@ -13,6 +13,9 @@ use tracing_subscriber::layer::Context; use tracing_subscriber::registry::LookupSpan; use tracing_subscriber::Layer; +const FIXED_MESSAGE_WIDTH: usize = 44; +const ALIGNED_LEVEL_WIDTH: usize = 5; + pub struct LoggingLayer { pub non_blocking_writer: NonBlocking, _guard: WorkerGuard, @@ -368,13 +371,18 @@ fn build_log_text<'a, S>( } } - let level_str = if use_color { - color_level_str + let pad = if plain_level_str.len() < ALIGNED_LEVEL_WIDTH { + " " } else { - plain_level_str + "" + }; + + let level_str = if use_color { + format!("{}{}", color_level_str, pad) + } else { + format!("{}{}", plain_level_str, pad) }; - let fixed_message_width = 44; let message_len = visitor.message.len(); let message_content = if use_color { @@ -383,7 +391,7 @@ fn build_log_text<'a, S>( visitor.message.clone() }; - let padded_message = if message_len < fixed_message_width { + let padded_message = if message_len < FIXED_MESSAGE_WIDTH { let extra_color_len = if use_color { bold_start.len() + bold_end.len() } else { @@ -392,7 +400,7 @@ fn build_log_text<'a, S>( format!( "{:() -> impl Filter( +) -> impl Filter + Copy { + warp::header::optional::(CONTENT_TYPE_HEADER) + .and(warp::body::bytes()) + .and_then(|header: Option, bytes: Bytes| async move { + if let Some(header) = header { + if header == SSZ_CONTENT_TYPE_HEADER { + return Err(reject::unsupported_media_type( + "The request's content-type is not supported".to_string(), + )); + } + } + + // Handle the case when the HTTP request has no body, i.e., without the -d header + if bytes.is_empty() { + return Ok(T::default()); + } + + Json::decode(bytes) + .map_err(|err| reject::custom_deserialize_error(format!("{:?}", err))) + }) +} diff --git a/consensus/context_deserialize/Cargo.toml b/consensus/context_deserialize/Cargo.toml new file mode 100644 index 0000000000..30dae76136 --- /dev/null +++ b/consensus/context_deserialize/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "context_deserialize" +version = "0.1.0" +edition = "2021" + +[dependencies] +milhouse = { workspace = true } +serde = { workspace = true } +ssz_types = { workspace = true } diff --git a/consensus/context_deserialize/src/impls.rs b/consensus/context_deserialize/src/impls.rs new file mode 100644 index 0000000000..803619365f --- /dev/null +++ b/consensus/context_deserialize/src/impls.rs @@ -0,0 +1,103 @@ +use crate::ContextDeserialize; +use serde::de::{Deserialize, DeserializeSeed, Deserializer, SeqAccess, Visitor}; +use std::marker::PhantomData; +use std::sync::Arc; + +impl<'de, C, T> ContextDeserialize<'de, T> for Arc +where + C: ContextDeserialize<'de, T>, +{ + fn context_deserialize(deserializer: D, context: T) -> Result + where + D: Deserializer<'de>, + { + Ok(Arc::new(C::context_deserialize(deserializer, context)?)) + } +} + +impl<'de, T, C> ContextDeserialize<'de, C> for Vec +where + T: ContextDeserialize<'de, C>, + C: Clone, +{ + fn context_deserialize(deserializer: D, context: C) -> Result + where + D: Deserializer<'de>, + { + // Our Visitor, which owns one copy of the context T + struct ContextVisitor { + context: T, + _marker: PhantomData, + } + + impl<'de, C, T> Visitor<'de> for ContextVisitor + where + C: ContextDeserialize<'de, T>, + T: Clone, + { + type Value = Vec; + + fn expecting(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + fmt.write_str("a sequence of context‐deserialized elements") + } + + fn visit_seq(self, mut seq: A) -> Result, A::Error> + where + A: SeqAccess<'de>, + { + let mut out = Vec::with_capacity(seq.size_hint().unwrap_or(0)); + // for each element, we clone the context and hand it to the seed + while let Some(elem) = seq.next_element_seed(ContextSeed { + context: self.context.clone(), + _marker: PhantomData, + })? { + out.push(elem); + } + Ok(out) + } + } + + // A little seed that hands the deserializer + context into C::context_deserialize + struct ContextSeed { + context: C, + _marker: PhantomData, + } + + impl<'de, T, C> DeserializeSeed<'de> for ContextSeed + where + T: ContextDeserialize<'de, C>, + C: Clone, + { + type Value = T; + + fn deserialize(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + T::context_deserialize(deserializer, self.context) + } + } + + deserializer.deserialize_seq(ContextVisitor { + context, + _marker: PhantomData, + }) + } +} + +macro_rules! trivial_deserialize { + ($($t:ty),* $(,)?) => { + $( + impl<'de, T> ContextDeserialize<'de, T> for $t { + fn context_deserialize(deserializer: D, _context: T) -> Result + where + D: Deserializer<'de>, + { + <$t>::deserialize(deserializer) + } + } + )* + }; +} + +trivial_deserialize!(bool, u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64); diff --git a/consensus/context_deserialize/src/lib.rs b/consensus/context_deserialize/src/lib.rs new file mode 100644 index 0000000000..9de819247b --- /dev/null +++ b/consensus/context_deserialize/src/lib.rs @@ -0,0 +1,13 @@ +pub mod impls; +pub mod milhouse; +pub mod ssz_impls; + +extern crate serde; +use serde::de::Deserializer; + +/// General-purpose deserialization trait that accepts extra context `C`. +pub trait ContextDeserialize<'de, C>: Sized { + fn context_deserialize(deserializer: D, context: C) -> Result + where + D: Deserializer<'de>; +} diff --git a/consensus/context_deserialize/src/milhouse.rs b/consensus/context_deserialize/src/milhouse.rs new file mode 100644 index 0000000000..3b86f067a3 --- /dev/null +++ b/consensus/context_deserialize/src/milhouse.rs @@ -0,0 +1,45 @@ +use crate::ContextDeserialize; +use milhouse::{List, Value, Vector}; +use serde::de::Deserializer; +use ssz_types::typenum::Unsigned; + +impl<'de, C, T, N> ContextDeserialize<'de, C> for List +where + T: ContextDeserialize<'de, C> + Value, + N: Unsigned, + C: Clone, +{ + fn context_deserialize(deserializer: D, context: C) -> Result + where + D: Deserializer<'de>, + { + // First deserialize as a Vec. + // This is not the most efficient implementation as it allocates a temporary Vec. In future + // we could write a more performant implementation using `List::builder()`. + let vec = Vec::::context_deserialize(deserializer, context)?; + + // Then convert to List, which will check the length. + List::new(vec) + .map_err(|e| serde::de::Error::custom(format!("Failed to create List: {:?}", e))) + } +} + +impl<'de, C, T, N> ContextDeserialize<'de, C> for Vector +where + T: ContextDeserialize<'de, C> + Value, + N: Unsigned, + C: Clone, +{ + fn context_deserialize(deserializer: D, context: C) -> Result + where + D: Deserializer<'de>, + { + // First deserialize as a List + let list = List::::context_deserialize(deserializer, context)?; + + // Then convert to Vector, which will check the length + Vector::try_from(list).map_err(|e| { + serde::de::Error::custom(format!("Failed to convert List to Vector: {:?}", e)) + }) + } +} diff --git a/consensus/context_deserialize/src/ssz_impls.rs b/consensus/context_deserialize/src/ssz_impls.rs new file mode 100644 index 0000000000..e989d67b29 --- /dev/null +++ b/consensus/context_deserialize/src/ssz_impls.rs @@ -0,0 +1,48 @@ +use crate::serde::de::Error; +use crate::ContextDeserialize; +use serde::de::Deserializer; +use serde::Deserialize; +use ssz_types::length::{Fixed, Variable}; +use ssz_types::typenum::Unsigned; +use ssz_types::{Bitfield, FixedVector}; + +impl<'de, C, T, N> ContextDeserialize<'de, C> for FixedVector +where + T: ContextDeserialize<'de, C>, + N: Unsigned, + C: Clone, +{ + fn context_deserialize(deserializer: D, context: C) -> Result + where + D: Deserializer<'de>, + { + let vec = Vec::::context_deserialize(deserializer, context)?; + FixedVector::new(vec).map_err(|e| D::Error::custom(format!("{:?}", e))) + } +} + +impl<'de, C, N> ContextDeserialize<'de, C> for Bitfield> +where + N: Unsigned + Clone, +{ + fn context_deserialize(deserializer: D, _context: C) -> Result + where + D: Deserializer<'de>, + { + Bitfield::>::deserialize(deserializer) + .map_err(|e| D::Error::custom(format!("{:?}", e))) + } +} + +impl<'de, C, N> ContextDeserialize<'de, C> for Bitfield> +where + N: Unsigned + Clone, +{ + fn context_deserialize(deserializer: D, _context: C) -> Result + where + D: Deserializer<'de>, + { + Bitfield::>::deserialize(deserializer) + .map_err(|e| D::Error::custom(format!("{:?}", e))) + } +} diff --git a/consensus/context_deserialize_derive/Cargo.toml b/consensus/context_deserialize_derive/Cargo.toml new file mode 100644 index 0000000000..eedae30cdf --- /dev/null +++ b/consensus/context_deserialize_derive/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "context_deserialize_derive" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +quote = { workspace = true } +syn = { workspace = true } + +[dev-dependencies] +context_deserialize = { path = "../context_deserialize" } +serde = { workspace = true } +serde_json = "1.0" diff --git a/consensus/context_deserialize_derive/src/lib.rs b/consensus/context_deserialize_derive/src/lib.rs new file mode 100644 index 0000000000..0b73a43b0a --- /dev/null +++ b/consensus/context_deserialize_derive/src/lib.rs @@ -0,0 +1,118 @@ +extern crate proc_macro; +extern crate quote; +extern crate syn; + +use proc_macro::TokenStream; +use quote::quote; +use syn::{ + parse_macro_input, AttributeArgs, DeriveInput, GenericParam, LifetimeDef, Meta, NestedMeta, + WhereClause, +}; + +#[proc_macro_attribute] +pub fn context_deserialize(attr: TokenStream, item: TokenStream) -> TokenStream { + let args = parse_macro_input!(attr as AttributeArgs); + let input = parse_macro_input!(item as DeriveInput); + let ident = &input.ident; + + let mut ctx_types = Vec::new(); + let mut explicit_where: Option = None; + + for meta in args { + match meta { + NestedMeta::Meta(Meta::Path(p)) => { + ctx_types.push(p); + } + NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("bound") => { + if let syn::Lit::Str(lit_str) = &nv.lit { + let where_string = format!("where {}", lit_str.value()); + match syn::parse_str::(&where_string) { + Ok(where_clause) => { + explicit_where = Some(where_clause); + } + Err(err) => { + return syn::Error::new_spanned( + lit_str, + format!("Invalid where clause '{}': {}", lit_str.value(), err), + ) + .to_compile_error() + .into(); + } + } + } else { + return syn::Error::new_spanned( + &nv, + "Expected a string literal for `bound` value", + ) + .to_compile_error() + .into(); + } + } + _ => { + return syn::Error::new_spanned( + &meta, + "Expected paths or `bound = \"...\"` in #[context_deserialize(...)]", + ) + .to_compile_error() + .into(); + } + } + } + + if ctx_types.is_empty() { + return quote! { + compile_error!("Usage: #[context_deserialize(Type1, Type2, ..., bound = \"...\")]"); + } + .into(); + } + + let original_generics = input.generics.clone(); + + // Clone and clean generics for impl use (remove default params) + let mut impl_generics = input.generics.clone(); + for param in impl_generics.params.iter_mut() { + if let GenericParam::Type(ty) = param { + ty.eq_token = None; + ty.default = None; + } + } + + // Ensure 'de lifetime exists in impl generics + let has_de = impl_generics + .lifetimes() + .any(|LifetimeDef { lifetime, .. }| lifetime.ident == "de"); + + if !has_de { + impl_generics.params.insert(0, syn::parse_quote! { 'de }); + } + + let (_, ty_generics, _) = original_generics.split_for_impl(); + let (impl_gens, _, _) = impl_generics.split_for_impl(); + + // Generate: no `'de` applied to the type name + let mut impls = quote! {}; + for ctx in ctx_types { + impls.extend(quote! { + impl #impl_gens context_deserialize::ContextDeserialize<'de, #ctx> + for #ident #ty_generics + #explicit_where + { + fn context_deserialize( + deserializer: D, + _context: #ctx, + ) -> Result + where + D: serde::de::Deserializer<'de>, + { + ::deserialize(deserializer) + } + } + }); + } + + quote! { + #input + #impls + } + .into() +} diff --git a/consensus/context_deserialize_derive/tests/context_deserialize_derive.rs b/consensus/context_deserialize_derive/tests/context_deserialize_derive.rs new file mode 100644 index 0000000000..d6883400e0 --- /dev/null +++ b/consensus/context_deserialize_derive/tests/context_deserialize_derive.rs @@ -0,0 +1,94 @@ +use context_deserialize::ContextDeserialize; +use context_deserialize_derive::context_deserialize; +use serde::{Deserialize, Serialize}; + +#[test] +fn test_context_deserialize_derive() { + type TestContext = (); + + #[context_deserialize(TestContext)] + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct Test { + field: String, + } + + let test = Test { + field: "test".to_string(), + }; + let serialized = serde_json::to_string(&test).unwrap(); + let deserialized = + Test::context_deserialize(&mut serde_json::Deserializer::from_str(&serialized), ()) + .unwrap(); + assert_eq!(test, deserialized); +} + +#[test] +fn test_context_deserialize_derive_multiple_types() { + #[allow(dead_code)] + struct TestContext1(u64); + #[allow(dead_code)] + struct TestContext2(String); + + // This will derive: + // - ContextDeserialize for Test + // - ContextDeserialize for Test + // by just leveraging the Deserialize impl + #[context_deserialize(TestContext1, TestContext2)] + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct Test { + field: String, + } + + let test = Test { + field: "test".to_string(), + }; + let serialized = serde_json::to_string(&test).unwrap(); + let deserialized = Test::context_deserialize( + &mut serde_json::Deserializer::from_str(&serialized), + TestContext1(1), + ) + .unwrap(); + assert_eq!(test, deserialized); + + let deserialized = Test::context_deserialize( + &mut serde_json::Deserializer::from_str(&serialized), + TestContext2("2".to_string()), + ) + .unwrap(); + + assert_eq!(test, deserialized); +} + +#[test] +fn test_context_deserialize_derive_bound() { + use std::fmt::Debug; + + struct TestContext; + + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct Inner { + value: u64, + } + + #[context_deserialize( + TestContext, + bound = "T: Serialize + for<'a> Deserialize<'a> + Debug + PartialEq" + )] + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct Wrapper { + inner: T, + } + + let val = Wrapper { + inner: Inner { value: 42 }, + }; + + let serialized = serde_json::to_string(&val).unwrap(); + let deserialized = Wrapper::::context_deserialize( + &mut serde_json::Deserializer::from_str(&serialized), + TestContext, + ) + .unwrap(); + + assert_eq!(val, deserialized); +} diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index ec08006a8e..0536e20f7a 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -1075,6 +1075,21 @@ impl ProtoArray { }) .map(|node| node.root) } + + /// Returns all nodes that have zero children and are descended from the finalized checkpoint. + /// + /// For informational purposes like the beacon HTTP API, we use this as the list of known heads, + /// even though some of them might not be viable. We do this to maintain consistency between the + /// definition of "head" used by pruning (which does not consider viability) and fork choice. + pub fn heads_descended_from_finalization(&self) -> Vec<&ProtoNode> { + self.nodes + .iter() + .filter(|node| { + node.best_child.is_none() + && self.is_finalized_checkpoint_or_descendant::(node.root) + }) + .collect() + } } /// A helper method to calculate the proposer boost based on the given `justified_balances`. diff --git a/consensus/proto_array/src/proto_array_fork_choice.rs b/consensus/proto_array/src/proto_array_fork_choice.rs index f8a2ae9242..d3be3f967e 100644 --- a/consensus/proto_array/src/proto_array_fork_choice.rs +++ b/consensus/proto_array/src/proto_array_fork_choice.rs @@ -858,10 +858,18 @@ impl ProtoArrayForkChoice { } /// See `ProtoArray::iter_nodes` - pub fn iter_nodes<'a>(&'a self, block_root: &Hash256) -> Iter<'a> { + pub fn iter_nodes(&self, block_root: &Hash256) -> Iter { self.proto_array.iter_nodes(block_root) } + /// See `ProtoArray::iter_block_roots` + pub fn iter_block_roots( + &self, + block_root: &Hash256, + ) -> impl Iterator + use<'_> { + self.proto_array.iter_block_roots(block_root) + } + pub fn as_bytes(&self) -> Vec { SszContainer::from(self).as_ssz_bytes() } @@ -887,6 +895,11 @@ impl ProtoArrayForkChoice { pub fn core_proto_array_mut(&mut self) -> &mut ProtoArray { &mut self.proto_array } + + /// Returns all nodes that have zero children and are descended from the finalized checkpoint. + pub fn heads_descended_from_finalization(&self) -> Vec<&ProtoNode> { + self.proto_array.heads_descended_from_finalization::() + } } /// Returns a list of `deltas`, where there is one delta for each of the indices in diff --git a/consensus/state_processing/src/genesis.rs b/consensus/state_processing/src/genesis.rs index 40e3995ecd..ebeb798b7d 100644 --- a/consensus/state_processing/src/genesis.rs +++ b/consensus/state_processing/src/genesis.rs @@ -124,8 +124,7 @@ pub fn initialize_beacon_state_from_eth1( // Remove intermediate Deneb fork from `state.fork`. state.fork_mut().previous_version = spec.electra_fork_version; - // TODO(electra): think about this more and determine the best way to - // do this. The spec tests will expect that the sync committees are + // The spec tests will expect that the sync committees are // calculated using the electra value for MAX_EFFECTIVE_BALANCE when // calling `initialize_beacon_state_from_eth1()`. But the sync committees // are actually calcuated back in `upgrade_to_altair()`. We need to diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index 79b92972a0..57cb412040 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -523,7 +523,7 @@ pub fn get_expected_withdrawals( let epoch = state.current_epoch(); let mut withdrawal_index = state.next_withdrawal_index()?; let mut validator_index = state.next_withdrawal_validator_index()?; - let mut withdrawals = vec![]; + let mut withdrawals = Vec::::with_capacity(E::max_withdrawals_per_payload()); let fork_name = state.fork_name_unchecked(); // [New in Electra:EIP7251] @@ -538,19 +538,27 @@ pub fn get_expected_withdrawals( break; } - let withdrawal_balance = state.get_balance(withdrawal.validator_index as usize)?; let validator = state.get_validator(withdrawal.validator_index as usize)?; let has_sufficient_effective_balance = validator.effective_balance >= spec.min_activation_balance; - let has_excess_balance = withdrawal_balance > spec.min_activation_balance; + let total_withdrawn = withdrawals + .iter() + .filter_map(|w| { + (w.validator_index == withdrawal.validator_index).then_some(w.amount) + }) + .safe_sum()?; + let balance = state + .get_balance(withdrawal.validator_index as usize)? + .safe_sub(total_withdrawn)?; + let has_excess_balance = balance > spec.min_activation_balance; if validator.exit_epoch == spec.far_future_epoch && has_sufficient_effective_balance && has_excess_balance { let withdrawable_balance = std::cmp::min( - withdrawal_balance.safe_sub(spec.min_activation_balance)?, + balance.safe_sub(spec.min_activation_balance)?, withdrawal.amount, ); withdrawals.push(Withdrawal { 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 0b399bea6c..6b4a394c73 100644 --- a/consensus/state_processing/src/per_block_processing/verify_attestation.rs +++ b/consensus/state_processing/src/per_block_processing/verify_attestation.rs @@ -63,7 +63,7 @@ pub fn verify_attestation_for_state<'ctxt, E: EthSpec>( ) -> Result> { let data = attestation.data(); - // TODO(electra) choosing a validation based on the attestation's fork + // NOTE: choosing a validation based on the attestation's fork // rather than the state's fork makes this simple, but technically the spec // defines this verification based on the state's fork. match attestation { diff --git a/consensus/state_processing/src/per_epoch_processing/altair.rs b/consensus/state_processing/src/per_epoch_processing/altair.rs index 5fcd147b2e..dc4dbe7cbc 100644 --- a/consensus/state_processing/src/per_epoch_processing/altair.rs +++ b/consensus/state_processing/src/per_epoch_processing/altair.rs @@ -84,7 +84,7 @@ pub fn process_epoch( Ok(EpochProcessingSummary::Altair { progressive_balances: current_epoch_progressive_balances, current_epoch_total_active_balance, - participation: participation_summary, + participation: participation_summary.into(), sync_committee, }) } 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 5508b80807..b2228a5a1d 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 @@ -17,7 +17,7 @@ pub enum EpochProcessingSummary { Altair { progressive_balances: ProgressiveBalancesCache, current_epoch_total_active_balance: u64, - participation: ParticipationEpochSummary, + participation: Box>, sync_committee: Arc>, }, } diff --git a/consensus/state_processing/src/upgrade/eip7805.rs b/consensus/state_processing/src/upgrade/eip7805.rs index 1a675ad343..164cca3930 100644 --- a/consensus/state_processing/src/upgrade/eip7805.rs +++ b/consensus/state_processing/src/upgrade/eip7805.rs @@ -7,7 +7,7 @@ use types::{ EthSpec, Fork, PendingDeposit, }; -/// Transform a `Deneb` state into an `Electra` state. +/// Transform a `Electra` state into an `Eip7805s` state. pub fn upgrade_to_eip7805( pre_state: &mut BeaconState, spec: &ChainSpec, diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index 013230f158..b58d4ef96f 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -17,6 +17,8 @@ arbitrary = { workspace = true, features = ["derive"] } bls = { workspace = true, features = ["arbitrary"] } compare_fields = { workspace = true } compare_fields_derive = { workspace = true } +context_deserialize = { workspace = true } +context_deserialize_derive = { workspace = true } derivative = { workspace = true } eth2_interop_keypairs = { path = "../../common/eth2_interop_keypairs" } ethereum_hashing = { workspace = true } diff --git a/consensus/types/presets/mainnet/electra.yaml b/consensus/types/presets/mainnet/electra.yaml index 42afbb233e..55308d5b1c 100644 --- a/consensus/types/presets/mainnet/electra.yaml +++ b/consensus/types/presets/mainnet/electra.yaml @@ -7,44 +7,44 @@ MIN_ACTIVATION_BALANCE: 32000000000 # 2**11 * 10**9 (= 2,048,000,000,000) Gwei MAX_EFFECTIVE_BALANCE_ELECTRA: 2048000000000 -# State list lengths +# Rewards and penalties # --------------------------------------------------------------- -# `uint64(2**27)` (= 134,217,728) -PENDING_DEPOSITS_LIMIT: 134217728 -# `uint64(2**27)` (= 134,217,728) -PENDING_PARTIAL_WITHDRAWALS_LIMIT: 134217728 -# `uint64(2**18)` (= 262,144) -PENDING_CONSOLIDATIONS_LIMIT: 262144 - -# Reward and penalty quotients -# --------------------------------------------------------------- -# `uint64(2**12)` (= 4,096) +# 2**12 (= 4,096) MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA: 4096 -# `uint64(2**12)` (= 4,096) +# 2**12 (= 4,096) WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA: 4096 -# # Max operations per block +# State list lengths # --------------------------------------------------------------- -# `uint64(2**0)` (= 1) +# 2**27 (= 134,217,728) pending deposits +PENDING_DEPOSITS_LIMIT: 134217728 +# 2**27 (= 134,217,728) pending partial withdrawals +PENDING_PARTIAL_WITHDRAWALS_LIMIT: 134217728 +# 2**18 (= 262,144) pending consolidations +PENDING_CONSOLIDATIONS_LIMIT: 262144 + +# Max operations per block +# --------------------------------------------------------------- +# 2**0 (= 1) attester slashings MAX_ATTESTER_SLASHINGS_ELECTRA: 1 -# `uint64(2**3)` (= 8) +# 2**3 (= 8) attestations MAX_ATTESTATIONS_ELECTRA: 8 -# `uint64(2**1)` (= 2) -MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: 2 # Execution # --------------------------------------------------------------- -# 2**13 (= 8192) deposit requests +# 2**13 (= 8,192) deposit requests MAX_DEPOSIT_REQUESTS_PER_PAYLOAD: 8192 # 2**4 (= 16) withdrawal requests MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: 16 +# 2**1 (= 2) consolidation requests +MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: 2 # Withdrawals processing # --------------------------------------------------------------- -# 2**3 ( = 8) pending withdrawals +# 2**3 (= 8) pending withdrawals MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: 8 # Pending deposits processing # --------------------------------------------------------------- -# 2**4 ( = 4) pending deposits +# 2**4 (= 16) pending deposits MAX_PENDING_DEPOSITS_PER_EPOCH: 16 diff --git a/consensus/types/presets/minimal/electra.yaml b/consensus/types/presets/minimal/electra.yaml index 44e4769756..f99effe0f1 100644 --- a/consensus/types/presets/minimal/electra.yaml +++ b/consensus/types/presets/minimal/electra.yaml @@ -7,44 +7,44 @@ MIN_ACTIVATION_BALANCE: 32000000000 # 2**11 * 10**9 (= 2,048,000,000,000) Gwei MAX_EFFECTIVE_BALANCE_ELECTRA: 2048000000000 -# State list lengths +# Rewards and penalties # --------------------------------------------------------------- -# `uint64(2**27)` (= 134,217,728) -PENDING_DEPOSITS_LIMIT: 134217728 -# [customized] `uint64(2**6)` (= 64) -PENDING_PARTIAL_WITHDRAWALS_LIMIT: 64 -# [customized] `uint64(2**6)` (= 64) -PENDING_CONSOLIDATIONS_LIMIT: 64 - -# Reward and penalty quotients -# --------------------------------------------------------------- -# `uint64(2**12)` (= 4,096) +# 2**12 (= 4,096) MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA: 4096 -# `uint64(2**12)` (= 4,096) +# 2**12 (= 4,096) WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA: 4096 -# # Max operations per block +# State list lengths # --------------------------------------------------------------- -# `uint64(2**0)` (= 1) +# 2**27 (= 134,217,728) pending deposits +PENDING_DEPOSITS_LIMIT: 134217728 +# [customized] 2**6 (= 64) pending partial withdrawals +PENDING_PARTIAL_WITHDRAWALS_LIMIT: 64 +# [customized] 2**6 (= 64) pending consolidations +PENDING_CONSOLIDATIONS_LIMIT: 64 + +# Max operations per block +# --------------------------------------------------------------- +# 2**0 (= 1) attester slashings MAX_ATTESTER_SLASHINGS_ELECTRA: 1 -# `uint64(2**3)` (= 8) +# 2**3 (= 8) attestations MAX_ATTESTATIONS_ELECTRA: 8 -# `uint64(2**1)` (= 2) -MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: 2 # Execution # --------------------------------------------------------------- -# [customized] +# [customized] 2**2 (= 4) deposit requests MAX_DEPOSIT_REQUESTS_PER_PAYLOAD: 4 # [customized] 2**1 (= 2) withdrawal requests MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: 2 +# 2**1 (= 2) consolidation requests +MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: 2 # Withdrawals processing # --------------------------------------------------------------- -# 2**1 ( = 2) pending withdrawals +# 2**1 (= 2) pending withdrawals MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: 2 # Pending deposits processing # --------------------------------------------------------------- -# 2**4 ( = 4) pending deposits +# 2**4 (= 16) pending deposits MAX_PENDING_DEPOSITS_PER_EPOCH: 16 diff --git a/consensus/types/src/aggregate_and_proof.rs b/consensus/types/src/aggregate_and_proof.rs index 6edd8d3892..a280afeaae 100644 --- a/consensus/types/src/aggregate_and_proof.rs +++ b/consensus/types/src/aggregate_and_proof.rs @@ -1,8 +1,9 @@ use super::{AttestationBase, AttestationElectra, AttestationRef}; use super::{ - ChainSpec, Domain, EthSpec, Fork, Hash256, PublicKey, SecretKey, SelectionProof, Signature, - SignedRoot, + ChainSpec, Domain, EthSpec, Fork, ForkName, Hash256, PublicKey, SecretKey, SelectionProof, + Signature, SignedRoot, }; +use crate::context_deserialize; use crate::test_utils::TestRandom; use crate::Attestation; use serde::{Deserialize, Serialize}; @@ -26,6 +27,7 @@ use tree_hash_derive::TreeHash; TestRandom, TreeHash, ), + context_deserialize(ForkName), serde(bound = "E: EthSpec"), arbitrary(bound = "E: EthSpec"), ), diff --git a/consensus/types/src/attestation.rs b/consensus/types/src/attestation.rs index 5d147f1e86..286e4622f8 100644 --- a/consensus/types/src/attestation.rs +++ b/consensus/types/src/attestation.rs @@ -1,10 +1,12 @@ +use crate::context_deserialize; use crate::slot_data::SlotData; use crate::{test_utils::TestRandom, Hash256, Slot}; -use crate::{Checkpoint, ForkVersionDeserialize}; +use crate::{Checkpoint, ContextDeserialize, ForkName}; use derivative::Derivative; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use ssz_derive::{Decode, Encode}; use ssz_types::BitVector; +use std::collections::HashSet; use std::hash::{Hash, Hasher}; use superstruct::superstruct; use test_random_derive::TestRandom; @@ -15,7 +17,7 @@ use super::{ Signature, SignedRoot, }; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub enum Error { SszTypesError(ssz_types::Error), BitfieldError(ssz::BitfieldError), @@ -46,6 +48,7 @@ impl From for Error { arbitrary::Arbitrary, TreeHash, ), + context_deserialize(ForkName), derivative(PartialEq, Hash(bound = "E: EthSpec")), serde(bound = "E: EthSpec", deny_unknown_fields), arbitrary(bound = "E: EthSpec"), @@ -210,6 +213,13 @@ impl Attestation { } } + pub fn get_committee_indices_map(&self) -> HashSet { + match self { + Attestation::Base(att) => HashSet::from([att.data.index]), + Attestation::Electra(att) => att.get_committee_indices().into_iter().collect(), + } + } + pub fn is_aggregation_bits_zero(&self) -> bool { match self { Attestation::Base(att) => att.aggregation_bits.is_zero(), @@ -293,7 +303,11 @@ impl AttestationRef<'_, E> { impl AttestationElectra { pub fn committee_index(&self) -> Option { - self.get_committee_indices().first().cloned() + self.committee_bits + .iter() + .enumerate() + .find(|&(_, bit)| bit) + .map(|(index, _)| index as u64) } pub fn get_aggregation_bits(&self) -> Vec { @@ -520,45 +534,44 @@ impl<'a, E: EthSpec> From> for AttestationRef<'a, E> } } -impl ForkVersionDeserialize for Attestation { - fn deserialize_by_fork<'de, D: serde::Deserializer<'de>>( - value: serde_json::Value, - fork_name: crate::ForkName, - ) -> Result { - if fork_name.electra_enabled() { - let attestation: AttestationElectra = - serde_json::from_value(value).map_err(serde::de::Error::custom)?; - Ok(Attestation::Electra(attestation)) +impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for Attestation { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { + if context.electra_enabled() { + AttestationElectra::::deserialize(deserializer) + .map_err(serde::de::Error::custom) + .map(Attestation::Electra) } else { - let attestation: AttestationBase = - serde_json::from_value(value).map_err(serde::de::Error::custom)?; - Ok(Attestation::Base(attestation)) + AttestationBase::::deserialize(deserializer) + .map_err(serde::de::Error::custom) + .map(Attestation::Base) } } } -impl ForkVersionDeserialize for Vec> { - fn deserialize_by_fork<'de, D: serde::Deserializer<'de>>( - value: serde_json::Value, - fork_name: crate::ForkName, - ) -> Result { - if fork_name.electra_enabled() { - let attestations: Vec> = - serde_json::from_value(value).map_err(serde::de::Error::custom)?; - Ok(attestations - .into_iter() - .map(Attestation::Electra) - .collect::>()) +/* +impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for Vec> { + fn context_deserialize( + deserializer: D, + context: ForkName, + ) -> Result + where + D: Deserializer<'de>, + { + if context.electra_enabled() { + >>::deserialize(deserializer) + .map_err(serde::de::Error::custom) + .map(|vec| vec.into_iter().map(Attestation::Electra).collect::>()) } else { - let attestations: Vec> = - serde_json::from_value(value).map_err(serde::de::Error::custom)?; - Ok(attestations - .into_iter() - .map(Attestation::Base) - .collect::>()) + >>::deserialize(deserializer) + .map_err(serde::de::Error::custom) + .map(|vec| vec.into_iter().map(Attestation::Base).collect::>()) } } } +*/ #[derive( Debug, @@ -573,6 +586,7 @@ impl ForkVersionDeserialize for Vec> { TreeHash, PartialEq, )] +#[context_deserialize(ForkName)] pub struct SingleAttestation { #[serde(with = "serde_utils::quoted_u64")] pub committee_index: u64, diff --git a/consensus/types/src/attestation_data.rs b/consensus/types/src/attestation_data.rs index 7578981f51..d0d4dcc553 100644 --- a/consensus/types/src/attestation_data.rs +++ b/consensus/types/src/attestation_data.rs @@ -1,12 +1,11 @@ -use crate::test_utils::TestRandom; -use crate::{Checkpoint, Hash256, SignedRoot, Slot}; - use crate::slot_data::SlotData; +use crate::test_utils::TestRandom; +use crate::{Checkpoint, ForkName, Hash256, SignedRoot, Slot}; +use context_deserialize_derive::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; - /// The data upon which an attestation is based. /// /// Spec v0.12.1 @@ -25,6 +24,7 @@ use tree_hash_derive::TreeHash; TestRandom, Default, )] +#[context_deserialize(ForkName)] pub struct AttestationData { pub slot: Slot, #[serde(with = "serde_utils::quoted_u64")] diff --git a/consensus/types/src/attester_slashing.rs b/consensus/types/src/attester_slashing.rs index f6aa654d44..8fb5862f21 100644 --- a/consensus/types/src/attester_slashing.rs +++ b/consensus/types/src/attester_slashing.rs @@ -1,10 +1,12 @@ +use crate::context_deserialize; use crate::indexed_attestation::{ IndexedAttestationBase, IndexedAttestationElectra, IndexedAttestationRef, }; use crate::{test_utils::TestRandom, EthSpec}; +use crate::{ContextDeserialize, ForkName}; use derivative::Derivative; use rand::{Rng, RngCore}; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use ssz_derive::{Decode, Encode}; use superstruct::superstruct; use test_random_derive::TestRandom; @@ -25,6 +27,7 @@ use tree_hash_derive::TreeHash; TestRandom, arbitrary::Arbitrary ), + context_deserialize(ForkName), derivative(PartialEq, Eq, Hash(bound = "E: EthSpec")), serde(bound = "E: EthSpec"), arbitrary(bound = "E: EthSpec") @@ -171,25 +174,27 @@ impl TestRandom for AttesterSlashing { } } -impl crate::ForkVersionDeserialize for Vec> { - fn deserialize_by_fork<'de, D: serde::Deserializer<'de>>( - value: serde_json::Value, - fork_name: crate::ForkName, - ) -> Result { - if fork_name.electra_enabled() { - let slashings: Vec> = - serde_json::from_value(value).map_err(serde::de::Error::custom)?; - Ok(slashings - .into_iter() - .map(AttesterSlashing::Electra) - .collect::>()) +impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for Vec> { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { + if context.electra_enabled() { + >>::deserialize(deserializer) + .map_err(serde::de::Error::custom) + .map(|vec| { + vec.into_iter() + .map(AttesterSlashing::Electra) + .collect::>() + }) } else { - let slashings: Vec> = - serde_json::from_value(value).map_err(serde::de::Error::custom)?; - Ok(slashings - .into_iter() - .map(AttesterSlashing::Base) - .collect::>()) + >>::deserialize(deserializer) + .map_err(serde::de::Error::custom) + .map(|vec| { + vec.into_iter() + .map(AttesterSlashing::Base) + .collect::>() + }) } } } diff --git a/consensus/types/src/beacon_block.rs b/consensus/types/src/beacon_block.rs index 0c215ed1c9..545f395d19 100644 --- a/consensus/types/src/beacon_block.rs +++ b/consensus/types/src/beacon_block.rs @@ -2,7 +2,7 @@ use crate::attestation::AttestationBase; use crate::test_utils::TestRandom; use crate::*; use derivative::Derivative; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use ssz::{Decode, DecodeError}; use ssz_derive::{Decode, Encode}; use std::fmt; @@ -802,23 +802,21 @@ impl From>> } } -impl> ForkVersionDeserialize +impl<'de, E: EthSpec, Payload: AbstractExecPayload> ContextDeserialize<'de, ForkName> for BeaconBlock { - fn deserialize_by_fork<'de, D: serde::Deserializer<'de>>( - value: serde_json::value::Value, - fork_name: ForkName, - ) -> Result { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { Ok(map_fork_name!( - fork_name, + context, Self, - serde_json::from_value(value).map_err(|e| serde::de::Error::custom(format!( - "BeaconBlock failed to deserialize: {:?}", - e - )))? + serde::Deserialize::deserialize(deserializer)? )) } } + pub enum BlockImportSource { Gossip, Lookup, diff --git a/consensus/types/src/beacon_block_body.rs b/consensus/types/src/beacon_block_body.rs index 6bb8b4bd7d..bf4b28e972 100644 --- a/consensus/types/src/beacon_block_body.rs +++ b/consensus/types/src/beacon_block_body.rs @@ -3,7 +3,7 @@ use crate::*; use derivative::Derivative; use merkle_proof::{MerkleTree, MerkleTreeError}; use metastruct::metastruct; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use ssz_derive::{Decode, Encode}; use std::marker::PhantomData; use superstruct::superstruct; @@ -48,6 +48,7 @@ pub const BLOB_KZG_COMMITMENTS_INDEX: usize = 11; deny_unknown_fields ), arbitrary(bound = "E: EthSpec, Payload: AbstractExecPayload"), + context_deserialize(ForkName), ), specific_variant_attributes( Base(metastruct(mappings(beacon_block_body_base_fields(groups(fields))))), @@ -62,10 +63,11 @@ pub const BLOB_KZG_COMMITMENTS_INDEX: usize = 11; cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"), partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant") )] -#[derive(Debug, Clone, Serialize, Deserialize, Derivative, arbitrary::Arbitrary)] +#[derive(Debug, Clone, Serialize, Deserialize, Derivative, TreeHash, arbitrary::Arbitrary)] #[derivative(PartialEq, Hash(bound = "E: EthSpec"))] #[serde(untagged)] #[serde(bound = "E: EthSpec, Payload: AbstractExecPayload")] +#[tree_hash(enum_behaviour = "transparent")] #[arbitrary(bound = "E: EthSpec, Payload: AbstractExecPayload")] pub struct BeaconBlockBody = FullPayload> { pub randao_reveal: Signature, @@ -1088,6 +1090,21 @@ impl From>> } } +impl<'de, E: EthSpec, Payload: AbstractExecPayload> ContextDeserialize<'de, ForkName> + for BeaconBlockBody +{ + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { + Ok(map_fork_name!( + context, + Self, + serde::Deserialize::deserialize(deserializer)? + )) + } +} + /// 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_block_header.rs b/consensus/types/src/beacon_block_header.rs index b382359313..8416f975db 100644 --- a/consensus/types/src/beacon_block_header.rs +++ b/consensus/types/src/beacon_block_header.rs @@ -1,6 +1,7 @@ use crate::test_utils::TestRandom; use crate::*; +use context_deserialize_derive::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; @@ -24,6 +25,7 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] +#[context_deserialize(ForkName)] pub struct BeaconBlockHeader { pub slot: Slot, #[serde(with = "serde_utils::quoted_u64")] diff --git a/consensus/types/src/beacon_response.rs b/consensus/types/src/beacon_response.rs new file mode 100644 index 0000000000..2e45854364 --- /dev/null +++ b/consensus/types/src/beacon_response.rs @@ -0,0 +1,239 @@ +use crate::{ContextDeserialize, ForkName}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::value::Value; + +pub trait ForkVersionDecode: Sized { + /// SSZ decode with explicit fork variant. + fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result; +} + +/// The metadata of type M should be set to `EmptyMetadata` if you don't care about adding fields other than +/// version. If you *do* care about adding other fields you can mix in any type that implements +/// `Deserialize`. +#[derive(Debug, PartialEq, Clone, Serialize)] +pub struct ForkVersionedResponse { + pub version: ForkName, + #[serde(flatten)] + pub metadata: M, + pub data: T, +} + +// Used for responses to V1 endpoints that don't have a version field. +/// The metadata of type M should be set to `EmptyMetadata` if you don't care about adding fields other than +/// version. If you *do* care about adding other fields you can mix in any type that implements +/// `Deserialize`. +#[derive(Debug, PartialEq, Clone, Serialize)] +pub struct UnversionedResponse { + pub metadata: M, + pub data: T, +} + +#[derive(Debug, PartialEq, Clone, Serialize)] +#[serde(untagged)] +pub enum BeaconResponse { + ForkVersioned(ForkVersionedResponse), + Unversioned(UnversionedResponse), +} + +impl BeaconResponse { + pub fn version(&self) -> Option { + match self { + BeaconResponse::ForkVersioned(response) => Some(response.version), + BeaconResponse::Unversioned(_) => None, + } + } + + pub fn data(&self) -> &T { + match self { + BeaconResponse::ForkVersioned(response) => &response.data, + BeaconResponse::Unversioned(response) => &response.data, + } + } + + pub fn metadata(&self) -> &M { + match self { + BeaconResponse::ForkVersioned(response) => &response.metadata, + BeaconResponse::Unversioned(response) => &response.metadata, + } + } +} + +/// Metadata type similar to unit (i.e. `()`) but deserializes from a map (`serde_json::Value`). +/// +/// Unfortunately the braces are semantically significant, i.e. `struct EmptyMetadata;` does not +/// work. +#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)] +pub struct EmptyMetadata {} + +/// Fork versioned response with extra information about finalization & optimistic execution. +pub type ExecutionOptimisticFinalizedBeaconResponse = + BeaconResponse; + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct ExecutionOptimisticFinalizedMetadata { + pub execution_optimistic: Option, + pub finalized: Option, +} + +impl<'de, T, M> Deserialize<'de> for ForkVersionedResponse +where + T: ContextDeserialize<'de, ForkName>, + M: DeserializeOwned, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + struct Helper { + version: ForkName, + #[serde(flatten)] + metadata: Value, + data: Value, + } + + let helper = Helper::deserialize(deserializer)?; + + // Deserialize metadata + let metadata = serde_json::from_value(helper.metadata).map_err(serde::de::Error::custom)?; + + // Deserialize `data` using ContextDeserialize + let data = T::context_deserialize(helper.data, helper.version) + .map_err(serde::de::Error::custom)?; + + Ok(ForkVersionedResponse { + version: helper.version, + metadata, + data, + }) + } +} + +impl<'de, T, M> Deserialize<'de> for UnversionedResponse +where + T: DeserializeOwned, + M: DeserializeOwned, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + struct Helper { + #[serde(flatten)] + metadata: M, + data: T, + } + + let helper = Helper::deserialize(deserializer)?; + + Ok(UnversionedResponse { + metadata: helper.metadata, + data: helper.data, + }) + } +} + +impl BeaconResponse { + pub fn map_data(self, f: impl FnOnce(T) -> U) -> BeaconResponse { + match self { + BeaconResponse::ForkVersioned(response) => { + BeaconResponse::ForkVersioned(response.map_data(f)) + } + BeaconResponse::Unversioned(response) => { + BeaconResponse::Unversioned(response.map_data(f)) + } + } + } + + pub fn into_data(self) -> T { + match self { + BeaconResponse::ForkVersioned(response) => response.data, + BeaconResponse::Unversioned(response) => response.data, + } + } +} + +impl UnversionedResponse { + pub fn map_data(self, f: impl FnOnce(T) -> U) -> UnversionedResponse { + let UnversionedResponse { metadata, data } = self; + UnversionedResponse { + metadata, + data: f(data), + } + } +} + +impl ForkVersionedResponse { + /// Apply a function to the inner `data`, potentially changing its type. + pub fn map_data(self, f: impl FnOnce(T) -> U) -> ForkVersionedResponse { + let ForkVersionedResponse { + version, + metadata, + data, + } = self; + ForkVersionedResponse { + version, + metadata, + data: f(data), + } + } +} + +impl From> for BeaconResponse { + fn from(response: ForkVersionedResponse) -> Self { + BeaconResponse::ForkVersioned(response) + } +} + +impl From> for BeaconResponse { + fn from(response: UnversionedResponse) -> Self { + BeaconResponse::Unversioned(response) + } +} + +#[cfg(test)] +mod fork_version_response_tests { + use crate::{ + ExecutionPayload, ExecutionPayloadBellatrix, ForkName, ForkVersionedResponse, + MainnetEthSpec, + }; + use serde_json::json; + + #[test] + fn fork_versioned_response_deserialize_correct_fork() { + type E = MainnetEthSpec; + + let response_json = + serde_json::to_string(&json!(ForkVersionedResponse::> { + version: ForkName::Bellatrix, + metadata: Default::default(), + data: ExecutionPayload::Bellatrix(ExecutionPayloadBellatrix::default()), + })) + .unwrap(); + + let result: Result>, _> = + serde_json::from_str(&response_json); + + assert!(result.is_ok()); + } + + #[test] + fn fork_versioned_response_deserialize_incorrect_fork() { + type E = MainnetEthSpec; + + let response_json = + serde_json::to_string(&json!(ForkVersionedResponse::> { + version: ForkName::Capella, + metadata: Default::default(), + data: ExecutionPayload::Bellatrix(ExecutionPayloadBellatrix::default()), + })) + .unwrap(); + + let result: Result>, _> = + serde_json::from_str(&response_json); + + assert!(result.is_err()); + } +} diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 2bb5f6dfff..1cc804546e 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -1,6 +1,7 @@ use self::committee_cache::get_active_validator_indices; use crate::historical_summary::HistoricalSummary; use crate::test_utils::TestRandom; +use crate::ContextDeserialize; use crate::FixedBytesExtended; use crate::*; use compare_fields::CompareFields; @@ -11,7 +12,7 @@ use int_to_bytes::{int_to_bytes4, int_to_bytes8}; use metastruct::{metastruct, NumFields}; pub use pubkey_cache::PubkeyCache; use safe_arith::{ArithError, SafeArith}; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use ssz::{ssz_encode, Decode, DecodeError, Encode}; use ssz_derive::{Decode, Encode}; use std::hash::Hash; @@ -2896,18 +2897,15 @@ impl CompareFields for BeaconState { } } -impl ForkVersionDeserialize for BeaconState { - fn deserialize_by_fork<'de, D: serde::Deserializer<'de>>( - value: serde_json::value::Value, - fork_name: ForkName, - ) -> Result { +impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for BeaconState { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { Ok(map_fork_name!( - fork_name, + context, Self, - serde_json::from_value(value).map_err(|e| serde::de::Error::custom(format!( - "BeaconState failed to deserialize: {:?}", - e - )))? + serde::Deserialize::deserialize(deserializer)? )) } } diff --git a/consensus/types/src/blob_sidecar.rs b/consensus/types/src/blob_sidecar.rs index ff4555747c..f7a5725c5a 100644 --- a/consensus/types/src/blob_sidecar.rs +++ b/consensus/types/src/blob_sidecar.rs @@ -1,9 +1,10 @@ +use crate::context_deserialize; use crate::test_utils::TestRandom; use crate::{ beacon_block_body::BLOB_KZG_COMMITMENTS_INDEX, AbstractExecPayload, BeaconBlockHeader, - BeaconStateError, Blob, ChainSpec, Epoch, EthSpec, FixedVector, ForkName, - ForkVersionDeserialize, Hash256, KzgProofs, RuntimeFixedVector, RuntimeVariableList, - SignedBeaconBlock, SignedBeaconBlockHeader, Slot, VariableList, + BeaconStateError, Blob, ChainSpec, Epoch, EthSpec, FixedVector, ForkName, Hash256, KzgProofs, + RuntimeFixedVector, RuntimeVariableList, SignedBeaconBlock, SignedBeaconBlockHeader, Slot, + VariableList, }; use bls::Signature; use derivative::Derivative; @@ -25,6 +26,7 @@ use tree_hash_derive::TreeHash; #[derive( Serialize, Deserialize, Encode, Decode, TreeHash, Copy, Clone, Debug, PartialEq, Eq, Hash, )] +#[context_deserialize(ForkName)] pub struct BlobIdentifier { pub block_root: Hash256, pub index: u64, @@ -54,6 +56,7 @@ impl Ord for BlobIdentifier { Derivative, arbitrary::Arbitrary, )] +#[context_deserialize(ForkName)] #[serde(bound = "E: EthSpec")] #[arbitrary(bound = "E: EthSpec")] #[derivative(PartialEq, Eq, Hash(bound = "E: EthSpec"))] @@ -296,12 +299,3 @@ pub type BlobSidecarList = RuntimeVariableList>>; /// Alias for a non length-constrained list of `BlobSidecar`s. pub type FixedBlobSidecarList = RuntimeFixedVector>>>; pub type BlobsList = VariableList, ::MaxBlobCommitmentsPerBlock>; - -impl ForkVersionDeserialize for BlobSidecarList { - fn deserialize_by_fork<'de, D: serde::Deserializer<'de>>( - value: serde_json::value::Value, - _: ForkName, - ) -> Result { - serde_json::from_value::>(value).map_err(serde::de::Error::custom) - } -} diff --git a/consensus/types/src/bls_to_execution_change.rs b/consensus/types/src/bls_to_execution_change.rs index 07d71b360f..b333862220 100644 --- a/consensus/types/src/bls_to_execution_change.rs +++ b/consensus/types/src/bls_to_execution_change.rs @@ -19,6 +19,7 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] +#[context_deserialize(ForkName)] pub struct BlsToExecutionChange { #[serde(with = "serde_utils::quoted_u64")] pub validator_index: u64, diff --git a/consensus/types/src/builder_bid.rs b/consensus/types/src/builder_bid.rs index b20dc0f316..29e07895d4 100644 --- a/consensus/types/src/builder_bid.rs +++ b/consensus/types/src/builder_bid.rs @@ -1,9 +1,10 @@ use crate::beacon_block_body::KzgCommitments; use crate::{ - ChainSpec, EthSpec, ExecutionPayloadHeaderBellatrix, ExecutionPayloadHeaderCapella, - ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderEip7805, ExecutionPayloadHeaderElectra, - ExecutionPayloadHeaderFulu, ExecutionPayloadHeaderRef, ExecutionPayloadHeaderRefMut, - ExecutionRequests, ForkName, ForkVersionDecode, ForkVersionDeserialize, SignedRoot, Uint256, + ChainSpec, ContextDeserialize, EthSpec, ExecutionPayloadHeaderBellatrix, + ExecutionPayloadHeaderCapella, ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderEip7805, + ExecutionPayloadHeaderElectra, ExecutionPayloadHeaderFulu, ExecutionPayloadHeaderRef, + ExecutionPayloadHeaderRefMut, ExecutionRequests, ForkName, ForkVersionDecode, + SignedRoot, Uint256, }; use bls::PublicKeyBytes; use bls::Signature; @@ -11,6 +12,8 @@ use serde::{Deserialize, Deserializer, Serialize}; use ssz::Decode; use ssz_derive::{Decode, Encode}; use superstruct::superstruct; +use crate::test_utils::TestRandom; +use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; #[superstruct( @@ -24,7 +27,8 @@ use tree_hash_derive::TreeHash; Deserialize, TreeHash, Decode, - Clone + Clone, + TestRandom ), serde(bound = "E: EthSpec", deny_unknown_fields) ), @@ -128,47 +132,61 @@ impl ForkVersionDecode for SignedBuilderBid { } } -impl ForkVersionDeserialize for BuilderBid { - fn deserialize_by_fork<'de, D: Deserializer<'de>>( - value: serde_json::value::Value, - fork_name: ForkName, - ) -> Result { +impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for BuilderBid { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { let convert_err = |e| serde::de::Error::custom(format!("BuilderBid failed to deserialize: {:?}", e)); - - Ok(match fork_name { + Ok(match context { ForkName::Bellatrix => { - Self::Bellatrix(serde_json::from_value(value).map_err(convert_err)?) + Self::Bellatrix(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Capella => { + Self::Capella(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Deneb => { + Self::Deneb(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Electra => { + Self::Electra(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Eip7805 => { + Self::Eip7805(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Fulu => { + Self::Fulu(Deserialize::deserialize(deserializer).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::Eip7805 => Self::Eip7805(serde_json::from_value(value).map_err(convert_err)?), - ForkName::Fulu => Self::Fulu(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 '{}'", - fork_name + context ))); } }) } } -impl ForkVersionDeserialize for SignedBuilderBid { - fn deserialize_by_fork<'de, D: Deserializer<'de>>( - value: serde_json::value::Value, - fork_name: ForkName, - ) -> Result { +impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for SignedBuilderBid { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { #[derive(Deserialize)] struct Helper { - pub message: serde_json::Value, - pub signature: Signature, + message: serde_json::Value, + signature: Signature, } - let helper: Helper = serde_json::from_value(value).map_err(serde::de::Error::custom)?; - Ok(Self { - message: BuilderBid::deserialize_by_fork::<'de, D>(helper.message, fork_name)?, + let helper = Helper::deserialize(deserializer)?; + + // Deserialize `data` using ContextDeserialize + let message = BuilderBid::::context_deserialize(helper.message, context) + .map_err(serde::de::Error::custom)?; + + Ok(SignedBuilderBid { + message, signature: helper.signature, }) } diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index fc2d5aa724..f7797da9f2 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -1,6 +1,6 @@ use crate::application_domain::{ApplicationDomain, APPLICATION_DOMAIN_BUILDER}; use crate::blob_sidecar::BlobIdentifier; -use crate::data_column_sidecar::DataColumnIdentifier; +use crate::data_column_sidecar::DataColumnsByRootIdentifier; use crate::*; use int_to_bytes::int_to_bytes4; use safe_arith::{ArithError, SafeArith}; @@ -219,10 +219,9 @@ pub struct ChainSpec { pub boot_nodes: Vec, pub network_id: u8, pub target_aggregators_per_committee: u64, - pub gossip_max_size: u64, + pub max_payload_size: u64, max_request_blocks: u64, pub min_epochs_for_block_requests: u64, - pub max_chunk_size: u64, pub ttfb_timeout: u64, pub resp_timeout: u64, pub attestation_propagation_slot_range: u64, @@ -250,6 +249,11 @@ pub struct ChainSpec { blob_sidecar_subnet_count_electra: u64, max_request_blob_sidecars_electra: u64, + /* + * Networking Fulu + */ + max_blobs_per_block_fulu: u64, + /* * Networking Derived * @@ -684,7 +688,9 @@ impl ChainSpec { /// Return the value of `MAX_BLOBS_PER_BLOCK` appropriate for `fork`. pub fn max_blobs_per_block_by_fork(&self, fork_name: ForkName) -> u64 { - if fork_name.electra_enabled() { + if fork_name.fulu_enabled() { + self.max_blobs_per_block_fulu + } else if fork_name.electra_enabled() { self.max_blobs_per_block_electra } else { self.max_blobs_per_block @@ -744,6 +750,35 @@ impl ChainSpec { (0..self.data_column_sidecar_subnet_count).map(DataColumnSubnetId::new) } + /// Worst-case compressed length for a given payload of size n when using snappy. + /// + /// https://github.com/google/snappy/blob/32ded457c0b1fe78ceb8397632c416568d6714a0/snappy.cc#L218C1-L218C47 + /// https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/p2p-interface.md#max_compressed_len + fn max_compressed_len_snappy(n: usize) -> Option { + 32_usize.checked_add(n)?.checked_add(n / 6) + } + + /// Max compressed length of a message that we receive over gossip. + pub fn max_compressed_len(&self) -> usize { + Self::max_compressed_len_snappy(self.max_payload_size as usize) + .expect("should not overflow") + } + + /// Max allowed size of a raw, compressed message received over the network. + /// + /// https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/p2p-interface.md#max_compressed_len + pub fn max_message_size(&self) -> usize { + std::cmp::max( + // 1024 to account for framing + encoding overhead + Self::max_compressed_len_snappy(self.max_payload_size as usize) + .expect("should not overflow") + .safe_add(1024) + .expect("should not overflow"), + //1MB + 1024 * 1024, + ) + } + /// Returns a `ChainSpec` compatible with the Ethereum Foundation specification. pub fn mainnet() -> Self { Self { @@ -911,7 +946,7 @@ impl ChainSpec { * Electra hard fork params */ electra_fork_version: [0x05, 00, 00, 00], - electra_fork_epoch: None, + electra_fork_epoch: Some(Epoch::new(364032)), unset_deposit_requests_start_index: u64::MAX, full_exit_request_amount: 0, min_activation_balance: option_wrapper(|| { @@ -966,9 +1001,8 @@ impl ChainSpec { subnets_per_node: 2, maximum_gossip_clock_disparity_millis: default_maximum_gossip_clock_disparity_millis(), target_aggregators_per_committee: 16, - gossip_max_size: default_gossip_max_size(), + max_payload_size: default_max_payload_size(), min_epochs_for_block_requests: default_min_epochs_for_block_requests(), - max_chunk_size: default_max_chunk_size(), ttfb_timeout: default_ttfb_timeout(), resp_timeout: default_resp_timeout(), message_domain_invalid_snappy: default_message_domain_invalid_snappy(), @@ -1001,6 +1035,11 @@ impl ChainSpec { blob_sidecar_subnet_count_electra: default_blob_sidecar_subnet_count_electra(), max_request_blob_sidecars_electra: default_max_request_blob_sidecars_electra(), + /* + * Networking Fulu specific + */ + max_blobs_per_block_fulu: default_max_blobs_per_block_fulu(), + /* * Application specific */ @@ -1252,7 +1291,7 @@ impl ChainSpec { * Electra hard fork params */ electra_fork_version: [0x05, 0x00, 0x00, 0x64], - electra_fork_epoch: None, + electra_fork_epoch: Some(Epoch::new(1337856)), unset_deposit_requests_start_index: u64::MAX, full_exit_request_amount: 0, min_activation_balance: option_wrapper(|| { @@ -1274,7 +1313,7 @@ impl ChainSpec { }) .expect("calculation does not overflow"), max_per_epoch_activation_exit_churn_limit: option_wrapper(|| { - u64::checked_pow(2, 8)?.checked_mul(u64::checked_pow(10, 9)?) + u64::checked_pow(2, 6)?.checked_mul(u64::checked_pow(10, 9)?) }) .expect("calculation does not overflow"), @@ -1307,9 +1346,8 @@ impl ChainSpec { subnets_per_node: 4, // Make this larger than usual to avoid network damage maximum_gossip_clock_disparity_millis: default_maximum_gossip_clock_disparity_millis(), target_aggregators_per_committee: 16, - gossip_max_size: default_gossip_max_size(), + max_payload_size: default_max_payload_size(), min_epochs_for_block_requests: 33024, - max_chunk_size: default_max_chunk_size(), ttfb_timeout: default_ttfb_timeout(), resp_timeout: default_resp_timeout(), message_domain_invalid_snappy: default_message_domain_invalid_snappy(), @@ -1325,7 +1363,7 @@ impl ChainSpec { max_request_data_column_sidecars: default_max_request_data_column_sidecars(), min_epochs_for_blob_sidecars_requests: 16384, blob_sidecar_subnet_count: default_blob_sidecar_subnet_count(), - max_blobs_per_block: default_max_blobs_per_block(), + max_blobs_per_block: 2, /* * Derived Deneb Specific @@ -1338,9 +1376,14 @@ impl ChainSpec { /* * Networking Electra specific */ - max_blobs_per_block_electra: default_max_blobs_per_block_electra(), - blob_sidecar_subnet_count_electra: default_blob_sidecar_subnet_count_electra(), - max_request_blob_sidecars_electra: default_max_request_blob_sidecars_electra(), + max_blobs_per_block_electra: 2, + blob_sidecar_subnet_count_electra: 2, + max_request_blob_sidecars_electra: 256, + + /* + * Networking Fulu specific + */ + max_blobs_per_block_fulu: default_max_blobs_per_block_fulu(), /* * Application specific @@ -1489,18 +1532,15 @@ pub struct Config { #[serde(with = "serde_utils::quoted_u64")] gas_limit_adjustment_factor: u64, - #[serde(default = "default_gossip_max_size")] + #[serde(default = "default_max_payload_size")] #[serde(with = "serde_utils::quoted_u64")] - gossip_max_size: u64, + max_payload_size: u64, #[serde(default = "default_max_request_blocks")] #[serde(with = "serde_utils::quoted_u64")] max_request_blocks: u64, #[serde(default = "default_min_epochs_for_block_requests")] #[serde(with = "serde_utils::quoted_u64")] min_epochs_for_block_requests: u64, - #[serde(default = "default_max_chunk_size")] - #[serde(with = "serde_utils::quoted_u64")] - max_chunk_size: u64, #[serde(default = "default_ttfb_timeout")] #[serde(with = "serde_utils::quoted_u64")] ttfb_timeout: u64, @@ -1572,6 +1612,9 @@ pub struct Config { #[serde(default = "default_custody_requirement")] #[serde(with = "serde_utils::quoted_u64")] custody_requirement: u64, + #[serde(default = "default_max_blobs_per_block_fulu")] + #[serde(with = "serde_utils::quoted_u64")] + max_blobs_per_block_fulu: u64, } fn default_bellatrix_fork_version() -> [u8; 4] { @@ -1640,7 +1683,7 @@ const fn default_gas_limit_adjustment_factor() -> u64 { 1024 } -const fn default_gossip_max_size() -> u64 { +const fn default_max_payload_size() -> u64 { 10485760 } @@ -1648,10 +1691,6 @@ const fn default_min_epochs_for_block_requests() -> u64 { 33024 } -const fn default_max_chunk_size() -> u64 { - 10485760 -} - const fn default_ttfb_timeout() -> u64 { 5 } @@ -1718,6 +1757,10 @@ const fn default_max_blobs_per_block_electra() -> u64 { 9 } +const fn default_max_blobs_per_block_fulu() -> u64 { + 12 +} + const fn default_attestation_propagation_slot_range() -> u64 { 32 } @@ -1771,15 +1814,21 @@ fn max_blobs_by_root_request_common(max_request_blob_sidecars: u64) -> usize { .len() } -fn max_data_columns_by_root_request_common(max_request_data_column_sidecars: u64) -> usize { - let max_request_data_column_sidecars = max_request_data_column_sidecars as usize; - let empty_data_column_id = DataColumnIdentifier { +fn max_data_columns_by_root_request_common( + max_request_blocks: u64, + number_of_columns: u64, +) -> usize { + let max_request_blocks = max_request_blocks as usize; + let number_of_columns = number_of_columns as usize; + + let empty_data_columns_by_root_id = DataColumnsByRootIdentifier { block_root: Hash256::zero(), - index: 0, + columns: RuntimeVariableList::from_vec(vec![0; number_of_columns], number_of_columns), }; - RuntimeVariableList::from_vec( - vec![empty_data_column_id; max_request_data_column_sidecars], - max_request_data_column_sidecars, + + RuntimeVariableList::::from_vec( + vec![empty_data_columns_by_root_id; max_request_blocks], + max_request_blocks, ) .as_ssz_bytes() .len() @@ -1798,7 +1847,10 @@ fn default_max_blobs_by_root_request() -> usize { } fn default_data_columns_by_root_request() -> usize { - max_data_columns_by_root_request_common(default_max_request_data_column_sidecars()) + max_data_columns_by_root_request_common( + default_max_request_blocks_deneb(), + default_number_of_columns(), + ) } impl Default for Config { @@ -1922,10 +1974,9 @@ impl Config { gas_limit_adjustment_factor: spec.gas_limit_adjustment_factor, - gossip_max_size: spec.gossip_max_size, + max_payload_size: spec.max_payload_size, max_request_blocks: spec.max_request_blocks, min_epochs_for_block_requests: spec.min_epochs_for_block_requests, - max_chunk_size: spec.max_chunk_size, ttfb_timeout: spec.ttfb_timeout, resp_timeout: spec.resp_timeout, attestation_propagation_slot_range: spec.attestation_propagation_slot_range, @@ -1951,6 +2002,7 @@ impl Config { data_column_sidecar_subnet_count: spec.data_column_sidecar_subnet_count, samples_per_slot: spec.samples_per_slot, custody_requirement: spec.custody_requirement, + max_blobs_per_block_fulu: spec.max_blobs_per_block_fulu, } } @@ -2005,9 +2057,8 @@ impl Config { deposit_network_id, deposit_contract_address, gas_limit_adjustment_factor, - gossip_max_size, + max_payload_size, min_epochs_for_block_requests, - max_chunk_size, ttfb_timeout, resp_timeout, message_domain_invalid_snappy, @@ -2032,6 +2083,7 @@ impl Config { data_column_sidecar_subnet_count, samples_per_slot, custody_requirement, + max_blobs_per_block_fulu, } = self; if preset_base != E::spec_name().to_string().as_str() { @@ -2078,9 +2130,8 @@ impl Config { terminal_total_difficulty, terminal_block_hash, terminal_block_hash_activation_epoch, - gossip_max_size, + max_payload_size, min_epochs_for_block_requests, - max_chunk_size, ttfb_timeout, resp_timeout, message_domain_invalid_snappy, @@ -2109,7 +2160,8 @@ impl Config { ), max_blobs_by_root_request: max_blobs_by_root_request_common(max_request_blob_sidecars), max_data_columns_by_root_request: max_data_columns_by_root_request_common( - max_request_data_column_sidecars, + max_request_blocks_deneb, + number_of_columns, ), number_of_columns, @@ -2117,6 +2169,7 @@ impl Config { data_column_sidecar_subnet_count, samples_per_slot, custody_requirement, + max_blobs_per_block_fulu, ..chain_spec.clone() }) @@ -2386,9 +2439,8 @@ mod yaml_tests { check_default!(terminal_block_hash); check_default!(terminal_block_hash_activation_epoch); check_default!(bellatrix_fork_version); - check_default!(gossip_max_size); + check_default!(max_payload_size); check_default!(min_epochs_for_block_requests); - check_default!(max_chunk_size); check_default!(ttfb_timeout); check_default!(resp_timeout); check_default!(message_domain_invalid_snappy); @@ -2414,4 +2466,17 @@ mod yaml_tests { [0, 0, 0, 1] ); } + + #[test] + fn test_max_network_limits_overflow() { + let mut spec = MainnetEthSpec::default_spec(); + // Should not overflow + let _ = spec.max_message_size(); + let _ = spec.max_compressed_len(); + + spec.max_payload_size *= 10; + // Should not overflow even with a 10x increase in max + let _ = spec.max_message_size(); + let _ = spec.max_compressed_len(); + } } diff --git a/consensus/types/src/checkpoint.rs b/consensus/types/src/checkpoint.rs index 044fc57f22..c3cb1d5c36 100644 --- a/consensus/types/src/checkpoint.rs +++ b/consensus/types/src/checkpoint.rs @@ -1,5 +1,6 @@ use crate::test_utils::TestRandom; -use crate::{Epoch, Hash256}; +use crate::{Epoch, ForkName, Hash256}; +use context_deserialize_derive::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; @@ -24,6 +25,7 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] +#[context_deserialize(ForkName)] pub struct Checkpoint { pub epoch: Epoch, pub root: Hash256, diff --git a/consensus/types/src/consolidation_request.rs b/consensus/types/src/consolidation_request.rs index e2df0bb972..c7375dab84 100644 --- a/consensus/types/src/consolidation_request.rs +++ b/consensus/types/src/consolidation_request.rs @@ -1,4 +1,5 @@ -use crate::{test_utils::TestRandom, Address, PublicKeyBytes, SignedRoot}; +use crate::context_deserialize; +use crate::{test_utils::TestRandom, Address, ForkName, PublicKeyBytes, SignedRoot}; use serde::{Deserialize, Serialize}; use ssz::Encode; use ssz_derive::{Decode, Encode}; @@ -19,6 +20,7 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] +#[context_deserialize(ForkName)] pub struct ConsolidationRequest { pub source_address: Address, pub source_pubkey: PublicKeyBytes, diff --git a/consensus/types/src/contribution_and_proof.rs b/consensus/types/src/contribution_and_proof.rs index 321c12d220..e918beacb0 100644 --- a/consensus/types/src/contribution_and_proof.rs +++ b/consensus/types/src/contribution_and_proof.rs @@ -1,7 +1,8 @@ use super::{ - ChainSpec, EthSpec, Fork, Hash256, SecretKey, Signature, SignedRoot, SyncCommitteeContribution, - SyncSelectionProof, + ChainSpec, EthSpec, Fork, ForkName, Hash256, SecretKey, Signature, SignedRoot, + SyncCommitteeContribution, SyncSelectionProof, }; +use crate::context_deserialize; use crate::test_utils::TestRandom; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; @@ -23,6 +24,7 @@ use tree_hash_derive::TreeHash; )] #[serde(bound = "E: EthSpec")] #[arbitrary(bound = "E: EthSpec")] +#[context_deserialize(ForkName)] pub struct ContributionAndProof { /// The index of the validator that created the sync contribution. #[serde(with = "serde_utils::quoted_u64")] diff --git a/consensus/types/src/data_column_sidecar.rs b/consensus/types/src/data_column_sidecar.rs index 90a914dfae..5ec2b28b2b 100644 --- a/consensus/types/src/data_column_sidecar.rs +++ b/consensus/types/src/data_column_sidecar.rs @@ -1,7 +1,10 @@ use crate::beacon_block_body::{KzgCommitments, BLOB_KZG_COMMITMENTS_INDEX}; +use crate::context_deserialize; use crate::test_utils::TestRandom; -use crate::BeaconStateError; -use crate::{BeaconBlockHeader, Epoch, EthSpec, Hash256, KzgProofs, SignedBeaconBlockHeader, Slot}; +use crate::{ + BeaconBlockHeader, BeaconStateError, Epoch, EthSpec, ForkName, Hash256, RuntimeVariableList, + SignedBeaconBlockHeader, Slot, +}; use bls::Signature; use derivative::Derivative; use kzg::Error as KzgError; @@ -9,11 +12,10 @@ use kzg::{KzgCommitment, KzgProof}; use merkle_proof::verify_merkle_proof; use safe_arith::ArithError; use serde::{Deserialize, Serialize}; -use ssz::Encode; +use ssz::{DecodeError, Encode}; use ssz_derive::{Decode, Encode}; use ssz_types::Error as SszError; use ssz_types::{FixedVector, VariableList}; -use std::hash::Hash; use std::sync::Arc; use test_random_derive::TestRandom; use tree_hash::TreeHash; @@ -23,13 +25,47 @@ pub type ColumnIndex = u64; pub type Cell = FixedVector::BytesPerCell>; pub type DataColumn = VariableList, ::MaxBlobCommitmentsPerBlock>; -/// Container of the data that identifies an individual data column. -#[derive( - Serialize, Deserialize, Encode, Decode, TreeHash, Copy, Clone, Debug, PartialEq, Eq, Hash, -)] -pub struct DataColumnIdentifier { +/// Identifies a set of data columns associated with a specific beacon block. +#[derive(Encode, Clone, Debug, PartialEq)] +pub struct DataColumnsByRootIdentifier { pub block_root: Hash256, - pub index: ColumnIndex, + pub columns: RuntimeVariableList, +} + +impl RuntimeVariableList { + pub fn from_ssz_bytes_with_nested( + bytes: &[u8], + max_len: usize, + num_columns: usize, + ) -> Result { + if bytes.is_empty() { + return Ok(RuntimeVariableList::empty(max_len)); + } + + let vec = ssz::decode_list_of_variable_length_items::, Vec>>( + bytes, + Some(max_len), + )? + .into_iter() + .map(|bytes| { + let mut builder = ssz::SszDecoderBuilder::new(&bytes); + builder.register_type::()?; + builder.register_anonymous_variable_length_item()?; + + let mut decoder = builder.build()?; + let block_root = decoder.decode_next()?; + let columns = decoder.decode_next_with(|bytes| { + RuntimeVariableList::from_ssz_bytes(bytes, num_columns) + })?; + Ok(DataColumnsByRootIdentifier { + block_root, + columns, + }) + }) + .collect::, _>>()?; + + Ok(RuntimeVariableList::from_vec(vec, max_len)) + } } pub type DataColumnSidecarList = Vec>>; @@ -49,6 +85,7 @@ pub type DataColumnSidecarList = Vec>>; #[serde(bound = "E: EthSpec")] #[arbitrary(bound = "E: EthSpec")] #[derivative(PartialEq, Eq, Hash(bound = "E: EthSpec"))] +#[context_deserialize(ForkName)] pub struct DataColumnSidecar { #[serde(with = "serde_utils::quoted_u64")] pub index: ColumnIndex, @@ -56,7 +93,7 @@ pub struct DataColumnSidecar { pub column: DataColumn, /// All the KZG commitments and proofs associated with the block, used for verifying sample cells. pub kzg_commitments: KzgCommitments, - pub kzg_proofs: KzgProofs, + pub kzg_proofs: VariableList, pub signed_block_header: SignedBeaconBlockHeader, /// An inclusion proof, proving the inclusion of `blob_kzg_commitments` in `BeaconBlockBody`. pub kzg_commitments_inclusion_proof: FixedVector, @@ -132,13 +169,6 @@ impl DataColumnSidecar { .as_ssz_bytes() .len() } - - pub fn id(&self) -> DataColumnIdentifier { - DataColumnIdentifier { - block_root: self.block_root(), - index: self.index, - } - } } #[derive(Debug)] @@ -178,3 +208,45 @@ impl From for DataColumnSidecarError { Self::SszError(e) } } + +#[cfg(test)] +mod test { + use super::*; + use bls::FixedBytesExtended; + + #[test] + fn round_trip_dcbroot_list() { + let max_outer = 5; + let max_inner = 10; + + let data = vec![ + DataColumnsByRootIdentifier { + block_root: Hash256::from_low_u64_be(10), + columns: RuntimeVariableList::::from_vec(vec![1u64, 2, 3], max_inner), + }, + DataColumnsByRootIdentifier { + block_root: Hash256::from_low_u64_be(20), + columns: RuntimeVariableList::::from_vec(vec![4u64, 5], max_inner), + }, + ]; + + let list = RuntimeVariableList::from_vec(data.clone(), max_outer); + + let ssz_bytes = list.as_ssz_bytes(); + + let decoded = + RuntimeVariableList::::from_ssz_bytes_with_nested( + &ssz_bytes, max_outer, max_inner, + ) + .expect("should decode list of DataColumnsByRootIdentifier"); + + assert_eq!(decoded.len(), data.len()); + for (original, decoded) in data.iter().zip(decoded.iter()) { + assert_eq!(decoded.block_root, original.block_root); + assert_eq!( + decoded.columns.iter().copied().collect::>(), + original.columns.iter().copied().collect::>() + ); + } + } +} diff --git a/consensus/types/src/deposit.rs b/consensus/types/src/deposit.rs index c818c7d808..8b4b6af95d 100644 --- a/consensus/types/src/deposit.rs +++ b/consensus/types/src/deposit.rs @@ -1,3 +1,4 @@ +use crate::context_deserialize; use crate::test_utils::TestRandom; use crate::*; use serde::{Deserialize, Serialize}; @@ -24,6 +25,7 @@ pub const DEPOSIT_TREE_DEPTH: usize = 32; TreeHash, TestRandom, )] +#[context_deserialize(ForkName)] pub struct Deposit { pub proof: FixedVector, pub data: DepositData, diff --git a/consensus/types/src/deposit_data.rs b/consensus/types/src/deposit_data.rs index f62829e795..d29e8c8d14 100644 --- a/consensus/types/src/deposit_data.rs +++ b/consensus/types/src/deposit_data.rs @@ -1,6 +1,5 @@ use crate::test_utils::TestRandom; use crate::*; - use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; @@ -22,6 +21,7 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] +#[context_deserialize(ForkName)] pub struct DepositData { pub pubkey: PublicKeyBytes, pub withdrawal_credentials: Hash256, diff --git a/consensus/types/src/deposit_message.rs b/consensus/types/src/deposit_message.rs index 6184d0aeb3..5c2a0b7c2b 100644 --- a/consensus/types/src/deposit_message.rs +++ b/consensus/types/src/deposit_message.rs @@ -21,6 +21,7 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] +#[context_deserialize(ForkName)] pub struct DepositMessage { pub pubkey: PublicKeyBytes, pub withdrawal_credentials: Hash256, diff --git a/consensus/types/src/deposit_request.rs b/consensus/types/src/deposit_request.rs index a21760551b..141258b5ab 100644 --- a/consensus/types/src/deposit_request.rs +++ b/consensus/types/src/deposit_request.rs @@ -1,5 +1,6 @@ +use crate::context_deserialize; use crate::test_utils::TestRandom; -use crate::{Hash256, PublicKeyBytes}; +use crate::{ForkName, Hash256, PublicKeyBytes}; use bls::SignatureBytes; use serde::{Deserialize, Serialize}; use ssz::Encode; @@ -20,6 +21,7 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] +#[context_deserialize(ForkName)] pub struct DepositRequest { pub pubkey: PublicKeyBytes, pub withdrawal_credentials: Hash256, diff --git a/consensus/types/src/eth1_data.rs b/consensus/types/src/eth1_data.rs index e2c4e511ef..7bd0d3228d 100644 --- a/consensus/types/src/eth1_data.rs +++ b/consensus/types/src/eth1_data.rs @@ -1,6 +1,7 @@ use super::Hash256; +use crate::context_deserialize; use crate::test_utils::TestRandom; - +use crate::ForkName; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; @@ -24,6 +25,7 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] +#[context_deserialize(ForkName)] pub struct Eth1Data { pub deposit_root: Hash256, #[serde(with = "serde_utils::quoted_u64")] diff --git a/consensus/types/src/eth_spec.rs b/consensus/types/src/eth_spec.rs index 1c28acccaf..7cd7aeb521 100644 --- a/consensus/types/src/eth_spec.rs +++ b/consensus/types/src/eth_spec.rs @@ -4,8 +4,8 @@ use safe_arith::SafeArith; use serde::{Deserialize, Serialize}; use ssz_types::typenum::{ bit::B0, UInt, U0, U1, U10, U1024, U1048576, U1073741824, U1099511627776, U128, U131072, - U134217728, U16, U16777216, U17, U2, U2048, U256, U262144, U32, U4, U4096, U512, U625, U64, - U65536, U8, U8192, + U134217728, U16, U16777216, U17, U2, U2048, U256, U262144, U32, U33554432, U4, U4096, U512, + U625, U64, U65536, U8, U8192, }; use std::fmt::{self, Debug}; use std::str::FromStr; @@ -146,6 +146,11 @@ pub trait EthSpec: /// Must be set to `BytesPerFieldElement * FieldElementsPerCell`. type BytesPerCell: Unsigned + Clone + Sync + Send + Debug + PartialEq; + /// The maximum number of cell commitments per block + /// + /// FieldElementsPerExtBlob * MaxBlobCommitmentsPerBlock + type MaxCellsPerBlock: Unsigned + Clone + Sync + Send + Debug + PartialEq; + /* * New in Electra */ @@ -437,6 +442,7 @@ impl EthSpec for MainnetEthSpec { type FieldElementsPerExtBlob = U8192; type BytesPerBlob = U131072; type BytesPerCell = U2048; + type MaxCellsPerBlock = U33554432; type KzgCommitmentInclusionProofDepth = U17; type KzgCommitmentsInclusionProofDepth = U4; // inclusion of the whole list of commitments type SyncSubcommitteeSize = U128; // 512 committee size / 4 sync committee subnet count @@ -492,6 +498,7 @@ impl EthSpec for MinimalEthSpec { type MaxWithdrawalRequestsPerPayload = U2; type FieldElementsPerCell = U64; type FieldElementsPerExtBlob = U8192; + type MaxCellsPerBlock = U33554432; type BytesPerCell = U2048; type KzgCommitmentsInclusionProofDepth = U4; @@ -586,6 +593,7 @@ impl EthSpec for GnosisEthSpec { type MaxPendingDepositsPerEpoch = U16; type FieldElementsPerCell = U64; type FieldElementsPerExtBlob = U8192; + type MaxCellsPerBlock = U33554432; type BytesPerCell = U2048; type KzgCommitmentsInclusionProofDepth = U4; type InclusionListCommitteeSize = U16; diff --git a/consensus/types/src/execution_block_hash.rs b/consensus/types/src/execution_block_hash.rs index 677b3d3408..6c031f6899 100644 --- a/consensus/types/src/execution_block_hash.rs +++ b/consensus/types/src/execution_block_hash.rs @@ -112,3 +112,22 @@ impl fmt::Display for ExecutionBlockHash { write!(f, "{}", self.0) } } + +impl From for ExecutionBlockHash { + fn from(hash: Hash256) -> Self { + Self(hash) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_hash256() { + let hash = Hash256::random(); + let ex_hash = ExecutionBlockHash::from(hash); + + assert_eq!(ExecutionBlockHash(hash), ex_hash); + } +} diff --git a/consensus/types/src/execution_payload.rs b/consensus/types/src/execution_payload.rs index 8fb29f2457..fdcc500948 100644 --- a/consensus/types/src/execution_payload.rs +++ b/consensus/types/src/execution_payload.rs @@ -1,6 +1,6 @@ use crate::{test_utils::TestRandom, *}; use derivative::Derivative; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; @@ -30,6 +30,7 @@ pub type Withdrawals = VariableList::MaxWithdrawal Derivative, arbitrary::Arbitrary ), + context_deserialize(ForkName), derivative(PartialEq, Hash(bound = "E: EthSpec")), serde(bound = "E: EthSpec", deny_unknown_fields), arbitrary(bound = "E: EthSpec") @@ -134,29 +135,38 @@ impl ExecutionPayload { } } -impl ForkVersionDeserialize for ExecutionPayload { - fn deserialize_by_fork<'de, D: serde::Deserializer<'de>>( - value: serde_json::value::Value, - fork_name: ForkName, - ) -> Result { +impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for ExecutionPayload { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { let convert_err = |e| { serde::de::Error::custom(format!("ExecutionPayload failed to deserialize: {:?}", e)) }; - - Ok(match fork_name { - ForkName::Bellatrix => { - Self::Bellatrix(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::Eip7805 => Self::Eip7805(serde_json::from_value(value).map_err(convert_err)?), - ForkName::Fulu => Self::Fulu(serde_json::from_value(value).map_err(convert_err)?), + Ok(match context { ForkName::Base | ForkName::Altair => { return Err(serde::de::Error::custom(format!( "ExecutionPayload failed to deserialize: unsupported fork '{}'", - fork_name - ))); + context + ))) + } + ForkName::Bellatrix => { + Self::Bellatrix(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Capella => { + Self::Capella(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Deneb => { + Self::Deneb(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Electra => { + Self::Electra(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Eip7805 => { + Self::Eip7805(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Fulu => { + Self::Fulu(Deserialize::deserialize(deserializer).map_err(convert_err)?) } }) } diff --git a/consensus/types/src/execution_payload_header.rs b/consensus/types/src/execution_payload_header.rs index 2ede858fd5..872aab46b4 100644 --- a/consensus/types/src/execution_payload_header.rs +++ b/consensus/types/src/execution_payload_header.rs @@ -1,6 +1,6 @@ use crate::{test_utils::TestRandom, *}; use derivative::Derivative; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; @@ -25,7 +25,8 @@ use tree_hash_derive::TreeHash; ), derivative(PartialEq, Hash(bound = "E: EthSpec")), serde(bound = "E: EthSpec", deny_unknown_fields), - arbitrary(bound = "E: EthSpec") + arbitrary(bound = "E: EthSpec"), + context_deserialize(ForkName), ), ref_attributes( derive(PartialEq, TreeHash, Debug), @@ -545,32 +546,41 @@ impl TryFrom> for ExecutionPayloadHeaderFu } } -impl ForkVersionDeserialize for ExecutionPayloadHeader { - fn deserialize_by_fork<'de, D: serde::Deserializer<'de>>( - value: serde_json::value::Value, - fork_name: ForkName, - ) -> Result { +impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for ExecutionPayloadHeader { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { let convert_err = |e| { serde::de::Error::custom(format!( "ExecutionPayloadHeader failed to deserialize: {:?}", e )) }; - - Ok(match fork_name { - ForkName::Bellatrix => { - Self::Bellatrix(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::Eip7805 => Self::Eip7805(serde_json::from_value(value).map_err(convert_err)?), - ForkName::Fulu => Self::Fulu(serde_json::from_value(value).map_err(convert_err)?), + Ok(match context { ForkName::Base | ForkName::Altair => { return Err(serde::de::Error::custom(format!( "ExecutionPayloadHeader failed to deserialize: unsupported fork '{}'", - fork_name - ))); + context + ))) + } + ForkName::Bellatrix => { + Self::Bellatrix(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Capella => { + Self::Capella(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Deneb => { + Self::Deneb(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Electra => { + Self::Electra(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Eip7805 => { + Self::Eip7805(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Fulu => { + Self::Fulu(Deserialize::deserialize(deserializer).map_err(convert_err)?) } }) } diff --git a/consensus/types/src/execution_requests.rs b/consensus/types/src/execution_requests.rs index 223c6444cc..2fec3b5f66 100644 --- a/consensus/types/src/execution_requests.rs +++ b/consensus/types/src/execution_requests.rs @@ -1,5 +1,6 @@ +use crate::context_deserialize; use crate::test_utils::TestRandom; -use crate::{ConsolidationRequest, DepositRequest, EthSpec, Hash256, WithdrawalRequest}; +use crate::{ConsolidationRequest, DepositRequest, EthSpec, ForkName, Hash256, WithdrawalRequest}; use alloy_primitives::Bytes; use derivative::Derivative; use ethereum_hashing::{DynamicContext, Sha256Context}; @@ -33,6 +34,7 @@ pub type ConsolidationRequests = #[serde(bound = "E: EthSpec")] #[arbitrary(bound = "E: EthSpec")] #[derivative(PartialEq, Eq, Hash(bound = "E: EthSpec"))] +#[context_deserialize(ForkName)] pub struct ExecutionRequests { pub deposits: DepositRequests, pub withdrawals: WithdrawalRequests, diff --git a/consensus/types/src/fork.rs b/consensus/types/src/fork.rs index b23113f436..239ffe33c0 100644 --- a/consensus/types/src/fork.rs +++ b/consensus/types/src/fork.rs @@ -1,5 +1,6 @@ use crate::test_utils::TestRandom; -use crate::Epoch; +use crate::{Epoch, ForkName}; +use context_deserialize_derive::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; @@ -23,6 +24,7 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] +#[context_deserialize(ForkName)] pub struct Fork { #[serde(with = "serde_utils::bytes_4_hex")] pub previous_version: [u8; 4], diff --git a/consensus/types/src/fork_data.rs b/consensus/types/src/fork_data.rs index 52ce57a2a9..1ac91084d2 100644 --- a/consensus/types/src/fork_data.rs +++ b/consensus/types/src/fork_data.rs @@ -1,5 +1,6 @@ use crate::test_utils::TestRandom; -use crate::{Hash256, SignedRoot}; +use crate::{ForkName, Hash256, SignedRoot}; +use context_deserialize_derive::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; @@ -22,6 +23,7 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] +#[context_deserialize(ForkName)] pub struct ForkData { #[serde(with = "serde_utils::bytes_4_hex")] pub current_version: [u8; 4], diff --git a/consensus/types/src/fork_versioned_response.rs b/consensus/types/src/fork_versioned_response.rs deleted file mode 100644 index 7e4efd05d6..0000000000 --- a/consensus/types/src/fork_versioned_response.rs +++ /dev/null @@ -1,152 +0,0 @@ -use crate::ForkName; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Deserializer, Serialize}; -use serde_json::value::Value; -use std::sync::Arc; - -pub trait ForkVersionDecode: Sized { - /// SSZ decode with explicit fork variant. - fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result; -} - -pub trait ForkVersionDeserialize: Sized + DeserializeOwned { - fn deserialize_by_fork<'de, D: Deserializer<'de>>( - value: Value, - fork_name: ForkName, - ) -> Result; -} - -/// Deserialize is only implemented for types that implement ForkVersionDeserialize. -/// -/// The metadata of type M should be set to `EmptyMetadata` if you don't care about adding fields other than -/// version. If you *do* care about adding other fields you can mix in any type that implements -/// `Deserialize`. -#[derive(Debug, PartialEq, Clone, Serialize)] -pub struct ForkVersionedResponse { - #[serde(skip_serializing_if = "Option::is_none")] - pub version: Option, - #[serde(flatten)] - pub metadata: M, - pub data: T, -} - -/// Metadata type similar to unit (i.e. `()`) but deserializes from a map (`serde_json::Value`). -/// -/// Unfortunately the braces are semantically significant, i.e. `struct EmptyMetadata;` does not -/// work. -#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)] -pub struct EmptyMetadata {} - -/// Fork versioned response with extra information about finalization & optimistic execution. -pub type ExecutionOptimisticFinalizedForkVersionedResponse = - ForkVersionedResponse; - -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -pub struct ExecutionOptimisticFinalizedMetadata { - pub execution_optimistic: Option, - pub finalized: Option, -} - -impl<'de, F, M> serde::Deserialize<'de> for ForkVersionedResponse -where - F: ForkVersionDeserialize, - M: DeserializeOwned, -{ - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - #[derive(Deserialize)] - struct Helper { - version: Option, - #[serde(flatten)] - metadata: serde_json::Value, - data: serde_json::Value, - } - - let helper = Helper::deserialize(deserializer)?; - let data = match helper.version { - Some(fork_name) => F::deserialize_by_fork::<'de, D>(helper.data, fork_name)?, - None => serde_json::from_value(helper.data).map_err(serde::de::Error::custom)?, - }; - let metadata = serde_json::from_value(helper.metadata).map_err(serde::de::Error::custom)?; - - Ok(ForkVersionedResponse { - version: helper.version, - metadata, - data, - }) - } -} - -impl ForkVersionDeserialize for Arc { - fn deserialize_by_fork<'de, D: Deserializer<'de>>( - value: Value, - fork_name: ForkName, - ) -> Result { - Ok(Arc::new(F::deserialize_by_fork::<'de, D>( - value, fork_name, - )?)) - } -} - -impl ForkVersionedResponse { - /// Apply a function to the inner `data`, potentially changing its type. - pub fn map_data(self, f: impl FnOnce(T) -> U) -> ForkVersionedResponse { - let ForkVersionedResponse { - version, - metadata, - data, - } = self; - ForkVersionedResponse { - version, - metadata, - data: f(data), - } - } -} - -#[cfg(test)] -mod fork_version_response_tests { - use crate::{ - ExecutionPayload, ExecutionPayloadBellatrix, ForkName, ForkVersionedResponse, - MainnetEthSpec, - }; - use serde_json::json; - - #[test] - fn fork_versioned_response_deserialize_correct_fork() { - type E = MainnetEthSpec; - - let response_json = - serde_json::to_string(&json!(ForkVersionedResponse::> { - version: Some(ForkName::Bellatrix), - metadata: Default::default(), - data: ExecutionPayload::Bellatrix(ExecutionPayloadBellatrix::default()), - })) - .unwrap(); - - let result: Result>, _> = - serde_json::from_str(&response_json); - - assert!(result.is_ok()); - } - - #[test] - fn fork_versioned_response_deserialize_incorrect_fork() { - type E = MainnetEthSpec; - - let response_json = - serde_json::to_string(&json!(ForkVersionedResponse::> { - version: Some(ForkName::Capella), - metadata: Default::default(), - data: ExecutionPayload::Bellatrix(ExecutionPayloadBellatrix::default()), - })) - .unwrap(); - - let result: Result>, _> = - serde_json::from_str(&response_json); - - assert!(result.is_err()); - } -} diff --git a/consensus/types/src/historical_batch.rs b/consensus/types/src/historical_batch.rs index 7bac9699eb..3a02810bba 100644 --- a/consensus/types/src/historical_batch.rs +++ b/consensus/types/src/historical_batch.rs @@ -22,6 +22,7 @@ use tree_hash_derive::TreeHash; arbitrary::Arbitrary, )] #[arbitrary(bound = "E: EthSpec")] +#[context_deserialize(ForkName)] pub struct HistoricalBatch { #[test_random(default)] pub block_roots: Vector, diff --git a/consensus/types/src/historical_summary.rs b/consensus/types/src/historical_summary.rs index 8c82d52b81..7ad423dade 100644 --- a/consensus/types/src/historical_summary.rs +++ b/consensus/types/src/historical_summary.rs @@ -1,5 +1,6 @@ +use crate::context_deserialize; use crate::test_utils::TestRandom; -use crate::{BeaconState, EthSpec, Hash256}; +use crate::{BeaconState, EthSpec, ForkName, Hash256}; use compare_fields_derive::CompareFields; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; @@ -28,6 +29,7 @@ use tree_hash_derive::TreeHash; Default, arbitrary::Arbitrary, )] +#[context_deserialize(ForkName)] pub struct HistoricalSummary { block_summary_root: Hash256, state_summary_root: Hash256, diff --git a/consensus/types/src/indexed_attestation.rs b/consensus/types/src/indexed_attestation.rs index f3243a9f05..ea65d78504 100644 --- a/consensus/types/src/indexed_attestation.rs +++ b/consensus/types/src/indexed_attestation.rs @@ -1,4 +1,7 @@ -use crate::{test_utils::TestRandom, AggregateSignature, AttestationData, EthSpec, VariableList}; +use crate::context_deserialize; +use crate::{ + test_utils::TestRandom, AggregateSignature, AttestationData, EthSpec, ForkName, VariableList, +}; use core::slice::Iter; use derivative::Derivative; use serde::{Deserialize, Serialize}; @@ -29,6 +32,7 @@ use tree_hash_derive::TreeHash; arbitrary::Arbitrary, TreeHash, ), + context_deserialize(ForkName), derivative(PartialEq, Hash(bound = "E: EthSpec")), serde(bound = "E: EthSpec", deny_unknown_fields), arbitrary(bound = "E: EthSpec"), diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index 847d1bc7b1..60a3cafa8e 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -22,6 +22,7 @@ pub mod beacon_block; pub mod beacon_block_body; pub mod beacon_block_header; pub mod beacon_committee; +pub mod beacon_response; pub mod beacon_state; pub mod bls_to_execution_change; pub mod builder_bid; @@ -44,7 +45,6 @@ pub mod execution_payload_header; pub mod fork; pub mod fork_data; pub mod fork_name; -pub mod fork_versioned_response; pub mod graffiti; pub mod historical_batch; pub mod historical_summary; @@ -141,6 +141,9 @@ pub use crate::beacon_block_body::{ }; pub use crate::beacon_block_header::BeaconBlockHeader; pub use crate::beacon_committee::{BeaconCommittee, OwnedBeaconCommittee}; +pub use crate::beacon_response::{ + BeaconResponse, ForkVersionDecode, ForkVersionedResponse, UnversionedResponse, +}; pub use crate::beacon_state::{Error as BeaconStateError, *}; pub use crate::blob_sidecar::{BlobIdentifier, BlobSidecar, BlobSidecarList, BlobsList}; pub use crate::bls_to_execution_change::BlsToExecutionChange; @@ -152,7 +155,7 @@ pub use crate::config_and_preset::{ pub use crate::consolidation_request::ConsolidationRequest; pub use crate::contribution_and_proof::ContributionAndProof; pub use crate::data_column_sidecar::{ - ColumnIndex, DataColumnIdentifier, DataColumnSidecar, DataColumnSidecarList, + ColumnIndex, DataColumnSidecar, DataColumnSidecarList, DataColumnsByRootIdentifier, }; pub use crate::data_column_subnet_id::DataColumnSubnetId; pub use crate::deposit::{Deposit, DEPOSIT_TREE_DEPTH}; @@ -181,9 +184,6 @@ pub use crate::fork::Fork; pub use crate::fork_context::ForkContext; pub use crate::fork_data::ForkData; pub use crate::fork_name::{ForkName, InconsistentFork}; -pub use crate::fork_versioned_response::{ - ForkVersionDecode, ForkVersionDeserialize, ForkVersionedResponse, -}; pub use crate::graffiti::{Graffiti, GRAFFITI_BYTES_LEN}; pub use crate::historical_batch::HistoricalBatch; pub use crate::inclusion_list::{InclusionList, InclusionListTransactions, SignedInclusionList}; @@ -280,7 +280,14 @@ pub type Address = fixed_bytes::Address; pub type ForkVersion = [u8; 4]; pub type BLSFieldElement = Uint256; pub type Blob = FixedVector::BytesPerBlob>; -pub type KzgProofs = VariableList::MaxBlobCommitmentsPerBlock>; +// Note on List limit: +// - Deneb to Electra: `MaxBlobCommitmentsPerBlock` +// - Fulu: `MaxCellsPerBlock` +// We choose to use a single type (with the larger value from Fulu as `N`) instead of having to +// introduce a new type for Fulu. This is to avoid messy conversions and having to add extra types +// with no gains - as `N` does not impact serialisation at all, and only affects merkleization, +// which we don't current do on `KzgProofs` anyway. +pub type KzgProofs = VariableList::MaxCellsPerBlock>; pub type VersionedHash = Hash256; pub type Hash64 = alloy_primitives::B64; @@ -288,6 +295,8 @@ pub use bls::{ AggregatePublicKey, AggregateSignature, Keypair, PublicKey, PublicKeyBytes, SecretKey, Signature, SignatureBytes, }; +pub use context_deserialize::ContextDeserialize; +pub use context_deserialize_derive::context_deserialize; pub use kzg::{KzgCommitment, KzgProof, VERSIONED_HASH_VERSION_KZG}; pub use milhouse::{self, List, Vector}; pub use ssz_types::{typenum, typenum::Unsigned, BitList, BitVector, FixedVector, VariableList}; diff --git a/consensus/types/src/light_client_bootstrap.rs b/consensus/types/src/light_client_bootstrap.rs index cfd8aab225..60b1dd9209 100644 --- a/consensus/types/src/light_client_bootstrap.rs +++ b/consensus/types/src/light_client_bootstrap.rs @@ -1,12 +1,12 @@ +use crate::context_deserialize; use crate::{ - light_client_update::*, test_utils::TestRandom, BeaconState, ChainSpec, EthSpec, FixedVector, - ForkName, ForkVersionDeserialize, Hash256, LightClientHeader, LightClientHeaderAltair, + light_client_update::*, test_utils::TestRandom, BeaconState, ChainSpec, ContextDeserialize, + EthSpec, FixedVector, ForkName, Hash256, LightClientHeader, LightClientHeaderAltair, LightClientHeaderCapella, LightClientHeaderDeneb, LightClientHeaderElectra, LightClientHeaderFulu, SignedBlindedBeaconBlock, Slot, SyncCommittee, }; use derivative::Derivative; use serde::{Deserialize, Deserializer, Serialize}; -use serde_json::Value; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use std::sync::Arc; @@ -34,6 +34,7 @@ use tree_hash_derive::TreeHash; ), serde(bound = "E: EthSpec", deny_unknown_fields), arbitrary(bound = "E: EthSpec"), + context_deserialize(ForkName), ) )] #[derive( @@ -217,20 +218,40 @@ impl LightClientBootstrap { } } -impl ForkVersionDeserialize for LightClientBootstrap { - fn deserialize_by_fork<'de, D: Deserializer<'de>>( - value: Value, - fork_name: ForkName, - ) -> Result { - if fork_name.altair_enabled() { - Ok(serde_json::from_value::>(value) - .map_err(serde::de::Error::custom))? - } else { - Err(serde::de::Error::custom(format!( - "LightClientBootstrap failed to deserialize: unsupported fork '{}'", - fork_name - ))) - } +impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for LightClientBootstrap { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { + let convert_err = |e| { + serde::de::Error::custom(format!( + "LightClientBootstrap failed to deserialize: {:?}", + e + )) + }; + Ok(match context { + ForkName::Base => { + return Err(serde::de::Error::custom(format!( + "LightClientBootstrap failed to deserialize: unsupported fork '{}'", + context + ))) + } + ForkName::Altair | ForkName::Bellatrix => { + Self::Altair(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Capella => { + Self::Capella(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Deneb => { + Self::Deneb(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Electra | ForkName::Eip7805 => { + Self::Electra(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Fulu => { + Self::Fulu(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + }) } } diff --git a/consensus/types/src/light_client_finality_update.rs b/consensus/types/src/light_client_finality_update.rs index 99c77e1272..927b93ee1d 100644 --- a/consensus/types/src/light_client_finality_update.rs +++ b/consensus/types/src/light_client_finality_update.rs @@ -1,13 +1,13 @@ use super::{EthSpec, FixedVector, Hash256, LightClientHeader, Slot, SyncAggregate}; +use crate::context_deserialize; use crate::ChainSpec; use crate::{ - light_client_update::*, test_utils::TestRandom, ForkName, ForkVersionDeserialize, + light_client_update::*, test_utils::TestRandom, ContextDeserialize, ForkName, LightClientHeaderAltair, LightClientHeaderCapella, LightClientHeaderDeneb, LightClientHeaderElectra, LightClientHeaderFulu, SignedBlindedBeaconBlock, }; use derivative::Derivative; use serde::{Deserialize, Deserializer, Serialize}; -use serde_json::Value; use ssz::{Decode, Encode}; use ssz_derive::Decode; use ssz_derive::Encode; @@ -33,11 +33,10 @@ use tree_hash_derive::TreeHash; ), serde(bound = "E: EthSpec", deny_unknown_fields), arbitrary(bound = "E: EthSpec"), + context_deserialize(ForkName), ) )] -#[derive( - Debug, Clone, Serialize, Encode, TreeHash, Deserialize, arbitrary::Arbitrary, PartialEq, -)] +#[derive(Debug, Clone, Serialize, Encode, TreeHash, arbitrary::Arbitrary, PartialEq)] #[serde(untagged)] #[tree_hash(enum_behaviour = "transparent")] #[ssz(enum_behaviour = "transparent")] @@ -237,20 +236,40 @@ impl LightClientFinalityUpdate { } } -impl ForkVersionDeserialize for LightClientFinalityUpdate { - fn deserialize_by_fork<'de, D: Deserializer<'de>>( - value: Value, - fork_name: ForkName, - ) -> Result { - if fork_name.altair_enabled() { - serde_json::from_value::>(value) - .map_err(serde::de::Error::custom) - } else { - Err(serde::de::Error::custom(format!( - "LightClientFinalityUpdate failed to deserialize: unsupported fork '{}'", - fork_name - ))) - } +impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for LightClientFinalityUpdate { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { + let convert_err = |e| { + serde::de::Error::custom(format!( + "LightClientFinalityUpdate failed to deserialize: {:?}", + e + )) + }; + Ok(match context { + ForkName::Base => { + return Err(serde::de::Error::custom(format!( + "LightClientFinalityUpdate failed to deserialize: unsupported fork '{}'", + context + ))) + } + ForkName::Altair | ForkName::Bellatrix => { + Self::Altair(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Capella => { + Self::Capella(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Deneb => { + Self::Deneb(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Electra | ForkName::Eip7805 => { + Self::Electra(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Fulu => { + Self::Fulu(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + }) } } diff --git a/consensus/types/src/light_client_header.rs b/consensus/types/src/light_client_header.rs index 111b1fbb6e..7dbc8b8bdf 100644 --- a/consensus/types/src/light_client_header.rs +++ b/consensus/types/src/light_client_header.rs @@ -1,6 +1,5 @@ +use crate::context_deserialize; use crate::ChainSpec; -use crate::ForkName; -use crate::ForkVersionDeserialize; use crate::{light_client_update::*, BeaconBlockBody}; use crate::{ test_utils::TestRandom, EthSpec, ExecutionPayloadHeaderCapella, ExecutionPayloadHeaderDeneb, @@ -8,8 +7,9 @@ use crate::{ SignedBlindedBeaconBlock, }; use crate::{BeaconBlockHeader, ExecutionPayloadHeader}; +use crate::{ContextDeserialize, ForkName}; use derivative::Derivative; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use ssz::Decode; use ssz_derive::{Decode, Encode}; use std::marker::PhantomData; @@ -35,11 +35,10 @@ use tree_hash_derive::TreeHash; ), serde(bound = "E: EthSpec", deny_unknown_fields), arbitrary(bound = "E: EthSpec"), + context_deserialize(ForkName), ) )] -#[derive( - Debug, Clone, Serialize, TreeHash, Encode, Deserialize, arbitrary::Arbitrary, PartialEq, -)] +#[derive(Debug, Clone, Serialize, TreeHash, Encode, arbitrary::Arbitrary, PartialEq)] #[serde(untagged)] #[tree_hash(enum_behaviour = "transparent")] #[ssz(enum_behaviour = "transparent")] @@ -334,31 +333,40 @@ impl Default for LightClientHeaderFulu { } } -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::Bellatrix => 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 => serde_json::from_value(value) - .map(|light_client_header| Self::Deneb(light_client_header)) - .map_err(serde::de::Error::custom), - ForkName::Electra | ForkName::Eip7805 => serde_json::from_value(value) - .map(|light_client_header| Self::Electra(light_client_header)) - .map_err(serde::de::Error::custom), - ForkName::Fulu => serde_json::from_value(value) - .map(|light_client_header| Self::Fulu(light_client_header)) - .map_err(serde::de::Error::custom), - ForkName::Base => Err(serde::de::Error::custom(format!( - "LightClientHeader deserialization for {fork_name} not implemented" - ))), - } +impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for LightClientHeader { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { + let convert_err = |e| { + serde::de::Error::custom(format!( + "LightClientFinalityUpdate failed to deserialize: {:?}", + e + )) + }; + Ok(match context { + ForkName::Base => { + return Err(serde::de::Error::custom(format!( + "LightClientFinalityUpdate failed to deserialize: unsupported fork '{}'", + context + ))) + } + ForkName::Altair | ForkName::Bellatrix => { + Self::Altair(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Capella => { + Self::Capella(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Deneb => { + Self::Deneb(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Electra | ForkName::Eip7805 => { + Self::Electra(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Fulu => { + Self::Fulu(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + }) } } diff --git a/consensus/types/src/light_client_optimistic_update.rs b/consensus/types/src/light_client_optimistic_update.rs index d373b2334d..1b8af6e1da 100644 --- a/consensus/types/src/light_client_optimistic_update.rs +++ b/consensus/types/src/light_client_optimistic_update.rs @@ -1,4 +1,5 @@ -use super::{EthSpec, ForkName, ForkVersionDeserialize, LightClientHeader, Slot, SyncAggregate}; +use super::{ContextDeserialize, EthSpec, ForkName, LightClientHeader, Slot, SyncAggregate}; +use crate::context_deserialize; use crate::test_utils::TestRandom; use crate::{ light_client_update::*, ChainSpec, LightClientHeaderAltair, LightClientHeaderCapella, @@ -7,7 +8,6 @@ use crate::{ }; use derivative::Derivative; use serde::{Deserialize, Deserializer, Serialize}; -use serde_json::Value; use ssz::{Decode, Encode}; use ssz_derive::Decode; use ssz_derive::Encode; @@ -36,11 +36,10 @@ use tree_hash_derive::TreeHash; ), serde(bound = "E: EthSpec", deny_unknown_fields), arbitrary(bound = "E: EthSpec"), + context_deserialize(ForkName), ) )] -#[derive( - Debug, Clone, Serialize, Encode, TreeHash, Deserialize, arbitrary::Arbitrary, PartialEq, -)] +#[derive(Debug, Clone, Serialize, Encode, TreeHash, arbitrary::Arbitrary, PartialEq)] #[serde(untagged)] #[tree_hash(enum_behaviour = "transparent")] #[ssz(enum_behaviour = "transparent")] @@ -210,22 +209,40 @@ impl LightClientOptimisticUpdate { } } -impl ForkVersionDeserialize for LightClientOptimisticUpdate { - fn deserialize_by_fork<'de, D: Deserializer<'de>>( - value: Value, - fork_name: ForkName, - ) -> Result { - if fork_name.altair_enabled() { - Ok( - serde_json::from_value::>(value) - .map_err(serde::de::Error::custom), - )? - } else { - Err(serde::de::Error::custom(format!( - "LightClientOptimisticUpdate failed to deserialize: unsupported fork '{}'", - fork_name - ))) - } +impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for LightClientOptimisticUpdate { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { + let convert_err = |e| { + serde::de::Error::custom(format!( + "LightClientOptimisticUpdate failed to deserialize: {:?}", + e + )) + }; + Ok(match context { + ForkName::Base => { + return Err(serde::de::Error::custom(format!( + "LightClientOptimisticUpdate failed to deserialize: unsupported fork '{}'", + context + ))) + } + ForkName::Altair | ForkName::Bellatrix => { + Self::Altair(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Capella => { + Self::Capella(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Deneb => { + Self::Deneb(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Electra | ForkName::Eip7805 => { + Self::Electra(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Fulu => { + Self::Fulu(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + }) } } diff --git a/consensus/types/src/light_client_update.rs b/consensus/types/src/light_client_update.rs index 4f339d2857..38ff25d8e8 100644 --- a/consensus/types/src/light_client_update.rs +++ b/consensus/types/src/light_client_update.rs @@ -1,8 +1,9 @@ use super::{EthSpec, FixedVector, Hash256, Slot, SyncAggregate, SyncCommittee}; +use crate::context_deserialize; use crate::light_client_header::LightClientHeaderElectra; use crate::LightClientHeader; use crate::{ - beacon_state, test_utils::TestRandom, ChainSpec, Epoch, ForkName, ForkVersionDeserialize, + beacon_state, test_utils::TestRandom, ChainSpec, ContextDeserialize, Epoch, ForkName, LightClientHeaderAltair, LightClientHeaderCapella, LightClientHeaderDeneb, LightClientHeaderFulu, SignedBlindedBeaconBlock, }; @@ -10,7 +11,6 @@ use derivative::Derivative; use safe_arith::ArithError; use safe_arith::SafeArith; use serde::{Deserialize, Deserializer, Serialize}; -use serde_json::Value; use ssz::{Decode, Encode}; use ssz_derive::Decode; use ssz_derive::Encode; @@ -117,11 +117,10 @@ impl From for Error { ), serde(bound = "E: EthSpec", deny_unknown_fields), arbitrary(bound = "E: EthSpec"), + context_deserialize(ForkName), ) )] -#[derive( - Debug, Clone, Serialize, Encode, TreeHash, Deserialize, arbitrary::Arbitrary, PartialEq, -)] +#[derive(Debug, Clone, Serialize, Encode, TreeHash, arbitrary::Arbitrary, PartialEq)] #[serde(untagged)] #[tree_hash(enum_behaviour = "transparent")] #[ssz(enum_behaviour = "transparent")] @@ -180,19 +179,37 @@ pub struct LightClientUpdate { pub signature_slot: Slot, } -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<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for LightClientUpdate { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { + let convert_err = |e| { + serde::de::Error::custom(format!("LightClientUpdate failed to deserialize: {:?}", e)) + }; + Ok(match context { + ForkName::Base => { + return Err(serde::de::Error::custom(format!( + "LightClientUpdate failed to deserialize: unsupported fork '{}'", + context + ))) + } + ForkName::Altair | ForkName::Bellatrix => { + Self::Altair(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Capella => { + Self::Capella(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Deneb => { + Self::Deneb(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Electra | ForkName::Eip7805 => { + Self::Electra(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Fulu => { + Self::Fulu(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + }) } } diff --git a/consensus/types/src/payload.rs b/consensus/types/src/payload.rs index 02bbaeb22f..03b542cef5 100644 --- a/consensus/types/src/payload.rs +++ b/consensus/types/src/payload.rs @@ -86,6 +86,7 @@ pub trait AbstractExecPayload: + TryInto + TryInto + TryInto + + Sync { type Ref<'a>: ExecPayload + Copy @@ -99,27 +100,33 @@ pub trait AbstractExecPayload: type Bellatrix: OwnedExecPayload + Into + for<'a> From>> - + TryFrom>; + + TryFrom> + + Sync; type Capella: OwnedExecPayload + Into + for<'a> From>> - + TryFrom>; + + TryFrom> + + Sync; type Deneb: OwnedExecPayload + Into + for<'a> From>> - + TryFrom>; + + TryFrom> + + Sync; type Electra: OwnedExecPayload + Into + for<'a> From>> - + TryFrom>; + + TryFrom> + + Sync; type Eip7805: OwnedExecPayload + Into + for<'a> From>> - + TryFrom>; + + TryFrom> + + Sync; type Fulu: OwnedExecPayload + Into + for<'a> From>> - + TryFrom>; + + TryFrom> + + Sync; } #[superstruct( diff --git a/consensus/types/src/pending_attestation.rs b/consensus/types/src/pending_attestation.rs index 0bccab5079..b7b4a19f4b 100644 --- a/consensus/types/src/pending_attestation.rs +++ b/consensus/types/src/pending_attestation.rs @@ -1,6 +1,6 @@ +use crate::context_deserialize; use crate::test_utils::TestRandom; -use crate::{AttestationData, BitList, EthSpec}; - +use crate::{AttestationData, BitList, EthSpec, ForkName}; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; @@ -22,6 +22,7 @@ use tree_hash_derive::TreeHash; arbitrary::Arbitrary, )] #[arbitrary(bound = "E: EthSpec")] +#[context_deserialize(ForkName)] pub struct PendingAttestation { pub aggregation_bits: BitList, pub data: AttestationData, diff --git a/consensus/types/src/pending_consolidation.rs b/consensus/types/src/pending_consolidation.rs index 6e0b74a738..9a513f2744 100644 --- a/consensus/types/src/pending_consolidation.rs +++ b/consensus/types/src/pending_consolidation.rs @@ -1,4 +1,6 @@ +use crate::context_deserialize; use crate::test_utils::TestRandom; +use crate::ForkName; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; @@ -18,6 +20,7 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] +#[context_deserialize(ForkName)] pub struct PendingConsolidation { #[serde(with = "serde_utils::quoted_u64")] pub source_index: u64, diff --git a/consensus/types/src/pending_deposit.rs b/consensus/types/src/pending_deposit.rs index 3bee86417d..970c326467 100644 --- a/consensus/types/src/pending_deposit.rs +++ b/consensus/types/src/pending_deposit.rs @@ -18,6 +18,7 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] +#[context_deserialize(ForkName)] pub struct PendingDeposit { pub pubkey: PublicKeyBytes, pub withdrawal_credentials: Hash256, diff --git a/consensus/types/src/pending_partial_withdrawal.rs b/consensus/types/src/pending_partial_withdrawal.rs index 846dd97360..ca49032859 100644 --- a/consensus/types/src/pending_partial_withdrawal.rs +++ b/consensus/types/src/pending_partial_withdrawal.rs @@ -1,5 +1,6 @@ +use crate::context_deserialize; use crate::test_utils::TestRandom; -use crate::Epoch; +use crate::{Epoch, ForkName}; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; @@ -19,6 +20,7 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] +#[context_deserialize(ForkName)] pub struct PendingPartialWithdrawal { #[serde(with = "serde_utils::quoted_u64")] pub validator_index: u64, diff --git a/consensus/types/src/preset.rs b/consensus/types/src/preset.rs index 98fa46029f..6ad9aaa4e5 100644 --- a/consensus/types/src/preset.rs +++ b/consensus/types/src/preset.rs @@ -227,28 +227,36 @@ pub struct ElectraPreset { pub min_activation_balance: u64, #[serde(with = "serde_utils::quoted_u64")] pub max_effective_balance_electra: u64, + #[serde(with = "serde_utils::quoted_u64")] pub min_slashing_penalty_quotient_electra: u64, #[serde(with = "serde_utils::quoted_u64")] pub whistleblower_reward_quotient_electra: u64, - #[serde(with = "serde_utils::quoted_u64")] - pub max_pending_partials_per_withdrawals_sweep: u64, + #[serde(with = "serde_utils::quoted_u64")] pub pending_deposits_limit: u64, #[serde(with = "serde_utils::quoted_u64")] pub pending_partial_withdrawals_limit: u64, #[serde(with = "serde_utils::quoted_u64")] pub pending_consolidations_limit: u64, - #[serde(with = "serde_utils::quoted_u64")] - pub max_consolidation_requests_per_payload: u64, - #[serde(with = "serde_utils::quoted_u64")] - pub max_deposit_requests_per_payload: u64, + #[serde(with = "serde_utils::quoted_u64")] pub max_attester_slashings_electra: u64, #[serde(with = "serde_utils::quoted_u64")] pub max_attestations_electra: u64, + + #[serde(with = "serde_utils::quoted_u64")] + pub max_deposit_requests_per_payload: u64, #[serde(with = "serde_utils::quoted_u64")] pub max_withdrawal_requests_per_payload: u64, + #[serde(with = "serde_utils::quoted_u64")] + pub max_consolidation_requests_per_payload: u64, + + #[serde(with = "serde_utils::quoted_u64")] + pub max_pending_partials_per_withdrawals_sweep: u64, + + #[serde(with = "serde_utils::quoted_u64")] + pub max_pending_deposits_per_epoch: u64, } impl ElectraPreset { @@ -256,19 +264,26 @@ impl ElectraPreset { Self { min_activation_balance: spec.min_activation_balance, max_effective_balance_electra: spec.max_effective_balance_electra, + min_slashing_penalty_quotient_electra: spec.min_slashing_penalty_quotient_electra, whistleblower_reward_quotient_electra: spec.whistleblower_reward_quotient_electra, - max_pending_partials_per_withdrawals_sweep: spec - .max_pending_partials_per_withdrawals_sweep, + pending_deposits_limit: E::pending_deposits_limit() as u64, pending_partial_withdrawals_limit: E::pending_partial_withdrawals_limit() as u64, pending_consolidations_limit: E::pending_consolidations_limit() as u64, - max_consolidation_requests_per_payload: E::max_consolidation_requests_per_payload() - as u64, - max_deposit_requests_per_payload: E::max_deposit_requests_per_payload() as u64, + max_attester_slashings_electra: E::max_attester_slashings_electra() as u64, max_attestations_electra: E::max_attestations_electra() as u64, + + max_deposit_requests_per_payload: E::max_deposit_requests_per_payload() as u64, max_withdrawal_requests_per_payload: E::max_withdrawal_requests_per_payload() as u64, + max_consolidation_requests_per_payload: E::max_consolidation_requests_per_payload() + as u64, + + max_pending_partials_per_withdrawals_sweep: spec + .max_pending_partials_per_withdrawals_sweep, + + max_pending_deposits_per_epoch: E::max_pending_deposits_per_epoch() as u64, } } } diff --git a/consensus/types/src/proposer_slashing.rs b/consensus/types/src/proposer_slashing.rs index ee55d62c20..7b03dbb83e 100644 --- a/consensus/types/src/proposer_slashing.rs +++ b/consensus/types/src/proposer_slashing.rs @@ -1,5 +1,6 @@ +use crate::context_deserialize; use crate::test_utils::TestRandom; -use crate::SignedBeaconBlockHeader; +use crate::{ForkName, SignedBeaconBlockHeader}; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; @@ -23,6 +24,7 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] +#[context_deserialize(ForkName)] pub struct ProposerSlashing { pub signed_header_1: SignedBeaconBlockHeader, pub signed_header_2: SignedBeaconBlockHeader, diff --git a/consensus/types/src/runtime_var_list.rs b/consensus/types/src/runtime_var_list.rs index d6b1c10e99..454c8b9e18 100644 --- a/consensus/types/src/runtime_var_list.rs +++ b/consensus/types/src/runtime_var_list.rs @@ -1,5 +1,7 @@ +use crate::ContextDeserialize; use derivative::Derivative; -use serde::{Deserialize, Serialize}; +use serde::de::Error as DeError; +use serde::{Deserialize, Deserializer, Serialize}; use ssz::Decode; use ssz_types::Error; use std::ops::{Deref, Index, IndexMut}; @@ -217,6 +219,28 @@ where } } +impl<'de, C, T> ContextDeserialize<'de, (C, usize)> for RuntimeVariableList +where + T: ContextDeserialize<'de, C>, + C: Clone, +{ + fn context_deserialize(deserializer: D, context: (C, usize)) -> Result + where + D: Deserializer<'de>, + { + // first parse out a Vec using the Vec impl you already have + let vec: Vec = Vec::context_deserialize(deserializer, context.0)?; + if vec.len() > context.1 { + return Err(DeError::custom(format!( + "RuntimeVariableList lengh {} exceeds max_len {}", + vec.len(), + context.1 + ))); + } + Ok(RuntimeVariableList::from_vec(vec, context.1)) + } +} + #[cfg(test)] mod test { use super::*; diff --git a/consensus/types/src/signed_aggregate_and_proof.rs b/consensus/types/src/signed_aggregate_and_proof.rs index 26eca19bf1..7b1f97e521 100644 --- a/consensus/types/src/signed_aggregate_and_proof.rs +++ b/consensus/types/src/signed_aggregate_and_proof.rs @@ -2,11 +2,11 @@ use super::{ AggregateAndProof, AggregateAndProofBase, AggregateAndProofElectra, AggregateAndProofRef, }; use super::{ - AttestationRef, ChainSpec, Domain, EthSpec, Fork, Hash256, SecretKey, SelectionProof, - Signature, SignedRoot, + Attestation, AttestationRef, ChainSpec, Domain, EthSpec, Fork, ForkName, Hash256, SecretKey, + SelectionProof, Signature, SignedRoot, }; +use crate::context_deserialize; use crate::test_utils::TestRandom; -use crate::Attestation; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use superstruct::superstruct; @@ -32,6 +32,7 @@ use tree_hash_derive::TreeHash; TestRandom, TreeHash, ), + context_deserialize(ForkName), serde(bound = "E: EthSpec"), arbitrary(bound = "E: EthSpec"), ), diff --git a/consensus/types/src/signed_beacon_block.rs b/consensus/types/src/signed_beacon_block.rs index 1d059ab3b3..37c938f8f7 100644 --- a/consensus/types/src/signed_beacon_block.rs +++ b/consensus/types/src/signed_beacon_block.rs @@ -1,11 +1,13 @@ use crate::beacon_block_body::{format_kzg_commitments, BLOB_KZG_COMMITMENTS_INDEX}; +use crate::test_utils::TestRandom; use crate::*; use derivative::Derivative; use merkle_proof::MerkleTree; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use ssz_derive::{Decode, Encode}; use std::fmt; use superstruct::superstruct; +use test_random_derive::TestRandom; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; @@ -49,7 +51,8 @@ impl From for Hash256 { Decode, TreeHash, Derivative, - arbitrary::Arbitrary + arbitrary::Arbitrary, + TestRandom ), derivative(PartialEq, Hash(bound = "E: EthSpec")), serde(bound = "E: EthSpec, Payload: AbstractExecPayload"), @@ -767,20 +770,17 @@ impl SignedBeaconBlock { } } -impl> ForkVersionDeserialize +impl<'de, E: EthSpec, Payload: AbstractExecPayload> ContextDeserialize<'de, ForkName> for SignedBeaconBlock { - fn deserialize_by_fork<'de, D: serde::Deserializer<'de>>( - value: serde_json::value::Value, - fork_name: ForkName, - ) -> Result { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { Ok(map_fork_name!( - fork_name, + context, Self, - serde_json::from_value(value).map_err(|e| serde::de::Error::custom(format!( - "SignedBeaconBlock failed to deserialize: {:?}", - e - )))? + serde::Deserialize::deserialize(deserializer)? )) } } diff --git a/consensus/types/src/signed_beacon_block_header.rs b/consensus/types/src/signed_beacon_block_header.rs index 3d4269a2ce..9106fa8372 100644 --- a/consensus/types/src/signed_beacon_block_header.rs +++ b/consensus/types/src/signed_beacon_block_header.rs @@ -1,5 +1,6 @@ +use crate::context_deserialize; use crate::{ - test_utils::TestRandom, BeaconBlockHeader, ChainSpec, Domain, EthSpec, Fork, Hash256, + test_utils::TestRandom, BeaconBlockHeader, ChainSpec, Domain, EthSpec, Fork, ForkName, Hash256, PublicKey, Signature, SignedRoot, }; use serde::{Deserialize, Serialize}; @@ -24,6 +25,7 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] +#[context_deserialize(ForkName)] pub struct SignedBeaconBlockHeader { pub message: BeaconBlockHeader, pub signature: Signature, diff --git a/consensus/types/src/signed_bls_to_execution_change.rs b/consensus/types/src/signed_bls_to_execution_change.rs index a7bfd7c271..383663e36b 100644 --- a/consensus/types/src/signed_bls_to_execution_change.rs +++ b/consensus/types/src/signed_bls_to_execution_change.rs @@ -19,6 +19,7 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] +#[context_deserialize(ForkName)] pub struct SignedBlsToExecutionChange { pub message: BlsToExecutionChange, pub signature: Signature, diff --git a/consensus/types/src/signed_contribution_and_proof.rs b/consensus/types/src/signed_contribution_and_proof.rs index 068fd980ae..42115bfbc0 100644 --- a/consensus/types/src/signed_contribution_and_proof.rs +++ b/consensus/types/src/signed_contribution_and_proof.rs @@ -1,7 +1,8 @@ use super::{ - ChainSpec, ContributionAndProof, Domain, EthSpec, Fork, Hash256, SecretKey, Signature, - SignedRoot, SyncCommitteeContribution, SyncSelectionProof, + ChainSpec, ContributionAndProof, Domain, EthSpec, Fork, ForkName, Hash256, SecretKey, + Signature, SignedRoot, SyncCommitteeContribution, SyncSelectionProof, }; +use crate::context_deserialize; use crate::test_utils::TestRandom; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; @@ -24,6 +25,7 @@ use tree_hash_derive::TreeHash; )] #[serde(bound = "E: EthSpec")] #[arbitrary(bound = "E: EthSpec")] +#[context_deserialize(ForkName)] pub struct SignedContributionAndProof { /// The `ContributionAndProof` that was signed. pub message: ContributionAndProof, diff --git a/consensus/types/src/signed_voluntary_exit.rs b/consensus/types/src/signed_voluntary_exit.rs index 30eda11791..b6451d3ab5 100644 --- a/consensus/types/src/signed_voluntary_exit.rs +++ b/consensus/types/src/signed_voluntary_exit.rs @@ -1,4 +1,5 @@ -use crate::{test_utils::TestRandom, VoluntaryExit}; +use crate::context_deserialize; +use crate::{test_utils::TestRandom, ForkName, VoluntaryExit}; use bls::Signature; use serde::{Deserialize, Serialize}; @@ -22,6 +23,7 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] +#[context_deserialize(ForkName)] pub struct SignedVoluntaryExit { pub message: VoluntaryExit, pub signature: Signature, diff --git a/consensus/types/src/signing_data.rs b/consensus/types/src/signing_data.rs index f30d5fdfcb..aa25ecffd9 100644 --- a/consensus/types/src/signing_data.rs +++ b/consensus/types/src/signing_data.rs @@ -1,5 +1,6 @@ +use crate::context_deserialize; use crate::test_utils::TestRandom; -use crate::Hash256; +use crate::{ForkName, Hash256}; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; @@ -19,6 +20,7 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] +#[context_deserialize(ForkName)] pub struct SigningData { pub object_root: Hash256, pub domain: Hash256, diff --git a/consensus/types/src/sync_aggregate.rs b/consensus/types/src/sync_aggregate.rs index 12b91501ae..4f810db22a 100644 --- a/consensus/types/src/sync_aggregate.rs +++ b/consensus/types/src/sync_aggregate.rs @@ -1,6 +1,7 @@ use crate::consts::altair::SYNC_COMMITTEE_SUBNET_COUNT; +use crate::context_deserialize; use crate::test_utils::TestRandom; -use crate::{AggregateSignature, BitVector, EthSpec, SyncCommitteeContribution}; +use crate::{AggregateSignature, BitVector, EthSpec, ForkName, SyncCommitteeContribution}; use derivative::Derivative; use safe_arith::{ArithError, SafeArith}; use serde::{Deserialize, Serialize}; @@ -36,6 +37,7 @@ impl From for Error { #[derivative(PartialEq, Hash(bound = "E: EthSpec"))] #[serde(bound = "E: EthSpec")] #[arbitrary(bound = "E: EthSpec")] +#[context_deserialize(ForkName)] pub struct SyncAggregate { pub sync_committee_bits: BitVector, pub sync_committee_signature: AggregateSignature, diff --git a/consensus/types/src/sync_aggregator_selection_data.rs b/consensus/types/src/sync_aggregator_selection_data.rs index 3da130bb06..a61cd47d04 100644 --- a/consensus/types/src/sync_aggregator_selection_data.rs +++ b/consensus/types/src/sync_aggregator_selection_data.rs @@ -1,6 +1,6 @@ +use crate::context_deserialize; use crate::test_utils::TestRandom; -use crate::{SignedRoot, Slot}; - +use crate::{ForkName, SignedRoot, Slot}; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; @@ -19,6 +19,7 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] +#[context_deserialize(ForkName)] pub struct SyncAggregatorSelectionData { pub slot: Slot, #[serde(with = "serde_utils::quoted_u64")] diff --git a/consensus/types/src/sync_committee.rs b/consensus/types/src/sync_committee.rs index 032f0d61f9..c7ec7bdcc3 100644 --- a/consensus/types/src/sync_committee.rs +++ b/consensus/types/src/sync_committee.rs @@ -1,5 +1,6 @@ +use crate::context_deserialize; use crate::test_utils::TestRandom; -use crate::{EthSpec, FixedVector, SyncSubnetId}; +use crate::{EthSpec, FixedVector, ForkName, SyncSubnetId}; use bls::PublicKeyBytes; use safe_arith::{ArithError, SafeArith}; use serde::{Deserialize, Serialize}; @@ -38,6 +39,7 @@ impl From for Error { )] #[serde(bound = "E: EthSpec")] #[arbitrary(bound = "E: EthSpec")] +#[context_deserialize(ForkName)] pub struct SyncCommittee { pub pubkeys: FixedVector, pub aggregate_pubkey: PublicKeyBytes, diff --git a/consensus/types/src/sync_committee_contribution.rs b/consensus/types/src/sync_committee_contribution.rs index e160332f45..e2ac414cfa 100644 --- a/consensus/types/src/sync_committee_contribution.rs +++ b/consensus/types/src/sync_committee_contribution.rs @@ -1,4 +1,5 @@ -use super::{AggregateSignature, EthSpec, SignedRoot}; +use super::{AggregateSignature, EthSpec, ForkName, SignedRoot}; +use crate::context_deserialize; use crate::slot_data::SlotData; use crate::{test_utils::TestRandom, BitVector, Hash256, Slot, SyncCommitteeMessage}; use serde::{Deserialize, Serialize}; @@ -28,6 +29,7 @@ pub enum Error { )] #[serde(bound = "E: EthSpec")] #[arbitrary(bound = "E: EthSpec")] +#[context_deserialize(ForkName)] pub struct SyncCommitteeContribution { pub slot: Slot, pub beacon_block_root: Hash256, diff --git a/consensus/types/src/sync_committee_message.rs b/consensus/types/src/sync_committee_message.rs index d7d309cd56..4b442b3053 100644 --- a/consensus/types/src/sync_committee_message.rs +++ b/consensus/types/src/sync_committee_message.rs @@ -1,7 +1,9 @@ -use crate::test_utils::TestRandom; -use crate::{ChainSpec, Domain, EthSpec, Fork, Hash256, SecretKey, Signature, SignedRoot, Slot}; - +use crate::context_deserialize; use crate::slot_data::SlotData; +use crate::test_utils::TestRandom; +use crate::{ + ChainSpec, Domain, EthSpec, Fork, ForkName, Hash256, SecretKey, Signature, SignedRoot, Slot, +}; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; @@ -20,6 +22,7 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] +#[context_deserialize(ForkName)] pub struct SyncCommitteeMessage { pub slot: Slot, pub beacon_block_root: Hash256, diff --git a/consensus/types/src/validator.rs b/consensus/types/src/validator.rs index 5aed90d2c1..165f477ff4 100644 --- a/consensus/types/src/validator.rs +++ b/consensus/types/src/validator.rs @@ -1,3 +1,4 @@ +use crate::context_deserialize; use crate::{ test_utils::TestRandom, Address, BeaconState, ChainSpec, Checkpoint, Epoch, EthSpec, FixedBytesExtended, ForkName, Hash256, PublicKeyBytes, @@ -23,6 +24,7 @@ use tree_hash_derive::TreeHash; TestRandom, TreeHash, )] +#[context_deserialize(ForkName)] pub struct Validator { pub pubkey: PublicKeyBytes, pub withdrawal_credentials: Hash256, @@ -249,7 +251,6 @@ impl Validator { } } - /// TODO(electra): refactor these functions and make it simpler.. this is a mess /// Returns `true` if the validator is partially withdrawable. fn is_partially_withdrawable_validator_capella(&self, balance: u64, spec: &ChainSpec) -> bool { self.has_eth1_withdrawal_credential(spec) diff --git a/consensus/types/src/voluntary_exit.rs b/consensus/types/src/voluntary_exit.rs index 153506f47a..75260add4b 100644 --- a/consensus/types/src/voluntary_exit.rs +++ b/consensus/types/src/voluntary_exit.rs @@ -1,3 +1,4 @@ +use crate::context_deserialize; use crate::{ test_utils::TestRandom, ChainSpec, Domain, Epoch, ForkName, Hash256, SecretKey, SignedRoot, SignedVoluntaryExit, @@ -24,6 +25,7 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] +#[context_deserialize(ForkName)] pub struct VoluntaryExit { /// Earliest epoch when voluntary exit can be processed. pub epoch: Epoch, @@ -40,6 +42,16 @@ impl VoluntaryExit { genesis_validators_root: Hash256, spec: &ChainSpec, ) -> SignedVoluntaryExit { + let domain = self.get_domain(genesis_validators_root, spec); + + let message = self.signing_root(domain); + SignedVoluntaryExit { + message: self, + signature: secret_key.sign(message), + } + } + + pub fn get_domain(&self, genesis_validators_root: Hash256, spec: &ChainSpec) -> Hash256 { let fork_name = spec.fork_name_at_epoch(self.epoch); let fork_version = if fork_name.deneb_enabled() { // EIP-7044 @@ -47,14 +59,7 @@ impl VoluntaryExit { } else { spec.fork_version_for_name(fork_name) }; - let domain = - spec.compute_domain(Domain::VoluntaryExit, fork_version, genesis_validators_root); - - let message = self.signing_root(domain); - SignedVoluntaryExit { - message: self, - signature: secret_key.sign(message), - } + spec.compute_domain(Domain::VoluntaryExit, fork_version, genesis_validators_root) } } diff --git a/consensus/types/src/withdrawal.rs b/consensus/types/src/withdrawal.rs index 7f98ff1e60..9ca50fccfb 100644 --- a/consensus/types/src/withdrawal.rs +++ b/consensus/types/src/withdrawal.rs @@ -19,6 +19,7 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] +#[context_deserialize(ForkName)] pub struct Withdrawal { #[serde(with = "serde_utils::quoted_u64")] pub index: u64, diff --git a/consensus/types/src/withdrawal_request.rs b/consensus/types/src/withdrawal_request.rs index 1296426ac0..57c6e798eb 100644 --- a/consensus/types/src/withdrawal_request.rs +++ b/consensus/types/src/withdrawal_request.rs @@ -1,5 +1,6 @@ +use crate::context_deserialize; use crate::test_utils::TestRandom; -use crate::{Address, PublicKeyBytes}; +use crate::{Address, ForkName, PublicKeyBytes}; use serde::{Deserialize, Serialize}; use ssz::Encode; use ssz_derive::{Decode, Encode}; @@ -20,6 +21,7 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] +#[context_deserialize(ForkName)] pub struct WithdrawalRequest { #[serde(with = "serde_utils::address_hex")] pub source_address: Address, diff --git a/crypto/kzg/src/lib.rs b/crypto/kzg/src/lib.rs index 2a5c6e47f5..5d752cc0a5 100644 --- a/crypto/kzg/src/lib.rs +++ b/crypto/kzg/src/lib.rs @@ -220,7 +220,7 @@ impl Kzg { .map_err(Into::into) } - /// Computes the cells and associated proofs for a given `blob` at index `index`. + /// Computes the cells and associated proofs for a given `blob`. pub fn compute_cells_and_proofs( &self, blob: KzgBlobRef<'_>, @@ -235,11 +235,14 @@ impl Kzg { Ok((cells, c_kzg_proof)) } + /// Computes the cells for a given `blob`. + pub fn compute_cells(&self, blob: KzgBlobRef<'_>) -> Result<[Cell; CELLS_PER_EXT_BLOB], Error> { + self.context() + .compute_cells(blob) + .map_err(Error::PeerDASKZG) + } + /// Verifies a batch of cell-proof-commitment triplets. - /// - /// Here, `coordinates` correspond to the (row, col) coordinate of the cell in the extended - /// blob "matrix". In the 1D extension, row corresponds to the blob index, and col corresponds - /// to the data column index. pub fn verify_cell_proof_batch( &self, cells: &[CellRef<'_>], diff --git a/lcli/Cargo.toml b/lcli/Cargo.toml index 22b19f7413..9acbe2569c 100644 --- a/lcli/Cargo.toml +++ b/lcli/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "lcli" description = "Lighthouse CLI (modeled after zcli)" -version = "7.0.0-beta.5" +version = "7.1.0-beta.0" authors = ["Paul Hauner "] edition = { workspace = true } diff --git a/lcli/src/block_root.rs b/lcli/src/block_root.rs index 80087fd6d4..3c07d4f9ef 100644 --- a/lcli/src/block_root.rs +++ b/lcli/src/block_root.rs @@ -79,7 +79,7 @@ pub fn run( .await .map_err(|e| format!("Failed to download block: {:?}", e))? .ok_or_else(|| format!("Unable to locate block at {:?}", block_id))? - .data; + .into_data(); Ok::<_, String>(block) }) .map_err(|e| format!("Failed to complete task: {:?}", e))? diff --git a/lcli/src/http_sync.rs b/lcli/src/http_sync.rs index 1ef40e6397..cb6a9d2b1d 100644 --- a/lcli/src/http_sync.rs +++ b/lcli/src/http_sync.rs @@ -123,11 +123,11 @@ async fn get_block_from_source( .unwrap() .unwrap(); let blobs_from_source = source - .get_blobs::(block_id, None) + .get_blobs::(block_id, None, spec) .await .unwrap() .unwrap() - .data; + .into_data(); let (kzg_proofs, blobs): (Vec<_>, Vec<_>) = blobs_from_source .iter() diff --git a/lcli/src/skip_slots.rs b/lcli/src/skip_slots.rs index 834123e939..9456f34570 100644 --- a/lcli/src/skip_slots.rs +++ b/lcli/src/skip_slots.rs @@ -102,7 +102,7 @@ pub fn run( }) .map_err(|e| format!("Failed to complete task: {:?}", e))? .ok_or_else(|| format!("Unable to locate state at {:?}", state_id))? - .data; + .into_data(); let state_root = match state_id { StateId::Root(root) => Some(root), _ => None, diff --git a/lcli/src/state_root.rs b/lcli/src/state_root.rs index b2308999d4..7b10ab9362 100644 --- a/lcli/src/state_root.rs +++ b/lcli/src/state_root.rs @@ -50,7 +50,7 @@ pub fn run( }) .map_err(|e| format!("Failed to complete task: {:?}", e))? .ok_or_else(|| format!("Unable to locate state at {:?}", state_id))? - .data + .into_data() } _ => return Err("must supply either --state-path or --beacon-url".into()), }; diff --git a/lcli/src/transition_blocks.rs b/lcli/src/transition_blocks.rs index 4831f86491..2226105c34 100644 --- a/lcli/src/transition_blocks.rs +++ b/lcli/src/transition_blocks.rs @@ -154,7 +154,7 @@ pub fn run( .await .map_err(|e| format!("Failed to download block: {:?}", e))? .ok_or_else(|| format!("Unable to locate block at {:?}", block_id))? - .data; + .into_data(); if block.slot() == inner_spec.genesis_slot { return Err("Cannot run on the genesis block".to_string()); @@ -165,7 +165,7 @@ pub fn run( .await .map_err(|e| format!("Failed to download parent block: {:?}", e))? .ok_or_else(|| format!("Unable to locate parent block at {:?}", block_id))? - .data; + .into_data(); let state_root = parent_block.state_root(); let state_id = StateId::Root(state_root); @@ -174,7 +174,7 @@ pub fn run( .await .map_err(|e| format!("Failed to download state: {:?}", e))? .ok_or_else(|| format!("Unable to locate state at {:?}", state_id))? - .data; + .into_data(); Ok((pre_state, Some(state_root), block)) }) diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 3774a9c458..04c8efcdba 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lighthouse" -version = "7.0.0-beta.5" +version = "7.1.0-beta.0" authors = ["Sigma Prime "] edition = { workspace = true } autotests = false diff --git a/lighthouse/environment/src/tracing_common.rs b/lighthouse/environment/src/tracing_common.rs index dd9fe45cad..b1e5078af1 100644 --- a/lighthouse/environment/src/tracing_common.rs +++ b/lighthouse/environment/src/tracing_common.rs @@ -37,7 +37,11 @@ pub fn construct_logger( environment_builder.init_tracing(logger_config.clone(), logfile_prefix); let libp2p_discv5_layer = if let Some(subcommand_name) = subcommand_name { - if subcommand_name == "beacon_node" || subcommand_name == "boot_node" { + if subcommand_name == "beacon_node" + || subcommand_name == "boot_node" + || subcommand_name == "basic-sim" + || subcommand_name == "fallback-sim" + { if logger_config.max_log_size == 0 || logger_config.max_log_number == 0 { // User has explicitly disabled logging to file. None diff --git a/lighthouse/environment/tests/testnet_dir/config.yaml b/lighthouse/environment/tests/testnet_dir/config.yaml index 34e42a61f6..3f72e2ea6c 100644 --- a/lighthouse/environment/tests/testnet_dir/config.yaml +++ b/lighthouse/environment/tests/testnet_dir/config.yaml @@ -87,9 +87,8 @@ DEPOSIT_CONTRACT_ADDRESS: 0x00000000219ab540356cBB839Cbe05303d7705Fa # Network # --------------------------------------------------------------- SUBNETS_PER_NODE: 2 -GOSSIP_MAX_SIZE: 10485760 +MAX_PAYLOAD_SIZE: 10485760 MIN_EPOCHS_FOR_BLOCK_REQUESTS: 33024 -MAX_CHUNK_SIZE: 10485760 TTFB_TIMEOUT: 5 RESP_TIMEOUT: 10 MESSAGE_DOMAIN_INVALID_SNAPPY: 0x00000000 diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index 66dae05326..7ddf04db01 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -68,6 +68,9 @@ fn bls_hardware_acceleration() -> bool { #[cfg(target_arch = "aarch64")] return std::arch::is_aarch64_feature_detected!("neon"); + + #[cfg(target_arch = "riscv64")] + return false; } fn allocator_name() -> String { diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index aad11c50d7..ea4716c010 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -1274,7 +1274,7 @@ fn default_backfill_rate_limiting_flag() { } #[test] fn default_boot_nodes() { - let number_of_boot_nodes = 15; + let number_of_boot_nodes = 17; CommandLineTest::new() .run_with_zero_port() diff --git a/lighthouse/tests/validator_client.rs b/lighthouse/tests/validator_client.rs index eccd97d486..f99fc3c460 100644 --- a/lighthouse/tests/validator_client.rs +++ b/lighthouse/tests/validator_client.rs @@ -70,6 +70,22 @@ fn validators_and_secrets_dir_flags() { }); } +#[test] +fn datadir_and_secrets_dir_flags() { + let dir = TempDir::new().expect("Unable to create temporary directory"); + CommandLineTest::new() + .flag("datadir", dir.path().join("data").to_str()) + .flag("secrets-dir", dir.path().join("secrets").to_str()) + .run_with_no_datadir() + .with_config(|config| { + assert_eq!( + config.validator_dir, + dir.path().join("data").join("validators") + ); + assert_eq!(config.secrets_dir, dir.path().join("secrets")); + }); +} + #[test] fn validators_dir_alias_flags() { let dir = TempDir::new().expect("Unable to create temporary directory"); @@ -301,6 +317,14 @@ fn missing_unencrypted_http_transport_flag() { .with_config(|config| assert_eq!(config.http_api.listen_addr, addr)); } #[test] +#[should_panic] +fn missing_http_http_port_flag() { + CommandLineTest::new() + .flag("http-port", Some("9090")) + .run() + .with_config(|config| assert_eq!(config.http_api.listen_port, 9090)); +} +#[test] fn http_port_flag() { CommandLineTest::new() .flag("http", None) @@ -481,7 +505,7 @@ fn no_doppelganger_protection_flag() { fn no_gas_limit_flag() { CommandLineTest::new() .run() - .with_config(|config| assert!(config.validator_store.gas_limit == Some(30_000_000))); + .with_config(|config| assert!(config.validator_store.gas_limit == Some(36_000_000))); } #[test] fn gas_limit_flag() { diff --git a/scripts/local_testnet/README.md b/scripts/local_testnet/README.md index 159c89badb..9d9844c4c4 100644 --- a/scripts/local_testnet/README.md +++ b/scripts/local_testnet/README.md @@ -83,3 +83,7 @@ The script comes with some CLI options, which can be viewed with `./start_local_ ```bash ./start_local_testnet.sh -b false ``` + +## Further reading about Kurtosis + +You may refer to [this article](https://ethpandaops.io/posts/kurtosis-deep-dive/) for information about Kurtosis. \ No newline at end of file diff --git a/scripts/local_testnet/network_params.yaml b/scripts/local_testnet/network_params.yaml index 87ffeb8d22..e671340afb 100644 --- a/scripts/local_testnet/network_params.yaml +++ b/scripts/local_testnet/network_params.yaml @@ -14,5 +14,5 @@ global_log_level: debug snooper_enabled: false additional_services: - dora - - spamoor_blob + - spamoor - prometheus_grafana diff --git a/scripts/local_testnet/network_params_das.yaml b/scripts/local_testnet/network_params_das.yaml index 80b4bc95c6..628b2696a5 100644 --- a/scripts/local_testnet/network_params_das.yaml +++ b/scripts/local_testnet/network_params_das.yaml @@ -1,29 +1,37 @@ participants: - cl_type: lighthouse cl_image: lighthouse:local + el_image: ethpandaops/geth:marius-engine-getblobs-v2 cl_extra_params: - --subscribe-all-data-column-subnets - --subscribe-all-subnets - # Note: useful for testing range sync (only produce block if node is in sync to prevent forking) + # Note: useful for testing range sync (only produce block if the node is in sync to prevent forking) - --sync-tolerance-epochs=0 - --target-peers=3 count: 2 - cl_type: lighthouse cl_image: lighthouse:local + el_image: ethpandaops/geth:marius-engine-getblobs-v2 cl_extra_params: - # Note: useful for testing range sync (only produce block if node is in sync to prevent forking) + # Note: useful for testing range sync (only produce block if the node is in sync to prevent forking) - --sync-tolerance-epochs=0 - --target-peers=3 count: 2 network_params: - electra_fork_epoch: 1 - fulu_fork_epoch: 2 + electra_fork_epoch: 0 + fulu_fork_epoch: 1 seconds_per_slot: 6 snooper_enabled: false global_log_level: debug additional_services: - dora - - spamoor_blob + - spamoor - prometheus_grafana -dora_params: - image: ethpandaops/dora:fulu-support \ No newline at end of file +spamoor_params: + spammers: + - scenario: eoatx + config: + throughput: 200 + - scenario: blobs + config: + throughput: 20 \ No newline at end of file diff --git a/scripts/mdlint.sh b/scripts/mdlint.sh index 5274f108d2..55d8d1f969 100755 --- a/scripts/mdlint.sh +++ b/scripts/mdlint.sh @@ -14,10 +14,10 @@ if [[ $exit_code == 0 ]]; then echo "All markdown files are properly formatted." exit 0 elif [[ $exit_code == 1 ]]; then - echo "Exiting with errors. Run 'make mdlint' locally and commit the changes. Note that not all errors can be fixed automatically, if there are still errors after running 'make mdlint', look for the errors and fix manually." + echo "Exiting with errors. Run 'make mdlint' locally and commit the changes. Note that not all errors can be fixed automatically, if there are still errors after running 'make mdlint', look for the errors and fix manually." docker run --rm -v ./book:/workdir ghcr.io/igorshubovych/markdownlint-cli:latest '**/*.md' --ignore node_modules --fix exit 1 else echo "Exiting with exit code >1. Check for the error logs and fix them accordingly." exit 1 -fi \ No newline at end of file +fi diff --git a/scripts/tests/doppelganger_protection.sh b/scripts/tests/doppelganger_protection.sh index 80070a0791..86c9705ee4 100755 --- a/scripts/tests/doppelganger_protection.sh +++ b/scripts/tests/doppelganger_protection.sh @@ -74,18 +74,27 @@ if [[ "$BEHAVIOR" == "failure" ]]; then vc_1_keys_artifact_id="1-lighthouse-geth-$vc_1_range_start-$vc_1_range_end" service_name=vc-1-doppelganger - kurtosis service add \ - --files /validator_keys:$vc_1_keys_artifact_id,/testnet:el_cl_genesis_data \ - $ENCLAVE_NAME $service_name $LH_IMAGE_NAME -- lighthouse \ - vc \ - --debug-level info \ - --testnet-dir=/testnet \ - --validators-dir=/validator_keys/keys \ - --secrets-dir=/validator_keys/secrets \ - --init-slashing-protection \ - --beacon-nodes=http://$bn_2_url:$bn_2_port \ - --enable-doppelganger-protection \ - --suggested-fee-recipient 0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990 + kurtosis service add $ENCLAVE_NAME $service_name --json-service-config - << EOF + { + "image": "$LH_IMAGE_NAME", + "files": { + "/validator_keys": ["$vc_1_keys_artifact_id"], + "/testnet": ["el_cl_genesis_data"] + }, + "cmd": [ + "lighthouse", + "vc", + "--debug-level", "info", + "--testnet-dir=/testnet", + "--validators-dir=/validator_keys/keys", + "--secrets-dir=/validator_keys/secrets", + "--init-slashing-protection", + "--beacon-nodes=http://$bn_2_url:$bn_2_port", + "--enable-doppelganger-protection", + "--suggested-fee-recipient", "0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990" + ] + } +EOF # Check if doppelganger VC has stopped and exited. Exit code 1 means the check timed out and VC is still running. check_exit_cmd="until [ \$(get_service_status $service_name) != 'RUNNING' ]; do sleep 1; done" @@ -110,25 +119,34 @@ if [[ "$BEHAVIOR" == "success" ]]; then vc_4_keys_artifact_id="4-lighthouse-geth-$vc_4_range_start-$vc_4_range_end" service_name=vc-4 - kurtosis service add \ - --files /validator_keys:$vc_4_keys_artifact_id,/testnet:el_cl_genesis_data \ - $ENCLAVE_NAME $service_name $LH_IMAGE_NAME -- lighthouse \ - vc \ - --debug-level debug \ - --testnet-dir=/testnet \ - --validators-dir=/validator_keys/keys \ - --secrets-dir=/validator_keys/secrets \ - --init-slashing-protection \ - --beacon-nodes=http://$bn_2_url:$bn_2_port \ - --enable-doppelganger-protection \ - --suggested-fee-recipient 0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990 + kurtosis service add $ENCLAVE_NAME $service_name --json-service-config - << EOF + { + "image": "$LH_IMAGE_NAME", + "files": { + "/validator_keys": ["$vc_4_keys_artifact_id"], + "/testnet": ["el_cl_genesis_data"] + }, + "cmd": [ + "lighthouse", + "vc", + "--debug-level", "info", + "--testnet-dir=/testnet", + "--validators-dir=/validator_keys/keys", + "--secrets-dir=/validator_keys/secrets", + "--init-slashing-protection", + "--beacon-nodes=http://$bn_2_url:$bn_2_port", + "--enable-doppelganger-protection", + "--suggested-fee-recipient", "0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990" + ] + } +EOF doppelganger_failure=0 # Sleep three epochs, then make sure all validators were active in epoch 2. Use # `is_previous_epoch_target_attester` from epoch 3 for a complete view of epoch 2 inclusion. # - # See: https://lighthouse-book.sigmaprime.io/validator-inclusion.html + # See: https://lighthouse-book.sigmaprime.io/api_validator_inclusion.html echo "Waiting three epochs..." sleep $(( $SECONDS_PER_SLOT * 32 * 3 )) @@ -156,7 +174,7 @@ if [[ "$BEHAVIOR" == "success" ]]; then # Sleep two epochs, then make sure all validators were active in epoch 4. Use # `is_previous_epoch_target_attester` from epoch 5 for a complete view of epoch 4 inclusion. # - # See: https://lighthouse-book.sigmaprime.io/validator-inclusion.html + # See: https://lighthouse-book.sigmaprime.io/api_validator_inclusion.html echo "Waiting two more epochs..." sleep $(( $SECONDS_PER_SLOT * 32 * 2 )) for val in 0x*; do diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 43e96e3f1e..b507383190 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -3,6 +3,7 @@ use crate::decode::{ssz_decode_file, ssz_decode_file_with, ssz_decode_state, yam 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::block_verification_types::RpcBlock; use beacon_chain::chain_config::{ DisallowedReOrgOffsets, DEFAULT_RE_ORG_HEAD_THRESHOLD, DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, DEFAULT_RE_ORG_PARENT_THRESHOLD, @@ -519,7 +520,7 @@ impl Tester { let result: Result, _> = self .block_on_dangerous(self.harness.chain.process_block( block_root, - block.clone(), + RpcBlock::new_without_blobs(Some(block_root), block.clone(), 0), NotifyExecutionLayer::Yes, BlockImportSource::Lookup, || Ok(()), diff --git a/testing/ef_tests/src/cases/light_client_verify_is_better_update.rs b/testing/ef_tests/src/cases/light_client_verify_is_better_update.rs index de281d906c..b2afc047c5 100644 --- a/testing/ef_tests/src/cases/light_client_verify_is_better_update.rs +++ b/testing/ef_tests/src/cases/light_client_verify_is_better_update.rs @@ -3,8 +3,7 @@ use decode::ssz_decode_light_client_update; use serde::Deserialize; use types::{LightClientUpdate, Slot}; -#[derive(Debug, Clone, Deserialize)] -#[serde(deny_unknown_fields)] +#[derive(Debug, Clone)] pub struct LightClientVerifyIsBetterUpdate { light_client_updates: Vec>, } diff --git a/testing/ef_tests/src/cases/merkle_proof_validity.rs b/testing/ef_tests/src/cases/merkle_proof_validity.rs index 8f9b0b0381..14779c7e0d 100644 --- a/testing/ef_tests/src/cases/merkle_proof_validity.rs +++ b/testing/ef_tests/src/cases/merkle_proof_validity.rs @@ -23,7 +23,7 @@ pub struct MerkleProof { #[derive(Debug)] pub enum GenericMerkleProofValidity { - BeaconState(BeaconStateMerkleProofValidity), + BeaconState(Box>), BeaconBlockBody(Box>), } @@ -48,6 +48,7 @@ impl LoadCase for GenericMerkleProofValidity { if suite_name == "BeaconState" { BeaconStateMerkleProofValidity::load_from_dir(path, fork_name) + .map(Box::new) .map(GenericMerkleProofValidity::BeaconState) } else if suite_name == "BeaconBlockBody" { BeaconBlockBodyMerkleProofValidity::load_from_dir(path, fork_name) diff --git a/testing/ef_tests/src/type_name.rs b/testing/ef_tests/src/type_name.rs index dfee385958..387e77310d 100644 --- a/testing/ef_tests/src/type_name.rs +++ b/testing/ef_tests/src/type_name.rs @@ -58,7 +58,7 @@ type_name_generic!(BeaconBlockBodyFulu, "BeaconBlockBody"); type_name!(BeaconBlockHeader); type_name_generic!(BeaconState); type_name!(BlobIdentifier); -type_name!(DataColumnIdentifier); +type_name!(DataColumnsByRootIdentifier); type_name_generic!(BlobSidecar); type_name_generic!(DataColumnSidecar); type_name!(Checkpoint); diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 3948708edf..d333cdbb11 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -667,11 +667,13 @@ mod ssz_static { } #[test] - fn data_column_identifier() { - SszStaticHandler::::default() - .run_for_feature(FeatureName::Fulu); - SszStaticHandler::::default() - .run_for_feature(FeatureName::Fulu); + #[ignore] + // TODO(das): enable once EF tests are updated to latest release. + fn data_column_by_root_identifier() { + // SszStaticHandler::::default() + // .run_for_feature(FeatureName::Fulu); + // SszStaticHandler::::default() + // .run_for_feature(FeatureName::Fulu); } #[test] diff --git a/testing/execution_engine_integration/Cargo.toml b/testing/execution_engine_integration/Cargo.toml index 28ff944799..55c42eb9d3 100644 --- a/testing/execution_engine_integration/Cargo.toml +++ b/testing/execution_engine_integration/Cargo.toml @@ -7,7 +7,9 @@ edition = { workspace = true } async-channel = { workspace = true } deposit_contract = { workspace = true } ethers-core = { workspace = true } +ethers-middleware = { workspace = true } ethers-providers = { workspace = true } +ethers-signers = { workspace = true } execution_layer = { workspace = true } fork_choice = { workspace = true } futures = { workspace = true } diff --git a/testing/execution_engine_integration/src/geth.rs b/testing/execution_engine_integration/src/geth.rs index ea143ed433..8c39fda4e3 100644 --- a/testing/execution_engine_integration/src/geth.rs +++ b/testing/execution_engine_integration/src/geth.rs @@ -7,10 +7,7 @@ use std::{env, fs}; use tempfile::TempDir; use unused_port::unused_tcp4_port; -// This is not currently used due to the following breaking changes in geth that requires updating our tests: -// 1. removal of `personal` namespace in v1.14.12: See #30704 -// 2. removal of `totalDifficulty` field from RPC in v1.14.11. See #30386. -// const GETH_BRANCH: &str = "master"; +const GETH_BRANCH: &str = "master"; const GETH_REPO_URL: &str = "https://github.com/ethereum/go-ethereum"; pub fn build_result(repo_dir: &Path) -> Output { @@ -30,14 +27,12 @@ pub fn build(execution_clients_dir: &Path) { } // Get the latest tag on the branch - // let last_release = build_utils::get_latest_release(&repo_dir, GETH_BRANCH).unwrap(); - // Using an older release due to breaking changes in recent releases. See comment on `GETH_BRANCH` const. - let release_tag = "v1.14.10"; - build_utils::checkout(&repo_dir, dbg!(release_tag)).unwrap(); + let last_release = build_utils::get_latest_release(&repo_dir, GETH_BRANCH).unwrap(); + build_utils::checkout(&repo_dir, dbg!(&last_release)).unwrap(); // Build geth build_utils::check_command_output(build_result(&repo_dir), || { - format!("geth make failed using release {release_tag}") + format!("geth make failed using release {last_release}") }); } @@ -102,7 +97,7 @@ impl GenericExecutionEngine for GethEngine { .arg(datadir.path().to_str().unwrap()) .arg("--http") .arg("--http.api") - .arg("engine,eth,personal") + .arg("engine,eth") .arg("--http.port") .arg(http_port.to_string()) .arg("--authrpc.port") diff --git a/testing/execution_engine_integration/src/main.rs b/testing/execution_engine_integration/src/main.rs index efb06833f6..d453c415d4 100644 --- a/testing/execution_engine_integration/src/main.rs +++ b/testing/execution_engine_integration/src/main.rs @@ -32,12 +32,12 @@ fn main() { fn test_geth() { let test_dir = build_utils::prepare_dir(); geth::build(&test_dir); - TestRig::new(GethEngine).perform_tests_blocking(); + TestRig::new(GethEngine, true).perform_tests_blocking(); geth::clean(&test_dir); } fn test_nethermind() { let test_dir = build_utils::prepare_dir(); nethermind::build(&test_dir); - TestRig::new(NethermindEngine).perform_tests_blocking(); + TestRig::new(NethermindEngine, false).perform_tests_blocking(); } diff --git a/testing/execution_engine_integration/src/test_rig.rs b/testing/execution_engine_integration/src/test_rig.rs index cf31c184fe..b0d115960c 100644 --- a/testing/execution_engine_integration/src/test_rig.rs +++ b/testing/execution_engine_integration/src/test_rig.rs @@ -2,7 +2,9 @@ use crate::execution_engine::{ ExecutionEngine, GenericExecutionEngine, ACCOUNT1, ACCOUNT2, KEYSTORE_PASSWORD, PRIVATE_KEYS, }; use crate::transactions::transactions; +use ethers_middleware::SignerMiddleware; use ethers_providers::Middleware; +use ethers_signers::LocalWallet; use execution_layer::test_utils::DEFAULT_GAS_LIMIT; use execution_layer::{ BlockProposalContentsType, BuilderParams, ChainHealth, ExecutionLayer, PayloadAttributes, @@ -44,6 +46,7 @@ pub struct TestRig { ee_b: ExecutionPair, spec: ChainSpec, _runtime_shutdown: async_channel::Sender<()>, + use_local_signing: bool, } /// Import a private key into the execution engine and unlock it so that we can @@ -104,7 +107,7 @@ async fn import_and_unlock(http_url: SensitiveUrl, priv_keys: &[&str], password: } impl TestRig { - pub fn new(generic_engine: Engine) -> Self { + pub fn new(generic_engine: Engine, use_local_signing: bool) -> Self { let runtime = Arc::new( tokio::runtime::Builder::new_multi_thread() .enable_all() @@ -166,6 +169,7 @@ impl TestRig { ee_b, spec, _runtime_shutdown: runtime_shutdown, + use_local_signing, } } @@ -197,15 +201,9 @@ impl TestRig { pub async fn perform_tests(&self) { self.wait_until_synced().await; - // Import and unlock all private keys to sign transactions - let _ = futures::future::join_all([&self.ee_a, &self.ee_b].iter().map(|ee| { - import_and_unlock( - ee.execution_engine.http_url(), - &PRIVATE_KEYS, - KEYSTORE_PASSWORD, - ) - })) - .await; + // Create a local signer in case we need to sign transactions locally + let wallet1: LocalWallet = PRIVATE_KEYS[0].parse().expect("Invalid private key"); + let signer = SignerMiddleware::new(&self.ee_a.execution_engine.provider, wallet1); // We hardcode the accounts here since some EEs start with a default unlocked account let account1 = ethers_core::types::Address::from_slice(&hex::decode(ACCOUNT1).unwrap()); @@ -236,15 +234,38 @@ impl TestRig { // Submit transactions before getting payload let txs = transactions::(account1, account2); let mut pending_txs = Vec::new(); - for tx in txs.clone().into_iter() { - let pending_tx = self - .ee_a - .execution_engine - .provider - .send_transaction(tx, None) - .await - .unwrap(); - pending_txs.push(pending_tx); + + if self.use_local_signing { + // Sign locally with the Signer middleware + for (i, tx) in txs.clone().into_iter().enumerate() { + // The local signer uses eth_sendRawTransaction, so we need to manually set the nonce + let mut tx = tx.clone(); + tx.set_nonce(i as u64); + let pending_tx = signer.send_transaction(tx, None).await.unwrap(); + pending_txs.push(pending_tx); + } + } else { + // Sign on the EE + // Import and unlock all private keys to sign transactions on the EE + let _ = futures::future::join_all([&self.ee_a, &self.ee_b].iter().map(|ee| { + import_and_unlock( + ee.execution_engine.http_url(), + &PRIVATE_KEYS, + KEYSTORE_PASSWORD, + ) + })) + .await; + + for tx in txs.clone().into_iter() { + let pending_tx = self + .ee_a + .execution_engine + .provider + .send_transaction(tx, None) + .await + .unwrap(); + pending_txs.push(pending_tx); + } } /* diff --git a/testing/node_test_rig/src/lib.rs b/testing/node_test_rig/src/lib.rs index 6e632ccf54..4021a6d2c5 100644 --- a/testing/node_test_rig/src/lib.rs +++ b/testing/node_test_rig/src/lib.rs @@ -7,7 +7,6 @@ use environment::RuntimeContext; use eth2::{reqwest::ClientBuilder, BeaconNodeHttpClient, Timeouts}; use sensitive_url::SensitiveUrl; use std::path::PathBuf; -use std::sync::Arc; use std::time::Duration; use std::time::{SystemTime, UNIX_EPOCH}; use tempfile::{Builder as TempBuilder, TempDir}; @@ -249,7 +248,7 @@ impl LocalExecutionNode { if let Err(e) = std::fs::write(jwt_file_path, config.jwt_key.hex_string()) { panic!("Failed to write jwt file {}", e); } - let spec = Arc::new(E::default_spec()); + let spec = context.eth2_config.spec.clone(); Self { server: MockServer::new_with_config( &context.executor.handle().unwrap(), diff --git a/testing/simulator/Cargo.toml b/testing/simulator/Cargo.toml index 12b0afcc75..cf0d03c24f 100644 --- a/testing/simulator/Cargo.toml +++ b/testing/simulator/Cargo.toml @@ -20,5 +20,6 @@ rayon = { workspace = true } sensitive_url = { path = "../../common/sensitive_url" } serde_json = { workspace = true } tokio = { workspace = true } +tracing = { workspace = true } tracing-subscriber = { workspace = true } types = { workspace = true } diff --git a/testing/simulator/src/basic_sim.rs b/testing/simulator/src/basic_sim.rs index 6afc7771d4..1c27ca7792 100644 --- a/testing/simulator/src/basic_sim.rs +++ b/testing/simulator/src/basic_sim.rs @@ -11,36 +11,39 @@ use node_test_rig::{ }; use rayon::prelude::*; use std::cmp::max; +use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; use environment::tracing_common; use tracing_subscriber::prelude::*; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; +use logging::build_workspace_filter; use tokio::time::sleep; +use tracing::Level; use types::{Epoch, EthSpec, MinimalEthSpec}; const END_EPOCH: u64 = 16; const GENESIS_DELAY: u64 = 32; const ALTAIR_FORK_EPOCH: u64 = 0; const BELLATRIX_FORK_EPOCH: u64 = 0; -const CAPELLA_FORK_EPOCH: u64 = 1; -const DENEB_FORK_EPOCH: u64 = 2; -// const ELECTRA_FORK_EPOCH: u64 = 3; -// const FULU_FORK_EPOCH: u64 = 4; +const CAPELLA_FORK_EPOCH: u64 = 0; +const DENEB_FORK_EPOCH: u64 = 0; +const ELECTRA_FORK_EPOCH: u64 = 2; const SUGGESTED_FEE_RECIPIENT: [u8; 20] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]; #[allow(clippy::large_stack_frames)] pub fn run_basic_sim(matches: &ArgMatches) -> Result<(), String> { - let node_count = matches + let (_name, subcommand_matches) = matches.subcommand().expect("subcommand"); + let node_count = subcommand_matches .get_one::("nodes") .expect("missing nodes default") .parse::() .expect("missing nodes default"); - let proposer_nodes = matches + let proposer_nodes = subcommand_matches .get_one::("proposer-nodes") .unwrap_or(&String::from("0")) .parse::() @@ -48,21 +51,25 @@ pub fn run_basic_sim(matches: &ArgMatches) -> Result<(), String> { // extra beacon node added with delay let extra_nodes: usize = 1; println!("PROPOSER-NODES: {}", proposer_nodes); - let validators_per_node = matches + let validators_per_node = subcommand_matches .get_one::("validators-per-node") .expect("missing validators-per-node default") .parse::() .expect("missing validators-per-node default"); - let speed_up_factor = matches + let speed_up_factor = subcommand_matches .get_one::("speed-up-factor") .expect("missing speed-up-factor default") .parse::() .expect("missing speed-up-factor default"); - let log_level = matches + let log_level = subcommand_matches .get_one::("debug-level") .expect("missing debug-level"); - let continue_after_checks = matches.get_flag("continue-after-checks"); + let continue_after_checks = subcommand_matches.get_flag("continue-after-checks"); + let log_dir = subcommand_matches + .get_one::("log-dir") + .map(PathBuf::from); + let disable_stdout_logging = subcommand_matches.get_flag("disable-stdout-logging"); println!("Basic Simulator:"); println!(" nodes: {}", node_count); @@ -70,6 +77,8 @@ pub fn run_basic_sim(matches: &ArgMatches) -> Result<(), String> { println!(" validators-per-node: {}", validators_per_node); println!(" speed-up-factor: {}", speed_up_factor); println!(" continue-after-checks: {}", continue_after_checks); + println!(" log-dir: {:?}", log_dir); + println!(" disable-stdout-logging: {}", disable_stdout_logging); // Generate the directories and keystores required for the validator clients. let validator_files = (0..node_count) @@ -91,21 +100,21 @@ pub fn run_basic_sim(matches: &ArgMatches) -> Result<(), String> { env_builder, logger_config, stdout_logging_layer, - _file_logging_layer, + file_logging_layer, _sse_logging_layer_opt, - _libp2p_discv5_layer, + libp2p_discv5_layer, ) = tracing_common::construct_logger( LoggerConfig { - path: None, + path: log_dir, debug_level: tracing_common::parse_level(&log_level.clone()), logfile_debug_level: tracing_common::parse_level(&log_level.clone()), log_format: None, logfile_format: None, log_color: true, - logfile_color: true, + logfile_color: false, disable_log_timestamp: false, - max_log_size: 0, - max_log_number: 0, + max_log_size: 200, + max_log_number: 5, compression: false, is_restricted: true, sse_logging: false, @@ -115,8 +124,38 @@ pub fn run_basic_sim(matches: &ArgMatches) -> Result<(), String> { EnvironmentBuilder::minimal(), ); + let workspace_filter = build_workspace_filter()?; + let mut logging_layers = vec![]; + if !disable_stdout_logging { + logging_layers.push( + stdout_logging_layer + .with_filter(logger_config.debug_level) + .with_filter(workspace_filter.clone()) + .boxed(), + ); + } + if let Some(file_logging_layer) = file_logging_layer { + logging_layers.push( + file_logging_layer + .with_filter(logger_config.logfile_debug_level) + .with_filter(workspace_filter) + .boxed(), + ); + } + if let Some(libp2p_discv5_layer) = libp2p_discv5_layer { + logging_layers.push( + libp2p_discv5_layer + .with_filter( + EnvFilter::builder() + .with_default_directive(Level::DEBUG.into()) + .from_env_lossy(), + ) + .boxed(), + ); + } + if let Err(e) = tracing_subscriber::registry() - .with(stdout_logging_layer.with_filter(logger_config.debug_level)) + .with(logging_layers) .try_init() { eprintln!("Failed to initialize dependency logging: {e}"); @@ -130,8 +169,8 @@ pub fn run_basic_sim(matches: &ArgMatches) -> Result<(), String> { let genesis_delay = GENESIS_DELAY; // Convenience variables. Update these values when adding a newer fork. - let latest_fork_version = spec.deneb_fork_version; - let latest_fork_start_epoch = DENEB_FORK_EPOCH; + let latest_fork_version = spec.electra_fork_version; + let latest_fork_start_epoch = ELECTRA_FORK_EPOCH; spec.seconds_per_slot /= speed_up_factor; spec.seconds_per_slot = max(1, spec.seconds_per_slot); @@ -142,8 +181,7 @@ pub fn run_basic_sim(matches: &ArgMatches) -> Result<(), String> { spec.bellatrix_fork_epoch = Some(Epoch::new(BELLATRIX_FORK_EPOCH)); spec.capella_fork_epoch = Some(Epoch::new(CAPELLA_FORK_EPOCH)); spec.deneb_fork_epoch = Some(Epoch::new(DENEB_FORK_EPOCH)); - //spec.electra_fork_epoch = Some(Epoch::new(ELECTRA_FORK_EPOCH)); - //spec.fulu_fork_epoch = Some(Epoch::new(FULU_FORK_EPOCH)); + spec.electra_fork_epoch = Some(Epoch::new(ELECTRA_FORK_EPOCH)); let spec = Arc::new(spec); env.eth2_config.spec = spec.clone(); diff --git a/testing/simulator/src/checks.rs b/testing/simulator/src/checks.rs index 35c2508b53..1b2d4024d1 100644 --- a/testing/simulator/src/checks.rs +++ b/testing/simulator/src/checks.rs @@ -97,7 +97,7 @@ async fn verify_validator_count( let vc = remote_node .get_debug_beacon_states::(StateId::Head) .await - .map(|body| body.unwrap().data) + .map(|body| body.unwrap().into_data()) .map_err(|e| format!("Get state root via http failed: {:?}", e))? .validators() .len(); @@ -128,17 +128,23 @@ pub async fn verify_full_block_production_up_to( slot_delay(slot, slot_duration).await; let beacon_nodes = network.beacon_nodes.read(); let beacon_chain = beacon_nodes[0].client.beacon_chain().unwrap(); - let num_blocks = beacon_chain + let block_slots = beacon_chain .chain_dump() .unwrap() .iter() .take_while(|s| s.beacon_block.slot() <= slot) - .count(); + .map(|s| s.beacon_block.slot().as_usize()) + .collect::>(); + let num_blocks = block_slots.len(); if num_blocks != slot.as_usize() + 1 { + let missed_slots = (0..slot.as_usize()) + .filter(|slot| !block_slots.contains(slot)) + .collect::>(); return Err(format!( - "There wasn't a block produced at every slot, got: {}, expected: {}", + "There wasn't a block produced at every slot, got: {}, expected: {}, missed: {:?}", num_blocks, - slot.as_usize() + 1 + slot.as_usize() + 1, + missed_slots )); } Ok(()) @@ -185,12 +191,17 @@ pub async fn verify_full_sync_aggregates_up_to( .get_beacon_blocks::(BlockId::Slot(Slot::new(slot))) .await .map(|resp| { - resp.unwrap() - .data - .message() - .body() - .sync_aggregate() - .map(|agg| agg.num_set_bits()) + resp.unwrap_or_else(|| { + panic!( + "Beacon block for slot {} not returned from Beacon API", + slot + ) + }) + .data() + .message() + .body() + .sync_aggregate() + .map(|agg| agg.num_set_bits()) }) .map_err(|e| format!("Error while getting beacon block: {:?}", e))? .map_err(|_| format!("Altair block {} should have sync aggregate", slot))?; @@ -224,7 +235,7 @@ pub async fn verify_transition_block_finalized( let execution_block_hash: ExecutionBlockHash = remote_node .get_beacon_blocks::(BlockId::Finalized) .await - .map(|body| body.unwrap().data) + .map(|body| body.unwrap().into_data()) .map_err(|e| format!("Get state root via http failed: {:?}", e))? .message() .execution_payload() @@ -297,7 +308,7 @@ pub(crate) async fn verify_light_client_updates( .await .map_err(|e| format!("Error while getting light client updates: {:?}", e))? .ok_or(format!("Light client optimistic update not found {slot:?}"))? - .data + .data() .signature_slot(); let signature_slot_distance = slot - signature_slot; if signature_slot_distance > light_client_update_slot_tolerance { @@ -326,7 +337,7 @@ pub(crate) async fn verify_light_client_updates( .await .map_err(|e| format!("Error while getting light client updates: {:?}", e))? .ok_or(format!("Light client finality update not found {slot:?}"))? - .data + .data() .signature_slot(); let signature_slot_distance = slot - signature_slot; if signature_slot_distance > light_client_update_slot_tolerance { @@ -374,7 +385,7 @@ pub async fn ensure_node_synced_up_to_slot( .ok() .flatten() .ok_or(format!("No head block exists on node {node_index}"))? - .data; + .into_data(); // Check the head block is synced with the rest of the network. if head.slot() >= upto_slot { @@ -411,7 +422,7 @@ pub async fn verify_full_blob_production_up_to( // the `verify_full_block_production_up_to` function. if block.is_some() { remote_node - .get_blobs::(BlockId::Slot(Slot::new(slot)), None) + .get_blobs::(BlockId::Slot(Slot::new(slot)), None, &E::default_spec()) .await .map_err(|e| format!("Failed to get blobs at slot {slot:?}: {e:?}"))? .ok_or_else(|| format!("No blobs available at slot {slot:?}"))?; diff --git a/testing/simulator/src/cli.rs b/testing/simulator/src/cli.rs index 3d61dcde74..707baf04a7 100644 --- a/testing/simulator/src/cli.rs +++ b/testing/simulator/src/cli.rs @@ -61,6 +61,18 @@ pub fn cli_app() -> Command { .long("continue_after_checks") .action(ArgAction::SetTrue) .help("Continue after checks (default false)"), + ) + .arg( + Arg::new("log-dir") + .long("log-dir") + .action(ArgAction::Set) + .help("Set a path for logs of beacon nodes that run in this simulation."), + ) + .arg( + Arg::new("disable-stdout-logging") + .long("disable-stdout-logging") + .action(ArgAction::SetTrue) + .help("Disables stdout logging."), ), ) .subcommand( @@ -120,6 +132,18 @@ pub fn cli_app() -> Command { .long("continue_after_checks") .action(ArgAction::SetTrue) .help("Continue after checks (default false)"), + ) + .arg( + Arg::new("log-dir") + .long("log-dir") + .action(ArgAction::Set) + .help("Set a path for logs of beacon nodes that run in this simulation."), + ) + .arg( + Arg::new("disable-stdout-logging") + .long("disable-stdout-logging") + .action(ArgAction::SetTrue) + .help("Disables stdout logging."), ), ) } diff --git a/testing/simulator/src/fallback_sim.rs b/testing/simulator/src/fallback_sim.rs index f4e0d20f38..2d0cacd941 100644 --- a/testing/simulator/src/fallback_sim.rs +++ b/testing/simulator/src/fallback_sim.rs @@ -5,17 +5,20 @@ use clap::ArgMatches; use crate::retry::with_retry; use environment::tracing_common; use futures::prelude::*; +use logging::build_workspace_filter; use node_test_rig::{ environment::{EnvironmentBuilder, LoggerConfig}, testing_validator_config, ValidatorFiles, }; use rayon::prelude::*; use std::cmp::max; +use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; use tokio::time::sleep; +use tracing::Level; use tracing_subscriber::prelude::*; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; use types::{Epoch, EthSpec, MinimalEthSpec}; const END_EPOCH: u64 = 16; const GENESIS_DELAY: u64 = 32; @@ -38,36 +41,43 @@ const SUGGESTED_FEE_RECIPIENT: [u8; 20] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]; pub fn run_fallback_sim(matches: &ArgMatches) -> Result<(), String> { - let vc_count = matches + let (_name, subcommand_matches) = matches.subcommand().expect("subcommand"); + let vc_count = subcommand_matches .get_one::("vc-count") .expect("missing vc-count default") .parse::() .expect("missing vc-count default"); - let validators_per_vc = matches + let validators_per_vc = subcommand_matches .get_one::("validators-per-vc") .expect("missing validators-per-vc default") .parse::() .expect("missing validators-per-vc default"); - let bns_per_vc = matches + let bns_per_vc = subcommand_matches .get_one::("bns-per-vc") .expect("missing bns-per-vc default") .parse::() .expect("missing bns-per-vc default"); assert!(bns_per_vc > 1); - let speed_up_factor = matches + let speed_up_factor = subcommand_matches .get_one::("speed-up-factor") .expect("missing speed-up-factor default") .parse::() .expect("missing speed-up-factor default"); - let log_level = matches + let log_level = subcommand_matches .get_one::("debug-level") .expect("missing debug-level default"); - let continue_after_checks = matches.get_flag("continue-after-checks"); + let continue_after_checks = subcommand_matches.get_flag("continue-after-checks"); + + let log_dir = subcommand_matches + .get_one::("log-dir") + .map(PathBuf::from); + + let disable_stdout_logging = subcommand_matches.get_flag("disable-stdout-logging"); println!("Fallback Simulator:"); println!(" vc-count: {}", vc_count); @@ -75,6 +85,8 @@ pub fn run_fallback_sim(matches: &ArgMatches) -> Result<(), String> { println!(" bns-per-vc: {}", bns_per_vc); println!(" speed-up-factor: {}", speed_up_factor); println!(" continue-after-checks: {}", continue_after_checks); + println!(" log-dir: {:?}", log_dir); + println!(" disable-stdout-logging: {}", disable_stdout_logging); // Generate the directories and keystores required for the validator clients. let validator_files = (0..vc_count) @@ -95,12 +107,12 @@ pub fn run_fallback_sim(matches: &ArgMatches) -> Result<(), String> { env_builder, logger_config, stdout_logging_layer, - _file_logging_layer, + file_logging_layer, _sse_logging_layer_opt, - _libp2p_discv5_layer, + libp2p_discv5_layer, ) = tracing_common::construct_logger( LoggerConfig { - path: None, + path: log_dir, debug_level: tracing_common::parse_level(&log_level.clone()), logfile_debug_level: tracing_common::parse_level(&log_level.clone()), log_format: None, @@ -108,8 +120,8 @@ pub fn run_fallback_sim(matches: &ArgMatches) -> Result<(), String> { log_color: true, logfile_color: false, disable_log_timestamp: false, - max_log_size: 0, - max_log_number: 0, + max_log_size: 200, + max_log_number: 5, compression: false, is_restricted: true, sse_logging: false, @@ -119,8 +131,38 @@ pub fn run_fallback_sim(matches: &ArgMatches) -> Result<(), String> { EnvironmentBuilder::minimal(), ); + let workspace_filter = build_workspace_filter()?; + let mut logging_layers = vec![]; + if !disable_stdout_logging { + logging_layers.push( + stdout_logging_layer + .with_filter(logger_config.debug_level) + .with_filter(workspace_filter.clone()) + .boxed(), + ); + } + if let Some(file_logging_layer) = file_logging_layer { + logging_layers.push( + file_logging_layer + .with_filter(logger_config.logfile_debug_level) + .with_filter(workspace_filter) + .boxed(), + ); + } + if let Some(libp2p_discv5_layer) = libp2p_discv5_layer { + logging_layers.push( + libp2p_discv5_layer + .with_filter( + EnvFilter::builder() + .with_default_directive(Level::DEBUG.into()) + .from_env_lossy(), + ) + .boxed(), + ); + } + if let Err(e) = tracing_subscriber::registry() - .with(stdout_logging_layer.with_filter(logger_config.debug_level)) + .with(logging_layers) .try_init() { eprintln!("Failed to initialize dependency logging: {e}"); diff --git a/testing/simulator/src/main.rs b/testing/simulator/src/main.rs index a259ac1133..1cc4a1779b 100644 --- a/testing/simulator/src/main.rs +++ b/testing/simulator/src/main.rs @@ -29,15 +29,15 @@ fn main() { Builder::from_env(Env::default()).init(); let matches = cli_app().get_matches(); - match matches.subcommand() { - Some(("basic-sim", matches)) => match basic_sim::run_basic_sim(matches) { + match matches.subcommand_name() { + Some("basic-sim") => match basic_sim::run_basic_sim(&matches) { Ok(()) => println!("Simulation exited successfully"), Err(e) => { eprintln!("Simulation exited with error: {}", e); std::process::exit(1) } }, - Some(("fallback-sim", matches)) => match fallback_sim::run_fallback_sim(matches) { + Some("fallback-sim") => match fallback_sim::run_fallback_sim(&matches) { Ok(()) => println!("Simulation exited successfully"), Err(e) => { eprintln!("Simulation exited with error: {}", e); diff --git a/testing/web3signer_tests/Cargo.toml b/testing/web3signer_tests/Cargo.toml index 376aa13406..b4637b4030 100644 --- a/testing/web3signer_tests/Cargo.toml +++ b/testing/web3signer_tests/Cargo.toml @@ -10,10 +10,12 @@ edition = { workspace = true } account_utils = { workspace = true } async-channel = { workspace = true } environment = { workspace = true } +eth2 = { workspace = true } eth2_keystore = { workspace = true } eth2_network_config = { workspace = true } futures = { workspace = true } initialized_validators = { workspace = true } +lighthouse_validator_store = { workspace = true } logging = { workspace = true } parking_lot = { workspace = true } reqwest = { workspace = true } diff --git a/testing/web3signer_tests/src/lib.rs b/testing/web3signer_tests/src/lib.rs index 1eb14cf1d5..4bc0f62346 100644 --- a/testing/web3signer_tests/src/lib.rs +++ b/testing/web3signer_tests/src/lib.rs @@ -20,11 +20,13 @@ mod tests { use account_utils::validator_definitions::{ SigningDefinition, ValidatorDefinition, ValidatorDefinitions, Web3SignerDefinition, }; + use eth2::types::FullBlockContents; use eth2_keystore::KeystoreBuilder; use eth2_network_config::Eth2NetworkConfig; use initialized_validators::{ load_pem_certificate, load_pkcs12_identity, InitializedValidators, }; + use lighthouse_validator_store::LighthouseValidatorStore; use parking_lot::Mutex; use reqwest::Client; use serde::Serialize; @@ -44,7 +46,9 @@ mod tests { use tokio::time::sleep; use types::{attestation::AttestationBase, *}; use url::Url; - use validator_store::{Error as ValidatorStoreError, ValidatorStore}; + use validator_store::{ + Error as ValidatorStoreError, SignedBlock, UnsignedBlock, ValidatorStore, + }; /// If the we are unable to reach the Web3Signer HTTP API within this time out then we will /// assume it failed to start. @@ -73,6 +77,7 @@ mod tests { impl SignedObject for Signature {} impl SignedObject for Attestation {} impl SignedObject for SignedBeaconBlock {} + impl SignedObject for SignedBlock {} impl SignedObject for SignedAggregateAndProof {} impl SignedObject for SelectionProof {} impl SignedObject for SyncSelectionProof {} @@ -301,7 +306,7 @@ mod tests { /// A testing rig which holds a `ValidatorStore`. struct ValidatorStoreRig { - validator_store: Arc>, + validator_store: Arc>, _validator_dir: TempDir, runtime: Arc, _runtime_shutdown: async_channel::Sender<()>, @@ -352,12 +357,12 @@ mod tests { let slot_clock = TestingSlotClock::new(Slot::new(0), Duration::from_secs(0), Duration::from_secs(1)); - let config = validator_store::Config { + let config = lighthouse_validator_store::Config { enable_web3signer_slashing_protection: slashing_protection_config.local, ..Default::default() }; - let validator_store = ValidatorStore::<_, E>::new( + let validator_store = LighthouseValidatorStore::<_, E>::new( initialized_validators, slashing_protection, Hash256::repeat_byte(42), @@ -481,7 +486,7 @@ mod tests { generate_sig: F, ) -> Self where - F: Fn(PublicKeyBytes, Arc>) -> R, + F: Fn(PublicKeyBytes, Arc>) -> R, R: Future, // We use the `SignedObject` trait to white-list objects for comparison. This avoids // accidentally comparing something meaningless like a `()`. @@ -516,8 +521,8 @@ mod tests { web3signer_should_sign: bool, ) -> Self where - F: Fn(PublicKeyBytes, Arc>) -> R, - R: Future>, + F: Fn(PublicKeyBytes, Arc>) -> R, + R: Future>, { for validator_rig in &self.validator_rigs { let result = @@ -591,10 +596,11 @@ mod tests { .assert_signatures_match("beacon_block_base", |pubkey, validator_store| { let spec = spec.clone(); async move { - let block = BeaconBlock::Base(BeaconBlockBase::empty(&spec)); + let block = BeaconBlock::::Base(BeaconBlockBase::empty(&spec)); let block_slot = block.slot(); + let unsigned_block = UnsignedBlock::Full(FullBlockContents::Block(block)); validator_store - .sign_block(pubkey, block, block_slot) + .sign_block(pubkey, unsigned_block, block_slot) .await .unwrap() } @@ -663,8 +669,10 @@ mod tests { async move { let mut altair_block = BeaconBlockAltair::empty(&spec); altair_block.slot = altair_fork_slot; + let unsigned_block = + UnsignedBlock::Full(FullBlockContents::Block(altair_block.into())); validator_store - .sign_block(pubkey, BeaconBlock::Altair(altair_block), altair_fork_slot) + .sign_block(pubkey, unsigned_block, altair_fork_slot) .await .unwrap() } @@ -746,12 +754,10 @@ mod tests { async move { let mut bellatrix_block = BeaconBlockBellatrix::empty(&spec); bellatrix_block.slot = bellatrix_fork_slot; + let unsigned_block = + UnsignedBlock::Full(FullBlockContents::Block(bellatrix_block.into())); validator_store - .sign_block( - pubkey, - BeaconBlock::Bellatrix(bellatrix_block), - bellatrix_fork_slot, - ) + .sign_block(pubkey, unsigned_block, bellatrix_fork_slot) .await .unwrap() } @@ -805,7 +811,7 @@ mod tests { }; let first_block = || { - let mut bellatrix_block = BeaconBlockBellatrix::empty(&spec); + let mut bellatrix_block = BeaconBlockBellatrix::::empty(&spec); bellatrix_block.slot = bellatrix_fork_slot; BeaconBlock::Bellatrix(bellatrix_block) }; @@ -870,8 +876,9 @@ mod tests { .assert_signatures_match("first_block", |pubkey, validator_store| async move { let block = first_block(); let slot = block.slot(); + let unsigned_block = UnsignedBlock::Full(FullBlockContents::Block(block)); validator_store - .sign_block(pubkey, block, slot) + .sign_block(pubkey, unsigned_block, slot) .await .unwrap() }) @@ -881,8 +888,9 @@ mod tests { move |pubkey, validator_store| async move { let block = double_vote_block(); let slot = block.slot(); + let unsigned_block = UnsignedBlock::Full(FullBlockContents::Block(block)); validator_store - .sign_block(pubkey, block, slot) + .sign_block(pubkey, unsigned_block, slot) .await .map(|_| ()) }, diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index 85517682bb..a8c8fd59f1 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -22,6 +22,7 @@ fdlimit = "0.3.0" graffiti_file = { workspace = true } hyper = { workspace = true } initialized_validators = { workspace = true } +lighthouse_validator_store = { workspace = true } metrics = { workspace = true } monitoring_api = { workspace = true } parking_lot = { workspace = true } diff --git a/validator_client/beacon_node_fallback/Cargo.toml b/validator_client/beacon_node_fallback/Cargo.toml index 4297bae15f..3bcb0d7034 100644 --- a/validator_client/beacon_node_fallback/Cargo.toml +++ b/validator_client/beacon_node_fallback/Cargo.toml @@ -10,18 +10,17 @@ path = "src/lib.rs" [dependencies] clap = { workspace = true } -environment = { workspace = true } eth2 = { workspace = true } futures = { workspace = true } itertools = { workspace = true } serde = { workspace = true } slot_clock = { workspace = true } strum = { workspace = true } +task_executor = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } types = { workspace = true } validator_metrics = { workspace = true } [dev-dependencies] -logging = { workspace = true } validator_test_rig = { workspace = true } diff --git a/validator_client/beacon_node_fallback/src/lib.rs b/validator_client/beacon_node_fallback/src/lib.rs index 182ba64681..e19da31e9a 100644 --- a/validator_client/beacon_node_fallback/src/lib.rs +++ b/validator_client/beacon_node_fallback/src/lib.rs @@ -8,7 +8,6 @@ use beacon_node_health::{ IsOptimistic, SyncDistanceTier, }; use clap::ValueEnum; -use environment::RuntimeContext; use eth2::BeaconNodeHttpClient; use futures::future; use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer}; @@ -17,11 +16,11 @@ use std::cmp::Ordering; use std::fmt; use std::fmt::Debug; use std::future::Future; -use std::marker::PhantomData; use std::sync::Arc; use std::time::{Duration, Instant}; use std::vec::Vec; use strum::EnumVariantNames; +use task_executor::TaskExecutor; use tokio::{sync::RwLock, time::sleep}; use tracing::{debug, error, warn}; use types::{ChainSpec, Config as ConfigSpec, EthSpec, Slot}; @@ -61,17 +60,16 @@ pub struct LatencyMeasurement { /// /// See `SLOT_LOOKAHEAD` for information about when this should run. pub fn start_fallback_updater_service( - context: RuntimeContext, - beacon_nodes: Arc>, + executor: TaskExecutor, + beacon_nodes: Arc>, ) -> Result<(), &'static str> { - let executor = context.executor; if beacon_nodes.slot_clock.is_none() { return Err("Cannot start fallback updater without slot clock"); } let future = async move { loop { - beacon_nodes.update_all_candidates().await; + beacon_nodes.update_all_candidates::().await; let sleep_time = beacon_nodes .slot_clock @@ -186,29 +184,27 @@ impl Serialize for CandidateInfo { /// Represents a `BeaconNodeHttpClient` inside a `BeaconNodeFallback` that may or may not be used /// for a query. #[derive(Clone, Debug)] -pub struct CandidateBeaconNode { +pub struct CandidateBeaconNode { pub index: usize, pub beacon_node: BeaconNodeHttpClient, pub health: Arc>>, - _phantom: PhantomData, } -impl PartialEq for CandidateBeaconNode { +impl PartialEq for CandidateBeaconNode { fn eq(&self, other: &Self) -> bool { self.index == other.index && self.beacon_node == other.beacon_node } } -impl Eq for CandidateBeaconNode {} +impl Eq for CandidateBeaconNode {} -impl CandidateBeaconNode { +impl CandidateBeaconNode { /// Instantiate a new node. pub fn new(beacon_node: BeaconNodeHttpClient, index: usize) -> Self { Self { index, beacon_node, health: Arc::new(RwLock::new(Err(CandidateError::Uninitialized))), - _phantom: PhantomData, } } @@ -217,13 +213,13 @@ impl CandidateBeaconNode { *self.health.read().await } - pub async fn refresh_health( + pub async fn refresh_health( &self, distance_tiers: &BeaconNodeSyncDistanceTiers, slot_clock: Option<&T>, spec: &ChainSpec, ) -> Result<(), CandidateError> { - if let Err(e) = self.is_compatible(spec).await { + if let Err(e) = self.is_compatible::(spec).await { *self.health.write().await = Err(e); return Err(e); } @@ -287,7 +283,7 @@ impl CandidateBeaconNode { } /// Checks if the node has the correct specification. - async fn is_compatible(&self, spec: &ChainSpec) -> Result<(), CandidateError> { + async fn is_compatible(&self, spec: &ChainSpec) -> Result<(), CandidateError> { let config = self .beacon_node .get_config_spec::() @@ -372,17 +368,17 @@ impl CandidateBeaconNode { /// behaviour, where the failure of one candidate results in the next candidate receiving an /// identical query. #[derive(Clone, Debug)] -pub struct BeaconNodeFallback { - pub candidates: Arc>>>, +pub struct BeaconNodeFallback { + pub candidates: Arc>>, distance_tiers: BeaconNodeSyncDistanceTiers, slot_clock: Option, broadcast_topics: Vec, spec: Arc, } -impl BeaconNodeFallback { +impl BeaconNodeFallback { pub fn new( - candidates: Vec>, + candidates: Vec, config: Config, broadcast_topics: Vec, spec: Arc, @@ -464,7 +460,7 @@ impl BeaconNodeFallback { /// It is possible for a node to return an unsynced status while continuing to serve /// low quality responses. To route around this it's best to poll all connected beacon nodes. /// A previous implementation of this function polled only the unavailable BNs. - pub async fn update_all_candidates(&self) { + pub async fn update_all_candidates(&self) { // Clone the vec, so we release the read lock immediately. // `candidate.health` is behind an Arc, so this would still allow us to mutate the values. let candidates = self.candidates.read().await.clone(); @@ -472,7 +468,7 @@ impl BeaconNodeFallback { let mut nodes = Vec::with_capacity(candidates.len()); for candidate in candidates.iter() { - futures.push(candidate.refresh_health( + futures.push(candidate.refresh_health::( &self.distance_tiers, self.slot_clock.as_ref(), &self.spec, @@ -486,12 +482,26 @@ impl BeaconNodeFallback { for (result, node) in results { if let Err(e) = result { - if *e != CandidateError::PreGenesis { - warn!( - error = ?e, - endpoint = %node, - "A connected beacon node errored during routine health check" - ); + match e { + // Avoid spamming warns before genesis. + CandidateError::PreGenesis => {} + // Uninitialized *should* only occur during start-up before the + // slot clock has been initialized. + // Seeing this log in any other circumstance would indicate a serious bug. + CandidateError::Uninitialized => { + debug!( + error = ?e, + endpoint = %node, + "A connected beacon node is uninitialized" + ); + } + _ => { + warn!( + error = ?e, + endpoint = %node, + "A connected beacon node errored during routine health check" + ); + } } } } @@ -675,7 +685,7 @@ impl BeaconNodeFallback { } /// Helper functions to allow sorting candidate nodes by health. -async fn sort_nodes_by_health(nodes: &mut Vec>) { +async fn sort_nodes_by_health(nodes: &mut Vec) { // Fetch all health values. let health_results: Vec> = future::join_all(nodes.iter().map(|node| node.health())).await; @@ -693,7 +703,7 @@ async fn sort_nodes_by_health(nodes: &mut Vec }); // Reorder candidates based on the sorted indices. - let sorted_nodes: Vec> = indices_with_health + let sorted_nodes: Vec = indices_with_health .into_iter() .map(|(index, _)| nodes[index].clone()) .collect(); @@ -759,7 +769,7 @@ mod tests { let optimistic_status = IsOptimistic::No; let execution_status = ExecutionEngineHealth::Healthy; - fn new_candidate(index: usize) -> CandidateBeaconNode { + fn new_candidate(index: usize) -> CandidateBeaconNode { let beacon_node = BeaconNodeHttpClient::new( SensitiveUrl::parse(&format!("http://example_{index}.com")).unwrap(), Timeouts::set_all(Duration::from_secs(index as u64)), @@ -866,21 +876,21 @@ mod tests { async fn new_mock_beacon_node( index: usize, spec: &ChainSpec, - ) -> (MockBeaconNode, CandidateBeaconNode) { + ) -> (MockBeaconNode, CandidateBeaconNode) { let mut mock_beacon_node = MockBeaconNode::::new().await; mock_beacon_node.mock_config_spec(spec); let beacon_node = - CandidateBeaconNode::::new(mock_beacon_node.beacon_api_client.clone(), index); + CandidateBeaconNode::new(mock_beacon_node.beacon_api_client.clone(), index); (mock_beacon_node, beacon_node) } fn create_beacon_node_fallback( - candidates: Vec>, + candidates: Vec, topics: Vec, spec: Arc, - ) -> BeaconNodeFallback { + ) -> BeaconNodeFallback { let mut beacon_node_fallback = BeaconNodeFallback::new(candidates, Config::default(), topics, spec); @@ -936,7 +946,7 @@ mod tests { sync_distance: Slot::new(0), }); - beacon_node_fallback.update_all_candidates().await; + beacon_node_fallback.update_all_candidates::().await; let candidates = beacon_node_fallback.candidates.read().await; assert_eq!( diff --git a/validator_client/doppelganger_service/Cargo.toml b/validator_client/doppelganger_service/Cargo.toml index 803dd94322..e5b183570d 100644 --- a/validator_client/doppelganger_service/Cargo.toml +++ b/validator_client/doppelganger_service/Cargo.toml @@ -15,6 +15,7 @@ task_executor = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } types = { workspace = true } +validator_store = { workspace = true } [dev-dependencies] futures = { workspace = true } diff --git a/validator_client/doppelganger_service/src/lib.rs b/validator_client/doppelganger_service/src/lib.rs index cb81b3ffc2..e3c7ce78b4 100644 --- a/validator_client/doppelganger_service/src/lib.rs +++ b/validator_client/doppelganger_service/src/lib.rs @@ -42,68 +42,7 @@ use task_executor::ShutdownReason; use tokio::time::sleep; use tracing::{error, info}; use types::{Epoch, EthSpec, PublicKeyBytes, Slot}; - -/// A wrapper around `PublicKeyBytes` which encodes information about the status of a validator -/// pubkey with regards to doppelganger protection. -#[derive(Debug, PartialEq)] -pub enum DoppelgangerStatus { - /// Doppelganger protection has approved this for signing. - /// - /// This is because the service has waited some period of time to - /// detect other instances of this key on the network. - SigningEnabled(PublicKeyBytes), - /// Doppelganger protection is still waiting to detect other instances. - /// - /// Do not use this pubkey for signing slashable messages!! - /// - /// However, it can safely be used for other non-slashable operations (e.g., collecting duties - /// or subscribing to subnets). - SigningDisabled(PublicKeyBytes), - /// This pubkey is unknown to the doppelganger service. - /// - /// This represents a serious internal error in the program. This validator will be permanently - /// disabled! - UnknownToDoppelganger(PublicKeyBytes), -} - -impl DoppelgangerStatus { - /// Only return a pubkey if it is explicitly safe for doppelganger protection. - /// - /// If `Some(pubkey)` is returned, doppelganger has declared it safe for signing. - /// - /// ## Note - /// - /// "Safe" is only best-effort by doppelganger. There is no guarantee that a doppelganger - /// doesn't exist. - pub fn only_safe(self) -> Option { - match self { - DoppelgangerStatus::SigningEnabled(pubkey) => Some(pubkey), - DoppelgangerStatus::SigningDisabled(_) => None, - DoppelgangerStatus::UnknownToDoppelganger(_) => None, - } - } - - /// Returns a key regardless of whether or not doppelganger has approved it. Such a key might be - /// used for signing non-slashable messages, duties collection or other activities. - /// - /// If the validator is unknown to doppelganger then `None` will be returned. - pub fn ignored(self) -> Option { - match self { - DoppelgangerStatus::SigningEnabled(pubkey) => Some(pubkey), - DoppelgangerStatus::SigningDisabled(pubkey) => Some(pubkey), - DoppelgangerStatus::UnknownToDoppelganger(_) => None, - } - } - - /// Only return a pubkey if it will not be used for signing due to doppelganger detection. - pub fn only_unsafe(self) -> Option { - match self { - DoppelgangerStatus::SigningEnabled(_) => None, - DoppelgangerStatus::SigningDisabled(pubkey) => Some(pubkey), - DoppelgangerStatus::UnknownToDoppelganger(pubkey) => Some(pubkey), - } - } -} +use validator_store::{DoppelgangerStatus, ValidatorStore}; struct LivenessResponses { current_epoch_responses: Vec, @@ -114,13 +53,6 @@ struct LivenessResponses { /// validators on the network. pub const DEFAULT_REMAINING_DETECTION_EPOCHS: u64 = 1; -/// This crate cannot depend on ValidatorStore as validator_store depends on this crate and -/// initialises the doppelganger protection. For this reason, we abstract the validator store -/// functions this service needs through the following trait -pub trait DoppelgangerValidatorStore { - fn get_validator_index(&self, pubkey: &PublicKeyBytes) -> Option; -} - /// Store the per-validator status of doppelganger checking. #[derive(Debug, PartialEq)] pub struct DoppelgangerState { @@ -163,8 +95,8 @@ impl DoppelgangerState { /// If the BN fails to respond to either of these requests, simply return an empty response. /// This behaviour is to help prevent spurious failures on the BN from needlessly preventing /// doppelganger progression. -async fn beacon_node_liveness( - beacon_nodes: Arc>, +async fn beacon_node_liveness( + beacon_nodes: Arc>, current_epoch: Epoch, validator_indices: Vec, ) -> LivenessResponses { @@ -280,20 +212,20 @@ impl DoppelgangerService { service: Arc, context: RuntimeContext, validator_store: Arc, - beacon_nodes: Arc>, + beacon_nodes: Arc>, slot_clock: T, ) -> Result<(), String> where E: EthSpec, T: 'static + SlotClock, - V: DoppelgangerValidatorStore + Send + Sync + 'static, + V: ValidatorStore + Send + Sync + 'static, { // Define the `get_index` function as one that uses the validator store. - let get_index = move |pubkey| validator_store.get_validator_index(&pubkey); + let get_index = move |pubkey| validator_store.validator_index(&pubkey); // Define the `get_liveness` function as one that queries the beacon node API. let get_liveness = move |current_epoch, validator_indices| { - beacon_node_liveness(beacon_nodes.clone(), current_epoch, validator_indices) + beacon_node_liveness::(beacon_nodes.clone(), current_epoch, validator_indices) }; let mut shutdown_sender = context.executor.shutdown_sender(); @@ -378,17 +310,18 @@ impl DoppelgangerService { /// /// Validators added during the genesis epoch will not have doppelganger protection applied to /// them. - pub fn register_new_validator( + pub fn register_new_validator( &self, validator: PublicKeyBytes, slot_clock: &T, + slots_per_epoch: u64, ) -> Result<(), String> { let current_epoch = slot_clock // If registering before genesis, use the genesis slot. .now_or_genesis() .ok_or_else(|| "Unable to read slot clock when registering validator".to_string())? - .epoch(E::slots_per_epoch()); - let genesis_epoch = slot_clock.genesis_slot().epoch(E::slots_per_epoch()); + .epoch(slots_per_epoch); + let genesis_epoch = slot_clock.genesis_slot().epoch(slots_per_epoch); let remaining_epochs = if current_epoch <= genesis_epoch { // Disable doppelganger protection when the validator was initialized before genesis. @@ -673,6 +606,7 @@ mod test { test_utils::{SeedableRng, TestRandom, XorShiftRng}, MainnetEthSpec, }; + use validator_store::DoppelgangerStatus; const DEFAULT_VALIDATORS: usize = 8; @@ -773,7 +707,7 @@ mod test { .expect("index should exist"); self.doppelganger - .register_new_validator::(pubkey, &self.slot_clock) + .register_new_validator(pubkey, &self.slot_clock, E::slots_per_epoch()) .unwrap(); self.doppelganger .doppelganger_states diff --git a/validator_client/http_api/Cargo.toml b/validator_client/http_api/Cargo.toml index 482212d890..588aa2ca93 100644 --- a/validator_client/http_api/Cargo.toml +++ b/validator_client/http_api/Cargo.toml @@ -16,13 +16,14 @@ deposit_contract = { workspace = true } directory = { workspace = true } dirs = { workspace = true } doppelganger_service = { workspace = true } -eth2 = { workspace = true } -eth2_keystore = { workspace = true } +eth2 = { workspace = true } +eth2_keystore = { workspace = true } ethereum_serde_utils = { workspace = true } filesystem = { workspace = true } graffiti_file = { workspace = true } health_metrics = { workspace = true } initialized_validators = { workspace = true } +lighthouse_validator_store = { workspace = true } lighthouse_version = { workspace = true } logging = { workspace = true } parking_lot = { workspace = true } @@ -32,19 +33,19 @@ serde = { workspace = true } serde_json = { workspace = true } signing_method = { workspace = true } slashing_protection = { workspace = true } -slot_clock = { workspace = true } -sysinfo = { workspace = true } -system_health = { workspace = true } -task_executor = { workspace = true } -tempfile = { workspace = true } -tokio = { workspace = true } -tokio-stream = { workspace = true } -tracing = { workspace = true } -types = { workspace = true } -url = { workspace = true } -validator_dir = { workspace = true } -validator_services = { workspace = true } -validator_store = { workspace = true } +slot_clock = { workspace = true } +sysinfo = { workspace = true } +system_health = { workspace = true } +task_executor = { workspace = true } +tempfile = { workspace = true } +tokio = { workspace = true } +tokio-stream = { workspace = true } +tracing = { workspace = true } +types = { workspace = true } +url = { workspace = true } +validator_dir = { workspace = true } +validator_services = { workspace = true } +validator_store = { workspace = true } warp = { workspace = true } warp_utils = { workspace = true } zeroize = { workspace = true } diff --git a/validator_client/http_api/src/create_signed_voluntary_exit.rs b/validator_client/http_api/src/create_signed_voluntary_exit.rs index 7a9dc798d6..b536a6aa7a 100644 --- a/validator_client/http_api/src/create_signed_voluntary_exit.rs +++ b/validator_client/http_api/src/create_signed_voluntary_exit.rs @@ -1,5 +1,6 @@ use bls::{PublicKey, PublicKeyBytes}; use eth2::types::GenericResponse; +use lighthouse_validator_store::LighthouseValidatorStore; use slot_clock::SlotClock; use std::sync::Arc; use tracing::info; @@ -9,7 +10,7 @@ use validator_store::ValidatorStore; pub async fn create_signed_voluntary_exit( pubkey: PublicKey, maybe_epoch: Option, - validator_store: Arc>, + validator_store: Arc>, slot_clock: T, ) -> Result, warp::Rejection> { let epoch = match maybe_epoch { diff --git a/validator_client/http_api/src/create_validator.rs b/validator_client/http_api/src/create_validator.rs index f90a1057a4..278274198d 100644 --- a/validator_client/http_api/src/create_validator.rs +++ b/validator_client/http_api/src/create_validator.rs @@ -5,12 +5,11 @@ use account_utils::{ random_mnemonic, random_password, }; use eth2::lighthouse_vc::types::{self as api_types}; +use lighthouse_validator_store::LighthouseValidatorStore; use slot_clock::SlotClock; use std::path::{Path, PathBuf}; -use types::ChainSpec; -use types::EthSpec; +use types::{ChainSpec, EthSpec}; use validator_dir::{keystore_password_path, Builder as ValidatorDirBuilder}; -use validator_store::ValidatorStore; use zeroize::Zeroizing; /// Create some validator EIP-2335 keystores and store them on disk. Then, enroll the validators in @@ -30,7 +29,7 @@ pub async fn create_validators_mnemonic, T: 'static + SlotClock, validator_requests: &[api_types::ValidatorRequest], validator_dir: P, secrets_dir: Option, - validator_store: &ValidatorStore, + validator_store: &LighthouseValidatorStore, spec: &ChainSpec, ) -> Result<(Vec, Mnemonic), warp::Rejection> { let mnemonic = mnemonic_opt.unwrap_or_else(random_mnemonic); @@ -178,7 +177,7 @@ pub async fn create_validators_mnemonic, T: 'static + SlotClock, pub async fn create_validators_web3signer( validators: Vec, - validator_store: &ValidatorStore, + validator_store: &LighthouseValidatorStore, ) -> Result<(), warp::Rejection> { for validator in validators { validator_store diff --git a/validator_client/http_api/src/graffiti.rs b/validator_client/http_api/src/graffiti.rs index 86238a697c..4372b14b04 100644 --- a/validator_client/http_api/src/graffiti.rs +++ b/validator_client/http_api/src/graffiti.rs @@ -1,12 +1,12 @@ use bls::PublicKey; +use lighthouse_validator_store::LighthouseValidatorStore; use slot_clock::SlotClock; use std::sync::Arc; use types::{graffiti::GraffitiString, EthSpec, Graffiti}; -use validator_store::ValidatorStore; pub fn get_graffiti( validator_pubkey: PublicKey, - validator_store: Arc>, + validator_store: Arc>, graffiti_flag: Option, ) -> Result { let initialized_validators_rw_lock = validator_store.initialized_validators(); @@ -29,7 +29,7 @@ pub fn get_graffiti( pub fn set_graffiti( validator_pubkey: PublicKey, graffiti: GraffitiString, - validator_store: Arc>, + validator_store: Arc>, ) -> Result<(), warp::Rejection> { let initialized_validators_rw_lock = validator_store.initialized_validators(); let mut initialized_validators = initialized_validators_rw_lock.write(); @@ -55,7 +55,7 @@ pub fn set_graffiti( pub fn delete_graffiti( validator_pubkey: PublicKey, - validator_store: Arc>, + validator_store: Arc>, ) -> Result<(), warp::Rejection> { let initialized_validators_rw_lock = validator_store.initialized_validators(); let mut initialized_validators = initialized_validators_rw_lock.write(); diff --git a/validator_client/http_api/src/keystores.rs b/validator_client/http_api/src/keystores.rs index c2bcfe5ab4..302b21d7d8 100644 --- a/validator_client/http_api/src/keystores.rs +++ b/validator_client/http_api/src/keystores.rs @@ -10,6 +10,7 @@ use eth2::lighthouse_vc::{ }; use eth2_keystore::Keystore; use initialized_validators::{Error, InitializedValidators}; +use lighthouse_validator_store::LighthouseValidatorStore; use signing_method::SigningMethod; use slot_clock::SlotClock; use std::path::PathBuf; @@ -19,13 +20,12 @@ use tokio::runtime::Handle; use tracing::{info, warn}; use types::{EthSpec, PublicKeyBytes}; use validator_dir::{keystore_password_path, Builder as ValidatorDirBuilder}; -use validator_store::ValidatorStore; use warp::Rejection; use warp_utils::reject::{custom_bad_request, custom_server_error}; use zeroize::Zeroizing; pub fn list( - validator_store: Arc>, + validator_store: Arc>, ) -> ListKeystoresResponse { let initialized_validators_rwlock = validator_store.initialized_validators(); let initialized_validators = initialized_validators_rwlock.read(); @@ -62,7 +62,7 @@ pub fn import( request: ImportKeystoresRequest, validator_dir: PathBuf, secrets_dir: Option, - validator_store: Arc>, + validator_store: Arc>, task_executor: TaskExecutor, ) -> Result { // Check request validity. This is the only cases in which we should return a 4xx code. @@ -117,7 +117,7 @@ pub fn import( ) } else if let Some(handle) = task_executor.handle() { // Import the keystore. - match import_single_keystore( + match import_single_keystore::<_, E>( keystore, password, validator_dir.clone(), @@ -164,7 +164,7 @@ fn import_single_keystore( password: Zeroizing, validator_dir_path: PathBuf, secrets_dir: Option, - validator_store: &ValidatorStore, + validator_store: &LighthouseValidatorStore, handle: Handle, ) -> Result { // Check if the validator key already exists, erroring if it is a remote signer validator. @@ -234,7 +234,7 @@ fn import_single_keystore( pub fn delete( request: DeleteKeystoresRequest, - validator_store: Arc>, + validator_store: Arc>, task_executor: TaskExecutor, ) -> Result { let export_response = export(request, validator_store, task_executor)?; @@ -265,7 +265,7 @@ pub fn delete( pub fn export( request: DeleteKeystoresRequest, - validator_store: Arc>, + validator_store: Arc>, task_executor: TaskExecutor, ) -> Result { // Remove from initialized validators. diff --git a/validator_client/http_api/src/lib.rs b/validator_client/http_api/src/lib.rs index 5bb4747bfe..aebe179567 100644 --- a/validator_client/http_api/src/lib.rs +++ b/validator_client/http_api/src/lib.rs @@ -13,6 +13,7 @@ use graffiti::{delete_graffiti, get_graffiti, set_graffiti}; use create_signed_voluntary_exit::create_signed_voluntary_exit; use graffiti_file::{determine_graffiti, GraffitiFile}; +use lighthouse_validator_store::LighthouseValidatorStore; use validator_store::ValidatorStore; use account_utils::{ @@ -41,7 +42,6 @@ use serde::{Deserialize, Serialize}; use slot_clock::SlotClock; use std::collections::HashMap; use std::future::Future; -use std::marker::PhantomData; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; use std::sync::Arc; @@ -77,11 +77,11 @@ impl From for Error { /// 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 task_executor: TaskExecutor, pub api_secret: ApiSecret, - pub block_service: Option>, - pub validator_store: Option>>, + pub block_service: Option, T>>, + pub validator_store: Option>>, pub validator_dir: Option, pub secrets_dir: Option, pub graffiti_file: Option, @@ -90,7 +90,6 @@ pub struct Context { pub config: Config, pub sse_logging_components: Option, pub slot_clock: T, - pub _phantom: PhantomData, } /// Configuration for the HTTP server. @@ -320,7 +319,7 @@ pub fn serve( .and(warp::path("validators")) .and(warp::path::end()) .and(validator_store_filter.clone()) - .then(|validator_store: Arc>| { + .then(|validator_store: Arc>| { blocking_json_task(move || { let validators = validator_store .initialized_validators() @@ -345,7 +344,7 @@ pub fn serve( .and(warp::path::end()) .and(validator_store_filter.clone()) .then( - |validator_pubkey: PublicKey, validator_store: Arc>| { + |validator_pubkey: PublicKey, validator_store: Arc>| { blocking_json_task(move || { let validator = validator_store .initialized_validators() @@ -395,7 +394,7 @@ pub fn serve( .and(graffiti_file_filter.clone()) .and(graffiti_flag_filter) .then( - |validator_store: Arc>, + |validator_store: Arc>, graffiti_file: Option, graffiti_flag: Option| { blocking_json_task(move || { @@ -418,39 +417,41 @@ pub fn serve( }, ); - // GET lighthouse/ui/fallback_health - let get_lighthouse_ui_fallback_health = warp::path("lighthouse") - .and(warp::path("ui")) - .and(warp::path("fallback_health")) + // GET lighthouse/beacon/health + let get_lighthouse_beacon_health = warp::path("lighthouse") + .and(warp::path("beacon")) + .and(warp::path("health")) .and(warp::path::end()) .and(block_service_filter.clone()) - .then(|block_filter: BlockService| async move { - let mut result: HashMap> = HashMap::new(); + .then( + |block_filter: BlockService, T>| async move { + let mut result: HashMap> = HashMap::new(); - let mut beacon_nodes = Vec::new(); - for node in &*block_filter.beacon_nodes.candidates.read().await { - beacon_nodes.push(CandidateInfo { - index: node.index, - endpoint: node.beacon_node.to_string(), - health: *node.health.read().await, - }); - } - result.insert("beacon_nodes".to_string(), beacon_nodes); - - if let Some(proposer_nodes_list) = &block_filter.proposer_nodes { - let mut proposer_nodes = Vec::new(); - for node in &*proposer_nodes_list.candidates.read().await { - proposer_nodes.push(CandidateInfo { + let mut beacon_nodes = Vec::new(); + for node in &*block_filter.beacon_nodes.candidates.read().await { + beacon_nodes.push(CandidateInfo { index: node.index, endpoint: node.beacon_node.to_string(), health: *node.health.read().await, }); } - result.insert("proposer_nodes".to_string(), proposer_nodes); - } + result.insert("beacon_nodes".to_string(), beacon_nodes); - blocking_json_task(move || Ok(api_types::GenericResponse::from(result))).await - }); + if let Some(proposer_nodes_list) = &block_filter.proposer_nodes { + let mut proposer_nodes = Vec::new(); + for node in &*proposer_nodes_list.candidates.read().await { + proposer_nodes.push(CandidateInfo { + index: node.index, + endpoint: node.beacon_node.to_string(), + health: *node.health.read().await, + }); + } + result.insert("proposer_nodes".to_string(), proposer_nodes); + } + + blocking_json_task(move || Ok(api_types::GenericResponse::from(result))).await + }, + ); // POST lighthouse/validators/ let post_validators = warp::path("lighthouse") @@ -466,14 +467,14 @@ pub fn serve( move |body: Vec, validator_dir: PathBuf, secrets_dir: PathBuf, - validator_store: Arc>, + validator_store: Arc>, spec: Arc, task_executor: TaskExecutor| { blocking_json_task(move || { let secrets_dir = store_passwords_in_secrets_dir.then_some(secrets_dir); if let Some(handle) = task_executor.handle() { let (validators, mnemonic) = - handle.block_on(create_validators_mnemonic( + handle.block_on(create_validators_mnemonic::<_, _, E>( None, None, &body, @@ -511,7 +512,7 @@ pub fn serve( move |body: api_types::CreateValidatorsMnemonicRequest, validator_dir: PathBuf, secrets_dir: PathBuf, - validator_store: Arc>, + validator_store: Arc>, spec: Arc, task_executor: TaskExecutor| { blocking_json_task(move || { @@ -525,7 +526,7 @@ pub fn serve( )) })?; let (validators, _mnemonic) = - handle.block_on(create_validators_mnemonic( + handle.block_on(create_validators_mnemonic::<_, _, E>( Some(mnemonic), Some(body.key_derivation_path_offset), &body.validators, @@ -558,7 +559,7 @@ pub fn serve( move |body: api_types::KeystoreValidatorsPostRequest, validator_dir: PathBuf, secrets_dir: PathBuf, - validator_store: Arc>, + validator_store: Arc>, task_executor: TaskExecutor| { blocking_json_task(move || { // Check to ensure the password is correct. @@ -644,7 +645,7 @@ pub fn serve( .and(task_executor_filter.clone()) .then( |body: Vec, - validator_store: Arc>, + validator_store: Arc>, task_executor: TaskExecutor| { blocking_json_task(move || { if let Some(handle) = task_executor.handle() { @@ -672,7 +673,7 @@ pub fn serve( ), }) .collect(); - handle.block_on(create_validators_web3signer( + handle.block_on(create_validators_web3signer::<_, E>( web3signers, &validator_store, ))?; @@ -698,7 +699,7 @@ pub fn serve( .then( |validator_pubkey: PublicKey, body: api_types::ValidatorPatchRequest, - validator_store: Arc>, + validator_store: Arc>, graffiti_file: Option, task_executor: TaskExecutor| { blocking_json_task(move || { @@ -851,7 +852,7 @@ pub fn serve( .and(warp::path::end()) .and(validator_store_filter.clone()) .then( - |validator_pubkey: PublicKey, validator_store: Arc>| { + |validator_pubkey: PublicKey, validator_store: Arc>| { blocking_json_task(move || { if validator_store .initialized_validators() @@ -892,7 +893,7 @@ pub fn serve( .then( |validator_pubkey: PublicKey, request: api_types::UpdateFeeRecipientRequest, - validator_store: Arc>| { + validator_store: Arc>| { blocking_json_task(move || { if validator_store .initialized_validators() @@ -928,7 +929,7 @@ pub fn serve( .and(warp::path::end()) .and(validator_store_filter.clone()) .then( - |validator_pubkey: PublicKey, validator_store: Arc>| { + |validator_pubkey: PublicKey, validator_store: Arc>| { blocking_json_task(move || { if validator_store .initialized_validators() @@ -964,7 +965,7 @@ pub fn serve( .and(warp::path::end()) .and(validator_store_filter.clone()) .then( - |validator_pubkey: PublicKey, validator_store: Arc>| { + |validator_pubkey: PublicKey, validator_store: Arc>| { blocking_json_task(move || { if validator_store .initialized_validators() @@ -997,7 +998,7 @@ pub fn serve( .then( |validator_pubkey: PublicKey, request: api_types::UpdateGasLimitRequest, - validator_store: Arc>| { + validator_store: Arc>| { blocking_json_task(move || { if validator_store .initialized_validators() @@ -1033,7 +1034,7 @@ pub fn serve( .and(warp::path::end()) .and(validator_store_filter.clone()) .then( - |validator_pubkey: PublicKey, validator_store: Arc>| { + |validator_pubkey: PublicKey, validator_store: Arc>| { blocking_json_task(move || { if validator_store .initialized_validators() @@ -1074,13 +1075,13 @@ pub fn serve( .then( |pubkey: PublicKey, query: api_types::VoluntaryExitQuery, - validator_store: Arc>, + validator_store: Arc>, slot_clock: T, task_executor: TaskExecutor| { blocking_json_task(move || { if let Some(handle) = task_executor.handle() { let signed_voluntary_exit = - handle.block_on(create_signed_voluntary_exit( + handle.block_on(create_signed_voluntary_exit::( pubkey, query.epoch, validator_store, @@ -1106,7 +1107,7 @@ pub fn serve( .and(graffiti_flag_filter) .then( |pubkey: PublicKey, - validator_store: Arc>, + validator_store: Arc>, graffiti_flag: Option| { blocking_json_task(move || { let graffiti = get_graffiti(pubkey.clone(), validator_store, graffiti_flag)?; @@ -1130,7 +1131,7 @@ pub fn serve( .then( |pubkey: PublicKey, query: SetGraffitiRequest, - validator_store: Arc>, + validator_store: Arc>, graffiti_file: Option| { blocking_json_task(move || { if graffiti_file.is_some() { @@ -1155,7 +1156,7 @@ pub fn serve( .and(graffiti_file_filter.clone()) .then( |pubkey: PublicKey, - validator_store: Arc>, + validator_store: Arc>, graffiti_file: Option| { blocking_json_task(move || { if graffiti_file.is_some() { @@ -1172,7 +1173,7 @@ pub fn serve( // GET /eth/v1/keystores let get_std_keystores = std_keystores.and(validator_store_filter.clone()).then( - |validator_store: Arc>| { + |validator_store: Arc>| { blocking_json_task(move || Ok(keystores::list(validator_store))) }, ); @@ -1188,7 +1189,7 @@ pub fn serve( move |request, validator_dir, secrets_dir, validator_store, task_executor| { let secrets_dir = store_passwords_in_secrets_dir.then_some(secrets_dir); blocking_json_task(move || { - keystores::import( + keystores::import::<_, E>( request, validator_dir, secrets_dir, @@ -1210,7 +1211,7 @@ pub fn serve( // GET /eth/v1/remotekeys let get_std_remotekeys = std_remotekeys.and(validator_store_filter.clone()).then( - |validator_store: Arc>| { + |validator_store: Arc>| { blocking_json_task(move || Ok(remotekeys::list(validator_store))) }, ); @@ -1221,7 +1222,9 @@ pub fn serve( .and(validator_store_filter.clone()) .and(task_executor_filter.clone()) .then(|request, validator_store, task_executor| { - blocking_json_task(move || remotekeys::import(request, validator_store, task_executor)) + blocking_json_task(move || { + remotekeys::import::<_, E>(request, validator_store, task_executor) + }) }); // DELETE /eth/v1/remotekeys @@ -1294,7 +1297,7 @@ pub fn serve( .or(get_lighthouse_validators_pubkey) .or(get_lighthouse_ui_health) .or(get_lighthouse_ui_graffiti) - .or(get_lighthouse_ui_fallback_health) + .or(get_lighthouse_beacon_health) .or(get_fee_recipient) .or(get_gas_limit) .or(get_graffiti) diff --git a/validator_client/http_api/src/remotekeys.rs b/validator_client/http_api/src/remotekeys.rs index 49d666f303..5aa63baac3 100644 --- a/validator_client/http_api/src/remotekeys.rs +++ b/validator_client/http_api/src/remotekeys.rs @@ -8,6 +8,7 @@ use eth2::lighthouse_vc::std_types::{ ListRemotekeysResponse, SingleListRemotekeysResponse, Status, }; use initialized_validators::{Error, InitializedValidators}; +use lighthouse_validator_store::LighthouseValidatorStore; use slot_clock::SlotClock; use std::sync::Arc; use task_executor::TaskExecutor; @@ -15,12 +16,11 @@ use tokio::runtime::Handle; use tracing::{info, warn}; use types::{EthSpec, PublicKeyBytes}; use url::Url; -use validator_store::ValidatorStore; use warp::Rejection; use warp_utils::reject::custom_server_error; pub fn list( - validator_store: Arc>, + validator_store: Arc>, ) -> ListRemotekeysResponse { let initialized_validators_rwlock = validator_store.initialized_validators(); let initialized_validators = initialized_validators_rwlock.read(); @@ -50,7 +50,7 @@ pub fn list( pub fn import( request: ImportRemotekeysRequest, - validator_store: Arc>, + validator_store: Arc>, task_executor: TaskExecutor, ) -> Result { info!( @@ -63,8 +63,12 @@ pub fn import( for remotekey in request.remote_keys { let status = if let Some(handle) = task_executor.handle() { // Import the keystore. - match import_single_remotekey(remotekey.pubkey, remotekey.url, &validator_store, handle) - { + match import_single_remotekey::<_, E>( + remotekey.pubkey, + remotekey.url, + &validator_store, + handle, + ) { Ok(status) => Status::ok(status), Err(e) => { warn!( @@ -89,7 +93,7 @@ pub fn import( fn import_single_remotekey( pubkey: PublicKeyBytes, url: String, - validator_store: &ValidatorStore, + validator_store: &LighthouseValidatorStore, handle: Handle, ) -> Result { if let Err(url_err) = Url::parse(&url) { @@ -143,7 +147,7 @@ fn import_single_remotekey( pub fn delete( request: DeleteRemotekeysRequest, - validator_store: Arc>, + validator_store: Arc>, task_executor: TaskExecutor, ) -> Result { info!( diff --git a/validator_client/http_api/src/test_utils.rs b/validator_client/http_api/src/test_utils.rs index 4a5d3b6cc7..08447a82ce 100644 --- a/validator_client/http_api/src/test_utils.rs +++ b/validator_client/http_api/src/test_utils.rs @@ -14,19 +14,19 @@ use eth2::{ use eth2_keystore::KeystoreBuilder; use initialized_validators::key_cache::{KeyCache, CACHE_FILENAME}; use initialized_validators::{InitializedValidators, OnDecryptFailure}; +use lighthouse_validator_store::{Config as ValidatorStoreConfig, LighthouseValidatorStore}; use parking_lot::RwLock; use sensitive_url::SensitiveUrl; use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; use slot_clock::{SlotClock, TestingSlotClock}; use std::future::Future; -use std::marker::PhantomData; use std::net::{IpAddr, Ipv4Addr}; use std::sync::Arc; use std::time::Duration; use task_executor::test_utils::TestRuntime; use tempfile::{tempdir, TempDir}; use tokio::sync::oneshot; -use validator_store::{Config as ValidatorStoreConfig, ValidatorStore}; +use validator_services::block_service::BlockService; use zeroize::Zeroizing; pub const PASSWORD_BYTES: &[u8] = &[42, 50, 37]; @@ -54,7 +54,7 @@ pub struct Web3SignerValidatorScenario { pub struct ApiTester { pub client: ValidatorClientHttpClient, pub initialized_validators: Arc>, - pub validator_store: Arc>, + pub validator_store: Arc>, pub url: SensitiveUrl, pub api_token: String, pub test_runtime: TestRuntime, @@ -101,7 +101,7 @@ impl ApiTester { let test_runtime = TestRuntime::default(); - let validator_store = Arc::new(ValidatorStore::<_, E>::new( + let validator_store = Arc::new(LighthouseValidatorStore::new( initialized_validators, slashing_protection, Hash256::repeat_byte(42), @@ -121,7 +121,7 @@ impl ApiTester { let context = Arc::new(Context { task_executor: test_runtime.task_executor.clone(), api_secret, - block_service: None, + block_service: None::, _>>, validator_dir: Some(validator_dir.path().into()), secrets_dir: Some(secrets_dir.path().into()), validator_store: Some(validator_store.clone()), @@ -131,7 +131,6 @@ impl ApiTester { config: http_config, sse_logging_components: None, slot_clock, - _phantom: PhantomData, }); let ctx = context; let (shutdown_tx, shutdown_rx) = oneshot::channel(); @@ -139,7 +138,7 @@ impl ApiTester { // It's not really interesting why this triggered, just that it happened. let _ = shutdown_rx.await; }; - let (listening_socket, server) = super::serve(ctx, server_shutdown).unwrap(); + let (listening_socket, server) = super::serve::<_, E>(ctx, server_shutdown).unwrap(); tokio::spawn(server); @@ -638,7 +637,7 @@ impl ApiTester { assert_eq!( self.validator_store - .get_builder_proposals(&validator.voting_pubkey), + .get_builder_proposals_testing_only(&validator.voting_pubkey), builder_proposals ); diff --git a/validator_client/http_api/src/tests.rs b/validator_client/http_api/src/tests.rs index 5468718fb5..4b1a3c0059 100644 --- a/validator_client/http_api/src/tests.rs +++ b/validator_client/http_api/src/tests.rs @@ -18,12 +18,12 @@ use eth2::{ Error as ApiError, }; use eth2_keystore::KeystoreBuilder; +use lighthouse_validator_store::{Config as ValidatorStoreConfig, LighthouseValidatorStore}; use parking_lot::RwLock; use sensitive_url::SensitiveUrl; use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; use slot_clock::{SlotClock, TestingSlotClock}; use std::future::Future; -use std::marker::PhantomData; use std::net::{IpAddr, Ipv4Addr}; use std::str::FromStr; use std::sync::Arc; @@ -31,7 +31,7 @@ use std::time::Duration; use task_executor::test_utils::TestRuntime; use tempfile::{tempdir, TempDir}; use types::graffiti::GraffitiString; -use validator_store::{Config as ValidatorStoreConfig, ValidatorStore}; +use validator_store::ValidatorStore; use zeroize::Zeroizing; const PASSWORD_BYTES: &[u8] = &[42, 50, 37]; @@ -42,7 +42,7 @@ type E = MainnetEthSpec; struct ApiTester { client: ValidatorClientHttpClient, initialized_validators: Arc>, - validator_store: Arc>, + validator_store: Arc>, url: SensitiveUrl, slot_clock: TestingSlotClock, _validator_dir: TempDir, @@ -91,7 +91,7 @@ impl ApiTester { let test_runtime = TestRuntime::default(); - let validator_store = Arc::new(ValidatorStore::<_, E>::new( + let validator_store = Arc::new(LighthouseValidatorStore::<_, E>::new( initialized_validators, slashing_protection, Hash256::repeat_byte(42), @@ -129,11 +129,10 @@ impl ApiTester { }, sse_logging_components: None, slot_clock: slot_clock.clone(), - _phantom: PhantomData, }); let ctx = context.clone(); let (listening_socket, server) = - super::serve(ctx, test_runtime.task_executor.exit()).unwrap(); + super::serve::<_, E>(ctx, test_runtime.task_executor.exit()).unwrap(); tokio::spawn(server); @@ -670,7 +669,7 @@ impl ApiTester { assert_eq!( self.validator_store - .get_builder_proposals(&validator.voting_pubkey), + .get_builder_proposals_testing_only(&validator.voting_pubkey), builder_proposals ); @@ -686,7 +685,7 @@ impl ApiTester { assert_eq!( self.validator_store - .get_builder_boost_factor(&validator.voting_pubkey), + .get_builder_boost_factor_testing_only(&validator.voting_pubkey), builder_boost_factor ); @@ -702,7 +701,7 @@ impl ApiTester { assert_eq!( self.validator_store - .determine_validator_builder_boost_factor(&validator.voting_pubkey), + .determine_builder_boost_factor(&validator.voting_pubkey), builder_boost_factor ); @@ -712,7 +711,7 @@ impl ApiTester { pub fn assert_default_builder_boost_factor(self, builder_boost_factor: Option) -> Self { assert_eq!( self.validator_store - .determine_default_builder_boost_factor(), + .determine_builder_boost_factor(&PublicKeyBytes::empty()), builder_boost_factor ); @@ -728,7 +727,7 @@ impl ApiTester { assert_eq!( self.validator_store - .get_prefer_builder_proposals(&validator.voting_pubkey), + .get_prefer_builder_proposals_testing_only(&validator.voting_pubkey), prefer_builder_proposals ); @@ -1159,7 +1158,7 @@ async fn validator_derived_builder_boost_factor_with_process_defaults() { }) .await .assert_default_builder_boost_factor(Some(80)) - .assert_validator_derived_builder_boost_factor(0, None) + .assert_validator_derived_builder_boost_factor(0, Some(80)) .await .set_builder_proposals(0, false) .await diff --git a/validator_client/http_api/src/tests/keystores.rs b/validator_client/http_api/src/tests/keystores.rs index 13494e5fa6..37f7513f37 100644 --- a/validator_client/http_api/src/tests/keystores.rs +++ b/validator_client/http_api/src/tests/keystores.rs @@ -8,12 +8,13 @@ use eth2::lighthouse_vc::{ types::Web3SignerValidatorRequest, }; use itertools::Itertools; +use lighthouse_validator_store::DEFAULT_GAS_LIMIT; use rand::{rngs::SmallRng, Rng, SeedableRng}; use slashing_protection::interchange::{Interchange, InterchangeMetadata}; use std::{collections::HashMap, path::Path}; use tokio::runtime::Handle; use types::{attestation::AttestationBase, Address}; -use validator_store::DEFAULT_GAS_LIMIT; +use validator_store::ValidatorStore; use zeroize::Zeroizing; fn new_keystore(password: Zeroizing) -> Keystore { diff --git a/validator_client/http_metrics/Cargo.toml b/validator_client/http_metrics/Cargo.toml index f2684da4b1..24cbff7cde 100644 --- a/validator_client/http_metrics/Cargo.toml +++ b/validator_client/http_metrics/Cargo.toml @@ -6,6 +6,7 @@ authors = ["Sigma Prime "] [dependencies] health_metrics = { workspace = true } +lighthouse_validator_store = { workspace = true } lighthouse_version = { workspace = true } logging = { workspace = true } malloc_utils = { workspace = true } @@ -17,6 +18,5 @@ tracing = { workspace = true } types = { workspace = true } validator_metrics = { workspace = true } validator_services = { workspace = true } -validator_store = { workspace = true } warp = { workspace = true } warp_utils = { workspace = true } diff --git a/validator_client/http_metrics/src/lib.rs b/validator_client/http_metrics/src/lib.rs index 6bf18e7b93..7441939957 100644 --- a/validator_client/http_metrics/src/lib.rs +++ b/validator_client/http_metrics/src/lib.rs @@ -2,6 +2,7 @@ //! //! For other endpoints, see the `http_api` crate. +use lighthouse_validator_store::LighthouseValidatorStore; use lighthouse_version::version_with_platform; use logging::crit; use malloc_utils::scrape_allocator_metrics; @@ -15,7 +16,6 @@ use std::time::{SystemTime, UNIX_EPOCH}; use tracing::info; use types::EthSpec; use validator_services::duties_service::DutiesService; -use validator_store::ValidatorStore; use warp::{http::Response, Filter}; #[derive(Debug)] @@ -36,17 +36,19 @@ impl From for Error { } } +type ValidatorStore = LighthouseValidatorStore; + /// 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, SystemTimeSlotClock>>>, 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>, } diff --git a/validator_client/lighthouse_validator_store/Cargo.toml b/validator_client/lighthouse_validator_store/Cargo.toml new file mode 100644 index 0000000000..0f8220bdc9 --- /dev/null +++ b/validator_client/lighthouse_validator_store/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "lighthouse_validator_store" +version = "0.1.0" +edition = { workspace = true } +authors = ["Sigma Prime "] + +[dependencies] +account_utils = { workspace = true } +beacon_node_fallback = { workspace = true } +doppelganger_service = { workspace = true } +either = { workspace = true } +environment = { workspace = true } +eth2 = { workspace = true } +initialized_validators = { workspace = true } +logging = { workspace = true } +parking_lot = { workspace = true } +serde = { workspace = true } +signing_method = { workspace = true } +slashing_protection = { workspace = true } +slot_clock = { workspace = true } +task_executor = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +types = { workspace = true } +validator_metrics = { workspace = true } +validator_store = { workspace = true } + +[dev-dependencies] +futures = { workspace = true } +logging = { workspace = true } diff --git a/validator_client/lighthouse_validator_store/src/lib.rs b/validator_client/lighthouse_validator_store/src/lib.rs new file mode 100644 index 0000000000..d7cf6fe36c --- /dev/null +++ b/validator_client/lighthouse_validator_store/src/lib.rs @@ -0,0 +1,1160 @@ +use account_utils::validator_definitions::{PasswordStorage, ValidatorDefinition}; +use doppelganger_service::DoppelgangerService; +use eth2::types::PublishBlockRequest; +use initialized_validators::InitializedValidators; +use logging::crit; +use parking_lot::{Mutex, RwLock}; +use serde::{Deserialize, Serialize}; +use signing_method::Error as SigningError; +use signing_method::{SignableMessage, SigningContext, SigningMethod}; +use slashing_protection::{ + interchange::Interchange, InterchangeError, NotSafe, Safe, SlashingDatabase, +}; +use slot_clock::SlotClock; +use std::marker::PhantomData; +use std::path::Path; +use std::sync::Arc; +use task_executor::TaskExecutor; +use tracing::{error, info, warn}; +use types::SignedInclusionList; +use types::{ + graffiti::GraffitiString, AbstractExecPayload, Address, AggregateAndProof, Attestation, + BeaconBlock, BlindedPayload, ChainSpec, ContributionAndProof, Domain, Epoch, EthSpec, Fork, + Graffiti, Hash256, InclusionList, PublicKeyBytes, SelectionProof, Signature, + SignedAggregateAndProof, SignedBeaconBlock, SignedContributionAndProof, SignedRoot, + SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, SyncAggregatorSelectionData, + SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, + ValidatorRegistrationData, VoluntaryExit, +}; +use validator_store::{ + DoppelgangerStatus, Error as ValidatorStoreError, ProposalData, SignedBlock, UnsignedBlock, + ValidatorStore, +}; + +pub type Error = ValidatorStoreError; + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct Config { + /// Fallback fee recipient address. + pub fee_recipient: Option
, + /// Fallback gas limit. + pub gas_limit: Option, + /// Enable use of the blinded block endpoints during proposals. + pub builder_proposals: bool, + /// Enable slashing protection even while using web3signer keys. + pub enable_web3signer_slashing_protection: bool, + /// If true, Lighthouse will prefer builder proposals, if available. + pub prefer_builder_proposals: bool, + /// Specifies the boost factor, a percentage multiplier to apply to the builder's payload value. + pub builder_boost_factor: Option, +} + +/// Number of epochs of slashing protection history to keep. +/// +/// This acts as a maximum safe-guard against clock drift. +const SLASHING_PROTECTION_HISTORY_EPOCHS: u64 = 512; + +/// Currently used as the default gas limit in execution clients. +/// +/// https://ethresear.ch/t/on-increasing-the-block-gas-limit-technical-considerations-path-forward/21225. +pub const DEFAULT_GAS_LIMIT: u64 = 36_000_000; + +pub struct LighthouseValidatorStore { + validators: Arc>, + slashing_protection: SlashingDatabase, + slashing_protection_last_prune: Arc>, + genesis_validators_root: Hash256, + spec: Arc, + doppelganger_service: Option>, + slot_clock: T, + fee_recipient_process: Option
, + gas_limit: Option, + builder_proposals: bool, + enable_web3signer_slashing_protection: bool, + prefer_builder_proposals: bool, + builder_boost_factor: Option, + task_executor: TaskExecutor, + _phantom: PhantomData, +} + +impl LighthouseValidatorStore { + // All arguments are different types. Making the fields `pub` is undesired. A builder seems + // unnecessary. + #[allow(clippy::too_many_arguments)] + pub fn new( + validators: InitializedValidators, + slashing_protection: SlashingDatabase, + genesis_validators_root: Hash256, + spec: Arc, + doppelganger_service: Option>, + slot_clock: T, + config: &Config, + task_executor: TaskExecutor, + ) -> Self { + Self { + validators: Arc::new(RwLock::new(validators)), + slashing_protection, + slashing_protection_last_prune: Arc::new(Mutex::new(Epoch::new(0))), + genesis_validators_root, + spec, + doppelganger_service, + slot_clock, + 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, + prefer_builder_proposals: config.prefer_builder_proposals, + builder_boost_factor: config.builder_boost_factor, + task_executor, + _phantom: PhantomData, + } + } + + /// Register all local validators in doppelganger protection to try and prevent instances of + /// duplicate validators operating on the network at the same time. + /// + /// This function has no effect if doppelganger protection is disabled. + pub fn register_all_in_doppelganger_protection_if_enabled(&self) -> Result<(), String> { + if let Some(doppelganger_service) = &self.doppelganger_service { + for pubkey in self.validators.read().iter_voting_pubkeys() { + doppelganger_service.register_new_validator( + *pubkey, + &self.slot_clock, + E::slots_per_epoch(), + )? + } + } + + Ok(()) + } + + /// Returns `true` if doppelganger protection is enabled, or else `false`. + pub fn doppelganger_protection_enabled(&self) -> bool { + self.doppelganger_service.is_some() + } + + pub fn initialized_validators(&self) -> Arc> { + self.validators.clone() + } + + /// Indicates if the `voting_public_key` exists in self and is enabled. + pub fn has_validator(&self, voting_public_key: &PublicKeyBytes) -> bool { + self.validators + .read() + .validator(voting_public_key) + .is_some() + } + + /// Insert a new validator to `self`, where the validator is represented by an EIP-2335 + /// keystore on the filesystem. + #[allow(clippy::too_many_arguments)] + pub async fn add_validator_keystore>( + &self, + voting_keystore_path: P, + password_storage: PasswordStorage, + enable: bool, + graffiti: Option, + suggested_fee_recipient: Option
, + gas_limit: Option, + builder_proposals: Option, + builder_boost_factor: Option, + prefer_builder_proposals: Option, + ) -> Result { + let mut validator_def = ValidatorDefinition::new_keystore_with_password( + voting_keystore_path, + password_storage, + graffiti, + suggested_fee_recipient, + gas_limit, + builder_proposals, + builder_boost_factor, + prefer_builder_proposals, + ) + .map_err(|e| format!("failed to create validator definitions: {:?}", e))?; + + validator_def.enabled = enable; + + self.add_validator(validator_def).await + } + + /// Insert a new validator to `self`. + /// + /// This function includes: + /// + /// - Adding the validator definition to the YAML file, saving it to the filesystem. + /// - Enabling the validator with the slashing protection database. + /// - If `enable == true`, starting to perform duties for the validator. + // FIXME: ignore this clippy lint until the validator store is refactored to use async locks + #[allow(clippy::await_holding_lock)] + pub async fn add_validator( + &self, + validator_def: ValidatorDefinition, + ) -> Result { + let validator_pubkey = validator_def.voting_public_key.compress(); + + self.slashing_protection + .register_validator(validator_pubkey) + .map_err(|e| format!("failed to register validator: {:?}", e))?; + + if let Some(doppelganger_service) = &self.doppelganger_service { + doppelganger_service.register_new_validator( + validator_pubkey, + &self.slot_clock, + E::slots_per_epoch(), + )?; + } + + self.validators + .write() + .add_definition_replace_disabled(validator_def.clone()) + .await + .map_err(|e| format!("Unable to add definition: {:?}", e))?; + + Ok(validator_def) + } + + /// Returns doppelganger statuses for all enabled validators. + #[allow(clippy::needless_collect)] // Collect is required to avoid holding a lock. + pub fn doppelganger_statuses(&self) -> Vec { + // Collect all the pubkeys first to avoid interleaving locks on `self.validators` and + // `self.doppelganger_service`. + let pubkeys = self + .validators + .read() + .iter_voting_pubkeys() + .cloned() + .collect::>(); + + pubkeys + .into_iter() + .map(|pubkey| { + self.doppelganger_service + .as_ref() + .map(|doppelganger_service| doppelganger_service.validator_status(pubkey)) + // Allow signing on all pubkeys if doppelganger protection is disabled. + .unwrap_or_else(|| DoppelgangerStatus::SigningEnabled(pubkey)) + }) + .collect() + } + + fn fork(&self, epoch: Epoch) -> Fork { + self.spec.fork_at_epoch(epoch) + } + + /// Returns a `SigningMethod` for `validator_pubkey` *only if* that validator is considered safe + /// by doppelganger protection. + fn doppelganger_checked_signing_method( + &self, + validator_pubkey: PublicKeyBytes, + ) -> Result, Error> { + if self.doppelganger_protection_allows_signing(validator_pubkey) { + self.validators + .read() + .signing_method(&validator_pubkey) + .ok_or(Error::UnknownPubkey(validator_pubkey)) + } else { + Err(Error::DoppelgangerProtected(validator_pubkey)) + } + } + + /// Returns a `SigningMethod` for `validator_pubkey` regardless of that validators doppelganger + /// protection status. + /// + /// ## Warning + /// + /// This method should only be used for signing non-slashable messages. + fn doppelganger_bypassed_signing_method( + &self, + validator_pubkey: PublicKeyBytes, + ) -> Result, Error> { + self.validators + .read() + .signing_method(&validator_pubkey) + .ok_or(Error::UnknownPubkey(validator_pubkey)) + } + + fn signing_context(&self, domain: Domain, signing_epoch: Epoch) -> SigningContext { + if domain == Domain::VoluntaryExit { + if self.spec.fork_name_at_epoch(signing_epoch).deneb_enabled() { + // EIP-7044 + SigningContext { + domain, + epoch: signing_epoch, + fork: Fork { + previous_version: self.spec.capella_fork_version, + current_version: self.spec.capella_fork_version, + epoch: signing_epoch, + }, + genesis_validators_root: self.genesis_validators_root, + } + } else { + SigningContext { + domain, + epoch: signing_epoch, + fork: self.fork(signing_epoch), + genesis_validators_root: self.genesis_validators_root, + } + } + } else { + SigningContext { + domain, + epoch: signing_epoch, + fork: self.fork(signing_epoch), + genesis_validators_root: self.genesis_validators_root, + } + } + } + + pub fn get_fee_recipient_defaulting(&self, fee_recipient: Option
) -> Option
{ + // If there's nothing in the file, try the process-level default value. + fee_recipient.or(self.fee_recipient_process) + } + + /// Returns the suggested_fee_recipient from `validator_definitions.yml` if any. + /// This has been pulled into a private function so the read lock is dropped easily + fn suggested_fee_recipient(&self, validator_pubkey: &PublicKeyBytes) -> Option
{ + self.validators + .read() + .suggested_fee_recipient(validator_pubkey) + } + + /// Returns the gas limit for the given public key. The priority order for fetching + /// the gas limit is: + /// + /// 1. validator_definitions.yml + /// 2. process level gas limit + /// 3. `DEFAULT_GAS_LIMIT` + pub fn get_gas_limit(&self, validator_pubkey: &PublicKeyBytes) -> u64 { + self.get_gas_limit_defaulting(self.validators.read().gas_limit(validator_pubkey)) + } + + fn get_gas_limit_defaulting(&self, gas_limit: Option) -> u64 { + // If there is a `gas_limit` in the validator definitions yaml + // file, use that value. + gas_limit + // If there's nothing in the file, try the process-level default value. + .or(self.gas_limit) + // If there's no process-level default, use the `DEFAULT_GAS_LIMIT`. + .unwrap_or(DEFAULT_GAS_LIMIT) + } + + /// Returns a `bool` for the given public key that denotes whether this validator should use the + /// builder API. The priority order for fetching this value is: + /// + /// 1. validator_definitions.yml + /// 2. process level flag + /// + /// This function is currently only used in tests because in prod it is translated and combined + /// with other flags into a builder boost factor (see `determine_builder_boost_factor`). + pub fn get_builder_proposals_testing_only(&self, validator_pubkey: &PublicKeyBytes) -> bool { + // If there is a `suggested_fee_recipient` in the validator definitions yaml + // file, use that value. + self.get_builder_proposals_defaulting( + self.validators.read().builder_proposals(validator_pubkey), + ) + } + + fn get_builder_proposals_defaulting(&self, builder_proposals: Option) -> bool { + builder_proposals + // If there's nothing in the file, try the process-level default value. + .unwrap_or(self.builder_proposals) + } + + /// Returns a `u64` for the given public key that denotes the builder boost factor. The priority order for fetching this value is: + /// + /// 1. validator_definitions.yml + /// 2. process level flag + /// + /// This function is currently only used in tests because in prod it is translated and combined + /// with other flags into a builder boost factor (see `determine_builder_boost_factor`). + pub fn get_builder_boost_factor_testing_only( + &self, + validator_pubkey: &PublicKeyBytes, + ) -> Option { + self.validators + .read() + .builder_boost_factor(validator_pubkey) + .or(self.builder_boost_factor) + } + + /// Returns a `bool` for the given public key that denotes whether this validator should prefer a + /// builder payload. The priority order for fetching this value is: + /// + /// 1. validator_definitions.yml + /// 2. process level flag + /// + /// This function is currently only used in tests because in prod it is translated and combined + /// with other flags into a builder boost factor (see `determine_builder_boost_factor`). + pub fn get_prefer_builder_proposals_testing_only( + &self, + validator_pubkey: &PublicKeyBytes, + ) -> bool { + self.validators + .read() + .prefer_builder_proposals(validator_pubkey) + .unwrap_or(self.prefer_builder_proposals) + } + + pub fn import_slashing_protection( + &self, + interchange: Interchange, + ) -> Result<(), InterchangeError> { + self.slashing_protection + .import_interchange_info(interchange, self.genesis_validators_root)?; + Ok(()) + } + + /// Export slashing protection data while also disabling the given keys in the database. + /// + /// If any key is unknown to the slashing protection database it will be silently omitted + /// from the result. It is the caller's responsibility to check whether all keys provided + /// had data returned for them. + pub fn export_slashing_protection_for_keys( + &self, + pubkeys: &[PublicKeyBytes], + ) -> Result { + self.slashing_protection.with_transaction(|txn| { + let known_pubkeys = pubkeys + .iter() + .filter_map(|pubkey| { + let validator_id = self + .slashing_protection + .get_validator_id_ignoring_status(txn, pubkey) + .ok()?; + + Some( + self.slashing_protection + .update_validator_status(txn, validator_id, false) + .map(|()| *pubkey), + ) + }) + .collect::, _>>()?; + self.slashing_protection.export_interchange_info_in_txn( + self.genesis_validators_root, + Some(&known_pubkeys), + txn, + ) + }) + } + + async fn sign_abstract_block>( + &self, + validator_pubkey: PublicKeyBytes, + block: BeaconBlock, + current_slot: Slot, + ) -> Result, Error> { + // Make sure the block slot is not higher than the current slot to avoid potential attacks. + if block.slot() > current_slot { + warn!( + block_slot = block.slot().as_u64(), + current_slot = current_slot.as_u64(), + "Not signing block with slot greater than current slot" + ); + return Err(Error::GreaterThanCurrentSlot { + slot: block.slot(), + current_slot, + }); + } + + let signing_epoch = block.epoch(); + 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 = 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) => { + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_BLOCKS_TOTAL, + &[validator_metrics::SUCCESS], + ); + + let signature = signing_method + .get_signature( + SignableMessage::BeaconBlock(&block), + signing_context, + &self.spec, + &self.task_executor, + ) + .await?; + Ok(SignedBeaconBlock::from_block(block, signature)) + } + Ok(Safe::SameData) => { + warn!("Skipping signing of previously signed block"); + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_BLOCKS_TOTAL, + &[validator_metrics::SAME_DATA], + ); + Err(Error::SameData) + } + Err(NotSafe::UnregisteredValidator(pk)) => { + warn!( + msg = "Carefully consider running with --init-slashing-protection (see --help)", + public_key = ?pk, + "Not signing block for unregistered validator" + ); + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_BLOCKS_TOTAL, + &[validator_metrics::UNREGISTERED], + ); + Err(Error::Slashable(NotSafe::UnregisteredValidator(pk))) + } + Err(e) => { + crit!( + error = ?e, + "Not signing slashable block" + ); + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_BLOCKS_TOTAL, + &[validator_metrics::SLASHABLE], + ); + Err(Error::Slashable(e)) + } + } + } + + pub async fn sign_voluntary_exit( + &self, + validator_pubkey: PublicKeyBytes, + voluntary_exit: VoluntaryExit, + ) -> Result { + let signing_epoch = voluntary_exit.epoch; + let signing_context = self.signing_context(Domain::VoluntaryExit, signing_epoch); + let signing_method = self.doppelganger_bypassed_signing_method(validator_pubkey)?; + + let signature = signing_method + .get_signature::>( + SignableMessage::VoluntaryExit(&voluntary_exit), + signing_context, + &self.spec, + &self.task_executor, + ) + .await?; + + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_VOLUNTARY_EXITS_TOTAL, + &[validator_metrics::SUCCESS], + ); + + Ok(SignedVoluntaryExit { + message: voluntary_exit, + signature, + }) + } +} + +impl ValidatorStore for LighthouseValidatorStore { + type Error = SigningError; + type E = E; + + /// Attempts to resolve the pubkey to a validator index. + /// + /// It may return `None` if the `pubkey` is: + /// + /// - Unknown. + /// - Known, but with an unknown index. + fn validator_index(&self, pubkey: &PublicKeyBytes) -> Option { + self.validators.read().get_index(pubkey) + } + + /// Returns all voting pubkeys for all enabled validators. + /// + /// The `filter_func` allows for filtering pubkeys based upon their `DoppelgangerStatus`. There + /// are two primary functions used here: + /// + /// - `DoppelgangerStatus::only_safe`: only returns pubkeys which have passed doppelganger + /// protection and are safe-enough to sign messages. + /// - `DoppelgangerStatus::ignored`: returns all the pubkeys from `only_safe` *plus* those still + /// undergoing protection. This is useful for collecting duties or other non-signing tasks. + #[allow(clippy::needless_collect)] // Collect is required to avoid holding a lock. + fn voting_pubkeys(&self, filter_func: F) -> I + where + I: FromIterator, + F: Fn(DoppelgangerStatus) -> Option, + { + // Collect all the pubkeys first to avoid interleaving locks on `self.validators` and + // `self.doppelganger_service()`. + let pubkeys = self + .validators + .read() + .iter_voting_pubkeys() + .cloned() + .collect::>(); + + pubkeys + .into_iter() + .map(|pubkey| { + self.doppelganger_service + .as_ref() + .map(|doppelganger_service| doppelganger_service.validator_status(pubkey)) + // Allow signing on all pubkeys if doppelganger protection is disabled. + .unwrap_or_else(|| DoppelgangerStatus::SigningEnabled(pubkey)) + }) + .filter_map(filter_func) + .collect() + } + + /// Check if the `validator_pubkey` is permitted by the doppleganger protection to sign + /// messages. + fn doppelganger_protection_allows_signing(&self, validator_pubkey: PublicKeyBytes) -> bool { + self.doppelganger_service + .as_ref() + // If there's no doppelganger service then we assume it is purposefully disabled and + // declare that all keys are safe with regard to it. + .is_none_or(|doppelganger_service| { + doppelganger_service + .validator_status(validator_pubkey) + .only_safe() + .is_some() + }) + } + + fn num_voting_validators(&self) -> usize { + self.validators.read().num_enabled() + } + + fn graffiti(&self, validator_pubkey: &PublicKeyBytes) -> Option { + self.validators.read().graffiti(validator_pubkey) + } + + /// Returns the fee recipient for the given public key. The priority order for fetching + /// the fee recipient is: + /// 1. validator_definitions.yml + /// 2. process level fee recipient + fn get_fee_recipient(&self, validator_pubkey: &PublicKeyBytes) -> Option
{ + // If there is a `suggested_fee_recipient` in the validator definitions yaml + // file, use that value. + self.get_fee_recipient_defaulting(self.suggested_fee_recipient(validator_pubkey)) + } + + /// Translate the per validator and per process `builder_proposals`, `builder_boost_factor` and + /// `prefer_builder_proposals` configurations to a boost factor, if available. + /// + /// Priority is given to per-validator values, and then if no preference is established by + /// these the process-level defaults are used. For both types of config, the logic is the same: + /// + /// - If `prefer_builder_proposals` is true, set boost factor to `u64::MAX` to indicate a + /// preference for builder payloads. + /// - If `builder_boost_factor` is a value other than None, return its value as the boost factor. + /// - If `builder_proposals` is set to false, set boost factor to 0 to indicate a preference for + /// local payloads. + /// - Else return `None` to indicate no preference between builder and local payloads. + fn determine_builder_boost_factor(&self, validator_pubkey: &PublicKeyBytes) -> Option { + let validator_prefer_builder_proposals = self + .validators + .read() + .prefer_builder_proposals(validator_pubkey); + + if matches!(validator_prefer_builder_proposals, Some(true)) { + return Some(u64::MAX); + } + + let factor = self + .validators + .read() + .builder_boost_factor(validator_pubkey) + .or_else(|| { + if matches!( + self.validators.read().builder_proposals(validator_pubkey), + Some(false) + ) { + return Some(0); + } + None + }); + + factor + .or_else(|| { + if self.prefer_builder_proposals { + return Some(u64::MAX); + } + self.builder_boost_factor.or({ + if !self.builder_proposals { + Some(0) + } else { + None + } + }) + }) + .and_then(|factor| { + // If builder boost factor is set to 100 it should be treated + // as None to prevent unnecessary calculations that could + // lead to loss of information. + if factor == 100 { + None + } else { + Some(factor) + } + }) + } + + async fn randao_reveal( + &self, + validator_pubkey: PublicKeyBytes, + signing_epoch: Epoch, + ) -> Result { + let signing_method = self.doppelganger_checked_signing_method(validator_pubkey)?; + let signing_context = self.signing_context(Domain::Randao, signing_epoch); + + let signature = signing_method + .get_signature::>( + SignableMessage::RandaoReveal(signing_epoch), + signing_context, + &self.spec, + &self.task_executor, + ) + .await?; + + Ok(signature) + } + + fn set_validator_index(&self, validator_pubkey: &PublicKeyBytes, index: u64) { + self.initialized_validators() + .write() + .set_index(validator_pubkey, index); + } + + async fn sign_block( + &self, + validator_pubkey: PublicKeyBytes, + block: UnsignedBlock, + current_slot: Slot, + ) -> Result, Error> { + match block { + UnsignedBlock::Full(block) => { + let (block, blobs) = block.deconstruct(); + self.sign_abstract_block(validator_pubkey, block, current_slot) + .await + .map(|block| { + SignedBlock::Full(PublishBlockRequest::new(Arc::new(block), blobs)) + }) + } + UnsignedBlock::Blinded(block) => self + .sign_abstract_block(validator_pubkey, block, current_slot) + .await + .map(|block| SignedBlock::Blinded(Arc::new(block))), + } + } + + async fn sign_attestation( + &self, + validator_pubkey: PublicKeyBytes, + validator_committee_position: usize, + attestation: &mut Attestation, + current_epoch: Epoch, + ) -> Result<(), Error> { + // Make sure the target epoch is not higher than the current epoch to avoid potential attacks. + if attestation.data().target.epoch > current_epoch { + return Err(Error::GreaterThanCurrentEpoch { + epoch: attestation.data().target.epoch, + current_epoch, + }); + } + + // 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 = 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 signature = signing_method + .get_signature::>( + SignableMessage::AttestationData(attestation.data()), + signing_context, + &self.spec, + &self.task_executor, + ) + .await?; + attestation + .add_signature(&signature, validator_committee_position) + .map_err(Error::UnableToSignAttestation)?; + + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_ATTESTATIONS_TOTAL, + &[validator_metrics::SUCCESS], + ); + + Ok(()) + } + Ok(Safe::SameData) => { + warn!("Skipping signing of previously signed attestation"); + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_ATTESTATIONS_TOTAL, + &[validator_metrics::SAME_DATA], + ); + Err(Error::SameData) + } + Err(NotSafe::UnregisteredValidator(pk)) => { + warn!( + msg = "Carefully consider running with --init-slashing-protection (see --help)", + public_key = format!("{:?}", pk), + "Not signing attestation for unregistered validator" + ); + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_ATTESTATIONS_TOTAL, + &[validator_metrics::UNREGISTERED], + ); + Err(Error::Slashable(NotSafe::UnregisteredValidator(pk))) + } + Err(e) => { + crit!( + attestation = format!("{:?}", attestation.data()), + error = format!("{:?}", e), + "Not signing slashable attestation" + ); + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_ATTESTATIONS_TOTAL, + &[validator_metrics::SLASHABLE], + ); + Err(Error::Slashable(e)) + } + } + } + + async fn sign_validator_registration_data( + &self, + validator_registration_data: ValidatorRegistrationData, + ) -> Result { + let domain_hash = self.spec.get_builder_domain(); + let signing_root = validator_registration_data.signing_root(domain_hash); + + let signing_method = + self.doppelganger_bypassed_signing_method(validator_registration_data.pubkey)?; + let signature = signing_method + .get_signature_from_root::>( + SignableMessage::ValidatorRegistration(&validator_registration_data), + signing_root, + &self.task_executor, + None, + ) + .await?; + + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_VALIDATOR_REGISTRATIONS_TOTAL, + &[validator_metrics::SUCCESS], + ); + + Ok(SignedValidatorRegistrationData { + message: validator_registration_data, + signature, + }) + } + + /// Signs an `AggregateAndProof` for a given validator. + /// + /// The resulting `SignedAggregateAndProof` is sent on the aggregation channel and cannot be + /// modified by actors other than the signing validator. + async fn produce_signed_aggregate_and_proof( + &self, + validator_pubkey: PublicKeyBytes, + aggregator_index: u64, + aggregate: Attestation, + selection_proof: SelectionProof, + ) -> Result, Error> { + let signing_epoch = aggregate.data().target.epoch; + let signing_context = self.signing_context(Domain::AggregateAndProof, signing_epoch); + + let message = + AggregateAndProof::from_attestation(aggregator_index, aggregate, selection_proof); + + let signing_method = self.doppelganger_checked_signing_method(validator_pubkey)?; + let signature = signing_method + .get_signature::>( + SignableMessage::SignedAggregateAndProof(message.to_ref()), + signing_context, + &self.spec, + &self.task_executor, + ) + .await?; + + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_AGGREGATES_TOTAL, + &[validator_metrics::SUCCESS], + ); + + Ok(SignedAggregateAndProof::from_aggregate_and_proof( + message, signature, + )) + } + + /// Produces a `SelectionProof` for the `slot`, signed by with corresponding secret key to + /// `validator_pubkey`. + async fn produce_selection_proof( + &self, + validator_pubkey: PublicKeyBytes, + slot: Slot, + ) -> Result { + let signing_epoch = slot.epoch(E::slots_per_epoch()); + let signing_context = self.signing_context(Domain::SelectionProof, signing_epoch); + + // Bypass the `with_validator_signing_method` function. + // + // This is because we don't care about doppelganger protection when it comes to selection + // proofs. They are not slashable and we need them to subscribe to subnets on the BN. + // + // As long as we disallow `SignedAggregateAndProof` then these selection proofs will never + // be published on the network. + let signing_method = self.doppelganger_bypassed_signing_method(validator_pubkey)?; + + let signature = signing_method + .get_signature::>( + SignableMessage::SelectionProof(slot), + signing_context, + &self.spec, + &self.task_executor, + ) + .await + .map_err(Error::SpecificError)?; + + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_SELECTION_PROOFS_TOTAL, + &[validator_metrics::SUCCESS], + ); + + Ok(signature.into()) + } + + /// Produce a `SyncSelectionProof` for `slot` signed by the secret key of `validator_pubkey`. + async fn produce_sync_selection_proof( + &self, + validator_pubkey: &PublicKeyBytes, + slot: Slot, + subnet_id: SyncSubnetId, + ) -> Result { + let signing_epoch = slot.epoch(E::slots_per_epoch()); + let signing_context = + self.signing_context(Domain::SyncCommitteeSelectionProof, signing_epoch); + + // Bypass `with_validator_signing_method`: sync committee messages are not slashable. + let signing_method = self.doppelganger_bypassed_signing_method(*validator_pubkey)?; + + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_SYNC_SELECTION_PROOFS_TOTAL, + &[validator_metrics::SUCCESS], + ); + + let message = SyncAggregatorSelectionData { + slot, + subcommittee_index: subnet_id.into(), + }; + + let signature = signing_method + .get_signature::>( + SignableMessage::SyncSelectionProof(&message), + signing_context, + &self.spec, + &self.task_executor, + ) + .await + .map_err(Error::SpecificError)?; + + Ok(signature.into()) + } + + async fn produce_sync_committee_signature( + &self, + slot: Slot, + beacon_block_root: Hash256, + validator_index: u64, + validator_pubkey: &PublicKeyBytes, + ) -> Result { + let signing_epoch = slot.epoch(E::slots_per_epoch()); + let signing_context = self.signing_context(Domain::SyncCommittee, signing_epoch); + + // Bypass `with_validator_signing_method`: sync committee messages are not slashable. + let signing_method = self.doppelganger_bypassed_signing_method(*validator_pubkey)?; + + let signature = signing_method + .get_signature::>( + SignableMessage::SyncCommitteeSignature { + beacon_block_root, + slot, + }, + signing_context, + &self.spec, + &self.task_executor, + ) + .await + .map_err(Error::SpecificError)?; + + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_SYNC_COMMITTEE_MESSAGES_TOTAL, + &[validator_metrics::SUCCESS], + ); + + Ok(SyncCommitteeMessage { + slot, + beacon_block_root, + validator_index, + signature, + }) + } + + async fn produce_signed_contribution_and_proof( + &self, + aggregator_index: u64, + aggregator_pubkey: PublicKeyBytes, + contribution: SyncCommitteeContribution, + selection_proof: SyncSelectionProof, + ) -> Result, Error> { + let signing_epoch = contribution.slot.epoch(E::slots_per_epoch()); + let signing_context = self.signing_context(Domain::ContributionAndProof, signing_epoch); + + // Bypass `with_validator_signing_method`: sync committee messages are not slashable. + let signing_method = self.doppelganger_bypassed_signing_method(aggregator_pubkey)?; + + let message = ContributionAndProof { + aggregator_index, + contribution, + selection_proof: selection_proof.into(), + }; + + let signature = signing_method + .get_signature::>( + SignableMessage::SignedContributionAndProof(&message), + signing_context, + &self.spec, + &self.task_executor, + ) + .await + .map_err(Error::SpecificError)?; + + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_SYNC_COMMITTEE_CONTRIBUTIONS_TOTAL, + &[validator_metrics::SUCCESS], + ); + + Ok(SignedContributionAndProof { message, signature }) + } + + /// Prune the slashing protection database so that it remains performant. + /// + /// This function will only do actual pruning periodically, so it should usually be + /// cheap to call. The `first_run` flag can be used to print a more verbose message when pruning + /// runs. + fn prune_slashing_protection_db(&self, current_epoch: Epoch, first_run: bool) { + // Attempt to prune every SLASHING_PROTECTION_HISTORY_EPOCHs, with a tolerance for + // missing the epoch that aligns exactly. + let mut last_prune = self.slashing_protection_last_prune.lock(); + if current_epoch / SLASHING_PROTECTION_HISTORY_EPOCHS + <= *last_prune / SLASHING_PROTECTION_HISTORY_EPOCHS + { + return; + } + + if first_run { + info!( + epoch = %current_epoch, + msg = "pruning may take several minutes the first time it runs", + "Pruning slashing protection DB" + ); + } else { + info!(epoch = %current_epoch, "Pruning slashing protection DB"); + } + + let _timer = + validator_metrics::start_timer(&validator_metrics::SLASHING_PROTECTION_PRUNE_TIMES); + + let new_min_target_epoch = current_epoch.saturating_sub(SLASHING_PROTECTION_HISTORY_EPOCHS); + let new_min_slot = new_min_target_epoch.start_slot(E::slots_per_epoch()); + + let all_pubkeys: Vec<_> = self.voting_pubkeys(DoppelgangerStatus::ignored); + + if let Err(e) = self + .slashing_protection + .prune_all_signed_attestations(all_pubkeys.iter(), new_min_target_epoch) + { + error!( + error = ?e, + "Error during pruning of signed attestations" + ); + return; + } + + if let Err(e) = self + .slashing_protection + .prune_all_signed_blocks(all_pubkeys.iter(), new_min_slot) + { + error!( + error = ?e, + "Error during pruning of signed blocks" + ); + return; + } + + *last_prune = current_epoch; + + info!("Completed pruning of slashing protection DB"); + } + + /// Returns `ProposalData` for the provided `pubkey` if it exists in `InitializedValidators`. + /// `ProposalData` fields include defaulting logic described in `get_fee_recipient_defaulting`, + /// `get_gas_limit_defaulting`, and `get_builder_proposals_defaulting`. + fn proposal_data(&self, pubkey: &PublicKeyBytes) -> Option { + self.validators + .read() + .validator(pubkey) + .map(|validator| ProposalData { + validator_index: validator.get_index(), + fee_recipient: self + .get_fee_recipient_defaulting(validator.get_suggested_fee_recipient()), + gas_limit: self.get_gas_limit_defaulting(validator.get_gas_limit()), + builder_proposals: self + .get_builder_proposals_defaulting(validator.get_builder_proposals()), + }) + } + + async fn sign_inclusion_list( + &self, + pubkey: PublicKeyBytes, + inclusion_list: InclusionList, + ) -> Result, ValidatorStoreError> { + let signing_epoch = inclusion_list.slot.epoch(E::slots_per_epoch()); + let signing_context = self.signing_context(Domain::InclusionListCommittee, signing_epoch); + let signing_method = self.doppelganger_bypassed_signing_method(pubkey)?; + + let signature = signing_method + .get_signature::>( + SignableMessage::InclusionList(&inclusion_list), + signing_context, + &self.spec, + &self.task_executor, + ) + .await?; + + Ok(SignedInclusionList { + message: inclusion_list, + signature, + }) + } +} diff --git a/validator_client/signing_method/src/lib.rs b/validator_client/signing_method/src/lib.rs index cf8f5f7736..869c037696 100644 --- a/validator_client/signing_method/src/lib.rs +++ b/validator_client/signing_method/src/lib.rs @@ -12,7 +12,7 @@ use std::sync::Arc; use task_executor::TaskExecutor; use types::*; use url::Url; -use web3signer::{ForkInfo, SigningRequest, SigningResponse}; +use web3signer::{ForkInfo, MessageType, SigningRequest, SigningResponse}; pub use web3signer::Web3SignerObject; @@ -154,8 +154,13 @@ impl SigningMethod { genesis_validators_root, }); - self.get_signature_from_root(signable_message, signing_root, executor, fork_info) - .await + self.get_signature_from_root::( + signable_message, + signing_root, + executor, + fork_info, + ) + .await } pub async fn get_signature_from_root>( @@ -230,11 +235,7 @@ impl SigningMethod { // Determine the Web3Signer message type. let message_type = object.message_type(); - - if matches!( - object, - Web3SignerObject::Deposit { .. } | Web3SignerObject::ValidatorRegistration(_) - ) && fork_info.is_some() + if matches!(message_type, MessageType::ValidatorRegistration) && fork_info.is_some() { return Err(Error::GenesisForkVersionRequired); } diff --git a/validator_client/slashing_protection/src/lib.rs b/validator_client/slashing_protection/src/lib.rs index 51dd3e3164..825a34cabc 100644 --- a/validator_client/slashing_protection/src/lib.rs +++ b/validator_client/slashing_protection/src/lib.rs @@ -27,7 +27,7 @@ pub const SLASHING_PROTECTION_FILENAME: &str = "slashing_protection.sqlite"; /// The attestation or block is not safe to sign. /// /// This could be because it's slashable, or because an error occurred. -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Clone)] pub enum NotSafe { UnregisteredValidator(PublicKeyBytes), DisabledValidator(PublicKeyBytes), diff --git a/validator_client/slashing_protection/src/signed_attestation.rs b/validator_client/slashing_protection/src/signed_attestation.rs index 779b5f770a..332f80c704 100644 --- a/validator_client/slashing_protection/src/signed_attestation.rs +++ b/validator_client/slashing_protection/src/signed_attestation.rs @@ -10,7 +10,7 @@ pub struct SignedAttestation { } /// Reasons why an attestation may be slashable (or invalid). -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Clone)] pub enum InvalidAttestation { /// The attestation has the same target epoch as an attestation from the DB (enclosed). DoubleVote(SignedAttestation), diff --git a/validator_client/slashing_protection/src/signed_block.rs b/validator_client/slashing_protection/src/signed_block.rs index 92ec2dcbe8..d46872529e 100644 --- a/validator_client/slashing_protection/src/signed_block.rs +++ b/validator_client/slashing_protection/src/signed_block.rs @@ -9,7 +9,7 @@ pub struct SignedBlock { } /// Reasons why a block may be slashable. -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Clone)] pub enum InvalidBlock { DoubleBlockProposal(SignedBlock), SlotViolatesLowerBound { block_slot: Slot, bound_slot: Slot }, diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index 18bd736957..cdbf9f8472 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -67,7 +67,6 @@ pub struct ValidatorClient { #[clap( long, value_name = "SECRETS_DIRECTORY", - conflicts_with = "datadir", help = "The directory which contains the password to unlock the validator \ voting keypairs. Each password should be contained in a file where the \ name is the 0x-prefixed hex representation of the validators voting public \ @@ -220,6 +219,7 @@ pub struct ValidatorClient { #[clap( long, + requires = "http", value_name = "PORT", default_value_t = 5062, help = "Set the listen TCP port for the RESTful HTTP API server.", @@ -388,7 +388,7 @@ pub struct ValidatorClient { #[clap( long, value_name = "INTEGER", - default_value_t = 30_000_000, + default_value_t = 36_000_000, requires = "builder_proposals", help = "The gas limit to be used in all builder proposals for all validators managed \ by this validator client. Note this will not necessarily be used if the gas limit \ diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index cfc88969c9..726aa96cf9 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -10,6 +10,7 @@ use directory::{ use eth2::types::Graffiti; use graffiti_file::GraffitiFile; use initialized_validators::Config as InitializedValidatorsConfig; +use lighthouse_validator_store::Config as ValidatorStoreConfig; use sensitive_url::SensitiveUrl; use serde::{Deserialize, Serialize}; use std::fs; @@ -20,7 +21,6 @@ use tracing::{info, warn}; use types::GRAFFITI_BYTES_LEN; use validator_http_api::{self, PK_FILENAME}; use validator_http_metrics; -use validator_store::Config as ValidatorStoreConfig; pub const DEFAULT_BEACON_NODE: &str = "http://localhost:5052/"; diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 590fa8fbef..92b10919f6 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -1,7 +1,5 @@ pub mod cli; pub mod config; -mod latency; -mod notifier; use crate::cli::ValidatorClient; pub use config::Config; @@ -20,14 +18,13 @@ use doppelganger_service::DoppelgangerService; use environment::RuntimeContext; use eth2::{reqwest::ClientBuilder, BeaconNodeHttpClient, StatusCode, Timeouts}; use initialized_validators::Error::UnableToOpenVotingKeystore; -use notifier::spawn_notifier; +use lighthouse_validator_store::LighthouseValidatorStore; use parking_lot::RwLock; use reqwest::Certificate; use slot_clock::SlotClock; use slot_clock::SystemTimeSlotClock; use std::fs::File; use std::io::Read; -use std::marker::PhantomData; use std::net::SocketAddr; use std::path::Path; use std::sync::Arc; @@ -39,16 +36,18 @@ use tokio::{ use tracing::{debug, error, info, warn}; use types::{EthSpec, Hash256}; use validator_http_api::ApiSecret; +use validator_services::inclusion_list_service::InclusionListServiceBuilder; +use validator_services::notifier_service::spawn_notifier; use validator_services::{ attestation_service::{AttestationService, AttestationServiceBuilder}, block_service::{BlockService, BlockServiceBuilder}, - duties_service::{self, DutiesService}, + duties_service::{self, DutiesService, DutiesServiceBuilder}, inclusion_list_service::InclusionListService, + latency_service, preparation_service::{PreparationService, PreparationServiceBuilder}, - sync::SyncDutiesMap, sync_committee_service::SyncCommitteeService, }; -use validator_store::ValidatorStore; +use validator_store::ValidatorStore as ValidatorStoreTrait; /// The interval between attempts to contact the beacon node during startup. const RETRY_DELAY: Duration = Duration::from_secs(2); @@ -72,24 +71,27 @@ const HTTP_GET_BEACON_BLOCK_SSZ_TIMEOUT_QUOTIENT: u32 = 4; const HTTP_GET_DEBUG_BEACON_STATE_QUOTIENT: u32 = 4; const HTTP_GET_DEPOSIT_SNAPSHOT_QUOTIENT: u32 = 4; const HTTP_GET_VALIDATOR_BLOCK_TIMEOUT_QUOTIENT: u32 = 4; +const HTTP_DEFAULT_TIMEOUT_QUOTIENT: u32 = 4; const DOPPELGANGER_SERVICE_NAME: &str = "doppelganger"; +type ValidatorStore = LighthouseValidatorStore; + #[derive(Clone)] pub struct ProductionValidatorClient { context: RuntimeContext, - duties_service: Arc>, - block_service: BlockService, - attestation_service: AttestationService, - sync_committee_service: SyncCommitteeService, - inclusion_list_service: InclusionListService, + duties_service: Arc, SystemTimeSlotClock>>, + block_service: BlockService, SystemTimeSlotClock>, + attestation_service: AttestationService, SystemTimeSlotClock>, + sync_committee_service: SyncCommitteeService, SystemTimeSlotClock>, + inclusion_list_service: InclusionListService, SystemTimeSlotClock>, doppelganger_service: Option>, - preparation_service: PreparationService, - validator_store: Arc>, + preparation_service: PreparationService, SystemTimeSlotClock>, + validator_store: Arc>, slot_clock: SystemTimeSlotClock, http_api_listen_addr: Option, config: Config, - beacon_nodes: Arc>, + beacon_nodes: Arc>, genesis_time: u64, } @@ -313,6 +315,7 @@ impl ProductionValidatorClient { get_debug_beacon_states: slot_duration / HTTP_GET_DEBUG_BEACON_STATE_QUOTIENT, get_deposit_snapshot: slot_duration / HTTP_GET_DEPOSIT_SNAPSHOT_QUOTIENT, get_validator_block: slot_duration / HTTP_GET_VALIDATOR_BLOCK_TIMEOUT_QUOTIENT, + default: slot_duration / HTTP_DEFAULT_TIMEOUT_QUOTIENT, } } else { Timeouts::set_all(slot_duration.saturating_mul(config.long_timeouts_multiplier)) @@ -374,14 +377,14 @@ impl ProductionValidatorClient { // Initialize the number of connected, avaliable beacon nodes to 0. set_gauge(&validator_metrics::AVAILABLE_BEACON_NODES_COUNT, 0); - let mut beacon_nodes: BeaconNodeFallback<_, E> = BeaconNodeFallback::new( + let mut beacon_nodes: BeaconNodeFallback<_> = BeaconNodeFallback::new( candidates, config.beacon_node_fallback, config.broadcast_topics.clone(), context.eth2_config.spec.clone(), ); - let mut proposer_nodes: BeaconNodeFallback<_, E> = BeaconNodeFallback::new( + let mut proposer_nodes: BeaconNodeFallback<_> = BeaconNodeFallback::new( proposer_candidates, config.beacon_node_fallback, config.broadcast_topics.clone(), @@ -390,7 +393,7 @@ impl ProductionValidatorClient { // Perform some potentially long-running initialization tasks. let (genesis_time, genesis_validators_root) = tokio::select! { - tuple = init_from_beacon_node(&beacon_nodes, &proposer_nodes) => tuple?, + tuple = init_from_beacon_node::(&beacon_nodes, &proposer_nodes) => tuple?, () = context.executor.exit() => return Err("Shutting down".to_string()) }; @@ -409,10 +412,10 @@ impl ProductionValidatorClient { proposer_nodes.set_slot_clock(slot_clock.clone()); let beacon_nodes = Arc::new(beacon_nodes); - start_fallback_updater_service(context.clone(), beacon_nodes.clone())?; + start_fallback_updater_service::<_, E>(context.executor.clone(), beacon_nodes.clone())?; let proposer_nodes = Arc::new(proposer_nodes); - start_fallback_updater_service(context.clone(), proposer_nodes.clone())?; + start_fallback_updater_service::<_, E>(context.executor.clone(), proposer_nodes.clone())?; let doppelganger_service = if config.enable_doppelganger_protection { Some(Arc::new(DoppelgangerService::default())) @@ -420,7 +423,7 @@ impl ProductionValidatorClient { None }; - let validator_store = Arc::new(ValidatorStore::new( + let validator_store = Arc::new(LighthouseValidatorStore::new( validators, slashing_protection, genesis_validators_root, @@ -446,22 +449,18 @@ impl ProductionValidatorClient { 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: SyncDutiesMap::new(config.distributed), - inclusion_list_duties: <_>::default(), - slot_clock: slot_clock.clone(), - beacon_nodes: beacon_nodes.clone(), - validator_store: validator_store.clone(), - unknown_validator_next_poll_slots: <_>::default(), - spec: context.eth2_config.spec.clone(), - context: duties_context, - enable_high_validator_count_metrics: config.enable_high_validator_count_metrics, - distributed: config.distributed, - disable_attesting: config.disable_attesting, - }); + let duties_service = Arc::new( + DutiesServiceBuilder::new() + .slot_clock(slot_clock.clone()) + .beacon_nodes(beacon_nodes.clone()) + .validator_store(validator_store.clone()) + .spec(context.eth2_config.spec.clone()) + .executor(context.executor.clone()) + .enable_high_validator_count_metrics(config.enable_high_validator_count_metrics) + .distributed(config.distributed) + .disable_attesting(config.disable_attesting) + .build()?, + ); // Update the metrics server. if let Some(ctx) = &validator_metrics_ctx { @@ -473,7 +472,8 @@ impl ProductionValidatorClient { .slot_clock(slot_clock.clone()) .validator_store(validator_store.clone()) .beacon_nodes(beacon_nodes.clone()) - .runtime_context(context.service_context("block".into())) + .executor(context.executor.clone()) + .chain_spec(context.eth2_config.spec.clone()) .graffiti(config.graffiti) .graffiti_file(config.graffiti_file.clone()); @@ -489,7 +489,8 @@ impl ProductionValidatorClient { .slot_clock(slot_clock.clone()) .validator_store(validator_store.clone()) .beacon_nodes(beacon_nodes.clone()) - .runtime_context(context.service_context("attestation".into())) + .executor(context.executor.clone()) + .chain_spec(context.eth2_config.spec.clone()) .disable(config.disable_attesting) .build()?; @@ -497,7 +498,7 @@ impl ProductionValidatorClient { .slot_clock(slot_clock.clone()) .validator_store(validator_store.clone()) .beacon_nodes(beacon_nodes.clone()) - .runtime_context(context.service_context("preparation".into())) + .executor(context.executor.clone()) .builder_registration_timestamp_override(config.builder_registration_timestamp_override) .validator_registration_batch_size(config.validator_registration_batch_size) .build()?; @@ -507,16 +508,19 @@ impl ProductionValidatorClient { validator_store.clone(), slot_clock.clone(), beacon_nodes.clone(), - context.service_context("sync_committee".into()), + context.executor.clone(), ); - let inclusion_list_service = InclusionListService::new( - duties_service.clone(), - validator_store.clone(), - slot_clock.clone(), - beacon_nodes.clone(), - context.service_context("inclusion_list".into()), - ); + let inclusion_list_service = InclusionListServiceBuilder::new() + .duties_service(duties_service.clone()) + .slot_clock(slot_clock.clone()) + .validator_store(validator_store.clone()) + .beacon_nodes(beacon_nodes.clone()) + .executor(context.executor.clone()) + .chain_spec(context.eth2_config.spec.clone()) + // TODO(focil) make config driven + .disable(false) + .build()?; Ok(Self { context, @@ -559,12 +563,11 @@ impl ProductionValidatorClient { config: self.config.http_api.clone(), sse_logging_components: self.context.sse_logging_components.clone(), slot_clock: self.slot_clock.clone(), - _phantom: PhantomData, }); let exit = self.context.executor.exit(); - let (listen_addr, server) = validator_http_api::serve(ctx, exit) + let (listen_addr, server) = validator_http_api::serve::<_, E>(ctx, exit) .map_err(|e| format!("Unable to start HTTP API server: {:?}", e))?; self.context @@ -622,11 +625,17 @@ impl ProductionValidatorClient { info!("Doppelganger protection disabled.") } - spawn_notifier(self).map_err(|e| format!("Failed to start notifier: {}", e))?; + let context = self.context.service_context("notifier".into()); + spawn_notifier( + self.duties_service.clone(), + context.executor, + &self.context.eth2_config.spec, + ) + .map_err(|e| format!("Failed to start notifier: {}", e))?; if self.config.enable_latency_measurement_service { - latency::start_latency_service( - self.context.clone(), + latency_service::start_latency_service( + self.context.executor.clone(), self.duties_service.slot_clock.clone(), self.duties_service.beacon_nodes.clone(), ); @@ -637,12 +646,12 @@ impl ProductionValidatorClient { } async fn init_from_beacon_node( - beacon_nodes: &BeaconNodeFallback, - proposer_nodes: &BeaconNodeFallback, + beacon_nodes: &BeaconNodeFallback, + proposer_nodes: &BeaconNodeFallback, ) -> Result<(u64, Hash256), String> { loop { - beacon_nodes.update_all_candidates().await; - proposer_nodes.update_all_candidates().await; + beacon_nodes.update_all_candidates::().await; + proposer_nodes.update_all_candidates::().await; let num_available = beacon_nodes.num_available().await; let num_total = beacon_nodes.num_total().await; @@ -719,8 +728,8 @@ async fn init_from_beacon_node( Ok((genesis.genesis_time, genesis.genesis_validators_root)) } -async fn wait_for_genesis( - beacon_nodes: &BeaconNodeFallback, +async fn wait_for_genesis( + beacon_nodes: &BeaconNodeFallback, genesis_time: u64, ) -> Result<(), String> { let now = SystemTime::now() @@ -762,8 +771,8 @@ async fn wait_for_genesis( /// Request the version from the node, looping back and trying again on failure. Exit once the node /// has been contacted. -async fn poll_whilst_waiting_for_genesis( - beacon_nodes: &BeaconNodeFallback, +async fn poll_whilst_waiting_for_genesis( + beacon_nodes: &BeaconNodeFallback, genesis_time: Duration, ) -> Result<(), String> { loop { diff --git a/validator_client/validator_services/Cargo.toml b/validator_client/validator_services/Cargo.toml index 4b023bb40a..c914940914 100644 --- a/validator_client/validator_services/Cargo.toml +++ b/validator_client/validator_services/Cargo.toml @@ -6,20 +6,19 @@ authors = ["Sigma Prime "] [dependencies] beacon_node_fallback = { workspace = true } -bls = { workspace = true } -doppelganger_service = { workspace = true } +bls = { workspace = true } either = { workspace = true } -environment = { workspace = true } eth2 = { workspace = true } futures = { workspace = true } graffiti_file = { workspace = true } logging = { workspace = true } parking_lot = { workspace = true } safe_arith = { workspace = true } -slot_clock = { workspace = true } -tokio = { workspace = true } +slot_clock = { workspace = true } +task_executor = { workspace = true } +tokio = { workspace = true } tracing = { workspace = true } -tree_hash = { workspace = true } -types = { workspace = true } +tree_hash = { workspace = true } +types = { workspace = true } validator_metrics = { workspace = true } validator_store = { workspace = true } diff --git a/validator_client/validator_services/src/attestation_service.rs b/validator_client/validator_services/src/attestation_service.rs index 8e098b81b0..f776567706 100644 --- a/validator_client/validator_services/src/attestation_service.rs +++ b/validator_client/validator_services/src/attestation_service.rs @@ -1,13 +1,13 @@ use crate::duties_service::{DutiesService, DutyAndProof}; use beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; use either::Either; -use environment::RuntimeContext; use futures::future::join_all; use logging::crit; use slot_clock::SlotClock; use std::collections::HashMap; use std::ops::Deref; use std::sync::Arc; +use task_executor::TaskExecutor; use tokio::time::{sleep, sleep_until, Duration, Instant}; use tracing::{debug, error, info, trace, warn}; use tree_hash::TreeHash; @@ -16,33 +16,35 @@ use validator_store::{Error as ValidatorStoreError, ValidatorStore}; /// Builds an `AttestationService`. #[derive(Default)] -pub struct AttestationServiceBuilder { - duties_service: Option>>, - validator_store: Option>>, +pub struct AttestationServiceBuilder { + duties_service: Option>>, + validator_store: Option>, slot_clock: Option, - beacon_nodes: Option>>, - context: Option>, + beacon_nodes: Option>>, + executor: Option, + chain_spec: Option>, disable: bool, } -impl AttestationServiceBuilder { +impl AttestationServiceBuilder { pub fn new() -> Self { Self { duties_service: None, validator_store: None, slot_clock: None, beacon_nodes: None, - context: None, + executor: None, + chain_spec: None, disable: false, } } - pub fn duties_service(mut self, service: Arc>) -> Self { + pub fn duties_service(mut self, service: Arc>) -> Self { self.duties_service = Some(service); self } - pub fn validator_store(mut self, store: Arc>) -> Self { + pub fn validator_store(mut self, store: Arc) -> Self { self.validator_store = Some(store); self } @@ -52,13 +54,18 @@ impl AttestationServiceBuilder { self } - pub fn beacon_nodes(mut self, beacon_nodes: Arc>) -> Self { + pub fn beacon_nodes(mut self, beacon_nodes: Arc>) -> Self { self.beacon_nodes = Some(beacon_nodes); self } - pub fn runtime_context(mut self, context: RuntimeContext) -> Self { - self.context = Some(context); + pub fn executor(mut self, executor: TaskExecutor) -> Self { + self.executor = Some(executor); + self + } + + pub fn chain_spec(mut self, chain_spec: Arc) -> Self { + self.chain_spec = Some(chain_spec); self } @@ -67,7 +74,7 @@ impl AttestationServiceBuilder { self } - pub fn build(self) -> Result, String> { + pub fn build(self) -> Result, String> { Ok(AttestationService { inner: Arc::new(Inner { duties_service: self @@ -82,9 +89,12 @@ impl AttestationServiceBuilder { beacon_nodes: self .beacon_nodes .ok_or("Cannot build AttestationService without beacon_nodes")?, - context: self - .context - .ok_or("Cannot build AttestationService without runtime_context")?, + executor: self + .executor + .ok_or("Cannot build AttestationService without executor")?, + chain_spec: self + .chain_spec + .ok_or("Cannot build AttestationService without chain_spec")?, disable: self.disable, }), }) @@ -92,12 +102,13 @@ impl AttestationServiceBuilder { } /// Helper to minimise `Arc` usage. -pub struct Inner { - duties_service: Arc>, - validator_store: Arc>, +pub struct Inner { + duties_service: Arc>, + validator_store: Arc, slot_clock: T, - beacon_nodes: Arc>, - context: RuntimeContext, + beacon_nodes: Arc>, + executor: TaskExecutor, + chain_spec: Arc, disable: bool, } @@ -106,11 +117,11 @@ pub struct Inner { /// If any validators are on the same committee, a single attestation will be downloaded and /// returned to the beacon node. This attestation will have a signature from each of the /// validators. -pub struct AttestationService { - inner: Arc>, +pub struct AttestationService { + inner: Arc>, } -impl Clone for AttestationService { +impl Clone for AttestationService { fn clone(&self) -> Self { Self { inner: self.inner.clone(), @@ -118,15 +129,15 @@ impl Clone for AttestationService { } } -impl Deref for AttestationService { - type Target = Inner; +impl Deref for AttestationService { + type Target = Inner; fn deref(&self) -> &Self::Target { self.inner.deref() } } -impl AttestationService { +impl AttestationService { /// Starts the service which periodically produces attestations. pub fn start_update_service(self, spec: &ChainSpec) -> Result<(), String> { if self.disable { @@ -145,7 +156,7 @@ impl AttestationService { "Attestation production service started" ); - let executor = self.context.executor.clone(); + let executor = self.executor.clone(); let interval_fut = async move { loop { @@ -205,7 +216,7 @@ impl AttestationService { .into_iter() .for_each(|(committee_index, validator_duties)| { // Spawn a separate task for each attestation. - self.inner.context.executor.spawn_ignoring_error( + self.inner.executor.spawn_ignoring_error( self.clone().publish_attestations_and_aggregates( slot, committee_index, @@ -332,7 +343,7 @@ impl AttestationService { .slot_clock .now() .ok_or("Unable to determine current slot from clock")? - .epoch(E::slots_per_epoch()); + .epoch(S::E::slots_per_epoch()); let attestation_data = self .beacon_nodes @@ -357,7 +368,7 @@ impl AttestationService { let attestation_data = attestation_data_ref; // Ensure that the attestation matches the duties. - if !duty.match_attestation_data::(attestation_data, &self.context.eth2_config.spec) { + if !duty.match_attestation_data::(attestation_data, &self.chain_spec) { crit!( validator = ?duty.pubkey, duty_slot = %duty.slot, @@ -369,14 +380,14 @@ impl AttestationService { return None; } - let mut attestation = match Attestation::::empty_for_signing( + let mut attestation = match Attestation::empty_for_signing( duty.committee_index, duty.committee_length as usize, attestation_data.slot, attestation_data.beacon_block_root, attestation_data.source, attestation_data.target, - &self.context.eth2_config.spec, + &self.chain_spec, ) { Ok(attestation) => attestation, Err(err) => { @@ -439,10 +450,8 @@ impl AttestationService { return Ok(None); } let fork_name = self - .context - .eth2_config - .spec - .fork_name_at_slot::(attestation_data.slot); + .chain_spec + .fork_name_at_slot::(attestation_data.slot); // Post the attestations to the BN. match self @@ -476,7 +485,7 @@ impl AttestationService { .collect::>(); beacon_node - .post_beacon_pool_attestations_v2::( + .post_beacon_pool_attestations_v2::( Either::Right(single_attestations), fork_name, ) @@ -538,10 +547,8 @@ impl AttestationService { } let fork_name = self - .context - .eth2_config - .spec - .fork_name_at_slot::(attestation_data.slot); + .chain_spec + .fork_name_at_slot::(attestation_data.slot); let aggregated_attestation = &self .beacon_nodes @@ -562,7 +569,7 @@ impl AttestationService { format!("Failed to produce an aggregate attestation: {:?}", e) })? .ok_or_else(|| format!("No aggregate available for {:?}", attestation_data)) - .map(|result| result.data) + .map(|result| result.into_data()) } else { beacon_node .get_validator_aggregate_attestation_v1( @@ -585,7 +592,7 @@ impl AttestationService { let duty = &duty_and_proof.duty; let selection_proof = duty_and_proof.selection_proof.as_ref()?; - if !duty.match_attestation_data::(attestation_data, &self.context.eth2_config.spec) { + if !duty.match_attestation_data::(attestation_data, &self.chain_spec) { crit!("Inconsistent validator duties during signing"); return None; } @@ -689,11 +696,11 @@ impl AttestationService { /// Start the task at `pruning_instant` to avoid interference with other tasks. fn spawn_slashing_protection_pruning_task(&self, slot: Slot, pruning_instant: Instant) { let attestation_service = self.clone(); - let executor = self.inner.context.executor.clone(); - let current_epoch = slot.epoch(E::slots_per_epoch()); + let executor = self.inner.executor.clone(); + let current_epoch = slot.epoch(S::E::slots_per_epoch()); // Wait for `pruning_instant` in a regular task, and then switch to a blocking one. - self.inner.context.executor.spawn( + self.inner.executor.spawn( async move { sleep_until(pruning_instant).await; diff --git a/validator_client/validator_services/src/block_service.rs b/validator_client/validator_services/src/block_service.rs index d2dbbb656e..01f786e160 100644 --- a/validator_client/validator_services/src/block_service.rs +++ b/validator_client/validator_services/src/block_service.rs @@ -1,7 +1,5 @@ use beacon_node_fallback::{ApiTopic, BeaconNodeFallback, Error as FallbackError, Errors}; use bls::SignatureBytes; -use environment::RuntimeContext; -use eth2::types::{FullBlockContents, PublishBlockRequest}; use eth2::{BeaconNodeHttpClient, StatusCode}; use graffiti_file::{determine_graffiti, GraffitiFile}; use logging::crit; @@ -11,13 +9,11 @@ use std::future::Future; use std::ops::Deref; use std::sync::Arc; use std::time::Duration; +use task_executor::TaskExecutor; use tokio::sync::mpsc; use tracing::{debug, error, info, trace, warn}; -use types::{ - BlindedBeaconBlock, BlockType, EthSpec, Graffiti, PublicKeyBytes, SignedBlindedBeaconBlock, - Slot, -}; -use validator_store::{Error as ValidatorStoreError, ValidatorStore}; +use types::{BlockType, ChainSpec, EthSpec, Graffiti, PublicKeyBytes, Slot}; +use validator_store::{Error as ValidatorStoreError, SignedBlock, UnsignedBlock, ValidatorStore}; #[derive(Debug)] pub enum BlockError { @@ -45,30 +41,32 @@ impl From> for BlockError { /// Builds a `BlockService`. #[derive(Default)] -pub struct BlockServiceBuilder { - validator_store: Option>>, +pub struct BlockServiceBuilder { + validator_store: Option>, slot_clock: Option>, - beacon_nodes: Option>>, - proposer_nodes: Option>>, - context: Option>, + beacon_nodes: Option>>, + proposer_nodes: Option>>, + executor: Option, + chain_spec: Option>, graffiti: Option, graffiti_file: Option, } -impl BlockServiceBuilder { +impl BlockServiceBuilder { pub fn new() -> Self { Self { validator_store: None, slot_clock: None, beacon_nodes: None, proposer_nodes: None, - context: None, + executor: None, + chain_spec: None, graffiti: None, graffiti_file: None, } } - pub fn validator_store(mut self, store: Arc>) -> Self { + pub fn validator_store(mut self, store: Arc) -> Self { self.validator_store = Some(store); self } @@ -78,18 +76,23 @@ impl BlockServiceBuilder { self } - pub fn beacon_nodes(mut self, beacon_nodes: Arc>) -> Self { + pub fn beacon_nodes(mut self, beacon_nodes: Arc>) -> Self { self.beacon_nodes = Some(beacon_nodes); self } - pub fn proposer_nodes(mut self, proposer_nodes: Arc>) -> Self { + pub fn proposer_nodes(mut self, proposer_nodes: Arc>) -> Self { self.proposer_nodes = Some(proposer_nodes); self } - pub fn runtime_context(mut self, context: RuntimeContext) -> Self { - self.context = Some(context); + pub fn executor(mut self, executor: TaskExecutor) -> Self { + self.executor = Some(executor); + self + } + + pub fn chain_spec(mut self, chain_spec: Arc) -> Self { + self.chain_spec = Some(chain_spec); self } @@ -103,7 +106,7 @@ impl BlockServiceBuilder { self } - pub fn build(self) -> Result, String> { + pub fn build(self) -> Result, String> { Ok(BlockService { inner: Arc::new(Inner { validator_store: self @@ -115,9 +118,12 @@ impl BlockServiceBuilder { beacon_nodes: self .beacon_nodes .ok_or("Cannot build BlockService without beacon_node")?, - context: self - .context - .ok_or("Cannot build BlockService without runtime_context")?, + executor: self + .executor + .ok_or("Cannot build BlockService without executor")?, + chain_spec: self + .chain_spec + .ok_or("Cannot build BlockService without chain_spec")?, proposer_nodes: self.proposer_nodes, graffiti: self.graffiti, graffiti_file: self.graffiti_file, @@ -128,12 +134,12 @@ impl BlockServiceBuilder { // Combines a set of non-block-proposing `beacon_nodes` and only-block-proposing // `proposer_nodes`. -pub struct ProposerFallback { - beacon_nodes: Arc>, - proposer_nodes: Option>>, +pub struct ProposerFallback { + beacon_nodes: Arc>, + proposer_nodes: Option>>, } -impl ProposerFallback { +impl ProposerFallback { // Try `func` on `self.proposer_nodes` first. If that doesn't work, try `self.beacon_nodes`. pub async fn request_proposers_first(&self, func: F) -> Result<(), Errors> where @@ -178,22 +184,23 @@ impl ProposerFallback { } /// Helper to minimise `Arc` usage. -pub struct Inner { - validator_store: Arc>, +pub struct Inner { + validator_store: Arc, slot_clock: Arc, - pub beacon_nodes: Arc>, - pub proposer_nodes: Option>>, - context: RuntimeContext, + pub beacon_nodes: Arc>, + pub proposer_nodes: Option>>, + executor: TaskExecutor, + chain_spec: Arc, graffiti: Option, graffiti_file: Option, } /// Attempts to produce attestations for any block producer(s) at the start of the epoch. -pub struct BlockService { - inner: Arc>, +pub struct BlockService { + inner: Arc>, } -impl Clone for BlockService { +impl Clone for BlockService { fn clone(&self) -> Self { Self { inner: self.inner.clone(), @@ -201,8 +208,8 @@ impl Clone for BlockService { } } -impl Deref for BlockService { - type Target = Inner; +impl Deref for BlockService { + type Target = Inner; fn deref(&self) -> &Self::Target { self.inner.deref() @@ -215,14 +222,14 @@ pub struct BlockServiceNotification { pub block_proposers: Vec, } -impl BlockService { +impl BlockService { pub fn start_update_service( self, mut notification_rx: mpsc::Receiver, ) -> Result<(), String> { info!("Block production service started"); - let executor = self.inner.context.executor.clone(); + let executor = self.inner.executor.clone(); executor.spawn( async move { @@ -258,7 +265,7 @@ impl BlockService { return Ok(()); } - if slot == self.context.eth2_config.spec.genesis_slot { + if slot == self.chain_spec.genesis_slot { debug!( proposers = format!("{:?}", notification.block_proposers), "Not producing block at genesis slot" @@ -285,9 +292,11 @@ impl BlockService { } for validator_pubkey in proposers { - let builder_boost_factor = self.get_builder_boost_factor(&validator_pubkey); + let builder_boost_factor = self + .validator_store + .determine_builder_boost_factor(&validator_pubkey); let service = self.clone(); - self.inner.context.executor.spawn( + self.inner.executor.spawn( async move { let result = service .publish_block(slot, validator_pubkey, builder_boost_factor) @@ -314,29 +323,18 @@ impl BlockService { #[allow(clippy::too_many_arguments)] async fn sign_and_publish_block( &self, - proposer_fallback: ProposerFallback, + proposer_fallback: ProposerFallback, slot: Slot, graffiti: Option, validator_pubkey: &PublicKeyBytes, - unsigned_block: UnsignedBlock, + unsigned_block: UnsignedBlock, ) -> Result<(), BlockError> { let signing_timer = validator_metrics::start_timer(&validator_metrics::BLOCK_SIGNING_TIMES); - let res = match unsigned_block { - UnsignedBlock::Full(block_contents) => { - let (block, maybe_blobs) = block_contents.deconstruct(); - self.validator_store - .sign_block(*validator_pubkey, block, slot) - .await - .map(|b| SignedBlock::Full(PublishBlockRequest::new(Arc::new(b), maybe_blobs))) - } - UnsignedBlock::Blinded(block) => self - .validator_store - .sign_block(*validator_pubkey, block, slot) - .await - .map(Arc::new) - .map(SignedBlock::Blinded), - }; + let res = self + .validator_store + .sign_block(*validator_pubkey, unsigned_block, slot) + .await; let signed_block = match res { Ok(block) => block, @@ -380,12 +378,13 @@ impl BlockService { }) .await?; + let metadata = BlockMetadata::from(&signed_block); info!( - block_type = ?signed_block.block_type(), - deposits = signed_block.num_deposits(), - attestations = signed_block.num_attestations(), + block_type = ?metadata.block_type, + deposits = metadata.num_deposits, + attestations = metadata.num_attestations, graffiti = ?graffiti.map(|g| g.as_utf8_lossy()), - slot = signed_block.slot().as_u64(), + slot = metadata.slot.as_u64(), "Successfully published block" ); Ok(()) @@ -404,7 +403,7 @@ impl BlockService { let randao_reveal = match self .validator_store - .randao_reveal(validator_pubkey, slot.epoch(E::slots_per_epoch())) + .randao_reveal(validator_pubkey, slot.epoch(S::E::slots_per_epoch())) .await { Ok(signature) => signature.into(), @@ -487,10 +486,9 @@ impl BlockService { async fn publish_signed_block_contents( &self, - signed_block: &SignedBlock, + signed_block: &SignedBlock, beacon_node: BeaconNodeHttpClient, ) -> Result<(), BlockError> { - let slot = signed_block.slot(); match signed_block { SignedBlock::Full(signed_block) => { let _post_timer = validator_metrics::start_timer_vec( @@ -500,7 +498,9 @@ impl BlockService { beacon_node .post_beacon_blocks_v2_ssz(signed_block, None) .await - .or_else(|e| handle_block_post_error(e, slot))? + .or_else(|e| { + handle_block_post_error(e, signed_block.signed_block().message().slot()) + })? } SignedBlock::Blinded(signed_block) => { let _post_timer = validator_metrics::start_timer_vec( @@ -510,7 +510,7 @@ impl BlockService { beacon_node .post_beacon_blinded_blocks_v2_ssz(signed_block, None) .await - .or_else(|e| handle_block_post_error(e, slot))? + .or_else(|e| handle_block_post_error(e, signed_block.message().slot()))? } } Ok::<_, BlockError>(()) @@ -523,9 +523,9 @@ impl BlockService { graffiti: Option, proposer_index: Option, builder_boost_factor: Option, - ) -> Result, BlockError> { + ) -> Result, BlockError> { let (block_response, _) = beacon_node - .get_validator_blocks_v3::( + .get_validator_blocks_v3::( slot, randao_reveal_ref, graffiti.as_ref(), @@ -539,13 +539,17 @@ impl BlockService { )) })?; - let unsigned_block = match block_response.data { - eth2::types::ProduceBlockV3Response::Full(block) => UnsignedBlock::Full(block), - eth2::types::ProduceBlockV3Response::Blinded(block) => UnsignedBlock::Blinded(block), + let (block_proposer, unsigned_block) = match block_response.data { + eth2::types::ProduceBlockV3Response::Full(block) => { + (block.block().proposer_index(), UnsignedBlock::Full(block)) + } + eth2::types::ProduceBlockV3Response::Blinded(block) => { + (block.proposer_index(), UnsignedBlock::Blinded(block)) + } }; info!(slot = slot.as_u64(), "Received unsigned block"); - if proposer_index != Some(unsigned_block.proposer_index()) { + if proposer_index != Some(block_proposer) { return Err(BlockError::Recoverable( "Proposer index does not match block proposer. Beacon chain re-orged".to_string(), )); @@ -553,81 +557,32 @@ impl BlockService { Ok::<_, BlockError>(unsigned_block) } - - /// Returns the builder boost factor of the given public key. - /// The priority order for fetching this value is: - /// - /// 1. validator_definitions.yml - /// 2. process level flag - fn get_builder_boost_factor(&self, validator_pubkey: &PublicKeyBytes) -> Option { - // Apply per validator configuration first. - let validator_builder_boost_factor = self - .validator_store - .determine_validator_builder_boost_factor(validator_pubkey); - - // Fallback to process-wide configuration if needed. - let maybe_builder_boost_factor = validator_builder_boost_factor.or_else(|| { - self.validator_store - .determine_default_builder_boost_factor() - }); - - if let Some(builder_boost_factor) = maybe_builder_boost_factor { - // if builder boost factor is set to 100 it should be treated - // as None to prevent unnecessary calculations that could - // lead to loss of information. - if builder_boost_factor == 100 { - return None; - } - return Some(builder_boost_factor); - } - - None - } } -pub enum UnsignedBlock { - Full(FullBlockContents), - Blinded(BlindedBeaconBlock), +/// Wrapper for values we want to log about a block we signed, for easy extraction from the possible +/// variants. +struct BlockMetadata { + block_type: BlockType, + slot: Slot, + num_deposits: usize, + num_attestations: usize, } -impl UnsignedBlock { - pub fn proposer_index(&self) -> u64 { - match self { - UnsignedBlock::Full(block) => block.block().proposer_index(), - UnsignedBlock::Blinded(block) => block.proposer_index(), - } - } -} - -#[derive(Debug)] -pub enum SignedBlock { - Full(PublishBlockRequest), - Blinded(Arc>), -} - -impl SignedBlock { - pub fn block_type(&self) -> BlockType { - match self { - SignedBlock::Full(_) => BlockType::Full, - SignedBlock::Blinded(_) => BlockType::Blinded, - } - } - pub fn slot(&self) -> Slot { - match self { - SignedBlock::Full(block) => block.signed_block().message().slot(), - SignedBlock::Blinded(block) => block.message().slot(), - } - } - pub fn num_deposits(&self) -> usize { - match self { - SignedBlock::Full(block) => block.signed_block().message().body().deposits().len(), - SignedBlock::Blinded(block) => block.message().body().deposits().len(), - } - } - pub fn num_attestations(&self) -> usize { - match self { - SignedBlock::Full(block) => block.signed_block().message().body().attestations_len(), - SignedBlock::Blinded(block) => block.message().body().attestations_len(), +impl From<&SignedBlock> for BlockMetadata { + fn from(value: &SignedBlock) -> Self { + match value { + SignedBlock::Full(block) => BlockMetadata { + block_type: BlockType::Full, + slot: block.signed_block().message().slot(), + num_deposits: block.signed_block().message().body().deposits().len(), + num_attestations: block.signed_block().message().body().attestations_len(), + }, + SignedBlock::Blinded(block) => BlockMetadata { + block_type: BlockType::Blinded, + slot: block.message().slot(), + num_deposits: block.message().body().deposits().len(), + num_attestations: block.message().body().attestations_len(), + }, } } } diff --git a/validator_client/validator_services/src/duties_service.rs b/validator_client/validator_services/src/duties_service.rs index cb5d967980..e04145f5f0 100644 --- a/validator_client/validator_services/src/duties_service.rs +++ b/validator_client/validator_services/src/duties_service.rs @@ -10,8 +10,6 @@ use crate::block_service::BlockServiceNotification; use crate::sync::poll_sync_committee_duties; use crate::sync::SyncDutiesMap; use beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; -use doppelganger_service::DoppelgangerStatus; -use environment::RuntimeContext; use eth2::types::{ AttesterData, BeaconCommitteeSubscription, DutiesResponse, InclusionListDuty, ProposerData, StateId, ValidatorId, @@ -25,11 +23,12 @@ use std::collections::{hash_map, BTreeMap, HashMap, HashSet}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; +use task_executor::TaskExecutor; use tokio::{sync::mpsc::Sender, time::sleep}; use tracing::{debug, error, info, warn}; use types::{ChainSpec, Epoch, EthSpec, Hash256, PublicKeyBytes, SelectionProof, Slot}; use validator_metrics::{get_int_gauge, set_int_gauge, ATTESTATION_DUTY}; -use validator_store::{Error as ValidatorStoreError, ValidatorStore}; +use validator_store::{DoppelgangerStatus, Error as ValidatorStoreError, ValidatorStore}; /// Only retain `HISTORICAL_DUTIES_EPOCHS` duties prior to the current epoch. const HISTORICAL_DUTIES_EPOCHS: u64 = 2; @@ -88,17 +87,17 @@ const _: () = assert!(ATTESTATION_SUBSCRIPTION_OFFSETS[0] > MIN_ATTESTATION_SUBS // The info in the enum variants is displayed in logging, clippy thinks it's dead code. #[derive(Debug)] -pub enum Error { +pub enum Error { UnableToReadSlotClock, FailedToDownloadAttesters(#[allow(dead_code)] String), - FailedToProduceSelectionProof(#[allow(dead_code)] ValidatorStoreError), + FailedToProduceSelectionProof(#[allow(dead_code)] ValidatorStoreError), InvalidModulo(#[allow(dead_code)] ArithError), Arith(#[allow(dead_code)] ArithError), SyncDutiesNotFound(#[allow(dead_code)] u64), FailedToDownloadInclusionListDuties(#[allow(dead_code)] String), } -impl From for Error { +impl From for Error { fn from(e: ArithError) -> Self { Self::Arith(e) } @@ -127,11 +126,11 @@ pub struct SubscriptionSlots { /// Create a selection proof for `duty`. /// /// Return `Ok(None)` if the attesting validator is not an aggregator. -async fn make_selection_proof( +async fn make_selection_proof( duty: &AttesterData, - validator_store: &ValidatorStore, + validator_store: &S, spec: &ChainSpec, -) -> Result, Error> { +) -> Result, Error> { let selection_proof = validator_store .produce_selection_proof(duty.pubkey, duty.slot) .await @@ -209,27 +208,135 @@ type ProposerMap = HashMap)>; type InclusionListDutiesMap = HashMap>; +pub struct DutiesServiceBuilder { + /// Provides the canonical list of locally-managed validators. + validator_store: Option>, + /// Tracks the current slot. + slot_clock: Option, + /// Provides HTTP access to remote beacon nodes. + beacon_nodes: Option>>, + /// The runtime for spawning tasks. + executor: Option, + /// The current chain spec. + spec: Option>, + //// Whether we permit large validator counts in the metrics. + enable_high_validator_count_metrics: bool, + /// If this validator is running in distributed mode. + distributed: bool, + disable_attesting: bool, +} + +impl Default for DutiesServiceBuilder { + fn default() -> Self { + Self::new() + } +} + +impl DutiesServiceBuilder { + pub fn new() -> Self { + Self { + validator_store: None, + slot_clock: None, + beacon_nodes: None, + executor: None, + spec: None, + enable_high_validator_count_metrics: false, + distributed: false, + disable_attesting: false, + } + } + + pub fn validator_store(mut self, validator_store: Arc) -> Self { + self.validator_store = Some(validator_store); + self + } + + pub fn slot_clock(mut self, slot_clock: T) -> Self { + self.slot_clock = Some(slot_clock); + self + } + + pub fn beacon_nodes(mut self, beacon_nodes: Arc>) -> Self { + self.beacon_nodes = Some(beacon_nodes); + self + } + + pub fn executor(mut self, executor: TaskExecutor) -> Self { + self.executor = Some(executor); + self + } + + pub fn spec(mut self, spec: Arc) -> Self { + self.spec = Some(spec); + self + } + + pub fn enable_high_validator_count_metrics( + mut self, + enable_high_validator_count_metrics: bool, + ) -> Self { + self.enable_high_validator_count_metrics = enable_high_validator_count_metrics; + self + } + + pub fn distributed(mut self, distributed: bool) -> Self { + self.distributed = distributed; + self + } + + pub fn disable_attesting(mut self, disable_attesting: bool) -> Self { + self.disable_attesting = disable_attesting; + self + } + + pub fn build(self) -> Result, String> { + Ok(DutiesService { + attesters: Default::default(), + proposers: Default::default(), + inclusion_list_duties: Default::default(), + sync_duties: SyncDutiesMap::new(self.distributed), + validator_store: self + .validator_store + .ok_or("Cannot build DutiesService without validator_store")?, + unknown_validator_next_poll_slots: Default::default(), + slot_clock: self + .slot_clock + .ok_or("Cannot build DutiesService without slot_clock")?, + beacon_nodes: self + .beacon_nodes + .ok_or("Cannot build DutiesService without beacon_nodes")?, + executor: self + .executor + .ok_or("Cannot build DutiesService without executor")?, + spec: self.spec.ok_or("Cannot build DutiesService without spec")?, + enable_high_validator_count_metrics: self.enable_high_validator_count_metrics, + distributed: self.distributed, + disable_attesting: self.disable_attesting, + }) + } +} + /// See the module-level documentation. -pub struct DutiesService { +pub struct DutiesService { /// Maps a validator public key to their duties for each epoch. pub attesters: RwLock, /// Maps an epoch to all *local* proposers in this epoch. Notably, this does not contain /// proposals for any validators which are not registered locally. pub proposers: RwLock, /// Map from validator index to sync committee duties. - pub sync_duties: SyncDutiesMap, /// Maps a validator public key to their inclusion list committee duties for each epoch. pub inclusion_list_duties: RwLock, + pub sync_duties: SyncDutiesMap, /// Provides the canonical list of locally-managed validators. - pub validator_store: Arc>, + pub validator_store: Arc, /// Maps unknown validator pubkeys to the next slot time when a poll should be conducted again. pub unknown_validator_next_poll_slots: RwLock>, /// Tracks the current slot. pub slot_clock: T, /// Provides HTTP access to remote beacon nodes. - pub beacon_nodes: Arc>, + pub beacon_nodes: Arc>, /// The runtime for spawning tasks. - pub context: RuntimeContext, + pub executor: TaskExecutor, /// The current chain spec. pub spec: Arc, //// Whether we permit large validator counts in the metrics. @@ -239,7 +346,7 @@ pub struct DutiesService { pub disable_attesting: bool, } -impl DutiesService { +impl DutiesService { /// Returns the total number of validators known to the duties service. pub fn total_validator_count(&self) -> usize { self.validator_store.num_voting_validators() @@ -290,7 +397,7 @@ impl DutiesService { /// It is possible that multiple validators have an identical proposal slot, however that is /// likely the result of heavy forking (lol) or inconsistent beacon node connections. pub fn block_proposers(&self, slot: Slot) -> HashSet { - let epoch = slot.epoch(E::slots_per_epoch()); + let epoch = slot.epoch(S::E::slots_per_epoch()); // Only collect validators that are considered safe in terms of doppelganger protection. let signing_pubkeys: HashSet<_> = self @@ -315,7 +422,7 @@ impl DutiesService { /// Returns all `ValidatorDuty` for the given `slot`. pub fn attesters(&self, slot: Slot) -> Vec { - let epoch = slot.epoch(E::slots_per_epoch()); + let epoch = slot.epoch(S::E::slots_per_epoch()); // Only collect validators that are considered safe in terms of doppelganger protection. let signing_pubkeys: HashSet<_> = self @@ -337,7 +444,7 @@ impl DutiesService { /// Returns all `InclusionListDuty` for the given `slot`. pub fn inclusion_list_duties(&self, slot: Slot) -> Vec { - let epoch = slot.epoch(E::slots_per_epoch()); + let epoch = slot.epoch(S::E::slots_per_epoch()); if !self.spec.is_focil_enabled_for_epoch(epoch) { return vec![]; @@ -376,15 +483,15 @@ impl DutiesService { /// process every slot, which has the chance of creating a theoretically unlimited backlog of tasks. /// It was a conscious decision to choose to drop tasks on an overloaded/latent system rather than /// overload it even more. -pub fn start_update_service( - core_duties_service: Arc>, +pub fn start_update_service( + core_duties_service: Arc>, mut block_service_tx: Sender, ) { /* * Spawn the task which updates the map of pubkey to validator index. */ let duties_service = core_duties_service.clone(); - core_duties_service.context.executor.spawn( + core_duties_service.executor.spawn( async move { loop { // Run this poll before the wait, this should hopefully download all the indices @@ -407,7 +514,7 @@ pub fn start_update_service( * Spawn the task which keeps track of local block proposal duties. */ let duties_service = core_duties_service.clone(); - core_duties_service.context.executor.spawn( + core_duties_service.executor.spawn( async move { loop { if let Some(duration) = duties_service.slot_clock.duration_to_next_slot() { @@ -440,7 +547,7 @@ pub fn start_update_service( * Spawn the task which keeps track of local attestation duties. */ let duties_service = core_duties_service.clone(); - core_duties_service.context.executor.spawn( + core_duties_service.executor.spawn( async move { loop { if let Some(duration) = duties_service.slot_clock.duration_to_next_slot() { @@ -465,7 +572,7 @@ pub fn start_update_service( // Spawn the task which keeps track of local sync committee duties. let duties_service = core_duties_service.clone(); - core_duties_service.context.executor.spawn( + core_duties_service.executor.spawn( async move { loop { if let Err(e) = poll_sync_committee_duties(&duties_service).await { @@ -496,7 +603,7 @@ pub fn start_update_service( * Spawn the task which keeps track of local inclusion list duties. */ let duties_service = core_duties_service.clone(); - core_duties_service.context.executor.spawn( + core_duties_service.executor.spawn( async move { loop { if let Some(duration) = duties_service.slot_clock.duration_to_next_slot() { @@ -508,7 +615,7 @@ pub fn start_update_service( continue; } - if let Err(e) = poll_beacon_inclusion_list_duties(&duties_service).await { + if let Err(e) = poll_beacon_inclusion_list_duties::(&duties_service).await { error!( error = ?e, "Failed to poll inclusion list duties" @@ -522,8 +629,8 @@ pub fn start_update_service( /// Iterate through all the voting pubkeys in the `ValidatorStore` and attempt to learn any unknown /// validator indices. -async fn poll_validator_indices( - duties_service: &DutiesService, +async fn poll_validator_indices( + duties_service: &DutiesService, ) { let _timer = validator_metrics::start_timer_vec( &validator_metrics::DUTIES_SERVICE_TIMES, @@ -542,16 +649,14 @@ async fn poll_validator_indices( // This is on its own line to avoid some weirdness with locks and if statements. let is_known = duties_service .validator_store - .initialized_validators() - .read() - .get_index(&pubkey) + .validator_index(&pubkey) .is_some(); if !is_known { let current_slot_opt = duties_service.slot_clock.now(); if let Some(current_slot) = current_slot_opt { - let is_first_slot_of_epoch = current_slot % E::slots_per_epoch() == 0; + let is_first_slot_of_epoch = current_slot % S::E::slots_per_epoch() == 0; // Query an unknown validator later if it was queried within the last epoch, or if // the current slot is the first slot of an epoch. @@ -602,9 +707,7 @@ async fn poll_validator_indices( ); duties_service .validator_store - .initialized_validators() - .write() - .set_index(&pubkey, response.data.index); + .set_validator_index(&pubkey, response.data.index); duties_service .unknown_validator_next_poll_slots @@ -615,7 +718,7 @@ async fn poll_validator_indices( // the beacon chain. Ok(None) => { if let Some(current_slot) = current_slot_opt { - let next_poll_slot = current_slot.saturating_add(E::slots_per_epoch()); + let next_poll_slot = current_slot.saturating_add(S::E::slots_per_epoch()); duties_service .unknown_validator_next_poll_slots .write() @@ -646,9 +749,9 @@ async fn poll_validator_indices( /// 2. As above, but for the next-epoch. /// 3. Push out any attestation subnet subscriptions to the BN. /// 4. Prune old entries from `duties_service.attesters`. -async fn poll_beacon_attesters( - duties_service: &Arc>, -) -> Result<(), Error> { +async fn poll_beacon_attesters( + duties_service: &Arc>, +) -> Result<(), Error> { let current_epoch_timer = validator_metrics::start_timer_vec( &validator_metrics::DUTIES_SERVICE_TIMES, &[validator_metrics::UPDATE_ATTESTERS_CURRENT_EPOCH], @@ -658,7 +761,7 @@ async fn poll_beacon_attesters( .slot_clock .now() .ok_or(Error::UnableToReadSlotClock)?; - let current_epoch = current_slot.epoch(E::slots_per_epoch()); + let current_epoch = current_slot.epoch(S::E::slots_per_epoch()); let next_epoch = current_epoch + 1; // Collect *all* pubkeys, even those undergoing doppelganger protection. @@ -672,10 +775,8 @@ async fn poll_beacon_attesters( let local_indices = { let mut local_indices = Vec::with_capacity(local_pubkeys.len()); - let vals_ref = duties_service.validator_store.initialized_validators(); - let vals = vals_ref.read(); for &pubkey in &local_pubkeys { - if let Some(validator_index) = vals.get_index(&pubkey) { + if let Some(validator_index) = duties_service.validator_store.validator_index(&pubkey) { local_indices.push(validator_index) } } @@ -699,7 +800,7 @@ async fn poll_beacon_attesters( ) } - update_per_validator_duty_metrics::(duties_service, current_epoch, current_slot); + update_per_validator_duty_metrics(duties_service, current_epoch, current_slot); drop(current_epoch_timer); let next_epoch_timer = validator_metrics::start_timer_vec( @@ -720,7 +821,7 @@ async fn poll_beacon_attesters( ) } - update_per_validator_duty_metrics::(duties_service, next_epoch, current_slot); + update_per_validator_duty_metrics(duties_service, next_epoch, current_slot); drop(next_epoch_timer); let subscriptions_timer = validator_metrics::start_timer_vec( @@ -741,7 +842,7 @@ async fn poll_beacon_attesters( * std::cmp::max( 1, local_pubkeys.len() * ATTESTATION_SUBSCRIPTION_OFFSETS.len() - / E::slots_per_epoch() as usize, + / S::E::slots_per_epoch() as usize, ) / overallocation_denominator; let mut subscriptions = Vec::with_capacity(num_expected_subscriptions); @@ -837,12 +938,12 @@ async fn poll_beacon_attesters( /// For the given `local_indices` and `local_pubkeys`, download the duties for the given `epoch` and /// store them in `duties_service.attesters`. -async fn poll_beacon_attesters_for_epoch( - duties_service: &Arc>, +async fn poll_beacon_attesters_for_epoch( + duties_service: &Arc>, epoch: Epoch, local_indices: &[u64], local_pubkeys: &HashSet, -) -> Result<(), Error> { +) -> Result<(), Error> { // No need to bother the BN if we don't have any validators. if local_indices.is_empty() { debug!( @@ -986,7 +1087,7 @@ async fn poll_beacon_attesters_for_epoch( // Spawn the background task to compute selection proofs. let subservice = duties_service.clone(); - duties_service.context.executor.spawn( + duties_service.executor.spawn( async move { fill_in_selection_proofs(subservice, new_duties, dependent_root).await; }, @@ -997,8 +1098,8 @@ async fn poll_beacon_attesters_for_epoch( } /// Get a filtered list of local validators for which we don't already know their duties for that epoch -fn get_uninitialized_validators( - duties_service: &Arc>, +fn get_uninitialized_validators( + duties_service: &Arc>, epoch: &Epoch, local_pubkeys: &HashSet, ) -> Vec { @@ -1014,8 +1115,8 @@ fn get_uninitialized_validators( .collect::>() } -fn update_per_validator_duty_metrics( - duties_service: &Arc>, +fn update_per_validator_duty_metrics( + duties_service: &Arc>, epoch: Epoch, current_slot: Slot, ) { @@ -1030,14 +1131,14 @@ fn update_per_validator_duty_metrics( get_int_gauge(&ATTESTATION_DUTY, &[&validator_index.to_string()]) { let existing_slot = Slot::new(existing_slot_gauge.get() as u64); - let existing_epoch = existing_slot.epoch(E::slots_per_epoch()); + let existing_epoch = existing_slot.epoch(S::E::slots_per_epoch()); // First condition ensures that we switch to the next epoch duty slot // once the current epoch duty slot passes. // Second condition is to ensure that next epoch duties don't override // current epoch duties. if existing_slot < current_slot - || (duty_slot.epoch(E::slots_per_epoch()) <= existing_epoch + || (duty_slot.epoch(S::E::slots_per_epoch()) <= existing_epoch && duty_slot > current_slot && duty_slot != existing_slot) { @@ -1055,11 +1156,11 @@ fn update_per_validator_duty_metrics( } } -async fn post_validator_duties_attester( - duties_service: &Arc>, +async fn post_validator_duties_attester( + duties_service: &Arc>, epoch: Epoch, validator_indices: &[u64], -) -> Result>, Error> { +) -> Result>, Error> { duties_service .beacon_nodes .first_success(|beacon_node| async move { @@ -1079,8 +1180,8 @@ async fn post_validator_duties_attester( /// /// Duties are computed in batches each slot. If a re-org is detected then the process will /// terminate early as it is assumed the selection proofs from `duties` are no longer relevant. -async fn fill_in_selection_proofs( - duties_service: Arc>, +async fn fill_in_selection_proofs( + duties_service: Arc>, duties: Vec, dependent_root: Hash256, ) { @@ -1131,7 +1232,7 @@ async fn fill_in_selection_proofs( .then(|duty| async { let opt_selection_proof = make_selection_proof( &duty, - &duties_service.validator_store, + duties_service.validator_store.as_ref(), &duties_service.spec, ) .await?; @@ -1170,7 +1271,7 @@ async fn fill_in_selection_proofs( }; let attester_map = attesters.entry(duty.pubkey).or_default(); - let epoch = duty.slot.epoch(E::slots_per_epoch()); + let epoch = duty.slot.epoch(S::E::slots_per_epoch()); match attester_map.entry(epoch) { hash_map::Entry::Occupied(mut entry) => { // No need to update duties for which no proof was computed. @@ -1234,15 +1335,15 @@ async fn fill_in_selection_proofs( /// 3. Prune old entries from `duties_service.inclusion_list_duties`. /// /// NOTE: There are no subscriptions to manage, since the inclusion list topics are global. -async fn poll_beacon_inclusion_list_duties( - duties_service: &Arc>, -) -> Result<(), Error> { +async fn poll_beacon_inclusion_list_duties( + duties_service: &Arc>, +) -> Result<(), Error> { // TODO: add timer metric for current epoch let current_slot = duties_service .slot_clock .now() .ok_or(Error::UnableToReadSlotClock)?; - let current_epoch = current_slot.epoch(E::slots_per_epoch()); + let current_epoch = current_slot.epoch(S::E::slots_per_epoch()); let next_epoch = current_epoch + 1; // Collect *all* pubkeys, even those undergoing doppelganger protection. @@ -1253,10 +1354,8 @@ async fn poll_beacon_inclusion_list_duties( let local_indices = { let mut local_indices = Vec::with_capacity(local_pubkeys.len()); - let vals_ref = duties_service.validator_store.initialized_validators(); - let vals = vals_ref.read(); for &pubkey in &local_pubkeys { - if let Some(validator_index) = vals.get_index(&pubkey) { + if let Some(validator_index) = duties_service.validator_store.validator_index(&pubkey) { local_indices.push(validator_index) } } @@ -1315,12 +1414,12 @@ async fn poll_beacon_inclusion_list_duties( /// For the given `local_indices` and `local_pubkeys`, download the inclusion list duties for the /// given epoch and store them in `duties_service.inclusion_list_duties`. -async fn poll_beacon_inclusion_list_duties_for_epoch( - duties_service: &Arc>, +async fn poll_beacon_inclusion_list_duties_for_epoch( + duties_service: &Arc>, epoch: Epoch, local_indices: &[u64], local_pubkeys: &HashSet, -) -> Result<(), Error> { +) -> Result<(), Error> { // No need to bother the BN if we don't have any validators. if local_indices.is_empty() { debug!( @@ -1446,11 +1545,11 @@ async fn poll_beacon_inclusion_list_duties_for_epoch( - duties_service: &Arc>, +async fn post_validator_duties_inclusion_list( + duties_service: &Arc>, epoch: Epoch, validator_indices: &[u64], -) -> Result>, Error> { +) -> Result>, Error> { duties_service .beacon_nodes .first_success(|beacon_node| async move { @@ -1486,10 +1585,10 @@ async fn post_validator_duties_inclusion_list( - duties_service: &DutiesService, +async fn poll_beacon_proposers( + duties_service: &DutiesService, block_service_tx: &mut Sender, -) -> Result<(), Error> { +) -> Result<(), Error> { let _timer = validator_metrics::start_timer_vec( &validator_metrics::DUTIES_SERVICE_TIMES, &[validator_metrics::UPDATE_PROPOSERS], @@ -1499,17 +1598,17 @@ async fn poll_beacon_proposers( .slot_clock .now() .ok_or(Error::UnableToReadSlotClock)?; - let current_epoch = current_slot.epoch(E::slots_per_epoch()); + let current_epoch = current_slot.epoch(S::E::slots_per_epoch()); // Notify the block proposal service for any proposals that we have in our cache. // // See the function-level documentation for more information. let initial_block_proposers = duties_service.block_proposers(current_slot); - notify_block_production_service( + notify_block_production_service::( current_slot, &initial_block_proposers, block_service_tx, - &duties_service.validator_store, + duties_service.validator_store.as_ref(), ) .await; @@ -1591,11 +1690,11 @@ async fn poll_beacon_proposers( // // See the function-level documentation for more reasoning about this behaviour. if !additional_block_producers.is_empty() { - notify_block_production_service( + notify_block_production_service::( current_slot, &additional_block_producers, block_service_tx, - &duties_service.validator_store, + duties_service.validator_store.as_ref(), ) .await; debug!( @@ -1616,11 +1715,11 @@ async fn poll_beacon_proposers( } /// Notify the block service if it should produce a block. -async fn notify_block_production_service( +async fn notify_block_production_service( current_slot: Slot, block_proposers: &HashSet, block_service_tx: &mut Sender, - validator_store: &ValidatorStore, + validator_store: &S, ) { let non_doppelganger_proposers = block_proposers .iter() diff --git a/validator_client/validator_services/src/inclusion_list_service.rs b/validator_client/validator_services/src/inclusion_list_service.rs index e8157f8f0c..0b6404d565 100644 --- a/validator_client/validator_services/src/inclusion_list_service.rs +++ b/validator_client/validator_services/src/inclusion_list_service.rs @@ -1,31 +1,123 @@ use crate::duties_service::DutiesService; use beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; -use environment::RuntimeContext; use futures::future::join_all; use logging::crit; use slot_clock::SlotClock; use std::ops::Deref; use std::sync::Arc; +use task_executor::TaskExecutor; use tokio::time::{sleep, Duration}; use tracing::{debug, error, info, trace, warn}; use types::{ChainSpec, EthSpec, InclusionList, InclusionListDuty, Slot}; use validator_store::{Error as ValidatorStoreError, ValidatorStore}; +/// Builds an `AttestationService`. +#[derive(Default)] +pub struct InclusionListServiceBuilder { + duties_service: Option>>, + validator_store: Option>, + slot_clock: Option, + beacon_nodes: Option>>, + executor: Option, + chain_spec: Option>, + disable: bool, +} + +impl InclusionListServiceBuilder { + pub fn new() -> Self { + Self { + duties_service: None, + validator_store: None, + slot_clock: None, + beacon_nodes: None, + executor: None, + chain_spec: None, + disable: true, + } + } + + pub fn duties_service(mut self, service: Arc>) -> Self { + self.duties_service = Some(service); + self + } + + pub fn validator_store(mut self, store: Arc) -> Self { + self.validator_store = Some(store); + self + } + + pub fn slot_clock(mut self, slot_clock: T) -> Self { + self.slot_clock = Some(slot_clock); + self + } + + pub fn beacon_nodes(mut self, beacon_nodes: Arc>) -> Self { + self.beacon_nodes = Some(beacon_nodes); + self + } + + pub fn executor(mut self, executor: TaskExecutor) -> Self { + self.executor = Some(executor); + self + } + + pub fn chain_spec(mut self, chain_spec: Arc) -> Self { + self.chain_spec = Some(chain_spec); + self + } + + pub fn disable(mut self, disable: bool) -> Self { + self.disable = disable; + self + } + + pub fn build(self) -> Result, String> { + Ok(InclusionListService { + inner: Arc::new(Inner { + duties_service: self + .duties_service + .ok_or("Cannot build AttestationService without duties_service")?, + validator_store: self + .validator_store + .ok_or("Cannot build AttestationService without validator_store")?, + slot_clock: self + .slot_clock + .ok_or("Cannot build AttestationService without slot_clock")?, + beacon_nodes: self + .beacon_nodes + .ok_or("Cannot build AttestationService without beacon_nodes")?, + executor: self + .executor + .ok_or("Cannot build AttestationService without executor")?, + chain_spec: self + .chain_spec + .ok_or("Cannot build AttestationService without chain_spec")?, + disable: self.disable, + }), + }) + } +} + /// Helper to minimise `Arc` usage. -pub struct Inner { - duties_service: Arc>, - validator_store: Arc>, +pub struct Inner { + duties_service: Arc>, + validator_store: Arc, slot_clock: T, - beacon_nodes: Arc>, - context: RuntimeContext, + beacon_nodes: Arc>, + executor: TaskExecutor, + // TODO(focil) + #[allow(dead_code)] + chain_spec: Arc, + #[allow(dead_code)] + disable: bool, } /// Attempts to produce inclusion lists for all known validators 3/4 of the way through each slot. -pub struct InclusionListService { - inner: Arc>, +pub struct InclusionListService { + inner: Arc>, } -impl Clone for InclusionListService { +impl Clone for InclusionListService { fn clone(&self) -> Self { Self { inner: self.inner.clone(), @@ -33,36 +125,19 @@ impl Clone for InclusionListService { } } -impl Deref for InclusionListService { - type Target = Inner; +impl Deref for InclusionListService { + type Target = Inner; fn deref(&self) -> &Self::Target { self.inner.deref() } } -impl InclusionListService { - pub fn new( - duties_service: Arc>, - validator_store: Arc>, - slot_clock: T, - beacon_nodes: Arc>, - context: RuntimeContext, - ) -> Self { - Self { - inner: Arc::new(Inner { - duties_service, - validator_store, - slot_clock, - beacon_nodes, - context, - }), - } - } - +impl InclusionListService { /// Starts the service which periodically produces inclusion lists. pub fn start_update_service(self, spec: &ChainSpec) -> Result<(), String> { let slot_duration = Duration::from_secs(spec.seconds_per_slot); + let duration_to_next_slot = self .slot_clock .duration_to_next_slot() @@ -73,7 +148,7 @@ impl InclusionListService { "Inclusion list production service started", ); - let executor = self.context.executor.clone(); + let executor = self.executor.clone(); let chain_spec = spec.clone(); let interval_fut = async move { loop { @@ -101,7 +176,7 @@ impl InclusionListService { Ok(()) } - /// Spawn a new task that downloads, signs and uploads the inclusion lists to the beacon node. + /// Spawn a task that downloads, signs and uploads the inclusion lists to the beacon node. // TODO(focil) I don't think we need `slot_duration` here, unless we need to make some calculation // related to the freeze deadline. fn spawn_inclusion_list_task( @@ -113,7 +188,7 @@ impl InclusionListService { let next_slot = self.slot_clock.now().ok_or("Failed to read slot clock")? + 1; - if !spec.is_focil_enabled_for_epoch(next_slot.epoch(E::slots_per_epoch())) { + if !spec.is_focil_enabled_for_epoch(next_slot.epoch(S::E::slots_per_epoch())) { debug!("FOCIL not enabled"); return Ok(()); } @@ -125,11 +200,21 @@ impl InclusionListService { .ok_or("Unable to determine duration to next slot")?; let inclusion_list_duties = self.duties_service.inclusion_list_duties(next_slot); - self.inner.context.executor.spawn_ignoring_error( - self.clone() - .produce_and_publish_inclusion_lists(next_slot, inclusion_list_duties), - "inclusion list publish", - ); + + let executor = self.executor.clone(); + + // TODO(focil) remove this clone + let this = self.clone(); + let interval_fut = async move { + if let Err(e) = this + .produce_and_publish_inclusion_lists(next_slot, inclusion_list_duties) + .await + { + error!(error = e, "Failed to produce and publish inclusion list") + }; + }; + + executor.spawn(interval_fut, "inclusion_list_service"); Ok(()) } @@ -148,34 +233,27 @@ impl InclusionListService { self, slot: Slot, validator_duties: Vec, - ) -> Result<(), ()> { + ) -> Result<(), String> { let validator_store = self.validator_store.clone(); if validator_duties.is_empty() { return Ok(()); } + // TODO(focil) + #[allow(unused_variables)] let current_epoch = self .slot_clock .now() - .ok_or("Unable to determine current slot from clock") - .map(|slot| slot.epoch(E::slots_per_epoch())); - - // TODO(focil) unused variable - let _current_epoch = current_epoch.map_err(|e| { - crit!( - error = format!("{:?}", e), - ?slot, - "Error during inclusion list routine", - ) - })?; + .ok_or("Unable to determine current slot from clock")? + .epoch(S::E::slots_per_epoch()); let inclusion_list_transactions = self .beacon_nodes .first_success(|beacon_node| async move { // TODO(focil) add timer metric beacon_node - .get_validator_inclusion_list::(slot) + .get_validator_inclusion_list::(slot) .await .map_err(|e| format!("Failed to produce inclusion list: {:?}", e)) .map(|result| result.ok_or("Inclusion list unavailable".to_string()))? @@ -187,7 +265,8 @@ impl InclusionListService { error = format!("{}", e), ?slot, "Error during inclusion list routine" - ) + ); + format!("{}", e) })?; // Create futures to produce signed `InclusionList` objects. diff --git a/validator_client/src/latency.rs b/validator_client/validator_services/src/latency_service.rs similarity index 89% rename from validator_client/src/latency.rs rename to validator_client/validator_services/src/latency_service.rs index edd8daa731..c810a03a80 100644 --- a/validator_client/src/latency.rs +++ b/validator_client/validator_services/src/latency_service.rs @@ -1,10 +1,9 @@ use beacon_node_fallback::BeaconNodeFallback; -use environment::RuntimeContext; use slot_clock::SlotClock; use std::sync::Arc; +use task_executor::TaskExecutor; use tokio::time::sleep; use tracing::debug; -use types::EthSpec; /// The latency service will run 11/12ths of the way through the slot. pub const SLOT_DELAY_MULTIPLIER: u32 = 11; @@ -12,10 +11,10 @@ pub const SLOT_DELAY_DENOMINATOR: u32 = 12; /// Starts a service that periodically checks the latency between the VC and the /// candidate BNs. -pub fn start_latency_service( - context: RuntimeContext, +pub fn start_latency_service( + executor: TaskExecutor, slot_clock: T, - beacon_nodes: Arc>, + beacon_nodes: Arc>, ) { let future = async move { loop { @@ -57,5 +56,5 @@ pub fn start_latency_service( } }; - context.executor.spawn(future, "latency"); + executor.spawn(future, "latency"); } diff --git a/validator_client/validator_services/src/lib.rs b/validator_client/validator_services/src/lib.rs index e3ddbf7ae7..6412464f8e 100644 --- a/validator_client/validator_services/src/lib.rs +++ b/validator_client/validator_services/src/lib.rs @@ -2,6 +2,8 @@ pub mod attestation_service; pub mod block_service; pub mod duties_service; pub mod inclusion_list_service; +pub mod latency_service; +pub mod notifier_service; pub mod preparation_service; pub mod sync; pub mod sync_committee_service; diff --git a/validator_client/src/notifier.rs b/validator_client/validator_services/src/notifier_service.rs similarity index 87% rename from validator_client/src/notifier.rs rename to validator_client/validator_services/src/notifier_service.rs index 75b3d46457..6b8ea04edb 100644 --- a/validator_client/src/notifier.rs +++ b/validator_client/validator_services/src/notifier_service.rs @@ -1,17 +1,20 @@ -use crate::{DutiesService, ProductionValidatorClient}; -use metrics::set_gauge; +use crate::duties_service::DutiesService; use slot_clock::SlotClock; +use std::sync::Arc; +use task_executor::TaskExecutor; use tokio::time::{sleep, Duration}; use tracing::{debug, error, info}; -use types::EthSpec; +use types::{ChainSpec, EthSpec}; +use validator_metrics::set_gauge; +use validator_store::ValidatorStore; /// Spawns a notifier service which periodically logs information about the node. -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(); - - let slot_duration = Duration::from_secs(context.eth2_config.spec.seconds_per_slot); +pub fn spawn_notifier( + duties_service: Arc>, + executor: TaskExecutor, + spec: &ChainSpec, +) -> Result<(), String> { + let slot_duration = Duration::from_secs(spec.seconds_per_slot); let interval_fut = async move { loop { @@ -32,7 +35,7 @@ pub fn spawn_notifier(client: &ProductionValidatorClient) -> Resu } /// Performs a single notification routine. -async fn notify(duties_service: &DutiesService) { +async fn notify(duties_service: &DutiesService) { let (candidate_info, num_available, num_synced) = duties_service.beacon_nodes.get_notifier_info().await; let num_total = candidate_info.len(); @@ -99,7 +102,7 @@ async fn notify(duties_service: &DutiesServi } if let Some(slot) = duties_service.slot_clock.now() { - let epoch = slot.epoch(E::slots_per_epoch()); + let epoch = slot.epoch(S::E::slots_per_epoch()); let total_validators = duties_service.total_validator_count(); let proposing_validators = duties_service.proposer_count(epoch); diff --git a/validator_client/validator_services/src/preparation_service.rs b/validator_client/validator_services/src/preparation_service.rs index 3367f2d6ca..b59e3266dc 100644 --- a/validator_client/validator_services/src/preparation_service.rs +++ b/validator_client/validator_services/src/preparation_service.rs @@ -1,7 +1,5 @@ use beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; use bls::PublicKeyBytes; -use doppelganger_service::DoppelgangerStatus; -use environment::RuntimeContext; use parking_lot::RwLock; use slot_clock::SlotClock; use std::collections::HashMap; @@ -9,13 +7,16 @@ use std::hash::Hash; use std::ops::Deref; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; +use task_executor::TaskExecutor; use tokio::time::{sleep, Duration}; use tracing::{debug, error, info, warn}; use types::{ Address, ChainSpec, EthSpec, ProposerPreparationData, SignedValidatorRegistrationData, ValidatorRegistrationData, }; -use validator_store::{Error as ValidatorStoreError, ProposalData, ValidatorStore}; +use validator_store::{ + DoppelgangerStatus, Error as ValidatorStoreError, ProposalData, ValidatorStore, +}; /// Number of epochs before the Bellatrix hard fork to begin posting proposer preparations. const PROPOSER_PREPARATION_LOOKAHEAD_EPOCHS: u64 = 2; @@ -25,28 +26,28 @@ const EPOCHS_PER_VALIDATOR_REGISTRATION_SUBMISSION: u64 = 1; /// Builds an `PreparationService`. #[derive(Default)] -pub struct PreparationServiceBuilder { - validator_store: Option>>, +pub struct PreparationServiceBuilder { + validator_store: Option>, slot_clock: Option, - beacon_nodes: Option>>, - context: Option>, + beacon_nodes: Option>>, + executor: Option, builder_registration_timestamp_override: Option, validator_registration_batch_size: Option, } -impl PreparationServiceBuilder { +impl PreparationServiceBuilder { pub fn new() -> Self { Self { validator_store: None, slot_clock: None, beacon_nodes: None, - context: None, + executor: None, builder_registration_timestamp_override: None, validator_registration_batch_size: None, } } - pub fn validator_store(mut self, store: Arc>) -> Self { + pub fn validator_store(mut self, store: Arc) -> Self { self.validator_store = Some(store); self } @@ -56,13 +57,13 @@ impl PreparationServiceBuilder { self } - pub fn beacon_nodes(mut self, beacon_nodes: Arc>) -> Self { + pub fn beacon_nodes(mut self, beacon_nodes: Arc>) -> Self { self.beacon_nodes = Some(beacon_nodes); self } - pub fn runtime_context(mut self, context: RuntimeContext) -> Self { - self.context = Some(context); + pub fn executor(mut self, executor: TaskExecutor) -> Self { + self.executor = Some(executor); self } @@ -82,7 +83,7 @@ impl PreparationServiceBuilder { self } - pub fn build(self) -> Result, String> { + pub fn build(self) -> Result, String> { Ok(PreparationService { inner: Arc::new(Inner { validator_store: self @@ -94,9 +95,9 @@ impl PreparationServiceBuilder { beacon_nodes: self .beacon_nodes .ok_or("Cannot build PreparationService without beacon_nodes")?, - context: self - .context - .ok_or("Cannot build PreparationService without runtime_context")?, + executor: self + .executor + .ok_or("Cannot build PreparationService without executor")?, builder_registration_timestamp_override: self .builder_registration_timestamp_override, validator_registration_batch_size: self.validator_registration_batch_size.ok_or( @@ -109,11 +110,11 @@ impl PreparationServiceBuilder { } /// Helper to minimise `Arc` usage. -pub struct Inner { - validator_store: Arc>, +pub struct Inner { + validator_store: Arc, slot_clock: T, - beacon_nodes: Arc>, - context: RuntimeContext, + beacon_nodes: Arc>, + executor: TaskExecutor, builder_registration_timestamp_override: Option, // Used to track unpublished validator registration changes. validator_registration_cache: @@ -145,11 +146,11 @@ impl From for ValidatorRegistrationKey { } /// Attempts to produce proposer preparations for all known validators at the beginning of each epoch. -pub struct PreparationService { - inner: Arc>, +pub struct PreparationService { + inner: Arc>, } -impl Clone for PreparationService { +impl Clone for PreparationService { fn clone(&self) -> Self { Self { inner: self.inner.clone(), @@ -157,15 +158,15 @@ impl Clone for PreparationService { } } -impl Deref for PreparationService { - type Target = Inner; +impl Deref for PreparationService { + type Target = Inner; fn deref(&self) -> &Self::Target { self.inner.deref() } } -impl PreparationService { +impl PreparationService { pub fn start_update_service(self, spec: &ChainSpec) -> Result<(), String> { self.clone().start_validator_registration_service(spec)?; self.start_proposer_prepare_service(spec) @@ -176,7 +177,7 @@ impl PreparationService { let slot_duration = Duration::from_secs(spec.seconds_per_slot); info!("Proposer preparation service started"); - let executor = self.context.executor.clone(); + let executor = self.executor.clone(); let spec = spec.clone(); let interval_fut = async move { @@ -215,7 +216,7 @@ impl PreparationService { let spec = spec.clone(); let slot_duration = Duration::from_secs(spec.seconds_per_slot); - let executor = self.context.executor.clone(); + let executor = self.executor.clone(); let validator_registration_fut = async move { loop { @@ -243,10 +244,9 @@ impl PreparationService { /// This avoids spamming the BN with preparations before the Bellatrix fork epoch, which may /// cause errors if it doesn't support the preparation API. fn should_publish_at_current_slot(&self, spec: &ChainSpec) -> bool { - let current_epoch = self - .slot_clock - .now() - .map_or(E::genesis_epoch(), |slot| slot.epoch(E::slots_per_epoch())); + let current_epoch = self.slot_clock.now().map_or(S::E::genesis_epoch(), |slot| { + slot.epoch(S::E::slots_per_epoch()) + }); spec.bellatrix_fork_epoch.is_some_and(|fork_epoch| { current_epoch + PROPOSER_PREPARATION_LOOKAHEAD_EPOCHS >= fork_epoch }) @@ -367,7 +367,8 @@ impl PreparationService { // Check if any have changed or it's been `EPOCHS_PER_VALIDATOR_REGISTRATION_SUBMISSION`. if let Some(slot) = self.slot_clock.now() { - if slot % (E::slots_per_epoch() * EPOCHS_PER_VALIDATOR_REGISTRATION_SUBMISSION) == 0 { + if slot % (S::E::slots_per_epoch() * EPOCHS_PER_VALIDATOR_REGISTRATION_SUBMISSION) == 0 + { self.publish_validator_registration_data(registration_keys) .await?; } else if !changed_keys.is_empty() { diff --git a/validator_client/validator_services/src/sync.rs b/validator_client/validator_services/src/sync.rs index 5151633514..c13b70db80 100644 --- a/validator_client/validator_services/src/sync.rs +++ b/validator_client/validator_services/src/sync.rs @@ -1,15 +1,13 @@ use crate::duties_service::{DutiesService, Error}; -use doppelganger_service::DoppelgangerStatus; use futures::future::join_all; use logging::crit; use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; use slot_clock::SlotClock; use std::collections::{HashMap, HashSet}; -use std::marker::PhantomData; use std::sync::Arc; use tracing::{debug, info, warn}; use types::{ChainSpec, EthSpec, PublicKeyBytes, Slot, SyncDuty, SyncSelectionProof, SyncSubnetId}; -use validator_store::Error as ValidatorStoreError; +use validator_store::{DoppelgangerStatus, Error as ValidatorStoreError, ValidatorStore}; /// Number of epochs in advance to compute selection proofs when not in `distributed` mode. pub const AGGREGATION_PRE_COMPUTE_EPOCHS: u64 = 2; @@ -28,12 +26,11 @@ pub const AGGREGATION_PRE_COMPUTE_SLOTS_DISTRIBUTED: u64 = 1; /// 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. @@ -81,12 +78,11 @@ pub struct SlotDuties { pub aggregators: HashMap>, } -impl SyncDutiesMap { +impl SyncDutiesMap { pub fn new(distributed: bool) -> Self { Self { committees: RwLock::new(HashMap::new()), distributed, - _phantom: PhantomData, } } @@ -104,7 +100,7 @@ impl SyncDutiesMap { } /// Number of slots in advance to compute selection proofs - fn aggregation_pre_compute_slots(&self) -> u64 { + fn aggregation_pre_compute_slots(&self) -> u64 { if self.distributed { AGGREGATION_PRE_COMPUTE_SLOTS_DISTRIBUTED } else { @@ -117,7 +113,7 @@ impl SyncDutiesMap { /// 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( + fn prepare_for_aggregator_pre_compute( &self, committee_period: u64, current_slot: Slot, @@ -127,7 +123,7 @@ impl SyncDutiesMap { current_slot, first_slot_of_period::(committee_period, spec), ); - let pre_compute_lookahead_slots = self.aggregation_pre_compute_slots(); + let pre_compute_lookahead_slots = self.aggregation_pre_compute_slots::(); let pre_compute_slot = std::cmp::min( current_slot + pre_compute_lookahead_slots, last_slot_of_period::(committee_period, spec), @@ -187,7 +183,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, @@ -284,16 +280,16 @@ fn last_slot_of_period(sync_committee_period: u64, spec: &ChainSpec) first_slot_of_period::(sync_committee_period + 1, spec) - 1 } -pub async fn poll_sync_committee_duties( - duties_service: &Arc>, -) -> Result<(), Error> { +pub async fn poll_sync_committee_duties( + duties_service: &Arc>, +) -> Result<(), Error> { let sync_duties = &duties_service.sync_duties; let spec = &duties_service.spec; let current_slot = duties_service .slot_clock .now() .ok_or(Error::UnableToReadSlotClock)?; - let current_epoch = current_slot.epoch(E::slots_per_epoch()); + let current_epoch = current_slot.epoch(S::E::slots_per_epoch()); // If the Altair fork is yet to be activated, do not attempt to poll for duties. if spec @@ -317,10 +313,8 @@ pub async fn poll_sync_committee_duties( let local_indices = { let mut local_indices = Vec::with_capacity(local_pubkeys.len()); - let vals_ref = duties_service.validator_store.initialized_validators(); - let vals = vals_ref.read(); for &pubkey in &local_pubkeys { - if let Some(validator_index) = vals.get_index(&pubkey) { + if let Some(validator_index) = duties_service.validator_store.validator_index(&pubkey) { local_indices.push(validator_index) } } @@ -342,11 +336,15 @@ pub async fn poll_sync_committee_duties( // Pre-compute aggregator selection proofs for the current period. let (current_pre_compute_slot, new_pre_compute_duties) = sync_duties - .prepare_for_aggregator_pre_compute(current_sync_committee_period, current_slot, spec); + .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(); - duties_service.context.executor.spawn( + duties_service.executor.spawn( async move { fill_in_aggregation_proofs( sub_duties_service, @@ -379,18 +377,22 @@ pub async fn poll_sync_committee_duties( } // Pre-compute aggregator selection proofs for the next period. - let aggregate_pre_compute_lookahead_slots = sync_duties.aggregation_pre_compute_slots(); + 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()) + .epoch(S::E::slots_per_epoch()) .sync_committee_period(spec)? == next_sync_committee_period { let (pre_compute_slot, new_pre_compute_duties) = sync_duties - .prepare_for_aggregator_pre_compute(next_sync_committee_period, current_slot, spec); + .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(); - duties_service.context.executor.spawn( + duties_service.executor.spawn( async move { fill_in_aggregation_proofs( sub_duties_service, @@ -409,11 +411,11 @@ pub async fn poll_sync_committee_duties( Ok(()) } -pub async fn poll_sync_committee_duties_for_period( - duties_service: &Arc>, +pub async fn poll_sync_committee_duties_for_period( + duties_service: &Arc>, local_indices: &[u64], sync_committee_period: u64, -) -> Result<(), Error> { +) -> Result<(), Error> { let spec = &duties_service.spec; // no local validators don't need to poll for sync committee @@ -496,8 +498,8 @@ pub async fn poll_sync_committee_duties_for_period( - duties_service: Arc>, +pub async fn fill_in_aggregation_proofs( + duties_service: Arc>, pre_compute_duties: &[(Slot, SyncDuty)], sync_committee_period: u64, current_slot: Slot, @@ -519,7 +521,7 @@ pub async fn fill_in_aggregation_proofs( continue; } - let subnet_ids = match duty.subnet_ids::() { + let subnet_ids = match duty.subnet_ids::() { Ok(subnet_ids) => subnet_ids, Err(e) => { crit!( @@ -564,7 +566,7 @@ pub async fn fill_in_aggregation_proofs( } }; - match proof.is_aggregator::() { + match proof.is_aggregator::() { Ok(true) => { debug!( validator_index = duty.validator_index, diff --git a/validator_client/validator_services/src/sync_committee_service.rs b/validator_client/validator_services/src/sync_committee_service.rs index d99c0d3107..be9e2918a4 100644 --- a/validator_client/validator_services/src/sync_committee_service.rs +++ b/validator_client/validator_services/src/sync_committee_service.rs @@ -1,6 +1,5 @@ use crate::duties_service::DutiesService; use beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; -use environment::RuntimeContext; use eth2::types::BlockId; use futures::future::join_all; use futures::future::FutureExt; @@ -10,6 +9,7 @@ use std::collections::HashMap; use std::ops::Deref; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use task_executor::TaskExecutor; use tokio::time::{sleep, sleep_until, Duration, Instant}; use tracing::{debug, error, info, trace, warn}; use types::{ @@ -20,11 +20,11 @@ use validator_store::{Error as ValidatorStoreError, ValidatorStore}; pub const SUBSCRIPTION_LOOKAHEAD_EPOCHS: u64 = 4; -pub struct SyncCommitteeService { - inner: Arc>, +pub struct SyncCommitteeService { + inner: Arc>, } -impl Clone for SyncCommitteeService { +impl Clone for SyncCommitteeService { fn clone(&self) -> Self { Self { inner: self.inner.clone(), @@ -32,33 +32,33 @@ impl Clone for SyncCommitteeService { } } -impl Deref for SyncCommitteeService { - type Target = Inner; +impl Deref for SyncCommitteeService { + type Target = Inner; fn deref(&self) -> &Self::Target { self.inner.deref() } } -pub struct Inner { - duties_service: Arc>, - validator_store: Arc>, +pub struct Inner { + duties_service: Arc>, + validator_store: Arc, slot_clock: T, - beacon_nodes: Arc>, - context: RuntimeContext, + beacon_nodes: Arc>, + executor: TaskExecutor, /// Boolean to track whether the service has posted subscriptions to the BN at least once. /// /// This acts as a latch that fires once upon start-up, and then never again. first_subscription_done: AtomicBool, } -impl SyncCommitteeService { +impl SyncCommitteeService { pub fn new( - duties_service: Arc>, - validator_store: Arc>, + duties_service: Arc>, + validator_store: Arc, slot_clock: T, - beacon_nodes: Arc>, - context: RuntimeContext, + beacon_nodes: Arc>, + executor: TaskExecutor, ) -> Self { Self { inner: Arc::new(Inner { @@ -66,7 +66,7 @@ impl SyncCommitteeService { validator_store, slot_clock, beacon_nodes, - context, + executor, first_subscription_done: AtomicBool::new(false), }), } @@ -80,7 +80,7 @@ impl SyncCommitteeService { .spec .altair_fork_epoch .and_then(|fork_epoch| { - let current_epoch = self.slot_clock.now()?.epoch(E::slots_per_epoch()); + let current_epoch = self.slot_clock.now()?.epoch(S::E::slots_per_epoch()); Some(current_epoch >= fork_epoch) }) .unwrap_or(false) @@ -103,7 +103,7 @@ impl SyncCommitteeService { "Sync committee service started" ); - let executor = self.context.executor.clone(); + let executor = self.executor.clone(); let interval_fut = async move { loop { @@ -156,7 +156,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!("No duties known for slot {}", slot); return Ok(()); @@ -202,7 +202,7 @@ impl SyncCommitteeService { // Spawn one task to publish all of the sync committee signatures. let validator_duties = slot_duties.duties; let service = self.clone(); - self.inner.context.executor.spawn( + self.inner.executor.spawn( async move { service .publish_sync_committee_signatures(slot, block_root, validator_duties) @@ -214,7 +214,7 @@ impl SyncCommitteeService { let aggregators = slot_duties.aggregators; let service = self.clone(); - self.inner.context.executor.spawn( + self.inner.executor.spawn( async move { service .publish_sync_committee_aggregates( @@ -316,7 +316,7 @@ impl SyncCommitteeService { ) { for (subnet_id, subnet_aggregators) in aggregators { let service = self.clone(); - self.inner.context.executor.spawn( + self.inner.executor.spawn( async move { service .publish_sync_committee_aggregate_for_subnet( @@ -354,7 +354,7 @@ impl SyncCommitteeService { }; beacon_node - .get_validator_sync_committee_contribution::(&sync_contribution_data) + .get_validator_sync_committee_contribution(&sync_contribution_data) .await }) .await @@ -440,7 +440,7 @@ impl SyncCommitteeService { fn spawn_subscription_tasks(&self) { let service = self.clone(); - self.inner.context.executor.spawn( + self.inner.executor.spawn( async move { service.publish_subscriptions().await.unwrap_or_else(|e| { error!( @@ -463,10 +463,10 @@ impl SyncCommitteeService { // At the start of every epoch during the current period, re-post the subscriptions // to the beacon node. This covers the case where the BN has forgotten the subscriptions // due to a restart, or where the VC has switched to a fallback BN. - let current_period = sync_period_of_slot::(slot, spec)?; + let current_period = sync_period_of_slot::(slot, spec)?; if !self.first_subscription_done.load(Ordering::Relaxed) - || slot.as_u64() % E::slots_per_epoch() == 0 + || slot.as_u64() % S::E::slots_per_epoch() == 0 { duty_slots.push((slot, current_period)); } @@ -474,9 +474,9 @@ impl SyncCommitteeService { // Near the end of the current period, push subscriptions for the next period to the // beacon node. We aggressively push every slot in the lead-up, as this is the main way // that we want to ensure that the BN is subscribed (well in advance). - let lookahead_slot = slot + SUBSCRIPTION_LOOKAHEAD_EPOCHS * E::slots_per_epoch(); + let lookahead_slot = slot + SUBSCRIPTION_LOOKAHEAD_EPOCHS * S::E::slots_per_epoch(); - let lookahead_period = sync_period_of_slot::(lookahead_slot, spec)?; + let lookahead_period = sync_period_of_slot::(lookahead_slot, spec)?; if lookahead_period > current_period { duty_slots.push((lookahead_slot, lookahead_period)); @@ -494,7 +494,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/validator_store/Cargo.toml b/validator_client/validator_store/Cargo.toml index 1338c2a07e..8c5451b2d0 100644 --- a/validator_client/validator_store/Cargo.toml +++ b/validator_client/validator_store/Cargo.toml @@ -4,21 +4,7 @@ version = "0.1.0" edition = { workspace = true } authors = ["Sigma Prime "] -[lib] -name = "validator_store" -path = "src/lib.rs" - [dependencies] -account_utils = { workspace = true } -doppelganger_service = { workspace = true } -initialized_validators = { workspace = true } -logging = { workspace = true } -parking_lot = { workspace = true } -serde = { workspace = true } -signing_method = { workspace = true } +eth2 = { workspace = true } slashing_protection = { workspace = true } -slot_clock = { workspace = true } -task_executor = { workspace = true } -tracing = { workspace = true } types = { workspace = true } -validator_metrics = { workspace = true } diff --git a/validator_client/validator_store/src/lib.rs b/validator_client/validator_store/src/lib.rs index d1442dcc2f..9183747004 100644 --- a/validator_client/validator_store/src/lib.rs +++ b/validator_client/validator_store/src/lib.rs @@ -1,31 +1,18 @@ -use account_utils::validator_definitions::{PasswordStorage, ValidatorDefinition}; -use doppelganger_service::{DoppelgangerService, DoppelgangerStatus, DoppelgangerValidatorStore}; -use initialized_validators::InitializedValidators; -use logging::crit; -use parking_lot::{Mutex, RwLock}; -use serde::{Deserialize, Serialize}; -use signing_method::{Error as SigningError, SignableMessage, SigningContext, SigningMethod}; -use slashing_protection::{ - interchange::Interchange, InterchangeError, NotSafe, Safe, SlashingDatabase, -}; -use slot_clock::SlotClock; -use std::marker::PhantomData; -use std::path::Path; +use eth2::types::{FullBlockContents, PublishBlockRequest}; +use slashing_protection::NotSafe; +use std::fmt::Debug; +use std::future::Future; use std::sync::Arc; -use task_executor::TaskExecutor; -use tracing::{error, info, warn}; use types::{ - attestation::Error as AttestationError, graffiti::GraffitiString, AbstractExecPayload, Address, - AggregateAndProof, Attestation, BeaconBlock, BlindedPayload, ChainSpec, ContributionAndProof, - Domain, Epoch, EthSpec, Fork, Graffiti, Hash256, InclusionList, PublicKeyBytes, SelectionProof, - Signature, SignedAggregateAndProof, SignedBeaconBlock, SignedContributionAndProof, - SignedInclusionList, SignedRoot, SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, - SyncAggregatorSelectionData, SyncCommitteeContribution, SyncCommitteeMessage, - SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, VoluntaryExit, + Address, Attestation, AttestationError, BlindedBeaconBlock, Epoch, EthSpec, Graffiti, Hash256, + InclusionList, PublicKeyBytes, SelectionProof, Signature, SignedAggregateAndProof, + SignedBlindedBeaconBlock, SignedContributionAndProof, SignedInclusionList, + SignedValidatorRegistrationData, Slot, SyncCommitteeContribution, SyncCommitteeMessage, + SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, }; -#[derive(Debug, PartialEq)] -pub enum Error { +#[derive(Debug, PartialEq, Clone)] +pub enum Error { DoppelgangerProtected(PublicKeyBytes), UnknownToDoppelgangerService(PublicKeyBytes), UnknownPubkey(PublicKeyBytes), @@ -34,31 +21,15 @@ pub enum Error { GreaterThanCurrentSlot { slot: Slot, current_slot: Slot }, GreaterThanCurrentEpoch { epoch: Epoch, current_epoch: Epoch }, UnableToSignAttestation(AttestationError), - UnableToSign(SigningError), + SpecificError(T), } -impl From for Error { - fn from(e: SigningError) -> Self { - Error::UnableToSign(e) +impl From for Error { + fn from(e: T) -> Self { + Error::SpecificError(e) } } -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct Config { - /// Fallback fee recipient address. - pub fee_recipient: Option
, - /// Fallback gas limit. - pub gas_limit: Option, - /// Enable use of the blinded block endpoints during proposals. - pub builder_proposals: bool, - /// Enable slashing protection even while using web3signer keys. - pub enable_web3signer_slashing_protection: bool, - /// If true, Lighthouse will prefer builder proposals, if available. - pub prefer_builder_proposals: bool, - /// Specifies the boost factor, a percentage multiplier to apply to the builder's payload value. - pub builder_boost_factor: Option, -} - /// A helper struct, used for passing data from the validator store to services. pub struct ProposalData { pub validator_index: Option, @@ -67,185 +38,9 @@ pub struct ProposalData { pub builder_proposals: bool, } -/// Number of epochs of slashing protection history to keep. -/// -/// This acts as a maximum safe-guard against clock drift. -const SLASHING_PROTECTION_HISTORY_EPOCHS: u64 = 512; - -/// Currently used as the default gas limit in execution clients. -/// -/// https://github.com/ethereum/builder-specs/issues/17 -pub const DEFAULT_GAS_LIMIT: u64 = 30_000_000; - -pub struct ValidatorStore { - validators: Arc>, - slashing_protection: SlashingDatabase, - slashing_protection_last_prune: Arc>, - genesis_validators_root: Hash256, - spec: Arc, - doppelganger_service: Option>, - slot_clock: T, - fee_recipient_process: Option
, - gas_limit: Option, - builder_proposals: bool, - enable_web3signer_slashing_protection: bool, - prefer_builder_proposals: bool, - builder_boost_factor: Option, - task_executor: TaskExecutor, - _phantom: PhantomData, -} - -impl DoppelgangerValidatorStore for ValidatorStore { - fn get_validator_index(&self, pubkey: &PublicKeyBytes) -> Option { - self.validator_index(pubkey) - } -} - -impl ValidatorStore { - // All arguments are different types. Making the fields `pub` is undesired. A builder seems - // unnecessary. - #[allow(clippy::too_many_arguments)] - pub fn new( - validators: InitializedValidators, - slashing_protection: SlashingDatabase, - genesis_validators_root: Hash256, - spec: Arc, - doppelganger_service: Option>, - slot_clock: T, - config: &Config, - task_executor: TaskExecutor, - ) -> Self { - Self { - validators: Arc::new(RwLock::new(validators)), - slashing_protection, - slashing_protection_last_prune: Arc::new(Mutex::new(Epoch::new(0))), - genesis_validators_root, - spec, - doppelganger_service, - slot_clock, - 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, - prefer_builder_proposals: config.prefer_builder_proposals, - builder_boost_factor: config.builder_boost_factor, - task_executor, - _phantom: PhantomData, - } - } - - /// Register all local validators in doppelganger protection to try and prevent instances of - /// duplicate validators operating on the network at the same time. - /// - /// This function has no effect if doppelganger protection is disabled. - pub fn register_all_in_doppelganger_protection_if_enabled(&self) -> Result<(), String> { - if let Some(doppelganger_service) = &self.doppelganger_service { - for pubkey in self.validators.read().iter_voting_pubkeys() { - doppelganger_service.register_new_validator::(*pubkey, &self.slot_clock)? - } - } - - Ok(()) - } - - /// Returns `true` if doppelganger protection is enabled, or else `false`. - pub fn doppelganger_protection_enabled(&self) -> bool { - self.doppelganger_service.is_some() - } - - pub fn initialized_validators(&self) -> Arc> { - self.validators.clone() - } - - /// Indicates if the `voting_public_key` exists in self and is enabled. - pub fn has_validator(&self, voting_public_key: &PublicKeyBytes) -> bool { - self.validators - .read() - .validator(voting_public_key) - .is_some() - } - - /// Insert a new validator to `self`, where the validator is represented by an EIP-2335 - /// keystore on the filesystem. - #[allow(clippy::too_many_arguments)] - pub async fn add_validator_keystore>( - &self, - voting_keystore_path: P, - password_storage: PasswordStorage, - enable: bool, - graffiti: Option, - suggested_fee_recipient: Option
, - gas_limit: Option, - builder_proposals: Option, - builder_boost_factor: Option, - prefer_builder_proposals: Option, - ) -> Result { - let mut validator_def = ValidatorDefinition::new_keystore_with_password( - voting_keystore_path, - password_storage, - graffiti, - suggested_fee_recipient, - gas_limit, - builder_proposals, - builder_boost_factor, - prefer_builder_proposals, - ) - .map_err(|e| format!("failed to create validator definitions: {:?}", e))?; - - validator_def.enabled = enable; - - self.add_validator(validator_def).await - } - - /// Insert a new validator to `self`. - /// - /// This function includes: - /// - /// - Adding the validator definition to the YAML file, saving it to the filesystem. - /// - Enabling the validator with the slashing protection database. - /// - If `enable == true`, starting to perform duties for the validator. - // FIXME: ignore this clippy lint until the validator store is refactored to use async locks - #[allow(clippy::await_holding_lock)] - pub async fn add_validator( - &self, - validator_def: ValidatorDefinition, - ) -> Result { - let validator_pubkey = validator_def.voting_public_key.compress(); - - self.slashing_protection - .register_validator(validator_pubkey) - .map_err(|e| format!("failed to register validator: {:?}", e))?; - - if let Some(doppelganger_service) = &self.doppelganger_service { - doppelganger_service - .register_new_validator::(validator_pubkey, &self.slot_clock)?; - } - - self.validators - .write() - .add_definition_replace_disabled(validator_def.clone()) - .await - .map_err(|e| format!("Unable to add definition: {:?}", e))?; - - Ok(validator_def) - } - - /// Returns `ProposalData` for the provided `pubkey` if it exists in `InitializedValidators`. - /// `ProposalData` fields include defaulting logic described in `get_fee_recipient_defaulting`, - /// `get_gas_limit_defaulting`, and `get_builder_proposals_defaulting`. - pub fn proposal_data(&self, pubkey: &PublicKeyBytes) -> Option { - self.validators - .read() - .validator(pubkey) - .map(|validator| ProposalData { - validator_index: validator.get_index(), - fee_recipient: self - .get_fee_recipient_defaulting(validator.get_suggested_fee_recipient()), - gas_limit: self.get_gas_limit_defaulting(validator.get_gas_limit()), - builder_proposals: self - .get_builder_proposals_defaulting(validator.get_builder_proposals()), - }) - } +pub trait ValidatorStore: Send + Sync { + type Error: Debug + Send + Sync; + type E: EthSpec; /// Attempts to resolve the pubkey to a validator index. /// @@ -253,9 +48,7 @@ impl ValidatorStore { /// /// - Unknown. /// - Known, but with an unknown index. - pub fn validator_index(&self, pubkey: &PublicKeyBytes) -> Option { - self.validators.read().get_index(pubkey) - } + fn validator_index(&self, pubkey: &PublicKeyBytes) -> Option; /// Returns all voting pubkeys for all enabled validators. /// @@ -266,255 +59,25 @@ impl ValidatorStore { /// protection and are safe-enough to sign messages. /// - `DoppelgangerStatus::ignored`: returns all the pubkeys from `only_safe` *plus* those still /// undergoing protection. This is useful for collecting duties or other non-signing tasks. - #[allow(clippy::needless_collect)] // Collect is required to avoid holding a lock. - pub fn voting_pubkeys(&self, filter_func: F) -> I + fn voting_pubkeys(&self, filter_func: F) -> I where I: FromIterator, - F: Fn(DoppelgangerStatus) -> Option, - { - // Collect all the pubkeys first to avoid interleaving locks on `self.validators` and - // `self.doppelganger_service()`. - let pubkeys = self - .validators - .read() - .iter_voting_pubkeys() - .cloned() - .collect::>(); - - pubkeys - .into_iter() - .map(|pubkey| { - self.doppelganger_service - .as_ref() - .map(|doppelganger_service| doppelganger_service.validator_status(pubkey)) - // Allow signing on all pubkeys if doppelganger protection is disabled. - .unwrap_or_else(|| DoppelgangerStatus::SigningEnabled(pubkey)) - }) - .filter_map(filter_func) - .collect() - } - - /// Returns doppelganger statuses for all enabled validators. - #[allow(clippy::needless_collect)] // Collect is required to avoid holding a lock. - pub fn doppelganger_statuses(&self) -> Vec { - // Collect all the pubkeys first to avoid interleaving locks on `self.validators` and - // `self.doppelganger_service`. - let pubkeys = self - .validators - .read() - .iter_voting_pubkeys() - .cloned() - .collect::>(); - - pubkeys - .into_iter() - .map(|pubkey| { - self.doppelganger_service - .as_ref() - .map(|doppelganger_service| doppelganger_service.validator_status(pubkey)) - // Allow signing on all pubkeys if doppelganger protection is disabled. - .unwrap_or_else(|| DoppelgangerStatus::SigningEnabled(pubkey)) - }) - .collect() - } + F: Fn(DoppelgangerStatus) -> Option; /// Check if the `validator_pubkey` is permitted by the doppleganger protection to sign /// messages. - pub fn doppelganger_protection_allows_signing(&self, validator_pubkey: PublicKeyBytes) -> bool { - self.doppelganger_service - .as_ref() - // If there's no doppelganger service then we assume it is purposefully disabled and - // declare that all keys are safe with regard to it. - .is_none_or(|doppelganger_service| { - doppelganger_service - .validator_status(validator_pubkey) - .only_safe() - .is_some() - }) - } + fn doppelganger_protection_allows_signing(&self, validator_pubkey: PublicKeyBytes) -> bool; - pub fn num_voting_validators(&self) -> usize { - self.validators.read().num_enabled() - } - - fn fork(&self, epoch: Epoch) -> Fork { - self.spec.fork_at_epoch(epoch) - } - - /// Returns a `SigningMethod` for `validator_pubkey` *only if* that validator is considered safe - /// by doppelganger protection. - fn doppelganger_checked_signing_method( - &self, - validator_pubkey: PublicKeyBytes, - ) -> Result, Error> { - if self.doppelganger_protection_allows_signing(validator_pubkey) { - self.validators - .read() - .signing_method(&validator_pubkey) - .ok_or(Error::UnknownPubkey(validator_pubkey)) - } else { - Err(Error::DoppelgangerProtected(validator_pubkey)) - } - } - - /// Returns a `SigningMethod` for `validator_pubkey` regardless of that validators doppelganger - /// protection status. - /// - /// ## Warning - /// - /// This method should only be used for signing non-slashable messages. - fn doppelganger_bypassed_signing_method( - &self, - validator_pubkey: PublicKeyBytes, - ) -> Result, Error> { - self.validators - .read() - .signing_method(&validator_pubkey) - .ok_or(Error::UnknownPubkey(validator_pubkey)) - } - - fn signing_context(&self, domain: Domain, signing_epoch: Epoch) -> SigningContext { - if domain == Domain::VoluntaryExit { - if self.spec.fork_name_at_epoch(signing_epoch).deneb_enabled() { - // EIP-7044 - SigningContext { - domain, - epoch: signing_epoch, - fork: Fork { - previous_version: self.spec.capella_fork_version, - current_version: self.spec.capella_fork_version, - epoch: signing_epoch, - }, - genesis_validators_root: self.genesis_validators_root, - } - } else { - SigningContext { - domain, - epoch: signing_epoch, - fork: self.fork(signing_epoch), - genesis_validators_root: self.genesis_validators_root, - } - } - } else { - SigningContext { - domain, - epoch: signing_epoch, - fork: self.fork(signing_epoch), - genesis_validators_root: self.genesis_validators_root, - } - } - } - - pub async fn randao_reveal( - &self, - validator_pubkey: PublicKeyBytes, - signing_epoch: Epoch, - ) -> Result { - let signing_method = self.doppelganger_checked_signing_method(validator_pubkey)?; - let signing_context = self.signing_context(Domain::Randao, signing_epoch); - - let signature = signing_method - .get_signature::>( - SignableMessage::RandaoReveal(signing_epoch), - signing_context, - &self.spec, - &self.task_executor, - ) - .await?; - - Ok(signature) - } - - pub fn graffiti(&self, validator_pubkey: &PublicKeyBytes) -> Option { - self.validators.read().graffiti(validator_pubkey) - } + fn num_voting_validators(&self) -> usize; + fn graffiti(&self, validator_pubkey: &PublicKeyBytes) -> Option; /// Returns the fee recipient for the given public key. The priority order for fetching /// the fee recipient is: /// 1. validator_definitions.yml /// 2. process level fee recipient - pub fn get_fee_recipient(&self, validator_pubkey: &PublicKeyBytes) -> Option
{ - // If there is a `suggested_fee_recipient` in the validator definitions yaml - // file, use that value. - self.get_fee_recipient_defaulting(self.suggested_fee_recipient(validator_pubkey)) - } + fn get_fee_recipient(&self, validator_pubkey: &PublicKeyBytes) -> Option
; - pub fn get_fee_recipient_defaulting(&self, fee_recipient: Option
) -> Option
{ - // If there's nothing in the file, try the process-level default value. - fee_recipient.or(self.fee_recipient_process) - } - - /// Returns the suggested_fee_recipient from `validator_definitions.yml` if any. - /// This has been pulled into a private function so the read lock is dropped easily - fn suggested_fee_recipient(&self, validator_pubkey: &PublicKeyBytes) -> Option
{ - self.validators - .read() - .suggested_fee_recipient(validator_pubkey) - } - - /// Returns the gas limit for the given public key. The priority order for fetching - /// the gas limit is: - /// - /// 1. validator_definitions.yml - /// 2. process level gas limit - /// 3. `DEFAULT_GAS_LIMIT` - pub fn get_gas_limit(&self, validator_pubkey: &PublicKeyBytes) -> u64 { - self.get_gas_limit_defaulting(self.validators.read().gas_limit(validator_pubkey)) - } - - fn get_gas_limit_defaulting(&self, gas_limit: Option) -> u64 { - // If there is a `gas_limit` in the validator definitions yaml - // file, use that value. - gas_limit - // If there's nothing in the file, try the process-level default value. - .or(self.gas_limit) - // If there's no process-level default, use the `DEFAULT_GAS_LIMIT`. - .unwrap_or(DEFAULT_GAS_LIMIT) - } - - /// Returns a `bool` for the given public key that denotes whether this validator should use the - /// builder API. The priority order for fetching this value is: - /// - /// 1. validator_definitions.yml - /// 2. process level flag - pub fn get_builder_proposals(&self, validator_pubkey: &PublicKeyBytes) -> bool { - // If there is a `suggested_fee_recipient` in the validator definitions yaml - // file, use that value. - self.get_builder_proposals_defaulting( - self.validators.read().builder_proposals(validator_pubkey), - ) - } - - /// Returns a `u64` for the given public key that denotes the builder boost factor. The priority order for fetching this value is: - /// - /// 1. validator_definitions.yml - /// 2. process level flag - pub fn get_builder_boost_factor(&self, validator_pubkey: &PublicKeyBytes) -> Option { - self.validators - .read() - .builder_boost_factor(validator_pubkey) - .or(self.builder_boost_factor) - } - - /// Returns a `bool` for the given public key that denotes whether this validator should prefer a - /// builder payload. The priority order for fetching this value is: - /// - /// 1. validator_definitions.yml - /// 2. process level flag - pub fn get_prefer_builder_proposals(&self, validator_pubkey: &PublicKeyBytes) -> bool { - self.validators - .read() - .prefer_builder_proposals(validator_pubkey) - .unwrap_or(self.prefer_builder_proposals) - } - - fn get_builder_proposals_defaulting(&self, builder_proposals: Option) -> bool { - builder_proposals - // If there's nothing in the file, try the process-level default value. - .unwrap_or(self.builder_proposals) - } - - /// Translate the per validator `builder_proposals`, `builder_boost_factor` and + /// Translate the `builder_proposals`, `builder_boost_factor` and /// `prefer_builder_proposals` to a boost factor, if available. /// - If `prefer_builder_proposals` is true, set boost factor to `u64::MAX` to indicate a /// preference for builder payloads. @@ -522,600 +85,169 @@ impl ValidatorStore { /// - If `builder_proposals` is set to false, set boost factor to 0 to indicate a preference for /// local payloads. /// - Else return `None` to indicate no preference between builder and local payloads. - pub fn determine_validator_builder_boost_factor( - &self, - validator_pubkey: &PublicKeyBytes, - ) -> Option { - let validator_prefer_builder_proposals = self - .validators - .read() - .prefer_builder_proposals(validator_pubkey); + fn determine_builder_boost_factor(&self, validator_pubkey: &PublicKeyBytes) -> Option; - if matches!(validator_prefer_builder_proposals, Some(true)) { - return Some(u64::MAX); - } - - self.validators - .read() - .builder_boost_factor(validator_pubkey) - .or_else(|| { - if matches!( - self.validators.read().builder_proposals(validator_pubkey), - Some(false) - ) { - return Some(0); - } - None - }) - } - - /// Translate the process-wide `builder_proposals`, `builder_boost_factor` and - /// `prefer_builder_proposals` configurations to a boost factor. - /// - If `prefer_builder_proposals` is true, set boost factor to `u64::MAX` to indicate a - /// preference for builder payloads. - /// - If `builder_boost_factor` is a value other than None, return its value as the boost factor. - /// - If `builder_proposals` is set to false, set boost factor to 0 to indicate a preference for - /// local payloads. - /// - Else return `None` to indicate no preference between builder and local payloads. - pub fn determine_default_builder_boost_factor(&self) -> Option { - if self.prefer_builder_proposals { - return Some(u64::MAX); - } - self.builder_boost_factor.or({ - if !self.builder_proposals { - Some(0) - } else { - None - } - }) - } - - pub async fn sign_block>( + fn randao_reveal( &self, validator_pubkey: PublicKeyBytes, - block: BeaconBlock, + signing_epoch: Epoch, + ) -> impl Future>> + Send; + + fn set_validator_index(&self, validator_pubkey: &PublicKeyBytes, index: u64); + + fn sign_block( + &self, + validator_pubkey: PublicKeyBytes, + block: UnsignedBlock, current_slot: Slot, - ) -> Result, Error> { - // Make sure the block slot is not higher than the current slot to avoid potential attacks. - if block.slot() > current_slot { - warn!( - block_slot = block.slot().as_u64(), - current_slot = current_slot.as_u64(), - "Not signing block with slot greater than current slot" - ); - return Err(Error::GreaterThanCurrentSlot { - slot: block.slot(), - current_slot, - }); - } + ) -> impl Future, Error>> + Send; - let signing_epoch = block.epoch(); - 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 = 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) => { - validator_metrics::inc_counter_vec( - &validator_metrics::SIGNED_BLOCKS_TOTAL, - &[validator_metrics::SUCCESS], - ); - - let signature = signing_method - .get_signature::( - SignableMessage::BeaconBlock(&block), - signing_context, - &self.spec, - &self.task_executor, - ) - .await?; - Ok(SignedBeaconBlock::from_block(block, signature)) - } - Ok(Safe::SameData) => { - warn!("Skipping signing of previously signed block"); - validator_metrics::inc_counter_vec( - &validator_metrics::SIGNED_BLOCKS_TOTAL, - &[validator_metrics::SAME_DATA], - ); - Err(Error::SameData) - } - Err(NotSafe::UnregisteredValidator(pk)) => { - warn!( - msg = "Carefully consider running with --init-slashing-protection (see --help)", - public_key = format!("{:?}", pk), - "Not signing block for unregistered validator" - ); - validator_metrics::inc_counter_vec( - &validator_metrics::SIGNED_BLOCKS_TOTAL, - &[validator_metrics::UNREGISTERED], - ); - Err(Error::Slashable(NotSafe::UnregisteredValidator(pk))) - } - Err(e) => { - crit!(error = format!("{:?}", e), "Not signing slashable block"); - validator_metrics::inc_counter_vec( - &validator_metrics::SIGNED_BLOCKS_TOTAL, - &[validator_metrics::SLASHABLE], - ); - Err(Error::Slashable(e)) - } - } - } - - pub async fn sign_attestation( + fn sign_attestation( &self, validator_pubkey: PublicKeyBytes, validator_committee_position: usize, - attestation: &mut Attestation, + attestation: &mut Attestation, current_epoch: Epoch, - ) -> Result<(), Error> { - // Make sure the target epoch is not higher than the current epoch to avoid potential attacks. - if attestation.data().target.epoch > current_epoch { - return Err(Error::GreaterThanCurrentEpoch { - epoch: attestation.data().target.epoch, - current_epoch, - }); - } + ) -> impl Future>> + Send; - // 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 = 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 signature = signing_method - .get_signature::>( - SignableMessage::AttestationData(attestation.data()), - signing_context, - &self.spec, - &self.task_executor, - ) - .await?; - attestation - .add_signature(&signature, validator_committee_position) - .map_err(Error::UnableToSignAttestation)?; - - validator_metrics::inc_counter_vec( - &validator_metrics::SIGNED_ATTESTATIONS_TOTAL, - &[validator_metrics::SUCCESS], - ); - - Ok(()) - } - Ok(Safe::SameData) => { - warn!("Skipping signing of previously signed attestation"); - validator_metrics::inc_counter_vec( - &validator_metrics::SIGNED_ATTESTATIONS_TOTAL, - &[validator_metrics::SAME_DATA], - ); - Err(Error::SameData) - } - Err(NotSafe::UnregisteredValidator(pk)) => { - warn!( - msg = "Carefully consider running with --init-slashing-protection (see --help)", - public_key = format!("{:?}", pk), - "Not signing attestation for unregistered validator" - ); - validator_metrics::inc_counter_vec( - &validator_metrics::SIGNED_ATTESTATIONS_TOTAL, - &[validator_metrics::UNREGISTERED], - ); - Err(Error::Slashable(NotSafe::UnregisteredValidator(pk))) - } - Err(e) => { - crit!( - attestation = format!("{:?}", attestation.data()), - error = format!("{:?}", e), - "Not signing slashable attestation" - ); - validator_metrics::inc_counter_vec( - &validator_metrics::SIGNED_ATTESTATIONS_TOTAL, - &[validator_metrics::SLASHABLE], - ); - Err(Error::Slashable(e)) - } - } - } - - pub async fn sign_voluntary_exit( - &self, - validator_pubkey: PublicKeyBytes, - voluntary_exit: VoluntaryExit, - ) -> Result { - let signing_epoch = voluntary_exit.epoch; - let signing_context = self.signing_context(Domain::VoluntaryExit, signing_epoch); - let signing_method = self.doppelganger_bypassed_signing_method(validator_pubkey)?; - - let signature = signing_method - .get_signature::>( - SignableMessage::VoluntaryExit(&voluntary_exit), - signing_context, - &self.spec, - &self.task_executor, - ) - .await?; - - validator_metrics::inc_counter_vec( - &validator_metrics::SIGNED_VOLUNTARY_EXITS_TOTAL, - &[validator_metrics::SUCCESS], - ); - - Ok(SignedVoluntaryExit { - message: voluntary_exit, - signature, - }) - } - - pub async fn sign_validator_registration_data( + fn sign_validator_registration_data( &self, validator_registration_data: ValidatorRegistrationData, - ) -> Result { - let domain_hash = self.spec.get_builder_domain(); - let signing_root = validator_registration_data.signing_root(domain_hash); - - let signing_method = - self.doppelganger_bypassed_signing_method(validator_registration_data.pubkey)?; - let signature = signing_method - .get_signature_from_root::>( - SignableMessage::ValidatorRegistration(&validator_registration_data), - signing_root, - &self.task_executor, - None, - ) - .await?; - - validator_metrics::inc_counter_vec( - &validator_metrics::SIGNED_VALIDATOR_REGISTRATIONS_TOTAL, - &[validator_metrics::SUCCESS], - ); - - Ok(SignedValidatorRegistrationData { - message: validator_registration_data, - signature, - }) - } + ) -> impl Future>> + Send; /// Signs an `AggregateAndProof` for a given validator. /// /// The resulting `SignedAggregateAndProof` is sent on the aggregation channel and cannot be /// modified by actors other than the signing validator. - pub async fn produce_signed_aggregate_and_proof( + fn produce_signed_aggregate_and_proof( &self, validator_pubkey: PublicKeyBytes, aggregator_index: u64, - aggregate: Attestation, + aggregate: Attestation, selection_proof: SelectionProof, - ) -> Result, Error> { - let signing_epoch = aggregate.data().target.epoch; - let signing_context = self.signing_context(Domain::AggregateAndProof, signing_epoch); - - let message = - AggregateAndProof::from_attestation(aggregator_index, aggregate, selection_proof); - - let signing_method = self.doppelganger_checked_signing_method(validator_pubkey)?; - let signature = signing_method - .get_signature::>( - SignableMessage::SignedAggregateAndProof(message.to_ref()), - signing_context, - &self.spec, - &self.task_executor, - ) - .await?; - - validator_metrics::inc_counter_vec( - &validator_metrics::SIGNED_AGGREGATES_TOTAL, - &[validator_metrics::SUCCESS], - ); - - Ok(SignedAggregateAndProof::from_aggregate_and_proof( - message, signature, - )) - } + ) -> impl Future, Error>> + Send; /// Produces a `SelectionProof` for the `slot`, signed by with corresponding secret key to /// `validator_pubkey`. - pub async fn produce_selection_proof( + fn produce_selection_proof( &self, validator_pubkey: PublicKeyBytes, slot: Slot, - ) -> Result { - let signing_epoch = slot.epoch(E::slots_per_epoch()); - let signing_context = self.signing_context(Domain::SelectionProof, signing_epoch); - - // Bypass the `with_validator_signing_method` function. - // - // This is because we don't care about doppelganger protection when it comes to selection - // proofs. They are not slashable and we need them to subscribe to subnets on the BN. - // - // As long as we disallow `SignedAggregateAndProof` then these selection proofs will never - // be published on the network. - let signing_method = self.doppelganger_bypassed_signing_method(validator_pubkey)?; - - let signature = signing_method - .get_signature::>( - SignableMessage::SelectionProof(slot), - signing_context, - &self.spec, - &self.task_executor, - ) - .await - .map_err(Error::UnableToSign)?; - - validator_metrics::inc_counter_vec( - &validator_metrics::SIGNED_SELECTION_PROOFS_TOTAL, - &[validator_metrics::SUCCESS], - ); - - Ok(signature.into()) - } + ) -> impl Future>> + Send; /// Produce a `SyncSelectionProof` for `slot` signed by the secret key of `validator_pubkey`. - pub async fn produce_sync_selection_proof( + fn produce_sync_selection_proof( &self, validator_pubkey: &PublicKeyBytes, slot: Slot, subnet_id: SyncSubnetId, - ) -> Result { - let signing_epoch = slot.epoch(E::slots_per_epoch()); - let signing_context = - self.signing_context(Domain::SyncCommitteeSelectionProof, signing_epoch); + ) -> impl Future>> + Send; - // Bypass `with_validator_signing_method`: sync committee messages are not slashable. - let signing_method = self.doppelganger_bypassed_signing_method(*validator_pubkey)?; - - validator_metrics::inc_counter_vec( - &validator_metrics::SIGNED_SYNC_SELECTION_PROOFS_TOTAL, - &[validator_metrics::SUCCESS], - ); - - let message = SyncAggregatorSelectionData { - slot, - subcommittee_index: subnet_id.into(), - }; - - let signature = signing_method - .get_signature::>( - SignableMessage::SyncSelectionProof(&message), - signing_context, - &self.spec, - &self.task_executor, - ) - .await - .map_err(Error::UnableToSign)?; - - Ok(signature.into()) - } - - pub async fn produce_sync_committee_signature( + fn produce_sync_committee_signature( &self, slot: Slot, beacon_block_root: Hash256, validator_index: u64, validator_pubkey: &PublicKeyBytes, - ) -> Result { - let signing_epoch = slot.epoch(E::slots_per_epoch()); - let signing_context = self.signing_context(Domain::SyncCommittee, signing_epoch); + ) -> impl Future>> + Send; - // Bypass `with_validator_signing_method`: sync committee messages are not slashable. - let signing_method = self.doppelganger_bypassed_signing_method(*validator_pubkey)?; - - let signature = signing_method - .get_signature::>( - SignableMessage::SyncCommitteeSignature { - beacon_block_root, - slot, - }, - signing_context, - &self.spec, - &self.task_executor, - ) - .await - .map_err(Error::UnableToSign)?; - - validator_metrics::inc_counter_vec( - &validator_metrics::SIGNED_SYNC_COMMITTEE_MESSAGES_TOTAL, - &[validator_metrics::SUCCESS], - ); - - Ok(SyncCommitteeMessage { - slot, - beacon_block_root, - validator_index, - signature, - }) - } - - pub async fn produce_signed_contribution_and_proof( + fn produce_signed_contribution_and_proof( &self, aggregator_index: u64, aggregator_pubkey: PublicKeyBytes, - contribution: SyncCommitteeContribution, + contribution: SyncCommitteeContribution, selection_proof: SyncSelectionProof, - ) -> Result, Error> { - let signing_epoch = contribution.slot.epoch(E::slots_per_epoch()); - let signing_context = self.signing_context(Domain::ContributionAndProof, signing_epoch); + ) -> impl Future, Error>> + Send; - // Bypass `with_validator_signing_method`: sync committee messages are not slashable. - let signing_method = self.doppelganger_bypassed_signing_method(aggregator_pubkey)?; - - let message = ContributionAndProof { - aggregator_index, - contribution, - selection_proof: selection_proof.into(), - }; - - let signature = signing_method - .get_signature::>( - SignableMessage::SignedContributionAndProof(&message), - signing_context, - &self.spec, - &self.task_executor, - ) - .await - .map_err(Error::UnableToSign)?; - - validator_metrics::inc_counter_vec( - &validator_metrics::SIGNED_SYNC_COMMITTEE_CONTRIBUTIONS_TOTAL, - &[validator_metrics::SUCCESS], - ); - - Ok(SignedContributionAndProof { message, signature }) - } - - pub async fn sign_inclusion_list( + fn sign_inclusion_list( &self, pubkey: PublicKeyBytes, - inclusion_list: InclusionList, - ) -> Result, Error> { - let signing_epoch = inclusion_list.slot.epoch(E::slots_per_epoch()); - let signing_context = self.signing_context(Domain::InclusionListCommittee, signing_epoch); - let signing_method = self.doppelganger_bypassed_signing_method(pubkey)?; - - let signature = signing_method - .get_signature::>( - SignableMessage::InclusionList(&inclusion_list), - signing_context, - &self.spec, - &self.task_executor, - ) - .await?; - - Ok(SignedInclusionList { - message: inclusion_list, - signature, - }) - } - - pub fn import_slashing_protection( - &self, - interchange: Interchange, - ) -> Result<(), InterchangeError> { - self.slashing_protection - .import_interchange_info(interchange, self.genesis_validators_root)?; - Ok(()) - } - - /// Export slashing protection data while also disabling the given keys in the database. - /// - /// If any key is unknown to the slashing protection database it will be silently omitted - /// from the result. It is the caller's responsibility to check whether all keys provided - /// had data returned for them. - pub fn export_slashing_protection_for_keys( - &self, - pubkeys: &[PublicKeyBytes], - ) -> Result { - self.slashing_protection.with_transaction(|txn| { - let known_pubkeys = pubkeys - .iter() - .filter_map(|pubkey| { - let validator_id = self - .slashing_protection - .get_validator_id_ignoring_status(txn, pubkey) - .ok()?; - - Some( - self.slashing_protection - .update_validator_status(txn, validator_id, false) - .map(|()| *pubkey), - ) - }) - .collect::, _>>()?; - self.slashing_protection.export_interchange_info_in_txn( - self.genesis_validators_root, - Some(&known_pubkeys), - txn, - ) - }) - } + inclusion_list: InclusionList, + ) -> impl Future, Error>> + Send; /// Prune the slashing protection database so that it remains performant. /// /// This function will only do actual pruning periodically, so it should usually be /// cheap to call. The `first_run` flag can be used to print a more verbose message when pruning /// runs. - pub fn prune_slashing_protection_db(&self, current_epoch: Epoch, first_run: bool) { - // Attempt to prune every SLASHING_PROTECTION_HISTORY_EPOCHs, with a tolerance for - // missing the epoch that aligns exactly. - let mut last_prune = self.slashing_protection_last_prune.lock(); - if current_epoch / SLASHING_PROTECTION_HISTORY_EPOCHS - <= *last_prune / SLASHING_PROTECTION_HISTORY_EPOCHS - { - return; + fn prune_slashing_protection_db(&self, current_epoch: Epoch, first_run: bool); + + /// Returns `ProposalData` for the provided `pubkey` if it exists in `InitializedValidators`. + /// `ProposalData` fields include defaulting logic described in `get_fee_recipient_defaulting`, + /// `get_gas_limit_defaulting`, and `get_builder_proposals_defaulting`. + fn proposal_data(&self, pubkey: &PublicKeyBytes) -> Option; +} + +#[derive(Debug)] +pub enum UnsignedBlock { + Full(FullBlockContents), + Blinded(BlindedBeaconBlock), +} + +#[derive(Clone, Debug, PartialEq)] +pub enum SignedBlock { + Full(PublishBlockRequest), + Blinded(Arc>), +} + +/// A wrapper around `PublicKeyBytes` which encodes information about the status of a validator +/// pubkey with regards to doppelganger protection. +#[derive(Debug, PartialEq)] +pub enum DoppelgangerStatus { + /// Doppelganger protection has approved this for signing. + /// + /// This is because the service has waited some period of time to + /// detect other instances of this key on the network. + SigningEnabled(PublicKeyBytes), + /// Doppelganger protection is still waiting to detect other instances. + /// + /// Do not use this pubkey for signing slashable messages!! + /// + /// However, it can safely be used for other non-slashable operations (e.g., collecting duties + /// or subscribing to subnets). + SigningDisabled(PublicKeyBytes), + /// This pubkey is unknown to the doppelganger service. + /// + /// This represents a serious internal error in the program. This validator will be permanently + /// disabled! + UnknownToDoppelganger(PublicKeyBytes), +} + +impl DoppelgangerStatus { + /// Only return a pubkey if it is explicitly safe for doppelganger protection. + /// + /// If `Some(pubkey)` is returned, doppelganger has declared it safe for signing. + /// + /// ## Note + /// + /// "Safe" is only best-effort by doppelganger. There is no guarantee that a doppelganger + /// doesn't exist. + pub fn only_safe(self) -> Option { + match self { + DoppelgangerStatus::SigningEnabled(pubkey) => Some(pubkey), + DoppelgangerStatus::SigningDisabled(_) => None, + DoppelgangerStatus::UnknownToDoppelganger(_) => None, } + } - if first_run { - info!( - epoch = %current_epoch, - msg = "pruning may take several minutes the first time it runs", - "Pruning slashing protection DB" - ); - } else { - info!(epoch = %current_epoch, "Pruning slashing protection DB"); + /// Returns a key regardless of whether or not doppelganger has approved it. Such a key might be + /// used for signing non-slashable messages, duties collection or other activities. + /// + /// If the validator is unknown to doppelganger then `None` will be returned. + pub fn ignored(self) -> Option { + match self { + DoppelgangerStatus::SigningEnabled(pubkey) => Some(pubkey), + DoppelgangerStatus::SigningDisabled(pubkey) => Some(pubkey), + DoppelgangerStatus::UnknownToDoppelganger(_) => None, } + } - let _timer = - validator_metrics::start_timer(&validator_metrics::SLASHING_PROTECTION_PRUNE_TIMES); - - let new_min_target_epoch = current_epoch.saturating_sub(SLASHING_PROTECTION_HISTORY_EPOCHS); - let new_min_slot = new_min_target_epoch.start_slot(E::slots_per_epoch()); - - let all_pubkeys: Vec<_> = self.voting_pubkeys(DoppelgangerStatus::ignored); - - if let Err(e) = self - .slashing_protection - .prune_all_signed_attestations(all_pubkeys.iter(), new_min_target_epoch) - { - error!( - error = ?e, - "Error during pruning of signed attestations" - ); - return; + /// Only return a pubkey if it will not be used for signing due to doppelganger detection. + pub fn only_unsafe(self) -> Option { + match self { + DoppelgangerStatus::SigningEnabled(_) => None, + DoppelgangerStatus::SigningDisabled(pubkey) => Some(pubkey), + DoppelgangerStatus::UnknownToDoppelganger(pubkey) => Some(pubkey), } - - if let Err(e) = self - .slashing_protection - .prune_all_signed_blocks(all_pubkeys.iter(), new_min_slot) - { - error!( - error = ?e, - "Error during pruning of signed blocks" - ); - return; - } - - *last_prune = current_epoch; - - info!("Completed pruning of slashing protection DB"); } } diff --git a/validator_manager/src/create_validators.rs b/validator_manager/src/create_validators.rs index b40fe61a82..07578033cd 100644 --- a/validator_manager/src/create_validators.rs +++ b/validator_manager/src/create_validators.rs @@ -84,7 +84,6 @@ pub fn cli_app() -> Command { .long(COUNT_FLAG) .value_name("VALIDATOR_COUNT") .help("The number of validators to create, regardless of how many already exist") - .conflicts_with("at-most") .action(ArgAction::Set) .display_order(0), ) diff --git a/wordlist.txt b/wordlist.txt index 7adbfe9032..682fae0261 100644 --- a/wordlist.txt +++ b/wordlist.txt @@ -46,6 +46,7 @@ Goerli Grafana Holesky Homebrew +Hoodi Infura IPs IPv @@ -66,6 +67,7 @@ Nethermind NodeJS NullLogger PathBuf +Pectra PowerShell PPA Pre @@ -236,6 +238,7 @@ validators validator's vc virt +walkthrough webapp withdrawable yaml