diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f9478d13691..a8919337a93 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ -beacon_node/network/ @jxs -beacon_node/lighthouse_network/ @jxs +/beacon_node/network/ @jxs +/beacon_node/lighthouse_network/ @jxs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cfba601fad6..de4fd294093 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 574f1eda791..a94a19900c1 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -24,8 +24,6 @@ env: LIGHTHOUSE_GITHUB_TOKEN: ${{ secrets.LIGHTHOUSE_GITHUB_TOKEN }} # Enable self-hosted runners for the sigp repo only. SELF_HOSTED_RUNNERS: ${{ github.repository == 'sigp/lighthouse' }} - # Self-hosted runners need to reference a different host for `./watch` tests. - WATCH_HOST: ${{ github.repository == 'sigp/lighthouse' && 'host.docker.internal' || 'localhost' }} # Disable incremental compilation CARGO_INCREMENTAL: 0 # Enable portable to prevent issues with caching `blst` for the wrong CPU type @@ -297,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] @@ -311,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 9e15ce9a582..1cf523e3e63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,7 @@ dependencies = [ "filesystem", "safe_arith", "sensitive_url", + "serde_json", "slashing_protection", "slot_clock", "tempfile", @@ -51,7 +52,7 @@ dependencies = [ "rpassword", "serde", "serde_yaml", - "slog", + "tracing", "types", "validator_dir", "zeroize", @@ -72,12 +73,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" -[[package]] -name = "adler32" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" - [[package]] name = "aead" version = "0.5.2" @@ -85,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]] @@ -98,7 +93,7 @@ dependencies = [ "cipher 0.3.0", "cpufeatures", "ctr 0.8.0", - "opaque-debug", + "opaque-debug 0.3.1", ] [[package]] @@ -128,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]] @@ -199,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.20" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc1360603efdfba91151e623f13a4f4d3dc4af4adc1cbd90bf37c81e84db4c77" +checksum = "8c77490fe91a0ce933a1f219029521f20fc28c2c0ca95d53fa4da9c00b8d9d4e" dependencies = [ "alloy-rlp", "arbitrary", @@ -214,11 +209,11 @@ dependencies = [ "cfg-if", "const-hex", "derive_arbitrary", - "derive_more 1.0.0", + "derive_more 2.0.1", "foldhash", - "getrandom 0.2.15", - "hashbrown 0.15.2", - "indexmap 2.7.1", + "getrandom 0.2.16", + "hashbrown 0.15.3", + "indexmap 2.9.0", "itoa", "k256 0.13.4", "keccak-asm", @@ -227,7 +222,7 @@ dependencies = [ "proptest-derive", "rand 0.8.5", "ruint", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "serde", "sha3 0.10.8", "tiny-keccak", @@ -252,7 +247,7 @@ checksum = "a40e1ef334153322fd878d07e86af7a529bcb86b2439525920a88eba87bcf943" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] @@ -328,9 +323,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.95" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "arbitrary" @@ -522,7 +517,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", "synstructure", ] @@ -534,7 +529,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] @@ -564,6 +559,18 @@ dependencies = [ "futures-core", ] +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-io" version = "2.4.0" @@ -602,18 +609,18 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] name = "async-trait" -version = "0.1.86" +version = "0.1.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] @@ -670,81 +677,38 @@ 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 2.0.98", + "syn 1.0.109", ] [[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "axum" -version = "0.7.9" +name = "auto_impl" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ - "async-trait", - "axum-core", - "bytes", - "futures-util", - "http 1.2.0", - "http-body 1.0.1", - "http-body-util", - "hyper 1.6.0", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper 1.0.2", - "tokio", - "tower", - "tower-layer", - "tower-service", - "tracing", + "proc-macro2", + "quote", + "syn 2.0.101", ] [[package]] -name = "axum-core" -version = "0.4.5" +name = "autocfg" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http 1.2.0", - "http-body 1.0.1", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper 1.0.2", - "tower-layer", - "tower-service", - "tracing", -] +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", @@ -773,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" @@ -793,9 +779,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.6.0" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" [[package]] name = "beacon_chain" @@ -827,6 +813,7 @@ dependencies = [ "maplit", "merkle_proof", "metrics", + "once_cell", "oneshot_broadcast", "operation_pool", "parking_lot 0.12.3", @@ -838,10 +825,6 @@ dependencies = [ "serde", "serde_json", "slasher", - "slog", - "slog-async", - "slog-term", - "sloggers", "slot_clock", "smallvec", "ssz_types", @@ -853,6 +836,7 @@ dependencies = [ "tempfile", "tokio", "tokio-stream", + "tracing", "tree_hash", "tree_hash_derive", "types", @@ -860,7 +844,7 @@ dependencies = [ [[package]] name = "beacon_node" -version = "7.0.0" +version = "7.1.0-beta.0" dependencies = [ "account_utils", "beacon_chain", @@ -882,10 +866,10 @@ dependencies = [ "sensitive_url", "serde_json", "slasher", - "slog", "store", "strum", "task_executor", + "tracing", "types", "unused_port", ] @@ -895,16 +879,15 @@ name = "beacon_node_fallback" version = "0.1.0" dependencies = [ "clap", - "environment", "eth2", "futures", "itertools 0.10.5", - "logging", "serde", - "slog", "slot_clock", "strum", + "task_executor", "tokio", + "tracing", "types", "validator_metrics", "validator_test_rig", @@ -923,15 +906,21 @@ dependencies = [ "num_cpus", "parking_lot 0.12.3", "serde", - "slog", "slot_clock", "strum", "task_executor", "tokio", "tokio-util", + "tracing", "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" @@ -947,7 +936,7 @@ version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "cexpr", "clang-sys", "itertools 0.12.1", @@ -960,24 +949,24 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.98", + "syn 2.0.101", "which", ] [[package]] name = "bit-set" -version = "0.5.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" -version = "0.6.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" @@ -987,9 +976,19 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.8.0" +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 = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "41262f11d771fd4a61aa3ce019fca363b4b6c282fca9da2a31186d3965a47a5c" +dependencies = [ + "either", + "radium 0.3.0", +] [[package]] name = "bitvec" @@ -1024,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]] @@ -1040,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]] @@ -1070,9 +1090,9 @@ dependencies = [ [[package]] name = "blst" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4378725facc195f1a538864863f6de233b500a8862747e7f165078a419d5e874" +checksum = "47c79a94619fade3c0b887670333513a67ac28a6a7e653eb260bf0d4103db38d" dependencies = [ "cc", "glob", @@ -1088,7 +1108,7 @@ checksum = "7a8a8ed6fefbeef4a8c7b460e4110e12c5e22a5b7cf32621aae6ad650c4dcf29" dependencies = [ "blst", "byte-slice-cast", - "ff 0.13.0", + "ff 0.13.1", "group 0.13.0", "pairing", "rand_core 0.6.4", @@ -1096,19 +1116,9 @@ dependencies = [ "subtle", ] -[[package]] -name = "bollard-stubs" -version = "1.42.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed59b5c00048f48d7af971b71f800fdf23e858844a6f9e4d32ca72e9399e7864" -dependencies = [ - "serde", - "serde_with", -] - [[package]] name = "boot_node" -version = "7.0.0" +version = "7.1.0-beta.0" dependencies = [ "beacon_node", "bytes", @@ -1121,11 +1131,9 @@ dependencies = [ "log", "logging", "serde", - "slog", - "slog-async", - "slog-scope", - "slog-term", "tokio", + "tracing", + "tracing-subscriber", "types", ] @@ -1165,9 +1173,15 @@ checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "byte-slice-cast" -version = "1.2.2" +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 = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" [[package]] name = "byteorder" @@ -1177,9 +1191,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" dependencies = [ "serde", ] @@ -1196,12 +1210,11 @@ dependencies = [ [[package]] name = "bzip2-sys" -version = "0.1.11+1.0.8" +version = "0.1.13+1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" dependencies = [ "cc", - "libc", "pkg-config", ] @@ -1246,12 +1259,26 @@ checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" dependencies = [ "camino", "cargo-platform", - "semver 1.0.25", + "semver 1.0.26", "serde", "serde_json", "thiserror 1.0.69", ] +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver 1.0.26", + "serde", + "serde_json", + "thiserror 2.0.12", +] + [[package]] name = "cast" version = "0.3.0" @@ -1260,9 +1287,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.11" +version = "1.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf" +checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0" dependencies = [ "jobserver", "libc", @@ -1316,14 +1343,16 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.39" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", - "windows-targets 0.52.6", + "wasm-bindgen", + "windows-link", ] [[package]] @@ -1359,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]] @@ -1386,9 +1415,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.27" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" +checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" dependencies = [ "clap_builder", "clap_derive", @@ -1396,9 +1425,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.27" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" +checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" dependencies = [ "anstream", "anstyle", @@ -1409,14 +1438,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.24" +version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] @@ -1461,17 +1490,18 @@ dependencies = [ "http_metrics", "kzg", "lighthouse_network", + "logging", "metrics", "monitoring_api", "network", "operation_pool", + "rand 0.8.5", "sensitive_url", "serde", "serde_json", "serde_yaml", "slasher", "slasher_service", - "slog", "slot_clock", "state_processing", "store", @@ -1479,18 +1509,77 @@ dependencies = [ "time", "timer", "tokio", + "tracing", + "tracing-subscriber", "types", ] [[package]] name = "cmake" -version = "0.1.53" +version = "0.1.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24a03c8b52922d68a1589ad61032f2c1aa5a8158d2aa0d93c6e9534944bbad6" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" 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" @@ -1499,11 +1588,10 @@ checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "colored" -version = "2.2.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ - "lazy_static", "windows-sys 0.59.0", ] @@ -1551,6 +1639,26 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const_format" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -1563,6 +1671,15 @@ 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" @@ -1599,13 +1716,13 @@ dependencies = [ [[package]] name = "crate_crypto_internal_eth_kzg_bls12_381" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48603155907d588e487aea229f61a28d9a918c95c9aa987055ba29502225810b" +checksum = "76f9cdad245e39a3659bc4c8958e93de34bd31ba3131ead14ccfb4b2cd60e52d" dependencies = [ "blst", "blstrs", - "ff 0.13.0", + "ff 0.13.1", "group 0.13.0", "pairing", "subtle", @@ -1613,9 +1730,9 @@ dependencies = [ [[package]] name = "crate_crypto_internal_eth_kzg_erasure_codes" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdf616e4b4f1799191bb1e70b8a29f65e95ab5d74c59972a34998de488d01efd" +checksum = "581d28bcc93eecd97a04cebc5293271e0f41650f03c102f24d6cd784cbedb9f2" dependencies = [ "crate_crypto_internal_eth_kzg_bls12_381", "crate_crypto_internal_eth_kzg_polynomial", @@ -1623,30 +1740,30 @@ dependencies = [ [[package]] name = "crate_crypto_internal_eth_kzg_maybe_rayon" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1ddd0330f34f0b92a9f0b29bc3f8494b30d596ab8b951233ec90b2d72ab132c" +checksum = "06fc0f984e585ea984a766c5b58d6bf6c51e463b0a0835b0dd4652d358b506b3" [[package]] name = "crate_crypto_internal_eth_kzg_polynomial" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7488314261926373e1c20121c404fabf5b57ca09f48eddc7fef38be1df79a006" +checksum = "56dff7a45e2d80308b21abdbc5520ec23c3ebfb3a94fafc02edfa7f356af6d7f" dependencies = [ "crate_crypto_internal_eth_kzg_bls12_381", ] [[package]] name = "crate_crypto_kzg_multi_open_fk20" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24efdb64e7518848f11069dd9de23bd04455146a9fd5486345d99ed8bfdb049" +checksum = "1a0c2f82695a88809e713e1ff9534cb90ceffab0a08f4bd33245db711f9d356f" dependencies = [ "crate_crypto_internal_eth_kzg_bls12_381", "crate_crypto_internal_eth_kzg_maybe_rayon", "crate_crypto_internal_eth_kzg_polynomial", "hex", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] @@ -1740,7 +1857,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", @@ -1752,7 +1869,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", @@ -1764,28 +1881,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", ] @@ -1809,9 +1916,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", @@ -1841,7 +1948,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] @@ -1856,12 +1963,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]] @@ -1880,16 +1987,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.98", + "syn 2.0.101", ] [[package]] @@ -1905,13 +2012,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.98", + "syn 2.0.101", ] [[package]] @@ -1934,23 +2041,17 @@ dependencies = [ "libc", ] -[[package]] -name = "dary_heap" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04d2cd9c18b9f454ed67da600630b021a8a80bf33f8c95896ab33aaf1c26b728" - [[package]] name = "data-encoding" -version = "2.7.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "data-encoding-macro" -version = "0.1.16" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b16d9d0d88a5273d830dac8b78ceb217ffc9b1d5404e5597a3542515329405b" +checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" dependencies = [ "data-encoding", "data-encoding-macro-internal", @@ -1958,12 +2059,12 @@ dependencies = [ [[package]] name = "data-encoding-macro-internal" -version = "0.1.14" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1145d32e826a7748b69ee8fc62d3e6355ff7f1051df53141e7048162fc90481b" +checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" dependencies = [ "data-encoding", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] @@ -1977,9 +2078,9 @@ dependencies = [ "environment", "hex", "serde", - "slog", "store", "strum", + "tracing", "types", ] @@ -1991,9 +2092,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", @@ -2026,9 +2127,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", @@ -2051,9 +2152,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", ] @@ -2077,20 +2178,20 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] name = "derive_more" -version = "0.99.18" +version = "0.99.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ - "convert_case", + "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version 0.4.1", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] @@ -2099,7 +2200,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" dependencies = [ - "derive_more-impl", + "derive_more-impl 1.0.0", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl 2.0.1", ] [[package]] @@ -2110,55 +2220,28 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", - "unicode-xid", + "syn 2.0.101", ] [[package]] -name = "diesel" -version = "2.2.7" +name = "derive_more-impl" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04001f23ba8843dc315804fa324000376084dfb1c30794ff68dd279e6e5696d5" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ - "bitflags 2.8.0", - "byteorder", - "diesel_derives", - "itoa", - "pq-sys", - "r2d2", + "proc-macro2", + "quote", + "syn 2.0.101", + "unicode-xid", ] [[package]] -name = "diesel_derives" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f2c3de51e2ba6bf2a648285696137aaf0f5f487bcbea93972fe8a364e131a4" -dependencies = [ - "diesel_table_macro_syntax", - "dsl_auto_type", - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "diesel_migrations" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a73ce704bad4231f001bff3314d91dce4aba0770cee8b233991859abc15c1f6" -dependencies = [ - "diesel", - "migrations_internals", - "migrations_macros", -] - -[[package]] -name = "diesel_table_macro_syntax" -version = "0.2.0" +name = "digest" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" dependencies = [ - "syn 2.0.98", + "generic-array 0.12.4", ] [[package]] @@ -2167,7 +2250,7 @@ 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]] @@ -2200,16 +2283,6 @@ dependencies = [ "dirs-sys", ] -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - [[package]] name = "dirs-sys" version = "0.3.7" @@ -2221,17 +2294,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - [[package]] name = "discv5" version = "0.9.1" @@ -2273,7 +2335,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] @@ -2286,32 +2348,19 @@ dependencies = [ "futures", "logging", "parking_lot 0.12.3", - "slog", "slot_clock", "task_executor", "tokio", + "tracing", "types", -] - -[[package]] -name = "dsl_auto_type" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ae9aca7527f85f26dd76483eb38533fd84bd571065da1739656ef71c5ff5b" -dependencies = [ - "darling 0.20.10", - "either", - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.98", + "validator_store", ] [[package]] name = "dtoa" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" [[package]] name = "dunce" @@ -2337,7 +2386,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", @@ -2365,11 +2414,23 @@ dependencies = [ "ed25519", "rand_core 0.6.4", "serde", - "sha2 0.10.8", + "sha2 0.10.9", "subtle", "zeroize", ] +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "ef_tests" version = "0.2.0" @@ -2404,9 +2465,9 @@ dependencies = [ [[package]] name = "either" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "elliptic-curve" @@ -2419,8 +2480,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", @@ -2436,8 +2498,8 @@ dependencies = [ "base16ct 0.2.0", "crypto-bigint 0.5.5", "digest 0.10.7", - "ff 0.13.0", - "generic-array", + "ff 0.13.1", + "generic-array 0.14.7", "group 0.13.0", "pem-rfc7468", "pkcs8 0.10.2", @@ -2484,7 +2546,27 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", +] + +[[package]] +name = "enum-ordinalize" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", ] [[package]] @@ -2514,46 +2596,60 @@ dependencies = [ name = "environment" version = "0.1.2" dependencies = [ - "async-channel", + "async-channel 1.9.0", + "clap", "ctrlc", "eth2_config", "eth2_network_config", "futures", "logging", + "logroller", "serde", - "slog", - "slog-async", - "slog-json", - "slog-term", - "sloggers", "task_executor", "tokio", + "tracing", + "tracing-appender", + "tracing-log", + "tracing-subscriber", "types", ] [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] -name = "erased-serde" -version = "0.3.31" +name = "errno" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ - "serde", + "libc", + "windows-sys 0.59.0", ] [[package]] -name = "errno" -version = "0.3.10" +name = "eth-keystore" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "1fda3bf123be441da5260717e0661c25a2fd9cb2b2c1d20bf2e05580047158ab" dependencies = [ - "libc", - "windows-sys 0.59.0", + "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]] @@ -2574,12 +2670,11 @@ dependencies = [ "sensitive_url", "serde", "serde_yaml", - "slog", - "sloggers", "state_processing", "superstruct", "task_executor", "tokio", + "tracing", "tree_hash", "types", ] @@ -2605,14 +2700,16 @@ version = "0.1.0" dependencies = [ "derivative", "either", + "enr", "eth2_keystore", "ethereum_serde_utils", "ethereum_ssz", "ethereum_ssz_derive", "futures", "futures-util", - "lighthouse_network", + "libp2p-identity", "mediatype", + "multiaddr", "pretty_reqwest_error", "proto_array", "rand 0.8.5", @@ -2623,7 +2720,6 @@ dependencies = [ "serde_json", "slashing_protection", "ssz_types", - "store", "test_random_derive", "tokio", "types", @@ -2674,7 +2770,7 @@ dependencies = [ "hmac 0.11.0", "pbkdf2 0.8.0", "rand 0.8.5", - "scrypt", + "scrypt 0.7.0", "serde", "serde_json", "serde_repr", @@ -2694,15 +2790,14 @@ dependencies = [ "eth2_config", "ethereum_ssz", "kzg", - "logging", "pretty_reqwest_error", "reqwest", "sensitive_url", "serde_yaml", "sha2 0.9.9", - "slog", "tempfile", "tokio", + "tracing", "types", "url", "zip", @@ -2831,7 +2926,7 @@ checksum = "c853bd72c9e5787f8aafc3df2907c2ed03cff3150c3acd94e2e53a98ab70a8ab" dependencies = [ "cpufeatures", "ring", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] @@ -2849,25 +2944,30 @@ dependencies = [ [[package]] name = "ethereum_ssz" -version = "0.7.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e999563461faea0ab9bc0024e5e66adcee35881f3d5062f52f31a4070fe1522" +checksum = "86da3096d1304f5f28476ce383005385459afeaf0eea08592b65ddbc9b258d16" dependencies = [ "alloy-primitives", + "arbitrary", + "ethereum_serde_utils", "itertools 0.13.0", + "serde", + "serde_derive", "smallvec", + "typenum", ] [[package]] name = "ethereum_ssz_derive" -version = "0.7.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3deae99c8e74829a00ba7a92d49055732b3c1f093f2ccfa3cbc621679b6fa91" +checksum = "d832a5c38eba0e7ad92592f7a22d693954637fbb332b4f669590d66a5c3183e5" dependencies = [ - "darling 0.20.10", + "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] @@ -2900,7 +3000,7 @@ dependencies = [ "dunce", "ethers-core", "eyre", - "getrandom 0.2.15", + "getrandom 0.2.16", "hex", "proc-macro2", "quote", @@ -2909,7 +3009,7 @@ dependencies = [ "serde", "serde_json", "syn 1.0.109", - "toml 0.5.11", + "toml", "url", "walkdir", ] @@ -2937,15 +3037,17 @@ checksum = "ade3e9c97727343984e1ceada4fdab11142d2ee3472d2c67027d56b1251d4f15" dependencies = [ "arrayvec", "bytes", - "cargo_metadata", + "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", @@ -2958,6 +3060,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" @@ -2965,13 +3110,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", @@ -2993,6 +3138,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" @@ -3012,9 +3175,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", @@ -3035,10 +3198,12 @@ dependencies = [ name = "execution_engine_integration" version = "0.1.0" dependencies = [ - "async-channel", + "async-channel 1.9.0", "deposit_contract", "ethers-core", + "ethers-middleware", "ethers-providers", + "ethers-signers", "execution_layer", "fork_choice", "futures", @@ -3065,7 +3230,6 @@ dependencies = [ "builder_client", "bytes", "eth2", - "eth2_network_config", "ethereum_serde_utils", "ethereum_ssz", "ethers-core", @@ -3089,7 +3253,6 @@ dependencies = [ "serde", "serde_json", "sha2 0.9.9", - "slog", "slot_clock", "ssz_types", "state_processing", @@ -3099,6 +3262,7 @@ dependencies = [ "tempfile", "tokio", "tokio-stream", + "tracing", "tree_hash", "tree_hash_derive", "triehash", @@ -3117,6 +3281,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" @@ -3142,7 +3312,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" dependencies = [ "arrayvec", - "auto_impl", + "auto_impl 1.3.0", "bytes", ] @@ -3153,7 +3323,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" dependencies = [ "arrayvec", - "auto_impl", + "auto_impl 1.3.0", "bytes", ] @@ -3179,9 +3349,9 @@ dependencies = [ [[package]] name = "ff" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ "bitvec 1.0.1", "rand_core 0.6.4", @@ -3252,9 +3422,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.35" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", "libz-sys", @@ -3269,9 +3439,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" @@ -3295,12 +3465,13 @@ dependencies = [ "beacon_chain", "ethereum_ssz", "ethereum_ssz_derive", + "logging", "metrics", "proto_array", - "slog", "state_processing", "store", "tokio", + "tracing", "types", ] @@ -3404,6 +3575,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" @@ -3412,7 +3593,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] @@ -3422,7 +3603,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" dependencies = [ "futures-io", - "rustls 0.23.22", + "rustls 0.23.27", "rustls-pki-types", ] @@ -3443,10 +3624,6 @@ name = "futures-timer" version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" -dependencies = [ - "gloo-timers", - "send_wrapper 0.4.0", -] [[package]] name = "futures-util" @@ -3488,6 +3665,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" @@ -3510,21 +3696,22 @@ dependencies = [ "ethereum_ssz", "futures", "int_to_bytes", + "logging", "merkle_proof", "rayon", "sensitive_url", - "slog", "state_processing", "tokio", + "tracing", "tree_hash", "types", ] [[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", @@ -3535,14 +3722,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]] @@ -3551,7 +3740,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", ] @@ -3578,7 +3767,7 @@ checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] @@ -3587,48 +3776,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" -[[package]] -name = "gloo-timers" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "gossipsub" -version = "0.5.0" -dependencies = [ - "async-channel", - "asynchronous-codec", - "base64 0.21.7", - "byteorder", - "bytes", - "either", - "fnv", - "futures", - "futures-timer", - "getrandom 0.2.15", - "hashlink 0.9.1", - "hex_fmt", - "libp2p", - "prometheus-client", - "quick-protobuf", - "quick-protobuf-codec", - "quickcheck", - "rand 0.8.5", - "regex", - "serde", - "sha2 0.10.8", - "tracing", - "void", - "web-time", -] - [[package]] name = "graffiti_file" version = "0.1.0" @@ -3636,8 +3783,8 @@ dependencies = [ "bls", "hex", "serde", - "slog", "tempfile", + "tracing", "types", ] @@ -3658,7 +3805,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ff 0.13.0", + "ff 0.13.1", "rand 0.8.5", "rand_core 0.6.4", "rand_xorshift", @@ -3677,7 +3824,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.7.1", + "indexmap 2.9.0", "slab", "tokio", "tokio-util", @@ -3686,17 +3833,17 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.7" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.2.0", - "indexmap 2.7.1", + "http 1.3.1", + "indexmap 2.9.0", "slab", "tokio", "tokio-util", @@ -3705,9 +3852,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", @@ -3746,9 +3893,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", @@ -3850,6 +3997,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +[[package]] +name = "hermit-abi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" + [[package]] name = "hex" version = "0.4.3" @@ -3882,9 +4035,9 @@ dependencies = [ "idna", "ipnet", "once_cell", - "rand 0.9.0", + "rand 0.9.1", "socket2", - "thiserror 2.0.11", + "thiserror 2.0.12", "tinyvec", "tokio", "tracing", @@ -3893,9 +4046,9 @@ dependencies = [ [[package]] name = "hickory-resolver" -version = "0.25.0-alpha.4" +version = "0.25.0-alpha.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42bc352e4412fb657e795f79b4efcf2bd60b59ee5ca0187f3554194cd1107a27" +checksum = "5762f69ebdbd4ddb2e975cd24690bf21fe6b2604039189c26acddbc427f12887" dependencies = [ "cfg-if", "futures-util", @@ -3904,10 +4057,10 @@ dependencies = [ "moka", "once_cell", "parking_lot 0.12.3", - "rand 0.8.5", + "rand 0.9.1", "resolv-conf", "smallvec", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tracing", ] @@ -3921,23 +4074,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", ] @@ -3950,17 +4093,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" @@ -3970,17 +4102,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" @@ -3994,9 +4115,9 @@ dependencies = [ [[package]] name = "http" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -4021,18 +4142,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.2.0", + "http 1.3.1", ] [[package]] name = "http-body-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", - "http 1.2.0", + "futures-core", + "http 1.3.1", "http-body 1.0.1", "pin-project-lite", ] @@ -4070,7 +4191,6 @@ dependencies = [ "sensitive_url", "serde", "serde_json", - "slog", "slot_clock", "state_processing", "store", @@ -4079,6 +4199,7 @@ dependencies = [ "task_executor", "tokio", "tokio-stream", + "tracing", "tree_hash", "types", "warp", @@ -4098,10 +4219,10 @@ dependencies = [ "metrics", "reqwest", "serde", - "slog", "slot_clock", "store", "tokio", + "tracing", "types", "warp", "warp_utils", @@ -4109,9 +4230,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -4121,9 +4242,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" @@ -4158,8 +4279,8 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.7", - "http 1.2.0", + "h2 0.4.10", + "http 1.3.1", "http-body 1.0.1", "httparse", "httpdate", @@ -4199,16 +4320,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.2.0", + "http 1.3.1", "http-body 1.0.1", "hyper 1.6.0", + "libc", "pin-project-lite", "socket2", "tokio", @@ -4218,16 +4340,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]] @@ -4280,9 +4403,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" @@ -4304,9 +4427,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" @@ -4325,9 +4448,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" @@ -4354,7 +4477,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] @@ -4427,7 +4550,7 @@ dependencies = [ "attohttpc", "bytes", "futures", - "http 1.2.0", + "http 1.3.1", "http-body-util", "hyper 1.6.0", "hyper-util", @@ -4440,20 +4563,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.2.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", @@ -4474,7 +4597,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" dependencies = [ - "parity-scale-codec 3.6.12", + "parity-scale-codec 3.7.4", ] [[package]] @@ -4512,7 +4635,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] @@ -4533,13 +4656,13 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "arbitrary", "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.15.3", "serde", ] @@ -4560,8 +4683,8 @@ dependencies = [ "serde", "serde_json", "signing_method", - "slog", "tokio", + "tracing", "types", "url", "validator_dir", @@ -4571,11 +4694,11 @@ dependencies = [ [[package]] name = "inout" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -4626,7 +4749,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", ] @@ -4639,11 +4762,11 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "is-terminal" -version = "0.4.15" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ - "hermit-abi 0.4.0", + "hermit-abi 0.5.1", "libc", "windows-sys 0.59.0", ] @@ -4683,16 +4806,17 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +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", ] @@ -4708,11 +4832,11 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "9.3.0" +version = "9.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "js-sys", "pem", "ring", @@ -4730,7 +4854,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", ] @@ -4744,7 +4868,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", ] @@ -4813,7 +4937,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lcli" -version = "7.0.0" +version = "7.1.0-beta.0" dependencies = [ "account_utils", "beacon_chain", @@ -4838,10 +4962,11 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "sloggers", "snap", "state_processing", "store", + "tracing", + "tracing-subscriber", "tree_hash", "types", "validator_dir", @@ -4872,33 +4997,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.169" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" - -[[package]] -name = "libflate" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45d9dfdc14ea4ef0900c1cddbc8dcd553fbaacd8a4a282cf4018ae9dd04fb21e" -dependencies = [ - "adler32", - "core2", - "crc32fast", - "dary_heap", - "libflate_lz77", -] - -[[package]] -name = "libflate_lz77" -version = "2.1.0" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e0d73b369f386f1c44abd9c570d5318f55ccde816ff4b562fa452e5182863d" -dependencies = [ - "core2", - "hashbrown 0.14.5", - "rle-decode-fast", -] +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libloading" @@ -4907,14 +5008,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[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" @@ -4923,7 +5024,7 @@ source = "git+https://github.com/sigp/libmdbx-rs?rev=e6ff4b9377c1619bcf0bfdf52be dependencies = [ "bitflags 1.3.2", "byteorder", - "derive_more 0.99.18", + "derive_more 0.99.20", "indexmap 1.9.3", "libc", "mdbx-sys", @@ -4941,7 +5042,7 @@ dependencies = [ "either", "futures", "futures-timer", - "getrandom 0.2.15", + "getrandom 0.2.16", "libp2p-allow-block-list", "libp2p-connection-limits", "libp2p-core", @@ -4960,7 +5061,7 @@ dependencies = [ "multiaddr", "pin-project", "rw-stream-sink", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -5005,7 +5106,7 @@ dependencies = [ "quick-protobuf", "rand 0.8.5", "rw-stream-sink", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", "unsigned-varint 0.8.0", "web-time", @@ -5027,6 +5128,36 @@ dependencies = [ "tracing", ] +[[package]] +name = "libp2p-gossipsub" +version = "0.49.0" +source = "git+https://github.com/sigp/rust-libp2p.git?rev=61b2820#61b2820de7a3fab5ae5e1362c4dfa93bd7c41e98" +dependencies = [ + "async-channel 2.3.1", + "asynchronous-codec", + "base64 0.22.1", + "byteorder", + "bytes", + "either", + "fnv", + "futures", + "futures-timer", + "getrandom 0.2.16", + "hashlink 0.9.1", + "hex_fmt", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "prometheus-client", + "quick-protobuf", + "quick-protobuf-codec", + "rand 0.8.5", + "regex", + "sha2 0.10.9", + "tracing", + "web-time", +] + [[package]] name = "libp2p-identify" version = "0.46.0" @@ -5044,28 +5175,28 @@ dependencies = [ "quick-protobuf", "quick-protobuf-codec", "smallvec", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] [[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", ] @@ -5142,7 +5273,7 @@ dependencies = [ "rand 0.8.5", "snow", "static_assertions", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", "x25519-dalek", "zeroize", @@ -5179,9 +5310,9 @@ dependencies = [ "quinn", "rand 0.8.5", "ring", - "rustls 0.23.22", + "rustls 0.23.27", "socket2", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tracing", ] @@ -5218,7 +5349,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] @@ -5249,9 +5380,9 @@ dependencies = [ "libp2p-identity", "rcgen", "ring", - "rustls 0.23.22", + "rustls 0.23.27", "rustls-webpki 0.101.7", - "thiserror 2.0.11", + "thiserror 2.0.12", "x509-parser", "yasna", ] @@ -5280,7 +5411,7 @@ dependencies = [ "either", "futures", "libp2p-core", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", "yamux 0.12.1", "yamux 0.13.4", @@ -5292,58 +5423,10 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "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" @@ -5357,9 +5440,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", @@ -5368,7 +5451,7 @@ dependencies = [ [[package]] name = "lighthouse" -version = "7.0.0" +version = "7.1.0-beta.0" dependencies = [ "account_manager", "account_utils", @@ -5399,10 +5482,11 @@ dependencies = [ "serde_yaml", "slasher", "slashing_protection", - "slog", "store", "task_executor", "tempfile", + "tracing", + "tracing-subscriber", "types", "unused_port", "validator_client", @@ -5417,21 +5501,22 @@ version = "0.2.0" dependencies = [ "alloy-primitives", "alloy-rlp", - "async-channel", + "async-channel 1.9.0", "bytes", "delay_map", "directory", "dirs", "discv5", "either", + "eth2", "ethereum_ssz", "ethereum_ssz_derive", "fnv", "futures", - "gossipsub", "hex", "itertools 0.10.5", "libp2p", + "libp2p-gossipsub", "libp2p-mplex", "lighthouse_version", "local-ip-address", @@ -5447,9 +5532,6 @@ dependencies = [ "regex", "serde", "sha2 0.9.9", - "slog", - "slog-async", - "slog-term", "smallvec", "snap", "ssz_types", @@ -5461,10 +5543,37 @@ dependencies = [ "tokio", "tokio-io-timeout", "tokio-util", + "tracing", + "tracing-subscriber", "types", "unsigned-varint 0.8.0", "unused_port", - "void", +] + +[[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]] @@ -5488,11 +5597,17 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "litemap" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" [[package]] name = "lmdb-rkv" @@ -5517,13 +5632,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", ] @@ -5547,29 +5662,38 @@ dependencies = [ [[package]] name = "log" -version = "0.4.25" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "logging" version = "0.2.0" dependencies = [ "chrono", + "logroller", "metrics", - "parking_lot 0.12.3", "serde", "serde_json", - "slog", - "slog-term", - "sloggers", - "take_mut", "tokio", "tracing", "tracing-appender", "tracing-core", "tracing-log", "tracing-subscriber", + "workspace_members", +] + +[[package]] +name = "logroller" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90536db32a1cb3672665cdf3269bf030b0f395fabee863895c27b75b9f7a8a7d" +dependencies = [ + "chrono", + "flate2", + "regex", + "thiserror 1.0.69", ] [[package]] @@ -5591,7 +5715,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]] @@ -5628,12 +5752,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" @@ -5649,22 +5767,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest 0.10.7", -] - [[package]] name = "mdbx-sys" version = "0.11.6-4" @@ -5678,9 +5780,9 @@ dependencies = [ [[package]] name = "mediatype" -version = "0.19.18" +version = "0.19.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8878cd8d1b3c8c8ae4b2ba0a36652b7cf192f618a599a7fbdfa25cffd4ea72dd" +checksum = "33746aadcb41349ec291e7f2f0a3aa6834d1d7c58066fb4b01f68efc4c4b7631" [[package]] name = "memchr" @@ -5739,36 +5841,15 @@ dependencies = [ "prometheus", ] -[[package]] -name = "migrations_internals" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd01039851e82f8799046eabbb354056283fb265c8ec0996af940f4e85a380ff" -dependencies = [ - "serde", - "toml 0.8.19", -] - -[[package]] -name = "migrations_macros" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb161cc72176cb37aa47f1fc520d3ef02263d67d661f44f05d05a079e1237fd" -dependencies = [ - "migrations_internals", - "proc-macro2", - "quote", -] - [[package]] name = "milhouse" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f68e33f98199224d1073f7c1468ea6abfea30736306fb79c7181a881e97ea32f" +checksum = "eb1ada1f56cc1c79f40517fdcbf57e19f60424a3a1ce372c3fe9b22e4fdd83eb" dependencies = [ "alloy-primitives", "arbitrary", - "derivative", + "educe", "ethereum_hashing", "ethereum_ssz", "ethereum_ssz_derive", @@ -5807,9 +5888,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.3" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", ] @@ -5833,21 +5914,21 @@ checksum = "9366861eb2a2c436c20b12c8dbec5f798cea6b47ad99216be0282942e2c81ea0" [[package]] name = "mockito" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "652cd6d169a36eaf9d1e6bce1a221130439a966d7f27858af66a33a66e9c4ee2" +checksum = "7760e0e418d9b7e5777c0374009ca4c93861b9066f18cb334a20ce50ab63aa48" dependencies = [ "assert-json-diff", "bytes", "colored", "futures-util", - "http 1.2.0", + "http 1.3.1", "http-body 1.0.1", "http-body-util", "hyper 1.6.0", "hyper-util", "log", - "rand 0.8.5", + "rand 0.9.1", "regex", "serde_json", "serde_urlencoded", @@ -5871,7 +5952,7 @@ dependencies = [ "smallvec", "tagptr", "thiserror 1.0.69", - "uuid 1.12.1", + "uuid 1.16.0", ] [[package]] @@ -5887,10 +5968,10 @@ dependencies = [ "sensitive_url", "serde", "serde_json", - "slog", "store", "task_executor", "tokio", + "tracing", ] [[package]] @@ -5955,9 +6036,9 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", @@ -6043,7 +6124,7 @@ dependencies = [ "log", "netlink-packet-core", "netlink-sys", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -6066,7 +6147,7 @@ dependencies = [ "alloy-primitives", "alloy-rlp", "anyhow", - "async-channel", + "async-channel 1.9.0", "beacon_chain", "beacon_processor", "bls", @@ -6079,11 +6160,12 @@ dependencies = [ "fnv", "futures", "genesis", - "gossipsub", "hex", - "igd-next 0.16.0", + "igd-next 0.16.1", "itertools 0.10.5", + "k256 0.13.4", "kzg", + "libp2p-gossipsub", "lighthouse_network", "logging", "lru_cache", @@ -6092,11 +6174,8 @@ dependencies = [ "operation_pool", "parking_lot 0.12.3", "rand 0.8.5", + "rand_chacha 0.3.1", "serde_json", - "slog", - "slog-async", - "slog-term", - "sloggers", "slot_clock", "smallvec", "ssz_types", @@ -6105,6 +6184,8 @@ dependencies = [ "task_executor", "tokio", "tokio-stream", + "tracing", + "tracing-subscriber", "types", ] @@ -6136,7 +6217,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "cfg-if", "cfg_aliases", "libc", @@ -6288,9 +6369,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "oneshot_broadcast" @@ -6301,9 +6382,15 @@ dependencies = [ [[package]] name = "oorandom" -version = "11.1.4" +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 = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" [[package]] name = "opaque-debug" @@ -6318,7 +6405,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", @@ -6342,7 +6429,7 @@ version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "cfg-if", "foreign-types", "libc", @@ -6359,7 +6446,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] @@ -6370,18 +6457,18 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-src" -version = "300.4.1+3.4.0" +version = "300.5.0+3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c" +checksum = "e8ce546f549326b0e6052b649198487d91320875da901e7bd11a06d1ee3f9c2f" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.107" +version = "0.9.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" +checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847" dependencies = [ "cc", "libc", @@ -6412,6 +6499,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" @@ -6427,7 +6523,7 @@ dependencies = [ "ecdsa 0.16.9", "elliptic-curve 0.13.8", "primeorder", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] @@ -6455,15 +6551,17 @@ dependencies = [ [[package]] name = "parity-scale-codec" -version = "3.6.12" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "306800abfa29c7f16596b5970a588435e3d5b3149683d00c12b699cc19f895ee" +checksum = "c9fde3d0718baf5bc92f577d652001da0f8d54cd03a7974e118d04fc888dc23d" dependencies = [ "arrayvec", "bitvec 1.0.1", "byte-slice-cast", + "const_format", "impl-trait-for-tuples", - "parity-scale-codec-derive 3.6.12", + "parity-scale-codec-derive 3.7.4", + "rustversion", "serde", ] @@ -6481,14 +6579,14 @@ dependencies = [ [[package]] name = "parity-scale-codec-derive" -version = "3.6.12" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d830939c76d294956402033aee57a6da7b438f2294eb94864c37b0569053a42c" +checksum = "581c837bb6b9541ce7faa9377c20616e4fb7650f6b0f68bc93c827ee504fb7b3" dependencies = [ - "proc-macro-crate 3.2.0", + "proc-macro-crate 3.3.0", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.101", ] [[package]] @@ -6540,7 +6638,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.8", + "redox_syscall 0.5.12", "smallvec", "windows-targets 0.52.6", ] @@ -6568,7 +6666,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]] @@ -6580,14 +6678,14 @@ dependencies = [ "digest 0.10.7", "hmac 0.12.1", "password-hash", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] name = "pem" -version = "3.0.4" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" dependencies = [ "base64 0.22.1", "serde", @@ -6610,12 +6708,12 @@ 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.11", + "thiserror 2.0.12", "ucd-trie", ] @@ -6629,42 +6727,24 @@ dependencies = [ "rustc_version 0.4.1", ] -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_shared", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - [[package]] name = "pin-project" -version = "1.1.8" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.8" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] @@ -6695,15 +6775,15 @@ 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", ] [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "platforms" @@ -6761,7 +6841,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ "cpufeatures", - "opaque-debug", + "opaque-debug 0.3.1", "universal-hash", ] @@ -6773,44 +6853,15 @@ checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", "cpufeatures", - "opaque-debug", + "opaque-debug 0.3.1", "universal-hash", ] [[package]] name = "portable-atomic" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" - -[[package]] -name = "postgres-protocol" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ff0abab4a9b844b93ef7b81f1efc0a366062aaef2cd702c76256b5dc075c54" -dependencies = [ - "base64 0.22.1", - "byteorder", - "bytes", - "fallible-iterator", - "hmac 0.12.1", - "md-5", - "memchr", - "rand 0.9.0", - "sha2 0.10.8", - "stringprep", -] - -[[package]] -name = "postgres-types" -version = "0.2.9" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613283563cd90e1dfc3518d548caee47e0e725455ed619881f5cf21f36de4b48" -dependencies = [ - "bytes", - "fallible-iterator", - "postgres-protocol", -] +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" [[package]] name = "powerfmt" @@ -6820,21 +6871,11 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" -dependencies = [ - "zerocopy 0.7.35", -] - -[[package]] -name = "pq-sys" -version = "0.7.0" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b51d65ebe1cb1f40641b15abae017fed35ccdda46e3dab1ff8768f625a3222" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "libc", - "vcpkg", + "zerocopy", ] [[package]] @@ -6847,12 +6888,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.29" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" +checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" dependencies = [ "proc-macro2", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] @@ -6903,18 +6944,42 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.2.0" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit 0.22.26", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ - "toml_edit 0.22.23", + "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.93" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -6968,18 +7033,18 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] name = "proptest" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" +checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.8.0", + "bitflags 2.9.0", "lazy_static", "num-traits", "rand 0.8.5", @@ -6999,7 +7064,7 @@ checksum = "4ee1c9ac207483d5e7db4940700de86a9aae46ef90c48b57f99fe7edb8345e49" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] @@ -7023,7 +7088,7 @@ checksum = "5e617cc9058daa5e1fe5a0d23ed745773a5ee354111dad1ec0235b0cc16b6730" dependencies = [ "cfg-if", "darwin-libproc", - "derive_more 0.99.18", + "derive_more 0.99.20", "glob", "mach2", "nix 0.24.3", @@ -7074,49 +7139,51 @@ 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.0", - "rustls 0.23.22", + "rustc-hash 2.1.1", + "rustls 0.23.27", "socket2", - "thiserror 2.0.11", + "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.0", - "rustls 0.23.22", + "rustc-hash 2.1.1", + "rustls 0.23.27", "rustls-pki-types", "slab", - "thiserror 2.0.11", + "thiserror 2.0.12", "tinyvec", "tracing", "web-time", @@ -7124,9 +7191,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.9" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904" +checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" dependencies = [ "cfg_aliases", "libc", @@ -7138,13 +7205,19 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +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" @@ -7166,6 +7239,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" @@ -7192,13 +7271,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.0", - "zerocopy 0.8.14", + "rand_core 0.9.3", ] [[package]] @@ -7218,7 +7296,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -7227,17 +7305,16 @@ 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]] name = "rand_core" -version = "0.9.0" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.1", - "zerocopy 0.8.14", + "getrandom 0.3.2", ] [[package]] @@ -7284,9 +7361,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", ] @@ -7302,11 +7379,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.8" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", ] [[package]] @@ -7315,7 +7392,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", ] @@ -7394,7 +7471,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 0.1.2", + "sync_wrapper", "system-configuration 0.5.1", "tokio", "tokio-native-tls", @@ -7428,13 +7505,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" @@ -7465,17 +7538,20 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", ] [[package]] -name = "rle-decode-fast" -version = "1.0.3" +name = "ripemd" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] [[package]] name = "rlp" @@ -7537,9 +7613,9 @@ dependencies = [ [[package]] name = "ruint" -version = "1.12.4" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5ef8fb1dd8de3870cb8400d51b4c2023854bbafd5431a3ac7e7317243e22d2f" +checksum = "78a46eb779843b2c4f21fac5773e25d6d5b7c8f0922876c91541790d2ca27eef" dependencies = [ "alloy-rlp", "arbitrary", @@ -7551,10 +7627,11 @@ dependencies = [ "num-bigint", "num-integer", "num-traits", - "parity-scale-codec 3.6.12", + "parity-scale-codec 3.7.4", "primitive-types 0.12.2", "proptest", "rand 0.8.5", + "rand 0.9.1", "rlp", "ruint-macro", "serde", @@ -7584,9 +7661,9 @@ dependencies = [ [[package]] name = "rust_eth_kzg" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a237a478ee68e491a0f40bbcbb958b79ba9b37aacce459f7ab3ba78f3cbfa9d0" +checksum = "3f83b5559e1dcd3f7721838909288faf4500fb466eff98eac99b67ac04335b93" dependencies = [ "crate_crypto_internal_eth_kzg_bls12_381", "crate_crypto_internal_eth_kzg_erasure_codes", @@ -7610,9 +7687,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustc-hex" @@ -7635,7 +7712,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.25", + "semver 1.0.26", ] [[package]] @@ -7667,13 +7744,26 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "errno", "libc", "linux-raw-sys 0.4.15", "windows-sys 0.59.0", ] +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.59.0", +] + [[package]] name = "rustls" version = "0.21.12" @@ -7702,14 +7792,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.22" +version = "0.23.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.102.8", + "rustls-webpki 0.103.2", "subtle", "zeroize", ] @@ -7734,11 +7824,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]] @@ -7762,11 +7853,22 @@ 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.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] name = "rusty-fork" @@ -7793,9 +7895,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "safe_arith" @@ -7810,6 +7912,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" @@ -7827,7 +7938,7 @@ checksum = "346a3b32eba2640d17a9cb5927056b08f3de90f65b72fe09402c2ad07d684d0b" dependencies = [ "cfg-if", "derive_more 1.0.0", - "parity-scale-codec 3.6.12", + "parity-scale-codec 3.7.4", "scale-info-derive", ] @@ -7837,10 +7948,10 @@ version = "2.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6630024bf739e2179b91fb424b28898baf819414262c5d376677dbff1fe7ebf" dependencies = [ - "proc-macro-crate 3.2.0", + "proc-macro-crate 3.3.0", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] @@ -7881,10 +7992,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" @@ -7903,7 +8026,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", @@ -7916,8 +8039,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", @@ -7929,7 +8052,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "core-foundation", "core-foundation-sys", "libc", @@ -7957,9 +8080,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" dependencies = [ "serde", ] @@ -7973,12 +8096,6 @@ dependencies = [ "pest", ] -[[package]] -name = "send_wrapper" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" - [[package]] name = "send_wrapper" version = "0.6.0" @@ -7995,13 +8112,34 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 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" @@ -8014,20 +8152,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] name = "serde_json" -version = "1.0.138" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", @@ -8035,34 +8173,15 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_path_to_error" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" -dependencies = [ - "itoa", - "serde", -] - [[package]] name = "serde_repr" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", -] - -[[package]] -name = "serde_spanned" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" -dependencies = [ - "serde", + "syn 2.0.101", ] [[package]] @@ -8077,35 +8196,13 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_with" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" -dependencies = [ - "serde", - "serde_with_macros", -] - -[[package]] -name = "serde_with_macros" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" -dependencies = [ - "darling 0.13.4", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "serde_yaml" version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.9.0", "itoa", "ryu", "serde", @@ -8123,6 +8220,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" @@ -8133,14 +8242,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", @@ -8156,7 +8265,7 @@ dependencies = [ "block-buffer 0.9.0", "digest 0.9.0", "keccak", - "opaque-debug", + "opaque-debug 0.3.1", ] [[package]] @@ -8196,9 +8305,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", ] @@ -8241,9 +8350,9 @@ dependencies = [ [[package]] name = "similar" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "simple_asn1" @@ -8253,7 +8362,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", ] @@ -8263,25 +8372,22 @@ version = "0.2.0" dependencies = [ "clap", "env_logger 0.9.3", + "environment", "eth2_network_config", "execution_layer", "futures", "kzg", + "logging", "node_test_rig", "parking_lot 0.12.3", "rayon", "sensitive_url", "serde_json", "tokio", + "tracing-subscriber", "types", ] -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - [[package]] name = "slab" version = "0.4.9" @@ -8305,7 +8411,6 @@ dependencies = [ "libmdbx", "lmdb-rkv", "lmdb-rkv-sys", - "logging", "lru", "maplit", "metrics", @@ -8315,10 +8420,10 @@ dependencies = [ "redb", "safe_arith", "serde", - "slog", "ssz_types", "strum", "tempfile", + "tracing", "tree_hash", "tree_hash_derive", "types", @@ -8333,11 +8438,11 @@ dependencies = [ "lighthouse_network", "network", "slasher", - "slog", "slot_clock", "state_processing", "task_executor", "tokio", + "tracing", "types", ] @@ -8355,111 +8460,10 @@ dependencies = [ "serde", "serde_json", "tempfile", + "tracing", "types", ] -[[package]] -name = "slog" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" -dependencies = [ - "erased-serde", -] - -[[package]] -name = "slog-async" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c8038f898a2c79507940990f05386455b3a317d8f18d4caea7cbc3d5096b84" -dependencies = [ - "crossbeam-channel", - "slog", - "take_mut", - "thread_local", -] - -[[package]] -name = "slog-json" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1e53f61af1e3c8b852eef0a9dee29008f55d6dd63794f3f12cef786cf0f219" -dependencies = [ - "serde", - "serde_json", - "slog", - "time", -] - -[[package]] -name = "slog-kvfilter" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae939ed7d169eed9699f4f5cd440f046f5dc5dfc27c19e3cd311619594c175e0" -dependencies = [ - "regex", - "slog", -] - -[[package]] -name = "slog-scope" -version = "4.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f95a4b4c3274cd2869549da82b57ccc930859bdbf5bcea0424bc5f140b3c786" -dependencies = [ - "arc-swap", - "lazy_static", - "slog", -] - -[[package]] -name = "slog-stdlog" -version = "4.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6706b2ace5bbae7291d3f8d2473e2bfab073ccd7d03670946197aec98471fa3e" -dependencies = [ - "log", - "slog", - "slog-scope", -] - -[[package]] -name = "slog-term" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6e022d0b998abfe5c3782c1f03551a596269450ccd677ea51c56f8b214610e8" -dependencies = [ - "is-terminal", - "slog", - "term", - "thread_local", - "time", -] - -[[package]] -name = "sloggers" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75062c2738b82cd45ae633623caae3393f43eb00aada1dc2d3ebe88db6b0db9b" -dependencies = [ - "chrono", - "libc", - "libflate", - "once_cell", - "regex", - "serde", - "slog", - "slog-async", - "slog-json", - "slog-kvfilter", - "slog-scope", - "slog-stdlog", - "slog-term", - "trackable", - "winapi", - "windows-acl", -] - [[package]] name = "slot_clock" version = "0.2.0" @@ -8471,9 +8475,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" dependencies = [ "arbitrary", ] @@ -8497,15 +8501,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", @@ -8534,17 +8538,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der 0.7.9", + "der 0.7.10", ] [[package]] name = "ssz_types" -version = "0.8.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e0719d2b86ac738a55ae71a8429f52aa2741da988f1fd0975b4cc610fd1e08" +checksum = "dad0fa7e9a85c06d0a6ba5100d733fff72e231eb6db2d86078225cf716fd2d95" dependencies = [ "arbitrary", - "derivative", "ethereum_serde_utils", "ethereum_ssz", "itertools 0.13.0", @@ -8627,27 +8630,16 @@ dependencies = [ "redb", "safe_arith", "serde", - "slog", - "sloggers", "smallvec", "state_processing", "strum", "superstruct", "tempfile", + "tracing", + "tracing-subscriber", "types", "xdelta3", - "zstd 0.13.2", -] - -[[package]] -name = "stringprep" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", + "zstd 0.13.3", ] [[package]] @@ -8727,9 +8719,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.98" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -8742,21 +8734,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" - [[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.98", + "syn 2.0.101", ] [[package]] @@ -8791,7 +8777,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.0", "core-foundation", "system-configuration-sys 0.6.0", ] @@ -8833,12 +8819,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" -[[package]] -name = "take_mut" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" - [[package]] name = "tap" version = "1.0.1" @@ -8862,66 +8842,43 @@ checksum = "c63f48baada5c52e65a29eef93ab4f8982681b67f9e8d29c7b05abcfec2b9ffe" name = "task_executor" version = "0.1.0" dependencies = [ - "async-channel", + "async-channel 1.9.0", "futures", - "logging", "metrics", - "slog", - "sloggers", "tokio", "tracing", ] [[package]] name = "tempfile" -version = "3.16.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ - "cfg-if", "fastrand", - "getrandom 0.3.1", + "getrandom 0.3.2", "once_cell", - "rustix 0.38.44", + "rustix 1.0.7", "windows-sys 0.59.0", ] -[[package]] -name = "term" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" -dependencies = [ - "dirs-next", - "rustversion", - "winapi", -] - [[package]] name = "termcolor" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ - "winapi-util", -] - -[[package]] -name = "terminal_size" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" -dependencies = [ - "rustix 0.38.44", - "windows-sys 0.59.0", + "winapi-util", ] [[package]] -name = "test-test_logger" -version = "0.1.0" +name = "terminal_size" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" dependencies = [ - "logging", - "slog", + "rustix 1.0.7", + "windows-sys 0.59.0", ] [[package]] @@ -8932,23 +8889,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "testcontainers" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d2931d7f521af5bae989f716c3fa43a6af9af7ec7a5e21b59ae40878cec00" -dependencies = [ - "bollard-stubs", - "futures", - "hex", - "hmac 0.12.1", - "log", - "rand 0.8.5", - "serde", - "serde_json", - "sha2 0.10.8", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -8960,11 +8900,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.12", ] [[package]] @@ -8975,18 +8915,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] @@ -9041,9 +8981,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.37" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", @@ -9056,15 +8996,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.19" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", @@ -9075,10 +9015,10 @@ name = "timer" version = "0.2.0" dependencies = [ "beacon_chain", - "slog", "slot_clock", "task_executor", "tokio", + "tracing", ] [[package]] @@ -9093,7 +9033,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", @@ -9131,9 +9071,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" dependencies = [ "tinyvec_macros", ] @@ -9146,9 +9086,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.43.0" +version = "1.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" dependencies = [ "backtrace", "bytes", @@ -9180,7 +9120,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] @@ -9193,32 +9133,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-postgres" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c95d533c83082bb6490e0189acaa0bbeef9084e60471b696ca6988cd0541fb0" -dependencies = [ - "async-trait", - "byteorder", - "bytes", - "fallible-iterator", - "futures-channel", - "futures-util", - "log", - "parking_lot 0.12.3", - "percent-encoding", - "phf", - "pin-project-lite", - "postgres-protocol", - "postgres-types", - "rand 0.9.0", - "socket2", - "tokio", - "tokio-util", - "whoami", -] - [[package]] name = "tokio-rustls" version = "0.24.1" @@ -9254,9 +9168,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", @@ -9276,26 +9190,11 @@ dependencies = [ "serde", ] -[[package]] -name = "toml" -version = "0.8.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit 0.22.23", -] - [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" -dependencies = [ - "serde", -] +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" [[package]] name = "toml_edit" @@ -9303,46 +9202,22 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.9.0", "toml_datetime", "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.22.23" +version = "0.22.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ - "indexmap 2.7.1", - "serde", - "serde_spanned", + "indexmap 2.9.0", "toml_datetime", - "winnow 0.7.0", -] - -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper 1.0.2", - "tokio", - "tower-layer", - "tower-service", - "tracing", + "winnow 0.7.10", ] -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - [[package]] name = "tower-service" version = "0.3.3" @@ -9381,7 +9256,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] @@ -9415,6 +9290,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.19" @@ -9425,54 +9310,40 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", -] - -[[package]] -name = "trackable" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15bd114abb99ef8cee977e517c8f37aee63f184f2d08e3e6ceca092373369ae" -dependencies = [ - "trackable_derive", -] - -[[package]] -name = "trackable_derive" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebeb235c5847e2f82cfe0f07eb971d1e5f6804b18dac2ae16349cc604380f82f" -dependencies = [ - "quote", - "syn 1.0.109", + "tracing-serde", ] [[package]] name = "tree_hash" -version = "0.8.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373495c23db675a5192de8b610395e1bec324d596f9e6111192ce903dc11403a" +checksum = "6c58eb0f518840670270d90d97ffee702d8662d9c5494870c9e1e9e0fa00f668" dependencies = [ "alloy-primitives", "ethereum_hashing", + "ethereum_ssz", "smallvec", + "typenum", ] [[package]] name = "tree_hash_derive" -version = "0.8.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0857056ca4eb5de8c417309be42bcff6017b47e86fbaddde609b4633f66061e" +checksum = "699e7fb6b3fdfe0c809916f251cf5132d64966858601695c3736630a87e7166a" dependencies = [ - "darling 0.20.10", + "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] @@ -9503,9 +9374,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "types" @@ -9530,7 +9401,6 @@ dependencies = [ "int_to_bytes", "itertools 0.10.5", "kzg", - "log", "maplit", "merkle_proof", "metastruct", @@ -9547,7 +9417,6 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "slog", "smallvec", "ssz_types", "state_processing", @@ -9556,6 +9425,7 @@ dependencies = [ "tempfile", "test_random_derive", "tokio", + "tracing", "tree_hash", "tree_hash_derive", ] @@ -9608,17 +9478,11 @@ version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - [[package]] name = "unicode-ident" -version = "1.0.16" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-normalization" @@ -9630,10 +9494,10 @@ dependencies = [ ] [[package]] -name = "unicode-properties" -version = "0.1.3" +name = "unicode-segmentation" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-xid" @@ -9723,17 +9587,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.12.1" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.3.2", ] [[package]] @@ -9753,6 +9617,7 @@ dependencies = [ "graffiti_file", "hyper 1.6.0", "initialized_validators", + "lighthouse_validator_store", "metrics", "monitoring_api", "parking_lot 0.12.3", @@ -9760,9 +9625,9 @@ dependencies = [ "sensitive_url", "serde", "slashing_protection", - "slog", "slot_clock", "tokio", + "tracing", "types", "validator_http_api", "validator_http_metrics", @@ -9808,15 +9673,16 @@ dependencies = [ "health_metrics", "initialized_validators", "itertools 0.10.5", + "lighthouse_validator_store", "lighthouse_version", "logging", "parking_lot 0.12.3", "rand 0.8.5", "sensitive_url", "serde", + "serde_json", "signing_method", "slashing_protection", - "slog", "slot_clock", "sysinfo", "system_health", @@ -9824,6 +9690,7 @@ dependencies = [ "tempfile", "tokio", "tokio-stream", + "tracing", "types", "url", "validator_dir", @@ -9839,17 +9706,18 @@ name = "validator_http_metrics" version = "0.1.0" dependencies = [ "health_metrics", + "lighthouse_validator_store", "lighthouse_version", + "logging", "malloc_utils", "metrics", "parking_lot 0.12.3", "serde", - "slog", "slot_clock", + "tracing", "types", "validator_metrics", "validator_services", - "validator_store", "warp", "warp_utils", ] @@ -9892,17 +9760,17 @@ version = "0.1.0" dependencies = [ "beacon_node_fallback", "bls", - "doppelganger_service", "either", - "environment", "eth2", "futures", "graffiti_file", + "logging", "parking_lot 0.12.3", "safe_arith", - "slog", "slot_clock", + "task_executor", "tokio", + "tracing", "tree_hash", "types", "validator_metrics", @@ -9913,18 +9781,8 @@ dependencies = [ name = "validator_store" version = "0.1.0" dependencies = [ - "account_utils", - "doppelganger_service", - "initialized_validators", - "parking_lot 0.12.3", - "serde", - "signing_method", "slashing_protection", - "slog", - "slot_clock", - "task_executor", "types", - "validator_metrics", ] [[package]] @@ -9932,12 +9790,11 @@ name = "validator_test_rig" version = "0.1.0" dependencies = [ "eth2", - "logging", "mockito", "regex", "sensitive_url", "serde_json", - "slog", + "tracing", "types", ] @@ -9965,17 +9822,11 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "void" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" - [[package]] name = "wait-timeout" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" dependencies = [ "libc", ] @@ -10035,7 +9886,6 @@ dependencies = [ "bytes", "eth2", "headers", - "metrics", "safe_arith", "serde", "serde_array_query", @@ -10053,19 +9903,13 @@ 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", ] -[[package]] -name = "wasite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" - [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -10088,7 +9932,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", "wasm-bindgen-shared", ] @@ -10123,7 +9967,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -10165,40 +10009,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "watch" -version = "0.1.0" -dependencies = [ - "axum", - "beacon_chain", - "beacon_node", - "bls", - "clap", - "clap_utils", - "diesel", - "diesel_migrations", - "env_logger 0.9.3", - "eth2", - "http_api", - "hyper 1.6.0", - "log", - "logging", - "network", - "r2d2", - "rand 0.8.5", - "reqwest", - "serde", - "serde_json", - "serde_yaml", - "task_executor", - "testcontainers", - "tokio", - "tokio-postgres", - "types", - "unused_port", - "url", -] - [[package]] name = "web-sys" version = "0.3.77" @@ -10224,12 +10034,13 @@ name = "web3signer_tests" version = "0.1.0" dependencies = [ "account_utils", - "async-channel", + "async-channel 1.9.0", "environment", "eth2_keystore", "eth2_network_config", "futures", "initialized_validators", + "lighthouse_validator_store", "logging", "parking_lot 0.12.3", "reqwest", @@ -10265,17 +10076,6 @@ dependencies = [ "rustix 0.38.44", ] -[[package]] -name = "whoami" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" -dependencies = [ - "redox_syscall 0.5.8", - "wasite", - "web-sys", -] - [[package]] name = "widestring" version = "0.4.3" @@ -10284,9 +10084,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" @@ -10351,15 +10151,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" @@ -10376,13 +10167,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" @@ -10391,7 +10195,18 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "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]] @@ -10402,9 +10217,26 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + [[package]] name = "windows-result" version = "0.1.2" @@ -10423,6 +10255,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" @@ -10433,6 +10274,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" @@ -10658,9 +10508,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.0" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e49d2d35d3fad69b39b94139037ecfb4f359f08958b9c11e7315ce770462419" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" dependencies = [ "memchr", ] @@ -10677,11 +10527,19 @@ 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", +] + +[[package]] +name = "workspace_members" +version = "0.1.0" dependencies = [ - "bitflags 2.8.0", + "cargo_metadata 0.19.2", + "quote", ] [[package]] @@ -10708,7 +10566,7 @@ dependencies = [ "log", "pharos", "rustc_version 0.4.1", - "send_wrapper 0.6.0", + "send_wrapper", "thiserror 1.0.69", "wasm-bindgen", "wasm-bindgen-futures", @@ -10775,9 +10633,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" @@ -10859,69 +10717,48 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", "synstructure", ] [[package]] name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "byteorder", - "zerocopy-derive 0.7.35", -] - -[[package]] -name = "zerocopy" -version = "0.8.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" -dependencies = [ - "zerocopy-derive 0.8.14", -] - -[[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 = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", + "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.14" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] name = "zerofrom" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", "synstructure", ] @@ -10943,7 +10780,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] @@ -10965,7 +10802,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.101", ] [[package]] @@ -10999,11 +10836,11 @@ dependencies = [ [[package]] name = "zstd" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" dependencies = [ - "zstd-safe 7.2.1", + "zstd-safe 7.2.4", ] [[package]] @@ -11018,18 +10855,18 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "7.2.1" +version = "7.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.13+zstd.1.5.6" +version = "2.0.15+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 49ea6a1108c..fa1cd9a0fd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,6 @@ members = [ "beacon_node/http_api", "beacon_node/http_metrics", "beacon_node/lighthouse_network", - "beacon_node/lighthouse_network/gossipsub", "beacon_node/network", "beacon_node/operation_pool", "beacon_node/store", @@ -52,6 +51,7 @@ members = [ "common/unused_port", "common/validator_dir", "common/warp_utils", + "common/workspace_members", "consensus/fixed_bytes", "consensus/fork_choice", @@ -85,7 +85,6 @@ members = [ "testing/node_test_rig", "testing/simulator", "testing/state_transition_vectors", - "testing/test-test_logger", "testing/validator_test_rig", "testing/web3signer_tests", @@ -97,15 +96,13 @@ 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", - - "watch", ] resolver = "2" @@ -124,6 +121,7 @@ bincode = "1" bitvec = "1" byteorder = "1" bytes = "1" +cargo_metadata = "0.19" clap = { version = "4.5.4", features = ["derive", "cargo", "wrap_help"] } # Turn off c-kzg's default features which include `blst/portable`. We can turn on blst's portable # feature ourselves when desired. @@ -134,31 +132,36 @@ delay_map = "0.4" derivative = "2" dirs = "3" either = "1.9" -rust_eth_kzg = "0.5.3" +rust_eth_kzg = "0.5.4" discv5 = { version = "0.9", features = ["libp2p"] } env_logger = "0.9" ethereum_hashing = "0.7.0" ethereum_serde_utils = "0.7" -ethereum_ssz = "0.7" -ethereum_ssz_derive = "0.7" +ethereum_ssz = "0.8.2" +ethereum_ssz_derive = "0.8.2" ethers-core = "1" ethers-providers = { version = "1", default-features = false } +ethers-signers = { version = "1", default-features = false } +ethers-middleware = { version = "1", default-features = false } 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 = "61b2820" } hex = "0.4" hashlink = "0.9.0" hyper = "1" itertools = "0.10" libsecp256k1 = "0.7" log = "0.4" +logroller = "0.1.4" lru = "0.12" maplit = "1" -milhouse = "0.3" +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 } @@ -184,17 +187,9 @@ serde_json = "1" serde_repr = "0.1" serde_yaml = "0.9" sha2 = "0.9" -slog = { version = "2", features = [ - "max_level_debug", - "release_max_level_debug", - "nested-values", -] } -slog-async = "2" -slog-term = "2" -sloggers = { version = "2", features = ["json"] } smallvec = { version = "1.11.2", features = ["arbitrary"] } snap = "1" -ssz_types = "0.8" +ssz_types = "0.10" strum = { version = "0.24", features = ["derive"] } superstruct = "0.8" syn = "1" @@ -212,9 +207,9 @@ tracing = "0.1.40" tracing-appender = "0.2" tracing-core = "0.1" tracing-log = "0.2" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -tree_hash = "0.8" -tree_hash_derive = "0.8" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } +tree_hash = "0.9" +tree_hash_derive = "0.9" url = "2" uuid = { version = "0.8", features = ["serde", "v4"] } warp = { version = "0.3.7", default-features = false, features = ["tls"] } @@ -233,7 +228,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" } @@ -248,7 +242,6 @@ fixed_bytes = { path = "consensus/fixed_bytes" } filesystem = { path = "common/filesystem" } fork_choice = { path = "consensus/fork_choice" } genesis = { path = "beacon_node/genesis" } -gossipsub = { path = "beacon_node/lighthouse_network/gossipsub/" } health_metrics = { path = "common/health_metrics" } http_api = { path = "beacon_node/http_api" } initialized_validators = { path = "validator_client/initialized_validators" } @@ -256,7 +249,9 @@ int_to_bytes = { path = "consensus/int_to_bytes" } kzg = { path = "crypto/kzg" } metrics = { path = "common/metrics" } lighthouse_network = { path = "beacon_node/lighthouse_network" } +lighthouse_validator_store = { path = "validator_client/lighthouse_validator_store" } lighthouse_version = { path = "common/lighthouse_version" } +workspace_members = { path = "common/workspace_members" } lockfile = { path = "common/lockfile" } logging = { path = "common/logging" } lru_cache = { path = "common/lru_cache" } @@ -286,6 +281,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" } diff --git a/Cross.toml b/Cross.toml index 8181967f327..391e8751c8a 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/Makefile b/Makefile index f621f38a63e..03bf33a6d85 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,8 @@ 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 # Create a `.tar.gz` containing a binary for a specific target. define tarball_release_binary @@ -95,6 +101,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 a7752d621ff..071e2681dd1 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 ea1a24da1ff..1393d0f1526 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 79dad05886f..30d6846964f 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "beacon_node" -version = "7.0.0" +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_rewards.rs b/beacon_node/beacon_chain/src/attestation_rewards.rs index 46dda286f19..7d88268cf9f 100644 --- a/beacon_node/beacon_chain/src/attestation_rewards.rs +++ b/beacon_node/beacon_chain/src/attestation_rewards.rs @@ -1,10 +1,9 @@ use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; -use eth2::lighthouse::attestation_rewards::{IdealAttestationRewards, TotalAttestationRewards}; -use eth2::lighthouse::StandardAttestationRewards; -use eth2::types::ValidatorId; +use eth2::types::{ + IdealAttestationRewards, StandardAttestationRewards, TotalAttestationRewards, ValidatorId, +}; use safe_arith::SafeArith; use serde_utils::quoted_u64::Quoted; -use slog::debug; use state_processing::common::base::{self, SqrtTotalActiveBalance}; use state_processing::per_epoch_processing::altair::{ process_inactivity_updates_slow, process_justification_and_finalization, @@ -29,6 +28,7 @@ use store::consts::altair::{ PARTICIPATION_FLAG_WEIGHTS, TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX, }; +use tracing::debug; use types::consts::altair::WEIGHT_DENOMINATOR; use types::{BeaconState, Epoch, EthSpec, RelativeEpoch}; @@ -38,7 +38,11 @@ impl BeaconChain { epoch: Epoch, validators: Vec, ) -> Result { - debug!(self.log, "computing attestation rewards"; "epoch" => epoch, "validator_count" => validators.len()); + debug!( + %epoch, + validator_count = validators.len(), + "computing attestation rewards" + ); // Get state let state_slot = (epoch + 1).end_slot(T::EthSpec::slots_per_epoch()); @@ -216,10 +220,9 @@ impl BeaconChain { // Return 0s for unknown/inactive validator indices. let Ok(validator) = state.get_validator(validator_index) else { debug!( - self.log, - "No rewards for inactive/unknown validator"; - "index" => validator_index, - "epoch" => previous_epoch + index = validator_index, + epoch = %previous_epoch, + "No rewards for inactive/unknown validator" ); total_rewards.push(TotalAttestationRewards { validator_index: validator_index as u64, diff --git a/beacon_node/beacon_chain/src/attestation_simulator.rs b/beacon_node/beacon_chain/src/attestation_simulator.rs index c97c4490af0..59d316578b9 100644 --- a/beacon_node/beacon_chain/src/attestation_simulator.rs +++ b/beacon_node/beacon_chain/src/attestation_simulator.rs @@ -1,9 +1,9 @@ use crate::{BeaconChain, BeaconChainTypes}; -use slog::{debug, error}; use slot_clock::SlotClock; use std::sync::Arc; use task_executor::TaskExecutor; use tokio::time::sleep; +use tracing::{debug, error}; use types::{EthSpec, Slot}; /// Don't run the attestation simulator if the head slot is this many epochs @@ -36,10 +36,7 @@ async fn attestation_simulator_service( Some(duration) => { sleep(duration + additional_delay).await; - debug!( - chain.log, - "Simulating unagg. attestation production"; - ); + debug!("Simulating unagg. attestation production"); // Run the task in the executor let inner_chain = chain.clone(); @@ -53,7 +50,7 @@ async fn attestation_simulator_service( ); } None => { - error!(chain.log, "Failed to read slot clock"); + error!("Failed to read slot clock"); // If we can't read the slot clock, just wait another slot. sleep(slot_duration).await; } @@ -85,10 +82,9 @@ pub fn produce_unaggregated_attestation( let data = unaggregated_attestation.data(); debug!( - chain.log, - "Produce unagg. attestation"; - "attestation_source" => data.source.root.to_string(), - "attestation_target" => data.target.root.to_string(), + attestation_source = data.source.root.to_string(), + attestation_target = data.target.root.to_string(), + "Produce unagg. attestation" ); chain @@ -98,9 +94,8 @@ pub fn produce_unaggregated_attestation( } Err(e) => { debug!( - chain.log, - "Failed to simulate attestation"; - "error" => ?e + error = ?e, + "Failed to simulate attestation" ); } } diff --git a/beacon_node/beacon_chain/src/attestation_verification.rs b/beacon_node/beacon_chain/src/attestation_verification.rs index 673a42392d4..6f1174c1ba5 100644 --- a/beacon_node/beacon_chain/src/attestation_verification.rs +++ b/beacon_node/beacon_chain/src/attestation_verification.rs @@ -43,7 +43,6 @@ use crate::{ use bls::verify_signature_sets; use itertools::Itertools; use proto_array::Block as ProtoBlock; -use slog::debug; use slot_clock::SlotClock; use state_processing::{ common::{ @@ -58,6 +57,7 @@ use state_processing::{ }; use std::borrow::Cow; use strum::AsRefStr; +use tracing::debug; use tree_hash::TreeHash; use types::{ Attestation, AttestationData, AttestationRef, BeaconCommittee, @@ -430,10 +430,9 @@ fn process_slash_info( Ok((indexed, _)) => (indexed, true, err), Err(e) => { debug!( - chain.log, - "Unable to obtain indexed form of attestation for slasher"; - "attestation_root" => format!("{:?}", attestation.tree_hash_root()), - "error" => format!("{:?}", e) + attestation_root = ?attestation.tree_hash_root(), + error = ?e, + "Unable to obtain indexed form of attestation for slasher" ); return err; } @@ -447,9 +446,8 @@ fn process_slash_info( if check_signature { if let Err(e) = verify_attestation_signature(chain, &indexed_attestation) { debug!( - chain.log, - "Signature verification for slasher failed"; - "error" => format!("{:?}", e), + error = ?e, + "Signature verification for slasher failed" ); return err; } diff --git a/beacon_node/beacon_chain/src/beacon_block_reward.rs b/beacon_node/beacon_chain/src/beacon_block_reward.rs index fb497901077..ecaa4f45e74 100644 --- a/beacon_node/beacon_chain/src/beacon_block_reward.rs +++ b/beacon_node/beacon_chain/src/beacon_block_reward.rs @@ -1,8 +1,7 @@ use crate::{BeaconChain, BeaconChainError, BeaconChainTypes, StateSkipConfig}; use attesting_indices_base::get_attesting_indices; -use eth2::lighthouse::StandardBlockReward; +use eth2::types::StandardBlockReward; use safe_arith::SafeArith; -use slog::error; use state_processing::common::attesting_indices_base; use state_processing::{ common::{ @@ -19,6 +18,7 @@ use store::{ consts::altair::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}, RelativeEpoch, }; +use tracing::error; use types::{AbstractExecPayload, BeaconBlockRef, BeaconState, BeaconStateError, EthSpec}; type BeaconBlockSubRewardValue = u64; @@ -56,9 +56,8 @@ impl BeaconChain { .compute_beacon_block_proposer_slashing_reward(block, state) .map_err(|e| { error!( - self.log, - "Error calculating proposer slashing reward"; - "error" => ?e + error = ?e, + "Error calculating proposer slashing reward" ); BeaconChainError::BlockRewardError })?; @@ -67,9 +66,8 @@ impl BeaconChain { .compute_beacon_block_attester_slashing_reward(block, state) .map_err(|e| { error!( - self.log, - "Error calculating attester slashing reward"; - "error" => ?e + error = ?e, + "Error calculating attester slashing reward" ); BeaconChainError::BlockRewardError })?; @@ -78,9 +76,8 @@ impl BeaconChain { self.compute_beacon_block_attestation_reward_base(block, state) .map_err(|e| { error!( - self.log, - "Error calculating base block attestation reward"; - "error" => ?e + error = ?e, + "Error calculating base block attestation reward" ); BeaconChainError::BlockRewardAttestationError })? @@ -88,9 +85,8 @@ impl BeaconChain { self.compute_beacon_block_attestation_reward_altair_deneb(block, state) .map_err(|e| { error!( - self.log, - "Error calculating altair block attestation reward"; - "error" => ?e + error = ?e, + "Error calculating altair block attestation reward" ); BeaconChainError::BlockRewardAttestationError })? diff --git a/beacon_node/beacon_chain/src/beacon_block_streamer.rs b/beacon_node/beacon_chain/src/beacon_block_streamer.rs index 32ec7768680..e37a69040db 100644 --- a/beacon_node/beacon_chain/src/beacon_block_streamer.rs +++ b/beacon_node/beacon_chain/src/beacon_block_streamer.rs @@ -1,6 +1,6 @@ use crate::{metrics, BeaconChain, BeaconChainError, BeaconChainTypes, BlockProcessStatus}; use execution_layer::{ExecutionLayer, ExecutionPayloadBodyV1}; -use slog::{crit, debug, error, Logger}; +use logging::crit; use std::collections::HashMap; use std::sync::Arc; use store::{DatabaseBlock, ExecutionPayloadDeneb}; @@ -9,6 +9,7 @@ use tokio::sync::{ RwLock, }; use tokio_stream::{wrappers::UnboundedReceiverStream, Stream}; +use tracing::{debug, error}; use types::{ ChainSpec, EthSpec, ExecPayload, ExecutionBlockHash, ForkName, Hash256, SignedBeaconBlock, SignedBlindedBeaconBlock, Slot, @@ -129,7 +130,6 @@ fn reconstruct_default_header_block( fn reconstruct_blocks( block_map: &mut HashMap>>, block_parts_with_bodies: HashMap>, - log: &Logger, ) { for (root, block_parts) in block_parts_with_bodies { if let Some(payload_body) = block_parts.body { @@ -156,7 +156,7 @@ fn reconstruct_blocks( reconstructed_transactions_root: header_from_payload .transactions_root(), }; - debug!(log, "Failed to reconstruct block"; "root" => ?root, "error" => ?error); + debug!(?root, ?error, "Failed to reconstruct block"); block_map.insert(root, Arc::new(Err(error))); } } @@ -232,7 +232,7 @@ impl BodiesByRange { } } - async fn execute(&mut self, execution_layer: &ExecutionLayer, log: &Logger) { + async fn execute(&mut self, execution_layer: &ExecutionLayer) { if let RequestState::UnSent(blocks_parts_ref) = &mut self.state { let block_parts_vec = std::mem::take(blocks_parts_ref); @@ -261,12 +261,12 @@ impl BodiesByRange { }); } - reconstruct_blocks(&mut block_map, with_bodies, log); + reconstruct_blocks(&mut block_map, with_bodies); } Err(e) => { let block_result = Arc::new(Err(Error::BlocksByRangeFailure(Box::new(e)).into())); - debug!(log, "Payload bodies by range failure"; "error" => ?block_result); + debug!(error = ?block_result, "Payload bodies by range failure"); for block_parts in block_parts_vec { block_map.insert(block_parts.root(), block_result.clone()); } @@ -280,9 +280,8 @@ impl BodiesByRange { &mut self, root: &Hash256, execution_layer: &ExecutionLayer, - log: &Logger, ) -> Option>> { - self.execute(execution_layer, log).await; + self.execute(execution_layer).await; if let RequestState::Sent(map) = &self.state { return map.get(root).cloned(); } @@ -313,7 +312,7 @@ impl EngineRequest { } } - pub async fn push_block_parts(&mut self, block_parts: BlockParts, log: &Logger) { + pub async fn push_block_parts(&mut self, block_parts: BlockParts) { match self { Self::ByRange(bodies_by_range) => { let mut request = bodies_by_range.write().await; @@ -327,28 +326,21 @@ impl EngineRequest { Self::NoRequest(_) => { // this should _never_ happen crit!( - log, - "Please notify the devs"; - "beacon_block_streamer" => "push_block_parts called on NoRequest Variant", + beacon_block_streamer = "push_block_parts called on NoRequest Variant", + "Please notify the devs" ); } } } - pub async fn push_block_result( - &mut self, - root: Hash256, - block_result: BlockResult, - log: &Logger, - ) { + pub async fn push_block_result(&mut self, root: Hash256, block_result: BlockResult) { // this function will only fail if something is seriously wrong match self { Self::ByRange(_) => { // this should _never_ happen crit!( - log, - "Please notify the devs"; - "beacon_block_streamer" => "push_block_result called on ByRange", + beacon_block_streamer = "push_block_result called on ByRange", + "Please notify the devs" ); } Self::NoRequest(results) => { @@ -361,24 +353,22 @@ impl EngineRequest { &self, root: &Hash256, execution_layer: &ExecutionLayer, - log: &Logger, ) -> Arc> { match self { Self::ByRange(by_range) => { by_range .write() .await - .get_block_result(root, execution_layer, log) + .get_block_result(root, execution_layer) .await } Self::NoRequest(map) => map.read().await.get(root).cloned(), } .unwrap_or_else(|| { crit!( - log, - "Please notify the devs"; - "beacon_block_streamer" => "block_result not found in request", - "root" => ?root, + beacon_block_streamer = "block_result not found in request", + ?root, + "Please notify the devs" ); Arc::new(Err(Error::BlockResultNotFound.into())) }) @@ -518,9 +508,7 @@ impl BeaconBlockStreamer { } }; - no_request - .push_block_result(root, block_result, &self.beacon_chain.log) - .await; + no_request.push_block_result(root, block_result).await; requests.insert(root, no_request.clone()); } @@ -529,9 +517,7 @@ impl BeaconBlockStreamer { by_range_blocks.sort_by_key(|block_parts| block_parts.slot()); for block_parts in by_range_blocks { let root = block_parts.root(); - by_range - .push_block_parts(block_parts, &self.beacon_chain.log) - .await; + by_range.push_block_parts(block_parts).await; requests.insert(root, by_range.clone()); } @@ -541,17 +527,12 @@ impl BeaconBlockStreamer { result.push((root, request.clone())) } else { crit!( - self.beacon_chain.log, - "Please notify the devs"; - "beacon_block_streamer" => "request not found", - "root" => ?root, + beacon_block_streamer = "request not found", + ?root, + "Please notify the devs" ); no_request - .push_block_result( - root, - Err(Error::RequestNotFound.into()), - &self.beacon_chain.log, - ) + .push_block_result(root, Err(Error::RequestNotFound.into())) .await; result.push((root, no_request.clone())); } @@ -566,10 +547,7 @@ impl BeaconBlockStreamer { block_roots: Vec, sender: UnboundedSender<(Hash256, Arc>)>, ) { - debug!( - self.beacon_chain.log, - "Using slower fallback method of eth_getBlockByHash()" - ); + debug!("Using slower fallback method of eth_getBlockByHash()"); for root in block_roots { let cached_block = self.check_caches(root); let block_result = if cached_block.is_some() { @@ -601,9 +579,8 @@ impl BeaconBlockStreamer { Ok(payloads) => payloads, Err(e) => { error!( - self.beacon_chain.log, - "BeaconBlockStreamer: Failed to load payloads"; - "error" => ?e + error = ?e, + "BeaconBlockStreamer: Failed to load payloads" ); return; } @@ -615,9 +592,7 @@ impl BeaconBlockStreamer { engine_requests += 1; } - let result = request - .get_block_result(&root, &self.execution_layer, &self.beacon_chain.log) - .await; + let result = request.get_block_result(&root, &self.execution_layer).await; let successful = result .as_ref() @@ -636,13 +611,12 @@ impl BeaconBlockStreamer { } debug!( - self.beacon_chain.log, - "BeaconBlockStreamer finished"; - "requested blocks" => n_roots, - "sent" => n_sent, - "succeeded" => n_success, - "failed" => (n_sent - n_success), - "engine requests" => engine_requests, + requested_blocks = n_roots, + sent = n_sent, + succeeded = n_success, + failed = (n_sent - n_success), + engine_requests, + "BeaconBlockStreamer finished" ); } @@ -678,9 +652,8 @@ impl BeaconBlockStreamer { ) -> impl Stream>)> { let (block_tx, block_rx) = mpsc::unbounded_channel(); debug!( - self.beacon_chain.log, - "Launching a BeaconBlockStreamer"; - "blocks" => block_roots.len(), + blocks = block_roots.len(), + "Launching a BeaconBlockStreamer" ); let executor = self.beacon_chain.task_executor.clone(); executor.spawn(self.stream(block_roots, block_tx), "get_blocks_sender"); @@ -732,7 +705,6 @@ mod tests { let harness = BeaconChainHarness::builder(MinimalEthSpec) .spec(spec) .keypairs(KEYPAIRS[0..validator_count].to_vec()) - .logger(logging::test_logger()) .fresh_ephemeral_store() .mock_execution_layer() .build(); diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 1dcdb077b5f..64ef5ef17e5 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -21,8 +21,8 @@ use crate::block_verification_types::{ pub use crate::canonical_head::CanonicalHead; use crate::chain_config::ChainConfig; use crate::data_availability_checker::{ - Availability, AvailabilityCheckError, AvailableBlock, DataAvailabilityChecker, - DataColumnReconstructionResult, + Availability, AvailabilityCheckError, AvailableBlock, AvailableBlockData, + DataAvailabilityChecker, DataColumnReconstructionResult, }; use crate::data_column_verification::{GossipDataColumnError, GossipVerifiedDataColumn}; use crate::early_attester_cache::EarlyAttesterCache; @@ -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::kzg_utils::reconstruct_blobs; use crate::light_client_finality_update_verification::{ Error as LightClientFinalityUpdateError, VerifiedLightClientFinalityUpdate, @@ -57,7 +57,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}; @@ -86,14 +86,15 @@ use futures::channel::mpsc::Sender; use itertools::process_results; use itertools::Itertools; use kzg::Kzg; +use logging::crit; use operation_pool::{ CompactAttestationRef, OperationPool, PersistedOperationPool, ReceivedPreCapella, }; use parking_lot::{Mutex, RwLock, RwLockWriteGuard}; use proto_array::{DoNotReOrg, ProposerHeadError}; +use rand::RngCore; use safe_arith::SafeArith; use slasher::Slasher; -use slog::{crit, debug, error, info, trace, warn, Logger}; use slot_clock::SlotClock; use ssz::Encode; use state_processing::{ @@ -122,8 +123,8 @@ 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}; @@ -453,8 +454,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 @@ -480,8 +479,6 @@ pub struct BeaconChain { /// Sender given to tasks, so that if they encounter a state in which execution cannot /// continue they can request that everything shuts down. pub shutdown_sender: Sender, - /// Logging to CLI, etc. - pub(crate) log: Logger, /// Arbitrary bytes included in the blocks. pub(crate) graffiti_calculator: GraffitiCalculator, /// Optional slasher. @@ -495,6 +492,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 { @@ -608,57 +607,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. @@ -666,7 +621,6 @@ impl BeaconChain { store: BeaconStore, reset_payload_statuses: ResetPayloadStatuses, spec: &ChainSpec, - log: &Logger, ) -> Result>, Error> { let Some(persisted_fork_choice) = store.get_item::(&FORK_CHOICE_DB_KEY)? @@ -682,7 +636,6 @@ impl BeaconChain { reset_payload_statuses, fc_store, spec, - log, )?)) } @@ -1215,9 +1168,8 @@ impl BeaconChain { if header_from_payload != execution_payload_header { for txn in execution_payload.transactions() { debug!( - self.log, - "Reconstructed txn"; - "bytes" => format!("0x{}", hex::encode(&**txn)), + bytes = format!("0x{}", hex::encode(&**txn)), + "Reconstructed txn" ); } @@ -1433,7 +1385,6 @@ impl BeaconChain { slot, &parent_root, &sync_aggregate, - &self.log, &self.spec, ) } @@ -1455,12 +1406,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. @@ -1479,10 +1431,9 @@ impl BeaconChain { Ordering::Greater => { if slot > head_state.slot() + T::EthSpec::slots_per_epoch() { warn!( - self.log, - "Skipping more than an epoch"; - "head_slot" => head_state.slot(), - "request_slot" => slot + head_slot = %head_state.slot(), + request_slot = %slot, + "Skipping more than an epoch" ) } @@ -1501,11 +1452,10 @@ impl BeaconChain { Ok(_) => (), Err(e) => { warn!( - self.log, - "Unable to load state at slot"; - "error" => ?e, - "head_slot" => head_state_slot, - "requested_slot" => slot + error = ?e, + head_slot= %head_state_slot, + requested_slot = %slot, + "Unable to load state at slot" ); return Err(Error::NoStateForSlot(slot)); } @@ -1742,8 +1692,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); @@ -1914,9 +1862,8 @@ impl BeaconChain { // The cache returned an error. Log the error and proceed with the rest of this // function. Err(e) => warn!( - self.log, - "Early attester cache failed"; - "error" => ?e + error = ?e, + "Early attester cache failed" ), } @@ -2061,11 +2008,10 @@ impl BeaconChain { cached_values } else { debug!( - self.log, - "Attester cache miss"; - "beacon_block_root" => ?beacon_block_root, - "head_state_slot" => %head_state_slot, - "request_slot" => %request_slot, + ?beacon_block_root, + %head_state_slot, + %request_slot, + "Attester cache miss" ); // Neither the head state, nor the attester cache was able to produce the required @@ -2325,30 +2271,27 @@ impl BeaconChain { match self.naive_aggregation_pool.write().insert(attestation) { Ok(outcome) => trace!( - self.log, - "Stored unaggregated attestation"; - "outcome" => ?outcome, - "index" => attestation.committee_index(), - "slot" => attestation.data().slot.as_u64(), + ?outcome, + index = attestation.committee_index(), + slot = attestation.data().slot.as_u64(), + "Stored unaggregated attestation" ), Err(NaiveAggregationError::SlotTooLow { slot, lowest_permissible_slot, }) => { trace!( - self.log, - "Refused to store unaggregated attestation"; - "lowest_permissible_slot" => lowest_permissible_slot.as_u64(), - "slot" => slot.as_u64(), + lowest_permissible_slot = lowest_permissible_slot.as_u64(), + slot = slot.as_u64(), + "Refused to store unaggregated attestation" ); } Err(e) => { error!( - self.log, - "Failed to store unaggregated attestation"; - "error" => ?e, - "index" => attestation.committee_index(), - "slot" => attestation.data().slot.as_u64(), + error = ?e, + index = attestation.committee_index(), + slot = attestation.data().slot.as_u64(), + "Failed to store unaggregated attestation" ); return Err(Error::from(e).into()); } @@ -2388,30 +2331,27 @@ impl BeaconChain { .insert(&contribution) { Ok(outcome) => trace!( - self.log, - "Stored unaggregated sync committee message"; - "outcome" => ?outcome, - "index" => sync_message.validator_index, - "slot" => sync_message.slot.as_u64(), + ?outcome, + index = sync_message.validator_index, + slot = sync_message.slot.as_u64(), + "Stored unaggregated sync committee message" ), Err(NaiveAggregationError::SlotTooLow { slot, lowest_permissible_slot, }) => { trace!( - self.log, - "Refused to store unaggregated sync committee message"; - "lowest_permissible_slot" => lowest_permissible_slot.as_u64(), - "slot" => slot.as_u64(), + lowest_permissible_slot = lowest_permissible_slot.as_u64(), + slot = slot.as_u64(), + "Refused to store unaggregated sync committee message" ); } Err(e) => { error!( - self.log, - "Failed to store unaggregated sync committee message"; - "error" => ?e, - "index" => sync_message.validator_index, - "slot" => sync_message.slot.as_u64(), + error = ?e, + index = sync_message.validator_index, + slot = sync_message.slot.as_u64(), + "Failed to store unaggregated sync committee message" ); return Err(Error::from(e).into()); } @@ -2504,11 +2444,10 @@ impl BeaconChain { self.shuffling_is_compatible_result(block_root, target_epoch, state) .unwrap_or_else(|e| { debug!( - self.log, - "Skipping attestation with incompatible shuffling"; - "block_root" => ?block_root, - "target_epoch" => target_epoch, - "reason" => ?e, + ?block_root, + %target_epoch, + reason = ?e, + "Skipping attestation with incompatible shuffling" ); false }) @@ -2549,11 +2488,10 @@ impl BeaconChain { } } else { debug!( - self.log, - "Skipping attestation with incompatible shuffling"; - "block_root" => ?block_root, - "target_epoch" => target_epoch, - "reason" => "target epoch less than block epoch" + ?block_root, + %target_epoch, + reason = "target epoch less than block epoch", + "Skipping attestation with incompatible shuffling" ); return Ok(false); }; @@ -2562,12 +2500,11 @@ impl BeaconChain { Ok(true) } else { debug!( - self.log, - "Skipping attestation with incompatible shuffling"; - "block_root" => ?block_root, - "target_epoch" => target_epoch, - "head_shuffling_id" => ?head_shuffling_id, - "block_shuffling_id" => ?block_shuffling_id, + ?block_root, + %target_epoch, + ?head_shuffling_id, + ?block_shuffling_id, + "Skipping attestation with incompatible shuffling" ); Ok(false) } @@ -2992,8 +2929,11 @@ impl BeaconChain { imported_blocks.push((block_root, block_slot)); } AvailabilityProcessingStatus::MissingComponents(slot, block_root) => { - warn!(self.log, "Blobs missing in response to range request"; - "block_root" => ?block_root, "slot" => slot); + warn!( + ?block_root, + %slot, + "Blobs missing in response to range request" + ); return ChainSegmentResult::Failed { imported_blocks, error: BlockError::AvailabilityCheck( @@ -3004,9 +2944,10 @@ impl BeaconChain { } } Err(BlockError::DuplicateFullyImported(block_root)) => { - debug!(self.log, - "Ignoring already known blocks while processing chain segment"; - "block_root" => ?block_root); + debug!( + ?block_root, + "Ignoring already known blocks while processing chain segment" + ); continue; } Err(error) => { @@ -3026,11 +2967,10 @@ impl BeaconChain { /// Only completed sampling results are received. Blocks are unavailable by default and should /// be pruned on finalization, on a timeout or by a max count. pub async fn process_sampling_completed(self: &Arc, block_root: Hash256) { - // TODO(das): update fork-choice + // TODO(das): update fork-choice, act on sampling result, adjust log level // NOTE: It is possible that sampling complets before block is imported into fork choice, // in that case we may need to update availability cache. - // TODO(das): These log levels are too high, reduce once DAS matures - info!(self.log, "Sampling completed"; "block_root" => %block_root); + info!(%block_root, "Sampling completed"); } /// Returns `Ok(GossipVerifiedBlock)` if the supplied `block` should be forwarded onto the @@ -3046,6 +2986,7 @@ impl BeaconChain { pub async fn verify_block_for_gossip( self: &Arc, block: Arc>, + custody_columns_count: usize, ) -> Result, BlockError> { let chain = self.clone(); self.task_executor @@ -3055,27 +2996,25 @@ impl BeaconChain { let slot = block.slot(); let graffiti_string = block.message().body().graffiti().as_utf8_lossy(); - match GossipVerifiedBlock::new(block, &chain) { + match GossipVerifiedBlock::new(block, &chain, custody_columns_count) { Ok(verified) => { let commitments_formatted = verified.block.commitments_formatted(); debug!( - chain.log, - "Successfully verified gossip block"; - "graffiti" => graffiti_string, - "slot" => slot, - "root" => ?verified.block_root(), - "commitments" => commitments_formatted, + graffiti = graffiti_string, + %slot, + root = ?verified.block_root(), + commitments = commitments_formatted, + "Successfully verified gossip block" ); Ok(verified) } Err(e) => { debug!( - chain.log, - "Rejected gossip block"; - "error" => e.to_string(), - "graffiti" => graffiti_string, - "slot" => slot, + error = e.to_string(), + graffiti = graffiti_string, + %slot, + "Rejected gossip block" ); Err(e) @@ -3201,16 +3140,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. @@ -3222,10 +3156,14 @@ impl BeaconChain { return Err(BlockError::DuplicateFullyImported(block_root)); } - self.emit_sse_blob_sidecar_events(&block_root, blobs.iter().flatten().map(Arc::as_ref)); + // process_engine_blobs is called for both pre and post PeerDAS. However, post PeerDAS + // consumers don't expect the blobs event to fire erratically. + if let EngineGetBlobsOutput::Blobs(blobs) = &engine_get_blobs_output { + self.emit_sse_blob_sidecar_events(&block_root, blobs.iter().flatten().map(Arc::as_ref)); + } 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) } @@ -3488,11 +3426,10 @@ impl BeaconChain { // The block was successfully verified and imported. Yay. Ok(status @ AvailabilityProcessingStatus::Imported(block_root)) => { debug!( - self.log, - "Beacon block imported"; - "block_root" => ?block_root, - "block_slot" => block_slot, - "source" => %block_source, + ?block_root, + %block_slot, + source = %block_source, + "Beacon block imported" ); // Increment the Prometheus counter for block processing successes. @@ -3501,20 +3438,14 @@ impl BeaconChain { Ok(status) } Ok(status @ AvailabilityProcessingStatus::MissingComponents(slot, block_root)) => { - debug!( - self.log, - "Beacon block awaiting blobs"; - "block_root" => ?block_root, - "block_slot" => slot, - ); + debug!(?block_root, %slot, "Beacon block awaiting blobs"); Ok(status) } Err(e @ BlockError::BeaconChainError(BeaconChainError::TokioJoin(_))) => { debug!( - self.log, - "Beacon block processing cancelled"; - "error" => ?e, + error = ?e, + "Beacon block processing cancelled" ); Err(e) } @@ -3522,19 +3453,14 @@ impl BeaconChain { // be partially verified or partially imported. Err(BlockError::BeaconChainError(e)) => { crit!( - self.log, - "Beacon block processing error"; - "error" => ?e, + error = ?e, + "Beacon block processing error" ); Err(BlockError::BeaconChainError(e)) } // The block failed verification. Err(other) => { - debug!( - self.log, - "Beacon block rejected"; - "reason" => other.to_string(), - ); + debug!(reason = other.to_string(), "Beacon block rejected"); Err(other) } } @@ -3561,31 +3487,24 @@ impl BeaconChain { // Log the PoS pandas if a merge transition just occurred. if payload_verification_outcome.is_valid_merge_transition_block { - info!(self.log, "{}", POS_PANDA_BANNER); - info!( - self.log, - "Proof of Stake Activated"; - "slot" => block.slot() - ); + info!("{}", POS_PANDA_BANNER); + info!(slot = %block.slot(), "Proof of Stake Activated"); info!( - self.log, ""; - "Terminal POW Block Hash" => ?block - .message() - .execution_payload()? - .parent_hash() - .into_root() + terminal_pow_block_hash = ?block + .message() + .execution_payload()? + .parent_hash() + .into_root(), ); info!( - self.log, ""; - "Merge Transition Block Root" => ?block.message().tree_hash_root() + merge_transition_block_root = ?block.message().tree_hash_root(), ); info!( - self.log, ""; - "Merge Transition Execution Hash" => ?block - .message() - .execution_payload()? - .block_hash() - .into_root() + merge_transition_execution_hash = ?block + .message() + .execution_payload()? + .block_hash() + .into_root(), ); } Ok(ExecutedBlock::new( @@ -3694,17 +3613,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, 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 @@ -3718,27 +3644,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. @@ -3750,6 +3656,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(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 @@ -3787,9 +3718,7 @@ impl BeaconChain { state, parent_block, parent_eth1_finalization_data, - confirmed_state_roots, consensus_context, - data_column_recv, } = import_data; // Record the time at which this block's blobs became available. @@ -3812,12 +3741,10 @@ impl BeaconChain { block, block_root, state, - confirmed_state_roots, payload_verification_outcome.payload_verification_status, parent_block, parent_eth1_finalization_data, consensus_context, - data_column_recv, ) }, "payload_verification_handle", @@ -3851,12 +3778,10 @@ 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, mut consensus_context: ConsensusContext, - data_column_recv: Option>>, ) -> Result { // ----------------------------- BLOCK NOT YET ATTESTABLE ---------------------------------- // Everything in this initial section is on the hot path between processing the block and @@ -3954,15 +3879,14 @@ impl BeaconChain { if let Some(proto_block) = fork_choice.get_block(&block_root) { if let Err(e) = self.early_attester_cache.add_head_block( block_root, - signed_block.clone(), + &signed_block, proto_block, &state, &self.spec, ) { warn!( - self.log, - "Early attester cache insert failed"; - "error" => ?e + error = ?e, + "Early attester cache insert failed" ); } else { let attestable_timestamp = @@ -3974,19 +3898,14 @@ impl BeaconChain { ) } } else { - warn!( - self.log, - "Early attester block missing"; - "block_root" => ?block_root - ); + warn!(?block_root, "Early attester block missing"); } } // This block did not become the head, nothing to do. Ok(_) => (), Err(e) => error!( - self.log, - "Failed to compute head during block import"; - "error" => ?e + error = ?e, + "Failed to compute head during block import" ), } drop(fork_choice_timer); @@ -4023,26 +3942,19 @@ impl BeaconChain { // If the write fails, revert fork choice to the version from disk, else we can // end up with blocks in fork choice that are missing from disk. // See https://github.com/sigp/lighthouse/issues/2028 - let (_, signed_block, blobs, data_columns) = signed_block.deconstruct(); + let (_, signed_block, block_data) = signed_block.deconstruct(); - match self.get_blobs_or_columns_store_op( - block_root, - signed_block.epoch(), - blobs, - data_columns, - data_column_recv, - ) { + match self.get_blobs_or_columns_store_op(block_root, block_data) { Ok(Some(blobs_or_columns_store_op)) => { ops.push(blobs_or_columns_store_op); } Ok(None) => {} Err(e) => { error!( - self.log, - "Failed to store data columns into the database"; - "msg" => "Restoring fork choice from disk", - "error" => &e, - "block_root" => ?block_root + msg = "Restoring fork choice from disk", + error = &e, + ?block_root, + "Failed to store data columns into the database" ); return Err(self .handle_import_block_db_write_error(fork_choice) @@ -4053,11 +3965,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)); @@ -4065,10 +3972,9 @@ impl BeaconChain { if let Err(e) = self.store.do_atomically_with_block_and_blobs_cache(ops) { error!( - self.log, - "Database write failed!"; - "msg" => "Restoring fork choice from disk", - "error" => ?e, + msg = "Restoring fork choice from disk", + error = ?e, + "Database write failed!" ); return Err(self .handle_import_block_db_write_error(fork_choice) @@ -4085,9 +3991,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(), @@ -4104,13 +4007,10 @@ impl BeaconChain { &mut state, ) .unwrap_or_else(|e| { - debug!(self.log, "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); @@ -4162,13 +4062,11 @@ impl BeaconChain { ), &self.store, &self.spec, - &self.log, ) { crit!( - self.log, - "No stored fork choice found to restore from"; - "error" => ?e, - "warning" => "The database is likely corrupt now, consider --purge-db" + error = ?e, + warning = "The database is likely corrupt now, consider --purge-db", + "No stored fork choice found to restore from" ); Err(BlockError::BeaconChainError(e)) } else { @@ -4207,17 +4105,15 @@ impl BeaconChain { { let mut shutdown_sender = self.shutdown_sender(); crit!( - self.log, - "Weak subjectivity checkpoint verification failed while importing block!"; - "block_root" => ?block_root, - "parent_root" => ?block.parent_root(), - "old_finalized_epoch" => ?current_head_finalized_checkpoint.epoch, - "new_finalized_epoch" => ?new_finalized_checkpoint.epoch, - "weak_subjectivity_epoch" => ?wss_checkpoint.epoch, - "error" => ?e + ?block_root, + parent_root = ?block.parent_root(), + old_finalized_epoch = ?current_head_finalized_checkpoint.epoch, + new_finalized_epoch = ?new_finalized_checkpoint.epoch, + weak_subjectivity_epoch = ?wss_checkpoint.epoch, + error = ?e, + "Weak subjectivity checkpoint verification failed while importing block!" ); crit!( - self.log, "You must use the `--purge-db` flag to clear the database and restart sync. \ You may be on a hostile network." ); @@ -4286,11 +4182,10 @@ impl BeaconChain { } Err(e) => { warn!( - self.log, - "Unable to fetch sync committee"; - "epoch" => duty_epoch, - "purpose" => "validator monitor", - "error" => ?e, + epoch = %duty_epoch, + purpose = "validator monitor", + error = ?e, + "Unable to fetch sync committee" ); } } @@ -4302,11 +4197,10 @@ impl BeaconChain { Ok(indexed) => indexed, Err(e) => { debug!( - self.log, - "Failed to get indexed attestation"; - "purpose" => "validator monitor", - "attestation_slot" => attestation.data().slot, - "error" => ?e, + purpose = "validator monitor", + attestation_slot = %attestation.data().slot, + error = ?e, + "Failed to get indexed attestation" ); continue; } @@ -4358,10 +4252,9 @@ impl BeaconChain { Ok(_) | Err(AttestationObservationError::SlotTooLow { .. }) => {} Err(e) => { debug!( - self.log, - "Failed to register observed attestation"; - "error" => ?e, - "epoch" => a.data().target.epoch + error = ?e, + epoch = %a.data().target.epoch, + "Failed to register observed attestation" ); } } @@ -4370,11 +4263,10 @@ impl BeaconChain { Ok(indexed) => indexed, Err(e) => { debug!( - self.log, - "Failed to get indexed attestation"; - "purpose" => "observation", - "attestation_slot" => a.data().slot, - "error" => ?e, + purpose = "observation", + attestation_slot = %a.data().slot, + error = ?e, + "Failed to get indexed attestation" ); continue; } @@ -4387,11 +4279,10 @@ impl BeaconChain { .observe_validator(a.data().target.epoch, validator_index as usize) { debug!( - self.log, - "Failed to register observed block attester"; - "error" => ?e, - "epoch" => a.data().target.epoch, - "validator_index" => validator_index, + error = ?e, + epoch = %a.data().target.epoch, + validator_index, + "Failed to register observed block attester" ) } } @@ -4411,11 +4302,10 @@ impl BeaconChain { Ok(indexed) => indexed, Err(e) => { debug!( - self.log, - "Failed to get indexed attestation"; - "purpose" => "slasher", - "attestation_slot" => attestation.data().slot, - "error" => ?e, + purpose = "slasher", + attestation_slot = %attestation.data().slot, + error = ?e, + "Failed to get indexed attestation" ); continue; } @@ -4487,9 +4377,8 @@ impl BeaconChain { sync_aggregate.clone(), )) { warn!( - self.log, - "Failed to send light_client server event"; - "error" => ?e + error = ?e, + "Failed to send light_client server event" ); } } @@ -4506,9 +4395,8 @@ impl BeaconChain { ) { if let Err(e) = self.import_block_update_shuffling_cache_fallible(block_root, state) { warn!( - self.log, - "Failed to prime shuffling cache"; - "error" => ?e + error = ?e, + "Failed to prime shuffling cache" ); } } @@ -4584,10 +4472,9 @@ impl BeaconChain { let finalized_deposit_count = finalized_eth1_data.deposit_count; eth1_chain.finalize_eth1_data(finalized_eth1_data); debug!( - self.log, - "called eth1_chain.finalize_eth1_data()"; - "epoch" => current_finalized_checkpoint.epoch, - "deposit count" => finalized_deposit_count, + epoch = %current_finalized_checkpoint.epoch, + deposit_count = %finalized_deposit_count, + "called eth1_chain.finalize_eth1_data()" ); } } @@ -4610,36 +4497,32 @@ impl BeaconChain { match rx.wait_for_fork_choice(slot, timeout) { ForkChoiceWaitResult::Success(fc_slot) => { debug!( - self.log, - "Fork choice successfully updated before block production"; - "slot" => slot, - "fork_choice_slot" => fc_slot, + %slot, + fork_choice_slot = %fc_slot, + "Fork choice successfully updated before block production" ); } ForkChoiceWaitResult::Behind(fc_slot) => { warn!( - self.log, - "Fork choice notifier out of sync with block production"; - "fork_choice_slot" => fc_slot, - "slot" => slot, - "message" => "this block may be orphaned", + fork_choice_slot = %fc_slot, + %slot, + message = "this block may be orphaned", + "Fork choice notifier out of sync with block production" ); } ForkChoiceWaitResult::TimeOut => { warn!( - self.log, - "Timed out waiting for fork choice before proposal"; - "message" => "this block may be orphaned", + message = "this block may be orphaned", + "Timed out waiting for fork choice before proposal" ); } } } else { error!( - self.log, - "Producing block at incorrect slot"; - "block_slot" => slot, - "current_slot" => current_slot, - "message" => "check clock sync, this block may be orphaned", + %slot, + %current_slot, + message = "check clock sync, this block may be orphaned", + "Producing block at incorrect slot" ); } } @@ -4715,10 +4598,9 @@ impl BeaconChain { self.get_state_for_re_org(slot, head_slot, head_block_root) { info!( - self.log, - "Proposing block to re-org current head"; - "slot" => slot, - "head_to_reorg" => %head_block_root, + %slot, + head_to_reorg = %head_block_root, + "Proposing block to re-org current head" ); (re_org_state, Some(re_org_state_root)) } else { @@ -4733,10 +4615,9 @@ impl BeaconChain { } } else { warn!( - self.log, - "Producing block that conflicts with head"; - "message" => "this block is more likely to be orphaned", - "slot" => slot, + message = "this block is more likely to be orphaned", + %slot, + "Producing block that conflicts with head" ); let state = self .state_at_slot(slot - 1, StateSkipConfig::WithStateRoots) @@ -4764,9 +4645,8 @@ impl BeaconChain { if self.spec.proposer_score_boost.is_none() { warn!( - self.log, - "Ignoring proposer re-org configuration"; - "reason" => "this network does not have proposer boost enabled" + reason = "this network does not have proposer boost enabled", + "Ignoring proposer re-org configuration" ); return None; } @@ -4775,11 +4655,7 @@ impl BeaconChain { .slot_clock .seconds_from_current_slot_start() .or_else(|| { - warn!( - self.log, - "Not attempting re-org"; - "error" => "unable to read slot clock" - ); + warn!(error = "unable to read slot clock", "Not attempting re-org"); None })?; @@ -4790,21 +4666,13 @@ impl BeaconChain { // 3. The `get_proposer_head` conditions from fork choice pass. let proposing_on_time = slot_delay < self.config.re_org_cutoff(self.spec.seconds_per_slot); if !proposing_on_time { - debug!( - self.log, - "Not attempting re-org"; - "reason" => "not proposing on time", - ); + debug!(reason = "not proposing on time", "Not attempting re-org"); return None; } let head_late = self.block_observed_after_attestation_deadline(canonical_head, head_slot); if !head_late { - debug!( - self.log, - "Not attempting re-org"; - "reason" => "head not late" - ); + debug!(reason = "head not late", "Not attempting re-org"); return None; } @@ -4825,16 +4693,14 @@ impl BeaconChain { .map_err(|e| match e { ProposerHeadError::DoNotReOrg(reason) => { debug!( - self.log, - "Not attempting re-org"; - "reason" => %reason, + %reason, + "Not attempting re-org" ); } ProposerHeadError::Error(e) => { warn!( - self.log, - "Not attempting re-org"; - "error" => ?e, + error = ?e, + "Not attempting re-org" ); } }) @@ -4846,21 +4712,16 @@ impl BeaconChain { .store .get_advanced_hot_state_from_cache(re_org_parent_block, slot) .or_else(|| { - warn!( - self.log, - "Not attempting re-org"; - "reason" => "no state in cache" - ); + warn!(reason = "no state in cache", "Not attempting re-org"); None })?; info!( - self.log, - "Attempting re-org due to weak head"; - "weak_head" => ?canonical_head, - "parent" => ?re_org_parent_block, - "head_weight" => proposer_head.head_node.weight, - "threshold_weight" => proposer_head.re_org_head_weight_threshold + weak_head = ?canonical_head, + parent = ?re_org_parent_block, + head_weight = proposer_head.head_node.weight, + threshold_weight = proposer_head.re_org_head_weight_threshold, + "Attempting re-org due to weak head" ); Some((state, state_root)) @@ -4884,10 +4745,9 @@ impl BeaconChain { // The proposer head must be equal to the canonical head or its parent. if proposer_head != head_block_root && proposer_head != head_parent_block_root { warn!( - self.log, - "Unable to compute payload attributes"; - "block_root" => ?proposer_head, - "head_block_root" => ?head_block_root, + block_root = ?proposer_head, + head_block_root = ?head_block_root, + "Unable to compute payload attributes" ); return Ok(None); } @@ -4911,12 +4771,11 @@ impl BeaconChain { } else { if head_epoch + self.config.sync_tolerance_epochs < proposal_epoch { warn!( - self.log, - "Skipping proposer preparation"; - "msg" => "this is a non-critical issue that can happen on unhealthy nodes or \ + msg = "this is a non-critical issue that can happen on unhealthy nodes or \ networks.", - "proposal_epoch" => proposal_epoch, - "head_epoch" => head_epoch, + %proposal_epoch, + %head_epoch, + "Skipping proposer preparation" ); // Don't skip the head forward more than two epochs. This avoids burdening an @@ -4949,10 +4808,7 @@ impl BeaconChain { // // Exit now, after updating the cache. if decision_root != shuffling_decision_root { - warn!( - self.log, - "Head changed during proposer preparation"; - ); + warn!("Head changed during proposer preparation"); return Ok(None); } @@ -5014,10 +4870,9 @@ impl BeaconChain { // Advance the state using the partial method. debug!( - self.log, - "Advancing state for withdrawals calculation"; - "proposal_slot" => proposal_slot, - "parent_block_root" => ?parent_block_root, + %proposal_slot, + ?parent_block_root, + "Advancing state for withdrawals calculation" ); let mut advanced_state = unadvanced_state.into_owned(); partial_state_advance( @@ -5047,9 +4902,8 @@ impl BeaconChain { .or_else(|e| match e { ProposerHeadError::DoNotReOrg(reason) => { trace!( - self.log, - "Not suppressing fork choice update"; - "reason" => %reason, + %reason, + "Not suppressing fork choice update" ); Ok(canonical_forkchoice_params) } @@ -5132,10 +4986,9 @@ impl BeaconChain { .get_slot::(shuffling_decision_root, re_org_block_slot) .ok_or_else(|| { debug!( - self.log, - "Fork choice override proposer shuffling miss"; - "slot" => re_org_block_slot, - "decision_root" => ?shuffling_decision_root, + slot = %re_org_block_slot, + decision_root = ?shuffling_decision_root, + "Fork choice override proposer shuffling miss" ); DoNotReOrg::NotProposing })? @@ -5196,11 +5049,10 @@ impl BeaconChain { }; debug!( - self.log, - "Fork choice update overridden"; - "canonical_head" => ?head_block_root, - "override" => ?info.parent_node.root, - "slot" => fork_choice_slot, + canonical_head = ?head_block_root, + ?info.parent_node.root, + slot = %fork_choice_slot, + "Fork choice update overridden" ); Ok(forkchoice_update_params) @@ -5453,9 +5305,8 @@ impl BeaconChain { if let Err(e) = import(attestation) { // Don't stop block production if there's an error, just create a log. error!( - self.log, - "Attestation did not transfer to op pool"; - "reason" => ?e + reason = ?e, + "Attestation did not transfer to op pool" ); } } @@ -5503,11 +5354,10 @@ impl BeaconChain { ) .map_err(|e| { warn!( - self.log, - "Attempted to include an invalid attestation"; - "err" => ?e, - "block_slot" => state.slot(), - "attestation" => ?att + err = ?e, + block_slot = %state.slot(), + attestation = ?att, + "Attempted to include an invalid attestation" ); }) .is_ok() @@ -5519,11 +5369,10 @@ impl BeaconChain { .validate(&state, &self.spec) .map_err(|e| { warn!( - self.log, - "Attempted to include an invalid proposer slashing"; - "err" => ?e, - "block_slot" => state.slot(), - "slashing" => ?slashing + err = ?e, + block_slot = %state.slot(), + ?slashing, + "Attempted to include an invalid proposer slashing" ); }) .is_ok() @@ -5535,11 +5384,10 @@ impl BeaconChain { .validate(&state, &self.spec) .map_err(|e| { warn!( - self.log, - "Attempted to include an invalid attester slashing"; - "err" => ?e, - "block_slot" => state.slot(), - "slashing" => ?slashing + err = ?e, + block_slot = %state.slot(), + ?slashing, + "Attempted to include an invalid attester slashing" ); }) .is_ok() @@ -5550,11 +5398,10 @@ impl BeaconChain { .validate(&state, &self.spec) .map_err(|e| { warn!( - self.log, - "Attempted to include an invalid proposer slashing"; - "err" => ?e, - "block_slot" => state.slot(), - "exit" => ?exit + err = ?e, + block_slot = %state.slot(), + ?exit, + "Attempted to include an invalid proposer slashing" ); }) .is_ok() @@ -5572,9 +5419,8 @@ impl BeaconChain { .map_err(BlockProductionError::OpPoolError)? .unwrap_or_else(|| { warn!( - self.log, - "Producing block with no sync contributions"; - "slot" => state.slot(), + slot = %state.slot(), + "Producing block with no sync contributions" ); SyncAggregate::new() }); @@ -5894,11 +5740,7 @@ impl BeaconChain { ); let block_size = block.ssz_bytes_len(); - debug!( - self.log, - "Produced block on state"; - "block_size" => block_size, - ); + debug!(%block_size, "Produced block on state"); metrics::observe(&metrics::BLOCK_SIZE, block_size as f64); @@ -5960,15 +5802,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)) } @@ -5980,11 +5833,10 @@ impl BeaconChain { metrics::inc_counter(&metrics::BLOCK_PRODUCTION_SUCCESSES); trace!( - self.log, - "Produced beacon block"; - "parent" => ?block.parent_root(), - "attestations" => block.body().attestations_len(), - "slot" => block.slot() + parent = ?block.parent_root(), + attestations = block.body().attestations_len(), + slot = %block.slot(), + "Produced beacon block" ); Ok(BeaconBlockResponse { @@ -6007,11 +5859,7 @@ impl BeaconChain { self: &Arc, op: &InvalidationOperation, ) -> Result<(), Error> { - debug!( - self.log, - "Processing payload invalidation"; - "op" => ?op, - ); + debug!(?op, "Processing payload invalidation"); // Update the execution status in fork choice. // @@ -6034,11 +5882,10 @@ impl BeaconChain { // Update fork choice. if let Err(e) = fork_choice_result { crit!( - self.log, - "Failed to process invalid payload"; - "error" => ?e, - "latest_valid_ancestor" => ?op.latest_valid_ancestor(), - "block_root" => ?op.block_root(), + error = ?e, + latest_valid_ancestor = ?op.latest_valid_ancestor(), + block_root = ?op.block_root(), + "Failed to process invalid payload" ); } @@ -6065,10 +5912,9 @@ impl BeaconChain { if justified_block.execution_status.is_invalid() { crit!( - self.log, - "The justified checkpoint is invalid"; - "msg" => "ensure you are not connected to a malicious network. This error is not \ - recoverable, please reach out to the lighthouse developers for assistance." + msg = "ensure you are not connected to a malicious network. This error is not \ + recoverable, please reach out to the lighthouse developers for assistance.", + "The justified checkpoint is invalid" ); let mut shutdown_sender = self.shutdown_sender(); @@ -6076,10 +5922,9 @@ impl BeaconChain { INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON, )) { crit!( - self.log, - "Unable to trigger client shut down"; - "msg" => "shut down may already be under way", - "error" => ?e + msg = "shut down may already be under way", + error = ?e, + "Unable to trigger client shut down" ); } @@ -6152,12 +5997,7 @@ impl BeaconChain { // This prevents the routine from running during sync. let head_slot = cached_head.head_slot(); if head_slot + tolerance_slots < current_slot { - debug!( - chain.log, - "Head too old for proposer prep"; - "head_slot" => head_slot, - "current_slot" => current_slot, - ); + debug!(%head_slot, %current_slot, "Head too old for proposer prep"); return Ok(None); } @@ -6245,11 +6085,10 @@ impl BeaconChain { // Only push a log to the user if this is the first time we've seen this proposer for // this slot. info!( - self.log, - "Prepared beacon proposer"; - "prepare_slot" => prepare_slot, - "validator" => proposer, - "parent_root" => ?head_root, + %prepare_slot, + validator = proposer, + parent_root = ?head_root, + "Prepared beacon proposer" ); payload_attributes }; @@ -6279,10 +6118,9 @@ impl BeaconChain { // // This scenario might occur on an overloaded/under-resourced node. warn!( - self.log, - "Delayed proposer preparation"; - "prepare_slot" => prepare_slot, - "validator" => proposer, + %prepare_slot, + validator = proposer, + "Delayed proposer preparation" ); return Ok(None); }; @@ -6293,10 +6131,9 @@ impl BeaconChain { || till_prepare_slot <= self.config.prepare_payload_lookahead { debug!( - self.log, - "Sending forkchoiceUpdate for proposer prep"; - "till_prepare_slot" => ?till_prepare_slot, - "prepare_slot" => prepare_slot + ?till_prepare_slot, + %prepare_slot, + "Sending forkchoiceUpdate for proposer prep" ); self.update_execution_engine_forkchoice( @@ -6398,8 +6235,8 @@ impl BeaconChain { .map_err(Error::ForkchoiceUpdate)? { info!( - self.log, - "Prepared POS transition block proposer"; "slot" => next_slot + slot = %next_slot, + "Prepared POS transition block proposer" ); ( params.head_root, @@ -6457,9 +6294,8 @@ impl BeaconChain { .await?; if let Err(e) = fork_choice_update_result { error!( - self.log, - "Failed to validate payload"; - "error" => ?e + error= ?e, + "Failed to validate payload" ) }; Ok(()) @@ -6473,11 +6309,10 @@ impl BeaconChain { // error. However, we create a log to bring attention to the issue. PayloadStatus::Accepted => { warn!( - self.log, - "Fork choice update received ACCEPTED"; - "msg" => "execution engine provided an unexpected response to a fork \ + msg = "execution engine provided an unexpected response to a fork \ choice update. although this is not a serious issue, please raise \ - an issue." + an issue.", + "Fork choice update received ACCEPTED" ); Ok(()) } @@ -6486,13 +6321,12 @@ impl BeaconChain { ref validation_error, } => { warn!( - self.log, - "Invalid execution payload"; - "validation_error" => ?validation_error, - "latest_valid_hash" => ?latest_valid_hash, - "head_hash" => ?head_hash, - "head_block_root" => ?head_block_root, - "method" => "fcU", + ?validation_error, + ?latest_valid_hash, + ?head_hash, + head_block_root = ?head_block_root, + method = "fcU", + "Invalid execution payload" ); match latest_valid_hash { @@ -6540,12 +6374,11 @@ impl BeaconChain { ref validation_error, } => { warn!( - self.log, - "Invalid execution payload block hash"; - "validation_error" => ?validation_error, - "head_hash" => ?head_hash, - "head_block_root" => ?head_block_root, - "method" => "fcU", + ?validation_error, + ?head_hash, + ?head_block_root, + method = "fcU", + "Invalid execution payload block hash" ); // The execution engine has stated that the head block is invalid, however it // hasn't returned a latest valid ancestor. @@ -6660,16 +6493,19 @@ impl BeaconChain { state: &BeaconState, ) -> Result<(), BeaconChainError> { let finalized_checkpoint = state.finalized_checkpoint(); - info!(self.log, "Verifying the configured weak subjectivity checkpoint"; "weak_subjectivity_epoch" => wss_checkpoint.epoch, "weak_subjectivity_root" => ?wss_checkpoint.root); + info!( + weak_subjectivity_epoch = %wss_checkpoint.epoch, + weak_subjectivity_root = ?wss_checkpoint.root, + "Verifying the configured weak subjectivity checkpoint" + ); // If epochs match, simply compare roots. if wss_checkpoint.epoch == finalized_checkpoint.epoch && wss_checkpoint.root != finalized_checkpoint.root { crit!( - self.log, - "Root found at the specified checkpoint differs"; - "weak_subjectivity_root" => ?wss_checkpoint.root, - "finalized_checkpoint_root" => ?finalized_checkpoint.root + weak_subjectivity_root = ?wss_checkpoint.root, + finalized_checkpoint_root = ?finalized_checkpoint.root, + "Root found at the specified checkpoint differs" ); return Err(BeaconChainError::WeakSubjectivtyVerificationFailure); } else if wss_checkpoint.epoch < finalized_checkpoint.epoch { @@ -6683,17 +6519,18 @@ impl BeaconChain { Some(root) => { if root != wss_checkpoint.root { crit!( - self.log, - "Root found at the specified checkpoint differs"; - "weak_subjectivity_root" => ?wss_checkpoint.root, - "finalized_checkpoint_root" => ?finalized_checkpoint.root + weak_subjectivity_root = ?wss_checkpoint.root, + finalized_checkpoint_root = ?finalized_checkpoint.root, + "Root found at the specified checkpoint differs" ); return Err(BeaconChainError::WeakSubjectivtyVerificationFailure); } } None => { - crit!(self.log, "The root at the start slot of the given epoch could not be found"; - "wss_checkpoint_slot" => ?slot); + crit!( + wss_checkpoint_slot = ?slot, + "The root at the start slot of the given epoch could not be found" + ); return Err(BeaconChainError::WeakSubjectivtyVerificationFailure); } } @@ -6708,11 +6545,7 @@ impl BeaconChain { /// `tokio::runtime::block_on` in certain cases. pub async fn per_slot_task(self: &Arc) { if let Some(slot) = self.slot_clock.now() { - debug!( - self.log, - "Running beacon chain per slot tasks"; - "slot" => ?slot - ); + debug!(?slot, "Running beacon chain per slot tasks"); // Always run the light-weight pruning tasks (these structures should be empty during // sync anyway). @@ -6738,10 +6571,9 @@ impl BeaconChain { if let Some(tx) = &chain.fork_choice_signal_tx { if let Err(e) = tx.notify_fork_choice_complete(slot) { warn!( - chain.log, - "Error signalling fork choice waiter"; - "error" => ?e, - "slot" => slot, + error = ?e, + %slot, + "Error signalling fork choice waiter" ); } } @@ -6834,10 +6666,9 @@ impl BeaconChain { drop(shuffling_cache); debug!( - self.log, - "Committee cache miss"; - "shuffling_id" => ?shuffling_epoch, - "head_block_root" => head_block_root.to_string(), + shuffling_id = ?shuffling_epoch, + head_block_root = head_block_root.to_string(), + "Committee cache miss" ); // If the block's state will be so far ahead of `shuffling_epoch` that even its @@ -7252,10 +7083,6 @@ impl BeaconChain { .is_peer_das_enabled_for_epoch(slot.epoch(T::EthSpec::slots_per_epoch())) } - pub fn logger(&self) -> &Logger { - &self.log - } - /// Gets the `LightClientBootstrap` object for a requested block root. /// /// Returns `None` when the state or block is not found in the database. @@ -7283,66 +7110,30 @@ impl BeaconChain { } } - fn get_blobs_or_columns_store_op( + pub(crate) fn get_blobs_or_columns_store_op( &self, block_root: Hash256, - block_epoch: Epoch, - blobs: Option>, - data_columns: Option>, - data_column_recv: Option>>, + block_data: AvailableBlockData, ) -> Result>, String> { - if self.spec.is_peer_das_enabled_for_epoch(block_epoch) { - // TODO(das) we currently store all subnet sampled columns. Tracking issue to exclude non - // custody columns: https://github.com/sigp/lighthouse/issues/6465 - let custody_columns_count = self.data_availability_checker.get_sampling_column_count(); - - let custody_columns_available = data_columns - .as_ref() - .as_ref() - .is_some_and(|columns| columns.len() == custody_columns_count); - - let data_columns_to_persist = if custody_columns_available { - // If the block was made available via custody columns received from gossip / rpc, use them - // since we already have them. - data_columns - } else if let Some(data_column_recv) = 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:?}"))?; - Some(computed_data_columns) - } else { - // No blobs in the block. - None - }; - - if let Some(data_columns) = data_columns_to_persist { - if !data_columns.is_empty() { - debug!( - self.log, "Writing data_columns to store"; - "block_root" => %block_root, - "count" => data_columns.len(), - ); - return Ok(Some(StoreOp::PutDataColumns(block_root, data_columns))); - } + match block_data { + AvailableBlockData::NoData => Ok(None), + AvailableBlockData::Blobs(blobs) => { + debug!( + %block_root, + count = blobs.len(), + "Writing blobs to store" + ); + Ok(Some(StoreOp::PutBlobs(block_root, blobs))) } - } else if let Some(blobs) = blobs { - if !blobs.is_empty() { + AvailableBlockData::DataColumns(data_columns) => { debug!( - self.log, "Writing blobs to store"; - "block_root" => %block_root, - "count" => blobs.len(), + %block_root, + count = data_columns.len(), + "Writing data columns to store" ); - return Ok(Some(StoreOp::PutBlobs(block_root, blobs))); + Ok(Some(StoreOp::PutDataColumns(block_root, data_columns))) } } - - Ok(None) } /// Retrieves block roots (in ascending slot order) within some slot range from fork choice. @@ -7374,22 +7165,18 @@ impl BeaconChain { 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() }; if let Err(e) = drop() { error!( - self.log, - "Failed to persist on BeaconChain drop"; - "error" => ?e + error = ?e, + "Failed to persist on BeaconChain drop" ) } else { - info!( - self.log, - "Saved beacon chain to disk"; - ) + info!("Saved beacon chain to disk") } } } diff --git a/beacon_node/beacon_chain/src/beacon_proposer_cache.rs b/beacon_node/beacon_chain/src/beacon_proposer_cache.rs index 567433caee1..56b13b0b772 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 786b627bb7e..fe9d8c6bfcb 100644 --- a/beacon_node/beacon_chain/src/blob_verification.rs +++ b/beacon_node/beacon_chain/src/blob_verification.rs @@ -12,9 +12,9 @@ use crate::kzg_utils::{validate_blob, validate_blobs}; use crate::observed_data_sidecars::{DoNotObserve, ObservationStrategy, Observe}; use crate::{metrics, BeaconChainError}; use kzg::{Error as KzgError, Kzg, KzgCommitment}; -use slog::debug; use ssz_derive::{Decode, Encode}; use std::time::Duration; +use tracing::debug; use tree_hash::TreeHash; use types::blob_sidecar::BlobIdentifier; use types::{ @@ -504,10 +504,9 @@ pub fn validate_blob_sidecar_for_gossip %block_root, - "index" => %blob_index, + %block_root, + %blob_index, + "Proposer shuffling cache miss for blob verification" ); let (parent_state_root, mut parent_state) = chain .store diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 48caea9c7ff..074ae93a790 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -74,7 +74,6 @@ use metrics::TryExt; use parking_lot::RwLockReadGuard; use proto_array::Block as ProtoBlock; use safe_arith::ArithError; -use slog::{debug, error, Logger}; use slot_clock::SlotClock; use ssz::Encode; use ssz_derive::{Decode, Encode}; @@ -94,11 +93,12 @@ use std::sync::Arc; use store::{Error as DBError, HotStateSummary, KeyValueStore, StoreOp}; use strum::AsRefStr; use task_executor::JoinHandle; +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#" @@ -683,6 +683,7 @@ pub struct GossipVerifiedBlock { pub block_root: Hash256, parent: Option>, consensus_context: ConsensusContext, + custody_columns_count: usize, } /// A wrapper around a `SignedBeaconBlock` that indicates that all signatures (except the deposit @@ -718,6 +719,7 @@ pub trait IntoGossipVerifiedBlock: Sized { fn into_gossip_verified_block( self, chain: &BeaconChain, + custody_columns_count: usize, ) -> Result, BlockError>; fn inner_block(&self) -> Arc>; } @@ -726,6 +728,7 @@ impl IntoGossipVerifiedBlock for GossipVerifiedBlock fn into_gossip_verified_block( self, _chain: &BeaconChain, + _custody_columns_count: usize, ) -> Result, BlockError> { Ok(self) } @@ -738,8 +741,9 @@ impl IntoGossipVerifiedBlock for Arc, + custody_columns_count: usize, ) -> Result, BlockError> { - GossipVerifiedBlock::new(self, chain) + GossipVerifiedBlock::new(self, chain, custody_columns_count) } fn inner_block(&self) -> Arc> { @@ -751,6 +755,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() { @@ -762,8 +767,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) } @@ -808,6 +819,7 @@ impl GossipVerifiedBlock { pub fn new( block: Arc>, chain: &BeaconChain, + custody_columns_count: usize, ) -> Result { // If the block is valid for gossip we don't supply it to the slasher here because // we assume it will be transformed into a fully verified block. We *do* need to supply @@ -817,12 +829,14 @@ impl GossipVerifiedBlock { // The `SignedBeaconBlock` and `SignedBeaconBlockHeader` have the same canonical root, // but it's way quicker to calculate root of the header since the hash of the tree rooted // at `BeaconBlockBody` is already computed in the header. - Self::new_without_slasher_checks(block, &header, chain).map_err(|e| { - process_block_slash_info::<_, BlockError>( - chain, - BlockSlashInfo::from_early_error_block(header, e), - ) - }) + Self::new_without_slasher_checks(block, &header, chain, custody_columns_count).map_err( + |e| { + process_block_slash_info::<_, BlockError>( + chain, + BlockSlashInfo::from_early_error_block(header, e), + ) + }, + ) } /// As for new, but doesn't pass the block to the slasher. @@ -830,6 +844,7 @@ impl GossipVerifiedBlock { block: Arc>, block_header: &SignedBeaconBlockHeader, chain: &BeaconChain, + custody_columns_count: usize, ) -> Result { // Ensure the block is the correct structure for the fork at `block.slot()`. block @@ -930,12 +945,11 @@ impl GossipVerifiedBlock { let (mut parent, block) = load_parent(block, chain)?; debug!( - chain.log, - "Proposer shuffling cache miss"; - "parent_root" => ?parent.beacon_block_root, - "parent_slot" => parent.beacon_block.slot(), - "block_root" => ?block_root, - "block_slot" => block.slot(), + parent_root = ?parent.beacon_block_root, + parent_slot = %parent.beacon_block.slot(), + ?block_root, + block_slot = %block.slot(), + "Proposer shuffling cache miss" ); // The state produced is only valid for determining proposer/attester shuffling indices. @@ -1037,6 +1051,7 @@ impl GossipVerifiedBlock { block_root, parent, consensus_context, + custody_columns_count, }) } @@ -1184,6 +1199,7 @@ impl SignatureVerifiedBlock { block: MaybeAvailableBlock::AvailabilityPending { block_root: from.block_root, block, + custody_columns_count: from.custody_columns_count, }, block_root: from.block_root, parent: Some(parent), @@ -1251,40 +1267,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. @@ -1444,22 +1426,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 { @@ -1506,38 +1474,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 }; @@ -1545,10 +1504,9 @@ impl ExecutionPendingBlock { // Expose Prometheus metrics. if let Err(e) = summary.observe_metrics() { error!( - chain.log, - "Failed to observe epoch summary metrics"; - "src" => "block_verification", - "error" => ?e + src = "block_verification", + error = ?e, + "Failed to observe epoch summary metrics" ); } summaries.push(summary); @@ -1576,9 +1534,8 @@ impl ExecutionPendingBlock { validator_monitor.process_validator_statuses(epoch, summary, &chain.spec) { error!( - chain.log, - "Failed to process validator statuses"; - "error" => ?e + error = ?e, + "Failed to process validator statuses" ); } } @@ -1618,12 +1575,8 @@ impl ExecutionPendingBlock { * invalid. */ - write_state( - &format!("state_pre_block_{}", block_root), - &state, - &chain.log, - ); - write_block(block.as_block(), block_root, &chain.log); + write_state(&format!("state_pre_block_{}", block_root), &state); + write_block(block.as_block(), block_root); let core_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_CORE); @@ -1656,11 +1609,7 @@ impl ExecutionPendingBlock { metrics::stop_timer(state_root_timer); - write_state( - &format!("state_post_block_{}", block_root), - &state, - &chain.log, - ); + write_state(&format!("state_post_block_{}", block_root), &state); /* * Check to ensure the state root on the block matches the one we have calculated. @@ -1714,9 +1663,7 @@ impl ExecutionPendingBlock { state, parent_block: parent.beacon_block, parent_eth1_finalization_data, - confirmed_state_roots, consensus_context, - data_column_recv: None, }, payload_verification_handle, }) @@ -1967,19 +1914,17 @@ fn load_parent>( if !state.all_caches_built() { debug!( - chain.log, - "Parent state lacks built caches"; - "block_slot" => block.slot(), - "state_slot" => state.slot(), + block_slot = %block.slot(), + state_slot = %state.slot(), + "Parent state lacks built caches" ); } if block.slot() != state.slot() { debug!( - chain.log, - "Parent state is not advanced"; - "block_slot" => block.slot(), - "state_slot" => state.slot(), + block_slot = %block.slot(), + state_slot = %state.slot(), + "Parent state is not advanced" ); } @@ -2185,14 +2130,11 @@ pub fn verify_header_signature( } } -fn write_state(prefix: &str, state: &BeaconState, log: &Logger) { +fn write_state(prefix: &str, state: &BeaconState) { if WRITE_BLOCK_PROCESSING_SSZ { let mut state = state.clone(); let Ok(root) = state.canonical_root() else { - error!( - log, - "Unable to hash state for writing"; - ); + error!("Unable to hash state for writing"); return; }; let filename = format!("{}_slot_{}_root_{}.ssz", prefix, state.slot(), root); @@ -2205,16 +2147,15 @@ fn write_state(prefix: &str, state: &BeaconState, log: &Logger) { let _ = file.write_all(&state.as_ssz_bytes()); } Err(e) => error!( - log, - "Failed to log state"; - "path" => format!("{:?}", path), - "error" => format!("{:?}", e) + ?path, + error = ?e, + "Failed to log state" ), } } } -fn write_block(block: &SignedBeaconBlock, root: Hash256, log: &Logger) { +fn write_block(block: &SignedBeaconBlock, root: Hash256) { if WRITE_BLOCK_PROCESSING_SSZ { let filename = format!("block_slot_{}_root{}.ssz", block.slot(), root); let mut path = std::env::temp_dir().join("lighthouse"); @@ -2226,10 +2167,9 @@ fn write_block(block: &SignedBeaconBlock, root: Hash256, log: &Lo let _ = file.write_all(&block.as_ssz_bytes()); } Err(e) => error!( - log, - "Failed to log block"; - "path" => format!("{:?}", path), - "error" => format!("{:?}", e) + ?path, + error = ?e, + "Failed to log block" ), } } diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index 38d0fc708ca..dab54dc823e 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -7,11 +7,10 @@ use derivative::Derivative; use state_processing::ConsensusContext; use std::fmt::{Debug, Formatter}; use std::sync::Arc; -use tokio::sync::oneshot; use types::blob_sidecar::BlobIdentifier; use types::{ - BeaconBlockRef, BeaconState, BlindedPayload, BlobSidecarList, ChainSpec, DataColumnSidecarList, - Epoch, EthSpec, Hash256, RuntimeVariableList, SignedBeaconBlock, SignedBeaconBlockHeader, Slot, + BeaconBlockRef, BeaconState, BlindedPayload, BlobSidecarList, ChainSpec, Epoch, EthSpec, + Hash256, RuntimeVariableList, SignedBeaconBlock, SignedBeaconBlockHeader, Slot, }; /// A block that has been received over RPC. It has 2 internal variants: @@ -32,6 +31,7 @@ use types::{ pub struct RpcBlock { block_root: Hash256, block: RpcBlockInner, + custody_columns_count: usize, } impl Debug for RpcBlock { @@ -45,6 +45,10 @@ impl RpcBlock { self.block_root } + pub fn custody_columns_count(&self) -> usize { + self.custody_columns_count + } + pub fn as_block(&self) -> &SignedBeaconBlock { match &self.block { RpcBlockInner::Block(block) => block, @@ -99,12 +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), + custody_columns_count, } } @@ -146,6 +152,8 @@ impl RpcBlock { Ok(Self { block_root, block: inner, + // Block is before PeerDAS + custody_columns_count: 0, }) } @@ -153,6 +161,7 @@ impl RpcBlock { block_root: Option, block: Arc>, custody_columns: Vec>, + custody_columns_count: usize, spec: &ChainSpec, ) -> Result { let block_root = block_root.unwrap_or_else(|| get_block_root(&block)); @@ -173,6 +182,7 @@ impl RpcBlock { Ok(Self { block_root, block: inner, + custody_columns_count, }) } @@ -240,10 +250,12 @@ impl ExecutedBlock { MaybeAvailableBlock::AvailabilityPending { block_root: _, block: pending_block, + custody_columns_count, } => Self::AvailabilityPending(AvailabilityPendingExecutedBlock::new( pending_block, import_data, payload_verification_outcome, + custody_columns_count, )), } } @@ -265,7 +277,6 @@ impl ExecutedBlock { /// A block that has completed all pre-deneb block processing checks including verification /// by an EL client **and** has all requisite blob data to be imported into fork choice. -#[derive(PartialEq)] pub struct AvailableExecutedBlock { pub block: AvailableBlock, pub import_data: BlockImportData, @@ -310,6 +321,7 @@ pub struct AvailabilityPendingExecutedBlock { pub block: Arc>, pub import_data: BlockImportData, pub payload_verification_outcome: PayloadVerificationOutcome, + pub custody_columns_count: usize, } impl AvailabilityPendingExecutedBlock { @@ -317,11 +329,13 @@ impl AvailabilityPendingExecutedBlock { block: Arc>, import_data: BlockImportData, payload_verification_outcome: PayloadVerificationOutcome, + custody_columns_count: usize, ) -> Self { Self { block, import_data, payload_verification_outcome, + custody_columns_count, } } @@ -338,21 +352,13 @@ impl AvailabilityPendingExecutedBlock { } } -#[derive(Debug, Derivative)] -#[derivative(PartialEq)] +#[derive(Debug, PartialEq)] pub struct BlockImportData { pub block_root: Hash256, pub state: BeaconState, pub parent_block: SignedBeaconBlock>, pub parent_eth1_finalization_data: Eth1FinalizationData, - pub confirmed_state_roots: Vec, pub consensus_context: ConsensusContext, - #[derivative(PartialEq = "ignore")] - /// An optional receiver for `DataColumnSidecarList`. - /// - /// This field is `Some` when data columns are being computed asynchronously. - /// The resulting `DataColumnSidecarList` will be sent through this receiver. - pub data_column_recv: Option>>, } impl BlockImportData { @@ -369,9 +375,7 @@ impl BlockImportData { eth1_data: <_>::default(), eth1_deposit_index: 0, }, - confirmed_state_roots: vec![], consensus_context: ConsensusContext::new(Slot::new(0)), - data_column_recv: None, } } } @@ -449,19 +453,13 @@ impl AsBlock for MaybeAvailableBlock { fn as_block(&self) -> &SignedBeaconBlock { match &self { MaybeAvailableBlock::Available(block) => block.as_block(), - MaybeAvailableBlock::AvailabilityPending { - block_root: _, - block, - } => block, + MaybeAvailableBlock::AvailabilityPending { block, .. } => block, } } fn block_cloned(&self) -> Arc> { match &self { MaybeAvailableBlock::Available(block) => block.block_cloned(), - MaybeAvailableBlock::AvailabilityPending { - block_root: _, - block, - } => block.clone(), + MaybeAvailableBlock::AvailabilityPending { block, .. } => block.clone(), } } fn canonical_root(&self) -> Hash256 { diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index a05b5e4ea54..812dcbeda7d 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; @@ -27,11 +26,13 @@ use execution_layer::ExecutionLayer; use fork_choice::{ForkChoice, ResetPayloadStatuses}; use futures::channel::mpsc::Sender; use kzg::Kzg; +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 slog::{crit, debug, error, info, o, Logger}; use slot_clock::{SlotClock, TestingSlotClock}; use state_processing::{per_slot_processing, AllCaches}; use std::marker::PhantomData; @@ -39,9 +40,10 @@ use std::sync::Arc; use std::time::Duration; use store::{Error as StoreError, HotColdDB, ItemStore, KeyValueStoreOp}; use task_executor::{ShutdownReason, TaskExecutor}; +use tracing::{debug, error, info}; use types::{ - 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 @@ -92,11 +94,9 @@ 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, - log: Option, beacon_graffiti: GraffitiOrigin, slasher: Option>>, // Pending I/O batch that is constructed during building and should be executed atomically @@ -106,6 +106,7 @@ pub struct BeaconChainBuilder { task_executor: Option, validator_monitor_config: Option, import_all_data_columns: bool, + rng: Option>, } impl @@ -136,11 +137,9 @@ 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(), - log: None, beacon_graffiti: GraffitiOrigin::default(), slasher: None, pending_io_batch: vec![], @@ -148,6 +147,7 @@ where task_executor: None, validator_monitor_config: None, import_all_data_columns: false, + rng: None, } } @@ -218,14 +218,6 @@ where self } - /// Sets the logger. - /// - /// Should generally be called early in the build chain. - pub fn logger(mut self, log: Logger) -> Self { - self.log = Some(log); - self - } - /// Sets the task executor. pub fn task_executor(mut self, task_executor: TaskExecutor) -> Self { self.task_executor = Some(task_executor); @@ -261,13 +253,7 @@ where /// /// May initialize several components; including the op_pool and finalized checkpoints. pub fn resume_from_db(mut self) -> Result { - let log = self.log.as_ref().ok_or("resume_from_db requires a log")?; - - info!( - log, - "Starting beacon chain"; - "method" => "resume" - ); + info!(method = "resume", "Starting beacon chain"); let store = self .store @@ -289,7 +275,6 @@ where self.chain_config.always_reset_payload_statuses, ), &self.spec, - log, ) .map_err(|e| format!("Unable to load fork choice from disk: {:?}", e))? .ok_or("Fork choice not found in store")?; @@ -330,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); @@ -456,19 +437,14 @@ where .store .clone() .ok_or("weak_subjectivity_state requires a store")?; - let log = self - .log - .as_ref() - .ok_or("weak_subjectivity_state requires a log")?; // Ensure the state is advanced to an epoch boundary. let slots_per_epoch = E::slots_per_epoch(); if weak_subj_state.slot() % slots_per_epoch != 0 { debug!( - log, - "Advancing checkpoint state to boundary"; - "state_slot" => weak_subj_state.slot(), - "block_slot" => weak_subj_block.slot(), + state_slot = %weak_subj_state.slot(), + block_slot = %weak_subj_block.slot(), + "Advancing checkpoint state to boundary" ); while weak_subj_state.slot() % slots_per_epoch != 0 { per_slot_processing(&mut weak_subj_state, None, &self.spec) @@ -574,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) @@ -725,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. @@ -736,7 +713,6 @@ where mut self, ) -> Result>, String> { - let log = self.log.ok_or("Cannot build without a logger")?; let slot_clock = self .slot_clock .ok_or("Cannot build without a slot_clock.")?; @@ -751,14 +727,11 @@ 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 = ValidatorMonitor::new( - validator_monitor_config, - beacon_proposer_cache.clone(), - log.new(o!("service" => "val_mon")), - ); + let mut validator_monitor = + ValidatorMonitor::new(validator_monitor_config, beacon_proposer_cache.clone()); let current_slot = if slot_clock .is_prior_to_genesis() @@ -781,23 +754,19 @@ where Ok(None) => return Err("Head block not found in store".into()), Err(StoreError::SszDecodeError(_)) => { error!( - log, - "Error decoding head block"; - "message" => "This node has likely missed a hard fork. \ - It will try to revert the invalid blocks and keep running, \ - but any stray blocks and states will not be deleted. \ - Long-term you should consider re-syncing this node." + message = "This node has likely missed a hard fork. \ + It will try to revert the invalid blocks and keep running, \ + but any stray blocks and states will not be deleted. \ + Long-term you should consider re-syncing this node.", + "Error decoding head block" ); let (block_root, block) = revert_to_fork_boundary( current_slot, initial_head_block_root, store.clone(), &self.spec, - &log, )?; - // 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)), @@ -858,9 +827,8 @@ where if !pubkey_store_ops.is_empty() { // Write any missed validators to disk debug!( - store.log, - "Topping up validator pubkey cache"; - "missing_validators" => pubkey_store_ops.len() + missing_validators = pubkey_store_ops.len(), + "Topping up validator pubkey cache" ); store .do_atomically_with_block_and_blobs_cache(pubkey_store_ops) @@ -874,12 +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, - log.clone(), - ); + let store_migrator = BackgroundMigrator::new(store.clone(), migrator_config); if let Some(slot) = slot_clock.now() { validator_monitor.process_valid_state( @@ -904,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, @@ -919,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(); @@ -1000,13 +961,11 @@ 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, - log.clone(), )), - eth1_finalization_cache: RwLock::new(Eth1FinalizationCache::new(log.clone())), + eth1_finalization_cache: RwLock::new(Eth1FinalizationCache::default()), beacon_proposer_cache, block_times_cache: <_>::default(), pre_finalization_block_cache: <_>::default(), @@ -1019,28 +978,20 @@ where shutdown_sender: self .shutdown_sender .ok_or("Cannot build without a shutdown sender.")?, - log: log.clone(), graffiti_calculator: GraffitiCalculator::new( self.beacon_graffiti, self.execution_layer, slot_clock.slot_duration() * E::slots_per_epoch() as u32, - log.clone(), ), slasher: self.slasher.clone(), validator_monitor: RwLock::new(validator_monitor), genesis_backfill_slot, data_availability_checker: Arc::new( - DataAvailabilityChecker::new( - slot_clock, - self.kzg.clone(), - store, - self.import_all_data_columns, - self.spec, - log.new(o!("service" => "data_availability_checker")), - ) - .map_err(|e| format!("Error initializing DataAvailabilityChecker: {:?}", e))?, + DataAvailabilityChecker::new(slot_clock, self.kzg.clone(), store, self.spec) + .map_err(|e| format!("Error initializing DataAvailabilityChecker: {:?}", e))?, ), kzg: self.kzg.clone(), + rng: Arc::new(Mutex::new(rng)), }; let head = beacon_chain.head_snapshot(); @@ -1063,25 +1014,23 @@ where &head.beacon_state, ) { crit!( - log, - "Weak subjectivity checkpoint verification failed on startup!"; - "head_block_root" => format!("{}", head.beacon_block_root), - "head_slot" => format!("{}", head.beacon_block.slot()), - "finalized_epoch" => format!("{}", head.beacon_state.finalized_checkpoint().epoch), - "wss_checkpoint_epoch" => format!("{}", wss_checkpoint.epoch), - "error" => format!("{:?}", e), + head_block_root = %head.beacon_block_root, + head_slot = %head.beacon_block.slot(), + finalized_epoch = %head.beacon_state.finalized_checkpoint().epoch, + wss_checkpoint_epoch = %wss_checkpoint.epoch, + error = ?e, + "Weak subjectivity checkpoint verification failed on startup!" ); - crit!(log, "You must use the `--purge-db` flag to clear the database and restart sync. You may be on a hostile network."); + crit!("You must use the `--purge-db` flag to clear the database and restart sync. You may be on a hostile network."); return Err(format!("Weak subjectivity verification failed: {:?}", e)); } } info!( - log, - "Beacon chain initialized"; - "head_state" => format!("{}", head.beacon_state_root()), - "head_block" => format!("{}", head.beacon_block_root), - "head_slot" => format!("{}", head.beacon_block.slot()), + head_state = %head.beacon_state_root(), + head_block = %head.beacon_block_root, + head_slot = %head.beacon_block.slot(), + "Beacon chain initialized" ); // Check for states to reconstruct (in the background). @@ -1094,11 +1043,10 @@ where // Prune finalized execution payloads in the background. if beacon_chain.store.get_config().prune_payloads { let store = beacon_chain.store.clone(); - let log = log.clone(); beacon_chain.task_executor.spawn_blocking( move || { if let Err(e) = store.try_prune_execution_payloads(false) { - error!(log, "Error pruning payloads in background"; "error" => ?e); + error!( error = ?e,"Error pruning payloads in background"); } }, "prune_payloads_background", @@ -1131,13 +1079,7 @@ where /// Sets the `BeaconChain` eth1 back-end to produce predictably junk data when producing blocks. pub fn dummy_eth1_backend(mut self) -> Result { - let log = self - .log - .as_ref() - .ok_or("dummy_eth1_backend requires a log")?; - - let backend = - CachingEth1Backend::new(Eth1Config::default(), log.clone(), self.spec.clone())?; + let backend = CachingEth1Backend::new(Eth1Config::default(), self.spec.clone())?; self.eth1_chain = Some(Eth1Chain::new_dummy(backend)); @@ -1203,6 +1145,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 { @@ -1212,7 +1197,8 @@ mod test { use genesis::{ generate_deterministic_keypairs, interop_genesis_state, DEFAULT_ETH1_BLOCK_HASH, }; - use sloggers::{null::NullLoggerBuilder, Build}; + use rand::rngs::StdRng; + use rand::SeedableRng; use ssz::Encode; use std::time::Duration; use store::config::StoreConfig; @@ -1223,27 +1209,16 @@ mod test { type TestEthSpec = MinimalEthSpec; type Builder = BeaconChainBuilder>; - fn get_logger() -> Logger { - let builder = NullLoggerBuilder; - builder.build().expect("should build logger") - } - #[test] fn recent_genesis() { let validator_count = 1; let genesis_time = 13_371_337; - let log = get_logger(); let store: HotColdDB< MinimalEthSpec, MemoryStore, MemoryStore, - > = HotColdDB::open_ephemeral( - StoreConfig::default(), - ChainSpec::minimal().into(), - log.clone(), - ) - .unwrap(); + > = HotColdDB::open_ephemeral(StoreConfig::default(), ChainSpec::minimal().into()).unwrap(); let spec = MinimalEthSpec::default_spec(); let genesis_state = interop_genesis_state( @@ -1261,7 +1236,6 @@ mod test { let kzg = get_kzg(&spec); let chain = Builder::new(MinimalEthSpec, kzg) - .logger(log.clone()) .store(Arc::new(store)) .task_executor(runtime.task_executor.clone()) .genesis_state(genesis_state) @@ -1271,6 +1245,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 4d2ff11b38e..a6f5179fdc6 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -47,14 +47,15 @@ use fork_choice::{ ResetPayloadStatuses, }; use itertools::process_results; +use logging::crit; use parking_lot::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; -use slog::{crit, debug, error, info, warn, Logger}; 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::*; /// Simple wrapper around `RwLock` that uses private visibility to prevent any other modules from @@ -286,10 +287,9 @@ impl CanonicalHead { reset_payload_statuses: ResetPayloadStatuses, store: &BeaconStore, spec: &ChainSpec, - log: &Logger, ) -> Result<(), Error> { let fork_choice = - >::load_fork_choice(store.clone(), reset_payload_statuses, spec, log)? + >::load_fork_choice(store.clone(), reset_payload_statuses, spec)? .ok_or(Error::MissingPersistedForkChoice)?; let fork_choice_view = fork_choice.cached_fork_choice_view(); let beacon_block_root = fork_choice_view.head_block_root; @@ -475,9 +475,8 @@ impl BeaconChain { match self.slot() { Ok(current_slot) => self.recompute_head_at_slot(current_slot).await, Err(e) => error!( - self.log, - "No slot when recomputing head"; - "error" => ?e + error = ?e, + "No slot when recomputing head" ), } } @@ -515,18 +514,13 @@ impl BeaconChain { Ok(Some(())) => (), // The async task did not complete successfully since the runtime is shutting down. Ok(None) => { - debug!( - self.log, - "Did not update EL fork choice"; - "info" => "shutting down" - ); + debug!(info = "shutting down", "Did not update EL fork choice"); } // The async task did not complete successfully, tokio returned an error. Err(e) => { error!( - self.log, - "Did not update EL fork choice"; - "error" => ?e + error = ?e, + "Did not update EL fork choice" ); } }, @@ -534,17 +528,15 @@ impl BeaconChain { Ok(Err(e)) => { metrics::inc_counter(&metrics::FORK_CHOICE_ERRORS); error!( - self.log, - "Error whist recomputing head"; - "error" => ?e + error = ?e, + "Error whist recomputing head" ); } // There was an error spawning the task. Err(e) => { error!( - self.log, - "Failed to spawn recompute head task"; - "error" => ?e + error = ?e, + "Failed to spawn recompute head task" ); } } @@ -627,9 +619,8 @@ impl BeaconChain { // nothing to do. if new_view == old_view { debug!( - self.log, - "No change in canonical head"; - "head" => ?new_view.head_block_root + head = ?new_view.head_block_root, + "No change in canonical head" ); return Ok(None); } @@ -639,7 +630,7 @@ impl BeaconChain { let new_forkchoice_update_parameters = fork_choice_read_lock.get_forkchoice_update_parameters(); - perform_debug_logging::(&old_view, &new_view, &fork_choice_read_lock, &self.log); + perform_debug_logging::(&old_view, &new_view, &fork_choice_read_lock); // Drop the read lock, it's no longer required and holding it any longer than necessary // will just cause lock contention. @@ -732,9 +723,8 @@ impl BeaconChain { self.after_new_head(&old_cached_head, &new_cached_head, new_head_proto_block) { crit!( - self.log, - "Error updating canonical head"; - "error" => ?e + error = ?e, + "Error updating canonical head" ); } } @@ -751,9 +741,8 @@ impl BeaconChain { self.after_finalization(&new_cached_head, new_view, finalized_proto_block) { crit!( - self.log, - "Error updating finalization"; - "error" => ?e + error = ?e, + "Error updating finalization" ); } } @@ -797,7 +786,6 @@ impl BeaconChain { &new_snapshot.beacon_state, new_snapshot.beacon_block_root, &self.spec, - &self.log, ); // Determine if the new head is in a later epoch to the previous head. @@ -830,10 +818,9 @@ impl BeaconChain { .update_head_shuffling_ids(head_shuffling_ids), Err(e) => { error!( - self.log, - "Failed to get head shuffling ids"; - "error" => ?e, - "head_block_root" => ?new_snapshot.beacon_block_root + error = ?e, + head_block_root = ?new_snapshot.beacon_block_root, + "Failed to get head shuffling ids" ); } } @@ -850,11 +837,10 @@ impl BeaconChain { .as_utf8_lossy(), &self.slot_clock, self.event_handler.as_ref(), - &self.log, ); 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()?); } @@ -878,9 +864,8 @@ impl BeaconChain { } (Err(e), _) | (_, Err(e)) => { warn!( - self.log, - "Unable to find dependent roots, cannot register head event"; - "error" => ?e + error = ?e, + "Unable to find dependent roots, cannot register head event" ); } } @@ -998,7 +983,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. @@ -1013,6 +997,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()) @@ -1043,11 +1035,10 @@ fn check_finalized_payload_validity( ) -> Result<(), Error> { if let ExecutionStatus::Invalid(block_hash) = finalized_proto_block.execution_status { crit!( - chain.log, - "Finalized block has an invalid payload"; - "msg" => "You must use the `--purge-db` flag to clear the database and restart sync. \ + ?block_hash, + msg = "You must use the `--purge-db` flag to clear the database and restart sync. \ You may be on a hostile network.", - "block_hash" => ?block_hash + "Finalized block has an invalid payload" ); let mut shutdown_sender = chain.shutdown_sender(); shutdown_sender @@ -1089,38 +1080,34 @@ fn perform_debug_logging( old_view: &ForkChoiceView, new_view: &ForkChoiceView, fork_choice: &BeaconForkChoice, - log: &Logger, ) { if new_view.head_block_root != old_view.head_block_root { debug!( - log, - "Fork choice updated head"; - "new_head_weight" => ?fork_choice - .get_block_weight(&new_view.head_block_root), - "new_head" => ?new_view.head_block_root, - "old_head_weight" => ?fork_choice - .get_block_weight(&old_view.head_block_root), - "old_head" => ?old_view.head_block_root, + new_head_weight = ?fork_choice + .get_block_weight(&new_view.head_block_root), + new_head = ?new_view.head_block_root, + old_head_weight = ?fork_choice + .get_block_weight(&old_view.head_block_root), + old_head = ?old_view.head_block_root, + "Fork choice updated head" ) } if new_view.justified_checkpoint != old_view.justified_checkpoint { debug!( - log, - "Fork choice justified"; - "new_root" => ?new_view.justified_checkpoint.root, - "new_epoch" => new_view.justified_checkpoint.epoch, - "old_root" => ?old_view.justified_checkpoint.root, - "old_epoch" => old_view.justified_checkpoint.epoch, + new_root = ?new_view.justified_checkpoint.root, + new_epoch = %new_view.justified_checkpoint.epoch, + old_root = ?old_view.justified_checkpoint.root, + old_epoch = %old_view.justified_checkpoint.epoch, + "Fork choice justified" ) } if new_view.finalized_checkpoint != old_view.finalized_checkpoint { debug!( - log, - "Fork choice finalized"; - "new_root" => ?new_view.finalized_checkpoint.root, - "new_epoch" => new_view.finalized_checkpoint.epoch, - "old_root" => ?old_view.finalized_checkpoint.root, - "old_epoch" => old_view.finalized_checkpoint.epoch, + new_root = ?new_view.finalized_checkpoint.root, + new_epoch = %new_view.finalized_checkpoint.epoch, + old_root = ?old_view.finalized_checkpoint.root, + old_epoch = %old_view.finalized_checkpoint.epoch, + "Fork choice finalized" ) } } @@ -1155,9 +1142,8 @@ fn spawn_execution_layer_updates( .await { crit!( - chain.log, - "Failed to update execution head"; - "error" => ?e + error = ?e, + "Failed to update execution head" ); } @@ -1171,9 +1157,8 @@ fn spawn_execution_layer_updates( // know. if let Err(e) = chain.prepare_beacon_proposer(current_slot).await { crit!( - chain.log, - "Failed to prepare proposers after fork choice"; - "error" => ?e + error = ?e, + "Failed to prepare proposers after fork choice" ); } }, @@ -1194,7 +1179,6 @@ fn detect_reorg( new_state: &BeaconState, new_block_root: Hash256, spec: &ChainSpec, - log: &Logger, ) -> Option { let is_reorg = new_state .get_block_root(old_state.slot()) @@ -1205,11 +1189,7 @@ fn detect_reorg( match find_reorg_slot(old_state, old_block_root, new_state, new_block_root, spec) { Ok(slot) => old_state.slot().saturating_sub(slot), Err(e) => { - warn!( - log, - "Could not find re-org depth"; - "error" => format!("{:?}", e), - ); + warn!(error = ?e, "Could not find re-org depth"); return None; } }; @@ -1221,13 +1201,12 @@ fn detect_reorg( reorg_distance.as_u64() as i64, ); info!( - log, - "Beacon chain re-org"; - "previous_head" => ?old_block_root, - "previous_slot" => old_state.slot(), - "new_head" => ?new_block_root, - "new_slot" => new_state.slot(), - "reorg_distance" => reorg_distance, + previous_head = ?old_block_root, + previous_slot = %old_state.slot(), + new_head = ?new_block_root, + new_slot = %new_state.slot(), + %reorg_distance, + "Beacon chain re-org" ); Some(reorg_distance) @@ -1307,7 +1286,6 @@ fn observe_head_block_delays( head_block_graffiti: String, slot_clock: &S, event_handler: Option<&ServerSentEventHandler>, - log: &Logger, ) { let block_time_set_as_head = timestamp_now(); let head_block_root = head_block.root; @@ -1440,37 +1418,35 @@ fn observe_head_block_delays( if late_head { metrics::inc_counter(&metrics::BEACON_BLOCK_DELAY_HEAD_SLOT_START_EXCEEDED_TOTAL); debug!( - log, - "Delayed head block"; - "block_root" => ?head_block_root, - "proposer_index" => head_block_proposer_index, - "slot" => head_block_slot, - "total_delay_ms" => block_delay_total.as_millis(), - "observed_delay_ms" => format_delay(&block_delays.observed), - "blob_delay_ms" => format_delay(&block_delays.all_blobs_observed), - "consensus_time_ms" => format_delay(&block_delays.consensus_verification_time), - "execution_time_ms" => format_delay(&block_delays.execution_time), - "available_delay_ms" => format_delay(&block_delays.available), - "attestable_delay_ms" => format_delay(&block_delays.attestable), - "imported_time_ms" => format_delay(&block_delays.imported), - "set_as_head_time_ms" => format_delay(&block_delays.set_as_head), + block_root = ?head_block_root, + proposer_index = head_block_proposer_index, + slot = %head_block_slot, + total_delay_ms = block_delay_total.as_millis(), + observed_delay_ms = format_delay(&block_delays.observed), + blob_delay_ms = format_delay(&block_delays.all_blobs_observed), + consensus_time_ms = format_delay(&block_delays.consensus_verification_time), + execution_time_ms = format_delay(&block_delays.execution_time), + available_delay_ms = format_delay(&block_delays.available), + attestable_delay_ms = format_delay(&block_delays.attestable), + imported_time_ms = format_delay(&block_delays.imported), + set_as_head_time_ms = format_delay(&block_delays.set_as_head), + "Delayed head block" ); } else { debug!( - log, - "On-time head block"; - "block_root" => ?head_block_root, - "proposer_index" => head_block_proposer_index, - "slot" => head_block_slot, - "total_delay_ms" => block_delay_total.as_millis(), - "observed_delay_ms" => format_delay(&block_delays.observed), - "blob_delay_ms" => format_delay(&block_delays.all_blobs_observed), - "consensus_time_ms" => format_delay(&block_delays.consensus_verification_time), - "execution_time_ms" => format_delay(&block_delays.execution_time), - "available_delay_ms" => format_delay(&block_delays.available), - "attestable_delay_ms" => format_delay(&block_delays.attestable), - "imported_time_ms" => format_delay(&block_delays.imported), - "set_as_head_time_ms" => format_delay(&block_delays.set_as_head), + block_root = ?head_block_root, + proposer_index = head_block_proposer_index, + slot = %head_block_slot, + total_delay_ms = block_delay_total.as_millis(), + observed_delay_ms = format_delay(&block_delays.observed), + blob_delay_ms = format_delay(&block_delays.all_blobs_observed), + consensus_time_ms = format_delay(&block_delays.consensus_verification_time), + execution_time_ms = format_delay(&block_delays.execution_time), + available_delay_ms = format_delay(&block_delays.available), + attestable_delay_ms = format_delay(&block_delays.attestable), + imported_time_ms = format_delay(&block_delays.imported), + set_as_head_time_ms = format_delay(&block_delays.set_as_head), + "On-time head block" ); } } diff --git a/beacon_node/beacon_chain/src/chain_config.rs b/beacon_node/beacon_chain/src/chain_config.rs index d45f0b5cc87..808c96d9650 100644 --- a/beacon_node/beacon_chain/src/chain_config.rs +++ b/beacon_node/beacon_chain/src/chain_config.rs @@ -107,6 +107,10 @@ pub struct ChainConfig { /// The max distance between the head block and the current slot at which Lighthouse will /// consider itself synced and still serve validator-related requests. pub sync_tolerance_epochs: u64, + /// Artificial delay for block publishing. For PeerDAS testing only. + pub block_publishing_delay: Option, + /// Artificial delay for data column publishing. For PeerDAS testing only. + pub data_column_publishing_delay: Option, /// Block roots of "banned" blocks which Lighthouse will refuse to import. /// /// On Holesky there is a block which is added to this set by default but which can be removed @@ -148,6 +152,8 @@ impl Default for ChainConfig { blob_publication_batches: 4, blob_publication_batch_interval: Duration::from_millis(300), sync_tolerance_epochs: DEFAULT_SYNC_TOLERANCE_EPOCHS, + block_publishing_delay: None, + data_column_publishing_delay: None, invalid_block_roots: HashSet::new(), } } diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index f10d59ca1a5..033b472da0c 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -7,7 +7,6 @@ use crate::data_availability_checker::overflow_lru_cache::{ }; use crate::{metrics, BeaconChain, BeaconChainTypes, BeaconStore}; use kzg::Kzg; -use slog::{debug, error, Logger}; use slot_clock::SlotClock; use std::fmt; use std::fmt::Debug; @@ -15,7 +14,7 @@ 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, @@ -75,7 +74,6 @@ pub struct DataAvailabilityChecker { slot_clock: T::SlotClock, kzg: Arc, spec: Arc, - log: Logger, } pub type AvailabilityAndReconstructedColumns = (Availability, DataColumnSidecarList); @@ -91,7 +89,6 @@ pub enum DataColumnReconstructionResult { /// /// Indicates if the block is fully `Available` or if we need blobs or blocks /// to "complete" the requirements for an `AvailableBlock`. -#[derive(PartialEq)] pub enum Availability { MissingComponents(Hash256), Available(Box>), @@ -113,39 +110,17 @@ impl DataAvailabilityChecker { slot_clock: T::SlotClock, kzg: Arc, store: BeaconStore, - import_all_data_columns: bool, spec: Arc, - log: Logger, ) -> Result { - let custody_group_count = spec.custody_group_count(import_all_data_columns); - // This should only panic if the chain spec contains invalid values. - let sampling_size = spec - .sampling_size(custody_group_count) - .expect("should compute node sampling size from valid chain spec"); - - let inner = DataAvailabilityCheckerInner::new( - OVERFLOW_LRU_CAPACITY, - store, - sampling_size as usize, - spec.clone(), - )?; + let inner = DataAvailabilityCheckerInner::new(OVERFLOW_LRU_CAPACITY, store, spec.clone())?; Ok(Self { availability_cache: Arc::new(inner), slot_clock, kzg, spec, - log, }) } - pub fn get_sampling_column_count(&self) -> usize { - self.availability_cache.sampling_column_count() - } - - pub(crate) fn is_supernode(&self) -> bool { - self.get_sampling_column_count() == self.spec.number_of_columns as usize - } - /// Checks if the block root is currenlty in the availability cache awaiting import because /// of missing components. pub fn get_execution_valid_block( @@ -219,7 +194,7 @@ impl DataAvailabilityChecker { .map_err(AvailabilityCheckError::InvalidBlobs)?; self.availability_cache - .put_kzg_verified_blobs(block_root, verified_blobs, None, &self.log) + .put_kzg_verified_blobs(block_root, verified_blobs) } /// Put a list of custody columns received via RPC into the availability cache. This performs KZG @@ -239,11 +214,8 @@ impl DataAvailabilityChecker { .map(KzgVerifiedCustodyDataColumn::from_asserted_custody) .collect::>(); - self.availability_cache.put_kzg_verified_data_columns( - block_root, - verified_custody_columns, - &self.log, - ) + self.availability_cache + .put_kzg_verified_data_columns(block_root, verified_custody_columns) } /// Put a list of blobs received from the EL pool into the availability cache. @@ -254,24 +226,46 @@ impl DataAvailabilityChecker { &self, block_root: Hash256, blobs: FixedBlobSidecarList, - data_column_recv: Option>>, ) -> Result, AvailabilityCheckError> { let seen_timestamp = self .slot_clock .now_duration() .ok_or(AvailabilityCheckError::SlotClockError)?; - - let verified_blobs = - KzgVerifiedBlobList::from_verified(blobs.iter().flatten().cloned(), seen_timestamp); - self.availability_cache.put_kzg_verified_blobs( block_root, - verified_blobs, - data_column_recv, - &self.log, + KzgVerifiedBlobList::from_verified(blobs.iter().flatten().cloned(), seen_timestamp), ) } + /// Put a list of data columns computed from blobs received from the EL pool into the + /// availability cache. + /// + /// This DOES NOT perform KZG proof and inclusion proof verification because + /// - The KZG proofs should have been verified by the trusted EL. + /// - The KZG commitments inclusion proof should have been constructed immediately prior to + /// calling this function so they are assumed to be valid. + /// + /// This method is used if the EL already has the blobs and returns them via the `getBlobsV2` + /// engine method. + /// More details in [fetch_blobs.rs](https://github.com/sigp/lighthouse/blob/44f8add41ea2252769bb967864af95b3c13af8ca/beacon_node/beacon_chain/src/fetch_blobs.rs). + pub fn put_engine_data_columns( + &self, + block_root: Hash256, + data_columns: DataColumnSidecarList, + ) -> Result, AvailabilityCheckError> { + let kzg_verified_custody_columns = data_columns + .into_iter() + .map(|d| { + KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::from_verified(d), + ) + }) + .collect::>(); + + self.availability_cache + .put_kzg_verified_data_columns(block_root, kzg_verified_custody_columns) + } + /// Check if we've cached other blobs for this block. If it completes a set and we also /// have a block cached, return the `Availability` variant triggering block import. /// Otherwise cache the blob sidecar. @@ -281,12 +275,8 @@ impl DataAvailabilityChecker { &self, gossip_blob: GossipVerifiedBlob, ) -> Result, AvailabilityCheckError> { - self.availability_cache.put_kzg_verified_blobs( - gossip_blob.block_root(), - vec![gossip_blob.into_inner()], - None, - &self.log, - ) + self.availability_cache + .put_kzg_verified_blobs(gossip_blob.block_root(), vec![gossip_blob.into_inner()]) } /// Check if we've cached other data columns for this block. If it satisfies the custody requirement and we also @@ -305,11 +295,8 @@ impl DataAvailabilityChecker { .map(|c| KzgVerifiedCustodyDataColumn::from_asserted_custody(c.into_inner())) .collect::>(); - self.availability_cache.put_kzg_verified_data_columns( - block_root, - custody_columns, - &self.log, - ) + self.availability_cache + .put_kzg_verified_data_columns(block_root, custody_columns) } /// Check if we have all the blobs for a block. Returns `Availability` which has information @@ -319,7 +306,7 @@ impl DataAvailabilityChecker { executed_block: AvailabilityPendingExecutedBlock, ) -> Result, AvailabilityCheckError> { self.availability_cache - .put_pending_executed_block(executed_block, &self.log) + .put_pending_executed_block(executed_block) } pub fn remove_pending_components(&self, block_root: Hash256) { @@ -336,21 +323,25 @@ impl DataAvailabilityChecker { &self, block: RpcBlock, ) -> Result, AvailabilityCheckError> { + let custody_columns_count = block.custody_columns_count(); let (block_root, block, blobs, data_columns) = block.deconstruct(); if self.blobs_required_for_block(&block) { - return if let Some(blob_list) = blobs.as_ref() { + return if let Some(blob_list) = blobs { verify_kzg_for_blob_list(blob_list.iter(), &self.kzg) .map_err(AvailabilityCheckError::InvalidBlobs)?; Ok(MaybeAvailableBlock::Available(AvailableBlock { block_root, block, - blobs, + blob_data: AvailableBlockData::Blobs(blob_list), blobs_available_timestamp: None, - data_columns: None, spec: self.spec.clone(), })) } else { - Ok(MaybeAvailableBlock::AvailabilityPending { block_root, block }) + Ok(MaybeAvailableBlock::AvailabilityPending { + block_root, + block, + custody_columns_count, + }) }; } if self.data_columns_required_for_block(&block) { @@ -365,27 +356,29 @@ impl DataAvailabilityChecker { Ok(MaybeAvailableBlock::Available(AvailableBlock { block_root, block, - blobs: None, - blobs_available_timestamp: None, - data_columns: Some( + blob_data: AvailableBlockData::DataColumns( data_column_list .into_iter() .map(|d| d.clone_arc()) .collect(), ), + blobs_available_timestamp: None, spec: self.spec.clone(), })) } else { - Ok(MaybeAvailableBlock::AvailabilityPending { block_root, block }) + Ok(MaybeAvailableBlock::AvailabilityPending { + block_root, + block, + custody_columns_count, + }) }; } Ok(MaybeAvailableBlock::Available(AvailableBlock { block_root, block, - blobs: None, + blob_data: AvailableBlockData::NoData, blobs_available_timestamp: None, - data_columns: None, spec: self.spec.clone(), })) } @@ -434,42 +427,48 @@ impl DataAvailabilityChecker { } for block in blocks { + let custody_columns_count = block.custody_columns_count(); let (block_root, block, blobs, data_columns) = block.deconstruct(); let maybe_available_block = if self.blobs_required_for_block(&block) { - if blobs.is_some() { + if let Some(blobs) = blobs { MaybeAvailableBlock::Available(AvailableBlock { block_root, block, - blobs, + blob_data: AvailableBlockData::Blobs(blobs), blobs_available_timestamp: None, - data_columns: None, spec: self.spec.clone(), }) } else { - MaybeAvailableBlock::AvailabilityPending { block_root, block } + MaybeAvailableBlock::AvailabilityPending { + block_root, + block, + custody_columns_count, + } } } else if self.data_columns_required_for_block(&block) { - if data_columns.is_some() { + if let Some(data_columns) = data_columns { MaybeAvailableBlock::Available(AvailableBlock { block_root, block, - blobs: None, - data_columns: data_columns.map(|data_columns| { - data_columns.into_iter().map(|d| d.into_inner()).collect() - }), + blob_data: AvailableBlockData::DataColumns( + data_columns.into_iter().map(|d| d.into_inner()).collect(), + ), blobs_available_timestamp: None, spec: self.spec.clone(), }) } else { - MaybeAvailableBlock::AvailabilityPending { block_root, block } + MaybeAvailableBlock::AvailabilityPending { + block_root, + block, + custody_columns_count, + } } } else { MaybeAvailableBlock::Available(AvailableBlock { block_root, block, - blobs: None, - data_columns: None, + blob_data: AvailableBlockData::NoData, blobs_available_timestamp: None, spec: self.spec.clone(), }) @@ -545,11 +544,11 @@ impl DataAvailabilityChecker { &self, block_root: &Hash256, ) -> Result, AvailabilityCheckError> { - let pending_components = match self + let verified_data_columns = match self .availability_cache .check_and_set_reconstruction_started(block_root) { - ReconstructColumnsDecision::Yes(pending_components) => pending_components, + ReconstructColumnsDecision::Yes(verified_data_columns) => verified_data_columns, ReconstructColumnsDecision::No(reason) => { return Ok(DataColumnReconstructionResult::NotStarted(reason)); } @@ -560,15 +559,14 @@ impl DataAvailabilityChecker { let all_data_columns = KzgVerifiedCustodyDataColumn::reconstruct_columns( &self.kzg, - &pending_components.verified_data_columns, + &verified_data_columns, &self.spec, ) .map_err(|e| { error!( - self.log, - "Error reconstructing data columns"; - "block_root" => ?block_root, - "error" => ?e + ?block_root, + error = ?e, + "Error reconstructing data columns" ); self.availability_cache .handle_reconstruction_failure(block_root); @@ -603,14 +601,15 @@ impl DataAvailabilityChecker { data_columns_to_publish.len() as u64, ); - debug!(self.log, "Reconstructed columns"; - "count" => data_columns_to_publish.len(), - "block_root" => ?block_root, - "slot" => slot, + debug!( + count = data_columns_to_publish.len(), + ?block_root, + %slot, + "Reconstructed columns" ); self.availability_cache - .put_kzg_verified_data_columns(*block_root, data_columns_to_publish.clone(), &self.log) + .put_kzg_verified_data_columns(*block_root, data_columns_to_publish.clone()) .map(|availability| { DataColumnReconstructionResult::Success(( availability, @@ -637,14 +636,18 @@ pub fn start_availability_cache_maintenance_service( if chain.spec.deneb_fork_epoch.is_some() { let overflow_cache = chain.data_availability_checker.availability_cache.clone(); executor.spawn( - async move { availability_cache_maintenance_service(chain, overflow_cache).await }, + async move { + availability_cache_maintenance_service(chain, overflow_cache) + .instrument(info_span!( + "DataAvailabilityChecker", + service = "data_availability_checker" + )) + .await + }, "availability_cache_service", ); } else { - debug!( - chain.log, - "Deneb fork not configured, not starting availability cache maintenance service" - ); + debug!("Deneb fork not configured, not starting availability cache maintenance service"); } } @@ -668,10 +671,7 @@ async fn availability_cache_maintenance_service( break; }; - debug!( - chain.log, - "Availability cache maintenance service firing"; - ); + debug!("Availability cache maintenance service firing"); let Some(current_epoch) = chain .slot_clock .now() @@ -701,11 +701,11 @@ async fn availability_cache_maintenance_service( ); if let Err(e) = overflow_cache.do_maintenance(cutoff_epoch) { - error!(chain.log, "Failed to maintain availability cache"; "error" => ?e); + error!(error = ?e,"Failed to maintain availability cache"); } } None => { - error!(chain.log, "Failed to read slot clock"); + error!("Failed to read slot clock"); // If we can't read the slot clock, just wait another slot. tokio::time::sleep(chain.slot_clock.slot_duration()).await; } @@ -713,13 +713,22 @@ async fn availability_cache_maintenance_service( } } +#[derive(Debug)] +pub enum AvailableBlockData { + /// Block is pre-Deneb or has zero blobs + NoData, + /// Block is post-Deneb, pre-PeerDAS and has more than zero blobs + Blobs(BlobSidecarList), + /// Block is post-PeerDAS and has more than zero blobs + DataColumns(DataColumnSidecarList), +} + /// A fully available block that is ready to be imported into fork choice. -#[derive(Clone, Debug, PartialEq)] +#[derive(Debug)] pub struct AvailableBlock { block_root: Hash256, block: Arc>, - blobs: Option>, - data_columns: Option>, + blob_data: AvailableBlockData, /// Timestamp at which this block first became available (UNIX timestamp, time since 1970). blobs_available_timestamp: Option, pub spec: Arc, @@ -729,15 +738,13 @@ impl AvailableBlock { pub fn __new_for_testing( block_root: Hash256, block: Arc>, - blobs: Option>, - data_columns: Option>, + data: AvailableBlockData, spec: Arc, ) -> Self { Self { block_root, block, - blobs, - data_columns, + blob_data: data, blobs_available_timestamp: None, spec, } @@ -750,39 +757,52 @@ impl AvailableBlock { self.block.clone() } - pub fn blobs(&self) -> Option<&BlobSidecarList> { - self.blobs.as_ref() - } - pub fn blobs_available_timestamp(&self) -> Option { self.blobs_available_timestamp } - pub fn data_columns(&self) -> Option<&DataColumnSidecarList> { - self.data_columns.as_ref() + pub fn data(&self) -> &AvailableBlockData { + &self.blob_data + } + + pub fn has_blobs(&self) -> bool { + match self.blob_data { + AvailableBlockData::NoData => false, + AvailableBlockData::Blobs(..) => true, + AvailableBlockData::DataColumns(_) => false, + } } #[allow(clippy::type_complexity)] - pub fn deconstruct( - self, - ) -> ( - Hash256, - Arc>, - Option>, - Option>, - ) { + pub fn deconstruct(self) -> (Hash256, Arc>, AvailableBlockData) { let AvailableBlock { block_root, block, - blobs, - data_columns, + blob_data, .. } = self; - (block_root, block, blobs, data_columns) + (block_root, block, blob_data) + } + + /// Only used for testing + pub fn __clone_without_recv(&self) -> Result { + Ok(Self { + block_root: self.block_root, + block: self.block.clone(), + blob_data: match &self.blob_data { + AvailableBlockData::NoData => AvailableBlockData::NoData, + AvailableBlockData::Blobs(blobs) => AvailableBlockData::Blobs(blobs.clone()), + AvailableBlockData::DataColumns(data_columns) => { + AvailableBlockData::DataColumns(data_columns.clone()) + } + }, + blobs_available_timestamp: self.blobs_available_timestamp, + spec: self.spec.clone(), + }) } } -#[derive(Debug, Clone)] +#[derive(Debug)] pub enum MaybeAvailableBlock { /// This variant is fully available. /// i.e. for pre-deneb blocks, it contains a (`SignedBeaconBlock`, `Blobs::None`) and for @@ -792,6 +812,7 @@ pub enum MaybeAvailableBlock { AvailabilityPending { block_root: Hash256, block: Arc>, + custody_columns_count: usize, }, } diff --git a/beacon_node/beacon_chain/src/data_availability_checker/error.rs b/beacon_node/beacon_chain/src/data_availability_checker/error.rs index 1ab85ab1056..d091d6fefb5 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/error.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/error.rs @@ -10,7 +10,7 @@ pub enum Error { blob_commitment: KzgCommitment, block_commitment: KzgCommitment, }, - Unexpected, + Unexpected(String), SszTypes(ssz_types::Error), MissingBlobs, MissingCustodyColumns, @@ -40,7 +40,7 @@ impl Error { | Error::MissingCustodyColumns | Error::StoreError(_) | Error::DecodeError(_) - | Error::Unexpected + | Error::Unexpected(_) | Error::ParentStateMissing(_) | Error::BlockReplayError(_) | Error::RebuildingStateCaches(_) 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 da6372a43aa..5b5a6fcc0df 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -1,4 +1,5 @@ use super::state_lru_cache::{DietAvailabilityPendingExecutedBlock, StateLRUCache}; +use super::AvailableBlockData; use crate::beacon_chain::BeaconStore; use crate::blob_verification::KzgVerifiedBlob; use crate::block_verification_types::{ @@ -9,15 +10,14 @@ use crate::data_column_verification::KzgVerifiedCustodyDataColumn; use crate::BeaconChainTypes; use lru::LruCache; use parking_lot::RwLock; -use slog::{debug, Logger}; +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, DataColumnIdentifier, DataColumnSidecar, Epoch, EthSpec, + Hash256, RuntimeFixedVector, RuntimeVariableList, SignedBeaconBlock, }; /// This represents the components of a partially available block @@ -30,28 +30,9 @@ 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 { - /// Clones the `PendingComponent` without cloning `data_column_recv`, as `Receiver` is not cloneable. - /// This should only be used when the receiver is no longer needed. - pub fn clone_without_column_recv(&self) -> Self { - PendingComponents { - block_root: self.block_root, - verified_blobs: self.verified_blobs.clone(), - verified_data_columns: self.verified_data_columns.clone(), - executed_block: self.executed_block.clone(), - reconstruction_started: self.reconstruction_started, - data_column_recv: None, - } - } - /// Returns an immutable reference to the cached block. pub fn get_cached_block(&self) -> &Option> { &self.executed_block @@ -95,35 +76,6 @@ impl PendingComponents { .unwrap_or(false) } - /// Returns the number of blobs that are expected to be present. Returns `None` if we don't have a - /// block. - /// - /// This corresponds to the number of commitments that are present in a block. - pub fn block_kzg_commitments_count(&self) -> Option { - self.get_cached_block() - .as_ref() - .map(|b| b.get_commitments().len()) - } - - /// Returns the number of blobs that have been received and are stored in the cache. - pub fn num_received_blobs(&self) -> usize { - self.get_cached_blobs().iter().flatten().count() - } - - /// Checks if a data column of a given index exists in the cache. - /// - /// Returns: - /// - `true` if a data column for the given index exists. - /// - `false` otherwise. - fn data_column_exists(&self, data_column_index: u64) -> bool { - self.get_cached_data_column(data_column_index).is_some() - } - - /// Returns the number of data columns that have been received and are stored in the cache. - pub fn num_received_data_columns(&self) -> usize { - self.verified_data_columns.len() - } - /// Returns the indices of cached custody columns pub fn get_cached_data_columns_indices(&self) -> Vec { self.verified_data_columns @@ -182,8 +134,7 @@ impl PendingComponents { kzg_verified_data_columns: I, ) -> Result<(), AvailabilityCheckError> { for data_column in kzg_verified_data_columns { - // TODO(das): Add equivalent checks for data columns if necessary - if !self.data_column_exists(data_column.index()) { + if self.get_cached_data_column(data_column.index()).is_none() { self.verified_data_columns.push(data_column); } } @@ -199,146 +150,138 @@ impl PendingComponents { self.merge_blobs(reinsert); } - /// Checks if the block and all of its expected blobs or custody columns (post-PeerDAS) are - /// available in the cache. - /// - /// Returns `true` if both the block exists and the number of received blobs / custody columns - /// matches the number of expected blobs / custody columns. - pub fn is_available(&self, custody_column_count: usize, log: &Logger) -> bool { - let block_kzg_commitments_count_opt = self.block_kzg_commitments_count(); - let expected_blobs_msg = block_kzg_commitments_count_opt - .as_ref() - .map(|num| num.to_string()) - .unwrap_or("unknown".to_string()); - - // No data columns when there are 0 blobs - let expected_columns_opt = block_kzg_commitments_count_opt.map(|blob_count| { - if blob_count > 0 { - custody_column_count - } else { - 0 - } - }); - let expected_columns_msg = expected_columns_opt - .as_ref() - .map(|num| num.to_string()) - .unwrap_or("unknown".to_string()); - - let num_received_blobs = self.num_received_blobs(); - let num_received_columns = self.num_received_data_columns(); - - debug!( - log, - "Component(s) added to data availability checker"; - "block_root" => ?self.block_root, - "received_blobs" => num_received_blobs, - "expected_blobs" => expected_blobs_msg, - "received_columns" => num_received_columns, - "expected_columns" => expected_columns_msg, - ); - - let all_blobs_received = block_kzg_commitments_count_opt - .is_some_and(|num_expected_blobs| num_expected_blobs == num_received_blobs); - - let all_columns_received = expected_columns_opt - .is_some_and(|num_expected_columns| num_expected_columns == num_received_columns); - - all_blobs_received || all_columns_received - } - - /// Returns an empty `PendingComponents` object with the given block root. - pub fn empty(block_root: Hash256, max_len: usize) -> Self { - Self { - block_root, - verified_blobs: RuntimeFixedVector::new(vec![None; max_len]), - verified_data_columns: vec![], - executed_block: None, - reconstruction_started: false, - data_column_recv: None, - } - } - - /// Verifies an `SignedBeaconBlock` against a set of KZG verified blobs. - /// This does not check whether a block *should* have blobs, these checks should have been - /// completed when producing the `AvailabilityPendingBlock`. + /// Returns Some if the block has received all its required data for import. The return value + /// must be persisted in the DB along with the block. /// /// WARNING: This function can potentially take a lot of time if the state needs to be /// reconstructed from disk. Ensure you are not holding any write locks while calling this. pub fn make_available( - self, + &mut self, spec: &Arc, recover: R, - ) -> Result, AvailabilityCheckError> + ) -> Result>, AvailabilityCheckError> where R: FnOnce( DietAvailabilityPendingExecutedBlock, ) -> Result, AvailabilityCheckError>, { - let Self { - block_root, - verified_blobs, - verified_data_columns, - executed_block, - data_column_recv, - .. - } = self; - - let blobs_available_timestamp = verified_blobs - .iter() - .flatten() - .map(|blob| blob.seen_timestamp()) - .max(); - - let Some(diet_executed_block) = executed_block else { - return Err(AvailabilityCheckError::Unexpected); + let Some(block) = &self.executed_block else { + // Block not available yet + return Ok(None); }; - let is_peer_das_enabled = spec.is_peer_das_enabled_for_epoch(diet_executed_block.epoch()); - let (blobs, data_columns) = if is_peer_das_enabled { - let data_columns = verified_data_columns - .into_iter() - .map(|d| d.into_inner()) - .collect::>(); - (None, Some(data_columns)) + let num_expected_blobs = block.num_blobs_expected(); + + let blob_data = if num_expected_blobs == 0 { + Some(AvailableBlockData::NoData) + } else if spec.is_peer_das_enabled_for_epoch(block.epoch()) { + let num_received_columns = self.verified_data_columns.len(); + let num_expected_columns = block.custody_columns_count(); + match num_received_columns.cmp(&num_expected_columns) { + Ordering::Greater => { + // Should never happen + return Err(AvailabilityCheckError::Unexpected(format!( + "too many columns got {num_received_columns} expected {num_expected_columns}" + ))); + } + Ordering::Equal => { + // Block is post-peerdas, and we got enough columns + let data_columns = self + .verified_data_columns + .iter() + .map(|d| d.clone().into_inner()) + .collect::>(); + Some(AvailableBlockData::DataColumns(data_columns)) + } + Ordering::Less => { + // Not enough data columns received yet + None + } + } } else { - let num_blobs_expected = diet_executed_block.num_blobs_expected(); - let Some(verified_blobs) = verified_blobs - .into_iter() - .map(|b| b.map(|b| b.to_blob())) - .take(num_blobs_expected) - .collect::>>() - else { - return Err(AvailabilityCheckError::Unexpected); - }; - let max_len = spec.max_blobs_per_block(diet_executed_block.as_block().epoch()) as usize; - ( - Some(RuntimeVariableList::new(verified_blobs, max_len)?), - None, - ) + // Before PeerDAS, blobs + let num_received_blobs = self.verified_blobs.iter().flatten().count(); + match num_received_blobs.cmp(&num_expected_blobs) { + Ordering::Greater => { + // Should never happen + return Err(AvailabilityCheckError::Unexpected(format!( + "too many blobs got {num_received_blobs} expected {num_expected_blobs}" + ))); + } + Ordering::Equal => { + let max_blobs = spec.max_blobs_per_block(block.epoch()) as usize; + let blobs_vec = self + .verified_blobs + .iter() + .flatten() + .map(|blob| blob.clone().to_blob()) + .collect::>(); + let blobs_len = blobs_vec.len(); + let blobs = RuntimeVariableList::new(blobs_vec, max_blobs).map_err(|_| { + AvailabilityCheckError::Unexpected(format!( + "over max_blobs len {blobs_len} max {max_blobs}" + )) + })?; + Some(AvailableBlockData::Blobs(blobs)) + } + Ordering::Less => { + // Not enough blobs received yet + None + } + } + }; + + // Block's data not available yet + let Some(blob_data) = blob_data else { + return Ok(None); + }; + + // Block is available, construct `AvailableExecutedBlock` + + let blobs_available_timestamp = match blob_data { + AvailableBlockData::NoData => None, + AvailableBlockData::Blobs(_) => self + .verified_blobs + .iter() + .flatten() + .map(|blob| blob.seen_timestamp()) + .max(), + // TODO(das): To be fixed with https://github.com/sigp/lighthouse/pull/6850 + AvailableBlockData::DataColumns(_) => None, }; - let executed_block = recover(diet_executed_block)?; let AvailabilityPendingExecutedBlock { block, - mut import_data, + import_data, payload_verification_outcome, - } = executed_block; - - import_data.data_column_recv = data_column_recv; + custody_columns_count: _, + } = recover(block.clone())?; let available_block = AvailableBlock { - block_root, + block_root: self.block_root, block, - blobs, - data_columns, + blob_data, blobs_available_timestamp, spec: spec.clone(), }; - Ok(Availability::Available(Box::new( - AvailableExecutedBlock::new(available_block, import_data, payload_verification_outcome), + Ok(Some(AvailableExecutedBlock::new( + available_block, + import_data, + payload_verification_outcome, ))) } + /// Returns an empty `PendingComponents` object with the given block root. + pub fn empty(block_root: Hash256, max_len: usize) -> Self { + Self { + block_root, + verified_blobs: RuntimeFixedVector::new(vec![None; max_len]), + verified_data_columns: vec![], + executed_block: None, + reconstruction_started: false, + } + } + /// Returns the epoch of the block if it is cached, otherwise returns the epoch of the first blob. pub fn epoch(&self) -> Option { self.executed_block @@ -364,6 +307,35 @@ impl PendingComponents { None }) } + + pub fn status_str(&self, block_epoch: Epoch, spec: &ChainSpec) -> String { + let block_count = if self.executed_block.is_some() { 1 } else { 0 }; + if spec.is_peer_das_enabled_for_epoch(block_epoch) { + let custody_columns_count = if let Some(block) = self.get_cached_block() { + &block.custody_columns_count().to_string() + } else { + "?" + }; + format!( + "block {} data_columns {}/{}", + block_count, + self.verified_data_columns.len(), + custody_columns_count, + ) + } else { + let num_expected_blobs = if let Some(block) = self.get_cached_block() { + &block.num_blobs_expected().to_string() + } else { + "?" + }; + format!( + "block {} blobs {}/{}", + block_count, + self.verified_blobs.iter().flatten().count(), + num_expected_blobs + ) + } + } } /// This is the main struct for this module. Outside methods should @@ -374,8 +346,6 @@ pub struct DataAvailabilityCheckerInner { /// This cache holds a limited number of states in memory and reconstructs them /// from disk when necessary. This is necessary until we merge tree-states state_cache: StateLRUCache, - /// The number of data columns the node is sampling via subnet sampling. - sampling_column_count: usize, spec: Arc, } @@ -384,7 +354,7 @@ pub struct DataAvailabilityCheckerInner { // the current usage, as it's deconstructed immediately. #[allow(clippy::large_enum_variant)] pub(crate) enum ReconstructColumnsDecision { - Yes(PendingComponents), + Yes(Vec>), No(&'static str), } @@ -392,21 +362,15 @@ impl DataAvailabilityCheckerInner { pub fn new( capacity: NonZeroUsize, beacon_store: BeaconStore, - sampling_column_count: usize, spec: Arc, ) -> Result { Ok(Self { critical: RwLock::new(LruCache::new(capacity)), state_cache: StateLRUCache::new(beacon_store, spec.clone()), - sampling_column_count, spec, }) } - pub fn sampling_column_count(&self) -> usize { - self.sampling_column_count - } - /// Returns true if the block root is known, without altering the LRU ordering pub fn get_execution_valid_block( &self, @@ -465,17 +429,10 @@ impl DataAvailabilityCheckerInner { } /// Puts the KZG verified blobs into the availability cache as pending components. - /// - /// The `data_column_recv` parameter is an optional `Receiver` for data columns that are - /// computed asynchronously. This method remains **used** after PeerDAS activation, because - /// blocks can be made available 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_kzg_verified_blobs>>( &self, block_root: Hash256, kzg_verified_blobs: I, - data_column_recv: Option>>, - log: &Logger, ) -> Result, AvailabilityCheckError> { let mut kzg_verified_blobs = kzg_verified_blobs.into_iter().peekable(); @@ -484,7 +441,7 @@ impl DataAvailabilityCheckerInner { .map(|verified_blob| verified_blob.as_blob().epoch()) else { // Verified blobs list should be non-empty. - return Err(AvailabilityCheckError::Unexpected); + return Err(AvailabilityCheckError::Unexpected("empty blobs".to_owned())); }; let mut fixed_blobs = @@ -509,21 +466,20 @@ impl DataAvailabilityCheckerInner { // Merge in the blobs. pending_components.merge_blobs(fixed_blobs); - if data_column_recv.is_some() { - // If `data_column_recv` is `Some`, it means 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. - pending_components.data_column_recv = data_column_recv; - } + debug!( + component = "blobs", + ?block_root, + status = pending_components.status_str(epoch, &self.spec), + "Component added to data availability checker" + ); - if pending_components.is_available(self.sampling_column_count, log) { + 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.clone_without_column_recv()); + write_lock.put(block_root, pending_components); drop(write_lock); - pending_components.make_available(&self.spec, |diet_block| { - self.state_cache.recover_pending_executed_block(diet_block) - }) + Ok(Availability::Available(Box::new(available_block))) } else { write_lock.put(block_root, pending_components); Ok(Availability::MissingComponents(block_root)) @@ -537,7 +493,6 @@ impl DataAvailabilityCheckerInner { &self, block_root: Hash256, kzg_verified_data_columns: I, - log: &Logger, ) -> Result, AvailabilityCheckError> { let mut kzg_verified_data_columns = kzg_verified_data_columns.into_iter().peekable(); let Some(epoch) = kzg_verified_data_columns @@ -545,7 +500,9 @@ impl DataAvailabilityCheckerInner { .map(|verified_blob| verified_blob.as_data_column().epoch()) else { // Verified data_columns list should be non-empty. - return Err(AvailabilityCheckError::Unexpected); + return Err(AvailabilityCheckError::Unexpected( + "empty columns".to_owned(), + )); }; let mut write_lock = self.critical.write(); @@ -561,14 +518,20 @@ impl DataAvailabilityCheckerInner { // Merge in the data columns. pending_components.merge_data_columns(kzg_verified_data_columns)?; - if pending_components.is_available(self.sampling_column_count, log) { + debug!( + component = "data_columns", + ?block_root, + status = pending_components.status_str(epoch, &self.spec), + "Component added to data availability checker" + ); + + if let Some(available_block) = pending_components.make_available(&self.spec, |block| { + self.state_cache.recover_pending_executed_block(block) + })? { // We keep the pending components in the availability cache during block import (#5845). - // `data_column_recv` is returned as part of the available block and is no longer needed here. - write_lock.put(block_root, pending_components.clone_without_column_recv()); + write_lock.put(block_root, pending_components); drop(write_lock); - pending_components.make_available(&self.spec, |diet_block| { - self.state_cache.recover_pending_executed_block(diet_block) - }) + Ok(Availability::Available(Box::new(available_block))) } else { write_lock.put(block_root, pending_components); Ok(Availability::MissingComponents(block_root)) @@ -595,16 +558,12 @@ impl DataAvailabilityCheckerInner { }; // If we're sampling all columns, it means we must be custodying all columns. - let custody_column_count = self.sampling_column_count(); let total_column_count = self.spec.number_of_columns as usize; let received_column_count = pending_components.verified_data_columns.len(); if pending_components.reconstruction_started { return ReconstructColumnsDecision::No("already started"); } - if custody_column_count != total_column_count { - return ReconstructColumnsDecision::No("not required for full node"); - } if received_column_count >= total_column_count { return ReconstructColumnsDecision::No("all columns received"); } @@ -613,7 +572,7 @@ impl DataAvailabilityCheckerInner { } pending_components.reconstruction_started = true; - ReconstructColumnsDecision::Yes(pending_components.clone_without_column_recv()) + ReconstructColumnsDecision::Yes(pending_components.verified_data_columns.clone()) } /// This could mean some invalid data columns made it through to the `DataAvailabilityChecker`. @@ -631,7 +590,6 @@ impl DataAvailabilityCheckerInner { pub fn put_pending_executed_block( &self, executed_block: AvailabilityPendingExecutedBlock, - log: &Logger, ) -> Result, AvailabilityCheckError> { let mut write_lock = self.critical.write(); let epoch = executed_block.as_block().epoch(); @@ -653,15 +611,21 @@ impl DataAvailabilityCheckerInner { // Merge in the block. pending_components.merge_block(diet_executed_block); + debug!( + component = "block", + ?block_root, + status = pending_components.status_str(epoch, &self.spec), + "Component added to data availability checker" + ); + // Check if we have all components and entire set is consistent. - if pending_components.is_available(self.sampling_column_count, log) { + 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.clone_without_column_recv()); + write_lock.put(block_root, pending_components); drop(write_lock); - pending_components.make_available(&self.spec, |diet_block| { - self.state_cache.recover_pending_executed_block(diet_block) - }) + Ok(Availability::Available(Box::new(available_block))) } else { write_lock.put(block_root, pending_components); Ok(Availability::MissingComponents(block_root)) @@ -725,13 +689,12 @@ mod test { test_utils::{BaseHarnessType, BeaconChainHarness, DiskHarnessType}, }; use fork_choice::PayloadVerificationStatus; - - use logging::test_logger; - use slog::{info, Logger}; + use logging::create_test_tracing_subscriber; use state_processing::ConsensusContext; use std::collections::VecDeque; use store::{database::interface::BeaconNodeBackend, HotColdDB, ItemStore, StoreConfig}; use tempfile::{tempdir, TempDir}; + use tracing::info; use types::non_zero_usize::new_non_zero_usize; use types::{ExecPayload, MinimalEthSpec}; @@ -741,7 +704,6 @@ mod test { fn get_store_with_spec( db_path: &TempDir, spec: Arc, - log: Logger, ) -> Arc, BeaconNodeBackend>> { let hot_path = db_path.path().join("hot_db"); let cold_path = db_path.path().join("cold_db"); @@ -755,14 +717,12 @@ mod test { |_, _, _| Ok(()), config, spec, - log, ) .expect("disk store should initialize") } // get a beacon chain harness advanced to just before deneb fork async fn get_deneb_chain( - log: Logger, db_path: &TempDir, ) -> BeaconChainHarness> { let altair_fork_epoch = Epoch::new(1); @@ -779,12 +739,11 @@ mod test { spec.deneb_fork_epoch = Some(deneb_fork_epoch); let spec = Arc::new(spec); - let chain_store = get_store_with_spec::(db_path, spec.clone(), log.clone()); + let chain_store = get_store_with_spec::(db_path, spec.clone()); let validators_keypairs = types::test_utils::generate_deterministic_keypairs(LOW_VALIDATOR_COUNT); let harness = BeaconChainHarness::builder(E::default()) .spec(spec.clone()) - .logger(log.clone()) .keypairs(validators_keypairs) .fresh_disk_store(chain_store) .mock_execution_layer() @@ -827,7 +786,6 @@ mod test { Cold: ItemStore, { let chain = &harness.chain; - let log = chain.log.clone(); let head = chain.head_snapshot(); let parent_state = head.beacon_state.clone(); @@ -855,7 +813,7 @@ mod test { ); // log kzg commitments - info!(log, "printing kzg commitments"); + info!("printing kzg commitments"); for comm in Vec::from( block .message() @@ -864,9 +822,9 @@ mod test { .expect("should be deneb fork") .clone(), ) { - info!(log, "kzg commitment"; "commitment" => ?comm); + info!(commitment = ?comm, "kzg commitment"); } - info!(log, "done printing kzg commitments"); + info!("done printing kzg commitments"); let gossip_verified_blobs = if let Some((kzg_proofs, blobs)) = maybe_blobs { let sidecars = @@ -890,9 +848,7 @@ mod test { state, parent_block, parent_eth1_finalization_data, - confirmed_state_roots: vec![], consensus_context, - data_column_recv: None, }; let payload_verification_outcome = PayloadVerificationOutcome { @@ -904,6 +860,7 @@ mod test { block, import_data, payload_verification_outcome, + custody_columns_count: DEFAULT_TEST_CUSTODY_COLUMN_COUNT, }; (availability_pending_block, gossip_verified_blobs) @@ -924,20 +881,15 @@ mod test { EthSpec = E, >, { - let log = test_logger(); + create_test_tracing_subscriber(); let chain_db_path = tempdir().expect("should get temp dir"); - let harness = get_deneb_chain(log.clone(), &chain_db_path).await; + let harness = get_deneb_chain(&chain_db_path).await; let spec = harness.spec.clone(); let test_store = harness.chain.store.clone(); let capacity_non_zero = new_non_zero_usize(capacity); let cache = Arc::new( - DataAvailabilityCheckerInner::::new( - capacity_non_zero, - test_store, - DEFAULT_TEST_CUSTODY_COLUMN_COUNT, - spec.clone(), - ) - .expect("should create cache"), + DataAvailabilityCheckerInner::::new(capacity_non_zero, test_store, spec.clone()) + .expect("should create cache"), ); (harness, cache, chain_db_path) } @@ -960,7 +912,7 @@ mod test { ); assert!(cache.critical.read().is_empty(), "cache should be empty"); let availability = cache - .put_pending_executed_block(pending_block, harness.logger()) + .put_pending_executed_block(pending_block) .expect("should put block"); if blobs_expected == 0 { assert!( @@ -999,7 +951,7 @@ mod test { for (blob_index, gossip_blob) in blobs.into_iter().enumerate() { kzg_verified_blobs.push(gossip_blob.into_inner()); let availability = cache - .put_kzg_verified_blobs(root, kzg_verified_blobs.clone(), None, harness.logger()) + .put_kzg_verified_blobs(root, kzg_verified_blobs.clone()) .expect("should put blob"); if blob_index == blobs_expected - 1 { assert!(matches!(availability, Availability::Available(_))); @@ -1027,17 +979,16 @@ mod test { for gossip_blob in blobs { kzg_verified_blobs.push(gossip_blob.into_inner()); let availability = cache - .put_kzg_verified_blobs(root, kzg_verified_blobs.clone(), None, harness.logger()) + .put_kzg_verified_blobs(root, kzg_verified_blobs.clone()) .expect("should put blob"); - assert_eq!( - availability, - Availability::MissingComponents(root), + assert!( + matches!(availability, Availability::MissingComponents(_)), "should be pending block" ); assert_eq!(cache.critical.read().len(), 1); } let availability = cache - .put_pending_executed_block(pending_block, harness.logger()) + .put_pending_executed_block(pending_block) .expect("should put block"); assert!( matches!(availability, Availability::Available(_)), @@ -1105,7 +1056,7 @@ mod test { // put the block in the cache let availability = cache - .put_pending_executed_block(pending_block, harness.logger()) + .put_pending_executed_block(pending_block) .expect("should put block"); // grab the diet block from the cache for later testing @@ -1281,14 +1232,14 @@ mod pending_components_tests { eth1_data: Default::default(), eth1_deposit_index: 0, }, - confirmed_state_roots: vec![], consensus_context: ConsensusContext::new(Slot::new(0)), - data_column_recv: None, }, payload_verification_outcome: PayloadVerificationOutcome { payload_verification_status: PayloadVerificationStatus::Verified, is_valid_merge_transition_block: false, }, + // Default custody columns count, doesn't matter here + custody_columns_count: 8, }; (block.into(), blobs, invalid_blobs) } 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 2a2a0431ccb..5fe674f30c1 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,28 +7,24 @@ 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, } /// just implementing the same methods as `AvailabilityPendingExecutedBlock` @@ -58,6 +54,10 @@ impl DietAvailabilityPendingExecutedBlock { .unwrap_or_default() } + pub fn custody_columns_count(&self) -> usize { + self.custody_columns_count + } + /// Returns the epoch corresponding to `self.slot()`. pub fn epoch(&self) -> Epoch { self.block.slot().epoch(E::slots_per_epoch()) @@ -103,11 +103,11 @@ 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, ), payload_verification_outcome: executed_block.payload_verification_outcome, + custody_columns_count: executed_block.custody_columns_count, } } @@ -132,13 +132,12 @@ 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(), - data_column_recv: None, }, payload_verification_outcome: diet_executed_block.payload_verification_outcome, + custody_columns_count: diet_executed_block.custody_columns_count, }) } @@ -221,11 +220,11 @@ 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, ), payload_verification_outcome: value.payload_verification_outcome, + custody_columns_count: value.custody_columns_count, } } } diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index 1262fcdeb8e..20b5c9aa029 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,13 +10,12 @@ use derivative::Derivative; use fork_choice::ProtoBlock; use kzg::{Error as KzgError, Kzg}; use proto_array::Block; -use slasher::test_utils::E; -use slog::debug; 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::{ BeaconStateError, ChainSpec, DataColumnSidecar, DataColumnSubnetId, EthSpec, Hash256, @@ -141,13 +141,23 @@ 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 { @@ -240,6 +250,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, @@ -473,10 +491,23 @@ fn verify_data_column_sidecar( 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(()) @@ -557,33 +588,37 @@ 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!( - chain.log, - "Proposer shuffling cache miss for column verification"; - "block_root" => %block_root, - "index" => %column_index, + %block_root, + index = %column_index, + "Proposer shuffling cache miss for column verification" ); let (parent_state_root, mut parent_state) = chain .store @@ -603,19 +638,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 = { diff --git a/beacon_node/beacon_chain/src/early_attester_cache.rs b/beacon_node/beacon_chain/src/early_attester_cache.rs index b62554f1b4d..5665ef3775c 100644 --- a/beacon_node/beacon_chain/src/early_attester_cache.rs +++ b/beacon_node/beacon_chain/src/early_attester_cache.rs @@ -1,4 +1,4 @@ -use crate::data_availability_checker::AvailableBlock; +use crate::data_availability_checker::{AvailableBlock, AvailableBlockData}; use crate::{ attester_cache::{CommitteeLengths, Error}, metrics, @@ -52,7 +52,7 @@ impl EarlyAttesterCache { pub fn add_head_block( &self, beacon_block_root: Hash256, - block: AvailableBlock, + block: &AvailableBlock, proto_block: ProtoBlock, state: &BeaconState, spec: &ChainSpec, @@ -70,14 +70,19 @@ impl EarlyAttesterCache { }, }; - let (_, block, blobs, data_columns) = block.deconstruct(); + let (blobs, data_columns) = match block.data() { + AvailableBlockData::NoData => (None, None), + AvailableBlockData::Blobs(blobs) => (Some(blobs.clone()), None), + AvailableBlockData::DataColumns(data_columns) => (None, Some(data_columns.clone())), + }; + let item = CacheItem { epoch, committee_lengths, beacon_block_root, source, target, - block, + block: block.block_cloned(), blobs, data_columns, proto_block, diff --git a/beacon_node/beacon_chain/src/eth1_chain.rs b/beacon_node/beacon_chain/src/eth1_chain.rs index 7ff2de95484..8a79bff4c7a 100644 --- a/beacon_node/beacon_chain/src/eth1_chain.rs +++ b/beacon_node/beacon_chain/src/eth1_chain.rs @@ -3,7 +3,6 @@ use eth1::{Config as Eth1Config, Eth1Block, Service as HttpService}; use eth2::lighthouse::Eth1SyncStatusData; use ethereum_hashing::hash; use int_to_bytes::int_to_bytes32; -use slog::{debug, error, trace, Logger}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use state_processing::per_block_processing::get_new_eth1_data; @@ -14,6 +13,7 @@ use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use store::{DBColumn, Error as StoreError, StoreItem}; use task_executor::TaskExecutor; +use tracing::{debug, error, trace}; use types::{ BeaconState, BeaconStateError, ChainSpec, Deposit, Eth1Data, EthSpec, Hash256, Slot, Unsigned, }; @@ -283,11 +283,9 @@ where pub fn from_ssz_container( ssz_container: &SszEth1, config: Eth1Config, - log: &Logger, spec: Arc, ) -> Result { - let backend = - Eth1ChainBackend::from_bytes(&ssz_container.backend_bytes, config, log.clone(), spec)?; + let backend = Eth1ChainBackend::from_bytes(&ssz_container.backend_bytes, config, spec)?; Ok(Self { use_dummy_backend: ssz_container.use_dummy_backend, backend, @@ -351,12 +349,7 @@ pub trait Eth1ChainBackend: Sized + Send + Sync { fn as_bytes(&self) -> Vec; /// Create a `Eth1ChainBackend` instance given encoded bytes. - fn from_bytes( - bytes: &[u8], - config: Eth1Config, - log: Logger, - spec: Arc, - ) -> Result; + fn from_bytes(bytes: &[u8], config: Eth1Config, spec: Arc) -> Result; } /// Provides a simple, testing-only backend that generates deterministic, meaningless eth1 data. @@ -418,7 +411,6 @@ impl Eth1ChainBackend for DummyEth1ChainBackend { fn from_bytes( _bytes: &[u8], _config: Eth1Config, - _log: Logger, _spec: Arc, ) -> Result { Ok(Self(PhantomData)) @@ -439,7 +431,6 @@ impl Default for DummyEth1ChainBackend { #[derive(Clone)] pub struct CachingEth1Backend { pub core: HttpService, - log: Logger, _phantom: PhantomData, } @@ -447,11 +438,10 @@ impl CachingEth1Backend { /// Instantiates `self` with empty caches. /// /// Does not connect to the eth1 node or start any tasks to keep the cache updated. - pub fn new(config: Eth1Config, log: Logger, spec: Arc) -> Result { + pub fn new(config: Eth1Config, spec: Arc) -> Result { Ok(Self { - core: HttpService::new(config, log.clone(), spec) + core: HttpService::new(config, spec) .map_err(|e| format!("Failed to create eth1 http service: {:?}", e))?, - log, _phantom: PhantomData, }) } @@ -464,7 +454,6 @@ impl CachingEth1Backend { /// Instantiates `self` from an existing service. pub fn from_service(service: HttpService) -> Self { Self { - log: service.log.clone(), core: service, _phantom: PhantomData, } @@ -493,9 +482,8 @@ impl Eth1ChainBackend for CachingEth1Backend { }; trace!( - self.log, - "Found eth1 data votes_to_consider"; - "votes_to_consider" => votes_to_consider.len(), + votes_to_consider = votes_to_consider.len(), + "Found eth1 data votes_to_consider" ); let valid_votes = collect_valid_votes(state, &votes_to_consider); @@ -512,22 +500,20 @@ impl Eth1ChainBackend for CachingEth1Backend { .map(|vote| { let vote = vote.0.clone(); debug!( - self.log, - "No valid eth1_data votes"; - "outcome" => "Casting vote corresponding to last candidate eth1 block", - "vote" => ?vote + outcome = "Casting vote corresponding to last candidate eth1 block", + ?vote, + "No valid eth1_data votes" ); vote }) .unwrap_or_else(|| { let vote = state.eth1_data().clone(); error!( - self.log, - "No valid eth1_data votes, `votes_to_consider` empty"; - "lowest_block_number" => self.core.lowest_block_number(), - "earliest_block_timestamp" => self.core.earliest_block_timestamp(), - "genesis_time" => state.genesis_time(), - "outcome" => "casting `state.eth1_data` as eth1 vote" + lowest_block_number = self.core.lowest_block_number(), + earliest_block_timestamp = self.core.earliest_block_timestamp(), + genesis_time = state.genesis_time(), + outcome = "casting `state.eth1_data` as eth1 vote", + "No valid eth1_data votes, `votes_to_consider` empty" ); metrics::inc_counter(&metrics::DEFAULT_ETH1_VOTES); vote @@ -535,11 +521,10 @@ impl Eth1ChainBackend for CachingEth1Backend { }; debug!( - self.log, - "Produced vote for eth1 chain"; - "deposit_root" => format!("{:?}", eth1_data.deposit_root), - "deposit_count" => eth1_data.deposit_count, - "block_hash" => format!("{:?}", eth1_data.block_hash), + deposit_root = ?eth1_data.deposit_root, + deposit_count = eth1_data.deposit_count, + block_hash = ?eth1_data.block_hash, + "Produced vote for eth1 chain" ); Ok(eth1_data) @@ -604,16 +589,10 @@ impl Eth1ChainBackend for CachingEth1Backend { } /// Recover the cached backend from encoded bytes. - fn from_bytes( - bytes: &[u8], - config: Eth1Config, - log: Logger, - spec: Arc, - ) -> Result { - let inner = HttpService::from_bytes(bytes, config, log.clone(), spec)?; + fn from_bytes(bytes: &[u8], config: Eth1Config, spec: Arc) -> Result { + let inner = HttpService::from_bytes(bytes, config, spec)?; Ok(Self { core: inner, - log, _phantom: PhantomData, }) } @@ -754,17 +733,18 @@ mod test { mod eth1_chain_json_backend { use super::*; use eth1::DepositLog; - use logging::test_logger; + use logging::create_test_tracing_subscriber; use types::{test_utils::generate_deterministic_keypair, MainnetEthSpec}; fn get_eth1_chain() -> Eth1Chain, E> { + create_test_tracing_subscriber(); + let eth1_config = Eth1Config { ..Eth1Config::default() }; - let log = test_logger(); Eth1Chain::new( - CachingEth1Backend::new(eth1_config, log, Arc::new(MainnetEthSpec::default_spec())) + CachingEth1Backend::new(eth1_config, Arc::new(MainnetEthSpec::default_spec())) .unwrap(), ) } diff --git a/beacon_node/beacon_chain/src/eth1_finalization_cache.rs b/beacon_node/beacon_chain/src/eth1_finalization_cache.rs index 8280d156751..0b9d19e156f 100644 --- a/beacon_node/beacon_chain/src/eth1_finalization_cache.rs +++ b/beacon_node/beacon_chain/src/eth1_finalization_cache.rs @@ -1,7 +1,7 @@ -use slog::{debug, Logger}; use ssz_derive::{Decode, Encode}; use std::cmp; use std::collections::BTreeMap; +use tracing::debug; use types::{Checkpoint, Epoch, Eth1Data, Hash256 as Root}; /// The default size of the cache. @@ -104,28 +104,27 @@ pub struct Eth1FinalizationCache { by_checkpoint: CheckpointMap, pending_eth1: BTreeMap, last_finalized: Option, - log: Logger, } -/// Provides a cache of `Eth1CacheData` at epoch boundaries. This is used to -/// finalize deposits when a new epoch is finalized. -/// -impl Eth1FinalizationCache { - pub fn new(log: Logger) -> Self { - Eth1FinalizationCache { +impl Default for Eth1FinalizationCache { + fn default() -> Self { + Self { by_checkpoint: CheckpointMap::new(), pending_eth1: BTreeMap::new(), last_finalized: None, - log, } } +} - pub fn with_capacity(log: Logger, capacity: usize) -> Self { +/// Provides a cache of `Eth1CacheData` at epoch boundaries. This is used to +/// finalize deposits when a new epoch is finalized. +/// +impl Eth1FinalizationCache { + pub fn with_capacity(capacity: usize) -> Self { Eth1FinalizationCache { by_checkpoint: CheckpointMap::with_capacity(capacity), pending_eth1: BTreeMap::new(), last_finalized: None, - log, } } @@ -136,10 +135,9 @@ impl Eth1FinalizationCache { eth1_finalization_data.eth1_data.clone(), ); debug!( - self.log, - "Eth1Cache: inserted pending eth1"; - "eth1_data.deposit_count" => eth1_finalization_data.eth1_data.deposit_count, - "eth1_deposit_index" => eth1_finalization_data.eth1_deposit_index, + eth1_data.deposit_count = eth1_finalization_data.eth1_data.deposit_count, + eth1_deposit_index = eth1_finalization_data.eth1_deposit_index, + "Eth1Cache: inserted pending eth1" ); } self.by_checkpoint @@ -154,10 +152,8 @@ impl Eth1FinalizationCache { if finalized_deposit_index >= pending_count { result = self.pending_eth1.remove(&pending_count); debug!( - self.log, - "Eth1Cache: dropped pending eth1"; - "pending_count" => pending_count, - "finalized_deposit_index" => finalized_deposit_index, + pending_count, + finalized_deposit_index, "Eth1Cache: dropped pending eth1" ); } else { break; @@ -172,9 +168,8 @@ impl Eth1FinalizationCache { self.last_finalized.clone() } else { debug!( - self.log, - "Eth1Cache: cache miss"; - "epoch" => checkpoint.epoch, + epoch = %checkpoint.epoch, + "Eth1Cache: cache miss" ); None } @@ -194,8 +189,6 @@ impl Eth1FinalizationCache { #[cfg(test)] pub mod tests { use super::*; - use sloggers::null::NullLoggerBuilder; - use sloggers::Build; use std::collections::HashMap; const SLOTS_PER_EPOCH: u64 = 32; @@ -203,8 +196,7 @@ pub mod tests { const EPOCHS_PER_ETH1_VOTING_PERIOD: u64 = 64; fn eth1cache() -> Eth1FinalizationCache { - let log_builder = NullLoggerBuilder; - Eth1FinalizationCache::new(log_builder.build().expect("should build log")) + Eth1FinalizationCache::default() } fn random_eth1_data(deposit_count: u64) -> Eth1Data { diff --git a/beacon_node/beacon_chain/src/events.rs b/beacon_node/beacon_chain/src/events.rs index 8c342893ae3..d09b74e6452 100644 --- a/beacon_node/beacon_chain/src/events.rs +++ b/beacon_node/beacon_chain/src/events.rs @@ -1,7 +1,7 @@ pub use eth2::types::{EventKind, SseBlock, SseFinalizedCheckpoint, SseHead}; -use slog::{trace, Logger}; use tokio::sync::broadcast; use tokio::sync::broadcast::{error::SendError, Receiver, Sender}; +use tracing::trace; use types::EthSpec; const DEFAULT_CHANNEL_CAPACITY: usize = 16; @@ -25,18 +25,14 @@ pub struct ServerSentEventHandler { attester_slashing_tx: Sender>, bls_to_execution_change_tx: Sender>, block_gossip_tx: Sender>, - log: Logger, } impl ServerSentEventHandler { - pub fn new(log: Logger, capacity_multiplier: usize) -> Self { - Self::new_with_capacity( - log, - capacity_multiplier.saturating_mul(DEFAULT_CHANNEL_CAPACITY), - ) + pub fn new(capacity_multiplier: usize) -> Self { + Self::new_with_capacity(capacity_multiplier.saturating_mul(DEFAULT_CHANNEL_CAPACITY)) } - pub fn new_with_capacity(log: Logger, capacity: usize) -> Self { + pub fn new_with_capacity(capacity: usize) -> Self { let (attestation_tx, _) = broadcast::channel(capacity); let (single_attestation_tx, _) = broadcast::channel(capacity); let (block_tx, _) = broadcast::channel(capacity); @@ -75,17 +71,15 @@ impl ServerSentEventHandler { attester_slashing_tx, bls_to_execution_change_tx, block_gossip_tx, - log, } } pub fn register(&self, kind: EventKind) { let log_count = |name, count| { trace!( - self.log, - "Registering server-sent event"; - "kind" => name, - "receiver_count" => count + kind = name, + receiver_count = count, + "Registering server-sent event" ); }; let result = match &kind { @@ -163,7 +157,7 @@ impl ServerSentEventHandler { .map(|count| log_count("block gossip", count)), }; if let Err(SendError(event)) = result { - trace!(self.log, "No receivers registered to listen for event"; "event" => ?event); + trace!(?event, "No receivers registered to listen for event"); } } diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index 720f98e2986..1da8cb413b5 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -17,7 +17,6 @@ use execution_layer::{ }; use fork_choice::{InvalidationOperation, PayloadVerificationStatus}; use proto_array::{Block as ProtoBlock, ExecutionStatus}; -use slog::{debug, warn}; use slot_clock::SlotClock; use state_processing::per_block_processing::{ compute_timestamp_at_slot, get_expected_withdrawals, is_execution_enabled, @@ -25,6 +24,7 @@ use state_processing::per_block_processing::{ }; use std::sync::Arc; use tokio::task::JoinHandle; +use tracing::{debug, warn}; use tree_hash::TreeHash; use types::payload::BlockProductionVersion; use types::*; @@ -85,11 +85,10 @@ impl PayloadNotifier { block_message.try_into()?; if let Err(e) = new_payload_request.perform_optimistic_sync_verifications() { warn!( - chain.log, - "Falling back to slow block hash verification"; - "block_number" => ?block_message.execution_payload().map(|payload| payload.block_number()), - "info" => "you can silence this warning with --disable-optimistic-finalized-sync", - "error" => ?e, + block_number = ?block_message.execution_payload().map(|payload| payload.block_number()), + info = "you can silence this warning with --disable-optimistic-finalized-sync", + error = ?e, + "Falling back to slow block hash verification" ); None } else { @@ -150,16 +149,15 @@ async fn notify_new_payload( ref validation_error, } => { warn!( - chain.log, - "Invalid execution payload"; - "validation_error" => ?validation_error, - "latest_valid_hash" => ?latest_valid_hash, - "execution_block_hash" => ?execution_block_hash, - "root" => ?block.tree_hash_root(), - "graffiti" => block.body().graffiti().as_utf8_lossy(), - "proposer_index" => block.proposer_index(), - "slot" => block.slot(), - "method" => "new_payload", + ?validation_error, + ?latest_valid_hash, + ?execution_block_hash, + root = ?block.tree_hash_root(), + graffiti = block.body().graffiti().as_utf8_lossy(), + proposer_index = block.proposer_index(), + slot = %block.slot(), + method = "new_payload", + "Invalid execution payload" ); // Only trigger payload invalidation in fork choice if the @@ -197,15 +195,14 @@ async fn notify_new_payload( ref validation_error, } => { warn!( - chain.log, - "Invalid execution payload block hash"; - "validation_error" => ?validation_error, - "execution_block_hash" => ?execution_block_hash, - "root" => ?block.tree_hash_root(), - "graffiti" => block.body().graffiti().as_utf8_lossy(), - "proposer_index" => block.proposer_index(), - "slot" => block.slot(), - "method" => "new_payload", + ?validation_error, + ?execution_block_hash, + root = ?block.tree_hash_root(), + graffiti = block.body().graffiti().as_utf8_lossy(), + proposer_index = block.proposer_index(), + slot = %block.slot(), + method = "new_payload", + "Invalid execution payload block hash" ); // Returning an error here should be sufficient to invalidate the block. We have no @@ -278,10 +275,9 @@ pub async fn validate_merge_block( None => { if allow_optimistic_import == AllowOptimisticImport::Yes { debug!( - chain.log, - "Optimistically importing merge transition block"; - "block_hash" => ?execution_payload.parent_hash(), - "msg" => "the terminal block/parent was unavailable" + block_hash = ?execution_payload.parent_hash(), + msg = "the terminal block/parent was unavailable", + "Optimistically importing merge transition block" ); Ok(()) } else { diff --git a/beacon_node/beacon_chain/src/fetch_blobs.rs b/beacon_node/beacon_chain/src/fetch_blobs.rs index 6e365f936dc..3b576da1c79 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, inc_counter_by, TryExt}; -use slog::{debug, error, o, Logger}; +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; 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(BeaconChainError), BlobProcessingError(BlockError), BlobSidecarError(BlobSidecarError), + DataColumnSidecarError(DataColumnSidecarError), ExecutionLayerMissing, InternalError(String), GossipBlob(GossipBlobError), @@ -48,13 +66,9 @@ 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 block_root_str = format!("{:?}", block_root); - let log = chain - .log - .new(o!("service" => "fetch_engine_blobs", "block_root" => block_root_str)); - let versioned_hashes = if let Some(kzg_commitments) = block .message() .body() @@ -67,36 +81,58 @@ pub async fn fetch_and_process_engine_blobs( .map(kzg_commitment_to_versioned_hash) .collect::>() } else { - debug!( - log, - "Fetch blobs not triggered - none required"; - ); + debug!("Fetch blobs not triggered - none required"); 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() .ok_or(FetchEngineBlobError::ExecutionLayerMissing)?; - debug!( - log, - "Fetching blobs from the EL"; - "num_expected_blobs" => num_expected_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); + }) .map_err(FetchEngineBlobError::RequestFailed)?; - if response.is_empty() || response.iter().all(|opt| opt.is_none()) { - debug!( - log, - "No blobs fetched from the EL"; - "num_expected_blobs" => num_expected_blobs, - ); + let num_fetched_blobs = response.iter().filter(|opt| opt.is_some()).count(); + metrics::observe(&metrics::BLOBS_FROM_EL_RECEIVED, num_fetched_blobs as f64); + + if num_fetched_blobs == 0 { + debug!(num_expected_blobs, "No blobs fetched from the EL"); inc_counter(&metrics::BLOBS_FROM_EL_MISS_TOTAL); return Ok(None); } else { @@ -115,20 +151,6 @@ pub async fn fetch_and_process_engine_blobs( &chain.spec, )?; - let num_fetched_blobs = fixed_blob_sidecar_list - .iter() - .filter(|b| b.is_some()) - .count(); - - inc_counter_by( - &metrics::BLOBS_FROM_EL_EXPECTED_TOTAL, - num_expected_blobs as u64, - ); - inc_counter_by( - &metrics::BLOBS_FROM_EL_RECEIVED_TOTAL, - num_fetched_blobs as u64, - ); - // Gossip verify blobs before publishing. This prevents blobs with invalid KZG proofs from // the EL making it into the data availability checker. We do not immediately add these // blobs to the observed blobs/columns cache because we want to allow blobs/columns to arrive on gossip @@ -148,64 +170,103 @@ 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!( - log, - "Not all blobs fetched from the EL"; - "info" => "Unable to compute data columns", - "num_fetched_blobs" => num_fetched_blobs, - "num_expected_blobs" => num_expected_blobs, - ); - return Ok(None); - } + if !blobs_to_import_and_publish.is_empty() { + publish_fn(BlobsOrDataColumns::Blobs(blobs_to_import_and_publish)); + } - if chain - .canonical_head - .fork_choice_read_lock() - .contains_block(&block_root) - { - // Avoid computing columns if block has already been imported. - debug!( - log, - "Ignoring EL blobs response"; - "info" => "block has already been imported", - ); - return Ok(None); - } + debug!(num_fetched_blobs, "Processing engine blobs"); - let data_columns_receiver = spawn_compute_and_publish_data_columns_task( - &chain, - block.clone(), - fixed_blob_sidecar_list.clone(), - publish_fn, - log.clone(), - ); + let availability_processing_status = chain + .process_engine_blobs( + block.slot(), + block_root, + EngineGetBlobsOutput::Blobs(fixed_blob_sidecar_list.clone()), + ) + .await + .map_err(FetchEngineBlobError::BlobProcessingError)?; - Some(data_columns_receiver) + Ok(Some(availability_processing_status)) +} + +async fn fetch_and_process_blobs_v2( + chain: Arc>, + block_root: Hash256, + block: Arc>, + versioned_hashes: Vec, + custody_columns_indices: HashSet, + publish_fn: impl Fn(BlobsOrDataColumns) + Send + 'static, +) -> Result, FetchEngineBlobError> { + let num_expected_blobs = versioned_hashes.len(); + let execution_layer = chain + .execution_layer + .as_ref() + .ok_or(FetchEngineBlobError::ExecutionLayerMissing)?; + + metrics::observe(&metrics::BLOBS_FROM_EL_EXPECTED, num_expected_blobs as f64); + debug!(num_expected_blobs, "Fetching blobs from the EL"); + let response = execution_layer + .get_blobs_v2(versioned_hashes) + .await + .inspect_err(|_| { + inc_counter(&metrics::BLOBS_FROM_EL_ERROR_TOTAL); + }) + .map_err(FetchEngineBlobError::RequestFailed)?; + + let (blobs, proofs): (Vec<_>, Vec<_>) = response + .into_iter() + .filter_map(|blob_and_proof_opt| { + blob_and_proof_opt.map(|blob_and_proof| { + let BlobAndProofV2 { blob, proofs } = blob_and_proof; + (blob, proofs) + }) + }) + .unzip(); + + let num_fetched_blobs = blobs.len(); + metrics::observe(&metrics::BLOBS_FROM_EL_RECEIVED, num_fetched_blobs as f64); + + // Partial blobs response isn't useful for PeerDAS, so we don't bother building and publishing data columns. + if num_fetched_blobs != num_expected_blobs { + debug!( + info = "Unable to compute data columns", + num_fetched_blobs, num_expected_blobs, "Not all blobs fetched from the EL" + ); + inc_counter(&metrics::BLOBS_FROM_EL_MISS_TOTAL); + return Ok(None); } else { - if !blobs_to_import_and_publish.is_empty() { - publish_fn(BlobsOrDataColumns::Blobs(blobs_to_import_and_publish)); - } + inc_counter(&metrics::BLOBS_FROM_EL_HIT_TOTAL); + } - 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); + } - debug!( - log, - "Processing engine blobs"; - "num_fetched_blobs" => num_fetched_blobs, - ); + 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, - fixed_blob_sidecar_list.clone(), - data_columns_receiver_opt, + EngineGetBlobsOutput::CustodyColumns(custody_columns), ) .await .map_err(FetchEngineBlobError::BlobProcessingError)?; @@ -213,79 +274,54 @@ 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( +/// 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, - log: Logger, -) -> oneshot::Receiver>>> { +) -> Result, FetchEngineBlobError> { let chain_cloned = chain.clone(); - let (data_columns_sender, data_columns_receiver) = oneshot::channel(); - - 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 all_data_columns = match data_columns_result { - Ok(d) => d, - Err(e) => { - error!( - log, - "Failed to build data column sidecars from blobs"; - "error" => ?e - ); - return; - } - }; - - 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!( - log, - "Failed to send computed data columns"; + chain + .spawn_blocking_handle( + move || { + let mut timer = metrics::start_timer_vec( + &metrics::DATA_COLUMN_SIDECAR_COMPUTATION, + &[&blobs.len().to_string()], ); - return; - }; - - // 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 !chain_cloned.data_availability_checker.is_supernode() { - return; - } - publish_fn(BlobsOrDataColumns::DataColumns(all_data_columns)); - }, - "compute_and_publish_data_columns", - ); + let blob_refs = blobs.iter().collect::>(); + let cell_proofs = proofs.into_iter().flatten().collect(); + let data_columns_result = blobs_to_data_column_sidecars( + &blob_refs, + cell_proofs, + &block, + &chain_cloned.kzg, + &chain_cloned.spec, + ) + .discard_timer_on_break(&mut timer); + drop(timer); + + // This filtering ensures we only import and publish the custody columns. + // `DataAvailabilityChecker` requires a strict match on custody columns count to + // consider a block available. + let custody_columns = data_columns_result + .map(|mut data_columns| { + data_columns.retain(|col| custody_columns_indices.contains(&col.index)); + data_columns + }) + .map_err(FetchEngineBlobError::DataColumnSidecarError)?; - data_columns_receiver + publish_fn(BlobsOrDataColumns::DataColumns(custody_columns.clone())); + Ok(custody_columns) + }, + "compute_and_publish_data_columns", + ) + .await + .map_err(FetchEngineBlobError::BeaconChainError) + .and_then(|r| r) } fn build_blob_sidecars( diff --git a/beacon_node/beacon_chain/src/fork_revert.rs b/beacon_node/beacon_chain/src/fork_revert.rs index 48ff87fe3c9..cde2950c89b 100644 --- a/beacon_node/beacon_chain/src/fork_revert.rs +++ b/beacon_node/beacon_chain/src/fork_revert.rs @@ -1,7 +1,6 @@ use crate::{BeaconForkChoiceStore, BeaconSnapshot}; use fork_choice::{ForkChoice, PayloadVerificationStatus}; use itertools::process_results; -use slog::{info, warn, Logger}; use state_processing::state_advance::complete_state_advance; use state_processing::{ per_block_processing, per_block_processing::BlockSignatureStrategy, ConsensusContext, @@ -10,6 +9,7 @@ use state_processing::{ use std::sync::Arc; use std::time::Duration; use store::{iter::ParentRootBlockIterator, HotColdDB, ItemStore}; +use tracing::{info, warn}; use types::{BeaconState, ChainSpec, EthSpec, ForkName, Hash256, SignedBeaconBlock, Slot}; const CORRUPT_DB_MESSAGE: &str = "The database could be corrupt. Check its file permissions or \ @@ -27,7 +27,6 @@ pub fn revert_to_fork_boundary, Cold: ItemStore head_block_root: Hash256, store: Arc>, spec: &ChainSpec, - log: &Logger, ) -> Result<(Hash256, SignedBeaconBlock), String> { let current_fork = spec.fork_name_at_slot::(current_slot); let fork_epoch = spec @@ -42,10 +41,9 @@ pub fn revert_to_fork_boundary, Cold: ItemStore } warn!( - log, - "Reverting invalid head block"; - "target_fork" => %current_fork, - "fork_epoch" => fork_epoch, + target_fork = %current_fork, + %fork_epoch, + "Reverting invalid head block" ); let block_iter = ParentRootBlockIterator::fork_tolerant(&store, head_block_root); @@ -55,10 +53,9 @@ pub fn revert_to_fork_boundary, Cold: ItemStore Some((block_root, block)) } else { info!( - log, - "Reverting block"; - "block_root" => ?block_root, - "slot" => block.slot(), + ?block_root, + slot = %block.slot(), + "Reverting block" ); None } diff --git a/beacon_node/beacon_chain/src/fulu_readiness.rs b/beacon_node/beacon_chain/src/fulu_readiness.rs index 872fe58f2ba..1107acad746 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/graffiti_calculator.rs b/beacon_node/beacon_chain/src/graffiti_calculator.rs index 8692d374ed5..23d1d69b1ca 100644 --- a/beacon_node/beacon_chain/src/graffiti_calculator.rs +++ b/beacon_node/beacon_chain/src/graffiti_calculator.rs @@ -1,11 +1,12 @@ use crate::BeaconChain; use crate::BeaconChainTypes; use execution_layer::{http::ENGINE_GET_CLIENT_VERSION_V1, CommitPrefix, ExecutionLayer}; +use logging::crit; use serde::{Deserialize, Serialize}; -use slog::{crit, debug, error, warn, Logger}; use slot_clock::SlotClock; use std::{fmt::Debug, time::Duration}; use task_executor::TaskExecutor; +use tracing::{debug, error, warn}; use types::{EthSpec, Graffiti, GRAFFITI_BYTES_LEN}; const ENGINE_VERSION_AGE_LIMIT_EPOCH_MULTIPLE: u32 = 6; // 6 epochs @@ -51,7 +52,6 @@ pub struct GraffitiCalculator { pub beacon_graffiti: GraffitiOrigin, execution_layer: Option>, pub epoch_duration: Duration, - log: Logger, } impl GraffitiCalculator { @@ -59,13 +59,11 @@ impl GraffitiCalculator { beacon_graffiti: GraffitiOrigin, execution_layer: Option>, epoch_duration: Duration, - log: Logger, ) -> Self { Self { beacon_graffiti, execution_layer, epoch_duration, - log, } } @@ -86,7 +84,7 @@ impl GraffitiCalculator { let Some(execution_layer) = self.execution_layer.as_ref() else { // Return default graffiti if there is no execution layer. This // shouldn't occur if we're actually producing blocks. - crit!(self.log, "No execution layer available for graffiti calculation during block production!"); + crit!("No execution layer available for graffiti calculation during block production!"); return default_graffiti; }; @@ -101,7 +99,7 @@ impl GraffitiCalculator { { Ok(engine_versions) => engine_versions, Err(el_error) => { - warn!(self.log, "Failed to determine execution engine version for graffiti"; "error" => ?el_error); + warn!(error = ?el_error, "Failed to determine execution engine version for graffiti"); return default_graffiti; } }; @@ -109,9 +107,8 @@ impl GraffitiCalculator { let Some(engine_version) = engine_versions.first() else { // Got an empty array which indicates the EL doesn't support the method debug!( - self.log, "Using default lighthouse graffiti: EL does not support {} method", - ENGINE_GET_CLIENT_VERSION_V1; + ENGINE_GET_CLIENT_VERSION_V1 ); return default_graffiti; }; @@ -119,19 +116,20 @@ impl GraffitiCalculator { // More than one version implies lighthouse is connected to // an EL multiplexer. We don't support modifying the graffiti // with these configurations. - warn!( - self.log, - "Execution Engine multiplexer detected, using default graffiti" - ); + warn!("Execution Engine multiplexer detected, using default graffiti"); return default_graffiti; } - let lighthouse_commit_prefix = CommitPrefix::try_from(lighthouse_version::COMMIT_PREFIX.to_string()) - .unwrap_or_else(|error_message| { - // This really shouldn't happen but we want to definitly log if it does - crit!(self.log, "Failed to parse lighthouse commit prefix"; "error" => error_message); - CommitPrefix("00000000".to_string()) - }); + let lighthouse_commit_prefix = + CommitPrefix::try_from(lighthouse_version::COMMIT_PREFIX.to_string()) + .unwrap_or_else(|error_message| { + // This really shouldn't happen but we want to definitly log if it does + crit!( + error = error_message, + "Failed to parse lighthouse commit prefix" + ); + CommitPrefix("00000000".to_string()) + }); engine_version.calculate_graffiti(lighthouse_commit_prefix) } @@ -144,36 +142,24 @@ pub fn start_engine_version_cache_refresh_service( executor: TaskExecutor, ) { let Some(el_ref) = chain.execution_layer.as_ref() else { - debug!( - chain.log, - "No execution layer configured, not starting engine version cache refresh service" - ); + debug!("No execution layer configured, not starting engine version cache refresh service"); return; }; if matches!( chain.graffiti_calculator.beacon_graffiti, GraffitiOrigin::UserSpecified(_) ) { - debug!( - chain.log, - "Graffiti is user-specified, not starting engine version cache refresh service" - ); + debug!("Graffiti is user-specified, not starting engine version cache refresh service"); return; } let execution_layer = el_ref.clone(); - let log = chain.log.clone(); let slot_clock = chain.slot_clock.clone(); let epoch_duration = chain.graffiti_calculator.epoch_duration; executor.spawn( async move { - engine_version_cache_refresh_service::( - execution_layer, - slot_clock, - epoch_duration, - log, - ) - .await + engine_version_cache_refresh_service::(execution_layer, slot_clock, epoch_duration) + .await }, "engine_version_cache_refresh_service", ); @@ -183,13 +169,15 @@ async fn engine_version_cache_refresh_service( execution_layer: ExecutionLayer, slot_clock: T::SlotClock, epoch_duration: Duration, - log: Logger, ) { // Preload the engine version cache after a brief delay to allow for EL initialization. // This initial priming ensures cache readiness before the service's regular update cycle begins. tokio::time::sleep(ENGINE_VERSION_CACHE_PRELOAD_STARTUP_DELAY).await; if let Err(e) = execution_layer.get_engine_version(None).await { - debug!(log, "Failed to preload engine version cache"; "error" => format!("{:?}", e)); + debug!( + error = ?e, + "Failed to preload engine version cache" + ); } // this service should run 3/8 of the way through the epoch @@ -203,18 +191,14 @@ async fn engine_version_cache_refresh_service( let firing_delay = partial_firing_delay + duration_to_next_epoch + epoch_delay; tokio::time::sleep(firing_delay).await; - debug!( - log, - "Engine version cache refresh service firing"; - ); + debug!("Engine version cache refresh service firing"); match execution_layer.get_engine_version(None).await { - Err(e) => warn!(log, "Failed to populate engine version cache"; "error" => ?e), + Err(e) => warn!( error = ?e, "Failed to populate engine version cache"), Ok(versions) => { if versions.is_empty() { // Empty array indicates the EL doesn't support the method debug!( - log, "EL does not support {} method. Sleeping twice as long before retry", ENGINE_GET_CLIENT_VERSION_V1 ); @@ -227,7 +211,7 @@ async fn engine_version_cache_refresh_service( } } None => { - error!(log, "Failed to read slot clock"); + error!("Failed to read slot clock"); // If we can't read the slot clock, just wait another slot. tokio::time::sleep(slot_clock.slot_duration()).await; } @@ -241,10 +225,10 @@ mod tests { use crate::ChainConfig; use execution_layer::test_utils::{DEFAULT_CLIENT_VERSION, DEFAULT_ENGINE_CAPABILITIES}; use execution_layer::EngineCapabilities; - use slog::info; use std::sync::Arc; use std::sync::LazyLock; use std::time::Duration; + use tracing::info; use types::{ChainSpec, Graffiti, Keypair, MinimalEthSpec, GRAFFITI_BYTES_LEN}; const VALIDATOR_COUNT: usize = 48; @@ -261,7 +245,6 @@ mod tests { .spec(spec) .chain_config(chain_config.unwrap_or_default()) .keypairs(KEYPAIRS[0..validator_count].to_vec()) - .logger(logging::test_logger()) .fresh_ephemeral_store() .mock_execution_layer() .build(); @@ -302,7 +285,10 @@ mod tests { let graffiti_str = std::str::from_utf8(graffiti_slice).expect("bytes should convert nicely to ascii"); - info!(harness.chain.log, "results"; "lighthouse_version" => lighthouse_version::VERSION, "graffiti_str" => graffiti_str); + info!( + lighthouse_version = lighthouse_version::VERSION, + graffiti_str, "results" + ); println!("lighthouse_version: '{}'", lighthouse_version::VERSION); println!("graffiti_str: '{}'", graffiti_str); @@ -339,7 +325,7 @@ mod tests { std::str::from_utf8(&found_graffiti_bytes[..expected_graffiti_prefix_len]) .expect("bytes should convert nicely to ascii"); - info!(harness.chain.log, "results"; "expected_graffiti_string" => &expected_graffiti_string, "found_graffiti_string" => &found_graffiti_string); + info!(expected_graffiti_string, found_graffiti_string, "results"); println!("expected_graffiti_string: '{}'", expected_graffiti_string); println!("found_graffiti_string: '{}'", found_graffiti_string); 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 9c06ef33a18..00000000000 --- 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 a48f32e7b40..348e6d52a64 100644 --- a/beacon_node/beacon_chain/src/historical_blocks.rs +++ b/beacon_node/beacon_chain/src/historical_blocks.rs @@ -1,7 +1,6 @@ -use crate::data_availability_checker::AvailableBlock; +use crate::data_availability_checker::{AvailableBlock, AvailableBlockData}; use crate::{metrics, BeaconChain, BeaconChainTypes}; use itertools::Itertools; -use slog::debug; use state_processing::{ per_block_processing::ParallelSignatureSets, signature_sets::{block_proposal_signature_set_from_parts, Error as SignatureSetError}, @@ -12,6 +11,7 @@ use std::time::Duration; use store::metadata::DataColumnInfo; use store::{AnchorInfo, BlobInfo, DBColumn, Error as StoreError, KeyValueStore, KeyValueStoreOp}; use strum::IntoStaticStr; +use tracing::debug; use types::{FixedBytesExtended, Hash256, Slot}; /// Use a longer timeout on the pubkey cache. @@ -82,11 +82,10 @@ impl BeaconChain { if blocks_to_import.len() != total_blocks { debug!( - self.log, - "Ignoring some historic blocks"; - "oldest_block_slot" => anchor_info.oldest_block_slot, - "total_blocks" => total_blocks, - "ignored" => total_blocks.saturating_sub(blocks_to_import.len()), + oldest_block_slot = %anchor_info.oldest_block_slot, + total_blocks, + ignored = total_blocks.saturating_sub(blocks_to_import.len()), + "Ignoring some historic blocks" ); } @@ -94,34 +93,18 @@ impl BeaconChain { return Ok(0); } - // Blobs are stored per block, and data columns are each stored individually - let n_blob_ops_per_block = if self.spec.is_peer_das_scheduled() { - // TODO(das): `available_block includes all sampled columns, but we only need to store - // custody columns. To be clarified in spec PR. - self.data_availability_checker.get_sampling_column_count() - } else { - 1 - }; - - let blob_batch_size = blocks_to_import - .iter() - .filter(|available_block| available_block.blobs().is_some()) - .count() - .saturating_mul(n_blob_ops_per_block); - let mut expected_block_root = anchor_info.oldest_block_parent; let mut prev_block_slot = anchor_info.oldest_block_slot; let mut new_oldest_blob_slot = blob_info.oldest_blob_slot; let mut new_oldest_data_column_slot = data_column_info.oldest_data_column_slot; - let mut blob_batch = Vec::with_capacity(blob_batch_size); + let mut blob_batch = Vec::::new(); let mut cold_batch = Vec::with_capacity(blocks_to_import.len()); let mut hot_batch = Vec::with_capacity(blocks_to_import.len()); let mut signed_blocks = Vec::with_capacity(blocks_to_import.len()); for available_block in blocks_to_import.into_iter().rev() { - let (block_root, block, maybe_blobs, maybe_data_columns) = - available_block.deconstruct(); + let (block_root, block, block_data) = available_block.deconstruct(); if block_root != expected_block_root { return Err(HistoricalBlockError::MismatchedBlockRoot { @@ -144,17 +127,26 @@ impl BeaconChain { ); } - // Store the blobs too - if let Some(blobs) = maybe_blobs { - new_oldest_blob_slot = Some(block.slot()); - self.store - .blobs_as_kv_store_ops(&block_root, blobs, &mut blob_batch); + match &block_data { + AvailableBlockData::NoData => {} + AvailableBlockData::Blobs(..) => { + new_oldest_blob_slot = Some(block.slot()); + } + AvailableBlockData::DataColumns(_) => { + new_oldest_data_column_slot = Some(block.slot()); + } } - // Store the data columns too - if let Some(data_columns) = maybe_data_columns { - new_oldest_data_column_slot = Some(block.slot()); - self.store - .data_columns_as_kv_store_ops(&block_root, data_columns, &mut blob_batch); + + // Store the blobs or data columns too + if let Some(op) = self + .get_blobs_or_columns_store_op(block_root, block_data) + .map_err(|e| { + HistoricalBlockError::StoreError(StoreError::DBError { + message: format!("get_blobs_or_columns_store_op error {e:?}"), + }) + })? + { + blob_batch.extend(self.store.convert_to_kv_batch(vec![op])?); } // Store block roots, including at all skip slots in the freezer DB. diff --git a/beacon_node/beacon_chain/src/kzg_utils.rs b/beacon_node/beacon_chain/src/kzg_utils.rs index 06cce141444..704fb3663ff 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 48168aeb02d..5b79312d371 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -33,7 +33,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 kzg_utils; pub mod light_client_finality_update_verification; @@ -56,6 +55,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 dd25da08471..8e29be97322 100644 --- a/beacon_node/beacon_chain/src/light_client_server_cache.rs +++ b/beacon_node/beacon_chain/src/light_client_server_cache.rs @@ -2,12 +2,12 @@ use crate::errors::BeaconChainError; use crate::{metrics, BeaconChainTypes, BeaconStore}; use parking_lot::{Mutex, RwLock}; use safe_arith::SafeArith; -use slog::{debug, Logger}; use ssz::Decode; use std::num::NonZeroUsize; use std::sync::Arc; use store::DBColumn; use store::KeyValueStore; +use tracing::debug; use tree_hash::TreeHash; use types::non_zero_usize::new_non_zero_usize; use types::{ @@ -82,7 +82,6 @@ impl LightClientServerCache { block_slot: Slot, block_parent_root: &Hash256, sync_aggregate: &SyncAggregate, - log: &Logger, chain_spec: &ChainSpec, ) -> Result<(), BeaconChainError> { metrics::inc_counter(&metrics::LIGHT_CLIENT_SERVER_CACHE_PROCESSING_REQUESTS); @@ -170,9 +169,8 @@ impl LightClientServerCache { )?); } else { debug!( - log, - "Finalized block not available in store for light_client server"; - "finalized_block_root" => format!("{}", cached_parts.finalized_block_root), + finalized_block_root = %cached_parts.finalized_block_root, + "Finalized block not available in store for light_client server" ); } } diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index d1c7a2a5dff..57012161eca 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -601,12 +601,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", @@ -1662,28 +1656,37 @@ pub static DATA_COLUMN_SIDECAR_GOSSIP_VERIFICATION_TIMES: LazyLock> = LazyLock::new(|| { try_create_int_counter( "beacon_blobs_from_el_hit_total", - "Number of blob batches fetched from the execution layer", + "Number of non-empty blob batches fetched from the execution layer", ) }); pub static BLOBS_FROM_EL_MISS_TOTAL: LazyLock> = LazyLock::new(|| { try_create_int_counter( "beacon_blobs_from_el_miss_total", - "Number of blob batches failed to fetch from the execution layer", + "Number of empty or incomplete blob responses from the execution layer", ) }); -pub static BLOBS_FROM_EL_EXPECTED_TOTAL: LazyLock> = LazyLock::new(|| { +pub static BLOBS_FROM_EL_ERROR_TOTAL: LazyLock> = LazyLock::new(|| { try_create_int_counter( - "beacon_blobs_from_el_expected_total", + "beacon_blobs_from_el_error_total", + "Number of failed blob fetches from the execution layer", + ) +}); + +pub static BLOBS_FROM_EL_EXPECTED: LazyLock> = LazyLock::new(|| { + try_create_histogram_with_buckets( + "beacon_blobs_from_el_expected", "Number of blobs expected from the execution layer", + Ok(vec![0.0, 3.0, 6.0, 9.0, 12.0, 18.0, 24.0, 30.0]), ) }); -pub static BLOBS_FROM_EL_RECEIVED_TOTAL: LazyLock> = LazyLock::new(|| { - try_create_int_counter( +pub static BLOBS_FROM_EL_RECEIVED: LazyLock> = LazyLock::new(|| { + try_create_histogram_with_buckets( "beacon_blobs_from_el_received_total", "Number of blobs fetched from the execution layer", + linear_buckets(0.0, 4.0, 20), ) }); diff --git a/beacon_node/beacon_chain/src/migrate.rs b/beacon_node/beacon_chain/src/migrate.rs index c7b4ba07963..94fa0a18909 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 slog::{debug, error, info, warn, Logger}; -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 types::{ - BeaconState, BeaconStateError, BeaconStateHash, Checkpoint, Epoch, EthSpec, FixedBytesExtended, - Hash256, SignedBeaconBlockHash, Slot, -}; +use tracing::{debug, error, info, warn}; +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,9 +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, - log: Logger, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -90,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 { @@ -117,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. @@ -131,26 +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, - log: Logger, - ) -> 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()), @@ -159,14 +146,12 @@ 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. if let Some(Notification::Finalization(notif)) = self.send_background_notification(Notification::Finalization(notif)) { - Self::run_migration(self.db.clone(), notif, &self.log); + Self::run_migration(self.db.clone(), notif); } Ok(()) @@ -203,7 +185,7 @@ impl, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator>, opt_tx: Option>, - log: &Logger, ) { match db.reconstruct_historic_states(Some(BLOCKS_PER_RECONSTRUCTION)) { Ok(()) => { @@ -246,9 +227,8 @@ impl, Cold: ItemStore> BackgroundMigrator ?e + error = ?e, + "Unable to requeue reconstruction notification" ); } } @@ -256,24 +236,18 @@ impl, Cold: ItemStore> BackgroundMigrator { error!( - log, - "State reconstruction failed"; - "error" => ?e, + error = ?e, + "State reconstruction failed" ); } } } - pub fn run_prune_blobs( - db: Arc>, - data_availability_boundary: Epoch, - log: &Logger, - ) { + pub fn run_prune_blobs(db: Arc>, data_availability_boundary: Epoch) { if let Err(e) = db.try_prune_blobs(false, data_availability_boundary) { error!( - log, - "Blob pruning failed"; - "error" => ?e, + error = ?e, + "Blob pruning failed" ); } } @@ -289,7 +263,7 @@ impl, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator format!("{:?}", thread_err) + reason = ?thread_err, + "Migration thread died, so it was restarted" ); } @@ -317,7 +290,6 @@ impl, Cold: ItemStore> BackgroundMigrator>, notif: ManualFinalizationNotification, - log: &Logger, ) { // We create a "dummy" prev migration let prev_migration = PrevMigration { @@ -327,29 +299,22 @@ impl, Cold: ItemStore> BackgroundMigrator>, - notif: FinalizationNotification, - log: &Logger, - ) { + fn run_migration(db: Arc>, notif: FinalizationNotification) { // Do not run too frequently. let epoch = notif.finalized_checkpoint.epoch; let mut prev_migration = notif.prev_migration.lock(); if epoch < prev_migration.epoch + prev_migration.epochs_per_migration { debug!( - log, - "Database consolidation deferred"; - "last_finalized_epoch" => prev_migration.epoch, - "new_finalized_epoch" => epoch, - "epochs_per_migration" => prev_migration.epochs_per_migration, + last_finalized_epoch = %prev_migration.epoch, + new_finalized_epoch = %epoch, + epochs_per_migration = prev_migration.epochs_per_migration, + "Database consolidation deferred" ); return; } @@ -360,7 +325,7 @@ impl, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator state, other => { error!( - log, - "Migrator failed to load state"; - "state_root" => ?finalized_state_root, - "error" => ?other + state_root = ?finalized_state_root, + error = ?other, + "Migrator failed to load state" ); return; } }; - let old_finalized_checkpoint = match Self::prune_abandoned_forks( + match migrate_database( db.clone(), - notif.head_tracker, - finalized_state_root, + 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" + ); + } + 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, - log, ) { Ok(PruningOutcome::Successful { - old_finalized_checkpoint, - }) => old_finalized_checkpoint, + old_finalized_checkpoint_epoch, + }) => old_finalized_checkpoint_epoch, Ok(PruningOutcome::DeferredConcurrentHeadTrackerMutation) => { warn!( - log, - "Pruning deferred because of a concurrent mutation"; - "message" => "this is expected only very rarely!" + message = "this is expected only very rarely!", + "Pruning deferred because of a concurrent mutation" ); return; } @@ -404,39 +383,17 @@ impl, Cold: ItemStore> BackgroundMigrator { warn!( - log, - "Ignoring out of order finalization request"; - "old_finalized_epoch" => old_finalized_checkpoint.epoch, - "new_finalized_epoch" => new_finalized_checkpoint.epoch, - "message" => "this is expected occasionally due to a (harmless) race condition" + old_finalized_epoch = %old_finalized_checkpoint.epoch, + new_finalized_epoch = %new_finalized_checkpoint.epoch, + message = "this is expected occasionally due to a (harmless) race condition", + "Ignoring out of order finalization request" ); return; } - Err(e) => { - warn!(log, "Block pruning failed"; "error" => ?e); - return; - } - }; - - match migrate_database( - db.clone(), - finalized_state_root.into(), - finalized_block_root, - &finalized_state, - ) { - Ok(()) => {} - Err(Error::HotColdDBError(HotColdDBError::FreezeSlotUnaligned(slot))) => { - debug!( - log, - "Database migration postponed, unaligned finalized block"; - "slot" => slot.as_u64() - ); - } Err(e) => { warn!( - log, - "Database migration failed"; - "error" => format!("{:?}", e) + error = ?e, + "Hot DB pruning failed" ); return; } @@ -445,22 +402,21 @@ impl, Cold: ItemStore> BackgroundMigrator format!("{:?}", e)); + warn!(error = ?e, "Database compaction failed"); } - debug!(log, "Database consolidation complete"); + debug!("Database consolidation complete"); } - fn run_manual_compaction(db: Arc>, log: &Logger) { - debug!(log, "Running manual compaction"); - if let Err(e) = db.compact() { - warn!(log, "Database compaction failed"; "error" => format!("{:?}", e)); + fn run_manual_compaction(db: Arc>) { + debug!("Running manual compaction"); + if let Err(error) = db.compact() { + warn!(?error, "Database compaction failed"); } else { - debug!(log, "Manual compaction completed"); + debug!("Manual compaction completed"); } } @@ -469,7 +425,6 @@ impl, Cold: ItemStore> BackgroundMigrator>, - log: Logger, ) -> (mpsc::Sender, thread::JoinHandle<()>) { let (tx, rx) = mpsc::channel(); let inner_tx = tx.clone(); @@ -521,19 +476,19 @@ 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, - log: &Logger, ) -> 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. @@ -579,205 +518,220 @@ impl, Cold: ItemStore> BackgroundMigrator new_finalized_slot { - return Ok(PruningOutcome::OutOfOrderFinalization { - old_finalized_checkpoint, - new_finalized_checkpoint, - }); - } - debug!( - log, - "Starting database pruning"; - "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>>()?; - // 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(); + // 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), + ); - let heads = head_tracker.heads(); - debug!( - log, - "Extra pruning information"; - "old_finalized_root" => format!("{:?}", old_finalized_checkpoint.root), - "new_finalized_root" => format!("{:?}", new_finalized_checkpoint.root), - "head_count" => heads.len(), - ); + // 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, + ), + )); + } - 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()) - } - Err(Error::SszDecodeError(e)) => { - warn!( - log, - "Forgetting invalid head block"; - "block_root" => ?head_hash, - "error" => ?e, - ); - abandoned_heads.insert(head_hash); - continue; - } - Err(e) => return Err(e.into()), - }; + StateSummariesDAG::new_from_v22(state_summaries) + .map_err(|e| PruningError::SummariesDagError("new StateSumariesDAG", e))? + }; - 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!( - log, - "Found a chain that should already have been pruned"; - "head_block_root" => format!("{:?}", head_hash), - "head_slot" => head_slot, - ); - 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()); - } + // 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, + "Prune state summaries dag found more than one root" + ); + } - // 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), - )); - } - } - } - } + // `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))?, + ), + ); - if let Some(abandoned_head) = potentially_abandoned_head { - debug!( - log, - "Pruning head"; - "head_block_root" => format!("{:?}", abandoned_head), - "head_slot" => head_slot, - ); - 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)), - )); + // 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 blocks_to_prune: HashSet = HashSet::new(); + let mut states_to_prune: HashSet<(Slot, Hash256)> = HashSet::new(); + + // 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 (_, 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)); + } } } - // 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(); - - // 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); + 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 + }; + + if should_prune { + blocks_to_prune.insert(block_root); } } - // Then remove them for real. - for head_hash in abandoned_heads { - head_tracker_lock.remove(&head_hash); + // 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); + + 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" + ); } - 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), @@ -785,50 +739,93 @@ 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( db: Arc>, old_finalized_epoch: Epoch, new_finalized_epoch: Epoch, - log: &Logger, ) -> Result<(), Error> { if !db.compact_on_prune() { return Ok(()); @@ -850,10 +847,9 @@ impl, Cold: ItemStore> BackgroundMigrator MIN_COMPACTION_PERIOD_SECONDS) { info!( - log, - "Starting database compaction"; - "old_finalized_epoch" => old_finalized_epoch, - "new_finalized_epoch" => new_finalized_epoch, + %old_finalized_epoch, + %new_finalized_epoch, + "Starting database compaction" ); db.compact()?; @@ -862,7 +858,7 @@ impl, Cold: ItemStore> BackgroundMigrator::EthSpec> + pub fn from_block(block: BeaconBlockRef) -> Self { + Self { + root: block.tree_hash_root(), + slot: block.slot(), + } + } + + pub fn root(&self) -> &Hash256 { + &self.root + } + + pub fn slot(&self) -> &Slot { + &self.slot + } + + pub fn persist_in_store(&self, store: A) -> Result<(), StoreError> + where + T: BeaconChainTypes, + A: AsRef>, + { + if store + .as_ref() + .item_exists::(&self.root)? + { + Ok(()) + } else { + store.as_ref().put_item(&self.root, self) + } + } + + pub fn remove_from_store(&self, store: A) -> Result<(), StoreError> + where + T: BeaconChainTypes, + A: AsRef>, + { + store + .as_ref() + .hot_db + .key_delete(OTBColumn.into(), self.root.as_slice()) + } + + fn is_canonical( + &self, + chain: &BeaconChain, + ) -> Result { + Ok(chain + .forwards_iter_block_roots_until(self.slot, self.slot)? + .next() + .transpose()? + .map(|(root, _)| root) + == Some(self.root)) + } +} + +impl StoreItem for OptimisticTransitionBlock { + fn db_column() -> DBColumn { + OTBColumn + } + + fn as_store_bytes(&self) -> Vec { + self.as_ssz_bytes() + } + + fn from_store_bytes(bytes: &[u8]) -> Result { + Ok(Self::from_ssz_bytes(bytes)?) + } +} + +/// The routine is expected to run once per epoch, 1/4th through the epoch. +pub const EPOCH_DELAY_FACTOR: u32 = 4; + +/// Spawns a routine which checks the validity of any optimistically imported transition blocks +/// +/// This routine will run once per epoch, at `epoch_duration / EPOCH_DELAY_FACTOR` after +/// the start of each epoch. +/// +/// The service will not be started if there is no `execution_layer` on the `chain`. +pub fn start_otb_verification_service( + executor: TaskExecutor, + chain: Arc>, +) { + // Avoid spawning the service if there's no EL, it'll just error anyway. + if chain.execution_layer.is_some() { + executor.spawn( + async move { otb_verification_service(chain).await }, + "otb_verification_service", + ); + } +} + +pub fn load_optimistic_transition_blocks( + chain: &BeaconChain, +) -> Result, StoreError> { + process_results( + chain.store.hot_db.iter_column::(OTBColumn), + |iter| { + iter.map(|(_, bytes)| OptimisticTransitionBlock::from_store_bytes(&bytes)) + .collect() + }, + )? +} + +#[derive(Debug)] +pub enum Error { + ForkChoice(String), + BeaconChain(BeaconChainError), + StoreError(StoreError), + NoBlockFound(OptimisticTransitionBlock), +} + +pub async fn validate_optimistic_transition_blocks( + chain: &Arc>, + otbs: Vec, +) -> Result<(), Error> { + let finalized_slot = chain + .canonical_head + .fork_choice_read_lock() + .get_finalized_block() + .map_err(|e| Error::ForkChoice(format!("{:?}", e)))? + .slot; + + // separate otbs into + // non-canonical + // finalized canonical + // unfinalized canonical + let mut non_canonical_otbs = vec![]; + let (finalized_canonical_otbs, unfinalized_canonical_otbs) = process_results( + otbs.into_iter().map(|otb| { + otb.is_canonical(chain) + .map(|is_canonical| (otb, is_canonical)) + }), + |pair_iter| { + pair_iter + .filter_map(|(otb, is_canonical)| { + if is_canonical { + Some(otb) + } else { + non_canonical_otbs.push(otb); + None + } + }) + .partition::, _>(|otb| *otb.slot() <= finalized_slot) + }, + ) + .map_err(Error::BeaconChain)?; + + // remove non-canonical blocks that conflict with finalized checkpoint from the database + for otb in non_canonical_otbs { + if *otb.slot() <= finalized_slot { + otb.remove_from_store::(&chain.store) + .map_err(Error::StoreError)?; + } + } + + // ensure finalized canonical otb are valid, otherwise kill client + for otb in finalized_canonical_otbs { + match chain.get_block(otb.root()).await { + Ok(Some(block)) => { + match validate_merge_block(chain, block.message(), AllowOptimisticImport::No).await + { + Ok(()) => { + // merge transition block is valid, remove it from OTB + otb.remove_from_store::(&chain.store) + .map_err(Error::StoreError)?; + info!( + block_root = %otb.root(), + "type" = "finalized", + "Validated merge transition block" + ); + } + // The block was not able to be verified by the EL. Leave the OTB in the + // database since the EL is likely still syncing and may verify the block + // later. + Err(BlockError::ExecutionPayloadError( + ExecutionPayloadError::UnverifiedNonOptimisticCandidate, + )) => (), + Err(BlockError::ExecutionPayloadError( + ExecutionPayloadError::InvalidTerminalPoWBlock { .. }, + )) => { + // Finalized Merge Transition Block is Invalid! Kill the Client! + crit!( + msg = "You must use the `--purge-db` flag to clear the database and restart sync. \ + You may be on a hostile network.", + block_hash = ?block.canonical_root(), + "Finalized merge transition block is invalid!" + ); + let mut shutdown_sender = chain.shutdown_sender(); + if let Err(e) = shutdown_sender.try_send(ShutdownReason::Failure( + INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON, + )) { + crit!( + error = ?e, + shutdown_reason = INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON, + "Failed to shut down client" + ); + } + } + _ => {} + } + } + Ok(None) => return Err(Error::NoBlockFound(otb)), + // Our database has pruned the payload and the payload was unavailable on the EL since + // the EL is still syncing or the payload is non-canonical. + Err(BeaconChainError::BlockHashMissingFromExecutionLayer(_)) => (), + Err(e) => return Err(Error::BeaconChain(e)), + } + } + + // attempt to validate any non-finalized canonical otb blocks + for otb in unfinalized_canonical_otbs { + match chain.get_block(otb.root()).await { + Ok(Some(block)) => { + match validate_merge_block(chain, block.message(), AllowOptimisticImport::No).await + { + Ok(()) => { + // merge transition block is valid, remove it from OTB + otb.remove_from_store::(&chain.store) + .map_err(Error::StoreError)?; + info!( + block_root = ?otb.root(), + "type" = "not finalized", + "Validated merge transition block" + ); + } + // The block was not able to be verified by the EL. Leave the OTB in the + // database since the EL is likely still syncing and may verify the block + // later. + Err(BlockError::ExecutionPayloadError( + ExecutionPayloadError::UnverifiedNonOptimisticCandidate, + )) => (), + Err(BlockError::ExecutionPayloadError( + ExecutionPayloadError::InvalidTerminalPoWBlock { .. }, + )) => { + // Unfinalized Merge Transition Block is Invalid -> Run process_invalid_execution_payload + warn!( + block_root = ?otb.root(), + "Merge transition block invalid" + ); + chain + .process_invalid_execution_payload( + &InvalidationOperation::InvalidateOne { + block_root: *otb.root(), + }, + ) + .await + .map_err(|e| { + warn!( + error = ?e, + location = "process_invalid_execution_payload", + "Error checking merge transition block" + ); + Error::BeaconChain(e) + })?; + } + _ => {} + } + } + Ok(None) => return Err(Error::NoBlockFound(otb)), + // Our database has pruned the payload and the payload was unavailable on the EL since + // the EL is still syncing or the payload is non-canonical. + Err(BeaconChainError::BlockHashMissingFromExecutionLayer(_)) => (), + Err(e) => return Err(Error::BeaconChain(e)), + } + } + + Ok(()) +} + +/// Loop until any optimistically imported merge transition blocks have been verified and +/// the merge has been finalized. +async fn otb_verification_service(chain: Arc>) { + let epoch_duration = chain.slot_clock.slot_duration() * T::EthSpec::slots_per_epoch() as u32; + loop { + match chain + .slot_clock + .duration_to_next_epoch(T::EthSpec::slots_per_epoch()) + { + Some(duration) => { + let additional_delay = epoch_duration / EPOCH_DELAY_FACTOR; + sleep(duration + additional_delay).await; + + debug!("OTB verification service firing"); + + if !is_merge_transition_complete( + &chain.canonical_head.cached_head().snapshot.beacon_state, + ) { + // We are pre-merge. Nothing to do yet. + continue; + } + + // load all optimistically imported transition blocks from the database + match load_optimistic_transition_blocks(chain.as_ref()) { + Ok(otbs) => { + if otbs.is_empty() { + if chain + .canonical_head + .fork_choice_read_lock() + .get_finalized_block() + .map_or(false, |block| { + block.execution_status.is_execution_enabled() + }) + { + // there are no optimistic blocks in the database, we can exit + // the service since the merge transition is finalized and we'll + // never see another transition block + break; + } else { + debug!( + info = "waiting for the merge transition to finalize", + "No optimistic transition blocks" + ) + } + } + if let Err(e) = validate_optimistic_transition_blocks(&chain, otbs).await { + warn!( + error = ?e, + "Error while validating optimistic transition blocks" + ); + } + } + Err(e) => { + error!( + error = ?e, + "Error loading optimistic transition blocks" + ); + } + }; + } + None => { + error!("Failed to read slot clock"); + // If we can't read the slot clock, just wait another slot. + sleep(chain.slot_clock.slot_duration()).await; + } + }; + } + debug!( + msg = "shutting down OTB verification service", + "No optimistic transition blocks in database" + ); +} diff --git a/beacon_node/beacon_chain/src/persisted_beacon_chain.rs b/beacon_node/beacon_chain/src/persisted_beacon_chain.rs index adb68def0df..83affb0dcd1 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/pre_finalization_cache.rs b/beacon_node/beacon_chain/src/pre_finalization_cache.rs index 22b76e026cb..5bd45dc59f6 100644 --- a/beacon_node/beacon_chain/src/pre_finalization_cache.rs +++ b/beacon_node/beacon_chain/src/pre_finalization_cache.rs @@ -2,9 +2,9 @@ use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; use itertools::process_results; use lru::LruCache; use parking_lot::Mutex; -use slog::debug; use std::num::NonZeroUsize; use std::time::Duration; +use tracing::debug; use types::non_zero_usize::new_non_zero_usize; use types::Hash256; @@ -87,10 +87,7 @@ impl BeaconChain { // blocks have been flushed out. Solving this issue isn't as simple as hooking the // beacon processor's functions that handle failed blocks because we need the block root // and it has been erased from the `BlockError` by that point. - debug!( - self.log, - "Pre-finalization lookup cache is full"; - ); + debug!("Pre-finalization lookup cache is full"); } Ok(false) } diff --git a/beacon_node/beacon_chain/src/proposer_prep_service.rs b/beacon_node/beacon_chain/src/proposer_prep_service.rs index 140a9659fce..14f7414abcc 100644 --- a/beacon_node/beacon_chain/src/proposer_prep_service.rs +++ b/beacon_node/beacon_chain/src/proposer_prep_service.rs @@ -1,9 +1,9 @@ use crate::{BeaconChain, BeaconChainTypes}; -use slog::{debug, error}; use slot_clock::SlotClock; use std::sync::Arc; use task_executor::TaskExecutor; use tokio::time::sleep; +use tracing::{debug, error}; /// Spawns a routine which ensures the EL is provided advance notice of any block producers. /// @@ -38,10 +38,7 @@ async fn proposer_prep_service( slot_duration.saturating_sub(chain.config.prepare_payload_lookahead); sleep(duration + additional_delay).await; - debug!( - chain.log, - "Proposer prepare routine firing"; - ); + debug!("Proposer prepare routine firing"); let inner_chain = chain.clone(); executor.spawn( @@ -50,20 +47,19 @@ async fn proposer_prep_service( if let Err(e) = inner_chain.prepare_beacon_proposer(current_slot).await { error!( - inner_chain.log, - "Proposer prepare routine failed"; - "error" => ?e + error = ?e, + "Proposer prepare routine failed" ); } } else { - debug!(inner_chain.log, "No slot for proposer prepare routine"); + debug!("No slot for proposer prepare routine"); } }, "proposer_prep_update", ); } None => { - error!(chain.log, "Failed to read slot clock"); + error!("Failed to read slot clock"); // If we can't read the slot clock, just wait another slot. sleep(slot_duration).await; } diff --git a/beacon_node/beacon_chain/src/schema_change.rs b/beacon_node/beacon_chain/src/schema_change.rs index 95049012292..49aa116f6c6 100644 --- a/beacon_node/beacon_chain/src/schema_change.rs +++ b/beacon_node/beacon_chain/src/schema_change.rs @@ -2,9 +2,9 @@ mod migration_schema_v20; mod migration_schema_v21; mod migration_schema_v22; +mod migration_schema_v23; use crate::beacon_chain::BeaconChainTypes; -use slog::Logger; use std::sync::Arc; use store::hot_cold_store::{HotColdDB, HotColdDBError}; use store::metadata::{SchemaVersion, CURRENT_SCHEMA_VERSION}; @@ -17,7 +17,6 @@ pub fn migrate_schema( genesis_state_root: Option, from: SchemaVersion, to: SchemaVersion, - log: Logger, ) -> Result<(), StoreError> { match (from, to) { // Migrating from the current schema version to itself is always OK, a no-op. @@ -25,39 +24,47 @@ pub fn migrate_schema( // Upgrade across multiple versions by recursively migrating one step at a time. (_, _) if from.as_u64() + 1 < to.as_u64() => { let next = SchemaVersion(from.as_u64() + 1); - migrate_schema::(db.clone(), genesis_state_root, from, next, log.clone())?; - migrate_schema::(db, genesis_state_root, next, to, log) + migrate_schema::(db.clone(), genesis_state_root, from, next)?; + migrate_schema::(db, genesis_state_root, next, to) } // Downgrade across multiple versions by recursively migrating one step at a time. (_, _) if to.as_u64() + 1 < from.as_u64() => { let next = SchemaVersion(from.as_u64() - 1); - migrate_schema::(db.clone(), genesis_state_root, from, next, log.clone())?; - migrate_schema::(db, genesis_state_root, next, to, log) + migrate_schema::(db.clone(), genesis_state_root, from, next)?; + migrate_schema::(db, genesis_state_root, next, to) } // // Migrations from before SchemaVersion(19) are deprecated. // (SchemaVersion(19), SchemaVersion(20)) => { - let ops = migration_schema_v20::upgrade_to_v20::(db.clone(), log)?; + let ops = migration_schema_v20::upgrade_to_v20::(db.clone())?; db.store_schema_version_atomically(to, ops) } (SchemaVersion(20), SchemaVersion(19)) => { - let ops = migration_schema_v20::downgrade_from_v20::(db.clone(), log)?; + let ops = migration_schema_v20::downgrade_from_v20::(db.clone())?; db.store_schema_version_atomically(to, ops) } (SchemaVersion(20), SchemaVersion(21)) => { - let ops = migration_schema_v21::upgrade_to_v21::(db.clone(), log)?; + let ops = migration_schema_v21::upgrade_to_v21::(db.clone())?; db.store_schema_version_atomically(to, ops) } (SchemaVersion(21), SchemaVersion(20)) => { - let ops = migration_schema_v21::downgrade_from_v21::(db.clone(), log)?; + let ops = migration_schema_v21::downgrade_from_v21::(db.clone())?; db.store_schema_version_atomically(to, ops) } (SchemaVersion(21), SchemaVersion(22)) => { // This migration needs to sync data between hot and cold DBs. The schema version is // bumped inside the upgrade_to_v22 fn - migration_schema_v22::upgrade_to_v22::(db.clone(), genesis_state_root, log) + 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 { diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v20.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v20.rs index d556d5988d6..13fde349f56 100644 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v20.rs +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v20.rs @@ -2,16 +2,15 @@ use crate::beacon_chain::{BeaconChainTypes, OP_POOL_DB_KEY}; use operation_pool::{ PersistedOperationPool, PersistedOperationPoolV15, PersistedOperationPoolV20, }; -use slog::{debug, info, Logger}; use std::sync::Arc; use store::{Error, HotColdDB, KeyValueStoreOp, StoreItem}; +use tracing::{debug, info}; use types::Attestation; pub fn upgrade_to_v20( db: Arc>, - log: Logger, ) -> Result, Error> { - info!(log, "Upgrading from v19 to v20"); + info!("Upgrading from v19 to v20"); // Load a V15 op pool and transform it to V20. let Some(PersistedOperationPoolV15:: { @@ -24,7 +23,7 @@ pub fn upgrade_to_v20( capella_bls_change_broadcast_indices, }) = db.get_item(&OP_POOL_DB_KEY)? else { - debug!(log, "Nothing to do, no operation pool stored"); + debug!("Nothing to do, no operation pool stored"); return Ok(vec![]); }; @@ -52,9 +51,8 @@ pub fn upgrade_to_v20( pub fn downgrade_from_v20( db: Arc>, - log: Logger, ) -> Result, Error> { - info!(log, "Downgrading from v20 to v19"); + info!("Downgrading from v20 to v19"); // Load a V20 op pool and transform it to V15. let Some(PersistedOperationPoolV20:: { @@ -67,7 +65,7 @@ pub fn downgrade_from_v20( capella_bls_change_broadcast_indices, }) = db.get_item(&OP_POOL_DB_KEY)? else { - debug!(log, "Nothing to do, no operation pool stored"); + debug!("Nothing to do, no operation pool stored"); return Ok(vec![]); }; @@ -77,7 +75,10 @@ pub fn downgrade_from_v20( if let Attestation::Base(attestation) = attestation.into() { Some((attestation, indices)) } else { - info!(log, "Dropping attestation during downgrade"; "reason" => "not a base attestation"); + info!( + reason = "not a base attestation", + "Dropping attestation during downgrade" + ); None } }) @@ -88,7 +89,10 @@ pub fn downgrade_from_v20( .filter_map(|slashing| match slashing.try_into() { Ok(slashing) => Some(slashing), Err(_) => { - info!(log, "Dropping attester slashing during downgrade"; "reason" => "not a base attester slashing"); + info!( + reason = "not a base attester slashing", + "Dropping attester slashing during downgrade" + ); None } }) diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v21.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v21.rs index f02f5ee6f3a..d73660cf3c4 100644 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v21.rs +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v21.rs @@ -1,18 +1,17 @@ use crate::beacon_chain::BeaconChainTypes; use crate::validator_pubkey_cache::DatabasePubkey; -use slog::{info, Logger}; use ssz::{Decode, Encode}; use std::sync::Arc; use store::{DBColumn, Error, HotColdDB, KeyValueStore, KeyValueStoreOp, StoreItem}; +use tracing::info; use types::{Hash256, PublicKey}; const LOG_EVERY: usize = 200_000; pub fn upgrade_to_v21( db: Arc>, - log: Logger, ) -> Result, Error> { - info!(log, "Upgrading from v20 to v21"); + info!("Upgrading from v20 to v21"); let mut ops = vec![]; @@ -29,22 +28,20 @@ pub fn upgrade_to_v21( if i > 0 && i % LOG_EVERY == 0 { info!( - log, - "Public key decompression in progress"; - "keys_decompressed" => i + keys_decompressed = i, + "Public key decompression in progress" ); } } - info!(log, "Public key decompression complete"); + info!("Public key decompression complete"); Ok(ops) } pub fn downgrade_from_v21( db: Arc>, - log: Logger, ) -> Result, Error> { - info!(log, "Downgrading from v21 to v20"); + info!("Downgrading from v21 to v20"); let mut ops = vec![]; @@ -67,15 +64,11 @@ pub fn downgrade_from_v21( )); if i > 0 && i % LOG_EVERY == 0 { - info!( - log, - "Public key compression in progress"; - "keys_compressed" => i - ); + info!(keys_compressed = i, "Public key compression in progress"); } } - info!(log, "Public key compression complete"); + info!("Public key compression complete"); Ok(ops) } diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs index 982c3ded467..0b64fdbe08e 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 @@ -1,5 +1,4 @@ use crate::beacon_chain::BeaconChainTypes; -use slog::{info, Logger}; use std::sync::Arc; use store::chunked_iter::ChunkedVectorIter; use store::{ @@ -10,6 +9,7 @@ use store::{ partial_beacon_state::PartialBeaconState, AnchorInfo, DBColumn, Error, HotColdDB, KeyValueStore, KeyValueStoreOp, }; +use tracing::info; use types::{BeaconState, Hash256, Slot}; const LOG_EVERY: usize = 200_000; @@ -40,9 +40,8 @@ fn load_old_schema_frozen_state( pub fn upgrade_to_v22( db: Arc>, genesis_state_root: Option, - log: Logger, ) -> Result<(), Error> { - info!(log, "Upgrading from v21 to v22"); + info!("Upgrading from v21 to v22"); let old_anchor = db.get_anchor_info(); @@ -71,9 +70,8 @@ pub fn upgrade_to_v22( // this write. if split_slot > 0 { info!( - log, - "Re-storing genesis state"; - "state_root" => ?genesis_state_root, + state_root = ?genesis_state_root, + "Re-storing genesis state" ); db.store_cold_state(&genesis_state_root, &genesis_state, &mut cold_ops)?; } @@ -87,7 +85,6 @@ pub fn upgrade_to_v22( oldest_block_slot, split_slot, &mut cold_ops, - &log, )?; // Commit this first batch of non-destructive cold database ops. @@ -107,14 +104,13 @@ pub fn upgrade_to_v22( db.store_schema_version_atomically(SchemaVersion(22), hot_ops)?; // Finally, clean up the old-format data from the freezer database. - delete_old_schema_freezer_data::(&db, &log)?; + delete_old_schema_freezer_data::(&db)?; Ok(()) } pub fn delete_old_schema_freezer_data( db: &Arc>, - log: &Logger, ) -> Result<(), Error> { let mut cold_ops = vec![]; @@ -140,11 +136,7 @@ pub fn delete_old_schema_freezer_data( } let delete_ops = cold_ops.len(); - info!( - log, - "Deleting historic states"; - "delete_ops" => delete_ops, - ); + info!(delete_ops, "Deleting historic states"); db.cold_db.do_atomically(cold_ops)?; // In order to reclaim space, we need to compact the freezer DB as well. @@ -159,13 +151,11 @@ pub fn write_new_schema_block_roots( oldest_block_slot: Slot, split_slot: Slot, cold_ops: &mut Vec, - log: &Logger, ) -> Result<(), Error> { info!( - log, - "Starting beacon block root migration"; - "oldest_block_slot" => oldest_block_slot, - "genesis_block_root" => ?genesis_block_root, + %oldest_block_slot, + ?genesis_block_root, + "Starting beacon block root migration" ); // Store the genesis block root if it would otherwise not be stored. @@ -196,9 +186,8 @@ pub fn write_new_schema_block_roots( if i > 0 && i % LOG_EVERY == 0 { info!( - log, - "Beacon block root migration in progress"; - "roots_migrated" => i + roots_migrated = i, + "Beacon block root migration in progress" ); } } 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 00000000000..e66178df535 --- /dev/null +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v23.rs @@ -0,0 +1,147 @@ +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 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> { + // 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) + { + ops.push(KeyValueStoreOp::DeleteKey( + DBColumn::BeaconStateTemporary, + state_root_result?.as_slice().to_vec(), + )); + } + + 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/shuffling_cache.rs b/beacon_node/beacon_chain/src/shuffling_cache.rs index dec73a763fe..1aa23c28fcf 100644 --- a/beacon_node/beacon_chain/src/shuffling_cache.rs +++ b/beacon_node/beacon_chain/src/shuffling_cache.rs @@ -2,9 +2,8 @@ use std::collections::HashMap; use std::sync::Arc; use itertools::Itertools; -use slog::{debug, Logger}; - use oneshot_broadcast::{oneshot, Receiver, Sender}; +use tracing::debug; use types::{ beacon_state::CommitteeCache, AttestationShufflingId, BeaconState, Epoch, EthSpec, Hash256, RelativeEpoch, @@ -61,16 +60,14 @@ pub struct ShufflingCache { cache: HashMap, cache_size: usize, head_shuffling_ids: BlockShufflingIds, - logger: Logger, } impl ShufflingCache { - pub fn new(cache_size: usize, head_shuffling_ids: BlockShufflingIds, logger: Logger) -> Self { + pub fn new(cache_size: usize, head_shuffling_ids: BlockShufflingIds) -> Self { Self { cache: HashMap::new(), cache_size, head_shuffling_ids, - logger, } } @@ -179,10 +176,9 @@ impl ShufflingCache { for shuffling_id in shuffling_ids_to_prune.iter() { debug!( - self.logger, - "Removing old shuffling from cache"; - "shuffling_epoch" => shuffling_id.shuffling_epoch, - "shuffling_decision_block" => ?shuffling_id.shuffling_decision_block + shuffling_epoch = %shuffling_id.shuffling_epoch, + shuffling_decision_block = ?shuffling_id.shuffling_decision_block, + "Removing old shuffling from cache" ); self.cache.remove(shuffling_id); } @@ -294,10 +290,10 @@ impl BlockShufflingIds { #[cfg(not(debug_assertions))] #[cfg(test)] mod test { - use task_executor::test_utils::test_logger; use types::*; use crate::test_utils::EphemeralHarnessType; + use logging::create_test_tracing_subscriber; use super::*; @@ -308,6 +304,8 @@ mod test { // Creates a new shuffling cache for testing fn new_shuffling_cache() -> ShufflingCache { + create_test_tracing_subscriber(); + let current_epoch = 8; let head_shuffling_ids = BlockShufflingIds { current: shuffling_id(current_epoch), @@ -315,8 +313,8 @@ mod test { previous: Some(shuffling_id(current_epoch - 1)), block_root: Hash256::from_low_u64_le(0), }; - let logger = test_logger(); - ShufflingCache::new(TEST_CACHE_SIZE, head_shuffling_ids, logger) + + ShufflingCache::new(TEST_CACHE_SIZE, head_shuffling_ids) } /// Returns two different committee caches for testing. diff --git a/beacon_node/beacon_chain/src/state_advance_timer.rs b/beacon_node/beacon_chain/src/state_advance_timer.rs index 1d8bfff216b..9135c3ce880 100644 --- a/beacon_node/beacon_chain/src/state_advance_timer.rs +++ b/beacon_node/beacon_chain/src/state_advance_timer.rs @@ -17,16 +17,15 @@ use crate::validator_monitor::HISTORIC_EPOCHS as VALIDATOR_MONITOR_HISTORIC_EPOC use crate::{ chain_config::FORK_CHOICE_LOOKAHEAD_FACTOR, BeaconChain, BeaconChainError, BeaconChainTypes, }; -use slog::{debug, error, warn, Logger}; use slot_clock::SlotClock; use state_processing::per_slot_processing; 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}; use types::{AttestationShufflingId, BeaconStateError, EthSpec, Hash256, RelativeEpoch, Slot}; /// If the head slot is more than `MAX_ADVANCE_DISTANCE` from the current slot, then don't perform @@ -107,10 +106,9 @@ impl Lock { pub fn spawn_state_advance_timer( executor: TaskExecutor, beacon_chain: Arc>, - log: Logger, ) { executor.spawn( - state_advance_timer(executor.clone(), beacon_chain, log), + state_advance_timer(executor.clone(), beacon_chain), "state_advance_timer", ); } @@ -119,7 +117,6 @@ pub fn spawn_state_advance_timer( async fn state_advance_timer( executor: TaskExecutor, beacon_chain: Arc>, - log: Logger, ) { let is_running = Lock::new(); let slot_clock = &beacon_chain.slot_clock; @@ -127,7 +124,7 @@ async fn state_advance_timer( loop { let Some(duration_to_next_slot) = beacon_chain.slot_clock.duration_to_next_slot() else { - error!(log, "Failed to read slot clock"); + error!("Failed to read slot clock"); // If we can't read the slot clock, just wait another slot. sleep(slot_duration).await; continue; @@ -161,9 +158,8 @@ async fn state_advance_timer( Ok(slot) => slot, Err(e) => { warn!( - log, - "Unable to determine slot in state advance timer"; - "error" => ?e + error = ?e, + "Unable to determine slot in state advance timer" ); // If we can't read the slot clock, just wait another slot. sleep(slot_duration).await; @@ -173,37 +169,27 @@ async fn state_advance_timer( // Only spawn the state advance task if the lock was previously free. if !is_running.lock() { - let log = log.clone(); let beacon_chain = beacon_chain.clone(); let is_running = is_running.clone(); executor.spawn_blocking( move || { - match advance_head(&beacon_chain, &log) { + match advance_head(&beacon_chain) { Ok(()) => (), Err(Error::BeaconChain(e)) => error!( - log, - "Failed to advance head state"; - "error" => ?e - ), - Err(Error::StateAlreadyAdvanced { block_root }) => debug!( - log, - "State already advanced on slot"; - "block_root" => ?block_root + error = ?e, + "Failed to advance head state" ), + Err(Error::StateAlreadyAdvanced { block_root }) => { + debug!(?block_root, "State already advanced on slot") + } Err(Error::MaxDistanceExceeded { current_slot, head_slot, - }) => debug!( - log, - "Refused to advance head state"; - "head_slot" => head_slot, - "current_slot" => current_slot, - ), + }) => debug!(%head_slot, %current_slot, "Refused to advance head state"), other => warn!( - log, - "Did not advance head state"; - "reason" => ?other + reason = ?other, + "Did not advance head state" ), }; @@ -214,9 +200,8 @@ async fn state_advance_timer( ); } else { warn!( - log, - "State advance routine overloaded"; - "msg" => "system resources may be overloaded" + msg = "system resources may be overloaded", + "State advance routine overloaded" ) } @@ -225,7 +210,6 @@ async fn state_advance_timer( // Wait for the fork choice instant (which may already be past). sleep_until(fork_choice_instant).await; - let log = log.clone(); let beacon_chain = beacon_chain.clone(); let next_slot = current_slot + 1; executor.spawn( @@ -245,10 +229,9 @@ async fn state_advance_timer( .await .unwrap_or_else(|e| { warn!( - log, - "Unable to prepare proposer with lookahead"; - "error" => ?e, - "slot" => next_slot, + error = ?e, + slot = %next_slot, + "Unable to prepare proposer with lookahead" ); None }); @@ -261,10 +244,9 @@ async fn state_advance_timer( if let Some(tx) = &beacon_chain.fork_choice_signal_tx { if let Err(e) = tx.notify_fork_choice_complete(next_slot) { warn!( - log, - "Error signalling fork choice waiter"; - "error" => ?e, - "slot" => next_slot, + error = ?e, + slot = %next_slot, + "Error signalling fork choice waiter" ); } } @@ -282,10 +264,7 @@ async fn state_advance_timer( /// slot then placed in the `state_cache` to be used for block verification. /// /// See the module-level documentation for rationale. -fn advance_head( - beacon_chain: &Arc>, - log: &Logger, -) -> Result<(), Error> { +fn advance_head(beacon_chain: &Arc>) -> Result<(), Error> { let current_slot = beacon_chain.slot()?; // These brackets ensure that the `head_slot` value is dropped before we run fork choice and @@ -317,7 +296,7 @@ fn advance_head( // 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 => { @@ -344,10 +323,9 @@ fn advance_head( // Expose Prometheus metrics. if let Err(e) = summary.observe_metrics() { error!( - log, - "Failed to observe epoch summary metrics"; - "src" => "state_advance_timer", - "error" => ?e + src = "state_advance_timer", + error = ?e, + "Failed to observe epoch summary metrics" ); } @@ -362,20 +340,18 @@ fn advance_head( .process_validator_statuses(state.current_epoch(), &summary, &beacon_chain.spec) { error!( - log, - "Unable to process validator statuses"; - "error" => ?e + error = ?e, + "Unable to process validator statuses" ); } } } debug!( - log, - "Advanced head state one slot"; - "head_block_root" => ?head_block_root, - "state_slot" => state.slot(), - "current_slot" => current_slot, + ?head_block_root, + state_slot = %state.slot(), + %current_slot, + "Advanced head state one slot" ); // Build the current epoch cache, to prepare to compute proposer duties. @@ -420,12 +396,11 @@ fn advance_head( .insert_committee_cache(shuffling_id.clone(), committee_cache); debug!( - log, - "Primed proposer and attester caches"; - "head_block_root" => ?head_block_root, - "next_epoch_shuffling_root" => ?shuffling_id.shuffling_decision_block, - "state_epoch" => state.current_epoch(), - "current_epoch" => current_slot.epoch(T::EthSpec::slots_per_epoch()), + ?head_block_root, + next_epoch_shuffling_root = ?shuffling_id.shuffling_decision_block, + state_epoch = %state.current_epoch(), + current_epoch = %current_slot.epoch(T::EthSpec::slots_per_epoch()), + "Primed proposer and attester caches" ); } @@ -447,37 +422,28 @@ fn advance_head( let current_slot = beacon_chain.slot()?; if starting_slot < current_slot { warn!( - log, - "State advance too slow"; - "head_block_root" => %head_block_root, - "advanced_slot" => final_slot, - "current_slot" => current_slot, - "starting_slot" => starting_slot, - "msg" => "system resources may be overloaded", + %head_block_root, + advanced_slot = %final_slot, + %current_slot, + %starting_slot, + msg = "system resources may be overloaded", + "State advance too slow" ); } - // 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!( - log, - "Completed state advance"; - "head_block_root" => ?head_block_root, - "advanced_slot" => final_slot, - "initial_slot" => initial_slot, + ?head_block_root, + advanced_slot = %final_slot, + %initial_slot, + "Completed state advance" ); Ok(()) 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 00000000000..ab379d1eb2e --- /dev/null +++ b/beacon_node/beacon_chain/src/summaries_dag.rs @@ -0,0 +1,464 @@ +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, + }, + StateSummariesNotContiguous { + state_root: Hash256, + state_slot: Slot, + latest_block_root: Hash256, + parent_block_root: Box, + parent_block_latest_state_summary: Box>, + }, + 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. + let parent_block_root = summary.block_parent_root; + if let Some(parent_block_summaries) = + state_summaries_by_block_root.get(&parent_block_root) + { + *parent_block_summaries + .get(&previous_slot) + // Should never error: summaries are contiguous, so if there's an + // entry it must contain at least one summary at the previous slot. + .ok_or(Error::StateSummariesNotContiguous { + state_root: *state_root, + state_slot: summary.slot, + latest_block_root: summary.latest_block_root, + parent_block_root: parent_block_root.into(), + parent_block_latest_state_summary: parent_block_summaries + .iter() + .max_by(|a, b| a.0.cmp(b.0)) + .map(|(slot, (state_root, _))| (*slot, **state_root)) + .into(), + })? + .0 + } else { + // We don't know of any summary with this parent block root. We'll + // consider this summary to be a root of `state_summaries_v22` + // collection and mark it as zero. + // The test store_tests::finalizes_non_epoch_start_slot manages to send two + // disjoint trees on its second migration. + Hash256::ZERO + } + } + }; + + 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_rewards.rs b/beacon_node/beacon_chain/src/sync_committee_rewards.rs index 9b35cff9432..cf4cf1fff21 100644 --- a/beacon_node/beacon_chain/src/sync_committee_rewards.rs +++ b/beacon_node/beacon_chain/src/sync_committee_rewards.rs @@ -1,11 +1,11 @@ use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; -use eth2::lighthouse::SyncCommitteeReward; +use eth2::types::SyncCommitteeReward; use safe_arith::SafeArith; -use slog::error; use state_processing::per_block_processing::altair::sync_committee::compute_sync_aggregate_rewards; use std::collections::HashMap; use store::RelativeEpoch; +use tracing::error; use types::{AbstractExecPayload, BeaconBlockRef, BeaconState}; impl BeaconChain { @@ -31,8 +31,8 @@ impl BeaconChain { let (participant_reward_value, proposer_reward_per_bit) = compute_sync_aggregate_rewards(state, spec).map_err(|e| { error!( - self.log, "Error calculating sync aggregate rewards"; - "error" => ?e + error = ?e, + "Error calculating sync aggregate rewards" ); BeaconChainError::SyncCommitteeRewardsSyncError })?; diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index fa26c9dbd9d..ca083f05721 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -35,26 +35,21 @@ pub use genesis::{InteropGenesisBuilder, DEFAULT_ETH1_BLOCK_HASH}; use int_to_bytes::int_to_bytes32; use kzg::trusted_setup::get_trusted_setup; use kzg::{Kzg, TrustedSetup}; +use logging::create_test_tracing_subscriber; use merkle_proof::MerkleTree; use operation_pool::ReceivedPreCapella; -use parking_lot::Mutex; -use parking_lot::RwLockWriteGuard; +use parking_lot::{Mutex, RwLockWriteGuard}; use rand::rngs::StdRng; use rand::Rng; use rand::SeedableRng; use rayon::prelude::*; use sensitive_url::SensitiveUrl; -use slog::{o, Drain, Logger}; -use slog_async::Async; -use slog_term::{FullFormat, PlainSyncDecorator, TermDecorator}; use slot_clock::{SlotClock, TestingSlotClock}; use state_processing::per_block_processing::compute_timestamp_at_slot; use state_processing::state_advance::complete_state_advance; use std::borrow::Cow; use std::collections::{HashMap, HashSet}; use std::fmt; -use std::fs::{File, OpenOptions}; -use std::io::BufWriter; use std::str::FromStr; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, LazyLock}; @@ -235,7 +230,6 @@ pub struct Builder { genesis_state_builder: Option>, import_all_data_columns: bool, runtime: TestRuntime, - log: Logger, } impl Builder> { @@ -247,12 +241,8 @@ impl Builder> { .expect("cannot build without validator keypairs"); let store = Arc::new( - HotColdDB::open_ephemeral( - self.store_config.clone().unwrap_or_default(), - spec.clone(), - self.log.clone(), - ) - .unwrap(), + HotColdDB::open_ephemeral(self.store_config.clone().unwrap_or_default(), spec.clone()) + .unwrap(), ); let genesis_state_builder = self.genesis_state_builder.take().unwrap_or_else(|| { // Set alternating withdrawal credentials if no builder is specified. @@ -283,12 +273,8 @@ impl Builder> { let spec = self.spec.as_ref().expect("cannot build without spec"); let store = Arc::new( - HotColdDB::open_ephemeral( - self.store_config.clone().unwrap_or_default(), - spec.clone(), - self.log.clone(), - ) - .unwrap(), + HotColdDB::open_ephemeral(self.store_config.clone().unwrap_or_default(), spec.clone()) + .unwrap(), ); let mutator = move |builder: BeaconChainBuilder<_>| { builder @@ -372,7 +358,6 @@ where { pub fn new(eth_spec_instance: E) -> Self { let runtime = TestRuntime::default(); - let log = runtime.log.clone(); Self { eth_spec_instance, @@ -391,7 +376,6 @@ where genesis_state_builder: None, import_all_data_columns: false, runtime, - log, } } @@ -439,12 +423,6 @@ where self } - pub fn logger(mut self, log: Logger) -> Self { - self.log = log.clone(); - self.runtime.set_logger(log); - self - } - /// This mutator will be run before the `store_mutator`. pub fn initial_mutator(mut self, mutator: BoxedMutator) -> Self { assert!( @@ -501,12 +479,8 @@ where suggested_fee_recipient: Some(Address::repeat_byte(42)), ..Default::default() }; - let execution_layer = ExecutionLayer::from_config( - config, - self.runtime.task_executor.clone(), - self.log.clone(), - ) - .unwrap(); + let execution_layer = + ExecutionLayer::from_config(config, self.runtime.task_executor.clone()).unwrap(); self.execution_layer = Some(execution_layer); self @@ -586,7 +560,6 @@ where pub fn build(self) -> BeaconChainHarness> { let (shutdown_tx, shutdown_receiver) = futures::channel::mpsc::channel(1); - let log = self.log; let spec = self.spec.expect("cannot build without spec"); let seconds_per_slot = spec.seconds_per_slot; let validator_keypairs = self @@ -599,7 +572,6 @@ where let chain_config = self.chain_config.unwrap_or_default(); let mut builder = BeaconChainBuilder::new(self.eth_spec_instance, kzg.clone()) - .logger(log.clone()) .custom_spec(spec.clone()) .store(self.store.expect("cannot build without store")) .store_migrator_config( @@ -614,11 +586,9 @@ where .shutdown_sender(shutdown_tx) .chain_config(chain_config) .import_all_data_columns(self.import_all_data_columns) - .event_handler(Some(ServerSentEventHandler::new_with_capacity( - log.clone(), - 5, - ))) - .validator_monitor_config(validator_monitor_config); + .event_handler(Some(ServerSentEventHandler::new_with_capacity(5))) + .validator_monitor_config(validator_monitor_config) + .rng(Box::new(StdRng::seed_from_u64(42))); builder = if let Some(mutator) = self.initial_mutator { mutator(builder) @@ -645,6 +615,12 @@ where let chain = builder.build().expect("should build"); + let sampling_column_count = if self.import_all_data_columns { + chain.spec.number_of_custody_groups as usize + } else { + chain.spec.custody_requirement as usize + }; + BeaconChainHarness { spec: chain.spec.clone(), chain: Arc::new(chain), @@ -655,6 +631,7 @@ where mock_execution_layer: self.mock_execution_layer, mock_builder: None, rng: make_rng(), + sampling_column_count, } } } @@ -711,6 +688,7 @@ pub struct BeaconChainHarness { pub mock_execution_layer: Option>, pub mock_builder: Option>>, + pub sampling_column_count: usize, pub rng: Mutex, } @@ -737,13 +715,10 @@ where Cold: ItemStore, { pub fn builder(eth_spec_instance: E) -> Builder> { + create_test_tracing_subscriber(); Builder::new(eth_spec_instance) } - pub fn logger(&self) -> &slog::Logger { - &self.chain.log - } - pub fn execution_block_generator(&self) -> RwLockWriteGuard<'_, ExecutionBlockGenerator> { self.mock_execution_layer .as_ref() @@ -815,6 +790,10 @@ where (0..self.validator_keypairs.len()).collect() } + pub fn get_sampling_column_count(&self) -> usize { + self.sampling_column_count + } + pub fn slots_per_epoch(&self) -> u64 { E::slots_per_epoch() } @@ -914,6 +893,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, @@ -2365,7 +2366,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) @@ -2375,8 +2376,14 @@ where .into_iter() .map(CustodyDataColumn::from_asserted_custody) .collect::>(); - RpcBlock::new_with_custody_columns(Some(block_root), block, custody_columns, &self.spec) - .unwrap() + RpcBlock::new_with_custody_columns( + Some(block_root), + block, + custody_columns, + self.get_sampling_column_count(), + &self.spec, + ) + .unwrap() } else { let blobs = self.chain.get_blobs(&block_root).unwrap().blobs(); RpcBlock::new(Some(block_root), block, blobs).unwrap() @@ -2391,10 +2398,7 @@ where blob_items: Option<(KzgProofs, BlobsList)>, ) -> Result, BlockError> { Ok(if self.spec.is_peer_das_enabled_for_epoch(block.epoch()) { - let sampling_column_count = self - .chain - .data_availability_checker - .get_sampling_column_count(); + let sampling_column_count = self.get_sampling_column_count(); if blob_items.is_some_and(|(_, blobs)| !blobs.is_empty()) { // Note: this method ignores the actual custody columns and just take the first @@ -2405,9 +2409,15 @@ where .take(sampling_column_count) .map(CustodyDataColumn::from_asserted_custody) .collect::>(); - RpcBlock::new_with_custody_columns(Some(block_root), block, columns, &self.spec)? + RpcBlock::new_with_custody_columns( + Some(block_root), + block, + columns, + sampling_column_count, + &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 @@ -2618,7 +2628,6 @@ where return; } - let log = self.logger(); let contributions = self.make_sync_contributions(state, block_root, slot, RelativeSyncCommittee::Current); @@ -2649,7 +2658,6 @@ where slot, &block_root, &sync_aggregate, - log, &self.spec, ); } @@ -2713,16 +2721,16 @@ where let mut block_hash_from_slot: HashMap = HashMap::new(); let mut state_hash_from_slot: HashMap = HashMap::new(); for slot in slots { - let (block_hash, new_state) = self - .add_attested_block_at_slot_with_sync( - *slot, - state, - state_root, - validators, - sync_committee_strategy, - ) - .await - .unwrap(); + // Using a `Box::pin` to reduce the stack size. Clippy was raising a lints. + let (block_hash, new_state) = Box::pin(self.add_attested_block_at_slot_with_sync( + *slot, + state, + state_root, + validators, + sync_committee_strategy, + )) + .await + .unwrap(); state = new_state; @@ -3106,10 +3114,7 @@ where let is_peerdas_enabled = self.chain.spec.is_peer_das_enabled_for_epoch(block.epoch()); if is_peerdas_enabled { let custody_columns = custody_columns_opt.unwrap_or_else(|| { - let sampling_column_count = self - .chain - .data_availability_checker - .get_sampling_column_count() as u64; + let sampling_column_count = self.get_sampling_column_count() as u64; (0..sampling_column_count).collect() }); @@ -3159,58 +3164,6 @@ pub struct MakeAttestationOptions { pub fork: Fork, } -pub enum LoggerType { - Test, - // The logs are output to files for each test. - CI, - // No logs will be printed. - Null, -} - -fn ci_decorator() -> PlainSyncDecorator> { - let log_dir = std::env::var(CI_LOGGER_DIR_ENV_VAR).unwrap_or_else(|e| { - panic!("{CI_LOGGER_DIR_ENV_VAR} env var must be defined when using ci_logger: {e:?}"); - }); - let fork_name = std::env::var(FORK_NAME_ENV_VAR) - .map(|s| format!("{s}_")) - .unwrap_or_default(); - // The current test name can be got via the thread name. - let test_name = std::thread::current() - .name() - .unwrap() - .to_string() - // Colons are not allowed in files that are uploaded to GitHub Artifacts. - .replace("::", "_"); - let log_path = format!("/{log_dir}/{fork_name}{test_name}.log"); - let file = OpenOptions::new() - .create(true) - .append(true) - .open(log_path) - .unwrap(); - let file = BufWriter::new(file); - PlainSyncDecorator::new(file) -} - -pub fn build_log(level: slog::Level, logger_type: LoggerType) -> Logger { - match logger_type { - LoggerType::Test => { - let drain = FullFormat::new(TermDecorator::new().build()).build().fuse(); - let drain = Async::new(drain).chan_size(10_000).build().fuse(); - Logger::root(drain.filter_level(level).fuse(), o!()) - } - LoggerType::CI => { - let drain = FullFormat::new(ci_decorator()).build().fuse(); - let drain = Async::new(drain).chan_size(10_000).build().fuse(); - Logger::root(drain.filter_level(level).fuse(), o!()) - } - LoggerType::Null => { - let drain = FullFormat::new(TermDecorator::new().build()).build().fuse(); - let drain = Async::new(drain).build().fuse(); - Logger::root(drain.filter(|_| false).fuse(), o!()) - } - } -} - pub enum NumBlobs { Random, Number(usize), @@ -3241,7 +3194,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) { @@ -3261,7 +3214,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(); @@ -3280,7 +3233,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/src/validator_monitor.rs b/beacon_node/beacon_chain/src/validator_monitor.rs index cb27f0727a3..16f4e3f1431 100644 --- a/beacon_node/beacon_chain/src/validator_monitor.rs +++ b/beacon_node/beacon_chain/src/validator_monitor.rs @@ -5,9 +5,9 @@ use crate::beacon_proposer_cache::{BeaconProposerCache, TYPICAL_SLOTS_PER_EPOCH}; use crate::metrics; use itertools::Itertools; +use logging::crit; use parking_lot::{Mutex, RwLock}; use serde::{Deserialize, Serialize}; -use slog::{crit, debug, error, info, warn, Logger}; use slot_clock::SlotClock; use smallvec::SmallVec; use state_processing::common::get_attestation_participation_flag_indices; @@ -21,6 +21,7 @@ use std::str::Utf8Error; use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use store::AbstractExecPayload; +use tracing::{debug, error, info, instrument, warn}; use types::consts::altair::{ TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX, }; @@ -30,7 +31,6 @@ use types::{ IndexedAttestationRef, ProposerSlashing, PublicKeyBytes, SignedAggregateAndProof, SignedContributionAndProof, Slot, SyncCommitteeMessage, VoluntaryExit, }; - /// Used for Prometheus labels. /// /// We've used `total` for this value to align with Nimbus, as per: @@ -401,15 +401,18 @@ pub struct ValidatorMonitor { beacon_proposer_cache: Arc>, // Unaggregated attestations generated by the committee index at each slot. unaggregated_attestations: HashMap>, - log: Logger, _phantom: PhantomData, } impl ValidatorMonitor { + #[instrument(parent = None, + level = "info", + name = "validator_monitor", + skip_all + )] pub fn new( config: ValidatorMonitorConfig, beacon_proposer_cache: Arc>, - log: Logger, ) -> Self { let ValidatorMonitorConfig { auto_register, @@ -425,7 +428,6 @@ impl ValidatorMonitor { missed_blocks: <_>::default(), beacon_proposer_cache, unaggregated_attestations: <_>::default(), - log, _phantom: PhantomData, }; for pubkey in validators { @@ -437,11 +439,23 @@ impl ValidatorMonitor { /// Returns `true` when the validator count is sufficiently low enough to /// emit metrics and logs on a per-validator basis (rather than just an /// aggregated basis). + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] fn individual_tracking(&self) -> bool { self.validators.len() <= self.individual_tracking_threshold } /// Add some validators to `self` for additional monitoring. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn add_validator_pubkey(&mut self, pubkey: PublicKeyBytes) { let index_opt = self .indices @@ -449,18 +463,22 @@ impl ValidatorMonitor { .find(|(_, candidate_pk)| **candidate_pk == pubkey) .map(|(index, _)| *index); - let log = self.log.clone(); self.validators.entry(pubkey).or_insert_with(|| { info!( - log, - "Started monitoring validator"; - "pubkey" => %pubkey, + %pubkey, + "Started monitoring validator" ); MonitoredValidator::new(pubkey, index_opt) }); } /// Add an unaggregated attestation + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn set_unaggregated_attestation(&mut self, attestation: Attestation) { let unaggregated_attestations = &mut self.unaggregated_attestations; @@ -474,12 +492,24 @@ impl ValidatorMonitor { self.unaggregated_attestations.insert(slot, attestation); } + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn get_unaggregated_attestation(&self, slot: Slot) -> Option<&Attestation> { self.unaggregated_attestations.get(&slot) } /// Reads information from the given `state`. The `state` *must* be valid (i.e, able to be /// imported). + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn process_valid_state( &mut self, current_epoch: Epoch, @@ -592,6 +622,12 @@ impl ValidatorMonitor { } /// Add missed non-finalized blocks for the monitored validators + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] fn add_validators_missed_blocks(&mut self, state: &BeaconState) { // Define range variables let current_slot = state.slot(); @@ -661,28 +697,25 @@ impl ValidatorMonitor { ); }); error!( - self.log, - "Validator missed a block"; - "index" => i, - "slot" => slot, - "parent block root" => ?prev_block_root, + index = i, + %slot, + ?prev_block_root, + "Validator missed a block" ); } } } else { warn!( - self.log, - "Missing validator index"; - "info" => "potentially inconsistency in the validator manager", - "index" => i, + info = "potentially inconsistency in the validator manager", + index = i, + "Missing validator index" ) } } else { debug!( - self.log, - "Could not get proposers from cache"; - "epoch" => ?slot_epoch, - "decision_root" => ?shuffling_decision_block, + epoch = ?slot_epoch, + decision_root = ?shuffling_decision_block, + "Could not get proposers from cache" ); } } @@ -691,6 +724,12 @@ impl ValidatorMonitor { } } + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] fn get_proposers_by_epoch_from_cache( &mut self, epoch: Epoch, @@ -704,6 +743,12 @@ impl ValidatorMonitor { /// Process the unaggregated attestations generated by the service `attestation_simulator_service` /// and check if the attestation qualifies for a reward matching the flags source/target/head + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] fn process_unaggregated_attestations(&mut self, state: &BeaconState, spec: &ChainSpec) { let current_slot = state.slot(); @@ -744,27 +789,23 @@ impl ValidatorMonitor { let head_hit = flag_indices.contains(&TIMELY_HEAD_FLAG_INDEX); let target_hit = flag_indices.contains(&TIMELY_TARGET_FLAG_INDEX); let source_hit = flag_indices.contains(&TIMELY_SOURCE_FLAG_INDEX); - register_simulated_attestation( - data, head_hit, target_hit, source_hit, &self.log, - ) + register_simulated_attestation(data, head_hit, target_hit, source_hit) } Err(BeaconStateError::IncorrectAttestationSource) => { - register_simulated_attestation(data, false, false, false, &self.log) + register_simulated_attestation(data, false, false, false) } Err(err) => { error!( - self.log, - "Failed to get attestation participation flag indices"; - "error" => ?err, - "unaggregated_attestation" => ?unaggregated_attestation, + error = ?err, + ?unaggregated_attestation, + "Failed to get attestation participation flag indices" ); } } } else { error!( - self.log, - "Failed to remove unaggregated attestation from the hashmap"; - "slot" => ?slot, + ?slot, + "Failed to remove unaggregated attestation from the hashmap" ); } } @@ -780,6 +821,12 @@ impl ValidatorMonitor { /// /// We allow disabling tracking metrics on an individual validator basis /// since it can result in untenable cardinality with high validator counts. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] fn aggregatable_metric(&self, individual_id: &str, func: F) { func(TOTAL_LABEL); @@ -788,6 +835,12 @@ impl ValidatorMonitor { } } + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn process_validator_statuses( &self, epoch: Epoch, @@ -867,13 +920,12 @@ impl ValidatorMonitor { attestation_success.push(id); if self.individual_tracking() { debug!( - self.log, - "Previous epoch attestation success"; - "matched_source" => previous_epoch_matched_source, - "matched_target" => previous_epoch_matched_target, - "matched_head" => previous_epoch_matched_head, - "epoch" => prev_epoch, - "validator" => id, + matched_source = previous_epoch_matched_source, + matched_target = previous_epoch_matched_target, + matched_head = previous_epoch_matched_head, + epoch = %prev_epoch, + validator = id, + "Previous epoch attestation success" ) } } else { @@ -886,10 +938,9 @@ impl ValidatorMonitor { attestation_miss.push(id); if self.individual_tracking() { debug!( - self.log, - "Previous epoch attestation missing"; - "epoch" => prev_epoch, - "validator" => id, + epoch = %prev_epoch, + validator = id, + "Previous epoch attestation missing" ) } } @@ -912,10 +963,9 @@ impl ValidatorMonitor { head_miss.push(id); if self.individual_tracking() { debug!( - self.log, - "Attestation failed to match head"; - "epoch" => prev_epoch, - "validator" => id, + epoch = %prev_epoch, + validator = id, + "Attestation failed to match head" ); } } @@ -938,10 +988,9 @@ impl ValidatorMonitor { target_miss.push(id); if self.individual_tracking() { debug!( - self.log, - "Attestation failed to match target"; - "epoch" => prev_epoch, - "validator" => id, + epoch = %prev_epoch, + validator = id, + "Attestation failed to match target" ); } } @@ -960,12 +1009,11 @@ impl ValidatorMonitor { suboptimal_inclusion.push(id); if self.individual_tracking() { debug!( - self.log, - "Potential sub-optimal inclusion delay"; - "optimal" => spec.min_attestation_inclusion_delay, - "delay" => inclusion_delay, - "epoch" => prev_epoch, - "validator" => id, + optimal = spec.min_attestation_inclusion_delay, + delay = inclusion_delay, + epoch = %prev_epoch, + validator = id, + "Potential sub-optimal inclusion delay" ); } } @@ -1003,12 +1051,11 @@ impl ValidatorMonitor { // logs that can be generated is capped by the size // of the sync committee. info!( - self.log, - "Current epoch sync signatures"; - "included" => summary.sync_signature_block_inclusions, - "expected" => E::slots_per_epoch(), - "epoch" => current_epoch, - "validator" => id, + included = summary.sync_signature_block_inclusions, + expected = E::slots_per_epoch(), + epoch = %current_epoch, + validator = id, + "Current epoch sync signatures" ); } } else if self.individual_tracking() { @@ -1018,10 +1065,9 @@ impl ValidatorMonitor { 0, ); debug!( - self.log, - "Validator isn't part of the current sync committee"; - "epoch" => current_epoch, - "validator" => id, + epoch = %current_epoch, + validator = id, + "Validator isn't part of the current sync committee" ); } } @@ -1032,51 +1078,52 @@ impl ValidatorMonitor { // for all validators managed by the validator monitor. if !attestation_success.is_empty() { info!( - self.log, - "Previous epoch attestation(s) success"; - "epoch" => prev_epoch, - "validators" => ?attestation_success, + epoch = %prev_epoch, + validators = ?attestation_success, + "Previous epoch attestation(s) success" ); } if !attestation_miss.is_empty() { info!( - self.log, - "Previous epoch attestation(s) missing"; - "epoch" => prev_epoch, - "validators" => ?attestation_miss, + epoch = %prev_epoch, + validators = ?attestation_miss, + "Previous epoch attestation(s) missing" ); } if !head_miss.is_empty() { info!( - self.log, - "Previous epoch attestation(s) failed to match head"; - "epoch" => prev_epoch, - "validators" => ?head_miss, + epoch = %prev_epoch, + validators = ?head_miss, + "Previous epoch attestation(s) failed to match head" ); } if !target_miss.is_empty() { info!( - self.log, - "Previous epoch attestation(s) failed to match target"; - "epoch" => prev_epoch, - "validators" => ?target_miss, + epoch = %prev_epoch, + validators = ?target_miss, + "Previous epoch attestation(s) failed to match target" ); } if !suboptimal_inclusion.is_empty() { info!( - self.log, - "Previous epoch attestation(s) had sub-optimal inclusion delay"; - "epoch" => prev_epoch, - "validators" => ?suboptimal_inclusion, + epoch = %prev_epoch, + validators = ?suboptimal_inclusion, + "Previous epoch attestation(s) had sub-optimal inclusion delay" ); } Ok(()) } + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] fn get_validator(&self, validator_index: u64) -> Option<&MonitoredValidator> { self.indices .get(&validator_index) @@ -1084,15 +1131,33 @@ impl ValidatorMonitor { } /// Returns the number of validators monitored by `self`. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn num_validators(&self) -> usize { self.validators.len() } - // Return the `id`'s of all monitored validators. + /// Return the `id`'s of all monitored validators. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn get_all_monitored_validators(&self) -> Vec { self.validators.values().map(|val| val.id.clone()).collect() } + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn get_monitored_validator(&self, index: u64) -> Option<&MonitoredValidator> { if let Some(pubkey) = self.indices.get(&index) { self.validators.get(pubkey) @@ -1101,6 +1166,12 @@ impl ValidatorMonitor { } } + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn get_monitored_validator_missed_block_count(&self, validator_index: u64) -> u64 { self.missed_blocks .iter() @@ -1108,12 +1179,24 @@ impl ValidatorMonitor { .count() as u64 } + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn get_beacon_proposer_cache(&self) -> Arc> { self.beacon_proposer_cache.clone() } /// If `self.auto_register == true`, add the `validator_index` to `self.monitored_validators`. /// Otherwise, do nothing. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn auto_register_local_validator(&mut self, validator_index: u64) { if !self.auto_register { return; @@ -1122,10 +1205,9 @@ impl ValidatorMonitor { if let Some(pubkey) = self.indices.get(&validator_index) { if !self.validators.contains_key(pubkey) { info!( - self.log, - "Started monitoring validator"; - "pubkey" => %pubkey, - "validator" => %validator_index, + %pubkey, + validator = %validator_index, + "Started monitoring validator" ); self.validators.insert( @@ -1137,6 +1219,12 @@ impl ValidatorMonitor { } /// Process a block received on gossip. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn register_gossip_block( &self, seen_timestamp: Duration, @@ -1148,6 +1236,12 @@ impl ValidatorMonitor { } /// Process a block received on the HTTP API from a local validator. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn register_api_block( &self, seen_timestamp: Duration, @@ -1158,6 +1252,12 @@ impl ValidatorMonitor { self.register_beacon_block("api", seen_timestamp, block, block_root, slot_clock) } + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] fn register_beacon_block( &self, src: &str, @@ -1184,13 +1284,12 @@ impl ValidatorMonitor { }); info!( - self.log, - "Block from monitored validator"; - "root" => ?block_root, - "delay" => %delay.as_millis(), - "slot" => %block.slot(), - "src" => src, - "validator" => %id, + ?block_root, + delay = %delay.as_millis(), + slot = %block.slot(), + src, + validator = %id, + "Block from monitored validator" ); validator.with_epoch_summary(epoch, |summary| summary.register_block(delay)); @@ -1198,6 +1297,12 @@ impl ValidatorMonitor { } /// Register an attestation seen on the gossip network. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn register_gossip_unaggregated_attestation( &self, seen_timestamp: Duration, @@ -1213,6 +1318,12 @@ impl ValidatorMonitor { } /// Register an attestation seen on the HTTP API. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn register_api_unaggregated_attestation( &self, seen_timestamp: Duration, @@ -1227,6 +1338,12 @@ impl ValidatorMonitor { ) } + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] fn register_unaggregated_attestation( &self, src: &str, @@ -1261,15 +1378,14 @@ impl ValidatorMonitor { if self.individual_tracking() { info!( - self.log, - "Unaggregated attestation"; - "head" => ?data.beacon_block_root, - "index" => %data.index, - "delay_ms" => %delay.as_millis(), - "epoch" => %epoch, - "slot" => %data.slot, - "src" => src, - "validator" => %id, + head = ?data.beacon_block_root, + index = %data.index, + delay_ms = %delay.as_millis(), + %epoch, + slot = %data.slot, + src, + validator = %id, + "Unaggregated attestation" ); } @@ -1314,6 +1430,12 @@ impl ValidatorMonitor { ) } + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] fn register_aggregated_attestation( &self, src: &str, @@ -1349,15 +1471,14 @@ impl ValidatorMonitor { if self.individual_tracking() { info!( - self.log, - "Aggregated attestation"; - "head" => ?data.beacon_block_root, - "index" => %data.index, - "delay_ms" => %delay.as_millis(), - "epoch" => %epoch, - "slot" => %data.slot, - "src" => src, - "validator" => %id, + head = ?data.beacon_block_root, + index = %data.index, + delay_ms = %delay.as_millis(), + %epoch, + slot = %data.slot, + src, + validator = %id, + "Aggregated attestation" ); } @@ -1396,28 +1517,26 @@ impl ValidatorMonitor { if is_first_inclusion_aggregate { info!( - self.log, - "Attestation included in aggregate"; - "head" => ?data.beacon_block_root, - "index" => %data.index, - "delay_ms" => %delay.as_millis(), - "epoch" => %epoch, - "slot" => %data.slot, - "src" => src, - "validator" => %id, + head = ?data.beacon_block_root, + index = %data.index, + delay_ms = %delay.as_millis(), + %epoch, + slot = %data.slot, + src, + validator = %id, + "Attestation included in aggregate" ); } else { // Downgrade to Debug for second and onwards of logging to reduce verbosity debug!( - self.log, - "Attestation included in aggregate"; - "head" => ?data.beacon_block_root, - "index" => %data.index, - "delay_ms" => %delay.as_millis(), - "epoch" => %epoch, - "slot" => %data.slot, - "src" => src, - "validator" => %id, + head = ?data.beacon_block_root, + index = %data.index, + delay_ms = %delay.as_millis(), + %epoch, + slot = %data.slot, + src, + validator = %id, + "Attestation included in aggregate" ) }; } @@ -1435,6 +1554,11 @@ impl ValidatorMonitor { /// We use the parent slot instead of block slot to ignore skip slots when calculating inclusion distance. /// /// Note: Blocks that get orphaned will skew the inclusion distance calculation. + #[instrument(parent = None, + level = "info", + name = "validator_monitor", + skip_all + )] pub fn register_attestation_in_block( &self, indexed_attestation: IndexedAttestationRef<'_, E>, @@ -1480,26 +1604,24 @@ impl ValidatorMonitor { if is_first_inclusion_block { info!( - self.log, - "Attestation included in block"; - "head" => ?data.beacon_block_root, - "index" => %data.index, - "inclusion_lag" => format!("{} slot(s)", delay), - "epoch" => %epoch, - "slot" => %data.slot, - "validator" => %id, + head = ?data.beacon_block_root, + index = %data.index, + inclusion_lag = format!("{} slot(s)", delay), + %epoch, + slot = %data.slot, + validator = %id, + "Attestation included in block" ); } else { // Downgrade to Debug for second and onwards of logging to reduce verbosity debug!( - self.log, - "Attestation included in block"; - "head" => ?data.beacon_block_root, - "index" => %data.index, - "inclusion_lag" => format!("{} slot(s)", delay), - "epoch" => %epoch, - "slot" => %data.slot, - "validator" => %id, + head = ?data.beacon_block_root, + index = %data.index, + inclusion_lag = format!("{} slot(s)", delay), + %epoch, + slot = %data.slot, + validator = %id, + "Attestation included in block" ); } } @@ -1512,6 +1634,12 @@ impl ValidatorMonitor { } /// Register a sync committee message received over gossip. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn register_gossip_sync_committee_message( &self, seen_timestamp: Duration, @@ -1527,6 +1655,12 @@ impl ValidatorMonitor { } /// Register a sync committee message received over the http api. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn register_api_sync_committee_message( &self, seen_timestamp: Duration, @@ -1542,6 +1676,12 @@ impl ValidatorMonitor { } /// Register a sync committee message. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] fn register_sync_committee_message( &self, src: &str, @@ -1574,14 +1714,13 @@ impl ValidatorMonitor { if self.individual_tracking() { info!( - self.log, - "Sync committee message"; - "head" => %sync_committee_message.beacon_block_root, - "delay_ms" => %delay.as_millis(), - "epoch" => %epoch, - "slot" => %sync_committee_message.slot, - "src" => src, - "validator" => %id, + head = %sync_committee_message.beacon_block_root, + delay_ms = %delay.as_millis(), + %epoch, + slot = %sync_committee_message.slot, + src, + validator = %id, + "Sync committee message" ); } @@ -1592,6 +1731,12 @@ impl ValidatorMonitor { } /// Register a sync committee contribution received over gossip. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn register_gossip_sync_committee_contribution( &self, seen_timestamp: Duration, @@ -1609,6 +1754,12 @@ impl ValidatorMonitor { } /// Register a sync committee contribution received over the http api. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn register_api_sync_committee_contribution( &self, seen_timestamp: Duration, @@ -1626,6 +1777,12 @@ impl ValidatorMonitor { } /// Register a sync committee contribution. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] fn register_sync_committee_contribution( &self, src: &str, @@ -1662,14 +1819,13 @@ impl ValidatorMonitor { if self.individual_tracking() { info!( - self.log, - "Sync contribution"; - "head" => %beacon_block_root, - "delay_ms" => %delay.as_millis(), - "epoch" => %epoch, - "slot" => %slot, - "src" => src, - "validator" => %id, + head = %beacon_block_root, + delay_ms = %delay.as_millis(), + %epoch, + %slot, + src, + validator = %id, + "Sync contribution" ); } @@ -1691,14 +1847,13 @@ impl ValidatorMonitor { if self.individual_tracking() { info!( - self.log, - "Sync signature included in contribution"; - "head" => %beacon_block_root, - "delay_ms" => %delay.as_millis(), - "epoch" => %epoch, - "slot" => %slot, - "src" => src, - "validator" => %id, + head = %beacon_block_root, + delay_ms = %delay.as_millis(), + %epoch, + %slot, + src, + validator = %id, + "Sync signature included in contribution" ); } @@ -1710,6 +1865,12 @@ impl ValidatorMonitor { } /// Register that the `sync_aggregate` was included in a *valid* `BeaconBlock`. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn register_sync_aggregate_in_block( &self, slot: Slot, @@ -1731,12 +1892,11 @@ impl ValidatorMonitor { if self.individual_tracking() { info!( - self.log, - "Sync signature included in block"; - "head" => %beacon_block_root, - "epoch" => %epoch, - "slot" => %slot, - "validator" => %id, + head = %beacon_block_root, + %epoch, + %slot, + validator = %id, + "Sync signature included in block" ); } @@ -1748,20 +1908,44 @@ impl ValidatorMonitor { } /// Register an exit from the gossip network. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn register_gossip_voluntary_exit(&self, exit: &VoluntaryExit) { self.register_voluntary_exit("gossip", exit) } /// Register an exit from the HTTP API. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn register_api_voluntary_exit(&self, exit: &VoluntaryExit) { self.register_voluntary_exit("api", exit) } /// Register an exit included in a *valid* beacon block. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn register_block_voluntary_exit(&self, exit: &VoluntaryExit) { self.register_voluntary_exit("block", exit) } + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] fn register_voluntary_exit(&self, src: &str, exit: &VoluntaryExit) { if let Some(validator) = self.get_validator(exit.validator_index) { let id = &validator.id; @@ -1774,11 +1958,10 @@ impl ValidatorMonitor { // Not gated behind `self.individual_tracking()` since it's an // infrequent and interesting message. info!( - self.log, - "Voluntary exit"; - "epoch" => %epoch, - "validator" => %id, - "src" => src, + %epoch, + validator = %id, + src, + "Voluntary exit" ); validator.with_epoch_summary(epoch, |summary| summary.register_exit()); @@ -1786,20 +1969,44 @@ impl ValidatorMonitor { } /// Register a proposer slashing from the gossip network. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn register_gossip_proposer_slashing(&self, slashing: &ProposerSlashing) { self.register_proposer_slashing("gossip", slashing) } /// Register a proposer slashing from the HTTP API. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn register_api_proposer_slashing(&self, slashing: &ProposerSlashing) { self.register_proposer_slashing("api", slashing) } /// Register a proposer slashing included in a *valid* `BeaconBlock`. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn register_block_proposer_slashing(&self, slashing: &ProposerSlashing) { self.register_proposer_slashing("block", slashing) } + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] fn register_proposer_slashing(&self, src: &str, slashing: &ProposerSlashing) { let proposer = slashing.signed_header_1.message.proposer_index; let slot = slashing.signed_header_1.message.slot; @@ -1820,13 +2027,12 @@ impl ValidatorMonitor { // Not gated behind `self.individual_tracking()` since it's an // infrequent and interesting message. crit!( - self.log, - "Proposer slashing"; - "root_2" => %root_2, - "root_1" => %root_1, - "slot" => %slot, - "validator" => %id, - "src" => src, + %root_2, + %root_1, + %slot, + validator = %id, + src, + "Proposer slashing" ); validator.with_epoch_summary(epoch, |summary| summary.register_proposer_slashing()); @@ -1834,20 +2040,44 @@ impl ValidatorMonitor { } /// Register an attester slashing from the gossip network. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn register_gossip_attester_slashing(&self, slashing: AttesterSlashingRef<'_, E>) { self.register_attester_slashing("gossip", slashing) } /// Register an attester slashing from the HTTP API. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn register_api_attester_slashing(&self, slashing: AttesterSlashingRef<'_, E>) { self.register_attester_slashing("api", slashing) } /// Register an attester slashing included in a *valid* `BeaconBlock`. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn register_block_attester_slashing(&self, slashing: AttesterSlashingRef<'_, E>) { self.register_attester_slashing("block", slashing) } + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] fn register_attester_slashing(&self, src: &str, slashing: AttesterSlashingRef<'_, E>) { let data = slashing.attestation_1().data(); let attestation_1_indices: HashSet = slashing @@ -1875,12 +2105,11 @@ impl ValidatorMonitor { // Not gated behind `self.individual_tracking()` since it's an // infrequent and interesting message. crit!( - self.log, - "Attester slashing"; - "epoch" => %epoch, - "slot" => %data.slot, - "validator" => %id, - "src" => src, + %epoch, + slot = %data.slot, + validator = %id, + src, + "Attester slashing" ); validator.with_epoch_summary(epoch, |summary| summary.register_attester_slashing()); @@ -1890,6 +2119,12 @@ impl ValidatorMonitor { /// Scrape `self` for metrics. /// /// Should be called whenever Prometheus is scraping Lighthouse. + #[instrument(parent = None, + level = "info", + fields(service = "validator_monitor"), + name = "validator_monitor", + skip_all + )] pub fn scrape_metrics(&self, slot_clock: &S, spec: &ChainSpec) { metrics::set_gauge( &metrics::VALIDATOR_MONITOR_VALIDATORS_TOTAL, @@ -2074,7 +2309,6 @@ fn register_simulated_attestation( head_hit: bool, target_hit: bool, source_hit: bool, - log: &Logger, ) { if head_hit { metrics::inc_counter(&metrics::VALIDATOR_MONITOR_ATTESTATION_SIMULATOR_HEAD_ATTESTER_HIT); @@ -2097,15 +2331,14 @@ fn register_simulated_attestation( } debug!( - log, - "Simulated attestation evaluated"; - "attestation_source" => ?data.source.root, - "attestation_target" => ?data.target.root, - "attestation_head" => ?data.beacon_block_root, - "attestation_slot" => ?data.slot, - "source_hit" => source_hit, - "target_hit" => target_hit, - "head_hit" => head_hit, + attestation_source = ?data.source.root, + attestation_target = ?data.target.root, + attestation_head = ?data.beacon_block_root, + attestation_slot = ?data.slot, + source_hit, + target_hit, + head_hit, + "Simulated attestation evaluated" ); } diff --git a/beacon_node/beacon_chain/src/validator_pubkey_cache.rs b/beacon_node/beacon_chain/src/validator_pubkey_cache.rs index 877c297a3b7..39d2c2c2d76 100644 --- a/beacon_node/beacon_chain/src/validator_pubkey_cache.rs +++ b/beacon_node/beacon_chain/src/validator_pubkey_cache.rs @@ -210,7 +210,7 @@ impl DatabasePubkey { mod test { use super::*; use crate::test_utils::{BeaconChainHarness, EphemeralHarnessType}; - use logging::test_logger; + use logging::create_test_tracing_subscriber; use std::sync::Arc; use store::HotColdDB; use types::{EthSpec, Keypair, MainnetEthSpec}; @@ -231,10 +231,8 @@ mod test { } fn get_store() -> BeaconStore { - Arc::new( - HotColdDB::open_ephemeral(<_>::default(), Arc::new(E::default_spec()), test_logger()) - .unwrap(), - ) + create_test_tracing_subscriber(); + Arc::new(HotColdDB::open_ephemeral(<_>::default(), Arc::new(E::default_spec())).unwrap()) } #[allow(clippy::needless_range_loop)] diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index 621475a3ece..d89a8530e1b 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -242,7 +242,7 @@ async fn produces_attestations() { .early_attester_cache .add_head_block( block_root, - available_block, + &available_block, proto_block, &state, &chain.spec, @@ -310,7 +310,7 @@ async fn early_attester_cache_old_request() { .early_attester_cache .add_head_block( head.beacon_block_root, - available_block, + &available_block, head_proto_block, &head.beacon_state, &harness.chain.spec, diff --git a/beacon_node/beacon_chain/tests/bellatrix.rs b/beacon_node/beacon_chain/tests/bellatrix.rs index 5080b0890bd..3a424e73bab 100644 --- a/beacon_node/beacon_chain/tests/bellatrix.rs +++ b/beacon_node/beacon_chain/tests/bellatrix.rs @@ -50,7 +50,6 @@ async fn merge_with_terminal_block_hash_override() { let harness = BeaconChainHarness::builder(E::default()) .spec(spec.into()) - .logger(logging::test_logger()) .deterministic_keypairs(VALIDATOR_COUNT) .fresh_ephemeral_store() .mock_execution_layer() @@ -107,7 +106,6 @@ async fn base_altair_bellatrix_with_terminal_block_after_fork() { let harness = BeaconChainHarness::builder(E::default()) .spec(spec.into()) - .logger(logging::test_logger()) .deterministic_keypairs(VALIDATOR_COUNT) .fresh_ephemeral_store() .mock_execution_layer() diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 2a881b5b0f9..9225ffd9f41 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -12,7 +12,7 @@ use beacon_chain::{ BeaconSnapshot, BlockError, ChainConfig, ChainSegmentResult, IntoExecutionPendingBlock, InvalidSignature, NotifyExecutionLayer, }; -use logging::test_logger; +use logging::create_test_tracing_subscriber; use slasher::{Config as SlasherConfig, Slasher}; use state_processing::{ common::{attesting_indices_base, attesting_indices_electra}, @@ -30,6 +30,8 @@ type E = MainnetEthSpec; const VALIDATOR_COUNT: usize = 24; const CHAIN_SEGMENT_LENGTH: usize = 64 * 5; const BLOCK_INDICES: &[usize] = &[0, 1, 32, 64, 68 + 1, 129, CHAIN_SEGMENT_LENGTH - 1]; +// Default custody group count for tests +const CGC: usize = 8; /// A cached set of keys. static KEYPAIRS: LazyLock> = @@ -142,9 +144,10 @@ fn build_rpc_block( RpcBlock::new(None, block, Some(blobs.clone())).unwrap() } Some(DataSidecars::DataColumns(columns)) => { - RpcBlock::new_with_custody_columns(None, block, columns.clone(), spec).unwrap() + 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), } } @@ -367,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!( @@ -404,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!( @@ -431,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!( @@ -572,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(()), @@ -991,6 +1002,7 @@ async fn block_gossip_verification() { let (chain_segment, chain_segment_blobs) = get_chain_segment().await; let block_index = CHAIN_SEGMENT_LENGTH - 2; + let cgc = harness.chain.spec.custody_requirement as usize; harness .chain @@ -1004,7 +1016,7 @@ async fn block_gossip_verification() { { let gossip_verified = harness .chain - .verify_block_for_gossip(snapshot.beacon_block.clone()) + .verify_block_for_gossip(snapshot.beacon_block.clone(), get_cgc(&blobs_opt)) .await .expect("should obtain gossip verified block"); @@ -1046,7 +1058,7 @@ async fn block_gossip_verification() { *block.slot_mut() = expected_block_slot; assert!( matches!( - unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature))).await), + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature)), cgc).await), BlockError::FutureSlot { present_slot, block_slot, @@ -1080,7 +1092,7 @@ async fn block_gossip_verification() { *block.slot_mut() = expected_finalized_slot; assert!( matches!( - unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature))).await), + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature)), cgc).await), BlockError::WouldRevertFinalizedSlot { block_slot, finalized_slot, @@ -1110,10 +1122,10 @@ async fn block_gossip_verification() { unwrap_err( harness .chain - .verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block( - block, - junk_signature() - ))) + .verify_block_for_gossip( + Arc::new(SignedBeaconBlock::from_block(block, junk_signature())), + cgc + ) .await ), BlockError::InvalidSignature(InvalidSignature::ProposerSignature) @@ -1138,7 +1150,7 @@ async fn block_gossip_verification() { *block.parent_root_mut() = parent_root; assert!( matches!( - unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature))).await), + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature)), cgc).await), BlockError::ParentUnknown {parent_root: p} if p == parent_root ), @@ -1164,7 +1176,7 @@ async fn block_gossip_verification() { *block.parent_root_mut() = parent_root; assert!( matches!( - unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature))).await), + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature)), cgc).await), BlockError::NotFinalizedDescendant { block_parent_root } if block_parent_root == parent_root ), @@ -1201,7 +1213,7 @@ async fn block_gossip_verification() { ); assert!( matches!( - unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(block.clone())).await), + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(block.clone()), cgc).await), BlockError::IncorrectBlockProposer { block, local_shuffling, @@ -1213,7 +1225,7 @@ async fn block_gossip_verification() { // Check to ensure that we registered this is a valid block from this proposer. assert!( matches!( - unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(block.clone())).await), + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(block.clone()), cgc).await), BlockError::DuplicateImportStatusUnknown(_), ), "should register any valid signature against the proposer, even if the block failed later verification" @@ -1221,7 +1233,11 @@ async fn block_gossip_verification() { let block = chain_segment[block_index].beacon_block.clone(); assert!( - harness.chain.verify_block_for_gossip(block).await.is_ok(), + harness + .chain + .verify_block_for_gossip(block, cgc) + .await + .is_ok(), "the valid block should be processed" ); @@ -1239,7 +1255,7 @@ async fn block_gossip_verification() { matches!( harness .chain - .verify_block_for_gossip(block.clone()) + .verify_block_for_gossip(block.clone(), cgc) .await .expect_err("should error when processing known block"), BlockError::DuplicateImportStatusUnknown(_) @@ -1295,15 +1311,11 @@ async fn verify_and_process_gossip_data_sidecars( #[tokio::test] async fn verify_block_for_gossip_slashing_detection() { + create_test_tracing_subscriber(); let slasher_dir = tempdir().unwrap(); let spec = Arc::new(test_spec::()); let slasher = Arc::new( - Slasher::open( - SlasherConfig::new(slasher_dir.path().into()), - spec.clone(), - test_logger(), - ) - .unwrap(), + Slasher::open(SlasherConfig::new(slasher_dir.path().into()), spec.clone()).unwrap(), ); let inner_slasher = slasher.clone(); @@ -1319,8 +1331,17 @@ async fn verify_block_for_gossip_slashing_detection() { let state = harness.get_current_state(); let ((block1, blobs1), _) = harness.make_block(state.clone(), Slot::new(1)).await; let ((block2, _blobs2), _) = harness.make_block(state, Slot::new(1)).await; + let cgc = if block1.fork_name_unchecked().fulu_enabled() { + harness.get_sampling_column_count() + } else { + 0 + }; - let verified_block = harness.chain.verify_block_for_gossip(block1).await.unwrap(); + let verified_block = harness + .chain + .verify_block_for_gossip(block1, cgc) + .await + .unwrap(); if let Some((kzg_proofs, blobs)) = blobs1 { harness @@ -1343,7 +1364,7 @@ async fn verify_block_for_gossip_slashing_detection() { ) .await .unwrap(); - unwrap_err(harness.chain.verify_block_for_gossip(block2).await); + unwrap_err(harness.chain.verify_block_for_gossip(block2, CGC).await); // Slasher should have been handed the two conflicting blocks and crafted a slashing. slasher.process_queued(Epoch::new(0)).unwrap(); @@ -1367,7 +1388,11 @@ async fn verify_block_for_gossip_doppelganger_detection() { .attestations() .map(|att| att.clone_as_attestation()) .collect::>(); - let verified_block = harness.chain.verify_block_for_gossip(block).await.unwrap(); + let verified_block = harness + .chain + .verify_block_for_gossip(block, CGC) + .await + .unwrap(); harness .chain .process_block( @@ -1514,7 +1539,7 @@ async fn add_base_block_to_altair_chain() { assert!(matches!( harness .chain - .verify_block_for_gossip(Arc::new(base_block.clone())) + .verify_block_for_gossip(Arc::new(base_block.clone()), CGC) .await .expect_err("should error when processing base block"), BlockError::InconsistentFork(InconsistentFork { @@ -1524,12 +1549,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(()), @@ -1547,7 +1573,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, @@ -1650,7 +1676,7 @@ async fn add_altair_block_to_base_chain() { assert!(matches!( harness .chain - .verify_block_for_gossip(Arc::new(altair_block.clone())) + .verify_block_for_gossip(Arc::new(altair_block.clone()), CGC) .await .expect_err("should error when processing altair block"), BlockError::InconsistentFork(InconsistentFork { @@ -1660,12 +1686,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(()), @@ -1683,7 +1710,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, @@ -1744,11 +1771,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(); @@ -1814,3 +1846,14 @@ async fn import_execution_pending_block( } } } + +fn get_cgc(blobs_opt: &Option>) -> usize { + if let Some(data_sidecars) = blobs_opt.as_ref() { + match data_sidecars { + DataSidecars::Blobs(_) => 0, + DataSidecars::DataColumns(d) => d.len(), + } + } else { + 0 + } +} diff --git a/beacon_node/beacon_chain/tests/capella.rs b/beacon_node/beacon_chain/tests/capella.rs index 3ce5702f2ea..2c2ba8e01a7 100644 --- a/beacon_node/beacon_chain/tests/capella.rs +++ b/beacon_node/beacon_chain/tests/capella.rs @@ -40,7 +40,6 @@ async fn base_altair_bellatrix_capella() { let harness = BeaconChainHarness::builder(E::default()) .spec(spec.into()) - .logger(logging::test_logger()) .deterministic_keypairs(VALIDATOR_COUNT) .fresh_ephemeral_store() .mock_execution_layer() diff --git a/beacon_node/beacon_chain/tests/op_verification.rs b/beacon_node/beacon_chain/tests/op_verification.rs index 44fb298d6c6..86ab0cce804 100644 --- a/beacon_node/beacon_chain/tests/op_verification.rs +++ b/beacon_node/beacon_chain/tests/op_verification.rs @@ -9,7 +9,6 @@ use beacon_chain::{ }, BeaconChainError, }; -use sloggers::{null::NullLoggerBuilder, Build}; use state_processing::per_block_processing::errors::{ AttesterSlashingInvalid, BlockOperationError, ExitInvalid, ProposerSlashingInvalid, }; @@ -35,7 +34,6 @@ fn get_store(db_path: &TempDir) -> Arc { let cold_path = db_path.path().join("cold_db"); let blobs_path = db_path.path().join("blobs_db"); let config = StoreConfig::default(); - let log = NullLoggerBuilder.build().expect("logger should build"); HotColdDB::open( &hot_path, &cold_path, @@ -43,7 +41,6 @@ fn get_store(db_path: &TempDir) -> Arc { |_, _, _| Ok(()), config, spec, - log, ) .expect("disk store should initialize") } diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index d41c33176ae..c6fc3416e05 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}, @@ -12,7 +13,6 @@ use execution_layer::{ ExecutionLayer, ForkchoiceState, PayloadAttributes, }; use fork_choice::{Error as ForkChoiceError, InvalidationOperation, PayloadVerificationStatus}; -use logging::test_logger; use proto_array::{Error as ProtoArrayError, ExecutionStatus}; use slot_clock::SlotClock; use std::collections::HashMap; @@ -22,6 +22,7 @@ use task_executor::ShutdownReason; use types::*; const VALIDATOR_COUNT: usize = 32; +const CGC: usize = 8; type E = MainnetEthSpec; @@ -56,7 +57,6 @@ impl InvalidPayloadRig { reconstruct_historic_states: true, ..ChainConfig::default() }) - .logger(test_logger()) .deterministic_keypairs(VALIDATOR_COUNT) .mock_execution_layer() .fresh_ephemeral_store() @@ -688,12 +688,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(()), @@ -789,12 +791,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(()), @@ -1052,14 +1056,16 @@ async fn invalid_parent() { // Ensure the block built atop an invalid payload is invalid for gossip. assert!(matches!( - rig.harness.chain.clone().verify_block_for_gossip(block.clone()).await, + rig.harness.chain.clone().verify_block_for_gossip(block.clone(), CGC).await, Err(BlockError::ParentExecutionPayloadInvalid { parent_root: invalid_root }) if invalid_root == parent_root )); // Ensure the block built atop an invalid payload is invalid for import. + let rpc_block = + RpcBlock::new_without_blobs(None, block.clone(), rig.harness.sampling_column_count); 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 }) @@ -1381,11 +1387,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(()), @@ -1420,8 +1428,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 8d34c65e471..fa2d028f224 100644 --- a/beacon_node/beacon_chain/tests/rewards.rs +++ b/beacon_node/beacon_chain/tests/rewards.rs @@ -9,9 +9,7 @@ use beacon_chain::{ types::{Epoch, EthSpec, Keypair, MinimalEthSpec}, BlockError, ChainConfig, StateSkipConfig, WhenSlotSkipped, }; -use eth2::lighthouse::attestation_rewards::TotalAttestationRewards; -use eth2::lighthouse::StandardAttestationRewards; -use eth2::types::ValidatorId; +use eth2::types::{StandardAttestationRewards, TotalAttestationRewards, ValidatorId}; use state_processing::{BlockReplayError, BlockReplayer}; use std::array::IntoIter; use std::collections::HashMap; @@ -365,8 +363,7 @@ async fn test_rewards_base_multi_inclusion() { .extend_slots(E::slots_per_epoch() as usize * 2 - 4) .await; - // pin to reduce stack size for clippy - Box::pin(check_all_base_rewards(&harness, initial_balances)).await; + check_all_base_rewards(&harness, initial_balances).await; } #[tokio::test] @@ -798,7 +795,8 @@ async fn check_all_base_rewards( harness: &BeaconChainHarness>, balances: Vec, ) { - check_all_base_rewards_for_subset(harness, balances, vec![]).await; + // The box reduces the size on the stack for a clippy lint. + Box::pin(check_all_base_rewards_for_subset(harness, balances, vec![])).await; } async fn check_all_base_rewards_for_subset( diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index d1950ab7ce2..3343dc101b5 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; @@ -14,8 +15,9 @@ use beacon_chain::{ migrate::MigratorConfig, BeaconChain, BeaconChainError, BeaconChainTypes, BeaconSnapshot, BlockError, ChainConfig, NotifyExecutionLayer, ServerSentEventHandler, WhenSlotSkipped, }; -use logging::test_logger; +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::*; @@ -62,10 +63,10 @@ fn get_store_generic( config: StoreConfig, spec: ChainSpec, ) -> Arc, BeaconNodeBackend>> { + create_test_tracing_subscriber(); let hot_path = db_path.path().join("chain_db"); let cold_path = db_path.path().join("freezer_db"); let blobs_path = db_path.path().join("blobs_db"); - let log = test_logger(); HotColdDB::open( &hot_path, @@ -74,7 +75,6 @@ fn get_store_generic( |_, _, _| Ok(()), config, spec.into(), - log, ) .expect("disk store should initialize") } @@ -112,7 +112,6 @@ fn get_harness_generic( let harness = TestHarness::builder(MinimalEthSpec) .spec(store.get_chain_spec().clone()) .keypairs(KEYPAIRS[0..validator_count].to_vec()) - .logger(store.logger().clone()) .fresh_disk_store(store) .mock_execution_layer() .chain_config(chain_config) @@ -122,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::(); @@ -1227,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) @@ -1275,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] @@ -1401,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)); } @@ -1494,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] @@ -1578,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)) @@ -1633,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. @@ -2152,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(); @@ -2224,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); @@ -2242,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. @@ -2261,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] @@ -2375,7 +2334,7 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { .await; let (shutdown_tx, _shutdown_rx) = futures::channel::mpsc::channel(1); - let log = harness.chain.logger().clone(); + let temp2 = tempdir().unwrap(); let store = get_store(&temp2); let spec = test_spec::(); @@ -2401,7 +2360,6 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { .store(store.clone()) .custom_spec(test_spec::().into()) .task_executor(harness.chain.task_executor.clone()) - .logger(log.clone()) .weak_subjectivity_state( wss_state, wss_block.clone(), @@ -2415,11 +2373,9 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { .slot_clock(slot_clock) .shutdown_sender(shutdown_tx) .chain_config(ChainConfig::default()) - .event_handler(Some(ServerSentEventHandler::new_with_capacity( - log.clone(), - 1, - ))) + .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"); @@ -2533,18 +2489,13 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { // Corrupt the signature on the 1st block to ensure that the backfill processor is checking // signatures correctly. Regression test for https://github.com/sigp/lighthouse/pull/5120. - let mut batch_with_invalid_first_block = available_blocks.clone(); + let mut batch_with_invalid_first_block = + available_blocks.iter().map(clone_block).collect::>(); batch_with_invalid_first_block[0] = { - let (block_root, block, blobs, data_columns) = available_blocks[0].clone().deconstruct(); + let (block_root, block, data) = clone_block(&available_blocks[0]).deconstruct(); let mut corrupt_block = (*block).clone(); *corrupt_block.signature_mut() = Signature::empty(); - AvailableBlock::__new_for_testing( - block_root, - Arc::new(corrupt_block), - blobs, - data_columns, - Arc::new(spec), - ) + AvailableBlock::__new_for_testing(block_root, Arc::new(corrupt_block), data, Arc::new(spec)) }; // Importing the invalid batch should error. @@ -2556,8 +2507,9 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { )); // Importing the batch with valid signatures should succeed. + let available_blocks_dup = available_blocks.iter().map(clone_block).collect::>(); beacon_chain - .import_historical_block_batch(available_blocks.clone()) + .import_historical_block_batch(available_blocks_dup) .unwrap(); assert_eq!(beacon_chain.store.get_oldest_block_slot(), 0); @@ -2692,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(()), @@ -2707,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(()), @@ -2795,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() @@ -3009,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(); @@ -3077,7 +3041,6 @@ async fn schema_downgrade_to_min_version() { genesis_state_root, CURRENT_SCHEMA_VERSION, min_version, - store.logger().clone(), ) .expect("schema downgrade to minimum version should work"); @@ -3087,7 +3050,6 @@ async fn schema_downgrade_to_min_version() { genesis_state_root, min_version, CURRENT_SCHEMA_VERSION, - store.logger().clone(), ) .expect("schema upgrade from minimum version should work"); @@ -3095,7 +3057,6 @@ async fn schema_downgrade_to_min_version() { let harness = BeaconChainHarness::builder(MinimalEthSpec) .default_spec() .keypairs(KEYPAIRS[0..LOW_VALIDATOR_COUNT].to_vec()) - .logger(store.logger().clone()) .testing_slot_clock(slot_clock) .resumed_disk_store(store.clone()) .mock_execution_layer() @@ -3113,7 +3074,6 @@ async fn schema_downgrade_to_min_version() { genesis_state_root, CURRENT_SCHEMA_VERSION, min_version_sub_1, - harness.logger().clone(), ) .expect_err("should not downgrade below minimum version"); } @@ -3716,3 +3676,7 @@ fn get_blocks( .map(|checkpoint| checkpoint.beacon_block_root.into()) .collect() } + +fn clone_block(block: &AvailableBlock) -> AvailableBlock { + block.__clone_without_recv().unwrap() +} diff --git a/beacon_node/beacon_chain/tests/validator_monitor.rs b/beacon_node/beacon_chain/tests/validator_monitor.rs index 180db6d76dd..bca37b4e6d9 100644 --- a/beacon_node/beacon_chain/tests/validator_monitor.rs +++ b/beacon_node/beacon_chain/tests/validator_monitor.rs @@ -2,7 +2,6 @@ use beacon_chain::test_utils::{ AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, }; use beacon_chain::validator_monitor::{ValidatorMonitorConfig, MISSED_BLOCK_LAG_SLOTS}; -use logging::test_logger; use std::sync::LazyLock; use types::{Epoch, EthSpec, Keypair, MainnetEthSpec, PublicKeyBytes, Slot}; @@ -22,7 +21,6 @@ fn get_harness( let harness = BeaconChainHarness::builder(MainnetEthSpec) .default_spec() .keypairs(KEYPAIRS[0..validator_count].to_vec()) - .logger(test_logger()) .fresh_ephemeral_store() .mock_execution_layer() .validator_monitor_config(ValidatorMonitorConfig { diff --git a/beacon_node/beacon_processor/Cargo.toml b/beacon_node/beacon_processor/Cargo.toml index c96e0868d73..afd4660c9a3 100644 --- a/beacon_node/beacon_processor/Cargo.toml +++ b/beacon_node/beacon_processor/Cargo.toml @@ -13,12 +13,12 @@ metrics = { workspace = true } num_cpus = { workspace = true } parking_lot = { workspace = true } serde = { workspace = true } -slog = { workspace = true } slot_clock = { workspace = true } strum = { workspace = true } task_executor = { workspace = true } tokio = { workspace = true } tokio-util = { workspace = true } +tracing = { workspace = true } types = { workspace = true } [dev-dependencies] diff --git a/beacon_node/beacon_processor/src/lib.rs b/beacon_node/beacon_processor/src/lib.rs index a8960d47c9e..e864cb1fd91 100644 --- a/beacon_node/beacon_processor/src/lib.rs +++ b/beacon_node/beacon_processor/src/lib.rs @@ -44,10 +44,10 @@ use crate::work_reprocessing_queue::{ use futures::stream::{Stream, StreamExt}; use futures::task::Poll; use lighthouse_network::{MessageId, NetworkGlobals, PeerId}; +use logging::crit; use logging::TimeLatch; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; -use slog::{crit, debug, error, trace, warn, Logger}; use slot_clock::SlotClock; use std::cmp; use std::collections::{HashSet, VecDeque}; @@ -61,6 +61,7 @@ use strum::IntoStaticStr; use task_executor::TaskExecutor; use tokio::sync::mpsc; use tokio::sync::mpsc::error::TrySendError; +use tracing::{debug, error, trace, warn}; use types::{ Attestation, BeaconState, ChainSpec, EthSpec, Hash256, RelativeEpoch, SignedAggregateAndProof, SingleAttestation, Slot, SubnetId, @@ -305,14 +306,13 @@ impl FifoQueue { /// Add a new item to the queue. /// /// Drops `item` if the queue is full. - pub fn push(&mut self, item: T, item_desc: &str, log: &Logger) { + pub fn push(&mut self, item: T, item_desc: &str) { if self.queue.len() == self.max_length { error!( - log, - "Work queue is full"; - "msg" => "the system has insufficient resources for load", - "queue_len" => self.max_length, - "queue" => item_desc, + msg = "the system has insufficient resources for load", + queue_len = self.max_length, + queue = item_desc, + "Work queue is full" ) } else { self.queue.push_back(item); @@ -827,7 +827,6 @@ pub struct BeaconProcessor { pub executor: TaskExecutor, pub current_workers: usize, pub config: BeaconProcessorConfig, - pub log: Logger, } impl BeaconProcessor { @@ -938,7 +937,6 @@ impl BeaconProcessor { work_reprocessing_rx, &self.executor, Arc::new(slot_clock), - self.log.clone(), maximum_gossip_clock_disparity, )?; @@ -969,9 +967,8 @@ impl BeaconProcessor { { Err(e) => { warn!( - self.log, - "Unable to queue backfill work event. Will try to process now."; - "error" => %e + error = %e, + "Unable to queue backfill work event. Will try to process now." ); match e { TrySendError::Full(reprocess_queue_message) @@ -982,9 +979,8 @@ impl BeaconProcessor { ) => Some(backfill_batch.into()), other => { crit!( - self.log, - "Unexpected queue message type"; - "message_type" => other.as_ref() + message_type = other.as_ref(), + "Unexpected queue message type" ); // This is an unhandled exception, drop the message. continue; @@ -1005,11 +1001,7 @@ impl BeaconProcessor { Some(InboundEvent::WorkEvent(event)) | Some(InboundEvent::ReprocessingWork(event)) => Some(event), None => { - debug!( - self.log, - "Gossip processor stopped"; - "msg" => "stream ended" - ); + debug!(msg = "stream ended", "Gossip processor stopped"); break; } }; @@ -1050,238 +1042,234 @@ impl BeaconProcessor { None if can_spawn => { // Check for chain segments first, they're the most efficient way to get // blocks into the system. - let work_event: Option> = if let Some(item) = - chain_segment_queue.pop() - { - Some(item) - // Check sync blocks before gossip blocks, since we've already explicitly - // requested these blocks. - } else if let Some(item) = rpc_block_queue.pop() { - Some(item) - } else if let Some(item) = rpc_blob_queue.pop() { - Some(item) - } else if let Some(item) = rpc_custody_column_queue.pop() { - Some(item) - // TODO(das): decide proper prioritization for sampling columns - } else if let Some(item) = rpc_custody_column_queue.pop() { - Some(item) - } else if let Some(item) = rpc_verify_data_column_queue.pop() { - Some(item) - } else if let Some(item) = sampling_result_queue.pop() { - Some(item) - // Check delayed blocks before gossip blocks, the gossip blocks might rely - // on the delayed ones. - } else if let Some(item) = delayed_block_queue.pop() { - Some(item) - // Check gossip blocks before gossip attestations, since a block might be - // required to verify some attestations. - } else if let Some(item) = gossip_block_queue.pop() { - Some(item) - } else if let Some(item) = gossip_blob_queue.pop() { - Some(item) - } else if let Some(item) = gossip_data_column_queue.pop() { - Some(item) - // Check the priority 0 API requests after blocks and blobs, but before attestations. - } else if let Some(item) = api_request_p0_queue.pop() { - Some(item) - // Check the aggregates, *then* the unaggregates since we assume that - // aggregates are more valuable to local validators and effectively give us - // more information with less signature verification time. - } else if aggregate_queue.len() > 0 { - let batch_size = cmp::min( - aggregate_queue.len(), - self.config.max_gossip_aggregate_batch_size, - ); - - if batch_size < 2 { - // One single aggregate is in the queue, process it individually. - aggregate_queue.pop() - } else { - // Collect two or more aggregates into a batch, so they can take - // advantage of batch signature verification. - // - // Note: this will convert the `Work::GossipAggregate` item into a - // `Work::GossipAggregateBatch` item. - let mut aggregates = Vec::with_capacity(batch_size); - let mut process_batch_opt = None; - for _ in 0..batch_size { - if let Some(item) = aggregate_queue.pop() { - match item { - Work::GossipAggregate { - aggregate, - process_individual: _, - process_batch, - } => { - aggregates.push(*aggregate); - if process_batch_opt.is_none() { - process_batch_opt = Some(process_batch); + let work_event: Option> = + if let Some(item) = chain_segment_queue.pop() { + Some(item) + // Check sync blocks before gossip blocks, since we've already explicitly + // requested these blocks. + } else if let Some(item) = rpc_block_queue.pop() { + Some(item) + } else if let Some(item) = rpc_blob_queue.pop() { + Some(item) + } else if let Some(item) = rpc_custody_column_queue.pop() { + Some(item) + // TODO(das): decide proper prioritization for sampling columns + } else if let Some(item) = rpc_custody_column_queue.pop() { + Some(item) + } else if let Some(item) = rpc_verify_data_column_queue.pop() { + Some(item) + } else if let Some(item) = sampling_result_queue.pop() { + Some(item) + // Check delayed blocks before gossip blocks, the gossip blocks might rely + // on the delayed ones. + } else if let Some(item) = delayed_block_queue.pop() { + Some(item) + // Check gossip blocks before gossip attestations, since a block might be + // required to verify some attestations. + } else if let Some(item) = gossip_block_queue.pop() { + Some(item) + } else if let Some(item) = gossip_blob_queue.pop() { + Some(item) + } else if let Some(item) = gossip_data_column_queue.pop() { + Some(item) + // Check the priority 0 API requests after blocks and blobs, but before attestations. + } else if let Some(item) = api_request_p0_queue.pop() { + Some(item) + // Check the aggregates, *then* the unaggregates since we assume that + // aggregates are more valuable to local validators and effectively give us + // more information with less signature verification time. + } else if aggregate_queue.len() > 0 { + let batch_size = cmp::min( + aggregate_queue.len(), + self.config.max_gossip_aggregate_batch_size, + ); + + if batch_size < 2 { + // One single aggregate is in the queue, process it individually. + aggregate_queue.pop() + } else { + // Collect two or more aggregates into a batch, so they can take + // advantage of batch signature verification. + // + // Note: this will convert the `Work::GossipAggregate` item into a + // `Work::GossipAggregateBatch` item. + let mut aggregates = Vec::with_capacity(batch_size); + let mut process_batch_opt = None; + for _ in 0..batch_size { + if let Some(item) = aggregate_queue.pop() { + match item { + Work::GossipAggregate { + aggregate, + process_individual: _, + process_batch, + } => { + aggregates.push(*aggregate); + if process_batch_opt.is_none() { + process_batch_opt = Some(process_batch); + } + } + _ => { + error!("Invalid item in aggregate queue"); } - } - _ => { - error!(self.log, "Invalid item in aggregate queue"); } } } - } - if let Some(process_batch) = process_batch_opt { - // Process all aggregates with a single worker. - Some(Work::GossipAggregateBatch { - aggregates, - process_batch, - }) - } else { - // There is no good reason for this to - // happen, it is a serious logic error. - // Since we only form batches when multiple - // work items exist, we should always have a - // work closure at this point. - crit!(self.log, "Missing aggregate work"); - None + if let Some(process_batch) = process_batch_opt { + // Process all aggregates with a single worker. + Some(Work::GossipAggregateBatch { + aggregates, + process_batch, + }) + } else { + // There is no good reason for this to + // happen, it is a serious logic error. + // Since we only form batches when multiple + // work items exist, we should always have a + // work closure at this point. + crit!("Missing aggregate work"); + None + } } - } - // Check the unaggregated attestation queue. - // - // Potentially use batching. - } else if attestation_queue.len() > 0 { - let batch_size = cmp::min( - attestation_queue.len(), - self.config.max_gossip_attestation_batch_size, - ); - - if batch_size < 2 { - // One single attestation is in the queue, process it individually. - attestation_queue.pop() - } else { - // Collect two or more attestations into a batch, so they can take - // advantage of batch signature verification. - // - // Note: this will convert the `Work::GossipAttestation` item into a - // `Work::GossipAttestationBatch` item. - let mut attestations = Vec::with_capacity(batch_size); - let mut process_batch_opt = None; - for _ in 0..batch_size { - if let Some(item) = attestation_queue.pop() { - match item { - Work::GossipAttestation { - attestation, - process_individual: _, - process_batch, - } => { - attestations.push(*attestation); - if process_batch_opt.is_none() { - process_batch_opt = Some(process_batch); + // Check the unaggregated attestation queue. + // + // Potentially use batching. + } else if attestation_queue.len() > 0 { + let batch_size = cmp::min( + attestation_queue.len(), + self.config.max_gossip_attestation_batch_size, + ); + + if batch_size < 2 { + // One single attestation is in the queue, process it individually. + attestation_queue.pop() + } else { + // Collect two or more attestations into a batch, so they can take + // advantage of batch signature verification. + // + // Note: this will convert the `Work::GossipAttestation` item into a + // `Work::GossipAttestationBatch` item. + let mut attestations = Vec::with_capacity(batch_size); + let mut process_batch_opt = None; + for _ in 0..batch_size { + if let Some(item) = attestation_queue.pop() { + match item { + Work::GossipAttestation { + attestation, + process_individual: _, + process_batch, + } => { + attestations.push(*attestation); + if process_batch_opt.is_none() { + process_batch_opt = Some(process_batch); + } } + _ => error!("Invalid item in attestation queue"), } - _ => error!( - self.log, - "Invalid item in attestation queue" - ), } } - } - if let Some(process_batch) = process_batch_opt { - // Process all attestations with a single worker. - Some(Work::GossipAttestationBatch { - attestations, - process_batch, - }) - } else { - // There is no good reason for this to - // happen, it is a serious logic error. - // Since we only form batches when multiple - // work items exist, we should always have a - // work closure at this point. - crit!(self.log, "Missing attestations work"); - None + if let Some(process_batch) = process_batch_opt { + // Process all attestations with a single worker. + Some(Work::GossipAttestationBatch { + attestations, + process_batch, + }) + } else { + // There is no good reason for this to + // happen, it is a serious logic error. + // Since we only form batches when multiple + // work items exist, we should always have a + // work closure at this point. + crit!("Missing attestations work"); + None + } } - } - // Convert any gossip attestations that need to be converted. - } else if let Some(item) = attestation_to_convert_queue.pop() { - Some(item) - // Check sync committee messages after attestations as their rewards are lesser - // and they don't influence fork choice. - } else if let Some(item) = sync_contribution_queue.pop() { - Some(item) - } else if let Some(item) = sync_message_queue.pop() { - Some(item) - // Aggregates and unaggregates queued for re-processing are older and we - // care about fresher ones, so check those first. - } else if let Some(item) = unknown_block_aggregate_queue.pop() { - Some(item) - } else if let Some(item) = unknown_block_attestation_queue.pop() { - Some(item) - // Check RPC methods next. Status messages are needed for sync so - // prioritize them over syncing requests from other peers (BlocksByRange - // and BlocksByRoot) - } else if let Some(item) = status_queue.pop() { - Some(item) - } else if let Some(item) = bbrange_queue.pop() { - Some(item) - } else if let Some(item) = bbroots_queue.pop() { - Some(item) - } else if let Some(item) = blbrange_queue.pop() { - Some(item) - } else if let Some(item) = blbroots_queue.pop() { - Some(item) - } else if let Some(item) = dcbroots_queue.pop() { - Some(item) - } else if let Some(item) = dcbrange_queue.pop() { - Some(item) - // Prioritize sampling requests after block syncing requests - } else if let Some(item) = unknown_block_sampling_request_queue.pop() { - Some(item) - // Check slashings after all other consensus messages so we prioritize - // following head. - // - // Check attester slashings before proposer slashings since they have the - // potential to slash multiple validators at once. - } else if let Some(item) = gossip_attester_slashing_queue.pop() { - Some(item) - } else if let Some(item) = gossip_proposer_slashing_queue.pop() { - Some(item) - // Check exits and address changes late since our validators don't get - // rewards from them. - } else if let Some(item) = gossip_voluntary_exit_queue.pop() { - Some(item) - } else if let Some(item) = gossip_bls_to_execution_change_queue.pop() { - Some(item) - // Check the priority 1 API requests after we've - // processed all the interesting things from the network - // and things required for us to stay in good repute - // with our P2P peers. - } else if let Some(item) = api_request_p1_queue.pop() { - Some(item) - // Handle backfill sync chain segments. - } else if let Some(item) = backfill_chain_segment.pop() { - Some(item) - // Handle light client requests. - } else if let Some(item) = lc_gossip_finality_update_queue.pop() { - Some(item) - } else if let Some(item) = lc_gossip_optimistic_update_queue.pop() { - Some(item) - } else if let Some(item) = unknown_light_client_update_queue.pop() { - Some(item) - } else if let Some(item) = lc_bootstrap_queue.pop() { - Some(item) - } else if let Some(item) = lc_rpc_optimistic_update_queue.pop() { - Some(item) - } else if let Some(item) = lc_rpc_finality_update_queue.pop() { - Some(item) - } else if let Some(item) = lc_update_range_queue.pop() { - Some(item) - // This statement should always be the final else statement. - } else { - // Let the journal know that a worker is freed and there's nothing else - // for it to do. - if let Some(work_journal_tx) = &work_journal_tx { - // We don't care if this message was successfully sent, we only use the journal - // during testing. - let _ = work_journal_tx.try_send(NOTHING_TO_DO); - } - None - }; + // Convert any gossip attestations that need to be converted. + } else if let Some(item) = attestation_to_convert_queue.pop() { + Some(item) + // Check sync committee messages after attestations as their rewards are lesser + // and they don't influence fork choice. + } else if let Some(item) = sync_contribution_queue.pop() { + Some(item) + } else if let Some(item) = sync_message_queue.pop() { + Some(item) + // Aggregates and unaggregates queued for re-processing are older and we + // care about fresher ones, so check those first. + } else if let Some(item) = unknown_block_aggregate_queue.pop() { + Some(item) + } else if let Some(item) = unknown_block_attestation_queue.pop() { + Some(item) + // Check RPC methods next. Status messages are needed for sync so + // prioritize them over syncing requests from other peers (BlocksByRange + // and BlocksByRoot) + } else if let Some(item) = status_queue.pop() { + Some(item) + } else if let Some(item) = bbrange_queue.pop() { + Some(item) + } else if let Some(item) = bbroots_queue.pop() { + Some(item) + } else if let Some(item) = blbrange_queue.pop() { + Some(item) + } else if let Some(item) = blbroots_queue.pop() { + Some(item) + } else if let Some(item) = dcbroots_queue.pop() { + Some(item) + } else if let Some(item) = dcbrange_queue.pop() { + Some(item) + // Prioritize sampling requests after block syncing requests + } else if let Some(item) = unknown_block_sampling_request_queue.pop() { + Some(item) + // Check slashings after all other consensus messages so we prioritize + // following head. + // + // Check attester slashings before proposer slashings since they have the + // potential to slash multiple validators at once. + } else if let Some(item) = gossip_attester_slashing_queue.pop() { + Some(item) + } else if let Some(item) = gossip_proposer_slashing_queue.pop() { + Some(item) + // Check exits and address changes late since our validators don't get + // rewards from them. + } else if let Some(item) = gossip_voluntary_exit_queue.pop() { + Some(item) + } else if let Some(item) = gossip_bls_to_execution_change_queue.pop() { + Some(item) + // Check the priority 1 API requests after we've + // processed all the interesting things from the network + // and things required for us to stay in good repute + // with our P2P peers. + } else if let Some(item) = api_request_p1_queue.pop() { + Some(item) + // Handle backfill sync chain segments. + } else if let Some(item) = backfill_chain_segment.pop() { + Some(item) + // Handle light client requests. + } else if let Some(item) = lc_gossip_finality_update_queue.pop() { + Some(item) + } else if let Some(item) = lc_gossip_optimistic_update_queue.pop() { + Some(item) + } else if let Some(item) = unknown_light_client_update_queue.pop() { + Some(item) + } else if let Some(item) = lc_bootstrap_queue.pop() { + Some(item) + } else if let Some(item) = lc_rpc_optimistic_update_queue.pop() { + Some(item) + } else if let Some(item) = lc_rpc_finality_update_queue.pop() { + Some(item) + } else if let Some(item) = lc_update_range_queue.pop() { + Some(item) + // This statement should always be the final else statement. + } else { + // Let the journal know that a worker is freed and there's nothing else + // for it to do. + if let Some(work_journal_tx) = &work_journal_tx { + // We don't care if this message was successfully sent, we only use the journal + // during testing. + let _ = work_journal_tx.try_send(NOTHING_TO_DO); + } + None + }; if let Some(work_event) = work_event { let work_type = work_event.to_type(); @@ -1296,9 +1284,8 @@ impl BeaconProcessor { // I cannot see any good reason why this would happen. None => { warn!( - self.log, - "Unexpected gossip processor condition"; - "msg" => "no new work and cannot spawn worker" + msg = "no new work and cannot spawn worker", + "Unexpected gossip processor condition" ); None } @@ -1313,10 +1300,9 @@ impl BeaconProcessor { &[work_id], ); trace!( - self.log, - "Gossip processor skipping work"; - "msg" => "chain is syncing", - "work_id" => work_id + msg = "chain is syncing", + work_id = work_id, + "Gossip processor skipping work" ); None } @@ -1335,89 +1321,75 @@ impl BeaconProcessor { // Attestation batches are formed internally within the // `BeaconProcessor`, they are not sent from external services. Work::GossipAttestationBatch { .. } => crit!( - self.log, - "Unsupported inbound event"; - "type" => "GossipAttestationBatch" + work_type = "GossipAttestationBatch", + "Unsupported inbound event" ), Work::GossipAggregate { .. } => aggregate_queue.push(work), // Aggregate batches are formed internally within the `BeaconProcessor`, // they are not sent from external services. - Work::GossipAggregateBatch { .. } => crit!( - self.log, - "Unsupported inbound event"; - "type" => "GossipAggregateBatch" - ), - Work::GossipBlock { .. } => { - gossip_block_queue.push(work, work_id, &self.log) - } - Work::GossipBlobSidecar { .. } => { - gossip_blob_queue.push(work, work_id, &self.log) + Work::GossipAggregateBatch { .. } => { + crit!( + work_type = "GossipAggregateBatch", + "Unsupported inbound event" + ) } + Work::GossipBlock { .. } => gossip_block_queue.push(work, work_id), + Work::GossipBlobSidecar { .. } => gossip_blob_queue.push(work, work_id), Work::GossipDataColumnSidecar { .. } => { - gossip_data_column_queue.push(work, work_id, &self.log) + gossip_data_column_queue.push(work, work_id) } Work::DelayedImportBlock { .. } => { - delayed_block_queue.push(work, work_id, &self.log) + delayed_block_queue.push(work, work_id) } Work::GossipVoluntaryExit { .. } => { - gossip_voluntary_exit_queue.push(work, work_id, &self.log) + gossip_voluntary_exit_queue.push(work, work_id) } Work::GossipProposerSlashing { .. } => { - gossip_proposer_slashing_queue.push(work, work_id, &self.log) + gossip_proposer_slashing_queue.push(work, work_id) } Work::GossipAttesterSlashing { .. } => { - gossip_attester_slashing_queue.push(work, work_id, &self.log) + gossip_attester_slashing_queue.push(work, work_id) } Work::GossipSyncSignature { .. } => sync_message_queue.push(work), Work::GossipSyncContribution { .. } => { sync_contribution_queue.push(work) } Work::GossipLightClientFinalityUpdate { .. } => { - lc_gossip_finality_update_queue.push(work, work_id, &self.log) + lc_gossip_finality_update_queue.push(work, work_id) } Work::GossipLightClientOptimisticUpdate { .. } => { - lc_gossip_optimistic_update_queue.push(work, work_id, &self.log) + lc_gossip_optimistic_update_queue.push(work, work_id) } Work::RpcBlock { .. } | Work::IgnoredRpcBlock { .. } => { - rpc_block_queue.push(work, work_id, &self.log) + rpc_block_queue.push(work, work_id) } - Work::RpcBlobs { .. } => rpc_blob_queue.push(work, work_id, &self.log), + Work::RpcBlobs { .. } => rpc_blob_queue.push(work, work_id), Work::RpcCustodyColumn { .. } => { - rpc_custody_column_queue.push(work, work_id, &self.log) + rpc_custody_column_queue.push(work, work_id) } Work::RpcVerifyDataColumn(_) => { - rpc_verify_data_column_queue.push(work, work_id, &self.log) - } - Work::SamplingResult(_) => { - sampling_result_queue.push(work, work_id, &self.log) - } - Work::ChainSegment { .. } => { - chain_segment_queue.push(work, work_id, &self.log) + rpc_verify_data_column_queue.push(work, work_id) } + Work::SamplingResult(_) => sampling_result_queue.push(work, work_id), + Work::ChainSegment { .. } => chain_segment_queue.push(work, work_id), Work::ChainSegmentBackfill { .. } => { - backfill_chain_segment.push(work, work_id, &self.log) - } - Work::Status { .. } => status_queue.push(work, work_id, &self.log), - Work::BlocksByRangeRequest { .. } => { - bbrange_queue.push(work, work_id, &self.log) - } - Work::BlocksByRootsRequest { .. } => { - bbroots_queue.push(work, work_id, &self.log) - } - Work::BlobsByRangeRequest { .. } => { - blbrange_queue.push(work, work_id, &self.log) + backfill_chain_segment.push(work, work_id) } + Work::Status { .. } => status_queue.push(work, work_id), + Work::BlocksByRangeRequest { .. } => bbrange_queue.push(work, work_id), + Work::BlocksByRootsRequest { .. } => bbroots_queue.push(work, work_id), + Work::BlobsByRangeRequest { .. } => blbrange_queue.push(work, work_id), Work::LightClientBootstrapRequest { .. } => { - lc_bootstrap_queue.push(work, work_id, &self.log) + lc_bootstrap_queue.push(work, work_id) } Work::LightClientOptimisticUpdateRequest { .. } => { - lc_rpc_optimistic_update_queue.push(work, work_id, &self.log) + lc_rpc_optimistic_update_queue.push(work, work_id) } Work::LightClientFinalityUpdateRequest { .. } => { - lc_rpc_finality_update_queue.push(work, work_id, &self.log) + lc_rpc_finality_update_queue.push(work, work_id) } Work::LightClientUpdatesByRangeRequest { .. } => { - lc_update_range_queue.push(work, work_id, &self.log) + lc_update_range_queue.push(work, work_id) } Work::UnknownBlockAttestation { .. } => { unknown_block_attestation_queue.push(work) @@ -1426,29 +1398,23 @@ impl BeaconProcessor { unknown_block_aggregate_queue.push(work) } Work::GossipBlsToExecutionChange { .. } => { - gossip_bls_to_execution_change_queue.push(work, work_id, &self.log) - } - Work::BlobsByRootsRequest { .. } => { - blbroots_queue.push(work, work_id, &self.log) + gossip_bls_to_execution_change_queue.push(work, work_id) } + Work::BlobsByRootsRequest { .. } => blbroots_queue.push(work, work_id), Work::DataColumnsByRootsRequest { .. } => { - dcbroots_queue.push(work, work_id, &self.log) + dcbroots_queue.push(work, work_id) } Work::DataColumnsByRangeRequest { .. } => { - dcbrange_queue.push(work, work_id, &self.log) + dcbrange_queue.push(work, work_id) } Work::UnknownLightClientOptimisticUpdate { .. } => { - unknown_light_client_update_queue.push(work, work_id, &self.log) + unknown_light_client_update_queue.push(work, work_id) } Work::UnknownBlockSamplingRequest { .. } => { - unknown_block_sampling_request_queue.push(work, work_id, &self.log) - } - Work::ApiRequestP0 { .. } => { - api_request_p0_queue.push(work, work_id, &self.log) - } - Work::ApiRequestP1 { .. } => { - api_request_p1_queue.push(work, work_id, &self.log) + unknown_block_sampling_request_queue.push(work, work_id) } + Work::ApiRequestP0 { .. } => api_request_p0_queue.push(work, work_id), + Work::ApiRequestP1 { .. } => api_request_p1_queue.push(work, work_id), }; Some(work_type) } @@ -1526,19 +1492,17 @@ impl BeaconProcessor { if aggregate_queue.is_full() && aggregate_debounce.elapsed() { error!( - self.log, - "Aggregate attestation queue full"; - "msg" => "the system has insufficient resources for load", - "queue_len" => aggregate_queue.max_length, + msg = "the system has insufficient resources for load", + queue_len = aggregate_queue.max_length, + "Aggregate attestation queue full" ) } if attestation_queue.is_full() && attestation_debounce.elapsed() { error!( - self.log, - "Attestation queue full"; - "msg" => "the system has insufficient resources for load", - "queue_len" => attestation_queue.max_length, + msg = "the system has insufficient resources for load", + queue_len = attestation_queue.max_length, + "Attestation queue full" ) } } @@ -1569,7 +1533,6 @@ impl BeaconProcessor { let send_idle_on_drop = SendOnDrop { tx: idle_tx, _worker_timer: worker_timer, - log: self.log.clone(), }; let worker_id = self.current_workers; @@ -1578,10 +1541,9 @@ impl BeaconProcessor { let executor = self.executor.clone(); trace!( - self.log, - "Spawning beacon processor worker"; - "work" => work_id, - "worker" => worker_id, + work = work_id, + worker = worker_id, + "Spawning beacon processor worker" ); let task_spawner = TaskSpawner { @@ -1719,8 +1681,8 @@ impl TaskSpawner { } } -/// This struct will send a message on `self.tx` when it is dropped. An error will be logged on -/// `self.log` if the send fails (this happens when the node is shutting down). +/// This struct will send a message on `self.tx` when it is dropped. An error will be logged +/// if the send fails (this happens when the node is shutting down). /// /// ## Purpose /// @@ -1733,17 +1695,15 @@ pub struct SendOnDrop { tx: mpsc::Sender<()>, // The field is unused, but it's here to ensure the timer is dropped once the task has finished. _worker_timer: Option, - log: Logger, } impl Drop for SendOnDrop { fn drop(&mut self) { if let Err(e) = self.tx.try_send(()) { warn!( - self.log, - "Unable to free worker"; - "msg" => "did not free worker, shutdown may be underway", - "error" => %e + msg = "did not free worker, shutdown may be underway", + error = %e, + "Unable to free worker" ) } } diff --git a/beacon_node/beacon_processor/src/work_reprocessing_queue.rs b/beacon_node/beacon_processor/src/work_reprocessing_queue.rs index a43310ac834..2b6e72ae0c3 100644 --- a/beacon_node/beacon_processor/src/work_reprocessing_queue.rs +++ b/beacon_node/beacon_processor/src/work_reprocessing_queue.rs @@ -16,8 +16,8 @@ use fnv::FnvHashMap; use futures::task::Poll; use futures::{Stream, StreamExt}; use itertools::Itertools; +use logging::crit; use logging::TimeLatch; -use slog::{crit, debug, error, trace, warn, Logger}; use slot_clock::SlotClock; use std::collections::{HashMap, HashSet}; use std::future::Future; @@ -29,6 +29,7 @@ use strum::AsRefStr; use task_executor::TaskExecutor; use tokio::sync::mpsc::{self, Receiver, Sender}; use tokio_util::time::delay_queue::{DelayQueue, Key as DelayKey}; +use tracing::{debug, error, trace, warn}; use types::{EthSpec, Hash256, Slot}; const TASK_NAME: &str = "beacon_processor_reprocess_queue"; @@ -374,7 +375,6 @@ pub fn spawn_reprocess_scheduler( work_reprocessing_rx: Receiver, executor: &TaskExecutor, slot_clock: Arc, - log: Logger, maximum_gossip_clock_disparity: Duration, ) -> Result<(), String> { // Sanity check @@ -386,14 +386,10 @@ pub fn spawn_reprocess_scheduler( executor.spawn( async move { while let Some(msg) = queue.next().await { - queue.handle_message(msg, &log); + queue.handle_message(msg); } - debug!( - log, - "Re-process queue stopped"; - "msg" => "shutting down" - ); + debug!(msg = "shutting down", "Re-process queue stopped"); }, TASK_NAME, ); @@ -436,7 +432,7 @@ impl ReprocessQueue { } } - fn handle_message(&mut self, msg: InboundEvent, log: &Logger) { + fn handle_message(&mut self, msg: InboundEvent) { use ReprocessQueueMessage::*; match msg { // Some block has been indicated as "early" and should be processed when the @@ -455,10 +451,9 @@ impl ReprocessQueue { if self.queued_gossip_block_roots.len() >= MAXIMUM_QUEUED_BLOCKS { if self.early_block_debounce.elapsed() { warn!( - log, - "Early blocks queue is full"; - "queue_size" => MAXIMUM_QUEUED_BLOCKS, - "msg" => "check system clock" + queue_size = MAXIMUM_QUEUED_BLOCKS, + msg = "system resources may be saturated", + "Early blocks queue is full" ); } // Drop the block. @@ -490,10 +485,7 @@ impl ReprocessQueue { .try_send(ReadyWork::Block(early_block)) .is_err() { - error!( - log, - "Failed to send block"; - ); + error!("Failed to send block"); } } } @@ -507,10 +499,9 @@ impl ReprocessQueue { if self.rpc_block_delay_queue.len() >= MAXIMUM_QUEUED_BLOCKS { if self.rpc_block_debounce.elapsed() { warn!( - log, - "RPC blocks queue is full"; - "queue_size" => MAXIMUM_QUEUED_BLOCKS, - "msg" => "check system clock" + queue_size = MAXIMUM_QUEUED_BLOCKS, + msg = "system resources may be saturated", + "RPC blocks queue is full" ); } // Return the block to the beacon processor signalling to @@ -522,10 +513,7 @@ impl ReprocessQueue { })) .is_err() { - error!( - log, - "Failed to send rpc block to beacon processor"; - ); + error!("Failed to send rpc block to beacon processor"); } return; } @@ -536,29 +524,24 @@ impl ReprocessQueue { } InboundEvent::ReadyRpcBlock(queued_rpc_block) => { debug!( - log, - "Sending rpc block for reprocessing"; - "block_root" => %queued_rpc_block.beacon_block_root + %queued_rpc_block.beacon_block_root, + "Sending rpc block for reprocessing" ); if self .ready_work_tx .try_send(ReadyWork::RpcBlock(queued_rpc_block)) .is_err() { - error!( - log, - "Failed to send rpc block to beacon processor"; - ); + error!("Failed to send rpc block to beacon processor"); } } InboundEvent::Msg(UnknownBlockAggregate(queued_aggregate)) => { if self.attestations_delay_queue.len() >= MAXIMUM_QUEUED_ATTESTATIONS { if self.attestation_delay_debounce.elapsed() { error!( - log, - "Aggregate attestation delay queue is full"; - "queue_size" => MAXIMUM_QUEUED_ATTESTATIONS, - "msg" => "check system clock" + queue_size = MAXIMUM_QUEUED_ATTESTATIONS, + msg = "system resources may be saturated", + "Aggregate attestation delay queue is full" ); } // Drop the attestation. @@ -588,10 +571,9 @@ impl ReprocessQueue { if self.attestations_delay_queue.len() >= MAXIMUM_QUEUED_ATTESTATIONS { if self.attestation_delay_debounce.elapsed() { error!( - log, - "Attestation delay queue is full"; - "queue_size" => MAXIMUM_QUEUED_ATTESTATIONS, - "msg" => "check system clock" + queue_size = MAXIMUM_QUEUED_ATTESTATIONS, + msg = "system resources may be saturated", + "Attestation delay queue is full" ); } // Drop the attestation. @@ -623,10 +605,9 @@ impl ReprocessQueue { if self.lc_updates_delay_queue.len() >= MAXIMUM_QUEUED_LIGHT_CLIENT_UPDATES { if self.lc_update_delay_debounce.elapsed() { error!( - log, - "Light client updates delay queue is full"; - "queue_size" => MAXIMUM_QUEUED_LIGHT_CLIENT_UPDATES, - "msg" => "check system clock" + queue_size = MAXIMUM_QUEUED_LIGHT_CLIENT_UPDATES, + msg = "system resources may be saturated", + "Light client updates delay queue is full" ); } // Drop the light client update. @@ -658,9 +639,8 @@ impl ReprocessQueue { if self.sampling_requests_delay_queue.len() >= MAXIMUM_QUEUED_SAMPLING_REQUESTS { if self.sampling_request_delay_debounce.elapsed() { error!( - log, - "Sampling requests delay queue is full"; - "queue_size" => MAXIMUM_QUEUED_SAMPLING_REQUESTS, + queue_size = MAXIMUM_QUEUED_SAMPLING_REQUESTS, + "Sampling requests delay queue is full" ); } // Drop the inbound message. @@ -724,23 +704,21 @@ impl ReprocessQueue { // There is a mismatch between the attestation ids registered for this // root and the queued attestations. This should never happen. error!( - log, - "Unknown queued attestation for block root"; - "block_root" => ?block_root, - "att_id" => ?id, + ?block_root, + att_id = ?id, + "Unknown queued attestation for block root" ); } } if failed_to_send_count > 0 { error!( - log, - "Ignored scheduled attestation(s) for block"; - "hint" => "system may be overloaded", - "parent_root" => ?parent_root, - "block_root" => ?block_root, - "failed_count" => failed_to_send_count, - "sent_count" => sent_count, + hint = "system may be overloaded", + ?parent_root, + ?block_root, + failed_count = failed_to_send_count, + sent_count, + "Ignored scheduled attestation(s) for block" ); } } @@ -772,18 +750,17 @@ impl ReprocessQueue { } } else { // This should never happen. - error!(log, "Unknown sampling request for block root"; "block_root" => ?block_root, "id" => ?id); + error!(?block_root, ?id, "Unknown sampling request for block root"); } } if failed_to_send_count > 0 { error!( - log, - "Ignored scheduled sampling requests for block"; - "hint" => "system may be overloaded", - "block_root" => ?block_root, - "failed_count" => failed_to_send_count, - "sent_count" => sent_count, + hint = "system may be overloaded", + ?block_root, + failed_to_send_count, + sent_count, + "Ignored scheduled sampling requests for block" ); } } @@ -795,10 +772,9 @@ impl ReprocessQueue { .remove(&parent_root) { debug!( - log, - "Dequeuing light client optimistic updates"; - "parent_root" => %parent_root, - "count" => queued_lc_id.len(), + %parent_root, + count = queued_lc_id.len(), + "Dequeuing light client optimistic updates" ); for lc_id in queued_lc_id { @@ -818,23 +794,16 @@ impl ReprocessQueue { // Send the work match self.ready_work_tx.try_send(work) { - Ok(_) => trace!( - log, - "reprocessing light client update sent"; - ), - Err(_) => error!( - log, - "Failed to send scheduled light client update"; - ), + Ok(_) => trace!("reprocessing light client update sent"), + Err(_) => error!("Failed to send scheduled light client update"), } } else { // There is a mismatch between the light client update ids registered for this // root and the queued light client updates. This should never happen. error!( - log, - "Unknown queued light client update for parent root"; - "parent_root" => ?parent_root, - "lc_id" => ?lc_id, + ?parent_root, + ?lc_id, + "Unknown queued light client update for parent root" ); } } @@ -855,11 +824,7 @@ impl ReprocessQueue { if !self.queued_gossip_block_roots.remove(&block_root) { // Log an error to alert that we've made a bad assumption about how this // program works, but still process the block anyway. - error!( - log, - "Unknown block in delay queue"; - "block_root" => ?block_root - ); + error!(?block_root, "Unknown block in delay queue"); } if self @@ -867,10 +832,7 @@ impl ReprocessQueue { .try_send(ReadyWork::Block(ready_block)) .is_err() { - error!( - log, - "Failed to pop queued block"; - ); + error!("Failed to pop queued block"); } } InboundEvent::ReadyAttestation(queued_id) => { @@ -901,10 +863,9 @@ impl ReprocessQueue { } { if self.ready_work_tx.try_send(work).is_err() { error!( - log, - "Ignored scheduled attestation"; - "hint" => "system may be overloaded", - "beacon_block_root" => ?root + hint = "system may be overloaded", + beacon_block_root = ?root, + "Ignored scheduled attestation" ); } @@ -929,10 +890,7 @@ impl ReprocessQueue { }, ) { if self.ready_work_tx.try_send(work).is_err() { - error!( - log, - "Failed to send scheduled light client optimistic update"; - ); + error!("Failed to send scheduled light client optimistic update"); } if let Some(queued_lc_updates) = self @@ -955,11 +913,7 @@ impl ReprocessQueue { duration.as_millis().to_string() }); - debug!( - log, - "Sending scheduled backfill work"; - "millis_from_slot_start" => millis_from_slot_start - ); + debug!(%millis_from_slot_start, "Sending scheduled backfill work"); match self .ready_work_tx @@ -971,9 +925,8 @@ impl ReprocessQueue { Err(mpsc::error::TrySendError::Full(ReadyWork::BackfillSync(batch))) | Err(mpsc::error::TrySendError::Closed(ReadyWork::BackfillSync(batch))) => { error!( - log, - "Failed to send scheduled backfill work"; - "info" => "sending work back to queue" + info = "sending work back to queue", + "Failed to send scheduled backfill work" ); self.queued_backfill_batches.insert(0, batch); @@ -984,10 +937,7 @@ impl ReprocessQueue { } // The message was not sent and we didn't get the correct // return result. This is a logic error. - _ => crit!( - log, - "Unexpected return from try_send error"; - ), + _ => crit!("Unexpected return from try_send error"), } } } @@ -1057,7 +1007,7 @@ impl ReprocessQueue { #[cfg(test)] mod tests { use super::*; - use logging::test_logger; + use logging::create_test_tracing_subscriber; use slot_clock::{ManualSlotClock, TestingSlotClock}; use std::ops::Add; use std::sync::Arc; @@ -1105,8 +1055,8 @@ mod tests { // See: https://github.com/sigp/lighthouse/issues/5504#issuecomment-2050930045 #[tokio::test] async fn backfill_schedule_failed_should_reschedule() { + create_test_tracing_subscriber(); let runtime = TestRuntime::default(); - let log = test_logger(); let (work_reprocessing_tx, work_reprocessing_rx) = mpsc::channel(1); let (ready_work_tx, mut ready_work_rx) = mpsc::channel(1); let slot_duration = 12; @@ -1117,7 +1067,6 @@ mod tests { work_reprocessing_rx, &runtime.task_executor, slot_clock.clone(), - log, Duration::from_millis(500), ) .unwrap(); diff --git a/beacon_node/client/Cargo.toml b/beacon_node/client/Cargo.toml index 614115eb588..195c53c4a01 100644 --- a/beacon_node/client/Cargo.toml +++ b/beacon_node/client/Cargo.toml @@ -27,19 +27,22 @@ http_api = { workspace = true } http_metrics = { path = "../http_metrics" } kzg = { workspace = true } lighthouse_network = { workspace = true } +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 } slasher = { workspace = true } slasher_service = { path = "../../slasher/service" } -slog = { workspace = true } slot_clock = { workspace = true } store = { workspace = true } task_executor = { workspace = true } time = "0.3.5" timer = { path = "../timer" } tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } types = { workspace = true } diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index e3bfd60a48b..3cb7b33aae2 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -33,9 +33,10 @@ 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 slog::{debug, info, warn, Logger}; use std::net::TcpListener; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -44,6 +45,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use store::database::interface::BeaconNodeBackend; use timer::spawn_timer; use tokio::sync::oneshot; +use tracing::{debug, info, warn}; use types::{ test_utils::generate_deterministic_keypairs, BeaconState, BlobSidecarList, ChainSpec, EthSpec, ExecutionBlockHash, Hash256, SignedBeaconBlock, @@ -170,11 +172,9 @@ where let runtime_context = runtime_context.ok_or("beacon_chain_start_method requires a runtime context")?; let context = runtime_context.service_context("beacon".into()); - let log = context.log(); let spec = chain_spec.ok_or("beacon_chain_start_method requires a chain spec")?; let event_handler = if self.http_api_config.enabled { Some(ServerSentEventHandler::new( - context.log().clone(), self.http_api_config.sse_capacity_multiplier, )) } else { @@ -183,12 +183,8 @@ where let execution_layer = if let Some(config) = config.execution_layer.clone() { let context = runtime_context.service_context("exec".into()); - let execution_layer = ExecutionLayer::from_config( - config, - context.executor.clone(), - context.log().clone(), - ) - .map_err(|e| format!("unable to start execution layer endpoints: {:?}", e))?; + let execution_layer = ExecutionLayer::from_config(config, context.executor.clone()) + .map_err(|e| format!("unable to start execution layer endpoints: {:?}", e))?; Some(execution_layer) } else { None @@ -205,7 +201,6 @@ where }; let builder = BeaconChainBuilder::new(eth_spec_instance, Arc::new(kzg)) - .logger(context.log().clone()) .store(store) .task_executor(context.executor.clone()) .custom_spec(spec.clone()) @@ -217,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) @@ -245,7 +243,7 @@ where // using it. let client_genesis = if matches!(client_genesis, ClientGenesis::FromStore) && !chain_exists { - info!(context.log(), "Defaulting to deposit contract genesis"); + info!("Defaulting to deposit contract genesis"); ClientGenesis::DepositContract } else if chain_exists { @@ -253,9 +251,8 @@ where || matches!(client_genesis, ClientGenesis::CheckpointSyncUrl { .. }) { info!( - context.log(), - "Refusing to checkpoint sync"; - "msg" => "database already exists, use --purge-db to force checkpoint sync" + msg = "database already exists, use --purge-db to force checkpoint sync", + "Refusing to checkpoint sync" ); } @@ -295,12 +292,9 @@ where builder.genesis_state(genesis_state).map(|v| (v, None))? } ClientGenesis::GenesisState => { - info!( - context.log(), - "Starting from known genesis state"; - ); + info!("Starting from known genesis state"); - let genesis_state = genesis_state(&runtime_context, &config, log).await?; + let genesis_state = genesis_state(&runtime_context, &config).await?; // If the user has not explicitly allowed genesis sync, prevent // them from trying to sync from genesis if we're outside of the @@ -348,12 +342,9 @@ where anchor_block_bytes, anchor_blobs_bytes, } => { - info!(context.log(), "Starting checkpoint sync"); + info!("Starting checkpoint sync"); if config.chain.genesis_backfill { - info!( - context.log(), - "Blocks will downloaded all the way back to genesis" - ); + info!("Blocks will downloaded all the way back to genesis"); } let anchor_state = BeaconState::from_ssz_bytes(&anchor_state_bytes, &spec) @@ -371,7 +362,7 @@ where } else { None }; - let genesis_state = genesis_state(&runtime_context, &config, log).await?; + let genesis_state = genesis_state(&runtime_context, &config).await?; builder .weak_subjectivity_state( @@ -384,15 +375,11 @@ where } ClientGenesis::CheckpointSyncUrl { url } => { info!( - context.log(), - "Starting checkpoint sync"; - "remote_url" => %url, + remote_url = %url, + "Starting checkpoint sync" ); if config.chain.genesis_backfill { - info!( - context.log(), - "Blocks will be downloaded all the way back to genesis" - ); + info!("Blocks will be downloaded all the way back to genesis"); } let remote = BeaconNodeHttpClient::new( @@ -406,7 +393,7 @@ where // We want to fetch deposit snapshot before fetching the finalized beacon state to // ensure that the snapshot is not newer than the beacon state that satisfies the // deposit finalization conditions - debug!(context.log(), "Downloading deposit snapshot"); + debug!("Downloading deposit snapshot"); let deposit_snapshot_result = remote .get_deposit_snapshot() .await @@ -423,22 +410,18 @@ where if deposit_snapshot.is_valid() { Some(deposit_snapshot) } else { - warn!(context.log(), "Remote BN sent invalid deposit snapshot!"); + warn!("Remote BN sent invalid deposit snapshot!"); None } } Ok(None) => { - warn!( - context.log(), - "Remote BN does not support EIP-4881 fast deposit sync" - ); + warn!("Remote BN does not support EIP-4881 fast deposit sync"); None } Err(e) => { warn!( - context.log(), - "Remote BN does not support EIP-4881 fast deposit sync"; - "error" => e + error = e, + "Remote BN does not support EIP-4881 fast deposit sync" ); None } @@ -447,21 +430,18 @@ where None }; - debug!( - context.log(), - "Downloading finalized state"; - ); + debug!("Downloading finalized state"); let state = remote .get_debug_beacon_states_ssz::(StateId::Finalized, &spec) .await .map_err(|e| format!("Error loading checkpoint state from remote: {:?}", e))? .ok_or_else(|| "Checkpoint state missing from remote".to_string())?; - debug!(context.log(), "Downloaded finalized state"; "slot" => ?state.slot()); + debug!(slot = ?state.slot(), "Downloaded finalized state"); let finalized_block_slot = state.latest_block_header().slot; - debug!(context.log(), "Downloading finalized block"; "block_slot" => ?finalized_block_slot); + debug!(block_slot = ?finalized_block_slot,"Downloading finalized block"); let block = remote .get_beacon_blocks_ssz::(BlockId::Slot(finalized_block_slot), &spec) .await @@ -476,24 +456,23 @@ where .ok_or("Finalized block missing from remote, it returned 404")?; let block_root = block.canonical_root(); - debug!(context.log(), "Downloaded finalized block"); + debug!("Downloaded finalized block"); let blobs = if block.message().body().has_blobs() { - debug!(context.log(), "Downloading finalized blobs"); + debug!("Downloading finalized blobs"); if let Some(response) = remote .get_blobs::(BlockId::Root(block_root), None) .await .map_err(|e| format!("Error fetching finalized blobs from remote: {e:?}"))? { - debug!(context.log(), "Downloaded finalized blobs"); + debug!("Downloaded finalized blobs"); Some(response.data) } else { warn!( - context.log(), - "Checkpoint server is missing blobs"; - "block_root" => %block_root, - "hint" => "use a different URL or ask the provider to update", - "impact" => "db will be slightly corrupt until these blobs are pruned", + block_root = %block_root, + hint = "use a different URL or ask the provider to update", + impact = "db will be slightly corrupt until these blobs are pruned", + "Checkpoint server is missing blobs" ); None } @@ -501,35 +480,31 @@ where None }; - let genesis_state = genesis_state(&runtime_context, &config, log).await?; + let genesis_state = genesis_state(&runtime_context, &config).await?; info!( - context.log(), - "Loaded checkpoint block and state"; - "block_slot" => block.slot(), - "state_slot" => state.slot(), - "block_root" => ?block_root, + block_slot = %block.slot(), + state_slot = %state.slot(), + block_root = ?block_root, + "Loaded checkpoint block and state" ); let service = deposit_snapshot.and_then(|snapshot| match Eth1Service::from_deposit_snapshot( config.eth1, - context.log().clone(), spec.clone(), &snapshot, ) { Ok(service) => { info!( - context.log(), - "Loaded deposit tree snapshot"; - "deposits loaded" => snapshot.deposit_count, + deposits_loaded = snapshot.deposit_count, + "Loaded deposit tree snapshot" ); Some(service) } Err(e) => { - warn!(context.log(), - "Unable to load deposit snapshot"; - "error" => ?e + warn!(error = ?e, + "Unable to load deposit snapshot" ); None } @@ -541,18 +516,14 @@ where } ClientGenesis::DepositContract => { info!( - context.log(), - "Waiting for eth2 genesis from eth1"; - "eth1_endpoints" => format!("{:?}", &config.eth1.endpoint), - "contract_deploy_block" => config.eth1.deposit_contract_deploy_block, - "deposit_contract" => &config.eth1.deposit_contract_address + eth1_endpoints = ?config.eth1.endpoint, + contract_deploy_block = config.eth1.deposit_contract_deploy_block, + deposit_contract = &config.eth1.deposit_contract_address, + "Waiting for eth2 genesis from eth1" ); - let genesis_service = Eth1GenesisService::new( - config.eth1, - context.log().clone(), - context.eth2_config().spec.clone(), - )?; + let genesis_service = + Eth1GenesisService::new(config.eth1, context.eth2_config().spec.clone())?; // If the HTTP API server is enabled, start an instance of it where it only // contains a reference to the eth1 service (all non-eth1 endpoints will fail @@ -575,7 +546,6 @@ where beacon_processor_send: None, beacon_processor_reprocess_send: None, eth1_service: Some(genesis_service.eth1_service.clone()), - log: context.log().clone(), sse_logging_components: runtime_context.sse_logging_components.clone(), }); @@ -587,10 +557,9 @@ where let (listen_addr, server) = http_api::serve(ctx, exit_future) .map_err(|e| format!("Unable to start HTTP API server: {:?}", e))?; - let log_clone = context.log().clone(); let http_api_task = async move { server.await; - debug!(log_clone, "HTTP API server task ended"); + debug!("HTTP API server task ended"); }; context @@ -617,9 +586,8 @@ where // We will restart it again after we've finished setting up for genesis. while TcpListener::bind(http_listen).is_err() { warn!( - context.log(), - "Waiting for HTTP server port to open"; - "port" => http_listen + port = %http_listen, + "Waiting for HTTP server port to open" ); tokio::time::sleep(Duration::from_secs(1)).await; } @@ -738,7 +706,7 @@ where .as_ref() .ok_or("monitoring_client requires a runtime_context")? .service_context("monitoring_client".into()); - let monitoring_client = MonitoringHttpClient::new(config, context.log().clone())?; + let monitoring_client = MonitoringHttpClient::new(config)?; monitoring_client.auto_update( context.executor, vec![ProcessType::BeaconNode, ProcessType::System], @@ -798,7 +766,6 @@ where .beacon_processor_config .take() .ok_or("build requires a beacon_processor_config")?; - let log = runtime_context.log().clone(); let http_api_listen_addr = if self.http_api_config.enabled { let ctx = Arc::new(http_api::Context { @@ -812,7 +779,6 @@ where beacon_processor_channels.work_reprocessing_tx.clone(), ), sse_logging_components: runtime_context.sse_logging_components.clone(), - log: log.clone(), }); let exit = runtime_context.executor.exit(); @@ -820,10 +786,9 @@ where let (listen_addr, server) = http_api::serve(ctx, exit) .map_err(|e| format!("Unable to start HTTP API server: {:?}", e))?; - let http_log = runtime_context.log().clone(); let http_api_task = async move { server.await; - debug!(http_log, "HTTP API server task ended"); + debug!("HTTP API server task ended"); }; runtime_context @@ -833,7 +798,7 @@ where Some(listen_addr) } else { - info!(log, "HTTP server is disabled"); + info!("HTTP server is disabled"); None }; @@ -844,7 +809,6 @@ where db_path: self.db_path.clone(), freezer_db_path: self.freezer_db_path.clone(), gossipsub_registry: self.libp2p_registry.take().map(std::sync::Mutex::new), - log: log.clone(), }); let exit = runtime_context.executor.exit(); @@ -858,7 +822,7 @@ where Some(listen_addr) } else { - debug!(log, "Metrics server is disabled"); + debug!("Metrics server is disabled"); None }; @@ -874,7 +838,6 @@ where executor: beacon_processor_context.executor.clone(), current_workers: 0, config: beacon_processor_config, - log: beacon_processor_context.log().clone(), } .spawn_manager( beacon_processor_channels.beacon_processor_rx, @@ -895,12 +858,7 @@ where } let state_advance_context = runtime_context.service_context("state_advance".into()); - let state_advance_log = state_advance_context.log().clone(); - spawn_state_advance_timer( - state_advance_context.executor, - beacon_chain.clone(), - state_advance_log, - ); + spawn_state_advance_timer(state_advance_context.executor, beacon_chain.clone()); if let Some(execution_layer) = beacon_chain.execution_layer.as_ref() { // Only send a head update *after* genesis. @@ -929,9 +887,8 @@ where // node comes online. if let Err(e) = result { warn!( - log, - "Failed to update head on execution engines"; - "error" => ?e + error = ?e, + "Failed to update head on execution engines" ); } }, @@ -954,14 +911,12 @@ where let inner_chain = beacon_chain.clone(); let light_client_update_context = runtime_context.service_context("lc_update".to_string()); - let log = light_client_update_context.log().clone(); light_client_update_context.executor.spawn( async move { compute_light_client_updates( &inner_chain, light_client_server_rv, beacon_processor_channels.work_reprocessing_tx, - &log, ) .await }, @@ -1044,7 +999,6 @@ where cold_path: &Path, blobs_path: &Path, config: StoreConfig, - log: Logger, ) -> Result { let context = self .runtime_context @@ -1073,7 +1027,6 @@ where genesis_state_root, from, to, - log, ) }; @@ -1084,7 +1037,6 @@ where schema_upgrade, config, spec, - context.log().clone(), ) .map_err(|e| format!("Unable to open database: {:?}", e))?; self.store = Some(store); @@ -1132,22 +1084,15 @@ where CachingEth1Backend::from_service(eth1_service_from_genesis) } else if config.purge_cache { - CachingEth1Backend::new(config, context.log().clone(), spec)? + CachingEth1Backend::new(config, spec)? } else { beacon_chain_builder .get_persisted_eth1_backend()? .map(|persisted| { - Eth1Chain::from_ssz_container( - &persisted, - config.clone(), - &context.log().clone(), - spec.clone(), - ) - .map(|chain| chain.into_backend()) + Eth1Chain::from_ssz_container(&persisted, config.clone(), spec.clone()) + .map(|chain| chain.into_backend()) }) - .unwrap_or_else(|| { - CachingEth1Backend::new(config, context.log().clone(), spec.clone()) - })? + .unwrap_or_else(|| CachingEth1Backend::new(config, spec.clone()))? }; self.eth1_service = Some(backend.core.clone()); @@ -1230,7 +1175,6 @@ where async fn genesis_state( context: &RuntimeContext, config: &ClientConfig, - log: &Logger, ) -> Result, String> { let eth2_network_config = context .eth2_network_config @@ -1240,7 +1184,6 @@ async fn genesis_state( .genesis_state::( config.genesis_state_url.as_deref(), config.genesis_state_url_timeout, - log, ) .await? .ok_or_else(|| "Genesis state is unknown".to_string()) diff --git a/beacon_node/client/src/compute_light_client_updates.rs b/beacon_node/client/src/compute_light_client_updates.rs index 1eb977d4213..fab284c4285 100644 --- a/beacon_node/client/src/compute_light_client_updates.rs +++ b/beacon_node/client/src/compute_light_client_updates.rs @@ -2,8 +2,8 @@ use beacon_chain::{BeaconChain, BeaconChainTypes, LightClientProducerEvent}; use beacon_processor::work_reprocessing_queue::ReprocessQueueMessage; use futures::channel::mpsc::Receiver; use futures::StreamExt; -use slog::{error, Logger}; use tokio::sync::mpsc::Sender; +use tracing::error; // Each `LightClientProducerEvent` is ~200 bytes. With the light_client server producing only recent // updates it is okay to drop some events in case of overloading. In normal network conditions @@ -15,7 +15,6 @@ pub async fn compute_light_client_updates( chain: &BeaconChain, mut light_client_server_rv: Receiver>, reprocess_tx: Sender, - log: &Logger, ) { // Should only receive events for recent blocks, import_block filters by blocks close to clock. // @@ -28,12 +27,12 @@ pub async fn compute_light_client_updates( chain .recompute_and_cache_light_client_updates(event) .unwrap_or_else(|e| { - error!(log, "error computing light_client updates {:?}", e); + error!("error computing light_client updates {:?}", e); }); let msg = ReprocessQueueMessage::NewLightClientOptimisticUpdate { parent_root }; if reprocess_tx.try_send(msg).is_err() { - error!(log, "Failed to inform light client update"; "parent_root" => %parent_root) + error!(%parent_root,"Failed to inform light client update") }; } } diff --git a/beacon_node/client/src/notifier.rs b/beacon_node/client/src/notifier.rs index c735d125380..53c9c85c001 100644 --- a/beacon_node/client/src/notifier.rs +++ b/beacon_node/client/src/notifier.rs @@ -8,12 +8,13 @@ use beacon_chain::{ BeaconChain, BeaconChainTypes, ExecutionStatus, }; use lighthouse_network::{types::SyncState, NetworkGlobals}; -use slog::{crit, debug, error, info, warn, Logger}; +use logging::crit; use slot_clock::SlotClock; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::Mutex; use tokio::time::sleep; +use tracing::{debug, error, info, warn}; use types::*; /// Create a warning log whenever the peer count is at or below this value. @@ -39,7 +40,6 @@ pub fn spawn_notifier( let slot_duration = Duration::from_secs(seconds_per_slot); let speedo = Mutex::new(Speedo::default()); - let log = executor.log().clone(); // Keep track of sync state and reset the speedo on specific sync state changes. // Specifically, if we switch between a sync and a backfill sync, reset the speedo. @@ -56,15 +56,14 @@ pub fn spawn_notifier( // waiting for genesis. Some(next_slot) if next_slot > slot_duration => { info!( - log, - "Waiting for genesis"; - "peers" => peer_count_pretty(network.connected_peers()), - "wait_time" => estimated_time_pretty(Some(next_slot.as_secs() as f64)), + peers = peer_count_pretty(network.connected_peers()), + wait_time = estimated_time_pretty(Some(next_slot.as_secs() as f64)), + "Waiting for genesis" ); - eth1_logging(&beacon_chain, &log); - bellatrix_readiness_logging(Slot::new(0), &beacon_chain, &log).await; - capella_readiness_logging(Slot::new(0), &beacon_chain, &log).await; - genesis_execution_payload_logging(&beacon_chain, &log).await; + eth1_logging(&beacon_chain); + bellatrix_readiness_logging(Slot::new(0), &beacon_chain).await; + capella_readiness_logging(Slot::new(0), &beacon_chain).await; + genesis_execution_payload_logging(&beacon_chain).await; sleep(slot_duration).await; } _ => break, @@ -82,7 +81,7 @@ pub fn spawn_notifier( let wait = match beacon_chain.slot_clock.duration_to_next_slot() { Some(duration) => duration + slot_duration / 2, None => { - warn!(log, "Unable to read current slot"); + warn!("Unable to read current slot"); sleep(slot_duration).await; continue; } @@ -120,11 +119,7 @@ pub fn spawn_notifier( let current_slot = match beacon_chain.slot() { Ok(slot) => slot, Err(e) => { - error!( - log, - "Unable to read current slot"; - "error" => format!("{:?}", e) - ); + error!(error = ?e, "Unable to read current slot"); break; } }; @@ -168,19 +163,21 @@ pub fn spawn_notifier( ); if connected_peer_count <= WARN_PEER_COUNT { - warn!(log, "Low peer count"; "peer_count" => peer_count_pretty(connected_peer_count)); + warn!( + peer_count = peer_count_pretty(connected_peer_count), + "Low peer count" + ); } debug!( - log, - "Slot timer"; - "peers" => peer_count_pretty(connected_peer_count), - "finalized_root" => format!("{}", finalized_checkpoint.root), - "finalized_epoch" => finalized_checkpoint.epoch, - "head_block" => format!("{}", head_root), - "head_slot" => head_slot, - "current_slot" => current_slot, - "sync_state" =>format!("{}", current_sync_state) + peers = peer_count_pretty(connected_peer_count), + finalized_root = %finalized_checkpoint.root, + finalized_epoch = %finalized_checkpoint.epoch, + head_block = %head_root, + %head_slot, + %current_slot, + sync_state = %current_sync_state, + "Slot timer" ); // Log if we are backfilling. @@ -202,26 +199,31 @@ pub fn spawn_notifier( if display_speed { info!( - log, - "Downloading historical blocks"; - "distance" => distance, - "speed" => sync_speed_pretty(speed), - "est_time" => estimated_time_pretty(speedo.estimated_time_till_slot(original_oldest_block_slot.saturating_sub(beacon_chain.genesis_backfill_slot))), + distance, + speed = sync_speed_pretty(speed), + est_time = estimated_time_pretty( + speedo.estimated_time_till_slot( + original_oldest_block_slot + .saturating_sub(beacon_chain.genesis_backfill_slot) + ) + ), + "Downloading historical blocks" ); } else { info!( - log, - "Downloading historical blocks"; - "distance" => distance, - "est_time" => estimated_time_pretty(speedo.estimated_time_till_slot(original_oldest_block_slot.saturating_sub(beacon_chain.genesis_backfill_slot))), + distance, + est_time = estimated_time_pretty( + speedo.estimated_time_till_slot( + original_oldest_block_slot + .saturating_sub(beacon_chain.genesis_backfill_slot) + ) + ), + "Downloading historical blocks" ); } } else if !is_backfilling && last_backfill_log_slot.is_some() { last_backfill_log_slot = None; - info!( - log, - "Historical block download complete"; - ); + info!("Historical block download complete"); } // Log if we are syncing @@ -238,20 +240,20 @@ pub fn spawn_notifier( if display_speed { info!( - log, - "Syncing"; - "peers" => peer_count_pretty(connected_peer_count), - "distance" => distance, - "speed" => sync_speed_pretty(speed), - "est_time" => estimated_time_pretty(speedo.estimated_time_till_slot(current_slot)), + peers = peer_count_pretty(connected_peer_count), + distance, + speed = sync_speed_pretty(speed), + est_time = + estimated_time_pretty(speedo.estimated_time_till_slot(current_slot)), + "Syncing" ); } else { info!( - log, - "Syncing"; - "peers" => peer_count_pretty(connected_peer_count), - "distance" => distance, - "est_time" => estimated_time_pretty(speedo.estimated_time_till_slot(current_slot)), + peers = peer_count_pretty(connected_peer_count), + distance, + est_time = + estimated_time_pretty(speedo.estimated_time_till_slot(current_slot)), + "Syncing" ); } } else if current_sync_state.is_synced() { @@ -267,20 +269,18 @@ pub fn spawn_notifier( Ok(ExecutionStatus::Valid(hash)) => format!("{} (verified)", hash), Ok(ExecutionStatus::Optimistic(hash)) => { warn!( - log, - "Head is optimistic"; - "info" => "chain not fully verified, \ - block and attestation production disabled until execution engine syncs", - "execution_block_hash" => ?hash, + info = "chain not fully verified, \ + block and attestation production disabled until execution engine syncs", + execution_block_hash = ?hash, + "Head is optimistic" ); format!("{} (unverified)", hash) } Ok(ExecutionStatus::Invalid(hash)) => { crit!( - log, - "Head execution payload is invalid"; - "msg" => "this scenario may be unrecoverable", - "execution_block_hash" => ?hash, + msg = "this scenario may be unrecoverable", + execution_block_hash = ?hash, + "Head execution payload is invalid" ); format!("{} (invalid)", hash) } @@ -288,35 +288,33 @@ pub fn spawn_notifier( }; info!( - log, - "Synced"; - "peers" => peer_count_pretty(connected_peer_count), - "exec_hash" => block_hash, - "finalized_root" => format!("{}", finalized_checkpoint.root), - "finalized_epoch" => finalized_checkpoint.epoch, - "epoch" => current_epoch, - "block" => block_info, - "slot" => current_slot, + peers = peer_count_pretty(connected_peer_count), + exec_hash = block_hash, + finalized_root = %finalized_checkpoint.root, + finalized_epoch = %finalized_checkpoint.epoch, + epoch = %current_epoch, + block = block_info, + slot = %current_slot, + "Synced" ); } else { metrics::set_gauge(&metrics::IS_SYNCED, 0); info!( - log, - "Searching for peers"; - "peers" => peer_count_pretty(connected_peer_count), - "finalized_root" => format!("{}", finalized_checkpoint.root), - "finalized_epoch" => finalized_checkpoint.epoch, - "head_slot" => head_slot, - "current_slot" => current_slot, + peers = peer_count_pretty(connected_peer_count), + finalized_root = %finalized_checkpoint.root, + finalized_epoch = %finalized_checkpoint.epoch, + %head_slot, + %current_slot, + "Searching for peers" ); } - eth1_logging(&beacon_chain, &log); - bellatrix_readiness_logging(current_slot, &beacon_chain, &log).await; - capella_readiness_logging(current_slot, &beacon_chain, &log).await; - deneb_readiness_logging(current_slot, &beacon_chain, &log).await; - electra_readiness_logging(current_slot, &beacon_chain, &log).await; - fulu_readiness_logging(current_slot, &beacon_chain, &log).await; + eth1_logging(&beacon_chain); + bellatrix_readiness_logging(current_slot, &beacon_chain).await; + capella_readiness_logging(current_slot, &beacon_chain).await; + deneb_readiness_logging(current_slot, &beacon_chain).await; + electra_readiness_logging(current_slot, &beacon_chain).await; + fulu_readiness_logging(current_slot, &beacon_chain).await; } }; @@ -331,7 +329,6 @@ pub fn spawn_notifier( async fn bellatrix_readiness_logging( current_slot: Slot, beacon_chain: &BeaconChain, - log: &Logger, ) { let merge_completed = beacon_chain .canonical_head @@ -355,10 +352,9 @@ async fn bellatrix_readiness_logging( // Logging of the EE being offline is handled in the other readiness logging functions. if !beacon_chain.is_time_to_prepare_for_capella(current_slot) { error!( - log, - "Execution endpoint required"; - "info" => "you need an execution engine to validate blocks, see: \ - https://lighthouse-book.sigmaprime.io/merge-migration.html" + info = "you need an execution engine to validate blocks, see: \ + https://lighthouse-book.sigmaprime.io/archived_merge_migration.html", + "Execution endpoint required" ); } return; @@ -375,12 +371,11 @@ async fn bellatrix_readiness_logging( terminal_block_hash_epoch: None, } => { info!( - log, - "Ready for Bellatrix"; - "terminal_total_difficulty" => %ttd, - "current_difficulty" => current_difficulty + terminal_total_difficulty = %ttd, + current_difficulty = current_difficulty .map(|d| d.to_string()) .unwrap_or_else(|| "??".into()), + "Ready for Bellatrix" ) } MergeConfig { @@ -389,29 +384,25 @@ async fn bellatrix_readiness_logging( terminal_block_hash_epoch: Some(terminal_block_hash_epoch), } => { info!( - log, - "Ready for Bellatrix"; - "info" => "you are using override parameters, please ensure that you \ - understand these parameters and their implications.", - "terminal_block_hash" => ?terminal_block_hash, - "terminal_block_hash_epoch" => ?terminal_block_hash_epoch, + info = "you are using override parameters, please ensure that you \ + understand these parameters and their implications.", + ?terminal_block_hash, + ?terminal_block_hash_epoch, + "Ready for Bellatrix" ) } other => error!( - log, - "Inconsistent merge configuration"; - "config" => ?other + config = ?other, + "Inconsistent merge configuration" ), }, readiness @ BellatrixReadiness::NotSynced => warn!( - log, - "Not ready Bellatrix"; - "info" => %readiness, + info = %readiness, + "Not ready Bellatrix" ), readiness @ BellatrixReadiness::NoExecutionEndpoint => warn!( - log, - "Not ready for Bellatrix"; - "info" => %readiness, + info = %readiness, + "Not ready for Bellatrix" ), } } @@ -420,7 +411,6 @@ async fn bellatrix_readiness_logging( async fn capella_readiness_logging( current_slot: Slot, beacon_chain: &BeaconChain, - log: &Logger, ) { let capella_completed = beacon_chain .canonical_head @@ -442,10 +432,9 @@ async fn capella_readiness_logging( // Logging of the EE being offline is handled in the other readiness logging functions. if !beacon_chain.is_time_to_prepare_for_deneb(current_slot) { error!( - log, - "Execution endpoint required"; - "info" => "you need a Capella enabled execution engine to validate blocks, see: \ - https://lighthouse-book.sigmaprime.io/merge-migration.html" + info = "you need a Capella enabled execution engine to validate blocks, see: \ + https://lighthouse-book.sigmaprime.io/archived_merge_migration.html", + "Execution endpoint required" ); } return; @@ -454,24 +443,21 @@ async fn capella_readiness_logging( match beacon_chain.check_capella_readiness().await { CapellaReadiness::Ready => { info!( - log, - "Ready for Capella"; - "info" => "ensure the execution endpoint is updated to the latest Capella/Shanghai release" + info = "ensure the execution endpoint is updated to the latest Capella/Shanghai release", + "Ready for Capella" ) } readiness @ CapellaReadiness::ExchangeCapabilitiesFailed { error: _ } => { error!( - log, - "Not ready for Capella"; - "hint" => "the execution endpoint may be offline", - "info" => %readiness, + hint = "the execution endpoint may be offline", + info = %readiness, + "Not ready for Capella" ) } readiness => warn!( - log, - "Not ready for Capella"; - "hint" => "try updating the execution endpoint", - "info" => %readiness, + hint = "try updating the execution endpoint", + info = %readiness, + "Not ready for Capella" ), } } @@ -480,7 +466,6 @@ async fn capella_readiness_logging( async fn deneb_readiness_logging( current_slot: Slot, beacon_chain: &BeaconChain, - log: &Logger, ) { let deneb_completed = beacon_chain .canonical_head @@ -500,9 +485,8 @@ async fn deneb_readiness_logging( if deneb_completed && !has_execution_layer { error!( - log, - "Execution endpoint required"; - "info" => "you need a Deneb enabled execution engine to validate blocks." + info = "you need a Deneb enabled execution engine to validate blocks.", + "Execution endpoint required" ); return; } @@ -510,24 +494,22 @@ async fn deneb_readiness_logging( match beacon_chain.check_deneb_readiness().await { DenebReadiness::Ready => { info!( - log, - "Ready for Deneb"; - "info" => "ensure the execution endpoint is updated to the latest Deneb/Cancun release" + info = + "ensure the execution endpoint is updated to the latest Deneb/Cancun release", + "Ready for Deneb" ) } readiness @ DenebReadiness::ExchangeCapabilitiesFailed { error: _ } => { error!( - log, - "Not ready for Deneb"; - "hint" => "the execution endpoint may be offline", - "info" => %readiness, + hint = "the execution endpoint may be offline", + info = %readiness, + "Not ready for Deneb" ) } readiness => warn!( - log, - "Not ready for Deneb"; - "hint" => "try updating the execution endpoint", - "info" => %readiness, + hint = "try updating the execution endpoint", + info = %readiness, + "Not ready for Deneb" ), } } @@ -535,7 +517,6 @@ async fn deneb_readiness_logging( async fn electra_readiness_logging( current_slot: Slot, beacon_chain: &BeaconChain, - log: &Logger, ) { let electra_completed = beacon_chain .canonical_head @@ -556,9 +537,8 @@ async fn electra_readiness_logging( if electra_completed && !has_execution_layer { // When adding a new fork, add a check for the next fork readiness here. error!( - log, - "Execution endpoint required"; - "info" => "you need a Electra enabled execution engine to validate blocks." + info = "you need a Electra enabled execution engine to validate blocks.", + "Execution endpoint required" ); return; } @@ -566,24 +546,22 @@ async fn electra_readiness_logging( match beacon_chain.check_electra_readiness().await { ElectraReadiness::Ready => { info!( - log, - "Ready for Electra"; - "info" => "ensure the execution endpoint is updated to the latest Electra/Prague release" + info = + "ensure the execution endpoint is updated to the latest Electra/Prague release", + "Ready for Electra" ) } readiness @ ElectraReadiness::ExchangeCapabilitiesFailed { error: _ } => { error!( - log, - "Not ready for Electra"; - "hint" => "the execution endpoint may be offline", - "info" => %readiness, + hint = "the execution endpoint may be offline", + info = %readiness, + "Not ready for Electra" ) } readiness => warn!( - log, - "Not ready for Electra"; - "hint" => "try updating the execution endpoint", - "info" => %readiness, + hint = "try updating the execution endpoint", + info = %readiness, + "Not ready for Electra" ), } } @@ -592,7 +570,6 @@ async fn electra_readiness_logging( async fn fulu_readiness_logging( current_slot: Slot, beacon_chain: &BeaconChain, - log: &Logger, ) { let fulu_completed = beacon_chain .canonical_head @@ -612,9 +589,8 @@ async fn fulu_readiness_logging( if fulu_completed && !has_execution_layer { error!( - log, - "Execution endpoint required"; - "info" => "you need a Fulu enabled execution engine to validate blocks." + info = "you need a Fulu enabled execution engine to validate blocks.", + "Execution endpoint required" ); return; } @@ -622,102 +598,86 @@ async fn fulu_readiness_logging( match beacon_chain.check_fulu_readiness().await { FuluReadiness::Ready => { info!( - log, - "Ready for Fulu"; - "info" => "ensure the execution endpoint is updated to the latest Fulu release" + info = "ensure the execution endpoint is updated to the latest Fulu release", + "Ready for Fulu" ) } readiness @ FuluReadiness::ExchangeCapabilitiesFailed { error: _ } => { error!( - log, - "Not ready for Fulu"; - "hint" => "the execution endpoint may be offline", - "info" => %readiness, + hint = "the execution endpoint may be offline", + info = %readiness, + "Not ready for Fulu" ) } readiness => warn!( - log, - "Not ready for Fulu"; - "hint" => "try updating the execution endpoint", - "info" => %readiness, + hint = "try updating the execution endpoint", + info = %readiness, + "Not ready for Fulu" ), } } -async fn genesis_execution_payload_logging( - beacon_chain: &BeaconChain, - log: &Logger, -) { +async fn genesis_execution_payload_logging(beacon_chain: &BeaconChain) { match beacon_chain .check_genesis_execution_payload_is_correct() .await { Ok(GenesisExecutionPayloadStatus::Correct(block_hash)) => { info!( - log, - "Execution enabled from genesis"; - "genesis_payload_block_hash" => ?block_hash, + genesis_payload_block_hash = ?block_hash, + "Execution enabled from genesis" ); } Ok(GenesisExecutionPayloadStatus::BlockHashMismatch { got, expected }) => { error!( - log, - "Genesis payload block hash mismatch"; - "info" => "genesis is misconfigured and likely to fail", - "consensus_node_block_hash" => ?expected, - "execution_node_block_hash" => ?got, + info = "genesis is misconfigured and likely to fail", + consensus_node_block_hash = ?expected, + execution_node_block_hash = ?got, + "Genesis payload block hash mismatch" ); } Ok(GenesisExecutionPayloadStatus::TransactionsRootMismatch { got, expected }) => { error!( - log, - "Genesis payload transactions root mismatch"; - "info" => "genesis is misconfigured and likely to fail", - "consensus_node_transactions_root" => ?expected, - "execution_node_transactions_root" => ?got, + info = "genesis is misconfigured and likely to fail", + consensus_node_transactions_root = ?expected, + execution_node_transactions_root = ?got, + "Genesis payload transactions root mismatch" ); } Ok(GenesisExecutionPayloadStatus::WithdrawalsRootMismatch { got, expected }) => { error!( - log, - "Genesis payload withdrawals root mismatch"; - "info" => "genesis is misconfigured and likely to fail", - "consensus_node_withdrawals_root" => ?expected, - "execution_node_withdrawals_root" => ?got, + info = "genesis is misconfigured and likely to fail", + consensus_node_withdrawals_root = ?expected, + execution_node_withdrawals_root = ?got, + "Genesis payload withdrawals root mismatch" ); } Ok(GenesisExecutionPayloadStatus::OtherMismatch) => { error!( - log, - "Genesis payload header mismatch"; - "info" => "genesis is misconfigured and likely to fail", - "detail" => "see debug logs for payload headers" + info = "genesis is misconfigured and likely to fail", + detail = "see debug logs for payload headers", + "Genesis payload header mismatch" ); } Ok(GenesisExecutionPayloadStatus::Irrelevant) => { - info!( - log, - "Execution is not enabled from genesis"; - ); + info!("Execution is not enabled from genesis"); } Ok(GenesisExecutionPayloadStatus::AlreadyHappened) => { warn!( - log, - "Unable to check genesis which has already occurred"; - "info" => "this is probably a race condition or a bug" + info = "this is probably a race condition or a bug", + "Unable to check genesis which has already occurred" ); } Err(e) => { error!( - log, - "Unable to check genesis execution payload"; - "error" => ?e + error = ?e, + "Unable to check genesis execution payload" ); } } } -fn eth1_logging(beacon_chain: &BeaconChain, log: &Logger) { +fn eth1_logging(beacon_chain: &BeaconChain) { let current_slot_opt = beacon_chain.slot().ok(); // Perform some logging about the eth1 chain @@ -733,13 +693,12 @@ fn eth1_logging(beacon_chain: &BeaconChain, log: &Logger &beacon_chain.spec, ) { debug!( - log, - "Eth1 cache sync status"; - "eth1_head_block" => status.head_block_number, - "latest_cached_block_number" => status.latest_cached_block_number, - "latest_cached_timestamp" => status.latest_cached_block_timestamp, - "voting_target_timestamp" => status.voting_target_timestamp, - "ready" => status.lighthouse_is_cached_and_ready + eth1_head_block = status.head_block_number, + latest_cached_block_number = status.latest_cached_block_number, + latest_cached_timestamp = status.latest_cached_block_timestamp, + voting_target_timestamp = status.voting_target_timestamp, + ready = status.lighthouse_is_cached_and_ready, + "Eth1 cache sync status" ); if !status.lighthouse_is_cached_and_ready { @@ -755,16 +714,12 @@ fn eth1_logging(beacon_chain: &BeaconChain, log: &Logger .unwrap_or_else(|| "initializing deposits".to_string()); warn!( - log, - "Syncing deposit contract block cache"; - "est_blocks_remaining" => distance, + est_blocks_remaining = distance, + "Syncing deposit contract block cache" ); } } else { - error!( - log, - "Unable to determine deposit contract sync status"; - ); + error!("Unable to determine deposit contract sync status"); } } } diff --git a/beacon_node/eth1/Cargo.toml b/beacon_node/eth1/Cargo.toml index 8ccd50aad8d..fa08364251c 100644 --- a/beacon_node/eth1/Cargo.toml +++ b/beacon_node/eth1/Cargo.toml @@ -8,7 +8,6 @@ edition = { workspace = true } environment = { workspace = true } eth1_test_rig = { workspace = true } serde_yaml = { workspace = true } -sloggers = { workspace = true } [dependencies] eth2 = { workspace = true } @@ -22,10 +21,10 @@ metrics = { workspace = true } parking_lot = { workspace = true } sensitive_url = { workspace = true } serde = { workspace = true } -slog = { workspace = true } state_processing = { workspace = true } superstruct = { workspace = true } task_executor = { workspace = true } tokio = { workspace = true } +tracing = { workspace = true } tree_hash = { workspace = true } types = { workspace = true } diff --git a/beacon_node/eth1/src/service.rs b/beacon_node/eth1/src/service.rs index 71ab98a6a20..6b10bd2215c 100644 --- a/beacon_node/eth1/src/service.rs +++ b/beacon_node/eth1/src/service.rs @@ -13,13 +13,13 @@ use futures::future::TryFutureExt; use parking_lot::{RwLock, RwLockReadGuard}; use sensitive_url::SensitiveUrl; use serde::{Deserialize, Serialize}; -use slog::{debug, error, info, trace, warn, Logger}; use std::fmt::Debug; use std::ops::{Range, RangeInclusive}; use std::path::PathBuf; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use tokio::time::{interval_at, Duration, Instant}; +use tracing::{debug, error, info, trace, warn}; use types::{ChainSpec, DepositTreeSnapshot, Eth1Data, EthSpec, Unsigned}; /// Indicates the default eth1 chain id we use for the deposit contract. @@ -58,22 +58,16 @@ type EndpointState = Result<(), EndpointError>; /// Returns `Ok` if the endpoint is usable, i.e. is reachable and has a correct network id and /// chain id. Otherwise it returns `Err`. -async fn endpoint_state( - endpoint: &HttpJsonRpc, - config_chain_id: &Eth1Id, - log: &Logger, -) -> EndpointState { +async fn endpoint_state(endpoint: &HttpJsonRpc, config_chain_id: &Eth1Id) -> EndpointState { let error_connecting = |e: String| { debug!( - log, - "eth1 endpoint error"; - "endpoint" => %endpoint, - "error" => &e, + %endpoint, + error = &e, + "eth1 endpoint error" ); warn!( - log, - "Error connecting to eth1 node endpoint"; - "endpoint" => %endpoint, + %endpoint, + "Error connecting to eth1 node endpoint" ); EndpointError::RequestFailed(e) }; @@ -86,19 +80,17 @@ async fn endpoint_state( // Handle the special case if chain_id == Eth1Id::Custom(0) { warn!( - log, - "Remote execution node is not synced"; - "endpoint" => %endpoint, + %endpoint, + "Remote execution node is not synced" ); return Err(EndpointError::FarBehind); } if &chain_id != config_chain_id { warn!( - log, - "Invalid execution chain ID. Please switch to correct chain ID on endpoint"; - "endpoint" => %endpoint, - "expected" => ?config_chain_id, - "received" => ?chain_id, + %endpoint, + expected = ?config_chain_id, + received = ?chain_id, + "Invalid execution chain ID. Please switch to correct chain ID on endpoint" ); Err(EndpointError::WrongChainId) } else { @@ -134,10 +126,9 @@ async fn get_remote_head_and_new_block_ranges( .unwrap_or(u64::MAX); if remote_head_block.timestamp + node_far_behind_seconds < now { warn!( - service.log, - "Execution endpoint is not synced"; - "endpoint" => %endpoint, - "last_seen_block_unix_timestamp" => remote_head_block.timestamp, + %endpoint, + last_seen_block_unix_timestamp = remote_head_block.timestamp, + "Execution endpoint is not synced" ); return Err(Error::EndpointError(EndpointError::FarBehind)); } @@ -145,9 +136,8 @@ async fn get_remote_head_and_new_block_ranges( let handle_remote_not_synced = |e| { if let Error::RemoteNotSynced { .. } = e { warn!( - service.log, - "Execution endpoint is not synced"; - "endpoint" => %endpoint, + %endpoint, + "Execution endpoint is not synced" ); } e @@ -392,12 +382,11 @@ pub fn endpoint_from_config(config: &Config) -> Result { #[derive(Clone)] pub struct Service { inner: Arc, - pub log: Logger, } impl Service { /// Creates a new service. Does not attempt to connect to the eth1 node. - pub fn new(config: Config, log: Logger, spec: Arc) -> Result { + pub fn new(config: Config, spec: Arc) -> Result { Ok(Self { inner: Arc::new(Inner { block_cache: <_>::default(), @@ -410,7 +399,6 @@ impl Service { config: RwLock::new(config), spec, }), - log, }) } @@ -425,7 +413,6 @@ impl Service { /// Creates a new service, initializing the deposit tree from a snapshot. pub fn from_deposit_snapshot( config: Config, - log: Logger, spec: Arc, deposit_snapshot: &DepositTreeSnapshot, ) -> Result { @@ -444,7 +431,6 @@ impl Service { config: RwLock::new(config), spec, }), - log, }) } @@ -464,16 +450,10 @@ impl Service { } /// Recover the deposit and block caches from encoded bytes. - pub fn from_bytes( - bytes: &[u8], - config: Config, - log: Logger, - spec: Arc, - ) -> Result { + pub fn from_bytes(bytes: &[u8], config: Config, spec: Arc) -> Result { let inner = Inner::from_bytes(bytes, config, spec)?; Ok(Self { inner: Arc::new(inner), - log, }) } @@ -621,11 +601,10 @@ impl Service { &self, ) -> Result<(DepositCacheUpdateOutcome, BlockCacheUpdateOutcome), String> { let client = self.client(); - let log = self.log.clone(); let chain_id = self.config().chain_id.clone(); let node_far_behind_seconds = self.inner.config.read().node_far_behind_seconds; - match endpoint_state(client, &chain_id, &log).await { + match endpoint_state(client, &chain_id).await { Ok(()) => crate::metrics::set_gauge(&metrics::ETH1_CONNECTED, 1), Err(e) => { crate::metrics::set_gauge(&metrics::ETH1_CONNECTED, 0); @@ -655,10 +634,9 @@ impl Service { { let mut deposit_cache = self.inner.deposit_cache.write(); debug!( - self.log, - "Resetting last processed block"; - "old_block_number" => deposit_cache.last_processed_block, - "new_block_number" => deposit_cache.cache.latest_block_number(), + old_block_number = deposit_cache.last_processed_block, + new_block_number = deposit_cache.cache.latest_block_number(), + "Resetting last processed block" ); deposit_cache.last_processed_block = Some(deposit_cache.cache.latest_block_number()); @@ -668,11 +646,11 @@ impl Service { outcome_result.map_err(|e| format!("Failed to update deposit cache: {:?}", e))?; trace!( - self.log, - "Updated deposit cache"; - "cached_deposits" => self.inner.deposit_cache.read().cache.len(), - "logs_imported" => outcome.logs_imported, - "last_processed_execution_block" => self.inner.deposit_cache.read().last_processed_block, + cached_deposits = self.inner.deposit_cache.read().cache.len(), + logs_imported = outcome.logs_imported, + last_processed_execution_block = + self.inner.deposit_cache.read().last_processed_block, + "Updated deposit cache" ); Ok::<_, String>(outcome) }; @@ -684,11 +662,10 @@ impl Service { .map_err(|e| format!("Failed to update deposit contract block cache: {:?}", e))?; trace!( - self.log, - "Updated deposit contract block cache"; - "cached_blocks" => self.inner.block_cache.read().len(), - "blocks_imported" => outcome.blocks_imported, - "head_block" => outcome.head_block_number, + cached_blocks = self.inner.block_cache.read().len(), + blocks_imported = outcome.blocks_imported, + head_block = outcome.head_block_number, + "Updated deposit contract block cache" ); Ok::<_, String>(outcome) }; @@ -727,17 +704,15 @@ impl Service { let update_result = self.update().await; match update_result { Err(e) => error!( - self.log, - "Error updating deposit contract cache"; - "retry_millis" => update_interval.as_millis(), - "error" => e, + retry_millis = update_interval.as_millis(), + error = e, + "Error updating deposit contract cache" ), Ok((deposit, block)) => debug!( - self.log, - "Updated deposit contract cache"; - "retry_millis" => update_interval.as_millis(), - "blocks" => format!("{:?}", block), - "deposits" => format!("{:?}", deposit), + retry_millis = update_interval.as_millis(), + ?block, + ?deposit, + "Updated deposit contract cache" ), }; let optional_eth1data = self.inner.to_finalize.write().take(); @@ -752,23 +727,20 @@ impl Service { if deposit_count_to_finalize > already_finalized { match self.finalize_deposits(eth1data_to_finalize) { Err(e) => warn!( - self.log, - "Failed to finalize deposit cache"; - "error" => ?e, - "info" => "this should resolve on its own" + error = ?e, + info = "this should resolve on its own", + "Failed to finalize deposit cache" ), Ok(()) => info!( - self.log, - "Successfully finalized deposit tree"; - "finalized deposit count" => deposit_count_to_finalize, + finalized_deposit_count = deposit_count_to_finalize, + "Successfully finalized deposit tree" ), } } else { debug!( - self.log, - "Deposits tree already finalized"; - "already_finalized" => already_finalized, - "deposit_count_to_finalize" => deposit_count_to_finalize, + %already_finalized, + %deposit_count_to_finalize, + "Deposits tree already finalized" ); } } @@ -889,10 +861,7 @@ impl Service { let deposit_contract_address_ref: &str = &deposit_contract_address; for block_range in block_number_chunks.into_iter() { if block_range.is_empty() { - debug!( - self.log, - "No new blocks to scan for logs"; - ); + debug!("No new blocks to scan for logs"); continue; } @@ -946,11 +915,7 @@ impl Service { Ok::<_, Error>(()) })?; - debug!( - self.log, - "Imported deposit logs chunk"; - "logs" => logs.len(), - ); + debug!(logs = logs.len(), "Imported deposit logs chunk"); cache.last_processed_block = Some(block_range.end.saturating_sub(1)); @@ -963,18 +928,16 @@ impl Service { if logs_imported > 0 { info!( - self.log, - "Imported deposit log(s)"; - "latest_block" => self.inner.deposit_cache.read().cache.latest_block_number(), - "total" => self.deposit_cache_len(), - "new" => logs_imported + latest_block = self.inner.deposit_cache.read().cache.latest_block_number(), + total = self.deposit_cache_len(), + new = logs_imported, + "Imported deposit log(s)" ); } else { debug!( - self.log, - "No new deposits found"; - "latest_block" => self.inner.deposit_cache.read().cache.latest_block_number(), - "total_deposits" => self.deposit_cache_len(), + latest_block = self.inner.deposit_cache.read().cache.latest_block_number(), + total_deposits = self.deposit_cache_len(), + "No new deposits found" ); } @@ -1058,10 +1021,9 @@ impl Service { .collect::>(); debug!( - self.log, - "Downloading execution blocks"; - "first" => ?required_block_numbers.first(), - "last" => ?required_block_numbers.last(), + first = ?required_block_numbers.first(), + last = ?required_block_numbers.last(), + "Downloading execution blocks" ); // Produce a stream from the list of required block numbers and return a future that @@ -1116,19 +1078,17 @@ impl Service { if blocks_imported > 0 { debug!( - self.log, - "Imported execution block(s)"; - "latest_block_age" => latest_block_mins, - "latest_block" => block_cache.highest_block_number(), - "total_cached_blocks" => block_cache.len(), - "new" => %blocks_imported + latest_block_age = latest_block_mins, + latest_block = block_cache.highest_block_number(), + total_cached_blocks = block_cache.len(), + new = %blocks_imported, + "Imported execution block(s)" ); } else { debug!( - self.log, - "No new execution blocks imported"; - "latest_block" => block_cache.highest_block_number(), - "cached_blocks" => block_cache.len(), + latest_block = block_cache.highest_block_number(), + cached_blocks = block_cache.len(), + "No new execution blocks imported" ); } diff --git a/beacon_node/eth1/tests/test.rs b/beacon_node/eth1/tests/test.rs index e442ce48630..48ed1892598 100644 --- a/beacon_node/eth1/tests/test.rs +++ b/beacon_node/eth1/tests/test.rs @@ -4,7 +4,7 @@ use eth1::{Config, Eth1Endpoint, Service}; use eth1::{DepositCache, DEFAULT_CHAIN_ID}; use eth1_test_rig::{AnvilEth1Instance, Http, Middleware, Provider}; use execution_layer::http::{deposit_methods::*, HttpJsonRpc, Log}; -use logging::test_logger; +use logging::create_test_tracing_subscriber; use merkle_proof::verify_merkle_proof; use sensitive_url::SensitiveUrl; use std::ops::Range; @@ -19,11 +19,10 @@ use types::{ const DEPOSIT_CONTRACT_TREE_DEPTH: usize = 32; pub fn new_env() -> Environment { + create_test_tracing_subscriber(); EnvironmentBuilder::minimal() .multi_threaded_tokio_runtime() .expect("should start tokio runtime") - .test_logger() - .expect("should start null logger") .build() .expect("should build env") } @@ -100,9 +99,8 @@ mod eth1_cache { #[tokio::test] async fn simple_scenario() { + create_test_tracing_subscriber(); async { - let log = test_logger(); - for follow_distance in 0..3 { let eth1 = new_anvil_instance() .await @@ -123,12 +121,8 @@ mod eth1_cache { }; let cache_follow_distance = config.cache_follow_distance(); - let service = Service::new( - config, - log.clone(), - Arc::new(MainnetEthSpec::default_spec()), - ) - .unwrap(); + let service = + Service::new(config, Arc::new(MainnetEthSpec::default_spec())).unwrap(); // Create some blocks and then consume them, performing the test `rounds` times. for round in 0..2 { @@ -186,9 +180,8 @@ mod eth1_cache { #[tokio::test] async fn big_skip() { + create_test_tracing_subscriber(); async { - let log = test_logger(); - let eth1 = new_anvil_instance() .await .expect("should start eth1 environment"); @@ -208,7 +201,6 @@ mod eth1_cache { block_cache_truncation: Some(cache_len), ..Config::default() }, - log, Arc::new(MainnetEthSpec::default_spec()), ) .unwrap(); @@ -241,9 +233,8 @@ mod eth1_cache { /// cache size. #[tokio::test] async fn pruning() { + create_test_tracing_subscriber(); async { - let log = test_logger(); - let eth1 = new_anvil_instance() .await .expect("should start eth1 environment"); @@ -263,7 +254,6 @@ mod eth1_cache { block_cache_truncation: Some(cache_len), ..Config::default() }, - log, Arc::new(MainnetEthSpec::default_spec()), ) .unwrap(); @@ -293,9 +283,8 @@ mod eth1_cache { #[tokio::test] async fn double_update() { + create_test_tracing_subscriber(); async { - let log = test_logger(); - let n = 16; let eth1 = new_anvil_instance() @@ -314,7 +303,6 @@ mod eth1_cache { follow_distance: 0, ..Config::default() }, - log, Arc::new(MainnetEthSpec::default_spec()), ) .unwrap(); @@ -346,9 +334,8 @@ mod deposit_tree { #[tokio::test] async fn updating() { + create_test_tracing_subscriber(); async { - let log = test_logger(); - let n = 4; let eth1 = new_anvil_instance() @@ -369,7 +356,6 @@ mod deposit_tree { follow_distance: 0, ..Config::default() }, - log, Arc::new(MainnetEthSpec::default_spec()), ) .unwrap(); @@ -427,9 +413,8 @@ mod deposit_tree { #[tokio::test] async fn double_update() { + create_test_tracing_subscriber(); async { - let log = test_logger(); - let n = 8; let eth1 = new_anvil_instance() @@ -451,7 +436,6 @@ mod deposit_tree { follow_distance: 0, ..Config::default() }, - log, Arc::new(MainnetEthSpec::default_spec()), ) .unwrap(); @@ -689,9 +673,8 @@ mod fast { // with the deposit count and root computed from the deposit cache. #[tokio::test] async fn deposit_cache_query() { + create_test_tracing_subscriber(); async { - let log = test_logger(); - let eth1 = new_anvil_instance() .await .expect("should start eth1 environment"); @@ -712,7 +695,6 @@ mod fast { block_cache_truncation: None, ..Config::default() }, - log, spec.clone(), ) .unwrap(); @@ -772,9 +754,8 @@ mod persist { use super::*; #[tokio::test] async fn test_persist_caches() { + create_test_tracing_subscriber(); async { - let log = test_logger(); - let eth1 = new_anvil_instance() .await .expect("should start eth1 environment"); @@ -793,12 +774,8 @@ mod persist { block_cache_truncation: None, ..Config::default() }; - let service = Service::new( - config.clone(), - log.clone(), - Arc::new(MainnetEthSpec::default_spec()), - ) - .unwrap(); + let service = + Service::new(config.clone(), Arc::new(MainnetEthSpec::default_spec())).unwrap(); let n = 10; let deposits: Vec<_> = (0..n).map(|_| random_deposit_data()).collect(); for deposit in &deposits { @@ -840,7 +817,6 @@ mod persist { let recovered_service = Service::from_bytes( ð1_bytes, config, - log, Arc::new(MainnetEthSpec::default_spec()), ) .unwrap(); diff --git a/beacon_node/execution_layer/Cargo.toml b/beacon_node/execution_layer/Cargo.toml index 7eb7b4a15e1..f56159c7b55 100644 --- a/beacon_node/execution_layer/Cargo.toml +++ b/beacon_node/execution_layer/Cargo.toml @@ -12,7 +12,6 @@ arc-swap = "1.6.0" builder_client = { path = "../builder_client" } bytes = { workspace = true } eth2 = { workspace = true } -eth2_network_config = { workspace = true } ethereum_serde_utils = { workspace = true } ethereum_ssz = { workspace = true } ethers-core = { workspace = true } @@ -36,7 +35,6 @@ sensitive_url = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } -slog = { workspace = true } slot_clock = { workspace = true } ssz_types = { workspace = true } state_processing = { workspace = true } @@ -46,6 +44,7 @@ task_executor = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true } tokio-stream = { workspace = true } +tracing = { workspace = true } tree_hash = { workspace = true } tree_hash_derive = { workspace = true } triehash = "0.8.4" diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index aed6cdba670..4bfee223ff4 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -1,10 +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_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_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, @@ -553,6 +554,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, } impl EngineCapabilities { @@ -609,6 +611,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); + } response } diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index 747383754a5..bf4c391a8d8 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); /// This error is returned during a `chainId` call by Geth. @@ -87,6 +88,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, ]; /// We opt to initialize the JsonClientVersionV1 rather than the ClientVersionV1 @@ -708,7 +710,7 @@ impl HttpJsonRpc { } } - pub async fn get_blobs( + pub async fn get_blobs_v1( &self, versioned_hashes: Vec, ) -> Result>>, Error> { @@ -722,6 +724,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 get_block_by_number( &self, query: BlockByNumberQuery<'_>, @@ -963,19 +979,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 @@ -1148,6 +1151,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), }) } @@ -1320,9 +1324,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 96615297d83..30d30481eaf 100644 --- a/beacon_node/execution_layer/src/engine_api/json_structures.rs +++ b/beacon_node/execution_layer/src/engine_api/json_structures.rs @@ -717,12 +717,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 75d0b872cef..b9e030703d9 100644 --- a/beacon_node/execution_layer/src/engines.rs +++ b/beacon_node/execution_layer/src/engines.rs @@ -6,7 +6,6 @@ use crate::engine_api::{ }; use crate::{ClientVersionV1, HttpJsonRpc}; use lru::LruCache; -use slog::{debug, error, info, warn, Logger}; use std::future::Future; use std::num::NonZeroUsize; use std::sync::Arc; @@ -14,6 +13,7 @@ use std::time::Duration; use task_executor::TaskExecutor; use tokio::sync::{watch, Mutex, RwLock}; use tokio_stream::wrappers::WatchStream; +use tracing::{debug, error, info, warn}; use types::non_zero_usize::new_non_zero_usize; use types::ExecutionBlockHash; @@ -128,19 +128,17 @@ pub struct Engine { state: RwLock, latest_forkchoice_state: RwLock>, executor: TaskExecutor, - log: Logger, } impl Engine { /// Creates a new, offline engine. - pub fn new(api: HttpJsonRpc, executor: TaskExecutor, log: &Logger) -> Self { + pub fn new(api: HttpJsonRpc, executor: TaskExecutor) -> Self { Self { api, payload_id_cache: Mutex::new(LruCache::new(PAYLOAD_ID_LRU_CACHE_SIZE)), state: Default::default(), latest_forkchoice_state: Default::default(), executor, - log: log.clone(), } } @@ -167,7 +165,6 @@ impl Engine { &self, forkchoice_state: ForkchoiceState, payload_attributes: Option, - log: &Logger, ) -> Result { let response = self .api @@ -180,11 +177,7 @@ impl Engine { { self.payload_id_cache.lock().await.put(key, payload_id); } else { - debug!( - log, - "Engine returned unexpected payload_id"; - "payload_id" => ?payload_id - ); + debug!(?payload_id, "Engine returned unexpected payload_id"); } } @@ -205,33 +198,24 @@ impl Engine { if let Some(forkchoice_state) = latest_forkchoice_state { if forkchoice_state.head_block_hash == ExecutionBlockHash::zero() { debug!( - self.log, - "No need to call forkchoiceUpdated"; - "msg" => "head does not have execution enabled", + msg = "head does not have execution enabled", + "No need to call forkchoiceUpdated" ); return; } - info!( - self.log, - "Issuing forkchoiceUpdated"; - "forkchoice_state" => ?forkchoice_state, - ); + info!(?forkchoice_state, "Issuing forkchoiceUpdated"); // For simplicity, payload attributes are never included in this call. It may be // reasonable to include them in the future. if let Err(e) = self.api.forkchoice_updated(forkchoice_state, None).await { debug!( - self.log, - "Failed to issue latest head to engine"; - "error" => ?e, + error = ?e, + "Failed to issue latest head to engine" ); } } else { - debug!( - self.log, - "No head, not sending to engine"; - ); + debug!("No head, not sending to engine"); } } @@ -252,18 +236,12 @@ impl Engine { Ok(()) => { let mut state = self.state.write().await; if **state != EngineStateInternal::Synced { - info!( - self.log, - "Execution engine online"; - ); + info!("Execution engine online"); // Send the node our latest forkchoice_state. self.send_latest_forkchoice_state().await; } else { - debug!( - self.log, - "Execution engine online"; - ); + debug!("Execution engine online"); } state.update(EngineStateInternal::Synced); (**state, ResponseCacheAction::Update) @@ -275,9 +253,8 @@ impl Engine { } Err(EngineApiError::Auth(err)) => { error!( - self.log, - "Failed jwt authorization"; - "error" => ?err, + error = ?err, + "Failed jwt authorization" ); let mut state = self.state.write().await; @@ -286,9 +263,8 @@ impl Engine { } Err(e) => { error!( - self.log, - "Error during execution engine upcheck"; - "error" => ?e, + error = ?e, + "Error during execution engine upcheck" ); let mut state = self.state.write().await; @@ -308,9 +284,9 @@ impl Engine { .get_engine_capabilities(Some(CACHED_RESPONSE_AGE_LIMIT)) .await { - warn!(self.log, - "Error during exchange capabilities"; - "error" => ?e, + warn!( + error = ?e, + "Error during exchange capabilities" ) } else { // no point in running this if there was an error fetching the capabilities @@ -326,11 +302,7 @@ impl Engine { } } - debug!( - self.log, - "Execution engine upcheck complete"; - "state" => ?state, - ); + debug!(?state, "Execution engine upcheck complete"); } /// Returns the execution engine capabilities resulting from a call to @@ -395,11 +367,7 @@ impl Engine { Ok(result) } Err(error) => { - warn!( - self.log, - "Execution engine call failed"; - "error" => ?error, - ); + warn!(?error, "Execution engine call failed"); // The node just returned an error, run an upcheck so we can update the endpoint // state. diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index b09205646e8..bbdf1a054b2 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,17 +16,17 @@ 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; +use logging::crit; use lru::LruCache; use payload_status::process_payload_status; pub use payload_status::PayloadStatus; use sensitive_url::SensitiveUrl; use serde::{Deserialize, Serialize}; -use slog::{crit, debug, error, info, warn, Logger}; use slot_clock::SlotClock; use std::collections::{hash_map::Entry, HashMap}; use std::fmt; @@ -43,6 +43,7 @@ use tokio::{ time::sleep, }; use tokio_stream::wrappers::WatchStream; +use tracing::{debug, error, info, warn}; use tree_hash::TreeHash; use types::beacon_block_body::KzgCommitments; use types::builder_bid::BuilderBid; @@ -423,7 +424,6 @@ struct Inner { proposers: RwLock>, executor: TaskExecutor, payload_cache: PayloadCache, - log: Logger, /// Track whether the last `newPayload` call errored. /// /// This is used *only* in the informational sync status endpoint, so that a VC using this @@ -467,7 +467,7 @@ pub struct ExecutionLayer { impl ExecutionLayer { /// Instantiate `Self` with an Execution engine specified in `Config`, using JSON-RPC via HTTP. - pub fn from_config(config: Config, executor: TaskExecutor, log: Logger) -> Result { + pub fn from_config(config: Config, executor: TaskExecutor) -> Result { let Config { execution_endpoint: url, builder_url, @@ -501,7 +501,7 @@ impl ExecutionLayer { .map_err(Error::InvalidJWTSecret) } else { // Create a new file and write a randomly generated secret to it if file does not exist - warn!(log, "No JWT found on disk. Generating"; "path" => %secret_file.display()); + warn!(path = %secret_file.display(),"No JWT found on disk. Generating"); std::fs::File::options() .write(true) .create_new(true) @@ -518,10 +518,10 @@ impl ExecutionLayer { let engine: Engine = { let auth = Auth::new(jwt_key, jwt_id, jwt_version); - debug!(log, "Loaded execution endpoint"; "endpoint" => %execution_url, "jwt_path" => ?secret_file.as_path()); + debug!(endpoint = %execution_url, jwt_path = ?secret_file.as_path(),"Loaded execution endpoint"); let api = HttpJsonRpc::new_with_auth(execution_url, auth, execution_timeout_multiplier) .map_err(Error::ApiError)?; - Engine::new(api, executor.clone(), &log) + Engine::new(api, executor.clone()) }; let inner = Inner { @@ -534,7 +534,6 @@ impl ExecutionLayer { execution_blocks: Mutex::new(LruCache::new(EXECUTION_BLOCKS_LRU_CACHE_SIZE)), executor, payload_cache: PayloadCache::default(), - log, last_new_payload_errored: RwLock::new(false), }; @@ -581,11 +580,10 @@ impl ExecutionLayer { ) .map_err(Error::Builder)?; info!( - self.log(), - "Using external block builder"; - "builder_url" => ?builder_url, - "local_user_agent" => builder_client.get_user_agent(), - "ssz_disabled" => disable_ssz + ?builder_url, + local_user_agent = builder_client.get_user_agent(), + ssz_disabled = disable_ssz, + "Using external block builder" ); self.inner.builder.swap(Some(Arc::new(builder_client))); Ok(()) @@ -599,13 +597,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 @@ -656,10 +648,6 @@ impl ExecutionLayer { &self.inner.proposers } - fn log(&self) -> &Logger { - &self.inner.log - } - pub async fn execution_engine_forkchoice_lock(&self) -> MutexGuard<'_, ()> { self.inner.execution_engine_forkchoice_lock.lock().await } @@ -717,16 +705,15 @@ impl ExecutionLayer { .await .map_err(|e| { error!( - el.log(), - "Failed to clean proposer preparation cache"; - "error" => format!("{:?}", e) + error = ?e, + "Failed to clean proposer preparation cache" ) }) .unwrap_or(()), - None => error!(el.log(), "Failed to get current epoch from slot clock"), + None => error!("Failed to get current epoch from slot clock"), } } else { - error!(el.log(), "Failed to read slot clock"); + error!("Failed to read slot clock"); // If we can't read the slot clock, just wait another slot and retry. sleep(slot_clock.slot_duration()).await; } @@ -866,12 +853,11 @@ impl ExecutionLayer { } else { // If there is no user-provided fee recipient, use a junk value and complain loudly. crit!( - self.log(), - "Fee recipient unknown"; - "msg" => "the suggested_fee_recipient was unknown during block production. \ + msg = "the suggested_fee_recipient was unknown during block production. \ a junk address was used, rewards were lost! \ check the --suggested-fee-recipient flag and VC configuration.", - "proposer_index" => ?proposer_index + ?proposer_index, + "Fee recipient unknown" ); Address::from_slice(&DEFAULT_SUGGESTED_FEE_RECIPIENT) @@ -988,11 +974,10 @@ impl ExecutionLayer { let parent_hash = payload_parameters.parent_hash; info!( - self.log(), - "Requesting blinded header from connected builder"; - "slot" => ?slot, - "pubkey" => ?pubkey, - "parent_hash" => ?parent_hash, + ?slot, + ?pubkey, + ?parent_hash, + "Requesting blinded header from connected builder" ); // Wait for the builder *and* local EL to produce a payload (or return an error). @@ -1013,20 +998,19 @@ impl ExecutionLayer { ); info!( - self.log(), - "Requested blinded execution payload"; - "relay_fee_recipient" => match &relay_result { + relay_fee_recipient = match &relay_result { Ok(Some(r)) => format!("{:?}", r.data.message.header().fee_recipient()), Ok(None) => "empty response".to_string(), Err(_) => "request failed".to_string(), }, - "relay_response_ms" => relay_duration.as_millis(), - "local_fee_recipient" => match &local_result { + relay_response_ms = relay_duration.as_millis(), + local_fee_recipient = match &local_result { Ok(get_payload_response) => format!("{:?}", get_payload_response.fee_recipient()), - Err(_) => "request failed".to_string() + Err(_) => "request failed".to_string(), }, - "local_response_ms" => local_duration.as_millis(), - "parent_hash" => ?parent_hash, + local_response_ms = local_duration.as_millis(), + ?parent_hash, + "Requested blinded execution payload" ); (relay_result, local_result) @@ -1053,24 +1037,21 @@ impl ExecutionLayer { // chain is unhealthy, gotta use local payload match builder_params.chain_health { ChainHealth::Unhealthy(condition) => info!( - self.log(), - "Chain is unhealthy, using local payload"; - "info" => "this helps protect the network. the --builder-fallback flags \ - can adjust the expected health conditions.", - "failed_condition" => ?condition + info = "this helps protect the network. the --builder-fallback flags \ + can adjust the expected health conditions.", + failed_condition = ?condition, + "Chain is unhealthy, using local payload" ), // Intentional no-op, so we never attempt builder API proposals pre-merge. ChainHealth::PreMerge => (), ChainHealth::Optimistic => info!( - self.log(), - "Chain is optimistic; can't build payload"; - "info" => "the local execution engine is syncing and the builder network \ - cannot safely be used - unable to propose block" - ), - ChainHealth::Healthy => crit!( - self.log(), - "got healthy but also not healthy.. this shouldn't happen!" + info = "the local execution engine is syncing and the builder network \ + cannot safely be used - unable to propose block", + "Chain is optimistic; can't build payload" ), + ChainHealth::Healthy => { + crit!("got healthy but also not healthy.. this shouldn't happen!") + } } return self .get_full_payload_caching(payload_parameters) @@ -1087,12 +1068,11 @@ impl ExecutionLayer { match (relay_result, local_result) { (Err(e), Ok(local)) => { warn!( - self.log(), - "Builder error when requesting payload"; - "info" => "falling back to local execution client", - "relay_error" => ?e, - "local_block_hash" => ?local.block_hash(), - "parent_hash" => ?parent_hash, + info = "falling back to local execution client", + relay_error = ?e, + local_block_hash = ?local.block_hash(), + ?parent_hash, + "Builder error when requesting payload" ); Ok(ProvenancedPayload::Local(BlockProposalContentsType::Full( local.try_into()?, @@ -1100,11 +1080,10 @@ impl ExecutionLayer { } (Ok(None), Ok(local)) => { info!( - self.log(), - "Builder did not return a payload"; - "info" => "falling back to local execution client", - "local_block_hash" => ?local.block_hash(), - "parent_hash" => ?parent_hash, + info = "falling back to local execution client", + local_block_hash=?local.block_hash(), + ?parent_hash, + "Builder did not return a payload" ); Ok(ProvenancedPayload::Local(BlockProposalContentsType::Full( local.try_into()?, @@ -1112,24 +1091,22 @@ impl ExecutionLayer { } (Err(relay_error), Err(local_error)) => { crit!( - self.log(), - "Unable to produce execution payload"; - "info" => "the local EL and builder both failed - unable to propose block", - "relay_error" => ?relay_error, - "local_error" => ?local_error, - "parent_hash" => ?parent_hash, + info = "the local EL and builder both failed - unable to propose block", + ?relay_error, + ?local_error, + ?parent_hash, + "Unable to produce execution payload" ); Err(Error::CannotProduceHeader) } (Ok(None), Err(local_error)) => { crit!( - self.log(), - "Unable to produce execution payload"; - "info" => "the local EL failed and the builder returned nothing - \ - the block proposal will be missed", - "local_error" => ?local_error, - "parent_hash" => ?parent_hash, + info = "the local EL failed and the builder returned nothing - \ + the block proposal will be missed", + ?local_error, + ?parent_hash, + "Unable to produce execution payload" ); Err(Error::CannotProduceHeader) @@ -1138,11 +1115,10 @@ impl ExecutionLayer { let header = &relay.data.message.header(); info!( - self.log(), - "Received local and builder payloads"; - "relay_block_hash" => ?header.block_hash(), - "local_block_hash" => ?local.block_hash(), - "parent_hash" => ?parent_hash, + relay_block_hash = ?header.block_hash(), + local_block_hash=?local.block_hash(), + ?parent_hash, + "Received local and builder payloads" ); // check relay payload validity @@ -1155,12 +1131,11 @@ impl ExecutionLayer { &[reason.as_ref().as_ref()], ); warn!( - self.log(), - "Builder returned invalid payload"; - "info" => "using local payload", - "reason" => %reason, - "relay_block_hash" => ?header.block_hash(), - "parent_hash" => ?parent_hash, + info = "using local payload", + %reason, + relay_block_hash = ?header.block_hash(), + ?parent_hash, + "Builder returned invalid payload" ); return Ok(ProvenancedPayload::Local(BlockProposalContentsType::Full( local.try_into()?, @@ -1179,12 +1154,11 @@ impl ExecutionLayer { if local_value >= boosted_relay_value { info!( - self.log(), - "Local block is more profitable than relay block"; - "local_block_value" => %local_value, - "relay_value" => %relay_value, - "boosted_relay_value" => %boosted_relay_value, - "builder_boost_factor" => ?builder_boost_factor, + %local_value, + %relay_value, + %boosted_relay_value, + ?builder_boost_factor, + "Local block is more profitable than relay block" ); return Ok(ProvenancedPayload::Local(BlockProposalContentsType::Full( local.try_into()?, @@ -1193,10 +1167,9 @@ impl ExecutionLayer { if local.should_override_builder().unwrap_or(false) { info!( - self.log(), - "Using local payload because execution engine suggested we ignore builder payload"; - "local_block_value" => %local_value, - "relay_value" => %relay_value + %local_value, + %relay_value, + "Using local payload because execution engine suggested we ignore builder payload" ); return Ok(ProvenancedPayload::Local(BlockProposalContentsType::Full( local.try_into()?, @@ -1204,12 +1177,11 @@ impl ExecutionLayer { } info!( - self.log(), - "Relay block is more profitable than local block"; - "local_block_value" => %local_value, - "relay_value" => %relay_value, - "boosted_relay_value" => %boosted_relay_value, - "builder_boost_factor" => ?builder_boost_factor + %local_value, + %relay_value, + %boosted_relay_value, + ?builder_boost_factor, + "Relay block is more profitable than local block" ); Ok(ProvenancedPayload::try_from(relay.data.message)?) @@ -1218,11 +1190,10 @@ impl ExecutionLayer { let header = &relay.data.message.header(); info!( - self.log(), - "Received builder payload with local error"; - "relay_block_hash" => ?header.block_hash(), - "local_error" => ?local_error, - "parent_hash" => ?parent_hash, + relay_block_hash = ?header.block_hash(), + ?local_error, + ?parent_hash, + "Received builder payload with local error" ); match verify_builder_bid(&relay, payload_parameters, None, spec) { @@ -1233,12 +1204,11 @@ impl ExecutionLayer { &[reason.as_ref().as_ref()], ); crit!( - self.log(), - "Builder returned invalid payload"; - "info" => "no local payload either - unable to propose block", - "reason" => %reason, - "relay_block_hash" => ?header.block_hash(), - "parent_hash" => ?parent_hash, + info = "no local payload either - unable to propose block", + %reason, + relay_block_hash = ?header.block_hash(), + ?parent_hash, + "Builder returned invalid payload" ); Err(Error::CannotProduceHeader) } @@ -1305,7 +1275,6 @@ impl ExecutionLayer { .notify_forkchoice_updated( fork_choice_state, Some(payload_attributes.clone()), - self.log(), ) .await?; @@ -1313,12 +1282,11 @@ impl ExecutionLayer { Some(payload_id) => payload_id, None => { error!( - self.log(), - "Exec engine unable to produce payload"; - "msg" => "No payload ID, the engine is likely syncing. \ - This has the potential to cause a missed block proposal.", - "status" => ?response.payload_status - ); + msg = "No payload ID, the engine is likely syncing. \ + This has the potential to cause a missed block proposal.", + status = ?response.payload_status, + "Exec engine unable to produce payload" + ); return Err(ApiError::PayloadIdUnavailable); } } @@ -1326,36 +1294,44 @@ impl ExecutionLayer { let payload_response = async { debug!( - self.log(), - "Issuing engine_getPayload"; - "suggested_fee_recipient" => ?payload_attributes.suggested_fee_recipient(), - "prev_randao" => ?payload_attributes.prev_randao(), - "timestamp" => payload_attributes.timestamp(), - "parent_hash" => ?parent_hash, + suggested_fee_recipient = ?payload_attributes.suggested_fee_recipient(), + prev_randao = ?payload_attributes.prev_randao(), + timestamp = payload_attributes.timestamp(), + ?parent_hash, + "Issuing engine_getPayload" ); let _timer = metrics::start_timer_vec( &metrics::EXECUTION_LAYER_REQUEST_TIMES, &[metrics::GET_PAYLOAD], ); engine.api.get_payload::(current_fork, payload_id).await - }.await?; + } + .await?; - if payload_response.execution_payload_ref().fee_recipient() != payload_attributes.suggested_fee_recipient() { + if payload_response.execution_payload_ref().fee_recipient() + != payload_attributes.suggested_fee_recipient() + { error!( - self.log(), - "Inconsistent fee recipient"; - "msg" => "The fee recipient returned from the Execution Engine differs \ + msg = "The fee recipient returned from the Execution Engine differs \ from the suggested_fee_recipient set on the beacon node. This could \ indicate that fees are being diverted to another address. Please \ ensure that the value of suggested_fee_recipient is set correctly and \ that the Execution Engine is trusted.", - "fee_recipient" => ?payload_response.execution_payload_ref().fee_recipient(), - "suggested_fee_recipient" => ?payload_attributes.suggested_fee_recipient(), + fee_recipient = ?payload_response.execution_payload_ref().fee_recipient(), + suggested_fee_recipient = ?payload_attributes.suggested_fee_recipient(), + "Inconsistent fee recipient" ); } - if cache_fn(self, (payload_response.execution_payload_ref(), payload_response.blobs_bundle().ok())).is_some() { + if cache_fn( + self, + ( + payload_response.execution_payload_ref(), + payload_response.blobs_bundle().ok(), + ), + ) + .is_some() + { warn!( - self.log(), "Duplicate payload cached, this might indicate redundant proposal \ attempts." ); @@ -1395,18 +1371,17 @@ impl ExecutionLayer { &["new_payload", status_str], ); debug!( - self.log(), - "Processed engine_newPayload"; - "status" => status_str, - "parent_hash" => ?parent_hash, - "block_hash" => ?block_hash, - "block_number" => block_number, - "response_time_ms" => timer.elapsed().as_millis() + status = status_str, + ?parent_hash, + ?block_hash, + block_number, + response_time_ms = timer.elapsed().as_millis(), + "Processed engine_newPayload" ); } *self.inner.last_new_payload_errored.write().await = result.is_err(); - process_payload_status(block_hash, result, self.log()) + process_payload_status(block_hash, result) .map_err(Box::new) .map_err(Error::EngineError) } @@ -1463,12 +1438,11 @@ impl ExecutionLayer { let proposer = self.proposers().read().await.get(&proposers_key).cloned()?; debug!( - self.log(), - "Beacon proposer found"; - "payload_attributes" => ?proposer.payload_attributes, - "head_block_root" => ?head_block_root, - "slot" => current_slot, - "validator_index" => proposer.validator_index, + payload_attributes = ?proposer.payload_attributes, + ?head_block_root, + slot = %current_slot, + validator_index = proposer.validator_index, + "Beacon proposer found" ); Some(proposer.payload_attributes) @@ -1489,13 +1463,12 @@ impl ExecutionLayer { ); debug!( - self.log(), - "Issuing engine_forkchoiceUpdated"; - "finalized_block_hash" => ?finalized_block_hash, - "justified_block_hash" => ?justified_block_hash, - "head_block_hash" => ?head_block_hash, - "head_block_root" => ?head_block_root, - "current_slot" => current_slot, + ?finalized_block_hash, + ?justified_block_hash, + ?head_block_hash, + ?head_block_root, + ?current_slot, + "Issuing engine_forkchoiceUpdated" ); let next_slot = current_slot + 1; @@ -1511,12 +1484,7 @@ impl ExecutionLayer { lookahead, ); } else { - debug!( - self.log(), - "Late payload attributes"; - "timestamp" => ?timestamp, - "now" => ?now, - ) + debug!(?timestamp, ?now, "Late payload attributes") } } } @@ -1535,7 +1503,7 @@ impl ExecutionLayer { .engine() .request(|engine| async move { engine - .notify_forkchoice_updated(forkchoice_state, payload_attributes, self.log()) + .notify_forkchoice_updated(forkchoice_state, payload_attributes) .await }) .await; @@ -1550,7 +1518,6 @@ impl ExecutionLayer { process_payload_status( head_block_hash, result.map(|response| response.payload_status), - self.log(), ) .map_err(Box::new) .map_err(Error::EngineError) @@ -1647,11 +1614,10 @@ impl ExecutionLayer { if let Some(hash) = &hash_opt { info!( - self.log(), - "Found terminal block hash"; - "terminal_block_hash_override" => ?spec.terminal_block_hash, - "terminal_total_difficulty" => ?spec.terminal_total_difficulty, - "block_hash" => ?hash, + terminal_block_hash_override = ?spec.terminal_block_hash, + terminal_total_difficulty = ?spec.terminal_total_difficulty, + block_hash = ?hash, + "Found terminal block hash" ); } @@ -1875,7 +1841,7 @@ impl ExecutionLayer { } } - pub async fn get_blobs( + pub async fn get_blobs_v1( &self, query: Vec, ) -> Result>>, Error> { @@ -1883,7 +1849,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) @@ -1908,21 +1891,16 @@ impl ExecutionLayer { block_root: Hash256, block: &SignedBlindedBeaconBlock, ) -> Result, Error> { - debug!( - self.log(), - "Sending block to builder"; - "root" => ?block_root, - ); + debug!(?block_root, "Sending block to builder"); if let Some(builder) = self.builder() { let (payload_result, duration) = timed_future(metrics::POST_BLINDED_PAYLOAD_BUILDER, async { let ssz_enabled = builder.is_ssz_available(); debug!( - self.log(), - "Calling submit_blinded_block on builder"; - "block_root" => ?block_root, - "ssz" => ssz_enabled + ?block_root, + ssz = ssz_enabled, + "Calling submit_blinded_block on builder" ); if ssz_enabled { builder @@ -1947,13 +1925,12 @@ impl ExecutionLayer { ); let payload = unblinded_response.payload_ref(); info!( - self.log(), - "Builder successfully revealed payload"; - "relay_response_ms" => duration.as_millis(), - "block_root" => ?block_root, - "fee_recipient" => ?payload.fee_recipient(), - "block_hash" => ?payload.block_hash(), - "parent_hash" => ?payload.parent_hash() + relay_response_ms = duration.as_millis(), + ?block_root, + fee_recipient = ?payload.fee_recipient(), + block_hash = ?payload.block_hash(), + parent_hash = ?payload.parent_hash(), + "Builder successfully revealed payload" ) } Err(e) => { @@ -1962,17 +1939,16 @@ impl ExecutionLayer { &[metrics::FAILURE], ); warn!( - self.log(), - "Builder failed to reveal payload"; - "info" => "this is common behaviour for some builders and may not indicate an issue", - "error" => ?e, - "relay_response_ms" => duration.as_millis(), - "block_root" => ?block_root, - "parent_hash" => ?block + info = "this is common behaviour for some builders and may not indicate an issue", + error = ?e, + relay_response_ms = duration.as_millis(), + ?block_root, + parent_hash = ?block .message() .execution_payload() .map(|payload| format!("{}", payload.parent_hash())) - .unwrap_or_else(|_| "unknown".to_string()) + .unwrap_or_else(|_| "unknown".to_string()), + "Builder failed to reveal payload" ) } } diff --git a/beacon_node/execution_layer/src/payload_status.rs b/beacon_node/execution_layer/src/payload_status.rs index cf0be8ed0d8..bbfd30239de 100644 --- a/beacon_node/execution_layer/src/payload_status.rs +++ b/beacon_node/execution_layer/src/payload_status.rs @@ -1,6 +1,6 @@ use crate::engine_api::{Error as ApiError, PayloadStatusV1, PayloadStatusV1Status}; use crate::engines::EngineError; -use slog::{warn, Logger}; +use tracing::warn; use types::ExecutionBlockHash; /// Provides a simpler, easier to parse version of `PayloadStatusV1` for upstream users. @@ -26,15 +26,10 @@ pub enum PayloadStatus { pub fn process_payload_status( head_block_hash: ExecutionBlockHash, status: Result, - log: &Logger, ) -> Result { match status { Err(error) => { - warn!( - log, - "Error whilst processing payload status"; - "error" => ?error, - ); + warn!(?error, "Error whilst processing payload status"); Err(error) } Ok(response) => match &response.status { @@ -66,10 +61,9 @@ pub fn process_payload_status( // warning here. if response.latest_valid_hash.is_some() { warn!( - log, - "Malformed response from execution engine"; - "msg" => "expected a null latest_valid_hash", - "status" => ?response.status + msg = "expected a null latest_valid_hash", + status = ?response.status, + "Malformed response from execution engine" ) } @@ -82,10 +76,9 @@ pub fn process_payload_status( // warning here. if response.latest_valid_hash.is_some() { warn!( - log, - "Malformed response from execution engine"; - "msg" => "expected a null latest_valid_hash", - "status" => ?response.status + msg = "expected a null latest_valid_hash", + status = ?response.status, + "Malformed response from execution engine" ) } @@ -96,10 +89,9 @@ pub fn process_payload_status( // warning here. if response.latest_valid_hash.is_some() { warn!( - log, - "Malformed response from execution engine"; - "msg" => "expected a null latest_valid_hash", - "status" => ?response.status + msg = "expected a null latest_valid_hash", + status = ?response.status, + "Malformed response from execution engine" ) } 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 81fb9bd7b8d..b057abe8877 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 @@ -20,13 +20,14 @@ use tree_hash_derive::TreeHash; use types::{ Blob, ChainSpec, EthSpec, ExecutionBlockHash, ExecutionPayload, ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb, ExecutionPayloadElectra, ExecutionPayloadFulu, - ExecutionPayloadHeader, FixedBytesExtended, ForkName, Hash256, Transaction, Transactions, - Uint256, + ExecutionPayloadHeader, FixedBytesExtended, 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; @@ -697,15 +698,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() @@ -721,7 +720,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, @@ -745,32 +745,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 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 tx = static_valid_tx::() + .map_err(|e| format!("error creating valid tx SSZ bytes: {:?}", e))?; + let transactions = vec![tx; n_blobs]; + + let bundle = if fork_name.fulu_enabled() { + let (kzg_commitment, kzg_proofs, blob) = load_test_blobs_bundle_v2::()?; + BlobsBundle { + commitments: vec![kzg_commitment; n_blobs].into(), + 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())) } @@ -905,7 +929,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] @@ -974,20 +998,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:?}"))?; @@ -995,6 +1027,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 00000000000..e57096c0766 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 d727d2c1590..70c21afed45 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -383,9 +383,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), @@ -451,22 +450,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 f07ee7ac6f8..87ea8642be5 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_builder.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_builder.rs @@ -13,7 +13,6 @@ use eth2::{ use fork_choice::ForkchoiceUpdateParameters; use parking_lot::RwLock; use sensitive_url::SensitiveUrl; -use slog::{debug, error, info, warn, Logger}; use ssz::Encode; use std::collections::HashMap; use std::fmt::Debug; @@ -24,6 +23,7 @@ use std::time::Duration; use task_executor::TaskExecutor; use tempfile::NamedTempFile; use tokio_stream::StreamExt; +use tracing::{debug, error, info, warn}; use tree_hash::TreeHash; use types::builder_bid::{ BuilderBid, BuilderBidBellatrix, BuilderBidCapella, BuilderBidDeneb, BuilderBidElectra, @@ -309,7 +309,6 @@ pub struct MockBuilder { max_bid: bool, /// A cache that stores the proposers index for a given epoch proposers_cache: Arc>>>, - log: Logger, } impl MockBuilder { @@ -331,8 +330,7 @@ impl MockBuilder { ..Default::default() }; - let el = - ExecutionLayer::from_config(config, executor.clone(), executor.log().clone()).unwrap(); + let el = ExecutionLayer::from_config(config, executor.clone()).unwrap(); let builder = MockBuilder::new( el, @@ -342,7 +340,6 @@ impl MockBuilder { false, spec, None, - executor.log().clone(), ); let host: Ipv4Addr = Ipv4Addr::LOCALHOST; let port = 0; @@ -359,16 +356,12 @@ impl MockBuilder { max_bid: bool, spec: Arc, sk: Option<&[u8]>, - log: Logger, ) -> Self { let builder_sk = if let Some(sk_bytes) = sk { match SecretKey::deserialize(sk_bytes) { Ok(sk) => sk, Err(_) => { - error!( - log, - "Invalid sk_bytes provided, generating random secret key" - ); + error!("Invalid sk_bytes provided, generating random secret key"); SecretKey::random() } } @@ -390,7 +383,6 @@ impl MockBuilder { apply_operations, max_bid, genesis_time: None, - log, } } @@ -425,18 +417,13 @@ impl MockBuilder { &self, registrations: Vec, ) -> Result<(), String> { - info!( - self.log, - "Registering validators"; - "count" => registrations.len(), - ); + info!(count = registrations.len(), "Registering validators"); for registration in registrations { if !registration.verify_signature(&self.spec) { error!( - self.log, - "Failed to register validator"; - "error" => "invalid signature", - "validator" => %registration.message.pubkey + error = "invalid signature", + validator = %registration.message.pubkey, + "Failed to register validator" ); return Err("invalid signature".to_string()); } @@ -472,9 +459,8 @@ impl MockBuilder { } }; info!( - self.log, - "Submitting blinded beacon block to builder"; - "block_hash" => %root + block_hash = %root, + "Submitting blinded beacon block to builder" ); let payload = self .el @@ -486,10 +472,9 @@ impl MockBuilder { .try_into_full_block(Some(payload.clone())) .ok_or("Internal error, just provided a payload")?; debug!( - self.log, - "Got full payload, sending to local beacon node for propagation"; - "txs_count" => payload.transactions().len(), - "blob_count" => blobs.as_ref().map(|b| b.commitments.len()) + txs_count = payload.transactions().len(), + blob_count = blobs.as_ref().map(|b| b.commitments.len()), + "Got full payload, sending to local beacon node for propagation" ); let publish_block_request = PublishBlockRequest::new( Arc::new(full_block), @@ -508,7 +493,7 @@ impl MockBuilder { parent_hash: ExecutionBlockHash, pubkey: PublicKeyBytes, ) -> Result, String> { - info!(self.log, "In get_header"); + info!("In get_header"); // Check if the pubkey has registered with the builder if required if self.validate_pubkey && !self.val_registration_cache.read().contains_key(&pubkey) { return Err("validator not registered with builder".to_string()); @@ -521,15 +506,12 @@ impl MockBuilder { let payload_parameters = match payload_parameters { Some(params) => params, None => { - warn!( - self.log, - "Payload params not cached for parent_hash {}", parent_hash - ); + warn!("Payload params not cached for parent_hash {}", parent_hash); self.get_payload_params(slot, None, pubkey, None).await? } }; - info!(self.log, "Got payload params"); + info!("Got payload params"); let fork = self.fork_name_at_slot(slot); let payload_response_type = self @@ -545,7 +527,7 @@ impl MockBuilder { .await .map_err(|e| format!("couldn't get payload {:?}", e))?; - info!(self.log, "Got payload message, fork {}", fork); + info!("Got payload message, fork {}", fork); let mut message = match payload_response_type { crate::GetPayloadResponseType::Full(payload_response) => { @@ -564,7 +546,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(), @@ -576,7 +558,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(), @@ -588,7 +570,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(), @@ -616,10 +598,10 @@ impl MockBuilder { }; if self.apply_operations { - info!(self.log, "Applying operations"); + info!("Applying operations"); self.apply_operations(&mut message); } - info!(self.log, "Signing builder message"); + info!("Signing builder message"); let mut signature = message.sign_builder_message(&self.builder_sk, &self.spec); @@ -627,7 +609,7 @@ impl MockBuilder { signature = Signature::empty(); }; let signed_bid = SignedBuilderBid { message, signature }; - info!(self.log, "Builder bid {:?}", &signed_bid.message.value()); + info!("Builder bid {:?}", &signed_bid.message.value()); Ok(signed_bid) } @@ -648,10 +630,7 @@ impl MockBuilder { /// Prepare the execution layer for payload creation every slot for the correct /// proposer index pub async fn prepare_execution_layer(&self) -> Result<(), String> { - info!( - self.log, - "Starting a task to prepare the execution layer"; - ); + info!("Starting a task to prepare the execution layer"); let mut head_event_stream = self .beacon_client .get_events::(&[EventTopic::Head]) @@ -662,9 +641,8 @@ impl MockBuilder { match event { EventKind::Head(head) => { debug!( - self.log, - "Got a new head event"; - "block_hash" => %head.block + block_hash = %head.block, + "Got a new head event" ); let next_slot = head.slot + 1; // Find the next proposer index from the cached data or through a beacon api call @@ -712,9 +690,8 @@ impl MockBuilder { } e => { warn!( - self.log, - "Got an unexpected event"; - "event" => %e.topic_name() + event = %e.topic_name(), + "Got an unexpected event" ); } } @@ -812,7 +789,6 @@ impl MockBuilder { ), None => { warn!( - self.log, "Validator not registered {}, using default fee recipient and gas limits", pubkey ); diff --git a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs index f45bfda9ffa..cbe5e3ae989 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs @@ -76,8 +76,7 @@ impl MockExecutionLayer { suggested_fee_recipient: Some(Address::repeat_byte(42)), ..Default::default() }; - let el = - ExecutionLayer::from_config(config, executor.clone(), executor.log().clone()).unwrap(); + let el = ExecutionLayer::from_config(config, executor.clone()).unwrap(); Self { server, diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index 75ff4358865..245aa71a157 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -9,11 +9,11 @@ use bytes::Bytes; use execution_block_generator::PoWBlock; use handle_rpc::handle_rpc; use kzg::Kzg; -use logging::test_logger; + +use logging::create_test_tracing_subscriber; use parking_lot::{Mutex, RwLock, RwLockWriteGuard}; use serde::{Deserialize, Serialize}; use serde_json::json; -use slog::{info, Logger}; use std::collections::HashMap; use std::convert::Infallible; use std::future::Future; @@ -21,6 +21,7 @@ use std::marker::PhantomData; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; use std::sync::{Arc, LazyLock}; use tokio::{runtime, sync::oneshot}; +use tracing::info; use types::{ChainSpec, EthSpec, ExecutionBlockHash, Uint256}; use warp::{http::StatusCode, Filter, Rejection}; @@ -57,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, }; pub static DEFAULT_CLIENT_VERSION: LazyLock = @@ -133,6 +135,7 @@ impl MockServer { spec: Arc, kzg: Option>, ) -> Self { + create_test_tracing_subscriber(); let MockExecutionConfig { jwt_key, terminal_difficulty, @@ -161,7 +164,6 @@ impl MockServer { let ctx: Arc> = Arc::new(Context { config: server_config, jwt_key, - log: test_logger(), last_echo_request: last_echo_request.clone(), execution_block_generator: RwLock::new(execution_block_generator), previous_request: <_>::default(), @@ -533,7 +535,7 @@ impl warp::reject::Reject for AuthError {} pub struct Context { pub config: Config, pub jwt_key: JwtKey, - pub log: Logger, + pub last_echo_request: Arc>>, pub execution_block_generator: RwLock>, pub preloaded_responses: Arc>>, @@ -671,7 +673,6 @@ pub fn serve( shutdown: impl Future + Send + Sync + 'static, ) -> Result<(SocketAddr, impl Future), Error> { let config = &ctx.config; - let log = ctx.log.clone(); let inner_ctx = ctx.clone(); let ctx_filter = warp::any().map(move || inner_ctx.clone()); @@ -751,9 +752,8 @@ pub fn serve( )?; info!( - log, - "Metrics HTTP server started"; - "listen_address" => listening_socket.to_string(), + listen_address = listening_socket.to_string(), + "Metrics HTTP server started" ); Ok((listening_socket, server)) diff --git a/beacon_node/genesis/Cargo.toml b/beacon_node/genesis/Cargo.toml index eeca393947d..6ba8998a01d 100644 --- a/beacon_node/genesis/Cargo.toml +++ b/beacon_node/genesis/Cargo.toml @@ -6,6 +6,7 @@ edition = { workspace = true } [dev-dependencies] eth1_test_rig = { workspace = true } +logging = { workspace = true } sensitive_url = { workspace = true } [dependencies] @@ -17,8 +18,8 @@ futures = { workspace = true } int_to_bytes = { workspace = true } merkle_proof = { workspace = true } rayon = { workspace = true } -slog = { workspace = true } state_processing = { workspace = true } tokio = { workspace = true } +tracing = { workspace = true } tree_hash = { workspace = true } types = { workspace = true } diff --git a/beacon_node/genesis/src/eth1_genesis_service.rs b/beacon_node/genesis/src/eth1_genesis_service.rs index 6e8f38627ca..dede96512c0 100644 --- a/beacon_node/genesis/src/eth1_genesis_service.rs +++ b/beacon_node/genesis/src/eth1_genesis_service.rs @@ -2,7 +2,6 @@ pub use crate::common::genesis_deposits; pub use eth1::Config as Eth1Config; use eth1::{DepositLog, Eth1Block, Service as Eth1Service}; -use slog::{debug, error, info, trace, Logger}; use state_processing::{ eth2_genesis_time, initialize_beacon_state_from_eth1, is_valid_genesis_state, per_block_processing::process_operations::apply_deposit, process_activations, @@ -13,6 +12,7 @@ use std::sync::{ }; use std::time::Duration; use tokio::time::sleep; +use tracing::{debug, error, info, trace}; use types::{BeaconState, ChainSpec, Deposit, Eth1Data, EthSpec, FixedBytesExtended, Hash256}; /// The number of blocks that are pulled per request whilst waiting for genesis. @@ -43,7 +43,7 @@ impl Eth1GenesisService { /// Creates a new service. Does not attempt to connect to the Eth1 node. /// /// Modifies the given `config` to make it more suitable to the task of listening to genesis. - pub fn new(config: Eth1Config, log: Logger, spec: Arc) -> Result { + pub fn new(config: Eth1Config, spec: Arc) -> Result { let config = Eth1Config { // Truncating the block cache makes searching for genesis more // complicated. @@ -65,7 +65,7 @@ impl Eth1GenesisService { }; Ok(Self { - eth1_service: Eth1Service::new(config, log, spec) + eth1_service: Eth1Service::new(config, spec) .map_err(|e| format!("Failed to create eth1 service: {:?}", e))?, stats: Arc::new(Statistics { highest_processed_block: AtomicU64::new(0), @@ -103,15 +103,11 @@ impl Eth1GenesisService { ) -> Result, String> { let eth1_service = &self.eth1_service; let spec = eth1_service.chain_spec(); - let log = ð1_service.log; let mut sync_blocks = false; let mut highest_processed_block = None; - info!( - log, - "Importing eth1 deposit logs"; - ); + info!("Importing eth1 deposit logs"); loop { let update_result = eth1_service @@ -120,11 +116,7 @@ impl Eth1GenesisService { .map_err(|e| format!("{:?}", e)); if let Err(e) = update_result { - error!( - log, - "Failed to update eth1 deposit cache"; - "error" => e - ) + error!(error = e, "Failed to update eth1 deposit cache") } self.stats @@ -135,19 +127,15 @@ impl Eth1GenesisService { if let Some(viable_eth1_block) = self .first_candidate_eth1_block(spec.min_genesis_active_validator_count as usize) { - info!( - log, - "Importing eth1 blocks"; - ); + info!("Importing eth1 blocks"); self.eth1_service.set_lowest_cached_block(viable_eth1_block); sync_blocks = true } else { info!( - log, - "Waiting for more deposits"; - "min_genesis_active_validators" => spec.min_genesis_active_validator_count, - "total_deposits" => eth1_service.deposit_cache_len(), - "valid_deposits" => eth1_service.get_raw_valid_signature_count(), + min_genesis_active_validators = spec.min_genesis_active_validator_count, + total_deposits = eth1_service.deposit_cache_len(), + valid_deposits = eth1_service.get_raw_valid_signature_count(), + "Waiting for more deposits" ); sleep(update_interval).await; @@ -160,19 +148,17 @@ impl Eth1GenesisService { let blocks_imported = match eth1_service.update_block_cache(None).await { Ok(outcome) => { debug!( - log, - "Imported eth1 blocks"; - "latest_block_timestamp" => eth1_service.latest_block_timestamp(), - "cache_head" => eth1_service.highest_safe_block(), - "count" => outcome.blocks_imported, + latest_block_timestamp = eth1_service.latest_block_timestamp(), + cache_head = eth1_service.highest_safe_block(), + count = outcome.blocks_imported, + "Imported eth1 blocks" ); outcome.blocks_imported } Err(e) => { error!( - log, - "Failed to update eth1 block cache"; - "error" => format!("{:?}", e) + error = ?e, + "Failed to update eth1 block cache" ); 0 } @@ -183,13 +169,12 @@ impl Eth1GenesisService { self.scan_new_blocks::(&mut highest_processed_block, spec)? { info!( - log, - "Genesis ceremony complete"; - "genesis_validators" => genesis_state + genesis_validators = genesis_state .get_active_validator_indices(E::genesis_epoch(), spec) .map_err(|e| format!("Genesis validators error: {:?}", e))? .len(), - "genesis_time" => genesis_state.genesis_time(), + genesis_time = genesis_state.genesis_time(), + "Genesis ceremony complete" ); break Ok(genesis_state); } @@ -207,21 +192,19 @@ impl Eth1GenesisService { // Indicate that we are awaiting adequate active validators. if (active_validator_count as u64) < spec.min_genesis_active_validator_count { info!( - log, - "Waiting for more validators"; - "min_genesis_active_validators" => spec.min_genesis_active_validator_count, - "active_validators" => active_validator_count, - "total_deposits" => total_deposit_count, - "valid_deposits" => eth1_service.get_valid_signature_count().unwrap_or(0), + min_genesis_active_validators = spec.min_genesis_active_validator_count, + active_validators = active_validator_count, + total_deposits = total_deposit_count, + valid_deposits = eth1_service.get_valid_signature_count().unwrap_or(0), + "Waiting for more validators" ); } } else { info!( - log, - "Waiting for adequate eth1 timestamp"; - "genesis_delay" => spec.genesis_delay, - "genesis_time" => spec.min_genesis_time, - "latest_eth1_timestamp" => latest_timestamp, + genesis_delay = spec.genesis_delay, + genesis_time = spec.min_genesis_time, + latest_eth1_timestamp = latest_timestamp, + "Waiting for adequate eth1 timestamp" ); } @@ -253,7 +236,6 @@ impl Eth1GenesisService { spec: &ChainSpec, ) -> Result>, String> { let eth1_service = &self.eth1_service; - let log = ð1_service.log; for block in eth1_service.blocks().read().iter() { // It's possible that the block and deposit caches aren't synced. Ignore any blocks @@ -286,12 +268,11 @@ impl Eth1GenesisService { // Ignore any block with an insufficient timestamp. if !timestamp_can_trigger_genesis(block.timestamp, spec)? { trace!( - log, - "Insufficient block timestamp"; - "genesis_delay" => spec.genesis_delay, - "min_genesis_time" => spec.min_genesis_time, - "eth1_block_timestamp" => block.timestamp, - "eth1_block_number" => block.number, + genesis_delay = spec.genesis_delay, + min_genesis_time = spec.min_genesis_time, + eth1_block_timestamp = block.timestamp, + eth1_block_number = block.number, + "Insufficient block timestamp" ); continue; } @@ -301,12 +282,11 @@ impl Eth1GenesisService { .unwrap_or(0); if (valid_signature_count as u64) < spec.min_genesis_active_validator_count { trace!( - log, - "Insufficient valid signatures"; - "genesis_delay" => spec.genesis_delay, - "valid_signature_count" => valid_signature_count, - "min_validator_count" => spec.min_genesis_active_validator_count, - "eth1_block_number" => block.number, + genesis_delay = spec.genesis_delay, + valid_signature_count = valid_signature_count, + min_validator_count = spec.min_genesis_active_validator_count, + eth1_block_number = block.number, + "Insufficient valid signatures" ); continue; } @@ -333,11 +313,11 @@ impl Eth1GenesisService { return Ok(Some(genesis_state)); } else { trace!( - log, - "Insufficient active validators"; - "min_genesis_active_validator_count" => format!("{}", spec.min_genesis_active_validator_count), - "active_validators" => active_validator_count, - "eth1_block_number" => block.number, + min_genesis_active_validator_count = + format!("{}", spec.min_genesis_active_validator_count), + active_validators = active_validator_count, + eth1_block_number = block.number, + "Insufficient active validators" ); } } diff --git a/beacon_node/genesis/tests/tests.rs b/beacon_node/genesis/tests/tests.rs index 6cc7517aa44..b5710e50fd4 100644 --- a/beacon_node/genesis/tests/tests.rs +++ b/beacon_node/genesis/tests/tests.rs @@ -3,6 +3,7 @@ use environment::{Environment, EnvironmentBuilder}; use eth1::{Eth1Endpoint, DEFAULT_CHAIN_ID}; use eth1_test_rig::{AnvilEth1Instance, DelayThenDeposit, Middleware}; use genesis::{Eth1Config, Eth1GenesisService}; +use logging::create_test_tracing_subscriber; use sensitive_url::SensitiveUrl; use state_processing::is_valid_genesis_state; use std::sync::Arc; @@ -12,11 +13,10 @@ use types::{ }; pub fn new_env() -> Environment { + create_test_tracing_subscriber(); EnvironmentBuilder::minimal() .multi_threaded_tokio_runtime() .expect("should start tokio runtime") - .test_logger() - .expect("should start null logger") .build() .expect("should build env") } @@ -24,7 +24,6 @@ pub fn new_env() -> Environment { #[test] fn basic() { let env = new_env(); - let log = env.core_context().log().clone(); let mut spec = (*env.eth2_config().spec).clone(); spec.min_genesis_time = 0; spec.min_genesis_active_validator_count = 8; @@ -55,7 +54,6 @@ fn basic() { block_cache_truncation: None, ..Eth1Config::default() }, - log, spec.clone(), ) .unwrap(); diff --git a/beacon_node/http_api/Cargo.toml b/beacon_node/http_api/Cargo.toml index 7861f030008..afc68ad96d4 100644 --- a/beacon_node/http_api/Cargo.toml +++ b/beacon_node/http_api/Cargo.toml @@ -34,7 +34,6 @@ safe_arith = { workspace = true } sensitive_url = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -slog = { workspace = true } slot_clock = { workspace = true } state_processing = { workspace = true } store = { workspace = true } @@ -43,6 +42,7 @@ system_health = { path = "../../common/system_health" } task_executor = { workspace = true } tokio = { workspace = true } tokio-stream = { workspace = true } +tracing = { workspace = true } tree_hash = { workspace = true } types = { workspace = true } warp = { workspace = true } @@ -50,9 +50,7 @@ warp_utils = { workspace = true } [dev-dependencies] genesis = { workspace = true } -logging = { workspace = true } proto_array = { workspace = true } -serde_json = { workspace = true } [[test]] name = "bn_http_api_tests" diff --git a/beacon_node/http_api/src/block_rewards.rs b/beacon_node/http_api/src/block_rewards.rs index 8466da4de12..29b23e89a79 100644 --- a/beacon_node/http_api/src/block_rewards.rs +++ b/beacon_node/http_api/src/block_rewards.rs @@ -1,10 +1,10 @@ use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes, WhenSlotSkipped}; use eth2::lighthouse::{BlockReward, BlockRewardsQuery}; use lru::LruCache; -use slog::{debug, warn, Logger}; use state_processing::BlockReplayer; use std::num::NonZeroUsize; use std::sync::Arc; +use tracing::{debug, warn}; use types::beacon_block::BlindedBeaconBlock; use types::non_zero_usize::new_non_zero_usize; use warp_utils::reject::{beacon_state_error, custom_bad_request, unhandled_error}; @@ -15,7 +15,6 @@ const STATE_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(2); pub fn get_block_rewards( query: BlockRewardsQuery, chain: Arc>, - log: Logger, ) -> Result, warp::Rejection> { let start_slot = query.start_slot; let end_slot = query.end_slot; @@ -83,12 +82,7 @@ pub fn get_block_rewards( .map_err(unhandled_error)?; if block_replayer.state_root_miss() { - warn!( - log, - "Block reward state root miss"; - "start_slot" => start_slot, - "end_slot" => end_slot, - ); + warn!(%start_slot, %end_slot, "Block reward state root miss"); } drop(block_replayer); @@ -100,7 +94,6 @@ pub fn get_block_rewards( pub fn compute_block_rewards( blocks: Vec>, chain: Arc>, - log: Logger, ) -> Result, warp::Rejection> { let mut block_rewards = Vec::with_capacity(blocks.len()); let mut state_cache = LruCache::new(STATE_CACHE_SIZE); @@ -112,18 +105,16 @@ pub fn compute_block_rewards( // Check LRU cache for a constructed state from a previous iteration. let state = if let Some(state) = state_cache.get(&(parent_root, block.slot())) { debug!( - log, - "Re-using cached state for block rewards"; - "parent_root" => ?parent_root, - "slot" => block.slot(), + ?parent_root, + slot = %block.slot(), + "Re-using cached state for block rewards" ); state } else { debug!( - log, - "Fetching state for block rewards"; - "parent_root" => ?parent_root, - "slot" => block.slot() + ?parent_root, + slot = %block.slot(), + "Fetching state for block rewards" ); let parent_block = chain .get_blinded_block(&parent_root) @@ -156,10 +147,9 @@ pub fn compute_block_rewards( if block_replayer.state_root_miss() { warn!( - log, - "Block reward state root miss"; - "parent_slot" => parent_block.slot(), - "slot" => block.slot(), + parent_slot = %parent_block.slot(), + slot = %block.slot(), + "Block reward state root miss" ); } diff --git a/beacon_node/http_api/src/database.rs b/beacon_node/http_api/src/database.rs index aa8b0e8ffca..8a50ec45b08 100644 --- a/beacon_node/http_api/src/database.rs +++ b/beacon_node/http_api/src/database.rs @@ -1,7 +1,17 @@ use beacon_chain::store::metadata::CURRENT_SCHEMA_VERSION; use beacon_chain::{BeaconChain, BeaconChainTypes}; -use eth2::lighthouse::DatabaseInfo; +use serde::Serialize; use std::sync::Arc; +use store::{AnchorInfo, BlobInfo, Split, StoreConfig}; + +#[derive(Debug, Serialize)] +pub struct DatabaseInfo { + pub schema_version: u64, + pub config: StoreConfig, + pub split: Split, + pub anchor: AnchorInfo, + pub blob_info: BlobInfo, +} pub fn info( chain: Arc>, diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index f101e35ed9a..386d9fe33aa 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -16,6 +16,7 @@ mod builder_states; mod database; mod light_client; mod metrics; +mod peer; mod produce_block; mod proposer_duties; mod publish_attestations; @@ -55,7 +56,7 @@ use health_metrics::observe::Observe; use lighthouse_network::rpc::methods::MetaData; use lighthouse_network::{types::SyncState, Enr, EnrExt, NetworkGlobals, PeerId, PubsubMessage}; use lighthouse_version::version_with_platform; -use logging::SSELoggingComponents; +use logging::{crit, SSELoggingComponents}; use network::{NetworkMessage, NetworkSenders, ValidatorSubscriptionMessage}; use operation_pool::ReceivedPreCapella; use parking_lot::RwLock; @@ -64,7 +65,6 @@ pub use publish_blocks::{ }; use serde::{Deserialize, Serialize}; use serde_json::Value; -use slog::{crit, debug, error, info, warn, Logger}; use slot_clock::SlotClock; use ssz::Encode; pub use state_id::StateId; @@ -86,6 +86,7 @@ use tokio_stream::{ wrappers::{errors::BroadcastStreamRecvError, BroadcastStream}, StreamExt, }; +use tracing::{debug, error, info, warn}; use types::AttestationData; use types::{ fork_versioned_response::EmptyMetadata, Attestation, AttestationShufflingId, AttesterSlashing, @@ -135,7 +136,6 @@ pub struct Context { pub beacon_processor_reprocess_send: Option>, pub eth1_service: Option, pub sse_logging_components: Option, - pub log: Logger, } /// Configuration for the HTTP server. @@ -151,7 +151,6 @@ pub struct Config { pub enable_beacon_processor: bool, #[serde(with = "eth2::types::serde_status_code")] pub duplicate_block_status_code: StatusCode, - pub enable_light_client_server: bool, pub target_peers: usize, } @@ -167,7 +166,6 @@ impl Default for Config { sse_capacity_multiplier: 1, enable_beacon_processor: true, duplicate_block_status_code: StatusCode::ACCEPTED, - enable_light_client_server: true, target_peers: 100, } } @@ -191,40 +189,6 @@ impl From for Error { } } -/// Creates a `warp` logging wrapper which we use to create `slog` logs. -pub fn slog_logging( - log: Logger, -) -> warp::filters::log::Log { - warp::log::custom(move |info| { - match info.status() { - status - if status == StatusCode::OK - || status == StatusCode::NOT_FOUND - || status == StatusCode::PARTIAL_CONTENT => - { - debug!( - log, - "Processed HTTP API request"; - "elapsed" => format!("{:?}", info.elapsed()), - "status" => status.to_string(), - "path" => info.path(), - "method" => info.method().to_string(), - ); - } - status => { - warn!( - log, - "Error processing HTTP API request"; - "elapsed" => format!("{:?}", info.elapsed()), - "status" => status.to_string(), - "path" => info.path(), - "method" => info.method().to_string(), - ); - } - }; - }) -} - /// Creates a `warp` logging wrapper which we use for Prometheus metrics (not necessarily logging, /// per say). pub fn prometheus_metrics() -> warp::filters::log::Log { @@ -251,35 +215,66 @@ pub fn prometheus_metrics() -> warp::filters::log::Log warp::filters::log::Log impl Filter + Clone { - warp::any() - .and_then(move || async move { - if is_enabled { - Ok(()) - } else { - Err(warp::reject::not_found()) - } - }) - .untuple_one() +/// Creates a `warp` logging wrapper which we use to create `tracing` logs. +pub fn tracing_logging() -> 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`. @@ -324,7 +339,6 @@ pub fn serve( shutdown: impl Future + Send + Sync + 'static, ) -> Result { let config = ctx.config.clone(); - let log = ctx.log.clone(); // Configure CORS. let cors_builder = { @@ -341,7 +355,7 @@ pub fn serve( // Sanity check. if !config.enabled { - crit!(log, "Cannot start disabled HTTP server"); + crit!("Cannot start disabled HTTP server"); return Err(Error::Other( "A disabled server should not be started".to_string(), )); @@ -490,9 +504,17 @@ pub fn serve( }, ); - // Create a `warp` filter that provides access to the logger. - let inner_ctx = ctx.clone(); - let log_filter = warp::any().map(move || inner_ctx.log.clone()); + // Create a `warp` filter that returns 404s if the light client server is disabled. + let light_client_server_filter = + warp::any() + .and(chain_filter.clone()) + .then(|chain: Arc>| async move { + if chain.config.enable_light_client_server { + Ok(()) + } else { + Err(warp::reject::not_found()) + } + }); let inner_components = ctx.sse_logging_components.clone(); let sse_component_filter = warp::any().map(move || inner_components.clone()); @@ -1388,21 +1410,18 @@ pub fn serve( .and(chain_filter.clone()) .and(network_tx_filter.clone()) .and(network_globals.clone()) - .and(log_filter.clone()) .then( move |block_contents: PublishBlockRequest, task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>, - network_globals: Arc>, - log: Logger| { + network_globals: Arc>| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { publish_blocks::publish_block( None, ProvenancedBlock::local_from_publish_request(block_contents), chain, &network_tx, - log, BroadcastValidation::default(), duplicate_block_status_code, network_globals, @@ -1422,15 +1441,13 @@ pub fn serve( .and(chain_filter.clone()) .and(network_tx_filter.clone()) .and(network_globals.clone()) - .and(log_filter.clone()) .then( move |block_bytes: Bytes, consensus_version: ForkName, task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>, - network_globals: Arc>, - log: Logger| { + network_globals: Arc>| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { let block_contents = PublishBlockRequest::::from_ssz_bytes( &block_bytes, @@ -1444,7 +1461,6 @@ pub fn serve( ProvenancedBlock::local_from_publish_request(block_contents), chain, &network_tx, - log, BroadcastValidation::default(), duplicate_block_status_code, network_globals, @@ -1464,22 +1480,19 @@ pub fn serve( .and(chain_filter.clone()) .and(network_tx_filter.clone()) .and(network_globals.clone()) - .and(log_filter.clone()) .then( move |validation_level: api_types::BroadcastValidationQuery, block_contents: PublishBlockRequest, task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>, - network_globals: Arc>, - log: Logger| { + network_globals: Arc>| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { publish_blocks::publish_block( None, ProvenancedBlock::local_from_publish_request(block_contents), chain, &network_tx, - log, validation_level.broadcast_validation, duplicate_block_status_code, network_globals, @@ -1500,7 +1513,6 @@ pub fn serve( .and(chain_filter.clone()) .and(network_tx_filter.clone()) .and(network_globals.clone()) - .and(log_filter.clone()) .then( move |validation_level: api_types::BroadcastValidationQuery, block_bytes: Bytes, @@ -1508,8 +1520,7 @@ pub fn serve( task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>, - network_globals: Arc>, - log: Logger| { + network_globals: Arc>| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { let block_contents = PublishBlockRequest::::from_ssz_bytes( &block_bytes, @@ -1523,7 +1534,6 @@ pub fn serve( ProvenancedBlock::local_from_publish_request(block_contents), chain, &network_tx, - log, validation_level.broadcast_validation, duplicate_block_status_code, network_globals, @@ -1547,20 +1557,17 @@ pub fn serve( .and(chain_filter.clone()) .and(network_tx_filter.clone()) .and(network_globals.clone()) - .and(log_filter.clone()) .then( move |block_contents: Arc>, task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>, - network_globals: Arc>, - log: Logger| { + network_globals: Arc>| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { publish_blocks::publish_blinded_block( block_contents, chain, &network_tx, - log, BroadcastValidation::default(), duplicate_block_status_code, network_globals, @@ -1580,14 +1587,12 @@ pub fn serve( .and(chain_filter.clone()) .and(network_tx_filter.clone()) .and(network_globals.clone()) - .and(log_filter.clone()) .then( move |block_bytes: Bytes, task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>, - network_globals: Arc>, - log: Logger| { + network_globals: Arc>| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { let block = SignedBlindedBeaconBlock::::from_ssz_bytes( &block_bytes, @@ -1601,7 +1606,6 @@ pub fn serve( block, chain, &network_tx, - log, BroadcastValidation::default(), duplicate_block_status_code, network_globals, @@ -1621,21 +1625,18 @@ pub fn serve( .and(chain_filter.clone()) .and(network_tx_filter.clone()) .and(network_globals.clone()) - .and(log_filter.clone()) .then( move |validation_level: api_types::BroadcastValidationQuery, blinded_block: Arc>, task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>, - network_globals: Arc>, - log: Logger| { + network_globals: Arc>| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { publish_blocks::publish_blinded_block( blinded_block, chain, &network_tx, - log, validation_level.broadcast_validation, duplicate_block_status_code, network_globals, @@ -1655,15 +1656,13 @@ pub fn serve( .and(chain_filter.clone()) .and(network_tx_filter.clone()) .and(network_globals.clone()) - .and(log_filter.clone()) .then( move |validation_level: api_types::BroadcastValidationQuery, block_bytes: Bytes, task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>, - network_globals: Arc>, - log: Logger| { + network_globals: Arc>| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { let block = SignedBlindedBeaconBlock::::from_ssz_bytes( &block_bytes, @@ -1677,7 +1676,6 @@ pub fn serve( block, chain, &network_tx, - log, validation_level.broadcast_validation, duplicate_block_status_code, network_globals, @@ -1947,14 +1945,12 @@ pub fn serve( .and(warp_utils::json::json()) .and(network_tx_filter.clone()) .and(reprocess_send_filter.clone()) - .and(log_filter.clone()) .then( |task_spawner: TaskSpawner, chain: Arc>, attestations: Vec>, network_tx: UnboundedSender>, - reprocess_tx: Option>, - log: Logger| async move { + reprocess_tx: Option>| async move { let attestations = attestations.into_iter().map(Either::Left).collect(); let result = crate::publish_attestations::publish_attestations( task_spawner, @@ -1962,7 +1958,6 @@ pub fn serve( attestations, network_tx, reprocess_tx, - log, ) .await .map(|()| warp::reply::json(&())); @@ -1978,25 +1973,22 @@ pub fn serve( .and(optional_consensus_version_header_filter) .and(network_tx_filter.clone()) .and(reprocess_send_filter.clone()) - .and(log_filter.clone()) .then( |task_spawner: TaskSpawner, chain: Arc>, payload: Value, fork_name: Option, network_tx: UnboundedSender>, - reprocess_tx: Option>, - log: Logger| async move { + reprocess_tx: Option>| async move { let attestations = match crate::publish_attestations::deserialize_attestation_payload::( - payload, fork_name, &log, + payload, fork_name, ) { Ok(attestations) => attestations, Err(err) => { warn!( - log, - "Unable to deserialize attestation POST request"; - "error" => ?err + error = ?err, + "Unable to deserialize attestation POST request" ); return warp::reply::with_status( warp::reply::json( @@ -2014,7 +2006,6 @@ pub fn serve( attestations, network_tx, reprocess_tx, - log, ) .await .map(|()| warp::reply::json(&())); @@ -2291,16 +2282,14 @@ pub fn serve( .and(warp::path::end()) .and(warp_utils::json::json()) .and(network_tx_filter.clone()) - .and(log_filter.clone()) .then( |task_spawner: TaskSpawner, chain: Arc>, signatures: Vec, - network_tx: UnboundedSender>, - log: Logger| { + network_tx: UnboundedSender>| { task_spawner.blocking_json_task(Priority::P0, move || { sync_committees::process_sync_committee_signatures( - signatures, network_tx, &chain, log, + signatures, network_tx, &chain, )?; Ok(api_types::GenericResponse::from(())) }) @@ -2328,13 +2317,11 @@ pub fn serve( .and(warp::path::end()) .and(warp_utils::json::json()) .and(network_tx_filter.clone()) - .and(log_filter.clone()) .then( |task_spawner: TaskSpawner, chain: Arc>, address_changes: Vec, - network_tx: UnboundedSender>, - log: Logger| { + network_tx: UnboundedSender>| { task_spawner.blocking_json_task(Priority::P0, move || { let mut failures = vec![]; @@ -2351,11 +2338,12 @@ pub fn serve( .to_execution_address; // New to P2P *and* op pool, gossip immediately if post-Capella. - let received_pre_capella = if chain.current_slot_is_post_capella().unwrap_or(false) { - ReceivedPreCapella::No - } else { - ReceivedPreCapella::Yes - }; + let received_pre_capella = + if chain.current_slot_is_post_capella().unwrap_or(false) { + ReceivedPreCapella::No + } else { + ReceivedPreCapella::Yes + }; if matches!(received_pre_capella, ReceivedPreCapella::No) { publish_pubsub_message( &network_tx, @@ -2366,32 +2354,29 @@ pub fn serve( } // Import to op pool (may return `false` if there's a race). - let imported = - chain.import_bls_to_execution_change(verified_address_change, received_pre_capella); + let imported = chain.import_bls_to_execution_change( + verified_address_change, + received_pre_capella, + ); info!( - log, - "Processed BLS to execution change"; - "validator_index" => validator_index, - "address" => ?address, - "published" => matches!(received_pre_capella, ReceivedPreCapella::No), - "imported" => imported, + %validator_index, + ?address, + published = + matches!(received_pre_capella, ReceivedPreCapella::No), + imported, + "Processed BLS to execution change" ); } Ok(ObservationOutcome::AlreadyKnown) => { - debug!( - log, - "BLS to execution change already known"; - "validator_index" => validator_index, - ); + debug!(%validator_index, "BLS to execution change already known"); } Err(e) => { warn!( - log, - "Invalid BLS to execution change"; - "validator_index" => validator_index, - "reason" => ?e, - "source" => "HTTP", + validator_index, + reason = ?e, + source = "HTTP", + "Invalid BLS to execution change" ); failures.push(api_types::Failure::new( index, @@ -2550,6 +2535,7 @@ pub fn serve( let beacon_light_client_path = eth_v1 .and(warp::path("beacon")) .and(warp::path("light_client")) + .and(light_client_server_filter) .and(chain_filter.clone()); // GET beacon/light_client/bootstrap/{block_root} @@ -2565,11 +2551,13 @@ pub fn serve( .and(warp::path::end()) .and(warp::header::optional::("accept")) .then( - |chain: Arc>, + |light_client_server_enabled: Result<(), Rejection>, + chain: Arc>, task_spawner: TaskSpawner, block_root: Hash256, accept_header: Option| { task_spawner.blocking_response_task(Priority::P1, move || { + light_client_server_enabled?; get_light_client_bootstrap::(chain, &block_root, accept_header) }) }, @@ -2583,10 +2571,12 @@ pub fn serve( .and(warp::path::end()) .and(warp::header::optional::("accept")) .then( - |chain: Arc>, + |light_client_server_enabled: Result<(), Rejection>, + chain: Arc>, task_spawner: TaskSpawner, accept_header: Option| { task_spawner.blocking_response_task(Priority::P1, move || { + light_client_server_enabled?; let update = chain .light_client_server_cache .get_latest_optimistic_update() @@ -2630,10 +2620,12 @@ pub fn serve( .and(warp::path::end()) .and(warp::header::optional::("accept")) .then( - |chain: Arc>, + |light_client_server_enabled: Result<(), Rejection>, + chain: Arc>, task_spawner: TaskSpawner, accept_header: Option| { task_spawner.blocking_response_task(Priority::P1, move || { + light_client_server_enabled?; let update = chain .light_client_server_cache .get_latest_finality_update() @@ -2678,11 +2670,13 @@ pub fn serve( .and(warp::query::()) .and(warp::header::optional::("accept")) .then( - |chain: Arc>, + |light_client_server_enabled: Result<(), Rejection>, + chain: Arc>, task_spawner: TaskSpawner, query: LightClientUpdatesQuery, accept_header: Option| { task_spawner.blocking_response_task(Priority::P1, move || { + light_client_server_enabled?; get_light_client_updates::(chain, query, accept_header) }) }, @@ -2755,17 +2749,15 @@ pub fn serve( .and(block_id_or_err) .and(warp::path::end()) .and(warp_utils::json::json()) - .and(log_filter.clone()) .then( |task_spawner: TaskSpawner, chain: Arc>, block_id: BlockId, - validators: Vec, - log: Logger| { + validators: Vec| { task_spawner.blocking_json_task(Priority::P1, move || { let (rewards, execution_optimistic, finalized) = sync_committee_rewards::compute_sync_committee_rewards( - chain, block_id, validators, log, + chain, block_id, validators, )?; Ok(api_types::GenericResponse::from(rewards)).map(|resp| { @@ -2852,14 +2844,12 @@ pub fn serve( .and(warp::header::optional::("accept")) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and(log_filter.clone()) .then( |endpoint_version: EndpointVersion, state_id: StateId, accept_header: Option, task_spawner: TaskSpawner, - chain: Arc>, - log: Logger| { + chain: Arc>| { task_spawner.blocking_response_task(Priority::P1, move || match accept_header { Some(api_types::Accept::Ssz) => { // We can ignore the optimistic status for the "fork" since it's a @@ -2874,10 +2864,9 @@ pub fn serve( let response_bytes = state.as_ssz_bytes(); drop(timer); debug!( - log, - "HTTP state load"; - "total_time_ms" => t.elapsed().as_millis(), - "target_slot" => state.slot() + total_time_ms = t.elapsed().as_millis(), + target_slot = %state.slot(), + "HTTP state load" ); Response::builder() @@ -3201,15 +3190,13 @@ pub fn serve( }; // the eth2 API spec implies only peers we have been connected to at some point should be included. - if let Some(dir) = peer_info.connection_direction().as_ref() { + if let Some(&dir) = peer_info.connection_direction() { return Ok(api_types::GenericResponse::from(api_types::PeerData { peer_id: peer_id.to_string(), enr: peer_info.enr().map(|enr| enr.to_base64()), last_seen_p2p_address: address, - direction: api_types::PeerDirection::from_connection_direction(dir), - state: api_types::PeerState::from_peer_connection_status( - peer_info.connection_status(), - ), + direction: dir.into(), + state: peer_info.connection_status().clone().into(), })); } } @@ -3250,12 +3237,9 @@ pub fn serve( }; // the eth2 API spec implies only peers we have been connected to at some point should be included. - if let Some(dir) = peer_info.connection_direction() { - let direction = - api_types::PeerDirection::from_connection_direction(dir); - let state = api_types::PeerState::from_peer_connection_status( - peer_info.connection_status(), - ); + if let Some(&dir) = peer_info.connection_direction() { + 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) @@ -3307,9 +3291,8 @@ pub fn serve( .read() .peers() .for_each(|(_, peer_info)| { - let state = api_types::PeerState::from_peer_connection_status( - peer_info.connection_status(), - ); + let state = + api_types::PeerState::from(peer_info.connection_status().clone()); match state { api_types::PeerState::Connected => connected += 1, api_types::PeerState::Connecting => connecting += 1, @@ -3345,16 +3328,14 @@ pub fn serve( .and(not_while_syncing_filter.clone()) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and(log_filter.clone()) .then( |epoch: Epoch, not_synced_filter: Result<(), Rejection>, task_spawner: TaskSpawner, - chain: Arc>, - log: Logger| { + chain: Arc>| { task_spawner.blocking_json_task(Priority::P0, move || { not_synced_filter?; - proposer_duties::proposer_duties(epoch, &chain, &log) + proposer_duties::proposer_duties(epoch, &chain) }) }, ); @@ -3374,7 +3355,6 @@ pub fn serve( .and(warp::query::()) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and(log_filter.clone()) .then( |endpoint_version: EndpointVersion, slot: Slot, @@ -3382,14 +3362,9 @@ pub fn serve( not_synced_filter: Result<(), Rejection>, query: api_types::ValidatorBlocksQuery, task_spawner: TaskSpawner, - chain: Arc>, - log: Logger| { + chain: Arc>| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { - debug!( - log, - "Block production request from HTTP API"; - "slot" => slot - ); + debug!(?slot, "Block production request from HTTP API"); not_synced_filter?; @@ -3596,7 +3571,6 @@ pub fn serve( .and(chain_filter.clone()) .and(warp_utils::json::json()) .and(network_tx_filter.clone()) - .and(log_filter.clone()) .then( // V1 and V2 are identical except V2 has a consensus version header in the request. // We only require this header for SSZ deserialization, which isn't supported for @@ -3606,7 +3580,7 @@ pub fn serve( task_spawner: TaskSpawner, chain: Arc>, aggregates: Vec>, - network_tx: UnboundedSender>, log: Logger| { + network_tx: UnboundedSender>| { task_spawner.blocking_json_task(Priority::P0, move || { not_synced_filter?; let seen_timestamp = timestamp_now(); @@ -3653,13 +3627,13 @@ pub fn serve( // aggregate has been successfully published by some other node. Err(AttnError::AggregatorAlreadyKnown(_)) => continue, Err(e) => { - error!(log, - "Failure verifying aggregate and proofs"; - "error" => format!("{:?}", e), - "request_index" => index, - "aggregator_index" => aggregate.message().aggregator_index(), - "attestation_index" => aggregate.message().aggregate().committee_index(), - "attestation_slot" => aggregate.message().aggregate().data().slot, + error!( + error = ?e, + request_index = index, + aggregator_index = aggregate.message().aggregator_index(), + attestation_index = aggregate.message().aggregate().committee_index(), + attestation_slot = %aggregate.message().aggregate().data().slot, + "Failure verifying aggregate and proofs" ); failures.push(api_types::Failure::new(index, format!("Verification: {:?}", e))); } @@ -3674,22 +3648,21 @@ pub fn serve( // Import aggregate attestations for (index, verified_aggregate) in verified_aggregates { if let Err(e) = chain.apply_attestation_to_fork_choice(&verified_aggregate) { - error!(log, - "Failure applying verified aggregate attestation to fork choice"; - "error" => format!("{:?}", e), - "request_index" => index, - "aggregator_index" => verified_aggregate.aggregate().message().aggregator_index(), - "attestation_index" => verified_aggregate.attestation().committee_index(), - "attestation_slot" => verified_aggregate.attestation().data().slot, + error!( + error = ?e, + request_index = index, + aggregator_index = verified_aggregate.aggregate().message().aggregator_index(), + attestation_index = verified_aggregate.attestation().committee_index(), + attestation_slot = %verified_aggregate.attestation().data().slot, + "Failure applying verified aggregate attestation to fork choice" ); failures.push(api_types::Failure::new(index, format!("Fork choice: {:?}", e))); } if let Err(e) = chain.add_to_block_inclusion_pool(verified_aggregate) { warn!( - log, - "Could not add verified aggregate attestation to the inclusion pool"; - "error" => ?e, - "request_index" => index, + error = ?e, + request_index = index, + "Could not add verified aggregate attestation to the inclusion pool" ); failures.push(api_types::Failure::new(index, format!("Op pool: {:?}", e))); } @@ -3715,21 +3688,18 @@ pub fn serve( .and(chain_filter.clone()) .and(warp_utils::json::json()) .and(network_tx_filter.clone()) - .and(log_filter.clone()) .then( |not_synced_filter: Result<(), Rejection>, task_spawner: TaskSpawner, chain: Arc>, contributions: Vec>, - network_tx: UnboundedSender>, - log: Logger| { + network_tx: UnboundedSender>| { task_spawner.blocking_json_task(Priority::P0, move || { not_synced_filter?; sync_committees::process_signed_contribution_and_proofs( contributions, network_tx, &chain, - log, )?; Ok(api_types::GenericResponse::from(())) }) @@ -3745,13 +3715,11 @@ pub fn serve( .and(validator_subscription_tx_filter.clone()) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and(log_filter.clone()) .then( |subscriptions: Vec, validator_subscription_tx: Sender, task_spawner: TaskSpawner, - chain: Arc>, - log: Logger| { + chain: Arc>| { task_spawner.blocking_json_task(Priority::P0, move || { let subscriptions: std::collections::BTreeSet<_> = subscriptions .iter() @@ -3772,10 +3740,9 @@ pub fn serve( ValidatorSubscriptionMessage::AttestationSubscribe { subscriptions }; if let Err(e) = validator_subscription_tx.try_send(message) { warn!( - log, - "Unable to process committee subscriptions"; - "info" => "the host may be overloaded or resource-constrained", - "error" => ?e, + info = "the host may be overloaded or resource-constrained", + error = ?e, + "Unable to process committee subscriptions" ); return Err(warp_utils::reject::custom_server_error( "unable to queue subscription, host may be overloaded or shutting down" @@ -3796,13 +3763,11 @@ pub fn serve( .and(not_while_syncing_filter.clone()) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and(log_filter.clone()) .and(warp_utils::json::json()) .then( |not_synced_filter: Result<(), Rejection>, task_spawner: TaskSpawner, chain: Arc>, - log: Logger, preparation_data: Vec| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { not_synced_filter?; @@ -3816,9 +3781,8 @@ pub fn serve( let current_epoch = current_slot.epoch(T::EthSpec::slots_per_epoch()); debug!( - log, - "Received proposer preparation data"; - "count" => preparation_data.len(), + count = preparation_data.len(), + "Received proposer preparation data" ); execution_layer @@ -3850,12 +3814,10 @@ pub fn serve( .and(warp::path::end()) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and(log_filter.clone()) .and(warp_utils::json::json()) .then( |task_spawner: TaskSpawner, chain: Arc>, - log: Logger, register_val_data: Vec| async { let (tx, rx) = oneshot::channel(); @@ -3874,9 +3836,8 @@ pub fn serve( let current_epoch = current_slot.epoch(T::EthSpec::slots_per_epoch()); debug!( - log, - "Received register validator request"; - "count" => register_val_data.len(), + count = register_val_data.len(), + "Received register validator request" ); let head_snapshot = chain.head_snapshot(); @@ -3951,9 +3912,8 @@ pub fn serve( })?; info!( - log, - "Forwarding register validator request to connected builder"; - "count" => filtered_registration_data.len(), + count = filtered_registration_data.len(), + "Forwarding register validator request to connected builder" ); // It's a waste of a `BeaconProcessor` worker to just @@ -3978,10 +3938,9 @@ pub fn serve( .map(|resp| warp::reply::json(&resp).into_response()) .map_err(|e| { warn!( - log, - "Relay error when registering validator(s)"; - "num_registrations" => filtered_registration_data.len(), - "error" => ?e + num_registrations = filtered_registration_data.len(), + error = ?e, + "Relay error when registering validator(s)" ); // Forward the HTTP status code if we are able to, otherwise fall back // to a server error. @@ -4035,13 +3994,11 @@ pub fn serve( .and(validator_subscription_tx_filter) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and(log_filter.clone()) .then( |subscriptions: Vec, validator_subscription_tx: Sender, task_spawner: TaskSpawner, chain: Arc>, - log: Logger | { task_spawner.blocking_json_task(Priority::P0, move || { for subscription in subscriptions { @@ -4055,10 +4012,9 @@ pub fn serve( }; if let Err(e) = validator_subscription_tx.try_send(message) { warn!( - log, - "Unable to process sync subscriptions"; - "info" => "the host may be overloaded or resource-constrained", - "error" => ?e + info = "the host may be overloaded or resource-constrained", + error = ?e, + "Unable to process sync subscriptions" ); return Err(warp_utils::reject::custom_server_error( "unable to queue subscription, host may be overloaded or shutting down".to_string(), @@ -4168,22 +4124,19 @@ pub fn serve( .and(task_spawner_filter.clone()) .and(network_globals.clone()) .and(network_tx_filter.clone()) - .and(log_filter.clone()) .then( |request_data: api_types::AdminPeer, task_spawner: TaskSpawner, network_globals: Arc>, - network_tx: UnboundedSender>, - log: Logger| { + network_tx: UnboundedSender>| { task_spawner.blocking_json_task(Priority::P0, move || { let enr = Enr::from_str(&request_data.enr).map_err(|e| { warp_utils::reject::custom_bad_request(format!("invalid enr error {}", e)) })?; info!( - log, - "Adding trusted peer"; - "peer_id" => %enr.peer_id(), - "multiaddr" => ?enr.multiaddr() + peer_id = %enr.peer_id(), + multiaddr = ?enr.multiaddr(), + "Adding trusted peer" ); network_globals.add_trusted_peer(enr.clone()); @@ -4202,22 +4155,19 @@ pub fn serve( .and(task_spawner_filter.clone()) .and(network_globals.clone()) .and(network_tx_filter.clone()) - .and(log_filter.clone()) .then( |request_data: api_types::AdminPeer, task_spawner: TaskSpawner, network_globals: Arc>, - network_tx: UnboundedSender>, - log: Logger| { + network_tx: UnboundedSender>| { task_spawner.blocking_json_task(Priority::P0, move || { let enr = Enr::from_str(&request_data.enr).map_err(|e| { warp_utils::reject::custom_bad_request(format!("invalid enr error {}", e)) })?; info!( - log, - "Removing trusted peer"; - "peer_id" => %enr.peer_id(), - "multiaddr" => ?enr.multiaddr() + peer_id = %enr.peer_id(), + multiaddr = ?enr.multiaddr(), + "Removing trusted peer" ); network_globals.remove_trusted_peer(enr.clone()); @@ -4412,7 +4362,7 @@ pub fn serve( .peers .read() .peers() - .map(|(peer_id, peer_info)| eth2::lighthouse::Peer { + .map(|(peer_id, peer_info)| peer::Peer { peer_id: peer_id.to_string(), peer_info: peer_info.clone(), }) @@ -4432,15 +4382,14 @@ pub fn serve( |task_spawner: TaskSpawner, network_globals: Arc>| { task_spawner.blocking_json_task(Priority::P1, move || { - Ok(network_globals - .peers - .read() - .connected_peers() - .map(|(peer_id, peer_info)| eth2::lighthouse::Peer { + let mut peers = vec![]; + for (peer_id, peer_info) in network_globals.peers.read().connected_peers() { + peers.push(peer::Peer { peer_id: peer_id.to_string(), peer_info: peer_info.clone(), - }) - .collect::>()) + }); + } + Ok(peers) }) }, ); @@ -4645,10 +4594,9 @@ pub fn serve( .and(warp::path::end()) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and(log_filter.clone()) - .then(|query, task_spawner: TaskSpawner, chain, log| { + .then(|query, task_spawner: TaskSpawner, chain| { task_spawner.blocking_json_task(Priority::P1, move || { - block_rewards::get_block_rewards(query, chain, log) + block_rewards::get_block_rewards(query, chain) }) }); @@ -4660,14 +4608,11 @@ pub fn serve( .and(warp::path::end()) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and(log_filter.clone()) - .then( - |blocks, task_spawner: TaskSpawner, chain, log| { - task_spawner.blocking_json_task(Priority::P1, move || { - block_rewards::compute_block_rewards(blocks, chain, log) - }) - }, - ); + .then(|blocks, task_spawner: TaskSpawner, chain| { + task_spawner.blocking_json_task(Priority::P1, move || { + block_rewards::compute_block_rewards(blocks, chain) + }) + }); // GET lighthouse/analysis/attestation_performance/{index} let get_lighthouse_attestation_performance = warp::path("lighthouse") @@ -4845,7 +4790,9 @@ pub fn serve( match msg { Ok(data) => { // Serialize to json - match data.to_json_string() { + match serde_json::to_string(&data) + .map_err(|e| format!("{:?}", e)) + { // Send the json as a Server Side Event Ok(json) => Ok(Event::default().data(json)), Err(e) => { @@ -4941,22 +4888,10 @@ pub fn serve( .uor(get_lighthouse_database_info) .uor(get_lighthouse_block_rewards) .uor(get_lighthouse_attestation_performance) - .uor( - enable(ctx.config.enable_light_client_server) - .and(get_beacon_light_client_optimistic_update), - ) - .uor( - enable(ctx.config.enable_light_client_server) - .and(get_beacon_light_client_finality_update), - ) - .uor( - enable(ctx.config.enable_light_client_server) - .and(get_beacon_light_client_bootstrap), - ) - .uor( - enable(ctx.config.enable_light_client_server) - .and(get_beacon_light_client_updates), - ) + .uor(get_beacon_light_client_optimistic_update) + .uor(get_beacon_light_client_finality_update) + .uor(get_beacon_light_client_bootstrap) + .uor(get_beacon_light_client_updates) .uor(get_lighthouse_block_packing_efficiency) .uor(get_lighthouse_merge_readiness) .uor(get_events) @@ -5012,7 +4947,7 @@ pub fn serve( ), ) .recover(warp_utils::reject::handle_rejection) - .with(slog_logging(log.clone())) + .with(tracing_logging()) .with(prometheus_metrics()) // Add a `Server` header. .map(|reply| warp::reply::with_header(reply, "Server", &version_with_platform())) @@ -5030,7 +4965,7 @@ pub fn serve( shutdown.await; })?; - info!(log, "HTTP API is being served over TLS";); + info!("HTTP API is being served over TLS"); (socket, Box::pin(server)) } @@ -5044,9 +4979,8 @@ pub fn serve( }; info!( - log, - "HTTP API started"; - "listen_address" => %http_server.0, + listen_address = %http_server.0, + "HTTP API started" ); Ok(http_server) diff --git a/beacon_node/http_api/src/peer.rs b/beacon_node/http_api/src/peer.rs new file mode 100644 index 00000000000..c9aea9d87cf --- /dev/null +++ b/beacon_node/http_api/src/peer.rs @@ -0,0 +1,13 @@ +use lighthouse_network::PeerInfo; +use serde::Serialize; +use types::EthSpec; + +/// Information returned by `peers` and `connected_peers`. +#[derive(Debug, Clone, Serialize)] +#[serde(bound = "E: EthSpec")] +pub(crate) struct Peer { + /// The Peer's ID + pub peer_id: String, + /// The PeerInfo associated with the peer. + pub peer_info: PeerInfo, +} diff --git a/beacon_node/http_api/src/proposer_duties.rs b/beacon_node/http_api/src/proposer_duties.rs index c4945df9d70..971571f4879 100644 --- a/beacon_node/http_api/src/proposer_duties.rs +++ b/beacon_node/http_api/src/proposer_duties.rs @@ -7,9 +7,9 @@ use beacon_chain::{ }; use eth2::types::{self as api_types}; use safe_arith::SafeArith; -use slog::{debug, Logger}; use slot_clock::SlotClock; use std::cmp::Ordering; +use tracing::debug; use types::{Epoch, EthSpec, Hash256, Slot}; /// The struct that is returned to the requesting HTTP client. @@ -19,7 +19,6 @@ type ApiDuties = api_types::DutiesResponse>; pub fn proposer_duties( request_epoch: Epoch, chain: &BeaconChain, - log: &Logger, ) -> Result { let current_epoch = chain .slot_clock @@ -52,11 +51,7 @@ pub fn proposer_duties( if let Some(duties) = try_proposer_duties_from_cache(request_epoch, chain)? { Ok(duties) } else { - debug!( - log, - "Proposer cache miss"; - "request_epoch" => request_epoch, - ); + debug!(%request_epoch, "Proposer cache miss"); compute_and_cache_proposer_duties(request_epoch, chain) } } else if request_epoch diff --git a/beacon_node/http_api/src/publish_attestations.rs b/beacon_node/http_api/src/publish_attestations.rs index 10d13e09a58..cd5e912bdf4 100644 --- a/beacon_node/http_api/src/publish_attestations.rs +++ b/beacon_node/http_api/src/publish_attestations.rs @@ -45,7 +45,6 @@ use eth2::types::Failure; use lighthouse_network::PubsubMessage; use network::NetworkMessage; use serde_json::Value; -use slog::{debug, error, warn, Logger}; use std::borrow::Cow; use std::sync::Arc; use std::time::Duration; @@ -53,6 +52,7 @@ use tokio::sync::{ mpsc::{Sender, UnboundedSender}, oneshot, }; +use tracing::{debug, error, warn}; use types::{Attestation, EthSpec, ForkName, SingleAttestation}; // Error variants are only used in `Debug` and considered `dead_code` by the compiler. @@ -80,14 +80,10 @@ enum PublishAttestationResult { pub fn deserialize_attestation_payload( payload: Value, fork_name: Option, - log: &Logger, ) -> Result, SingleAttestation>>, Error> { if fork_name.is_some_and(|fork_name| fork_name.electra_enabled()) || fork_name.is_none() { if fork_name.is_none() { - warn!( - log, - "No Consensus Version header specified."; - ); + warn!("No Consensus Version header specified."); } Ok(serde_json::from_value::>(payload) @@ -111,7 +107,6 @@ fn verify_and_publish_attestation( either_attestation: &Either, SingleAttestation>, seen_timestamp: Duration, network_tx: &UnboundedSender>, - log: &Logger, ) -> Result<(), Error> { let attestation = convert_to_attestation(chain, either_attestation)?; let verified_attestation = chain @@ -157,16 +152,14 @@ fn verify_and_publish_attestation( if let Err(e) = &fc_result { warn!( - log, - "Attestation invalid for fork choice"; - "err" => ?e, + err = ?e, + "Attestation invalid for fork choice" ); } if let Err(e) = &naive_aggregation_result { warn!( - log, - "Attestation invalid for aggregation"; - "err" => ?e + err = ?e, + "Attestation invalid for aggregation" ); } @@ -232,7 +225,6 @@ pub async fn publish_attestations( attestations: Vec, SingleAttestation>>, network_tx: UnboundedSender>, reprocess_send: Option>, - log: Logger, ) -> Result<(), warp::Rejection> { // Collect metadata about attestations which we'll use to report failures. We need to // move the `attestations` vec into the blocking task, so this small overhead is unavoidable. @@ -246,7 +238,6 @@ pub async fn publish_attestations( // Gossip validate and publish attestations that can be immediately processed. let seen_timestamp = timestamp_now(); - let inner_log = log.clone(); let mut prelim_results = task_spawner .blocking_task(Priority::P0, move || { Ok(attestations @@ -257,7 +248,6 @@ pub async fn publish_attestations( &attestation, seen_timestamp, &network_tx, - &inner_log, ) { Ok(()) => PublishAttestationResult::Success, Err(Error::Validation(AttestationError::UnknownHeadBlock { @@ -270,14 +260,12 @@ pub async fn publish_attestations( let (tx, rx) = oneshot::channel(); let reprocess_chain = chain.clone(); let reprocess_network_tx = network_tx.clone(); - let reprocess_log = inner_log.clone(); let reprocess_fn = move || { let result = verify_and_publish_attestation( &reprocess_chain, &attestation, seen_timestamp, &reprocess_network_tx, - &reprocess_log, ); // Ignore failure on the oneshot that reports the result. This // shouldn't happen unless some catastrophe befalls the waiting @@ -330,10 +318,9 @@ pub async fn publish_attestations( for (i, reprocess_result) in reprocess_indices.into_iter().zip(reprocess_results) { let Some(result_entry) = prelim_results.get_mut(i) else { error!( - log, - "Unreachable case in attestation publishing"; - "case" => "prelim out of bounds", - "request_index" => i, + case = "prelim out of bounds", + request_index = i, + "Unreachable case in attestation publishing" ); continue; }; @@ -361,39 +348,35 @@ pub async fn publish_attestations( Some(PublishAttestationResult::Failure(e)) => { if let Some((slot, committee_index)) = attestation_metadata.get(index) { error!( - log, - "Failure verifying attestation for gossip"; - "error" => ?e, - "request_index" => index, - "committee_index" => committee_index, - "attestation_slot" => slot, + error = ?e, + request_index = index, + committee_index, + attestation_slot = %slot, + "Failure verifying attestation for gossip" ); failures.push(Failure::new(index, format!("{e:?}"))); } else { error!( - log, - "Unreachable case in attestation publishing"; - "case" => "out of bounds", - "request_index" => index + case = "out of bounds", + request_index = index, + "Unreachable case in attestation publishing" ); failures.push(Failure::new(index, "metadata logic error".into())); } } Some(PublishAttestationResult::Reprocessing(_)) => { error!( - log, - "Unreachable case in attestation publishing"; - "case" => "reprocessing", - "request_index" => index + case = "reprocessing", + request_index = index, + "Unreachable case in attestation publishing" ); failures.push(Failure::new(index, "reprocess logic error".into())); } None => { error!( - log, - "Unreachable case in attestation publishing"; - "case" => "result is None", - "request_index" => index + case = "result is None", + request_index = index, + "Unreachable case in attestation publishing" ); failures.push(Failure::new(index, "result logic error".into())); } @@ -402,9 +385,8 @@ pub async fn publish_attestations( if num_already_known > 0 { debug!( - log, - "Some unagg attestations already known"; - "count" => num_already_known + count = num_already_known, + "Some unagg attestations already known" ); } diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 60d4b2f16ed..b613cf84672 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::{ @@ -18,13 +18,13 @@ use futures::TryFutureExt; use lighthouse_network::{NetworkGlobals, PubsubMessage}; use network::NetworkMessage; use rand::prelude::SliceRandom; -use slog::{debug, error, info, warn, Logger}; use slot_clock::SlotClock; use std::marker::PhantomData; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc::UnboundedSender; +use tracing::{debug, error, info, warn}; use tree_hash::TreeHash; use types::{ AbstractExecPayload, BeaconBlockRef, BlobSidecar, BlobsList, BlockImportSource, @@ -80,12 +80,13 @@ pub async fn publish_block>( provenanced_block: ProvenancedBlock, chain: Arc>, network_tx: &UnboundedSender>, - log: Logger, validation_level: BroadcastValidation, duplicate_status_code: StatusCode, network_globals: Arc>, ) -> Result { let seen_timestamp = timestamp_now(); + let block_publishing_delay_for_testing = chain.config.block_publishing_delay; + let data_column_publishing_delay_for_testing = chain.config.data_column_publishing_delay; let (unverified_block, unverified_blobs, is_locally_built_block) = match provenanced_block { ProvenancedBlock::Local(block, blobs, _) => (block, blobs, true), @@ -97,12 +98,12 @@ pub async fn publish_block>( "builder" }; let block = unverified_block.inner_block(); - debug!(log, "Signed block received in HTTP API"; "slot" => block.slot()); + + debug!(slot = %block.slot(), "Signed block received in HTTP API"); /* actually publish a block */ let publish_block_p2p = move |block: Arc>, sender, - log, seen_timestamp| -> Result<(), BlockError> { let publish_timestamp = timestamp_now(); @@ -117,10 +118,9 @@ pub async fn publish_block>( ); info!( - log, - "Signed block published to network via HTTP API"; - "slot" => block.slot(), - "publish_delay_ms" => publish_delay.as_millis(), + slot = %block.slot(), + publish_delay_ms = publish_delay.as_millis(), + "Signed block published to network via HTTP API" ); crate::publish_pubsub_message(&sender, PubsubMessage::BeaconBlock(block.clone())) @@ -134,10 +134,11 @@ pub async fn publish_block>( let sender_clone = network_tx.clone(); let build_sidecar_task_handle = - spawn_build_data_sidecar_task(chain.clone(), block.clone(), unverified_blobs, log.clone())?; + spawn_build_data_sidecar_task(chain.clone(), block.clone(), unverified_blobs)?; // Gossip verify the block and blobs/data columns separately. - let gossip_verified_block_result = unverified_block.into_gossip_verified_block(&chain); + let gossip_verified_block_result = unverified_block + .into_gossip_verified_block(&chain, network_globals.custody_columns_count() as usize); let block_root = block_root.unwrap_or_else(|| { gossip_verified_block_result.as_ref().map_or_else( |_| block.canonical_root(), @@ -147,13 +148,15 @@ pub async fn publish_block>( let should_publish_block = gossip_verified_block_result.is_ok(); if BroadcastValidation::Gossip == validation_level && should_publish_block { - publish_block_p2p( - block.clone(), - sender_clone.clone(), - log.clone(), - seen_timestamp, - ) - .map_err(|_| warp_utils::reject::custom_server_error("unable to publish".into()))?; + if let Some(block_publishing_delay) = block_publishing_delay_for_testing { + debug!( + ?block_publishing_delay, + "Publishing block with artificial delay" + ); + tokio::time::sleep(block_publishing_delay).await; + } + publish_block_p2p(block.clone(), sender_clone.clone(), seen_timestamp) + .map_err(|_| warp_utils::reject::custom_server_error("unable to publish".into()))?; } let publish_fn_completed = Arc::new(AtomicBool::new(false)); @@ -165,15 +168,13 @@ pub async fn publish_block>( BroadcastValidation::Consensus => publish_block_p2p( block_to_publish.clone(), sender_clone.clone(), - log.clone(), seen_timestamp, )?, BroadcastValidation::ConsensusAndEquivocation => { - check_slashable(&chain, block_root, &block_to_publish, &log)?; + check_slashable(&chain, block_root, &block_to_publish)?; publish_block_p2p( block_to_publish.clone(), sender_clone.clone(), - log.clone(), seen_timestamp, )?; } @@ -196,17 +197,29 @@ pub async fn publish_block>( return if let BroadcastValidation::Gossip = validation_level { Err(warp_utils::reject::broadcast_without_import(msg)) } else { - error!( - log, - "Invalid blob provided to HTTP API"; - "reason" => &msg - ); + error!(reason = &msg, "Invalid blob provided to HTTP API"); Err(warp_utils::reject::custom_bad_request(msg)) }; } } if gossip_verified_columns.iter().map(Option::is_some).count() > 0 { + if let Some(data_column_publishing_delay) = data_column_publishing_delay_for_testing { + // Subtract block publishing delay if it is also used. + // Note: if `data_column_publishing_delay` is less than `block_publishing_delay`, it + // will still be delayed by `block_publishing_delay`. This could be solved with spawning + // async tasks but the limitation is minor and I believe it's probably not worth + // affecting the mainnet code path. + let block_publishing_delay = block_publishing_delay_for_testing.unwrap_or_default(); + let delay = data_column_publishing_delay.saturating_sub(block_publishing_delay); + if !delay.is_zero() { + debug!( + ?data_column_publishing_delay, + "Publishing data columns with artificial delay" + ); + tokio::time::sleep(delay).await; + } + } publish_column_sidecars(network_tx, &gossip_verified_columns, &chain).map_err(|_| { warp_utils::reject::custom_server_error("unable to publish data column sidecars".into()) })?; @@ -227,9 +240,8 @@ pub async fn publish_block>( Err(warp_utils::reject::broadcast_without_import(msg)) } else { error!( - log, - "Invalid data column during block publication"; - "reason" => &msg + reason = &msg, + "Invalid data column during block publication" ); Err(warp_utils::reject::custom_bad_request(msg)) }; @@ -253,7 +265,6 @@ pub async fn publish_block>( is_locally_built_block, seen_timestamp, &chain, - &log, ) .await } @@ -266,7 +277,6 @@ pub async fn publish_block>( is_locally_built_block, seen_timestamp, &chain, - &log, ) .await } else { @@ -286,14 +296,17 @@ pub async fn publish_block>( } Err(BlockError::DuplicateImportStatusUnknown(root)) => { debug!( - log, - "Block previously seen"; - "block_root" => ?root, - "slot" => block.slot(), + block_root = ?root, + slot = %block.slot(), + "Block previously seen" ); 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, @@ -306,16 +319,14 @@ pub async fn publish_block>( is_locally_built_block, seen_timestamp, &chain, - &log, ) .await } Err(e) => { warn!( - log, - "Not publishing block - not gossip verified"; - "slot" => slot, - "error" => %e + %slot, + error = %e, + "Not publishing block - not gossip verified" ); Err(warp_utils::reject::custom_bad_request(e.to_string())) } @@ -338,7 +349,6 @@ fn spawn_build_data_sidecar_task( chain: Arc>, block: Arc>>, proofs_and_blobs: UnverifiedBlobs, - log: Logger, ) -> Result>, Rejection> { chain .clone() @@ -353,12 +363,12 @@ fn spawn_build_data_sidecar_task( if !peer_das_enabled { // Pre-PeerDAS: construct blob sidecars for the network. let gossip_verified_blobs = - build_gossip_verified_blobs(&chain, &block, blobs, kzg_proofs, &log)?; + build_gossip_verified_blobs(&chain, &block, blobs, kzg_proofs)?; Ok((gossip_verified_blobs, vec![])) } else { // Post PeerDAS: construct data columns. let gossip_verified_data_columns = - build_gossip_verified_data_columns(&chain, &block, blobs, &log)?; + build_gossip_verified_data_columns(&chain, &block, blobs, kzg_proofs)?; Ok((vec![], gossip_verified_data_columns)) } }, @@ -377,16 +387,15 @@ fn build_gossip_verified_data_columns( chain: &BeaconChain, block: &SignedBeaconBlock>, blobs: BlobsList, - log: &Logger, + 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!( - log, - "Invalid data column - not publishing block"; - "error" => ?e, - "slot" => slot + error = ?e, + %slot, + "Invalid data column - not publishing block" ); warp_utils::reject::custom_bad_request(format!("{e:?}")) })?; @@ -407,21 +416,19 @@ fn build_gossip_verified_data_columns( // or some of the other data columns if the block & data columns are only // partially published by the other publisher. debug!( - log, - "Data column for publication already known"; - "column_index" => column_index, - "slot" => slot, - "proposer" => proposer, + column_index, + %slot, + proposer, + "Data column for publication already known" ); Ok(None) } Err(e) => { error!( - log, - "Data column for publication is gossip-invalid"; - "column_index" => column_index, - "slot" => slot, - "error" => ?e, + column_index, + %slot, + error = ?e, + "Data column for publication is gossip-invalid" ); Err(warp_utils::reject::custom_bad_request(format!("{e:?}"))) } @@ -437,7 +444,6 @@ fn build_gossip_verified_blobs( block: &SignedBeaconBlock>, blobs: BlobsList, kzg_proofs: KzgProofs, - log: &Logger, ) -> Result>>, Rejection> { let slot = block.slot(); let gossip_verified_blobs = kzg_proofs @@ -452,11 +458,10 @@ fn build_gossip_verified_blobs( .map(Arc::new) .map_err(|e| { error!( - log, - "Invalid blob - not publishing block"; - "error" => ?e, - "blob_index" => i, - "slot" => slot, + error = ?e, + blob_index = i, + %slot, + "Invalid blob - not publishing block" ); warp_utils::reject::custom_bad_request(format!("{e:?}")) })?; @@ -472,21 +477,19 @@ fn build_gossip_verified_blobs( // or some of the other blobs if the block & blobs are only partially published // by the other publisher. debug!( - log, - "Blob for publication already known"; - "blob_index" => blob_sidecar.index, - "slot" => slot, - "proposer" => proposer, + blob_index = blob_sidecar.index, + %slot, + proposer, + "Blob for publication already known" ); Ok(None) } Err(e) => { error!( - log, - "Blob for publication is gossip-invalid"; - "blob_index" => blob_sidecar.index, - "slot" => slot, - "error" => ?e, + blob_index = blob_sidecar.index, + %slot, + error = ?e, + "Blob for publication is gossip-invalid" ); Err(warp_utils::reject::custom_bad_request(e.to_string())) } @@ -497,6 +500,15 @@ fn build_gossip_verified_blobs( Ok(gossip_verified_blobs) } +fn publish_blob_sidecars( + sender_clone: &UnboundedSender>, + blob: &GossipVerifiedBlob, +) -> 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)) +} + fn publish_column_sidecars( sender_clone: &UnboundedSender>, data_column_sidecars: &[Option>], @@ -513,7 +525,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 @@ -527,15 +539,6 @@ fn publish_column_sidecars( .map_err(|_| BlockError::BeaconChainError(BeaconChainError::UnableToPublish)) } -fn publish_blob_sidecars( - sender_clone: &UnboundedSender>, - blob: &GossipVerifiedBlob, -) -> 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)) -} - async fn post_block_import_logging_and_response( result: Result, validation_level: BroadcastValidation, @@ -543,7 +546,6 @@ async fn post_block_import_logging_and_response( is_locally_built_block: bool, seen_timestamp: Duration, chain: &Arc>, - log: &Logger, ) -> Result { match result { // The `DuplicateFullyImported` case here captures the case where the block finishes @@ -555,12 +557,11 @@ async fn post_block_import_logging_and_response( | Err(BlockError::DuplicateFullyImported(root)) => { let delay = get_block_delay_ms(seen_timestamp, block.message(), &chain.slot_clock); info!( - log, - "Valid block from HTTP API"; - "block_delay" => ?delay, - "root" => %root, - "proposer_index" => block.message().proposer_index(), - "slot" => block.slot(), + block_delay = ?delay, + root = %root, + proposer_index = block.message().proposer_index(), + slot = %block.slot(), + "Valid block from HTTP API" ); // Notify the validator monitor. @@ -579,7 +580,7 @@ async fn post_block_import_logging_and_response( // blocks built with builders we consider the broadcast time to be // when the blinded block is published to the builder. if is_locally_built_block { - late_block_logging(chain, seen_timestamp, block.message(), root, "local", log) + late_block_logging(chain, seen_timestamp, block.message(), root, "local") } Ok(warp::reply().into_response()) } @@ -588,11 +589,7 @@ async fn post_block_import_logging_and_response( if let BroadcastValidation::Gossip = validation_level { Err(warp_utils::reject::broadcast_without_import(msg)) } else { - error!( - log, - "Invalid block provided to HTTP API"; - "reason" => &msg - ); + error!(reason = &msg, "Invalid block provided to HTTP API"); Err(warp_utils::reject::custom_bad_request(msg)) } } @@ -609,9 +606,8 @@ async fn post_block_import_logging_and_response( Err(warp_utils::reject::broadcast_without_import(format!("{e}"))) } else { error!( - log, - "Invalid block provided to HTTP API"; - "reason" => ?e, + reason = ?e, + "Invalid block provided to HTTP API" ); Err(warp_utils::reject::custom_bad_request(format!( "Invalid block: {e}" @@ -627,20 +623,17 @@ pub async fn publish_blinded_block( blinded_block: Arc>, chain: Arc>, network_tx: &UnboundedSender>, - log: Logger, validation_level: BroadcastValidation, duplicate_status_code: StatusCode, network_globals: Arc>, ) -> Result { let block_root = blinded_block.canonical_root(); - let full_block = - reconstruct_block(chain.clone(), block_root, blinded_block, log.clone()).await?; + let full_block = reconstruct_block(chain.clone(), block_root, blinded_block).await?; publish_block::( Some(block_root), full_block, chain, network_tx, - log, validation_level, duplicate_status_code, network_globals, @@ -655,7 +648,6 @@ pub async fn reconstruct_block( chain: Arc>, block_root: Hash256, block: Arc>, - log: Logger, ) -> Result>>, Rejection> { let full_payload_opt = if let Ok(payload_header) = block.message().body().execution_payload() { let el = chain.execution_layer.as_ref().ok_or_else(|| { @@ -679,7 +671,7 @@ pub async fn reconstruct_block( } else if let Some(cached_payload) = el.get_payload_by_root(&payload_header.tree_hash_root()) { - info!(log, "Reconstructing a full block using a local payload"; "block_hash" => ?cached_payload.block_hash()); + info!(block_hash = ?cached_payload.block_hash(), "Reconstructing a full block using a local payload"); ProvenancedPayload::Local(cached_payload) // Otherwise, this means we are attempting a blind block proposal. } else { @@ -694,7 +686,6 @@ pub async fn reconstruct_block( block.message(), block_root, "builder", - &log, ); let full_payload = el @@ -706,7 +697,7 @@ pub async fn reconstruct_block( e )) })?; - info!(log, "Successfully published a block to the builder network"; "block_hash" => ?full_payload.block_hash()); + info!(block_hash = ?full_payload.block_hash(), "Successfully published a block to the builder network"); ProvenancedPayload::Builder(full_payload) }; @@ -748,7 +739,6 @@ fn late_block_logging>( block: BeaconBlockRef, root: Hash256, provenance: &str, - log: &Logger, ) { let delay = get_block_delay_ms(seen_timestamp, block, &chain.slot_clock); @@ -767,23 +757,21 @@ fn late_block_logging>( let delayed_threshold = too_late_threshold / 2; if delay >= too_late_threshold { error!( - log, - "Block was broadcast too late"; - "msg" => "system may be overloaded, block likely to be orphaned", - "provenance" => provenance, - "delay_ms" => delay.as_millis(), - "slot" => block.slot(), - "root" => ?root, + msg = "system may be overloaded, block likely to be orphaned", + provenance, + delay_ms = delay.as_millis(), + slot = %block.slot(), + ?root, + "Block was broadcast too late" ) } else if delay >= delayed_threshold { error!( - log, - "Block broadcast was delayed"; - "msg" => "system may be overloaded, block may be orphaned", - "provenance" => provenance, - "delay_ms" => delay.as_millis(), - "slot" => block.slot(), - "root" => ?root, + msg = "system may be overloaded, block may be orphaned", + provenance, + delay_ms = delay.as_millis(), + slot = %block.slot(), + ?root, + "Block broadcast was delayed" ) } } @@ -793,7 +781,6 @@ fn check_slashable( chain_clone: &BeaconChain, block_root: Hash256, block_clone: &SignedBeaconBlock>, - log_clone: &Logger, ) -> Result<(), BlockError> { let slashable_cache = chain_clone.observed_slashable.read(); if slashable_cache @@ -805,9 +792,8 @@ fn check_slashable( .map_err(|e| BlockError::BeaconChainError(e.into()))? { warn!( - log_clone, - "Not publishing equivocating block"; - "slot" => block_clone.slot() + slot = %block_clone.slot(), + "Not publishing equivocating block" ); return Err(BlockError::Slashable); } diff --git a/beacon_node/http_api/src/standard_block_rewards.rs b/beacon_node/http_api/src/standard_block_rewards.rs index 372a2765da4..2f78649d78f 100644 --- a/beacon_node/http_api/src/standard_block_rewards.rs +++ b/beacon_node/http_api/src/standard_block_rewards.rs @@ -2,7 +2,7 @@ use crate::sync_committee_rewards::get_state_before_applying_block; use crate::BlockId; use crate::ExecutionOptimistic; use beacon_chain::{BeaconChain, BeaconChainTypes}; -use eth2::lighthouse::StandardBlockReward; +use eth2::types::StandardBlockReward; use std::sync::Arc; use warp_utils::reject::unhandled_error; /// The difference between block_rewards and beacon_block_rewards is the later returns block diff --git a/beacon_node/http_api/src/sync_committee_rewards.rs b/beacon_node/http_api/src/sync_committee_rewards.rs index 987dfdff59e..9bc1f6ead4d 100644 --- a/beacon_node/http_api/src/sync_committee_rewards.rs +++ b/beacon_node/http_api/src/sync_committee_rewards.rs @@ -1,10 +1,9 @@ use crate::{BlockId, ExecutionOptimistic}; use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes}; -use eth2::lighthouse::SyncCommitteeReward; -use eth2::types::ValidatorId; -use slog::{debug, Logger}; +use eth2::types::{SyncCommitteeReward, ValidatorId}; use state_processing::BlockReplayer; use std::sync::Arc; +use tracing::debug; use types::{BeaconState, SignedBlindedBeaconBlock}; use warp_utils::reject::{custom_not_found, unhandled_error}; @@ -12,7 +11,6 @@ pub fn compute_sync_committee_rewards( chain: Arc>, block_id: BlockId, validators: Vec, - log: Logger, ) -> Result<(Option>, ExecutionOptimistic, bool), warp::Rejection> { let (block, execution_optimistic, finalized) = block_id.blinded_block(&chain)?; @@ -23,7 +21,7 @@ pub fn compute_sync_committee_rewards( .map_err(unhandled_error)?; let data = if reward_payload.is_empty() { - debug!(log, "compute_sync_committee_rewards returned empty"); + debug!("compute_sync_committee_rewards returned empty"); None } else if validators.is_empty() { Some(reward_payload) diff --git a/beacon_node/http_api/src/sync_committees.rs b/beacon_node/http_api/src/sync_committees.rs index da9f9b7a063..0ae90f49b2b 100644 --- a/beacon_node/http_api/src/sync_committees.rs +++ b/beacon_node/http_api/src/sync_committees.rs @@ -11,11 +11,11 @@ use beacon_chain::{ use eth2::types::{self as api_types}; use lighthouse_network::PubsubMessage; use network::NetworkMessage; -use slog::{debug, error, warn, Logger}; use slot_clock::SlotClock; -use std::cmp::max; +use store::metadata::STATE_UPPER_LIMIT_NO_RETAIN; use std::collections::HashMap; use tokio::sync::mpsc::UnboundedSender; +use tracing::{debug, error, warn}; use types::{ slot_data::SlotData, BeaconStateError, Epoch, EthSpec, SignedContributionAndProof, SyncCommitteeMessage, SyncDuty, SyncSubnetId, @@ -108,14 +108,25 @@ fn duties_from_state_load( // have to transition the head to start of the current period). // // We also need to ensure that the load slot is after the Altair fork. - let load_slot = max( + let anchor_info = chain.store.get_anchor_info(); + let state_upper_limit = anchor_info.state_upper_limit; + // Compute the lower bound in epoch units. + let computed_epoch = std::cmp::max( chain.spec.epochs_per_sync_committee_period * sync_committee_period.saturating_sub(1), altair_fork_epoch, - ) - .start_slot(T::EthSpec::slots_per_epoch()); + ); + let computed_slot = computed_epoch.start_slot(T::EthSpec::slots_per_epoch()); + let effective_state_upper_limit = if state_upper_limit == STATE_UPPER_LIMIT_NO_RETAIN { + computed_slot + } else { + state_upper_limit + }; + + let load_slot = std::cmp::max(computed_slot, effective_state_upper_limit); let state = chain.state_at_slot(load_slot, StateSkipConfig::WithoutStateRoots)?; + state .get_sync_committee_duties(request_epoch, request_indices, &chain.spec) .map_err(BeaconChainError::SyncDutiesError) @@ -178,7 +189,6 @@ pub fn process_sync_committee_signatures( sync_committee_signatures: Vec, network_tx: UnboundedSender>, chain: &BeaconChain, - log: Logger, ) -> Result<(), warp::reject::Rejection> { let mut failures = vec![]; @@ -192,10 +202,9 @@ pub fn process_sync_committee_signatures( Ok(positions) => positions, Err(e) => { error!( - log, - "Unable to compute subnet positions for sync message"; - "error" => ?e, - "slot" => sync_committee_signature.slot, + error = ?e, + slot = %sync_committee_signature.slot, + "Unable to compute subnet positions for sync message" ); failures.push(api_types::Failure::new(i, format!("Verification: {:?}", e))); continue; @@ -248,22 +257,20 @@ pub fn process_sync_committee_signatures( new_root, }) => { debug!( - log, - "Ignoring already-known sync message"; - "new_root" => ?new_root, - "prev_root" => ?prev_root, - "slot" => slot, - "validator_index" => validator_index, + ?new_root, + ?prev_root, + %slot, + validator_index, + "Ignoring already-known sync message" ); } Err(e) => { error!( - log, - "Failure verifying sync committee signature for gossip"; - "error" => ?e, - "request_index" => i, - "slot" => sync_committee_signature.slot, - "validator_index" => sync_committee_signature.validator_index, + error = ?e, + request_index = i, + slot = %sync_committee_signature.slot, + validator_index = sync_committee_signature.validator_index, + "Failure verifying sync committee signature for gossip" ); failures.push(api_types::Failure::new(i, format!("Verification: {:?}", e))); } @@ -273,11 +280,10 @@ pub fn process_sync_committee_signatures( if let Some(verified) = verified_for_pool { if let Err(e) = chain.add_to_naive_sync_aggregation_pool(verified) { error!( - log, - "Unable to add sync committee signature to pool"; - "error" => ?e, - "slot" => sync_committee_signature.slot, - "validator_index" => sync_committee_signature.validator_index, + error = ?e, + slot = %sync_committee_signature.slot, + validator_index = sync_committee_signature.validator_index, + "Unable to add sync committee signature to pool" ); } } @@ -312,7 +318,6 @@ pub fn process_signed_contribution_and_proofs( signed_contribution_and_proofs: Vec>, network_tx: UnboundedSender>, chain: &BeaconChain, - log: Logger, ) -> Result<(), warp::reject::Rejection> { let mut verified_contributions = Vec::with_capacity(signed_contribution_and_proofs.len()); let mut failures = vec![]; @@ -362,13 +367,12 @@ pub fn process_signed_contribution_and_proofs( Err(SyncVerificationError::AggregatorAlreadyKnown(_)) => continue, Err(e) => { error!( - log, - "Failure verifying signed contribution and proof"; - "error" => ?e, - "request_index" => index, - "aggregator_index" => aggregator_index, - "subcommittee_index" => subcommittee_index, - "contribution_slot" => contribution_slot, + error = ?e, + request_index = index, + aggregator_index = aggregator_index, + subcommittee_index = subcommittee_index, + contribution_slot = %contribution_slot, + "Failure verifying signed contribution and proof" ); failures.push(api_types::Failure::new( index, @@ -382,10 +386,9 @@ pub fn process_signed_contribution_and_proofs( for (index, verified_contribution) in verified_contributions { if let Err(e) = chain.add_contribution_to_block_inclusion_pool(verified_contribution) { warn!( - log, - "Could not add verified sync contribution to the inclusion pool"; - "error" => ?e, - "request_index" => index, + error = ?e, + request_index = index, + "Could not add verified sync contribution to the inclusion pool" ); failures.push(api_types::Failure::new(index, format!("Op pool: {:?}", e))); } @@ -399,4 +402,4 @@ pub fn process_signed_contribution_and_proofs( } else { Ok(()) } -} +} \ No newline at end of file diff --git a/beacon_node/http_api/src/test_utils.rs b/beacon_node/http_api/src/test_utils.rs index fbc92a45cce..f78a361dad3 100644 --- a/beacon_node/http_api/src/test_utils.rs +++ b/beacon_node/http_api/src/test_utils.rs @@ -19,10 +19,8 @@ use lighthouse_network::{ types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield, SyncState}, ConnectedPoint, Enr, NetworkConfig, NetworkGlobals, PeerId, PeerManager, }; -use logging::test_logger; use network::{NetworkReceivers, NetworkSenders}; use sensitive_url::SensitiveUrl; -use slog::Logger; use std::future::Future; use std::net::SocketAddr; use std::sync::Arc; @@ -75,7 +73,6 @@ impl InteractiveTester { ) -> Self { let mut harness_builder = BeaconChainHarness::builder(E::default()) .spec_or_default(spec.map(Arc::new)) - .logger(test_logger()) .mock_execution_layer(); harness_builder = if let Some(initializer) = initializer { @@ -102,13 +99,7 @@ impl InteractiveTester { listening_socket, network_rx, .. - } = create_api_server_with_config( - harness.chain.clone(), - config, - &harness.runtime, - harness.logger().clone(), - ) - .await; + } = create_api_server_with_config(harness.chain.clone(), config, &harness.runtime).await; tokio::spawn(server); @@ -134,16 +125,14 @@ impl InteractiveTester { pub async fn create_api_server( chain: Arc>, test_runtime: &TestRuntime, - log: Logger, ) -> ApiServer> { - create_api_server_with_config(chain, Config::default(), test_runtime, log).await + create_api_server_with_config(chain, Config::default(), test_runtime).await } pub async fn create_api_server_with_config( chain: Arc>, http_config: Config, test_runtime: &TestRuntime, - log: Logger, ) -> ApiServer> { // Use port 0 to allocate a new unused port. let port = 0; @@ -174,14 +163,13 @@ pub async fn create_api_server_with_config( meta_data, vec![], false, - &log, network_config, chain.spec.clone(), )); // Only a peer manager can add peers, so we create a dummy manager. let config = lighthouse_network::peer_manager::config::Config::default(); - let mut pm = PeerManager::new(config, network_globals.clone(), &log).unwrap(); + let mut pm = PeerManager::new(config, network_globals.clone()).unwrap(); // add a peer let peer_id = PeerId::random(); @@ -200,8 +188,7 @@ pub async fn create_api_server_with_config( })); *network_globals.sync_state.write() = SyncState::Synced; - let eth1_service = - eth1::Service::new(eth1::Config::default(), log.clone(), chain.spec.clone()).unwrap(); + let eth1_service = eth1::Service::new(eth1::Config::default(), chain.spec.clone()).unwrap(); let beacon_processor_config = BeaconProcessorConfig { // The number of workers must be greater than one. Tests which use the @@ -225,7 +212,6 @@ pub async fn create_api_server_with_config( executor: test_runtime.task_executor.clone(), current_workers: 0, config: beacon_processor_config, - log: log.clone(), } .spawn_manager( beacon_processor_rx, @@ -249,7 +235,6 @@ pub async fn create_api_server_with_config( enabled: true, listen_port: port, data_dir: std::path::PathBuf::from(DEFAULT_ROOT_DIR), - enable_light_client_server: true, ..http_config }, chain: Some(chain), @@ -259,7 +244,6 @@ pub async fn create_api_server_with_config( beacon_processor_reprocess_send: Some(reprocess_send), eth1_service: Some(eth1_service), sse_logging_components: None, - log, }); let (listening_socket, server) = diff --git a/beacon_node/http_api/tests/broadcast_validation_tests.rs b/beacon_node/http_api/tests/broadcast_validation_tests.rs index 1baa71699c7..cd590580be4 100644 --- a/beacon_node/http_api/tests/broadcast_validation_tests.rs +++ b/beacon_node/http_api/tests/broadcast_validation_tests.rs @@ -39,6 +39,9 @@ type E = MainnetEthSpec; * */ +// Default custody group count for tests +const CGC: usize = 8; + /// This test checks that a block that is **invalid** from a gossip perspective gets rejected when using `broadcast_validation=gossip`. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] pub async fn gossip_invalid() { @@ -331,7 +334,6 @@ pub async fn consensus_partial_pass_only_consensus() { let validator_count = 64; let num_initial: u64 = 31; let tester = InteractiveTester::::new(None, validator_count).await; - let test_logger = tester.harness.logger().clone(); // Create some chain depth. tester.harness.advance_slot(); @@ -365,9 +367,9 @@ pub async fn consensus_partial_pass_only_consensus() { ); assert_ne!(block_a.state_root(), block_b.state_root()); - let gossip_block_b = block_b.into_gossip_verified_block(&tester.harness.chain); + let gossip_block_b = block_b.into_gossip_verified_block(&tester.harness.chain, CGC); assert!(gossip_block_b.is_ok()); - let gossip_block_a = block_a.into_gossip_verified_block(&tester.harness.chain); + let gossip_block_a = block_a.into_gossip_verified_block(&tester.harness.chain, CGC); assert!(gossip_block_a.is_err()); /* submit `block_b` which should induce equivocation */ @@ -379,7 +381,6 @@ pub async fn consensus_partial_pass_only_consensus() { ProvenancedBlock::local(gossip_block_b.unwrap(), blobs_b), tester.harness.chain.clone(), &channel.0, - test_logger, validation_level, StatusCode::ACCEPTED, network_globals, @@ -624,7 +625,6 @@ pub async fn equivocation_consensus_late_equivocation() { let validator_count = 64; let num_initial: u64 = 31; let tester = InteractiveTester::::new(None, validator_count).await; - let test_logger = tester.harness.logger().clone(); // Create some chain depth. tester.harness.advance_slot(); @@ -657,10 +657,10 @@ pub async fn equivocation_consensus_late_equivocation() { ); assert_ne!(block_a.state_root(), block_b.state_root()); - let gossip_block_b = block_b.into_gossip_verified_block(&tester.harness.chain); + let gossip_block_b = block_b.into_gossip_verified_block(&tester.harness.chain, CGC); assert!(gossip_block_b.is_ok()); - let gossip_block_a = block_a.into_gossip_verified_block(&tester.harness.chain); + let gossip_block_a = block_a.into_gossip_verified_block(&tester.harness.chain, CGC); assert!(gossip_block_a.is_err()); let channel = tokio::sync::mpsc::unbounded_channel(); @@ -671,7 +671,6 @@ pub async fn equivocation_consensus_late_equivocation() { ProvenancedBlock::local(gossip_block_b.unwrap(), blobs_b), tester.harness.chain, &channel.0, - test_logger, validation_level, StatusCode::ACCEPTED, network_globals, @@ -1236,7 +1235,6 @@ pub async fn blinded_equivocation_consensus_late_equivocation() { let validator_count = 64; let num_initial: u64 = 31; let tester = InteractiveTester::::new(None, validator_count).await; - let test_logger = tester.harness.logger().clone(); // Create some chain depth. tester.harness.advance_slot(); @@ -1276,7 +1274,6 @@ pub async fn blinded_equivocation_consensus_late_equivocation() { tester.harness.chain.clone(), block_a.canonical_root(), Arc::new(block_a), - test_logger.clone(), ) .await .unwrap(); @@ -1284,7 +1281,6 @@ pub async fn blinded_equivocation_consensus_late_equivocation() { tester.harness.chain.clone(), block_b.canonical_root(), block_b.clone(), - test_logger.clone(), ) .await .unwrap(); @@ -1298,9 +1294,9 @@ pub async fn blinded_equivocation_consensus_late_equivocation() { ProvenancedBlock::Builder(b, _, _) => b, }; - let gossip_block_b = GossipVerifiedBlock::new(inner_block_b, &tester.harness.chain); + let gossip_block_b = GossipVerifiedBlock::new(inner_block_b, &tester.harness.chain, CGC); assert!(gossip_block_b.is_ok()); - let gossip_block_a = GossipVerifiedBlock::new(inner_block_a, &tester.harness.chain); + let gossip_block_a = GossipVerifiedBlock::new(inner_block_a, &tester.harness.chain, CGC); assert!(gossip_block_a.is_err()); let channel = tokio::sync::mpsc::unbounded_channel(); @@ -1310,7 +1306,6 @@ pub async fn blinded_equivocation_consensus_late_equivocation() { block_b, tester.harness.chain, &channel.0, - test_logger, validation_level, StatusCode::ACCEPTED, network_globals, @@ -1403,7 +1398,7 @@ pub async fn block_seen_on_gossip_without_blobs() { // Simulate the block being seen on gossip. block .clone() - .into_gossip_verified_block(&tester.harness.chain) + .into_gossip_verified_block(&tester.harness.chain, CGC) .unwrap(); // It should not yet be added to fork choice because blobs have not been seen. @@ -1472,7 +1467,7 @@ pub async fn block_seen_on_gossip_with_some_blobs() { // Simulate the block being seen on gossip. block .clone() - .into_gossip_verified_block(&tester.harness.chain) + .into_gossip_verified_block(&tester.harness.chain, CGC) .unwrap(); // Simulate some of the blobs being seen on gossip. diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 7e9d1e49fb0..5c9504d4a58 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -4,7 +4,6 @@ use beacon_chain::{ BeaconChain, ChainConfig, StateSkipConfig, WhenSlotSkipped, }; use either::Either; -use eth2::lighthouse::StandardBlockReward; use eth2::{ mixin::{RequestAccept, ResponseForkName, ResponseOptional}, reqwest::RequestBuilder, @@ -27,7 +26,6 @@ use http_api::{ BlockId, StateId, }; use lighthouse_network::{types::SyncState, Enr, EnrExt, PeerId}; -use logging::test_logger; use network::NetworkReceivers; use operation_pool::attestation_storage::CheckpointKey; use proto_array::ExecutionStatus; @@ -137,7 +135,6 @@ impl ApiTester { reconstruct_historic_states: config.retain_historic_states, ..ChainConfig::default() }) - .logger(logging::test_logger()) .deterministic_keypairs(VALIDATOR_COUNT) .deterministic_withdrawal_keypairs(VALIDATOR_COUNT) .fresh_ephemeral_store() @@ -279,8 +276,6 @@ impl ApiTester { "precondition: justification" ); - let log = test_logger(); - let ApiServer { ctx, server, @@ -288,7 +283,7 @@ impl ApiTester { network_rx, local_enr, external_peer_id, - } = create_api_server(chain.clone(), &harness.runtime, log).await; + } = create_api_server(chain.clone(), &harness.runtime).await; harness.runtime.task_executor.spawn(server, "api_server"); @@ -377,7 +372,6 @@ impl ApiTester { let bls_to_execution_change = harness.make_bls_to_execution_change(4, Address::zero()); let chain = harness.chain.clone(); - let log = test_logger(); let ApiServer { ctx, @@ -386,7 +380,7 @@ impl ApiTester { network_rx, local_enr, external_peer_id, - } = create_api_server(chain.clone(), &harness.runtime, log).await; + } = create_api_server(chain.clone(), &harness.runtime).await; harness.runtime.task_executor.spawn(server, "api_server"); @@ -5845,19 +5839,6 @@ impl ApiTester { self } - pub async fn test_get_lighthouse_database_info(self) -> Self { - let info = self.client.get_lighthouse_database_info().await.unwrap(); - - assert_eq!(info.anchor, self.chain.store.get_anchor_info()); - assert_eq!(info.split, self.chain.store.get_split_info()); - assert_eq!( - info.schema_version, - store::metadata::CURRENT_SCHEMA_VERSION.as_u64() - ); - - self - } - pub async fn test_post_lighthouse_database_reconstruct(self) -> Self { let response = self .client @@ -7500,8 +7481,6 @@ async fn lighthouse_endpoints() { .await .test_get_lighthouse_staking() .await - .test_get_lighthouse_database_info() - .await .test_post_lighthouse_database_reconstruct() .await .test_post_lighthouse_liveness() diff --git a/beacon_node/http_metrics/Cargo.toml b/beacon_node/http_metrics/Cargo.toml index 9ad073439d1..e12053ac43b 100644 --- a/beacon_node/http_metrics/Cargo.toml +++ b/beacon_node/http_metrics/Cargo.toml @@ -10,12 +10,13 @@ beacon_chain = { workspace = true } health_metrics = { workspace = true } lighthouse_network = { workspace = true } lighthouse_version = { workspace = true } +logging = { workspace = true } malloc_utils = { workspace = true } metrics = { workspace = true } serde = { workspace = true } -slog = { workspace = true } slot_clock = { workspace = true } store = { workspace = true } +tracing = { workspace = true } warp = { workspace = true } warp_utils = { workspace = true } diff --git a/beacon_node/http_metrics/src/lib.rs b/beacon_node/http_metrics/src/lib.rs index 2895506c3b3..6cbb485d718 100644 --- a/beacon_node/http_metrics/src/lib.rs +++ b/beacon_node/http_metrics/src/lib.rs @@ -6,12 +6,13 @@ mod metrics; use beacon_chain::{BeaconChain, BeaconChainTypes}; use lighthouse_network::prometheus_client::registry::Registry; use lighthouse_version::version_with_platform; +use logging::crit; use serde::{Deserialize, Serialize}; -use slog::{crit, info, Logger}; use std::future::Future; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; use std::sync::Arc; +use tracing::info; use warp::{http::Response, Filter}; #[derive(Debug)] @@ -41,7 +42,6 @@ pub struct Context { pub db_path: Option, pub freezer_db_path: Option, pub gossipsub_registry: Option>, - pub log: Logger, } /// Configuration for the HTTP server. @@ -86,7 +86,6 @@ pub fn serve( shutdown: impl Future + Send + Sync + 'static, ) -> Result<(SocketAddr, impl Future), Error> { let config = &ctx.config; - let log = ctx.log.clone(); // Configure CORS. let cors_builder = { @@ -103,7 +102,7 @@ pub fn serve( // Sanity check. if !config.enabled { - crit!(log, "Cannot start disabled metrics HTTP server"); + crit!("Cannot start disabled metrics HTTP server"); return Err(Error::Other( "A disabled metrics server should not be started".to_string(), )); @@ -144,9 +143,8 @@ pub fn serve( )?; info!( - log, - "Metrics HTTP server started"; - "listen_address" => listening_socket.to_string(), + listen_address = listening_socket.to_string(), + "Metrics HTTP server started" ); Ok((listening_socket, server)) diff --git a/beacon_node/http_metrics/tests/tests.rs b/beacon_node/http_metrics/tests/tests.rs index d903e233fba..2de2fd96f86 100644 --- a/beacon_node/http_metrics/tests/tests.rs +++ b/beacon_node/http_metrics/tests/tests.rs @@ -1,6 +1,6 @@ use beacon_chain::test_utils::EphemeralHarnessType; use http_metrics::Config; -use logging::test_logger; +use logging::create_test_tracing_subscriber; use reqwest::header::HeaderValue; use reqwest::StatusCode; use std::net::{IpAddr, Ipv4Addr}; @@ -12,9 +12,8 @@ type Context = http_metrics::Context>; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn returns_200_ok() { + create_test_tracing_subscriber(); async { - let log = test_logger(); - let context = Arc::new(Context { config: Config { enabled: true, @@ -27,7 +26,6 @@ async fn returns_200_ok() { db_path: None, freezer_db_path: None, gossipsub_registry: None, - log, }); let ctx = context.clone(); diff --git a/beacon_node/lighthouse_network/Cargo.toml b/beacon_node/lighthouse_network/Cargo.toml index b16ccc2a8ca..4f1825af201 100644 --- a/beacon_node/lighthouse_network/Cargo.toml +++ b/beacon_node/lighthouse_network/Cargo.toml @@ -13,6 +13,7 @@ directory = { workspace = true } dirs = { workspace = true } discv5 = { workspace = true } either = { workspace = true } +eth2 = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } fnv = { workspace = true } @@ -23,6 +24,7 @@ itertools = { workspace = true } libp2p-mplex = "0.43" lighthouse_version = { workspace = true } local-ip-address = "0.6" +logging = { workspace = true } lru = { workspace = true } lru_cache = { workspace = true } metrics = { workspace = true } @@ -32,7 +34,6 @@ rand = { workspace = true } regex = { workspace = true } serde = { workspace = true } sha2 = { workspace = true } -slog = { workspace = true } smallvec = { workspace = true } snap = { workspace = true } ssz_types = { workspace = true } @@ -43,13 +44,12 @@ tiny-keccak = "2" tokio = { workspace = true } tokio-io-timeout = "1" tokio-util = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } types = { workspace = true } unsigned-varint = { version = "0.8", features = ["codec"] } unused_port = { workspace = true } -# Local dependencies -void = "1.0.2" - [dependencies.libp2p] version = "0.55" default-features = false @@ -60,8 +60,6 @@ async-channel = { workspace = true } logging = { workspace = true } quickcheck = { workspace = true } quickcheck_macros = { workspace = true } -slog-async = { workspace = true } -slog-term = { workspace = true } tempfile = { workspace = true } [features] diff --git a/beacon_node/lighthouse_network/gossipsub/CHANGELOG.md b/beacon_node/lighthouse_network/gossipsub/CHANGELOG.md deleted file mode 100644 index aba85f61842..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/CHANGELOG.md +++ /dev/null @@ -1,386 +0,0 @@ -## 0.5 Sigma Prime fork -- Remove the beta tag from the v1.2 upgrade. - See [PR 6344](https://github.com/sigp/lighthouse/pull/6344) - -- Correct state inconsistencies with the mesh and connected peers due to the fanout mapping. - See [PR 6244](https://github.com/sigp/lighthouse/pull/6244) - -- Implement IDONTWANT messages as per [spec](https://github.com/libp2p/specs/pull/548). - See [PR 5422](https://github.com/sigp/lighthouse/pull/5422) - -- Attempt to publish to at least mesh_n peers when publishing a message when flood publish is disabled. - See [PR 5357](https://github.com/sigp/lighthouse/pull/5357). -- Drop `Publish` and `Forward` gossipsub stale messages when polling ConnectionHandler. - See [PR 5175](https://github.com/sigp/lighthouse/pull/5175). -- Apply back pressure by setting a limit in the ConnectionHandler message queue. - See [PR 5066](https://github.com/sigp/lighthouse/pull/5066). - -## 0.46.1 - -- Deprecate `Rpc` in preparation for removing it from the public API because it is an internal type. - See [PR 4833](https://github.com/libp2p/rust-libp2p/pull/4833). - -## 0.46.0 - -- Remove `fast_message_id_fn` mechanism from `Config`. - See [PR 4285](https://github.com/libp2p/rust-libp2p/pull/4285). -- Remove deprecated `gossipsub::Config::idle_timeout` in favor of `SwarmBuilder::idle_connection_timeout`. - See [PR 4642](https://github.com/libp2p/rust-libp2p/pull/4642). -- Return typed error from config builder. - See [PR 4445](https://github.com/libp2p/rust-libp2p/pull/4445). -- Process outbound stream before inbound stream in `EnabledHandler::poll(..)`. - See [PR 4778](https://github.com/libp2p/rust-libp2p/pull/4778). - -## 0.45.2 - -- Deprecate `gossipsub::Config::idle_timeout` in favor of `SwarmBuilder::idle_connection_timeout`. - See [PR 4648]. - - - -[PR 4648]: (https://github.com/libp2p/rust-libp2p/pull/4648) - - - -## 0.45.1 - -- Add getter function to o btain `TopicScoreParams`. - See [PR 4231]. - -[PR 4231]: https://github.com/libp2p/rust-libp2p/pull/4231 - -## 0.45.0 - -- Raise MSRV to 1.65. - See [PR 3715]. -- Remove deprecated items. See [PR 3862]. - -[PR 3715]: https://github.com/libp2p/rust-libp2p/pull/3715 -[PR 3862]: https://github.com/libp2p/rust-libp2p/pull/3862 - -## 0.44.4 - -- Deprecate `metrics`, `protocol`, `subscription_filter`, `time_cache` modules to make them private. See [PR 3777]. -- Honor the `gossipsub::Config::support_floodsub` in all cases. - Previously, it was ignored when a custom protocol id was set via `gossipsub::Config::protocol_id`. - See [PR 3837]. - -[PR 3777]: https://github.com/libp2p/rust-libp2p/pull/3777 -[PR 3837]: https://github.com/libp2p/rust-libp2p/pull/3837 - -## 0.44.3 - -- Fix erroneously duplicate message IDs. See [PR 3716]. - -- Gracefully disable handler on stream errors. Deprecate a few variants of `HandlerError`. - See [PR 3625]. - -[PR 3716]: https://github.com/libp2p/rust-libp2p/pull/3716 -[PR 3625]: https://github.com/libp2p/rust-libp2p/pull/3325 - -## 0.44.2 - -- Signed messages now use sequential integers in the sequence number field. - See [PR 3551]. - -[PR 3551]: https://github.com/libp2p/rust-libp2p/pull/3551 - -## 0.44.1 - -- Migrate from `prost` to `quick-protobuf`. This removes `protoc` dependency. See [PR 3312]. - -[PR 3312]: https://github.com/libp2p/rust-libp2p/pull/3312 - -## 0.44.0 - -- Update to `prometheus-client` `v0.19.0`. See [PR 3207]. - -- Update to `libp2p-core` `v0.39.0`. - -- Update to `libp2p-swarm` `v0.42.0`. - -- Initialize `ProtocolConfig` via `GossipsubConfig`. See [PR 3381]. - -- Rename types as per [discussion 2174]. - `Gossipsub` has been renamed to `Behaviour`. - The `Gossipsub` prefix has been removed from various types like `GossipsubConfig` or `GossipsubMessage`. - It is preferred to import the gossipsub protocol as a module (`use libp2p::gossipsub;`), and refer to its types via `gossipsub::`. - For example: `gossipsub::Behaviour` or `gossipsub::RawMessage`. See [PR 3303]. - -[PR 3207]: https://github.com/libp2p/rust-libp2p/pull/3207/ -[PR 3303]: https://github.com/libp2p/rust-libp2p/pull/3303/ -[PR 3381]: https://github.com/libp2p/rust-libp2p/pull/3381/ -[discussion 2174]: https://github.com/libp2p/rust-libp2p/discussions/2174 - -## 0.43.0 - -- Update to `libp2p-core` `v0.38.0`. - -- Update to `libp2p-swarm` `v0.41.0`. - -- Update to `prost-codec` `v0.3.0`. - -- Refactoring GossipsubCodec to use common protobuf Codec. See [PR 3070]. - -- Replace `Gossipsub`'s `NetworkBehaviour` implementation `inject_*` methods with the new `on_*` methods. - See [PR 3011]. - -- Replace `GossipsubHandler`'s `ConnectionHandler` implementation `inject_*` methods with the new `on_*` methods. - See [PR 3085]. - -- Update `rust-version` to reflect the actual MSRV: 1.62.0. See [PR 3090]. - -[PR 3085]: https://github.com/libp2p/rust-libp2p/pull/3085 -[PR 3070]: https://github.com/libp2p/rust-libp2p/pull/3070 -[PR 3011]: https://github.com/libp2p/rust-libp2p/pull/3011 -[PR 3090]: https://github.com/libp2p/rust-libp2p/pull/3090 - -## 0.42.0 - -- Bump rand to 0.8 and quickcheck to 1. See [PR 2857]. - -- Update to `libp2p-core` `v0.37.0`. - -- Update to `libp2p-swarm` `v0.40.0`. - -[PR 2857]: https://github.com/libp2p/rust-libp2p/pull/2857 - -## 0.41.0 - -- Update to `libp2p-swarm` `v0.39.0`. - -- Update to `libp2p-core` `v0.36.0`. - -- Allow publishing with any `impl Into` as a topic. See [PR 2862]. - -[PR 2862]: https://github.com/libp2p/rust-libp2p/pull/2862 - -## 0.40.0 - -- Update prost requirement from 0.10 to 0.11 which no longer installs the protoc Protobuf compiler. - Thus you will need protoc installed locally. See [PR 2788]. - -- Update to `libp2p-swarm` `v0.38.0`. - -- Update to `libp2p-core` `v0.35.0`. - -- Update to `prometheus-client` `v0.18.0`. See [PR 2822]. - -[PR 2822]: https://github.com/libp2p/rust-libp2p/pull/2761/ -[PR 2788]: https://github.com/libp2p/rust-libp2p/pull/2788 - -## 0.39.0 - -- Update to `libp2p-core` `v0.34.0`. - -- Update to `libp2p-swarm` `v0.37.0`. - -- Allow for custom protocol ID via `GossipsubConfigBuilder::protocol_id()`. See [PR 2718]. - -[PR 2718]: https://github.com/libp2p/rust-libp2p/pull/2718/ - -## 0.38.1 - -- Fix duplicate connection id. See [PR 2702]. - -[PR 2702]: https://github.com/libp2p/rust-libp2p/pull/2702 - -## 0.38.0 - -- Update to `libp2p-core` `v0.33.0`. - -- Update to `libp2p-swarm` `v0.36.0`. - -- changed `TimeCache::contains_key` and `DuplicateCache::contains` to immutable methods. See [PR 2620]. - -- Update to `prometheus-client` `v0.16.0`. See [PR 2631]. - -[PR 2620]: https://github.com/libp2p/rust-libp2p/pull/2620 -[PR 2631]: https://github.com/libp2p/rust-libp2p/pull/2631 - -## 0.37.0 - -- Update to `libp2p-swarm` `v0.35.0`. - -- Fix gossipsub metric (see [PR 2558]). - -- Allow the user to set the buckets for the score histogram, and to adjust them from the score thresholds. See [PR 2595]. - -[PR 2558]: https://github.com/libp2p/rust-libp2p/pull/2558 -[PR 2595]: https://github.com/libp2p/rust-libp2p/pull/2595 - -## 0.36.0 [2022-02-22] - -- Update to `libp2p-core` `v0.32.0`. - -- Update to `libp2p-swarm` `v0.34.0`. - -- Move from `open-metrics-client` to `prometheus-client` (see [PR 2442]). - -- Emit gossip of all non empty topics (see [PR 2481]). - -- Merge NetworkBehaviour's inject_\* paired methods (see [PR 2445]). - -- Revert to wasm-timer (see [PR 2506]). - -- Do not overwrite msg's peers if put again into mcache (see [PR 2493]). - -[PR 2442]: https://github.com/libp2p/rust-libp2p/pull/2442 -[PR 2481]: https://github.com/libp2p/rust-libp2p/pull/2481 -[PR 2445]: https://github.com/libp2p/rust-libp2p/pull/2445 -[PR 2506]: https://github.com/libp2p/rust-libp2p/pull/2506 -[PR 2493]: https://github.com/libp2p/rust-libp2p/pull/2493 - -## 0.35.0 [2022-01-27] - -- Update dependencies. - -- Migrate to Rust edition 2021 (see [PR 2339]). - -- Add metrics for network and configuration performance analysis (see [PR 2346]). - -- Improve bandwidth performance by tracking IWANTs and reducing duplicate sends - (see [PR 2327]). - -- Implement `Serialize` and `Deserialize` for `MessageId` and `FastMessageId` (see [PR 2408]) - -- Fix `GossipsubConfigBuilder::build()` requiring `&self` to live for `'static` (see [PR 2409]) - -- Implement Unsubscribe backoff as per [libp2p specs PR 383] (see [PR 2403]). - -[PR 2346]: https://github.com/libp2p/rust-libp2p/pull/2346 -[PR 2339]: https://github.com/libp2p/rust-libp2p/pull/2339 -[PR 2327]: https://github.com/libp2p/rust-libp2p/pull/2327 -[PR 2408]: https://github.com/libp2p/rust-libp2p/pull/2408 -[PR 2409]: https://github.com/libp2p/rust-libp2p/pull/2409 -[PR 2403]: https://github.com/libp2p/rust-libp2p/pull/2403 -[libp2p specs PR 383]: https://github.com/libp2p/specs/pull/383 - -## 0.34.0 [2021-11-16] - -- Add topic and mesh metrics (see [PR 2316]). - -- Fix bug in internal peer's topics tracking (see [PR 2325]). - -- Use `instant` and `futures-timer` instead of `wasm-timer` (see [PR 2245]). - -- Update dependencies. - -[PR 2245]: https://github.com/libp2p/rust-libp2p/pull/2245 -[PR 2325]: https://github.com/libp2p/rust-libp2p/pull/2325 -[PR 2316]: https://github.com/libp2p/rust-libp2p/pull/2316 - -## 0.33.0 [2021-11-01] - -- Add an event to register peers that do not support the gossipsub protocol - [PR 2241](https://github.com/libp2p/rust-libp2p/pull/2241) - -- Make default features of `libp2p-core` optional. - [PR 2181](https://github.com/libp2p/rust-libp2p/pull/2181) - -- Improve internal peer tracking. - [PR 2175](https://github.com/libp2p/rust-libp2p/pull/2175) - -- Update dependencies. - -- Allow `message_id_fn`s to accept closures that capture variables. - [PR 2103](https://github.com/libp2p/rust-libp2p/pull/2103) - -- Implement std::error::Error for error types. - [PR 2254](https://github.com/libp2p/rust-libp2p/pull/2254) - -## 0.32.0 [2021-07-12] - -- Update dependencies. - -- Reduce log levels across the crate to lessen noisiness of libp2p-gossipsub (see [PR 2101]). - -[PR 2101]: https://github.com/libp2p/rust-libp2p/pull/2101 - -## 0.31.0 [2021-05-17] - -- Keep connections to peers in a mesh alive. Allow closing idle connections to peers not in a mesh - [PR-2043]. - -[PR-2043]: https://github.com/libp2p/rust-libp2p/pull/2043https://github.com/libp2p/rust-libp2p/pull/2043 - -## 0.30.1 [2021-04-27] - -- Remove `regex-filter` feature flag thus always enabling `regex::RegexSubscriptionFilter` [PR - 2056](https://github.com/libp2p/rust-libp2p/pull/2056). - -## 0.30.0 [2021-04-13] - -- Update `libp2p-swarm`. - -- Update dependencies. - -## 0.29.0 [2021-03-17] - -- Update `libp2p-swarm`. - -- Update dependencies. - -## 0.28.0 [2021-02-15] - -- Prevent non-published messages being added to caches. - [PR 1930](https://github.com/libp2p/rust-libp2p/pull/1930) - -- Update dependencies. - -## 0.27.0 [2021-01-12] - -- Update dependencies. - -- Implement Gossipsub v1.1 specification. - [PR 1720](https://github.com/libp2p/rust-libp2p/pull/1720) - -## 0.26.0 [2020-12-17] - -- Update `libp2p-swarm` and `libp2p-core`. - -## 0.25.0 [2020-11-25] - -- Update `libp2p-swarm` and `libp2p-core`. - -## 0.24.0 [2020-11-09] - -- Update dependencies. - -## 0.23.0 [2020-10-16] - -- Update dependencies. - -## 0.22.0 [2020-09-09] - -- Update `libp2p-swarm` and `libp2p-core`. - -## 0.21.0 [2020-08-18] - -- Add public API to list topics and peers. [PR 1677](https://github.com/libp2p/rust-libp2p/pull/1677). - -- Add message signing and extended privacy/validation configurations. [PR 1583](https://github.com/libp2p/rust-libp2p/pull/1583). - -- `Debug` instance for `Gossipsub`. [PR 1673](https://github.com/libp2p/rust-libp2p/pull/1673). - -- Bump `libp2p-core` and `libp2p-swarm` dependency. - -## 0.20.0 [2020-07-01] - -- Updated dependencies. - -## 0.19.3 [2020-06-23] - -- Maintenance release fixing linter warnings. - -## 0.19.2 [2020-06-22] - -- Updated dependencies. diff --git a/beacon_node/lighthouse_network/gossipsub/Cargo.toml b/beacon_node/lighthouse_network/gossipsub/Cargo.toml deleted file mode 100644 index 239caae47ad..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/Cargo.toml +++ /dev/null @@ -1,49 +0,0 @@ -[package] -name = "gossipsub" -edition = "2021" -description = "Sigma prime's version of Gossipsub protocol for libp2p" -version = "0.5.0" -authors = ["Age Manning "] -license = "MIT" -repository = "https://github.com/sigp/lighthouse/" -keywords = ["peer-to-peer", "libp2p", "networking"] -categories = ["network-programming", "asynchronous"] - -[features] -wasm-bindgen = ["getrandom/js", "futures-timer/wasm-bindgen"] -rsa = [] - -[dependencies] -async-channel = { workspace = true } -asynchronous-codec = "0.7.0" -base64 = "0.21.7" -byteorder = "1.5.0" -bytes = "1.5" -either = "1.9" -fnv = "1.0.7" -futures = "0.3.30" -futures-timer = "3.0.2" -getrandom = "0.2.12" -hashlink = { workspace = true } -hex_fmt = "0.3.0" -libp2p = { version = "0.55", default-features = false } -prometheus-client = "0.22.0" -quick-protobuf = "0.8" -quick-protobuf-codec = "0.3" -rand = "0.8" -regex = "1.10.3" -serde = { version = "1", optional = true, features = ["derive"] } -sha2 = "0.10.8" -tracing = "0.1.37" -void = "1.0.2" -web-time = "1.1.0" - -[dev-dependencies] -quickcheck = { workspace = true } - -# Passing arguments to the docsrs builder in order to properly document cfg's. -# More information: https://docs.rs/about/builds#cross-compiling -[package.metadata.docs.rs] -all-features = true -rustdoc-args = ["--cfg", "docsrs"] -rustc-args = ["--cfg", "docsrs"] diff --git a/beacon_node/lighthouse_network/gossipsub/src/backoff.rs b/beacon_node/lighthouse_network/gossipsub/src/backoff.rs deleted file mode 100644 index 0d77e2cd0f9..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/backoff.rs +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright 2020 Sigma Prime Pty Ltd. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -//! Data structure for efficiently storing known back-off's when pruning peers. -use crate::topic::TopicHash; -use libp2p::identity::PeerId; -use std::collections::{ - hash_map::{Entry, HashMap}, - HashSet, -}; -use std::time::Duration; -use web_time::Instant; - -#[derive(Copy, Clone)] -struct HeartbeatIndex(usize); - -/// Stores backoffs in an efficient manner. -pub(crate) struct BackoffStorage { - /// Stores backoffs and the index in backoffs_by_heartbeat per peer per topic. - backoffs: HashMap>, - /// Stores peer topic pairs per heartbeat (this is cyclic the current index is - /// heartbeat_index). - backoffs_by_heartbeat: Vec>, - /// The index in the backoffs_by_heartbeat vector corresponding to the current heartbeat. - heartbeat_index: HeartbeatIndex, - /// The heartbeat interval duration from the config. - heartbeat_interval: Duration, - /// Backoff slack from the config. - backoff_slack: u32, -} - -impl BackoffStorage { - fn heartbeats(d: &Duration, heartbeat_interval: &Duration) -> usize { - d.as_nanos().div_ceil(heartbeat_interval.as_nanos()) as usize - } - - pub(crate) fn new( - prune_backoff: &Duration, - heartbeat_interval: Duration, - backoff_slack: u32, - ) -> BackoffStorage { - // We add one additional slot for partial heartbeat - let max_heartbeats = - Self::heartbeats(prune_backoff, &heartbeat_interval) + backoff_slack as usize + 1; - BackoffStorage { - backoffs: HashMap::new(), - backoffs_by_heartbeat: vec![HashSet::new(); max_heartbeats], - heartbeat_index: HeartbeatIndex(0), - heartbeat_interval, - backoff_slack, - } - } - - /// Updates the backoff for a peer (if there is already a more restrictive backoff then this call - /// doesn't change anything). - pub(crate) fn update_backoff(&mut self, topic: &TopicHash, peer: &PeerId, time: Duration) { - let instant = Instant::now() + time; - let insert_into_backoffs_by_heartbeat = - |heartbeat_index: HeartbeatIndex, - backoffs_by_heartbeat: &mut Vec>, - heartbeat_interval, - backoff_slack| { - let pair = (topic.clone(), *peer); - let index = (heartbeat_index.0 - + Self::heartbeats(&time, heartbeat_interval) - + backoff_slack as usize) - % backoffs_by_heartbeat.len(); - backoffs_by_heartbeat[index].insert(pair); - HeartbeatIndex(index) - }; - match self.backoffs.entry(topic.clone()).or_default().entry(*peer) { - Entry::Occupied(mut o) => { - let (backoff, index) = o.get(); - if backoff < &instant { - let pair = (topic.clone(), *peer); - if let Some(s) = self.backoffs_by_heartbeat.get_mut(index.0) { - s.remove(&pair); - } - let index = insert_into_backoffs_by_heartbeat( - self.heartbeat_index, - &mut self.backoffs_by_heartbeat, - &self.heartbeat_interval, - self.backoff_slack, - ); - o.insert((instant, index)); - } - } - Entry::Vacant(v) => { - let index = insert_into_backoffs_by_heartbeat( - self.heartbeat_index, - &mut self.backoffs_by_heartbeat, - &self.heartbeat_interval, - self.backoff_slack, - ); - v.insert((instant, index)); - } - }; - } - - /// Checks if a given peer is backoffed for the given topic. This method respects the - /// configured BACKOFF_SLACK and may return true even if the backup is already over. - /// It is guaranteed to return false if the backoff is not over and eventually if enough time - /// passed true if the backoff is over. - /// - /// This method should be used for deciding if we can already send a GRAFT to a previously - /// backoffed peer. - pub(crate) fn is_backoff_with_slack(&self, topic: &TopicHash, peer: &PeerId) -> bool { - self.backoffs - .get(topic) - .is_some_and(|m| m.contains_key(peer)) - } - - pub(crate) fn get_backoff_time(&self, topic: &TopicHash, peer: &PeerId) -> Option { - Self::get_backoff_time_from_backoffs(&self.backoffs, topic, peer) - } - - fn get_backoff_time_from_backoffs( - backoffs: &HashMap>, - topic: &TopicHash, - peer: &PeerId, - ) -> Option { - backoffs - .get(topic) - .and_then(|m| m.get(peer).map(|(i, _)| *i)) - } - - /// Applies a heartbeat. That should be called regularly in intervals of length - /// `heartbeat_interval`. - pub(crate) fn heartbeat(&mut self) { - // Clean up backoffs_by_heartbeat - if let Some(s) = self.backoffs_by_heartbeat.get_mut(self.heartbeat_index.0) { - let backoffs = &mut self.backoffs; - let slack = self.heartbeat_interval * self.backoff_slack; - let now = Instant::now(); - s.retain(|(topic, peer)| { - let keep = match Self::get_backoff_time_from_backoffs(backoffs, topic, peer) { - Some(backoff_time) => backoff_time + slack > now, - None => false, - }; - if !keep { - //remove from backoffs - if let Entry::Occupied(mut m) = backoffs.entry(topic.clone()) { - if m.get_mut().remove(peer).is_some() && m.get().is_empty() { - m.remove(); - } - } - } - - keep - }); - } - - // Increase heartbeat index - self.heartbeat_index = - HeartbeatIndex((self.heartbeat_index.0 + 1) % self.backoffs_by_heartbeat.len()); - } -} diff --git a/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs b/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs deleted file mode 100644 index 7eb35cc49bc..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs +++ /dev/null @@ -1,3672 +0,0 @@ -// Copyright 2020 Sigma Prime Pty Ltd. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -use std::{ - cmp::{max, Ordering}, - collections::HashSet, - collections::VecDeque, - collections::{BTreeSet, HashMap}, - fmt, - net::IpAddr, - task::{Context, Poll}, - time::Duration, -}; - -use futures::FutureExt; -use hashlink::LinkedHashMap; -use prometheus_client::registry::Registry; -use rand::{seq::SliceRandom, thread_rng}; - -use libp2p::core::{ - multiaddr::Protocol::{Ip4, Ip6}, - transport::PortUse, - Endpoint, Multiaddr, -}; -use libp2p::identity::Keypair; -use libp2p::identity::PeerId; -use libp2p::swarm::{ - behaviour::{AddressChange, ConnectionClosed, ConnectionEstablished, FromSwarm}, - dial_opts::DialOpts, - ConnectionDenied, ConnectionId, NetworkBehaviour, NotifyHandler, THandler, THandlerInEvent, - THandlerOutEvent, ToSwarm, -}; -use web_time::{Instant, SystemTime}; - -use crate::types::IDontWant; - -use super::gossip_promises::GossipPromises; -use super::handler::{Handler, HandlerEvent, HandlerIn}; -use super::mcache::MessageCache; -use super::metrics::{Churn, Config as MetricsConfig, Inclusion, Metrics, Penalty}; -use super::peer_score::{PeerScore, PeerScoreParams, PeerScoreThresholds, RejectReason}; -use super::protocol::SIGNING_PREFIX; -use super::rpc_proto::proto; -use super::subscription_filter::{AllowAllSubscriptionFilter, TopicSubscriptionFilter}; -use super::time_cache::DuplicateCache; -use super::topic::{Hasher, Topic, TopicHash}; -use super::transform::{DataTransform, IdentityTransform}; -use super::types::{ - ControlAction, FailedMessages, Message, MessageAcceptance, MessageId, PeerInfo, RawMessage, - Subscription, SubscriptionAction, -}; -use super::types::{Graft, IHave, IWant, PeerConnections, PeerKind, Prune}; -use super::{backoff::BackoffStorage, types::RpcSender}; -use super::{ - config::{Config, ValidationMode}, - types::RpcOut, -}; -use super::{PublishError, SubscriptionError, TopicScoreParams, ValidationError}; -use futures_timer::Delay; -use quick_protobuf::{MessageWrite, Writer}; -use std::{cmp::Ordering::Equal, fmt::Debug}; - -#[cfg(test)] -mod tests; - -/// IDONTWANT cache capacity. -const IDONTWANT_CAP: usize = 10_000; - -/// IDONTWANT timeout before removal. -const IDONTWANT_TIMEOUT: Duration = Duration::new(3, 0); - -/// Determines if published messages should be signed or not. -/// -/// Without signing, a number of privacy preserving modes can be selected. -/// -/// NOTE: The default validation settings are to require signatures. The [`ValidationMode`] -/// should be updated in the [`Config`] to allow for unsigned messages. -#[derive(Clone)] -pub enum MessageAuthenticity { - /// Message signing is enabled. The author will be the owner of the key and the sequence number - /// will be linearly increasing. - Signed(Keypair), - /// Message signing is disabled. - /// - /// The specified [`PeerId`] will be used as the author of all published messages. The sequence - /// number will be randomized. - Author(PeerId), - /// Message signing is disabled. - /// - /// A random [`PeerId`] will be used when publishing each message. The sequence number will be - /// randomized. - RandomAuthor, - /// Message signing is disabled. - /// - /// The author of the message and the sequence numbers are excluded from the message. - /// - /// NOTE: Excluding these fields may make these messages invalid by other nodes who - /// enforce validation of these fields. See [`ValidationMode`] in the [`Config`] - /// for how to customise this for rust-libp2p gossipsub. A custom `message_id` - /// function will need to be set to prevent all messages from a peer being filtered - /// as duplicates. - Anonymous, -} - -impl MessageAuthenticity { - /// Returns true if signing is enabled. - pub fn is_signing(&self) -> bool { - matches!(self, MessageAuthenticity::Signed(_)) - } - - pub fn is_anonymous(&self) -> bool { - matches!(self, MessageAuthenticity::Anonymous) - } -} - -/// Event that can be emitted by the gossipsub behaviour. -#[derive(Debug)] -pub enum Event { - /// A message has been received. - Message { - /// The peer that forwarded us this message. - propagation_source: PeerId, - /// The [`MessageId`] of the message. This should be referenced by the application when - /// validating a message (if required). - message_id: MessageId, - /// The decompressed message itself. - message: Message, - }, - /// A remote subscribed to a topic. - Subscribed { - /// Remote that has subscribed. - peer_id: PeerId, - /// The topic it has subscribed to. - topic: TopicHash, - }, - /// A remote unsubscribed from a topic. - Unsubscribed { - /// Remote that has unsubscribed. - peer_id: PeerId, - /// The topic it has subscribed from. - topic: TopicHash, - }, - /// A peer that does not support gossipsub has connected. - GossipsubNotSupported { peer_id: PeerId }, - /// A peer is not able to download messages in time. - SlowPeer { - /// The peer_id - peer_id: PeerId, - /// The types and amounts of failed messages that are occurring for this peer. - failed_messages: FailedMessages, - }, -} - -/// A data structure for storing configuration for publishing messages. See [`MessageAuthenticity`] -/// for further details. -#[allow(clippy::large_enum_variant)] -enum PublishConfig { - Signing { - keypair: Keypair, - author: PeerId, - inline_key: Option>, - last_seq_no: SequenceNumber, - }, - Author(PeerId), - RandomAuthor, - Anonymous, -} - -/// A strictly linearly increasing sequence number. -/// -/// We start from the current time as unix timestamp in milliseconds. -#[derive(Debug)] -struct SequenceNumber(u64); - -impl SequenceNumber { - fn new() -> Self { - let unix_timestamp = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("time to be linear") - .as_nanos(); - - Self(unix_timestamp as u64) - } - - fn next(&mut self) -> u64 { - self.0 = self - .0 - .checked_add(1) - .expect("to not exhaust u64 space for sequence numbers"); - - self.0 - } -} - -impl PublishConfig { - pub(crate) fn get_own_id(&self) -> Option<&PeerId> { - match self { - Self::Signing { author, .. } => Some(author), - Self::Author(author) => Some(author), - _ => None, - } - } -} - -impl From for PublishConfig { - fn from(authenticity: MessageAuthenticity) -> Self { - match authenticity { - MessageAuthenticity::Signed(keypair) => { - let public_key = keypair.public(); - let key_enc = public_key.encode_protobuf(); - let key = if key_enc.len() <= 42 { - // The public key can be inlined in [`rpc_proto::proto::::Message::from`], so we don't include it - // specifically in the [`rpc_proto::proto::Message::key`] field. - None - } else { - // Include the protobuf encoding of the public key in the message. - Some(key_enc) - }; - - PublishConfig::Signing { - keypair, - author: public_key.to_peer_id(), - inline_key: key, - last_seq_no: SequenceNumber::new(), - } - } - MessageAuthenticity::Author(peer_id) => PublishConfig::Author(peer_id), - MessageAuthenticity::RandomAuthor => PublishConfig::RandomAuthor, - MessageAuthenticity::Anonymous => PublishConfig::Anonymous, - } - } -} - -/// Network behaviour that handles the gossipsub protocol. -/// -/// NOTE: Initialisation requires a [`MessageAuthenticity`] and [`Config`] instance. If -/// message signing is disabled, the [`ValidationMode`] in the config should be adjusted to an -/// appropriate level to accept unsigned messages. -/// -/// The DataTransform trait allows applications to optionally add extra encoding/decoding -/// functionality to the underlying messages. This is intended for custom compression algorithms. -/// -/// The TopicSubscriptionFilter allows applications to implement specific filters on topics to -/// prevent unwanted messages being propagated and evaluated. -pub struct Behaviour { - /// Configuration providing gossipsub performance parameters. - config: Config, - - /// Events that need to be yielded to the outside when polling. - events: VecDeque>, - - /// Information used for publishing messages. - publish_config: PublishConfig, - - /// An LRU Time cache for storing seen messages (based on their ID). This cache prevents - /// duplicates from being propagated to the application and on the network. - duplicate_cache: DuplicateCache, - - /// A set of connected peers, indexed by their [`PeerId`] tracking both the [`PeerKind`] and - /// the set of [`ConnectionId`]s. - connected_peers: HashMap, - - /// A set of all explicit peers. These are peers that remain connected and we unconditionally - /// forward messages to, outside of the scoring system. - explicit_peers: HashSet, - - /// A list of peers that have been blacklisted by the user. - /// Messages are not sent to and are rejected from these peers. - blacklisted_peers: HashSet, - - /// Overlay network of connected peers - Maps topics to connected gossipsub peers. - mesh: HashMap>, - - /// Map of topics to list of peers that we publish to, but don't subscribe to. - fanout: HashMap>, - - /// The last publish time for fanout topics. - fanout_last_pub: HashMap, - - ///Storage for backoffs - backoffs: BackoffStorage, - - /// Message cache for the last few heartbeats. - mcache: MessageCache, - - /// Heartbeat interval stream. - heartbeat: Delay, - - /// Number of heartbeats since the beginning of time; this allows us to amortize some resource - /// clean up -- eg backoff clean up. - heartbeat_ticks: u64, - - /// We remember all peers we found through peer exchange, since those peers are not considered - /// as safe as randomly discovered outbound peers. This behaviour diverges from the go - /// implementation to avoid possible love bombing attacks in PX. When disconnecting peers will - /// be removed from this list which may result in a true outbound rediscovery. - px_peers: HashSet, - - /// Set of connected outbound peers (we only consider true outbound peers found through - /// discovery and not by PX). - outbound_peers: HashSet, - - /// Stores optional peer score data together with thresholds and decay interval. - peer_score: Option<(PeerScore, PeerScoreThresholds, Delay)>, - - /// Counts the number of `IHAVE` received from each peer since the last heartbeat. - count_received_ihave: HashMap, - - /// Counts the number of `IWANT` that we sent the each peer since the last heartbeat. - count_sent_iwant: HashMap, - - /// Short term cache for published message ids. This is used for penalizing peers sending - /// our own messages back if the messages are anonymous or use a random author. - published_message_ids: DuplicateCache, - - /// The filter used to handle message subscriptions. - subscription_filter: F, - - /// A general transformation function that can be applied to data received from the wire before - /// calculating the message-id and sending to the application. This is designed to allow the - /// user to implement arbitrary topic-based compression algorithms. - data_transform: D, - - /// Keep track of a set of internal metrics relating to gossipsub. - metrics: Option, - - /// Tracks the numbers of failed messages per peer-id. - failed_messages: HashMap, - - /// Tracks recently sent `IWANT` messages and checks if peers respond to them. - gossip_promises: GossipPromises, -} - -impl Behaviour -where - D: DataTransform + Default, - F: TopicSubscriptionFilter + Default, -{ - /// Creates a Gossipsub [`Behaviour`] struct given a set of parameters specified via a - /// [`Config`]. This has no subscription filter and uses no compression. - pub fn new(privacy: MessageAuthenticity, config: Config) -> Result { - Self::new_with_subscription_filter_and_transform( - privacy, - config, - None, - F::default(), - D::default(), - ) - } - - /// Creates a Gossipsub [`Behaviour`] struct given a set of parameters specified via a - /// [`Config`]. This has no subscription filter and uses no compression. - /// Metrics can be evaluated by passing a reference to a [`Registry`]. - pub fn new_with_metrics( - privacy: MessageAuthenticity, - config: Config, - metrics_registry: &mut Registry, - metrics_config: MetricsConfig, - ) -> Result { - Self::new_with_subscription_filter_and_transform( - privacy, - config, - Some((metrics_registry, metrics_config)), - F::default(), - D::default(), - ) - } -} - -impl Behaviour -where - D: DataTransform + Default, - F: TopicSubscriptionFilter, -{ - /// Creates a Gossipsub [`Behaviour`] struct given a set of parameters specified via a - /// [`Config`] and a custom subscription filter. - pub fn new_with_subscription_filter( - privacy: MessageAuthenticity, - config: Config, - metrics: Option<(&mut Registry, MetricsConfig)>, - subscription_filter: F, - ) -> Result { - Self::new_with_subscription_filter_and_transform( - privacy, - config, - metrics, - subscription_filter, - D::default(), - ) - } -} - -impl Behaviour -where - D: DataTransform, - F: TopicSubscriptionFilter + Default, -{ - /// Creates a Gossipsub [`Behaviour`] struct given a set of parameters specified via a - /// [`Config`] and a custom data transform. - pub fn new_with_transform( - privacy: MessageAuthenticity, - config: Config, - metrics: Option<(&mut Registry, MetricsConfig)>, - data_transform: D, - ) -> Result { - Self::new_with_subscription_filter_and_transform( - privacy, - config, - metrics, - F::default(), - data_transform, - ) - } -} - -impl Behaviour -where - D: DataTransform, - F: TopicSubscriptionFilter, -{ - /// Creates a Gossipsub [`Behaviour`] struct given a set of parameters specified via a - /// [`Config`] and a custom subscription filter and data transform. - pub fn new_with_subscription_filter_and_transform( - privacy: MessageAuthenticity, - config: Config, - metrics: Option<(&mut Registry, MetricsConfig)>, - subscription_filter: F, - data_transform: D, - ) -> Result { - // Set up the router given the configuration settings. - - // We do not allow configurations where a published message would also be rejected if it - // were received locally. - validate_config(&privacy, config.validation_mode())?; - - Ok(Behaviour { - metrics: metrics.map(|(registry, cfg)| Metrics::new(registry, cfg)), - events: VecDeque::new(), - publish_config: privacy.into(), - duplicate_cache: DuplicateCache::new(config.duplicate_cache_time()), - explicit_peers: HashSet::new(), - blacklisted_peers: HashSet::new(), - mesh: HashMap::new(), - fanout: HashMap::new(), - fanout_last_pub: HashMap::new(), - backoffs: BackoffStorage::new( - &config.prune_backoff(), - config.heartbeat_interval(), - config.backoff_slack(), - ), - mcache: MessageCache::new(config.history_gossip(), config.history_length()), - heartbeat: Delay::new(config.heartbeat_interval() + config.heartbeat_initial_delay()), - heartbeat_ticks: 0, - px_peers: HashSet::new(), - outbound_peers: HashSet::new(), - peer_score: None, - count_received_ihave: HashMap::new(), - count_sent_iwant: HashMap::new(), - connected_peers: HashMap::new(), - published_message_ids: DuplicateCache::new(config.published_message_ids_cache_time()), - config, - subscription_filter, - data_transform, - failed_messages: Default::default(), - gossip_promises: Default::default(), - }) - } -} - -impl Behaviour -where - D: DataTransform + Send + 'static, - F: TopicSubscriptionFilter + Send + 'static, -{ - /// Lists the hashes of the topics we are currently subscribed to. - pub fn topics(&self) -> impl Iterator { - self.mesh.keys() - } - - /// Lists all mesh peers for a certain topic hash. - pub fn mesh_peers(&self, topic_hash: &TopicHash) -> impl Iterator { - self.mesh.get(topic_hash).into_iter().flat_map(|x| x.iter()) - } - - pub fn all_mesh_peers(&self) -> impl Iterator { - let mut res = BTreeSet::new(); - for peers in self.mesh.values() { - res.extend(peers); - } - res.into_iter() - } - - /// Lists all known peers and their associated subscribed topics. - pub fn all_peers(&self) -> impl Iterator)> { - self.connected_peers - .iter() - .map(|(peer_id, peer)| (peer_id, peer.topics.iter().collect())) - } - - /// Lists all known peers and their associated protocol. - pub fn peer_protocol(&self) -> impl Iterator { - self.connected_peers.iter().map(|(k, v)| (k, &v.kind)) - } - - /// Returns the gossipsub score for a given peer, if one exists. - pub fn peer_score(&self, peer_id: &PeerId) -> Option { - self.peer_score - .as_ref() - .map(|(score, ..)| score.score(peer_id)) - } - - /// Subscribe to a topic. - /// - /// Returns [`Ok(true)`] if the subscription worked. Returns [`Ok(false)`] if we were already - /// subscribed. - pub fn subscribe(&mut self, topic: &Topic) -> Result { - tracing::debug!(%topic, "Subscribing to topic"); - let topic_hash = topic.hash(); - if !self.subscription_filter.can_subscribe(&topic_hash) { - return Err(SubscriptionError::NotAllowed); - } - - if self.mesh.contains_key(&topic_hash) { - tracing::debug!(%topic, "Topic is already in the mesh"); - return Ok(false); - } - - // send subscription request to all peers - for (peer_id, peer) in self.connected_peers.iter_mut() { - tracing::debug!(%peer_id, "Sending SUBSCRIBE to peer"); - - peer.sender.subscribe(topic_hash.clone()); - } - - // call JOIN(topic) - // this will add new peers to the mesh for the topic - self.join(&topic_hash); - tracing::debug!(%topic, "Subscribed to topic"); - Ok(true) - } - - /// Unsubscribes from a topic. - /// - /// Returns [`Ok(true)`] if we were subscribed to this topic. - pub fn unsubscribe(&mut self, topic: &Topic) -> Result { - tracing::debug!(%topic, "Unsubscribing from topic"); - let topic_hash = topic.hash(); - - if !self.mesh.contains_key(&topic_hash) { - tracing::debug!(topic=%topic_hash, "Already unsubscribed from topic"); - // we are not subscribed - return Ok(false); - } - - // announce to all peers - for (peer_id, peer) in self.connected_peers.iter_mut() { - tracing::debug!(%peer_id, "Sending UNSUBSCRIBE to peer"); - peer.sender.unsubscribe(topic_hash.clone()); - } - - // call LEAVE(topic) - // this will remove the topic from the mesh - self.leave(&topic_hash); - - tracing::debug!(topic=%topic_hash, "Unsubscribed from topic"); - Ok(true) - } - - /// Publishes a message with multiple topics to the network. - pub fn publish( - &mut self, - topic: impl Into, - data: impl Into>, - ) -> Result { - let data = data.into(); - let topic = topic.into(); - - // Transform the data before building a raw_message. - let transformed_data = self - .data_transform - .outbound_transform(&topic, data.clone())?; - - let raw_message = self.build_raw_message(topic, transformed_data)?; - - // calculate the message id from the un-transformed data - let msg_id = self.config.message_id(&Message { - source: raw_message.source, - data, // the uncompressed form - sequence_number: raw_message.sequence_number, - topic: raw_message.topic.clone(), - }); - - // check that the size doesn't exceed the max transmission size - if raw_message.raw_protobuf_len() > self.config.max_transmit_size() { - return Err(PublishError::MessageTooLarge); - } - - // Check the if the message has been published before - if self.duplicate_cache.contains(&msg_id) { - // This message has already been seen. We don't re-publish messages that have already - // been published on the network. - tracing::warn!( - message=%msg_id, - "Not publishing a message that has already been published" - ); - return Err(PublishError::Duplicate); - } - - tracing::trace!(message=%msg_id, "Publishing message"); - - let topic_hash = raw_message.topic.clone(); - - let mut peers_on_topic = self - .connected_peers - .iter() - .filter(|(_, p)| p.topics.contains(&topic_hash)) - .map(|(peer_id, _)| peer_id) - .peekable(); - - if peers_on_topic.peek().is_none() { - return Err(PublishError::InsufficientPeers); - } - - let mut recipient_peers = HashSet::new(); - - if self.config.flood_publish() { - // Forward to all peers above score and all explicit peers - recipient_peers.extend(peers_on_topic.filter(|p| { - self.explicit_peers.contains(*p) - || !self.score_below_threshold(p, |ts| ts.publish_threshold).0 - })); - } else { - match self.mesh.get(&topic_hash) { - // Mesh peers - Some(mesh_peers) => { - // We have a mesh set. We want to make sure to publish to at least `mesh_n` - // peers (if possible). - let needed_extra_peers = self.config.mesh_n().saturating_sub(mesh_peers.len()); - - if needed_extra_peers > 0 { - // We don't have `mesh_n` peers in our mesh, we will randomly select extras - // and publish to them. - - // Get a random set of peers that are appropriate to send messages too. - let peer_list = get_random_peers( - &self.connected_peers, - &topic_hash, - needed_extra_peers, - |peer| { - !mesh_peers.contains(peer) - && !self.explicit_peers.contains(peer) - && !self - .score_below_threshold(peer, |pst| pst.publish_threshold) - .0 - }, - ); - recipient_peers.extend(peer_list); - } - - recipient_peers.extend(mesh_peers); - } - // Gossipsub peers - None => { - tracing::debug!(topic=%topic_hash, "Topic not in the mesh"); - // `fanout_peers` is always non-empty if it's `Some`. - let fanout_peers = self - .fanout - .get(&topic_hash) - .map(|peers| if peers.is_empty() { None } else { Some(peers) }) - .unwrap_or(None); - // If we have fanout peers add them to the map. - if let Some(peers) = fanout_peers { - for peer in peers { - recipient_peers.insert(*peer); - } - } else { - // We have no fanout peers, select mesh_n of them and add them to the fanout - let mesh_n = self.config.mesh_n(); - let new_peers = - get_random_peers(&self.connected_peers, &topic_hash, mesh_n, { - |p| { - !self.explicit_peers.contains(p) - && !self - .score_below_threshold(p, |pst| pst.publish_threshold) - .0 - } - }); - // Add the new peers to the fanout and recipient peers - self.fanout.insert(topic_hash.clone(), new_peers.clone()); - for peer in new_peers { - tracing::debug!(%peer, "Peer added to fanout"); - recipient_peers.insert(peer); - } - } - // We are publishing to fanout peers - update the time we published - self.fanout_last_pub - .insert(topic_hash.clone(), Instant::now()); - } - } - - // Explicit peers that are part of the topic - recipient_peers - .extend(peers_on_topic.filter(|peer_id| self.explicit_peers.contains(peer_id))); - - // Floodsub peers - for (peer, connections) in &self.connected_peers { - if connections.kind == PeerKind::Floodsub - && !self - .score_below_threshold(peer, |ts| ts.publish_threshold) - .0 - { - recipient_peers.insert(*peer); - } - } - } - - // If the message isn't a duplicate and we have sent it to some peers add it to the - // duplicate cache and memcache. - self.duplicate_cache.insert(msg_id.clone()); - self.mcache.put(&msg_id, raw_message.clone()); - - // If the message is anonymous or has a random author add it to the published message ids - // cache. - if let PublishConfig::RandomAuthor | PublishConfig::Anonymous = self.publish_config { - if !self.config.allow_self_origin() { - self.published_message_ids.insert(msg_id.clone()); - } - } - - // Send to peers we know are subscribed to the topic. - let mut publish_failed = true; - for peer_id in recipient_peers.iter() { - if let Some(peer) = self.connected_peers.get_mut(peer_id) { - tracing::trace!(peer=%peer_id, "Sending message to peer"); - match peer.sender.publish( - raw_message.clone(), - self.config.publish_queue_duration(), - self.metrics.as_mut(), - ) { - Ok(_) => publish_failed = false, - Err(_) => { - self.failed_messages.entry(*peer_id).or_default().priority += 1; - - tracing::warn!(peer_id=%peer_id, "Publish queue full. Could not publish to peer"); - // Downscore the peer due to failed message. - if let Some((peer_score, ..)) = &mut self.peer_score { - peer_score.failed_message_slow_peer(peer_id); - } - } - } - } else { - tracing::error!(peer_id = %peer_id, - "Could not send PUBLISH, peer doesn't exist in connected peer list"); - } - } - - if recipient_peers.is_empty() { - return Err(PublishError::InsufficientPeers); - } - - if publish_failed { - return Err(PublishError::AllQueuesFull(recipient_peers.len())); - } - - // Broadcast IDONTWANT messages - if raw_message.raw_protobuf_len() > self.config.idontwant_message_size_threshold() { - self.send_idontwant(&raw_message, &msg_id, raw_message.source.as_ref()); - } - - tracing::debug!(message=%msg_id, "Published message"); - - if let Some(metrics) = self.metrics.as_mut() { - metrics.register_published_message(&topic_hash); - } - - Ok(msg_id) - } - - /// This function should be called when [`Config::validate_messages()`] is `true` after - /// the message got validated by the caller. Messages are stored in the ['Memcache'] and - /// validation is expected to be fast enough that the messages should still exist in the cache. - /// There are three possible validation outcomes and the outcome is given in acceptance. - /// - /// If acceptance = [`MessageAcceptance::Accept`] the message will get propagated to the - /// network. The `propagation_source` parameter indicates who the message was received by and - /// will not be forwarded back to that peer. - /// - /// If acceptance = [`MessageAcceptance::Reject`] the message will be deleted from the memcache - /// and the Pâ‚„ penalty will be applied to the `propagation_source`. - // - /// If acceptance = [`MessageAcceptance::Ignore`] the message will be deleted from the memcache - /// but no Pâ‚„ penalty will be applied. - /// - /// This function will return true if the message was found in the cache and false if was not - /// in the cache anymore. - /// - /// This should only be called once per message. - pub fn report_message_validation_result( - &mut self, - msg_id: &MessageId, - propagation_source: &PeerId, - acceptance: MessageAcceptance, - ) -> Result { - let reject_reason = match acceptance { - MessageAcceptance::Accept => { - let (raw_message, originating_peers) = match self.mcache.validate(msg_id) { - Some((raw_message, originating_peers)) => { - (raw_message.clone(), originating_peers) - } - None => { - tracing::warn!( - message=%msg_id, - "Message not in cache. Ignoring forwarding" - ); - if let Some(metrics) = self.metrics.as_mut() { - metrics.memcache_miss(); - } - return Ok(false); - } - }; - - if let Some(metrics) = self.metrics.as_mut() { - metrics.register_msg_validation(&raw_message.topic, &acceptance); - } - - self.forward_msg( - msg_id, - raw_message, - Some(propagation_source), - originating_peers, - )?; - return Ok(true); - } - MessageAcceptance::Reject => RejectReason::ValidationFailed, - MessageAcceptance::Ignore => RejectReason::ValidationIgnored, - }; - - if let Some((raw_message, originating_peers)) = self.mcache.remove(msg_id) { - if let Some(metrics) = self.metrics.as_mut() { - metrics.register_msg_validation(&raw_message.topic, &acceptance); - } - - // Tell peer_score about reject - // Reject the original source, and any duplicates we've seen from other peers. - if let Some((peer_score, ..)) = &mut self.peer_score { - peer_score.reject_message( - propagation_source, - msg_id, - &raw_message.topic, - reject_reason, - ); - for peer in originating_peers.iter() { - peer_score.reject_message(peer, msg_id, &raw_message.topic, reject_reason); - } - } - Ok(true) - } else { - tracing::warn!(message=%msg_id, "Rejected message not in cache"); - Ok(false) - } - } - - /// Register topics to ensure metrics are recorded correctly for these topics. - pub fn register_topics_for_metrics(&mut self, topics: Vec) { - if let Some(metrics) = &mut self.metrics { - metrics.register_allowed_topics(topics); - } - } - - /// Adds a new peer to the list of explicitly connected peers. - pub fn add_explicit_peer(&mut self, peer_id: &PeerId) { - tracing::debug!(peer=%peer_id, "Adding explicit peer"); - - self.explicit_peers.insert(*peer_id); - - self.check_explicit_peer_connection(peer_id); - } - - /// This removes the peer from explicitly connected peers, note that this does not disconnect - /// the peer. - pub fn remove_explicit_peer(&mut self, peer_id: &PeerId) { - tracing::debug!(peer=%peer_id, "Removing explicit peer"); - self.explicit_peers.remove(peer_id); - } - - /// Blacklists a peer. All messages from this peer will be rejected and any message that was - /// created by this peer will be rejected. - pub fn blacklist_peer(&mut self, peer_id: &PeerId) { - if self.blacklisted_peers.insert(*peer_id) { - tracing::debug!(peer=%peer_id, "Peer has been blacklisted"); - } - } - - /// Removes a peer from the blacklist if it has previously been blacklisted. - pub fn remove_blacklisted_peer(&mut self, peer_id: &PeerId) { - if self.blacklisted_peers.remove(peer_id) { - tracing::debug!(peer=%peer_id, "Peer has been removed from the blacklist"); - } - } - - /// Activates the peer scoring system with the given parameters. This will reset all scores - /// if there was already another peer scoring system activated. Returns an error if the - /// params are not valid or if they got already set. - pub fn with_peer_score( - &mut self, - params: PeerScoreParams, - threshold: PeerScoreThresholds, - ) -> Result<(), String> { - self.with_peer_score_and_message_delivery_time_callback(params, threshold, None) - } - - /// Activates the peer scoring system with the given parameters and a message delivery time - /// callback. Returns an error if the parameters got already set. - pub fn with_peer_score_and_message_delivery_time_callback( - &mut self, - params: PeerScoreParams, - threshold: PeerScoreThresholds, - callback: Option, - ) -> Result<(), String> { - params.validate()?; - threshold.validate()?; - - if self.peer_score.is_some() { - return Err("Peer score set twice".into()); - } - - let interval = Delay::new(params.decay_interval); - let peer_score = PeerScore::new_with_message_delivery_time_callback(params, callback); - self.peer_score = Some((peer_score, threshold, interval)); - Ok(()) - } - - /// Sets scoring parameters for a topic. - /// - /// The [`Self::with_peer_score()`] must first be called to initialise peer scoring. - pub fn set_topic_params( - &mut self, - topic: Topic, - params: TopicScoreParams, - ) -> Result<(), &'static str> { - if let Some((peer_score, ..)) = &mut self.peer_score { - peer_score.set_topic_params(topic.hash(), params); - Ok(()) - } else { - Err("Peer score must be initialised with `with_peer_score()`") - } - } - - /// Returns a scoring parameters for a topic if existent. - pub fn get_topic_params(&self, topic: &Topic) -> Option<&TopicScoreParams> { - self.peer_score.as_ref()?.0.get_topic_params(&topic.hash()) - } - - /// Sets the application specific score for a peer. Returns true if scoring is active and - /// the peer is connected or if the score of the peer is not yet expired, false otherwise. - pub fn set_application_score(&mut self, peer_id: &PeerId, new_score: f64) -> bool { - if let Some((peer_score, ..)) = &mut self.peer_score { - peer_score.set_application_score(peer_id, new_score) - } else { - false - } - } - - /// Gossipsub JOIN(topic) - adds topic peers to mesh and sends them GRAFT messages. - fn join(&mut self, topic_hash: &TopicHash) { - tracing::debug!(topic=%topic_hash, "Running JOIN for topic"); - - // if we are already in the mesh, return - if self.mesh.contains_key(topic_hash) { - tracing::debug!(topic=%topic_hash, "JOIN: The topic is already in the mesh, ignoring JOIN"); - return; - } - - let mut added_peers = HashSet::new(); - - if let Some(m) = self.metrics.as_mut() { - m.joined(topic_hash) - } - - // check if we have mesh_n peers in fanout[topic] and add them to the mesh if we do, - // removing the fanout entry. - if let Some((_, mut peers)) = self.fanout.remove_entry(topic_hash) { - tracing::debug!( - topic=%topic_hash, - "JOIN: Removing peers from the fanout for topic" - ); - - // remove explicit peers, peers with negative scores, and backoffed peers - peers.retain(|p| { - !self.explicit_peers.contains(p) - && !self.score_below_threshold(p, |_| 0.0).0 - && !self.backoffs.is_backoff_with_slack(topic_hash, p) - }); - - // Add up to mesh_n of them them to the mesh - // NOTE: These aren't randomly added, currently FIFO - let add_peers = std::cmp::min(peers.len(), self.config.mesh_n()); - tracing::debug!( - topic=%topic_hash, - "JOIN: Adding {:?} peers from the fanout for topic", - add_peers - ); - added_peers.extend(peers.iter().take(add_peers)); - - self.mesh.insert( - topic_hash.clone(), - peers.into_iter().take(add_peers).collect(), - ); - - // remove the last published time - self.fanout_last_pub.remove(topic_hash); - } - - let fanaout_added = added_peers.len(); - if let Some(m) = self.metrics.as_mut() { - m.peers_included(topic_hash, Inclusion::Fanout, fanaout_added) - } - - // check if we need to get more peers, which we randomly select - if added_peers.len() < self.config.mesh_n() { - // get the peers - let new_peers = get_random_peers( - &self.connected_peers, - topic_hash, - self.config.mesh_n() - added_peers.len(), - |peer| { - !added_peers.contains(peer) - && !self.explicit_peers.contains(peer) - && !self.score_below_threshold(peer, |_| 0.0).0 - && !self.backoffs.is_backoff_with_slack(topic_hash, peer) - }, - ); - added_peers.extend(new_peers.clone()); - // add them to the mesh - tracing::debug!( - "JOIN: Inserting {:?} random peers into the mesh", - new_peers.len() - ); - let mesh_peers = self.mesh.entry(topic_hash.clone()).or_default(); - mesh_peers.extend(new_peers); - } - - let random_added = added_peers.len() - fanaout_added; - if let Some(m) = self.metrics.as_mut() { - m.peers_included(topic_hash, Inclusion::Random, random_added) - } - - for peer_id in added_peers { - // Send a GRAFT control message - if let Some((peer_score, ..)) = &mut self.peer_score { - peer_score.graft(&peer_id, topic_hash.clone()); - } - if let Some(peer) = &mut self.connected_peers.get_mut(&peer_id) { - tracing::debug!(peer=%peer_id, "JOIN: Sending Graft message to peer"); - peer.sender.graft(Graft { - topic_hash: topic_hash.clone(), - }); - } else { - tracing::error!(peer = %peer_id, - "Could not send GRAFT, peer doesn't exist in connected peer list"); - } - - // If the peer did not previously exist in any mesh, inform the handler - peer_added_to_mesh( - peer_id, - vec![topic_hash], - &self.mesh, - &mut self.events, - &self.connected_peers, - ); - } - - let mesh_peers = self.mesh_peers(topic_hash).count(); - if let Some(m) = self.metrics.as_mut() { - m.set_mesh_peers(topic_hash, mesh_peers) - } - - tracing::debug!(topic=%topic_hash, "Completed JOIN for topic"); - } - - /// Creates a PRUNE gossipsub action. - fn make_prune( - &mut self, - topic_hash: &TopicHash, - peer: &PeerId, - do_px: bool, - on_unsubscribe: bool, - ) -> Prune { - if let Some((peer_score, ..)) = &mut self.peer_score { - peer_score.prune(peer, topic_hash.clone()); - } - - match self.connected_peers.get(peer).map(|v| &v.kind) { - Some(PeerKind::Floodsub) => { - tracing::error!("Attempted to prune a Floodsub peer"); - } - Some(PeerKind::Gossipsub) => { - // GossipSub v1.0 -- no peer exchange, the peer won't be able to parse it anyway - return Prune { - topic_hash: topic_hash.clone(), - peers: Vec::new(), - backoff: None, - }; - } - None => { - tracing::error!("Attempted to Prune an unknown peer"); - } - _ => {} // Gossipsub 1.1 peer perform the `Prune` - } - - // Select peers for peer exchange - let peers = if do_px { - get_random_peers( - &self.connected_peers, - topic_hash, - self.config.prune_peers(), - |p| p != peer && !self.score_below_threshold(p, |_| 0.0).0, - ) - .into_iter() - .map(|p| PeerInfo { peer_id: Some(p) }) - .collect() - } else { - Vec::new() - }; - - let backoff = if on_unsubscribe { - self.config.unsubscribe_backoff() - } else { - self.config.prune_backoff() - }; - - // update backoff - self.backoffs.update_backoff(topic_hash, peer, backoff); - - Prune { - topic_hash: topic_hash.clone(), - peers, - backoff: Some(backoff.as_secs()), - } - } - - /// Gossipsub LEAVE(topic) - Notifies mesh\[topic\] peers with PRUNE messages. - fn leave(&mut self, topic_hash: &TopicHash) { - tracing::debug!(topic=%topic_hash, "Running LEAVE for topic"); - - // If our mesh contains the topic, send prune to peers and delete it from the mesh - if let Some((_, peers)) = self.mesh.remove_entry(topic_hash) { - if let Some(m) = self.metrics.as_mut() { - m.left(topic_hash) - } - for peer_id in peers { - // Send a PRUNE control message - let prune = self.make_prune(topic_hash, &peer_id, self.config.do_px(), true); - if let Some(peer) = &mut self.connected_peers.get_mut(&peer_id) { - tracing::debug!(%peer_id, "LEAVE: Sending PRUNE to peer"); - peer.sender.prune(prune); - } else { - tracing::error!(peer = %peer_id, - "Could not send PRUNE, peer doesn't exist in connected peer list"); - } - - // If the peer did not previously exist in any mesh, inform the handler - peer_removed_from_mesh( - peer_id, - topic_hash, - &self.mesh, - &mut self.events, - &self.connected_peers, - ); - } - } - tracing::debug!(topic=%topic_hash, "Completed LEAVE for topic"); - } - - /// Checks if the given peer is still connected and if not dials the peer again. - fn check_explicit_peer_connection(&mut self, peer_id: &PeerId) { - if !self.connected_peers.contains_key(peer_id) { - // Connect to peer - tracing::debug!(peer=%peer_id, "Connecting to explicit peer"); - self.events.push_back(ToSwarm::Dial { - opts: DialOpts::peer_id(*peer_id).build(), - }); - } - } - - /// Determines if a peer's score is below a given `PeerScoreThreshold` chosen via the - /// `threshold` parameter. - fn score_below_threshold( - &self, - peer_id: &PeerId, - threshold: impl Fn(&PeerScoreThresholds) -> f64, - ) -> (bool, f64) { - Self::score_below_threshold_from_scores(&self.peer_score, peer_id, threshold) - } - - fn score_below_threshold_from_scores( - peer_score: &Option<(PeerScore, PeerScoreThresholds, Delay)>, - peer_id: &PeerId, - threshold: impl Fn(&PeerScoreThresholds) -> f64, - ) -> (bool, f64) { - if let Some((peer_score, thresholds, ..)) = peer_score { - let score = peer_score.score(peer_id); - if score < threshold(thresholds) { - return (true, score); - } - (false, score) - } else { - (false, 0.0) - } - } - - /// Handles an IHAVE control message. Checks our cache of messages. If the message is unknown, - /// requests it with an IWANT control message. - fn handle_ihave(&mut self, peer_id: &PeerId, ihave_msgs: Vec<(TopicHash, Vec)>) { - // We ignore IHAVE gossip from any peer whose score is below the gossip threshold - if let (true, score) = self.score_below_threshold(peer_id, |pst| pst.gossip_threshold) { - tracing::debug!( - peer=%peer_id, - %score, - "IHAVE: ignoring peer with score below threshold" - ); - return; - } - - // IHAVE flood protection - let peer_have = self.count_received_ihave.entry(*peer_id).or_insert(0); - *peer_have += 1; - if *peer_have > self.config.max_ihave_messages() { - tracing::debug!( - peer=%peer_id, - "IHAVE: peer has advertised too many times ({}) within this heartbeat \ - interval; ignoring", - *peer_have - ); - return; - } - - if let Some(iasked) = self.count_sent_iwant.get(peer_id) { - if *iasked >= self.config.max_ihave_length() { - tracing::debug!( - peer=%peer_id, - "IHAVE: peer has already advertised too many messages ({}); ignoring", - *iasked - ); - return; - } - } - - tracing::trace!(peer=%peer_id, "Handling IHAVE for peer"); - - let mut iwant_ids = HashSet::new(); - - let want_message = |id: &MessageId| { - if self.duplicate_cache.contains(id) { - return false; - } - - !self.gossip_promises.contains(id) - }; - - for (topic, ids) in ihave_msgs { - // only process the message if we are subscribed - if !self.mesh.contains_key(&topic) { - tracing::debug!( - %topic, - "IHAVE: Ignoring IHAVE - Not subscribed to topic" - ); - continue; - } - - for id in ids.into_iter().filter(want_message) { - // have not seen this message and are not currently requesting it - if iwant_ids.insert(id) { - // Register the IWANT metric - if let Some(metrics) = self.metrics.as_mut() { - metrics.register_iwant(&topic); - } - } - } - } - - if !iwant_ids.is_empty() { - let iasked = self.count_sent_iwant.entry(*peer_id).or_insert(0); - let mut iask = iwant_ids.len(); - if *iasked + iask > self.config.max_ihave_length() { - iask = self.config.max_ihave_length().saturating_sub(*iasked); - } - - // Send the list of IWANT control messages - tracing::debug!( - peer=%peer_id, - "IHAVE: Asking for {} out of {} messages from peer", - iask, - iwant_ids.len() - ); - - // Ask in random order - let mut iwant_ids_vec: Vec<_> = iwant_ids.into_iter().collect(); - let mut rng = thread_rng(); - iwant_ids_vec.partial_shuffle(&mut rng, iask); - - iwant_ids_vec.truncate(iask); - *iasked += iask; - - self.gossip_promises.add_promise( - *peer_id, - &iwant_ids_vec, - Instant::now() + self.config.iwant_followup_time(), - ); - - if let Some(peer) = &mut self.connected_peers.get_mut(peer_id) { - tracing::trace!( - peer=%peer_id, - "IHAVE: Asking for the following messages from peer: {:?}", - iwant_ids_vec - ); - - if peer - .sender - .iwant(IWant { - message_ids: iwant_ids_vec, - }) - .is_err() - { - tracing::warn!(peer=%peer_id, "Send Queue full. Could not send IWANT"); - - if let Some((peer_score, ..)) = &mut self.peer_score { - peer_score.failed_message_slow_peer(peer_id); - } - // Increment failed message count - self.failed_messages - .entry(*peer_id) - .or_default() - .non_priority += 1; - } - } else { - tracing::error!(peer = %peer_id, - "Could not send IWANT, peer doesn't exist in connected peer list"); - } - } - tracing::trace!(peer=%peer_id, "Completed IHAVE handling for peer"); - } - - /// Handles an IWANT control message. Checks our cache of messages. If the message exists it is - /// forwarded to the requesting peer. - fn handle_iwant(&mut self, peer_id: &PeerId, iwant_msgs: Vec) { - // We ignore IWANT gossip from any peer whose score is below the gossip threshold - if let (true, score) = self.score_below_threshold(peer_id, |pst| pst.gossip_threshold) { - tracing::debug!( - peer=%peer_id, - "IWANT: ignoring peer with score below threshold [score = {}]", - score - ); - return; - } - - tracing::debug!(peer=%peer_id, "Handling IWANT for peer"); - - for id in iwant_msgs { - // If we have it and the IHAVE count is not above the threshold, - // forward the message. - if let Some((msg, count)) = self - .mcache - .get_with_iwant_counts(&id, peer_id) - .map(|(msg, count)| (msg.clone(), count)) - { - if count > self.config.gossip_retransimission() { - tracing::debug!( - peer=%peer_id, - message=%id, - "IWANT: Peer has asked for message too many times; ignoring request" - ); - } else if let Some(peer) = &mut self.connected_peers.get_mut(peer_id) { - if peer.dont_send_received.get(&id).is_some() { - tracing::debug!(%peer_id, message=%id, "Peer already sent IDONTWANT for this message"); - continue; - } - - tracing::debug!(peer=%peer_id, "IWANT: Sending cached messages to peer"); - if peer - .sender - .forward( - msg, - self.config.forward_queue_duration(), - self.metrics.as_mut(), - ) - .is_err() - { - // Downscore the peer - if let Some((peer_score, ..)) = &mut self.peer_score { - peer_score.failed_message_slow_peer(peer_id); - } - // Increment the failed message count - self.failed_messages - .entry(*peer_id) - .or_default() - .non_priority += 1; - } - } else { - tracing::error!(peer = %peer_id, - "Could not send IWANT, peer doesn't exist in connected peer list"); - } - } - } - tracing::debug!(peer=%peer_id, "Completed IWANT handling for peer"); - } - - /// Handles GRAFT control messages. If subscribed to the topic, adds the peer to mesh, if not, - /// responds with PRUNE messages. - fn handle_graft(&mut self, peer_id: &PeerId, topics: Vec) { - tracing::debug!(peer=%peer_id, "Handling GRAFT message for peer"); - - let mut to_prune_topics = HashSet::new(); - - let mut do_px = self.config.do_px(); - - // For each topic, if a peer has grafted us, then we necessarily must be in their mesh - // and they must be subscribed to the topic. Ensure we have recorded the mapping. - for topic in &topics { - let Some(connected_peer) = self.connected_peers.get_mut(peer_id) else { - tracing::error!(peer_id = %peer_id, "Peer non-existent when handling graft"); - return; - }; - if connected_peer.topics.insert(topic.clone()) { - if let Some(m) = self.metrics.as_mut() { - m.inc_topic_peers(topic); - } - } - } - - // we don't GRAFT to/from explicit peers; complain loudly if this happens - if self.explicit_peers.contains(peer_id) { - tracing::warn!(peer=%peer_id, "GRAFT: ignoring request from direct peer"); - // this is possibly a bug from non-reciprocal configuration; send a PRUNE for all topics - to_prune_topics = topics.into_iter().collect(); - // but don't PX - do_px = false - } else { - let (below_zero, score) = self.score_below_threshold(peer_id, |_| 0.0); - let now = Instant::now(); - for topic_hash in topics { - if let Some(peers) = self.mesh.get_mut(&topic_hash) { - // if the peer is already in the mesh ignore the graft - if peers.contains(peer_id) { - tracing::debug!( - peer=%peer_id, - topic=%&topic_hash, - "GRAFT: Received graft for peer that is already in topic" - ); - continue; - } - - // make sure we are not backing off that peer - if let Some(backoff_time) = self.backoffs.get_backoff_time(&topic_hash, peer_id) - { - if backoff_time > now { - tracing::warn!( - peer=%peer_id, - "[Penalty] Peer attempted graft within backoff time, penalizing" - ); - // add behavioural penalty - if let Some((peer_score, ..)) = &mut self.peer_score { - if let Some(metrics) = self.metrics.as_mut() { - metrics.register_score_penalty(Penalty::GraftBackoff); - } - peer_score.add_penalty(peer_id, 1); - - // check the flood cutoff - // See: https://github.com/rust-lang/rust-clippy/issues/10061 - #[allow(unknown_lints, clippy::unchecked_duration_subtraction)] - let flood_cutoff = (backoff_time - + self.config.graft_flood_threshold()) - - self.config.prune_backoff(); - if flood_cutoff > now { - //extra penalty - peer_score.add_penalty(peer_id, 1); - } - } - // no PX - do_px = false; - - to_prune_topics.insert(topic_hash.clone()); - continue; - } - } - - // check the score - if below_zero { - // we don't GRAFT peers with negative score - tracing::debug!( - peer=%peer_id, - %score, - topic=%topic_hash, - "GRAFT: ignoring peer with negative score" - ); - // we do send them PRUNE however, because it's a matter of protocol correctness - to_prune_topics.insert(topic_hash.clone()); - // but we won't PX to them - do_px = false; - continue; - } - - // check mesh upper bound and only allow graft if the upper bound is not reached or - // if it is an outbound peer - if peers.len() >= self.config.mesh_n_high() - && !self.outbound_peers.contains(peer_id) - { - to_prune_topics.insert(topic_hash.clone()); - continue; - } - - // add peer to the mesh - tracing::debug!( - peer=%peer_id, - topic=%topic_hash, - "GRAFT: Mesh link added for peer in topic" - ); - - if peers.insert(*peer_id) { - if let Some(m) = self.metrics.as_mut() { - m.peers_included(&topic_hash, Inclusion::Subscribed, 1) - } - } - - // If the peer did not previously exist in any mesh, inform the handler - peer_added_to_mesh( - *peer_id, - vec![&topic_hash], - &self.mesh, - &mut self.events, - &self.connected_peers, - ); - - if let Some((peer_score, ..)) = &mut self.peer_score { - peer_score.graft(peer_id, topic_hash); - } - } else { - // don't do PX when there is an unknown topic to avoid leaking our peers - do_px = false; - tracing::debug!( - peer=%peer_id, - topic=%topic_hash, - "GRAFT: Received graft for unknown topic from peer" - ); - // spam hardening: ignore GRAFTs for unknown topics - continue; - } - } - } - - if !to_prune_topics.is_empty() { - // build the prune messages to send - let on_unsubscribe = false; - - let mut sender = match self.connected_peers.get_mut(peer_id) { - Some(connected_peer) => connected_peer.sender.clone(), - None => { - tracing::error!(peer_id = %peer_id, "Peer non-existent when handling graft and obtaining a sender"); - return; - } - }; - - for prune in to_prune_topics - .iter() - .map(|t| self.make_prune(t, peer_id, do_px, on_unsubscribe)) - { - sender.prune(prune); - } - // Send the prune messages to the peer - tracing::debug!( - peer=%peer_id, - "GRAFT: Not subscribed to topics - Sending PRUNE to peer" - ); - } - tracing::debug!(peer=%peer_id, "Completed GRAFT handling for peer"); - } - - fn remove_peer_from_mesh( - &mut self, - peer_id: &PeerId, - topic_hash: &TopicHash, - backoff: Option, - always_update_backoff: bool, - reason: Churn, - ) { - let mut update_backoff = always_update_backoff; - if let Some(peers) = self.mesh.get_mut(topic_hash) { - // remove the peer if it exists in the mesh - if peers.remove(peer_id) { - tracing::debug!( - peer=%peer_id, - topic=%topic_hash, - "PRUNE: Removing peer from the mesh for topic" - ); - if let Some(m) = self.metrics.as_mut() { - m.peers_removed(topic_hash, reason, 1) - } - - if let Some((peer_score, ..)) = &mut self.peer_score { - peer_score.prune(peer_id, topic_hash.clone()); - } - - update_backoff = true; - - // inform the handler - peer_removed_from_mesh( - *peer_id, - topic_hash, - &self.mesh, - &mut self.events, - &self.connected_peers, - ); - } - } - if update_backoff { - let time = if let Some(backoff) = backoff { - Duration::from_secs(backoff) - } else { - self.config.prune_backoff() - }; - // is there a backoff specified by the peer? if so obey it. - self.backoffs.update_backoff(topic_hash, peer_id, time); - } - } - - /// Handles PRUNE control messages. Removes peer from the mesh. - fn handle_prune( - &mut self, - peer_id: &PeerId, - prune_data: Vec<(TopicHash, Vec, Option)>, - ) { - tracing::debug!(peer=%peer_id, "Handling PRUNE message for peer"); - let (below_threshold, score) = - self.score_below_threshold(peer_id, |pst| pst.accept_px_threshold); - for (topic_hash, px, backoff) in prune_data { - self.remove_peer_from_mesh(peer_id, &topic_hash, backoff, true, Churn::Prune); - - if self.mesh.contains_key(&topic_hash) { - //connect to px peers - if !px.is_empty() { - // we ignore PX from peers with insufficient score - if below_threshold { - tracing::debug!( - peer=%peer_id, - %score, - topic=%topic_hash, - "PRUNE: ignoring PX from peer with insufficient score" - ); - continue; - } - - // NOTE: We cannot dial any peers from PX currently as we typically will not - // know their multiaddr. Until SignedRecords are spec'd this - // remains a stub. By default `config.prune_peers()` is set to zero and - // this is skipped. If the user modifies this, this will only be able to - // dial already known peers (from an external discovery mechanism for - // example). - if self.config.prune_peers() > 0 { - self.px_connect(px); - } - } - } - } - tracing::debug!(peer=%peer_id, "Completed PRUNE handling for peer"); - } - - fn px_connect(&mut self, mut px: Vec) { - let n = self.config.prune_peers(); - // Ignore peerInfo with no ID - // - //TODO: Once signed records are spec'd: Can we use peerInfo without any IDs if they have a - // signed peer record? - px.retain(|p| p.peer_id.is_some()); - if px.len() > n { - // only use at most prune_peers many random peers - let mut rng = thread_rng(); - px.partial_shuffle(&mut rng, n); - px = px.into_iter().take(n).collect(); - } - - for p in px { - // TODO: Once signed records are spec'd: extract signed peer record if given and handle - // it, see https://github.com/libp2p/specs/pull/217 - if let Some(peer_id) = p.peer_id { - // mark as px peer - self.px_peers.insert(peer_id); - - // dial peer - self.events.push_back(ToSwarm::Dial { - opts: DialOpts::peer_id(peer_id).build(), - }); - } - } - } - - /// Applies some basic checks to whether this message is valid. Does not apply user validation - /// checks. - fn message_is_valid( - &mut self, - msg_id: &MessageId, - raw_message: &mut RawMessage, - propagation_source: &PeerId, - ) -> bool { - tracing::debug!( - peer=%propagation_source, - message=%msg_id, - "Handling message from peer" - ); - - // Reject any message from a blacklisted peer - if self.blacklisted_peers.contains(propagation_source) { - tracing::debug!( - peer=%propagation_source, - "Rejecting message from blacklisted peer" - ); - self.gossip_promises - .reject_message(msg_id, &RejectReason::BlackListedPeer); - if let Some((peer_score, ..)) = &mut self.peer_score { - peer_score.reject_message( - propagation_source, - msg_id, - &raw_message.topic, - RejectReason::BlackListedPeer, - ); - } - return false; - } - - // Also reject any message that originated from a blacklisted peer - if let Some(source) = raw_message.source.as_ref() { - if self.blacklisted_peers.contains(source) { - tracing::debug!( - peer=%propagation_source, - %source, - "Rejecting message from peer because of blacklisted source" - ); - self.handle_invalid_message( - propagation_source, - raw_message, - RejectReason::BlackListedSource, - ); - return false; - } - } - - // If we are not validating messages, assume this message is validated - // This will allow the message to be gossiped without explicitly calling - // `validate_message`. - if !self.config.validate_messages() { - raw_message.validated = true; - } - - // reject messages claiming to be from ourselves but not locally published - let self_published = !self.config.allow_self_origin() - && if let Some(own_id) = self.publish_config.get_own_id() { - own_id != propagation_source && raw_message.source.as_ref() == Some(own_id) - } else { - self.published_message_ids.contains(msg_id) - }; - - if self_published { - tracing::debug!( - message=%msg_id, - source=%propagation_source, - "Dropping message claiming to be from self but forwarded from source" - ); - self.handle_invalid_message(propagation_source, raw_message, RejectReason::SelfOrigin); - return false; - } - - true - } - - /// Handles a newly received [`RawMessage`]. - /// - /// Forwards the message to all peers in the mesh. - fn handle_received_message( - &mut self, - mut raw_message: RawMessage, - propagation_source: &PeerId, - ) { - // Record the received metric - if let Some(metrics) = self.metrics.as_mut() { - metrics.msg_recvd_unfiltered(&raw_message.topic, raw_message.raw_protobuf_len()); - } - - // Try and perform the data transform to the message. If it fails, consider it invalid. - let message = match self.data_transform.inbound_transform(raw_message.clone()) { - Ok(message) => message, - Err(e) => { - tracing::debug!("Invalid message. Transform error: {:?}", e); - // Reject the message and return - self.handle_invalid_message( - propagation_source, - &raw_message, - RejectReason::ValidationError(ValidationError::TransformFailed), - ); - return; - } - }; - - // Calculate the message id on the transformed data. - let msg_id = self.config.message_id(&message); - - if let Some(metrics) = self.metrics.as_mut() { - if let Some(peer) = self.connected_peers.get_mut(propagation_source) { - // Record if we received a message that we already sent a IDONTWANT for to the peer - if peer.dont_send_sent.contains_key(&msg_id) { - metrics.register_idontwant_messages_ignored_per_topic(&raw_message.topic); - } - } - } - - // Check the validity of the message - // Peers get penalized if this message is invalid. We don't add it to the duplicate cache - // and instead continually penalize peers that repeatedly send this message. - if !self.message_is_valid(&msg_id, &mut raw_message, propagation_source) { - return; - } - - if !self.duplicate_cache.insert(msg_id.clone()) { - tracing::debug!(message=%msg_id, "Message already received, ignoring"); - if let Some((peer_score, ..)) = &mut self.peer_score { - peer_score.duplicated_message(propagation_source, &msg_id, &message.topic); - } - self.mcache.observe_duplicate(&msg_id, propagation_source); - // track metrics for the source of the duplicates - if let Some(metrics) = self.metrics.as_mut() { - if self - .mesh - .get(&message.topic) - .is_some_and(|peers| peers.contains(propagation_source)) - { - // duplicate was received from a mesh peer - metrics.mesh_duplicates(&message.topic); - } else if self - .gossip_promises - .contains_peer(&msg_id, propagation_source) - { - // duplicate was received from an iwant request - metrics.iwant_duplicates(&message.topic); - } else { - tracing::warn!( - messsage=%msg_id, - peer=%propagation_source, - topic=%message.topic, - "Peer should not have sent message" - ); - } - } - return; - } - - // Broadcast IDONTWANT messages - if raw_message.raw_protobuf_len() > self.config.idontwant_message_size_threshold() { - self.send_idontwant(&raw_message, &msg_id, Some(propagation_source)); - } - - tracing::debug!( - message=%msg_id, - "Put message in duplicate_cache and resolve promises" - ); - - // Record the received message with the metrics - if let Some(metrics) = self.metrics.as_mut() { - metrics.msg_recvd(&message.topic); - } - - // Consider the message as delivered for gossip promises. - self.gossip_promises.message_delivered(&msg_id); - - // Tells score that message arrived (but is maybe not fully validated yet). - if let Some((peer_score, ..)) = &mut self.peer_score { - peer_score.validate_message(propagation_source, &msg_id, &message.topic); - } - - // Add the message to our memcache - self.mcache.put(&msg_id, raw_message.clone()); - - // Dispatch the message to the user if we are subscribed to any of the topics - if self.mesh.contains_key(&message.topic) { - tracing::debug!("Sending received message to user"); - self.events - .push_back(ToSwarm::GenerateEvent(Event::Message { - propagation_source: *propagation_source, - message_id: msg_id.clone(), - message, - })); - } else { - tracing::debug!( - topic=%message.topic, - "Received message on a topic we are not subscribed to" - ); - return; - } - - // forward the message to mesh peers, if no validation is required - if !self.config.validate_messages() { - if self - .forward_msg( - &msg_id, - raw_message, - Some(propagation_source), - HashSet::new(), - ) - .is_err() - { - tracing::error!("Failed to forward message. Too large"); - } - tracing::debug!(message=%msg_id, "Completed message handling for message"); - } - } - - // Handles invalid messages received. - fn handle_invalid_message( - &mut self, - propagation_source: &PeerId, - raw_message: &RawMessage, - reject_reason: RejectReason, - ) { - if let Some((peer_score, ..)) = &mut self.peer_score { - if let Some(metrics) = self.metrics.as_mut() { - metrics.register_invalid_message(&raw_message.topic); - } - - if let Ok(message) = self.data_transform.inbound_transform(raw_message.clone()) { - let message_id = self.config.message_id(&message); - - peer_score.reject_message( - propagation_source, - &message_id, - &message.topic, - reject_reason, - ); - - self.gossip_promises - .reject_message(&message_id, &reject_reason); - } else { - // The message is invalid, we reject it ignoring any gossip promises. If a peer is - // advertising this message via an IHAVE and it's invalid it will be double - // penalized, one for sending us an invalid and again for breaking a promise. - peer_score.reject_invalid_message(propagation_source, &raw_message.topic); - } - } - } - - /// Handles received subscriptions. - fn handle_received_subscriptions( - &mut self, - subscriptions: &[Subscription], - propagation_source: &PeerId, - ) { - tracing::debug!( - source=%propagation_source, - "Handling subscriptions: {:?}", - subscriptions, - ); - - let mut unsubscribed_peers = Vec::new(); - - let Some(peer) = self.connected_peers.get_mut(propagation_source) else { - tracing::error!( - peer=%propagation_source, - "Subscription by unknown peer" - ); - return; - }; - - // Collect potential graft topics for the peer. - let mut topics_to_graft = Vec::new(); - - // Notify the application about the subscription, after the grafts are sent. - let mut application_event = Vec::new(); - - let filtered_topics = match self - .subscription_filter - .filter_incoming_subscriptions(subscriptions, &peer.topics) - { - Ok(topics) => topics, - Err(s) => { - tracing::error!( - peer=%propagation_source, - "Subscription filter error: {}; ignoring RPC from peer", - s - ); - return; - } - }; - - for subscription in filtered_topics { - // get the peers from the mapping, or insert empty lists if the topic doesn't exist - let topic_hash = &subscription.topic_hash; - - match subscription.action { - SubscriptionAction::Subscribe => { - // add to the peer_topics mapping - if peer.topics.insert(topic_hash.clone()) { - tracing::debug!( - peer=%propagation_source, - topic=%topic_hash, - "SUBSCRIPTION: Adding gossip peer to topic" - ); - - if let Some(m) = self.metrics.as_mut() { - m.inc_topic_peers(topic_hash); - } - } - // if the mesh needs peers add the peer to the mesh - if !self.explicit_peers.contains(propagation_source) - && peer.kind.is_gossipsub() - && !Self::score_below_threshold_from_scores( - &self.peer_score, - propagation_source, - |_| 0.0, - ) - .0 - && !self - .backoffs - .is_backoff_with_slack(topic_hash, propagation_source) - { - if let Some(peers) = self.mesh.get_mut(topic_hash) { - if peers.len() < self.config.mesh_n_low() - && peers.insert(*propagation_source) - { - tracing::debug!( - peer=%propagation_source, - topic=%topic_hash, - "SUBSCRIPTION: Adding peer to the mesh for topic" - ); - if let Some(m) = self.metrics.as_mut() { - m.peers_included(topic_hash, Inclusion::Subscribed, 1) - } - // send graft to the peer - tracing::debug!( - peer=%propagation_source, - topic=%topic_hash, - "Sending GRAFT to peer for topic" - ); - if let Some((peer_score, ..)) = &mut self.peer_score { - peer_score.graft(propagation_source, topic_hash.clone()); - } - topics_to_graft.push(topic_hash.clone()); - } - } - } - // generates a subscription event to be polled - application_event.push(ToSwarm::GenerateEvent(Event::Subscribed { - peer_id: *propagation_source, - topic: topic_hash.clone(), - })); - } - SubscriptionAction::Unsubscribe => { - // remove topic from the peer_topics mapping - if peer.topics.remove(topic_hash) { - tracing::debug!( - peer=%propagation_source, - topic=%topic_hash, - "SUBSCRIPTION: Removing gossip peer from topic" - ); - - if let Some(m) = self.metrics.as_mut() { - m.dec_topic_peers(topic_hash); - } - } - - unsubscribed_peers.push((*propagation_source, topic_hash.clone())); - // generate an unsubscribe event to be polled - application_event.push(ToSwarm::GenerateEvent(Event::Unsubscribed { - peer_id: *propagation_source, - topic: topic_hash.clone(), - })); - } - } - } - - // remove unsubscribed peers from the mesh and fanout if they exist there. - for (peer_id, topic_hash) in unsubscribed_peers { - self.fanout - .get_mut(&topic_hash) - .map(|peers| peers.remove(&peer_id)); - self.remove_peer_from_mesh(&peer_id, &topic_hash, None, false, Churn::Unsub); - } - - // Potentially inform the handler if we have added this peer to a mesh for the first time. - let topics_joined = topics_to_graft.iter().collect::>(); - if !topics_joined.is_empty() { - peer_added_to_mesh( - *propagation_source, - topics_joined, - &self.mesh, - &mut self.events, - &self.connected_peers, - ); - } - - // If we need to send grafts to peer, do so immediately, rather than waiting for the - // heartbeat. - if let Some(peer) = &mut self.connected_peers.get_mut(propagation_source) { - for topic_hash in topics_to_graft.into_iter() { - peer.sender.graft(Graft { topic_hash }); - } - } else { - tracing::error!(peer = %propagation_source, - "Could not send GRAFT, peer doesn't exist in connected peer list"); - } - - // Notify the application of the subscriptions - for event in application_event { - self.events.push_back(event); - } - - tracing::trace!( - source=%propagation_source, - "Completed handling subscriptions from source" - ); - } - - /// Applies penalties to peers that did not respond to our IWANT requests. - fn apply_iwant_penalties(&mut self) { - if let Some((peer_score, ..)) = &mut self.peer_score { - for (peer, count) in self.gossip_promises.get_broken_promises() { - // We do not apply penalties to nodes that have disconnected. - if self.connected_peers.contains_key(&peer) { - peer_score.add_penalty(&peer, count); - if let Some(metrics) = self.metrics.as_mut() { - metrics.register_score_penalty(Penalty::BrokenPromise); - } - } - } - } - } - - /// Heartbeat function which shifts the memcache and updates the mesh. - fn heartbeat(&mut self) { - tracing::debug!("Starting heartbeat"); - let start = Instant::now(); - - // Every heartbeat we sample the send queues to add to our metrics. We do this intentionally - // before we add all the gossip from this heartbeat in order to gain a true measure of - // steady-state size of the queues. - if let Some(m) = &mut self.metrics { - for sender_queue in self.connected_peers.values_mut().map(|v| &v.sender) { - m.observe_priority_queue_size(sender_queue.priority_len()); - m.observe_non_priority_queue_size(sender_queue.non_priority_len()); - } - } - - self.heartbeat_ticks += 1; - - let mut to_graft = HashMap::new(); - let mut to_prune = HashMap::new(); - let mut no_px = HashSet::new(); - - // clean up expired backoffs - self.backoffs.heartbeat(); - - // clean up ihave counters - self.count_sent_iwant.clear(); - self.count_received_ihave.clear(); - - // apply iwant penalties - self.apply_iwant_penalties(); - - // check connections to explicit peers - if self.heartbeat_ticks % self.config.check_explicit_peers_ticks() == 0 { - for p in self.explicit_peers.clone() { - self.check_explicit_peer_connection(&p); - } - } - - // Cache the scores of all connected peers, and record metrics for current penalties. - let mut scores = HashMap::with_capacity(self.connected_peers.len()); - if let Some((peer_score, ..)) = &self.peer_score { - for peer_id in self.connected_peers.keys() { - scores - .entry(peer_id) - .or_insert_with(|| peer_score.metric_score(peer_id, self.metrics.as_mut())); - } - } - - // maintain the mesh for each topic - for (topic_hash, peers) in self.mesh.iter_mut() { - let explicit_peers = &self.explicit_peers; - let backoffs = &self.backoffs; - let outbound_peers = &self.outbound_peers; - - // drop all peers with negative score, without PX - // if there is at some point a stable retain method for BTreeSet the following can be - // written more efficiently with retain. - let mut to_remove_peers = Vec::new(); - for peer_id in peers.iter() { - let peer_score = *scores.get(peer_id).unwrap_or(&0.0); - - // Record the score per mesh - if let Some(metrics) = self.metrics.as_mut() { - metrics.observe_mesh_peers_score(topic_hash, peer_score); - } - - if peer_score < 0.0 { - tracing::debug!( - peer=%peer_id, - score=%peer_score, - topic=%topic_hash, - "HEARTBEAT: Prune peer with negative score" - ); - - let current_topic = to_prune.entry(*peer_id).or_insert_with(Vec::new); - current_topic.push(topic_hash.clone()); - no_px.insert(*peer_id); - to_remove_peers.push(*peer_id); - } - } - - if let Some(m) = self.metrics.as_mut() { - m.peers_removed(topic_hash, Churn::BadScore, to_remove_peers.len()) - } - - for peer_id in to_remove_peers { - peers.remove(&peer_id); - } - - // too little peers - add some - if peers.len() < self.config.mesh_n_low() { - tracing::debug!( - topic=%topic_hash, - "HEARTBEAT: Mesh low. Topic contains: {} needs: {}", - peers.len(), - self.config.mesh_n_low() - ); - // not enough peers - get mesh_n - current_length more - let desired_peers = self.config.mesh_n() - peers.len(); - let peer_list = - get_random_peers(&self.connected_peers, topic_hash, desired_peers, |peer| { - !peers.contains(peer) - && !explicit_peers.contains(peer) - && !backoffs.is_backoff_with_slack(topic_hash, peer) - && *scores.get(peer).unwrap_or(&0.0) >= 0.0 - }); - for peer in &peer_list { - let current_topic = to_graft.entry(*peer).or_insert_with(Vec::new); - current_topic.push(topic_hash.clone()); - } - // update the mesh - tracing::debug!("Updating mesh, new mesh: {:?}", peer_list); - if let Some(m) = self.metrics.as_mut() { - m.peers_included(topic_hash, Inclusion::Random, peer_list.len()) - } - peers.extend(peer_list); - } - - // too many peers - remove some - if peers.len() > self.config.mesh_n_high() { - tracing::debug!( - topic=%topic_hash, - "HEARTBEAT: Mesh high. Topic contains: {} needs: {}", - peers.len(), - self.config.mesh_n_high() - ); - let excess_peer_no = peers.len() - self.config.mesh_n(); - - // shuffle the peers and then sort by score ascending beginning with the worst - let mut rng = thread_rng(); - let mut shuffled = peers.iter().copied().collect::>(); - shuffled.shuffle(&mut rng); - shuffled.sort_by(|p1, p2| { - let score_p1 = *scores.get(p1).unwrap_or(&0.0); - let score_p2 = *scores.get(p2).unwrap_or(&0.0); - - score_p1.partial_cmp(&score_p2).unwrap_or(Ordering::Equal) - }); - // shuffle everything except the last retain_scores many peers (the best ones) - shuffled[..peers.len() - self.config.retain_scores()].shuffle(&mut rng); - - // count total number of outbound peers - let mut outbound = { - let outbound_peers = &self.outbound_peers; - shuffled - .iter() - .filter(|p| outbound_peers.contains(*p)) - .count() - }; - - // remove the first excess_peer_no allowed (by outbound restrictions) peers adding - // them to to_prune - let mut removed = 0; - for peer in shuffled { - if removed == excess_peer_no { - break; - } - if self.outbound_peers.contains(&peer) { - if outbound <= self.config.mesh_outbound_min() { - // do not remove anymore outbound peers - continue; - } - // an outbound peer gets removed - outbound -= 1; - } - - // remove the peer - peers.remove(&peer); - let current_topic = to_prune.entry(peer).or_insert_with(Vec::new); - current_topic.push(topic_hash.clone()); - removed += 1; - } - - if let Some(m) = self.metrics.as_mut() { - m.peers_removed(topic_hash, Churn::Excess, removed) - } - } - - // do we have enough outbound peers? - if peers.len() >= self.config.mesh_n_low() { - // count number of outbound peers we have - let outbound = { peers.iter().filter(|p| outbound_peers.contains(*p)).count() }; - - // if we have not enough outbound peers, graft to some new outbound peers - if outbound < self.config.mesh_outbound_min() { - let needed = self.config.mesh_outbound_min() - outbound; - let peer_list = - get_random_peers(&self.connected_peers, topic_hash, needed, |peer| { - !peers.contains(peer) - && !explicit_peers.contains(peer) - && !backoffs.is_backoff_with_slack(topic_hash, peer) - && *scores.get(peer).unwrap_or(&0.0) >= 0.0 - && outbound_peers.contains(peer) - }); - for peer in &peer_list { - let current_topic = to_graft.entry(*peer).or_insert_with(Vec::new); - current_topic.push(topic_hash.clone()); - } - // update the mesh - tracing::debug!("Updating mesh, new mesh: {:?}", peer_list); - if let Some(m) = self.metrics.as_mut() { - m.peers_included(topic_hash, Inclusion::Outbound, peer_list.len()) - } - peers.extend(peer_list); - } - } - - // should we try to improve the mesh with opportunistic grafting? - if self.heartbeat_ticks % self.config.opportunistic_graft_ticks() == 0 - && peers.len() > 1 - && self.peer_score.is_some() - { - if let Some((_, thresholds, _)) = &self.peer_score { - // Opportunistic grafting works as follows: we check the median score of peers - // in the mesh; if this score is below the opportunisticGraftThreshold, we - // select a few peers at random with score over the median. - // The intention is to (slowly) improve an underperforming mesh by introducing - // good scoring peers that may have been gossiping at us. This allows us to - // get out of sticky situations where we are stuck with poor peers and also - // recover from churn of good peers. - - // now compute the median peer score in the mesh - let mut peers_by_score: Vec<_> = peers.iter().collect(); - peers_by_score.sort_by(|p1, p2| { - let p1_score = *scores.get(p1).unwrap_or(&0.0); - let p2_score = *scores.get(p2).unwrap_or(&0.0); - p1_score.partial_cmp(&p2_score).unwrap_or(Equal) - }); - - let middle = peers_by_score.len() / 2; - let median = if peers_by_score.len() % 2 == 0 { - let sub_middle_peer = *peers_by_score - .get(middle - 1) - .expect("middle < vector length and middle > 0 since peers.len() > 0"); - let sub_middle_score = *scores.get(sub_middle_peer).unwrap_or(&0.0); - let middle_peer = - *peers_by_score.get(middle).expect("middle < vector length"); - let middle_score = *scores.get(middle_peer).unwrap_or(&0.0); - - (sub_middle_score + middle_score) * 0.5 - } else { - *scores - .get(*peers_by_score.get(middle).expect("middle < vector length")) - .unwrap_or(&0.0) - }; - - // if the median score is below the threshold, select a better peer (if any) and - // GRAFT - if median < thresholds.opportunistic_graft_threshold { - let peer_list = get_random_peers( - &self.connected_peers, - topic_hash, - self.config.opportunistic_graft_peers(), - |peer_id| { - !peers.contains(peer_id) - && !explicit_peers.contains(peer_id) - && !backoffs.is_backoff_with_slack(topic_hash, peer_id) - && *scores.get(peer_id).unwrap_or(&0.0) > median - }, - ); - for peer in &peer_list { - let current_topic = to_graft.entry(*peer).or_insert_with(Vec::new); - current_topic.push(topic_hash.clone()); - } - // update the mesh - tracing::debug!( - topic=%topic_hash, - "Opportunistically graft in topic with peers {:?}", - peer_list - ); - if let Some(m) = self.metrics.as_mut() { - m.peers_included(topic_hash, Inclusion::Random, peer_list.len()) - } - peers.extend(peer_list); - } - } - } - // Register the final count of peers in the mesh - if let Some(m) = self.metrics.as_mut() { - m.set_mesh_peers(topic_hash, peers.len()) - } - } - - // remove expired fanout topics - { - let fanout = &mut self.fanout; // help the borrow checker - let fanout_ttl = self.config.fanout_ttl(); - self.fanout_last_pub.retain(|topic_hash, last_pub_time| { - if *last_pub_time + fanout_ttl < Instant::now() { - tracing::debug!( - topic=%topic_hash, - "HEARTBEAT: Fanout topic removed due to timeout" - ); - fanout.remove(topic_hash); - return false; - } - true - }); - } - - // maintain fanout - // check if our peers are still a part of the topic - for (topic_hash, peers) in self.fanout.iter_mut() { - let mut to_remove_peers = Vec::new(); - let publish_threshold = match &self.peer_score { - Some((_, thresholds, _)) => thresholds.publish_threshold, - _ => 0.0, - }; - for peer_id in peers.iter() { - // is the peer still subscribed to the topic? - let peer_score = *scores.get(peer_id).unwrap_or(&0.0); - match self.connected_peers.get(peer_id) { - Some(peer) => { - if !peer.topics.contains(topic_hash) || peer_score < publish_threshold { - tracing::debug!( - topic=%topic_hash, - "HEARTBEAT: Peer removed from fanout for topic" - ); - to_remove_peers.push(*peer_id); - } - } - None => { - // remove if the peer has disconnected - to_remove_peers.push(*peer_id); - } - } - } - for to_remove in to_remove_peers { - peers.remove(&to_remove); - } - - // not enough peers - if peers.len() < self.config.mesh_n() { - tracing::debug!( - "HEARTBEAT: Fanout low. Contains: {:?} needs: {:?}", - peers.len(), - self.config.mesh_n() - ); - let needed_peers = self.config.mesh_n() - peers.len(); - let explicit_peers = &self.explicit_peers; - let new_peers = - get_random_peers(&self.connected_peers, topic_hash, needed_peers, |peer_id| { - !peers.contains(peer_id) - && !explicit_peers.contains(peer_id) - && *scores.get(peer_id).unwrap_or(&0.0) < publish_threshold - }); - peers.extend(new_peers); - } - } - - if self.peer_score.is_some() { - tracing::trace!("Mesh message deliveries: {:?}", { - self.mesh - .iter() - .map(|(t, peers)| { - ( - t.clone(), - peers - .iter() - .map(|p| { - ( - *p, - self.peer_score - .as_ref() - .expect("peer_score.is_some()") - .0 - .mesh_message_deliveries(p, t) - .unwrap_or(0.0), - ) - }) - .collect::>(), - ) - }) - .collect::>>() - }) - } - - self.emit_gossip(); - - // send graft/prunes - if !to_graft.is_empty() | !to_prune.is_empty() { - self.send_graft_prune(to_graft, to_prune, no_px); - } - - // shift the memcache - self.mcache.shift(); - - // Report expired messages - for (peer_id, failed_messages) in self.failed_messages.drain() { - tracing::debug!("Peer couldn't consume messages: {:?}", failed_messages); - self.events - .push_back(ToSwarm::GenerateEvent(Event::SlowPeer { - peer_id, - failed_messages, - })); - } - self.failed_messages.shrink_to_fit(); - - // Flush stale IDONTWANTs. - for peer in self.connected_peers.values_mut() { - while let Some((_front, instant)) = peer.dont_send_received.front() { - if (*instant + IDONTWANT_TIMEOUT) >= Instant::now() { - break; - } else { - peer.dont_send_received.pop_front(); - } - } - // If metrics are not enabled, this queue would be empty. - while let Some((_front, instant)) = peer.dont_send_sent.front() { - if (*instant + IDONTWANT_TIMEOUT) >= Instant::now() { - break; - } else { - peer.dont_send_sent.pop_front(); - } - } - } - - tracing::debug!("Completed Heartbeat"); - if let Some(metrics) = self.metrics.as_mut() { - let duration = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX); - metrics.observe_heartbeat_duration(duration); - } - } - - /// Emits gossip - Send IHAVE messages to a random set of gossip peers. This is applied to mesh - /// and fanout peers - fn emit_gossip(&mut self) { - let mut rng = thread_rng(); - for (topic_hash, peers) in self.mesh.iter().chain(self.fanout.iter()) { - let mut message_ids = self.mcache.get_gossip_message_ids(topic_hash); - if message_ids.is_empty() { - continue; - } - - // if we are emitting more than GossipSubMaxIHaveLength message_ids, truncate the list - if message_ids.len() > self.config.max_ihave_length() { - // we do the truncation (with shuffling) per peer below - tracing::debug!( - "too many messages for gossip; will truncate IHAVE list ({} messages)", - message_ids.len() - ); - } else { - // shuffle to emit in random order - message_ids.shuffle(&mut rng); - } - - // dynamic number of peers to gossip based on `gossip_factor` with minimum `gossip_lazy` - let n_map = |m| { - max( - self.config.gossip_lazy(), - (self.config.gossip_factor() * m as f64) as usize, - ) - }; - // get gossip_lazy random peers - let to_msg_peers = - get_random_peers_dynamic(&self.connected_peers, topic_hash, n_map, |peer| { - !peers.contains(peer) - && !self.explicit_peers.contains(peer) - && !self.score_below_threshold(peer, |ts| ts.gossip_threshold).0 - }); - - tracing::debug!("Gossiping IHAVE to {} peers", to_msg_peers.len()); - - for peer_id in to_msg_peers { - let mut peer_message_ids = message_ids.clone(); - - if peer_message_ids.len() > self.config.max_ihave_length() { - // We do this per peer so that we emit a different set for each peer. - // we have enough redundancy in the system that this will significantly increase - // the message coverage when we do truncate. - peer_message_ids.partial_shuffle(&mut rng, self.config.max_ihave_length()); - peer_message_ids.truncate(self.config.max_ihave_length()); - } - - // send an IHAVE message - if let Some(peer) = &mut self.connected_peers.get_mut(&peer_id) { - if peer - .sender - .ihave(IHave { - topic_hash: topic_hash.clone(), - message_ids: peer_message_ids, - }) - .is_err() - { - tracing::warn!(peer=%peer_id, "Send Queue full. Could not send IHAVE"); - - if let Some((peer_score, ..)) = &mut self.peer_score { - peer_score.failed_message_slow_peer(&peer_id); - } - // Increment failed message count - self.failed_messages - .entry(peer_id) - .or_default() - .non_priority += 1; - } - } else { - tracing::error!(peer = %peer_id, - "Could not send IHAVE, peer doesn't exist in connected peer list"); - } - } - } - } - - /// Handles multiple GRAFT/PRUNE messages and coalesces them into chunked gossip control - /// messages. - fn send_graft_prune( - &mut self, - to_graft: HashMap>, - mut to_prune: HashMap>, - no_px: HashSet, - ) { - // handle the grafts and overlapping prunes per peer - for (peer_id, topics) in to_graft.into_iter() { - for topic in &topics { - // inform scoring of graft - if let Some((peer_score, ..)) = &mut self.peer_score { - peer_score.graft(&peer_id, topic.clone()); - } - - // inform the handler of the peer being added to the mesh - // If the peer did not previously exist in any mesh, inform the handler - peer_added_to_mesh( - peer_id, - vec![topic], - &self.mesh, - &mut self.events, - &self.connected_peers, - ); - } - - // If there are prunes associated with the same peer add them. - // NOTE: In this case a peer has been added to a topic mesh, and removed from another. - // It therefore must be in at least one mesh and we do not need to inform the handler - // of its removal from another. - - // send the control messages - let mut sender = match self.connected_peers.get_mut(&peer_id) { - Some(connected_peer) => connected_peer.sender.clone(), - None => { - tracing::error!(peer_id = %peer_id, "Peer non-existent when sending graft/prune"); - return; - } - }; - - // The following prunes are not due to unsubscribing. - let prunes = to_prune - .remove(&peer_id) - .into_iter() - .flatten() - .map(|topic_hash| { - self.make_prune( - &topic_hash, - &peer_id, - self.config.do_px() && !no_px.contains(&peer_id), - false, - ) - }); - - for topic_hash in topics { - sender.graft(Graft { - topic_hash: topic_hash.clone(), - }); - } - - for prune in prunes { - sender.prune(prune); - } - } - - // handle the remaining prunes - // The following prunes are not due to unsubscribing. - for (peer_id, topics) in to_prune.iter() { - for topic_hash in topics { - let prune = self.make_prune( - topic_hash, - peer_id, - self.config.do_px() && !no_px.contains(peer_id), - false, - ); - if let Some(peer) = self.connected_peers.get_mut(peer_id) { - peer.sender.prune(prune); - } else { - tracing::error!(peer = %peer_id, - "Could not send PRUNE, peer doesn't exist in connected peer list"); - } - - // inform the handler - peer_removed_from_mesh( - *peer_id, - topic_hash, - &self.mesh, - &mut self.events, - &self.connected_peers, - ); - } - } - } - - /// Helper function which sends an IDONTWANT message to mesh\[topic\] peers. - fn send_idontwant( - &mut self, - message: &RawMessage, - msg_id: &MessageId, - propagation_source: Option<&PeerId>, - ) { - let Some(mesh_peers) = self.mesh.get(&message.topic) else { - return; - }; - - let iwant_peers = self.gossip_promises.peers_for_message(msg_id); - - let recipient_peers = mesh_peers - .iter() - .chain(iwant_peers.iter()) - .filter(|&peer_id| { - Some(peer_id) != propagation_source && Some(peer_id) != message.source.as_ref() - }); - - for peer_id in recipient_peers { - let Some(peer) = self.connected_peers.get_mut(peer_id) else { - // It can be the case that promises to disconnected peers appear here. In this case - // we simply ignore the peer-id. - continue; - }; - - // Only gossipsub 1.2 peers support IDONTWANT. - if peer.kind != PeerKind::Gossipsubv1_2 { - continue; - } - - if peer - .sender - .idontwant(IDontWant { - message_ids: vec![msg_id.clone()], - }) - .is_err() - { - tracing::warn!(peer=%peer_id, "Send Queue full. Could not send IDONTWANT"); - - if let Some((peer_score, ..)) = &mut self.peer_score { - peer_score.failed_message_slow_peer(peer_id); - } - // Increment failed message count - self.failed_messages - .entry(*peer_id) - .or_default() - .non_priority += 1; - return; - } - // IDONTWANT sent successfully. - if let Some(metrics) = self.metrics.as_mut() { - peer.dont_send_sent.insert(msg_id.clone(), Instant::now()); - // Don't exceed capacity. - if peer.dont_send_sent.len() > IDONTWANT_CAP { - peer.dont_send_sent.pop_front(); - } - metrics.register_idontwant_messages_sent_per_topic(&message.topic); - } - } - } - - /// Helper function which forwards a message to mesh\[topic\] peers. - /// - /// Returns true if at least one peer was messaged. - fn forward_msg( - &mut self, - msg_id: &MessageId, - message: RawMessage, - propagation_source: Option<&PeerId>, - originating_peers: HashSet, - ) -> Result { - // message is fully validated inform peer_score - if let Some((peer_score, ..)) = &mut self.peer_score { - if let Some(peer) = propagation_source { - peer_score.deliver_message(peer, msg_id, &message.topic); - } - } - - tracing::debug!(message=%msg_id, "Forwarding message"); - let mut recipient_peers = HashSet::new(); - - // Populate the recipient peers mapping - - // Add explicit peers - for peer_id in &self.explicit_peers { - if let Some(peer) = self.connected_peers.get(peer_id) { - if Some(peer_id) != propagation_source - && !originating_peers.contains(peer_id) - && Some(peer_id) != message.source.as_ref() - && peer.topics.contains(&message.topic) - { - recipient_peers.insert(*peer_id); - } - } - } - - // add mesh peers - let topic = &message.topic; - // mesh - if let Some(mesh_peers) = self.mesh.get(topic) { - for peer_id in mesh_peers { - if Some(peer_id) != propagation_source - && !originating_peers.contains(peer_id) - && Some(peer_id) != message.source.as_ref() - { - recipient_peers.insert(*peer_id); - } - } - } - - // forward the message to peers - if !recipient_peers.is_empty() { - for peer_id in recipient_peers.iter() { - if let Some(peer) = self.connected_peers.get_mut(peer_id) { - if peer.dont_send_received.get(msg_id).is_some() { - tracing::debug!(%peer_id, message=%msg_id, "Peer doesn't want message"); - continue; - } - - tracing::debug!(%peer_id, message=%msg_id, "Sending message to peer"); - if peer - .sender - .forward( - message.clone(), - self.config.forward_queue_duration(), - self.metrics.as_mut(), - ) - .is_err() - { - // Downscore the peer - if let Some((peer_score, ..)) = &mut self.peer_score { - peer_score.failed_message_slow_peer(peer_id); - } - // Increment the failed message count - self.failed_messages - .entry(*peer_id) - .or_default() - .non_priority += 1; - } - } else { - tracing::error!(peer = %peer_id, - "Could not FORWARD, peer doesn't exist in connected peer list"); - } - } - tracing::debug!("Completed forwarding message"); - Ok(true) - } else { - Ok(false) - } - } - - /// Constructs a [`RawMessage`] performing message signing if required. - pub(crate) fn build_raw_message( - &mut self, - topic: TopicHash, - data: Vec, - ) -> Result { - match &mut self.publish_config { - PublishConfig::Signing { - ref keypair, - author, - inline_key, - last_seq_no, - } => { - let sequence_number = last_seq_no.next(); - - let signature = { - let message = proto::Message { - from: Some(author.to_bytes()), - data: Some(data.clone()), - seqno: Some(sequence_number.to_be_bytes().to_vec()), - topic: topic.clone().into_string(), - signature: None, - key: None, - }; - - let mut buf = Vec::with_capacity(message.get_size()); - let mut writer = Writer::new(&mut buf); - - message - .write_message(&mut writer) - .expect("Encoding to succeed"); - - // the signature is over the bytes "libp2p-pubsub:" - let mut signature_bytes = SIGNING_PREFIX.to_vec(); - signature_bytes.extend_from_slice(&buf); - Some(keypair.sign(&signature_bytes)?) - }; - - Ok(RawMessage { - source: Some(*author), - data, - // To be interoperable with the go-implementation this is treated as a 64-bit - // big-endian uint. - sequence_number: Some(sequence_number), - topic, - signature, - key: inline_key.clone(), - validated: true, // all published messages are valid - }) - } - PublishConfig::Author(peer_id) => { - Ok(RawMessage { - source: Some(*peer_id), - data, - // To be interoperable with the go-implementation this is treated as a 64-bit - // big-endian uint. - sequence_number: Some(rand::random()), - topic, - signature: None, - key: None, - validated: true, // all published messages are valid - }) - } - PublishConfig::RandomAuthor => { - Ok(RawMessage { - source: Some(PeerId::random()), - data, - // To be interoperable with the go-implementation this is treated as a 64-bit - // big-endian uint. - sequence_number: Some(rand::random()), - topic, - signature: None, - key: None, - validated: true, // all published messages are valid - }) - } - PublishConfig::Anonymous => { - Ok(RawMessage { - source: None, - data, - // To be interoperable with the go-implementation this is treated as a 64-bit - // big-endian uint. - sequence_number: None, - topic, - signature: None, - key: None, - validated: true, // all published messages are valid - }) - } - } - } - - fn on_connection_established( - &mut self, - ConnectionEstablished { - peer_id, - endpoint, - other_established, - .. - }: ConnectionEstablished, - ) { - // Diverging from the go implementation we only want to consider a peer as outbound peer - // if its first connection is outbound. - - if endpoint.is_dialer() && other_established == 0 && !self.px_peers.contains(&peer_id) { - // The first connection is outbound and it is not a peer from peer exchange => mark - // it as outbound peer - self.outbound_peers.insert(peer_id); - } - - // Add the IP to the peer scoring system - if let Some((peer_score, ..)) = &mut self.peer_score { - if let Some(ip) = get_ip_addr(endpoint.get_remote_address()) { - peer_score.add_ip(&peer_id, ip); - } else { - tracing::trace!( - peer=%peer_id, - "Couldn't extract ip from endpoint of peer with endpoint {:?}", - endpoint - ) - } - } - - if other_established > 0 { - return; // Not our first connection to this peer, hence nothing to do. - } - - if let Some((peer_score, ..)) = &mut self.peer_score { - peer_score.add_peer(peer_id); - } - - // Ignore connections from blacklisted peers. - if self.blacklisted_peers.contains(&peer_id) { - tracing::debug!(peer=%peer_id, "Ignoring connection from blacklisted peer"); - return; - } - - tracing::debug!(peer=%peer_id, "New peer connected"); - // We need to send our subscriptions to the newly-connected node. - if let Some(peer) = self.connected_peers.get_mut(&peer_id) { - for topic_hash in self.mesh.clone().into_keys() { - peer.sender.subscribe(topic_hash); - } - } else { - tracing::error!(peer = %peer_id, - "Could not send SUBSCRIBE, peer doesn't exist in connected peer list"); - } - } - - fn on_connection_closed( - &mut self, - ConnectionClosed { - peer_id, - connection_id, - endpoint, - remaining_established, - .. - }: ConnectionClosed, - ) { - // Remove IP from peer scoring system - if let Some((peer_score, ..)) = &mut self.peer_score { - if let Some(ip) = get_ip_addr(endpoint.get_remote_address()) { - peer_score.remove_ip(&peer_id, &ip); - } else { - tracing::trace!( - peer=%peer_id, - "Couldn't extract ip from endpoint of peer with endpoint {:?}", - endpoint - ) - } - } - - if remaining_established != 0 { - // Remove the connection from the list - if let Some(peer) = self.connected_peers.get_mut(&peer_id) { - let index = peer - .connections - .iter() - .position(|v| v == &connection_id) - .expect("Previously established connection to peer must be present"); - peer.connections.remove(index); - - // If there are more connections and this peer is in a mesh, inform the first connection - // handler. - if !peer.connections.is_empty() { - for topic in &peer.topics { - if let Some(mesh_peers) = self.mesh.get(topic) { - if mesh_peers.contains(&peer_id) { - self.events.push_back(ToSwarm::NotifyHandler { - peer_id, - event: HandlerIn::JoinedMesh, - handler: NotifyHandler::One(peer.connections[0]), - }); - break; - } - } - } - } - } - } else { - // remove from mesh, topic_peers, peer_topic and the fanout - tracing::debug!(peer=%peer_id, "Peer disconnected"); - - let Some(connected_peer) = self.connected_peers.get(&peer_id) else { - tracing::error!(peer_id = %peer_id, "Peer non-existent when handling disconnection"); - return; - }; - - // remove peer from all mappings - for topic in &connected_peer.topics { - // check the mesh for the topic - if let Some(mesh_peers) = self.mesh.get_mut(topic) { - // check if the peer is in the mesh and remove it - if mesh_peers.remove(&peer_id) { - if let Some(m) = self.metrics.as_mut() { - m.peers_removed(topic, Churn::Dc, 1); - m.set_mesh_peers(topic, mesh_peers.len()); - } - }; - } - - if let Some(m) = self.metrics.as_mut() { - m.dec_topic_peers(topic); - } - - // remove from fanout - self.fanout - .get_mut(topic) - .map(|peers| peers.remove(&peer_id)); - } - - // Forget px and outbound status for this peer - self.px_peers.remove(&peer_id); - self.outbound_peers.remove(&peer_id); - - // If metrics are enabled, register the disconnection of a peer based on its protocol. - if let Some(metrics) = self.metrics.as_mut() { - metrics.peer_protocol_disconnected(connected_peer.kind.clone()); - } - - self.connected_peers.remove(&peer_id); - - if let Some((peer_score, ..)) = &mut self.peer_score { - peer_score.remove_peer(&peer_id); - } - } - } - - fn on_address_change( - &mut self, - AddressChange { - peer_id, - old: endpoint_old, - new: endpoint_new, - .. - }: AddressChange, - ) { - // Exchange IP in peer scoring system - if let Some((peer_score, ..)) = &mut self.peer_score { - if let Some(ip) = get_ip_addr(endpoint_old.get_remote_address()) { - peer_score.remove_ip(&peer_id, &ip); - } else { - tracing::trace!( - peer=%&peer_id, - "Couldn't extract ip from endpoint of peer with endpoint {:?}", - endpoint_old - ) - } - if let Some(ip) = get_ip_addr(endpoint_new.get_remote_address()) { - peer_score.add_ip(&peer_id, ip); - } else { - tracing::trace!( - peer=%peer_id, - "Couldn't extract ip from endpoint of peer with endpoint {:?}", - endpoint_new - ) - } - } - } -} - -fn get_ip_addr(addr: &Multiaddr) -> Option { - addr.iter().find_map(|p| match p { - Ip4(addr) => Some(IpAddr::V4(addr)), - Ip6(addr) => Some(IpAddr::V6(addr)), - _ => None, - }) -} - -impl NetworkBehaviour for Behaviour -where - C: Send + 'static + DataTransform, - F: Send + 'static + TopicSubscriptionFilter, -{ - type ConnectionHandler = Handler; - type ToSwarm = Event; - - fn handle_established_inbound_connection( - &mut self, - connection_id: ConnectionId, - peer_id: PeerId, - _: &Multiaddr, - _: &Multiaddr, - ) -> Result, ConnectionDenied> { - // By default we assume a peer is only a floodsub peer. - // - // The protocol negotiation occurs once a message is sent/received. Once this happens we - // update the type of peer that this is in order to determine which kind of routing should - // occur. - let connected_peer = self - .connected_peers - .entry(peer_id) - .or_insert(PeerConnections { - kind: PeerKind::Floodsub, - connections: vec![], - sender: RpcSender::new(self.config.connection_handler_queue_len()), - topics: Default::default(), - dont_send_received: LinkedHashMap::new(), - dont_send_sent: LinkedHashMap::new(), - }); - // Add the new connection - connected_peer.connections.push(connection_id); - - Ok(Handler::new( - self.config.protocol_config(), - connected_peer.sender.new_receiver(), - )) - } - - fn handle_established_outbound_connection( - &mut self, - connection_id: ConnectionId, - peer_id: PeerId, - _: &Multiaddr, - _: Endpoint, - _: PortUse, - ) -> Result, ConnectionDenied> { - // By default we assume a peer is only a floodsub peer. - // - // The protocol negotiation occurs once a message is sent/received. Once this happens we - // update the type of peer that this is in order to determine which kind of routing should - // occur. - let connected_peer = self - .connected_peers - .entry(peer_id) - .or_insert(PeerConnections { - kind: PeerKind::Floodsub, - connections: vec![], - sender: RpcSender::new(self.config.connection_handler_queue_len()), - topics: Default::default(), - dont_send_received: LinkedHashMap::new(), - dont_send_sent: LinkedHashMap::new(), - }); - // Add the new connection - connected_peer.connections.push(connection_id); - - Ok(Handler::new( - self.config.protocol_config(), - connected_peer.sender.new_receiver(), - )) - } - - fn on_connection_handler_event( - &mut self, - propagation_source: PeerId, - _connection_id: ConnectionId, - handler_event: THandlerOutEvent, - ) { - match handler_event { - HandlerEvent::PeerKind(kind) => { - // We have identified the protocol this peer is using - - if let Some(metrics) = self.metrics.as_mut() { - metrics.peer_protocol_connected(kind.clone()); - } - - if let PeerKind::NotSupported = kind { - tracing::debug!( - peer=%propagation_source, - "Peer does not support gossipsub protocols" - ); - self.events - .push_back(ToSwarm::GenerateEvent(Event::GossipsubNotSupported { - peer_id: propagation_source, - })); - } else if let Some(conn) = self.connected_peers.get_mut(&propagation_source) { - // Only change the value if the old value is Floodsub (the default set in - // `NetworkBehaviour::on_event` with FromSwarm::ConnectionEstablished). - // All other PeerKind changes are ignored. - tracing::debug!( - peer=%propagation_source, - peer_type=%kind, - "New peer type found for peer" - ); - if let PeerKind::Floodsub = conn.kind { - conn.kind = kind; - } - } - } - HandlerEvent::MessageDropped(rpc) => { - // Account for this in the scoring logic - if let Some((peer_score, _, _)) = &mut self.peer_score { - peer_score.failed_message_slow_peer(&propagation_source); - } - - // Keep track of expired messages for the application layer. - match rpc { - RpcOut::Publish { .. } => { - self.failed_messages - .entry(propagation_source) - .or_default() - .publish += 1; - } - RpcOut::Forward { .. } => { - self.failed_messages - .entry(propagation_source) - .or_default() - .forward += 1; - } - _ => {} // - } - - // Record metrics on the failure. - if let Some(metrics) = self.metrics.as_mut() { - match rpc { - RpcOut::Publish { message, .. } => { - metrics.publish_msg_dropped(&message.topic); - } - RpcOut::Forward { message, .. } => { - metrics.forward_msg_dropped(&message.topic); - } - _ => {} - } - } - } - HandlerEvent::Message { - rpc, - invalid_messages, - } => { - // Handle the gossipsub RPC - - // Handle subscriptions - // Update connected peers topics - if !rpc.subscriptions.is_empty() { - self.handle_received_subscriptions(&rpc.subscriptions, &propagation_source); - } - - // Check if peer is graylisted in which case we ignore the event - if let (true, _) = - self.score_below_threshold(&propagation_source, |pst| pst.graylist_threshold) - { - tracing::debug!(peer=%propagation_source, "RPC Dropped from greylisted peer"); - return; - } - - // Handle any invalid messages from this peer - if self.peer_score.is_some() { - for (raw_message, validation_error) in invalid_messages { - self.handle_invalid_message( - &propagation_source, - &raw_message, - RejectReason::ValidationError(validation_error), - ) - } - } else { - // log the invalid messages - for (message, validation_error) in invalid_messages { - tracing::warn!( - peer=%propagation_source, - source=?message.source, - "Invalid message from peer. Reason: {:?}", - validation_error, - ); - } - } - - // Handle messages - for (count, raw_message) in rpc.messages.into_iter().enumerate() { - // Only process the amount of messages the configuration allows. - if self.config.max_messages_per_rpc().is_some() - && Some(count) >= self.config.max_messages_per_rpc() - { - tracing::warn!("Received more messages than permitted. Ignoring further messages. Processed: {}", count); - break; - } - self.handle_received_message(raw_message, &propagation_source); - } - - // Handle control messages - // group some control messages, this minimises SendEvents (code is simplified to handle each event at a time however) - let mut ihave_msgs = vec![]; - let mut graft_msgs = vec![]; - let mut prune_msgs = vec![]; - for control_msg in rpc.control_msgs { - match control_msg { - ControlAction::IHave(IHave { - topic_hash, - message_ids, - }) => { - ihave_msgs.push((topic_hash, message_ids)); - } - ControlAction::IWant(IWant { message_ids }) => { - self.handle_iwant(&propagation_source, message_ids) - } - ControlAction::Graft(Graft { topic_hash }) => graft_msgs.push(topic_hash), - ControlAction::Prune(Prune { - topic_hash, - peers, - backoff, - }) => prune_msgs.push((topic_hash, peers, backoff)), - ControlAction::IDontWant(IDontWant { message_ids }) => { - let Some(peer) = self.connected_peers.get_mut(&propagation_source) - else { - tracing::error!(peer = %propagation_source, - "Could not handle IDONTWANT, peer doesn't exist in connected peer list"); - continue; - }; - if let Some(metrics) = self.metrics.as_mut() { - metrics.register_idontwant(message_ids.len()); - let idontwant_size = message_ids.iter().map(|id| id.0.len()).sum(); - metrics.register_idontwant_bytes(idontwant_size); - } - for message_id in message_ids { - peer.dont_send_received.insert(message_id, Instant::now()); - // Don't exceed capacity. - if peer.dont_send_received.len() > IDONTWANT_CAP { - peer.dont_send_received.pop_front(); - } - } - } - } - } - if !ihave_msgs.is_empty() { - self.handle_ihave(&propagation_source, ihave_msgs); - } - if !graft_msgs.is_empty() { - self.handle_graft(&propagation_source, graft_msgs); - } - if !prune_msgs.is_empty() { - self.handle_prune(&propagation_source, prune_msgs); - } - } - } - } - - #[tracing::instrument(level = "trace", name = "NetworkBehaviour::poll", skip(self, cx))] - fn poll( - &mut self, - cx: &mut Context<'_>, - ) -> Poll>> { - if let Some(event) = self.events.pop_front() { - return Poll::Ready(event); - } - - // update scores - if let Some((peer_score, _, delay)) = &mut self.peer_score { - if delay.poll_unpin(cx).is_ready() { - peer_score.refresh_scores(); - delay.reset(peer_score.params.decay_interval); - } - } - - if self.heartbeat.poll_unpin(cx).is_ready() { - self.heartbeat(); - self.heartbeat.reset(self.config.heartbeat_interval()); - } - - Poll::Pending - } - - fn on_swarm_event(&mut self, event: FromSwarm) { - match event { - FromSwarm::ConnectionEstablished(connection_established) => { - self.on_connection_established(connection_established) - } - FromSwarm::ConnectionClosed(connection_closed) => { - self.on_connection_closed(connection_closed) - } - FromSwarm::AddressChange(address_change) => self.on_address_change(address_change), - _ => {} - } - } -} - -/// This is called when peers are added to any mesh. It checks if the peer existed -/// in any other mesh. If this is the first mesh they have joined, it queues a message to notify -/// the appropriate connection handler to maintain a connection. -fn peer_added_to_mesh( - peer_id: PeerId, - new_topics: Vec<&TopicHash>, - mesh: &HashMap>, - events: &mut VecDeque>, - connections: &HashMap, -) { - // Ensure there is an active connection - let connection_id = match connections.get(&peer_id) { - Some(p) => p - .connections - .first() - .expect("There should be at least one connection to a peer."), - None => { - tracing::error!(peer_id=%peer_id, "Peer not existent when added to the mesh"); - return; - } - }; - - if let Some(peer) = connections.get(&peer_id) { - for topic in &peer.topics { - if !new_topics.contains(&topic) { - if let Some(mesh_peers) = mesh.get(topic) { - if mesh_peers.contains(&peer_id) { - // the peer is already in a mesh for another topic - return; - } - } - } - } - } - // This is the first mesh the peer has joined, inform the handler - events.push_back(ToSwarm::NotifyHandler { - peer_id, - event: HandlerIn::JoinedMesh, - handler: NotifyHandler::One(*connection_id), - }); -} - -/// This is called when peers are removed from a mesh. It checks if the peer exists -/// in any other mesh. If this is the last mesh they have joined, we return true, in order to -/// notify the handler to no longer maintain a connection. -fn peer_removed_from_mesh( - peer_id: PeerId, - old_topic: &TopicHash, - mesh: &HashMap>, - events: &mut VecDeque>, - connections: &HashMap, -) { - // Ensure there is an active connection - let connection_id = match connections.get(&peer_id) { - Some(p) => p - .connections - .first() - .expect("There should be at least one connection to a peer."), - None => { - tracing::error!(peer_id=%peer_id, "Peer not existent when removed from mesh"); - return; - } - }; - - if let Some(peer) = connections.get(&peer_id) { - for topic in &peer.topics { - if topic != old_topic { - if let Some(mesh_peers) = mesh.get(topic) { - if mesh_peers.contains(&peer_id) { - // the peer exists in another mesh still - return; - } - } - } - } - } - // The peer is not in any other mesh, inform the handler - events.push_back(ToSwarm::NotifyHandler { - peer_id, - event: HandlerIn::LeftMesh, - handler: NotifyHandler::One(*connection_id), - }); -} - -/// Helper function to get a subset of random gossipsub peers for a `topic_hash` -/// filtered by the function `f`. The number of peers to get equals the output of `n_map` -/// that gets as input the number of filtered peers. -fn get_random_peers_dynamic( - connected_peers: &HashMap, - topic_hash: &TopicHash, - // maps the number of total peers to the number of selected peers - n_map: impl Fn(usize) -> usize, - mut f: impl FnMut(&PeerId) -> bool, -) -> BTreeSet { - let mut gossip_peers = connected_peers - .iter() - .filter(|(_, p)| p.topics.contains(topic_hash)) - .filter(|(peer_id, _)| f(peer_id)) - .filter(|(_, p)| p.kind.is_gossipsub()) - .map(|(peer_id, _)| *peer_id) - .collect::>(); - - // if we have less than needed, return them - let n = n_map(gossip_peers.len()); - if gossip_peers.len() <= n { - tracing::debug!("RANDOM PEERS: Got {:?} peers", gossip_peers.len()); - return gossip_peers.into_iter().collect(); - } - - // we have more peers than needed, shuffle them and return n of them - let mut rng = thread_rng(); - gossip_peers.partial_shuffle(&mut rng, n); - - tracing::debug!("RANDOM PEERS: Got {:?} peers", n); - - gossip_peers.into_iter().take(n).collect() -} - -/// Helper function to get a set of `n` random gossipsub peers for a `topic_hash` -/// filtered by the function `f`. -fn get_random_peers( - connected_peers: &HashMap, - topic_hash: &TopicHash, - n: usize, - f: impl FnMut(&PeerId) -> bool, -) -> BTreeSet { - get_random_peers_dynamic(connected_peers, topic_hash, |_| n, f) -} - -/// Validates the combination of signing, privacy and message validation to ensure the -/// configuration will not reject published messages. -fn validate_config( - authenticity: &MessageAuthenticity, - validation_mode: &ValidationMode, -) -> Result<(), &'static str> { - match validation_mode { - ValidationMode::Anonymous => { - if authenticity.is_signing() { - return Err("Cannot enable message signing with an Anonymous validation mode. Consider changing either the ValidationMode or MessageAuthenticity"); - } - - if !authenticity.is_anonymous() { - return Err("Published messages contain an author but incoming messages with an author will be rejected. Consider adjusting the validation or privacy settings in the config"); - } - } - ValidationMode::Strict => { - if !authenticity.is_signing() { - return Err( - "Messages will be - published unsigned and incoming unsigned messages will be rejected. Consider adjusting - the validation or privacy settings in the config" - ); - } - } - _ => {} - } - Ok(()) -} - -impl fmt::Debug for Behaviour { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Behaviour") - .field("config", &self.config) - .field("events", &self.events.len()) - .field("publish_config", &self.publish_config) - .field("mesh", &self.mesh) - .field("fanout", &self.fanout) - .field("fanout_last_pub", &self.fanout_last_pub) - .field("mcache", &self.mcache) - .field("heartbeat", &self.heartbeat) - .finish() - } -} - -impl fmt::Debug for PublishConfig { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - PublishConfig::Signing { author, .. } => { - f.write_fmt(format_args!("PublishConfig::Signing({author})")) - } - PublishConfig::Author(author) => { - f.write_fmt(format_args!("PublishConfig::Author({author})")) - } - PublishConfig::RandomAuthor => f.write_fmt(format_args!("PublishConfig::RandomAuthor")), - PublishConfig::Anonymous => f.write_fmt(format_args!("PublishConfig::Anonymous")), - } - } -} diff --git a/beacon_node/lighthouse_network/gossipsub/src/behaviour/tests.rs b/beacon_node/lighthouse_network/gossipsub/src/behaviour/tests.rs deleted file mode 100644 index 90b8fe43fb5..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/behaviour/tests.rs +++ /dev/null @@ -1,5486 +0,0 @@ -// Copyright 2020 Sigma Prime Pty Ltd. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -// Collection of tests for the gossipsub network behaviour - -use super::*; -use crate::subscription_filter::WhitelistSubscriptionFilter; -use crate::types::RpcReceiver; -use crate::{config::ConfigBuilder, types::Rpc, IdentTopic as Topic}; -use byteorder::{BigEndian, ByteOrder}; -use futures::StreamExt; -use libp2p::core::ConnectedPoint; -use rand::Rng; -use std::net::Ipv4Addr; -use std::thread::sleep; - -#[derive(Default, Debug)] -struct InjectNodes { - peer_no: usize, - topics: Vec, - to_subscribe: bool, - gs_config: Config, - explicit: usize, - outbound: usize, - scoring: Option<(PeerScoreParams, PeerScoreThresholds)>, - data_transform: D, - subscription_filter: F, - peer_kind: Option, -} - -impl InjectNodes -where - D: DataTransform + Default + Clone + Send + 'static, - F: TopicSubscriptionFilter + Clone + Default + Send + 'static, -{ - #[allow(clippy::type_complexity)] - pub(crate) fn create_network( - self, - ) -> ( - Behaviour, - Vec, - HashMap, - Vec, - ) { - let keypair = libp2p::identity::Keypair::generate_ed25519(); - // create a gossipsub struct - let mut gs: Behaviour = Behaviour::new_with_subscription_filter_and_transform( - MessageAuthenticity::Signed(keypair), - self.gs_config, - None, - self.subscription_filter, - self.data_transform, - ) - .unwrap(); - - if let Some((scoring_params, scoring_thresholds)) = self.scoring { - gs.with_peer_score(scoring_params, scoring_thresholds) - .unwrap(); - } - - let mut topic_hashes = vec![]; - - // subscribe to the topics - for t in self.topics { - let topic = Topic::new(t); - gs.subscribe(&topic).unwrap(); - topic_hashes.push(topic.hash().clone()); - } - - // build and connect peer_no random peers - let mut peers = vec![]; - let mut receivers = HashMap::new(); - - let empty = vec![]; - for i in 0..self.peer_no { - let (peer, receiver) = add_peer_with_addr_and_kind( - &mut gs, - if self.to_subscribe { - &topic_hashes - } else { - &empty - }, - i < self.outbound, - i < self.explicit, - Multiaddr::empty(), - self.peer_kind.clone().or(Some(PeerKind::Gossipsubv1_1)), - ); - peers.push(peer); - receivers.insert(peer, receiver); - } - - (gs, peers, receivers, topic_hashes) - } - - fn peer_no(mut self, peer_no: usize) -> Self { - self.peer_no = peer_no; - self - } - - fn topics(mut self, topics: Vec) -> Self { - self.topics = topics; - self - } - - #[allow(clippy::wrong_self_convention)] - fn to_subscribe(mut self, to_subscribe: bool) -> Self { - self.to_subscribe = to_subscribe; - self - } - - fn gs_config(mut self, gs_config: Config) -> Self { - self.gs_config = gs_config; - self - } - - fn explicit(mut self, explicit: usize) -> Self { - self.explicit = explicit; - self - } - - fn outbound(mut self, outbound: usize) -> Self { - self.outbound = outbound; - self - } - - fn scoring(mut self, scoring: Option<(PeerScoreParams, PeerScoreThresholds)>) -> Self { - self.scoring = scoring; - self - } - - fn subscription_filter(mut self, subscription_filter: F) -> Self { - self.subscription_filter = subscription_filter; - self - } - - fn peer_kind(mut self, peer_kind: PeerKind) -> Self { - self.peer_kind = Some(peer_kind); - self - } -} - -fn inject_nodes() -> InjectNodes -where - D: DataTransform + Default + Clone + Send + 'static, - F: TopicSubscriptionFilter + Clone + Default + Send + 'static, -{ - InjectNodes::default() -} - -fn inject_nodes1() -> InjectNodes { - InjectNodes::::default() -} - -// helper functions for testing - -fn add_peer( - gs: &mut Behaviour, - topic_hashes: &[TopicHash], - outbound: bool, - explicit: bool, -) -> (PeerId, RpcReceiver) -where - D: DataTransform + Default + Clone + Send + 'static, - F: TopicSubscriptionFilter + Clone + Default + Send + 'static, -{ - add_peer_with_addr(gs, topic_hashes, outbound, explicit, Multiaddr::empty()) -} - -fn add_peer_with_addr( - gs: &mut Behaviour, - topic_hashes: &[TopicHash], - outbound: bool, - explicit: bool, - address: Multiaddr, -) -> (PeerId, RpcReceiver) -where - D: DataTransform + Default + Clone + Send + 'static, - F: TopicSubscriptionFilter + Clone + Default + Send + 'static, -{ - add_peer_with_addr_and_kind( - gs, - topic_hashes, - outbound, - explicit, - address, - Some(PeerKind::Gossipsubv1_1), - ) -} - -fn add_peer_with_addr_and_kind( - gs: &mut Behaviour, - topic_hashes: &[TopicHash], - outbound: bool, - explicit: bool, - address: Multiaddr, - kind: Option, -) -> (PeerId, RpcReceiver) -where - D: DataTransform + Default + Clone + Send + 'static, - F: TopicSubscriptionFilter + Clone + Default + Send + 'static, -{ - let peer = PeerId::random(); - let endpoint = if outbound { - ConnectedPoint::Dialer { - address, - role_override: Endpoint::Dialer, - port_use: PortUse::Reuse, - } - } else { - ConnectedPoint::Listener { - local_addr: Multiaddr::empty(), - send_back_addr: address, - } - }; - - let sender = RpcSender::new(gs.config.connection_handler_queue_len()); - let receiver = sender.new_receiver(); - let connection_id = ConnectionId::new_unchecked(0); - gs.connected_peers.insert( - peer, - PeerConnections { - kind: kind.clone().unwrap_or(PeerKind::Floodsub), - connections: vec![connection_id], - topics: Default::default(), - dont_send_received: LinkedHashMap::new(), - dont_send_sent: LinkedHashMap::new(), - sender, - }, - ); - - gs.on_swarm_event(FromSwarm::ConnectionEstablished(ConnectionEstablished { - peer_id: peer, - connection_id, - endpoint: &endpoint, - failed_addresses: &[], - other_established: 0, // first connection - })); - if let Some(kind) = kind { - gs.on_connection_handler_event( - peer, - ConnectionId::new_unchecked(0), - HandlerEvent::PeerKind(kind), - ); - } - if explicit { - gs.add_explicit_peer(&peer); - } - if !topic_hashes.is_empty() { - gs.handle_received_subscriptions( - &topic_hashes - .iter() - .cloned() - .map(|t| Subscription { - action: SubscriptionAction::Subscribe, - topic_hash: t, - }) - .collect::>(), - &peer, - ); - } - (peer, receiver) -} - -fn disconnect_peer(gs: &mut Behaviour, peer_id: &PeerId) -where - D: DataTransform + Default + Clone + Send + 'static, - F: TopicSubscriptionFilter + Clone + Default + Send + 'static, -{ - if let Some(peer_connections) = gs.connected_peers.get(peer_id) { - let fake_endpoint = ConnectedPoint::Dialer { - address: Multiaddr::empty(), - role_override: Endpoint::Dialer, - port_use: PortUse::Reuse, - }; // this is not relevant - // peer_connections.connections should never be empty. - - let mut active_connections = peer_connections.connections.len(); - for connection_id in peer_connections.connections.clone() { - active_connections = active_connections.checked_sub(1).unwrap(); - - gs.on_swarm_event(FromSwarm::ConnectionClosed(ConnectionClosed { - peer_id: *peer_id, - connection_id, - endpoint: &fake_endpoint, - remaining_established: active_connections, - cause: None, - })); - } - } -} - -// Converts a protobuf message into a gossipsub message for reading the Gossipsub event queue. -fn proto_to_message(rpc: &proto::RPC) -> Rpc { - // Store valid messages. - let mut messages = Vec::with_capacity(rpc.publish.len()); - let rpc = rpc.clone(); - for message in rpc.publish.into_iter() { - messages.push(RawMessage { - source: message.from.map(|x| PeerId::from_bytes(&x).unwrap()), - data: message.data.unwrap_or_default(), - sequence_number: message.seqno.map(|x| BigEndian::read_u64(&x)), // don't inform the application - topic: TopicHash::from_raw(message.topic), - signature: message.signature, // don't inform the application - key: None, - validated: false, - }); - } - let mut control_msgs = Vec::new(); - if let Some(rpc_control) = rpc.control { - // Collect the gossipsub control messages - let ihave_msgs: Vec = rpc_control - .ihave - .into_iter() - .map(|ihave| { - ControlAction::IHave(IHave { - topic_hash: TopicHash::from_raw(ihave.topic_id.unwrap_or_default()), - message_ids: ihave - .message_ids - .into_iter() - .map(MessageId::from) - .collect::>(), - }) - }) - .collect(); - - let iwant_msgs: Vec = rpc_control - .iwant - .into_iter() - .map(|iwant| { - ControlAction::IWant(IWant { - message_ids: iwant - .message_ids - .into_iter() - .map(MessageId::from) - .collect::>(), - }) - }) - .collect(); - - let graft_msgs: Vec = rpc_control - .graft - .into_iter() - .map(|graft| { - ControlAction::Graft(Graft { - topic_hash: TopicHash::from_raw(graft.topic_id.unwrap_or_default()), - }) - }) - .collect(); - - let mut prune_msgs = Vec::new(); - - for prune in rpc_control.prune { - // filter out invalid peers - let peers = prune - .peers - .into_iter() - .filter_map(|info| { - info.peer_id - .and_then(|id| PeerId::from_bytes(&id).ok()) - .map(|peer_id| - //TODO signedPeerRecord, see https://github.com/libp2p/specs/pull/217 - PeerInfo { - peer_id: Some(peer_id), - }) - }) - .collect::>(); - - let topic_hash = TopicHash::from_raw(prune.topic_id.unwrap_or_default()); - prune_msgs.push(ControlAction::Prune(Prune { - topic_hash, - peers, - backoff: prune.backoff, - })); - } - - control_msgs.extend(ihave_msgs); - control_msgs.extend(iwant_msgs); - control_msgs.extend(graft_msgs); - control_msgs.extend(prune_msgs); - } - - Rpc { - messages, - subscriptions: rpc - .subscriptions - .into_iter() - .map(|sub| Subscription { - action: if Some(true) == sub.subscribe { - SubscriptionAction::Subscribe - } else { - SubscriptionAction::Unsubscribe - }, - topic_hash: TopicHash::from_raw(sub.topic_id.unwrap_or_default()), - }) - .collect(), - control_msgs, - } -} - -#[test] -/// Test local node subscribing to a topic -fn test_subscribe() { - // The node should: - // - Create an empty vector in mesh[topic] - // - Send subscription request to all peers - // - run JOIN(topic) - - let subscribe_topic = vec![String::from("test_subscribe")]; - let (gs, _, receivers, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(subscribe_topic) - .to_subscribe(true) - .create_network(); - - assert!( - gs.mesh.contains_key(&topic_hashes[0]), - "Subscribe should add a new entry to the mesh[topic] hashmap" - ); - - // collect all the subscriptions - let subscriptions = receivers - .into_values() - .fold(0, |mut collected_subscriptions, c| { - let priority = c.priority.into_inner(); - while !priority.is_empty() { - if let Ok(RpcOut::Subscribe(_)) = priority.try_recv() { - collected_subscriptions += 1 - } - } - collected_subscriptions - }); - - // we sent a subscribe to all known peers - assert_eq!(subscriptions, 20); -} - -/// Test unsubscribe. -#[test] -fn test_unsubscribe() { - // Unsubscribe should: - // - Remove the mesh entry for topic - // - Send UNSUBSCRIBE to all known peers - // - Call Leave - - let topic_strings = vec![String::from("topic1"), String::from("topic2")]; - let topics = topic_strings - .iter() - .map(|t| Topic::new(t.clone())) - .collect::>(); - - // subscribe to topic_strings - let (mut gs, _, receivers, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(topic_strings) - .to_subscribe(true) - .create_network(); - - for topic_hash in &topic_hashes { - assert!( - gs.connected_peers - .values() - .any(|p| p.topics.contains(topic_hash)), - "Topic_peers contain a topic entry" - ); - assert!( - gs.mesh.contains_key(topic_hash), - "mesh should contain a topic entry" - ); - } - - // unsubscribe from both topics - assert!( - gs.unsubscribe(&topics[0]).unwrap(), - "should be able to unsubscribe successfully from each topic", - ); - assert!( - gs.unsubscribe(&topics[1]).unwrap(), - "should be able to unsubscribe successfully from each topic", - ); - - // collect all the subscriptions - let subscriptions = receivers - .into_values() - .fold(0, |mut collected_subscriptions, c| { - let priority = c.priority.into_inner(); - while !priority.is_empty() { - if let Ok(RpcOut::Subscribe(_)) = priority.try_recv() { - collected_subscriptions += 1 - } - } - collected_subscriptions - }); - - // we sent a unsubscribe to all known peers, for two topics - assert_eq!(subscriptions, 40); - - // check we clean up internal structures - for topic_hash in &topic_hashes { - assert!( - !gs.mesh.contains_key(topic_hash), - "All topics should have been removed from the mesh" - ); - } -} - -/// Test JOIN(topic) functionality. -#[test] -fn test_join() { - // The Join function should: - // - Remove peers from fanout[topic] - // - Add any fanout[topic] peers to the mesh (up to mesh_n) - // - Fill up to mesh_n peers from known gossipsub peers in the topic - // - Send GRAFT messages to all nodes added to the mesh - - // This test is not an isolated unit test, rather it uses higher level, - // subscribe/unsubscribe to perform the test. - - let topic_strings = vec![String::from("topic1"), String::from("topic2")]; - let topics = topic_strings - .iter() - .map(|t| Topic::new(t.clone())) - .collect::>(); - - let (mut gs, _, mut receivers, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(topic_strings) - .to_subscribe(true) - .create_network(); - - // Flush previous GRAFT messages. - receivers = flush_events(&mut gs, receivers); - - // unsubscribe, then call join to invoke functionality - assert!( - gs.unsubscribe(&topics[0]).unwrap(), - "should be able to unsubscribe successfully" - ); - assert!( - gs.unsubscribe(&topics[1]).unwrap(), - "should be able to unsubscribe successfully" - ); - - // re-subscribe - there should be peers associated with the topic - assert!( - gs.subscribe(&topics[0]).unwrap(), - "should be able to subscribe successfully" - ); - - // should have added mesh_n nodes to the mesh - assert!( - gs.mesh.get(&topic_hashes[0]).unwrap().len() == 6, - "Should have added 6 nodes to the mesh" - ); - - fn count_grafts( - receivers: HashMap, - ) -> (usize, HashMap) { - let mut new_receivers = HashMap::new(); - let mut acc = 0; - - for (peer_id, c) in receivers.into_iter() { - let priority = c.priority.into_inner(); - while !priority.is_empty() { - if let Ok(RpcOut::Graft(_)) = priority.try_recv() { - acc += 1; - } - } - new_receivers.insert( - peer_id, - RpcReceiver { - priority_len: c.priority_len, - priority: priority.peekable(), - non_priority: c.non_priority, - }, - ); - } - (acc, new_receivers) - } - - // there should be mesh_n GRAFT messages. - let (graft_messages, mut receivers) = count_grafts(receivers); - - assert_eq!( - graft_messages, 6, - "There should be 6 grafts messages sent to peers" - ); - - // verify fanout nodes - // add 3 random peers to the fanout[topic1] - gs.fanout - .insert(topic_hashes[1].clone(), Default::default()); - let mut new_peers: Vec = vec![]; - - for _ in 0..3 { - let random_peer = PeerId::random(); - // inform the behaviour of a new peer - let address = "/ip4/127.0.0.1".parse::().unwrap(); - gs.handle_established_inbound_connection( - ConnectionId::new_unchecked(0), - random_peer, - &address, - &address, - ) - .unwrap(); - let sender = RpcSender::new(gs.config.connection_handler_queue_len()); - let receiver = sender.new_receiver(); - let connection_id = ConnectionId::new_unchecked(0); - gs.connected_peers.insert( - random_peer, - PeerConnections { - kind: PeerKind::Floodsub, - connections: vec![connection_id], - topics: Default::default(), - dont_send_received: LinkedHashMap::new(), - dont_send_sent: LinkedHashMap::new(), - sender, - }, - ); - receivers.insert(random_peer, receiver); - - gs.on_swarm_event(FromSwarm::ConnectionEstablished(ConnectionEstablished { - peer_id: random_peer, - connection_id, - endpoint: &ConnectedPoint::Dialer { - address, - role_override: Endpoint::Dialer, - port_use: PortUse::Reuse, - }, - failed_addresses: &[], - other_established: 0, - })); - - // add the new peer to the fanout - let fanout_peers = gs.fanout.get_mut(&topic_hashes[1]).unwrap(); - fanout_peers.insert(random_peer); - new_peers.push(random_peer); - } - - // subscribe to topic1 - gs.subscribe(&topics[1]).unwrap(); - - // the three new peers should have been added, along with 3 more from the pool. - assert!( - gs.mesh.get(&topic_hashes[1]).unwrap().len() == 6, - "Should have added 6 nodes to the mesh" - ); - let mesh_peers = gs.mesh.get(&topic_hashes[1]).unwrap(); - for new_peer in new_peers { - assert!( - mesh_peers.contains(&new_peer), - "Fanout peer should be included in the mesh" - ); - } - - // there should now 6 graft messages to be sent - let (graft_messages, _) = count_grafts(receivers); - - assert_eq!( - graft_messages, 6, - "There should be 6 grafts messages sent to peers" - ); -} - -/// Test local node publish to subscribed topic -#[test] -fn test_publish_without_flood_publishing() { - // node should: - // - Send publish message to all peers - // - Insert message into gs.mcache and gs.received - - //turn off flood publish to test old behaviour - let config = ConfigBuilder::default() - .flood_publish(false) - .build() - .unwrap(); - - let publish_topic = String::from("test_publish"); - let (mut gs, _, receivers, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(vec![publish_topic.clone()]) - .to_subscribe(true) - .gs_config(config) - .create_network(); - - assert!( - gs.mesh.contains_key(&topic_hashes[0]), - "Subscribe should add a new entry to the mesh[topic] hashmap" - ); - - // all peers should be subscribed to the topic - assert_eq!( - gs.connected_peers - .values() - .filter(|p| p.topics.contains(&topic_hashes[0])) - .count(), - 20, - "Peers should be subscribed to the topic" - ); - - // publish on topic - let publish_data = vec![0; 42]; - gs.publish(Topic::new(publish_topic), publish_data).unwrap(); - - // Collect all publish messages - let publishes = receivers - .into_values() - .fold(vec![], |mut collected_publish, c| { - let priority = c.priority.into_inner(); - while !priority.is_empty() { - if let Ok(RpcOut::Publish { message, .. }) = priority.try_recv() { - collected_publish.push(message); - } - } - collected_publish - }); - - // Transform the inbound message - let message = &gs - .data_transform - .inbound_transform( - publishes - .first() - .expect("Should contain > 0 entries") - .clone(), - ) - .unwrap(); - - let msg_id = gs.config.message_id(message); - - let config: Config = Config::default(); - assert_eq!( - publishes.len(), - config.mesh_n(), - "Should send a publish message to at least mesh_n peers" - ); - - assert!( - gs.mcache.get(&msg_id).is_some(), - "Message cache should contain published message" - ); -} - -/// Test local node publish to unsubscribed topic -#[test] -fn test_fanout() { - // node should: - // - Populate fanout peers - // - Send publish message to fanout peers - // - Insert message into gs.mcache and gs.received - - //turn off flood publish to test fanout behaviour - let config = ConfigBuilder::default() - .flood_publish(false) - .build() - .unwrap(); - - let fanout_topic = String::from("test_fanout"); - let (mut gs, _, receivers, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(vec![fanout_topic.clone()]) - .to_subscribe(true) - .gs_config(config) - .create_network(); - - assert!( - gs.mesh.contains_key(&topic_hashes[0]), - "Subscribe should add a new entry to the mesh[topic] hashmap" - ); - // Unsubscribe from topic - assert!( - gs.unsubscribe(&Topic::new(fanout_topic.clone())).unwrap(), - "should be able to unsubscribe successfully from topic" - ); - - // Publish on unsubscribed topic - let publish_data = vec![0; 42]; - gs.publish(Topic::new(fanout_topic.clone()), publish_data) - .unwrap(); - - assert_eq!( - gs.fanout - .get(&TopicHash::from_raw(fanout_topic)) - .unwrap() - .len(), - gs.config.mesh_n(), - "Fanout should contain `mesh_n` peers for fanout topic" - ); - - // Collect all publish messages - let publishes = receivers - .into_values() - .fold(vec![], |mut collected_publish, c| { - let priority = c.priority.into_inner(); - while !priority.is_empty() { - if let Ok(RpcOut::Publish { message, .. }) = priority.try_recv() { - collected_publish.push(message); - } - } - collected_publish - }); - - // Transform the inbound message - let message = &gs - .data_transform - .inbound_transform( - publishes - .first() - .expect("Should contain > 0 entries") - .clone(), - ) - .unwrap(); - - let msg_id = gs.config.message_id(message); - - assert_eq!( - publishes.len(), - gs.config.mesh_n(), - "Should send a publish message to `mesh_n` fanout peers" - ); - - assert!( - gs.mcache.get(&msg_id).is_some(), - "Message cache should contain published message" - ); -} - -/// Test the gossipsub NetworkBehaviour peer connection logic. -#[test] -fn test_inject_connected() { - let (gs, peers, receivers, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(vec![String::from("topic1"), String::from("topic2")]) - .to_subscribe(true) - .create_network(); - - // check that our subscriptions are sent to each of the peers - // collect all the SendEvents - let subscriptions = receivers.into_iter().fold( - HashMap::>::new(), - |mut collected_subscriptions, (peer, c)| { - let priority = c.priority.into_inner(); - while !priority.is_empty() { - if let Ok(RpcOut::Subscribe(topic)) = priority.try_recv() { - let mut peer_subs = collected_subscriptions.remove(&peer).unwrap_or_default(); - peer_subs.push(topic.into_string()); - collected_subscriptions.insert(peer, peer_subs); - } - } - collected_subscriptions - }, - ); - - // check that there are two subscriptions sent to each peer - for peer_subs in subscriptions.values() { - assert!(peer_subs.contains(&String::from("topic1"))); - assert!(peer_subs.contains(&String::from("topic2"))); - assert_eq!(peer_subs.len(), 2); - } - - // check that there are 20 send events created - assert_eq!(subscriptions.len(), 20); - - // should add the new peers to `peer_topics` with an empty vec as a gossipsub node - for peer in peers { - let peer = gs.connected_peers.get(&peer).unwrap(); - assert!( - peer.topics == topic_hashes.iter().cloned().collect(), - "The topics for each node should all topics" - ); - } -} - -/// Test subscription handling -#[test] -fn test_handle_received_subscriptions() { - // For every subscription: - // SUBSCRIBE: - Add subscribed topic to peer_topics for peer. - // - Add peer to topics_peer. - // UNSUBSCRIBE - Remove topic from peer_topics for peer. - // - Remove peer from topic_peers. - - let topics = ["topic1", "topic2", "topic3", "topic4"] - .iter() - .map(|&t| String::from(t)) - .collect(); - let (mut gs, peers, _receivers, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(topics) - .to_subscribe(false) - .create_network(); - - // The first peer sends 3 subscriptions and 1 unsubscription - let mut subscriptions = topic_hashes[..3] - .iter() - .map(|topic_hash| Subscription { - action: SubscriptionAction::Subscribe, - topic_hash: topic_hash.clone(), - }) - .collect::>(); - - subscriptions.push(Subscription { - action: SubscriptionAction::Unsubscribe, - topic_hash: topic_hashes[topic_hashes.len() - 1].clone(), - }); - - let unknown_peer = PeerId::random(); - // process the subscriptions - // first and second peers send subscriptions - gs.handle_received_subscriptions(&subscriptions, &peers[0]); - gs.handle_received_subscriptions(&subscriptions, &peers[1]); - // unknown peer sends the same subscriptions - gs.handle_received_subscriptions(&subscriptions, &unknown_peer); - - // verify the result - - let peer = gs.connected_peers.get(&peers[0]).unwrap(); - assert!( - peer.topics - == topic_hashes - .iter() - .take(3) - .cloned() - .collect::>(), - "First peer should be subscribed to three topics" - ); - let peer1 = gs.connected_peers.get(&peers[1]).unwrap(); - assert!( - peer1.topics - == topic_hashes - .iter() - .take(3) - .cloned() - .collect::>(), - "Second peer should be subscribed to three topics" - ); - - assert!( - !gs.connected_peers.contains_key(&unknown_peer), - "Unknown peer should not have been added" - ); - - for topic_hash in topic_hashes[..3].iter() { - let topic_peers = gs - .connected_peers - .iter() - .filter(|(_, p)| p.topics.contains(topic_hash)) - .map(|(peer_id, _)| *peer_id) - .collect::>(); - assert!( - topic_peers == peers[..2].iter().cloned().collect(), - "Two peers should be added to the first three topics" - ); - } - - // Peer 0 unsubscribes from the first topic - - gs.handle_received_subscriptions( - &[Subscription { - action: SubscriptionAction::Unsubscribe, - topic_hash: topic_hashes[0].clone(), - }], - &peers[0], - ); - - let peer = gs.connected_peers.get(&peers[0]).unwrap(); - assert!( - peer.topics == topic_hashes[1..3].iter().cloned().collect::>(), - "Peer should be subscribed to two topics" - ); - - // only gossipsub at the moment - let topic_peers = gs - .connected_peers - .iter() - .filter(|(_, p)| p.topics.contains(&topic_hashes[0])) - .map(|(peer_id, _)| *peer_id) - .collect::>(); - - assert!( - topic_peers == peers[1..2].iter().cloned().collect(), - "Only the second peers should be in the first topic" - ); -} - -/// Test Gossipsub.get_random_peers() function -#[test] -fn test_get_random_peers() { - // generate a default Config - let gs_config = ConfigBuilder::default() - .validation_mode(ValidationMode::Anonymous) - .build() - .unwrap(); - // create a gossipsub struct - let mut gs: Behaviour = Behaviour::new(MessageAuthenticity::Anonymous, gs_config).unwrap(); - - // create a topic and fill it with some peers - let topic_hash = Topic::new("Test").hash(); - let mut peers = vec![]; - let mut topics = BTreeSet::new(); - topics.insert(topic_hash.clone()); - - for _ in 0..20 { - let peer_id = PeerId::random(); - peers.push(peer_id); - gs.connected_peers.insert( - peer_id, - PeerConnections { - kind: PeerKind::Gossipsubv1_1, - connections: vec![ConnectionId::new_unchecked(0)], - topics: topics.clone(), - sender: RpcSender::new(gs.config.connection_handler_queue_len()), - dont_send_sent: LinkedHashMap::new(), - dont_send_received: LinkedHashMap::new(), - }, - ); - } - - let random_peers = get_random_peers(&gs.connected_peers, &topic_hash, 5, |_| true); - assert_eq!(random_peers.len(), 5, "Expected 5 peers to be returned"); - let random_peers = get_random_peers(&gs.connected_peers, &topic_hash, 30, |_| true); - assert!(random_peers.len() == 20, "Expected 20 peers to be returned"); - assert!( - random_peers == peers.iter().cloned().collect(), - "Expected no shuffling" - ); - let random_peers = get_random_peers(&gs.connected_peers, &topic_hash, 20, |_| true); - assert!(random_peers.len() == 20, "Expected 20 peers to be returned"); - assert!( - random_peers == peers.iter().cloned().collect(), - "Expected no shuffling" - ); - let random_peers = get_random_peers(&gs.connected_peers, &topic_hash, 0, |_| true); - assert!(random_peers.is_empty(), "Expected 0 peers to be returned"); - // test the filter - let random_peers = get_random_peers(&gs.connected_peers, &topic_hash, 5, |_| false); - assert!(random_peers.is_empty(), "Expected 0 peers to be returned"); - let random_peers = get_random_peers(&gs.connected_peers, &topic_hash, 10, { - |peer| peers.contains(peer) - }); - assert!(random_peers.len() == 10, "Expected 10 peers to be returned"); -} - -/// Tests that the correct message is sent when a peer asks for a message in our cache. -#[test] -fn test_handle_iwant_msg_cached() { - let (mut gs, peers, receivers, _) = inject_nodes1() - .peer_no(20) - .topics(Vec::new()) - .to_subscribe(true) - .create_network(); - - let raw_message = RawMessage { - source: Some(peers[11]), - data: vec![1, 2, 3, 4], - sequence_number: Some(1u64), - topic: TopicHash::from_raw("topic"), - signature: None, - key: None, - validated: true, - }; - - // Transform the inbound message - let message = &gs - .data_transform - .inbound_transform(raw_message.clone()) - .unwrap(); - - let msg_id = gs.config.message_id(message); - gs.mcache.put(&msg_id, raw_message); - - gs.handle_iwant(&peers[7], vec![msg_id.clone()]); - - // the messages we are sending - let sent_messages = receivers - .into_values() - .fold(vec![], |mut collected_messages, c| { - let non_priority = c.non_priority.into_inner(); - while !non_priority.is_empty() { - if let Ok(RpcOut::Forward { message, .. }) = non_priority.try_recv() { - collected_messages.push(message) - } - } - collected_messages - }); - - assert!( - sent_messages - .iter() - .map(|msg| gs.data_transform.inbound_transform(msg.clone()).unwrap()) - .any(|msg| gs.config.message_id(&msg) == msg_id), - "Expected the cached message to be sent to an IWANT peer" - ); -} - -/// Tests that messages are sent correctly depending on the shifting of the message cache. -#[test] -fn test_handle_iwant_msg_cached_shifted() { - let (mut gs, peers, mut receivers, _) = inject_nodes1() - .peer_no(20) - .topics(Vec::new()) - .to_subscribe(true) - .create_network(); - - // perform 10 memshifts and check that it leaves the cache - for shift in 1..10 { - let raw_message = RawMessage { - source: Some(peers[11]), - data: vec![1, 2, 3, 4], - sequence_number: Some(shift), - topic: TopicHash::from_raw("topic"), - signature: None, - key: None, - validated: true, - }; - - // Transform the inbound message - let message = &gs - .data_transform - .inbound_transform(raw_message.clone()) - .unwrap(); - - let msg_id = gs.config.message_id(message); - gs.mcache.put(&msg_id, raw_message); - for _ in 0..shift { - gs.mcache.shift(); - } - - gs.handle_iwant(&peers[7], vec![msg_id.clone()]); - - // is the message is being sent? - let mut message_exists = false; - receivers = receivers.into_iter().map(|(peer_id, c)| { - let non_priority = c.non_priority.into_inner(); - while !non_priority.is_empty() { - if matches!(non_priority.try_recv(), Ok(RpcOut::Forward{message, timeout: _ }) if - gs.config.message_id( - &gs.data_transform - .inbound_transform(message.clone()) - .unwrap(), - ) == msg_id) - { - message_exists = true; - } - } - ( - peer_id, - RpcReceiver { - priority_len: c.priority_len, - priority: c.priority, - non_priority: non_priority.peekable(), - }, - ) - }).collect(); - // default history_length is 5, expect no messages after shift > 5 - if shift < 5 { - assert!( - message_exists, - "Expected the cached message to be sent to an IWANT peer before 5 shifts" - ); - } else { - assert!( - !message_exists, - "Expected the cached message to not be sent to an IWANT peer after 5 shifts" - ); - } - } -} - -/// tests that an event is not created when a peers asks for a message not in our cache -#[test] -fn test_handle_iwant_msg_not_cached() { - let (mut gs, peers, _, _) = inject_nodes1() - .peer_no(20) - .topics(Vec::new()) - .to_subscribe(true) - .create_network(); - - let events_before = gs.events.len(); - gs.handle_iwant(&peers[7], vec![MessageId::new(b"unknown id")]); - let events_after = gs.events.len(); - - assert_eq!( - events_before, events_after, - "Expected event count to stay the same" - ); -} - -/// tests that an event is created when a peer shares that it has a message we want -#[test] -fn test_handle_ihave_subscribed_and_msg_not_cached() { - let (mut gs, peers, mut receivers, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .create_network(); - - gs.handle_ihave( - &peers[7], - vec![(topic_hashes[0].clone(), vec![MessageId::new(b"unknown id")])], - ); - - // check that we sent an IWANT request for `unknown id` - let mut iwant_exists = false; - let receiver = receivers.remove(&peers[7]).unwrap(); - let non_priority = receiver.non_priority.into_inner(); - while !non_priority.is_empty() { - if let Ok(RpcOut::IWant(IWant { message_ids })) = non_priority.try_recv() { - if message_ids - .iter() - .any(|m| *m == MessageId::new(b"unknown id")) - { - iwant_exists = true; - break; - } - } - } - - assert!( - iwant_exists, - "Expected to send an IWANT control message for unkown message id" - ); -} - -/// tests that an event is not created when a peer shares that it has a message that -/// we already have -#[test] -fn test_handle_ihave_subscribed_and_msg_cached() { - let (mut gs, peers, _, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .create_network(); - - let msg_id = MessageId::new(b"known id"); - - let events_before = gs.events.len(); - gs.handle_ihave(&peers[7], vec![(topic_hashes[0].clone(), vec![msg_id])]); - let events_after = gs.events.len(); - - assert_eq!( - events_before, events_after, - "Expected event count to stay the same" - ) -} - -/// test that an event is not created when a peer shares that it has a message in -/// a topic that we are not subscribed to -#[test] -fn test_handle_ihave_not_subscribed() { - let (mut gs, peers, _, _) = inject_nodes1() - .peer_no(20) - .topics(vec![]) - .to_subscribe(true) - .create_network(); - - let events_before = gs.events.len(); - gs.handle_ihave( - &peers[7], - vec![( - TopicHash::from_raw(String::from("unsubscribed topic")), - vec![MessageId::new(b"irrelevant id")], - )], - ); - let events_after = gs.events.len(); - - assert_eq!( - events_before, events_after, - "Expected event count to stay the same" - ) -} - -/// tests that a peer is added to our mesh when we are both subscribed -/// to the same topic -#[test] -fn test_handle_graft_is_subscribed() { - let (mut gs, peers, _, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .create_network(); - - gs.handle_graft(&peers[7], topic_hashes.clone()); - - assert!( - gs.mesh.get(&topic_hashes[0]).unwrap().contains(&peers[7]), - "Expected peer to have been added to mesh" - ); -} - -/// tests that a peer is not added to our mesh when they are subscribed to -/// a topic that we are not -#[test] -fn test_handle_graft_is_not_subscribed() { - let (mut gs, peers, _, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .create_network(); - - gs.handle_graft( - &peers[7], - vec![TopicHash::from_raw(String::from("unsubscribed topic"))], - ); - - assert!( - !gs.mesh.get(&topic_hashes[0]).unwrap().contains(&peers[7]), - "Expected peer to have been added to mesh" - ); -} - -/// tests multiple topics in a single graft message -#[test] -fn test_handle_graft_multiple_topics() { - let topics: Vec = ["topic1", "topic2", "topic3", "topic4"] - .iter() - .map(|&t| String::from(t)) - .collect(); - - let (mut gs, peers, _, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(topics) - .to_subscribe(true) - .create_network(); - - let mut their_topics = topic_hashes.clone(); - // their_topics = [topic1, topic2, topic3] - // our_topics = [topic1, topic2, topic4] - their_topics.pop(); - gs.leave(&their_topics[2]); - - gs.handle_graft(&peers[7], their_topics.clone()); - - for hash in topic_hashes.iter().take(2) { - assert!( - gs.mesh.get(hash).unwrap().contains(&peers[7]), - "Expected peer to be in the mesh for the first 2 topics" - ); - } - - assert!( - !gs.mesh.contains_key(&topic_hashes[2]), - "Expected the second topic to not be in the mesh" - ); -} - -/// tests that a peer is removed from our mesh -#[test] -fn test_handle_prune_peer_in_mesh() { - let (mut gs, peers, _, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .create_network(); - - // insert peer into our mesh for 'topic1' - gs.mesh - .insert(topic_hashes[0].clone(), peers.iter().cloned().collect()); - assert!( - gs.mesh.get(&topic_hashes[0]).unwrap().contains(&peers[7]), - "Expected peer to be in mesh" - ); - - gs.handle_prune( - &peers[7], - topic_hashes - .iter() - .map(|h| (h.clone(), vec![], None)) - .collect(), - ); - assert!( - !gs.mesh.get(&topic_hashes[0]).unwrap().contains(&peers[7]), - "Expected peer to be removed from mesh" - ); -} - -fn count_control_msgs( - receivers: HashMap, - mut filter: impl FnMut(&PeerId, &RpcOut) -> bool, -) -> (usize, HashMap) { - let mut new_receivers = HashMap::new(); - let mut collected_messages = 0; - for (peer_id, c) in receivers.into_iter() { - let priority = c.priority.into_inner(); - let non_priority = c.non_priority.into_inner(); - while !priority.is_empty() || !non_priority.is_empty() { - if let Ok(rpc) = priority.try_recv() { - if filter(&peer_id, &rpc) { - collected_messages += 1; - } - } - if let Ok(rpc) = non_priority.try_recv() { - if filter(&peer_id, &rpc) { - collected_messages += 1; - } - } - } - new_receivers.insert( - peer_id, - RpcReceiver { - priority_len: c.priority_len, - priority: priority.peekable(), - non_priority: non_priority.peekable(), - }, - ); - } - (collected_messages, new_receivers) -} - -fn flush_events( - gs: &mut Behaviour, - receivers: HashMap, -) -> HashMap { - gs.events.clear(); - let mut new_receivers = HashMap::new(); - for (peer_id, c) in receivers.into_iter() { - let priority = c.priority.into_inner(); - let non_priority = c.non_priority.into_inner(); - while !priority.is_empty() || !non_priority.is_empty() { - let _ = priority.try_recv(); - let _ = non_priority.try_recv(); - } - new_receivers.insert( - peer_id, - RpcReceiver { - priority_len: c.priority_len, - priority: priority.peekable(), - non_priority: non_priority.peekable(), - }, - ); - } - new_receivers -} - -/// tests that a peer added as explicit peer gets connected to -#[test] -fn test_explicit_peer_gets_connected() { - let (mut gs, _, _, _) = inject_nodes1() - .peer_no(0) - .topics(Vec::new()) - .to_subscribe(true) - .create_network(); - - //create new peer - let peer = PeerId::random(); - - //add peer as explicit peer - gs.add_explicit_peer(&peer); - - let num_events = gs - .events - .iter() - .filter(|e| match e { - ToSwarm::Dial { opts } => opts.get_peer_id() == Some(peer), - _ => false, - }) - .count(); - - assert_eq!( - num_events, 1, - "There was no dial peer event for the explicit peer" - ); -} - -#[test] -fn test_explicit_peer_reconnects() { - let config = ConfigBuilder::default() - .check_explicit_peers_ticks(2) - .build() - .unwrap(); - let (mut gs, others, receivers, _) = inject_nodes1() - .peer_no(1) - .topics(Vec::new()) - .to_subscribe(true) - .gs_config(config) - .create_network(); - - let peer = others.first().unwrap(); - - //add peer as explicit peer - gs.add_explicit_peer(peer); - - flush_events(&mut gs, receivers); - - //disconnect peer - disconnect_peer(&mut gs, peer); - - gs.heartbeat(); - - //check that no reconnect after first heartbeat since `explicit_peer_ticks == 2` - assert_eq!( - gs.events - .iter() - .filter(|e| match e { - ToSwarm::Dial { opts } => opts.get_peer_id() == Some(*peer), - _ => false, - }) - .count(), - 0, - "There was a dial peer event before explicit_peer_ticks heartbeats" - ); - - gs.heartbeat(); - - //check that there is a reconnect after second heartbeat - assert!( - gs.events - .iter() - .filter(|e| match e { - ToSwarm::Dial { opts } => opts.get_peer_id() == Some(*peer), - _ => false, - }) - .count() - >= 1, - "There was no dial peer event for the explicit peer" - ); -} - -#[test] -fn test_handle_graft_explicit_peer() { - let (mut gs, peers, receivers, topic_hashes) = inject_nodes1() - .peer_no(1) - .topics(vec![String::from("topic1"), String::from("topic2")]) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .create_network(); - - let peer = peers.first().unwrap(); - - gs.handle_graft(peer, topic_hashes.clone()); - - //peer got not added to mesh - assert!(gs.mesh[&topic_hashes[0]].is_empty()); - assert!(gs.mesh[&topic_hashes[1]].is_empty()); - - //check prunes - let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { - peer_id == peer - && match m { - RpcOut::Prune(Prune { topic_hash, .. }) => { - topic_hash == &topic_hashes[0] || topic_hash == &topic_hashes[1] - } - _ => false, - } - }); - assert!( - control_msgs >= 2, - "Not enough prunes sent when grafting from explicit peer" - ); -} - -#[test] -fn explicit_peers_not_added_to_mesh_on_receiving_subscription() { - let (gs, peers, receivers, topic_hashes) = inject_nodes1() - .peer_no(2) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .create_network(); - - //only peer 1 is in the mesh not peer 0 (which is an explicit peer) - assert_eq!( - gs.mesh[&topic_hashes[0]], - vec![peers[1]].into_iter().collect() - ); - - //assert that graft gets created to non-explicit peer - let (control_msgs, receivers) = count_control_msgs(receivers, |peer_id, m| { - peer_id == &peers[1] && matches!(m, RpcOut::Graft { .. }) - }); - assert!( - control_msgs >= 1, - "No graft message got created to non-explicit peer" - ); - - //assert that no graft gets created to explicit peer - let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { - peer_id == &peers[0] && matches!(m, RpcOut::Graft { .. }) - }); - assert_eq!( - control_msgs, 0, - "A graft message got created to an explicit peer" - ); -} - -#[test] -fn do_not_graft_explicit_peer() { - let (mut gs, others, receivers, topic_hashes) = inject_nodes1() - .peer_no(1) - .topics(vec![String::from("topic")]) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .create_network(); - - gs.heartbeat(); - - //mesh stays empty - assert_eq!(gs.mesh[&topic_hashes[0]], BTreeSet::new()); - - //assert that no graft gets created to explicit peer - let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { - peer_id == &others[0] && matches!(m, RpcOut::Graft { .. }) - }); - assert_eq!( - control_msgs, 0, - "A graft message got created to an explicit peer" - ); -} - -#[test] -fn do_forward_messages_to_explicit_peers() { - let (mut gs, peers, receivers, topic_hashes) = inject_nodes1() - .peer_no(2) - .topics(vec![String::from("topic1"), String::from("topic2")]) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .create_network(); - - let local_id = PeerId::random(); - - let message = RawMessage { - source: Some(peers[1]), - data: vec![12], - sequence_number: Some(0), - topic: topic_hashes[0].clone(), - signature: None, - key: None, - validated: true, - }; - gs.handle_received_message(message.clone(), &local_id); - assert_eq!( - receivers.into_iter().fold(0, |mut fwds, (peer_id, c)| { - let non_priority = c.non_priority.into_inner(); - while !non_priority.is_empty() { - if matches!(non_priority.try_recv(), Ok(RpcOut::Forward{message: m, timeout: _}) if peer_id == peers[0] && m.data == message.data) { - fwds +=1; - } - } - fwds - }), - 1, - "The message did not get forwarded to the explicit peer" - ); -} - -#[test] -fn explicit_peers_not_added_to_mesh_on_subscribe() { - let (mut gs, peers, receivers, _) = inject_nodes1() - .peer_no(2) - .topics(Vec::new()) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .create_network(); - - //create new topic, both peers subscribing to it but we do not subscribe to it - let topic = Topic::new(String::from("t")); - let topic_hash = topic.hash(); - for peer in peers.iter().take(2) { - gs.handle_received_subscriptions( - &[Subscription { - action: SubscriptionAction::Subscribe, - topic_hash: topic_hash.clone(), - }], - peer, - ); - } - - //subscribe now to topic - gs.subscribe(&topic).unwrap(); - - //only peer 1 is in the mesh not peer 0 (which is an explicit peer) - assert_eq!(gs.mesh[&topic_hash], vec![peers[1]].into_iter().collect()); - - //assert that graft gets created to non-explicit peer - let (control_msgs, receivers) = count_control_msgs(receivers, |peer_id, m| { - peer_id == &peers[1] && matches!(m, RpcOut::Graft { .. }) - }); - assert!( - control_msgs > 0, - "No graft message got created to non-explicit peer" - ); - - //assert that no graft gets created to explicit peer - let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { - peer_id == &peers[0] && matches!(m, RpcOut::Graft { .. }) - }); - assert_eq!( - control_msgs, 0, - "A graft message got created to an explicit peer" - ); -} - -#[test] -fn explicit_peers_not_added_to_mesh_from_fanout_on_subscribe() { - let (mut gs, peers, receivers, _) = inject_nodes1() - .peer_no(2) - .topics(Vec::new()) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .create_network(); - - //create new topic, both peers subscribing to it but we do not subscribe to it - let topic = Topic::new(String::from("t")); - let topic_hash = topic.hash(); - for peer in peers.iter().take(2) { - gs.handle_received_subscriptions( - &[Subscription { - action: SubscriptionAction::Subscribe, - topic_hash: topic_hash.clone(), - }], - peer, - ); - } - - //we send a message for this topic => this will initialize the fanout - gs.publish(topic.clone(), vec![1, 2, 3]).unwrap(); - - //subscribe now to topic - gs.subscribe(&topic).unwrap(); - - //only peer 1 is in the mesh not peer 0 (which is an explicit peer) - assert_eq!(gs.mesh[&topic_hash], vec![peers[1]].into_iter().collect()); - - //assert that graft gets created to non-explicit peer - let (control_msgs, receivers) = count_control_msgs(receivers, |peer_id, m| { - peer_id == &peers[1] && matches!(m, RpcOut::Graft { .. }) - }); - assert!( - control_msgs >= 1, - "No graft message got created to non-explicit peer" - ); - - //assert that no graft gets created to explicit peer - let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { - peer_id == &peers[0] && matches!(m, RpcOut::Graft { .. }) - }); - assert_eq!( - control_msgs, 0, - "A graft message got created to an explicit peer" - ); -} - -#[test] -fn no_gossip_gets_sent_to_explicit_peers() { - let (mut gs, peers, mut receivers, topic_hashes) = inject_nodes1() - .peer_no(2) - .topics(vec![String::from("topic1"), String::from("topic2")]) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .create_network(); - - let local_id = PeerId::random(); - - let message = RawMessage { - source: Some(peers[1]), - data: vec![], - sequence_number: Some(0), - topic: topic_hashes[0].clone(), - signature: None, - key: None, - validated: true, - }; - - //forward the message - gs.handle_received_message(message, &local_id); - - //simulate multiple gossip calls (for randomness) - for _ in 0..3 { - gs.emit_gossip(); - } - - //assert that no gossip gets sent to explicit peer - let receiver = receivers.remove(&peers[0]).unwrap(); - let mut gossips = 0; - let non_priority = receiver.non_priority.into_inner(); - while !non_priority.is_empty() { - if let Ok(RpcOut::IHave(_)) = non_priority.try_recv() { - gossips += 1; - } - } - assert_eq!(gossips, 0, "Gossip got emitted to explicit peer"); -} - -/// Tests the mesh maintenance addition -#[test] -fn test_mesh_addition() { - let config: Config = Config::default(); - - // Adds mesh_low peers and PRUNE 2 giving us a deficit. - let (mut gs, peers, _receivers, topics) = inject_nodes1() - .peer_no(config.mesh_n() + 1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .create_network(); - - let to_remove_peers = config.mesh_n() + 1 - config.mesh_n_low() - 1; - - for peer in peers.iter().take(to_remove_peers) { - gs.handle_prune( - peer, - topics.iter().map(|h| (h.clone(), vec![], None)).collect(), - ); - } - - // Verify the pruned peers are removed from the mesh. - assert_eq!( - gs.mesh.get(&topics[0]).unwrap().len(), - config.mesh_n_low() - 1 - ); - - // run a heartbeat - gs.heartbeat(); - - // Peers should be added to reach mesh_n - assert_eq!(gs.mesh.get(&topics[0]).unwrap().len(), config.mesh_n()); -} - -/// Tests the mesh maintenance subtraction -#[test] -fn test_mesh_subtraction() { - let config = Config::default(); - - // Adds mesh_low peers and PRUNE 2 giving us a deficit. - let n = config.mesh_n_high() + 10; - //make all outbound connections so that we allow grafting to all - let (mut gs, peers, _receivers, topics) = inject_nodes1() - .peer_no(n) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config.clone()) - .outbound(n) - .create_network(); - - // graft all the peers - for peer in peers { - gs.handle_graft(&peer, topics.clone()); - } - - // run a heartbeat - gs.heartbeat(); - - // Peers should be removed to reach mesh_n - assert_eq!(gs.mesh.get(&topics[0]).unwrap().len(), config.mesh_n()); -} - -#[test] -fn test_connect_to_px_peers_on_handle_prune() { - let config: Config = Config::default(); - - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .create_network(); - - //handle prune from single peer with px peers - - let mut px = Vec::new(); - //propose more px peers than config.prune_peers() - for _ in 0..config.prune_peers() + 5 { - px.push(PeerInfo { - peer_id: Some(PeerId::random()), - }); - } - - gs.handle_prune( - &peers[0], - vec![( - topics[0].clone(), - px.clone(), - Some(config.prune_backoff().as_secs()), - )], - ); - - //Check DialPeer events for px peers - let dials: Vec<_> = gs - .events - .iter() - .filter_map(|e| match e { - ToSwarm::Dial { opts } => opts.get_peer_id(), - _ => None, - }) - .collect(); - - // Exactly config.prune_peers() many random peers should be dialled - assert_eq!(dials.len(), config.prune_peers()); - - let dials_set: HashSet<_> = dials.into_iter().collect(); - - // No duplicates - assert_eq!(dials_set.len(), config.prune_peers()); - - //all dial peers must be in px - assert!(dials_set.is_subset( - &px.iter() - .map(|i| *i.peer_id.as_ref().unwrap()) - .collect::>() - )); -} - -#[test] -fn test_send_px_and_backoff_in_prune() { - let config: Config = Config::default(); - - //build mesh with enough peers for px - let (mut gs, peers, receivers, topics) = inject_nodes1() - .peer_no(config.prune_peers() + 1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .create_network(); - - //send prune to peer - gs.send_graft_prune( - HashMap::new(), - vec![(peers[0], vec![topics[0].clone()])] - .into_iter() - .collect(), - HashSet::new(), - ); - - //check prune message - let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { - peer_id == &peers[0] - && match m { - RpcOut::Prune(Prune { - topic_hash, - peers, - backoff, - }) => { - topic_hash == &topics[0] && - peers.len() == config.prune_peers() && - //all peers are different - peers.iter().collect::>().len() == - config.prune_peers() && - backoff.unwrap() == config.prune_backoff().as_secs() - } - _ => false, - } - }); - assert_eq!(control_msgs, 1); -} - -#[test] -fn test_prune_backoffed_peer_on_graft() { - let config: Config = Config::default(); - - //build mesh with enough peers for px - let (mut gs, peers, receivers, topics) = inject_nodes1() - .peer_no(config.prune_peers() + 1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .create_network(); - - //remove peer from mesh and send prune to peer => this adds a backoff for this peer - gs.mesh.get_mut(&topics[0]).unwrap().remove(&peers[0]); - gs.send_graft_prune( - HashMap::new(), - vec![(peers[0], vec![topics[0].clone()])] - .into_iter() - .collect(), - HashSet::new(), - ); - - //ignore all messages until now - let receivers = flush_events(&mut gs, receivers); - - //handle graft - gs.handle_graft(&peers[0], vec![topics[0].clone()]); - - //check prune message - let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { - peer_id == &peers[0] - && match m { - RpcOut::Prune(Prune { - topic_hash, - peers, - backoff, - }) => { - topic_hash == &topics[0] && - //no px in this case - peers.is_empty() && - backoff.unwrap() == config.prune_backoff().as_secs() - } - _ => false, - } - }); - assert_eq!(control_msgs, 1); -} - -#[test] -fn test_do_not_graft_within_backoff_period() { - let config = ConfigBuilder::default() - .backoff_slack(1) - .heartbeat_interval(Duration::from_millis(100)) - .build() - .unwrap(); - //only one peer => mesh too small and will try to regraft as early as possible - let (mut gs, peers, receivers, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .create_network(); - - //handle prune from peer with backoff of one second - gs.handle_prune(&peers[0], vec![(topics[0].clone(), Vec::new(), Some(1))]); - - //forget all events until now - let receivers = flush_events(&mut gs, receivers); - - //call heartbeat - gs.heartbeat(); - - //Sleep for one second and apply 10 regular heartbeats (interval = 100ms). - for _ in 0..10 { - sleep(Duration::from_millis(100)); - gs.heartbeat(); - } - - //Check that no graft got created (we have backoff_slack = 1 therefore one more heartbeat - // is needed). - let (control_msgs, receivers) = - count_control_msgs(receivers, |_, m| matches!(m, RpcOut::Graft { .. })); - assert_eq!( - control_msgs, 0, - "Graft message created too early within backoff period" - ); - - //Heartbeat one more time this should graft now - sleep(Duration::from_millis(100)); - gs.heartbeat(); - - //check that graft got created - let (control_msgs, _) = count_control_msgs(receivers, |_, m| matches!(m, RpcOut::Graft { .. })); - assert!( - control_msgs > 0, - "No graft message was created after backoff period" - ); -} - -#[test] -fn test_do_not_graft_within_default_backoff_period_after_receiving_prune_without_backoff() { - //set default backoff period to 1 second - let config = ConfigBuilder::default() - .prune_backoff(Duration::from_millis(90)) - .backoff_slack(1) - .heartbeat_interval(Duration::from_millis(100)) - .build() - .unwrap(); - //only one peer => mesh too small and will try to regraft as early as possible - let (mut gs, peers, receivers, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .create_network(); - - //handle prune from peer without a specified backoff - gs.handle_prune(&peers[0], vec![(topics[0].clone(), Vec::new(), None)]); - - //forget all events until now - let receivers = flush_events(&mut gs, receivers); - - //call heartbeat - gs.heartbeat(); - - //Apply one more heartbeat - sleep(Duration::from_millis(100)); - gs.heartbeat(); - - //Check that no graft got created (we have backoff_slack = 1 therefore one more heartbeat - // is needed). - let (control_msgs, receivers) = - count_control_msgs(receivers, |_, m| matches!(m, RpcOut::Graft { .. })); - assert_eq!( - control_msgs, 0, - "Graft message created too early within backoff period" - ); - - //Heartbeat one more time this should graft now - sleep(Duration::from_millis(100)); - gs.heartbeat(); - - //check that graft got created - let (control_msgs, _) = count_control_msgs(receivers, |_, m| matches!(m, RpcOut::Graft { .. })); - assert!( - control_msgs > 0, - "No graft message was created after backoff period" - ); -} - -#[test] -fn test_unsubscribe_backoff() { - const HEARTBEAT_INTERVAL: Duration = Duration::from_millis(100); - let config = ConfigBuilder::default() - .backoff_slack(1) - // ensure a prune_backoff > unsubscribe_backoff - .prune_backoff(Duration::from_secs(5)) - .unsubscribe_backoff(1) - .heartbeat_interval(HEARTBEAT_INTERVAL) - .build() - .unwrap(); - - let topic = String::from("test"); - // only one peer => mesh too small and will try to regraft as early as possible - let (mut gs, _, receivers, topics) = inject_nodes1() - .peer_no(1) - .topics(vec![topic.clone()]) - .to_subscribe(true) - .gs_config(config) - .create_network(); - - let _ = gs.unsubscribe(&Topic::new(topic)); - - let (control_msgs, receivers) = count_control_msgs(receivers, |_, m| match m { - RpcOut::Prune(Prune { backoff, .. }) => backoff == &Some(1), - _ => false, - }); - assert_eq!( - control_msgs, 1, - "Peer should be pruned with `unsubscribe_backoff`." - ); - - let _ = gs.subscribe(&Topic::new(topics[0].to_string())); - - // forget all events until now - let receivers = flush_events(&mut gs, receivers); - - // call heartbeat - gs.heartbeat(); - - // Sleep for one second and apply 10 regular heartbeats (interval = 100ms). - for _ in 0..10 { - sleep(HEARTBEAT_INTERVAL); - gs.heartbeat(); - } - - // Check that no graft got created (we have backoff_slack = 1 therefore one more heartbeat - // is needed). - let (control_msgs, receivers) = - count_control_msgs(receivers, |_, m| matches!(m, RpcOut::Graft { .. })); - assert_eq!( - control_msgs, 0, - "Graft message created too early within backoff period" - ); - - // Heartbeat one more time this should graft now - sleep(HEARTBEAT_INTERVAL); - gs.heartbeat(); - - // check that graft got created - let (control_msgs, _) = count_control_msgs(receivers, |_, m| matches!(m, RpcOut::Graft { .. })); - assert!( - control_msgs > 0, - "No graft message was created after backoff period" - ); -} - -#[test] -fn test_flood_publish() { - let config: Config = Config::default(); - - let topic = "test"; - // Adds more peers than mesh can hold to test flood publishing - let (mut gs, _, receivers, _) = inject_nodes1() - .peer_no(config.mesh_n_high() + 10) - .topics(vec![topic.into()]) - .to_subscribe(true) - .create_network(); - - //publish message - let publish_data = vec![0; 42]; - gs.publish(Topic::new(topic), publish_data).unwrap(); - - // Collect all publish messages - let publishes = receivers - .into_values() - .fold(vec![], |mut collected_publish, c| { - let priority = c.priority.into_inner(); - while !priority.is_empty() { - if let Ok(RpcOut::Publish { message, .. }) = priority.try_recv() { - collected_publish.push(message); - } - } - collected_publish - }); - - // Transform the inbound message - let message = &gs - .data_transform - .inbound_transform( - publishes - .first() - .expect("Should contain > 0 entries") - .clone(), - ) - .unwrap(); - - let msg_id = gs.config.message_id(message); - - let config: Config = Config::default(); - assert_eq!( - publishes.len(), - config.mesh_n_high() + 10, - "Should send a publish message to all known peers" - ); - - assert!( - gs.mcache.get(&msg_id).is_some(), - "Message cache should contain published message" - ); -} - -#[test] -fn test_gossip_to_at_least_gossip_lazy_peers() { - let config: Config = Config::default(); - - //add more peers than in mesh to test gossipping - //by default only mesh_n_low peers will get added to mesh - let (mut gs, _, receivers, topic_hashes) = inject_nodes1() - .peer_no(config.mesh_n_low() + config.gossip_lazy() + 1) - .topics(vec!["topic".into()]) - .to_subscribe(true) - .create_network(); - - //receive message - let raw_message = RawMessage { - source: Some(PeerId::random()), - data: vec![], - sequence_number: Some(0), - topic: topic_hashes[0].clone(), - signature: None, - key: None, - validated: true, - }; - gs.handle_received_message(raw_message.clone(), &PeerId::random()); - - //emit gossip - gs.emit_gossip(); - - // Transform the inbound message - let message = &gs.data_transform.inbound_transform(raw_message).unwrap(); - - let msg_id = gs.config.message_id(message); - - //check that exactly config.gossip_lazy() many gossip messages were sent. - let (control_msgs, _) = count_control_msgs(receivers, |_, action| match action { - RpcOut::IHave(IHave { - topic_hash, - message_ids, - }) => topic_hash == &topic_hashes[0] && message_ids.iter().any(|id| id == &msg_id), - _ => false, - }); - assert_eq!(control_msgs, config.gossip_lazy()); -} - -#[test] -fn test_gossip_to_at_most_gossip_factor_peers() { - let config: Config = Config::default(); - - //add a lot of peers - let m = config.mesh_n_low() + config.gossip_lazy() * (2.0 / config.gossip_factor()) as usize; - let (mut gs, _, receivers, topic_hashes) = inject_nodes1() - .peer_no(m) - .topics(vec!["topic".into()]) - .to_subscribe(true) - .create_network(); - - //receive message - let raw_message = RawMessage { - source: Some(PeerId::random()), - data: vec![], - sequence_number: Some(0), - topic: topic_hashes[0].clone(), - signature: None, - key: None, - validated: true, - }; - gs.handle_received_message(raw_message.clone(), &PeerId::random()); - - //emit gossip - gs.emit_gossip(); - - // Transform the inbound message - let message = &gs.data_transform.inbound_transform(raw_message).unwrap(); - - let msg_id = gs.config.message_id(message); - //check that exactly config.gossip_lazy() many gossip messages were sent. - let (control_msgs, _) = count_control_msgs(receivers, |_, action| match action { - RpcOut::IHave(IHave { - topic_hash, - message_ids, - }) => topic_hash == &topic_hashes[0] && message_ids.iter().any(|id| id == &msg_id), - _ => false, - }); - assert_eq!( - control_msgs, - ((m - config.mesh_n_low()) as f64 * config.gossip_factor()) as usize - ); -} - -#[test] -fn test_accept_only_outbound_peer_grafts_when_mesh_full() { - let config: Config = Config::default(); - - //enough peers to fill the mesh - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(config.mesh_n_high()) - .topics(vec!["test".into()]) - .to_subscribe(true) - .create_network(); - - // graft all the peers => this will fill the mesh - for peer in peers { - gs.handle_graft(&peer, topics.clone()); - } - - //assert current mesh size - assert_eq!(gs.mesh[&topics[0]].len(), config.mesh_n_high()); - - //create an outbound and an inbound peer - let (inbound, _in_reciver) = add_peer(&mut gs, &topics, false, false); - let (outbound, _out_receiver) = add_peer(&mut gs, &topics, true, false); - - //send grafts - gs.handle_graft(&inbound, vec![topics[0].clone()]); - gs.handle_graft(&outbound, vec![topics[0].clone()]); - - //assert mesh size - assert_eq!(gs.mesh[&topics[0]].len(), config.mesh_n_high() + 1); - - //inbound is not in mesh - assert!(!gs.mesh[&topics[0]].contains(&inbound)); - - //outbound is in mesh - assert!(gs.mesh[&topics[0]].contains(&outbound)); -} - -#[test] -fn test_do_not_remove_too_many_outbound_peers() { - //use an extreme case to catch errors with high probability - let m = 50; - let n = 2 * m; - let config = ConfigBuilder::default() - .mesh_n_high(n) - .mesh_n(n) - .mesh_n_low(n) - .mesh_outbound_min(m) - .build() - .unwrap(); - - //fill the mesh with inbound connections - let (mut gs, peers, _receivers, topics) = inject_nodes1() - .peer_no(n) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .create_network(); - - // graft all the peers - for peer in peers { - gs.handle_graft(&peer, topics.clone()); - } - - //create m outbound connections and graft (we will accept the graft) - let mut outbound = HashSet::new(); - for _ in 0..m { - let (peer, _) = add_peer(&mut gs, &topics, true, false); - outbound.insert(peer); - gs.handle_graft(&peer, topics.clone()); - } - - //mesh is overly full - assert_eq!(gs.mesh.get(&topics[0]).unwrap().len(), n + m); - - // run a heartbeat - gs.heartbeat(); - - // Peers should be removed to reach n - assert_eq!(gs.mesh.get(&topics[0]).unwrap().len(), n); - - //all outbound peers are still in the mesh - assert!(outbound.iter().all(|p| gs.mesh[&topics[0]].contains(p))); -} - -#[test] -fn test_add_outbound_peers_if_min_is_not_satisfied() { - let config: Config = Config::default(); - - // Fill full mesh with inbound peers - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(config.mesh_n_high()) - .topics(vec!["test".into()]) - .to_subscribe(true) - .create_network(); - - // graft all the peers - for peer in peers { - gs.handle_graft(&peer, topics.clone()); - } - - //create config.mesh_outbound_min() many outbound connections without grafting - let mut peers = vec![]; - for _ in 0..config.mesh_outbound_min() { - peers.push(add_peer(&mut gs, &topics, true, false)); - } - - // Nothing changed in the mesh yet - assert_eq!(gs.mesh[&topics[0]].len(), config.mesh_n_high()); - - // run a heartbeat - gs.heartbeat(); - - // The outbound peers got additionally added - assert_eq!( - gs.mesh[&topics[0]].len(), - config.mesh_n_high() + config.mesh_outbound_min() - ); -} - -#[test] -fn test_prune_negative_scored_peers() { - let config = Config::default(); - - //build mesh with one peer - let (mut gs, peers, receivers, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config.clone()) - .explicit(0) - .outbound(0) - .scoring(Some(( - PeerScoreParams::default(), - PeerScoreThresholds::default(), - ))) - .create_network(); - - //add penalty to peer - gs.peer_score.as_mut().unwrap().0.add_penalty(&peers[0], 1); - - //execute heartbeat - gs.heartbeat(); - - //peer should not be in mesh anymore - assert!(gs.mesh[&topics[0]].is_empty()); - - //check prune message - let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { - peer_id == &peers[0] - && match m { - RpcOut::Prune(Prune { - topic_hash, - peers, - backoff, - }) => { - topic_hash == &topics[0] && - //no px in this case - peers.is_empty() && - backoff.unwrap() == config.prune_backoff().as_secs() - } - _ => false, - } - }); - assert_eq!(control_msgs, 1); -} - -#[test] -fn test_dont_graft_to_negative_scored_peers() { - let config = Config::default(); - //init full mesh - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(config.mesh_n_high()) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .scoring(Some(( - PeerScoreParams::default(), - PeerScoreThresholds::default(), - ))) - .create_network(); - - //add two additional peers that will not be part of the mesh - let (p1, _receiver1) = add_peer(&mut gs, &topics, false, false); - let (p2, _receiver2) = add_peer(&mut gs, &topics, false, false); - - //reduce score of p1 to negative - gs.peer_score.as_mut().unwrap().0.add_penalty(&p1, 1); - - //handle prunes of all other peers - for p in peers { - gs.handle_prune(&p, vec![(topics[0].clone(), Vec::new(), None)]); - } - - //heartbeat - gs.heartbeat(); - - //assert that mesh only contains p2 - assert_eq!(gs.mesh.get(&topics[0]).unwrap().len(), 1); - assert!(gs.mesh.get(&topics[0]).unwrap().contains(&p2)); -} - -///Note that in this test also without a penalty the px would be ignored because of the -/// acceptPXThreshold, but the spec still explicitely states the rule that px from negative -/// peers should get ignored, therefore we test it here. -#[test] -fn test_ignore_px_from_negative_scored_peer() { - let config = Config::default(); - - //build mesh with one peer - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config.clone()) - .scoring(Some(( - PeerScoreParams::default(), - PeerScoreThresholds::default(), - ))) - .create_network(); - - //penalize peer - gs.peer_score.as_mut().unwrap().0.add_penalty(&peers[0], 1); - - //handle prune from single peer with px peers - let px = vec![PeerInfo { - peer_id: Some(PeerId::random()), - }]; - - gs.handle_prune( - &peers[0], - vec![( - topics[0].clone(), - px, - Some(config.prune_backoff().as_secs()), - )], - ); - - //assert no dials - assert_eq!( - gs.events - .iter() - .filter(|e| matches!(e, ToSwarm::Dial { .. })) - .count(), - 0 - ); -} - -#[test] -fn test_only_send_nonnegative_scoring_peers_in_px() { - let config = ConfigBuilder::default() - .prune_peers(16) - .do_px() - .build() - .unwrap(); - - // Build mesh with three peer - let (mut gs, peers, receivers, topics) = inject_nodes1() - .peer_no(3) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .explicit(0) - .outbound(0) - .scoring(Some(( - PeerScoreParams::default(), - PeerScoreThresholds::default(), - ))) - .create_network(); - - // Penalize first peer - gs.peer_score.as_mut().unwrap().0.add_penalty(&peers[0], 1); - - // Prune second peer - gs.send_graft_prune( - HashMap::new(), - vec![(peers[1], vec![topics[0].clone()])] - .into_iter() - .collect(), - HashSet::new(), - ); - - // Check that px in prune message only contains third peer - let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { - peer_id == &peers[1] - && match m { - RpcOut::Prune(Prune { - topic_hash, - peers: px, - .. - }) => { - topic_hash == &topics[0] - && px.len() == 1 - && px[0].peer_id.as_ref().unwrap() == &peers[2] - } - _ => false, - } - }); - assert_eq!(control_msgs, 1); -} - -#[test] -fn test_do_not_gossip_to_peers_below_gossip_threshold() { - let config = Config::default(); - let peer_score_params = PeerScoreParams::default(); - let peer_score_thresholds = PeerScoreThresholds { - gossip_threshold: 3.0 * peer_score_params.behaviour_penalty_weight, - ..PeerScoreThresholds::default() - }; - - // Build full mesh - let (mut gs, peers, mut receivers, topics) = inject_nodes1() - .peer_no(config.mesh_n_high()) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - // Graft all the peer - for peer in peers { - gs.handle_graft(&peer, topics.clone()); - } - - // Add two additional peers that will not be part of the mesh - let (p1, receiver1) = add_peer(&mut gs, &topics, false, false); - receivers.insert(p1, receiver1); - let (p2, receiver2) = add_peer(&mut gs, &topics, false, false); - receivers.insert(p2, receiver2); - - // Reduce score of p1 below peer_score_thresholds.gossip_threshold - // note that penalties get squared so two penalties means a score of - // 4 * peer_score_params.behaviour_penalty_weight. - gs.peer_score.as_mut().unwrap().0.add_penalty(&p1, 2); - - // Reduce score of p2 below 0 but not below peer_score_thresholds.gossip_threshold - gs.peer_score.as_mut().unwrap().0.add_penalty(&p2, 1); - - // Receive message - let raw_message = RawMessage { - source: Some(PeerId::random()), - data: vec![], - sequence_number: Some(0), - topic: topics[0].clone(), - signature: None, - key: None, - validated: true, - }; - gs.handle_received_message(raw_message.clone(), &PeerId::random()); - - // Transform the inbound message - let message = &gs.data_transform.inbound_transform(raw_message).unwrap(); - - let msg_id = gs.config.message_id(message); - - // Emit gossip - gs.emit_gossip(); - - // Check that exactly one gossip messages got sent and it got sent to p2 - let (control_msgs, _) = count_control_msgs(receivers, |peer, action| match action { - RpcOut::IHave(IHave { - topic_hash, - message_ids, - }) => { - if topic_hash == &topics[0] && message_ids.iter().any(|id| id == &msg_id) { - assert_eq!(peer, &p2); - true - } else { - false - } - } - _ => false, - }); - assert_eq!(control_msgs, 1); -} - -#[test] -fn test_iwant_msg_from_peer_below_gossip_threshold_gets_ignored() { - let config = Config::default(); - let peer_score_params = PeerScoreParams::default(); - let peer_score_thresholds = PeerScoreThresholds { - gossip_threshold: 3.0 * peer_score_params.behaviour_penalty_weight, - ..PeerScoreThresholds::default() - }; - - // Build full mesh - let (mut gs, peers, mut receivers, topics) = inject_nodes1() - .peer_no(config.mesh_n_high()) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - // Graft all the peer - for peer in peers { - gs.handle_graft(&peer, topics.clone()); - } - - // Add two additional peers that will not be part of the mesh - let (p1, receiver1) = add_peer(&mut gs, &topics, false, false); - receivers.insert(p1, receiver1); - let (p2, receiver2) = add_peer(&mut gs, &topics, false, false); - receivers.insert(p2, receiver2); - - // Reduce score of p1 below peer_score_thresholds.gossip_threshold - // note that penalties get squared so two penalties means a score of - // 4 * peer_score_params.behaviour_penalty_weight. - gs.peer_score.as_mut().unwrap().0.add_penalty(&p1, 2); - - // Reduce score of p2 below 0 but not below peer_score_thresholds.gossip_threshold - gs.peer_score.as_mut().unwrap().0.add_penalty(&p2, 1); - - // Receive message - let raw_message = RawMessage { - source: Some(PeerId::random()), - data: vec![], - sequence_number: Some(0), - topic: topics[0].clone(), - signature: None, - key: None, - validated: true, - }; - gs.handle_received_message(raw_message.clone(), &PeerId::random()); - - // Transform the inbound message - let message = &gs.data_transform.inbound_transform(raw_message).unwrap(); - - let msg_id = gs.config.message_id(message); - - gs.handle_iwant(&p1, vec![msg_id.clone()]); - gs.handle_iwant(&p2, vec![msg_id.clone()]); - - // the messages we are sending - let sent_messages = - receivers - .into_iter() - .fold(vec![], |mut collected_messages, (peer_id, c)| { - let non_priority = c.non_priority.into_inner(); - while !non_priority.is_empty() { - if let Ok(RpcOut::Forward { message, .. }) = non_priority.try_recv() { - collected_messages.push((peer_id, message)); - } - } - collected_messages - }); - - //the message got sent to p2 - assert!(sent_messages - .iter() - .map(|(peer_id, msg)| ( - peer_id, - gs.data_transform.inbound_transform(msg.clone()).unwrap() - )) - .any(|(peer_id, msg)| peer_id == &p2 && gs.config.message_id(&msg) == msg_id)); - //the message got not sent to p1 - assert!(sent_messages - .iter() - .map(|(peer_id, msg)| ( - peer_id, - gs.data_transform.inbound_transform(msg.clone()).unwrap() - )) - .all(|(peer_id, msg)| !(peer_id == &p1 && gs.config.message_id(&msg) == msg_id))); -} - -#[test] -fn test_ihave_msg_from_peer_below_gossip_threshold_gets_ignored() { - let config = Config::default(); - let peer_score_params = PeerScoreParams::default(); - let peer_score_thresholds = PeerScoreThresholds { - gossip_threshold: 3.0 * peer_score_params.behaviour_penalty_weight, - ..PeerScoreThresholds::default() - }; - //build full mesh - let (mut gs, peers, mut receivers, topics) = inject_nodes1() - .peer_no(config.mesh_n_high()) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - // graft all the peer - for peer in peers { - gs.handle_graft(&peer, topics.clone()); - } - - //add two additional peers that will not be part of the mesh - let (p1, receiver1) = add_peer(&mut gs, &topics, false, false); - receivers.insert(p1, receiver1); - let (p2, receiver2) = add_peer(&mut gs, &topics, false, false); - receivers.insert(p2, receiver2); - - //reduce score of p1 below peer_score_thresholds.gossip_threshold - //note that penalties get squared so two penalties means a score of - // 4 * peer_score_params.behaviour_penalty_weight. - gs.peer_score.as_mut().unwrap().0.add_penalty(&p1, 2); - - //reduce score of p2 below 0 but not below peer_score_thresholds.gossip_threshold - gs.peer_score.as_mut().unwrap().0.add_penalty(&p2, 1); - - //message that other peers have - let raw_message = RawMessage { - source: Some(PeerId::random()), - data: vec![], - sequence_number: Some(0), - topic: topics[0].clone(), - signature: None, - key: None, - validated: true, - }; - - // Transform the inbound message - let message = &gs.data_transform.inbound_transform(raw_message).unwrap(); - - let msg_id = gs.config.message_id(message); - - gs.handle_ihave(&p1, vec![(topics[0].clone(), vec![msg_id.clone()])]); - gs.handle_ihave(&p2, vec![(topics[0].clone(), vec![msg_id.clone()])]); - - // check that we sent exactly one IWANT request to p2 - let (control_msgs, _) = count_control_msgs(receivers, |peer, c| match c { - RpcOut::IWant(IWant { message_ids }) => { - if message_ids.iter().any(|m| m == &msg_id) { - assert_eq!(peer, &p2); - true - } else { - false - } - } - _ => false, - }); - assert_eq!(control_msgs, 1); -} - -#[test] -fn test_do_not_publish_to_peer_below_publish_threshold() { - let config = ConfigBuilder::default() - .flood_publish(false) - .build() - .unwrap(); - let peer_score_params = PeerScoreParams::default(); - let peer_score_thresholds = PeerScoreThresholds { - gossip_threshold: 0.5 * peer_score_params.behaviour_penalty_weight, - publish_threshold: 3.0 * peer_score_params.behaviour_penalty_weight, - ..PeerScoreThresholds::default() - }; - - //build mesh with no peers and no subscribed topics - let (mut gs, _, mut receivers, _) = inject_nodes1() - .gs_config(config) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - //create a new topic for which we are not subscribed - let topic = Topic::new("test"); - let topics = vec![topic.hash()]; - - //add two additional peers that will be added to the mesh - let (p1, receiver1) = add_peer(&mut gs, &topics, false, false); - receivers.insert(p1, receiver1); - let (p2, receiver2) = add_peer(&mut gs, &topics, false, false); - receivers.insert(p2, receiver2); - - //reduce score of p1 below peer_score_thresholds.publish_threshold - //note that penalties get squared so two penalties means a score of - // 4 * peer_score_params.behaviour_penalty_weight. - gs.peer_score.as_mut().unwrap().0.add_penalty(&p1, 2); - - //reduce score of p2 below 0 but not below peer_score_thresholds.publish_threshold - gs.peer_score.as_mut().unwrap().0.add_penalty(&p2, 1); - - //a heartbeat will remove the peers from the mesh - gs.heartbeat(); - - // publish on topic - let publish_data = vec![0; 42]; - gs.publish(topic, publish_data).unwrap(); - - // Collect all publish messages - let publishes = receivers - .into_iter() - .fold(vec![], |mut collected_publish, (peer_id, c)| { - let priority = c.priority.into_inner(); - while !priority.is_empty() { - if let Ok(RpcOut::Publish { message, .. }) = priority.try_recv() { - collected_publish.push((peer_id, message)); - } - } - collected_publish - }); - - //assert only published to p2 - assert_eq!(publishes.len(), 1); - assert_eq!(publishes[0].0, p2); -} - -#[test] -fn test_do_not_flood_publish_to_peer_below_publish_threshold() { - let config = Config::default(); - let peer_score_params = PeerScoreParams::default(); - let peer_score_thresholds = PeerScoreThresholds { - gossip_threshold: 0.5 * peer_score_params.behaviour_penalty_weight, - publish_threshold: 3.0 * peer_score_params.behaviour_penalty_weight, - ..PeerScoreThresholds::default() - }; - //build mesh with no peers - let (mut gs, _, mut receivers, topics) = inject_nodes1() - .topics(vec!["test".into()]) - .gs_config(config) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - //add two additional peers that will be added to the mesh - let (p1, receiver1) = add_peer(&mut gs, &topics, false, false); - receivers.insert(p1, receiver1); - let (p2, receiver2) = add_peer(&mut gs, &topics, false, false); - receivers.insert(p2, receiver2); - - //reduce score of p1 below peer_score_thresholds.publish_threshold - //note that penalties get squared so two penalties means a score of - // 4 * peer_score_params.behaviour_penalty_weight. - gs.peer_score.as_mut().unwrap().0.add_penalty(&p1, 2); - - //reduce score of p2 below 0 but not below peer_score_thresholds.publish_threshold - gs.peer_score.as_mut().unwrap().0.add_penalty(&p2, 1); - - //a heartbeat will remove the peers from the mesh - gs.heartbeat(); - - // publish on topic - let publish_data = vec![0; 42]; - gs.publish(Topic::new("test"), publish_data).unwrap(); - - // Collect all publish messages - let publishes = receivers - .into_iter() - .fold(vec![], |mut collected_publish, (peer_id, c)| { - let priority = c.priority.into_inner(); - while !priority.is_empty() { - if let Ok(RpcOut::Publish { message, .. }) = priority.try_recv() { - collected_publish.push((peer_id, message)) - } - } - collected_publish - }); - - //assert only published to p2 - assert_eq!(publishes.len(), 1); - assert!(publishes[0].0 == p2); -} - -#[test] -fn test_ignore_rpc_from_peers_below_graylist_threshold() { - let config = Config::default(); - let peer_score_params = PeerScoreParams::default(); - let peer_score_thresholds = PeerScoreThresholds { - gossip_threshold: 0.5 * peer_score_params.behaviour_penalty_weight, - publish_threshold: 0.5 * peer_score_params.behaviour_penalty_weight, - graylist_threshold: 3.0 * peer_score_params.behaviour_penalty_weight, - ..PeerScoreThresholds::default() - }; - - //build mesh with no peers - let (mut gs, _, _, topics) = inject_nodes1() - .topics(vec!["test".into()]) - .gs_config(config.clone()) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - //add two additional peers that will be added to the mesh - let (p1, _receiver1) = add_peer(&mut gs, &topics, false, false); - let (p2, _receiver2) = add_peer(&mut gs, &topics, false, false); - - //reduce score of p1 below peer_score_thresholds.graylist_threshold - //note that penalties get squared so two penalties means a score of - // 4 * peer_score_params.behaviour_penalty_weight. - gs.peer_score.as_mut().unwrap().0.add_penalty(&p1, 2); - - //reduce score of p2 below publish_threshold but not below graylist_threshold - gs.peer_score.as_mut().unwrap().0.add_penalty(&p2, 1); - - let raw_message1 = RawMessage { - source: Some(PeerId::random()), - data: vec![1, 2, 3, 4], - sequence_number: Some(1u64), - topic: topics[0].clone(), - signature: None, - key: None, - validated: true, - }; - - let raw_message2 = RawMessage { - source: Some(PeerId::random()), - data: vec![1, 2, 3, 4, 5], - sequence_number: Some(2u64), - topic: topics[0].clone(), - signature: None, - key: None, - validated: true, - }; - - let raw_message3 = RawMessage { - source: Some(PeerId::random()), - data: vec![1, 2, 3, 4, 5, 6], - sequence_number: Some(3u64), - topic: topics[0].clone(), - signature: None, - key: None, - validated: true, - }; - - let raw_message4 = RawMessage { - source: Some(PeerId::random()), - data: vec![1, 2, 3, 4, 5, 6, 7], - sequence_number: Some(4u64), - topic: topics[0].clone(), - signature: None, - key: None, - validated: true, - }; - - // Transform the inbound message - let message2 = &gs.data_transform.inbound_transform(raw_message2).unwrap(); - - // Transform the inbound message - let message4 = &gs.data_transform.inbound_transform(raw_message4).unwrap(); - - let subscription = Subscription { - action: SubscriptionAction::Subscribe, - topic_hash: topics[0].clone(), - }; - - let control_action = ControlAction::IHave(IHave { - topic_hash: topics[0].clone(), - message_ids: vec![config.message_id(message2)], - }); - - //clear events - gs.events.clear(); - - //receive from p1 - gs.on_connection_handler_event( - p1, - ConnectionId::new_unchecked(0), - HandlerEvent::Message { - rpc: Rpc { - messages: vec![raw_message1], - subscriptions: vec![subscription.clone()], - control_msgs: vec![control_action], - }, - invalid_messages: Vec::new(), - }, - ); - - //only the subscription event gets processed, the rest is dropped - assert_eq!(gs.events.len(), 1); - assert!(matches!( - gs.events[0], - ToSwarm::GenerateEvent(Event::Subscribed { .. }) - )); - - let control_action = ControlAction::IHave(IHave { - topic_hash: topics[0].clone(), - message_ids: vec![config.message_id(message4)], - }); - - //receive from p2 - gs.on_connection_handler_event( - p2, - ConnectionId::new_unchecked(0), - HandlerEvent::Message { - rpc: Rpc { - messages: vec![raw_message3], - subscriptions: vec![subscription], - control_msgs: vec![control_action], - }, - invalid_messages: Vec::new(), - }, - ); - - //events got processed - assert!(gs.events.len() > 1); -} - -#[test] -fn test_ignore_px_from_peers_below_accept_px_threshold() { - let config = ConfigBuilder::default().prune_peers(16).build().unwrap(); - let peer_score_params = PeerScoreParams::default(); - let peer_score_thresholds = PeerScoreThresholds { - accept_px_threshold: peer_score_params.app_specific_weight, - ..PeerScoreThresholds::default() - }; - // Build mesh with two peers - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(2) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config.clone()) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - // Decrease score of first peer to less than accept_px_threshold - gs.set_application_score(&peers[0], 0.99); - - // Increase score of second peer to accept_px_threshold - gs.set_application_score(&peers[1], 1.0); - - // Handle prune from peer peers[0] with px peers - let px = vec![PeerInfo { - peer_id: Some(PeerId::random()), - }]; - gs.handle_prune( - &peers[0], - vec![( - topics[0].clone(), - px, - Some(config.prune_backoff().as_secs()), - )], - ); - - // Assert no dials - assert_eq!( - gs.events - .iter() - .filter(|e| matches!(e, ToSwarm::Dial { .. })) - .count(), - 0 - ); - - //handle prune from peer peers[1] with px peers - let px = vec![PeerInfo { - peer_id: Some(PeerId::random()), - }]; - gs.handle_prune( - &peers[1], - vec![( - topics[0].clone(), - px, - Some(config.prune_backoff().as_secs()), - )], - ); - - //assert there are dials now - assert!( - gs.events - .iter() - .filter(|e| matches!(e, ToSwarm::Dial { .. })) - .count() - > 0 - ); -} - -#[test] -fn test_keep_best_scoring_peers_on_oversubscription() { - let config = ConfigBuilder::default() - .mesh_n_low(15) - .mesh_n(30) - .mesh_n_high(60) - .retain_scores(29) - .build() - .unwrap(); - - //build mesh with more peers than mesh can hold - let n = config.mesh_n_high() + 1; - let (mut gs, peers, _receivers, topics) = inject_nodes1() - .peer_no(n) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config.clone()) - .explicit(0) - .outbound(n) - .scoring(Some(( - PeerScoreParams::default(), - PeerScoreThresholds::default(), - ))) - .create_network(); - - // graft all, will be accepted since the are outbound - for peer in &peers { - gs.handle_graft(peer, topics.clone()); - } - - //assign scores to peers equalling their index - - //set random positive scores - for (index, peer) in peers.iter().enumerate() { - gs.set_application_score(peer, index as f64); - } - - assert_eq!(gs.mesh[&topics[0]].len(), n); - - //heartbeat to prune some peers - gs.heartbeat(); - - assert_eq!(gs.mesh[&topics[0]].len(), config.mesh_n()); - - //mesh contains retain_scores best peers - assert!(gs.mesh[&topics[0]].is_superset( - &peers[(n - config.retain_scores())..] - .iter() - .cloned() - .collect() - )); -} - -#[test] -fn test_scoring_p1() { - let config = Config::default(); - let mut peer_score_params = PeerScoreParams::default(); - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let topic_params = TopicScoreParams { - time_in_mesh_weight: 2.0, - time_in_mesh_quantum: Duration::from_millis(50), - time_in_mesh_cap: 10.0, - topic_weight: 0.7, - ..TopicScoreParams::default() - }; - peer_score_params - .topics - .insert(topic_hash, topic_params.clone()); - let peer_score_thresholds = PeerScoreThresholds::default(); - - //build mesh with one peer - let (mut gs, peers, _, _) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - //sleep for 2 times the mesh_quantum - sleep(topic_params.time_in_mesh_quantum * 2); - //refresh scores - gs.peer_score.as_mut().unwrap().0.refresh_scores(); - assert!( - gs.peer_score.as_ref().unwrap().0.score(&peers[0]) - >= 2.0 * topic_params.time_in_mesh_weight * topic_params.topic_weight, - "score should be at least 2 * time_in_mesh_weight * topic_weight" - ); - assert!( - gs.peer_score.as_ref().unwrap().0.score(&peers[0]) - < 3.0 * topic_params.time_in_mesh_weight * topic_params.topic_weight, - "score should be less than 3 * time_in_mesh_weight * topic_weight" - ); - - //sleep again for 2 times the mesh_quantum - sleep(topic_params.time_in_mesh_quantum * 2); - //refresh scores - gs.peer_score.as_mut().unwrap().0.refresh_scores(); - assert!( - gs.peer_score.as_ref().unwrap().0.score(&peers[0]) - >= 2.0 * topic_params.time_in_mesh_weight * topic_params.topic_weight, - "score should be at least 4 * time_in_mesh_weight * topic_weight" - ); - - //sleep for enough periods to reach maximum - sleep(topic_params.time_in_mesh_quantum * (topic_params.time_in_mesh_cap - 3.0) as u32); - //refresh scores - gs.peer_score.as_mut().unwrap().0.refresh_scores(); - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&peers[0]), - topic_params.time_in_mesh_cap - * topic_params.time_in_mesh_weight - * topic_params.topic_weight, - "score should be exactly time_in_mesh_cap * time_in_mesh_weight * topic_weight" - ); -} - -fn random_message(seq: &mut u64, topics: &[TopicHash]) -> RawMessage { - let mut rng = rand::thread_rng(); - *seq += 1; - RawMessage { - source: Some(PeerId::random()), - data: (0..rng.gen_range(10..30)).map(|_| rng.gen()).collect(), - sequence_number: Some(*seq), - topic: topics[rng.gen_range(0..topics.len())].clone(), - signature: None, - key: None, - validated: true, - } -} - -#[test] -fn test_scoring_p2() { - let config = Config::default(); - let mut peer_score_params = PeerScoreParams::default(); - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let topic_params = TopicScoreParams { - time_in_mesh_weight: 0.0, //deactivate time in mesh - first_message_deliveries_weight: 2.0, - first_message_deliveries_cap: 10.0, - first_message_deliveries_decay: 0.9, - topic_weight: 0.7, - ..TopicScoreParams::default() - }; - peer_score_params - .topics - .insert(topic_hash, topic_params.clone()); - let peer_score_thresholds = PeerScoreThresholds::default(); - - //build mesh with one peer - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(2) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - let mut seq = 0; - let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { - gs.handle_received_message(msg, &peers[index]); - }; - - let m1 = random_message(&mut seq, &topics); - //peer 0 delivers message first - deliver_message(&mut gs, 0, m1.clone()); - //peer 1 delivers message second - deliver_message(&mut gs, 1, m1); - - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&peers[0]), - 1.0 * topic_params.first_message_deliveries_weight * topic_params.topic_weight, - "score should be exactly first_message_deliveries_weight * topic_weight" - ); - - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&peers[1]), - 0.0, - "there should be no score for second message deliveries * topic_weight" - ); - - //peer 2 delivers two new messages - deliver_message(&mut gs, 1, random_message(&mut seq, &topics)); - deliver_message(&mut gs, 1, random_message(&mut seq, &topics)); - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&peers[1]), - 2.0 * topic_params.first_message_deliveries_weight * topic_params.topic_weight, - "score should be exactly 2 * first_message_deliveries_weight * topic_weight" - ); - - //test decaying - gs.peer_score.as_mut().unwrap().0.refresh_scores(); - - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&peers[0]), - 1.0 * topic_params.first_message_deliveries_decay - * topic_params.first_message_deliveries_weight - * topic_params.topic_weight, - "score should be exactly first_message_deliveries_decay * \ - first_message_deliveries_weight * topic_weight" - ); - - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&peers[1]), - 2.0 * topic_params.first_message_deliveries_decay - * topic_params.first_message_deliveries_weight - * topic_params.topic_weight, - "score should be exactly 2 * first_message_deliveries_decay * \ - first_message_deliveries_weight * topic_weight" - ); - - //test cap - for _ in 0..topic_params.first_message_deliveries_cap as u64 { - deliver_message(&mut gs, 1, random_message(&mut seq, &topics)); - } - - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&peers[1]), - topic_params.first_message_deliveries_cap - * topic_params.first_message_deliveries_weight - * topic_params.topic_weight, - "score should be exactly first_message_deliveries_cap * \ - first_message_deliveries_weight * topic_weight" - ); -} - -#[test] -fn test_scoring_p3() { - let config = Config::default(); - let mut peer_score_params = PeerScoreParams::default(); - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let topic_params = TopicScoreParams { - time_in_mesh_weight: 0.0, //deactivate time in mesh - first_message_deliveries_weight: 0.0, //deactivate first time deliveries - mesh_message_deliveries_weight: -2.0, - mesh_message_deliveries_decay: 0.9, - mesh_message_deliveries_cap: 10.0, - mesh_message_deliveries_threshold: 5.0, - mesh_message_deliveries_activation: Duration::from_secs(1), - mesh_message_deliveries_window: Duration::from_millis(100), - topic_weight: 0.7, - ..TopicScoreParams::default() - }; - peer_score_params.topics.insert(topic_hash, topic_params); - let peer_score_thresholds = PeerScoreThresholds::default(); - - //build mesh with two peers - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(2) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - let mut seq = 0; - let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { - gs.handle_received_message(msg, &peers[index]); - }; - - let mut expected_message_deliveries = 0.0; - - //messages used to test window - let m1 = random_message(&mut seq, &topics); - let m2 = random_message(&mut seq, &topics); - - //peer 1 delivers m1 - deliver_message(&mut gs, 1, m1.clone()); - - //peer 0 delivers two message - deliver_message(&mut gs, 0, random_message(&mut seq, &topics)); - deliver_message(&mut gs, 0, random_message(&mut seq, &topics)); - expected_message_deliveries += 2.0; - - sleep(Duration::from_millis(60)); - - //peer 1 delivers m2 - deliver_message(&mut gs, 1, m2.clone()); - - sleep(Duration::from_millis(70)); - //peer 0 delivers m1 and m2 only m2 gets counted - deliver_message(&mut gs, 0, m1); - deliver_message(&mut gs, 0, m2); - expected_message_deliveries += 1.0; - - sleep(Duration::from_millis(900)); - - //message deliveries penalties get activated, peer 0 has only delivered 3 messages and - // therefore gets a penalty - gs.peer_score.as_mut().unwrap().0.refresh_scores(); - expected_message_deliveries *= 0.9; //decay - - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&peers[0]), - (5f64 - expected_message_deliveries).powi(2) * -2.0 * 0.7 - ); - - // peer 0 delivers a lot of messages => message_deliveries should be capped at 10 - for _ in 0..20 { - deliver_message(&mut gs, 0, random_message(&mut seq, &topics)); - } - - expected_message_deliveries = 10.0; - - assert_eq!(gs.peer_score.as_ref().unwrap().0.score(&peers[0]), 0.0); - - //apply 10 decays - for _ in 0..10 { - gs.peer_score.as_mut().unwrap().0.refresh_scores(); - expected_message_deliveries *= 0.9; //decay - } - - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&peers[0]), - (5f64 - expected_message_deliveries).powi(2) * -2.0 * 0.7 - ); -} - -#[test] -fn test_scoring_p3b() { - let config = ConfigBuilder::default() - .prune_backoff(Duration::from_millis(100)) - .build() - .unwrap(); - let mut peer_score_params = PeerScoreParams::default(); - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let topic_params = TopicScoreParams { - time_in_mesh_weight: 0.0, //deactivate time in mesh - first_message_deliveries_weight: 0.0, //deactivate first time deliveries - mesh_message_deliveries_weight: -2.0, - mesh_message_deliveries_decay: 0.9, - mesh_message_deliveries_cap: 10.0, - mesh_message_deliveries_threshold: 5.0, - mesh_message_deliveries_activation: Duration::from_secs(1), - mesh_message_deliveries_window: Duration::from_millis(100), - mesh_failure_penalty_weight: -3.0, - mesh_failure_penalty_decay: 0.95, - topic_weight: 0.7, - ..Default::default() - }; - peer_score_params.topics.insert(topic_hash, topic_params); - peer_score_params.app_specific_weight = 1.0; - let peer_score_thresholds = PeerScoreThresholds::default(); - - //build mesh with one peer - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - let mut seq = 0; - let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { - gs.handle_received_message(msg, &peers[index]); - }; - - let mut expected_message_deliveries = 0.0; - - //add some positive score - gs.peer_score - .as_mut() - .unwrap() - .0 - .set_application_score(&peers[0], 100.0); - - //peer 0 delivers two message - deliver_message(&mut gs, 0, random_message(&mut seq, &topics)); - deliver_message(&mut gs, 0, random_message(&mut seq, &topics)); - expected_message_deliveries += 2.0; - - sleep(Duration::from_millis(1050)); - - //activation kicks in - gs.peer_score.as_mut().unwrap().0.refresh_scores(); - expected_message_deliveries *= 0.9; //decay - - //prune peer - gs.handle_prune(&peers[0], vec![(topics[0].clone(), vec![], None)]); - - //wait backoff - sleep(Duration::from_millis(130)); - - //regraft peer - gs.handle_graft(&peers[0], topics.clone()); - - //the score should now consider p3b - let mut expected_b3 = (5f64 - expected_message_deliveries).powi(2); - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&peers[0]), - 100.0 + expected_b3 * -3.0 * 0.7 - ); - - //we can also add a new p3 to the score - - //peer 0 delivers one message - deliver_message(&mut gs, 0, random_message(&mut seq, &topics)); - expected_message_deliveries += 1.0; - - sleep(Duration::from_millis(1050)); - gs.peer_score.as_mut().unwrap().0.refresh_scores(); - expected_message_deliveries *= 0.9; //decay - expected_b3 *= 0.95; - - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&peers[0]), - 100.0 + (expected_b3 * -3.0 + (5f64 - expected_message_deliveries).powi(2) * -2.0) * 0.7 - ); -} - -#[test] -fn test_scoring_p4_valid_message() { - let config = ConfigBuilder::default() - .validate_messages() - .build() - .unwrap(); - let mut peer_score_params = PeerScoreParams::default(); - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let topic_params = TopicScoreParams { - time_in_mesh_weight: 0.0, //deactivate time in mesh - first_message_deliveries_weight: 0.0, //deactivate first time deliveries - mesh_message_deliveries_weight: 0.0, //deactivate message deliveries - mesh_failure_penalty_weight: 0.0, //deactivate mesh failure penalties - invalid_message_deliveries_weight: -2.0, - invalid_message_deliveries_decay: 0.9, - topic_weight: 0.7, - ..Default::default() - }; - peer_score_params.topics.insert(topic_hash, topic_params); - peer_score_params.app_specific_weight = 1.0; - let peer_score_thresholds = PeerScoreThresholds::default(); - - //build mesh with two peers - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config.clone()) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - let mut seq = 0; - let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { - gs.handle_received_message(msg, &peers[index]); - }; - - //peer 0 delivers valid message - let m1 = random_message(&mut seq, &topics); - deliver_message(&mut gs, 0, m1.clone()); - - // Transform the inbound message - let message1 = &gs.data_transform.inbound_transform(m1).unwrap(); - - assert_eq!(gs.peer_score.as_ref().unwrap().0.score(&peers[0]), 0.0); - - //message m1 gets validated - gs.report_message_validation_result( - &config.message_id(message1), - &peers[0], - MessageAcceptance::Accept, - ) - .unwrap(); - - assert_eq!(gs.peer_score.as_ref().unwrap().0.score(&peers[0]), 0.0); -} - -#[test] -fn test_scoring_p4_invalid_signature() { - let config = ConfigBuilder::default() - .validate_messages() - .build() - .unwrap(); - let mut peer_score_params = PeerScoreParams::default(); - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let topic_params = TopicScoreParams { - time_in_mesh_weight: 0.0, //deactivate time in mesh - first_message_deliveries_weight: 0.0, //deactivate first time deliveries - mesh_message_deliveries_weight: 0.0, //deactivate message deliveries - mesh_failure_penalty_weight: 0.0, //deactivate mesh failure penalties - invalid_message_deliveries_weight: -2.0, - invalid_message_deliveries_decay: 0.9, - topic_weight: 0.7, - ..Default::default() - }; - peer_score_params.topics.insert(topic_hash, topic_params); - peer_score_params.app_specific_weight = 1.0; - let peer_score_thresholds = PeerScoreThresholds::default(); - - //build mesh with one peer - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - let mut seq = 0; - - //peer 0 delivers message with invalid signature - let m = random_message(&mut seq, &topics); - - gs.on_connection_handler_event( - peers[0], - ConnectionId::new_unchecked(0), - HandlerEvent::Message { - rpc: Rpc { - messages: vec![], - subscriptions: vec![], - control_msgs: vec![], - }, - invalid_messages: vec![(m, ValidationError::InvalidSignature)], - }, - ); - - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&peers[0]), - -2.0 * 0.7 - ); -} - -#[test] -fn test_scoring_p4_message_from_self() { - let config = ConfigBuilder::default() - .validate_messages() - .build() - .unwrap(); - let mut peer_score_params = PeerScoreParams::default(); - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let topic_params = TopicScoreParams { - time_in_mesh_weight: 0.0, //deactivate time in mesh - first_message_deliveries_weight: 0.0, //deactivate first time deliveries - mesh_message_deliveries_weight: 0.0, //deactivate message deliveries - mesh_failure_penalty_weight: 0.0, //deactivate mesh failure penalties - invalid_message_deliveries_weight: -2.0, - invalid_message_deliveries_decay: 0.9, - topic_weight: 0.7, - ..Default::default() - }; - peer_score_params.topics.insert(topic_hash, topic_params); - peer_score_params.app_specific_weight = 1.0; - let peer_score_thresholds = PeerScoreThresholds::default(); - - //build mesh with two peers - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - let mut seq = 0; - let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { - gs.handle_received_message(msg, &peers[index]); - }; - - //peer 0 delivers invalid message from self - let mut m = random_message(&mut seq, &topics); - m.source = Some(*gs.publish_config.get_own_id().unwrap()); - - deliver_message(&mut gs, 0, m); - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&peers[0]), - -2.0 * 0.7 - ); -} - -#[test] -fn test_scoring_p4_ignored_message() { - let config = ConfigBuilder::default() - .validate_messages() - .build() - .unwrap(); - let mut peer_score_params = PeerScoreParams::default(); - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let topic_params = TopicScoreParams { - time_in_mesh_weight: 0.0, //deactivate time in mesh - first_message_deliveries_weight: 0.0, //deactivate first time deliveries - mesh_message_deliveries_weight: 0.0, //deactivate message deliveries - mesh_failure_penalty_weight: 0.0, //deactivate mesh failure penalties - invalid_message_deliveries_weight: -2.0, - invalid_message_deliveries_decay: 0.9, - topic_weight: 0.7, - ..Default::default() - }; - peer_score_params.topics.insert(topic_hash, topic_params); - peer_score_params.app_specific_weight = 1.0; - let peer_score_thresholds = PeerScoreThresholds::default(); - - //build mesh with two peers - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config.clone()) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - let mut seq = 0; - let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { - gs.handle_received_message(msg, &peers[index]); - }; - - //peer 0 delivers ignored message - let m1 = random_message(&mut seq, &topics); - deliver_message(&mut gs, 0, m1.clone()); - - assert_eq!(gs.peer_score.as_ref().unwrap().0.score(&peers[0]), 0.0); - - // Transform the inbound message - let message1 = &gs.data_transform.inbound_transform(m1).unwrap(); - - //message m1 gets ignored - gs.report_message_validation_result( - &config.message_id(message1), - &peers[0], - MessageAcceptance::Ignore, - ) - .unwrap(); - - assert_eq!(gs.peer_score.as_ref().unwrap().0.score(&peers[0]), 0.0); -} - -#[test] -fn test_scoring_p4_application_invalidated_message() { - let config = ConfigBuilder::default() - .validate_messages() - .build() - .unwrap(); - let mut peer_score_params = PeerScoreParams::default(); - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let topic_params = TopicScoreParams { - time_in_mesh_weight: 0.0, //deactivate time in mesh - first_message_deliveries_weight: 0.0, //deactivate first time deliveries - mesh_message_deliveries_weight: 0.0, //deactivate message deliveries - mesh_failure_penalty_weight: 0.0, //deactivate mesh failure penalties - invalid_message_deliveries_weight: -2.0, - invalid_message_deliveries_decay: 0.9, - topic_weight: 0.7, - ..Default::default() - }; - peer_score_params.topics.insert(topic_hash, topic_params); - peer_score_params.app_specific_weight = 1.0; - let peer_score_thresholds = PeerScoreThresholds::default(); - - //build mesh with two peers - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config.clone()) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - let mut seq = 0; - let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { - gs.handle_received_message(msg, &peers[index]); - }; - - //peer 0 delivers invalid message - let m1 = random_message(&mut seq, &topics); - deliver_message(&mut gs, 0, m1.clone()); - - assert_eq!(gs.peer_score.as_ref().unwrap().0.score(&peers[0]), 0.0); - - // Transform the inbound message - let message1 = &gs.data_transform.inbound_transform(m1).unwrap(); - - //message m1 gets rejected - gs.report_message_validation_result( - &config.message_id(message1), - &peers[0], - MessageAcceptance::Reject, - ) - .unwrap(); - - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&peers[0]), - -2.0 * 0.7 - ); -} - -#[test] -fn test_scoring_p4_application_invalid_message_from_two_peers() { - let config = ConfigBuilder::default() - .validate_messages() - .build() - .unwrap(); - let mut peer_score_params = PeerScoreParams::default(); - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let topic_params = TopicScoreParams { - time_in_mesh_weight: 0.0, //deactivate time in mesh - first_message_deliveries_weight: 0.0, //deactivate first time deliveries - mesh_message_deliveries_weight: 0.0, //deactivate message deliveries - mesh_failure_penalty_weight: 0.0, //deactivate mesh failure penalties - invalid_message_deliveries_weight: -2.0, - invalid_message_deliveries_decay: 0.9, - topic_weight: 0.7, - ..Default::default() - }; - peer_score_params.topics.insert(topic_hash, topic_params); - peer_score_params.app_specific_weight = 1.0; - let peer_score_thresholds = PeerScoreThresholds::default(); - - //build mesh with two peers - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(2) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config.clone()) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - let mut seq = 0; - let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { - gs.handle_received_message(msg, &peers[index]); - }; - - //peer 0 delivers invalid message - let m1 = random_message(&mut seq, &topics); - deliver_message(&mut gs, 0, m1.clone()); - - // Transform the inbound message - let message1 = &gs.data_transform.inbound_transform(m1.clone()).unwrap(); - - //peer 1 delivers same message - deliver_message(&mut gs, 1, m1); - - assert_eq!(gs.peer_score.as_ref().unwrap().0.score(&peers[0]), 0.0); - assert_eq!(gs.peer_score.as_ref().unwrap().0.score(&peers[1]), 0.0); - - //message m1 gets rejected - gs.report_message_validation_result( - &config.message_id(message1), - &peers[0], - MessageAcceptance::Reject, - ) - .unwrap(); - - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&peers[0]), - -2.0 * 0.7 - ); - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&peers[1]), - -2.0 * 0.7 - ); -} - -#[test] -fn test_scoring_p4_three_application_invalid_messages() { - let config = ConfigBuilder::default() - .validate_messages() - .build() - .unwrap(); - let mut peer_score_params = PeerScoreParams::default(); - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let topic_params = TopicScoreParams { - time_in_mesh_weight: 0.0, //deactivate time in mesh - first_message_deliveries_weight: 0.0, //deactivate first time deliveries - mesh_message_deliveries_weight: 0.0, //deactivate message deliveries - mesh_failure_penalty_weight: 0.0, //deactivate mesh failure penalties - invalid_message_deliveries_weight: -2.0, - invalid_message_deliveries_decay: 0.9, - topic_weight: 0.7, - ..Default::default() - }; - peer_score_params.topics.insert(topic_hash, topic_params); - peer_score_params.app_specific_weight = 1.0; - let peer_score_thresholds = PeerScoreThresholds::default(); - - //build mesh with one peer - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config.clone()) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - let mut seq = 0; - let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { - gs.handle_received_message(msg, &peers[index]); - }; - - //peer 0 delivers two invalid message - let m1 = random_message(&mut seq, &topics); - let m2 = random_message(&mut seq, &topics); - let m3 = random_message(&mut seq, &topics); - deliver_message(&mut gs, 0, m1.clone()); - deliver_message(&mut gs, 0, m2.clone()); - deliver_message(&mut gs, 0, m3.clone()); - - // Transform the inbound message - let message1 = &gs.data_transform.inbound_transform(m1).unwrap(); - - // Transform the inbound message - let message2 = &gs.data_transform.inbound_transform(m2).unwrap(); - // Transform the inbound message - let message3 = &gs.data_transform.inbound_transform(m3).unwrap(); - - assert_eq!(gs.peer_score.as_ref().unwrap().0.score(&peers[0]), 0.0); - - //messages gets rejected - gs.report_message_validation_result( - &config.message_id(message1), - &peers[0], - MessageAcceptance::Reject, - ) - .unwrap(); - gs.report_message_validation_result( - &config.message_id(message2), - &peers[0], - MessageAcceptance::Reject, - ) - .unwrap(); - gs.report_message_validation_result( - &config.message_id(message3), - &peers[0], - MessageAcceptance::Reject, - ) - .unwrap(); - - //number of invalid messages gets squared - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&peers[0]), - 9.0 * -2.0 * 0.7 - ); -} - -#[test] -fn test_scoring_p4_decay() { - let config = ConfigBuilder::default() - .validate_messages() - .build() - .unwrap(); - let mut peer_score_params = PeerScoreParams::default(); - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let topic_params = TopicScoreParams { - time_in_mesh_weight: 0.0, //deactivate time in mesh - first_message_deliveries_weight: 0.0, //deactivate first time deliveries - mesh_message_deliveries_weight: 0.0, //deactivate message deliveries - mesh_failure_penalty_weight: 0.0, //deactivate mesh failure penalties - invalid_message_deliveries_weight: -2.0, - invalid_message_deliveries_decay: 0.9, - topic_weight: 0.7, - ..Default::default() - }; - peer_score_params.topics.insert(topic_hash, topic_params); - peer_score_params.app_specific_weight = 1.0; - let peer_score_thresholds = PeerScoreThresholds::default(); - - //build mesh with one peer - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config.clone()) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - let mut seq = 0; - let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { - gs.handle_received_message(msg, &peers[index]); - }; - - //peer 0 delivers invalid message - let m1 = random_message(&mut seq, &topics); - deliver_message(&mut gs, 0, m1.clone()); - - // Transform the inbound message - let message1 = &gs.data_transform.inbound_transform(m1).unwrap(); - assert_eq!(gs.peer_score.as_ref().unwrap().0.score(&peers[0]), 0.0); - - //message m1 gets rejected - gs.report_message_validation_result( - &config.message_id(message1), - &peers[0], - MessageAcceptance::Reject, - ) - .unwrap(); - - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&peers[0]), - -2.0 * 0.7 - ); - - //we decay - gs.peer_score.as_mut().unwrap().0.refresh_scores(); - - // the number of invalids gets decayed to 0.9 and then squared in the score - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&peers[0]), - 0.9 * 0.9 * -2.0 * 0.7 - ); -} - -#[test] -fn test_scoring_p5() { - let peer_score_params = PeerScoreParams { - app_specific_weight: 2.0, - ..PeerScoreParams::default() - }; - - //build mesh with one peer - let (mut gs, peers, _, _) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, PeerScoreThresholds::default()))) - .create_network(); - - gs.set_application_score(&peers[0], 1.1); - - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&peers[0]), - 1.1 * 2.0 - ); -} - -#[test] -fn test_scoring_p6() { - let peer_score_params = PeerScoreParams { - ip_colocation_factor_threshold: 5.0, - ip_colocation_factor_weight: -2.0, - ..Default::default() - }; - - let (mut gs, _, _, _) = inject_nodes1() - .peer_no(0) - .topics(vec![]) - .to_subscribe(false) - .gs_config(Config::default()) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, PeerScoreThresholds::default()))) - .create_network(); - - //create 5 peers with the same ip - let addr = Multiaddr::from(Ipv4Addr::new(10, 1, 2, 3)); - let peers = vec![ - add_peer_with_addr(&mut gs, &[], false, false, addr.clone()).0, - add_peer_with_addr(&mut gs, &[], false, false, addr.clone()).0, - add_peer_with_addr(&mut gs, &[], true, false, addr.clone()).0, - add_peer_with_addr(&mut gs, &[], true, false, addr.clone()).0, - add_peer_with_addr(&mut gs, &[], true, true, addr.clone()).0, - ]; - - //create 4 other peers with other ip - let addr2 = Multiaddr::from(Ipv4Addr::new(10, 1, 2, 4)); - let others = vec![ - add_peer_with_addr(&mut gs, &[], false, false, addr2.clone()).0, - add_peer_with_addr(&mut gs, &[], false, false, addr2.clone()).0, - add_peer_with_addr(&mut gs, &[], true, false, addr2.clone()).0, - add_peer_with_addr(&mut gs, &[], true, false, addr2.clone()).0, - ]; - - //no penalties yet - for peer in peers.iter().chain(others.iter()) { - assert_eq!(gs.peer_score.as_ref().unwrap().0.score(peer), 0.0); - } - - //add additional connection for 3 others with addr - for id in others.iter().take(3) { - gs.on_swarm_event(FromSwarm::ConnectionEstablished(ConnectionEstablished { - peer_id: *id, - connection_id: ConnectionId::new_unchecked(0), - endpoint: &ConnectedPoint::Dialer { - address: addr.clone(), - role_override: Endpoint::Dialer, - port_use: PortUse::Reuse, - }, - failed_addresses: &[], - other_established: 0, - })); - } - - //penalties apply squared - for peer in peers.iter().chain(others.iter().take(3)) { - assert_eq!(gs.peer_score.as_ref().unwrap().0.score(peer), 9.0 * -2.0); - } - //fourth other peer still no penalty - assert_eq!(gs.peer_score.as_ref().unwrap().0.score(&others[3]), 0.0); - - //add additional connection for 3 of the peers to addr2 - for peer in peers.iter().take(3) { - gs.on_swarm_event(FromSwarm::ConnectionEstablished(ConnectionEstablished { - peer_id: *peer, - connection_id: ConnectionId::new_unchecked(0), - endpoint: &ConnectedPoint::Dialer { - address: addr2.clone(), - role_override: Endpoint::Dialer, - port_use: PortUse::Reuse, - }, - failed_addresses: &[], - other_established: 1, - })); - } - - //double penalties for the first three of each - for peer in peers.iter().take(3).chain(others.iter().take(3)) { - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(peer), - (9.0 + 4.0) * -2.0 - ); - } - - //single penalties for the rest - for peer in peers.iter().skip(3) { - assert_eq!(gs.peer_score.as_ref().unwrap().0.score(peer), 9.0 * -2.0); - } - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&others[3]), - 4.0 * -2.0 - ); - - //two times same ip doesn't count twice - gs.on_swarm_event(FromSwarm::ConnectionEstablished(ConnectionEstablished { - peer_id: peers[0], - connection_id: ConnectionId::new_unchecked(0), - endpoint: &ConnectedPoint::Dialer { - address: addr, - role_override: Endpoint::Dialer, - port_use: PortUse::Reuse, - }, - failed_addresses: &[], - other_established: 2, - })); - - //nothing changed - //double penalties for the first three of each - for peer in peers.iter().take(3).chain(others.iter().take(3)) { - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(peer), - (9.0 + 4.0) * -2.0 - ); - } - - //single penalties for the rest - for peer in peers.iter().skip(3) { - assert_eq!(gs.peer_score.as_ref().unwrap().0.score(peer), 9.0 * -2.0); - } - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&others[3]), - 4.0 * -2.0 - ); -} - -#[test] -fn test_scoring_p7_grafts_before_backoff() { - let config = ConfigBuilder::default() - .prune_backoff(Duration::from_millis(200)) - .graft_flood_threshold(Duration::from_millis(100)) - .build() - .unwrap(); - let peer_score_params = PeerScoreParams { - behaviour_penalty_weight: -2.0, - behaviour_penalty_decay: 0.9, - ..Default::default() - }; - - let (mut gs, peers, _receivers, topics) = inject_nodes1() - .peer_no(2) - .topics(vec!["test".into()]) - .to_subscribe(false) - .gs_config(config) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, PeerScoreThresholds::default()))) - .create_network(); - - //remove peers from mesh and send prune to them => this adds a backoff for the peers - for peer in peers.iter().take(2) { - gs.mesh.get_mut(&topics[0]).unwrap().remove(peer); - gs.send_graft_prune( - HashMap::new(), - HashMap::from([(*peer, vec![topics[0].clone()])]), - HashSet::new(), - ); - } - - //wait 50 millisecs - sleep(Duration::from_millis(50)); - - //first peer tries to graft - gs.handle_graft(&peers[0], vec![topics[0].clone()]); - - //double behaviour penalty for first peer (squared) - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&peers[0]), - 4.0 * -2.0 - ); - - //wait 100 millisecs - sleep(Duration::from_millis(100)); - - //second peer tries to graft - gs.handle_graft(&peers[1], vec![topics[0].clone()]); - - //single behaviour penalty for second peer - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&peers[1]), - 1.0 * -2.0 - ); - - //test decay - gs.peer_score.as_mut().unwrap().0.refresh_scores(); - - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&peers[0]), - 4.0 * 0.9 * 0.9 * -2.0 - ); - assert_eq!( - gs.peer_score.as_ref().unwrap().0.score(&peers[1]), - 1.0 * 0.9 * 0.9 * -2.0 - ); -} - -#[test] -fn test_opportunistic_grafting() { - let config = ConfigBuilder::default() - .mesh_n_low(3) - .mesh_n(5) - .mesh_n_high(7) - .mesh_outbound_min(0) //deactivate outbound handling - .opportunistic_graft_ticks(2) - .opportunistic_graft_peers(2) - .build() - .unwrap(); - let peer_score_params = PeerScoreParams { - app_specific_weight: 1.0, - ..Default::default() - }; - let thresholds = PeerScoreThresholds { - opportunistic_graft_threshold: 2.0, - ..Default::default() - }; - - let (mut gs, peers, _receivers, topics) = inject_nodes1() - .peer_no(5) - .topics(vec!["test".into()]) - .to_subscribe(false) - .gs_config(config) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, thresholds))) - .create_network(); - - //fill mesh with 5 peers - for peer in &peers { - gs.handle_graft(peer, topics.clone()); - } - - //add additional 5 peers - let others: Vec<_> = (0..5) - .map(|_| add_peer(&mut gs, &topics, false, false)) - .collect(); - - //currently mesh equals peers - assert_eq!(gs.mesh[&topics[0]], peers.iter().cloned().collect()); - - //give others high scores (but the first two have not high enough scores) - for (i, peer) in peers.iter().enumerate().take(5) { - gs.set_application_score(peer, 0.0 + i as f64); - } - - //set scores for peers in the mesh - for (i, (peer, _receiver)) in others.iter().enumerate().take(5) { - gs.set_application_score(peer, 0.0 + i as f64); - } - - //this gives a median of exactly 2.0 => should not apply opportunistic grafting - gs.heartbeat(); - gs.heartbeat(); - - assert_eq!( - gs.mesh[&topics[0]].len(), - 5, - "should not apply opportunistic grafting" - ); - - //reduce middle score to 1.0 giving a median of 1.0 - gs.set_application_score(&peers[2], 1.0); - - //opportunistic grafting after two heartbeats - - gs.heartbeat(); - assert_eq!( - gs.mesh[&topics[0]].len(), - 5, - "should not apply opportunistic grafting after first tick" - ); - - gs.heartbeat(); - - assert_eq!( - gs.mesh[&topics[0]].len(), - 7, - "opportunistic grafting should have added 2 peers" - ); - - assert!( - gs.mesh[&topics[0]].is_superset(&peers.iter().cloned().collect()), - "old peers are still part of the mesh" - ); - - assert!( - gs.mesh[&topics[0]].is_disjoint(&others.iter().map(|(p, _)| p).cloned().take(2).collect()), - "peers below or equal to median should not be added in opportunistic grafting" - ); -} - -#[test] -fn test_ignore_graft_from_unknown_topic() { - //build gossipsub without subscribing to any topics - let (mut gs, peers, receivers, _) = inject_nodes1() - .peer_no(1) - .topics(vec![]) - .to_subscribe(false) - .create_network(); - - //handle an incoming graft for some topic - gs.handle_graft(&peers[0], vec![Topic::new("test").hash()]); - - //assert that no prune got created - let (control_msgs, _) = count_control_msgs(receivers, |_, a| matches!(a, RpcOut::Prune { .. })); - assert_eq!( - control_msgs, 0, - "we should not prune after graft in unknown topic" - ); -} - -#[test] -fn test_ignore_too_many_iwants_from_same_peer_for_same_message() { - let config = Config::default(); - //build gossipsub with full mesh - let (mut gs, _, mut receivers, topics) = inject_nodes1() - .peer_no(config.mesh_n_high()) - .topics(vec!["test".into()]) - .to_subscribe(false) - .create_network(); - - //add another peer not in the mesh - let (peer, receiver) = add_peer(&mut gs, &topics, false, false); - receivers.insert(peer, receiver); - - //receive a message - let mut seq = 0; - let m1 = random_message(&mut seq, &topics); - - // Transform the inbound message - let message1 = &gs.data_transform.inbound_transform(m1.clone()).unwrap(); - - let id = config.message_id(message1); - - gs.handle_received_message(m1, &PeerId::random()); - - //clear events - let receivers = flush_events(&mut gs, receivers); - - //the first gossip_retransimission many iwants return the valid message, all others are - // ignored. - for _ in 0..(2 * config.gossip_retransimission() + 10) { - gs.handle_iwant(&peer, vec![id.clone()]); - } - - assert_eq!( - receivers.into_values().fold(0, |mut fwds, c| { - let non_priority = c.non_priority.into_inner(); - while !non_priority.is_empty() { - if let Ok(RpcOut::Forward { .. }) = non_priority.try_recv() { - fwds += 1; - } - } - fwds - }), - config.gossip_retransimission() as usize, - "not more then gossip_retransmission many messages get sent back" - ); -} - -#[test] -fn test_ignore_too_many_ihaves() { - let config = ConfigBuilder::default() - .max_ihave_messages(10) - .build() - .unwrap(); - //build gossipsub with full mesh - let (mut gs, _, mut receivers, topics) = inject_nodes1() - .peer_no(config.mesh_n_high()) - .topics(vec!["test".into()]) - .to_subscribe(false) - .gs_config(config.clone()) - .create_network(); - - //add another peer not in the mesh - let (peer, receiver) = add_peer(&mut gs, &topics, false, false); - receivers.insert(peer, receiver); - - //peer has 20 messages - let mut seq = 0; - let messages: Vec<_> = (0..20).map(|_| random_message(&mut seq, &topics)).collect(); - - //peer sends us one ihave for each message in order - for raw_message in &messages { - // Transform the inbound message - let message = &gs - .data_transform - .inbound_transform(raw_message.clone()) - .unwrap(); - - gs.handle_ihave( - &peer, - vec![(topics[0].clone(), vec![config.message_id(message)])], - ); - } - - let first_ten: HashSet<_> = messages - .iter() - .take(10) - .map(|msg| gs.data_transform.inbound_transform(msg.clone()).unwrap()) - .map(|m| config.message_id(&m)) - .collect(); - - //we send iwant only for the first 10 messages - let (control_msgs, receivers) = count_control_msgs(receivers, |p, action| { - p == &peer - && matches!(action, RpcOut::IWant(IWant { message_ids }) if message_ids.len() == 1 && first_ten.contains(&message_ids[0])) - }); - assert_eq!( - control_msgs, 10, - "exactly the first ten ihaves should be processed and one iwant for each created" - ); - - //after a heartbeat everything is forgotten - gs.heartbeat(); - - for raw_message in messages[10..].iter() { - // Transform the inbound message - let message = &gs - .data_transform - .inbound_transform(raw_message.clone()) - .unwrap(); - - gs.handle_ihave( - &peer, - vec![(topics[0].clone(), vec![config.message_id(message)])], - ); - } - - //we sent iwant for all 10 messages - let (control_msgs, _) = count_control_msgs(receivers, |p, action| { - p == &peer - && matches!(action, RpcOut::IWant(IWant { message_ids }) if message_ids.len() == 1) - }); - assert_eq!(control_msgs, 10, "all 20 should get sent"); -} - -#[test] -fn test_ignore_too_many_messages_in_ihave() { - let config = ConfigBuilder::default() - .max_ihave_messages(10) - .max_ihave_length(10) - .build() - .unwrap(); - //build gossipsub with full mesh - let (mut gs, _, mut receivers, topics) = inject_nodes1() - .peer_no(config.mesh_n_high()) - .topics(vec!["test".into()]) - .to_subscribe(false) - .gs_config(config.clone()) - .create_network(); - - //add another peer not in the mesh - let (peer, receiver) = add_peer(&mut gs, &topics, false, false); - receivers.insert(peer, receiver); - - //peer has 30 messages - let mut seq = 0; - let message_ids: Vec<_> = (0..30) - .map(|_| random_message(&mut seq, &topics)) - .map(|msg| gs.data_transform.inbound_transform(msg).unwrap()) - .map(|msg| config.message_id(&msg)) - .collect(); - - //peer sends us three ihaves - gs.handle_ihave(&peer, vec![(topics[0].clone(), message_ids[0..8].to_vec())]); - gs.handle_ihave( - &peer, - vec![(topics[0].clone(), message_ids[0..12].to_vec())], - ); - gs.handle_ihave( - &peer, - vec![(topics[0].clone(), message_ids[0..20].to_vec())], - ); - - let first_twelve: HashSet<_> = message_ids.iter().take(12).collect(); - - //we send iwant only for the first 10 messages - let mut sum = 0; - let (control_msgs, receivers) = count_control_msgs(receivers, |p, rpc| match rpc { - RpcOut::IWant(IWant { message_ids }) => { - p == &peer && { - assert!(first_twelve.is_superset(&message_ids.iter().collect())); - sum += message_ids.len(); - true - } - } - _ => false, - }); - assert_eq!( - control_msgs, 2, - "the third ihave should get ignored and no iwant sent" - ); - - assert_eq!(sum, 10, "exactly the first ten ihaves should be processed"); - - //after a heartbeat everything is forgotten - gs.heartbeat(); - gs.handle_ihave( - &peer, - vec![(topics[0].clone(), message_ids[20..30].to_vec())], - ); - - //we sent 10 iwant messages ids via a IWANT rpc. - let mut sum = 0; - let (control_msgs, _) = count_control_msgs(receivers, |p, rpc| match rpc { - RpcOut::IWant(IWant { message_ids }) => { - p == &peer && { - sum += message_ids.len(); - true - } - } - _ => false, - }); - assert_eq!(control_msgs, 1); - assert_eq!(sum, 10, "exactly 20 iwants should get sent"); -} - -#[test] -fn test_limit_number_of_message_ids_inside_ihave() { - let config = ConfigBuilder::default() - .max_ihave_messages(10) - .max_ihave_length(100) - .build() - .unwrap(); - //build gossipsub with full mesh - let (mut gs, peers, mut receivers, topics) = inject_nodes1() - .peer_no(config.mesh_n_high()) - .topics(vec!["test".into()]) - .to_subscribe(false) - .gs_config(config) - .create_network(); - - //graft to all peers to really fill the mesh with all the peers - for peer in peers { - gs.handle_graft(&peer, topics.clone()); - } - - //add two other peers not in the mesh - let (p1, receiver1) = add_peer(&mut gs, &topics, false, false); - receivers.insert(p1, receiver1); - let (p2, receiver2) = add_peer(&mut gs, &topics, false, false); - receivers.insert(p2, receiver2); - - //receive 200 messages from another peer - let mut seq = 0; - for _ in 0..200 { - gs.handle_received_message(random_message(&mut seq, &topics), &PeerId::random()); - } - - //emit gossip - gs.emit_gossip(); - - // both peers should have gotten 100 random ihave messages, to asser the randomness, we - // assert that both have not gotten the same set of messages, but have an intersection - // (which is the case with very high probability, the probabiltity of failure is < 10^-58). - - let mut ihaves1 = HashSet::new(); - let mut ihaves2 = HashSet::new(); - - let (control_msgs, _) = count_control_msgs(receivers, |p, action| match action { - RpcOut::IHave(IHave { message_ids, .. }) => { - if p == &p1 { - ihaves1 = message_ids.iter().cloned().collect(); - true - } else if p == &p2 { - ihaves2 = message_ids.iter().cloned().collect(); - true - } else { - false - } - } - _ => false, - }); - assert_eq!( - control_msgs, 2, - "should have emitted one ihave to p1 and one to p2" - ); - - assert_eq!( - ihaves1.len(), - 100, - "should have sent 100 message ids in ihave to p1" - ); - assert_eq!( - ihaves2.len(), - 100, - "should have sent 100 message ids in ihave to p2" - ); - assert!( - ihaves1 != ihaves2, - "should have sent different random messages to p1 and p2 \ - (this may fail with a probability < 10^-58" - ); - assert!( - ihaves1.intersection(&ihaves2).count() > 0, - "should have sent random messages with some common messages to p1 and p2 \ - (this may fail with a probability < 10^-58" - ); -} - -#[test] -fn test_iwant_penalties() { - /* - use tracing_subscriber::EnvFilter; - let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .try_init(); - */ - let config = ConfigBuilder::default() - .iwant_followup_time(Duration::from_secs(4)) - .build() - .unwrap(); - let peer_score_params = PeerScoreParams { - behaviour_penalty_weight: -1.0, - ..Default::default() - }; - - // fill the mesh - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(2) - .topics(vec!["test".into()]) - .to_subscribe(false) - .gs_config(config.clone()) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, PeerScoreThresholds::default()))) - .create_network(); - - // graft to all peers to really fill the mesh with all the peers - for peer in peers { - gs.handle_graft(&peer, topics.clone()); - } - - // add 100 more peers - let other_peers: Vec<_> = (0..100) - .map(|_| add_peer(&mut gs, &topics, false, false)) - .collect(); - - // each peer sends us an ihave containing each two message ids - let mut first_messages = Vec::new(); - let mut second_messages = Vec::new(); - let mut seq = 0; - for (peer, _receiver) in &other_peers { - let msg1 = random_message(&mut seq, &topics); - let msg2 = random_message(&mut seq, &topics); - - // Decompress the raw message and calculate the message id. - // Transform the inbound message - let message1 = &gs.data_transform.inbound_transform(msg1.clone()).unwrap(); - - // Transform the inbound message - let message2 = &gs.data_transform.inbound_transform(msg2.clone()).unwrap(); - - first_messages.push(msg1.clone()); - second_messages.push(msg2.clone()); - gs.handle_ihave( - peer, - vec![( - topics[0].clone(), - vec![config.message_id(message1), config.message_id(message2)], - )], - ); - } - - // the peers send us all the first message ids in time - for (index, (peer, _receiver)) in other_peers.iter().enumerate() { - gs.handle_received_message(first_messages[index].clone(), peer); - } - - // now we do a heartbeat no penalization should have been applied yet - gs.heartbeat(); - - for (peer, _receiver) in &other_peers { - assert_eq!(gs.peer_score.as_ref().unwrap().0.score(peer), 0.0); - } - - // receive the first twenty of the other peers then send their response - for (index, (peer, _receiver)) in other_peers.iter().enumerate().take(20) { - gs.handle_received_message(second_messages[index].clone(), peer); - } - - // sleep for the promise duration - sleep(Duration::from_secs(4)); - - // now we do a heartbeat to apply penalization - gs.heartbeat(); - - // now we get the second messages from the last 80 peers. - for (index, (peer, _receiver)) in other_peers.iter().enumerate() { - if index > 19 { - gs.handle_received_message(second_messages[index].clone(), peer); - } - } - - // no further penalizations should get applied - gs.heartbeat(); - - // Only the last 80 peers should be penalized for not responding in time - let mut not_penalized = 0; - let mut single_penalized = 0; - let mut double_penalized = 0; - - for (i, (peer, _receiver)) in other_peers.iter().enumerate() { - let score = gs.peer_score.as_ref().unwrap().0.score(peer); - if score == 0.0 { - not_penalized += 1; - } else if score == -1.0 { - assert!(i > 9); - single_penalized += 1; - } else if score == -4.0 { - assert!(i > 9); - double_penalized += 1 - } else { - println!("{peer}"); - println!("{score}"); - panic!("Invalid score of peer"); - } - } - - assert_eq!(not_penalized, 20); - assert_eq!(single_penalized, 80); - assert_eq!(double_penalized, 0); -} - -#[test] -fn test_publish_to_floodsub_peers_without_flood_publish() { - let config = ConfigBuilder::default() - .flood_publish(false) - .build() - .unwrap(); - let (mut gs, _, mut receivers, topics) = inject_nodes1() - .peer_no(config.mesh_n_low() - 1) - .topics(vec!["test".into()]) - .to_subscribe(false) - .gs_config(config) - .create_network(); - - //add two floodsub peer, one explicit, one implicit - let (p1, receiver1) = add_peer_with_addr_and_kind( - &mut gs, - &topics, - false, - false, - Multiaddr::empty(), - Some(PeerKind::Floodsub), - ); - receivers.insert(p1, receiver1); - - let (p2, receiver2) = - add_peer_with_addr_and_kind(&mut gs, &topics, false, false, Multiaddr::empty(), None); - receivers.insert(p2, receiver2); - - //p1 and p2 are not in the mesh - assert!(!gs.mesh[&topics[0]].contains(&p1) && !gs.mesh[&topics[0]].contains(&p2)); - - //publish a message - let publish_data = vec![0; 42]; - gs.publish(Topic::new("test"), publish_data).unwrap(); - - // Collect publish messages to floodsub peers - let publishes = receivers - .into_iter() - .fold(0, |mut collected_publish, (peer_id, c)| { - let priority = c.priority.into_inner(); - while !priority.is_empty() { - if matches!(priority.try_recv(), - Ok(RpcOut::Publish{..}) if peer_id == p1 || peer_id == p2) - { - collected_publish += 1; - } - } - collected_publish - }); - - assert_eq!( - publishes, 2, - "Should send a publish message to all floodsub peers" - ); -} - -#[test] -fn test_do_not_use_floodsub_in_fanout() { - let config = ConfigBuilder::default() - .flood_publish(false) - .build() - .unwrap(); - let (mut gs, _, mut receivers, _) = inject_nodes1() - .peer_no(config.mesh_n_low() - 1) - .topics(Vec::new()) - .to_subscribe(false) - .gs_config(config) - .create_network(); - - let topic = Topic::new("test"); - let topics = vec![topic.hash()]; - - //add two floodsub peer, one explicit, one implicit - let (p1, receiver1) = add_peer_with_addr_and_kind( - &mut gs, - &topics, - false, - false, - Multiaddr::empty(), - Some(PeerKind::Floodsub), - ); - - receivers.insert(p1, receiver1); - let (p2, receiver2) = - add_peer_with_addr_and_kind(&mut gs, &topics, false, false, Multiaddr::empty(), None); - - receivers.insert(p2, receiver2); - //publish a message - let publish_data = vec![0; 42]; - gs.publish(Topic::new("test"), publish_data).unwrap(); - - // Collect publish messages to floodsub peers - let publishes = receivers - .into_iter() - .fold(0, |mut collected_publish, (peer_id, c)| { - let priority = c.priority.into_inner(); - while !priority.is_empty() { - if matches!(priority.try_recv(), - Ok(RpcOut::Publish{..}) if peer_id == p1 || peer_id == p2) - { - collected_publish += 1; - } - } - collected_publish - }); - - assert_eq!( - publishes, 2, - "Should send a publish message to all floodsub peers" - ); - - assert!( - !gs.fanout[&topics[0]].contains(&p1) && !gs.fanout[&topics[0]].contains(&p2), - "Floodsub peers are not allowed in fanout" - ); -} - -#[test] -fn test_dont_add_floodsub_peers_to_mesh_on_join() { - let (mut gs, _, _, _) = inject_nodes1() - .peer_no(0) - .topics(Vec::new()) - .to_subscribe(false) - .create_network(); - - let topic = Topic::new("test"); - let topics = vec![topic.hash()]; - - //add two floodsub peer, one explicit, one implicit - let _p1 = add_peer_with_addr_and_kind( - &mut gs, - &topics, - false, - false, - Multiaddr::empty(), - Some(PeerKind::Floodsub), - ); - let _p2 = add_peer_with_addr_and_kind(&mut gs, &topics, false, false, Multiaddr::empty(), None); - - gs.join(&topics[0]); - - assert!( - gs.mesh[&topics[0]].is_empty(), - "Floodsub peers should not get added to mesh" - ); -} - -#[test] -fn test_dont_send_px_to_old_gossipsub_peers() { - let (mut gs, _, receivers, topics) = inject_nodes1() - .peer_no(0) - .topics(vec!["test".into()]) - .to_subscribe(false) - .create_network(); - - //add an old gossipsub peer - let (p1, _receiver1) = add_peer_with_addr_and_kind( - &mut gs, - &topics, - false, - false, - Multiaddr::empty(), - Some(PeerKind::Gossipsub), - ); - - //prune the peer - gs.send_graft_prune( - HashMap::new(), - vec![(p1, topics.clone())].into_iter().collect(), - HashSet::new(), - ); - - //check that prune does not contain px - let (control_msgs, _) = count_control_msgs(receivers, |_, m| match m { - RpcOut::Prune(Prune { peers: px, .. }) => !px.is_empty(), - _ => false, - }); - assert_eq!(control_msgs, 0, "Should not send px to floodsub peers"); -} - -#[test] -fn test_dont_send_floodsub_peers_in_px() { - //build mesh with one peer - let (mut gs, peers, receivers, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .create_network(); - - //add two floodsub peers - let _p1 = add_peer_with_addr_and_kind( - &mut gs, - &topics, - false, - false, - Multiaddr::empty(), - Some(PeerKind::Floodsub), - ); - let _p2 = add_peer_with_addr_and_kind(&mut gs, &topics, false, false, Multiaddr::empty(), None); - - //prune only mesh node - gs.send_graft_prune( - HashMap::new(), - vec![(peers[0], topics.clone())].into_iter().collect(), - HashSet::new(), - ); - - //check that px in prune message is empty - let (control_msgs, _) = count_control_msgs(receivers, |_, m| match m { - RpcOut::Prune(Prune { peers: px, .. }) => !px.is_empty(), - _ => false, - }); - assert_eq!(control_msgs, 0, "Should not include floodsub peers in px"); -} - -#[test] -fn test_dont_add_floodsub_peers_to_mesh_in_heartbeat() { - let (mut gs, _, _, topics) = inject_nodes1() - .peer_no(0) - .topics(vec!["test".into()]) - .to_subscribe(false) - .create_network(); - - //add two floodsub peer, one explicit, one implicit - let _p1 = add_peer_with_addr_and_kind( - &mut gs, - &topics, - true, - false, - Multiaddr::empty(), - Some(PeerKind::Floodsub), - ); - let _p2 = add_peer_with_addr_and_kind(&mut gs, &topics, true, false, Multiaddr::empty(), None); - - gs.heartbeat(); - - assert!( - gs.mesh[&topics[0]].is_empty(), - "Floodsub peers should not get added to mesh" - ); -} - -// Some very basic test of public api methods. -#[test] -fn test_public_api() { - let (gs, peers, _, topic_hashes) = inject_nodes1() - .peer_no(4) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .create_network(); - let peers = peers.into_iter().collect::>(); - - assert_eq!( - gs.topics().cloned().collect::>(), - topic_hashes, - "Expected topics to match registered topic." - ); - - assert_eq!( - gs.mesh_peers(&TopicHash::from_raw("topic1")) - .cloned() - .collect::>(), - peers, - "Expected peers for a registered topic to contain all peers." - ); - - assert_eq!( - gs.all_mesh_peers().cloned().collect::>(), - peers, - "Expected all_peers to contain all peers." - ); -} - -#[test] -fn test_subscribe_to_invalid_topic() { - let t1 = Topic::new("t1"); - let t2 = Topic::new("t2"); - let (mut gs, _, _, _) = inject_nodes::() - .subscription_filter(WhitelistSubscriptionFilter( - vec![t1.hash()].into_iter().collect(), - )) - .to_subscribe(false) - .create_network(); - - assert!(gs.subscribe(&t1).is_ok()); - assert!(gs.subscribe(&t2).is_err()); -} - -#[test] -fn test_subscribe_and_graft_with_negative_score() { - //simulate a communication between two gossipsub instances - let (mut gs1, _, _, topic_hashes) = inject_nodes1() - .topics(vec!["test".into()]) - .scoring(Some(( - PeerScoreParams::default(), - PeerScoreThresholds::default(), - ))) - .create_network(); - - let (mut gs2, _, receivers, _) = inject_nodes1().create_network(); - - let connection_id = ConnectionId::new_unchecked(0); - - let topic = Topic::new("test"); - - let (p2, _receiver1) = add_peer(&mut gs1, &Vec::new(), true, false); - let (p1, _receiver2) = add_peer(&mut gs2, &topic_hashes, false, false); - - //add penalty to peer p2 - gs1.peer_score.as_mut().unwrap().0.add_penalty(&p2, 1); - - let original_score = gs1.peer_score.as_ref().unwrap().0.score(&p2); - - //subscribe to topic in gs2 - gs2.subscribe(&topic).unwrap(); - - let forward_messages_to_p1 = |gs1: &mut Behaviour<_, _>, - p1: PeerId, - p2: PeerId, - connection_id: ConnectionId, - receivers: HashMap| - -> HashMap { - let new_receivers = HashMap::new(); - for (peer_id, receiver) in receivers.into_iter() { - let non_priority = receiver.non_priority.into_inner(); - match non_priority.try_recv() { - Ok(rpc) if peer_id == p1 => { - gs1.on_connection_handler_event( - p2, - connection_id, - HandlerEvent::Message { - rpc: proto_to_message(&rpc.into_protobuf()), - invalid_messages: vec![], - }, - ); - } - _ => {} - } - } - new_receivers - }; - - //forward the subscribe message - let receivers = forward_messages_to_p1(&mut gs1, p1, p2, connection_id, receivers); - - //heartbeats on both - gs1.heartbeat(); - gs2.heartbeat(); - - //forward messages again - forward_messages_to_p1(&mut gs1, p1, p2, connection_id, receivers); - - //nobody got penalized - assert!(gs1.peer_score.as_ref().unwrap().0.score(&p2) >= original_score); -} - -#[test] -/// Test nodes that send grafts without subscriptions. -fn test_graft_without_subscribe() { - // The node should: - // - Create an empty vector in mesh[topic] - // - Send subscription request to all peers - // - run JOIN(topic) - - let topic = String::from("test_subscribe"); - let subscribe_topic = vec![topic.clone()]; - let subscribe_topic_hash = vec![Topic::new(topic.clone()).hash()]; - let (mut gs, peers, _, topic_hashes) = inject_nodes1() - .peer_no(1) - .topics(subscribe_topic) - .to_subscribe(false) - .create_network(); - - assert!( - gs.mesh.contains_key(&topic_hashes[0]), - "Subscribe should add a new entry to the mesh[topic] hashmap" - ); - - // The node sends a graft for the subscribe topic. - gs.handle_graft(&peers[0], subscribe_topic_hash); - - // The node disconnects - disconnect_peer(&mut gs, &peers[0]); - - // We unsubscribe from the topic. - let _ = gs.unsubscribe(&Topic::new(topic)); -} - -/// Test that a node sends IDONTWANT messages to the mesh peers -/// that run Gossipsub v1.2. -#[test] -fn sends_idontwant() { - let (mut gs, peers, receivers, topic_hashes) = inject_nodes1() - .peer_no(5) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .peer_kind(PeerKind::Gossipsubv1_2) - .create_network(); - - let local_id = PeerId::random(); - - let message = RawMessage { - source: Some(peers[1]), - data: vec![12u8; 1024], - sequence_number: Some(0), - topic: topic_hashes[0].clone(), - signature: None, - key: None, - validated: true, - }; - - gs.handle_received_message(message.clone(), &local_id); - assert_eq!( - receivers - .into_iter() - .fold(0, |mut idontwants, (peer_id, c)| { - let non_priority = c.non_priority.into_inner(); - while !non_priority.is_empty() { - if let Ok(RpcOut::IDontWant(_)) = non_priority.try_recv() { - assert_ne!(peer_id, peers[1]); - idontwants += 1; - } - } - idontwants - }), - 3, - "IDONTWANT was not sent" - ); -} - -#[test] -fn doesnt_sends_idontwant_for_lower_message_size() { - let (mut gs, peers, receivers, topic_hashes) = inject_nodes1() - .peer_no(5) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .peer_kind(PeerKind::Gossipsubv1_2) - .create_network(); - - let local_id = PeerId::random(); - - let message = RawMessage { - source: Some(peers[1]), - data: vec![12], - sequence_number: Some(0), - topic: topic_hashes[0].clone(), - signature: None, - key: None, - validated: true, - }; - - gs.handle_received_message(message.clone(), &local_id); - assert_eq!( - receivers - .into_iter() - .fold(0, |mut idontwants, (peer_id, c)| { - let non_priority = c.non_priority.into_inner(); - while !non_priority.is_empty() { - if let Ok(RpcOut::IDontWant(_)) = non_priority.try_recv() { - assert_ne!(peer_id, peers[1]); - idontwants += 1; - } - } - idontwants - }), - 0, - "IDONTWANT was sent" - ); -} - -/// Test that a node doesn't send IDONTWANT messages to the mesh peers -/// that don't run Gossipsub v1.2. -#[test] -fn doesnt_send_idontwant() { - let (mut gs, peers, receivers, topic_hashes) = inject_nodes1() - .peer_no(5) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .peer_kind(PeerKind::Gossipsubv1_1) - .create_network(); - - let local_id = PeerId::random(); - - let message = RawMessage { - source: Some(peers[1]), - data: vec![12], - sequence_number: Some(0), - topic: topic_hashes[0].clone(), - signature: None, - key: None, - validated: true, - }; - - gs.handle_received_message(message.clone(), &local_id); - assert_eq!( - receivers - .into_iter() - .fold(0, |mut idontwants, (peer_id, c)| { - let non_priority = c.non_priority.into_inner(); - while !non_priority.is_empty() { - if matches!(non_priority.try_recv(), Ok(RpcOut::IDontWant(_)) if peer_id != peers[1]) { - idontwants += 1; - } - } - idontwants - }), - 0, - "IDONTWANT were sent" - ); -} - -/// Test that a node doesn't forward a messages to the mesh peers -/// that sent IDONTWANT. -#[test] -fn doesnt_forward_idontwant() { - let (mut gs, peers, receivers, topic_hashes) = inject_nodes1() - .peer_no(4) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .peer_kind(PeerKind::Gossipsubv1_2) - .create_network(); - - let local_id = PeerId::random(); - - let raw_message = RawMessage { - source: Some(peers[1]), - data: vec![12], - sequence_number: Some(0), - topic: topic_hashes[0].clone(), - signature: None, - key: None, - validated: true, - }; - let message = gs - .data_transform - .inbound_transform(raw_message.clone()) - .unwrap(); - let message_id = gs.config.message_id(&message); - let peer = gs.connected_peers.get_mut(&peers[2]).unwrap(); - peer.dont_send_received.insert(message_id, Instant::now()); - - gs.handle_received_message(raw_message.clone(), &local_id); - assert_eq!( - receivers.into_iter().fold(0, |mut fwds, (peer_id, c)| { - let non_priority = c.non_priority.into_inner(); - while !non_priority.is_empty() { - if let Ok(RpcOut::Forward { .. }) = non_priority.try_recv() { - assert_ne!(peer_id, peers[2]); - fwds += 1; - } - } - fwds - }), - 2, - "IDONTWANT was not sent" - ); -} - -/// Test that a node parses an -/// IDONTWANT message to the respective peer. -#[test] -fn parses_idontwant() { - let (mut gs, peers, _receivers, _topic_hashes) = inject_nodes1() - .peer_no(2) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .peer_kind(PeerKind::Gossipsubv1_2) - .create_network(); - - let message_id = MessageId::new(&[0, 1, 2, 3]); - let rpc = Rpc { - messages: vec![], - subscriptions: vec![], - control_msgs: vec![ControlAction::IDontWant(IDontWant { - message_ids: vec![message_id.clone()], - })], - }; - gs.on_connection_handler_event( - peers[1], - ConnectionId::new_unchecked(0), - HandlerEvent::Message { - rpc, - invalid_messages: vec![], - }, - ); - let peer = gs.connected_peers.get_mut(&peers[1]).unwrap(); - assert!(peer.dont_send_received.get(&message_id).is_some()); -} - -/// Test that a node clears stale IDONTWANT messages. -#[test] -fn clear_stale_idontwant() { - let (mut gs, peers, _receivers, _topic_hashes) = inject_nodes1() - .peer_no(4) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .peer_kind(PeerKind::Gossipsubv1_2) - .create_network(); - - let peer = gs.connected_peers.get_mut(&peers[2]).unwrap(); - peer.dont_send_received - .insert(MessageId::new(&[1, 2, 3, 4]), Instant::now()); - std::thread::sleep(Duration::from_secs(3)); - gs.heartbeat(); - let peer = gs.connected_peers.get_mut(&peers[2]).unwrap(); - assert!(peer.dont_send_received.is_empty()); -} diff --git a/beacon_node/lighthouse_network/gossipsub/src/config.rs b/beacon_node/lighthouse_network/gossipsub/src/config.rs deleted file mode 100644 index eb8dd432a33..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/config.rs +++ /dev/null @@ -1,1051 +0,0 @@ -// Copyright 2020 Sigma Prime Pty Ltd. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -use std::borrow::Cow; -use std::sync::Arc; -use std::time::Duration; - -use super::error::ConfigBuilderError; -use super::protocol::{ProtocolConfig, ProtocolId, FLOODSUB_PROTOCOL}; -use super::types::{Message, MessageId, PeerKind}; - -use libp2p::identity::PeerId; -use libp2p::swarm::StreamProtocol; - -/// The types of message validation that can be employed by gossipsub. -#[derive(Debug, Clone)] -pub enum ValidationMode { - /// This is the default setting. This requires the message author to be a valid [`PeerId`] and to - /// be present as well as the sequence number. All messages must have valid signatures. - /// - /// NOTE: This setting will reject messages from nodes using - /// [`crate::behaviour::MessageAuthenticity::Anonymous`] and all messages that do not have - /// signatures. - Strict, - /// This setting permits messages that have no author, sequence number or signature. If any of - /// these fields exist in the message these are validated. - Permissive, - /// This setting requires the author, sequence number and signature fields of a message to be - /// empty. Any message that contains these fields is considered invalid. - Anonymous, - /// This setting does not check the author, sequence number or signature fields of incoming - /// messages. If these fields contain data, they are simply ignored. - /// - /// NOTE: This setting will consider messages with invalid signatures as valid messages. - None, -} - -/// Selector for custom Protocol Id -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Version { - V1_0, - V1_1, -} - -/// Configuration parameters that define the performance of the gossipsub network. -#[derive(Clone)] -pub struct Config { - protocol: ProtocolConfig, - history_length: usize, - history_gossip: usize, - mesh_n: usize, - mesh_n_low: usize, - mesh_n_high: usize, - retain_scores: usize, - gossip_lazy: usize, - gossip_factor: f64, - heartbeat_initial_delay: Duration, - heartbeat_interval: Duration, - fanout_ttl: Duration, - check_explicit_peers_ticks: u64, - duplicate_cache_time: Duration, - validate_messages: bool, - message_id_fn: Arc MessageId + Send + Sync + 'static>, - allow_self_origin: bool, - do_px: bool, - prune_peers: usize, - prune_backoff: Duration, - unsubscribe_backoff: Duration, - backoff_slack: u32, - flood_publish: bool, - graft_flood_threshold: Duration, - mesh_outbound_min: usize, - opportunistic_graft_ticks: u64, - opportunistic_graft_peers: usize, - gossip_retransimission: u32, - max_messages_per_rpc: Option, - max_ihave_length: usize, - max_ihave_messages: usize, - iwant_followup_time: Duration, - published_message_ids_cache_time: Duration, - connection_handler_queue_len: usize, - connection_handler_publish_duration: Duration, - connection_handler_forward_duration: Duration, - idontwant_message_size_threshold: usize, -} - -impl Config { - pub(crate) fn protocol_config(&self) -> ProtocolConfig { - self.protocol.clone() - } - - // Overlay network parameters. - /// Number of heartbeats to keep in the `memcache` (default is 5). - pub fn history_length(&self) -> usize { - self.history_length - } - - /// Number of past heartbeats to gossip about (default is 3). - pub fn history_gossip(&self) -> usize { - self.history_gossip - } - - /// Target number of peers for the mesh network (D in the spec, default is 6). - pub fn mesh_n(&self) -> usize { - self.mesh_n - } - - /// Minimum number of peers in mesh network before adding more (D_lo in the spec, default is 5). - pub fn mesh_n_low(&self) -> usize { - self.mesh_n_low - } - - /// Maximum number of peers in mesh network before removing some (D_high in the spec, default - /// is 12). - pub fn mesh_n_high(&self) -> usize { - self.mesh_n_high - } - - /// Affects how peers are selected when pruning a mesh due to over subscription. - /// - /// At least `retain_scores` of the retained peers will be high-scoring, while the remainder are - /// chosen randomly (D_score in the spec, default is 4). - pub fn retain_scores(&self) -> usize { - self.retain_scores - } - - /// Minimum number of peers to emit gossip to during a heartbeat (D_lazy in the spec, - /// default is 6). - pub fn gossip_lazy(&self) -> usize { - self.gossip_lazy - } - - /// Affects how many peers we will emit gossip to at each heartbeat. - /// - /// We will send gossip to `gossip_factor * (total number of non-mesh peers)`, or - /// `gossip_lazy`, whichever is greater. The default is 0.25. - pub fn gossip_factor(&self) -> f64 { - self.gossip_factor - } - - /// Initial delay in each heartbeat (default is 5 seconds). - pub fn heartbeat_initial_delay(&self) -> Duration { - self.heartbeat_initial_delay - } - - /// Time between each heartbeat (default is 1 second). - pub fn heartbeat_interval(&self) -> Duration { - self.heartbeat_interval - } - - /// Time to live for fanout peers (default is 60 seconds). - pub fn fanout_ttl(&self) -> Duration { - self.fanout_ttl - } - - /// The number of heartbeat ticks until we recheck the connection to explicit peers and - /// reconnecting if necessary (default 300). - pub fn check_explicit_peers_ticks(&self) -> u64 { - self.check_explicit_peers_ticks - } - - /// The maximum byte size for each gossipsub RPC (default is 65536 bytes). - /// - /// This represents the maximum size of the entire protobuf payload. It must be at least - /// large enough to support basic control messages. If Peer eXchange is enabled, this - /// must be large enough to transmit the desired peer information on pruning. It must be at - /// least 100 bytes. Default is 65536 bytes. - pub fn max_transmit_size(&self) -> usize { - self.protocol.max_transmit_size - } - - /// Duplicates are prevented by storing message id's of known messages in an LRU time cache. - /// This settings sets the time period that messages are stored in the cache. Duplicates can be - /// received if duplicate messages are sent at a time greater than this setting apart. The - /// default is 1 minute. - pub fn duplicate_cache_time(&self) -> Duration { - self.duplicate_cache_time - } - - /// When set to `true`, prevents automatic forwarding of all received messages. This setting - /// allows a user to validate the messages before propagating them to their peers. If set to - /// true, the user must manually call [`crate::Behaviour::report_message_validation_result()`] - /// on the behaviour to forward message once validated (default is `false`). - /// The default is `false`. - pub fn validate_messages(&self) -> bool { - self.validate_messages - } - - /// Determines the level of validation used when receiving messages. See [`ValidationMode`] - /// for the available types. The default is ValidationMode::Strict. - pub fn validation_mode(&self) -> &ValidationMode { - &self.protocol.validation_mode - } - - /// A user-defined function allowing the user to specify the message id of a gossipsub message. - /// The default value is to concatenate the source peer id with a sequence number. Setting this - /// parameter allows the user to address packets arbitrarily. One example is content based - /// addressing, where this function may be set to `hash(message)`. This would prevent messages - /// of the same content from being duplicated. - /// - /// The function takes a [`Message`] as input and outputs a String to be interpreted as - /// the message id. - pub fn message_id(&self, message: &Message) -> MessageId { - (self.message_id_fn)(message) - } - - /// By default, gossipsub will reject messages that are sent to us that have the same message - /// source as we have specified locally. Enabling this, allows these messages and prevents - /// penalizing the peer that sent us the message. Default is false. - pub fn allow_self_origin(&self) -> bool { - self.allow_self_origin - } - - /// Whether Peer eXchange is enabled; this should be enabled in bootstrappers and other well - /// connected/trusted nodes. The default is false. - /// - /// Note: Peer exchange is not implemented today, see - /// . - pub fn do_px(&self) -> bool { - self.do_px - } - - /// Controls the number of peers to include in prune Peer eXchange. - /// When we prune a peer that's eligible for PX (has a good score, etc), we will try to - /// send them signed peer records for up to `prune_peers` other peers that we - /// know of. It is recommended that this value is larger than `mesh_n_high` so that the pruned - /// peer can reliably form a full mesh. The default is typically 16 however until signed - /// records are spec'd this is disabled and set to 0. - pub fn prune_peers(&self) -> usize { - self.prune_peers - } - - /// Controls the backoff time for pruned peers. This is how long - /// a peer must wait before attempting to graft into our mesh again after being pruned. - /// When pruning a peer, we send them our value of `prune_backoff` so they know - /// the minimum time to wait. Peers running older versions may not send a backoff time, - /// so if we receive a prune message without one, we will wait at least `prune_backoff` - /// before attempting to re-graft. The default is one minute. - pub fn prune_backoff(&self) -> Duration { - self.prune_backoff - } - - /// Controls the backoff time when unsubscribing from a topic. - /// - /// This is how long to wait before resubscribing to the topic. A short backoff period in case - /// of an unsubscribe event allows reaching a healthy mesh in a more timely manner. The default - /// is 10 seconds. - pub fn unsubscribe_backoff(&self) -> Duration { - self.unsubscribe_backoff - } - - /// Number of heartbeat slots considered as slack for backoffs. This gurantees that we wait - /// at least backoff_slack heartbeats after a backoff is over before we try to graft. This - /// solves problems occuring through high latencies. In particular if - /// `backoff_slack * heartbeat_interval` is longer than any latencies between processing - /// prunes on our side and processing prunes on the receiving side this guarantees that we - /// get not punished for too early grafting. The default is 1. - pub fn backoff_slack(&self) -> u32 { - self.backoff_slack - } - - /// Whether to do flood publishing or not. If enabled newly created messages will always be - /// sent to all peers that are subscribed to the topic and have a good enough score. - /// The default is true. - pub fn flood_publish(&self) -> bool { - self.flood_publish - } - - /// If a GRAFT comes before `graft_flood_threshold` has elapsed since the last PRUNE, - /// then there is an extra score penalty applied to the peer through P7. - pub fn graft_flood_threshold(&self) -> Duration { - self.graft_flood_threshold - } - - /// Minimum number of outbound peers in the mesh network before adding more (D_out in the spec). - /// This value must be smaller or equal than `mesh_n / 2` and smaller than `mesh_n_low`. - /// The default is 2. - pub fn mesh_outbound_min(&self) -> usize { - self.mesh_outbound_min - } - - /// Number of heartbeat ticks that specifcy the interval in which opportunistic grafting is - /// applied. Every `opportunistic_graft_ticks` we will attempt to select some high-scoring mesh - /// peers to replace lower-scoring ones, if the median score of our mesh peers falls below a - /// threshold (see ). - /// The default is 60. - pub fn opportunistic_graft_ticks(&self) -> u64 { - self.opportunistic_graft_ticks - } - - /// Controls how many times we will allow a peer to request the same message id through IWANT - /// gossip before we start ignoring them. This is designed to prevent peers from spamming us - /// with requests and wasting our resources. The default is 3. - pub fn gossip_retransimission(&self) -> u32 { - self.gossip_retransimission - } - - /// The maximum number of new peers to graft to during opportunistic grafting. The default is 2. - pub fn opportunistic_graft_peers(&self) -> usize { - self.opportunistic_graft_peers - } - - /// The maximum number of messages we will process in a given RPC. If this is unset, there is - /// no limit. The default is None. - pub fn max_messages_per_rpc(&self) -> Option { - self.max_messages_per_rpc - } - - /// The maximum number of messages to include in an IHAVE message. - /// Also controls the maximum number of IHAVE ids we will accept and request with IWANT from a - /// peer within a heartbeat, to protect from IHAVE floods. You should adjust this value from the - /// default if your system is pushing more than 5000 messages in GossipSubHistoryGossip - /// heartbeats; with the defaults this is 1666 messages/s. The default is 5000. - pub fn max_ihave_length(&self) -> usize { - self.max_ihave_length - } - - /// GossipSubMaxIHaveMessages is the maximum number of IHAVE messages to accept from a peer - /// within a heartbeat. - pub fn max_ihave_messages(&self) -> usize { - self.max_ihave_messages - } - - /// Time to wait for a message requested through IWANT following an IHAVE advertisement. - /// If the message is not received within this window, a broken promise is declared and - /// the router may apply behavioural penalties. The default is 3 seconds. - pub fn iwant_followup_time(&self) -> Duration { - self.iwant_followup_time - } - - /// Enable support for flooodsub peers. Default false. - pub fn support_floodsub(&self) -> bool { - self.protocol.protocol_ids.contains(&FLOODSUB_PROTOCOL) - } - - /// Published message ids time cache duration. The default is 10 seconds. - pub fn published_message_ids_cache_time(&self) -> Duration { - self.published_message_ids_cache_time - } - - /// The max number of messages a `ConnectionHandler` can buffer. The default is 5000. - pub fn connection_handler_queue_len(&self) -> usize { - self.connection_handler_queue_len - } - - /// The duration a message to be published can wait to be sent before it is abandoned. The - /// default is 5 seconds. - pub fn publish_queue_duration(&self) -> Duration { - self.connection_handler_publish_duration - } - - /// The duration a message to be forwarded can wait to be sent before it is abandoned. The - /// default is 1s. - pub fn forward_queue_duration(&self) -> Duration { - self.connection_handler_forward_duration - } - - // The message size threshold for which IDONTWANT messages are sent. - // Sending IDONTWANT messages for small messages can have a negative effect to the overall - // traffic and CPU load. This acts as a lower bound cutoff for the message size to which - // IDONTWANT won't be sent to peers. Only works if the peers support Gossipsub1.2 - // (see https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.2.md#idontwant-message) - // default is 1kB - pub fn idontwant_message_size_threshold(&self) -> usize { - self.idontwant_message_size_threshold - } -} - -impl Default for Config { - fn default() -> Self { - // use ConfigBuilder to also validate defaults - ConfigBuilder::default() - .build() - .expect("Default config parameters should be valid parameters") - } -} - -/// The builder struct for constructing a gossipsub configuration. -pub struct ConfigBuilder { - config: Config, - invalid_protocol: bool, // This is a bit of a hack to only expose one error to the user. -} - -impl Default for ConfigBuilder { - fn default() -> Self { - ConfigBuilder { - config: Config { - protocol: ProtocolConfig::default(), - history_length: 5, - history_gossip: 3, - mesh_n: 6, - mesh_n_low: 5, - mesh_n_high: 12, - retain_scores: 4, - gossip_lazy: 6, // default to mesh_n - gossip_factor: 0.25, - heartbeat_initial_delay: Duration::from_secs(5), - heartbeat_interval: Duration::from_secs(1), - fanout_ttl: Duration::from_secs(60), - check_explicit_peers_ticks: 300, - duplicate_cache_time: Duration::from_secs(60), - validate_messages: false, - message_id_fn: Arc::new(|message| { - // default message id is: source + sequence number - // NOTE: If either the peer_id or source is not provided, we set to 0; - let mut source_string = if let Some(peer_id) = message.source.as_ref() { - peer_id.to_base58() - } else { - PeerId::from_bytes(&[0, 1, 0]) - .expect("Valid peer id") - .to_base58() - }; - source_string - .push_str(&message.sequence_number.unwrap_or_default().to_string()); - MessageId::from(source_string) - }), - allow_self_origin: false, - do_px: false, - prune_peers: 0, // NOTE: Increasing this currently has little effect until Signed records are implemented. - prune_backoff: Duration::from_secs(60), - unsubscribe_backoff: Duration::from_secs(10), - backoff_slack: 1, - flood_publish: true, - graft_flood_threshold: Duration::from_secs(10), - mesh_outbound_min: 2, - opportunistic_graft_ticks: 60, - opportunistic_graft_peers: 2, - gossip_retransimission: 3, - max_messages_per_rpc: None, - max_ihave_length: 5000, - max_ihave_messages: 10, - iwant_followup_time: Duration::from_secs(3), - published_message_ids_cache_time: Duration::from_secs(10), - connection_handler_queue_len: 5000, - connection_handler_publish_duration: Duration::from_secs(5), - connection_handler_forward_duration: Duration::from_millis(1000), - idontwant_message_size_threshold: 1000, - }, - invalid_protocol: false, - } - } -} - -impl From for ConfigBuilder { - fn from(config: Config) -> Self { - ConfigBuilder { - config, - invalid_protocol: false, - } - } -} - -impl ConfigBuilder { - /// The protocol id prefix to negotiate this protocol (default is `/meshsub/1.1.0` and `/meshsub/1.0.0`). - pub fn protocol_id_prefix( - &mut self, - protocol_id_prefix: impl Into>, - ) -> &mut Self { - let cow = protocol_id_prefix.into(); - - match ( - StreamProtocol::try_from_owned(format!("{}/1.1.0", cow)), - StreamProtocol::try_from_owned(format!("{}/1.0.0", cow)), - ) { - (Ok(p1), Ok(p2)) => { - self.config.protocol.protocol_ids = vec![ - ProtocolId { - protocol: p1, - kind: PeerKind::Gossipsubv1_1, - }, - ProtocolId { - protocol: p2, - kind: PeerKind::Gossipsub, - }, - ] - } - _ => { - self.invalid_protocol = true; - } - } - - self - } - - /// The full protocol id to negotiate this protocol (does not append `/1.0.0` or `/1.1.0`). - pub fn protocol_id( - &mut self, - protocol_id: impl Into>, - custom_id_version: Version, - ) -> &mut Self { - let cow = protocol_id.into(); - - match StreamProtocol::try_from_owned(cow.to_string()) { - Ok(protocol) => { - self.config.protocol.protocol_ids = vec![ProtocolId { - protocol, - kind: match custom_id_version { - Version::V1_1 => PeerKind::Gossipsubv1_1, - Version::V1_0 => PeerKind::Gossipsub, - }, - }] - } - _ => { - self.invalid_protocol = true; - } - } - - self - } - - /// Number of heartbeats to keep in the `memcache` (default is 5). - pub fn history_length(&mut self, history_length: usize) -> &mut Self { - self.config.history_length = history_length; - self - } - - /// Number of past heartbeats to gossip about (default is 3). - pub fn history_gossip(&mut self, history_gossip: usize) -> &mut Self { - self.config.history_gossip = history_gossip; - self - } - - /// Target number of peers for the mesh network (D in the spec, default is 6). - pub fn mesh_n(&mut self, mesh_n: usize) -> &mut Self { - self.config.mesh_n = mesh_n; - self - } - - /// Minimum number of peers in mesh network before adding more (D_lo in the spec, default is 4). - pub fn mesh_n_low(&mut self, mesh_n_low: usize) -> &mut Self { - self.config.mesh_n_low = mesh_n_low; - self - } - - /// Maximum number of peers in mesh network before removing some (D_high in the spec, default - /// is 12). - pub fn mesh_n_high(&mut self, mesh_n_high: usize) -> &mut Self { - self.config.mesh_n_high = mesh_n_high; - self - } - - /// Affects how peers are selected when pruning a mesh due to over subscription. - /// - /// At least [`Self::retain_scores`] of the retained peers will be high-scoring, while the remainder are - /// chosen randomly (D_score in the spec, default is 4). - pub fn retain_scores(&mut self, retain_scores: usize) -> &mut Self { - self.config.retain_scores = retain_scores; - self - } - - /// Minimum number of peers to emit gossip to during a heartbeat (D_lazy in the spec, - /// default is 6). - pub fn gossip_lazy(&mut self, gossip_lazy: usize) -> &mut Self { - self.config.gossip_lazy = gossip_lazy; - self - } - - /// Affects how many peers we will emit gossip to at each heartbeat. - /// - /// We will send gossip to `gossip_factor * (total number of non-mesh peers)`, or - /// `gossip_lazy`, whichever is greater. The default is 0.25. - pub fn gossip_factor(&mut self, gossip_factor: f64) -> &mut Self { - self.config.gossip_factor = gossip_factor; - self - } - - /// Initial delay in each heartbeat (default is 5 seconds). - pub fn heartbeat_initial_delay(&mut self, heartbeat_initial_delay: Duration) -> &mut Self { - self.config.heartbeat_initial_delay = heartbeat_initial_delay; - self - } - - /// Time between each heartbeat (default is 1 second). - pub fn heartbeat_interval(&mut self, heartbeat_interval: Duration) -> &mut Self { - self.config.heartbeat_interval = heartbeat_interval; - self - } - - /// The number of heartbeat ticks until we recheck the connection to explicit peers and - /// reconnecting if necessary (default 300). - pub fn check_explicit_peers_ticks(&mut self, check_explicit_peers_ticks: u64) -> &mut Self { - self.config.check_explicit_peers_ticks = check_explicit_peers_ticks; - self - } - - /// Time to live for fanout peers (default is 60 seconds). - pub fn fanout_ttl(&mut self, fanout_ttl: Duration) -> &mut Self { - self.config.fanout_ttl = fanout_ttl; - self - } - - /// The maximum byte size for each gossip (default is 2048 bytes). - pub fn max_transmit_size(&mut self, max_transmit_size: usize) -> &mut Self { - self.config.protocol.max_transmit_size = max_transmit_size; - self - } - - /// Duplicates are prevented by storing message id's of known messages in an LRU time cache. - /// This settings sets the time period that messages are stored in the cache. Duplicates can be - /// received if duplicate messages are sent at a time greater than this setting apart. The - /// default is 1 minute. - pub fn duplicate_cache_time(&mut self, cache_size: Duration) -> &mut Self { - self.config.duplicate_cache_time = cache_size; - self - } - - /// When set, prevents automatic forwarding of all received messages. This setting - /// allows a user to validate the messages before propagating them to their peers. If set, - /// the user must manually call [`crate::Behaviour::report_message_validation_result()`] on the - /// behaviour to forward a message once validated. - pub fn validate_messages(&mut self) -> &mut Self { - self.config.validate_messages = true; - self - } - - /// Determines the level of validation used when receiving messages. See [`ValidationMode`] - /// for the available types. The default is ValidationMode::Strict. - pub fn validation_mode(&mut self, validation_mode: ValidationMode) -> &mut Self { - self.config.protocol.validation_mode = validation_mode; - self - } - - /// A user-defined function allowing the user to specify the message id of a gossipsub message. - /// The default value is to concatenate the source peer id with a sequence number. Setting this - /// parameter allows the user to address packets arbitrarily. One example is content based - /// addressing, where this function may be set to `hash(message)`. This would prevent messages - /// of the same content from being duplicated. - /// - /// The function takes a [`Message`] as input and outputs a String to be - /// interpreted as the message id. - pub fn message_id_fn(&mut self, id_fn: F) -> &mut Self - where - F: Fn(&Message) -> MessageId + Send + Sync + 'static, - { - self.config.message_id_fn = Arc::new(id_fn); - self - } - - /// Enables Peer eXchange. This should be enabled in bootstrappers and other well - /// connected/trusted nodes. The default is false. - /// - /// Note: Peer exchange is not implemented today, see - /// . - pub fn do_px(&mut self) -> &mut Self { - self.config.do_px = true; - self - } - - /// Controls the number of peers to include in prune Peer eXchange. - /// - /// When we prune a peer that's eligible for PX (has a good score, etc), we will try to - /// send them signed peer records for up to [`Self::prune_peers] other peers that we - /// know of. It is recommended that this value is larger than [`Self::mesh_n_high`] so that the - /// pruned peer can reliably form a full mesh. The default is 16. - pub fn prune_peers(&mut self, prune_peers: usize) -> &mut Self { - self.config.prune_peers = prune_peers; - self - } - - /// Controls the backoff time for pruned peers. This is how long - /// a peer must wait before attempting to graft into our mesh again after being pruned. - /// When pruning a peer, we send them our value of [`Self::prune_backoff`] so they know - /// the minimum time to wait. Peers running older versions may not send a backoff time, - /// so if we receive a prune message without one, we will wait at least [`Self::prune_backoff`] - /// before attempting to re-graft. The default is one minute. - pub fn prune_backoff(&mut self, prune_backoff: Duration) -> &mut Self { - self.config.prune_backoff = prune_backoff; - self - } - - /// Controls the backoff time when unsubscribing from a topic. - /// - /// This is how long to wait before resubscribing to the topic. A short backoff period in case - /// of an unsubscribe event allows reaching a healthy mesh in a more timely manner. The default - /// is 10 seconds. - pub fn unsubscribe_backoff(&mut self, unsubscribe_backoff: u64) -> &mut Self { - self.config.unsubscribe_backoff = Duration::from_secs(unsubscribe_backoff); - self - } - - /// Number of heartbeat slots considered as slack for backoffs. This gurantees that we wait - /// at least backoff_slack heartbeats after a backoff is over before we try to graft. This - /// solves problems occuring through high latencies. In particular if - /// `backoff_slack * heartbeat_interval` is longer than any latencies between processing - /// prunes on our side and processing prunes on the receiving side this guarantees that we - /// get not punished for too early grafting. The default is 1. - pub fn backoff_slack(&mut self, backoff_slack: u32) -> &mut Self { - self.config.backoff_slack = backoff_slack; - self - } - - /// Whether to do flood publishing or not. If enabled newly created messages will always be - /// sent to all peers that are subscribed to the topic and have a good enough score. - /// The default is true. - pub fn flood_publish(&mut self, flood_publish: bool) -> &mut Self { - self.config.flood_publish = flood_publish; - self - } - - /// If a GRAFT comes before `graft_flood_threshold` has elapsed since the last PRUNE, - /// then there is an extra score penalty applied to the peer through P7. - pub fn graft_flood_threshold(&mut self, graft_flood_threshold: Duration) -> &mut Self { - self.config.graft_flood_threshold = graft_flood_threshold; - self - } - - /// Minimum number of outbound peers in the mesh network before adding more (D_out in the spec). - /// This value must be smaller or equal than `mesh_n / 2` and smaller than `mesh_n_low`. - /// The default is 2. - pub fn mesh_outbound_min(&mut self, mesh_outbound_min: usize) -> &mut Self { - self.config.mesh_outbound_min = mesh_outbound_min; - self - } - - /// Number of heartbeat ticks that specifcy the interval in which opportunistic grafting is - /// applied. Every `opportunistic_graft_ticks` we will attempt to select some high-scoring mesh - /// peers to replace lower-scoring ones, if the median score of our mesh peers falls below a - /// threshold (see ). - /// The default is 60. - pub fn opportunistic_graft_ticks(&mut self, opportunistic_graft_ticks: u64) -> &mut Self { - self.config.opportunistic_graft_ticks = opportunistic_graft_ticks; - self - } - - /// Controls how many times we will allow a peer to request the same message id through IWANT - /// gossip before we start ignoring them. This is designed to prevent peers from spamming us - /// with requests and wasting our resources. - pub fn gossip_retransimission(&mut self, gossip_retransimission: u32) -> &mut Self { - self.config.gossip_retransimission = gossip_retransimission; - self - } - - /// The maximum number of new peers to graft to during opportunistic grafting. The default is 2. - pub fn opportunistic_graft_peers(&mut self, opportunistic_graft_peers: usize) -> &mut Self { - self.config.opportunistic_graft_peers = opportunistic_graft_peers; - self - } - - /// The maximum number of messages we will process in a given RPC. If this is unset, there is - /// no limit. The default is None. - pub fn max_messages_per_rpc(&mut self, max: Option) -> &mut Self { - self.config.max_messages_per_rpc = max; - self - } - - /// The maximum number of messages to include in an IHAVE message. - /// Also controls the maximum number of IHAVE ids we will accept and request with IWANT from a - /// peer within a heartbeat, to protect from IHAVE floods. You should adjust this value from the - /// default if your system is pushing more than 5000 messages in GossipSubHistoryGossip - /// heartbeats; with the defaults this is 1666 messages/s. The default is 5000. - pub fn max_ihave_length(&mut self, max_ihave_length: usize) -> &mut Self { - self.config.max_ihave_length = max_ihave_length; - self - } - - /// GossipSubMaxIHaveMessages is the maximum number of IHAVE messages to accept from a peer - /// within a heartbeat. - pub fn max_ihave_messages(&mut self, max_ihave_messages: usize) -> &mut Self { - self.config.max_ihave_messages = max_ihave_messages; - self - } - - /// By default, gossipsub will reject messages that are sent to us that has the same message - /// source as we have specified locally. Enabling this, allows these messages and prevents - /// penalizing the peer that sent us the message. Default is false. - pub fn allow_self_origin(&mut self, allow_self_origin: bool) -> &mut Self { - self.config.allow_self_origin = allow_self_origin; - self - } - - /// Time to wait for a message requested through IWANT following an IHAVE advertisement. - /// If the message is not received within this window, a broken promise is declared and - /// the router may apply behavioural penalties. The default is 3 seconds. - pub fn iwant_followup_time(&mut self, iwant_followup_time: Duration) -> &mut Self { - self.config.iwant_followup_time = iwant_followup_time; - self - } - - /// Enable support for flooodsub peers. - pub fn support_floodsub(&mut self) -> &mut Self { - if self - .config - .protocol - .protocol_ids - .contains(&FLOODSUB_PROTOCOL) - { - return self; - } - - self.config.protocol.protocol_ids.push(FLOODSUB_PROTOCOL); - self - } - - /// Published message ids time cache duration. The default is 10 seconds. - pub fn published_message_ids_cache_time( - &mut self, - published_message_ids_cache_time: Duration, - ) -> &mut Self { - self.config.published_message_ids_cache_time = published_message_ids_cache_time; - self - } - - /// The max number of messages a `ConnectionHandler` can buffer. The default is 5000. - pub fn connection_handler_queue_len(&mut self, len: usize) -> &mut Self { - self.config.connection_handler_queue_len = len; - self - } - - /// The duration a message to be published can wait to be sent before it is abandoned. The - /// default is 5 seconds. - pub fn publish_queue_duration(&mut self, duration: Duration) -> &mut Self { - self.config.connection_handler_publish_duration = duration; - self - } - - /// The duration a message to be forwarded can wait to be sent before it is abandoned. The - /// default is 1s. - pub fn forward_queue_duration(&mut self, duration: Duration) -> &mut Self { - self.config.connection_handler_forward_duration = duration; - self - } - - // The message size threshold for which IDONTWANT messages are sent. - // Sending IDONTWANT messages for small messages can have a negative effect to the overall - // traffic and CPU load. This acts as a lower bound cutoff for the message size to which - // IDONTWANT won't be sent to peers. Only works if the peers support Gossipsub1.2 - // (see https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.2.md#idontwant-message) - // default is 1kB - pub fn idontwant_message_size_threshold(&mut self, size: usize) -> &mut Self { - self.config.idontwant_message_size_threshold = size; - self - } - - /// Constructs a [`Config`] from the given configuration and validates the settings. - pub fn build(&self) -> Result { - // check all constraints on config - - if self.config.protocol.max_transmit_size < 100 { - return Err(ConfigBuilderError::MaxTransmissionSizeTooSmall); - } - - if self.config.history_length < self.config.history_gossip { - return Err(ConfigBuilderError::HistoryLengthTooSmall); - } - - if !(self.config.mesh_outbound_min <= self.config.mesh_n_low - && self.config.mesh_n_low <= self.config.mesh_n - && self.config.mesh_n <= self.config.mesh_n_high) - { - return Err(ConfigBuilderError::MeshParametersInvalid); - } - - if self.config.mesh_outbound_min * 2 > self.config.mesh_n { - return Err(ConfigBuilderError::MeshOutboundInvalid); - } - - if self.config.unsubscribe_backoff.as_millis() == 0 { - return Err(ConfigBuilderError::UnsubscribeBackoffIsZero); - } - - if self.invalid_protocol { - return Err(ConfigBuilderError::InvalidProtocol); - } - - Ok(self.config.clone()) - } -} - -impl std::fmt::Debug for Config { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut builder = f.debug_struct("GossipsubConfig"); - let _ = builder.field("protocol", &self.protocol); - let _ = builder.field("history_length", &self.history_length); - let _ = builder.field("history_gossip", &self.history_gossip); - let _ = builder.field("mesh_n", &self.mesh_n); - let _ = builder.field("mesh_n_low", &self.mesh_n_low); - let _ = builder.field("mesh_n_high", &self.mesh_n_high); - let _ = builder.field("retain_scores", &self.retain_scores); - let _ = builder.field("gossip_lazy", &self.gossip_lazy); - let _ = builder.field("gossip_factor", &self.gossip_factor); - let _ = builder.field("heartbeat_initial_delay", &self.heartbeat_initial_delay); - let _ = builder.field("heartbeat_interval", &self.heartbeat_interval); - let _ = builder.field("fanout_ttl", &self.fanout_ttl); - let _ = builder.field("duplicate_cache_time", &self.duplicate_cache_time); - let _ = builder.field("validate_messages", &self.validate_messages); - let _ = builder.field("allow_self_origin", &self.allow_self_origin); - let _ = builder.field("do_px", &self.do_px); - let _ = builder.field("prune_peers", &self.prune_peers); - let _ = builder.field("prune_backoff", &self.prune_backoff); - let _ = builder.field("backoff_slack", &self.backoff_slack); - let _ = builder.field("flood_publish", &self.flood_publish); - let _ = builder.field("graft_flood_threshold", &self.graft_flood_threshold); - let _ = builder.field("mesh_outbound_min", &self.mesh_outbound_min); - let _ = builder.field("opportunistic_graft_ticks", &self.opportunistic_graft_ticks); - let _ = builder.field("opportunistic_graft_peers", &self.opportunistic_graft_peers); - let _ = builder.field("max_messages_per_rpc", &self.max_messages_per_rpc); - let _ = builder.field("max_ihave_length", &self.max_ihave_length); - let _ = builder.field("max_ihave_messages", &self.max_ihave_messages); - let _ = builder.field("iwant_followup_time", &self.iwant_followup_time); - let _ = builder.field( - "published_message_ids_cache_time", - &self.published_message_ids_cache_time, - ); - let _ = builder.field( - "idontwant_message_size_threhold", - &self.idontwant_message_size_threshold, - ); - builder.finish() - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::topic::IdentityHash; - use crate::Topic; - use libp2p::core::UpgradeInfo; - use std::collections::hash_map::DefaultHasher; - use std::hash::{Hash, Hasher}; - - #[test] - fn create_config_with_message_id_as_plain_function() { - let config = ConfigBuilder::default() - .message_id_fn(message_id_plain_function) - .build() - .unwrap(); - - let result = config.message_id(&get_gossipsub_message()); - - assert_eq!(result, get_expected_message_id()); - } - - #[test] - fn create_config_with_message_id_as_closure() { - let config = ConfigBuilder::default() - .message_id_fn(|message: &Message| { - let mut s = DefaultHasher::new(); - message.data.hash(&mut s); - let mut v = s.finish().to_string(); - v.push('e'); - MessageId::from(v) - }) - .build() - .unwrap(); - - let result = config.message_id(&get_gossipsub_message()); - - assert_eq!(result, get_expected_message_id()); - } - - #[test] - fn create_config_with_message_id_as_closure_with_variable_capture() { - let captured: char = 'e'; - - let config = ConfigBuilder::default() - .message_id_fn(move |message: &Message| { - let mut s = DefaultHasher::new(); - message.data.hash(&mut s); - let mut v = s.finish().to_string(); - v.push(captured); - MessageId::from(v) - }) - .build() - .unwrap(); - - let result = config.message_id(&get_gossipsub_message()); - - assert_eq!(result, get_expected_message_id()); - } - - #[test] - fn create_config_with_protocol_id_prefix() { - let protocol_config = ConfigBuilder::default() - .protocol_id_prefix("/purple") - .build() - .unwrap() - .protocol_config(); - - let protocol_ids = protocol_config.protocol_info(); - - assert_eq!(protocol_ids.len(), 2); - - assert_eq!( - protocol_ids[0].protocol, - StreamProtocol::new("/purple/1.1.0") - ); - assert_eq!(protocol_ids[0].kind, PeerKind::Gossipsubv1_1); - - assert_eq!( - protocol_ids[1].protocol, - StreamProtocol::new("/purple/1.0.0") - ); - assert_eq!(protocol_ids[1].kind, PeerKind::Gossipsub); - } - - #[test] - fn create_config_with_custom_protocol_id() { - let protocol_config = ConfigBuilder::default() - .protocol_id("/purple", Version::V1_0) - .build() - .unwrap() - .protocol_config(); - - let protocol_ids = protocol_config.protocol_info(); - - assert_eq!(protocol_ids.len(), 1); - - assert_eq!(protocol_ids[0].protocol, "/purple"); - assert_eq!(protocol_ids[0].kind, PeerKind::Gossipsub); - } - - fn get_gossipsub_message() -> Message { - Message { - source: None, - data: vec![12, 34, 56], - sequence_number: None, - topic: Topic::::new("test").hash(), - } - } - - fn get_expected_message_id() -> MessageId { - MessageId::from([ - 49, 55, 56, 51, 56, 52, 49, 51, 52, 51, 52, 55, 51, 51, 53, 52, 54, 54, 52, 49, 101, - ]) - } - - fn message_id_plain_function(message: &Message) -> MessageId { - let mut s = DefaultHasher::new(); - message.data.hash(&mut s); - let mut v = s.finish().to_string(); - v.push('e'); - MessageId::from(v) - } -} diff --git a/beacon_node/lighthouse_network/gossipsub/src/error.rs b/beacon_node/lighthouse_network/gossipsub/src/error.rs deleted file mode 100644 index df3332bc923..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/error.rs +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright 2020 Sigma Prime Pty Ltd. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -//! Error types that can result from gossipsub. - -use libp2p::identity::SigningError; - -/// Error associated with publishing a gossipsub message. -#[derive(Debug)] -pub enum PublishError { - /// This message has already been published. - Duplicate, - /// An error occurred whilst signing the message. - SigningError(SigningError), - /// There were no peers to send this message to. - InsufficientPeers, - /// The overall message was too large. This could be due to excessive topics or an excessive - /// message size. - MessageTooLarge, - /// The compression algorithm failed. - TransformFailed(std::io::Error), - /// Messages could not be sent because all queues for peers were full. The usize represents the - /// number of peers that have full queues. - AllQueuesFull(usize), -} - -impl std::fmt::Display for PublishError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{self:?}") - } -} - -impl std::error::Error for PublishError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::SigningError(err) => Some(err), - Self::TransformFailed(err) => Some(err), - _ => None, - } - } -} - -/// Error associated with subscribing to a topic. -#[derive(Debug)] -pub enum SubscriptionError { - /// Couldn't publish our subscription - PublishError(PublishError), - /// We are not allowed to subscribe to this topic by the subscription filter - NotAllowed, -} - -impl std::fmt::Display for SubscriptionError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{self:?}") - } -} - -impl std::error::Error for SubscriptionError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::PublishError(err) => Some(err), - _ => None, - } - } -} - -impl From for PublishError { - fn from(error: SigningError) -> Self { - PublishError::SigningError(error) - } -} - -#[derive(Debug, Clone, Copy)] -pub enum ValidationError { - /// The message has an invalid signature, - InvalidSignature, - /// The sequence number was empty, expected a value. - EmptySequenceNumber, - /// The sequence number was the incorrect size - InvalidSequenceNumber, - /// The PeerId was invalid - InvalidPeerId, - /// Signature existed when validation has been sent to - /// [`crate::behaviour::MessageAuthenticity::Anonymous`]. - SignaturePresent, - /// Sequence number existed when validation has been sent to - /// [`crate::behaviour::MessageAuthenticity::Anonymous`]. - SequenceNumberPresent, - /// Message source existed when validation has been sent to - /// [`crate::behaviour::MessageAuthenticity::Anonymous`]. - MessageSourcePresent, - /// The data transformation failed. - TransformFailed, -} - -impl std::fmt::Display for ValidationError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{self:?}") - } -} - -impl std::error::Error for ValidationError {} - -impl From for PublishError { - fn from(error: std::io::Error) -> PublishError { - PublishError::TransformFailed(error) - } -} - -/// Error associated with Config building. -#[derive(Debug)] -pub enum ConfigBuilderError { - /// Maximum transmission size is too small. - MaxTransmissionSizeTooSmall, - /// Histroy length less than history gossip length. - HistoryLengthTooSmall, - /// The ineauality doesn't hold mesh_outbound_min <= mesh_n_low <= mesh_n <= mesh_n_high - MeshParametersInvalid, - /// The inequality doesn't hold mesh_outbound_min <= self.config.mesh_n / 2 - MeshOutboundInvalid, - /// unsubscribe_backoff is zero - UnsubscribeBackoffIsZero, - /// Invalid protocol - InvalidProtocol, -} - -impl std::error::Error for ConfigBuilderError {} - -impl std::fmt::Display for ConfigBuilderError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Self::MaxTransmissionSizeTooSmall => { - write!(f, "Maximum transmission size is too small") - } - Self::HistoryLengthTooSmall => write!(f, "Histroy length less than history gossip length"), - Self::MeshParametersInvalid => write!(f, "The ineauality doesn't hold mesh_outbound_min <= mesh_n_low <= mesh_n <= mesh_n_high"), - Self::MeshOutboundInvalid => write!(f, "The inequality doesn't hold mesh_outbound_min <= self.config.mesh_n / 2"), - Self::UnsubscribeBackoffIsZero => write!(f, "unsubscribe_backoff is zero"), - Self::InvalidProtocol => write!(f, "Invalid protocol"), - } - } -} diff --git a/beacon_node/lighthouse_network/gossipsub/src/generated/compat.proto b/beacon_node/lighthouse_network/gossipsub/src/generated/compat.proto deleted file mode 100644 index b2753bf7e41..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/generated/compat.proto +++ /dev/null @@ -1,12 +0,0 @@ -syntax = "proto2"; - -package compat.pb; - -message Message { - optional bytes from = 1; - optional bytes data = 2; - optional bytes seqno = 3; - repeated string topic_ids = 4; - optional bytes signature = 5; - optional bytes key = 6; -} \ No newline at end of file diff --git a/beacon_node/lighthouse_network/gossipsub/src/generated/compat/mod.rs b/beacon_node/lighthouse_network/gossipsub/src/generated/compat/mod.rs deleted file mode 100644 index aec6164c7ef..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/generated/compat/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -// Automatically generated mod.rs -pub mod pb; diff --git a/beacon_node/lighthouse_network/gossipsub/src/generated/compat/pb.rs b/beacon_node/lighthouse_network/gossipsub/src/generated/compat/pb.rs deleted file mode 100644 index fd59c38e2b4..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/generated/compat/pb.rs +++ /dev/null @@ -1,67 +0,0 @@ -// Automatically generated rust module for 'compat.proto' file - -#![allow(non_snake_case)] -#![allow(non_upper_case_globals)] -#![allow(non_camel_case_types)] -#![allow(unused_imports)] -#![allow(unknown_lints)] -#![allow(clippy::all)] -#![cfg_attr(rustfmt, rustfmt_skip)] - - -use quick_protobuf::{MessageInfo, MessageRead, MessageWrite, BytesReader, Writer, WriterBackend, Result}; -use quick_protobuf::sizeofs::*; -use super::super::*; - -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Debug, Default, PartialEq, Clone)] -pub struct Message { - pub from: Option>, - pub data: Option>, - pub seqno: Option>, - pub topic_ids: Vec, - pub signature: Option>, - pub key: Option>, -} - -impl<'a> MessageRead<'a> for Message { - fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { - let mut msg = Self::default(); - while !r.is_eof() { - match r.next_tag(bytes) { - Ok(10) => msg.from = Some(r.read_bytes(bytes)?.to_owned()), - Ok(18) => msg.data = Some(r.read_bytes(bytes)?.to_owned()), - Ok(26) => msg.seqno = Some(r.read_bytes(bytes)?.to_owned()), - Ok(34) => msg.topic_ids.push(r.read_string(bytes)?.to_owned()), - Ok(42) => msg.signature = Some(r.read_bytes(bytes)?.to_owned()), - Ok(50) => msg.key = Some(r.read_bytes(bytes)?.to_owned()), - Ok(t) => { r.read_unknown(bytes, t)?; } - Err(e) => return Err(e), - } - } - Ok(msg) - } -} - -impl MessageWrite for Message { - fn get_size(&self) -> usize { - 0 - + self.from.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) - + self.data.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) - + self.seqno.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) - + self.topic_ids.iter().map(|s| 1 + sizeof_len((s).len())).sum::() - + self.signature.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) - + self.key.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) - } - - fn write_message(&self, w: &mut Writer) -> Result<()> { - if let Some(ref s) = self.from { w.write_with_tag(10, |w| w.write_bytes(&**s))?; } - if let Some(ref s) = self.data { w.write_with_tag(18, |w| w.write_bytes(&**s))?; } - if let Some(ref s) = self.seqno { w.write_with_tag(26, |w| w.write_bytes(&**s))?; } - for s in &self.topic_ids { w.write_with_tag(34, |w| w.write_string(&**s))?; } - if let Some(ref s) = self.signature { w.write_with_tag(42, |w| w.write_bytes(&**s))?; } - if let Some(ref s) = self.key { w.write_with_tag(50, |w| w.write_bytes(&**s))?; } - Ok(()) - } -} - diff --git a/beacon_node/lighthouse_network/gossipsub/src/generated/gossipsub/mod.rs b/beacon_node/lighthouse_network/gossipsub/src/generated/gossipsub/mod.rs deleted file mode 100644 index aec6164c7ef..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/generated/gossipsub/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -// Automatically generated mod.rs -pub mod pb; diff --git a/beacon_node/lighthouse_network/gossipsub/src/generated/gossipsub/pb.rs b/beacon_node/lighthouse_network/gossipsub/src/generated/gossipsub/pb.rs deleted file mode 100644 index 24ac80d2755..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/generated/gossipsub/pb.rs +++ /dev/null @@ -1,603 +0,0 @@ -// Automatically generated rust module for 'rpc.proto' file - -#![allow(non_snake_case)] -#![allow(non_upper_case_globals)] -#![allow(non_camel_case_types)] -#![allow(unused_imports)] -#![allow(unknown_lints)] -#![allow(clippy::all)] -#![cfg_attr(rustfmt, rustfmt_skip)] - - -use quick_protobuf::{MessageInfo, MessageRead, MessageWrite, BytesReader, Writer, WriterBackend, Result}; -use quick_protobuf::sizeofs::*; -use super::super::*; - -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Debug, Default, PartialEq, Clone)] -pub struct RPC { - pub subscriptions: Vec, - pub publish: Vec, - pub control: Option, -} - -impl<'a> MessageRead<'a> for RPC { - fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { - let mut msg = Self::default(); - while !r.is_eof() { - match r.next_tag(bytes) { - Ok(10) => msg.subscriptions.push(r.read_message::(bytes)?), - Ok(18) => msg.publish.push(r.read_message::(bytes)?), - Ok(26) => msg.control = Some(r.read_message::(bytes)?), - Ok(t) => { r.read_unknown(bytes, t)?; } - Err(e) => return Err(e), - } - } - Ok(msg) - } -} - -impl MessageWrite for RPC { - fn get_size(&self) -> usize { - 0 - + self.subscriptions.iter().map(|s| 1 + sizeof_len((s).get_size())).sum::() - + self.publish.iter().map(|s| 1 + sizeof_len((s).get_size())).sum::() - + self.control.as_ref().map_or(0, |m| 1 + sizeof_len((m).get_size())) - } - - fn write_message(&self, w: &mut Writer) -> Result<()> { - for s in &self.subscriptions { w.write_with_tag(10, |w| w.write_message(s))?; } - for s in &self.publish { w.write_with_tag(18, |w| w.write_message(s))?; } - if let Some(ref s) = self.control { w.write_with_tag(26, |w| w.write_message(s))?; } - Ok(()) - } -} - -pub mod mod_RPC { - -use super::*; - -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Debug, Default, PartialEq, Clone)] -pub struct SubOpts { - pub subscribe: Option, - pub topic_id: Option, -} - -impl<'a> MessageRead<'a> for SubOpts { - fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { - let mut msg = Self::default(); - while !r.is_eof() { - match r.next_tag(bytes) { - Ok(8) => msg.subscribe = Some(r.read_bool(bytes)?), - Ok(18) => msg.topic_id = Some(r.read_string(bytes)?.to_owned()), - Ok(t) => { r.read_unknown(bytes, t)?; } - Err(e) => return Err(e), - } - } - Ok(msg) - } -} - -impl MessageWrite for SubOpts { - fn get_size(&self) -> usize { - 0 - + self.subscribe.as_ref().map_or(0, |m| 1 + sizeof_varint(*(m) as u64)) - + self.topic_id.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) - } - - fn write_message(&self, w: &mut Writer) -> Result<()> { - if let Some(ref s) = self.subscribe { w.write_with_tag(8, |w| w.write_bool(*s))?; } - if let Some(ref s) = self.topic_id { w.write_with_tag(18, |w| w.write_string(&**s))?; } - Ok(()) - } -} - -} - -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Debug, Default, PartialEq, Clone)] -pub struct Message { - pub from: Option>, - pub data: Option>, - pub seqno: Option>, - pub topic: String, - pub signature: Option>, - pub key: Option>, -} - -impl<'a> MessageRead<'a> for Message { - fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { - let mut msg = Self::default(); - while !r.is_eof() { - match r.next_tag(bytes) { - Ok(10) => msg.from = Some(r.read_bytes(bytes)?.to_owned()), - Ok(18) => msg.data = Some(r.read_bytes(bytes)?.to_owned()), - Ok(26) => msg.seqno = Some(r.read_bytes(bytes)?.to_owned()), - Ok(34) => msg.topic = r.read_string(bytes)?.to_owned(), - Ok(42) => msg.signature = Some(r.read_bytes(bytes)?.to_owned()), - Ok(50) => msg.key = Some(r.read_bytes(bytes)?.to_owned()), - Ok(t) => { r.read_unknown(bytes, t)?; } - Err(e) => return Err(e), - } - } - Ok(msg) - } -} - -impl MessageWrite for Message { - fn get_size(&self) -> usize { - 0 - + self.from.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) - + self.data.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) - + self.seqno.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) - + 1 + sizeof_len((&self.topic).len()) - + self.signature.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) - + self.key.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) - } - - fn write_message(&self, w: &mut Writer) -> Result<()> { - if let Some(ref s) = self.from { w.write_with_tag(10, |w| w.write_bytes(&**s))?; } - if let Some(ref s) = self.data { w.write_with_tag(18, |w| w.write_bytes(&**s))?; } - if let Some(ref s) = self.seqno { w.write_with_tag(26, |w| w.write_bytes(&**s))?; } - w.write_with_tag(34, |w| w.write_string(&**&self.topic))?; - if let Some(ref s) = self.signature { w.write_with_tag(42, |w| w.write_bytes(&**s))?; } - if let Some(ref s) = self.key { w.write_with_tag(50, |w| w.write_bytes(&**s))?; } - Ok(()) - } -} - -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Debug, Default, PartialEq, Clone)] -pub struct ControlMessage { - pub ihave: Vec, - pub iwant: Vec, - pub graft: Vec, - pub prune: Vec, - pub idontwant: Vec, -} - -impl<'a> MessageRead<'a> for ControlMessage { - fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { - let mut msg = Self::default(); - while !r.is_eof() { - match r.next_tag(bytes) { - Ok(10) => msg.ihave.push(r.read_message::(bytes)?), - Ok(18) => msg.iwant.push(r.read_message::(bytes)?), - Ok(26) => msg.graft.push(r.read_message::(bytes)?), - Ok(34) => msg.prune.push(r.read_message::(bytes)?), - Ok(42) => msg.idontwant.push(r.read_message::(bytes)?), - Ok(t) => { r.read_unknown(bytes, t)?; } - Err(e) => return Err(e), - } - } - Ok(msg) - } -} - -impl MessageWrite for ControlMessage { - fn get_size(&self) -> usize { - 0 - + self.ihave.iter().map(|s| 1 + sizeof_len((s).get_size())).sum::() - + self.iwant.iter().map(|s| 1 + sizeof_len((s).get_size())).sum::() - + self.graft.iter().map(|s| 1 + sizeof_len((s).get_size())).sum::() - + self.prune.iter().map(|s| 1 + sizeof_len((s).get_size())).sum::() - + self.idontwant.iter().map(|s| 1 + sizeof_len((s).get_size())).sum::() - } - - fn write_message(&self, w: &mut Writer) -> Result<()> { - for s in &self.ihave { w.write_with_tag(10, |w| w.write_message(s))?; } - for s in &self.iwant { w.write_with_tag(18, |w| w.write_message(s))?; } - for s in &self.graft { w.write_with_tag(26, |w| w.write_message(s))?; } - for s in &self.prune { w.write_with_tag(34, |w| w.write_message(s))?; } - for s in &self.idontwant { w.write_with_tag(42, |w| w.write_message(s))?; } - Ok(()) - } -} - -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Debug, Default, PartialEq, Clone)] -pub struct ControlIHave { - pub topic_id: Option, - pub message_ids: Vec>, -} - -impl<'a> MessageRead<'a> for ControlIHave { - fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { - let mut msg = Self::default(); - while !r.is_eof() { - match r.next_tag(bytes) { - Ok(10) => msg.topic_id = Some(r.read_string(bytes)?.to_owned()), - Ok(18) => msg.message_ids.push(r.read_bytes(bytes)?.to_owned()), - Ok(t) => { r.read_unknown(bytes, t)?; } - Err(e) => return Err(e), - } - } - Ok(msg) - } -} - -impl MessageWrite for ControlIHave { - fn get_size(&self) -> usize { - 0 - + self.topic_id.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) - + self.message_ids.iter().map(|s| 1 + sizeof_len((s).len())).sum::() - } - - fn write_message(&self, w: &mut Writer) -> Result<()> { - if let Some(ref s) = self.topic_id { w.write_with_tag(10, |w| w.write_string(&**s))?; } - for s in &self.message_ids { w.write_with_tag(18, |w| w.write_bytes(&**s))?; } - Ok(()) - } -} - -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Debug, Default, PartialEq, Clone)] -pub struct ControlIWant { - pub message_ids: Vec>, -} - -impl<'a> MessageRead<'a> for ControlIWant { - fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { - let mut msg = Self::default(); - while !r.is_eof() { - match r.next_tag(bytes) { - Ok(10) => msg.message_ids.push(r.read_bytes(bytes)?.to_owned()), - Ok(t) => { r.read_unknown(bytes, t)?; } - Err(e) => return Err(e), - } - } - Ok(msg) - } -} - -impl MessageWrite for ControlIWant { - fn get_size(&self) -> usize { - 0 - + self.message_ids.iter().map(|s| 1 + sizeof_len((s).len())).sum::() - } - - fn write_message(&self, w: &mut Writer) -> Result<()> { - for s in &self.message_ids { w.write_with_tag(10, |w| w.write_bytes(&**s))?; } - Ok(()) - } -} - -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Debug, Default, PartialEq, Clone)] -pub struct ControlGraft { - pub topic_id: Option, -} - -impl<'a> MessageRead<'a> for ControlGraft { - fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { - let mut msg = Self::default(); - while !r.is_eof() { - match r.next_tag(bytes) { - Ok(10) => msg.topic_id = Some(r.read_string(bytes)?.to_owned()), - Ok(t) => { r.read_unknown(bytes, t)?; } - Err(e) => return Err(e), - } - } - Ok(msg) - } -} - -impl MessageWrite for ControlGraft { - fn get_size(&self) -> usize { - 0 - + self.topic_id.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) - } - - fn write_message(&self, w: &mut Writer) -> Result<()> { - if let Some(ref s) = self.topic_id { w.write_with_tag(10, |w| w.write_string(&**s))?; } - Ok(()) - } -} - -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Debug, Default, PartialEq, Clone)] -pub struct ControlPrune { - pub topic_id: Option, - pub peers: Vec, - pub backoff: Option, -} - -impl<'a> MessageRead<'a> for ControlPrune { - fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { - let mut msg = Self::default(); - while !r.is_eof() { - match r.next_tag(bytes) { - Ok(10) => msg.topic_id = Some(r.read_string(bytes)?.to_owned()), - Ok(18) => msg.peers.push(r.read_message::(bytes)?), - Ok(24) => msg.backoff = Some(r.read_uint64(bytes)?), - Ok(t) => { r.read_unknown(bytes, t)?; } - Err(e) => return Err(e), - } - } - Ok(msg) - } -} - -impl MessageWrite for ControlPrune { - fn get_size(&self) -> usize { - 0 - + self.topic_id.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) - + self.peers.iter().map(|s| 1 + sizeof_len((s).get_size())).sum::() - + self.backoff.as_ref().map_or(0, |m| 1 + sizeof_varint(*(m) as u64)) - } - - fn write_message(&self, w: &mut Writer) -> Result<()> { - if let Some(ref s) = self.topic_id { w.write_with_tag(10, |w| w.write_string(&**s))?; } - for s in &self.peers { w.write_with_tag(18, |w| w.write_message(s))?; } - if let Some(ref s) = self.backoff { w.write_with_tag(24, |w| w.write_uint64(*s))?; } - Ok(()) - } -} - -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Debug, Default, PartialEq, Clone)] -pub struct ControlIDontWant { - pub message_ids: Vec>, -} - -impl<'a> MessageRead<'a> for ControlIDontWant { - fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { - let mut msg = Self::default(); - while !r.is_eof() { - match r.next_tag(bytes) { - Ok(10) => msg.message_ids.push(r.read_bytes(bytes)?.to_owned()), - Ok(t) => { r.read_unknown(bytes, t)?; } - Err(e) => return Err(e), - } - } - Ok(msg) - } -} - -impl MessageWrite for ControlIDontWant { - fn get_size(&self) -> usize { - 0 - + self.message_ids.iter().map(|s| 1 + sizeof_len((s).len())).sum::() - } - - fn write_message(&self, w: &mut Writer) -> Result<()> { - for s in &self.message_ids { w.write_with_tag(10, |w| w.write_bytes(&**s))?; } - Ok(()) - } -} - -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Debug, Default, PartialEq, Clone)] -pub struct PeerInfo { - pub peer_id: Option>, - pub signed_peer_record: Option>, -} - -impl<'a> MessageRead<'a> for PeerInfo { - fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { - let mut msg = Self::default(); - while !r.is_eof() { - match r.next_tag(bytes) { - Ok(10) => msg.peer_id = Some(r.read_bytes(bytes)?.to_owned()), - Ok(18) => msg.signed_peer_record = Some(r.read_bytes(bytes)?.to_owned()), - Ok(t) => { r.read_unknown(bytes, t)?; } - Err(e) => return Err(e), - } - } - Ok(msg) - } -} - -impl MessageWrite for PeerInfo { - fn get_size(&self) -> usize { - 0 - + self.peer_id.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) - + self.signed_peer_record.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) - } - - fn write_message(&self, w: &mut Writer) -> Result<()> { - if let Some(ref s) = self.peer_id { w.write_with_tag(10, |w| w.write_bytes(&**s))?; } - if let Some(ref s) = self.signed_peer_record { w.write_with_tag(18, |w| w.write_bytes(&**s))?; } - Ok(()) - } -} - -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Debug, Default, PartialEq, Clone)] -pub struct TopicDescriptor { - pub name: Option, - pub auth: Option, - pub enc: Option, -} - -impl<'a> MessageRead<'a> for TopicDescriptor { - fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { - let mut msg = Self::default(); - while !r.is_eof() { - match r.next_tag(bytes) { - Ok(10) => msg.name = Some(r.read_string(bytes)?.to_owned()), - Ok(18) => msg.auth = Some(r.read_message::(bytes)?), - Ok(26) => msg.enc = Some(r.read_message::(bytes)?), - Ok(t) => { r.read_unknown(bytes, t)?; } - Err(e) => return Err(e), - } - } - Ok(msg) - } -} - -impl MessageWrite for TopicDescriptor { - fn get_size(&self) -> usize { - 0 - + self.name.as_ref().map_or(0, |m| 1 + sizeof_len((m).len())) - + self.auth.as_ref().map_or(0, |m| 1 + sizeof_len((m).get_size())) - + self.enc.as_ref().map_or(0, |m| 1 + sizeof_len((m).get_size())) - } - - fn write_message(&self, w: &mut Writer) -> Result<()> { - if let Some(ref s) = self.name { w.write_with_tag(10, |w| w.write_string(&**s))?; } - if let Some(ref s) = self.auth { w.write_with_tag(18, |w| w.write_message(s))?; } - if let Some(ref s) = self.enc { w.write_with_tag(26, |w| w.write_message(s))?; } - Ok(()) - } -} - -pub mod mod_TopicDescriptor { - -use super::*; - -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Debug, Default, PartialEq, Clone)] -pub struct AuthOpts { - pub mode: Option, - pub keys: Vec>, -} - -impl<'a> MessageRead<'a> for AuthOpts { - fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { - let mut msg = Self::default(); - while !r.is_eof() { - match r.next_tag(bytes) { - Ok(8) => msg.mode = Some(r.read_enum(bytes)?), - Ok(18) => msg.keys.push(r.read_bytes(bytes)?.to_owned()), - Ok(t) => { r.read_unknown(bytes, t)?; } - Err(e) => return Err(e), - } - } - Ok(msg) - } -} - -impl MessageWrite for AuthOpts { - fn get_size(&self) -> usize { - 0 - + self.mode.as_ref().map_or(0, |m| 1 + sizeof_varint(*(m) as u64)) - + self.keys.iter().map(|s| 1 + sizeof_len((s).len())).sum::() - } - - fn write_message(&self, w: &mut Writer) -> Result<()> { - if let Some(ref s) = self.mode { w.write_with_tag(8, |w| w.write_enum(*s as i32))?; } - for s in &self.keys { w.write_with_tag(18, |w| w.write_bytes(&**s))?; } - Ok(()) - } -} - -pub mod mod_AuthOpts { - - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum AuthMode { - NONE = 0, - KEY = 1, - WOT = 2, -} - -impl Default for AuthMode { - fn default() -> Self { - AuthMode::NONE - } -} - -impl From for AuthMode { - fn from(i: i32) -> Self { - match i { - 0 => AuthMode::NONE, - 1 => AuthMode::KEY, - 2 => AuthMode::WOT, - _ => Self::default(), - } - } -} - -impl<'a> From<&'a str> for AuthMode { - fn from(s: &'a str) -> Self { - match s { - "NONE" => AuthMode::NONE, - "KEY" => AuthMode::KEY, - "WOT" => AuthMode::WOT, - _ => Self::default(), - } - } -} - -} - -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Debug, Default, PartialEq, Clone)] -pub struct EncOpts { - pub mode: Option, - pub key_hashes: Vec>, -} - -impl<'a> MessageRead<'a> for EncOpts { - fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { - let mut msg = Self::default(); - while !r.is_eof() { - match r.next_tag(bytes) { - Ok(8) => msg.mode = Some(r.read_enum(bytes)?), - Ok(18) => msg.key_hashes.push(r.read_bytes(bytes)?.to_owned()), - Ok(t) => { r.read_unknown(bytes, t)?; } - Err(e) => return Err(e), - } - } - Ok(msg) - } -} - -impl MessageWrite for EncOpts { - fn get_size(&self) -> usize { - 0 - + self.mode.as_ref().map_or(0, |m| 1 + sizeof_varint(*(m) as u64)) - + self.key_hashes.iter().map(|s| 1 + sizeof_len((s).len())).sum::() - } - - fn write_message(&self, w: &mut Writer) -> Result<()> { - if let Some(ref s) = self.mode { w.write_with_tag(8, |w| w.write_enum(*s as i32))?; } - for s in &self.key_hashes { w.write_with_tag(18, |w| w.write_bytes(&**s))?; } - Ok(()) - } -} - -pub mod mod_EncOpts { - - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum EncMode { - NONE = 0, - SHAREDKEY = 1, - WOT = 2, -} - -impl Default for EncMode { - fn default() -> Self { - EncMode::NONE - } -} - -impl From for EncMode { - fn from(i: i32) -> Self { - match i { - 0 => EncMode::NONE, - 1 => EncMode::SHAREDKEY, - 2 => EncMode::WOT, - _ => Self::default(), - } - } -} - -impl<'a> From<&'a str> for EncMode { - fn from(s: &'a str) -> Self { - match s { - "NONE" => EncMode::NONE, - "SHAREDKEY" => EncMode::SHAREDKEY, - "WOT" => EncMode::WOT, - _ => Self::default(), - } - } -} - -} - -} - diff --git a/beacon_node/lighthouse_network/gossipsub/src/generated/mod.rs b/beacon_node/lighthouse_network/gossipsub/src/generated/mod.rs deleted file mode 100644 index 7ac564f3c36..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/generated/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -// Automatically generated mod.rs -pub mod compat; -pub mod gossipsub; diff --git a/beacon_node/lighthouse_network/gossipsub/src/generated/rpc.proto b/beacon_node/lighthouse_network/gossipsub/src/generated/rpc.proto deleted file mode 100644 index e3b5888d2c0..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/generated/rpc.proto +++ /dev/null @@ -1,89 +0,0 @@ -syntax = "proto2"; - -package gossipsub.pb; - -message RPC { - repeated SubOpts subscriptions = 1; - repeated Message publish = 2; - - message SubOpts { - optional bool subscribe = 1; // subscribe or unsubscribe - optional string topic_id = 2; - } - - optional ControlMessage control = 3; -} - -message Message { - optional bytes from = 1; - optional bytes data = 2; - optional bytes seqno = 3; - required string topic = 4; - optional bytes signature = 5; - optional bytes key = 6; -} - -message ControlMessage { - repeated ControlIHave ihave = 1; - repeated ControlIWant iwant = 2; - repeated ControlGraft graft = 3; - repeated ControlPrune prune = 4; - repeated ControlIDontWant idontwant = 5; -} - -message ControlIHave { - optional string topic_id = 1; - repeated bytes message_ids = 2; -} - -message ControlIWant { - repeated bytes message_ids= 1; -} - -message ControlGraft { - optional string topic_id = 1; -} - -message ControlPrune { - optional string topic_id = 1; - repeated PeerInfo peers = 2; // gossipsub v1.1 PX - optional uint64 backoff = 3; // gossipsub v1.1 backoff time (in seconds) -} - -message ControlIDontWant { - repeated bytes message_ids = 1; -} - -message PeerInfo { - optional bytes peer_id = 1; - optional bytes signed_peer_record = 2; -} - -// topicID = hash(topicDescriptor); (not the topic.name) -message TopicDescriptor { - optional string name = 1; - optional AuthOpts auth = 2; - optional EncOpts enc = 3; - - message AuthOpts { - optional AuthMode mode = 1; - repeated bytes keys = 2; // root keys to trust - - enum AuthMode { - NONE = 0; // no authentication, anyone can publish - KEY = 1; // only messages signed by keys in the topic descriptor are accepted - WOT = 2; // web of trust, certificates can allow publisher set to grow - } - } - - message EncOpts { - optional EncMode mode = 1; - repeated bytes key_hashes = 2; // the hashes of the shared keys used (salted) - - enum EncMode { - NONE = 0; // no encryption, anyone can read - SHAREDKEY = 1; // messages are encrypted with shared key - WOT = 2; // web of trust, certificates can allow publisher set to grow - } - } -} diff --git a/beacon_node/lighthouse_network/gossipsub/src/gossip_promises.rs b/beacon_node/lighthouse_network/gossipsub/src/gossip_promises.rs deleted file mode 100644 index ce1dee2a72c..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/gossip_promises.rs +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2020 Sigma Prime Pty Ltd. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -use super::peer_score::RejectReason; -use super::MessageId; -use super::ValidationError; -use libp2p::identity::PeerId; -use std::collections::HashMap; -use web_time::Instant; - -/// Tracks recently sent `IWANT` messages and checks if peers respond to them. -#[derive(Default)] -pub(crate) struct GossipPromises { - /// Stores for each tracked message id and peer the instant when this promise expires. - /// - /// If the peer didn't respond until then we consider the promise as broken and penalize the - /// peer. - promises: HashMap>, -} - -impl GossipPromises { - /// Returns true if the message id exists in the promises. - pub(crate) fn contains(&self, message: &MessageId) -> bool { - self.promises.contains_key(message) - } - - /// Returns true if the message id exists in the promises and contains the given peer. - pub(crate) fn contains_peer(&self, message: &MessageId, peer: &PeerId) -> bool { - self.promises - .get(message) - .is_some_and(|peers| peers.contains_key(peer)) - } - - ///Get the peers we sent IWANT the input message id. - pub(crate) fn peers_for_message(&self, message_id: &MessageId) -> Vec { - self.promises - .get(message_id) - .map(|peers| peers.keys().copied().collect()) - .unwrap_or_default() - } - - /// Track a promise to deliver a message from a list of [`MessageId`]s we are requesting. - pub(crate) fn add_promise(&mut self, peer: PeerId, messages: &[MessageId], expires: Instant) { - for message_id in messages { - // If a promise for this message id and peer already exists we don't update the expiry! - self.promises - .entry(message_id.clone()) - .or_default() - .entry(peer) - .or_insert(expires); - } - } - - pub(crate) fn message_delivered(&mut self, message_id: &MessageId) { - // Someone delivered a message, we can stop tracking all promises for it. - self.promises.remove(message_id); - } - - pub(crate) fn reject_message(&mut self, message_id: &MessageId, reason: &RejectReason) { - // A message got rejected, so we can stop tracking promises and let the score penalty apply - // from invalid message delivery. - // We do take exception and apply promise penalty regardless in the following cases, where - // the peer delivered an obviously invalid message. - match reason { - RejectReason::ValidationError(ValidationError::InvalidSignature) => (), - RejectReason::SelfOrigin => (), - _ => { - self.promises.remove(message_id); - } - }; - } - - /// Returns the number of broken promises for each peer who didn't follow up on an IWANT - /// request. - /// This should be called not too often relative to the expire times, since it iterates over - /// the whole stored data. - pub(crate) fn get_broken_promises(&mut self) -> HashMap { - let now = Instant::now(); - let mut result = HashMap::new(); - self.promises.retain(|msg, peers| { - peers.retain(|peer_id, expires| { - if *expires < now { - let count = result.entry(*peer_id).or_insert(0); - *count += 1; - tracing::debug!( - peer=%peer_id, - message=%msg, - "[Penalty] The peer broke the promise to deliver message in time!" - ); - false - } else { - true - } - }); - !peers.is_empty() - }); - result - } -} diff --git a/beacon_node/lighthouse_network/gossipsub/src/handler.rs b/beacon_node/lighthouse_network/gossipsub/src/handler.rs deleted file mode 100644 index 0f25db6e3df..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/handler.rs +++ /dev/null @@ -1,558 +0,0 @@ -// Copyright 2020 Sigma Prime Pty Ltd. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -use super::protocol::{GossipsubCodec, ProtocolConfig}; -use super::rpc_proto::proto; -use super::types::{PeerKind, RawMessage, Rpc, RpcOut, RpcReceiver}; -use super::ValidationError; -use asynchronous_codec::Framed; -use futures::future::Either; -use futures::prelude::*; -use futures::StreamExt; -use libp2p::core::upgrade::DeniedUpgrade; -use libp2p::swarm::handler::{ - ConnectionEvent, ConnectionHandler, ConnectionHandlerEvent, DialUpgradeError, - FullyNegotiatedInbound, FullyNegotiatedOutbound, StreamUpgradeError, SubstreamProtocol, -}; -use libp2p::swarm::Stream; -use std::{ - pin::Pin, - task::{Context, Poll}, -}; -use web_time::Instant; - -/// The event emitted by the Handler. This informs the behaviour of various events created -/// by the handler. -#[derive(Debug)] -pub enum HandlerEvent { - /// A GossipsubRPC message has been received. This also contains a list of invalid messages (if - /// any) that were received. - Message { - /// The GossipsubRPC message excluding any invalid messages. - rpc: Rpc, - /// Any invalid messages that were received in the RPC, along with the associated - /// validation error. - invalid_messages: Vec<(RawMessage, ValidationError)>, - }, - /// An inbound or outbound substream has been established with the peer and this informs over - /// which protocol. This message only occurs once per connection. - PeerKind(PeerKind), - /// A message to be published was dropped because it could not be sent in time. - MessageDropped(RpcOut), -} - -/// A message sent from the behaviour to the handler. -#[allow(clippy::large_enum_variant)] -#[derive(Debug)] -pub enum HandlerIn { - /// The peer has joined the mesh. - JoinedMesh, - /// The peer has left the mesh. - LeftMesh, -} - -/// The maximum number of inbound or outbound substreams attempts we allow. -/// -/// Gossipsub is supposed to have a single long-lived inbound and outbound substream. On failure we -/// attempt to recreate these. This imposes an upper bound of new substreams before we consider the -/// connection faulty and disable the handler. This also prevents against potential substream -/// creation loops. -const MAX_SUBSTREAM_ATTEMPTS: usize = 5; - -#[allow(clippy::large_enum_variant)] -pub enum Handler { - Enabled(EnabledHandler), - Disabled(DisabledHandler), -} - -/// Protocol Handler that manages a single long-lived substream with a peer. -pub struct EnabledHandler { - /// Upgrade configuration for the gossipsub protocol. - listen_protocol: ProtocolConfig, - - /// The single long-lived outbound substream. - outbound_substream: Option, - - /// The single long-lived inbound substream. - inbound_substream: Option, - - /// Queue of values that we want to send to the remote - send_queue: RpcReceiver, - - /// Flag indicating that an outbound substream is being established to prevent duplicate - /// requests. - outbound_substream_establishing: bool, - - /// The number of outbound substreams we have requested. - outbound_substream_attempts: usize, - - /// The number of inbound substreams that have been created by the peer. - inbound_substream_attempts: usize, - - /// The type of peer this handler is associated to. - peer_kind: Option, - - /// Keeps track on whether we have sent the peer kind to the behaviour. - // - // NOTE: Use this flag rather than checking the substream count each poll. - peer_kind_sent: bool, - - last_io_activity: Instant, - - /// Keeps track of whether this connection is for a peer in the mesh. This is used to make - /// decisions about the keep alive state for this connection. - in_mesh: bool, -} - -pub enum DisabledHandler { - /// If the peer doesn't support the gossipsub protocol we do not immediately disconnect. - /// Rather, we disable the handler and prevent any incoming or outgoing substreams from being - /// established. - ProtocolUnsupported { - /// Keeps track on whether we have sent the peer kind to the behaviour. - peer_kind_sent: bool, - }, - /// The maximum number of inbound or outbound substream attempts have happened and thereby the - /// handler has been disabled. - MaxSubstreamAttempts, -} - -/// State of the inbound substream, opened either by us or by the remote. -enum InboundSubstreamState { - /// Waiting for a message from the remote. The idle state for an inbound substream. - WaitingInput(Framed), - /// The substream is being closed. - Closing(Framed), - /// An error occurred during processing. - Poisoned, -} - -/// State of the outbound substream, opened either by us or by the remote. -enum OutboundSubstreamState { - /// Waiting for the user to send a message. The idle state for an outbound substream. - WaitingOutput(Framed), - /// Waiting to send a message to the remote. - PendingSend(Framed, proto::RPC), - /// Waiting to flush the substream so that the data arrives to the remote. - PendingFlush(Framed), - /// An error occurred during processing. - Poisoned, -} - -impl Handler { - /// Builds a new [`Handler`]. - pub fn new(protocol_config: ProtocolConfig, message_queue: RpcReceiver) -> Self { - Handler::Enabled(EnabledHandler { - listen_protocol: protocol_config, - inbound_substream: None, - outbound_substream: None, - outbound_substream_establishing: false, - outbound_substream_attempts: 0, - inbound_substream_attempts: 0, - peer_kind: None, - peer_kind_sent: false, - last_io_activity: Instant::now(), - in_mesh: false, - send_queue: message_queue, - }) - } -} - -impl EnabledHandler { - fn on_fully_negotiated_inbound( - &mut self, - (substream, peer_kind): (Framed, PeerKind), - ) { - // update the known kind of peer - if self.peer_kind.is_none() { - self.peer_kind = Some(peer_kind); - } - - // new inbound substream. Replace the current one, if it exists. - tracing::trace!("New inbound substream request"); - self.inbound_substream = Some(InboundSubstreamState::WaitingInput(substream)); - } - - fn on_fully_negotiated_outbound( - &mut self, - FullyNegotiatedOutbound { protocol, .. }: FullyNegotiatedOutbound< - ::OutboundProtocol, - >, - ) { - let (substream, peer_kind) = protocol; - - // update the known kind of peer - if self.peer_kind.is_none() { - self.peer_kind = Some(peer_kind); - } - - assert!( - self.outbound_substream.is_none(), - "Established an outbound substream with one already available" - ); - self.outbound_substream = Some(OutboundSubstreamState::WaitingOutput(substream)); - } - - fn poll( - &mut self, - cx: &mut Context<'_>, - ) -> Poll< - ConnectionHandlerEvent< - ::OutboundProtocol, - (), - ::ToBehaviour, - >, - > { - if !self.peer_kind_sent { - if let Some(peer_kind) = self.peer_kind.as_ref() { - self.peer_kind_sent = true; - return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour( - HandlerEvent::PeerKind(peer_kind.clone()), - )); - } - } - - // determine if we need to create the outbound stream - if !self.send_queue.poll_is_empty(cx) - && self.outbound_substream.is_none() - && !self.outbound_substream_establishing - { - self.outbound_substream_establishing = true; - return Poll::Ready(ConnectionHandlerEvent::OutboundSubstreamRequest { - protocol: SubstreamProtocol::new(self.listen_protocol.clone(), ()), - }); - } - - // process outbound stream - loop { - match std::mem::replace( - &mut self.outbound_substream, - Some(OutboundSubstreamState::Poisoned), - ) { - // outbound idle state - Some(OutboundSubstreamState::WaitingOutput(substream)) => { - if let Poll::Ready(Some(mut message)) = self.send_queue.poll_next_unpin(cx) { - match message { - RpcOut::Publish { - message: _, - ref mut timeout, - } - | RpcOut::Forward { - message: _, - ref mut timeout, - } => { - if Pin::new(timeout).poll(cx).is_ready() { - // Inform the behaviour and end the poll. - self.outbound_substream = - Some(OutboundSubstreamState::WaitingOutput(substream)); - return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour( - HandlerEvent::MessageDropped(message), - )); - } - } - _ => {} // All other messages are not time-bound. - } - self.outbound_substream = Some(OutboundSubstreamState::PendingSend( - substream, - message.into_protobuf(), - )); - continue; - } - - self.outbound_substream = - Some(OutboundSubstreamState::WaitingOutput(substream)); - break; - } - Some(OutboundSubstreamState::PendingSend(mut substream, message)) => { - match Sink::poll_ready(Pin::new(&mut substream), cx) { - Poll::Ready(Ok(())) => { - match Sink::start_send(Pin::new(&mut substream), message) { - Ok(()) => { - self.outbound_substream = - Some(OutboundSubstreamState::PendingFlush(substream)) - } - Err(e) => { - tracing::debug!( - "Failed to send message on outbound stream: {e}" - ); - self.outbound_substream = None; - break; - } - } - } - Poll::Ready(Err(e)) => { - tracing::debug!("Failed to send message on outbound stream: {e}"); - self.outbound_substream = None; - break; - } - Poll::Pending => { - self.outbound_substream = - Some(OutboundSubstreamState::PendingSend(substream, message)); - break; - } - } - } - Some(OutboundSubstreamState::PendingFlush(mut substream)) => { - match Sink::poll_flush(Pin::new(&mut substream), cx) { - Poll::Ready(Ok(())) => { - self.last_io_activity = Instant::now(); - self.outbound_substream = - Some(OutboundSubstreamState::WaitingOutput(substream)) - } - Poll::Ready(Err(e)) => { - tracing::debug!("Failed to flush outbound stream: {e}"); - self.outbound_substream = None; - break; - } - Poll::Pending => { - self.outbound_substream = - Some(OutboundSubstreamState::PendingFlush(substream)); - break; - } - } - } - None => { - self.outbound_substream = None; - break; - } - Some(OutboundSubstreamState::Poisoned) => { - unreachable!("Error occurred during outbound stream processing") - } - } - } - - // Handle inbound messages. - loop { - match std::mem::replace( - &mut self.inbound_substream, - Some(InboundSubstreamState::Poisoned), - ) { - // inbound idle state - Some(InboundSubstreamState::WaitingInput(mut substream)) => { - match substream.poll_next_unpin(cx) { - Poll::Ready(Some(Ok(message))) => { - self.last_io_activity = Instant::now(); - self.inbound_substream = - Some(InboundSubstreamState::WaitingInput(substream)); - return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour(message)); - } - Poll::Ready(Some(Err(error))) => { - tracing::debug!("Failed to read from inbound stream: {error}"); - // Close this side of the stream. If the - // peer is still around, they will re-establish their - // outbound stream i.e. our inbound stream. - self.inbound_substream = - Some(InboundSubstreamState::Closing(substream)); - } - // peer closed the stream - Poll::Ready(None) => { - tracing::debug!("Inbound stream closed by remote"); - self.inbound_substream = - Some(InboundSubstreamState::Closing(substream)); - } - Poll::Pending => { - self.inbound_substream = - Some(InboundSubstreamState::WaitingInput(substream)); - break; - } - } - } - Some(InboundSubstreamState::Closing(mut substream)) => { - match Sink::poll_close(Pin::new(&mut substream), cx) { - Poll::Ready(res) => { - if let Err(e) = res { - // Don't close the connection but just drop the inbound substream. - // In case the remote has more to send, they will open up a new - // substream. - tracing::debug!("Inbound substream error while closing: {e}"); - } - self.inbound_substream = None; - break; - } - Poll::Pending => { - self.inbound_substream = - Some(InboundSubstreamState::Closing(substream)); - break; - } - } - } - None => { - self.inbound_substream = None; - break; - } - Some(InboundSubstreamState::Poisoned) => { - unreachable!("Error occurred during inbound stream processing") - } - } - } - - // Drop the next message in queue if it's stale. - if let Poll::Ready(Some(rpc)) = self.send_queue.poll_stale(cx) { - return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour( - HandlerEvent::MessageDropped(rpc), - )); - } - - Poll::Pending - } -} - -impl ConnectionHandler for Handler { - type FromBehaviour = HandlerIn; - type ToBehaviour = HandlerEvent; - type InboundOpenInfo = (); - type InboundProtocol = either::Either; - type OutboundOpenInfo = (); - type OutboundProtocol = ProtocolConfig; - - fn listen_protocol(&self) -> SubstreamProtocol { - match self { - Handler::Enabled(handler) => { - SubstreamProtocol::new(either::Either::Left(handler.listen_protocol.clone()), ()) - } - Handler::Disabled(_) => { - SubstreamProtocol::new(either::Either::Right(DeniedUpgrade), ()) - } - } - } - - fn on_behaviour_event(&mut self, message: HandlerIn) { - match self { - Handler::Enabled(handler) => match message { - HandlerIn::JoinedMesh => { - handler.in_mesh = true; - } - HandlerIn::LeftMesh => { - handler.in_mesh = false; - } - }, - Handler::Disabled(_) => { - tracing::debug!(?message, "Handler is disabled. Dropping message"); - } - } - } - - fn connection_keep_alive(&self) -> bool { - matches!(self, Handler::Enabled(h) if h.in_mesh) - } - - #[tracing::instrument(level = "trace", name = "ConnectionHandler::poll", skip(self, cx))] - fn poll( - &mut self, - cx: &mut Context<'_>, - ) -> Poll> { - match self { - Handler::Enabled(handler) => handler.poll(cx), - Handler::Disabled(DisabledHandler::ProtocolUnsupported { peer_kind_sent }) => { - if !*peer_kind_sent { - *peer_kind_sent = true; - return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour( - HandlerEvent::PeerKind(PeerKind::NotSupported), - )); - } - - Poll::Pending - } - Handler::Disabled(DisabledHandler::MaxSubstreamAttempts) => Poll::Pending, - } - } - - fn on_connection_event( - &mut self, - event: ConnectionEvent, - ) { - match self { - Handler::Enabled(handler) => { - if event.is_inbound() { - handler.inbound_substream_attempts += 1; - - if handler.inbound_substream_attempts == MAX_SUBSTREAM_ATTEMPTS { - tracing::warn!( - "The maximum number of inbound substreams attempts has been exceeded" - ); - *self = Handler::Disabled(DisabledHandler::MaxSubstreamAttempts); - return; - } - } - - if event.is_outbound() { - handler.outbound_substream_establishing = false; - - handler.outbound_substream_attempts += 1; - - if handler.outbound_substream_attempts == MAX_SUBSTREAM_ATTEMPTS { - tracing::warn!( - "The maximum number of outbound substream attempts has been exceeded" - ); - *self = Handler::Disabled(DisabledHandler::MaxSubstreamAttempts); - return; - } - } - - match event { - ConnectionEvent::FullyNegotiatedInbound(FullyNegotiatedInbound { - protocol, - .. - }) => match protocol { - Either::Left(protocol) => handler.on_fully_negotiated_inbound(protocol), - #[allow(unreachable_patterns)] - Either::Right(v) => libp2p::core::util::unreachable(v), - }, - ConnectionEvent::FullyNegotiatedOutbound(fully_negotiated_outbound) => { - handler.on_fully_negotiated_outbound(fully_negotiated_outbound) - } - ConnectionEvent::DialUpgradeError(DialUpgradeError { - error: StreamUpgradeError::Timeout, - .. - }) => { - tracing::debug!("Dial upgrade error: Protocol negotiation timeout"); - } - // This pattern is unreachable as of Rust 1.82, we can remove it once the - // MSRV is increased past that version. - #[allow(unreachable_patterns)] - ConnectionEvent::DialUpgradeError(DialUpgradeError { - error: StreamUpgradeError::Apply(e), - .. - }) => void::unreachable(e), - ConnectionEvent::DialUpgradeError(DialUpgradeError { - error: StreamUpgradeError::NegotiationFailed, - .. - }) => { - // The protocol is not supported - tracing::debug!( - "The remote peer does not support gossipsub on this connection" - ); - *self = Handler::Disabled(DisabledHandler::ProtocolUnsupported { - peer_kind_sent: false, - }); - } - ConnectionEvent::DialUpgradeError(DialUpgradeError { - error: StreamUpgradeError::Io(e), - .. - }) => { - tracing::debug!("Protocol negotiation failed: {e}") - } - _ => {} - } - } - Handler::Disabled(_) => {} - } - } -} diff --git a/beacon_node/lighthouse_network/gossipsub/src/lib.rs b/beacon_node/lighthouse_network/gossipsub/src/lib.rs deleted file mode 100644 index 1d29aaa7598..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/lib.rs +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright 2020 Sigma Prime Pty Ltd. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -//! Implementation of the [Gossipsub](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/README.md) protocol. -//! -//! Gossipsub is a P2P pubsub (publish/subscription) routing layer designed to extend upon -//! floodsub and meshsub routing protocols. -//! -//! # Overview -//! -//! *Note: The gossipsub protocol specifications -//! () provide an outline for the -//! routing protocol. They should be consulted for further detail.* -//! -//! Gossipsub is a blend of meshsub for data and randomsub for mesh metadata. It provides bounded -//! degree and amplification factor with the meshsub construction and augments it using gossip -//! propagation of metadata with the randomsub technique. -//! -//! The router maintains an overlay mesh network of peers on which to efficiently send messages and -//! metadata. Peers use control messages to broadcast and request known messages and -//! subscribe/unsubscribe from topics in the mesh network. -//! -//! # Important Discrepancies -//! -//! This section outlines the current implementation's potential discrepancies from that of other -//! implementations, due to undefined elements in the current specification. -//! -//! - **Topics** - In gossipsub, topics configurable by the `hash_topics` configuration parameter. -//! Topics are of type [`TopicHash`]. The current go implementation uses raw utf-8 strings, and this -//! is default configuration in rust-libp2p. Topics can be hashed (SHA256 hashed then base64 -//! encoded) by setting the `hash_topics` configuration parameter to true. -//! -//! - **Sequence Numbers** - A message on the gossipsub network is identified by the source -//! [`PeerId`](libp2p_identity::PeerId) and a nonce (sequence number) of the message. The sequence numbers in -//! this implementation are sent as raw bytes across the wire. They are 64-bit big-endian unsigned -//! integers. When messages are signed, they are monotonically increasing integers starting from a -//! random value and wrapping around u64::MAX. When messages are unsigned, they are chosen at random. -//! NOTE: These numbers are sequential in the current go implementation. -//! -//! # Peer Discovery -//! -//! Gossipsub does not provide peer discovery by itself. Peer discovery is the process by which -//! peers in a p2p network exchange information about each other among other reasons to become resistant -//! against the failure or replacement of the -//! [boot nodes](https://docs.libp2p.io/reference/glossary/#boot-node) of the network. -//! -//! Peer -//! discovery can e.g. be implemented with the help of the [Kademlia](https://github.com/libp2p/specs/blob/master/kad-dht/README.md) protocol -//! in combination with the [Identify](https://github.com/libp2p/specs/tree/master/identify) protocol. See the -//! Kademlia implementation documentation for more information. -//! -//! # Using Gossipsub -//! -//! ## Gossipsub Config -//! -//! The [`Config`] struct specifies various network performance/tuning configuration -//! parameters. Specifically it specifies: -//! -//! [`Config`]: struct.Config.html -//! -//! This struct implements the [`Default`] trait and can be initialised via -//! [`Config::default()`]. -//! -//! -//! ## Behaviour -//! -//! The [`Behaviour`] struct implements the [`libp2p_swarm::NetworkBehaviour`] trait allowing it to -//! act as the routing behaviour in a [`libp2p_swarm::Swarm`]. This struct requires an instance of -//! [`PeerId`](libp2p_identity::PeerId) and [`Config`]. -//! -//! [`Behaviour`]: struct.Behaviour.html - -//! ## Example -//! -//! For an example on how to use gossipsub, see the [chat-example](https://github.com/libp2p/rust-libp2p/tree/master/examples/chat). - -#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] - -mod backoff; -mod behaviour; -mod config; -mod error; -mod gossip_promises; -mod handler; -mod mcache; -mod metrics; -mod peer_score; -mod protocol; -mod rpc_proto; -mod subscription_filter; -mod time_cache; -mod topic; -mod transform; -mod types; - -pub use self::behaviour::{Behaviour, Event, MessageAuthenticity}; -pub use self::config::{Config, ConfigBuilder, ValidationMode, Version}; -pub use self::error::{ConfigBuilderError, PublishError, SubscriptionError, ValidationError}; -pub use self::metrics::Config as MetricsConfig; -pub use self::peer_score::{ - score_parameter_decay, score_parameter_decay_with_base, PeerScoreParams, PeerScoreThresholds, - TopicScoreParams, -}; -pub use self::subscription_filter::{ - AllowAllSubscriptionFilter, CallbackSubscriptionFilter, CombinedSubscriptionFilters, - MaxCountSubscriptionFilter, RegexSubscriptionFilter, TopicSubscriptionFilter, - WhitelistSubscriptionFilter, -}; -pub use self::topic::{Hasher, Topic, TopicHash}; -pub use self::transform::{DataTransform, IdentityTransform}; -pub use self::types::{FailedMessages, Message, MessageAcceptance, MessageId, RawMessage}; - -#[deprecated(note = "Will be removed from the public API.")] -pub type Rpc = self::types::Rpc; - -pub type IdentTopic = Topic; -pub type Sha256Topic = Topic; diff --git a/beacon_node/lighthouse_network/gossipsub/src/mcache.rs b/beacon_node/lighthouse_network/gossipsub/src/mcache.rs deleted file mode 100644 index eced0456d68..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/mcache.rs +++ /dev/null @@ -1,385 +0,0 @@ -// Copyright 2020 Sigma Prime Pty Ltd. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -use super::topic::TopicHash; -use super::types::{MessageId, RawMessage}; -use libp2p::identity::PeerId; -use std::collections::hash_map::Entry; -use std::fmt::Debug; -use std::{ - collections::{HashMap, HashSet}, - fmt, -}; - -/// CacheEntry stored in the history. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub(crate) struct CacheEntry { - mid: MessageId, - topic: TopicHash, -} - -/// MessageCache struct holding history of messages. -#[derive(Clone)] -pub(crate) struct MessageCache { - msgs: HashMap)>, - /// For every message and peer the number of times this peer asked for the message - iwant_counts: HashMap>, - history: Vec>, - /// The number of indices in the cache history used for gossiping. That means that a message - /// won't get gossiped anymore when shift got called `gossip` many times after inserting the - /// message in the cache. - gossip: usize, -} - -impl fmt::Debug for MessageCache { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("MessageCache") - .field("msgs", &self.msgs) - .field("history", &self.history) - .field("gossip", &self.gossip) - .finish() - } -} - -/// Implementation of the MessageCache. -impl MessageCache { - pub(crate) fn new(gossip: usize, history_capacity: usize) -> Self { - MessageCache { - gossip, - msgs: HashMap::default(), - iwant_counts: HashMap::default(), - history: vec![Vec::new(); history_capacity], - } - } - - /// Put a message into the memory cache. - /// - /// Returns true if the message didn't already exist in the cache. - pub(crate) fn put(&mut self, message_id: &MessageId, msg: RawMessage) -> bool { - match self.msgs.entry(message_id.clone()) { - Entry::Occupied(_) => { - // Don't add duplicate entries to the cache. - false - } - Entry::Vacant(entry) => { - let cache_entry = CacheEntry { - mid: message_id.clone(), - topic: msg.topic.clone(), - }; - entry.insert((msg, HashSet::default())); - self.history[0].push(cache_entry); - - tracing::trace!(message=?message_id, "Put message in mcache"); - true - } - } - } - - /// Keeps track of peers we know have received the message to prevent forwarding to said peers. - pub(crate) fn observe_duplicate(&mut self, message_id: &MessageId, source: &PeerId) { - if let Some((message, originating_peers)) = self.msgs.get_mut(message_id) { - // if the message is already validated, we don't need to store extra peers sending us - // duplicates as the message has already been forwarded - if message.validated { - return; - } - - originating_peers.insert(*source); - } - } - - /// Get a message with `message_id` - #[cfg(test)] - pub(crate) fn get(&self, message_id: &MessageId) -> Option<&RawMessage> { - self.msgs.get(message_id).map(|(message, _)| message) - } - - /// Increases the iwant count for the given message by one and returns the message together - /// with the iwant if the message exists. - pub(crate) fn get_with_iwant_counts( - &mut self, - message_id: &MessageId, - peer: &PeerId, - ) -> Option<(&RawMessage, u32)> { - let iwant_counts = &mut self.iwant_counts; - self.msgs.get(message_id).and_then(|(message, _)| { - if !message.validated { - None - } else { - Some((message, { - let count = iwant_counts - .entry(message_id.clone()) - .or_default() - .entry(*peer) - .or_default(); - *count += 1; - *count - })) - } - }) - } - - /// Gets a message with [`MessageId`] and tags it as validated. - /// This function also returns the known peers that have sent us this message. This is used to - /// prevent us sending redundant messages to peers who have already propagated it. - pub(crate) fn validate( - &mut self, - message_id: &MessageId, - ) -> Option<(&RawMessage, HashSet)> { - self.msgs.get_mut(message_id).map(|(message, known_peers)| { - message.validated = true; - // Clear the known peers list (after a message is validated, it is forwarded and we no - // longer need to store the originating peers). - let originating_peers = std::mem::take(known_peers); - (&*message, originating_peers) - }) - } - - /// Get a list of [`MessageId`]s for a given topic. - pub(crate) fn get_gossip_message_ids(&self, topic: &TopicHash) -> Vec { - self.history[..self.gossip] - .iter() - .fold(vec![], |mut current_entries, entries| { - // search for entries with desired topic - let mut found_entries: Vec = entries - .iter() - .filter_map(|entry| { - if &entry.topic == topic { - let mid = &entry.mid; - // Only gossip validated messages - if let Some(true) = self.msgs.get(mid).map(|(msg, _)| msg.validated) { - Some(mid.clone()) - } else { - None - } - } else { - None - } - }) - .collect(); - - // generate the list - current_entries.append(&mut found_entries); - current_entries - }) - } - - /// Shift the history array down one and delete messages associated with the - /// last entry. - pub(crate) fn shift(&mut self) { - for entry in self.history.pop().expect("history is always > 1") { - if let Some((msg, _)) = self.msgs.remove(&entry.mid) { - if !msg.validated { - // If GossipsubConfig::validate_messages is true, the implementing - // application has to ensure that Gossipsub::validate_message gets called for - // each received message within the cache timeout time." - tracing::debug!( - message=%&entry.mid, - "The message got removed from the cache without being validated." - ); - } - } - tracing::trace!(message=%&entry.mid, "Remove message from the cache"); - - self.iwant_counts.remove(&entry.mid); - } - - // Insert an empty vec in position 0 - self.history.insert(0, Vec::new()); - } - - /// Removes a message from the cache and returns it if existent - pub(crate) fn remove( - &mut self, - message_id: &MessageId, - ) -> Option<(RawMessage, HashSet)> { - //We only remove the message from msgs and iwant_count and keep the message_id in the - // history vector. Zhe id in the history vector will simply be ignored on popping. - - self.iwant_counts.remove(message_id); - self.msgs.remove(message_id) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::IdentTopic as Topic; - - fn gen_testm(x: u64, topic: TopicHash) -> (MessageId, RawMessage) { - let default_id = |message: &RawMessage| { - // default message id is: source + sequence number - let mut source_string = message.source.as_ref().unwrap().to_base58(); - source_string.push_str(&message.sequence_number.unwrap().to_string()); - MessageId::from(source_string) - }; - let u8x: u8 = x as u8; - let source = Some(PeerId::random()); - let data: Vec = vec![u8x]; - let sequence_number = Some(x); - - let m = RawMessage { - source, - data, - sequence_number, - topic, - signature: None, - key: None, - validated: false, - }; - - let id = default_id(&m); - (id, m) - } - - fn new_cache(gossip_size: usize, history: usize) -> MessageCache { - MessageCache::new(gossip_size, history) - } - - #[test] - /// Test that the message cache can be created. - fn test_new_cache() { - let x: usize = 3; - let mc = new_cache(x, 5); - - assert_eq!(mc.gossip, x); - } - - #[test] - /// Test you can put one message and get one. - fn test_put_get_one() { - let mut mc = new_cache(10, 15); - - let topic1_hash = Topic::new("topic1").hash(); - let (id, m) = gen_testm(10, topic1_hash); - - mc.put(&id, m.clone()); - - assert_eq!(mc.history[0].len(), 1); - - let fetched = mc.get(&id); - - assert_eq!(fetched.unwrap(), &m); - } - - #[test] - /// Test attempting to 'get' with a wrong id. - fn test_get_wrong() { - let mut mc = new_cache(10, 15); - - let topic1_hash = Topic::new("topic1").hash(); - let (id, m) = gen_testm(10, topic1_hash); - - mc.put(&id, m); - - // Try to get an incorrect ID - let wrong_id = MessageId::new(b"wrongid"); - let fetched = mc.get(&wrong_id); - assert!(fetched.is_none()); - } - - #[test] - /// Test attempting to 'get' empty message cache. - fn test_get_empty() { - let mc = new_cache(10, 15); - - // Try to get an incorrect ID - let wrong_string = MessageId::new(b"imempty"); - let fetched = mc.get(&wrong_string); - assert!(fetched.is_none()); - } - - #[test] - /// Test shift mechanism. - fn test_shift() { - let mut mc = new_cache(1, 5); - - let topic1_hash = Topic::new("topic1").hash(); - - // Build the message - for i in 0..10 { - let (id, m) = gen_testm(i, topic1_hash.clone()); - mc.put(&id, m.clone()); - } - - mc.shift(); - - // Ensure the shift occurred - assert!(mc.history[0].is_empty()); - assert!(mc.history[1].len() == 10); - - // Make sure no messages deleted - assert!(mc.msgs.len() == 10); - } - - #[test] - /// Test Shift with no additions. - fn test_empty_shift() { - let mut mc = new_cache(1, 5); - - let topic1_hash = Topic::new("topic1").hash(); - - // Build the message - for i in 0..10 { - let (id, m) = gen_testm(i, topic1_hash.clone()); - mc.put(&id, m.clone()); - } - - mc.shift(); - - // Ensure the shift occurred - assert!(mc.history[0].is_empty()); - assert!(mc.history[1].len() == 10); - - mc.shift(); - - assert!(mc.history[2].len() == 10); - assert!(mc.history[1].is_empty()); - assert!(mc.history[0].is_empty()); - } - - #[test] - /// Test shift to see if the last history messages are removed. - fn test_remove_last_from_shift() { - let mut mc = new_cache(4, 5); - - let topic1_hash = Topic::new("topic1").hash(); - - // Build the message - for i in 0..10 { - let (id, m) = gen_testm(i, topic1_hash.clone()); - mc.put(&id, m.clone()); - } - - // Shift right until deleting messages - mc.shift(); - mc.shift(); - mc.shift(); - mc.shift(); - - assert_eq!(mc.history[mc.history.len() - 1].len(), 10); - - // Shift and delete the messages - mc.shift(); - assert_eq!(mc.history[mc.history.len() - 1].len(), 0); - assert_eq!(mc.history[0].len(), 0); - assert_eq!(mc.msgs.len(), 0); - } -} diff --git a/beacon_node/lighthouse_network/gossipsub/src/metrics.rs b/beacon_node/lighthouse_network/gossipsub/src/metrics.rs deleted file mode 100644 index 2989f95a26b..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/metrics.rs +++ /dev/null @@ -1,800 +0,0 @@ -// Copyright 2020 Sigma Prime Pty Ltd. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -//! A set of metrics used to help track and diagnose the network behaviour of the gossipsub -//! protocol. - -use std::collections::HashMap; - -use prometheus_client::encoding::{EncodeLabelSet, EncodeLabelValue}; -use prometheus_client::metrics::counter::Counter; -use prometheus_client::metrics::family::{Family, MetricConstructor}; -use prometheus_client::metrics::gauge::Gauge; -use prometheus_client::metrics::histogram::{linear_buckets, Histogram}; -use prometheus_client::registry::Registry; - -use super::topic::TopicHash; -use super::types::{MessageAcceptance, PeerKind}; - -// Default value that limits for how many topics do we store metrics. -const DEFAULT_MAX_TOPICS: usize = 300; - -// Default value that limits how many topics for which there has never been a subscription do we -// store metrics. -const DEFAULT_MAX_NEVER_SUBSCRIBED_TOPICS: usize = 100; - -#[derive(Debug, Clone)] -pub struct Config { - /// This provides an upper bound to the number of mesh topics we create metrics for. It - /// prevents unbounded labels being created in the metrics. - pub max_topics: usize, - /// Mesh topics are controlled by the user via subscriptions whereas non-mesh topics are - /// determined by users on the network. This limit permits a fixed amount of topics to allow, - /// in-addition to the mesh topics. - pub max_never_subscribed_topics: usize, - /// Buckets used for the score histograms. - pub score_buckets: Vec, -} - -impl Config { - /// Create buckets for the score histograms based on score thresholds. - pub fn buckets_using_scoring_thresholds(&mut self, params: &super::PeerScoreThresholds) { - self.score_buckets = vec![ - params.graylist_threshold, - params.publish_threshold, - params.gossip_threshold, - params.gossip_threshold / 2.0, - params.gossip_threshold / 4.0, - 0.0, - 1.0, - 10.0, - 100.0, - ]; - } -} - -impl Default for Config { - fn default() -> Self { - // Some sensible defaults - let gossip_threshold = -4000.0; - let publish_threshold = -8000.0; - let graylist_threshold = -16000.0; - let score_buckets: Vec = vec![ - graylist_threshold, - publish_threshold, - gossip_threshold, - gossip_threshold / 2.0, - gossip_threshold / 4.0, - 0.0, - 1.0, - 10.0, - 100.0, - ]; - Config { - max_topics: DEFAULT_MAX_TOPICS, - max_never_subscribed_topics: DEFAULT_MAX_NEVER_SUBSCRIBED_TOPICS, - score_buckets, - } - } -} - -/// Whether we have ever been subscribed to this topic. -type EverSubscribed = bool; - -/// A collection of metrics used throughout the Gossipsub behaviour. -pub(crate) struct Metrics { - /* Configuration parameters */ - /// Maximum number of topics for which we store metrics. This helps keep the metrics bounded. - max_topics: usize, - /// Maximum number of topics for which we store metrics, where the topic in not one to which we - /// have subscribed at some point. This helps keep the metrics bounded, since these topics come - /// from received messages and not explicit application subscriptions. - max_never_subscribed_topics: usize, - - /* Auxiliary variables */ - /// Information needed to decide if a topic is allowed or not. - topic_info: HashMap, - - /* Metrics per known topic */ - /// Status of our subscription to this topic. This metric allows analyzing other topic metrics - /// filtered by our current subscription status. - topic_subscription_status: Family, - /// Number of peers subscribed to each topic. This allows us to analyze a topic's behaviour - /// regardless of our subscription status. - topic_peers_count: Family, - /// The number of invalid messages received for a given topic. - invalid_messages: Family, - /// The number of messages accepted by the application (validation result). - accepted_messages: Family, - /// The number of messages ignored by the application (validation result). - ignored_messages: Family, - /// The number of messages rejected by the application (validation result). - rejected_messages: Family, - /// The number of publish messages dropped by the sender. - publish_messages_dropped: Family, - /// The number of forward messages dropped by the sender. - forward_messages_dropped: Family, - - /* Metrics regarding mesh state */ - /// Number of peers in our mesh. This metric should be updated with the count of peers for a - /// topic in the mesh regardless of inclusion and churn events. - mesh_peer_counts: Family, - /// Number of times we include peers in a topic mesh for different reasons. - mesh_peer_inclusion_events: Family, - /// Number of times we remove peers in a topic mesh for different reasons. - mesh_peer_churn_events: Family, - - /* Metrics regarding messages sent/received */ - /// Number of gossip messages sent to each topic. - topic_msg_sent_counts: Family, - /// Bytes from gossip messages sent to each topic. - topic_msg_sent_bytes: Family, - /// Number of gossipsub messages published to each topic. - topic_msg_published: Family, - - /// Number of gossipsub messages received on each topic (without filtering duplicates). - topic_msg_recv_counts_unfiltered: Family, - /// Number of gossipsub messages received on each topic (after filtering duplicates). - topic_msg_recv_counts: Family, - /// Bytes received from gossip messages for each topic. - topic_msg_recv_bytes: Family, - - /* Metrics related to scoring */ - /// Histogram of the scores for each mesh topic. - score_per_mesh: Family, - /// A counter of the kind of penalties being applied to peers. - scoring_penalties: Family, - - /* General Metrics */ - /// Gossipsub supports floodsub, gossipsub v1.0 and gossipsub v1.1. Peers are classified based - /// on which protocol they support. This metric keeps track of the number of peers that are - /// connected of each type. - peers_per_protocol: Family, - /// The time it takes to complete one iteration of the heartbeat. - heartbeat_duration: Histogram, - - /* Performance metrics */ - /// When the user validates a message, it tries to re propagate it to its mesh peers. If the - /// message expires from the memcache before it can be validated, we count this a cache miss - /// and it is an indicator that the memcache size should be increased. - memcache_misses: Counter, - /// The number of times we have decided that an IWANT control message is required for this - /// topic. A very high metric might indicate an underperforming network. - topic_iwant_msgs: Family, - - /// The number of times we have received an IDONTWANT control message. - idontwant_msgs: Counter, - - /// The number of msg_id's we have received in every IDONTWANT control message. - idontwant_msgs_ids: Counter, - - /// The number of bytes we have received in every IDONTWANT control message. - idontwant_bytes: Counter, - - /// Number of IDONTWANT messages sent per topic. - idontwant_messages_sent_per_topic: Family, - - /// Number of full messages we received that we previously sent a IDONTWANT for. - idontwant_messages_ignored_per_topic: Family, - - /// Count of duplicate messages we have received from mesh peers for a given topic. - mesh_duplicates: Family, - - /// Count of duplicate messages we have received from by requesting them over iwant for a given topic. - iwant_duplicates: Family, - - /// The size of the priority queue. - priority_queue_size: Histogram, - /// The size of the non-priority queue. - non_priority_queue_size: Histogram, -} - -impl Metrics { - pub(crate) fn new(registry: &mut Registry, config: Config) -> Self { - // Destructure the config to be sure everything is used. - let Config { - max_topics, - max_never_subscribed_topics, - score_buckets, - } = config; - - macro_rules! register_family { - ($name:expr, $help:expr) => {{ - let fam = Family::default(); - registry.register($name, $help, fam.clone()); - fam - }}; - } - - let topic_subscription_status = register_family!( - "topic_subscription_status", - "Subscription status per known topic" - ); - let topic_peers_count = register_family!( - "topic_peers_counts", - "Number of peers subscribed to each topic" - ); - - let invalid_messages = register_family!( - "invalid_messages_per_topic", - "Number of invalid messages received for each topic" - ); - - let accepted_messages = register_family!( - "accepted_messages_per_topic", - "Number of accepted messages received for each topic" - ); - - let ignored_messages = register_family!( - "ignored_messages_per_topic", - "Number of ignored messages received for each topic" - ); - - let rejected_messages = register_family!( - "rejected_messages_per_topic", - "Number of rejected messages received for each topic" - ); - - let publish_messages_dropped = register_family!( - "publish_messages_dropped_per_topic", - "Number of publish messages dropped per topic" - ); - - let forward_messages_dropped = register_family!( - "forward_messages_dropped_per_topic", - "Number of forward messages dropped per topic" - ); - - let mesh_peer_counts = register_family!( - "mesh_peer_counts", - "Number of peers in each topic in our mesh" - ); - let mesh_peer_inclusion_events = register_family!( - "mesh_peer_inclusion_events", - "Number of times a peer gets added to our mesh for different reasons" - ); - let mesh_peer_churn_events = register_family!( - "mesh_peer_churn_events", - "Number of times a peer gets removed from our mesh for different reasons" - ); - let topic_msg_sent_counts = register_family!( - "topic_msg_sent_counts", - "Number of gossip messages sent to each topic" - ); - let topic_msg_published = register_family!( - "topic_msg_published", - "Number of gossip messages published to each topic" - ); - let topic_msg_sent_bytes = register_family!( - "topic_msg_sent_bytes", - "Bytes from gossip messages sent to each topic" - ); - - let topic_msg_recv_counts_unfiltered = register_family!( - "topic_msg_recv_counts_unfiltered", - "Number of gossip messages received on each topic (without duplicates being filtered)" - ); - - let topic_msg_recv_counts = register_family!( - "topic_msg_recv_counts", - "Number of gossip messages received on each topic (after duplicates have been filtered)" - ); - let topic_msg_recv_bytes = register_family!( - "topic_msg_recv_bytes", - "Bytes received from gossip messages for each topic" - ); - - let hist_builder = HistBuilder { - buckets: score_buckets, - }; - - let score_per_mesh: Family<_, _, HistBuilder> = Family::new_with_constructor(hist_builder); - registry.register( - "score_per_mesh", - "Histogram of scores per mesh topic", - score_per_mesh.clone(), - ); - - let scoring_penalties = register_family!( - "scoring_penalties", - "Counter of types of scoring penalties given to peers" - ); - let peers_per_protocol = register_family!( - "peers_per_protocol", - "Number of connected peers by protocol type" - ); - - let heartbeat_duration = Histogram::new(linear_buckets(0.0, 50.0, 10)); - registry.register( - "heartbeat_duration", - "Histogram of observed heartbeat durations", - heartbeat_duration.clone(), - ); - - let topic_iwant_msgs = register_family!( - "topic_iwant_msgs", - "Number of times we have decided an IWANT is required for this topic" - ); - - let idontwant_msgs = { - let metric = Counter::default(); - registry.register( - "idontwant_msgs", - "The number of times we have received an IDONTWANT control message", - metric.clone(), - ); - metric - }; - - let idontwant_msgs_ids = { - let metric = Counter::default(); - registry.register( - "idontwant_msgs_ids", - "The number of msg_id's we have received in every IDONTWANT control message.", - metric.clone(), - ); - metric - }; - - // IDONTWANT messages sent per topic - let idontwant_messages_sent_per_topic = register_family!( - "idonttwant_messages_sent_per_topic", - "Number of IDONTWANT messages sent per topic" - ); - - // IDONTWANTs which were ignored, and we still received the message per topic - let idontwant_messages_ignored_per_topic = register_family!( - "idontwant_messages_ignored_per_topic", - "IDONTWANT messages that were sent but we received the full message regardless" - ); - - let mesh_duplicates = register_family!( - "mesh_duplicates_per_topic", - "Count of duplicate messages received from mesh peers per topic" - ); - - let iwant_duplicates = register_family!( - "iwant_duplicates_per_topic", - "Count of duplicate messages received from non-mesh peers that we sent iwants for" - ); - - let idontwant_bytes = { - let metric = Counter::default(); - registry.register( - "idontwant_bytes", - "The total bytes we have received an IDONTWANT control messages", - metric.clone(), - ); - metric - }; - - let memcache_misses = { - let metric = Counter::default(); - registry.register( - "memcache_misses", - "Number of times a message is not found in the duplicate cache when validating", - metric.clone(), - ); - metric - }; - - let priority_queue_size = Histogram::new(linear_buckets(0.0, 25.0, 100)); - registry.register( - "priority_queue_size", - "Histogram of observed priority queue sizes", - priority_queue_size.clone(), - ); - - let non_priority_queue_size = Histogram::new(linear_buckets(0.0, 25.0, 100)); - registry.register( - "non_priority_queue_size", - "Histogram of observed non-priority queue sizes", - non_priority_queue_size.clone(), - ); - - Self { - max_topics, - max_never_subscribed_topics, - topic_info: HashMap::default(), - topic_subscription_status, - topic_peers_count, - invalid_messages, - accepted_messages, - ignored_messages, - rejected_messages, - publish_messages_dropped, - forward_messages_dropped, - mesh_peer_counts, - mesh_peer_inclusion_events, - mesh_peer_churn_events, - topic_msg_sent_counts, - topic_msg_sent_bytes, - topic_msg_published, - topic_msg_recv_counts_unfiltered, - topic_msg_recv_counts, - topic_msg_recv_bytes, - score_per_mesh, - scoring_penalties, - peers_per_protocol, - heartbeat_duration, - memcache_misses, - topic_iwant_msgs, - idontwant_msgs, - idontwant_bytes, - idontwant_msgs_ids, - idontwant_messages_sent_per_topic, - idontwant_messages_ignored_per_topic, - mesh_duplicates, - iwant_duplicates, - priority_queue_size, - non_priority_queue_size, - } - } - - fn non_subscription_topics_count(&self) -> usize { - self.topic_info - .values() - .filter(|&ever_subscribed| !ever_subscribed) - .count() - } - - /// Registers a topic if not already known and if the bounds allow it. - fn register_topic(&mut self, topic: &TopicHash) -> Result<(), ()> { - if self.topic_info.contains_key(topic) { - Ok(()) - } else if self.topic_info.len() < self.max_topics - && self.non_subscription_topics_count() < self.max_never_subscribed_topics - { - // This is a topic without an explicit subscription and we register it if we are within - // the configured bounds. - self.topic_info.entry(topic.clone()).or_insert(false); - self.topic_subscription_status.get_or_create(topic).set(0); - Ok(()) - } else { - // We don't know this topic and there is no space left to store it - Err(()) - } - } - - /// Registers a set of topics that we want to store calculate metrics for. - pub(crate) fn register_allowed_topics(&mut self, topics: Vec) { - for topic_hash in topics { - self.topic_info.insert(topic_hash, true); - } - } - - /// Increase the number of peers that are subscribed to this topic. - pub(crate) fn inc_topic_peers(&mut self, topic: &TopicHash) { - if self.register_topic(topic).is_ok() { - self.topic_peers_count.get_or_create(topic).inc(); - } - } - - /// Decrease the number of peers that are subscribed to this topic. - pub(crate) fn dec_topic_peers(&mut self, topic: &TopicHash) { - if self.register_topic(topic).is_ok() { - self.topic_peers_count.get_or_create(topic).dec(); - } - } - - /* Mesh related methods */ - - /// Registers the subscription to a topic if the configured limits allow it. - /// Sets the registered number of peers in the mesh to 0. - pub(crate) fn joined(&mut self, topic: &TopicHash) { - if self.topic_info.contains_key(topic) || self.topic_info.len() < self.max_topics { - self.topic_info.insert(topic.clone(), true); - let was_subscribed = self.topic_subscription_status.get_or_create(topic).set(1); - debug_assert_eq!(was_subscribed, 0); - self.mesh_peer_counts.get_or_create(topic).set(0); - } - } - - /// Registers the unsubscription to a topic if the topic was previously allowed. - /// Sets the registered number of peers in the mesh to 0. - pub(crate) fn left(&mut self, topic: &TopicHash) { - if self.topic_info.contains_key(topic) { - // Depending on the configured topic bounds we could miss a mesh topic. - // So, check first if the topic was previously allowed. - let was_subscribed = self.topic_subscription_status.get_or_create(topic).set(0); - debug_assert_eq!(was_subscribed, 1); - self.mesh_peer_counts.get_or_create(topic).set(0); - } - } - - /// Register the inclusion of peers in our mesh due to some reason. - pub(crate) fn peers_included(&mut self, topic: &TopicHash, reason: Inclusion, count: usize) { - if self.register_topic(topic).is_ok() { - self.mesh_peer_inclusion_events - .get_or_create(&InclusionLabel { - hash: topic.to_string(), - reason, - }) - .inc_by(count as u64); - } - } - - /// Register the removal of peers in our mesh due to some reason. - pub(crate) fn peers_removed(&mut self, topic: &TopicHash, reason: Churn, count: usize) { - if self.register_topic(topic).is_ok() { - self.mesh_peer_churn_events - .get_or_create(&ChurnLabel { - hash: topic.to_string(), - reason, - }) - .inc_by(count as u64); - } - } - - /// Register the current number of peers in our mesh for this topic. - pub(crate) fn set_mesh_peers(&mut self, topic: &TopicHash, count: usize) { - if self.register_topic(topic).is_ok() { - // Due to limits, this topic could have not been allowed, so we check. - self.mesh_peer_counts.get_or_create(topic).set(count as i64); - } - } - - /// Register that an invalid message was received on a specific topic. - pub(crate) fn register_invalid_message(&mut self, topic: &TopicHash) { - if self.register_topic(topic).is_ok() { - self.invalid_messages.get_or_create(topic).inc(); - } - } - - /// Register a score penalty. - pub(crate) fn register_score_penalty(&mut self, penalty: Penalty) { - self.scoring_penalties - .get_or_create(&PenaltyLabel { penalty }) - .inc(); - } - - /// Registers that a message was published on a specific topic. - pub(crate) fn register_published_message(&mut self, topic: &TopicHash) { - if self.register_topic(topic).is_ok() { - self.topic_msg_published.get_or_create(topic).inc(); - } - } - - /// Register sending a message over a topic. - pub(crate) fn msg_sent(&mut self, topic: &TopicHash, bytes: usize) { - if self.register_topic(topic).is_ok() { - self.topic_msg_sent_counts.get_or_create(topic).inc(); - self.topic_msg_sent_bytes - .get_or_create(topic) - .inc_by(bytes as u64); - } - } - - /// Register sending a message over a topic. - pub(crate) fn publish_msg_dropped(&mut self, topic: &TopicHash) { - if self.register_topic(topic).is_ok() { - self.publish_messages_dropped.get_or_create(topic).inc(); - } - } - - /// Register dropping a message over a topic. - pub(crate) fn forward_msg_dropped(&mut self, topic: &TopicHash) { - if self.register_topic(topic).is_ok() { - self.forward_messages_dropped.get_or_create(topic).inc(); - } - } - - /// Register that a message was received (and was not a duplicate). - pub(crate) fn msg_recvd(&mut self, topic: &TopicHash) { - if self.register_topic(topic).is_ok() { - self.topic_msg_recv_counts.get_or_create(topic).inc(); - } - } - - /// Register that a message was received (could have been a duplicate). - pub(crate) fn msg_recvd_unfiltered(&mut self, topic: &TopicHash, bytes: usize) { - if self.register_topic(topic).is_ok() { - self.topic_msg_recv_counts_unfiltered - .get_or_create(topic) - .inc(); - self.topic_msg_recv_bytes - .get_or_create(topic) - .inc_by(bytes as u64); - } - } - - /// Register a duplicate message received from a mesh peer. - pub(crate) fn mesh_duplicates(&mut self, topic: &TopicHash) { - if self.register_topic(topic).is_ok() { - self.mesh_duplicates.get_or_create(topic).inc(); - } - } - - /// Register a duplicate message received from a non-mesh peer on an iwant request. - pub(crate) fn iwant_duplicates(&mut self, topic: &TopicHash) { - if self.register_topic(topic).is_ok() { - self.iwant_duplicates.get_or_create(topic).inc(); - } - } - - pub(crate) fn register_msg_validation( - &mut self, - topic: &TopicHash, - validation: &MessageAcceptance, - ) { - if self.register_topic(topic).is_ok() { - match validation { - MessageAcceptance::Accept => self.accepted_messages.get_or_create(topic).inc(), - MessageAcceptance::Ignore => self.ignored_messages.get_or_create(topic).inc(), - MessageAcceptance::Reject => self.rejected_messages.get_or_create(topic).inc(), - }; - } - } - - /// Register a memcache miss. - pub(crate) fn memcache_miss(&mut self) { - self.memcache_misses.inc(); - } - - /// Register sending an IWANT msg for this topic. - pub(crate) fn register_iwant(&mut self, topic: &TopicHash) { - if self.register_topic(topic).is_ok() { - self.topic_iwant_msgs.get_or_create(topic).inc(); - } - } - - /// Register receiving the total bytes of an IDONTWANT control message. - pub(crate) fn register_idontwant_bytes(&mut self, bytes: usize) { - self.idontwant_bytes.inc_by(bytes as u64); - } - - /// Register receiving an IDONTWANT control message for a given topic. - pub(crate) fn register_idontwant_messages_sent_per_topic(&mut self, topic: &TopicHash) { - self.idontwant_messages_sent_per_topic - .get_or_create(topic) - .inc(); - } - - /// Register receiving a message for an already sent IDONTWANT. - pub(crate) fn register_idontwant_messages_ignored_per_topic(&mut self, topic: &TopicHash) { - self.idontwant_messages_ignored_per_topic - .get_or_create(topic) - .inc(); - } - - /// Register receiving an IDONTWANT msg for this topic. - pub(crate) fn register_idontwant(&mut self, msgs: usize) { - self.idontwant_msgs.inc(); - self.idontwant_msgs_ids.inc_by(msgs as u64); - } - - /// Observes a heartbeat duration. - pub(crate) fn observe_heartbeat_duration(&mut self, millis: u64) { - self.heartbeat_duration.observe(millis as f64); - } - - /// Observes a priority queue size. - pub(crate) fn observe_priority_queue_size(&mut self, len: usize) { - self.priority_queue_size.observe(len as f64); - } - - /// Observes a non-priority queue size. - pub(crate) fn observe_non_priority_queue_size(&mut self, len: usize) { - self.non_priority_queue_size.observe(len as f64); - } - - /// Observe a score of a mesh peer. - pub(crate) fn observe_mesh_peers_score(&mut self, topic: &TopicHash, score: f64) { - if self.register_topic(topic).is_ok() { - self.score_per_mesh.get_or_create(topic).observe(score); - } - } - - /// Register a new peers connection based on its protocol. - pub(crate) fn peer_protocol_connected(&mut self, kind: PeerKind) { - self.peers_per_protocol - .get_or_create(&ProtocolLabel { protocol: kind }) - .inc(); - } - - /// Removes a peer from the counter based on its protocol when it disconnects. - pub(crate) fn peer_protocol_disconnected(&mut self, kind: PeerKind) { - let metric = self - .peers_per_protocol - .get_or_create(&ProtocolLabel { protocol: kind }); - if metric.get() != 0 { - // decrement the counter - metric.set(metric.get() - 1); - } - } -} - -/// Reasons why a peer was included in the mesh. -#[derive(PartialEq, Eq, Hash, EncodeLabelValue, Clone, Debug)] -pub(crate) enum Inclusion { - /// Peer was a fanaout peer. - Fanout, - /// Included from random selection. - Random, - /// Peer subscribed. - Subscribed, - /// Peer was included to fill the outbound quota. - Outbound, -} - -/// Reasons why a peer was removed from the mesh. -#[derive(PartialEq, Eq, Hash, EncodeLabelValue, Clone, Debug)] -pub(crate) enum Churn { - /// Peer disconnected. - Dc, - /// Peer had a bad score. - BadScore, - /// Peer sent a PRUNE. - Prune, - /// Peer unsubscribed. - Unsub, - /// Too many peers. - Excess, -} - -/// Kinds of reasons a peer's score has been penalized -#[derive(PartialEq, Eq, Hash, EncodeLabelValue, Clone, Debug)] -pub(crate) enum Penalty { - /// A peer grafted before waiting the back-off time. - GraftBackoff, - /// A Peer did not respond to an IWANT request in time. - BrokenPromise, - /// A Peer did not send enough messages as expected. - MessageDeficit, - /// Too many peers under one IP address. - IPColocation, -} - -/// Label for the mesh inclusion event metrics. -#[derive(PartialEq, Eq, Hash, EncodeLabelSet, Clone, Debug)] -struct InclusionLabel { - hash: String, - reason: Inclusion, -} - -/// Label for the mesh churn event metrics. -#[derive(PartialEq, Eq, Hash, EncodeLabelSet, Clone, Debug)] -struct ChurnLabel { - hash: String, - reason: Churn, -} - -/// Label for the kinds of protocols peers can connect as. -#[derive(PartialEq, Eq, Hash, EncodeLabelSet, Clone, Debug)] -struct ProtocolLabel { - protocol: PeerKind, -} - -/// Label for the kinds of scoring penalties that can occur -#[derive(PartialEq, Eq, Hash, EncodeLabelSet, Clone, Debug)] -struct PenaltyLabel { - penalty: Penalty, -} - -#[derive(Clone)] -struct HistBuilder { - buckets: Vec, -} - -impl MetricConstructor for HistBuilder { - fn new_metric(&self) -> Histogram { - Histogram::new(self.buckets.clone().into_iter()) - } -} diff --git a/beacon_node/lighthouse_network/gossipsub/src/mod.rs b/beacon_node/lighthouse_network/gossipsub/src/mod.rs deleted file mode 100644 index 8ccdc32cdd4..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/mod.rs +++ /dev/null @@ -1,111 +0,0 @@ -//! Implementation of the [Gossipsub](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/README.md) protocol. -//! -//! Gossipsub is a P2P pubsub (publish/subscription) routing layer designed to extend upon -//! floodsub and meshsub routing protocols. -//! -//! # Overview -//! -//! *Note: The gossipsub protocol specifications -//! () provide an outline for the -//! routing protocol. They should be consulted for further detail.* -//! -//! Gossipsub is a blend of meshsub for data and randomsub for mesh metadata. It provides bounded -//! degree and amplification factor with the meshsub construction and augments it using gossip -//! propagation of metadata with the randomsub technique. -//! -//! The router maintains an overlay mesh network of peers on which to efficiently send messages and -//! metadata. Peers use control messages to broadcast and request known messages and -//! subscribe/unsubscribe from topics in the mesh network. -//! -//! # Important Discrepancies -//! -//! This section outlines the current implementation's potential discrepancies from that of other -//! implementations, due to undefined elements in the current specification. -//! -//! - **Topics** - In gossipsub, topics configurable by the `hash_topics` configuration parameter. -//! Topics are of type [`TopicHash`]. The current go implementation uses raw utf-8 strings, and this -//! is default configuration in rust-libp2p. Topics can be hashed (SHA256 hashed then base64 -//! encoded) by setting the `hash_topics` configuration parameter to true. -//! -//! - **Sequence Numbers** - A message on the gossipsub network is identified by the source -//! [`PeerId`](libp2p_identity::PeerId) and a nonce (sequence number) of the message. The sequence numbers in -//! this implementation are sent as raw bytes across the wire. They are 64-bit big-endian unsigned -//! integers. When messages are signed, they are monotonically increasing integers starting from a -//! random value and wrapping around u64::MAX. When messages are unsigned, they are chosen at random. -//! NOTE: These numbers are sequential in the current go implementation. -//! -//! # Peer Discovery -//! -//! Gossipsub does not provide peer discovery by itself. Peer discovery is the process by which -//! peers in a p2p network exchange information about each other among other reasons to become resistant -//! against the failure or replacement of the -//! [boot nodes](https://docs.libp2p.io/reference/glossary/#boot-node) of the network. -//! -//! Peer -//! discovery can e.g. be implemented with the help of the [Kademlia](https://github.com/libp2p/specs/blob/master/kad-dht/README.md) protocol -//! in combination with the [Identify](https://github.com/libp2p/specs/tree/master/identify) protocol. See the -//! Kademlia implementation documentation for more information. -//! -//! # Using Gossipsub -//! -//! ## Gossipsub Config -//! -//! The [`Config`] struct specifies various network performance/tuning configuration -//! parameters. Specifically it specifies: -//! -//! [`Config`]: struct.Config.html -//! -//! This struct implements the [`Default`] trait and can be initialised via -//! [`Config::default()`]. -//! -//! -//! ## Behaviour -//! -//! The [`Behaviour`] struct implements the [`libp2p_swarm::NetworkBehaviour`] trait allowing it to -//! act as the routing behaviour in a [`libp2p_swarm::Swarm`]. This struct requires an instance of -//! [`PeerId`](libp2p_identity::PeerId) and [`Config`]. -//! -//! [`Behaviour`]: struct.Behaviour.html - -//! ## Example -//! -//! For an example on how to use gossipsub, see the [chat-example](https://github.com/libp2p/rust-libp2p/tree/master/examples/chat). - -#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] - -mod backoff; -mod behaviour; -mod config; -mod error; -mod gossip_promises; -mod handler; -mod mcache; -mod metrics; -mod peer_score; -mod protocol; -mod rpc_proto; -mod subscription_filter; -mod time_cache; -mod topic; -mod transform; -mod types; - -pub use self::behaviour::{Behaviour, Event, MessageAuthenticity}; -pub use self::config::{Config, ConfigBuilder, ValidationMode, Version}; -pub use self::error::{ConfigBuilderError, PublishError, SubscriptionError, ValidationError}; -pub use self::metrics::Config as MetricsConfig; -pub use self::peer_score::{ - score_parameter_decay, score_parameter_decay_with_base, PeerScoreParams, PeerScoreThresholds, - TopicScoreParams, -}; -pub use self::subscription_filter::{ - AllowAllSubscriptionFilter, CallbackSubscriptionFilter, CombinedSubscriptionFilters, - MaxCountSubscriptionFilter, RegexSubscriptionFilter, TopicSubscriptionFilter, - WhitelistSubscriptionFilter, -}; -pub use self::topic::{Hasher, Topic, TopicHash}; -pub use self::transform::{DataTransform, IdentityTransform}; -pub use self::types::{Message, MessageAcceptance, MessageId, RawMessage}; -pub type IdentTopic = Topic; -pub type Sha256Topic = Topic; -pub use self::types::FailedMessages; diff --git a/beacon_node/lighthouse_network/gossipsub/src/peer_score.rs b/beacon_node/lighthouse_network/gossipsub/src/peer_score.rs deleted file mode 100644 index ec6fe7bdb6e..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/peer_score.rs +++ /dev/null @@ -1,937 +0,0 @@ -// Copyright 2020 Sigma Prime Pty Ltd. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -//! -//! Manages and stores the Scoring logic of a particular peer on the gossipsub behaviour. - -use super::metrics::{Metrics, Penalty}; -use super::time_cache::TimeCache; -use super::{MessageId, TopicHash}; -use libp2p::identity::PeerId; -use std::collections::{hash_map, HashMap, HashSet}; -use std::net::IpAddr; -use std::time::Duration; -use web_time::Instant; - -mod params; -use super::ValidationError; -pub use params::{ - score_parameter_decay, score_parameter_decay_with_base, PeerScoreParams, PeerScoreThresholds, - TopicScoreParams, -}; - -#[cfg(test)] -mod tests; - -/// The number of seconds delivery messages are stored in the cache. -const TIME_CACHE_DURATION: u64 = 120; - -pub(crate) struct PeerScore { - pub(crate) params: PeerScoreParams, - /// The score parameters. - peer_stats: HashMap, - /// Tracking peers per IP. - peer_ips: HashMap>, - /// Message delivery tracking. This is a time-cache of [`DeliveryRecord`]s. - deliveries: TimeCache, - /// callback for monitoring message delivery times - message_delivery_time_callback: Option, -} - -/// General statistics for a given gossipsub peer. -struct PeerStats { - /// Connection status of the peer. - status: ConnectionStatus, - /// Stats per topic. - topics: HashMap, - /// IP tracking for individual peers. - known_ips: HashSet, - /// Behaviour penalty that is applied to the peer, assigned by the behaviour. - behaviour_penalty: f64, - /// Application specific score. Can be manipulated by calling PeerScore::set_application_score - application_score: f64, - /// Scoring based on how whether this peer consumes messages fast enough or not. - slow_peer_penalty: f64, -} - -enum ConnectionStatus { - /// The peer is connected. - Connected, - /// The peer is disconnected - Disconnected { - /// Expiration time of the score state for disconnected peers. - expire: Instant, - }, -} - -impl Default for PeerStats { - fn default() -> Self { - PeerStats { - status: ConnectionStatus::Connected, - topics: HashMap::new(), - known_ips: HashSet::new(), - behaviour_penalty: 0f64, - application_score: 0f64, - slow_peer_penalty: 0f64, - } - } -} - -impl PeerStats { - /// Returns a mutable reference to topic stats if they exist, otherwise if the supplied parameters score the - /// topic, inserts the default stats and returns a reference to those. If neither apply, returns None. - pub(crate) fn stats_or_default_mut( - &mut self, - topic_hash: TopicHash, - params: &PeerScoreParams, - ) -> Option<&mut TopicStats> { - if params.topics.contains_key(&topic_hash) { - Some(self.topics.entry(topic_hash).or_default()) - } else { - self.topics.get_mut(&topic_hash) - } - } -} - -/// Stats assigned to peer for each topic. -struct TopicStats { - mesh_status: MeshStatus, - /// Number of first message deliveries. - first_message_deliveries: f64, - /// True if the peer has been in the mesh for enough time to activate mesh message deliveries. - mesh_message_deliveries_active: bool, - /// Number of message deliveries from the mesh. - mesh_message_deliveries: f64, - /// Mesh rate failure penalty. - mesh_failure_penalty: f64, - /// Invalid message counter. - invalid_message_deliveries: f64, -} - -impl TopicStats { - /// Returns true if the peer is in the `mesh`. - pub(crate) fn in_mesh(&self) -> bool { - matches!(self.mesh_status, MeshStatus::Active { .. }) - } -} - -/// Status defining a peer's inclusion in the mesh and associated parameters. -enum MeshStatus { - Active { - /// The time the peer was last GRAFTed; - graft_time: Instant, - /// The time the peer has been in the mesh. - mesh_time: Duration, - }, - InActive, -} - -impl MeshStatus { - /// Initialises a new [`MeshStatus::Active`] mesh status. - pub(crate) fn new_active() -> Self { - MeshStatus::Active { - graft_time: Instant::now(), - mesh_time: Duration::from_secs(0), - } - } -} - -impl Default for TopicStats { - fn default() -> Self { - TopicStats { - mesh_status: MeshStatus::InActive, - first_message_deliveries: Default::default(), - mesh_message_deliveries_active: Default::default(), - mesh_message_deliveries: Default::default(), - mesh_failure_penalty: Default::default(), - invalid_message_deliveries: Default::default(), - } - } -} - -#[derive(PartialEq, Debug)] -struct DeliveryRecord { - status: DeliveryStatus, - first_seen: Instant, - peers: HashSet, -} - -#[derive(PartialEq, Debug)] -enum DeliveryStatus { - /// Don't know (yet) if the message is valid. - Unknown, - /// The message is valid together with the validated time. - Valid(Instant), - /// The message is invalid. - Invalid, - /// Instructed by the validator to ignore the message. - Ignored, -} - -impl Default for DeliveryRecord { - fn default() -> Self { - DeliveryRecord { - status: DeliveryStatus::Unknown, - first_seen: Instant::now(), - peers: HashSet::new(), - } - } -} - -impl PeerScore { - /// Creates a new [`PeerScore`] using a given set of peer scoring parameters. - #[allow(dead_code)] - pub(crate) fn new(params: PeerScoreParams) -> Self { - Self::new_with_message_delivery_time_callback(params, None) - } - - pub(crate) fn new_with_message_delivery_time_callback( - params: PeerScoreParams, - callback: Option, - ) -> Self { - PeerScore { - params, - peer_stats: HashMap::new(), - peer_ips: HashMap::new(), - deliveries: TimeCache::new(Duration::from_secs(TIME_CACHE_DURATION)), - message_delivery_time_callback: callback, - } - } - - /// Returns the score for a peer - pub(crate) fn score(&self, peer_id: &PeerId) -> f64 { - self.metric_score(peer_id, None) - } - - /// Returns the score for a peer, logging metrics. This is called from the heartbeat and - /// increments the metric counts for penalties. - pub(crate) fn metric_score(&self, peer_id: &PeerId, mut metrics: Option<&mut Metrics>) -> f64 { - let Some(peer_stats) = self.peer_stats.get(peer_id) else { - return 0.0; - }; - let mut score = 0.0; - - // topic scores - for (topic, topic_stats) in peer_stats.topics.iter() { - // topic parameters - if let Some(topic_params) = self.params.topics.get(topic) { - // we are tracking the topic - - // the topic score - let mut topic_score = 0.0; - - // P1: time in mesh - if let MeshStatus::Active { mesh_time, .. } = topic_stats.mesh_status { - let p1 = { - let v = mesh_time.as_secs_f64() - / topic_params.time_in_mesh_quantum.as_secs_f64(); - if v < topic_params.time_in_mesh_cap { - v - } else { - topic_params.time_in_mesh_cap - } - }; - topic_score += p1 * topic_params.time_in_mesh_weight; - } - - // P2: first message deliveries - let p2 = { - let v = topic_stats.first_message_deliveries; - if v < topic_params.first_message_deliveries_cap { - v - } else { - topic_params.first_message_deliveries_cap - } - }; - topic_score += p2 * topic_params.first_message_deliveries_weight; - - // P3: mesh message deliveries - if topic_stats.mesh_message_deliveries_active - && topic_stats.mesh_message_deliveries - < topic_params.mesh_message_deliveries_threshold - { - let deficit = topic_params.mesh_message_deliveries_threshold - - topic_stats.mesh_message_deliveries; - let p3 = deficit * deficit; - topic_score += p3 * topic_params.mesh_message_deliveries_weight; - if let Some(metrics) = metrics.as_mut() { - metrics.register_score_penalty(Penalty::MessageDeficit); - } - tracing::debug!( - peer=%peer_id, - %topic, - %deficit, - penalty=%topic_score, - "[Penalty] The peer has a mesh deliveries deficit and will be penalized" - ); - } - - // P3b: - // NOTE: the weight of P3b is negative (validated in TopicScoreParams.validate), so this detracts. - let p3b = topic_stats.mesh_failure_penalty; - topic_score += p3b * topic_params.mesh_failure_penalty_weight; - - // P4: invalid messages - // NOTE: the weight of P4 is negative (validated in TopicScoreParams.validate), so this detracts. - let p4 = - topic_stats.invalid_message_deliveries * topic_stats.invalid_message_deliveries; - topic_score += p4 * topic_params.invalid_message_deliveries_weight; - - // update score, mixing with topic weight - score += topic_score * topic_params.topic_weight; - } - } - - // apply the topic score cap, if any - if self.params.topic_score_cap > 0f64 && score > self.params.topic_score_cap { - score = self.params.topic_score_cap; - } - - // P5: application-specific score - let p5 = peer_stats.application_score; - score += p5 * self.params.app_specific_weight; - - // P6: IP collocation factor - for ip in peer_stats.known_ips.iter() { - if self.params.ip_colocation_factor_whitelist.contains(ip) { - continue; - } - - // P6 has a cliff (ip_colocation_factor_threshold); it's only applied iff - // at least that many peers are connected to us from that source IP - // addr. It is quadratic, and the weight is negative (validated by - // peer_score_params.validate()). - if let Some(peers_in_ip) = self.peer_ips.get(ip).map(|peers| peers.len()) { - if (peers_in_ip as f64) > self.params.ip_colocation_factor_threshold { - let surplus = (peers_in_ip as f64) - self.params.ip_colocation_factor_threshold; - let p6 = surplus * surplus; - if let Some(metrics) = metrics.as_mut() { - metrics.register_score_penalty(Penalty::IPColocation); - } - tracing::debug!( - peer=%peer_id, - surplus_ip=%ip, - surplus=%surplus, - "[Penalty] The peer gets penalized because of too many peers with the same ip" - ); - score += p6 * self.params.ip_colocation_factor_weight; - } - } - } - - // P7: behavioural pattern penalty - if peer_stats.behaviour_penalty > self.params.behaviour_penalty_threshold { - let excess = peer_stats.behaviour_penalty - self.params.behaviour_penalty_threshold; - let p7 = excess * excess; - score += p7 * self.params.behaviour_penalty_weight; - } - - // Slow peer weighting - if peer_stats.slow_peer_penalty > self.params.slow_peer_threshold { - let excess = peer_stats.slow_peer_penalty - self.params.slow_peer_threshold; - score += excess * self.params.slow_peer_weight; - } - - score - } - - pub(crate) fn add_penalty(&mut self, peer_id: &PeerId, count: usize) { - if let Some(peer_stats) = self.peer_stats.get_mut(peer_id) { - tracing::debug!( - peer=%peer_id, - %count, - "[Penalty] Behavioral penalty for peer" - ); - peer_stats.behaviour_penalty += count as f64; - } - } - - fn remove_ips_for_peer( - peer_stats: &PeerStats, - peer_ips: &mut HashMap>, - peer_id: &PeerId, - ) { - for ip in peer_stats.known_ips.iter() { - if let Some(peer_set) = peer_ips.get_mut(ip) { - peer_set.remove(peer_id); - } - } - } - - pub(crate) fn refresh_scores(&mut self) { - let now = Instant::now(); - let params_ref = &self.params; - let peer_ips_ref = &mut self.peer_ips; - self.peer_stats.retain(|peer_id, peer_stats| { - if let ConnectionStatus::Disconnected { expire } = peer_stats.status { - // has the retention period expired? - if now > expire { - // yes, throw it away (but clean up the IP tracking first) - Self::remove_ips_for_peer(peer_stats, peer_ips_ref, peer_id); - // re address this, use retain or entry - return false; - } - - // we don't decay retained scores, as the peer is not active. - // this way the peer cannot reset a negative score by simply disconnecting and reconnecting, - // unless the retention period has elapsed. - // similarly, a well behaved peer does not lose its score by getting disconnected. - return true; - } - - for (topic, topic_stats) in peer_stats.topics.iter_mut() { - // the topic parameters - if let Some(topic_params) = params_ref.topics.get(topic) { - // decay counters - topic_stats.first_message_deliveries *= - topic_params.first_message_deliveries_decay; - if topic_stats.first_message_deliveries < params_ref.decay_to_zero { - topic_stats.first_message_deliveries = 0.0; - } - topic_stats.mesh_message_deliveries *= - topic_params.mesh_message_deliveries_decay; - if topic_stats.mesh_message_deliveries < params_ref.decay_to_zero { - topic_stats.mesh_message_deliveries = 0.0; - } - topic_stats.mesh_failure_penalty *= topic_params.mesh_failure_penalty_decay; - if topic_stats.mesh_failure_penalty < params_ref.decay_to_zero { - topic_stats.mesh_failure_penalty = 0.0; - } - topic_stats.invalid_message_deliveries *= - topic_params.invalid_message_deliveries_decay; - if topic_stats.invalid_message_deliveries < params_ref.decay_to_zero { - topic_stats.invalid_message_deliveries = 0.0; - } - // update mesh time and activate mesh message delivery parameter if need be - if let MeshStatus::Active { - ref mut mesh_time, - ref mut graft_time, - } = topic_stats.mesh_status - { - *mesh_time = now.duration_since(*graft_time); - if *mesh_time > topic_params.mesh_message_deliveries_activation { - topic_stats.mesh_message_deliveries_active = true; - } - } - } - } - - // decay P7 counter - peer_stats.behaviour_penalty *= params_ref.behaviour_penalty_decay; - if peer_stats.behaviour_penalty < params_ref.decay_to_zero { - peer_stats.behaviour_penalty = 0.0; - } - - // decay slow peer score - peer_stats.slow_peer_penalty *= params_ref.slow_peer_decay; - if peer_stats.slow_peer_penalty < params_ref.decay_to_zero { - peer_stats.slow_peer_penalty = 0.0; - } - - true - }); - } - - /// Adds a connected peer to [`PeerScore`], initialising with empty ips (ips get added later - /// through add_ip. - pub(crate) fn add_peer(&mut self, peer_id: PeerId) { - let peer_stats = self.peer_stats.entry(peer_id).or_default(); - - // mark the peer as connected - peer_stats.status = ConnectionStatus::Connected; - } - - /// Adds a new ip to a peer, if the peer is not yet known creates a new peer_stats entry for it - pub(crate) fn add_ip(&mut self, peer_id: &PeerId, ip: IpAddr) { - tracing::trace!(peer=%peer_id, %ip, "Add ip for peer"); - let peer_stats = self.peer_stats.entry(*peer_id).or_default(); - - // Mark the peer as connected (currently the default is connected, but we don't want to - // rely on the default). - peer_stats.status = ConnectionStatus::Connected; - - // Insert the ip - peer_stats.known_ips.insert(ip); - self.peer_ips.entry(ip).or_default().insert(*peer_id); - } - - /// Indicate that a peer has been too slow to consume a message. - pub(crate) fn failed_message_slow_peer(&mut self, peer_id: &PeerId) { - if let Some(peer_stats) = self.peer_stats.get_mut(peer_id) { - peer_stats.slow_peer_penalty += 1.0; - tracing::debug!(peer=%peer_id, %peer_stats.slow_peer_penalty, "[Penalty] Expired message penalty."); - } - } - - /// Removes an ip from a peer - pub(crate) fn remove_ip(&mut self, peer_id: &PeerId, ip: &IpAddr) { - if let Some(peer_stats) = self.peer_stats.get_mut(peer_id) { - peer_stats.known_ips.remove(ip); - if let Some(peer_ids) = self.peer_ips.get_mut(ip) { - tracing::trace!(peer=%peer_id, %ip, "Remove ip for peer"); - peer_ids.remove(peer_id); - } else { - tracing::trace!( - peer=%peer_id, - %ip, - "No entry in peer_ips for ip which should get removed for peer" - ); - } - } else { - tracing::trace!( - peer=%peer_id, - %ip, - "No peer_stats for peer which should remove the ip" - ); - } - } - - /// Removes a peer from the score table. This retains peer statistics if their score is - /// non-positive. - pub(crate) fn remove_peer(&mut self, peer_id: &PeerId) { - // we only retain non-positive scores of peers - if self.score(peer_id) > 0f64 { - if let hash_map::Entry::Occupied(entry) = self.peer_stats.entry(*peer_id) { - Self::remove_ips_for_peer(entry.get(), &mut self.peer_ips, peer_id); - entry.remove(); - } - return; - } - - // if the peer is retained (including it's score) the `first_message_delivery` counters - // are reset to 0 and mesh delivery penalties applied. - if let Some(peer_stats) = self.peer_stats.get_mut(peer_id) { - for (topic, topic_stats) in peer_stats.topics.iter_mut() { - topic_stats.first_message_deliveries = 0f64; - - if let Some(threshold) = self - .params - .topics - .get(topic) - .map(|param| param.mesh_message_deliveries_threshold) - { - if topic_stats.in_mesh() - && topic_stats.mesh_message_deliveries_active - && topic_stats.mesh_message_deliveries < threshold - { - let deficit = threshold - topic_stats.mesh_message_deliveries; - topic_stats.mesh_failure_penalty += deficit * deficit; - } - } - - topic_stats.mesh_status = MeshStatus::InActive; - topic_stats.mesh_message_deliveries_active = false; - } - - peer_stats.status = ConnectionStatus::Disconnected { - expire: Instant::now() + self.params.retain_score, - }; - } - } - - /// Handles scoring functionality as a peer GRAFTs to a topic. - pub(crate) fn graft(&mut self, peer_id: &PeerId, topic: impl Into) { - let topic = topic.into(); - if let Some(peer_stats) = self.peer_stats.get_mut(peer_id) { - // if we are scoring the topic, update the mesh status. - if let Some(topic_stats) = peer_stats.stats_or_default_mut(topic, &self.params) { - topic_stats.mesh_status = MeshStatus::new_active(); - topic_stats.mesh_message_deliveries_active = false; - } - } - } - - /// Handles scoring functionality as a peer PRUNEs from a topic. - pub(crate) fn prune(&mut self, peer_id: &PeerId, topic: TopicHash) { - if let Some(peer_stats) = self.peer_stats.get_mut(peer_id) { - // if we are scoring the topic, update the mesh status. - if let Some(topic_stats) = peer_stats.stats_or_default_mut(topic.clone(), &self.params) - { - // sticky mesh delivery rate failure penalty - let threshold = self - .params - .topics - .get(&topic) - .expect("Topic must exist in order for there to be topic stats") - .mesh_message_deliveries_threshold; - if topic_stats.mesh_message_deliveries_active - && topic_stats.mesh_message_deliveries < threshold - { - let deficit = threshold - topic_stats.mesh_message_deliveries; - topic_stats.mesh_failure_penalty += deficit * deficit; - } - topic_stats.mesh_message_deliveries_active = false; - topic_stats.mesh_status = MeshStatus::InActive; - } - } - } - - pub(crate) fn validate_message( - &mut self, - from: &PeerId, - msg_id: &MessageId, - topic_hash: &TopicHash, - ) { - // adds an empty record with the message id - self.deliveries.entry(msg_id.clone()).or_default(); - - if let Some(callback) = self.message_delivery_time_callback { - if self - .peer_stats - .get(from) - .and_then(|s| s.topics.get(topic_hash)) - .map(|ts| ts.in_mesh()) - .unwrap_or(false) - { - callback(from, topic_hash, 0.0); - } - } - } - - pub(crate) fn deliver_message( - &mut self, - from: &PeerId, - msg_id: &MessageId, - topic_hash: &TopicHash, - ) { - self.mark_first_message_delivery(from, topic_hash); - - let record = self.deliveries.entry(msg_id.clone()).or_default(); - - // this should be the first delivery trace - if record.status != DeliveryStatus::Unknown { - tracing::warn!( - peer=%from, - status=?record.status, - first_seen=?record.first_seen.elapsed().as_secs(), - "Unexpected delivery trace" - ); - return; - } - - // mark the message as valid and reward mesh peers that have already forwarded it to us - record.status = DeliveryStatus::Valid(Instant::now()); - for peer in record.peers.iter().cloned().collect::>() { - // this check is to make sure a peer can't send us a message twice and get a double - // count if it is a first delivery - if &peer != from { - self.mark_duplicate_message_delivery(&peer, topic_hash, None); - } - } - } - - /// Similar to `reject_message` except does not require the message id or reason for an invalid message. - pub(crate) fn reject_invalid_message(&mut self, from: &PeerId, topic_hash: &TopicHash) { - tracing::debug!( - peer=%from, - "[Penalty] Message from peer rejected because of ValidationError or SelfOrigin" - ); - - self.mark_invalid_message_delivery(from, topic_hash); - } - - // Reject a message. - pub(crate) fn reject_message( - &mut self, - from: &PeerId, - msg_id: &MessageId, - topic_hash: &TopicHash, - reason: RejectReason, - ) { - match reason { - // these messages are not tracked, but the peer is penalized as they are invalid - RejectReason::ValidationError(_) | RejectReason::SelfOrigin => { - self.reject_invalid_message(from, topic_hash); - return; - } - // we ignore those messages, so do nothing. - RejectReason::BlackListedPeer | RejectReason::BlackListedSource => { - return; - } - _ => {} // the rest are handled after record creation - } - - let peers: Vec<_> = { - let record = self.deliveries.entry(msg_id.clone()).or_default(); - - // Multiple peers can now reject the same message as we track which peers send us the - // message. If we have already updated the status, return. - if record.status != DeliveryStatus::Unknown { - return; - } - - if let RejectReason::ValidationIgnored = reason { - // we were explicitly instructed by the validator to ignore the message but not penalize - // the peer - record.status = DeliveryStatus::Ignored; - record.peers.clear(); - return; - } - - // mark the message as invalid and penalize peers that have already forwarded it. - record.status = DeliveryStatus::Invalid; - // release the delivery time tracking map to free some memory early - record.peers.drain().collect() - }; - - self.mark_invalid_message_delivery(from, topic_hash); - for peer_id in peers.iter() { - self.mark_invalid_message_delivery(peer_id, topic_hash) - } - } - - pub(crate) fn duplicated_message( - &mut self, - from: &PeerId, - msg_id: &MessageId, - topic_hash: &TopicHash, - ) { - let record = self.deliveries.entry(msg_id.clone()).or_default(); - - if record.peers.contains(from) { - // we have already seen this duplicate! - return; - } - - if let Some(callback) = self.message_delivery_time_callback { - let time = if let DeliveryStatus::Valid(validated) = record.status { - validated.elapsed().as_secs_f64() - } else { - 0.0 - }; - if self - .peer_stats - .get(from) - .and_then(|s| s.topics.get(topic_hash)) - .map(|ts| ts.in_mesh()) - .unwrap_or(false) - { - callback(from, topic_hash, time); - } - } - - match record.status { - DeliveryStatus::Unknown => { - // the message is being validated; track the peer delivery and wait for - // the Deliver/Reject notification. - record.peers.insert(*from); - } - DeliveryStatus::Valid(validated) => { - // mark the peer delivery time to only count a duplicate delivery once. - record.peers.insert(*from); - self.mark_duplicate_message_delivery(from, topic_hash, Some(validated)); - } - DeliveryStatus::Invalid => { - // we no longer track delivery time - self.mark_invalid_message_delivery(from, topic_hash); - } - DeliveryStatus::Ignored => { - // the message was ignored; do nothing (we don't know if it was valid) - } - } - } - - /// Sets the application specific score for a peer. Returns true if the peer is the peer is - /// connected or if the score of the peer is not yet expired and false otherwise. - pub(crate) fn set_application_score(&mut self, peer_id: &PeerId, new_score: f64) -> bool { - if let Some(peer_stats) = self.peer_stats.get_mut(peer_id) { - peer_stats.application_score = new_score; - true - } else { - false - } - } - - /// Sets scoring parameters for a topic. - pub(crate) fn set_topic_params(&mut self, topic_hash: TopicHash, params: TopicScoreParams) { - use hash_map::Entry::*; - match self.params.topics.entry(topic_hash.clone()) { - Occupied(mut entry) => { - let first_message_deliveries_cap = params.first_message_deliveries_cap; - let mesh_message_deliveries_cap = params.mesh_message_deliveries_cap; - let old_params = entry.insert(params); - - if old_params.first_message_deliveries_cap > first_message_deliveries_cap { - for stats in &mut self.peer_stats.values_mut() { - if let Some(tstats) = stats.topics.get_mut(&topic_hash) { - if tstats.first_message_deliveries > first_message_deliveries_cap { - tstats.first_message_deliveries = first_message_deliveries_cap; - } - } - } - } - - if old_params.mesh_message_deliveries_cap > mesh_message_deliveries_cap { - for stats in self.peer_stats.values_mut() { - if let Some(tstats) = stats.topics.get_mut(&topic_hash) { - if tstats.mesh_message_deliveries > mesh_message_deliveries_cap { - tstats.mesh_message_deliveries = mesh_message_deliveries_cap; - } - } - } - } - } - Vacant(entry) => { - entry.insert(params); - } - } - } - - /// Returns a scoring parameters for a topic if existent. - pub(crate) fn get_topic_params(&self, topic_hash: &TopicHash) -> Option<&TopicScoreParams> { - self.params.topics.get(topic_hash) - } - - /// Increments the "invalid message deliveries" counter for all scored topics the message - /// is published in. - fn mark_invalid_message_delivery(&mut self, peer_id: &PeerId, topic_hash: &TopicHash) { - if let Some(peer_stats) = self.peer_stats.get_mut(peer_id) { - if let Some(topic_stats) = - peer_stats.stats_or_default_mut(topic_hash.clone(), &self.params) - { - tracing::debug!( - peer=%peer_id, - topic=%topic_hash, - "[Penalty] Peer delivered an invalid message in topic and gets penalized \ - for it", - ); - topic_stats.invalid_message_deliveries += 1f64; - } - } - } - - /// Increments the "first message deliveries" counter for all scored topics the message is - /// published in, as well as the "mesh message deliveries" counter, if the peer is in the - /// mesh for the topic. - fn mark_first_message_delivery(&mut self, peer_id: &PeerId, topic_hash: &TopicHash) { - if let Some(peer_stats) = self.peer_stats.get_mut(peer_id) { - if let Some(topic_stats) = - peer_stats.stats_or_default_mut(topic_hash.clone(), &self.params) - { - let cap = self - .params - .topics - .get(topic_hash) - .expect("Topic must exist if there are known topic_stats") - .first_message_deliveries_cap; - topic_stats.first_message_deliveries = - if topic_stats.first_message_deliveries + 1f64 > cap { - cap - } else { - topic_stats.first_message_deliveries + 1f64 - }; - - if let MeshStatus::Active { .. } = topic_stats.mesh_status { - let cap = self - .params - .topics - .get(topic_hash) - .expect("Topic must exist if there are known topic_stats") - .mesh_message_deliveries_cap; - - topic_stats.mesh_message_deliveries = - if topic_stats.mesh_message_deliveries + 1f64 > cap { - cap - } else { - topic_stats.mesh_message_deliveries + 1f64 - }; - } - } - } - } - - /// Increments the "mesh message deliveries" counter for messages we've seen before, as long the - /// message was received within the P3 window. - fn mark_duplicate_message_delivery( - &mut self, - peer_id: &PeerId, - topic_hash: &TopicHash, - validated_time: Option, - ) { - if let Some(peer_stats) = self.peer_stats.get_mut(peer_id) { - let now = if validated_time.is_some() { - Some(Instant::now()) - } else { - None - }; - if let Some(topic_stats) = - peer_stats.stats_or_default_mut(topic_hash.clone(), &self.params) - { - if let MeshStatus::Active { .. } = topic_stats.mesh_status { - let topic_params = self - .params - .topics - .get(topic_hash) - .expect("Topic must exist if there are known topic_stats"); - - // check against the mesh delivery window -- if the validated time is passed as 0, then - // the message was received before we finished validation and thus falls within the mesh - // delivery window. - let mut falls_in_mesh_deliver_window = true; - if let Some(validated_time) = validated_time { - if let Some(now) = &now { - //should always be true - let window_time = validated_time - .checked_add(topic_params.mesh_message_deliveries_window) - .unwrap_or(*now); - if now > &window_time { - falls_in_mesh_deliver_window = false; - } - } - } - - if falls_in_mesh_deliver_window { - let cap = topic_params.mesh_message_deliveries_cap; - topic_stats.mesh_message_deliveries = - if topic_stats.mesh_message_deliveries + 1f64 > cap { - cap - } else { - topic_stats.mesh_message_deliveries + 1f64 - }; - } - } - } - } - } - - pub(crate) fn mesh_message_deliveries(&self, peer: &PeerId, topic: &TopicHash) -> Option { - self.peer_stats - .get(peer) - .and_then(|s| s.topics.get(topic)) - .map(|t| t.mesh_message_deliveries) - } -} - -/// The reason a Gossipsub message has been rejected. -#[derive(Clone, Copy)] -pub(crate) enum RejectReason { - /// The message failed the configured validation during decoding. - ValidationError(ValidationError), - /// The message source is us. - SelfOrigin, - /// The peer that sent the message was blacklisted. - BlackListedPeer, - /// The source (from field) of the message was blacklisted. - BlackListedSource, - /// The validation was ignored. - ValidationIgnored, - /// The validation failed. - ValidationFailed, -} diff --git a/beacon_node/lighthouse_network/gossipsub/src/peer_score/params.rs b/beacon_node/lighthouse_network/gossipsub/src/peer_score/params.rs deleted file mode 100644 index a5ac1b63b51..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/peer_score/params.rs +++ /dev/null @@ -1,404 +0,0 @@ -// Copyright 2020 Sigma Prime Pty Ltd. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -use crate::TopicHash; -use std::collections::{HashMap, HashSet}; -use std::net::IpAddr; -use std::time::Duration; - -/// The default number of seconds for a decay interval. -const DEFAULT_DECAY_INTERVAL: u64 = 1; -/// The default rate to decay to 0. -const DEFAULT_DECAY_TO_ZERO: f64 = 0.1; - -/// Computes the decay factor for a parameter, assuming the `decay_interval` is 1s -/// and that the value decays to zero if it drops below 0.01. -pub fn score_parameter_decay(decay: Duration) -> f64 { - score_parameter_decay_with_base( - decay, - Duration::from_secs(DEFAULT_DECAY_INTERVAL), - DEFAULT_DECAY_TO_ZERO, - ) -} - -/// Computes the decay factor for a parameter using base as the `decay_interval`. -pub fn score_parameter_decay_with_base(decay: Duration, base: Duration, decay_to_zero: f64) -> f64 { - // the decay is linear, so after n ticks the value is factor^n - // so factor^n = decay_to_zero => factor = decay_to_zero^(1/n) - let ticks = decay.as_secs_f64() / base.as_secs_f64(); - decay_to_zero.powf(1f64 / ticks) -} - -#[derive(Debug, Clone)] -pub struct PeerScoreThresholds { - /// The score threshold below which gossip propagation is suppressed; - /// should be negative. - pub gossip_threshold: f64, - - /// The score threshold below which we shouldn't publish when using flood - /// publishing (also applies to fanout peers); should be negative and <= `gossip_threshold`. - pub publish_threshold: f64, - - /// The score threshold below which message processing is suppressed altogether, - /// implementing an effective graylist according to peer score; should be negative and - /// <= `publish_threshold`. - pub graylist_threshold: f64, - - /// The score threshold below which px will be ignored; this should be positive - /// and limited to scores attainable by bootstrappers and other trusted nodes. - pub accept_px_threshold: f64, - - /// The median mesh score threshold before triggering opportunistic - /// grafting; this should have a small positive value. - pub opportunistic_graft_threshold: f64, -} - -impl Default for PeerScoreThresholds { - fn default() -> Self { - PeerScoreThresholds { - gossip_threshold: -10.0, - publish_threshold: -50.0, - graylist_threshold: -80.0, - accept_px_threshold: 10.0, - opportunistic_graft_threshold: 20.0, - } - } -} - -impl PeerScoreThresholds { - pub fn validate(&self) -> Result<(), &'static str> { - if self.gossip_threshold > 0f64 { - return Err("invalid gossip threshold; it must be <= 0"); - } - if self.publish_threshold > 0f64 || self.publish_threshold > self.gossip_threshold { - return Err("Invalid publish threshold; it must be <= 0 and <= gossip threshold"); - } - if self.graylist_threshold > 0f64 || self.graylist_threshold > self.publish_threshold { - return Err("Invalid graylist threshold; it must be <= 0 and <= publish threshold"); - } - if self.accept_px_threshold < 0f64 { - return Err("Invalid accept px threshold; it must be >= 0"); - } - if self.opportunistic_graft_threshold < 0f64 { - return Err("Invalid opportunistic grafting threshold; it must be >= 0"); - } - Ok(()) - } -} - -#[derive(Debug, Clone)] -pub struct PeerScoreParams { - /// Score parameters per topic. - pub topics: HashMap, - - /// Aggregate topic score cap; this limits the total contribution of topics towards a positive - /// score. It must be positive (or 0 for no cap). - pub topic_score_cap: f64, - - /// P5: Application-specific peer scoring - pub app_specific_weight: f64, - - /// P6: IP-colocation factor. - /// The parameter has an associated counter which counts the number of peers with the same IP. - /// If the number of peers in the same IP exceeds `ip_colocation_factor_threshold, then the value - /// is the square of the difference, ie `(peers_in_same_ip - ip_colocation_threshold)^2`. - /// If the number of peers in the same IP is less than the threshold, then the value is 0. - /// The weight of the parameter MUST be negative, unless you want to disable for testing. - /// Note: In order to simulate many IPs in a manageable manner when testing, you can set the weight to 0 - /// thus disabling the IP colocation penalty. - pub ip_colocation_factor_weight: f64, - pub ip_colocation_factor_threshold: f64, - pub ip_colocation_factor_whitelist: HashSet, - - /// P7: behavioural pattern penalties. - /// This parameter has an associated counter which tracks misbehaviour as detected by the - /// router. The router currently applies penalties for the following behaviors: - /// - attempting to re-graft before the prune backoff time has elapsed. - /// - not following up in IWANT requests for messages advertised with IHAVE. - /// - /// The value of the parameter is the square of the counter over the threshold, which decays - /// with BehaviourPenaltyDecay. - /// The weight of the parameter MUST be negative (or zero to disable). - pub behaviour_penalty_weight: f64, - pub behaviour_penalty_threshold: f64, - pub behaviour_penalty_decay: f64, - - /// The decay interval for parameter counters. - pub decay_interval: Duration, - - /// Counter value below which it is considered 0. - pub decay_to_zero: f64, - - /// Time to remember counters for a disconnected peer. - pub retain_score: Duration, - - /// Slow peer penalty conditions - pub slow_peer_weight: f64, - pub slow_peer_threshold: f64, - pub slow_peer_decay: f64, -} - -impl Default for PeerScoreParams { - fn default() -> Self { - PeerScoreParams { - topics: HashMap::new(), - topic_score_cap: 3600.0, - app_specific_weight: 10.0, - ip_colocation_factor_weight: -5.0, - ip_colocation_factor_threshold: 10.0, - ip_colocation_factor_whitelist: HashSet::new(), - behaviour_penalty_weight: -10.0, - behaviour_penalty_threshold: 0.0, - behaviour_penalty_decay: 0.2, - decay_interval: Duration::from_secs(DEFAULT_DECAY_INTERVAL), - decay_to_zero: DEFAULT_DECAY_TO_ZERO, - retain_score: Duration::from_secs(3600), - slow_peer_weight: -0.2, - slow_peer_threshold: 0.0, - slow_peer_decay: 0.2, - } - } -} - -/// Peer score parameter validation -impl PeerScoreParams { - pub fn validate(&self) -> Result<(), String> { - for (topic, params) in self.topics.iter() { - if let Err(e) = params.validate() { - return Err(format!("Invalid score parameters for topic {topic}: {e}")); - } - } - - // check that the topic score is 0 or something positive - if self.topic_score_cap < 0f64 { - return Err("Invalid topic score cap; must be positive (or 0 for no cap)".into()); - } - - // check the IP colocation factor - if self.ip_colocation_factor_weight > 0f64 { - return Err( - "Invalid ip_colocation_factor_weight; must be negative (or 0 to disable)".into(), - ); - } - if self.ip_colocation_factor_weight != 0f64 && self.ip_colocation_factor_threshold < 1f64 { - return Err("Invalid ip_colocation_factor_threshold; must be at least 1".into()); - } - - // check the behaviour penalty - if self.behaviour_penalty_weight > 0f64 { - return Err( - "Invalid behaviour_penalty_weight; must be negative (or 0 to disable)".into(), - ); - } - if self.behaviour_penalty_weight != 0f64 - && (self.behaviour_penalty_decay <= 0f64 || self.behaviour_penalty_decay >= 1f64) - { - return Err("invalid behaviour_penalty_decay; must be between 0 and 1".into()); - } - - if self.behaviour_penalty_threshold < 0f64 { - return Err("invalid behaviour_penalty_threshold; must be >= 0".into()); - } - - // check the decay parameters - if self.decay_interval < Duration::from_secs(1) { - return Err("Invalid decay_interval; must be at least 1s".into()); - } - if self.decay_to_zero <= 0f64 || self.decay_to_zero >= 1f64 { - return Err("Invalid decay_to_zero; must be between 0 and 1".into()); - } - - // no need to check the score retention; a value of 0 means that we don't retain scores - Ok(()) - } -} - -#[derive(Debug, Clone)] -pub struct TopicScoreParams { - /// The weight of the topic. - pub topic_weight: f64, - - /// P1: time in the mesh - /// This is the time the peer has been grafted in the mesh. - /// The value of of the parameter is the `time/time_in_mesh_quantum`, capped by `time_in_mesh_cap` - /// The weight of the parameter must be positive (or zero to disable). - pub time_in_mesh_weight: f64, - pub time_in_mesh_quantum: Duration, - pub time_in_mesh_cap: f64, - - /// P2: first message deliveries - /// This is the number of message deliveries in the topic. - /// The value of the parameter is a counter, decaying with `first_message_deliveries_decay`, and capped - /// by `first_message_deliveries_cap`. - /// The weight of the parameter MUST be positive (or zero to disable). - pub first_message_deliveries_weight: f64, - pub first_message_deliveries_decay: f64, - pub first_message_deliveries_cap: f64, - - /// P3: mesh message deliveries - /// This is the number of message deliveries in the mesh, within the - /// `mesh_message_deliveries_window` of message validation; deliveries during validation also - /// count and are retroactively applied when validation succeeds. - /// This window accounts for the minimum time before a hostile mesh peer trying to game the - /// score could replay back a valid message we just sent them. - /// It effectively tracks first and near-first deliveries, ie a message seen from a mesh peer - /// before we have forwarded it to them. - /// The parameter has an associated counter, decaying with `mesh_message_deliveries_decay`. - /// If the counter exceeds the threshold, its value is 0. - /// If the counter is below the `mesh_message_deliveries_threshold`, the value is the square of - /// the deficit, ie (`message_deliveries_threshold - counter)^2` - /// The penalty is only activated after `mesh_message_deliveries_activation` time in the mesh. - /// The weight of the parameter MUST be negative (or zero to disable). - pub mesh_message_deliveries_weight: f64, - pub mesh_message_deliveries_decay: f64, - pub mesh_message_deliveries_cap: f64, - pub mesh_message_deliveries_threshold: f64, - pub mesh_message_deliveries_window: Duration, - pub mesh_message_deliveries_activation: Duration, - - /// P3b: sticky mesh propagation failures - /// This is a sticky penalty that applies when a peer gets pruned from the mesh with an active - /// mesh message delivery penalty. - /// The weight of the parameter MUST be negative (or zero to disable) - pub mesh_failure_penalty_weight: f64, - pub mesh_failure_penalty_decay: f64, - - /// P4: invalid messages - /// This is the number of invalid messages in the topic. - /// The value of the parameter is the square of the counter, decaying with - /// `invalid_message_deliveries_decay`. - /// The weight of the parameter MUST be negative (or zero to disable). - pub invalid_message_deliveries_weight: f64, - pub invalid_message_deliveries_decay: f64, -} - -/// NOTE: The topic score parameters are very network specific. -/// For any production system, these values should be manually set. -impl Default for TopicScoreParams { - fn default() -> Self { - TopicScoreParams { - topic_weight: 0.5, - // P1 - time_in_mesh_weight: 1.0, - time_in_mesh_quantum: Duration::from_millis(1), - time_in_mesh_cap: 3600.0, - // P2 - first_message_deliveries_weight: 1.0, - first_message_deliveries_decay: 0.5, - first_message_deliveries_cap: 2000.0, - // P3 - mesh_message_deliveries_weight: -1.0, - mesh_message_deliveries_decay: 0.5, - mesh_message_deliveries_cap: 100.0, - mesh_message_deliveries_threshold: 20.0, - mesh_message_deliveries_window: Duration::from_millis(10), - mesh_message_deliveries_activation: Duration::from_secs(5), - // P3b - mesh_failure_penalty_weight: -1.0, - mesh_failure_penalty_decay: 0.5, - // P4 - invalid_message_deliveries_weight: -1.0, - invalid_message_deliveries_decay: 0.3, - } - } -} - -impl TopicScoreParams { - pub fn validate(&self) -> Result<(), &'static str> { - // make sure we have a sane topic weight - if self.topic_weight < 0f64 { - return Err("invalid topic weight; must be >= 0"); - } - - if self.time_in_mesh_quantum == Duration::from_secs(0) { - return Err("Invalid time_in_mesh_quantum; must be non zero"); - } - if self.time_in_mesh_weight < 0f64 { - return Err("Invalid time_in_mesh_weight; must be positive (or 0 to disable)"); - } - if self.time_in_mesh_weight != 0f64 && self.time_in_mesh_cap <= 0f64 { - return Err("Invalid time_in_mesh_cap must be positive"); - } - - if self.first_message_deliveries_weight < 0f64 { - return Err( - "Invalid first_message_deliveries_weight; must be positive (or 0 to disable)", - ); - } - if self.first_message_deliveries_weight != 0f64 - && (self.first_message_deliveries_decay <= 0f64 - || self.first_message_deliveries_decay >= 1f64) - { - return Err("Invalid first_message_deliveries_decay; must be between 0 and 1"); - } - if self.first_message_deliveries_weight != 0f64 && self.first_message_deliveries_cap <= 0f64 - { - return Err("Invalid first_message_deliveries_cap must be positive"); - } - - if self.mesh_message_deliveries_weight > 0f64 { - return Err( - "Invalid mesh_message_deliveries_weight; must be negative (or 0 to disable)", - ); - } - if self.mesh_message_deliveries_weight != 0f64 - && (self.mesh_message_deliveries_decay <= 0f64 - || self.mesh_message_deliveries_decay >= 1f64) - { - return Err("Invalid mesh_message_deliveries_decay; must be between 0 and 1"); - } - if self.mesh_message_deliveries_weight != 0f64 && self.mesh_message_deliveries_cap <= 0f64 { - return Err("Invalid mesh_message_deliveries_cap must be positive"); - } - if self.mesh_message_deliveries_weight != 0f64 - && self.mesh_message_deliveries_threshold <= 0f64 - { - return Err("Invalid mesh_message_deliveries_threshold; must be positive"); - } - if self.mesh_message_deliveries_weight != 0f64 - && self.mesh_message_deliveries_activation < Duration::from_secs(1) - { - return Err("Invalid mesh_message_deliveries_activation; must be at least 1s"); - } - - // check P3b - if self.mesh_failure_penalty_weight > 0f64 { - return Err("Invalid mesh_failure_penalty_weight; must be negative (or 0 to disable)"); - } - if self.mesh_failure_penalty_weight != 0f64 - && (self.mesh_failure_penalty_decay <= 0f64 || self.mesh_failure_penalty_decay >= 1f64) - { - return Err("Invalid mesh_failure_penalty_decay; must be between 0 and 1"); - } - - // check P4 - if self.invalid_message_deliveries_weight > 0f64 { - return Err( - "Invalid invalid_message_deliveries_weight; must be negative (or 0 to disable)", - ); - } - if self.invalid_message_deliveries_decay <= 0f64 - || self.invalid_message_deliveries_decay >= 1f64 - { - return Err("Invalid invalid_message_deliveries_decay; must be between 0 and 1"); - } - Ok(()) - } -} diff --git a/beacon_node/lighthouse_network/gossipsub/src/peer_score/tests.rs b/beacon_node/lighthouse_network/gossipsub/src/peer_score/tests.rs deleted file mode 100644 index 064e277eed7..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/peer_score/tests.rs +++ /dev/null @@ -1,978 +0,0 @@ -// Copyright 2020 Sigma Prime Pty Ltd. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -/// A collection of unit tests mostly ported from the go implementation. -use super::*; - -use crate::types::RawMessage; -use crate::{IdentTopic as Topic, Message}; - -// estimates a value within variance -fn within_variance(value: f64, expected: f64, variance: f64) -> bool { - if expected >= 0.0 { - return value > expected * (1.0 - variance) && value < expected * (1.0 + variance); - } - value > expected * (1.0 + variance) && value < expected * (1.0 - variance) -} - -// generates a random gossipsub message with sequence number i -fn make_test_message(seq: u64) -> (MessageId, RawMessage) { - let raw_message = RawMessage { - source: Some(PeerId::random()), - data: vec![12, 34, 56], - sequence_number: Some(seq), - topic: Topic::new("test").hash(), - signature: None, - key: None, - validated: true, - }; - - let message = Message { - source: raw_message.source, - data: raw_message.data.clone(), - sequence_number: raw_message.sequence_number, - topic: raw_message.topic.clone(), - }; - - let id = default_message_id()(&message); - (id, raw_message) -} - -fn default_message_id() -> fn(&Message) -> MessageId { - |message| { - // default message id is: source + sequence number - // NOTE: If either the peer_id or source is not provided, we set to 0; - let mut source_string = if let Some(peer_id) = message.source.as_ref() { - peer_id.to_base58() - } else { - PeerId::from_bytes(&[0, 1, 0]) - .expect("Valid peer id") - .to_base58() - }; - source_string.push_str(&message.sequence_number.unwrap_or_default().to_string()); - MessageId::from(source_string) - } -} - -#[test] -fn test_score_time_in_mesh() { - // Create parameters with reasonable default values - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let mut params = PeerScoreParams { - topic_score_cap: 1000.0, - ..Default::default() - }; - - let topic_params = TopicScoreParams { - topic_weight: 0.5, - time_in_mesh_weight: 1.0, - time_in_mesh_quantum: Duration::from_millis(1), - time_in_mesh_cap: 3600.0, - ..Default::default() - }; - - params.topics.insert(topic_hash, topic_params.clone()); - - let peer_id = PeerId::random(); - - let mut peer_score = PeerScore::new(params); - // Peer score should start at 0 - peer_score.add_peer(peer_id); - - let score = peer_score.score(&peer_id); - assert!( - score == 0.0, - "expected score to start at zero. Score found: {score}" - ); - - // The time in mesh depends on how long the peer has been grafted - peer_score.graft(&peer_id, topic); - let elapsed = topic_params.time_in_mesh_quantum * 200; - std::thread::sleep(elapsed); - peer_score.refresh_scores(); - - let score = peer_score.score(&peer_id); - let expected = topic_params.topic_weight - * topic_params.time_in_mesh_weight - * (elapsed.as_millis() / topic_params.time_in_mesh_quantum.as_millis()) as f64; - assert!( - score >= expected, - "The score: {score} should be greater than or equal to: {expected}" - ); -} - -#[test] -fn test_score_time_in_mesh_cap() { - // Create parameters with reasonable default values - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let mut params = PeerScoreParams::default(); - - let topic_params = TopicScoreParams { - topic_weight: 0.5, - time_in_mesh_weight: 1.0, - time_in_mesh_quantum: Duration::from_millis(1), - time_in_mesh_cap: 10.0, - ..Default::default() - }; - - params.topics.insert(topic_hash, topic_params.clone()); - - let peer_id = PeerId::random(); - - let mut peer_score = PeerScore::new(params); - // Peer score should start at 0 - peer_score.add_peer(peer_id); - - let score = peer_score.score(&peer_id); - assert!( - score == 0.0, - "expected score to start at zero. Score found: {score}" - ); - - // The time in mesh depends on how long the peer has been grafted - peer_score.graft(&peer_id, topic); - let elapsed = topic_params.time_in_mesh_quantum * 40; - std::thread::sleep(elapsed); - peer_score.refresh_scores(); - - let score = peer_score.score(&peer_id); - let expected = topic_params.topic_weight - * topic_params.time_in_mesh_weight - * topic_params.time_in_mesh_cap; - let variance = 0.5; - assert!( - within_variance(score, expected, variance), - "The score: {} should be within {} of {}", - score, - score * variance, - expected - ); -} - -#[test] -fn test_score_first_message_deliveries() { - // Create parameters with reasonable default values - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let mut params = PeerScoreParams::default(); - - let topic_params = TopicScoreParams { - topic_weight: 1.0, - first_message_deliveries_weight: 1.0, - first_message_deliveries_decay: 1.0, - first_message_deliveries_cap: 2000.0, - time_in_mesh_weight: 0.0, - ..Default::default() - }; - - params.topics.insert(topic_hash, topic_params.clone()); - - let peer_id = PeerId::random(); - - let mut peer_score = PeerScore::new(params); - // Peer score should start at 0 - peer_score.add_peer(peer_id); - peer_score.graft(&peer_id, topic); - - // deliver a bunch of messages from the peer - let messages = 100; - for seq in 0..messages { - let (id, msg) = make_test_message(seq); - peer_score.validate_message(&peer_id, &id, &msg.topic); - peer_score.deliver_message(&peer_id, &id, &msg.topic); - } - - peer_score.refresh_scores(); - - let score = peer_score.score(&peer_id); - let expected = - topic_params.topic_weight * topic_params.first_message_deliveries_weight * messages as f64; - assert!(score == expected, "The score: {score} should be {expected}"); -} - -#[test] -fn test_score_first_message_deliveries_cap() { - // Create parameters with reasonable default values - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let mut params = PeerScoreParams::default(); - - let topic_params = TopicScoreParams { - topic_weight: 1.0, - first_message_deliveries_weight: 1.0, - first_message_deliveries_decay: 1.0, // test without decay - first_message_deliveries_cap: 50.0, - time_in_mesh_weight: 0.0, - ..Default::default() - }; - - params.topics.insert(topic_hash, topic_params.clone()); - - let peer_id = PeerId::random(); - - let mut peer_score = PeerScore::new(params); - // Peer score should start at 0 - peer_score.add_peer(peer_id); - peer_score.graft(&peer_id, topic); - - // deliver a bunch of messages from the peer - let messages = 100; - for seq in 0..messages { - let (id, msg) = make_test_message(seq); - peer_score.validate_message(&peer_id, &id, &msg.topic); - peer_score.deliver_message(&peer_id, &id, &msg.topic); - } - - peer_score.refresh_scores(); - let score = peer_score.score(&peer_id); - let expected = topic_params.topic_weight - * topic_params.first_message_deliveries_weight - * topic_params.first_message_deliveries_cap; - assert!(score == expected, "The score: {score} should be {expected}"); -} - -#[test] -fn test_score_first_message_deliveries_decay() { - // Create parameters with reasonable default values - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let mut params = PeerScoreParams::default(); - - let topic_params = TopicScoreParams { - topic_weight: 1.0, - first_message_deliveries_weight: 1.0, - first_message_deliveries_decay: 0.9, // decay 10% per decay interval - first_message_deliveries_cap: 2000.0, - time_in_mesh_weight: 0.0, - ..Default::default() - }; - - params.topics.insert(topic_hash, topic_params.clone()); - let peer_id = PeerId::random(); - let mut peer_score = PeerScore::new(params); - peer_score.add_peer(peer_id); - peer_score.graft(&peer_id, topic); - - // deliver a bunch of messages from the peer - let messages = 100; - for seq in 0..messages { - let (id, msg) = make_test_message(seq); - peer_score.validate_message(&peer_id, &id, &msg.topic); - peer_score.deliver_message(&peer_id, &id, &msg.topic); - } - - peer_score.refresh_scores(); - let score = peer_score.score(&peer_id); - let mut expected = topic_params.topic_weight - * topic_params.first_message_deliveries_weight - * topic_params.first_message_deliveries_decay - * messages as f64; - assert!(score == expected, "The score: {score} should be {expected}"); - - // refreshing the scores applies the decay param - let decay_intervals = 10; - for _ in 0..decay_intervals { - peer_score.refresh_scores(); - expected *= topic_params.first_message_deliveries_decay; - } - let score = peer_score.score(&peer_id); - assert!(score == expected, "The score: {score} should be {expected}"); -} - -#[test] -fn test_score_mesh_message_deliveries() { - // Create parameters with reasonable default values - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let mut params = PeerScoreParams::default(); - - let topic_params = TopicScoreParams { - topic_weight: 1.0, - mesh_message_deliveries_weight: -1.0, - mesh_message_deliveries_activation: Duration::from_secs(1), - mesh_message_deliveries_window: Duration::from_millis(10), - mesh_message_deliveries_threshold: 20.0, - mesh_message_deliveries_cap: 100.0, - mesh_message_deliveries_decay: 1.0, - first_message_deliveries_weight: 0.0, - time_in_mesh_weight: 0.0, - mesh_failure_penalty_weight: 0.0, - ..Default::default() - }; - - params.topics.insert(topic_hash, topic_params.clone()); - let mut peer_score = PeerScore::new(params); - - // peer A always delivers the message first. - // peer B delivers next (within the delivery window). - // peer C delivers outside the delivery window. - // we expect peers A and B to have a score of zero, since all other parameter weights are zero. - // Peer C should have a negative score. - let peer_id_a = PeerId::random(); - let peer_id_b = PeerId::random(); - let peer_id_c = PeerId::random(); - - let peers = vec![peer_id_a, peer_id_b, peer_id_c]; - - for peer_id in &peers { - peer_score.add_peer(*peer_id); - peer_score.graft(peer_id, topic.clone()); - } - - // assert that nobody has been penalized yet for not delivering messages before activation time - peer_score.refresh_scores(); - for peer_id in &peers { - let score = peer_score.score(peer_id); - assert!( - score >= 0.0, - "expected no mesh delivery penalty before activation time, got score {score}" - ); - } - - // wait for the activation time to kick in - std::thread::sleep(topic_params.mesh_message_deliveries_activation); - - // deliver a bunch of messages from peer A, with duplicates within the window from peer B, - // and duplicates outside the window from peer C. - let messages = 100; - let mut messages_to_send = Vec::new(); - for seq in 0..messages { - let (id, msg) = make_test_message(seq); - peer_score.validate_message(&peer_id_a, &id, &msg.topic); - peer_score.deliver_message(&peer_id_a, &id, &msg.topic); - - peer_score.duplicated_message(&peer_id_b, &id, &msg.topic); - messages_to_send.push((id, msg)); - } - - std::thread::sleep(topic_params.mesh_message_deliveries_window + Duration::from_millis(20)); - - for (id, msg) in messages_to_send { - peer_score.duplicated_message(&peer_id_c, &id, &msg.topic); - } - - peer_score.refresh_scores(); - let score_a = peer_score.score(&peer_id_a); - let score_b = peer_score.score(&peer_id_b); - let score_c = peer_score.score(&peer_id_c); - - assert!( - score_a >= 0.0, - "expected non-negative score for Peer A, got score {score_a}" - ); - assert!( - score_b >= 0.0, - "expected non-negative score for Peer B, got score {score_b}" - ); - - // the penalty is the difference between the threshold and the actual mesh deliveries, squared. - // since we didn't deliver anything, this is just the value of the threshold - let penalty = topic_params.mesh_message_deliveries_threshold - * topic_params.mesh_message_deliveries_threshold; - let expected = - topic_params.topic_weight * topic_params.mesh_message_deliveries_weight * penalty; - - assert!(score_c == expected, "Score: {score_c}. Expected {expected}"); -} - -#[test] -fn test_score_mesh_message_deliveries_decay() { - // Create parameters with reasonable default values - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let mut params = PeerScoreParams::default(); - - let topic_params = TopicScoreParams { - topic_weight: 1.0, - mesh_message_deliveries_weight: -1.0, - mesh_message_deliveries_activation: Duration::from_secs(0), - mesh_message_deliveries_window: Duration::from_millis(10), - mesh_message_deliveries_threshold: 20.0, - mesh_message_deliveries_cap: 100.0, - mesh_message_deliveries_decay: 0.9, - first_message_deliveries_weight: 0.0, - time_in_mesh_weight: 0.0, - time_in_mesh_quantum: Duration::from_secs(1), - mesh_failure_penalty_weight: 0.0, - ..Default::default() - }; - - params.topics.insert(topic_hash, topic_params.clone()); - let mut peer_score = PeerScore::new(params); - - let peer_id_a = PeerId::random(); - peer_score.add_peer(peer_id_a); - peer_score.graft(&peer_id_a, topic); - - // deliver a bunch of messages from peer A - let messages = 100; - for seq in 0..messages { - let (id, msg) = make_test_message(seq); - peer_score.validate_message(&peer_id_a, &id, &msg.topic); - peer_score.deliver_message(&peer_id_a, &id, &msg.topic); - } - - // we should have a positive score, since we delivered more messages than the threshold - peer_score.refresh_scores(); - - let score_a = peer_score.score(&peer_id_a); - assert!( - score_a >= 0.0, - "expected non-negative score for Peer A, got score {score_a}" - ); - - let mut decayed_delivery_count = (messages as f64) * topic_params.mesh_message_deliveries_decay; - for _ in 0..20 { - peer_score.refresh_scores(); - decayed_delivery_count *= topic_params.mesh_message_deliveries_decay; - } - - let score_a = peer_score.score(&peer_id_a); - // the penalty is the difference between the threshold and the (decayed) mesh deliveries, squared. - let deficit = topic_params.mesh_message_deliveries_threshold - decayed_delivery_count; - let penalty = deficit * deficit; - let expected = - topic_params.topic_weight * topic_params.mesh_message_deliveries_weight * penalty; - - assert_eq!(score_a, expected, "Invalid score"); -} - -#[test] -fn test_score_mesh_failure_penalty() { - // Create parameters with reasonable default values - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let mut params = PeerScoreParams::default(); - - let topic_params = TopicScoreParams { - // the mesh failure penalty is applied when a peer is pruned while their - // mesh deliveries are under the threshold. - // for this test, we set the mesh delivery threshold, but set - // mesh_message_deliveries to zero, so the only affect on the score - // is from the mesh failure penalty - topic_weight: 1.0, - mesh_message_deliveries_weight: 0.0, - mesh_message_deliveries_activation: Duration::from_secs(0), - mesh_message_deliveries_window: Duration::from_millis(10), - mesh_message_deliveries_threshold: 20.0, - mesh_message_deliveries_cap: 100.0, - mesh_message_deliveries_decay: 1.0, - first_message_deliveries_weight: 0.0, - time_in_mesh_weight: 0.0, - time_in_mesh_quantum: Duration::from_secs(1), - mesh_failure_penalty_weight: -1.0, - mesh_failure_penalty_decay: 1.0, - ..Default::default() - }; - - params.topics.insert(topic_hash, topic_params.clone()); - let mut peer_score = PeerScore::new(params); - - let peer_id_a = PeerId::random(); - let peer_id_b = PeerId::random(); - - let peers = vec![peer_id_a, peer_id_b]; - - for peer_id in &peers { - peer_score.add_peer(*peer_id); - peer_score.graft(peer_id, topic.clone()); - } - - // deliver a bunch of messages from peer A - let messages = 100; - for seq in 0..messages { - let (id, msg) = make_test_message(seq); - - peer_score.validate_message(&peer_id_a, &id, &msg.topic); - peer_score.deliver_message(&peer_id_a, &id, &msg.topic); - } - - // peers A and B should both have zero scores, since the failure penalty hasn't been applied yet - peer_score.refresh_scores(); - let score_a = peer_score.score(&peer_id_a); - let score_b = peer_score.score(&peer_id_b); - assert!( - score_a >= 0.0, - "expected non-negative score for Peer A, got score {score_a}" - ); - assert!( - score_b >= 0.0, - "expected non-negative score for Peer B, got score {score_b}" - ); - - // prune peer B to apply the penalty - peer_score.prune(&peer_id_b, topic.hash()); - peer_score.refresh_scores(); - let score_a = peer_score.score(&peer_id_a); - - assert_eq!(score_a, 0.0, "expected Peer A to have a 0"); - - // penalty calculation is the same as for mesh_message_deliveries, but multiplied by - // mesh_failure_penalty_weigh - // instead of mesh_message_deliveries_weight - let penalty = topic_params.mesh_message_deliveries_threshold - * topic_params.mesh_message_deliveries_threshold; - let expected = topic_params.topic_weight * topic_params.mesh_failure_penalty_weight * penalty; - - let score_b = peer_score.score(&peer_id_b); - - assert_eq!(score_b, expected, "Peer B should have expected score",); -} - -#[test] -fn test_score_invalid_message_deliveries() { - // Create parameters with reasonable default values - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let mut params = PeerScoreParams::default(); - - let topic_params = TopicScoreParams { - topic_weight: 1.0, - mesh_message_deliveries_weight: 0.0, - mesh_message_deliveries_activation: Duration::from_secs(1), - mesh_message_deliveries_window: Duration::from_millis(10), - mesh_message_deliveries_threshold: 20.0, - mesh_message_deliveries_cap: 100.0, - mesh_message_deliveries_decay: 1.0, - first_message_deliveries_weight: 0.0, - time_in_mesh_weight: 0.0, - mesh_failure_penalty_weight: 0.0, - invalid_message_deliveries_weight: -1.0, - invalid_message_deliveries_decay: 1.0, - ..Default::default() - }; - - params.topics.insert(topic_hash, topic_params.clone()); - let mut peer_score = PeerScore::new(params); - - let peer_id_a = PeerId::random(); - peer_score.add_peer(peer_id_a); - peer_score.graft(&peer_id_a, topic); - - // reject a bunch of messages from peer A - let messages = 100; - for seq in 0..messages { - let (id, msg) = make_test_message(seq); - peer_score.reject_message(&peer_id_a, &id, &msg.topic, RejectReason::ValidationFailed); - } - - peer_score.refresh_scores(); - let score_a = peer_score.score(&peer_id_a); - - let expected = topic_params.topic_weight - * topic_params.invalid_message_deliveries_weight - * (messages * messages) as f64; - - assert_eq!(score_a, expected, "Peer has unexpected score",); -} - -#[test] -fn test_score_invalid_message_deliveris_decay() { - // Create parameters with reasonable default values - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let mut params = PeerScoreParams::default(); - - let topic_params = TopicScoreParams { - topic_weight: 1.0, - mesh_message_deliveries_weight: 0.0, - mesh_message_deliveries_activation: Duration::from_secs(1), - mesh_message_deliveries_window: Duration::from_millis(10), - mesh_message_deliveries_threshold: 20.0, - mesh_message_deliveries_cap: 100.0, - mesh_message_deliveries_decay: 1.0, - first_message_deliveries_weight: 0.0, - time_in_mesh_weight: 0.0, - mesh_failure_penalty_weight: 0.0, - invalid_message_deliveries_weight: -1.0, - invalid_message_deliveries_decay: 0.9, - ..Default::default() - }; - - params.topics.insert(topic_hash, topic_params.clone()); - let mut peer_score = PeerScore::new(params); - - let peer_id_a = PeerId::random(); - peer_score.add_peer(peer_id_a); - peer_score.graft(&peer_id_a, topic); - - // reject a bunch of messages from peer A - let messages = 100; - for seq in 0..messages { - let (id, msg) = make_test_message(seq); - peer_score.reject_message(&peer_id_a, &id, &msg.topic, RejectReason::ValidationFailed); - } - - peer_score.refresh_scores(); - - let decay = topic_params.invalid_message_deliveries_decay * messages as f64; - - let mut expected = - topic_params.topic_weight * topic_params.invalid_message_deliveries_weight * decay * decay; - - let score_a = peer_score.score(&peer_id_a); - assert_eq!(score_a, expected, "Peer has unexpected score"); - - // refresh scores a few times to apply decay - for _ in 0..10 { - peer_score.refresh_scores(); - expected *= topic_params.invalid_message_deliveries_decay - * topic_params.invalid_message_deliveries_decay; - } - - let score_a = peer_score.score(&peer_id_a); - assert_eq!(score_a, expected, "Peer has unexpected score"); -} - -#[test] -fn test_score_reject_message_deliveries() { - // This tests adds coverage for the dark corners of rejection tracing - - // Create parameters with reasonable default values - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let mut params = PeerScoreParams::default(); - - let topic_params = TopicScoreParams { - topic_weight: 1.0, - mesh_message_deliveries_weight: 0.0, - first_message_deliveries_weight: 0.0, - mesh_failure_penalty_weight: 0.0, - time_in_mesh_weight: 0.0, - time_in_mesh_quantum: Duration::from_secs(1), - invalid_message_deliveries_weight: -1.0, - invalid_message_deliveries_decay: 1.0, - ..Default::default() - }; - - params.topics.insert(topic_hash, topic_params); - let mut peer_score = PeerScore::new(params); - - let peer_id_a = PeerId::random(); - let peer_id_b = PeerId::random(); - - let peers = vec![peer_id_a, peer_id_b]; - - for peer_id in &peers { - peer_score.add_peer(*peer_id); - } - - let (id, msg) = make_test_message(1); - - // these should have no effect in the score - peer_score.reject_message(&peer_id_a, &id, &msg.topic, RejectReason::BlackListedPeer); - peer_score.reject_message(&peer_id_a, &id, &msg.topic, RejectReason::BlackListedSource); - peer_score.reject_message(&peer_id_a, &id, &msg.topic, RejectReason::ValidationIgnored); - - peer_score.refresh_scores(); - let score_a = peer_score.score(&peer_id_a); - let score_b = peer_score.score(&peer_id_b); - - assert_eq!(score_a, 0.0, "Should have no effect on the score"); - assert_eq!(score_b, 0.0, "Should have no effect on the score"); - - // insert a record in the message deliveries - peer_score.validate_message(&peer_id_a, &id, &msg.topic); - - // this should have no effect in the score, and subsequent duplicate messages should have no - // effect either - peer_score.reject_message(&peer_id_a, &id, &msg.topic, RejectReason::ValidationIgnored); - peer_score.duplicated_message(&peer_id_b, &id, &msg.topic); - - peer_score.refresh_scores(); - let score_a = peer_score.score(&peer_id_a); - let score_b = peer_score.score(&peer_id_b); - - assert_eq!(score_a, 0.0, "Should have no effect on the score"); - assert_eq!(score_b, 0.0, "Should have no effect on the score"); - - // now clear the delivery record - peer_score.deliveries.clear(); - - // insert a record in the message deliveries - peer_score.validate_message(&peer_id_a, &id, &msg.topic); - - // this should have no effect in the score, and subsequent duplicate messages should have no - // effect either - peer_score.reject_message(&peer_id_a, &id, &msg.topic, RejectReason::ValidationIgnored); - peer_score.duplicated_message(&peer_id_b, &id, &msg.topic); - - peer_score.refresh_scores(); - let score_a = peer_score.score(&peer_id_a); - let score_b = peer_score.score(&peer_id_b); - - assert_eq!(score_a, 0.0, "Should have no effect on the score"); - assert_eq!(score_b, 0.0, "Should have no effect on the score"); - - // now clear the delivery record - peer_score.deliveries.clear(); - - // insert a new record in the message deliveries - peer_score.validate_message(&peer_id_a, &id, &msg.topic); - - // and reject the message to make sure duplicates are also penalized - peer_score.reject_message(&peer_id_a, &id, &msg.topic, RejectReason::ValidationFailed); - peer_score.duplicated_message(&peer_id_b, &id, &msg.topic); - - peer_score.refresh_scores(); - let score_a = peer_score.score(&peer_id_a); - let score_b = peer_score.score(&peer_id_b); - - assert_eq!(score_a, -1.0, "Score should be effected"); - assert_eq!(score_b, -1.0, "Score should be effected"); - - // now clear the delivery record again - peer_score.deliveries.clear(); - - // insert a new record in the message deliveries - peer_score.validate_message(&peer_id_a, &id, &msg.topic); - - // and reject the message after a duplicate has arrived - peer_score.duplicated_message(&peer_id_b, &id, &msg.topic); - peer_score.reject_message(&peer_id_a, &id, &msg.topic, RejectReason::ValidationFailed); - - peer_score.refresh_scores(); - let score_a = peer_score.score(&peer_id_a); - let score_b = peer_score.score(&peer_id_b); - - assert_eq!(score_a, -4.0, "Score should be effected"); - assert_eq!(score_b, -4.0, "Score should be effected"); -} - -#[test] -fn test_application_score() { - // Create parameters with reasonable default values - let app_specific_weight = 0.5; - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let mut params = PeerScoreParams { - app_specific_weight, - ..Default::default() - }; - - let topic_params = TopicScoreParams { - topic_weight: 1.0, - mesh_message_deliveries_weight: 0.0, - first_message_deliveries_weight: 0.0, - mesh_failure_penalty_weight: 0.0, - time_in_mesh_weight: 0.0, - time_in_mesh_quantum: Duration::from_secs(1), - invalid_message_deliveries_weight: 0.0, - invalid_message_deliveries_decay: 1.0, - ..Default::default() - }; - - params.topics.insert(topic_hash, topic_params); - let mut peer_score = PeerScore::new(params); - - let peer_id_a = PeerId::random(); - peer_score.add_peer(peer_id_a); - peer_score.graft(&peer_id_a, topic); - - let messages = 100; - for i in -100..messages { - let app_score_value = i as f64; - peer_score.set_application_score(&peer_id_a, app_score_value); - peer_score.refresh_scores(); - let score_a = peer_score.score(&peer_id_a); - let expected = (i as f64) * app_specific_weight; - assert_eq!(score_a, expected, "Peer has unexpected score"); - } -} - -#[test] -fn test_score_ip_colocation() { - // Create parameters with reasonable default values - let ip_colocation_factor_weight = -1.0; - let ip_colocation_factor_threshold = 1.0; - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let mut params = PeerScoreParams { - ip_colocation_factor_weight, - ip_colocation_factor_threshold, - ..Default::default() - }; - - let topic_params = TopicScoreParams { - topic_weight: 1.0, - mesh_message_deliveries_weight: 0.0, - first_message_deliveries_weight: 0.0, - mesh_failure_penalty_weight: 0.0, - time_in_mesh_weight: 0.0, - time_in_mesh_quantum: Duration::from_secs(1), - invalid_message_deliveries_weight: 0.0, - ..Default::default() - }; - - params.topics.insert(topic_hash, topic_params); - let mut peer_score = PeerScore::new(params); - - let peer_id_a = PeerId::random(); - let peer_id_b = PeerId::random(); - let peer_id_c = PeerId::random(); - let peer_id_d = PeerId::random(); - - let peers = vec![peer_id_a, peer_id_b, peer_id_c, peer_id_d]; - for peer_id in &peers { - peer_score.add_peer(*peer_id); - peer_score.graft(peer_id, topic.clone()); - } - - // peerA should have no penalty, but B, C, and D should be penalized for sharing an IP - peer_score.add_ip(&peer_id_a, "1.2.3.4".parse().unwrap()); - peer_score.add_ip(&peer_id_b, "2.3.4.5".parse().unwrap()); - peer_score.add_ip(&peer_id_c, "2.3.4.5".parse().unwrap()); - peer_score.add_ip(&peer_id_c, "3.4.5.6".parse().unwrap()); - peer_score.add_ip(&peer_id_d, "2.3.4.5".parse().unwrap()); - - peer_score.refresh_scores(); - let score_a = peer_score.score(&peer_id_a); - let score_b = peer_score.score(&peer_id_b); - let score_c = peer_score.score(&peer_id_c); - let score_d = peer_score.score(&peer_id_d); - - assert_eq!(score_a, 0.0, "Peer A should be unaffected"); - - let n_shared = 3.0; - let ip_surplus = n_shared - ip_colocation_factor_threshold; - let penalty = ip_surplus * ip_surplus; - let expected = ip_colocation_factor_weight * penalty; - - assert_eq!(score_b, expected, "Peer B should have expected score"); - assert_eq!(score_c, expected, "Peer C should have expected score"); - assert_eq!(score_d, expected, "Peer D should have expected score"); -} - -#[test] -fn test_score_behaviour_penality() { - // Create parameters with reasonable default values - let behaviour_penalty_weight = -1.0; - let behaviour_penalty_decay = 0.99; - - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let mut params = PeerScoreParams { - behaviour_penalty_decay, - behaviour_penalty_weight, - ..Default::default() - }; - - let topic_params = TopicScoreParams { - topic_weight: 1.0, - mesh_message_deliveries_weight: 0.0, - first_message_deliveries_weight: 0.0, - mesh_failure_penalty_weight: 0.0, - time_in_mesh_weight: 0.0, - time_in_mesh_quantum: Duration::from_secs(1), - invalid_message_deliveries_weight: 0.0, - ..Default::default() - }; - - params.topics.insert(topic_hash, topic_params); - let mut peer_score = PeerScore::new(params); - - let peer_id_a = PeerId::random(); - - // add a penalty to a non-existent peer. - peer_score.add_penalty(&peer_id_a, 1); - - let score_a = peer_score.score(&peer_id_a); - assert_eq!(score_a, 0.0, "Peer A should be unaffected"); - - // add the peer and test penalties - peer_score.add_peer(peer_id_a); - assert_eq!(score_a, 0.0, "Peer A should be unaffected"); - - peer_score.add_penalty(&peer_id_a, 1); - - let score_a = peer_score.score(&peer_id_a); - assert_eq!(score_a, -1.0, "Peer A should have been penalized"); - - peer_score.add_penalty(&peer_id_a, 1); - let score_a = peer_score.score(&peer_id_a); - assert_eq!(score_a, -4.0, "Peer A should have been penalized"); - - peer_score.refresh_scores(); - - let score_a = peer_score.score(&peer_id_a); - assert_eq!(score_a, -3.9204, "Peer A should have been penalized"); -} - -#[test] -fn test_score_retention() { - // Create parameters with reasonable default values - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let app_specific_weight = 1.0; - let app_score_value = -1000.0; - let retain_score = Duration::from_secs(1); - let mut params = PeerScoreParams { - app_specific_weight, - retain_score, - ..Default::default() - }; - - let topic_params = TopicScoreParams { - topic_weight: 0.0, - mesh_message_deliveries_weight: 0.0, - mesh_message_deliveries_activation: Duration::from_secs(0), - first_message_deliveries_weight: 0.0, - time_in_mesh_weight: 0.0, - ..Default::default() - }; - - params.topics.insert(topic_hash, topic_params); - let mut peer_score = PeerScore::new(params); - - let peer_id_a = PeerId::random(); - peer_score.add_peer(peer_id_a); - peer_score.graft(&peer_id_a, topic); - - peer_score.set_application_score(&peer_id_a, app_score_value); - - // score should equal -1000 (app specific score) - peer_score.refresh_scores(); - let score_a = peer_score.score(&peer_id_a); - assert_eq!( - score_a, app_score_value, - "Score should be the application specific score" - ); - - // disconnect & wait half of RetainScore time. Should still have negative score - peer_score.remove_peer(&peer_id_a); - std::thread::sleep(retain_score / 2); - peer_score.refresh_scores(); - let score_a = peer_score.score(&peer_id_a); - assert_eq!( - score_a, app_score_value, - "Score should be the application specific score" - ); - - // wait remaining time (plus a little slop) and the score should reset to zero - std::thread::sleep(retain_score / 2 + Duration::from_millis(50)); - peer_score.refresh_scores(); - let score_a = peer_score.score(&peer_id_a); - assert_eq!( - score_a, 0.0, - "Score should be the application specific score" - ); -} diff --git a/beacon_node/lighthouse_network/gossipsub/src/protocol.rs b/beacon_node/lighthouse_network/gossipsub/src/protocol.rs deleted file mode 100644 index b72f4ccc9b5..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/protocol.rs +++ /dev/null @@ -1,646 +0,0 @@ -// Copyright 2020 Sigma Prime Pty Ltd. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -use super::config::ValidationMode; -use super::handler::HandlerEvent; -use super::rpc_proto::proto; -use super::topic::TopicHash; -use super::types::{ - ControlAction, Graft, IDontWant, IHave, IWant, MessageId, PeerInfo, PeerKind, Prune, - RawMessage, Rpc, Subscription, SubscriptionAction, -}; -use super::ValidationError; -use asynchronous_codec::{Decoder, Encoder, Framed}; -use byteorder::{BigEndian, ByteOrder}; -use bytes::BytesMut; -use futures::prelude::*; -use libp2p::core::{InboundUpgrade, OutboundUpgrade, UpgradeInfo}; -use libp2p::identity::{PeerId, PublicKey}; -use libp2p::swarm::StreamProtocol; -use quick_protobuf::Writer; -use std::pin::Pin; -use void::Void; - -pub(crate) const SIGNING_PREFIX: &[u8] = b"libp2p-pubsub:"; - -pub(crate) const GOSSIPSUB_1_2_0_PROTOCOL: ProtocolId = ProtocolId { - protocol: StreamProtocol::new("/meshsub/1.2.0"), - kind: PeerKind::Gossipsubv1_2, -}; -pub(crate) const GOSSIPSUB_1_1_0_PROTOCOL: ProtocolId = ProtocolId { - protocol: StreamProtocol::new("/meshsub/1.1.0"), - kind: PeerKind::Gossipsubv1_1, -}; -pub(crate) const GOSSIPSUB_1_0_0_PROTOCOL: ProtocolId = ProtocolId { - protocol: StreamProtocol::new("/meshsub/1.0.0"), - kind: PeerKind::Gossipsub, -}; -pub(crate) const FLOODSUB_PROTOCOL: ProtocolId = ProtocolId { - protocol: StreamProtocol::new("/floodsub/1.0.0"), - kind: PeerKind::Floodsub, -}; - -/// Implementation of [`InboundUpgrade`] and [`OutboundUpgrade`] for the Gossipsub protocol. -#[derive(Debug, Clone)] -pub struct ProtocolConfig { - /// The Gossipsub protocol id to listen on. - pub(crate) protocol_ids: Vec, - /// The maximum transmit size for a packet. - pub(crate) max_transmit_size: usize, - /// Determines the level of validation to be done on incoming messages. - pub(crate) validation_mode: ValidationMode, -} - -impl Default for ProtocolConfig { - fn default() -> Self { - Self { - max_transmit_size: 65536, - validation_mode: ValidationMode::Strict, - protocol_ids: vec![ - GOSSIPSUB_1_2_0_PROTOCOL, - GOSSIPSUB_1_1_0_PROTOCOL, - GOSSIPSUB_1_0_0_PROTOCOL, - ], - } - } -} - -/// The protocol ID -#[derive(Clone, Debug, PartialEq)] -pub struct ProtocolId { - /// The RPC message type/name. - pub protocol: StreamProtocol, - /// The type of protocol we support - pub kind: PeerKind, -} - -impl AsRef for ProtocolId { - fn as_ref(&self) -> &str { - self.protocol.as_ref() - } -} - -impl UpgradeInfo for ProtocolConfig { - type Info = ProtocolId; - type InfoIter = Vec; - - fn protocol_info(&self) -> Self::InfoIter { - self.protocol_ids.clone() - } -} - -impl InboundUpgrade for ProtocolConfig -where - TSocket: AsyncRead + AsyncWrite + Unpin + Send + 'static, -{ - type Output = (Framed, PeerKind); - type Error = Void; - type Future = Pin> + Send>>; - - fn upgrade_inbound(self, socket: TSocket, protocol_id: Self::Info) -> Self::Future { - Box::pin(future::ok(( - Framed::new( - socket, - GossipsubCodec::new(self.max_transmit_size, self.validation_mode), - ), - protocol_id.kind, - ))) - } -} - -impl OutboundUpgrade for ProtocolConfig -where - TSocket: AsyncWrite + AsyncRead + Unpin + Send + 'static, -{ - type Output = (Framed, PeerKind); - type Error = Void; - type Future = Pin> + Send>>; - - fn upgrade_outbound(self, socket: TSocket, protocol_id: Self::Info) -> Self::Future { - Box::pin(future::ok(( - Framed::new( - socket, - GossipsubCodec::new(self.max_transmit_size, self.validation_mode), - ), - protocol_id.kind, - ))) - } -} - -/* Gossip codec for the framing */ - -pub struct GossipsubCodec { - /// Determines the level of validation performed on incoming messages. - validation_mode: ValidationMode, - /// The codec to handle common encoding/decoding of protobuf messages - codec: quick_protobuf_codec::Codec, -} - -impl GossipsubCodec { - pub fn new(max_length: usize, validation_mode: ValidationMode) -> GossipsubCodec { - let codec = quick_protobuf_codec::Codec::new(max_length); - GossipsubCodec { - validation_mode, - codec, - } - } - - /// Verifies a gossipsub message. This returns either a success or failure. All errors - /// are logged, which prevents error handling in the codec and handler. We simply drop invalid - /// messages and log warnings, rather than propagating errors through the codec. - fn verify_signature(message: &proto::Message) -> bool { - use quick_protobuf::MessageWrite; - - let Some(from) = message.from.as_ref() else { - tracing::debug!("Signature verification failed: No source id given"); - return false; - }; - - let Ok(source) = PeerId::from_bytes(from) else { - tracing::debug!("Signature verification failed: Invalid Peer Id"); - return false; - }; - - let Some(signature) = message.signature.as_ref() else { - tracing::debug!("Signature verification failed: No signature provided"); - return false; - }; - - // If there is a key value in the protobuf, use that key otherwise the key must be - // obtained from the inlined source peer_id. - let public_key = match message.key.as_deref().map(PublicKey::try_decode_protobuf) { - Some(Ok(key)) => key, - _ => match PublicKey::try_decode_protobuf(&source.to_bytes()[2..]) { - Ok(v) => v, - Err(_) => { - tracing::warn!("Signature verification failed: No valid public key supplied"); - return false; - } - }, - }; - - // The key must match the peer_id - if source != public_key.to_peer_id() { - tracing::warn!( - "Signature verification failed: Public key doesn't match source peer id" - ); - return false; - } - - // Construct the signature bytes - let mut message_sig = message.clone(); - message_sig.signature = None; - message_sig.key = None; - let mut buf = Vec::with_capacity(message_sig.get_size()); - let mut writer = Writer::new(&mut buf); - message_sig - .write_message(&mut writer) - .expect("Encoding to succeed"); - let mut signature_bytes = SIGNING_PREFIX.to_vec(); - signature_bytes.extend_from_slice(&buf); - public_key.verify(&signature_bytes, signature) - } -} - -impl Encoder for GossipsubCodec { - type Item<'a> = proto::RPC; - type Error = quick_protobuf_codec::Error; - - fn encode(&mut self, item: Self::Item<'_>, dst: &mut BytesMut) -> Result<(), Self::Error> { - self.codec.encode(item, dst) - } -} - -impl Decoder for GossipsubCodec { - type Item = HandlerEvent; - type Error = quick_protobuf_codec::Error; - - fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { - let Some(rpc) = self.codec.decode(src)? else { - return Ok(None); - }; - // Store valid messages. - let mut messages = Vec::with_capacity(rpc.publish.len()); - // Store any invalid messages. - let mut invalid_messages = Vec::new(); - - for message in rpc.publish.into_iter() { - // Keep track of the type of invalid message. - let mut invalid_kind = None; - let mut verify_signature = false; - let mut verify_sequence_no = false; - let mut verify_source = false; - - match self.validation_mode { - ValidationMode::Strict => { - // Validate everything - verify_signature = true; - verify_sequence_no = true; - verify_source = true; - } - ValidationMode::Permissive => { - // If the fields exist, validate them - if message.signature.is_some() { - verify_signature = true; - } - if message.seqno.is_some() { - verify_sequence_no = true; - } - if message.from.is_some() { - verify_source = true; - } - } - ValidationMode::Anonymous => { - if message.signature.is_some() { - tracing::warn!( - "Signature field was non-empty and anonymous validation mode is set" - ); - invalid_kind = Some(ValidationError::SignaturePresent); - } else if message.seqno.is_some() { - tracing::warn!( - "Sequence number was non-empty and anonymous validation mode is set" - ); - invalid_kind = Some(ValidationError::SequenceNumberPresent); - } else if message.from.is_some() { - tracing::warn!("Message dropped. Message source was non-empty and anonymous validation mode is set"); - invalid_kind = Some(ValidationError::MessageSourcePresent); - } - } - ValidationMode::None => {} - } - - // If the initial validation logic failed, add the message to invalid messages and - // continue processing the others. - if let Some(validation_error) = invalid_kind.take() { - let message = RawMessage { - source: None, // don't bother inform the application - data: message.data.unwrap_or_default(), - sequence_number: None, // don't inform the application - topic: TopicHash::from_raw(message.topic), - signature: None, // don't inform the application - key: message.key, - validated: false, - }; - invalid_messages.push((message, validation_error)); - // proceed to the next message - continue; - } - - // verify message signatures if required - if verify_signature && !GossipsubCodec::verify_signature(&message) { - tracing::warn!("Invalid signature for received message"); - - // Build the invalid message (ignoring further validation of sequence number - // and source) - let message = RawMessage { - source: None, // don't bother inform the application - data: message.data.unwrap_or_default(), - sequence_number: None, // don't inform the application - topic: TopicHash::from_raw(message.topic), - signature: None, // don't inform the application - key: message.key, - validated: false, - }; - invalid_messages.push((message, ValidationError::InvalidSignature)); - // proceed to the next message - continue; - } - - // ensure the sequence number is a u64 - let sequence_number = if verify_sequence_no { - if let Some(seq_no) = message.seqno { - if seq_no.is_empty() { - None - } else if seq_no.len() != 8 { - tracing::debug!( - sequence_number=?seq_no, - sequence_length=%seq_no.len(), - "Invalid sequence number length for received message" - ); - let message = RawMessage { - source: None, // don't bother inform the application - data: message.data.unwrap_or_default(), - sequence_number: None, // don't inform the application - topic: TopicHash::from_raw(message.topic), - signature: message.signature, // don't inform the application - key: message.key, - validated: false, - }; - invalid_messages.push((message, ValidationError::InvalidSequenceNumber)); - // proceed to the next message - continue; - } else { - // valid sequence number - Some(BigEndian::read_u64(&seq_no)) - } - } else { - // sequence number was not present - tracing::debug!("Sequence number not present but expected"); - let message = RawMessage { - source: None, // don't bother inform the application - data: message.data.unwrap_or_default(), - sequence_number: None, // don't inform the application - topic: TopicHash::from_raw(message.topic), - signature: message.signature, // don't inform the application - key: message.key, - validated: false, - }; - invalid_messages.push((message, ValidationError::EmptySequenceNumber)); - continue; - } - } else { - // Do not verify the sequence number, consider it empty - None - }; - - // Verify the message source if required - let source = if verify_source { - if let Some(bytes) = message.from { - if !bytes.is_empty() { - match PeerId::from_bytes(&bytes) { - Ok(peer_id) => Some(peer_id), // valid peer id - Err(_) => { - // invalid peer id, add to invalid messages - tracing::debug!("Message source has an invalid PeerId"); - let message = RawMessage { - source: None, // don't bother inform the application - data: message.data.unwrap_or_default(), - sequence_number, - topic: TopicHash::from_raw(message.topic), - signature: message.signature, // don't inform the application - key: message.key, - validated: false, - }; - invalid_messages.push((message, ValidationError::InvalidPeerId)); - continue; - } - } - } else { - None - } - } else { - None - } - } else { - None - }; - - // This message has passed all validation, add it to the validated messages. - messages.push(RawMessage { - source, - data: message.data.unwrap_or_default(), - sequence_number, - topic: TopicHash::from_raw(message.topic), - signature: message.signature, - key: message.key, - validated: false, - }); - } - - let mut control_msgs = Vec::new(); - - if let Some(rpc_control) = rpc.control { - // Collect the gossipsub control messages - let ihave_msgs: Vec = rpc_control - .ihave - .into_iter() - .map(|ihave| { - ControlAction::IHave(IHave { - topic_hash: TopicHash::from_raw(ihave.topic_id.unwrap_or_default()), - message_ids: ihave - .message_ids - .into_iter() - .map(MessageId::from) - .collect::>(), - }) - }) - .collect(); - - let iwant_msgs: Vec = rpc_control - .iwant - .into_iter() - .map(|iwant| { - ControlAction::IWant(IWant { - message_ids: iwant - .message_ids - .into_iter() - .map(MessageId::from) - .collect::>(), - }) - }) - .collect(); - - let graft_msgs: Vec = rpc_control - .graft - .into_iter() - .map(|graft| { - ControlAction::Graft(Graft { - topic_hash: TopicHash::from_raw(graft.topic_id.unwrap_or_default()), - }) - }) - .collect(); - - let mut prune_msgs = Vec::new(); - - for prune in rpc_control.prune { - // filter out invalid peers - let peers = prune - .peers - .into_iter() - .filter_map(|info| { - info.peer_id - .as_ref() - .and_then(|id| PeerId::from_bytes(id).ok()) - .map(|peer_id| - //TODO signedPeerRecord, see https://github.com/libp2p/specs/pull/217 - PeerInfo { - peer_id: Some(peer_id), - }) - }) - .collect::>(); - - let topic_hash = TopicHash::from_raw(prune.topic_id.unwrap_or_default()); - prune_msgs.push(ControlAction::Prune(Prune { - topic_hash, - peers, - backoff: prune.backoff, - })); - } - - let idontwant_msgs: Vec = rpc_control - .idontwant - .into_iter() - .map(|idontwant| { - ControlAction::IDontWant(IDontWant { - message_ids: idontwant - .message_ids - .into_iter() - .map(MessageId::from) - .collect::>(), - }) - }) - .collect(); - - control_msgs.extend(ihave_msgs); - control_msgs.extend(iwant_msgs); - control_msgs.extend(graft_msgs); - control_msgs.extend(prune_msgs); - control_msgs.extend(idontwant_msgs); - } - - Ok(Some(HandlerEvent::Message { - rpc: Rpc { - messages, - subscriptions: rpc - .subscriptions - .into_iter() - .map(|sub| Subscription { - action: if Some(true) == sub.subscribe { - SubscriptionAction::Subscribe - } else { - SubscriptionAction::Unsubscribe - }, - topic_hash: TopicHash::from_raw(sub.topic_id.unwrap_or_default()), - }) - .collect(), - control_msgs, - }, - invalid_messages, - })) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::config::Config; - use crate::{Behaviour, ConfigBuilder, MessageAuthenticity}; - use crate::{IdentTopic as Topic, Version}; - use libp2p::identity::Keypair; - use quickcheck::*; - - #[derive(Clone, Debug)] - struct Message(RawMessage); - - impl Arbitrary for Message { - fn arbitrary(g: &mut Gen) -> Self { - let keypair = TestKeypair::arbitrary(g); - - // generate an arbitrary GossipsubMessage using the behaviour signing functionality - let config = Config::default(); - let mut gs: Behaviour = - Behaviour::new(MessageAuthenticity::Signed(keypair.0), config).unwrap(); - let mut data_g = quickcheck::Gen::new(10024); - let data = (0..u8::arbitrary(&mut data_g)) - .map(|_| u8::arbitrary(g)) - .collect::>(); - let topic_id = TopicId::arbitrary(g).0; - Message(gs.build_raw_message(topic_id, data).unwrap()) - } - } - - #[derive(Clone, Debug)] - struct TopicId(TopicHash); - - impl Arbitrary for TopicId { - fn arbitrary(g: &mut Gen) -> Self { - let mut data_g = quickcheck::Gen::new(1024); - let topic_string: String = (0..u8::arbitrary(&mut data_g)) - .map(|_| char::arbitrary(g)) - .collect::(); - TopicId(Topic::new(topic_string).into()) - } - } - - #[derive(Clone)] - struct TestKeypair(Keypair); - - impl Arbitrary for TestKeypair { - #[cfg(feature = "rsa")] - fn arbitrary(g: &mut Gen) -> Self { - let keypair = if bool::arbitrary(g) { - // Small enough to be inlined. - Keypair::generate_ed25519() - } else { - // Too large to be inlined. - let mut rsa_key = hex::decode("308204bd020100300d06092a864886f70d0101010500048204a7308204a30201000282010100ef930f41a71288b643c1cbecbf5f72ab53992249e2b00835bf07390b6745419f3848cbcc5b030faa127bc88cdcda1c1d6f3ff699f0524c15ab9d2c9d8015f5d4bd09881069aad4e9f91b8b0d2964d215cdbbae83ddd31a7622a8228acee07079f6e501aea95508fa26c6122816ef7b00ac526d422bd12aed347c37fff6c1c307f3ba57bb28a7f28609e0bdcc839da4eedca39f5d2fa855ba4b0f9c763e9764937db929a1839054642175312a3de2d3405c9d27bdf6505ef471ce85c5e015eee85bf7874b3d512f715de58d0794fd8afe021c197fbd385bb88a930342fac8da31c27166e2edab00fa55dc1c3814448ba38363077f4e8fe2bdea1c081f85f1aa6f02030100010282010028ff427a1aac1a470e7b4879601a6656193d3857ea79f33db74df61e14730e92bf9ffd78200efb0c40937c3356cbe049cd32e5f15be5c96d5febcaa9bd3484d7fded76a25062d282a3856a1b3b7d2c525cdd8434beae147628e21adf241dd64198d5819f310d033743915ba40ea0b6acdbd0533022ad6daa1ff42de51885f9e8bab2306c6ef1181902d1cd7709006eba1ab0587842b724e0519f295c24f6d848907f772ae9a0953fc931f4af16a07df450fb8bfa94572562437056613647818c238a6ff3f606cffa0533e4b8755da33418dfbc64a85110b1a036623c947400a536bb8df65e5ebe46f2dfd0cfc86e7aeeddd7574c253e8fbf755562b3669525d902818100f9fff30c6677b78dd31ec7a634361438457e80be7a7faf390903067ea8355faa78a1204a82b6e99cb7d9058d23c1ecf6cfe4a900137a00cecc0113fd68c5931602980267ea9a95d182d48ba0a6b4d5dd32fdac685cb2e5d8b42509b2eb59c9579ea6a67ccc7547427e2bd1fb1f23b0ccb4dd6ba7d206c8dd93253d70a451701302818100f5530dfef678d73ce6a401ae47043af10a2e3f224c71ae933035ecd68ccbc4df52d72bc6ca2b17e8faf3e548b483a2506c0369ab80df3b137b54d53fac98f95547c2bc245b416e650ce617e0d29db36066f1335a9ba02ad3e0edf9dc3d58fd835835042663edebce81803972696c789012847cb1f854ab2ac0a1bd3867ac7fb502818029c53010d456105f2bf52a9a8482bca2224a5eac74bf3cc1a4d5d291fafcdffd15a6a6448cce8efdd661f6617ca5fc37c8c885cc3374e109ac6049bcbf72b37eabf44602a2da2d4a1237fd145c863e6d75059976de762d9d258c42b0984e2a2befa01c95217c3ee9c736ff209c355466ff99375194eff943bc402ea1d172a1ed02818027175bf493bbbfb8719c12b47d967bf9eac061c90a5b5711172e9095c38bb8cc493c063abffe4bea110b0a2f22ac9311b3947ba31b7ef6bfecf8209eebd6d86c316a2366bbafda7279b2b47d5bb24b6202254f249205dcad347b574433f6593733b806f84316276c1990a016ce1bbdbe5f650325acc7791aefe515ecc60063bd02818100b6a2077f4adcf15a17092d9c4a346d6022ac48f3861b73cf714f84c440a07419a7ce75a73b9cbff4597c53c128bf81e87b272d70428a272d99f90cd9b9ea1033298e108f919c6477400145a102df3fb5601ffc4588203cf710002517bfa24e6ad32f4d09c6b1a995fa28a3104131bedd9072f3b4fb4a5c2056232643d310453f").unwrap(); - Keypair::rsa_from_pkcs8(&mut rsa_key).unwrap() - }; - TestKeypair(keypair) - } - - #[cfg(not(feature = "rsa"))] - fn arbitrary(_g: &mut Gen) -> Self { - // Small enough to be inlined. - TestKeypair(Keypair::generate_ed25519()) - } - } - - impl std::fmt::Debug for TestKeypair { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("TestKeypair") - .field("public", &self.0.public()) - .finish() - } - } - - #[test] - /// Test that RPC messages can be encoded and decoded successfully. - fn encode_decode() { - fn prop(message: Message) { - let message = message.0; - - let rpc = crate::types::Rpc { - messages: vec![message.clone()], - subscriptions: vec![], - control_msgs: vec![], - }; - - let mut codec = GossipsubCodec::new(u32::MAX as usize, ValidationMode::Strict); - let mut buf = BytesMut::new(); - codec.encode(rpc.into_protobuf(), &mut buf).unwrap(); - let decoded_rpc = codec.decode(&mut buf).unwrap().unwrap(); - // mark as validated as its a published message - match decoded_rpc { - HandlerEvent::Message { mut rpc, .. } => { - rpc.messages[0].validated = true; - - assert_eq!(vec![message], rpc.messages); - } - _ => panic!("Must decode a message"), - } - } - - QuickCheck::new().quickcheck(prop as fn(_) -> _) - } - - #[test] - fn support_floodsub_with_custom_protocol() { - let protocol_config = ConfigBuilder::default() - .protocol_id("/foosub", Version::V1_1) - .support_floodsub() - .build() - .unwrap() - .protocol_config(); - - assert_eq!(protocol_config.protocol_ids[0].protocol, "/foosub"); - assert_eq!(protocol_config.protocol_ids[1].protocol, "/floodsub/1.0.0"); - } -} diff --git a/beacon_node/lighthouse_network/gossipsub/src/rpc_proto.rs b/beacon_node/lighthouse_network/gossipsub/src/rpc_proto.rs deleted file mode 100644 index f653779ba2e..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/rpc_proto.rs +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright 2020 Sigma Prime Pty Ltd. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -pub(crate) mod proto { - #![allow(unreachable_pub)] - include!("generated/mod.rs"); - pub use self::gossipsub::pb::{mod_RPC::SubOpts, *}; -} - -#[cfg(test)] -mod test { - use crate::rpc_proto::proto::compat; - use crate::IdentTopic as Topic; - use libp2p::identity::PeerId; - use quick_protobuf::{BytesReader, MessageRead, MessageWrite, Writer}; - use rand::Rng; - - #[test] - fn test_multi_topic_message_compatibility() { - let topic1 = Topic::new("t1").hash(); - let topic2 = Topic::new("t2").hash(); - - let new_message1 = super::proto::Message { - from: Some(PeerId::random().to_bytes()), - data: Some(rand::thread_rng().gen::<[u8; 32]>().to_vec()), - seqno: Some(rand::thread_rng().gen::<[u8; 8]>().to_vec()), - topic: topic1.clone().into_string(), - signature: Some(rand::thread_rng().gen::<[u8; 32]>().to_vec()), - key: Some(rand::thread_rng().gen::<[u8; 32]>().to_vec()), - }; - let old_message1 = compat::pb::Message { - from: Some(PeerId::random().to_bytes()), - data: Some(rand::thread_rng().gen::<[u8; 32]>().to_vec()), - seqno: Some(rand::thread_rng().gen::<[u8; 8]>().to_vec()), - topic_ids: vec![topic1.clone().into_string()], - signature: Some(rand::thread_rng().gen::<[u8; 32]>().to_vec()), - key: Some(rand::thread_rng().gen::<[u8; 32]>().to_vec()), - }; - let old_message2 = compat::pb::Message { - from: Some(PeerId::random().to_bytes()), - data: Some(rand::thread_rng().gen::<[u8; 32]>().to_vec()), - seqno: Some(rand::thread_rng().gen::<[u8; 8]>().to_vec()), - topic_ids: vec![topic1.clone().into_string(), topic2.clone().into_string()], - signature: Some(rand::thread_rng().gen::<[u8; 32]>().to_vec()), - key: Some(rand::thread_rng().gen::<[u8; 32]>().to_vec()), - }; - - let mut new_message1b = Vec::with_capacity(new_message1.get_size()); - let mut writer = Writer::new(&mut new_message1b); - new_message1.write_message(&mut writer).unwrap(); - - let mut old_message1b = Vec::with_capacity(old_message1.get_size()); - let mut writer = Writer::new(&mut old_message1b); - old_message1.write_message(&mut writer).unwrap(); - - let mut old_message2b = Vec::with_capacity(old_message2.get_size()); - let mut writer = Writer::new(&mut old_message2b); - old_message2.write_message(&mut writer).unwrap(); - - let mut reader = BytesReader::from_bytes(&old_message1b[..]); - let new_message = - super::proto::Message::from_reader(&mut reader, &old_message1b[..]).unwrap(); - assert_eq!(new_message.topic, topic1.clone().into_string()); - - let mut reader = BytesReader::from_bytes(&old_message2b[..]); - let new_message = - super::proto::Message::from_reader(&mut reader, &old_message2b[..]).unwrap(); - assert_eq!(new_message.topic, topic2.into_string()); - - let mut reader = BytesReader::from_bytes(&new_message1b[..]); - let old_message = - compat::pb::Message::from_reader(&mut reader, &new_message1b[..]).unwrap(); - assert_eq!(old_message.topic_ids, vec![topic1.into_string()]); - } -} diff --git a/beacon_node/lighthouse_network/gossipsub/src/subscription_filter.rs b/beacon_node/lighthouse_network/gossipsub/src/subscription_filter.rs deleted file mode 100644 index 02bb9b4eab6..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/subscription_filter.rs +++ /dev/null @@ -1,435 +0,0 @@ -// Copyright 2020 Sigma Prime Pty Ltd. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -use crate::types::Subscription; -use crate::TopicHash; -use std::collections::{BTreeSet, HashMap, HashSet}; - -pub trait TopicSubscriptionFilter { - /// Returns true iff the topic is of interest and we can subscribe to it. - fn can_subscribe(&mut self, topic_hash: &TopicHash) -> bool; - - /// Filters a list of incoming subscriptions and returns a filtered set - /// By default this deduplicates the subscriptions and calls - /// [`Self::filter_incoming_subscription_set`] on the filtered set. - fn filter_incoming_subscriptions<'a>( - &mut self, - subscriptions: &'a [Subscription], - currently_subscribed_topics: &BTreeSet, - ) -> Result, String> { - let mut filtered_subscriptions: HashMap = HashMap::new(); - for subscription in subscriptions { - use std::collections::hash_map::Entry::*; - match filtered_subscriptions.entry(subscription.topic_hash.clone()) { - Occupied(entry) => { - if entry.get().action != subscription.action { - entry.remove(); - } - } - Vacant(entry) => { - entry.insert(subscription); - } - } - } - self.filter_incoming_subscription_set( - filtered_subscriptions.into_values().collect(), - currently_subscribed_topics, - ) - } - - /// Filters a set of deduplicated subscriptions - /// By default this filters the elements based on [`Self::allow_incoming_subscription`]. - fn filter_incoming_subscription_set<'a>( - &mut self, - mut subscriptions: HashSet<&'a Subscription>, - _currently_subscribed_topics: &BTreeSet, - ) -> Result, String> { - subscriptions.retain(|s| { - if self.allow_incoming_subscription(s) { - true - } else { - tracing::debug!(subscription=?s, "Filtered incoming subscription"); - false - } - }); - Ok(subscriptions) - } - - /// Returns true iff we allow an incoming subscription. - /// This is used by the default implementation of filter_incoming_subscription_set to decide - /// whether to filter out a subscription or not. - /// By default this uses can_subscribe to decide the same for incoming subscriptions as for - /// outgoing ones. - fn allow_incoming_subscription(&mut self, subscription: &Subscription) -> bool { - self.can_subscribe(&subscription.topic_hash) - } -} - -//some useful implementers - -/// Allows all subscriptions -#[derive(Default, Clone)] -pub struct AllowAllSubscriptionFilter {} - -impl TopicSubscriptionFilter for AllowAllSubscriptionFilter { - fn can_subscribe(&mut self, _: &TopicHash) -> bool { - true - } -} - -/// Allows only whitelisted subscriptions -#[derive(Default, Clone)] -pub struct WhitelistSubscriptionFilter(pub HashSet); - -impl TopicSubscriptionFilter for WhitelistSubscriptionFilter { - fn can_subscribe(&mut self, topic_hash: &TopicHash) -> bool { - self.0.contains(topic_hash) - } -} - -/// Adds a max count to a given subscription filter -pub struct MaxCountSubscriptionFilter { - pub filter: T, - pub max_subscribed_topics: usize, - pub max_subscriptions_per_request: usize, -} - -impl TopicSubscriptionFilter for MaxCountSubscriptionFilter { - fn can_subscribe(&mut self, topic_hash: &TopicHash) -> bool { - self.filter.can_subscribe(topic_hash) - } - - fn filter_incoming_subscriptions<'a>( - &mut self, - subscriptions: &'a [Subscription], - currently_subscribed_topics: &BTreeSet, - ) -> Result, String> { - if subscriptions.len() > self.max_subscriptions_per_request { - return Err("too many subscriptions per request".into()); - } - let result = self - .filter - .filter_incoming_subscriptions(subscriptions, currently_subscribed_topics)?; - - use crate::types::SubscriptionAction::*; - - let mut unsubscribed = 0; - let mut new_subscribed = 0; - for s in &result { - let currently_contained = currently_subscribed_topics.contains(&s.topic_hash); - match s.action { - Unsubscribe => { - if currently_contained { - unsubscribed += 1; - } - } - Subscribe => { - if !currently_contained { - new_subscribed += 1; - } - } - } - } - - if new_subscribed + currently_subscribed_topics.len() - > self.max_subscribed_topics + unsubscribed - { - return Err("too many subscribed topics".into()); - } - - Ok(result) - } -} - -/// Combines two subscription filters -pub struct CombinedSubscriptionFilters { - pub filter1: T, - pub filter2: S, -} - -impl TopicSubscriptionFilter for CombinedSubscriptionFilters -where - T: TopicSubscriptionFilter, - S: TopicSubscriptionFilter, -{ - fn can_subscribe(&mut self, topic_hash: &TopicHash) -> bool { - self.filter1.can_subscribe(topic_hash) && self.filter2.can_subscribe(topic_hash) - } - - fn filter_incoming_subscription_set<'a>( - &mut self, - subscriptions: HashSet<&'a Subscription>, - currently_subscribed_topics: &BTreeSet, - ) -> Result, String> { - let intermediate = self - .filter1 - .filter_incoming_subscription_set(subscriptions, currently_subscribed_topics)?; - self.filter2 - .filter_incoming_subscription_set(intermediate, currently_subscribed_topics) - } -} - -pub struct CallbackSubscriptionFilter(pub T) -where - T: FnMut(&TopicHash) -> bool; - -impl TopicSubscriptionFilter for CallbackSubscriptionFilter -where - T: FnMut(&TopicHash) -> bool, -{ - fn can_subscribe(&mut self, topic_hash: &TopicHash) -> bool { - (self.0)(topic_hash) - } -} - -///A subscription filter that filters topics based on a regular expression. -pub struct RegexSubscriptionFilter(pub regex::Regex); - -impl TopicSubscriptionFilter for RegexSubscriptionFilter { - fn can_subscribe(&mut self, topic_hash: &TopicHash) -> bool { - self.0.is_match(topic_hash.as_str()) - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::types::SubscriptionAction::*; - - #[test] - fn test_filter_incoming_allow_all_with_duplicates() { - let mut filter = AllowAllSubscriptionFilter {}; - - let t1 = TopicHash::from_raw("t1"); - let t2 = TopicHash::from_raw("t2"); - - let old = BTreeSet::from_iter(vec![t1.clone()]); - let subscriptions = vec![ - Subscription { - action: Unsubscribe, - topic_hash: t1.clone(), - }, - Subscription { - action: Unsubscribe, - topic_hash: t2.clone(), - }, - Subscription { - action: Subscribe, - topic_hash: t2, - }, - Subscription { - action: Subscribe, - topic_hash: t1.clone(), - }, - Subscription { - action: Unsubscribe, - topic_hash: t1, - }, - ]; - - let result = filter - .filter_incoming_subscriptions(&subscriptions, &old) - .unwrap(); - assert_eq!(result, vec![&subscriptions[4]].into_iter().collect()); - } - - #[test] - fn test_filter_incoming_whitelist() { - let t1 = TopicHash::from_raw("t1"); - let t2 = TopicHash::from_raw("t2"); - - let mut filter = WhitelistSubscriptionFilter(HashSet::from_iter(vec![t1.clone()])); - - let old = Default::default(); - let subscriptions = vec![ - Subscription { - action: Subscribe, - topic_hash: t1, - }, - Subscription { - action: Subscribe, - topic_hash: t2, - }, - ]; - - let result = filter - .filter_incoming_subscriptions(&subscriptions, &old) - .unwrap(); - assert_eq!(result, vec![&subscriptions[0]].into_iter().collect()); - } - - #[test] - fn test_filter_incoming_too_many_subscriptions_per_request() { - let t1 = TopicHash::from_raw("t1"); - - let mut filter = MaxCountSubscriptionFilter { - filter: AllowAllSubscriptionFilter {}, - max_subscribed_topics: 100, - max_subscriptions_per_request: 2, - }; - - let old = Default::default(); - - let subscriptions = vec![ - Subscription { - action: Subscribe, - topic_hash: t1.clone(), - }, - Subscription { - action: Unsubscribe, - topic_hash: t1.clone(), - }, - Subscription { - action: Subscribe, - topic_hash: t1, - }, - ]; - - let result = filter.filter_incoming_subscriptions(&subscriptions, &old); - assert_eq!(result, Err("too many subscriptions per request".into())); - } - - #[test] - fn test_filter_incoming_too_many_subscriptions() { - let t: Vec<_> = (0..4) - .map(|i| TopicHash::from_raw(format!("t{i}"))) - .collect(); - - let mut filter = MaxCountSubscriptionFilter { - filter: AllowAllSubscriptionFilter {}, - max_subscribed_topics: 3, - max_subscriptions_per_request: 2, - }; - - let old = t[0..2].iter().cloned().collect(); - - let subscriptions = vec![ - Subscription { - action: Subscribe, - topic_hash: t[2].clone(), - }, - Subscription { - action: Subscribe, - topic_hash: t[3].clone(), - }, - ]; - - let result = filter.filter_incoming_subscriptions(&subscriptions, &old); - assert_eq!(result, Err("too many subscribed topics".into())); - } - - #[test] - fn test_filter_incoming_max_subscribed_valid() { - let t: Vec<_> = (0..5) - .map(|i| TopicHash::from_raw(format!("t{i}"))) - .collect(); - - let mut filter = MaxCountSubscriptionFilter { - filter: WhitelistSubscriptionFilter(t.iter().take(4).cloned().collect()), - max_subscribed_topics: 2, - max_subscriptions_per_request: 5, - }; - - let old = t[0..2].iter().cloned().collect(); - - let subscriptions = vec![ - Subscription { - action: Subscribe, - topic_hash: t[4].clone(), - }, - Subscription { - action: Subscribe, - topic_hash: t[2].clone(), - }, - Subscription { - action: Subscribe, - topic_hash: t[3].clone(), - }, - Subscription { - action: Unsubscribe, - topic_hash: t[0].clone(), - }, - Subscription { - action: Unsubscribe, - topic_hash: t[1].clone(), - }, - ]; - - let result = filter - .filter_incoming_subscriptions(&subscriptions, &old) - .unwrap(); - assert_eq!(result, subscriptions[1..].iter().collect()); - } - - #[test] - fn test_callback_filter() { - let t1 = TopicHash::from_raw("t1"); - let t2 = TopicHash::from_raw("t2"); - - let mut filter = CallbackSubscriptionFilter(|h| h.as_str() == "t1"); - - let old = Default::default(); - let subscriptions = vec![ - Subscription { - action: Subscribe, - topic_hash: t1, - }, - Subscription { - action: Subscribe, - topic_hash: t2, - }, - ]; - - let result = filter - .filter_incoming_subscriptions(&subscriptions, &old) - .unwrap(); - assert_eq!(result, vec![&subscriptions[0]].into_iter().collect()); - } - - #[test] - fn test_regex_subscription_filter() { - let t1 = TopicHash::from_raw("tt"); - let t2 = TopicHash::from_raw("et3t3te"); - let t3 = TopicHash::from_raw("abcdefghijklmnopqrsuvwxyz"); - - let mut filter = RegexSubscriptionFilter(regex::Regex::new("t.*t").unwrap()); - - let old = Default::default(); - let subscriptions = vec![ - Subscription { - action: Subscribe, - topic_hash: t1, - }, - Subscription { - action: Subscribe, - topic_hash: t2, - }, - Subscription { - action: Subscribe, - topic_hash: t3, - }, - ]; - - let result = filter - .filter_incoming_subscriptions(&subscriptions, &old) - .unwrap(); - assert_eq!(result, subscriptions[..2].iter().collect()); - } -} diff --git a/beacon_node/lighthouse_network/gossipsub/src/time_cache.rs b/beacon_node/lighthouse_network/gossipsub/src/time_cache.rs deleted file mode 100644 index a3e5c01ac4c..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/time_cache.rs +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright 2020 Sigma Prime Pty Ltd. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -//! This implements a time-based LRU cache for checking gossipsub message duplicates. - -use fnv::FnvHashMap; -use std::collections::hash_map::{ - self, - Entry::{Occupied, Vacant}, -}; -use std::collections::VecDeque; -use std::time::Duration; -use web_time::Instant; - -struct ExpiringElement { - /// The element that expires - element: Element, - /// The expire time. - expires: Instant, -} - -pub(crate) struct TimeCache { - /// Mapping a key to its value together with its latest expire time (can be updated through - /// reinserts). - map: FnvHashMap>, - /// An ordered list of keys by expires time. - list: VecDeque>, - /// The time elements remain in the cache. - ttl: Duration, -} - -pub(crate) struct OccupiedEntry<'a, K, V> { - entry: hash_map::OccupiedEntry<'a, K, ExpiringElement>, -} - -impl<'a, K, V> OccupiedEntry<'a, K, V> -where - K: Eq + std::hash::Hash + Clone, -{ - pub(crate) fn into_mut(self) -> &'a mut V { - &mut self.entry.into_mut().element - } -} - -pub(crate) struct VacantEntry<'a, K, V> { - expiration: Instant, - entry: hash_map::VacantEntry<'a, K, ExpiringElement>, - list: &'a mut VecDeque>, -} - -impl<'a, K, V> VacantEntry<'a, K, V> -where - K: Eq + std::hash::Hash + Clone, -{ - pub(crate) fn insert(self, value: V) -> &'a mut V { - self.list.push_back(ExpiringElement { - element: self.entry.key().clone(), - expires: self.expiration, - }); - &mut self - .entry - .insert(ExpiringElement { - element: value, - expires: self.expiration, - }) - .element - } -} - -pub(crate) enum Entry<'a, K: 'a, V: 'a> { - Occupied(OccupiedEntry<'a, K, V>), - Vacant(VacantEntry<'a, K, V>), -} - -impl<'a, K: 'a, V: 'a> Entry<'a, K, V> -where - K: Eq + std::hash::Hash + Clone, -{ - pub(crate) fn or_default(self) -> &'a mut V - where - V: Default, - { - match self { - Entry::Occupied(entry) => entry.into_mut(), - Entry::Vacant(entry) => entry.insert(V::default()), - } - } -} - -impl TimeCache -where - Key: Eq + std::hash::Hash + Clone, -{ - pub(crate) fn new(ttl: Duration) -> Self { - TimeCache { - map: FnvHashMap::default(), - list: VecDeque::new(), - ttl, - } - } - - fn remove_expired_keys(&mut self, now: Instant) { - while let Some(element) = self.list.pop_front() { - if element.expires > now { - self.list.push_front(element); - break; - } - if let Occupied(entry) = self.map.entry(element.element.clone()) { - if entry.get().expires <= now { - entry.remove(); - } - } - } - } - - pub(crate) fn entry(&mut self, key: Key) -> Entry { - let now = Instant::now(); - self.remove_expired_keys(now); - match self.map.entry(key) { - Occupied(entry) => Entry::Occupied(OccupiedEntry { entry }), - Vacant(entry) => Entry::Vacant(VacantEntry { - expiration: now + self.ttl, - entry, - list: &mut self.list, - }), - } - } - - /// Empties the entire cache. - #[cfg(test)] - pub(crate) fn clear(&mut self) { - self.map.clear(); - self.list.clear(); - } - - pub(crate) fn contains_key(&self, key: &Key) -> bool { - self.map.contains_key(key) - } -} - -pub(crate) struct DuplicateCache(TimeCache); - -impl DuplicateCache -where - Key: Eq + std::hash::Hash + Clone, -{ - pub(crate) fn new(ttl: Duration) -> Self { - Self(TimeCache::new(ttl)) - } - - // Inserts new elements and removes any expired elements. - // - // If the key was not present this returns `true`. If the value was already present this - // returns `false`. - pub(crate) fn insert(&mut self, key: Key) -> bool { - if let Entry::Vacant(entry) = self.0.entry(key) { - entry.insert(()); - true - } else { - false - } - } - - pub(crate) fn contains(&self, key: &Key) -> bool { - self.0.contains_key(key) - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn cache_added_entries_exist() { - let mut cache = DuplicateCache::new(Duration::from_secs(10)); - - cache.insert("t"); - cache.insert("e"); - - // Should report that 't' and 't' already exists - assert!(!cache.insert("t")); - assert!(!cache.insert("e")); - } - - #[test] - fn cache_entries_expire() { - let mut cache = DuplicateCache::new(Duration::from_millis(100)); - - cache.insert("t"); - assert!(!cache.insert("t")); - cache.insert("e"); - //assert!(!cache.insert("t")); - assert!(!cache.insert("e")); - // sleep until cache expiry - std::thread::sleep(Duration::from_millis(101)); - // add another element to clear previous cache - cache.insert("s"); - - // should be removed from the cache - assert!(cache.insert("t")); - } -} diff --git a/beacon_node/lighthouse_network/gossipsub/src/topic.rs b/beacon_node/lighthouse_network/gossipsub/src/topic.rs deleted file mode 100644 index a73496b53f2..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/topic.rs +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright 2020 Sigma Prime Pty Ltd. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -use crate::rpc_proto::proto; -use base64::prelude::*; -use prometheus_client::encoding::EncodeLabelSet; -use quick_protobuf::Writer; -use sha2::{Digest, Sha256}; -use std::fmt; - -/// A generic trait that can be extended for various hashing types for a topic. -pub trait Hasher { - /// The function that takes a topic string and creates a topic hash. - fn hash(topic_string: String) -> TopicHash; -} - -/// A type for representing topics who use the identity hash. -#[derive(Debug, Clone)] -pub struct IdentityHash {} -impl Hasher for IdentityHash { - /// Creates a [`TopicHash`] as a raw string. - fn hash(topic_string: String) -> TopicHash { - TopicHash { hash: topic_string } - } -} - -#[derive(Debug, Clone)] -pub struct Sha256Hash {} -impl Hasher for Sha256Hash { - /// Creates a [`TopicHash`] by SHA256 hashing the topic then base64 encoding the - /// hash. - fn hash(topic_string: String) -> TopicHash { - use quick_protobuf::MessageWrite; - - let topic_descripter = proto::TopicDescriptor { - name: Some(topic_string), - auth: None, - enc: None, - }; - let mut bytes = Vec::with_capacity(topic_descripter.get_size()); - let mut writer = Writer::new(&mut bytes); - topic_descripter - .write_message(&mut writer) - .expect("Encoding to succeed"); - let hash = BASE64_STANDARD.encode(Sha256::digest(&bytes)); - TopicHash { hash } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, EncodeLabelSet)] -pub struct TopicHash { - /// The topic hash. Stored as a string to align with the protobuf API. - hash: String, -} - -impl TopicHash { - pub fn from_raw(hash: impl Into) -> TopicHash { - TopicHash { hash: hash.into() } - } - - pub fn into_string(self) -> String { - self.hash - } - - pub fn as_str(&self) -> &str { - &self.hash - } -} - -/// A gossipsub topic. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct Topic { - topic: String, - phantom_data: std::marker::PhantomData, -} - -impl From> for TopicHash { - fn from(topic: Topic) -> TopicHash { - topic.hash() - } -} - -impl Topic { - pub fn new(topic: impl Into) -> Self { - Topic { - topic: topic.into(), - phantom_data: std::marker::PhantomData, - } - } - - pub fn hash(&self) -> TopicHash { - H::hash(self.topic.clone()) - } -} - -impl fmt::Display for Topic { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.topic) - } -} - -impl fmt::Display for TopicHash { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.hash) - } -} diff --git a/beacon_node/lighthouse_network/gossipsub/src/transform.rs b/beacon_node/lighthouse_network/gossipsub/src/transform.rs deleted file mode 100644 index 6f57d9fc46b..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/transform.rs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2020 Sigma Prime Pty Ltd. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -//! This trait allows of extended user-level decoding that can apply to message-data before a -//! message-id is calculated. -//! -//! This is primarily designed to allow applications to implement their own custom compression -//! algorithms that can be topic-specific. Once the raw data is transformed the message-id is then -//! calculated, allowing for applications to employ message-id functions post compression. - -use crate::{Message, RawMessage, TopicHash}; - -/// A general trait of transforming a [`RawMessage`] into a [`Message`]. The -/// [`RawMessage`] is obtained from the wire and the [`Message`] is used to -/// calculate the [`crate::MessageId`] of the message and is what is sent to the application. -/// -/// The inbound/outbound transforms must be inverses. Applying the inbound transform and then the -/// outbound transform MUST leave the underlying data un-modified. -/// -/// By default, this is the identity transform for all fields in [`Message`]. -pub trait DataTransform { - /// Takes a [`RawMessage`] received and converts it to a [`Message`]. - fn inbound_transform(&self, raw_message: RawMessage) -> Result; - - /// Takes the data to be published (a topic and associated data) transforms the data. The - /// transformed data will then be used to create a [`crate::RawMessage`] to be sent to peers. - fn outbound_transform( - &self, - topic: &TopicHash, - data: Vec, - ) -> Result, std::io::Error>; -} - -/// The default transform, the raw data is propagated as is to the application layer gossipsub. -#[derive(Default, Clone)] -pub struct IdentityTransform; - -impl DataTransform for IdentityTransform { - fn inbound_transform(&self, raw_message: RawMessage) -> Result { - Ok(Message { - source: raw_message.source, - data: raw_message.data, - sequence_number: raw_message.sequence_number, - topic: raw_message.topic, - }) - } - - fn outbound_transform( - &self, - _topic: &TopicHash, - data: Vec, - ) -> Result, std::io::Error> { - Ok(data) - } -} diff --git a/beacon_node/lighthouse_network/gossipsub/src/types.rs b/beacon_node/lighthouse_network/gossipsub/src/types.rs deleted file mode 100644 index f5dac380e32..00000000000 --- a/beacon_node/lighthouse_network/gossipsub/src/types.rs +++ /dev/null @@ -1,882 +0,0 @@ -// Copyright 2020 Sigma Prime Pty Ltd. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -//! A collection of types using the Gossipsub system. -use crate::metrics::Metrics; -use crate::TopicHash; -use async_channel::{Receiver, Sender}; -use futures::stream::Peekable; -use futures::{Future, Stream, StreamExt}; -use futures_timer::Delay; -use hashlink::LinkedHashMap; -use libp2p::identity::PeerId; -use libp2p::swarm::ConnectionId; -use prometheus_client::encoding::EncodeLabelValue; -use quick_protobuf::MessageWrite; -use std::collections::BTreeSet; -use std::fmt::Debug; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; -use std::task::{Context, Poll}; -use std::time::Instant; -use std::{fmt, pin::Pin}; -use web_time::Duration; - -use crate::rpc_proto::proto; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -/// The type of messages that have expired while attempting to send to a peer. -#[derive(Clone, Debug, Default)] -pub struct FailedMessages { - /// The number of publish messages that failed to be published in a heartbeat. - pub publish: usize, - /// The number of forward messages that failed to be published in a heartbeat. - pub forward: usize, - /// The number of messages that were failed to be sent to the priority queue as it was full. - pub priority: usize, - /// The number of messages that were failed to be sent to the non-priority queue as it was full. - pub non_priority: usize, -} - -impl FailedMessages { - /// The total number of messages that expired due a timeout. - pub fn total_timeout(&self) -> usize { - self.publish + self.forward - } - - /// The total number of messages that failed due to the queue being full. - pub fn total_queue_full(&self) -> usize { - self.priority + self.non_priority - } - - /// The total failed messages in a heartbeat. - pub fn total(&self) -> usize { - self.total_timeout() + self.total_queue_full() - } -} - -#[derive(Debug)] -/// Validation kinds from the application for received messages. -pub enum MessageAcceptance { - /// The message is considered valid, and it should be delivered and forwarded to the network. - Accept, - /// The message is considered invalid, and it should be rejected and trigger the Pâ‚„ penalty. - Reject, - /// The message is neither delivered nor forwarded to the network, but the router does not - /// trigger the Pâ‚„ penalty. - Ignore, -} - -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct MessageId(pub Vec); - -impl MessageId { - pub fn new(value: &[u8]) -> Self { - Self(value.to_vec()) - } -} - -impl>> From for MessageId { - fn from(value: T) -> Self { - Self(value.into()) - } -} - -impl std::fmt::Display for MessageId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", hex_fmt::HexFmt(&self.0)) - } -} - -impl std::fmt::Debug for MessageId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "MessageId({})", hex_fmt::HexFmt(&self.0)) - } -} - -#[derive(Debug, Clone)] -pub(crate) struct PeerConnections { - /// The kind of protocol the peer supports. - pub(crate) kind: PeerKind, - /// Its current connections. - pub(crate) connections: Vec, - /// The rpc sender to the peer. - pub(crate) sender: RpcSender, - /// Subscribed topics. - pub(crate) topics: BTreeSet, - /// IDONTWANT messages received from the peer. - pub(crate) dont_send_received: LinkedHashMap, - /// IDONTWANT messages we sent to the peer. - pub(crate) dont_send_sent: LinkedHashMap, -} - -/// Describes the types of peers that can exist in the gossipsub context. -#[derive(Debug, Clone, PartialEq, Hash, EncodeLabelValue, Eq)] -#[allow(non_camel_case_types)] -pub enum PeerKind { - /// A gossipsub 1.2 peer. - Gossipsubv1_2, - /// A gossipsub 1.1 peer. - Gossipsubv1_1, - /// A gossipsub 1.0 peer. - Gossipsub, - /// A floodsub peer. - Floodsub, - /// The peer doesn't support any of the protocols. - NotSupported, -} - -impl PeerKind { - /// Returns true if peer speaks any gossipsub version. - pub(crate) fn is_gossipsub(&self) -> bool { - matches!( - self, - Self::Gossipsubv1_2 | Self::Gossipsubv1_1 | Self::Gossipsub - ) - } -} - -/// A message received by the gossipsub system and stored locally in caches.. -#[derive(Clone, PartialEq, Eq, Hash, Debug)] -pub struct RawMessage { - /// Id of the peer that published this message. - pub source: Option, - - /// Content of the message. Its meaning is out of scope of this library. - pub data: Vec, - - /// A random sequence number. - pub sequence_number: Option, - - /// The topic this message belongs to - pub topic: TopicHash, - - /// The signature of the message if it's signed. - pub signature: Option>, - - /// The public key of the message if it is signed and the source [`PeerId`] cannot be inlined. - pub key: Option>, - - /// Flag indicating if this message has been validated by the application or not. - pub validated: bool, -} - -impl RawMessage { - /// Calculates the encoded length of this message (used for calculating metrics). - pub fn raw_protobuf_len(&self) -> usize { - let message = proto::Message { - from: self.source.map(|m| m.to_bytes()), - data: Some(self.data.clone()), - seqno: self.sequence_number.map(|s| s.to_be_bytes().to_vec()), - topic: TopicHash::into_string(self.topic.clone()), - signature: self.signature.clone(), - key: self.key.clone(), - }; - message.get_size() - } -} - -impl From for proto::Message { - fn from(raw: RawMessage) -> Self { - proto::Message { - from: raw.source.map(|m| m.to_bytes()), - data: Some(raw.data), - seqno: raw.sequence_number.map(|s| s.to_be_bytes().to_vec()), - topic: TopicHash::into_string(raw.topic), - signature: raw.signature, - key: raw.key, - } - } -} - -/// The message sent to the user after a [`RawMessage`] has been transformed by a -/// [`crate::DataTransform`]. -#[derive(Clone, PartialEq, Eq, Hash)] -pub struct Message { - /// Id of the peer that published this message. - pub source: Option, - - /// Content of the message. - pub data: Vec, - - /// A random sequence number. - pub sequence_number: Option, - - /// The topic this message belongs to - pub topic: TopicHash, -} - -impl fmt::Debug for Message { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Message") - .field( - "data", - &format_args!("{:<20}", &hex_fmt::HexFmt(&self.data)), - ) - .field("source", &self.source) - .field("sequence_number", &self.sequence_number) - .field("topic", &self.topic) - .finish() - } -} - -/// A subscription received by the gossipsub system. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Subscription { - /// Action to perform. - pub action: SubscriptionAction, - /// The topic from which to subscribe or unsubscribe. - pub topic_hash: TopicHash, -} - -/// Action that a subscription wants to perform. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum SubscriptionAction { - /// The remote wants to subscribe to the given topic. - Subscribe, - /// The remote wants to unsubscribe from the given topic. - Unsubscribe, -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub(crate) struct PeerInfo { - pub(crate) peer_id: Option, - //TODO add this when RFC: Signed Address Records got added to the spec (see pull request - // https://github.com/libp2p/specs/pull/217) - //pub signed_peer_record: ?, -} - -/// A Control message received by the gossipsub system. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum ControlAction { - /// Node broadcasts known messages per topic - IHave control message. - IHave(IHave), - /// The node requests specific message ids (peer_id + sequence _number) - IWant control message. - IWant(IWant), - /// The node has been added to the mesh - Graft control message. - Graft(Graft), - /// The node has been removed from the mesh - Prune control message. - Prune(Prune), - /// The node requests us to not forward message ids (peer_id + sequence _number) - IDontWant control message. - IDontWant(IDontWant), -} - -/// Node broadcasts known messages per topic - IHave control message. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct IHave { - /// The topic of the messages. - pub(crate) topic_hash: TopicHash, - /// A list of known message ids (peer_id + sequence _number) as a string. - pub(crate) message_ids: Vec, -} - -/// The node requests specific message ids (peer_id + sequence _number) - IWant control message. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct IWant { - /// A list of known message ids (peer_id + sequence _number) as a string. - pub(crate) message_ids: Vec, -} - -/// The node has been added to the mesh - Graft control message. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Graft { - /// The mesh topic the peer should be added to. - pub(crate) topic_hash: TopicHash, -} - -/// The node has been removed from the mesh - Prune control message. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Prune { - /// The mesh topic the peer should be removed from. - pub(crate) topic_hash: TopicHash, - /// A list of peers to be proposed to the removed peer as peer exchange - pub(crate) peers: Vec, - /// The backoff time in seconds before we allow to reconnect - pub(crate) backoff: Option, -} - -/// The node requests us to not forward message ids - IDontWant control message. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct IDontWant { - /// A list of known message ids. - pub(crate) message_ids: Vec, -} - -/// A Gossipsub RPC message sent. -#[derive(Debug)] -pub enum RpcOut { - /// Publish a Gossipsub message on network. The [`Delay`] tags the time we attempted to - /// send it. - Publish { message: RawMessage, timeout: Delay }, - /// Forward a Gossipsub message to the network. The [`Delay`] tags the time we attempted to - /// send it. - Forward { message: RawMessage, timeout: Delay }, - /// Subscribe a topic. - Subscribe(TopicHash), - /// Unsubscribe a topic. - Unsubscribe(TopicHash), - /// Send a GRAFT control message. - Graft(Graft), - /// Send a PRUNE control message. - Prune(Prune), - /// Send a IHave control message. - IHave(IHave), - /// Send a IWant control message. - IWant(IWant), - /// Send a IDontWant control message. - IDontWant(IDontWant), -} - -impl RpcOut { - /// Converts the GossipsubRPC into its protobuf format. - // A convenience function to avoid explicitly specifying types. - pub fn into_protobuf(self) -> proto::RPC { - self.into() - } -} - -impl From for proto::RPC { - /// Converts the RPC into protobuf format. - fn from(rpc: RpcOut) -> Self { - match rpc { - RpcOut::Publish { - message, - timeout: _, - } => proto::RPC { - subscriptions: Vec::new(), - publish: vec![message.into()], - control: None, - }, - RpcOut::Forward { - message, - timeout: _, - } => proto::RPC { - publish: vec![message.into()], - subscriptions: Vec::new(), - control: None, - }, - RpcOut::Subscribe(topic) => proto::RPC { - publish: Vec::new(), - subscriptions: vec![proto::SubOpts { - subscribe: Some(true), - topic_id: Some(topic.into_string()), - }], - control: None, - }, - RpcOut::Unsubscribe(topic) => proto::RPC { - publish: Vec::new(), - subscriptions: vec![proto::SubOpts { - subscribe: Some(false), - topic_id: Some(topic.into_string()), - }], - control: None, - }, - RpcOut::IHave(IHave { - topic_hash, - message_ids, - }) => proto::RPC { - publish: Vec::new(), - subscriptions: Vec::new(), - control: Some(proto::ControlMessage { - ihave: vec![proto::ControlIHave { - topic_id: Some(topic_hash.into_string()), - message_ids: message_ids.into_iter().map(|msg_id| msg_id.0).collect(), - }], - iwant: vec![], - graft: vec![], - prune: vec![], - idontwant: vec![], - }), - }, - RpcOut::IWant(IWant { message_ids }) => proto::RPC { - publish: Vec::new(), - subscriptions: Vec::new(), - control: Some(proto::ControlMessage { - ihave: vec![], - iwant: vec![proto::ControlIWant { - message_ids: message_ids.into_iter().map(|msg_id| msg_id.0).collect(), - }], - graft: vec![], - prune: vec![], - idontwant: vec![], - }), - }, - RpcOut::Graft(Graft { topic_hash }) => proto::RPC { - publish: Vec::new(), - subscriptions: vec![], - control: Some(proto::ControlMessage { - ihave: vec![], - iwant: vec![], - graft: vec![proto::ControlGraft { - topic_id: Some(topic_hash.into_string()), - }], - prune: vec![], - idontwant: vec![], - }), - }, - RpcOut::Prune(Prune { - topic_hash, - peers, - backoff, - }) => { - proto::RPC { - publish: Vec::new(), - subscriptions: vec![], - control: Some(proto::ControlMessage { - ihave: vec![], - iwant: vec![], - graft: vec![], - prune: vec![proto::ControlPrune { - topic_id: Some(topic_hash.into_string()), - peers: peers - .into_iter() - .map(|info| proto::PeerInfo { - peer_id: info.peer_id.map(|id| id.to_bytes()), - // TODO, see https://github.com/libp2p/specs/pull/217 - signed_peer_record: None, - }) - .collect(), - backoff, - }], - idontwant: vec![], - }), - } - } - RpcOut::IDontWant(IDontWant { message_ids }) => proto::RPC { - publish: Vec::new(), - subscriptions: Vec::new(), - control: Some(proto::ControlMessage { - ihave: vec![], - iwant: vec![], - graft: vec![], - prune: vec![], - idontwant: vec![proto::ControlIDontWant { - message_ids: message_ids.into_iter().map(|msg_id| msg_id.0).collect(), - }], - }), - }, - } - } -} - -/// An RPC received/sent. -#[derive(Clone, PartialEq, Eq, Hash)] -pub struct Rpc { - /// List of messages that were part of this RPC query. - pub messages: Vec, - /// List of subscriptions. - pub subscriptions: Vec, - /// List of Gossipsub control messages. - pub control_msgs: Vec, -} - -impl Rpc { - /// Converts the GossipsubRPC into its protobuf format. - // A convenience function to avoid explicitly specifying types. - pub fn into_protobuf(self) -> proto::RPC { - self.into() - } -} - -impl From for proto::RPC { - /// Converts the RPC into protobuf format. - fn from(rpc: Rpc) -> Self { - // Messages - let mut publish = Vec::new(); - - for message in rpc.messages.into_iter() { - let message = proto::Message { - from: message.source.map(|m| m.to_bytes()), - data: Some(message.data), - seqno: message.sequence_number.map(|s| s.to_be_bytes().to_vec()), - topic: TopicHash::into_string(message.topic), - signature: message.signature, - key: message.key, - }; - - publish.push(message); - } - - // subscriptions - let subscriptions = rpc - .subscriptions - .into_iter() - .map(|sub| proto::SubOpts { - subscribe: Some(sub.action == SubscriptionAction::Subscribe), - topic_id: Some(sub.topic_hash.into_string()), - }) - .collect::>(); - - // control messages - let mut control = proto::ControlMessage { - ihave: Vec::new(), - iwant: Vec::new(), - graft: Vec::new(), - prune: Vec::new(), - idontwant: Vec::new(), - }; - - let empty_control_msg = rpc.control_msgs.is_empty(); - - for action in rpc.control_msgs { - match action { - // collect all ihave messages - ControlAction::IHave(IHave { - topic_hash, - message_ids, - }) => { - let rpc_ihave = proto::ControlIHave { - topic_id: Some(topic_hash.into_string()), - message_ids: message_ids.into_iter().map(|msg_id| msg_id.0).collect(), - }; - control.ihave.push(rpc_ihave); - } - ControlAction::IWant(IWant { message_ids }) => { - let rpc_iwant = proto::ControlIWant { - message_ids: message_ids.into_iter().map(|msg_id| msg_id.0).collect(), - }; - control.iwant.push(rpc_iwant); - } - ControlAction::Graft(Graft { topic_hash }) => { - let rpc_graft = proto::ControlGraft { - topic_id: Some(topic_hash.into_string()), - }; - control.graft.push(rpc_graft); - } - ControlAction::Prune(Prune { - topic_hash, - peers, - backoff, - }) => { - let rpc_prune = proto::ControlPrune { - topic_id: Some(topic_hash.into_string()), - peers: peers - .into_iter() - .map(|info| proto::PeerInfo { - peer_id: info.peer_id.map(|id| id.to_bytes()), - // TODO, see https://github.com/libp2p/specs/pull/217 - signed_peer_record: None, - }) - .collect(), - backoff, - }; - control.prune.push(rpc_prune); - } - ControlAction::IDontWant(IDontWant { message_ids }) => { - let rpc_idontwant = proto::ControlIDontWant { - message_ids: message_ids.into_iter().map(|msg_id| msg_id.0).collect(), - }; - control.idontwant.push(rpc_idontwant); - } - } - } - - proto::RPC { - subscriptions, - publish, - control: if empty_control_msg { - None - } else { - Some(control) - }, - } - } -} - -impl fmt::Debug for Rpc { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut b = f.debug_struct("GossipsubRpc"); - if !self.messages.is_empty() { - b.field("messages", &self.messages); - } - if !self.subscriptions.is_empty() { - b.field("subscriptions", &self.subscriptions); - } - if !self.control_msgs.is_empty() { - b.field("control_msgs", &self.control_msgs); - } - b.finish() - } -} - -impl PeerKind { - pub fn as_static_ref(&self) -> &'static str { - match self { - Self::NotSupported => "Not Supported", - Self::Floodsub => "Floodsub", - Self::Gossipsub => "Gossipsub v1.0", - Self::Gossipsubv1_1 => "Gossipsub v1.1", - Self::Gossipsubv1_2 => "Gossipsub v1.2", - } - } -} - -impl AsRef for PeerKind { - fn as_ref(&self) -> &str { - self.as_static_ref() - } -} - -impl fmt::Display for PeerKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.as_ref()) - } -} - -/// `RpcOut` sender that is priority aware. -#[derive(Debug, Clone)] -pub(crate) struct RpcSender { - cap: usize, - len: Arc, - pub(crate) priority_sender: Sender, - pub(crate) non_priority_sender: Sender, - priority_receiver: Receiver, - non_priority_receiver: Receiver, -} - -impl RpcSender { - /// Create a RpcSender. - pub(crate) fn new(cap: usize) -> RpcSender { - let (priority_sender, priority_receiver) = async_channel::unbounded(); - let (non_priority_sender, non_priority_receiver) = async_channel::bounded(cap / 2); - let len = Arc::new(AtomicUsize::new(0)); - RpcSender { - cap: cap / 2, - len, - priority_sender, - non_priority_sender, - priority_receiver, - non_priority_receiver, - } - } - - /// Create a new Receiver to the sender. - pub(crate) fn new_receiver(&self) -> RpcReceiver { - RpcReceiver { - priority_len: self.len.clone(), - priority: self.priority_receiver.clone().peekable(), - non_priority: self.non_priority_receiver.clone().peekable(), - } - } - - /// Send a `RpcOut::Graft` message to the `RpcReceiver` - /// this is high priority. - pub(crate) fn graft(&mut self, graft: Graft) { - self.priority_sender - .try_send(RpcOut::Graft(graft)) - .expect("Channel is unbounded and should always be open"); - } - - /// Send a `RpcOut::Prune` message to the `RpcReceiver` - /// this is high priority. - pub(crate) fn prune(&mut self, prune: Prune) { - self.priority_sender - .try_send(RpcOut::Prune(prune)) - .expect("Channel is unbounded and should always be open"); - } - - /// Send a `RpcOut::IHave` message to the `RpcReceiver` - /// this is low priority, if the queue is full an Err is returned. - #[allow(clippy::result_large_err)] - pub(crate) fn ihave(&mut self, ihave: IHave) -> Result<(), RpcOut> { - self.non_priority_sender - .try_send(RpcOut::IHave(ihave)) - .map_err(|err| err.into_inner()) - } - - /// Send a `RpcOut::IHave` message to the `RpcReceiver` - /// this is low priority, if the queue is full an Err is returned. - #[allow(clippy::result_large_err)] - pub(crate) fn iwant(&mut self, iwant: IWant) -> Result<(), RpcOut> { - self.non_priority_sender - .try_send(RpcOut::IWant(iwant)) - .map_err(|err| err.into_inner()) - } - - /// Send a `RpcOut::IWant` message to the `RpcReceiver` - /// this is low priority, if the queue is full an Err is returned. - #[allow(clippy::result_large_err)] - pub(crate) fn idontwant(&mut self, idontwant: IDontWant) -> Result<(), RpcOut> { - self.non_priority_sender - .try_send(RpcOut::IDontWant(idontwant)) - .map_err(|err| err.into_inner()) - } - - /// Send a `RpcOut::Subscribe` message to the `RpcReceiver` - /// this is high priority. - pub(crate) fn subscribe(&mut self, topic: TopicHash) { - self.priority_sender - .try_send(RpcOut::Subscribe(topic)) - .expect("Channel is unbounded and should always be open"); - } - - /// Send a `RpcOut::Unsubscribe` message to the `RpcReceiver` - /// this is high priority. - pub(crate) fn unsubscribe(&mut self, topic: TopicHash) { - self.priority_sender - .try_send(RpcOut::Unsubscribe(topic)) - .expect("Channel is unbounded and should always be open"); - } - - /// Send a `RpcOut::Publish` message to the `RpcReceiver` - /// this is high priority. If message sending fails, an `Err` is returned. - pub(crate) fn publish( - &mut self, - message: RawMessage, - timeout: Duration, - metrics: Option<&mut Metrics>, - ) -> Result<(), ()> { - if self.len.load(Ordering::Relaxed) >= self.cap { - return Err(()); - } - self.priority_sender - .try_send(RpcOut::Publish { - message: message.clone(), - timeout: Delay::new(timeout), - }) - .expect("Channel is unbounded and should always be open"); - self.len.fetch_add(1, Ordering::Relaxed); - - if let Some(m) = metrics { - m.msg_sent(&message.topic, message.raw_protobuf_len()); - } - - Ok(()) - } - - /// Send a `RpcOut::Forward` message to the `RpcReceiver` - /// this is high priority. If the queue is full the message is discarded. - pub(crate) fn forward( - &mut self, - message: RawMessage, - timeout: Duration, - metrics: Option<&mut Metrics>, - ) -> Result<(), ()> { - self.non_priority_sender - .try_send(RpcOut::Forward { - message: message.clone(), - timeout: Delay::new(timeout), - }) - .map_err(|_| ())?; - - if let Some(m) = metrics { - m.msg_sent(&message.topic, message.raw_protobuf_len()); - } - - Ok(()) - } - - /// Returns the current size of the priority queue. - pub(crate) fn priority_len(&self) -> usize { - self.len.load(Ordering::Relaxed) - } - - /// Returns the current size of the non-priority queue. - pub(crate) fn non_priority_len(&self) -> usize { - self.non_priority_sender.len() - } -} - -/// `RpcOut` sender that is priority aware. -#[derive(Debug)] -pub struct RpcReceiver { - /// The maximum length of the priority queue. - pub(crate) priority_len: Arc, - /// The priority queue receiver. - pub(crate) priority: Peekable>, - /// The non priority queue receiver. - pub(crate) non_priority: Peekable>, -} - -impl RpcReceiver { - // Peek the next message in the queues and return it if its timeout has elapsed. - // Returns `None` if there aren't any more messages on the stream or none is stale. - pub(crate) fn poll_stale(&mut self, cx: &mut Context<'_>) -> Poll> { - // Peek priority queue. - let priority = match Pin::new(&mut self.priority).poll_peek_mut(cx) { - Poll::Ready(Some(RpcOut::Publish { - message: _, - ref mut timeout, - })) => { - if Pin::new(timeout).poll(cx).is_ready() { - // Return the message. - let dropped = futures::ready!(self.priority.poll_next_unpin(cx)) - .expect("There should be a message"); - return Poll::Ready(Some(dropped)); - } - Poll::Ready(None) - } - poll => poll, - }; - - let non_priority = match Pin::new(&mut self.non_priority).poll_peek_mut(cx) { - Poll::Ready(Some(RpcOut::Forward { - message: _, - ref mut timeout, - })) => { - if Pin::new(timeout).poll(cx).is_ready() { - // Return the message. - let dropped = futures::ready!(self.non_priority.poll_next_unpin(cx)) - .expect("There should be a message"); - return Poll::Ready(Some(dropped)); - } - Poll::Ready(None) - } - poll => poll, - }; - - match (priority, non_priority) { - (Poll::Ready(None), Poll::Ready(None)) => Poll::Ready(None), - _ => Poll::Pending, - } - } - - /// Poll queues and return true if both are empty. - pub(crate) fn poll_is_empty(&mut self, cx: &mut Context<'_>) -> bool { - matches!( - ( - Pin::new(&mut self.priority).poll_peek(cx), - Pin::new(&mut self.non_priority).poll_peek(cx), - ), - (Poll::Ready(None), Poll::Ready(None)) - ) - } -} - -impl Stream for RpcReceiver { - type Item = RpcOut; - - fn poll_next( - mut self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - // The priority queue is first polled. - if let Poll::Ready(rpc) = Pin::new(&mut self.priority).poll_next(cx) { - if let Some(RpcOut::Publish { .. }) = rpc { - self.priority_len.fetch_sub(1, Ordering::Relaxed); - } - return Poll::Ready(rpc); - } - // Then we poll the non priority. - Pin::new(&mut self.non_priority).poll_next(cx) - } -} diff --git a/beacon_node/lighthouse_network/src/discovery/enr.rs b/beacon_node/lighthouse_network/src/discovery/enr.rs index 80677119546..e70c8047e0c 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr.rs +++ b/beacon_node/lighthouse_network/src/discovery/enr.rs @@ -9,13 +9,13 @@ use crate::NetworkConfig; use alloy_rlp::bytes::Bytes; use libp2p::identity::Keypair; use lighthouse_version::{client_name, version}; -use slog::{debug, warn}; use ssz::{Decode, Encode}; use ssz_types::BitVector; use std::fs::File; use std::io::prelude::*; use std::path::Path; use std::str::FromStr; +use tracing::{debug, warn}; use types::{ChainSpec, EnrForkId, EthSpec}; use super::enr_ext::{EnrExt, QUIC6_ENR_KEY, QUIC_ENR_KEY}; @@ -99,20 +99,19 @@ pub fn use_or_load_enr( enr_key: &CombinedKey, local_enr: &mut Enr, config: &NetworkConfig, - log: &slog::Logger, ) -> Result<(), String> { let enr_f = config.network_dir.join(ENR_FILENAME); if let Ok(mut enr_file) = File::open(enr_f.clone()) { let mut enr_string = String::new(); match enr_file.read_to_string(&mut enr_string) { - Err(_) => debug!(log, "Could not read ENR from file"), + Err(_) => debug!("Could not read ENR from file"), Ok(_) => { match Enr::from_str(&enr_string) { Ok(disk_enr) => { // if the same node id, then we may need to update our sequence number if local_enr.node_id() == disk_enr.node_id() { if compare_enr(local_enr, &disk_enr) { - debug!(log, "ENR loaded from disk"; "file" => ?enr_f); + debug!(file = ?enr_f,"ENR loaded from disk"); // the stored ENR has the same configuration, use it *local_enr = disk_enr; return Ok(()); @@ -125,18 +124,18 @@ pub fn use_or_load_enr( local_enr.set_seq(new_seq_no, enr_key).map_err(|e| { format!("Could not update ENR sequence number: {:?}", e) })?; - debug!(log, "ENR sequence number increased"; "seq" => new_seq_no); + debug!(seq = new_seq_no, "ENR sequence number increased"); } } Err(e) => { - warn!(log, "ENR from file could not be decoded"; "error" => ?e); + warn!(error = ?e,"ENR from file could not be decoded"); } } } } } - save_enr_to_disk(&config.network_dir, local_enr, log); + save_enr_to_disk(&config.network_dir, local_enr); Ok(()) } @@ -150,7 +149,6 @@ pub fn build_or_load_enr( local_key: Keypair, config: &NetworkConfig, enr_fork_id: &EnrForkId, - log: &slog::Logger, spec: &ChainSpec, ) -> Result { // Build the local ENR. @@ -159,7 +157,7 @@ pub fn build_or_load_enr( let enr_key = CombinedKey::from_libp2p(local_key)?; let mut local_enr = build_enr::(&enr_key, config, enr_fork_id, spec)?; - use_or_load_enr(&enr_key, &mut local_enr, config, log)?; + use_or_load_enr(&enr_key, &mut local_enr, config)?; Ok(local_enr) } @@ -314,18 +312,19 @@ pub fn load_enr_from_disk(dir: &Path) -> Result { } /// Saves an ENR to disk -pub fn save_enr_to_disk(dir: &Path, enr: &Enr, log: &slog::Logger) { +pub fn save_enr_to_disk(dir: &Path, enr: &Enr) { let _ = std::fs::create_dir_all(dir); match File::create(dir.join(Path::new(ENR_FILENAME))) .and_then(|mut f| f.write_all(enr.to_base64().as_bytes())) { Ok(_) => { - debug!(log, "ENR written to disk"); + debug!("ENR written to disk"); } Err(e) => { warn!( - log, - "Could not write ENR to file"; "file" => format!("{:?}{:?}",dir, ENR_FILENAME), "error" => %e + file = format!("{:?}{:?}",dir, ENR_FILENAME), + error = %e, + "Could not write ENR to file" ); } } diff --git a/beacon_node/lighthouse_network/src/discovery/mod.rs b/beacon_node/lighthouse_network/src/discovery/mod.rs index 33c7775ae26..ad54c6b8b1f 100644 --- a/beacon_node/lighthouse_network/src/discovery/mod.rs +++ b/beacon_node/lighthouse_network/src/discovery/mod.rs @@ -31,8 +31,8 @@ pub use libp2p::{ SubstreamProtocol, ToSwarm, }, }; +use logging::crit; use lru::LruCache; -use slog::{crit, debug, error, info, trace, warn}; use ssz::Encode; use std::num::NonZeroUsize; use std::{ @@ -45,6 +45,7 @@ use std::{ time::{Duration, Instant}, }; use tokio::sync::mpsc; +use tracing::{debug, error, info, trace, warn}; use types::{ChainSpec, EnrForkId, EthSpec}; mod subnet_predicate; @@ -192,8 +193,6 @@ pub struct Discovery { /// Specifies whether various port numbers should be updated after the discovery service has been started update_ports: UpdatePorts, - /// Logger for the discovery behaviour. - log: slog::Logger, spec: Arc, } @@ -203,11 +202,8 @@ impl Discovery { local_key: Keypair, config: &NetworkConfig, network_globals: Arc>, - log: &slog::Logger, spec: &ChainSpec, ) -> Result { - let log = log.clone(); - let enr_dir = match config.network_dir.to_str() { Some(path) => String::from(path), None => String::from(""), @@ -216,9 +212,11 @@ impl Discovery { let local_enr = network_globals.local_enr.read().clone(); let local_node_id = local_enr.node_id(); - info!(log, "ENR Initialised"; "enr" => local_enr.to_base64(), "seq" => local_enr.seq(), "id"=> %local_enr.node_id(), - "ip4" => ?local_enr.ip4(), "udp4"=> ?local_enr.udp4(), "tcp4" => ?local_enr.tcp4(), "tcp6" => ?local_enr.tcp6(), "udp6" => ?local_enr.udp6(), - "quic4" => ?local_enr.quic4(), "quic6" => ?local_enr.quic6() + info!( + enr = local_enr.to_base64(), seq = local_enr.seq(), id = %local_enr.node_id(), + ip4 = ?local_enr.ip4(), udp4= ?local_enr.udp4(), tcp4 = ?local_enr.tcp4(), tcp6 = ?local_enr.tcp6(), udp6 = ?local_enr.udp6(), + quic4 = ?local_enr.quic4(), quic6 = ?local_enr.quic6(), + "ENR Initialised" ); // convert the keypair into an ENR key @@ -234,22 +232,20 @@ impl Discovery { continue; } debug!( - log, - "Adding node to routing table"; - "node_id" => %bootnode_enr.node_id(), - "peer_id" => %bootnode_enr.peer_id(), - "ip" => ?bootnode_enr.ip4(), - "udp" => ?bootnode_enr.udp4(), - "tcp" => ?bootnode_enr.tcp4(), - "quic" => ?bootnode_enr.quic4() + node_id = %bootnode_enr.node_id(), + peer_id = %bootnode_enr.peer_id(), + ip = ?bootnode_enr.ip4(), + udp = ?bootnode_enr.udp4(), + tcp = ?bootnode_enr.tcp4(), + quic = bootnode_enr.quic4(), + "Adding node to routing table" ); let repr = bootnode_enr.to_string(); let _ = discv5.add_enr(bootnode_enr).map_err(|e| { error!( - log, - "Could not add peer to the local routing table"; - "addr" => repr, - "error" => e.to_string(), + addr = repr, + error = e.to_string(), + "Could not add peer to the local routing table" ) }); } @@ -257,14 +253,14 @@ impl Discovery { // Start the discv5 service and obtain an event stream let event_stream = if !config.disable_discovery { discv5.start().map_err(|e| e.to_string()).await?; - debug!(log, "Discovery service started"); + debug!("Discovery service started"); EventStream::Awaiting(Box::pin(discv5.event_stream())) } else { EventStream::InActive }; if !config.boot_nodes_multiaddr.is_empty() { - info!(log, "Contacting Multiaddr boot-nodes for their ENR"); + info!("Contacting Multiaddr boot-nodes for their ENR"); } // get futures for requesting the Enrs associated to these multiaddr and wait for their @@ -286,26 +282,28 @@ impl Discovery { match result { Ok(enr) => { debug!( - log, - "Adding node to routing table"; - "node_id" => %enr.node_id(), - "peer_id" => %enr.peer_id(), - "ip" => ?enr.ip4(), - "udp" => ?enr.udp4(), - "tcp" => ?enr.tcp4(), - "quic" => ?enr.quic4() + node_id = %enr.node_id(), + peer_id = %enr.peer_id(), + ip4 = ?enr.ip4(), + udp4 = ?enr.udp4(), + tcp4 = ?enr.tcp4(), + quic4 = ?enr.quic4(), + "Adding node to routing table" ); let _ = discv5.add_enr(enr).map_err(|e| { error!( - log, - "Could not add peer to the local routing table"; - "addr" => original_addr.to_string(), - "error" => e.to_string(), + addr = original_addr.to_string(), + error = e.to_string(), + "Could not add peer to the local routing table" ) }); } Err(e) => { - error!(log, "Error getting mapping to ENR"; "multiaddr" => original_addr.to_string(), "error" => e.to_string()) + error!( + multiaddr = original_addr.to_string(), + error = e.to_string(), + "Error getting mapping to ENR" + ) } } } @@ -327,7 +325,6 @@ impl Discovery { event_stream, started: !config.disable_discovery, update_ports, - log, enr_dir, spec: Arc::new(spec.clone()), }) @@ -358,7 +355,7 @@ impl Discovery { } // Immediately start a FindNode query let target_peers = std::cmp::min(FIND_NODE_QUERY_CLOSEST_PEERS, target_peers); - debug!(self.log, "Starting a peer discovery request"; "target_peers" => target_peers ); + debug!(target_peers, "Starting a peer discovery request"); self.find_peer_active = true; self.start_query(QueryType::FindPeers, target_peers, |_| true); } @@ -370,9 +367,8 @@ impl Discovery { return; } trace!( - self.log, - "Starting discovery query for subnets"; - "subnets" => ?subnets_to_discover.iter().map(|s| s.subnet).collect::>() + subnets = ?subnets_to_discover.iter().map(|s| s.subnet).collect::>(), + "Starting discovery query for subnets" ); for subnet in subnets_to_discover { self.add_subnet_query(subnet.subnet, subnet.min_ttl, 0); @@ -386,9 +382,8 @@ impl Discovery { if let Err(e) = self.discv5.add_enr(enr) { debug!( - self.log, - "Could not add peer to the local routing table"; - "error" => %e + error = %e, + "Could not add peer to the local routing table" ) } } @@ -427,7 +422,7 @@ impl Discovery { // replace the global version *self.network_globals.local_enr.write() = self.discv5.local_enr(); // persist modified enr to disk - enr::save_enr_to_disk(Path::new(&self.enr_dir), &self.local_enr(), &self.log); + enr::save_enr_to_disk(Path::new(&self.enr_dir), &self.local_enr()); Ok(true) } @@ -463,7 +458,7 @@ impl Discovery { // replace the global version *self.network_globals.local_enr.write() = self.discv5.local_enr(); // persist modified enr to disk - enr::save_enr_to_disk(Path::new(&self.enr_dir), &self.local_enr(), &self.log); + enr::save_enr_to_disk(Path::new(&self.enr_dir), &self.local_enr()); Ok(true) } @@ -475,7 +470,7 @@ impl Discovery { const IS_TCP: bool = false; if self.discv5.update_local_enr_socket(socket_addr, IS_TCP) { // persist modified enr to disk - enr::save_enr_to_disk(Path::new(&self.enr_dir), &self.local_enr(), &self.log); + enr::save_enr_to_disk(Path::new(&self.enr_dir), &self.local_enr()); } *self.network_globals.local_enr.write() = self.discv5.local_enr(); Ok(()) @@ -561,7 +556,7 @@ impl Discovery { *self.network_globals.local_enr.write() = self.discv5.local_enr(); // persist modified enr to disk - enr::save_enr_to_disk(Path::new(&self.enr_dir), &self.local_enr(), &self.log); + enr::save_enr_to_disk(Path::new(&self.enr_dir), &self.local_enr()); Ok(()) } @@ -575,10 +570,11 @@ impl Discovery { format!("{:?}", enr_fork_id.next_fork_epoch) }; - info!(self.log, "Updating the ENR fork version"; - "fork_digest" => ?enr_fork_id.fork_digest, - "next_fork_version" => ?enr_fork_id.next_fork_version, - "next_fork_epoch" => next_fork_epoch_log, + info!( + fork_digest = ?enr_fork_id.fork_digest, + next_fork_version = ?enr_fork_id.next_fork_version, + next_fork_epoch = next_fork_epoch_log, + "Updating the ENR fork version" ); let _ = self @@ -586,9 +582,8 @@ impl Discovery { .enr_insert::(ETH2_ENR_KEY, &enr_fork_id.as_ssz_bytes().into()) .map_err(|e| { warn!( - self.log, - "Could not update eth2 ENR field"; - "error" => ?e + error = ?e, + "Could not update eth2 ENR field" ) }); @@ -596,7 +591,7 @@ impl Discovery { *self.network_globals.local_enr.write() = self.discv5.local_enr(); // persist modified enr to disk - enr::save_enr_to_disk(Path::new(&self.enr_dir), &self.local_enr(), &self.log); + enr::save_enr_to_disk(Path::new(&self.enr_dir), &self.local_enr()); } // Bans a peer and it's associated seen IP addresses. @@ -642,10 +637,7 @@ impl Discovery { fn add_subnet_query(&mut self, subnet: Subnet, min_ttl: Option, retries: usize) { // remove the entry and complete the query if greater than the maximum search count if retries > MAX_DISCOVERY_RETRY { - debug!( - self.log, - "Subnet peer discovery did not find sufficient peers. Reached max retry limit" - ); + debug!("Subnet peer discovery did not find sufficient peers. Reached max retry limit"); return; } @@ -666,7 +658,7 @@ impl Discovery { } if !found { // update the metrics and insert into the queue. - trace!(self.log, "Queuing subnet query"; "subnet" => ?subnet, "retries" => retries); + trace!(?subnet, retries, "Queuing subnet query"); self.queued_queries.push_back(SubnetQuery { subnet, min_ttl, @@ -737,19 +729,21 @@ impl Discovery { .count(); if peers_on_subnet >= TARGET_SUBNET_PEERS { - debug!(self.log, "Discovery ignored"; - "reason" => "Already connected to desired peers", - "connected_peers_on_subnet" => peers_on_subnet, - "target_subnet_peers" => TARGET_SUBNET_PEERS, + debug!( + reason = "Already connected to desired peers", + connected_peers_on_subnet = peers_on_subnet, + target_subnet_peers = TARGET_SUBNET_PEERS, + "Discovery ignored" ); return false; } let target_peers = TARGET_SUBNET_PEERS.saturating_sub(peers_on_subnet); - trace!(self.log, "Discovery query started for subnet"; - "subnet_query" => ?subnet_query, - "connected_peers_on_subnet" => peers_on_subnet, - "peers_to_find" => target_peers, + trace!( + ?subnet_query, + connected_peers_on_subnet = peers_on_subnet, + peers_to_find = target_peers, + "Discovery query started for subnet" ); filtered_subnets.push(subnet_query.subnet); @@ -760,13 +754,11 @@ impl Discovery { // Only start a discovery query if we have a subnet to look for. if !filtered_subnet_queries.is_empty() { // build the subnet predicate as a combination of the eth2_fork_predicate and the subnet predicate - let subnet_predicate = - subnet_predicate::(filtered_subnets, &self.log, self.spec.clone()); + let subnet_predicate = subnet_predicate::(filtered_subnets, self.spec.clone()); debug!( - self.log, - "Starting grouped subnet query"; - "subnets" => ?filtered_subnet_queries, + subnets = ?filtered_subnet_queries, + "Starting grouped subnet query" ); self.start_query( QueryType::Subnet(filtered_subnet_queries), @@ -790,7 +782,7 @@ impl Discovery { let enr_fork_id = match self.local_enr().eth2() { Ok(v) => v, Err(e) => { - crit!(self.log, "Local ENR has no fork id"; "error" => e); + crit!(error = e, "Local ENR has no fork id"); return; } }; @@ -831,10 +823,10 @@ impl Discovery { self.find_peer_active = false; match query.result { Ok(r) if r.is_empty() => { - debug!(self.log, "Discovery query yielded no results."); + debug!("Discovery query yielded no results."); } Ok(r) => { - debug!(self.log, "Discovery query completed"; "peers_found" => r.len()); + debug!(peers_found = r.len(), "Discovery query completed"); let results = r .into_iter() .map(|enr| { @@ -846,7 +838,7 @@ impl Discovery { return Some(results); } Err(e) => { - warn!(self.log, "Discovery query failed"; "error" => %e); + warn!(error = %e, "Discovery query failed"); } } } @@ -855,13 +847,20 @@ impl Discovery { queries.iter().map(|query| query.subnet).collect(); match query.result { Ok(r) if r.is_empty() => { - debug!(self.log, "Grouped subnet discovery query yielded no results."; "subnets_searched_for" => ?subnets_searched_for); + debug!( + ?subnets_searched_for, + "Grouped subnet discovery query yielded no results." + ); queries.iter().for_each(|query| { self.add_subnet_query(query.subnet, query.min_ttl, query.retries + 1); }) } Ok(r) => { - debug!(self.log, "Peer grouped subnet discovery request completed"; "peers_found" => r.len(), "subnets_searched_for" => ?subnets_searched_for); + debug!( + peers_found = r.len(), + ?subnets_searched_for, + "Peer grouped subnet discovery request completed" + ); let mut mapped_results = HashMap::new(); @@ -888,11 +887,8 @@ impl Discovery { self.add_subnet_query(query.subnet, query.min_ttl, query.retries + 1); // Check the specific subnet against the enr - let subnet_predicate = subnet_predicate::( - vec![query.subnet], - &self.log, - self.spec.clone(), - ); + let subnet_predicate = + subnet_predicate::(vec![query.subnet], self.spec.clone()); r.clone() .into_iter() @@ -941,7 +937,7 @@ impl Discovery { } } Err(e) => { - warn!(self.log,"Grouped subnet discovery query failed"; "subnets_searched_for" => ?subnets_searched_for, "error" => %e); + warn!(?subnets_searched_for, error = %e,"Grouped subnet discovery query failed"); } } } @@ -1020,11 +1016,11 @@ impl NetworkBehaviour for Discovery { if let Poll::Ready(event_stream) = fut.poll_unpin(cx) { match event_stream { Ok(stream) => { - debug!(self.log, "Discv5 event stream ready"); + debug!("Discv5 event stream ready"); self.event_stream = EventStream::Present(stream); } Err(e) => { - slog::crit!(self.log, "Discv5 event stream failed"; "error" => %e); + crit!(error = %e, "Discv5 event stream failed"); self.event_stream = EventStream::InActive; } } @@ -1042,15 +1038,15 @@ impl NetworkBehaviour for Discovery { // log these to see if we are unnecessarily dropping discovered peers /* if enr.eth2() == self.local_enr().eth2() { - trace!(self.log, "Peer found in process of query"; "peer_id" => format!("{}", enr.peer_id()), "tcp_socket" => enr.tcp_socket()); + trace!( "Peer found in process of query"; "peer_id" => format!("{}", enr.peer_id()), "tcp_socket" => enr.tcp_socket()); } else { // this is temporary warning for debugging the DHT - warn!(self.log, "Found peer during discovery not on correct fork"; "peer_id" => format!("{}", enr.peer_id()), "tcp_socket" => enr.tcp_socket()); + warn!( "Found peer during discovery not on correct fork"; "peer_id" => format!("{}", enr.peer_id()), "tcp_socket" => enr.tcp_socket()); } */ } discv5::Event::SocketUpdated(socket_addr) => { - info!(self.log, "Address updated"; "ip" => %socket_addr.ip(), "udp_port" => %socket_addr.port()); + info!(ip = %socket_addr.ip(), udp_port = %socket_addr.port(),"Address updated"); metrics::inc_counter(&metrics::ADDRESS_UPDATE_COUNT); // Discv5 will have updated our local ENR. We save the updated version // to disk. @@ -1062,7 +1058,7 @@ impl NetworkBehaviour for Discovery { self.discv5.update_local_enr_socket(socket_addr, true); } let enr = self.discv5.local_enr(); - enr::save_enr_to_disk(Path::new(&self.enr_dir), &enr, &self.log); + enr::save_enr_to_disk(Path::new(&self.enr_dir), &enr); // update network globals *self.network_globals.local_enr.write() = enr; // A new UDP socket has been detected. @@ -1086,7 +1082,11 @@ impl NetworkBehaviour for Discovery { let addr = ev.addr; let listener_id = ev.listener_id; - trace!(self.log, "Received NewListenAddr event from swarm"; "listener_id" => ?listener_id, "addr" => ?addr); + trace!( + ?listener_id, + ?addr, + "Received NewListenAddr event from swarm" + ); let mut addr_iter = addr.iter(); @@ -1094,7 +1094,7 @@ impl NetworkBehaviour for Discovery { Some(Protocol::Ip4(_)) => match (addr_iter.next(), addr_iter.next()) { (Some(Protocol::Tcp(port)), None) => { if !self.update_ports.tcp4 { - debug!(self.log, "Skipping ENR update"; "multiaddr" => ?addr); + debug!(multiaddr = ?addr, "Skipping ENR update"); return; } @@ -1102,21 +1102,21 @@ impl NetworkBehaviour for Discovery { } (Some(Protocol::Udp(port)), Some(Protocol::QuicV1)) => { if !self.update_ports.quic4 { - debug!(self.log, "Skipping ENR update"; "multiaddr" => ?addr); + debug!(?addr, "Skipping ENR update"); return; } self.update_enr_quic_port(port, false) } _ => { - debug!(self.log, "Encountered unacceptable multiaddr for listening (unsupported transport)"; "addr" => ?addr); + debug!(?addr, "Encountered unacceptable multiaddr for listening (unsupported transport)"); return; } }, Some(Protocol::Ip6(_)) => match (addr_iter.next(), addr_iter.next()) { (Some(Protocol::Tcp(port)), None) => { if !self.update_ports.tcp6 { - debug!(self.log, "Skipping ENR update"; "multiaddr" => ?addr); + debug!(?addr, "Skipping ENR update"); return; } @@ -1124,19 +1124,22 @@ impl NetworkBehaviour for Discovery { } (Some(Protocol::Udp(port)), Some(Protocol::QuicV1)) => { if !self.update_ports.quic6 { - debug!(self.log, "Skipping ENR update"; "multiaddr" => ?addr); + debug!(?addr, "Skipping ENR update"); return; } self.update_enr_quic_port(port, true) } _ => { - debug!(self.log, "Encountered unacceptable multiaddr for listening (unsupported transport)"; "addr" => ?addr); + debug!(?addr, "Encountered unacceptable multiaddr for listening (unsupported transport)"); return; } }, _ => { - debug!(self.log, "Encountered unacceptable multiaddr for listening (no IP)"; "addr" => ?addr); + debug!( + ?addr, + "Encountered unacceptable multiaddr for listening (no IP)" + ); return; } }; @@ -1145,10 +1148,10 @@ impl NetworkBehaviour for Discovery { match attempt_enr_update { Ok(true) => { - info!(self.log, "Updated local ENR"; "enr" => local_enr.to_base64(), "seq" => local_enr.seq(), "id"=> %local_enr.node_id(), "ip4" => ?local_enr.ip4(), "udp4"=> ?local_enr.udp4(), "tcp4" => ?local_enr.tcp4(), "tcp6" => ?local_enr.tcp6(), "udp6" => ?local_enr.udp6()) + info!(enr = local_enr.to_base64(), seq = local_enr.seq(), id = %local_enr.node_id(), ip4 = ?local_enr.ip4(), udp4 = ?local_enr.udp4(), tcp4 = ?local_enr.tcp4(), tcp6 = ?local_enr.tcp6(), udp6 = ?local_enr.udp6(),"Updated local ENR") } Ok(false) => {} // Nothing to do, ENR already configured - Err(e) => warn!(self.log, "Failed to update ENR"; "error" => ?e), + Err(e) => warn!(error = ?e,"Failed to update ENR"), } } _ => { @@ -1171,7 +1174,7 @@ impl Discovery { return; } // set peer as disconnected in discovery DHT - debug!(self.log, "Marking peer disconnected in DHT"; "peer_id" => %peer_id, "error" => %ClearDialError(error)); + debug!(%peer_id, error = %ClearDialError(error),"Marking peer disconnected in DHT"); self.disconnect_peer(&peer_id); } DialError::LocalPeerId { .. } @@ -1179,7 +1182,7 @@ impl Discovery { | DialError::Transport(_) | DialError::WrongPeerId { .. } => { // set peer as disconnected in discovery DHT - debug!(self.log, "Marking peer disconnected in DHT"; "peer_id" => %peer_id, "error" => %ClearDialError(error)); + debug!(%peer_id, error = %ClearDialError(error),"Marking peer disconnected in DHT"); self.disconnect_peer(&peer_id); } DialError::DialPeerConditionFalse(_) | DialError::Aborted => {} @@ -1193,23 +1196,10 @@ mod tests { use super::*; use crate::rpc::methods::{MetaData, MetaDataV2}; use libp2p::identity::secp256k1; - use slog::{o, Drain}; use types::{BitVector, MinimalEthSpec, SubnetId}; type E = MinimalEthSpec; - pub fn build_log(level: slog::Level, enabled: bool) -> slog::Logger { - let decorator = slog_term::TermDecorator::new().build(); - let drain = slog_term::FullFormat::new(decorator).build().fuse(); - let drain = slog_async::Async::new(drain).build().fuse(); - - if enabled { - slog::Logger::root(drain.filter_level(level).fuse(), o!()) - } else { - slog::Logger::root(drain.filter(|_| false).fuse(), o!()) - } - } - async fn build_discovery() -> Discovery { let spec = Arc::new(ChainSpec::default()); let keypair = secp256k1::Keypair::generate(); @@ -1218,7 +1208,6 @@ mod tests { let config = Arc::new(config); let enr_key: CombinedKey = CombinedKey::from_secp256k1(&keypair); let enr: Enr = build_enr::(&enr_key, &config, &EnrForkId::default(), &spec).unwrap(); - let log = build_log(slog::Level::Debug, false); let globals = NetworkGlobals::new( enr, MetaData::V2(MetaDataV2 { @@ -1228,12 +1217,11 @@ mod tests { }), vec![], false, - &log, config.clone(), spec.clone(), ); let keypair = keypair.into(); - Discovery::new(keypair, &config, Arc::new(globals), &log, &spec) + Discovery::new(keypair, &config, Arc::new(globals), &spec) .await .unwrap() } diff --git a/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs b/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs index 400a0c2d562..735ef5b0f28 100644 --- a/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs +++ b/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs @@ -1,22 +1,19 @@ //! The subnet predicate used for searching for a particular subnet. use super::*; use crate::types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield}; -use slog::trace; use std::ops::Deref; +use tracing::trace; use types::data_column_custody_group::compute_subnets_for_node; use types::ChainSpec; /// Returns the predicate for a given subnet. pub fn subnet_predicate( subnets: Vec, - log: &slog::Logger, spec: Arc, ) -> impl Fn(&Enr) -> bool + Send where E: EthSpec, { - let log_clone = log.clone(); - move |enr: &Enr| { let attestation_bitfield: EnrAttestationBitfield = match enr.attestation_bitfield::() { @@ -48,9 +45,8 @@ where if !predicate { trace!( - log_clone, - "Peer found but not on any of the desired subnets"; - "peer_id" => %enr.peer_id() + peer_id = %enr.peer_id(), + "Peer found but not on any of the desired subnets" ); } predicate diff --git a/beacon_node/lighthouse_network/src/lib.rs b/beacon_node/lighthouse_network/src/lib.rs index 98c61bd0687..40fdd71b383 100644 --- a/beacon_node/lighthouse_network/src/lib.rs +++ b/beacon_node/lighthouse_network/src/lib.rs @@ -121,6 +121,6 @@ pub use peer_manager::{ ConnectionDirection, PeerConnectionStatus, PeerInfo, PeerManager, SyncInfo, SyncStatus, }; // pub use service::{load_private_key, Context, Libp2pEvent, Service, NETWORK_KEY_FILENAME}; -pub use service::api_types::{PeerRequestId, Response}; +pub use service::api_types::Response; pub use service::utils::*; pub use service::{Gossipsub, NetworkEvent}; diff --git a/beacon_node/lighthouse_network/src/listen_addr.rs b/beacon_node/lighthouse_network/src/listen_addr.rs index 53f7d9dacae..3b0ff98b34f 100644 --- a/beacon_node/lighthouse_network/src/listen_addr.rs +++ b/beacon_node/lighthouse_network/src/listen_addr.rs @@ -104,25 +104,3 @@ impl ListenAddress { }) } } - -impl slog::KV for ListenAddress { - fn serialize( - &self, - _record: &slog::Record, - serializer: &mut dyn slog::Serializer, - ) -> slog::Result { - if let Some(v4_addr) = self.v4() { - serializer.emit_arguments("ip4_address", &format_args!("{}", v4_addr.addr))?; - serializer.emit_u16("disc4_port", v4_addr.disc_port)?; - serializer.emit_u16("quic4_port", v4_addr.quic_port)?; - serializer.emit_u16("tcp4_port", v4_addr.tcp_port)?; - } - if let Some(v6_addr) = self.v6() { - serializer.emit_arguments("ip6_address", &format_args!("{}", v6_addr.addr))?; - serializer.emit_u16("disc6_port", v6_addr.disc_port)?; - serializer.emit_u16("quic6_port", v6_addr.quic_port)?; - serializer.emit_u16("tcp6_port", v6_addr.tcp_port)?; - } - slog::Result::Ok(()) - } -} diff --git a/beacon_node/lighthouse_network/src/metrics.rs b/beacon_node/lighthouse_network/src/metrics.rs index b36cb8075d1..da986f28841 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 baeb5976768..01cc1611058 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/mod.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/mod.rs @@ -11,12 +11,12 @@ use libp2p::identify::Info as IdentifyInfo; use lru_cache::LRUTimeCache; use peerdb::{BanOperation, BanResult, ScoreUpdateResult}; use rand::seq::SliceRandom; -use slog::{debug, error, trace, warn}; use smallvec::SmallVec; use std::{ sync::Arc, time::{Duration, Instant}, }; +use tracing::{debug, error, trace, warn}; use types::{DataColumnSubnetId, EthSpec, SyncSubnetId}; pub use libp2p::core::Multiaddr; @@ -115,8 +115,6 @@ pub struct PeerManager { /// Keeps track of whether the QUIC protocol is enabled or not. quic_enabled: bool, trusted_peers: HashSet, - /// The logger associated with the `PeerManager`. - log: slog::Logger, } /// The events that the `PeerManager` outputs (requests). @@ -151,7 +149,6 @@ impl PeerManager { pub fn new( cfg: config::Config, network_globals: Arc>, - log: &slog::Logger, ) -> Result { let config::Config { discovery_enabled, @@ -197,7 +194,6 @@ impl PeerManager { metrics_enabled, quic_enabled, trusted_peers: Default::default(), - log: log.clone(), }) } @@ -211,7 +207,7 @@ impl PeerManager { pub fn goodbye_peer(&mut self, peer_id: &PeerId, reason: GoodbyeReason, source: ReportSource) { // Update the sync status if required if let Some(info) = self.network_globals.peers.write().peer_info_mut(peer_id) { - debug!(self.log, "Sending goodbye to peer"; "peer_id" => %peer_id, "reason" => %reason, "score" => %info.score()); + debug!(%peer_id, %reason, score = %info.score(), "Sending goodbye to peer"); if matches!(reason, GoodbyeReason::IrrelevantNetwork) { info.update_sync_status(SyncStatus::IrrelevantPeer); } @@ -371,7 +367,7 @@ impl PeerManager { .update_min_ttl(&peer_id, min_ttl); } if self.dial_peer(enr) { - debug!(self.log, "Added discovered ENR peer to dial queue"; "peer_id" => %peer_id); + debug!(%peer_id, "Added discovered ENR peer to dial queue"); to_dial_peers += 1; } } @@ -384,7 +380,10 @@ impl PeerManager { // reach out target. To prevent the infinite loop, if a query returns no useful peers, we // will cancel the recursiveness and wait for the heartbeat to trigger another query latter. if results_count > 0 && to_dial_peers == 0 { - debug!(self.log, "Skipping recursive discovery query after finding no useful results"; "results" => results_count); + debug!( + results = results_count, + "Skipping recursive discovery query after finding no useful results" + ); metrics::inc_counter(&metrics::DISCOVERY_NO_USEFUL_ENRS); } else { // Queue another discovery if we need to @@ -483,16 +482,21 @@ impl PeerManager { if previous_kind != peer_info.client().kind || *peer_info.listening_addresses() != previous_listening_addresses { - debug!(self.log, "Identified Peer"; "peer" => %peer_id, - "protocol_version" => &info.protocol_version, - "agent_version" => &info.agent_version, - "listening_addresses" => ?info.listen_addrs, - "observed_address" => ?info.observed_addr, - "protocols" => ?info.protocols + debug!( + %peer_id, + protocol_version = &info.protocol_version, + agent_version = &info.agent_version, + listening_addresses = ?info.listen_addrs, + observed_address = ?info.observed_addr, + protocols = ?info.protocols, + "Identified Peer" ); } } else { - error!(self.log, "Received an Identify response from an unknown peer"; "peer_id" => peer_id.to_string()); + error!( + peer_id = peer_id.to_string(), + "Received an Identify response from an unknown peer" + ); } } @@ -508,8 +512,7 @@ impl PeerManager { ) { let client = self.network_globals.client(peer_id); let score = self.network_globals.peers.read().score(peer_id); - debug!(self.log, "RPC Error"; "protocol" => %protocol, "err" => %err, "client" => %client, - "peer_id" => %peer_id, "score" => %score, "direction" => ?direction); + debug!(%protocol, %err, %client, %peer_id, %score, ?direction, "RPC Error"); metrics::inc_counter_vec( &metrics::TOTAL_RPC_ERRORS_PER_CLIENT, &[ @@ -526,7 +529,7 @@ impl PeerManager { PeerAction::MidToleranceError } RPCError::InternalError(e) => { - debug!(self.log, "Internal RPC Error"; "error" => %e, "peer_id" => %peer_id); + debug!(error = %e, %peer_id, "Internal RPC Error"); return; } RPCError::HandlerRejected => PeerAction::Fatal, @@ -619,7 +622,7 @@ impl PeerManager { RPCError::StreamTimeout => match direction { ConnectionDirection::Incoming => { // There was a timeout responding to a peer. - debug!(self.log, "Timed out responding to RPC Request"; "peer_id" => %peer_id); + debug!(%peer_id, "Timed out responding to RPC Request"); return; } ConnectionDirection::Outgoing => match protocol { @@ -658,7 +661,7 @@ impl PeerManager { if let Some(peer_info) = self.network_globals.peers.read().peer_info(peer_id) { // received a ping // reset the to-ping timer for this peer - trace!(self.log, "Received a ping request"; "peer_id" => %peer_id, "seq_no" => seq); + trace!(%peer_id, seq_no = seq, "Received a ping request"); match peer_info.connection_direction() { Some(ConnectionDirection::Incoming) => { self.inbound_ping_peers.insert(*peer_id); @@ -667,26 +670,23 @@ impl PeerManager { self.outbound_ping_peers.insert(*peer_id); } None => { - warn!(self.log, "Received a ping from a peer with an unknown connection direction"; "peer_id" => %peer_id); + warn!(%peer_id, "Received a ping from a peer with an unknown connection direction"); } } // if the sequence number is unknown send an update the meta data of the peer. if let Some(meta_data) = &peer_info.meta_data() { if *meta_data.seq_number() < seq { - trace!(self.log, "Requesting new metadata from peer"; - "peer_id" => %peer_id, "known_seq_no" => meta_data.seq_number(), "ping_seq_no" => seq); + trace!(%peer_id, known_seq_no = meta_data.seq_number(), ping_seq_no = seq, "Requesting new metadata from peer"); self.events.push(PeerManagerEvent::MetaData(*peer_id)); } } else { // if we don't know the meta-data, request it - debug!(self.log, "Requesting first metadata from peer"; - "peer_id" => %peer_id); + debug!(%peer_id, "Requesting first metadata from peer"); self.events.push(PeerManagerEvent::MetaData(*peer_id)); } } else { - error!(self.log, "Received a PING from an unknown peer"; - "peer_id" => %peer_id); + error!(%peer_id, "Received a PING from an unknown peer"); } } @@ -698,48 +698,48 @@ impl PeerManager { // if the sequence number is unknown send update the meta data of the peer. if let Some(meta_data) = &peer_info.meta_data() { if *meta_data.seq_number() < seq { - trace!(self.log, "Requesting new metadata from peer"; - "peer_id" => %peer_id, "known_seq_no" => meta_data.seq_number(), "pong_seq_no" => seq); + trace!(%peer_id, known_seq_no = meta_data.seq_number(), pong_seq_no = seq, "Requesting new metadata from peer"); self.events.push(PeerManagerEvent::MetaData(*peer_id)); } } else { // if we don't know the meta-data, request it - trace!(self.log, "Requesting first metadata from peer"; - "peer_id" => %peer_id); + trace!(%peer_id, "Requesting first metadata from peer"); self.events.push(PeerManagerEvent::MetaData(*peer_id)); } } else { - error!(self.log, "Received a PONG from an unknown peer"; "peer_id" => %peer_id); + error!(%peer_id, "Received a PONG from an unknown peer"); } } /// 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() { if *known_meta_data.seq_number() < *meta_data.seq_number() { - trace!(self.log, "Updating peer's metadata"; - "peer_id" => %peer_id, "known_seq_no" => known_meta_data.seq_number(), "new_seq_no" => meta_data.seq_number()); + trace!(%peer_id, known_seq_no = known_meta_data.seq_number(), new_seq_no = meta_data.seq_number(), "Updating peer's metadata"); } else { - trace!(self.log, "Received old metadata"; - "peer_id" => %peer_id, "known_seq_no" => known_meta_data.seq_number(), "new_seq_no" => meta_data.seq_number()); + trace!(%peer_id, known_seq_no = known_meta_data.seq_number(), new_seq_no = meta_data.seq_number(), "Received old metadata"); // Updating metadata even in this case to prevent storing // incorrect `attnets/syncnets` for a peer } } else { // we have no meta-data for this peer, update - debug!(self.log, "Obtained peer's metadata"; - "peer_id" => %peer_id, "new_seq_no" => meta_data.seq_number()); + 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) => { @@ -751,23 +751,25 @@ impl PeerManager { .cloned() .unwrap_or_else(|| { warn!( - self.log, - "Custody group not found in subnet mapping"; - "custody_index" => custody_index, - "peer_id" => %peer_id + %custody_index, + %peer_id, + "Custody group not found in subnet mapping" ); vec![] }) }) .collect(); peer_info.set_custody_subnets(custody_subnets); + + updated_cgc = Some(custody_group_count) != known_custody_group_count; } Err(err) => { - debug!(self.log, "Unable to compute peer custody groups from metadata"; - "info" => "Sending goodbye to peer", - "peer_id" => %peer_id, - "custody_group_count" => custody_group_count, - "error" => ?err, + debug!( + info = "Sending goodbye to peer", + peer_id = %peer_id, + custody_group_count, + error = ?err, + "Unable to compute peer custody groups from metadata" ); invalid_meta_data = true; } @@ -775,14 +777,15 @@ impl PeerManager { } } } else { - error!(self.log, "Received METADATA from an unknown peer"; - "peer_id" => %peer_id); + error!(%peer_id, "Received METADATA from an unknown peer"); } // Disconnect peers with invalid metadata and find other peers instead. 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. @@ -868,7 +871,7 @@ impl PeerManager { let mut peerdb = self.network_globals.peers.write(); if peerdb.ban_status(peer_id).is_some() { // don't connect if the peer is banned - error!(self.log, "Connection has been allowed to a banned peer"; "peer_id" => %peer_id); + error!(%peer_id, "Connection has been allowed to a banned peer"); } match connection { @@ -936,9 +939,8 @@ impl PeerManager { // request the subnet query from discovery if !subnets_to_discover.is_empty() { debug!( - self.log, - "Making subnet queries for maintaining sync committee peers"; - "subnets" => ?subnets_to_discover.iter().map(|s| s.subnet).collect::>() + subnets = ?subnets_to_discover.iter().map(|s| s.subnet).collect::>(), + "Making subnet queries for maintaining sync committee peers" ); self.events .push(PeerManagerEvent::DiscoverSubnetPeers(subnets_to_discover)); @@ -974,7 +976,13 @@ impl PeerManager { if wanted_peers != 0 { // We need more peers, re-queue a discovery lookup. - debug!(self.log, "Starting a new peer discovery query"; "connected" => peer_count, "target" => self.target_peers, "outbound" => outbound_only_peer_count, "wanted" => wanted_peers); + debug!( + connected = peer_count, + target = self.target_peers, + outbound = outbound_only_peer_count, + wanted = wanted_peers, + "Starting a new peer discovery query" + ); self.events .push(PeerManagerEvent::DiscoverPeers(wanted_peers)); } @@ -1488,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 { @@ -1508,21 +1525,9 @@ enum ConnectingType { #[cfg(test)] mod tests { use super::*; + use crate::rpc::MetaDataV3; use crate::NetworkConfig; - use slog::{o, Drain}; - use types::MainnetEthSpec as E; - - pub fn build_log(level: slog::Level, enabled: bool) -> slog::Logger { - let decorator = slog_term::TermDecorator::new().build(); - let drain = slog_term::FullFormat::new(decorator).build().fuse(); - let drain = slog_async::Async::new(drain).build().fuse(); - - if enabled { - slog::Logger::root(drain.filter_level(level).fuse(), o!()) - } else { - slog::Logger::root(drain.filter(|_| false).fuse(), o!()) - } - } + 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 @@ -1531,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, @@ -1541,10 +1555,8 @@ mod tests { target_peers: target_peer_count, ..Default::default() }); - let log = build_log(slog::Level::Debug, false); - let spec = Arc::new(E::default_spec()); - let globals = NetworkGlobals::new_test_globals(trusted_peers, &log, network_config, spec); - PeerManager::new(config, Arc::new(globals), &log).unwrap() + let globals = NetworkGlobals::new_test_globals(trusted_peers, network_config, spec); + PeerManager::new(config, Arc::new(globals)).unwrap() } #[tokio::test] @@ -1893,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/network_behaviour.rs b/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs index ee2a7461428..1ad55ce5c4a 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs @@ -13,7 +13,7 @@ use libp2p::swarm::dial_opts::{DialOpts, PeerCondition}; use libp2p::swarm::dummy::ConnectionHandler; use libp2p::swarm::{ConnectionDenied, ConnectionId, NetworkBehaviour, ToSwarm}; pub use metrics::{set_gauge_vec, NAT_OPEN}; -use slog::{debug, error, trace}; +use tracing::{debug, error, trace}; use types::EthSpec; use crate::discovery::enr_ext::EnrExt; @@ -54,7 +54,10 @@ impl NetworkBehaviour for PeerManager { self.events.push(PeerManagerEvent::Ping(peer_id)); } Poll::Ready(Some(Err(e))) => { - error!(self.log, "Failed to check for inbound peers to ping"; "error" => e.to_string()) + error!( + error = e.to_string(), + "Failed to check for inbound peers to ping" + ) } Poll::Ready(None) | Poll::Pending => break, } @@ -67,7 +70,10 @@ impl NetworkBehaviour for PeerManager { self.events.push(PeerManagerEvent::Ping(peer_id)); } Poll::Ready(Some(Err(e))) => { - error!(self.log, "Failed to check for outbound peers to ping"; "error" => e.to_string()) + error!( + error = e.to_string(), + "Failed to check for outbound peers to ping" + ) } Poll::Ready(None) | Poll::Pending => break, } @@ -84,7 +90,7 @@ impl NetworkBehaviour for PeerManager { self.events.push(PeerManagerEvent::Status(peer_id)) } Poll::Ready(Some(Err(e))) => { - error!(self.log, "Failed to check for peers to ping"; "error" => e.to_string()) + error!(error = e.to_string(), "Failed to check for peers to ping") } Poll::Ready(None) | Poll::Pending => break, } @@ -109,7 +115,7 @@ impl NetworkBehaviour for PeerManager { ] .concat(); - debug!(self.log, "Dialing peer"; "peer_id"=> %enr.peer_id(), "multiaddrs" => ?multiaddrs); + debug!(peer_id = %enr.peer_id(), ?multiaddrs, "Dialing peer"); return Poll::Ready(ToSwarm::Dial { opts: DialOpts::peer_id(enr.peer_id()) .condition(PeerCondition::Disconnected) @@ -141,7 +147,7 @@ impl NetworkBehaviour for PeerManager { error, connection_id: _, }) => { - debug!(self.log, "Failed to dial peer"; "peer_id"=> ?peer_id, "error" => %ClearDialError(error)); + debug!(?peer_id, error = %ClearDialError(error),"Failed to dial peer"); self.on_dial_failure(peer_id); } _ => { @@ -186,7 +192,7 @@ impl NetworkBehaviour for PeerManager { _local_addr: &libp2p::Multiaddr, remote_addr: &libp2p::Multiaddr, ) -> Result, ConnectionDenied> { - trace!(self.log, "Inbound connection"; "peer_id" => %peer_id, "multiaddr" => %remote_addr); + trace!(%peer_id, multiaddr = %remote_addr, "Inbound connection"); // We already checked if the peer was banned on `handle_pending_inbound_connection`. if self.ban_status(&peer_id).is_some() { return Err(ConnectionDenied::new( @@ -227,9 +233,9 @@ impl NetworkBehaviour for PeerManager { _role_override: libp2p::core::Endpoint, _port_use: PortUse, ) -> Result, libp2p::swarm::ConnectionDenied> { - trace!(self.log, "Outbound connection"; "peer_id" => %peer_id, "multiaddr" => %addr); + trace!(%peer_id, multiaddr = %addr,"Outbound connection"); if let Some(cause) = self.ban_status(&peer_id) { - error!(self.log, "Connected a banned peer. Rejecting connection"; "peer_id" => %peer_id); + error!(%peer_id, "Connected a banned peer. Rejecting connection"); return Err(ConnectionDenied::new(cause)); } @@ -258,9 +264,11 @@ impl PeerManager { endpoint: &ConnectedPoint, _other_established: usize, ) { - debug!(self.log, "Connection established"; "peer_id" => %peer_id, - "multiaddr" => %endpoint.get_remote_address(), - "connection" => ?endpoint.to_endpoint() + debug!( + multiaddr = %endpoint.get_remote_address(), + connection = ?endpoint.to_endpoint(), + %peer_id, + "Connection established" ); // Update the prometheus metrics @@ -309,7 +317,7 @@ impl PeerManager { // Inform the application. self.events .push(PeerManagerEvent::PeerDisconnected(peer_id)); - debug!(self.log, "Peer disconnected"; "peer_id" => %peer_id); + debug!(%peer_id,"Peer disconnected"); } // NOTE: It may be the case that a rejected node, due to too many peers is disconnected diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs index 0912bd1cd24..95a4e82fa21 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs @@ -1,10 +1,12 @@ 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}; use score::{PeerAction, ReportSource, Score, ScoreState}; -use slog::{crit, debug, error, trace, warn}; use std::net::IpAddr; use std::time::Instant; use std::{cmp::Ordering, fmt::Display}; @@ -13,8 +15,9 @@ use std::{ fmt::Formatter, }; 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; @@ -44,19 +47,16 @@ pub struct PeerDB { banned_peers_count: BannedPeersCount, /// Specifies if peer scoring is disabled. disable_peer_scoring: bool, - /// PeerDB's logger - log: slog::Logger, } impl PeerDB { - pub fn new(trusted_peers: Vec, disable_peer_scoring: bool, log: &slog::Logger) -> Self { + pub fn new(trusted_peers: Vec, disable_peer_scoring: bool) -> Self { // Initialize the peers hashmap with trusted peers let peers = trusted_peers .into_iter() .map(|peer_id| (peer_id, PeerInfo::trusted_peer_info())) .collect(); Self { - log: log.clone(), disconnected_peers: 0, banned_peers_count: BannedPeersCount::default(), disable_peer_scoring, @@ -412,15 +412,15 @@ impl PeerDB { // Update scores info.score_update(); - match Self::handle_score_transition(previous_state, peer_id, info, &self.log) { + match Self::handle_score_transition(previous_state, peer_id, info) { // A peer should not be able to be banned from a score update. ScoreTransitionResult::Banned => { - error!(self.log, "Peer has been banned in an update"; "peer_id" => %peer_id) + error!(%peer_id, "Peer has been banned in an update"); } // A peer should not be able to transition to a disconnected state from a healthy // state in a score update. ScoreTransitionResult::Disconnected => { - error!(self.log, "Peer has been disconnected in an update"; "peer_id" => %peer_id) + error!(%peer_id, "Peer has been disconnected in an update"); } ScoreTransitionResult::Unbanned => { peers_to_unban.push(*peer_id); @@ -493,7 +493,7 @@ impl PeerDB { actions.push(( *peer_id, - Self::handle_score_transition(previous_state, peer_id, info, &self.log), + Self::handle_score_transition(previous_state, peer_id, info), )); } @@ -564,15 +564,13 @@ impl PeerDB { &metrics::PEER_ACTION_EVENTS_PER_CLIENT, &[info.client().kind.as_ref(), action.as_ref(), source.into()], ); - let result = - Self::handle_score_transition(previous_state, peer_id, info, &self.log); + let result = Self::handle_score_transition(previous_state, peer_id, info); if previous_state == info.score_state() { debug!( - self.log, - "Peer score adjusted"; - "msg" => %msg, - "peer_id" => %peer_id, - "score" => %info.score() + %msg, + %peer_id, + score = %info.score(), + "Peer score adjusted" ); } match result { @@ -594,10 +592,9 @@ impl PeerDB { ScoreTransitionResult::NoAction => ScoreUpdateResult::NoAction, ScoreTransitionResult::Unbanned => { error!( - self.log, - "Report peer action lead to an unbanning"; - "msg" => %msg, - "peer_id" => %peer_id + %msg, + %peer_id, + "Report peer action lead to an unbanning" ); ScoreUpdateResult::NoAction } @@ -605,10 +602,9 @@ impl PeerDB { } None => { debug!( - self.log, - "Reporting a peer that doesn't exist"; - "msg" => %msg, - "peer_id" =>%peer_id + %msg, + %peer_id, + "Reporting a peer that doesn't exist" ); ScoreUpdateResult::NoAction } @@ -628,7 +624,7 @@ impl PeerDB { .checked_duration_since(Instant::now()) .map(|duration| duration.as_secs()) .unwrap_or_else(|| 0); - debug!(self.log, "Updating the time a peer is required for"; "peer_id" => %peer_id, "future_min_ttl_secs" => min_ttl_secs); + debug!(%peer_id, future_min_ttl_secs = min_ttl_secs, "Updating the time a peer is required for"); } } @@ -652,12 +648,14 @@ impl PeerDB { /// min_ttl than what's given. // VISIBILITY: The behaviour is able to adjust subscriptions. pub(crate) fn extend_peers_on_subnet(&mut self, subnet: &Subnet, min_ttl: Instant) { - let log = &self.log; - self.peers.iter_mut() + self.peers + .iter_mut() .filter(move |(_, info)| { - info.is_connected() && info.on_subnet_metadata(subnet) && info.on_subnet_gossipsub(subnet) + info.is_connected() + && info.on_subnet_metadata(subnet) + && info.on_subnet_gossipsub(subnet) }) - .for_each(|(peer_id,info)| { + .for_each(|(peer_id, info)| { if info.min_ttl().is_none() || Some(&min_ttl) > info.min_ttl() { info.set_min_ttl(min_ttl); } @@ -665,7 +663,7 @@ impl PeerDB { .checked_duration_since(Instant::now()) .map(|duration| duration.as_secs()) .unwrap_or_else(|| 0); - trace!(log, "Updating minimum duration a peer is required for"; "peer_id" => %peer_id, "min_ttl" => min_ttl_secs); + trace!(%peer_id, min_ttl_secs, "Updating minimum duration a peer is required for"); }); } @@ -716,8 +714,8 @@ impl PeerDB { &mut self, supernode: bool, spec: &ChainSpec, + enr_key: CombinedKey, ) -> PeerId { - let enr_key = CombinedKey::generate_secp256k1(); let mut enr = Enr::builder().build(&enr_key).unwrap(); let peer_id = enr.peer_id(); @@ -739,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) @@ -767,7 +778,6 @@ impl PeerDB { peer_id: &PeerId, new_state: NewConnectionState, ) -> Option { - let log_ref = &self.log; let info = self.peers.entry(*peer_id).or_insert_with(|| { // If we are not creating a new connection (or dropping a current inbound connection) log a warning indicating we are updating a // connection state for an unknown peer. @@ -779,8 +789,7 @@ impl PeerDB { | NewConnectionState::Disconnected // Dialing a peer that responds by a different ID can be immediately // disconnected without having being stored in the db before ) { - warn!(log_ref, "Updating state of unknown peer"; - "peer_id" => %peer_id, "new_state" => ?new_state); + warn!(%peer_id, ?new_state, "Updating state of unknown peer"); } if self.disable_peer_scoring { PeerInfo::trusted_peer_info() @@ -795,7 +804,7 @@ impl PeerDB { ScoreState::Banned => {} _ => { // If score isn't low enough to ban, this function has been called incorrectly. - error!(self.log, "Banning a peer with a good score"; "peer_id" => %peer_id); + error!(%peer_id, "Banning a peer with a good score"); info.apply_peer_action_to_score(score::PeerAction::Fatal); } } @@ -826,13 +835,13 @@ impl PeerDB { self.disconnected_peers = self.disconnected_peers.saturating_sub(1); } PeerConnectionStatus::Banned { .. } => { - error!(self.log, "Accepted a connection from a banned peer"; "peer_id" => %peer_id); + error!(%peer_id, "Accepted a connection from a banned peer"); // TODO: check if this happens and report the unban back self.banned_peers_count .remove_banned_peer(info.seen_ip_addresses()); } PeerConnectionStatus::Disconnecting { .. } => { - warn!(self.log, "Connected to a disconnecting peer"; "peer_id" => %peer_id) + warn!(%peer_id, "Connected to a disconnecting peer"); } PeerConnectionStatus::Unknown | PeerConnectionStatus::Connected { .. } @@ -854,7 +863,7 @@ impl PeerDB { (old_state, NewConnectionState::Dialing { enr }) => { match old_state { PeerConnectionStatus::Banned { .. } => { - warn!(self.log, "Dialing a banned peer"; "peer_id" => %peer_id); + warn!(%peer_id, "Dialing a banned peer"); self.banned_peers_count .remove_banned_peer(info.seen_ip_addresses()); } @@ -862,13 +871,13 @@ impl PeerDB { self.disconnected_peers = self.disconnected_peers.saturating_sub(1); } PeerConnectionStatus::Connected { .. } => { - warn!(self.log, "Dialing an already connected peer"; "peer_id" => %peer_id) + warn!(%peer_id, "Dialing an already connected peer"); } PeerConnectionStatus::Dialing { .. } => { - warn!(self.log, "Dialing an already dialing peer"; "peer_id" => %peer_id) + warn!(%peer_id, "Dialing an already dialing peer"); } PeerConnectionStatus::Disconnecting { .. } => { - warn!(self.log, "Dialing a disconnecting peer"; "peer_id" => %peer_id) + warn!(%peer_id, "Dialing a disconnecting peer"); } PeerConnectionStatus::Unknown => {} // default behaviour } @@ -878,7 +887,7 @@ impl PeerDB { } if let Err(e) = info.set_dialing_peer() { - error!(self.log, "{}", e; "peer_id" => %peer_id); + error!(%peer_id, e); } } @@ -934,7 +943,7 @@ impl PeerDB { * Handles the transition to a disconnecting state */ (PeerConnectionStatus::Banned { .. }, NewConnectionState::Disconnecting { to_ban }) => { - error!(self.log, "Disconnecting from a banned peer"; "peer_id" => %peer_id); + error!(%peer_id, "Disconnecting from a banned peer"); info.set_connection_status(PeerConnectionStatus::Disconnecting { to_ban }); } ( @@ -978,13 +987,13 @@ impl PeerDB { (PeerConnectionStatus::Disconnecting { .. }, NewConnectionState::Banned) => { // NOTE: This can occur due a rapid downscore of a peer. It goes through the // disconnection phase and straight into banning in a short time-frame. - debug!(log_ref, "Banning peer that is currently disconnecting"; "peer_id" => %peer_id); + debug!(%peer_id, "Banning peer that is currently disconnecting"); // Ban the peer once the disconnection process completes. info.set_connection_status(PeerConnectionStatus::Disconnecting { to_ban: true }); return Some(BanOperation::PeerDisconnecting); } (PeerConnectionStatus::Banned { .. }, NewConnectionState::Banned) => { - error!(log_ref, "Banning already banned peer"; "peer_id" => %peer_id); + error!(%peer_id, "Banning already banned peer"); let known_banned_ips = self.banned_peers_count.banned_ips(); let banned_ips = info .seen_ip_addresses() @@ -1002,7 +1011,7 @@ impl PeerDB { } (PeerConnectionStatus::Unknown, NewConnectionState::Banned) => { // shift the peer straight to banned - warn!(log_ref, "Banning a peer of unknown connection state"; "peer_id" => %peer_id); + warn!(%peer_id, "Banning a peer of unknown connection state"); self.banned_peers_count .add_banned_peer(info.seen_ip_addresses()); info.set_connection_status(PeerConnectionStatus::Banned { @@ -1023,15 +1032,15 @@ impl PeerDB { */ (old_state, NewConnectionState::Unbanned) => { if matches!(info.score_state(), ScoreState::Banned) { - error!(self.log, "Unbanning a banned peer"; "peer_id" => %peer_id); + error!(%peer_id, "Unbanning a banned peer"); } match old_state { PeerConnectionStatus::Unknown | PeerConnectionStatus::Connected { .. } => { - error!(self.log, "Unbanning a connected peer"; "peer_id" => %peer_id); + error!(%peer_id, "Unbanning a connected peer"); } PeerConnectionStatus::Disconnected { .. } | PeerConnectionStatus::Disconnecting { .. } => { - debug!(self.log, "Unbanning disconnected or disconnecting peer"; "peer_id" => %peer_id); + debug!(%peer_id, "Unbanning disconnected or disconnecting peer"); } // These are odd but fine. PeerConnectionStatus::Dialing { .. } => {} // Also odd but acceptable PeerConnectionStatus::Banned { since } => { @@ -1100,15 +1109,12 @@ impl PeerDB { Some((*id, unbanned_ips)) } else { // If there is no minimum, this is a coding error. - crit!( - self.log, - "banned_peers > MAX_BANNED_PEERS despite no banned peers in db!" - ); + crit!("banned_peers > MAX_BANNED_PEERS despite no banned peers in db!"); // reset banned_peers this will also exit the loop self.banned_peers_count = BannedPeersCount::default(); None } { - debug!(self.log, "Removing old banned peer"; "peer_id" => %to_drop); + debug!(peer_id = %to_drop, "Removing old banned peer"); self.peers.remove(&to_drop); unbanned_peers.push((to_drop, unbanned_ips)) } @@ -1127,7 +1133,11 @@ impl PeerDB { .min_by_key(|(_, age)| *age) .map(|(id, _)| *id) { - debug!(self.log, "Removing old disconnected peer"; "peer_id" => %to_drop, "disconnected_size" => self.disconnected_peers.saturating_sub(1)); + debug!( + peer_id = %to_drop, + disconnected_size = self.disconnected_peers.saturating_sub(1), + "Removing old disconnected peer" + ); self.peers.remove(&to_drop); } // If there is no minimum, this is a coding error. For safety we decrease @@ -1144,15 +1154,19 @@ impl PeerDB { previous_state: ScoreState, peer_id: &PeerId, info: &PeerInfo, - log: &slog::Logger, ) -> ScoreTransitionResult { match (info.score_state(), previous_state) { (ScoreState::Banned, ScoreState::Healthy | ScoreState::ForcedDisconnect) => { - debug!(log, "Peer has been banned"; "peer_id" => %peer_id, "score" => %info.score()); + debug!(%peer_id, score = %info.score(), "Peer has been banned"); ScoreTransitionResult::Banned } (ScoreState::ForcedDisconnect, ScoreState::Banned | ScoreState::Healthy) => { - debug!(log, "Peer transitioned to forced disconnect score state"; "peer_id" => %peer_id, "score" => %info.score(), "past_score_state" => %previous_state); + debug!( + %peer_id, + score = %info.score(), + past_score_state = %previous_state, + "Peer transitioned to forced disconnect score state" + ); // disconnect the peer if it's currently connected or dialing if info.is_connected_or_dialing() { ScoreTransitionResult::Disconnected @@ -1165,11 +1179,21 @@ impl PeerDB { } } (ScoreState::Healthy, ScoreState::ForcedDisconnect) => { - debug!(log, "Peer transitioned to healthy score state"; "peer_id" => %peer_id, "score" => %info.score(), "past_score_state" => %previous_state); + debug!( + %peer_id, + score = %info.score(), + past_score_state = %previous_state, + "Peer transitioned to healthy score state" + ); ScoreTransitionResult::NoAction } (ScoreState::Healthy, ScoreState::Banned) => { - debug!(log, "Peer transitioned to healthy score state"; "peer_id" => %peer_id, "score" => %info.score(), "past_score_state" => %previous_state); + debug!( + %peer_id, + score = %info.score(), + past_score_state = %previous_state, + "Peer transitioned to healthy score state" + ); // unban the peer if it was previously banned. ScoreTransitionResult::Unbanned } @@ -1336,24 +1360,11 @@ impl BannedPeersCount { mod tests { use super::*; use libp2p::core::multiaddr::Protocol; - use slog::{o, Drain}; use std::net::{Ipv4Addr, Ipv6Addr}; use types::MinimalEthSpec; type M = MinimalEthSpec; - pub fn build_log(level: slog::Level, enabled: bool) -> slog::Logger { - let decorator = slog_term::TermDecorator::new().build(); - let drain = slog_term::FullFormat::new(decorator).build().fuse(); - let drain = slog_async::Async::new(drain).build().fuse(); - - if enabled { - slog::Logger::root(drain.filter_level(level).fuse(), o!()) - } else { - slog::Logger::root(drain.filter(|_| false).fuse(), o!()) - } - } - fn add_score(db: &mut PeerDB, peer_id: &PeerId, score: f64) { if let Some(info) = db.peer_info_mut(peer_id) { info.add_to_score(score); @@ -1367,8 +1378,7 @@ mod tests { } fn get_db() -> PeerDB { - let log = build_log(slog::Level::Debug, false); - PeerDB::new(vec![], false, &log) + PeerDB::new(vec![], false) } #[test] @@ -2066,8 +2076,7 @@ mod tests { #[allow(clippy::float_cmp)] fn test_trusted_peers_score() { let trusted_peer = PeerId::random(); - let log = build_log(slog::Level::Debug, false); - let mut pdb: PeerDB = PeerDB::new(vec![trusted_peer], false, &log); + let mut pdb: PeerDB = PeerDB::new(vec![trusted_peer], false); pdb.connect_ingoing(&trusted_peer, "/ip4/0.0.0.0".parse().unwrap(), None); @@ -2090,8 +2099,7 @@ mod tests { #[test] fn test_disable_peer_scoring() { let peer = PeerId::random(); - let log = build_log(slog::Level::Debug, false); - let mut pdb: PeerDB = PeerDB::new(vec![], true, &log); + let mut pdb: PeerDB = PeerDB::new(vec![], true); pdb.connect_ingoing(&peer, "/ip4/0.0.0.0".parse().unwrap(), None); diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs index 05a29ac47e5..4c47df63437 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs @@ -4,6 +4,7 @@ use super::sync_status::SyncStatus; use crate::discovery::Eth2Enr; use crate::{rpc::MetaData, types::Subnet}; use discv5::Enr; +use eth2::types::{PeerDirection, PeerState}; use libp2p::core::multiaddr::{Multiaddr, Protocol}; use serde::{ ser::{SerializeStruct, Serializer}, @@ -234,6 +235,11 @@ impl PeerInfo { self.custody_subnets.contains(subnet) } + /// Returns an iterator on this peer's custody subnets + pub fn custody_subnets_iter(&self) -> impl Iterator { + self.custody_subnets.iter() + } + /// Returns true if the peer is connected to a long-lived subnet. pub fn has_long_lived_subnet(&self) -> bool { // Check the meta_data @@ -517,7 +523,7 @@ impl PeerInfo { } /// Connection Direction of connection. -#[derive(Debug, Clone, Serialize, AsRefStr)] +#[derive(Debug, Clone, Copy, Serialize, AsRefStr)] #[strum(serialize_all = "snake_case")] pub enum ConnectionDirection { /// The connection was established by a peer dialing us. @@ -526,6 +532,15 @@ pub enum ConnectionDirection { Outgoing, } +impl From for PeerDirection { + fn from(direction: ConnectionDirection) -> Self { + match direction { + ConnectionDirection::Incoming => PeerDirection::Inbound, + ConnectionDirection::Outgoing => PeerDirection::Outbound, + } + } +} + /// Connection Status of the peer. #[derive(Debug, Clone, Default)] pub enum PeerConnectionStatus { @@ -619,3 +634,14 @@ impl Serialize for PeerConnectionStatus { } } } + +impl From for PeerState { + fn from(status: PeerConnectionStatus) -> Self { + match status { + Connected { .. } => PeerState::Connected, + Dialing { .. } => PeerState::Connecting, + Disconnecting { .. } => PeerState::Disconnecting, + Disconnected { .. } | Banned { .. } | Unknown => PeerState::Disconnected, + } + } +} diff --git a/beacon_node/lighthouse_network/src/rpc/codec.rs b/beacon_node/lighthouse_network/src/rpc/codec.rs index b3239fa6cb5..2612172e61b 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec.rs @@ -765,8 +765,8 @@ fn handle_rpc_response( SupportedProtocol::PingV1 => Ok(Some(RpcSuccessResponse::Pong(Ping { data: u64::from_ssz_bytes(decoded_buffer)?, }))), - SupportedProtocol::MetaDataV1 => Ok(Some(RpcSuccessResponse::MetaData(MetaData::V1( - MetaDataV1::from_ssz_bytes(decoded_buffer)?, + SupportedProtocol::MetaDataV1 => Ok(Some(RpcSuccessResponse::MetaData(Arc::new( + MetaData::V1(MetaDataV1::from_ssz_bytes(decoded_buffer)?), )))), SupportedProtocol::LightClientBootstrapV1 => match fork_name { Some(fork_name) => Ok(Some(RpcSuccessResponse::LightClientBootstrap(Arc::new( @@ -826,11 +826,11 @@ fn handle_rpc_response( )), }, // MetaData V2/V3 responses have no context bytes, so behave similarly to V1 responses - SupportedProtocol::MetaDataV3 => Ok(Some(RpcSuccessResponse::MetaData(MetaData::V3( - MetaDataV3::from_ssz_bytes(decoded_buffer)?, + SupportedProtocol::MetaDataV3 => Ok(Some(RpcSuccessResponse::MetaData(Arc::new( + MetaData::V3(MetaDataV3::from_ssz_bytes(decoded_buffer)?), )))), - SupportedProtocol::MetaDataV2 => Ok(Some(RpcSuccessResponse::MetaData(MetaData::V2( - MetaDataV2::from_ssz_bytes(decoded_buffer)?, + SupportedProtocol::MetaDataV2 => Ok(Some(RpcSuccessResponse::MetaData(Arc::new( + MetaData::V2(MetaDataV2::from_ssz_bytes(decoded_buffer)?), )))), SupportedProtocol::BlocksByRangeV2 => match fork_name { Some(ForkName::Altair) => Ok(Some(RpcSuccessResponse::BlocksByRange(Arc::new( @@ -1005,6 +1005,7 @@ mod tests { fn bellatrix_block_small(spec: &ChainSpec) -> SignedBeaconBlock { let mut block: BeaconBlockBellatrix<_, FullPayload> = BeaconBlockBellatrix::empty(&Spec::default_spec()); + let tx = VariableList::from(vec![0; 1024]); let txs = VariableList::from(std::iter::repeat_n(tx, 5000).collect::>()); @@ -1021,6 +1022,7 @@ mod tests { fn bellatrix_block_large(spec: &ChainSpec) -> SignedBeaconBlock { let mut block: BeaconBlockBellatrix<_, FullPayload> = BeaconBlockBellatrix::empty(&Spec::default_spec()); + let tx = VariableList::from(vec![0; 1024]); let txs = VariableList::from(std::iter::repeat_n(tx, 100000).collect::>()); @@ -1099,28 +1101,31 @@ mod tests { Ping { data: 1 } } - fn metadata() -> MetaData { + fn metadata() -> Arc> { MetaData::V1(MetaDataV1 { seq_number: 1, attnets: EnrAttestationBitfield::::default(), }) + .into() } - fn metadata_v2() -> MetaData { + fn metadata_v2() -> Arc> { MetaData::V2(MetaDataV2 { seq_number: 1, attnets: EnrAttestationBitfield::::default(), syncnets: EnrSyncCommitteeBitfield::::default(), }) + .into() } - fn metadata_v3() -> MetaData { + fn metadata_v3() -> Arc> { MetaData::V3(MetaDataV3 { seq_number: 1, attnets: EnrAttestationBitfield::::default(), syncnets: EnrSyncCommitteeBitfield::::default(), custody_group_count: 1, }) + .into() } /// Encodes the given protocol response as bytes. diff --git a/beacon_node/lighthouse_network/src/rpc/handler.rs b/beacon_node/lighthouse_network/src/rpc/handler.rs index 03203fcade0..33c5521c3bc 100644 --- a/beacon_node/lighthouse_network/src/rpc/handler.rs +++ b/beacon_node/lighthouse_network/src/rpc/handler.rs @@ -4,8 +4,7 @@ use super::methods::{GoodbyeReason, RpcErrorResponse, RpcResponse}; use super::outbound::OutboundRequestContainer; use super::protocol::{InboundOutput, Protocol, RPCError, RPCProtocol, RequestType}; -use super::RequestId; -use super::{RPCReceived, RPCSend, ReqId, Request}; +use super::{RPCReceived, RPCSend, ReqId}; use crate::rpc::outbound::OutboundFramed; use crate::rpc::protocol::InboundFramed; use fnv::FnvHashMap; @@ -15,8 +14,9 @@ use libp2p::swarm::handler::{ ConnectionEvent, ConnectionHandler, ConnectionHandlerEvent, DialUpgradeError, FullyNegotiatedInbound, FullyNegotiatedOutbound, StreamUpgradeError, SubstreamProtocol, }; -use libp2p::swarm::Stream; -use slog::{crit, debug, trace}; +use libp2p::swarm::{ConnectionId, Stream}; +use libp2p::PeerId; +use logging::crit; use smallvec::SmallVec; use std::{ collections::{hash_map::Entry, VecDeque}, @@ -27,6 +27,7 @@ use std::{ }; use tokio::time::{sleep, Sleep}; use tokio_util::time::{delay_queue, DelayQueue}; +use tracing::{debug, trace}; use types::{EthSpec, ForkContext}; /// The number of times to retry an outbound upgrade in the case of IO errors. @@ -89,6 +90,11 @@ pub struct RPCHandler where E: EthSpec, { + /// The PeerId matching this `ConnectionHandler`. + peer_id: PeerId, + + /// The ConnectionId matching this `ConnectionHandler`. + connection_id: ConnectionId, /// The upgrade for inbound substreams. listen_protocol: SubstreamProtocol, ()>, @@ -135,10 +141,7 @@ where /// Waker, to be sure the handler gets polled when needed. waker: Option, - /// Logger for handling RPC streams - log: slog::Logger, - - /// Timeout that will me used for inbound and outbound responses. + /// Timeout that will be used for inbound and outbound responses. resp_timeout: Duration, } @@ -221,10 +224,13 @@ where pub fn new( listen_protocol: SubstreamProtocol, ()>, fork_context: Arc, - log: &slog::Logger, resp_timeout: Duration, + peer_id: PeerId, + connection_id: ConnectionId, ) -> Self { RPCHandler { + connection_id, + peer_id, listen_protocol, events_out: SmallVec::new(), dial_queue: SmallVec::new(), @@ -240,7 +246,6 @@ where outbound_io_error_retries: 0, fork_context, waker: None, - log: log.clone(), resp_timeout, } } @@ -250,7 +255,12 @@ where fn shutdown(&mut self, goodbye_reason: Option<(Id, GoodbyeReason)>) { if matches!(self.state, HandlerState::Active) { if !self.dial_queue.is_empty() { - debug!(self.log, "Starting handler shutdown"; "unsent_queued_requests" => self.dial_queue.len()); + debug!( + unsent_queued_requests = self.dial_queue.len(), + peer_id = %self.peer_id, + connection_id = %self.connection_id, + "Starting handler shutdown" + ); } // We now drive to completion communications already dialed/established while let Some((id, req)) = self.dial_queue.pop() { @@ -297,11 +307,14 @@ where let Some(inbound_info) = self.inbound_substreams.get_mut(&inbound_id) else { if !matches!(response, RpcResponse::StreamTermination(..)) { // the stream is closed after sending the expected number of responses - trace!(self.log, "Inbound stream has expired. Response not sent"; - "response" => %response, "id" => inbound_id); + trace!(%response, id = ?inbound_id, + peer_id = %self.peer_id, + connection_id = %self.connection_id, + "Inbound stream has expired. Response not sent"); } 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 { @@ -313,10 +326,13 @@ where if matches!(self.state, HandlerState::Deactivated) { // we no longer send responses after the handler is deactivated - debug!(self.log, "Response not sent. Deactivated handler"; - "response" => %response, "id" => inbound_id); + debug!(%response, id = ?inbound_id, + peer_id = %self.peer_id, + connection_id = %self.connection_id, + "Response not sent. Deactivated handler"); return; } + inbound_info.pending_items.push_back(response); } } @@ -381,7 +397,11 @@ where match delay.as_mut().poll(cx) { Poll::Ready(_) => { self.state = HandlerState::Deactivated; - debug!(self.log, "Shutdown timeout elapsed, Handler deactivated"); + debug!( + peer_id = %self.peer_id, + connection_id = %self.connection_id, + "Shutdown timeout elapsed, Handler deactivated" + ); return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour( HandlerEvent::Close(RPCError::Disconnected), )); @@ -428,7 +448,10 @@ where outbound_err, ))); } else { - crit!(self.log, "timed out substream not in the books"; "stream_id" => outbound_id.get_ref()); + crit!( + peer_id = %self.peer_id, + connection_id = %self.connection_id, + stream_id = ?outbound_id.get_ref(), "timed out substream not in the books"); } } @@ -557,10 +580,24 @@ where // BlocksByRange is the one that typically consumes the most time. // Its useful to log when the request was completed. if matches!(info.protocol, Protocol::BlocksByRange) { - debug!(self.log, "BlocksByRange Response sent"; "duration" => Instant::now().duration_since(info.request_start_time).as_secs()); + debug!( + peer_id = %self.peer_id, + connection_id = %self.connection_id, + duration = Instant::now() + .duration_since(info.request_start_time) + .as_secs(), + "BlocksByRange Response sent" + ); } if matches!(info.protocol, Protocol::BlobsByRange) { - debug!(self.log, "BlobsByRange Response sent"; "duration" => Instant::now().duration_since(info.request_start_time).as_secs()); + debug!( + peer_id = %self.peer_id, + connection_id = %self.connection_id, + duration = Instant::now() + .duration_since(info.request_start_time) + .as_secs(), + "BlobsByRange Response sent" + ); } // There is nothing more to process on this substream as it has @@ -583,10 +620,20 @@ where })); if matches!(info.protocol, Protocol::BlocksByRange) { - debug!(self.log, "BlocksByRange Response failed"; "duration" => info.request_start_time.elapsed().as_secs()); + debug!( + peer_id = %self.peer_id, + connection_id = %self.connection_id, + duration = info.request_start_time.elapsed().as_secs(), + "BlocksByRange Response failed" + ); } if matches!(info.protocol, Protocol::BlobsByRange) { - debug!(self.log, "BlobsByRange Response failed"; "duration" => info.request_start_time.elapsed().as_secs()); + debug!( + peer_id = %self.peer_id, + connection_id = %self.connection_id, + duration = info.request_start_time.elapsed().as_secs(), + "BlobsByRange Response failed" + ); } break; } @@ -695,7 +742,7 @@ where // stream closed // if we expected multiple streams send a stream termination, // else report the stream terminating only. - //trace!(self.log, "RPC Response - stream closed by remote"); + //"RPC Response - stream closed by remote"); // drop the stream let delay_key = &entry.get().delay_key; let request_id = entry.get().req_id; @@ -772,7 +819,11 @@ where } } OutboundSubstreamState::Poisoned => { - crit!(self.log, "Poisoned outbound substream"); + crit!( + peer_id = %self.peer_id, + connection_id = %self.connection_id, + "Poisoned outbound substream" + ); unreachable!("Coding Error: Outbound substream is poisoned") } } @@ -804,7 +855,11 @@ where && self.events_out.is_empty() && self.dial_negotiated == 0 { - debug!(self.log, "Goodbye sent, Handler deactivated"); + debug!( + peer_id = %self.peer_id, + connection_id = %self.connection_id, + "Goodbye sent, Handler deactivated" + ); self.state = HandlerState::Deactivated; return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour( HandlerEvent::Close(RPCError::Disconnected), @@ -935,12 +990,13 @@ where self.shutdown(None); } - self.events_out - .push(HandlerEvent::Ok(RPCReceived::Request(Request { - id: RequestId::next(), + self.events_out.push(HandlerEvent::Ok(RPCReceived::Request( + super::InboundRequestId { + connection_id: self.connection_id, substream_id: self.current_inbound_substream_id, - r#type: req, - }))); + }, + req, + ))); self.current_inbound_substream_id.0 += 1; } @@ -997,7 +1053,10 @@ where ) .is_some() { - crit!(self.log, "Duplicate outbound substream id"; "id" => self.current_outbound_substream_id); + crit!( + peer_id = %self.peer_id, + connection_id = %self.connection_id, + id = ?self.current_outbound_substream_id, "Duplicate outbound substream id"); } self.current_outbound_substream_id.0 += 1; } @@ -1045,17 +1104,6 @@ where } } -impl slog::Value for SubstreamId { - fn serialize( - &self, - record: &slog::Record, - key: slog::Key, - serializer: &mut dyn slog::Serializer, - ) -> slog::Result { - slog::Value::serialize(&self.0, record, key, serializer) - } -} - /// Creates a future that can be polled that will send any queued message to the peer. /// /// This function returns the given substream, along with whether it has been closed or not. Any diff --git a/beacon_node/lighthouse_network/src/rpc/methods.rs b/beacon_node/lighthouse_network/src/rpc/methods.rs index 2f6200a836b..e6939e36d8e 100644 --- a/beacon_node/lighthouse_network/src/rpc/methods.rs +++ b/beacon_node/lighthouse_network/src/rpc/methods.rs @@ -578,7 +578,7 @@ pub enum RpcSuccessResponse { Pong(Ping), /// A response to a META_DATA request. - MetaData(MetaData), + MetaData(Arc>), } /// Indicates which response is being terminated by a stream termination response. @@ -606,6 +606,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)] @@ -864,19 +878,3 @@ impl std::fmt::Display for DataColumnsByRootRequest { ) } } - -impl slog::KV for StatusMessage { - fn serialize( - &self, - record: &slog::Record, - serializer: &mut dyn slog::Serializer, - ) -> slog::Result { - use slog::Value; - serializer.emit_arguments("fork_digest", &format_args!("{:?}", self.fork_digest))?; - Value::serialize(&self.finalized_epoch, record, "finalized_epoch", serializer)?; - serializer.emit_arguments("finalized_root", &format_args!("{}", self.finalized_root))?; - Value::serialize(&self.head_slot, record, "head_slot", serializer)?; - serializer.emit_arguments("head_root", &format_args!("{}", self.head_root))?; - slog::Result::Ok(()) - } -} diff --git a/beacon_node/lighthouse_network/src/rpc/mod.rs b/beacon_node/lighthouse_network/src/rpc/mod.rs index 0e7686175a2..8cb720132a8 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 rate_limiter::{RPCRateLimiter as RateLimiter, RateLimitedErr}; -use slog::{crit, debug, o, trace}; +use std::collections::HashMap; use std::marker::PhantomData; -use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use std::task::{Context, Poll}; use std::time::Duration; +use tracing::{debug, error, instrument, trace}; use types::{EthSpec, ForkContext}; pub(crate) use handler::{HandlerErr, HandlerEvent}; @@ -28,6 +26,11 @@ 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, @@ -35,10 +38,6 @@ pub use methods::{ }; pub use protocol::{Protocol, RPCError}; -use self::config::{InboundRateLimiterConfig, OutboundRateLimiterConfig}; -use self::protocol::RPCProtocol; -use self::self_limiter::SelfRateLimiter; - pub(crate) mod codec; pub mod config; mod handler; @@ -46,9 +45,11 @@ pub mod methods; mod outbound; mod protocol; mod rate_limiter; +mod response_limiter; mod self_limiter; -static NEXT_REQUEST_ID: AtomicUsize = AtomicUsize::new(1); +// 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 {} @@ -79,7 +80,7 @@ pub enum RPCReceived { /// /// The `SubstreamId` is given by the `RPCHandler` as it identifies this request with the /// *inbound* substream over which it is managed. - Request(Request), + Request(InboundRequestId, RequestType), /// A response received from the outside. /// /// The `Id` corresponds to the application given ID of the original request sent to the @@ -90,35 +91,30 @@ pub enum RPCReceived { EndOfStream(Id, ResponseTermination), } -/// Rpc `Request` identifier. -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] -pub struct RequestId(usize); - -impl RequestId { - /// Returns the next available [`RequestId`]. - pub fn next() -> Self { - Self(NEXT_REQUEST_ID.fetch_add(1, Ordering::SeqCst)) - } +// An identifier for the inbound requests received via Rpc. +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub struct InboundRequestId { + /// The connection ID of the peer that sent the request. + connection_id: ConnectionId, + /// The ID of the substream that sent the request. + substream_id: SubstreamId, +} - /// Creates an _unchecked_ [`RequestId`]. +impl InboundRequestId { + /// Creates an _unchecked_ [`InboundRequestId`]. /// - /// [`Rpc`] enforces that [`RequestId`]s are unique and not reused. + /// [`Rpc`] enforces that [`InboundRequestId`]s are unique and not reused. /// This constructor does not, hence the _unchecked_. /// /// It is primarily meant for allowing manual tests. - pub fn new_unchecked(id: usize) -> Self { - Self(id) + pub fn new_unchecked(connection_id: usize, substream_id: usize) -> Self { + Self { + connection_id: ConnectionId::new_unchecked(connection_id), + substream_id: SubstreamId::new(substream_id), + } } } -/// An Rpc Request. -#[derive(Debug, Clone)] -pub struct Request { - pub id: RequestId, - pub substream_id: SubstreamId, - pub r#type: RequestType, -} - impl std::fmt::Display for RPCSend { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -135,7 +131,7 @@ pub struct RPCMessage { /// The peer that sent the message. pub peer_id: PeerId, /// Handler managing this message. - pub conn_id: ConnectionId, + pub connection_id: ConnectionId, /// The message that was sent. pub message: Result, HandlerErr>, } @@ -151,16 +147,16 @@ 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, enable_light_client_server: bool, - /// Slog logger for RPC behaviour. - log: slog::Logger, /// Networking constant values network_params: NetworkParams, /// A sequential counter indicating when data gets modified. @@ -168,35 +164,37 @@ pub struct RPC { } impl RPC { + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p_rpc"), + name = "libp2p_rpc", + skip_all + )] pub fn new( fork_context: Arc, enable_light_client_server: bool, inbound_rate_limiter_config: Option, outbound_rate_limiter_config: Option, - log: slog::Logger, network_params: NetworkParams, seq_number: u64, ) -> Self { - let log = log.new(o!("service" => "libp2p_rpc")); - - let inbound_limiter = inbound_rate_limiter_config.map(|config| { - debug!(log, "Using inbound rate limiting params"; "config" => ?config); - 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(), log.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, - log, network_params, seq_number, } @@ -205,45 +203,96 @@ impl RPC { /// Sends an RPC response. /// /// The peer must be connected for this to succeed. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p_rpc"), + name = "libp2p_rpc", + skip_all + )] pub fn send_response( &mut self, peer_id: PeerId, - id: (ConnectionId, SubstreamId), - _request_id: RequestId, - event: RpcResponse, + 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(id.0), - event: RPCSend::Response(id.1, event), + handler: NotifyHandler::One(request_id.connection_id), + event: RPCSend::Response(request_id.substream_id, response), }); } /// Submits an RPC request. /// /// The peer must be connected for this to succeed. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p_rpc"), + name = "libp2p_rpc", + skip_all + )] pub fn send_request(&mut self, peer_id: PeerId, request_id: Id, req: RequestType) { - 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; - } - } - } else { - ToSwarm::NotifyHandler { + match self + .outbound_request_limiter + .allows(peer_id, request_id, req) + { + Ok(event) => self.events.push(BehaviourAction::NotifyHandler { peer_id, handler: NotifyHandler::Any, - event: RPCSend::Request(request_id, req), + event, + }), + Err(_e) => { + // Request is logged and queued internally in the self rate limiter. } - }; - - self.events.push(event); + } } /// Lighthouse wishes to disconnect from this peer by sending a Goodbye message. This /// gracefully terminates the RPC behaviour with a goodbye message. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p_rpc"), + name = "libp2p_rpc", + skip_all + )] pub fn shutdown(&mut self, peer_id: PeerId, id: Id, reason: GoodbyeReason) { self.events.push(ToSwarm::NotifyHandler { peer_id, @@ -252,16 +301,28 @@ impl RPC { }); } + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p_rpc"), + name = "libp2p_rpc", + skip_all + )] pub fn update_seq_number(&mut self, seq_number: u64) { self.seq_number = seq_number } /// Send a Ping request to the destination `PeerId` via `ConnectionId`. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p_rpc"), + name = "libp2p_rpc", + skip_all + )] pub fn ping(&mut self, peer_id: PeerId, id: Id) { let ping = Ping { data: self.seq_number, }; - trace!(self.log, "Sending Ping"; "peer_id" => %peer_id); + trace!(%peer_id, "Sending Ping"); self.send_request(peer_id, id, RequestType::Ping(ping)); } } @@ -291,14 +352,13 @@ where }, (), ); - let log = self - .log - .new(slog::o!("peer_id" => peer_id.to_string(), "connection_id" => connection_id.to_string())); + let handler = RPCHandler::new( protocol, self.fork_context.clone(), - &log, self.network_params.resp_timeout, + peer_id, + connection_id, ); Ok(handler) @@ -323,15 +383,12 @@ where (), ); - let log = self - .log - .new(slog::o!("peer_id" => peer_id.to_string(), "connection_id" => connection_id.to_string())); - let handler = RPCHandler::new( protocol, self.fork_context.clone(), - &log, self.network_params.resp_timeout, + peer_id, + connection_id, ); Ok(handler) @@ -355,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, - conn_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 @@ -381,7 +445,7 @@ where } if *p == peer_id => { *event = ToSwarm::GenerateEvent(RPCMessage { peer_id, - conn_id: connection_id, + connection_id, message: Err(HandlerErr::Outbound { id: *request_id, proto: req.versioned_protocol().protocol(), @@ -397,76 +461,50 @@ where fn on_connection_handler_event( &mut self, peer_id: PeerId, - conn_id: ConnectionId, + connection_id: ConnectionId, event: ::ToBehaviour, ) { match event { - HandlerEvent::Ok(RPCReceived::Request(Request { - id, - substream_id, - r#type, - })) => { - if let Some(limiter) = self.limiter.as_mut() { - // check if the request is conformant to the quota - match limiter.allows(&peer_id, &r#type) { - Err(RateLimitedErr::TooLarge) => { - // we set the batch sizes, so this is a coding/config err for most protocols - let protocol = r#type.versioned_protocol().protocol(); - if matches!( - protocol, - Protocol::BlocksByRange - | Protocol::BlobsByRange - | Protocol::DataColumnsByRange - | Protocol::BlocksByRoot - | Protocol::BlobsByRoot - | Protocol::DataColumnsByRoot - ) { - debug!(self.log, "Request too large to process"; "request" => %r#type, "protocol" => %protocol); - } else { - // Other protocols shouldn't be sending large messages, we should flag the peer kind - crit!(self.log, "Request size too large to ever be processed"; "protocol" => %protocol); - } - // send an error code to the peer. - // the handler upon receiving the error code will send it back to the behaviour - self.send_response( - peer_id, - (conn_id, substream_id), - id, - RpcResponse::Error( - RpcErrorResponse::RateLimited, - "Rate limited. Request too large".into(), - ), - ); - return; - } - Err(RateLimitedErr::TooSoon(wait_time)) => { - debug!(self.log, "Request exceeds the rate limit"; - "request" => %r#type, "peer_id" => %peer_id, "wait_time_ms" => wait_time.as_millis()); - // send an error code to the peer. - // the handler upon receiving the error code will send it back to the behaviour - self.send_response( - peer_id, - (conn_id, substream_id), - id, - RpcResponse::Error( - RpcErrorResponse::RateLimited, - format!("Wait {:?}", wait_time).into(), - ), - ); - return; - } - // No rate limiting, continue. - Ok(()) => {} - } + HandlerEvent::Ok(RPCReceived::Request(request_id, request_type)) => { + 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(_) = r#type { - trace!(self.log, "Received Ping, queueing Pong";"connection_id" => %conn_id, "peer_id" => %peer_id); + if let RequestType::Ping(_) = request_type { + trace!(connection_id = %connection_id, %peer_id, "Received Ping, queueing Pong"); self.send_response( peer_id, - (conn_id, substream_id), - id, + request_id, RpcResponse::Success(RpcSuccessResponse::Pong(Ping { data: self.seq_number, })), @@ -475,25 +513,45 @@ where self.events.push(ToSwarm::GenerateEvent(RPCMessage { peer_id, - conn_id, - message: Ok(RPCReceived::Request(Request { - id, - substream_id, - r#type, - })), + connection_id, + message: Ok(RPCReceived::Request(request_id, request_type)), + })); + } + 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(RPCReceived::Response(id, response)), })); } - HandlerEvent::Ok(rpc) => { + 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, - conn_id, - message: Ok(rpc), + 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, - conn_id, + connection_id, message: Err(err), })); } @@ -508,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() { @@ -526,53 +589,3 @@ where Poll::Pending } } - -impl slog::KV for RPCMessage -where - E: EthSpec, - Id: ReqId, -{ - fn serialize( - &self, - _record: &slog::Record, - serializer: &mut dyn slog::Serializer, - ) -> slog::Result { - serializer.emit_arguments("peer_id", &format_args!("{}", self.peer_id))?; - match &self.message { - Ok(received) => { - let (msg_kind, protocol) = match received { - RPCReceived::Request(Request { r#type, .. }) => { - ("request", r#type.versioned_protocol().protocol()) - } - RPCReceived::Response(_, res) => ("response", res.protocol()), - RPCReceived::EndOfStream(_, end) => ( - "end_of_stream", - match end { - 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 - } - }, - ), - }; - serializer.emit_str("msg_kind", msg_kind)?; - serializer.emit_arguments("protocol", &format_args!("{}", protocol))?; - } - Err(error) => { - let (msg_kind, protocol) = match &error { - HandlerErr::Inbound { proto, .. } => ("inbound_err", *proto), - HandlerErr::Outbound { proto, .. } => ("outbound_err", *proto), - }; - serializer.emit_str("msg_kind", msg_kind)?; - serializer.emit_arguments("protocol", &format_args!("{}", protocol))?; - } - }; - - slog::Result::Ok(()) - } -} diff --git a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs index b9e82a5f1ee..f666c30d528 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 00000000000..c583baaadd1 --- /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 ae63e5cdb5a..e5b685676fb 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, @@ -7,37 +14,37 @@ use std::{ use futures::FutureExt; use libp2p::{swarm::NotifyHandler, PeerId}; -use slog::{crit, debug, Logger}; +use logging::crit; use smallvec::SmallVec; 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<[BehaviourAction; 3]>, - /// Slog logger. - log: Logger, + ready_requests: SmallVec<[(PeerId, RPCSend, Duration); 3]>, } /// Error returned when the rate limiter does not accept a request. @@ -50,21 +57,24 @@ 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, - log: Logger, ) -> Result { - debug!(log, "Using self rate limiting params"; "config" => ?config); - let limiter = RateLimiter::new_with_config(config.0, fork_context)?; + debug!(?config, "Using self rate limiting params"); + 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(), - log, }) } @@ -76,15 +86,25 @@ impl SelfRateLimiter { peer_id: PeerId, request_id: Id, req: RequestType, - ) -> Result, Error> { + ) -> Result, Error> { let protocol = req.versioned_protocol().protocol(); // First check that there are not already other requests waiting to be sent. if let Some(queued_requests) = self.delayed_requests.get_mut(&(peer_id, protocol)) { - 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, &self.log) { + 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); @@ -102,43 +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, - log: &Logger, - ) -> Result, (QueuedRequest, Duration)> { - match limiter.allows(&peer_id, &req) { - Ok(()) => Ok(BehaviourAction::NotifyHandler { - peer_id, - handler: NotifyHandler::Any, - event: 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!( - log, - "Self rate limiting error for a batch that will never fit. Sending request anyway. Check configuration parameters."; - "protocol" => %req.versioned_protocol().protocol() - ); - Ok(BehaviourAction::NotifyHandler { - peer_id, - handler: NotifyHandler::Any, - event: RPCSend::Request(request_id, req), - }) - } - RateLimitedErr::TooSoon(wait_time) => { - debug!(log, "Self rate limiting"; "protocol" => %protocol.protocol(), "wait_time_ms" => wait_time.as_millis(), "peer_id" => %peer_id); - Err((QueuedRequest { req, request_id }, wait_time)) + ) -> Result, (QueuedRequest, Duration)> { + if let Some(active_request) = active_requests.get(&peer_id) { + if let Some(count) = active_request.get(&req.protocol()) { + if *count >= MAX_CONCURRENT_REQUESTS { + debug!( + %peer_id, + protocol = %req.protocol(), + "Self rate limiting due to the number of concurrent requests" + ); + return Err(( + QueuedRequest { + req, + request_id, + queued_at: timestamp_now(), + }, + Duration::from_millis(WAIT_TIME_DUE_TO_CONCURRENT_REQUESTS), + )); + } + } + } + + if let Some(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 @@ -146,17 +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, &self.log) - { - 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(event), + Ok(event) => self.ready_requests.push((peer_id, event, queued_at)), } } if queued_requests.is_empty() { @@ -170,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(); @@ -191,41 +256,73 @@ 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 !self.ready_requests.is_empty() { - return Poll::Ready(self.ready_requests.remove(0)); + 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, + event, + }); } Poll::Pending } } +/// 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::service::api_types::{AppRequestId, RequestId, SingleLookupReqId, SyncRequestId}; + use crate::rpc::{Ping, Protocol, RPCSend, RequestType}; + use crate::service::api_types::{AppRequestId, SingleLookupReqId, SyncRequestId}; use libp2p::PeerId; + use logging::create_test_tracing_subscriber; use std::time::Duration; use types::{EthSpec, ForkContext, Hash256, MainnetEthSpec, Slot}; /// Test that `next_peer_request_ready` correctly maintains the queue. #[tokio::test] async fn test_next_peer_request_ready() { - let log = logging::test_logger(); + create_test_tracing_subscriber(); let config = OutboundRateLimiterConfig(RateLimiterConfig { ping_quota: Quota::n_every(1, 2), ..Default::default() @@ -235,20 +332,20 @@ mod tests { Hash256::ZERO, &MainnetEthSpec::default_spec(), )); - let mut limiter: SelfRateLimiter = - SelfRateLimiter::new(config, fork_context, log).unwrap(); + let mut limiter: SelfRateLimiter = + SelfRateLimiter::new(Some(config), fork_context).unwrap(); let peer_id = PeerId::random(); let lookup_id = 0; for i in 1..=5u32 { let _ = limiter.allows( peer_id, - RequestId::Application(AppRequestId::Sync(SyncRequestId::SingleBlock { + AppRequestId::Sync(SyncRequestId::SingleBlock { id: SingleLookupReqId { lookup_id, req_id: i, }, - })), + }), RequestType::Ping(Ping { data: i as u64 }), ); } @@ -265,9 +362,9 @@ mod tests { for i in 2..=5u32 { assert!(matches!( iter.next().unwrap().request_id, - RequestId::Application(AppRequestId::Sync(SyncRequestId::SingleBlock { + AppRequestId::Sync(SyncRequestId::SingleBlock { id: SingleLookupReqId { req_id, .. }, - })) if req_id == i, + }) if req_id == i, )); } @@ -290,13 +387,158 @@ mod tests { for i in 3..=5 { assert!(matches!( iter.next().unwrap().request_id, - RequestId::Application(AppRequestId::Sync(SyncRequestId::SingleBlock { + AppRequestId::Sync(SyncRequestId::SingleBlock { id: SingleLookupReqId { req_id, .. }, - })) if req_id == i, + }) if req_id == i, )); } 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/api_types.rs b/beacon_node/lighthouse_network/src/service/api_types.rs index e69c7aa5f78..b36f8cc2154 100644 --- a/beacon_node/lighthouse_network/src/service/api_types.rs +++ b/beacon_node/lighthouse_network/src/service/api_types.rs @@ -1,8 +1,4 @@ -use crate::rpc::{ - methods::{ResponseTermination, RpcResponse, RpcSuccessResponse, StatusMessage}, - SubstreamId, -}; -use libp2p::swarm::ConnectionId; +use crate::rpc::methods::{ResponseTermination, RpcResponse, RpcSuccessResponse, StatusMessage}; use std::fmt::{Display, Formatter}; use std::sync::Arc; use types::{ @@ -10,9 +6,6 @@ use types::{ LightClientFinalityUpdate, LightClientOptimisticUpdate, LightClientUpdate, SignedBeaconBlock, }; -/// Identifier of requests sent by a peer. -pub type PeerRequestId = (ConnectionId, SubstreamId); - pub type Id = u32; #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] @@ -130,12 +123,6 @@ pub struct CustodyRequester(pub SingleLookupReqId); pub enum AppRequestId { Sync(SyncRequestId), Router, -} - -/// Global identifier of a request. -#[derive(Debug, Clone, Copy)] -pub enum RequestId { - Application(AppRequestId), Internal, } @@ -218,22 +205,6 @@ impl std::convert::From> for RpcResponse { } } -impl slog::Value for RequestId { - fn serialize( - &self, - record: &slog::Record, - key: slog::Key, - serializer: &mut dyn slog::Serializer, - ) -> slog::Result { - match self { - RequestId::Internal => slog::Value::serialize("Behaviour", record, key, serializer), - RequestId::Application(ref id) => { - slog::Value::serialize(&format_args!("{:?}", id), record, key, serializer) - } - } - } -} - macro_rules! impl_display { ($structname: ty, $format: literal, $($field:ident),*) => { impl Display for $structname { diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index 7fc7de3eddf..23060df9e6a 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -10,18 +10,18 @@ use crate::peer_manager::{ use crate::peer_manager::{MIN_OUTBOUND_ONLY_FACTOR, PEER_EXCESS_FACTOR, PRIORITY_PEER_EXCESS}; use crate::rpc::methods::MetadataRequest; use crate::rpc::{ - self, GoodbyeReason, HandlerErr, NetworkParams, Protocol, RPCError, RPCMessage, RPCReceived, - RequestType, ResponseTermination, RpcErrorResponse, RpcResponse, RpcSuccessResponse, RPC, + GoodbyeReason, HandlerErr, InboundRequestId, NetworkParams, Protocol, RPCError, RPCMessage, + RPCReceived, RequestType, ResponseTermination, RpcErrorResponse, RpcResponse, + RpcSuccessResponse, RPC, }; use crate::types::{ - attestation_sync_committee_topics, fork_core_topics, subnet_from_topic_hash, GossipEncoding, - GossipKind, GossipTopic, SnappyTransform, Subnet, SubnetDiscovery, ALTAIR_CORE_TOPICS, - BASE_CORE_TOPICS, CAPELLA_CORE_TOPICS, LIGHT_CLIENT_GOSSIP_TOPICS, + all_topics_at_fork, core_topics_to_subscribe, is_fork_non_core_topic, subnet_from_topic_hash, + GossipEncoding, GossipKind, GossipTopic, SnappyTransform, Subnet, SubnetDiscovery, }; use crate::EnrExt; use crate::Eth2Enr; use crate::{metrics, Enr, NetworkGlobals, PubsubMessage, TopicHash}; -use api_types::{AppRequestId, PeerRequestId, RequestId, Response}; +use api_types::{AppRequestId, Response}; use futures::stream::StreamExt; use gossipsub::{ IdentTopic as Topic, MessageAcceptance, MessageAuthenticity, MessageId, PublishError, @@ -33,12 +33,13 @@ use libp2p::swarm::behaviour::toggle::Toggle; use libp2p::swarm::{NetworkBehaviour, Swarm, SwarmEvent}; use libp2p::upnp::tokio::Behaviour as Upnp; use libp2p::{identify, PeerId, SwarmBuilder}; -use slog::{crit, debug, info, o, trace, warn}; +use logging::crit; use std::num::{NonZeroU8, NonZeroUsize}; use std::path::PathBuf; use std::pin::Pin; use std::sync::Arc; use std::time::Duration; +use tracing::{debug, info, instrument, trace, warn}; use types::{ consts::altair::SYNC_COMMITTEE_SUBNET_COUNT, EnrForkId, EthSpec, ForkContext, Slot, SubnetId, }; @@ -66,7 +67,7 @@ pub enum NetworkEvent { /// An RPC Request that was sent failed. RPCFailed { /// The id of the failed request. - id: AppRequestId, + app_request_id: AppRequestId, /// The peer to which this request was sent. peer_id: PeerId, /// The error of the failed request. @@ -76,15 +77,15 @@ pub enum NetworkEvent { /// The peer that sent the request. peer_id: PeerId, /// Identifier of the request. All responses to this request must use this id. - id: PeerRequestId, + inbound_request_id: InboundRequestId, /// Request the peer sent. - request: rpc::Request, + request_type: RequestType, }, ResponseReceived { /// Peer that sent the response. peer_id: PeerId, /// Id of the request to which the peer is responding. - id: AppRequestId, + app_request_id: AppRequestId, /// Response the peer sent. response: Response, }, @@ -102,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; @@ -126,7 +129,7 @@ where /// The peer manager that keeps track of peer's reputation and status. pub peer_manager: PeerManager, /// The Eth2 RPC specified in the wire-0 protocol. - pub eth2_rpc: RPC, + pub eth2_rpc: RPC, /// Discv5 Discovery protocol. pub discovery: Discovery, /// Keep regular connection to peers and disconnect if absent. @@ -161,23 +164,24 @@ pub struct Network { gossip_cache: GossipCache, /// This node's PeerId. pub local_peer_id: PeerId, - /// Logger for behaviour actions. - log: slog::Logger, } /// Implements the combined behaviour for the libp2p service. impl Network { + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub async fn new( executor: task_executor::TaskExecutor, mut ctx: ServiceContext<'_>, - log: &slog::Logger, ) -> Result<(Self, Arc>), String> { - let log = log.new(o!("service"=> "libp2p")); - let config = ctx.config.clone(); - trace!(log, "Libp2p Service starting"); + trace!("Libp2p Service starting"); // initialise the node's ID - let local_keypair = utils::load_private_key(&config, &log); + let local_keypair = utils::load_private_key(&config); // Trusted peers will also be marked as explicit in GossipSub. // Cfr. https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.1.md#explicit-peering-agreements @@ -193,7 +197,6 @@ impl Network { local_keypair.clone(), &config, &ctx.enr_fork_id, - &log, &ctx.chain_spec, )?; @@ -202,15 +205,13 @@ impl Network { ctx.chain_spec .custody_group_count(config.subscribe_all_data_column_subnets) }); - let meta_data = - utils::load_or_build_metadata(&config.network_dir, custody_group_count, &log); + let meta_data = utils::load_or_build_metadata(&config.network_dir, custody_group_count); let seq_number = *meta_data.seq_number(); let globals = NetworkGlobals::new( enr, meta_data, trusted_peers, config.disable_peer_scoring, - &log, config.clone(), ctx.chain_spec.clone(), ); @@ -275,19 +276,44 @@ impl Network { )? }; - trace!(log, "Using peer score params"; "params" => ?params); + trace!(?params, "Using peer score params"); // Set up a scoring update interval let update_gossipsub_scores = tokio::time::interval(params.decay_interval); - let max_topics = ctx.chain_spec.attestation_subnet_count as usize - + SYNC_COMMITTEE_SUBNET_COUNT as usize - + ctx.chain_spec.blob_sidecar_subnet_count_max() as usize - + ctx.chain_spec.data_column_sidecar_subnet_count as usize - + BASE_CORE_TOPICS.len() - + ALTAIR_CORE_TOPICS.len() - + CAPELLA_CORE_TOPICS.len() // 0 core deneb and electra topics - + LIGHT_CLIENT_GOSSIP_TOPICS.len(); + let current_and_future_forks = ForkName::list_all().into_iter().filter_map(|fork| { + if fork >= ctx.fork_context.current_fork() { + ctx.fork_context + .to_context_bytes(fork) + .map(|fork_digest| (fork, fork_digest)) + } else { + None + } + }); + + let all_topics_for_forks = current_and_future_forks + .map(|(fork, fork_digest)| { + all_topics_at_fork::(fork, &ctx.chain_spec) + .into_iter() + .map(|topic| { + Topic::new(GossipTopic::new( + topic, + GossipEncoding::default(), + fork_digest, + )) + .into() + }) + .collect::>() + }) + .collect::>(); + + // For simplicity find the fork with the most individual topics and assume all forks + // have the same topic count + let max_topics_at_any_fork = all_topics_for_forks + .iter() + .map(|topics| topics.len()) + .max() + .expect("each fork has at least 5 hardcoded core topics"); let possible_fork_digests = ctx.fork_context.all_fork_digests(); let filter = gossipsub::MaxCountSubscriptionFilter { @@ -297,9 +323,9 @@ impl Network { SYNC_COMMITTEE_SUBNET_COUNT, ), // during a fork we subscribe to both the old and new topics - max_subscribed_topics: max_topics * 4, + max_subscribed_topics: max_topics_at_any_fork * 4, // 424 in theory = (64 attestation + 4 sync committee + 7 core topics + 9 blob topics + 128 column topics) * 2 - max_subscriptions_per_request: max_topics * 2, + max_subscriptions_per_request: max_topics_at_any_fork * 2, }; // If metrics are enabled for libp2p build the configuration @@ -334,17 +360,9 @@ impl Network { // If we are using metrics, then register which topics we want to make sure to keep // track of if ctx.libp2p_registry.is_some() { - let topics_to_keep_metrics_for = attestation_sync_committee_topics::() - .map(|gossip_kind| { - Topic::from(GossipTopic::new( - gossip_kind, - GossipEncoding::default(), - enr_fork_id.fork_digest, - )) - .into() - }) - .collect::>(); - gossipsub.register_topics_for_metrics(topics_to_keep_metrics_for); + for topics in all_topics_for_forks { + gossipsub.register_topics_for_metrics(topics); + } } (gossipsub, update_gossipsub_scores) @@ -360,7 +378,6 @@ impl Network { config.enable_light_client_server, config.inbound_rate_limiter_config.clone(), config.outbound_rate_limiter_config.clone(), - log.clone(), network_params, seq_number, ); @@ -371,7 +388,6 @@ impl Network { local_keypair.clone(), &config, network_globals.clone(), - &log, &ctx.chain_spec, ) .await?; @@ -404,7 +420,7 @@ impl Network { target_peer_count: config.target_peers, ..Default::default() }; - PeerManager::new(peer_manager_cfg, network_globals.clone(), &log)? + PeerManager::new(peer_manager_cfg, network_globals.clone())? }; let connection_limits = { @@ -499,7 +515,6 @@ impl Network { update_gossipsub_scores, gossip_cache, local_peer_id, - log, }; network.start(&config).await?; @@ -514,10 +529,25 @@ impl Network { /// - Starts listening in the given ports. /// - Dials boot-nodes and libp2p peers. /// - Subscribes to starting gossipsub topics. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] async fn start(&mut self, config: &crate::NetworkConfig) -> Result<(), String> { let enr = self.network_globals.local_enr(); - info!(self.log, "Libp2p Starting"; "peer_id" => %enr.peer_id(), "bandwidth_config" => format!("{}-{}", config.network_load, NetworkLoad::from(config.network_load).name)); - debug!(self.log, "Attempting to open listening ports"; config.listen_addrs(), "discovery_enabled" => !config.disable_discovery, "quic_enabled" => !config.disable_quic_support); + info!( + peer_id = %enr.peer_id(), + bandwidth_config = format!("{}-{}", config.network_load, NetworkLoad::from(config.network_load).name), + "Libp2p Starting" + ); + debug!( + listen_addrs = ?config.listen_addrs(), + discovery_enabled = !config.disable_discovery, + quic_enabled = !config.disable_quic_support, + "Attempting to open listening ports" + ); for listen_multiaddr in config.listen_addrs().libp2p_addresses() { // If QUIC is disabled, ignore listening on QUIC ports @@ -531,14 +561,13 @@ impl Network { Ok(_) => { let mut log_address = listen_multiaddr; log_address.push(MProtocol::P2p(enr.peer_id())); - info!(self.log, "Listening established"; "address" => %log_address); + info!(address = %log_address, "Listening established"); } Err(err) => { crit!( - self.log, - "Unable to listen on libp2p address"; - "error" => ?err, - "listen_multiaddr" => %listen_multiaddr, + error = ?err, + %listen_multiaddr, + "Unable to listen on libp2p address" ); return Err("Libp2p was unable to listen on the given listen address.".into()); } @@ -550,9 +579,9 @@ impl Network { // strip the p2p protocol if it exists strip_peer_id(&mut multiaddr); match self.swarm.dial(multiaddr.clone()) { - Ok(()) => debug!(self.log, "Dialing libp2p peer"; "address" => %multiaddr), + Ok(()) => debug!(address = %multiaddr, "Dialing libp2p peer"), Err(err) => { - debug!(self.log, "Could not connect to peer"; "address" => %multiaddr, "error" => ?err) + debug!(address = %multiaddr, error = ?err, "Could not connect to peer") } }; }; @@ -615,12 +644,12 @@ impl Network { if self.subscribe_kind(topic_kind.clone()) { subscribed_topics.push(topic_kind.clone()); } else { - warn!(self.log, "Could not subscribe to topic"; "topic" => %topic_kind); + warn!(topic = %topic_kind, "Could not subscribe to topic"); } } if !subscribed_topics.is_empty() { - info!(self.log, "Subscribed to topics"; "topics" => ?subscribed_topics); + info!(topics = ?subscribed_topics, "Subscribed to topics"); } Ok(()) @@ -629,48 +658,114 @@ impl Network { /* Public Accessible Functions to interact with the behaviour */ /// The routing pub-sub mechanism for eth2. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn gossipsub_mut(&mut self) -> &mut Gossipsub { &mut self.swarm.behaviour_mut().gossipsub } /// The Eth2 RPC specified in the wire-0 protocol. - pub fn eth2_rpc_mut(&mut self) -> &mut RPC { + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] + pub fn eth2_rpc_mut(&mut self) -> &mut RPC { &mut self.swarm.behaviour_mut().eth2_rpc } /// Discv5 Discovery protocol. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn discovery_mut(&mut self) -> &mut Discovery { &mut self.swarm.behaviour_mut().discovery } /// Provides IP addresses and peer information. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn identify_mut(&mut self) -> &mut identify::Behaviour { &mut self.swarm.behaviour_mut().identify } /// The peer manager that keeps track of peer's reputation and status. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn peer_manager_mut(&mut self) -> &mut PeerManager { &mut self.swarm.behaviour_mut().peer_manager } /// The routing pub-sub mechanism for eth2. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn gossipsub(&self) -> &Gossipsub { &self.swarm.behaviour().gossipsub } /// The Eth2 RPC specified in the wire-0 protocol. - pub fn eth2_rpc(&self) -> &RPC { + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] + pub fn eth2_rpc(&self) -> &RPC { &self.swarm.behaviour().eth2_rpc } /// Discv5 Discovery protocol. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn discovery(&self) -> &Discovery { &self.swarm.behaviour().discovery } /// Provides IP addresses and peer information. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn identify(&self) -> &identify::Behaviour { &self.swarm.behaviour().identify } /// The peer manager that keeps track of peer's reputation and status. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn peer_manager(&self) -> &PeerManager { &self.swarm.behaviour().peer_manager } /// Returns the local ENR of the node. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn local_enr(&self) -> Enr { self.network_globals.local_enr() } @@ -679,6 +774,12 @@ impl Network { /// Subscribes to a gossipsub topic kind, letting the network service determine the /// encoding and fork version. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn subscribe_kind(&mut self, kind: GossipKind) -> bool { let gossip_topic = GossipTopic::new( kind, @@ -691,6 +792,12 @@ impl Network { /// Unsubscribes from a gossipsub topic kind, letting the network service determine the /// encoding and fork version. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn unsubscribe_kind(&mut self, kind: GossipKind) -> bool { let gossip_topic = GossipTopic::new( kind, @@ -701,42 +808,42 @@ impl Network { } /// Subscribe to all required topics for the `new_fork` with the given `new_fork_digest`. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn subscribe_new_fork_topics(&mut self, new_fork: ForkName, new_fork_digest: [u8; 4]) { - // Subscribe to existing topics with new fork digest + // Re-subscribe to non-core topics with the new fork digest let subscriptions = self.network_globals.gossipsub_subscriptions.read().clone(); for mut topic in subscriptions.into_iter() { - topic.fork_digest = new_fork_digest; - self.subscribe(topic); + if is_fork_non_core_topic(&topic, new_fork) { + topic.fork_digest = new_fork_digest; + self.subscribe(topic); + } } // Subscribe to core topics for the new fork - for kind in fork_core_topics::( - &new_fork, - &self.fork_context.spec, + for kind in core_topics_to_subscribe::( + new_fork, &self.network_globals.as_topic_config(), + &self.fork_context.spec, ) { let topic = GossipTopic::new(kind, GossipEncoding::default(), new_fork_digest); self.subscribe(topic); } - // TODO(das): unsubscribe from blob topics at the Fulu fork - - // Register the new topics for metrics - let topics_to_keep_metrics_for = attestation_sync_committee_topics::() - .map(|gossip_kind| { - Topic::from(GossipTopic::new( - gossip_kind, - GossipEncoding::default(), - new_fork_digest, - )) - .into() - }) - .collect::>(); - self.gossipsub_mut() - .register_topics_for_metrics(topics_to_keep_metrics_for); + // Already registered all possible gossipsub topics for metrics } /// Unsubscribe from all topics that doesn't have the given fork_digest + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn unsubscribe_from_fork_topics_except(&mut self, except: [u8; 4]) { let subscriptions = self.network_globals.gossipsub_subscriptions.read().clone(); for topic in subscriptions @@ -749,6 +856,12 @@ impl Network { } /// Remove topic weight from all topics that don't have the given fork digest. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn remove_topic_weight_except(&mut self, except: [u8; 4]) { let new_param = TopicScoreParams { topic_weight: 0.0, @@ -764,15 +877,21 @@ impl Network { .gossipsub_mut() .set_topic_params(libp2p_topic, new_param.clone()) { - Ok(_) => debug!(self.log, "Removed topic weight"; "topic" => %topic), + Ok(_) => debug!(%topic, "Removed topic weight"), Err(e) => { - warn!(self.log, "Failed to remove topic weight"; "topic" => %topic, "error" => e) + warn!(%topic, error = e, "Failed to remove topic weight") } } } } /// Returns the scoring parameters for a topic if set. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn get_topic_params(&self, topic: GossipTopic) -> Option<&TopicScoreParams> { self.swarm .behaviour() @@ -783,6 +902,12 @@ impl Network { /// Subscribes to a gossipsub topic. /// /// Returns `true` if the subscription was successful and `false` otherwise. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn subscribe(&mut self, topic: GossipTopic) -> bool { // update the network globals self.network_globals @@ -794,17 +919,23 @@ impl Network { match self.gossipsub_mut().subscribe(&topic) { Err(e) => { - warn!(self.log, "Failed to subscribe to topic"; "topic" => %topic, "error" => ?e); + warn!(%topic, error = ?e, "Failed to subscribe to topic"); false } Ok(_) => { - debug!(self.log, "Subscribed to topic"; "topic" => %topic); + debug!(%topic, "Subscribed to topic"); true } } } /// Unsubscribe from a gossipsub topic. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn unsubscribe(&mut self, topic: GossipTopic) -> bool { // update the network globals self.network_globals @@ -815,20 +946,17 @@ impl Network { // unsubscribe from the topic let libp2p_topic: Topic = topic.clone().into(); - match self.gossipsub_mut().unsubscribe(&libp2p_topic) { - Err(_) => { - warn!(self.log, "Failed to unsubscribe from topic"; "topic" => %libp2p_topic); - false - } - Ok(v) => { - // Inform the network - debug!(self.log, "Unsubscribed to topic"; "topic" => %topic); - v - } - } + debug!(%topic, "Unsubscribed to topic"); + self.gossipsub_mut().unsubscribe(&libp2p_topic) } /// Publishes a list of messages on the pubsub (gossipsub) behaviour, choosing the encoding. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn publish(&mut self, messages: Vec>) { for message in messages { for topic in message.topics(GossipEncoding::default(), self.enr_fork_id.fork_digest) { @@ -840,17 +968,15 @@ impl Network { match e { PublishError::Duplicate => { debug!( - self.log, - "Attempted to publish duplicate message"; - "kind" => %topic.kind(), + kind = %topic.kind(), + "Attempted to publish duplicate message" ); } ref e => { warn!( - self.log, - "Could not publish message"; - "error" => ?e, - "kind" => %topic.kind(), + error = ?e, + kind = %topic.kind(), + "Could not publish message" ); } } @@ -875,7 +1001,7 @@ impl Network { } } - if let PublishError::InsufficientPeers = e { + if let PublishError::NoPeersSubscribedToTopic = e { self.gossip_cache.insert(topic, message_data); } } @@ -885,6 +1011,12 @@ impl Network { /// Informs the gossipsub about the result of a message validation. /// If the message is valid it will get propagated by gossipsub. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn report_message_validation_result( &mut self, propagation_source: &PeerId, @@ -910,17 +1042,21 @@ impl Network { } } - if let Err(e) = self.gossipsub_mut().report_message_validation_result( + self.gossipsub_mut().report_message_validation_result( &message_id, propagation_source, validation_result, - ) { - warn!(self.log, "Failed to report message validation"; "message_id" => %message_id, "peer_id" => %propagation_source, "error" => ?e); - } + ); } /// Updates the current gossipsub scoring parameters based on the validator count and current /// slot. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn update_gossipsub_parameters( &mut self, active_validators: usize, @@ -935,12 +1071,12 @@ impl Network { GossipTopic::new(kind, GossipEncoding::default(), fork_digest).into() }; - debug!(self.log, "Updating gossipsub score parameters"; - "active_validators" => active_validators); - trace!(self.log, "Updated gossipsub score parameters"; - "beacon_block_params" => ?beacon_block_params, - "beacon_aggregate_proof_params" => ?beacon_aggregate_proof_params, - "beacon_attestation_subnet_params" => ?beacon_attestation_subnet_params, + debug!(active_validators, "Updating gossipsub score parameters"); + trace!( + ?beacon_block_params, + ?beacon_aggregate_proof_params, + ?beacon_attestation_subnet_params, + "Updated gossipsub score parameters" ); self.gossipsub_mut() @@ -964,57 +1100,83 @@ impl Network { /* Eth2 RPC behaviour functions */ /// Send a request to a peer over RPC. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn send_request( &mut self, peer_id: PeerId, - request_id: AppRequestId, + app_request_id: AppRequestId, request: RequestType, ) -> Result<(), (AppRequestId, RPCError)> { // Check if the peer is connected before sending an RPC request if !self.swarm.is_connected(&peer_id) { - return Err((request_id, RPCError::Disconnected)); + return Err((app_request_id, RPCError::Disconnected)); } self.eth2_rpc_mut() - .send_request(peer_id, RequestId::Application(request_id), request); + .send_request(peer_id, app_request_id, request); Ok(()) } /// Send a successful response to a peer over RPC. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn send_response( &mut self, peer_id: PeerId, - id: PeerRequestId, - request_id: rpc::RequestId, + inbound_request_id: InboundRequestId, response: Response, ) { self.eth2_rpc_mut() - .send_response(peer_id, id, request_id, response.into()) + .send_response(peer_id, inbound_request_id, response.into()) } /// Inform the peer that their request produced an error. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn send_error_response( &mut self, peer_id: PeerId, - id: PeerRequestId, - request_id: rpc::RequestId, + inbound_request_id: InboundRequestId, error: RpcErrorResponse, reason: String, ) { self.eth2_rpc_mut().send_response( peer_id, - id, - request_id, + inbound_request_id, RpcResponse::Error(error, reason.into()), ) } /* Peer management functions */ - + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn testing_dial(&mut self, addr: Multiaddr) -> Result<(), libp2p::swarm::DialError> { self.swarm.dial(addr) } + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn report_peer( &mut self, peer_id: &PeerId, @@ -1030,6 +1192,12 @@ impl Network { /// /// This will send a goodbye, disconnect and then ban the peer. /// This is fatal for a peer, and should be used in unrecoverable circumstances. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn goodbye_peer(&mut self, peer_id: &PeerId, reason: GoodbyeReason, source: ReportSource) { self.peer_manager_mut() .goodbye_peer(peer_id, reason, source); @@ -1037,16 +1205,34 @@ impl Network { /// Hard (ungraceful) disconnect for testing purposes only /// Use goodbye_peer for disconnections, do not use this function. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn __hard_disconnect_testing_only(&mut self, peer_id: PeerId) { let _ = self.swarm.disconnect_peer_id(peer_id); } /// Returns an iterator over all enr entries in the DHT. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn enr_entries(&self) -> Vec { self.discovery().table_entries_enr() } /// Add an ENR to the routing table of the discovery mechanism. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn add_enr(&mut self, enr: Enr) { self.discovery_mut().add_enr(enr); } @@ -1054,9 +1240,15 @@ impl Network { /// Updates a subnet value to the ENR attnets/syncnets bitfield. /// /// The `value` is `true` if a subnet is being added and false otherwise. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn update_enr_subnet(&mut self, subnet_id: Subnet, value: bool) { if let Err(e) = self.discovery_mut().update_enr_bitfield(subnet_id, value) { - crit!(self.log, "Could not update ENR bitfield"; "error" => e); + crit!(error = e, "Could not update ENR bitfield"); } // update the local meta data which informs our peers of the update during PINGS self.update_metadata_bitfields(); @@ -1064,6 +1256,12 @@ impl Network { /// Attempts to discover new peers for a given subnet. The `min_ttl` gives the time at which we /// would like to retain the peers for. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn discover_subnet_peers(&mut self, subnets_to_discover: Vec) { // If discovery is not started or disabled, ignore the request if !self.discovery().started { @@ -1094,12 +1292,11 @@ impl Network { .count(); if peers_on_subnet >= TARGET_SUBNET_PEERS { trace!( - self.log, - "Discovery query ignored"; - "subnet" => ?s.subnet, - "reason" => "Already connected to desired peers", - "connected_peers_on_subnet" => peers_on_subnet, - "target_subnet_peers" => TARGET_SUBNET_PEERS, + subnet = ?s.subnet, + reason = "Already connected to desired peers", + connected_peers_on_subnet = peers_on_subnet, + target_subnet_peers = TARGET_SUBNET_PEERS, + "Discovery query ignored" ); false // Queue an outgoing connection request to the cached peers that are on `s.subnet_id`. @@ -1119,6 +1316,12 @@ impl Network { } /// Updates the local ENR's "eth2" field with the latest EnrForkId. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub fn update_fork_version(&mut self, enr_fork_id: EnrForkId) { self.discovery_mut().update_eth2_enr(enr_fork_id.clone()); @@ -1129,6 +1332,12 @@ impl Network { /* Private internal functions */ /// Updates the current meta data of the node to match the local ENR. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] fn update_metadata_bitfields(&mut self) { let local_attnets = self .discovery_mut() @@ -1156,15 +1365,27 @@ impl Network { drop(meta_data_w); self.eth2_rpc_mut().update_seq_number(seq_number); // Save the updated metadata to disk - utils::save_metadata_to_disk(&self.network_dir, meta_data, &self.log); + utils::save_metadata_to_disk(&self.network_dir, meta_data); } /// Sends a Ping request to the peer. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] fn ping(&mut self, peer_id: PeerId) { - self.eth2_rpc_mut().ping(peer_id, RequestId::Internal); + self.eth2_rpc_mut().ping(peer_id, AppRequestId::Internal); } /// Sends a METADATA request to a peer. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] fn send_meta_data_request(&mut self, peer_id: PeerId) { let event = if self.fork_context.spec.is_peer_das_scheduled() { // Nodes with higher custody will probably start advertising it @@ -1175,47 +1396,64 @@ impl Network { RequestType::MetaData(MetadataRequest::new_v2()) }; self.eth2_rpc_mut() - .send_request(peer_id, RequestId::Internal, event); + .send_request(peer_id, AppRequestId::Internal, event); } /// Sends a METADATA response to a peer. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] fn send_meta_data_response( &mut self, _req: MetadataRequest, - id: PeerRequestId, - request_id: rpc::RequestId, + inbound_request_id: InboundRequestId, peer_id: PeerId, ) { let metadata = self.network_globals.local_metadata.read().clone(); // The encoder is responsible for sending the negotiated version of the metadata - let event = RpcResponse::Success(RpcSuccessResponse::MetaData(metadata)); + let event = RpcResponse::Success(RpcSuccessResponse::MetaData(Arc::new(metadata))); self.eth2_rpc_mut() - .send_response(peer_id, id, request_id, event); + .send_response(peer_id, inbound_request_id, event); } // RPC Propagation methods /// Queues the response to be sent upwards as long at it was requested outside the Behaviour. #[must_use = "return the response"] + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] fn build_response( &mut self, - id: RequestId, + app_request_id: AppRequestId, peer_id: PeerId, response: Response, ) -> Option> { - match id { - RequestId::Application(id) => Some(NetworkEvent::ResponseReceived { + match app_request_id { + AppRequestId::Internal => None, + _ => Some(NetworkEvent::ResponseReceived { peer_id, - id, + app_request_id, response, }), - RequestId::Internal => None, } } /// Dial cached Enrs in discovery service that are in the given `subnet_id` and aren't /// in Connected, Dialing or Banned state. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] fn dial_cached_enrs_in_subnet(&mut self, subnet: Subnet, spec: Arc) { - let predicate = subnet_predicate::(vec![subnet], &self.log, spec); + let predicate = subnet_predicate::(vec![subnet], spec); let peers_to_dial: Vec = self .discovery() .cached_enrs() @@ -1233,7 +1471,7 @@ impl Network { self.discovery_mut().remove_cached_enr(&enr.peer_id()); let peer_id = enr.peer_id(); if self.peer_manager_mut().dial_peer(enr) { - debug!(self.log, "Added cached ENR peer to dial queue"; "peer_id" => %peer_id); + debug!(%peer_id, "Added cached ENR peer to dial queue"); } } } @@ -1256,6 +1494,12 @@ impl Network { /* Sub-behaviour event handling functions */ /// Handle a gossipsub event. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] fn inject_gs_event(&mut self, event: gossipsub::Event) -> Option> { match event { gossipsub::Event::Message { @@ -1267,15 +1511,13 @@ impl Network { // peer that originally published the message. match PubsubMessage::decode(&gs_msg.topic, &gs_msg.data, &self.fork_context) { Err(e) => { - debug!(self.log, "Could not decode gossipsub message"; "topic" => ?gs_msg.topic,"error" => e); + debug!(topic = ?gs_msg.topic, error = e, "Could not decode gossipsub message"); //reject the message - if let Err(e) = self.gossipsub_mut().report_message_validation_result( + self.gossipsub_mut().report_message_validation_result( &id, &propagation_source, MessageAcceptance::Reject, - ) { - warn!(self.log, "Failed to report message validation"; "message_id" => %id, "peer_id" => %propagation_source, "error" => ?e); - } + ); } Ok(msg) => { // Notify the network @@ -1307,11 +1549,7 @@ impl Network { .publish(Topic::from(topic.clone()), data) { Ok(_) => { - debug!( - self.log, - "Gossip message published on retry"; - "topic" => topic_str - ); + debug!(topic = topic_str, "Gossip message published on retry"); metrics::inc_counter_vec( &metrics::GOSSIP_LATE_PUBLISH_PER_TOPIC_KIND, &[topic_str], @@ -1319,10 +1557,9 @@ impl Network { } Err(PublishError::Duplicate) => { debug!( - self.log, - "Gossip message publish ignored on retry"; - "reason" => "duplicate", - "topic" => topic_str + reason = "duplicate", + topic = topic_str, + "Gossip message publish ignored on retry" ); metrics::inc_counter_vec( &metrics::GOSSIP_FAILED_LATE_PUBLISH_PER_TOPIC_KIND, @@ -1331,10 +1568,9 @@ impl Network { } Err(e) => { warn!( - self.log, - "Gossip message publish failed on retry"; - "topic" => topic_str, - "error" => %e + topic = topic_str, + error = %e, + "Gossip message publish failed on retry" ); metrics::inc_counter_vec( &metrics::GOSSIP_FAILED_LATE_PUBLISH_PER_TOPIC_KIND, @@ -1355,7 +1591,7 @@ impl Network { } } gossipsub::Event::GossipsubNotSupported { peer_id } => { - debug!(self.log, "Peer does not support gossipsub"; "peer_id" => %peer_id); + debug!(%peer_id, "Peer does not support gossipsub"); self.peer_manager_mut().report_peer( &peer_id, PeerAction::Fatal, @@ -1368,10 +1604,17 @@ impl Network { peer_id, failed_messages, } => { - debug!(self.log, "Slow gossipsub peer"; "peer_id" => %peer_id, "publish" => failed_messages.publish, "forward" => failed_messages.forward, "priority" => failed_messages.priority, "non_priority" => failed_messages.non_priority); + debug!( + peer_id = %peer_id, + publish = failed_messages.publish, + forward = failed_messages.forward, + priority = failed_messages.priority, + non_priority = failed_messages.non_priority, + "Slow gossipsub peer" + ); // Punish the peer if it cannot handle priority messages - if failed_messages.total_timeout() > 10 { - debug!(self.log, "Slow gossipsub peer penalized for priority failure"; "peer_id" => %peer_id); + if failed_messages.timeout > 10 { + debug!(%peer_id, "Slow gossipsub peer penalized for priority failure"); self.peer_manager_mut().report_peer( &peer_id, PeerAction::HighToleranceError, @@ -1380,7 +1623,7 @@ impl Network { "publish_timeout_penalty", ); } else if failed_messages.total_queue_full() > 10 { - debug!(self.log, "Slow gossipsub peer penalized for send queue full"; "peer_id" => %peer_id); + debug!(%peer_id, "Slow gossipsub peer penalized for send queue full"); self.peer_manager_mut().report_peer( &peer_id, PeerAction::HighToleranceError, @@ -1395,7 +1638,13 @@ impl Network { } /// Handle an RPC event. - fn inject_rpc_event(&mut self, event: RPCMessage) -> Option> { + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] + fn inject_rpc_event(&mut self, event: RPCMessage) -> Option> { let peer_id = event.peer_id; // Do not permit Inbound events from peers that are being disconnected or RPC requests, @@ -1404,16 +1653,11 @@ impl Network { && (matches!(event.message, Err(HandlerErr::Inbound { .. })) || matches!(event.message, Ok(RPCReceived::Request(..)))) { - debug!( - self.log, - "Ignoring rpc message of disconnecting peer"; - event - ); + debug!(?event, "Ignoring rpc message of disconnecting peer"); return None; } - let connection_id = event.conn_id; - // 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 { @@ -1442,16 +1686,20 @@ impl Network { ConnectionDirection::Outgoing, ); // inform failures of requests coming outside the behaviour - if let RequestId::Application(id) = id { - Some(NetworkEvent::RPCFailed { peer_id, id, error }) - } else { + if let AppRequestId::Internal = id { None + } else { + Some(NetworkEvent::RPCFailed { + peer_id, + app_request_id: id, + error, + }) } } } } - Ok(RPCReceived::Request(request)) => { - match request.r#type { + Ok(RPCReceived::Request(inbound_request_id, request_type)) => { + match request_type { /* Behaviour managed protocols: Ping and Metadata */ RequestType::Ping(ping) => { // inform the peer manager and send the response @@ -1460,21 +1708,16 @@ impl Network { } RequestType::MetaData(req) => { // send the requested meta-data - self.send_meta_data_response( - req, - (connection_id, request.substream_id), - request.id, - peer_id, - ); + self.send_meta_data_response(req, inbound_request_id, peer_id); None } RequestType::Goodbye(reason) => { // queue for disconnection without a goodbye message debug!( - self.log, "Peer sent Goodbye"; - "peer_id" => %peer_id, - "reason" => %reason, - "client" => %self.network_globals.client(&peer_id), + %peer_id, + %reason, + client = %self.network_globals.client(&peer_id), + "Peer sent Goodbye" ); // NOTE: We currently do not inform the application that we are // disconnecting here. The RPC handler will automatically @@ -1490,8 +1733,8 @@ impl Network { // propagate the STATUS message upwards Some(NetworkEvent::RequestReceived { peer_id, - id: (connection_id, request.substream_id), - request, + inbound_request_id, + request_type, }) } RequestType::BlocksByRange(ref req) => { @@ -1513,32 +1756,32 @@ impl Network { ); Some(NetworkEvent::RequestReceived { peer_id, - id: (connection_id, request.substream_id), - request, + inbound_request_id, + request_type, }) } RequestType::BlocksByRoot(_) => { metrics::inc_counter_vec(&metrics::TOTAL_RPC_REQUESTS, &["blocks_by_root"]); Some(NetworkEvent::RequestReceived { peer_id, - id: (connection_id, request.substream_id), - request, + inbound_request_id, + request_type, }) } RequestType::BlobsByRange(_) => { metrics::inc_counter_vec(&metrics::TOTAL_RPC_REQUESTS, &["blobs_by_range"]); Some(NetworkEvent::RequestReceived { peer_id, - id: (connection_id, request.substream_id), - request, + inbound_request_id, + request_type, }) } RequestType::BlobsByRoot(_) => { metrics::inc_counter_vec(&metrics::TOTAL_RPC_REQUESTS, &["blobs_by_root"]); Some(NetworkEvent::RequestReceived { peer_id, - id: (connection_id, request.substream_id), - request, + inbound_request_id, + request_type, }) } RequestType::DataColumnsByRoot(_) => { @@ -1548,8 +1791,8 @@ impl Network { ); Some(NetworkEvent::RequestReceived { peer_id, - id: (connection_id, request.substream_id), - request, + inbound_request_id, + request_type, }) } RequestType::DataColumnsByRange(_) => { @@ -1559,8 +1802,8 @@ impl Network { ); Some(NetworkEvent::RequestReceived { peer_id, - id: (connection_id, request.substream_id), - request, + inbound_request_id, + request_type, }) } RequestType::LightClientBootstrap(_) => { @@ -1570,8 +1813,8 @@ impl Network { ); Some(NetworkEvent::RequestReceived { peer_id, - id: (connection_id, request.substream_id), - request, + inbound_request_id, + request_type, }) } RequestType::LightClientOptimisticUpdate => { @@ -1581,8 +1824,8 @@ impl Network { ); Some(NetworkEvent::RequestReceived { peer_id, - id: (connection_id, request.substream_id), - request, + inbound_request_id, + request_type, }) } RequestType::LightClientFinalityUpdate => { @@ -1592,8 +1835,8 @@ impl Network { ); Some(NetworkEvent::RequestReceived { peer_id, - id: (connection_id, request.substream_id), - request, + inbound_request_id, + request_type, }) } RequestType::LightClientUpdatesByRange(_) => { @@ -1603,8 +1846,8 @@ impl Network { ); Some(NetworkEvent::RequestReceived { peer_id, - id: (connection_id, request.substream_id), - request, + inbound_request_id, + request_type, }) } } @@ -1617,9 +1860,11 @@ impl Network { None } RpcSuccessResponse::MetaData(meta_data) => { - self.peer_manager_mut() - .meta_data_response(&peer_id, meta_data); - None + let updated_cgc = self + .peer_manager_mut() + .meta_data_response(&peer_id, meta_data.as_ref().clone()); + // 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) => { @@ -1685,6 +1930,12 @@ impl Network { } /// Handle an identify event. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] fn inject_identify_event(&mut self, event: identify::Event) -> Option> { match event { identify::Event::Received { @@ -1693,10 +1944,7 @@ impl Network { connection_id: _, } => { if info.listen_addrs.len() > MAX_IDENTIFY_ADDRESSES { - debug!( - self.log, - "More than 10 addresses have been identified, truncating" - ); + debug!("More than 10 addresses have been identified, truncating"); info.listen_addrs.truncate(MAX_IDENTIFY_ADDRESSES); } // send peer info to the peer manager. @@ -1710,6 +1958,12 @@ impl Network { } /// Handle a peer manager event. + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] fn inject_pm_event(&mut self, event: PeerManagerEvent) -> Option> { match event { PeerManagerEvent::PeerConnectedIncoming(peer_id) => { @@ -1754,20 +2008,25 @@ impl Network { None } PeerManagerEvent::DisconnectPeer(peer_id, reason) => { - debug!(self.log, "Peer Manager disconnecting peer"; - "peer_id" => %peer_id, "reason" => %reason); + debug!(%peer_id, %reason, "Peer Manager disconnecting peer"); // send one goodbye self.eth2_rpc_mut() - .shutdown(peer_id, RequestId::Internal, reason); + .shutdown(peer_id, AppRequestId::Internal, reason); None } } } + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] fn inject_upnp_event(&mut self, event: libp2p::upnp::Event) { match event { libp2p::upnp::Event::NewExternalAddr(addr) => { - info!(self.log, "UPnP route established"; "addr" => %addr); + info!(%addr, "UPnP route established"); let mut iter = addr.iter(); let is_ip6 = { let addr = iter.next(); @@ -1779,38 +2038,40 @@ impl Network { if let Err(e) = self.discovery_mut().update_enr_quic_port(udp_port, is_ip6) { - warn!(self.log, "Failed to update ENR"; "error" => e); + warn!(error = e, "Failed to update ENR"); } } _ => { - trace!(self.log, "UPnP address mapped multiaddr from unknown transport"; "addr" => %addr) + trace!(%addr, "UPnP address mapped multiaddr from unknown transport"); } }, Some(multiaddr::Protocol::Tcp(tcp_port)) => { if let Err(e) = self.discovery_mut().update_enr_tcp_port(tcp_port, is_ip6) { - warn!(self.log, "Failed to update ENR"; "error" => e); + warn!(error = e, "Failed to update ENR"); } } _ => { - trace!(self.log, "UPnP address mapped multiaddr from unknown transport"; "addr" => %addr); + trace!(%addr, "UPnP address mapped multiaddr from unknown transport"); } } } libp2p::upnp::Event::ExpiredExternalAddr(_) => {} libp2p::upnp::Event::GatewayNotFound => { - info!(self.log, "UPnP not available"); + info!("UPnP not available"); } libp2p::upnp::Event::NonRoutableGateway => { - info!( - self.log, - "UPnP is available but gateway is not exposed to public network" - ); + info!("UPnP is available but gateway is not exposed to public network"); } } } /* Networking polling */ - + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] pub async fn next_event(&mut self) -> NetworkEvent { loop { tokio::select! { @@ -1821,7 +2082,6 @@ impl Network { return event; } }, - // perform gossipsub score updates when necessary _ = self.update_gossipsub_scores.tick() => { let this = self.swarm.behaviour_mut(); @@ -1830,7 +2090,7 @@ impl Network { // poll the gossipsub cache to clear expired messages Some(result) = self.gossip_cache.next() => { match result { - Err(e) => warn!(self.log, "Gossip cache error"; "error" => e), + Err(e) => warn!(error = e, "Gossip cache error"), Ok(expired_topic) => { if let Some(v) = metrics::get_int_counter( &metrics::GOSSIP_EXPIRED_LATE_PUBLISH_PER_TOPIC_KIND, @@ -1845,6 +2105,12 @@ impl Network { } } + #[instrument(parent = None, + level = "trace", + fields(service = "libp2p"), + name = "libp2p", + skip_all + )] fn parse_swarm_event( &mut self, event: SwarmEvent>, @@ -1878,7 +2144,7 @@ impl Network { send_back_addr, connection_id: _, } => { - trace!(self.log, "Incoming connection"; "our_addr" => %local_addr, "from" => %send_back_addr); + trace!(our_addr = %local_addr, from = %send_back_addr, "Incoming connection"); None } SwarmEvent::IncomingConnectionError { @@ -1909,7 +2175,7 @@ impl Network { } }, }; - debug!(self.log, "Failed incoming connection"; "our_addr" => %local_addr, "from" => %send_back_addr, "error" => error_repr); + debug!(our_addr = %local_addr, from = %send_back_addr, error = error_repr, "Failed incoming connection"); None } SwarmEvent::OutgoingConnectionError { @@ -1924,7 +2190,7 @@ impl Network { } SwarmEvent::NewListenAddr { address, .. } => Some(NetworkEvent::NewListenAddr(address)), SwarmEvent::ExpiredListenAddr { address, .. } => { - debug!(self.log, "Listen address expired"; "address" => %address); + debug!(%address, "Listen address expired"); None } SwarmEvent::ListenerClosed { @@ -1932,10 +2198,10 @@ impl Network { } => { match reason { Ok(_) => { - debug!(self.log, "Listener gracefully closed"; "addresses" => ?addresses) + debug!(?addresses, "Listener gracefully closed") } Err(reason) => { - crit!(self.log, "Listener abruptly closed"; "addresses" => ?addresses, "reason" => ?reason) + crit!(?addresses, ?reason, "Listener abruptly closed") } }; if Swarm::listeners(&self.swarm).count() == 0 { @@ -1945,7 +2211,7 @@ impl Network { } } SwarmEvent::ListenerError { error, .. } => { - debug!(self.log, "Listener closed connection attempt"; "reason" => ?error); + debug!(reason = ?error, "Listener closed connection attempt"); None } _ => { diff --git a/beacon_node/lighthouse_network/src/service/utils.rs b/beacon_node/lighthouse_network/src/service/utils.rs index 72c2b291022..01929bcb01c 100644 --- a/beacon_node/lighthouse_network/src/service/utils.rs +++ b/beacon_node/lighthouse_network/src/service/utils.rs @@ -9,7 +9,6 @@ use libp2p::core::{multiaddr::Multiaddr, muxing::StreamMuxerBox, transport::Boxe use libp2p::identity::{secp256k1, Keypair}; use libp2p::{core, noise, yamux, PeerId, Transport}; use prometheus_client::registry::Registry; -use slog::{debug, warn}; use ssz::Decode; use std::collections::HashSet; use std::fs::File; @@ -17,6 +16,7 @@ use std::io::prelude::*; use std::path::Path; use std::sync::Arc; use std::time::Duration; +use tracing::{debug, warn}; use types::{ ChainSpec, DataColumnSubnetId, EnrForkId, EthSpec, ForkContext, SubnetId, SyncSubnetId, }; @@ -107,21 +107,21 @@ fn keypair_from_bytes(mut bytes: Vec) -> Result { /// generated and is then saved to disk. /// /// Currently only secp256k1 keys are allowed, as these are the only keys supported by discv5. -pub fn load_private_key(config: &NetworkConfig, log: &slog::Logger) -> Keypair { +pub fn load_private_key(config: &NetworkConfig) -> Keypair { // check for key from disk let network_key_f = config.network_dir.join(NETWORK_KEY_FILENAME); if let Ok(mut network_key_file) = File::open(network_key_f.clone()) { let mut key_bytes: Vec = Vec::with_capacity(36); match network_key_file.read_to_end(&mut key_bytes) { - Err(_) => debug!(log, "Could not read network key file"), + Err(_) => debug!("Could not read network key file"), Ok(_) => { // only accept secp256k1 keys for now if let Ok(secret_key) = secp256k1::SecretKey::try_from_bytes(&mut key_bytes) { let kp: secp256k1::Keypair = secret_key.into(); - debug!(log, "Loaded network key from disk."); + debug!("Loaded network key from disk."); return kp.into(); } else { - debug!(log, "Network key file is not a valid secp256k1 key"); + debug!("Network key file is not a valid secp256k1 key"); } } } @@ -134,12 +134,12 @@ pub fn load_private_key(config: &NetworkConfig, log: &slog::Logger) -> Keypair { .and_then(|mut f| f.write_all(&local_private_key.secret().to_bytes())) { Ok(_) => { - debug!(log, "New network key generated and written to disk"); + debug!("New network key generated and written to disk"); } Err(e) => { warn!( - log, - "Could not write node key to file: {:?}. error: {}", network_key_f, e + "Could not write node key to file: {:?}. error: {}", + network_key_f, e ); } } @@ -166,7 +166,6 @@ pub fn strip_peer_id(addr: &mut Multiaddr) { pub fn load_or_build_metadata( network_dir: &Path, custody_group_count_opt: Option, - log: &slog::Logger, ) -> MetaData { // We load a V2 metadata version by default (regardless of current fork) // since a V2 metadata can be converted to V1. The RPC encoder is responsible @@ -192,7 +191,7 @@ pub fn load_or_build_metadata( { meta_data.seq_number += 1; } - debug!(log, "Loaded metadata from disk"); + debug!("Loaded metadata from disk"); } Err(_) => { match MetaDataV1::::from_ssz_bytes(&metadata_ssz) { @@ -200,13 +199,12 @@ pub fn load_or_build_metadata( let persisted_metadata = MetaData::V1(persisted_metadata); // Increment seq number as the persisted metadata version is updated meta_data.seq_number = *persisted_metadata.seq_number() + 1; - debug!(log, "Loaded metadata from disk"); + debug!("Loaded metadata from disk"); } Err(e) => { debug!( - log, - "Metadata from file could not be decoded"; - "error" => ?e, + error = ?e, + "Metadata from file could not be decoded" ); } } @@ -227,8 +225,8 @@ pub fn load_or_build_metadata( MetaData::V2(meta_data) }; - debug!(log, "Metadata sequence number"; "seq_num" => meta_data.seq_number()); - save_metadata_to_disk(network_dir, meta_data.clone(), log); + debug!(seq_num = meta_data.seq_number(), "Metadata sequence number"); + save_metadata_to_disk(network_dir, meta_data.clone()); meta_data } @@ -275,11 +273,7 @@ pub(crate) fn create_whitelist_filter( } /// Persist metadata to disk -pub(crate) fn save_metadata_to_disk( - dir: &Path, - metadata: MetaData, - log: &slog::Logger, -) { +pub(crate) fn save_metadata_to_disk(dir: &Path, metadata: MetaData) { let _ = std::fs::create_dir_all(dir); // We always store the metadata v2 to disk because // custody_group_count parameter doesn't need to be persisted across runs. @@ -288,14 +282,13 @@ pub(crate) fn save_metadata_to_disk( let metadata_bytes = metadata.metadata_v2().as_ssz_bytes(); match File::create(dir.join(METADATA_FILENAME)).and_then(|mut f| f.write_all(&metadata_bytes)) { Ok(_) => { - debug!(log, "Metadata written to disk"); + debug!("Metadata written to disk"); } Err(e) => { warn!( - log, - "Could not write metadata to disk"; - "file" => format!("{:?}{:?}", dir, METADATA_FILENAME), - "error" => %e + file = format!("{:?}{:?}", dir, METADATA_FILENAME), + error = %e, + "Could not write metadata to disk" ); } } diff --git a/beacon_node/lighthouse_network/src/types/globals.rs b/beacon_node/lighthouse_network/src/types/globals.rs index b63754fd4eb..fd99d935890 100644 --- a/beacon_node/lighthouse_network/src/types/globals.rs +++ b/beacon_node/lighthouse_network/src/types/globals.rs @@ -5,9 +5,9 @@ use crate::rpc::{MetaData, MetaDataV3}; use crate::types::{BackFillState, SyncState}; use crate::{Client, Enr, EnrExt, GossipTopic, Multiaddr, NetworkConfig, PeerId}; use parking_lot::RwLock; -use slog::error; use std::collections::HashSet; use std::sync::Arc; +use tracing::error; use types::data_column_custody_group::{ compute_columns_for_custody_group, compute_subnets_from_custody_group, get_custody_groups, }; @@ -33,6 +33,8 @@ pub struct NetworkGlobals { /// The computed sampling subnets and columns is stored to avoid re-computing. pub sampling_subnets: HashSet, pub sampling_columns: HashSet, + /// Constant custody group count (CGC) set at startup + custody_group_count: u64, /// Network-related configuration. Immutable after initialization. pub config: Arc, /// Ethereum chain configuration. Immutable after initialization. @@ -45,63 +47,58 @@ impl NetworkGlobals { local_metadata: MetaData, trusted_peers: Vec, disable_peer_scoring: bool, - log: &slog::Logger, config: Arc, spec: Arc, ) -> Self { - let (sampling_subnets, sampling_columns) = if spec.is_peer_das_scheduled() { - let node_id = enr.node_id().raw(); + let node_id = enr.node_id().raw(); - let custody_group_count = match local_metadata.custody_group_count() { - Ok(&cgc) if cgc <= spec.number_of_custody_groups => cgc, - _ => { + let custody_group_count = match local_metadata.custody_group_count() { + Ok(&cgc) if cgc <= spec.number_of_custody_groups => cgc, + _ => { + if spec.is_peer_das_scheduled() { error!( - log, - "custody_group_count from metadata is either invalid or not set. This is a bug!"; - "info" => "falling back to default custody requirement" + info = "falling back to default custody requirement", + "custody_group_count from metadata is either invalid or not set. This is a bug!" ); - spec.custody_requirement } - }; - - // The below `expect` calls will panic on start up if the chain spec config values used - // are invalid - let sampling_size = spec - .sampling_size(custody_group_count) - .expect("should compute node sampling size from valid chain spec"); - let custody_groups = get_custody_groups(node_id, sampling_size, &spec) - .expect("should compute node custody groups"); - - let mut sampling_subnets = HashSet::new(); - for custody_index in &custody_groups { - let subnets = compute_subnets_from_custody_group(*custody_index, &spec) - .expect("should compute custody subnets for node"); - sampling_subnets.extend(subnets); + spec.custody_requirement } + }; - let mut sampling_columns = HashSet::new(); - for custody_index in &custody_groups { - let columns = compute_columns_for_custody_group(*custody_index, &spec) - .expect("should compute custody columns for node"); - sampling_columns.extend(columns); - } + // The below `expect` calls will panic on start up if the chain spec config values used + // are invalid + let sampling_size = spec + .sampling_size(custody_group_count) + .expect("should compute node sampling size from valid chain spec"); + let custody_groups = get_custody_groups(node_id, sampling_size, &spec) + .expect("should compute node custody groups"); + + let mut sampling_subnets = HashSet::new(); + for custody_index in &custody_groups { + let subnets = compute_subnets_from_custody_group(*custody_index, &spec) + .expect("should compute custody subnets for node"); + sampling_subnets.extend(subnets); + } - (sampling_subnets, sampling_columns) - } else { - (HashSet::new(), HashSet::new()) - }; + let mut sampling_columns = HashSet::new(); + for custody_index in &custody_groups { + let columns = compute_columns_for_custody_group(*custody_index, &spec) + .expect("should compute custody columns for node"); + sampling_columns.extend(columns); + } NetworkGlobals { local_enr: RwLock::new(enr.clone()), peer_id: RwLock::new(enr.peer_id()), listen_multiaddrs: RwLock::new(Vec::new()), local_metadata: RwLock::new(local_metadata), - peers: RwLock::new(PeerDB::new(trusted_peers, disable_peer_scoring, log)), + peers: RwLock::new(PeerDB::new(trusted_peers, disable_peer_scoring)), gossipsub_subscriptions: RwLock::new(HashSet::new()), sync_state: RwLock::new(SyncState::Stalled), backfill_state: RwLock::new(BackFillState::Paused), sampling_subnets, sampling_columns, + custody_group_count, config, spec, } @@ -123,6 +120,19 @@ impl NetworkGlobals { self.listen_multiaddrs.read().clone() } + /// Returns true if this node is configured as a PeerDAS supernode + pub fn is_supernode(&self) -> bool { + self.custody_group_count == self.spec.number_of_custody_groups + } + + /// Returns the count of custody columns this node must sample for block import + pub fn custody_columns_count(&self) -> u64 { + // This only panics if the chain spec contains invalid values + self.spec + .sampling_size(self.custody_group_count) + .expect("should compute node sampling size from valid chain spec") + } + /// Returns the number of libp2p connected peers. pub fn connected_peers(&self) -> usize { self.peers.read().connected_peer_ids().count() @@ -196,9 +206,25 @@ 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 { + enable_light_client_server: self.config.enable_light_client_server, + subscribe_all_subnets: self.config.subscribe_all_subnets, subscribe_all_data_column_subnets: self.config.subscribe_all_data_column_subnets, sampling_subnets: &self.sampling_subnets, } @@ -207,7 +233,6 @@ impl NetworkGlobals { /// TESTING ONLY. Build a dummy NetworkGlobals instance. pub fn new_test_globals( trusted_peers: Vec, - log: &slog::Logger, config: Arc, spec: Arc, ) -> NetworkGlobals { @@ -217,13 +242,12 @@ impl NetworkGlobals { syncnets: Default::default(), custody_group_count: spec.custody_requirement, }); - Self::new_test_globals_with_metadata(trusted_peers, metadata, log, config, spec) + Self::new_test_globals_with_metadata(trusted_peers, metadata, config, spec) } pub(crate) fn new_test_globals_with_metadata( trusted_peers: Vec, metadata: MetaData, - log: &slog::Logger, config: Arc, spec: Arc, ) -> NetworkGlobals { @@ -231,18 +255,19 @@ impl NetworkGlobals { let keypair = libp2p::identity::secp256k1::Keypair::generate(); let enr_key: discv5::enr::CombinedKey = discv5::enr::CombinedKey::from_secp256k1(&keypair); let enr = discv5::enr::Enr::builder().build(&enr_key).unwrap(); - NetworkGlobals::new(enr, metadata, trusted_peers, false, log, config, spec) + NetworkGlobals::new(enr, metadata, trusted_peers, false, config, spec) } } #[cfg(test)] mod test { use super::*; + use logging::create_test_tracing_subscriber; use types::{Epoch, EthSpec, MainnetEthSpec as E}; #[test] fn test_sampling_subnets() { - let log = logging::test_logger(); + create_test_tracing_subscriber(); let mut spec = E::default_spec(); spec.fulu_fork_epoch = Some(Epoch::new(0)); @@ -254,7 +279,6 @@ mod test { let globals = NetworkGlobals::::new_test_globals_with_metadata( vec![], metadata, - &log, config, Arc::new(spec), ); @@ -266,7 +290,7 @@ mod test { #[test] fn test_sampling_columns() { - let log = logging::test_logger(); + create_test_tracing_subscriber(); let mut spec = E::default_spec(); spec.fulu_fork_epoch = Some(Epoch::new(0)); @@ -278,7 +302,6 @@ mod test { let globals = NetworkGlobals::::new_test_globals_with_metadata( vec![], metadata, - &log, config, Arc::new(spec), ); diff --git a/beacon_node/lighthouse_network/src/types/mod.rs b/beacon_node/lighthouse_network/src/types/mod.rs index 58ba7588b98..868cdb6eb9f 100644 --- a/beacon_node/lighthouse_network/src/types/mod.rs +++ b/beacon_node/lighthouse_network/src/types/mod.rs @@ -1,7 +1,6 @@ mod globals; mod pubsub; mod subnet; -mod sync_state; mod topics; use types::{BitVector, EthSpec}; @@ -11,12 +10,11 @@ pub type EnrSyncCommitteeBitfield = BitVector<::SyncCommitteeSu pub type Enr = discv5::enr::Enr; +pub use eth2::lighthouse::sync_state::{BackFillState, SyncState}; pub use globals::NetworkGlobals; pub use pubsub::{PubsubMessage, SnappyTransform}; pub use subnet::{Subnet, SubnetDiscovery}; -pub use sync_state::{BackFillState, SyncState}; pub use topics::{ - attestation_sync_committee_topics, core_topics_to_subscribe, fork_core_topics, - subnet_from_topic_hash, GossipEncoding, GossipKind, GossipTopic, TopicConfig, - ALTAIR_CORE_TOPICS, BASE_CORE_TOPICS, CAPELLA_CORE_TOPICS, LIGHT_CLIENT_GOSSIP_TOPICS, + all_topics_at_fork, core_topics_to_subscribe, is_fork_non_core_topic, subnet_from_topic_hash, + GossipEncoding, GossipKind, GossipTopic, TopicConfig, }; diff --git a/beacon_node/lighthouse_network/src/types/topics.rs b/beacon_node/lighthouse_network/src/types/topics.rs index 171dab09a35..56b97303d3e 100644 --- a/beacon_node/lighthouse_network/src/types/topics.rs +++ b/beacon_node/lighthouse_network/src/types/topics.rs @@ -25,104 +25,110 @@ pub const BLS_TO_EXECUTION_CHANGE_TOPIC: &str = "bls_to_execution_change"; pub const LIGHT_CLIENT_FINALITY_UPDATE: &str = "light_client_finality_update"; pub const LIGHT_CLIENT_OPTIMISTIC_UPDATE: &str = "light_client_optimistic_update"; -pub const BASE_CORE_TOPICS: [GossipKind; 5] = [ - GossipKind::BeaconBlock, - GossipKind::BeaconAggregateAndProof, - GossipKind::VoluntaryExit, - GossipKind::ProposerSlashing, - GossipKind::AttesterSlashing, -]; - -pub const ALTAIR_CORE_TOPICS: [GossipKind; 1] = [GossipKind::SignedContributionAndProof]; - -pub const CAPELLA_CORE_TOPICS: [GossipKind; 1] = [GossipKind::BlsToExecutionChange]; - -pub const LIGHT_CLIENT_GOSSIP_TOPICS: [GossipKind; 2] = [ - GossipKind::LightClientFinalityUpdate, - GossipKind::LightClientOptimisticUpdate, -]; - #[derive(Debug)] pub struct TopicConfig<'a> { + pub enable_light_client_server: bool, + pub subscribe_all_subnets: bool, pub subscribe_all_data_column_subnets: bool, pub sampling_subnets: &'a HashSet, } -/// Returns the core topics associated with each fork that are new to the previous fork -pub fn fork_core_topics( - fork_name: &ForkName, +/// Returns all the topics the node should subscribe at `fork_name` +pub fn core_topics_to_subscribe( + fork_name: ForkName, + opts: &TopicConfig, spec: &ChainSpec, - topic_config: &TopicConfig, ) -> Vec { - match fork_name { - ForkName::Base => BASE_CORE_TOPICS.to_vec(), - ForkName::Altair => ALTAIR_CORE_TOPICS.to_vec(), - ForkName::Bellatrix => vec![], - ForkName::Capella => CAPELLA_CORE_TOPICS.to_vec(), - ForkName::Deneb => { - // All of deneb blob topics are core topics - let mut deneb_blob_topics = Vec::new(); - for i in 0..spec.blob_sidecar_subnet_count(ForkName::Deneb) { - deneb_blob_topics.push(GossipKind::BlobSidecar(i)); - } - deneb_blob_topics + let mut topics = vec![ + GossipKind::BeaconBlock, + GossipKind::BeaconAggregateAndProof, + GossipKind::VoluntaryExit, + GossipKind::ProposerSlashing, + GossipKind::AttesterSlashing, + ]; + + if opts.subscribe_all_subnets { + for i in 0..spec.attestation_subnet_count { + topics.push(GossipKind::Attestation(i.into())); } - ForkName::Electra => { - // All of electra blob topics are core topics - let mut electra_blob_topics = Vec::new(); - for i in 0..spec.blob_sidecar_subnet_count(ForkName::Electra) { - electra_blob_topics.push(GossipKind::BlobSidecar(i)); + } + + if fork_name.altair_enabled() { + topics.push(GossipKind::SignedContributionAndProof); + + if opts.subscribe_all_subnets { + for i in 0..E::SyncCommitteeSubnetCount::to_u64() { + topics.push(GossipKind::SyncCommitteeMessage(i.into())); } - electra_blob_topics } - ForkName::Fulu => { - let mut topics = vec![]; - if topic_config.subscribe_all_data_column_subnets { - for column_subnet in 0..spec.data_column_sidecar_subnet_count { - topics.push(GossipKind::DataColumnSidecar(DataColumnSubnetId::new( - column_subnet, - ))); - } - } else { - for column_subnet in topic_config.sampling_subnets { - topics.push(GossipKind::DataColumnSidecar(*column_subnet)); - } + + if opts.enable_light_client_server { + topics.push(GossipKind::LightClientFinalityUpdate); + topics.push(GossipKind::LightClientOptimisticUpdate); + } + } + + if fork_name.capella_enabled() { + topics.push(GossipKind::BlsToExecutionChange); + } + + if fork_name.deneb_enabled() && !fork_name.fulu_enabled() { + // All of deneb blob topics are core topics + for i in 0..spec.blob_sidecar_subnet_count(fork_name) { + topics.push(GossipKind::BlobSidecar(i)); + } + } + + if fork_name.fulu_enabled() { + if opts.subscribe_all_data_column_subnets { + for i in 0..spec.data_column_sidecar_subnet_count { + topics.push(GossipKind::DataColumnSidecar(i.into())); + } + } else { + for subnet in opts.sampling_subnets { + topics.push(GossipKind::DataColumnSidecar(*subnet)); } - topics } } -} -/// Returns all the attestation and sync committee topics, for a given fork. -pub fn attestation_sync_committee_topics() -> impl Iterator { - (0..E::SubnetBitfieldLength::to_usize()) - .map(|subnet_id| GossipKind::Attestation(SubnetId::new(subnet_id as u64))) - .chain( - (0..E::SyncCommitteeSubnetCount::to_usize()).map(|sync_committee_id| { - GossipKind::SyncCommitteeMessage(SyncSubnetId::new(sync_committee_id as u64)) - }), - ) + topics } -/// Returns all the topics that we need to subscribe to for a given fork -/// including topics from older forks and new topics for the current fork. -pub fn core_topics_to_subscribe( - mut current_fork: ForkName, - spec: &ChainSpec, - topic_config: &TopicConfig, -) -> Vec { - let mut topics = fork_core_topics::(¤t_fork, spec, topic_config); - while let Some(previous_fork) = current_fork.previous_fork() { - let previous_fork_topics = fork_core_topics::(&previous_fork, spec, topic_config); - topics.extend(previous_fork_topics); - current_fork = previous_fork; +/// Returns true if a given non-core `GossipTopic` MAY be subscribe at this fork. +/// +/// For example: the `Attestation` topic is not subscribed as a core topic if +/// subscribe_all_subnets = false` but we may subscribe to it outside of a fork +/// boundary if the node is an aggregator. +pub fn is_fork_non_core_topic(topic: &GossipTopic, _fork_name: ForkName) -> bool { + match topic.kind() { + // Node may be aggregator of attestation and sync_committee_message topics for all known + // forks + GossipKind::Attestation(_) | GossipKind::SyncCommitteeMessage(_) => true, + // All these topics are core-only + GossipKind::BeaconBlock + | GossipKind::BeaconAggregateAndProof + | GossipKind::BlobSidecar(_) + | GossipKind::DataColumnSidecar(_) + | GossipKind::VoluntaryExit + | GossipKind::ProposerSlashing + | GossipKind::AttesterSlashing + | GossipKind::SignedContributionAndProof + | GossipKind::BlsToExecutionChange + | GossipKind::LightClientFinalityUpdate + | GossipKind::LightClientOptimisticUpdate => false, } - // Remove duplicates - topics - .into_iter() - .collect::>() - .into_iter() - .collect() +} + +pub fn all_topics_at_fork(fork: ForkName, spec: &ChainSpec) -> Vec { + // Compute the worst case of all forks + let sampling_subnets = HashSet::from_iter(spec.all_data_column_sidecar_subnets()); + let opts = TopicConfig { + enable_light_client_server: true, + subscribe_all_subnets: true, + subscribe_all_data_column_subnets: true, + sampling_subnets: &sampling_subnets, + }; + core_topics_to_subscribe::(fork, &opts, spec) } /// A gossipsub topic which encapsulates the type of messages that should be sent and received over @@ -368,10 +374,9 @@ fn subnet_topic_index(topic: &str) -> Option { #[cfg(test)] mod tests { - use types::MainnetEthSpec; - use super::GossipKind::*; use super::*; + use types::{Epoch, MainnetEthSpec as E}; const GOOD_FORK_DIGEST: &str = "e1925f3b"; const BAD_PREFIX: &str = "tezos"; @@ -496,31 +501,94 @@ mod tests { assert_eq!("attester_slashing", AttesterSlashing.as_ref()); } - #[test] - fn test_core_topics_to_subscribe() { - type E = MainnetEthSpec; - let spec = E::default_spec(); - let mut all_topics = Vec::new(); - let topic_config = TopicConfig { + fn get_spec() -> ChainSpec { + let mut spec = E::default_spec(); + spec.altair_fork_epoch = Some(Epoch::new(1)); + spec.bellatrix_fork_epoch = Some(Epoch::new(2)); + spec.capella_fork_epoch = Some(Epoch::new(3)); + spec.deneb_fork_epoch = Some(Epoch::new(4)); + spec.electra_fork_epoch = Some(Epoch::new(5)); + spec.fulu_fork_epoch = Some(Epoch::new(6)); + spec + } + + fn get_sampling_subnets() -> HashSet { + HashSet::new() + } + + fn get_topic_config(sampling_subnets: &HashSet) -> TopicConfig { + TopicConfig { + enable_light_client_server: false, + subscribe_all_subnets: false, subscribe_all_data_column_subnets: false, - sampling_subnets: &HashSet::from_iter([1, 2].map(DataColumnSubnetId::new)), - }; - let mut fulu_core_topics = fork_core_topics::(&ForkName::Fulu, &spec, &topic_config); - let mut electra_core_topics = - fork_core_topics::(&ForkName::Electra, &spec, &topic_config); - let mut deneb_core_topics = fork_core_topics::(&ForkName::Deneb, &spec, &topic_config); - all_topics.append(&mut fulu_core_topics); - all_topics.append(&mut electra_core_topics); - all_topics.append(&mut deneb_core_topics); - all_topics.extend(CAPELLA_CORE_TOPICS); - all_topics.extend(ALTAIR_CORE_TOPICS); - all_topics.extend(BASE_CORE_TOPICS); + sampling_subnets, + } + } + + #[test] + fn base_topics_are_always_active() { + let spec = get_spec(); + let s = get_sampling_subnets(); + let topic_config = get_topic_config(&s); + for fork in ForkName::list_all() { + assert!(core_topics_to_subscribe::(fork, &topic_config, &spec,) + .contains(&GossipKind::BeaconBlock)); + } + } + #[test] + fn blobs_are_not_subscribed_in_peerdas() { + let spec = get_spec(); + let s = get_sampling_subnets(); + let topic_config = get_topic_config(&s); + assert!( + !core_topics_to_subscribe::(ForkName::Fulu, &topic_config, &spec,) + .contains(&GossipKind::BlobSidecar(0)) + ); + } + + #[test] + fn columns_are_subscribed_in_peerdas() { + let spec = get_spec(); + let s = get_sampling_subnets(); + let mut topic_config = get_topic_config(&s); + topic_config.subscribe_all_data_column_subnets = true; + assert!( + core_topics_to_subscribe::(ForkName::Fulu, &topic_config, &spec) + .contains(&GossipKind::DataColumnSidecar(0.into())) + ); + } + + #[test] + fn test_core_topics_to_subscribe() { + let spec = get_spec(); + let s = HashSet::from_iter([1, 2].map(DataColumnSubnetId::new)); + let mut topic_config = get_topic_config(&s); + topic_config.enable_light_client_server = true; let latest_fork = *ForkName::list_all().last().unwrap(); - let core_topics = core_topics_to_subscribe::(latest_fork, &spec, &topic_config); + let topics = core_topics_to_subscribe::(latest_fork, &topic_config, &spec); + + let mut expected_topics = vec![ + GossipKind::BeaconBlock, + GossipKind::BeaconAggregateAndProof, + GossipKind::VoluntaryExit, + GossipKind::ProposerSlashing, + GossipKind::AttesterSlashing, + GossipKind::SignedContributionAndProof, + GossipKind::LightClientFinalityUpdate, + GossipKind::LightClientOptimisticUpdate, + GossipKind::BlsToExecutionChange, + ]; + for subnet in s { + expected_topics.push(GossipKind::DataColumnSidecar(subnet)); + } // Need to check all the topics exist in an order independent manner - for topic in all_topics { - assert!(core_topics.contains(&topic)); + for expected_topic in expected_topics { + assert!( + topics.contains(&expected_topic), + "Should contain {:?}", + expected_topic + ); } } } diff --git a/beacon_node/lighthouse_network/tests/common.rs b/beacon_node/lighthouse_network/tests/common.rs index 6a3ec6dd322..d979ef9265a 100644 --- a/beacon_node/lighthouse_network/tests/common.rs +++ b/beacon_node/lighthouse_network/tests/common.rs @@ -4,10 +4,11 @@ use lighthouse_network::Enr; use lighthouse_network::EnrExt; use lighthouse_network::Multiaddr; use lighthouse_network::{NetworkConfig, NetworkEvent}; -use slog::{debug, error, o, Drain}; use std::sync::Arc; use std::sync::Weak; use tokio::runtime::Runtime; +use tracing::{debug, error, info_span, Instrument}; +use tracing_subscriber::EnvFilter; use types::{ ChainSpec, EnrForkId, Epoch, EthSpec, FixedBytesExtended, ForkContext, ForkName, Hash256, MinimalEthSpec, Slot, @@ -15,6 +16,7 @@ use types::{ type E = MinimalEthSpec; +use lighthouse_network::rpc::config::InboundRateLimiterConfig; use tempfile::Builder as TempBuilder; /// Returns a dummy fork context @@ -67,19 +69,20 @@ impl std::ops::DerefMut for Libp2pInstance { } #[allow(unused)] -pub fn build_log(level: slog::Level, enabled: bool) -> slog::Logger { - let decorator = slog_term::TermDecorator::new().build(); - let drain = slog_term::FullFormat::new(decorator).build().fuse(); - let drain = slog_async::Async::new(drain).build().fuse(); - +pub fn build_tracing_subscriber(level: &str, enabled: bool) { if enabled { - slog::Logger::root(drain.filter_level(level).fuse(), o!()) - } else { - slog::Logger::root(drain.filter(|_| false).fuse(), o!()) + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::try_new(level).unwrap()) + .try_init() + .unwrap(); } } -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. @@ -95,22 +98,26 @@ 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) } pub async fn build_libp2p_instance( rt: Weak, boot_nodes: Vec, - log: slog::Logger, 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); let (shutdown_tx, _) = futures::channel::mpsc::channel(1); - let executor = task_executor::TaskExecutor::new(rt, exit, log.clone(), shutdown_tx); + let executor = task_executor::TaskExecutor::new(rt, exit, shutdown_tx, service_name); let libp2p_context = lighthouse_network::Context { config, enr_fork_id: EnrForkId::default(), @@ -119,7 +126,7 @@ pub async fn build_libp2p_instance( libp2p_registry: None, }; Libp2pInstance( - LibP2PService::new(executor, libp2p_context, &log) + LibP2PService::new(executor, libp2p_context) .await .expect("should build libp2p instance") .0, @@ -143,18 +150,32 @@ pub enum Protocol { #[allow(dead_code)] pub async fn build_node_pair( rt: Weak, - log: &slog::Logger, fork_name: ForkName, spec: Arc, protocol: Protocol, + disable_peer_scoring: bool, + inbound_rate_limiter: Option, ) -> (Libp2pInstance, Libp2pInstance) { - let sender_log = log.new(o!("who" => "sender")); - let receiver_log = log.new(o!("who" => "receiver")); - - let mut sender = - build_libp2p_instance(rt.clone(), vec![], sender_log, fork_name, spec.clone()).await; - let mut receiver = - build_libp2p_instance(rt, vec![], receiver_log, fork_name, spec.clone()).await; + let mut sender = build_libp2p_instance( + rt.clone(), + vec![], + 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 the two nodes set up listeners let sender_fut = async { @@ -179,7 +200,8 @@ pub async fn build_node_pair( } } } - }; + } + .instrument(info_span!("Sender", who = "sender")); let receiver_fut = async { loop { if let NetworkEvent::NewListenAddr(addr) = receiver.next_event().await { @@ -201,7 +223,8 @@ pub async fn build_node_pair( } } } - }; + } + .instrument(info_span!("Receiver", who = "receiver")); let joined = futures::future::join(sender_fut, receiver_fut); @@ -209,9 +232,9 @@ pub async fn build_node_pair( match sender.testing_dial(receiver_multiaddr.clone()) { Ok(()) => { - debug!(log, "Sender dialed receiver"; "address" => format!("{:?}", receiver_multiaddr)) + debug!(address = ?receiver_multiaddr, "Sender dialed receiver") } - Err(_) => error!(log, "Dialing failed"), + Err(_) => error!("Dialing failed"), }; (sender, receiver) } @@ -220,7 +243,6 @@ pub async fn build_node_pair( #[allow(dead_code)] pub async fn build_linear( rt: Weak, - log: slog::Logger, n: usize, fork_name: ForkName, spec: Arc, @@ -228,7 +250,16 @@ pub async fn build_linear( let mut nodes = Vec::with_capacity(n); for _ in 0..n { nodes.push( - build_libp2p_instance(rt.clone(), vec![], log.clone(), fork_name, spec.clone()).await, + build_libp2p_instance( + rt.clone(), + vec![], + fork_name, + spec.clone(), + "linear".to_string(), + false, + None, + ) + .await, ); } @@ -238,8 +269,8 @@ pub async fn build_linear( .collect(); for i in 0..n - 1 { match nodes[i].testing_dial(multiaddrs[i + 1].clone()) { - Ok(()) => debug!(log, "Connected"), - Err(_) => error!(log, "Failed to connect"), + Ok(()) => debug!("Connected"), + Err(_) => error!("Failed to connect"), }; } nodes diff --git a/beacon_node/lighthouse_network/tests/rpc_tests.rs b/beacon_node/lighthouse_network/tests/rpc_tests.rs index 0d83c4f74e5..9b43e8b5812 100644 --- a/beacon_node/lighthouse_network/tests/rpc_tests.rs +++ b/beacon_node/lighthouse_network/tests/rpc_tests.rs @@ -2,17 +2,17 @@ mod common; -use common::Protocol; +use common::{build_tracing_subscriber, Protocol}; use lighthouse_network::rpc::{methods::*, RequestType}; use lighthouse_network::service::api_types::AppRequestId; use lighthouse_network::{NetworkEvent, ReportSource, Response}; -use slog::{debug, warn, Level}; 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, error, warn}; use types::{ BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BeaconBlockBellatrix, BlobSidecar, ChainSpec, EmptyBlock, Epoch, EthSpec, FixedBytesExtended, ForkName, Hash256, MinimalEthSpec, @@ -53,24 +53,24 @@ fn bellatrix_block_large(spec: &ChainSpec) -> BeaconBlock { #[test] #[allow(clippy::single_match)] fn test_tcp_status_rpc() { - // set up the logging. The level and enabled logging or not - let log_level = Level::Debug; + // Set up the logging. + let log_level = "debug"; let enable_logging = false; + build_tracing_subscriber(log_level, enable_logging); let rt = Arc::new(Runtime::new().unwrap()); - let log = common::build_log(log_level, enable_logging); - 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), - &log, ForkName::Base, spec, Protocol::Tcp, + false, + None, ) .await; @@ -98,20 +98,20 @@ fn test_tcp_status_rpc() { match sender.next_event().await { NetworkEvent::PeerConnectedOutgoing(peer_id) => { // Send a STATUS message - debug!(log, "Sending RPC"); + debug!("Sending RPC"); sender .send_request(peer_id, AppRequestId::Router, rpc_request.clone()) .unwrap(); } NetworkEvent::ResponseReceived { peer_id: _, - id: AppRequestId::Router, + app_request_id: AppRequestId::Router, response, } => { // Should receive the RPC response - debug!(log, "Sender Received"); + debug!("Sender Received"); assert_eq!(response, rpc_response.clone()); - debug!(log, "Sender Completed"); + debug!("Sender Completed"); return; } _ => {} @@ -125,13 +125,17 @@ fn test_tcp_status_rpc() { match receiver.next_event().await { NetworkEvent::RequestReceived { peer_id, - id, - request, + inbound_request_id, + request_type, } => { - if request.r#type == rpc_request { + if request_type == rpc_request { // send the response - debug!(log, "Receiver Received"); - receiver.send_response(peer_id, id, request.id, rpc_response.clone()); + debug!("Receiver Received"); + receiver.send_response( + peer_id, + inbound_request_id, + rpc_response.clone(), + ); } } _ => {} // Ignore other events @@ -153,14 +157,13 @@ fn test_tcp_status_rpc() { #[test] #[allow(clippy::single_match)] fn test_tcp_blocks_by_range_chunked_rpc() { - // set up the logging. The level and enabled logging or not - let log_level = Level::Debug; + // Set up the logging. + let log_level = "debug"; let enable_logging = false; + build_tracing_subscriber(log_level, enable_logging); let messages_to_send = 6; - let log = common::build_log(log_level, enable_logging); - let rt = Arc::new(Runtime::new().unwrap()); let spec = Arc::new(E::default_spec()); @@ -169,10 +172,11 @@ fn test_tcp_blocks_by_range_chunked_rpc() { // get sender/receiver let (mut sender, mut receiver) = common::build_node_pair( Arc::downgrade(&rt), - &log, ForkName::Bellatrix, spec.clone(), Protocol::Tcp, + false, + None, ) .await; @@ -206,17 +210,17 @@ fn test_tcp_blocks_by_range_chunked_rpc() { match sender.next_event().await { NetworkEvent::PeerConnectedOutgoing(peer_id) => { // Send a STATUS message - debug!(log, "Sending RPC"); + debug!("Sending RPC"); sender .send_request(peer_id, AppRequestId::Router, rpc_request.clone()) .unwrap(); } NetworkEvent::ResponseReceived { peer_id: _, - id: _, + app_request_id: _, response, } => { - warn!(log, "Sender received a response"); + warn!("Sender received a response"); match response { Response::BlocksByRange(Some(_)) => { if messages_received < 2 { @@ -227,7 +231,7 @@ fn test_tcp_blocks_by_range_chunked_rpc() { assert_eq!(response, rpc_response_bellatrix_small.clone()); } messages_received += 1; - warn!(log, "Chunk received"); + warn!("Chunk received"); } Response::BlocksByRange(None) => { // should be exactly `messages_to_send` messages before terminating @@ -249,12 +253,12 @@ fn test_tcp_blocks_by_range_chunked_rpc() { match receiver.next_event().await { NetworkEvent::RequestReceived { peer_id, - id, - request, + inbound_request_id, + request_type, } => { - if request.r#type == rpc_request { + if request_type == rpc_request { // send the response - warn!(log, "Receiver got request"); + warn!("Receiver got request"); for i in 0..messages_to_send { // Send first third of responses as base blocks, // second as altair and third as bellatrix. @@ -267,16 +271,14 @@ fn test_tcp_blocks_by_range_chunked_rpc() { }; receiver.send_response( peer_id, - id, - request.id, + inbound_request_id, rpc_response.clone(), ); } // send the stream termination receiver.send_response( peer_id, - id, - request.id, + inbound_request_id, Response::BlocksByRange(None), ); } @@ -300,15 +302,14 @@ fn test_tcp_blocks_by_range_chunked_rpc() { #[test] #[allow(clippy::single_match)] fn test_blobs_by_range_chunked_rpc() { - // set up the logging. The level and enabled logging or not - let log_level = Level::Debug; + // Set up the logging. + let log_level = "debug"; let enable_logging = false; + build_tracing_subscriber(log_level, enable_logging); let slot_count = 32; let messages_to_send = 34; - let log = common::build_log(log_level, enable_logging); - let rt = Arc::new(Runtime::new().unwrap()); rt.block_on(async { @@ -316,10 +317,11 @@ fn test_blobs_by_range_chunked_rpc() { let spec = Arc::new(E::default_spec()); let (mut sender, mut receiver) = common::build_node_pair( Arc::downgrade(&rt), - &log, ForkName::Deneb, spec.clone(), Protocol::Tcp, + false, + None, ) .await; @@ -342,22 +344,22 @@ fn test_blobs_by_range_chunked_rpc() { match sender.next_event().await { NetworkEvent::PeerConnectedOutgoing(peer_id) => { // Send a STATUS message - debug!(log, "Sending RPC"); + debug!("Sending RPC"); sender .send_request(peer_id, AppRequestId::Router, rpc_request.clone()) .unwrap(); } NetworkEvent::ResponseReceived { peer_id: _, - id: _, + app_request_id: _, response, } => { - warn!(log, "Sender received a response"); + warn!("Sender received a response"); match response { Response::BlobsByRange(Some(_)) => { assert_eq!(response, rpc_response.clone()); messages_received += 1; - warn!(log, "Chunk received"); + warn!("Chunk received"); } Response::BlobsByRange(None) => { // should be exactly `messages_to_send` messages before terminating @@ -379,27 +381,25 @@ fn test_blobs_by_range_chunked_rpc() { match receiver.next_event().await { NetworkEvent::RequestReceived { peer_id, - id, - request, + inbound_request_id, + request_type, } => { - if request.r#type == rpc_request { + if request_type == rpc_request { // send the response - warn!(log, "Receiver got request"); + warn!("Receiver got request"); for _ in 0..messages_to_send { // Send first third of responses as base blocks, // second as altair and third as bellatrix. receiver.send_response( peer_id, - id, - request.id, + inbound_request_id, rpc_response.clone(), ); } // send the stream termination receiver.send_response( peer_id, - id, - request.id, + inbound_request_id, Response::BlobsByRange(None), ); } @@ -423,14 +423,13 @@ fn test_blobs_by_range_chunked_rpc() { #[test] #[allow(clippy::single_match)] fn test_tcp_blocks_by_range_over_limit() { - // set up the logging. The level and enabled logging or not - let log_level = Level::Debug; + // Set up the logging. + let log_level = "debug"; let enable_logging = false; + build_tracing_subscriber(log_level, enable_logging); let messages_to_send = 5; - let log = common::build_log(log_level, enable_logging); - let rt = Arc::new(Runtime::new().unwrap()); let spec = Arc::new(E::default_spec()); @@ -439,10 +438,11 @@ fn test_tcp_blocks_by_range_over_limit() { // get sender/receiver let (mut sender, mut receiver) = common::build_node_pair( Arc::downgrade(&rt), - &log, ForkName::Bellatrix, spec.clone(), Protocol::Tcp, + false, + None, ) .await; @@ -466,14 +466,14 @@ fn test_tcp_blocks_by_range_over_limit() { match sender.next_event().await { NetworkEvent::PeerConnectedOutgoing(peer_id) => { // Send a STATUS message - debug!(log, "Sending RPC"); + debug!("Sending RPC"); sender .send_request(peer_id, AppRequestId::Router, rpc_request.clone()) .unwrap(); } // The request will fail because the sender will refuse to send anything > MAX_RPC_SIZE - NetworkEvent::RPCFailed { id, .. } => { - assert!(matches!(id, AppRequestId::Router)); + NetworkEvent::RPCFailed { app_request_id, .. } => { + assert!(matches!(app_request_id, AppRequestId::Router)); return; } _ => {} // Ignore other behaviour events @@ -487,26 +487,24 @@ fn test_tcp_blocks_by_range_over_limit() { match receiver.next_event().await { NetworkEvent::RequestReceived { peer_id, - id, - request, + inbound_request_id, + request_type, } => { - if request.r#type == rpc_request { + if request_type == rpc_request { // send the response - warn!(log, "Receiver got request"); + warn!("Receiver got request"); for _ in 0..messages_to_send { let rpc_response = rpc_response_bellatrix_large.clone(); receiver.send_response( peer_id, - id, - request.id, + inbound_request_id, rpc_response.clone(), ); } // send the stream termination receiver.send_response( peer_id, - id, - request.id, + inbound_request_id, Response::BlocksByRange(None), ); } @@ -529,15 +527,14 @@ fn test_tcp_blocks_by_range_over_limit() { // Tests that a streamed BlocksByRange RPC Message terminates when all expected chunks were received #[test] fn test_tcp_blocks_by_range_chunked_rpc_terminates_correctly() { - // set up the logging. The level and enabled logging or not - let log_level = Level::Debug; + // Set up the logging. + let log_level = "debug"; let enable_logging = false; + build_tracing_subscriber(log_level, enable_logging); let messages_to_send = 10; let extra_messages_to_send = 10; - let log = common::build_log(log_level, enable_logging); - let rt = Arc::new(Runtime::new().unwrap()); let spec = Arc::new(E::default_spec()); @@ -546,10 +543,11 @@ fn test_tcp_blocks_by_range_chunked_rpc_terminates_correctly() { // get sender/receiver let (mut sender, mut receiver) = common::build_node_pair( Arc::downgrade(&rt), - &log, ForkName::Base, spec.clone(), Protocol::Tcp, + false, + None, ) .await; @@ -574,19 +572,19 @@ fn test_tcp_blocks_by_range_chunked_rpc_terminates_correctly() { match sender.next_event().await { NetworkEvent::PeerConnectedOutgoing(peer_id) => { // Send a STATUS message - debug!(log, "Sending RPC"); + debug!("Sending RPC"); sender .send_request(peer_id, AppRequestId::Router, rpc_request.clone()) .unwrap(); } NetworkEvent::ResponseReceived { peer_id: _, - id: _, + app_request_id: _, response, } => // Should receive the RPC response { - debug!(log, "Sender received a response"); + debug!("Sender received a response"); match response { Response::BlocksByRange(Some(_)) => { assert_eq!(response, rpc_response.clone()); @@ -623,15 +621,15 @@ fn test_tcp_blocks_by_range_chunked_rpc_terminates_correctly() { futures::future::Either::Left(( NetworkEvent::RequestReceived { peer_id, - id, - request, + inbound_request_id, + request_type, }, _, )) => { - if request.r#type == rpc_request { + if request_type == rpc_request { // send the response - warn!(log, "Receiver got request"); - message_info = Some((peer_id, id, request.id)); + warn!("Receiver got request"); + message_info = Some((peer_id, inbound_request_id)); } } futures::future::Either::Right((_, _)) => {} // The timeout hit, send messages if required @@ -641,9 +639,9 @@ fn test_tcp_blocks_by_range_chunked_rpc_terminates_correctly() { // if we need to send messages send them here. This will happen after a delay if message_info.is_some() { messages_sent += 1; - let (peer_id, stream_id, request_id) = message_info.as_ref().unwrap(); - receiver.send_response(*peer_id, *stream_id, *request_id, rpc_response.clone()); - debug!(log, "Sending message {}", messages_sent); + let (peer_id, inbound_request_id) = message_info.as_ref().unwrap(); + receiver.send_response(*peer_id, *inbound_request_id, rpc_response.clone()); + debug!("Sending message {}", messages_sent); if messages_sent == messages_to_send + extra_messages_to_send { // stop sending messages return; @@ -666,11 +664,11 @@ fn test_tcp_blocks_by_range_chunked_rpc_terminates_correctly() { #[test] #[allow(clippy::single_match)] fn test_tcp_blocks_by_range_single_empty_rpc() { - // set up the logging. The level and enabled logging or not - let log_level = Level::Trace; + // Set up the logging. + let log_level = "trace"; let enable_logging = false; + build_tracing_subscriber(log_level, enable_logging); - let log = common::build_log(log_level, enable_logging); let rt = Arc::new(Runtime::new().unwrap()); let spec = Arc::new(E::default_spec()); @@ -679,10 +677,11 @@ fn test_tcp_blocks_by_range_single_empty_rpc() { // get sender/receiver let (mut sender, mut receiver) = common::build_node_pair( Arc::downgrade(&rt), - &log, ForkName::Base, spec.clone(), Protocol::Tcp, + false, + None, ) .await; @@ -709,20 +708,20 @@ fn test_tcp_blocks_by_range_single_empty_rpc() { match sender.next_event().await { NetworkEvent::PeerConnectedOutgoing(peer_id) => { // Send a STATUS message - debug!(log, "Sending RPC"); + debug!("Sending RPC"); sender .send_request(peer_id, AppRequestId::Router, rpc_request.clone()) .unwrap(); } NetworkEvent::ResponseReceived { peer_id: _, - id: AppRequestId::Router, + app_request_id: AppRequestId::Router, response, } => match response { Response::BlocksByRange(Some(_)) => { assert_eq!(response, rpc_response.clone()); messages_received += 1; - warn!(log, "Chunk received"); + warn!("Chunk received"); } Response::BlocksByRange(None) => { // should be exactly 10 messages before terminating @@ -743,26 +742,24 @@ fn test_tcp_blocks_by_range_single_empty_rpc() { match receiver.next_event().await { NetworkEvent::RequestReceived { peer_id, - id, - request, + inbound_request_id, + request_type, } => { - if request.r#type == rpc_request { + if request_type == rpc_request { // send the response - warn!(log, "Receiver got request"); + warn!("Receiver got request"); for _ in 1..=messages_to_send { receiver.send_response( peer_id, - id, - request.id, + inbound_request_id, rpc_response.clone(), ); } // send the stream termination receiver.send_response( peer_id, - id, - request.id, + inbound_request_id, Response::BlocksByRange(None), ); } @@ -788,13 +785,13 @@ fn test_tcp_blocks_by_range_single_empty_rpc() { #[test] #[allow(clippy::single_match)] fn test_tcp_blocks_by_root_chunked_rpc() { - // set up the logging. The level and enabled logging or not - let log_level = Level::Debug; + // Set up the logging. + let log_level = "debug"; let enable_logging = false; + build_tracing_subscriber(log_level, enable_logging); let messages_to_send = 6; - let log = common::build_log(log_level, enable_logging); let spec = Arc::new(E::default_spec()); let rt = Arc::new(Runtime::new().unwrap()); @@ -802,10 +799,11 @@ fn test_tcp_blocks_by_root_chunked_rpc() { rt.block_on(async { let (mut sender, mut receiver) = common::build_node_pair( Arc::downgrade(&rt), - &log, ForkName::Bellatrix, spec.clone(), Protocol::Tcp, + false, + None, ) .await; @@ -847,14 +845,14 @@ fn test_tcp_blocks_by_root_chunked_rpc() { match sender.next_event().await { NetworkEvent::PeerConnectedOutgoing(peer_id) => { // Send a STATUS message - debug!(log, "Sending RPC"); + debug!("Sending RPC"); sender .send_request(peer_id, AppRequestId::Router, rpc_request.clone()) .unwrap(); } NetworkEvent::ResponseReceived { peer_id: _, - id: AppRequestId::Router, + app_request_id: AppRequestId::Router, response, } => match response { Response::BlocksByRoot(Some(_)) => { @@ -866,7 +864,7 @@ fn test_tcp_blocks_by_root_chunked_rpc() { assert_eq!(response, rpc_response_bellatrix_small.clone()); } messages_received += 1; - debug!(log, "Chunk received"); + debug!("Chunk received"); } Response::BlocksByRoot(None) => { // should be exactly messages_to_send @@ -887,12 +885,12 @@ fn test_tcp_blocks_by_root_chunked_rpc() { match receiver.next_event().await { NetworkEvent::RequestReceived { peer_id, - id, - request, + inbound_request_id, + request_type, } => { - if request.r#type == rpc_request { + if request_type == rpc_request { // send the response - debug!(log, "Receiver got request"); + debug!("Receiver got request"); for i in 0..messages_to_send { // Send equal base, altair and bellatrix blocks @@ -903,17 +901,16 @@ fn test_tcp_blocks_by_root_chunked_rpc() { } else { rpc_response_bellatrix_small.clone() }; - receiver.send_response(peer_id, id, request.id, rpc_response); - debug!(log, "Sending message"); + receiver.send_response(peer_id, inbound_request_id, rpc_response); + debug!("Sending message"); } // send the stream termination receiver.send_response( peer_id, - id, - request.id, + inbound_request_id, Response::BlocksByRange(None), ); - debug!(log, "Send stream term"); + debug!("Send stream term"); } } _ => {} // Ignore other events @@ -933,14 +930,14 @@ fn test_tcp_blocks_by_root_chunked_rpc() { // Tests a streamed, chunked BlocksByRoot RPC Message terminates when all expected reponses have been received #[test] fn test_tcp_blocks_by_root_chunked_rpc_terminates_correctly() { - // set up the logging. The level and enabled logging or not - let log_level = Level::Debug; + // Set up the logging. + let log_level = "debug"; let enable_logging = false; + build_tracing_subscriber(log_level, enable_logging); let messages_to_send: u64 = 10; let extra_messages_to_send: u64 = 10; - let log = common::build_log(log_level, enable_logging); let spec = Arc::new(E::default_spec()); let rt = Arc::new(Runtime::new().unwrap()); @@ -948,10 +945,11 @@ fn test_tcp_blocks_by_root_chunked_rpc_terminates_correctly() { rt.block_on(async { let (mut sender, mut receiver) = common::build_node_pair( Arc::downgrade(&rt), - &log, ForkName::Base, spec.clone(), Protocol::Tcp, + false, + None, ) .await; @@ -988,22 +986,22 @@ fn test_tcp_blocks_by_root_chunked_rpc_terminates_correctly() { match sender.next_event().await { NetworkEvent::PeerConnectedOutgoing(peer_id) => { // Send a STATUS message - debug!(log, "Sending RPC"); + debug!("Sending RPC"); sender .send_request(peer_id, AppRequestId::Router, rpc_request.clone()) .unwrap(); } NetworkEvent::ResponseReceived { peer_id: _, - id: AppRequestId::Router, + app_request_id: AppRequestId::Router, response, } => { - debug!(log, "Sender received a response"); + debug!("Sender received a response"); match response { Response::BlocksByRoot(Some(_)) => { assert_eq!(response, rpc_response.clone()); messages_received += 1; - debug!(log, "Chunk received"); + debug!("Chunk received"); } Response::BlocksByRoot(None) => { // should be exactly messages_to_send @@ -1037,15 +1035,15 @@ fn test_tcp_blocks_by_root_chunked_rpc_terminates_correctly() { futures::future::Either::Left(( NetworkEvent::RequestReceived { peer_id, - id, - request, + inbound_request_id, + request_type, }, _, )) => { - if request.r#type == rpc_request { + if request_type == rpc_request { // send the response - warn!(log, "Receiver got request"); - message_info = Some((peer_id, id, request.id)); + warn!("Receiver got request"); + message_info = Some((peer_id, inbound_request_id)); } } futures::future::Either::Right((_, _)) => {} // The timeout hit, send messages if required @@ -1055,9 +1053,9 @@ fn test_tcp_blocks_by_root_chunked_rpc_terminates_correctly() { // if we need to send messages send them here. This will happen after a delay if message_info.is_some() { messages_sent += 1; - let (peer_id, stream_id, request_id) = message_info.as_ref().unwrap(); - receiver.send_response(*peer_id, *stream_id, *request_id, rpc_response.clone()); - debug!(log, "Sending message {}", messages_sent); + let (peer_id, inbound_request_id) = message_info.as_ref().unwrap(); + receiver.send_response(*peer_id, *inbound_request_id, rpc_response.clone()); + debug!("Sending message {}", messages_sent); if messages_sent == messages_to_send + extra_messages_to_send { // stop sending messages return; @@ -1078,8 +1076,9 @@ fn test_tcp_blocks_by_root_chunked_rpc_terminates_correctly() { /// Establishes a pair of nodes and disconnects the pair based on the selected protocol via an RPC /// Goodbye message. -fn goodbye_test(log_level: Level, enable_logging: bool, protocol: Protocol) { - let log = common::build_log(log_level, enable_logging); +fn goodbye_test(log_level: &str, enable_logging: bool, protocol: Protocol) { + // Set up the logging. + build_tracing_subscriber(log_level, enable_logging); let rt = Arc::new(Runtime::new().unwrap()); @@ -1087,9 +1086,15 @@ fn goodbye_test(log_level: Level, enable_logging: bool, protocol: Protocol) { // get sender/receiver rt.block_on(async { - let (mut sender, mut receiver) = - common::build_node_pair(Arc::downgrade(&rt), &log, 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 { @@ -1097,7 +1102,7 @@ fn goodbye_test(log_level: Level, enable_logging: bool, protocol: Protocol) { match sender.next_event().await { NetworkEvent::PeerConnectedOutgoing(peer_id) => { // Send a goodbye and disconnect - debug!(log, "Sending RPC"); + debug!("Sending RPC"); sender.goodbye_peer( &peer_id, GoodbyeReason::IrrelevantNetwork, @@ -1137,18 +1142,252 @@ fn goodbye_test(log_level: Level, enable_logging: bool, protocol: Protocol) { #[test] #[allow(clippy::single_match)] fn tcp_test_goodbye_rpc() { - // set up the logging. The level and enabled logging or not - let log_level = Level::Debug; - let enable_logging = false; - goodbye_test(log_level, enable_logging, Protocol::Tcp); + let log_level = "debug"; + let enabled_logging = false; + goodbye_test(log_level, enabled_logging, Protocol::Tcp); } // Tests a Goodbye RPC message #[test] #[allow(clippy::single_match)] fn quic_test_goodbye_rpc() { - // set up the logging. The level and enabled logging or not - let log_level = Level::Debug; - let enable_logging = false; - goodbye_test(log_level, enable_logging, Protocol::Quic); + let log_level = "debug"; + 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/Cargo.toml b/beacon_node/network/Cargo.toml index 09179c4a516..4e369538803 100644 --- a/beacon_node/network/Cargo.toml +++ b/beacon_node/network/Cargo.toml @@ -10,12 +10,11 @@ eth2 = { workspace = true } eth2_network_config = { workspace = true } genesis = { workspace = true } gossipsub = { workspace = true } +k256 = "0.13.4" kzg = { workspace = true } matches = "0.1.8" +rand_chacha = "0.3.1" serde_json = { workspace = true } -slog-async = { workspace = true } -slog-term = { workspace = true } -sloggers = { workspace = true } [dependencies] alloy-primitives = { workspace = true } @@ -40,7 +39,6 @@ metrics = { workspace = true } operation_pool = { workspace = true } parking_lot = { workspace = true } rand = { workspace = true } -slog = { workspace = true } slot_clock = { workspace = true } smallvec = { workspace = true } ssz_types = { workspace = true } @@ -49,6 +47,8 @@ strum = { workspace = true } task_executor = { workspace = true } tokio = { workspace = true } tokio-stream = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } types = { workspace = true } [features] diff --git a/beacon_node/network/src/metrics.rs b/beacon_node/network/src/metrics.rs index 92b33495778..b129b548416 100644 --- a/beacon_node/network/src/metrics.rs +++ b/beacon_node/network/src/metrics.rs @@ -14,6 +14,7 @@ pub use metrics::*; use std::sync::{Arc, LazyLock}; use strum::AsRefStr; use strum::IntoEnumIterator; +use types::DataColumnSubnetId; use types::EthSpec; pub const SUCCESS: &str = "SUCCESS"; @@ -383,11 +384,18 @@ pub static PEERS_PER_SYNC_TYPE: LazyLock> = LazyLock::new(|| }); pub static PEERS_PER_COLUMN_SUBNET: LazyLock> = LazyLock::new(|| { try_create_int_gauge_vec( - "peers_per_column_subnet", + "sync_peers_per_column_subnet", "Number of connected peers per column subnet", &["subnet_id"], ) }); +pub static PEERS_PER_CUSTODY_COLUMN_SUBNET: LazyLock> = LazyLock::new(|| { + try_create_int_gauge_vec( + "sync_peers_per_custody_column_subnet", + "Number of connected peers per custody column subnet", + &["subnet_id"], + ) +}); pub static SYNCING_CHAINS_COUNT: LazyLock> = LazyLock::new(|| { try_create_int_gauge_vec( "sync_range_chains", @@ -755,16 +763,42 @@ pub fn update_sync_metrics(network_globals: &Arc>) // count per sync status, the number of connected peers let mut peers_per_sync_type = FnvHashMap::default(); - for sync_type in network_globals - .peers - .read() - .connected_peers() - .map(|(_peer_id, info)| info.sync_status().as_str()) - { + let mut peers_per_column_subnet = FnvHashMap::default(); + + for (_, info) in network_globals.peers.read().connected_peers() { + let sync_type = info.sync_status().as_str(); *peers_per_sync_type.entry(sync_type).or_default() += 1; + + for subnet in info.custody_subnets_iter() { + *peers_per_column_subnet.entry(*subnet).or_default() += 1; + } } for (sync_type, peer_count) in peers_per_sync_type { set_gauge_entry(&PEERS_PER_SYNC_TYPE, &[sync_type], peer_count); } + + let all_column_subnets = + (0..network_globals.spec.data_column_sidecar_subnet_count).map(DataColumnSubnetId::new); + let custody_column_subnets = network_globals.sampling_subnets.iter(); + + // Iterate all subnet values to set to zero the empty entries in peers_per_column_subnet + for subnet in all_column_subnets { + set_gauge_entry( + &PEERS_PER_COLUMN_SUBNET, + &[&format!("{subnet}")], + peers_per_column_subnet.get(&subnet).copied().unwrap_or(0), + ); + } + + // Registering this metric is a duplicate for supernodes but helpful for fullnodes. This way + // operators can monitor the health of only the subnets of their interest without complex + // Grafana queries. + for subnet in custody_column_subnets { + set_gauge_entry( + &PEERS_PER_CUSTODY_COLUMN_SUBNET, + &[&format!("{subnet}")], + peers_per_column_subnet.get(subnet).copied().unwrap_or(0), + ); + } } diff --git a/beacon_node/network/src/nat.rs b/beacon_node/network/src/nat.rs index e63ff550398..ce9d241d43d 100644 --- a/beacon_node/network/src/nat.rs +++ b/beacon_node/network/src/nat.rs @@ -5,10 +5,10 @@ use anyhow::{bail, Context, Error}; use igd_next::{aio::tokio as igd, PortMappingProtocol}; -use slog::debug; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::time::Duration; use tokio::time::sleep; +use tracing::debug; /// The duration in seconds of a port mapping on the gateway. const MAPPING_DURATION: u32 = 3600; @@ -17,11 +17,7 @@ const MAPPING_DURATION: u32 = 3600; const MAPPING_TIMEOUT: u64 = MAPPING_DURATION as u64 / 2; /// Attempts to map Discovery external port mappings with UPnP. -pub async fn construct_upnp_mappings( - addr: Ipv4Addr, - port: u16, - log: slog::Logger, -) -> Result<(), Error> { +pub async fn construct_upnp_mappings(addr: Ipv4Addr, port: u16) -> Result<(), Error> { let gateway = igd::search_gateway(Default::default()) .await .context("Gateway does not support UPnP")?; @@ -54,7 +50,7 @@ pub async fn construct_upnp_mappings( ) .await .with_context(|| format!("Could not UPnP map port: {} on the gateway", port))?; - debug!(log, "Discovery UPnP port mapped"; "port" => %port); + debug!(%port,"Discovery UPnP port mapped"); sleep(Duration::from_secs(MAPPING_TIMEOUT)).await; } } diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index af75791e4d7..cf0e98cda89 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -21,8 +21,8 @@ use beacon_chain::{ GossipVerifiedBlock, NotifyExecutionLayer, }; use lighthouse_network::{Client, MessageAcceptance, MessageId, PeerAction, PeerId, ReportSource}; +use logging::crit; use operation_pool::ReceivedPreCapella; -use slog::{crit, debug, error, info, trace, warn, Logger}; use slot_clock::SlotClock; use ssz::Encode; use std::fs; @@ -32,6 +32,7 @@ use std::sync::Arc; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use store::hot_cold_store::HotColdDBError; use tokio::sync::mpsc; +use tracing::{debug, error, info, trace, warn}; use types::{ beacon_block::BlockImportSource, Attestation, AttestationData, AttestationRef, AttesterSlashing, BlobSidecar, DataColumnSidecar, DataColumnSubnetId, EthSpec, Hash256, @@ -251,9 +252,8 @@ impl NetworkBeaconProcessor { Ok(results) => results, Err(e) => { error!( - self.log, - "Batch unagg. attn verification failed"; - "error" => ?e + error = ?e, + "Batch unagg. attn verification failed" ); return; } @@ -264,10 +264,9 @@ impl NetworkBeaconProcessor { // The log is `crit` since in this scenario we might be penalizing/rewarding the wrong // peer. crit!( - self.log, - "Batch attestation result mismatch"; - "results" => results.len(), - "packages" => packages.len(), + results = results.len(), + packages = packages.len(), + "Batch attestation result mismatch" ) } @@ -355,19 +354,17 @@ impl NetworkBeaconProcessor { e, )) => { debug!( - self.log, - "Attestation invalid for fork choice"; - "reason" => ?e, - "peer" => %peer_id, - "beacon_block_root" => ?beacon_block_root + reason = ?e, + %peer_id, + ?beacon_block_root, + "Attestation invalid for fork choice" ) } e => error!( - self.log, - "Error applying attestation to fork choice"; - "reason" => ?e, - "peer" => %peer_id, - "beacon_block_root" => ?beacon_block_root + reason = ?e, + %peer_id, + ?beacon_block_root, + "Error applying attestation to fork choice" ), } } @@ -377,11 +374,10 @@ impl NetworkBeaconProcessor { .add_to_naive_aggregation_pool(&verified_attestation) { debug!( - self.log, - "Attestation invalid for agg pool"; - "reason" => ?e, - "peer" => %peer_id, - "beacon_block_root" => ?beacon_block_root + reason = ?e, + %peer_id, + ?beacon_block_root, + "Attestation invalid for agg pool" ) } @@ -459,10 +455,9 @@ impl NetworkBeaconProcessor { seen_timestamp, ) { error!( - &self.log, - "Unable to queue converted SingleAttestation"; - "error" => %e, - "slot" => slot, + error = %e, + %slot, + "Unable to queue converted SingleAttestation" ); self.propagate_validation_result( message_id, @@ -486,11 +481,7 @@ impl NetworkBeaconProcessor { beacon_block_root, )) .unwrap_or_else(|_| { - warn!( - self.log, - "Failed to send to sync service"; - "msg" => "UnknownBlockHash" - ) + warn!(msg = "UnknownBlockHash", "Failed to send to sync service") }); let processor = self.clone(); // Do not allow this attestation to be re-processed beyond this point. @@ -510,10 +501,7 @@ impl NetworkBeaconProcessor { }), }); if sender.try_send(reprocess_msg).is_err() { - error!( - self.log, - "Failed to send attestation for re-processing"; - ) + error!("Failed to send attestation for re-processing") } } else { // We shouldn't make any further attempts to process this attestation. @@ -611,9 +599,8 @@ impl NetworkBeaconProcessor { Ok(results) => results, Err(e) => { error!( - self.log, - "Batch agg. attn verification failed"; - "error" => ?e + error = ?e, + "Batch agg. attn verification failed" ); return; } @@ -624,10 +611,9 @@ impl NetworkBeaconProcessor { // The log is `crit` since in this scenario we might be penalizing/rewarding the wrong // peer. crit!( - self.log, - "Batch agg. attestation result mismatch"; - "results" => results.len(), - "packages" => packages.len(), + results = results.len(), + packages = packages.len(), + "Batch agg. attestation result mismatch" ) } @@ -707,30 +693,27 @@ impl NetworkBeaconProcessor { e, )) => { debug!( - self.log, - "Aggregate invalid for fork choice"; - "reason" => ?e, - "peer" => %peer_id, - "beacon_block_root" => ?beacon_block_root + reason = ?e, + %peer_id, + ?beacon_block_root, + "Aggregate invalid for fork choice" ) } e => error!( - self.log, - "Error applying aggregate to fork choice"; - "reason" => ?e, - "peer" => %peer_id, - "beacon_block_root" => ?beacon_block_root + reason = ?e, + %peer_id, + ?beacon_block_root, + "Error applying aggregate to fork choice" ), } } if let Err(e) = self.chain.add_to_block_inclusion_pool(verified_aggregate) { debug!( - self.log, - "Attestation invalid for op pool"; - "reason" => ?e, - "peer" => %peer_id, - "beacon_block_root" => ?beacon_block_root + reason = ?e, + %peer_id, + ?beacon_block_root, + "Attestation invalid for op pool" ) } @@ -786,11 +769,10 @@ impl NetworkBeaconProcessor { ); debug!( - self.log, - "Successfully verified gossip data column sidecar"; - "slot" => %slot, - "block_root" => %block_root, - "index" => %index, + %slot, + %block_root, + %index, + "Successfully verified gossip data column sidecar" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept); @@ -817,11 +799,10 @@ impl NetworkBeaconProcessor { match err { GossipDataColumnError::ParentUnknown { parent_root } => { debug!( - self.log, - "Unknown parent hash for column"; - "action" => "requesting parent", - "block_root" => %block_root, - "parent_root" => %parent_root, + action = "requesting parent", + %block_root, + %parent_root, + "Unknown parent hash for column" ); self.send_sync_message(SyncMessage::UnknownParentDataColumn( peer_id, @@ -831,9 +812,8 @@ impl NetworkBeaconProcessor { GossipDataColumnError::PubkeyCacheTimeout | GossipDataColumnError::BeaconChainError(_) => { crit!( - self.log, - "Internal error when verifying column sidecar"; - "error" => ?err, + error = ?err, + "Internal error when verifying column sidecar" ) } GossipDataColumnError::ProposalSignatureInvalid @@ -845,15 +825,15 @@ impl NetworkBeaconProcessor { | GossipDataColumnError::InvalidKzgProof { .. } | GossipDataColumnError::UnexpectedDataColumn | GossipDataColumnError::InvalidColumnIndex(_) - | GossipDataColumnError::InconsistentCommitmentsOrProofLength + | GossipDataColumnError::InconsistentCommitmentsLength { .. } + | GossipDataColumnError::InconsistentProofsLength { .. } | GossipDataColumnError::NotFinalizedDescendant { .. } => { debug!( - self.log, - "Could not verify column sidecar for gossip. Rejecting the column sidecar"; - "error" => ?err, - "slot" => %slot, - "block_root" => %block_root, - "index" => %index, + error = ?err, + %slot, + %block_root, + %index, + "Could not verify column sidecar for gossip. Rejecting the column sidecar" ); // Prevent recurring behaviour by penalizing the peer slightly. self.gossip_penalize_peer( @@ -872,22 +852,20 @@ impl NetworkBeaconProcessor { // Do not penalise the peer. // Gossip filter should filter any duplicates received after this. debug!( - self.log, - "Received already available column sidecar. Ignoring the column sidecar"; - "slot" => %slot, - "block_root" => %block_root, - "index" => %index, + %slot, + %block_root, + %index, + "Received already available column sidecar. Ignoring the column sidecar" ) } GossipDataColumnError::FutureSlot { .. } | GossipDataColumnError::PastFinalizedSlot { .. } => { debug!( - self.log, - "Could not verify column sidecar for gossip. Ignoring the column sidecar"; - "error" => ?err, - "slot" => %slot, - "block_root" => %block_root, - "index" => %index, + error = ?err, + %slot, + %block_root, + %index, + "Could not verify column sidecar for gossip. Ignoring the column sidecar" ); // Prevent recurring behaviour by penalizing the peer slightly. self.gossip_penalize_peer( @@ -933,23 +911,21 @@ impl NetworkBeaconProcessor { if delay >= self.chain.slot_clock.unagg_attestation_production_delay() { metrics::inc_counter(&metrics::BEACON_BLOB_GOSSIP_ARRIVED_LATE_TOTAL); debug!( - self.log, - "Gossip blob arrived late"; - "block_root" => ?gossip_verified_blob.block_root(), - "proposer_index" => gossip_verified_blob.block_proposer_index(), - "slot" => gossip_verified_blob.slot(), - "delay" => ?delay, - "commitment" => %gossip_verified_blob.kzg_commitment(), + block_root = ?gossip_verified_blob.block_root(), + proposer_index = gossip_verified_blob.block_proposer_index(), + slot = %gossip_verified_blob.slot(), + delay = ?delay, + commitment = %gossip_verified_blob.kzg_commitment(), + "Gossip blob arrived late" ); } debug!( - self.log, - "Successfully verified gossip blob"; - "slot" => %slot, - "root" => %root, - "index" => %index, - "commitment" => %gossip_verified_blob.kzg_commitment(), + %slot, + %root, + %index, + commitment = %gossip_verified_blob.kzg_commitment(), + "Successfully verified gossip blob" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept); @@ -972,12 +948,11 @@ impl NetworkBeaconProcessor { match err { GossipBlobError::BlobParentUnknown { parent_root } => { debug!( - self.log, - "Unknown parent hash for blob"; - "action" => "requesting parent", - "block_root" => %root, - "parent_root" => %parent_root, - "commitment" => %commitment, + action = "requesting parent", + block_root = %root, + parent_root = %parent_root, + %commitment, + "Unknown parent hash for blob" ); self.send_sync_message(SyncMessage::UnknownParentBlob( peer_id, @@ -986,9 +961,8 @@ impl NetworkBeaconProcessor { } GossipBlobError::PubkeyCacheTimeout | GossipBlobError::BeaconChainError(_) => { crit!( - self.log, - "Internal error when verifying blob sidecar"; - "error" => ?err, + error = ?err, + "Internal error when verifying blob sidecar" ) } GossipBlobError::ProposalSignatureInvalid @@ -1000,13 +974,12 @@ impl NetworkBeaconProcessor { | GossipBlobError::KzgError(_) | GossipBlobError::NotFinalizedDescendant { .. } => { warn!( - self.log, - "Could not verify blob sidecar for gossip. Rejecting the blob sidecar"; - "error" => ?err, - "slot" => %slot, - "root" => %root, - "index" => %index, - "commitment" => %commitment, + error = ?err, + %slot, + %root, + %index, + %commitment, + "Could not verify blob sidecar for gossip. Rejecting the blob sidecar" ); // Prevent recurring behaviour by penalizing the peer. self.gossip_penalize_peer( @@ -1024,22 +997,20 @@ impl NetworkBeaconProcessor { // We may have received the blob from the EL. Do not penalise the peer. // Gossip filter should filter any duplicates received after this. debug!( - self.log, - "Received already available blob sidecar. Ignoring the blob sidecar"; - "slot" => %slot, - "root" => %root, - "index" => %index, + %slot, + %root, + %index, + "Received already available blob sidecar. Ignoring the blob sidecar" ) } GossipBlobError::FutureSlot { .. } => { debug!( - self.log, - "Could not verify blob sidecar for gossip. Ignoring the blob sidecar"; - "error" => ?err, - "slot" => %slot, - "root" => %root, - "index" => %index, - "commitment" => %commitment, + error = ?err, + %slot, + %root, + %index, + %commitment, + "Could not verify blob sidecar for gossip. Ignoring the blob sidecar" ); // Prevent recurring behaviour by penalizing the peer slightly. self.gossip_penalize_peer( @@ -1055,13 +1026,12 @@ impl NetworkBeaconProcessor { } GossipBlobError::PastFinalizedSlot { .. } => { debug!( - self.log, - "Could not verify blob sidecar for gossip. Ignoring the blob sidecar"; - "error" => ?err, - "slot" => %slot, - "root" => %root, - "index" => %index, - "commitment" => %commitment, + error = ?err, + %slot, + %root, + %index, + %commitment, + "Could not verify blob sidecar for gossip. Ignoring the blob sidecar" ); // Prevent recurring behaviour by penalizing the peer. A low-tolerance // error is fine because there's no reason for peers to be propagating old @@ -1099,9 +1069,8 @@ impl NetworkBeaconProcessor { match &result { Ok(AvailabilityProcessingStatus::Imported(block_root)) => { info!( - self.log, - "Gossipsub blob processed - imported fully available block"; - "block_root" => %block_root + %block_root, + "Gossipsub blob processed - imported fully available block" ); self.chain.recompute_head_at_current_slot().await; @@ -1112,29 +1081,25 @@ impl NetworkBeaconProcessor { } Ok(AvailabilityProcessingStatus::MissingComponents(slot, block_root)) => { debug!( - self.log, - "Processed gossip blob - waiting for other components"; - "slot" => %slot, - "blob_index" => %blob_index, - "block_root" => %block_root, + %slot, + %blob_index, + %block_root, + "Processed gossip blob - waiting for other components" ); } Err(BlockError::DuplicateFullyImported(_)) => { debug!( - self.log, - "Ignoring gossip blob already imported"; - "block_root" => ?block_root, - "blob_index" => blob_index, + ?block_root, + blob_index, "Ignoring gossip blob already imported" ); } Err(err) => { debug!( - self.log, - "Invalid gossip blob"; - "outcome" => ?err, - "block_root" => ?block_root, - "block_slot" => blob_slot, - "blob_index" => blob_index, + outcome = ?err, + ?block_root, + %blob_slot, + blob_index, + "Invalid gossip blob" ); self.gossip_penalize_peer( peer_id, @@ -1177,9 +1142,8 @@ impl NetworkBeaconProcessor { Ok(availability) => match availability { AvailabilityProcessingStatus::Imported(block_root) => { info!( - self.log, - "Gossipsub data column processed, imported fully available block"; - "block_root" => %block_root + %block_root, + "Gossipsub data column processed, imported fully available block" ); self.chain.recompute_head_at_current_slot().await; @@ -1190,32 +1154,29 @@ impl NetworkBeaconProcessor { } AvailabilityProcessingStatus::MissingComponents(slot, block_root) => { trace!( - self.log, - "Processed data column, waiting for other components"; - "slot" => %slot, - "data_column_index" => %data_column_index, - "block_root" => %block_root, + %slot, + %data_column_index, + %block_root, + "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(_)) => { debug!( - self.log, - "Ignoring gossip column already imported"; - "block_root" => ?block_root, - "data_column_index" => data_column_index, + ?block_root, + data_column_index, "Ignoring gossip column already imported" ); } Err(err) => { debug!( - self.log, - "Invalid gossip data column"; - "outcome" => ?err, - "block root" => ?block_root, - "block slot" => data_column_slot, - "data column index" => data_column_index, + outcome = ?err, + ?block_root, + block_slot = %data_column_slot, + data_column_index, + "Invalid gossip data column" ); self.gossip_penalize_peer( peer_id, @@ -1271,9 +1232,8 @@ impl NetworkBeaconProcessor { drop(handle); } else { debug!( - self.log, - "RPC block is being imported"; - "block_root" => %block_root, + %block_root, + "RPC block is being imported" ); } } @@ -1299,7 +1259,10 @@ impl NetworkBeaconProcessor { let verification_result = self .chain .clone() - .verify_block_for_gossip(block.clone()) + .verify_block_for_gossip( + block.clone(), + self.network_globals.custody_columns_count() as usize, + ) .await; if verification_result.is_ok() { @@ -1329,20 +1292,18 @@ impl NetworkBeaconProcessor { if block_delay >= self.chain.slot_clock.unagg_attestation_production_delay() { metrics::inc_counter(&metrics::BEACON_BLOCK_DELAY_GOSSIP_ARRIVED_LATE_TOTAL); debug!( - self.log, - "Gossip block arrived late"; - "block_root" => ?verified_block.block_root, - "proposer_index" => verified_block.block.message().proposer_index(), - "slot" => verified_block.block.slot(), - "block_delay" => ?block_delay, + block_root = ?verified_block.block_root, + proposer_index = verified_block.block.message().proposer_index(), + slot = ?verified_block.block.slot(), + ?block_delay, + "Gossip block arrived late" ); } info!( - self.log, - "New block received"; - "slot" => verified_block.block.slot(), - "root" => ?verified_block.block_root + slot = %verified_block.block.slot(), + root = ?verified_block.block_root, + "New block received" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept); @@ -1362,9 +1323,8 @@ impl NetworkBeaconProcessor { } Err(e @ BlockError::Slashable) => { warn!( - self.log, - "Received equivocating block from peer"; - "error" => ?e + error = ?e, + "Received equivocating block from peer" ); /* punish peer for submitting an equivocation, but not too harshly as honest peers may conceivably forward equivocating blocks to us from time to time */ self.gossip_penalize_peer( @@ -1375,19 +1335,14 @@ impl NetworkBeaconProcessor { return None; } Err(BlockError::ParentUnknown { .. }) => { - debug!( - self.log, - "Unknown parent for gossip block"; - "root" => ?block_root - ); + debug!(?block_root, "Unknown parent for gossip block"); self.send_sync_message(SyncMessage::UnknownParentBlock(peer_id, block, block_root)); return None; } Err(e @ BlockError::BeaconChainError(_)) => { debug!( - self.log, - "Gossip block beacon chain error"; - "error" => ?e, + error = ?e, + "Gossip block beacon chain error" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); return None; @@ -1397,18 +1352,16 @@ impl NetworkBeaconProcessor { | BlockError::DuplicateImportStatusUnknown(..), ) => { debug!( - self.log, - "Gossip block is already known"; - "block_root" => %block_root, + %block_root, + "Gossip block is already known" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); return None; } Err(e @ BlockError::FutureSlot { .. }) => { debug!( - self.log, - "Could not verify block for gossip. Ignoring the block"; - "error" => %e + error = %e, + "Could not verify block for gossip. Ignoring the block" ); // Prevent recurring behaviour by penalizing the peer slightly. self.gossip_penalize_peer( @@ -1422,9 +1375,8 @@ impl NetworkBeaconProcessor { Err(e @ BlockError::WouldRevertFinalizedSlot { .. }) | Err(e @ BlockError::NotFinalizedDescendant { .. }) => { debug!( - self.log, - "Could not verify block for gossip. Ignoring the block"; - "error" => %e + error = %e, + "Could not verify block for gossip. Ignoring the block" ); // The spec says we must IGNORE these blocks but there's no reason for an honest // and non-buggy client to be gossiping blocks that blatantly conflict with @@ -1439,8 +1391,7 @@ impl NetworkBeaconProcessor { return None; } Err(ref e @ BlockError::ExecutionPayloadError(ref epe)) if !epe.penalize_peer() => { - debug!(self.log, "Could not verify block for gossip. Ignoring the block"; - "error" => %e); + debug!(error = %e, "Could not verify block for gossip. Ignoring the block"); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); return None; } @@ -1459,8 +1410,7 @@ impl NetworkBeaconProcessor { | Err(e @ BlockError::ParentExecutionPayloadInvalid { .. }) | Err(e @ BlockError::KnownInvalidExecutionPayload(_)) | Err(e @ BlockError::GenesisBlock) => { - warn!(self.log, "Could not verify block for gossip. Rejecting the block"; - "error" => %e); + warn!(error = %e, "Could not verify block for gossip. Rejecting the block"); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); self.gossip_penalize_peer( peer_id, @@ -1472,26 +1422,12 @@ impl NetworkBeaconProcessor { // Note: This error variant cannot be reached when doing gossip validation // as we do not do availability checks here. Err(e @ BlockError::AvailabilityCheck(_)) => { - crit!(self.log, "Internal block gossip validation error. Availability check during - gossip validation"; - "error" => %e - ); - return None; - } - Err(e @ BlockError::InternalError(_)) => { - error!(self.log, "Internal block gossip validation error"; - "error" => %e - ); + crit!(error = %e, "Internal block gossip validation error. Availability check during gossip validation"); return None; } - Err(e @ BlockError::BlobNotRequired(_)) => { - // TODO(das): penalty not implemented yet as other clients may still send us blobs - // during early stage of implementation. - debug!(self.log, "Received blobs for slot after PeerDAS epoch from peer"; - "error" => %e, - "peer_id" => %peer_id, - ); - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); + // BlobNotRequired is unreachable. Only constructed in `process_gossip_blob` + Err(e @ BlockError::InternalError(_)) | Err(e @ BlockError::BlobNotRequired(_)) => { + error!(error = %e, "Internal block gossip validation error"); return None; } }; @@ -1520,11 +1456,10 @@ impl NetworkBeaconProcessor { // tolerance for block imports. Ok(current_slot) if block_slot > current_slot => { warn!( - self.log, - "Block arrived early"; - "block_slot" => %block_slot, - "block_root" => ?block_root, - "msg" => "if this happens consistently, check system clock" + %block_slot, + ?block_root, + msg = "if this happens consistently, check system clock", + "Block arrived early" ); // Take note of how early this block arrived. @@ -1565,11 +1500,10 @@ impl NetworkBeaconProcessor { .is_err() { error!( - self.log, - "Failed to defer block import"; - "block_slot" => %block_slot, - "block_root" => ?block_root, - "location" => "block gossip" + %block_slot, + ?block_root, + location = "block gossip", + "Failed to defer block import" ) } None @@ -1577,12 +1511,11 @@ impl NetworkBeaconProcessor { Ok(_) => Some(verified_block), Err(e) => { error!( - self.log, - "Failed to defer block import"; - "error" => ?e, - "block_slot" => %block_slot, - "block_root" => ?block_root, - "location" => "block gossip" + error = ?e, + %block_slot, + ?block_root, + location = "block gossip", + "Failed to defer block import" ); None } @@ -1604,9 +1537,10 @@ impl NetworkBeaconProcessor { let block = verified_block.block.block_cloned(); let block_root = verified_block.block_root; - // TODO(das) Might be too early to issue a request here. We haven't checked that the block - // actually includes blob transactions and thus has data. A peer could send a block is - // garbage commitments, and make us trigger sampling for a block that does not have data. + // Note: okay to issue sampling request before the block is execution verified. If the + // proposer sends us a block with invalid blob transactions it can trigger us to issue + // sampling queries that will never resolve. This attack is equivalent to withholding data. + // Dismissed proposal to move this block to post-execution: https://github.com/sigp/lighthouse/pull/6492 if block.num_expected_blobs() > 0 { // Trigger sampling for block not yet execution valid. At this point column custodials are // unlikely to have received their columns. Triggering sampling so early is only viable with @@ -1653,18 +1587,16 @@ impl NetworkBeaconProcessor { .is_err() { error!( - self.log, - "Failed to inform block import"; - "source" => "gossip", - "block_root" => ?block_root, + source = "gossip", + ?block_root, + "Failed to inform block import" ) }; debug!( - self.log, - "Gossipsub block processed"; - "block" => ?block_root, - "peer_id" => %peer_id + ?block_root, + %peer_id, + "Gossipsub block processed" ); self.chain.recompute_head_at_current_slot().await; @@ -1676,10 +1608,9 @@ impl NetworkBeaconProcessor { } Ok(AvailabilityProcessingStatus::MissingComponents(slot, block_root)) => { trace!( - self.log, - "Processed block, waiting for other components"; - "slot" => slot, - "block_root" => %block_root, + %slot, + %block_root, + "Processed block, waiting for other components" ); } Err(BlockError::ParentUnknown { .. }) => { @@ -1689,26 +1620,23 @@ impl NetworkBeaconProcessor { // can recover by receiving another block / blob / attestation referencing the // chain that includes this block. error!( - self.log, - "Block with unknown parent attempted to be processed"; - "block_root" => %block_root, - "peer_id" => %peer_id + %block_root, + %peer_id, + "Block with unknown parent attempted to be processed" ); } Err(ref e @ BlockError::ExecutionPayloadError(ref epe)) if !epe.penalize_peer() => { debug!( - self.log, - "Failed to verify execution payload"; - "error" => %e + error = %e, + "Failed to verify execution payload" ); } Err(BlockError::AvailabilityCheck(err)) => { match err.category() { AvailabilityCheckErrorCategory::Internal => { warn!( - self.log, - "Internal availability check error"; - "error" => ?err, + error = ?err, + "Internal availability check error" ); } AvailabilityCheckErrorCategory::Malicious => { @@ -1720,20 +1648,18 @@ impl NetworkBeaconProcessor { // 2. The proposer being malicious and sending inconsistent // blocks and blobs. warn!( - self.log, - "Received invalid blob or malicious proposer"; - "error" => ?err + error = ?err, + "Received invalid blob or malicious proposer" ); } } } other => { debug!( - self.log, - "Invalid gossip beacon block"; - "outcome" => ?other, - "block root" => ?block_root, - "block slot" => block.slot() + outcome = ?other, + ?block_root, + block_slot = %block.slot(), + "Invalid gossip beacon block" ); self.gossip_penalize_peer( peer_id, @@ -1741,21 +1667,14 @@ impl NetworkBeaconProcessor { "bad_gossip_block_ssz", ); trace!( - self.log, - "Invalid gossip beacon block ssz"; - "ssz" => format_args!("0x{}", hex::encode(block.as_ssz_bytes())), + ssz = format_args!("0x{}", hex::encode(block.as_ssz_bytes())), + "Invalid gossip beacon block ssz" ); } }; if let Err(e) = &result { - self.maybe_store_invalid_block( - &invalid_block_storage, - block_root, - &block, - e, - &self.log, - ); + self.maybe_store_invalid_block(&invalid_block_storage, block_root, &block, e); } self.send_sync_message(SyncMessage::GossipBlockProcessResult { @@ -1777,20 +1696,18 @@ impl NetworkBeaconProcessor { Ok(ObservationOutcome::AlreadyKnown) => { self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); debug!( - self.log, - "Dropping exit for already exiting validator"; - "validator_index" => validator_index, - "peer" => %peer_id + validator_index, + peer = %peer_id, + "Dropping exit for already exiting validator" ); return; } Err(e) => { debug!( - self.log, - "Dropping invalid exit"; - "validator_index" => validator_index, - "peer" => %peer_id, - "error" => ?e + validator_index, + %peer_id, + error = ?e, + "Dropping invalid exit" ); // These errors occur due to a fault in the beacon chain. It is not necessarily // the fault on the peer. @@ -1817,7 +1734,7 @@ impl NetworkBeaconProcessor { self.chain.import_voluntary_exit(exit); - debug!(self.log, "Successfully imported voluntary exit"); + debug!("Successfully imported voluntary exit"); metrics::inc_counter(&metrics::BEACON_PROCESSOR_EXIT_IMPORTED_TOTAL); } @@ -1837,11 +1754,10 @@ impl NetworkBeaconProcessor { Ok(ObservationOutcome::New(slashing)) => slashing, Ok(ObservationOutcome::AlreadyKnown) => { debug!( - self.log, - "Dropping proposer slashing"; - "reason" => "Already seen a proposer slashing for that validator", - "validator_index" => validator_index, - "peer" => %peer_id + reason = "Already seen a proposer slashing for that validator", + validator_index, + peer = %peer_id, + "Dropping proposer slashing" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); return; @@ -1850,11 +1766,10 @@ impl NetworkBeaconProcessor { // This is likely a fault with the beacon chain and not necessarily a // malicious message from the peer. debug!( - self.log, - "Dropping invalid proposer slashing"; - "validator_index" => validator_index, - "peer" => %peer_id, - "error" => ?e + validator_index, + %peer_id, + error = ?e, + "Dropping invalid proposer slashing" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); @@ -1879,7 +1794,7 @@ impl NetworkBeaconProcessor { .register_gossip_proposer_slashing(slashing.as_inner()); self.chain.import_proposer_slashing(slashing); - debug!(self.log, "Successfully imported proposer slashing"); + debug!("Successfully imported proposer slashing"); metrics::inc_counter(&metrics::BEACON_PROCESSOR_PROPOSER_SLASHING_IMPORTED_TOTAL); } @@ -1897,20 +1812,18 @@ impl NetworkBeaconProcessor { Ok(ObservationOutcome::New(slashing)) => slashing, Ok(ObservationOutcome::AlreadyKnown) => { debug!( - self.log, - "Dropping attester slashing"; - "reason" => "Slashings already known for all slashed validators", - "peer" => %peer_id + reason = "Slashings already known for all slashed validators", + peer = %peer_id, + "Dropping attester slashing" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); return; } Err(e) => { debug!( - self.log, - "Dropping invalid attester slashing"; - "peer" => %peer_id, - "error" => ?e + %peer_id, + error = ?e, + "Dropping invalid attester slashing" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); // Penalize peer slightly for invalids. @@ -1934,7 +1847,7 @@ impl NetworkBeaconProcessor { .register_gossip_attester_slashing(slashing.as_inner().to_ref()); self.chain.import_attester_slashing(slashing); - debug!(self.log, "Successfully imported attester slashing"); + debug!("Successfully imported attester slashing"); metrics::inc_counter(&metrics::BEACON_PROCESSOR_ATTESTER_SLASHING_IMPORTED_TOTAL); } @@ -1955,20 +1868,18 @@ impl NetworkBeaconProcessor { Ok(ObservationOutcome::AlreadyKnown) => { self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); debug!( - self.log, - "Dropping BLS to execution change"; - "validator_index" => validator_index, - "peer" => %peer_id + validator_index, + peer = %peer_id, + "Dropping BLS to execution change" ); return; } Err(e) => { debug!( - self.log, - "Dropping invalid BLS to execution change"; - "validator_index" => validator_index, - "peer" => %peer_id, - "error" => ?e + validator_index, + %peer_id, + error = ?e, + "Dropping invalid BLS to execution change" ); // We ignore pre-capella messages without penalizing peers. if matches!(e, BeaconChainError::BlsToExecutionPriorToCapella) { @@ -2006,10 +1917,9 @@ impl NetworkBeaconProcessor { .import_bls_to_execution_change(change, received_pre_capella); debug!( - self.log, - "Successfully imported BLS to execution change"; - "validator_index" => validator_index, - "address" => ?address, + validator_index, + ?address, + "Successfully imported BLS to execution change" ); metrics::inc_counter(&metrics::BEACON_PROCESSOR_BLS_TO_EXECUTION_CHANGE_IMPORTED_TOTAL); @@ -2068,10 +1978,9 @@ impl NetworkBeaconProcessor { .add_to_naive_sync_aggregation_pool(sync_signature) { debug!( - self.log, - "Sync committee signature invalid for agg pool"; - "reason" => ?e, - "peer" => %peer_id, + reason = ?e, + %peer_id, + "Sync committee signature invalid for agg pool" ) } @@ -2130,10 +2039,9 @@ impl NetworkBeaconProcessor { .add_contribution_to_block_inclusion_pool(sync_contribution) { debug!( - self.log, - "Sync contribution invalid for op pool"; - "reason" => ?e, - "peer" => %peer_id, + reason = ?e, + %peer_id, + "Sync contribution invalid for op pool" ) } metrics::inc_counter(&metrics::BEACON_PROCESSOR_SYNC_CONTRIBUTION_IMPORTED_TOTAL); @@ -2158,10 +2066,9 @@ impl NetworkBeaconProcessor { match e { LightClientFinalityUpdateError::InvalidLightClientFinalityUpdate => { debug!( - self.log, - "Light client invalid finality update"; - "peer" => %peer_id, - "error" => ?e, + %peer_id, + error = ?e, + "Light client invalid finality update" ); self.gossip_penalize_peer( @@ -2172,10 +2079,9 @@ impl NetworkBeaconProcessor { } LightClientFinalityUpdateError::TooEarly => { debug!( - self.log, - "Light client finality update too early"; - "peer" => %peer_id, - "error" => ?e, + %peer_id, + error = ?e, + "Light client finality update too early" ); self.gossip_penalize_peer( @@ -2186,10 +2092,9 @@ impl NetworkBeaconProcessor { } LightClientFinalityUpdateError::SigSlotStartIsNone | LightClientFinalityUpdateError::FailedConstructingUpdate => debug!( - self.log, - "Light client error constructing finality update"; - "peer" => %peer_id, - "error" => ?e, + %peer_id, + error = ?e, + "Light client error constructing finality update" ), } self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); @@ -2211,10 +2116,9 @@ impl NetworkBeaconProcessor { ) { Ok(verified_light_client_optimistic_update) => { debug!( - self.log, - "Light client successful optimistic update"; - "peer" => %peer_id, - "parent_root" => %verified_light_client_optimistic_update.parent_root, + %peer_id, + parent_root = %verified_light_client_optimistic_update.parent_root, + "Light client successful optimistic update" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept); @@ -2226,10 +2130,9 @@ impl NetworkBeaconProcessor { &metrics::BEACON_PROCESSOR_REPROCESSING_QUEUE_SENT_OPTIMISTIC_UPDATES, ); debug!( - self.log, - "Optimistic update for unknown block"; - "peer_id" => %peer_id, - "parent_root" => ?parent_root + %peer_id, + ?parent_root, + "Optimistic update for unknown block" ); if let Some(sender) = reprocess_tx { @@ -2250,17 +2153,13 @@ impl NetworkBeaconProcessor { ); if sender.try_send(msg).is_err() { - error!( - self.log, - "Failed to send optimistic update for re-processing"; - ) + error!("Failed to send optimistic update for re-processing") } } else { debug!( - self.log, - "Not sending light client update because it had been reprocessed"; - "peer_id" => %peer_id, - "parent_root" => ?parent_root + %peer_id, + ?parent_root, + "Not sending light client update because it had been reprocessed" ); self.propagate_validation_result( @@ -2275,10 +2174,9 @@ impl NetworkBeaconProcessor { metrics::register_optimistic_update_error(&e); debug!( - self.log, - "Light client invalid optimistic update"; - "peer" => %peer_id, - "error" => ?e, + %peer_id, + error = ?e, + "Light client invalid optimistic update" ); self.gossip_penalize_peer( @@ -2290,10 +2188,9 @@ impl NetworkBeaconProcessor { LightClientOptimisticUpdateError::TooEarly => { metrics::register_optimistic_update_error(&e); debug!( - self.log, - "Light client optimistic update too early"; - "peer" => %peer_id, - "error" => ?e, + %peer_id, + error = ?e, + "Light client optimistic update too early" ); self.gossip_penalize_peer( @@ -2307,10 +2204,9 @@ impl NetworkBeaconProcessor { metrics::register_optimistic_update_error(&e); debug!( - self.log, - "Light client error constructing optimistic update"; - "peer" => %peer_id, - "error" => ?e, + %peer_id, + error = ?e, + "Light client error constructing optimistic update" ) } } @@ -2342,11 +2238,10 @@ impl NetworkBeaconProcessor { * The peer has published an invalid consensus message, _only_ if we trust our own clock. */ trace!( - self.log, - "Attestation is not within the last ATTESTATION_PROPAGATION_SLOT_RANGE slots"; - "peer_id" => %peer_id, - "block" => ?beacon_block_root, - "type" => ?attestation_type, + %peer_id, + block = ?beacon_block_root, + ?attestation_type, + "Attestation is not within the last ATTESTATION_PROPAGATION_SLOT_RANGE slots" ); // Peers that are slow or not to spec can spam us with these messages draining our @@ -2473,11 +2368,10 @@ impl NetworkBeaconProcessor { * The peer is not necessarily faulty. */ trace!( - self.log, - "Attestation already known"; - "peer_id" => %peer_id, - "block" => ?beacon_block_root, - "type" => ?attestation_type, + %peer_id, + block = ?beacon_block_root, + ?attestation_type, + "Attestation already known" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); return; @@ -2490,11 +2384,10 @@ impl NetworkBeaconProcessor { * The peer is not necessarily faulty. */ trace!( - self.log, - "Aggregator already known"; - "peer_id" => %peer_id, - "block" => ?beacon_block_root, - "type" => ?attestation_type, + %peer_id, + block = ?beacon_block_root, + ?attestation_type, + "Aggregator already known" ); // This is an allowed behaviour. self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); @@ -2511,13 +2404,12 @@ impl NetworkBeaconProcessor { * The peer is not necessarily faulty. */ debug!( - self.log, - "Prior attestation known"; - "peer_id" => %peer_id, - "block" => ?beacon_block_root, - "epoch" => %epoch, - "validator_index" => validator_index, - "type" => ?attestation_type, + %peer_id, + block = ?beacon_block_root, + %epoch, + validator_index, + ?attestation_type, + "Prior attestation known" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); @@ -2532,11 +2424,10 @@ impl NetworkBeaconProcessor { * The peer has published an invalid consensus message. */ debug!( - self.log, - "Validation Index too high"; - "peer_id" => %peer_id, - "block" => ?beacon_block_root, - "type" => ?attestation_type, + %peer_id, + block = ?beacon_block_root, + ?attestation_type, + "Validation Index too high" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); self.gossip_penalize_peer( @@ -2552,12 +2443,11 @@ impl NetworkBeaconProcessor { * The peer has published an invalid consensus message. */ debug!( - self.log, - "Committee index non zero"; - "peer_id" => %peer_id, - "block" => ?beacon_block_root, - "type" => ?attestation_type, - "committee_index" => index, + %peer_id, + block = ?beacon_block_root, + ?attestation_type, + committee_index = index, + "Committee index non zero" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); self.gossip_penalize_peer( @@ -2568,10 +2458,9 @@ impl NetworkBeaconProcessor { } AttnError::UnknownHeadBlock { beacon_block_root } => { trace!( - self.log, - "Attestation for unknown block"; - "peer_id" => %peer_id, - "block" => ?beacon_block_root + %peer_id, + block = ?beacon_block_root, + "Attestation for unknown block" ); if let Some(sender) = reprocess_tx { // We don't know the block, get the sync manager to handle the block lookup, and @@ -2582,11 +2471,7 @@ impl NetworkBeaconProcessor { *beacon_block_root, )) .unwrap_or_else(|_| { - warn!( - self.log, - "Failed to send to sync service"; - "msg" => "UnknownBlockHash" - ) + warn!(msg = "UnknownBlockHash", "Failed to send to sync service") }); let msg = match failed_att { FailedAtt::Aggregate { @@ -2615,9 +2500,8 @@ impl NetworkBeaconProcessor { // for `SingleAttestation`s separately and should not be able to hit // an `UnknownHeadBlock` error. error!( - self.log, - "Dropping SingleAttestation instead of requeueing"; - "block_root" => ?beacon_block_root, + block_root = ?beacon_block_root, + "Dropping SingleAttestation instead of requeueing" ); return; } @@ -2649,10 +2533,7 @@ impl NetworkBeaconProcessor { }; if sender.try_send(msg).is_err() { - error!( - self.log, - "Failed to send attestation for re-processing"; - ) + error!("Failed to send attestation for re-processing") } } else { // We shouldn't make any further attempts to process this attestation. @@ -2763,10 +2644,9 @@ impl NetworkBeaconProcessor { * The attestation was received on an incorrect subnet id. */ debug!( - self.log, - "Received attestation on incorrect subnet"; - "expected" => ?expected, - "received" => ?received, + ?expected, + ?received, + "Received attestation on incorrect subnet" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); self.gossip_penalize_peer( @@ -2824,10 +2704,9 @@ impl NetworkBeaconProcessor { * The message is not necessarily invalid, but we choose to ignore it. */ debug!( - self.log, - "Rejected long skip slot attestation"; - "head_block_slot" => head_block_slot, - "attestation_slot" => attestation_slot, + %head_block_slot, + %attestation_slot, + "Rejected long skip slot attestation" ); // In this case we wish to penalize gossipsub peers that do this to avoid future // attestations that have too many skip slots. @@ -2840,10 +2719,9 @@ impl NetworkBeaconProcessor { } AttnError::HeadBlockFinalized { beacon_block_root } => { debug!( - self.log, - "Ignored attestation to finalized block"; - "block_root" => ?beacon_block_root, - "attestation_slot" => failed_att.attestation_data().slot, + block_root = ?beacon_block_root, + attestation_slot = %failed_att.attestation_data().slot, + "Ignored attestation to finalized block" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); @@ -2859,19 +2737,18 @@ impl NetworkBeaconProcessor { AttnError::BeaconChainError(BeaconChainError::DBError(Error::HotColdDBError( HotColdDBError::FinalizedStateNotInHotDatabase { .. }, ))) => { - debug!(self.log, "Attestation for finalized state"; "peer_id" => % peer_id); + debug!(%peer_id, "Attestation for finalized state"); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); } e @ AttnError::BeaconChainError(BeaconChainError::MaxCommitteePromises(_)) => { debug!( - self.log, - "Dropping attestation"; - "target_root" => ?failed_att.attestation_data().target.root, - "beacon_block_root" => ?beacon_block_root, - "slot" => ?failed_att.attestation_data().slot, - "type" => ?attestation_type, - "error" => ?e, - "peer_id" => % peer_id + 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); } @@ -2884,25 +2761,23 @@ impl NetworkBeaconProcessor { * It's not clear if the message is invalid/malicious. */ error!( - self.log, - "Unable to validate attestation"; - "beacon_block_root" => ?beacon_block_root, - "slot" => ?failed_att.attestation_data().slot, - "type" => ?attestation_type, - "peer_id" => %peer_id, - "error" => ?e, + ?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); } } debug!( - self.log, - "Invalid attestation from network"; - "reason" => ?error, - "block" => ?beacon_block_root, - "peer_id" => %peer_id, - "type" => ?attestation_type, + reason = ?error, + block = ?beacon_block_root, + %peer_id, + ?attestation_type, + "Invalid attestation from network" ); } @@ -2928,10 +2803,9 @@ impl NetworkBeaconProcessor { * The peer has published an invalid consensus message, _only_ if we trust our own clock. */ trace!( - self.log, - "Sync committee message is not within the last MAXIMUM_GOSSIP_CLOCK_DISPARITY slots"; - "peer_id" => %peer_id, - "type" => ?message_type, + %peer_id, + ?message_type, + "Sync committee message is not within the last MAXIMUM_GOSSIP_CLOCK_DISPARITY slots" ); // Unlike attestations, we have a zero slot buffer in case of sync committee messages, @@ -2953,10 +2827,9 @@ impl NetworkBeaconProcessor { * The peer has published an invalid consensus message, _only_ if we trust our own clock. */ trace!( - self.log, - "Sync committee message is not within the last MAXIMUM_GOSSIP_CLOCK_DISPARITY slots"; - "peer_id" => %peer_id, - "type" => ?message_type, + %peer_id, + ?message_type, + "Sync committee message is not within the last MAXIMUM_GOSSIP_CLOCK_DISPARITY slots" ); // Compute the slot when we received the message. @@ -3046,10 +2919,9 @@ impl NetworkBeaconProcessor { * The peer is not necessarily faulty. */ trace!( - self.log, - "Sync committee message is already known"; - "peer_id" => %peer_id, - "type" => ?message_type, + %peer_id, + ?message_type, + "Sync committee message is already known" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); return; @@ -3062,10 +2934,9 @@ impl NetworkBeaconProcessor { * The peer has published an invalid consensus message. */ debug!( - self.log, - "Validation Index too high"; - "peer_id" => %peer_id, - "type" => ?message_type, + %peer_id, + ?message_type, + "Validation Index too high" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); self.gossip_penalize_peer( @@ -3076,10 +2947,9 @@ impl NetworkBeaconProcessor { } SyncCommitteeError::UnknownValidatorPubkey(_) => { debug!( - self.log, - "Validator pubkey is unknown"; - "peer_id" => %peer_id, - "type" => ?message_type, + %peer_id, + ?message_type, + "Validator pubkey is unknown" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); self.gossip_penalize_peer( @@ -3093,10 +2963,9 @@ impl NetworkBeaconProcessor { * The sync committee message was received on an incorrect subnet id. */ debug!( - self.log, - "Received sync committee message on incorrect subnet"; - "expected" => ?expected, - "received" => ?received, + ?expected, + ?received, + "Received sync committee message on incorrect subnet" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); self.gossip_penalize_peer( @@ -3125,10 +2994,9 @@ impl NetworkBeaconProcessor { * The peer is not necessarily faulty. */ debug!( - self.log, - "Prior sync committee message known"; - "peer_id" => %peer_id, - "type" => ?message_type, + %peer_id, + ?message_type, + "Prior sync committee message known" ); // Do not penalize the peer. @@ -3144,10 +3012,9 @@ impl NetworkBeaconProcessor { * The peer is not necessarily faulty. */ debug!( - self.log, - "Prior sync contribution message known"; - "peer_id" => %peer_id, - "type" => ?message_type, + %peer_id, + ?message_type, + "Prior sync contribution message known" ); // We still penalize the peer slightly. We don't want this to be a recurring // behaviour. @@ -3170,10 +3037,9 @@ impl NetworkBeaconProcessor { * It's not clear if the message is invalid/malicious. */ error!( - self.log, - "Unable to validate sync committee message"; - "peer_id" => %peer_id, - "error" => ?e, + %peer_id, + error = ?e, + "Unable to validate sync committee message" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); } @@ -3186,10 +3052,9 @@ impl NetworkBeaconProcessor { * It's not clear if the message is invalid/malicious. */ error!( - self.log, - "Unable to validate sync committee message"; - "peer_id" => %peer_id, - "error" => ?e, + %peer_id, + error = ?e, + "Unable to validate sync committee message" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); // Penalize the peer slightly @@ -3201,10 +3066,9 @@ impl NetworkBeaconProcessor { } SyncCommitteeError::ContributionError(e) => { error!( - self.log, - "Error while processing sync contribution"; - "peer_id" => %peer_id, - "error" => ?e, + %peer_id, + error = ?e, + "Error while processing sync contribution" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); // Penalize the peer slightly @@ -3216,10 +3080,9 @@ impl NetworkBeaconProcessor { } SyncCommitteeError::SyncCommitteeError(e) => { error!( - self.log, - "Error while processing sync committee message"; - "peer_id" => %peer_id, - "error" => ?e, + %peer_id, + error = ?e, + "Error while processing sync committee message" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); // Penalize the peer slightly @@ -3234,10 +3097,9 @@ impl NetworkBeaconProcessor { This would most likely imply incompatible configs or an invalid message. */ error!( - self.log, - "Arithematic error while processing sync committee message"; - "peer_id" => %peer_id, - "error" => ?e, + %peer_id, + error = ?e, + "Arithematic error while processing sync committee message" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); self.gossip_penalize_peer( @@ -3260,11 +3122,10 @@ impl NetworkBeaconProcessor { } } debug!( - self.log, - "Invalid sync committee message from network"; - "reason" => ?error, - "peer_id" => %peer_id, - "type" => ?message_type, + reason = ?error, + %peer_id, + ?message_type, + "Invalid sync committee message from network" ); } @@ -3322,7 +3183,6 @@ impl NetworkBeaconProcessor { block_root: Hash256, block: &SignedBeaconBlock, error: &BlockError, - log: &Logger, ) { if let InvalidBlockStorage::Enabled(base_dir) = invalid_block_storage { let block_path = base_dir.join(format!("{}_{:?}.ssz", block.slot(), block_root)); @@ -3350,20 +3210,18 @@ impl NetworkBeaconProcessor { }); if let Err(e) = write_result { error!( - log, - "Failed to store invalid block/error"; - "error" => e, - "path" => ?path, - "root" => ?block_root, - "slot" => block.slot(), + error = e, + ?path, + ?block_root, + slot = %block.slot(), + "Failed to store invalid block/error" ) } else { info!( - log, - "Stored invalid block/error "; - "path" => ?path, - "root" => ?block_root, - "slot" => block.slot(), + ?path, + ?block_root, + slot = %block.slot(), + "Stored invalid block/error" ) } }; diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 13c7df8095b..ba681eed14b 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -9,34 +9,28 @@ use beacon_chain::fetch_blobs::{ }; use beacon_chain::observed_data_sidecars::DoNotObserve; use beacon_chain::{ - builder::Witness, eth1_chain::CachingEth1Backend, AvailabilityProcessingStatus, BeaconChain, - BeaconChainTypes, BlockError, NotifyExecutionLayer, + AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError, NotifyExecutionLayer, }; use beacon_processor::{ - work_reprocessing_queue::ReprocessQueueMessage, BeaconProcessorChannels, BeaconProcessorSend, - DuplicateCache, GossipAggregatePackage, GossipAttestationPackage, Work, - WorkEvent as BeaconWorkEvent, + work_reprocessing_queue::ReprocessQueueMessage, BeaconProcessorSend, DuplicateCache, + GossipAggregatePackage, GossipAttestationPackage, Work, WorkEvent as BeaconWorkEvent, }; -use lighthouse_network::discovery::ConnectionId; use lighthouse_network::rpc::methods::{ BlobsByRangeRequest, BlobsByRootRequest, DataColumnsByRangeRequest, DataColumnsByRootRequest, LightClientUpdatesByRangeRequest, }; -use lighthouse_network::rpc::{RequestId, SubstreamId}; +use lighthouse_network::rpc::InboundRequestId; use lighthouse_network::{ rpc::{BlocksByRangeRequest, BlocksByRootRequest, LightClientBootstrapRequest, StatusMessage}, Client, MessageId, NetworkGlobals, PeerId, PubsubMessage, }; use rand::prelude::SliceRandom; -use slog::{debug, error, trace, warn, Logger}; -use slot_clock::ManualSlotClock; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; -use store::MemoryStore; use task_executor::TaskExecutor; -use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::{self, error::TrySendError}; +use tracing::{debug, error, trace, warn, Instrument}; use types::*; pub use sync_methods::ChainSegmentProcessId; @@ -71,7 +65,6 @@ pub struct NetworkBeaconProcessor { pub network_globals: Arc>, pub invalid_block_storage: InvalidBlockStorage, pub executor: TaskExecutor, - pub log: Logger, } // Publish blobs in batches of exponentially increasing size. @@ -600,10 +593,7 @@ impl NetworkBeaconProcessor { blocks: Vec>, ) -> Result<(), Error> { let is_backfill = matches!(&process_id, ChainSegmentProcessId::BackSyncBatchId { .. }); - debug!(self.log, "Batch sending for process"; - "blocks" => blocks.len(), - "id" => ?process_id, - ); + debug!(blocks = blocks.len(), id = ?process_id, "Batch sending for process"); let processor = self.clone(); let process_fn = async move { @@ -656,21 +646,13 @@ impl NetworkBeaconProcessor { pub fn send_blocks_by_range_request( self: &Arc, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, // Use ResponseId here request: BlocksByRangeRequest, ) -> Result<(), Error> { let processor = self.clone(); let process_fn = async move { processor - .handle_blocks_by_range_request( - peer_id, - connection_id, - substream_id, - request_id, - request, - ) + .handle_blocks_by_range_request(peer_id, inbound_request_id, request) .await; }; @@ -684,21 +666,13 @@ impl NetworkBeaconProcessor { pub fn send_blocks_by_roots_request( self: &Arc, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, // Use ResponseId here request: BlocksByRootRequest, ) -> Result<(), Error> { let processor = self.clone(); let process_fn = async move { processor - .handle_blocks_by_root_request( - peer_id, - connection_id, - substream_id, - request_id, - request, - ) + .handle_blocks_by_root_request(peer_id, inbound_request_id, request) .await; }; @@ -712,21 +686,12 @@ impl NetworkBeaconProcessor { pub fn send_blobs_by_range_request( self: &Arc, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, request: BlobsByRangeRequest, ) -> Result<(), Error> { let processor = self.clone(); - let process_fn = move || { - processor.handle_blobs_by_range_request( - peer_id, - connection_id, - substream_id, - request_id, - request, - ) - }; + let process_fn = + move || processor.handle_blobs_by_range_request(peer_id, inbound_request_id, request); self.try_send(BeaconWorkEvent { drop_during_sync: false, @@ -738,21 +703,12 @@ impl NetworkBeaconProcessor { pub fn send_blobs_by_roots_request( self: &Arc, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, request: BlobsByRootRequest, ) -> Result<(), Error> { let processor = self.clone(); - let process_fn = move || { - processor.handle_blobs_by_root_request( - peer_id, - connection_id, - substream_id, - request_id, - request, - ) - }; + let process_fn = + move || processor.handle_blobs_by_root_request(peer_id, inbound_request_id, request); self.try_send(BeaconWorkEvent { drop_during_sync: false, @@ -764,20 +720,12 @@ impl NetworkBeaconProcessor { pub fn send_data_columns_by_roots_request( self: &Arc, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, request: DataColumnsByRootRequest, ) -> Result<(), Error> { let processor = self.clone(); let process_fn = move || { - processor.handle_data_columns_by_root_request( - peer_id, - connection_id, - substream_id, - request_id, - request, - ) + processor.handle_data_columns_by_root_request(peer_id, inbound_request_id, request) }; self.try_send(BeaconWorkEvent { @@ -790,20 +738,12 @@ impl NetworkBeaconProcessor { pub fn send_data_columns_by_range_request( self: &Arc, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, request: DataColumnsByRangeRequest, ) -> Result<(), Error> { let processor = self.clone(); let process_fn = move || { - processor.handle_data_columns_by_range_request( - peer_id, - connection_id, - substream_id, - request_id, - request, - ) + processor.handle_data_columns_by_range_request(peer_id, inbound_request_id, request) }; self.try_send(BeaconWorkEvent { @@ -816,21 +756,12 @@ impl NetworkBeaconProcessor { pub fn send_light_client_bootstrap_request( self: &Arc, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, request: LightClientBootstrapRequest, ) -> Result<(), Error> { let processor = self.clone(); - let process_fn = move || { - processor.handle_light_client_bootstrap( - peer_id, - connection_id, - substream_id, - request_id, - request, - ) - }; + let process_fn = + move || processor.handle_light_client_bootstrap(peer_id, inbound_request_id, request); self.try_send(BeaconWorkEvent { drop_during_sync: true, @@ -842,19 +773,11 @@ impl NetworkBeaconProcessor { pub fn send_light_client_optimistic_update_request( self: &Arc, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, ) -> Result<(), Error> { let processor = self.clone(); - let process_fn = move || { - processor.handle_light_client_optimistic_update( - peer_id, - connection_id, - substream_id, - request_id, - ) - }; + let process_fn = + move || processor.handle_light_client_optimistic_update(peer_id, inbound_request_id); self.try_send(BeaconWorkEvent { drop_during_sync: true, @@ -866,19 +789,11 @@ impl NetworkBeaconProcessor { pub fn send_light_client_finality_update_request( self: &Arc, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, ) -> Result<(), Error> { let processor = self.clone(); - let process_fn = move || { - processor.handle_light_client_finality_update( - peer_id, - connection_id, - substream_id, - request_id, - ) - }; + let process_fn = + move || processor.handle_light_client_finality_update(peer_id, inbound_request_id); self.try_send(BeaconWorkEvent { drop_during_sync: true, @@ -890,20 +805,12 @@ impl NetworkBeaconProcessor { pub fn send_light_client_updates_by_range_request( self: &Arc, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, request: LightClientUpdatesByRangeRequest, ) -> Result<(), Error> { let processor = self.clone(); let process_fn = move || { - processor.handle_light_client_updates_by_range( - peer_id, - connection_id, - substream_id, - request_id, - request, - ) + processor.handle_light_client_updates_by_range(peer_id, inbound_request_id, request) }; self.try_send(BeaconWorkEvent { @@ -916,10 +823,9 @@ impl NetworkBeaconProcessor { /// /// Creates a log if there is an internal error. pub(crate) fn send_sync_message(&self, message: SyncMessage) { - self.sync_tx.send(message).unwrap_or_else(|e| { - debug!(self.log, "Could not send message to the sync service"; - "error" => %e) - }); + self.sync_tx + .send(message) + .unwrap_or_else(|e| debug!(error = %e, "Could not send message to the sync service")); } /// Send a message to `network_tx`. @@ -927,8 +833,7 @@ impl NetworkBeaconProcessor { /// Creates a log if there is an internal error. fn send_network_message(&self, message: NetworkMessage) { self.network_tx.send(message).unwrap_or_else(|e| { - debug!(self.log, "Could not send message to the network service. Likely shutdown"; - "error" => %e) + debug!(error = %e, "Could not send message to the network service. Likely shutdown") }); } @@ -938,6 +843,7 @@ impl NetworkBeaconProcessor { block_root: Hash256, publish_blobs: bool, ) { + let custody_columns = self.network_globals.sampling_columns.clone(); let self_cloned = self.clone(); let publish_fn = move |blobs_or_data_column| { if publish_blobs { @@ -956,50 +862,51 @@ impl NetworkBeaconProcessor { self.chain.clone(), block_root, block.clone(), + custody_columns, publish_fn, ) + .instrument(tracing::info_span!( + "", + service = "fetch_engine_blobs", + block_root = format!("{:?}", block_root) + )) .await { Ok(Some(availability)) => match availability { AvailabilityProcessingStatus::Imported(_) => { debug!( - self.log, - "Block components retrieved from EL"; - "result" => "imported block and custody columns", - "block_root" => %block_root, + result = "imported block and custody columns", + %block_root, + "Block components retrieved from EL" ); self.chain.recompute_head_at_current_slot().await; } AvailabilityProcessingStatus::MissingComponents(_, _) => { debug!( - self.log, - "Still missing blobs after engine blobs processed successfully"; - "block_root" => %block_root, + %block_root, + "Still missing blobs after engine blobs processed successfully" ); } }, Ok(None) => { debug!( - self.log, - "Fetch blobs completed without import"; - "block_root" => %block_root, + %block_root, + "Fetch blobs completed without import" ); } Err(FetchEngineBlobError::BlobProcessingError(BlockError::DuplicateFullyImported( .., ))) => { debug!( - self.log, - "Fetch blobs duplicate import"; - "block_root" => %block_root, + %block_root, + "Fetch blobs duplicate import" ); } Err(e) => { error!( - self.log, - "Error fetching or processing blobs from EL"; - "error" => ?e, - "block_root" => %block_root, + error = ?e, + %block_root, + "Error fetching or processing blobs from EL" ); } } @@ -1011,30 +918,39 @@ 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() { + return None; + } + 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!( - self.log, - "Block components available via reconstruction"; - "result" => "imported block and custody columns", - "block_hash" => %hash, + result = "imported block and custody columns", + block_hash = %hash, + "Block components available via reconstruction" ); self.chain.recompute_head_at_current_slot().await; } AvailabilityProcessingStatus::MissingComponents(_, _) => { debug!( - self.log, - "Block components still missing block after reconstruction"; - "result" => "imported all custody columns", - "block_hash" => %block_root, + result = "imported all custody columns", + block_hash = %block_root, + "Block components still missing block after reconstruction" ); } } @@ -1044,18 +960,16 @@ impl NetworkBeaconProcessor { Ok(None) => { // reason is tracked via the `KZG_DATA_COLUMN_RECONSTRUCTION_INCOMPLETE_TOTAL` metric trace!( - self.log, - "Reconstruction not required for block"; - "block_hash" => %block_root, + block_hash = %block_root, + "Reconstruction not required for block" ); None } Err(e) => { error!( - self.log, - "Error during data column reconstruction"; - "block_root" => %block_root, - "error" => ?e + %block_root, + error = ?e, + "Error during data column reconstruction" ); None } @@ -1078,7 +992,6 @@ impl NetworkBeaconProcessor { self.executor.spawn( async move { let chain = self_clone.chain.clone(); - let log = self_clone.chain.logger(); let publish_fn = |blobs: Vec>>| { self_clone.send_network_message(NetworkMessage::Publish { messages: blobs @@ -1107,9 +1020,8 @@ impl NetworkBeaconProcessor { Err(GossipBlobError::RepeatBlob { .. }) => None, Err(e) => { warn!( - log, - "Previously verified blob is invalid"; - "error" => ?e + error = ?e, + "Previously verified blob is invalid" ); None } @@ -1118,10 +1030,9 @@ impl NetworkBeaconProcessor { if !publishable.is_empty() { debug!( - log, - "Publishing blob batch"; - "publish_count" => publishable.len(), - "block_root" => ?block_root, + publish_count = publishable.len(), + ?block_root, + "Publishing blob batch" ); publish_count += publishable.len(); publish_fn(publishable); @@ -1132,12 +1043,11 @@ impl NetworkBeaconProcessor { } debug!( - log, - "Batch blob publication complete"; - "batch_interval" => blob_publication_batch_interval.as_millis(), - "blob_count" => blob_count, - "published_count" => publish_count, - "block_root" => ?block_root, + batch_interval = blob_publication_batch_interval.as_millis(), + blob_count, + publish_count, + ?block_root, + "Batch blob publication complete" ) }, "gradual_blob_publication", @@ -1148,7 +1058,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, @@ -1160,7 +1070,6 @@ impl NetworkBeaconProcessor { self.executor.spawn( async move { let chain = self_clone.chain.clone(); - let log = self_clone.chain.logger(); let publish_fn = |columns: DataColumnSidecarList| { self_clone.send_network_message(NetworkMessage::Publish { messages: columns @@ -1174,9 +1083,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; @@ -1193,9 +1102,8 @@ impl NetworkBeaconProcessor { Err(GossipDataColumnError::PriorKnown { .. }) => None, Err(e) => { warn!( - log, - "Previously verified data column is invalid"; - "error" => ?e + error = ?e, + "Previously verified data column is invalid" ); None } @@ -1204,10 +1112,9 @@ impl NetworkBeaconProcessor { if !publishable.is_empty() { debug!( - log, - "Publishing data column batch"; - "publish_count" => publishable.len(), - "block_root" => ?block_root, + publish_count = publishable.len(), + ?block_root, + "Publishing data column batch" ); publish_count += publishable.len(); publish_fn(publishable); @@ -1217,13 +1124,12 @@ impl NetworkBeaconProcessor { } debug!( - log, - "Batch data column publishing complete"; - "batch_size" => batch_size, - "batch_interval" => blob_publication_batch_interval.as_millis(), - "data_columns_to_publish_count" => data_columns_to_publish.len(), - "published_count" => publish_count, - "block_root" => ?block_root, + batch_size, + batch_interval = blob_publication_batch_interval.as_millis(), + data_columns_to_publish_count = data_columns_to_publish.len(), + publish_count, + ?block_root, + "Batch data column publishing complete" ) }, "gradual_data_column_publication", @@ -1231,9 +1137,20 @@ impl NetworkBeaconProcessor { } } -type TestBeaconChainType = +#[cfg(test)] +use { + beacon_chain::{builder::Witness, eth1_chain::CachingEth1Backend}, + beacon_processor::BeaconProcessorChannels, + slot_clock::ManualSlotClock, + store::MemoryStore, + tokio::sync::mpsc::UnboundedSender, +}; + +#[cfg(test)] +pub(crate) type TestBeaconChainType = Witness, E, MemoryStore, MemoryStore>; +#[cfg(test)] impl NetworkBeaconProcessor> { // Instantiates a mostly non-functional version of `Self` and returns the // event receiver that would normally go to the beacon processor. This is @@ -1244,7 +1161,6 @@ impl NetworkBeaconProcessor> { sync_tx: UnboundedSender>, chain: Arc>>, executor: TaskExecutor, - log: Logger, ) -> (Self, mpsc::Receiver>) { let BeaconProcessorChannels { beacon_processor_tx, @@ -1265,7 +1181,6 @@ impl NetworkBeaconProcessor> { network_globals, invalid_block_storage: InvalidBlockStorage::Disabled, executor, - log, }; (network_beacon_processor, beacon_processor_rx) 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 dec28eeb729..bc97f884922 100644 --- a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs @@ -5,18 +5,17 @@ use crate::status::ToStatusMessage; use crate::sync::SyncMessage; use beacon_chain::{BeaconChainError, BeaconChainTypes, WhenSlotSkipped}; use itertools::{process_results, Itertools}; -use lighthouse_network::discovery::ConnectionId; use lighthouse_network::rpc::methods::{ BlobsByRangeRequest, BlobsByRootRequest, DataColumnsByRangeRequest, DataColumnsByRootRequest, }; use lighthouse_network::rpc::*; -use lighthouse_network::{PeerId, PeerRequestId, ReportSource, Response, SyncInfo}; +use lighthouse_network::{PeerId, ReportSource, Response, SyncInfo}; use methods::LightClientUpdatesByRangeRequest; -use slog::{debug, error, warn}; use slot_clock::SlotClock; use std::collections::{hash_map::Entry, HashMap}; use std::sync::Arc; use tokio_stream::StreamExt; +use tracing::{debug, error, warn}; use types::blob_sidecar::BlobIdentifier; use types::{Epoch, EthSpec, Hash256, Slot}; @@ -35,15 +34,12 @@ impl NetworkBeaconProcessor { pub fn send_response( &self, peer_id: PeerId, + inbound_request_id: InboundRequestId, response: Response, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, ) { self.send_network_message(NetworkMessage::SendResponse { peer_id, - request_id, - id: (connection_id, substream_id), + inbound_request_id, response, }) } @@ -53,15 +49,13 @@ impl NetworkBeaconProcessor { peer_id: PeerId, error: RpcErrorResponse, reason: String, - id: PeerRequestId, - request_id: RequestId, + inbound_request_id: InboundRequestId, ) { self.send_network_message(NetworkMessage::SendErrorResponse { peer_id, error, reason, - id, - request_id, + inbound_request_id, }) } @@ -138,7 +132,7 @@ impl NetworkBeaconProcessor { pub fn process_status(&self, peer_id: PeerId, status: StatusMessage) { match self.check_peer_relevance(&status) { Ok(Some(irrelevant_reason)) => { - debug!(self.log, "Handshake Failure"; "peer" => %peer_id, "reason" => irrelevant_reason); + debug!(%peer_id, reason = irrelevant_reason, "Handshake Failure"); self.goodbye_peer(peer_id, GoodbyeReason::IrrelevantNetwork); } Ok(None) => { @@ -150,9 +144,10 @@ impl NetworkBeaconProcessor { }; self.send_sync_message(SyncMessage::AddPeer(peer_id, info)); } - Err(e) => error!(self.log, "Could not process status message"; - "peer" => %peer_id, - "error" => ?e + Err(e) => error!( + %peer_id, + error = ?e, + "Could not process status message" ), } } @@ -161,24 +156,14 @@ impl NetworkBeaconProcessor { pub async fn handle_blocks_by_root_request( self: Arc, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, request: BlocksByRootRequest, ) { self.terminate_response_stream( peer_id, - connection_id, - substream_id, - request_id, + inbound_request_id, self.clone() - .handle_blocks_by_root_request_inner( - peer_id, - connection_id, - substream_id, - request_id, - request, - ) + .handle_blocks_by_root_request_inner(peer_id, inbound_request_id, request) .await, Response::BlocksByRoot, ); @@ -188,18 +173,15 @@ impl NetworkBeaconProcessor { pub async fn handle_blocks_by_root_request_inner( self: Arc, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, request: BlocksByRootRequest, ) -> Result<(), (RpcErrorResponse, &'static str)> { let log_results = |peer_id, requested_blocks, send_block_count| { debug!( - self.log, - "BlocksByRoot outgoing response processed"; - "peer" => %peer_id, - "requested" => requested_blocks, - "returned" => %send_block_count + %peer_id, + requested = requested_blocks, + returned = %send_block_count, + "BlocksByRoot outgoing response processed" ); }; @@ -210,7 +192,7 @@ impl NetworkBeaconProcessor { { Ok(block_stream) => block_stream, Err(e) => { - error!(self.log, "Error getting block stream"; "error" => ?e); + error!( error = ?e, "Error getting block stream"); return Err((RpcErrorResponse::ServerError, "Error getting block stream")); } }; @@ -221,27 +203,23 @@ impl NetworkBeaconProcessor { Ok(Some(block)) => { self.send_response( peer_id, + inbound_request_id, Response::BlocksByRoot(Some(block.clone())), - connection_id, - substream_id, - request_id, ); send_block_count += 1; } Ok(None) => { debug!( - self.log, - "Peer requested unknown block"; - "peer" => %peer_id, - "request_root" => ?root + %peer_id, + request_root = ?root, + "Peer requested unknown block" ); } Err(BeaconChainError::BlockHashMissingFromExecutionLayer(_)) => { debug!( - self.log, - "Failed to fetch execution payload for blocks by root request"; - "block_root" => ?root, - "reason" => "execution layer not synced", + block_root = ?root, + reason = "execution layer not synced", + "Failed to fetch execution payload for blocks by root request" ); log_results(peer_id, requested_blocks, send_block_count); return Err(( @@ -251,11 +229,10 @@ impl NetworkBeaconProcessor { } Err(e) => { debug!( - self.log, - "Error fetching block for peer"; - "peer" => %peer_id, - "request_root" => ?root, - "error" => ?e, + ?peer_id, + request_root = ?root, + error = ?e, + "Error fetching block for peer" ); } } @@ -269,23 +246,13 @@ impl NetworkBeaconProcessor { pub fn handle_blobs_by_root_request( self: Arc, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, request: BlobsByRootRequest, ) { self.terminate_response_stream( peer_id, - connection_id, - substream_id, - request_id, - self.handle_blobs_by_root_request_inner( - peer_id, - connection_id, - substream_id, - request_id, - request, - ), + inbound_request_id, + self.handle_blobs_by_root_request_inner(peer_id, inbound_request_id, request), Response::BlobsByRoot, ); } @@ -294,9 +261,7 @@ impl NetworkBeaconProcessor { pub fn handle_blobs_by_root_request_inner( &self, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, request: BlobsByRootRequest, ) -> Result<(), (RpcErrorResponse, &'static str)> { let Some(requested_root) = request.blob_ids.as_slice().first().map(|id| id.block_root) @@ -318,10 +283,8 @@ impl NetworkBeaconProcessor { if let Ok(Some(blob)) = self.chain.data_availability_checker.get_blob(id) { self.send_response( peer_id, + inbound_request_id, Response::BlobsByRoot(Some(blob)), - connection_id, - substream_id, - request_id, ); send_blob_count += 1; } else { @@ -343,10 +306,8 @@ impl NetworkBeaconProcessor { if blob_sidecar.index == *index { self.send_response( peer_id, + inbound_request_id, Response::BlobsByRoot(Some(blob_sidecar.clone())), - connection_id, - substream_id, - request_id, ); send_blob_count += 1; break 'inner; @@ -355,23 +316,21 @@ impl NetworkBeaconProcessor { } Err(e) => { debug!( - self.log, - "Error fetching blob for peer"; - "peer" => %peer_id, - "request_root" => ?root, - "error" => ?e, + ?peer_id, + request_root = ?root, + error = ?e, + "Error fetching blob for peer" ); } } } } debug!( - self.log, - "BlobsByRoot outgoing response processed"; - "peer" => %peer_id, - "request_root" => %requested_root, - "request_indices" => ?requested_indices, - "returned" => send_blob_count + %peer_id, + %requested_root, + ?requested_indices, + returned = send_blob_count, + "BlobsByRoot outgoing response processed" ); Ok(()) @@ -381,23 +340,13 @@ impl NetworkBeaconProcessor { pub fn handle_data_columns_by_root_request( self: Arc, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, request: DataColumnsByRootRequest, ) { self.terminate_response_stream( peer_id, - connection_id, - substream_id, - request_id, - self.handle_data_columns_by_root_request_inner( - peer_id, - connection_id, - substream_id, - request_id, - request, - ), + inbound_request_id, + self.handle_data_columns_by_root_request_inner(peer_id, inbound_request_id, request), Response::DataColumnsByRoot, ); } @@ -406,9 +355,7 @@ impl NetworkBeaconProcessor { pub fn handle_data_columns_by_root_request_inner( &self, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, request: DataColumnsByRootRequest, ) -> Result<(), (RpcErrorResponse, &'static str)> { let mut send_data_column_count = 0; @@ -422,19 +369,18 @@ impl NetworkBeaconProcessor { send_data_column_count += 1; self.send_response( peer_id, + inbound_request_id, Response::DataColumnsByRoot(Some(data_column)), - connection_id, - substream_id, - request_id, ); } Ok(None) => {} // no-op Err(e) => { // TODO(das): lower log level when feature is stabilized - error!(self.log, "Error getting data column"; - "block_root" => ?data_column_id.block_root, - "peer" => %peer_id, - "error" => ?e + error!( + block_root = ?data_column_id.block_root, + %peer_id, + error = ?e, + "Error getting data column" ); return Err((RpcErrorResponse::ServerError, "Error getting data column")); } @@ -442,11 +388,10 @@ impl NetworkBeaconProcessor { } debug!( - self.log, - "Received DataColumnsByRoot Request"; - "peer" => %peer_id, - "request" => ?request.group_by_ordered_block_root(), - "returned" => send_data_column_count + %peer_id, + request = ?request.group_by_ordered_block_root(), + returned = send_data_column_count, + "Received DataColumnsByRoot Request" ); Ok(()) @@ -455,22 +400,16 @@ impl NetworkBeaconProcessor { pub fn handle_light_client_updates_by_range( self: &Arc, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, request: LightClientUpdatesByRangeRequest, ) { self.terminate_response_stream( peer_id, - connection_id, - substream_id, - request_id, + inbound_request_id, self.clone() .handle_light_client_updates_by_range_request_inner( peer_id, - connection_id, - substream_id, - request_id, + inbound_request_id, request, ), Response::LightClientUpdatesByRange, @@ -481,15 +420,14 @@ impl NetworkBeaconProcessor { pub fn handle_light_client_updates_by_range_request_inner( self: Arc, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, req: LightClientUpdatesByRangeRequest, ) -> Result<(), (RpcErrorResponse, &'static str)> { - debug!(self.log, "Received LightClientUpdatesByRange Request"; - "peer_id" => %peer_id, - "count" => req.count, - "start_period" => req.start_period, + debug!( + %peer_id, + count = req.count, + start_period = req.start_period, + "Received LightClientUpdatesByRange Request" ); // Should not send more than max light client updates @@ -507,10 +445,11 @@ impl NetworkBeaconProcessor { { Ok(lc_updates) => lc_updates, Err(e) => { - error!(self.log, "Unable to obtain light client updates"; - "request" => ?req, - "peer" => %peer_id, - "error" => ?e + error!( + request = ?req, + peer = %peer_id, + error = ?e, + "Unable to obtain light client updates" ); return Err((RpcErrorResponse::ServerError, "Database error")); } @@ -520,8 +459,7 @@ impl NetworkBeaconProcessor { self.send_network_message(NetworkMessage::SendResponse { peer_id, response: Response::LightClientUpdatesByRange(Some(Arc::new(lc_update.clone()))), - request_id, - id: (connection_id, substream_id), + inbound_request_id, }); } @@ -529,22 +467,20 @@ impl NetworkBeaconProcessor { if lc_updates_sent < req.count as usize { debug!( - self.log, - "LightClientUpdatesByRange outgoing response processed"; - "peer" => %peer_id, - "info" => "Failed to return all requested light client updates. The peer may have requested data ahead of whats currently available", - "start_period" => req.start_period, - "requested" => req.count, - "returned" => lc_updates_sent + peer = %peer_id, + info = "Failed to return all requested light client updates. The peer may have requested data ahead of whats currently available", + start_period = req.start_period, + requested = req.count, + returned = lc_updates_sent, + "LightClientUpdatesByRange outgoing response processed" ); } else { debug!( - self.log, - "LightClientUpdatesByRange outgoing response processed"; - "peer" => %peer_id, - "start_period" => req.start_period, - "requested" => req.count, - "returned" => lc_updates_sent + peer = %peer_id, + start_period = req.start_period, + requested = req.count, + returned = lc_updates_sent, + "LightClientUpdatesByRange outgoing response processed" ); } @@ -555,16 +491,12 @@ impl NetworkBeaconProcessor { pub fn handle_light_client_bootstrap( self: &Arc, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, request: LightClientBootstrapRequest, ) { self.terminate_response_single_item( peer_id, - connection_id, - substream_id, - request_id, + inbound_request_id, match self.chain.get_light_client_bootstrap(&request.root) { Ok(Some((bootstrap, _))) => Ok(Arc::new(bootstrap)), Ok(None) => Err(( @@ -572,10 +504,11 @@ impl NetworkBeaconProcessor { "Bootstrap not available".to_string(), )), Err(e) => { - error!(self.log, "Error getting LightClientBootstrap instance"; - "block_root" => ?request.root, - "peer" => %peer_id, - "error" => ?e + error!( + block_root = ?request.root, + %peer_id, + error = ?e, + "Error getting LightClientBootstrap instance" ); Err((RpcErrorResponse::ResourceUnavailable, format!("{:?}", e))) } @@ -588,15 +521,11 @@ impl NetworkBeaconProcessor { pub fn handle_light_client_optimistic_update( self: &Arc, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, ) { self.terminate_response_single_item( peer_id, - connection_id, - substream_id, - request_id, + inbound_request_id, match self .chain .light_client_server_cache @@ -616,15 +545,11 @@ impl NetworkBeaconProcessor { pub fn handle_light_client_finality_update( self: &Arc, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, ) { self.terminate_response_single_item( peer_id, - connection_id, - substream_id, - request_id, + inbound_request_id, match self .chain .light_client_server_cache @@ -644,24 +569,14 @@ impl NetworkBeaconProcessor { pub async fn handle_blocks_by_range_request( self: Arc, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, req: BlocksByRangeRequest, ) { self.terminate_response_stream( peer_id, - connection_id, - substream_id, - request_id, + inbound_request_id, self.clone() - .handle_blocks_by_range_request_inner( - peer_id, - connection_id, - substream_id, - request_id, - req, - ) + .handle_blocks_by_range_request_inner(peer_id, inbound_request_id, req) .await, Response::BlocksByRange, ); @@ -671,18 +586,17 @@ impl NetworkBeaconProcessor { pub async fn handle_blocks_by_range_request_inner( self: Arc, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, req: BlocksByRangeRequest, ) -> Result<(), (RpcErrorResponse, &'static str)> { let req_start_slot = *req.start_slot(); let req_count = *req.count(); - debug!(self.log, "Received BlocksByRange Request"; - "peer_id" => %peer_id, - "start_slot" => req_start_slot, - "count" => req_count, + debug!( + %peer_id, + count = req_count, + start_slot = %req_start_slot, + "Received BlocksByRange Request" ); // Spawn a blocking handle since get_block_roots_for_slot_range takes a sync lock on the @@ -712,24 +626,22 @@ impl NetworkBeaconProcessor { let log_results = |peer_id, blocks_sent| { if blocks_sent < (req_count as usize) { debug!( - self.log, - "BlocksByRange outgoing response processed"; - "peer" => %peer_id, - "msg" => "Failed to return all requested blocks", - "start_slot" => req_start_slot, - "current_slot" => current_slot, - "requested" => req_count, - "returned" => blocks_sent + %peer_id, + msg = "Failed to return all requested blocks", + start_slot = %req_start_slot, + %current_slot, + requested = req_count, + returned = blocks_sent, + "BlocksByRange outgoing response processed" ); } else { debug!( - self.log, - "BlocksByRange outgoing response processed"; - "peer" => %peer_id, - "start_slot" => req_start_slot, - "current_slot" => current_slot, - "requested" => req_count, - "returned" => blocks_sent + %peer_id, + start_slot = %req_start_slot, + %current_slot, + requested = req_count, + returned = blocks_sent, + "BlocksByRange outgoing response processed" ); } }; @@ -737,7 +649,7 @@ impl NetworkBeaconProcessor { let mut block_stream = match self.chain.get_blocks(block_roots) { Ok(block_stream) => block_stream, Err(e) => { - error!(self.log, "Error getting block stream"; "error" => ?e); + error!(error = ?e, "Error getting block stream"); return Err((RpcErrorResponse::ServerError, "Iterator error")); } }; @@ -754,29 +666,26 @@ impl NetworkBeaconProcessor { blocks_sent += 1; self.send_network_message(NetworkMessage::SendResponse { peer_id, - request_id, + inbound_request_id, response: Response::BlocksByRange(Some(block.clone())), - id: (connection_id, substream_id), }); } } Ok(None) => { error!( - self.log, - "Block in the chain is not in the store"; - "request" => ?req, - "peer" => %peer_id, - "request_root" => ?root + request = ?req, + %peer_id, + request_root = ?root, + "Block in the chain is not in the store" ); log_results(peer_id, blocks_sent); return Err((RpcErrorResponse::ServerError, "Database inconsistency")); } Err(BeaconChainError::BlockHashMissingFromExecutionLayer(_)) => { debug!( - self.log, - "Failed to fetch execution payload for blocks by range request"; - "block_root" => ?root, - "reason" => "execution layer not synced", + block_root = ?root, + reason = "execution layer not synced", + "Failed to fetch execution payload for blocks by range request" ); log_results(peer_id, blocks_sent); // send the stream terminator @@ -792,18 +701,16 @@ impl NetworkBeaconProcessor { if matches!(**boxed_error, execution_layer::Error::EngineError(_)) ) { warn!( - self.log, - "Error rebuilding payload for peer"; - "info" => "this may occur occasionally when the EE is busy", - "block_root" => ?root, - "error" => ?e, + info = "this may occur occasionally when the EE is busy", + block_root = ?root, + error = ?e, + "Error rebuilding payload for peer" ); } else { error!( - self.log, - "Error fetching block for peer"; - "block_root" => ?root, - "error" => ?e + block_root = ?root, + error = ?e, + "Error fetching block for peer" ); } log_results(peer_id, blocks_sent); @@ -873,15 +780,14 @@ impl NetworkBeaconProcessor { ); debug!( - self.log, - "Range request block roots retrieved"; - "req_type" => req_type, - "start_slot" => req_start_slot, - "req_count" => req_count, - "roots_count" => block_roots.len(), - "source" => source, - "elapsed" => ?elapsed, - "finalized_slot" => finalized_slot + 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) @@ -900,17 +806,19 @@ impl NetworkBeaconProcessor { slot, oldest_block_slot, }) => { - debug!(self.log, "Range request failed during backfill"; - "requested_slot" => slot, - "oldest_known_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!(self.log, "Unable to obtain root iter for range request"; - "start_slot" => start_slot, - "count" => count, - "error" => ?e + error!( + %start_slot, + count, + error = ?e, + "Unable to obtain root iter for range request" ); return Err((RpcErrorResponse::ServerError, "Database error")); } @@ -925,10 +833,11 @@ impl NetworkBeaconProcessor { let block_roots = match maybe_block_roots { Ok(block_roots) => block_roots, Err(e) => { - error!(self.log, "Error during iteration over blocks for range request"; - "start_slot" => start_slot, - "count" => count, - "error" => ?e + error!( + %start_slot, + count, + error = ?e, + "Error during iteration over blocks for range request" ); return Err((RpcErrorResponse::ServerError, "Iteration error")); } @@ -946,23 +855,13 @@ impl NetworkBeaconProcessor { pub fn handle_blobs_by_range_request( self: Arc, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, req: BlobsByRangeRequest, ) { self.terminate_response_stream( peer_id, - connection_id, - substream_id, - request_id, - self.handle_blobs_by_range_request_inner( - peer_id, - connection_id, - substream_id, - request_id, - req, - ), + inbound_request_id, + self.handle_blobs_by_range_request_inner(peer_id, inbound_request_id, req), Response::BlobsByRange, ); } @@ -971,15 +870,14 @@ impl NetworkBeaconProcessor { fn handle_blobs_by_range_request_inner( &self, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, req: BlobsByRangeRequest, ) -> Result<(), (RpcErrorResponse, &'static str)> { - debug!(self.log, "Received BlobsByRange Request"; - "peer_id" => %peer_id, - "count" => req.count, - "start_slot" => req.start_slot, + debug!( + ?peer_id, + count = req.count, + start_slot = req.start_slot, + "Received BlobsByRange Request" ); let request_start_slot = Slot::from(req.start_slot); @@ -987,7 +885,7 @@ impl NetworkBeaconProcessor { let data_availability_boundary_slot = match self.chain.data_availability_boundary() { Some(boundary) => boundary.start_slot(T::EthSpec::slots_per_epoch()), None => { - debug!(self.log, "Deneb fork is disabled"); + debug!("Deneb fork is disabled"); return Err((RpcErrorResponse::InvalidRequest, "Deneb fork is disabled")); } }; @@ -1000,11 +898,10 @@ impl NetworkBeaconProcessor { .unwrap_or(data_availability_boundary_slot); if request_start_slot < oldest_blob_slot { debug!( - self.log, - "Range request start slot is older than data availability boundary."; - "requested_slot" => request_start_slot, - "oldest_blob_slot" => oldest_blob_slot, - "data_availability_boundary" => data_availability_boundary_slot + %request_start_slot, + %oldest_blob_slot, + %data_availability_boundary_slot, + "Range request start slot is older than data availability boundary." ); return if data_availability_boundary_slot < oldest_blob_slot { @@ -1030,13 +927,12 @@ impl NetworkBeaconProcessor { let log_results = |peer_id, req: BlobsByRangeRequest, blobs_sent| { debug!( - self.log, - "BlobsByRange outgoing response processed"; - "peer" => %peer_id, - "start_slot" => req.start_slot, - "current_slot" => current_slot, - "requested" => req.count, - "returned" => blobs_sent + %peer_id, + start_slot = req.start_slot, + %current_slot, + requested = req.count, + returned = blobs_sent, + "BlobsByRange outgoing response processed" ); }; @@ -1049,20 +945,18 @@ impl NetworkBeaconProcessor { blobs_sent += 1; self.send_network_message(NetworkMessage::SendResponse { peer_id, + inbound_request_id, response: Response::BlobsByRange(Some(blob_sidecar.clone())), - request_id, - id: (connection_id, substream_id), }); } } Err(e) => { error!( - self.log, - "Error fetching blobs block root"; - "request" => ?req, - "peer" => %peer_id, - "block_root" => ?root, - "error" => ?e + request = ?req, + %peer_id, + block_root = ?root, + error = ?e, + "Error fetching blobs block root" ); log_results(peer_id, req, blobs_sent); @@ -1082,23 +976,13 @@ impl NetworkBeaconProcessor { pub fn handle_data_columns_by_range_request( &self, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, req: DataColumnsByRangeRequest, ) { self.terminate_response_stream( peer_id, - connection_id, - substream_id, - request_id, - self.handle_data_columns_by_range_request_inner( - peer_id, - connection_id, - substream_id, - request_id, - req, - ), + inbound_request_id, + self.handle_data_columns_by_range_request_inner(peer_id, inbound_request_id, req), Response::DataColumnsByRange, ); } @@ -1107,15 +991,14 @@ impl NetworkBeaconProcessor { pub fn handle_data_columns_by_range_request_inner( &self, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, req: DataColumnsByRangeRequest, ) -> Result<(), (RpcErrorResponse, &'static str)> { - debug!(self.log, "Received DataColumnsByRange Request"; - "peer_id" => %peer_id, - "count" => req.count, - "start_slot" => req.start_slot, + debug!( + %peer_id, + count = req.count, + start_slot = req.start_slot, + "Received DataColumnsByRange Request" ); // Should not send more than max request data columns @@ -1131,7 +1014,7 @@ impl NetworkBeaconProcessor { let data_availability_boundary_slot = match self.chain.data_availability_boundary() { Some(boundary) => boundary.start_slot(T::EthSpec::slots_per_epoch()), None => { - debug!(self.log, "Deneb fork is disabled"); + debug!("Deneb fork is disabled"); return Err((RpcErrorResponse::InvalidRequest, "Deneb fork is disabled")); } }; @@ -1145,11 +1028,10 @@ impl NetworkBeaconProcessor { if request_start_slot < oldest_data_column_slot { debug!( - self.log, - "Range request start slot is older than data availability boundary."; - "requested_slot" => request_start_slot, - "oldest_data_column_slot" => oldest_data_column_slot, - "data_availability_boundary" => data_availability_boundary_slot + %request_start_slot, + %oldest_data_column_slot, + %data_availability_boundary_slot, + "Range request start slot is older than data availability boundary." ); return if data_availability_boundary_slot < oldest_data_column_slot { @@ -1176,22 +1058,20 @@ impl NetworkBeaconProcessor { data_columns_sent += 1; self.send_network_message(NetworkMessage::SendResponse { peer_id, - request_id, + inbound_request_id, response: Response::DataColumnsByRange(Some( data_column_sidecar.clone(), )), - id: (connection_id, substream_id), }); } Ok(None) => {} // no-op Err(e) => { error!( - self.log, - "Error fetching data columns block root"; - "request" => ?req, - "peer" => %peer_id, - "block_root" => ?root, - "error" => ?e + request = ?req, + %peer_id, + block_root = ?root, + error = ?e, + "Error fetching data columns block root" ); return Err(( RpcErrorResponse::ServerError, @@ -1208,13 +1088,12 @@ impl NetworkBeaconProcessor { .unwrap_or_else(|_| self.chain.slot_clock.genesis_slot()); debug!( - self.log, - "DataColumnsByRange Response processed"; - "peer" => %peer_id, - "start_slot" => req.start_slot, - "current_slot" => current_slot, - "requested" => req.count, - "returned" => data_columns_sent + %peer_id, + start_slot = req.start_slot, + %current_slot, + requested = req.count, + returned = data_columns_sent, + "DataColumnsByRange Response processed" ); Ok(()) @@ -1225,32 +1104,20 @@ impl NetworkBeaconProcessor { fn terminate_response_single_item Response>( &self, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, result: Result, into_response: F, ) { match result { Ok(resp) => { - // Not necessary to explicitly send a termination message if this InboundRequest - // returns <= 1 for InboundRequest::expected_responses - // https://github.com/sigp/lighthouse/blob/3058b96f2560f1da04ada4f9d8ba8e5651794ff6/beacon_node/lighthouse_network/src/rpc/handler.rs#L555-L558 self.send_network_message(NetworkMessage::SendResponse { peer_id, - request_id, + inbound_request_id, response: into_response(resp), - id: (connection_id, substream_id), }); } Err((error_code, reason)) => { - self.send_error_response( - peer_id, - error_code, - reason, - (connection_id, substream_id), - request_id, - ); + self.send_error_response(peer_id, error_code, reason, inbound_request_id); } } } @@ -1260,27 +1127,18 @@ impl NetworkBeaconProcessor { fn terminate_response_stream) -> Response>( &self, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, result: Result<(), (RpcErrorResponse, &'static str)>, into_response: F, ) { match result { Ok(_) => self.send_network_message(NetworkMessage::SendResponse { peer_id, - request_id, + inbound_request_id, response: into_response(None), - id: (connection_id, substream_id), }), Err((error_code, reason)) => { - self.send_error_response( - peer_id, - error_code, - reason.into(), - (connection_id, substream_id), - request_id, - ); + self.send_error_response(peer_id, error_code, reason.into(), inbound_request_id); } } } diff --git a/beacon_node/network/src/network_beacon_processor/sync_methods.rs b/beacon_node/network/src/network_beacon_processor/sync_methods.rs index 6b8ed607ab1..31b17a41a42 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -18,11 +18,11 @@ use beacon_processor::{ AsyncFn, BlockingFn, DuplicateCache, }; use lighthouse_network::PeerAction; -use slog::{debug, error, info, warn}; use std::sync::Arc; use std::time::Duration; use store::KzgCommitment; use tokio::sync::mpsc; +use tracing::{debug, error, info, warn}; use types::beacon_block_body::format_kzg_commitments; use types::blob_sidecar::FixedBlobSidecarList; use types::{BlockImportSource, DataColumnSidecar, DataColumnSidecarList, Epoch, Hash256}; @@ -112,11 +112,10 @@ impl NetworkBeaconProcessor { // Check if the block is already being imported through another source let Some(handle) = duplicate_cache.check_and_insert(block_root) else { debug!( - self.log, - "Gossip block is being processed"; - "action" => "sending rpc block to reprocessing queue", - "block_root" => %block_root, - "process_type" => ?process_type, + action = "sending rpc block to reprocessing queue", + %block_root, + ?process_type, + "Gossip block is being processed" ); // Send message to work reprocess queue to retry the block @@ -133,7 +132,7 @@ impl NetworkBeaconProcessor { }); if reprocess_tx.try_send(reprocess_msg).is_err() { - error!(self.log, "Failed to inform block import"; "source" => "rpc", "block_root" => %block_root) + error!(source = "rpc", %block_root,"Failed to inform block import") }; return; }; @@ -144,13 +143,12 @@ impl NetworkBeaconProcessor { let commitments_formatted = block.as_block().commitments_formatted(); debug!( - self.log, - "Processing RPC block"; - "block_root" => ?block_root, - "proposer" => block.message().proposer_index(), - "slot" => block.slot(), - "commitments" => commitments_formatted, - "process_type" => ?process_type, + ?block_root, + proposer = block.message().proposer_index(), + slot = %block.slot(), + commitments_formatted, + ?process_type, + "Processing RPC block" ); let signed_beacon_block = block.block_cloned(); @@ -168,15 +166,22 @@ impl NetworkBeaconProcessor { // RPC block imported, regardless of process type match result.as_ref() { Ok(AvailabilityProcessingStatus::Imported(hash)) => { - info!(self.log, "New RPC block received"; "slot" => slot, "hash" => %hash); - + info!( + %slot, + %hash, + "New RPC block received", + ); // Trigger processing for work referencing this block. let reprocess_msg = ReprocessQueueMessage::BlockImported { block_root: *hash, parent_root, }; if reprocess_tx.try_send(reprocess_msg).is_err() { - error!(self.log, "Failed to inform block import"; "source" => "rpc", "block_root" => %hash) + error!( + source = "rpc", + block_root = %hash, + "Failed to inform block import" + ); }; self.chain.block_times_cache.write().set_time_observed( *hash, @@ -265,12 +270,11 @@ impl NetworkBeaconProcessor { let commitments = format_kzg_commitments(&commitments); debug!( - self.log, - "RPC blobs received"; - "indices" => ?indices, - "block_root" => %block_root, - "slot" => %slot, - "commitments" => commitments, + ?indices, + %block_root, + %slot, + commitments, + "RPC blobs received" ); if let Ok(current_slot) = self.chain.slot() { @@ -290,37 +294,33 @@ impl NetworkBeaconProcessor { match &result { Ok(AvailabilityProcessingStatus::Imported(hash)) => { debug!( - self.log, - "Block components retrieved"; - "result" => "imported block and blobs", - "slot" => %slot, - "block_hash" => %hash, + result = "imported block and blobs", + %slot, + block_hash = %hash, + "Block components retrieved" ); self.chain.recompute_head_at_current_slot().await; } Ok(AvailabilityProcessingStatus::MissingComponents(_, _)) => { debug!( - self.log, - "Missing components over rpc"; - "block_hash" => %block_root, - "slot" => %slot, + block_hash = %block_root, + %slot, + "Missing components over rpc" ); } Err(BlockError::DuplicateFullyImported(_)) => { debug!( - self.log, - "Blobs have already been imported"; - "block_hash" => %block_root, - "slot" => %slot, + block_hash = %block_root, + %slot, + "Blobs have already been imported" ); } Err(e) => { warn!( - self.log, - "Error when importing rpc blobs"; - "error" => ?e, - "block_hash" => %block_root, - "slot" => %slot, + error = ?e, + block_hash = %block_root, + %slot, + "Error when importing rpc blobs" ); } } @@ -336,9 +336,30 @@ impl NetworkBeaconProcessor { self: Arc>, block_root: Hash256, custody_columns: DataColumnSidecarList, - _seen_timestamp: Duration, + seen_timestamp: Duration, process_type: BlockProcessType, ) { + // custody_columns must always have at least one element + let Some(slot) = custody_columns.first().map(|d| d.slot()) else { + return; + }; + + if let Ok(current_slot) = self.chain.slot() { + if current_slot == slot { + let delay = get_slot_delay_ms(seen_timestamp, slot, &self.chain.slot_clock); + metrics::observe_duration(&metrics::BEACON_BLOB_RPC_SLOT_START_DELAY_TIME, delay); + } + } + + let mut indices = custody_columns.iter().map(|d| d.index).collect::>(); + indices.sort_unstable(); + debug!( + ?indices, + %block_root, + %slot, + "RPC custody data columns received" + ); + let mut result = self .chain .process_rpc_custody_columns(custody_columns) @@ -349,23 +370,25 @@ impl NetworkBeaconProcessor { Ok(availability) => match availability { AvailabilityProcessingStatus::Imported(hash) => { debug!( - self.log, - "Block components retrieved"; - "result" => "imported block and custody columns", - "block_hash" => %hash, + result = "imported block and custody columns", + block_hash = %hash, + "Block components retrieved" ); self.chain.recompute_head_at_current_slot().await; } AvailabilityProcessingStatus::MissingComponents(_, _) => { debug!( - self.log, - "Missing components over rpc"; - "block_hash" => %block_root, + block_hash = %block_root, + "Missing components over rpc" ); // Attempt reconstruction here before notifying sync, to avoid sending out more requests // that we may no longer need. - 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) } @@ -373,17 +396,15 @@ impl NetworkBeaconProcessor { }, Err(BlockError::DuplicateFullyImported(_)) => { debug!( - self.log, - "Custody columns have already been imported"; - "block_hash" => %block_root, + block_hash = %block_root, + "Custody columns have already been imported" ); } Err(e) => { warn!( - self.log, - "Error when importing rpc custody columns"; - "error" => ?e, - "block_hash" => %block_root, + error = ?e, + block_hash = %block_root, + "Error when importing rpc custody columns" ); } } @@ -433,27 +454,29 @@ impl NetworkBeaconProcessor { .await { (imported_blocks, Ok(_)) => { - debug!(self.log, "Batch processed"; - "batch_epoch" => epoch, - "first_block_slot" => start_slot, - "chain" => chain_id, - "last_block_slot" => end_slot, - "processed_blocks" => sent_blocks, - "service"=> "sync"); + debug!( + batch_epoch = %epoch, + first_block_slot = start_slot, + chain = chain_id, + last_block_slot = end_slot, + processed_blocks = sent_blocks, + service= "sync", + "Batch processed"); BatchProcessResult::Success { sent_blocks, imported_blocks, } } (imported_blocks, Err(e)) => { - debug!(self.log, "Batch processing failed"; - "batch_epoch" => epoch, - "first_block_slot" => start_slot, - "chain" => chain_id, - "last_block_slot" => end_slot, - "imported_blocks" => imported_blocks, - "error" => %e.message, - "service" => "sync"); + debug!( + batch_epoch = %epoch, + first_block_slot = start_slot, + chain = chain_id, + last_block_slot = end_slot, + imported_blocks, + error = %e.message, + service = "sync", + "Batch processing failed"); match e.peer_action { Some(penalty) => BatchProcessResult::FaultyFailure { imported_blocks, @@ -480,28 +503,31 @@ impl NetworkBeaconProcessor { match self.process_backfill_blocks(downloaded_blocks) { (imported_blocks, Ok(_)) => { - debug!(self.log, "Backfill batch processed"; - "batch_epoch" => epoch, - "first_block_slot" => start_slot, - "keep_execution_payload" => !self.chain.store.get_config().prune_payloads, - "last_block_slot" => end_slot, - "processed_blocks" => sent_blocks, - "processed_blobs" => n_blobs, - "processed_data_columns" => n_data_columns, - "service"=> "sync"); + debug!( + batch_epoch = %epoch, + first_block_slot = start_slot, + keep_execution_payload = !self.chain.store.get_config().prune_payloads, + last_block_slot = end_slot, + processed_blocks = sent_blocks, + processed_blobs = n_blobs, + processed_data_columns = n_data_columns, + service= "sync", + "Backfill batch processed"); BatchProcessResult::Success { sent_blocks, imported_blocks, } } (_, Err(e)) => { - debug!(self.log, "Backfill batch processing failed"; - "batch_epoch" => epoch, - "first_block_slot" => start_slot, - "last_block_slot" => end_slot, - "processed_blobs" => n_blobs, - "error" => %e.message, - "service" => "sync"); + debug!( + batch_epoch = %epoch, + first_block_slot = start_slot, + last_block_slot = end_slot, + processed_blobs = n_blobs, + error = %e.message, + service = "sync", + "Backfill batch processing failed" + ); match e.peer_action { Some(penalty) => BatchProcessResult::FaultyFailure { imported_blocks: 0, @@ -630,11 +656,10 @@ impl NetworkBeaconProcessor { expected_block_root, } => { debug!( - self.log, - "Backfill batch processing error"; - "error" => "mismatched_block_root", - "block_root" => ?block_root, - "expected_root" => ?expected_block_root + error = "mismatched_block_root", + ?block_root, + expected_root = ?expected_block_root, + "Backfill batch processing error" ); // The peer is faulty if they send blocks with bad roots. Some(PeerAction::LowToleranceError) @@ -642,33 +667,30 @@ impl NetworkBeaconProcessor { HistoricalBlockError::InvalidSignature | HistoricalBlockError::SignatureSet(_) => { warn!( - self.log, - "Backfill batch processing error"; - "error" => ?e + error = ?e, + "Backfill batch processing error" ); // The peer is faulty if they bad signatures. Some(PeerAction::LowToleranceError) } HistoricalBlockError::ValidatorPubkeyCacheTimeout => { warn!( - self.log, - "Backfill batch processing error"; - "error" => "pubkey_cache_timeout" + error = "pubkey_cache_timeout", + "Backfill batch processing error" ); // This is an internal error, do not penalize the peer. None } HistoricalBlockError::IndexOutOfBounds => { error!( - self.log, - "Backfill batch OOB error"; - "error" => ?e, + error = ?e, + "Backfill batch OOB error" ); // This should never occur, don't penalize the peer. None } HistoricalBlockError::StoreError(e) => { - warn!(self.log, "Backfill batch processing error"; "error" => ?e); + warn!(error = ?e, "Backfill batch processing error"); // This is an internal error, don't penalize the peer. None } // @@ -711,19 +733,19 @@ impl NetworkBeaconProcessor { if present_slot + FUTURE_SLOT_TOLERANCE >= block_slot { // The block is too far in the future, drop it. warn!( - self.log, "Block is ahead of our slot clock"; - "msg" => "block for future slot rejected, check your time", - "present_slot" => present_slot, - "block_slot" => block_slot, - "FUTURE_SLOT_TOLERANCE" => FUTURE_SLOT_TOLERANCE, + msg = "block for future slot rejected, check your time", + %present_slot, + %block_slot, + FUTURE_SLOT_TOLERANCE, + "Block is ahead of our slot clock" ); } else { // The block is in the future, but not too far. debug!( - self.log, "Block is slightly ahead of our slot clock. Ignoring."; - "present_slot" => present_slot, - "block_slot" => block_slot, - "FUTURE_SLOT_TOLERANCE" => FUTURE_SLOT_TOLERANCE, + %present_slot, + %block_slot, + FUTURE_SLOT_TOLERANCE, + "Block is slightly ahead of our slot clock. Ignoring." ); } @@ -737,12 +759,11 @@ impl NetworkBeaconProcessor { }) } BlockError::WouldRevertFinalizedSlot { .. } => { - debug!(self.log, "Finalized or earlier block processed";); + debug!("Finalized or earlier block processed"); Ok(()) } BlockError::NotFinalizedDescendant { block_parent_root } => { debug!( - self.log, "Not syncing to a chain that conflicts with the canonical or manual finalized checkpoint" ); Err(ChainSegmentFailed { @@ -754,14 +775,14 @@ impl NetworkBeaconProcessor { }) } BlockError::GenesisBlock => { - debug!(self.log, "Genesis block was processed"); + debug!("Genesis block was processed"); Ok(()) } BlockError::BeaconChainError(e) => { warn!( - self.log, "BlockProcessingFailure"; - "msg" => "unexpected condition in processing block.", - "outcome" => ?e, + msg = "unexpected condition in processing block.", + outcome = ?e, + "BlockProcessingFailure" ); Err(ChainSegmentFailed { @@ -774,10 +795,10 @@ impl NetworkBeaconProcessor { if !epe.penalize_peer() { // These errors indicate an issue with the EL and not the `ChainSegment`. // Pause the syncing while the EL recovers - debug!(self.log, - "Execution layer verification failed"; - "outcome" => "pausing sync", - "err" => ?err + debug!( + outcome = "pausing sync", + ?err, + "Execution layer verification failed" ); Err(ChainSegmentFailed { message: format!("Execution layer offline. Reason: {:?}", err), @@ -785,9 +806,9 @@ impl NetworkBeaconProcessor { peer_action: None, }) } else { - debug!(self.log, - "Invalid execution payload"; - "error" => ?err + debug!( + error = ?err, + "Invalid execution payload" ); Err(ChainSegmentFailed { message: format!( @@ -800,10 +821,9 @@ impl NetworkBeaconProcessor { } ref err @ BlockError::ParentExecutionPayloadInvalid { ref parent_root } => { warn!( - self.log, - "Failed to sync chain built on invalid parent"; - "parent_root" => ?parent_root, - "advice" => "check execution node for corruption then restart it and Lighthouse", + ?parent_root, + advice = "check execution node for corruption then restart it and Lighthouse", + "Failed to sync chain built on invalid parent" ); Err(ChainSegmentFailed { message: format!("Peer sent invalid block. Reason: {err:?}"), @@ -815,11 +835,7 @@ impl NetworkBeaconProcessor { } // Penalise peers for sending us banned blocks. BlockError::KnownInvalidExecutionPayload(block_root) => { - warn!( - self.log, - "Received block known to be invalid"; - "block_root" => ?block_root, - ); + warn!(?block_root, "Received block known to be invalid",); Err(ChainSegmentFailed { message: format!("Banned block: {block_root:?}"), peer_action: Some(PeerAction::Fatal), @@ -827,9 +843,9 @@ impl NetworkBeaconProcessor { } other => { debug!( - self.log, "Invalid block received"; - "msg" => "peer sent invalid block", - "outcome" => %other, + msg = "peer sent invalid block", + outcome = %other, + "Invalid block received" ); Err(ChainSegmentFailed { diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index 8415ece6383..292e894870f 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -9,14 +9,16 @@ 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 lighthouse_network::discovery::ConnectionId; +use itertools::Itertools; use lighthouse_network::rpc::methods::{BlobsByRangeRequest, MetaDataV3}; -use lighthouse_network::rpc::{RequestId, SubstreamId}; +use lighthouse_network::rpc::InboundRequestId; use lighthouse_network::{ discv5::enr::{self, CombinedKey}, rpc::methods::{MetaData, MetaDataV2}, @@ -30,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; @@ -53,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>, @@ -182,8 +185,6 @@ impl TestRig { let (network_tx, _network_rx) = mpsc::unbounded_channel(); - let log = harness.logger().clone(); - let beacon_processor_config = BeaconProcessorConfig { enable_backfill_rate_limiting, ..Default::default() @@ -221,7 +222,6 @@ impl TestRig { meta_data, vec![], false, - &log, network_config, spec, )); @@ -241,16 +241,14 @@ impl TestRig { network_globals: network_globals.clone(), invalid_block_storage: InvalidBlockStorage::Disabled, executor: executor.clone(), - log: log.clone(), }; let network_beacon_processor = Arc::new(network_beacon_processor); let beacon_processor = BeaconProcessor { - network_globals, + network_globals: network_globals.clone(), executor, current_workers: 0, config: beacon_processor_config, - log: log.clone(), } .spawn_manager( beacon_processor_rx, @@ -268,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, @@ -329,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 }, ) @@ -346,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 }, ) @@ -367,13 +416,24 @@ 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( PeerId::random(), - ConnectionId::new_unchecked(42), - SubstreamId::new(24), - RequestId::new_unchecked(0), + InboundRequestId::new_unchecked(42, 24), BlobsByRangeRequest { start_slot: 0, count, @@ -626,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. @@ -702,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(), @@ -767,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 => { @@ -781,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(); @@ -789,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); + } } }; @@ -848,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 => { @@ -862,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(); @@ -870,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); + } } }; @@ -1054,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()); @@ -1154,8 +1240,7 @@ async fn test_blobs_by_range() { if let NetworkMessage::SendResponse { peer_id: _, response: Response::BlobsByRange(blob), - id: _, - request_id: _, + inbound_request_id: _, } = next { if blob.is_some() { diff --git a/beacon_node/network/src/persisted_dht.rs b/beacon_node/network/src/persisted_dht.rs index 1e1420883e8..9c112dba860 100644 --- a/beacon_node/network/src/persisted_dht.rs +++ b/beacon_node/network/src/persisted_dht.rs @@ -69,20 +69,17 @@ impl StoreItem for PersistedDht { #[cfg(test)] mod tests { use super::*; - use sloggers::{null::NullLoggerBuilder, Build}; use std::str::FromStr; use store::config::StoreConfig; use store::MemoryStore; use types::{ChainSpec, MinimalEthSpec}; #[test] fn test_persisted_dht() { - let log = NullLoggerBuilder.build().unwrap(); let store: HotColdDB< MinimalEthSpec, MemoryStore, MemoryStore, - > = HotColdDB::open_ephemeral(StoreConfig::default(), ChainSpec::minimal().into(), log) - .unwrap(); + > = HotColdDB::open_ephemeral(StoreConfig::default(), ChainSpec::minimal().into()).unwrap(); let enrs = vec![Enr::from_str("enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8").unwrap()]; store .put_item(&DHT_DB_KEY, &PersistedDht { enrs: enrs.clone() }) diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index 36e5c391e91..2a7bc597c26 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -14,20 +14,18 @@ use beacon_processor::{ work_reprocessing_queue::ReprocessQueueMessage, BeaconProcessorSend, DuplicateCache, }; use futures::prelude::*; -use lighthouse_network::discovery::ConnectionId; use lighthouse_network::rpc::*; use lighthouse_network::{ - rpc, service::api_types::{AppRequestId, SyncRequestId}, - MessageId, NetworkGlobals, PeerId, PeerRequestId, PubsubMessage, Response, + MessageId, NetworkGlobals, PeerId, PubsubMessage, Response, }; +use logging::crit; use logging::TimeLatch; -use slog::{crit, debug, o, trace}; -use slog::{error, warn}; use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; +use tracing::{debug, error, info_span, trace, warn, Instrument}; use types::{BlobSidecar, DataColumnSidecar, EthSpec, ForkContext, SignedBeaconBlock}; /// Handles messages from the network and routes them to the appropriate service to be handled. @@ -42,8 +40,6 @@ pub struct Router { network: HandlerNetworkContext, /// A multi-threaded, non-blocking processor for applying messages to the beacon chain. network_beacon_processor: Arc>, - /// The `Router` logger. - log: slog::Logger, /// Provides de-bounce functionality for logging. logger_debounce: TimeLatch, } @@ -56,19 +52,19 @@ pub enum RouterMessage { /// An RPC request has been received. RPCRequestReceived { peer_id: PeerId, - id: PeerRequestId, - request: rpc::Request, + inbound_request_id: InboundRequestId, + request_type: RequestType, }, /// An RPC response has been received. RPCResponseReceived { peer_id: PeerId, - request_id: AppRequestId, + app_request_id: AppRequestId, response: Response, }, /// An RPC request failed RPCFailed { peer_id: PeerId, - request_id: AppRequestId, + app_request_id: AppRequestId, error: RPCError, }, /// A gossip message has been received. The fields are: message id, the peer that sent us this @@ -77,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 { @@ -91,14 +89,11 @@ impl Router { beacon_processor_send: BeaconProcessorSend, beacon_processor_reprocess_tx: mpsc::Sender, fork_context: Arc, - log: slog::Logger, ) -> Result>, String> { - let message_handler_log = log.new(o!("service"=> "router")); - trace!(message_handler_log, "Service starting"); + trace!("Service starting"); let (handler_send, handler_recv) = mpsc::unbounded_channel(); - let sync_logger = log.new(o!("service"=> "sync")); // generate the message channel let (sync_send, sync_recv) = mpsc::unbounded_channel::>(); @@ -112,7 +107,6 @@ impl Router { network_globals: network_globals.clone(), invalid_block_storage, executor: executor.clone(), - log: log.clone(), }; let network_beacon_processor = Arc::new(network_beacon_processor); @@ -124,7 +118,6 @@ impl Router { network_beacon_processor.clone(), sync_recv, fork_context, - sync_logger, ); // generate the Message handler @@ -132,18 +125,18 @@ impl Router { network_globals, chain: beacon_chain, sync_send, - network: HandlerNetworkContext::new(network_send, log.clone()), + network: HandlerNetworkContext::new(network_send), network_beacon_processor, - log: message_handler_log, logger_debounce: TimeLatch::default(), }; // spawn handler task and move the message handler instance into the spawned thread executor.spawn( async move { - debug!(log, "Network message router started"); + debug!("Network message router started"); UnboundedReceiverStream::new(handler_recv) .for_each(move |msg| future::ready(handler.handle_message(msg))) + .instrument(info_span!("", service = "router")) .await; }, "router", @@ -164,26 +157,30 @@ 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, - id, - request, + inbound_request_id, + request_type, } => { - self.handle_rpc_request(peer_id, id, request); + self.handle_rpc_request(peer_id, inbound_request_id, request_type); } RouterMessage::RPCResponseReceived { peer_id, - request_id, + app_request_id, response, } => { - self.handle_rpc_response(peer_id, request_id, response); + self.handle_rpc_response(peer_id, app_request_id, response); } RouterMessage::RPCFailed { peer_id, - request_id, + app_request_id, error, } => { - self.on_rpc_error(peer_id, request_id, error); + self.on_rpc_error(peer_id, app_request_id, error); } RouterMessage::PubsubMessage(id, peer_id, gossip, should_process) => { self.handle_gossip(id, peer_id, gossip, should_process); @@ -197,23 +194,18 @@ impl Router { fn handle_rpc_request( &mut self, peer_id: PeerId, - request_id: PeerRequestId, - rpc_request: rpc::Request, + inbound_request_id: InboundRequestId, // Use ResponseId here + request_type: RequestType, ) { if !self.network_globals.peers.read().is_connected(&peer_id) { - debug!(self.log, "Dropping request of disconnected peer"; "peer_id" => %peer_id, "request" => ?rpc_request); + debug!(%peer_id, request = ?request_type, "Dropping request of disconnected peer"); return; } - match rpc_request.r#type { - RequestType::Status(status_message) => self.on_status_request( - peer_id, - request_id.0, - request_id.1, - rpc_request.id, - status_message, - ), + match request_type { + RequestType::Status(status_message) => { + self.on_status_request(peer_id, inbound_request_id, status_message) + } RequestType::BlocksByRange(request) => { - // return just one block in case the step parameter is used. https://github.com/ethereum/consensus-specs/pull/2856 let mut count = *request.count(); if *request.step() > 1 { count = 1; @@ -230,9 +222,7 @@ impl Router { self.handle_beacon_processor_send_result( self.network_beacon_processor.send_blocks_by_range_request( peer_id, - request_id.0, - request_id.1, - rpc_request.id, + inbound_request_id, blocks_request, ), ) @@ -240,86 +230,50 @@ impl Router { RequestType::BlocksByRoot(request) => self.handle_beacon_processor_send_result( self.network_beacon_processor.send_blocks_by_roots_request( peer_id, - request_id.0, - request_id.1, - rpc_request.id, + inbound_request_id, request, ), ), RequestType::BlobsByRange(request) => self.handle_beacon_processor_send_result( self.network_beacon_processor.send_blobs_by_range_request( peer_id, - request_id.0, - request_id.1, - rpc_request.id, + inbound_request_id, request, ), ), RequestType::BlobsByRoot(request) => self.handle_beacon_processor_send_result( self.network_beacon_processor.send_blobs_by_roots_request( peer_id, - request_id.0, - request_id.1, - rpc_request.id, + inbound_request_id, request, ), ), RequestType::DataColumnsByRoot(request) => self.handle_beacon_processor_send_result( self.network_beacon_processor - .send_data_columns_by_roots_request( - peer_id, - request_id.0, - request_id.1, - rpc_request.id, - request, - ), + .send_data_columns_by_roots_request(peer_id, inbound_request_id, request), ), RequestType::DataColumnsByRange(request) => self.handle_beacon_processor_send_result( self.network_beacon_processor - .send_data_columns_by_range_request( - peer_id, - request_id.0, - request_id.1, - rpc_request.id, - request, - ), + .send_data_columns_by_range_request(peer_id, inbound_request_id, request), ), RequestType::LightClientBootstrap(request) => self.handle_beacon_processor_send_result( self.network_beacon_processor - .send_light_client_bootstrap_request( - peer_id, - request_id.0, - request_id.1, - rpc_request.id, - request, - ), + .send_light_client_bootstrap_request(peer_id, inbound_request_id, request), ), RequestType::LightClientOptimisticUpdate => self.handle_beacon_processor_send_result( self.network_beacon_processor - .send_light_client_optimistic_update_request( - peer_id, - request_id.0, - request_id.1, - rpc_request.id, - ), + .send_light_client_optimistic_update_request(peer_id, inbound_request_id), ), RequestType::LightClientFinalityUpdate => self.handle_beacon_processor_send_result( self.network_beacon_processor - .send_light_client_finality_update_request( - peer_id, - request_id.0, - request_id.1, - rpc_request.id, - ), + .send_light_client_finality_update_request(peer_id, inbound_request_id), ), RequestType::LightClientUpdatesByRange(request) => self .handle_beacon_processor_send_result( self.network_beacon_processor .send_light_client_updates_by_range_request( peer_id, - request_id.0, - request_id.1, - rpc_request.id, + inbound_request_id, request, ), ), @@ -331,34 +285,34 @@ impl Router { fn handle_rpc_response( &mut self, peer_id: PeerId, - request_id: AppRequestId, + app_request_id: AppRequestId, response: Response, ) { match response { Response::Status(status_message) => { - debug!(self.log, "Received Status Response"; "peer_id" => %peer_id, &status_message); + debug!(%peer_id, ?status_message,"Received Status Response"); self.handle_beacon_processor_send_result( self.network_beacon_processor .send_status_message(peer_id, status_message), ) } Response::BlocksByRange(beacon_block) => { - self.on_blocks_by_range_response(peer_id, request_id, beacon_block); + self.on_blocks_by_range_response(peer_id, app_request_id, beacon_block); } Response::BlocksByRoot(beacon_block) => { - self.on_blocks_by_root_response(peer_id, request_id, beacon_block); + self.on_blocks_by_root_response(peer_id, app_request_id, beacon_block); } Response::BlobsByRange(blob) => { - self.on_blobs_by_range_response(peer_id, request_id, blob); + self.on_blobs_by_range_response(peer_id, app_request_id, blob); } Response::BlobsByRoot(blob) => { - self.on_blobs_by_root_response(peer_id, request_id, blob); + self.on_blobs_by_root_response(peer_id, app_request_id, blob); } Response::DataColumnsByRoot(data_column) => { - self.on_data_columns_by_root_response(peer_id, request_id, data_column); + self.on_data_columns_by_root_response(peer_id, app_request_id, data_column); } Response::DataColumnsByRange(data_column) => { - self.on_data_columns_by_range_response(peer_id, request_id, data_column); + self.on_data_columns_by_range_response(peer_id, app_request_id, data_column); } // Light client responses should not be received Response::LightClientBootstrap(_) @@ -448,7 +402,7 @@ impl Router { ) } PubsubMessage::VoluntaryExit(exit) => { - debug!(self.log, "Received a voluntary exit"; "peer_id" => %peer_id); + debug!(%peer_id, "Received a voluntary exit"); self.handle_beacon_processor_send_result( self.network_beacon_processor .send_gossip_voluntary_exit(message_id, peer_id, exit), @@ -456,9 +410,8 @@ impl Router { } PubsubMessage::ProposerSlashing(proposer_slashing) => { debug!( - self.log, - "Received a proposer slashing"; - "peer_id" => %peer_id + %peer_id, + "Received a proposer slashing" ); self.handle_beacon_processor_send_result( self.network_beacon_processor.send_gossip_proposer_slashing( @@ -470,9 +423,8 @@ impl Router { } PubsubMessage::AttesterSlashing(attester_slashing) => { debug!( - self.log, - "Received a attester slashing"; - "peer_id" => %peer_id + %peer_id, + "Received a attester slashing" ); self.handle_beacon_processor_send_result( self.network_beacon_processor.send_gossip_attester_slashing( @@ -484,9 +436,8 @@ impl Router { } PubsubMessage::SignedContributionAndProof(contribution_and_proof) => { trace!( - self.log, - "Received sync committee aggregate"; - "peer_id" => %peer_id + %peer_id, + "Received sync committee aggregate" ); self.handle_beacon_processor_send_result( self.network_beacon_processor.send_gossip_sync_contribution( @@ -499,9 +450,8 @@ impl Router { } PubsubMessage::SyncCommitteeMessage(sync_committtee_msg) => { trace!( - self.log, - "Received sync committee signature"; - "peer_id" => %peer_id + %peer_id, + "Received sync committee signature" ); self.handle_beacon_processor_send_result( self.network_beacon_processor.send_gossip_sync_signature( @@ -515,9 +465,8 @@ impl Router { } PubsubMessage::LightClientFinalityUpdate(light_client_finality_update) => { trace!( - self.log, - "Received light client finality update"; - "peer_id" => %peer_id + %peer_id, + "Received light client finality update" ); self.handle_beacon_processor_send_result( self.network_beacon_processor @@ -531,9 +480,9 @@ impl Router { } PubsubMessage::LightClientOptimisticUpdate(light_client_optimistic_update) => { trace!( - self.log, - "Received light client optimistic update"; - "peer_id" => %peer_id + %peer_id, + "Received light client optimistic update" + ); self.handle_beacon_processor_send_result( self.network_beacon_processor @@ -559,7 +508,7 @@ impl Router { fn send_status(&mut self, peer_id: PeerId) { let status_message = status_message(&self.chain); - debug!(self.log, "Sending Status Request"; "peer" => %peer_id, &status_message); + debug!(%peer_id, ?status_message, "Sending Status Request"); self.network .send_processor_request(peer_id, RequestType::Status(status_message)); } @@ -567,21 +516,20 @@ impl Router { fn send_to_sync(&mut self, message: SyncMessage) { self.sync_send.send(message).unwrap_or_else(|e| { warn!( - self.log, - "Could not send message to the sync service"; - "error" => %e, + error = %e, + "Could not send message to the sync service" ) }); } /// An error occurred during an RPC request. The state is maintained by the sync manager, so /// this function notifies the sync manager of the error. - pub fn on_rpc_error(&mut self, peer_id: PeerId, request_id: AppRequestId, error: RPCError) { + pub fn on_rpc_error(&mut self, peer_id: PeerId, app_request_id: AppRequestId, error: RPCError) { // Check if the failed RPC belongs to sync - if let AppRequestId::Sync(request_id) = request_id { + if let AppRequestId::Sync(sync_request_id) = app_request_id { self.send_to_sync(SyncMessage::RpcError { peer_id, - request_id, + sync_request_id, error, }); } @@ -593,19 +541,16 @@ impl Router { pub fn on_status_request( &mut self, peer_id: PeerId, - connection_id: ConnectionId, - substream_id: SubstreamId, - request_id: RequestId, + inbound_request_id: InboundRequestId, // Use ResponseId here status: StatusMessage, ) { - debug!(self.log, "Received Status Request"; "peer_id" => %peer_id, &status); + debug!(%peer_id, ?status, "Received Status Request"); // Say status back. self.network.send_response( peer_id, + inbound_request_id, Response::Status(status_message(&self.chain)), - (connection_id, substream_id), - request_id, ); self.handle_beacon_processor_send_result( @@ -619,32 +564,33 @@ impl Router { pub fn on_blocks_by_range_response( &mut self, peer_id: PeerId, - request_id: AppRequestId, + app_request_id: AppRequestId, beacon_block: Option>>, ) { - let request_id = match request_id { - AppRequestId::Sync(sync_id) => match sync_id { + let sync_request_id = match app_request_id { + AppRequestId::Sync(sync_request_id) => match sync_request_id { id @ SyncRequestId::BlocksByRange { .. } => id, other => { - crit!(self.log, "BlocksByRange response on incorrect request"; "request" => ?other); + crit!(request = ?other, "BlocksByRange response on incorrect request"); return; } }, AppRequestId::Router => { - crit!(self.log, "All BBRange requests belong to sync"; "peer_id" => %peer_id); + crit!(%peer_id, "All BBRange requests belong to sync"); return; } + AppRequestId::Internal => unreachable!("Handled internally"), }; trace!( - self.log, - "Received BlocksByRange Response"; - "peer" => %peer_id, + %peer_id, + "Received BlocksByRange Response" + ); self.send_to_sync(SyncMessage::RpcBlock { peer_id, - request_id, + sync_request_id, beacon_block, seen_timestamp: timestamp_now(), }); @@ -653,27 +599,23 @@ impl Router { pub fn on_blobs_by_range_response( &mut self, peer_id: PeerId, - request_id: AppRequestId, + app_request_id: AppRequestId, blob_sidecar: Option>>, ) { trace!( - self.log, - "Received BlobsByRange Response"; - "peer" => %peer_id, + %peer_id, + "Received BlobsByRange Response" ); - if let AppRequestId::Sync(id) = request_id { + if let AppRequestId::Sync(sync_request_id) = app_request_id { self.send_to_sync(SyncMessage::RpcBlob { peer_id, - request_id: id, + sync_request_id, blob_sidecar, seen_timestamp: timestamp_now(), }); } else { - crit!( - self.log, - "All blobs by range responses should belong to sync" - ); + crit!("All blobs by range responses should belong to sync"); } } @@ -681,31 +623,31 @@ impl Router { pub fn on_blocks_by_root_response( &mut self, peer_id: PeerId, - request_id: AppRequestId, + app_request_id: AppRequestId, beacon_block: Option>>, ) { - let request_id = match request_id { + let sync_request_id = match app_request_id { AppRequestId::Sync(sync_id) => match sync_id { id @ SyncRequestId::SingleBlock { .. } => id, other => { - crit!(self.log, "BlocksByRoot response on incorrect request"; "request" => ?other); + crit!(request = ?other, "BlocksByRoot response on incorrect request"); return; } }, AppRequestId::Router => { - crit!(self.log, "All BBRoot requests belong to sync"; "peer_id" => %peer_id); + crit!(%peer_id, "All BBRoot requests belong to sync"); return; } + AppRequestId::Internal => unreachable!("Handled internally"), }; trace!( - self.log, - "Received BlocksByRoot Response"; - "peer" => %peer_id, + %peer_id, + "Received BlocksByRoot Response" ); self.send_to_sync(SyncMessage::RpcBlock { peer_id, - request_id, + sync_request_id, beacon_block, seen_timestamp: timestamp_now(), }); @@ -715,30 +657,30 @@ impl Router { pub fn on_blobs_by_root_response( &mut self, peer_id: PeerId, - request_id: AppRequestId, + app_request_id: AppRequestId, blob_sidecar: Option>>, ) { - let request_id = match request_id { + let sync_request_id = match app_request_id { AppRequestId::Sync(sync_id) => match sync_id { id @ SyncRequestId::SingleBlob { .. } => id, other => { - crit!(self.log, "BlobsByRoot response on incorrect request"; "request" => ?other); + crit!(request = ?other, "BlobsByRoot response on incorrect request"); return; } }, AppRequestId::Router => { - crit!(self.log, "All BlobsByRoot requests belong to sync"; "peer_id" => %peer_id); + crit!(%peer_id, "All BlobsByRoot requests belong to sync"); return; } + AppRequestId::Internal => unreachable!("Handled internally"), }; trace!( - self.log, - "Received BlobsByRoot Response"; - "peer" => %peer_id, + %peer_id, + "Received BlobsByRoot Response" ); self.send_to_sync(SyncMessage::RpcBlob { - request_id, + sync_request_id, peer_id, blob_sidecar, seen_timestamp: timestamp_now(), @@ -749,30 +691,30 @@ impl Router { pub fn on_data_columns_by_root_response( &mut self, peer_id: PeerId, - request_id: AppRequestId, + app_request_id: AppRequestId, data_column: Option>>, ) { - let request_id = match request_id { + let sync_request_id = match app_request_id { AppRequestId::Sync(sync_id) => match sync_id { id @ SyncRequestId::DataColumnsByRoot { .. } => id, other => { - crit!(self.log, "DataColumnsByRoot response on incorrect request"; "request" => ?other); + crit!(request = ?other, "DataColumnsByRoot response on incorrect request"); return; } }, AppRequestId::Router => { - crit!(self.log, "All DataColumnsByRoot requests belong to sync"; "peer_id" => %peer_id); + crit!(%peer_id, "All DataColumnsByRoot requests belong to sync"); return; } + AppRequestId::Internal => unreachable!("Handled internally"), }; trace!( - self.log, - "Received DataColumnsByRoot Response"; - "peer" => %peer_id, + %peer_id, + "Received DataColumnsByRoot Response" ); self.send_to_sync(SyncMessage::RpcDataColumn { - request_id, + sync_request_id, peer_id, data_column, seen_timestamp: timestamp_now(), @@ -782,27 +724,23 @@ impl Router { pub fn on_data_columns_by_range_response( &mut self, peer_id: PeerId, - request_id: AppRequestId, + app_request_id: AppRequestId, data_column: Option>>, ) { trace!( - self.log, - "Received DataColumnsByRange Response"; - "peer" => %peer_id, + %peer_id, + "Received DataColumnsByRange Response" ); - if let AppRequestId::Sync(id) = request_id { + if let AppRequestId::Sync(sync_request_id) = app_request_id { self.send_to_sync(SyncMessage::RpcDataColumn { peer_id, - request_id: id, + sync_request_id, data_column, seen_timestamp: timestamp_now(), }); } else { - crit!( - self.log, - "All data columns by range responses should belong to sync" - ); + crit!("All data columns by range responses should belong to sync"); } } @@ -818,8 +756,7 @@ impl Router { }; if self.logger_debounce.elapsed() { - error!(&self.log, "Unable to send message to the beacon processor"; - "error" => %e, "type" => work_type) + error!(error = %e, work_type, "Unable to send message to the beacon processor") } } } @@ -831,27 +768,25 @@ impl Router { pub struct HandlerNetworkContext { /// The network channel to relay messages to the Network service. network_send: mpsc::UnboundedSender>, - /// Logger for the `NetworkContext`. - log: slog::Logger, } impl HandlerNetworkContext { - pub fn new(network_send: mpsc::UnboundedSender>, log: slog::Logger) -> Self { - Self { network_send, log } + pub fn new(network_send: mpsc::UnboundedSender>) -> Self { + Self { network_send } } /// Sends a message to the network task. fn inform_network(&mut self, msg: NetworkMessage) { - self.network_send.send(msg).unwrap_or_else( - |e| warn!(self.log, "Could not send message to the network service"; "error" => %e), - ) + self.network_send + .send(msg) + .unwrap_or_else(|e| warn!(error = %e,"Could not send message to the network service")) } /// Sends a request to the network task. pub fn send_processor_request(&mut self, peer_id: PeerId, request: RequestType) { self.inform_network(NetworkMessage::SendRequest { peer_id, - request_id: AppRequestId::Router, + app_request_id: AppRequestId::Router, request, }) } @@ -860,14 +795,12 @@ impl HandlerNetworkContext { pub fn send_response( &mut self, peer_id: PeerId, + inbound_request_id: InboundRequestId, response: Response, - id: PeerRequestId, - request_id: RequestId, ) { self.inform_network(NetworkMessage::SendResponse { - request_id, peer_id, - id, + inbound_request_id, response, }) } diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index 42889169a25..77204b455da 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -10,22 +10,22 @@ use beacon_processor::{work_reprocessing_queue::ReprocessQueueMessage, BeaconPro use futures::channel::mpsc::Sender; use futures::future::OptionFuture; use futures::prelude::*; -use futures::StreamExt; -use lighthouse_network::rpc::{RequestId, RequestType}; +use lighthouse_network::rpc::InboundRequestId; +use lighthouse_network::rpc::RequestType; use lighthouse_network::service::Network; use lighthouse_network::types::GossipKind; use lighthouse_network::Enr; use lighthouse_network::{prometheus_client::registry::Registry, MessageAcceptance}; use lighthouse_network::{ rpc::{GoodbyeReason, RpcErrorResponse}, - Context, PeerAction, PeerRequestId, PubsubMessage, ReportSource, Response, Subnet, + Context, PeerAction, PubsubMessage, ReportSource, Response, Subnet, }; use lighthouse_network::{ service::api_types::AppRequestId, types::{core_topics_to_subscribe, GossipEncoding, GossipTopic}, MessageId, NetworkEvent, NetworkGlobals, PeerId, }; -use slog::{crit, debug, error, info, o, trace, warn}; +use logging::crit; use std::collections::BTreeSet; use std::{collections::HashSet, pin::Pin, sync::Arc, time::Duration}; use store::HotColdDB; @@ -33,6 +33,7 @@ use strum::IntoStaticStr; use task_executor::ShutdownReason; use tokio::sync::mpsc; use tokio::time::Sleep; +use tracing::{debug, error, info, info_span, trace, warn, Instrument}; use types::{ ChainSpec, EthSpec, ForkContext, Slot, SubnetId, SyncCommitteeSubscription, SyncSubnetId, Unsigned, ValidatorSubscription, @@ -61,22 +62,20 @@ pub enum NetworkMessage { SendRequest { peer_id: PeerId, request: RequestType, - request_id: AppRequestId, + app_request_id: AppRequestId, }, /// Send a successful Response to the libp2p service. SendResponse { peer_id: PeerId, - request_id: RequestId, + inbound_request_id: InboundRequestId, response: Response, - id: PeerRequestId, }, /// Sends an error response to an RPC request. SendErrorResponse { peer_id: PeerId, - request_id: RequestId, + inbound_request_id: InboundRequestId, error: RpcErrorResponse, reason: String, - id: PeerRequestId, }, /// Publish a list of messages to the gossipsub protocol. Publish { messages: Vec> }, @@ -186,8 +185,6 @@ pub struct NetworkService { next_fork_subscriptions: Pin>>, /// A delay that expires when we need to unsubscribe from old fork topics. next_unsubscribe: Pin>>, - /// Subscribe to all the subnets once synced. - subscribe_all_subnets: bool, /// Shutdown beacon node after sync is complete. shutdown_after_sync: bool, /// Whether metrics are enabled or not. @@ -196,11 +193,8 @@ pub struct NetworkService { metrics_update: tokio::time::Interval, /// gossipsub_parameter_update timer gossipsub_parameter_update: tokio::time::Interval, - /// enable_light_client_server indicator - enable_light_client_server: bool, - /// The logger for the network service. + /// Provides fork specific info. fork_context: Arc, - log: slog::Logger, } impl NetworkService { @@ -219,30 +213,23 @@ impl NetworkService { ), String, > { - let network_log = executor.log().clone(); // build the channels for external comms let (network_senders, network_receivers) = NetworkSenders::new(); #[cfg(feature = "disable-backfill")] - warn!( - network_log, - "Backfill is disabled. DO NOT RUN IN PRODUCTION" - ); + warn!("Backfill is disabled. DO NOT RUN IN PRODUCTION"); if let (true, false, Some(v4)) = ( config.upnp_enabled, config.disable_discovery, config.listen_addrs().v4(), ) { - let nw = network_log.clone(); let v4 = v4.clone(); executor.spawn( async move { - info!(nw, "UPnP Attempting to initialise routes"); - if let Err(e) = - nat::construct_upnp_mappings(v4.addr, v4.disc_port, nw.clone()).await - { - info!(nw, "Could not UPnP map Discovery port"; "error" => %e); + info!("UPnP Attempting to initialise routes"); + if let Err(e) = nat::construct_upnp_mappings(v4.addr, v4.disc_port).await { + info!(error = %e, "Could not UPnP map Discovery port"); } }, "UPnP", @@ -271,7 +258,7 @@ impl NetworkService { &beacon_chain.spec, )); - debug!(network_log, "Current fork"; "fork_name" => ?fork_context.current_fork()); + debug!(fork_name = ?fork_context.current_fork(), "Current fork"); // construct the libp2p service context let service_context = Context { @@ -283,15 +270,14 @@ impl NetworkService { }; // launch libp2p service - let (mut libp2p, network_globals) = - Network::new(executor.clone(), service_context, &network_log).await?; + let (mut libp2p, network_globals) = Network::new(executor.clone(), service_context).await?; // Repopulate the DHT with stored ENR's if discovery is not disabled. if !config.disable_discovery { let enrs_to_load = load_dht::(store.clone()); debug!( - network_log, - "Loading peers into the routing table"; "peers" => enrs_to_load.len() + peers = enrs_to_load.len(), + "Loading peers into the routing table" ); for enr in enrs_to_load { libp2p.add_enr(enr.clone()); @@ -316,7 +302,6 @@ impl NetworkService { beacon_processor_send, beacon_processor_reprocess_tx, fork_context.clone(), - network_log.clone(), )?; // attestation and sync committee subnet service @@ -324,7 +309,6 @@ impl NetworkService { beacon_chain.clone(), network_globals.local_enr().node_id(), &config, - &network_log, ); // create a timer for updating network metrics @@ -339,7 +323,6 @@ impl NetworkService { } = network_receivers; // create the network service and spawn the task - let network_log = network_log.new(o!("service" => "network")); let network_service = NetworkService { beacon_chain, libp2p, @@ -352,14 +335,11 @@ impl NetworkService { next_fork_update, next_fork_subscriptions, next_unsubscribe, - subscribe_all_subnets: config.subscribe_all_subnets, shutdown_after_sync: config.shutdown_after_sync, metrics_enabled: config.metrics_enabled, metrics_update, gossipsub_parameter_update, fork_context, - log: network_log, - enable_light_client_server: config.enable_light_client_server, }; Ok((network_service, network_globals, network_senders)) @@ -428,7 +408,7 @@ impl NetworkService { fn send_to_router(&mut self, msg: RouterMessage) { if let Err(mpsc::error::SendError(msg)) = self.router_send.send(msg) { - debug!(self.log, "Failed to send msg to router"; "msg" => ?msg); + debug!(?msg, "Failed to send msg to router"); } } @@ -467,7 +447,7 @@ impl NetworkService { Some(_) = &mut self.next_unsubscribe => { let new_enr_fork_id = self.beacon_chain.enr_fork_id(); self.libp2p.unsubscribe_from_fork_topics_except(new_enr_fork_id.fork_digest); - info!(self.log, "Unsubscribed from old fork topics"); + info!("Unsubscribed from old fork topics"); self.next_unsubscribe = Box::pin(None.into()); } @@ -475,17 +455,17 @@ impl NetworkService { if let Some((fork_name, _)) = self.beacon_chain.duration_to_next_fork() { let fork_version = self.beacon_chain.spec.fork_version_for_name(fork_name); let fork_digest = ChainSpec::compute_fork_digest(fork_version, self.beacon_chain.genesis_validators_root); - info!(self.log, "Subscribing to new fork topics"); + info!("Subscribing to new fork topics"); self.libp2p.subscribe_new_fork_topics(fork_name, fork_digest); self.next_fork_subscriptions = Box::pin(None.into()); } else { - error!(self.log, "Fork subscription scheduled but no fork scheduled"); + error!( "Fork subscription scheduled but no fork scheduled"); } } } } - }; + }.instrument(info_span!("", service = "network")); executor.spawn(service_fut, "network"); } @@ -505,32 +485,39 @@ 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, - id, - request, + inbound_request_id, + request_type, } => { self.send_to_router(RouterMessage::RPCRequestReceived { peer_id, - id, - request, + inbound_request_id, + request_type, }); } NetworkEvent::ResponseReceived { peer_id, - id, + app_request_id, response, } => { self.send_to_router(RouterMessage::RPCResponseReceived { peer_id, - request_id: id, + app_request_id, response, }); } - NetworkEvent::RPCFailed { id, peer_id, error } => { + NetworkEvent::RPCFailed { + app_request_id, + peer_id, + error, + } => { self.send_to_router(RouterMessage::RPCFailed { peer_id, - request_id: id, + app_request_id, error, }); } @@ -599,9 +586,8 @@ impl NetworkService { .await .map_err(|e| { warn!( - self.log, - "failed to send a shutdown signal"; - "error" => %e + error = %e, + "failed to send a shutdown signal" ) }); } @@ -621,45 +607,43 @@ impl NetworkService { NetworkMessage::SendRequest { peer_id, request, - request_id, + app_request_id, } => { - if let Err((request_id, error)) = - self.libp2p.send_request(peer_id, request_id, request) + if let Err((app_request_id, error)) = + self.libp2p.send_request(peer_id, app_request_id, request) { self.send_to_router(RouterMessage::RPCFailed { peer_id, - request_id, + app_request_id, error, }); } } NetworkMessage::SendResponse { peer_id, + inbound_request_id, response, - id, - request_id, } => { - self.libp2p.send_response(peer_id, id, request_id, response); + self.libp2p + .send_response(peer_id, inbound_request_id, response); } NetworkMessage::SendErrorResponse { peer_id, error, - id, - request_id, + inbound_request_id, reason, } => { self.libp2p - .send_error_response(peer_id, id, request_id, error, reason); + .send_error_response(peer_id, inbound_request_id, error, reason); } NetworkMessage::ValidationResult { propagation_source, message_id, validation_result, } => { - trace!(self.log, "Propagating gossipsub message"; - "propagation_peer" => ?propagation_source, - "message_id" => %message_id, - "validation_result" => ?validation_result + trace!( propagation_peer = ?propagation_source, + %message_id, + ?validation_result, "Propagating gossipsub message" ); self.libp2p.report_message_validation_result( &propagation_source, @@ -675,10 +659,9 @@ impl NetworkService { } } debug!( - self.log, - "Sending pubsub messages"; - "count" => messages.len(), - "topics" => ?topic_kinds + count = messages.len(), + topics = ?topic_kinds, + "Sending pubsub messages" ); self.libp2p.publish(messages); } @@ -713,9 +696,8 @@ impl NetworkService { .await { warn!( - self.log, - "failed to send a shutdown signal"; - "error" => %e + error = %e, + "failed to send a shutdown signal" ) } return; @@ -724,8 +706,8 @@ impl NetworkService { let mut subscribed_topics: Vec = vec![]; for topic_kind in core_topics_to_subscribe::( self.fork_context.current_fork(), - &self.fork_context.spec, &self.network_globals.as_topic_config(), + &self.fork_context.spec, ) { for fork_digest in self.required_gossip_fork_digests() { let topic = GossipTopic::new( @@ -736,70 +718,30 @@ impl NetworkService { if self.libp2p.subscribe(topic.clone()) { subscribed_topics.push(topic); } else { - warn!(self.log, "Could not subscribe to topic"; "topic" => %topic); - } - } - } - - if self.enable_light_client_server { - for light_client_topic_kind in - lighthouse_network::types::LIGHT_CLIENT_GOSSIP_TOPICS.iter() - { - for fork_digest in self.required_gossip_fork_digests() { - let light_client_topic = GossipTopic::new( - light_client_topic_kind.clone(), - GossipEncoding::default(), - fork_digest, - ); - if self.libp2p.subscribe(light_client_topic.clone()) { - subscribed_topics.push(light_client_topic); - } else { - warn!(self.log, "Could not subscribe to topic"; "topic" => %light_client_topic); - } + warn!(%topic, "Could not subscribe to topic"); } } } // If we are to subscribe to all subnets we do it here - if self.subscribe_all_subnets { + if self.network_globals.config.subscribe_all_subnets { for subnet_id in 0..<::EthSpec as EthSpec>::SubnetBitfieldLength::to_u64() { let subnet = Subnet::Attestation(SubnetId::new(subnet_id)); // Update the ENR bitfield self.libp2p.update_enr_subnet(subnet, true); - for fork_digest in self.required_gossip_fork_digests() { - let topic = GossipTopic::new(subnet.into(), GossipEncoding::default(), fork_digest); - if self.libp2p.subscribe(topic.clone()) { - subscribed_topics.push(topic); - } else { - warn!(self.log, "Could not subscribe to topic"; "topic" => %topic); - } - } } let subnet_max = <::EthSpec as EthSpec>::SyncCommitteeSubnetCount::to_u64(); for subnet_id in 0..subnet_max { let subnet = Subnet::SyncCommittee(SyncSubnetId::new(subnet_id)); // Update the ENR bitfield self.libp2p.update_enr_subnet(subnet, true); - for fork_digest in self.required_gossip_fork_digests() { - let topic = GossipTopic::new( - subnet.into(), - GossipEncoding::default(), - fork_digest, - ); - if self.libp2p.subscribe(topic.clone()) { - subscribed_topics.push(topic); - } else { - warn!(self.log, "Could not subscribe to topic"; "topic" => %topic); - } - } } } if !subscribed_topics.is_empty() { info!( - self.log, - "Subscribed to topics"; - "topics" => ?subscribed_topics.into_iter().map(|topic| format!("{}", topic)).collect::>() + topics = ?subscribed_topics.into_iter().map(|topic| format!("{}", topic)).collect::>(), + "Subscribed to topics" ); } } @@ -833,19 +775,14 @@ impl NetworkService { .update_gossipsub_parameters(active_validators, slot) .is_err() { - error!( - self.log, - "Failed to update gossipsub parameters"; - "active_validators" => active_validators - ); + error!(active_validators, "Failed to update gossipsub parameters"); } } else { // This scenario will only happen if the caches on the cached canonical head aren't // built. That should never be the case. error!( - self.log, - "Active validator count unavailable"; - "info" => "please report this bug" + info = "please report this bug", + "Active validator count unavailable" ); } } @@ -887,10 +824,9 @@ impl NetworkService { let fork_context = &self.fork_context; if let Some(new_fork_name) = fork_context.from_context_bytes(new_fork_digest) { info!( - self.log, - "Transitioned to new fork"; - "old_fork" => ?fork_context.current_fork(), - "new_fork" => ?new_fork_name, + old_fork = ?fork_context.current_fork(), + new_fork = ?new_fork_name, + "Transitioned to new fork" ); fork_context.update_current_fork(*new_fork_name); @@ -907,21 +843,24 @@ impl NetworkService { self.next_fork_subscriptions = Box::pin(next_fork_subscriptions_delay(&self.beacon_chain).into()); self.next_unsubscribe = Box::pin(Some(tokio::time::sleep(unsubscribe_delay)).into()); - info!(self.log, "Network will unsubscribe from old fork gossip topics in a few epochs"; "remaining_epochs" => UNSUBSCRIBE_DELAY_EPOCHS); + info!( + remaining_epochs = UNSUBSCRIBE_DELAY_EPOCHS, + "Network will unsubscribe from old fork gossip topics in a few epochs" + ); // Remove topic weight from old fork topics to prevent peers that left on the mesh on // old topics from being penalized for not sending us messages. self.libp2p.remove_topic_weight_except(new_fork_digest); } else { - crit!(self.log, "Unknown new enr fork id"; "new_fork_id" => ?new_enr_fork_id); + crit!(new_fork_id = ?new_enr_fork_id, "Unknown new enr fork id"); } } fn subscribed_core_topics(&self) -> bool { let core_topics = core_topics_to_subscribe::( self.fork_context.current_fork(), - &self.fork_context.spec, &self.network_globals.as_topic_config(), + &self.fork_context.spec, ); let core_topics: HashSet<&GossipKind> = HashSet::from_iter(&core_topics); let subscriptions = self.network_globals.gossipsub_subscriptions.read(); @@ -962,26 +901,18 @@ impl Drop for NetworkService { fn drop(&mut self) { // network thread is terminating let enrs = self.libp2p.enr_entries(); - debug!( - self.log, - "Persisting DHT to store"; - "Number of peers" => enrs.len(), - ); + debug!(number_of_peers = enrs.len(), "Persisting DHT to store"); if let Err(e) = clear_dht::(self.store.clone()) { - error!(self.log, "Failed to clear old DHT entries"; "error" => ?e); + error!(error = ?e, "Failed to clear old DHT entries"); } // Still try to update new entries match persist_dht::(self.store.clone(), enrs) { Err(e) => error!( - self.log, - "Failed to persist DHT on drop"; - "error" => ?e - ), - Ok(_) => info!( - self.log, - "Saved DHT state"; + error = ?e, + "Failed to persist DHT on drop" ), + Ok(_) => info!("Saved DHT state"), } - info!(self.log, "Network service shutdown"); + info!("Network service shutdown"); } } diff --git a/beacon_node/network/src/service/tests.rs b/beacon_node/network/src/service/tests.rs index 32bbfcbcaa1..15c3321e94d 100644 --- a/beacon_node/network/src/service/tests.rs +++ b/beacon_node/network/src/service/tests.rs @@ -8,8 +8,6 @@ use beacon_processor::{BeaconProcessorChannels, BeaconProcessorConfig}; use futures::StreamExt; use lighthouse_network::types::{GossipEncoding, GossipKind}; use lighthouse_network::{Enr, GossipTopic}; -use slog::{o, Drain, Level, Logger}; -use sloggers::{null::NullLoggerBuilder, Build}; use std::str::FromStr; use std::sync::Arc; use tokio::runtime::Runtime; @@ -21,28 +19,8 @@ impl NetworkService { } } -fn get_logger(actual_log: bool) -> Logger { - if actual_log { - let drain = { - let decorator = slog_term::TermDecorator::new().build(); - let decorator = - logging::AlignedTermDecorator::new(decorator, logging::MAX_MESSAGE_WIDTH); - let drain = slog_term::FullFormat::new(decorator).build().fuse(); - let drain = slog_async::Async::new(drain).chan_size(2048).build(); - drain.filter_level(Level::Debug) - }; - - Logger::root(drain.fuse(), o!()) - } else { - let builder = NullLoggerBuilder; - builder.build().expect("should build logger") - } -} - #[test] fn test_dht_persistence() { - let log = get_logger(false); - let beacon_chain = BeaconChainHarness::builder(MinimalEthSpec) .default_spec() .deterministic_keypairs(8) @@ -60,8 +38,12 @@ fn test_dht_persistence() { let (signal, exit) = async_channel::bounded(1); let (shutdown_tx, _) = futures::channel::mpsc::channel(1); - let executor = - task_executor::TaskExecutor::new(Arc::downgrade(&runtime), exit, log.clone(), shutdown_tx); + let executor = task_executor::TaskExecutor::new( + Arc::downgrade(&runtime), + exit, + shutdown_tx, + "test-dht-persistence".to_string(), + ); let mut config = NetworkConfig::default(); config.set_ipv4_listening_address(std::net::Ipv4Addr::UNSPECIFIED, 21212, 21212, 21213); @@ -137,8 +119,8 @@ fn test_removing_topic_weight_on_old_topics() { let executor = task_executor::TaskExecutor::new( Arc::downgrade(&runtime), exit, - get_logger(false), shutdown_tx, + "test-removing-topic-weight-on-old-topics".to_string(), ); let mut config = NetworkConfig::default(); diff --git a/beacon_node/network/src/subnet_service/attestation_subnets.rs b/beacon_node/network/src/subnet_service/attestation_subnets.rs new file mode 100644 index 00000000000..dd4724b261a --- /dev/null +++ b/beacon_node/network/src/subnet_service/attestation_subnets.rs @@ -0,0 +1,681 @@ +//! This service keeps track of which shard subnet the beacon node should be subscribed to at any +//! given time. It schedules subscriptions to shard subnets, requests peer discoveries and +//! determines whether attestations should be aggregated and/or passed to the beacon node. + +use super::SubnetServiceMessage; +use std::collections::HashSet; +use std::collections::{HashMap, VecDeque}; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; +use std::time::Duration; + +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use delay_map::{HashMapDelay, HashSetDelay}; +use futures::prelude::*; +use lighthouse_network::{discv5::enr::NodeId, NetworkConfig, Subnet, SubnetDiscovery}; +use slot_clock::SlotClock; +use tracing::{debug, error, info, trace, warn}; +use types::{Attestation, EthSpec, Slot, SubnetId, ValidatorSubscription}; + +use crate::metrics; + +/// The minimum number of slots ahead that we attempt to discover peers for a subscription. If the +/// slot is less than this number, skip the peer discovery process. +/// Subnet discovery query takes at most 30 secs, 2 slots take 24s. +pub(crate) const MIN_PEER_DISCOVERY_SLOT_LOOK_AHEAD: u64 = 2; +/// The fraction of a slot that we subscribe to a subnet before the required slot. +/// +/// Currently a whole slot ahead. +const ADVANCE_SUBSCRIBE_SLOT_FRACTION: u32 = 1; + +/// The number of slots after an aggregator duty where we remove the entry from +/// `aggregate_validators_on_subnet` delay map. +const UNSUBSCRIBE_AFTER_AGGREGATOR_DUTY: u32 = 2; + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub(crate) enum SubscriptionKind { + /// Long lived subscriptions. + /// + /// These have a longer duration and are advertised in our ENR. + LongLived, + /// Short lived subscriptions. + /// + /// Subscribing to these subnets has a short duration and we don't advertise it in our ENR. + ShortLived, +} + +/// A particular subnet at a given slot. +#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy)] +pub struct ExactSubnet { + /// The `SubnetId` associated with this subnet. + pub subnet_id: SubnetId, + /// The `Slot` associated with this subnet. + pub slot: Slot, +} + +pub struct AttestationService { + /// Queued events to return to the driving service. + events: VecDeque, + + /// A reference to the beacon chain to process received attestations. + pub(crate) beacon_chain: Arc>, + + /// Subnets we are currently subscribed to as short lived subscriptions. + /// + /// Once they expire, we unsubscribe from these. + /// We subscribe to subnets when we are an aggregator for an exact subnet. + short_lived_subscriptions: HashMapDelay, + + /// Subnets we are currently subscribed to as long lived subscriptions. + /// + /// We advertise these in our ENR. When these expire, the subnet is removed from our ENR. + /// These are required of all beacon nodes. The exact number is determined by the chain + /// specification. + long_lived_subscriptions: HashSet, + + /// Short lived subscriptions that need to be executed in the future. + scheduled_short_lived_subscriptions: HashSetDelay, + + /// A collection timeouts to track the existence of aggregate validator subscriptions at an + /// `ExactSubnet`. + aggregate_validators_on_subnet: Option>, + + /// The waker for the current thread. + waker: Option, + + /// The discovery mechanism of lighthouse is disabled. + discovery_disabled: bool, + + /// We are always subscribed to all subnets. + subscribe_all_subnets: bool, + + /// Our Discv5 node_id. + node_id: NodeId, + + /// Future used to manage subscribing and unsubscribing from long lived subnets. + next_long_lived_subscription_event: Pin>, + + /// Whether this node is a block proposer-only node. + proposer_only: bool, +} + +impl AttestationService { + /* Public functions */ + + /// Establish the service based on the passed configuration. + pub fn new(beacon_chain: Arc>, node_id: NodeId, config: &NetworkConfig) -> Self { + let slot_duration = beacon_chain.slot_clock.slot_duration(); + + if config.subscribe_all_subnets { + info!("Subscribing to all subnets"); + } else { + info!( + subnets_per_node = beacon_chain.spec.subnets_per_node, + subscription_duration_in_epochs = beacon_chain.spec.epochs_per_subnet_subscription, + "Deterministic long lived subnets enabled" + ); + } + + let track_validators = !config.import_all_attestations; + let aggregate_validators_on_subnet = + track_validators.then(|| HashSetDelay::new(slot_duration)); + let mut service = AttestationService { + events: VecDeque::with_capacity(10), + beacon_chain, + short_lived_subscriptions: HashMapDelay::new(slot_duration), + long_lived_subscriptions: HashSet::default(), + scheduled_short_lived_subscriptions: HashSetDelay::default(), + aggregate_validators_on_subnet, + waker: None, + discovery_disabled: config.disable_discovery, + subscribe_all_subnets: config.subscribe_all_subnets, + node_id, + next_long_lived_subscription_event: { + // Set a dummy sleep. Calculating the current subnet subscriptions will update this + // value with a smarter timing + Box::pin(tokio::time::sleep(Duration::from_secs(1))) + }, + proposer_only: config.proposer_only, + }; + + // If we are not subscribed to all subnets, handle the deterministic set of subnets + if !config.subscribe_all_subnets { + service.recompute_long_lived_subnets(); + } + + service + } + + /// Return count of all currently subscribed subnets (long-lived **and** short-lived). + #[cfg(test)] + pub fn subscription_count(&self) -> usize { + if self.subscribe_all_subnets { + self.beacon_chain.spec.attestation_subnet_count as usize + } else { + let count = self + .short_lived_subscriptions + .keys() + .chain(self.long_lived_subscriptions.iter()) + .collect::>() + .len(); + count + } + } + + /// Returns whether we are subscribed to a subnet for testing purposes. + #[cfg(test)] + pub(crate) fn is_subscribed( + &self, + subnet_id: &SubnetId, + subscription_kind: SubscriptionKind, + ) -> bool { + match subscription_kind { + SubscriptionKind::LongLived => self.long_lived_subscriptions.contains(subnet_id), + SubscriptionKind::ShortLived => self.short_lived_subscriptions.contains_key(subnet_id), + } + } + + #[cfg(test)] + pub(crate) fn long_lived_subscriptions(&self) -> &HashSet { + &self.long_lived_subscriptions + } + + /// Processes a list of validator subscriptions. + /// + /// This will: + /// - Register new validators as being known. + /// - Search for peers for required subnets. + /// - Request subscriptions for subnets on specific slots when required. + /// - Build the timeouts for each of these events. + /// + /// This returns a result simply for the ergonomics of using ?. The result can be + /// safely dropped. + pub fn validator_subscriptions( + &mut self, + subscriptions: impl Iterator, + ) -> Result<(), String> { + // If the node is in a proposer-only state, we ignore all subnet subscriptions. + if self.proposer_only { + return Ok(()); + } + + // Maps each subnet_id subscription to it's highest slot + let mut subnets_to_discover: HashMap = HashMap::new(); + + // Registers the validator with the attestation service. + for subscription in subscriptions { + metrics::inc_counter(&metrics::SUBNET_SUBSCRIPTION_REQUESTS); + + trace!(?subscription, "Validator subscription"); + + // Compute the subnet that is associated with this subscription + let subnet_id = match SubnetId::compute_subnet::( + subscription.slot, + subscription.attestation_committee_index, + subscription.committee_count_at_slot, + &self.beacon_chain.spec, + ) { + Ok(subnet_id) => subnet_id, + Err(e) => { + warn!( + error = ?e, + "Failed to compute subnet id for validator subscription" + ); + continue; + } + }; + // Ensure each subnet_id inserted into the map has the highest slot as it's value. + // Higher slot corresponds to higher min_ttl in the `SubnetDiscovery` entry. + if let Some(slot) = subnets_to_discover.get(&subnet_id) { + if subscription.slot > *slot { + subnets_to_discover.insert(subnet_id, subscription.slot); + } + } else if !self.discovery_disabled { + subnets_to_discover.insert(subnet_id, subscription.slot); + } + + let exact_subnet = ExactSubnet { + subnet_id, + slot: subscription.slot, + }; + + // Determine if the validator is an aggregator. If so, we subscribe to the subnet and + // if successful add the validator to a mapping of known aggregators for that exact + // subnet. + + if subscription.is_aggregator { + metrics::inc_counter(&metrics::SUBNET_SUBSCRIPTION_AGGREGATOR_REQUESTS); + if let Err(e) = self.subscribe_to_short_lived_subnet(exact_subnet) { + warn!(error = e, "Subscription to subnet error"); + } else { + trace!(?exact_subnet, "Subscribed to subnet for aggregator duties"); + } + } + } + + // If the discovery mechanism isn't disabled, attempt to set up a peer discovery for the + // required subnets. + if !self.discovery_disabled { + if let Err(e) = self.discover_peers_request( + subnets_to_discover + .into_iter() + .map(|(subnet_id, slot)| ExactSubnet { subnet_id, slot }), + ) { + warn!(error = e, "Discovery lookup request error"); + }; + } + + Ok(()) + } + + fn recompute_long_lived_subnets(&mut self) { + // Ensure the next computation is scheduled even if assigning subnets fails. + let next_subscription_event = self + .recompute_long_lived_subnets_inner() + .unwrap_or_else(|_| self.beacon_chain.slot_clock.slot_duration()); + + debug!("Recomputing deterministic long lived subnets"); + self.next_long_lived_subscription_event = + Box::pin(tokio::time::sleep(next_subscription_event)); + + if let Some(waker) = self.waker.as_ref() { + waker.wake_by_ref(); + } + } + + /// Gets the long lived subnets the node should be subscribed to during the current epoch and + /// the remaining duration for which they remain valid. + fn recompute_long_lived_subnets_inner(&mut self) -> Result { + let current_epoch = self.beacon_chain.epoch().map_err(|e| { + if !self + .beacon_chain + .slot_clock + .is_prior_to_genesis() + .unwrap_or(false) + { + error!(err = ?e,"Failed to get the current epoch from clock") + } + })?; + + let (subnets, next_subscription_epoch) = SubnetId::compute_subnets_for_epoch::( + self.node_id.raw(), + current_epoch, + &self.beacon_chain.spec, + ) + .map_err(|e| error!(err = e, "Could not compute subnets for current epoch"))?; + + let next_subscription_slot = + next_subscription_epoch.start_slot(T::EthSpec::slots_per_epoch()); + let next_subscription_event = self + .beacon_chain + .slot_clock + .duration_to_slot(next_subscription_slot) + .ok_or_else(|| { + error!("Failed to compute duration to next to long lived subscription event") + })?; + + self.update_long_lived_subnets(subnets.collect()); + + Ok(next_subscription_event) + } + + /// Updates the long lived subnets. + /// + /// New subnets are registered as subscribed, removed subnets as unsubscribed and the Enr + /// updated accordingly. + fn update_long_lived_subnets(&mut self, mut subnets: HashSet) { + info!(subnets = ?subnets.iter().collect::>(),"Subscribing to long-lived subnets"); + for subnet in &subnets { + // Add the events for those subnets that are new as long lived subscriptions. + if !self.long_lived_subscriptions.contains(subnet) { + // Check if this subnet is new and send the subscription event if needed. + if !self.short_lived_subscriptions.contains_key(subnet) { + debug!( + ?subnet, + subscription_kind = ?SubscriptionKind::LongLived, + "Subscribing to subnet" + ); + self.queue_event(SubnetServiceMessage::Subscribe(Subnet::Attestation( + *subnet, + ))); + } + self.queue_event(SubnetServiceMessage::EnrAdd(Subnet::Attestation(*subnet))); + if !self.discovery_disabled { + self.queue_event(SubnetServiceMessage::DiscoverPeers(vec![SubnetDiscovery { + subnet: Subnet::Attestation(*subnet), + min_ttl: None, + }])) + } + } + } + + // Update the long_lived_subnets set and check for subnets that are being removed + std::mem::swap(&mut self.long_lived_subscriptions, &mut subnets); + for subnet in subnets { + if !self.long_lived_subscriptions.contains(&subnet) { + self.handle_removed_subnet(subnet, SubscriptionKind::LongLived); + } + } + } + + /// Checks if we have subscribed aggregate validators for the subnet. If not, checks the gossip + /// verification, re-propagates and returns false. + pub fn should_process_attestation( + &self, + subnet: SubnetId, + attestation: &Attestation, + ) -> bool { + // Proposer-only mode does not need to process attestations + if self.proposer_only { + return false; + } + self.aggregate_validators_on_subnet + .as_ref() + .map(|tracked_vals| { + tracked_vals.contains_key(&ExactSubnet { + subnet_id: subnet, + slot: attestation.data().slot, + }) + }) + .unwrap_or(true) + } + + /* Internal private functions */ + + /// Adds an event to the event queue and notifies that this service is ready to be polled + /// again. + fn queue_event(&mut self, ev: SubnetServiceMessage) { + self.events.push_back(ev); + if let Some(waker) = &self.waker { + waker.wake_by_ref() + } + } + /// Checks if there are currently queued discovery requests and the time required to make the + /// request. + /// + /// If there is sufficient time, queues a peer discovery request for all the required subnets. + fn discover_peers_request( + &mut self, + exact_subnets: impl Iterator, + ) -> Result<(), &'static str> { + let current_slot = self + .beacon_chain + .slot_clock + .now() + .ok_or("Could not get the current slot")?; + + let discovery_subnets: Vec = exact_subnets + .filter_map(|exact_subnet| { + // Check if there is enough time to perform a discovery lookup. + if exact_subnet.slot + >= current_slot.saturating_add(MIN_PEER_DISCOVERY_SLOT_LOOK_AHEAD) + { + // Send out an event to start looking for peers. + // Require the peer for an additional slot to ensure we keep the peer for the + // duration of the subscription. + let min_ttl = self + .beacon_chain + .slot_clock + .duration_to_slot(exact_subnet.slot + 1) + .map(|duration| std::time::Instant::now() + duration); + Some(SubnetDiscovery { + subnet: Subnet::Attestation(exact_subnet.subnet_id), + min_ttl, + }) + } else { + // We may want to check the global PeerInfo to see estimated timeouts for each + // peer before they can be removed. + warn!( + subnet_id = ?exact_subnet, + "Not enough time for a discovery search" + ); + None + } + }) + .collect(); + + if !discovery_subnets.is_empty() { + self.queue_event(SubnetServiceMessage::DiscoverPeers(discovery_subnets)); + } + Ok(()) + } + + // Subscribes to the subnet if it should be done immediately, or schedules it if required. + fn subscribe_to_short_lived_subnet( + &mut self, + ExactSubnet { subnet_id, slot }: ExactSubnet, + ) -> Result<(), &'static str> { + let slot_duration = self.beacon_chain.slot_clock.slot_duration(); + + // The short time we schedule the subscription before it's actually required. This + // ensures we are subscribed on time, and allows consecutive subscriptions to the same + // subnet to overlap, reducing subnet churn. + let advance_subscription_duration = slot_duration / ADVANCE_SUBSCRIBE_SLOT_FRACTION; + // The time to the required slot. + let time_to_subscription_slot = self + .beacon_chain + .slot_clock + .duration_to_slot(slot) + .unwrap_or_default(); // If this is a past slot we will just get a 0 duration. + + // Calculate how long before we need to subscribe to the subnet. + let time_to_subscription_start = + time_to_subscription_slot.saturating_sub(advance_subscription_duration); + + // The time after a duty slot where we no longer need it in the `aggregate_validators_on_subnet` + // delay map. + let time_to_unsubscribe = + time_to_subscription_slot + UNSUBSCRIBE_AFTER_AGGREGATOR_DUTY * slot_duration; + if let Some(tracked_vals) = self.aggregate_validators_on_subnet.as_mut() { + tracked_vals.insert_at(ExactSubnet { subnet_id, slot }, time_to_unsubscribe); + } + + // If the subscription should be done in the future, schedule it. Otherwise subscribe + // immediately. + if time_to_subscription_start.is_zero() { + // This is a current or past slot, we subscribe immediately. + self.subscribe_to_short_lived_subnet_immediately(subnet_id, slot + 1)?; + } else { + // This is a future slot, schedule subscribing. + trace!(subnet = ?subnet_id, ?time_to_subscription_start,"Scheduling subnet subscription"); + self.scheduled_short_lived_subscriptions + .insert_at(ExactSubnet { subnet_id, slot }, time_to_subscription_start); + } + + Ok(()) + } + + /* A collection of functions that handle the various timeouts */ + + /// Registers a subnet as subscribed. + /// + /// Checks that the time in which the subscription would end is not in the past. If we are + /// already subscribed, extends the timeout if necessary. If this is a new subscription, we send + /// out the appropriate events. + /// + /// On determinist long lived subnets, this is only used for short lived subscriptions. + fn subscribe_to_short_lived_subnet_immediately( + &mut self, + subnet_id: SubnetId, + end_slot: Slot, + ) -> Result<(), &'static str> { + if self.subscribe_all_subnets { + // Case not handled by this service. + return Ok(()); + } + + let time_to_subscription_end = self + .beacon_chain + .slot_clock + .duration_to_slot(end_slot) + .unwrap_or_default(); + + // First check this is worth doing. + if time_to_subscription_end.is_zero() { + return Err("Time when subscription would end has already passed."); + } + + let subscription_kind = SubscriptionKind::ShortLived; + + // We need to check and add a subscription for the right kind, regardless of the presence + // of the subnet as a subscription of the other kind. This is mainly since long lived + // subscriptions can be removed at any time when a validator goes offline. + + let (subscriptions, already_subscribed_as_other_kind) = ( + &mut self.short_lived_subscriptions, + self.long_lived_subscriptions.contains(&subnet_id), + ); + + match subscriptions.get(&subnet_id) { + Some(current_end_slot) => { + // We are already subscribed. Check if we need to extend the subscription. + if &end_slot > current_end_slot { + trace!( + subnet = ?subnet_id, + prev_end_slot = %current_end_slot, + new_end_slot = %end_slot, + ?subscription_kind, + "Extending subscription to subnet" + ); + subscriptions.insert_at(subnet_id, end_slot, time_to_subscription_end); + } + } + None => { + // This is a new subscription. Add with the corresponding timeout and send the + // notification. + subscriptions.insert_at(subnet_id, end_slot, time_to_subscription_end); + + // Inform of the subscription. + if !already_subscribed_as_other_kind { + debug!( + subnet = ?subnet_id, + %end_slot, + ?subscription_kind, + "Subscribing to subnet" + ); + self.queue_event(SubnetServiceMessage::Subscribe(Subnet::Attestation( + subnet_id, + ))); + } + } + } + + Ok(()) + } + + // Unsubscribes from a subnet that was removed if it does not continue to exist as a + // subscription of the other kind. For long lived subscriptions, it also removes the + // advertisement from our ENR. + fn handle_removed_subnet(&mut self, subnet_id: SubnetId, subscription_kind: SubscriptionKind) { + let exists_in_other_subscriptions = match subscription_kind { + SubscriptionKind::LongLived => self.short_lived_subscriptions.contains_key(&subnet_id), + SubscriptionKind::ShortLived => self.long_lived_subscriptions.contains(&subnet_id), + }; + + if !exists_in_other_subscriptions { + // Subscription no longer exists as short lived or long lived. + debug!( + subnet = ?subnet_id, + ?subscription_kind, + "Unsubscribing from subnet" + ); + self.queue_event(SubnetServiceMessage::Unsubscribe(Subnet::Attestation( + subnet_id, + ))); + } + + if subscription_kind == SubscriptionKind::LongLived { + // Remove from our ENR even if we remain subscribed in other way. + self.queue_event(SubnetServiceMessage::EnrRemove(Subnet::Attestation( + subnet_id, + ))); + } + } +} + +impl Stream for AttestationService { + type Item = SubnetServiceMessage; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + // Update the waker if needed. + if let Some(waker) = &self.waker { + if waker.will_wake(cx.waker()) { + self.waker = Some(cx.waker().clone()); + } + } else { + self.waker = Some(cx.waker().clone()); + } + + // Send out any generated events. + if let Some(event) = self.events.pop_front() { + return Poll::Ready(Some(event)); + } + + // If we aren't subscribed to all subnets, handle the deterministic long-lived subnets + if !self.subscribe_all_subnets { + match self.next_long_lived_subscription_event.as_mut().poll(cx) { + Poll::Ready(_) => { + self.recompute_long_lived_subnets(); + // We re-wake the task as there could be other subscriptions to process + self.waker + .as_ref() + .expect("Waker has been set") + .wake_by_ref(); + } + Poll::Pending => {} + } + } + + // Process scheduled subscriptions that might be ready, since those can extend a soon to + // expire subscription. + match self.scheduled_short_lived_subscriptions.poll_next_unpin(cx) { + Poll::Ready(Some(Ok(ExactSubnet { subnet_id, slot }))) => { + if let Err(e) = + self.subscribe_to_short_lived_subnet_immediately(subnet_id, slot + 1) + { + debug!(subnet = ?subnet_id, err = e,"Failed to subscribe to short lived subnet"); + } + self.waker + .as_ref() + .expect("Waker has been set") + .wake_by_ref(); + } + Poll::Ready(Some(Err(e))) => { + error!( + error = e, + "Failed to check for scheduled subnet subscriptions" + ); + } + Poll::Ready(None) | Poll::Pending => {} + } + + // Finally process any expired subscriptions. + match self.short_lived_subscriptions.poll_next_unpin(cx) { + Poll::Ready(Some(Ok((subnet_id, _end_slot)))) => { + self.handle_removed_subnet(subnet_id, SubscriptionKind::ShortLived); + // We re-wake the task as there could be other subscriptions to process + self.waker + .as_ref() + .expect("Waker has been set") + .wake_by_ref(); + } + Poll::Ready(Some(Err(e))) => { + error!(error = e, "Failed to check for subnet unsubscription times"); + } + Poll::Ready(None) | Poll::Pending => {} + } + + // Poll to remove entries on expiration, no need to act on expiration events. + if let Some(tracked_vals) = self.aggregate_validators_on_subnet.as_mut() { + if let Poll::Ready(Some(Err(e))) = tracked_vals.poll_next_unpin(cx) { + error!( + error = e, + "Failed to check for aggregate validator on subnet expirations" + ); + } + } + + Poll::Pending + } +} diff --git a/beacon_node/network/src/subnet_service/mod.rs b/beacon_node/network/src/subnet_service/mod.rs index de90e222543..5340538e522 100644 --- a/beacon_node/network/src/subnet_service/mod.rs +++ b/beacon_node/network/src/subnet_service/mod.rs @@ -14,8 +14,8 @@ use beacon_chain::{BeaconChain, BeaconChainTypes}; use delay_map::HashSetDelay; use futures::prelude::*; use lighthouse_network::{discv5::enr::NodeId, NetworkConfig, Subnet, SubnetDiscovery}; -use slog::{debug, error, o, warn}; use slot_clock::SlotClock; +use tracing::{debug, error, info, instrument, warn}; use types::{ AttestationData, EthSpec, Slot, SubnetId, SyncCommitteeSubscription, SyncSubnetId, ValidatorSubscription, @@ -107,27 +107,23 @@ pub struct SubnetService { /// Whether this node is a block proposer-only node. proposer_only: bool, - - /// The logger for the attestation service. - log: slog::Logger, } impl SubnetService { /* Public functions */ /// Establish the service based on the passed configuration. - pub fn new( - beacon_chain: Arc>, - node_id: NodeId, - config: &NetworkConfig, - log: &slog::Logger, - ) -> Self { - let log = log.new(o!("service" => "subnet_service")); - + #[instrument(parent = None, + level = "info", + fields(service = "subnet_service"), + name = "subnet_service", + skip_all + )] + pub fn new(beacon_chain: Arc>, node_id: NodeId, config: &NetworkConfig) -> Self { let slot_duration = beacon_chain.slot_clock.slot_duration(); if config.subscribe_all_subnets { - slog::info!(log, "Subscribing to all subnets"); + info!("Subscribing to all subnets"); } // Build the list of known permanent subscriptions, so that we know not to subscribe or @@ -194,7 +190,6 @@ impl SubnetService { discovery_disabled: config.disable_discovery, subscribe_all_subnets: config.subscribe_all_subnets, proposer_only: config.proposer_only, - log, } } @@ -233,6 +228,12 @@ impl SubnetService { /// /// This returns a result simply for the ergonomics of using ?. The result can be /// safely dropped. + #[instrument(parent = None, + level = "info", + fields(service = "subnet_service"), + name = "subnet_service", + skip_all + )] pub fn validator_subscriptions(&mut self, subscriptions: impl Iterator) { // If the node is in a proposer-only state, we ignore all subnet subscriptions. if self.proposer_only { @@ -257,9 +258,9 @@ impl SubnetService { ) { Ok(subnet_id) => Subnet::Attestation(subnet_id), Err(e) => { - warn!(self.log, - "Failed to compute subnet id for validator subscription"; - "error" => ?e, + warn!( + error = ?e, + "Failed to compute subnet id for validator subscription" ); continue; } @@ -287,10 +288,7 @@ impl SubnetService { if subscription.is_aggregator { metrics::inc_counter(&metrics::SUBNET_SUBSCRIPTION_AGGREGATOR_REQUESTS); if let Err(e) = self.subscribe_to_subnet(exact_subnet) { - warn!(self.log, - "Subscription to subnet error"; - "error" => e, - ); + warn!(error = e, "Subscription to subnet error"); } } } @@ -305,10 +303,10 @@ impl SubnetService { ) { Ok(subnet_ids) => subnet_ids, Err(e) => { - warn!(self.log, - "Failed to compute subnet id for sync committee subscription"; - "error" => ?e, - "validator_index" => subscription.validator_index + warn!( + error = ?e, + validator_index = subscription.validator_index, + "Failed to compute subnet id for sync committee subscription" ); continue; } @@ -326,7 +324,11 @@ impl SubnetService { .slot_clock .duration_to_slot(slot_required_until) else { - warn!(self.log, "Subscription to sync subnet error"; "error" => "Unable to determine duration to unsubscription slot", "validator_index" => subscription.validator_index); + warn!( + error = "Unable to determine duration to unsubscription slot", + validator_index = subscription.validator_index, + "Subscription to sync subnet error" + ); continue; }; @@ -337,11 +339,11 @@ impl SubnetService { .now() .unwrap_or(Slot::from(0u64)); warn!( - self.log, - "Sync committee subscription is past expiration"; - "subnet" => ?subnet, - "current_slot" => ?current_slot, - "unsubscribe_slot" => ?slot_required_until, ); + ?subnet, + ?current_slot, + unsubscribe_slot = ?slot_required_until, + "Sync committee subscription is past expiration" + ); continue; } @@ -359,13 +361,19 @@ impl SubnetService { // required subnets. if !self.discovery_disabled { if let Err(e) = self.discover_peers_request(subnets_to_discover.into_iter()) { - warn!(self.log, "Discovery lookup request error"; "error" => e); + warn!(error = e, "Discovery lookup request error"); }; } } /// Checks if we have subscribed aggregate validators for the subnet. If not, checks the gossip /// verification, re-propagates and returns false. + #[instrument(parent = None, + level = "info", + fields(service = "subnet_service"), + name = "subnet_service", + skip_all + )] pub fn should_process_attestation( &self, subnet: Subnet, @@ -390,6 +398,12 @@ impl SubnetService { /// Adds an event to the event queue and notifies that this service is ready to be polled /// again. + #[instrument(parent = None, + level = "info", + fields(service = "subnet_service"), + name = "subnet_service", + skip_all + )] fn queue_event(&mut self, ev: SubnetServiceMessage) { self.events.push_back(ev); if let Some(waker) = &self.waker { @@ -401,6 +415,11 @@ impl SubnetService { /// /// If there is sufficient time, queues a peer discovery request for all the required subnets. // NOTE: Sending early subscriptions results in early searching for peers on subnets. + #[instrument(parent = None, + level = "info", + name = "subnet_service", + skip_all + )] fn discover_peers_request( &mut self, subnets_to_discover: impl Iterator, @@ -432,9 +451,9 @@ impl SubnetService { } else { // We may want to check the global PeerInfo to see estimated timeouts for each // peer before they can be removed. - warn!(self.log, - "Not enough time for a discovery search"; - "subnet_id" => ?subnet, + warn!( + subnet_id = ?subnet, + "Not enough time for a discovery search" ); None } @@ -448,6 +467,12 @@ impl SubnetService { } // Subscribes to the subnet if it should be done immediately, or schedules it if required. + #[instrument(parent = None, + level = "info", + fields(service = "subnet_service"), + name = "subnet_service", + skip_all + )] fn subscribe_to_subnet( &mut self, ExactSubnet { subnet, slot }: ExactSubnet, @@ -500,6 +525,12 @@ impl SubnetService { } /// Adds a subscription event to the sync subnet. + #[instrument(parent = None, + level = "info", + fields(service = "subnet_service"), + name = "subnet_service", + skip_all + )] fn subscribe_to_sync_subnet( &mut self, subnet: Subnet, @@ -529,7 +560,11 @@ impl SubnetService { self.subscriptions .insert_at(subnet, duration_to_unsubscribe); // We are not currently subscribed and have no waiting subscription, create one - debug!(self.log, "Subscribing to subnet"; "subnet" => ?subnet, "until" => ?slot_required_until); + debug!( + ?subnet, + until = ?slot_required_until, + "Subscribing to subnet" + ); self.events .push_back(SubnetServiceMessage::Subscribe(subnet)); @@ -545,6 +580,12 @@ impl SubnetService { /// Checks that the time in which the subscription would end is not in the past. If we are /// already subscribed, extends the timeout if necessary. If this is a new subscription, we send /// out the appropriate events. + #[instrument(parent = None, + level = "info", + fields(service = "subnet_service"), + name = "subnet_service", + skip_all + )] fn subscribe_to_subnet_immediately( &mut self, subnet: Subnet, @@ -588,9 +629,10 @@ impl SubnetService { .insert_at(subnet, time_to_subscription_end); // Inform of the subscription. - debug!(self.log, "Subscribing to subnet"; - "subnet" => ?subnet, - "end_slot" => end_slot, + debug!( + ?subnet, + %end_slot, + "Subscribing to subnet" ); self.queue_event(SubnetServiceMessage::Subscribe(subnet)); } @@ -599,10 +641,16 @@ impl SubnetService { } // Unsubscribes from a subnet that was removed. + #[instrument(parent = None, + level = "info", + fields(service = "subnet_service"), + name = "subnet_service", + skip_all + )] fn handle_removed_subnet(&mut self, subnet: Subnet) { if !self.subscriptions.contains_key(&subnet) { // Subscription no longer exists as short lived subnet - debug!(self.log, "Unsubscribing from subnet"; "subnet" => ?subnet); + debug!(?subnet, "Unsubscribing from subnet"); self.queue_event(SubnetServiceMessage::Unsubscribe(subnet)); // If this is a sync subnet, we need to remove it from our ENR. @@ -616,6 +664,12 @@ impl SubnetService { impl Stream for SubnetService { type Item = SubnetServiceMessage; + #[instrument(parent = None, + level = "info", + fields(service = "subnet_service"), + name = "subnet_service", + skip_all + )] fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { // Update the waker if needed. if let Some(waker) = &self.waker { @@ -639,7 +693,11 @@ impl Stream for SubnetService { // Set the `end_slot` for the subscription to be `duty.slot + 1` so that we unsubscribe // only at the end of the duty slot. if let Err(e) = self.subscribe_to_subnet_immediately(subnet, slot + 1) { - debug!(self.log, "Failed to subscribe to short lived subnet"; "subnet" => ?subnet, "err" => e); + debug!( + subnet = ?subnet, + err = e, + "Failed to subscribe to short lived subnet" + ); } self.waker .as_ref() @@ -647,7 +705,10 @@ impl Stream for SubnetService { .wake_by_ref(); } Poll::Ready(Some(Err(e))) => { - error!(self.log, "Failed to check for scheduled subnet subscriptions"; "error"=> e); + error!( + error = e, + "Failed to check for scheduled subnet subscriptions" + ); } Poll::Ready(None) | Poll::Pending => {} } @@ -663,7 +724,7 @@ impl Stream for SubnetService { .wake_by_ref(); } Poll::Ready(Some(Err(e))) => { - error!(self.log, "Failed to check for subnet unsubscription times"; "error"=> e); + error!(error = e, "Failed to check for subnet unsubscription times"); } Poll::Ready(None) | Poll::Pending => {} } @@ -671,7 +732,10 @@ impl Stream for SubnetService { // Poll to remove entries on expiration, no need to act on expiration events. if let Some(tracked_vals) = self.aggregate_validators_on_subnet.as_mut() { if let Poll::Ready(Some(Err(e))) = tracked_vals.poll_next_unpin(cx) { - error!(self.log, "Failed to check for aggregate validator on subnet expirations"; "error"=> e); + error!( + error = e, + "Failed to check for aggregate validator on subnet expirations" + ); } } diff --git a/beacon_node/network/src/subnet_service/sync_subnets.rs b/beacon_node/network/src/subnet_service/sync_subnets.rs new file mode 100644 index 00000000000..59ec278a958 --- /dev/null +++ b/beacon_node/network/src/subnet_service/sync_subnets.rs @@ -0,0 +1,345 @@ +//! This service keeps track of which sync committee subnet the beacon node should be subscribed to at any +//! given time. It schedules subscriptions to sync committee subnets and requests peer discoveries. + +use std::collections::{hash_map::Entry, HashMap, VecDeque}; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; +use std::time::Duration; + +use futures::prelude::*; +use tracing::{debug, error, trace, warn}; + +use super::SubnetServiceMessage; +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use delay_map::HashSetDelay; +use lighthouse_network::{NetworkConfig, Subnet, SubnetDiscovery}; +use slot_clock::SlotClock; +use types::{Epoch, EthSpec, SyncCommitteeSubscription, SyncSubnetId}; + +use crate::metrics; + +/// The minimum number of slots ahead that we attempt to discover peers for a subscription. If the +/// slot is less than this number, skip the peer discovery process. +/// Subnet discovery query takes at most 30 secs, 2 slots take 24s. +const MIN_PEER_DISCOVERY_SLOT_LOOK_AHEAD: u64 = 2; + +/// A particular subnet at a given slot. +#[derive(PartialEq, Eq, Hash, Clone, Debug)] +pub struct ExactSubnet { + /// The `SyncSubnetId` associated with this subnet. + pub subnet_id: SyncSubnetId, + /// The epoch until which we need to stay subscribed to the subnet. + pub until_epoch: Epoch, +} +pub struct SyncCommitteeService { + /// Queued events to return to the driving service. + events: VecDeque, + + /// A reference to the beacon chain to process received attestations. + pub(crate) beacon_chain: Arc>, + + /// The collection of all currently subscribed subnets. + subscriptions: HashMap, + + /// A collection of timeouts for when to unsubscribe from a subnet. + unsubscriptions: HashSetDelay, + + /// The waker for the current thread. + waker: Option, + + /// The discovery mechanism of lighthouse is disabled. + discovery_disabled: bool, + + /// We are always subscribed to all subnets. + subscribe_all_subnets: bool, + + /// Whether this node is a block proposer-only node. + proposer_only: bool, +} + +impl SyncCommitteeService { + /* Public functions */ + + pub fn new(beacon_chain: Arc>, config: &NetworkConfig) -> Self { + let spec = &beacon_chain.spec; + let epoch_duration_secs = + beacon_chain.slot_clock.slot_duration().as_secs() * T::EthSpec::slots_per_epoch(); + let default_timeout = + epoch_duration_secs.saturating_mul(spec.epochs_per_sync_committee_period.as_u64()); + + SyncCommitteeService { + events: VecDeque::with_capacity(10), + beacon_chain, + subscriptions: HashMap::new(), + unsubscriptions: HashSetDelay::new(Duration::from_secs(default_timeout)), + waker: None, + subscribe_all_subnets: config.subscribe_all_subnets, + discovery_disabled: config.disable_discovery, + proposer_only: config.proposer_only, + } + } + + /// Return count of all currently subscribed subnets. + #[cfg(test)] + pub fn subscription_count(&self) -> usize { + use types::consts::altair::SYNC_COMMITTEE_SUBNET_COUNT; + if self.subscribe_all_subnets { + SYNC_COMMITTEE_SUBNET_COUNT as usize + } else { + self.subscriptions.len() + } + } + + /// Processes a list of sync committee subscriptions. + /// + /// This will: + /// - Search for peers for required subnets. + /// - Request subscriptions required subnets. + /// - Build the timeouts for each of these events. + /// + /// This returns a result simply for the ergonomics of using ?. The result can be + /// safely dropped. + pub fn validator_subscriptions( + &mut self, + subscriptions: Vec, + ) -> Result<(), String> { + // A proposer-only node does not subscribe to any sync-committees + if self.proposer_only { + return Ok(()); + } + + let mut subnets_to_discover = Vec::new(); + for subscription in subscriptions { + metrics::inc_counter(&metrics::SYNC_COMMITTEE_SUBSCRIPTION_REQUESTS); + //NOTE: We assume all subscriptions have been verified before reaching this service + + // Registers the validator with the subnet service. + // This will subscribe to long-lived random subnets if required. + trace!(?subscription, "Sync committee subscription"); + + let subnet_ids = match SyncSubnetId::compute_subnets_for_sync_committee::( + &subscription.sync_committee_indices, + ) { + Ok(subnet_ids) => subnet_ids, + Err(e) => { + warn!( + error = ?e, + validator_index = subscription.validator_index, + "Failed to compute subnet id for sync committee subscription" + ); + continue; + } + }; + + for subnet_id in subnet_ids { + let exact_subnet = ExactSubnet { + subnet_id, + until_epoch: subscription.until_epoch, + }; + subnets_to_discover.push(exact_subnet.clone()); + if let Err(e) = self.subscribe_to_subnet(exact_subnet.clone()) { + warn!( + error = e, + validator_index = subscription.validator_index, + "Subscription to sync subnet error" + ); + } else { + trace!( + ?exact_subnet, + validator_index = subscription.validator_index, + "Subscribed to subnet for sync committee duties" + ); + } + } + } + // If the discovery mechanism isn't disabled, attempt to set up a peer discovery for the + // required subnets. + if !self.discovery_disabled { + if let Err(e) = self.discover_peers_request(subnets_to_discover.iter()) { + warn!(error = e, "Discovery lookup request error"); + }; + } + + // pre-emptively wake the thread to check for new events + if let Some(waker) = &self.waker { + waker.wake_by_ref(); + } + Ok(()) + } + + /* Internal private functions */ + + /// Checks if there are currently queued discovery requests and the time required to make the + /// request. + /// + /// If there is sufficient time, queues a peer discovery request for all the required subnets. + fn discover_peers_request<'a>( + &mut self, + exact_subnets: impl Iterator, + ) -> Result<(), &'static str> { + let current_slot = self + .beacon_chain + .slot_clock + .now() + .ok_or("Could not get the current slot")?; + + let slots_per_epoch = T::EthSpec::slots_per_epoch(); + + let discovery_subnets: Vec = exact_subnets + .filter_map(|exact_subnet| { + let until_slot = exact_subnet.until_epoch.end_slot(slots_per_epoch); + // check if there is enough time to perform a discovery lookup + if until_slot >= current_slot.saturating_add(MIN_PEER_DISCOVERY_SLOT_LOOK_AHEAD) { + // if the slot is more than epoch away, add an event to start looking for peers + // add one slot to ensure we keep the peer for the subscription slot + let min_ttl = self + .beacon_chain + .slot_clock + .duration_to_slot(until_slot + 1) + .map(|duration| std::time::Instant::now() + duration); + Some(SubnetDiscovery { + subnet: Subnet::SyncCommittee(exact_subnet.subnet_id), + min_ttl, + }) + } else { + // We may want to check the global PeerInfo to see estimated timeouts for each + // peer before they can be removed. + warn!( + subnet_id = ?exact_subnet, + "Not enough time for a discovery search" + ); + None + } + }) + .collect(); + + if !discovery_subnets.is_empty() { + self.events + .push_back(SubnetServiceMessage::DiscoverPeers(discovery_subnets)); + } + Ok(()) + } + + /// Adds a subscription event and an associated unsubscription event if required. + fn subscribe_to_subnet(&mut self, exact_subnet: ExactSubnet) -> Result<(), &'static str> { + // Return if we have subscribed to all subnets + if self.subscribe_all_subnets { + return Ok(()); + } + + // Return if we already have a subscription for exact_subnet + if self.subscriptions.get(&exact_subnet.subnet_id) == Some(&exact_subnet.until_epoch) { + return Ok(()); + } + + // Return if we already have subscription set to expire later than the current request. + if let Some(until_epoch) = self.subscriptions.get(&exact_subnet.subnet_id) { + if *until_epoch >= exact_subnet.until_epoch { + return Ok(()); + } + } + + // initialise timing variables + let current_slot = self + .beacon_chain + .slot_clock + .now() + .ok_or("Could not get the current slot")?; + + let slots_per_epoch = T::EthSpec::slots_per_epoch(); + let until_slot = exact_subnet.until_epoch.end_slot(slots_per_epoch); + // Calculate the duration to the unsubscription event. + let expected_end_subscription_duration = if current_slot >= until_slot { + warn!( + %current_slot, + ?exact_subnet, + "Sync committee subscription is past expiration" + ); + return Ok(()); + } else { + let slot_duration = self.beacon_chain.slot_clock.slot_duration(); + + // the duration until we no longer need this subscription. We assume a single slot is + // sufficient. + self.beacon_chain + .slot_clock + .duration_to_slot(until_slot) + .ok_or("Unable to determine duration to unsubscription slot")? + + slot_duration + }; + + if let Entry::Vacant(e) = self.subscriptions.entry(exact_subnet.subnet_id) { + // We are not currently subscribed and have no waiting subscription, create one + debug!(subnet = *exact_subnet.subnet_id, until_epoch = ?exact_subnet.until_epoch, "Subscribing to subnet"); + e.insert(exact_subnet.until_epoch); + self.events + .push_back(SubnetServiceMessage::Subscribe(Subnet::SyncCommittee( + exact_subnet.subnet_id, + ))); + + // add the subnet to the ENR bitfield + self.events + .push_back(SubnetServiceMessage::EnrAdd(Subnet::SyncCommittee( + exact_subnet.subnet_id, + ))); + + // add an unsubscription event to remove ourselves from the subnet once completed + self.unsubscriptions + .insert_at(exact_subnet.subnet_id, expected_end_subscription_duration); + } else { + // We are already subscribed, extend the unsubscription duration + self.unsubscriptions + .update_timeout(&exact_subnet.subnet_id, expected_end_subscription_duration); + } + + Ok(()) + } + + /// A queued unsubscription is ready. + fn handle_unsubscriptions(&mut self, subnet_id: SyncSubnetId) { + debug!(subnet = *subnet_id, "Unsubscribing from subnet"); + + self.subscriptions.remove(&subnet_id); + self.events + .push_back(SubnetServiceMessage::Unsubscribe(Subnet::SyncCommittee( + subnet_id, + ))); + + self.events + .push_back(SubnetServiceMessage::EnrRemove(Subnet::SyncCommittee( + subnet_id, + ))); + } +} + +impl Stream for SyncCommitteeService { + type Item = SubnetServiceMessage; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + // update the waker if needed + if let Some(waker) = &self.waker { + if waker.will_wake(cx.waker()) { + self.waker = Some(cx.waker().clone()); + } + } else { + self.waker = Some(cx.waker().clone()); + } + + // process any un-subscription events + match self.unsubscriptions.poll_next_unpin(cx) { + Poll::Ready(Some(Ok(exact_subnet))) => self.handle_unsubscriptions(exact_subnet), + Poll::Ready(Some(Err(e))) => { + error!(error = e, "Failed to check for subnet unsubscription times"); + } + Poll::Ready(None) | Poll::Pending => {} + } + + // process any generated events + if let Some(event) = self.events.pop_front() { + return Poll::Ready(Some(event)); + } + + Poll::Pending + } +} diff --git a/beacon_node/network/src/subnet_service/tests/mod.rs b/beacon_node/network/src/subnet_service/tests/mod.rs index 0f3343df638..7fdf9047fc3 100644 --- a/beacon_node/network/src/subnet_service/tests/mod.rs +++ b/beacon_node/network/src/subnet_service/tests/mod.rs @@ -7,12 +7,15 @@ 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}; use store::config::StoreConfig; use store::{HotColdDB, MemoryStore}; use task_executor::test_utils::TestRuntime; +use tracing_subscriber::EnvFilter; use types::{ CommitteeIndex, Epoch, EthSpec, Hash256, MainnetEthSpec, Slot, SubnetId, SyncCommitteeSubscription, SyncSubnetId, ValidatorSubscription, @@ -20,6 +23,8 @@ use types::{ const SLOT_DURATION_MILLIS: u64 = 400; +const TEST_LOG_LEVEL: Option<&str> = None; + type TestBeaconChainType = Witness< SystemTimeSlotClock, CachingEth1Backend, @@ -37,11 +42,11 @@ impl TestBeaconChain { pub fn new_with_system_clock() -> Self { let spec = Arc::new(MainnetEthSpec::default_spec()); + get_tracing_subscriber(TEST_LOG_LEVEL); + let keypairs = generate_deterministic_keypairs(1); - let log = logging::test_logger(); - let store = - HotColdDB::open_ephemeral(StoreConfig::default(), spec.clone(), log.clone()).unwrap(); + let store = HotColdDB::open_ephemeral(StoreConfig::default(), spec.clone()).unwrap(); let kzg = get_kzg(&spec); @@ -51,7 +56,6 @@ impl TestBeaconChain { let chain = Arc::new( BeaconChainBuilder::new(MainnetEthSpec, kzg.clone()) - .logger(log.clone()) .custom_spec(spec.clone()) .store(Arc::new(store)) .task_executor(test_runtime.task_executor.clone()) @@ -74,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"), ); @@ -91,10 +96,18 @@ pub fn recent_genesis_time() -> u64 { .as_secs() } +fn get_tracing_subscriber(log_level: Option<&str>) { + if let Some(level) = log_level { + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::try_new(level).unwrap()) + .try_init() + .unwrap(); + } +} + static CHAIN: LazyLock = LazyLock::new(TestBeaconChain::new_with_system_clock); fn get_subnet_service() -> SubnetService { - let log = logging::test_logger(); let config = NetworkConfig::default(); let beacon_chain = CHAIN.chain.clone(); @@ -103,22 +116,19 @@ fn get_subnet_service() -> SubnetService { beacon_chain, lighthouse_network::discv5::enr::NodeId::random(), &config, - &log, ) } -// gets a number of events from the subscription service, or returns none if it times out after a number -// of slots -async fn get_events + Unpin>( +// gets a number of events from the subscription service, or returns none if it times out after a +// specified duration. +async fn get_events_until_timeout + Unpin>( stream: &mut S, num_events: Option, - num_slots_before_timeout: u32, + timeout: Duration, ) -> Vec { let mut events = Vec::new(); - - let timeout = - tokio::time::sleep(Duration::from_millis(SLOT_DURATION_MILLIS) * num_slots_before_timeout); - futures::pin_mut!(timeout); + let sleep = tokio::time::sleep(timeout); + futures::pin_mut!(sleep); loop { tokio::select! { @@ -130,7 +140,7 @@ async fn get_events + Unpin>( } } } - _ = timeout.as_mut() => { + _ = sleep.as_mut() => { break; } @@ -140,6 +150,17 @@ async fn get_events + Unpin>( events } +// gets a number of events from the subscription service, or returns none if it times out after a number +// of slots +async fn get_events_until_num_slots + Unpin>( + stream: &mut S, + num_events: Option, + num_slots_before_timeout: u32, +) -> Vec { + let timeout = Duration::from_millis(SLOT_DURATION_MILLIS) * num_slots_before_timeout; + get_events_until_timeout(stream, num_events, timeout).await +} + mod test { #[cfg(not(windows))] @@ -187,7 +208,7 @@ mod test { // create the attestation service and subscriptions let mut subnet_service = get_subnet_service(); - let _events = get_events(&mut subnet_service, None, 1).await; + let _events = get_events_until_num_slots(&mut subnet_service, None, 1).await; let current_slot = subnet_service .beacon_chain @@ -240,7 +261,7 @@ mod test { ]; // Wait for 1 slot duration to get the unsubscription event - let events = get_events( + let events = get_events_until_num_slots( &mut subnet_service, Some(2), (MainnetEthSpec::slots_per_epoch()) as u32, @@ -272,7 +293,7 @@ mod test { // create the subnet service and subscriptions let mut subnet_service = get_subnet_service(); - let _events = get_events(&mut subnet_service, None, 0).await; + let _events = get_events_until_num_slots(&mut subnet_service, None, 0).await; let current_slot = subnet_service .beacon_chain .slot_clock @@ -321,14 +342,14 @@ mod test { if subnet_service.is_subscribed(&Subnet::Attestation(subnet_id1)) { // If we are permanently subscribed to this subnet, we won't see a subscribe message - let _ = get_events(&mut subnet_service, None, 1).await; + let _ = get_events_until_num_slots(&mut subnet_service, None, 1).await; } else { - let subscription = get_events(&mut subnet_service, None, 1).await; + let subscription = get_events_until_num_slots(&mut subnet_service, None, 1).await; assert_eq!(subscription, [expected]); } // Get event for 1 more slot duration, we should get the unsubscribe event now. - let unsubscribe_event = get_events(&mut subnet_service, None, 1).await; + let unsubscribe_event = get_events_until_num_slots(&mut subnet_service, None, 1).await; // If the long lived and short lived subnets are different, we should get an unsubscription // event. @@ -367,7 +388,7 @@ mod test { // submit the subscriptions subnet_service.validator_subscriptions(subscriptions.into_iter()); - let events = get_events(&mut subnet_service, Some(130), 10).await; + let events = get_events_until_num_slots(&mut subnet_service, Some(130), 10).await; let mut discover_peer_count = 0; let mut enr_add_count = 0; let mut unsubscribe_event_count = 0; @@ -436,7 +457,7 @@ mod test { // submit the subscriptions subnet_service.validator_subscriptions(subscriptions.into_iter()); - let events = get_events(&mut subnet_service, None, 3).await; + let events = get_events_until_num_slots(&mut subnet_service, None, 3).await; let mut discover_peer_count = 0; let mut enr_add_count = 0; let mut unexpected_msg_count = 0; @@ -486,7 +507,7 @@ mod test { // create the attestation service and subscriptions let mut subnet_service = get_subnet_service(); // Remove permanent events - let _events = get_events(&mut subnet_service, None, 0).await; + let _events = get_events_until_num_slots(&mut subnet_service, None, 0).await; let current_slot = subnet_service .beacon_chain @@ -551,7 +572,7 @@ mod test { // Unsubscription event should happen at the end of the slot. // We wait for 2 slots, to avoid timeout issues - let events = get_events(&mut subnet_service, None, 2).await; + let events = get_events_until_num_slots(&mut subnet_service, None, 2).await; let expected_subscription = SubnetServiceMessage::Subscribe(Subnet::Attestation(subnet_id1)); @@ -568,28 +589,26 @@ mod test { println!("{events:?}"); let subscription_slot = current_slot + subscription_slot2 - 1; // one less do to the // advance subscription time - let wait_slots = subnet_service + let wait_duration = subnet_service .beacon_chain .slot_clock .duration_to_slot(subscription_slot) - .unwrap() - .as_millis() as u64 - / SLOT_DURATION_MILLIS; + .unwrap(); - let no_events = dbg!(get_events(&mut subnet_service, None, wait_slots as u32).await); + let no_events = + dbg!(get_events_until_timeout(&mut subnet_service, None, wait_duration).await); assert_eq!(no_events, []); let subscription_end_slot = current_slot + subscription_slot2 + 2; // +1 to get to the end of the duty slot, +1 for the slot to complete - let wait_slots = subnet_service + let wait_duration = subnet_service .beacon_chain .slot_clock .duration_to_slot(subscription_end_slot) - .unwrap() - .as_millis() as u64 - / SLOT_DURATION_MILLIS; + .unwrap(); - let second_subscribe_event = get_events(&mut subnet_service, None, wait_slots as u32).await; + let second_subscribe_event = + get_events_until_timeout(&mut subnet_service, None, wait_duration).await; // If the permanent and short lived subnets are different, we should get an unsubscription event. if !subnet_service.is_subscribed_permanent(&Subnet::Attestation(subnet_id1)) { assert_eq!( @@ -603,28 +622,26 @@ mod test { let subscription_slot = current_slot + subscription_slot3 - 1; - let wait_slots = subnet_service + let wait_duration = subnet_service .beacon_chain .slot_clock .duration_to_slot(subscription_slot) - .unwrap() - .as_millis() as u64 - / SLOT_DURATION_MILLIS; + .unwrap(); - let no_events = dbg!(get_events(&mut subnet_service, None, wait_slots as u32).await); + let no_events = + dbg!(get_events_until_timeout(&mut subnet_service, None, wait_duration).await); assert_eq!(no_events, []); let subscription_end_slot = current_slot + subscription_slot3 + 2; // +1 to get to the end of the duty slot, +1 for the slot to complete - let wait_slots = subnet_service + let wait_duration = subnet_service .beacon_chain .slot_clock .duration_to_slot(subscription_end_slot) - .unwrap() - .as_millis() as u64 - / SLOT_DURATION_MILLIS; + .unwrap(); - let third_subscribe_event = get_events(&mut subnet_service, None, wait_slots as u32).await; + let third_subscribe_event = + get_events_until_timeout(&mut subnet_service, None, wait_duration).await; if !subnet_service.is_subscribed_permanent(&Subnet::Attestation(subnet_id1)) { assert_eq!( @@ -643,7 +660,7 @@ mod test { // create the attestation service and subscriptions let mut subnet_service = get_subnet_service(); - let _events = get_events(&mut subnet_service, None, 0).await; + let _events = get_events_until_num_slots(&mut subnet_service, None, 0).await; let subscriptions = std::iter::once(Subscription::SyncCommittee(SyncCommitteeSubscription { @@ -664,7 +681,7 @@ mod test { let subnet_id = subnet_ids.iter().next().unwrap(); // Note: the unsubscription event takes 2 epochs (8 * 2 * 0.4 secs = 3.2 secs) - let events = get_events( + let events = get_events_until_num_slots( &mut subnet_service, Some(5), (MainnetEthSpec::slots_per_epoch() * 3) as u32, // Have some buffer time before getting 5 events @@ -700,7 +717,7 @@ mod test { // create the attestation service and subscriptions let mut subnet_service = get_subnet_service(); // Get the initial events from permanent subnet subscriptions - let _events = get_events(&mut subnet_service, None, 1).await; + let _events = get_events_until_num_slots(&mut subnet_service, None, 1).await; let subscriptions = std::iter::once(Subscription::SyncCommittee(SyncCommitteeSubscription { @@ -713,7 +730,7 @@ mod test { subnet_service.validator_subscriptions(subscriptions); // Get all immediate events (won't include unsubscriptions) - let events = get_events(&mut subnet_service, None, 1).await; + let events = get_events_until_num_slots(&mut subnet_service, None, 1).await; matches::assert_matches!( events[..], [ @@ -743,7 +760,7 @@ mod test { subnet_service.validator_subscriptions(subscriptions.into_iter()); // Get all immediate events (won't include unsubscriptions) - let events = get_events(&mut subnet_service, None, 1).await; + let events = get_events_until_num_slots(&mut subnet_service, None, 1).await; matches::assert_matches!(events[..], [SubnetServiceMessage::DiscoverPeers(_),]); // Should be unsubscribed at the end. diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index 4220f85fc3e..fcef06271f0 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -10,8 +10,9 @@ use crate::network_beacon_processor::ChainSegmentProcessId; use crate::sync::manager::BatchProcessResult; -use crate::sync::network_context::RangeRequestId; -use crate::sync::network_context::SyncNetworkContext; +use crate::sync::network_context::{ + RangeRequestId, RpcRequestSendError, RpcResponseError, SyncNetworkContext, +}; use crate::sync::range_sync::{ BatchConfig, BatchId, BatchInfo, BatchOperationOutcome, BatchProcessingResult, BatchState, }; @@ -20,13 +21,13 @@ use beacon_chain::{BeaconChain, BeaconChainTypes}; use lighthouse_network::service::api_types::Id; use lighthouse_network::types::{BackFillState, NetworkGlobals}; use lighthouse_network::{PeerAction, PeerId}; -use rand::seq::SliceRandom; -use slog::{crit, debug, error, info, warn}; +use logging::crit; use std::collections::{ btree_map::{BTreeMap, Entry}, - HashMap, HashSet, + HashSet, }; use std::sync::Arc; +use tracing::{debug, error, info, instrument, warn}; use types::{Epoch, EthSpec}; /// Blocks are downloaded in batches from peers. This constant specifies how many epochs worth of @@ -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, @@ -146,16 +144,17 @@ pub struct BackFillSync { /// Reference to the network globals in order to obtain valid peers to backfill blocks from /// (i.e synced peers). network_globals: Arc>, - - /// A logger for backfill sync. - log: slog::Logger, } impl BackFillSync { + #[instrument(parent = None, + level = "info", + name = "backfill_sync", + skip_all + )] pub fn new( beacon_chain: Arc>, network_globals: Arc>, - log: slog::Logger, ) -> Self { // Determine if backfill is enabled or not. // If, for some reason a backfill has already been completed (or we've used a trusted @@ -175,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, @@ -186,7 +184,6 @@ impl BackFillSync { participating_peers: HashSet::new(), restart_failed_sync: false, beacon_chain, - log, }; // Update the global network state with the current backfill state. @@ -195,9 +192,15 @@ impl BackFillSync { } /// Pauses the backfill sync if it's currently syncing. + #[instrument(parent = None, + level = "info", + fields(service = "backfill_sync"), + name = "backfill_sync", + skip_all + )] pub fn pause(&mut self) { if let BackFillState::Syncing = self.state() { - debug!(self.log, "Backfill sync paused"; "processed_epochs" => self.validated_batches, "to_be_processed" => self.current_start); + debug!(processed_epochs = %self.validated_batches, to_be_processed = %self.current_start,"Backfill sync paused"); self.set_state(BackFillState::Paused); } } @@ -206,6 +209,12 @@ impl BackFillSync { /// /// If resuming is successful, reports back the current syncing metrics. #[must_use = "A failure here indicates the backfill sync has failed and the global sync state should be updated"] + #[instrument(parent = None, + level = "info", + fields(service = "backfill_sync"), + name = "backfill_sync", + skip_all + )] pub fn start( &mut self, network: &mut SyncNetworkContext, @@ -222,7 +231,7 @@ impl BackFillSync { .is_some() { // If there are peers to resume with, begin the resume. - debug!(self.log, "Resuming backfill sync"; "start_epoch" => self.current_start, "awaiting_batches" => self.batches.len(), "processing_target" => self.processing_target); + debug!(start_epoch = ?self.current_start, awaiting_batches = self.batches.len(), processing_target = ?self.processing_target, "Resuming backfill sync"); self.set_state(BackFillState::Syncing); // Resume any previously failed batches. self.resume_batches(network)?; @@ -251,14 +260,14 @@ impl BackFillSync { // This infallible match exists to force us to update this code if a future // refactor of `ResetEpochError` adds a variant. let ResetEpochError::SyncCompleted = e; - error!(self.log, "Backfill sync completed whilst in failed status"); + error!("Backfill sync completed whilst in failed status"); self.set_state(BackFillState::Completed); return Err(BackFillError::InvalidSyncState(String::from( "chain completed", ))); } - debug!(self.log, "Resuming a failed backfill sync"; "start_epoch" => self.current_start); + debug!(start_epoch = %self.current_start, "Resuming a failed backfill sync"); // begin requesting blocks from the peer pool, until all peers are exhausted. self.request_batches(network)?; @@ -281,6 +290,12 @@ impl BackFillSync { /// A fully synced peer has joined us. /// If we are in a failed state, update a local variable to indicate we are able to restart /// the failed sync on the next attempt. + #[instrument(parent = None, + level = "info", + fields(service = "backfill_sync"), + name = "backfill_sync", + skip_all + )] pub fn fully_synced_peer_joined(&mut self) { if matches!(self.state(), BackFillState::Failed) { self.restart_failed_sync = true; @@ -289,48 +304,18 @@ impl BackFillSync { /// A peer has disconnected. /// If the peer has active batches, those are considered failed and re-requested. + #[instrument(parent = None, + level = "info", + fields(service = "backfill_sync"), + name = "backfill_sync", + skip_all + )] #[must_use = "A failure here indicates the backfill sync has failed and the global sync state should be updated"] - pub fn peer_disconnected( - &mut self, - peer_id: &PeerId, - 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!( - self.log, - "Batch could not be retried"; - "batch_id" => id, - "error" => "no synced peers" - ); - } - } else { - debug!(self.log, "Batch not found while removing peer"; - "peer" => %peer_id, "batch" => id) - } - } - } - // Remove the peer from the participation list self.participating_peers.remove(peer_id); Ok(()) @@ -339,6 +324,12 @@ impl BackFillSync { /// An RPC error has occurred. /// /// If the batch exists it is re-requested. + #[instrument(parent = None, + level = "info", + fields(service = "backfill_sync"), + name = "backfill_sync", + skip_all + )] #[must_use = "A failure here indicates the backfill sync has failed and the global sync state should be updated"] pub fn inject_error( &mut self, @@ -346,6 +337,7 @@ impl BackFillSync { batch_id: BatchId, peer_id: &PeerId, request_id: Id, + err: RpcResponseError, ) -> Result<(), BackFillError> { if let Some(batch) = self.batches.get_mut(&batch_id) { // A batch could be retried without the peer failing the request (disconnecting/ @@ -356,16 +348,13 @@ impl BackFillSync { if !batch.is_expecting_block(&request_id) { return Ok(()); } - debug!(self.log, "Batch failed"; "batch_epoch" => batch_id, "error" => "rpc_error"); - if let Some(active_requests) = self.active_requests.get_mut(peer_id) { - active_requests.remove(&batch_id); - } - match batch.download_failed(true) { + debug!(batch_epoch = %batch_id, error = ?err, "Batch download failed"); + 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 @@ -378,6 +367,12 @@ impl BackFillSync { /// If this returns an error, the backfill sync has failed and will be restarted once new peers /// join the system. /// The sync manager should update the global sync state on failure. + #[instrument(parent = None, + level = "info", + fields(service = "backfill_sync"), + name = "backfill_sync", + skip_all + )] #[must_use = "A failure here indicates the backfill sync has failed and the global sync state should be updated"] pub fn on_block_response( &mut self, @@ -391,7 +386,7 @@ impl BackFillSync { let Some(batch) = self.batches.get_mut(&batch_id) else { if !matches!(self.state(), BackFillState::Failed) { // A batch might get removed when the chain advances, so this is non fatal. - debug!(self.log, "Received a block for unknown batch"; "epoch" => batch_id); + debug!(epoch = %batch_id, "Received a block for unknown batch"); } return Ok(ProcessResult::Successful); }; @@ -400,23 +395,20 @@ 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; - debug!(self.log, "Completed batch received"; "epoch" => batch_id, "blocks" => received, "awaiting_batches" => awaiting_batches); + debug!( + epoch = %batch_id, + blocks = received, + %awaiting_batches, + "Completed batch received" + ); // pre-emptively request more blocks from peers whilst we process current blocks, self.request_batches(network)?; @@ -432,6 +424,12 @@ impl BackFillSync { /// The syncing process has failed. /// /// This resets past variables, to allow for a fresh start when resuming. + #[instrument(parent = None, + level = "info", + fields(service = "backfill_sync"), + name = "backfill_sync", + skip_all + )] fn fail_sync(&mut self, error: BackFillError) -> Result<(), BackFillError> { // Some errors shouldn't fail the chain. if matches!(error, BackFillError::Paused) { @@ -442,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; @@ -455,7 +452,7 @@ impl BackFillSync { // NOTE: Lets keep validated_batches for posterity // Emit the log here - error!(self.log, "Backfill sync failed"; "error" => ?error); + error!(?error, "Backfill sync failed"); // Return the error, kinda weird pattern, but I want to use // `self.fail_chain(_)?` in other parts of the code. @@ -464,6 +461,12 @@ impl BackFillSync { /// Processes the batch with the given id. /// The batch must exist and be ready for processing + #[instrument(parent = None, + level = "info", + fields(service = "backfill_sync"), + name = "backfill_sync", + skip_all + )] fn process_batch( &mut self, network: &mut SyncNetworkContext, @@ -503,8 +506,12 @@ impl BackFillSync { .beacon_processor() .send_chain_segment(process_id, blocks) { - crit!(self.log, "Failed to send backfill segment to processor."; "msg" => "process_batch", - "error" => %e, "batch" => self.processing_target); + crit!( + msg = "process_batch", + error = %e, + batch = ?self.processing_target, + "Failed to send backfill segment to processor." + ); // This is unlikely to happen but it would stall syncing since the batch now has no // blocks to continue, and the chain is expecting a processing result that won't // arrive. To mitigate this, (fake) fail this processing so that the batch is @@ -518,6 +525,12 @@ impl BackFillSync { /// The block processor has completed processing a batch. This function handles the result /// of the batch processor. /// If an error is returned the BackFill sync has failed. + #[instrument(parent = None, + level = "info", + fields(service = "backfill_sync"), + name = "backfill_sync", + skip_all + )] #[must_use = "A failure here indicates the backfill sync has failed and the global sync state should be updated"] pub fn on_batch_process_result( &mut self, @@ -530,13 +543,15 @@ impl BackFillSync { // result let batch = match &self.current_processing_batch { Some(processing_id) if *processing_id != batch_id => { - debug!(self.log, "Unexpected batch result"; - "batch_epoch" => batch_id, "expected_batch_epoch" => processing_id); + debug!( + batch_epoch = %batch_id.as_u64(), + expected_batch_epoch = processing_id.as_u64(), + "Unexpected batch result" + ); return Ok(ProcessResult::Successful); } None => { - debug!(self.log, "Chain was not expecting a batch result"; - "batch_epoch" => batch_id); + debug!(%batch_id, "Chain was not expecting a batch result"); return Ok(ProcessResult::Successful); } _ => { @@ -558,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"), @@ -566,8 +581,14 @@ impl BackFillSync { return Ok(ProcessResult::Successful); }; - debug!(self.log, "Backfill batch processed"; "result" => ?result, &batch, - "batch_epoch" => batch_id, "peer" => %peer, "client" => %network.client_type(peer)); + debug!( + ?result, + %batch, + batch_epoch = %batch_id, + %peer, + client = %network.client_type(peer), + "Backfill batch processed" + ); match result { BatchProcessResult::Success { @@ -591,7 +612,10 @@ impl BackFillSync { // check if the chain has completed syncing if self.check_completed() { // chain is completed - info!(self.log, "Backfill sync completed"; "blocks_processed" => self.validated_batches * T::EthSpec::slots_per_epoch()); + info!( + blocks_processed = self.validated_batches * T::EthSpec::slots_per_epoch(), + "Backfill sync completed" + ); self.set_state(BackFillState::Completed); Ok(ProcessResult::SyncCompleted) } else { @@ -619,13 +643,14 @@ impl BackFillSync { // repeatedly and are either malicious or faulty. We stop the backfill sync and // report all synced peers that have participated. warn!( - self.log, - "Backfill batch failed to download. Penalizing peers"; - "score_adjustment" => %penalty, - "batch_epoch"=> batch_id + score_adjustment = %penalty, + batch_epoch = %batch_id, + "Backfill batch failed to download. Penalizing peers" ); 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)) @@ -651,13 +676,19 @@ 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) } } } /// Processes the next ready batch. + #[instrument(parent = None, + level = "info", + fields(service = "backfill_sync"), + name = "backfill_sync", + skip_all + )] fn process_completed_batches( &mut self, network: &mut SyncNetworkContext, @@ -692,7 +723,10 @@ impl BackFillSync { BatchState::AwaitingValidation(_) => { // TODO: I don't think this state is possible, log a CRIT just in case. // If this is not observed, add it to the failed state branch above. - crit!(self.log, "Chain encountered a robust batch awaiting validation"; "batch" => self.processing_target); + crit!( + batch = ?self.processing_target, + "Chain encountered a robust batch awaiting validation" + ); self.processing_target -= BACKFILL_EPOCHS_PER_BATCH; if self.to_be_downloaded >= self.processing_target { @@ -718,6 +752,12 @@ impl BackFillSync { /// /// If a previous batch has been validated and it had been re-processed, penalize the original /// peer. + #[instrument(parent = None, + level = "info", + fields(service = "backfill_sync"), + name = "backfill_sync", + skip_all + )] fn advance_chain(&mut self, network: &mut SyncNetworkContext, validating_epoch: Epoch) { // make sure this epoch produces an advancement if validating_epoch >= self.current_start { @@ -745,9 +785,12 @@ impl BackFillSync { // A different peer sent the correct batch, the previous peer did not // We negatively score the original peer. let action = PeerAction::LowToleranceError; - debug!(self.log, "Re-processed batch validated. Scoring original peer"; - "batch_epoch" => id, "score_adjustment" => %action, - "original_peer" => %attempt.peer_id, "new_peer" => %processed_attempt.peer_id + debug!( + batch_epoch = ?id, + score_adjustment = %action, + original_peer = %attempt.peer_id, + new_peer = %processed_attempt.peer_id, + "Re-processed batch validated. Scoring original peer" ); network.report_peer( attempt.peer_id, @@ -758,9 +801,12 @@ impl BackFillSync { // The same peer corrected it's previous mistake. There was an error, so we // negative score the original peer. let action = PeerAction::MidToleranceError; - debug!(self.log, "Re-processed batch validated by the same peer"; - "batch_epoch" => id, "score_adjustment" => %action, - "original_peer" => %attempt.peer_id, "new_peer" => %processed_attempt.peer_id + debug!( + batch_epoch = ?id, + score_adjustment = %action, + original_peer = %attempt.peer_id, + new_peer = %processed_attempt.peer_id, + "Re-processed batch validated by the same peer" ); network.report_peer( attempt.peer_id, @@ -771,21 +817,13 @@ 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!( - self.log, - "batch indicates inconsistent chain state while advancing chain" - ) + crit!("batch indicates inconsistent chain state while advancing chain") } BatchState::AwaitingProcessing(..) => {} BatchState::Processing(_) => { - debug!(self.log, "Advancing chain while processing a batch"; "batch" => id, batch); + debug!(batch = %id, %batch, "Advancing chain while processing a batch"); if let Some(processing_id) = self.current_processing_batch { if id >= processing_id { self.current_processing_batch = None; @@ -803,7 +841,7 @@ impl BackFillSync { // won't have this batch, so we need to request it. self.to_be_downloaded -= BACKFILL_EPOCHS_PER_BATCH; } - debug!(self.log, "Backfill advanced"; "validated_epoch" => validating_epoch, "processing_target" => self.processing_target); + debug!(?validating_epoch, processing_target = ?self.processing_target, "Backfill advanced"); } /// An invalid batch has been received that could not be processed, but that can be retried. @@ -811,6 +849,12 @@ impl BackFillSync { /// These events occur when a peer has successfully responded with blocks, but the blocks we /// have received are incorrect or invalid. This indicates the peer has not performed as /// intended and can result in downvoting a peer. + #[instrument(parent = None, + level = "info", + fields(service = "backfill_sync"), + name = "backfill_sync", + skip_all + )] fn handle_invalid_batch( &mut self, network: &mut SyncNetworkContext, @@ -855,106 +899,82 @@ 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. - 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!(self.log, "Backfill sync paused"; "reason" => "insufficient_synced_peers"); - 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. + #[instrument(parent = None, + level = "info", + fields(service = "backfill_sync"), + name = "backfill_sync", + skip_all + )] fn send_batch( &mut self, network: &mut SyncNetworkContext, batch_id: BatchId, - 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!(self.log, "Requesting batch"; "epoch" => batch_id, &batch); + 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!(self.log, "Could not send batch request"; - "batch_id" => batch_id, "error" => ?e, &batch); - // 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)); - - 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))? + 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)); } - 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) + } } } - } + }, } } @@ -963,6 +983,12 @@ impl BackFillSync { /// When resuming a chain, this function searches for batches that need to be re-downloaded and /// transitions their state to redownload the batch. + #[instrument(parent = None, + level = "info", + fields(service = "backfill_sync"), + name = "backfill_sync", + skip_all + )] fn resume_batches(&mut self, network: &mut SyncNetworkContext) -> Result<(), BackFillError> { let batch_ids_to_retry = self .batches @@ -980,13 +1006,19 @@ 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(()) } /// Attempts to request the next required batches from the peer pool if the chain is syncing. It will exhaust the peer /// pool and left over batches until the batch buffer is reached or all peers are exhausted. + #[instrument(parent = None, + level = "info", + fields(service = "backfill_sync"), + name = "backfill_sync", + skip_all + )] fn request_batches( &mut self, network: &mut SyncNetworkContext, @@ -996,39 +1028,27 @@ 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(()) } /// Creates the next required batch from the chain. If there are no more batches required, /// `false` is returned. + #[instrument(parent = None, + level = "info", + fields(service = "backfill_sync"), + name = "backfill_sync", + skip_all + )] fn include_next_batch(&mut self, network: &mut SyncNetworkContext) -> Option { // don't request batches beyond genesis; if self.last_batch_downloaded { @@ -1090,6 +1110,12 @@ impl BackFillSync { /// /// This errors if the beacon chain indicates that backfill sync has already completed or is /// not required. + #[instrument(parent = None, + level = "info", + fields(service = "backfill_sync"), + name = "backfill_sync", + skip_all + )] fn reset_start_epoch(&mut self) -> Result<(), ResetEpochError> { let anchor_info = self.beacon_chain.store.get_anchor_info(); if anchor_info.block_backfill_complete(self.beacon_chain.genesis_backfill_slot) { @@ -1103,6 +1129,12 @@ impl BackFillSync { } /// Checks with the beacon chain if backfill sync has completed. + #[instrument(parent = None, + level = "info", + fields(service = "backfill_sync"), + name = "backfill_sync", + skip_all + )] fn check_completed(&mut self) -> bool { if self.would_complete(self.current_start) { // Check that the beacon chain agrees @@ -1111,13 +1143,19 @@ impl BackFillSync { if anchor_info.block_backfill_complete(self.beacon_chain.genesis_backfill_slot) { return true; } else { - error!(self.log, "Backfill out of sync with beacon chain"); + error!("Backfill out of sync with beacon chain"); } } false } /// Checks if backfill would complete by syncing to `start_epoch`. + #[instrument(parent = None, + level = "info", + fields(service = "backfill_sync"), + name = "backfill_sync", + skip_all + )] fn would_complete(&self, start_epoch: Epoch) -> bool { start_epoch <= self @@ -1127,10 +1165,22 @@ impl BackFillSync { } /// Updates the global network state indicating the current state of a backfill sync. + #[instrument(parent = None, + level = "info", + fields(service = "backfill_sync"), + name = "backfill_sync", + skip_all + )] fn set_state(&self, state: BackFillState) { *self.network_globals.backfill_state.write() = state; } + #[instrument(parent = None, + level = "info", + fields(service = "backfill_sync"), + name = "backfill_sync", + skip_all + )] fn state(&self) -> BackFillState { self.network_globals.backfill_state.read().clone() } @@ -1141,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 8eefb2d6756..86b6894bac4 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_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index 2172c8dcd8d..8c884f644e1 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -41,11 +41,11 @@ use lighthouse_network::service::api_types::SingleLookupReqId; use lighthouse_network::{PeerAction, PeerId}; use lru_cache::LRUTimeCache; pub use single_block_lookup::{BlobRequestState, BlockRequestState, CustodyRequestState}; -use slog::{debug, error, warn, Logger}; use std::collections::hash_map::Entry; use std::sync::Arc; use std::time::Duration; use store::Hash256; +use tracing::{debug, error, instrument, warn}; use types::{BlobSidecar, DataColumnSidecar, EthSpec, SignedBeaconBlock}; pub mod common; @@ -116,9 +116,6 @@ pub struct BlockLookups { // TODO: Why not index lookups by block_root? single_block_lookups: FnvHashMap>, - - /// The logger for the import manager. - log: Logger, } #[cfg(test)] @@ -130,27 +127,45 @@ use lighthouse_network::service::api_types::Id; pub(crate) type BlockLookupSummary = (Id, Hash256, Option, Vec); impl BlockLookups { - pub fn new(log: Logger) -> Self { + #[instrument(parent = None,level = "info", fields(service = "lookup_sync"), name = "lookup_sync")] + pub fn new() -> Self { Self { failed_chains: LRUTimeCache::new(Duration::from_secs( FAILED_CHAINS_CACHE_EXPIRY_SECONDS, )), single_block_lookups: Default::default(), - log, } } #[cfg(test)] + #[instrument(parent = None, + level = "info", + fields(service = "lookup_sync"), + name = "lookup_sync", + skip_all + )] pub(crate) fn insert_failed_chain(&mut self, block_root: Hash256) { self.failed_chains.insert(block_root); } #[cfg(test)] + #[instrument(parent = None, + level = "info", + fields(service = "lookup_sync"), + name = "lookup_sync", + skip_all + )] pub(crate) fn get_failed_chains(&mut self) -> Vec { self.failed_chains.keys().cloned().collect() } #[cfg(test)] + #[instrument(parent = None, + level = "info", + fields(service = "lookup_sync"), + name = "lookup_sync", + skip_all + )] pub(crate) fn active_single_lookups(&self) -> Vec { self.single_block_lookups .iter() @@ -159,6 +174,12 @@ impl BlockLookups { } /// Returns a vec of all parent lookup chains by tip, in descending slot order (tip first) + #[instrument(parent = None, + level = "info", + fields(service = "lookup_sync"), + name = "lookup_sync", + skip_all + )] pub(crate) fn active_parent_lookups(&self) -> Vec { compute_parent_chains( &self @@ -173,6 +194,12 @@ impl BlockLookups { /// Creates a parent lookup for the block with the given `block_root` and immediately triggers it. /// If a parent lookup exists or is triggered, a current lookup will be created. + #[instrument(parent = None, + level = "info", + fields(service = "lookup_sync"), + name = "lookup_sync", + skip_all + )] pub fn search_child_and_parent( &mut self, block_root: Hash256, @@ -202,6 +229,12 @@ impl BlockLookups { /// Seach a block whose parent root is unknown. /// Returns true if the lookup is created or already exists + #[instrument(parent = None, + level = "info", + fields(service = "lookup_sync"), + name = "lookup_sync", + skip_all + )] pub fn search_unknown_block( &mut self, block_root: Hash256, @@ -217,6 +250,12 @@ impl BlockLookups { /// - `block_root_to_search` is a failed chain /// /// Returns true if the lookup is created or already exists + #[instrument(parent = None, + level = "info", + fields(service = "lookup_sync"), + name = "lookup_sync", + skip_all + )] pub fn search_parent_of_child( &mut self, block_root_to_search: Hash256, @@ -238,7 +277,7 @@ impl BlockLookups { if (block_would_extend_chain || trigger_is_chain_tip) && parent_chain.len() >= PARENT_DEPTH_TOLERANCE { - debug!(self.log, "Parent lookup chain too long"; "block_root" => ?block_root_to_search); + debug!(block_root = ?block_root_to_search, "Parent lookup chain too long"); // Searching for this parent would extend a parent chain over the max // Insert the tip only to failed chains @@ -283,9 +322,10 @@ impl BlockLookups { }); } else { // Should never happen, log error and continue the lookup drop - error!(self.log, "Unable to transition lookup to range sync"; - "error" => "Parent chain tip lookup not found", - "block_root" => ?parent_chain_tip + error!( + error = "Parent chain tip lookup not found", + block_root = ?parent_chain_tip, + "Unable to transition lookup to range sync" ); } @@ -299,9 +339,10 @@ impl BlockLookups { self.drop_lookup_and_children(*lookup_id); } else { // Should never happen - error!(self.log, "Unable to transition lookup to range sync"; - "error" => "Block to drop lookup not found", - "block_root" => ?block_to_drop + error!( + error = "Block to drop lookup not found", + block_root = ?block_to_drop, + "Unable to transition lookup to range sync" ); } @@ -316,6 +357,12 @@ impl BlockLookups { /// Searches for a single block hash. If the blocks parent is unknown, a chain of blocks is /// constructed. /// Returns true if the lookup is created or already exists + #[instrument(parent = None, + level = "info", + fields(service = "lookup_sync"), + name = "lookup_sync", + skip_all + )] fn new_current_lookup( &mut self, block_root: Hash256, @@ -326,7 +373,7 @@ impl BlockLookups { ) -> bool { // If this block or it's parent is part of a known failed chain, ignore it. if self.failed_chains.contains(&block_root) { - debug!(self.log, "Block is from a past failed chain. Dropping"; "block_root" => ?block_root); + debug!(?block_root, "Block is from a past failed chain. Dropping"); for peer_id in peers { cx.report_peer(*peer_id, PeerAction::MidToleranceError, "failed_chain"); } @@ -343,12 +390,15 @@ impl BlockLookups { let component_type = block_component.get_type(); let imported = lookup.add_child_components(block_component); if !imported { - debug!(self.log, "Lookup child component ignored"; "block_root" => ?block_root, "type" => component_type); + debug!( + ?block_root, + component_type, "Lookup child component ignored" + ); } } if let Err(e) = self.add_peers_to_lookup_and_ancestors(lookup_id, peers, cx) { - warn!(self.log, "Error adding peers to ancestor lookup"; "error" => ?e); + warn!(error = ?e, "Error adding peers to ancestor lookup"); } return true; @@ -361,7 +411,7 @@ impl BlockLookups { .iter() .any(|(_, lookup)| lookup.is_for_block(awaiting_parent)) { - warn!(self.log, "Ignoring child lookup parent lookup not found"; "block_root" => ?awaiting_parent); + warn!(block_root = ?awaiting_parent, "Ignoring child lookup parent lookup not found"); return false; } } @@ -369,7 +419,7 @@ impl BlockLookups { // Lookups contain untrusted data, bound the total count of lookups hold in memory to reduce // the risk of OOM in case of bugs of malicious activity. if self.single_block_lookups.len() > MAX_LOOKUPS { - warn!(self.log, "Dropping lookup reached max"; "block_root" => ?block_root); + warn!(?block_root, "Dropping lookup reached max"); return false; } @@ -387,18 +437,19 @@ impl BlockLookups { Entry::Vacant(entry) => entry.insert(lookup), Entry::Occupied(_) => { // Should never happen - warn!(self.log, "Lookup exists with same id"; "id" => id); + warn!(id, "Lookup exists with same id"); return false; } }; debug!( - self.log, - "Created block lookup"; - "peer_ids" => ?peers, - "block_root" => ?block_root, - "awaiting_parent" => awaiting_parent.map(|root| root.to_string()).unwrap_or("none".to_owned()), - "id" => lookup.id, + ?peers, + ?block_root, + awaiting_parent = awaiting_parent + .map(|root| root.to_string()) + .unwrap_or("none".to_owned()), + id = lookup.id, + "Created block lookup" ); metrics::inc_counter(&metrics::SYNC_LOOKUP_CREATED); @@ -414,6 +465,12 @@ impl BlockLookups { /* Lookup responses */ /// Process a block or blob response received from a single lookup request. + #[instrument(parent = None, + level = "info", + fields(service = "lookup_sync"), + name = "lookup_sync", + skip_all + )] pub fn on_download_response>( &mut self, id: SingleLookupReqId, @@ -437,7 +494,7 @@ impl BlockLookups { let Some(lookup) = self.single_block_lookups.get_mut(&id.lookup_id) else { // We don't have the ability to cancel in-flight RPC requests. So this can happen // if we started this RPC request, and later saw the block/blobs via gossip. - debug!(self.log, "Block returned for single block lookup not present"; "id" => ?id); + debug!(?id, "Block returned for single block lookup not present"); return Err(LookupRequestError::UnknownLookup); }; @@ -448,12 +505,12 @@ impl BlockLookups { match response { Ok((response, peer_group, seen_timestamp)) => { - debug!(self.log, - "Received lookup download success"; - "block_root" => ?block_root, - "id" => ?id, - "peer_group" => ?peer_group, - "response_type" => ?response_type, + debug!( + ?block_root, + ?id, + ?peer_group, + ?response_type, + "Received lookup download success" ); // Here we could check if response extends a parent chain beyond its max length. @@ -479,14 +536,14 @@ impl BlockLookups { // continue_request will send for processing as the request state is AwaitingProcessing } Err(e) => { - // TODO(das): is it okay to not log the peer source of request failures? Then we - // should log individual requests failures in the SyncNetworkContext - debug!(self.log, - "Received lookup download failure"; - "block_root" => ?block_root, - "id" => ?id, - "response_type" => ?response_type, - "error" => ?e, + // No need to log peer source here. When sending a DataColumnsByRoot request we log + // the peer and the request ID which is linked to this `id` value here. + debug!( + ?block_root, + ?id, + ?response_type, + error = ?e, + "Received lookup download failure" ); request_state.on_download_failure(id.req_id)?; @@ -499,6 +556,12 @@ impl BlockLookups { /* Error responses */ + #[instrument(parent = None, + level = "info", + fields(service = "lookup_sync"), + name = "lookup_sync", + skip_all + )] pub fn peer_disconnected(&mut self, peer_id: &PeerId) { for (_, lookup) in self.single_block_lookups.iter_mut() { lookup.remove_peer(peer_id); @@ -507,6 +570,12 @@ impl BlockLookups { /* Processing responses */ + #[instrument(parent = None, + level = "info", + fields(service = "lookup_sync"), + name = "lookup_sync", + skip_all + )] pub fn on_processing_result( &mut self, process_type: BlockProcessType, @@ -527,6 +596,12 @@ impl BlockLookups { self.on_lookup_result(process_type.id(), lookup_result, "processing_result", cx); } + #[instrument(parent = None, + level = "info", + fields(service = "lookup_sync"), + name = "lookup_sync", + skip_all + )] pub fn on_processing_result_inner>( &mut self, lookup_id: SingleLookupId, @@ -534,7 +609,7 @@ impl BlockLookups { cx: &mut SyncNetworkContext, ) -> Result { let Some(lookup) = self.single_block_lookups.get_mut(&lookup_id) else { - debug!(self.log, "Unknown single block lookup"; "id" => lookup_id); + debug!(id = lookup_id, "Unknown single block lookup"); return Err(LookupRequestError::UnknownLookup); }; @@ -544,12 +619,11 @@ impl BlockLookups { .get_state_mut(); debug!( - self.log, - "Received lookup processing result"; - "component" => ?R::response_type(), - "block_root" => ?block_root, - "id" => lookup_id, - "result" => ?result, + component = ?R::response_type(), + ?block_root, + id = lookup_id, + ?result, + "Received lookup processing result" ); let action = match result { @@ -581,20 +655,15 @@ impl BlockLookups { BlockProcessingResult::Err(BlockError::DuplicateImportStatusUnknown(..)) => { // This is unreachable because RPC blocks do not undergo gossip verification, and // this error can *only* come from gossip verification. - error!( - self.log, - "Single block lookup hit unreachable condition"; - "block_root" => ?block_root - ); + error!(?block_root, "Single block lookup hit unreachable condition"); Action::Drop } BlockProcessingResult::Ignored => { // Beacon processor signalled to ignore the block processing result. // This implies that the cpu is overloaded. Drop the request. warn!( - self.log, - "Lookup component processing ignored, cpu might be overloaded"; - "component" => ?R::response_type(), + component = ?R::response_type(), + "Lookup component processing ignored, cpu might be overloaded" ); Action::Drop } @@ -602,7 +671,7 @@ impl BlockLookups { match e { BlockError::BeaconChainError(e) => { // Internal error - error!(self.log, "Beacon chain error processing lookup component"; "block_root" => %block_root, "error" => ?e); + error!(%block_root, error = ?e, "Beacon chain error processing lookup component"); Action::Drop } BlockError::ParentUnknown { parent_root, .. } => { @@ -618,10 +687,9 @@ impl BlockLookups { // These errors indicate that the execution layer is offline // and failed to validate the execution payload. Do not downscore peer. debug!( - self.log, - "Single block lookup failed. Execution layer is offline / unsynced / misconfigured"; - "block_root" => ?block_root, - "error" => ?e + ?block_root, + error = ?e, + "Single block lookup failed. Execution layer is offline / unsynced / misconfigured" ); Action::Drop } @@ -629,7 +697,7 @@ impl BlockLookups { if e.category() == AvailabilityCheckErrorCategory::Internal => { // There errors indicate internal problems and should not downscore the peer - warn!(self.log, "Internal availability check failure"; "block_root" => ?block_root, "error" => ?e); + warn!(?block_root, error = ?e, "Internal availability check failure"); // Here we choose *not* to call `on_processing_failure` because this could result in a bad // lookup state transition. This error invalidates both blob and block requests, and we don't know the @@ -638,7 +706,12 @@ impl BlockLookups { Action::Drop } other => { - debug!(self.log, "Invalid lookup component"; "block_root" => ?block_root, "component" => ?R::response_type(), "error" => ?other); + debug!( + ?block_root, + component = ?R::response_type(), + error = ?other, + "Invalid lookup component" + ); let peer_group = request_state.on_processing_failure()?; let peers_to_penalize: Vec<_> = match other { // Note: currenlty only InvalidColumn errors have index granularity, @@ -685,7 +758,12 @@ impl BlockLookups { Action::ParentUnknown { parent_root } => { let peers = lookup.all_peers(); lookup.set_awaiting_parent(parent_root); - debug!(self.log, "Marking lookup as awaiting parent"; "id" => lookup.id, "block_root" => ?block_root, "parent_root" => ?parent_root); + debug!( + id = lookup.id, + ?block_root, + ?parent_root, + "Marking lookup as awaiting parent" + ); self.search_parent_of_child(parent_root, block_root, &peers, cx); Ok(LookupResult::Pending) } @@ -700,6 +778,12 @@ impl BlockLookups { } } + #[instrument(parent = None, + level = "info", + fields(service = "lookup_sync"), + name = "lookup_sync", + skip_all + )] pub fn on_external_processing_result( &mut self, block_root: Hash256, @@ -725,13 +809,24 @@ impl BlockLookups { } /// Makes progress on the immediate children of `block_root` + #[instrument(parent = None, + level = "info", + fields(service = "lookup_sync"), + name = "lookup_sync", + skip_all + )] pub fn continue_child_lookups(&mut self, block_root: Hash256, cx: &mut SyncNetworkContext) { let mut lookup_results = vec![]; // < need to buffer lookup results to not re-borrow &mut self for (id, lookup) in self.single_block_lookups.iter_mut() { if lookup.awaiting_parent() == Some(block_root) { lookup.resolve_awaiting_parent(); - debug!(self.log, "Continuing child lookup"; "parent_root" => ?block_root, "id" => id, "block_root" => ?lookup.block_root()); + debug!( + parent_root = ?block_root, + id, + block_root = ?lookup.block_root(), + "Continuing child lookup" + ); let result = lookup.continue_requests(cx); lookup_results.push((*id, result)); } @@ -745,12 +840,19 @@ impl BlockLookups { /// Drops `dropped_id` lookup and all its children recursively. Lookups awaiting a parent need /// the parent to make progress to resolve, therefore we must drop them if the parent is /// dropped. + #[instrument(parent = None, + level = "info", + fields(service = "lookup_sync"), + name = "lookup_sync", + skip_all + )] pub fn drop_lookup_and_children(&mut self, dropped_id: SingleLookupId) { if let Some(dropped_lookup) = self.single_block_lookups.remove(&dropped_id) { - debug!(self.log, "Dropping lookup"; - "id" => ?dropped_id, - "block_root" => ?dropped_lookup.block_root(), - "awaiting_parent" => ?dropped_lookup.awaiting_parent(), + debug!( + id = ?dropped_id, + block_root = ?dropped_lookup.block_root(), + awaiting_parent = ?dropped_lookup.awaiting_parent(), + "Dropping lookup" ); let child_lookups = self @@ -768,6 +870,12 @@ impl BlockLookups { /// Common handler a lookup request error, drop it and update metrics /// Returns true if the lookup is created or already exists + #[instrument(parent = None, + level = "info", + fields(service = "lookup_sync"), + name = "lookup_sync", + skip_all + )] fn on_lookup_result( &mut self, id: SingleLookupId, @@ -779,13 +887,13 @@ impl BlockLookups { Ok(LookupResult::Pending) => true, // no action Ok(LookupResult::Completed) => { if let Some(lookup) = self.single_block_lookups.remove(&id) { - debug!(self.log, "Dropping completed lookup"; "block" => ?lookup.block_root(), "id" => id); + debug!(block = ?lookup.block_root(), id, "Dropping completed lookup"); metrics::inc_counter(&metrics::SYNC_LOOKUP_COMPLETED); // Block imported, continue the requests of pending child blocks self.continue_child_lookups(lookup.block_root(), cx); self.update_metrics(); } else { - debug!(self.log, "Attempting to drop non-existent lookup"; "id" => id); + debug!(id, "Attempting to drop non-existent lookup"); } false } @@ -793,7 +901,7 @@ impl BlockLookups { // update metrics because the lookup does not exist. Err(LookupRequestError::UnknownLookup) => false, Err(error) => { - debug!(self.log, "Dropping lookup on request error"; "id" => id, "source" => source, "error" => ?error); + debug!(id, source, ?error, "Dropping lookup on request error"); metrics::inc_counter_vec(&metrics::SYNC_LOOKUP_DROPPED, &[error.into()]); self.drop_lookup_and_children(id); self.update_metrics(); @@ -805,12 +913,24 @@ impl BlockLookups { /* Helper functions */ /// Drops all the single block requests and returns how many requests were dropped. + #[instrument(parent = None, + level = "info", + fields(service = "lookup_sync"), + name = "lookup_sync", + skip_all + )] pub fn drop_single_block_requests(&mut self) -> usize { let requests_to_drop = self.single_block_lookups.len(); self.single_block_lookups.clear(); requests_to_drop } + #[instrument(parent = None, + level = "info", + fields(service = "lookup_sync"), + name = "lookup_sync", + skip_all + )] pub fn update_metrics(&self) { metrics::set_gauge( &metrics::SYNC_SINGLE_BLOCK_LOOKUPS, @@ -819,6 +939,12 @@ impl BlockLookups { } /// Perform some prune operations on lookups on some interval + #[instrument(parent = None, + level = "info", + fields(service = "lookup_sync"), + name = "lookup_sync", + skip_all + )] pub fn prune_lookups(&mut self) { self.drop_lookups_without_peers(); self.drop_stuck_lookups(); @@ -842,6 +968,12 @@ impl BlockLookups { /// /// Instead there's no negative for keeping lookups with no peers around for some time. If we /// regularly prune them, it should not be a memory concern (TODO: maybe yes!). + #[instrument(parent = None, + level = "info", + fields(service = "lookup_sync"), + name = "lookup_sync", + skip_all + )] fn drop_lookups_without_peers(&mut self) { for (lookup_id, block_root) in self .single_block_lookups @@ -857,9 +989,10 @@ impl BlockLookups { .map(|lookup| (lookup.id, lookup.block_root())) .collect::>() { - debug!(self.log, "Dropping lookup with no peers"; - "id" => lookup_id, - "block_root" => ?block_root + debug!( + id = lookup_id, + %block_root, + "Dropping lookup with no peers" ); self.drop_lookup_and_children(lookup_id); } @@ -878,6 +1011,12 @@ impl BlockLookups { /// /// - One single clear warn level log per stuck incident /// - If the original bug is sporadic, it reduces the time a node is stuck from forever to 15 min + #[instrument(parent = None, + level = "info", + fields(service = "lookup_sync"), + name = "lookup_sync", + skip_all + )] fn drop_stuck_lookups(&mut self) { // While loop to find and drop all disjoint trees of potentially stuck lookups. while let Some(stuck_lookup) = self.single_block_lookups.values().find(|lookup| { @@ -886,7 +1025,7 @@ impl BlockLookups { let ancestor_stuck_lookup = match self.find_oldest_ancestor_lookup(stuck_lookup) { Ok(lookup) => lookup, Err(e) => { - warn!(self.log, "Error finding oldest ancestor lookup"; "error" => ?e); + warn!(error = ?e,"Error finding oldest ancestor lookup"); // Default to dropping the lookup that exceeds the max duration so at least // eventually sync should be unstuck stuck_lookup @@ -894,16 +1033,18 @@ impl BlockLookups { }; if stuck_lookup.id == ancestor_stuck_lookup.id { - warn!(self.log, "Notify the devs a sync lookup is stuck"; - "block_root" => ?stuck_lookup.block_root(), - "lookup" => ?stuck_lookup, + warn!( + block_root = ?stuck_lookup.block_root(), + lookup = ?stuck_lookup, + "Notify the devs a sync lookup is stuck" ); } else { - warn!(self.log, "Notify the devs a sync lookup is stuck"; - "block_root" => ?stuck_lookup.block_root(), - "lookup" => ?stuck_lookup, - "ancestor_block_root" => ?ancestor_stuck_lookup.block_root(), - "ancestor_lookup" => ?ancestor_stuck_lookup, + warn!( + block_root = ?stuck_lookup.block_root(), + lookup = ?stuck_lookup, + ancestor_block_root = ?ancestor_stuck_lookup.block_root(), + ancestor_lookup = ?ancestor_stuck_lookup, + "Notify the devs a sync lookup is stuck" ); } @@ -913,6 +1054,12 @@ impl BlockLookups { } /// Recursively find the oldest ancestor lookup of another lookup + #[instrument(parent = None, + level = "info", + fields(service = "lookup_sync"), + name = "lookup_sync", + skip_all + )] fn find_oldest_ancestor_lookup<'a>( &'a self, lookup: &'a SingleBlockLookup, @@ -937,6 +1084,12 @@ impl BlockLookups { /// Adds peers to a lookup and its ancestors recursively. /// Note: Takes a `lookup_id` as argument to allow recursion on mutable lookups, without having /// to duplicate the code to add peers to a lookup + #[instrument(parent = None, + level = "info", + fields(service = "lookup_sync"), + name = "lookup_sync", + skip_all + )] fn add_peers_to_lookup_and_ancestors( &mut self, lookup_id: SingleLookupId, @@ -952,9 +1105,10 @@ impl BlockLookups { for peer in peers { if lookup.add_peer(*peer) { added_some_peer = true; - debug!(self.log, "Adding peer to existing single block lookup"; - "block_root" => ?lookup.block_root(), - "peer" => ?peer + debug!( + block_root = ?lookup.block_root(), + ?peer, + "Adding peer to existing single block lookup" ); } } diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index 6c8a8eab633..99428b0c805 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -1,89 +1,156 @@ use beacon_chain::{ block_verification_types::RpcBlock, data_column_verification::CustodyDataColumn, get_block_root, }; -use std::{ - collections::{HashMap, VecDeque}, - sync::Arc, +use lighthouse_network::service::api_types::{ + BlobsByRangeRequestId, BlocksByRangeRequestId, DataColumnsByRangeRequestId, }; +use std::{collections::HashMap, sync::Arc}; use types::{ - BlobSidecar, ChainSpec, ColumnIndex, DataColumnSidecar, EthSpec, Hash256, RuntimeVariableList, - SignedBeaconBlock, + BlobSidecar, ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, + Hash256, RuntimeVariableList, SignedBeaconBlock, }; -#[derive(Debug)] pub struct RangeBlockComponentsRequest { /// Blocks we have received awaiting for their corresponding sidecar. - blocks: VecDeque>>, + blocks_request: ByRangeRequest>>>, /// Sidecars we have received awaiting for their corresponding block. - blobs: VecDeque>>, - data_columns: VecDeque>>, - /// Whether the individual RPC request for blocks is finished or not. - is_blocks_stream_terminated: bool, - /// Whether the individual RPC request for sidecars is finished or not. - is_sidecars_stream_terminated: bool, - custody_columns_streams_terminated: usize, - /// Used to determine if this accumulator should wait for a sidecars stream termination - expects_blobs: bool, - expects_custody_columns: Option>, - /// Used to determine if the number of data columns stream termination this accumulator should - /// wait for. This may be less than the number of `expects_custody_columns` due to request batching. - num_custody_column_requests: Option, + block_data_request: RangeBlockDataRequest, +} + +enum ByRangeRequest { + Active(I), + Complete(T), +} + +enum RangeBlockDataRequest { + NoData, + Blobs(ByRangeRequest>>>), + DataColumns { + requests: HashMap< + DataColumnsByRangeRequestId, + ByRangeRequest>, + >, + expected_custody_columns: Vec, + }, } impl RangeBlockComponentsRequest { pub fn new( - expects_blobs: bool, - expects_custody_columns: Option>, - num_custody_column_requests: Option, + blocks_req_id: BlocksByRangeRequestId, + blobs_req_id: Option, + data_columns: Option<(Vec, Vec)>, ) -> Self { + let block_data_request = if let Some(blobs_req_id) = blobs_req_id { + RangeBlockDataRequest::Blobs(ByRangeRequest::Active(blobs_req_id)) + } else if let Some((requests, expected_custody_columns)) = data_columns { + RangeBlockDataRequest::DataColumns { + requests: requests + .into_iter() + .map(|id| (id, ByRangeRequest::Active(id))) + .collect(), + expected_custody_columns, + } + } else { + RangeBlockDataRequest::NoData + }; + Self { - blocks: <_>::default(), - blobs: <_>::default(), - data_columns: <_>::default(), - is_blocks_stream_terminated: false, - is_sidecars_stream_terminated: false, - custody_columns_streams_terminated: 0, - expects_blobs, - expects_custody_columns, - num_custody_column_requests, + blocks_request: ByRangeRequest::Active(blocks_req_id), + block_data_request, } } - pub fn add_blocks(&mut self, blocks: Vec>>) { - for block in blocks { - self.blocks.push_back(block); - } - self.is_blocks_stream_terminated = true; + pub fn add_blocks( + &mut self, + req_id: BlocksByRangeRequestId, + blocks: Vec>>, + ) -> Result<(), String> { + self.blocks_request.finish(req_id, blocks) } - pub fn add_blobs(&mut self, blobs: Vec>>) { - for blob in blobs { - self.blobs.push_back(blob); + pub fn add_blobs( + &mut self, + req_id: BlobsByRangeRequestId, + blobs: Vec>>, + ) -> Result<(), String> { + match &mut self.block_data_request { + RangeBlockDataRequest::NoData => Err("received blobs but expected no data".to_owned()), + RangeBlockDataRequest::Blobs(ref mut req) => req.finish(req_id, blobs), + RangeBlockDataRequest::DataColumns { .. } => { + Err("received blobs but expected data columns".to_owned()) + } } - self.is_sidecars_stream_terminated = true; } - pub fn add_custody_columns(&mut self, columns: Vec>>) { - for column in columns { - self.data_columns.push_back(column); + pub fn add_custody_columns( + &mut self, + req_id: DataColumnsByRangeRequestId, + columns: Vec>>, + ) -> Result<(), String> { + match &mut self.block_data_request { + RangeBlockDataRequest::NoData => { + Err("received data columns but expected no data".to_owned()) + } + RangeBlockDataRequest::Blobs(_) => { + Err("received data columns but expected blobs".to_owned()) + } + RangeBlockDataRequest::DataColumns { + ref mut requests, .. + } => { + let req = requests + .get_mut(&req_id) + .ok_or(format!("unknown data columns by range req_id {req_id}"))?; + req.finish(req_id, columns) + } } - // TODO(das): this mechanism is dangerous, if somehow there are two requests for the - // same column index it can terminate early. This struct should track that all requests - // for all custody columns terminate. - self.custody_columns_streams_terminated += 1; } - pub fn into_responses(self, spec: &ChainSpec) -> Result>, String> { - if let Some(expects_custody_columns) = self.expects_custody_columns.clone() { - self.into_responses_with_custody_columns(expects_custody_columns, spec) - } else { - self.into_responses_with_blobs(spec) + pub fn responses(&self, spec: &ChainSpec) -> Option>, String>> { + let Some(blocks) = self.blocks_request.to_finished() else { + return None; + }; + + match &self.block_data_request { + RangeBlockDataRequest::NoData => { + Some(Self::responses_with_blobs(blocks.to_vec(), vec![], spec)) + } + RangeBlockDataRequest::Blobs(request) => { + let Some(blobs) = request.to_finished() else { + return None; + }; + Some(Self::responses_with_blobs( + blocks.to_vec(), + blobs.to_vec(), + spec, + )) + } + RangeBlockDataRequest::DataColumns { + requests, + expected_custody_columns, + } => { + let mut data_columns = vec![]; + for req in requests.values() { + let Some(data) = req.to_finished() else { + return None; + }; + data_columns.extend(data.clone()) + } + + Some(Self::responses_with_custody_columns( + blocks.to_vec(), + data_columns, + expected_custody_columns, + spec, + )) + } } } - fn into_responses_with_blobs(self, spec: &ChainSpec) -> Result>, String> { - let RangeBlockComponentsRequest { blocks, blobs, .. } = self; - + fn responses_with_blobs( + blocks: Vec>>, + blobs: Vec>>, + spec: &ChainSpec, + ) -> Result>, String> { // There can't be more more blobs than blocks. i.e. sending any blob (empty // included) for a skipped slot is not permitted. let mut responses = Vec::with_capacity(blocks.len()); @@ -129,17 +196,12 @@ impl RangeBlockComponentsRequest { Ok(responses) } - fn into_responses_with_custody_columns( - self, - expects_custody_columns: Vec, + fn responses_with_custody_columns( + blocks: Vec>>, + data_columns: DataColumnSidecarList, + expects_custody_columns: &[ColumnIndex], spec: &ChainSpec, ) -> Result>, String> { - let RangeBlockComponentsRequest { - blocks, - data_columns, - .. - } = self; - // Group data columns by block_root and index let mut data_columns_by_block = HashMap::>>>::new(); @@ -177,7 +239,7 @@ impl RangeBlockComponentsRequest { }; let mut custody_columns = vec![]; - for index in &expects_custody_columns { + for index in expects_custody_columns { let Some(data_column) = data_columns_by_index.remove(index) else { return Err(format!("No column for block {block_root:?} index {index}")); }; @@ -195,10 +257,17 @@ impl RangeBlockComponentsRequest { )); } - RpcBlock::new_with_custody_columns(Some(block_root), block, custody_columns, spec) - .map_err(|e| format!("{e:?}"))? + RpcBlock::new_with_custody_columns( + Some(block_root), + block, + custody_columns, + expects_custody_columns.len(), + spec, + ) + .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) }); } @@ -210,20 +279,27 @@ impl RangeBlockComponentsRequest { Ok(rpc_blocks) } +} - pub fn is_finished(&self) -> bool { - if !self.is_blocks_stream_terminated { - return false; - } - if self.expects_blobs && !self.is_sidecars_stream_terminated { - return false; - } - if let Some(expects_custody_column_responses) = self.num_custody_column_requests { - if self.custody_columns_streams_terminated < expects_custody_column_responses { - return false; +impl ByRangeRequest { + fn finish(&mut self, id: I, data: T) -> Result<(), String> { + match self { + Self::Active(expected_id) => { + if expected_id != &id { + return Err(format!("unexpected req_id expected {expected_id} got {id}")); + } + *self = Self::Complete(data); + Ok(()) } + Self::Complete(_) => Err("request already complete".to_owned()), + } + } + + fn to_finished(&self) -> Option<&T> { + match self { + Self::Active(_) => None, + Self::Complete(data) => Some(data), } - true } } @@ -233,9 +309,52 @@ mod tests { use beacon_chain::test_utils::{ generate_rand_block_and_blobs, generate_rand_block_and_data_columns, test_spec, NumBlobs, }; + use lighthouse_network::service::api_types::{ + BlobsByRangeRequestId, BlocksByRangeRequestId, ComponentsByRangeRequestId, + DataColumnsByRangeRequestId, Id, RangeRequestId, + }; use rand::SeedableRng; use std::sync::Arc; - use types::{test_utils::XorShiftRng, ForkName, MinimalEthSpec as E, SignedBeaconBlock}; + use types::{test_utils::XorShiftRng, Epoch, ForkName, MinimalEthSpec as E, SignedBeaconBlock}; + + fn components_id() -> ComponentsByRangeRequestId { + ComponentsByRangeRequestId { + id: 0, + requester: RangeRequestId::RangeSync { + chain_id: 1, + batch_id: Epoch::new(0), + }, + } + } + + fn blocks_id(parent_request_id: ComponentsByRangeRequestId) -> BlocksByRangeRequestId { + BlocksByRangeRequestId { + id: 1, + parent_request_id, + } + } + + fn blobs_id(parent_request_id: ComponentsByRangeRequestId) -> BlobsByRangeRequestId { + BlobsByRangeRequestId { + id: 1, + parent_request_id, + } + } + + fn columns_id( + id: Id, + parent_request_id: ComponentsByRangeRequestId, + ) -> DataColumnsByRangeRequestId { + DataColumnsByRangeRequestId { + id, + parent_request_id, + } + } + + fn is_finished(info: &RangeBlockComponentsRequest) -> bool { + let spec = test_spec::(); + info.responses(&spec).is_some() + } #[test] fn no_blobs_into_responses() { @@ -248,14 +367,15 @@ mod tests { .into() }) .collect::>>>(); - let mut info = RangeBlockComponentsRequest::::new(false, None, None); + + let blocks_req_id = blocks_id(components_id()); + let mut info = RangeBlockComponentsRequest::::new(blocks_req_id, None, None); // Send blocks and complete terminate response - info.add_blocks(blocks); + info.add_blocks(blocks_req_id, blocks).unwrap(); // Assert response is finished and RpcBlocks can be constructed - assert!(info.is_finished()); - info.into_responses(&test_spec::()).unwrap(); + info.responses(&test_spec::()).unwrap().unwrap(); } #[test] @@ -275,18 +395,22 @@ mod tests { .into() }) .collect::>>>(); - let mut info = RangeBlockComponentsRequest::::new(true, None, None); + + let components_id = components_id(); + let blocks_req_id = blocks_id(components_id); + let blobs_req_id = blobs_id(components_id); + let mut info = + RangeBlockComponentsRequest::::new(blocks_req_id, Some(blobs_req_id), None); // Send blocks and complete terminate response - info.add_blocks(blocks); + info.add_blocks(blocks_req_id, blocks).unwrap(); // Expect no blobs returned - info.add_blobs(vec![]); + info.add_blobs(blobs_req_id, vec![]).unwrap(); // Assert response is finished and RpcBlocks can be constructed, even if blobs weren't returned. // This makes sure we don't expect blobs here when they have expired. Checking this logic should // be hendled elsewhere. - assert!(info.is_finished()); - info.into_responses(&test_spec::()).unwrap(); + info.responses(&test_spec::()).unwrap().unwrap(); } #[test] @@ -304,40 +428,49 @@ mod tests { ) }) .collect::>(); + + let components_id = components_id(); + let blocks_req_id = blocks_id(components_id); + let columns_req_id = expects_custody_columns + .iter() + .enumerate() + .map(|(i, _)| columns_id(i as Id, components_id)) + .collect::>(); let mut info = RangeBlockComponentsRequest::::new( - false, - Some(expects_custody_columns.clone()), - Some(expects_custody_columns.len()), + blocks_req_id, + None, + Some((columns_req_id.clone(), expects_custody_columns.clone())), ); // Send blocks and complete terminate response - info.add_blocks(blocks.iter().map(|b| b.0.clone().into()).collect()); + info.add_blocks( + blocks_req_id, + blocks.iter().map(|b| b.0.clone().into()).collect(), + ) + .unwrap(); // Assert response is not finished - assert!(!info.is_finished()); + assert!(!is_finished(&info)); // Send data columns for (i, &column_index) in expects_custody_columns.iter().enumerate() { info.add_custody_columns( + columns_req_id.get(i).copied().unwrap(), blocks .iter() .flat_map(|b| b.1.iter().filter(|d| d.index == column_index).cloned()) .collect(), - ); + ) + .unwrap(); if i < expects_custody_columns.len() - 1 { assert!( - !info.is_finished(), + !is_finished(&info), "requested should not be finished at loop {i}" ); - } else { - assert!( - info.is_finished(), - "request should be finishied at loop {i}" - ); } } // All completed construct response - info.into_responses(&spec).unwrap(); + info.responses(&spec).unwrap().unwrap(); } #[test] @@ -353,10 +486,18 @@ mod tests { (0..batched_column_requests.len() as u32).collect::>(); let num_of_data_column_requests = custody_column_request_ids.len(); + let components_id = components_id(); + let blocks_req_id = blocks_id(components_id); + let columns_req_id = batched_column_requests + .iter() + .enumerate() + .map(|(i, _)| columns_id(i as Id, components_id)) + .collect::>(); + let mut info = RangeBlockComponentsRequest::::new( - false, - Some(expects_custody_columns.clone()), - Some(num_of_data_column_requests), + blocks_req_id, + None, + Some((columns_req_id.clone(), expects_custody_columns.clone())), ); let mut rng = XorShiftRng::from_seed([42; 16]); @@ -372,13 +513,18 @@ mod tests { .collect::>(); // Send blocks and complete terminate response - info.add_blocks(blocks.iter().map(|b| b.0.clone().into()).collect()); + info.add_blocks( + blocks_req_id, + blocks.iter().map(|b| b.0.clone().into()).collect(), + ) + .unwrap(); // Assert response is not finished - assert!(!info.is_finished()); + assert!(!is_finished(&info)); for (i, column_indices) in batched_column_requests.iter().enumerate() { // Send the set of columns in the same batch request info.add_custody_columns( + columns_req_id.get(i).copied().unwrap(), blocks .iter() .flat_map(|b| { @@ -387,19 +533,18 @@ mod tests { .cloned() }) .collect::>(), - ); + ) + .unwrap(); if i < num_of_data_column_requests - 1 { assert!( - !info.is_finished(), + !is_finished(&info), "requested should not be finished at loop {i}" ); - } else { - assert!(info.is_finished(), "request should be finished at loop {i}"); } } // All completed construct response - info.into_responses(&spec).unwrap(); + info.responses(&spec).unwrap().unwrap(); } } diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 041b1dba9f5..473881f182e 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -63,12 +63,13 @@ use lighthouse_network::service::api_types::{ use lighthouse_network::types::{NetworkGlobals, SyncState}; use lighthouse_network::SyncInfo; use lighthouse_network::{PeerAction, PeerId}; +use logging::crit; use lru_cache::LRUTimeCache; -use slog::{crit, debug, error, info, o, trace, warn, Logger}; use std::ops::Sub; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; +use tracing::{debug, error, info, info_span, trace, warn, Instrument}; use types::{ BlobSidecar, DataColumnSidecar, EthSpec, ForkContext, Hash256, SignedBeaconBlock, Slot, }; @@ -105,9 +106,12 @@ 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 { - request_id: SyncRequestId, + sync_request_id: SyncRequestId, peer_id: PeerId, beacon_block: Option>>, seen_timestamp: Duration, @@ -115,7 +119,7 @@ pub enum SyncMessage { /// A blob has been received from the RPC. RpcBlob { - request_id: SyncRequestId, + sync_request_id: SyncRequestId, peer_id: PeerId, blob_sidecar: Option>>, seen_timestamp: Duration, @@ -123,7 +127,7 @@ pub enum SyncMessage { /// A data columns has been received from the RPC RpcDataColumn { - request_id: SyncRequestId, + sync_request_id: SyncRequestId, peer_id: PeerId, data_column: Option>>, seen_timestamp: Duration, @@ -152,7 +156,7 @@ pub enum SyncMessage { /// An RPC Error has occurred on a request. RpcError { peer_id: PeerId, - request_id: SyncRequestId, + sync_request_id: SyncRequestId, error: RPCError, }, @@ -246,9 +250,6 @@ pub struct SyncManager { notified_unknown_roots: LRUTimeCache<(PeerId, Hash256)>, sampling: Sampling, - - /// The logger for the import manager. - log: Logger, } /// Spawns a new `SyncManager` thread which has a weak reference to underlying beacon @@ -261,7 +262,6 @@ pub fn spawn( beacon_processor: Arc>, sync_recv: mpsc::UnboundedReceiver>, fork_context: Arc, - log: slog::Logger, ) { assert!( beacon_chain.spec.max_request_blocks(fork_context.current_fork()) as u64 >= T::EthSpec::slots_per_epoch() * EPOCHS_PER_BATCH, @@ -276,12 +276,18 @@ pub fn spawn( sync_recv, SamplingConfig::Default, fork_context, - log.clone(), ); // spawn the sync manager thread - debug!(log, "Sync Manager started"); - executor.spawn(async move { Box::pin(sync_manager.main()).await }, "sync"); + debug!("Sync Manager started"); + executor.spawn( + async move { + Box::pin(sync_manager.main()) + .instrument(info_span!("", service = "sync")) + .await + }, + "sync", + ); } impl SyncManager { @@ -292,7 +298,6 @@ impl SyncManager { sync_recv: mpsc::UnboundedReceiver>, sampling_config: SamplingConfig, fork_context: Arc, - log: slog::Logger, ) -> Self { let network_globals = beacon_processor.network_globals.clone(); Self { @@ -303,23 +308,14 @@ impl SyncManager { beacon_processor.clone(), beacon_chain.clone(), fork_context.clone(), - log.clone(), - ), - range_sync: RangeSync::new( - beacon_chain.clone(), - log.new(o!("service" => "range_sync")), ), - backfill_sync: BackFillSync::new( - beacon_chain.clone(), - network_globals, - log.new(o!("service" => "backfill_sync")), - ), - block_lookups: BlockLookups::new(log.new(o!("service"=> "lookup_sync"))), + range_sync: RangeSync::new(beacon_chain.clone()), + backfill_sync: BackFillSync::new(beacon_chain.clone(), network_globals), + block_lookups: BlockLookups::new(), notified_unknown_roots: LRUTimeCache::new(Duration::from_secs( NOTIFIED_UNKNOWN_ROOT_EXPIRY_SECONDS, )), - sampling: Sampling::new(sampling_config, log.new(o!("service" => "sampling"))), - log: log.clone(), + sampling: Sampling::new(sampling_config), } } @@ -344,6 +340,16 @@ impl SyncManager { self.range_sync.state() } + #[cfg(test)] + pub(crate) fn range_sync_state(&self) -> super::range_sync::SyncChainStatus { + self.range_sync.state() + } + + #[cfg(test)] + pub(crate) fn __range_failed_chains(&mut self) -> Vec { + self.range_sync.__failed_chains() + } + #[cfg(test)] pub(crate) fn get_failed_chains(&mut self) -> Vec { self.block_lookups.get_failed_chains() @@ -368,11 +374,6 @@ impl SyncManager { self.sampling.get_request_status(block_root, index) } - #[cfg(test)] - pub(crate) fn range_sync_state(&self) -> super::range_sync::SyncChainStatus { - self.range_sync.state() - } - #[cfg(test)] pub(crate) fn update_execution_engine_state(&mut self, state: EngineState) { self.handle_new_execution_engine_state(state); @@ -456,10 +457,10 @@ impl SyncManager { }; let head_slot = head_slot.unwrap_or_else(|| { - debug!(self.log, - "On add peers force range sync assuming local head_slot"; - "local_head_slot" => local.head_slot, - "head_root" => ?head_root + debug!( + local_head_slot = %local.head_slot, + ?head_root, + "On add peers force range sync assuming local head_slot" ); local.head_slot }); @@ -478,10 +479,20 @@ 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, request_id: SyncRequestId, error: RPCError) { - trace!(self.log, "Sync manager received a failed RPC"); - match request_id { + fn inject_error(&mut self, peer_id: PeerId, sync_request_id: SyncRequestId, error: RPCError) { + trace!("Sync manager received a failed RPC"); + match sync_request_id { SyncRequestId::SingleBlock { id } => { self.on_single_block_response(id, peer_id, RpcEvent::RPCError(error)) } @@ -511,15 +522,13 @@ impl SyncManager { fn peer_disconnect(&mut self, peer_id: &PeerId) { // Inject a Disconnected error on all requests associated with the disconnected peer // to retry all batches/lookups - for request_id in self.network.peer_disconnected(peer_id) { - self.inject_error(*peer_id, request_id, RPCError::Disconnected); + for sync_request_id in self.network.peer_disconnected(peer_id) { + self.inject_error(*peer_id, sync_request_id, RPCError::Disconnected); } // 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. @@ -560,15 +569,14 @@ impl SyncManager { let is_connected = self.network_globals().peers.read().is_connected(peer_id); if was_updated { debug!( - self.log, - "Peer transitioned sync state"; - "peer_id" => %peer_id, - "new_state" => rpr, - "our_head_slot" => local_sync_info.head_slot, - "our_finalized_epoch" => local_sync_info.finalized_epoch, - "their_head_slot" => remote_sync_info.head_slot, - "their_finalized_epoch" => remote_sync_info.finalized_epoch, - "is_connected" => is_connected + %peer_id, + new_state = rpr, + our_head_slot = %local_sync_info.head_slot, + our_finalized_epoch = %local_sync_info.finalized_epoch, + their_head_slot = %remote_sync_info.head_slot, + their_finalized_epoch = %remote_sync_info.finalized_epoch, + is_connected, + "Peer transitioned sync state" ); // A peer has transitioned its sync state. If the new state is "synced" we @@ -579,7 +587,7 @@ impl SyncManager { } is_connected } else { - error!(self.log, "Status'd peer is unknown"; "peer_id" => %peer_id); + error!(%peer_id, "Status'd peer is unknown"); false } } @@ -598,7 +606,7 @@ impl SyncManager { fn update_sync_state(&mut self) { let new_state: SyncState = match self.range_sync.state() { Err(e) => { - crit!(self.log, "Error getting range sync state"; "error" => %e); + crit!(error = %e, "Error getting range sync state"); return; } Ok(state) => match state { @@ -647,7 +655,7 @@ impl SyncManager { } Ok(SyncStart::NotSyncing) => {} // Ignore updating the state if the backfill sync state didn't start. Err(e) => { - error!(self.log, "Backfill sync failed to start"; "error" => ?e); + error!(error = ?e, "Backfill sync failed to start"); } } } @@ -681,7 +689,7 @@ impl SyncManager { let old_state = self.network_globals().set_sync_state(new_state); let new_state = self.network_globals().sync_state.read().clone(); if !new_state.eq(&old_state) { - info!(self.log, "Sync state updated"; "old_state" => %old_state, "new_state" => %new_state); + info!(%old_state, %new_state, "Sync state updated"); // If we have become synced - Subscribe to all the core subnet topics // We don't need to subscribe if the old state is a state that would have already // invoked this call. @@ -753,30 +761,39 @@ 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 { - request_id, + sync_request_id, peer_id, beacon_block, seen_timestamp, } => { - self.rpc_block_received(request_id, peer_id, beacon_block, seen_timestamp); + self.rpc_block_received(sync_request_id, peer_id, beacon_block, seen_timestamp); } SyncMessage::RpcBlob { - request_id, + sync_request_id, peer_id, blob_sidecar, seen_timestamp, - } => self.rpc_blob_received(request_id, peer_id, blob_sidecar, seen_timestamp), + } => self.rpc_blob_received(sync_request_id, peer_id, blob_sidecar, seen_timestamp), SyncMessage::RpcDataColumn { - request_id, + sync_request_id, peer_id, data_column, seen_timestamp, - } => self.rpc_data_column_received(request_id, peer_id, data_column, seen_timestamp), + } => { + self.rpc_data_column_received(sync_request_id, peer_id, data_column, seen_timestamp) + } SyncMessage::UnknownParentBlock(peer_id, block, block_root) => { let block_slot = block.slot(); let parent_root = block.parent_root(); - debug!(self.log, "Received unknown parent block message"; "block_root" => %block_root, "parent_root" => %parent_root); + debug!(%block_root, %parent_root, "Received unknown parent block message"); self.handle_unknown_parent( peer_id, block_root, @@ -794,7 +811,7 @@ impl SyncManager { let blob_slot = blob.slot(); let block_root = blob.block_root(); let parent_root = blob.block_parent_root(); - debug!(self.log, "Received unknown parent blob message"; "block_root" => %block_root, "parent_root" => %parent_root); + debug!(%block_root, %parent_root, "Received unknown parent blob message"); self.handle_unknown_parent( peer_id, block_root, @@ -812,7 +829,7 @@ impl SyncManager { let data_column_slot = data_column.slot(); let block_root = data_column.block_root(); let parent_root = data_column.block_parent_root(); - debug!(self.log, "Received unknown parent data column message"; "block_root" => %block_root, "parent_root" => %parent_root); + debug!(%block_root, %parent_root, "Received unknown parent data column message"); self.handle_unknown_parent( peer_id, block_root, @@ -829,12 +846,12 @@ impl SyncManager { SyncMessage::UnknownBlockHashFromAttestation(peer_id, block_root) => { if !self.notified_unknown_roots.contains(&(peer_id, block_root)) { self.notified_unknown_roots.insert((peer_id, block_root)); - debug!(self.log, "Received unknown block hash message"; "block_root" => ?block_root, "peer" => ?peer_id); + debug!(?block_root, ?peer_id, "Received unknown block hash message"); self.handle_unknown_block_root(peer_id, block_root); } } SyncMessage::SampleBlock(block_root, block_slot) => { - debug!(self.log, "Received SampleBlock message"; "block_root" => %block_root, "slot" => block_slot); + debug!(%block_root, slot = %block_slot, "Received SampleBlock message"); if let Some((requester, result)) = self .sampling .on_new_sample_request(block_root, &mut self.network) @@ -843,14 +860,14 @@ impl SyncManager { } } SyncMessage::Disconnect(peer_id) => { - debug!(self.log, "Received disconnected message"; "peer_id" => %peer_id); + debug!(%peer_id, "Received disconnected message"); self.peer_disconnect(&peer_id); } SyncMessage::RpcError { peer_id, - request_id, + sync_request_id, error, - } => self.inject_error(peer_id, request_id, error), + } => self.inject_error(peer_id, sync_request_id, error), SyncMessage::BlockComponentProcessed { process_type, result, @@ -884,7 +901,7 @@ impl SyncManager { Ok(ProcessResult::Successful) => {} Ok(ProcessResult::SyncCompleted) => self.update_sync_state(), Err(error) => { - error!(self.log, "Backfill sync failed"; "error" => ?error); + error!(error = ?error, "Backfill sync failed"); // Update the global status self.update_sync_state(); } @@ -920,7 +937,7 @@ impl SyncManager { ); } Err(reason) => { - debug!(self.log, "Ignoring unknown parent request"; "block_root" => %block_root, "parent_root" => %parent_root, "reason" => reason); + debug!(%block_root, %parent_root, reason, "Ignoring unknown parent request"); } } } @@ -932,7 +949,7 @@ impl SyncManager { .search_unknown_block(block_root, &[peer_id], &mut self.network); } Err(reason) => { - debug!(self.log, "Ignoring unknown block request"; "block_root" => %block_root, "reason" => reason); + debug!(%block_root, reason, "Ignoring unknown block request"); } } } @@ -1010,8 +1027,9 @@ impl SyncManager { // Some logs. if dropped_single_blocks_requests > 0 { - debug!(self.log, "Execution engine not online. Dropping active requests."; - "dropped_single_blocks_requests" => dropped_single_blocks_requests, + debug!( + dropped_single_blocks_requests, + "Execution engine not online. Dropping active requests." ); } } @@ -1020,12 +1038,12 @@ impl SyncManager { fn rpc_block_received( &mut self, - request_id: SyncRequestId, + sync_request_id: SyncRequestId, peer_id: PeerId, block: Option>>, seen_timestamp: Duration, ) { - match request_id { + match sync_request_id { SyncRequestId::SingleBlock { id } => self.on_single_block_response( id, peer_id, @@ -1037,7 +1055,7 @@ impl SyncManager { RpcEvent::from_chunk(block, seen_timestamp), ), _ => { - crit!(self.log, "bad request id for block"; "peer_id" => %peer_id ); + crit!(%peer_id, "bad request id for block"); } } } @@ -1062,12 +1080,12 @@ impl SyncManager { fn rpc_blob_received( &mut self, - request_id: SyncRequestId, + sync_request_id: SyncRequestId, peer_id: PeerId, blob: Option>>, seen_timestamp: Duration, ) { - match request_id { + match sync_request_id { SyncRequestId::SingleBlob { id } => self.on_single_blob_response( id, peer_id, @@ -1079,19 +1097,19 @@ impl SyncManager { RpcEvent::from_chunk(blob, seen_timestamp), ), _ => { - crit!(self.log, "bad request id for blob"; "peer_id" => %peer_id); + crit!(%peer_id, "bad request id for blob"); } } } fn rpc_data_column_received( &mut self, - request_id: SyncRequestId, + sync_request_id: SyncRequestId, peer_id: PeerId, data_column: Option>>, seen_timestamp: Duration, ) { - match request_id { + match sync_request_id { SyncRequestId::DataColumnsByRoot(req_id) => { self.on_data_columns_by_root_response( req_id, @@ -1105,7 +1123,7 @@ impl SyncManager { RpcEvent::from_chunk(data_column, seen_timestamp), ), _ => { - crit!(self.log, "bad request id for data_column"; "peer_id" => %peer_id); + crit!(%peer_id, "bad request id for data_column"); } } } @@ -1169,7 +1187,7 @@ impl SyncManager { self.on_range_components_response( id.parent_request_id, peer_id, - RangeBlockComponent::Block(resp), + RangeBlockComponent::Block(id, resp), ); } } @@ -1184,7 +1202,7 @@ impl SyncManager { self.on_range_components_response( id.parent_request_id, peer_id, - RangeBlockComponent::Blob(resp), + RangeBlockComponent::Blob(id, resp), ); } } @@ -1202,7 +1220,7 @@ impl SyncManager { self.on_range_components_response( id.parent_request_id, peer_id, - RangeBlockComponent::CustodyColumns(resp), + RangeBlockComponent::CustodyColumns(id, resp), ); } } @@ -1212,12 +1230,10 @@ impl SyncManager { requester: CustodyRequester, response: CustodyByRootResult, ) { - // TODO(das): get proper timestamp - let seen_timestamp = timestamp_now(); self.block_lookups .on_download_response::>( requester.0, - response.map(|(columns, peer_group)| (columns, peer_group, seen_timestamp)), + response, &mut self.network, ); } @@ -1225,7 +1241,7 @@ impl SyncManager { fn on_sampling_result(&mut self, requester: SamplingRequester, result: SamplingResult) { match requester { SamplingRequester::ImportedBlock(block_root) => { - debug!(self.log, "Sampling result"; "block_root" => %block_root, "result" => ?result); + debug!(%block_root, ?result, "Sampling result"); match result { Ok(_) => { @@ -1236,11 +1252,11 @@ impl SyncManager { .beacon_processor() .send_sampling_completed(block_root) { - warn!(self.log, "Error sending sampling result"; "block_root" => ?block_root, "reason" => ?e); + warn!(?block_root, reason = ?e, "Error sending sampling result"); } } Err(e) => { - warn!(self.log, "Sampling failed"; "block_root" => %block_root, "reason" => ?e); + warn!(?block_root, reason = ?e, "Sampling failed"); } } } @@ -1292,7 +1308,7 @@ impl SyncManager { } } } - Err(_) => match range_request_id.requester { + Err(e) => match range_request_id.requester { RangeRequestId::RangeSync { chain_id, batch_id } => { self.range_sync.inject_error( &mut self.network, @@ -1300,16 +1316,22 @@ impl SyncManager { batch_id, chain_id, range_request_id.id, + e, ); self.update_sync_state(); } - RangeRequestId::BackfillSync { batch_id } => match self - .backfill_sync - .inject_error(&mut self.network, batch_id, &peer_id, range_request_id.id) - { - Ok(_) => {} - Err(_) => self.update_sync_state(), - }, + RangeRequestId::BackfillSync { batch_id } => { + match self.backfill_sync.inject_error( + &mut self.network, + batch_id, + &peer_id, + range_request_id.id, + e, + ) { + Ok(_) => {} + Err(_) => self.update_sync_state(), + } + } }, } } diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index b03a446add2..d9eda651e73 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,20 +29,22 @@ 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, }; -use slog::{debug, error, warn}; +#[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; use types::{ BlobSidecar, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, ForkContext, @@ -68,23 +72,32 @@ impl RpcEvent { pub type RpcResponseResult = Result<(T, Duration), RpcResponseError>; -pub type CustodyByRootResult = Result<(DataColumnSidecarList, PeerGroup), RpcResponseError>; +/// Duration = latest seen timestamp of all received data columns +pub type CustodyByRootResult = + Result<(DataColumnSidecarList, PeerGroup, Duration), RpcResponseError>; #[derive(Debug)] pub enum RpcResponseError { - RpcError(RPCError), + RpcError(#[allow(dead_code)] RPCError), VerifyError(LookupVerifyError), - CustodyRequestError(CustodyRequestError), - BlockComponentCouplingError(String), + CustodyRequestError(#[allow(dead_code)] CustodyRequestError), + BlockComponentCouplingError(#[allow(dead_code)] String), } #[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), +} + +/// Type of peer missing that caused a `RpcRequestSendError::NoPeers` +#[derive(Debug, PartialEq, Eq)] +pub enum NoPeerError { + BlockPeer, + CustodyPeer(ColumnIndex), } #[derive(Debug, PartialEq, Eq)] @@ -199,16 +212,51 @@ pub struct SyncNetworkContext { pub chain: Arc>, fork_context: Arc, - - /// Logger for the `SyncNetworkContext`. - pub log: slog::Logger, } /// Small enumeration to make dealing with block and blob requests easier. pub enum RangeBlockComponent { - Block(RpcResponseResult>>>), - Blob(RpcResponseResult>>>), - CustodyColumns(RpcResponseResult>>>), + Block( + BlocksByRangeRequestId, + RpcResponseResult>>>, + ), + Blob( + BlobsByRangeRequestId, + RpcResponseResult>>>, + ), + CustodyColumns( + DataColumnsByRangeRequestId, + RpcResponseResult>>>, + ), +} + +#[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 { @@ -217,8 +265,13 @@ impl SyncNetworkContext { network_beacon_processor: Arc>, chain: Arc>, fork_context: Arc, - log: slog::Logger, ) -> Self { + let span = span!( + Level::INFO, + "SyncNetworkContext", + service = "network_context" + ); + let _enter = span.enter(); SyncNetworkContext { network_send, execution_engine_state: EngineState::Online, // always assume `Online` at the start @@ -234,7 +287,6 @@ impl SyncNetworkContext { network_beacon_processor, chain, fork_context, - log, } } @@ -265,7 +317,6 @@ impl SyncNetworkContext { network_beacon_processor: _, chain: _, fork_context: _, - log: _, } = self; let blocks_by_root_ids = blocks_by_root_requests @@ -307,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 } @@ -328,48 +373,131 @@ impl SyncNetworkContext { } pub fn status_peers(&self, chain: &C, peers: impl Iterator) { + let span = span!( + Level::INFO, + "SyncNetworkContext", + service = "network_context" + ); + let _enter = span.enter(); + let status_message = chain.status_message(); for peer_id in peers { debug!( - self.log, - "Sending Status Request"; - "peer" => %peer_id, - "fork_digest" => ?status_message.fork_digest, - "finalized_root" => ?status_message.finalized_root, - "finalized_epoch" => ?status_message.finalized_epoch, - "head_root" => %status_message.head_root, - "head_slot" => %status_message.head_slot, + peer = %peer_id, + fork_digest = ?status_message.fork_digest, + finalized_root = ?status_message.finalized_root, + finalized_epoch = ?status_message.finalized_epoch, + head_root = %status_message.head_root, + head_slot = %status_message.head_slot, + "Sending Status Request" ); let request = RequestType::Status(status_message.clone()); - let request_id = AppRequestId::Router; + let app_request_id = AppRequestId::Router; let _ = self.send_network_msg(NetworkMessage::SendRequest { peer_id, request, - request_id, + app_request_id, }); } } + 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, }; - 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(), @@ -380,73 +508,98 @@ impl SyncNetworkContext { None }; - let (expects_columns, data_column_requests) = - if matches!(batch_type, ByRangeRequestType::BlocksAndColumns) { - let column_indexes = self.network_globals().sampling_columns.clone(); - - let data_column_requests = self - .make_columns_by_range_requests(request, &column_indexes)? + 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_by_range_request)| { + .map(|(peer_id, columns)| { self.send_data_columns_by_range_request( peer_id, - columns_by_range_request, + DataColumnsByRangeRequest { + start_slot: *request.start_slot(), + count: *request.count(), + columns, + }, id, ) }) - .collect::, _>>()?; + .collect::, _>>() + }) + .transpose()?; + let info = RangeBlockComponentsRequest::new( + blocks_req_id, + blobs_req_id, + data_column_requests.map(|data_column_requests| { ( - Some(column_indexes.into_iter().collect::>()), - Some(data_column_requests), + data_column_requests, + self.network_globals() + .sampling_columns + .clone() + .iter() + .copied() + .collect(), ) - } else { - (None, None) - }; - - let expected_blobs = blobs_req_id.is_some(); - let info = RangeBlockComponentsRequest::new( - expected_blobs, - expects_columns, - data_column_requests.map(|items| items.len()), + }), ); 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 ' @@ -464,28 +617,33 @@ impl SyncNetworkContext { if let Err(e) = { let request = entry.get_mut(); match range_block_component { - RangeBlockComponent::Block(resp) => resp.map(|(blocks, _)| { - request.add_blocks(blocks); + RangeBlockComponent::Block(req_id, resp) => resp.and_then(|(blocks, _)| { + request + .add_blocks(req_id, blocks) + .map_err(RpcResponseError::BlockComponentCouplingError) }), - RangeBlockComponent::Blob(resp) => resp.map(|(blobs, _)| { - request.add_blobs(blobs); - }), - RangeBlockComponent::CustodyColumns(resp) => resp.map(|(custody_columns, _)| { - request.add_custody_columns(custody_columns); + RangeBlockComponent::Blob(req_id, resp) => resp.and_then(|(blobs, _)| { + request + .add_blobs(req_id, blobs) + .map_err(RpcResponseError::BlockComponentCouplingError) }), + RangeBlockComponent::CustodyColumns(req_id, resp) => { + resp.and_then(|(custody_columns, _)| { + request + .add_custody_columns(req_id, custody_columns) + .map_err(RpcResponseError::BlockComponentCouplingError) + }) + } } } { entry.remove(); return Some(Err(e)); } - if entry.get_mut().is_finished() { + if let Some(blocks_result) = entry.get().responses(&self.chain.spec) { + entry.remove(); // If the request is finished, dequeue everything - let request = entry.remove(); - let blocks = request - .into_responses(&self.chain.spec) - .map_err(RpcResponseError::BlockComponentCouplingError); - Some(blocks) + Some(blocks_result.map_err(RpcResponseError::BlockComponentCouplingError)) } else { None } @@ -501,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 @@ -516,6 +684,13 @@ impl SyncNetworkContext { return Ok(LookupRequestResult::Pending("no peers")); }; + let span = span!( + Level::INFO, + "SyncNetworkContext", + service = "network_context" + ); + let _enter = span.enter(); + match self.chain.get_block_process_status(&block_root) { // Unknown block, continue request to download BlockProcessStatus::Unknown => {} @@ -553,17 +728,16 @@ impl SyncNetworkContext { .send(NetworkMessage::SendRequest { peer_id, request: RequestType::BlocksByRoot(request.into_request(&self.fork_context)), - request_id: AppRequestId::Sync(SyncRequestId::SingleBlock { id }), + app_request_id: AppRequestId::Sync(SyncRequestId::SingleBlock { id }), }) - .map_err(|_| RpcRequestSendError::NetworkSendError)?; + .map_err(|_| RpcRequestSendError::InternalError("network send error".to_owned()))?; debug!( - self.log, - "Sync RPC request sent"; - "method" => "BlocksByRoot", - "block_root" => ?block_root, - "peer" => %peer_id, - "id" => %id + method = "BlocksByRoot", + ?block_root, + peer = %peer_id, + %id, + "Sync RPC request sent" ); self.blocks_by_root_requests.insert( @@ -591,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 @@ -606,6 +790,13 @@ impl SyncNetworkContext { return Ok(LookupRequestResult::Pending("no peers")); }; + let span = span!( + Level::INFO, + "SyncNetworkContext", + service = "network_context" + ); + let _enter = span.enter(); + let imported_blob_indexes = self .chain .data_availability_checker @@ -636,18 +827,17 @@ impl SyncNetworkContext { .send(NetworkMessage::SendRequest { peer_id, request: RequestType::BlobsByRoot(request.clone().into_request(&self.fork_context)), - request_id: AppRequestId::Sync(SyncRequestId::SingleBlob { id }), + app_request_id: AppRequestId::Sync(SyncRequestId::SingleBlob { id }), }) - .map_err(|_| RpcRequestSendError::NetworkSendError)?; + .map_err(|_| RpcRequestSendError::InternalError("network send error".to_owned()))?; debug!( - self.log, - "Sync RPC request sent"; - "method" => "BlobsByRoot", - "block_root" => ?block_root, - "blob_indices" => ?indices, - "peer" => %peer_id, - "id" => %id + method = "BlobsByRoot", + ?block_root, + blob_indices = ?indices, + peer = %peer_id, + %id, + "Sync RPC request sent" ); self.blobs_by_root_requests.insert( @@ -671,6 +861,13 @@ impl SyncNetworkContext { request: DataColumnsByRootSingleBlockRequest, expect_max_responses: bool, ) -> Result, &'static str> { + let span = span!( + Level::INFO, + "SyncNetworkContext", + service = "network_context" + ); + let _enter = span.enter(); + let id = DataColumnsByRootRequestId { id: self.next_id(), requester, @@ -679,17 +876,16 @@ impl SyncNetworkContext { self.send_network_msg(NetworkMessage::SendRequest { peer_id, request: RequestType::DataColumnsByRoot(request.clone().into_request(&self.chain.spec)), - request_id: AppRequestId::Sync(SyncRequestId::DataColumnsByRoot(id)), + app_request_id: AppRequestId::Sync(SyncRequestId::DataColumnsByRoot(id)), })?; debug!( - self.log, - "Sync RPC request sent"; - "method" => "DataColumnsByRoot", - "block_root" => ?request.block_root, - "indices" => ?request.indices, - "peer" => %peer_id, - "id" => %id, + method = "DataColumnsByRoot", + block_root = ?request.block_root, + indices = ?request.indices, + peer = %peer_id, + %id, + "Sync RPC request sent" ); self.data_columns_by_root_requests.insert( @@ -712,6 +908,13 @@ impl SyncNetworkContext { block_root: Hash256, lookup_peers: Arc>>, ) -> Result { + let span = span!( + Level::INFO, + "SyncNetworkContext", + service = "network_context" + ); + let _enter = span.enter(); + let custody_indexes_imported = self .chain .data_availability_checker @@ -738,11 +941,10 @@ impl SyncNetworkContext { }; debug!( - self.log, - "Starting custody columns request"; - "block_root" => ?block_root, - "indices" => ?custody_indexes_to_fetch, - "id" => %id + ?block_root, + indices = ?custody_indexes_to_fetch, + %id, + "Starting custody columns request" ); let requester = CustodyRequester(id); @@ -751,7 +953,6 @@ impl SyncNetworkContext { CustodyId { requester }, &custody_indexes_to_fetch, lookup_peers, - self.log.clone(), ); // Note that you can only send, but not handle a response here @@ -763,7 +964,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:?}")) + } + }), } } @@ -781,18 +1000,17 @@ impl SyncNetworkContext { .send(NetworkMessage::SendRequest { peer_id, request: RequestType::BlocksByRange(request.clone().into()), - request_id: AppRequestId::Sync(SyncRequestId::BlocksByRange(id)), + app_request_id: AppRequestId::Sync(SyncRequestId::BlocksByRange(id)), }) - .map_err(|_| RpcRequestSendError::NetworkSendError)?; + .map_err(|_| RpcRequestSendError::InternalError("network send error".to_owned()))?; debug!( - self.log, - "Sync RPC request sent"; - "method" => "BlocksByRange", - "slots" => request.count(), - "epoch" => Slot::new(*request.start_slot()).epoch(T::EthSpec::slots_per_epoch()), - "peer" => %peer_id, - "id" => %id, + method = "BlocksByRange", + slots = request.count(), + epoch = %Slot::new(*request.start_slot()).epoch(T::EthSpec::slots_per_epoch()), + peer = %peer_id, + %id, + "Sync RPC request sent" ); self.blocks_by_range_requests.insert( @@ -823,18 +1041,17 @@ impl SyncNetworkContext { .send(NetworkMessage::SendRequest { peer_id, request: RequestType::BlobsByRange(request.clone()), - request_id: AppRequestId::Sync(SyncRequestId::BlobsByRange(id)), + app_request_id: AppRequestId::Sync(SyncRequestId::BlobsByRange(id)), }) - .map_err(|_| RpcRequestSendError::NetworkSendError)?; + .map_err(|_| RpcRequestSendError::InternalError("network send error".to_owned()))?; debug!( - self.log, - "Sync RPC request sent"; - "method" => "BlobsByRange", - "slots" => request.count, - "epoch" => request_epoch, - "peer" => %peer_id, - "id" => %id, + method = "BlobsByRange", + slots = request.count, + epoch = %request_epoch, + peer = %peer_id, + %id, + "Sync RPC request sent" ); let max_blobs_per_block = self.chain.spec.max_blobs_per_block(request_epoch); @@ -863,19 +1080,18 @@ impl SyncNetworkContext { self.send_network_msg(NetworkMessage::SendRequest { peer_id, request: RequestType::DataColumnsByRange(request.clone()), - request_id: AppRequestId::Sync(SyncRequestId::DataColumnsByRange(id)), + app_request_id: AppRequestId::Sync(SyncRequestId::DataColumnsByRange(id)), }) - .map_err(|_| RpcRequestSendError::NetworkSendError)?; + .map_err(|_| RpcRequestSendError::InternalError("network send error".to_owned()))?; debug!( - self.log, - "Sync RPC request sent"; - "method" => "DataColumnsByRange", - "slots" => request.count, - "epoch" => Slot::new(request.start_slot).epoch(T::EthSpec::slots_per_epoch()), - "columns" => ?request.columns, - "peer" => %peer_id, - "id" => %id, + method = "DataColumnsByRange", + slots = request.count, + epoch = %Slot::new(request.start_slot).epoch(T::EthSpec::slots_per_epoch()), + columns = ?request.columns, + peer = %peer_id, + %id, + "Sync RPC request sent" ); self.data_columns_by_range_requests.insert( @@ -894,13 +1110,26 @@ impl SyncNetworkContext { } pub fn update_execution_engine_state(&mut self, engine_state: EngineState) { - debug!(self.log, "Sync's view on execution engine state updated"; - "past_state" => ?self.execution_engine_state, "new_state" => ?engine_state); + let span = span!( + Level::INFO, + "SyncNetworkContext", + service = "network_context" + ); + let _enter = span.enter(); + + debug!(past_state = ?self.execution_engine_state, new_state = ?engine_state, "Sync's view on execution engine state updated"); self.execution_engine_state = engine_state; } /// Terminates the connection with the peer and bans them. pub fn goodbye_peer(&mut self, peer_id: PeerId, reason: GoodbyeReason) { + let span = span!( + Level::INFO, + "SyncNetworkContext", + service = "network_context" + ); + let _enter = span.enter(); + self.network_send .send(NetworkMessage::GoodbyePeer { peer_id, @@ -908,13 +1137,20 @@ impl SyncNetworkContext { source: ReportSource::SyncService, }) .unwrap_or_else(|_| { - warn!(self.log, "Could not report peer: channel failed"); + warn!("Could not report peer: channel failed"); }); } /// Reports to the scoring algorithm the behaviour of a peer. pub fn report_peer(&self, peer_id: PeerId, action: PeerAction, msg: &'static str) { - debug!(self.log, "Sync reporting peer"; "peer_id" => %peer_id, "action" => %action, "msg" => %msg); + let span = span!( + Level::INFO, + "SyncNetworkContext", + service = "network_context" + ); + let _enter = span.enter(); + + debug!(%peer_id, %action, %msg, "Sync reporting peer"); self.network_send .send(NetworkMessage::ReportPeer { peer_id, @@ -923,23 +1159,37 @@ impl SyncNetworkContext { msg, }) .unwrap_or_else(|e| { - warn!(self.log, "Could not report peer: channel failed"; "error"=> %e); + warn!(error = %e, "Could not report peer: channel failed"); }); } /// Subscribes to core topics. pub fn subscribe_core_topics(&self) { + let span = span!( + Level::INFO, + "SyncNetworkContext", + service = "network_context" + ); + let _enter = span.enter(); + self.network_send .send(NetworkMessage::SubscribeCoreTopics) .unwrap_or_else(|e| { - warn!(self.log, "Could not subscribe to core topics."; "error" => %e); + warn!(error = %e, "Could not subscribe to core topics."); }); } /// Sends an arbitrary network message. fn send_network_msg(&self, msg: NetworkMessage) -> Result<(), &'static str> { + let span = span!( + Level::INFO, + "SyncNetworkContext", + service = "network_context" + ); + let _enter = span.enter(); + self.network_send.send(msg).map_err(|_| { - debug!(self.log, "Could not send message to the network service"); + debug!("Could not send message to the network service"); "Network channel send Failed" }) } @@ -1126,20 +1376,18 @@ impl SyncNetworkContext { None => {} Some(Ok((v, _))) => { debug!( - self.log, - "Sync RPC request completed"; - "id" => %id, - "method" => method, - "count" => get_count(v) + %id, + method, + count = get_count(v), + "Sync RPC request completed" ); } Some(Err(e)) => { debug!( - self.log, - "Sync RPC request error"; - "id" => %id, - "method" => method, - "error" => ?e + %id, + method, + error = ?e, + "Sync RPC request error" ); } } @@ -1164,11 +1412,18 @@ impl SyncNetworkContext { peer_id: PeerId, resp: RpcResponseResult>>>, ) -> Option> { + let span = span!( + Level::INFO, + "SyncNetworkContext", + service = "network_context" + ); + let _enter = span.enter(); + // Note: need to remove the request to borrow self again below. Otherwise we can't // do nested requests let Some(mut request) = self.custody_by_root_requests.remove(&id.requester) else { // TOOD(das): This log can happen if the request is error'ed early and dropped - debug!(self.log, "Custody column downloaded event for unknown request"; "id" => ?id); + debug!(?id, "Custody column downloaded event for unknown request"); return None; }; @@ -1183,6 +1438,13 @@ impl SyncNetworkContext { request: ActiveCustodyRequest, result: CustodyRequestResult, ) -> Option> { + let span = span!( + Level::INFO, + "SyncNetworkContext", + service = "network_context" + ); + let _enter = span.enter(); + let result = result .map_err(RpcResponseError::CustodyRequestError) .transpose(); @@ -1190,11 +1452,11 @@ impl SyncNetworkContext { // Convert a result from internal format of `ActiveCustodyRequest` (error first to use ?) to // an Option first to use in an `if let Some() { act on result }` block. match result.as_ref() { - Some(Ok((columns, peer_group))) => { - debug!(self.log, "Custody request success, removing"; "id" => ?id, "count" => columns.len(), "peers" => ?peer_group) + Some(Ok((columns, peer_group, _))) => { + debug!(?id, count = columns.len(), peers = ?peer_group, "Custody request success, removing") } Some(Err(e)) => { - debug!(self.log, "Custody request failure, removing"; "id" => ?id, "error" => ?e) + debug!(?id, error = ?e, "Custody request failure, removing" ) } None => { self.custody_by_root_requests.insert(id, request); @@ -1207,28 +1469,40 @@ impl SyncNetworkContext { &self, id: Id, block_root: Hash256, - block: RpcBlock, - duration: Duration, + block: Arc>, + seen_timestamp: Duration, ) -> Result<(), SendErrorProcessor> { + let span = span!( + Level::INFO, + "SyncNetworkContext", + service = "network_context" + ); + let _enter = span.enter(); + let beacon_processor = self .beacon_processor_if_enabled() .ok_or(SendErrorProcessor::ProcessorNotAvailable)?; - debug!(self.log, "Sending block for processing"; "block" => ?block_root, "id" => id); + 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 beacon_processor .send_rpc_beacon_block( block_root, block, - duration, + seen_timestamp, BlockProcessType::SingleBlock { id }, ) .map_err(|e| { error!( - self.log, - "Failed to send sync block to processor"; - "error" => ?e + error = ?e, + "Failed to send sync block to processor" ); SendErrorProcessor::SendError }) @@ -1239,27 +1513,33 @@ impl SyncNetworkContext { id: Id, block_root: Hash256, blobs: FixedBlobSidecarList, - duration: Duration, + seen_timestamp: Duration, ) -> Result<(), SendErrorProcessor> { + let span = span!( + Level::INFO, + "SyncNetworkContext", + service = "network_context" + ); + let _enter = span.enter(); + let beacon_processor = self .beacon_processor_if_enabled() .ok_or(SendErrorProcessor::ProcessorNotAvailable)?; - debug!(self.log, "Sending blobs for processing"; "block" => ?block_root, "id" => id); + debug!(?block_root, ?id, "Sending blobs for processing"); // Lookup sync event safety: If `beacon_processor.send_rpc_blobs` returns Ok() sync // must receive a single `SyncMessage::BlockComponentProcessed` event with this process type beacon_processor .send_rpc_blobs( block_root, blobs, - duration, + seen_timestamp, BlockProcessType::SingleBlob { id }, ) .map_err(|e| { error!( - self.log, - "Failed to send sync blobs to processor"; - "error" => ?e + error = ?e, + "Failed to send sync blobs to processor" ); SendErrorProcessor::SendError }) @@ -1270,22 +1550,32 @@ impl SyncNetworkContext { _id: Id, block_root: Hash256, custody_columns: DataColumnSidecarList, - duration: Duration, + seen_timestamp: Duration, process_type: BlockProcessType, ) -> Result<(), SendErrorProcessor> { + let span = span!( + Level::INFO, + "SyncNetworkContext", + service = "network_context" + ); + let _enter = span.enter(); + let beacon_processor = self .beacon_processor_if_enabled() .ok_or(SendErrorProcessor::ProcessorNotAvailable)?; - debug!(self.log, "Sending custody columns for processing"; "block" => ?block_root, "process_type" => ?process_type); + debug!( + ?block_root, + ?process_type, + "Sending custody columns for processing" + ); beacon_processor - .send_rpc_custody_columns(block_root, custody_columns, duration, process_type) + .send_rpc_custody_columns(block_root, custody_columns, seen_timestamp, process_type) .map_err(|e| { error!( - self.log, - "Failed to send sync custody columns to processor"; - "error" => ?e + error = ?e, + "Failed to send sync custody columns to processor" ); SendErrorProcessor::SendError }) diff --git a/beacon_node/network/src/sync/network_context/custody.rs b/beacon_node/network/src/sync/network_context/custody.rs index 8a29545c21d..f4d010b881e 100644 --- a/beacon_node/network/src/sync/network_context/custody.rs +++ b/beacon_node/network/src/sync/network_context/custody.rs @@ -1,7 +1,7 @@ use crate::sync::network_context::{ DataColumnsByRootRequestId, DataColumnsByRootSingleBlockRequest, }; - +use beacon_chain::validator_monitor::timestamp_now; use beacon_chain::BeaconChainTypes; use fnv::FnvHashMap; use lighthouse_network::service::api_types::{CustodyId, DataColumnsByRootRequester}; @@ -9,10 +9,10 @@ use lighthouse_network::PeerId; use lru_cache::LRUTimeCache; use parking_lot::RwLock; use rand::Rng; -use slog::{debug, warn}; use std::collections::HashSet; use std::time::{Duration, Instant}; use std::{collections::HashMap, marker::PhantomData, sync::Arc}; +use tracing::{debug, warn}; use types::EthSpec; use types::{data_column_sidecar::ColumnIndex, DataColumnSidecar, Hash256}; @@ -36,8 +36,7 @@ pub struct ActiveCustodyRequest { failed_peers: LRUTimeCache, /// Set of peers that claim to have imported this block and their custody columns lookup_peers: Arc>>, - /// Logger for the `SyncNetworkContext`. - pub log: slog::Logger, + _phantom: PhantomData, } @@ -46,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. @@ -57,11 +56,11 @@ pub enum Error { } struct ActiveBatchColumnsRequest { - peer_id: PeerId, indices: Vec, } -pub type CustodyRequestResult = Result, PeerGroup)>, Error>; +pub type CustodyRequestResult = + Result, PeerGroup, Duration)>, Error>; impl ActiveCustodyRequest { pub(crate) fn new( @@ -69,7 +68,6 @@ impl ActiveCustodyRequest { custody_id: CustodyId, column_indices: &[ColumnIndex], lookup_peers: Arc>>, - log: slog::Logger, ) -> Self { Self { block_root, @@ -82,7 +80,6 @@ impl ActiveCustodyRequest { active_batch_columns_requests: <_>::default(), failed_peers: LRUTimeCache::new(Duration::from_secs(FAILED_PEERS_CACHE_EXPIRY_SECONDS)), lookup_peers, - log, _phantom: PhantomData, } } @@ -102,27 +99,23 @@ impl ActiveCustodyRequest { resp: RpcResponseResult>, cx: &mut SyncNetworkContext, ) -> CustodyRequestResult { - // TODO(das): Should downscore peers for verify errors here - let Some(batch_request) = self.active_batch_columns_requests.get_mut(&req_id) else { - warn!(self.log, - "Received custody column response for unrequested index"; - "id" => ?self.custody_id, - "block_root" => ?self.block_root, - "req_id" => %req_id, + warn!( + block_root = ?self.block_root, + %req_id, + "Received custody column response for unrequested index" ); return Ok(None); }; match resp { - Ok((data_columns, _seen_timestamp)) => { - debug!(self.log, - "Custody column download success"; - "id" => ?self.custody_id, - "block_root" => ?self.block_root, - "req_id" => %req_id, - "peer" => %peer_id, - "count" => data_columns.len() + Ok((data_columns, seen_timestamp)) => { + debug!( + block_root = ?self.block_root, + %req_id, + %peer_id, + count = data_columns.len(), + "Custody column download success" ); // Map columns by index as an optimization to not loop the returned list on each @@ -141,7 +134,12 @@ impl ActiveCustodyRequest { .ok_or(Error::BadState("unknown column_index".to_owned()))?; if let Some(data_column) = data_columns.remove(column_index) { - column_request.on_download_success(req_id, peer_id, data_column)?; + column_request.on_download_success( + req_id, + peer_id, + data_column, + seen_timestamp, + )?; } else { // Peer does not have the requested data. // TODO(das) do not consider this case a success. We know for sure the block has @@ -159,27 +157,25 @@ impl ActiveCustodyRequest { if !missing_column_indexes.is_empty() { // Note: Batch logging that columns are missing to not spam logger - debug!(self.log, - "Custody column peer claims to not have some data"; - "id" => ?self.custody_id, - "block_root" => ?self.block_root, - "req_id" => %req_id, - "peer" => %peer_id, + debug!( + block_root = ?self.block_root, + %req_id, + %peer_id, // TODO(das): this property can become very noisy, being the full range 0..128 - "missing_column_indexes" => ?missing_column_indexes + ?missing_column_indexes, + "Custody column peer claims to not have some data" ); self.failed_peers.insert(peer_id); } } Err(err) => { - debug!(self.log, - "Custody column download error"; - "id" => ?self.custody_id, - "block_root" => ?self.block_root, - "req_id" => %req_id, - "peer" => %peer_id, - "error" => ?err + debug!( + block_root = ?self.block_root, + %req_id, + %peer_id, + error = ?err, + "Custody column download error" ); // TODO(das): Should mark peer as failed and try from another peer @@ -204,22 +200,26 @@ impl ActiveCustodyRequest { if self.column_requests.values().all(|r| r.is_downloaded()) { // All requests have completed successfully. let mut peers = HashMap::>::new(); + let mut seen_timestamps = vec![]; let columns = std::mem::take(&mut self.column_requests) .into_values() .map(|request| { - let (peer, data_column) = request.complete()?; + let (peer, data_column, seen_timestamp) = request.complete()?; peers .entry(peer) .or_default() .push(data_column.index as usize); + seen_timestamps.push(seen_timestamp); Ok(data_column) }) .collect::, _>>()?; let peer_group = PeerGroup::from_set(peers); - return Ok(Some((columns, peer_group))); + let max_seen_timestamp = seen_timestamps.into_iter().max().unwrap_or(timestamp_now()); + 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!(), @@ -335,7 +335,7 @@ struct ColumnRequest { enum Status { NotStarted(Instant), Downloading(DataColumnsByRootRequestId), - Downloaded(PeerId, Arc>), + Downloaded(PeerId, Arc>, Duration), } impl ColumnRequest { @@ -404,6 +404,7 @@ impl ColumnRequest { req_id: DataColumnsByRootRequestId, peer_id: PeerId, data_column: Arc>, + seen_timestamp: Duration, ) -> Result<(), Error> { match &self.status { Status::Downloading(expected_req_id) => { @@ -413,7 +414,7 @@ impl ColumnRequest { req_id, }); } - self.status = Status::Downloaded(peer_id, data_column); + self.status = Status::Downloaded(peer_id, data_column, seen_timestamp); Ok(()) } other => Err(Error::BadState(format!( @@ -422,9 +423,11 @@ impl ColumnRequest { } } - fn complete(self) -> Result<(PeerId, Arc>), Error> { + fn complete(self) -> Result<(PeerId, Arc>, Duration), Error> { match self.status { - Status::Downloaded(peer_id, data_column) => Ok((peer_id, data_column)), + Status::Downloaded(peer_id, data_column, seen_timestamp) => { + Ok((peer_id, data_column, seen_timestamp)) + } other => Err(Error::BadState(format!( "bad state complete expected Downloaded got {other:?}" ))), diff --git a/beacon_node/network/src/sync/network_context/requests.rs b/beacon_node/network/src/sync/network_context/requests.rs index c9b85e47b69..963b633ed6d 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/peer_sampling.rs b/beacon_node/network/src/sync/peer_sampling.rs index 289ed73cdd2..59b751787e3 100644 --- a/beacon_node/network/src/sync/peer_sampling.rs +++ b/beacon_node/network/src/sync/peer_sampling.rs @@ -12,11 +12,11 @@ use lighthouse_network::service::api_types::{ }; use lighthouse_network::{PeerAction, PeerId}; use rand::{seq::SliceRandom, thread_rng}; -use slog::{debug, error, warn}; use std::{ collections::hash_map::Entry, collections::HashMap, marker::PhantomData, sync::Arc, time::Duration, }; +use tracing::{debug, error, instrument, warn}; use types::{data_column_sidecar::ColumnIndex, ChainSpec, DataColumnSidecar, Hash256}; pub type SamplingResult = Result<(), SamplingError>; @@ -26,24 +26,35 @@ type DataColumnSidecarList = Vec>>; pub struct Sampling { requests: HashMap>, sampling_config: SamplingConfig, - log: slog::Logger, } impl Sampling { - pub fn new(sampling_config: SamplingConfig, log: slog::Logger) -> Self { + #[instrument(parent = None,level = "info", fields(service = "sampling"), name = "sampling")] + pub fn new(sampling_config: SamplingConfig) -> Self { Self { requests: <_>::default(), sampling_config, - log, } } #[cfg(test)] + #[instrument(parent = None, + level = "info", + fields(service = "sampling"), + name = "sampling", + skip_all + )] pub fn active_sampling_requests(&self) -> Vec { self.requests.values().map(|r| r.block_root).collect() } #[cfg(test)] + #[instrument(parent = None, + level = "info", + fields(service = "sampling"), + name = "sampling", + skip_all + )] pub fn get_request_status( &self, block_root: Hash256, @@ -61,6 +72,12 @@ impl Sampling { /// /// - `Some`: Request completed, won't make more progress. Expect requester to act on the result. /// - `None`: Request still active, requester should do no action + #[instrument(parent = None, + level = "info", + fields(service = "sampling"), + name = "sampling", + skip_all + )] pub fn on_new_sample_request( &mut self, block_root: Hash256, @@ -73,7 +90,6 @@ impl Sampling { block_root, id, &self.sampling_config, - self.log.clone(), &cx.chain.spec, )), Entry::Occupied(_) => { @@ -82,15 +98,15 @@ impl Sampling { // TODO(das): Should track failed sampling request for some time? Otherwise there's // a risk of a loop with multiple triggers creating the request, then failing, // and repeat. - debug!(self.log, "Ignoring duplicate sampling request"; "id" => ?id); + debug!(?id, "Ignoring duplicate sampling request"); return None; } }; - debug!(self.log, - "Created new sample request"; - "id" => ?id, - "column_selection" => ?request.column_selection() + debug!( + ?id, + column_selection = ?request.column_selection(), + "Created new sample request" ); // TOOD(das): If a node has very little peers, continue_sampling() will attempt to find enough @@ -107,6 +123,12 @@ impl Sampling { /// /// - `Some`: Request completed, won't make more progress. Expect requester to act on the result. /// - `None`: Request still active, requester should do no action + #[instrument(parent = None, + level = "info", + fields(service = "sampling"), + name = "sampling", + skip_all + )] pub fn on_sample_downloaded( &mut self, id: SamplingId, @@ -116,7 +138,7 @@ impl Sampling { ) -> Option<(SamplingRequester, SamplingResult)> { let Some(request) = self.requests.get_mut(&id.id) else { // TOOD(das): This log can happen if the request is error'ed early and dropped - debug!(self.log, "Sample downloaded event for unknown request"; "id" => ?id); + debug!(?id, "Sample downloaded event for unknown request"); return None; }; @@ -131,6 +153,12 @@ impl Sampling { /// /// - `Some`: Request completed, won't make more progress. Expect requester to act on the result. /// - `None`: Request still active, requester should do no action + #[instrument(parent = None, + level = "info", + fields(service = "sampling"), + name = "sampling", + skip_all + )] pub fn on_sample_verified( &mut self, id: SamplingId, @@ -139,7 +167,7 @@ impl Sampling { ) -> Option<(SamplingRequester, SamplingResult)> { let Some(request) = self.requests.get_mut(&id.id) else { // TOOD(das): This log can happen if the request is error'ed early and dropped - debug!(self.log, "Sample verified event for unknown request"; "id" => ?id); + debug!(?id, "Sample verified event for unknown request"); return None; }; @@ -150,6 +178,12 @@ impl Sampling { /// Converts a result from the internal format of `ActiveSamplingRequest` (error first to use ? /// conveniently), to an Option first format to use an `if let Some() { act on result }` pattern /// in the sync manager. + #[instrument(parent = None, + level = "info", + fields(service = "sampling"), + name = "sampling", + skip_all + )] fn handle_sampling_result( &mut self, result: Result, SamplingError>, @@ -157,7 +191,7 @@ impl Sampling { ) -> Option<(SamplingRequester, SamplingResult)> { let result = result.transpose(); if let Some(result) = result { - debug!(self.log, "Sampling request completed, removing"; "id" => ?id, "result" => ?result); + debug!(?id, ?result, "Sampling request completed, removing"); metrics::inc_counter_vec( &metrics::SAMPLING_REQUEST_RESULT, &[metrics::from_result(&result)], @@ -180,8 +214,6 @@ pub struct ActiveSamplingRequest { current_sampling_request_id: SamplingRequestId, column_shuffle: Vec, required_successes: Vec, - /// Logger for the `SyncNetworkContext`. - pub log: slog::Logger, _phantom: PhantomData, } @@ -212,7 +244,6 @@ impl ActiveSamplingRequest { block_root: Hash256, requester_id: SamplingRequester, sampling_config: &SamplingConfig, - log: slog::Logger, spec: &ChainSpec, ) -> Self { // Select ahead of time the full list of to-sample columns @@ -232,7 +263,6 @@ impl ActiveSamplingRequest { SamplingConfig::Default => REQUIRED_SUCCESSES.to_vec(), SamplingConfig::Custom { required_successes } => required_successes.clone(), }, - log, _phantom: PhantomData, } } @@ -275,9 +305,9 @@ impl ActiveSamplingRequest { .column_indexes_by_sampling_request .get(&sampling_request_id) else { - error!(self.log, - "Column indexes for the sampling request ID not found"; - "sampling_request_id" => ?sampling_request_id + error!( + ?sampling_request_id, + "Column indexes for the sampling request ID not found" ); return Ok(None); }; @@ -288,11 +318,11 @@ impl ActiveSamplingRequest { .iter() .map(|r| r.index) .collect::>(); - debug!(self.log, - "Sample download success"; - "block_root" => %self.block_root, - "column_indexes" => ?resp_column_indexes, - "count" => resp_data_columns.len() + debug!( + block_root = %self.block_root, + column_indexes = ?resp_column_indexes, + count = resp_data_columns.len(), + "Sample download success" ); metrics::inc_counter_vec(&metrics::SAMPLE_DOWNLOAD_RESULT, &[metrics::SUCCESS]); @@ -300,10 +330,10 @@ impl ActiveSamplingRequest { let mut data_columns = vec![]; for column_index in column_indexes { let Some(request) = self.column_requests.get_mut(column_index) else { - warn!(self.log, - "Active column sample request not found"; - "block_root" => %self.block_root, - "column_index" => column_index + warn!( + block_root = %self.block_root, + column_index, + "Active column sample request not found" ); continue; }; @@ -314,10 +344,10 @@ impl ActiveSamplingRequest { else { // Peer does not have the requested data, mark peer as "dont have" and try // again with a different peer. - debug!(self.log, - "Sampling peer claims to not have the data"; - "block_root" => %self.block_root, - "column_index" => column_index + debug!( + block_root = %self.block_root, + column_index, + "Sampling peer claims to not have the data" ); request.on_sampling_error()?; continue; @@ -331,16 +361,16 @@ impl ActiveSamplingRequest { .iter() .map(|d| d.index) .collect::>(); - debug!(self.log, - "Received data that was not requested"; - "block_root" => %self.block_root, - "column_indexes" => ?resp_column_indexes + debug!( + block_root = %self.block_root, + column_indexes = ?resp_column_indexes, + "Received data that was not requested" ); } // Handle the downloaded data columns. if data_columns.is_empty() { - debug!(self.log, "Received empty response"; "block_root" => %self.block_root); + debug!(block_root = %self.block_root, "Received empty response"); self.column_indexes_by_sampling_request .remove(&sampling_request_id); } else { @@ -351,17 +381,17 @@ impl ActiveSamplingRequest { // Peer has data column, send to verify let Some(beacon_processor) = cx.beacon_processor_if_enabled() else { // If processor is not available, error the entire sampling - debug!(self.log, - "Dropping sampling"; - "block" => %self.block_root, - "reason" => "beacon processor unavailable" + debug!( + block = %self.block_root, + reason = "beacon processor unavailable", + "Dropping sampling" ); return Err(SamplingError::ProcessorUnavailable); }; - debug!(self.log, - "Sending data_column for verification"; - "block" => ?self.block_root, - "column_indexes" => ?column_indexes + debug!( + block = ?self.block_root, + ?column_indexes, + "Sending data_column for verification" ); if let Err(e) = beacon_processor.send_rpc_validate_data_columns( self.block_root, @@ -375,20 +405,21 @@ impl ActiveSamplingRequest { // Beacon processor is overloaded, drop sampling attempt. Failing to sample // is not a permanent state so we should recover once the node has capacity // and receives a descendant block. - error!(self.log, - "Dropping sampling"; - "block" => %self.block_root, - "reason" => e.to_string() + error!( + block = %self.block_root, + reason = e.to_string(), + "Dropping sampling" ); return Err(SamplingError::SendFailed("beacon processor send failure")); } } } Err(err) => { - debug!(self.log, "Sample download error"; - "block_root" => %self.block_root, - "column_indexes" => ?column_indexes, - "error" => ?err + debug!( + block_root = %self.block_root, + ?column_indexes, + error = ?err, + "Sample download error" ); metrics::inc_counter_vec(&metrics::SAMPLE_DOWNLOAD_RESULT, &[metrics::FAILURE]); @@ -396,10 +427,10 @@ impl ActiveSamplingRequest { // reaching this function. Mark the peer as failed and try again with another. for column_index in column_indexes { let Some(request) = self.column_requests.get_mut(column_index) else { - warn!(self.log, - "Active column sample request not found"; - "block_root" => %self.block_root, - "column_index" => column_index + warn!( + block_root = %self.block_root, + column_index, + "Active column sample request not found" ); continue; }; @@ -429,21 +460,24 @@ impl ActiveSamplingRequest { .column_indexes_by_sampling_request .get(&sampling_request_id) else { - error!(self.log, "Column indexes for the sampling request ID not found"; "sampling_request_id" => ?sampling_request_id); + error!( + ?sampling_request_id, + "Column indexes for the sampling request ID not found" + ); return Ok(None); }; match result { Ok(_) => { - debug!(self.log, "Sample verification success"; "block_root" => %self.block_root, "column_indexes" => ?column_indexes); + debug!(block_root = %self.block_root,?column_indexes, "Sample verification success"); metrics::inc_counter_vec(&metrics::SAMPLE_VERIFY_RESULT, &[metrics::SUCCESS]); // Valid, continue_sampling will maybe consider sampling succees for column_index in column_indexes { let Some(request) = self.column_requests.get_mut(column_index) else { warn!( - self.log, - "Active column sample request not found"; "block_root" => %self.block_root, "column_index" => column_index + block_root = %self.block_root, column_index, + "Active column sample request not found" ); continue; }; @@ -451,7 +485,7 @@ impl ActiveSamplingRequest { } } Err(err) => { - debug!(self.log, "Sample verification failure"; "block_root" => %self.block_root, "column_indexes" => ?column_indexes, "reason" => ?err); + debug!(block_root = %self.block_root, ?column_indexes, reason = ?err, "Sample verification failure"); metrics::inc_counter_vec(&metrics::SAMPLE_VERIFY_RESULT, &[metrics::FAILURE]); // Peer sent invalid data, penalize and try again from different peer @@ -459,8 +493,9 @@ impl ActiveSamplingRequest { for column_index in column_indexes { let Some(request) = self.column_requests.get_mut(column_index) else { warn!( - self.log, - "Active column sample request not found"; "block_root" => %self.block_root, "column_index" => column_index + block_root = %self.block_root, + column_index, + "Active column sample request not found" ); continue; }; @@ -570,7 +605,7 @@ impl ActiveSamplingRequest { // request was sent, loop to increase the required_successes until the sampling fails if // there are no peers. if ongoings == 0 && !sent_request { - debug!(self.log, "Sampling request stalled"; "block_root" => %self.block_root); + debug!(block_root = %self.block_root, "Sampling request stalled"); } Ok(None) diff --git a/beacon_node/network/src/sync/range_sync/batch.rs b/beacon_node/network/src/sync/range_sync/batch.rs index 912287a8a4c..264f83ee820 100644 --- a/beacon_node/network/src/sync/range_sync/batch.rs +++ b/beacon_node/network/src/sync/range_sync/batch.rs @@ -3,6 +3,7 @@ use lighthouse_network::rpc::methods::BlocksByRangeRequest; use lighthouse_network::service::api_types::Id; use lighthouse_network::PeerId; use std::collections::HashSet; +use std::fmt; use std::hash::{Hash, Hasher}; use std::ops::Sub; use std::time::{Duration, Instant}; @@ -61,6 +62,7 @@ pub trait BatchConfig { fn batch_attempt_hash(blocks: &[RpcBlock]) -> u64; } +#[derive(Debug)] pub struct RangeSyncBatchConfig {} impl BatchConfig for RangeSyncBatchConfig { @@ -93,6 +95,7 @@ pub enum BatchProcessingResult { NonFaultyFailure, } +#[derive(Debug)] /// A segment of a chain. pub struct BatchInfo { /// Start slot of the batch. @@ -104,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. @@ -113,12 +116,23 @@ pub struct BatchInfo { marker: std::marker::PhantomData, } +impl fmt::Display for BatchInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Start Slot: {}, End Slot: {}, State: {}", + self.start_slot, self.end_slot, self.state + ) + } +} + +#[derive(Display)] /// Current state of a batch 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. @@ -183,36 +197,26 @@ 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 } - /// Return the number of times this batch has failed downloading and failed processing, in this - /// order. - pub fn failed_attempts(&self) -> (usize, usize) { - ( - self.failed_download_attempts.len(), - self.failed_processing_attempts.len(), - ) - } - /// 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"), @@ -271,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) @@ -292,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 { @@ -326,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"), @@ -456,39 +456,6 @@ impl Attempt { } } -impl slog::KV for &mut BatchInfo { - fn serialize( - &self, - record: &slog::Record, - serializer: &mut dyn slog::Serializer, - ) -> slog::Result { - slog::KV::serialize(*self, record, serializer) - } -} - -impl slog::KV for BatchInfo { - fn serialize( - &self, - record: &slog::Record, - serializer: &mut dyn slog::Serializer, - ) -> slog::Result { - use slog::Value; - Value::serialize(&self.start_slot, record, "start_slot", serializer)?; - Value::serialize( - &(self.end_slot - 1), // NOTE: The -1 shows inclusive blocks - record, - "end_slot", - serializer, - )?; - serializer.emit_usize("downloaded", self.failed_download_attempts.len())?; - serializer.emit_usize("processed", self.failed_processing_attempts.len())?; - serializer.emit_u8("processed_no_penalty", self.non_faulty_processing_attempts)?; - serializer.emit_arguments("state", &format_args!("{:?}", self.state))?; - serializer.emit_arguments("batch_ty", &format_args!("{}", self.batch_type))?; - slog::Result::Ok(()) - } -} - impl std::fmt::Debug for BatchState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -505,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 f02262e4b5d..be017344170 100644 --- a/beacon_node/network/src/sync/range_sync/chain.rs +++ b/beacon_node/network/src/sync/range_sync/chain.rs @@ -1,21 +1,17 @@ use super::batch::{BatchInfo, BatchProcessingResult, BatchState}; use super::RangeSyncType; use crate::metrics; -use crate::metrics::PEERS_PER_COLUMN_SUBNET; use crate::network_beacon_processor::ChainSegmentProcessId; -use crate::sync::network_context::RangeRequestId; +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 metrics::set_int_gauge; -use rand::seq::SliceRandom; -use rand::Rng; -use slog::{crit, debug, o, warn}; +use logging::crit; use std::collections::{btree_map::Entry, BTreeMap, HashSet}; use strum::IntoStaticStr; +use tracing::{debug, instrument, warn}; use types::{Epoch, EthSpec, Hash256, Slot}; /// Blocks are downloaded in batches from peers. This constant specifies how many epochs worth of @@ -39,6 +35,7 @@ pub type ProcessingResult = Result; /// Reasons for removing a chain #[derive(Debug)] +#[allow(dead_code)] pub enum RemoveChain { EmptyPeerPool, ChainCompleted, @@ -68,6 +65,7 @@ pub enum SyncingChainType { /// A chain of blocks that need to be downloaded. Peers who claim to contain the target head /// root are grouped into the peer pool and queried for batches when downloading the /// chain. +#[derive(Debug)] pub struct SyncingChain { /// A random id used to identify this chain. id: ChainId, @@ -90,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, @@ -112,9 +110,6 @@ pub struct SyncingChain { /// The current processing batch, if any. current_processing_batch: Option, - - /// The chain's log. - log: slog::Logger, } #[derive(PartialEq, Debug)] @@ -134,11 +129,7 @@ impl SyncingChain { target_head_root: Hash256, peer_id: PeerId, chain_type: SyncingChainType, - log: &slog::Logger, ) -> Self { - let mut peers = FnvHashMap::default(); - peers.insert(peer_id, Default::default()); - SyncingChain { id, chain_type, @@ -146,14 +137,13 @@ 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, attempted_optimistic_starts: HashSet::default(), state: ChainSyncingState::Stopped, current_processing_batch: None, - log: log.new(o!("chain" => id)), } } @@ -163,21 +153,25 @@ impl SyncingChain { } /// Check if the chain has peers from which to process batches. + #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn available_peers(&self) -> usize { self.peers.len() } /// Get the chain's id. - pub fn get_id(&self) -> ChainId { + #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] + pub fn id(&self) -> ChainId { self.id } /// Peers currently syncing this chain. + #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn peers(&self) -> impl Iterator + '_ { - self.peers.keys().cloned() + self.peers.iter().cloned() } /// Progress in epochs made by the chain + #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn processed_epochs(&self) -> u64 { self.processing_target .saturating_sub(self.start_epoch) @@ -185,6 +179,7 @@ impl SyncingChain { } /// Returns the total count of pending blocks in all the batches of this chain + #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn pending_blocks(&self) -> usize { self.batches .values() @@ -194,30 +189,9 @@ impl SyncingChain { /// Removes a peer from the chain. /// If the peer has active batches, those are considered failed and re-requested. - 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!(self.log, "Batch not found while removing peer"; - "peer" => %peer_id, "batch" => id) - } - } - } + #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] + pub fn remove_peer(&mut self, peer_id: &PeerId) -> ProcessingResult { + self.peers.remove(peer_id); if self.peers.is_empty() { Err(RemoveChain::EmptyPeerPool) @@ -227,6 +201,7 @@ impl SyncingChain { } /// Returns the latest slot number that has been processed. + #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] fn current_processed_slot(&self) -> Slot { // the last slot we processed was included in the previous batch, and corresponds to the // first slot of the current target epoch @@ -236,6 +211,7 @@ impl SyncingChain { /// A block has been received for a batch on this chain. /// If the block correctly completes the batch it will be processed if possible. + #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn on_block_response( &mut self, network: &mut SyncNetworkContext, @@ -247,7 +223,7 @@ impl SyncingChain { // check if we have this batch let batch = match self.batches.get_mut(&batch_id) { None => { - debug!(self.log, "Received a block for unknown batch"; "epoch" => batch_id); + debug!(epoch = %batch_id, "Received a block for unknown batch"); // A batch might get removed when the chain advances, so this is non fatal. return Ok(KeepChain); } @@ -267,15 +243,13 @@ 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; - debug!(self.log, "Batch downloaded"; "epoch" => batch_id, "blocks" => received, "batch_state" => self.visualize_batch_state(), "awaiting_batches" => awaiting_batches); + debug!(epoch = %batch_id, blocks = received, batch_state = self.visualize_batch_state(), %awaiting_batches,"Batch downloaded"); // pre-emptively request more blocks from peers whilst we process current blocks, self.request_batches(network)?; @@ -284,6 +258,7 @@ impl SyncingChain { /// Processes the batch with the given id. /// The batch must exist and be ready for processing + #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] fn process_batch( &mut self, network: &mut SyncNetworkContext, @@ -319,8 +294,7 @@ impl SyncingChain { self.current_processing_batch = Some(batch_id); if let Err(e) = beacon_processor.send_chain_segment(process_id, blocks) { - crit!(self.log, "Failed to send chain segment to processor."; "msg" => "process_batch", - "error" => %e, "batch" => self.processing_target); + crit!(msg = "process_batch",error = %e, batch = ?self.processing_target, "Failed to send chain segment to processor."); // This is unlikely to happen but it would stall syncing since the batch now has no // blocks to continue, and the chain is expecting a processing result that won't // arrive. To mitigate this, (fake) fail this processing so that the batch is @@ -332,6 +306,7 @@ impl SyncingChain { } /// Processes the next ready batch, prioritizing optimistic batches over the processing target. + #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] fn process_completed_batches( &mut self, network: &mut SyncNetworkContext, @@ -351,7 +326,7 @@ impl SyncingChain { match state { BatchState::AwaitingProcessing(..) => { // this batch is ready - debug!(self.log, "Processing optimistic start"; "epoch" => epoch); + debug!(%epoch, "Processing optimistic start"); return self.process_batch(network, epoch); } BatchState::Downloading(..) => { @@ -379,7 +354,7 @@ impl SyncingChain { // batch has been requested and processed we can land here. We drop the // optimistic candidate since we can't conclude whether the batch included // blocks or not at this point - debug!(self.log, "Dropping optimistic candidate"; "batch" => epoch); + debug!(batch = %epoch, "Dropping optimistic candidate"); self.optimistic_start = None; } } @@ -413,7 +388,10 @@ impl SyncingChain { // inside the download buffer (between `self.processing_target` and // `self.to_be_downloaded`). In this case, eventually the chain advances to the // batch (`self.processing_target` reaches this point). - debug!(self.log, "Chain encountered a robust batch awaiting validation"; "batch" => self.processing_target); + debug!( + batch = %self.processing_target, + "Chain encountered a robust batch awaiting validation" + ); self.processing_target += EPOCHS_PER_BATCH; if self.to_be_downloaded <= self.processing_target { @@ -438,6 +416,7 @@ impl SyncingChain { /// The block processor has completed processing a batch. This function handles the result /// of the batch processor. + #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn on_batch_process_result( &mut self, network: &mut SyncNetworkContext, @@ -449,13 +428,11 @@ impl SyncingChain { let batch_state = self.visualize_batch_state(); let batch = match &self.current_processing_batch { Some(processing_id) if *processing_id != batch_id => { - debug!(self.log, "Unexpected batch result"; - "batch_epoch" => batch_id, "expected_batch_epoch" => processing_id); + debug!(batch_epoch = %batch_id, expected_batch_epoch = %processing_id,"Unexpected batch result"); return Ok(KeepChain); } None => { - debug!(self.log, "Chain was not expecting a batch result"; - "batch_epoch" => batch_id); + debug!(batch_epoch = %batch_id,"Chain was not expecting a batch result"); return Ok(KeepChain); } _ => { @@ -470,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(), @@ -478,8 +455,14 @@ impl SyncingChain { })?; // Log the process result and the batch for debugging purposes. - debug!(self.log, "Batch processing result"; "result" => ?result, &batch, - "batch_epoch" => batch_id, "client" => %network.client_type(&peer), "batch_state" => batch_state); + debug!( + result = ?result, + batch_epoch = %batch_id, + client = %network.client_type(&peer), + batch_state = ?batch_state, + ?batch, + "Batch processing result" + ); // We consider three cases. Batch was successfully processed, Batch failed processing due // to a faulty peer, or batch failed processing but the peer can't be deemed faulty. @@ -565,13 +548,12 @@ impl SyncingChain { // There are some edge cases with forks that could land us in this situation. // This should be unlikely, so we tolerate these errors, but not often. warn!( - self.log, - "Batch failed to download. Dropping chain scoring peers"; - "score_adjustment" => %penalty, - "batch_epoch"=> batch_id, + score_adjustment = %penalty, + batch_epoch = %batch_id, + "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 { @@ -584,11 +566,12 @@ 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) } } } + #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] fn reject_optimistic_batch( &mut self, network: &mut SyncNetworkContext, @@ -601,13 +584,13 @@ impl SyncingChain { // it. NOTE: this is done to prevent non-sequential batches coming from optimistic // starts from filling up the buffer size if epoch < self.to_be_downloaded { - debug!(self.log, "Rejected optimistic batch left for future use"; "epoch" => %epoch, "reason" => reason); + 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!(self.log, "Rejected optimistic batch"; "epoch" => %epoch, "reason" => reason); + debug!(%epoch, reason, "Rejected optimistic batch"); self.batches.remove(&epoch); } } @@ -623,6 +606,7 @@ impl SyncingChain { /// If a previous batch has been validated and it had been re-processed, penalize the original /// peer. #[allow(clippy::modulo_one)] + #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] fn advance_chain(&mut self, network: &mut SyncNetworkContext, validating_epoch: Epoch) { // make sure this epoch produces an advancement if validating_epoch <= self.start_epoch { @@ -631,7 +615,7 @@ impl SyncingChain { // safety check for batch boundaries if validating_epoch % EPOCHS_PER_BATCH != self.start_epoch % EPOCHS_PER_BATCH { - crit!(self.log, "Validating Epoch is not aligned"); + crit!("Validating Epoch is not aligned"); return; } @@ -653,9 +637,10 @@ impl SyncingChain { // A different peer sent the correct batch, the previous peer did not // We negatively score the original peer. let action = PeerAction::LowToleranceError; - debug!(self.log, "Re-processed batch validated. Scoring original peer"; - "batch_epoch" => id, "score_adjustment" => %action, - "original_peer" => %attempt.peer_id, "new_peer" => %processed_attempt.peer_id + debug!( + batch_epoch = %id, score_adjustment = %action, + original_peer = %attempt.peer_id, new_peer = %processed_attempt.peer_id, + "Re-processed batch validated. Scoring original peer" ); network.report_peer( attempt.peer_id, @@ -666,9 +651,12 @@ impl SyncingChain { // The same peer corrected it's previous mistake. There was an error, so we // negative score the original peer. let action = PeerAction::MidToleranceError; - debug!(self.log, "Re-processed batch validated by the same peer"; - "batch_epoch" => id, "score_adjustment" => %action, - "original_peer" => %attempt.peer_id, "new_peer" => %processed_attempt.peer_id + debug!( + batch_epoch = %id, + score_adjustment = %action, + original_peer = %attempt.peer_id, + new_peer = %processed_attempt.peer_id, + "Re-processed batch validated by the same peer" ); network.report_peer( attempt.peer_id, @@ -679,19 +667,13 @@ 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") } - BatchState::Failed | BatchState::Poisoned | BatchState::AwaitingDownload => crit!( - self.log, - "batch indicates inconsistent chain state while advancing chain" - ), BatchState::AwaitingProcessing(..) => {} BatchState::Processing(_) => { - debug!(self.log, "Advancing chain while processing a batch"; "batch" => id, batch); + debug!(batch = %id, %batch, "Advancing chain while processing a batch"); if let Some(processing_id) = self.current_processing_batch { if id <= processing_id { self.current_processing_batch = None; @@ -715,8 +697,12 @@ impl SyncingChain { self.optimistic_start = None; } } - debug!(self.log, "Chain advanced"; "previous_start" => old_start, - "new_start" => self.start_epoch, "processing_target" => self.processing_target); + debug!( + previous_start = %old_start, + new_start = %self.start_epoch, + processing_target = %self.processing_target, + "Chain advanced" + ); } /// An invalid batch has been received that could not be processed, but that can be retried. @@ -724,6 +710,7 @@ impl SyncingChain { /// These events occur when a peer has successfully responded with blocks, but the blocks we /// have received are incorrect or invalid. This indicates the peer has not performed as /// intended and can result in downvoting a peer. + #[instrument(parent = None,level = "info", fields(service = self.id, network), skip_all)] fn handle_invalid_batch( &mut self, network: &mut SyncNetworkContext, @@ -769,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) { @@ -783,6 +770,7 @@ impl SyncingChain { /// This chain has been requested to start syncing. /// /// This could be new chain, or an old chain that is being resumed. + #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn start_syncing( &mut self, network: &mut SyncNetworkContext, @@ -821,29 +809,27 @@ impl SyncingChain { /// Add a peer to the chain. /// /// If the chain is active, this starts requesting batches from this peer. + #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn add_peer( &mut self, network: &mut SyncNetworkContext, peer_id: PeerId, ) -> ProcessingResult { - // 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. /// /// If the batch exists it is re-requested. + #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn inject_error( &mut self, network: &mut SyncNetworkContext, batch_id: BatchId, peer_id: &PeerId, request_id: Id, + err: RpcResponseError, ) -> ProcessingResult { let batch_state = self.visualize_batch_state(); if let Some(batch) = self.batches.get_mut(&batch_id) { @@ -854,152 +840,118 @@ impl SyncingChain { // columns. if !batch.is_expecting_block(&request_id) { debug!( - self.log, - "Batch not expecting block"; - "batch_epoch" => batch_id, - "batch_state" => ?batch.state(), - "peer_id" => %peer_id, - "request_id" => %request_id, - "batch_state" => batch_state + batch_epoch = %batch_id, + batch_state = ?batch.state(), + %peer_id, + %request_id, + ?batch_state, + "Batch not expecting block" ); return Ok(KeepChain); } debug!( - self.log, - "Batch failed. RPC Error"; - "batch_epoch" => batch_id, - "batch_state" => ?batch.state(), - "peer_id" => %peer_id, - "request_id" => %request_id, - "batch_state" => batch_state + batch_epoch = %batch_id, + batch_state = ?batch.state(), + error = ?err, + %peer_id, + %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!( - self.log, - "Batch not found"; - "batch_epoch" => batch_id, - "peer_id" => %peer_id, - "request_id" => %request_id, - "batch_state" => batch_state + batch_epoch = %batch_id, + %peer_id, + %request_id, + batch_state, + "Batch not found" ); // this could be an error for an old batch, removed when the chain advances Ok(KeepChain) } } - /// Sends and registers the request of a batch awaiting download. - 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) .unwrap_or(false) { - debug!(self.log, "Requesting optimistic batch"; "epoch" => batch_id, &batch, "batch_state" => batch_state); + debug!(epoch = %batch_id, %batch, %batch_state, "Requesting optimistic batch"); } else { - debug!(self.log, "Requesting batch"; "epoch" => batch_id, &batch, "batch_state" => batch_state); + 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!(self.log, "Could not send batch request"; - "batch_id" => batch_id, "error" => ?e, &batch); - // 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) + } } } - } + }, } } @@ -1007,6 +959,7 @@ impl SyncingChain { } /// Returns true if this chain is currently syncing. + #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn is_syncing(&self) -> bool { match self.state { ChainSyncingState::Syncing => true, @@ -1016,6 +969,7 @@ impl SyncingChain { /// Kickstarts the chain by sending for processing batches that are ready and requesting more /// batches if needed. + #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn resume( &mut self, network: &mut SyncNetworkContext, @@ -1028,6 +982,7 @@ impl SyncingChain { /// Attempts to request the next required batches from the peer pool if the chain is syncing. It will exhaust the peer /// pool and left over batches until the batch buffer is reached or all peers are exhausted. + #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] fn request_batches(&mut self, network: &mut SyncNetworkContext) -> ProcessingResult { if !matches!(self.state, ChainSyncingState::Syncing) { return Ok(KeepChain); @@ -1035,53 +990,34 @@ 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 { if !self.good_peers_on_sampling_subnets(epoch, network) { - debug!( - self.log, - "Waiting for peers to be available on sampling column subnets" - ); + debug!("Waiting for peers to be available on sampling column subnets"); return Ok(KeepChain); } 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) } @@ -1106,11 +1042,6 @@ impl SyncingChain { .good_custody_subnet_peer(*subnet_id) .count(); - set_int_gauge( - &PEERS_PER_COLUMN_SUBNET, - &[&subnet_id.to_string()], - peer_count as i64, - ); peer_count > 0 }); peers_on_all_custody_subnets @@ -1121,6 +1052,7 @@ impl SyncingChain { /// Creates the next required batch from the chain. If there are no more batches required, /// `false` is returned. + #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] fn include_next_batch(&mut self, network: &mut SyncNetworkContext) -> Option { // don't request batches beyond the target head slot if self @@ -1130,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. @@ -1154,26 +1087,24 @@ impl SyncingChain { // block and data column requests are currently coupled. This can be removed once we find a // way to decouple the requests and do retries individually, see issue #6258. if !self.good_peers_on_sampling_subnets(self.to_be_downloaded, network) { - debug!( - self.log, - "Waiting for peers to be available on custody column subnets" - ); + debug!("Waiting for peers to be available on custody column subnets"); 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) } } } @@ -1184,6 +1115,7 @@ impl SyncingChain { /// This produces a string of the form: [D,E,E,E,E] /// to indicate the current buffer state of the chain. The symbols are defined on each of the /// batch states. See [BatchState::visualize] for symbol definitions. + #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] fn visualize_batch_state(&self) -> String { let mut visualization_string = String::with_capacity((BATCH_BUFFER_SIZE * 3) as usize); @@ -1219,45 +1151,6 @@ impl SyncingChain { } } -impl slog::KV for &mut SyncingChain { - fn serialize( - &self, - record: &slog::Record, - serializer: &mut dyn slog::Serializer, - ) -> slog::Result { - slog::KV::serialize(*self, record, serializer) - } -} - -impl slog::KV for SyncingChain { - fn serialize( - &self, - record: &slog::Record, - serializer: &mut dyn slog::Serializer, - ) -> slog::Result { - use slog::Value; - serializer.emit_u32("id", self.id)?; - Value::serialize(&self.start_epoch, record, "from", serializer)?; - Value::serialize( - &self.target_head_slot.epoch(T::EthSpec::slots_per_epoch()), - record, - "to", - serializer, - )?; - serializer.emit_arguments("end_root", &format_args!("{}", self.target_head_root))?; - Value::serialize( - &self.processing_target, - record, - "current_target", - serializer, - )?; - serializer.emit_usize("batches", self.batches.len())?; - serializer.emit_usize("peers", self.peers.len())?; - serializer.emit_arguments("state", &format_args!("{:?}", self.state))?; - slog::Result::Ok(()) - } -} - use super::batch::WrongState as WrongBatchState; impl From for RemoveChain { fn from(err: WrongBatchState) -> Self { 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 15bdf85e203..9f500c61e0b 100644 --- a/beacon_node/network/src/sync/range_sync/chain_collection.rs +++ b/beacon_node/network/src/sync/range_sync/chain_collection.rs @@ -12,11 +12,12 @@ use fnv::FnvHashMap; use lighthouse_network::service::api_types::Id; use lighthouse_network::PeerId; use lighthouse_network::SyncInfo; -use slog::{crit, debug, error}; +use logging::crit; use smallvec::SmallVec; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::sync::Arc; +use tracing::{debug, error}; use types::EthSpec; use types::{Epoch, Hash256, Slot}; @@ -50,18 +51,15 @@ pub struct ChainCollection { head_chains: FnvHashMap>, /// The current sync state of the process. state: RangeSyncState, - /// Logger for the collection. - log: slog::Logger, } impl ChainCollection { - pub fn new(beacon_chain: Arc>, log: slog::Logger) -> Self { + pub fn new(beacon_chain: Arc>) -> Self { ChainCollection { beacon_chain, finalized_chains: FnvHashMap::default(), head_chains: FnvHashMap::default(), state: RangeSyncState::Idle, - log, } } @@ -295,9 +293,8 @@ impl ChainCollection { .expect("Chain exists"); match old_id { - Some(Some(old_id)) => debug!(self.log, "Switching finalized chains"; - "old_id" => old_id, &chain), - None => debug!(self.log, "Syncing new finalized chain"; &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. } @@ -309,10 +306,10 @@ impl ChainCollection { if let Err(remove_reason) = chain.start_syncing(network, local_epoch, local_head_epoch) { if remove_reason.is_critical() { - crit!(self.log, "Chain removed while switching chains"; "chain" => new_id, "reason" => ?remove_reason); + crit!(chain = new_id, reason = ?remove_reason, "Chain removed while switching chains"); } else { // this happens only if sending a batch over the `network` fails a lot - error!(self.log, "Chain removed while switching chains"; "chain" => new_id, "reason" => ?remove_reason); + error!(chain = new_id, reason = ?remove_reason, "Chain removed while switching chains"); } self.finalized_chains.remove(&new_id); self.on_chain_removed(&new_id, true, RangeSyncType::Finalized); @@ -330,7 +327,7 @@ impl ChainCollection { ) { // Include the awaiting head peers for (peer_id, peer_sync_info) in awaiting_head_peers.drain() { - debug!(self.log, "including head peer"); + debug!("including head peer"); self.add_peer_or_create_chain( local_epoch, peer_sync_info.head_root, @@ -362,16 +359,16 @@ impl ChainCollection { if syncing_chains.len() < PARALLEL_HEAD_CHAINS { // start this chain if it's not already syncing if !chain.is_syncing() { - debug!(self.log, "New head chain started syncing"; &chain); + debug!(id = chain.id(), "New head chain started syncing"); } if let Err(remove_reason) = chain.start_syncing(network, local_epoch, local_head_epoch) { self.head_chains.remove(&id); if remove_reason.is_critical() { - crit!(self.log, "Chain removed while switching head chains"; "chain" => id, "reason" => ?remove_reason); + crit!(chain = id, reason = ?remove_reason, "Chain removed while switching head chains"); } else { - error!(self.log, "Chain removed while switching head chains"; "chain" => id, "reason" => ?remove_reason); + error!(chain = id, reason = ?remove_reason, "Chain removed while switching head chains"); } } else { syncing_chains.push(id); @@ -407,7 +404,6 @@ impl ChainCollection { .start_slot(T::EthSpec::slots_per_epoch()); let beacon_chain = &self.beacon_chain; - let log_ref = &self.log; let is_outdated = |target_slot: &Slot, target_root: &Hash256| { target_slot <= &local_finalized_slot @@ -425,7 +421,7 @@ impl ChainCollection { if is_outdated(&chain.target_head_slot, &chain.target_head_root) || chain.available_peers() == 0 { - debug!(log_ref, "Purging out of finalized chain"; &chain); + debug!(id, "Purging out of finalized chain"); Some((*id, chain.is_syncing(), RangeSyncType::Finalized)) } else { None @@ -436,7 +432,7 @@ impl ChainCollection { if is_outdated(&chain.target_head_slot, &chain.target_head_root) || chain.available_peers() == 0 { - debug!(log_ref, "Purging out of date head chain"; &chain); + debug!(id, "Purging out of date head chain"); Some((*id, chain.is_syncing(), RangeSyncType::Head)) } else { None @@ -477,14 +473,14 @@ impl ChainCollection { .find(|(_, chain)| chain.has_same_target(target_head_slot, target_head_root)) { Some((&id, chain)) => { - debug!(self.log, "Adding peer to known chain"; "peer_id" => %peer, "sync_type" => ?sync_type, &chain); + debug!(peer_id = %peer, ?sync_type, id, "Adding peer to known chain"); debug_assert_eq!(chain.target_head_root, target_head_root); 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!(self.log, "Chain removed after adding peer"; "chain" => id, "reason" => ?remove_reason); + crit!(id, reason = ?remove_reason, "Chain removed after adding peer"); } else { - error!(self.log, "Chain removed after adding peer"; "chain" => id, "reason" => ?remove_reason); + error!(id, reason = ?remove_reason, "Chain removed after adding peer"); } let is_syncing = chain.is_syncing(); collection.remove(&id); @@ -501,9 +497,17 @@ impl ChainCollection { target_head_root, peer, sync_type.into(), - &self.log, ); - debug!(self.log, "New chain added to sync"; "peer_id" => peer_rpr, "sync_type" => ?sync_type, &new_chain); + + 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 78679403bb4..1ec14409916 100644 --- a/beacon_node/network/src/sync/range_sync/range.rs +++ b/beacon_node/network/src/sync/range_sync/range.rs @@ -44,17 +44,18 @@ use super::chain_collection::{ChainCollection, SyncChainStatus}; use super::sync_type::RangeSyncType; use crate::metrics; use crate::status::ToStatusMessage; -use crate::sync::network_context::SyncNetworkContext; +use crate::sync::network_context::{RpcResponseError, SyncNetworkContext}; use crate::sync::BatchProcessResult; use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::{BeaconChain, BeaconChainTypes}; use lighthouse_network::rpc::GoodbyeReason; use lighthouse_network::service::api_types::Id; use lighthouse_network::{PeerId, SyncInfo}; +use logging::crit; use lru_cache::LRUTimeCache; -use slog::{crit, debug, trace, warn}; use std::collections::HashMap; use std::sync::Arc; +use tracing::{debug, instrument, trace, warn}; use types::{Epoch, EthSpec, Hash256}; /// For how long we store failed finalized chains to prevent retries. @@ -74,26 +75,40 @@ pub struct RangeSync { chains: ChainCollection, /// Chains that have failed and are stored to prevent being retried. failed_chains: LRUTimeCache, - /// The syncing logger. - log: slog::Logger, } impl RangeSync where T: BeaconChainTypes, { - pub fn new(beacon_chain: Arc>, log: slog::Logger) -> Self { + #[instrument(parent = None, + level = "info", + fields(component = "range_sync"), + name = "range_sync", + skip_all + )] + pub fn new(beacon_chain: Arc>) -> Self { RangeSync { beacon_chain: beacon_chain.clone(), - chains: ChainCollection::new(beacon_chain, log.clone()), + chains: ChainCollection::new(beacon_chain), failed_chains: LRUTimeCache::new(std::time::Duration::from_secs( FAILED_CHAINS_EXPIRY_SECONDS, )), awaiting_head_peers: HashMap::new(), - log, } } + #[cfg(test)] + pub(crate) fn __failed_chains(&mut self) -> Vec { + self.failed_chains.keys().copied().collect() + } + + #[instrument(parent = None, + level = "info", + fields(component = "range_sync"), + name = "range_sync", + skip_all + )] pub fn state(&self) -> SyncChainStatus { self.chains.state() } @@ -103,6 +118,12 @@ where /// may need to be synced as a result. A new peer, may increase the peer pool of a finalized /// chain, this may result in a different finalized chain from syncing as finalized chains are /// prioritised by peer-pool size. + #[instrument(parent = None, + level = "info", + fields(component = "range_sync"), + name = "range_sync", + skip_all + )] pub fn add_peer( &mut self, network: &mut SyncNetworkContext, @@ -128,14 +149,13 @@ where RangeSyncType::Finalized => { // Make sure we have not recently tried this chain if self.failed_chains.contains(&remote_info.finalized_root) { - debug!(self.log, "Disconnecting peer that belongs to previously failed chain"; - "failed_root" => %remote_info.finalized_root, "peer_id" => %peer_id); + debug!(failed_root = ?remote_info.finalized_root, %peer_id,"Disconnecting peer that belongs to previously failed chain"); network.goodbye_peer(peer_id, GoodbyeReason::IrrelevantNetwork); return; } // Finalized chain search - debug!(self.log, "Finalization sync peer joined"; "peer_id" => %peer_id); + debug!(%peer_id, "Finalization sync peer joined"); self.awaiting_head_peers.remove(&peer_id); // Because of our change in finalized sync batch size from 2 to 1 and our transition @@ -166,8 +186,7 @@ where if self.chains.is_finalizing_sync() { // If there are finalized chains to sync, finish these first, before syncing head // chains. - trace!(self.log, "Waiting for finalized sync to complete"; - "peer_id" => %peer_id, "awaiting_head_peers" => &self.awaiting_head_peers.len()); + trace!(%peer_id, awaiting_head_peers = &self.awaiting_head_peers.len(),"Waiting for finalized sync to complete"); self.awaiting_head_peers.insert(peer_id, remote_info); return; } @@ -199,6 +218,12 @@ where /// /// This function finds the chain that made this request. Once found, processes the result. /// This request could complete a chain or simply add to its progress. + #[instrument(parent = None, + level = "info", + fields(component = "range_sync"), + name = "range_sync", + skip_all + )] pub fn blocks_by_range_response( &mut self, network: &mut SyncNetworkContext, @@ -224,11 +249,17 @@ where } } Err(_) => { - trace!(self.log, "BlocksByRange response for removed chain"; "chain" => chain_id) + trace!(%chain_id, "BlocksByRange response for removed chain") } } } + #[instrument(parent = None, + level = "info", + fields(component = "range_sync"), + name = "range_sync", + skip_all + )] pub fn handle_block_process_result( &mut self, network: &mut SyncNetworkContext, @@ -254,13 +285,19 @@ where } Err(_) => { - trace!(self.log, "BlocksByRange response for removed chain"; "chain" => chain_id) + trace!(%chain_id, "BlocksByRange response for removed chain") } } } /// A peer has disconnected. This removes the peer from any ongoing chains and mappings. A /// disconnected peer could remove a chain + #[instrument(parent = None, + level = "info", + fields(component = "range_sync"), + name = "range_sync", + skip_all + )] pub fn peer_disconnect(&mut self, network: &mut SyncNetworkContext, peer_id: &PeerId) { // if the peer is in the awaiting head mapping, remove it self.awaiting_head_peers.remove(peer_id); @@ -273,10 +310,15 @@ where /// which pool the peer is in. The chain may also have a batch or batches awaiting /// for this peer. If so we mark the batch as failed. The batch may then hit it's maximum /// retries. In this case, we need to remove the chain. + #[instrument(parent = None, + level = "info", + fields(component = "range_sync"), + name = "range_sync", + skip_all + )] fn remove_peer(&mut self, network: &mut SyncNetworkContext, peer_id: &PeerId) { - for (removed_chain, sync_type, remove_reason) in self - .chains - .call_all(|chain| chain.remove_peer(peer_id, 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, @@ -292,6 +334,12 @@ where /// /// Check to see if the request corresponds to a pending batch. If so, re-request it if possible, if there have /// been too many failed attempts for the batch, remove the chain. + #[instrument(parent = None, + level = "info", + fields(component = "range_sync"), + name = "range_sync", + skip_all + )] pub fn inject_error( &mut self, network: &mut SyncNetworkContext, @@ -299,10 +347,11 @@ where batch_id: BatchId, chain_id: ChainId, request_id: Id, + err: RpcResponseError, ) { // check that this request is pending match self.chains.call_by_id(chain_id, |chain| { - chain.inject_error(network, batch_id, &peer_id, request_id) + chain.inject_error(network, batch_id, &peer_id, request_id, err) }) { Ok((removed_chain, sync_type)) => { if let Some((removed_chain, remove_reason)) = removed_chain { @@ -316,11 +365,17 @@ where } } Err(_) => { - trace!(self.log, "BlocksByRange response for removed chain"; "chain" => chain_id) + trace!(%chain_id, "BlocksByRange response for removed chain") } } } + #[instrument(parent = None, + level = "info", + fields(component = "range_sync"), + name = "range_sync", + skip_all + )] fn on_chain_removed( &mut self, chain: SyncingChain, @@ -330,14 +385,18 @@ where op: &'static str, ) { if remove_reason.is_critical() { - crit!(self.log, "Chain removed"; "sync_type" => ?sync_type, &chain, "reason" => ?remove_reason, "op" => op); + crit!(id = chain.id(), ?sync_type, reason = ?remove_reason, op, "Chain removed"); } else { - debug!(self.log, "Chain removed"; "sync_type" => ?sync_type, &chain, "reason" => ?remove_reason, "op" => op); + 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!(self.log, "Chain failed! Syncing to its head won't be retried for at least the next {} seconds", FAILED_CHAINS_EXPIRY_SECONDS; &chain); + warn!( + id = chain.id(), + "Chain failed! Syncing to its head won't be retried for at least the next {} seconds", + FAILED_CHAINS_EXPIRY_SECONDS + ); self.failed_chains.insert(chain.target_head_root); } } @@ -364,6 +423,12 @@ where } /// Kickstarts sync. + #[instrument(parent = None, + level = "info", + fields(component = "range_sync"), + name = "range_sync", + skip_all + )] pub fn resume(&mut self, network: &mut SyncNetworkContext) { for (removed_chain, sync_type, remove_reason) in self.chains.call_all(|chain| chain.resume(network)) diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index 271b2322faf..565d7bc9f81 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -19,14 +19,15 @@ use beacon_chain::{ block_verification_types::{AsBlock, BlockImportData}, data_availability_checker::Availability, test_utils::{ - build_log, generate_rand_block_and_blobs, generate_rand_block_and_data_columns, test_spec, - BeaconChainHarness, EphemeralHarnessType, LoggerType, NumBlobs, + generate_rand_block_and_blobs, generate_rand_block_and_data_columns, test_spec, + BeaconChainHarness, EphemeralHarnessType, NumBlobs, }, validator_monitor::timestamp_now, AvailabilityPendingExecutedBlock, AvailabilityProcessingStatus, BlockError, PayloadVerificationOutcome, PayloadVerificationStatus, }; use beacon_processor::WorkEvent; +use lighthouse_network::discovery::CombinedKey; use lighthouse_network::{ rpc::{RPCError, RequestType, RpcErrorResponse}, service::api_types::{ @@ -36,42 +37,30 @@ use lighthouse_network::{ types::SyncState, NetworkConfig, NetworkGlobals, PeerId, }; -use slog::info; use slot_clock::{SlotClock, TestingSlotClock}; use tokio::sync::mpsc; -use types::ForkContext; +use tracing::info; use types::{ data_column_sidecar::ColumnIndex, test_utils::{SeedableRng, TestRandom, XorShiftRng}, - BeaconState, BeaconStateBase, BlobSidecar, DataColumnSidecar, EthSpec, ForkName, Hash256, - MinimalEthSpec as E, SignedBeaconBlock, Slot, + BeaconState, BeaconStateBase, BlobSidecar, DataColumnSidecar, EthSpec, ForkContext, ForkName, + Hash256, MinimalEthSpec as E, SignedBeaconBlock, Slot, }; const D: Duration = Duration::new(0, 0); const PARENT_FAIL_TOLERANCE: u8 = SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS; const SAMPLING_REQUIRED_SUCCESSES: usize = 2; - type DCByRootIds = Vec; type DCByRootId = (SyncRequestId, Vec); impl TestRig { pub fn test_setup() -> Self { - let logger_type = if cfg!(feature = "test_logger") { - LoggerType::Test - } else if cfg!(feature = "ci_logger") { - LoggerType::CI - } else { - LoggerType::Null - }; - let log = build_log(slog::Level::Trace, logger_type); - // Use `fork_from_env` logic to set correct fork epochs let spec = test_spec::(); // Initialise a new beacon chain let harness = BeaconChainHarness::>::builder(E) .spec(Arc::new(spec)) - .logger(log.clone()) .deterministic_keypairs(1) .fresh_ephemeral_store() .mock_execution_layer() @@ -96,7 +85,6 @@ impl TestRig { let network_config = Arc::new(NetworkConfig::default()); let globals = Arc::new(NetworkGlobals::new_test_globals( Vec::new(), - &log, network_config, chain.spec.clone(), )); @@ -105,7 +93,6 @@ impl TestRig { sync_tx, chain.clone(), harness.runtime.task_executor.clone(), - log.clone(), ); let fork_name = chain.spec.fork_name_at_slot::(chain.slot().unwrap()); @@ -117,7 +104,9 @@ impl TestRig { let spec = chain.spec.clone(); - let rng = XorShiftRng::from_seed([42; 16]); + // deterministic seed + let rng = ChaCha20Rng::from_seed([0u8; 32]); + TestRig { beacon_processor_rx, beacon_processor_rx_queue: vec![], @@ -136,11 +125,9 @@ impl TestRig { required_successes: vec![SAMPLING_REQUIRED_SUCCESSES], }, fork_context, - log.clone(), ), harness, fork_name, - log, spec, } } @@ -154,7 +141,7 @@ impl TestRig { } } - fn test_setup_after_fulu() -> Option { + pub fn test_setup_after_fulu() -> Option { let r = Self::test_setup(); if r.fork_name.fulu_enabled() { Some(r) @@ -164,7 +151,7 @@ impl TestRig { } pub fn log(&self, msg: &str) { - info!(self.log, "TEST_RIG"; "msg" => msg); + info!(msg, "TEST_RIG"); } pub fn after_deneb(&self) -> bool { @@ -369,20 +356,29 @@ impl TestRig { } pub fn new_connected_peer(&mut self) -> PeerId { - self.network_globals + let key = self.determinstic_key(); + let peer_id = self + .network_globals .peers .write() - .__add_connected_peer_testing_only(false, &self.harness.spec) + .__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 { + let key = self.determinstic_key(); self.network_globals .peers .write() - .__add_connected_peer_testing_only(true, &self.harness.spec) + .__add_connected_peer_testing_only(true, &self.harness.spec, key) } - fn new_connected_peers_for_peerdas(&mut self) { + fn determinstic_key(&mut self) -> CombinedKey { + k256::ecdsa::SigningKey::random(&mut self.rng).into() + } + + pub fn new_connected_peers_for_peerdas(&mut self) { // Enough sampling peers with few columns for _ in 0..100 { self.new_connected_peer(); @@ -467,7 +463,7 @@ impl TestRig { ) { self.log("parent_lookup_block_response"); self.send_sync_message(SyncMessage::RpcBlock { - request_id: SyncRequestId::SingleBlock { id }, + sync_request_id: SyncRequestId::SingleBlock { id }, peer_id, beacon_block, seen_timestamp: D, @@ -482,7 +478,7 @@ impl TestRig { ) { self.log("single_lookup_block_response"); self.send_sync_message(SyncMessage::RpcBlock { - request_id: SyncRequestId::SingleBlock { id }, + sync_request_id: SyncRequestId::SingleBlock { id }, peer_id, beacon_block, seen_timestamp: D, @@ -500,7 +496,7 @@ impl TestRig { blob_sidecar.as_ref().map(|b| b.index) )); self.send_sync_message(SyncMessage::RpcBlob { - request_id: SyncRequestId::SingleBlob { id }, + sync_request_id: SyncRequestId::SingleBlob { id }, peer_id, blob_sidecar, seen_timestamp: D, @@ -514,7 +510,7 @@ impl TestRig { blob_sidecar: Option>>, ) { self.send_sync_message(SyncMessage::RpcBlob { - request_id: SyncRequestId::SingleBlob { id }, + sync_request_id: SyncRequestId::SingleBlob { id }, peer_id, blob_sidecar, seen_timestamp: D, @@ -590,7 +586,7 @@ impl TestRig { fn parent_lookup_failed(&mut self, id: SingleLookupReqId, peer_id: PeerId, error: RPCError) { self.send_sync_message(SyncMessage::RpcError { peer_id, - request_id: SyncRequestId::SingleBlock { id }, + sync_request_id: SyncRequestId::SingleBlock { id }, error, }) } @@ -609,7 +605,7 @@ impl TestRig { fn single_lookup_failed(&mut self, id: SingleLookupReqId, peer_id: PeerId, error: RPCError) { self.send_sync_message(SyncMessage::RpcError { peer_id, - request_id: SyncRequestId::SingleBlock { id }, + sync_request_id: SyncRequestId::SingleBlock { id }, error, }) } @@ -621,11 +617,11 @@ impl TestRig { } } - fn return_empty_sampling_request(&mut self, (request_id, _): DCByRootId) { + fn return_empty_sampling_request(&mut self, (sync_request_id, _): DCByRootId) { let peer_id = PeerId::random(); // Send stream termination self.send_sync_message(SyncMessage::RpcDataColumn { - request_id, + sync_request_id, peer_id, data_column: None, seen_timestamp: timestamp_now(), @@ -638,10 +634,10 @@ impl TestRig { peer_id: PeerId, error: RPCError, ) { - for (request_id, _) in sampling_ids { + for (sync_request_id, _) in sampling_ids { self.send_sync_message(SyncMessage::RpcError { peer_id, - request_id, + sync_request_id, error: error.clone(), }) } @@ -706,7 +702,6 @@ impl TestRig { self.complete_data_columns_by_root_request(id, data_columns); // Expect work event - // TODO(das): worth it to append sender id to the work event for stricter assertion? self.expect_rpc_sample_verify_work_event(); // Respond with valid result @@ -748,7 +743,6 @@ impl TestRig { } // Expect work event - // TODO(das): worth it to append sender id to the work event for stricter assertion? self.expect_rpc_custody_column_work_event(); // Respond with valid result @@ -769,14 +763,14 @@ impl TestRig { fn complete_data_columns_by_root_request( &mut self, - (request_id, _): DCByRootId, + (sync_request_id, _): DCByRootId, data_columns: &[Arc>], ) { let peer_id = PeerId::random(); for data_column in data_columns { // Send chunks self.send_sync_message(SyncMessage::RpcDataColumn { - request_id, + sync_request_id, peer_id, data_column: Some(data_column.clone()), seen_timestamp: timestamp_now(), @@ -784,7 +778,7 @@ impl TestRig { } // Send stream termination self.send_sync_message(SyncMessage::RpcDataColumn { - request_id, + sync_request_id, peer_id, data_column: None, seen_timestamp: timestamp_now(), @@ -794,17 +788,17 @@ impl TestRig { /// Return RPCErrors for all active requests of peer fn rpc_error_all_active_requests(&mut self, disconnected_peer_id: PeerId) { self.drain_network_rx(); - while let Ok(request_id) = self.pop_received_network_event(|ev| match ev { + while let Ok(sync_request_id) = self.pop_received_network_event(|ev| match ev { NetworkMessage::SendRequest { peer_id, - request_id: AppRequestId::Sync(id), + app_request_id: AppRequestId::Sync(id), .. } if *peer_id == disconnected_peer_id => Some(*id), _ => None, }) { self.send_sync_message(SyncMessage::RpcError { peer_id: disconnected_peer_id, - request_id, + sync_request_id, error: RPCError::Disconnected, }); } @@ -888,7 +882,7 @@ impl TestRig { NetworkMessage::SendRequest { peer_id: _, request: RequestType::BlocksByRoot(request), - request_id: AppRequestId::Sync(SyncRequestId::SingleBlock { id }), + app_request_id: AppRequestId::Sync(SyncRequestId::SingleBlock { id }), } if request.block_roots().to_vec().contains(&for_block) => Some(*id), _ => None, }) @@ -908,7 +902,7 @@ impl TestRig { NetworkMessage::SendRequest { peer_id: _, request: RequestType::BlobsByRoot(request), - request_id: AppRequestId::Sync(SyncRequestId::SingleBlob { id }), + app_request_id: AppRequestId::Sync(SyncRequestId::SingleBlob { id }), } if request .blob_ids .to_vec() @@ -933,7 +927,7 @@ impl TestRig { NetworkMessage::SendRequest { peer_id: _, request: RequestType::BlocksByRoot(request), - request_id: AppRequestId::Sync(SyncRequestId::SingleBlock { id }), + app_request_id: AppRequestId::Sync(SyncRequestId::SingleBlock { id }), } if request.block_roots().to_vec().contains(&for_block) => Some(*id), _ => None, }) @@ -955,7 +949,7 @@ impl TestRig { NetworkMessage::SendRequest { peer_id: _, request: RequestType::BlobsByRoot(request), - request_id: AppRequestId::Sync(SyncRequestId::SingleBlob { id }), + app_request_id: AppRequestId::Sync(SyncRequestId::SingleBlob { id }), } if request .blob_ids .to_vec() @@ -983,7 +977,8 @@ impl TestRig { NetworkMessage::SendRequest { peer_id: _, request: RequestType::DataColumnsByRoot(request), - request_id: AppRequestId::Sync(id @ SyncRequestId::DataColumnsByRoot { .. }), + app_request_id: + AppRequestId::Sync(id @ SyncRequestId::DataColumnsByRoot { .. }), } if request .data_column_ids .to_vec() @@ -1113,7 +1108,7 @@ impl TestRig { } #[track_caller] - fn expect_empty_network(&mut self) { + pub fn expect_empty_network(&mut self) { self.drain_network_rx(); if !self.network_rx_queue.is_empty() { let n = self.network_rx_queue.len(); @@ -1212,8 +1207,12 @@ impl TestRig { payload_verification_status: PayloadVerificationStatus::Verified, is_valid_merge_transition_block: false, }; - let executed_block = - AvailabilityPendingExecutedBlock::new(block, import_data, payload_verification_outcome); + let executed_block = AvailabilityPendingExecutedBlock::new( + block, + import_data, + payload_verification_outcome, + self.network_globals.custody_columns_count() as usize, + ); match self .harness .chain @@ -2313,11 +2312,6 @@ mod deneb_only { }) } - fn log(self, msg: &str) -> Self { - self.rig.log(msg); - self - } - fn trigger_unknown_block_from_attestation(mut self) -> Self { let block_root = self.block.canonical_root(); self.rig @@ -2621,6 +2615,11 @@ mod deneb_only { .block_imported() } + fn log(self, msg: &str) -> Self { + self.rig.log(msg); + self + } + fn parent_block_then_empty_parent_blobs(self) -> Self { self.log( " Return empty blobs for parent, block errors with missing components, downscore", diff --git a/beacon_node/network/src/sync/tests/mod.rs b/beacon_node/network/src/sync/tests/mod.rs index 6ed5c7f8fab..ec24ddb036a 100644 --- a/beacon_node/network/src/sync/tests/mod.rs +++ b/beacon_node/network/src/sync/tests/mod.rs @@ -7,12 +7,12 @@ use beacon_chain::eth1_chain::CachingEth1Backend; use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; use beacon_processor::WorkEvent; use lighthouse_network::NetworkGlobals; -use slog::Logger; +use rand_chacha::ChaCha20Rng; use slot_clock::ManualSlotClock; use std::sync::Arc; use store::MemoryStore; use tokio::sync::mpsc; -use types::{test_utils::XorShiftRng, ChainSpec, ForkName, MinimalEthSpec as E}; +use types::{ChainSpec, ForkName, MinimalEthSpec as E}; mod lookups; mod range; @@ -61,8 +61,7 @@ struct TestRig { /// Beacon chain harness harness: BeaconChainHarness>, /// `rng` for generating test blocks and blobs. - rng: XorShiftRng, + rng: ChaCha20Rng, fork_name: ForkName, - log: Logger, spec: Arc, } diff --git a/beacon_node/network/src/sync/tests/range.rs b/beacon_node/network/src/sync/tests/range.rs index f78b44308d1..932f485dd0d 100644 --- a/beacon_node/network/src/sync/tests/range.rs +++ b/beacon_node/network/src/sync/tests/range.rs @@ -1,11 +1,14 @@ use super::*; +use crate::network_beacon_processor::ChainSegmentProcessId; use crate::status::ToStatusMessage; use crate::sync::manager::SLOT_IMPORT_TOLERANCE; +use crate::sync::network_context::RangeRequestId; use crate::sync::range_sync::RangeSyncType; use crate::sync::SyncMessage; use beacon_chain::data_column_verification::CustodyDataColumn; use beacon_chain::test_utils::{AttestationStrategy, BlockStrategy}; use beacon_chain::{block_verification_types::RpcBlock, EngineState, NotifyExecutionLayer}; +use beacon_processor::WorkType; use lighthouse_network::rpc::methods::{ BlobsByRangeRequest, DataColumnsByRangeRequest, OldBlocksByRangeRequest, OldBlocksByRangeRequestV2, @@ -18,8 +21,8 @@ use lighthouse_network::service::api_types::{ use lighthouse_network::{PeerId, SyncInfo}; use std::time::Duration; use types::{ - BlobSidecarList, BlockImportSource, EthSpec, Hash256, MinimalEthSpec as E, SignedBeaconBlock, - SignedBeaconBlockHash, Slot, + BlobSidecarList, BlockImportSource, Epoch, EthSpec, Hash256, MinimalEthSpec as E, + SignedBeaconBlock, SignedBeaconBlockHash, Slot, }; const D: Duration = Duration::new(0, 0); @@ -43,7 +46,7 @@ enum ByRangeDataRequestIds { /// To make writting tests succint, the machinery in this testing rig automatically identifies /// _which_ request to complete. Picking the right request is critical for tests to pass, so this /// filter allows better expressivity on the criteria to identify the right request. -#[derive(Default)] +#[derive(Default, Debug, Clone)] struct RequestFilter { peer: Option, epoch: Option, @@ -74,7 +77,7 @@ impl TestRig { /// Produce a head peer with an advanced head fn add_head_peer_with_root(&mut self, head_root: Hash256) -> PeerId { let local_info = self.local_info(); - self.add_peer(SyncInfo { + self.add_random_peer(SyncInfo { head_root, head_slot: local_info.head_slot + 1 + Slot::new(SLOT_IMPORT_TOLERANCE as u64), ..local_info @@ -90,7 +93,7 @@ impl TestRig { fn add_finalized_peer_with_root(&mut self, finalized_root: Hash256) -> PeerId { let local_info = self.local_info(); let finalized_epoch = local_info.finalized_epoch + 2; - self.add_peer(SyncInfo { + self.add_random_peer(SyncInfo { finalized_epoch, finalized_root, head_slot: finalized_epoch.start_slot(E::slots_per_epoch()), @@ -98,6 +101,17 @@ impl TestRig { }) } + fn finalized_remote_info_advanced_by(&self, advanced_epochs: Epoch) -> SyncInfo { + let local_info = self.local_info(); + let finalized_epoch = local_info.finalized_epoch + advanced_epochs; + SyncInfo { + finalized_epoch, + finalized_root: Hash256::random(), + head_slot: finalized_epoch.start_slot(E::slots_per_epoch()), + head_root: Hash256::random(), + } + } + fn local_info(&self) -> SyncInfo { let StatusMessage { fork_digest: _, @@ -114,28 +128,59 @@ impl TestRig { } } - fn add_peer(&mut self, remote_info: SyncInfo) -> PeerId { + fn add_random_peer_not_supernode(&mut self, remote_info: SyncInfo) -> PeerId { + let peer_id = self.new_connected_peer(); + self.send_sync_message(SyncMessage::AddPeer(peer_id, remote_info)); + peer_id + } + + fn add_random_peer(&mut self, remote_info: SyncInfo) -> PeerId { // Create valid peer known to network globals // TODO(fulu): Using supernode peers to ensure we have peer across all column // subnets for syncing. Should add tests connecting to full node peers. let peer_id = self.new_connected_supernode_peer(); // Send peer to sync - self.send_sync_message(SyncMessage::AddPeer(peer_id, remote_info.clone())); + self.send_sync_message(SyncMessage::AddPeer(peer_id, remote_info)); peer_id } + fn add_random_peers(&mut self, remote_info: SyncInfo, count: usize) { + for _ in 0..count { + let peer = self.new_connected_peer(); + self.add_peer(peer, remote_info.clone()); + } + } + + fn add_peer(&mut self, peer: PeerId, remote_info: SyncInfo) { + self.send_sync_message(SyncMessage::AddPeer(peer, remote_info)); + } + fn assert_state(&self, state: RangeSyncType) { assert_eq!( self.sync_manager .range_sync_state() .expect("State is ok") - .expect("Range should be syncing") + .expect("Range should be syncing, there are no chains") .0, state, "not expected range sync state" ); } + fn assert_no_chains_exist(&self) { + if let Some(chain) = self.sync_manager.get_range_sync_chains().unwrap() { + panic!("There still exists a chain {chain:?}"); + } + } + + fn assert_no_failed_chains(&mut self) { + assert_eq!( + self.sync_manager.__range_failed_chains(), + Vec::::new(), + "Expected no failed chains" + ) + } + #[track_caller] fn expect_chain_segments(&mut self, count: usize) { for i in 0..count { @@ -170,7 +215,7 @@ impl TestRig { true }; - let block_req_id = self + let block_req = self .pop_received_network_event(|ev| match ev { NetworkMessage::SendRequest { peer_id, @@ -178,11 +223,13 @@ impl TestRig { RequestType::BlocksByRange(OldBlocksByRangeRequest::V2( OldBlocksByRangeRequestV2 { start_slot, .. }, )), - request_id: AppRequestId::Sync(SyncRequestId::BlocksByRange(id)), + app_request_id: AppRequestId::Sync(SyncRequestId::BlocksByRange(id)), } if filter_f(*peer_id, *start_slot) => Some((*id, *peer_id)), _ => None, }) - .expect("Should have a blocks by range request"); + .unwrap_or_else(|e| { + panic!("Should have a BlocksByRange request, filter {request_filter:?}: {e:?}") + }); let by_range_data_requests = if self.after_fulu() { let mut data_columns_requests = vec![]; @@ -193,14 +240,14 @@ impl TestRig { RequestType::DataColumnsByRange(DataColumnsByRangeRequest { start_slot, .. }), - request_id: AppRequestId::Sync(SyncRequestId::DataColumnsByRange(id)), + app_request_id: AppRequestId::Sync(SyncRequestId::DataColumnsByRange(id)), } if filter_f(*peer_id, *start_slot) => Some((*id, *peer_id)), _ => None, }) { data_columns_requests.push(data_columns_request); } if data_columns_requests.is_empty() { - panic!("Found zero DataColumnsByRange requests"); + panic!("Found zero DataColumnsByRange requests, filter {request_filter:?}"); } ByRangeDataRequestIds::PostPeerDAS(data_columns_requests) } else if self.after_deneb() { @@ -209,20 +256,25 @@ impl TestRig { NetworkMessage::SendRequest { peer_id, request: RequestType::BlobsByRange(BlobsByRangeRequest { start_slot, .. }), - request_id: AppRequestId::Sync(SyncRequestId::BlobsByRange(id)), + app_request_id: AppRequestId::Sync(SyncRequestId::BlobsByRange(id)), } if filter_f(*peer_id, *start_slot) => Some((*id, *peer_id)), _ => None, }) - .expect("Should have a blobs by range request"); + .unwrap_or_else(|e| { + panic!("Should have a blobs by range request, filter {request_filter:?}: {e:?}") + }); ByRangeDataRequestIds::PrePeerDAS(id, peer) } else { ByRangeDataRequestIds::PreDeneb }; - (block_req_id, by_range_data_requests) + (block_req, by_range_data_requests) } - fn find_and_complete_blocks_by_range_request(&mut self, request_filter: RequestFilter) { + fn find_and_complete_blocks_by_range_request( + &mut self, + request_filter: RequestFilter, + ) -> RangeRequestId { let ((blocks_req_id, block_peer), by_range_data_request_ids) = self.find_blocks_by_range_request(request_filter); @@ -231,7 +283,7 @@ impl TestRig { "Completing BlocksByRange request {blocks_req_id:?} with empty stream" )); self.send_sync_message(SyncMessage::RpcBlock { - request_id: SyncRequestId::BlocksByRange(blocks_req_id), + sync_request_id: SyncRequestId::BlocksByRange(blocks_req_id), peer_id: block_peer, beacon_block: None, seen_timestamp: D, @@ -245,7 +297,7 @@ impl TestRig { "Completing BlobsByRange request {id:?} with empty stream" )); self.send_sync_message(SyncMessage::RpcBlob { - request_id: SyncRequestId::BlobsByRange(id), + sync_request_id: SyncRequestId::BlobsByRange(id), peer_id, blob_sidecar: None, seen_timestamp: D, @@ -258,7 +310,7 @@ impl TestRig { "Completing DataColumnsByRange request {id:?} with empty stream" )); self.send_sync_message(SyncMessage::RpcDataColumn { - request_id: SyncRequestId::DataColumnsByRange(id), + sync_request_id: SyncRequestId::DataColumnsByRange(id), peer_id, data_column: None, seen_timestamp: D, @@ -266,6 +318,60 @@ impl TestRig { } } } + + blocks_req_id.parent_request_id.requester + } + + fn find_and_complete_processing_chain_segment(&mut self, id: ChainSegmentProcessId) { + self.pop_received_processor_event(|ev| { + (ev.work_type() == WorkType::ChainSegment).then_some(()) + }) + .unwrap_or_else(|e| panic!("Expected chain segment work event: {e}")); + + self.log(&format!( + "Completing ChainSegment processing work {id:?} with success" + )); + self.send_sync_message(SyncMessage::BatchProcessed { + sync_type: id, + result: crate::sync::BatchProcessResult::Success { + sent_blocks: 8, + imported_blocks: 8, + }, + }); + } + + fn complete_and_process_range_sync_until( + &mut self, + last_epoch: u64, + request_filter: RequestFilter, + ) { + for epoch in 0..last_epoch { + // Note: In this test we can't predict the block peer + let id = + self.find_and_complete_blocks_by_range_request(request_filter.clone().epoch(epoch)); + if let RangeRequestId::RangeSync { batch_id, .. } = id { + assert_eq!(batch_id.as_u64(), epoch, "Unexpected batch_id"); + } else { + panic!("unexpected RangeRequestId {id:?}"); + } + + let id = match id { + RangeRequestId::RangeSync { chain_id, batch_id } => { + ChainSegmentProcessId::RangeBatchId(chain_id, batch_id) + } + RangeRequestId::BackfillSync { batch_id } => { + ChainSegmentProcessId::BackSyncBatchId(batch_id) + } + }; + + self.find_and_complete_processing_chain_segment(id); + if epoch < last_epoch - 1 { + self.assert_state(RangeSyncType::Finalized); + } else { + self.assert_no_chains_exist(); + self.assert_no_failed_chains(); + } + } } async fn create_canonical_block(&mut self) -> (SignedBeaconBlock, Option>) { @@ -343,9 +449,18 @@ fn build_rpc_block( RpcBlock::new(None, block, Some(blobs.clone())).unwrap() } Some(DataSidecars::DataColumns(columns)) => { - RpcBlock::new_with_custody_columns(None, block, columns.clone(), spec).unwrap() + RpcBlock::new_with_custody_columns( + None, + block, + columns.clone(), + // TODO(das): Assumes CGC = max value. Change if we want to do more complex tests + columns.len(), + spec, + ) + .unwrap() } - None => RpcBlock::new_without_blobs(None, block), + // Block has no data, expects zero columns + None => RpcBlock::new_without_blobs(None, block, 0), } } @@ -442,3 +557,65 @@ fn pause_and_resume_on_ee_offline() { // The head chain and finalized chain (2) should be in the processing queue rig.expect_chain_segments(2); } + +/// To attempt to finalize the peer's status finalized checkpoint we synced to its finalized epoch + +/// 2 epochs + 1 slot. +const EXTRA_SYNCED_EPOCHS: u64 = 2 + 1; + +#[test] +fn finalized_sync_enough_global_custody_peers_few_chain_peers() { + // Run for all forks + let mut r = TestRig::test_setup(); + // This test creates enough global custody peers to satisfy column queries but only adds few + // peers to the chain + r.new_connected_peers_for_peerdas(); + + let advanced_epochs: u64 = 2; + let remote_info = r.finalized_remote_info_advanced_by(advanced_epochs.into()); + + // Current priorization only sends batches to idle peers, so we need enough peers for each batch + // TODO: Test this with a single peer in the chain, it should still work + r.add_random_peers( + remote_info, + (advanced_epochs + EXTRA_SYNCED_EPOCHS) as usize, + ); + r.assert_state(RangeSyncType::Finalized); + + let last_epoch = advanced_epochs + EXTRA_SYNCED_EPOCHS; + r.complete_and_process_range_sync_until(last_epoch, filter()); +} + +#[test] +fn finalized_sync_not_enough_custody_peers_on_start() { + let mut r = TestRig::test_setup(); + // Only run post-PeerDAS + if !r.fork_name.fulu_enabled() { + return; + } + + let advanced_epochs: u64 = 2; + let remote_info = r.finalized_remote_info_advanced_by(advanced_epochs.into()); + + // Unikely that the single peer we added has enough columns for us. Tests are determinstic and + // this error should never be hit + r.add_random_peer_not_supernode(remote_info.clone()); + r.assert_state(RangeSyncType::Finalized); + + // Because we don't have enough peers on all columns we haven't sent any request. + // NOTE: There's a small chance that this single peer happens to custody exactly the set we + // expect, in that case the test will fail. Find a way to make the test deterministic. + r.expect_empty_network(); + + // Generate enough peers and supernodes to cover all custody columns + r.new_connected_peers_for_peerdas(); + // Note: not necessary to add this peers to the chain, as we draw from the global pool + // We still need to add enough peers to trigger batch downloads with idle peers. Same issue as + // the test above. + r.add_random_peers( + remote_info, + (advanced_epochs + EXTRA_SYNCED_EPOCHS - 1) as usize, + ); + + let last_epoch = advanced_epochs + EXTRA_SYNCED_EPOCHS; + r.complete_and_process_range_sync_until(last_epoch, filter()); +} diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 5de096b25ff..7d086dcc326 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -1630,6 +1630,31 @@ pub fn cli_app() -> Command { .action(ArgAction::Set) .display_order(0) ) + .arg( + Arg::new("delay-block-publishing") + .long("delay-block-publishing") + .value_name("SECONDS") + .action(ArgAction::Set) + .help_heading(FLAG_HEADER) + .help("TESTING ONLY: Artificially delay block publishing by the specified number of seconds. \ + This only works for if `BroadcastValidation::Gossip` is used (default). \ + DO NOT USE IN PRODUCTION.") + .hide(true) + .display_order(0) + ) + .arg( + Arg::new("delay-data-column-publishing") + .long("delay-data-column-publishing") + .value_name("SECONDS") + .action(ArgAction::Set) + .help_heading(FLAG_HEADER) + .help("TESTING ONLY: Artificially delay data column publishing by the specified number of seconds. \ + Limitation: If `delay-block-publishing` is also used, data columns will be delayed for a \ + minimum of `delay-block-publishing` seconds. + DO NOT USE IN PRODUCTION.") + .hide(true) + .display_order(0) + ) .arg( Arg::new("invalid-block-roots") .long("invalid-block-roots") diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 686843b0000..e887aa9abce 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -18,7 +18,6 @@ use http_api::TlsConfig; use lighthouse_network::ListenAddress; use lighthouse_network::{multiaddr::Protocol, Enr, Multiaddr, NetworkConfig, PeerIdSerialized}; use sensitive_url::SensitiveUrl; -use slog::{info, warn, Logger}; use std::cmp::max; use std::collections::HashSet; use std::fmt::Debug; @@ -30,6 +29,7 @@ use std::num::NonZeroU16; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::time::Duration; +use tracing::{error, info, warn}; use types::graffiti::GraffitiString; use types::{Checkpoint, Epoch, EthSpec, Hash256, PublicKeyBytes}; @@ -47,7 +47,6 @@ pub fn get_config( context: &RuntimeContext, ) -> Result { let spec = &context.eth2_config.spec; - let log = context.log(); let mut client_config = ClientConfig::default(); @@ -65,12 +64,10 @@ pub fn get_config( let stdin_inputs = cfg!(windows) || cli_args.get_flag(STDIN_INPUTS_FLAG); if std::io::stdin().is_terminal() || stdin_inputs { info!( - log, "You are about to delete the chain database. This is irreversable \ and you will need to resync the chain." ); info!( - log, "Type 'confirm' to delete the database. Any other input will leave \ the database intact and Lighthouse will exit." ); @@ -81,14 +78,13 @@ pub fn get_config( let freezer_db = client_config.get_freezer_db_path(); let blobs_db = client_config.get_blobs_db_path(); purge_db(chain_db, freezer_db, blobs_db)?; - info!(log, "Database was deleted."); + info!("Database was deleted."); } else { - info!(log, "Database was not deleted. Lighthouse will now close."); + info!("Database was not deleted. Lighthouse will now close."); std::process::exit(1); } } else { warn!( - log, "The `--purge-db` flag was passed, but Lighthouse is not running \ interactively. The database was not purged. Use `--purge-db-force` \ to purge the database without requiring confirmation." @@ -105,7 +101,7 @@ pub fn get_config( let mut log_dir = client_config.data_dir().clone(); // remove /beacon from the end log_dir.pop(); - info!(log, "Data directory initialised"; "datadir" => log_dir.into_os_string().into_string().expect("Datadir should be a valid os string")); + info!(datadir = %log_dir.into_os_string().into_string().expect("Datadir should be a valid os string"), "Data directory initialised"); /* * Networking @@ -113,7 +109,7 @@ pub fn get_config( let data_dir_ref = client_config.data_dir().clone(); - set_network_config(&mut client_config.network, cli_args, &data_dir_ref, log)?; + set_network_config(&mut client_config.network, cli_args, &data_dir_ref)?; /* * Staking flag @@ -175,14 +171,10 @@ pub fn get_config( client_config.http_api.duplicate_block_status_code = parse_required(cli_args, "http-duplicate-block-status")?; - - client_config.http_api.enable_light_client_server = - !cli_args.get_flag("disable-light-client-server"); } if cli_args.get_flag("light-client-server") { warn!( - log, "The --light-client-server flag is deprecated. The light client server is enabled \ by default" ); @@ -263,8 +255,8 @@ pub fn get_config( // (e.g. using the --staking flag). if cli_args.get_flag("staking") { warn!( - log, - "Running HTTP server on port {}", client_config.http_api.listen_port + "Running HTTP server on port {}", + client_config.http_api.listen_port ); } @@ -278,11 +270,11 @@ pub fn get_config( */ if cli_args.get_flag("dummy-eth1") { - warn!(log, "The --dummy-eth1 flag is deprecated"); + warn!("The --dummy-eth1 flag is deprecated"); } if cli_args.get_flag("eth1") { - warn!(log, "The --eth1 flag is deprecated"); + warn!("The --eth1 flag is deprecated"); } if let Some(val) = cli_args.get_one::("eth1-blocks-per-log-query") { @@ -310,18 +302,14 @@ pub fn get_config( endpoints.as_str(), SensitiveUrl::parse, "--execution-endpoint", - log, )?; // JWTs are required if `--execution-endpoint` is supplied. They can be either passed via // file_path or directly as string. - let secret_file: PathBuf; // Parse a single JWT secret from a given file_path, logging warnings if multiple are supplied. if let Some(secret_files) = cli_args.get_one::("execution-jwt") { - secret_file = - parse_only_one_value(secret_files, PathBuf::from_str, "--execution-jwt", log)?; - + secret_file = parse_only_one_value(secret_files, PathBuf::from_str, "--execution-jwt")?; // Check if the JWT secret key is passed directly via cli flag and persist it to the default // file location. } else if let Some(jwt_secret_key) = cli_args.get_one::("execution-jwt-secret-key") { @@ -344,8 +332,7 @@ pub fn get_config( // Parse and set the payload builder, if any. if let Some(endpoint) = cli_args.get_one::("builder") { - let payload_builder = - parse_only_one_value(endpoint, SensitiveUrl::parse, "--builder", log)?; + let payload_builder = parse_only_one_value(endpoint, SensitiveUrl::parse, "--builder")?; el_config.builder_url = Some(payload_builder); el_config.builder_user_agent = clap_utils::parse_optional(cli_args, "builder-user-agent")?; @@ -446,7 +433,7 @@ pub fn get_config( } if clap_utils::parse_optional::(cli_args, "slots-per-restore-point")?.is_some() { - warn!(log, "The slots-per-restore-point flag is deprecated"); + warn!("The slots-per-restore-point flag is deprecated"); } if let Some(backend) = clap_utils::parse_optional(cli_args, "beacon-node-backend")? { @@ -525,10 +512,9 @@ pub fn get_config( client_config.eth1.set_block_cache_truncation::(spec); info!( - log, - "Deposit contract"; - "deploy_block" => client_config.eth1.deposit_contract_deploy_block, - "address" => &client_config.eth1.deposit_contract_address + deploy_block = client_config.eth1.deposit_contract_deploy_block, + address = &client_config.eth1.deposit_contract_address, + "Deposit contract" ); // Only append network config bootnodes if discovery is not disabled @@ -907,6 +893,14 @@ pub fn get_config( .max_gossip_aggregate_batch_size = clap_utils::parse_required(cli_args, "beacon-processor-aggregate-batch-size")?; + if let Some(delay) = clap_utils::parse_optional(cli_args, "delay-block-publishing")? { + client_config.chain.block_publishing_delay = Some(Duration::from_secs_f64(delay)); + } + + if let Some(delay) = clap_utils::parse_optional(cli_args, "delay-data-column-publishing")? { + client_config.chain.data_column_publishing_delay = Some(Duration::from_secs_f64(delay)); + } + if let Some(invalid_block_roots_file_path) = clap_utils::parse_optional::(cli_args, "invalid-block-roots")? { @@ -920,13 +914,8 @@ pub fn get_config( .filter_map( |s| match Hash256::from_str(s.strip_prefix("0x").unwrap_or(s).trim()) { Ok(block_root) => Some(block_root), - Err(e) => { - warn!( - log, - "Unable to parse invalid block root"; - "block_root" => s, - "error" => ?e, - ); + Err(error) => { + warn!(block_root = s, ?error, "Unable to parse invalid block root",); None } }, @@ -945,10 +934,7 @@ pub fn get_config( } /// Gets the listening_addresses for lighthouse based on the cli options. -pub fn parse_listening_addresses( - cli_args: &ArgMatches, - log: &Logger, -) -> Result { +pub fn parse_listening_addresses(cli_args: &ArgMatches) -> Result { let listen_addresses_str = cli_args .get_many::("listen-address") .unwrap_or_default(); @@ -1051,7 +1037,7 @@ pub fn parse_listening_addresses( (None, Some(ipv6)) => { // A single ipv6 address was provided. Set the ports if cli_args.value_source("port6") == Some(ValueSource::CommandLine) { - warn!(log, "When listening only over IPv6, use the --port flag. The value of --port6 will be ignored."); + warn!("When listening only over IPv6, use the --port flag. The value of --port6 will be ignored."); } // If we are only listening on ipv6 and the user has specified --port6, lets just use @@ -1065,11 +1051,11 @@ pub fn parse_listening_addresses( .unwrap_or(port); if maybe_disc6_port.is_some() { - warn!(log, "When listening only over IPv6, use the --discovery-port flag. The value of --discovery-port6 will be ignored.") + warn!("When listening only over IPv6, use the --discovery-port flag. The value of --discovery-port6 will be ignored.") } if maybe_quic6_port.is_some() { - warn!(log, "When listening only over IPv6, use the --quic-port flag. The value of --quic-port6 will be ignored.") + warn!("When listening only over IPv6, use the --quic-port flag. The value of --quic-port6 will be ignored.") } // use zero ports if required. If not, use the specific udp port. If none given, use @@ -1191,7 +1177,6 @@ pub fn set_network_config( config: &mut NetworkConfig, cli_args: &ArgMatches, data_dir: &Path, - log: &Logger, ) -> Result<(), String> { // If a network dir has been specified, override the `datadir` definition. if let Some(dir) = cli_args.get_one::("network-dir") { @@ -1216,7 +1201,7 @@ pub fn set_network_config( config.shutdown_after_sync = true; } - config.set_listening_addr(parse_listening_addresses(cli_args, log)?); + config.set_listening_addr(parse_listening_addresses(cli_args)?); // A custom target-peers command will overwrite the --proposer-only default. if let Some(target_peers_str) = cli_args.get_one::("target-peers") { @@ -1244,10 +1229,10 @@ pub fn set_network_config( .parse() .map_err(|_| format!("Not valid as ENR nor Multiaddr: {}", addr))?; if !multi.iter().any(|proto| matches!(proto, Protocol::Udp(_))) { - slog::error!(log, "Missing UDP in Multiaddr {}", multi.to_string()); + error!(multiaddr = multi.to_string(), "Missing UDP in Multiaddr"); } if !multi.iter().any(|proto| matches!(proto, Protocol::P2p(_))) { - slog::error!(log, "Missing P2P in Multiaddr {}", multi.to_string()); + error!(multiaddr = multi.to_string(), "Missing P2P in Multiaddr"); } multiaddrs.push(multi); } @@ -1282,7 +1267,7 @@ pub fn set_network_config( }) .collect::, _>>()?; if config.trusted_peers.len() >= config.target_peers { - slog::warn!(log, "More trusted peers than the target peer limit. This will prevent efficient peer selection criteria."; "target_peers" => config.target_peers, "trusted_peers" => config.trusted_peers.len()); + warn!( target_peers = config.target_peers, trusted_peers = config.trusted_peers.len(),"More trusted peers than the target peer limit. This will prevent efficient peer selection criteria."); } } @@ -1382,14 +1367,14 @@ pub fn set_network_config( match addr.parse::() { Ok(IpAddr::V4(v4_addr)) => { if let Some(used) = enr_ip4.as_ref() { - warn!(log, "More than one Ipv4 ENR address provided"; "used" => %used, "ignored" => %v4_addr) + warn!(used = %used, ignored = %v4_addr, "More than one Ipv4 ENR address provided") } else { enr_ip4 = Some(v4_addr) } } Ok(IpAddr::V6(v6_addr)) => { if let Some(used) = enr_ip6.as_ref() { - warn!(log, "More than one Ipv6 ENR address provided"; "used" => %used, "ignored" => %v6_addr) + warn!(used = %used, ignored = %v6_addr,"More than one Ipv6 ENR address provided") } else { enr_ip6 = Some(v6_addr) } @@ -1455,13 +1440,13 @@ pub fn set_network_config( } if parse_flag(cli_args, "disable-packet-filter") { - warn!(log, "Discv5 packet filter is disabled"); + warn!("Discv5 packet filter is disabled"); config.discv5_config.enable_packet_filter = false; } if parse_flag(cli_args, "disable-discovery") { config.disable_discovery = true; - warn!(log, "Discovery is disabled. New peers will not be found"); + warn!("Discovery is disabled. New peers will not be found"); } if parse_flag(cli_args, "disable-quic") { @@ -1508,7 +1493,10 @@ pub fn set_network_config( config.target_peers = 15; } config.proposer_only = true; - warn!(log, "Proposer-only mode enabled"; "info"=> "Do not connect a validator client to this node unless via the --proposer-nodes flag"); + warn!( + info = "Proposer-only mode enabled", + "Do not connect a validator client to this node unless via the --proposer-nodes flag" + ); } // The inbound rate limiter is enabled by default unless `disabled` via the // `disable-inbound-rate-limiter` flag. @@ -1566,7 +1554,6 @@ pub fn parse_only_one_value( cli_value: &str, parser: F, flag_name: &str, - log: &Logger, ) -> Result where F: Fn(&str) -> Result, @@ -1580,11 +1567,10 @@ where if values.len() > 1 { warn!( - log, - "Multiple values provided"; - "info" => "multiple values are deprecated, only the first value will be used", - "count" => values.len(), - "flag" => flag_name + info = "Multiple values provided", + count = values.len(), + flag = flag_name, + "multiple values are deprecated, only the first value will be used" ); } diff --git a/beacon_node/src/lib.rs b/beacon_node/src/lib.rs index e3802c837cb..a7f92434ce3 100644 --- a/beacon_node/src/lib.rs +++ b/beacon_node/src/lib.rs @@ -12,10 +12,10 @@ pub use config::{get_config, get_data_dir, set_network_config}; use environment::RuntimeContext; pub use eth2_config::Eth2Config; use slasher::{DatabaseBackendOverride, Slasher}; -use slog::{info, warn}; use std::ops::{Deref, DerefMut}; use std::sync::Arc; use store::database::interface::BeaconNodeBackend; +use tracing::{info, warn}; use types::{ChainSpec, Epoch, EthSpec, ForkName}; /// A type-alias to the tighten the definition of a production-intended `Client`. @@ -63,7 +63,6 @@ impl ProductionBeaconNode { let spec = context.eth2_config().spec.clone(); let client_genesis = client_config.genesis.clone(); let store_config = client_config.store.clone(); - let log = context.log().clone(); let _datadir = client_config.create_data_dir()?; let db_path = client_config.create_db_path()?; let freezer_db_path = client_config.create_freezer_db_path()?; @@ -72,20 +71,18 @@ impl ProductionBeaconNode { if let Some(legacy_dir) = client_config.get_existing_legacy_data_dir() { warn!( - log, - "Legacy datadir location"; - "msg" => "this occurs when using relative paths for a datadir location", - "location" => ?legacy_dir, + msg = "this occurs when using relative paths for a datadir location", + location = ?legacy_dir, + "Legacy datadir location" ) } if let Err(misaligned_forks) = validator_fork_epochs(&spec) { warn!( - log, - "Fork boundaries are not well aligned / multiples of 256"; - "info" => "This may cause issues as fork boundaries do not align with the \ - start of sync committee period.", - "misaligned_forks" => ?misaligned_forks, + info = "This may cause issues as fork boundaries do not align with the \ + start of sync committee period.", + ?misaligned_forks, + "Fork boundaries are not well aligned / multiples of 256" ); } @@ -94,42 +91,30 @@ impl ProductionBeaconNode { .chain_spec(spec.clone()) .beacon_processor(client_config.beacon_processor.clone()) .http_api_config(client_config.http_api.clone()) - .disk_store( - &db_path, - &freezer_db_path, - &blobs_db_path, - store_config, - log.clone(), - )?; + .disk_store(&db_path, &freezer_db_path, &blobs_db_path, store_config)?; let builder = if let Some(mut slasher_config) = client_config.slasher.clone() { match slasher_config.override_backend() { DatabaseBackendOverride::Success(old_backend) => { info!( - log, - "Slasher backend overridden"; - "reason" => "database exists", - "configured_backend" => %old_backend, - "override_backend" => %slasher_config.backend, + reason = "database exists", + configured_backend = %old_backend, + override_backend = %slasher_config.backend, + "Slasher backend overridden" ); } DatabaseBackendOverride::Failure(path) => { warn!( - log, - "Slasher backend override failed"; - "advice" => "delete old MDBX database or enable MDBX backend", - "path" => path.display() + advice = "delete old MDBX database or enable MDBX backend", + path = %path.display(), + "Slasher backend override failed" ); } _ => {} } let slasher = Arc::new( - Slasher::open( - slasher_config, - spec, - log.new(slog::o!("service" => "slasher")), - ) - .map_err(|e| format!("Slasher open error: {:?}", e))?, + Slasher::open(slasher_config, spec) + .map_err(|e| format!("Slasher open error: {:?}", e))?, ); builder.slasher(slasher) } else { @@ -149,19 +134,17 @@ impl ProductionBeaconNode { .await?; let builder = if client_config.sync_eth1_chain { info!( - log, - "Block production enabled"; - "endpoint" => format!("{:?}", &client_config.eth1.endpoint), - "method" => "json rpc via http" + endpoint = ?client_config.eth1.endpoint, + method = "json rpc via http", + "Block production enabled" ); builder .caching_eth1_backend(client_config.eth1.clone()) .await? } else { info!( - log, - "Block production disabled"; - "reason" => "no eth1 backend configured" + reason = "no eth1 backend configured", + "Block production disabled" ); builder.no_eth1_backend()? }; diff --git a/beacon_node/store/Cargo.toml b/beacon_node/store/Cargo.toml index 57330eb5832..908f0759a90 100644 --- a/beacon_node/store/Cargo.toml +++ b/beacon_node/store/Cargo.toml @@ -30,12 +30,12 @@ parking_lot = { workspace = true } redb = { version = "2.1.3", optional = true } safe_arith = { workspace = true } serde = { workspace = true } -slog = { workspace = true } -sloggers = { workspace = true } smallvec = { workspace = true } state_processing = { workspace = true } strum = { workspace = true } superstruct = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } types = { workspace = true } xdelta3 = { workspace = true } zstd = { workspace = true } diff --git a/beacon_node/store/src/chunked_iter.rs b/beacon_node/store/src/chunked_iter.rs index 8f6682e7581..f2821286ec9 100644 --- a/beacon_node/store/src/chunked_iter.rs +++ b/beacon_node/store/src/chunked_iter.rs @@ -1,6 +1,6 @@ use crate::chunked_vector::{chunk_key, Chunk, Field}; use crate::{HotColdDB, ItemStore}; -use slog::error; +use tracing::error; use types::{ChainSpec, EthSpec, Slot}; /// Iterator over the values of a `BeaconState` vector field (like `block_roots`). @@ -82,9 +82,8 @@ where .cloned() .or_else(|| { error!( - self.store.log, - "Missing chunk value in forwards iterator"; - "vector index" => vindex + vector_index = vindex, + "Missing chunk value in forwards iterator" ); None })?; @@ -100,19 +99,17 @@ where ) .map_err(|e| { error!( - self.store.log, - "Database error in forwards iterator"; - "chunk index" => self.next_cindex, - "error" => format!("{:?}", e) + chunk_index = self.next_cindex, + error = ?e, + "Database error in forwards iterator" ); e }) .ok()? .or_else(|| { error!( - self.store.log, - "Missing chunk in forwards iterator"; - "chunk index" => self.next_cindex + chunk_index = self.next_cindex, + "Missing chunk in forwards iterator" ); None })?; diff --git a/beacon_node/store/src/database/leveldb_impl.rs b/beacon_node/store/src/database/leveldb_impl.rs index 3d8bbe14737..81d6d1d4bd2 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 41fd17ef437..ed6154da802 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. diff --git a/beacon_node/store/src/garbage_collection.rs b/beacon_node/store/src/garbage_collection.rs deleted file mode 100644 index 06393f2d219..00000000000 --- a/beacon_node/store/src/garbage_collection.rs +++ /dev/null @@ -1,40 +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 slog::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!( - self.log, - "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 42d1fd31c2b..362c5d8014e 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -14,15 +14,14 @@ 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; use parking_lot::{Mutex, RwLock}; use safe_arith::SafeArith; use serde::{Deserialize, Serialize}; -use slog::{debug, error, info, trace, warn, Logger}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use state_processing::{ @@ -37,6 +36,7 @@ use std::num::NonZeroUsize; use std::path::Path; use std::sync::Arc; use std::time::Duration; +use tracing::{debug, error, info, warn}; use types::data_column_sidecar::{ColumnIndex, DataColumnSidecar, DataColumnSidecarList}; use types::*; use zstd::{Decoder, Encoder}; @@ -80,9 +80,7 @@ pub struct HotColdDB, Cold: ItemStore> { /// HTTP API. historic_state_cache: Mutex>, /// Chain spec. - pub(crate) spec: Arc, - /// Logger. - pub log: Logger, + pub spec: Arc, /// Mere vessel for E. _phantom: PhantomData, } @@ -163,7 +161,7 @@ pub enum HotColdDBError { MissingRestorePoint(Hash256), MissingColdStateSummary(Hash256), MissingHotStateSummary(Hash256), - MissingEpochBoundaryState(Hash256), + MissingEpochBoundaryState(Hash256, Hash256), MissingPrevState(Hash256), MissingSplitState(Hash256, Slot), MissingStateDiff(Hash256), @@ -203,7 +201,6 @@ impl HotColdDB, MemoryStore> { pub fn open_ephemeral( config: StoreConfig, spec: Arc, - log: Logger, ) -> Result, MemoryStore>, Error> { config.verify::()?; @@ -229,7 +226,6 @@ impl HotColdDB, MemoryStore> { config, hierarchy, spec, - log, _phantom: PhantomData, }; @@ -249,7 +245,6 @@ impl HotColdDB, BeaconNodeBackend> { migrate_schema: impl FnOnce(Arc, SchemaVersion, SchemaVersion) -> Result<(), Error>, config: StoreConfig, spec: Arc, - log: Logger, ) -> Result, Error> { config.verify::()?; @@ -278,10 +273,8 @@ impl HotColdDB, BeaconNodeBackend> { config, hierarchy, spec, - log, _phantom: PhantomData, }; - // Load the config from disk but don't error on a failed read because the config itself may // need migrating. let _ = db.load_config(); @@ -293,10 +286,9 @@ impl HotColdDB, BeaconNodeBackend> { *db.split.write() = split; info!( - db.log, - "Hot-Cold DB initialized"; - "split_slot" => split.slot, - "split_state" => ?split.state_root + %split.slot, + split_state = ?split.state_root, + "Hot-Cold DB initialized" ); } @@ -358,11 +350,10 @@ impl HotColdDB, BeaconNodeBackend> { )?; info!( - db.log, - "Blob DB initialized"; - "path" => ?blobs_db_path, - "oldest_blob_slot" => ?new_blob_info.oldest_blob_slot, - "oldest_data_column_slot" => ?new_data_column_info.oldest_data_column_slot, + path = ?blobs_db_path, + oldest_blob_slot = ?new_blob_info.oldest_blob_slot, + oldest_data_column_slot = ?new_data_column_info.oldest_data_column_slot, + "Blob DB initialized" ); // Ensure that the schema version of the on-disk database matches the software. @@ -370,10 +361,9 @@ impl HotColdDB, BeaconNodeBackend> { let db = Arc::new(db); if let Some(schema_version) = db.load_schema_version()? { debug!( - db.log, - "Attempting schema migration"; - "from_version" => schema_version.as_u64(), - "to_version" => CURRENT_SCHEMA_VERSION.as_u64(), + from_version = schema_version.as_u64(), + to_version = CURRENT_SCHEMA_VERSION.as_u64(), + "Attempting schema migration" ); migrate_schema(db.clone(), schema_version, CURRENT_SCHEMA_VERSION)?; } else { @@ -391,34 +381,30 @@ impl HotColdDB, BeaconNodeBackend> { if let Ok(hierarchy_config) = disk_config.hierarchy_config() { if &db.config.hierarchy_config != hierarchy_config { info!( - db.log, - "Updating historic state config"; - "previous_config" => %hierarchy_config, - "new_config" => %db.config.hierarchy_config, + previous_config = %hierarchy_config, + new_config = %db.config.hierarchy_config, + "Updating historic state config" ); } } } 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 { - info!(db.log, "Running foreground compaction"); + info!("Running foreground compaction"); db.compact()?; - info!(db.log, "Foreground compaction complete"); + info!("Foreground compaction complete"); } 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 { @@ -914,26 +900,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) } @@ -998,12 +969,7 @@ impl, Cold: ItemStore> HotColdDB let split = self.split.read_recursive(); if state_root != split.state_root { - warn!( - self.log, - "State cache missed"; - "state_root" => ?state_root, - "block_root" => ?block_root, - ); + warn!(?state_root, ?block_root, "State cache missed"); } // Sanity check max-slot against the split slot. @@ -1035,12 +1001,11 @@ impl, Cold: ItemStore> HotColdDB .put_state(*state_root, block_root, state)? { debug!( - self.log, - "Cached state"; - "location" => "get_advanced_hot_state", - "deleted_states" => ?deleted_states, - "state_root" => ?state_root, - "slot" => state.slot(), + ?state_root, + state_slot = %state.slot(), + ?deleted_states, + location = "get_advanced_hot_state", + "Cached state", ); } } @@ -1155,6 +1120,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 { @@ -1218,17 +1184,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, @@ -1258,13 +1213,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, @@ -1325,9 +1273,9 @@ impl, Cold: ItemStore> HotColdDB Ok(BlobSidecarListFromRoot::NoBlobs | BlobSidecarListFromRoot::NoRoot) => {} Err(e) => { error!( - self.log, "Error getting blobs"; - "block_root" => %block_root, - "error" => ?e + %block_root, + error = ?e, + "Error getting blobs" ); } } @@ -1350,9 +1298,9 @@ impl, Cold: ItemStore> HotColdDB } Err(e) => { error!( - self.log, "Error getting data columns"; - "block_root" => %block_root, - "error" => ?e + %block_root, + error = ?e, + "Error getting data columns" ); } } @@ -1380,10 +1328,9 @@ impl, Cold: ItemStore> HotColdDB // Rollback on failure if let Err(e) = tx_res { error!( - self.log, - "Database write failed"; - "error" => ?e, - "action" => "reverting blob DB changes" + error = ?e, + action = "reverting blob DB changes", + "Database write failed" ); let mut blob_cache_ops = blob_cache_ops; for op in blob_cache_ops.iter_mut() { @@ -1426,10 +1373,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); @@ -1490,20 +1433,18 @@ impl, Cold: ItemStore> HotColdDB )? { PutStateOutcome::New(deleted_states) => { debug!( - self.log, - "Cached state"; - "location" => "store_hot_state", - "deleted_states" => ?deleted_states, - "state_root" => ?state_root, - "slot" => state.slot(), + ?state_root, + state_slot = %state.slot(), + ?deleted_states, + location = "store_hot_state", + "Cached state", ); } PutStateOutcome::Duplicate => { debug!( - self.log, - "State already exists in state cache"; - "slot" => state.slot(), - "state_root" => ?state_root + ?state_root, + state_slot = %state.slot(), + "State already exists in state cache", ); return Ok(()); } @@ -1512,11 +1453,10 @@ impl, Cold: ItemStore> HotColdDB // On the epoch boundary, store the full state. if state.slot() % E::slots_per_epoch() == 0 { - trace!( - self.log, - "Storing full state on epoch boundary"; - "slot" => state.slot().as_u64(), - "state_root" => format!("{:?}", state_root) + debug!( + slot = %state.slot(), + ?state_root, + "Storing full state on epoch boundary" ); store_full_state(state_root, state, ops)?; } @@ -1543,11 +1483,7 @@ impl, Cold: ItemStore> HotColdDB if *state_root != self.get_split_info().state_root { // Do not warn on start up when loading the split state. - warn!( - self.log, - "State cache missed"; - "state_root" => ?state_root, - ); + warn!(?state_root, "State cache missed"); } let state_from_disk = self.load_hot_state(state_root, update_cache)?; @@ -1562,20 +1498,18 @@ impl, Cold: ItemStore> HotColdDB .put_state(*state_root, block_root, &state)? { debug!( - self.log, - "Cached state"; - "location" => "get_hot_state", - "deleted_states" => ?deleted_states, - "state_root" => ?state_root, - "slot" => state.slot(), + ?state_root, + state_slot = %state.slot(), + ?deleted_states, + location = "get_hot_state", + "Cached state", ); } } else { debug!( - self.log, - "Did not cache state"; - "state_root" => ?state_root, - "slot" => state.slot(), + ?state_root, + state_slot = %state.slot(), + "Did not cache state", ); } @@ -1598,12 +1532,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, @@ -1612,7 +1540,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 @@ -1644,11 +1575,10 @@ impl, Cold: ItemStore> HotColdDB .put_state(state_root, latest_block_root, state)? { debug!( - self.log, - "Cached ancestor state"; - "state_root" => ?state_root, - "state_slot" => state.slot(), - "descendant_slot" => slot, + ?state_root, + state_slot = %state.slot(), + descendant_slot = %slot, + "Cached ancestor state", ); } Ok(()) @@ -1700,35 +1630,31 @@ impl, Cold: ItemStore> HotColdDB match self.hierarchy.storage_strategy(slot)? { StorageStrategy::ReplayFrom(from) => { debug!( - self.log, - "Storing cold state"; - "strategy" => "replay", - "from_slot" => from, - "slot" => state.slot(), + strategy = "replay", + from_slot = %from, + %slot, + "Storing cold state", ); // Already have persisted the state summary, don't persist anything else } StorageStrategy::Snapshot => { debug!( - self.log, - "Storing cold state"; - "strategy" => "snapshot", - "slot" => state.slot(), + strategy = "snapshot", + %slot, + "Storing cold state" ); self.store_cold_state_as_snapshot(state, ops)?; } StorageStrategy::DiffFrom(from) => { debug!( - self.log, - "Storing cold state"; - "strategy" => "diff", - "from_slot" => from, - "slot" => state.slot(), + strategy = "diff", + from_slot = %from, + %slot, + "Storing cold state" ); self.store_cold_state_as_diff(state, from, ops)?; } } - Ok(()) } @@ -1887,10 +1813,9 @@ impl, Cold: ItemStore> HotColdDB metrics::start_timer(&metrics::STORE_BEACON_COLD_BUILD_BEACON_CACHES_TIME); base_state.build_all_caches(&self.spec)?; debug!( - self.log, - "Built caches for historic state"; - "target_slot" => slot, - "build_time_ms" => metrics::stop_timer_with_duration(cache_timer).as_millis() + target_slot = %slot, + build_time_ms = metrics::stop_timer_with_duration(cache_timer).as_millis(), + "Built caches for historic state" ); self.historic_state_cache .lock() @@ -1912,10 +1837,9 @@ impl, Cold: ItemStore> HotColdDB })?; let state = self.replay_blocks(base_state, blocks, slot, Some(state_root_iter), None)?; debug!( - self.log, - "Replayed blocks for historic state"; - "target_slot" => slot, - "replay_time_ms" => metrics::stop_timer_with_duration(replay_timer).as_millis() + target_slot = %slot, + replay_time_ms = metrics::stop_timer_with_duration(replay_timer).as_millis(), + "Replayed blocks for historic state" ); self.historic_state_cache @@ -1943,9 +1867,8 @@ impl, Cold: ItemStore> HotColdDB fn load_hdiff_buffer_for_slot(&self, slot: Slot) -> Result<(Slot, HDiffBuffer), Error> { if let Some(buffer) = self.historic_state_cache.lock().get_hdiff_buffer(slot) { debug!( - self.log, - "Hit hdiff buffer cache"; - "slot" => slot + %slot, + "Hit hdiff buffer cache" ); metrics::inc_counter(&metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_HIT); return Ok((slot, buffer)); @@ -1969,10 +1892,9 @@ impl, Cold: ItemStore> HotColdDB let load_time_ms = t.elapsed().as_millis(); debug!( - self.log, - "Cached state and hdiff buffer"; - "load_time_ms" => load_time_ms, - "slot" => slot + load_time_ms, + %slot, + "Cached state and hdiff buffer" ); Ok((slot, buffer)) @@ -1995,10 +1917,9 @@ impl, Cold: ItemStore> HotColdDB let load_time_ms = t.elapsed().as_millis(); debug!( - self.log, - "Cached hdiff buffer"; - "load_time_ms" => load_time_ms, - "slot" => slot + load_time_ms, + %slot, + "Cached hdiff buffer" ); Ok((slot, buffer)) @@ -2102,9 +2023,8 @@ impl, Cold: ItemStore> HotColdDB .map(|block_replayer| { if have_state_root_iterator && block_replayer.state_root_miss() { warn!( - self.log, - "State root cache miss during block replay"; - "slot" => target_slot, + slot = %target_slot, + "State root cache miss during block replay" ); } block_replayer.into_state() @@ -2230,11 +2150,6 @@ impl, Cold: ItemStore> HotColdDB &self.spec } - /// Get a reference to the `Logger` used by the database. - pub fn logger(&self) -> &Logger { - &self.log - } - /// Fetch a copy of the current split slot from memory. pub fn get_split_slot(&self) -> Slot { self.split.read_recursive().slot @@ -2588,15 +2503,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. @@ -2629,17 +2545,9 @@ impl, Cold: ItemStore> HotColdDB columns.extend(previous_schema_columns); for column in columns { - info!( - self.log, - "Starting compaction"; - "column" => ?column - ); + info!(?column, "Starting compaction"); self.cold_db.compact_column(column)?; - info!( - self.log, - "Finishing compaction"; - "column" => ?column - ); + info!(?column, "Finishing compaction"); } Ok(()) } @@ -2745,16 +2653,15 @@ impl, Cold: ItemStore> HotColdDB })??; if already_pruned && !force { - info!(self.log, "Execution payloads are pruned"); + info!("Execution payloads are pruned"); return Ok(()); } // Iterate block roots backwards to the Bellatrix fork or the anchor slot, whichever comes // first. warn!( - self.log, - "Pruning finalized payloads"; - "info" => "you may notice degraded I/O performance while this runs" + info = "you may notice degraded I/O performance while this runs", + "Pruning finalized payloads" ); let anchor_info = self.get_anchor_info(); @@ -2768,58 +2675,41 @@ impl, Cold: ItemStore> HotColdDB Ok(tuple) => tuple, Err(e) => { warn!( - self.log, - "Stopping payload pruning early"; - "error" => ?e, + error = ?e, + "Stopping payload pruning early" ); break; } }; if slot < bellatrix_fork_slot { - info!( - self.log, - "Payload pruning reached Bellatrix boundary"; - ); + info!("Payload pruning reached Bellatrix boundary"); break; } if Some(block_root) != last_pruned_block_root && self.execution_payload_exists(&block_root)? { - debug!( - self.log, - "Pruning execution payload"; - "slot" => slot, - "block_root" => ?block_root, - ); + debug!(%slot, ?block_root, "Pruning execution payload"); last_pruned_block_root = Some(block_root); ops.push(StoreOp::DeleteExecutionPayload(block_root)); } if slot <= anchor_info.oldest_block_slot { - info!( - self.log, - "Payload pruning reached anchor oldest block slot"; - "slot" => slot - ); + info!(%slot, "Payload pruning reached anchor oldest block slot"); break; } } let payloads_pruned = ops.len(); self.do_atomically_with_block_and_blobs_cache(ops)?; - info!( - self.log, - "Execution payload pruning complete"; - "payloads_pruned" => payloads_pruned, - ); + info!(%payloads_pruned, "Execution payload pruning complete"); Ok(()) } /// Try to prune blobs, approximating the current epoch from the split slot. pub fn try_prune_most_blobs(&self, force: bool) -> Result<(), Error> { let Some(deneb_fork_epoch) = self.spec.deneb_fork_epoch else { - debug!(self.log, "Deneb fork is disabled"); + debug!("Deneb fork is disabled"); return Ok(()); }; // The current epoch is >= split_epoch + 2. It could be greater if the database is @@ -2850,7 +2740,7 @@ impl, Cold: ItemStore> HotColdDB data_availability_boundary: Epoch, ) -> Result<(), Error> { if self.spec.deneb_fork_epoch.is_none() { - debug!(self.log, "Deneb fork is disabled"); + debug!("Deneb fork is disabled"); return Ok(()); } @@ -2859,17 +2749,13 @@ impl, Cold: ItemStore> HotColdDB let epochs_per_blob_prune = self.get_config().epochs_per_blob_prune; if !force && !pruning_enabled { - debug!( - self.log, - "Blob pruning is disabled"; - "prune_blobs" => pruning_enabled - ); + debug!(prune_blobs = pruning_enabled, "Blob pruning is disabled"); return Ok(()); } let blob_info = self.get_blob_info(); let Some(oldest_blob_slot) = blob_info.oldest_blob_slot else { - error!(self.log, "Slot of oldest blob is not known"); + error!("Slot of oldest blob is not known"); return Err(HotColdDBError::BlobPruneLogicError.into()); }; @@ -2892,13 +2778,12 @@ impl, Cold: ItemStore> HotColdDB if !force && !should_prune || !can_prune { debug!( - self.log, - "Blobs are pruned"; - "oldest_blob_slot" => oldest_blob_slot, - "data_availability_boundary" => data_availability_boundary, - "split_slot" => split.slot, - "end_epoch" => end_epoch, - "start_epoch" => start_epoch, + %oldest_blob_slot, + %data_availability_boundary, + %split.slot, + %end_epoch, + %start_epoch, + "Blobs are pruned" ); return Ok(()); } @@ -2907,21 +2792,19 @@ impl, Cold: ItemStore> HotColdDB let anchor = self.get_anchor_info(); if oldest_blob_slot < anchor.oldest_block_slot { error!( - self.log, - "Oldest blob is older than oldest block"; - "oldest_blob_slot" => oldest_blob_slot, - "oldest_block_slot" => anchor.oldest_block_slot + %oldest_blob_slot, + oldest_block_slot = %anchor.oldest_block_slot, + "Oldest blob is older than oldest block" ); return Err(HotColdDBError::BlobPruneLogicError.into()); } // Iterate block roots forwards from the oldest blob slot. debug!( - self.log, - "Pruning blobs"; - "start_epoch" => start_epoch, - "end_epoch" => end_epoch, - "data_availability_boundary" => data_availability_boundary, + %start_epoch, + %end_epoch, + %data_availability_boundary, + "Pruning blobs" ); // We collect block roots of deleted blobs in memory. Even for 10y of blob history this @@ -2977,10 +2860,7 @@ impl, Cold: ItemStore> HotColdDB let op = self.compare_and_set_blob_info(blob_info, new_blob_info)?; self.do_atomically_with_block_and_blobs_cache(vec![StoreOp::KeyValueOp(op)])?; - debug!( - self.log, - "Blob pruning complete"; - ); + debug!("Blob pruning complete"); Ok(()) } @@ -3050,18 +2930,13 @@ impl, Cold: ItemStore> HotColdDB // If we just deleted the genesis state, re-store it using the current* schema. if self.get_split_slot() > 0 { info!( - self.log, - "Re-storing genesis state"; - "state_root" => ?genesis_state_root, + state_root = ?genesis_state_root, + "Re-storing genesis state" ); self.store_cold_state(&genesis_state_root, genesis_state, &mut cold_ops)?; } - info!( - self.log, - "Deleting historic states"; - "delete_ops" => delete_ops, - ); + info!(delete_ops, "Deleting historic states"); self.cold_db.do_atomically(cold_ops)?; // In order to reclaim space, we need to compact the freezer DB as well. @@ -3069,60 +2944,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!( - self.log, - "Database state pruning started"; - "split_slot" => split.slot, - ); - 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!( - self.log, - "Deleting state"; - "state_root" => ?state_root, - "slot" => summary.slot, - "reason" => reason, - ); - 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!( - self.log, - "Database state pruning complete"; - "num_deleted_states" => num_deleted_states, - ); - 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, @@ -3130,9 +2958,8 @@ pub fn migrate_database, Cold: ItemStore>( finalized_state: &BeaconState, ) -> Result<(), Error> { debug!( - store.log, - "Freezer migration started"; - "slot" => finalized_state.slot() + slot = %finalized_state.slot(), + "Freezer migration started" ); // 0. Check that the migration is sensible. @@ -3155,29 +2982,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, @@ -3185,44 +3000,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!(store.log, "Pruning finalized state"; "slot" => slot); 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. @@ -3230,36 +3028,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)?; - } - - // 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)); - }); + store.cold_db.do_atomically(cold_db_state_ops)?; + } - // 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()?; { @@ -3270,10 +3054,9 @@ pub fn migrate_database, Cold: ItemStore>( // place in code. if latest_split_slot != current_split_slot { error!( - store.log, - "Race condition detected: Split point changed while moving states to the freezer"; - "previous split slot" => current_split_slot, - "current split slot" => latest_split_slot, + previous_split_slot = %current_split_slot, + current_split_slot = %latest_split_slot, + "Race condition detected: Split point changed while copying states to the freezer" ); // Assume the freezing procedure will be retried in case this happens. @@ -3298,9 +3081,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, @@ -3309,9 +3089,8 @@ pub fn migrate_database, Cold: ItemStore>( )?; debug!( - store.log, - "Freezer migration complete"; - "slot" => finalized_state.slot() + slot = %finalized_state.slot(), + "Freezer migration complete" ); Ok(()) @@ -3418,23 +3197,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/iter.rs b/beacon_node/store/src/iter.rs index a344bea8d47..8419dde4a2c 100644 --- a/beacon_node/store/src/iter.rs +++ b/beacon_node/store/src/iter.rs @@ -387,7 +387,6 @@ mod test { use crate::StoreConfig as Config; use beacon_chain::test_utils::BeaconChainHarness; use beacon_chain::types::{ChainSpec, MainnetEthSpec}; - use sloggers::{null::NullLoggerBuilder, Build}; use std::sync::Arc; use types::FixedBytesExtended; @@ -403,10 +402,8 @@ mod test { #[test] fn block_root_iter() { - let log = NullLoggerBuilder.build().unwrap(); let store = - HotColdDB::open_ephemeral(Config::default(), Arc::new(ChainSpec::minimal()), log) - .unwrap(); + HotColdDB::open_ephemeral(Config::default(), Arc::new(ChainSpec::minimal())).unwrap(); let slots_per_historical_root = MainnetEthSpec::slots_per_historical_root(); let mut state_a: BeaconState = get_state(); @@ -452,10 +449,8 @@ mod test { #[test] fn state_root_iter() { - let log = NullLoggerBuilder.build().unwrap(); let store = - HotColdDB::open_ephemeral(Config::default(), Arc::new(ChainSpec::minimal()), log) - .unwrap(); + HotColdDB::open_ephemeral(Config::default(), Arc::new(ChainSpec::minimal())).unwrap(); let slots_per_historical_root = MainnetEthSpec::slots_per_historical_root(); let mut state_a: BeaconState = get_state(); diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index 2b5be034894..5b30971fd8e 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 1d70e105b94..55c64bf8508 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/store/src/reconstruct.rs b/beacon_node/store/src/reconstruct.rs index 2a3b208aae8..30df552b7be 100644 --- a/beacon_node/store/src/reconstruct.rs +++ b/beacon_node/store/src/reconstruct.rs @@ -4,12 +4,12 @@ use crate::metadata::ANCHOR_FOR_ARCHIVE_NODE; use crate::metrics; use crate::{Error, ItemStore}; use itertools::{process_results, Itertools}; -use slog::{debug, info}; use state_processing::{ per_block_processing, per_slot_processing, BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, }; use std::sync::Arc; +use tracing::{debug, info}; use types::EthSpec; impl HotColdDB @@ -37,9 +37,8 @@ where } debug!( - self.log, - "Starting state reconstruction batch"; - "start_slot" => anchor.state_lower_limit, + start_slot = %anchor.state_lower_limit, + "Starting state reconstruction batch" ); let _t = metrics::start_timer(&metrics::STORE_BEACON_RECONSTRUCTION_TIME); @@ -124,10 +123,9 @@ where || reconstruction_complete { info!( - self.log, - "State reconstruction in progress"; - "slot" => slot, - "remaining" => upper_limit_slot - 1 - slot + %slot, + remaining = %(upper_limit_slot - 1 - slot), + "State reconstruction in progress" ); self.cold_db.do_atomically(std::mem::take(&mut io_batch))?; @@ -164,10 +162,9 @@ where // batch when there is idle capacity. if batch_complete { debug!( - self.log, - "Finished state reconstruction batch"; - "start_slot" => lower_limit_slot, - "end_slot" => slot, + start_slot = %lower_limit_slot, + end_slot = %slot, + "Finished state reconstruction batch" ); return Ok(()); } diff --git a/beacon_node/tests/test.rs b/beacon_node/tests/test.rs index 0738b12ec06..0d448e6c069 100644 --- a/beacon_node/tests/test.rs +++ b/beacon_node/tests/test.rs @@ -25,8 +25,6 @@ fn build_node(env: &mut Environment) -> LocalBeaconNode { #[test] fn http_server_genesis_state() { let mut env = env_builder() - .test_logger() - .expect("should build env logger") .multi_threaded_tokio_runtime() .expect("should start tokio runtime") .build() diff --git a/beacon_node/timer/Cargo.toml b/beacon_node/timer/Cargo.toml index 546cc2ed41c..53fa2c01323 100644 --- a/beacon_node/timer/Cargo.toml +++ b/beacon_node/timer/Cargo.toml @@ -6,7 +6,7 @@ edition = { workspace = true } [dependencies] beacon_chain = { workspace = true } -slog = { workspace = true } slot_clock = { workspace = true } task_executor = { workspace = true } tokio = { workspace = true } +tracing = { workspace = true } diff --git a/beacon_node/timer/src/lib.rs b/beacon_node/timer/src/lib.rs index 7c2db69604b..1bd1c1e8ea6 100644 --- a/beacon_node/timer/src/lib.rs +++ b/beacon_node/timer/src/lib.rs @@ -3,22 +3,21 @@ //! This service allows task execution on the beacon node for various functionality. use beacon_chain::{BeaconChain, BeaconChainTypes}; -use slog::{info, warn}; use slot_clock::SlotClock; use std::sync::Arc; use tokio::time::sleep; +use tracing::{info, warn}; /// Spawns a timer service which periodically executes tasks for the beacon chain pub fn spawn_timer( executor: task_executor::TaskExecutor, beacon_chain: Arc>, ) -> Result<(), &'static str> { - let log = executor.log().clone(); let timer_future = async move { loop { let Some(duration_to_next_slot) = beacon_chain.slot_clock.duration_to_next_slot() else { - warn!(log, "Unable to determine duration to next slot"); + warn!("Unable to determine duration to next slot"); return; }; @@ -28,7 +27,7 @@ pub fn spawn_timer( }; executor.spawn(timer_future, "timer"); - info!(executor.log(), "Timer service started"); + info!("Timer service started"); Ok(()) } diff --git a/book/.markdownlint.yml b/book/.markdownlint.yml index 5d6bda29f1e..4f7d1133648 100644 --- a/book/.markdownlint.yml +++ b/book/.markdownlint.yml @@ -8,7 +8,7 @@ MD010: MD013: false # MD028: set to false to allow blank line between blockquote: https://github.com/DavidAnson/markdownlint/blob/main/doc/md028.md -# This is because the blockquotes are shown separatedly (a deisred outcome) when having a blank line in between +# This is because the blockquotes are shown separately (a desired outcome) when having a blank line in between MD028: false # MD024: set siblings_only to true so that same headings with different parent headings are allowed diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 44d7702e5ff..44f08615643 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -2,58 +2,55 @@ * [Introduction](./intro.md) * [Installation](./installation.md) - * [Pre-Built Binaries](./installation-binaries.md) - * [Docker](./docker.md) - * [Build from Source](./installation-source.md) - * [Raspberry Pi 4](./pi.md) - * [Cross-Compiling](./cross-compiling.md) - * [Homebrew](./homebrew.md) - * [Update Priorities](./installation-priorities.md) + * [Pre-Built Binaries](./installation_binaries.md) + * [Docker](./installation_docker.md) + * [Build from Source](./installation_source.md) + * [Cross-Compiling](./installation_cross_compiling.md) + * [Homebrew](./installation_homebrew.md) + * [Update Priorities](./installation_priorities.md) * [Run a Node](./run_a_node.md) -* [Become a Validator](./mainnet-validator.md) -* [Validator Management](./validator-management.md) - * [The `validator-manager` Command](./validator-manager.md) - * [Creating validators](./validator-manager-create.md) - * [Moving validators](./validator-manager-move.md) - * [Managing validators](./validator-manager-api.md) - * [Slashing Protection](./slashing-protection.md) - * [Voluntary Exits](./voluntary-exit.md) - * [Partial Withdrawals](./partial-withdrawal.md) - * [Validator Monitoring](./validator-monitoring.md) - * [Doppelganger Protection](./validator-doppelganger.md) - * [Suggested Fee Recipient](./suggested-fee-recipient.md) - * [Validator Graffiti](./graffiti.md) +* [Become a Validator](./mainnet_validator.md) +* [Validator Management](./validator_management.md) + * [The `validator-manager` Command](./validator_manager.md) + * [Creating validators](./validator_manager_create.md) + * [Moving validators](./validator_manager_move.md) + * [Managing validators](./validator_manager_api.md) + * [Slashing Protection](./validator_slashing_protection.md) + * [Voluntary Exits](./validator_voluntary_exit.md) + * [Validator Sweep](./validator_sweep.md) + * [Validator Monitoring](./validator_monitoring.md) + * [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) - * [Validator Inclusion APIs](./validator-inclusion.md) - * [Validator Client API](./api-vc.md) - * [Endpoints](./api-vc-endpoints.md) - * [Authorization Header](./api-vc-auth-header.md) - * [Signature Header](./api-vc-sig-header.md) - * [Prometheus Metrics](./advanced_metrics.md) -* [Lighthouse UI (Siren)](./lighthouse-ui.md) - * [Configuration](./ui-configuration.md) - * [Authentication](./ui-authentication.md) - * [Usage](./ui-usage.md) - * [FAQs](./ui-faqs.md) + * [Beacon Node API](./api_bn.md) + * [Lighthouse API](./api_lighthouse.md) + * [Validator Inclusion APIs](./api_validator_inclusion.md) + * [Validator Client API](./api_vc.md) + * [Endpoints](./api_vc_endpoints.md) + * [Authorization Header](./api_vc_auth_header.md) + * [Prometheus Metrics](./api_metrics.md) +* [Lighthouse UI (Siren)](./ui.md) + * [Configuration](./ui_configuration.md) + * [Authentication](./ui_authentication.md) + * [Usage](./ui_usage.md) + * [FAQs](./ui_faqs.md) * [Advanced Usage](./advanced.md) - * [Checkpoint Sync](./checkpoint-sync.md) - * [Custom Data Directories](./advanced-datadir.md) - * [Proposer Only Beacon Nodes](./advanced-proposer-only.md) - * [Remote Signing with Web3Signer](./validator-web3signer.md) + * [Checkpoint Sync](./advanced_checkpoint_sync.md) + * [Custom Data Directories](./advanced_datadir.md) + * [Proposer Only Beacon Nodes](./advanced_proposer_only.md) + * [Remote Signing with Web3Signer](./advanced_web3signer.md) * [Database Configuration](./advanced_database.md) - * [Database Migrations](./database-migrations.md) - * [Key Management (Deprecated)](./key-management.md) - * [Key Recovery](./key-recovery.md) + * [Database Migrations](./advanced_database_migrations.md) + * [Key Recovery](./advanced_key_recovery.md) * [Advanced Networking](./advanced_networking.md) - * [Running a Slasher](./slasher.md) - * [Redundancy](./redundancy.md) - * [Release Candidates](./advanced-release-candidates.md) - * [MEV](./builders.md) - * [Merge Migration](./merge-migration.md) - * [Late Block Re-orgs](./late-block-re-orgs.md) - * [Blobs](./advanced-blobs.md) + * [Running a Slasher](./advanced_slasher.md) + * [Redundancy](./advanced_redundancy.md) + * [Release Candidates](./advanced_release_candidates.md) + * [MEV](./advanced_builders.md) + * [Late Block Re-orgs](./advanced_re-orgs.md) + * [Blobs](./advanced_blobs.md) * [Command Line Reference (CLI)](./help_general.md) * [Beacon Node](./help_bn.md) * [Validator Client](./help_vc.md) @@ -62,7 +59,12 @@ * [Import](./help_vm_import.md) * [Move](./help_vm_move.md) * [Contributing](./contributing.md) - * [Development Environment](./setup.md) + * [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) + * [Raspberry Pi 4](./archived_pi.md) + * [Key Management](./archived_key_management.md) diff --git a/book/src/advanced.md b/book/src/advanced.md index 1a882835a47..76a7fed2029 100644 --- a/book/src/advanced.md +++ b/book/src/advanced.md @@ -6,19 +6,17 @@ elsewhere? This section provides detailed information about configuring Lighthouse for specific use cases, and tips about how things work under the hood. -* [Checkpoint Sync](./checkpoint-sync.md): quickly sync the beacon chain to perform validator duties. -* [Custom Data Directories](./advanced-datadir.md): modify the data directory to your preferred location. -* [Proposer Only Beacon Nodes](./advanced-proposer-only.md): beacon node only for proposer duty for increased anonymity. -* [Remote Signing with Web3Signer](./validator-web3signer.md): don't want to store your keystore in local node? Use web3signer. +* [Checkpoint Sync](./advanced_checkpoint_sync.md): quickly sync the beacon chain to perform validator duties. +* [Custom Data Directories](./advanced_datadir.md): modify the data directory to your preferred location. +* [Proposer Only Beacon Nodes](./advanced_proposer_only.md): beacon node only for proposer duty for increased anonymity. +* [Remote Signing with Web3Signer](./advanced_web3signer.md): don't want to store your keystore in local node? Use web3signer. * [Database Configuration](./advanced_database.md): understanding space-time trade-offs in the database. -* [Database Migrations](./database-migrations.md): have a look at all previous Lighthouse database scheme versions. -* [Key Management](./key-management.md): explore how to generate wallet with Lighthouse. -* [Key Recovery](./key-recovery.md): explore how to recover wallet and validator with Lighthouse. +* [Database Migrations](./advanced_database_migrations.md): have a look at all previous Lighthouse database scheme versions. +* [Key Recovery](./advanced_key_recovery.md): explore how to recover wallet and validator with Lighthouse. * [Advanced Networking](./advanced_networking.md): open your ports to have a diverse and healthy set of peers. -* [Running a Slasher](./slasher.md): contribute to the health of the network by running a slasher. -* [Redundancy](./redundancy.md): want to have more than one beacon node as backup? This is for you. -* [Release Candidates](./advanced-release-candidates.md): latest release of Lighthouse to get feedback from users. -* [Maximal Extractable Value](./builders.md): use external builders for a potential higher rewards during block proposals -* [Merge Migration](./merge-migration.md): look at what you need to do during a significant network upgrade: The Merge -* [Late Block Re-orgs](./late-block-re-orgs.md): read information about Lighthouse late block re-orgs. -* [Blobs](./advanced-blobs.md): information about blobs in Deneb upgrade +* [Running a Slasher](./advanced_slasher.md): contribute to the health of the network by running a slasher. +* [Redundancy](./advanced_redundancy.md): want to have more than one beacon node as backup? This is for you. +* [Release Candidates](./advanced_release_candidates.md): latest release of Lighthouse to get feedback from users. +* [Maximal Extractable Value](./advanced_builders.md): use external builders for a potential higher rewards during block proposals +* [Late Block Re-orgs](./advanced_re-orgs.md): read information about Lighthouse late block re-orgs. +* [Blobs](./advanced_blobs.md): information about blobs in Deneb upgrade diff --git a/book/src/advanced-blobs.md b/book/src/advanced_blobs.md similarity index 78% rename from book/src/advanced-blobs.md rename to book/src/advanced_blobs.md index 785bd5797dd..524f70219f1 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. @@ -38,4 +40,4 @@ In the Deneb network upgrade, one of the changes is the implementation of EIP-48 curl "http://localhost:5052/lighthouse/database/info" | jq ``` - Refer to [Lighthouse API](./api-lighthouse.md#lighthousedatabaseinfo) for an example response. + Refer to [Lighthouse API](./api_lighthouse.md#lighthousedatabaseinfo) for an example response. diff --git a/book/src/builders.md b/book/src/advanced_builders.md similarity index 95% rename from book/src/builders.md rename to book/src/advanced_builders.md index 5b8e9ddb8b7..d9468898b4a 100644 --- a/book/src/builders.md +++ b/book/src/advanced_builders.md @@ -83,11 +83,11 @@ is something afoot. To update gas limit per-validator you can use the [standard key manager API][gas-limit-api]. -Alternatively, you can use the [lighthouse API](api-vc-endpoints.md). See below for an example. +Alternatively, you can use the [lighthouse API](api_vc_endpoints.md). See below for an example. ### Enable/Disable builder proposals via HTTP -Use the [lighthouse API](api-vc-endpoints.md) to enable/disable use of the builder API on a per-validator basis. +Use the [lighthouse API](api_vc_endpoints.md) to enable/disable use of the builder API on a per-validator basis. You can also update the configured gas limit with these requests. #### `PATCH /lighthouse/validators/:voting_pubkey` @@ -98,7 +98,7 @@ You can also update the configured gas limit with these requests. |-------------------|--------------------------------------------| | Path | `/lighthouse/validators/:voting_pubkey` | | Method | PATCH | -| Required Headers | [`Authorization`](./api-vc-auth-header.md) | +| Required Headers | [`Authorization`](./api_vc_auth_header.md) | | Typical Responses | 200, 400 | #### Example Path @@ -147,7 +147,7 @@ INFO Published validator registrations to the builder network, count: 3, service ### Fee Recipient -Refer to [suggested fee recipient](suggested-fee-recipient.md) documentation. +Refer to [suggested fee recipient](validator_fee_recipient.md) documentation. ### Validator definitions example @@ -244,16 +244,9 @@ INFO Builder payload ignored INFO Chain is unhealthy, using local payload ``` -In case of fallback you should see a log indicating that the locally produced payload was -used in place of one from the builder: - -```text -INFO Reconstructing a full block using a local payload -``` - ## Information for block builders and relays -Block builders and relays can query beacon node events from the [Events API](https://ethereum.github.io/beacon-APIs/#/Events/eventstream). An example of querying the payload attributes in the Events API is outlined in [Beacon node API - Events API](./api-bn.md#events-api) +Block builders and relays can query beacon node events from the [Events API](https://ethereum.github.io/beacon-APIs/#/Events/eventstream). An example of querying the payload attributes in the Events API is outlined in [Beacon node API - Events API](./api_bn.md#events-api) [mev-rs]: https://github.com/ralexstokes/mev-rs [mev-boost]: https://github.com/flashbots/mev-boost diff --git a/book/src/checkpoint-sync.md b/book/src/advanced_checkpoint_sync.md similarity index 99% rename from book/src/checkpoint-sync.md rename to book/src/advanced_checkpoint_sync.md index 8dd63f77c9b..45aed6ef58a 100644 --- a/book/src/checkpoint-sync.md +++ b/book/src/advanced_checkpoint_sync.md @@ -134,7 +134,7 @@ Important information to be aware of: * It is safe to interrupt state reconstruction by gracefully terminating the node – it will pick up from where it left off when it restarts. * You can start reconstruction from the HTTP API, and view its progress. See the - [`/lighthouse/database`](./api-lighthouse.md) APIs. + [`/lighthouse/database`](./api_lighthouse.md) APIs. For more information on historic state storage see the [Database Configuration](./advanced_database.md) page. diff --git a/book/src/advanced_database.md b/book/src/advanced_database.md index b558279730e..4e77046c2dd 100644 --- a/book/src/advanced_database.md +++ b/book/src/advanced_database.md @@ -61,6 +61,26 @@ that we have observed are: to apply. We observed no significant performance benefit from `--hierarchy-exponents 5,7,11`, and a substantial increase in space consumed. +The following table lists the data for different configurations. Note that the disk space requirement is for the `chain_db` and `freezer_db`, excluding the `blobs_db`. + +| Hierarchy Exponents | Storage Requirement | Sequential Slot Query | Uncached Query | Time to Sync | +|---|---|---|---|---| +| 5,9,11,13,16,18,21 (default) | 418 GiB | 250-700 ms | up to 10 s | 1 week | +| 5,7,11 (frequent snapshots) | 589 GiB | 250-700 ms | up to 6 s | 1 week | +| 0,5,7,11 (per-slot diffs) | 2500 GiB | 250-700 ms | up to 4 s | 7 weeks | + +[Jim](https://github.com/mcdee) has done some experiments to study the response time of querying random slots (uncached query) for `--hierarchy-exponents 0,5,7,11` (per-slot diffs) and `--hierarchy-exponents 5,9,11,13,17,21` (per-epoch diffs), as show in the figures below. From the figures, two points can be concluded: + +- response time (y-axis) increases with slot number (x-axis) due to state growth. +- response time for per-slot configuration in general is 2x faster than that of per-epoch. + +In short, setting different configurations is a trade-off between disk space requirement, sync time and response time. The data presented here is useful to help users choosing the configuration that suit their needs. + +_We acknowledge the data provided by [Jim](https://github.com/mcdee) and his consent for us to share it here._ + +![Response time for per-epoch archive](./imgs/per-epoch.png) +![Response time for per-slot archive](./imgs/per-slot.png) + If in doubt, we recommend running with the default configuration! It takes a long time to reconstruct states in any given configuration, so it might be some time before the optimal configuration is determined. diff --git a/book/src/database-migrations.md b/book/src/advanced_database_migrations.md similarity index 95% rename from book/src/database-migrations.md rename to book/src/advanced_database_migrations.md index a9bfb00ccda..3c56fcadc13 100644 --- a/book/src/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). @@ -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-datadir.md b/book/src/advanced_datadir.md similarity index 98% rename from book/src/advanced-datadir.md rename to book/src/advanced_datadir.md index 7ad993a1072..1be8ed5a348 100644 --- a/book/src/advanced-datadir.md +++ b/book/src/advanced_datadir.md @@ -12,7 +12,7 @@ lighthouse --network mainnet --datadir /var/lib/my-custom-dir bn --staking lighthouse --network mainnet --datadir /var/lib/my-custom-dir vc ``` -The first step creates a `validators` directory under `/var/lib/my-custom-dir` which contains the imported keys and [`validator_definitions.yml`](./validator-management.md). +The first step creates a `validators` directory under `/var/lib/my-custom-dir` which contains the imported keys and [`validator_definitions.yml`](./validator_management.md). After that, we simply run the beacon chain and validator client with the custom dir path. ## Relative Paths diff --git a/book/src/key-recovery.md b/book/src/advanced_key_recovery.md similarity index 100% rename from book/src/key-recovery.md rename to book/src/advanced_key_recovery.md diff --git a/book/src/advanced_networking.md b/book/src/advanced_networking.md index 0dc1000aa04..0dc53bd42aa 100644 --- a/book/src/advanced_networking.md +++ b/book/src/advanced_networking.md @@ -123,8 +123,12 @@ Lighthouse listens for connections, and the parameters used to tell other peers how to connect to your node. This distinction is relevant and applies to most nodes that do not run directly on a public network. +Since Lighthouse v7.0.0, Lighthouse listens to both IPv4 and IPv6 by default if it detects a globally routable IPv6 address. This means that dual-stack is enabled by default. + ### Configuring Lighthouse to listen over IPv4/IPv6/Dual stack +To listen over only IPv4 and not IPv6, use the flag `--listen-address 0.0.0.0`. + To listen over only IPv6 use the same parameters as done when listening over IPv4 only: @@ -136,7 +140,7 @@ TCP and UDP. If the specified port is 9909, QUIC will use port 9910 for IPv6 UDP connections. This can be configured with `--quic-port`. -To listen over both IPv4 and IPv6: +To listen over both IPv4 and IPv6 and using a different port for IPv6:: - Set two listening addresses using the `--listen-address` flag twice ensuring the two addresses are one IPv4, and the other IPv6. When doing so, the @@ -165,7 +169,7 @@ To listen over both IPv4 and IPv6: > It listens on the default value of --port6 (`9000`) for both UDP and TCP. > QUIC will use port `9001` for UDP, which is the default `--port6` value (`9000`) + 1. -> When using `--listen-address :: --listen-address --port 9909 --discovery-port6 9999`, listening will be set up as follows: +> When using `--listen-address :: --listen-address 0.0.0.0 --port 9909 --discovery-port6 9999`, listening will be set up as follows: > > **IPv4**: > diff --git a/book/src/advanced-proposer-only.md b/book/src/advanced_proposer_only.md similarity index 97% rename from book/src/advanced-proposer-only.md rename to book/src/advanced_proposer_only.md index 1ea36109883..f55e51606cf 100644 --- a/book/src/advanced-proposer-only.md +++ b/book/src/advanced_proposer_only.md @@ -56,7 +56,7 @@ these nodes for added security). The intended set-up to take advantage of this mechanism is to run one (or more) normal beacon nodes in conjunction with one (or more) proposer-only beacon -nodes. See the [Redundancy](./redundancy.md) section for more information about +nodes. See the [Redundancy](./advanced_redundancy.md) section for more information about setting up redundant beacon nodes. The proposer-only beacon nodes should be setup to use a different IP address than the primary (non proposer-only) nodes. For added security, the IP addresses of the proposer-only nodes should be diff --git a/book/src/late-block-re-orgs.md b/book/src/advanced_re-orgs.md similarity index 100% rename from book/src/late-block-re-orgs.md rename to book/src/advanced_re-orgs.md diff --git a/book/src/redundancy.md b/book/src/advanced_redundancy.md similarity index 80% rename from book/src/redundancy.md rename to book/src/advanced_redundancy.md index daf0eb4a5b4..4c231ed6ab3 100644 --- a/book/src/redundancy.md +++ b/book/src/advanced_redundancy.md @@ -9,7 +9,7 @@ There are three places in Lighthouse where redundancy is notable: We mention (3) since it is unsafe and should not be confused with the other two uses of redundancy. **Running the same validator keypair in more than one validator client (Lighthouse, or otherwise) will eventually lead to slashing.** -See [Slashing Protection](./slashing-protection.md) for more information. +See [Slashing Protection](./validator_slashing_protection.md) for more information. From this paragraph, this document will *only* refer to the first two items (1, 2). We *never* recommend that users implement redundancy for validator keypairs. @@ -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 @@ -58,8 +55,8 @@ following flags: > Note: You could also use `--http-address 0.0.0.0`, but this allows *any* external IP address to access the HTTP server. As such, a firewall should be configured to deny unauthorized access to port `5052`. -- `--execution-endpoint`: see [Merge Migration](./merge-migration.md). -- `--execution-jwt`: see [Merge Migration](./merge-migration.md). +- `--execution-endpoint`: see [Merge Migration](./archived_merge_migration.md). +- `--execution-jwt`: see [Merge Migration](./archived_merge_migration.md). For example one could use the following command to provide a backup beacon node: @@ -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 @@ -107,7 +120,7 @@ The default is `--broadcast subscriptions`. To also broadcast blocks for example Lighthouse previously supported redundant execution nodes for fetching data from the deposit contract. On merged networks *this is no longer supported*. Each Lighthouse beacon node must be configured in a 1:1 relationship with an execution node. For more information on the rationale -behind this decision please see the [Merge Migration](./merge-migration.md) documentation. +behind this decision please see the [Merge Migration](./archived_merge_migration.md) documentation. To achieve redundancy we recommend configuring [Redundant beacon nodes](#redundant-beacon-nodes) where each has its own execution engine. diff --git a/book/src/advanced-release-candidates.md b/book/src/advanced_release_candidates.md similarity index 100% rename from book/src/advanced-release-candidates.md rename to book/src/advanced_release_candidates.md diff --git a/book/src/slasher.md b/book/src/advanced_slasher.md similarity index 99% rename from book/src/slasher.md rename to book/src/advanced_slasher.md index 3310f6c9eff..b354c9deb25 100644 --- a/book/src/slasher.md +++ b/book/src/advanced_slasher.md @@ -81,7 +81,7 @@ WARN Slasher backend override failed advice: delete old MDBX database or enab In this case you should either obtain a Lighthouse binary with the MDBX backend enabled, or delete the files for the old backend. The pre-built Lighthouse binaries and Docker images have MDBX enabled, -or if you're [building from source](./installation-source.md) you can enable the `slasher-mdbx` feature. +or if you're [building from source](./installation_source.md) you can enable the `slasher-mdbx` feature. To delete the files, use the `path` from the `WARN` log, and then delete the `mbdx.dat` and `mdbx.lck` files. diff --git a/book/src/validator-web3signer.md b/book/src/advanced_web3signer.md similarity index 95% rename from book/src/validator-web3signer.md rename to book/src/advanced_web3signer.md index 6a518af3cfc..6145fd4a716 100644 --- a/book/src/validator-web3signer.md +++ b/book/src/advanced_web3signer.md @@ -30,7 +30,7 @@ or effectiveness. ## Usage A remote signing validator is added to Lighthouse in much the same way as one that uses a local -keystore, via the [`validator_definitions.yml`](./validator-management.md) file or via the [`POST /lighthouse/validators/web3signer`](./api-vc-endpoints.md#post-lighthousevalidatorsweb3signer) API endpoint. +keystore, via the [`validator_definitions.yml`](./validator_management.md) file or via the [`POST /lighthouse/validators/web3signer`](./api_vc_endpoints.md#post-lighthousevalidatorsweb3signer) API endpoint. Here is an example of a `validator_definitions.yml` file containing one validator which uses a remote signer: diff --git a/book/src/api.md b/book/src/api.md index 5837ad9654a..912c8658b67 100644 --- a/book/src/api.md +++ b/book/src/api.md @@ -5,5 +5,5 @@ RESTful HTTP/JSON APIs. There are two APIs served by Lighthouse: -- [Beacon Node API](./api-bn.md) -- [Validator Client API](./api-vc.md) +- [Beacon Node API](./api_bn.md) +- [Validator Client API](./api_vc.md) diff --git a/book/src/api-bn.md b/book/src/api_bn.md similarity index 100% rename from book/src/api-bn.md rename to book/src/api_bn.md diff --git a/book/src/api-lighthouse.md b/book/src/api_lighthouse.md similarity index 98% rename from book/src/api-lighthouse.md rename to book/src/api_lighthouse.md index 5428ab8f9ae..b65bef47628 100644 --- a/book/src/api-lighthouse.md +++ b/book/src/api_lighthouse.md @@ -347,11 +347,11 @@ curl -X GET "http://localhost:5052/lighthouse/proto_array" -H "accept: applicat ## `/lighthouse/validator_inclusion/{epoch}/{validator_id}` -See [Validator Inclusion APIs](./validator-inclusion.md). +See [Validator Inclusion APIs](./api_validator_inclusion.md). ## `/lighthouse/validator_inclusion/{epoch}/global` -See [Validator Inclusion APIs](./validator-inclusion.md). +See [Validator Inclusion APIs](./api_validator_inclusion.md). ## `/lighthouse/eth1/syncing` @@ -565,7 +565,7 @@ For archive nodes, the `anchor` will be: indicating that all states with slots `>= 0` are available, i.e., full state history. For more information on the specific meanings of these fields see the docs on [Checkpoint -Sync](./checkpoint-sync.md#reconstructing-states). +Sync](./advanced_checkpoint_sync.md#reconstructing-states). ## `/lighthouse/merge_readiness` @@ -812,9 +812,15 @@ Checks if the ports are open. curl -X GET "http://localhost:5052/lighthouse/nat" | jq ``` -An open port will return: +An example of response: ```json { - "data": true + "data": { + "discv5_ipv4": true, + "discv5_ipv6": false, + "libp2p_ipv4": true, + "libp2p_ipv6": false + } } +``` diff --git a/book/src/advanced_metrics.md b/book/src/api_metrics.md similarity index 97% rename from book/src/advanced_metrics.md rename to book/src/api_metrics.md index 323ba8f58a7..c124d3acb75 100644 --- a/book/src/advanced_metrics.md +++ b/book/src/api_metrics.md @@ -68,7 +68,7 @@ The specification for the monitoring endpoint can be found here: - -_Note: the similarly named [Validator Monitor](./validator-monitoring.md) feature is entirely +_Note: the similarly named [Validator Monitor](./validator_monitoring.md) feature is entirely independent of remote metric monitoring_. ### Update Period diff --git a/book/src/validator-inclusion.md b/book/src/api_validator_inclusion.md similarity index 100% rename from book/src/validator-inclusion.md rename to book/src/api_validator_inclusion.md diff --git a/book/src/api-vc.md b/book/src/api_vc.md similarity index 91% rename from book/src/api-vc.md rename to book/src/api_vc.md index 630a0320066..f5df5df76c3 100644 --- a/book/src/api-vc.md +++ b/book/src/api_vc.md @@ -6,11 +6,10 @@ of validators and keys. The API includes all of the endpoints from the [standard keymanager API](https://ethereum.github.io/keymanager-APIs/) that is implemented by other clients and remote signers. It also includes some Lighthouse-specific endpoints which are described in -[Endpoints](./api-vc-endpoints.md). +[Endpoints](./api_vc_endpoints.md). > Note: All requests to the HTTP server must supply an -> [`Authorization`](./api-vc-auth-header.md) header. All responses contain a -> [`Signature`](./api-vc-sig-header.md) header for optional verification. +> [`Authorization`](./api_vc_auth_header.md) header. ## Starting the server diff --git a/book/src/api-vc-auth-header.md b/book/src/api_vc_auth_header.md similarity index 100% rename from book/src/api-vc-auth-header.md rename to book/src/api_vc_auth_header.md diff --git a/book/src/api-vc-endpoints.md b/book/src/api_vc_endpoints.md similarity index 92% rename from book/src/api-vc-endpoints.md rename to book/src/api_vc_endpoints.md index 98605a3dcd0..e51f5d29ae1 100644 --- a/book/src/api-vc-endpoints.md +++ b/book/src/api_vc_endpoints.md @@ -18,8 +18,9 @@ | [`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). +The query to Lighthouse API endpoints requires authorization, see [Authorization Header](./api_vc_auth_header.md). In addition to the above endpoints Lighthouse also supports all of the [standard keymanager APIs](https://ethereum.github.io/keymanager-APIs/). @@ -33,7 +34,7 @@ Returns the software version and `git` commit hash for the Lighthouse binary. |-------------------|--------------------------------------------| | Path | `/lighthouse/version` | | Method | GET | -| Required Headers | [`Authorization`](./api-vc-auth-header.md) | +| Required Headers | [`Authorization`](./api_vc_auth_header.md) | | Typical Responses | 200 | Command: @@ -71,7 +72,7 @@ Returns information regarding the health of the host machine. |-------------------|--------------------------------------------| | Path | `/lighthouse/health` | | Method | GET | -| Required Headers | [`Authorization`](./api-vc-auth-header.md) | +| Required Headers | [`Authorization`](./api_vc_auth_header.md) | | Typical Responses | 200 | *Note: this endpoint is presently only available on Linux.* @@ -132,7 +133,7 @@ Returns information regarding the health of the host machine. |-------------------|--------------------------------------------| | Path | `/lighthouse/ui/health` | | Method | GET | -| Required Headers | [`Authorization`](./api-vc-auth-header.md) | +| Required Headers | [`Authorization`](./api_vc_auth_header.md) | | Typical Responses | 200 | Command: @@ -178,7 +179,7 @@ Returns the graffiti that will be used for the next block proposal of each valid |-------------------|--------------------------------------------| | Path | `/lighthouse/ui/graffiti` | | Method | GET | -| Required Headers | [`Authorization`](./api-vc-auth-header.md) | +| Required Headers | [`Authorization`](./api_vc_auth_header.md) | | Typical Responses | 200 | Command: @@ -210,7 +211,7 @@ Returns the Ethereum proof-of-stake consensus specification loaded for this vali |-------------------|--------------------------------------------| | Path | `/lighthouse/spec` | | Method | GET | -| Required Headers | [`Authorization`](./api-vc-auth-header.md) | +| Required Headers | [`Authorization`](./api_vc_auth_header.md) | | Typical Responses | 200 | Command: @@ -326,7 +327,7 @@ Example Response Body ## `GET /lighthouse/auth` -Fetch the filesystem path of the [authorization token](./api-vc-auth-header.md). +Fetch the filesystem path of the [authorization token](./api_vc_auth_header.md). Unlike the other endpoints this may be called *without* providing an authorization token. This API is intended to be called from the same machine as the validator client, so that the token @@ -365,7 +366,7 @@ Lists all validators managed by this validator client. |-------------------|--------------------------------------------| | Path | `/lighthouse/validators` | | Method | GET | -| Required Headers | [`Authorization`](./api-vc-auth-header.md) | +| Required Headers | [`Authorization`](./api_vc_auth_header.md) | | Typical Responses | 200 | Command: @@ -409,7 +410,7 @@ Get a validator by their `voting_pubkey`. |-------------------|--------------------------------------------| | Path | `/lighthouse/validators/:voting_pubkey` | | Method | GET | -| Required Headers | [`Authorization`](./api-vc-auth-header.md) | +| Required Headers | [`Authorization`](./api_vc_auth_header.md) | | Typical Responses | 200, 400 | Command: @@ -441,7 +442,7 @@ and `graffiti`. The following example updates a validator from `enabled: true` |-------------------|--------------------------------------------| | Path | `/lighthouse/validators/:voting_pubkey` | | Method | PATCH | -| Required Headers | [`Authorization`](./api-vc-auth-header.md) | +| Required Headers | [`Authorization`](./api_vc_auth_header.md) | | Typical Responses | 200, 400 | Example Request Body @@ -491,7 +492,7 @@ Validators are generated from the mnemonic according to |-------------------|--------------------------------------------| | Path | `/lighthouse/validators` | | Method | POST | -| Required Headers | [`Authorization`](./api-vc-auth-header.md) | +| Required Headers | [`Authorization`](./api_vc_auth_header.md) | | Typical Responses | 200 | ### Example Request Body @@ -580,7 +581,7 @@ Import a keystore into the validator client. |-------------------|--------------------------------------------| | Path | `/lighthouse/validators/keystore` | | Method | POST | -| Required Headers | [`Authorization`](./api-vc-auth-header.md) | +| Required Headers | [`Authorization`](./api_vc_auth_header.md) | | Typical Responses | 200 | ### Example Request Body @@ -676,7 +677,7 @@ generated with the path `m/12381/3600/i/42`. |-------------------|--------------------------------------------| | Path | `/lighthouse/validators/mnemonic` | | Method | POST | -| Required Headers | [`Authorization`](./api-vc-auth-header.md) | +| Required Headers | [`Authorization`](./api_vc_auth_header.md) | | Typical Responses | 200 | ### Example Request Body @@ -739,7 +740,7 @@ Create any number of new validators, all of which will refer to a |-------------------|--------------------------------------------| | Path | `/lighthouse/validators/web3signer` | | Method | POST | -| Required Headers | [`Authorization`](./api-vc-auth-header.md) | +| Required Headers | [`Authorization`](./api_vc_auth_header.md) | | Typical Responses | 200, 400 | ### Example Request Body @@ -816,3 +817,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 new file mode 100644 index 00000000000..d37cd9aa154 --- /dev/null +++ b/book/src/archived.md @@ -0,0 +1,3 @@ +# Archived + +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/key-management.md b/book/src/archived_key_management.md similarity index 98% rename from book/src/key-management.md rename to book/src/archived_key_management.md index fa6e99a2aa6..3f600794e06 100644 --- a/book/src/key-management.md +++ b/book/src/archived_key_management.md @@ -1,4 +1,4 @@ -# Key Management (Deprecated) +# Key Management [launchpad]: https://launchpad.ethereum.org/ @@ -22,7 +22,7 @@ Rather than continuing to read this page, we recommend users visit either: - The [Staking Launchpad][launchpad] for detailed, beginner-friendly instructions. - The [staking-deposit-cli](https://github.com/ethereum/staking-deposit-cli) for a CLI tool used by the [Staking Launchpad][launchpad]. -- The [validator-manager documentation](./validator-manager.md) for a Lighthouse-specific tool for streamlined validator management tools. +- The [validator-manager documentation](./validator_manager.md) for a Lighthouse-specific tool for streamlined validator management tools. ## The `lighthouse account-manager` diff --git a/book/src/merge-migration.md b/book/src/archived_merge_migration.md similarity index 99% rename from book/src/merge-migration.md rename to book/src/archived_merge_migration.md index 7a123254bf7..ac9c78c5e3b 100644 --- a/book/src/merge-migration.md +++ b/book/src/archived_merge_migration.md @@ -14,7 +14,7 @@ the merge: 2. If your Lighthouse node has validators attached you *must* nominate an Ethereum address to receive transactions tips from blocks proposed by your validators. These changes should be made to your `lighthouse vc` configuration, and are covered on the - [Suggested fee recipient](./suggested-fee-recipient.md) page. + [Suggested fee recipient](./validator_fee_recipient.md) page. Additionally, you *must* update Lighthouse to v3.0.0 (or later), and must update your execution engine to a merge-ready version. diff --git a/book/src/pi.md b/book/src/archived_pi.md similarity index 91% rename from book/src/pi.md rename to book/src/archived_pi.md index b91ecab548c..6afbcebd66e 100644 --- a/book/src/pi.md +++ b/book/src/archived_pi.md @@ -7,7 +7,7 @@ Tested on: - Raspberry Pi 4 Model B (4GB) - `Ubuntu 20.04 LTS (GNU/Linux 5.4.0-1011-raspi aarch64)` -*Note: [Lighthouse supports cross-compiling](./cross-compiling.md) to target a +*Note: [Lighthouse supports cross-compiling](./installation_cross_compiling.md) to target a Raspberry Pi (`aarch64`). Compiling on a faster machine (i.e., `x86_64` desktop) may be convenient.* @@ -58,7 +58,7 @@ make > > Compiling Lighthouse can take up to an hour. The safety guarantees provided by the Rust language unfortunately result in a lengthy compilation time on a low-spec CPU like a Raspberry Pi. For faster -compilation on low-spec hardware, try [cross-compiling](./cross-compiling.md) on a more powerful +compilation on low-spec hardware, try [cross-compiling](./installation_cross_compiling.md) on a more powerful computer (e.g., compile for RasPi from your desktop computer). Once installation has finished, confirm Lighthouse is installed by viewing the diff --git a/book/src/contributing.md b/book/src/contributing.md index 312acccbc04..332afbfd702 100644 --- a/book/src/contributing.md +++ b/book/src/contributing.md @@ -15,7 +15,7 @@ to work on. To start contributing, 1. Read our [how to contribute](https://github.com/sigp/lighthouse/blob/unstable/CONTRIBUTING.md) document. -2. Setup a [development environment](./setup.md). +2. Setup a [development environment](./contributing_setup.md). 3. Browse through the [open issues](https://github.com/sigp/lighthouse/issues) (tip: look for the [good first issue](https://github.com/sigp/lighthouse/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) @@ -127,5 +127,5 @@ suggest: - [Rust by example](https://doc.rust-lang.org/stable/rust-by-example/) - [Learning Rust With Entirely Too Many Linked Lists](http://cglab.ca/~abeinges/blah/too-many-lists/book/) - [Rustlings](https://github.com/rustlings/rustlings) -- [Rust Exercism](https://exercism.io/tracks/rust) +- [Rust Exercism](https://exercism.org/tracks/rust) - [Learn X in Y minutes - Rust](https://learnxinyminutes.com/docs/rust/) diff --git a/book/src/setup.md b/book/src/contributing_setup.md similarity index 97% rename from book/src/setup.md rename to book/src/contributing_setup.md index d3da68f97cc..7143c8f0fb0 100644 --- a/book/src/setup.md +++ b/book/src/contributing_setup.md @@ -17,9 +17,6 @@ The additional requirements for developers are: some dependencies. See [`Installation Guide`](./installation.md) for more info. - [`java 17 runtime`](https://openjdk.java.net/projects/jdk/). 17 is the minimum, used by web3signer_tests. -- [`libpq-dev`](https://www.postgresql.org/docs/devel/libpq.html). Also known as - `libpq-devel` on some systems. -- [`docker`](https://www.docker.com/). Some tests need docker installed and **running**. ## Using `make` diff --git a/book/src/developers_architecture.md b/book/src/developers_architecture.md new file mode 100644 index 00000000000..11505255120 --- /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 d23951c8c77..62a93166b1a 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? @@ -146,7 +142,7 @@ An example of the full log is shown below: WARN BlockProcessingFailure outcome: MissingBeaconBlock(0xbdba211f8d72029554e405d8e4906690dca807d1d7b1bc8c9b88d7970f1648bc), msg: unexpected condition in processing block. ``` -`MissingBeaconBlock` suggests that the database has corrupted. You should wipe the database and use [Checkpoint Sync](./checkpoint-sync.md) to resync the beacon chain. +`MissingBeaconBlock` suggests that the database has corrupted. You should wipe the database and use [Checkpoint Sync](./advanced_checkpoint_sync.md) to resync the beacon chain. ### After checkpoint sync, the progress of `downloading historical blocks` is slow. Why? @@ -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: - -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 - ``` - - 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: +An example of the log is shown below - ```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 - ``` +```text +WARN Error processing HTTP API request method: GET, path: /eth/v1/validator/attestation_data, status: 500 Internal Server Error, elapsed: 305.65µs +``` - 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,84 +192,13 @@ 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 duplicate your JSON keystores and don't run `lighthouse vc` twice). This will lead to slashing. However, there are some components which can be configured with redundancy. See the -[Redundancy](./redundancy.md) guide for more information. +[Redundancy](./advanced_redundancy.md) guide for more information. ### I am missing attestations. Why? @@ -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/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. @@ -341,13 +258,13 @@ No. You can just import new validator keys to the destination directory. If the Generally yes. -If you do not want to stop `lighthouse vc`, you can use the [key manager API](./api-vc-endpoints.md) to import keys. +If you do not want to stop `lighthouse vc`, you can use the [key manager API](./api_vc_endpoints.md) to import keys. ### How can I delete my validator once it is imported? -Lighthouse supports the [KeyManager API](https://ethereum.github.io/keymanager-APIs/#/Local%20Key%20Manager/deleteKeys) to delete validators and remove them from the `validator_definitions.yml` file. To do so, start the validator client with the flag `--http` and call the API. +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. +If you are looking to delete the validators in one node and import it to another, you can use the [validator-manager](./validator_manager_move.md) to move the validators across nodes without the hassle of deleting and importing the keys. ## Network, Monitoring and Maintenance @@ -389,9 +306,9 @@ expect, there are a few things to check on: ### How do I update lighthouse? -If you are updating to new release binaries, it will be the same process as described [here.](./installation-binaries.md) +If you are updating to new release binaries, it will be the same process as described [here.](./installation_binaries.md) -If you are updating by rebuilding from source, see [here.](./installation-source.md#update-lighthouse) +If you are updating by rebuilding from source, see [here.](./installation_source.md#update-lighthouse) If you are running the docker image provided by Sigma Prime on Dockerhub, you can update to specific versions, for example: @@ -399,7 +316,7 @@ If you are running the docker image provided by Sigma Prime on Dockerhub, you ca docker pull sigp/lighthouse:v1.0.0 ``` -If you are building a docker image, the process will be similar to the one described [here.](./docker.md#building-the-docker-image) +If you are building a docker image, the process will be similar to the one described [here.](./installation_docker.md#building-the-docker-image) You just need to make sure the code you have checked out is up to date. ### Do I need to set up any port mappings (port forwarding)? @@ -436,7 +353,7 @@ Opening these ports will make your Lighthouse node maximally contactable. Apart from using block explorers, you may use the "Validator Monitor" built into Lighthouse which provides logging and Prometheus/Grafana metrics for individual validators. See [Validator -Monitoring](./validator-monitoring.md) for more information. Lighthouse has also developed Lighthouse UI (Siren) to monitor performance, see [Lighthouse UI (Siren)](./lighthouse-ui.md). +Monitoring](./validator_monitoring.md) for more information. Lighthouse has also developed Lighthouse UI (Siren) to monitor performance, see [Lighthouse UI (Siren)](./ui.md). ### My beacon node and validator client are on different servers. How can I point the validator client to the beacon node? @@ -454,7 +371,7 @@ The setting on the beacon node is the same for both cases below. In the beacon n curl "http://local_IP:5052/eth/v1/node/version" ``` - You can refer to [Redundancy](./redundancy.md) for more information. + You can refer to [Redundancy](./advanced_redundancy.md) for more information. 2. If the beacon node and validator clients are on different servers _and different networks_, it is necessary to perform port forwarding of the SSH port (e.g., the default port 22) on the router, and also allow firewall on the SSH port. The connection can be established via port forwarding on the router. @@ -514,11 +431,11 @@ which shows that there are a total of 36 peers connected via QUIC. ### What should I do if I lose my slashing protection database? -See [here](./slashing-protection.md#misplaced-slashing-database). +See [here](./validator_slashing_protection.md#misplaced-slashing-database). ### I can't compile lighthouse -See [here.](./installation-source.md#troubleshooting) +See [here.](./installation_source.md#troubleshooting) ### How do I check the version of Lighthouse that is running? @@ -550,7 +467,7 @@ which says that the version is v4.1.0. ### Does Lighthouse have pruning function like the execution client to save disk space? -Yes, Lighthouse supports [state pruning](./database-migrations.md#how-to-prune-historic-states) which can help to save disk space. +Yes, Lighthouse supports [state pruning](./advanced_database_migrations.md#how-to-prune-historic-states) which can help to save disk space. ### Can I use a HDD for the freezer database and only have the hot db on SSD? diff --git a/book/src/help_bn.md b/book/src/help_bn.md index 1942f737dfe..35ad020b74f 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -78,8 +78,7 @@ Options: custom datadirs for different networks. --debug-level Specifies the verbosity level used when emitting logs to the terminal. - [default: info] [possible values: info, debug, trace, warn, error, - crit] + [default: info] [possible values: info, debug, trace, warn, error] --discovery-port The UDP port that discovery will listen on. Defaults to `port` --discovery-port6 @@ -167,7 +166,7 @@ Options: then this value will be ignored. --genesis-state-url-timeout The timeout in seconds for the request to --genesis-state-url. - [default: 180] + [default: 300] --graffiti Specify your custom graffiti to be included in blocks. Defaults to the current version and commit, truncated to fit in 32 bytes. @@ -245,15 +244,11 @@ Options: --log-format Specifies the log format used when emitting logs to the terminal. [possible values: JSON] - --logfile - File path where the log file will be stored. Once it grows to the - value specified in `--logfile-max-size` a new log file is generated - where future logs are stored. Once the number of log files exceeds the - value specified in `--logfile-max-number` the oldest log file will be - overwritten. --logfile-debug-level The verbosity level used when emitting logs to the log file. [default: - debug] [possible values: info, debug, trace, warn, error, crit] + debug] [possible values: info, debug, trace, warn, error] + --logfile-dir + Directory path where the log file will be stored --logfile-format Specifies the log format used when emitting logs to the logfile. [possible values: DEFAULT, JSON] @@ -520,8 +515,13 @@ Flags: all attestations are received for import. --light-client-server DEPRECATED - --log-color - Force outputting colors when emitting logs to the terminal. + --log-color [] + Enables/Disables colors for logs in terminal. Set it to false to + disable colors. [default: true] [possible values: true, false] + --log-extra-info + If present, show module,file,line in logs + --logfile-color + Enables colors in logfile. --logfile-compress If present, compress old log files. This can help reduce the space needed to store old logs. diff --git a/book/src/help_general.md b/book/src/help_general.md index f45f552f58c..fbc3ca25578 100644 --- a/book/src/help_general.md +++ b/book/src/help_general.md @@ -42,8 +42,7 @@ Options: custom datadirs for different networks. --debug-level Specifies the verbosity level used when emitting logs to the terminal. - [default: info] [possible values: info, debug, trace, warn, error, - crit] + [default: info] [possible values: info, debug, trace, warn, error] --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 @@ -52,19 +51,15 @@ Options: then this value will be ignored. --genesis-state-url-timeout The timeout in seconds for the request to --genesis-state-url. - [default: 180] + [default: 300] --log-format Specifies the log format used when emitting logs to the terminal. [possible values: JSON] - --logfile - File path where the log file will be stored. Once it grows to the - value specified in `--logfile-max-size` a new log file is generated - where future logs are stored. Once the number of log files exceeds the - value specified in `--logfile-max-number` the oldest log file will be - overwritten. --logfile-debug-level The verbosity level used when emitting logs to the log file. [default: - debug] [possible values: info, debug, trace, warn, error, crit] + debug] [possible values: info, debug, trace, warn, error] + --logfile-dir + Directory path where the log file will be stored --logfile-format Specifies the log format used when emitting logs to the logfile. [possible values: DEFAULT, JSON] @@ -93,8 +88,13 @@ Flags: debugging specific memory allocation issues. -h, --help Prints help information - --log-color - Force outputting colors when emitting logs to the terminal. + --log-color [] + Enables/Disables colors for logs in terminal. Set it to false to + disable colors. [default: true] [possible values: true, false] + --log-extra-info + If present, show module,file,line in logs + --logfile-color + Enables colors in logfile. --logfile-compress If present, compress old log files. This can help reduce the space needed to store old logs. diff --git a/book/src/help_vc.md b/book/src/help_vc.md index 87f47a1eca1..15b5c209a70 100644 --- a/book/src/help_vc.md +++ b/book/src/help_vc.md @@ -35,13 +35,12 @@ Options: custom datadirs for different networks. --debug-level Specifies the verbosity level used when emitting logs to the terminal. - [default: info] [possible values: info, debug, trace, warn, error, - crit] + [default: info] [possible values: info, debug, trace, warn, error] --gas-limit 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 @@ -50,7 +49,7 @@ Options: then this value will be ignored. --genesis-state-url-timeout The timeout in seconds for the request to --genesis-state-url. - [default: 180] + [default: 300] --graffiti Specify your custom graffiti to be included in blocks. --graffiti-file @@ -77,15 +76,11 @@ Options: --log-format Specifies the log format used when emitting logs to the terminal. [possible values: JSON] - --logfile - File path where the log file will be stored. Once it grows to the - value specified in `--logfile-max-size` a new log file is generated - where future logs are stored. Once the number of log files exceeds the - value specified in `--logfile-max-number` the oldest log file will be - overwritten. --logfile-debug-level The verbosity level used when emitting logs to the log file. [default: - debug] [possible values: info, debug, trace, warn, error, crit] + debug] [possible values: info, debug, trace, warn, error] + --logfile-dir + Directory path where the log file will be stored --logfile-format Specifies the log format used when emitting logs to the logfile. [possible values: DEFAULT, JSON] @@ -240,8 +235,13 @@ Flags: database will have been initialized when you imported your validator keys. If you misplace your database and then run with this flag you risk being slashed. - --log-color - Force outputting colors when emitting logs to the terminal. + --log-color [] + Enables/Disables colors for logs in terminal. Set it to false to + disable colors. [default: true] [possible values: true, false] + --log-extra-info + If present, show module,file,line in logs + --logfile-color + Enables colors in logfile. --logfile-compress If present, compress old log files. This can help reduce the space needed to store old logs. diff --git a/book/src/help_vm.md b/book/src/help_vm.md index edc0686c45f..85e1a1168fa 100644 --- a/book/src/help_vm.md +++ b/book/src/help_vm.md @@ -39,8 +39,7 @@ Options: custom datadirs for different networks. --debug-level Specifies the verbosity level used when emitting logs to the terminal. - [default: info] [possible values: info, debug, trace, warn, error, - crit] + [default: info] [possible values: info, debug, trace, warn, error] --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 @@ -49,19 +48,15 @@ Options: then this value will be ignored. --genesis-state-url-timeout The timeout in seconds for the request to --genesis-state-url. - [default: 180] + [default: 300] --log-format Specifies the log format used when emitting logs to the terminal. [possible values: JSON] - --logfile - File path where the log file will be stored. Once it grows to the - value specified in `--logfile-max-size` a new log file is generated - where future logs are stored. Once the number of log files exceeds the - value specified in `--logfile-max-number` the oldest log file will be - overwritten. --logfile-debug-level The verbosity level used when emitting logs to the log file. [default: - debug] [possible values: info, debug, trace, warn, error, crit] + debug] [possible values: info, debug, trace, warn, error] + --logfile-dir + Directory path where the log file will be stored --logfile-format Specifies the log format used when emitting logs to the logfile. [possible values: DEFAULT, JSON] @@ -88,8 +83,13 @@ Flags: debugging specific memory allocation issues. -h, --help Prints help information - --log-color - Force outputting colors when emitting logs to the terminal. + --log-color [] + Enables/Disables colors for logs in terminal. Set it to false to + disable colors. [default: true] [possible values: true, false] + --log-extra-info + If present, show module,file,line in logs + --logfile-color + Enables colors in logfile. --logfile-compress If present, compress old log files. This can help reduce the space needed to store old logs. diff --git a/book/src/help_vm_create.md b/book/src/help_vm_create.md index 99de2fc66b8..3b88206397a 100644 --- a/book/src/help_vm_create.md +++ b/book/src/help_vm_create.md @@ -33,8 +33,7 @@ Options: custom datadirs for different networks. --debug-level Specifies the verbosity level used when emitting logs to the terminal. - [default: info] [possible values: info, debug, trace, warn, error, - crit] + [default: info] [possible values: info, debug, trace, warn, error] --deposit-gwei The GWEI value of the deposit amount. Defaults to the minimum amount required for an active validator (MAX_EFFECTIVE_BALANCE) @@ -56,19 +55,15 @@ Options: then this value will be ignored. --genesis-state-url-timeout The timeout in seconds for the request to --genesis-state-url. - [default: 180] + [default: 300] --log-format Specifies the log format used when emitting logs to the terminal. [possible values: JSON] - --logfile - File path where the log file will be stored. Once it grows to the - value specified in `--logfile-max-size` a new log file is generated - where future logs are stored. Once the number of log files exceeds the - value specified in `--logfile-max-number` the oldest log file will be - overwritten. --logfile-debug-level The verbosity level used when emitting logs to the log file. [default: - debug] [possible values: info, debug, trace, warn, error, crit] + debug] [possible values: info, debug, trace, warn, error] + --logfile-dir + Directory path where the log file will be stored --logfile-format Specifies the log format used when emitting logs to the logfile. [possible values: DEFAULT, JSON] @@ -118,8 +113,13 @@ Flags: address. This is not recommended. -h, --help Prints help information - --log-color - Force outputting colors when emitting logs to the terminal. + --log-color [] + Enables/Disables colors for logs in terminal. Set it to false to + disable colors. [default: true] [possible values: true, false] + --log-extra-info + If present, show module,file,line in logs + --logfile-color + Enables colors in logfile. --logfile-compress If present, compress old log files. This can help reduce the space needed to store old logs. diff --git a/book/src/help_vm_import.md b/book/src/help_vm_import.md index 230c38be337..63cca91ee57 100644 --- a/book/src/help_vm_import.md +++ b/book/src/help_vm_import.md @@ -23,8 +23,7 @@ Options: custom datadirs for different networks. --debug-level Specifies the verbosity level used when emitting logs to the terminal. - [default: info] [possible values: info, debug, trace, warn, error, - crit] + [default: info] [possible values: info, debug, trace, warn, error] --gas-limit When provided, the imported validator will use this gas limit. It is recommended to leave this as the default value by not specifying this @@ -37,7 +36,7 @@ Options: then this value will be ignored. --genesis-state-url-timeout The timeout in seconds for the request to --genesis-state-url. - [default: 180] + [default: 300] --keystore-file The path to a keystore JSON file to be imported to the validator client. This file is usually created using staking-deposit-cli or @@ -45,15 +44,11 @@ Options: --log-format Specifies the log format used when emitting logs to the terminal. [possible values: JSON] - --logfile - File path where the log file will be stored. Once it grows to the - value specified in `--logfile-max-size` a new log file is generated - where future logs are stored. Once the number of log files exceeds the - value specified in `--logfile-max-number` the oldest log file will be - overwritten. --logfile-debug-level The verbosity level used when emitting logs to the log file. [default: - debug] [possible values: info, debug, trace, warn, error, crit] + debug] [possible values: info, debug, trace, warn, error] + --logfile-dir + Directory path where the log file will be stored --logfile-format Specifies the log format used when emitting logs to the logfile. [possible values: DEFAULT, JSON] @@ -104,8 +99,13 @@ Flags: directly cause slashable conditions, it might be an indicator that something is amiss. Users should also be careful to avoid submitting duplicate deposits for validators that already exist on the VC. - --log-color - Force outputting colors when emitting logs to the terminal. + --log-color [] + Enables/Disables colors for logs in terminal. Set it to false to + disable colors. [default: true] [possible values: true, false] + --log-extra-info + If present, show module,file,line in logs + --logfile-color + Enables colors in logfile. --logfile-compress If present, compress old log files. This can help reduce the space needed to store old logs. diff --git a/book/src/help_vm_move.md b/book/src/help_vm_move.md index 8a769d5ce54..b7320ca2905 100644 --- a/book/src/help_vm_move.md +++ b/book/src/help_vm_move.md @@ -25,8 +25,7 @@ Options: custom datadirs for different networks. --debug-level Specifies the verbosity level used when emitting logs to the terminal. - [default: info] [possible values: info, debug, trace, warn, error, - crit] + [default: info] [possible values: info, debug, trace, warn, error] --dest-vc-token The file containing a token required by the destination validator client. @@ -45,19 +44,15 @@ Options: then this value will be ignored. --genesis-state-url-timeout The timeout in seconds for the request to --genesis-state-url. - [default: 180] + [default: 300] --log-format Specifies the log format used when emitting logs to the terminal. [possible values: JSON] - --logfile - File path where the log file will be stored. Once it grows to the - value specified in `--logfile-max-size` a new log file is generated - where future logs are stored. Once the number of log files exceeds the - value specified in `--logfile-max-number` the oldest log file will be - overwritten. --logfile-debug-level The verbosity level used when emitting logs to the log file. [default: - debug] [possible values: info, debug, trace, warn, error, crit] + debug] [possible values: info, debug, trace, warn, error] + --logfile-dir + Directory path where the log file will be stored --logfile-format Specifies the log format used when emitting logs to the logfile. [possible values: DEFAULT, JSON] @@ -100,8 +95,13 @@ Flags: debugging specific memory allocation issues. -h, --help Prints help information - --log-color - Force outputting colors when emitting logs to the terminal. + --log-color [] + Enables/Disables colors for logs in terminal. Set it to false to + disable colors. [default: true] [possible values: true, false] + --log-extra-info + If present, show module,file,line in logs + --logfile-color + Enables colors in logfile. --logfile-compress If present, compress old log files. This can help reduce the space needed to store old logs. diff --git a/book/src/imgs/developers_architecture.svg b/book/src/imgs/developers_architecture.svg new file mode 100644 index 00000000000..66c9c0ec892 --- /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/per-epoch.png b/book/src/imgs/per-epoch.png new file mode 100644 index 00000000000..d4ac77ecbbe Binary files /dev/null and b/book/src/imgs/per-epoch.png differ diff --git a/book/src/imgs/per-slot.png b/book/src/imgs/per-slot.png new file mode 100644 index 00000000000..91b9c12e4c6 Binary files /dev/null and b/book/src/imgs/per-slot.png differ diff --git a/book/src/installation.md b/book/src/installation.md index 137a00b918b..95550e0807e 100644 --- a/book/src/installation.md +++ b/book/src/installation.md @@ -4,18 +4,18 @@ Lighthouse runs on Linux, macOS, and Windows. There are three core methods to obtain the Lighthouse application: -- [Pre-built binaries](./installation-binaries.md). -- [Docker images](./docker.md). -- [Building from source](./installation-source.md). +- [Pre-built binaries](./installation_binaries.md). +- [Docker images](./installation_docker.md). +- [Building from source](./installation_source.md). Additionally, there are two extra guides for specific uses: -- [Raspberry Pi 4 guide](./pi.md). (Archived) -- [Cross-compiling guide for developers](./cross-compiling.md). +- [Raspberry Pi 4 guide](./archived_pi.md). (Archived) +- [Cross-compiling guide for developers](./installation_cross_compiling.md). There are also community-maintained installation methods: -- [Homebrew package](./homebrew.md). +- [Homebrew package](./installation_homebrew.md). - Arch Linux AUR packages: [source](https://aur.archlinux.org/packages/lighthouse-ethereum), [binary](https://aur.archlinux.org/packages/lighthouse-ethereum-bin). diff --git a/book/src/installation-binaries.md b/book/src/installation_binaries.md similarity index 100% rename from book/src/installation-binaries.md rename to book/src/installation_binaries.md diff --git a/book/src/cross-compiling.md b/book/src/installation_cross_compiling.md similarity index 83% rename from book/src/cross-compiling.md rename to book/src/installation_cross_compiling.md index c90001d561f..59fa3762c2e 100644 --- a/book/src/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 @@ -34,10 +35,10 @@ in `lighthouse/target/aarch64-unknown-linux-gnu/release`. When using the makefile the set of features used for building can be controlled with the environment variable `CROSS_FEATURES`. See [Feature - Flags](./installation-source.md#feature-flags) for available features. + Flags](./installation_source.md#feature-flags) for available features. ## Compilation Profiles When using the makefile the build profile can be controlled with the environment variable -`CROSS_PROFILE`. See [Compilation Profiles](./installation-source.md#compilation-profiles) for +`CROSS_PROFILE`. See [Compilation Profiles](./installation_source.md#compilation-profiles) for available profiles. diff --git a/book/src/docker.md b/book/src/installation_docker.md similarity index 100% rename from book/src/docker.md rename to book/src/installation_docker.md diff --git a/book/src/homebrew.md b/book/src/installation_homebrew.md similarity index 100% rename from book/src/homebrew.md rename to book/src/installation_homebrew.md diff --git a/book/src/installation-priorities.md b/book/src/installation_priorities.md similarity index 100% rename from book/src/installation-priorities.md rename to book/src/installation_priorities.md diff --git a/book/src/installation-source.md b/book/src/installation_source.md similarity index 95% rename from book/src/installation-source.md rename to book/src/installation_source.md index 19098a5bc8d..0aa8a99a5e5 100644 --- a/book/src/installation-source.md +++ b/book/src/installation_source.md @@ -23,6 +23,8 @@ The rustup installer provides an easy way to update the Rust compiler, and works With Rust installed, follow the instructions below to install dependencies relevant to your operating system. +> Note: For Linux OS, general Linux File Systems such as Ext4 or XFS are fine. We recommend to avoid using Btrfs file system as it has been reported to be slow and the node will suffer from performance degradation as a result. + ### Ubuntu Install the following packages: @@ -216,7 +218,7 @@ Rust Version (MSRV) which is listed under the `rust-version` key in Lighthouse's If compilation fails with `(signal: 9, SIGKILL: kill)`, this could mean your machine ran out of memory during compilation. If you are on a resource-constrained device you can -look into [cross compilation](./cross-compiling.md), or use a [pre-built +look into [cross compilation](./installation_cross_compiling.md), or use a [pre-built binary](https://github.com/sigp/lighthouse/releases). If compilation fails with `error: linking with cc failed: exit code: 1`, try running `cargo clean`. diff --git a/book/src/intro.md b/book/src/intro.md index 9892a8a49db..e5729046854 100644 --- a/book/src/intro.md +++ b/book/src/intro.md @@ -19,9 +19,9 @@ You may read this book from start to finish, or jump to some of these topics: - Follow the [Installation Guide](./installation.md) to install Lighthouse. - Run your very [own beacon node](./run_a_node.md). -- Learn about [becoming a mainnet validator](./mainnet-validator.md). -- Get hacking with the [Development Environment Guide](./setup.md). -- Utilize the whole stack by starting a [local testnet](./setup.md#local-testnets). +- Learn about [becoming a mainnet validator](./mainnet_validator.md). +- Get hacking with the [Development Environment Guide](./contributing_setup.md). +- Utilize the whole stack by starting a [local testnet](./contributing_setup.md#local-testnets). - Query the [RESTful HTTP API](./api.md) using `curl`. Prospective contributors can read the [Contributing](./contributing.md) section diff --git a/book/src/mainnet-validator.md b/book/src/mainnet_validator.md similarity index 92% rename from book/src/mainnet-validator.md rename to book/src/mainnet_validator.md index c53be97ccf9..ba35ba6f122 100644 --- a/book/src/mainnet-validator.md +++ b/book/src/mainnet_validator.md @@ -1,9 +1,9 @@ # Become an Ethereum Consensus Mainnet Validator [launchpad]: https://launchpad.ethereum.org/ -[advanced-datadir]: ./advanced-datadir.md +[advanced-datadir]: ./advanced_datadir.md [license]: https://github.com/sigp/lighthouse/blob/stable/LICENSE -[slashing]: ./slashing-protection.md +[slashing]: ./validator_slashing_protection.md [discord]: https://discord.gg/cyAszAh Becoming an Ethereum consensus validator is rewarding, but it's not for the faint of heart. You'll need to be @@ -33,7 +33,7 @@ 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". @@ -54,7 +54,7 @@ and follow the instructions to generate the keys. When prompted for a network, s Upon completing this step, the files `deposit_data-*.json` and `keystore-m_*.json` will be created. The keys that are generated from staking-deposit-cli can be easily loaded into a Lighthouse validator client (`lighthouse vc`) in [Step 3](#step-3-import-validator-keys-to-lighthouse). In fact, both of these programs are designed to work with each other. -> Lighthouse also supports creating validator keys, see [Key management](./key-management.md) for more info. +> Lighthouse also supports creating validator keys, see [Validator Manager Create](./validator_manager_create.md) for more info. ### Step 2. Start an execution client and Lighthouse beacon node @@ -99,7 +99,7 @@ Enter the keystore password, or press enter to omit it: ``` The user can choose whether or not they'd like to store the validator password -in the [`validator_definitions.yml`](./validator-management.md) file. If the +in the [`validator_definitions.yml`](./validator_management.md) file. If the password is *not* stored here, the validator client (`lighthouse vc`) application will ask for the password each time it starts. This might be nice for some users from a security perspective (i.e., if it is a shared computer), @@ -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 [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 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: @@ -179,7 +179,7 @@ After the validator is running and performing its duties, it is important to kee The next important thing is to stay up to date with updates to Lighthouse and the execution client. Updates are released from time to time, typically once or twice a month. For Lighthouse updates, you can subscribe to notifications on [Github](https://github.com/sigp/lighthouse) by clicking on `Watch`. If you only want to receive notification on new releases, select `Custom`, then `Releases`. You could also join [Lighthouse Discord](https://discord.gg/cyAszAh) where we will make an announcement when there is a new release. -You may also want to try out [Siren](./lighthouse-ui.md), a UI developed by Lighthouse to monitor validator performance. +You may also want to try out [Siren](./ui.md), a UI developed by Lighthouse to monitor validator performance. Once you are familiar with running a validator and server maintenance, you'll find that running Lighthouse is easy. Install it, start it, monitor it and keep it updated. You shouldn't need to interact with it on a day-to-day basis. Happy staking! diff --git a/book/src/run_a_node.md b/book/src/run_a_node.md index 9b9e0cba8e5..15567497e50 100644 --- a/book/src/run_a_node.md +++ b/book/src/run_a_node.md @@ -129,7 +129,7 @@ INFO Downloading historical blocks est_time: 5 hrs 0 mins, speed: 111.96 slots/ Once backfill is complete, a `INFO Historical block download complete` log will be emitted. -Check out the [FAQ](./checkpoint-sync.md#faq) for more information on checkpoint sync. +Check out the [FAQ](./advanced_checkpoint_sync.md#faq) for more information on checkpoint sync. ### Logs - Syncing @@ -146,11 +146,10 @@ Once you see the above message - congratulations! This means that your node is s Several other resources are the next logical step to explore after running your beacon node: -- If you intend to run a validator, proceed to [become a validator](./mainnet-validator.md); -- Explore how to [manage your keys](./key-management.md); -- Research on [validator management](./validator-management.md); +- If you intend to run a validator, proceed to [become a validator](./mainnet_validator.md); +- Explore how to [manage your keys](./archived_key_management.md); +- Research on [validator management](./validator_management.md); - Dig into the [APIs](./api.md) that the beacon node and validator client provide; -- Study even more about [checkpoint sync](./checkpoint-sync.md); or -- Investigate what steps had to be taken in the past to execute a smooth [merge migration](./merge-migration.md). +- Study even more about [checkpoint sync](./advanced_checkpoint_sync.md); or Finally, if you are struggling with anything, join our [Discord](https://discord.gg/cyAszAh). We are happy to help! diff --git a/book/src/ui-installation.md b/book/src/ui-installation.md deleted file mode 100644 index 9cd84e5160b..00000000000 --- a/book/src/ui-installation.md +++ /dev/null @@ -1,73 +0,0 @@ -# 📦 Installation - -Siren supports any operating system that supports containers and/or NodeJS 18, this includes Linux, macOS, and Windows. The recommended way of running Siren is by launching the [docker container](https://hub.docker.com/r/sigp/siren) , but running the application directly is also possible. - -## Version Requirement - -To ensure proper functionality, the Siren app requires Lighthouse v4.3.0 or higher. You can find these versions on the [releases](https://github.com/sigp/lighthouse/releases) page of the Lighthouse repository. - -## Running the Docker container (Recommended) - -The most convenient way to run Siren is to use the Docker images built and published by Sigma Prime. - - They can be found on [Docker hub](https://hub.docker.com/r/sigp/siren/tags), or pulled directly with `docker pull sigp/siren` - -Configuration is done through environment variables, the easiest way to get started is by copying `.env.example` to `.env` and editing the relevant sections (typically, this would at least include adding `BEACON_URL`, `VALIDATOR_URL`, `API_TOKEN` and `SESSION_PASSWORD`) - -Then to run the image: - -`docker compose up` -or -`docker run --rm -ti --name siren -p 4443:443 --env-file $PWD/.env sigp/siren` - -This command will open port 4443, allowing your browser to connect. - -To start Siren, visit `https://localhost:4443` in your web browser. - -Advanced users can mount their own certificates, see the `SSL Certificates` section below - -## Building From Source - -### Docker - -The docker image can be built with the following command: -`docker build -f Dockerfile -t siren .` - -### Building locally - -To build from source, ensure that your system has `Node v18.18` and `yarn` installed. - -#### Build and run the backend - -Navigate to the backend directory `cd backend`. Install all required Node packages by running `yarn`. Once the installation is complete, compile the backend with `yarn build`. Deploy the backend in a production environment, `yarn start:production`. This ensures optimal performance. - -#### Build and run the frontend - -After initializing the backend, return to the root directory. Install all frontend dependencies by executing `yarn`. Build the frontend using `yarn build`. Start the frontend production server with `yarn start`. - -This will allow you to access siren at `http://localhost:3000` by default. - -## Advanced configuration - -### About self-signed SSL certificates - -By default, Siren will generate and use a self-signed certificate on startup. -This will generate a security warning when you try to access the interface. -We recommend to only disable SSL if you would access Siren over a local LAN or otherwise highly trusted or encrypted network (i.e. VPN). - -#### Generating persistent SSL certificates and installing them to your system - -[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) - -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. - -### Configuration through environment variables - -For those who prefer to use environment variables to configure Siren instead of using an `.env` file, this is fully supported. In some cases this may even be preferred. - -#### Docker installed through `snap` - -If you installed Docker through a snap (i.e. on Ubuntu), Docker will have trouble accessing the `.env` file. In this case it is highly recommended to pass the config to the container with environment variables. -Note that the defaults in `.env.example` will be used as fallback, if no other value is provided. diff --git a/book/src/lighthouse-ui.md b/book/src/ui.md similarity index 79% rename from book/src/lighthouse-ui.md rename to book/src/ui.md index f2662f4a69a..e980e902687 100644 --- a/book/src/lighthouse-ui.md +++ b/book/src/ui.md @@ -21,11 +21,11 @@ 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 +- [Configuration Guide](./ui_configuration.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. -- [FAQs](./ui-faqs.md) - Frequently Asked Questions. +- [Authentication Guide](./ui_authentication.md) - Explanation of how Siren authentication works and protects validator actions. +- [Usage](./ui_usage.md) - Details various Siren components. +- [FAQs](./ui_faqs.md) - Frequently Asked Questions. ## Contributing diff --git a/book/src/ui-authentication.md b/book/src/ui_authentication.md similarity index 87% rename from book/src/ui-authentication.md rename to book/src/ui_authentication.md index 81b867bae26..36e3835e3bf 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 [configuration](./ui_configuration.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 [configuration process](./ui_configuration.md). ![exit](imgs/ui-auth.png) diff --git a/book/src/ui-configuration.md b/book/src/ui_configuration.md similarity index 99% rename from book/src/ui-configuration.md rename to book/src/ui_configuration.md index 34cc9fe7ca6..64b293372bb 100644 --- a/book/src/ui-configuration.md +++ b/book/src/ui_configuration.md @@ -29,7 +29,7 @@ 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). diff --git a/book/src/ui-faqs.md b/book/src/ui_faqs.md similarity index 92% rename from book/src/ui-faqs.md rename to book/src/ui_faqs.md index 29de889e5fc..db365e2fa0e 100644 --- a/book/src/ui-faqs.md +++ b/book/src/ui_faqs.md @@ -6,11 +6,11 @@ Yes, the most current Siren version requires Lighthouse v4.3.0 or higher to func ## 2. Where can I find my API token? -The required API token may be found in the default data directory of the validator client. For more information please refer to the lighthouse ui configuration [`api token section`](./api-vc-auth-header.md). +The required API token may be found in the default data directory of the validator client. For more information please refer to the lighthouse ui configuration [`api token section`](./api_vc_auth_header.md). ## 3. How do I fix the Node Network Errors? -If you 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 [`configuration`](./ui_configuration.md#configuration). ## 4. How do I connect Siren to Lighthouse from a different computer on the same network? @@ -19,7 +19,7 @@ That being said, it is entirely possible to have it published over the internet, ## 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 [`configuration`](./ui_configuration.md#configuration). ## 6. Does Siren support reverse proxy or DNS named addresses? diff --git a/book/src/ui-usage.md b/book/src/ui_usage.md similarity index 100% rename from book/src/ui-usage.md rename to book/src/ui_usage.md diff --git a/book/src/validator_consolidation.md b/book/src/validator_consolidation.md new file mode 100644 index 00000000000..3c9860a5149 --- /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-doppelganger.md b/book/src/validator_doppelganger.md similarity index 98% rename from book/src/validator-doppelganger.md rename to book/src/validator_doppelganger.md index a3d60d31b3c..006df50bd90 100644 --- a/book/src/validator-doppelganger.md +++ b/book/src/validator_doppelganger.md @@ -1,8 +1,8 @@ # Doppelganger Protection [doppelgänger]: https://en.wikipedia.org/wiki/Doppelg%C3%A4nger -[Slashing Protection]: ./slashing-protection.md -[VC HTTP API]: ./api-vc.md +[Slashing Protection]: ./validator_slashing_protection.md +[VC HTTP API]: ./api_vc.md From Lighthouse `v1.5.0`, the *Doppelganger Protection* feature is available for the Validator Client. Taken from the German *[doppelgänger]*, which translates literally to "double-walker", a diff --git a/book/src/suggested-fee-recipient.md b/book/src/validator_fee_recipient.md similarity index 96% rename from book/src/suggested-fee-recipient.md rename to book/src/validator_fee_recipient.md index 4a9be7b963a..2b125f5033d 100644 --- a/book/src/suggested-fee-recipient.md +++ b/book/src/validator_fee_recipient.md @@ -82,7 +82,7 @@ validator client in order for the execution node to be given adequate notice of ## Setting the fee recipient dynamically using the keymanager API -When the [validator client API](api-vc.md) is enabled, the +When the [validator client API](api_vc.md) is enabled, the [standard keymanager API](https://ethereum.github.io/keymanager-APIs/) includes an endpoint for setting the fee recipient dynamically for a given public key. When used, the fee recipient will be saved in `validator_definitions.yml` so that it persists across restarts of the validator @@ -92,7 +92,7 @@ client. |-------------------|--------------------------------------------| | Path | `/eth/v1/validator/{pubkey}/feerecipient` | | Method | POST | -| Required Headers | [`Authorization`](./api-vc-auth-header.md) | +| Required Headers | [`Authorization`](./api_vc_auth_header.md) | | Typical Responses | 202, 404 | ### Example Request Body @@ -117,7 +117,7 @@ curl -X POST \ http://localhost:5062/eth/v1/validator/${PUBKEY}/feerecipient | jq ``` -Note that an authorization header is required to interact with the API. This is specified with the header `-H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)"` which read the API token to supply the authentication. Refer to [Authorization Header](./api-vc-auth-header.md) for more information. If you are having permission issue with accessing the API token file, you can modify the header to become `-H "Authorization: Bearer $(sudo cat ${DATADIR}/validators/api-token.txt)"`. +Note that an authorization header is required to interact with the API. This is specified with the header `-H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)"` which read the API token to supply the authentication. Refer to [Authorization Header](./api_vc_auth_header.md) for more information. If you are having permission issue with accessing the API token file, you can modify the header to become `-H "Authorization: Bearer $(sudo cat ${DATADIR}/validators/api-token.txt)"`. #### Successful Response (202) @@ -135,7 +135,7 @@ The same path with a `GET` request can be used to query the fee recipient for a |-------------------|--------------------------------------------| | Path | `/eth/v1/validator/{pubkey}/feerecipient` | | Method | GET | -| Required Headers | [`Authorization`](./api-vc-auth-header.md) | +| Required Headers | [`Authorization`](./api_vc_auth_header.md) | | Typical Responses | 200, 404 | Command: @@ -170,7 +170,7 @@ This is useful if you want the fee recipient to fall back to the validator clien |-------------------|--------------------------------------------| | Path | `/eth/v1/validator/{pubkey}/feerecipient` | | Method | DELETE | -| Required Headers | [`Authorization`](./api-vc-auth-header.md) | +| Required Headers | [`Authorization`](./api_vc_auth_header.md) | | Typical Responses | 204, 404 | Command: diff --git a/book/src/graffiti.md b/book/src/validator_graffiti.md similarity index 95% rename from book/src/graffiti.md rename to book/src/validator_graffiti.md index 7b402ea866f..9908d056da3 100644 --- a/book/src/graffiti.md +++ b/book/src/validator_graffiti.md @@ -32,7 +32,7 @@ Lighthouse will first search for the graffiti corresponding to the public key of Users can set validator specific graffitis in `validator_definitions.yml` with the `graffiti` key. This option is recommended for static setups where the graffitis won't change on every new block proposal. -You can also update the graffitis in the `validator_definitions.yml` file using the [Lighthouse API](api-vc-endpoints.html#patch-lighthousevalidatorsvoting_pubkey). See example in [Set Graffiti via HTTP](#set-graffiti-via-http). +You can also update the graffitis in the `validator_definitions.yml` file using the [Lighthouse API](api_vc_endpoints.html#patch-lighthousevalidatorsvoting_pubkey). See example in [Set Graffiti via HTTP](#set-graffiti-via-http). Below is an example of the validator_definitions.yml with validator specific graffitis: @@ -74,11 +74,11 @@ Usage: `lighthouse bn --graffiti fortytwo` ## Set Graffiti via HTTP -Use the [Lighthouse API](api-vc-endpoints.md) to set graffiti on a per-validator basis. This method updates the graffiti +Use the [Lighthouse API](api_vc_endpoints.md) to set graffiti on a per-validator basis. This method updates the graffiti both in memory and in the `validator_definitions.yml` file. The new graffiti will be used in the next block proposal without requiring a validator client restart. -Refer to [Lighthouse API](api-vc-endpoints.html#patch-lighthousevalidatorsvoting_pubkey) for API specification. +Refer to [Lighthouse API](api_vc_endpoints.html#patch-lighthousevalidatorsvoting_pubkey) for API specification. ### Example Command diff --git a/book/src/validator-management.md b/book/src/validator_management.md similarity index 97% rename from book/src/validator-management.md rename to book/src/validator_management.md index b9610b69675..3bfac37ac6b 100644 --- a/book/src/validator-management.md +++ b/book/src/validator_management.md @@ -13,7 +13,7 @@ standard directories and do not start their `lighthouse vc` with the this document. However, users with more complex needs may find this document useful. -The [lighthouse validator-manager](./validator-manager.md) command can be used +The [lighthouse validator-manager](./validator_manager.md) command can be used to create and import validators to a Lighthouse VC. It can also be used to move validators between two Lighthouse VCs. @@ -54,7 +54,7 @@ Each permitted field of the file is listed below for reference: - `enabled`: A `true`/`false` indicating if the validator client should consider this validator "enabled". - `voting_public_key`: A validator public key. -- `type`: How the validator signs messages (this can be `local_keystore` or `web3signer` (see [Web3Signer](./validator-web3signer.md))). +- `type`: How the validator signs messages (this can be `local_keystore` or `web3signer` (see [Web3Signer](./advanced_web3signer.md))). - `voting_keystore_path`: The path to a EIP-2335 keystore. - `voting_keystore_password_path`: The path to the password for the EIP-2335 keystore. - `voting_keystore_password`: The password to the EIP-2335 keystore. @@ -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-manager.md b/book/src/validator_manager.md similarity index 93% rename from book/src/validator-manager.md rename to book/src/validator_manager.md index 11df2af0378..c610340b390 100644 --- a/book/src/validator-manager.md +++ b/book/src/validator_manager.md @@ -30,6 +30,6 @@ The `validator-manager` boasts the following features: ## Guides -- [Creating and importing validators using the `create` and `import` commands.](./validator-manager-create.md) -- [Moving validators between two VCs using the `move` command.](./validator-manager-move.md) -- [Managing validators such as delete, import and list validators.](./validator-manager-api.md) +- [Creating and importing validators using the `create` and `import` commands.](./validator_manager_create.md) +- [Moving validators between two VCs using the `move` command.](./validator_manager_move.md) +- [Managing validators such as delete, import and list validators.](./validator_manager_api.md) diff --git a/book/src/validator-manager-api.md b/book/src/validator_manager_api.md similarity index 100% rename from book/src/validator-manager-api.md rename to book/src/validator_manager_api.md diff --git a/book/src/validator-manager-create.md b/book/src/validator_manager_create.md similarity index 98% rename from book/src/validator-manager-create.md rename to book/src/validator_manager_create.md index b4c86dc6da8..458907bc65b 100644 --- a/book/src/validator-manager-create.md +++ b/book/src/validator_manager_create.md @@ -69,7 +69,7 @@ lighthouse \ > Be sure to remove `./validators.json` after the import is successful since it > contains unencrypted validator keystores. -> Note: To import validators with validator-manager using keystore files created using the staking deposit CLI, refer to [Managing Validators](./validator-manager-api.md#import). +> Note: To import validators with validator-manager using keystore files created using the staking deposit CLI, refer to [Managing Validators](./validator_manager_api.md#import). ## Detailed Guide @@ -179,7 +179,7 @@ INFO Modified key_cache saved successfully The WARN message means that the `validators.json` file does not contain the slashing protection data. This is normal if you are starting a new validator. The flag `--enable-doppelganger-protection` will also protect users from potential slashing risk. The validators will now go through 2-3 epochs of [doppelganger -protection](./validator-doppelganger.md) and will automatically start performing +protection](./validator_doppelganger.md) and will automatically start performing their duties when they are deposited and activated. If the host VC contains the same public key as the `validators.json` file, an error will be shown and the `import` process will stop: diff --git a/book/src/validator-manager-move.md b/book/src/validator_manager_move.md similarity index 100% rename from book/src/validator-manager-move.md rename to book/src/validator_manager_move.md diff --git a/book/src/validator-monitoring.md b/book/src/validator_monitoring.md similarity index 98% rename from book/src/validator-monitoring.md rename to book/src/validator_monitoring.md index bbc95460ec9..d7f00521c41 100644 --- a/book/src/validator-monitoring.md +++ b/book/src/validator_monitoring.md @@ -5,7 +5,7 @@ Generally users will want to use this function to track their own validators, ho used for any validator, regardless of who controls it. _Note: If you are looking for remote metric monitoring, please see the docs on -[Prometheus Metrics](./advanced_metrics.md)_. +[Prometheus Metrics](./api_metrics.md)_. ## Monitoring is in the Beacon Node @@ -64,7 +64,7 @@ lighthouse bn --validator-monitor-pubkeys 0x933ad9491b62059dd065b560d256d8957a8c Enrolling a validator for additional monitoring results in: - Additional logs to be printed during BN operation. -- Additional [Prometheus metrics](./advanced_metrics.md) from the BN. +- Additional [Prometheus metrics](./api_metrics.md) from the BN. ### Logging diff --git a/book/src/slashing-protection.md b/book/src/validator_slashing_protection.md similarity index 97% rename from book/src/slashing-protection.md rename to book/src/validator_slashing_protection.md index 2d580f1c312..3e0fe184e5b 100644 --- a/book/src/slashing-protection.md +++ b/book/src/validator_slashing_protection.md @@ -22,9 +22,9 @@ and carefully to keep your validators safe. See the [Troubleshooting](#troublesh The database will be automatically created, and your validators registered with it when: * Importing keys from another source (e.g. [staking-deposit-cli](https://github.com/ethereum/staking-deposit-cli/releases), Lodestar, Nimbus, Prysm, Teku, [ethdo](https://github.com/wealdtech/ethdo)). - See [import validator keys](./mainnet-validator.md#step-3-import-validator-keys-to-lighthouse). + See [import validator keys](./mainnet_validator.md#step-3-import-validator-keys-to-lighthouse). * Creating keys using Lighthouse itself (`lighthouse account validator create`) -* Creating keys via the [validator client API](./api-vc.md). +* Creating keys via the [validator client API](./api_vc.md). ## Avoiding Slashing @@ -79,7 +79,7 @@ lighthouse account validator slashing-protection import filename.json ``` When importing an interchange file, you still need to import the validator keystores themselves -separately, using the instructions for [import validator keys](./mainnet-validator.md#step-3-import-validator-keys-to-lighthouse). +separately, using the instructions for [import validator keys](./mainnet_validator.md#step-3-import-validator-keys-to-lighthouse). --- diff --git a/book/src/partial-withdrawal.md b/book/src/validator_sweep.md similarity index 58% rename from book/src/partial-withdrawal.md rename to book/src/validator_sweep.md index 26003e1f2fe..0755c06d51c 100644 --- a/book/src/partial-withdrawal.md +++ b/book/src/validator_sweep.md @@ -1,15 +1,19 @@ -# Partial Withdrawals +# Validator "Sweeping" (Automatic Partial Withdrawals) After the [Capella](https://ethereum.org/en/history/#capella) upgrade on 12th April 2023: - 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. At the time of writing, with 560,000+ validators on the Ethereum mainnet, you shall expect to receive the rewards approximately every 5 days. +- 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`? - Refer [here](./voluntary-exit.md#1-how-to-know-if-i-have-the-withdrawal-credentials-type-0x01). + Refer [here](./validator_voluntary_exit.md#1-how-to-know-if-i-have-the-withdrawal-credentials-type-0x01). 2. My validator has withdrawal credentials type `0x00`, is there a deadline to update my withdrawal credentials? @@ -17,7 +21,7 @@ After the [Capella](https://ethereum.org/en/history/#capella) upgrade on 12 3. Do I have to do anything to get my rewards after I update the withdrawal credentials to type `0x01`? - No. The "validator sweep" occurs automatically and you can expect to receive the rewards every *n* days, [more information here](./voluntary-exit.md#4-when-will-i-get-my-staked-fund-after-voluntary-exit-if-my-validator-is-of-type-0x01). + No. The "validator sweep" occurs automatically and you can expect to receive the rewards every *n* days, [more information here](./validator_voluntary_exit.md#4-when-will-i-get-my-staked-fund-after-voluntary-exit-if-my-validator-is-of-type-0x01). Figure below summarizes partial withdrawals. diff --git a/book/src/voluntary-exit.md b/book/src/validator_voluntary_exit.md similarity index 93% rename from book/src/voluntary-exit.md rename to book/src/validator_voluntary_exit.md index 6261f2e2675..c17c0f4fc44 100644 --- a/book/src/voluntary-exit.md +++ b/book/src/validator_voluntary_exit.md @@ -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,10 @@ Please keep your validator running till exit epoch Exit epoch in approximately 1920 secs ``` +## 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. diff --git a/boot_node/Cargo.toml b/boot_node/Cargo.toml index 4f5c1e010a5..d1b059f3b2a 100644 --- a/boot_node/Cargo.toml +++ b/boot_node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "boot_node" -version = "7.0.0" +version = "7.1.0-beta.0" authors = ["Sigma Prime "] edition = { workspace = true } @@ -16,9 +16,7 @@ lighthouse_network = { workspace = true } log = { workspace = true } logging = { workspace = true } serde = { workspace = true } -slog = { workspace = true } -slog-async = { workspace = true } -slog-scope = "4.3.0" -slog-term = { workspace = true } tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } types = { workspace = true } diff --git a/boot_node/src/config.rs b/boot_node/src/config.rs index bb7678631fd..c43a8b397b1 100644 --- a/boot_node/src/config.rs +++ b/boot_node/src/config.rs @@ -53,9 +53,7 @@ impl BootNodeConfig { let mut network_config = NetworkConfig::default(); - let logger = slog_scope::logger(); - - set_network_config(&mut network_config, matches, &data_dir, &logger)?; + set_network_config(&mut network_config, matches, &data_dir)?; // Set the Enr Discovery ports to the listening ports if not present. if let Some(listening_addr_v4) = network_config.listen_addrs().v4() { @@ -85,7 +83,7 @@ impl BootNodeConfig { network_config.discv5_config.enr_update = false; } - let private_key = load_private_key(&network_config, &logger); + let private_key = load_private_key(&network_config); let local_key = CombinedKey::from_libp2p(private_key)?; let local_enr = if let Some(dir) = matches.get_one::("network-dir") { @@ -104,7 +102,7 @@ impl BootNodeConfig { if eth2_network_config.genesis_state_is_known() { let mut genesis_state = eth2_network_config - .genesis_state::(genesis_state_url.as_deref(), genesis_state_url_timeout, &logger).await? + .genesis_state::(genesis_state_url.as_deref(), genesis_state_url_timeout).await? .ok_or_else(|| { "The genesis state for this network is not known, this is an unsupported mode" .to_string() @@ -113,7 +111,7 @@ impl BootNodeConfig { let genesis_state_root = genesis_state .canonical_root() .map_err(|e| format!("Error hashing genesis state: {e:?}"))?; - slog::info!(logger, "Genesis state found"; "root" => ?genesis_state_root); + tracing::info!(root = ?genesis_state_root, "Genesis state found"); let enr_fork = spec.enr_fork_id::( types::Slot::from(0u64), genesis_state.genesis_validators_root(), @@ -121,10 +119,7 @@ impl BootNodeConfig { Some(enr_fork.as_ssz_bytes()) } else { - slog::warn!( - logger, - "No genesis state provided. No Eth2 field added to the ENR" - ); + tracing::warn!("No genesis state provided. No Eth2 field added to the ENR"); None } }; @@ -160,7 +155,7 @@ impl BootNodeConfig { .map_err(|e| format!("Failed to build ENR: {:?}", e))? }; - use_or_load_enr(&local_key, &mut local_enr, &network_config, &logger)?; + use_or_load_enr(&local_key, &mut local_enr, &network_config)?; local_enr }; diff --git a/boot_node/src/lib.rs b/boot_node/src/lib.rs index 669b126bd37..70a45b2f922 100644 --- a/boot_node/src/lib.rs +++ b/boot_node/src/lib.rs @@ -1,6 +1,5 @@ //! Creates a simple DISCV5 server which can be used to bootstrap an Eth2 network. use clap::ArgMatches; -use slog::{o, Drain, Level, Logger}; use eth2_network_config::Eth2NetworkConfig; mod cli; @@ -8,10 +7,9 @@ pub mod config; mod server; pub use cli::cli_app; use config::BootNodeConfig; +use tracing_subscriber::EnvFilter; use types::{EthSpec, EthSpecId}; -const LOG_CHANNEL_SIZE: usize = 2048; - /// Run the bootnode given the CLI configuration. pub fn run( lh_matches: &ArgMatches, @@ -20,49 +18,27 @@ pub fn run( eth2_network_config: &Eth2NetworkConfig, debug_level: String, ) { - let debug_level = match debug_level.as_str() { - "trace" => log::Level::Trace, - "debug" => log::Level::Debug, - "info" => log::Level::Info, - "warn" => log::Level::Warn, - "error" => log::Level::Error, - "crit" => log::Level::Error, - _ => unreachable!(), - }; - - // Setting up the initial logger format and building it. - let drain = { - let decorator = slog_term::TermDecorator::new().build(); - let decorator = logging::AlignedTermDecorator::new(decorator, logging::MAX_MESSAGE_WIDTH); - let drain = slog_term::FullFormat::new(decorator).build().fuse(); - slog_async::Async::new(drain) - .chan_size(LOG_CHANNEL_SIZE) - .build() - }; - - let drain = match debug_level { - log::Level::Info => drain.filter_level(Level::Info), - log::Level::Debug => drain.filter_level(Level::Debug), - log::Level::Trace => drain.filter_level(Level::Trace), - log::Level::Warn => drain.filter_level(Level::Warning), - log::Level::Error => drain.filter_level(Level::Error), - }; + let filter_layer = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new(debug_level.to_string().to_lowercase())) + .unwrap(); - let log = Logger::root(drain.fuse(), o!()); + tracing_subscriber::fmt() + .with_env_filter(filter_layer) + .init(); // Run the main function emitting any errors if let Err(e) = match eth_spec_id { EthSpecId::Minimal => { - main::(lh_matches, bn_matches, eth2_network_config, log) + main::(lh_matches, bn_matches, eth2_network_config) } EthSpecId::Mainnet => { - main::(lh_matches, bn_matches, eth2_network_config, log) + main::(lh_matches, bn_matches, eth2_network_config) } EthSpecId::Gnosis => { - main::(lh_matches, bn_matches, eth2_network_config, log) + main::(lh_matches, bn_matches, eth2_network_config) } } { - slog::crit!(slog_scope::logger(), "{}", e); + logging::crit!(?e); } } @@ -70,7 +46,6 @@ fn main( lh_matches: &ArgMatches, bn_matches: &ArgMatches, eth2_network_config: &Eth2NetworkConfig, - log: slog::Logger, ) -> Result<(), String> { // Builds a custom executor for the bootnode let runtime = tokio::runtime::Builder::new_multi_thread() @@ -83,7 +58,6 @@ fn main( lh_matches, bn_matches, eth2_network_config, - log, ))?; Ok(()) diff --git a/boot_node/src/server.rs b/boot_node/src/server.rs index 96032dddcc9..d96ac0c726f 100644 --- a/boot_node/src/server.rs +++ b/boot_node/src/server.rs @@ -8,14 +8,13 @@ use lighthouse_network::{ discv5::{self, enr::NodeId, Discv5}, EnrExt, Eth2Enr, }; -use slog::info; +use tracing::{info, warn}; use types::EthSpec; pub async fn run( lh_matches: &ArgMatches, bn_matches: &ArgMatches, eth2_network_config: &Eth2NetworkConfig, - log: slog::Logger, ) -> Result<(), String> { // parse the CLI args into a useable config let config: BootNodeConfig = BootNodeConfig::new(bn_matches, eth2_network_config).await?; @@ -52,19 +51,19 @@ pub async fn run( let pretty_v4_socket = enr_v4_socket.as_ref().map(|addr| addr.to_string()); let pretty_v6_socket = enr_v6_socket.as_ref().map(|addr| addr.to_string()); info!( - log, "Configuration parameters"; - "listening_address" => ?discv5_config.listen_config, - "advertised_v4_address" => ?pretty_v4_socket, - "advertised_v6_address" => ?pretty_v6_socket, - "eth2" => eth2_field + listening_address = ?discv5_config.listen_config, + advertised_v4_address = ?pretty_v4_socket, + advertised_v6_address = ?pretty_v6_socket, + eth2 = eth2_field, + "Configuration parameters" ); - info!(log, "Identity established"; "peer_id" => %local_enr.peer_id(), "node_id" => %local_enr.node_id()); + info!(peer_id = %local_enr.peer_id(), node_id = %local_enr.node_id(), "Identity established"); // build the contactable multiaddr list, adding the p2p protocol - info!(log, "Contact information"; "enr" => local_enr.to_base64()); - info!(log, "Enr details"; "enr" => ?local_enr); - info!(log, "Contact information"; "multiaddrs" => ?local_enr.multiaddr_p2p()); + info!(enr = local_enr.to_base64(), "Contact information"); + info!(enr = ?local_enr, "Enr details"); + info!(multiaddrs = ?local_enr.multiaddr_p2p(), "Contact information"); // construct the discv5 server let mut discv5: Discv5 = Discv5::new(local_enr.clone(), local_key, discv5_config).unwrap(); @@ -72,16 +71,15 @@ pub async fn run( // If there are any bootnodes add them to the routing table for enr in boot_nodes { info!( - log, - "Adding bootnode"; - "ipv4_address" => ?enr.udp4_socket(), - "ipv6_address" => ?enr.udp6_socket(), - "peer_id" => ?enr.peer_id(), - "node_id" => ?enr.node_id() + ipv4_address = ?enr.udp4_socket(), + ipv6_address = ?enr.udp6_socket(), + peer_id = ?enr.peer_id(), + node_id = ?enr.node_id(), + "Adding bootnode" ); if enr != local_enr { if let Err(e) = discv5.add_enr(enr) { - slog::warn!(log, "Failed adding ENR"; "error" => ?e); + warn!(error = ?e, "Failed adding ENR"); } } } @@ -93,7 +91,7 @@ pub async fn run( // if there are peers in the local routing table, establish a session by running a query if !discv5.table_entries_id().is_empty() { - info!(log, "Executing bootstrap query..."); + info!("Executing bootstrap query..."); let _ = discv5.find_node(NodeId::random()).await; } @@ -131,14 +129,14 @@ pub async fn run( // display server metrics let metrics = discv5.metrics(); info!( - log, "Server metrics"; - "connected_peers" => discv5.connected_peers(), - "active_sessions" => metrics.active_sessions, - "requests/s" => format_args!("{:.2}", metrics.unsolicited_requests_per_second), - "ipv4_nodes" => ipv4_only_reachable, - "ipv6_only_nodes" => ipv6_only_reachable, - "dual_stack_nodes" => ipv4_ipv6_reachable, - "unreachable_nodes" => unreachable_nodes, + connected_peers = discv5.connected_peers(), + active_sessions = metrics.active_sessions, + "requests/s" = format_args!("{:.2}", metrics.unsolicited_requests_per_second), + ipv4_nodes = ipv4_only_reachable, + ipv6_only_nodes = ipv6_only_reachable, + dual_stack_nodes = ipv4_ipv6_reachable, + unreachable_nodes, + "Server metrics", ); } @@ -149,7 +147,7 @@ pub async fn run( // Ignore these events here } discv5::Event::SocketUpdated(socket_addr) => { - info!(log, "Advertised socket address updated"; "socket_addr" => %socket_addr); + info!(%socket_addr, "Advertised socket address updated"); } _ => {} // Ignore } diff --git a/common/account_utils/Cargo.toml b/common/account_utils/Cargo.toml index 3ab60346886..00c74a13038 100644 --- a/common/account_utils/Cargo.toml +++ b/common/account_utils/Cargo.toml @@ -14,7 +14,7 @@ regex = { workspace = true } rpassword = "5.0.0" serde = { workspace = true } serde_yaml = { workspace = true } -slog = { workspace = true } +tracing = { workspace = true } types = { workspace = true } validator_dir = { workspace = true } zeroize = { workspace = true } diff --git a/common/account_utils/src/validator_definitions.rs b/common/account_utils/src/validator_definitions.rs index 25cf368c900..4c253283fe4 100644 --- a/common/account_utils/src/validator_definitions.rs +++ b/common/account_utils/src/validator_definitions.rs @@ -7,11 +7,11 @@ use crate::{default_keystore_password_path, read_password_string, write_file_via use eth2_keystore::Keystore; use regex::Regex; use serde::{Deserialize, Serialize}; -use slog::{error, Logger}; use std::collections::HashSet; use std::fs::{self, create_dir_all, File}; use std::io; use std::path::{Path, PathBuf}; +use tracing::error; use types::{graffiti::GraffitiString, Address, PublicKey}; use validator_dir::VOTING_KEYSTORE_FILE; use zeroize::Zeroizing; @@ -266,7 +266,6 @@ impl ValidatorDefinitions { &mut self, validators_dir: P, secrets_dir: P, - log: &Logger, ) -> Result { let mut keystore_paths = vec![]; recursively_find_voting_keystores(validators_dir, &mut keystore_paths) @@ -311,10 +310,9 @@ impl ValidatorDefinitions { Ok(keystore) => keystore, Err(e) => { error!( - log, - "Unable to read validator keystore"; - "error" => e, - "keystore" => format!("{:?}", voting_keystore_path) + error = ?e, + keystore = ?voting_keystore_path, + "Unable to read validator keystore" ); return None; } @@ -336,9 +334,8 @@ impl ValidatorDefinitions { } None => { error!( - log, - "Invalid keystore public key"; - "keystore" => format!("{:?}", voting_keystore_path) + keystore = ?voting_keystore_path, + "Invalid keystore public key" ); return None; } diff --git a/common/eth2/Cargo.toml b/common/eth2/Cargo.toml index a39a58ac140..5d0ad1f45e0 100644 --- a/common/eth2/Cargo.toml +++ b/common/eth2/Cargo.toml @@ -3,19 +3,20 @@ name = "eth2" version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] derivative = { workspace = true } either = { workspace = true } +enr = { version = "0.13.0", features = ["ed25519"] } eth2_keystore = { workspace = true } ethereum_serde_utils = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } futures = { workspace = true } futures-util = "0.3.8" -lighthouse_network = { workspace = true } +libp2p-identity = { version = "0.2", features = ["peerid"] } mediatype = "0.19.13" +multiaddr = "0.18.2" pretty_reqwest_error = { workspace = true } proto_array = { workspace = true } rand = { workspace = true } @@ -26,7 +27,6 @@ serde = { workspace = true } serde_json = { workspace = true } slashing_protection = { workspace = true } ssz_types = { workspace = true } -store = { 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 28ca21e16ed..fc12a4c5f35 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -16,12 +16,12 @@ pub mod types; use self::mixin::{RequestAccept, ResponseOptional}; use self::types::{Error as ResponseError, *}; +use ::types::fork_versioned_response::ExecutionOptimisticFinalizedForkVersionedResponse; use derivative::Derivative; use either::Either; use futures::Stream; use futures_util::StreamExt; -use lighthouse::StandardBlockReward; -use lighthouse_network::PeerId; +use libp2p_identity::PeerId; use pretty_reqwest_error::PrettyReqwestError; pub use reqwest; use reqwest::{ @@ -37,7 +37,6 @@ use std::fmt; use std::future::Future; use std::path::PathBuf; use std::time::Duration; -use store::fork_versioned_response::ExecutionOptimisticFinalizedForkVersionedResponse; pub const V1: EndpointVersion = EndpointVersion(1); pub const V2: EndpointVersion = EndpointVersion(2); @@ -145,6 +144,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 { @@ -162,6 +162,7 @@ impl Timeouts { get_debug_beacon_states: timeout, get_deposit_snapshot: timeout, get_validator_block: timeout, + default: timeout, } } } @@ -236,7 +237,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 } @@ -330,7 +333,6 @@ impl BeaconNodeHttpClient { } /// Perform a HTTP POST request, returning a JSON response. - #[cfg(feature = "lighthouse")] async fn post_with_response( &self, url: U, @@ -400,11 +402,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 } @@ -417,10 +418,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) @@ -435,7 +436,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( @@ -454,10 +455,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, @@ -1662,19 +1663,19 @@ impl BeaconNodeHttpClient { /// `POST beacon/rewards/sync_committee` pub async fn post_beacon_rewards_sync_committee( &self, - rewards: &[Option>], - ) -> Result<(), Error> { + block_id: BlockId, + validators: &[ValidatorId], + ) -> Result>, Error> { let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") .push("rewards") - .push("sync_committee"); - - self.post(path, &rewards).await?; + .push("sync_committee") + .push(&block_id.to_string()); - Ok(()) + self.post_with_response(path, &validators).await } /// `GET beacon/rewards/blocks` @@ -1697,19 +1698,19 @@ impl BeaconNodeHttpClient { /// `POST beacon/rewards/attestations` pub async fn post_beacon_rewards_attestations( &self, - attestations: &[ValidatorId], - ) -> Result<(), Error> { + epoch: Epoch, + validators: &[ValidatorId], + ) -> Result { let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") .push("rewards") - .push("attestations"); - - self.post(path, &attestations).await?; + .push("attestations") + .push(&epoch.to_string()); - Ok(()) + self.post_with_response(path, &validators).await } // GET builder/states/{state_id}/expected_withdrawals @@ -1870,7 +1871,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 { diff --git a/common/eth2/src/lighthouse.rs b/common/eth2/src/lighthouse.rs index 2f61c524760..9a5d9100cf5 100644 --- a/common/eth2/src/lighthouse.rs +++ b/common/eth2/src/lighthouse.rs @@ -1,15 +1,14 @@ //! This module contains endpoints that are non-standard and only available on Lighthouse servers. mod attestation_performance; -pub mod attestation_rewards; mod block_packing_efficiency; mod block_rewards; -mod standard_block_rewards; -mod sync_committee_rewards; +pub mod sync_state; use crate::{ + lighthouse::sync_state::SyncState, types::{ - AdminPeer, DepositTreeSnapshot, Epoch, EthSpec, FinalizedExecutionBlock, GenericResponse, + AdminPeer, DepositTreeSnapshot, Epoch, FinalizedExecutionBlock, GenericResponse, ValidatorId, }, BeaconNodeHttpClient, DepositData, Error, Eth1Data, Hash256, Slot, @@ -18,36 +17,20 @@ use proto_array::core::ProtoArray; use serde::{Deserialize, Serialize}; use ssz::four_byte_option_impl; use ssz_derive::{Decode, Encode}; -use store::{AnchorInfo, BlobInfo, Split, StoreConfig}; pub use attestation_performance::{ AttestationPerformance, AttestationPerformanceQuery, AttestationPerformanceStatistics, }; -pub use attestation_rewards::StandardAttestationRewards; pub use block_packing_efficiency::{ BlockPackingEfficiency, BlockPackingEfficiencyQuery, ProposerInfo, UniqueAttestation, }; pub use block_rewards::{AttestationRewards, BlockReward, BlockRewardMeta, BlockRewardsQuery}; -pub use lighthouse_network::{types::SyncState, PeerInfo}; -pub use standard_block_rewards::StandardBlockReward; -pub use sync_committee_rewards::SyncCommitteeReward; // Define "legacy" implementations of `Option` which use four bytes for encoding the union // selector. four_byte_option_impl!(four_byte_option_u64, u64); four_byte_option_impl!(four_byte_option_hash256, Hash256); -/// Information returned by `peers` and `connected_peers`. -// TODO: this should be deserializable.. -#[derive(Debug, Clone, Serialize)] -#[serde(bound = "E: EthSpec")] -pub struct Peer { - /// The Peer's ID - pub peer_id: String, - /// The PeerInfo associated with the peer. - pub peer_info: PeerInfo, -} - /// The results of validators voting during an epoch. /// /// Provides information about the current and previous epochs. @@ -235,15 +218,6 @@ impl From for FinalizedExecutionBlock { } } -#[derive(Debug, Serialize, Deserialize)] -pub struct DatabaseInfo { - pub schema_version: u64, - pub config: StoreConfig, - pub split: Split, - pub anchor: AnchorInfo, - pub blob_info: BlobInfo, -} - impl BeaconNodeHttpClient { /// `GET lighthouse/health` pub async fn get_lighthouse_health(&self) -> Result, Error> { @@ -381,19 +355,6 @@ impl BeaconNodeHttpClient { self.get_opt::<(), _>(path).await.map(|opt| opt.is_some()) } - /// `GET lighthouse/database/info` - pub async fn get_lighthouse_database_info(&self) -> Result { - let mut path = self.server.full.clone(); - - path.path_segments_mut() - .map_err(|()| Error::InvalidUrl(self.server.clone()))? - .push("lighthouse") - .push("database") - .push("info"); - - self.get(path).await - } - /// `POST lighthouse/database/reconstruct` pub async fn post_lighthouse_database_reconstruct(&self) -> Result { let mut path = self.server.full.clone(); diff --git a/common/eth2/src/lighthouse/attestation_rewards.rs b/common/eth2/src/lighthouse/attestation_rewards.rs deleted file mode 100644 index fa3f93d06f8..00000000000 --- a/common/eth2/src/lighthouse/attestation_rewards.rs +++ /dev/null @@ -1,55 +0,0 @@ -use serde::{Deserialize, Serialize}; -use serde_utils::quoted_u64::Quoted; - -// Details about the rewards paid for attestations -// All rewards in GWei - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct IdealAttestationRewards { - // Validator's effective balance in gwei - #[serde(with = "serde_utils::quoted_u64")] - pub effective_balance: u64, - // Ideal attester's reward for head vote in gwei - #[serde(with = "serde_utils::quoted_u64")] - pub head: u64, - // Ideal attester's reward for target vote in gwei - #[serde(with = "serde_utils::quoted_u64")] - pub target: u64, - // Ideal attester's reward for source vote in gwei - #[serde(with = "serde_utils::quoted_u64")] - pub source: u64, - // Ideal attester's inclusion_delay reward in gwei (phase0 only) - #[serde(skip_serializing_if = "Option::is_none")] - pub inclusion_delay: Option>, - // Ideal attester's inactivity penalty in gwei - #[serde(with = "serde_utils::quoted_i64")] - pub inactivity: i64, -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct TotalAttestationRewards { - // one entry for every validator based on their attestations in the epoch - #[serde(with = "serde_utils::quoted_u64")] - pub validator_index: u64, - // attester's reward for head vote in gwei - #[serde(with = "serde_utils::quoted_i64")] - pub head: i64, - // attester's reward for target vote in gwei - #[serde(with = "serde_utils::quoted_i64")] - pub target: i64, - // attester's reward for source vote in gwei - #[serde(with = "serde_utils::quoted_i64")] - pub source: i64, - // attester's inclusion_delay reward in gwei (phase0 only) - #[serde(skip_serializing_if = "Option::is_none")] - pub inclusion_delay: Option>, - // attester's inactivity penalty in gwei - #[serde(with = "serde_utils::quoted_i64")] - pub inactivity: i64, -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct StandardAttestationRewards { - pub ideal_rewards: Vec, - pub total_rewards: Vec, -} diff --git a/common/eth2/src/lighthouse/standard_block_rewards.rs b/common/eth2/src/lighthouse/standard_block_rewards.rs deleted file mode 100644 index 15fcdc60667..00000000000 --- a/common/eth2/src/lighthouse/standard_block_rewards.rs +++ /dev/null @@ -1,26 +0,0 @@ -use serde::{Deserialize, Serialize}; - -// Details about the rewards for a single block -// All rewards in GWei -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -pub struct StandardBlockReward { - // proposer of the block, the proposer index who receives these rewards - #[serde(with = "serde_utils::quoted_u64")] - pub proposer_index: u64, - // total block reward in gwei, - // equal to attestations + sync_aggregate + proposer_slashings + attester_slashings - #[serde(with = "serde_utils::quoted_u64")] - pub total: u64, - // block reward component due to included attestations in gwei - #[serde(with = "serde_utils::quoted_u64")] - pub attestations: u64, - // block reward component due to included sync_aggregate in gwei - #[serde(with = "serde_utils::quoted_u64")] - pub sync_aggregate: u64, - // block reward component due to included proposer_slashings in gwei - #[serde(with = "serde_utils::quoted_u64")] - pub proposer_slashings: u64, - // block reward component due to included attester_slashings in gwei - #[serde(with = "serde_utils::quoted_u64")] - pub attester_slashings: u64, -} diff --git a/common/eth2/src/lighthouse/sync_committee_rewards.rs b/common/eth2/src/lighthouse/sync_committee_rewards.rs deleted file mode 100644 index 66a721dc229..00000000000 --- a/common/eth2/src/lighthouse/sync_committee_rewards.rs +++ /dev/null @@ -1,13 +0,0 @@ -use serde::{Deserialize, Serialize}; - -// Details about the rewards paid to sync committee members for attesting headers -// All rewards in GWei - -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -pub struct SyncCommitteeReward { - #[serde(with = "serde_utils::quoted_u64")] - pub validator_index: u64, - // sync committee reward in gwei for the validator - #[serde(with = "serde_utils::quoted_i64")] - pub reward: i64, -} diff --git a/beacon_node/lighthouse_network/src/types/sync_state.rs b/common/eth2/src/lighthouse/sync_state.rs similarity index 100% rename from beacon_node/lighthouse_network/src/types/sync_state.rs rename to common/eth2/src/lighthouse/sync_state.rs diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index 7ce486b855b..0f9fa7f2c2d 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -5,11 +5,13 @@ use crate::{ Error as ServerError, CONSENSUS_BLOCK_VALUE_HEADER, CONSENSUS_VERSION_HEADER, EXECUTION_PAYLOAD_BLINDED_HEADER, EXECUTION_PAYLOAD_VALUE_HEADER, }; -use lighthouse_network::{ConnectionDirection, Enr, Multiaddr, PeerConnectionStatus}; +use enr::{CombinedKey, Enr}; 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}; use std::fmt::{self, Display}; @@ -580,7 +582,7 @@ pub struct ChainHeadData { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct IdentityData { pub peer_id: String, - pub enr: Enr, + pub enr: Enr, pub p2p_addresses: Vec, pub discovery_addresses: Vec, pub metadata: MetaData, @@ -863,19 +865,6 @@ pub enum PeerState { Disconnecting, } -impl PeerState { - pub fn from_peer_connection_status(status: &PeerConnectionStatus) -> Self { - match status { - PeerConnectionStatus::Connected { .. } => PeerState::Connected, - PeerConnectionStatus::Dialing { .. } => PeerState::Connecting, - PeerConnectionStatus::Disconnecting { .. } => PeerState::Disconnecting, - PeerConnectionStatus::Disconnected { .. } - | PeerConnectionStatus::Banned { .. } - | PeerConnectionStatus::Unknown => PeerState::Disconnected, - } - } -} - impl FromStr for PeerState { type Err = String; @@ -908,15 +897,6 @@ pub enum PeerDirection { Outbound, } -impl PeerDirection { - pub fn from_connection_direction(direction: &ConnectionDirection) -> Self { - match direction { - ConnectionDirection::Incoming => PeerDirection::Inbound, - ConnectionDirection::Outgoing => PeerDirection::Outbound, - } - } -} - impl FromStr for PeerDirection { type Err = String; @@ -2102,6 +2082,90 @@ pub struct BlobsBundle { pub blobs: BlobsList, } +/// Details about the rewards paid to sync committee members for attesting headers +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct SyncCommitteeReward { + #[serde(with = "serde_utils::quoted_u64")] + pub validator_index: u64, + /// sync committee reward in gwei for the validator + #[serde(with = "serde_utils::quoted_i64")] + pub reward: i64, +} + +/// Details about the rewards for a single block +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct StandardBlockReward { + /// proposer of the block, the proposer index who receives these rewards + #[serde(with = "serde_utils::quoted_u64")] + pub proposer_index: u64, + /// total block reward in gwei, + /// equal to attestations + sync_aggregate + proposer_slashings + attester_slashings + #[serde(with = "serde_utils::quoted_u64")] + pub total: u64, + /// block reward component due to included attestations in gwei + #[serde(with = "serde_utils::quoted_u64")] + pub attestations: u64, + /// block reward component due to included sync_aggregate in gwei + #[serde(with = "serde_utils::quoted_u64")] + pub sync_aggregate: u64, + /// block reward component due to included proposer_slashings in gwei + #[serde(with = "serde_utils::quoted_u64")] + pub proposer_slashings: u64, + /// block reward component due to included attester_slashings in gwei + #[serde(with = "serde_utils::quoted_u64")] + pub attester_slashings: u64, +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct IdealAttestationRewards { + /// Validator's effective balance in gwei + #[serde(with = "serde_utils::quoted_u64")] + pub effective_balance: u64, + /// Ideal attester's reward for head vote in gwei + #[serde(with = "serde_utils::quoted_u64")] + pub head: u64, + /// Ideal attester's reward for target vote in gwei + #[serde(with = "serde_utils::quoted_u64")] + pub target: u64, + /// Ideal attester's reward for source vote in gwei + #[serde(with = "serde_utils::quoted_u64")] + pub source: u64, + /// Ideal attester's inclusion_delay reward in gwei (phase0 only) + #[serde(skip_serializing_if = "Option::is_none")] + pub inclusion_delay: Option>, + /// Ideal attester's inactivity penalty in gwei + #[serde(with = "serde_utils::quoted_i64")] + pub inactivity: i64, +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct TotalAttestationRewards { + /// one entry for every validator based on their attestations in the epoch + #[serde(with = "serde_utils::quoted_u64")] + pub validator_index: u64, + /// attester's reward for head vote in gwei + #[serde(with = "serde_utils::quoted_i64")] + pub head: i64, + /// attester's reward for target vote in gwei + #[serde(with = "serde_utils::quoted_i64")] + pub target: i64, + /// attester's reward for source vote in gwei + #[serde(with = "serde_utils::quoted_i64")] + pub source: i64, + /// attester's inclusion_delay reward in gwei (phase0 only) + #[serde(skip_serializing_if = "Option::is_none")] + pub inclusion_delay: Option>, + /// attester's inactivity penalty in gwei + #[serde(with = "serde_utils::quoted_i64")] + pub inactivity: i64, +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct StandardAttestationRewards { + pub ideal_rewards: Vec, + pub total_rewards: Vec, +} + #[cfg(test)] mod test { use std::fmt::Debug; diff --git a/common/eth2_config/src/lib.rs b/common/eth2_config/src/lib.rs index 017bdf288d8..544138f0fa9 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/Cargo.toml b/common/eth2_network_config/Cargo.toml index a255e042291..da6c4dfd95a 100644 --- a/common/eth2_network_config/Cargo.toml +++ b/common/eth2_network_config/Cargo.toml @@ -20,12 +20,11 @@ bytes = { workspace = true } discv5 = { workspace = true } eth2_config = { workspace = true } kzg = { workspace = true } -logging = { workspace = true } pretty_reqwest_error = { workspace = true } reqwest = { workspace = true } sensitive_url = { workspace = true } serde_yaml = { workspace = true } sha2 = { workspace = true } -slog = { workspace = true } +tracing = { workspace = true } types = { workspace = true } url = { workspace = true } 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 dbfe2707d70..4d4ccdf7179 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 @@ -148,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 4413c21c4b8..eece34b89ce 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 @@ -132,9 +132,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/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 58010991bf0..19a3f79cc03 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 @@ -137,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/mainnet/boot_enr.yaml b/common/eth2_network_config/built_in_network_configs/mainnet/boot_enr.yaml deleted file mode 100644 index 1ae519387ae..00000000000 --- 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 00000000000..70aeaac9c57 --- /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 375441e504f..886e5d12ed9 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 @@ -152,9 +152,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/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 e9e8a3ab149..10be107263f 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 @@ -138,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 5d5a50574b1..ac488ed2a3f 100644 --- a/common/eth2_network_config/src/lib.rs +++ b/common/eth2_network_config/src/lib.rs @@ -19,19 +19,19 @@ use pretty_reqwest_error::PrettyReqwestError; use reqwest::{Client, Error}; use sensitive_url::SensitiveUrl; use sha2::{Digest, Sha256}; -use slog::{info, warn, Logger}; use std::fs::{create_dir_all, File}; use std::io::{Read, Write}; use std::path::PathBuf; use std::str::FromStr; use std::time::Duration; +use tracing::{info, warn}; use types::{BeaconState, ChainSpec, Config, EthSpec, EthSpecId, Hash256}; 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"; @@ -198,7 +198,6 @@ impl Eth2NetworkConfig { &self, genesis_state_url: Option<&str>, timeout: Duration, - log: &Logger, ) -> Result>, String> { let spec = self.chain_spec::()?; match &self.genesis_state_source { @@ -217,9 +216,9 @@ impl Eth2NetworkConfig { format!("Unable to parse genesis state bytes checksum: {:?}", e) })?; let bytes = if let Some(specified_url) = genesis_state_url { - download_genesis_state(&[specified_url], timeout, checksum, log).await + download_genesis_state(&[specified_url], timeout, checksum).await } else { - download_genesis_state(built_in_urls, timeout, checksum, log).await + download_genesis_state(built_in_urls, timeout, checksum).await }?; let state = BeaconState::from_ssz_bytes(bytes.as_ref(), &spec).map_err(|e| { format!("Downloaded genesis state SSZ bytes are invalid: {:?}", e) @@ -387,7 +386,6 @@ async fn download_genesis_state( urls: &[&str], timeout: Duration, checksum: Hash256, - log: &Logger, ) -> Result, String> { if urls.is_empty() { return Err( @@ -407,11 +405,10 @@ async fn download_genesis_state( .unwrap_or_else(|_| "".to_string()); info!( - log, - "Downloading genesis state"; - "server" => &redacted_url, - "timeout" => ?timeout, - "info" => "this may take some time on testnets with large validator counts" + server = &redacted_url, + timeout = ?timeout, + info = "this may take some time on testnets with large validator counts", + "Downloading genesis state" ); let client = Client::new(); @@ -424,10 +421,9 @@ async fn download_genesis_state( return Ok(bytes.into()); } else { warn!( - log, - "Genesis state download failed"; - "server" => &redacted_url, - "timeout" => ?timeout, + server = &redacted_url, + timeout = ?timeout, + "Genesis state download failed" ); errors.push(format!( "Response from {} did not match local checksum", @@ -505,7 +501,7 @@ mod tests { async fn mainnet_genesis_state() { let config = Eth2NetworkConfig::from_hardcoded_net(&MAINNET).unwrap(); config - .genesis_state::(None, Duration::from_secs(1), &logging::test_logger()) + .genesis_state::(None, Duration::from_secs(1)) .await .expect("beacon state can decode"); } diff --git a/common/lighthouse_version/Cargo.toml b/common/lighthouse_version/Cargo.toml index 164e3e47a7a..cb4a43e4077 100644 --- a/common/lighthouse_version/Cargo.toml +++ b/common/lighthouse_version/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "lighthouse_version" version = "0.1.0" -authors = ["Paul Hauner "] +authors = ["Sigma Prime "] edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/common/lighthouse_version/src/lib.rs b/common/lighthouse_version/src/lib.rs index 919e3976e7a..b20708e7b03 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-", - fallback = "Lighthouse/v7.0.0" + 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" + "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 b2829a48d8f..41c82dbd61b 100644 --- a/common/logging/Cargo.toml +++ b/common/logging/Cargo.toml @@ -9,17 +9,14 @@ test_logger = [] # Print log output to stderr when running tests instead of drop [dependencies] chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } +logroller = { workspace = true } metrics = { workspace = true } -parking_lot = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -slog = { workspace = true } -slog-term = { workspace = true } -sloggers = { workspace = true } -take_mut = "0.2.2" tokio = { workspace = true, features = [ "time" ] } -tracing = "0.1" +tracing = { workspace = true } tracing-appender = { workspace = true } tracing-core = { workspace = true } tracing-log = { workspace = true } tracing-subscriber = { workspace = true } +workspace_members = { workspace = true } diff --git a/common/logging/src/async_record.rs b/common/logging/src/async_record.rs deleted file mode 100644 index 7a97fa1a759..00000000000 --- a/common/logging/src/async_record.rs +++ /dev/null @@ -1,307 +0,0 @@ -//! An object that can be used to pass through a channel and be cloned. It can therefore be used -//! via the broadcast channel. - -use parking_lot::Mutex; -use serde::ser::SerializeMap; -use serde::serde_if_integer128; -use serde::Serialize; -use slog::{BorrowedKV, Key, Level, OwnedKVList, Record, RecordStatic, Serializer, SingleKV, KV}; -use std::cell::RefCell; -use std::fmt; -use std::fmt::Write; -use std::sync::Arc; -use take_mut::take; - -thread_local! { - static TL_BUF: RefCell = RefCell::new(String::with_capacity(128)) -} - -/// Serialized record. -#[derive(Clone)] -pub struct AsyncRecord { - msg: String, - level: Level, - location: Box, - tag: String, - logger_values: OwnedKVList, - kv: Arc>, -} - -impl AsyncRecord { - /// Serializes a `Record` and an `OwnedKVList`. - pub fn from(record: &Record, logger_values: &OwnedKVList) -> Self { - let mut ser = ToSendSerializer::new(); - record - .kv() - .serialize(record, &mut ser) - .expect("`ToSendSerializer` can't fail"); - - AsyncRecord { - msg: fmt::format(*record.msg()), - level: record.level(), - location: Box::new(*record.location()), - tag: String::from(record.tag()), - logger_values: logger_values.clone(), - kv: Arc::new(Mutex::new(ser.finish())), - } - } - - pub fn to_json_string(&self) -> Result { - serde_json::to_string(&self).map_err(|e| format!("{:?}", e)) - } -} - -pub struct ToSendSerializer { - kv: Box, -} - -impl ToSendSerializer { - fn new() -> Self { - ToSendSerializer { kv: Box::new(()) } - } - - fn finish(self) -> Box { - self.kv - } -} - -impl Serializer for ToSendSerializer { - fn emit_bool(&mut self, key: Key, val: bool) -> slog::Result { - take(&mut self.kv, |kv| Box::new((kv, SingleKV(key, val)))); - Ok(()) - } - fn emit_unit(&mut self, key: Key) -> slog::Result { - take(&mut self.kv, |kv| Box::new((kv, SingleKV(key, ())))); - Ok(()) - } - fn emit_none(&mut self, key: Key) -> slog::Result { - let val: Option<()> = None; - take(&mut self.kv, |kv| Box::new((kv, SingleKV(key, val)))); - Ok(()) - } - fn emit_char(&mut self, key: Key, val: char) -> slog::Result { - take(&mut self.kv, |kv| Box::new((kv, SingleKV(key, val)))); - Ok(()) - } - fn emit_u8(&mut self, key: Key, val: u8) -> slog::Result { - take(&mut self.kv, |kv| Box::new((kv, SingleKV(key, val)))); - Ok(()) - } - fn emit_i8(&mut self, key: Key, val: i8) -> slog::Result { - take(&mut self.kv, |kv| Box::new((kv, SingleKV(key, val)))); - Ok(()) - } - fn emit_u16(&mut self, key: Key, val: u16) -> slog::Result { - take(&mut self.kv, |kv| Box::new((kv, SingleKV(key, val)))); - Ok(()) - } - fn emit_i16(&mut self, key: Key, val: i16) -> slog::Result { - take(&mut self.kv, |kv| Box::new((kv, SingleKV(key, val)))); - Ok(()) - } - fn emit_u32(&mut self, key: Key, val: u32) -> slog::Result { - take(&mut self.kv, |kv| Box::new((kv, SingleKV(key, val)))); - Ok(()) - } - fn emit_i32(&mut self, key: Key, val: i32) -> slog::Result { - take(&mut self.kv, |kv| Box::new((kv, SingleKV(key, val)))); - Ok(()) - } - fn emit_f32(&mut self, key: Key, val: f32) -> slog::Result { - take(&mut self.kv, |kv| Box::new((kv, SingleKV(key, val)))); - Ok(()) - } - fn emit_u64(&mut self, key: Key, val: u64) -> slog::Result { - take(&mut self.kv, |kv| Box::new((kv, SingleKV(key, val)))); - Ok(()) - } - fn emit_i64(&mut self, key: Key, val: i64) -> slog::Result { - take(&mut self.kv, |kv| Box::new((kv, SingleKV(key, val)))); - Ok(()) - } - fn emit_f64(&mut self, key: Key, val: f64) -> slog::Result { - take(&mut self.kv, |kv| Box::new((kv, SingleKV(key, val)))); - Ok(()) - } - fn emit_u128(&mut self, key: Key, val: u128) -> slog::Result { - take(&mut self.kv, |kv| Box::new((kv, SingleKV(key, val)))); - Ok(()) - } - fn emit_i128(&mut self, key: Key, val: i128) -> slog::Result { - take(&mut self.kv, |kv| Box::new((kv, SingleKV(key, val)))); - Ok(()) - } - fn emit_usize(&mut self, key: Key, val: usize) -> slog::Result { - take(&mut self.kv, |kv| Box::new((kv, SingleKV(key, val)))); - Ok(()) - } - fn emit_isize(&mut self, key: Key, val: isize) -> slog::Result { - take(&mut self.kv, |kv| Box::new((kv, SingleKV(key, val)))); - Ok(()) - } - fn emit_str(&mut self, key: Key, val: &str) -> slog::Result { - let val = val.to_owned(); - take(&mut self.kv, |kv| Box::new((kv, SingleKV(key, val)))); - Ok(()) - } - fn emit_arguments(&mut self, key: Key, val: &fmt::Arguments) -> slog::Result { - let val = fmt::format(*val); - take(&mut self.kv, |kv| Box::new((kv, SingleKV(key, val)))); - Ok(()) - } -} - -impl Serialize for AsyncRecord { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - // Get the current time - let dt = chrono::Local::now().format("%b %e %T").to_string(); - - let rs = RecordStatic { - location: &self.location, - level: self.level, - tag: &self.tag, - }; - let mut map_serializer = SerdeSerializer::new(serializer)?; - - // Serialize the time and log level first - map_serializer.serialize_entry("time", &dt)?; - map_serializer.serialize_entry("level", self.level.as_short_str())?; - - let kv = self.kv.lock(); - - // Convoluted pattern to avoid binding `format_args!` to a temporary. - // See: https://stackoverflow.com/questions/56304313/cannot-use-format-args-due-to-temporary-value-is-freed-at-the-end-of-this-state - let mut f = |msg: std::fmt::Arguments| { - map_serializer.serialize_entry("msg", msg.to_string())?; - - let record = Record::new(&rs, &msg, BorrowedKV(&(*kv))); - self.logger_values - .serialize(&record, &mut map_serializer) - .map_err(serde::ser::Error::custom)?; - record - .kv() - .serialize(&record, &mut map_serializer) - .map_err(serde::ser::Error::custom) - }; - f(format_args!("{}", self.msg))?; - map_serializer.end() - } -} - -struct SerdeSerializer { - /// Current state of map serializing: `serde::Serializer::MapState` - ser_map: S::SerializeMap, -} - -impl SerdeSerializer { - fn new(ser: S) -> Result { - let ser_map = ser.serialize_map(None)?; - Ok(SerdeSerializer { ser_map }) - } - - fn serialize_entry(&mut self, key: K, value: V) -> Result<(), S::Error> - where - K: serde::Serialize, - V: serde::Serialize, - { - self.ser_map.serialize_entry(&key, &value) - } - - /// Finish serialization, and return the serializer - fn end(self) -> Result { - self.ser_map.end() - } -} - -// NOTE: This is borrowed from slog_json -macro_rules! impl_m( - ($s:expr, $key:expr, $val:expr) => ({ - let k_s: &str = $key.as_ref(); - $s.ser_map.serialize_entry(k_s, $val) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("serde serialization error: {}", e)))?; - Ok(()) - }); -); - -impl slog::Serializer for SerdeSerializer -where - S: serde::Serializer, -{ - fn emit_bool(&mut self, key: Key, val: bool) -> slog::Result { - impl_m!(self, key, &val) - } - - fn emit_unit(&mut self, key: Key) -> slog::Result { - impl_m!(self, key, &()) - } - - fn emit_char(&mut self, key: Key, val: char) -> slog::Result { - impl_m!(self, key, &val) - } - - fn emit_none(&mut self, key: Key) -> slog::Result { - let val: Option<()> = None; - impl_m!(self, key, &val) - } - fn emit_u8(&mut self, key: Key, val: u8) -> slog::Result { - impl_m!(self, key, &val) - } - fn emit_i8(&mut self, key: Key, val: i8) -> slog::Result { - impl_m!(self, key, &val) - } - fn emit_u16(&mut self, key: Key, val: u16) -> slog::Result { - impl_m!(self, key, &val) - } - fn emit_i16(&mut self, key: Key, val: i16) -> slog::Result { - impl_m!(self, key, &val) - } - fn emit_usize(&mut self, key: Key, val: usize) -> slog::Result { - impl_m!(self, key, &val) - } - fn emit_isize(&mut self, key: Key, val: isize) -> slog::Result { - impl_m!(self, key, &val) - } - fn emit_u32(&mut self, key: Key, val: u32) -> slog::Result { - impl_m!(self, key, &val) - } - fn emit_i32(&mut self, key: Key, val: i32) -> slog::Result { - impl_m!(self, key, &val) - } - fn emit_f32(&mut self, key: Key, val: f32) -> slog::Result { - impl_m!(self, key, &val) - } - fn emit_u64(&mut self, key: Key, val: u64) -> slog::Result { - impl_m!(self, key, &val) - } - fn emit_i64(&mut self, key: Key, val: i64) -> slog::Result { - impl_m!(self, key, &val) - } - fn emit_f64(&mut self, key: Key, val: f64) -> slog::Result { - impl_m!(self, key, &val) - } - serde_if_integer128! { - fn emit_u128(&mut self, key: Key, val: u128) -> slog::Result { - impl_m!(self, key, &val) - } - fn emit_i128(&mut self, key: Key, val: i128) -> slog::Result { - impl_m!(self, key, &val) - } - } - fn emit_str(&mut self, key: Key, val: &str) -> slog::Result { - impl_m!(self, key, &val) - } - fn emit_arguments(&mut self, key: Key, val: &fmt::Arguments) -> slog::Result { - TL_BUF.with(|buf| { - let mut buf = buf.borrow_mut(); - - buf.write_fmt(*val).unwrap(); - - let res = { || impl_m!(self, key, &*buf) }(); - buf.clear(); - res - }) - } -} diff --git a/common/logging/src/lib.rs b/common/logging/src/lib.rs index 0ddd867c2f8..5c4de1fd61c 100644 --- a/common/logging/src/lib.rs +++ b/common/logging/src/lib.rs @@ -1,24 +1,24 @@ -use metrics::{inc_counter, try_create_int_counter, IntCounter, Result as MetricsResult}; -use slog::Logger; -use slog_term::Decorator; -use std::io::{Result, Write}; -use std::path::PathBuf; +use metrics::{try_create_int_counter, IntCounter, Result as MetricsResult}; use std::sync::LazyLock; use std::time::{Duration, Instant}; -use tracing_appender::non_blocking::NonBlocking; -use tracing_appender::rolling::{RollingFileAppender, Rotation}; -use tracing_logging_layer::LoggingLayer; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +use tracing_subscriber::EnvFilter; pub const MAX_MESSAGE_WIDTH: usize = 40; -pub mod async_record; +pub mod macros; mod sse_logging_components; -mod tracing_logging_layer; +mod tracing_libp2p_discv5_logging_layer; +pub mod tracing_logging_layer; mod tracing_metrics_layer; +mod utils; pub use sse_logging_components::SSELoggingComponents; +pub use tracing_libp2p_discv5_logging_layer::{ + create_libp2p_discv5_tracing_layer, Libp2pDiscv5TracingLayer, +}; +pub use tracing_logging_layer::LoggingLayer; pub use tracing_metrics_layer::MetricsLayer; +pub use utils::build_workspace_filter; /// The minimum interval between log messages indicating that a queue is full. const LOG_DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30); @@ -32,169 +32,6 @@ pub static ERRORS_TOTAL: LazyLock> = pub static CRITS_TOTAL: LazyLock> = LazyLock::new(|| try_create_int_counter("crit_total", "Count of crits logged")); -pub struct AlignedTermDecorator { - wrapped: D, - message_width: usize, -} - -impl AlignedTermDecorator { - pub fn new(decorator: D, message_width: usize) -> Self { - AlignedTermDecorator { - wrapped: decorator, - message_width, - } - } -} - -impl Decorator for AlignedTermDecorator { - fn with_record( - &self, - record: &slog::Record, - _logger_values: &slog::OwnedKVList, - f: F, - ) -> Result<()> - where - F: FnOnce(&mut dyn slog_term::RecordDecorator) -> std::io::Result<()>, - { - match record.level() { - slog::Level::Info => inc_counter(&INFOS_TOTAL), - slog::Level::Warning => inc_counter(&WARNS_TOTAL), - slog::Level::Error => inc_counter(&ERRORS_TOTAL), - slog::Level::Critical => inc_counter(&CRITS_TOTAL), - _ => (), - } - - self.wrapped.with_record(record, _logger_values, |deco| { - f(&mut AlignedRecordDecorator::new(deco, self.message_width)) - }) - } -} - -struct AlignedRecordDecorator<'a> { - wrapped: &'a mut dyn slog_term::RecordDecorator, - message_count: usize, - message_active: bool, - ignore_comma: bool, - message_width: usize, -} - -impl<'a> AlignedRecordDecorator<'a> { - fn new( - decorator: &'a mut dyn slog_term::RecordDecorator, - message_width: usize, - ) -> AlignedRecordDecorator<'a> { - AlignedRecordDecorator { - wrapped: decorator, - message_count: 0, - ignore_comma: false, - message_active: false, - message_width, - } - } - - fn filtered_write(&mut self, buf: &[u8]) -> Result { - if self.ignore_comma { - //don't write comma - self.ignore_comma = false; - Ok(buf.len()) - } else if self.message_active { - self.wrapped.write(buf).inspect(|n| self.message_count += n) - } else { - self.wrapped.write(buf) - } - } -} - -impl Write for AlignedRecordDecorator<'_> { - fn write(&mut self, buf: &[u8]) -> Result { - if buf.iter().any(u8::is_ascii_control) { - let filtered = buf - .iter() - .cloned() - .map(|c| if !is_ascii_control(&c) { c } else { b'_' }) - .collect::>(); - self.filtered_write(&filtered) - } else { - self.filtered_write(buf) - } - } - - fn flush(&mut self) -> Result<()> { - self.wrapped.flush() - } -} - -impl slog_term::RecordDecorator for AlignedRecordDecorator<'_> { - fn reset(&mut self) -> Result<()> { - self.message_active = false; - self.message_count = 0; - self.ignore_comma = false; - self.wrapped.reset() - } - - fn start_whitespace(&mut self) -> Result<()> { - self.wrapped.start_whitespace() - } - - fn start_msg(&mut self) -> Result<()> { - self.message_active = true; - self.ignore_comma = false; - self.wrapped.start_msg() - } - - fn start_timestamp(&mut self) -> Result<()> { - self.wrapped.start_timestamp() - } - - fn start_level(&mut self) -> Result<()> { - self.wrapped.start_level() - } - - fn start_comma(&mut self) -> Result<()> { - if self.message_active && self.message_count + 1 < self.message_width { - self.ignore_comma = true; - } - self.wrapped.start_comma() - } - - fn start_key(&mut self) -> Result<()> { - if self.message_active && self.message_count + 1 < self.message_width { - write!( - self, - "{}", - " ".repeat(self.message_width - self.message_count) - )?; - self.message_active = false; - self.message_count = 0; - self.ignore_comma = false; - } - self.wrapped.start_key() - } - - fn start_value(&mut self) -> Result<()> { - self.wrapped.start_value() - } - - fn start_separator(&mut self) -> Result<()> { - self.wrapped.start_separator() - } -} - -/// Function to filter out ascii control codes. -/// -/// This helps to keep log formatting consistent. -/// Whitespace and padding control codes are excluded. -fn is_ascii_control(character: &u8) -> bool { - matches!( - character, - b'\x00'..=b'\x08' | - b'\x0b'..=b'\x0c' | - b'\x0e'..=b'\x1f' | - b'\x7f' | - b'\x81'..=b'\x9f' - ) -} - /// Provides de-bounce functionality for logging. #[derive(Default)] pub struct TimeLatch(Option); @@ -214,75 +51,7 @@ impl TimeLatch { } } -pub fn create_tracing_layer(base_tracing_log_path: PathBuf) { - let mut tracing_log_path = PathBuf::new(); - - // Ensure that `tracing_log_path` only contains directories. - for p in base_tracing_log_path.iter() { - tracing_log_path = tracing_log_path.join(p); - if let Ok(metadata) = tracing_log_path.metadata() { - if !metadata.is_dir() { - tracing_log_path.pop(); - break; - } - } - } - - let filter_layer = match tracing_subscriber::EnvFilter::try_from_default_env() - .or_else(|_| tracing_subscriber::EnvFilter::try_new("warn")) - { - Ok(filter) => filter, - Err(e) => { - eprintln!("Failed to initialize dependency logging {e}"); - return; - } - }; - - let Ok(libp2p_writer) = RollingFileAppender::builder() - .rotation(Rotation::DAILY) - .max_log_files(2) - .filename_prefix("libp2p") - .filename_suffix("log") - .build(tracing_log_path.clone()) - else { - eprintln!("Failed to initialize libp2p rolling file appender"); - return; - }; - - let Ok(discv5_writer) = RollingFileAppender::builder() - .rotation(Rotation::DAILY) - .max_log_files(2) - .filename_prefix("discv5") - .filename_suffix("log") - .build(tracing_log_path) - else { - eprintln!("Failed to initialize discv5 rolling file appender"); - return; - }; - - let (libp2p_non_blocking_writer, _libp2p_guard) = NonBlocking::new(libp2p_writer); - let (discv5_non_blocking_writer, _discv5_guard) = NonBlocking::new(discv5_writer); - - let custom_layer = LoggingLayer { - libp2p_non_blocking_writer, - _libp2p_guard, - discv5_non_blocking_writer, - _discv5_guard, - }; - - if let Err(e) = tracing_subscriber::fmt() - .with_env_filter(filter_layer) - .with_writer(std::io::sink) - .finish() - .with(MetricsLayer) - .with(custom_layer) - .try_init() - { - eprintln!("Failed to initialize dependency logging {e}"); - } -} - -/// Return a logger suitable for test usage. +/// Return a tracing subscriber suitable for test usage. /// /// By default no logs will be printed, but they can be enabled via /// the `test_logger` feature. This feature can be enabled for any @@ -290,17 +59,10 @@ pub fn create_tracing_layer(base_tracing_log_path: PathBuf) { /// ```bash /// cargo test -p beacon_chain --features logging/test_logger /// ``` -pub fn test_logger() -> Logger { - use sloggers::Build; - +pub fn create_test_tracing_subscriber() { if cfg!(feature = "test_logger") { - sloggers::terminal::TerminalLoggerBuilder::new() - .level(sloggers::types::Severity::Debug) - .build() - .expect("Should build TerminalLoggerBuilder") - } else { - sloggers::null::NullLoggerBuilder - .build() - .expect("Should build NullLoggerBuilder") + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::try_new("debug").unwrap()) + .try_init(); } } diff --git a/common/logging/src/macros.rs b/common/logging/src/macros.rs new file mode 100644 index 00000000000..eb25eba56ce --- /dev/null +++ b/common/logging/src/macros.rs @@ -0,0 +1,6 @@ +#[macro_export] +macro_rules! crit { + ($($arg:tt)*) => { + tracing::error!(error_type = "crit", $($arg)*); + }; +} diff --git a/common/logging/src/sse_logging_components.rs b/common/logging/src/sse_logging_components.rs index 244d09fbd12..d526f2b040d 100644 --- a/common/logging/src/sse_logging_components.rs +++ b/common/logging/src/sse_logging_components.rs @@ -1,46 +1,108 @@ -//! 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 crate::async_record::AsyncRecord; -use slog::{Drain, OwnedKVList, Record}; -use std::panic::AssertUnwindSafe; +use serde_json::json; +use serde_json::Value; use std::sync::Arc; use tokio::sync::broadcast::Sender; +use tracing::field::{Field, Visit}; +use tracing::{Event, Subscriber}; +use tracing_subscriber::layer::{Context, Layer}; /// Default log level for SSE Events. // NOTE: Made this a constant. Debug level seems to be pretty intense. Can make this // configurable later if needed. -const LOG_LEVEL: slog::Level = slog::Level::Info; +const LOG_LEVEL: tracing::Level = tracing::Level::INFO; /// The components required in the HTTP API task to receive logged events. #[derive(Clone)] pub struct SSELoggingComponents { /// The channel to receive events from. - pub sender: Arc>>, + pub sender: Arc>>, } impl SSELoggingComponents { - /// Create a new SSE drain. pub fn new(channel_size: usize) -> Self { let (sender, _receiver) = tokio::sync::broadcast::channel(channel_size); - let sender = Arc::new(AssertUnwindSafe(sender)); - SSELoggingComponents { sender } + SSELoggingComponents { + sender: Arc::new(sender), + } } } -impl Drain for SSELoggingComponents { - type Ok = (); - type Err = &'static str; +impl Layer for SSELoggingComponents { + fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { + if *event.metadata().level() > LOG_LEVEL { + return; + } + + let mut visitor = TracingEventVisitor::new(); + event.record(&mut visitor); + let mut log_entry = visitor.finish(event.metadata()); - fn log(&self, record: &Record, logger_values: &OwnedKVList) -> Result { - if record.level().is_at_least(LOG_LEVEL) { - // Attempt to send the logs - match self.sender.send(AsyncRecord::from(record, logger_values)) { - Ok(_num_sent) => {} // Everything got sent - Err(_err) => {} // There are no subscribers, do nothing + if let Some(error_type) = log_entry + .get("fields") + .and_then(|fields| fields.get("error_type")) + .and_then(|val| val.as_str()) + { + if error_type.eq_ignore_ascii_case("crit") { + log_entry["level"] = json!("CRIT"); + + if let Some(Value::Object(ref mut map)) = log_entry.get_mut("fields") { + map.remove("error_type"); + } } } - Ok(()) + + let _ = self.sender.send(Arc::new(log_entry)); + } +} +struct TracingEventVisitor { + fields: serde_json::Map, +} + +impl TracingEventVisitor { + fn new() -> Self { + TracingEventVisitor { + fields: serde_json::Map::new(), + } + } + + fn finish(self, metadata: &tracing::Metadata<'_>) -> Value { + let mut log_entry = serde_json::Map::new(); + log_entry.insert( + "time".to_string(), + json!(chrono::Local::now() + .format("%b %d %H:%M:%S%.3f") + .to_string()), + ); + log_entry.insert("level".to_string(), json!(metadata.level().to_string())); + log_entry.insert("target".to_string(), json!(metadata.target())); + log_entry.insert("fields".to_string(), Value::Object(self.fields)); + Value::Object(log_entry) + } +} + +impl Visit for TracingEventVisitor { + fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { + self.fields + .insert(field.name().to_string(), json!(format!("{:?}", value))); + } + + fn record_str(&mut self, field: &Field, value: &str) { + self.fields.insert(field.name().to_string(), json!(value)); + } + + fn record_i64(&mut self, field: &Field, value: i64) { + self.fields.insert(field.name().to_string(), json!(value)); + } + + fn record_u64(&mut self, field: &Field, value: u64) { + self.fields.insert(field.name().to_string(), json!(value)); + } + + fn record_bool(&mut self, field: &Field, value: bool) { + self.fields.insert(field.name().to_string(), json!(value)); } } diff --git a/common/logging/src/tracing_libp2p_discv5_logging_layer.rs b/common/logging/src/tracing_libp2p_discv5_logging_layer.rs new file mode 100644 index 00000000000..90033d11ad7 --- /dev/null +++ b/common/logging/src/tracing_libp2p_discv5_logging_layer.rs @@ -0,0 +1,113 @@ +use chrono::Local; +use logroller::{LogRollerBuilder, Rotation, RotationSize}; +use std::io::Write; +use std::path::PathBuf; +use tracing::Subscriber; +use tracing_appender::non_blocking::{NonBlocking, WorkerGuard}; +use tracing_subscriber::{layer::Context, Layer}; + +pub struct Libp2pDiscv5TracingLayer { + pub libp2p_non_blocking_writer: NonBlocking, + _libp2p_guard: WorkerGuard, + pub discv5_non_blocking_writer: NonBlocking, + _discv5_guard: WorkerGuard, +} + +impl Layer for Libp2pDiscv5TracingLayer +where + S: Subscriber, +{ + fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context) { + let meta = event.metadata(); + let log_level = meta.level(); + let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); + + let target = match meta.target().split_once("::") { + Some((crate_name, _)) => crate_name, + None => "unknown", + }; + + let mut writer = match target { + "libp2p_gossipsub" => self.libp2p_non_blocking_writer.clone(), + "discv5" => self.discv5_non_blocking_writer.clone(), + _ => return, + }; + + let mut visitor = LogMessageExtractor { + message: String::default(), + }; + + event.record(&mut visitor); + let message = format!("{} {} {}\n", timestamp, log_level, visitor.message); + + if let Err(e) = writer.write_all(message.as_bytes()) { + eprintln!("Failed to write log: {}", e); + } + } +} + +struct LogMessageExtractor { + message: String, +} + +impl tracing_core::field::Visit for LogMessageExtractor { + fn record_debug(&mut self, _: &tracing_core::Field, value: &dyn std::fmt::Debug) { + self.message = format!("{} {:?}", self.message, value); + } +} + +pub fn create_libp2p_discv5_tracing_layer( + base_tracing_log_path: Option, + max_log_size: u64, +) -> Option { + if let Some(mut tracing_log_path) = base_tracing_log_path { + // Ensure that `tracing_log_path` only contains directories. + for p in tracing_log_path.clone().iter() { + tracing_log_path = tracing_log_path.join(p); + if let Ok(metadata) = tracing_log_path.metadata() { + if !metadata.is_dir() { + tracing_log_path.pop(); + break; + } + } + } + + let libp2p_writer = + LogRollerBuilder::new(tracing_log_path.clone(), PathBuf::from("libp2p.log")) + .rotation(Rotation::SizeBased(RotationSize::MB(max_log_size))) + .max_keep_files(1); + + let discv5_writer = + LogRollerBuilder::new(tracing_log_path.clone(), PathBuf::from("discv5.log")) + .rotation(Rotation::SizeBased(RotationSize::MB(max_log_size))) + .max_keep_files(1); + + let libp2p_writer = match libp2p_writer.build() { + Ok(writer) => writer, + Err(e) => { + eprintln!("Failed to initialize libp2p rolling file appender: {e}"); + std::process::exit(1); + } + }; + + let discv5_writer = match discv5_writer.build() { + Ok(writer) => writer, + Err(e) => { + eprintln!("Failed to initialize discv5 rolling file appender: {e}"); + std::process::exit(1); + } + }; + + let (libp2p_non_blocking_writer, _libp2p_guard) = NonBlocking::new(libp2p_writer); + let (discv5_non_blocking_writer, _discv5_guard) = NonBlocking::new(discv5_writer); + + Some(Libp2pDiscv5TracingLayer { + libp2p_non_blocking_writer, + _libp2p_guard, + discv5_non_blocking_writer, + _discv5_guard, + }) + } else { + None + } +} diff --git a/common/logging/src/tracing_logging_layer.rs b/common/logging/src/tracing_logging_layer.rs index a9ddae828ae..c3784a8f621 100644 --- a/common/logging/src/tracing_logging_layer.rs +++ b/common/logging/src/tracing_logging_layer.rs @@ -1,56 +1,457 @@ +use crate::utils::is_ascii_control; + use chrono::prelude::*; +use serde_json::{Map, Value}; +use std::collections::HashMap; use std::io::Write; +use std::sync::{Arc, Mutex}; +use tracing::field::Field; +use tracing::span::Id; use tracing::Subscriber; use tracing_appender::non_blocking::{NonBlocking, WorkerGuard}; 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 libp2p_non_blocking_writer: NonBlocking, - pub _libp2p_guard: WorkerGuard, - pub discv5_non_blocking_writer: NonBlocking, - pub _discv5_guard: WorkerGuard, + pub non_blocking_writer: NonBlocking, + _guard: WorkerGuard, + pub disable_log_timestamp: bool, + pub log_color: bool, + pub log_format: Option, + pub extra_info: bool, + span_fields: Arc>>, +} + +impl LoggingLayer { + #[allow(clippy::too_many_arguments)] + pub fn new( + non_blocking_writer: NonBlocking, + _guard: WorkerGuard, + disable_log_timestamp: bool, + log_color: bool, + log_format: Option, + extra_info: bool, + ) -> Self { + Self { + non_blocking_writer, + _guard, + disable_log_timestamp, + log_color, + log_format, + extra_info, + span_fields: Arc::new(Mutex::new(HashMap::new())), + } + } } impl Layer for LoggingLayer where - S: Subscriber, + S: Subscriber + for<'a> LookupSpan<'a>, { - fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context) { + fn on_new_span(&self, attrs: &tracing::span::Attributes<'_>, id: &Id, _ctx: Context) { + let metadata = attrs.metadata(); + let span_name = metadata.name(); + + let mut visitor = SpanFieldsExtractor::default(); + attrs.record(&mut visitor); + + let span_data = SpanData { + name: span_name.to_string(), + fields: visitor.fields, + }; + + let mut span_fields = match self.span_fields.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + span_fields.insert(id.clone(), span_data); + } + + fn on_event(&self, event: &tracing::Event<'_>, ctx: Context) { let meta = event.metadata(); let log_level = meta.level(); - let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); + let timestamp = if !self.disable_log_timestamp { + Local::now().format("%b %d %H:%M:%S%.3f").to_string() + } else { + String::new() + }; + + let mut writer = self.non_blocking_writer.clone(); - let target = match meta.target().split_once("::") { - Some((crate_name, _)) => crate_name, - None => "unknown", + let mut visitor = LogMessageExtractor { + message: String::new(), + fields: Vec::new(), + is_crit: false, }; + event.record(&mut visitor); - let mut writer = match target { - "gossipsub" => self.libp2p_non_blocking_writer.clone(), - "discv5" => self.discv5_non_blocking_writer.clone(), - _ => return, + // Remove ascii control codes from message. + // All following formatting and logs components are predetermined or known. + if visitor.message.as_bytes().iter().any(u8::is_ascii_control) { + let filtered = visitor + .message + .as_bytes() + .iter() + .map(|c| if is_ascii_control(c) { b'_' } else { *c }) + .collect::>(); + visitor.message = String::from_utf8(filtered).unwrap_or_default(); }; - let mut visitor = LogMessageExtractor { - message: String::default(), + let module = meta.module_path().unwrap_or(""); + let file = meta.file().unwrap_or(""); + let line = match meta.line() { + Some(line) => line.to_string(), + None => "".to_string(), }; - event.record(&mut visitor); - let message = format!("{} {} {}\n", timestamp, log_level, visitor.message); + let gray = "\x1b[90m"; + let reset = "\x1b[0m"; + let location = if self.extra_info { + if self.log_color { + format!("{}{}::{}:{}{}", gray, module, file, line, reset) + } else { + format!("{}::{}:{}", module, file, line) + } + } else { + String::new() + }; - if let Err(e) = writer.write_all(message.as_bytes()) { - eprintln!("Failed to write log: {}", e); + let plain_level_str = if visitor.is_crit { + "CRIT" + } else { + match *log_level { + tracing::Level::ERROR => "ERROR", + tracing::Level::WARN => "WARN", + tracing::Level::INFO => "INFO", + tracing::Level::DEBUG => "DEBUG", + tracing::Level::TRACE => "TRACE", + } + }; + + let color_level_str = if visitor.is_crit { + "\x1b[35mCRIT\x1b[0m" + } else { + match *log_level { + tracing::Level::ERROR => "\x1b[31mERROR\x1b[0m", + tracing::Level::WARN => "\x1b[33mWARN\x1b[0m", + tracing::Level::INFO => "\x1b[32mINFO\x1b[0m", + tracing::Level::DEBUG => "\x1b[34mDEBUG\x1b[0m", + tracing::Level::TRACE => "\x1b[35mTRACE\x1b[0m", + } + }; + + if self.log_format.as_deref() == Some("JSON") { + build_log_json( + &visitor, + plain_level_str, + meta, + &ctx, + &self.span_fields, + event, + &mut writer, + ); + } else { + build_log_text( + &visitor, + plain_level_str, + ×tamp, + &ctx, + &self.span_fields, + event, + &location, + color_level_str, + self.log_color, + &mut writer, + ); } } } +struct SpanData { + name: String, + fields: Vec<(String, String)>, +} + +#[derive(Default)] +struct SpanFieldsExtractor { + fields: Vec<(String, String)>, +} + +impl tracing_core::field::Visit for SpanFieldsExtractor { + fn record_str(&mut self, field: &Field, value: &str) { + self.fields + .push((field.name().to_string(), format!("\"{}\"", value))); + } + + fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { + self.fields + .push((field.name().to_string(), format!("{:?}", value))); + } + + fn record_i64(&mut self, field: &Field, value: i64) { + self.fields + .push((field.name().to_string(), value.to_string())); + } + + fn record_u64(&mut self, field: &Field, value: u64) { + self.fields + .push((field.name().to_string(), value.to_string())); + } + + fn record_bool(&mut self, field: &Field, value: bool) { + self.fields + .push((field.name().to_string(), value.to_string())); + } +} + struct LogMessageExtractor { message: String, + fields: Vec<(String, String)>, + is_crit: bool, } impl tracing_core::field::Visit for LogMessageExtractor { - fn record_debug(&mut self, _: &tracing_core::Field, value: &dyn std::fmt::Debug) { - self.message = format!("{} {:?}", self.message, value); + fn record_str(&mut self, field: &Field, value: &str) { + if field.name() == "message" { + if self.message.is_empty() { + self.message = value.to_string(); + } else { + self.fields + .push(("msg_id".to_string(), format!("\"{}\"", value))); + } + } else if field.name() == "error_type" && value == "crit" { + self.is_crit = true; + } else { + self.fields + .push((field.name().to_string(), format!("\"{}\"", value))); + } + } + + fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { + if field.name() == "message" { + if self.message.is_empty() { + self.message = format!("{:?}", value); + } else { + self.fields + .push(("msg_id".to_string(), format!("{:?}", value))); + } + } else if field.name() == "error_type" && format!("{:?}", value) == "\"crit\"" { + self.is_crit = true; + } else { + self.fields + .push((field.name().to_string(), format!("{:?}", value))); + } + } + + fn record_i64(&mut self, field: &Field, value: i64) { + self.fields + .push((field.name().to_string(), value.to_string())); } + + fn record_u64(&mut self, field: &Field, value: u64) { + self.fields + .push((field.name().to_string(), value.to_string())); + } + + fn record_bool(&mut self, field: &Field, value: bool) { + self.fields + .push((field.name().to_string(), value.to_string())); + } +} + +fn build_log_json<'a, S>( + visitor: &LogMessageExtractor, + plain_level_str: &str, + meta: &tracing::Metadata<'_>, + ctx: &Context<'_, S>, + span_fields: &Arc>>, + event: &tracing::Event<'_>, + writer: &mut impl Write, +) where + S: Subscriber + for<'lookup> LookupSpan<'lookup>, +{ + let utc_timestamp = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Micros, true); + let mut log_map = Map::new(); + + log_map.insert("msg".to_string(), Value::String(visitor.message.clone())); + log_map.insert( + "level".to_string(), + Value::String(plain_level_str.to_string()), + ); + log_map.insert("ts".to_string(), Value::String(utc_timestamp)); + + let module_path = meta.module_path().unwrap_or(""); + let line_number = meta + .line() + .map_or("".to_string(), |l| l.to_string()); + let module_field = format!("{}:{}", module_path, line_number); + log_map.insert("module".to_string(), Value::String(module_field)); + + for (key, val) in visitor.fields.clone().into_iter() { + let cleaned_value = if val.starts_with('\"') && val.ends_with('\"') && val.len() >= 2 { + &val[1..val.len() - 1] + } else { + &val + }; + let parsed_val = + serde_json::from_str(cleaned_value).unwrap_or(Value::String(cleaned_value.to_string())); + log_map.insert(key, parsed_val); + } + + if let Some(scope) = ctx.event_scope(event) { + let guard = span_fields.lock().ok(); + if let Some(span_map) = guard { + for span in scope { + let id = span.id(); + if let Some(span_data) = span_map.get(&id) { + for (key, val) in &span_data.fields { + let parsed_span_val = parse_field(val); + log_map.insert(key.clone(), parsed_span_val); + } + } + } + } + } + + let json_obj = Value::Object(log_map); + let output = format!("{}\n", json_obj); + + if let Err(e) = writer.write_all(output.as_bytes()) { + eprintln!("Failed to write log: {}", e); + } +} + +#[allow(clippy::too_many_arguments)] +fn build_log_text<'a, S>( + visitor: &LogMessageExtractor, + plain_level_str: &str, + timestamp: &str, + ctx: &Context<'_, S>, + span_fields: &Arc>>, + event: &tracing::Event<'_>, + location: &str, + color_level_str: &str, + use_color: bool, + writer: &mut impl Write, +) where + S: Subscriber + for<'lookup> LookupSpan<'lookup>, +{ + let bold_start = "\x1b[1m"; + let bold_end = "\x1b[0m"; + let mut collected_span_fields = Vec::new(); + + if let Some(scope) = ctx.event_scope(event) { + for span in scope { + let id = span.id(); + let span_fields_map = span_fields.lock().unwrap(); + if let Some(span_data) = span_fields_map.get(&id) { + collected_span_fields.push((span_data.name.clone(), span_data.fields.clone())); + } + } + } + + let mut formatted_spans = String::new(); + for (_, fields) in collected_span_fields.iter().rev() { + for (i, (field_name, field_value)) in fields.iter().enumerate() { + if i > 0 && !visitor.fields.is_empty() { + formatted_spans.push_str(", "); + } + if use_color { + formatted_spans.push_str(&format!( + "{}{}{}: {}", + bold_start, field_name, bold_end, field_value + )); + } else { + formatted_spans.push_str(&format!("{}: {}", field_name, field_value)); + } + } + } + + let pad = if plain_level_str.len() < ALIGNED_LEVEL_WIDTH { + " " + } else { + "" + }; + + let level_str = if use_color { + format!("{}{}", color_level_str, pad) + } else { + format!("{}{}", plain_level_str, pad) + }; + + let message_len = visitor.message.len(); + + let message_content = if use_color { + format!("{}{}{}", bold_start, visitor.message, bold_end) + } else { + visitor.message.clone() + }; + + let padded_message = if message_len < FIXED_MESSAGE_WIDTH { + let extra_color_len = if use_color { + bold_start.len() + bold_end.len() + } else { + 0 + }; + format!( + "{: 0 { + formatted_fields.push_str(", "); + } + if use_color { + formatted_fields.push_str(&format!( + "{}{}{}: {}", + bold_start, field_name, bold_end, field_value + )); + } else { + formatted_fields.push_str(&format!("{}: {}", field_name, field_value)); + } + if i == visitor.fields.len() - 1 && !collected_span_fields.is_empty() { + formatted_fields.push(','); + } + } + + let full_message = if !formatted_fields.is_empty() { + format!("{} {}", padded_message, formatted_fields) + } else { + padded_message.to_string() + }; + + let message = if !location.is_empty() { + format!( + "{} {} {} {} {}\n", + timestamp, level_str, location, full_message, formatted_spans + ) + } else { + format!( + "{} {} {} {}\n", + timestamp, level_str, full_message, formatted_spans + ) + }; + + if let Err(e) = writer.write_all(message.as_bytes()) { + eprintln!("Failed to write log: {}", e); + } +} + +fn parse_field(val: &str) -> Value { + let cleaned = if val.starts_with('"') && val.ends_with('"') && val.len() >= 2 { + &val[1..val.len() - 1] + } else { + val + }; + serde_json::from_str(cleaned).unwrap_or(Value::String(cleaned.to_string())) } diff --git a/common/logging/src/utils.rs b/common/logging/src/utils.rs new file mode 100644 index 00000000000..784cd5ca705 --- /dev/null +++ b/common/logging/src/utils.rs @@ -0,0 +1,31 @@ +use std::collections::HashSet; +use tracing_subscriber::filter::FilterFn; +use workspace_members::workspace_crates; + +const WORKSPACE_CRATES: &[&str] = workspace_crates!(); + +/// Constructs a filter which only permits logging from crates which are members of the workspace. +pub fn build_workspace_filter( +) -> Result bool + Clone>, String> { + let workspace_crates: HashSet<&str> = WORKSPACE_CRATES.iter().copied().collect(); + + Ok(tracing_subscriber::filter::FilterFn::new(move |metadata| { + let target_crate = metadata.target().split("::").next().unwrap_or(""); + workspace_crates.contains(target_crate) + })) +} + +/// Function to filter out ascii control codes. +/// +/// This helps to keep log formatting consistent. +/// Whitespace and padding control codes are excluded. +pub fn is_ascii_control(character: &u8) -> bool { + matches!( + character, + b'\x00'..=b'\x08' | + b'\x0b'..=b'\x0c' | + b'\x0e'..=b'\x1f' | + b'\x7f' | + b'\x81'..=b'\x9f' + ) +} diff --git a/common/logging/tests/test.rs b/common/logging/tests/test.rs deleted file mode 100644 index f39f2b6d5aa..00000000000 --- a/common/logging/tests/test.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::env; -use std::process::Command; -use std::process::Output; - -fn run_cmd(cmd_line: &str) -> Result { - if cfg!(target_os = "windows") { - Command::new(r#"cmd"#).args(["/C", cmd_line]).output() - } else { - Command::new(r#"sh"#).args(["-c", cmd_line]).output() - } -} - -#[test] -fn test_test_logger_with_feature_test_logger() { - let cur_dir = env::current_dir().unwrap(); - let test_dir = cur_dir - .join("..") - .join("..") - .join("testing") - .join("test-test_logger"); - let cmd_line = format!( - "cd {} && cargo test --features logging/test_logger", - test_dir.to_str().unwrap() - ); - - let output = run_cmd(&cmd_line); - - // Assert output data DOES contain "INFO hi, " - let data = String::from_utf8(output.unwrap().stderr).unwrap(); - println!("data={}", data); - assert!(data.contains("INFO hi, ")); -} - -#[test] -fn test_test_logger_no_features() { - // Test without features - let cur_dir = env::current_dir().unwrap(); - let test_dir = cur_dir - .join("..") - .join("..") - .join("testing") - .join("test-test_logger"); - let cmd_line = format!("cd {} && cargo test", test_dir.to_str().unwrap()); - - let output = run_cmd(&cmd_line); - - // Assert output data DOES contain "INFO hi, " - let data = String::from_utf8(output.unwrap().stderr).unwrap(); - println!("data={}", data); - assert!(!data.contains("INFO hi, ")); -} diff --git a/common/malloc_utils/src/jemalloc.rs b/common/malloc_utils/src/jemalloc.rs index f3a35fc41c0..2e90c0ddf33 100644 --- a/common/malloc_utils/src/jemalloc.rs +++ b/common/malloc_utils/src/jemalloc.rs @@ -7,9 +7,11 @@ //! //! A) `JEMALLOC_SYS_WITH_MALLOC_CONF` at compile-time. //! B) `_RJEM_MALLOC_CONF` at runtime. -use metrics::{set_gauge, try_create_int_gauge, IntGauge}; +use metrics::{ + set_gauge, set_gauge_vec, try_create_int_gauge, try_create_int_gauge_vec, IntGauge, IntGaugeVec, +}; use std::sync::LazyLock; -use tikv_jemalloc_ctl::{arenas, epoch, stats, Access, AsName, Error}; +use tikv_jemalloc_ctl::{arenas, epoch, raw, stats, Access, AsName, Error}; #[global_allocator] static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; @@ -33,6 +35,38 @@ pub static BYTES_RESIDENT: LazyLock> = LazyLock::new(| pub static BYTES_RETAINED: LazyLock> = LazyLock::new(|| { try_create_int_gauge("jemalloc_bytes_retained", "Equivalent to stats.retained") }); +pub static JEMALLOC_ARENAS_SMALL_NMALLOC: LazyLock> = + LazyLock::new(|| { + try_create_int_gauge_vec( + "jemalloc_arenas_small_nmalloc", + "Equivalent to stats.arenas..small.nmalloc", + &["arena"], + ) + }); +pub static JEMALLOC_ARENAS_SMALL_NDALLOC: LazyLock> = + LazyLock::new(|| { + try_create_int_gauge_vec( + "jemalloc_arenas_small_ndalloc", + "Equivalent to stats.arenas..small.ndalloc", + &["arena"], + ) + }); +pub static JEMALLOC_ARENAS_LARGE_NMALLOC: LazyLock> = + LazyLock::new(|| { + try_create_int_gauge_vec( + "jemalloc_arenas_large_nmalloc", + "Equivalent to stats.arenas..large.nmalloc", + &["arena"], + ) + }); +pub static JEMALLOC_ARENAS_LARGE_NDALLOC: LazyLock> = + LazyLock::new(|| { + try_create_int_gauge_vec( + "jemalloc_arenas_large_ndalloc", + "Equivalent to stats.arenas..large.ndalloc", + &["arena"], + ) + }); pub fn scrape_jemalloc_metrics() { scrape_jemalloc_metrics_fallible().unwrap() @@ -42,7 +76,8 @@ pub fn scrape_jemalloc_metrics_fallible() -> Result<(), Error> { // Advance the epoch so that the underlying statistics are updated. epoch::advance()?; - set_gauge(&NUM_ARENAS, arenas::narenas::read()? as i64); + let num_arenas = arenas::narenas::read()?; + set_gauge(&NUM_ARENAS, num_arenas as i64); set_gauge(&BYTES_ALLOCATED, stats::allocated::read()? as i64); set_gauge(&BYTES_ACTIVE, stats::active::read()? as i64); set_gauge(&BYTES_MAPPED, stats::mapped::read()? as i64); @@ -50,9 +85,40 @@ pub fn scrape_jemalloc_metrics_fallible() -> Result<(), Error> { set_gauge(&BYTES_RESIDENT, stats::resident::read()? as i64); set_gauge(&BYTES_RETAINED, stats::retained::read()? as i64); + for arena in 0..num_arenas { + unsafe { + set_stats_gauge( + &JEMALLOC_ARENAS_SMALL_NMALLOC, + arena, + &format!("stats.arenas.{arena}.small.nmalloc\0"), + ); + set_stats_gauge( + &JEMALLOC_ARENAS_SMALL_NDALLOC, + arena, + &format!("stats.arenas.{arena}.small.ndalloc\0"), + ); + set_stats_gauge( + &JEMALLOC_ARENAS_LARGE_NMALLOC, + arena, + &format!("stats.arenas.{arena}.large.nmalloc\0"), + ); + set_stats_gauge( + &JEMALLOC_ARENAS_LARGE_NDALLOC, + arena, + &format!("stats.arenas.{arena}.large.ndalloc\0"), + ); + } + } + Ok(()) } +unsafe fn set_stats_gauge(metric: &metrics::Result, arena: u32, stat: &str) { + if let Ok(val) = raw::read::(stat.as_bytes()) { + set_gauge_vec(metric, &[&format!("arena_{arena}")], val as i64); + } +} + pub fn page_size() -> Result { // Full list of keys: https://jemalloc.net/jemalloc.3.html "arenas.page\0".name().read() diff --git a/common/monitoring_api/Cargo.toml b/common/monitoring_api/Cargo.toml index cb52cff29a5..9e2c36e2c76 100644 --- a/common/monitoring_api/Cargo.toml +++ b/common/monitoring_api/Cargo.toml @@ -15,7 +15,7 @@ reqwest = { workspace = true } sensitive_url = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -slog = { workspace = true } store = { workspace = true } task_executor = { workspace = true } tokio = { workspace = true } +tracing = { workspace = true } diff --git a/common/monitoring_api/src/lib.rs b/common/monitoring_api/src/lib.rs index 6f919971b02..966a1a30542 100644 --- a/common/monitoring_api/src/lib.rs +++ b/common/monitoring_api/src/lib.rs @@ -9,9 +9,9 @@ use reqwest::{IntoUrl, Response}; pub use reqwest::{StatusCode, Url}; use sensitive_url::SensitiveUrl; use serde::{Deserialize, Serialize}; -use slog::{debug, error, info}; use task_executor::TaskExecutor; use tokio::time::{interval_at, Instant}; +use tracing::{debug, error, info}; use types::*; pub use types::ProcessType; @@ -69,11 +69,10 @@ pub struct MonitoringHttpClient { freezer_db_path: Option, update_period: Duration, monitoring_endpoint: SensitiveUrl, - log: slog::Logger, } impl MonitoringHttpClient { - pub fn new(config: &Config, log: slog::Logger) -> Result { + pub fn new(config: &Config) -> Result { Ok(Self { client: reqwest::Client::new(), db_path: config.db_path.clone(), @@ -83,7 +82,6 @@ impl MonitoringHttpClient { ), monitoring_endpoint: SensitiveUrl::parse(&config.monitoring_endpoint) .map_err(|e| format!("Invalid monitoring endpoint: {:?}", e))?, - log, }) } @@ -111,10 +109,9 @@ impl MonitoringHttpClient { ); info!( - self.log, - "Starting monitoring API"; - "endpoint" => %self.monitoring_endpoint, - "update_period" => format!("{}s", self.update_period.as_secs()), + endpoint = %self.monitoring_endpoint, + update_period = format!("{}s", self.update_period.as_secs()), + "Starting monitoring API" ); let update_future = async move { @@ -122,10 +119,10 @@ impl MonitoringHttpClient { interval.tick().await; match self.send_metrics(&processes).await { Ok(()) => { - debug!(self.log, "Metrics sent to remote server"; "endpoint" => %self.monitoring_endpoint); + debug!(endpoint = %self.monitoring_endpoint, "Metrics sent to remote server"); } Err(e) => { - error!(self.log, "Failed to send metrics to remote endpoint"; "error" => %e) + error!(error = %e, "Failed to send metrics to remote endpoint") } } } @@ -187,18 +184,16 @@ impl MonitoringHttpClient { for process in processes { match self.get_metrics(process).await { Err(e) => error!( - self.log, - "Failed to get metrics"; - "process_type" => ?process, - "error" => %e + process_type = ?process, + error = %e, + "Failed to get metrics" ), Ok(metric) => metrics.push(metric), } } info!( - self.log, - "Sending metrics to remote endpoint"; - "endpoint" => %self.monitoring_endpoint + endpoint = %self.monitoring_endpoint, + "Sending metrics to remote endpoint" ); self.post(self.monitoring_endpoint.full.clone(), &metrics) .await diff --git a/common/task_executor/Cargo.toml b/common/task_executor/Cargo.toml index c1ac4b55a91..4224f00acc0 100644 --- a/common/task_executor/Cargo.toml +++ b/common/task_executor/Cargo.toml @@ -4,17 +4,9 @@ version = "0.1.0" authors = ["Sigma Prime "] edition = { workspace = true } -[features] -default = ["slog"] -slog = ["dep:slog", "dep:sloggers", "dep:logging"] -tracing = ["dep:tracing"] - [dependencies] async-channel = { workspace = true } futures = { workspace = true } -logging = { workspace = true, optional = true } metrics = { workspace = true } -slog = { workspace = true, optional = true } -sloggers = { workspace = true, optional = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } -tracing = { workspace = true, optional = true } +tracing = { workspace = true } diff --git a/common/task_executor/src/lib.rs b/common/task_executor/src/lib.rs index 92ddb7c0be2..dbdac600f33 100644 --- a/common/task_executor/src/lib.rs +++ b/common/task_executor/src/lib.rs @@ -1,20 +1,14 @@ mod metrics; -#[cfg(not(feature = "tracing"))] pub mod test_utils; use futures::channel::mpsc::Sender; use futures::prelude::*; use std::sync::Weak; use tokio::runtime::{Handle, Runtime}; +use tracing::{debug, instrument}; pub use tokio::task::JoinHandle; -// Set up logging framework -#[cfg(not(feature = "tracing"))] -use slog::{debug, o}; -#[cfg(feature = "tracing")] -use tracing::debug; - /// Provides a reason when Lighthouse is shut down. #[derive(Copy, Clone, Debug, PartialEq)] pub enum ShutdownReason { @@ -85,8 +79,9 @@ pub struct TaskExecutor { /// /// The task must provide a reason for shutting down. signal_tx: Sender, - #[cfg(not(feature = "tracing"))] - log: slog::Logger, + + /// The name of the service for inclusion in the logger output. + service_name: String, } impl TaskExecutor { @@ -97,39 +92,29 @@ impl TaskExecutor { /// This function should only be used during testing. In production, prefer to obtain an /// instance of `Self` via a `environment::RuntimeContext` (see the `lighthouse/environment` /// crate). + #[instrument(parent = None,level = "info", fields(service = service_name), name = "task_executor", skip_all)] pub fn new>( handle: T, exit: async_channel::Receiver<()>, - #[cfg(not(feature = "tracing"))] log: slog::Logger, signal_tx: Sender, + service_name: String, ) -> Self { Self { handle_provider: handle.into(), exit, signal_tx, - #[cfg(not(feature = "tracing"))] - log, + service_name, } } /// Clones the task executor adding a service name. - #[cfg(not(feature = "tracing"))] + #[instrument(parent = None,level = "info", fields(service = service_name), name = "task_executor", skip_all)] pub fn clone_with_name(&self, service_name: String) -> Self { TaskExecutor { handle_provider: self.handle_provider.clone(), exit: self.exit.clone(), signal_tx: self.signal_tx.clone(), - log: self.log.new(o!("service" => service_name)), - } - } - - /// Clones the task executor adding a service name. - #[cfg(feature = "tracing")] - pub fn clone(&self) -> Self { - TaskExecutor { - handle_provider: self.handle_provider.clone(), - exit: self.exit.clone(), - signal_tx: self.signal_tx.clone(), + service_name, } } @@ -139,6 +124,7 @@ impl TaskExecutor { /// The purpose of this function is to create a compile error if some function which previously /// returned `()` starts returning something else. Such a case may otherwise result in /// accidental error suppression. + #[instrument(parent = None,level = "info", fields(service = self.service_name), name = "task_executor", skip_all)] pub fn spawn_ignoring_error( &self, task: impl Future> + Send + 'static, @@ -150,6 +136,7 @@ impl TaskExecutor { /// Spawn a task to monitor the completion of another task. /// /// If the other task exits by panicking, then the monitor task will shut down the executor. + #[instrument(parent = None,level = "info", fields(service = self.service_name), name = "task_executor", skip_all)] fn spawn_monitor( &self, task_handle: impl Future> + Send + 'static, @@ -168,13 +155,7 @@ impl TaskExecutor { drop(timer); }); } else { - #[cfg(not(feature = "tracing"))] - debug!( - self.log, - "Couldn't spawn monitor task. Runtime shutting down" - ); - #[cfg(feature = "tracing")] - debug!("Couldn't spawn monitor task. Runtime shutting down"); + debug!("Couldn't spawn monitor task. Runtime shutting down") } } @@ -187,6 +168,7 @@ impl TaskExecutor { /// of a panic, the executor will be shut down via `self.signal_tx`. /// /// This function generates prometheus metrics on number of tasks and task duration. + #[instrument(parent = None,level = "info", fields(service = self.service_name), name = "task_executor", skip_all)] pub fn spawn(&self, task: impl Future + Send + 'static, name: &'static str) { if let Some(task_handle) = self.spawn_handle(task, name) { self.spawn_monitor(task_handle, name) @@ -202,6 +184,7 @@ impl TaskExecutor { /// This is useful in cases where the future to be spawned needs to do additional cleanup work when /// the task is completed/canceled (e.g. writing local variables to disk) or the task is created from /// some framework which does its own cleanup (e.g. a hyper server). + #[instrument(parent = None,level = "info", fields(service = self.service_name), name = "task_executor", skip_all)] pub fn spawn_without_exit( &self, task: impl Future + Send + 'static, @@ -218,9 +201,6 @@ impl TaskExecutor { if let Some(handle) = self.handle() { handle.spawn(future); } else { - #[cfg(not(feature = "tracing"))] - debug!(self.log, "Couldn't spawn task. Runtime shutting down"); - #[cfg(feature = "tracing")] debug!("Couldn't spawn task. Runtime shutting down"); } } @@ -242,16 +222,13 @@ impl TaskExecutor { /// The task is cancelled when the corresponding async-channel is dropped. /// /// This function generates prometheus metrics on number of tasks and task duration. + #[instrument(parent = None,level = "info", fields(service = self.service_name), name = "task_executor", skip_all)] pub fn spawn_handle( &self, task: impl Future + Send + 'static, name: &'static str, ) -> Option>> { let exit = self.exit(); - - #[cfg(not(feature = "tracing"))] - let log = self.log.clone(); - if let Some(int_gauge) = metrics::get_int_gauge(&metrics::ASYNC_TASKS_COUNT, &[name]) { // Task is shutdown before it completes if `exit` receives let int_gauge_1 = int_gauge.clone(); @@ -262,9 +239,6 @@ impl TaskExecutor { let result = match future::select(Box::pin(task), exit).await { future::Either::Left((value, _)) => Some(value), future::Either::Right(_) => { - #[cfg(not(feature = "tracing"))] - debug!(log, "Async task shutdown, exit received"; "task" => name); - #[cfg(feature = "tracing")] debug!(task = name, "Async task shutdown, exit received"); None } @@ -273,9 +247,6 @@ impl TaskExecutor { result })) } else { - #[cfg(not(feature = "tracing"))] - debug!(log, "Couldn't spawn task. Runtime shutting down"); - #[cfg(feature = "tracing")] debug!("Couldn't spawn task. Runtime shutting down"); None } @@ -290,6 +261,7 @@ impl TaskExecutor { /// The Future returned behaves like the standard JoinHandle which can return an error if the /// task failed. /// This function generates prometheus metrics on number of tasks and task duration. + #[instrument(parent = None,level = "info", fields(service = self.service_name), name = "task_executor", skip_all)] pub fn spawn_blocking_handle( &self, task: F, @@ -299,18 +271,12 @@ impl TaskExecutor { F: FnOnce() -> R + Send + 'static, R: Send + 'static, { - #[cfg(not(feature = "tracing"))] - let log = self.log.clone(); - let timer = metrics::start_timer_vec(&metrics::BLOCKING_TASKS_HISTOGRAM, &[name]); metrics::inc_gauge_vec(&metrics::BLOCKING_TASKS_COUNT, &[name]); let join_handle = if let Some(handle) = self.handle() { handle.spawn_blocking(task) } else { - #[cfg(not(feature = "tracing"))] - debug!(self.log, "Couldn't spawn task. Runtime shutting down"); - #[cfg(feature = "tracing")] debug!("Couldn't spawn task. Runtime shutting down"); return None; }; @@ -319,9 +285,6 @@ impl TaskExecutor { let result = match join_handle.await { Ok(result) => Ok(result), Err(error) => { - #[cfg(not(feature = "tracing"))] - debug!(log, "Blocking task ended unexpectedly"; "error" => %error); - #[cfg(feature = "tracing")] debug!(%error, "Blocking task ended unexpectedly"); Err(error) } @@ -347,6 +310,7 @@ impl TaskExecutor { /// a `tokio` context present in the thread-local storage due to some `rayon` funkiness. Talk to /// @paulhauner if you plan to use this function in production. He has put metrics in here to /// track any use of it, so don't think you can pull a sneaky one on him. + #[instrument(parent = None,level = "info", fields(service = self.service_name), name = "task_executor", skip_all)] pub fn block_on_dangerous( &self, future: F, @@ -354,44 +318,20 @@ impl TaskExecutor { ) -> Option { let timer = metrics::start_timer_vec(&metrics::BLOCK_ON_TASKS_HISTOGRAM, &[name]); metrics::inc_gauge_vec(&metrics::BLOCK_ON_TASKS_COUNT, &[name]); - #[cfg(not(feature = "tracing"))] - let log = self.log.clone(); let handle = self.handle()?; let exit = self.exit(); - #[cfg(not(feature = "tracing"))] - debug!( - log, - "Starting block_on task"; - "name" => name - ); - - #[cfg(feature = "tracing")] debug!(name, "Starting block_on task"); handle.block_on(async { let output = tokio::select! { output = future => { - #[cfg(not(feature = "tracing"))] - debug!( - log, - "Completed block_on task"; - "name" => name - ); - #[cfg(feature = "tracing")] debug!( name, "Completed block_on task" ); Some(output) - }, + } _ = exit => { - #[cfg(not(feature = "tracing"))] - debug!( - log, - "Cancelled block_on task"; - "name" => name, - ); - #[cfg(feature = "tracing")] debug!( name, "Cancelled block_on task" @@ -406,6 +346,7 @@ impl TaskExecutor { } /// Returns a `Handle` to the current runtime. + #[instrument(parent = None,level = "info", fields(service = self.service_name), name = "task_executor", skip_all)] pub fn handle(&self) -> Option { self.handle_provider.handle() } @@ -420,13 +361,8 @@ impl TaskExecutor { } /// Get a channel to request shutting down. + #[instrument(parent = None,level = "info", fields(service = self.service_name), name = "task_executor", skip_all)] pub fn shutdown_sender(&self) -> Sender { self.signal_tx.clone() } - - /// Returns a reference to the logger. - #[cfg(not(feature = "tracing"))] - pub fn log(&self) -> &slog::Logger { - &self.log - } } diff --git a/common/task_executor/src/test_utils.rs b/common/task_executor/src/test_utils.rs index 46fbff7eacd..698152f6c13 100644 --- a/common/task_executor/src/test_utils.rs +++ b/common/task_executor/src/test_utils.rs @@ -1,6 +1,4 @@ use crate::TaskExecutor; -pub use logging::test_logger; -use slog::Logger; use std::sync::Arc; use tokio::runtime; @@ -16,7 +14,6 @@ pub struct TestRuntime { runtime: Option>, _runtime_shutdown: async_channel::Sender<()>, pub task_executor: TaskExecutor, - pub log: Logger, } impl Default for TestRuntime { @@ -26,7 +23,6 @@ impl Default for TestRuntime { fn default() -> Self { let (runtime_shutdown, exit) = async_channel::bounded(1); let (shutdown_tx, _) = futures::channel::mpsc::channel(1); - let log = test_logger(); let (runtime, handle) = if let Ok(handle) = runtime::Handle::try_current() { (None, handle) @@ -41,13 +37,12 @@ impl Default for TestRuntime { (Some(runtime), handle) }; - let task_executor = TaskExecutor::new(handle, exit, log.clone(), shutdown_tx); + let task_executor = TaskExecutor::new(handle, exit, shutdown_tx, "test".to_string()); Self { runtime, _runtime_shutdown: runtime_shutdown, task_executor, - log, } } } @@ -59,10 +54,3 @@ impl Drop for TestRuntime { } } } - -impl TestRuntime { - pub fn set_logger(&mut self, log: Logger) { - self.log = log.clone(); - self.task_executor.log = log; - } -} diff --git a/common/warp_utils/Cargo.toml b/common/warp_utils/Cargo.toml index ec2d23686b1..32a540a69d8 100644 --- a/common/warp_utils/Cargo.toml +++ b/common/warp_utils/Cargo.toml @@ -9,7 +9,6 @@ edition = { workspace = true } bytes = { workspace = true } eth2 = { workspace = true } headers = "0.3.2" -metrics = { workspace = true } safe_arith = { workspace = true } serde = { workspace = true } serde_array_query = "0.1.0" diff --git a/common/workspace_members/Cargo.toml b/common/workspace_members/Cargo.toml new file mode 100644 index 00000000000..05924590e38 --- /dev/null +++ b/common/workspace_members/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "workspace_members" +version = "0.1.0" +edition = { workspace = true } + +[lib] +proc-macro = true + +[dependencies] +cargo_metadata = { workspace = true } +quote = { workspace = true } diff --git a/common/workspace_members/src/lib.rs b/common/workspace_members/src/lib.rs new file mode 100644 index 00000000000..1eea0e60e28 --- /dev/null +++ b/common/workspace_members/src/lib.rs @@ -0,0 +1,39 @@ +use cargo_metadata::MetadataCommand; +use proc_macro::TokenStream; +use quote::quote; +use std::error::Error; + +fn get_workspace_crates() -> Result, Box> { + let metadata = MetadataCommand::new().no_deps().exec()?; + + Ok(metadata + .workspace_members + .iter() + .filter_map(|member_id| { + metadata + .packages + .iter() + .find(|package| &package.id == member_id) + .map(|package| package.name.clone()) + }) + .collect()) +} + +#[proc_macro] +pub fn workspace_crates(_input: TokenStream) -> TokenStream { + match get_workspace_crates() { + Ok(crate_names) => { + let crate_strs = crate_names.iter().map(|s| s.as_str()); + quote! { + &[#(#crate_strs),*] + } + } + Err(e) => { + let msg = format!("Failed to get workspace crates: {e}"); + quote! { + compile_error!(#msg); + } + } + } + .into() +} diff --git a/consensus/fork_choice/Cargo.toml b/consensus/fork_choice/Cargo.toml index 3bd18e922aa..5c009a5e78f 100644 --- a/consensus/fork_choice/Cargo.toml +++ b/consensus/fork_choice/Cargo.toml @@ -8,10 +8,11 @@ edition = { workspace = true } [dependencies] ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } +logging = { workspace = true } metrics = { workspace = true } proto_array = { workspace = true } -slog = { workspace = true } state_processing = { workspace = true } +tracing = { workspace = true } types = { workspace = true } [dev-dependencies] diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index ddd5923849c..91b44c7af10 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -1,10 +1,10 @@ use crate::metrics::{self, scrape_for_metrics}; use crate::{ForkChoiceStore, InvalidationOperation}; +use logging::crit; use proto_array::{ Block as ProtoBlock, DisallowedReOrgOffsets, ExecutionStatus, ProposerHeadError, ProposerHeadInfo, ProtoArrayForkChoice, ReOrgThreshold, }; -use slog::{crit, debug, warn, Logger}; use ssz_derive::{Decode, Encode}; use state_processing::{ per_block_processing::errors::AttesterSlashingValidationError, per_epoch_processing, @@ -13,6 +13,7 @@ use std::cmp::Ordering; use std::collections::BTreeSet; use std::marker::PhantomData; use std::time::Duration; +use tracing::{debug, warn}; use types::{ consts::bellatrix::INTERVALS_PER_SLOT, AbstractExecPayload, AttestationShufflingId, AttesterSlashingRef, BeaconBlockRef, BeaconState, BeaconStateError, ChainSpec, Checkpoint, @@ -1370,17 +1371,14 @@ where persisted: &PersistedForkChoice, reset_payload_statuses: ResetPayloadStatuses, spec: &ChainSpec, - log: &Logger, ) -> Result> { let mut proto_array = ProtoArrayForkChoice::from_bytes(&persisted.proto_array_bytes) .map_err(Error::InvalidProtoArrayBytes)?; let contains_invalid_payloads = proto_array.contains_invalid_payloads(); debug!( - log, - "Restoring fork choice from persisted"; - "reset_payload_statuses" => ?reset_payload_statuses, - "contains_invalid_payloads" => contains_invalid_payloads, + ?reset_payload_statuses, + contains_invalid_payloads, "Restoring fork choice from persisted" ); // Exit early if there are no "invalid" payloads, if requested. @@ -1399,18 +1397,14 @@ where // back to a proto-array which does not have the reset applied. This indicates a // significant error in Lighthouse and warrants detailed investigation. crit!( - log, - "Failed to reset payload statuses"; - "error" => e, - "info" => "please report this error", + error = ?e, + info = "please report this error", + "Failed to reset payload statuses" ); ProtoArrayForkChoice::from_bytes(&persisted.proto_array_bytes) .map_err(Error::InvalidProtoArrayBytes) } else { - debug!( - log, - "Successfully reset all payload statuses"; - ); + debug!("Successfully reset all payload statuses"); Ok(proto_array) } } @@ -1422,10 +1416,9 @@ where reset_payload_statuses: ResetPayloadStatuses, fc_store: T, spec: &ChainSpec, - log: &Logger, ) -> Result> { let proto_array = - Self::proto_array_from_persisted(&persisted, reset_payload_statuses, spec, log)?; + Self::proto_array_from_persisted(&persisted, reset_payload_statuses, spec)?; let current_slot = fc_store.get_current_slot(); @@ -1449,10 +1442,9 @@ where // an optimistic status so that we can have a head to start from. if let Err(e) = fork_choice.get_head(current_slot, spec) { warn!( - log, - "Could not find head on persisted FC"; - "info" => "resetting all payload statuses and retrying", - "error" => ?e + info = "resetting all payload statuses and retrying", + error = ?e, + "Could not find head on persisted FC" ); // Although we may have already made this call whilst loading `proto_array`, try it // again since we may have mutated the `proto_array` during `get_head` and therefore may diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index cf6ebb3b004..cbae54bd362 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -1041,6 +1041,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 e7e6b54f1d0..dde24117871 100644 --- a/consensus/proto_array/src/proto_array_fork_choice.rs +++ b/consensus/proto_array/src/proto_array_fork_choice.rs @@ -893,6 +893,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/per_block_processing/errors.rs b/consensus/state_processing/src/per_block_processing/errors.rs index fdeec6f08c3..ff7c0204e24 100644 --- a/consensus/state_processing/src/per_block_processing/errors.rs +++ b/consensus/state_processing/src/per_block_processing/errors.rs @@ -60,6 +60,7 @@ pub enum BlockProcessingError { SignatureSetError(SignatureSetError), SszTypesError(ssz_types::Error), SszDecodeError(DecodeError), + BitfieldError(ssz::BitfieldError), MerkleTreeError(MerkleTreeError), ArithError(ArithError), InconsistentBlockFork(InconsistentFork), @@ -153,6 +154,7 @@ impl From> for BlockProcessingError { BlockOperationError::BeaconStateError(e) => BlockProcessingError::BeaconStateError(e), BlockOperationError::SignatureSetError(e) => BlockProcessingError::SignatureSetError(e), BlockOperationError::SszTypesError(e) => BlockProcessingError::SszTypesError(e), + BlockOperationError::BitfieldError(e) => BlockProcessingError::BitfieldError(e), BlockOperationError::ConsensusContext(e) => BlockProcessingError::ConsensusContext(e), BlockOperationError::ArithError(e) => BlockProcessingError::ArithError(e), } @@ -181,6 +183,7 @@ macro_rules! impl_into_block_processing_error_with_index { BlockOperationError::BeaconStateError(e) => BlockProcessingError::BeaconStateError(e), BlockOperationError::SignatureSetError(e) => BlockProcessingError::SignatureSetError(e), BlockOperationError::SszTypesError(e) => BlockProcessingError::SszTypesError(e), + BlockOperationError::BitfieldError(e) => BlockProcessingError::BitfieldError(e), BlockOperationError::ConsensusContext(e) => BlockProcessingError::ConsensusContext(e), BlockOperationError::ArithError(e) => BlockProcessingError::ArithError(e), } @@ -215,6 +218,7 @@ pub enum BlockOperationError { BeaconStateError(BeaconStateError), SignatureSetError(SignatureSetError), SszTypesError(ssz_types::Error), + BitfieldError(ssz::BitfieldError), ConsensusContext(ContextError), ArithError(ArithError), } @@ -242,6 +246,12 @@ impl From for BlockOperationError { } } +impl From for BlockOperationError { + fn from(error: ssz::BitfieldError) -> Self { + BlockOperationError::BitfieldError(error) + } +} + impl From for BlockOperationError { fn from(e: ArithError) -> Self { BlockOperationError::ArithError(e) @@ -367,6 +377,7 @@ impl From> BlockOperationError::BeaconStateError(e) => BlockOperationError::BeaconStateError(e), BlockOperationError::SignatureSetError(e) => BlockOperationError::SignatureSetError(e), BlockOperationError::SszTypesError(e) => BlockOperationError::SszTypesError(e), + BlockOperationError::BitfieldError(e) => BlockOperationError::BitfieldError(e), BlockOperationError::ConsensusContext(e) => BlockOperationError::ConsensusContext(e), BlockOperationError::ArithError(e) => BlockOperationError::ArithError(e), } diff --git a/consensus/state_processing/src/per_epoch_processing/errors.rs b/consensus/state_processing/src/per_epoch_processing/errors.rs index f45c55a7acf..7485e365ec2 100644 --- a/consensus/state_processing/src/per_epoch_processing/errors.rs +++ b/consensus/state_processing/src/per_epoch_processing/errors.rs @@ -19,9 +19,10 @@ pub enum EpochProcessingError { BeaconStateError(BeaconStateError), InclusionError(InclusionError), SszTypesError(ssz_types::Error), + BitfieldError(ssz::BitfieldError), ArithError(safe_arith::ArithError), InconsistentStateFork(InconsistentFork), - InvalidJustificationBit(ssz_types::Error), + InvalidJustificationBit(ssz::BitfieldError), InvalidFlagIndex(usize), MilhouseError(milhouse::Error), EpochCache(EpochCacheError), @@ -49,6 +50,12 @@ impl From for EpochProcessingError { } } +impl From for EpochProcessingError { + fn from(e: ssz::BitfieldError) -> EpochProcessingError { + EpochProcessingError::BitfieldError(e) + } +} + impl From for EpochProcessingError { fn from(e: safe_arith::ArithError) -> EpochProcessingError { EpochProcessingError::ArithError(e) diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index 79beb812820..013230f1581 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -28,7 +28,6 @@ hex = { workspace = true } int_to_bytes = { workspace = true } itertools = { workspace = true } kzg = { workspace = true } -log = { workspace = true } maplit = { workspace = true } merkle_proof = { workspace = true } metastruct = "0.1.0" @@ -39,18 +38,18 @@ rand_xorshift = "0.3.0" rayon = { workspace = true } regex = { workspace = true } rpds = { workspace = true } -rusqlite = { workspace = true } +rusqlite = { workspace = true, optional = true } safe_arith = { workspace = true } serde = { workspace = true, features = ["rc"] } serde_json = { workspace = true } serde_yaml = { workspace = true } -slog = { workspace = true } smallvec = { workspace = true } ssz_types = { workspace = true, features = ["arbitrary"] } superstruct = { workspace = true } swap_or_not_shuffle = { workspace = true, features = ["arbitrary"] } tempfile = { workspace = true } test_random_derive = { path = "../../common/test_random_derive" } +tracing = { workspace = true } tree_hash = { workspace = true } tree_hash_derive = { workspace = true } @@ -65,7 +64,7 @@ tokio = { workspace = true } default = ["sqlite", "legacy-arith"] # Allow saturating arithmetic on slots and epochs. Enabled by default, but deprecated. legacy-arith = [] -sqlite = [] +sqlite = ["dep:rusqlite"] # The `arbitrary-fuzz` feature is a no-op provided for backwards compatibility. # For simplicity `Arbitrary` is now derived regardless of the feature's presence. arbitrary-fuzz = [] diff --git a/consensus/types/src/attestation.rs b/consensus/types/src/attestation.rs index 08953770637..e2973132b04 100644 --- a/consensus/types/src/attestation.rs +++ b/consensus/types/src/attestation.rs @@ -16,9 +16,10 @@ use super::{ Signature, SignedRoot, }; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub enum Error { SszTypesError(ssz_types::Error), + BitfieldError(ssz::BitfieldError), AlreadySigned(usize), IncorrectStateVariant, InvalidCommitteeLength, @@ -231,7 +232,7 @@ impl Attestation { } } - pub fn get_aggregation_bit(&self, index: usize) -> Result { + pub fn get_aggregation_bit(&self, index: usize) -> Result { match self { Attestation::Base(att) => att.aggregation_bits.get(index), Attestation::Electra(att) => att.aggregation_bits.get(index), @@ -365,13 +366,13 @@ impl AttestationElectra { if self .aggregation_bits .get(committee_position) - .map_err(Error::SszTypesError)? + .map_err(Error::BitfieldError)? { Err(Error::AlreadySigned(committee_position)) } else { self.aggregation_bits .set(committee_position, true) - .map_err(Error::SszTypesError)?; + .map_err(Error::BitfieldError)?; self.signature.add_assign(signature); @@ -439,13 +440,13 @@ impl AttestationBase { if self .aggregation_bits .get(committee_position) - .map_err(Error::SszTypesError)? + .map_err(Error::BitfieldError)? { Err(Error::AlreadySigned(committee_position)) } else { self.aggregation_bits .set(committee_position, true) - .map_err(Error::SszTypesError)?; + .map_err(Error::BitfieldError)?; self.signature.add_assign(signature); @@ -455,7 +456,7 @@ impl AttestationBase { pub fn extend_aggregation_bits( &self, - ) -> Result, ssz_types::Error> { + ) -> Result, ssz::BitfieldError> { self.aggregation_bits.resize::() } } @@ -612,12 +613,12 @@ mod tests { let attestation_data = size_of::(); let signature = size_of::(); - assert_eq!(aggregation_bits, 56); + assert_eq!(aggregation_bits, 152); assert_eq!(attestation_data, 128); assert_eq!(signature, 288 + 16); let attestation_expected = aggregation_bits + attestation_data + signature; - assert_eq!(attestation_expected, 488); + assert_eq!(attestation_expected, 584); assert_eq!( size_of::>(), attestation_expected @@ -635,13 +636,13 @@ mod tests { size_of::::MaxCommitteesPerSlot>>(); let signature = size_of::(); - assert_eq!(aggregation_bits, 56); - assert_eq!(committee_bits, 56); + assert_eq!(aggregation_bits, 152); + assert_eq!(committee_bits, 152); assert_eq!(attestation_data, 128); assert_eq!(signature, 288 + 16); let attestation_expected = aggregation_bits + committee_bits + attestation_data + signature; - assert_eq!(attestation_expected, 544); + assert_eq!(attestation_expected, 736); assert_eq!( size_of::>(), attestation_expected diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index a9908e87f36..2b29ef1f108 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -240,6 +240,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 * @@ -655,7 +660,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 @@ -711,6 +718,10 @@ impl ChainSpec { } } + pub fn all_data_column_sidecar_subnets(&self) -> impl Iterator { + (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 @@ -988,6 +999,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 */ @@ -1317,6 +1333,11 @@ impl ChainSpec { 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 */ @@ -1536,6 +1557,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] { @@ -1673,6 +1697,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 } @@ -1900,6 +1928,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, } } @@ -1978,6 +2007,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() { @@ -2060,6 +2090,7 @@ impl Config { data_column_sidecar_subnet_count, samples_per_slot, custody_requirement, + max_blobs_per_block_fulu, ..chain_spec.clone() }) diff --git a/consensus/types/src/data_column_custody_group.rs b/consensus/types/src/data_column_custody_group.rs index bb204c34a2d..9e9505da9f1 100644 --- a/consensus/types/src/data_column_custody_group.rs +++ b/consensus/types/src/data_column_custody_group.rs @@ -17,6 +17,8 @@ pub enum DataColumnCustodyGroupError { /// The `get_custody_groups` function is used to determine the custody groups that a node is /// assigned to. /// +/// Note: `get_custody_groups(node_id, x)` is a subset of `get_custody_groups(node_id, y)` if `x < y`. +/// /// spec: https://github.com/ethereum/consensus-specs/blob/8e0d0d48e81d6c7c5a8253ab61340f5ea5bac66a/specs/fulu/das-core.md#get_custody_groups pub fn get_custody_groups( raw_node_id: [u8; 32], diff --git a/consensus/types/src/data_column_sidecar.rs b/consensus/types/src/data_column_sidecar.rs index 90a914dfae3..03ab6a74f83 100644 --- a/consensus/types/src/data_column_sidecar.rs +++ b/consensus/types/src/data_column_sidecar.rs @@ -1,7 +1,7 @@ use crate::beacon_block_body::{KzgCommitments, BLOB_KZG_COMMITMENTS_INDEX}; use crate::test_utils::TestRandom; use crate::BeaconStateError; -use crate::{BeaconBlockHeader, Epoch, EthSpec, Hash256, KzgProofs, SignedBeaconBlockHeader, Slot}; +use crate::{BeaconBlockHeader, Epoch, EthSpec, Hash256, SignedBeaconBlockHeader, Slot}; use bls::Signature; use derivative::Derivative; use kzg::Error as KzgError; @@ -56,7 +56,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, diff --git a/consensus/types/src/eth_spec.rs b/consensus/types/src/eth_spec.rs index 0bc074072f6..6f1b3e6ce6c 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 */ @@ -421,6 +426,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 @@ -474,6 +480,7 @@ impl EthSpec for MinimalEthSpec { type MaxWithdrawalRequestsPerPayload = U2; type FieldElementsPerCell = U64; type FieldElementsPerExtBlob = U8192; + type MaxCellsPerBlock = U33554432; type BytesPerCell = U2048; type KzgCommitmentsInclusionProofDepth = U4; @@ -566,6 +573,7 @@ impl EthSpec for GnosisEthSpec { type MaxPendingDepositsPerEpoch = U16; type FieldElementsPerCell = U64; type FieldElementsPerExtBlob = U8192; + type MaxCellsPerBlock = U33554432; type BytesPerCell = U2048; type KzgCommitmentsInclusionProofDepth = U4; diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index 73a50b4ef3e..1d39c89cab5 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -272,7 +272,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; diff --git a/consensus/types/src/payload.rs b/consensus/types/src/payload.rs index abc9afd34c8..c0262a2cf84 100644 --- a/consensus/types/src/payload.rs +++ b/consensus/types/src/payload.rs @@ -85,6 +85,7 @@ pub trait AbstractExecPayload: + TryInto + TryInto + TryInto + + Sync { type Ref<'a>: ExecPayload + Copy @@ -97,23 +98,28 @@ 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 Fulu: OwnedExecPayload + Into + for<'a> From>> - + TryFrom>; + + TryFrom> + + Sync; } #[superstruct( diff --git a/consensus/types/src/slot_epoch_macros.rs b/consensus/types/src/slot_epoch_macros.rs index 42e7a0f2ee2..eee267355ad 100644 --- a/consensus/types/src/slot_epoch_macros.rs +++ b/consensus/types/src/slot_epoch_macros.rs @@ -227,17 +227,6 @@ macro_rules! impl_display { write!(f, "{}", self.0) } } - - impl slog::Value for $type { - fn serialize( - &self, - record: &slog::Record, - key: slog::Key, - serializer: &mut dyn slog::Serializer, - ) -> slog::Result { - slog::Value::serialize(&self.0, record, key, serializer) - } - } }; } diff --git a/consensus/types/src/sync_aggregate.rs b/consensus/types/src/sync_aggregate.rs index 43f72a39240..12b91501ae0 100644 --- a/consensus/types/src/sync_aggregate.rs +++ b/consensus/types/src/sync_aggregate.rs @@ -11,6 +11,7 @@ use tree_hash_derive::TreeHash; #[derive(Debug, PartialEq)] pub enum Error { SszTypesError(ssz_types::Error), + BitfieldError(ssz::BitfieldError), ArithError(ArithError), } @@ -68,7 +69,7 @@ impl SyncAggregate { sync_aggregate .sync_committee_bits .set(participant_index, true) - .map_err(Error::SszTypesError)?; + .map_err(Error::BitfieldError)?; } } sync_aggregate diff --git a/consensus/types/src/sync_committee_contribution.rs b/consensus/types/src/sync_committee_contribution.rs index 090e16fc6d4..e160332f455 100644 --- a/consensus/types/src/sync_committee_contribution.rs +++ b/consensus/types/src/sync_committee_contribution.rs @@ -9,6 +9,7 @@ use tree_hash_derive::TreeHash; #[derive(Debug, PartialEq)] pub enum Error { SszTypesError(ssz_types::Error), + BitfieldError(ssz::BitfieldError), AlreadySigned(usize), } @@ -51,7 +52,7 @@ impl SyncCommitteeContribution { ) -> Result { let mut bits = BitVector::new(); bits.set(validator_sync_committee_index, true) - .map_err(Error::SszTypesError)?; + .map_err(Error::BitfieldError)?; Ok(Self { slot: message.slot, beacon_block_root: message.beacon_block_root, diff --git a/consensus/types/src/test_utils/generate_deterministic_keypairs.rs b/consensus/types/src/test_utils/generate_deterministic_keypairs.rs index 92534369eee..f30afda257e 100644 --- a/consensus/types/src/test_utils/generate_deterministic_keypairs.rs +++ b/consensus/types/src/test_utils/generate_deterministic_keypairs.rs @@ -1,8 +1,8 @@ use crate::*; use eth2_interop_keypairs::{keypair, keypairs_from_yaml_file}; -use log::debug; use rayon::prelude::*; use std::path::PathBuf; +use tracing::debug; /// Generates `validator_count` keypairs where the secret key is derived solely from the index of /// the validator. diff --git a/consensus/types/src/validator_registration_data.rs b/consensus/types/src/validator_registration_data.rs index cdafd355e7c..345771074c5 100644 --- a/consensus/types/src/validator_registration_data.rs +++ b/consensus/types/src/validator_registration_data.rs @@ -4,7 +4,7 @@ use ssz_derive::{Decode, Encode}; use tree_hash_derive::TreeHash; /// Validator registration, for use in interacting with servers implementing the builder API. -#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)] +#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Encode, Decode)] pub struct SignedValidatorRegistrationData { pub message: ValidatorRegistrationData, pub signature: Signature, diff --git a/crypto/bls/src/generic_secret_key.rs b/crypto/bls/src/generic_secret_key.rs index a0a43311107..62bfc1467db 100644 --- a/crypto/bls/src/generic_secret_key.rs +++ b/crypto/bls/src/generic_secret_key.rs @@ -61,6 +61,11 @@ where GenericPublicKey::from_point(self.point.public_key()) } + /// Returns a reference to the underlying BLS point. + pub fn point(&self) -> &Sec { + &self.point + } + /// Serialize `self` as compressed bytes. /// /// ## Note @@ -89,3 +94,20 @@ where } } } + +impl GenericSecretKey +where + Sig: TSignature, + Pub: TPublicKey, + Sec: TSecretKey + Clone, +{ + /// Instantiates `Self` from a `point`. + /// Takes a reference, as moves might accidentally leave behind key material + pub fn from_point(point: &Sec) -> Self { + Self { + point: point.clone(), + _phantom_signature: PhantomData, + _phantom_public_key: PhantomData, + } + } +} diff --git a/crypto/bls/src/generic_signature.rs b/crypto/bls/src/generic_signature.rs index 05e0a222bd5..0b375d3edd5 100644 --- a/crypto/bls/src/generic_signature.rs +++ b/crypto/bls/src/generic_signature.rs @@ -14,6 +14,9 @@ use tree_hash::TreeHash; /// The byte-length of a BLS signature when serialized in compressed form. pub const SIGNATURE_BYTES_LEN: usize = 96; +/// The byte-length of a BLS signature when serialized in uncompressed form. +pub const SIGNATURE_UNCOMPRESSED_BYTES_LEN: usize = 192; + /// Represents the signature at infinity. pub const INFINITY_SIGNATURE: [u8; SIGNATURE_BYTES_LEN] = [ 0xc0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -22,6 +25,16 @@ pub const INFINITY_SIGNATURE: [u8; SIGNATURE_BYTES_LEN] = [ 0, ]; +pub const INFINITY_SIGNATURE_UNCOMPRESSED: [u8; SIGNATURE_UNCOMPRESSED_BYTES_LEN] = [ + 0x40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, +]; + /// The compressed bytes used to represent `GenericSignature::empty()`. pub const NONE_SIGNATURE: [u8; SIGNATURE_BYTES_LEN] = [0; SIGNATURE_BYTES_LEN]; @@ -31,9 +44,15 @@ pub trait TSignature: Sized + Clone { /// Serialize `self` as compressed bytes. fn serialize(&self) -> [u8; SIGNATURE_BYTES_LEN]; + /// Serialize `self` as uncompressed bytes. + fn serialize_uncompressed(&self) -> [u8; SIGNATURE_UNCOMPRESSED_BYTES_LEN]; + /// Deserialize `self` from compressed bytes. fn deserialize(bytes: &[u8]) -> Result; + /// Serialize `self` from uncompressed bytes. + fn deserialize_uncompressed(bytes: &[u8]) -> Result; + /// Returns `true` if `self` is a signature across `msg` by `pubkey`. fn verify(&self, pubkey: &GenericPublicKey, msg: Hash256) -> bool; } @@ -93,12 +112,12 @@ where } /// Returns a reference to the underlying BLS point. - pub(crate) fn point(&self) -> Option<&Sig> { + pub fn point(&self) -> Option<&Sig> { self.point.as_ref() } /// Instantiates `Self` from a `point`. - pub(crate) fn from_point(point: Sig, is_infinity: bool) -> Self { + pub fn from_point(point: Sig, is_infinity: bool) -> Self { Self { point: Some(point), is_infinity, @@ -115,6 +134,13 @@ where } } + /// Serialize `self` as compressed bytes. + pub fn serialize_uncompressed(&self) -> Option<[u8; SIGNATURE_UNCOMPRESSED_BYTES_LEN]> { + self.point + .as_ref() + .map(|point| point.serialize_uncompressed()) + } + /// Deserialize `self` from compressed bytes. pub fn deserialize(bytes: &[u8]) -> Result { let point = if bytes == &NONE_SIGNATURE[..] { @@ -129,6 +155,17 @@ where _phantom: PhantomData, }) } + + /// Deserialize `self` from uncompressed bytes. + pub fn deserialize_uncompressed(bytes: &[u8]) -> Result { + // The "none signature" is a beacon chain concept. As we never directly deal with + // uncompressed signatures on the beacon chain, it does not apply here. + Ok(Self { + point: Some(Sig::deserialize_uncompressed(bytes)?), + is_infinity: bytes == &INFINITY_SIGNATURE_UNCOMPRESSED[..], + _phantom: PhantomData, + }) + } } impl GenericSignature diff --git a/crypto/bls/src/impls/blst.rs b/crypto/bls/src/impls/blst.rs index baa704e05a9..6ca0fe09b2d 100644 --- a/crypto/bls/src/impls/blst.rs +++ b/crypto/bls/src/impls/blst.rs @@ -5,7 +5,7 @@ use crate::{ GenericPublicKey, TPublicKey, PUBLIC_KEY_BYTES_LEN, PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN, }, generic_secret_key::TSecretKey, - generic_signature::{TSignature, SIGNATURE_BYTES_LEN}, + generic_signature::{TSignature, SIGNATURE_BYTES_LEN, SIGNATURE_UNCOMPRESSED_BYTES_LEN}, BlstError, Error, Hash256, ZeroizeHash, INFINITY_SIGNATURE, }; pub use blst::min_pk as blst_core; @@ -189,10 +189,18 @@ impl TSignature for blst_core::Signature { self.to_bytes() } + fn serialize_uncompressed(&self) -> [u8; SIGNATURE_UNCOMPRESSED_BYTES_LEN] { + self.serialize() + } + fn deserialize(bytes: &[u8]) -> Result { Self::from_bytes(bytes).map_err(Into::into) } + fn deserialize_uncompressed(bytes: &[u8]) -> Result { + Self::deserialize(bytes).map_err(Into::into) + } + fn verify(&self, pubkey: &blst_core::PublicKey, msg: Hash256) -> bool { // Public keys have already been checked for subgroup and infinity // Check Signature inside function for subgroup diff --git a/crypto/bls/src/impls/fake_crypto.rs b/crypto/bls/src/impls/fake_crypto.rs index a09fb347e6b..7273697597b 100644 --- a/crypto/bls/src/impls/fake_crypto.rs +++ b/crypto/bls/src/impls/fake_crypto.rs @@ -5,7 +5,7 @@ use crate::{ GenericPublicKey, TPublicKey, PUBLIC_KEY_BYTES_LEN, PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN, }, generic_secret_key::{TSecretKey, SECRET_KEY_BYTES_LEN}, - generic_signature::{TSignature, SIGNATURE_BYTES_LEN}, + generic_signature::{TSignature, SIGNATURE_BYTES_LEN, SIGNATURE_UNCOMPRESSED_BYTES_LEN}, Error, Hash256, ZeroizeHash, INFINITY_PUBLIC_KEY, INFINITY_SIGNATURE, }; @@ -106,12 +106,22 @@ impl TSignature for Signature { self.0 } + fn serialize_uncompressed(&self) -> [u8; SIGNATURE_UNCOMPRESSED_BYTES_LEN] { + let mut ret = [0; SIGNATURE_UNCOMPRESSED_BYTES_LEN]; + ret[0..SIGNATURE_BYTES_LEN].copy_from_slice(&self.0); + ret + } + fn deserialize(bytes: &[u8]) -> Result { let mut signature = Self::infinity(); signature.0[..].copy_from_slice(&bytes[0..SIGNATURE_BYTES_LEN]); Ok(signature) } + fn deserialize_uncompressed(bytes: &[u8]) -> Result { + Self::deserialize(bytes) + } + fn verify(&self, _pubkey: &PublicKey, _msg: Hash256) -> bool { true } diff --git a/crypto/bls/src/lib.rs b/crypto/bls/src/lib.rs index d05b34f9891..ac2d83b2041 100644 --- a/crypto/bls/src/lib.rs +++ b/crypto/bls/src/lib.rs @@ -37,7 +37,10 @@ pub use generic_public_key::{ INFINITY_PUBLIC_KEY, PUBLIC_KEY_BYTES_LEN, PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN, }; pub use generic_secret_key::SECRET_KEY_BYTES_LEN; -pub use generic_signature::{INFINITY_SIGNATURE, SIGNATURE_BYTES_LEN}; +pub use generic_signature::{ + INFINITY_SIGNATURE, INFINITY_SIGNATURE_UNCOMPRESSED, SIGNATURE_BYTES_LEN, + SIGNATURE_UNCOMPRESSED_BYTES_LEN, +}; pub use get_withdrawal_credentials::get_withdrawal_credentials; pub use zeroize_hash::ZeroizeHash; diff --git a/crypto/bls/tests/tests.rs b/crypto/bls/tests/tests.rs index 26215771b5f..611dabbd648 100644 --- a/crypto/bls/tests/tests.rs +++ b/crypto/bls/tests/tests.rs @@ -1,4 +1,7 @@ -use bls::{FixedBytesExtended, Hash256, INFINITY_SIGNATURE, SECRET_KEY_BYTES_LEN}; +use bls::{ + FixedBytesExtended, Hash256, INFINITY_SIGNATURE, INFINITY_SIGNATURE_UNCOMPRESSED, + SECRET_KEY_BYTES_LEN, +}; use ssz::{Decode, Encode}; use std::borrow::Cow; use std::fmt::Debug; @@ -37,6 +40,18 @@ macro_rules! test_suite { assert!(AggregateSignature::infinity().is_infinity()); } + #[test] + fn infinity_sig_serializations_match() { + let sig = Signature::deserialize(&INFINITY_SIGNATURE).unwrap(); + assert_eq!( + sig.serialize_uncompressed().unwrap(), + INFINITY_SIGNATURE_UNCOMPRESSED + ); + let sig = + Signature::deserialize_uncompressed(&INFINITY_SIGNATURE_UNCOMPRESSED).unwrap(); + assert_eq!(sig.serialize(), INFINITY_SIGNATURE); + } + #[test] fn ssz_round_trip_multiple_types() { let mut agg_sig = AggregateSignature::infinity(); diff --git a/crypto/kzg/src/lib.rs b/crypto/kzg/src/lib.rs index 2a5c6e47f59..5d752cc0a57 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/database_manager/Cargo.toml b/database_manager/Cargo.toml index a7a54b1416c..99bef75a72c 100644 --- a/database_manager/Cargo.toml +++ b/database_manager/Cargo.toml @@ -11,7 +11,7 @@ clap_utils = { workspace = true } environment = { workspace = true } hex = { workspace = true } serde = { workspace = true } -slog = { workspace = true } store = { workspace = true } strum = { workspace = true } +tracing = { workspace = true } types = { workspace = true } diff --git a/database_manager/src/lib.rs b/database_manager/src/lib.rs index bed90df9df0..f38c28d8b02 100644 --- a/database_manager/src/lib.rs +++ b/database_manager/src/lib.rs @@ -12,7 +12,6 @@ use clap::ValueEnum; use cli::{Compact, Inspect}; use environment::{Environment, RuntimeContext}; use serde::{Deserialize, Serialize}; -use slog::{info, warn, Logger}; use std::fs; use std::io::Write; use std::path::PathBuf; @@ -24,6 +23,7 @@ use store::{ DBColumn, HotColdDB, }; use strum::{EnumString, EnumVariantNames}; +use tracing::{info, warn}; use types::{BeaconState, EthSpec, Slot}; fn parse_client_config( @@ -49,7 +49,6 @@ fn parse_client_config( pub fn display_db_version( client_config: ClientConfig, runtime_context: &RuntimeContext, - log: Logger, ) -> Result<(), Error> { let spec = runtime_context.eth2_config.spec.clone(); let hot_path = client_config.get_db_path(); @@ -67,16 +66,14 @@ pub fn display_db_version( }, client_config.store, spec, - log.clone(), )?; - info!(log, "Database version: {}", version.as_u64()); + info!(version = version.as_u64(), "Database"); if version != CURRENT_SCHEMA_VERSION { info!( - log, - "Latest schema version: {}", - CURRENT_SCHEMA_VERSION.as_u64(), + current_schema_version = CURRENT_SCHEMA_VERSION.as_u64(), + "Latest schema" ); } @@ -260,7 +257,6 @@ fn parse_compact_config(compact_config: &Compact) -> Result( compact_config: CompactConfig, client_config: ClientConfig, - log: Logger, ) -> Result<(), Error> { let hot_path = client_config.get_db_path(); let cold_path = client_config.get_freezer_db_path(); @@ -284,10 +280,9 @@ pub fn compact_db( ) }; info!( - log, - "Compacting database"; - "db" => db_name, - "column" => ?column + db = db_name, + column = ?column, + "Compacting database" ); sub_db.compact_column(column)?; Ok(()) @@ -308,7 +303,6 @@ pub fn migrate_db( client_config: ClientConfig, mut genesis_state: BeaconState, runtime_context: &RuntimeContext, - log: Logger, ) -> Result<(), Error> { let spec = runtime_context.eth2_config.spec.clone(); let hot_path = client_config.get_db_path(); @@ -327,14 +321,12 @@ pub fn migrate_db( }, client_config.store.clone(), spec.clone(), - log.clone(), )?; info!( - log, - "Migrating database schema"; - "from" => from.as_u64(), - "to" => to.as_u64(), + from = from.as_u64(), + to = to.as_u64(), + "Migrating database schema" ); let genesis_state_root = genesis_state.canonical_root()?; @@ -343,14 +335,12 @@ pub fn migrate_db( Some(genesis_state_root), from, to, - log, ) } pub fn prune_payloads( client_config: ClientConfig, runtime_context: &RuntimeContext, - log: Logger, ) -> Result<(), Error> { let spec = &runtime_context.eth2_config.spec; let hot_path = client_config.get_db_path(); @@ -364,7 +354,6 @@ pub fn prune_payloads( |_, _, _| Ok(()), client_config.store, spec.clone(), - log, )?; // If we're trigging a prune manually then ignore the check on the split's parent that bails @@ -376,7 +365,6 @@ pub fn prune_payloads( pub fn prune_blobs( client_config: ClientConfig, runtime_context: &RuntimeContext, - log: Logger, ) -> Result<(), Error> { let spec = &runtime_context.eth2_config.spec; let hot_path = client_config.get_db_path(); @@ -390,7 +378,6 @@ pub fn prune_blobs( |_, _, _| Ok(()), client_config.store, spec.clone(), - log, )?; // If we're triggering a prune manually then ignore the check on `epochs_per_blob_prune` that @@ -413,7 +400,6 @@ pub fn prune_states( prune_config: PruneStatesConfig, mut genesis_state: BeaconState, runtime_context: &RuntimeContext, - log: Logger, ) -> Result<(), String> { let spec = &runtime_context.eth2_config.spec; let hot_path = client_config.get_db_path(); @@ -427,7 +413,6 @@ pub fn prune_states( |_, _, _| Ok(()), client_config.store, spec.clone(), - log.clone(), ) .map_err(|e| format!("Unable to open database: {e:?}"))?; @@ -447,20 +432,14 @@ pub fn prune_states( // Check that the user has confirmed they want to proceed. if !prune_config.confirm { if db.get_anchor_info().full_state_pruning_enabled() { - info!(log, "States have already been pruned"); + info!("States have already been pruned"); return Ok(()); } - info!(log, "Ready to prune states"); - warn!( - log, - "Pruning states is irreversible"; - ); - warn!( - log, - "Re-run this command with --confirm to commit to state deletion" - ); - info!(log, "Nothing has been pruned on this run"); + info!("Ready to prune states"); + warn!("Pruning states is irreversible"); + warn!("Re-run this command with --confirm to commit to state deletion"); + info!("Nothing has been pruned on this run"); return Err("Error: confirmation flag required".into()); } @@ -471,7 +450,7 @@ pub fn prune_states( db.prune_historic_states(genesis_state_root, &genesis_state) .map_err(|e| format!("Failed to prune due to error: {e:?}"))?; - info!(log, "Historic states pruned successfully"); + info!("Historic states pruned successfully"); Ok(()) } @@ -483,7 +462,6 @@ pub fn run( ) -> Result<(), String> { let client_config = parse_client_config(cli_args, db_manager_config, &env)?; let context = env.core_context(); - let log = context.log().clone(); let format_err = |e| format!("Fatal error: {:?}", e); let get_genesis_state = || { @@ -498,7 +476,6 @@ pub fn run( network_config.genesis_state::( client_config.genesis_state_url.as_deref(), client_config.genesis_state_url_timeout, - &log, ), "get_genesis_state", ) @@ -511,30 +488,29 @@ pub fn run( cli::DatabaseManagerSubcommand::Migrate(migrate_config) => { let migrate_config = parse_migrate_config(migrate_config)?; let genesis_state = get_genesis_state()?; - migrate_db(migrate_config, client_config, genesis_state, &context, log) - .map_err(format_err) + migrate_db(migrate_config, client_config, genesis_state, &context).map_err(format_err) } cli::DatabaseManagerSubcommand::Inspect(inspect_config) => { let inspect_config = parse_inspect_config(inspect_config)?; inspect_db::(inspect_config, client_config) } cli::DatabaseManagerSubcommand::Version(_) => { - display_db_version(client_config, &context, log).map_err(format_err) + display_db_version(client_config, &context).map_err(format_err) } cli::DatabaseManagerSubcommand::PrunePayloads(_) => { - prune_payloads(client_config, &context, log).map_err(format_err) + prune_payloads(client_config, &context).map_err(format_err) } cli::DatabaseManagerSubcommand::PruneBlobs(_) => { - prune_blobs(client_config, &context, log).map_err(format_err) + prune_blobs(client_config, &context).map_err(format_err) } cli::DatabaseManagerSubcommand::PruneStates(prune_states_config) => { let prune_config = parse_prune_states_config(prune_states_config)?; let genesis_state = get_genesis_state()?; - prune_states(client_config, prune_config, genesis_state, &context, log) + prune_states(client_config, prune_config, genesis_state, &context) } cli::DatabaseManagerSubcommand::Compact(compact_config) => { let compact_config = parse_compact_config(compact_config)?; - compact_db::(compact_config, client_config, log).map_err(format_err) + compact_db::(compact_config, client_config).map_err(format_err) } } } diff --git a/lcli/Cargo.toml b/lcli/Cargo.toml index 639f2130a47..9acbe2569c8 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" +version = "7.1.0-beta.0" authors = ["Paul Hauner "] edition = { workspace = true } @@ -34,10 +34,11 @@ rayon = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde_yaml = { workspace = true } -sloggers = { workspace = true } snap = { workspace = true } state_processing = { workspace = true } store = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } tree_hash = { workspace = true } types = { workspace = true } validator_dir = { workspace = true } diff --git a/lcli/src/block_root.rs b/lcli/src/block_root.rs index a90a4843d8a..80087fd6d4a 100644 --- a/lcli/src/block_root.rs +++ b/lcli/src/block_root.rs @@ -32,9 +32,9 @@ use clap_utils::{parse_optional, parse_required}; use environment::Environment; use eth2::{types::BlockId, BeaconNodeHttpClient, SensitiveUrl, Timeouts}; use eth2_network_config::Eth2NetworkConfig; -use log::info; use std::path::PathBuf; use std::time::{Duration, Instant}; +use tracing::info; use types::{EthSpec, FullPayload, SignedBeaconBlock}; const HTTP_TIMEOUT: Duration = Duration::from_secs(5); @@ -102,7 +102,7 @@ pub fn run( } if let Some(block_root) = block_root { - info!("Block root is {:?}", block_root); + info!(%block_root,"Block root"); } Ok(()) diff --git a/lcli/src/main.rs b/lcli/src/main.rs index f055a23b362..05f4900c468 100644 --- a/lcli/src/main.rs +++ b/lcli/src/main.rs @@ -18,6 +18,7 @@ use parse_ssz::run_parse_ssz; use std::path::PathBuf; use std::process; use std::str::FromStr; +use tracing_subscriber::filter::LevelFilter; use types::{EthSpec, EthSpecId}; fn main() { @@ -552,6 +553,15 @@ fn main() { until Prague is triggered on mainnet.") .display_order(0) ) + .arg( + Arg::new("osaka-time") + .long("osaka-time") + .value_name("UNIX_TIMESTAMP") + .action(ArgAction::Set) + .help("The payload timestamp that enables Osaka. No default is provided \ + until Osaka is triggered on mainnet.") + .display_order(0) + ) ) .subcommand( Command::new("http-sync") @@ -643,24 +653,31 @@ fn main() { } fn run(env_builder: EnvironmentBuilder, matches: &ArgMatches) -> Result<(), String> { + let (env_builder, _file_logging_layer, _stdout_logging_layer, _sse_logging_layer_opt) = + env_builder + .multi_threaded_tokio_runtime() + .map_err(|e| format!("should start tokio runtime: {:?}", e))? + .init_tracing( + LoggerConfig { + path: None, + debug_level: LevelFilter::TRACE, + logfile_debug_level: LevelFilter::TRACE, + log_format: None, + logfile_format: None, + log_color: true, + logfile_color: false, + disable_log_timestamp: false, + max_log_size: 0, + max_log_number: 0, + compression: false, + is_restricted: true, + sse_logging: false, // No SSE Logging in LCLI + extra_info: false, + }, + "", + ); + let env = env_builder - .multi_threaded_tokio_runtime() - .map_err(|e| format!("should start tokio runtime: {:?}", e))? - .initialize_logger(LoggerConfig { - path: None, - debug_level: String::from("trace"), - logfile_debug_level: String::from("trace"), - log_format: None, - logfile_format: None, - log_color: false, - disable_log_timestamp: false, - max_log_size: 0, - max_log_number: 0, - compression: false, - is_restricted: true, - sse_logging: false, // No SSE Logging in LCLI - }) - .map_err(|e| format!("should start logger: {:?}", e))? .build() .map_err(|e| format!("should build env: {:?}", e))?; diff --git a/lcli/src/parse_ssz.rs b/lcli/src/parse_ssz.rs index dd13f6847b4..f1e5c5759ae 100644 --- a/lcli/src/parse_ssz.rs +++ b/lcli/src/parse_ssz.rs @@ -1,7 +1,6 @@ use clap::ArgMatches; use clap_utils::parse_required; use eth2_network_config::Eth2NetworkConfig; -use log::info; use serde::Serialize; use snap::raw::Decoder; use ssz::Decode; @@ -9,6 +8,7 @@ use std::fs; use std::fs::File; use std::io::Read; use std::str::FromStr; +use tracing::info; use types::*; enum OutputFormat { @@ -59,7 +59,7 @@ pub fn run_parse_ssz( spec.config_name.as_deref().unwrap_or("unknown"), E::spec_name() ); - info!("Type: {type_str}"); + info!(%type_str, "Type"); // More fork-specific decoders may need to be added in future, but shouldn't be 100% necessary, // as the fork-generic decoder will always be available (requires correct --network flag). diff --git a/lcli/src/skip_slots.rs b/lcli/src/skip_slots.rs index 2ad79051ea4..834123e9391 100644 --- a/lcli/src/skip_slots.rs +++ b/lcli/src/skip_slots.rs @@ -50,7 +50,6 @@ use clap_utils::{parse_optional, parse_required}; use environment::Environment; use eth2::{types::StateId, BeaconNodeHttpClient, SensitiveUrl, Timeouts}; use eth2_network_config::Eth2NetworkConfig; -use log::info; use ssz::Encode; use state_processing::state_advance::{complete_state_advance, partial_state_advance}; use state_processing::AllCaches; @@ -58,6 +57,7 @@ use std::fs::File; use std::io::prelude::*; use std::path::PathBuf; use std::time::{Duration, Instant}; +use tracing::info; use types::{BeaconState, EthSpec, Hash256}; const HTTP_TIMEOUT: Duration = Duration::from_secs(10); diff --git a/lcli/src/state_root.rs b/lcli/src/state_root.rs index 17a947b2f00..b2308999d42 100644 --- a/lcli/src/state_root.rs +++ b/lcli/src/state_root.rs @@ -4,9 +4,9 @@ use clap_utils::{parse_optional, parse_required}; use environment::Environment; use eth2::{types::StateId, BeaconNodeHttpClient, SensitiveUrl, Timeouts}; use eth2_network_config::Eth2NetworkConfig; -use log::info; use std::path::PathBuf; use std::time::{Duration, Instant}; +use tracing::info; use types::{BeaconState, EthSpec}; const HTTP_TIMEOUT: Duration = Duration::from_secs(10); diff --git a/lcli/src/transition_blocks.rs b/lcli/src/transition_blocks.rs index ecfa04fc816..4831f864913 100644 --- a/lcli/src/transition_blocks.rs +++ b/lcli/src/transition_blocks.rs @@ -72,8 +72,6 @@ use eth2::{ BeaconNodeHttpClient, SensitiveUrl, Timeouts, }; use eth2_network_config::Eth2NetworkConfig; -use log::{debug, info}; -use sloggers::{null::NullLoggerBuilder, Build}; use ssz::Encode; use state_processing::state_advance::complete_state_advance; use state_processing::{ @@ -87,6 +85,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::{Duration, Instant}; use store::HotColdDB; +use tracing::{debug, info}; use types::{BeaconState, ChainSpec, EthSpec, Hash256, SignedBeaconBlock}; const HTTP_TIMEOUT: Duration = Duration::from_secs(10); @@ -196,14 +195,8 @@ pub fn run( * Create a `BeaconStore` and `ValidatorPubkeyCache` for block signature verification. */ - let store = HotColdDB::open_ephemeral( - <_>::default(), - spec.clone(), - NullLoggerBuilder - .build() - .map_err(|e| format!("Error on NullLoggerBuilder: {:?}", e))?, - ) - .map_err(|e| format!("Failed to create ephemeral store: {:?}", e))?; + let store = HotColdDB::open_ephemeral(<_>::default(), spec.clone()) + .map_err(|e| format!("Failed to create ephemeral store: {:?}", e))?; let store = Arc::new(store); debug!("Building pubkey cache (might take some time)"); diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 50a80cbbe31..04c8efcdba5 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lighthouse" -version = "7.0.0" +version = "7.1.0-beta.0" authors = ["Sigma Prime "] edition = { workspace = true } autotests = false @@ -60,9 +60,10 @@ serde = { workspace = true } serde_json = { workspace = true } serde_yaml = { workspace = true } slasher = { workspace = true } -slog = { workspace = true } store = { workspace = true } task_executor = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } types = { workspace = true } unused_port = { workspace = true } validator_client = { workspace = true } diff --git a/lighthouse/environment/Cargo.toml b/lighthouse/environment/Cargo.toml index 02b8e0b6552..6d6ffa1725f 100644 --- a/lighthouse/environment/Cargo.toml +++ b/lighthouse/environment/Cargo.toml @@ -6,18 +6,19 @@ edition = { workspace = true } [dependencies] async-channel = { workspace = true } +clap = { workspace = true } eth2_config = { workspace = true } eth2_network_config = { workspace = true } futures = { workspace = true } logging = { workspace = true } +logroller = { workspace = true } serde = { workspace = true } -slog = { workspace = true } -slog-async = { workspace = true } -slog-json = "2.3.0" -slog-term = { workspace = true } -sloggers = { workspace = true } task_executor = { workspace = true } tokio = { workspace = true } +tracing = { workspace = true } +tracing-appender = { workspace = true } +tracing-log = { workspace = true } +tracing-subscriber = { workspace = true } types = { workspace = true } [target.'cfg(not(target_family = "unix"))'.dependencies] diff --git a/lighthouse/environment/src/lib.rs b/lighthouse/environment/src/lib.rs index 89d759d6629..9b0284e06d4 100644 --- a/lighthouse/environment/src/lib.rs +++ b/lighthouse/environment/src/lib.rs @@ -11,31 +11,38 @@ use eth2_config::Eth2Config; use eth2_network_config::Eth2NetworkConfig; use futures::channel::mpsc::{channel, Receiver, Sender}; use futures::{future, StreamExt}; - -use logging::{test_logger, SSELoggingComponents}; +use logging::tracing_logging_layer::LoggingLayer; +use logging::SSELoggingComponents; +use logroller::{Compression, LogRollerBuilder, Rotation, RotationSize}; use serde::{Deserialize, Serialize}; -use slog::{error, info, o, warn, Drain, Duplicate, Level, Logger}; -use sloggers::{file::FileLoggerBuilder, types::Format, types::Severity, Build}; -use std::fs::create_dir_all; -use std::io::{Result as IOResult, Write}; use std::path::PathBuf; use std::sync::Arc; use task_executor::{ShutdownReason, TaskExecutor}; use tokio::runtime::{Builder as RuntimeBuilder, Runtime}; +use tracing::{error, info, warn}; +use tracing_subscriber::filter::LevelFilter; use types::{EthSpec, GnosisEthSpec, MainnetEthSpec, MinimalEthSpec}; #[cfg(target_family = "unix")] use { futures::Future, - std::{pin::Pin, task::Context, task::Poll}, + std::{ + fs::{read_dir, set_permissions, Permissions}, + os::unix::fs::PermissionsExt, + path::Path, + pin::Pin, + task::Context, + task::Poll, + }, tokio::signal::unix::{signal, Signal, SignalKind}, }; #[cfg(not(target_family = "unix"))] use {futures::channel::oneshot, std::cell::RefCell}; -const LOG_CHANNEL_SIZE: usize = 16384; -const SSE_LOG_CHANNEL_SIZE: usize = 2048; +pub mod tracing_common; + +pub const SSE_LOG_CHANNEL_SIZE: usize = 2048; /// The maximum time in seconds the client will wait for all internal tasks to shutdown. const MAXIMUM_SHUTDOWN_TIME: u64 = 15; @@ -47,37 +54,54 @@ const MAXIMUM_SHUTDOWN_TIME: u64 = 15; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LoggerConfig { pub path: Option, - pub debug_level: String, - pub logfile_debug_level: String, + #[serde(skip_serializing, skip_deserializing, default = "default_debug_level")] + pub debug_level: LevelFilter, + #[serde( + skip_serializing, + skip_deserializing, + default = "default_logfile_debug_level" + )] + pub logfile_debug_level: LevelFilter, pub log_format: Option, pub logfile_format: Option, pub log_color: bool, + pub logfile_color: bool, pub disable_log_timestamp: bool, pub max_log_size: u64, pub max_log_number: usize, pub compression: bool, pub is_restricted: bool, pub sse_logging: bool, + pub extra_info: bool, } impl Default for LoggerConfig { fn default() -> Self { LoggerConfig { path: None, - debug_level: String::from("info"), - logfile_debug_level: String::from("debug"), + debug_level: LevelFilter::INFO, + logfile_debug_level: LevelFilter::DEBUG, log_format: None, + log_color: true, logfile_format: None, - log_color: false, + logfile_color: false, disable_log_timestamp: false, max_log_size: 200, max_log_number: 5, compression: false, is_restricted: true, sse_logging: false, + extra_info: false, } } } +fn default_debug_level() -> LevelFilter { + LevelFilter::INFO +} + +fn default_logfile_debug_level() -> LevelFilter { + LevelFilter::DEBUG +} /// An execution context that can be used by a service. /// /// Distinct from an `Environment` because a `Context` is not able to give a mutable reference to a @@ -109,17 +133,11 @@ impl RuntimeContext { pub fn eth2_config(&self) -> &Eth2Config { &self.eth2_config } - - /// Returns a reference to the logger for this service. - pub fn log(&self) -> &slog::Logger { - self.executor.log() - } } /// Builds an `Environment`. pub struct EnvironmentBuilder { runtime: Option>, - log: Option, sse_logging_components: Option, eth_spec_instance: E, eth2_config: Eth2Config, @@ -131,7 +149,6 @@ impl EnvironmentBuilder { pub fn minimal() -> Self { Self { runtime: None, - log: None, sse_logging_components: None, eth_spec_instance: MinimalEthSpec, eth2_config: Eth2Config::minimal(), @@ -145,7 +162,6 @@ impl EnvironmentBuilder { pub fn mainnet() -> Self { Self { runtime: None, - log: None, sse_logging_components: None, eth_spec_instance: MainnetEthSpec, eth2_config: Eth2Config::mainnet(), @@ -159,7 +175,6 @@ impl EnvironmentBuilder { pub fn gnosis() -> Self { Self { runtime: None, - log: None, sse_logging_components: None, eth_spec_instance: GnosisEthSpec, eth2_config: Eth2Config::gnosis(), @@ -182,149 +197,103 @@ impl EnvironmentBuilder { Ok(self) } - /// Sets a logger suitable for test usage. - pub fn test_logger(mut self) -> Result { - self.log = Some(test_logger()); - Ok(self) - } + /// Initialize the Lighthouse-specific tracing logging components from + /// the provided config. + /// + /// This consists of 3 tracing `Layers`: + /// - A `Layer` which logs to `stdout` + /// - An `Option` which logs to a log file + /// - An `Option` which emits logs to an SSE stream + pub fn init_tracing( + mut self, + config: LoggerConfig, + logfile_prefix: &str, + ) -> ( + Self, + LoggingLayer, + Option, + Option, + ) { + let filename_prefix = match logfile_prefix { + "beacon_node" => "beacon", + "validator_client" => "validator", + _ => logfile_prefix, + }; - fn log_nothing(_: &mut dyn Write) -> IOResult<()> { - Ok(()) - } + #[cfg(target_family = "unix")] + let file_mode = if config.is_restricted { 0o600 } else { 0o644 }; - /// Initializes the logger using the specified configuration. - /// The logger is "async" because it has a dedicated thread that accepts logs and then - /// asynchronously flushes them to stdout/files/etc. This means the thread that raised the log - /// does not have to wait for the logs to be flushed. - /// The logger can be duplicated and more detailed logs can be output to `logfile`. - /// Note that background file logging will spawn a new thread. - pub fn initialize_logger(mut self, config: LoggerConfig) -> Result { - // Setting up the initial logger format and build it. - let stdout_drain = if let Some(ref format) = config.log_format { - match format.to_uppercase().as_str() { - "JSON" => { - let stdout_drain = slog_json::Json::default(std::io::stdout()).fuse(); - slog_async::Async::new(stdout_drain) - .chan_size(LOG_CHANNEL_SIZE) - .build() - } - _ => return Err("Logging format provided is not supported".to_string()), - } - } else { - let stdout_decorator_builder = slog_term::TermDecorator::new(); - let stdout_decorator = if config.log_color { - stdout_decorator_builder.force_color() - } else { - stdout_decorator_builder + let file_logging_layer = match config.path { + None => { + eprintln!("No logfile path provided, logging to file is disabled"); + None } - .build(); - let stdout_decorator = - logging::AlignedTermDecorator::new(stdout_decorator, logging::MAX_MESSAGE_WIDTH); - let stdout_drain = slog_term::FullFormat::new(stdout_decorator); - let stdout_drain = if config.disable_log_timestamp { - stdout_drain.use_custom_timestamp(Self::log_nothing) - } else { - stdout_drain + Some(_) if config.max_log_number == 0 || config.max_log_size == 0 => { + // User has explicitly disabled logging to file, so don't emit a message. + None } - .build() - .fuse(); - slog_async::Async::new(stdout_drain) - .chan_size(LOG_CHANNEL_SIZE) - .build() - }; - - let stdout_drain = match config.debug_level.as_str() { - "info" => stdout_drain.filter_level(Level::Info), - "debug" => stdout_drain.filter_level(Level::Debug), - "trace" => stdout_drain.filter_level(Level::Trace), - "warn" => stdout_drain.filter_level(Level::Warning), - "error" => stdout_drain.filter_level(Level::Error), - "crit" => stdout_drain.filter_level(Level::Critical), - unknown => return Err(format!("Unknown debug-level: {}", unknown)), - }; - - let stdout_logger = Logger::root(stdout_drain.fuse(), o!()); - - // Disable file logging if values set to 0. - if config.max_log_size == 0 || config.max_log_number == 0 { - self.log = Some(stdout_logger); - return Ok(self); - } - - // Disable file logging if no path is specified. - let Some(path) = config.path else { - self.log = Some(stdout_logger); - return Ok(self); - }; - - // Ensure directories are created becfore the logfile. - if !path.exists() { - let mut dir = path.clone(); - dir.pop(); - - // Create the necessary directories for the correct service and network. - if !dir.exists() { - let res = create_dir_all(dir); + Some(path) => { + let log_filename = PathBuf::from(format!("{}.log", filename_prefix)); + let mut appender = LogRollerBuilder::new(path.clone(), log_filename) + .rotation(Rotation::SizeBased(RotationSize::MB(config.max_log_size))) + .max_keep_files(config.max_log_number.try_into().unwrap_or_else(|e| { + eprintln!("Failed to convert max_log_number to u64: {}", e); + 10 + })); + + if config.compression { + appender = appender.compression(Compression::Gzip); + } - // If the directories cannot be created, warn and disable the logger. - match res { - Ok(_) => (), + match appender.build() { + Ok(file_appender) => { + #[cfg(target_family = "unix")] + set_logfile_permissions(&path, filename_prefix, file_mode); + + let (writer, guard) = tracing_appender::non_blocking(file_appender); + Some(LoggingLayer::new( + writer, + guard, + config.disable_log_timestamp, + config.logfile_color, + config.logfile_format.clone(), + config.extra_info, + )) + } Err(e) => { - let log = stdout_logger; - warn!( - log, - "Background file logging is disabled"; - "error" => e); - self.log = Some(log); - return Ok(self); + eprintln!("Failed to initialize rolling file appender: {}", e); + None } } } - } - - let logfile_level = match config.logfile_debug_level.as_str() { - "info" => Severity::Info, - "debug" => Severity::Debug, - "trace" => Severity::Trace, - "warn" => Severity::Warning, - "error" => Severity::Error, - "crit" => Severity::Critical, - unknown => return Err(format!("Unknown loglevel-debug-level: {}", unknown)), }; - let file_logger = FileLoggerBuilder::new(&path) - .level(logfile_level) - .channel_size(LOG_CHANNEL_SIZE) - .format(match config.logfile_format.as_deref() { - Some("JSON") => Format::Json, - _ => Format::default(), - }) - .rotate_size(config.max_log_size) - .rotate_keep(config.max_log_number) - .rotate_compress(config.compression) - .restrict_permissions(config.is_restricted) - .build() - .map_err(|e| format!("Unable to build file logger: {}", e))?; - - let mut log = Logger::root(Duplicate::new(stdout_logger, file_logger).fuse(), o!()); - - info!( - log, - "Logging to file"; - "path" => format!("{:?}", path) - ); + let (stdout_non_blocking_writer, stdout_guard) = + tracing_appender::non_blocking(std::io::stdout()); - // If the http API is enabled, we may need to send logs to be consumed by subscribers. - if config.sse_logging { - let sse_logger = SSELoggingComponents::new(SSE_LOG_CHANNEL_SIZE); - self.sse_logging_components = Some(sse_logger.clone()); + let stdout_logging_layer = LoggingLayer::new( + stdout_non_blocking_writer, + stdout_guard, + config.disable_log_timestamp, + config.log_color, + config.log_format, + config.extra_info, + ); - log = Logger::root(Duplicate::new(log, sse_logger).fuse(), o!()); - } + let sse_logging_layer_opt = if config.sse_logging { + Some(SSELoggingComponents::new(SSE_LOG_CHANNEL_SIZE)) + } else { + None + }; - self.log = Some(log); + self.sse_logging_components = sse_logging_layer_opt.clone(); - Ok(self) + ( + self, + stdout_logging_layer, + file_logging_layer, + sse_logging_layer_opt, + ) } /// Adds a network configuration to the environment. @@ -351,7 +320,6 @@ impl EnvironmentBuilder { signal_rx: Some(signal_rx), signal: Some(signal), exit, - log: self.log.ok_or("Cannot build environment without log")?, sse_logging_components: self.sse_logging_components, eth_spec_instance: self.eth_spec_instance, eth2_config: self.eth2_config, @@ -370,7 +338,6 @@ pub struct Environment { signal_tx: Sender, signal: Option>, exit: async_channel::Receiver<()>, - log: Logger, sse_logging_components: Option, eth_spec_instance: E, pub eth2_config: Eth2Config, @@ -386,14 +353,14 @@ impl Environment { &self.runtime } - /// Returns a `Context` where no "service" has been added to the logger output. + /// Returns a `Context` where a "core" service has been added to the logger output. pub fn core_context(&self) -> RuntimeContext { RuntimeContext { executor: TaskExecutor::new( Arc::downgrade(self.runtime()), self.exit.clone(), - self.log.clone(), self.signal_tx.clone(), + "core".to_string(), ), eth_spec_instance: self.eth_spec_instance.clone(), eth2_config: self.eth2_config.clone(), @@ -408,8 +375,8 @@ impl Environment { executor: TaskExecutor::new( Arc::downgrade(self.runtime()), self.exit.clone(), - self.log.new(o!("service" => service_name)), self.signal_tx.clone(), + service_name, ), eth_spec_instance: self.eth_spec_instance.clone(), eth2_config: self.eth2_config.clone(), @@ -441,7 +408,7 @@ impl Environment { let terminate = SignalFuture::new(terminate_stream, "Received SIGTERM"); handles.push(terminate); } - Err(e) => error!(self.log, "Could not register SIGTERM handler"; "error" => e), + Err(e) => error!(error = ?e, "Could not register SIGTERM handler"), }; // setup for handling SIGINT @@ -450,7 +417,7 @@ impl Environment { let interrupt = SignalFuture::new(interrupt_stream, "Received SIGINT"); handles.push(interrupt); } - Err(e) => error!(self.log, "Could not register SIGINT handler"; "error" => e), + Err(e) => error!(error = ?e, "Could not register SIGINT handler"), } // setup for handling a SIGHUP @@ -459,7 +426,7 @@ impl Environment { let hup = SignalFuture::new(hup_stream, "Received SIGHUP"); handles.push(hup); } - Err(e) => error!(self.log, "Could not register SIGHUP handler"; "error" => e), + Err(e) => error!(error = ?e, "Could not register SIGHUP handler"), } future::select(inner_shutdown, future::select_all(handles.into_iter())).await @@ -467,7 +434,7 @@ impl Environment { match self.runtime().block_on(register_handlers) { future::Either::Left((Ok(reason), _)) => { - info!(self.log, "Internal shutdown received"; "reason" => reason.message()); + info!("Internal shutdown received"); Ok(reason) } future::Either::Left((Err(e), _)) => Err(e.into()), @@ -494,14 +461,12 @@ impl Environment { // setup for handling a Ctrl-C let (ctrlc_send, ctrlc_oneshot) = oneshot::channel(); let ctrlc_send_c = RefCell::new(Some(ctrlc_send)); - let log = self.log.clone(); ctrlc::set_handler(move || { if let Some(ctrlc_send) = ctrlc_send_c.try_borrow_mut().unwrap().take() { if let Err(e) = ctrlc_send.send(()) { error!( - log, - "Error sending ctrl-c message"; - "error" => e + error = ?e, + "Error sending ctrl-c message" ); } } @@ -514,7 +479,7 @@ impl Environment { .block_on(future::select(inner_shutdown, ctrlc_oneshot)) { future::Either::Left((Ok(reason), _)) => { - info!(self.log, "Internal shutdown received"; "reason" => reason.message()); + info!(reason = reason.message(), "Internal shutdown received"); Ok(reason) } future::Either::Left((Err(e), _)) => Err(e.into()), @@ -531,9 +496,8 @@ impl Environment { runtime.shutdown_timeout(std::time::Duration::from_secs(MAXIMUM_SHUTDOWN_TIME)) } Err(e) => warn!( - self.log, - "Failed to obtain runtime access to shutdown gracefully"; - "error" => ?e + error = ?e, + "Failed to obtain runtime access to shutdown gracefully" ), } } @@ -579,3 +543,37 @@ impl Future for SignalFuture { } } } + +#[cfg(target_family = "unix")] +fn set_logfile_permissions(log_dir: &Path, filename_prefix: &str, file_mode: u32) { + let newest = read_dir(log_dir) + .ok() + .into_iter() + .flat_map(|entries| entries.filter_map(Result::ok)) + .filter_map(|entry| { + let path = entry.path(); + let fname = path.file_name()?.to_string_lossy(); + if path.is_file() && fname.starts_with(filename_prefix) && fname.ends_with(".log") { + let modified = entry.metadata().ok()?.modified().ok()?; + Some((path, modified)) + } else { + None + } + }) + .max_by_key(|(_path, mtime)| *mtime); + + match newest { + Some((file, _mtime)) => { + if let Err(e) = set_permissions(&file, Permissions::from_mode(file_mode)) { + eprintln!("Failed to set permissions on {}: {}", file.display(), e); + } + } + None => { + eprintln!( + "Couldn't find a newly created logfile in {} matching prefix \"{}\".", + log_dir.display(), + filename_prefix + ); + } + } +} diff --git a/lighthouse/environment/src/tracing_common.rs b/lighthouse/environment/src/tracing_common.rs new file mode 100644 index 00000000000..dd9fe45cadf --- /dev/null +++ b/lighthouse/environment/src/tracing_common.rs @@ -0,0 +1,80 @@ +use crate::{EnvironmentBuilder, LoggerConfig}; +use clap::ArgMatches; +use logging::Libp2pDiscv5TracingLayer; +use logging::{ + create_libp2p_discv5_tracing_layer, tracing_logging_layer::LoggingLayer, SSELoggingComponents, +}; +use std::process; + +use tracing_subscriber::filter::LevelFilter; +use types::EthSpec; + +/// Constructs all logging layers including both Lighthouse-specific and +/// dependency logging. +/// +/// The `Layer`s are as follows: +/// - A `Layer` which logs to `stdout` +/// - An `Option` which logs to a log file +/// - An `Option` which emits logs to an SSE stream +/// - An `Option` which logs relevant dependencies to their +/// own log files. (Currently only `libp2p` and `discv5`) +pub fn construct_logger( + logger_config: LoggerConfig, + matches: &ArgMatches, + environment_builder: EnvironmentBuilder, +) -> ( + EnvironmentBuilder, + LoggerConfig, + LoggingLayer, + Option, + Option, + Option, +) { + let subcommand_name = matches.subcommand_name(); + let logfile_prefix = subcommand_name.unwrap_or("lighthouse"); + + let (builder, stdout_logging_layer, file_logging_layer, sse_logging_layer_opt) = + 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 logger_config.max_log_size == 0 || logger_config.max_log_number == 0 { + // User has explicitly disabled logging to file. + None + } else { + create_libp2p_discv5_tracing_layer( + logger_config.path.clone(), + logger_config.max_log_size, + ) + } + } else { + // Disable libp2p and discv5 logs when running other subcommands. + None + } + } else { + None + }; + + ( + builder, + logger_config, + stdout_logging_layer, + file_logging_layer, + sse_logging_layer_opt, + libp2p_discv5_layer, + ) +} + +pub fn parse_level(level: &str) -> LevelFilter { + match level.to_lowercase().as_str() { + "error" => LevelFilter::ERROR, + "warn" => LevelFilter::WARN, + "info" => LevelFilter::INFO, + "debug" => LevelFilter::DEBUG, + "trace" => LevelFilter::TRACE, + _ => { + eprintln!("Unsupported log level"); + process::exit(1) + } + } +} diff --git a/lighthouse/environment/tests/environment_builder.rs b/lighthouse/environment/tests/environment_builder.rs index b0c847612a5..a98caf8df5b 100644 --- a/lighthouse/environment/tests/environment_builder.rs +++ b/lighthouse/environment/tests/environment_builder.rs @@ -9,8 +9,6 @@ fn builder() -> EnvironmentBuilder { EnvironmentBuilder::mainnet() .multi_threaded_tokio_runtime() .expect("should set runtime") - .test_logger() - .expect("should set logger") } fn eth2_network_config() -> Option { diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index d7a14e38092..7ddf04db017 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -11,18 +11,22 @@ use clap_utils::{ }; use cli::LighthouseSubcommands; use directory::{parse_path_or_default, DEFAULT_BEACON_NODE_DIR, DEFAULT_VALIDATOR_DIR}; +use environment::tracing_common; use environment::{EnvironmentBuilder, LoggerConfig}; use eth2_network_config::{Eth2NetworkConfig, DEFAULT_HARDCODED_NETWORK, HARDCODED_NET_NAMES}; use ethereum_hashing::have_sha_extensions; use futures::TryFutureExt; use lighthouse_version::VERSION; +use logging::{build_workspace_filter, crit, MetricsLayer}; use malloc_utils::configure_memory_allocator; -use slog::{crit, info}; use std::backtrace::Backtrace; +use std::io::IsTerminal; use std::path::PathBuf; use std::process::exit; use std::sync::LazyLock; use task_executor::ShutdownReason; +use tracing::{info, warn, Level}; +use tracing_subscriber::{filter::EnvFilter, layer::SubscriberExt, util::SubscriberInitExt, Layer}; use types::{EthSpec, EthSpecId}; use validator_client::ProductionValidatorClient; @@ -64,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 { @@ -120,15 +127,11 @@ fn main() { .display_order(0), ) .arg( - Arg::new("logfile") - .long("logfile") - .value_name("FILE") + Arg::new("logfile-dir") + .long("logfile-dir") + .value_name("DIR") .help( - "File path where the log file will be stored. Once it grows to the \ - value specified in `--logfile-max-size` a new log file is generated where \ - future logs are stored. \ - Once the number of log files exceeds the value specified in \ - `--logfile-max-number` the oldest log file will be overwritten.") + "Directory path where the log file will be stored") .action(ArgAction::Set) .global(true) .display_order(0) @@ -139,7 +142,7 @@ fn main() { .value_name("LEVEL") .help("The verbosity level used when emitting logs to the log file.") .action(ArgAction::Set) - .value_parser(["info", "debug", "trace", "warn", "error", "crit"]) + .value_parser(["info", "debug", "trace", "warn", "error"]) .default_value("debug") .global(true) .display_order(0) @@ -215,13 +218,36 @@ fn main() { .arg( Arg::new("log-color") .long("log-color") - .alias("log-colour") - .help("Force outputting colors when emitting logs to the terminal.") + .alias("log-color") + .help("Enables/Disables colors for logs in terminal. \ + Set it to false to disable colors.") + .num_args(0..=1) + .default_missing_value("true") + .default_value("true") + .value_parser(clap::value_parser!(bool)) + .help_heading(FLAG_HEADER) + .global(true) + .display_order(0) + ) + .arg( + Arg::new("logfile-color") + .long("logfile-color") + .alias("logfile-colour") + .help("Enables colors in logfile.") .action(ArgAction::SetTrue) .help_heading(FLAG_HEADER) .global(true) .display_order(0) ) + .arg( + Arg::new("log-extra-info") + .long("log-extra-info") + .action(ArgAction::SetTrue) + .help_heading(FLAG_HEADER) + .help("If present, show module,file,line in logs") + .global(true) + .display_order(0) + ) .arg( Arg::new("disable-log-timestamp") .long("disable-log-timestamp") @@ -237,7 +263,7 @@ fn main() { .value_name("LEVEL") .help("Specifies the verbosity level used when emitting logs to the terminal.") .action(ArgAction::Set) - .value_parser(["info", "debug", "trace", "warn", "error", "crit"]) + .value_parser(["info", "debug", "trace", "warn", "error"]) .global(true) .default_value("info") .display_order(0) @@ -387,7 +413,7 @@ fn main() { "The timeout in seconds for the request to --genesis-state-url.", ) .action(ArgAction::Set) - .default_value("180") + .default_value("300") .global(true) .display_order(0) ) @@ -499,10 +525,21 @@ fn run( let log_format = matches.get_one::("log-format"); - let log_color = matches.get_flag("log-color"); + let log_color = if std::io::stdin().is_terminal() { + matches + .get_one::("log-color") + .copied() + .unwrap_or(true) + } else { + // Disable color when in non-interactive mode. + false + }; + + let logfile_color = matches.get_flag("logfile-color"); let disable_log_timestamp = matches.get_flag("disable-log-timestamp"); + let extra_info = matches.get_flag("log-extra-info"); let logfile_debug_level = matches .get_one::("logfile-debug-level") .ok_or("Expected --logfile-debug-level flag")?; @@ -529,15 +566,13 @@ fn run( let logfile_restricted = !matches.get_flag("logfile-no-restricted-perms"); // Construct the path to the log file. - let mut log_path: Option = clap_utils::parse_optional(matches, "logfile")?; + let mut log_path: Option = clap_utils::parse_optional(matches, "logfile-dir")?; if log_path.is_none() { log_path = match matches.subcommand() { Some(("beacon_node", _)) => Some( parse_path_or_default(matches, "datadir")? .join(DEFAULT_BEACON_NODE_DIR) - .join("logs") - .join("beacon") - .with_extension("log"), + .join("logs"), ), Some(("validator_client", vc_matches)) => { let base_path = if vc_matches.contains_id("validators-dir") { @@ -546,12 +581,7 @@ fn run( parse_path_or_default(matches, "datadir")?.join(DEFAULT_VALIDATOR_DIR) }; - Some( - base_path - .join("logs") - .join("validator") - .with_extension("log"), - ) + Some(base_path.join("logs")) } _ => None, }; @@ -567,57 +597,109 @@ fn run( } }; - let logger_config = LoggerConfig { - path: log_path.clone(), - debug_level: String::from(debug_level), - logfile_debug_level: String::from(logfile_debug_level), - log_format: log_format.map(String::from), - logfile_format: logfile_format.map(String::from), - log_color, - disable_log_timestamp, - max_log_size: logfile_max_size * 1_024 * 1_024, - max_log_number: logfile_max_number, - compression: logfile_compress, - is_restricted: logfile_restricted, - sse_logging, - }; + let ( + builder, + logger_config, + stdout_logging_layer, + file_logging_layer, + sse_logging_layer_opt, + libp2p_discv5_layer, + ) = tracing_common::construct_logger( + LoggerConfig { + path: log_path.clone(), + debug_level: tracing_common::parse_level(debug_level), + logfile_debug_level: tracing_common::parse_level(logfile_debug_level), + log_format: log_format.map(String::from), + logfile_format: logfile_format.map(String::from), + log_color, + logfile_color, + disable_log_timestamp, + max_log_size: logfile_max_size, + max_log_number: logfile_max_number, + compression: logfile_compress, + is_restricted: logfile_restricted, + sse_logging, + extra_info, + }, + matches, + environment_builder, + ); + + let workspace_filter = build_workspace_filter()?; - let builder = environment_builder.initialize_logger(logger_config.clone())?; + let mut logging_layers = Vec::new(); + + 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(sse_logging_layer) = sse_logging_layer_opt { + logging_layers.push(sse_logging_layer.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(), + ); + } + + logging_layers.push(MetricsLayer.boxed()); + + let logging_result = tracing_subscriber::registry() + .with(logging_layers) + .try_init(); + + if let Err(e) = logging_result { + eprintln!("Failed to initialize logger: {e}"); + } let mut environment = builder .multi_threaded_tokio_runtime()? .eth2_network_config(eth2_network_config)? .build()?; - let log = environment.core_context().log().clone(); - // Log panics properly. { - let log = log.clone(); std::panic::set_hook(Box::new(move |info| { crit!( - log, - "Task panic. This is a bug!"; - "location" => info.location().map(ToString::to_string), - "message" => info.payload().downcast_ref::(), - "backtrace" => %Backtrace::capture(), - "advice" => "Please check above for a backtrace and notify the developers", + location = info.location().map(ToString::to_string), + message = info.payload().downcast_ref::(), + backtrace = %Backtrace::capture(), + advice = "Please check above for a backtrace and notify the developers", + "Task panic. This is a bug!" ); })); } // Allow Prometheus to export the time at which the process was started. - metrics::expose_process_start_time(&log); + metrics::expose_process_start_time(); // Allow Prometheus access to the version and commit of the Lighthouse build. metrics::expose_lighthouse_version(); #[cfg(all(feature = "modern", target_arch = "x86_64"))] if !std::is_x86_feature_detected!("adx") { - slog::warn!( - log, - "CPU seems incompatible with optimized Lighthouse build"; - "advice" => "If you get a SIGILL, please try Lighthouse portable build" + tracing::warn!( + advice = "If you get a SIGILL, please try Lighthouse portable build", + "CPU seems incompatible with optimized Lighthouse build" ); } @@ -631,7 +713,7 @@ fn run( ]; for flag in deprecated_flags { if matches.get_one::(flag).is_some() { - slog::warn!(log, "The {} flag is deprecated and does nothing", flag); + warn!("The {} flag is deprecated and does nothing", flag); } } @@ -675,26 +757,21 @@ fn run( match LighthouseSubcommands::from_arg_matches(matches) { Ok(LighthouseSubcommands::DatabaseManager(db_manager_config)) => { - info!(log, "Running database manager for {} network", network_name); + info!("Running database manager for {} network", network_name); database_manager::run(matches, &db_manager_config, environment)?; return Ok(()); } Ok(LighthouseSubcommands::ValidatorClient(validator_client_config)) => { let context = environment.core_context(); - let log = context.log().clone(); let executor = context.executor.clone(); - let config = validator_client::Config::from_cli( - matches, - &validator_client_config, - context.log(), - ) - .map_err(|e| format!("Unable to initialize validator config: {}", e))?; + let config = validator_client::Config::from_cli(matches, &validator_client_config) + .map_err(|e| format!("Unable to initialize validator config: {}", e))?; // Dump configs if `dump-config` or `dump-chain-config` flags are set clap_utils::check_dump_configs::<_, E>(matches, &config, &context.eth2_config.spec)?; let shutdown_flag = matches.get_flag("immediate-shutdown"); if shutdown_flag { - info!(log, "Validator client immediate shutdown triggered."); + info!("Validator client immediate shutdown triggered."); return Ok(()); } @@ -704,7 +781,7 @@ fn run( .and_then(|mut vc| async move { vc.start_service().await }) .await { - crit!(log, "Failed to start validator client"; "reason" => e); + crit!(reason = e, "Failed to start validator client"); // Ignore the error since it always occurs during normal operation when // shutting down. let _ = executor @@ -718,17 +795,12 @@ fn run( Err(_) => (), }; - info!(log, "Lighthouse started"; "version" => VERSION); - info!( - log, - "Configured for network"; - "name" => &network_name - ); + info!(version = VERSION, "Lighthouse started"); + info!(network_name, "Configured network"); match matches.subcommand() { Some(("beacon_node", matches)) => { let context = environment.core_context(); - let log = context.log().clone(); let executor = context.executor.clone(); let mut config = beacon_node::get_config::(matches, &context)?; config.logger_config = logger_config; @@ -737,29 +809,14 @@ fn run( let shutdown_flag = matches.get_flag("immediate-shutdown"); if shutdown_flag { - info!(log, "Beacon node immediate shutdown triggered."); + info!("Beacon node immediate shutdown triggered."); return Ok(()); } - let mut tracing_log_path: Option = - clap_utils::parse_optional(matches, "logfile")?; - - if tracing_log_path.is_none() { - tracing_log_path = Some( - parse_path_or_default(matches, "datadir")? - .join(DEFAULT_BEACON_NODE_DIR) - .join("logs"), - ) - } - - let path = tracing_log_path.clone().unwrap(); - - logging::create_tracing_layer(path); - executor.clone().spawn( async move { if let Err(e) = ProductionBeaconNode::new(context.clone(), config).await { - crit!(log, "Failed to start beacon node"; "reason" => e); + crit!(reason = ?e, "Failed to start beacon node"); // Ignore the error since it always occurs during normal operation when // shutting down. let _ = executor @@ -774,14 +831,14 @@ fn run( // Qt the moment this needs to exist so that we dont trigger a crit. Some(("validator_client", _)) => (), _ => { - crit!(log, "No subcommand supplied. See --help ."); + crit!("No subcommand supplied. See --help ."); return Err("No subcommand supplied.".into()); } }; // Block this thread until we get a ctrl-c or a task sends a shutdown signal. let shutdown_reason = environment.block_until_shutdown_requested()?; - info!(log, "Shutting down.."; "reason" => ?shutdown_reason); + info!(reason = ?shutdown_reason, "Shutting down.."); environment.fire_signal(); diff --git a/lighthouse/src/metrics.rs b/lighthouse/src/metrics.rs index 30e0120582a..6b464a18be7 100644 --- a/lighthouse/src/metrics.rs +++ b/lighthouse/src/metrics.rs @@ -1,8 +1,8 @@ use lighthouse_version::VERSION; pub use metrics::*; -use slog::{error, Logger}; use std::sync::LazyLock; use std::time::{SystemTime, UNIX_EPOCH}; +use tracing::error; pub static PROCESS_START_TIME_SECONDS: LazyLock> = LazyLock::new(|| { try_create_int_gauge( @@ -19,13 +19,12 @@ pub static LIGHTHOUSE_VERSION: LazyLock> = LazyLock::new(|| ) }); -pub fn expose_process_start_time(log: &Logger) { +pub fn expose_process_start_time() { match SystemTime::now().duration_since(UNIX_EPOCH) { Ok(duration) => set_gauge(&PROCESS_START_TIME_SECONDS, duration.as_secs() as i64), Err(e) => error!( - log, - "Failed to read system time"; - "error" => %e + error = %e, + "Failed to read system time" ), } } diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 86104ce050a..ea4716c0103 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() @@ -2431,20 +2431,20 @@ fn monitoring_endpoint() { // Tests for Logger flags. #[test] -fn default_log_color_flag() { +fn default_logfile_color_flag() { CommandLineTest::new() .run_with_zero_port() .with_config(|config| { - assert!(!config.logger_config.log_color); + assert!(!config.logger_config.logfile_color); }); } #[test] -fn enabled_log_color_flag() { +fn enabled_logfile_color_flag() { CommandLineTest::new() - .flag("log-color", None) + .flag("logfile-color", None) .run_with_zero_port() .with_config(|config| { - assert!(config.logger_config.log_color); + assert!(config.logger_config.logfile_color); }); } #[test] @@ -2555,7 +2555,6 @@ fn light_client_server_default() { .with_config(|config| { assert!(config.network.enable_light_client_server); assert!(config.chain.enable_light_client_server); - assert!(config.http_api.enable_light_client_server); }); } @@ -2588,7 +2587,6 @@ fn light_client_http_server_disabled() { .flag("disable-light-client-server", None) .run_with_zero_port() .with_config(|config| { - assert!(!config.http_api.enable_light_client_server); assert!(!config.network.enable_light_client_server); assert!(!config.chain.enable_light_client_server); }); @@ -2760,7 +2758,7 @@ fn genesis_state_url_default() { .run_with_zero_port() .with_config(|config| { assert_eq!(config.genesis_state_url, None); - assert_eq!(config.genesis_state_url_timeout, Duration::from_secs(180)); + assert_eq!(config.genesis_state_url_timeout, Duration::from_secs(300)); }); } @@ -2789,6 +2787,32 @@ fn beacon_node_backend_override() { }); } +#[test] +fn block_publishing_delay_for_testing() { + CommandLineTest::new() + .flag("delay-block-publishing", Some("2.5")) + .run_with_zero_port() + .with_config(|config| { + assert_eq!( + config.chain.block_publishing_delay, + Some(Duration::from_secs_f64(2.5f64)) + ); + }); +} + +#[test] +fn data_column_publishing_delay_for_testing() { + CommandLineTest::new() + .flag("delay-data-column-publishing", Some("3.5")) + .run_with_zero_port() + .with_config(|config| { + assert_eq!( + config.chain.data_column_publishing_delay, + Some(Duration::from_secs_f64(3.5f64)) + ); + }); +} + #[test] fn invalid_block_roots_flag() { let dir = TempDir::new().expect("Unable to create temporary directory"); diff --git a/lighthouse/tests/validator_client.rs b/lighthouse/tests/validator_client.rs index eccd97d4864..f99fc3c4605 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/network_params.yaml b/scripts/local_testnet/network_params.yaml index b53d88e52c5..e671340afb8 100644 --- a/scripts/local_testnet/network_params.yaml +++ b/scripts/local_testnet/network_params.yaml @@ -14,4 +14,5 @@ global_log_level: debug snooper_enabled: false additional_services: - dora + - spamoor - prometheus_grafana diff --git a/scripts/local_testnet/network_params_das.yaml b/scripts/local_testnet/network_params_das.yaml index 80b4bc95c60..d47dfa6b5a5 100644 --- a/scripts/local_testnet/network_params_das.yaml +++ b/scripts/local_testnet/network_params_das.yaml @@ -1,6 +1,7 @@ participants: - cl_type: lighthouse cl_image: lighthouse:local + el_image: ethpandaops/geth:engine-getblobs-v2-3676b56 cl_extra_params: - --subscribe-all-data-column-subnets - --subscribe-all-subnets @@ -10,6 +11,7 @@ participants: count: 2 - cl_type: lighthouse cl_image: lighthouse:local + el_image: ethpandaops/geth:engine-getblobs-v2-3676b56 cl_extra_params: # Note: useful for testing range sync (only produce block if node is in sync to prevent forking) - --sync-tolerance-epochs=0 @@ -19,6 +21,10 @@ network_params: electra_fork_epoch: 1 fulu_fork_epoch: 2 seconds_per_slot: 6 + max_blobs_per_block_electra: 64 + target_blobs_per_block_electra: 48 + max_blobs_per_block_fulu: 64 + target_blobs_per_block_fulu: 48 snooper_enabled: false global_log_level: debug additional_services: @@ -26,4 +32,8 @@ additional_services: - spamoor_blob - prometheus_grafana dora_params: - image: ethpandaops/dora:fulu-support \ No newline at end of file + image: ethpandaops/dora:fulu-support +spamoor_blob_params: + # Throughput of spamoor + # Defaults to 3 + throughput: 32 \ No newline at end of file diff --git a/scripts/tests/doppelganger_protection.sh b/scripts/tests/doppelganger_protection.sh index 5be5c13dee9..86c9705ee43 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 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_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/slasher/Cargo.toml b/slasher/Cargo.toml index fcecc2fc233..b2f6eca9c37 100644 --- a/slasher/Cargo.toml +++ b/slasher/Cargo.toml @@ -32,16 +32,14 @@ rand = { workspace = true } redb = { version = "2.1.4", optional = true } safe_arith = { workspace = true } serde = { workspace = true } -slog = { workspace = true } ssz_types = { workspace = true } strum = { workspace = true } +tracing = { workspace = true } tree_hash = { workspace = true } tree_hash_derive = { workspace = true } types = { workspace = true } [dev-dependencies] -logging = { workspace = true } maplit = { workspace = true } rayon = { workspace = true } tempfile = { workspace = true } - diff --git a/slasher/service/Cargo.toml b/slasher/service/Cargo.toml index 41e3b5b90ad..19398fada83 100644 --- a/slasher/service/Cargo.toml +++ b/slasher/service/Cargo.toml @@ -10,9 +10,9 @@ directory = { workspace = true } lighthouse_network = { workspace = true } network = { workspace = true } slasher = { workspace = true } -slog = { workspace = true } slot_clock = { workspace = true } state_processing = { workspace = true } task_executor = { workspace = true } tokio = { workspace = true } +tracing = { workspace = true } types = { workspace = true } diff --git a/slasher/service/src/service.rs b/slasher/service/src/service.rs index 091a95dc4cb..2409a24c789 100644 --- a/slasher/service/src/service.rs +++ b/slasher/service/src/service.rs @@ -8,7 +8,6 @@ use slasher::{ metrics::{self, SLASHER_DATABASE_SIZE, SLASHER_RUN_TIME}, Slasher, }; -use slog::{debug, error, info, trace, warn, Logger}; use slot_clock::SlotClock; use state_processing::{ per_block_processing::errors::{ @@ -21,6 +20,7 @@ use std::sync::Arc; use task_executor::TaskExecutor; use tokio::sync::mpsc::UnboundedSender; use tokio::time::{interval_at, Duration, Instant}; +use tracing::{debug, error, info, info_span, trace, warn, Instrument}; use types::{AttesterSlashing, Epoch, EthSpec, ProposerSlashing}; pub struct SlasherService { @@ -47,9 +47,8 @@ impl SlasherService { .slasher .clone() .ok_or("No slasher is configured")?; - let log = slasher.log().clone(); - info!(log, "Starting slasher"; "broadcast" => slasher.config().broadcast); + info!(broadcast = slasher.config().broadcast, "Starting slasher"); // Buffer just a single message in the channel. If the receiver is still processing, we // don't need to burden them with more work (we can wait). @@ -65,13 +64,17 @@ impl SlasherService { update_period, slot_offset, notif_sender, - log, - ), + ) + .instrument(tracing::info_span!("slasher", service = "slasher")), "slasher_server_notifier", ); executor.spawn_blocking( - || Self::run_processor(beacon_chain, slasher, notif_receiver, network_sender), + || { + let span = info_span!("slasher", service = "slasher"); + let _ = span.enter(); + Self::run_processor(beacon_chain, slasher, notif_receiver, network_sender); + }, "slasher_server_processor", ); @@ -84,14 +87,13 @@ impl SlasherService { update_period: u64, slot_offset: f64, notif_sender: SyncSender, - log: Logger, ) { let slot_offset = Duration::from_secs_f64(slot_offset); let start_instant = if let Some(duration_to_next_slot) = beacon_chain.slot_clock.duration_to_next_slot() { Instant::now() + duration_to_next_slot + slot_offset } else { - error!(log, "Error aligning slasher to slot clock"); + error!("Error aligning slasher to slot clock"); Instant::now() }; let mut interval = interval_at(start_instant, Duration::from_secs(update_period)); @@ -104,7 +106,7 @@ impl SlasherService { break; } } else { - trace!(log, "Slasher has nothing to do: we are pre-genesis"); + trace!("Slasher has nothing to do: we are pre-genesis"); } } } @@ -116,7 +118,6 @@ impl SlasherService { notif_receiver: Receiver, network_sender: UnboundedSender>, ) { - let log = slasher.log(); while let Ok(current_epoch) = notif_receiver.recv() { let t = Instant::now(); @@ -125,10 +126,9 @@ impl SlasherService { Ok(stats) => Some(stats), Err(e) => { error!( - log, - "Error during scheduled slasher processing"; - "epoch" => current_epoch, - "error" => ?e, + epoch = %current_epoch, + error = ?e, + "Error during scheduled slasher processing" ); None } @@ -139,10 +139,9 @@ impl SlasherService { // If the database is full then pruning could help to free it up. if let Err(e) = slasher.prune_database(current_epoch) { error!( - log, - "Error during slasher database pruning"; - "epoch" => current_epoch, - "error" => ?e, + epoch = %current_epoch, + error = ?e, + "Error during slasher database pruning" ); continue; }; @@ -155,12 +154,11 @@ impl SlasherService { if let Some(stats) = stats { debug!( - log, - "Completed slasher update"; - "epoch" => current_epoch, - "time_taken" => format!("{}ms", t.elapsed().as_millis()), - "num_attestations" => stats.attestation_stats.num_processed, - "num_blocks" => stats.block_stats.num_processed, + epoch = %current_epoch, + time_taken = format!("{}ms", t.elapsed().as_millis()), + num_attestations = stats.attestation_stats.num_processed, + num_blocks = stats.block_stats.num_processed, + "Completed slasher update" ); } } @@ -181,7 +179,6 @@ impl SlasherService { slasher: &Slasher, network_sender: &UnboundedSender>, ) { - let log = slasher.log(); let attester_slashings = slasher.get_attester_slashings(); for slashing in attester_slashings { @@ -198,18 +195,16 @@ impl SlasherService { BlockOperationError::Invalid(AttesterSlashingInvalid::NoSlashableIndices), )) => { debug!( - log, - "Skipping attester slashing for slashed validators"; - "slashing" => ?slashing, + ?slashing, + "Skipping attester slashing for slashed validators" ); continue; } Err(e) => { warn!( - log, - "Attester slashing produced is invalid"; - "error" => ?e, - "slashing" => ?slashing, + error = ?e, + ?slashing, + "Attester slashing produced is invalid" ); continue; } @@ -224,9 +219,8 @@ impl SlasherService { Self::publish_attester_slashing(beacon_chain, network_sender, slashing) { debug!( - log, - "Unable to publish attester slashing"; - "error" => e, + error = ?e, + "Unable to publish attester slashing" ); } } @@ -238,7 +232,6 @@ impl SlasherService { slasher: &Slasher, network_sender: &UnboundedSender>, ) { - let log = slasher.log(); let proposer_slashings = slasher.get_proposer_slashings(); for slashing in proposer_slashings { @@ -254,18 +247,16 @@ impl SlasherService { )), )) => { debug!( - log, - "Skipping proposer slashing for slashed validator"; - "validator_index" => index, + validator_index = index, + "Skipping proposer slashing for slashed validator" ); continue; } Err(e) => { error!( - log, - "Proposer slashing produced is invalid"; - "error" => ?e, - "slashing" => ?slashing, + error = ?e, + ?slashing, + "Proposer slashing produced is invalid" ); continue; } @@ -277,9 +268,8 @@ impl SlasherService { Self::publish_proposer_slashing(beacon_chain, network_sender, slashing) { debug!( - log, - "Unable to publish proposer slashing"; - "error" => e, + error = ?e, + "Unable to publish proposer slashing" ); } } diff --git a/slasher/src/database.rs b/slasher/src/database.rs index 9e5e827034a..071109e00ca 100644 --- a/slasher/src/database.rs +++ b/slasher/src/database.rs @@ -12,12 +12,12 @@ use interface::{Environment, OpenDatabases, RwTransaction}; use lru::LruCache; use parking_lot::Mutex; use serde::de::DeserializeOwned; -use slog::{info, Logger}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use std::borrow::{Borrow, Cow}; use std::marker::PhantomData; use std::sync::Arc; +use tracing::info; use tree_hash::TreeHash; use types::{ AggregateSignature, AttestationData, ChainSpec, Epoch, EthSpec, Hash256, IndexedAttestation, @@ -287,8 +287,8 @@ fn ssz_decode(bytes: Cow<[u8]>) -> Result { } impl SlasherDB { - pub fn open(config: Arc, spec: Arc, log: Logger) -> Result { - info!(log, "Opening slasher database"; "backend" => %config.backend); + pub fn open(config: Arc, spec: Arc) -> Result { + info!(backend = %config.backend, "Opening slasher database"); std::fs::create_dir_all(&config.database_path)?; diff --git a/slasher/src/slasher.rs b/slasher/src/slasher.rs index 19f2cd138de..12f35e657ef 100644 --- a/slasher/src/slasher.rs +++ b/slasher/src/slasher.rs @@ -9,9 +9,9 @@ use crate::{ IndexedAttestationId, ProposerSlashingStatus, RwTransaction, SimpleBatch, SlasherDB, }; use parking_lot::Mutex; -use slog::{debug, error, info, Logger}; use std::collections::HashSet; use std::sync::Arc; +use tracing::{debug, error, info}; use types::{ AttesterSlashing, ChainSpec, Epoch, EthSpec, IndexedAttestation, ProposerSlashing, SignedBeaconBlockHeader, @@ -25,26 +25,21 @@ pub struct Slasher { attester_slashings: Mutex>>, proposer_slashings: Mutex>, config: Arc, - log: Logger, } impl Slasher { - pub fn open(config: Config, spec: Arc, log: Logger) -> Result { + pub fn open(config: Config, spec: Arc) -> Result { config.validate()?; let config = Arc::new(config); - let db = SlasherDB::open(config.clone(), spec, log.clone())?; - Self::from_config_and_db(config, db, log) + let db = SlasherDB::open(config.clone(), spec)?; + Self::from_config_and_db(config, db) } /// TESTING ONLY. /// /// Initialise a slasher database from an existing `db`. The caller must ensure that the /// database's config matches the one provided. - pub fn from_config_and_db( - config: Arc, - db: SlasherDB, - log: Logger, - ) -> Result { + pub fn from_config_and_db(config: Arc, db: SlasherDB) -> Result { config.validate()?; let attester_slashings = Mutex::new(HashSet::new()); let proposer_slashings = Mutex::new(HashSet::new()); @@ -57,7 +52,6 @@ impl Slasher { attester_slashings, proposer_slashings, config, - log, }) } @@ -80,10 +74,6 @@ impl Slasher { &self.config } - pub fn log(&self) -> &Logger { - &self.log - } - /// Accept an attestation from the network and queue it for processing. pub fn accept_attestation(&self, attestation: IndexedAttestation) { self.attestation_queue.queue(attestation); @@ -126,11 +116,7 @@ impl Slasher { let num_slashings = slashings.len(); if !slashings.is_empty() { - info!( - self.log, - "Found {} new proposer slashings!", - slashings.len(), - ); + info!("Found {} new proposer slashings!", slashings.len()); self.proposer_slashings.lock().extend(slashings); } @@ -156,11 +142,10 @@ impl Slasher { self.attestation_queue.requeue(deferred); debug!( - self.log, - "Pre-processing attestations for slasher"; - "num_valid" => num_valid, - "num_deferred" => num_deferred, - "num_dropped" => num_dropped, + %num_valid, + num_deferred, + num_dropped, + "Pre-processing attestations for slasher" ); metrics::set_gauge(&SLASHER_NUM_ATTESTATIONS_VALID, num_valid as i64); metrics::set_gauge(&SLASHER_NUM_ATTESTATIONS_DEFERRED, num_deferred as i64); @@ -194,12 +179,7 @@ impl Slasher { } } - debug!( - self.log, - "Stored attestations in slasher DB"; - "num_stored" => num_stored, - "num_valid" => num_valid, - ); + debug!(num_stored, ?num_valid, "Stored attestations in slasher DB"); metrics::set_gauge( &SLASHER_NUM_ATTESTATIONS_STORED_PER_BATCH, num_stored as i64, @@ -239,19 +219,14 @@ impl Slasher { ) { Ok(slashings) => { if !slashings.is_empty() { - info!( - self.log, - "Found {} new double-vote slashings!", - slashings.len() - ); + info!("Found {} new double-vote slashings!", slashings.len()); } self.attester_slashings.lock().extend(slashings); } Err(e) => { error!( - self.log, - "Error checking for double votes"; - "error" => format!("{:?}", e) + error = ?e, + "Error checking for double votes" ); return Err(e); } @@ -269,20 +244,12 @@ impl Slasher { ) { Ok(slashings) => { if !slashings.is_empty() { - info!( - self.log, - "Found {} new surround slashings!", - slashings.len() - ); + info!("Found {} new surround slashings!", slashings.len()); } self.attester_slashings.lock().extend(slashings); } Err(e) => { - error!( - self.log, - "Error processing array update"; - "error" => format!("{:?}", e), - ); + error!(error = ?e, "Error processing array update"); return Err(e); } } @@ -315,10 +282,9 @@ impl Slasher { if let Some(slashing) = slashing_status.into_slashing(attestation) { debug!( - self.log, - "Found double-vote slashing"; - "validator_index" => validator_index, - "epoch" => slashing.attestation_1().data().target.epoch, + validator_index, + epoch = %slashing.attestation_1().data().target.epoch, + "Found double-vote slashing" ); slashings.insert(slashing); } diff --git a/slasher/tests/attester_slashings.rs b/slasher/tests/attester_slashings.rs index cc6e57d95d7..22c9cfc1288 100644 --- a/slasher/tests/attester_slashings.rs +++ b/slasher/tests/attester_slashings.rs @@ -1,6 +1,5 @@ #![cfg(any(feature = "mdbx", feature = "lmdb", feature = "redb"))] -use logging::test_logger; use maplit::hashset; use rayon::prelude::*; use slasher::{ @@ -272,7 +271,7 @@ fn slasher_test( let tempdir = tempdir().unwrap(); let config = Config::new(tempdir.path().into()); let spec = chain_spec(); - let slasher = Slasher::open(config, spec, test_logger()).unwrap(); + let slasher = Slasher::open(config, spec).unwrap(); let current_epoch = Epoch::new(current_epoch); for (i, attestation) in attestations.iter().enumerate() { @@ -302,7 +301,7 @@ fn parallel_slasher_test( let tempdir = tempdir().unwrap(); let config = Config::new(tempdir.path().into()); let spec = chain_spec(); - let slasher = Slasher::open(config, spec, test_logger()).unwrap(); + let slasher = Slasher::open(config, spec).unwrap(); let current_epoch = Epoch::new(current_epoch); attestations diff --git a/slasher/tests/proposer_slashings.rs b/slasher/tests/proposer_slashings.rs index 6d2a1f5176b..ef525c6f3f9 100644 --- a/slasher/tests/proposer_slashings.rs +++ b/slasher/tests/proposer_slashings.rs @@ -1,6 +1,5 @@ #![cfg(any(feature = "mdbx", feature = "lmdb", feature = "redb"))] -use logging::test_logger; use slasher::{ test_utils::{block as test_block, chain_spec, E}, Config, Slasher, @@ -13,7 +12,7 @@ fn empty_pruning() { let tempdir = tempdir().unwrap(); let config = Config::new(tempdir.path().into()); let spec = chain_spec(); - let slasher = Slasher::::open(config, spec, test_logger()).unwrap(); + let slasher = Slasher::::open(config, spec).unwrap(); slasher.prune_database(Epoch::new(0)).unwrap(); } @@ -27,7 +26,7 @@ fn block_pruning() { config.history_length = 2; let spec = chain_spec(); - let slasher = Slasher::::open(config.clone(), spec, test_logger()).unwrap(); + let slasher = Slasher::::open(config.clone(), spec).unwrap(); let current_epoch = Epoch::from(2 * config.history_length); // Pruning the empty database should be safe. diff --git a/slasher/tests/random.rs b/slasher/tests/random.rs index ff234dff3fe..3270700d881 100644 --- a/slasher/tests/random.rs +++ b/slasher/tests/random.rs @@ -1,6 +1,5 @@ #![cfg(any(feature = "mdbx", feature = "lmdb", feature = "redb"))] -use logging::test_logger; use rand::prelude::*; use slasher::{ test_utils::{ @@ -36,9 +35,8 @@ impl Default for TestConfig { fn make_db() -> (TempDir, SlasherDB) { let tempdir = tempdir().unwrap(); let initial_config = Arc::new(Config::new(tempdir.path().into())); - let logger = test_logger(); let spec = chain_spec(); - let db = SlasherDB::open(initial_config.clone(), spec, logger).unwrap(); + let db = SlasherDB::open(initial_config.clone(), spec).unwrap(); (tempdir, db) } @@ -60,7 +58,7 @@ fn random_test(seed: u64, mut db: SlasherDB, test_config: TestConfig) -> Slas let config = Arc::new(config); db.update_config(config.clone()); - let slasher = Slasher::::from_config_and_db(config.clone(), db, test_logger()).unwrap(); + let slasher = Slasher::::from_config_and_db(config.clone(), db).unwrap(); let validators = (0..num_validators as u64).collect::>(); diff --git a/slasher/tests/wrap_around.rs b/slasher/tests/wrap_around.rs index 2ec56bc7d5d..e34d0f2233c 100644 --- a/slasher/tests/wrap_around.rs +++ b/slasher/tests/wrap_around.rs @@ -1,6 +1,5 @@ #![cfg(any(feature = "mdbx", feature = "lmdb", feature = "redb"))] -use logging::test_logger; use slasher::{ test_utils::{chain_spec, indexed_att}, Config, Slasher, @@ -17,7 +16,7 @@ fn attestation_pruning_empty_wrap_around() { config.chunk_size = 16; config.history_length = 16; - let slasher = Slasher::open(config.clone(), spec, test_logger()).unwrap(); + let slasher = Slasher::open(config.clone(), spec).unwrap(); let v = vec![0]; let history_length = config.history_length as u64; diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index c3835f425e1..b507383190f 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, @@ -374,7 +375,6 @@ impl Tester { } let harness = BeaconChainHarness::>::builder(E::default()) - .logger(logging::test_logger()) .spec(spec.clone()) .keypairs(vec![]) .chain_config(ChainConfig { @@ -520,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/execution_engine_integration/Cargo.toml b/testing/execution_engine_integration/Cargo.toml index 28ff944799c..55c42eb9d36 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 ea143ed4331..8c39fda4e36 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 efb06833f63..d453c415d47 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 f6645093049..b0d115960cb 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,8 +107,7 @@ async fn import_and_unlock(http_url: SensitiveUrl, priv_keys: &[&str], password: } impl TestRig { - pub fn new(generic_engine: Engine) -> Self { - let log = logging::test_logger(); + pub fn new(generic_engine: Engine, use_local_signing: bool) -> Self { let runtime = Arc::new( tokio::runtime::Builder::new_multi_thread() .enable_all() @@ -114,7 +116,12 @@ impl TestRig { ); let (runtime_shutdown, exit) = async_channel::bounded(1); let (shutdown_tx, _) = futures::channel::mpsc::channel(1); - let executor = TaskExecutor::new(Arc::downgrade(&runtime), exit, log.clone(), shutdown_tx); + let executor = TaskExecutor::new( + Arc::downgrade(&runtime), + exit, + shutdown_tx, + "test".to_string(), + ); let mut spec = TEST_FORK.make_genesis_spec(MainnetEthSpec::default_spec()); spec.terminal_total_difficulty = Uint256::ZERO; @@ -131,8 +138,7 @@ impl TestRig { default_datadir: execution_engine.datadir(), ..Default::default() }; - let execution_layer = - ExecutionLayer::from_config(config, executor.clone(), log.clone()).unwrap(); + let execution_layer = ExecutionLayer::from_config(config, executor.clone()).unwrap(); ExecutionPair { execution_engine, execution_layer, @@ -150,8 +156,7 @@ impl TestRig { default_datadir: execution_engine.datadir(), ..Default::default() }; - let execution_layer = - ExecutionLayer::from_config(config, executor, log.clone()).unwrap(); + let execution_layer = ExecutionLayer::from_config(config, executor).unwrap(); ExecutionPair { execution_engine, execution_layer, @@ -164,6 +169,7 @@ impl TestRig { ee_b, spec, _runtime_shutdown: runtime_shutdown, + use_local_signing, } } @@ -195,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()); @@ -234,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 6e632ccf549..4021a6d2c50 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 77645dba457..12b0afcc753 100644 --- a/testing/simulator/Cargo.toml +++ b/testing/simulator/Cargo.toml @@ -8,14 +8,17 @@ edition = { workspace = true } [dependencies] clap = { workspace = true } env_logger = { workspace = true } +environment = { workspace = true } eth2_network_config = { workspace = true } execution_layer = { workspace = true } futures = { workspace = true } kzg = { workspace = true } +logging = { workspace = true } node_test_rig = { path = "../node_test_rig" } parking_lot = { workspace = true } rayon = { workspace = true } sensitive_url = { path = "../../common/sensitive_url" } serde_json = { workspace = true } tokio = { 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 82a70285827..f27fc7f8754 100644 --- a/testing/simulator/src/basic_sim.rs +++ b/testing/simulator/src/basic_sim.rs @@ -11,8 +11,15 @@ 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 logging::build_workspace_filter; use tokio::time::sleep; use types::{Epoch, EthSpec, MinimalEthSpec}; @@ -20,10 +27,9 @@ 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]; @@ -58,6 +64,8 @@ pub fn run_basic_sim(matches: &ArgMatches) -> Result<(), String> { .expect("missing debug-level"); let continue_after_checks = matches.get_flag("continue-after-checks"); + let log_dir = matches.get_one::("log-dir").map(PathBuf::from); + let disable_stdout_logging = matches.get_flag("disable-stdout-logging"); println!("Basic Simulator:"); println!(" nodes: {}", node_count); @@ -65,6 +73,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) @@ -82,23 +92,61 @@ pub fn run_basic_sim(matches: &ArgMatches) -> Result<(), String> { }) .collect::>(); - let mut env = EnvironmentBuilder::minimal() - .initialize_logger(LoggerConfig { - path: None, - debug_level: log_level.clone(), - logfile_debug_level: log_level.clone(), + let ( + env_builder, + logger_config, + stdout_logging_layer, + file_logging_layer, + _sse_logging_layer_opt, + _libp2p_discv5_layer, + ) = tracing_common::construct_logger( + LoggerConfig { + 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: false, + 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, - })? - .multi_threaded_tokio_runtime()? - .build()?; + extra_info: false, + }, + matches, + 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 Err(e) = tracing_subscriber::registry() + .with(logging_layers) + .try_init() + { + eprintln!("Failed to initialize dependency logging: {e}"); + } + + let mut env = env_builder.multi_threaded_tokio_runtime()?.build()?; let mut spec = (*env.eth2_config.spec).clone(); @@ -106,8 +154,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); @@ -118,8 +166,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 35c2508b53c..cd0e2e726e5 100644 --- a/testing/simulator/src/checks.rs +++ b/testing/simulator/src/checks.rs @@ -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))?; diff --git a/testing/simulator/src/cli.rs b/testing/simulator/src/cli.rs index 3d61dcde74a..707baf04a70 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 7d4bdfa264e..b77efbfeae6 100644 --- a/testing/simulator/src/fallback_sim.rs +++ b/testing/simulator/src/fallback_sim.rs @@ -3,6 +3,7 @@ use crate::{checks, LocalNetwork}; use clap::ArgMatches; use crate::retry::with_retry; +use environment::tracing_common; use futures::prelude::*; use node_test_rig::{ environment::{EnvironmentBuilder, LoggerConfig}, @@ -10,11 +11,13 @@ use node_test_rig::{ }; 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_subscriber::prelude::*; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use types::{Epoch, EthSpec, MinimalEthSpec}; - const END_EPOCH: u64 = 16; const GENESIS_DELAY: u64 = 32; const ALTAIR_FORK_EPOCH: u64 = 0; @@ -67,12 +70,18 @@ pub fn run_fallback_sim(matches: &ArgMatches) -> Result<(), String> { let continue_after_checks = matches.get_flag("continue-after-checks"); + let log_dir = matches.get_one::("log-dir").map(PathBuf::from); + + let disable_stdout_logging = matches.get_flag("disable-stdout-logging"); + println!("Fallback Simulator:"); println!(" vc-count: {}", vc_count); println!(" validators-per-vc: {}", validators_per_vc); 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) @@ -89,23 +98,58 @@ pub fn run_fallback_sim(matches: &ArgMatches) -> Result<(), String> { }) .collect::>(); - let mut env = EnvironmentBuilder::minimal() - .initialize_logger(LoggerConfig { - path: None, - debug_level: log_level.clone(), - logfile_debug_level: log_level.clone(), + let ( + env_builder, + logger_config, + stdout_logging_layer, + file_logging_layer, + _sse_logging_layer_opt, + _libp2p_discv5_layer, + ) = tracing_common::construct_logger( + LoggerConfig { + 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: false, + 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, - })? - .multi_threaded_tokio_runtime()? - .build()?; + extra_info: false, + }, + matches, + EnvironmentBuilder::minimal(), + ); + + let mut logging_layers = vec![]; + if !disable_stdout_logging { + logging_layers.push( + stdout_logging_layer + .with_filter(logger_config.debug_level) + .boxed(), + ); + } + if let Some(file_logging_layer) = file_logging_layer { + logging_layers.push( + file_logging_layer + .with_filter(logger_config.logfile_debug_level) + .boxed(), + ); + } + + if let Err(e) = tracing_subscriber::registry() + .with(logging_layers) + .try_init() + { + eprintln!("Failed to initialize dependency logging: {e}"); + } + + let mut env = env_builder.multi_threaded_tokio_runtime()?.build()?; let mut spec = (*env.eth2_config.spec).clone(); diff --git a/testing/simulator/src/local_network.rs b/testing/simulator/src/local_network.rs index a95c15c2313..3914d33f936 100644 --- a/testing/simulator/src/local_network.rs +++ b/testing/simulator/src/local_network.rs @@ -44,7 +44,6 @@ fn default_client_config(network_params: LocalNetworkParams, genesis_time: u64) beacon_config.network.enable_light_client_server = true; beacon_config.network.discv5_config.enable_packet_filter = false; beacon_config.chain.enable_light_client_server = true; - beacon_config.http_api.enable_light_client_server = true; beacon_config.chain.optimistic_finalized_sync = false; beacon_config.trusted_setup = serde_json::from_reader(get_trusted_setup().as_slice()) .expect("Trusted setup bytes should be valid"); diff --git a/testing/test-test_logger/Cargo.toml b/testing/test-test_logger/Cargo.toml deleted file mode 100644 index d2d705f714a..00000000000 --- a/testing/test-test_logger/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "test-test_logger" -version = "0.1.0" -edition = { workspace = true } -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -logging = { workspace = true } -slog = { workspace = true } diff --git a/testing/test-test_logger/src/lib.rs b/testing/test-test_logger/src/lib.rs deleted file mode 100644 index a2e2a80943b..00000000000 --- a/testing/test-test_logger/src/lib.rs +++ /dev/null @@ -1,22 +0,0 @@ -use slog::{info, Logger}; - -pub struct Config { - log: Logger, -} - -pub fn fn_with_logging(config: &Config) { - info!(&config.log, "hi"); -} - -#[cfg(test)] -mod tests { - use super::*; - use logging::test_logger; - - #[test] - fn test_fn_with_logging() { - let config = Config { log: test_logger() }; - - fn_with_logging(&config); - } -} diff --git a/testing/validator_test_rig/Cargo.toml b/testing/validator_test_rig/Cargo.toml index 76560b8afc5..f28a423433b 100644 --- a/testing/validator_test_rig/Cargo.toml +++ b/testing/validator_test_rig/Cargo.toml @@ -5,10 +5,9 @@ edition = { workspace = true } [dependencies] eth2 = { workspace = true } -logging = { workspace = true } mockito = { workspace = true } regex = { workspace = true } sensitive_url = { workspace = true } serde_json = { workspace = true } -slog = { workspace = true } +tracing = { workspace = true } types = { workspace = true } diff --git a/testing/validator_test_rig/src/mock_beacon_node.rs b/testing/validator_test_rig/src/mock_beacon_node.rs index f875116155a..7a902709138 100644 --- a/testing/validator_test_rig/src/mock_beacon_node.rs +++ b/testing/validator_test_rig/src/mock_beacon_node.rs @@ -1,20 +1,18 @@ use eth2::types::{GenericResponse, SyncingData}; use eth2::{BeaconNodeHttpClient, StatusCode, Timeouts}; -use logging::test_logger; use mockito::{Matcher, Mock, Server, ServerGuard}; use regex::Regex; use sensitive_url::SensitiveUrl; -use slog::{info, Logger}; use std::marker::PhantomData; use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::time::Duration; +use tracing::info; use types::{ChainSpec, ConfigAndPreset, EthSpec, SignedBlindedBeaconBlock}; pub struct MockBeaconNode { server: ServerGuard, pub beacon_api_client: BeaconNodeHttpClient, - log: Logger, _phantom: PhantomData, pub received_blocks: Arc>>>, } @@ -27,11 +25,9 @@ impl MockBeaconNode { SensitiveUrl::from_str(&server.url()).unwrap(), Timeouts::set_all(Duration::from_secs(1)), ); - let log = test_logger(); Self { server, beacon_api_client, - log, _phantom: PhantomData, received_blocks: Arc::new(Mutex::new(Vec::new())), } @@ -69,7 +65,6 @@ impl MockBeaconNode { /// Mocks the `post_beacon_blinded_blocks_v2_ssz` response with an optional `delay`. pub fn mock_post_beacon_blinded_blocks_v2_ssz(&mut self, delay: Duration) -> Mock { let path_pattern = Regex::new(r"^/eth/v2/beacon/blinded_blocks$").unwrap(); - let log = self.log.clone(); let url = self.server.url(); let received_blocks = Arc::clone(&self.received_blocks); @@ -80,7 +75,6 @@ impl MockBeaconNode { .with_status(200) .with_body_from_request(move |request| { info!( - log, "{}", format!( "Received published block request on server {} with delay {} s", diff --git a/testing/web3signer_tests/Cargo.toml b/testing/web3signer_tests/Cargo.toml index 376aa13406e..f68fa56e16d 100644 --- a/testing/web3signer_tests/Cargo.toml +++ b/testing/web3signer_tests/Cargo.toml @@ -14,6 +14,7 @@ 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 659495b2b3c..8678eff0ee5 100644 --- a/testing/web3signer_tests/src/lib.rs +++ b/testing/web3signer_tests/src/lib.rs @@ -25,7 +25,7 @@ mod tests { use initialized_validators::{ load_pem_certificate, load_pkcs12_identity, InitializedValidators, }; - use logging::test_logger; + use lighthouse_validator_store::LighthouseValidatorStore; use parking_lot::Mutex; use reqwest::Client; use serde::Serialize; @@ -45,7 +45,7 @@ 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, ValidatorStore}; /// If the we are unable to reach the Web3Signer HTTP API within this time out then we will /// assume it failed to start. @@ -74,6 +74,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 {} @@ -302,7 +303,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<()>, @@ -316,7 +317,6 @@ mod tests { using_web3signer: bool, spec: Arc, ) -> Self { - let log = test_logger(); let validator_dir = TempDir::new().unwrap(); let config = initialized_validators::Config::default(); @@ -325,7 +325,6 @@ mod tests { validator_definitions, validator_dir.path().into(), config.clone(), - log.clone(), ) .await .unwrap(); @@ -340,8 +339,12 @@ mod tests { ); let (runtime_shutdown, exit) = async_channel::bounded(1); let (shutdown_tx, _) = futures::channel::mpsc::channel(1); - let executor = - TaskExecutor::new(Arc::downgrade(&runtime), exit, log.clone(), shutdown_tx); + let executor = TaskExecutor::new( + Arc::downgrade(&runtime), + exit, + shutdown_tx, + "test".to_string(), + ); let slashing_db_path = validator_dir.path().join(SLASHING_PROTECTION_FILENAME); let slashing_protection = SlashingDatabase::open_or_create(&slashing_db_path).unwrap(); @@ -351,12 +354,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), @@ -365,7 +368,6 @@ mod tests { slot_clock, &config, executor, - log.clone(), ); Self { @@ -481,7 +483,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 +518,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 +593,10 @@ 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(); validator_store - .sign_block(pubkey, block, block_slot) + .sign_block(pubkey, block.into(), block_slot) .await .unwrap() } @@ -664,7 +666,11 @@ mod tests { let mut altair_block = BeaconBlockAltair::empty(&spec); altair_block.slot = altair_fork_slot; validator_store - .sign_block(pubkey, BeaconBlock::Altair(altair_block), altair_fork_slot) + .sign_block( + pubkey, + BeaconBlock::::Altair(altair_block).into(), + altair_fork_slot, + ) .await .unwrap() } @@ -749,7 +755,7 @@ mod tests { validator_store .sign_block( pubkey, - BeaconBlock::Bellatrix(bellatrix_block), + BeaconBlock::::Bellatrix(bellatrix_block).into(), bellatrix_fork_slot, ) .await @@ -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) }; @@ -871,7 +877,7 @@ mod tests { let block = first_block(); let slot = block.slot(); validator_store - .sign_block(pubkey, block, slot) + .sign_block(pubkey, block.into(), slot) .await .unwrap() }) @@ -882,7 +888,7 @@ mod tests { let block = double_vote_block(); let slot = block.slot(); validator_store - .sign_block(pubkey, block, slot) + .sign_block(pubkey, block.into(), slot) .await .map(|_| ()) }, diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index fb6007b00a6..a8c8fd59f13 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 } @@ -29,9 +30,9 @@ reqwest = { workspace = true } sensitive_url = { workspace = true } serde = { workspace = true } slashing_protection = { workspace = true } -slog = { workspace = true } slot_clock = { workspace = true } tokio = { workspace = true } +tracing = { workspace = true } types = { workspace = true } validator_http_api = { workspace = true } validator_http_metrics = { workspace = true } diff --git a/validator_client/beacon_node_fallback/Cargo.toml b/validator_client/beacon_node_fallback/Cargo.toml index 598020d1379..3bcb0d7034c 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 } -slog = { 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/beacon_node_health.rs b/validator_client/beacon_node_fallback/src/beacon_node_health.rs index 80d3fb7efd7..1b5d5b98cb8 100644 --- a/validator_client/beacon_node_fallback/src/beacon_node_health.rs +++ b/validator_client/beacon_node_fallback/src/beacon_node_health.rs @@ -1,9 +1,9 @@ use super::CandidateError; use eth2::BeaconNodeHttpClient; use serde::{Deserialize, Serialize}; -use slog::{warn, Logger}; use std::cmp::Ordering; use std::fmt::{Debug, Display, Formatter}; +use tracing::warn; use types::Slot; /// Sync distances between 0 and DEFAULT_SYNC_TOLERANCE are considered `synced`. @@ -276,15 +276,13 @@ impl BeaconNodeHealth { pub async fn check_node_health( beacon_node: &BeaconNodeHttpClient, - log: &Logger, ) -> Result<(Slot, bool, bool), CandidateError> { let resp = match beacon_node.get_node_syncing().await { Ok(resp) => resp, Err(e) => { warn!( - log, - "Unable connect to beacon node"; - "error" => %e + error = %e, + "Unable connect to beacon node" ); return Err(CandidateError::Offline); diff --git a/validator_client/beacon_node_fallback/src/lib.rs b/validator_client/beacon_node_fallback/src/lib.rs index abcf74a1a62..8d022f8e758 100644 --- a/validator_client/beacon_node_fallback/src/lib.rs +++ b/validator_client/beacon_node_fallback/src/lib.rs @@ -8,22 +8,21 @@ 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}; -use slog::{debug, error, warn, Logger}; use slot_clock::SlotClock; 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}; use validator_metrics::{inc_counter_vec, ENDPOINT_ERRORS, ENDPOINT_REQUESTS}; @@ -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,20 +213,19 @@ 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, - log: &Logger, ) -> Result<(), CandidateError> { - if let Err(e) = self.is_compatible(spec, log).await { + if let Err(e) = self.is_compatible::(spec).await { *self.health.write().await = Err(e); return Err(e); } if let Some(slot_clock) = slot_clock { - match check_node_health(&self.beacon_node, log).await { + match check_node_health(&self.beacon_node).await { Ok((head, is_optimistic, el_offline)) => { let Some(slot_clock_head) = slot_clock.now() else { let e = match slot_clock.is_prior_to_genesis() { @@ -288,17 +283,16 @@ impl CandidateBeaconNode { } /// Checks if the node has the correct specification. - async fn is_compatible(&self, spec: &ChainSpec, log: &Logger) -> Result<(), CandidateError> { + async fn is_compatible(&self, spec: &ChainSpec) -> Result<(), CandidateError> { let config = self .beacon_node .get_config_spec::() .await .map_err(|e| { error!( - log, - "Unable to read spec from beacon node"; - "error" => %e, - "endpoint" => %self.beacon_node, + error = %e, + endpoint = %self.beacon_node, + "Unable to read spec from beacon node" ); CandidateError::Offline })? @@ -306,71 +300,64 @@ impl CandidateBeaconNode { let beacon_node_spec = ChainSpec::from_config::(&config).ok_or_else(|| { error!( - log, + endpoint = %self.beacon_node, "The minimal/mainnet spec type of the beacon node does not match the validator \ - client. See the --network command."; - "endpoint" => %self.beacon_node, + client. See the --network command." + ); CandidateError::Incompatible })?; if beacon_node_spec.genesis_fork_version != spec.genesis_fork_version { error!( - log, - "Beacon node is configured for a different network"; - "endpoint" => %self.beacon_node, - "bn_genesis_fork" => ?beacon_node_spec.genesis_fork_version, - "our_genesis_fork" => ?spec.genesis_fork_version, + endpoint = %self.beacon_node, + bn_genesis_fork = ?beacon_node_spec.genesis_fork_version, + our_genesis_fork = ?spec.genesis_fork_version, + "Beacon node is configured for a different network" ); return Err(CandidateError::Incompatible); } else if beacon_node_spec.altair_fork_epoch != spec.altair_fork_epoch { warn!( - log, - "Beacon node has mismatched Altair fork epoch"; - "endpoint" => %self.beacon_node, - "endpoint_altair_fork_epoch" => ?beacon_node_spec.altair_fork_epoch, - "hint" => UPDATE_REQUIRED_LOG_HINT, + endpoint = %self.beacon_node, + endpoint_altair_fork_epoch = ?beacon_node_spec.altair_fork_epoch, + hint = UPDATE_REQUIRED_LOG_HINT, + "Beacon node has mismatched Altair fork epoch" ); } else if beacon_node_spec.bellatrix_fork_epoch != spec.bellatrix_fork_epoch { warn!( - log, - "Beacon node has mismatched Bellatrix fork epoch"; - "endpoint" => %self.beacon_node, - "endpoint_bellatrix_fork_epoch" => ?beacon_node_spec.bellatrix_fork_epoch, - "hint" => UPDATE_REQUIRED_LOG_HINT, + endpoint = %self.beacon_node, + endpoint_bellatrix_fork_epoch = ?beacon_node_spec.bellatrix_fork_epoch, + hint = UPDATE_REQUIRED_LOG_HINT, + "Beacon node has mismatched Bellatrix fork epoch" ); } else if beacon_node_spec.capella_fork_epoch != spec.capella_fork_epoch { warn!( - log, - "Beacon node has mismatched Capella fork epoch"; - "endpoint" => %self.beacon_node, - "endpoint_capella_fork_epoch" => ?beacon_node_spec.capella_fork_epoch, - "hint" => UPDATE_REQUIRED_LOG_HINT, + endpoint = %self.beacon_node, + endpoint_capella_fork_epoch = ?beacon_node_spec.capella_fork_epoch, + hint = UPDATE_REQUIRED_LOG_HINT, + "Beacon node has mismatched Capella fork epoch" ); } else if beacon_node_spec.deneb_fork_epoch != spec.deneb_fork_epoch { warn!( - log, - "Beacon node has mismatched Deneb fork epoch"; - "endpoint" => %self.beacon_node, - "endpoint_deneb_fork_epoch" => ?beacon_node_spec.deneb_fork_epoch, - "hint" => UPDATE_REQUIRED_LOG_HINT, + endpoint = %self.beacon_node, + endpoint_deneb_fork_epoch = ?beacon_node_spec.deneb_fork_epoch, + hint = UPDATE_REQUIRED_LOG_HINT, + "Beacon node has mismatched Deneb fork epoch" ); } else if beacon_node_spec.electra_fork_epoch != spec.electra_fork_epoch { warn!( - log, - "Beacon node has mismatched Electra fork epoch"; - "endpoint" => %self.beacon_node, - "endpoint_electra_fork_epoch" => ?beacon_node_spec.electra_fork_epoch, - "hint" => UPDATE_REQUIRED_LOG_HINT, + endpoint = %self.beacon_node, + endpoint_electra_fork_epoch = ?beacon_node_spec.electra_fork_epoch, + hint = UPDATE_REQUIRED_LOG_HINT, + "Beacon node has mismatched Electra fork epoch" ); } else if beacon_node_spec.fulu_fork_epoch != spec.fulu_fork_epoch { warn!( - log, - "Beacon node has mismatched Fulu fork epoch"; - "endpoint" => %self.beacon_node, - "endpoint_fulu_fork_epoch" => ?beacon_node_spec.fulu_fork_epoch, - "hint" => UPDATE_REQUIRED_LOG_HINT, - ); + endpoint = %self.beacon_node, + endpoint_fulu_fork_epoch = ?beacon_node_spec.fulu_fork_epoch, + hint = UPDATE_REQUIRED_LOG_HINT, + "Beacon node has mismatched Fulu fork epoch" + ); } Ok(()) @@ -381,22 +368,20 @@ 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, - log: Logger, } -impl BeaconNodeFallback { +impl BeaconNodeFallback { pub fn new( - candidates: Vec>, + candidates: Vec, config: Config, broadcast_topics: Vec, spec: Arc, - log: Logger, ) -> Self { let distance_tiers = config.sync_tolerances; Self { @@ -405,7 +390,6 @@ impl BeaconNodeFallback { slot_clock: None, broadcast_topics, spec, - log, } } @@ -476,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(); @@ -484,11 +468,10 @@ 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, - &self.log, )); nodes.push(candidate.beacon_node.to_string()); } @@ -501,10 +484,9 @@ impl BeaconNodeFallback { if let Err(e) = result { if *e != CandidateError::PreGenesis { warn!( - self.log, - "A connected beacon node errored during routine health check"; - "error" => ?e, - "endpoint" => node, + error = ?e, + endpoint = %node, + "A connected beacon node errored during routine health check" ); } } @@ -576,11 +558,7 @@ impl BeaconNodeFallback { // Run `func` using a `candidate`, returning the value or capturing errors. for candidate in candidates.iter() { - futures.push(Self::run_on_candidate( - candidate.beacon_node.clone(), - &func, - &self.log, - )); + futures.push(Self::run_on_candidate(candidate.beacon_node.clone(), &func)); } drop(candidates); @@ -598,11 +576,7 @@ impl BeaconNodeFallback { // Run `func` using a `candidate`, returning the value or capturing errors. for candidate in candidates.iter() { - futures.push(Self::run_on_candidate( - candidate.beacon_node.clone(), - &func, - &self.log, - )); + futures.push(Self::run_on_candidate(candidate.beacon_node.clone(), &func)); } drop(candidates); @@ -621,7 +595,6 @@ impl BeaconNodeFallback { async fn run_on_candidate( candidate: BeaconNodeHttpClient, func: F, - log: &Logger, ) -> Result)> where F: Fn(BeaconNodeHttpClient) -> R, @@ -636,10 +609,9 @@ impl BeaconNodeFallback { Ok(val) => Ok(val), Err(e) => { debug!( - log, - "Request to beacon node failed"; - "node" => %candidate, - "error" => ?e, + node = %candidate, + error = ?e, + "Request to beacon node failed" ); inc_counter_vec(&ENDPOINT_ERRORS, &[candidate.as_ref()]); Err((candidate.to_string(), Error::RequestFailed(e))) @@ -666,11 +638,7 @@ impl BeaconNodeFallback { // Run `func` using a `candidate`, returning the value or capturing errors. for candidate in candidates.iter() { - futures.push(Self::run_on_candidate( - candidate.beacon_node.clone(), - &func, - &self.log, - )); + futures.push(Self::run_on_candidate(candidate.beacon_node.clone(), &func)); } drop(candidates); @@ -703,7 +671,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; @@ -721,7 +689,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(); @@ -752,7 +720,6 @@ mod tests { use crate::beacon_node_health::BeaconNodeHealthTier; use eth2::SensitiveUrl; use eth2::Timeouts; - use logging::test_logger; use slot_clock::TestingSlotClock; use strum::VariantNames; use types::{BeaconBlockDeneb, MainnetEthSpec, Slot}; @@ -781,7 +748,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)), @@ -888,24 +855,23 @@ 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, - log: Logger, - ) -> BeaconNodeFallback { + ) -> BeaconNodeFallback { let mut beacon_node_fallback = - BeaconNodeFallback::new(candidates, Config::default(), topics, spec, log); + BeaconNodeFallback::new(candidates, Config::default(), topics, spec); beacon_node_fallback.set_slot_clock(TestingSlotClock::new( Slot::new(1), @@ -932,7 +898,6 @@ mod tests { ], vec![], spec.clone(), - test_logger(), ); // BeaconNodeHealthTier 1 @@ -960,7 +925,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!( @@ -979,7 +944,6 @@ mod tests { vec![beacon_node_1, beacon_node_2], vec![ApiTopic::Blocks], spec.clone(), - test_logger(), ); mock_beacon_node_1.mock_post_beacon_blinded_blocks_v2_ssz(Duration::from_secs(0)); @@ -1021,7 +985,6 @@ mod tests { vec![beacon_node_1, beacon_node_2, beacon_node_3], vec![], spec.clone(), - test_logger(), ); let mock1 = mock_beacon_node_1.mock_offline_node(); diff --git a/validator_client/doppelganger_service/Cargo.toml b/validator_client/doppelganger_service/Cargo.toml index 66b61a411b6..e5b183570de 100644 --- a/validator_client/doppelganger_service/Cargo.toml +++ b/validator_client/doppelganger_service/Cargo.toml @@ -8,12 +8,14 @@ authors = ["Sigma Prime "] beacon_node_fallback = { workspace = true } environment = { workspace = true } eth2 = { workspace = true } +logging = { workspace = true } parking_lot = { workspace = true } -slog = { workspace = true } slot_clock = { workspace = true } 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 4a593c2700e..e3c7ce78b44 100644 --- a/validator_client/doppelganger_service/src/lib.rs +++ b/validator_client/doppelganger_service/src/lib.rs @@ -32,77 +32,17 @@ use beacon_node_fallback::BeaconNodeFallback; use environment::RuntimeContext; use eth2::types::LivenessResponseData; +use logging::crit; use parking_lot::RwLock; -use slog::{crit, error, info, Logger}; use slot_clock::SlotClock; use std::collections::{HashMap, HashSet}; use std::future::Future; use std::sync::Arc; 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, @@ -113,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 { @@ -162,9 +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>, - log: Logger, +async fn beacon_node_liveness( + beacon_nodes: Arc>, current_epoch: Epoch, validator_indices: Vec, ) -> LivenessResponses { @@ -203,10 +135,9 @@ async fn beacon_node_liveness( .await .unwrap_or_else(|e| { crit!( - log, - "Failed previous epoch liveness query"; - "error" => %e, - "previous_epoch" => %previous_epoch, + error = %e, + previous_epoch = %previous_epoch, + "Failed previous epoch liveness query" ); // Return an empty vec. In effect, this means to keep trying to make doppelganger // progress even if some of the calls are failing. @@ -239,10 +170,9 @@ async fn beacon_node_liveness( .await .unwrap_or_else(|e| { crit!( - log, - "Failed current epoch liveness query"; - "error" => %e, - "current_epoch" => %current_epoch, + error = %e, + current_epoch = %current_epoch, + "Failed current epoch liveness query" ); // Return an empty vec. In effect, this means to keep trying to make doppelganger // progress even if some of the calls are failing. @@ -257,11 +187,10 @@ async fn beacon_node_liveness( || current_epoch_responses.len() != previous_epoch_responses.len() { error!( - log, - "Liveness query omitted validators"; - "previous_epoch_response" => previous_epoch_responses.len(), - "current_epoch_response" => current_epoch_responses.len(), - "requested" => validator_indices.len(), + previous_epoch_response = previous_epoch_responses.len(), + current_epoch_response = current_epoch_responses.len(), + requested = validator_indices.len(), + "Liveness query omitted validators" ) } @@ -271,66 +200,49 @@ async fn beacon_node_liveness( } } +#[derive(Default)] pub struct DoppelgangerService { doppelganger_states: RwLock>, - log: Logger, } impl DoppelgangerService { - pub fn new(log: Logger) -> Self { - Self { - doppelganger_states: <_>::default(), - log, - } - } - /// Starts a reoccurring future which will try to keep the doppelganger service updated each /// slot. pub fn start_update_service( 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 log = service.log.clone(); let get_liveness = move |current_epoch, validator_indices| { - beacon_node_liveness( - beacon_nodes.clone(), - log.clone(), - current_epoch, - validator_indices, - ) + beacon_node_liveness::(beacon_nodes.clone(), current_epoch, validator_indices) }; let mut shutdown_sender = context.executor.shutdown_sender(); - let log = service.log.clone(); + let mut shutdown_func = move || { if let Err(e) = shutdown_sender.try_send(ShutdownReason::Failure("Doppelganger detected.")) { crit!( - log, - "Failed to send shutdown signal"; - "msg" => "terminate this process immediately", - "error" => ?e + msg = "terminate this process immediately", + error = ?e, + "Failed to send shutdown signal" ); } }; - info!( - service.log, - "Doppelganger detection service started"; - ); + info!("Doppelganger detection service started"); context.executor.spawn( async move { @@ -360,9 +272,8 @@ impl DoppelgangerService { .await { error!( - service.log, - "Error during doppelganger detection"; - "error" => ?e + error = ?e, + "Error during doppelganger detection" ); } } @@ -387,10 +298,9 @@ impl DoppelgangerService { }) .unwrap_or_else(|| { crit!( - self.log, - "Validator unknown to doppelganger service"; - "msg" => "preventing validator from performing duties", - "pubkey" => ?validator + msg = "preventing validator from performing duties", + pubkey = ?validator, + "Validator unknown to doppelganger service" ); DoppelgangerStatus::UnknownToDoppelganger(validator) }) @@ -400,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. @@ -552,11 +463,7 @@ impl DoppelgangerService { // Resolve the index from the server response back to a public key. let Some(pubkey) = indices_map.get(&response.index) else { - crit!( - self.log, - "Inconsistent indices map"; - "validator_index" => response.index, - ); + crit!(validator_index = response.index, "Inconsistent indices map"); // Skip this result if an inconsistency is detected. continue; }; @@ -566,9 +473,8 @@ impl DoppelgangerService { state.next_check_epoch } else { crit!( - self.log, - "Inconsistent doppelganger state"; - "validator_pubkey" => ?pubkey, + validator_pubkey = ?pubkey, + "Inconsistent doppelganger state" ); // Skip this result if an inconsistency is detected. continue; @@ -582,15 +488,14 @@ impl DoppelgangerService { let violators_exist = !violators.is_empty(); if violators_exist { crit!( - self.log, - "Doppelganger(s) detected"; - "msg" => "A doppelganger occurs when two different validator clients run the \ - same public key. This validator client detected another instance of a local \ - validator on the network and is shutting down to prevent potential slashable \ - offences. Ensure that you are not running a duplicate or overlapping \ - validator client", - "doppelganger_indices" => ?violators - ) + msg = "A doppelganger occurs when two different validator clients run the \ + same public key. This validator client detected another instance of a local \ + validator on the network and is shutting down to prevent potential slashable \ + offences. Ensure that you are not running a duplicate or overlapping \ + validator client", + doppelganger_indices = ?violators, + "Doppelganger(s) detected" + ); } // The concept of "epoch satisfaction" is that for some epoch `e` we are *satisfied* that @@ -665,19 +570,17 @@ impl DoppelgangerService { doppelganger_state.complete_detection_in_epoch(previous_epoch); info!( - self.log, - "Found no doppelganger"; - "further_checks_remaining" => doppelganger_state.remaining_epochs, - "epoch" => response.epoch, - "validator_index" => response.index + further_checks_remaining = doppelganger_state.remaining_epochs, + epoch = %response.epoch, + validator_index = response.index, + "Found no doppelganger" ); if doppelganger_state.remaining_epochs == 0 { info!( - self.log, - "Doppelganger detection complete"; - "msg" => "starting validator", - "validator_index" => response.index + msg = "starting validator", + validator_index = response.index, + "Doppelganger detection complete" ); } } @@ -696,7 +599,6 @@ impl DoppelgangerService { mod test { use super::*; use futures::executor::block_on; - use logging::test_logger; use slot_clock::TestingSlotClock; use std::future; use std::time::Duration; @@ -704,6 +606,7 @@ mod test { test_utils::{SeedableRng, TestRandom, XorShiftRng}, MainnetEthSpec, }; + use validator_store::DoppelgangerStatus; const DEFAULT_VALIDATORS: usize = 8; @@ -740,13 +643,12 @@ mod test { fn build(self) -> TestScenario { let mut rng = XorShiftRng::from_seed([42; 16]); let slot_clock = TestingSlotClock::new(Slot::new(0), GENESIS_TIME, SLOT_DURATION); - let log = test_logger(); TestScenario { validators: (0..self.validator_count) .map(|_| PublicKeyBytes::random_for_test(&mut rng)) .collect(), - doppelganger: DoppelgangerService::new(log), + doppelganger: DoppelgangerService::default(), slot_clock, } } @@ -805,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/graffiti_file/Cargo.toml b/validator_client/graffiti_file/Cargo.toml index 8868f5aec81..b3bbeb1fd78 100644 --- a/validator_client/graffiti_file/Cargo.toml +++ b/validator_client/graffiti_file/Cargo.toml @@ -11,7 +11,7 @@ path = "src/lib.rs" [dependencies] bls = { workspace = true } serde = { workspace = true } -slog = { workspace = true } +tracing = { workspace = true } types = { workspace = true } [dev-dependencies] diff --git a/validator_client/graffiti_file/src/lib.rs b/validator_client/graffiti_file/src/lib.rs index 9dab2e78272..86f582aa38e 100644 --- a/validator_client/graffiti_file/src/lib.rs +++ b/validator_client/graffiti_file/src/lib.rs @@ -1,12 +1,11 @@ +use bls::PublicKeyBytes; use serde::{Deserialize, Serialize}; -use slog::warn; use std::collections::HashMap; use std::fs::File; use std::io::{prelude::*, BufReader}; use std::path::PathBuf; use std::str::FromStr; - -use bls::PublicKeyBytes; +use tracing::warn; use types::{graffiti::GraffitiString, Graffiti}; #[derive(Debug)] @@ -108,7 +107,6 @@ fn read_line(line: &str) -> Result<(Option, Graffiti), Error> { // the next block produced by the validator with the given public key. pub fn determine_graffiti( validator_pubkey: &PublicKeyBytes, - log: &slog::Logger, graffiti_file: Option, validator_definition_graffiti: Option, graffiti_flag: Option, @@ -117,7 +115,7 @@ pub fn determine_graffiti( .and_then(|mut g| match g.load_graffiti(validator_pubkey) { Ok(g) => g, Err(e) => { - warn!(log, "Failed to read graffiti file"; "error" => ?e); + warn!(error = ?e, "Failed to read graffiti file"); None } }) diff --git a/validator_client/http_api/Cargo.toml b/validator_client/http_api/Cargo.toml index 651e658a7a5..588aa2ca931 100644 --- a/validator_client/http_api/Cargo.toml +++ b/validator_client/http_api/Cargo.toml @@ -16,34 +16,36 @@ 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 } rand = { workspace = true } sensitive_url = { workspace = true } serde = { workspace = true } +serde_json = { workspace = true } signing_method = { workspace = true } slashing_protection = { workspace = true } -slog = { 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 } -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 32269b202b0..b536a6aa7ad 100644 --- a/validator_client/http_api/src/create_signed_voluntary_exit.rs +++ b/validator_client/http_api/src/create_signed_voluntary_exit.rs @@ -1,17 +1,17 @@ use bls::{PublicKey, PublicKeyBytes}; use eth2::types::GenericResponse; -use slog::{info, Logger}; +use lighthouse_validator_store::LighthouseValidatorStore; use slot_clock::SlotClock; use std::sync::Arc; +use tracing::info; use types::{Epoch, EthSpec, SignedVoluntaryExit, VoluntaryExit}; 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, - log: Logger, ) -> Result, warp::Rejection> { let epoch = match maybe_epoch { Some(epoch) => epoch, @@ -45,10 +45,9 @@ pub async fn create_signed_voluntary_exit pubkey_bytes.as_hex_string(), - "epoch" => epoch + validator = pubkey_bytes.as_hex_string(), + %epoch, + "Signing voluntary exit" ); let signed_voluntary_exit = validator_store diff --git a/validator_client/http_api/src/create_validator.rs b/validator_client/http_api/src/create_validator.rs index f90a1057a43..278274198d5 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 86238a697c6..4372b14b04a 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 fd6b4fdae51..302b21d7d8e 100644 --- a/validator_client/http_api/src/keystores.rs +++ b/validator_client/http_api/src/keystores.rs @@ -10,22 +10,22 @@ use eth2::lighthouse_vc::{ }; use eth2_keystore::Keystore; use initialized_validators::{Error, InitializedValidators}; +use lighthouse_validator_store::LighthouseValidatorStore; use signing_method::SigningMethod; -use slog::{info, warn, Logger}; use slot_clock::SlotClock; use std::path::PathBuf; use std::sync::Arc; use task_executor::TaskExecutor; use tokio::runtime::Handle; +use tracing::{info, warn}; use types::{EthSpec, PublicKeyBytes}; use validator_dir::{keystore_password_path, Builder as ValidatorDirBuilder}; -use 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,9 +62,8 @@ pub fn import( request: ImportKeystoresRequest, validator_dir: PathBuf, secrets_dir: Option, - validator_store: Arc>, + validator_store: Arc>, task_executor: TaskExecutor, - log: Logger, ) -> Result { // Check request validity. This is the only cases in which we should return a 4xx code. if request.keystores.len() != request.passwords.len() { @@ -88,18 +87,14 @@ pub fn import( .iter() .any(|data| data.pubkey == pubkey_bytes) { - warn!( - log, - "Slashing protection data not provided"; - "public_key" => ?public_key, - ); + warn!(?public_key, "Slashing protection data not provided"); } } } validator_store.import_slashing_protection(slashing_protection) } else { - warn!(log, "No slashing protection data provided with keystores"); + warn!("No slashing protection data provided with keystores"); Ok(()) }; @@ -122,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(), @@ -133,10 +128,9 @@ pub fn import( Ok(status) => Status::ok(status), Err(e) => { warn!( - log, - "Error importing keystore, skipped"; - "pubkey" => pubkey_str, - "error" => ?e, + pubkey = pubkey_str, + error = ?e, + "Error importing keystore, skipped" ); Status::error(ImportKeystoreStatus::Error, e) } @@ -157,9 +151,8 @@ pub fn import( if successful_import > 0 { info!( - log, - "Imported keystores via standard HTTP API"; - "count" => successful_import, + count = successful_import, + "Imported keystores via standard HTTP API" ); } @@ -171,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. @@ -241,11 +234,10 @@ fn import_single_keystore( pub fn delete( request: DeleteKeystoresRequest, - validator_store: Arc>, + validator_store: Arc>, task_executor: TaskExecutor, - log: Logger, ) -> Result { - let export_response = export(request, validator_store, task_executor, log.clone())?; + let export_response = export(request, validator_store, task_executor)?; // Check the status is Deleted to confirm deletion is successful, then only display the log let successful_deletion = export_response @@ -256,9 +248,8 @@ pub fn delete( if successful_deletion > 0 { info!( - log, - "Deleted keystore via standard HTTP API"; - "count" => successful_deletion, + count = successful_deletion, + "Deleted keystore via standard HTTP API" ); } @@ -274,9 +265,8 @@ pub fn delete( pub fn export( request: DeleteKeystoresRequest, - validator_store: Arc>, + validator_store: Arc>, task_executor: TaskExecutor, - log: Logger, ) -> Result { // Remove from initialized validators. let initialized_validators_rwlock = validator_store.initialized_validators(); @@ -294,10 +284,9 @@ pub fn export( Ok(status) => status, Err(error) => { warn!( - log, - "Error deleting keystore"; - "pubkey" => ?pubkey_bytes, - "error" => ?error, + pubkey = ?pubkey_bytes, + ?error, + "Error deleting keystore" ); SingleExportKeystoresResponse { status: Status::error(DeleteKeystoreStatus::Error, error), diff --git a/validator_client/http_api/src/lib.rs b/validator_client/http_api/src/lib.rs index ae50b6a927a..aebe179567e 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::{ @@ -34,14 +35,13 @@ use eth2::lighthouse_vc::{ }; use health_metrics::observe::Observe; use lighthouse_version::version_with_platform; +use logging::crit; use logging::SSELoggingComponents; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; -use slog::{crit, info, warn, Logger}; 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; @@ -49,6 +49,7 @@ use sysinfo::{System, SystemExt}; use system_health::observe_system_health_vc; use task_executor::TaskExecutor; use tokio_stream::{wrappers::BroadcastStream, StreamExt}; +use tracing::{info, warn}; use types::{ChainSpec, ConfigAndPreset, EthSpec}; use validator_dir::Builder as ValidatorDirBuilder; use validator_services::block_service::BlockService; @@ -76,21 +77,19 @@ 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, pub graffiti_flag: Option, pub spec: Arc, pub config: Config, - pub log: Logger, pub sse_logging_components: Option, pub slot_clock: T, - pub _phantom: PhantomData, } /// Configuration for the HTTP server. @@ -148,7 +147,6 @@ pub fn serve( let config = &ctx.config; let allow_keystore_export = config.allow_keystore_export; let store_passwords_in_secrets_dir = config.store_passwords_in_secrets_dir; - let log = ctx.log.clone(); // Configure CORS. let cors_builder = { @@ -165,7 +163,7 @@ pub fn serve( // Sanity check. if !config.enabled { - crit!(log, "Cannot start disabled metrics HTTP server"); + crit!("Cannot start disabled metrics HTTP server"); return Err(Error::Other( "A disabled metrics server should not be started".to_string(), )); @@ -179,9 +177,8 @@ pub fn serve( Ok(abs_path) => api_token_path = abs_path, Err(e) => { warn!( - log, - "Error canonicalizing token path"; - "error" => ?e, + error = ?e, + "Error canonicalizing token path" ); } }; @@ -239,9 +236,6 @@ pub fn serve( let inner_graffiti_flag = ctx.graffiti_flag; let graffiti_flag_filter = warp::any().map(move || inner_graffiti_flag); - let inner_ctx = ctx.clone(); - let log_filter = warp::any().map(move || inner_ctx.log.clone()); - let inner_slot_clock = ctx.slot_clock.clone(); let slot_clock_filter = warp::any().map(move || inner_slot_clock.clone()); @@ -325,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() @@ -350,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() @@ -399,12 +393,10 @@ pub fn serve( .and(validator_store_filter.clone()) .and(graffiti_file_filter.clone()) .and(graffiti_flag_filter) - .and(log_filter.clone()) .then( - |validator_store: Arc>, + |validator_store: Arc>, graffiti_file: Option, - graffiti_flag: Option, - log| { + graffiti_flag: Option| { blocking_json_task(move || { let mut result = HashMap::new(); for (key, graffiti_definition) in validator_store @@ -414,7 +406,6 @@ pub fn serve( { let graffiti = determine_graffiti( key, - &log, graffiti_file.clone(), graffiti_definition, graffiti_flag, @@ -426,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(); - - 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 { + .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("proposer_nodes".to_string(), proposer_nodes); - } + 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 { + 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 - }); + blocking_json_task(move || Ok(api_types::GenericResponse::from(result))).await + }, + ); // POST lighthouse/validators/ let post_validators = warp::path("lighthouse") @@ -474,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, @@ -519,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 || { @@ -533,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, @@ -566,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. @@ -652,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() { @@ -680,7 +673,7 @@ pub fn serve( ), }) .collect(); - handle.block_on(create_validators_web3signer( + handle.block_on(create_validators_web3signer::<_, E>( web3signers, &validator_store, ))?; @@ -706,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 || { @@ -834,11 +827,10 @@ pub fn serve( .and(warp::body::json()) .and(validator_store_filter.clone()) .and(task_executor_filter.clone()) - .and(log_filter.clone()) - .then(move |request, validator_store, task_executor, log| { + .then(move |request, validator_store, task_executor| { blocking_json_task(move || { if allow_keystore_export { - keystores::export(request, validator_store, task_executor, log) + keystores::export(request, validator_store, task_executor) } else { Err(warp_utils::reject::custom_bad_request( "keystore export is disabled".to_string(), @@ -860,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() @@ -901,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() @@ -937,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() @@ -973,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() @@ -1006,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() @@ -1042,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() @@ -1079,24 +1071,21 @@ pub fn serve( .and(warp::path::end()) .and(validator_store_filter.clone()) .and(slot_clock_filter) - .and(log_filter.clone()) .and(task_executor_filter.clone()) .then( |pubkey: PublicKey, query: api_types::VoluntaryExitQuery, - validator_store: Arc>, + validator_store: Arc>, slot_clock: T, - log, 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, slot_clock, - log, ))?; Ok(signed_voluntary_exit) } else { @@ -1118,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)?; @@ -1142,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() { @@ -1167,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() { @@ -1184,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))) }, ); @@ -1196,18 +1185,16 @@ pub fn serve( .and(secrets_dir_filter) .and(validator_store_filter.clone()) .and(task_executor_filter.clone()) - .and(log_filter.clone()) .then( - move |request, validator_dir, secrets_dir, validator_store, task_executor, log| { + 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, validator_store, task_executor, - log, ) }) }, @@ -1218,16 +1205,13 @@ pub fn serve( .and(warp::body::json()) .and(validator_store_filter.clone()) .and(task_executor_filter.clone()) - .and(log_filter.clone()) - .then(|request, validator_store, task_executor, log| { - blocking_json_task(move || { - keystores::delete(request, validator_store, task_executor, log) - }) + .then(|request, validator_store, task_executor| { + blocking_json_task(move || keystores::delete(request, validator_store, task_executor)) }); // 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))) }, ); @@ -1237,10 +1221,9 @@ pub fn serve( .and(warp::body::json()) .and(validator_store_filter.clone()) .and(task_executor_filter.clone()) - .and(log_filter.clone()) - .then(|request, validator_store, task_executor, log| { + .then(|request, validator_store, task_executor| { blocking_json_task(move || { - remotekeys::import(request, validator_store, task_executor, log) + remotekeys::import::<_, E>(request, validator_store, task_executor) }) }); @@ -1249,11 +1232,8 @@ pub fn serve( .and(warp::body::json()) .and(validator_store_filter) .and(task_executor_filter) - .and(log_filter.clone()) - .then(|request, validator_store, task_executor, log| { - blocking_json_task(move || { - remotekeys::delete(request, validator_store, task_executor, log) - }) + .then(|request, validator_store, task_executor| { + blocking_json_task(move || remotekeys::delete(request, validator_store, task_executor)) }); // Subscribe to get VC logs via Server side events @@ -1271,7 +1251,9 @@ pub fn serve( match msg { Ok(data) => { // Serialize to json - match data.to_json_string() { + match serde_json::to_string(&data) + .map_err(|e| format!("{:?}", e)) + { // Send the json as a Server Sent Event Ok(json) => Event::default().json_data(json).map_err(|e| { warp_utils::reject::server_sent_event_error(format!( @@ -1315,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) @@ -1364,10 +1346,9 @@ pub fn serve( )?; info!( - log, - "HTTP API started"; - "listen_address" => listening_socket.to_string(), - "api_token_file" => ?api_token_path, + listen_address = listening_socket.to_string(), + ?api_token_path, + "HTTP API started" ); Ok((listening_socket, server)) diff --git a/validator_client/http_api/src/remotekeys.rs b/validator_client/http_api/src/remotekeys.rs index 289be571825..5aa63baac3b 100644 --- a/validator_client/http_api/src/remotekeys.rs +++ b/validator_client/http_api/src/remotekeys.rs @@ -8,19 +8,19 @@ use eth2::lighthouse_vc::std_types::{ ListRemotekeysResponse, SingleListRemotekeysResponse, Status, }; use initialized_validators::{Error, InitializedValidators}; -use slog::{info, warn, Logger}; +use lighthouse_validator_store::LighthouseValidatorStore; use slot_clock::SlotClock; use std::sync::Arc; use task_executor::TaskExecutor; 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,14 +50,12 @@ pub fn list( pub fn import( request: ImportRemotekeysRequest, - validator_store: Arc>, + validator_store: Arc>, task_executor: TaskExecutor, - log: Logger, ) -> Result { info!( - log, - "Importing remotekeys via standard HTTP API"; - "count" => request.remote_keys.len(), + count = request.remote_keys.len(), + "Importing remotekeys via standard HTTP API" ); // Import each remotekey. Some remotekeys may fail to be imported, so we record a status for each. let mut statuses = Vec::with_capacity(request.remote_keys.len()); @@ -65,15 +63,18 @@ 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!( - log, - "Error importing keystore, skipped"; - "pubkey" => remotekey.pubkey.to_string(), - "error" => ?e, + pubkey = remotekey.pubkey.to_string(), + error = ?e, + "Error importing keystore, skipped" ); Status::error(ImportRemotekeyStatus::Error, e) } @@ -92,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) { @@ -146,14 +147,12 @@ fn import_single_remotekey( pub fn delete( request: DeleteRemotekeysRequest, - validator_store: Arc>, + validator_store: Arc>, task_executor: TaskExecutor, - log: Logger, ) -> Result { info!( - log, - "Deleting remotekeys via standard HTTP API"; - "count" => request.pubkeys.len(), + count = request.pubkeys.len(), + "Deleting remotekeys via standard HTTP API" ); // Remove from initialized validators. let initialized_validators_rwlock = validator_store.initialized_validators(); @@ -171,10 +170,9 @@ pub fn delete( Ok(status) => Status::ok(status), Err(error) => { warn!( - log, - "Error deleting keystore"; - "pubkey" => ?pubkey_bytes, - "error" => ?error, + pubkey = ?pubkey_bytes, + ?error, + "Error deleting keystore" ); Status::error(DeleteRemotekeyStatus::Error, error) } diff --git a/validator_client/http_api/src/test_utils.rs b/validator_client/http_api/src/test_utils.rs index 0531626846b..08447a82ce7 100644 --- a/validator_client/http_api/src/test_utils.rs +++ b/validator_client/http_api/src/test_utils.rs @@ -14,20 +14,19 @@ use eth2::{ use eth2_keystore::KeystoreBuilder; use initialized_validators::key_cache::{KeyCache, CACHE_FILENAME}; use initialized_validators::{InitializedValidators, OnDecryptFailure}; -use logging::test_logger; +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]; @@ -55,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, @@ -70,8 +69,6 @@ impl ApiTester { } pub async fn new_with_http_config(http_config: HttpConfig) -> Self { - let log = test_logger(); - let validator_dir = tempdir().unwrap(); let secrets_dir = tempdir().unwrap(); let token_path = tempdir().unwrap().path().join(PK_FILENAME); @@ -82,7 +79,6 @@ impl ApiTester { validator_defs, validator_dir.path().into(), Default::default(), - log.clone(), ) .await .unwrap(); @@ -105,16 +101,15 @@ 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), spec.clone(), - Some(Arc::new(DoppelgangerService::new(log.clone()))), + Some(Arc::new(DoppelgangerService::default())), slot_clock.clone(), &config, test_runtime.task_executor.clone(), - log.clone(), )); validator_store @@ -126,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()), @@ -134,10 +129,8 @@ impl ApiTester { graffiti_flag: Some(Graffiti::default()), spec, config: http_config, - log, sse_logging_components: None, slot_clock, - _phantom: PhantomData, }); let ctx = context; let (shutdown_tx, shutdown_rx) = oneshot::channel(); @@ -145,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); @@ -644,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 4e9acc42378..4b1a3c0059c 100644 --- a/validator_client/http_api/src/tests.rs +++ b/validator_client/http_api/src/tests.rs @@ -18,13 +18,12 @@ use eth2::{ Error as ApiError, }; use eth2_keystore::KeystoreBuilder; -use logging::test_logger; +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; @@ -32,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]; @@ -43,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, @@ -61,8 +60,6 @@ impl ApiTester { } pub async fn new_with_config(config: ValidatorStoreConfig) -> Self { - let log = test_logger(); - let validator_dir = tempdir().unwrap(); let secrets_dir = tempdir().unwrap(); let token_path = tempdir().unwrap().path().join("api-token.txt"); @@ -73,7 +70,6 @@ impl ApiTester { validator_defs, validator_dir.path().into(), InitializedValidatorsConfig::default(), - log.clone(), ) .await .unwrap(); @@ -95,16 +91,15 @@ 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), spec.clone(), - Some(Arc::new(DoppelgangerService::new(log.clone()))), + Some(Arc::new(DoppelgangerService::default())), slot_clock.clone(), &config, test_runtime.task_executor.clone(), - log.clone(), )); validator_store @@ -133,13 +128,11 @@ impl ApiTester { http_token_path: token_path, }, sse_logging_components: None, - log, 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); @@ -676,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 ); @@ -692,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 ); @@ -708,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 ); @@ -718,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 ); @@ -734,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 ); @@ -1165,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 13494e5fa69..37f7513f379 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 a3432410bc7..24cbff7cde6 100644 --- a/validator_client/http_metrics/Cargo.toml +++ b/validator_client/http_metrics/Cargo.toml @@ -6,16 +6,17 @@ authors = ["Sigma Prime "] [dependencies] health_metrics = { workspace = true } +lighthouse_validator_store = { workspace = true } lighthouse_version = { workspace = true } +logging = { workspace = true } malloc_utils = { workspace = true } metrics = { workspace = true } parking_lot = { workspace = true } serde = { workspace = true } -slog = { workspace = true } slot_clock = { workspace = true } +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 f1c6d4ed8ad..74419399576 100644 --- a/validator_client/http_metrics/src/lib.rs +++ b/validator_client/http_metrics/src/lib.rs @@ -2,19 +2,20 @@ //! //! 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; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; -use slog::{crit, info, Logger}; use slot_clock::{SlotClock, SystemTimeSlotClock}; use std::future::Future; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::Arc; 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)] @@ -35,20 +36,21 @@ 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>, - pub log: Logger, } /// Configuration for the HTTP server. @@ -93,7 +95,6 @@ pub fn serve( shutdown: impl Future + Send + Sync + 'static, ) -> Result<(SocketAddr, impl Future), Error> { let config = &ctx.config; - let log = ctx.log.clone(); // Configure CORS. let cors_builder = { @@ -110,7 +111,7 @@ pub fn serve( // Sanity check. if !config.enabled { - crit!(log, "Cannot start disabled metrics HTTP server"); + crit!("Cannot start disabled metrics HTTP server"); return Err(Error::Other( "A disabled metrics server should not be started".to_string(), )); @@ -151,9 +152,8 @@ pub fn serve( )?; info!( - log, - "Metrics HTTP server started"; - "listen_address" => listening_socket.to_string(), + listen_address = listening_socket.to_string(), + "Metrics HTTP server started" ); Ok((listening_socket, server)) diff --git a/validator_client/initialized_validators/Cargo.toml b/validator_client/initialized_validators/Cargo.toml index 05e85261f9a..8b2ae62aea3 100644 --- a/validator_client/initialized_validators/Cargo.toml +++ b/validator_client/initialized_validators/Cargo.toml @@ -18,8 +18,8 @@ reqwest = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } signing_method = { workspace = true } -slog = { workspace = true } tokio = { workspace = true } +tracing = { workspace = true } types = { workspace = true } url = { workspace = true } validator_dir = { workspace = true } diff --git a/validator_client/initialized_validators/src/lib.rs b/validator_client/initialized_validators/src/lib.rs index bd64091dae4..cbc1287a851 100644 --- a/validator_client/initialized_validators/src/lib.rs +++ b/validator_client/initialized_validators/src/lib.rs @@ -22,13 +22,13 @@ use parking_lot::{MappedMutexGuard, Mutex, MutexGuard}; use reqwest::{Certificate, Client, Error as ReqwestError, Identity}; use serde::{Deserialize, Serialize}; use signing_method::SigningMethod; -use slog::{debug, error, info, warn, Logger}; use std::collections::{HashMap, HashSet}; use std::fs::{self, File}; use std::io::{self, Read}; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; +use tracing::{debug, error, info, warn}; use types::graffiti::GraffitiString; use types::{Address, Graffiti, Keypair, PublicKey, PublicKeyBytes}; use url::{ParseError, Url}; @@ -503,8 +503,6 @@ pub struct InitializedValidators { validators: HashMap, /// The clients used for communications with a remote signer. web3_signer_client_map: Option>, - /// For logging via `slog`. - log: Logger, config: Config, } @@ -514,7 +512,6 @@ impl InitializedValidators { definitions: ValidatorDefinitions, validators_dir: PathBuf, config: Config, - log: Logger, ) -> Result { let mut this = Self { validators_dir, @@ -522,7 +519,6 @@ impl InitializedValidators { validators: HashMap::default(), web3_signer_client_map: None, config, - log, }; this.update_validators().await?; Ok(this) @@ -1151,10 +1147,9 @@ impl InitializedValidators { for uuid in cache.uuids() { if !definitions_map.contains_key(uuid) { debug!( - self.log, - "Resetting the key cache"; - "keystore_uuid" => %uuid, - "reason" => "impossible to decrypt due to missing keystore", + keystore_uuid = %uuid, + reason = "impossible to decrypt due to missing keystore", + "Resetting the key cache" ); return Ok(KeyCache::new()); } @@ -1281,30 +1276,27 @@ impl InitializedValidators { self.validators .insert(init.voting_public_key().compress(), init); info!( - self.log, - "Enabled validator"; - "signing_method" => "local_keystore", - "voting_pubkey" => format!("{:?}", def.voting_public_key), + signing_method = "local_keystore", + voting_pubkey = format!("{:?}", def.voting_public_key), + "Enabled validator" ); if let Some(lockfile_path) = existing_lockfile_path { warn!( - self.log, - "Ignored stale lockfile"; - "path" => lockfile_path.display(), - "cause" => "Ungraceful shutdown (harmless) OR \ + path = ?lockfile_path.display(), + cause = "Ungraceful shutdown (harmless) OR \ non-Lighthouse client using this keystore \ - (risky)" + (risky)", + "Ignored stale lockfile" ); } } Err(e) => { error!( - self.log, - "Failed to initialize validator"; - "error" => format!("{:?}", e), - "signing_method" => "local_keystore", - "validator" => format!("{:?}", def.voting_public_key) + error = format!("{:?}", e), + signing_method = "local_keystore", + validator = format!("{:?}", def.voting_public_key), + "Failed to initialize validator" ); // Exit on an invalid validator. @@ -1327,19 +1319,17 @@ impl InitializedValidators { .insert(init.voting_public_key().compress(), init); info!( - self.log, - "Enabled validator"; - "signing_method" => "remote_signer", - "voting_pubkey" => format!("{:?}", def.voting_public_key), + signing_method = "remote_signer", + voting_pubkey = format!("{:?}", def.voting_public_key), + "Enabled validator" ); } Err(e) => { error!( - self.log, - "Failed to initialize validator"; - "error" => format!("{:?}", e), - "signing_method" => "remote_signer", - "validator" => format!("{:?}", def.voting_public_key) + error = format!("{:?}", e), + signing_method = "remote_signer", + validator = format!("{:?}", def.voting_public_key), + "Failed to initialize validator" ); // Exit on an invalid validator. @@ -1364,9 +1354,8 @@ impl InitializedValidators { } info!( - self.log, - "Disabled validator"; - "voting_pubkey" => format!("{:?}", def.voting_public_key) + voting_pubkey = format!("{:?}", def.voting_public_key), + "Disabled validator" ); } } @@ -1378,23 +1367,18 @@ impl InitializedValidators { } let validators_dir = self.validators_dir.clone(); - let log = self.log.clone(); if has_local_definitions && key_cache.is_modified() { tokio::task::spawn_blocking(move || { match key_cache.save(validators_dir) { - Err(e) => warn!( - log, - "Error during saving of key_cache"; - "err" => format!("{:?}", e) - ), - Ok(true) => info!(log, "Modified key_cache saved successfully"), + Err(e) => warn!(err = format!("{:?}", e), "Error during saving of key_cache"), + Ok(true) => info!("Modified key_cache saved successfully"), _ => {} }; }) .await .map_err(Error::TokioJoin)?; } else { - debug!(log, "Key cache not modified"); + debug!("Key cache not modified"); } // Update the enabled and total validator counts diff --git a/validator_client/lighthouse_validator_store/Cargo.toml b/validator_client/lighthouse_validator_store/Cargo.toml new file mode 100644 index 00000000000..0f8220bdc9f --- /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 00000000000..d07f95f11ca --- /dev/null +++ b/validator_client/lighthouse_validator_store/src/lib.rs @@ -0,0 +1,1130 @@ +use account_utils::validator_definitions::{PasswordStorage, ValidatorDefinition}; +use doppelganger_service::DoppelgangerService; +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::{ + graffiti::GraffitiString, AbstractExecPayload, Address, AggregateAndProof, Attestation, + BeaconBlock, BlindedPayload, ChainSpec, ContributionAndProof, Domain, Epoch, EthSpec, Fork, + Graffiti, Hash256, 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) => self + .sign_abstract_block(validator_pubkey, block, current_slot) + .await + .map(SignedBlock::Full), + UnsignedBlock::Blinded(block) => self + .sign_abstract_block(validator_pubkey, block, current_slot) + .await + .map(SignedBlock::Blinded), + } + } + + 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()), + }) + } +} diff --git a/validator_client/signing_method/src/lib.rs b/validator_client/signing_method/src/lib.rs index f3b62c9500b..316c1d2205c 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; @@ -152,8 +152,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>( @@ -227,11 +232,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/Cargo.toml b/validator_client/slashing_protection/Cargo.toml index 1a098742d89..88e6dd794d5 100644 --- a/validator_client/slashing_protection/Cargo.toml +++ b/validator_client/slashing_protection/Cargo.toml @@ -19,6 +19,7 @@ rusqlite = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tempfile = { workspace = true } +tracing = { workspace = true } types = { workspace = true } [dev-dependencies] diff --git a/validator_client/slashing_protection/src/lib.rs b/validator_client/slashing_protection/src/lib.rs index 51dd3e31642..825a34cabc7 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 779b5f770aa..332f80c7045 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 92ec2dcbe87..d46872529e9 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/check_synced.rs b/validator_client/src/check_synced.rs new file mode 100644 index 00000000000..5f3e0fe0360 --- /dev/null +++ b/validator_client/src/check_synced.rs @@ -0,0 +1,25 @@ +use crate::beacon_node_fallback::CandidateError; +use eth2::{types::Slot, BeaconNodeHttpClient}; +use tracing::warn; + +pub async fn check_node_health( + beacon_node: &BeaconNodeHttpClient, +) -> Result<(Slot, bool, bool), CandidateError> { + let resp = match beacon_node.get_node_syncing().await { + Ok(resp) => resp, + Err(e) => { + warn!( + error = %e, + "Unable connect to beacon node" + ); + + return Err(CandidateError::Offline); + } + }; + + Ok(( + resp.data.head_slot, + resp.data.is_optimistic, + resp.data.el_offline, + )) +} diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index 18bd736957c..cdbf9f8472b 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 358bdacf5c9..726aa96cf9d 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -10,17 +10,17 @@ 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 slog::{info, warn, Logger}; use std::fs; use std::net::IpAddr; use std::path::PathBuf; use std::time::Duration; +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/"; @@ -141,7 +141,6 @@ impl Config { pub fn from_cli( cli_args: &ArgMatches, validator_client_config: &ValidatorClient, - log: &Logger, ) -> Result { let mut config = Config::default(); @@ -207,7 +206,10 @@ impl Config { .read_graffiti_file() .map_err(|e| format!("Error reading graffiti file: {:?}", e))?; config.graffiti_file = Some(graffiti_file); - info!(log, "Successfully loaded graffiti file"; "path" => graffiti_file_path.to_str()); + info!( + path = graffiti_file_path.to_str(), + "Successfully loaded graffiti file" + ); } if let Some(input_graffiti) = validator_client_config.graffiti.as_ref() { @@ -375,10 +377,9 @@ impl Config { config.validator_store.enable_web3signer_slashing_protection = if validator_client_config.disable_slashing_protection_web3signer { warn!( - log, - "Slashing protection for remote keys disabled"; - "info" => "ensure slashing protection on web3signer is enabled or you WILL \ - get slashed" + info = "ensure slashing protection on web3signer is enabled or you WILL \ + get slashed", + "Slashing protection for remote keys disabled" ); false } else { diff --git a/validator_client/src/latency.rs b/validator_client/src/latency.rs index 22f02c7c0bc..2382d350af5 100644 --- a/validator_client/src/latency.rs +++ b/validator_client/src/latency.rs @@ -1,9 +1,9 @@ use beacon_node_fallback::BeaconNodeFallback; use environment::RuntimeContext; -use slog::debug; use slot_clock::SlotClock; use std::sync::Arc; use tokio::time::sleep; +use tracing::debug; use types::EthSpec; /// The latency service will run 11/12ths of the way through the slot. @@ -15,10 +15,8 @@ pub const SLOT_DELAY_DENOMINATOR: u32 = 12; pub fn start_latency_service( context: RuntimeContext, slot_clock: T, - beacon_nodes: Arc>, + beacon_nodes: Arc>, ) { - let log = context.log().clone(); - let future = async move { loop { let sleep_time = slot_clock @@ -39,10 +37,9 @@ pub fn start_latency_service( for (i, measurement) in beacon_nodes.measure_latency().await.iter().enumerate() { if let Some(latency) = measurement.latency { debug!( - log, - "Measured BN latency"; - "node" => &measurement.beacon_node_id, - "latency" => latency.as_millis(), + node = &measurement.beacon_node_id, + latency = latency.as_millis(), + "Measured BN latency" ); validator_metrics::observe_timer_vec( &validator_metrics::VC_BEACON_NODE_LATENCY, diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 1b91eb71c28..a7993dc879c 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -20,15 +20,14 @@ use doppelganger_service::DoppelgangerService; use environment::RuntimeContext; use eth2::{reqwest::ClientBuilder, BeaconNodeHttpClient, StatusCode, Timeouts}; use initialized_validators::Error::UnableToOpenVotingKeystore; +use lighthouse_validator_store::LighthouseValidatorStore; use notifier::spawn_notifier; use parking_lot::RwLock; use reqwest::Certificate; -use slog::{debug, error, info, warn, Logger}; 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; @@ -37,17 +36,17 @@ use tokio::{ sync::mpsc, time::{sleep, Duration}, }; +use tracing::{debug, error, info, warn}; use types::{EthSpec, Hash256}; use validator_http_api::ApiSecret; use validator_services::{ attestation_service::{AttestationService, AttestationServiceBuilder}, block_service::{BlockService, BlockServiceBuilder}, - duties_service::{self, DutiesService}, + duties_service::{self, DutiesService, DutiesServiceBuilder}, 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); @@ -69,23 +68,26 @@ 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, + duties_service: Arc, SystemTimeSlotClock>>, + block_service: BlockService, SystemTimeSlotClock>, + attestation_service: AttestationService, SystemTimeSlotClock>, + sync_committee_service: SyncCommitteeService, 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, } @@ -97,7 +99,7 @@ impl ProductionValidatorClient { cli_args: &ArgMatches, validator_client_config: &ValidatorClient, ) -> Result { - let config = Config::from_cli(cli_args, validator_client_config, context.log()) + let config = Config::from_cli(cli_args, validator_client_config) .map_err(|e| format!("Unable to initialize config: {}", e))?; Self::new(context, config).await } @@ -105,8 +107,6 @@ impl ProductionValidatorClient { /// Instantiates the validator client, _without_ starting the timers to trigger block /// and attestation production. pub async fn new(context: RuntimeContext, config: Config) -> Result { - let log = context.log().clone(); - // Attempt to raise soft fd limit. The behavior is OS specific: // `linux` - raise soft fd limit to hard // `macos` - raise soft fd limit to `min(kernel limit, hard fd limit)` @@ -114,25 +114,20 @@ impl ProductionValidatorClient { match fdlimit::raise_fd_limit().map_err(|e| format!("Unable to raise fd limit: {}", e))? { fdlimit::Outcome::LimitRaised { from, to } => { debug!( - log, - "Raised soft open file descriptor resource limit"; - "old_limit" => from, - "new_limit" => to + old_limit = from, + new_limit = to, + "Raised soft open file descriptor resource limit" ); } fdlimit::Outcome::Unsupported => { - debug!( - log, - "Raising soft open file descriptor resource limit is not supported" - ); + debug!("Raising soft open file descriptor resource limit is not supported"); } }; info!( - log, - "Starting validator client"; - "beacon_nodes" => format!("{:?}", &config.beacon_nodes), - "validator_dir" => format!("{:?}", config.validator_dir), + beacon_nodes = ?config.beacon_nodes, + validator_dir = ?config.validator_dir, + "Starting validator client" ); // Optionally start the metrics server. @@ -147,7 +142,6 @@ impl ProductionValidatorClient { Arc::new(validator_http_metrics::Context { config: config.http_metrics.clone(), shared: RwLock::new(shared), - log: log.clone(), }); let exit = context.executor.exit(); @@ -162,15 +156,14 @@ impl ProductionValidatorClient { Some(ctx) } else { - info!(log, "HTTP metrics server is disabled"); + info!("HTTP metrics server is disabled"); None }; // Start the explorer client which periodically sends validator process // and system metrics to the configured endpoint. if let Some(monitoring_config) = &config.monitoring_api { - let monitoring_client = - MonitoringHttpClient::new(monitoring_config, context.log().clone())?; + let monitoring_client = MonitoringHttpClient::new(monitoring_config)?; monitoring_client.auto_update( context.executor.clone(), vec![ProcessType::Validator, ProcessType::System], @@ -182,7 +175,7 @@ impl ProductionValidatorClient { if !config.disable_auto_discover { let new_validators = validator_defs - .discover_local_keystores(&config.validator_dir, &config.secrets_dir, &log) + .discover_local_keystores(&config.validator_dir, &config.secrets_dir) .map_err(|e| format!("Unable to discover local validator keystores: {:?}", e))?; validator_defs.save(&config.validator_dir).map_err(|e| { format!( @@ -190,18 +183,13 @@ impl ProductionValidatorClient { e ) })?; - info!( - log, - "Completed validator discovery"; - "new_validators" => new_validators, - ); + info!(new_validators, "Completed validator discovery"); } let validators = InitializedValidators::from_definitions( validator_defs, config.validator_dir.clone(), config.initialized_validators.clone(), - log.clone(), ) .await .map_err(|e| { @@ -218,17 +206,17 @@ impl ProductionValidatorClient { let voting_pubkeys: Vec<_> = validators.iter_voting_pubkeys().collect(); info!( - log, - "Initialized validators"; - "disabled" => validators.num_total().saturating_sub(validators.num_enabled()), - "enabled" => validators.num_enabled(), + disabled = validators + .num_total() + .saturating_sub(validators.num_enabled()), + enabled = validators.num_enabled(), + "Initialized validators" ); if voting_pubkeys.is_empty() { warn!( - log, - "No enabled validators"; - "hint" => "create validators via the API, or the `lighthouse account` CLI command" + hint = "create validators via the API, or the `lighthouse account` CLI command", + "No enabled validators" ); } @@ -303,10 +291,7 @@ impl ProductionValidatorClient { // Use quicker timeouts if a fallback beacon node exists. let timeouts = if i < last_beacon_node_index && !config.use_long_timeouts { - info!( - log, - "Fallback endpoints are available, using optimized timeouts."; - ); + info!("Fallback endpoints are available, using optimized timeouts."); Timeouts { attestation: slot_duration / HTTP_ATTESTATION_TIMEOUT_QUOTIENT, attester_duties: slot_duration / HTTP_ATTESTER_DUTIES_TIMEOUT_QUOTIENT, @@ -323,6 +308,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)) @@ -384,25 +370,23 @@ 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(), - log.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(), context.eth2_config.spec.clone(), - log.clone(), ); // Perform some potentially long-running initialization tasks. let (genesis_time, genesis_validators_root) = tokio::select! { - tuple = init_from_beacon_node(&beacon_nodes, &proposer_nodes, &context) => tuple?, + tuple = init_from_beacon_node::(&beacon_nodes, &proposer_nodes) => tuple?, () = context.executor.exit() => return Err("Shutting down".to_string()) }; @@ -421,23 +405,18 @@ 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::new( - context - .service_context(DOPPELGANGER_SERVICE_NAME.into()) - .log() - .clone(), - ))) + Some(Arc::new(DoppelgangerService::default())) } else { None }; - let validator_store = Arc::new(ValidatorStore::new( + let validator_store = Arc::new(LighthouseValidatorStore::new( validators, slashing_protection, genesis_validators_root, @@ -446,16 +425,14 @@ impl ProductionValidatorClient { slot_clock.clone(), &config.validator_store, context.executor.clone(), - log.clone(), )); // Ensure all validators are registered in doppelganger protection. validator_store.register_all_in_doppelganger_protection_if_enabled()?; info!( - log, - "Loaded validator keypair store"; - "voting_validators" => validator_store.num_voting_validators() + voting_validators = validator_store.num_voting_validators(), + "Loaded validator keypair store" ); // Perform pruning of the slashing protection database on start-up. In case the database is @@ -465,21 +442,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), - 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 { @@ -491,7 +465,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()); @@ -507,7 +482,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()?; @@ -515,7 +491,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()?; @@ -525,7 +501,7 @@ impl ProductionValidatorClient { validator_store.clone(), slot_clock.clone(), beacon_nodes.clone(), - context.service_context("sync_committee".into()), + context.executor.clone(), ); Ok(Self { @@ -551,7 +527,6 @@ impl ProductionValidatorClient { // whole epoch! let channel_capacity = E::slots_per_epoch() as usize; let (block_service_tx, block_service_rx) = mpsc::channel(channel_capacity); - let log = self.context.log(); let api_secret = ApiSecret::create_or_open(&self.config.http_api.http_token_path)?; @@ -569,13 +544,11 @@ impl ProductionValidatorClient { config: self.config.http_api.clone(), sse_logging_components: self.context.sse_logging_components.clone(), slot_clock: self.slot_clock.clone(), - log: log.clone(), - _phantom: PhantomData, }); let exit = self.context.executor.exit(); - let (listen_addr, server) = 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 @@ -585,12 +558,12 @@ impl ProductionValidatorClient { Some(listen_addr) } else { - info!(log, "HTTP API server is disabled"); + info!("HTTP API server is disabled"); None }; // Wait until genesis has occurred. - wait_for_genesis(&self.beacon_nodes, self.genesis_time, &self.context).await?; + wait_for_genesis(&self.beacon_nodes, self.genesis_time).await?; duties_service::start_update_service(self.duties_service.clone(), block_service_tx); @@ -625,7 +598,7 @@ impl ProductionValidatorClient { ) .map_err(|e| format!("Unable to start doppelganger service: {}", e))? } else { - info!(log, "Doppelganger protection disabled.") + info!("Doppelganger protection disabled.") } spawn_notifier(self).map_err(|e| format!("Failed to start notifier: {}", e))?; @@ -643,13 +616,12 @@ impl ProductionValidatorClient { } async fn init_from_beacon_node( - beacon_nodes: &BeaconNodeFallback, - proposer_nodes: &BeaconNodeFallback, - context: &RuntimeContext, + 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; @@ -659,41 +631,37 @@ async fn init_from_beacon_node( if proposer_total > 0 && proposer_available == 0 { warn!( - context.log(), - "Unable to connect to a proposer node"; - "retry in" => format!("{} seconds", RETRY_DELAY.as_secs()), - "total_proposers" => proposer_total, - "available_proposers" => proposer_available, - "total_beacon_nodes" => num_total, - "available_beacon_nodes" => num_available, + retry_in = format!("{} seconds", RETRY_DELAY.as_secs()), + total_proposers = proposer_total, + available_proposers = proposer_available, + total_beacon_nodes = num_total, + available_beacon_nodes = num_available, + "Unable to connect to a proposer node" ); } if num_available > 0 && proposer_available == 0 { info!( - context.log(), - "Initialized beacon node connections"; - "total" => num_total, - "available" => num_available, + total = num_total, + available = num_available, + "Initialized beacon node connections" ); break; } else if num_available > 0 { info!( - context.log(), - "Initialized beacon node connections"; - "total" => num_total, - "available" => num_available, - "proposers_available" => proposer_available, - "proposers_total" => proposer_total, + total = num_total, + available = num_available, + proposer_available, + proposer_total, + "Initialized beacon node connections" ); break; } else { warn!( - context.log(), - "Unable to connect to a beacon node"; - "retry in" => format!("{} seconds", RETRY_DELAY.as_secs()), - "total" => num_total, - "available" => num_available, + retry_in = format!("{} seconds", RETRY_DELAY.as_secs()), + total = num_total, + available = num_available, + "Unable to connect to a beacon node" ); sleep(RETRY_DELAY).await; } @@ -714,15 +682,11 @@ async fn init_from_beacon_node( .filter_map(|(_, e)| e.request_failure()) .any(|e| e.status() == Some(StatusCode::NOT_FOUND)) { - info!( - context.log(), - "Waiting for genesis"; - ); + info!("Waiting for genesis"); } else { error!( - context.log(), - "Errors polling beacon node"; - "error" => %errors + %errors, + "Errors polling beacon node" ); } } @@ -734,10 +698,9 @@ 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, - context: &RuntimeContext, ) -> Result<(), String> { let now = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -751,28 +714,25 @@ async fn wait_for_genesis( // the slot clock. if now < genesis_time { info!( - context.log(), - "Starting node prior to genesis"; - "seconds_to_wait" => (genesis_time - now).as_secs() + seconds_to_wait = (genesis_time - now).as_secs(), + "Starting node prior to genesis" ); // Start polling the node for pre-genesis information, cancelling the polling as soon as the // timer runs out. tokio::select! { - result = poll_whilst_waiting_for_genesis(beacon_nodes, genesis_time, context.log()) => result?, + result = poll_whilst_waiting_for_genesis(beacon_nodes, genesis_time) => result?, () = sleep(genesis_time - now) => () }; info!( - context.log(), - "Genesis has occurred"; - "ms_since_genesis" => (genesis_time - now).as_millis() + ms_since_genesis = (genesis_time - now).as_millis(), + "Genesis has occurred" ); } else { info!( - context.log(), - "Genesis has already occurred"; - "seconds_ago" => (now - genesis_time).as_secs() + seconds_ago = (now - genesis_time).as_secs(), + "Genesis has already occurred" ); } @@ -781,10 +741,9 @@ 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, - log: &Logger, ) -> Result<(), String> { loop { match beacon_nodes @@ -798,19 +757,17 @@ async fn poll_whilst_waiting_for_genesis( if !is_staking { error!( - log, - "Staking is disabled for beacon node"; - "msg" => "this will caused missed duties", - "info" => "see the --staking CLI flag on the beacon node" + msg = "this will caused missed duties", + info = "see the --staking CLI flag on the beacon node", + "Staking is disabled for beacon node" ); } if now < genesis_time { info!( - log, - "Waiting for genesis"; - "bn_staking_enabled" => is_staking, - "seconds_to_wait" => (genesis_time - now).as_secs() + bn_staking_enabled = is_staking, + seconds_to_wait = (genesis_time - now).as_secs(), + "Waiting for genesis" ); } else { break Ok(()); @@ -818,9 +775,8 @@ async fn poll_whilst_waiting_for_genesis( } Err(e) => { error!( - log, - "Error polling beacon node"; - "error" => %e + error = %e, + "Error polling beacon node" ); } } diff --git a/validator_client/src/notifier.rs b/validator_client/src/notifier.rs index ff66517795b..05f1c919d20 100644 --- a/validator_client/src/notifier.rs +++ b/validator_client/src/notifier.rs @@ -1,8 +1,9 @@ use crate::{DutiesService, ProductionValidatorClient}; +use lighthouse_validator_store::LighthouseValidatorStore; use metrics::set_gauge; -use slog::{debug, error, info, Logger}; use slot_clock::SlotClock; use tokio::time::{sleep, Duration}; +use tracing::{debug, error, info}; use types::EthSpec; /// Spawns a notifier service which periodically logs information about the node. @@ -14,14 +15,12 @@ pub fn spawn_notifier(client: &ProductionValidatorClient) -> Resu let slot_duration = Duration::from_secs(context.eth2_config.spec.seconds_per_slot); let interval_fut = async move { - let log = context.log(); - loop { if let Some(duration_to_next_slot) = duties_service.slot_clock.duration_to_next_slot() { sleep(duration_to_next_slot + slot_duration / 2).await; - notify(&duties_service, log).await; + notify(&duties_service).await; } else { - error!(log, "Failed to read slot clock"); + error!("Failed to read slot clock"); // If we can't read the slot clock, just wait another slot. sleep(slot_duration).await; continue; @@ -35,8 +34,7 @@ pub fn spawn_notifier(client: &ProductionValidatorClient) -> Resu /// Performs a single notification routine. async fn notify( - duties_service: &DutiesService, - log: &Logger, + duties_service: &DutiesService, T>, ) { let (candidate_info, num_available, num_synced) = duties_service.beacon_nodes.get_notifier_info().await; @@ -61,20 +59,18 @@ async fn notify( .map(|candidate| candidate.endpoint.as_str()) .unwrap_or("None"); info!( - log, - "Connected to beacon node(s)"; - "primary" => primary, - "total" => num_total, - "available" => num_available, - "synced" => num_synced, + primary, + total = num_total, + available = num_available, + synced = num_synced, + "Connected to beacon node(s)" ) } else { error!( - log, - "No synced beacon nodes"; - "total" => num_total, - "available" => num_available, - "synced" => num_synced, + total = num_total, + available = num_available, + synced = num_synced, + "No synced beacon nodes" ) } if num_synced_fallback > 0 { @@ -86,23 +82,21 @@ async fn notify( for info in candidate_info { if let Ok(health) = info.health { debug!( - log, - "Beacon node info"; - "status" => "Connected", - "index" => info.index, - "endpoint" => info.endpoint, - "head_slot" => %health.head, - "is_optimistic" => ?health.optimistic_status, - "execution_engine_status" => ?health.execution_status, - "health_tier" => %health.health_tier, + status = "Connected", + index = info.index, + endpoint = info.endpoint, + head_slot = %health.head, + is_optimistic = ?health.optimistic_status, + execution_engine_status = ?health.execution_status, + health_tier = %health.health_tier, + "Beacon node info" ); } else { debug!( - log, - "Beacon node info"; - "status" => "Disconnected", - "index" => info.index, - "endpoint" => info.endpoint, + status = "Disconnected", + index = info.index, + endpoint = info.endpoint, + "Beacon node info" ); } } @@ -116,45 +110,44 @@ async fn notify( let doppelganger_detecting_validators = duties_service.doppelganger_detecting_count(); if doppelganger_detecting_validators > 0 { - info!(log, "Listening for doppelgangers"; "doppelganger_detecting_validators" => doppelganger_detecting_validators) + info!( + doppelganger_detecting_validators, + "Listening for doppelgangers" + ) } if total_validators == 0 { info!( - log, - "No validators present"; - "msg" => "see `lighthouse vm create --help` or the HTTP API documentation" + msg = "see `lighthouse vm create --help` or the HTTP API documentation", + "No validators present" ) } else if total_validators == attesting_validators { info!( - log, - "All validators active"; - "current_epoch_proposers" => proposing_validators, - "active_validators" => attesting_validators, - "total_validators" => total_validators, - "epoch" => format!("{}", epoch), - "slot" => format!("{}", slot), + current_epoch_proposers = proposing_validators, + active_validators = attesting_validators, + total_validators = total_validators, + %epoch, + %slot, + "All validators active" ); } else if attesting_validators > 0 { info!( - log, - "Some validators active"; - "current_epoch_proposers" => proposing_validators, - "active_validators" => attesting_validators, - "total_validators" => total_validators, - "epoch" => format!("{}", epoch), - "slot" => format!("{}", slot), + current_epoch_proposers = proposing_validators, + active_validators = attesting_validators, + total_validators = total_validators, + %epoch, + %slot, + "Some validators active" ); } else { info!( - log, - "Awaiting activation"; - "validators" => total_validators, - "epoch" => format!("{}", epoch), - "slot" => format!("{}", slot), + validators = total_validators, + %epoch, + %slot, + "Awaiting activation" ); } } else { - error!(log, "Unable to read slot clock"); + error!("Unable to read slot clock"); } } diff --git a/validator_client/validator_services/Cargo.toml b/validator_client/validator_services/Cargo.toml index b4495a7c818..86208dadef3 100644 --- a/validator_client/validator_services/Cargo.toml +++ b/validator_client/validator_services/Cargo.toml @@ -6,18 +6,18 @@ 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 } -slog = { workspace = true } slot_clock = { workspace = true } +task_executor = { workspace = true } tokio = { workspace = true } +tracing = { workspace = true } tree_hash = { workspace = true } types = { workspace = true } validator_metrics = { workspace = true } diff --git a/validator_client/validator_services/src/attestation_service.rs b/validator_client/validator_services/src/attestation_service.rs index 961741a9770..c1e96a28087 100644 --- a/validator_client/validator_services/src/attestation_service.rs +++ b/validator_client/validator_services/src/attestation_service.rs @@ -1,47 +1,50 @@ use crate::duties_service::{DutiesService, DutyAndProof}; use beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; use either::Either; -use environment::RuntimeContext; use futures::future::join_all; -use slog::{crit, debug, error, info, trace, warn}; +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; use types::{Attestation, AttestationData, ChainSpec, CommitteeIndex, EthSpec, Slot}; 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 } @@ -51,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 } @@ -66,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 @@ -81,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, }), }) @@ -91,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, } @@ -105,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(), @@ -117,20 +129,19 @@ 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> { - let log = self.context.log().clone(); if self.disable { - info!(log, "Attestation service disabled"); + info!("Attestation service disabled"); return Ok(()); } @@ -141,33 +152,24 @@ impl AttestationService { .ok_or("Unable to determine duration to next slot")?; info!( - log, - "Attestation production service started"; - "next_update_millis" => duration_to_next_slot.as_millis() + next_update_millis = duration_to_next_slot.as_millis(), + "Attestation production service started" ); - let executor = self.context.executor.clone(); + let executor = self.executor.clone(); let interval_fut = async move { loop { if let Some(duration_to_next_slot) = self.slot_clock.duration_to_next_slot() { sleep(duration_to_next_slot + slot_duration / 3).await; - let log = self.context.log(); if let Err(e) = self.spawn_attestation_tasks(slot_duration) { - crit!( - log, - "Failed to spawn attestation tasks"; - "error" => e - ) + crit!(error = e, "Failed to spawn attestation tasks") } else { - trace!( - log, - "Spawned attestation tasks"; - ) + trace!("Spawned attestation tasks"); } } else { - error!(log, "Failed to read slot clock"); + error!("Failed to read slot clock"); // If we can't read the slot clock, just wait another slot. sleep(slot_duration).await; continue; @@ -214,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, @@ -249,7 +251,6 @@ impl AttestationService { validator_duties: Vec, aggregate_production_instant: Instant, ) -> Result<(), ()> { - let log = self.context.log(); let attestations_timer = validator_metrics::start_timer_vec( &validator_metrics::ATTESTATION_SERVICE_TIMES, &[validator_metrics::ATTESTATIONS], @@ -269,11 +270,10 @@ impl AttestationService { .await .map_err(move |e| { crit!( - log, - "Error during attestation routine"; - "error" => format!("{:?}", e), - "committee_index" => committee_index, - "slot" => slot.as_u64(), + error = format!("{:?}", e), + committee_index, + slot = slot.as_u64(), + "Error during attestation routine" ) })?; @@ -306,11 +306,10 @@ impl AttestationService { .await .map_err(move |e| { crit!( - log, - "Error during attestation routine"; - "error" => format!("{:?}", e), - "committee_index" => committee_index, - "slot" => slot.as_u64(), + error = format!("{:?}", e), + committee_index, + slot = slot.as_u64(), + "Error during attestation routine" ) })?; } @@ -336,8 +335,6 @@ impl AttestationService { committee_index: CommitteeIndex, validator_duties: &[DutyAndProof], ) -> Result, String> { - let log = self.context.log(); - if validator_duties.is_empty() { return Ok(None); } @@ -346,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 @@ -371,36 +368,34 @@ 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!( - log, - "Inconsistent validator duties during signing"; - "validator" => ?duty.pubkey, - "duty_slot" => duty.slot, - "attestation_slot" => attestation_data.slot, - "duty_index" => duty.committee_index, - "attestation_index" => attestation_data.index, + validator = ?duty.pubkey, + duty_slot = %duty.slot, + attestation_slot = %attestation_data.slot, + duty_index = duty.committee_index, + attestation_index = attestation_data.index, + "Inconsistent validator duties during signing" ); return None; } - let mut attestation = match Attestation::::empty_for_signing( + 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) => { crit!( - log, - "Invalid validator duties during signing"; - "validator" => ?duty.pubkey, - "duty" => ?duty, - "err" => ?err, + validator = ?duty.pubkey, + ?duty, + ?err, + "Invalid validator duties during signing" ); return None; } @@ -421,24 +416,22 @@ impl AttestationService { // A pubkey can be missing when a validator was recently // removed via the API. warn!( - log, - "Missing pubkey for attestation"; - "info" => "a validator may have recently been removed from this VC", - "pubkey" => ?pubkey, - "validator" => ?duty.pubkey, - "committee_index" => committee_index, - "slot" => slot.as_u64(), + info = "a validator may have recently been removed from this VC", + pubkey = ?pubkey, + validator = ?duty.pubkey, + committee_index = committee_index, + slot = slot.as_u64(), + "Missing pubkey for attestation" ); None } Err(e) => { crit!( - log, - "Failed to sign attestation"; - "error" => ?e, - "validator" => ?duty.pubkey, - "committee_index" => committee_index, - "slot" => slot.as_u64(), + error = ?e, + validator = ?duty.pubkey, + committee_index, + slot = slot.as_u64(), + "Failed to sign attestation" ); None } @@ -453,14 +446,12 @@ impl AttestationService { .unzip(); if attestations.is_empty() { - warn!(log, "No attestations were published"); + warn!("No attestations were published"); 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 @@ -481,12 +472,11 @@ impl AttestationService { // This shouldn't happen unless BN and VC are out of sync with // respect to the Electra fork. error!( - log, - "Unable to convert to SingleAttestation"; - "error" => ?e, - "committee_index" => attestation_data.index, - "slot" => slot.as_u64(), - "type" => "unaggregated", + error = ?e, + committee_index = attestation_data.index, + slot = slot.as_u64(), + "type" = "unaggregated", + "Unable to convert to SingleAttestation" ); None } @@ -495,7 +485,7 @@ impl AttestationService { .collect::>(); beacon_node - .post_beacon_pool_attestations_v2::( + .post_beacon_pool_attestations_v2::( Either::Right(single_attestations), fork_name, ) @@ -509,22 +499,20 @@ impl AttestationService { .await { Ok(()) => info!( - log, - "Successfully published attestations"; - "count" => attestations.len(), - "validator_indices" => ?validator_indices, - "head_block" => ?attestation_data.beacon_block_root, - "committee_index" => attestation_data.index, - "slot" => attestation_data.slot.as_u64(), - "type" => "unaggregated", + count = attestations.len(), + validator_indices = ?validator_indices, + head_block = ?attestation_data.beacon_block_root, + committee_index = attestation_data.index, + slot = attestation_data.slot.as_u64(), + "type" = "unaggregated", + "Successfully published attestations" ), Err(e) => error!( - log, - "Unable to publish attestations"; - "error" => %e, - "committee_index" => attestation_data.index, - "slot" => slot.as_u64(), - "type" => "unaggregated", + error = %e, + committee_index = attestation_data.index, + slot = slot.as_u64(), + "type" = "unaggregated", + "Unable to publish attestations" ), } @@ -550,8 +538,6 @@ impl AttestationService { committee_index: CommitteeIndex, validator_duties: &[DutyAndProof], ) -> Result<(), String> { - let log = self.context.log(); - if !validator_duties .iter() .any(|duty_and_proof| duty_and_proof.selection_proof.is_some()) @@ -561,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 @@ -608,8 +592,8 @@ 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) { - crit!(log, "Inconsistent validator duties during signing"); + if !duty.match_attestation_data::(attestation_data, &self.chain_spec) { + crit!("Inconsistent validator duties during signing"); return None; } @@ -627,19 +611,14 @@ impl AttestationService { Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { // A pubkey can be missing when a validator was recently // removed via the API. - debug!( - log, - "Missing pubkey for aggregate"; - "pubkey" => ?pubkey, - ); + debug!(?pubkey, "Missing pubkey for aggregate"); None } Err(e) => { crit!( - log, - "Failed to sign aggregate"; - "error" => ?e, - "pubkey" => ?duty.pubkey, + error = ?e, + pubkey = ?duty.pubkey, + "Failed to sign aggregate" ); None } @@ -683,14 +662,13 @@ impl AttestationService { for signed_aggregate_and_proof in signed_aggregate_and_proofs { let attestation = signed_aggregate_and_proof.message().aggregate(); info!( - log, - "Successfully published attestation"; - "aggregator" => signed_aggregate_and_proof.message().aggregator_index(), - "signatures" => attestation.num_set_aggregation_bits(), - "head_block" => format!("{:?}", attestation.data().beacon_block_root), - "committee_index" => attestation.committee_index(), - "slot" => attestation.data().slot.as_u64(), - "type" => "aggregated", + aggregator = signed_aggregate_and_proof.message().aggregator_index(), + signatures = attestation.num_set_aggregation_bits(), + head_block = format!("{:?}", attestation.data().beacon_block_root), + committee_index = attestation.committee_index(), + slot = attestation.data().slot.as_u64(), + "type" = "aggregated", + "Successfully published attestation" ); } } @@ -698,13 +676,12 @@ impl AttestationService { for signed_aggregate_and_proof in signed_aggregate_and_proofs { let attestation = &signed_aggregate_and_proof.message().aggregate(); crit!( - log, - "Failed to publish attestation"; - "error" => %e, - "aggregator" => signed_aggregate_and_proof.message().aggregator_index(), - "committee_index" => attestation.committee_index(), - "slot" => attestation.data().slot.as_u64(), - "type" => "aggregated", + error = %e, + aggregator = signed_aggregate_and_proof.message().aggregator_index(), + committee_index = attestation.committee_index(), + slot = attestation.data().slot.as_u64(), + "type" = "aggregated", + "Failed to publish attestation" ); } } @@ -719,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 60eb0361ad3..2f29c1feb7c 100644 --- a/validator_client/validator_services/src/block_service.rs +++ b/validator_client/validator_services/src/block_service.rs @@ -1,20 +1,21 @@ 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 slog::{crit, debug, error, info, trace, warn, Logger}; +use logging::crit; use slot_clock::SlotClock; use std::fmt::Debug; 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, + BlindedBeaconBlock, BlockType, ChainSpec, EthSpec, Graffiti, PublicKeyBytes, + SignedBlindedBeaconBlock, Slot, }; use validator_store::{Error as ValidatorStoreError, ValidatorStore}; @@ -44,30 +45,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 } @@ -77,18 +80,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 } @@ -102,7 +110,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 @@ -114,9 +122,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, @@ -127,12 +138,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 @@ -177,22 +188,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(), @@ -200,8 +212,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() @@ -214,23 +226,21 @@ pub struct BlockServiceNotification { pub block_proposers: Vec, } -impl BlockService { +impl BlockService { pub fn start_update_service( self, mut notification_rx: mpsc::Receiver, ) -> Result<(), String> { - let log = self.context.log().clone(); + info!("Block production service started"); - info!(log, "Block production service started"); - - let executor = self.inner.context.executor.clone(); + let executor = self.inner.executor.clone(); executor.spawn( async move { while let Some(notif) = notification_rx.recv().await { self.do_update(notif).await.ok(); } - debug!(log, "Block service shutting down"); + debug!("Block service shutting down"); }, "block_service", ); @@ -240,65 +250,57 @@ impl BlockService { /// Attempt to produce a block for any block producers in the `ValidatorStore`. async fn do_update(&self, notification: BlockServiceNotification) -> Result<(), ()> { - let log = self.context.log(); let _timer = validator_metrics::start_timer_vec( &validator_metrics::BLOCK_SERVICE_TIMES, &[validator_metrics::FULL_UPDATE], ); let slot = self.slot_clock.now().ok_or_else(move || { - crit!(log, "Duties manager failed to read slot clock"); + crit!("Duties manager failed to read slot clock"); })?; if notification.slot != slot { warn!( - log, - "Skipping block production for expired slot"; - "current_slot" => slot.as_u64(), - "notification_slot" => notification.slot.as_u64(), - "info" => "Your machine could be overloaded" + current_slot = slot.as_u64(), + notification_slot = notification.slot.as_u64(), + info = "Your machine could be overloaded", + "Skipping block production for expired slot" ); return Ok(()); } - if slot == self.context.eth2_config.spec.genesis_slot { + if slot == self.chain_spec.genesis_slot { debug!( - log, - "Not producing block at genesis slot"; - "proposers" => format!("{:?}", notification.block_proposers), + proposers = format!("{:?}", notification.block_proposers), + "Not producing block at genesis slot" ); return Ok(()); } - trace!( - log, - "Block service update started"; - "slot" => slot.as_u64() - ); + trace!(slot = slot.as_u64(), "Block service update started"); let proposers = notification.block_proposers; if proposers.is_empty() { trace!( - log, - "No local block proposers for this slot"; - "slot" => slot.as_u64() + slot = slot.as_u64(), + "No local block proposers for this slot" ) } else if proposers.len() > 1 { error!( - log, - "Multiple block proposers for this slot"; - "action" => "producing blocks for all proposers", - "num_proposers" => proposers.len(), - "slot" => slot.as_u64(), + action = "producing blocks for all proposers", + num_proposers = proposers.len(), + slot = slot.as_u64(), + "Multiple block proposers for this slot" ) } 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(); - let log = log.clone(); - self.inner.context.executor.spawn( + self.inner.executor.spawn( async move { let result = service .publish_block(slot, validator_pubkey, builder_boost_factor) @@ -308,11 +310,10 @@ impl BlockService { Ok(_) => {} Err(BlockError::Recoverable(e)) | Err(BlockError::Irrecoverable(e)) => { error!( - log, - "Error whilst producing block"; - "error" => ?e, - "block_slot" => ?slot, - "info" => "block v3 proposal failed, this error may or may not result in a missed block" + error = ?e, + block_slot = ?slot, + info = "block v3 proposal failed, this error may or may not result in a missed block", + "Error whilst producing block" ); } } @@ -326,42 +327,45 @@ 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 log = self.context.log(); let signing_timer = validator_metrics::start_timer(&validator_metrics::BLOCK_SIGNING_TIMES); - let res = match unsigned_block { + let (block, maybe_blobs) = 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))) + (block.into(), maybe_blobs) } - UnsignedBlock::Blinded(block) => self - .validator_store - .sign_block(*validator_pubkey, block, slot) - .await - .map(Arc::new) - .map(SignedBlock::Blinded), + UnsignedBlock::Blinded(block) => (block.into(), None), }; + let res = self + .validator_store + .sign_block(*validator_pubkey, block, slot) + .await + .map(|block| match block { + validator_store::SignedBlock::Full(block) => { + SignedBlock::Full(PublishBlockRequest::new(Arc::new(block), maybe_blobs)) + } + validator_store::SignedBlock::Blinded(block) => { + SignedBlock::Blinded(Arc::new(block)) + } + }); + let signed_block = match res { Ok(block) => block, Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { // A pubkey can be missing when a validator was recently removed // via the API. warn!( - log, - "Missing pubkey for block"; - "info" => "a validator may have recently been removed from this VC", - "pubkey" => ?pubkey, - "slot" => ?slot + info = "a validator may have recently been removed from this VC", + ?pubkey, + ?slot, + "Missing pubkey for block" ); return Ok(()); } @@ -377,10 +381,9 @@ impl BlockService { Duration::from_secs_f64(signing_timer.map_or(0.0, |t| t.stop_and_record())).as_millis(); info!( - log, - "Publishing signed block"; - "slot" => slot.as_u64(), - "signing_time_ms" => signing_time_ms, + slot = slot.as_u64(), + signing_time_ms = signing_time_ms, + "Publishing signed block" ); // Publish block with first available beacon node. @@ -396,13 +399,12 @@ impl BlockService { .await?; info!( - log, - "Successfully published block"; - "block_type" => ?signed_block.block_type(), - "deposits" => signed_block.num_deposits(), - "attestations" => signed_block.num_attestations(), - "graffiti" => ?graffiti.map(|g| g.as_utf8_lossy()), - "slot" => signed_block.slot().as_u64(), + block_type = ?signed_block.block_type(), + deposits = signed_block.num_deposits(), + attestations = signed_block.num_attestations(), + graffiti = ?graffiti.map(|g| g.as_utf8_lossy()), + slot = signed_block.slot().as_u64(), + "Successfully published block" ); Ok(()) } @@ -413,7 +415,6 @@ impl BlockService { validator_pubkey: PublicKeyBytes, builder_boost_factor: Option, ) -> Result<(), BlockError> { - let log = self.context.log(); let _timer = validator_metrics::start_timer_vec( &validator_metrics::BLOCK_SERVICE_TIMES, &[validator_metrics::BEACON_BLOCK], @@ -421,7 +422,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(), @@ -429,11 +430,10 @@ impl BlockService { // A pubkey can be missing when a validator was recently removed // via the API. warn!( - log, - "Missing pubkey for block randao"; - "info" => "a validator may have recently been removed from this VC", - "pubkey" => ?pubkey, - "slot" => ?slot + info = "a validator may have recently been removed from this VC", + ?pubkey, + ?slot, + "Missing pubkey for block randao" ); return Ok(()); } @@ -447,7 +447,6 @@ impl BlockService { let graffiti = determine_graffiti( &validator_pubkey, - log, self.graffiti_file.clone(), self.validator_store.graffiti(&validator_pubkey), self.graffiti, @@ -461,11 +460,7 @@ impl BlockService { proposer_nodes: self.proposer_nodes.clone(), }; - info!( - log, - "Requesting unsigned block"; - "slot" => slot.as_u64(), - ); + info!(slot = slot.as_u64(), "Requesting unsigned block"); // Request block from first responsive beacon node. // @@ -484,7 +479,6 @@ impl BlockService { graffiti, proposer_index, builder_boost_factor, - log, ) .await .map_err(|e| { @@ -511,10 +505,9 @@ impl BlockService { async fn publish_signed_block_contents( &self, - signed_block: &SignedBlock, + signed_block: &SignedBlock, beacon_node: BeaconNodeHttpClient, ) -> Result<(), BlockError> { - let log = self.context.log(); let slot = signed_block.slot(); match signed_block { SignedBlock::Full(signed_block) => { @@ -525,7 +518,7 @@ impl BlockService { beacon_node .post_beacon_blocks_v2_ssz(signed_block, None) .await - .or_else(|e| handle_block_post_error(e, slot, log))? + .or_else(|e| handle_block_post_error(e, slot))? } SignedBlock::Blinded(signed_block) => { let _post_timer = validator_metrics::start_timer_vec( @@ -535,7 +528,7 @@ impl BlockService { beacon_node .post_beacon_blinded_blocks_v2_ssz(signed_block, None) .await - .or_else(|e| handle_block_post_error(e, slot, log))? + .or_else(|e| handle_block_post_error(e, slot))? } } Ok::<_, BlockError>(()) @@ -548,10 +541,9 @@ impl BlockService { graffiti: Option, proposer_index: Option, builder_boost_factor: Option, - log: &Logger, - ) -> Result, BlockError> { + ) -> Result, BlockError> { let (block_response, _) = beacon_node - .get_validator_blocks_v3::( + .get_validator_blocks_v3::( slot, randao_reveal_ref, graffiti.as_ref(), @@ -570,11 +562,7 @@ impl BlockService { eth2::types::ProduceBlockV3Response::Blinded(block) => UnsignedBlock::Blinded(block), }; - info!( - log, - "Received unsigned block"; - "slot" => slot.as_u64(), - ); + info!(slot = slot.as_u64(), "Received unsigned block"); if proposer_index != Some(unsigned_block.proposer_index()) { return Err(BlockError::Recoverable( "Proposer index does not match block proposer. Beacon chain re-orged".to_string(), @@ -583,36 +571,6 @@ 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 { @@ -662,23 +620,21 @@ impl SignedBlock { } } -fn handle_block_post_error(err: eth2::Error, slot: Slot, log: &Logger) -> Result<(), BlockError> { +fn handle_block_post_error(err: eth2::Error, slot: Slot) -> Result<(), BlockError> { // Handle non-200 success codes. if let Some(status) = err.status() { if status == StatusCode::ACCEPTED { info!( - log, - "Block is already known to BN or might be invalid"; - "slot" => slot, - "status_code" => status.as_u16(), + %slot, + status_code = status.as_u16(), + "Block is already known to BN or might be invalid" ); return Ok(()); } else if status.is_success() { debug!( - log, - "Block published with non-standard success code"; - "slot" => slot, - "status_code" => status.as_u16(), + %slot, + status_code = status.as_u16(), + "Block published with non-standard success code" ); return Ok(()); } diff --git a/validator_client/validator_services/src/duties_service.rs b/validator_client/validator_services/src/duties_service.rs index 7437ff8bcf7..b4d9bae2732 100644 --- a/validator_client/validator_services/src/duties_service.rs +++ b/validator_client/validator_services/src/duties_service.rs @@ -10,25 +10,24 @@ 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, ProposerData, StateId, ValidatorId, }; use futures::{stream, StreamExt}; use parking_lot::RwLock; use safe_arith::{ArithError, SafeArith}; -use slog::{debug, error, info, warn, Logger}; use slot_clock::SlotClock; use std::cmp::min; 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; @@ -87,16 +86,16 @@ 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), } -impl From for Error { +impl From for Error { fn from(e: ArithError) -> Self { Self::Arith(e) } @@ -125,11 +124,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 @@ -205,25 +204,132 @@ type DependentRoot = Hash256; type AttesterMap = HashMap>; type ProposerMap = 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(), + 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, + 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. @@ -233,7 +339,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() @@ -284,7 +390,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 @@ -309,7 +415,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 @@ -347,15 +453,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 @@ -378,8 +484,7 @@ pub fn start_update_service( * Spawn the task which keeps track of local block proposal duties. */ let duties_service = core_duties_service.clone(); - let log = core_duties_service.context.log().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() { @@ -394,9 +499,8 @@ pub fn start_update_service( if let Err(e) = poll_beacon_proposers(&duties_service, &mut block_service_tx).await { error!( - log, - "Failed to poll beacon proposers"; - "error" => ?e + error = ?e, + "Failed to poll beacon proposers" ) } } @@ -413,8 +517,7 @@ pub fn start_update_service( * Spawn the task which keeps track of local attestation duties. */ let duties_service = core_duties_service.clone(); - let log = core_duties_service.context.log().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() { @@ -428,9 +531,8 @@ pub fn start_update_service( if let Err(e) = poll_beacon_attesters(&duties_service).await { error!( - log, - "Failed to poll beacon attesters"; - "error" => ?e + error = ?e, + "Failed to poll beacon attesters" ); } } @@ -440,15 +542,13 @@ pub fn start_update_service( // Spawn the task which keeps track of local sync committee duties. let duties_service = core_duties_service.clone(); - let log = core_duties_service.context.log().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 { error!( - log, - "Failed to poll sync committee duties"; - "error" => ?e + error = ?e, + "Failed to poll sync committee duties" ); } @@ -472,16 +572,14 @@ 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, &[validator_metrics::UPDATE_INDICES], ); - let log = duties_service.context.log(); - // Collect *all* pubkeys for resolving indices, even those undergoing doppelganger protection. // // Since doppelganger protection queries rely on validator indices it is important to ensure we @@ -494,16 +592,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. @@ -547,17 +643,14 @@ async fn poll_validator_indices( match download_result { Ok(Some(response)) => { info!( - log, - "Validator exists in beacon chain"; - "pubkey" => ?pubkey, - "validator_index" => response.data.index, - "fee_recipient" => fee_recipient + ?pubkey, + validator_index = response.data.index, + fee_recipient, + "Validator exists in beacon chain" ); 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 @@ -568,28 +661,22 @@ 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() .insert(pubkey, next_poll_slot); } - debug!( - log, - "Validator without index"; - "pubkey" => ?pubkey, - "fee_recipient" => fee_recipient - ) + debug!(?pubkey, fee_recipient, "Validator without index") } // Don't exit early on an error, keep attempting to resolve other indices. Err(e) => { error!( - log, - "Failed to resolve pubkey to index"; - "error" => %e, - "pubkey" => ?pubkey, - "fee_recipient" => fee_recipient + error = %e, + ?pubkey, + fee_recipient, + "Failed to resolve pubkey to index" ) } } @@ -605,21 +692,19 @@ 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], ); - let log = duties_service.context.log(); - 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. @@ -633,10 +718,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) } } @@ -653,15 +736,14 @@ async fn poll_beacon_attesters( .await { error!( - log, - "Failed to download attester duties"; - "current_epoch" => current_epoch, - "request_epoch" => current_epoch, - "err" => ?e, + %current_epoch, + request_epoch = %current_epoch, + err = ?e, + "Failed to download attester duties" ) } - 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( @@ -675,15 +757,14 @@ async fn poll_beacon_attesters( .await { error!( - log, - "Failed to download attester duties"; - "current_epoch" => current_epoch, - "request_epoch" => next_epoch, - "err" => ?e, + %current_epoch, + request_epoch = %next_epoch, + err = ?e, + "Failed to download attester duties" ) } - 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( @@ -704,7 +785,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); @@ -758,9 +839,8 @@ async fn poll_beacon_attesters( .await; if subscription_result.as_ref().is_ok() { debug!( - log, - "Broadcast attestation subscriptions"; - "count" => subscriptions.len(), + count = subscriptions.len(), + "Broadcast attestation subscriptions" ); for subscription_slots in subscription_slots_to_confirm { subscription_slots.record_successful_subscription_at(current_slot); @@ -768,9 +848,8 @@ async fn poll_beacon_attesters( } else if let Err(e) = subscription_result { if e.num_errors() < duties_service.beacon_nodes.num_total().await { warn!( - log, - "Some subscriptions failed"; - "error" => %e, + error = %e, + "Some subscriptions failed" ); // If subscriptions were sent to at least one node, regard that as a success. // There is some redundancy built into the subscription schedule to handle failures. @@ -779,9 +858,8 @@ async fn poll_beacon_attesters( } } else { error!( - log, - "All subscriptions failed"; - "error" => %e + error = %e, + "All subscriptions failed" ); } } @@ -803,20 +881,17 @@ 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> { - let log = duties_service.context.log(); - +) -> Result<(), Error> { // No need to bother the BN if we don't have any validators. if local_indices.is_empty() { debug!( - duties_service.context.log(), - "No validators, not downloading duties"; - "epoch" => epoch, + %epoch, + "No validators, not downloading duties" ); return Ok(()); } @@ -895,10 +970,9 @@ async fn poll_beacon_attesters_for_epoch( ); debug!( - log, - "Downloaded attester duties"; - "dependent_root" => %dependent_root, - "num_new_duties" => new_duties.len(), + %dependent_root, + num_new_duties = new_duties.len(), + "Downloaded attester duties" ); // Update the duties service with the new `DutyAndProof` messages. @@ -929,10 +1003,9 @@ async fn poll_beacon_attesters_for_epoch( && prior_duty_and_proof.duty == duty_and_proof.duty { warn!( - log, - "Redundant attester duty update"; - "dependent_root" => %dependent_root, - "validator_index" => duty.validator_index, + %dependent_root, + validator_index = duty.validator_index, + "Redundant attester duty update" ); continue; } @@ -940,11 +1013,10 @@ async fn poll_beacon_attesters_for_epoch( // Using `already_warned` avoids excessive logs. if dependent_root != *prior_dependent_root && already_warned.take().is_some() { warn!( - log, - "Attester duties re-org"; - "prior_dependent_root" => %prior_dependent_root, - "dependent_root" => %dependent_root, - "note" => "this may happen from time to time" + %prior_dependent_root, + %dependent_root, + note = "this may happen from time to time", + "Attester duties re-org" ) } *mut_value = (dependent_root, duty_and_proof); @@ -958,7 +1030,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; }, @@ -969,8 +1041,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 { @@ -986,8 +1058,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, ) { @@ -1002,14 +1074,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) { @@ -1027,11 +1099,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 { @@ -1051,13 +1123,11 @@ 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, ) { - let log = duties_service.context.log(); - // Sort duties by slot in a BTreeMap. let mut duties_by_slot: BTreeMap> = BTreeMap::new(); @@ -1105,7 +1175,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?; @@ -1125,20 +1195,18 @@ async fn fill_in_selection_proofs( // A pubkey can be missing when a validator was recently // removed via the API. warn!( - log, - "Missing pubkey for duty and proof"; - "info" => "a validator may have recently been removed from this VC", - "pubkey" => ?pubkey, + info = "a validator may have recently been removed from this VC", + ?pubkey, + "Missing pubkey for duty and proof" ); // Do not abort the entire batch for a single failure. continue; } Err(e) => { error!( - log, - "Failed to produce duty and proof"; - "error" => ?e, - "msg" => "may impair attestation duties" + error = ?e, + msg = "may impair attestation duties", + "Failed to produce duty and proof" ); // Do not abort the entire batch for a single failure. continue; @@ -1146,7 +1214,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. @@ -1163,9 +1231,8 @@ async fn fill_in_selection_proofs( // Our selection proofs are no longer relevant due to a reorg, abandon // this entire background process. debug!( - log, - "Stopping selection proof background task"; - "reason" => "re-org" + reason = "re-org", + "Stopping selection proof background task" ); return; } @@ -1188,11 +1255,10 @@ async fn fill_in_selection_proofs( let time_taken_ms = Duration::from_secs_f64(timer.map_or(0.0, |t| t.stop_and_record())).as_millis(); debug!( - log, - "Computed attestation selection proofs"; - "batch_size" => batch_size, - "lookahead_slot" => lookahead_slot, - "time_taken_ms" => time_taken_ms + batch_size, + %lookahead_slot, + time_taken_ms, + "Computed attestation selection proofs" ); } else { // Just sleep for one slot if we are unable to read the system clock, this gives @@ -1225,33 +1291,30 @@ async fn fill_in_selection_proofs( /// through the slow path every time. I.e., the proposal will only happen after we've been able to /// download and process the duties from the BN. This means it is very important to ensure this /// function is as fast as possible. -async fn poll_beacon_proposers( - 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], ); - let log = duties_service.context.log(); - 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()); // 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, - log, + duties_service.validator_store.as_ref(), ) .await; @@ -1290,10 +1353,9 @@ async fn poll_beacon_proposers( .collect::>(); debug!( - log, - "Downloaded proposer duties"; - "dependent_root" => %dependent_root, - "num_relevant_duties" => relevant_duties.len(), + %dependent_root, + num_relevant_duties = relevant_duties.len(), + "Downloaded proposer duties" ); if let Some((prior_dependent_root, _)) = duties_service @@ -1303,20 +1365,18 @@ async fn poll_beacon_proposers( { if dependent_root != prior_dependent_root { warn!( - log, - "Proposer duties re-org"; - "prior_dependent_root" => %prior_dependent_root, - "dependent_root" => %dependent_root, - "msg" => "this may happen from time to time" + %prior_dependent_root, + %dependent_root, + msg = "this may happen from time to time", + "Proposer duties re-org" ) } } } // Don't return early here, we still want to try and produce blocks using the cached values. Err(e) => error!( - log, - "Failed to download proposer duties"; - "err" => %e, + err = %e, + "Failed to download proposer duties" ), } @@ -1336,18 +1396,16 @@ 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, - log, + duties_service.validator_store.as_ref(), ) .await; debug!( - log, - "Detected new block proposer"; - "current_slot" => current_slot, + %current_slot, + "Detected new block proposer" ); validator_metrics::inc_counter(&validator_metrics::PROPOSAL_CHANGED); } @@ -1363,12 +1421,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, - log: &Logger, + validator_store: &S, ) { let non_doppelganger_proposers = block_proposers .iter() @@ -1385,10 +1442,9 @@ async fn notify_block_production_service( .await { error!( - log, - "Failed to notify block service"; - "current_slot" => current_slot, - "error" => %e + %current_slot, + error = %e, + "Failed to notify block service" ); }; } diff --git a/validator_client/validator_services/src/preparation_service.rs b/validator_client/validator_services/src/preparation_service.rs index fe6eab3a8a7..b59e3266dc9 100644 --- a/validator_client/validator_services/src/preparation_service.rs +++ b/validator_client/validator_services/src/preparation_service.rs @@ -1,21 +1,22 @@ use beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; use bls::PublicKeyBytes; -use doppelganger_service::DoppelgangerStatus; -use environment::RuntimeContext; use parking_lot::RwLock; -use slog::{debug, error, info, warn}; use slot_clock::SlotClock; use std::collections::HashMap; 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) @@ -173,15 +174,10 @@ impl PreparationService { /// Starts the service which periodically produces proposer preparations. pub fn start_proposer_prepare_service(self, spec: &ChainSpec) -> Result<(), String> { - let log = self.context.log().clone(); - let slot_duration = Duration::from_secs(spec.seconds_per_slot); - info!( - log, - "Proposer preparation service started"; - ); + 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 { @@ -192,9 +188,8 @@ impl PreparationService { .await .map_err(|e| { error!( - log, - "Error during proposer preparation"; - "error" => ?e, + error = ?e, + "Error during proposer preparation" ) }) .unwrap_or(()); @@ -203,7 +198,7 @@ impl PreparationService { if let Some(duration_to_next_slot) = self.slot_clock.duration_to_next_slot() { sleep(duration_to_next_slot).await; } else { - error!(log, "Failed to read slot clock"); + error!("Failed to read slot clock"); // If we can't read the slot clock, just wait another slot. sleep(slot_duration).await; } @@ -216,30 +211,25 @@ impl PreparationService { /// Starts the service which periodically sends connected beacon nodes validator registration information. pub fn start_validator_registration_service(self, spec: &ChainSpec) -> Result<(), String> { - let log = self.context.log().clone(); - - info!( - log, - "Validator registration service started"; - ); + info!("Validator registration service started"); 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 { // Poll the endpoint immediately to ensure fee recipients are received. if let Err(e) = self.register_validators().await { - error!(log,"Error during validator registration";"error" => ?e); + error!(error = ?e, "Error during validator registration"); } // Wait one slot if the register validator request fails or if we should not publish at the current slot. if let Some(duration_to_next_slot) = self.slot_clock.duration_to_next_slot() { sleep(duration_to_next_slot).await; } else { - error!(log, "Failed to read slot clock"); + error!("Failed to read slot clock"); // If we can't read the slot clock, just wait another slot. sleep(slot_duration).await; } @@ -254,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 }) @@ -274,7 +263,6 @@ impl PreparationService { } fn collect_preparation_data(&self, spec: &ChainSpec) -> Vec { - let log = self.context.log(); self.collect_proposal_data(|pubkey, proposal_data| { if let Some(fee_recipient) = proposal_data.fee_recipient { Some(ProposerPreparationData { @@ -285,10 +273,9 @@ impl PreparationService { } else { if spec.bellatrix_fork_epoch.is_some() { error!( - log, - "Validator is missing fee recipient"; - "msg" => "update validator_definitions.yml", - "pubkey" => ?pubkey + msg = "update validator_definitions.yml", + ?pubkey, + "Validator is missing fee recipient" ); } None @@ -336,8 +323,6 @@ impl PreparationService { &self, preparation_data: Vec, ) -> Result<(), String> { - let log = self.context.log(); - // Post the proposer preparations to the BN. let preparation_data_len = preparation_data.len(); let preparation_entries = preparation_data.as_slice(); @@ -351,14 +336,12 @@ impl PreparationService { .await { Ok(()) => debug!( - log, - "Published proposer preparation"; - "count" => preparation_data_len, + count = preparation_data_len, + "Published proposer preparation" ), Err(e) => error!( - log, - "Unable to publish proposer preparation to all beacon nodes"; - "error" => %e, + error = %e, + "Unable to publish proposer preparation to all beacon nodes" ), } Ok(()) @@ -384,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() { @@ -400,8 +384,6 @@ impl PreparationService { &self, registration_keys: Vec, ) -> Result<(), String> { - let log = self.context.log(); - let registration_data_len = registration_keys.len(); let mut signed = Vec::with_capacity(registration_data_len); @@ -428,7 +410,7 @@ impl PreparationService { pubkey, } = key.clone(); - let signed_data = match self + match self .validator_store .sign_validator_registration_data(ValidatorRegistrationData { fee_recipient, @@ -442,29 +424,18 @@ impl PreparationService { Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { // A pubkey can be missing when a validator was recently // removed via the API. - debug!( - log, - "Missing pubkey for registration data"; - "pubkey" => ?pubkey, - ); + debug!(?pubkey, "Missing pubkey for registration data"); continue; } Err(e) => { error!( - log, - "Unable to sign validator registration data"; - "error" => ?e, - "pubkey" => ?pubkey + error = ?e, + ?pubkey, + "Unable to sign validator registration data" ); continue; } - }; - - self.validator_registration_cache - .write() - .insert(key, signed_data.clone()); - - signed_data + } }; signed.push(signed_data); } @@ -478,15 +449,22 @@ impl PreparationService { }) .await { - Ok(()) => info!( - log, - "Published validator registrations to the builder network"; - "count" => batch.len(), - ), + Ok(()) => { + info!( + count = batch.len(), + "Published validator registrations to the builder network" + ); + let mut guard = self.validator_registration_cache.write(); + for signed_data in batch { + guard.insert( + ValidatorRegistrationKey::from(signed_data.message.clone()), + signed_data.clone(), + ); + } + } Err(e) => warn!( - log, - "Unable to publish validator registrations to the builder network"; - "error" => %e, + error = %e, + "Unable to publish validator registrations to the builder network" ), } } diff --git a/validator_client/validator_services/src/sync.rs b/validator_client/validator_services/src/sync.rs index 6c983b54307..c13b70db802 100644 --- a/validator_client/validator_services/src/sync.rs +++ b/validator_client/validator_services/src/sync.rs @@ -1,14 +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 slog::{crit, debug, info, warn}; 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; @@ -27,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. @@ -80,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, } } @@ -103,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 { @@ -116,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, @@ -126,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), @@ -186,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, @@ -283,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 @@ -316,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) } } @@ -341,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, @@ -378,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, @@ -408,29 +411,26 @@ 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; - let log = duties_service.context.log(); // no local validators don't need to poll for sync committee if local_indices.is_empty() { debug!( - duties_service.context.log(), - "No validators, not polling for sync committee duties"; - "sync_committee_period" => sync_committee_period, + sync_committee_period, + "No validators, not polling for sync committee duties" ); return Ok(()); } debug!( - log, - "Fetching sync committee duties"; - "sync_committee_period" => sync_committee_period, - "num_validators" => local_indices.len(), + sync_committee_period, + num_validators = local_indices.len(), + "Fetching sync committee duties" ); let period_start_epoch = spec.epochs_per_sync_committee_period * sync_committee_period; @@ -452,16 +452,15 @@ pub async fn poll_sync_committee_duties_for_period res.data, Err(e) => { warn!( - log, - "Failed to download sync committee duties"; - "sync_committee_period" => sync_committee_period, - "error" => %e, + sync_committee_period, + error = %e, + "Failed to download sync committee duties" ); return Ok(()); } }; - debug!(log, "Fetched sync duties from BN"; "count" => duties.len()); + debug!(count = duties.len(), "Fetched sync duties from BN"); // Add duties to map. let committee_duties = duties_service @@ -479,9 +478,8 @@ pub async fn poll_sync_committee_duties_for_period "this could be due to a really long re-org, or a bug" + message = "this could be due to a really long re-org, or a bug", + "Sync committee duties changed" ); } updated_due_to_reorg @@ -489,10 +487,8 @@ pub async fn poll_sync_committee_duties_for_period duty.validator_index, - "sync_committee_period" => sync_committee_period, + validator_index = duty.validator_index, + sync_committee_period, "Validator in sync committee" ); *validator_duties = Some(ValidatorDuties::new(duty)); @@ -502,21 +498,18 @@ 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, pre_compute_slot: Slot, ) { - let log = duties_service.context.log(); - debug!( - log, - "Calculating sync selection proofs"; - "period" => sync_committee_period, - "current_slot" => current_slot, - "pre_compute_slot" => pre_compute_slot + period = sync_committee_period, + %current_slot, + %pre_compute_slot, + "Calculating sync selection proofs" ); // Generate selection proofs for each validator at each slot, one slot at a time. @@ -528,13 +521,12 @@ 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!( - log, - "Arithmetic error computing subnet IDs"; - "error" => ?e, + error = ?e, + "Arithmetic error computing subnet IDs" ); continue; } @@ -556,45 +548,41 @@ pub async fn fill_in_aggregation_proofs( // A pubkey can be missing when a validator was recently // removed via the API. debug!( - log, - "Missing pubkey for sync selection proof"; - "pubkey" => ?pubkey, - "pubkey" => ?duty.pubkey, - "slot" => proof_slot, + ?pubkey, + pubkey = ?duty.pubkey, + slot = %proof_slot, + "Missing pubkey for sync selection proof" ); return None; } Err(e) => { warn!( - log, - "Unable to sign selection proof"; - "error" => ?e, - "pubkey" => ?duty.pubkey, - "slot" => proof_slot, + error = ?e, + pubkey = ?duty.pubkey, + slot = %proof_slot, + "Unable to sign selection proof" ); return None; } }; - match proof.is_aggregator::() { + match proof.is_aggregator::() { Ok(true) => { debug!( - log, - "Validator is sync aggregator"; - "validator_index" => duty.validator_index, - "slot" => proof_slot, - "subnet_id" => %subnet_id, + validator_index = duty.validator_index, + slot = %proof_slot, + %subnet_id, + "Validator is sync aggregator" ); Some(((proof_slot, *subnet_id), proof)) } Ok(false) => None, Err(e) => { warn!( - log, - "Error determining is_aggregator"; - "pubkey" => ?duty.pubkey, - "slot" => proof_slot, - "error" => ?e, + pubkey = ?duty.pubkey, + slot = %proof_slot, + error = ?e, + "Error determining is_aggregator" ); None } @@ -614,11 +602,7 @@ pub async fn fill_in_aggregation_proofs( // Add to global storage (we add regularly so the proofs can be used ASAP). let sync_map = duties_service.sync_duties.committees.read(); let Some(committee_duties) = sync_map.get(&sync_committee_period) else { - debug!( - log, - "Missing sync duties"; - "period" => sync_committee_period, - ); + debug!(period = sync_committee_period, "Missing sync duties"); continue; }; let validators = committee_duties.validators.read(); @@ -629,20 +613,18 @@ pub async fn fill_in_aggregation_proofs( duty.aggregation_duties.proofs.write().extend(proofs); } else { debug!( - log, - "Missing sync duty to update"; - "validator_index" => validator_index, - "period" => sync_committee_period, + validator_index, + period = sync_committee_period, + "Missing sync duty to update" ); } } if num_validators_updated > 0 { debug!( - log, - "Finished computing sync selection proofs"; - "slot" => slot, - "updated_validators" => num_validators_updated, + %slot, + updated_validators = num_validators_updated, + "Finished computing sync selection proofs" ); } } diff --git a/validator_client/validator_services/src/sync_committee_service.rs b/validator_client/validator_services/src/sync_committee_service.rs index 5f84c517f37..be9e2918a4b 100644 --- a/validator_client/validator_services/src/sync_committee_service.rs +++ b/validator_client/validator_services/src/sync_committee_service.rs @@ -1,16 +1,17 @@ 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; -use slog::{crit, debug, error, info, trace, warn}; +use logging::crit; use slot_clock::SlotClock; use std::collections::HashMap; use std::ops::Deref; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use task_executor::TaskExecutor; use tokio::time::{sleep, sleep_until, Duration, Instant}; +use tracing::{debug, error, info, trace, warn}; use types::{ ChainSpec, EthSpec, Hash256, PublicKeyBytes, Slot, SyncCommitteeSubscription, SyncContributionData, SyncDuty, SyncSelectionProof, SyncSubnetId, @@ -19,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(), @@ -31,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 { @@ -65,7 +66,7 @@ impl SyncCommitteeService { validator_store, slot_clock, beacon_nodes, - context, + executor, first_subscription_done: AtomicBool::new(false), }), } @@ -79,16 +80,15 @@ 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) } pub fn start_update_service(self, spec: &ChainSpec) -> Result<(), String> { - let log = self.context.log().clone(); if self.duties_service.disable_attesting { - info!(log, "Sync committee service disabled"); + info!("Sync committee service disabled"); return Ok(()); } @@ -99,18 +99,16 @@ impl SyncCommitteeService { .ok_or("Unable to determine duration to next slot")?; info!( - log, - "Sync committee service started"; - "next_update_millis" => duration_to_next_slot.as_millis() + next_update_millis = duration_to_next_slot.as_millis(), + "Sync committee service started" ); - let executor = self.context.executor.clone(); + let executor = self.executor.clone(); let interval_fut = async move { loop { if let Some(duration_to_next_slot) = self.slot_clock.duration_to_next_slot() { // Wait for contribution broadcast interval 1/3 of the way through the slot. - let log = self.context.log(); sleep(duration_to_next_slot + slot_duration / 3).await; // Do nothing if the Altair fork has not yet occurred. @@ -120,21 +118,17 @@ impl SyncCommitteeService { if let Err(e) = self.spawn_contribution_tasks(slot_duration).await { crit!( - log, - "Failed to spawn sync contribution tasks"; - "error" => e + error = ?e, + "Failed to spawn sync contribution tasks" ) } else { - trace!( - log, - "Spawned sync contribution tasks"; - ) + trace!("Spawned sync contribution tasks") } // Do subscriptions for future slots/epochs. self.spawn_subscription_tasks(); } else { - error!(log, "Failed to read slot clock"); + error!("Failed to read slot clock"); // If we can't read the slot clock, just wait another slot. sleep(slot_duration).await; } @@ -146,7 +140,6 @@ impl SyncCommitteeService { } async fn spawn_contribution_tasks(&self, slot_duration: Duration) -> Result<(), String> { - let log = self.context.log().clone(); let slot = self.slot_clock.now().ok_or("Failed to read slot clock")?; let duration_to_next_slot = self .slot_clock @@ -163,18 +156,14 @@ impl SyncCommitteeService { let Some(slot_duties) = self .duties_service .sync_duties - .get_duties_for_slot(slot, &self.duties_service.spec) + .get_duties_for_slot::(slot, &self.duties_service.spec) else { - debug!(log, "No duties known for slot {}", slot); + debug!("No duties known for slot {}", slot); return Ok(()); }; if slot_duties.duties.is_empty() { - debug!( - log, - "No local validators in current sync committee"; - "slot" => slot, - ); + debug!(%slot, "No local validators in current sync committee"); return Ok(()); } @@ -201,11 +190,10 @@ impl SyncCommitteeService { Ok(block) => block.data.root, Err(errs) => { warn!( - log, + errors = errs.to_string(), + %slot, "Refusing to sign sync committee messages for an optimistic head block or \ - a block head with unknown optimistic status"; - "errors" => errs.to_string(), - "slot" => slot, + a block head with unknown optimistic status" ); return Ok(()); } @@ -214,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) @@ -226,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( @@ -251,8 +239,6 @@ impl SyncCommitteeService { beacon_block_root: Hash256, validator_duties: Vec, ) -> Result<(), ()> { - let log = self.context.log(); - // Create futures to produce sync committee signatures. let signature_futures = validator_duties.iter().map(|duty| async move { match self @@ -270,21 +256,19 @@ impl SyncCommitteeService { // A pubkey can be missing when a validator was recently // removed via the API. debug!( - log, - "Missing pubkey for sync committee signature"; - "pubkey" => ?pubkey, - "validator_index" => duty.validator_index, - "slot" => slot, + ?pubkey, + validator_index = duty.validator_index, + %slot, + "Missing pubkey for sync committee signature" ); None } Err(e) => { crit!( - log, - "Failed to sign sync committee signature"; - "validator_index" => duty.validator_index, - "slot" => slot, - "error" => ?e, + validator_index = duty.validator_index, + %slot, + error = ?e, + "Failed to sign sync committee signature" ); None } @@ -307,19 +291,17 @@ impl SyncCommitteeService { .await .map_err(|e| { error!( - log, - "Unable to publish sync committee messages"; - "slot" => slot, - "error" => %e, + %slot, + error = %e, + "Unable to publish sync committee messages" ); })?; info!( - log, - "Successfully published sync committee messages"; - "count" => committee_signatures.len(), - "head_block" => ?beacon_block_root, - "slot" => slot, + count = committee_signatures.len(), + head_block = ?beacon_block_root, + %slot, + "Successfully published sync committee messages" ); Ok(()) @@ -334,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( @@ -362,8 +344,6 @@ impl SyncCommitteeService { ) -> Result<(), ()> { sleep_until(aggregate_instant).await; - let log = self.context.log(); - let contribution = &self .beacon_nodes .first_success(|beacon_node| async move { @@ -374,26 +354,20 @@ impl SyncCommitteeService { }; beacon_node - .get_validator_sync_committee_contribution::(&sync_contribution_data) + .get_validator_sync_committee_contribution(&sync_contribution_data) .await }) .await .map_err(|e| { crit!( - log, - "Failed to produce sync contribution"; - "slot" => slot, - "beacon_block_root" => ?beacon_block_root, - "error" => %e, + %slot, + ?beacon_block_root, + error = %e, + "Failed to produce sync contribution" ) })? .ok_or_else(|| { - crit!( - log, - "No aggregate contribution found"; - "slot" => slot, - "beacon_block_root" => ?beacon_block_root, - ); + crit!(%slot, ?beacon_block_root, "No aggregate contribution found"); })? .data; @@ -414,20 +388,14 @@ impl SyncCommitteeService { Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { // A pubkey can be missing when a validator was recently // removed via the API. - debug!( - log, - "Missing pubkey for sync contribution"; - "pubkey" => ?pubkey, - "slot" => slot, - ); + debug!(?pubkey, %slot, "Missing pubkey for sync contribution"); None } Err(e) => { crit!( - log, - "Unable to sign sync committee contribution"; - "slot" => slot, - "error" => ?e, + %slot, + error = ?e, + "Unable to sign sync committee contribution" ); None } @@ -452,20 +420,18 @@ impl SyncCommitteeService { .await .map_err(|e| { error!( - log, - "Unable to publish signed contributions and proofs"; - "slot" => slot, - "error" => %e, + %slot, + error = %e, + "Unable to publish signed contributions and proofs" ); })?; info!( - log, - "Successfully published sync contributions"; - "subnet" => %subnet_id, - "beacon_block_root" => %beacon_block_root, - "num_signers" => contribution.aggregation_bits.num_set_bits(), - "slot" => slot, + subnet = %subnet_id, + beacon_block_root = %beacon_block_root, + num_signers = contribution.aggregation_bits.num_set_bits(), + %slot, + "Successfully published sync contributions" ); Ok(()) @@ -473,14 +439,13 @@ impl SyncCommitteeService { fn spawn_subscription_tasks(&self) { let service = self.clone(); - let log = self.context.log().clone(); - self.inner.context.executor.spawn( + + self.inner.executor.spawn( async move { service.publish_subscriptions().await.unwrap_or_else(|e| { error!( - log, - "Error publishing subscriptions"; - "error" => ?e, + error = ?e, + "Error publishing subscriptions" ) }); }, @@ -489,7 +454,6 @@ impl SyncCommitteeService { } async fn publish_subscriptions(self) -> Result<(), String> { - let log = self.context.log().clone(); let spec = &self.duties_service.spec; let slot = self.slot_clock.now().ok_or("Failed to read slot clock")?; @@ -499,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)); } @@ -510,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)); @@ -526,16 +490,11 @@ impl SyncCommitteeService { let mut subscriptions = vec![]; for (duty_slot, sync_committee_period) in duty_slots { - debug!( - log, - "Fetching subscription duties"; - "duty_slot" => duty_slot, - "current_slot" => slot, - ); + debug!(%duty_slot, %slot, "Fetching subscription duties"); 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, @@ -544,9 +503,8 @@ impl SyncCommitteeService { )), None => { debug!( - log, - "No duties for subscription"; - "slot" => duty_slot, + slot = %duty_slot, + "No duties for subscription" ); all_succeeded = false; } @@ -554,29 +512,23 @@ impl SyncCommitteeService { } if subscriptions.is_empty() { - debug!( - log, - "No sync subscriptions to send"; - "slot" => slot, - ); + debug!(%slot, "No sync subscriptions to send"); return Ok(()); } // Post subscriptions to BN. debug!( - log, - "Posting sync subscriptions to BN"; - "count" => subscriptions.len(), + count = subscriptions.len(), + "Posting sync subscriptions to BN" ); let subscriptions_slice = &subscriptions; for subscription in subscriptions_slice { debug!( - log, - "Subscription"; - "validator_index" => subscription.validator_index, - "validator_sync_committee_indices" => ?subscription.sync_committee_indices, - "until_epoch" => subscription.until_epoch, + validator_index = subscription.validator_index, + validator_sync_committee_indices = ?subscription.sync_committee_indices, + until_epoch = %subscription.until_epoch, + "Subscription" ); } @@ -590,10 +542,9 @@ impl SyncCommitteeService { .await { error!( - log, - "Unable to post sync committee subscriptions"; - "slot" => slot, - "error" => %e, + %slot, + error = %e, + "Unable to post sync committee subscriptions" ); all_succeeded = false; } diff --git a/validator_client/validator_store/Cargo.toml b/validator_client/validator_store/Cargo.toml index 99c3025a30d..91df9dc3abd 100644 --- a/validator_client/validator_store/Cargo.toml +++ b/validator_client/validator_store/Cargo.toml @@ -4,20 +4,6 @@ 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 } -parking_lot = { workspace = true } -serde = { workspace = true } -signing_method = { workspace = true } slashing_protection = { workspace = true } -slog = { workspace = true } -slot_clock = { workspace = true } -task_executor = { 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 51140003257..9de3a6d66a0 100644 --- a/validator_client/validator_store/src/lib.rs +++ b/validator_client/validator_store/src/lib.rs @@ -1,30 +1,16 @@ -use account_utils::validator_definitions::{PasswordStorage, ValidatorDefinition}; -use doppelganger_service::{DoppelgangerService, DoppelgangerStatus, DoppelgangerValidatorStore}; -use initialized_validators::InitializedValidators; -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 slog::{crit, error, info, warn, Logger}; -use slot_clock::SlotClock; -use std::marker::PhantomData; -use std::path::Path; -use std::sync::Arc; -use task_executor::TaskExecutor; +use slashing_protection::NotSafe; +use std::fmt::Debug; +use std::future::Future; use types::{ - attestation::Error as AttestationError, graffiti::GraffitiString, AbstractExecPayload, Address, - AggregateAndProof, Attestation, BeaconBlock, BlindedPayload, ChainSpec, ContributionAndProof, - Domain, Epoch, EthSpec, Fork, Graffiti, Hash256, PublicKeyBytes, SelectionProof, Signature, - SignedAggregateAndProof, SignedBeaconBlock, SignedContributionAndProof, SignedRoot, - SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, SyncAggregatorSelectionData, - SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, - ValidatorRegistrationData, VoluntaryExit, + Address, Attestation, AttestationError, BeaconBlock, BlindedBeaconBlock, Epoch, EthSpec, + Graffiti, Hash256, PublicKeyBytes, SelectionProof, Signature, SignedAggregateAndProof, + SignedBeaconBlock, SignedBlindedBeaconBlock, SignedContributionAndProof, + 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), @@ -33,31 +19,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, @@ -66,188 +36,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, - log: Logger, - 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, - log: Logger, - ) -> 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, - log, - 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. /// @@ -255,9 +46,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. /// @@ -268,255 +57,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)) - } - - 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), - ) - } + fn get_fee_recipient(&self, validator_pubkey: &PublicKeyBytes) -> Option
; - /// 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. @@ -524,593 +83,187 @@ 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); - } + fn randao_reveal( + &self, + validator_pubkey: PublicKeyBytes, + signing_epoch: Epoch, + ) -> impl Future>> + Send; - 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 - }) - } + fn set_validator_index(&self, validator_pubkey: &PublicKeyBytes, index: u64); - /// 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 sign_block( &self, validator_pubkey: PublicKeyBytes, - block: BeaconBlock, + 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!( - self.log, - "Not signing block with slot greater than current slot"; - "block_slot" => block.slot().as_u64(), - "current_slot" => current_slot.as_u64() - ); - 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)?; + ) -> impl Future, Error>> + Send; - // 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!( - self.log, - "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!( - self.log, - "Not signing block for unregistered validator"; - "msg" => "Carefully consider running with --init-slashing-protection (see --help)", - "public_key" => format!("{:?}", pk) - ); - validator_metrics::inc_counter_vec( - &validator_metrics::SIGNED_BLOCKS_TOTAL, - &[validator_metrics::UNREGISTERED], - ); - Err(Error::Slashable(NotSafe::UnregisteredValidator(pk))) - } - Err(e) => { - crit!( - self.log, - "Not signing slashable block"; - "error" => format!("{:?}", e) - ); - 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, - }); - } - - // 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)?; + ) -> impl Future>> + Send; - validator_metrics::inc_counter_vec( - &validator_metrics::SIGNED_ATTESTATIONS_TOTAL, - &[validator_metrics::SUCCESS], - ); - - Ok(()) - } - Ok(Safe::SameData) => { - warn!( - self.log, - "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!( - self.log, - "Not signing attestation for unregistered validator"; - "msg" => "Carefully consider running with --init-slashing-protection (see --help)", - "public_key" => format!("{:?}", pk) - ); - validator_metrics::inc_counter_vec( - &validator_metrics::SIGNED_ATTESTATIONS_TOTAL, - &[validator_metrics::UNREGISTERED], - ); - Err(Error::Slashable(NotSafe::UnregisteredValidator(pk))) - } - Err(e) => { - crit!( - self.log, - "Not signing slashable attestation"; - "attestation" => format!("{:?}", attestation.data()), - "error" => format!("{:?}", e) - ); - 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); - - // Bypass `with_validator_signing_method`: sync committee messages are not slashable. - let signing_method = self.doppelganger_bypassed_signing_method(*validator_pubkey)?; + ) -> impl Future>> + Send; - 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); - - // 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)?; + ) -> impl Future>> + Send; - 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(), - }; + /// 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); - let signature = signing_method - .get_signature::>( - SignableMessage::SignedContributionAndProof(&message), - signing_context, - &self.spec, - &self.task_executor, - ) - .await - .map_err(Error::UnableToSign)?; + /// 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; +} - validator_metrics::inc_counter_vec( - &validator_metrics::SIGNED_SYNC_COMMITTEE_CONTRIBUTIONS_TOTAL, - &[validator_metrics::SUCCESS], - ); +#[derive(Clone, Debug, PartialEq)] +pub enum UnsignedBlock { + Full(BeaconBlock), + Blinded(BlindedBeaconBlock), +} - Ok(SignedContributionAndProof { message, signature }) +impl From> for UnsignedBlock { + fn from(block: BeaconBlock) -> Self { + UnsignedBlock::Full(block) } +} - pub fn import_slashing_protection( - &self, - interchange: Interchange, - ) -> Result<(), InterchangeError> { - self.slashing_protection - .import_interchange_info(interchange, self.genesis_validators_root)?; - Ok(()) +impl From> for UnsignedBlock { + fn from(block: BlindedBeaconBlock) -> Self { + UnsignedBlock::Blinded(block) } +} - /// 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()?; +#[derive(Clone, Debug, PartialEq)] +pub enum SignedBlock { + Full(SignedBeaconBlock), + Blinded(SignedBlindedBeaconBlock), +} - 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, - ) - }) +impl From> for SignedBlock { + fn from(block: SignedBeaconBlock) -> Self { + SignedBlock::Full(block) } +} - /// Prune the slashing protection database so that it remains performant. +impl From> for SignedBlock { + fn from(block: SignedBlindedBeaconBlock) -> Self { + SignedBlock::Blinded(block) + } +} + +/// 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 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; - } + /// 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), +} - if first_run { - info!( - self.log, - "Pruning slashing protection DB"; - "epoch" => current_epoch, - "msg" => "pruning may take several minutes the first time it runs" - ); - } else { - info!(self.log, "Pruning slashing protection DB"; "epoch" => current_epoch); +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, } + } - 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!( - self.log, - "Error during pruning of signed attestations"; - "error" => ?e, - ); - return; + /// 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, } + } - if let Err(e) = self - .slashing_protection - .prune_all_signed_blocks(all_pubkeys.iter(), new_min_slot) - { - error!( - self.log, - "Error during pruning of signed blocks"; - "error" => ?e, - ); - 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), } - - *last_prune = current_epoch; - - info!(self.log, "Completed pruning of slashing protection DB"); } } diff --git a/watch/.gitignore b/watch/.gitignore deleted file mode 100644 index 5b6b0720c9e..00000000000 --- a/watch/.gitignore +++ /dev/null @@ -1 +0,0 @@ -config.yaml diff --git a/watch/Cargo.toml b/watch/Cargo.toml deleted file mode 100644 index 41cfb58e287..00000000000 --- a/watch/Cargo.toml +++ /dev/null @@ -1,45 +0,0 @@ -[package] -name = "watch" -version = "0.1.0" -edition = { workspace = true } - -[lib] -name = "watch" -path = "src/lib.rs" - -[[bin]] -name = "watch" -path = "src/main.rs" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -axum = "0.7" -beacon_node = { workspace = true } -bls = { workspace = true } -clap = { workspace = true } -clap_utils = { workspace = true } -diesel = { version = "2.0.2", features = ["postgres", "r2d2"] } -diesel_migrations = { version = "2.0.0", features = ["postgres"] } -env_logger = { workspace = true } -eth2 = { workspace = true } -hyper = { workspace = true } -log = { workspace = true } -r2d2 = { workspace = true } -rand = { workspace = true } -reqwest = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -serde_yaml = { workspace = true } -tokio = { workspace = true } -types = { workspace = true } -url = { workspace = true } - -[dev-dependencies] -beacon_chain = { workspace = true } -http_api = { workspace = true } -logging = { workspace = true } -network = { workspace = true } -task_executor = { workspace = true } -testcontainers = "0.15" -tokio-postgres = "0.7.5" -unused_port = { workspace = true } diff --git a/watch/README.md b/watch/README.md deleted file mode 100644 index 877cddf2346..00000000000 --- a/watch/README.md +++ /dev/null @@ -1,458 +0,0 @@ -## beacon.watch - ->beacon.watch is pre-MVP and still under active development and subject to change. - -beacon.watch is an Ethereum Beacon Chain monitoring platform whose goal is to provide fast access to -data which is: -1. Not already stored natively in the Beacon Chain -2. Too specialized for Block Explorers -3. Too sensitive for public Block Explorers - - -### Requirements -- `git` -- `rust` : https://rustup.rs/ -- `libpq` : https://www.postgresql.org/download/ -- `diesel_cli` : -``` -cargo install diesel_cli --no-default-features --features postgres -``` -- `docker` : https://docs.docker.com/engine/install/ -- `docker-compose` : https://docs.docker.com/compose/install/ - -### Setup -1. Setup the database: -``` -cd postgres_docker_compose -docker-compose up -``` - -1. Ensure the tests pass: -``` -cargo test --release -``` - -1. Drop the database (if it already exists) and run the required migrations: -``` -diesel database reset --database-url postgres://postgres:postgres@localhost/dev -``` - -1. Ensure a synced Lighthouse beacon node with historical states is available -at `localhost:5052`. - -1. Run the updater daemon: -``` -cargo run --release -- run-updater -``` - -1. Start the HTTP API server: -``` -cargo run --release -- serve -``` - -1. Ensure connectivity: -``` -curl "http://localhost:5059/v1/slots/highest" -``` - -> Functionality on MacOS has not been tested. Windows is not supported. - - -### Configuration -beacon.watch can be configured through the use of a config file. -Available options can be seen in `config.yaml.default`. - -You can specify a config file during runtime: -``` -cargo run -- run-updater --config path/to/config.yaml -cargo run -- serve --config path/to/config.yaml -``` - -You can specify only the parts of the config file which you need changed. -Missing values will remain as their defaults. - -For example, if you wish to run with default settings but only wish to alter `log_level` -your config file would be: -```yaml -# config.yaml -log_level = "info" -``` - -### Available Endpoints -As beacon.watch continues to develop, more endpoints will be added. - -> In these examples any data containing information from blockprint has either been redacted or fabricated. - -#### `/v1/slots/{slot}` -```bash -curl "http://localhost:5059/v1/slots/4635296" -``` -```json -{ - "slot": "4635296", - "root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62", - "skipped": false, - "beacon_block": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62" -} -``` - -#### `/v1/slots?start_slot={}&end_slot={}` -```bash -curl "http://localhost:5059/v1/slots?start_slot=4635296&end_slot=4635297" -``` -```json -[ - { - "slot": "4635297", - "root": "0x04ad2e963811207e344bebeba5b1217805bcc3a9e2ed9fcf2205d491778c6182", - "skipped": false, - "beacon_block": "0x04ad2e963811207e344bebeba5b1217805bcc3a9e2ed9fcf2205d491778c6182" - }, - { - "slot": "4635296", - "root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62", - "skipped": false, - "beacon_block": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62" - } -] -``` - -#### `/v1/slots/lowest` -```bash -curl "http://localhost:5059/v1/slots/lowest" -``` -```json -{ - "slot": "4635296", - "root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62", - "skipped": false, - "beacon_block": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62" -} -``` - -#### `/v1/slots/highest` -```bash -curl "http://localhost:5059/v1/slots/highest" -``` -```json -{ - "slot": "4635358", - "root": "0xe9eff13560688f1bf15cf07b60c84963d4d04a4a885ed0eb19ceb8450011894b", - "skipped": false, - "beacon_block": "0xe9eff13560688f1bf15cf07b60c84963d4d04a4a885ed0eb19ceb8450011894b" -} -``` - -#### `v1/slots/{slot}/block` -```bash -curl "http://localhost:5059/v1/slots/4635296/block" -``` -```json -{ - "slot": "4635296", - "root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62", - "parent_root": "0x7c4860b420a23de9d126da71f9043b3744af98c847efd9e1440f2da8fbf7f31b" -} -``` - -#### `/v1/blocks/{block_id}` -```bash -curl "http://localhost:5059/v1/blocks/4635296" -# OR -curl "http://localhost:5059/v1/blocks/0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62" -``` -```json -{ - "slot": "4635296", - "root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62", - "parent_root": "0x7c4860b420a23de9d126da71f9043b3744af98c847efd9e1440f2da8fbf7f31b" -} -``` - -#### `/v1/blocks?start_slot={}&end_slot={}` -```bash -curl "http://localhost:5059/v1/blocks?start_slot=4635296&end_slot=4635297" -``` -```json -[ - { - "slot": "4635297", - "root": "0x04ad2e963811207e344bebeba5b1217805bcc3a9e2ed9fcf2205d491778c6182", - "parent_root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62" - }, - { - "slot": "4635296", - "root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62", - "parent_root": "0x7c4860b420a23de9d126da71f9043b3744af98c847efd9e1440f2da8fbf7f31b" - } -] -``` - -#### `/v1/blocks/{block_id}/previous` -```bash -curl "http://localhost:5059/v1/blocks/4635297/previous" -# OR -curl "http://localhost:5059/v1/blocks/0x04ad2e963811207e344bebeba5b1217805bcc3a9e2ed9fcf2205d491778c6182/previous" -``` -```json -{ - "slot": "4635296", - "root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62", - "parent_root": "0x7c4860b420a23de9d126da71f9043b3744af98c847efd9e1440f2da8fbf7f31b" -} -``` - -#### `/v1/blocks/{block_id}/next` -```bash -curl "http://localhost:5059/v1/blocks/4635296/next" -# OR -curl "http://localhost:5059/v1/blocks/0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62/next" -``` -```json -{ - "slot": "4635297", - "root": "0x04ad2e963811207e344bebeba5b1217805bcc3a9e2ed9fcf2205d491778c6182", - "parent_root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62" -} -``` - -#### `/v1/blocks/lowest` -```bash -curl "http://localhost:5059/v1/blocks/lowest" -``` -```json -{ - "slot": "4635296", - "root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62", - "parent_root": "0x7c4860b420a23de9d126da71f9043b3744af98c847efd9e1440f2da8fbf7f31b" -} -``` - -#### `/v1/blocks/highest` -```bash -curl "http://localhost:5059/v1/blocks/highest" -``` -```json -{ - "slot": "4635358", - "root": "0xe9eff13560688f1bf15cf07b60c84963d4d04a4a885ed0eb19ceb8450011894b", - "parent_root": "0xb66e05418bb5b1d4a965c994e1f0e5b5f0d7b780e0df12f3f6321510654fa1d2" -} -``` - -#### `/v1/blocks/{block_id}/proposer` -```bash -curl "http://localhost:5059/v1/blocks/4635296/proposer" -# OR -curl "http://localhost:5059/v1/blocks/0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62/proposer" - -``` -```json -{ - "slot": "4635296", - "proposer_index": 223126, - "graffiti": "" -} -``` - -#### `/v1/blocks/{block_id}/rewards` -```bash -curl "http://localhost:5059/v1/blocks/4635296/reward" -# OR -curl "http://localhost:5059/v1/blocks/0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62/reward" - -``` -```json -{ - "slot": "4635296", - "total": 25380059, - "attestation_reward": 24351867, - "sync_committee_reward": 1028192 -} -``` - -#### `/v1/blocks/{block_id}/packing` -```bash -curl "http://localhost:5059/v1/blocks/4635296/packing" -# OR -curl "http://localhost:5059/v1/blocks/0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62/packing" - -``` -```json -{ - "slot": "4635296", - "available": 16152, - "included": 13101, - "prior_skip_slots": 0 -} -``` - -#### `/v1/validators/{validator}` -```bash -curl "http://localhost:5059/v1/validators/1" -# OR -curl "http://localhost:5059/v1/validators/0xa1d1ad0714035353258038e964ae9675dc0252ee22cea896825c01458e1807bfad2f9969338798548d9858a571f7425c" -``` -```json -{ - "index": 1, - "public_key": "0xa1d1ad0714035353258038e964ae9675dc0252ee22cea896825c01458e1807bfad2f9969338798548d9858a571f7425c", - "status": "active_ongoing", - "client": null, - "activation_epoch": 0, - "exit_epoch": null -} -``` - -#### `/v1/validators/{validator}/attestation/{epoch}` -```bash -curl "http://localhost:5059/v1/validators/1/attestation/144853" -# OR -curl "http://localhost:5059/v1/validators/0xa1d1ad0714035353258038e964ae9675dc0252ee22cea896825c01458e1807bfad2f9969338798548d9858a571f7425c/attestation/144853" -``` -```json -{ - "index": 1, - "epoch": "144853", - "source": true, - "head": true, - "target": true -} -``` - -#### `/v1/validators/missed/{vote}/{epoch}` -```bash -curl "http://localhost:5059/v1/validators/missed/head/144853" -``` -```json -[ - 63, - 67, - 98, - ... -] -``` - -#### `/v1/validators/missed/{vote}/{epoch}/graffiti` -```bash -curl "http://localhost:5059/v1/validators/missed/head/144853/graffiti" -``` -```json -{ - "Mr F was here": 3, - "Lighthouse/v3.1.0-aa022f4": 5, - ... -} -``` - -#### `/v1/clients/missed/{vote}/{epoch}` -```bash -curl "http://localhost:5059/v1/clients/missed/source/144853" -``` -```json -{ - "Lighthouse": 100, - "Lodestar": 100, - "Nimbus": 100, - "Prysm": 100, - "Teku": 100, - "Unknown": 100 -} -``` - -#### `/v1/clients/missed/{vote}/{epoch}/percentages` -Note that this endpoint expresses the following: -``` -What percentage of each client implementation missed this vote? -``` - -```bash -curl "http://localhost:5059/v1/clients/missed/target/144853/percentages" -``` -```json -{ - "Lighthouse": 0.51234567890, - "Lodestar": 0.51234567890, - "Nimbus": 0.51234567890, - "Prysm": 0.09876543210, - "Teku": 0.09876543210, - "Unknown": 0.05647382910 -} -``` - -#### `/v1/clients/missed/{vote}/{epoch}/percentages/relative` -Note that this endpoint expresses the following: -``` -For the validators which did miss this vote, what percentage of them were from each client implementation? -``` -You can check these values against the output of `/v1/clients/percentages` to see any discrepancies. - -```bash -curl "http://localhost:5059/v1/clients/missed/target/144853/percentages/relative" -``` -```json -{ - "Lighthouse": 11.11111111111111, - "Lodestar": 11.11111111111111, - "Nimbus": 11.11111111111111, - "Prysm": 16.66666666666667, - "Teku": 16.66666666666667, - "Unknown": 33.33333333333333 -} - -``` - -#### `/v1/clients` -```bash -curl "http://localhost:5059/v1/clients" -``` -```json -{ - "Lighthouse": 5000, - "Lodestar": 5000, - "Nimbus": 5000, - "Prysm": 5000, - "Teku": 5000, - "Unknown": 5000 -} -``` - -#### `/v1/clients/percentages` -```bash -curl "http://localhost:5059/v1/clients/percentages" -``` -```json -{ - "Lighthouse": 16.66666666666667, - "Lodestar": 16.66666666666667, - "Nimbus": 16.66666666666667, - "Prysm": 16.66666666666667, - "Teku": 16.66666666666667, - "Unknown": 16.66666666666667 -} -``` - -### Future work -- New tables - - `skip_slots`? - - -- More API endpoints - - `/v1/proposers?start_epoch={}&end_epoch={}` and similar - - `/v1/validators/{status}/count` - - -- Concurrently backfill and forwards fill, so forwards fill is not bottlenecked by large backfills. - - -- Better/prettier (async?) logging. - - -- Connect to a range of beacon_nodes to sync different components concurrently. -Generally, processing certain api queries such as `block_packing` and `attestation_performance` take the longest to sync. - - -### Architecture -Connection Pooling: -- 1 Pool for Updater (read and write) -- 1 Pool for HTTP Server (should be read only, although not sure if we can enforce this) diff --git a/watch/config.yaml.default b/watch/config.yaml.default deleted file mode 100644 index 131609237cb..00000000000 --- a/watch/config.yaml.default +++ /dev/null @@ -1,49 +0,0 @@ ---- -database: - user: "postgres" - password: "postgres" - dbname: "dev" - default_dbname: "postgres" - host: "localhost" - port: 5432 - connect_timeout_millis: 2000 - -server: - listen_addr: "127.0.0.1" - listen_port: 5059 - -updater: - # The URL of the Beacon Node to perform sync tasks with. - # Cannot yet accept multiple beacon nodes. - beacon_node_url: "http://localhost:5052" - # The number of epochs to backfill. Must be below 100. - max_backfill_size_epochs: 2 - # The epoch at which to stop backfilling. - backfill_stop_epoch: 0 - # Whether to sync the attestations table. - attestations: true - # Whether to sync the proposer_info table. - proposer_info: true - # Whether to sync the block_rewards table. - block_rewards: true - # Whether to sync the block_packing table. - block_packing: true - -blockprint: - # Whether to sync client information from blockprint. - enabled: false - # The URL of the blockprint server. - url: "" - # The username used to authenticate to the blockprint server. - username: "" - # The password used to authenticate to the blockprint server. - password: "" - -# Log level. -# Valid options are: -# - "trace" -# - "debug" -# - "info" -# - "warn" -# - "error" -log_level: "debug" diff --git a/watch/diesel.toml b/watch/diesel.toml deleted file mode 100644 index bfb01bccf0f..00000000000 --- a/watch/diesel.toml +++ /dev/null @@ -1,5 +0,0 @@ -# For documentation on how to configure this file, -# see diesel.rs/guides/configuring-diesel-cli - -[print_schema] -file = "src/database/schema.rs" diff --git a/watch/migrations/.gitkeep b/watch/migrations/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/watch/migrations/00000000000000_diesel_initial_setup/down.sql b/watch/migrations/00000000000000_diesel_initial_setup/down.sql deleted file mode 100644 index a9f52609119..00000000000 --- a/watch/migrations/00000000000000_diesel_initial_setup/down.sql +++ /dev/null @@ -1,6 +0,0 @@ --- This file was automatically created by Diesel to setup helper functions --- and other internal bookkeeping. This file is safe to edit, any future --- changes will be added to existing projects as new migrations. - -DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); -DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/watch/migrations/00000000000000_diesel_initial_setup/up.sql b/watch/migrations/00000000000000_diesel_initial_setup/up.sql deleted file mode 100644 index d68895b1a7b..00000000000 --- a/watch/migrations/00000000000000_diesel_initial_setup/up.sql +++ /dev/null @@ -1,36 +0,0 @@ --- This file was automatically created by Diesel to setup helper functions --- and other internal bookkeeping. This file is safe to edit, any future --- changes will be added to existing projects as new migrations. - - - - --- Sets up a trigger for the given table to automatically set a column called --- `updated_at` whenever the row is modified (unless `updated_at` was included --- in the modified columns) --- --- # Example --- --- ```sql --- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); --- --- SELECT diesel_manage_updated_at('users'); --- ``` -CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ -BEGIN - EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s - FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); -END; -$$ LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ -BEGIN - IF ( - NEW IS DISTINCT FROM OLD AND - NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at - ) THEN - NEW.updated_at := current_timestamp; - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; diff --git a/watch/migrations/2022-01-01-000000_canonical_slots/down.sql b/watch/migrations/2022-01-01-000000_canonical_slots/down.sql deleted file mode 100644 index 551ed6605c7..00000000000 --- a/watch/migrations/2022-01-01-000000_canonical_slots/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE canonical_slots diff --git a/watch/migrations/2022-01-01-000000_canonical_slots/up.sql b/watch/migrations/2022-01-01-000000_canonical_slots/up.sql deleted file mode 100644 index 2629f11a4c7..00000000000 --- a/watch/migrations/2022-01-01-000000_canonical_slots/up.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE canonical_slots ( - slot integer PRIMARY KEY, - root bytea NOT NULL, - skipped boolean NOT NULL, - beacon_block bytea UNIQUE -) diff --git a/watch/migrations/2022-01-01-000001_beacon_blocks/down.sql b/watch/migrations/2022-01-01-000001_beacon_blocks/down.sql deleted file mode 100644 index 8901956f47c..00000000000 --- a/watch/migrations/2022-01-01-000001_beacon_blocks/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE beacon_blocks diff --git a/watch/migrations/2022-01-01-000001_beacon_blocks/up.sql b/watch/migrations/2022-01-01-000001_beacon_blocks/up.sql deleted file mode 100644 index 250c667b232..00000000000 --- a/watch/migrations/2022-01-01-000001_beacon_blocks/up.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE TABLE beacon_blocks ( - slot integer PRIMARY KEY REFERENCES canonical_slots(slot) ON DELETE CASCADE, - root bytea REFERENCES canonical_slots(beacon_block) NOT NULL, - parent_root bytea NOT NULL, - attestation_count integer NOT NULL, - transaction_count integer -) diff --git a/watch/migrations/2022-01-01-000002_validators/down.sql b/watch/migrations/2022-01-01-000002_validators/down.sql deleted file mode 100644 index 17819fc3491..00000000000 --- a/watch/migrations/2022-01-01-000002_validators/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE validators diff --git a/watch/migrations/2022-01-01-000002_validators/up.sql b/watch/migrations/2022-01-01-000002_validators/up.sql deleted file mode 100644 index 69cfef6772b..00000000000 --- a/watch/migrations/2022-01-01-000002_validators/up.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE TABLE validators ( - index integer PRIMARY KEY, - public_key bytea NOT NULL, - status text NOT NULL, - activation_epoch integer, - exit_epoch integer -) diff --git a/watch/migrations/2022-01-01-000003_proposer_info/down.sql b/watch/migrations/2022-01-01-000003_proposer_info/down.sql deleted file mode 100644 index d61330be5b2..00000000000 --- a/watch/migrations/2022-01-01-000003_proposer_info/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE proposer_info diff --git a/watch/migrations/2022-01-01-000003_proposer_info/up.sql b/watch/migrations/2022-01-01-000003_proposer_info/up.sql deleted file mode 100644 index 488aedb2730..00000000000 --- a/watch/migrations/2022-01-01-000003_proposer_info/up.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE TABLE proposer_info ( - slot integer PRIMARY KEY REFERENCES beacon_blocks(slot) ON DELETE CASCADE, - proposer_index integer REFERENCES validators(index) ON DELETE CASCADE NOT NULL, - graffiti text NOT NULL -) diff --git a/watch/migrations/2022-01-01-000004_active_config/down.sql b/watch/migrations/2022-01-01-000004_active_config/down.sql deleted file mode 100644 index b4304eb7b72..00000000000 --- a/watch/migrations/2022-01-01-000004_active_config/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE active_config diff --git a/watch/migrations/2022-01-01-000004_active_config/up.sql b/watch/migrations/2022-01-01-000004_active_config/up.sql deleted file mode 100644 index 476a0911607..00000000000 --- a/watch/migrations/2022-01-01-000004_active_config/up.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE TABLE active_config ( - id integer PRIMARY KEY CHECK (id=1), - config_name text NOT NULL, - slots_per_epoch integer NOT NULL -) diff --git a/watch/migrations/2022-01-01-000010_blockprint/down.sql b/watch/migrations/2022-01-01-000010_blockprint/down.sql deleted file mode 100644 index fa53325dad1..00000000000 --- a/watch/migrations/2022-01-01-000010_blockprint/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE blockprint diff --git a/watch/migrations/2022-01-01-000010_blockprint/up.sql b/watch/migrations/2022-01-01-000010_blockprint/up.sql deleted file mode 100644 index 2d5741f50b7..00000000000 --- a/watch/migrations/2022-01-01-000010_blockprint/up.sql +++ /dev/null @@ -1,4 +0,0 @@ -CREATE TABLE blockprint ( - slot integer PRIMARY KEY REFERENCES beacon_blocks(slot) ON DELETE CASCADE, - best_guess text NOT NULL -) diff --git a/watch/migrations/2022-01-01-000011_block_rewards/down.sql b/watch/migrations/2022-01-01-000011_block_rewards/down.sql deleted file mode 100644 index 2dc87995c74..00000000000 --- a/watch/migrations/2022-01-01-000011_block_rewards/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE block_rewards diff --git a/watch/migrations/2022-01-01-000011_block_rewards/up.sql b/watch/migrations/2022-01-01-000011_block_rewards/up.sql deleted file mode 100644 index 47cb4304f06..00000000000 --- a/watch/migrations/2022-01-01-000011_block_rewards/up.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE block_rewards ( - slot integer PRIMARY KEY REFERENCES beacon_blocks(slot) ON DELETE CASCADE, - total integer NOT NULL, - attestation_reward integer NOT NULL, - sync_committee_reward integer NOT NULL -) diff --git a/watch/migrations/2022-01-01-000012_block_packing/down.sql b/watch/migrations/2022-01-01-000012_block_packing/down.sql deleted file mode 100644 index e9e7755e3e0..00000000000 --- a/watch/migrations/2022-01-01-000012_block_packing/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE block_packing diff --git a/watch/migrations/2022-01-01-000012_block_packing/up.sql b/watch/migrations/2022-01-01-000012_block_packing/up.sql deleted file mode 100644 index 63a9925f920..00000000000 --- a/watch/migrations/2022-01-01-000012_block_packing/up.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE block_packing ( - slot integer PRIMARY KEY REFERENCES beacon_blocks(slot) ON DELETE CASCADE, - available integer NOT NULL, - included integer NOT NULL, - prior_skip_slots integer NOT NULL -) diff --git a/watch/migrations/2022-01-01-000013_suboptimal_attestations/down.sql b/watch/migrations/2022-01-01-000013_suboptimal_attestations/down.sql deleted file mode 100644 index 0f32b6b4f33..00000000000 --- a/watch/migrations/2022-01-01-000013_suboptimal_attestations/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE suboptimal_attestations diff --git a/watch/migrations/2022-01-01-000013_suboptimal_attestations/up.sql b/watch/migrations/2022-01-01-000013_suboptimal_attestations/up.sql deleted file mode 100644 index 5352afefc8d..00000000000 --- a/watch/migrations/2022-01-01-000013_suboptimal_attestations/up.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE suboptimal_attestations ( - epoch_start_slot integer CHECK (epoch_start_slot % 32 = 0) REFERENCES canonical_slots(slot) ON DELETE CASCADE, - index integer NOT NULL REFERENCES validators(index) ON DELETE CASCADE, - source boolean NOT NULL, - head boolean NOT NULL, - target boolean NOT NULL, - PRIMARY KEY(epoch_start_slot, index) -) diff --git a/watch/migrations/2022-01-01-000020_capella/down.sql b/watch/migrations/2022-01-01-000020_capella/down.sql deleted file mode 100644 index 5903b351db9..00000000000 --- a/watch/migrations/2022-01-01-000020_capella/down.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE beacon_blocks -DROP COLUMN withdrawal_count; diff --git a/watch/migrations/2022-01-01-000020_capella/up.sql b/watch/migrations/2022-01-01-000020_capella/up.sql deleted file mode 100644 index b52b4b00998..00000000000 --- a/watch/migrations/2022-01-01-000020_capella/up.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE beacon_blocks -ADD COLUMN withdrawal_count integer; - diff --git a/watch/postgres_docker_compose/compose.yml b/watch/postgres_docker_compose/compose.yml deleted file mode 100644 index eae4de4a2ba..00000000000 --- a/watch/postgres_docker_compose/compose.yml +++ /dev/null @@ -1,16 +0,0 @@ -version: "3" - -services: - postgres: - image: postgres:12.3-alpine - restart: always - environment: - POSTGRES_PASSWORD: postgres - POSTGRES_USER: postgres - volumes: - - postgres:/var/lib/postgresql/data - ports: - - 127.0.0.1:5432:5432 - -volumes: - postgres: diff --git a/watch/src/block_packing/database.rs b/watch/src/block_packing/database.rs deleted file mode 100644 index f7375431cb3..00000000000 --- a/watch/src/block_packing/database.rs +++ /dev/null @@ -1,140 +0,0 @@ -use crate::database::{ - schema::{beacon_blocks, block_packing}, - watch_types::{WatchHash, WatchSlot}, - Error, PgConn, MAX_SIZE_BATCH_INSERT, -}; - -use diesel::prelude::*; -use diesel::{Insertable, Queryable}; -use log::debug; -use serde::{Deserialize, Serialize}; -use std::time::Instant; - -#[derive(Debug, Queryable, Insertable, Serialize, Deserialize)] -#[diesel(table_name = block_packing)] -pub struct WatchBlockPacking { - pub slot: WatchSlot, - pub available: i32, - pub included: i32, - pub prior_skip_slots: i32, -} - -/// Insert a batch of values into the `block_packing` table. -/// -/// On a conflict, it will do nothing, leaving the old value. -pub fn insert_batch_block_packing( - conn: &mut PgConn, - packing: Vec, -) -> Result<(), Error> { - use self::block_packing::dsl::*; - - let mut count = 0; - let timer = Instant::now(); - - for chunk in packing.chunks(MAX_SIZE_BATCH_INSERT) { - count += diesel::insert_into(block_packing) - .values(chunk) - .on_conflict_do_nothing() - .execute(conn)?; - } - - let time_taken = timer.elapsed(); - debug!("Block packing inserted, count: {count}, time taken: {time_taken:?}"); - Ok(()) -} - -/// Selects the row from the `block_packing` table where `slot` is minimum. -pub fn get_lowest_block_packing(conn: &mut PgConn) -> Result, Error> { - use self::block_packing::dsl::*; - let timer = Instant::now(); - - let result = block_packing - .order_by(slot.asc()) - .limit(1) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Block packing requested: lowest, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects the row from the `block_packing` table where `slot` is maximum. -pub fn get_highest_block_packing(conn: &mut PgConn) -> Result, Error> { - use self::block_packing::dsl::*; - let timer = Instant::now(); - - let result = block_packing - .order_by(slot.desc()) - .limit(1) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Block packing requested: highest, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row of the `block_packing` table corresponding to a given `root_query`. -pub fn get_block_packing_by_root( - conn: &mut PgConn, - root_query: WatchHash, -) -> Result, Error> { - use self::beacon_blocks::dsl::{beacon_blocks, root}; - use self::block_packing::dsl::*; - let timer = Instant::now(); - - let join = beacon_blocks.inner_join(block_packing); - - let result = join - .select((slot, available, included, prior_skip_slots)) - .filter(root.eq(root_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Block packing requested: {root_query}, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row of the `block_packing` table corresponding to a given `slot_query`. -pub fn get_block_packing_by_slot( - conn: &mut PgConn, - slot_query: WatchSlot, -) -> Result, Error> { - use self::block_packing::dsl::*; - let timer = Instant::now(); - - let result = block_packing - .filter(slot.eq(slot_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Block packing requested: {slot_query}, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects `slot` from all rows of the `beacon_blocks` table which do not have a corresponding -/// row in `block_packing`. -#[allow(dead_code)] -pub fn get_unknown_block_packing( - conn: &mut PgConn, - slots_per_epoch: u64, -) -> Result>, Error> { - use self::beacon_blocks::dsl::{beacon_blocks, root, slot}; - use self::block_packing::dsl::block_packing; - - let join = beacon_blocks.left_join(block_packing); - - let result = join - .select(slot) - .filter(root.is_null()) - // Block packing cannot be retrieved for epoch 0 so we need to exclude them. - .filter(slot.ge(slots_per_epoch as i32)) - .order_by(slot.desc()) - .nullable() - .load::>(conn)?; - - Ok(result) -} diff --git a/watch/src/block_packing/mod.rs b/watch/src/block_packing/mod.rs deleted file mode 100644 index 5d74fc59799..00000000000 --- a/watch/src/block_packing/mod.rs +++ /dev/null @@ -1,38 +0,0 @@ -pub mod database; -pub mod server; -pub mod updater; - -use crate::database::watch_types::WatchSlot; -use crate::updater::error::Error; - -pub use database::{ - get_block_packing_by_root, get_block_packing_by_slot, get_highest_block_packing, - get_lowest_block_packing, get_unknown_block_packing, insert_batch_block_packing, - WatchBlockPacking, -}; -pub use server::block_packing_routes; - -use eth2::BeaconNodeHttpClient; -use types::Epoch; - -/// Sends a request to `lighthouse/analysis/block_packing`. -/// Formats the response into a vector of `WatchBlockPacking`. -/// -/// Will fail if `start_epoch == 0`. -pub async fn get_block_packing( - bn: &BeaconNodeHttpClient, - start_epoch: Epoch, - end_epoch: Epoch, -) -> Result, Error> { - Ok(bn - .get_lighthouse_analysis_block_packing(start_epoch, end_epoch) - .await? - .into_iter() - .map(|data| WatchBlockPacking { - slot: WatchSlot::from_slot(data.slot), - available: data.available_attestations as i32, - included: data.included_attestations as i32, - prior_skip_slots: data.prior_skip_slots as i32, - }) - .collect()) -} diff --git a/watch/src/block_packing/server.rs b/watch/src/block_packing/server.rs deleted file mode 100644 index 819144562a5..00000000000 --- a/watch/src/block_packing/server.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::block_packing::database::{ - get_block_packing_by_root, get_block_packing_by_slot, WatchBlockPacking, -}; -use crate::database::{get_connection, PgPool, WatchHash, WatchSlot}; -use crate::server::Error; - -use axum::{extract::Path, routing::get, Extension, Json, Router}; -use eth2::types::BlockId; -use std::str::FromStr; - -pub async fn get_block_packing( - Path(block_query): Path, - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = get_connection(&pool).map_err(Error::Database)?; - match BlockId::from_str(&block_query).map_err(|_| Error::BadRequest)? { - BlockId::Root(root) => Ok(Json(get_block_packing_by_root( - &mut conn, - WatchHash::from_hash(root), - )?)), - BlockId::Slot(slot) => Ok(Json(get_block_packing_by_slot( - &mut conn, - WatchSlot::from_slot(slot), - )?)), - _ => Err(Error::BadRequest), - } -} - -pub fn block_packing_routes() -> Router { - Router::new().route("/v1/blocks/:block/packing", get(get_block_packing)) -} diff --git a/watch/src/block_packing/updater.rs b/watch/src/block_packing/updater.rs deleted file mode 100644 index 34847f6264d..00000000000 --- a/watch/src/block_packing/updater.rs +++ /dev/null @@ -1,211 +0,0 @@ -use crate::database::{self, Error as DbError}; -use crate::updater::{Error, UpdateHandler}; - -use crate::block_packing::get_block_packing; - -use eth2::types::{Epoch, EthSpec}; -use log::{debug, error, warn}; - -const MAX_SIZE_SINGLE_REQUEST_BLOCK_PACKING: u64 = 50; - -impl UpdateHandler { - /// Forward fills the `block_packing` table starting from the entry with the - /// highest slot. - /// - /// It constructs a request to the `get_block_packing` API with: - /// `start_epoch` -> highest completely filled epoch + 1 (or epoch of lowest beacon block) - /// `end_epoch` -> epoch of highest beacon block - /// - /// It will resync the latest epoch if it is not fully filled. - /// That is, `if highest_filled_slot % slots_per_epoch != 31` - /// This means that if the last slot of an epoch is a skip slot, the whole epoch will be - //// resynced during the next head update. - /// - /// Request range will not exceed `MAX_SIZE_SINGLE_REQUEST_BLOCK_PACKING`. - pub async fn fill_block_packing(&mut self) -> Result<(), Error> { - let mut conn = database::get_connection(&self.pool)?; - - // Get the slot of the highest entry in the `block_packing` table. - let highest_filled_slot_opt = if self.config.block_packing { - database::get_highest_block_packing(&mut conn)?.map(|packing| packing.slot) - } else { - return Err(Error::NotEnabled("block_packing".to_string())); - }; - - let mut start_epoch = if let Some(highest_filled_slot) = highest_filled_slot_opt { - if highest_filled_slot.as_slot() % self.slots_per_epoch - == self.slots_per_epoch.saturating_sub(1) - { - // The whole epoch is filled so we can begin syncing the next one. - highest_filled_slot.as_slot().epoch(self.slots_per_epoch) + 1 - } else { - // The epoch is only partially synced. Try to sync it fully. - highest_filled_slot.as_slot().epoch(self.slots_per_epoch) - } - } else { - // No entries in the `block_packing` table. Use `beacon_blocks` instead. - if let Some(lowest_beacon_block) = database::get_lowest_beacon_block(&mut conn)? { - lowest_beacon_block - .slot - .as_slot() - .epoch(self.slots_per_epoch) - } else { - // There are no blocks in the database, do not fill the `block_packing` table. - warn!("Refusing to fill block packing as there are no blocks in the database"); - return Ok(()); - } - }; - - // The `get_block_packing` API endpoint cannot accept `start_epoch == 0`. - if start_epoch == 0 { - start_epoch += 1 - } - - if let Some(highest_block_slot) = - database::get_highest_beacon_block(&mut conn)?.map(|block| block.slot.as_slot()) - { - let mut end_epoch = highest_block_slot.epoch(self.slots_per_epoch); - - if start_epoch > end_epoch { - debug!("Block packing is up to date with the head of the database"); - return Ok(()); - } - - // Ensure the size of the request does not exceed the maximum allowed value. - if start_epoch < end_epoch.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCK_PACKING) { - end_epoch = start_epoch + MAX_SIZE_SINGLE_REQUEST_BLOCK_PACKING - } - - if let Some(lowest_block_slot) = - database::get_lowest_beacon_block(&mut conn)?.map(|block| block.slot.as_slot()) - { - let mut packing = get_block_packing(&self.bn, start_epoch, end_epoch).await?; - - // Since we pull a full epoch of data but are not guaranteed to have all blocks of - // that epoch available, only insert blocks with corresponding `beacon_block`s. - packing.retain(|packing| { - packing.slot.as_slot() >= lowest_block_slot - && packing.slot.as_slot() <= highest_block_slot - }); - database::insert_batch_block_packing(&mut conn, packing)?; - } else { - return Err(Error::Database(DbError::Other( - "Database did not return a lowest block when one exists".to_string(), - ))); - } - } else { - // There are no blocks in the `beacon_blocks` database, but there are entries in the - // `block_packing` table. This is a critical failure. It usually means someone has - // manually tampered with the database tables and should not occur during normal - // operation. - error!("Database is corrupted. Please re-sync the database"); - return Err(Error::Database(DbError::DatabaseCorrupted)); - } - - Ok(()) - } - - /// Backfill the `block_packing` table starting from the entry with the lowest slot. - /// - /// It constructs a request to the `get_block_packing` function with: - /// `start_epoch` -> epoch of lowest_beacon_block - /// `end_epoch` -> epoch of lowest filled `block_packing` - 1 (or epoch of highest beacon block) - /// - /// It will resync the lowest epoch if it is not fully filled. - /// That is, `if lowest_filled_slot % slots_per_epoch != 0` - /// This means that if the last slot of an epoch is a skip slot, the whole epoch will be - //// resynced during the next head update. - /// - /// Request range will not exceed `MAX_SIZE_SINGLE_REQUEST_BLOCK_PACKING`. - pub async fn backfill_block_packing(&mut self) -> Result<(), Error> { - let mut conn = database::get_connection(&self.pool)?; - let max_block_packing_backfill = self.config.max_backfill_size_epochs; - - // Get the slot of the lowest entry in the `block_packing` table. - let lowest_filled_slot_opt = if self.config.block_packing { - database::get_lowest_block_packing(&mut conn)?.map(|packing| packing.slot) - } else { - return Err(Error::NotEnabled("block_packing".to_string())); - }; - - let end_epoch = if let Some(lowest_filled_slot) = lowest_filled_slot_opt { - if lowest_filled_slot.as_slot() % self.slots_per_epoch == 0 { - lowest_filled_slot - .as_slot() - .epoch(self.slots_per_epoch) - .saturating_sub(Epoch::new(1)) - } else { - // The epoch is only partially synced. Try to sync it fully. - lowest_filled_slot.as_slot().epoch(self.slots_per_epoch) - } - } else { - // No entries in the `block_packing` table. Use `beacon_blocks` instead. - if let Some(highest_beacon_block) = - database::get_highest_beacon_block(&mut conn)?.map(|block| block.slot) - { - highest_beacon_block.as_slot().epoch(self.slots_per_epoch) - } else { - // There are no blocks in the database, do not backfill the `block_packing` table. - warn!("Refusing to backfill block packing as there are no blocks in the database"); - return Ok(()); - } - }; - - if end_epoch <= 1 { - debug!("Block packing backfill is complete"); - return Ok(()); - } - - if let Some(lowest_block_slot) = - database::get_lowest_beacon_block(&mut conn)?.map(|block| block.slot.as_slot()) - { - let mut start_epoch = lowest_block_slot.epoch(self.slots_per_epoch); - - if start_epoch >= end_epoch { - debug!("Block packing is up to date with the base of the database"); - return Ok(()); - } - - // Ensure that the request range does not exceed `max_block_packing_backfill` or - // `MAX_SIZE_SINGLE_REQUEST_BLOCK_PACKING`. - if start_epoch < end_epoch.saturating_sub(max_block_packing_backfill) { - start_epoch = end_epoch.saturating_sub(max_block_packing_backfill) - } - if start_epoch < end_epoch.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCK_PACKING) { - start_epoch = end_epoch.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCK_PACKING) - } - - // The `block_packing` API cannot accept `start_epoch == 0`. - if start_epoch == 0 { - start_epoch += 1 - } - - if let Some(highest_block_slot) = - database::get_highest_beacon_block(&mut conn)?.map(|block| block.slot.as_slot()) - { - let mut packing = get_block_packing(&self.bn, start_epoch, end_epoch).await?; - - // Only insert blocks with corresponding `beacon_block`s. - packing.retain(|packing| { - packing.slot.as_slot() >= lowest_block_slot - && packing.slot.as_slot() <= highest_block_slot - }); - - database::insert_batch_block_packing(&mut conn, packing)?; - } else { - return Err(Error::Database(DbError::Other( - "Database did not return a lowest block when one exists".to_string(), - ))); - } - } else { - // There are no blocks in the `beacon_blocks` database, but there are entries in the - // `block_packing` table. This is a critical failure. It usually means someone has - // manually tampered with the database tables and should not occur during normal - // operation. - error!("Database is corrupted. Please re-sync the database"); - return Err(Error::Database(DbError::DatabaseCorrupted)); - } - - Ok(()) - } -} diff --git a/watch/src/block_rewards/database.rs b/watch/src/block_rewards/database.rs deleted file mode 100644 index a2bf49f3e4d..00000000000 --- a/watch/src/block_rewards/database.rs +++ /dev/null @@ -1,137 +0,0 @@ -use crate::database::{ - schema::{beacon_blocks, block_rewards}, - watch_types::{WatchHash, WatchSlot}, - Error, PgConn, MAX_SIZE_BATCH_INSERT, -}; - -use diesel::prelude::*; -use diesel::{Insertable, Queryable}; -use log::debug; -use serde::{Deserialize, Serialize}; -use std::time::Instant; - -#[derive(Debug, Queryable, Insertable, Serialize, Deserialize)] -#[diesel(table_name = block_rewards)] -pub struct WatchBlockRewards { - pub slot: WatchSlot, - pub total: i32, - pub attestation_reward: i32, - pub sync_committee_reward: i32, -} - -/// Insert a batch of values into the `block_rewards` table. -/// -/// On a conflict, it will do nothing, leaving the old value. -pub fn insert_batch_block_rewards( - conn: &mut PgConn, - rewards: Vec, -) -> Result<(), Error> { - use self::block_rewards::dsl::*; - - let mut count = 0; - let timer = Instant::now(); - - for chunk in rewards.chunks(MAX_SIZE_BATCH_INSERT) { - count += diesel::insert_into(block_rewards) - .values(chunk) - .on_conflict_do_nothing() - .execute(conn)?; - } - - let time_taken = timer.elapsed(); - debug!("Block rewards inserted, count: {count}, time_taken: {time_taken:?}"); - Ok(()) -} - -/// Selects the row from the `block_rewards` table where `slot` is minimum. -pub fn get_lowest_block_rewards(conn: &mut PgConn) -> Result, Error> { - use self::block_rewards::dsl::*; - let timer = Instant::now(); - - let result = block_rewards - .order_by(slot.asc()) - .limit(1) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Block rewards requested: lowest, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects the row from the `block_rewards` table where `slot` is maximum. -pub fn get_highest_block_rewards(conn: &mut PgConn) -> Result, Error> { - use self::block_rewards::dsl::*; - let timer = Instant::now(); - - let result = block_rewards - .order_by(slot.desc()) - .limit(1) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Block rewards requested: highest, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row of the `block_rewards` table corresponding to a given `root_query`. -pub fn get_block_rewards_by_root( - conn: &mut PgConn, - root_query: WatchHash, -) -> Result, Error> { - use self::beacon_blocks::dsl::{beacon_blocks, root}; - use self::block_rewards::dsl::*; - let timer = Instant::now(); - - let join = beacon_blocks.inner_join(block_rewards); - - let result = join - .select((slot, total, attestation_reward, sync_committee_reward)) - .filter(root.eq(root_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Block rewards requested: {root_query}, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row of the `block_rewards` table corresponding to a given `slot_query`. -pub fn get_block_rewards_by_slot( - conn: &mut PgConn, - slot_query: WatchSlot, -) -> Result, Error> { - use self::block_rewards::dsl::*; - let timer = Instant::now(); - - let result = block_rewards - .filter(slot.eq(slot_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Block rewards requested: {slot_query}, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects `slot` from all rows of the `beacon_blocks` table which do not have a corresponding -/// row in `block_rewards`. -#[allow(dead_code)] -pub fn get_unknown_block_rewards(conn: &mut PgConn) -> Result>, Error> { - use self::beacon_blocks::dsl::{beacon_blocks, root, slot}; - use self::block_rewards::dsl::block_rewards; - - let join = beacon_blocks.left_join(block_rewards); - - let result = join - .select(slot) - .filter(root.is_null()) - // Block rewards cannot be retrieved for `slot == 0` so we need to exclude it. - .filter(slot.ne(0)) - .order_by(slot.desc()) - .nullable() - .load::>(conn)?; - - Ok(result) -} diff --git a/watch/src/block_rewards/mod.rs b/watch/src/block_rewards/mod.rs deleted file mode 100644 index 0dac88ea58d..00000000000 --- a/watch/src/block_rewards/mod.rs +++ /dev/null @@ -1,38 +0,0 @@ -pub mod database; -mod server; -mod updater; - -use crate::database::watch_types::WatchSlot; -use crate::updater::error::Error; - -pub use database::{ - get_block_rewards_by_root, get_block_rewards_by_slot, get_highest_block_rewards, - get_lowest_block_rewards, get_unknown_block_rewards, insert_batch_block_rewards, - WatchBlockRewards, -}; -pub use server::block_rewards_routes; - -use eth2::BeaconNodeHttpClient; -use types::Slot; - -/// Sends a request to `lighthouse/analysis/block_rewards`. -/// Formats the response into a vector of `WatchBlockRewards`. -/// -/// Will fail if `start_slot == 0`. -pub async fn get_block_rewards( - bn: &BeaconNodeHttpClient, - start_slot: Slot, - end_slot: Slot, -) -> Result, Error> { - Ok(bn - .get_lighthouse_analysis_block_rewards(start_slot, end_slot) - .await? - .into_iter() - .map(|data| WatchBlockRewards { - slot: WatchSlot::from_slot(data.meta.slot), - total: data.total as i32, - attestation_reward: data.attestation_rewards.total as i32, - sync_committee_reward: data.sync_committee_rewards as i32, - }) - .collect()) -} diff --git a/watch/src/block_rewards/server.rs b/watch/src/block_rewards/server.rs deleted file mode 100644 index 480346e25b3..00000000000 --- a/watch/src/block_rewards/server.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::block_rewards::database::{ - get_block_rewards_by_root, get_block_rewards_by_slot, WatchBlockRewards, -}; -use crate::database::{get_connection, PgPool, WatchHash, WatchSlot}; -use crate::server::Error; - -use axum::{extract::Path, routing::get, Extension, Json, Router}; -use eth2::types::BlockId; -use std::str::FromStr; - -pub async fn get_block_rewards( - Path(block_query): Path, - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = get_connection(&pool).map_err(Error::Database)?; - match BlockId::from_str(&block_query).map_err(|_| Error::BadRequest)? { - BlockId::Root(root) => Ok(Json(get_block_rewards_by_root( - &mut conn, - WatchHash::from_hash(root), - )?)), - BlockId::Slot(slot) => Ok(Json(get_block_rewards_by_slot( - &mut conn, - WatchSlot::from_slot(slot), - )?)), - _ => Err(Error::BadRequest), - } -} - -pub fn block_rewards_routes() -> Router { - Router::new().route("/v1/blocks/:block/rewards", get(get_block_rewards)) -} diff --git a/watch/src/block_rewards/updater.rs b/watch/src/block_rewards/updater.rs deleted file mode 100644 index e2893ad0fea..00000000000 --- a/watch/src/block_rewards/updater.rs +++ /dev/null @@ -1,157 +0,0 @@ -use crate::database::{self, Error as DbError}; -use crate::updater::{Error, UpdateHandler}; - -use crate::block_rewards::get_block_rewards; - -use eth2::types::EthSpec; -use log::{debug, error, warn}; - -const MAX_SIZE_SINGLE_REQUEST_BLOCK_REWARDS: u64 = 1600; - -impl UpdateHandler { - /// Forward fills the `block_rewards` table starting from the entry with the - /// highest slot. - /// - /// It constructs a request to the `get_block_rewards` API with: - /// `start_slot` -> highest filled `block_rewards` + 1 (or lowest beacon block) - /// `end_slot` -> highest beacon block - /// - /// Request range will not exceed `MAX_SIZE_SINGLE_REQUEST_BLOCK_REWARDS`. - pub async fn fill_block_rewards(&mut self) -> Result<(), Error> { - let mut conn = database::get_connection(&self.pool)?; - - // Get the slot of the highest entry in the `block_rewards` table. - let highest_filled_slot_opt = if self.config.block_rewards { - database::get_highest_block_rewards(&mut conn)?.map(|reward| reward.slot) - } else { - return Err(Error::NotEnabled("block_rewards".to_string())); - }; - - let mut start_slot = if let Some(highest_filled_slot) = highest_filled_slot_opt { - highest_filled_slot.as_slot() + 1 - } else { - // No entries in the `block_rewards` table. Use `beacon_blocks` instead. - if let Some(lowest_beacon_block) = - database::get_lowest_beacon_block(&mut conn)?.map(|block| block.slot) - { - lowest_beacon_block.as_slot() - } else { - // There are no blocks in the database, do not fill the `block_rewards` table. - warn!("Refusing to fill block rewards as there are no blocks in the database"); - return Ok(()); - } - }; - - // The `block_rewards` API cannot accept `start_slot == 0`. - if start_slot == 0 { - start_slot += 1; - } - - if let Some(highest_beacon_block) = - database::get_highest_beacon_block(&mut conn)?.map(|block| block.slot) - { - let mut end_slot = highest_beacon_block.as_slot(); - - if start_slot > end_slot { - debug!("Block rewards are up to date with the head of the database"); - return Ok(()); - } - - // Ensure the size of the request does not exceed the maximum allowed value. - if start_slot < end_slot.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCK_REWARDS) { - end_slot = start_slot + MAX_SIZE_SINGLE_REQUEST_BLOCK_REWARDS - } - - let rewards = get_block_rewards(&self.bn, start_slot, end_slot).await?; - database::insert_batch_block_rewards(&mut conn, rewards)?; - } else { - // There are no blocks in the `beacon_blocks` database, but there are entries in the - // `block_rewards` table. This is a critical failure. It usually means someone has - // manually tampered with the database tables and should not occur during normal - // operation. - error!("Database is corrupted. Please re-sync the database"); - return Err(Error::Database(DbError::DatabaseCorrupted)); - } - - Ok(()) - } - - /// Backfill the `block_rewards` tables starting from the entry with the - /// lowest slot. - /// - /// It constructs a request to the `get_block_rewards` API with: - /// `start_slot` -> lowest_beacon_block - /// `end_slot` -> lowest filled `block_rewards` - 1 (or highest beacon block) - /// - /// Request range will not exceed `MAX_SIZE_SINGLE_REQUEST_BLOCK_REWARDS`. - pub async fn backfill_block_rewards(&mut self) -> Result<(), Error> { - let mut conn = database::get_connection(&self.pool)?; - let max_block_reward_backfill = self.config.max_backfill_size_epochs * self.slots_per_epoch; - - // Get the slot of the lowest entry in the `block_rewards` table. - let lowest_filled_slot_opt = if self.config.block_rewards { - database::get_lowest_block_rewards(&mut conn)?.map(|reward| reward.slot) - } else { - return Err(Error::NotEnabled("block_rewards".to_string())); - }; - - let end_slot = if let Some(lowest_filled_slot) = lowest_filled_slot_opt { - lowest_filled_slot.as_slot().saturating_sub(1_u64) - } else { - // No entries in the `block_rewards` table. Use `beacon_blocks` instead. - if let Some(highest_beacon_block) = - database::get_highest_beacon_block(&mut conn)?.map(|block| block.slot) - { - highest_beacon_block.as_slot() - } else { - // There are no blocks in the database, do not backfill the `block_rewards` table. - warn!("Refusing to backfill block rewards as there are no blocks in the database"); - return Ok(()); - } - }; - - if end_slot <= 1 { - debug!("Block rewards backfill is complete"); - return Ok(()); - } - - if let Some(lowest_block_slot) = database::get_lowest_beacon_block(&mut conn)? { - let mut start_slot = lowest_block_slot.slot.as_slot(); - - if start_slot >= end_slot { - debug!("Block rewards are up to date with the base of the database"); - return Ok(()); - } - - // Ensure that the request range does not exceed `max_block_reward_backfill` or - // `MAX_SIZE_SINGLE_REQUEST_BLOCK_REWARDS`. - if start_slot < end_slot.saturating_sub(max_block_reward_backfill) { - start_slot = end_slot.saturating_sub(max_block_reward_backfill) - } - - if start_slot < end_slot.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCK_REWARDS) { - start_slot = end_slot.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCK_REWARDS) - } - - // The `block_rewards` API cannot accept `start_slot == 0`. - if start_slot == 0 { - start_slot += 1 - } - - let rewards = get_block_rewards(&self.bn, start_slot, end_slot).await?; - - if self.config.block_rewards { - database::insert_batch_block_rewards(&mut conn, rewards)?; - } - } else { - // There are no blocks in the `beacon_blocks` database, but there are entries in the - // `block_rewards` table. This is a critical failure. It usually means someone has - // manually tampered with the database tables and should not occur during normal - // operation. - error!("Database is corrupted. Please re-sync the database"); - return Err(Error::Database(DbError::DatabaseCorrupted)); - } - - Ok(()) - } -} diff --git a/watch/src/blockprint/config.rs b/watch/src/blockprint/config.rs deleted file mode 100644 index 721fa7cb197..00000000000 --- a/watch/src/blockprint/config.rs +++ /dev/null @@ -1,40 +0,0 @@ -use serde::{Deserialize, Serialize}; - -pub const fn enabled() -> bool { - false -} - -pub const fn url() -> Option { - None -} - -pub const fn username() -> Option { - None -} - -pub const fn password() -> Option { - None -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Config { - #[serde(default = "enabled")] - pub enabled: bool, - #[serde(default = "url")] - pub url: Option, - #[serde(default = "username")] - pub username: Option, - #[serde(default = "password")] - pub password: Option, -} - -impl Default for Config { - fn default() -> Self { - Config { - enabled: enabled(), - url: url(), - username: username(), - password: password(), - } - } -} diff --git a/watch/src/blockprint/database.rs b/watch/src/blockprint/database.rs deleted file mode 100644 index f0bc3f8ac86..00000000000 --- a/watch/src/blockprint/database.rs +++ /dev/null @@ -1,225 +0,0 @@ -use crate::database::{ - self, - schema::{beacon_blocks, blockprint}, - watch_types::{WatchHash, WatchSlot}, - Error, PgConn, MAX_SIZE_BATCH_INSERT, -}; - -use diesel::prelude::*; -use diesel::sql_types::{Integer, Text}; -use diesel::{Insertable, Queryable}; -use log::debug; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::time::Instant; - -type WatchConsensusClient = String; -pub fn list_consensus_clients() -> Vec { - vec![ - "Lighthouse".to_string(), - "Lodestar".to_string(), - "Nimbus".to_string(), - "Prysm".to_string(), - "Teku".to_string(), - "Unknown".to_string(), - ] -} - -#[derive(Debug, Queryable, Insertable, Serialize, Deserialize)] -#[diesel(table_name = blockprint)] -pub struct WatchBlockprint { - pub slot: WatchSlot, - pub best_guess: WatchConsensusClient, -} - -#[derive(Debug, QueryableByName, diesel::FromSqlRow)] -#[allow(dead_code)] -pub struct WatchValidatorBlockprint { - #[diesel(sql_type = Integer)] - pub proposer_index: i32, - #[diesel(sql_type = Text)] - pub best_guess: WatchConsensusClient, - #[diesel(sql_type = Integer)] - pub slot: WatchSlot, -} - -/// Insert a batch of values into the `blockprint` table. -/// -/// On a conflict, it will do nothing, leaving the old value. -pub fn insert_batch_blockprint( - conn: &mut PgConn, - prints: Vec, -) -> Result<(), Error> { - use self::blockprint::dsl::*; - - let mut count = 0; - let timer = Instant::now(); - - for chunk in prints.chunks(MAX_SIZE_BATCH_INSERT) { - count += diesel::insert_into(blockprint) - .values(chunk) - .on_conflict_do_nothing() - .execute(conn)?; - } - - let time_taken = timer.elapsed(); - debug!("Blockprint inserted, count: {count}, time_taken: {time_taken:?}"); - Ok(()) -} - -/// Selects the row from the `blockprint` table where `slot` is minimum. -pub fn get_lowest_blockprint(conn: &mut PgConn) -> Result, Error> { - use self::blockprint::dsl::*; - let timer = Instant::now(); - - let result = blockprint - .order_by(slot.asc()) - .limit(1) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Blockprint requested: lowest, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects the row from the `blockprint` table where `slot` is maximum. -pub fn get_highest_blockprint(conn: &mut PgConn) -> Result, Error> { - use self::blockprint::dsl::*; - let timer = Instant::now(); - - let result = blockprint - .order_by(slot.desc()) - .limit(1) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Blockprint requested: highest, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row of the `blockprint` table corresponding to a given `root_query`. -pub fn get_blockprint_by_root( - conn: &mut PgConn, - root_query: WatchHash, -) -> Result, Error> { - use self::beacon_blocks::dsl::{beacon_blocks, root}; - use self::blockprint::dsl::*; - let timer = Instant::now(); - - let join = beacon_blocks.inner_join(blockprint); - - let result = join - .select((slot, best_guess)) - .filter(root.eq(root_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Blockprint requested: {root_query}, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row of the `blockprint` table corresponding to a given `slot_query`. -pub fn get_blockprint_by_slot( - conn: &mut PgConn, - slot_query: WatchSlot, -) -> Result, Error> { - use self::blockprint::dsl::*; - let timer = Instant::now(); - - let result = blockprint - .filter(slot.eq(slot_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Blockprint requested: {slot_query}, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects `slot` from all rows of the `beacon_blocks` table which do not have a corresponding -/// row in `blockprint`. -#[allow(dead_code)] -pub fn get_unknown_blockprint(conn: &mut PgConn) -> Result>, Error> { - use self::beacon_blocks::dsl::{beacon_blocks, root, slot}; - use self::blockprint::dsl::blockprint; - - let join = beacon_blocks.left_join(blockprint); - - let result = join - .select(slot) - .filter(root.is_null()) - .order_by(slot.desc()) - .nullable() - .load::>(conn)?; - - Ok(result) -} - -/// Constructs a HashMap of `index` -> `best_guess` for each validator's latest proposal at or before -/// `target_slot`. -/// Inserts `"Unknown" if no prior proposals exist. -pub fn construct_validator_blockprints_at_slot( - conn: &mut PgConn, - target_slot: WatchSlot, - slots_per_epoch: u64, -) -> Result, Error> { - use self::blockprint::dsl::{blockprint, slot}; - - let total_validators = - database::count_validators_activated_before_slot(conn, target_slot, slots_per_epoch)? - as usize; - - let mut blockprint_map = HashMap::with_capacity(total_validators); - - let latest_proposals = - database::get_all_validators_latest_proposer_info_at_slot(conn, target_slot)?; - - let latest_proposal_slots: Vec = latest_proposals.clone().into_keys().collect(); - - let result = blockprint - .filter(slot.eq_any(latest_proposal_slots)) - .load::(conn)?; - - // Insert the validators which have available blockprints. - for print in result { - if let Some(proposer) = latest_proposals.get(&print.slot) { - blockprint_map.insert(*proposer, print.best_guess); - } - } - - // Insert the rest of the unknown validators. - for validator_index in 0..total_validators { - blockprint_map - .entry(validator_index as i32) - .or_insert_with(|| "Unknown".to_string()); - } - - Ok(blockprint_map) -} - -/// Counts the number of occurances of each `client` present in the `validators` table at or before some -/// `target_slot`. -pub fn get_validators_clients_at_slot( - conn: &mut PgConn, - target_slot: WatchSlot, - slots_per_epoch: u64, -) -> Result, Error> { - let mut client_map: HashMap = HashMap::new(); - - // This includes all validators which were activated at or before `target_slot`. - let validator_blockprints = - construct_validator_blockprints_at_slot(conn, target_slot, slots_per_epoch)?; - - for client in list_consensus_clients() { - let count = validator_blockprints - .iter() - .filter(|(_, v)| (*v).clone() == client) - .count(); - client_map.insert(client, count); - } - - Ok(client_map) -} diff --git a/watch/src/blockprint/mod.rs b/watch/src/blockprint/mod.rs deleted file mode 100644 index 319090c6565..00000000000 --- a/watch/src/blockprint/mod.rs +++ /dev/null @@ -1,150 +0,0 @@ -pub mod database; -pub mod server; -pub mod updater; - -mod config; - -use crate::database::WatchSlot; - -use eth2::SensitiveUrl; -use reqwest::{Client, Response, Url}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::time::Duration; -use types::Slot; - -pub use config::Config; -pub use database::{ - get_blockprint_by_root, get_blockprint_by_slot, get_highest_blockprint, get_lowest_blockprint, - get_unknown_blockprint, get_validators_clients_at_slot, insert_batch_blockprint, - WatchBlockprint, -}; -pub use server::blockprint_routes; - -const TIMEOUT: Duration = Duration::from_secs(50); - -#[derive(Debug)] -#[allow(dead_code)] -pub enum Error { - Reqwest(reqwest::Error), - Url(url::ParseError), - BlockprintNotSynced, - Other(String), -} - -impl From for Error { - fn from(e: reqwest::Error) -> Self { - Error::Reqwest(e) - } -} - -impl From for Error { - fn from(e: url::ParseError) -> Self { - Error::Url(e) - } -} - -pub struct WatchBlockprintClient { - pub client: Client, - pub server: SensitiveUrl, - pub username: Option, - pub password: Option, -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct BlockprintSyncingResponse { - pub greatest_block_slot: Slot, - pub synced: bool, -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct BlockprintResponse { - pub proposer_index: i32, - pub slot: Slot, - pub best_guess_single: String, -} - -impl WatchBlockprintClient { - async fn get(&self, url: Url) -> Result { - let mut builder = self.client.get(url).timeout(TIMEOUT); - if let Some(username) = &self.username { - builder = builder.basic_auth(username, self.password.as_ref()); - } - let response = builder.send().await.map_err(Error::Reqwest)?; - - if !response.status().is_success() { - return Err(Error::Other(response.text().await?)); - } - - Ok(response) - } - - // Returns the `greatest_block_slot` as reported by the Blockprint server. - // Will error if the Blockprint server is not synced. - #[allow(dead_code)] - pub async fn ensure_synced(&self) -> Result { - let url = self.server.full.join("sync/")?.join("status")?; - - let response = self.get(url).await?; - - let result = response.json::().await?; - if !result.synced { - return Err(Error::BlockprintNotSynced); - } - - Ok(result.greatest_block_slot) - } - - // Pulls the latest blockprint for all validators. - #[allow(dead_code)] - pub async fn blockprint_all_validators( - &self, - highest_validator: i32, - ) -> Result, Error> { - let url = self - .server - .full - .join("validator/")? - .join("blocks/")? - .join("latest")?; - - let response = self.get(url).await?; - - let mut result = response.json::>().await?; - result.retain(|print| print.proposer_index <= highest_validator); - - let mut map: HashMap = HashMap::with_capacity(result.len()); - for print in result { - map.insert(print.proposer_index, print.best_guess_single); - } - - Ok(map) - } - - // Construct a request to the Blockprint server for a range of slots between `start_slot` and - // `end_slot`. - pub async fn get_blockprint( - &self, - start_slot: Slot, - end_slot: Slot, - ) -> Result, Error> { - let url = self - .server - .full - .join("blocks/")? - .join(&format!("{start_slot}/{end_slot}"))?; - - let response = self.get(url).await?; - - let result = response - .json::>() - .await? - .iter() - .map(|response| WatchBlockprint { - slot: WatchSlot::from_slot(response.slot), - best_guess: response.best_guess_single.clone(), - }) - .collect(); - Ok(result) - } -} diff --git a/watch/src/blockprint/server.rs b/watch/src/blockprint/server.rs deleted file mode 100644 index 488af157174..00000000000 --- a/watch/src/blockprint/server.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::blockprint::database::{ - get_blockprint_by_root, get_blockprint_by_slot, WatchBlockprint, -}; -use crate::database::{get_connection, PgPool, WatchHash, WatchSlot}; -use crate::server::Error; - -use axum::{extract::Path, routing::get, Extension, Json, Router}; -use eth2::types::BlockId; -use std::str::FromStr; - -pub async fn get_blockprint( - Path(block_query): Path, - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = get_connection(&pool).map_err(Error::Database)?; - match BlockId::from_str(&block_query).map_err(|_| Error::BadRequest)? { - BlockId::Root(root) => Ok(Json(get_blockprint_by_root( - &mut conn, - WatchHash::from_hash(root), - )?)), - BlockId::Slot(slot) => Ok(Json(get_blockprint_by_slot( - &mut conn, - WatchSlot::from_slot(slot), - )?)), - _ => Err(Error::BadRequest), - } -} - -pub fn blockprint_routes() -> Router { - Router::new().route("/v1/blocks/:block/blockprint", get(get_blockprint)) -} diff --git a/watch/src/blockprint/updater.rs b/watch/src/blockprint/updater.rs deleted file mode 100644 index 7ec56dd9c81..00000000000 --- a/watch/src/blockprint/updater.rs +++ /dev/null @@ -1,172 +0,0 @@ -use crate::database::{self, Error as DbError}; -use crate::updater::{Error, UpdateHandler}; - -use eth2::types::EthSpec; -use log::{debug, error, warn}; - -const MAX_SIZE_SINGLE_REQUEST_BLOCKPRINT: u64 = 1600; - -impl UpdateHandler { - /// Forward fills the `blockprint` table starting from the entry with the - /// highest slot. - /// - /// It constructs a request to the `get_blockprint` API with: - /// `start_slot` -> highest filled `blockprint` + 1 (or lowest beacon block) - /// `end_slot` -> highest beacon block - /// - /// Request range will not exceed `MAX_SIZE_SINGLE_REQUEST_BLOCKPRINT`. - pub async fn fill_blockprint(&mut self) -> Result<(), Error> { - // Ensure blockprint in enabled. - if let Some(blockprint_client) = &self.blockprint { - let mut conn = database::get_connection(&self.pool)?; - - // Get the slot of the highest entry in the `blockprint` table. - let mut start_slot = if let Some(highest_filled_slot) = - database::get_highest_blockprint(&mut conn)?.map(|print| print.slot) - { - highest_filled_slot.as_slot() + 1 - } else { - // No entries in the `blockprint` table. Use `beacon_blocks` instead. - if let Some(lowest_beacon_block) = - database::get_lowest_beacon_block(&mut conn)?.map(|block| block.slot) - { - lowest_beacon_block.as_slot() - } else { - // There are no blocks in the database, do not fill the `blockprint` table. - warn!("Refusing to fill blockprint as there are no blocks in the database"); - return Ok(()); - } - }; - - // The `blockprint` API cannot accept `start_slot == 0`. - if start_slot == 0 { - start_slot += 1; - } - - if let Some(highest_beacon_block) = - database::get_highest_beacon_block(&mut conn)?.map(|block| block.slot) - { - let mut end_slot = highest_beacon_block.as_slot(); - - if start_slot > end_slot { - debug!("Blockprint is up to date with the head of the database"); - return Ok(()); - } - - // Ensure the size of the request does not exceed the maximum allowed value. - if start_slot < end_slot.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCKPRINT) { - end_slot = start_slot + MAX_SIZE_SINGLE_REQUEST_BLOCKPRINT - } - - let mut prints = blockprint_client - .get_blockprint(start_slot, end_slot) - .await?; - - // Ensure the prints returned from blockprint are for slots which exist in the - // `beacon_blocks` table. - prints.retain(|print| { - database::get_beacon_block_by_slot(&mut conn, print.slot) - .ok() - .flatten() - .is_some() - }); - - database::insert_batch_blockprint(&mut conn, prints)?; - } else { - // There are no blocks in the `beacon_blocks` database, but there are entries in either - // `blockprint` table. This is a critical failure. It usually means - // someone has manually tampered with the database tables and should not occur during - // normal operation. - error!("Database is corrupted. Please re-sync the database"); - return Err(Error::Database(DbError::DatabaseCorrupted)); - } - } - - Ok(()) - } - - /// Backfill the `blockprint` table starting from the entry with the lowest slot. - /// - /// It constructs a request to the `get_blockprint` API with: - /// `start_slot` -> lowest_beacon_block - /// `end_slot` -> lowest filled `blockprint` - 1 (or highest beacon block) - /// - /// Request range will not exceed `MAX_SIZE_SINGLE_REQUEST_BLOCKPRINT`. - pub async fn backfill_blockprint(&mut self) -> Result<(), Error> { - // Ensure blockprint in enabled. - if let Some(blockprint_client) = &self.blockprint { - let mut conn = database::get_connection(&self.pool)?; - let max_blockprint_backfill = - self.config.max_backfill_size_epochs * self.slots_per_epoch; - - // Get the slot of the lowest entry in the `blockprint` table. - let end_slot = if let Some(lowest_filled_slot) = - database::get_lowest_blockprint(&mut conn)?.map(|print| print.slot) - { - lowest_filled_slot.as_slot().saturating_sub(1_u64) - } else { - // No entries in the `blockprint` table. Use `beacon_blocks` instead. - if let Some(highest_beacon_block) = - database::get_highest_beacon_block(&mut conn)?.map(|block| block.slot) - { - highest_beacon_block.as_slot() - } else { - // There are no blocks in the database, do not backfill the `blockprint` table. - warn!("Refusing to backfill blockprint as there are no blocks in the database"); - return Ok(()); - } - }; - - if end_slot <= 1 { - debug!("Blockprint backfill is complete"); - return Ok(()); - } - - if let Some(lowest_block_slot) = database::get_lowest_beacon_block(&mut conn)? { - let mut start_slot = lowest_block_slot.slot.as_slot(); - - if start_slot >= end_slot { - debug!("Blockprint are up to date with the base of the database"); - return Ok(()); - } - - // Ensure that the request range does not exceed `max_blockprint_backfill` or - // `MAX_SIZE_SINGLE_REQUEST_BLOCKPRINT`. - if start_slot < end_slot.saturating_sub(max_blockprint_backfill) { - start_slot = end_slot.saturating_sub(max_blockprint_backfill) - } - - if start_slot < end_slot.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCKPRINT) { - start_slot = end_slot.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCKPRINT) - } - - // The `blockprint` API cannot accept `start_slot == 0`. - if start_slot == 0 { - start_slot += 1 - } - - let mut prints = blockprint_client - .get_blockprint(start_slot, end_slot) - .await?; - - // Ensure the prints returned from blockprint are for slots which exist in the - // `beacon_blocks` table. - prints.retain(|print| { - database::get_beacon_block_by_slot(&mut conn, print.slot) - .ok() - .flatten() - .is_some() - }); - - database::insert_batch_blockprint(&mut conn, prints)?; - } else { - // There are no blocks in the `beacon_blocks` database, but there are entries in the `blockprint` - // table. This is a critical failure. It usually means someone has manually tampered with the - // database tables and should not occur during normal operation. - error!("Database is corrupted. Please re-sync the database"); - return Err(Error::Database(DbError::DatabaseCorrupted)); - } - } - Ok(()) - } -} diff --git a/watch/src/cli.rs b/watch/src/cli.rs deleted file mode 100644 index b7179efe5d4..00000000000 --- a/watch/src/cli.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::{config::Config, logger, server, updater}; -use clap::{Arg, ArgAction, Command}; -use clap_utils::get_color_style; - -pub const SERVE: &str = "serve"; -pub const RUN_UPDATER: &str = "run-updater"; -pub const CONFIG: &str = "config"; - -fn run_updater() -> Command { - Command::new(RUN_UPDATER).styles(get_color_style()) -} - -fn serve() -> Command { - Command::new(SERVE).styles(get_color_style()) -} - -pub fn app() -> Command { - Command::new("beacon_watch_daemon") - .author("Sigma Prime ") - .styles(get_color_style()) - .arg( - Arg::new(CONFIG) - .long(CONFIG) - .value_name("PATH_TO_CONFIG") - .help("Path to configuration file") - .action(ArgAction::Set) - .global(true), - ) - .subcommand(run_updater()) - .subcommand(serve()) -} - -pub async fn run() -> Result<(), String> { - let matches = app().get_matches(); - - let config = match matches.get_one::(CONFIG) { - Some(path) => Config::load_from_file(path.to_string())?, - None => Config::default(), - }; - - logger::init_logger(&config.log_level); - - match matches.subcommand() { - Some((RUN_UPDATER, _)) => updater::run_updater(config) - .await - .map_err(|e| format!("Failure: {:?}", e)), - Some((SERVE, _)) => server::serve(config) - .await - .map_err(|e| format!("Failure: {:?}", e)), - _ => Err("Unsupported subcommand. See --help".into()), - } -} diff --git a/watch/src/client.rs b/watch/src/client.rs deleted file mode 100644 index 43aaccde343..00000000000 --- a/watch/src/client.rs +++ /dev/null @@ -1,178 +0,0 @@ -use crate::block_packing::WatchBlockPacking; -use crate::block_rewards::WatchBlockRewards; -use crate::database::models::{ - WatchBeaconBlock, WatchCanonicalSlot, WatchProposerInfo, WatchValidator, -}; -use crate::suboptimal_attestations::WatchAttestation; - -use eth2::types::BlockId; -use reqwest::Client; -use serde::de::DeserializeOwned; -use types::Hash256; -use url::Url; - -#[derive(Debug)] -pub enum Error { - Reqwest(reqwest::Error), - Url(url::ParseError), -} - -impl From for Error { - fn from(e: reqwest::Error) -> Self { - Error::Reqwest(e) - } -} - -impl From for Error { - fn from(e: url::ParseError) -> Self { - Error::Url(e) - } -} - -pub struct WatchHttpClient { - pub client: Client, - pub server: Url, -} - -impl WatchHttpClient { - async fn get_opt(&self, url: Url) -> Result, Error> { - let response = self.client.get(url).send().await?; - - if response.status() == 404 { - Ok(None) - } else { - response - .error_for_status()? - .json() - .await - .map_err(Into::into) - } - } - - pub async fn get_beacon_blocks( - &self, - block_id: BlockId, - ) -> Result, Error> { - let url = self - .server - .join("v1/")? - .join("blocks/")? - .join(&block_id.to_string())?; - - self.get_opt(url).await - } - - pub async fn get_lowest_canonical_slot(&self) -> Result, Error> { - let url = self.server.join("v1/")?.join("slots/")?.join("lowest")?; - - self.get_opt(url).await - } - - pub async fn get_highest_canonical_slot(&self) -> Result, Error> { - let url = self.server.join("v1/")?.join("slots/")?.join("highest")?; - - self.get_opt(url).await - } - - pub async fn get_lowest_beacon_block(&self) -> Result, Error> { - let url = self.server.join("v1/")?.join("blocks/")?.join("lowest")?; - - self.get_opt(url).await - } - - pub async fn get_highest_beacon_block(&self) -> Result, Error> { - let url = self.server.join("v1/")?.join("blocks/")?.join("highest")?; - - self.get_opt(url).await - } - - pub async fn get_next_beacon_block( - &self, - parent: Hash256, - ) -> Result, Error> { - let url = self - .server - .join("v1/")? - .join("blocks/")? - .join(&format!("{parent:?}/"))? - .join("next")?; - - self.get_opt(url).await - } - - pub async fn get_validator_by_index( - &self, - index: i32, - ) -> Result, Error> { - let url = self - .server - .join("v1/")? - .join("validators/")? - .join(&format!("{index}"))?; - - self.get_opt(url).await - } - - pub async fn get_proposer_info( - &self, - block_id: BlockId, - ) -> Result, Error> { - let url = self - .server - .join("v1/")? - .join("blocks/")? - .join(&format!("{block_id}/"))? - .join("proposer")?; - - self.get_opt(url).await - } - - pub async fn get_block_reward( - &self, - block_id: BlockId, - ) -> Result, Error> { - let url = self - .server - .join("v1/")? - .join("blocks/")? - .join(&format!("{block_id}/"))? - .join("rewards")?; - - self.get_opt(url).await - } - - pub async fn get_block_packing( - &self, - block_id: BlockId, - ) -> Result, Error> { - let url = self - .server - .join("v1/")? - .join("blocks/")? - .join(&format!("{block_id}/"))? - .join("packing")?; - - self.get_opt(url).await - } - - pub async fn get_all_validators(&self) -> Result>, Error> { - let url = self.server.join("v1/")?.join("validators/")?.join("all")?; - - self.get_opt(url).await - } - - pub async fn get_attestations( - &self, - epoch: i32, - ) -> Result>, Error> { - let url = self - .server - .join("v1/")? - .join("validators/")? - .join("all/")? - .join("attestation/")? - .join(&format!("{epoch}"))?; - - self.get_opt(url).await - } -} diff --git a/watch/src/config.rs b/watch/src/config.rs deleted file mode 100644 index 4e61f9df9ca..00000000000 --- a/watch/src/config.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::blockprint::Config as BlockprintConfig; -use crate::database::Config as DatabaseConfig; -use crate::server::Config as ServerConfig; -use crate::updater::Config as UpdaterConfig; - -use serde::{Deserialize, Serialize}; -use std::fs::File; - -pub const LOG_LEVEL: &str = "debug"; - -fn log_level() -> String { - LOG_LEVEL.to_string() -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Config { - #[serde(default)] - pub blockprint: BlockprintConfig, - #[serde(default)] - pub database: DatabaseConfig, - #[serde(default)] - pub server: ServerConfig, - #[serde(default)] - pub updater: UpdaterConfig, - /// The minimum severity for log messages. - #[serde(default = "log_level")] - pub log_level: String, -} - -impl Default for Config { - fn default() -> Self { - Self { - blockprint: BlockprintConfig::default(), - database: DatabaseConfig::default(), - server: ServerConfig::default(), - updater: UpdaterConfig::default(), - log_level: log_level(), - } - } -} - -impl Config { - pub fn load_from_file(path_to_file: String) -> Result { - let file = - File::open(path_to_file).map_err(|e| format!("Error reading config file: {:?}", e))?; - let config: Config = serde_yaml::from_reader(file) - .map_err(|e| format!("Error parsing config file: {:?}", e))?; - Ok(config) - } -} diff --git a/watch/src/database/compat.rs b/watch/src/database/compat.rs deleted file mode 100644 index e3e9e0df6fe..00000000000 --- a/watch/src/database/compat.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! Implementations of PostgreSQL compatibility traits. -use crate::database::watch_types::{WatchHash, WatchPK, WatchSlot}; -use diesel::deserialize::{self, FromSql}; -use diesel::pg::{Pg, PgValue}; -use diesel::serialize::{self, Output, ToSql}; -use diesel::sql_types::{Binary, Integer}; - -macro_rules! impl_to_from_sql_int { - ($type:ty) => { - impl ToSql for $type - where - i32: ToSql, - { - fn to_sql<'a>(&'a self, out: &mut Output<'a, '_, Pg>) -> serialize::Result { - let v = i32::try_from(self.as_u64()).map_err(|e| Box::new(e))?; - >::to_sql(&v, &mut out.reborrow()) - } - } - - impl FromSql for $type { - fn from_sql(bytes: PgValue<'_>) -> deserialize::Result { - Ok(Self::new(i32::from_sql(bytes)? as u64)) - } - } - }; -} - -macro_rules! impl_to_from_sql_binary { - ($type:ty) => { - impl ToSql for $type { - fn to_sql<'a>(&'a self, out: &mut Output<'a, '_, Pg>) -> serialize::Result { - let b = self.as_bytes(); - <&[u8] as ToSql>::to_sql(&b, &mut out.reborrow()) - } - } - - impl FromSql for $type { - fn from_sql(bytes: PgValue<'_>) -> deserialize::Result { - Self::from_bytes(bytes.as_bytes()).map_err(|e| e.to_string().into()) - } - } - }; -} - -impl_to_from_sql_int!(WatchSlot); -impl_to_from_sql_binary!(WatchHash); -impl_to_from_sql_binary!(WatchPK); diff --git a/watch/src/database/config.rs b/watch/src/database/config.rs deleted file mode 100644 index dc0c70832f4..00000000000 --- a/watch/src/database/config.rs +++ /dev/null @@ -1,74 +0,0 @@ -use serde::{Deserialize, Serialize}; - -pub const USER: &str = "postgres"; -pub const PASSWORD: &str = "postgres"; -pub const DBNAME: &str = "dev"; -pub const DEFAULT_DBNAME: &str = "postgres"; -pub const HOST: &str = "localhost"; -pub const fn port() -> u16 { - 5432 -} -pub const fn connect_timeout_millis() -> u64 { - 2_000 // 2s -} - -fn user() -> String { - USER.to_string() -} - -fn password() -> String { - PASSWORD.to_string() -} - -fn dbname() -> String { - DBNAME.to_string() -} - -fn default_dbname() -> String { - DEFAULT_DBNAME.to_string() -} - -fn host() -> String { - HOST.to_string() -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Config { - #[serde(default = "user")] - pub user: String, - #[serde(default = "password")] - pub password: String, - #[serde(default = "dbname")] - pub dbname: String, - #[serde(default = "default_dbname")] - pub default_dbname: String, - #[serde(default = "host")] - pub host: String, - #[serde(default = "port")] - pub port: u16, - #[serde(default = "connect_timeout_millis")] - pub connect_timeout_millis: u64, -} - -impl Default for Config { - fn default() -> Self { - Self { - user: user(), - password: password(), - dbname: dbname(), - default_dbname: default_dbname(), - host: host(), - port: port(), - connect_timeout_millis: connect_timeout_millis(), - } - } -} - -impl Config { - pub fn build_database_url(&self) -> String { - format!( - "postgres://{}:{}@{}:{}/{}", - self.user, self.password, self.host, self.port, self.dbname - ) - } -} diff --git a/watch/src/database/error.rs b/watch/src/database/error.rs deleted file mode 100644 index 8c5088fa133..00000000000 --- a/watch/src/database/error.rs +++ /dev/null @@ -1,55 +0,0 @@ -use bls::Error as BlsError; -use diesel::result::{ConnectionError, Error as PgError}; -use eth2::SensitiveError; -use r2d2::Error as PoolError; -use std::fmt; -use types::BeaconStateError; - -#[derive(Debug)] -pub enum Error { - BeaconState(BeaconStateError), - Database(PgError), - DatabaseCorrupted, - InvalidSig(BlsError), - PostgresConnection(ConnectionError), - Pool(PoolError), - SensitiveUrl(SensitiveError), - InvalidRoot, - Other(String), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self) - } -} - -impl From for Error { - fn from(e: BeaconStateError) -> Self { - Error::BeaconState(e) - } -} - -impl From for Error { - fn from(e: ConnectionError) -> Self { - Error::PostgresConnection(e) - } -} - -impl From for Error { - fn from(e: PgError) -> Self { - Error::Database(e) - } -} - -impl From for Error { - fn from(e: PoolError) -> Self { - Error::Pool(e) - } -} - -impl From for Error { - fn from(e: BlsError) -> Self { - Error::InvalidSig(e) - } -} diff --git a/watch/src/database/mod.rs b/watch/src/database/mod.rs deleted file mode 100644 index 7193b0744aa..00000000000 --- a/watch/src/database/mod.rs +++ /dev/null @@ -1,786 +0,0 @@ -mod config; -mod error; - -pub mod compat; -pub mod models; -pub mod schema; -pub mod utils; -pub mod watch_types; - -use self::schema::{ - active_config, beacon_blocks, canonical_slots, proposer_info, suboptimal_attestations, - validators, -}; - -use diesel::dsl::max; -use diesel::prelude::*; -use diesel::r2d2::{Builder, ConnectionManager, Pool, PooledConnection}; -use diesel::upsert::excluded; -use log::{debug, info}; -use std::collections::HashMap; -use std::time::Instant; -use types::{EthSpec, SignedBeaconBlock}; - -pub use self::error::Error; -pub use self::models::{WatchBeaconBlock, WatchCanonicalSlot, WatchProposerInfo, WatchValidator}; -pub use self::watch_types::{WatchHash, WatchPK, WatchSlot}; - -// Clippy has false positives on these re-exports from Rust 1.75.0-beta.1. -#[allow(unused_imports)] -pub use crate::block_rewards::{ - get_block_rewards_by_root, get_block_rewards_by_slot, get_highest_block_rewards, - get_lowest_block_rewards, get_unknown_block_rewards, insert_batch_block_rewards, - WatchBlockRewards, -}; - -#[allow(unused_imports)] -pub use crate::block_packing::{ - get_block_packing_by_root, get_block_packing_by_slot, get_highest_block_packing, - get_lowest_block_packing, get_unknown_block_packing, insert_batch_block_packing, - WatchBlockPacking, -}; - -#[allow(unused_imports)] -pub use crate::suboptimal_attestations::{ - get_all_suboptimal_attestations_for_epoch, get_attestation_by_index, get_attestation_by_pubkey, - get_highest_attestation, get_lowest_attestation, insert_batch_suboptimal_attestations, - WatchAttestation, WatchSuboptimalAttestation, -}; - -#[allow(unused_imports)] -pub use crate::blockprint::{ - get_blockprint_by_root, get_blockprint_by_slot, get_highest_blockprint, get_lowest_blockprint, - get_unknown_blockprint, get_validators_clients_at_slot, insert_batch_blockprint, - WatchBlockprint, -}; - -pub use config::Config; - -/// Batch inserts cannot exceed a certain size. -/// See https://github.com/diesel-rs/diesel/issues/2414. -/// For some reason, this seems to translate to 65535 / 5 (13107) records. -pub const MAX_SIZE_BATCH_INSERT: usize = 13107; - -pub type PgPool = Pool>; -pub type PgConn = PooledConnection>; - -/// Connect to a Postgresql database and build a connection pool. -pub fn build_connection_pool(config: &Config) -> Result { - let database_url = config.clone().build_database_url(); - info!("Building connection pool at: {database_url}"); - let pg = ConnectionManager::::new(&database_url); - Builder::new().build(pg).map_err(Error::Pool) -} - -/// Retrieve an idle connection from the pool. -pub fn get_connection(pool: &PgPool) -> Result { - pool.get().map_err(Error::Pool) -} - -/// Insert the active config into the database. This is used to check if the connected beacon node -/// is compatible with the database. These values will not change (except -/// `current_blockprint_checkpoint`). -pub fn insert_active_config( - conn: &mut PgConn, - new_config_name: String, - new_slots_per_epoch: u64, -) -> Result<(), Error> { - use self::active_config::dsl::*; - - diesel::insert_into(active_config) - .values(&vec![( - id.eq(1), - config_name.eq(new_config_name), - slots_per_epoch.eq(new_slots_per_epoch as i32), - )]) - .on_conflict_do_nothing() - .execute(conn)?; - - Ok(()) -} - -/// Get the active config from the database. -pub fn get_active_config(conn: &mut PgConn) -> Result, Error> { - use self::active_config::dsl::*; - Ok(active_config - .select((config_name, slots_per_epoch)) - .filter(id.eq(1)) - .first::<(String, i32)>(conn) - .optional()?) -} - -/* - * INSERT statements - */ - -/// Inserts a single row into the `canonical_slots` table. -/// If `new_slot.beacon_block` is `None`, the value in the row will be `null`. -/// -/// On a conflict, it will do nothing, leaving the old value. -pub fn insert_canonical_slot(conn: &mut PgConn, new_slot: WatchCanonicalSlot) -> Result<(), Error> { - diesel::insert_into(canonical_slots::table) - .values(&new_slot) - .on_conflict_do_nothing() - .execute(conn)?; - - debug!("Canonical slot inserted: {}", new_slot.slot); - Ok(()) -} - -pub fn insert_beacon_block( - conn: &mut PgConn, - block: SignedBeaconBlock, - root: WatchHash, -) -> Result<(), Error> { - use self::canonical_slots::dsl::{beacon_block, slot as canonical_slot}; - - let block_message = block.message(); - - // Pull out relevant values from the block. - let slot = WatchSlot::from_slot(block.slot()); - let parent_root = WatchHash::from_hash(block.parent_root()); - let proposer_index = block_message.proposer_index() as i32; - let graffiti = block_message.body().graffiti().as_utf8_lossy(); - let attestation_count = block_message.body().attestations_len() as i32; - - let full_payload = block_message.execution_payload().ok(); - - let transaction_count: Option = if let Some(bellatrix_payload) = - full_payload.and_then(|payload| payload.execution_payload_bellatrix().ok()) - { - Some(bellatrix_payload.transactions.len() as i32) - } else { - full_payload - .and_then(|payload| payload.execution_payload_capella().ok()) - .map(|payload| payload.transactions.len() as i32) - }; - - let withdrawal_count: Option = full_payload - .and_then(|payload| payload.execution_payload_capella().ok()) - .map(|payload| payload.withdrawals.len() as i32); - - let block_to_add = WatchBeaconBlock { - slot, - root, - parent_root, - attestation_count, - transaction_count, - withdrawal_count, - }; - - let proposer_info_to_add = WatchProposerInfo { - slot, - proposer_index, - graffiti, - }; - - // Update the canonical slots table. - diesel::update(canonical_slots::table) - .set(beacon_block.eq(root)) - .filter(canonical_slot.eq(slot)) - // Do not overwrite the value if it already exists. - .filter(beacon_block.is_null()) - .execute(conn)?; - - diesel::insert_into(beacon_blocks::table) - .values(block_to_add) - .on_conflict_do_nothing() - .execute(conn)?; - - diesel::insert_into(proposer_info::table) - .values(proposer_info_to_add) - .on_conflict_do_nothing() - .execute(conn)?; - - debug!("Beacon block inserted at slot: {slot}, root: {root}, parent: {parent_root}"); - Ok(()) -} - -/// Insert a validator into the `validators` table -/// -/// On a conflict, it will only overwrite `status`, `activation_epoch` and `exit_epoch`. -pub fn insert_validator(conn: &mut PgConn, validator: WatchValidator) -> Result<(), Error> { - use self::validators::dsl::*; - let new_index = validator.index; - let new_public_key = validator.public_key; - - diesel::insert_into(validators) - .values(validator) - .on_conflict(index) - .do_update() - .set(( - status.eq(excluded(status)), - activation_epoch.eq(excluded(activation_epoch)), - exit_epoch.eq(excluded(exit_epoch)), - )) - .execute(conn)?; - - debug!("Validator inserted, index: {new_index}, public_key: {new_public_key}"); - Ok(()) -} - -/// Insert a batch of values into the `validators` table. -/// -/// On a conflict, it will do nothing. -/// -/// Should not be used when updating validators. -/// Validators should be updated through the `insert_validator` function which contains the correct -/// `on_conflict` clauses. -pub fn insert_batch_validators( - conn: &mut PgConn, - all_validators: Vec, -) -> Result<(), Error> { - use self::validators::dsl::*; - - let mut count = 0; - - for chunk in all_validators.chunks(1000) { - count += diesel::insert_into(validators) - .values(chunk) - .on_conflict_do_nothing() - .execute(conn)?; - } - - debug!("Validators inserted, count: {count}"); - Ok(()) -} - -/* - * SELECT statements - */ - -/// Selects a single row of the `canonical_slots` table corresponding to a given `slot_query`. -pub fn get_canonical_slot( - conn: &mut PgConn, - slot_query: WatchSlot, -) -> Result, Error> { - use self::canonical_slots::dsl::*; - let timer = Instant::now(); - - let result = canonical_slots - .filter(slot.eq(slot_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Canonical slot requested: {slot_query}, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row of the `canonical_slots` table corresponding to a given `root_query`. -/// Only returns the non-skipped slot which matches `root`. -pub fn get_canonical_slot_by_root( - conn: &mut PgConn, - root_query: WatchHash, -) -> Result, Error> { - use self::canonical_slots::dsl::*; - let timer = Instant::now(); - - let result = canonical_slots - .filter(root.eq(root_query)) - .filter(skipped.eq(false)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Canonical root requested: {root_query}, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects `root` from a single row of the `canonical_slots` table corresponding to a given -/// `slot_query`. -#[allow(dead_code)] -pub fn get_root_at_slot( - conn: &mut PgConn, - slot_query: WatchSlot, -) -> Result, Error> { - use self::canonical_slots::dsl::*; - let timer = Instant::now(); - - let result = canonical_slots - .select(root) - .filter(slot.eq(slot_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Canonical slot requested: {slot_query}, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects `slot` from the row of the `canonical_slots` table corresponding to the minimum value -/// of `slot`. -pub fn get_lowest_canonical_slot(conn: &mut PgConn) -> Result, Error> { - use self::canonical_slots::dsl::*; - let timer = Instant::now(); - - let result = canonical_slots - .order_by(slot.asc()) - .limit(1) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Canonical slot requested: lowest, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects `slot` from the row of the `canonical_slots` table corresponding to the minimum value -/// of `slot` and where `skipped == false`. -pub fn get_lowest_non_skipped_canonical_slot( - conn: &mut PgConn, -) -> Result, Error> { - use self::canonical_slots::dsl::*; - let timer = Instant::now(); - - let result = canonical_slots - .filter(skipped.eq(false)) - .order_by(slot.asc()) - .limit(1) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Canonical slot requested: lowest_non_skipped, time taken: {time_taken:?})"); - Ok(result) -} - -/// Select 'slot' from the row of the `canonical_slots` table corresponding to the maximum value -/// of `slot`. -pub fn get_highest_canonical_slot(conn: &mut PgConn) -> Result, Error> { - use self::canonical_slots::dsl::*; - let timer = Instant::now(); - - let result = canonical_slots - .order_by(slot.desc()) - .limit(1) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Canonical slot requested: highest, time taken: {time_taken:?}"); - Ok(result) -} - -/// Select 'slot' from the row of the `canonical_slots` table corresponding to the maximum value -/// of `slot` and where `skipped == false`. -pub fn get_highest_non_skipped_canonical_slot( - conn: &mut PgConn, -) -> Result, Error> { - use self::canonical_slots::dsl::*; - let timer = Instant::now(); - - let result = canonical_slots - .filter(skipped.eq(false)) - .order_by(slot.desc()) - .limit(1) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Canonical slot requested: highest_non_skipped, time taken: {time_taken:?}"); - Ok(result) -} - -/// Select all rows of the `canonical_slots` table where `slot >= `start_slot && slot <= -/// `end_slot`. -pub fn get_canonical_slots_by_range( - conn: &mut PgConn, - start_slot: WatchSlot, - end_slot: WatchSlot, -) -> Result>, Error> { - use self::canonical_slots::dsl::*; - let timer = Instant::now(); - - let result = canonical_slots - .filter(slot.ge(start_slot)) - .filter(slot.le(end_slot)) - .load::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!( - "Canonical slots by range requested, start_slot: {}, end_slot: {}, time_taken: {:?}", - start_slot.as_u64(), - end_slot.as_u64(), - time_taken - ); - Ok(result) -} - -/// Selects `root` from all rows of the `canonical_slots` table which have `beacon_block == null` -/// and `skipped == false` -pub fn get_unknown_canonical_blocks(conn: &mut PgConn) -> Result, Error> { - use self::canonical_slots::dsl::*; - - let result = canonical_slots - .select(root) - .filter(beacon_block.is_null()) - .filter(skipped.eq(false)) - .order_by(slot.desc()) - .load::(conn)?; - - Ok(result) -} - -/// Selects the row from the `beacon_blocks` table where `slot` is minimum. -pub fn get_lowest_beacon_block(conn: &mut PgConn) -> Result, Error> { - use self::beacon_blocks::dsl::*; - let timer = Instant::now(); - - let result = beacon_blocks - .order_by(slot.asc()) - .limit(1) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Beacon block requested: lowest, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects the row from the `beacon_blocks` table where `slot` is maximum. -pub fn get_highest_beacon_block(conn: &mut PgConn) -> Result, Error> { - use self::beacon_blocks::dsl::*; - let timer = Instant::now(); - - let result = beacon_blocks - .order_by(slot.desc()) - .limit(1) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Beacon block requested: highest, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row from the `beacon_blocks` table corresponding to a given `root_query`. -pub fn get_beacon_block_by_root( - conn: &mut PgConn, - root_query: WatchHash, -) -> Result, Error> { - use self::beacon_blocks::dsl::*; - let timer = Instant::now(); - - let result = beacon_blocks - .filter(root.eq(root_query)) - .first::(conn) - .optional()?; - let time_taken = timer.elapsed(); - debug!("Beacon block requested: {root_query}, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row from the `beacon_blocks` table corresponding to a given `slot_query`. -pub fn get_beacon_block_by_slot( - conn: &mut PgConn, - slot_query: WatchSlot, -) -> Result, Error> { - use self::beacon_blocks::dsl::*; - let timer = Instant::now(); - - let result = beacon_blocks - .filter(slot.eq(slot_query)) - .first::(conn) - .optional()?; - let time_taken = timer.elapsed(); - debug!("Beacon block requested: {slot_query}, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects the row from the `beacon_blocks` table where `parent_root` equals the given `parent`. -/// This fetches the next block in the database. -/// -/// Will return `Ok(None)` if there are no matching blocks (e.g. the tip of the chain). -pub fn get_beacon_block_with_parent( - conn: &mut PgConn, - parent: WatchHash, -) -> Result, Error> { - use self::beacon_blocks::dsl::*; - let timer = Instant::now(); - - let result = beacon_blocks - .filter(parent_root.eq(parent)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Next beacon block requested: {parent}, time taken: {time_taken:?}"); - Ok(result) -} - -/// Select all rows of the `beacon_blocks` table where `slot >= `start_slot && slot <= -/// `end_slot`. -pub fn get_beacon_blocks_by_range( - conn: &mut PgConn, - start_slot: WatchSlot, - end_slot: WatchSlot, -) -> Result>, Error> { - use self::beacon_blocks::dsl::*; - let timer = Instant::now(); - - let result = beacon_blocks - .filter(slot.ge(start_slot)) - .filter(slot.le(end_slot)) - .load::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Beacon blocks by range requested, start_slot: {start_slot}, end_slot: {end_slot}, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row of the `proposer_info` table corresponding to a given `root_query`. -pub fn get_proposer_info_by_root( - conn: &mut PgConn, - root_query: WatchHash, -) -> Result, Error> { - use self::beacon_blocks::dsl::{beacon_blocks, root}; - use self::proposer_info::dsl::*; - let timer = Instant::now(); - - let join = beacon_blocks.inner_join(proposer_info); - - let result = join - .select((slot, proposer_index, graffiti)) - .filter(root.eq(root_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Proposer info requested for block: {root_query}, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row of the `proposer_info` table corresponding to a given `slot_query`. -pub fn get_proposer_info_by_slot( - conn: &mut PgConn, - slot_query: WatchSlot, -) -> Result, Error> { - use self::proposer_info::dsl::*; - let timer = Instant::now(); - - let result = proposer_info - .filter(slot.eq(slot_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Proposer info requested for slot: {slot_query}, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects multiple rows of the `proposer_info` table between `start_slot` and `end_slot`. -/// Selects a single row of the `proposer_info` table corresponding to a given `slot_query`. -#[allow(dead_code)] -pub fn get_proposer_info_by_range( - conn: &mut PgConn, - start_slot: WatchSlot, - end_slot: WatchSlot, -) -> Result>, Error> { - use self::proposer_info::dsl::*; - let timer = Instant::now(); - - let result = proposer_info - .filter(slot.ge(start_slot)) - .filter(slot.le(end_slot)) - .load::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!( - "Proposer info requested for range: {start_slot} to {end_slot}, time taken: {time_taken:?}" - ); - Ok(result) -} - -pub fn get_validators_latest_proposer_info( - conn: &mut PgConn, - indices_query: Vec, -) -> Result, Error> { - use self::proposer_info::dsl::*; - - let proposers = proposer_info - .filter(proposer_index.eq_any(indices_query)) - .load::(conn)?; - - let mut result = HashMap::new(); - for proposer in proposers { - result - .entry(proposer.proposer_index) - .or_insert_with(|| proposer.clone()); - let entry = result - .get_mut(&proposer.proposer_index) - .ok_or_else(|| Error::Other("An internal error occured".to_string()))?; - if proposer.slot > entry.slot { - entry.slot = proposer.slot - } - } - - Ok(result) -} - -/// Selects the max(`slot`) and `proposer_index` of each unique index in the -/// `proposer_info` table and returns them formatted as a `HashMap`. -/// Only returns rows which have `slot <= target_slot`. -/// -/// Ideally, this would return the full row, but I have not found a way to do that without using -/// a much more expensive SQL query. -pub fn get_all_validators_latest_proposer_info_at_slot( - conn: &mut PgConn, - target_slot: WatchSlot, -) -> Result, Error> { - use self::proposer_info::dsl::*; - - let latest_proposals: Vec<(i32, Option)> = proposer_info - .group_by(proposer_index) - .select((proposer_index, max(slot))) - .filter(slot.le(target_slot)) - .load::<(i32, Option)>(conn)?; - - let mut result = HashMap::new(); - - for proposal in latest_proposals { - if let Some(latest_slot) = proposal.1 { - result.insert(latest_slot, proposal.0); - } - } - - Ok(result) -} - -/// Selects a single row from the `validators` table corresponding to a given -/// `validator_index_query`. -pub fn get_validator_by_index( - conn: &mut PgConn, - validator_index_query: i32, -) -> Result, Error> { - use self::validators::dsl::*; - let timer = Instant::now(); - - let result = validators - .filter(index.eq(validator_index_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Validator requested: {validator_index_query}, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row from the `validators` table corresponding to a given -/// `public_key_query`. -pub fn get_validator_by_public_key( - conn: &mut PgConn, - public_key_query: WatchPK, -) -> Result, Error> { - use self::validators::dsl::*; - let timer = Instant::now(); - - let result = validators - .filter(public_key.eq(public_key_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Validator requested: {public_key_query}, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects all rows from the `validators` table which have an `index` contained in -/// the `indices_query`. -#[allow(dead_code)] -pub fn get_validators_by_indices( - conn: &mut PgConn, - indices_query: Vec, -) -> Result, Error> { - use self::validators::dsl::*; - let timer = Instant::now(); - - let query_len = indices_query.len(); - let result = validators - .filter(index.eq_any(indices_query)) - .load::(conn)?; - - let time_taken = timer.elapsed(); - debug!("{query_len} validators requested, time taken: {time_taken:?}"); - Ok(result) -} - -// Selects all rows from the `validators` table. -pub fn get_all_validators(conn: &mut PgConn) -> Result, Error> { - use self::validators::dsl::*; - let timer = Instant::now(); - - let result = validators.load::(conn)?; - - let time_taken = timer.elapsed(); - debug!("All validators requested, time taken: {time_taken:?}"); - Ok(result) -} - -/// Counts the number of rows in the `validators` table. -#[allow(dead_code)] -pub fn count_validators(conn: &mut PgConn) -> Result { - use self::validators::dsl::*; - - validators.count().get_result(conn).map_err(Error::Database) -} - -/// Counts the number of rows in the `validators` table where -/// `activation_epoch <= target_slot.epoch()`. -pub fn count_validators_activated_before_slot( - conn: &mut PgConn, - target_slot: WatchSlot, - slots_per_epoch: u64, -) -> Result { - use self::validators::dsl::*; - - let target_epoch = target_slot.epoch(slots_per_epoch); - - validators - .count() - .filter(activation_epoch.le(target_epoch.as_u64() as i32)) - .get_result(conn) - .map_err(Error::Database) -} - -/* - * DELETE statements. - */ - -/// Deletes all rows of the `canonical_slots` table which have `slot` greater than `slot_query`. -/// -/// Due to the ON DELETE CASCADE clause present in the database migration SQL, deleting rows from -/// `canonical_slots` will delete all corresponding rows in `beacon_blocks, `block_rewards`, -/// `block_packing` and `proposer_info`. -pub fn delete_canonical_slots_above( - conn: &mut PgConn, - slot_query: WatchSlot, -) -> Result { - use self::canonical_slots::dsl::*; - - let result = diesel::delete(canonical_slots) - .filter(slot.gt(slot_query)) - .execute(conn)?; - - debug!("Deleted canonical slots above {slot_query}: {result} rows deleted"); - Ok(result) -} - -/// Deletes all rows of the `suboptimal_attestations` table which have `epoch_start_slot` greater -/// than `epoch_start_slot_query`. -pub fn delete_suboptimal_attestations_above( - conn: &mut PgConn, - epoch_start_slot_query: WatchSlot, -) -> Result { - use self::suboptimal_attestations::dsl::*; - - let result = diesel::delete(suboptimal_attestations) - .filter(epoch_start_slot.gt(epoch_start_slot_query)) - .execute(conn)?; - - debug!("Deleted attestations above: {epoch_start_slot_query}, rows deleted: {result}"); - Ok(result) -} diff --git a/watch/src/database/models.rs b/watch/src/database/models.rs deleted file mode 100644 index f42444d6612..00000000000 --- a/watch/src/database/models.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::database::{ - schema::{beacon_blocks, canonical_slots, proposer_info, validators}, - watch_types::{WatchHash, WatchPK, WatchSlot}, -}; -use diesel::{Insertable, Queryable}; -use serde::{Deserialize, Serialize}; -use std::hash::{Hash, Hasher}; - -pub type WatchEpoch = i32; - -#[derive(Debug, Queryable, Insertable, Serialize, Deserialize)] -#[diesel(table_name = canonical_slots)] -pub struct WatchCanonicalSlot { - pub slot: WatchSlot, - pub root: WatchHash, - pub skipped: bool, - pub beacon_block: Option, -} - -#[derive(Debug, Queryable, Insertable, Serialize, Deserialize)] -#[diesel(table_name = beacon_blocks)] -pub struct WatchBeaconBlock { - pub slot: WatchSlot, - pub root: WatchHash, - pub parent_root: WatchHash, - pub attestation_count: i32, - pub transaction_count: Option, - pub withdrawal_count: Option, -} - -#[derive(Clone, Debug, Queryable, Insertable, Serialize, Deserialize)] -#[diesel(table_name = validators)] -pub struct WatchValidator { - pub index: i32, - pub public_key: WatchPK, - pub status: String, - pub activation_epoch: Option, - pub exit_epoch: Option, -} - -// Implement a minimal version of `Hash` and `Eq` so that we know if a validator status has changed. -impl Hash for WatchValidator { - fn hash(&self, state: &mut H) { - self.index.hash(state); - self.status.hash(state); - self.activation_epoch.hash(state); - self.exit_epoch.hash(state); - } -} - -impl PartialEq for WatchValidator { - fn eq(&self, other: &Self) -> bool { - self.index == other.index - && self.status == other.status - && self.activation_epoch == other.activation_epoch - && self.exit_epoch == other.exit_epoch - } -} -impl Eq for WatchValidator {} - -#[derive(Clone, Debug, Queryable, Insertable, Serialize, Deserialize)] -#[diesel(table_name = proposer_info)] -pub struct WatchProposerInfo { - pub slot: WatchSlot, - pub proposer_index: i32, - pub graffiti: String, -} diff --git a/watch/src/database/schema.rs b/watch/src/database/schema.rs deleted file mode 100644 index 32f22d506db..00000000000 --- a/watch/src/database/schema.rs +++ /dev/null @@ -1,102 +0,0 @@ -// @generated automatically by Diesel CLI. - -diesel::table! { - active_config (id) { - id -> Int4, - config_name -> Text, - slots_per_epoch -> Int4, - } -} - -diesel::table! { - beacon_blocks (slot) { - slot -> Int4, - root -> Bytea, - parent_root -> Bytea, - attestation_count -> Int4, - transaction_count -> Nullable, - withdrawal_count -> Nullable, - } -} - -diesel::table! { - block_packing (slot) { - slot -> Int4, - available -> Int4, - included -> Int4, - prior_skip_slots -> Int4, - } -} - -diesel::table! { - block_rewards (slot) { - slot -> Int4, - total -> Int4, - attestation_reward -> Int4, - sync_committee_reward -> Int4, - } -} - -diesel::table! { - blockprint (slot) { - slot -> Int4, - best_guess -> Text, - } -} - -diesel::table! { - canonical_slots (slot) { - slot -> Int4, - root -> Bytea, - skipped -> Bool, - beacon_block -> Nullable, - } -} - -diesel::table! { - proposer_info (slot) { - slot -> Int4, - proposer_index -> Int4, - graffiti -> Text, - } -} - -diesel::table! { - suboptimal_attestations (epoch_start_slot, index) { - epoch_start_slot -> Int4, - index -> Int4, - source -> Bool, - head -> Bool, - target -> Bool, - } -} - -diesel::table! { - validators (index) { - index -> Int4, - public_key -> Bytea, - status -> Text, - activation_epoch -> Nullable, - exit_epoch -> Nullable, - } -} - -diesel::joinable!(block_packing -> beacon_blocks (slot)); -diesel::joinable!(block_rewards -> beacon_blocks (slot)); -diesel::joinable!(blockprint -> beacon_blocks (slot)); -diesel::joinable!(proposer_info -> beacon_blocks (slot)); -diesel::joinable!(proposer_info -> validators (proposer_index)); -diesel::joinable!(suboptimal_attestations -> canonical_slots (epoch_start_slot)); -diesel::joinable!(suboptimal_attestations -> validators (index)); - -diesel::allow_tables_to_appear_in_same_query!( - active_config, - beacon_blocks, - block_packing, - block_rewards, - blockprint, - canonical_slots, - proposer_info, - suboptimal_attestations, - validators, -); diff --git a/watch/src/database/utils.rs b/watch/src/database/utils.rs deleted file mode 100644 index 9134c3698f6..00000000000 --- a/watch/src/database/utils.rs +++ /dev/null @@ -1,28 +0,0 @@ -#![allow(dead_code)] -use crate::database::config::Config; -use diesel::prelude::*; -use diesel_migrations::{FileBasedMigrations, MigrationHarness}; - -/// Sets `config.dbname` to `config.default_dbname` and returns `(new_config, old_dbname)`. -/// -/// This is useful for creating or dropping databases, since these actions must be done by -/// logging into another database. -pub fn get_config_using_default_db(config: &Config) -> (Config, String) { - let mut config = config.clone(); - let new_dbname = std::mem::replace(&mut config.dbname, config.default_dbname.clone()); - (config, new_dbname) -} - -/// Runs the set of migrations as detected in the local directory. -/// Equivalent to `diesel migration run`. -/// -/// Contains `unwrap`s so is only suitable for test code. -/// TODO(mac) refactor to return Result -pub fn run_migrations(config: &Config) -> PgConnection { - let database_url = config.clone().build_database_url(); - let mut conn = PgConnection::establish(&database_url).unwrap(); - let migrations = FileBasedMigrations::find_migrations_directory().unwrap(); - conn.run_pending_migrations(migrations).unwrap(); - conn.begin_test_transaction().unwrap(); - conn -} diff --git a/watch/src/database/watch_types.rs b/watch/src/database/watch_types.rs deleted file mode 100644 index c2b67084c94..00000000000 --- a/watch/src/database/watch_types.rs +++ /dev/null @@ -1,119 +0,0 @@ -use crate::database::error::Error; -use diesel::{ - sql_types::{Binary, Integer}, - AsExpression, FromSqlRow, -}; -use serde::{Deserialize, Serialize}; -use std::fmt; -use std::str::FromStr; -use types::{Epoch, Hash256, PublicKeyBytes, Slot}; -#[derive( - Clone, - Copy, - Debug, - AsExpression, - FromSqlRow, - Deserialize, - Serialize, - Hash, - PartialEq, - Eq, - PartialOrd, - Ord, -)] -#[diesel(sql_type = Integer)] -pub struct WatchSlot(Slot); - -impl fmt::Display for WatchSlot { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl WatchSlot { - pub fn new(slot: u64) -> Self { - Self(Slot::new(slot)) - } - - pub fn from_slot(slot: Slot) -> Self { - Self(slot) - } - - pub fn as_slot(self) -> Slot { - self.0 - } - - pub fn as_u64(self) -> u64 { - self.0.as_u64() - } - - pub fn epoch(self, slots_per_epoch: u64) -> Epoch { - self.as_slot().epoch(slots_per_epoch) - } -} - -#[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, Deserialize, Serialize)] -#[diesel(sql_type = Binary)] -pub struct WatchHash(Hash256); - -impl fmt::Display for WatchHash { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self.0) - } -} - -impl WatchHash { - pub fn as_hash(&self) -> Hash256 { - self.0 - } - - pub fn from_hash(hash: Hash256) -> Self { - WatchHash(hash) - } - - pub fn as_bytes(&self) -> &[u8] { - self.0.as_slice() - } - - pub fn from_bytes(src: &[u8]) -> Result { - if src.len() == 32 { - Ok(WatchHash(Hash256::from_slice(src))) - } else { - Err(Error::InvalidRoot) - } - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq, AsExpression, FromSqlRow, Serialize, Deserialize)] -#[diesel(sql_type = Binary)] -pub struct WatchPK(PublicKeyBytes); - -impl fmt::Display for WatchPK { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self.0) - } -} - -impl WatchPK { - pub fn as_bytes(&self) -> &[u8] { - self.0.as_serialized() - } - - pub fn from_bytes(src: &[u8]) -> Result { - Ok(WatchPK(PublicKeyBytes::deserialize(src)?)) - } - - pub fn from_pubkey(key: PublicKeyBytes) -> Self { - WatchPK(key) - } -} - -impl FromStr for WatchPK { - type Err = String; - - fn from_str(s: &str) -> Result { - Ok(WatchPK( - PublicKeyBytes::from_str(s).map_err(|e| format!("Cannot be parsed: {}", e))?, - )) - } -} diff --git a/watch/src/lib.rs b/watch/src/lib.rs deleted file mode 100644 index 664c9451655..00000000000 --- a/watch/src/lib.rs +++ /dev/null @@ -1,12 +0,0 @@ -#![cfg(unix)] -pub mod block_packing; -pub mod block_rewards; -pub mod blockprint; -pub mod cli; -pub mod client; -pub mod config; -pub mod database; -pub mod logger; -pub mod server; -pub mod suboptimal_attestations; -pub mod updater; diff --git a/watch/src/logger.rs b/watch/src/logger.rs deleted file mode 100644 index 49310b42aae..00000000000 --- a/watch/src/logger.rs +++ /dev/null @@ -1,24 +0,0 @@ -use env_logger::Builder; -use log::{info, LevelFilter}; -use std::process; - -pub fn init_logger(log_level: &str) { - let log_level = match log_level.to_lowercase().as_str() { - "trace" => LevelFilter::Trace, - "debug" => LevelFilter::Debug, - "info" => LevelFilter::Info, - "warn" => LevelFilter::Warn, - "error" => LevelFilter::Error, - _ => { - eprintln!("Unsupported log level"); - process::exit(1) - } - }; - - let mut builder = Builder::new(); - builder.filter(Some("watch"), log_level); - - builder.init(); - - info!("Logger initialized with log-level: {log_level}"); -} diff --git a/watch/src/main.rs b/watch/src/main.rs deleted file mode 100644 index f971747da42..00000000000 --- a/watch/src/main.rs +++ /dev/null @@ -1,41 +0,0 @@ -#[cfg(unix)] -use std::process; - -#[cfg(unix)] -mod block_packing; -#[cfg(unix)] -mod block_rewards; -#[cfg(unix)] -mod blockprint; -#[cfg(unix)] -mod cli; -#[cfg(unix)] -mod config; -#[cfg(unix)] -mod database; -#[cfg(unix)] -mod logger; -#[cfg(unix)] -mod server; -#[cfg(unix)] -mod suboptimal_attestations; -#[cfg(unix)] -mod updater; - -#[cfg(unix)] -#[tokio::main] -async fn main() { - match cli::run().await { - Ok(()) => process::exit(0), - Err(e) => { - eprintln!("Command failed with: {}", e); - drop(e); - process::exit(1) - } - } -} - -#[cfg(windows)] -fn main() { - eprintln!("Windows is not supported. Exiting."); -} diff --git a/watch/src/server/config.rs b/watch/src/server/config.rs deleted file mode 100644 index a7d38e706f8..00000000000 --- a/watch/src/server/config.rs +++ /dev/null @@ -1,28 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::net::IpAddr; - -pub const LISTEN_ADDR: &str = "127.0.0.1"; - -pub const fn listen_port() -> u16 { - 5059 -} -fn listen_addr() -> IpAddr { - LISTEN_ADDR.parse().expect("Server address is not valid") -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Config { - #[serde(default = "listen_addr")] - pub listen_addr: IpAddr, - #[serde(default = "listen_port")] - pub listen_port: u16, -} - -impl Default for Config { - fn default() -> Self { - Self { - listen_addr: listen_addr(), - listen_port: listen_port(), - } - } -} diff --git a/watch/src/server/error.rs b/watch/src/server/error.rs deleted file mode 100644 index e2c8f0f42ac..00000000000 --- a/watch/src/server/error.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::database::Error as DbError; -use axum::Error as AxumError; -use axum::{http::StatusCode, response::IntoResponse, Json}; -use hyper::Error as HyperError; -use serde_json::json; -use std::io::Error as IoError; - -#[derive(Debug)] -#[allow(dead_code)] -pub enum Error { - Axum(AxumError), - Hyper(HyperError), - Database(DbError), - IoError(IoError), - BadRequest, - NotFound, - Other(String), -} - -impl IntoResponse for Error { - fn into_response(self) -> axum::response::Response { - let (status, error_message) = match self { - Self::BadRequest => (StatusCode::BAD_REQUEST, "Bad Request"), - Self::NotFound => (StatusCode::NOT_FOUND, "Not Found"), - _ => (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error"), - }; - (status, Json(json!({ "error": error_message }))).into_response() - } -} - -impl From for Error { - fn from(e: HyperError) -> Self { - Error::Hyper(e) - } -} - -impl From for Error { - fn from(e: AxumError) -> Self { - Error::Axum(e) - } -} - -impl From for Error { - fn from(e: DbError) -> Self { - Error::Database(e) - } -} - -impl From for Error { - fn from(e: IoError) -> Self { - Error::IoError(e) - } -} - -impl From for Error { - fn from(e: String) -> Self { - Error::Other(e) - } -} diff --git a/watch/src/server/handler.rs b/watch/src/server/handler.rs deleted file mode 100644 index 6777026867e..00000000000 --- a/watch/src/server/handler.rs +++ /dev/null @@ -1,266 +0,0 @@ -use crate::database::{ - self, Error as DbError, PgPool, WatchBeaconBlock, WatchCanonicalSlot, WatchHash, WatchPK, - WatchProposerInfo, WatchSlot, WatchValidator, -}; -use crate::server::Error; -use axum::{ - extract::{Path, Query}, - Extension, Json, -}; -use eth2::types::BlockId; -use std::collections::HashMap; -use std::str::FromStr; - -pub async fn get_slot( - Path(slot): Path, - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - Ok(Json(database::get_canonical_slot( - &mut conn, - WatchSlot::new(slot), - )?)) -} - -pub async fn get_slot_lowest( - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - Ok(Json(database::get_lowest_canonical_slot(&mut conn)?)) -} - -pub async fn get_slot_highest( - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - Ok(Json(database::get_highest_canonical_slot(&mut conn)?)) -} - -pub async fn get_slots_by_range( - Query(query): Query>, - Extension(pool): Extension, -) -> Result>>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - if let Some(start_slot) = query.get("start_slot") { - if let Some(end_slot) = query.get("end_slot") { - if start_slot > end_slot { - Err(Error::BadRequest) - } else { - Ok(Json(database::get_canonical_slots_by_range( - &mut conn, - WatchSlot::new(*start_slot), - WatchSlot::new(*end_slot), - )?)) - } - } else { - Err(Error::BadRequest) - } - } else { - Err(Error::BadRequest) - } -} - -pub async fn get_block( - Path(block_query): Path, - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - let block_id: BlockId = BlockId::from_str(&block_query).map_err(|_| Error::BadRequest)?; - match block_id { - BlockId::Slot(slot) => Ok(Json(database::get_beacon_block_by_slot( - &mut conn, - WatchSlot::from_slot(slot), - )?)), - BlockId::Root(root) => Ok(Json(database::get_beacon_block_by_root( - &mut conn, - WatchHash::from_hash(root), - )?)), - _ => Err(Error::BadRequest), - } -} - -pub async fn get_block_lowest( - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - Ok(Json(database::get_lowest_beacon_block(&mut conn)?)) -} - -pub async fn get_block_highest( - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - Ok(Json(database::get_highest_beacon_block(&mut conn)?)) -} - -pub async fn get_block_previous( - Path(block_query): Path, - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - match BlockId::from_str(&block_query).map_err(|_| Error::BadRequest)? { - BlockId::Root(root) => { - if let Some(block) = - database::get_beacon_block_by_root(&mut conn, WatchHash::from_hash(root))? - .map(|block| block.parent_root) - { - Ok(Json(database::get_beacon_block_by_root(&mut conn, block)?)) - } else { - Err(Error::NotFound) - } - } - BlockId::Slot(slot) => Ok(Json(database::get_beacon_block_by_slot( - &mut conn, - WatchSlot::new(slot.as_u64().checked_sub(1_u64).ok_or(Error::NotFound)?), - )?)), - _ => Err(Error::BadRequest), - } -} - -pub async fn get_block_next( - Path(block_query): Path, - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - match BlockId::from_str(&block_query).map_err(|_| Error::BadRequest)? { - BlockId::Root(root) => Ok(Json(database::get_beacon_block_with_parent( - &mut conn, - WatchHash::from_hash(root), - )?)), - BlockId::Slot(slot) => Ok(Json(database::get_beacon_block_by_slot( - &mut conn, - WatchSlot::from_slot(slot + 1_u64), - )?)), - _ => Err(Error::BadRequest), - } -} - -pub async fn get_blocks_by_range( - Query(query): Query>, - Extension(pool): Extension, -) -> Result>>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - if let Some(start_slot) = query.get("start_slot") { - if let Some(end_slot) = query.get("end_slot") { - if start_slot > end_slot { - Err(Error::BadRequest) - } else { - Ok(Json(database::get_beacon_blocks_by_range( - &mut conn, - WatchSlot::new(*start_slot), - WatchSlot::new(*end_slot), - )?)) - } - } else { - Err(Error::BadRequest) - } - } else { - Err(Error::BadRequest) - } -} - -pub async fn get_block_proposer( - Path(block_query): Path, - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - match BlockId::from_str(&block_query).map_err(|_| Error::BadRequest)? { - BlockId::Root(root) => Ok(Json(database::get_proposer_info_by_root( - &mut conn, - WatchHash::from_hash(root), - )?)), - BlockId::Slot(slot) => Ok(Json(database::get_proposer_info_by_slot( - &mut conn, - WatchSlot::from_slot(slot), - )?)), - _ => Err(Error::BadRequest), - } -} - -pub async fn get_validator( - Path(validator_query): Path, - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - if validator_query.starts_with("0x") { - let pubkey = WatchPK::from_str(&validator_query).map_err(|_| Error::BadRequest)?; - Ok(Json(database::get_validator_by_public_key( - &mut conn, pubkey, - )?)) - } else { - let index = i32::from_str(&validator_query).map_err(|_| Error::BadRequest)?; - Ok(Json(database::get_validator_by_index(&mut conn, index)?)) - } -} - -pub async fn get_all_validators( - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - Ok(Json(database::get_all_validators(&mut conn)?)) -} - -pub async fn get_validator_latest_proposal( - Path(validator_query): Path, - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - if validator_query.starts_with("0x") { - let pubkey = WatchPK::from_str(&validator_query).map_err(|_| Error::BadRequest)?; - let validator = - database::get_validator_by_public_key(&mut conn, pubkey)?.ok_or(Error::NotFound)?; - Ok(Json(database::get_validators_latest_proposer_info( - &mut conn, - vec![validator.index], - )?)) - } else { - let index = i32::from_str(&validator_query).map_err(|_| Error::BadRequest)?; - Ok(Json(database::get_validators_latest_proposer_info( - &mut conn, - vec![index], - )?)) - } -} - -pub async fn get_client_breakdown( - Extension(pool): Extension, - Extension(slots_per_epoch): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - - if let Some(target_slot) = database::get_highest_canonical_slot(&mut conn)? { - Ok(Json(database::get_validators_clients_at_slot( - &mut conn, - target_slot.slot, - slots_per_epoch, - )?)) - } else { - Err(Error::Database(DbError::Other( - "No slots found in database.".to_string(), - ))) - } -} - -pub async fn get_client_breakdown_percentages( - Extension(pool): Extension, - Extension(slots_per_epoch): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - - let mut result = HashMap::new(); - if let Some(target_slot) = database::get_highest_canonical_slot(&mut conn)? { - let total = database::count_validators_activated_before_slot( - &mut conn, - target_slot.slot, - slots_per_epoch, - )?; - let clients = - database::get_validators_clients_at_slot(&mut conn, target_slot.slot, slots_per_epoch)?; - for (client, number) in clients.iter() { - let percentage: f64 = *number as f64 / total as f64 * 100.0; - result.insert(client.to_string(), percentage); - } - } - - Ok(Json(result)) -} diff --git a/watch/src/server/mod.rs b/watch/src/server/mod.rs deleted file mode 100644 index 08036db9510..00000000000 --- a/watch/src/server/mod.rs +++ /dev/null @@ -1,136 +0,0 @@ -use crate::block_packing::block_packing_routes; -use crate::block_rewards::block_rewards_routes; -use crate::blockprint::blockprint_routes; -use crate::config::Config as FullConfig; -use crate::database::{self, PgPool}; -use crate::suboptimal_attestations::{attestation_routes, blockprint_attestation_routes}; -use axum::{ - http::{StatusCode, Uri}, - routing::get, - Extension, Json, Router, -}; -use eth2::types::ErrorMessage; -use log::info; -use std::future::{Future, IntoFuture}; -use std::net::{SocketAddr, TcpListener}; - -pub use config::Config; -pub use error::Error; - -mod config; -mod error; -mod handler; - -pub async fn serve(config: FullConfig) -> Result<(), Error> { - let db = database::build_connection_pool(&config.database)?; - let (_, slots_per_epoch) = database::get_active_config(&mut database::get_connection(&db)?)? - .ok_or_else(|| { - Error::Other( - "Database not found. Please run the updater prior to starting the server" - .to_string(), - ) - })?; - - let (_addr, server) = start_server(&config, slots_per_epoch as u64, db)?; - - server.await?; - - Ok(()) -} - -/// Creates a server that will serve requests using information from `config`. -/// -/// The server will create its own connection pool to serve connections to the database. -/// This is separate to the connection pool that is used for the `updater`. -/// -/// The server will shut down gracefully when the `shutdown` future resolves. -/// -/// ## Returns -/// -/// This function will bind the server to the address specified in the config and then return a -/// Future representing the actual server that will need to be awaited. -/// -/// ## Errors -/// -/// Returns an error if the server is unable to bind or there is another error during -/// configuration. -pub fn start_server( - config: &FullConfig, - slots_per_epoch: u64, - pool: PgPool, -) -> Result< - ( - SocketAddr, - impl Future> + 'static, - ), - Error, -> { - let mut routes = Router::new() - .route("/v1/slots", get(handler::get_slots_by_range)) - .route("/v1/slots/:slot", get(handler::get_slot)) - .route("/v1/slots/lowest", get(handler::get_slot_lowest)) - .route("/v1/slots/highest", get(handler::get_slot_highest)) - .route("/v1/slots/:slot/block", get(handler::get_block)) - .route("/v1/blocks", get(handler::get_blocks_by_range)) - .route("/v1/blocks/:block", get(handler::get_block)) - .route("/v1/blocks/lowest", get(handler::get_block_lowest)) - .route("/v1/blocks/highest", get(handler::get_block_highest)) - .route( - "/v1/blocks/:block/previous", - get(handler::get_block_previous), - ) - .route("/v1/blocks/:block/next", get(handler::get_block_next)) - .route( - "/v1/blocks/:block/proposer", - get(handler::get_block_proposer), - ) - .route("/v1/validators/:validator", get(handler::get_validator)) - .route("/v1/validators/all", get(handler::get_all_validators)) - .route( - "/v1/validators/:validator/latest_proposal", - get(handler::get_validator_latest_proposal), - ) - .route("/v1/clients", get(handler::get_client_breakdown)) - .route( - "/v1/clients/percentages", - get(handler::get_client_breakdown_percentages), - ) - .merge(attestation_routes()) - .merge(blockprint_routes()) - .merge(block_packing_routes()) - .merge(block_rewards_routes()); - - if config.blockprint.enabled && config.updater.attestations { - routes = routes.merge(blockprint_attestation_routes()) - } - - let app = routes - .fallback(route_not_found) - .layer(Extension(pool)) - .layer(Extension(slots_per_epoch)); - - let addr = SocketAddr::new(config.server.listen_addr, config.server.listen_port); - let listener = TcpListener::bind(addr)?; - listener.set_nonblocking(true)?; - - // Read the socket address (it may be different from `addr` if listening on port 0). - let socket_addr = listener.local_addr()?; - - let serve = axum::serve(tokio::net::TcpListener::from_std(listener)?, app); - - info!("HTTP server listening on {}", addr); - - Ok((socket_addr, serve.into_future())) -} - -// The default route indicating that no available routes matched the request. -async fn route_not_found(uri: Uri) -> (StatusCode, Json) { - ( - StatusCode::METHOD_NOT_ALLOWED, - Json(ErrorMessage { - code: StatusCode::METHOD_NOT_ALLOWED.as_u16(), - message: format!("No route for {uri}"), - stacktraces: vec![], - }), - ) -} diff --git a/watch/src/suboptimal_attestations/database.rs b/watch/src/suboptimal_attestations/database.rs deleted file mode 100644 index cb947d250a2..00000000000 --- a/watch/src/suboptimal_attestations/database.rs +++ /dev/null @@ -1,224 +0,0 @@ -use crate::database::{ - schema::{suboptimal_attestations, validators}, - watch_types::{WatchPK, WatchSlot}, - Error, PgConn, MAX_SIZE_BATCH_INSERT, -}; - -use diesel::prelude::*; -use diesel::{Insertable, Queryable}; -use log::debug; -use serde::{Deserialize, Serialize}; -use std::time::Instant; - -use types::Epoch; - -#[derive(Clone, Copy, Debug, Serialize, Deserialize)] -pub struct WatchAttestation { - pub index: i32, - pub epoch: Epoch, - pub source: bool, - pub head: bool, - pub target: bool, -} - -impl WatchAttestation { - pub fn optimal(index: i32, epoch: Epoch) -> WatchAttestation { - WatchAttestation { - index, - epoch, - source: true, - head: true, - target: true, - } - } -} - -#[derive(Debug, Queryable, Insertable, Serialize, Deserialize)] -#[diesel(table_name = suboptimal_attestations)] -pub struct WatchSuboptimalAttestation { - pub epoch_start_slot: WatchSlot, - pub index: i32, - pub source: bool, - pub head: bool, - pub target: bool, -} - -impl WatchSuboptimalAttestation { - pub fn to_attestation(&self, slots_per_epoch: u64) -> WatchAttestation { - WatchAttestation { - index: self.index, - epoch: self.epoch_start_slot.epoch(slots_per_epoch), - source: self.source, - head: self.head, - target: self.target, - } - } -} - -/// Insert a batch of values into the `suboptimal_attestations` table -/// -/// Since attestations technically occur per-slot but we only store them per-epoch (via its -/// `start_slot`) so if any slot in the epoch changes, we need to resync the whole epoch as a -/// 'suboptimal' attestation could now be 'optimal'. -/// -/// This is handled in the update code, where in the case of a re-org, the affected epoch is -/// deleted completely. -/// -/// On a conflict, it will do nothing. -pub fn insert_batch_suboptimal_attestations( - conn: &mut PgConn, - attestations: Vec, -) -> Result<(), Error> { - use self::suboptimal_attestations::dsl::*; - - let mut count = 0; - let timer = Instant::now(); - - for chunk in attestations.chunks(MAX_SIZE_BATCH_INSERT) { - count += diesel::insert_into(suboptimal_attestations) - .values(chunk) - .on_conflict_do_nothing() - .execute(conn)?; - } - - let time_taken = timer.elapsed(); - debug!("Attestations inserted, count: {count}, time taken: {time_taken:?}"); - Ok(()) -} - -/// Selects the row from the `suboptimal_attestations` table where `epoch_start_slot` is minimum. -pub fn get_lowest_attestation( - conn: &mut PgConn, -) -> Result, Error> { - use self::suboptimal_attestations::dsl::*; - - Ok(suboptimal_attestations - .order_by(epoch_start_slot.asc()) - .limit(1) - .first::(conn) - .optional()?) -} - -/// Selects the row from the `suboptimal_attestations` table where `epoch_start_slot` is maximum. -pub fn get_highest_attestation( - conn: &mut PgConn, -) -> Result, Error> { - use self::suboptimal_attestations::dsl::*; - - Ok(suboptimal_attestations - .order_by(epoch_start_slot.desc()) - .limit(1) - .first::(conn) - .optional()?) -} - -/// Selects a single row from the `suboptimal_attestations` table corresponding to a given -/// `index_query` and `epoch_query`. -pub fn get_attestation_by_index( - conn: &mut PgConn, - index_query: i32, - epoch_query: Epoch, - slots_per_epoch: u64, -) -> Result, Error> { - use self::suboptimal_attestations::dsl::*; - let timer = Instant::now(); - - let result = suboptimal_attestations - .filter(epoch_start_slot.eq(WatchSlot::from_slot( - epoch_query.start_slot(slots_per_epoch), - ))) - .filter(index.eq(index_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Attestation requested for validator: {index_query}, epoch: {epoch_query}, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row from the `suboptimal_attestations` table corresponding -/// to a given `pubkey_query` and `epoch_query`. -#[allow(dead_code)] -pub fn get_attestation_by_pubkey( - conn: &mut PgConn, - pubkey_query: WatchPK, - epoch_query: Epoch, - slots_per_epoch: u64, -) -> Result, Error> { - use self::suboptimal_attestations::dsl::*; - use self::validators::dsl::{public_key, validators}; - let timer = Instant::now(); - - let join = validators.inner_join(suboptimal_attestations); - - let result = join - .select((epoch_start_slot, index, source, head, target)) - .filter(epoch_start_slot.eq(WatchSlot::from_slot( - epoch_query.start_slot(slots_per_epoch), - ))) - .filter(public_key.eq(pubkey_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Attestation requested for validator: {pubkey_query}, epoch: {epoch_query}, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects `index` for all validators in the suboptimal_attestations table -/// that have `source == false` for the corresponding `epoch_start_slot_query`. -pub fn get_validators_missed_source( - conn: &mut PgConn, - epoch_start_slot_query: WatchSlot, -) -> Result, Error> { - use self::suboptimal_attestations::dsl::*; - - Ok(suboptimal_attestations - .select(index) - .filter(epoch_start_slot.eq(epoch_start_slot_query)) - .filter(source.eq(false)) - .load::(conn)?) -} - -/// Selects `index` for all validators in the suboptimal_attestations table -/// that have `head == false` for the corresponding `epoch_start_slot_query`. -pub fn get_validators_missed_head( - conn: &mut PgConn, - epoch_start_slot_query: WatchSlot, -) -> Result, Error> { - use self::suboptimal_attestations::dsl::*; - - Ok(suboptimal_attestations - .select(index) - .filter(epoch_start_slot.eq(epoch_start_slot_query)) - .filter(head.eq(false)) - .load::(conn)?) -} - -/// Selects `index` for all validators in the suboptimal_attestations table -/// that have `target == false` for the corresponding `epoch_start_slot_query`. -pub fn get_validators_missed_target( - conn: &mut PgConn, - epoch_start_slot_query: WatchSlot, -) -> Result, Error> { - use self::suboptimal_attestations::dsl::*; - - Ok(suboptimal_attestations - .select(index) - .filter(epoch_start_slot.eq(epoch_start_slot_query)) - .filter(target.eq(false)) - .load::(conn)?) -} - -/// Selects all rows from the `suboptimal_attestations` table for the given -/// `epoch_start_slot_query`. -pub fn get_all_suboptimal_attestations_for_epoch( - conn: &mut PgConn, - epoch_start_slot_query: WatchSlot, -) -> Result, Error> { - use self::suboptimal_attestations::dsl::*; - - Ok(suboptimal_attestations - .filter(epoch_start_slot.eq(epoch_start_slot_query)) - .load::(conn)?) -} diff --git a/watch/src/suboptimal_attestations/mod.rs b/watch/src/suboptimal_attestations/mod.rs deleted file mode 100644 index a94532e8ab2..00000000000 --- a/watch/src/suboptimal_attestations/mod.rs +++ /dev/null @@ -1,56 +0,0 @@ -pub mod database; -pub mod server; -pub mod updater; - -use crate::database::watch_types::WatchSlot; -use crate::updater::error::Error; - -pub use database::{ - get_all_suboptimal_attestations_for_epoch, get_attestation_by_index, get_attestation_by_pubkey, - get_highest_attestation, get_lowest_attestation, insert_batch_suboptimal_attestations, - WatchAttestation, WatchSuboptimalAttestation, -}; - -pub use server::{attestation_routes, blockprint_attestation_routes}; - -use eth2::BeaconNodeHttpClient; -use types::Epoch; - -/// Sends a request to `lighthouse/analysis/attestation_performance`. -/// Formats the response into a vector of `WatchSuboptimalAttestation`. -/// -/// Any attestations with `source == true && head == true && target == true` are ignored. -pub async fn get_attestation_performances( - bn: &BeaconNodeHttpClient, - start_epoch: Epoch, - end_epoch: Epoch, - slots_per_epoch: u64, -) -> Result, Error> { - let mut output = Vec::new(); - let result = bn - .get_lighthouse_analysis_attestation_performance( - start_epoch, - end_epoch, - "global".to_string(), - ) - .await?; - for index in result { - for epoch in index.epochs { - if epoch.1.active { - // Check if the attestation is suboptimal. - if !epoch.1.source || !epoch.1.head || !epoch.1.target { - output.push(WatchSuboptimalAttestation { - epoch_start_slot: WatchSlot::from_slot( - Epoch::new(epoch.0).start_slot(slots_per_epoch), - ), - index: index.index as i32, - source: epoch.1.source, - head: epoch.1.head, - target: epoch.1.target, - }) - } - } - } - } - Ok(output) -} diff --git a/watch/src/suboptimal_attestations/server.rs b/watch/src/suboptimal_attestations/server.rs deleted file mode 100644 index 391db9a41b5..00000000000 --- a/watch/src/suboptimal_attestations/server.rs +++ /dev/null @@ -1,299 +0,0 @@ -use crate::database::{ - get_canonical_slot, get_connection, get_validator_by_index, get_validator_by_public_key, - get_validators_clients_at_slot, get_validators_latest_proposer_info, PgPool, WatchPK, - WatchSlot, -}; - -use crate::blockprint::database::construct_validator_blockprints_at_slot; -use crate::server::Error; -use crate::suboptimal_attestations::database::{ - get_all_suboptimal_attestations_for_epoch, get_attestation_by_index, - get_validators_missed_head, get_validators_missed_source, get_validators_missed_target, - WatchAttestation, WatchSuboptimalAttestation, -}; - -use axum::{extract::Path, routing::get, Extension, Json, Router}; -use std::collections::{HashMap, HashSet}; -use std::str::FromStr; -use types::Epoch; - -// Will return Ok(None) if the epoch is not synced or if the validator does not exist. -// In the future it might be worth differentiating these events. -pub async fn get_validator_attestation( - Path((validator_query, epoch_query)): Path<(String, u64)>, - Extension(pool): Extension, - Extension(slots_per_epoch): Extension, -) -> Result>, Error> { - let mut conn = get_connection(&pool).map_err(Error::Database)?; - let epoch = Epoch::new(epoch_query); - - // Ensure the database has synced the target epoch. - if get_canonical_slot( - &mut conn, - WatchSlot::from_slot(epoch.end_slot(slots_per_epoch)), - )? - .is_none() - { - // Epoch is not fully synced. - return Ok(Json(None)); - } - - let index = if validator_query.starts_with("0x") { - let pubkey = WatchPK::from_str(&validator_query).map_err(|_| Error::BadRequest)?; - get_validator_by_public_key(&mut conn, pubkey)? - .ok_or(Error::NotFound)? - .index - } else { - i32::from_str(&validator_query).map_err(|_| Error::BadRequest)? - }; - let attestation = if let Some(suboptimal_attestation) = - get_attestation_by_index(&mut conn, index, epoch, slots_per_epoch)? - { - Some(suboptimal_attestation.to_attestation(slots_per_epoch)) - } else { - // Attestation was not in database. Check if the validator was active. - match get_validator_by_index(&mut conn, index)? { - Some(validator) => { - if let Some(activation_epoch) = validator.activation_epoch { - if activation_epoch <= epoch.as_u64() as i32 { - if let Some(exit_epoch) = validator.exit_epoch { - if exit_epoch > epoch.as_u64() as i32 { - // Validator is active and has not yet exited. - Some(WatchAttestation::optimal(index, epoch)) - } else { - // Validator has exited. - None - } - } else { - // Validator is active and has not yet exited. - Some(WatchAttestation::optimal(index, epoch)) - } - } else { - // Validator is not yet active. - None - } - } else { - // Validator is not yet active. - None - } - } - None => return Err(Error::Other("Validator index does not exist".to_string())), - } - }; - Ok(Json(attestation)) -} - -pub async fn get_all_validators_attestations( - Path(epoch): Path, - Extension(pool): Extension, - Extension(slots_per_epoch): Extension, -) -> Result>, Error> { - let mut conn = get_connection(&pool).map_err(Error::Database)?; - - let epoch_start_slot = WatchSlot::from_slot(Epoch::new(epoch).start_slot(slots_per_epoch)); - - Ok(Json(get_all_suboptimal_attestations_for_epoch( - &mut conn, - epoch_start_slot, - )?)) -} - -pub async fn get_validators_missed_vote( - Path((vote, epoch)): Path<(String, u64)>, - Extension(pool): Extension, - Extension(slots_per_epoch): Extension, -) -> Result>, Error> { - let mut conn = get_connection(&pool).map_err(Error::Database)?; - - let epoch_start_slot = WatchSlot::from_slot(Epoch::new(epoch).start_slot(slots_per_epoch)); - match vote.to_lowercase().as_str() { - "source" => Ok(Json(get_validators_missed_source( - &mut conn, - epoch_start_slot, - )?)), - "head" => Ok(Json(get_validators_missed_head( - &mut conn, - epoch_start_slot, - )?)), - "target" => Ok(Json(get_validators_missed_target( - &mut conn, - epoch_start_slot, - )?)), - _ => Err(Error::BadRequest), - } -} - -pub async fn get_validators_missed_vote_graffiti( - Path((vote, epoch)): Path<(String, u64)>, - Extension(pool): Extension, - Extension(slots_per_epoch): Extension, -) -> Result>, Error> { - let mut conn = get_connection(&pool).map_err(Error::Database)?; - - let Json(indices) = get_validators_missed_vote( - Path((vote, epoch)), - Extension(pool), - Extension(slots_per_epoch), - ) - .await?; - - let graffitis = get_validators_latest_proposer_info(&mut conn, indices)? - .values() - .map(|info| info.graffiti.clone()) - .collect::>(); - - let mut result = HashMap::new(); - for graffiti in graffitis { - if !result.contains_key(&graffiti) { - result.insert(graffiti.clone(), 0); - } - *result - .get_mut(&graffiti) - .ok_or_else(|| Error::Other("An unexpected error occurred".to_string()))? += 1; - } - - Ok(Json(result)) -} - -pub fn attestation_routes() -> Router { - Router::new() - .route( - "/v1/validators/:validator/attestation/:epoch", - get(get_validator_attestation), - ) - .route( - "/v1/validators/all/attestation/:epoch", - get(get_all_validators_attestations), - ) - .route( - "/v1/validators/missed/:vote/:epoch", - get(get_validators_missed_vote), - ) - .route( - "/v1/validators/missed/:vote/:epoch/graffiti", - get(get_validators_missed_vote_graffiti), - ) -} - -/// The functions below are dependent on Blockprint and if it is disabled, the endpoints will be -/// disabled. -pub async fn get_clients_missed_vote( - Path((vote, epoch)): Path<(String, u64)>, - Extension(pool): Extension, - Extension(slots_per_epoch): Extension, -) -> Result>, Error> { - let mut conn = get_connection(&pool).map_err(Error::Database)?; - - let Json(indices) = get_validators_missed_vote( - Path((vote, epoch)), - Extension(pool), - Extension(slots_per_epoch), - ) - .await?; - - // All validators which missed the vote. - let indices_map = indices.into_iter().collect::>(); - - let target_slot = WatchSlot::from_slot(Epoch::new(epoch).start_slot(slots_per_epoch)); - - // All validators. - let client_map = - construct_validator_blockprints_at_slot(&mut conn, target_slot, slots_per_epoch)?; - - let mut result = HashMap::new(); - - for index in indices_map { - if let Some(print) = client_map.get(&index) { - if !result.contains_key(print) { - result.insert(print.clone(), 0); - } - *result - .get_mut(print) - .ok_or_else(|| Error::Other("An unexpected error occurred".to_string()))? += 1; - } - } - - Ok(Json(result)) -} - -pub async fn get_clients_missed_vote_percentages( - Path((vote, epoch)): Path<(String, u64)>, - Extension(pool): Extension, - Extension(slots_per_epoch): Extension, -) -> Result>, Error> { - let Json(clients_counts) = get_clients_missed_vote( - Path((vote, epoch)), - Extension(pool.clone()), - Extension(slots_per_epoch), - ) - .await?; - - let target_slot = WatchSlot::from_slot(Epoch::new(epoch).start_slot(slots_per_epoch)); - - let mut conn = get_connection(&pool)?; - let totals = get_validators_clients_at_slot(&mut conn, target_slot, slots_per_epoch)?; - - let mut result = HashMap::new(); - for (client, count) in clients_counts.iter() { - let client_total: f64 = *totals - .get(client) - .ok_or_else(|| Error::Other("Client type mismatch".to_string()))? - as f64; - // `client_total` should never be `0`, but if it is, return `0` instead of `inf`. - if client_total == 0.0 { - result.insert(client.to_string(), 0.0); - } else { - let percentage: f64 = *count as f64 / client_total * 100.0; - result.insert(client.to_string(), percentage); - } - } - - Ok(Json(result)) -} - -pub async fn get_clients_missed_vote_percentages_relative( - Path((vote, epoch)): Path<(String, u64)>, - Extension(pool): Extension, - Extension(slots_per_epoch): Extension, -) -> Result>, Error> { - let Json(clients_counts) = get_clients_missed_vote( - Path((vote, epoch)), - Extension(pool), - Extension(slots_per_epoch), - ) - .await?; - - let mut total: u64 = 0; - for (_, count) in clients_counts.iter() { - total += *count - } - - let mut result = HashMap::new(); - for (client, count) in clients_counts.iter() { - // `total` should never be 0, but if it is, return `-` instead of `inf`. - if total == 0 { - result.insert(client.to_string(), 0.0); - } else { - let percentage: f64 = *count as f64 / total as f64 * 100.0; - result.insert(client.to_string(), percentage); - } - } - - Ok(Json(result)) -} - -pub fn blockprint_attestation_routes() -> Router { - Router::new() - .route( - "/v1/clients/missed/:vote/:epoch", - get(get_clients_missed_vote), - ) - .route( - "/v1/clients/missed/:vote/:epoch/percentages", - get(get_clients_missed_vote_percentages), - ) - .route( - "/v1/clients/missed/:vote/:epoch/percentages/relative", - get(get_clients_missed_vote_percentages_relative), - ) -} diff --git a/watch/src/suboptimal_attestations/updater.rs b/watch/src/suboptimal_attestations/updater.rs deleted file mode 100644 index d8f6ec57d5a..00000000000 --- a/watch/src/suboptimal_attestations/updater.rs +++ /dev/null @@ -1,236 +0,0 @@ -use crate::database::{self, Error as DbError}; -use crate::updater::{Error, UpdateHandler}; - -use crate::suboptimal_attestations::get_attestation_performances; - -use eth2::types::EthSpec; -use log::{debug, error, warn}; - -const MAX_SIZE_SINGLE_REQUEST_ATTESTATIONS: u64 = 50; - -impl UpdateHandler { - /// Forward fills the `suboptimal_attestations` table starting from the entry with the highest - /// slot. - /// - /// It construts a request to the `attestation_performance` API endpoint with: - /// `start_epoch` -> highest completely filled epoch + 1 (or epoch of lowest canonical slot) - /// `end_epoch` -> epoch of highest canonical slot - /// - /// It will resync the latest epoch if it is not fully filled but will not overwrite existing - /// values unless there is a re-org. - /// That is, `if highest_filled_slot % slots_per_epoch != 31`. - /// - /// In the event the most recent epoch has no suboptimal attestations, it will attempt to - /// resync that epoch. The odds of this occuring on mainnet are vanishingly small so it is not - /// accounted for. - /// - /// Request range will not exceed `MAX_SIZE_SINGLE_REQUEST_ATTESTATIONS`. - pub async fn fill_suboptimal_attestations(&mut self) -> Result<(), Error> { - let mut conn = database::get_connection(&self.pool)?; - - let highest_filled_slot_opt = if self.config.attestations { - database::get_highest_attestation(&mut conn)? - .map(|attestation| attestation.epoch_start_slot.as_slot()) - } else { - return Err(Error::NotEnabled("attestations".to_string())); - }; - - let start_epoch = if let Some(highest_filled_slot) = highest_filled_slot_opt { - if highest_filled_slot % self.slots_per_epoch == self.slots_per_epoch.saturating_sub(1) - { - // The whole epoch is filled so we can begin syncing the next one. - highest_filled_slot.epoch(self.slots_per_epoch) + 1 - } else { - // The epoch is only partially synced. Try to sync it fully. - highest_filled_slot.epoch(self.slots_per_epoch) - } - } else { - // No rows present in the `suboptimal_attestations` table. Use `canonical_slots` - // instead. - if let Some(lowest_canonical_slot) = database::get_lowest_canonical_slot(&mut conn)? { - lowest_canonical_slot - .slot - .as_slot() - .epoch(self.slots_per_epoch) - } else { - // There are no slots in the database, do not fill the `suboptimal_attestations` - // table. - warn!("Refusing to fill the `suboptimal_attestations` table as there are no slots in the database"); - return Ok(()); - } - }; - - if let Some(highest_canonical_slot) = - database::get_highest_canonical_slot(&mut conn)?.map(|slot| slot.slot.as_slot()) - { - let mut end_epoch = highest_canonical_slot.epoch(self.slots_per_epoch); - - // The `lighthouse/analysis/attestation_performance` endpoint can only retrieve attestations - // which are more than 1 epoch old. - // We assume that `highest_canonical_slot` is near the head of the chain. - end_epoch = end_epoch.saturating_sub(2_u64); - - // If end_epoch == 0 then the chain just started so we need to wait until - // `current_epoch >= 2`. - if end_epoch == 0 { - debug!("Chain just begun, refusing to sync attestations"); - return Ok(()); - } - - if start_epoch > end_epoch { - debug!("Attestations are up to date with the head of the database"); - return Ok(()); - } - - // Ensure the size of the request does not exceed the maximum allowed value. - if start_epoch < end_epoch.saturating_sub(MAX_SIZE_SINGLE_REQUEST_ATTESTATIONS) { - end_epoch = start_epoch + MAX_SIZE_SINGLE_REQUEST_ATTESTATIONS - } - - if let Some(lowest_canonical_slot) = - database::get_lowest_canonical_slot(&mut conn)?.map(|slot| slot.slot.as_slot()) - { - let mut attestations = get_attestation_performances( - &self.bn, - start_epoch, - end_epoch, - self.slots_per_epoch, - ) - .await?; - - // Only insert attestations with corresponding `canonical_slot`s. - attestations.retain(|attestation| { - attestation.epoch_start_slot.as_slot() >= lowest_canonical_slot - && attestation.epoch_start_slot.as_slot() <= highest_canonical_slot - }); - database::insert_batch_suboptimal_attestations(&mut conn, attestations)?; - } else { - return Err(Error::Database(DbError::Other( - "Database did not return a lowest canonical slot when one exists".to_string(), - ))); - } - } else { - // There are no slots in the `canonical_slots` table, but there are entries in the - // `suboptimal_attestations` table. This is a critical failure. It usually means - // someone has manually tampered with the database tables and should not occur during - // normal operation. - error!("Database is corrupted. Please re-sync the database"); - return Err(Error::Database(DbError::DatabaseCorrupted)); - } - - Ok(()) - } - - /// Backfill the `suboptimal_attestations` table starting from the entry with the lowest slot. - /// - /// It constructs a request to the `attestation_performance` API endpoint with: - /// `start_epoch` -> epoch of the lowest `canonical_slot`. - /// `end_epoch` -> epoch of the lowest filled `suboptimal_attestation` - 1 (or epoch of highest - /// canonical slot) - /// - /// It will resync the lowest epoch if it is not fully filled. - /// That is, `if lowest_filled_slot % slots_per_epoch != 0` - /// - /// In the event there are no suboptimal attestations present in the lowest epoch, it will attempt to - /// resync the epoch. The odds of this occuring on mainnet are vanishingly small so it is not - /// accounted for. - /// - /// Request range will not exceed `MAX_SIZE_SINGLE_REQUEST_ATTESTATIONS`. - pub async fn backfill_suboptimal_attestations(&mut self) -> Result<(), Error> { - let mut conn = database::get_connection(&self.pool)?; - let max_attestation_backfill = self.config.max_backfill_size_epochs; - - // Get the slot of the lowest entry in the `suboptimal_attestations` table. - let lowest_filled_slot_opt = if self.config.attestations { - database::get_lowest_attestation(&mut conn)? - .map(|attestation| attestation.epoch_start_slot.as_slot()) - } else { - return Err(Error::NotEnabled("attestations".to_string())); - }; - - let end_epoch = if let Some(lowest_filled_slot) = lowest_filled_slot_opt { - if lowest_filled_slot % self.slots_per_epoch == 0 { - lowest_filled_slot - .epoch(self.slots_per_epoch) - .saturating_sub(1_u64) - } else { - // The epoch is only partially synced. Try to sync it fully. - lowest_filled_slot.epoch(self.slots_per_epoch) - } - } else { - // No entries in the `suboptimal_attestations` table. Use `canonical_slots` instead. - if let Some(highest_canonical_slot) = - database::get_highest_canonical_slot(&mut conn)?.map(|slot| slot.slot.as_slot()) - { - // Subtract 2 since `end_epoch` must be less than the current epoch - 1. - // We assume that `highest_canonical_slot` is near the head of the chain. - highest_canonical_slot - .epoch(self.slots_per_epoch) - .saturating_sub(2_u64) - } else { - // There are no slots in the database, do not backfill the - // `suboptimal_attestations` table. - warn!("Refusing to backfill attestations as there are no slots in the database"); - return Ok(()); - } - }; - - if end_epoch == 0 { - debug!("Attestations backfill is complete"); - return Ok(()); - } - - if let Some(lowest_canonical_slot) = - database::get_lowest_canonical_slot(&mut conn)?.map(|slot| slot.slot.as_slot()) - { - let mut start_epoch = lowest_canonical_slot.epoch(self.slots_per_epoch); - - if start_epoch > end_epoch { - debug!("Attestations are up to date with the base of the database"); - return Ok(()); - } - - // Ensure the request range does not exceed `max_attestation_backfill` or - // `MAX_SIZE_SINGLE_REQUEST_ATTESTATIONS`. - if start_epoch < end_epoch.saturating_sub(max_attestation_backfill) { - start_epoch = end_epoch.saturating_sub(max_attestation_backfill) - } - if start_epoch < end_epoch.saturating_sub(MAX_SIZE_SINGLE_REQUEST_ATTESTATIONS) { - start_epoch = end_epoch.saturating_sub(MAX_SIZE_SINGLE_REQUEST_ATTESTATIONS) - } - - if let Some(highest_canonical_slot) = - database::get_highest_canonical_slot(&mut conn)?.map(|slot| slot.slot.as_slot()) - { - let mut attestations = get_attestation_performances( - &self.bn, - start_epoch, - end_epoch, - self.slots_per_epoch, - ) - .await?; - - // Only insert `suboptimal_attestations` with corresponding `canonical_slots`. - attestations.retain(|attestation| { - attestation.epoch_start_slot.as_slot() >= lowest_canonical_slot - && attestation.epoch_start_slot.as_slot() <= highest_canonical_slot - }); - - database::insert_batch_suboptimal_attestations(&mut conn, attestations)?; - } else { - return Err(Error::Database(DbError::Other( - "Database did not return a lowest slot when one exists".to_string(), - ))); - } - } else { - // There are no slots in the `canonical_slot` table, but there are entries in the - // `suboptimal_attestations` table. This is a critical failure. It usually means - // someone has manually tampered with the database tables and should not occur during - // normal operation. - error!("Database is corrupted. Please re-sync the database"); - return Err(Error::Database(DbError::DatabaseCorrupted)); - } - - Ok(()) - } -} diff --git a/watch/src/updater/config.rs b/watch/src/updater/config.rs deleted file mode 100644 index 0179be73db6..00000000000 --- a/watch/src/updater/config.rs +++ /dev/null @@ -1,65 +0,0 @@ -use serde::{Deserialize, Serialize}; - -pub const BEACON_NODE_URL: &str = "http://127.0.0.1:5052"; - -pub const fn max_backfill_size_epochs() -> u64 { - 2 -} -pub const fn backfill_stop_epoch() -> u64 { - 0 -} -pub const fn attestations() -> bool { - true -} -pub const fn proposer_info() -> bool { - true -} -pub const fn block_rewards() -> bool { - true -} -pub const fn block_packing() -> bool { - true -} - -fn beacon_node_url() -> String { - BEACON_NODE_URL.to_string() -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Config { - /// The URL of the beacon you wish to sync from. - #[serde(default = "beacon_node_url")] - pub beacon_node_url: String, - /// The maximum size each backfill iteration will allow per request (in epochs). - #[serde(default = "max_backfill_size_epochs")] - pub max_backfill_size_epochs: u64, - /// The epoch at which to never backfill past. - #[serde(default = "backfill_stop_epoch")] - pub backfill_stop_epoch: u64, - /// Whether to sync the suboptimal_attestations table. - #[serde(default = "attestations")] - pub attestations: bool, - /// Whether to sync the proposer_info table. - #[serde(default = "proposer_info")] - pub proposer_info: bool, - /// Whether to sync the block_rewards table. - #[serde(default = "block_rewards")] - pub block_rewards: bool, - /// Whether to sync the block_packing table. - #[serde(default = "block_packing")] - pub block_packing: bool, -} - -impl Default for Config { - fn default() -> Self { - Self { - beacon_node_url: beacon_node_url(), - max_backfill_size_epochs: max_backfill_size_epochs(), - backfill_stop_epoch: backfill_stop_epoch(), - attestations: attestations(), - proposer_info: proposer_info(), - block_rewards: block_rewards(), - block_packing: block_packing(), - } - } -} diff --git a/watch/src/updater/error.rs b/watch/src/updater/error.rs deleted file mode 100644 index 13c83bcf010..00000000000 --- a/watch/src/updater/error.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::blockprint::Error as BlockprintError; -use crate::database::Error as DbError; -use beacon_node::beacon_chain::BeaconChainError; -use eth2::{Error as Eth2Error, SensitiveError}; -use std::fmt; - -#[derive(Debug)] -#[allow(dead_code)] -pub enum Error { - BeaconChain(BeaconChainError), - Eth2(Eth2Error), - SensitiveUrl(SensitiveError), - Database(DbError), - Blockprint(BlockprintError), - UnableToGetRemoteHead, - BeaconNodeSyncing, - NotEnabled(String), - NoValidatorsFound, - BeaconNodeNotCompatible(String), - InvalidConfig(String), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self) - } -} - -impl From for Error { - fn from(e: BeaconChainError) -> Self { - Error::BeaconChain(e) - } -} - -impl From for Error { - fn from(e: Eth2Error) -> Self { - Error::Eth2(e) - } -} - -impl From for Error { - fn from(e: SensitiveError) -> Self { - Error::SensitiveUrl(e) - } -} - -impl From for Error { - fn from(e: DbError) -> Self { - Error::Database(e) - } -} - -impl From for Error { - fn from(e: BlockprintError) -> Self { - Error::Blockprint(e) - } -} diff --git a/watch/src/updater/handler.rs b/watch/src/updater/handler.rs deleted file mode 100644 index 8f5e3f8e4a3..00000000000 --- a/watch/src/updater/handler.rs +++ /dev/null @@ -1,471 +0,0 @@ -use crate::blockprint::WatchBlockprintClient; -use crate::config::Config as FullConfig; -use crate::database::{self, PgPool, WatchCanonicalSlot, WatchHash, WatchSlot}; -use crate::updater::{Config, Error, WatchSpec}; -use beacon_node::beacon_chain::BeaconChainError; -use eth2::{ - types::{BlockId, SyncingData}, - BeaconNodeHttpClient, SensitiveUrl, -}; -use log::{debug, error, info, warn}; -use std::collections::HashSet; -use std::marker::PhantomData; -use types::{BeaconBlockHeader, EthSpec, Hash256, SignedBeaconBlock, Slot}; - -use crate::updater::{get_beacon_block, get_header, get_validators}; - -const MAX_EXPECTED_REORG_LENGTH: u64 = 32; - -/// Ensure the existing database is valid for this run. -pub async fn ensure_valid_database( - spec: &WatchSpec, - pool: &mut PgPool, -) -> Result<(), Error> { - let mut conn = database::get_connection(pool)?; - - let bn_slots_per_epoch = spec.slots_per_epoch(); - let bn_config_name = spec.network.clone(); - - if let Some((db_config_name, db_slots_per_epoch)) = database::get_active_config(&mut conn)? { - if db_config_name != bn_config_name || db_slots_per_epoch != bn_slots_per_epoch as i32 { - Err(Error::InvalidConfig( - "The config stored in the database does not match the beacon node.".to_string(), - )) - } else { - // Configs match. - Ok(()) - } - } else { - // No config exists in the DB. - database::insert_active_config(&mut conn, bn_config_name, bn_slots_per_epoch)?; - Ok(()) - } -} - -pub struct UpdateHandler { - pub pool: PgPool, - pub bn: BeaconNodeHttpClient, - pub blockprint: Option, - pub config: Config, - pub slots_per_epoch: u64, - pub _phantom: PhantomData, -} - -impl UpdateHandler { - pub async fn new( - bn: BeaconNodeHttpClient, - spec: WatchSpec, - config: FullConfig, - ) -> Result, Error> { - let blockprint = if config.blockprint.enabled { - if let Some(server) = config.blockprint.url { - let blockprint_url = SensitiveUrl::parse(&server).map_err(Error::SensitiveUrl)?; - Some(WatchBlockprintClient { - client: reqwest::Client::new(), - server: blockprint_url, - username: config.blockprint.username, - password: config.blockprint.password, - }) - } else { - return Err(Error::NotEnabled( - "blockprint was enabled but url was not set".to_string(), - )); - } - } else { - None - }; - - let mut pool = database::build_connection_pool(&config.database)?; - - ensure_valid_database(&spec, &mut pool).await?; - - Ok(Self { - pool, - bn, - blockprint, - config: config.updater, - slots_per_epoch: spec.slots_per_epoch(), - _phantom: PhantomData, - }) - } - - /// Gets the syncing status of the connected beacon node. - pub async fn get_bn_syncing_status(&mut self) -> Result { - Ok(self.bn.get_node_syncing().await?.data) - } - - /// Gets a list of block roots from the database which do not yet contain a corresponding - /// entry in the `beacon_blocks` table and inserts them. - pub async fn update_unknown_blocks(&mut self) -> Result<(), Error> { - let mut conn = database::get_connection(&self.pool)?; - let roots = database::get_unknown_canonical_blocks(&mut conn)?; - for root in roots { - let block_opt: Option> = - get_beacon_block(&self.bn, BlockId::Root(root.as_hash())).await?; - if let Some(block) = block_opt { - database::insert_beacon_block(&mut conn, block, root)?; - } - } - - Ok(()) - } - - /// Performs a head update with the following steps: - /// 1. Pull the latest header from the beacon node and the latest canonical slot from the - /// database. - /// 2. Loop back through the beacon node and database to find the first matching slot -> root - /// pair. - /// 3. Go back `MAX_EXPECTED_REORG_LENGTH` slots through the database ensuring it is - /// consistent with the beacon node. If a re-org occurs beyond this range, we cannot recover. - /// 4. Remove any invalid slots from the database. - /// 5. Sync all blocks between the first valid block of the database and the head of the beacon - /// chain. - /// - /// In the event there are no slots present in the database, it will sync from the head block - /// block back to the first slot of the epoch. - /// This will ensure backfills are always done in full epochs (which helps keep certain syncing - /// tasks efficient). - pub async fn perform_head_update(&mut self) -> Result<(), Error> { - let mut conn = database::get_connection(&self.pool)?; - // Load the head from the beacon node. - let bn_header = get_header(&self.bn, BlockId::Head) - .await? - .ok_or(Error::UnableToGetRemoteHead)?; - let header_root = bn_header.canonical_root(); - - if let Some(latest_matching_canonical_slot) = - self.get_first_matching_block(bn_header.clone()).await? - { - // Check for reorgs. - let latest_db_slot = self.check_for_reorg(latest_matching_canonical_slot).await?; - - // Remove all slots above `latest_db_slot` from the database. - let result = database::delete_canonical_slots_above( - &mut conn, - WatchSlot::from_slot(latest_db_slot), - )?; - info!("{result} old records removed during head update"); - - if result > 0 { - // If slots were removed, we need to resync the suboptimal_attestations table for - // the epoch since they will have changed and cannot be fixed by a simple update. - let epoch = latest_db_slot - .epoch(self.slots_per_epoch) - .saturating_sub(1_u64); - debug!("Preparing to resync attestations above epoch {epoch}"); - database::delete_suboptimal_attestations_above( - &mut conn, - WatchSlot::from_slot(epoch.start_slot(self.slots_per_epoch)), - )?; - } - - // Since we are syncing backwards, `start_slot > `end_slot`. - let start_slot = bn_header.slot; - let end_slot = latest_db_slot + 1; - self.reverse_fill_canonical_slots(bn_header, header_root, false, start_slot, end_slot) - .await?; - info!("Reverse sync begun at slot {start_slot} and stopped at slot {end_slot}"); - - // Attempt to sync new blocks with blockprint. - //self.sync_blockprint_until(start_slot).await?; - } else { - // There are no matching parent blocks. Sync from the head block back until the first - // block of the epoch. - let start_slot = bn_header.slot; - let end_slot = start_slot.saturating_sub(start_slot % self.slots_per_epoch); - self.reverse_fill_canonical_slots(bn_header, header_root, false, start_slot, end_slot) - .await?; - info!("Reverse sync begun at slot {start_slot} and stopped at slot {end_slot}"); - } - - Ok(()) - } - - /// Attempt to find a row in the `canonical_slots` table which matches the `canonical_root` of - /// the block header as reported by the beacon node. - /// - /// Any blocks above this value are not canonical according to the beacon node. - /// - /// Note: In the event that there are skip slots above the slot returned by the function, - /// they will not be returned, so may be pruned or re-synced by other code despite being - /// canonical. - pub async fn get_first_matching_block( - &mut self, - mut bn_header: BeaconBlockHeader, - ) -> Result, Error> { - let mut conn = database::get_connection(&self.pool)?; - - // Load latest non-skipped canonical slot from database. - if let Some(db_canonical_slot) = - database::get_highest_non_skipped_canonical_slot(&mut conn)? - { - // Check if the header or parent root matches the entry in the database. - if bn_header.parent_root == db_canonical_slot.root.as_hash() - || bn_header.canonical_root() == db_canonical_slot.root.as_hash() - { - Ok(Some(db_canonical_slot)) - } else { - // Header is not the child of the highest entry in the database. - // From here we need to iterate backwards through the database until we find - // a slot -> root pair that matches the beacon node. - loop { - // Store working `parent_root`. - let parent_root = bn_header.parent_root; - - // Try the next header. - let next_header = get_header(&self.bn, BlockId::Root(parent_root)).await?; - if let Some(header) = next_header { - bn_header = header.clone(); - if let Some(db_canonical_slot) = database::get_canonical_slot_by_root( - &mut conn, - WatchHash::from_hash(header.parent_root), - )? { - // Check if the entry in the database matches the parent of - // the header. - if header.parent_root == db_canonical_slot.root.as_hash() { - return Ok(Some(db_canonical_slot)); - } else { - // Move on to the next header. - continue; - } - } else { - // Database does not have the referenced root. Try the next header. - continue; - } - } else { - // If we get this error it means that the `parent_root` of the header - // did not reference a canonical block. - return Err(Error::BeaconChain(BeaconChainError::MissingBeaconBlock( - parent_root, - ))); - } - } - } - } else { - // There are no non-skipped blocks present in the database. - Ok(None) - } - } - - /// Given the latest slot in the database which matches a root in the beacon node, - /// traverse back through the database for `MAX_EXPECTED_REORG_LENGTH` slots to ensure the tip - /// of the database is consistent with the beacon node (in the case that reorgs have occured). - /// - /// Returns the slot before the oldest canonical_slot which has an invalid child. - pub async fn check_for_reorg( - &mut self, - latest_canonical_slot: WatchCanonicalSlot, - ) -> Result { - let mut conn = database::get_connection(&self.pool)?; - - let end_slot = latest_canonical_slot.slot.as_u64(); - let start_slot = end_slot.saturating_sub(MAX_EXPECTED_REORG_LENGTH); - - for i in start_slot..end_slot { - let slot = Slot::new(i); - let db_canonical_slot_opt = - database::get_canonical_slot(&mut conn, WatchSlot::from_slot(slot))?; - if let Some(db_canonical_slot) = db_canonical_slot_opt { - let header_opt = get_header(&self.bn, BlockId::Slot(slot)).await?; - if let Some(header) = header_opt { - if header.canonical_root() == db_canonical_slot.root.as_hash() { - // The roots match (or are both skip slots). - continue; - } else { - // The block roots do not match. We need to re-sync from here. - warn!("Block {slot} does not match the beacon node. Resyncing"); - return Ok(slot.saturating_sub(1_u64)); - } - } else if !db_canonical_slot.skipped { - // The block exists in the database, but does not exist on the beacon node. - // We need to re-sync from here. - warn!("Block {slot} does not exist on the beacon node. Resyncing"); - return Ok(slot.saturating_sub(1_u64)); - } - } else { - // This slot does not exist in the database. - let lowest_slot = database::get_lowest_canonical_slot(&mut conn)? - .map(|canonical_slot| canonical_slot.slot.as_slot()); - if lowest_slot > Some(slot) { - // The database has not back-filled this slot yet, so skip it. - continue; - } else { - // The database does not contain this block, but has back-filled past it. - // We need to resync from here. - warn!("Slot {slot} missing from database. Resyncing"); - return Ok(slot.saturating_sub(1_u64)); - } - } - } - - // The database is consistent with the beacon node, so return the head of the database. - Ok(latest_canonical_slot.slot.as_slot()) - } - - /// Fills the canonical slots table beginning from `start_slot` and ending at `end_slot`. - /// It fills in reverse order, that is, `start_slot` is higher than `end_slot`. - /// - /// Skip slots set `root` to the root of the previous non-skipped slot and also sets - /// `skipped == true`. - /// - /// Since it uses `insert_canonical_slot` to interact with the database, it WILL NOT overwrite - /// existing rows. This means that any part of the chain within `end_slot..=start_slot` that - /// needs to be resynced, must first be deleted from the database. - pub async fn reverse_fill_canonical_slots( - &mut self, - mut header: BeaconBlockHeader, - mut header_root: Hash256, - mut skipped: bool, - start_slot: Slot, - end_slot: Slot, - ) -> Result { - let mut count = 0; - - let mut conn = database::get_connection(&self.pool)?; - - // Iterate, descending from `start_slot` (higher) to `end_slot` (lower). - for slot in (end_slot.as_u64()..=start_slot.as_u64()).rev() { - // Insert header. - database::insert_canonical_slot( - &mut conn, - WatchCanonicalSlot { - slot: WatchSlot::new(slot), - root: WatchHash::from_hash(header_root), - skipped, - beacon_block: None, - }, - )?; - count += 1; - - // Load the next header: - // We must use BlockId::Slot since we want to include skip slots. - header = if let Some(new_header) = get_header( - &self.bn, - BlockId::Slot(Slot::new(slot.saturating_sub(1_u64))), - ) - .await? - { - header_root = new_header.canonical_root(); - skipped = false; - new_header - } else { - if header.slot == 0 { - info!("Reverse fill exhausted at slot 0"); - break; - } - // Slot was skipped, so use the parent_root (most recent non-skipped block). - skipped = true; - header_root = header.parent_root; - header - }; - } - - Ok(count) - } - - /// Backfills the `canonical_slots` table starting from the lowest non-skipped slot and - /// stopping after `max_backfill_size_epochs` epochs. - pub async fn backfill_canonical_slots(&mut self) -> Result<(), Error> { - let mut conn = database::get_connection(&self.pool)?; - let backfill_stop_slot = self.config.backfill_stop_epoch * self.slots_per_epoch; - // Check to see if we have finished backfilling. - if let Some(lowest_slot) = database::get_lowest_canonical_slot(&mut conn)? { - if lowest_slot.slot.as_slot() == backfill_stop_slot { - debug!("Backfill sync complete, all slots filled"); - return Ok(()); - } - } - - let backfill_slot_count = self.config.max_backfill_size_epochs * self.slots_per_epoch; - - if let Some(lowest_non_skipped_canonical_slot) = - database::get_lowest_non_skipped_canonical_slot(&mut conn)? - { - // Set `start_slot` equal to the lowest non-skipped slot in the database. - // While this will attempt to resync some parts of the bottom of the chain, it reduces - // complexity when dealing with skip slots. - let start_slot = lowest_non_skipped_canonical_slot.slot.as_slot(); - let mut end_slot = lowest_non_skipped_canonical_slot - .slot - .as_slot() - .saturating_sub(backfill_slot_count); - - // Ensure end_slot doesn't go below `backfill_stop_epoch` - if end_slot <= backfill_stop_slot { - end_slot = Slot::new(backfill_stop_slot); - } - - let header_opt = get_header(&self.bn, BlockId::Slot(start_slot)).await?; - - if let Some(header) = header_opt { - let header_root = header.canonical_root(); - let count = self - .reverse_fill_canonical_slots(header, header_root, false, start_slot, end_slot) - .await?; - - info!("Backfill completed to slot: {end_slot}, records added: {count}"); - } else { - // The lowest slot of the database is inconsistent with the beacon node. - // Currently we have no way to recover from this. The entire database will need to - // be re-synced. - error!( - "Database is inconsistent with the beacon node. \ - Please ensure your beacon node is set to the right network, \ - otherwise you may need to resync" - ); - } - } else { - // There are no blocks in the database. Forward sync needs to happen first. - info!("Backfill was not performed since there are no blocks in the database"); - return Ok(()); - }; - - Ok(()) - } - - // Attempt to update the validator set. - // This downloads the latest validator set from the beacon node, and pulls the known validator - // set from the database. - // We then take any new or updated validators and insert them into the database (overwriting - // exiting validators). - // - // In the event there are no validators in the database, it will initialize the validator set. - pub async fn update_validator_set(&mut self) -> Result<(), Error> { - let mut conn = database::get_connection(&self.pool)?; - - let current_validators = database::get_all_validators(&mut conn)?; - - if !current_validators.is_empty() { - let old_validators = HashSet::from_iter(current_validators); - - // Pull the new validator set from the beacon node. - let new_validators = get_validators(&self.bn).await?; - - // The difference should only contain validators that contain either a new `exit_epoch` (implying an - // exit) or a new `index` (implying a validator activation). - let val_diff = new_validators.difference(&old_validators); - - for diff in val_diff { - database::insert_validator(&mut conn, diff.clone())?; - } - } else { - info!("No validators present in database. Initializing the validator set"); - self.initialize_validator_set().await?; - } - - Ok(()) - } - - // Initialize the validator set by downloading it from the beacon node, inserting blockprint - // data (if required) and writing it to the database. - pub async fn initialize_validator_set(&mut self) -> Result<(), Error> { - let mut conn = database::get_connection(&self.pool)?; - - // Pull all validators from the beacon node. - let validators = Vec::from_iter(get_validators(&self.bn).await?); - - database::insert_batch_validators(&mut conn, validators)?; - - Ok(()) - } -} diff --git a/watch/src/updater/mod.rs b/watch/src/updater/mod.rs deleted file mode 100644 index 65e0a90a2b4..00000000000 --- a/watch/src/updater/mod.rs +++ /dev/null @@ -1,234 +0,0 @@ -use crate::config::Config as FullConfig; -use crate::database::{WatchPK, WatchValidator}; -use eth2::{ - types::{BlockId, StateId}, - BeaconNodeHttpClient, SensitiveUrl, Timeouts, -}; -use log::{debug, error, info}; -use std::collections::{HashMap, HashSet}; -use std::marker::PhantomData; -use std::time::{Duration, Instant}; -use types::{BeaconBlockHeader, EthSpec, GnosisEthSpec, MainnetEthSpec, SignedBeaconBlock}; - -pub use config::Config; -pub use error::Error; -pub use handler::UpdateHandler; - -mod config; -pub mod error; -pub mod handler; - -const FAR_FUTURE_EPOCH: u64 = u64::MAX; -const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5); - -const MAINNET: &str = "mainnet"; -const GNOSIS: &str = "gnosis"; - -pub struct WatchSpec { - network: String, - spec: PhantomData, -} - -impl WatchSpec { - fn slots_per_epoch(&self) -> u64 { - E::slots_per_epoch() - } -} - -impl WatchSpec { - pub fn mainnet(network: String) -> Self { - Self { - network, - spec: PhantomData, - } - } -} - -impl WatchSpec { - fn gnosis(network: String) -> Self { - Self { - network, - spec: PhantomData, - } - } -} - -pub async fn run_updater(config: FullConfig) -> Result<(), Error> { - let beacon_node_url = - SensitiveUrl::parse(&config.updater.beacon_node_url).map_err(Error::SensitiveUrl)?; - let bn = BeaconNodeHttpClient::new(beacon_node_url, Timeouts::set_all(DEFAULT_TIMEOUT)); - - let config_map = bn.get_config_spec::>().await?.data; - - let config_name = config_map - .get("CONFIG_NAME") - .ok_or_else(|| { - Error::BeaconNodeNotCompatible("No field CONFIG_NAME on beacon node spec".to_string()) - })? - .clone(); - - match config_map - .get("PRESET_BASE") - .ok_or_else(|| { - Error::BeaconNodeNotCompatible("No field PRESET_BASE on beacon node spec".to_string()) - })? - .to_lowercase() - .as_str() - { - MAINNET => { - let spec = WatchSpec::mainnet(config_name); - run_once(bn, spec, config).await - } - GNOSIS => { - let spec = WatchSpec::gnosis(config_name); - run_once(bn, spec, config).await - } - _ => unimplemented!("unsupported PRESET_BASE"), - } -} - -pub async fn run_once( - bn: BeaconNodeHttpClient, - spec: WatchSpec, - config: FullConfig, -) -> Result<(), Error> { - let mut watch = UpdateHandler::new(bn, spec, config.clone()).await?; - - let sync_data = watch.get_bn_syncing_status().await?; - if sync_data.is_syncing { - error!( - "Connected beacon node is still syncing: head_slot => {:?}, distance => {}", - sync_data.head_slot, sync_data.sync_distance - ); - return Err(Error::BeaconNodeSyncing); - } - - info!("Performing head update"); - let head_timer = Instant::now(); - watch.perform_head_update().await?; - let head_timer_elapsed = head_timer.elapsed(); - debug!("Head update complete, time taken: {head_timer_elapsed:?}"); - - info!("Performing block backfill"); - let block_backfill_timer = Instant::now(); - watch.backfill_canonical_slots().await?; - let block_backfill_timer_elapsed = block_backfill_timer.elapsed(); - debug!("Block backfill complete, time taken: {block_backfill_timer_elapsed:?}"); - - info!("Updating validator set"); - let validator_timer = Instant::now(); - watch.update_validator_set().await?; - let validator_timer_elapsed = validator_timer.elapsed(); - debug!("Validator update complete, time taken: {validator_timer_elapsed:?}"); - - // Update blocks after updating the validator set since the `proposer_index` must exist in the - // `validators` table. - info!("Updating unknown blocks"); - let unknown_block_timer = Instant::now(); - watch.update_unknown_blocks().await?; - let unknown_block_timer_elapsed = unknown_block_timer.elapsed(); - debug!("Unknown block update complete, time taken: {unknown_block_timer_elapsed:?}"); - - // Run additional modules - if config.updater.attestations { - info!("Updating suboptimal attestations"); - let attestation_timer = Instant::now(); - watch.fill_suboptimal_attestations().await?; - watch.backfill_suboptimal_attestations().await?; - let attestation_timer_elapsed = attestation_timer.elapsed(); - debug!("Attestation update complete, time taken: {attestation_timer_elapsed:?}"); - } - - if config.updater.block_rewards { - info!("Updating block rewards"); - let rewards_timer = Instant::now(); - watch.fill_block_rewards().await?; - watch.backfill_block_rewards().await?; - let rewards_timer_elapsed = rewards_timer.elapsed(); - debug!("Block Rewards update complete, time taken: {rewards_timer_elapsed:?}"); - } - - if config.updater.block_packing { - info!("Updating block packing statistics"); - let packing_timer = Instant::now(); - watch.fill_block_packing().await?; - watch.backfill_block_packing().await?; - let packing_timer_elapsed = packing_timer.elapsed(); - debug!("Block packing update complete, time taken: {packing_timer_elapsed:?}"); - } - - if config.blockprint.enabled { - info!("Updating blockprint"); - let blockprint_timer = Instant::now(); - watch.fill_blockprint().await?; - watch.backfill_blockprint().await?; - let blockprint_timer_elapsed = blockprint_timer.elapsed(); - debug!("Blockprint update complete, time taken: {blockprint_timer_elapsed:?}"); - } - - Ok(()) -} - -/// Queries the beacon node for a given `BlockId` and returns the `BeaconBlockHeader` if it exists. -pub async fn get_header( - bn: &BeaconNodeHttpClient, - block_id: BlockId, -) -> Result, Error> { - let resp = bn - .get_beacon_headers_block_id(block_id) - .await? - .map(|resp| (resp.data.root, resp.data.header.message)); - // When quering with root == 0x000... , slot 0 will be returned with parent_root == 0x0000... - // This check escapes the loop. - if let Some((root, header)) = resp { - if root == header.parent_root { - return Ok(None); - } else { - return Ok(Some(header)); - } - } - Ok(None) -} - -pub async fn get_beacon_block( - bn: &BeaconNodeHttpClient, - block_id: BlockId, -) -> Result>, Error> { - let block = bn.get_beacon_blocks(block_id).await?.map(|resp| resp.data); - - Ok(block) -} - -/// Queries the beacon node for the current validator set. -pub async fn get_validators(bn: &BeaconNodeHttpClient) -> Result, Error> { - let mut validator_map = HashSet::new(); - - let validators = bn - .get_beacon_states_validators(StateId::Head, None, None) - .await? - .ok_or(Error::NoValidatorsFound)? - .data; - - for val in validators { - // Only store `activation_epoch` if it not the `FAR_FUTURE_EPOCH`. - let activation_epoch = if val.validator.activation_epoch.as_u64() == FAR_FUTURE_EPOCH { - None - } else { - Some(val.validator.activation_epoch.as_u64() as i32) - }; - // Only store `exit_epoch` if it is not the `FAR_FUTURE_EPOCH`. - let exit_epoch = if val.validator.exit_epoch.as_u64() == FAR_FUTURE_EPOCH { - None - } else { - Some(val.validator.exit_epoch.as_u64() as i32) - }; - validator_map.insert(WatchValidator { - index: val.index as i32, - public_key: WatchPK::from_pubkey(val.validator.pubkey), - status: val.status.to_string(), - activation_epoch, - exit_epoch, - }); - } - Ok(validator_map) -} diff --git a/watch/tests/tests.rs b/watch/tests/tests.rs deleted file mode 100644 index e21cf151b11..00000000000 --- a/watch/tests/tests.rs +++ /dev/null @@ -1,1294 +0,0 @@ -#![recursion_limit = "256"] -#![cfg(unix)] - -use beacon_chain::{ - test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType}, - ChainConfig, -}; -use eth2::{types::BlockId, BeaconNodeHttpClient, SensitiveUrl, Timeouts}; -use http_api::test_utils::{create_api_server, ApiServer}; -use log::error; -use logging::test_logger; -use network::NetworkReceivers; -use rand::distributions::Alphanumeric; -use rand::{thread_rng, Rng}; -use std::collections::HashMap; -use std::env; -use std::time::Duration; -use testcontainers::{clients::Cli, core::WaitFor, Image, RunnableImage}; -use tokio::{runtime, task::JoinHandle}; -use tokio_postgres::{config::Config as PostgresConfig, Client, NoTls}; -use types::{Hash256, MainnetEthSpec, Slot}; -use unused_port::unused_tcp4_port; -use url::Url; -use watch::{ - client::WatchHttpClient, - config::Config, - database::{self, Config as DatabaseConfig, PgPool, WatchSlot}, - server::{start_server, Config as ServerConfig}, - updater::{handler::*, run_updater, Config as UpdaterConfig, WatchSpec}, -}; - -#[derive(Debug)] -pub struct Postgres(HashMap); - -impl Default for Postgres { - fn default() -> Self { - let mut env_vars = HashMap::new(); - env_vars.insert("POSTGRES_DB".to_owned(), "postgres".to_owned()); - env_vars.insert("POSTGRES_HOST_AUTH_METHOD".into(), "trust".into()); - - Self(env_vars) - } -} - -impl Image for Postgres { - type Args = (); - - fn name(&self) -> String { - "postgres".to_owned() - } - - fn tag(&self) -> String { - "11-alpine".to_owned() - } - - fn ready_conditions(&self) -> Vec { - vec![WaitFor::message_on_stderr( - "database system is ready to accept connections", - )] - } - - fn env_vars(&self) -> Box + '_> { - Box::new(self.0.iter()) - } -} - -type E = MainnetEthSpec; - -const VALIDATOR_COUNT: usize = 32; -const SLOTS_PER_EPOCH: u64 = 32; -const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5); - -/// Set this environment variable to use a different hostname for connecting to -/// the database. Can be set to `host.docker.internal` for docker-in-docker -/// setups. -const WATCH_HOST_ENV_VARIABLE: &str = "WATCH_HOST"; - -fn build_test_config(config: &DatabaseConfig) -> PostgresConfig { - let mut postgres_config = PostgresConfig::new(); - postgres_config - .user(&config.user) - .password(&config.password) - .dbname(&config.default_dbname) - .host(&config.host) - .port(config.port) - .connect_timeout(Duration::from_millis(config.connect_timeout_millis)); - postgres_config -} - -async fn connect(config: &DatabaseConfig) -> (Client, JoinHandle<()>) { - let db_config = build_test_config(config); - let (client, conn) = db_config - .connect(NoTls) - .await - .expect("Could not connect to db"); - let connection = runtime::Handle::current().spawn(async move { - if let Err(e) = conn.await { - error!("Connection error {:?}", e); - } - }); - - (client, connection) -} - -pub async fn create_test_database(config: &DatabaseConfig) { - let (db, _) = connect(config).await; - - db.execute(&format!("CREATE DATABASE {};", config.dbname), &[]) - .await - .expect("Database creation failed"); -} - -pub fn get_host_from_env() -> String { - env::var(WATCH_HOST_ENV_VARIABLE).unwrap_or_else(|_| "localhost".to_string()) -} - -struct TesterBuilder { - pub harness: BeaconChainHarness>, - pub config: Config, - _bn_network_rx: NetworkReceivers, -} - -impl TesterBuilder { - pub async fn new() -> TesterBuilder { - let harness = BeaconChainHarness::builder(E::default()) - .default_spec() - .chain_config(ChainConfig { - reconstruct_historic_states: true, - ..ChainConfig::default() - }) - .logger(test_logger()) - .deterministic_keypairs(VALIDATOR_COUNT) - .fresh_ephemeral_store() - .build(); - - /* - * Spawn a Beacon Node HTTP API. - */ - let ApiServer { - server, - listening_socket: bn_api_listening_socket, - network_rx: _bn_network_rx, - .. - } = create_api_server( - harness.chain.clone(), - &harness.runtime, - harness.logger().clone(), - ) - .await; - tokio::spawn(server); - - /* - * Create a watch configuration - */ - let database_port = unused_tcp4_port().expect("Unable to find unused port."); - let server_port = 0; - let config = Config { - database: DatabaseConfig { - dbname: random_dbname(), - port: database_port, - host: get_host_from_env(), - ..Default::default() - }, - server: ServerConfig { - listen_port: server_port, - ..Default::default() - }, - updater: UpdaterConfig { - beacon_node_url: format!( - "http://{}:{}", - bn_api_listening_socket.ip(), - bn_api_listening_socket.port() - ), - ..Default::default() - }, - ..Default::default() - }; - - Self { - harness, - config, - _bn_network_rx, - } - } - pub async fn build(self, pool: PgPool) -> Tester { - /* - * Spawn a Watch HTTP API. - */ - let (addr, watch_server) = start_server(&self.config, SLOTS_PER_EPOCH, pool).unwrap(); - tokio::spawn(watch_server); - - /* - * Create a HTTP client to talk to the watch HTTP API. - */ - let client = WatchHttpClient { - client: reqwest::Client::new(), - server: Url::parse(&format!("http://{}:{}", addr.ip(), addr.port())).unwrap(), - }; - - /* - * Create a HTTP client to talk to the Beacon Node API. - */ - let beacon_node_url = SensitiveUrl::parse(&self.config.updater.beacon_node_url).unwrap(); - let bn = BeaconNodeHttpClient::new(beacon_node_url, Timeouts::set_all(DEFAULT_TIMEOUT)); - let spec = WatchSpec::mainnet("mainnet".to_string()); - - /* - * Build update service - */ - let updater = UpdateHandler::new(bn, spec, self.config.clone()) - .await - .unwrap(); - - Tester { - harness: self.harness, - client, - config: self.config, - updater, - _bn_network_rx: self._bn_network_rx, - } - } - async fn initialize_database(&self) -> PgPool { - create_test_database(&self.config.database).await; - database::utils::run_migrations(&self.config.database); - database::build_connection_pool(&self.config.database) - .expect("Could not build connection pool") - } -} - -struct Tester { - pub harness: BeaconChainHarness>, - pub client: WatchHttpClient, - pub config: Config, - pub updater: UpdateHandler, - _bn_network_rx: NetworkReceivers, -} - -impl Tester { - /// Extend the chain on the beacon chain harness. Do not update the beacon watch database. - pub async fn extend_chain(&mut self, num_blocks: u64) -> &mut Self { - self.harness.advance_slot(); - self.harness - .extend_chain( - num_blocks as usize, - BlockStrategy::OnCanonicalHead, - AttestationStrategy::AllValidators, - ) - .await; - self - } - - // Advance the slot clock without a block. This results in a skipped slot. - pub fn skip_slot(&mut self) -> &mut Self { - self.harness.advance_slot(); - self - } - - // Perform a single slot re-org. - pub async fn reorg_chain(&mut self) -> &mut Self { - let previous_slot = self.harness.get_current_slot(); - self.harness.advance_slot(); - let first_slot = self.harness.get_current_slot(); - self.harness - .extend_chain( - 1, - BlockStrategy::ForkCanonicalChainAt { - previous_slot, - first_slot, - }, - AttestationStrategy::AllValidators, - ) - .await; - self - } - - /// Run the watch updater service. - pub async fn run_update_service(&mut self, num_runs: usize) -> &mut Self { - for _ in 0..num_runs { - run_updater(self.config.clone()).await.unwrap(); - } - self - } - - pub async fn perform_head_update(&mut self) -> &mut Self { - self.updater.perform_head_update().await.unwrap(); - self - } - - pub async fn perform_backfill(&mut self) -> &mut Self { - self.updater.backfill_canonical_slots().await.unwrap(); - self - } - - pub async fn update_unknown_blocks(&mut self) -> &mut Self { - self.updater.update_unknown_blocks().await.unwrap(); - self - } - - pub async fn update_validator_set(&mut self) -> &mut Self { - self.updater.update_validator_set().await.unwrap(); - self - } - - pub async fn fill_suboptimal_attestations(&mut self) -> &mut Self { - self.updater.fill_suboptimal_attestations().await.unwrap(); - - self - } - - pub async fn backfill_suboptimal_attestations(&mut self) -> &mut Self { - self.updater - .backfill_suboptimal_attestations() - .await - .unwrap(); - - self - } - - pub async fn fill_block_rewards(&mut self) -> &mut Self { - self.updater.fill_block_rewards().await.unwrap(); - - self - } - - pub async fn backfill_block_rewards(&mut self) -> &mut Self { - self.updater.backfill_block_rewards().await.unwrap(); - - self - } - - pub async fn fill_block_packing(&mut self) -> &mut Self { - self.updater.fill_block_packing().await.unwrap(); - - self - } - - pub async fn backfill_block_packing(&mut self) -> &mut Self { - self.updater.backfill_block_packing().await.unwrap(); - - self - } - - pub async fn assert_canonical_slots_empty(&mut self) -> &mut Self { - let lowest_slot = self - .client - .get_lowest_canonical_slot() - .await - .unwrap() - .map(|slot| slot.slot.as_slot()); - - assert_eq!(lowest_slot, None); - - self - } - - pub async fn assert_lowest_canonical_slot(&mut self, expected: u64) -> &mut Self { - let slot = self - .client - .get_lowest_canonical_slot() - .await - .unwrap() - .unwrap() - .slot - .as_slot(); - - assert_eq!(slot, Slot::new(expected)); - - self - } - - pub async fn assert_highest_canonical_slot(&mut self, expected: u64) -> &mut Self { - let slot = self - .client - .get_highest_canonical_slot() - .await - .unwrap() - .unwrap() - .slot - .as_slot(); - - assert_eq!(slot, Slot::new(expected)); - - self - } - - pub async fn assert_canonical_slots_not_empty(&mut self) -> &mut Self { - self.client - .get_lowest_canonical_slot() - .await - .unwrap() - .unwrap(); - - self - } - - pub async fn assert_slot_is_skipped(&mut self, slot: u64) -> &mut Self { - assert!(self - .client - .get_beacon_blocks(BlockId::Slot(Slot::new(slot))) - .await - .unwrap() - .is_none()); - self - } - - pub async fn assert_all_validators_exist(&mut self) -> &mut Self { - assert_eq!( - self.client - .get_all_validators() - .await - .unwrap() - .unwrap() - .len(), - VALIDATOR_COUNT - ); - self - } - - pub async fn assert_lowest_block_has_proposer_info(&mut self) -> &mut Self { - let mut block = self - .client - .get_lowest_beacon_block() - .await - .unwrap() - .unwrap(); - - if block.slot.as_slot() == 0 { - block = self - .client - .get_next_beacon_block(block.root.as_hash()) - .await - .unwrap() - .unwrap() - } - - self.client - .get_proposer_info(BlockId::Root(block.root.as_hash())) - .await - .unwrap() - .unwrap(); - - self - } - - pub async fn assert_highest_block_has_proposer_info(&mut self) -> &mut Self { - let block = self - .client - .get_highest_beacon_block() - .await - .unwrap() - .unwrap(); - - self.client - .get_proposer_info(BlockId::Root(block.root.as_hash())) - .await - .unwrap() - .unwrap(); - - self - } - - pub async fn assert_lowest_block_has_block_rewards(&mut self) -> &mut Self { - let mut block = self - .client - .get_lowest_beacon_block() - .await - .unwrap() - .unwrap(); - - if block.slot.as_slot() == 0 { - block = self - .client - .get_next_beacon_block(block.root.as_hash()) - .await - .unwrap() - .unwrap() - } - - self.client - .get_block_reward(BlockId::Root(block.root.as_hash())) - .await - .unwrap() - .unwrap(); - - self - } - - pub async fn assert_highest_block_has_block_rewards(&mut self) -> &mut Self { - let block = self - .client - .get_highest_beacon_block() - .await - .unwrap() - .unwrap(); - - self.client - .get_block_reward(BlockId::Root(block.root.as_hash())) - .await - .unwrap() - .unwrap(); - - self - } - - pub async fn assert_lowest_block_has_block_packing(&mut self) -> &mut Self { - let mut block = self - .client - .get_lowest_beacon_block() - .await - .unwrap() - .unwrap(); - - while block.slot.as_slot() <= SLOTS_PER_EPOCH { - block = self - .client - .get_next_beacon_block(block.root.as_hash()) - .await - .unwrap() - .unwrap() - } - - self.client - .get_block_packing(BlockId::Root(block.root.as_hash())) - .await - .unwrap() - .unwrap(); - - self - } - - pub async fn assert_highest_block_has_block_packing(&mut self) -> &mut Self { - let block = self - .client - .get_highest_beacon_block() - .await - .unwrap() - .unwrap(); - - self.client - .get_block_packing(BlockId::Root(block.root.as_hash())) - .await - .unwrap() - .unwrap(); - - self - } - - /// Check that the canonical chain in watch matches that of the harness. Also check that all - /// canonical blocks can be retrieved. - pub async fn assert_canonical_chain_consistent(&mut self, last_slot: u64) -> &mut Self { - let head_root = self.harness.chain.head_beacon_block_root(); - let mut chain: Vec<(Hash256, Slot)> = self - .harness - .chain - .rev_iter_block_roots_from(head_root) - .unwrap() - .map(Result::unwrap) - .collect(); - - // `chain` contains skip slots, but the `watch` API will not return blocks that do not - // exist. - // We need to filter them out. - chain.reverse(); - chain.dedup_by(|(hash1, _), (hash2, _)| hash1 == hash2); - - // Remove any slots below `last_slot` since it is known that the database has not - // backfilled past it. - chain.retain(|(_, slot)| slot.as_u64() >= last_slot); - - for (root, slot) in &chain { - let block = self - .client - .get_beacon_blocks(BlockId::Root(*root)) - .await - .unwrap() - .unwrap(); - assert_eq!(block.slot.as_slot(), *slot); - } - - self - } - - /// Check that every block in the `beacon_blocks` table has corresponding entries in the - /// `proposer_info`, `block_rewards` and `block_packing` tables. - pub async fn assert_all_blocks_have_metadata(&mut self) -> &mut Self { - let pool = database::build_connection_pool(&self.config.database).unwrap(); - - let mut conn = database::get_connection(&pool).unwrap(); - let highest_block_slot = database::get_highest_beacon_block(&mut conn) - .unwrap() - .unwrap() - .slot - .as_slot(); - let lowest_block_slot = database::get_lowest_beacon_block(&mut conn) - .unwrap() - .unwrap() - .slot - .as_slot(); - for slot in lowest_block_slot.as_u64()..=highest_block_slot.as_u64() { - let canonical_slot = database::get_canonical_slot(&mut conn, WatchSlot::new(slot)) - .unwrap() - .unwrap(); - if !canonical_slot.skipped { - database::get_block_rewards_by_slot(&mut conn, WatchSlot::new(slot)) - .unwrap() - .unwrap(); - database::get_proposer_info_by_slot(&mut conn, WatchSlot::new(slot)) - .unwrap() - .unwrap(); - database::get_block_packing_by_slot(&mut conn, WatchSlot::new(slot)) - .unwrap() - .unwrap(); - } - } - - self - } -} - -pub fn random_dbname() -> String { - let mut s: String = thread_rng() - .sample_iter(&Alphanumeric) - .take(8) - .map(char::from) - .collect(); - // Postgres gets weird about capitals in database names. - s.make_ascii_lowercase(); - format!("test_{}", s) -} - -#[cfg(unix)] -#[tokio::test] -async fn short_chain() { - let builder = TesterBuilder::new().await; - - let docker = Cli::default(); - let image = RunnableImage::from(Postgres::default()) - .with_mapped_port((builder.config.database.port, 5432)); - let _node = docker.run(image); - - let pool = builder.initialize_database().await; - let mut tester = builder.build(pool).await; - - tester - .extend_chain(16) - .await - .assert_canonical_slots_empty() - .await - .run_update_service(1) - .await - .assert_all_validators_exist() - .await - .assert_canonical_slots_not_empty() - .await - .assert_canonical_chain_consistent(0) - .await; -} - -#[cfg(unix)] -#[tokio::test] -async fn short_chain_sync_starts_on_skip_slot() { - let builder = TesterBuilder::new().await; - - let docker = Cli::default(); - let image = RunnableImage::from(Postgres::default()) - .with_mapped_port((builder.config.database.port, 5432)); - let _node = docker.run(image); - - let pool = builder.initialize_database().await; - let mut tester = builder.build(pool).await; - - tester - .skip_slot() - .skip_slot() - .extend_chain(6) - .await - .skip_slot() - .extend_chain(6) - .await - .skip_slot() - .assert_canonical_slots_empty() - .await - .run_update_service(1) - .await - .assert_all_validators_exist() - .await - .assert_canonical_slots_not_empty() - .await - .assert_canonical_chain_consistent(0) - .await - .assert_lowest_block_has_block_rewards() - .await - .assert_highest_block_has_block_rewards() - .await; -} - -#[cfg(unix)] -#[tokio::test] -async fn short_chain_with_skip_slot() { - let builder = TesterBuilder::new().await; - - let docker = Cli::default(); - let image = RunnableImage::from(Postgres::default()) - .with_mapped_port((builder.config.database.port, 5432)); - let _node = docker.run(image); - - let pool = builder.initialize_database().await; - let mut tester = builder.build(pool).await; - - tester - .extend_chain(5) - .await - .assert_canonical_slots_empty() - .await - .run_update_service(1) - .await - .assert_all_validators_exist() - .await - .assert_canonical_slots_not_empty() - .await - .assert_highest_canonical_slot(5) - .await - .assert_lowest_canonical_slot(0) - .await - .assert_canonical_chain_consistent(0) - .await - .skip_slot() - .extend_chain(1) - .await - .run_update_service(1) - .await - .assert_all_validators_exist() - .await - .assert_highest_canonical_slot(7) - .await - .assert_slot_is_skipped(6) - .await - .assert_canonical_chain_consistent(0) - .await; -} - -#[cfg(unix)] -#[tokio::test] -async fn short_chain_with_reorg() { - let builder = TesterBuilder::new().await; - - let docker = Cli::default(); - let image = RunnableImage::from(Postgres::default()) - .with_mapped_port((builder.config.database.port, 5432)); - let _node = docker.run(image); - - let pool = builder.initialize_database().await; - let mut tester = builder.build(pool).await; - - tester - .extend_chain(5) - .await - .assert_canonical_slots_empty() - .await - .run_update_service(1) - .await - .assert_all_validators_exist() - .await - .assert_canonical_slots_not_empty() - .await - .assert_highest_canonical_slot(5) - .await - .assert_lowest_canonical_slot(0) - .await - .assert_canonical_chain_consistent(0) - .await - .skip_slot() - .reorg_chain() - .await - .extend_chain(1) - .await - .run_update_service(1) - .await - .assert_all_validators_exist() - .await - .assert_highest_canonical_slot(8) - .await - .assert_slot_is_skipped(6) - .await - .assert_canonical_chain_consistent(0) - .await; -} - -#[cfg(unix)] -#[tokio::test] -async fn chain_grows() { - let builder = TesterBuilder::new().await; - - let docker = Cli::default(); - let image = RunnableImage::from(Postgres::default()) - .with_mapped_port((builder.config.database.port, 5432)); - let _node = docker.run(image); - - let pool = builder.initialize_database().await; - let mut tester = builder.build(pool).await; - - // Apply four blocks to the chain. - tester - .extend_chain(4) - .await - .perform_head_update() - .await - // Head update should insert the head block. - .assert_highest_canonical_slot(4) - .await - // And also backfill to the epoch boundary. - .assert_lowest_canonical_slot(0) - .await - // Fill back to genesis. - .perform_backfill() - .await - // All blocks should be present. - .assert_lowest_canonical_slot(0) - .await - .assert_highest_canonical_slot(4) - .await - // Apply one block to the chain. - .extend_chain(1) - .await - .perform_head_update() - .await - // All blocks should be present. - .assert_lowest_canonical_slot(0) - .await - .assert_highest_canonical_slot(5) - .await - // Apply two blocks to the chain. - .extend_chain(2) - .await - // Update the head. - .perform_head_update() - .await - // All blocks should be present. - .assert_lowest_canonical_slot(0) - .await - .assert_highest_canonical_slot(7) - .await - .update_validator_set() - .await - // Insert all blocks. - .update_unknown_blocks() - .await - // Check the chain is consistent - .assert_canonical_chain_consistent(0) - .await; -} - -#[cfg(unix)] -#[tokio::test] -#[allow(clippy::large_stack_frames)] -async fn chain_grows_with_metadata() { - let builder = TesterBuilder::new().await; - - let docker = Cli::default(); - let image = RunnableImage::from(Postgres::default()) - .with_mapped_port((builder.config.database.port, 5432)); - let _node = docker.run(image); - - let pool = builder.initialize_database().await; - let mut tester = builder.build(pool).await; - - tester - // Apply four blocks to the chain. - .extend_chain(4) - .await - .perform_head_update() - .await - // Head update should insert the head block. - .assert_highest_canonical_slot(4) - .await - // And also backfill to the epoch boundary. - .assert_lowest_canonical_slot(0) - .await - // Fill back to genesis. - .perform_backfill() - .await - // Insert all validators - .update_validator_set() - .await - // Insert all blocks. - .update_unknown_blocks() - .await - // All validators should be present. - .assert_all_validators_exist() - .await - // Check the chain is consistent - .assert_canonical_chain_consistent(0) - .await - // Get other chain data. - // Backfill before forward fill to ensure order is arbitrary. - .backfill_block_rewards() - .await - .fill_block_rewards() - .await - // All blocks should be present. - .assert_lowest_canonical_slot(0) - .await - .assert_highest_canonical_slot(4) - .await - // All rewards should be present. - .assert_lowest_block_has_block_rewards() - .await - .assert_highest_block_has_block_rewards() - .await - // All proposers should be present. - .assert_lowest_block_has_proposer_info() - .await - .assert_highest_block_has_proposer_info() - .await - // Apply one block to the chain. - .extend_chain(1) - .await - .perform_head_update() - .await - // All blocks should be present. - .assert_lowest_canonical_slot(0) - .await - .assert_highest_canonical_slot(5) - .await - // Apply two blocks to the chain. - .extend_chain(2) - .await - // Update the head. - .perform_head_update() - .await - // All blocks should be present. - .assert_lowest_canonical_slot(0) - .await - .assert_highest_canonical_slot(7) - .await - .update_validator_set() - .await - // Insert all blocks. - .update_unknown_blocks() - .await - // Check the chain is consistent - .assert_canonical_chain_consistent(0) - .await - // Get other chain data. - .fill_block_rewards() - .await - .backfill_block_rewards() - .await - // All rewards should be present. - .assert_lowest_block_has_block_rewards() - .await - .assert_highest_block_has_block_rewards() - .await - // All proposers should be present. - .assert_lowest_block_has_proposer_info() - .await - .assert_highest_block_has_proposer_info() - .await; -} - -#[cfg(unix)] -#[tokio::test] -#[allow(clippy::large_stack_frames)] -async fn chain_grows_with_metadata_and_multiple_skip_slots() { - let builder = TesterBuilder::new().await; - - let docker = Cli::default(); - let image = RunnableImage::from(Postgres::default()) - .with_mapped_port((builder.config.database.port, 5432)); - let _node = docker.run(image); - - let pool = builder.initialize_database().await; - let mut tester = builder.build(pool).await; - - // Apply four blocks to the chain. - tester - .extend_chain(4) - .await - .perform_head_update() - .await - // Head update should insert the head block. - .assert_highest_canonical_slot(4) - // And also backfill to the epoch boundary. - .await - .assert_lowest_canonical_slot(0) - .await - // Fill back to genesis. - .perform_backfill() - .await - // Insert all validators - .update_validator_set() - .await - // Insert all blocks. - .update_unknown_blocks() - .await - // All validators should be present. - .assert_all_validators_exist() - .await - // Check the chain is consistent. - .assert_canonical_chain_consistent(0) - .await - // Get other chain data. - .fill_block_rewards() - .await - .backfill_block_rewards() - .await - // All blocks should be present. - .assert_lowest_canonical_slot(0) - .await - .assert_highest_canonical_slot(4) - .await - // All rewards should be present. - .assert_lowest_block_has_block_rewards() - .await - .assert_highest_block_has_block_rewards() - .await - // All proposers should be present. - .assert_lowest_block_has_proposer_info() - .await - .assert_highest_block_has_proposer_info() - .await - // Add multiple skip slots. - .skip_slot() - .skip_slot() - .skip_slot() - // Apply one block to the chain. - .extend_chain(1) - .await - .perform_head_update() - .await - // All blocks should be present. - .assert_lowest_canonical_slot(0) - .await - .assert_highest_canonical_slot(8) - .await - // Apply two blocks to the chain. - .extend_chain(2) - .await - // Update the head. - .perform_head_update() - .await - // All validators should be present. - .assert_all_validators_exist() - .await - // All blocks should be present. - .assert_lowest_canonical_slot(0) - .await - .assert_highest_canonical_slot(10) - .await - .update_validator_set() - .await - // Insert all blocks. - .update_unknown_blocks() - .await - // Check the chain is consistent - .assert_canonical_chain_consistent(0) - .await - // Get other chain data. - // Backfill before forward fill to ensure order is arbitrary. - .backfill_block_rewards() - .await - .fill_block_rewards() - .await - // All rewards should be present. - .assert_lowest_block_has_block_rewards() - .await - .assert_highest_block_has_block_rewards() - .await - // All proposers should be present. - .assert_lowest_block_has_proposer_info() - .await - .assert_highest_block_has_proposer_info() - .await; -} - -#[cfg(unix)] -#[tokio::test] -async fn chain_grows_to_second_epoch() { - let builder = TesterBuilder::new().await; - - let docker = Cli::default(); - let image = RunnableImage::from(Postgres::default()) - .with_mapped_port((builder.config.database.port, 5432)); - let _node = docker.run(image); - - let pool = builder.initialize_database().await; - let mut tester = builder.build(pool).await; - // Apply 40 blocks to the chain. - tester - .extend_chain(40) - .await - .perform_head_update() - .await - // Head update should insert the head block. - .assert_highest_canonical_slot(40) - .await - // And also backfill to the epoch boundary. - .assert_lowest_canonical_slot(32) - .await - // Fill back to genesis. - .perform_backfill() - .await - // Insert all validators - .update_validator_set() - .await - // Insert all blocks. - .update_unknown_blocks() - .await - // All validators should be present. - .assert_all_validators_exist() - .await - // Check the chain is consistent. - .assert_canonical_chain_consistent(0) - .await - // Get block packings. - .fill_block_packing() - .await - .backfill_block_packing() - .await - // All blocks should be present. - .assert_lowest_canonical_slot(0) - .await - .assert_highest_canonical_slot(40) - .await - // All packings should be present. - .assert_lowest_block_has_block_packing() - .await - .assert_highest_block_has_block_packing() - .await - // Skip a slot - .skip_slot() - // Apply two blocks to the chain. - .extend_chain(2) - .await - // Update the head. - .perform_head_update() - .await - // All blocks should be present. - .assert_lowest_canonical_slot(0) - .await - .assert_highest_canonical_slot(43) - .await - .update_validator_set() - .await - // Insert all blocks. - .update_unknown_blocks() - .await - // Update new block_packing - // Backfill before forward fill to ensure order is arbitrary - .backfill_block_packing() - .await - .fill_block_packing() - .await - // All packings should be present. - .assert_lowest_block_has_block_packing() - .await - .assert_highest_block_has_block_packing() - .await - // Check the chain is consistent - .assert_canonical_chain_consistent(0) - .await; -} - -#[cfg(unix)] -#[tokio::test] -async fn large_chain() { - let builder = TesterBuilder::new().await; - - let docker = Cli::default(); - let image = RunnableImage::from(Postgres::default()) - .with_mapped_port((builder.config.database.port, 5432)); - let _node = docker.run(image); - - let pool = builder.initialize_database().await; - let mut tester = builder.build(pool).await; - // Apply 40 blocks to the chain. - tester - .extend_chain(400) - .await - .perform_head_update() - .await - // Head update should insert the head block. - .assert_highest_canonical_slot(400) - .await - // And also backfill to the epoch boundary. - .assert_lowest_canonical_slot(384) - .await - // Backfill 2 epochs as per default config. - .perform_backfill() - .await - // Insert all validators - .update_validator_set() - .await - // Insert all blocks. - .update_unknown_blocks() - .await - // All validators should be present. - .assert_all_validators_exist() - .await - // Check the chain is consistent. - .assert_canonical_chain_consistent(384) - .await - // Get block rewards and proposer info. - .fill_block_rewards() - .await - .backfill_block_rewards() - .await - // Get block packings. - .fill_block_packing() - .await - .backfill_block_packing() - .await - // Should have backfilled 2 more epochs. - .assert_lowest_canonical_slot(320) - .await - .assert_highest_canonical_slot(400) - .await - // All rewards should be present. - .assert_lowest_block_has_block_rewards() - .await - .assert_highest_block_has_block_rewards() - .await - // All proposers should be present. - .assert_lowest_block_has_proposer_info() - .await - .assert_highest_block_has_proposer_info() - .await - // All packings should be present. - .assert_lowest_block_has_block_packing() - .await - .assert_highest_block_has_block_packing() - .await - // Skip a slot - .skip_slot() - // Apply two blocks to the chain. - .extend_chain(2) - .await - // Update the head. - .perform_head_update() - .await - .perform_backfill() - .await - // Should have backfilled 2 more epochs - .assert_lowest_canonical_slot(256) - .await - .assert_highest_canonical_slot(403) - .await - // Update validators - .update_validator_set() - .await - // Insert all blocks. - .update_unknown_blocks() - .await - // All validators should be present. - .assert_all_validators_exist() - .await - // Get suboptimal attestations. - .fill_suboptimal_attestations() - .await - .backfill_suboptimal_attestations() - .await - // Get block rewards and proposer info. - .fill_block_rewards() - .await - .backfill_block_rewards() - .await - // Get block packing. - // Backfill before forward fill to ensure order is arbitrary. - .backfill_block_packing() - .await - .fill_block_packing() - .await - // All rewards should be present. - .assert_lowest_block_has_block_rewards() - .await - .assert_highest_block_has_block_rewards() - .await - // All proposers should be present. - .assert_lowest_block_has_proposer_info() - .await - .assert_highest_block_has_proposer_info() - .await - // All packings should be present. - .assert_lowest_block_has_block_packing() - .await - .assert_highest_block_has_block_packing() - .await - // Check the chain is consistent. - .assert_canonical_chain_consistent(256) - .await - // Check every block has rewards, proposer info and packing statistics. - .assert_all_blocks_have_metadata() - .await; -} diff --git a/wordlist.txt b/wordlist.txt index bb8b46b525e..9feb07b67bc 100644 --- a/wordlist.txt +++ b/wordlist.txt @@ -12,6 +12,7 @@ BN BNs BTC BTEC +Btrfs Casper CentOS Chiado @@ -38,6 +39,7 @@ Exercism Extractable FFG Geth +GiB Gitcoin Gnosis Goerli @@ -64,6 +66,7 @@ Nethermind NodeJS NullLogger PathBuf +Pectra PowerShell PPA Pre @@ -91,6 +94,7 @@ TLS TODOs UDP UI +Uncached UPnP USD UX @@ -100,6 +104,7 @@ VCs VPN Withdrawable WSL +XFS YAML aarch anonymize @@ -196,6 +201,7 @@ redb reimport resync roadmap +routable rustfmt rustup schemas @@ -220,6 +226,7 @@ tweakers ui unadvanced unaggregated +uncached unencrypted unfinalized untrusted @@ -230,6 +237,7 @@ validators validator's vc virt +walkthrough webapp withdrawable yaml