From 8a3eb4df9c0e15a99b88fe3ffa7cfc2ad9a3ff57 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Mon, 15 May 2023 07:22:02 +0000 Subject: [PATCH 01/63] Replace ganache-cli with anvil (#3555) ## Issue Addressed N/A ## Proposed Changes Replace ganache-cli with anvil https://github.com/foundry-rs/foundry/blob/master/anvil/README.md We can lose all js dependencies in CI as a consequence. ## Additional info Also changes the ethers-rs version used in the execution layer (for the transaction reconstruction) to a newer one. This was necessary to get use the ethers utils for anvil. The fixed execution engine integration tests should catch any potential issues with the payload reconstruction after #3592 Co-authored-by: Michael Sproul --- .github/workflows/local-testnet.yml | 4 +- .github/workflows/test-suite.yml | 32 +-- Cargo.lock | 235 +++++++++++-------- beacon_node/eth1/Cargo.toml | 1 - beacon_node/eth1/tests/test.rs | 101 ++++---- beacon_node/genesis/tests/tests.rs | 17 +- book/src/setup.md | 4 +- lcli/Cargo.toml | 1 - lcli/src/deploy_deposit_contract.rs | 9 +- lighthouse/tests/account_manager.rs | 4 - scripts/local_testnet/README.md | 6 +- scripts/local_testnet/anvil_test_node.sh | 14 ++ scripts/local_testnet/ganache_test_node.sh | 14 -- scripts/local_testnet/start_local_testnet.sh | 6 +- scripts/local_testnet/vars.env | 2 +- scripts/tests/doppelganger_protection.sh | 14 +- scripts/tests/vars.env | 2 +- testing/eth1_test_rig/Cargo.toml | 5 +- testing/eth1_test_rig/src/anvil.rs | 101 ++++++++ testing/eth1_test_rig/src/ganache.rs | 193 --------------- testing/eth1_test_rig/src/lib.rs | 186 ++++++++------- testing/simulator/src/cli.rs | 2 +- testing/simulator/src/eth1_sim.rs | 16 +- testing/simulator/src/main.rs | 2 +- 24 files changed, 465 insertions(+), 506 deletions(-) create mode 100755 scripts/local_testnet/anvil_test_node.sh delete mode 100755 scripts/local_testnet/ganache_test_node.sh create mode 100644 testing/eth1_test_rig/src/anvil.rs delete mode 100644 testing/eth1_test_rig/src/ganache.rs diff --git a/.github/workflows/local-testnet.yml b/.github/workflows/local-testnet.yml index 1ca1006c1f..d4982ae194 100644 --- a/.github/workflows/local-testnet.yml +++ b/.github/workflows/local-testnet.yml @@ -25,8 +25,8 @@ jobs: uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install ganache - run: npm install ganache@latest --global + - name: Install anvil + run: cargo install --git https://github.com/foundry-rs/foundry --locked anvil # https://github.com/actions/cache/blob/main/examples.md#rust---cargo - uses: actions/cache@v3 diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 27c91f2262..4fa61e6aaa 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -58,8 +58,8 @@ jobs: uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install ganache - run: sudo npm install -g ganache + - name: Install anvil + run: cargo install --git https://github.com/foundry-rs/foundry --locked anvil - name: Run tests in release run: make test-release release-tests-windows: @@ -78,8 +78,8 @@ jobs: run: | choco install python protoc visualstudio2019-workload-vctools -y npm config set msvs_version 2019 - - name: Install ganache - run: npm install -g ganache --loglevel verbose + - name: Install anvil + run: cargo install --git https://github.com/foundry-rs/foundry --locked anvil - name: Install make run: choco install -y make - uses: KyleMayes/install-llvm-action@v1 @@ -140,8 +140,8 @@ jobs: uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install ganache - run: sudo npm install -g ganache + - name: Install anvil + run: cargo install --git https://github.com/foundry-rs/foundry --locked anvil - name: Run tests in debug run: make test-debug state-transition-vectors-ubuntu: @@ -196,8 +196,8 @@ jobs: uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install ganache - run: sudo npm install -g ganache + - name: Install anvil + run: cargo install --git https://github.com/foundry-rs/foundry --locked anvil - name: Run the beacon chain sim that starts from an eth1 contract run: cargo run --release --bin simulator eth1-sim merge-transition-ubuntu: @@ -212,8 +212,8 @@ jobs: uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install ganache - run: sudo npm install -g ganache + - name: Install anvil + run: cargo install --git https://github.com/foundry-rs/foundry --locked anvil - name: Run the beacon chain sim and go through the merge transition run: cargo run --release --bin simulator eth1-sim --post-merge no-eth1-simulator-ubuntu: @@ -228,8 +228,8 @@ jobs: uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install ganache - run: sudo npm install -g ganache + - name: Install anvil + run: cargo install --git https://github.com/foundry-rs/foundry --locked anvil - name: Run the beacon chain sim without an eth1 connection run: cargo run --release --bin simulator no-eth1-sim syncing-simulator-ubuntu: @@ -244,8 +244,8 @@ jobs: uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install ganache - run: sudo npm install -g ganache + - name: Install anvil + run: cargo install --git https://github.com/foundry-rs/foundry --locked anvil - name: Run the syncing simulator run: cargo run --release --bin simulator syncing-sim doppelganger-protection-test: @@ -260,8 +260,8 @@ jobs: uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install ganache - run: sudo npm install -g ganache + - name: Install anvil + run: cargo install --git https://github.com/foundry-rs/foundry --locked anvil - name: Install lighthouse and lcli run: | make diff --git a/Cargo.lock b/Cargo.lock index 52e15630d2..bd70ad0201 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "account_manager" version = "0.3.5" @@ -934,6 +944,38 @@ dependencies = [ "tree_hash", ] +[[package]] +name = "camino" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c530edf18f37068ac2d977409ed5cd50d53d73bc653c7647b48eb78976ac9ae2" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a1ec454bc3eead8719cb56e15dbbfecdbc14e4b3a3ae4936cc6e31f5fc0d07" +dependencies = [ + "camino", + "cargo-platform", + "semver 1.0.17", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "cast" version = "0.3.0" @@ -1429,9 +1471,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.0.0-rc.2" +version = "4.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d928d978dbec61a1167414f5ec534f24bea0d7a0d24dd9b6233d3d8223e585" +checksum = "8d4ba9852b42210c7538b75484f9daa0655e9a3ac04f693747bb0f02cf3cfe16" dependencies = [ "cfg-if", "fiat-crypto", @@ -1911,6 +1953,12 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65d09067bfacaa79114679b279d7f5885b53295b1e2cfb4e79c8e4bd3d633169" +[[package]] +name = "dunce" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bd4b30a6560bbd9b4620f4de34c3f14f60848e58a9b7216801afcb4c7b31c3c" + [[package]] name = "ecdsa" version = "0.14.8" @@ -2170,7 +2218,6 @@ dependencies = [ "tokio", "tree_hash", "types", - "web3", ] [[package]] @@ -2178,11 +2225,14 @@ name = "eth1_test_rig" version = "0.2.0" dependencies = [ "deposit_contract", + "ethers-contract", + "ethers-core", + "ethers-providers", + "hex", "serde_json", "tokio", "types", "unused_port", - "web3", ] [[package]] @@ -2469,6 +2519,65 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ethers-contract" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c3c3e119a89f0a9a1e539e7faecea815f74ddcf7c90d0b00d1f524db2fdc9c" +dependencies = [ + "ethers-contract-abigen", + "ethers-contract-derive", + "ethers-core", + "ethers-providers", + "futures-util", + "hex", + "once_cell", + "pin-project", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "ethers-contract-abigen" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d4e5ad46aede34901f71afdb7bb555710ed9613d88d644245c657dc371aa228" +dependencies = [ + "Inflector", + "cfg-if", + "dunce", + "ethers-core", + "eyre", + "getrandom 0.2.8", + "hex", + "proc-macro2", + "quote", + "regex", + "reqwest", + "serde", + "serde_json", + "syn 1.0.109", + "toml", + "url", + "walkdir", +] + +[[package]] +name = "ethers-contract-derive" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f192e8e4cf2b038318aae01e94e7644e0659a76219e94bcd3203df744341d61f" +dependencies = [ + "ethers-contract-abigen", + "ethers-core", + "hex", + "proc-macro2", + "quote", + "serde_json", + "syn 1.0.109", +] + [[package]] name = "ethers-core" version = "1.0.2" @@ -2477,12 +2586,14 @@ checksum = "ade3e9c97727343984e1ceada4fdab11142d2ee3472d2c67027d56b1251d4f15" dependencies = [ "arrayvec", "bytes", + "cargo_metadata", "chrono", "elliptic-curve", "ethabi 18.0.0", "generic-array", "hex", "k256", + "once_cell", "open-fastrlp", "rand 0.8.5", "rlp", @@ -2490,6 +2601,7 @@ dependencies = [ "serde", "serde_json", "strum", + "syn 1.0.109", "thiserror", "tiny-keccak", "unicode-xid", @@ -2621,6 +2733,16 @@ dependencies = [ "futures", ] +[[package]] +name = "eyre" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -3589,6 +3711,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + [[package]] name = "indexmap" version = "1.9.3" @@ -3741,21 +3869,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "jsonrpc-core" -version = "18.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f7f76aef2d054868398427f6c54943cf3d1caa9a7ec7d0c38d69df97a965eb" -dependencies = [ - "futures", - "futures-executor", - "futures-util", - "log", - "serde", - "serde_derive", - "serde_json", -] - [[package]] name = "jsonwebtoken" version = "8.3.0" @@ -3851,7 +3964,6 @@ dependencies = [ "tree_hash", "types", "validator_dir", - "web3", ] [[package]] @@ -6949,24 +7061,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "secp256k1" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c42e6f1735c5f00f51e43e28d6634141f2bcad10931b2609ddd74a86d751260" -dependencies = [ - "secp256k1-sys", -] - -[[package]] -name = "secp256k1-sys" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957da2573cde917463ece3570eab4a0b3f19de6f1646cde62e6fd3868f566036" -dependencies = [ - "cc", -] - [[package]] name = "security-framework" version = "2.8.2" @@ -7004,6 +7098,9 @@ name = "semver" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +dependencies = [ + "serde", +] [[package]] name = "semver-parser" @@ -7485,14 +7582,14 @@ checksum = "5e9f0ab6ef7eb7353d9119c170a436d1bf248eea575ac42d19d12f4e34130831" [[package]] name = "snow" -version = "0.9.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "774d05a3edae07ce6d68ea6984f3c05e9bba8927e3dd591e3b479e5b03213d0d" +checksum = "5ccba027ba85743e09d15c03296797cad56395089b832b48b5a5217880f57733" dependencies = [ "aes-gcm 0.9.4", "blake2", "chacha20poly1305", - "curve25519-dalek 4.0.0-rc.2", + "curve25519-dalek 4.0.0-rc.1", "rand_core 0.6.4", "ring", "rustc_version 0.4.0", @@ -9142,53 +9239,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web3" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f258e254752d210b84fe117b31f1e3cc9cbf04c0d747eb7f8cf7cf5e370f6d" -dependencies = [ - "arrayvec", - "base64 0.13.1", - "bytes", - "derive_more", - "ethabi 16.0.0", - "ethereum-types 0.12.1", - "futures", - "futures-timer", - "headers", - "hex", - "idna 0.2.3", - "jsonrpc-core", - "log", - "once_cell", - "parking_lot 0.12.1", - "pin-project", - "reqwest", - "rlp", - "secp256k1", - "serde", - "serde_json", - "soketto", - "tiny-keccak", - "tokio", - "tokio-util 0.6.10", - "url", - "web3-async-native-tls", -] - -[[package]] -name = "web3-async-native-tls" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f6d8d1636b2627fe63518d5a9b38a569405d9c9bc665c43c9c341de57227ebb" -dependencies = [ - "native-tls", - "thiserror", - "tokio", - "url", -] - [[package]] name = "web3signer_tests" version = "0.1.0" @@ -9338,7 +9388,7 @@ dependencies = [ "tokio", "webpki 0.21.4", "webrtc-util", - "x25519-dalek 2.0.0-rc.2", + "x25519-dalek 2.0.0-pre.1", "x509-parser 0.13.2", ] @@ -9718,13 +9768,12 @@ dependencies = [ [[package]] name = "x25519-dalek" -version = "2.0.0-rc.2" +version = "2.0.0-pre.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabd6e16dd08033932fc3265ad4510cc2eab24656058a6dcb107ffe274abcc95" +checksum = "e5da623d8af10a62342bcbbb230e33e58a63255a58012f8653c578e54bab48df" dependencies = [ - "curve25519-dalek 4.0.0-rc.2", + "curve25519-dalek 3.2.0", "rand_core 0.6.4", - "serde", "zeroize", ] diff --git a/beacon_node/eth1/Cargo.toml b/beacon_node/eth1/Cargo.toml index 1148f063d8..cc982aee08 100644 --- a/beacon_node/eth1/Cargo.toml +++ b/beacon_node/eth1/Cargo.toml @@ -7,7 +7,6 @@ edition = "2021" [dev-dependencies] eth1_test_rig = { path = "../../testing/eth1_test_rig" } serde_yaml = "0.8.13" -web3 = { version = "0.18.0", default-features = false, features = ["http-tls", "signing", "ws-tls-tokio"] } sloggers = { version = "2.1.1", features = ["json"] } environment = { path = "../../lighthouse/environment" } diff --git a/beacon_node/eth1/tests/test.rs b/beacon_node/eth1/tests/test.rs index cd680478cc..505e4a4796 100644 --- a/beacon_node/eth1/tests/test.rs +++ b/beacon_node/eth1/tests/test.rs @@ -2,7 +2,7 @@ use environment::{Environment, EnvironmentBuilder}; use eth1::{Config, Eth1Endpoint, Service}; use eth1::{DepositCache, DEFAULT_CHAIN_ID}; -use eth1_test_rig::GanacheEth1Instance; +use eth1_test_rig::{AnvilEth1Instance, Http, Middleware, Provider}; use execution_layer::http::{deposit_methods::*, HttpJsonRpc, Log}; use merkle_proof::verify_merkle_proof; use sensitive_url::SensitiveUrl; @@ -12,7 +12,6 @@ use std::ops::Range; use std::time::Duration; use tree_hash::TreeHash; use types::{DepositData, EthSpec, Hash256, Keypair, MainnetEthSpec, MinimalEthSpec, Signature}; -use web3::{transports::Http, Web3}; const DEPOSIT_CONTRACT_TREE_DEPTH: usize = 32; @@ -53,7 +52,7 @@ fn random_deposit_data() -> DepositData { /// Blocking operation to get the deposit logs from the `deposit_contract`. async fn blocking_deposit_logs( client: &HttpJsonRpc, - eth1: &GanacheEth1Instance, + eth1: &AnvilEth1Instance, range: Range, ) -> Vec { client @@ -65,7 +64,7 @@ async fn blocking_deposit_logs( /// Blocking operation to get the deposit root from the `deposit_contract`. async fn blocking_deposit_root( client: &HttpJsonRpc, - eth1: &GanacheEth1Instance, + eth1: &AnvilEth1Instance, block_number: u64, ) -> Option { client @@ -77,7 +76,7 @@ async fn blocking_deposit_root( /// Blocking operation to get the deposit count from the `deposit_contract`. async fn blocking_deposit_count( client: &HttpJsonRpc, - eth1: &GanacheEth1Instance, + eth1: &AnvilEth1Instance, block_number: u64, ) -> Option { client @@ -86,16 +85,16 @@ async fn blocking_deposit_count( .expect("should get deposit count") } -async fn get_block_number(web3: &Web3) -> u64 { - web3.eth() - .block_number() +async fn get_block_number(client: &Provider) -> u64 { + client + .get_block_number() .await .map(|v| v.as_u64()) .expect("should get block number") } -async fn new_ganache_instance() -> Result { - GanacheEth1Instance::new(DEFAULT_CHAIN_ID.into()).await +async fn new_anvil_instance() -> Result { + AnvilEth1Instance::new(DEFAULT_CHAIN_ID.into()).await } mod eth1_cache { @@ -108,13 +107,13 @@ mod eth1_cache { let log = null_logger(); for follow_distance in 0..3 { - let eth1 = new_ganache_instance() + let eth1 = new_anvil_instance() .await .expect("should start eth1 environment"); let deposit_contract = ð1.deposit_contract; - let web3 = eth1.web3(); + let anvil_client = eth1.json_rpc_client(); - let initial_block_number = get_block_number(&web3).await; + let initial_block_number = get_block_number(&anvil_client).await; let config = Config { endpoint: Eth1Endpoint::NoAuth( @@ -146,7 +145,7 @@ mod eth1_cache { }; for _ in 0..blocks { - eth1.ganache.evm_mine().await.expect("should mine block"); + eth1.anvil.evm_mine().await.expect("should mine block"); } service @@ -189,11 +188,11 @@ mod eth1_cache { async { let log = null_logger(); - let eth1 = new_ganache_instance() + let eth1 = new_anvil_instance() .await .expect("should start eth1 environment"); let deposit_contract = ð1.deposit_contract; - let web3 = eth1.web3(); + let anvil_client = eth1.json_rpc_client(); let cache_len = 4; @@ -203,7 +202,7 @@ mod eth1_cache { SensitiveUrl::parse(eth1.endpoint().as_str()).unwrap(), ), deposit_contract_address: deposit_contract.address(), - lowest_cached_block_number: get_block_number(&web3).await, + lowest_cached_block_number: get_block_number(&anvil_client).await, follow_distance: 0, block_cache_truncation: Some(cache_len), ..Config::default() @@ -216,7 +215,7 @@ mod eth1_cache { let blocks = cache_len * 2; for _ in 0..blocks { - eth1.ganache.evm_mine().await.expect("should mine block") + eth1.anvil.evm_mine().await.expect("should mine block") } service @@ -244,11 +243,11 @@ mod eth1_cache { async { let log = null_logger(); - let eth1 = new_ganache_instance() + let eth1 = new_anvil_instance() .await .expect("should start eth1 environment"); let deposit_contract = ð1.deposit_contract; - let web3 = eth1.web3(); + let anvil_client = eth1.json_rpc_client(); let cache_len = 4; @@ -258,7 +257,7 @@ mod eth1_cache { SensitiveUrl::parse(eth1.endpoint().as_str()).unwrap(), ), deposit_contract_address: deposit_contract.address(), - lowest_cached_block_number: get_block_number(&web3).await, + lowest_cached_block_number: get_block_number(&anvil_client).await, follow_distance: 0, block_cache_truncation: Some(cache_len), ..Config::default() @@ -270,7 +269,7 @@ mod eth1_cache { for _ in 0..4u8 { for _ in 0..cache_len / 2 { - eth1.ganache.evm_mine().await.expect("should mine block") + eth1.anvil.evm_mine().await.expect("should mine block") } service .update_deposit_cache(None) @@ -298,11 +297,11 @@ mod eth1_cache { let n = 16; - let eth1 = new_ganache_instance() + let eth1 = new_anvil_instance() .await .expect("should start eth1 environment"); let deposit_contract = ð1.deposit_contract; - let web3 = eth1.web3(); + let anvil_client = eth1.json_rpc_client(); let service = Service::new( Config { @@ -310,7 +309,7 @@ mod eth1_cache { SensitiveUrl::parse(eth1.endpoint().as_str()).unwrap(), ), deposit_contract_address: deposit_contract.address(), - lowest_cached_block_number: get_block_number(&web3).await, + lowest_cached_block_number: get_block_number(&anvil_client).await, follow_distance: 0, ..Config::default() }, @@ -320,7 +319,7 @@ mod eth1_cache { .unwrap(); for _ in 0..n { - eth1.ganache.evm_mine().await.expect("should mine block") + eth1.anvil.evm_mine().await.expect("should mine block") } futures::try_join!( @@ -341,6 +340,7 @@ mod eth1_cache { } mod deposit_tree { + use super::*; #[tokio::test] @@ -350,13 +350,13 @@ mod deposit_tree { let n = 4; - let eth1 = new_ganache_instance() + let eth1 = new_anvil_instance() .await .expect("should start eth1 environment"); let deposit_contract = ð1.deposit_contract; - let web3 = eth1.web3(); + let anvil_client = eth1.json_rpc_client(); - let start_block = get_block_number(&web3).await; + let start_block = get_block_number(&anvil_client).await; let service = Service::new( Config { @@ -431,13 +431,13 @@ mod deposit_tree { let n = 8; - let eth1 = new_ganache_instance() + let eth1 = new_anvil_instance() .await .expect("should start eth1 environment"); let deposit_contract = ð1.deposit_contract; - let web3 = eth1.web3(); + let anvil_client = eth1.json_rpc_client(); - let start_block = get_block_number(&web3).await; + let start_block = get_block_number(&anvil_client).await; let service = Service::new( Config { @@ -484,11 +484,12 @@ mod deposit_tree { let deposits: Vec<_> = (0..n).map(|_| random_deposit_data()).collect(); - let eth1 = new_ganache_instance() + let eth1 = new_anvil_instance() .await .expect("should start eth1 environment"); + let deposit_contract = ð1.deposit_contract; - let web3 = eth1.web3(); + let anvil_client = eth1.json_rpc_client(); let mut deposit_roots = vec![]; let mut deposit_counts = vec![]; @@ -502,7 +503,7 @@ mod deposit_tree { .deposit(deposit.clone()) .await .expect("should perform a deposit"); - let block_number = get_block_number(&web3).await; + let block_number = get_block_number(&anvil_client).await; deposit_roots.push( blocking_deposit_root(&client, ð1, block_number) .await @@ -518,7 +519,7 @@ mod deposit_tree { let mut tree = DepositCache::default(); // Pull all the deposit logs from the contract. - let block_number = get_block_number(&web3).await; + let block_number = get_block_number(&anvil_client).await; let logs: Vec<_> = blocking_deposit_logs(&client, ð1, 0..block_number) .await .iter() @@ -593,15 +594,15 @@ mod http { #[tokio::test] async fn incrementing_deposits() { async { - let eth1 = new_ganache_instance() + let eth1 = new_anvil_instance() .await .expect("should start eth1 environment"); let deposit_contract = ð1.deposit_contract; - let web3 = eth1.web3(); + let anvil_client = eth1.json_rpc_client(); let client = HttpJsonRpc::new(SensitiveUrl::parse(ð1.endpoint()).unwrap(), None).unwrap(); - let block_number = get_block_number(&web3).await; + let block_number = get_block_number(&anvil_client).await; let logs = blocking_deposit_logs(&client, ð1, 0..block_number).await; assert_eq!(logs.len(), 0); @@ -616,10 +617,10 @@ mod http { ); for i in 1..=8 { - eth1.ganache + eth1.anvil .increase_time(1) .await - .expect("should be able to increase time on ganache"); + .expect("should be able to increase time on anvil"); deposit_contract .deposit(random_deposit_data()) @@ -627,7 +628,7 @@ mod http { .expect("should perform a deposit"); // Check the logs. - let block_number = get_block_number(&web3).await; + let block_number = get_block_number(&anvil_client).await; let logs = blocking_deposit_logs(&client, ð1, 0..block_number).await; assert_eq!(logs.len(), i, "the number of logs should be as expected"); @@ -690,13 +691,13 @@ mod fast { async { let log = null_logger(); - let eth1 = new_ganache_instance() + let eth1 = new_anvil_instance() .await .expect("should start eth1 environment"); let deposit_contract = ð1.deposit_contract; - let web3 = eth1.web3(); + let anvil_client = eth1.json_rpc_client(); - let now = get_block_number(&web3).await; + let now = get_block_number(&anvil_client).await; let spec = MainnetEthSpec::default_spec(); let service = Service::new( Config { @@ -724,7 +725,7 @@ mod fast { .await .expect("should perform a deposit"); // Mine an extra block between deposits to test for corner cases - eth1.ganache.evm_mine().await.expect("should mine block"); + eth1.anvil.evm_mine().await.expect("should mine block"); } service @@ -737,7 +738,7 @@ mod fast { "should have imported n deposits" ); - for block_num in 0..=get_block_number(&web3).await { + for block_num in 0..=get_block_number(&anvil_client).await { let expected_deposit_count = blocking_deposit_count(&client, ð1, block_num).await; let expected_deposit_root = blocking_deposit_root(&client, ð1, block_num).await; @@ -773,13 +774,13 @@ mod persist { async { let log = null_logger(); - let eth1 = new_ganache_instance() + let eth1 = new_anvil_instance() .await .expect("should start eth1 environment"); let deposit_contract = ð1.deposit_contract; - let web3 = eth1.web3(); + let anvil_client = eth1.json_rpc_client(); - let now = get_block_number(&web3).await; + let now = get_block_number(&anvil_client).await; let config = Config { endpoint: Eth1Endpoint::NoAuth( SensitiveUrl::parse(eth1.endpoint().as_str()).unwrap(), diff --git a/beacon_node/genesis/tests/tests.rs b/beacon_node/genesis/tests/tests.rs index aaf6a7bea1..f99fcb55bf 100644 --- a/beacon_node/genesis/tests/tests.rs +++ b/beacon_node/genesis/tests/tests.rs @@ -1,11 +1,11 @@ -//! NOTE: These tests will not pass unless ganache is running on `ENDPOINT` (see below). +//! NOTE: These tests will not pass unless an anvil is running on `ENDPOINT` (see below). //! -//! You can start a suitable instance using the `ganache_test_node.sh` script in the `scripts` +//! You can start a suitable instance using the `anvil_test_node.sh` script in the `scripts` //! dir in the root of the `lighthouse` repo. #![cfg(test)] use environment::{Environment, EnvironmentBuilder}; use eth1::{Eth1Endpoint, DEFAULT_CHAIN_ID}; -use eth1_test_rig::{DelayThenDeposit, GanacheEth1Instance}; +use eth1_test_rig::{AnvilEth1Instance, DelayThenDeposit, Middleware}; use genesis::{Eth1Config, Eth1GenesisService}; use sensitive_url::SensitiveUrl; use state_processing::is_valid_genesis_state; @@ -29,15 +29,14 @@ fn basic() { let mut spec = env.eth2_config().spec.clone(); env.runtime().block_on(async { - let eth1 = GanacheEth1Instance::new(DEFAULT_CHAIN_ID.into()) + let eth1 = AnvilEth1Instance::new(DEFAULT_CHAIN_ID.into()) .await .expect("should start eth1 environment"); let deposit_contract = ð1.deposit_contract; - let web3 = eth1.web3(); + let client = eth1.json_rpc_client(); - let now = web3 - .eth() - .block_number() + let now = client + .get_block_number() .await .map(|v| v.as_u64()) .expect("should get block number"); @@ -89,7 +88,7 @@ fn basic() { .map(|(_, state)| state) .expect("should finish waiting for genesis"); - // Note: using ganache these deposits are 1-per-block, therefore we know there should only be + // Note: using anvil these deposits are 1-per-block, therefore we know there should only be // the minimum number of validators. assert_eq!( state.validators().len(), diff --git a/book/src/setup.md b/book/src/setup.md index a1febe4a02..62580ac1be 100644 --- a/book/src/setup.md +++ b/book/src/setup.md @@ -9,9 +9,9 @@ particularly useful for development but still a good way to ensure you have the base dependencies. The additional requirements for developers are: -- [`ganache v7`](https://github.com/trufflesuite/ganache). This is used to +- [`anvil`](https://github.com/foundry-rs/foundry/tree/master/anvil). This is used to simulate the execution chain during tests. You'll get failures during tests if you - don't have `ganache` available on your `PATH` or if ganache is older than v7. + don't have `anvil` available on your `PATH`. - [`cmake`](https://cmake.org/cmake/help/latest/command/install.html). Used by some dependencies. See [`Installation Guide`](./installation.md) for more info. - [`protoc`](https://github.com/protocolbuffers/protobuf/releases) required for diff --git a/lcli/Cargo.toml b/lcli/Cargo.toml index 9e7f2fdb08..af8df1b6b0 100644 --- a/lcli/Cargo.toml +++ b/lcli/Cargo.toml @@ -34,7 +34,6 @@ lighthouse_version = { path = "../common/lighthouse_version" } directory = { path = "../common/directory" } account_utils = { path = "../common/account_utils" } eth2_wallet = { path = "../crypto/eth2_wallet" } -web3 = { version = "0.18.0", default-features = false, features = ["http-tls", "signing", "ws-tls-tokio"] } eth1_test_rig = { path = "../testing/eth1_test_rig" } sensitive_url = { path = "../common/sensitive_url" } eth2 = { path = "../common/eth2" } diff --git a/lcli/src/deploy_deposit_contract.rs b/lcli/src/deploy_deposit_contract.rs index 1128eb52ab..8919ebdaf5 100644 --- a/lcli/src/deploy_deposit_contract.rs +++ b/lcli/src/deploy_deposit_contract.rs @@ -2,19 +2,18 @@ use clap::ArgMatches; use environment::Environment; use types::EthSpec; -use web3::{transports::Http, Web3}; +use eth1_test_rig::{Http, Provider}; pub fn run(env: Environment, matches: &ArgMatches<'_>) -> Result<(), String> { let eth1_http: String = clap_utils::parse_required(matches, "eth1-http")?; let confirmations: usize = clap_utils::parse_required(matches, "confirmations")?; let validator_count: Option = clap_utils::parse_optional(matches, "validator-count")?; - let transport = - Http::new(ð1_http).map_err(|e| format!("Unable to connect to eth1 HTTP: {:?}", e))?; - let web3 = Web3::new(transport); + let client = Provider::::try_from(ð1_http) + .map_err(|e| format!("Unable to connect to eth1 HTTP: {:?}", e))?; env.runtime().block_on(async { - let contract = eth1_test_rig::DepositContract::deploy(web3, confirmations, None) + let contract = eth1_test_rig::DepositContract::deploy(client, confirmations, None) .await .map_err(|e| format!("Failed to deploy deposit contract: {:?}", e))?; diff --git a/lighthouse/tests/account_manager.rs b/lighthouse/tests/account_manager.rs index 696830a0d1..63d79fceb2 100644 --- a/lighthouse/tests/account_manager.rs +++ b/lighthouse/tests/account_manager.rs @@ -28,10 +28,6 @@ use tempfile::{tempdir, TempDir}; use types::{Keypair, PublicKey}; use validator_dir::ValidatorDir; -// TODO: create tests for the `lighthouse account validator deposit` command. This involves getting -// access to an IPC endpoint during testing or adding support for deposit submission via HTTP and -// using ganache. - /// Returns the `lighthouse account` command. fn account_cmd() -> Command { let lighthouse_bin = env!("CARGO_BIN_EXE_lighthouse"); diff --git a/scripts/local_testnet/README.md b/scripts/local_testnet/README.md index c4050ac934..6a8a05f9cd 100644 --- a/scripts/local_testnet/README.md +++ b/scripts/local_testnet/README.md @@ -17,7 +17,7 @@ make install-lcli Modify `vars.env` as desired. -Start a local eth1 ganache server plus boot node along with `BN_COUNT` +Start a local eth1 anvil server plus boot node along with `BN_COUNT` number of beacon nodes and `VC_COUNT` validator clients. The `start_local_testnet.sh` script takes four options `-v VC_COUNT`, `-d DEBUG_LEVEL`, `-p` to enable builder proposals and `-h` for help. @@ -41,9 +41,9 @@ This is not necessary before `start_local_testnet.sh` as it invokes `stop_local_ These scripts are used by ./start_local_testnet.sh and may be used to manually -Start a local eth1 ganache server +Start a local eth1 anvil server ```bash -./ganache_test_node.sh +./anvil_test_node.sh ``` Assuming you are happy with the configuration in `vars.env`, deploy the deposit contract, make deposits, diff --git a/scripts/local_testnet/anvil_test_node.sh b/scripts/local_testnet/anvil_test_node.sh new file mode 100755 index 0000000000..41be917560 --- /dev/null +++ b/scripts/local_testnet/anvil_test_node.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +source ./vars.env + +exec anvil \ + --balance 1000000000 \ + --gas-limit 1000000000 \ + --accounts 10 \ + --mnemonic "$ETH1_NETWORK_MNEMONIC" \ + --block-time $SECONDS_PER_ETH1_BLOCK \ + --port 8545 \ + --chain-id "$CHAIN_ID" diff --git a/scripts/local_testnet/ganache_test_node.sh b/scripts/local_testnet/ganache_test_node.sh deleted file mode 100755 index a489c33224..0000000000 --- a/scripts/local_testnet/ganache_test_node.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -set -Eeuo pipefail - -source ./vars.env - -exec ganache \ - --defaultBalanceEther 1000000000 \ - --gasLimit 1000000000 \ - --accounts 10 \ - --mnemonic "$ETH1_NETWORK_MNEMONIC" \ - --port 8545 \ - --blockTime $SECONDS_PER_ETH1_BLOCK \ - --chain.chainId "$CHAIN_ID" diff --git a/scripts/local_testnet/start_local_testnet.sh b/scripts/local_testnet/start_local_testnet.sh index e3aba5c3ad..a6f5ec7a8c 100755 --- a/scripts/local_testnet/start_local_testnet.sh +++ b/scripts/local_testnet/start_local_testnet.sh @@ -92,11 +92,11 @@ execute_command_add_PID() { echo "$!" >> $PID_FILE } -# Start ganache, setup things up and start the bootnode. +# Start anvil, setup things up and start the bootnode. # The delays are necessary, hopefully there is a better way :( -# Delay to let ganache to get started -execute_command_add_PID ganache_test_node.log ./ganache_test_node.sh +# Delay to let anvil to get started +execute_command_add_PID anvil_test_node.log ./anvil_test_node.sh sleeping 10 # Setup data diff --git a/scripts/local_testnet/vars.env b/scripts/local_testnet/vars.env index 1ade173286..80c4ef1331 100644 --- a/scripts/local_testnet/vars.env +++ b/scripts/local_testnet/vars.env @@ -4,7 +4,7 @@ DATADIR=~/.lighthouse/local-testnet # Directory for the eth2 config TESTNET_DIR=$DATADIR/testnet -# Mnemonic for the ganache test network +# Mnemonic for the anvil test network ETH1_NETWORK_MNEMONIC="vast thought differ pull jewel broom cook wrist tribe word before omit" # Hardcoded deposit contract based on ETH1_NETWORK_MNEMONIC diff --git a/scripts/tests/doppelganger_protection.sh b/scripts/tests/doppelganger_protection.sh index 95dfff5696..e68ca21516 100755 --- a/scripts/tests/doppelganger_protection.sh +++ b/scripts/tests/doppelganger_protection.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Requires `lighthouse`, ``lcli`, `ganache`, `curl`, `jq` +# Requires `lighthouse`, ``lcli`, `anvil`, `curl`, `jq` BEHAVIOR=$1 @@ -23,12 +23,12 @@ source ./vars.env exit_if_fails ../local_testnet/clean.sh -echo "Starting ganache" +echo "Starting anvil" -exit_if_fails ../local_testnet/ganache_test_node.sh &> /dev/null & -GANACHE_PID=$! +exit_if_fails ../local_testnet/anvil_test_node.sh &> /dev/null & +ANVIL_PID=$! -# Wait for ganache to start +# Wait for anvil to start sleep 5 echo "Setting up local testnet" @@ -79,7 +79,7 @@ if [[ "$BEHAVIOR" == "failure" ]]; then echo "Shutting down" # Cleanup - kill $BOOT_PID $BEACON_PID $BEACON_PID2 $BEACON_PID3 $GANACHE_PID $VALIDATOR_1_PID $VALIDATOR_2_PID $VALIDATOR_3_PID + kill $BOOT_PID $BEACON_PID $BEACON_PID2 $BEACON_PID3 $ANVIL_PID $VALIDATOR_1_PID $VALIDATOR_2_PID $VALIDATOR_3_PID echo "Done" @@ -144,7 +144,7 @@ if [[ "$BEHAVIOR" == "success" ]]; then # Cleanup cd $PREVIOUS_DIR - kill $BOOT_PID $BEACON_PID $BEACON_PID2 $BEACON_PID3 $GANACHE_PID $VALIDATOR_1_PID $VALIDATOR_2_PID $VALIDATOR_3_PID $VALIDATOR_4_PID + kill $BOOT_PID $BEACON_PID $BEACON_PID2 $BEACON_PID3 $ANVIL_PID $VALIDATOR_1_PID $VALIDATOR_2_PID $VALIDATOR_3_PID $VALIDATOR_4_PID echo "Done" diff --git a/scripts/tests/vars.env b/scripts/tests/vars.env index 778a0afca5..7429a35eb6 100644 --- a/scripts/tests/vars.env +++ b/scripts/tests/vars.env @@ -4,7 +4,7 @@ DATADIR=~/.lighthouse/local-testnet # Directory for the eth2 config TESTNET_DIR=$DATADIR/testnet -# Mnemonic for the ganache test network +# Mnemonic for the anvil test network ETH1_NETWORK_MNEMONIC="vast thought differ pull jewel broom cook wrist tribe word before omit" # Hardcoded deposit contract based on ETH1_NETWORK_MNEMONIC diff --git a/testing/eth1_test_rig/Cargo.toml b/testing/eth1_test_rig/Cargo.toml index 08766f14fc..5c78c09022 100644 --- a/testing/eth1_test_rig/Cargo.toml +++ b/testing/eth1_test_rig/Cargo.toml @@ -6,8 +6,11 @@ edition = "2021" [dependencies] tokio = { version = "1.14.0", features = ["time"] } -web3 = { version = "0.18.0", default-features = false, features = ["http-tls", "signing", "ws-tls-tokio"] } +ethers-core = "1.0.2" +ethers-providers = "1.0.2" +ethers-contract = "1.0.2" types = { path = "../../consensus/types"} serde_json = "1.0.58" deposit_contract = { path = "../../common/deposit_contract"} unused_port = { path = "../../common/unused_port" } +hex = "0.4.2" diff --git a/testing/eth1_test_rig/src/anvil.rs b/testing/eth1_test_rig/src/anvil.rs new file mode 100644 index 0000000000..1b86711c2f --- /dev/null +++ b/testing/eth1_test_rig/src/anvil.rs @@ -0,0 +1,101 @@ +use ethers_core::utils::{Anvil, AnvilInstance}; +use ethers_providers::{Http, Middleware, Provider}; +use serde_json::json; +use std::convert::TryFrom; +use unused_port::unused_tcp4_port; + +/// Provides a dedicated `anvil` instance. +/// +/// Requires that `anvil` is installed and available on `PATH`. +pub struct AnvilCliInstance { + pub port: u16, + pub anvil: AnvilInstance, + pub client: Provider, + chain_id: u64, +} + +impl AnvilCliInstance { + fn new_from_child(anvil_instance: Anvil, chain_id: u64, port: u16) -> Result { + let client = Provider::::try_from(&endpoint(port)) + .map_err(|e| format!("Failed to start HTTP transport connected to anvil: {:?}", e))?; + Ok(Self { + port, + anvil: anvil_instance.spawn(), + client, + chain_id, + }) + } + pub fn new(chain_id: u64) -> Result { + let port = unused_tcp4_port()?; + + let anvil = Anvil::new() + .port(port) + .mnemonic("vast thought differ pull jewel broom cook wrist tribe word before omit") + .arg("--balance") + .arg("1000000000") + .arg("--gas-limit") + .arg("1000000000") + .arg("--accounts") + .arg("10") + .arg("--chain-id") + .arg(format!("{}", chain_id)); + + Self::new_from_child(anvil, chain_id, port) + } + + pub fn fork(&self) -> Result { + let port = unused_tcp4_port()?; + + let anvil = Anvil::new() + .port(port) + .arg("--chain-id") + .arg(format!("{}", self.chain_id())) + .fork(self.endpoint()); + + Self::new_from_child(anvil, self.chain_id, port) + } + + /// Returns the endpoint that this instance is listening on. + pub fn endpoint(&self) -> String { + endpoint(self.port) + } + + /// Returns the chain id of the anvil instance + pub fn chain_id(&self) -> u64 { + self.chain_id + } + + /// Increase the timestamp on future blocks by `increase_by` seconds. + pub async fn increase_time(&self, increase_by: u64) -> Result<(), String> { + self.client + .request("evm_increaseTime", vec![json!(increase_by)]) + .await + .map(|_json_value: u64| ()) + .map_err(|e| format!("Failed to increase time on EVM (is this anvil?): {:?}", e)) + } + + /// Returns the current block number, as u64 + pub async fn block_number(&self) -> Result { + self.client + .get_block_number() + .await + .map(|v| v.as_u64()) + .map_err(|e| format!("Failed to get block number: {:?}", e)) + } + + /// Mines a single block. + pub async fn evm_mine(&self) -> Result<(), String> { + self.client + .request("evm_mine", ()) + .await + .map(|_: String| ()) + .map_err(|_| { + "utils should mine new block with evm_mine (only works with anvil/ganache!)" + .to_string() + }) + } +} + +fn endpoint(port: u16) -> String { + format!("http://127.0.0.1:{}", port) +} diff --git a/testing/eth1_test_rig/src/ganache.rs b/testing/eth1_test_rig/src/ganache.rs deleted file mode 100644 index 898a089ba0..0000000000 --- a/testing/eth1_test_rig/src/ganache.rs +++ /dev/null @@ -1,193 +0,0 @@ -use serde_json::json; -use std::io::prelude::*; -use std::io::BufReader; -use std::process::{Child, Command, Stdio}; -use std::time::{Duration, Instant}; -use unused_port::unused_tcp4_port; -use web3::{transports::Http, Transport, Web3}; - -/// How long we will wait for ganache to indicate that it is ready. -const GANACHE_STARTUP_TIMEOUT_MILLIS: u64 = 10_000; - -/// Provides a dedicated `ganachi-cli` instance with a connected `Web3` instance. -/// -/// Requires that `ganachi-cli` is installed and available on `PATH`. -pub struct GanacheInstance { - pub port: u16, - child: Child, - pub web3: Web3, - chain_id: u64, -} - -impl GanacheInstance { - fn new_from_child(mut child: Child, port: u16, chain_id: u64) -> Result { - let stdout = child - .stdout - .ok_or("Unable to get stdout for ganache child process")?; - - let start = Instant::now(); - let mut reader = BufReader::new(stdout); - loop { - if start + Duration::from_millis(GANACHE_STARTUP_TIMEOUT_MILLIS) <= Instant::now() { - break Err( - "Timed out waiting for ganache to start. Is ganache installed?".to_string(), - ); - } - - let mut line = String::new(); - if let Err(e) = reader.read_line(&mut line) { - break Err(format!("Failed to read line from ganache process: {:?}", e)); - } else if line.starts_with("RPC Listening on") { - break Ok(()); - } else { - continue; - } - }?; - - let transport = Http::new(&endpoint(port)).map_err(|e| { - format!( - "Failed to start HTTP transport connected to ganache: {:?}", - e - ) - })?; - let web3 = Web3::new(transport); - - child.stdout = Some(reader.into_inner()); - - Ok(Self { - port, - child, - web3, - chain_id, - }) - } - - /// Start a new `ganache` process, waiting until it indicates that it is ready to accept - /// RPC connections. - pub fn new(chain_id: u64) -> Result { - let port = unused_tcp4_port()?; - let binary = match cfg!(windows) { - true => "ganache.cmd", - false => "ganache", - }; - let child = Command::new(binary) - .stdout(Stdio::piped()) - .arg("--defaultBalanceEther") - .arg("1000000000") - .arg("--gasLimit") - .arg("1000000000") - .arg("--accounts") - .arg("10") - .arg("--port") - .arg(format!("{}", port)) - .arg("--mnemonic") - .arg("\"vast thought differ pull jewel broom cook wrist tribe word before omit\"") - .arg("--chain.chainId") - .arg(format!("{}", chain_id)) - .spawn() - .map_err(|e| { - format!( - "Failed to start {}. \ - Is it installed and available on $PATH? Error: {:?}", - binary, e - ) - })?; - - Self::new_from_child(child, port, chain_id) - } - - pub fn fork(&self) -> Result { - let port = unused_tcp4_port()?; - let binary = match cfg!(windows) { - true => "ganache.cmd", - false => "ganache", - }; - let child = Command::new(binary) - .stdout(Stdio::piped()) - .arg("--fork") - .arg(self.endpoint()) - .arg("--port") - .arg(format!("{}", port)) - .arg("--chain.chainId") - .arg(format!("{}", self.chain_id)) - .spawn() - .map_err(|e| { - format!( - "Failed to start {}. \ - Is it installed and available on $PATH? Error: {:?}", - binary, e - ) - })?; - - Self::new_from_child(child, port, self.chain_id) - } - - /// Returns the endpoint that this instance is listening on. - pub fn endpoint(&self) -> String { - endpoint(self.port) - } - - /// Returns the chain id of the ganache instance - pub fn chain_id(&self) -> u64 { - self.chain_id - } - - /// Increase the timestamp on future blocks by `increase_by` seconds. - pub async fn increase_time(&self, increase_by: u64) -> Result<(), String> { - self.web3 - .transport() - .execute("evm_increaseTime", vec![json!(increase_by)]) - .await - .map(|_json_value| ()) - .map_err(|e| format!("Failed to increase time on EVM (is this ganache?): {:?}", e)) - } - - /// Returns the current block number, as u64 - pub async fn block_number(&self) -> Result { - self.web3 - .eth() - .block_number() - .await - .map(|v| v.as_u64()) - .map_err(|e| format!("Failed to get block number: {:?}", e)) - } - - /// Mines a single block. - pub async fn evm_mine(&self) -> Result<(), String> { - self.web3 - .transport() - .execute("evm_mine", vec![]) - .await - .map(|_| ()) - .map_err(|_| { - "utils should mine new block with evm_mine (only works with ganache!)".to_string() - }) - } -} - -fn endpoint(port: u16) -> String { - format!("http://127.0.0.1:{}", port) -} - -impl Drop for GanacheInstance { - fn drop(&mut self) { - if cfg!(windows) { - // Calling child.kill() in Windows will only kill the process - // that spawned ganache, leaving the actual ganache process - // intact. You have to kill the whole process tree. What's more, - // if you don't spawn ganache with --keepAliveTimeout=0, Windows - // will STILL keep the server running even after you've ended - // the process tree and it's disappeared from the task manager. - // Unbelievable... - Command::new("taskkill") - .arg("/pid") - .arg(self.child.id().to_string()) - .arg("/T") - .arg("/F") - .output() - .expect("failed to execute taskkill"); - } else { - let _ = self.child.kill(); - } - } -} diff --git a/testing/eth1_test_rig/src/lib.rs b/testing/eth1_test_rig/src/lib.rs index 42081a60e7..0063975ee1 100644 --- a/testing/eth1_test_rig/src/lib.rs +++ b/testing/eth1_test_rig/src/lib.rs @@ -1,77 +1,79 @@ //! Provides utilities for deploying and manipulating the eth2 deposit contract on the eth1 chain. //! -//! Presently used with [`ganache`](https://github.com/trufflesuite/ganache) to simulate +//! Presently used with [`anvil`](https://github.com/foundry-rs/foundry/tree/master/anvil) to simulate //! the deposit contract for testing beacon node eth1 integration. //! //! Not tested to work with actual clients (e.g., geth). It should work fine, however there may be //! some initial issues. -mod ganache; +mod anvil; +use anvil::AnvilCliInstance; use deposit_contract::{ encode_eth1_tx_data, testnet, ABI, BYTECODE, CONTRACT_DEPLOY_GAS, DEPOSIT_GAS, }; -use ganache::GanacheInstance; +use ethers_contract::Contract; +use ethers_core::{ + abi::Abi, + types::{transaction::eip2718::TypedTransaction, Address, Bytes, TransactionRequest, U256}, +}; +pub use ethers_providers::{Http, Middleware, Provider}; use std::time::Duration; use tokio::time::sleep; use types::DepositData; use types::{test_utils::generate_deterministic_keypair, EthSpec, Hash256, Keypair, Signature}; -use web3::contract::{Contract, Options}; -use web3::transports::Http; -use web3::types::{Address, TransactionRequest, U256}; -use web3::Web3; pub const DEPLOYER_ACCOUNTS_INDEX: usize = 0; pub const DEPOSIT_ACCOUNTS_INDEX: usize = 0; -/// Provides a dedicated ganache instance with the deposit contract already deployed. -pub struct GanacheEth1Instance { - pub ganache: GanacheInstance, +/// Provides a dedicated anvil instance with the deposit contract already deployed. +pub struct AnvilEth1Instance { + pub anvil: AnvilCliInstance, pub deposit_contract: DepositContract, } -impl GanacheEth1Instance { +impl AnvilEth1Instance { pub async fn new(chain_id: u64) -> Result { - let ganache = GanacheInstance::new(chain_id)?; - DepositContract::deploy(ganache.web3.clone(), 0, None) + let anvil = AnvilCliInstance::new(chain_id)?; + DepositContract::deploy(anvil.client.clone(), 0, None) .await .map(|deposit_contract| Self { - ganache, + anvil, deposit_contract, }) } pub fn endpoint(&self) -> String { - self.ganache.endpoint() + self.anvil.endpoint() } - pub fn web3(&self) -> Web3 { - self.ganache.web3.clone() + pub fn json_rpc_client(&self) -> Provider { + self.anvil.client.clone() } } /// Deploys and provides functions for the eth2 deposit contract, deployed on the eth1 chain. #[derive(Clone, Debug)] pub struct DepositContract { - web3: Web3, - contract: Contract, + client: Provider, + contract: Contract>, } impl DepositContract { pub async fn deploy( - web3: Web3, + client: Provider, confirmations: usize, password: Option, ) -> Result { - Self::deploy_bytecode(web3, confirmations, BYTECODE, ABI, password).await + Self::deploy_bytecode(client, confirmations, BYTECODE, ABI, password).await } pub async fn deploy_testnet( - web3: Web3, + client: Provider, confirmations: usize, password: Option, ) -> Result { Self::deploy_bytecode( - web3, + client, confirmations, testnet::BYTECODE, testnet::ABI, @@ -81,29 +83,25 @@ impl DepositContract { } async fn deploy_bytecode( - web3: Web3, + client: Provider, confirmations: usize, bytecode: &[u8], abi: &[u8], password: Option, ) -> Result { - let address = deploy_deposit_contract( - web3.clone(), - confirmations, - bytecode.to_vec(), - abi.to_vec(), - password, - ) - .await - .map_err(|e| { - format!( - "Failed to deploy contract: {}. Is scripts/ganache_tests_node.sh running?.", - e - ) - })?; - Contract::from_json(web3.clone().eth(), address, ABI) - .map_err(|e| format!("Failed to init contract: {:?}", e)) - .map(move |contract| Self { web3, contract }) + let abi = Abi::load(abi).map_err(|e| format!("Invalid deposit contract abi: {:?}", e))?; + let address = + deploy_deposit_contract(client.clone(), confirmations, bytecode.to_vec(), password) + .await + .map_err(|e| { + format!( + "Failed to deploy contract: {}. Is scripts/anvil_tests_node.sh running?.", + e + ) + })?; + + let contract = Contract::new(address, abi, client.clone()); + Ok(Self { client, contract }) } /// The deposit contract's address in `0x00ab...` format. @@ -178,9 +176,8 @@ impl DepositContract { /// Performs a non-blocking deposit. pub async fn deposit_async(&self, deposit_data: DepositData) -> Result<(), String> { let from = self - .web3 - .eth() - .accounts() + .client + .get_accounts() .await .map_err(|e| format!("Failed to get accounts: {:?}", e)) .and_then(|accounts| { @@ -189,32 +186,33 @@ impl DepositContract { .cloned() .ok_or_else(|| "Insufficient accounts for deposit".to_string()) })?; - let tx_request = TransactionRequest { - from, - to: Some(self.contract.address()), - gas: Some(U256::from(DEPOSIT_GAS)), - gas_price: None, - max_fee_per_gas: None, - max_priority_fee_per_gas: None, - value: Some(from_gwei(deposit_data.amount)), - // Note: the reason we use this `TransactionRequest` instead of just using the - // function in `self.contract` is so that the `eth1_tx_data` function gets used - // during testing. - // - // It's important that `eth1_tx_data` stays correct and does not suffer from - // code-rot. - data: encode_eth1_tx_data(&deposit_data).map(Into::into).ok(), - nonce: None, - condition: None, - transaction_type: None, - access_list: None, - }; + // Note: the reason we use this `TransactionRequest` instead of just using the + // function in `self.contract` is so that the `eth1_tx_data` function gets used + // during testing. + // + // It's important that `eth1_tx_data` stays correct and does not suffer from + // code-rot. + let tx_request = TransactionRequest::new() + .from(from) + .to(self.contract.address()) + .gas(DEPOSIT_GAS) + .value(from_gwei(deposit_data.amount)) + .data(Bytes::from(encode_eth1_tx_data(&deposit_data).map_err( + |e| format!("Failed to encode deposit data: {:?}", e), + )?)); - self.web3 - .eth() - .send_transaction(tx_request) + let pending_tx = self + .client + .send_transaction(tx_request, None) .await .map_err(|e| format!("Failed to call deposit fn: {:?}", e))?; + + pending_tx + .interval(Duration::from_millis(10)) + .confirmations(0) + .await + .map_err(|e| format!("Transaction failed to resolve: {:?}", e))? + .ok_or_else(|| "Transaction dropped from mempool".to_string())?; Ok(()) } @@ -245,17 +243,13 @@ fn from_gwei(gwei: u64) -> U256 { /// Deploys the deposit contract to the given web3 instance using the account with index /// `DEPLOYER_ACCOUNTS_INDEX`. async fn deploy_deposit_contract( - web3: Web3, + client: Provider, confirmations: usize, bytecode: Vec, - abi: Vec, password_opt: Option, ) -> Result { - let bytecode = String::from_utf8(bytecode).expect("bytecode must be valid utf8"); - - let from_address = web3 - .eth() - .accounts() + let from_address = client + .get_accounts() .await .map_err(|e| format!("Failed to get accounts: {:?}", e)) .and_then(|accounts| { @@ -266,30 +260,42 @@ async fn deploy_deposit_contract( })?; let deploy_address = if let Some(password) = password_opt { - let result = web3 - .personal() - .unlock_account(from_address, &password, None) + let result = client + .request( + "personal_unlockAccount", + vec![from_address.to_string(), password], + ) .await; + match result { - Ok(true) => return Ok(from_address), + Ok(true) => from_address, Ok(false) => return Err("Eth1 node refused to unlock account".to_string()), Err(e) => return Err(format!("Eth1 unlock request failed: {:?}", e)), - }; + } } else { from_address }; - let pending_contract = Contract::deploy(web3.eth(), &abi) - .map_err(|e| format!("Unable to build contract deployer: {:?}", e))? - .confirmations(confirmations) - .options(Options { - gas: Some(U256::from(CONTRACT_DEPLOY_GAS)), - ..Options::default() - }) - .execute(bytecode, (), deploy_address); + let mut bytecode = String::from_utf8(bytecode).unwrap(); + bytecode.retain(|c| c.is_ascii_hexdigit()); + let bytecode = hex::decode(&bytecode[1..]).unwrap(); - pending_contract + let deploy_tx: TypedTransaction = TransactionRequest::new() + .from(deploy_address) + .data(Bytes::from(bytecode)) + .gas(CONTRACT_DEPLOY_GAS) + .into(); + + let pending_tx = client + .send_transaction(deploy_tx, None) .await - .map(|contract| contract.address()) - .map_err(|e| format!("Unable to resolve pending contract: {:?}", e)) + .map_err(|e| format!("Failed to send tx: {:?}", e))?; + + let tx = pending_tx + .interval(Duration::from_millis(500)) + .confirmations(confirmations) + .await + .map_err(|e| format!("Failed to fetch tx receipt: {:?}", e))?; + tx.and_then(|tx| tx.contract_address) + .ok_or_else(|| "Deposit contract not deployed successfully".to_string()) } diff --git a/testing/simulator/src/cli.rs b/testing/simulator/src/cli.rs index 9668ee8cb4..5dc2d5ec84 100644 --- a/testing/simulator/src/cli.rs +++ b/testing/simulator/src/cli.rs @@ -10,7 +10,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .about( "Lighthouse Beacon Chain Simulator creates `n` beacon node and validator clients, \ each with `v` validators. A deposit contract is deployed at the start of the \ - simulation using a local `ganache` instance (you must have `ganache` \ + simulation using a local `anvil` instance (you must have `anvil` \ installed and avaliable on your path). All beacon nodes independently listen \ for genesis from the deposit contract, then start operating. \ \ diff --git a/testing/simulator/src/eth1_sim.rs b/testing/simulator/src/eth1_sim.rs index 1699c0e9ee..a5462da396 100644 --- a/testing/simulator/src/eth1_sim.rs +++ b/testing/simulator/src/eth1_sim.rs @@ -2,7 +2,7 @@ use crate::local_network::{EXECUTION_PORT, TERMINAL_BLOCK, TERMINAL_DIFFICULTY}; use crate::{checks, LocalNetwork, E}; use clap::ArgMatches; use eth1::{Eth1Endpoint, DEFAULT_CHAIN_ID}; -use eth1_test_rig::GanacheEth1Instance; +use eth1_test_rig::AnvilEth1Instance; use execution_layer::http::deposit_methods::Eth1Id; use futures::prelude::*; @@ -110,12 +110,12 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> { * Deploy the deposit contract, spawn tasks to keep creating new blocks and deposit * validators. */ - let ganache_eth1_instance = GanacheEth1Instance::new(DEFAULT_CHAIN_ID.into()).await?; - let deposit_contract = ganache_eth1_instance.deposit_contract; - let chain_id = ganache_eth1_instance.ganache.chain_id(); - let ganache = ganache_eth1_instance.ganache; - let eth1_endpoint = SensitiveUrl::parse(ganache.endpoint().as_str()) - .expect("Unable to parse ganache endpoint."); + let anvil_eth1_instance = AnvilEth1Instance::new(DEFAULT_CHAIN_ID.into()).await?; + let deposit_contract = anvil_eth1_instance.deposit_contract; + let chain_id = anvil_eth1_instance.anvil.chain_id(); + let anvil = anvil_eth1_instance.anvil; + let eth1_endpoint = SensitiveUrl::parse(anvil.endpoint().as_str()) + .expect("Unable to parse anvil endpoint."); let deposit_contract_address = deposit_contract.address(); // Start a timer that produces eth1 blocks on an interval. @@ -123,7 +123,7 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> { let mut interval = tokio::time::interval(eth1_block_time); loop { interval.tick().await; - let _ = ganache.evm_mine().await; + let _ = anvil.evm_mine().await; } }); diff --git a/testing/simulator/src/main.rs b/testing/simulator/src/main.rs index 922149537c..a19777c5ab 100644 --- a/testing/simulator/src/main.rs +++ b/testing/simulator/src/main.rs @@ -1,6 +1,6 @@ //! This crate provides a simluation that creates `n` beacon node and validator clients, each with //! `v` validators. A deposit contract is deployed at the start of the simulation using a local -//! `ganache` instance (you must have `ganache` installed and avaliable on your path). All +//! `anvil` instance (you must have `anvil` installed and avaliable on your path). All //! beacon nodes independently listen for genesis from the deposit contract, then start operating. //! //! As the simulation runs, there are checks made to ensure that all components are running From 714ed538391470f020b16609a63b167ea814a872 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 15 May 2023 07:22:03 +0000 Subject: [PATCH 02/63] Add a flag for storing invalid blocks (#4194) ## Issue Addressed NA ## Proposed Changes Adds a flag to store invalid blocks on disk for teh debugz. Only *some* invalid blocks are stored, those which: - Were received via gossip (rather than RPC, for instance) - This keeps things simple to start with and should capture most blocks. - Passed gossip verification - This reduces the ability for random people to fill up our disk. A proposer signature is required to write something to disk. ## Additional Info It's possible that we'll store blocks that aren't necessarily invalid, but we had an internal error during verification. Those blocks seem like they might be useful sometimes. --- beacon_node/lighthouse_network/src/config.rs | 4 + .../network/src/beacon_processor/mod.rs | 55 +++++++---- .../network/src/beacon_processor/tests.rs | 1 + .../beacon_processor/worker/gossip_methods.rs | 93 +++++++++++++++++-- beacon_node/network/src/router.rs | 4 +- beacon_node/network/src/service.rs | 8 ++ beacon_node/src/cli.rs | 9 ++ beacon_node/src/config.rs | 5 + lighthouse/tests/beacon_node.rs | 21 +++++ 9 files changed, 173 insertions(+), 27 deletions(-) diff --git a/beacon_node/lighthouse_network/src/config.rs b/beacon_node/lighthouse_network/src/config.rs index f4b3b78d04..5a11890a26 100644 --- a/beacon_node/lighthouse_network/src/config.rs +++ b/beacon_node/lighthouse_network/src/config.rs @@ -145,6 +145,9 @@ pub struct Config { /// Configuration for the outbound rate limiter (requests made by this node). pub outbound_rate_limiter_config: Option, + + /// Configures if/where invalid blocks should be stored. + pub invalid_block_storage: Option, } impl Config { @@ -329,6 +332,7 @@ impl Default for Config { metrics_enabled: false, enable_light_client_server: false, outbound_rate_limiter_config: None, + invalid_block_storage: None, } } } diff --git a/beacon_node/network/src/beacon_processor/mod.rs b/beacon_node/network/src/beacon_processor/mod.rs index 9603205228..26d2c19b51 100644 --- a/beacon_node/network/src/beacon_processor/mod.rs +++ b/beacon_node/network/src/beacon_processor/mod.rs @@ -54,6 +54,7 @@ use logging::TimeLatch; use slog::{crit, debug, error, trace, warn, Logger}; use std::collections::VecDeque; use std::future::Future; +use std::path::PathBuf; use std::pin::Pin; use std::sync::{Arc, Weak}; use std::task::Context; @@ -982,6 +983,13 @@ impl Stream for InboundEvents { } } +/// Defines if and where we will store the SSZ files of invalid blocks. +#[derive(Clone)] +pub enum InvalidBlockStorage { + Enabled(PathBuf), + Disabled, +} + /// A mutli-threaded processor for messages received on the network /// that need to be processed by the `BeaconChain` /// @@ -995,6 +1003,7 @@ pub struct BeaconProcessor { pub max_workers: usize, pub current_workers: usize, pub importing_blocks: DuplicateCache, + pub invalid_block_storage: InvalidBlockStorage, pub log: Logger, } @@ -1676,19 +1685,23 @@ impl BeaconProcessor { peer_client, block, seen_timestamp, - } => task_spawner.spawn_async(async move { - worker - .process_gossip_block( - message_id, - peer_id, - peer_client, - block, - work_reprocessing_tx, - duplicate_cache, - seen_timestamp, - ) - .await - }), + } => { + let invalid_block_storage = self.invalid_block_storage.clone(); + task_spawner.spawn_async(async move { + worker + .process_gossip_block( + message_id, + peer_id, + peer_client, + block, + work_reprocessing_tx, + duplicate_cache, + invalid_block_storage, + seen_timestamp, + ) + .await + }) + } /* * Import for blocks that we received earlier than their intended slot. */ @@ -1696,12 +1709,16 @@ impl BeaconProcessor { peer_id, block, seen_timestamp, - } => task_spawner.spawn_async(worker.process_gossip_verified_block( - peer_id, - *block, - work_reprocessing_tx, - seen_timestamp, - )), + } => { + let invalid_block_storage = self.invalid_block_storage.clone(); + task_spawner.spawn_async(worker.process_gossip_verified_block( + peer_id, + *block, + work_reprocessing_tx, + invalid_block_storage, + seen_timestamp, + )) + } /* * Voluntary exits received on gossip. */ diff --git a/beacon_node/network/src/beacon_processor/tests.rs b/beacon_node/network/src/beacon_processor/tests.rs index 4b0a159eb4..b93e83ad78 100644 --- a/beacon_node/network/src/beacon_processor/tests.rs +++ b/beacon_node/network/src/beacon_processor/tests.rs @@ -203,6 +203,7 @@ impl TestRig { max_workers: cmp::max(1, num_cpus::get()), current_workers: 0, importing_blocks: duplicate_cache.clone(), + invalid_block_storage: InvalidBlockStorage::Disabled, log: log.clone(), } .spawn_manager(beacon_processor_rx, Some(work_journal_tx)); diff --git a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs b/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs index 1ec03ae954..9d85bc545e 100644 --- a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs +++ b/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs @@ -13,9 +13,12 @@ use beacon_chain::{ }; use lighthouse_network::{Client, MessageAcceptance, MessageId, PeerAction, PeerId, ReportSource}; use operation_pool::ReceivedPreCapella; -use slog::{crit, debug, error, info, trace, warn}; +use slog::{crit, debug, error, info, trace, warn, Logger}; use slot_clock::SlotClock; use ssz::Encode; +use std::fs; +use std::io::Write; +use std::path::PathBuf; use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use store::hot_cold_store::HotColdDBError; @@ -34,7 +37,7 @@ use super::{ }, Worker, }; -use crate::beacon_processor::DuplicateCache; +use crate::beacon_processor::{DuplicateCache, InvalidBlockStorage}; /// Set to `true` to introduce stricter penalties for peers who send some types of late consensus /// messages. @@ -663,6 +666,7 @@ impl Worker { block: Arc>, reprocess_tx: mpsc::Sender>, duplicate_cache: DuplicateCache, + invalid_block_storage: InvalidBlockStorage, seen_duration: Duration, ) { if let Some(gossip_verified_block) = self @@ -683,6 +687,7 @@ impl Worker { peer_id, gossip_verified_block, reprocess_tx, + invalid_block_storage, seen_duration, ) .await; @@ -935,13 +940,14 @@ impl Worker { peer_id: PeerId, verified_block: GossipVerifiedBlock, reprocess_tx: mpsc::Sender>, + invalid_block_storage: InvalidBlockStorage, // This value is not used presently, but it might come in handy for debugging. _seen_duration: Duration, ) { let block: Arc<_> = verified_block.block.clone(); let block_root = verified_block.block_root; - match self + let result = self .chain .process_block( block_root, @@ -949,14 +955,15 @@ impl Worker { CountUnrealized::True, NotifyExecutionLayer::Yes, ) - .await - { + .await; + + match &result { Ok(block_root) => { metrics::inc_counter(&metrics::BEACON_PROCESSOR_GOSSIP_BLOCK_IMPORTED_TOTAL); if reprocess_tx .try_send(ReprocessQueueMessage::BlockImported { - block_root, + block_root: *block_root, parent_root: block.message().parent_root(), }) .is_err() @@ -986,7 +993,11 @@ impl Worker { "Block with unknown parent attempted to be processed"; "peer_id" => %peer_id ); - self.send_sync_message(SyncMessage::UnknownBlock(peer_id, block, block_root)); + self.send_sync_message(SyncMessage::UnknownBlock( + peer_id, + block.clone(), + block_root, + )); } Err(ref e @ BlockError::ExecutionPayloadError(ref epe)) if !epe.penalize_peer() => { debug!( @@ -1015,6 +1026,16 @@ impl Worker { ); } }; + + if let Err(e) = &result { + self.maybe_store_invalid_block( + &invalid_block_storage, + block_root, + &block, + e, + &self.log, + ); + } } pub fn process_gossip_voluntary_exit( @@ -2486,4 +2507,62 @@ impl Worker { self.propagate_if_timely(is_timely, message_id, peer_id) } + + /// Stores a block as a SSZ file, if and where `invalid_block_storage` dictates. + fn maybe_store_invalid_block( + &self, + invalid_block_storage: &InvalidBlockStorage, + 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)); + let error_path = base_dir.join(format!("{}_{:?}.error", block.slot(), block_root)); + + let write_file = |path: PathBuf, bytes: &[u8]| { + // No need to write the same file twice. For the error file, + // this means that we'll remember the first error message but + // forget the rest. + if path.exists() { + return; + } + + // Write to the file. + let write_result = fs::OpenOptions::new() + // Only succeed if the file doesn't already exist. We should + // have checked for this earlier. + .create_new(true) + .write(true) + .open(&path) + .map_err(|e| format!("Failed to open file: {:?}", e)) + .map(|mut file| { + file.write_all(bytes) + .map_err(|e| format!("Failed to write file: {:?}", e)) + }); + if let Err(e) = write_result { + error!( + log, + "Failed to store invalid block/error"; + "error" => e, + "path" => ?path, + "root" => ?block_root, + "slot" => block.slot(), + ) + } else { + info!( + log, + "Stored invalid block/error "; + "path" => ?path, + "root" => ?block_root, + "slot" => block.slot(), + ) + } + }; + + write_file(block_path, &block.as_ssz_bytes()); + write_file(error_path, error.to_string().as_bytes()); + } + } } diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index 7f75a27fe2..1b0f1fb41e 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -6,7 +6,7 @@ #![allow(clippy::unit_arg)] use crate::beacon_processor::{ - BeaconProcessor, WorkEvent as BeaconWorkEvent, MAX_WORK_EVENT_QUEUE_LEN, + BeaconProcessor, InvalidBlockStorage, WorkEvent as BeaconWorkEvent, MAX_WORK_EVENT_QUEUE_LEN, }; use crate::error; use crate::service::{NetworkMessage, RequestId}; @@ -81,6 +81,7 @@ impl Router { network_globals: Arc>, network_send: mpsc::UnboundedSender>, executor: task_executor::TaskExecutor, + invalid_block_storage: InvalidBlockStorage, log: slog::Logger, ) -> error::Result>> { let message_handler_log = log.new(o!("service"=> "router")); @@ -112,6 +113,7 @@ impl Router { max_workers: cmp::max(1, num_cpus::get()), current_workers: 0, importing_blocks: Default::default(), + invalid_block_storage, log: log.clone(), } .spawn_manager(beacon_processor_receive, None); diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index d630cf9c39..edc1d5c2ef 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -1,4 +1,5 @@ use super::sync::manager::RequestId as SyncId; +use crate::beacon_processor::InvalidBlockStorage; use crate::persisted_dht::{clear_dht, load_dht, persist_dht}; use crate::router::{Router, RouterMessage}; use crate::subnet_service::SyncCommitteeService; @@ -295,6 +296,12 @@ impl NetworkService { } } + let invalid_block_storage = config + .invalid_block_storage + .clone() + .map(InvalidBlockStorage::Enabled) + .unwrap_or(InvalidBlockStorage::Disabled); + // launch derived network services // router task @@ -303,6 +310,7 @@ impl NetworkService { network_globals.clone(), network_senders.network_send(), executor.clone(), + invalid_block_storage, network_log.clone(), )?; diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 633cbf0438..b20b5c0a95 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -1093,4 +1093,13 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { // always using the builder. .conflicts_with("builder-profit-threshold") ) + .arg( + Arg::with_name("invalid-gossip-verified-blocks-path") + .long("invalid-gossip-verified-blocks-path") + .value_name("PATH") + .help("If a block succeeds gossip validation whilst failing full validation, store \ + the block SSZ as a file at this path. This feature is only recommended for \ + developers. This directory is not pruned, users should be careful to avoid \ + filling up their disks.") + ) } diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index f05fea2db1..6f626bee8d 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -793,6 +793,11 @@ pub fn get_config( client_config.chain.enable_backfill_rate_limiting = !cli_args.is_present("disable-backfill-rate-limiting"); + if let Some(path) = clap_utils::parse_optional(cli_args, "invalid-gossip-verified-blocks-path")? + { + client_config.network.invalid_block_storage = Some(path); + } + Ok(client_config) } diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 7e647c904d..75bcccc9de 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -2199,3 +2199,24 @@ fn disable_optimistic_finalized_sync() { assert!(!config.chain.optimistic_finalized_sync); }); } + +#[test] +fn invalid_gossip_verified_blocks_path_default() { + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| assert_eq!(config.network.invalid_block_storage, None)); +} + +#[test] +fn invalid_gossip_verified_blocks_path() { + let path = "/home/karlm/naughty-blocks"; + CommandLineTest::new() + .flag("invalid-gossip-verified-blocks-path", Some(path)) + .run_with_zero_port() + .with_config(|config| { + assert_eq!( + config.network.invalid_block_storage, + Some(PathBuf::from(path)) + ) + }); +} From 7c0b2755c299b8e9f8fe77f31047ff376050b68f Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 15 May 2023 07:22:04 +0000 Subject: [PATCH 03/63] Don't requeue already-known RPC blocks (#4214) ## Issue Addressed NA ## Proposed Changes Adds an additional check to a feature introduced in #4179 to prevent us from re-queuing already-known blocks that could be rejected immediately. ## Additional Info Ideally this would have been included in v4.1.0, however we came across it too late to release it safely. We decided that the safest path forward is to release *without* this check and then patch it in the next version. The lack of this check should only result in a very minor performance impact (the impact is totally negligible in my assessment). --- .../beacon_processor/worker/sync_methods.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/beacon_node/network/src/beacon_processor/worker/sync_methods.rs b/beacon_node/network/src/beacon_processor/worker/sync_methods.rs index ca2095348a..2dbb5a346c 100644 --- a/beacon_node/network/src/beacon_processor/worker/sync_methods.rs +++ b/beacon_node/network/src/beacon_processor/worker/sync_methods.rs @@ -114,10 +114,26 @@ impl Worker { } }; + // Returns `true` if the block is already known to fork choice. Notably, + // this will return `false` for blocks that we've already imported but + // ancestors of the finalized checkpoint. That should not be an issue + // for our use here since finalized blocks will always be late and won't + // be requeued anyway. + let block_is_already_known = || { + self.chain + .canonical_head + .fork_choice_read_lock() + .contains_block(&block_root) + }; + // If we've already seen a block from this proposer *and* the block // arrived before the attestation deadline, requeue it to ensure it is // imported late enough that it won't receive a proposer boost. - if !block_is_late && proposal_already_known() { + // + // Don't requeue blocks if they're already known to fork choice, just + // push them through to block processing so they can be handled through + // the normal channels. + if !block_is_late && proposal_already_known() && !block_is_already_known() { debug!( self.log, "Delaying processing of duplicate RPC block"; From b29bb2e037fc209d1c3b39bcd1523091c7bc3224 Mon Sep 17 00:00:00 2001 From: Jack McPherson Date: Tue, 16 May 2023 01:10:47 +0000 Subject: [PATCH 04/63] Remove redundant gossipsub tests (#4294) ## Issue Addressed #2335 ## Proposed Changes - Remove the `lighthouse-network::tests::gossipsub_tests` module - Remove dead code from the `lighthouse-network::tests::common` helper module (`build_full_mesh`) ## Additional Info After discussion with both @divagant-martian and @AgeManning, these tests seem to have two main issues in that they are: - Redundant, in that they don't test anything meaningful (due to our handling of duplicate messages) - Out-of-place, in that it doesn't really test Lighthouse-specific functionality (rather libp2p functionality) As such, this PR supersedes #4286. --- .../lighthouse_network/tests/common.rs | 30 --- .../tests/gossipsub_tests.rs | 171 ------------------ 2 files changed, 201 deletions(-) delete mode 100644 beacon_node/lighthouse_network/tests/gossipsub_tests.rs diff --git a/beacon_node/lighthouse_network/tests/common.rs b/beacon_node/lighthouse_network/tests/common.rs index d44f20c080..64714cbc0a 100644 --- a/beacon_node/lighthouse_network/tests/common.rs +++ b/beacon_node/lighthouse_network/tests/common.rs @@ -123,36 +123,6 @@ pub fn get_enr(node: &LibP2PService) -> Enr { node.local_enr() } -// Returns `n` libp2p peers in fully connected topology. -#[allow(dead_code)] -/* -pub async fn build_full_mesh( - rt: Weak, - log: slog::Logger, - n: usize, - fork_name: ForkName, -) -> Vec { - let mut nodes = Vec::with_capacity(n); - for _ in 0..n { - nodes.push(build_libp2p_instance(rt.clone(), vec![], log.clone(), fork_name).await); - } - let multiaddrs: Vec = nodes - .iter() - .map(|x| get_enr(x).multiaddr()[1].clone()) - .collect(); - - for (i, node) in nodes.iter_mut().enumerate().take(n) { - for (j, multiaddr) in multiaddrs.iter().enumerate().skip(i) { - if i != j { - match libp2p::Swarm::dial(&mut node.swarm, multiaddr.clone()) { - Ok(()) => debug!(log, "Connected"), - Err(_) => error!(log, "Failed to connect"), - }; - } - } - } - nodes -}*/ // Constructs a pair of nodes with separate loggers. The sender dials the receiver. // This returns a (sender, receiver) pair. #[allow(dead_code)] diff --git a/beacon_node/lighthouse_network/tests/gossipsub_tests.rs b/beacon_node/lighthouse_network/tests/gossipsub_tests.rs deleted file mode 100644 index c5b661cf70..0000000000 --- a/beacon_node/lighthouse_network/tests/gossipsub_tests.rs +++ /dev/null @@ -1,171 +0,0 @@ -/* These are temporarily disabled due to their non-deterministic behaviour and impending update to - * gossipsub 1.1. We leave these here as a template for future test upgrades - - -#![cfg(test)] -use crate::types::GossipEncoding; -use ::types::{BeaconBlock, EthSpec, MinimalEthSpec, Signature, SignedBeaconBlock}; -use lighthouse_network::*; -use slog::{debug, Level}; - -type E = MinimalEthSpec; - -mod common; - -/* Gossipsub tests */ -// Note: The aim of these tests is not to test the robustness of the gossip network -// but to check if the gossipsub implementation is behaving according to the specifications. - -// Test if gossipsub message are forwarded by nodes with a simple linear topology. -// -// Topology used in test -// -// node1 <-> node2 <-> node3 ..... <-> node(n-1) <-> node(n) - -#[tokio::test] -async fn test_gossipsub_forward() { - // set up the logging. The level and enabled or not - let log = common::build_log(Level::Info, false); - - let num_nodes = 20; - let mut nodes = common::build_linear(log.clone(), num_nodes); - let mut received_count = 0; - let spec = E::default_spec(); - let empty_block = BeaconBlock::empty(&spec); - let signed_block = SignedBeaconBlock { - message: empty_block, - signature: Signature::empty_signature(), - }; - let pubsub_message = PubsubMessage::BeaconBlock(Box::new(signed_block)); - let publishing_topic: String = pubsub_message - .topics(GossipEncoding::default(), [0, 0, 0, 0]) - .first() - .unwrap() - .clone() - .into(); - let mut subscribed_count = 0; - let fut = async move { - for node in nodes.iter_mut() { - loop { - match node.next_event().await { - Libp2pEvent::Behaviour(b) => match b { - BehaviourEvent::PubsubMessage { - topics, - message, - source, - id, - } => { - assert_eq!(topics.len(), 1); - // Assert topic is the published topic - assert_eq!( - topics.first().unwrap(), - &TopicHash::from_raw(publishing_topic.clone()) - ); - // Assert message received is the correct one - assert_eq!(message, pubsub_message.clone()); - received_count += 1; - // Since `propagate_message` is false, need to propagate manually - node.swarm.propagate_message(&source, id); - // Test should succeed if all nodes except the publisher receive the message - if received_count == num_nodes - 1 { - debug!(log.clone(), "Received message at {} nodes", num_nodes - 1); - return; - } - } - BehaviourEvent::PeerSubscribed(_, topic) => { - // Publish on beacon block topic - if topic == TopicHash::from_raw(publishing_topic.clone()) { - subscribed_count += 1; - // Every node except the corner nodes are connected to 2 nodes. - if subscribed_count == (num_nodes * 2) - 2 { - node.swarm.publish(vec![pubsub_message.clone()]); - } - } - } - _ => break, - }, - _ => break, - } - } - } - }; - - tokio::select! { - _ = fut => {} - _ = tokio::time::delay_for(tokio::time::Duration::from_millis(800)) => { - panic!("Future timed out"); - } - } -} - -// Test publishing of a message with a full mesh for the topic -// Not very useful but this is the bare minimum functionality. -#[tokio::test] -async fn test_gossipsub_full_mesh_publish() { - // set up the logging. The level and enabled or not - let log = common::build_log(Level::Debug, false); - - // Note: This test does not propagate gossipsub messages. - // Having `num_nodes` > `mesh_n_high` may give inconsistent results - // as nodes may get pruned out of the mesh before the gossipsub message - // is published to them. - let num_nodes = 12; - let mut nodes = common::build_full_mesh(log, num_nodes); - let mut publishing_node = nodes.pop().unwrap(); - let spec = E::default_spec(); - let empty_block = BeaconBlock::empty(&spec); - let signed_block = SignedBeaconBlock { - message: empty_block, - signature: Signature::empty_signature(), - }; - let pubsub_message = PubsubMessage::BeaconBlock(Box::new(signed_block)); - let publishing_topic: String = pubsub_message - .topics(GossipEncoding::default(), [0, 0, 0, 0]) - .first() - .unwrap() - .clone() - .into(); - let mut subscribed_count = 0; - let mut received_count = 0; - let fut = async move { - for node in nodes.iter_mut() { - while let Libp2pEvent::Behaviour(BehaviourEvent::PubsubMessage { - topics, - message, - .. - }) = node.next_event().await - { - assert_eq!(topics.len(), 1); - // Assert topic is the published topic - assert_eq!( - topics.first().unwrap(), - &TopicHash::from_raw(publishing_topic.clone()) - ); - // Assert message received is the correct one - assert_eq!(message, pubsub_message.clone()); - received_count += 1; - if received_count == num_nodes - 1 { - return; - } - } - } - while let Libp2pEvent::Behaviour(BehaviourEvent::PeerSubscribed(_, topic)) = - publishing_node.next_event().await - { - // Publish on beacon block topic - if topic == TopicHash::from_raw(publishing_topic.clone()) { - subscribed_count += 1; - if subscribed_count == num_nodes - 1 { - publishing_node.swarm.publish(vec![pubsub_message.clone()]); - } - } - } - }; - tokio::select! { - _ = fut => {} - _ = tokio::time::delay_for(tokio::time::Duration::from_millis(800)) => { - panic!("Future timed out"); - } - } -} -*/ From 91a7f51ab093ba211f120425efc34123628d35c4 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Wed, 17 May 2023 05:51:54 +0000 Subject: [PATCH 05/63] Post merge local testnets (#3807) ## Issue Addressed N/A ## Proposed Changes Modifies the local testnet scripts to start a network with genesis validators embedded into the genesis state. This allows us to start a local testnet without the need for deploying a deposit contract or depositing validators pre-genesis. This also enables us to start a local test network at any fork we want without going through fork transitions. Also adds scripts to start multiple geth clients and peer them with each other and peer the geth clients with beacon nodes to start a post merge local testnet. ## Additional info Adds a new lcli command `mnemonics-validators` that generates validator directories derived from a given mnemonic. Adds a new `derived-genesis-state` option to the `lcli new-testnet` command to generate a genesis state populated with validators derived from a mnemonic. --- .github/workflows/local-testnet.yml | 26 +- .github/workflows/test-suite.yml | 19 +- Cargo.lock | 2 + lcli/Cargo.toml | 2 + lcli/src/main.rs | 80 +- lcli/src/mnemonic_validators.rs | 104 +++ lcli/src/new_testnet.rs | 269 ++++-- scripts/local_testnet/README.md | 50 +- scripts/local_testnet/beacon_node.sh | 13 +- scripts/local_testnet/el_bootnode.sh | 3 + scripts/local_testnet/genesis.json | 860 +++++++++++++++++++ scripts/local_testnet/geth.sh | 54 ++ scripts/local_testnet/kill_processes.sh | 2 +- scripts/local_testnet/setup.sh | 24 +- scripts/local_testnet/start_local_testnet.sh | 39 +- scripts/local_testnet/validator_client.sh | 1 + scripts/local_testnet/vars.env | 23 +- scripts/tests/doppelganger_protection.sh | 49 +- scripts/tests/genesis.json | 850 ++++++++++++++++++ scripts/tests/vars.env | 25 +- 20 files changed, 2347 insertions(+), 148 deletions(-) create mode 100644 lcli/src/mnemonic_validators.rs create mode 100755 scripts/local_testnet/el_bootnode.sh create mode 100644 scripts/local_testnet/genesis.json create mode 100755 scripts/local_testnet/geth.sh create mode 100644 scripts/tests/genesis.json diff --git a/.github/workflows/local-testnet.yml b/.github/workflows/local-testnet.yml index d4982ae194..9223c40e15 100644 --- a/.github/workflows/local-testnet.yml +++ b/.github/workflows/local-testnet.yml @@ -25,9 +25,23 @@ jobs: uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install anvil - run: cargo install --git https://github.com/foundry-rs/foundry --locked anvil - + - name: Install geth (ubuntu) + if: matrix.os == 'ubuntu-22.04' + run: | + sudo add-apt-repository -y ppa:ethereum/ethereum + sudo apt-get update + sudo apt-get install ethereum + - name: Install geth (mac) + if: matrix.os == 'macos-12' + run: | + brew tap ethereum/ethereum + brew install ethereum + - name: Install GNU sed & GNU grep + if: matrix.os == 'macos-12' + run: | + brew install gnu-sed grep + echo "$(brew --prefix)/opt/gnu-sed/libexec/gnubin" >> $GITHUB_PATH + echo "$(brew --prefix)/opt/grep/libexec/gnubin" >> $GITHUB_PATH # https://github.com/actions/cache/blob/main/examples.md#rust---cargo - uses: actions/cache@v3 id: cache-cargo @@ -44,7 +58,7 @@ jobs: run: make && make install-lcli - name: Start local testnet - run: ./start_local_testnet.sh && sleep 60 + run: ./start_local_testnet.sh genesis.json && sleep 60 working-directory: scripts/local_testnet - name: Print logs @@ -60,7 +74,7 @@ jobs: working-directory: scripts/local_testnet - name: Start local testnet with blinded block production - run: ./start_local_testnet.sh -p && sleep 60 + run: ./start_local_testnet.sh -p genesis.json && sleep 60 working-directory: scripts/local_testnet - name: Print logs for blinded block testnet @@ -69,4 +83,4 @@ jobs: - name: Stop local testnet with blinded block production run: ./stop_local_testnet.sh - working-directory: scripts/local_testnet + working-directory: scripts/local_testnet \ No newline at end of file diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 4fa61e6aaa..32643b147b 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -228,8 +228,6 @@ jobs: uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install anvil - run: cargo install --git https://github.com/foundry-rs/foundry --locked anvil - name: Run the beacon chain sim without an eth1 connection run: cargo run --release --bin simulator no-eth1-sim syncing-simulator-ubuntu: @@ -260,20 +258,23 @@ jobs: uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install anvil - run: cargo install --git https://github.com/foundry-rs/foundry --locked anvil + - name: Install geth + run: | + sudo add-apt-repository -y ppa:ethereum/ethereum + sudo apt-get update + sudo apt-get install ethereum - name: Install lighthouse and lcli run: | make make install-lcli - - name: Run the doppelganger protection success test script - run: | - cd scripts/tests - ./doppelganger_protection.sh success - name: Run the doppelganger protection failure test script run: | cd scripts/tests - ./doppelganger_protection.sh failure + ./doppelganger_protection.sh failure genesis.json + - name: Run the doppelganger protection success test script + run: | + cd scripts/tests + ./doppelganger_protection.sh success genesis.json execution-engine-integration-ubuntu: name: execution-engine-integration-ubuntu runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index bd70ad0201..d6e21d4d22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3947,6 +3947,7 @@ dependencies = [ "eth2", "eth2_network_config", "eth2_wallet", + "ethereum_hashing", "ethereum_ssz", "genesis", "int_to_bytes", @@ -3954,6 +3955,7 @@ dependencies = [ "lighthouse_version", "log", "malloc_utils", + "rayon", "sensitive_url", "serde", "serde_json", diff --git a/lcli/Cargo.toml b/lcli/Cargo.toml index af8df1b6b0..3d875f54bb 100644 --- a/lcli/Cargo.toml +++ b/lcli/Cargo.toml @@ -21,6 +21,7 @@ env_logger = "0.9.0" types = { path = "../consensus/types" } state_processing = { path = "../consensus/state_processing" } int_to_bytes = { path = "../consensus/int_to_bytes" } +ethereum_hashing = "1.0.0-beta.2" ethereum_ssz = "0.5.0" environment = { path = "../lighthouse/environment" } eth2_network_config = { path = "../common/eth2_network_config" } @@ -41,6 +42,7 @@ snap = "1.0.1" beacon_chain = { path = "../beacon_node/beacon_chain" } store = { path = "../beacon_node/store" } malloc_utils = { path = "../common/malloc_utils" } +rayon = "1.7.0" [package.metadata.cargo-udeps.ignore] normal = ["malloc_utils"] diff --git a/lcli/src/main.rs b/lcli/src/main.rs index eeb098f04d..bc39c34e26 100644 --- a/lcli/src/main.rs +++ b/lcli/src/main.rs @@ -10,6 +10,7 @@ mod generate_bootnode_enr; mod indexed_attestations; mod insecure_validators; mod interop_genesis; +mod mnemonic_validators; mod new_testnet; mod parse_ssz; mod replace_state_pubkeys; @@ -449,6 +450,22 @@ fn main() { "If present, a interop-style genesis.ssz file will be generated.", ), ) + .arg( + Arg::with_name("derived-genesis-state") + .long("derived-genesis-state") + .takes_value(false) + .help( + "If present, a genesis.ssz file will be generated with keys generated from a given mnemonic.", + ), + ) + .arg( + Arg::with_name("mnemonic-phrase") + .long("mnemonic-phrase") + .value_name("MNEMONIC_PHRASE") + .takes_value(true) + .requires("derived-genesis-state") + .help("The mnemonic with which we generate the validator keys for a derived genesis state"), + ) .arg( Arg::with_name("min-genesis-time") .long("min-genesis-time") @@ -568,14 +585,32 @@ fn main() { ), ) .arg( - Arg::with_name("merge-fork-epoch") - .long("merge-fork-epoch") + Arg::with_name("bellatrix-fork-epoch") + .long("bellatrix-fork-epoch") .value_name("EPOCH") .takes_value(true) .help( "The epoch at which to enable the Merge hard fork", ), ) + .arg( + Arg::with_name("capella-fork-epoch") + .long("capella-fork-epoch") + .value_name("EPOCH") + .takes_value(true) + .help( + "The epoch at which to enable the Capella hard fork", + ), + ) + .arg( + Arg::with_name("ttd") + .long("ttd") + .value_name("TTD") + .takes_value(true) + .help( + "The terminal total difficulty", + ), + ) .arg( Arg::with_name("eth1-block-hash") .long("eth1-block-hash") @@ -695,6 +730,7 @@ fn main() { .long("count") .value_name("COUNT") .takes_value(true) + .required(true) .help("Produces validators in the range of 0..count."), ) .arg( @@ -702,6 +738,7 @@ fn main() { .long("base-dir") .value_name("BASE_DIR") .takes_value(true) + .required(true) .help("The base directory where validator keypairs and secrets are stored"), ) .arg( @@ -712,6 +749,43 @@ fn main() { .help("The number of nodes to divide the validator keys to"), ) ) + .subcommand( + SubCommand::with_name("mnemonic-validators") + .about("Produces validator directories by deriving the keys from \ + a mnemonic. For testing purposes only, DO NOT USE IN \ + PRODUCTION!") + .arg( + Arg::with_name("count") + .long("count") + .value_name("COUNT") + .takes_value(true) + .required(true) + .help("Produces validators in the range of 0..count."), + ) + .arg( + Arg::with_name("base-dir") + .long("base-dir") + .value_name("BASE_DIR") + .takes_value(true) + .required(true) + .help("The base directory where validator keypairs and secrets are stored"), + ) + .arg( + Arg::with_name("node-count") + .long("node-count") + .value_name("NODE_COUNT") + .takes_value(true) + .help("The number of nodes to divide the validator keys to"), + ) + .arg( + Arg::with_name("mnemonic-phrase") + .long("mnemonic-phrase") + .value_name("MNEMONIC_PHRASE") + .takes_value(true) + .required(true) + .help("The mnemonic with which we generate the validator keys"), + ) + ) .subcommand( SubCommand::with_name("indexed-attestations") .about("Convert attestations to indexed form, using the committees from a state.") @@ -853,6 +927,8 @@ fn run( .map_err(|e| format!("Failed to run generate-bootnode-enr command: {}", e)), ("insecure-validators", Some(matches)) => insecure_validators::run(matches) .map_err(|e| format!("Failed to run insecure-validators command: {}", e)), + ("mnemonic-validators", Some(matches)) => mnemonic_validators::run(matches) + .map_err(|e| format!("Failed to run mnemonic-validators command: {}", e)), ("indexed-attestations", Some(matches)) => indexed_attestations::run::(matches) .map_err(|e| format!("Failed to run indexed-attestations command: {}", e)), ("block-root", Some(matches)) => block_root::run::(env, matches) diff --git a/lcli/src/mnemonic_validators.rs b/lcli/src/mnemonic_validators.rs new file mode 100644 index 0000000000..2653aee149 --- /dev/null +++ b/lcli/src/mnemonic_validators.rs @@ -0,0 +1,104 @@ +use account_utils::eth2_keystore::{keypair_from_secret, Keystore, KeystoreBuilder}; +use account_utils::random_password; +use clap::ArgMatches; +use eth2_wallet::bip39::Seed; +use eth2_wallet::bip39::{Language, Mnemonic}; +use eth2_wallet::{recover_validator_secret_from_mnemonic, KeyType}; +use rayon::prelude::*; +use std::fs; +use std::path::PathBuf; +use validator_dir::Builder as ValidatorBuilder; + +/// Generates validator directories with keys derived from the given mnemonic. +pub fn generate_validator_dirs( + indices: &[usize], + mnemonic_phrase: &str, + validators_dir: PathBuf, + secrets_dir: PathBuf, +) -> Result<(), String> { + if !validators_dir.exists() { + fs::create_dir_all(&validators_dir) + .map_err(|e| format!("Unable to create validators dir: {:?}", e))?; + } + + if !secrets_dir.exists() { + fs::create_dir_all(&secrets_dir) + .map_err(|e| format!("Unable to create secrets dir: {:?}", e))?; + } + let mnemonic = Mnemonic::from_phrase(mnemonic_phrase, Language::English).map_err(|e| { + format!( + "Unable to derive mnemonic from string {:?}: {:?}", + mnemonic_phrase, e + ) + })?; + + let seed = Seed::new(&mnemonic, ""); + + let _: Vec<_> = indices + .par_iter() + .map(|index| { + let voting_password = random_password(); + + let derive = |key_type: KeyType, password: &[u8]| -> Result { + let (secret, path) = recover_validator_secret_from_mnemonic( + seed.as_bytes(), + *index as u32, + key_type, + ) + .map_err(|e| format!("Unable to recover validator keys: {:?}", e))?; + + let keypair = keypair_from_secret(secret.as_bytes()) + .map_err(|e| format!("Unable build keystore: {:?}", e))?; + + KeystoreBuilder::new(&keypair, password, format!("{}", path)) + .map_err(|e| format!("Unable build keystore: {:?}", e))? + .build() + .map_err(|e| format!("Unable build keystore: {:?}", e)) + }; + + let voting_keystore = derive(KeyType::Voting, voting_password.as_bytes()).unwrap(); + + println!("Validator {}", index + 1); + + ValidatorBuilder::new(validators_dir.clone()) + .password_dir(secrets_dir.clone()) + .store_withdrawal_keystore(false) + .voting_keystore(voting_keystore, voting_password.as_bytes()) + .build() + .map_err(|e| format!("Unable to build validator: {:?}", e)) + .unwrap() + }) + .collect(); + + Ok(()) +} + +pub fn run(matches: &ArgMatches) -> Result<(), String> { + let validator_count: usize = clap_utils::parse_required(matches, "count")?; + let base_dir: PathBuf = clap_utils::parse_required(matches, "base-dir")?; + let node_count: Option = clap_utils::parse_optional(matches, "node-count")?; + let mnemonic_phrase: String = clap_utils::parse_required(matches, "mnemonic-phrase")?; + if let Some(node_count) = node_count { + let validators_per_node = validator_count / node_count; + let validator_range = (0..validator_count).collect::>(); + let indices_range = validator_range + .chunks(validators_per_node) + .collect::>(); + + for (i, indices) in indices_range.iter().enumerate() { + let validators_dir = base_dir.join(format!("node_{}", i + 1)).join("validators"); + let secrets_dir = base_dir.join(format!("node_{}", i + 1)).join("secrets"); + generate_validator_dirs(indices, &mnemonic_phrase, validators_dir, secrets_dir)?; + } + } else { + let validators_dir = base_dir.join("validators"); + let secrets_dir = base_dir.join("secrets"); + generate_validator_dirs( + (0..validator_count).collect::>().as_slice(), + &mnemonic_phrase, + validators_dir, + secrets_dir, + )?; + } + Ok(()) +} diff --git a/lcli/src/new_testnet.rs b/lcli/src/new_testnet.rs index 5af22731f3..aa5f52eef8 100644 --- a/lcli/src/new_testnet.rs +++ b/lcli/src/new_testnet.rs @@ -1,16 +1,26 @@ +use account_utils::eth2_keystore::keypair_from_secret; use clap::ArgMatches; use clap_utils::{parse_optional, parse_required, parse_ssz_optional}; use eth2_network_config::Eth2NetworkConfig; -use genesis::interop_genesis_state; +use eth2_wallet::bip39::Seed; +use eth2_wallet::bip39::{Language, Mnemonic}; +use eth2_wallet::{recover_validator_secret_from_mnemonic, KeyType}; +use ethereum_hashing::hash; use ssz::Decode; use ssz::Encode; +use state_processing::process_activations; +use state_processing::upgrade::{upgrade_to_altair, upgrade_to_bellatrix}; use std::fs::File; use std::io::Read; use std::path::PathBuf; +use std::str::FromStr; use std::time::{SystemTime, UNIX_EPOCH}; +use types::ExecutionBlockHash; use types::{ - test_utils::generate_deterministic_keypairs, Address, Config, Epoch, EthSpec, - ExecutionPayloadHeader, ExecutionPayloadHeaderCapella, ExecutionPayloadHeaderMerge, ForkName, + test_utils::generate_deterministic_keypairs, Address, BeaconState, ChainSpec, Config, Epoch, + Eth1Data, EthSpec, ExecutionPayloadHeader, ExecutionPayloadHeaderCapella, + ExecutionPayloadHeaderMerge, ExecutionPayloadHeaderRefMut, ForkName, Hash256, Keypair, + PublicKey, Validator, }; pub fn run(testnet_dir_path: PathBuf, matches: &ArgMatches) -> Result<(), String> { @@ -67,63 +77,69 @@ pub fn run(testnet_dir_path: PathBuf, matches: &ArgMatches) -> Resul spec.altair_fork_epoch = Some(fork_epoch); } - if let Some(fork_epoch) = parse_optional(matches, "merge-fork-epoch")? { + if let Some(fork_epoch) = parse_optional(matches, "bellatrix-fork-epoch")? { spec.bellatrix_fork_epoch = Some(fork_epoch); } - let genesis_state_bytes = if matches.is_present("interop-genesis-state") { - let execution_payload_header: Option> = - parse_optional(matches, "execution-payload-header")? - .map(|filename: String| { - let mut bytes = vec![]; - let mut file = File::open(filename.as_str()) - .map_err(|e| format!("Unable to open {}: {}", filename, e))?; - file.read_to_end(&mut bytes) - .map_err(|e| format!("Unable to read {}: {}", filename, e))?; - let fork_name = spec.fork_name_at_epoch(Epoch::new(0)); - match fork_name { - ForkName::Base | ForkName::Altair => Err(ssz::DecodeError::BytesInvalid( - "genesis fork must be post-merge".to_string(), - )), - ForkName::Merge => { - ExecutionPayloadHeaderMerge::::from_ssz_bytes(bytes.as_slice()) - .map(ExecutionPayloadHeader::Merge) - } - ForkName::Capella => { - ExecutionPayloadHeaderCapella::::from_ssz_bytes(bytes.as_slice()) - .map(ExecutionPayloadHeader::Capella) - } + if let Some(fork_epoch) = parse_optional(matches, "capella-fork-epoch")? { + spec.capella_fork_epoch = Some(fork_epoch); + } + + if let Some(ttd) = parse_optional(matches, "ttd")? { + spec.terminal_total_difficulty = ttd; + } + + let validator_count = parse_required(matches, "validator-count")?; + let execution_payload_header: Option> = + parse_optional(matches, "execution-payload-header")? + .map(|filename: String| { + let mut bytes = vec![]; + let mut file = File::open(filename.as_str()) + .map_err(|e| format!("Unable to open {}: {}", filename, e))?; + file.read_to_end(&mut bytes) + .map_err(|e| format!("Unable to read {}: {}", filename, e))?; + let fork_name = spec.fork_name_at_epoch(Epoch::new(0)); + match fork_name { + ForkName::Base | ForkName::Altair => Err(ssz::DecodeError::BytesInvalid( + "genesis fork must be post-merge".to_string(), + )), + ForkName::Merge => { + ExecutionPayloadHeaderMerge::::from_ssz_bytes(bytes.as_slice()) + .map(ExecutionPayloadHeader::Merge) } - .map_err(|e| format!("SSZ decode failed: {:?}", e)) - }) - .transpose()?; + ForkName::Capella => { + ExecutionPayloadHeaderCapella::::from_ssz_bytes(bytes.as_slice()) + .map(ExecutionPayloadHeader::Capella) + } + } + .map_err(|e| format!("SSZ decode failed: {:?}", e)) + }) + .transpose()?; - let (eth1_block_hash, genesis_time) = if let Some(payload) = - execution_payload_header.as_ref() - { - let eth1_block_hash = - parse_optional(matches, "eth1-block-hash")?.unwrap_or_else(|| payload.block_hash()); - let genesis_time = - parse_optional(matches, "genesis-time")?.unwrap_or_else(|| payload.timestamp()); - (eth1_block_hash, genesis_time) - } else { - let eth1_block_hash = parse_required(matches, "eth1-block-hash").map_err(|_| { - "One of `--execution-payload-header` or `--eth1-block-hash` must be set".to_string() - })?; - let genesis_time = parse_optional(matches, "genesis-time")?.unwrap_or( - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_err(|e| format!("Unable to get time: {:?}", e))? - .as_secs(), - ); - (eth1_block_hash, genesis_time) - }; - - let validator_count = parse_required(matches, "validator-count")?; + let (eth1_block_hash, genesis_time) = if let Some(payload) = execution_payload_header.as_ref() { + let eth1_block_hash = + parse_optional(matches, "eth1-block-hash")?.unwrap_or_else(|| payload.block_hash()); + let genesis_time = + parse_optional(matches, "genesis-time")?.unwrap_or_else(|| payload.timestamp()); + (eth1_block_hash, genesis_time) + } else { + let eth1_block_hash = parse_required(matches, "eth1-block-hash").map_err(|_| { + "One of `--execution-payload-header` or `--eth1-block-hash` must be set".to_string() + })?; + let genesis_time = parse_optional(matches, "genesis-time")?.unwrap_or( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|e| format!("Unable to get time: {:?}", e))? + .as_secs(), + ); + (eth1_block_hash, genesis_time) + }; + let genesis_state_bytes = if matches.is_present("interop-genesis-state") { let keypairs = generate_deterministic_keypairs(validator_count); + let keypairs: Vec<_> = keypairs.into_iter().map(|kp| (kp.clone(), kp)).collect(); - let genesis_state = interop_genesis_state::( + let genesis_state = initialize_state_with_validators::( &keypairs, genesis_time, eth1_block_hash.into_root(), @@ -131,6 +147,41 @@ pub fn run(testnet_dir_path: PathBuf, matches: &ArgMatches) -> Resul &spec, )?; + Some(genesis_state.as_ssz_bytes()) + } else if matches.is_present("derived-genesis-state") { + let mnemonic_phrase: String = clap_utils::parse_required(matches, "mnemonic-phrase")?; + let mnemonic = Mnemonic::from_phrase(&mnemonic_phrase, Language::English).map_err(|e| { + format!( + "Unable to derive mnemonic from string {:?}: {:?}", + mnemonic_phrase, e + ) + })?; + let seed = Seed::new(&mnemonic, ""); + let keypairs = (0..validator_count as u32) + .map(|index| { + let (secret, _) = + recover_validator_secret_from_mnemonic(seed.as_bytes(), index, KeyType::Voting) + .unwrap(); + + let voting_keypair = keypair_from_secret(secret.as_bytes()).unwrap(); + + let (secret, _) = recover_validator_secret_from_mnemonic( + seed.as_bytes(), + index, + KeyType::Withdrawal, + ) + .unwrap(); + let withdrawal_keypair = keypair_from_secret(secret.as_bytes()).unwrap(); + (voting_keypair, withdrawal_keypair) + }) + .collect::>(); + let genesis_state = initialize_state_with_validators::( + &keypairs, + genesis_time, + eth1_block_hash.into_root(), + execution_payload_header, + &spec, + )?; Some(genesis_state.as_ssz_bytes()) } else { None @@ -145,3 +196,117 @@ pub fn run(testnet_dir_path: PathBuf, matches: &ArgMatches) -> Resul testnet.write_to_file(testnet_dir_path, overwrite_files) } + +/// Returns a `BeaconState` with the given validator keypairs embedded into the +/// genesis state. This allows us to start testnets without having to deposit validators +/// manually. +/// +/// The optional `execution_payload_header` allows us to start a network from the bellatrix +/// fork without the need to transition to altair and bellatrix. +/// +/// We need to ensure that `eth1_block_hash` is equal to the genesis block hash that is +/// generated from the execution side `genesis.json`. +fn initialize_state_with_validators( + keypairs: &[(Keypair, Keypair)], // Voting and Withdrawal keypairs + genesis_time: u64, + eth1_block_hash: Hash256, + execution_payload_header: Option>, + spec: &ChainSpec, +) -> Result, String> { + // If no header is provided, then start from a Bellatrix state by default + let default_header: ExecutionPayloadHeader = + ExecutionPayloadHeader::Merge(ExecutionPayloadHeaderMerge { + block_hash: ExecutionBlockHash::from_root(eth1_block_hash), + parent_hash: ExecutionBlockHash::zero(), + ..ExecutionPayloadHeaderMerge::default() + }); + let execution_payload_header = execution_payload_header.unwrap_or(default_header); + // Empty eth1 data + let eth1_data = Eth1Data { + block_hash: eth1_block_hash, + deposit_count: 0, + deposit_root: Hash256::from_str( + "0xd70a234731285c6804c2a4f56711ddb8c82c99740f207854891028af34e27e5e", + ) + .unwrap(), // empty deposit tree root + }; + let mut state = BeaconState::new(genesis_time, eth1_data, spec); + + // Seed RANDAO with Eth1 entropy + state.fill_randao_mixes_with(eth1_block_hash); + + for keypair in keypairs.iter() { + let withdrawal_credentials = |pubkey: &PublicKey| { + let mut credentials = hash(&pubkey.as_ssz_bytes()); + credentials[0] = spec.bls_withdrawal_prefix_byte; + Hash256::from_slice(&credentials) + }; + let amount = spec.max_effective_balance; + // Create a new validator. + let validator = Validator { + pubkey: keypair.0.pk.clone().into(), + withdrawal_credentials: withdrawal_credentials(&keypair.1.pk), + activation_eligibility_epoch: spec.far_future_epoch, + activation_epoch: spec.far_future_epoch, + exit_epoch: spec.far_future_epoch, + withdrawable_epoch: spec.far_future_epoch, + effective_balance: std::cmp::min( + amount - amount % (spec.effective_balance_increment), + spec.max_effective_balance, + ), + slashed: false, + }; + state.validators_mut().push(validator).unwrap(); + state.balances_mut().push(amount).unwrap(); + } + + process_activations(&mut state, spec).unwrap(); + + if spec + .altair_fork_epoch + .map_or(false, |fork_epoch| fork_epoch == T::genesis_epoch()) + { + upgrade_to_altair(&mut state, spec).unwrap(); + + state.fork_mut().previous_version = spec.altair_fork_version; + } + + // Similarly, perform an upgrade to the merge if configured from genesis. + if spec + .bellatrix_fork_epoch + .map_or(false, |fork_epoch| fork_epoch == T::genesis_epoch()) + { + upgrade_to_bellatrix(&mut state, spec).unwrap(); + + // Remove intermediate Altair fork from `state.fork`. + state.fork_mut().previous_version = spec.bellatrix_fork_version; + + // Override latest execution payload header. + // See https://github.com/ethereum/consensus-specs/blob/v1.1.0/specs/merge/beacon-chain.md#testing + + // Currently, we only support starting from a bellatrix state + match state + .latest_execution_payload_header_mut() + .map_err(|e| format!("Failed to get execution payload header: {:?}", e))? + { + ExecutionPayloadHeaderRefMut::Merge(header_mut) => { + if let ExecutionPayloadHeader::Merge(eph) = execution_payload_header { + *header_mut = eph; + } else { + return Err("Execution payload header must be a bellatrix header".to_string()); + } + } + ExecutionPayloadHeaderRefMut::Capella(_) => { + return Err("Cannot start genesis from a capella state".to_string()) + } + } + } + + // Now that we have our validators, initialize the caches (including the committees) + state.build_all_caches(spec).unwrap(); + + // Set genesis validators root for domain separation and chain versioning + *state.genesis_validators_root_mut() = state.update_validators_tree_hash_cache().unwrap(); + + Ok(state) +} diff --git a/scripts/local_testnet/README.md b/scripts/local_testnet/README.md index 6a8a05f9cd..f261ea67fd 100644 --- a/scripts/local_testnet/README.md +++ b/scripts/local_testnet/README.md @@ -1,11 +1,16 @@ # Simple Local Testnet -These scripts allow for running a small local testnet with multiple beacon nodes and validator clients. +These scripts allow for running a small local testnet with multiple beacon nodes and validator clients and a geth execution client. This setup can be useful for testing and development. ## Requirements -The scripts require `lcli` and `lighthouse` to be installed on `PATH`. From the +The scripts require `lcli`, `lighthouse`, `geth`, `bootnode` to be installed on `PATH`. + + +MacOS users need to install GNU `sed` and GNU `grep`, and add them both to `PATH` as well. + +From the root of this repository, run: ```bash @@ -17,17 +22,23 @@ make install-lcli Modify `vars.env` as desired. -Start a local eth1 anvil server plus boot node along with `BN_COUNT` -number of beacon nodes and `VC_COUNT` validator clients. +The testnet starts with a post-merge genesis state. +Start a consensus layer and execution layer boot node along with `BN_COUNT` +number of beacon nodes each connected to a geth execution client and `VC_COUNT` validator clients. + +The `start_local_testnet.sh` script takes four options `-v VC_COUNT`, `-d DEBUG_LEVEL`, `-p` to enable builder proposals and `-h` for help. It also takes a mandatory `GENESIS_FILE` for initialising geth's state. +A sample `genesis.json` is provided in this directory. + +The `ETH1_BLOCK_HASH` environment variable is set to the block_hash of the genesis execution layer block which depends on the contents of `genesis.json`. Users of these scripts need to ensure that the `ETH1_BLOCK_HASH` variable is updated if genesis file is modified. -The `start_local_testnet.sh` script takes four options `-v VC_COUNT`, `-d DEBUG_LEVEL`, `-p` to enable builder proposals and `-h` for help. The options may be in any order or absent in which case they take the default value specified. - VC_COUNT: the number of validator clients to create, default: `BN_COUNT` - DEBUG_LEVEL: one of { error, warn, info, debug, trace }, default: `info` + ```bash -./start_local_testnet.sh +./start_local_testnet.sh genesis.json ``` ## Stopping the testnet @@ -41,31 +52,38 @@ This is not necessary before `start_local_testnet.sh` as it invokes `stop_local_ These scripts are used by ./start_local_testnet.sh and may be used to manually -Start a local eth1 anvil server -```bash -./anvil_test_node.sh -``` - -Assuming you are happy with the configuration in `vars.env`, deploy the deposit contract, make deposits, -create the testnet directory, genesis state and validator keys with: +Assuming you are happy with the configuration in `vars.env`, +create the testnet directory, genesis state with embedded validators and validator keys with: ```bash ./setup.sh ``` -Generate bootnode enr and start a discv5 bootnode so that multiple beacon nodes can find each other +Note: The generated genesis validators are embedded into the genesis state as genesis validators and hence do not require manual deposits to activate. + +Generate bootnode enr and start an EL and CL bootnode so that multiple nodes can find each other ```bash ./bootnode.sh +./el_bootnode.sh +``` + +Start a geth node: +```bash +./geth.sh +``` +e.g. +```bash +./geth.sh $HOME/.lighthouse/local-testnet/geth_1 5000 6000 7000 genesis.json ``` Start a beacon node: ```bash -./beacon_node.sh +./beacon_node.sh ``` e.g. ```bash -./beacon_node.sh $HOME/.lighthouse/local-testnet/node_1 9000 8000 +./beacon_node.sh $HOME/.lighthouse/local-testnet/node_1 9000 8000 http://localhost:6000 ~/.lighthouse/local-testnet/geth_1/geth/jwtsecret ``` In a new terminal, start the validator client which will attach to the first diff --git a/scripts/local_testnet/beacon_node.sh b/scripts/local_testnet/beacon_node.sh index ac61b54dfb..1a04d12d4a 100755 --- a/scripts/local_testnet/beacon_node.sh +++ b/scripts/local_testnet/beacon_node.sh @@ -30,6 +30,8 @@ while getopts "d:sh" flag; do echo " DATADIR Value for --datadir parameter" echo " NETWORK-PORT Value for --enr-udp-port, --enr-tcp-port and --port" echo " HTTP-PORT Value for --http-port" + echo " EXECUTION-ENDPOINT Value for --execution-endpoint" + echo " EXECUTION-JWT Value for --execution-jwt" exit ;; esac @@ -39,14 +41,19 @@ done data_dir=${@:$OPTIND+0:1} network_port=${@:$OPTIND+1:1} http_port=${@:$OPTIND+2:1} +execution_endpoint=${@:$OPTIND+3:1} +execution_jwt=${@:$OPTIND+4:1} -exec lighthouse \ +lighthouse_binary=lighthouse + +exec $lighthouse_binary \ --debug-level $DEBUG_LEVEL \ bn \ $SUBSCRIBE_ALL_SUBNETS \ --datadir $data_dir \ --testnet-dir $TESTNET_DIR \ --enable-private-discovery \ + --disable-peer-scoring \ --staking \ --enr-address 127.0.0.1 \ --enr-udp-port $network_port \ @@ -54,4 +61,6 @@ exec lighthouse \ --port $network_port \ --http-port $http_port \ --disable-packet-filter \ - --target-peers $((BN_COUNT - 1)) + --target-peers $((BN_COUNT - 1)) \ + --execution-endpoint $execution_endpoint \ + --execution-jwt $execution_jwt diff --git a/scripts/local_testnet/el_bootnode.sh b/scripts/local_testnet/el_bootnode.sh new file mode 100755 index 0000000000..d73a463f6d --- /dev/null +++ b/scripts/local_testnet/el_bootnode.sh @@ -0,0 +1,3 @@ +priv_key="02fd74636e96a8ffac8e7b01b0de8dea94d6bcf4989513b38cf59eb32163ff91" +source ./vars.env +$EL_BOOTNODE_BINARY --nodekeyhex $priv_key \ No newline at end of file diff --git a/scripts/local_testnet/genesis.json b/scripts/local_testnet/genesis.json new file mode 100644 index 0000000000..f92a5f5d00 --- /dev/null +++ b/scripts/local_testnet/genesis.json @@ -0,0 +1,860 @@ +{ + "config": { + "chainId": 4242, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "mergeNetsplitBlock": 0, + "shanghaiTime": 0, + "terminalTotalDifficulty": 0 + }, + "alloc": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0x6d6172697573766477000000" + }, + "0x7b8C3a386C0eea54693fFB0DA17373ffC9228139": { + "balance": "10000000000000000000000000" + }, + "0xdA2DD7560DB7e212B945fC72cEB54B7D8C886D77": { + "balance": "10000000000000000000000000" + }, + "0x0000000000000000000000000000000000000000": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000001": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000002": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000003": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000004": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000005": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000006": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000007": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000008": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000009": { + "balance": "1" + }, + "0x000000000000000000000000000000000000000a": { + "balance": "1" + }, + "0x000000000000000000000000000000000000000b": { + "balance": "1" + }, + "0x000000000000000000000000000000000000000c": { + "balance": "1" + }, + "0x000000000000000000000000000000000000000d": { + "balance": "1" + }, + "0x000000000000000000000000000000000000000e": { + "balance": "1" + }, + "0x000000000000000000000000000000000000000f": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000010": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000011": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000012": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000013": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000014": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000015": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000016": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000017": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000018": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000019": { + "balance": "1" + }, + "0x000000000000000000000000000000000000001a": { + "balance": "1" + }, + "0x000000000000000000000000000000000000001b": { + "balance": "1" + }, + "0x000000000000000000000000000000000000001c": { + "balance": "1" + }, + "0x000000000000000000000000000000000000001d": { + "balance": "1" + }, + "0x000000000000000000000000000000000000001e": { + "balance": "1" + }, + "0x000000000000000000000000000000000000001f": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000020": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000021": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000022": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000023": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000024": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000025": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000026": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000027": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000028": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000029": { + "balance": "1" + }, + "0x000000000000000000000000000000000000002a": { + "balance": "1" + }, + "0x000000000000000000000000000000000000002b": { + "balance": "1" + }, + "0x000000000000000000000000000000000000002c": { + "balance": "1" + }, + "0x000000000000000000000000000000000000002d": { + "balance": "1" + }, + "0x000000000000000000000000000000000000002e": { + "balance": "1" + }, + "0x000000000000000000000000000000000000002f": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000030": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000031": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000032": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000033": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000034": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000035": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000036": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000037": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000038": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000039": { + "balance": "1" + }, + "0x000000000000000000000000000000000000003a": { + "balance": "1" + }, + "0x000000000000000000000000000000000000003b": { + "balance": "1" + }, + "0x000000000000000000000000000000000000003c": { + "balance": "1" + }, + "0x000000000000000000000000000000000000003d": { + "balance": "1" + }, + "0x000000000000000000000000000000000000003e": { + "balance": "1" + }, + "0x000000000000000000000000000000000000003f": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000040": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000041": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000042": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000043": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000044": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000045": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000046": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000047": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000048": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000049": { + "balance": "1" + }, + "0x000000000000000000000000000000000000004a": { + "balance": "1" + }, + "0x000000000000000000000000000000000000004b": { + "balance": "1" + }, + "0x000000000000000000000000000000000000004c": { + "balance": "1" + }, + "0x000000000000000000000000000000000000004d": { + "balance": "1" + }, + "0x000000000000000000000000000000000000004e": { + "balance": "1" + }, + "0x000000000000000000000000000000000000004f": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000050": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000051": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000052": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000053": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000054": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000055": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000056": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000057": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000058": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000059": { + "balance": "1" + }, + "0x000000000000000000000000000000000000005a": { + "balance": "1" + }, + "0x000000000000000000000000000000000000005b": { + "balance": "1" + }, + "0x000000000000000000000000000000000000005c": { + "balance": "1" + }, + "0x000000000000000000000000000000000000005d": { + "balance": "1" + }, + "0x000000000000000000000000000000000000005e": { + "balance": "1" + }, + "0x000000000000000000000000000000000000005f": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000060": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000061": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000062": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000063": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000064": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000065": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000066": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000067": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000068": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000069": { + "balance": "1" + }, + "0x000000000000000000000000000000000000006a": { + "balance": "1" + }, + "0x000000000000000000000000000000000000006b": { + "balance": "1" + }, + "0x000000000000000000000000000000000000006c": { + "balance": "1" + }, + "0x000000000000000000000000000000000000006d": { + "balance": "1" + }, + "0x000000000000000000000000000000000000006e": { + "balance": "1" + }, + "0x000000000000000000000000000000000000006f": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000070": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000071": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000072": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000073": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000074": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000075": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000076": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000077": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000078": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000079": { + "balance": "1" + }, + "0x000000000000000000000000000000000000007a": { + "balance": "1" + }, + "0x000000000000000000000000000000000000007b": { + "balance": "1" + }, + "0x000000000000000000000000000000000000007c": { + "balance": "1" + }, + "0x000000000000000000000000000000000000007d": { + "balance": "1" + }, + "0x000000000000000000000000000000000000007e": { + "balance": "1" + }, + "0x000000000000000000000000000000000000007f": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000080": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000081": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000082": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000083": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000084": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000085": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000086": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000087": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000088": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000089": { + "balance": "1" + }, + "0x000000000000000000000000000000000000008a": { + "balance": "1" + }, + "0x000000000000000000000000000000000000008b": { + "balance": "1" + }, + "0x000000000000000000000000000000000000008c": { + "balance": "1" + }, + "0x000000000000000000000000000000000000008d": { + "balance": "1" + }, + "0x000000000000000000000000000000000000008e": { + "balance": "1" + }, + "0x000000000000000000000000000000000000008f": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000090": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000091": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000092": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000093": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000094": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000095": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000096": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000097": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000098": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000099": { + "balance": "1" + }, + "0x000000000000000000000000000000000000009a": { + "balance": "1" + }, + "0x000000000000000000000000000000000000009b": { + "balance": "1" + }, + "0x000000000000000000000000000000000000009c": { + "balance": "1" + }, + "0x000000000000000000000000000000000000009d": { + "balance": "1" + }, + "0x000000000000000000000000000000000000009e": { + "balance": "1" + }, + "0x000000000000000000000000000000000000009f": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000a0": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000a1": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000a2": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000a3": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000a4": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000a5": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000a6": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000a7": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000a8": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000a9": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000aa": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ab": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ac": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ad": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ae": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000af": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000b0": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000b1": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000b2": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000b3": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000b4": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000b5": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000b6": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000b7": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000b8": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000b9": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ba": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000bb": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000bc": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000bd": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000be": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000bf": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000c0": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000c1": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000c2": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000c3": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000c4": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000c5": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000c6": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000c7": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000c8": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000c9": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ca": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000cb": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000cc": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000cd": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ce": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000cf": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000d0": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000d1": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000d2": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000d3": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000d4": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000d5": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000d6": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000d7": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000d8": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000d9": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000da": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000db": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000dc": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000dd": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000de": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000df": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000e0": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000e1": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000e2": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000e3": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000e4": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000e5": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000e6": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000e7": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000e8": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000e9": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ea": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000eb": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ec": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ed": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ee": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ef": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000f0": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000f1": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000f2": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000f3": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000f4": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000f5": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000f6": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000f7": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000f8": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000f9": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000fa": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000fb": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000fc": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000fd": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000fe": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ff": { + "balance": "1" + }, + "0x4242424242424242424242424242424242424242": { + "balance": "0", + "code": "0x60806040526004361061003f5760003560e01c806301ffc9a71461004457806322895118146100a4578063621fd130146101ba578063c5f2892f14610244575b600080fd5b34801561005057600080fd5b506100906004803603602081101561006757600080fd5b50357fffffffff000000000000000000000000000000000000000000000000000000001661026b565b604080519115158252519081900360200190f35b6101b8600480360360808110156100ba57600080fd5b8101906020810181356401000000008111156100d557600080fd5b8201836020820111156100e757600080fd5b8035906020019184600183028401116401000000008311171561010957600080fd5b91939092909160208101903564010000000081111561012757600080fd5b82018360208201111561013957600080fd5b8035906020019184600183028401116401000000008311171561015b57600080fd5b91939092909160208101903564010000000081111561017957600080fd5b82018360208201111561018b57600080fd5b803590602001918460018302840111640100000000831117156101ad57600080fd5b919350915035610304565b005b3480156101c657600080fd5b506101cf6110b5565b6040805160208082528351818301528351919283929083019185019080838360005b838110156102095781810151838201526020016101f1565b50505050905090810190601f1680156102365780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561025057600080fd5b506102596110c7565b60408051918252519081900360200190f35b60007fffffffff0000000000000000000000000000000000000000000000000000000082167f01ffc9a70000000000000000000000000000000000000000000000000000000014806102fe57507fffffffff0000000000000000000000000000000000000000000000000000000082167f8564090700000000000000000000000000000000000000000000000000000000145b92915050565b6030861461035d576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806118056026913960400191505060405180910390fd5b602084146103b6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603681526020018061179c6036913960400191505060405180910390fd5b6060821461040f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806118786029913960400191505060405180910390fd5b670de0b6b3a7640000341015610470576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806118526026913960400191505060405180910390fd5b633b9aca003406156104cd576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260338152602001806117d26033913960400191505060405180910390fd5b633b9aca00340467ffffffffffffffff811115610535576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602781526020018061182b6027913960400191505060405180910390fd5b6060610540826114ba565b90507f649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c589898989858a8a6105756020546114ba565b6040805160a0808252810189905290819060208201908201606083016080840160c085018e8e80828437600083820152601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01690910187810386528c815260200190508c8c808284376000838201819052601f9091017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01690920188810386528c5181528c51602091820193918e019250908190849084905b83811015610648578181015183820152602001610630565b50505050905090810190601f1680156106755780820380516001836020036101000a031916815260200191505b5086810383528881526020018989808284376000838201819052601f9091017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169092018881038452895181528951602091820193918b019250908190849084905b838110156106ef5781810151838201526020016106d7565b50505050905090810190601f16801561071c5780820380516001836020036101000a031916815260200191505b509d505050505050505050505050505060405180910390a1600060028a8a600060801b604051602001808484808284377fffffffffffffffffffffffffffffffff0000000000000000000000000000000090941691909301908152604080517ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0818403018152601090920190819052815191955093508392506020850191508083835b602083106107fc57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016107bf565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610859573d6000803e3d6000fd5b5050506040513d602081101561086e57600080fd5b5051905060006002806108846040848a8c6116fe565b6040516020018083838082843780830192505050925050506040516020818303038152906040526040518082805190602001908083835b602083106108f857805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016108bb565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610955573d6000803e3d6000fd5b5050506040513d602081101561096a57600080fd5b5051600261097b896040818d6116fe565b60405160009060200180848480828437919091019283525050604080518083038152602092830191829052805190945090925082918401908083835b602083106109f457805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016109b7565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610a51573d6000803e3d6000fd5b5050506040513d6020811015610a6657600080fd5b5051604080516020818101949094528082019290925280518083038201815260609092019081905281519192909182918401908083835b60208310610ada57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610a9d565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610b37573d6000803e3d6000fd5b5050506040513d6020811015610b4c57600080fd5b50516040805160208101858152929350600092600292839287928f928f92018383808284378083019250505093505050506040516020818303038152906040526040518082805190602001908083835b60208310610bd957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610b9c565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610c36573d6000803e3d6000fd5b5050506040513d6020811015610c4b57600080fd5b50516040518651600291889160009188916020918201918291908601908083835b60208310610ca957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610c6c565b6001836020036101000a0380198251168184511680821785525050505050509050018367ffffffffffffffff191667ffffffffffffffff1916815260180182815260200193505050506040516020818303038152906040526040518082805190602001908083835b60208310610d4e57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610d11565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610dab573d6000803e3d6000fd5b5050506040513d6020811015610dc057600080fd5b5051604080516020818101949094528082019290925280518083038201815260609092019081905281519192909182918401908083835b60208310610e3457805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610df7565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610e91573d6000803e3d6000fd5b5050506040513d6020811015610ea657600080fd5b50519050858114610f02576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260548152602001806117486054913960600191505060405180910390fd5b60205463ffffffff11610f60576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806117276021913960400191505060405180910390fd5b602080546001019081905560005b60208110156110a9578160011660011415610fa0578260008260208110610f9157fe5b0155506110ac95505050505050565b600260008260208110610faf57fe5b01548460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061102557805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610fe8565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015611082573d6000803e3d6000fd5b5050506040513d602081101561109757600080fd5b50519250600282049150600101610f6e565b50fe5b50505050505050565b60606110c26020546114ba565b905090565b6020546000908190815b60208110156112f05781600116600114156111e6576002600082602081106110f557fe5b01548460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061116b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161112e565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa1580156111c8573d6000803e3d6000fd5b5050506040513d60208110156111dd57600080fd5b505192506112e2565b600283602183602081106111f657fe5b015460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061126b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161122e565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa1580156112c8573d6000803e3d6000fd5b5050506040513d60208110156112dd57600080fd5b505192505b6002820491506001016110d1565b506002826112ff6020546114ba565b600060401b6040516020018084815260200183805190602001908083835b6020831061135a57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161131d565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790527fffffffffffffffffffffffffffffffffffffffffffffffff000000000000000095909516920191825250604080518083037ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8018152601890920190819052815191955093508392850191508083835b6020831061143f57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101611402565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa15801561149c573d6000803e3d6000fd5b5050506040513d60208110156114b157600080fd5b50519250505090565b60408051600880825281830190925260609160208201818036833701905050905060c082901b8060071a60f81b826000815181106114f457fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060061a60f81b8260018151811061153757fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060051a60f81b8260028151811061157a57fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060041a60f81b826003815181106115bd57fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060031a60f81b8260048151811061160057fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060021a60f81b8260058151811061164357fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060011a60f81b8260068151811061168657fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060001a60f81b826007815181106116c957fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a90535050919050565b6000808585111561170d578182fd5b83861115611719578182fd5b505082019391909203915056fe4465706f736974436f6e74726163743a206d65726b6c6520747265652066756c6c4465706f736974436f6e74726163743a207265636f6e7374727563746564204465706f7369744461746120646f6573206e6f74206d6174636820737570706c696564206465706f7369745f646174615f726f6f744465706f736974436f6e74726163743a20696e76616c6964207769746864726177616c5f63726564656e7469616c73206c656e6774684465706f736974436f6e74726163743a206465706f7369742076616c7565206e6f74206d756c7469706c65206f6620677765694465706f736974436f6e74726163743a20696e76616c6964207075626b6579206c656e6774684465706f736974436f6e74726163743a206465706f7369742076616c756520746f6f20686967684465706f736974436f6e74726163743a206465706f7369742076616c756520746f6f206c6f774465706f736974436f6e74726163743a20696e76616c6964207369676e6174757265206c656e677468a26469706673582212201dd26f37a621703009abf16e77e69c93dc50c79db7f6cc37543e3e0e3decdc9764736f6c634300060b0033", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000022": "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", + "0x0000000000000000000000000000000000000000000000000000000000000023": "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71", + "0x0000000000000000000000000000000000000000000000000000000000000024": "0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c", + "0x0000000000000000000000000000000000000000000000000000000000000025": "0x536d98837f2dd165a55d5eeae91485954472d56f246df256bf3cae19352a123c", + "0x0000000000000000000000000000000000000000000000000000000000000026": "0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30", + "0x0000000000000000000000000000000000000000000000000000000000000027": "0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1", + "0x0000000000000000000000000000000000000000000000000000000000000028": "0x87eb0ddba57e35f6d286673802a4af5975e22506c7cf4c64bb6be5ee11527f2c", + "0x0000000000000000000000000000000000000000000000000000000000000029": "0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193", + "0x000000000000000000000000000000000000000000000000000000000000002a": "0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1", + "0x000000000000000000000000000000000000000000000000000000000000002b": "0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b", + "0x000000000000000000000000000000000000000000000000000000000000002c": "0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220", + "0x000000000000000000000000000000000000000000000000000000000000002d": "0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f", + "0x000000000000000000000000000000000000000000000000000000000000002e": "0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e", + "0x000000000000000000000000000000000000000000000000000000000000002f": "0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784", + "0x0000000000000000000000000000000000000000000000000000000000000030": "0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb", + "0x0000000000000000000000000000000000000000000000000000000000000031": "0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb", + "0x0000000000000000000000000000000000000000000000000000000000000032": "0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab", + "0x0000000000000000000000000000000000000000000000000000000000000033": "0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4", + "0x0000000000000000000000000000000000000000000000000000000000000034": "0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f", + "0x0000000000000000000000000000000000000000000000000000000000000035": "0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa", + "0x0000000000000000000000000000000000000000000000000000000000000036": "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", + "0x0000000000000000000000000000000000000000000000000000000000000037": "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", + "0x0000000000000000000000000000000000000000000000000000000000000038": "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", + "0x0000000000000000000000000000000000000000000000000000000000000039": "0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0", + "0x000000000000000000000000000000000000000000000000000000000000003a": "0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544", + "0x000000000000000000000000000000000000000000000000000000000000003b": "0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765", + "0x000000000000000000000000000000000000000000000000000000000000003c": "0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4", + "0x000000000000000000000000000000000000000000000000000000000000003d": "0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1", + "0x000000000000000000000000000000000000000000000000000000000000003e": "0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636", + "0x000000000000000000000000000000000000000000000000000000000000003f": "0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c", + "0x0000000000000000000000000000000000000000000000000000000000000040": "0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7" + } + }, + "0x9a4aa7d9C2F6386e5F24d790eB2FFB9fd543A170": { + "balance": "1000000000000000000000000000" + }, + "0x5E3141B900ac5f5608b0d057D10d45a0e4927cD9": { + "balance": "1000000000000000000000000000" + }, + "0x7cF5Dbc49F0904065664b5B6C0d69CaB55F33988": { + "balance": "1000000000000000000000000000" + }, + "0x8D12b071A6F3823A535D38C4a583a2FA1859e822": { + "balance": "1000000000000000000000000000" + }, + "0x3B575D3cda6b30736A38B031E0d245E646A21135": { + "balance": "1000000000000000000000000000" + }, + "0x53bDe6CF93461674F590E532006b4022dA57A724": { + "balance": "1000000000000000000000000000" + } + }, + "coinbase": "0x0000000000000000000000000000000000000000", + "difficulty": "0x01", + "extraData": "", + "gasLimit": "0x400000", + "nonce": "0x1234", + "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "timestamp": "1662465600" +} \ No newline at end of file diff --git a/scripts/local_testnet/geth.sh b/scripts/local_testnet/geth.sh new file mode 100755 index 0000000000..d3923cdd89 --- /dev/null +++ b/scripts/local_testnet/geth.sh @@ -0,0 +1,54 @@ +set -Eeuo pipefail + +source ./vars.env + +# Get options +while getopts "d:sh" flag; do + case "${flag}" in + d) DEBUG_LEVEL=${OPTARG};; + s) SUBSCRIBE_ALL_SUBNETS="--subscribe-all-subnets";; + h) + echo "Start a geth node" + echo + echo "usage: $0 " + echo + echo "Options:" + echo " -h: this help" + echo + echo "Positional arguments:" + echo " DATADIR Value for --datadir parameter" + echo " NETWORK-PORT Value for --port" + echo " HTTP-PORT Value for --http.port" + echo " AUTH-PORT Value for --authrpc.port" + echo " GENESIS_FILE Value for geth init" + exit + ;; + esac +done + +# Get positional arguments +data_dir=${@:$OPTIND+0:1} +network_port=${@:$OPTIND+1:1} +http_port=${@:$OPTIND+2:1} +auth_port=${@:$OPTIND+3:1} +genesis_file=${@:$OPTIND+4:1} + + +# Init +$GETH_BINARY init \ + --datadir $data_dir \ + $genesis_file + +echo "Completed init" + +exec $GETH_BINARY \ + --datadir $data_dir \ + --ipcdisable \ + --http \ + --http.api="engine,eth,web3,net,debug" \ + --networkid=$CHAIN_ID \ + --syncmode=full \ + --bootnodes $EL_BOOTNODE_ENODE \ + --port $network_port \ + --http.port $http_port \ + --authrpc.port $auth_port \ No newline at end of file diff --git a/scripts/local_testnet/kill_processes.sh b/scripts/local_testnet/kill_processes.sh index d63725ac14..83a0027337 100755 --- a/scripts/local_testnet/kill_processes.sh +++ b/scripts/local_testnet/kill_processes.sh @@ -12,7 +12,7 @@ if [ -f "$1" ]; then [[ -n "$pid" ]] || continue echo killing $pid - kill $pid + kill $pid || true done < $1 fi diff --git a/scripts/local_testnet/setup.sh b/scripts/local_testnet/setup.sh index 82336984af..283aa0c026 100755 --- a/scripts/local_testnet/setup.sh +++ b/scripts/local_testnet/setup.sh @@ -1,7 +1,6 @@ #!/usr/bin/env bash # -# Deploys the deposit contract and makes deposits for $VALIDATOR_COUNT insecure deterministic validators. # Produces a testnet specification and a genesis state where the genesis time # is now + $GENESIS_DELAY. # @@ -13,11 +12,6 @@ set -o nounset -o errexit -o pipefail source ./vars.env -lcli \ - deploy-deposit-contract \ - --eth1-http http://localhost:8545 \ - --confirmations 1 \ - --validator-count $VALIDATOR_COUNT NOW=`date +%s` GENESIS_TIME=`expr $NOW + $GENESIS_DELAY` @@ -32,14 +26,20 @@ lcli \ --genesis-delay $GENESIS_DELAY \ --genesis-fork-version $GENESIS_FORK_VERSION \ --altair-fork-epoch $ALTAIR_FORK_EPOCH \ + --bellatrix-fork-epoch $BELLATRIX_FORK_EPOCH \ + --capella-fork-epoch $CAPELLA_FORK_EPOCH \ + --ttd $TTD \ + --eth1-block-hash $ETH1_BLOCK_HASH \ --eth1-id $CHAIN_ID \ --eth1-follow-distance 1 \ --seconds-per-slot $SECONDS_PER_SLOT \ --seconds-per-eth1-block $SECONDS_PER_ETH1_BLOCK \ --proposer-score-boost "$PROPOSER_SCORE_BOOST" \ + --validator-count $GENESIS_VALIDATOR_COUNT \ + --interop-genesis-state \ --force -echo Specification generated at $TESTNET_DIR. +echo Specification and genesis.ssz generated at $TESTNET_DIR. echo "Generating $VALIDATOR_COUNT validators concurrently... (this may take a while)" lcli \ @@ -49,13 +49,3 @@ lcli \ --node-count $BN_COUNT echo Validators generated with keystore passwords at $DATADIR. -echo "Building genesis state... (this might take a while)" - -lcli \ - interop-genesis \ - --spec $SPEC_PRESET \ - --genesis-time $GENESIS_TIME \ - --testnet-dir $TESTNET_DIR \ - $GENESIS_VALIDATOR_COUNT - -echo Created genesis state in $TESTNET_DIR diff --git a/scripts/local_testnet/start_local_testnet.sh b/scripts/local_testnet/start_local_testnet.sh index a6f5ec7a8c..64111d5627 100755 --- a/scripts/local_testnet/start_local_testnet.sh +++ b/scripts/local_testnet/start_local_testnet.sh @@ -40,6 +40,8 @@ if (( $VC_COUNT > $BN_COUNT )); then exit fi +genesis_file=${@:$OPTIND+0:1} + # Init some constants PID_FILE=$TESTNET_DIR/PIDS.pid LOG_DIR=$TESTNET_DIR @@ -55,6 +57,9 @@ mkdir -p $LOG_DIR for (( bn=1; bn<=$BN_COUNT; bn++ )); do touch $LOG_DIR/beacon_node_$bn.log done +for (( el=1; el<=$BN_COUNT; el++ )); do + touch $LOG_DIR/geth_$el.log +done for (( vc=1; vc<=$VC_COUNT; vc++ )); do touch $LOG_DIR/validator_node_$vc.log done @@ -92,29 +97,49 @@ execute_command_add_PID() { echo "$!" >> $PID_FILE } -# Start anvil, setup things up and start the bootnode. -# The delays are necessary, hopefully there is a better way :( - -# Delay to let anvil to get started -execute_command_add_PID anvil_test_node.log ./anvil_test_node.sh -sleeping 10 # Setup data echo "executing: ./setup.sh >> $LOG_DIR/setup.log" ./setup.sh >> $LOG_DIR/setup.log 2>&1 +# Update future hardforks time in the EL genesis file based on the CL genesis time +GENESIS_TIME=$(lcli pretty-ssz state_merge $TESTNET_DIR/genesis.ssz | jq | grep -Po 'genesis_time": "\K.*\d') +echo $GENESIS_TIME +CAPELLA_TIME=$((GENESIS_TIME + (CAPELLA_FORK_EPOCH * 32 * SECONDS_PER_SLOT))) +echo $CAPELLA_TIME +sed -i 's/"shanghaiTime".*$/"shanghaiTime": '"$CAPELLA_TIME"',/g' $genesis_file +cat $genesis_file + # Delay to let boot_enr.yaml to be created execute_command_add_PID bootnode.log ./bootnode.sh sleeping 1 +execute_command_add_PID el_bootnode.log ./el_bootnode.sh +sleeping 1 + # Start beacon nodes BN_udp_tcp_base=9000 BN_http_port_base=8000 +EL_base_network=7000 +EL_base_http=6000 +EL_base_auth_http=5000 + (( $VC_COUNT < $BN_COUNT )) && SAS=-s || SAS= +for (( el=1; el<=$BN_COUNT; el++ )); do + execute_command_add_PID geth_$el.log ./geth.sh $DATADIR/geth_datadir$el $((EL_base_network + $el)) $((EL_base_http + $el)) $((EL_base_auth_http + $el)) $genesis_file +done + +sleeping 20 + +# Reset the `genesis.json` config file fork times. +sed -i 's/"shanghaiTime".*$/"shanghaiTime": 0,/g' $genesis_file + for (( bn=1; bn<=$BN_COUNT; bn++ )); do - execute_command_add_PID beacon_node_$bn.log ./beacon_node.sh $SAS -d $DEBUG_LEVEL $DATADIR/node_$bn $((BN_udp_tcp_base + $bn)) $((BN_http_port_base + $bn)) + secret=$DATADIR/geth_datadir$bn/geth/jwtsecret + echo $secret + execute_command_add_PID beacon_node_$bn.log ./beacon_node.sh $SAS -d $DEBUG_LEVEL $DATADIR/node_$bn $((BN_udp_tcp_base + $bn)) $((BN_http_port_base + $bn)) http://localhost:$((EL_base_auth_http + $bn)) $secret done # Start requested number of validator clients diff --git a/scripts/local_testnet/validator_client.sh b/scripts/local_testnet/validator_client.sh index 975a2a6753..d88a1833cb 100755 --- a/scripts/local_testnet/validator_client.sh +++ b/scripts/local_testnet/validator_client.sh @@ -30,4 +30,5 @@ exec lighthouse \ --testnet-dir $TESTNET_DIR \ --init-slashing-protection \ --beacon-nodes ${@:$OPTIND+1:1} \ + --suggested-fee-recipient 0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990 \ $VC_ARGS diff --git a/scripts/local_testnet/vars.env b/scripts/local_testnet/vars.env index 80c4ef1331..6e05f0c411 100644 --- a/scripts/local_testnet/vars.env +++ b/scripts/local_testnet/vars.env @@ -1,17 +1,26 @@ +# Path to the geth binary +GETH_BINARY=geth +EL_BOOTNODE_BINARY=bootnode + # Base directories for the validator keys and secrets DATADIR=~/.lighthouse/local-testnet # Directory for the eth2 config TESTNET_DIR=$DATADIR/testnet -# Mnemonic for the anvil test network -ETH1_NETWORK_MNEMONIC="vast thought differ pull jewel broom cook wrist tribe word before omit" +# Mnemonic for generating validator keys +MNEMONIC_PHRASE="vast thought differ pull jewel broom cook wrist tribe word before omit" -# Hardcoded deposit contract based on ETH1_NETWORK_MNEMONIC -DEPOSIT_CONTRACT_ADDRESS=8c594691c0e592ffa21f153a16ae41db5befcaaa +EL_BOOTNODE_ENODE="enode://51ea9bb34d31efc3491a842ed13b8cab70e753af108526b57916d716978b380ed713f4336a80cdb85ec2a115d5a8c0ae9f3247bed3c84d3cb025c6bab311062c@127.0.0.1:0?discport=30301" + +# Hardcoded deposit contract +DEPOSIT_CONTRACT_ADDRESS=4242424242424242424242424242424242424242 GENESIS_FORK_VERSION=0x42424242 +# Block hash generated from genesis.json in directory +ETH1_BLOCK_HASH=4b0e17cf5c04616d64526d292b80a1f2720cf2195d990006e4ea6950c5bbcb9f + VALIDATOR_COUNT=80 GENESIS_VALIDATOR_COUNT=80 @@ -33,7 +42,11 @@ BOOTNODE_PORT=4242 CHAIN_ID=4242 # Hard fork configuration -ALTAIR_FORK_EPOCH=18446744073709551615 +ALTAIR_FORK_EPOCH=0 +BELLATRIX_FORK_EPOCH=0 +CAPELLA_FORK_EPOCH=1 + +TTD=0 # Spec version (mainnet or minimal) SPEC_PRESET=mainnet diff --git a/scripts/tests/doppelganger_protection.sh b/scripts/tests/doppelganger_protection.sh index e68ca21516..e9d3e39ce5 100755 --- a/scripts/tests/doppelganger_protection.sh +++ b/scripts/tests/doppelganger_protection.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Requires `lighthouse`, ``lcli`, `anvil`, `curl`, `jq` +# Requires `lighthouse`, `lcli`, `geth`, `bootnode`, `curl`, `jq` BEHAVIOR=$1 @@ -15,21 +15,15 @@ exit_if_fails() { $@ EXIT_CODE=$? if [[ $EXIT_CODE -eq 1 ]]; then - exit 111 + exit 1 fi } +genesis_file=$2 source ./vars.env exit_if_fails ../local_testnet/clean.sh -echo "Starting anvil" - -exit_if_fails ../local_testnet/anvil_test_node.sh &> /dev/null & -ANVIL_PID=$! - -# Wait for anvil to start -sleep 5 echo "Setting up local testnet" @@ -41,28 +35,31 @@ exit_if_fails cp -R $HOME/.lighthouse/local-testnet/node_1 $HOME/.lighthouse/loc echo "Starting bootnode" exit_if_fails ../local_testnet/bootnode.sh &> /dev/null & -BOOT_PID=$! + +exit_if_fails ../local_testnet/el_bootnode.sh &> /dev/null & # wait for the bootnode to start sleep 10 +echo "Starting local execution nodes" + +exit_if_fails ../local_testnet/geth.sh $HOME/.lighthouse/local-testnet/geth_datadir1 7000 6000 5000 $genesis_file &> geth.log & +exit_if_fails ../local_testnet/geth.sh $HOME/.lighthouse/local-testnet/geth_datadir2 7100 6100 5100 $genesis_file &> /dev/null & +exit_if_fails ../local_testnet/geth.sh $HOME/.lighthouse/local-testnet/geth_datadir3 7200 6200 5200 $genesis_file &> /dev/null & + +sleep 20 + echo "Starting local beacon nodes" -exit_if_fails ../local_testnet/beacon_node.sh $HOME/.lighthouse/local-testnet/node_1 9000 8000 &> /dev/null & -BEACON_PID=$! -exit_if_fails ../local_testnet/beacon_node.sh $HOME/.lighthouse/local-testnet/node_2 9100 8100 &> /dev/null & -BEACON_PID2=$! -exit_if_fails ../local_testnet/beacon_node.sh $HOME/.lighthouse/local-testnet/node_3 9200 8200 &> /dev/null & -BEACON_PID3=$! +exit_if_fails ../local_testnet/beacon_node.sh -d debug $HOME/.lighthouse/local-testnet/node_1 9000 8000 http://localhost:5000 $HOME/.lighthouse/local-testnet/geth_datadir1/geth/jwtsecret &> beacon1.log & +exit_if_fails ../local_testnet/beacon_node.sh $HOME/.lighthouse/local-testnet/node_2 9100 8100 http://localhost:5100 $HOME/.lighthouse/local-testnet/geth_datadir2/geth/jwtsecret &> /dev/null & +exit_if_fails ../local_testnet/beacon_node.sh $HOME/.lighthouse/local-testnet/node_3 9200 8200 http://localhost:5200 $HOME/.lighthouse/local-testnet/geth_datadir3/geth/jwtsecret &> /dev/null & echo "Starting local validator clients" exit_if_fails ../local_testnet/validator_client.sh $HOME/.lighthouse/local-testnet/node_1 http://localhost:8000 &> /dev/null & -VALIDATOR_1_PID=$! exit_if_fails ../local_testnet/validator_client.sh $HOME/.lighthouse/local-testnet/node_2 http://localhost:8100 &> /dev/null & -VALIDATOR_2_PID=$! exit_if_fails ../local_testnet/validator_client.sh $HOME/.lighthouse/local-testnet/node_3 http://localhost:8200 &> /dev/null & -VALIDATOR_3_PID=$! echo "Waiting an epoch before starting the next validator client" sleep $(( $SECONDS_PER_SLOT * 32 )) @@ -71,7 +68,7 @@ if [[ "$BEHAVIOR" == "failure" ]]; then echo "Starting the doppelganger validator client" - # Use same keys as keys from VC1, but connect to BN2 + # Use same keys as keys from VC1 and connect to BN2 # This process should not last longer than 2 epochs timeout $(( $SECONDS_PER_SLOT * 32 * 2 )) ../local_testnet/validator_client.sh $HOME/.lighthouse/local-testnet/node_1_doppelganger http://localhost:8100 DOPPELGANGER_EXIT=$? @@ -79,7 +76,9 @@ if [[ "$BEHAVIOR" == "failure" ]]; then echo "Shutting down" # Cleanup - kill $BOOT_PID $BEACON_PID $BEACON_PID2 $BEACON_PID3 $ANVIL_PID $VALIDATOR_1_PID $VALIDATOR_2_PID $VALIDATOR_3_PID + killall geth + killall lighthouse + killall bootnode echo "Done" @@ -98,7 +97,6 @@ if [[ "$BEHAVIOR" == "success" ]]; then echo "Starting the last validator client" ../local_testnet/validator_client.sh $HOME/.lighthouse/local-testnet/node_4 http://localhost:8100 & - VALIDATOR_4_PID=$! DOPPELGANGER_FAILURE=0 # Sleep three epochs, then make sure all validators were active in epoch 2. Use @@ -144,7 +142,10 @@ if [[ "$BEHAVIOR" == "success" ]]; then # Cleanup cd $PREVIOUS_DIR - kill $BOOT_PID $BEACON_PID $BEACON_PID2 $BEACON_PID3 $ANVIL_PID $VALIDATOR_1_PID $VALIDATOR_2_PID $VALIDATOR_3_PID $VALIDATOR_4_PID + + killall geth + killall lighthouse + killall bootnode echo "Done" @@ -153,4 +154,4 @@ if [[ "$BEHAVIOR" == "success" ]]; then fi fi -exit 0 +exit 0 \ No newline at end of file diff --git a/scripts/tests/genesis.json b/scripts/tests/genesis.json new file mode 100644 index 0000000000..985bb9cef8 --- /dev/null +++ b/scripts/tests/genesis.json @@ -0,0 +1,850 @@ +{ + "config": { + "chainId": 4242, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "mergeForkBlock": 0, + "terminalTotalDifficulty": 0 + }, + "alloc": { + "0x0000000000000000000000000000000000000000": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000001": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000002": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000003": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000004": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000005": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000006": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000007": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000008": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000009": { + "balance": "1" + }, + "0x000000000000000000000000000000000000000a": { + "balance": "1" + }, + "0x000000000000000000000000000000000000000b": { + "balance": "1" + }, + "0x000000000000000000000000000000000000000c": { + "balance": "1" + }, + "0x000000000000000000000000000000000000000d": { + "balance": "1" + }, + "0x000000000000000000000000000000000000000e": { + "balance": "1" + }, + "0x000000000000000000000000000000000000000f": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000010": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000011": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000012": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000013": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000014": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000015": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000016": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000017": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000018": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000019": { + "balance": "1" + }, + "0x000000000000000000000000000000000000001a": { + "balance": "1" + }, + "0x000000000000000000000000000000000000001b": { + "balance": "1" + }, + "0x000000000000000000000000000000000000001c": { + "balance": "1" + }, + "0x000000000000000000000000000000000000001d": { + "balance": "1" + }, + "0x000000000000000000000000000000000000001e": { + "balance": "1" + }, + "0x000000000000000000000000000000000000001f": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000020": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000021": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000022": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000023": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000024": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000025": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000026": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000027": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000028": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000029": { + "balance": "1" + }, + "0x000000000000000000000000000000000000002a": { + "balance": "1" + }, + "0x000000000000000000000000000000000000002b": { + "balance": "1" + }, + "0x000000000000000000000000000000000000002c": { + "balance": "1" + }, + "0x000000000000000000000000000000000000002d": { + "balance": "1" + }, + "0x000000000000000000000000000000000000002e": { + "balance": "1" + }, + "0x000000000000000000000000000000000000002f": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000030": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000031": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000032": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000033": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000034": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000035": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000036": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000037": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000038": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000039": { + "balance": "1" + }, + "0x000000000000000000000000000000000000003a": { + "balance": "1" + }, + "0x000000000000000000000000000000000000003b": { + "balance": "1" + }, + "0x000000000000000000000000000000000000003c": { + "balance": "1" + }, + "0x000000000000000000000000000000000000003d": { + "balance": "1" + }, + "0x000000000000000000000000000000000000003e": { + "balance": "1" + }, + "0x000000000000000000000000000000000000003f": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000040": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000041": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000042": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000043": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000044": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000045": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000046": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000047": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000048": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000049": { + "balance": "1" + }, + "0x000000000000000000000000000000000000004a": { + "balance": "1" + }, + "0x000000000000000000000000000000000000004b": { + "balance": "1" + }, + "0x000000000000000000000000000000000000004c": { + "balance": "1" + }, + "0x000000000000000000000000000000000000004d": { + "balance": "1" + }, + "0x000000000000000000000000000000000000004e": { + "balance": "1" + }, + "0x000000000000000000000000000000000000004f": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000050": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000051": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000052": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000053": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000054": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000055": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000056": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000057": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000058": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000059": { + "balance": "1" + }, + "0x000000000000000000000000000000000000005a": { + "balance": "1" + }, + "0x000000000000000000000000000000000000005b": { + "balance": "1" + }, + "0x000000000000000000000000000000000000005c": { + "balance": "1" + }, + "0x000000000000000000000000000000000000005d": { + "balance": "1" + }, + "0x000000000000000000000000000000000000005e": { + "balance": "1" + }, + "0x000000000000000000000000000000000000005f": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000060": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000061": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000062": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000063": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000064": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000065": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000066": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000067": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000068": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000069": { + "balance": "1" + }, + "0x000000000000000000000000000000000000006a": { + "balance": "1" + }, + "0x000000000000000000000000000000000000006b": { + "balance": "1" + }, + "0x000000000000000000000000000000000000006c": { + "balance": "1" + }, + "0x000000000000000000000000000000000000006d": { + "balance": "1" + }, + "0x000000000000000000000000000000000000006e": { + "balance": "1" + }, + "0x000000000000000000000000000000000000006f": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000070": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000071": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000072": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000073": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000074": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000075": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000076": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000077": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000078": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000079": { + "balance": "1" + }, + "0x000000000000000000000000000000000000007a": { + "balance": "1" + }, + "0x000000000000000000000000000000000000007b": { + "balance": "1" + }, + "0x000000000000000000000000000000000000007c": { + "balance": "1" + }, + "0x000000000000000000000000000000000000007d": { + "balance": "1" + }, + "0x000000000000000000000000000000000000007e": { + "balance": "1" + }, + "0x000000000000000000000000000000000000007f": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000080": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000081": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000082": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000083": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000084": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000085": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000086": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000087": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000088": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000089": { + "balance": "1" + }, + "0x000000000000000000000000000000000000008a": { + "balance": "1" + }, + "0x000000000000000000000000000000000000008b": { + "balance": "1" + }, + "0x000000000000000000000000000000000000008c": { + "balance": "1" + }, + "0x000000000000000000000000000000000000008d": { + "balance": "1" + }, + "0x000000000000000000000000000000000000008e": { + "balance": "1" + }, + "0x000000000000000000000000000000000000008f": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000090": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000091": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000092": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000093": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000094": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000095": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000096": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000097": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000098": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000099": { + "balance": "1" + }, + "0x000000000000000000000000000000000000009a": { + "balance": "1" + }, + "0x000000000000000000000000000000000000009b": { + "balance": "1" + }, + "0x000000000000000000000000000000000000009c": { + "balance": "1" + }, + "0x000000000000000000000000000000000000009d": { + "balance": "1" + }, + "0x000000000000000000000000000000000000009e": { + "balance": "1" + }, + "0x000000000000000000000000000000000000009f": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000a0": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000a1": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000a2": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000a3": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000a4": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000a5": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000a6": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000a7": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000a8": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000a9": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000aa": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ab": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ac": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ad": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ae": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000af": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000b0": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000b1": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000b2": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000b3": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000b4": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000b5": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000b6": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000b7": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000b8": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000b9": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ba": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000bb": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000bc": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000bd": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000be": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000bf": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000c0": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000c1": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000c2": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000c3": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000c4": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000c5": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000c6": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000c7": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000c8": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000c9": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ca": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000cb": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000cc": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000cd": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ce": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000cf": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000d0": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000d1": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000d2": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000d3": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000d4": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000d5": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000d6": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000d7": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000d8": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000d9": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000da": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000db": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000dc": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000dd": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000de": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000df": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000e0": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000e1": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000e2": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000e3": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000e4": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000e5": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000e6": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000e7": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000e8": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000e9": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ea": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000eb": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ec": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ed": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ee": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ef": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000f0": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000f1": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000f2": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000f3": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000f4": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000f5": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000f6": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000f7": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000f8": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000f9": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000fa": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000fb": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000fc": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000fd": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000fe": { + "balance": "1" + }, + "0x00000000000000000000000000000000000000ff": { + "balance": "1" + }, + "0x4242424242424242424242424242424242424242": { + "balance": "0", + "code": "0x60806040526004361061003f5760003560e01c806301ffc9a71461004457806322895118146100a4578063621fd130146101ba578063c5f2892f14610244575b600080fd5b34801561005057600080fd5b506100906004803603602081101561006757600080fd5b50357fffffffff000000000000000000000000000000000000000000000000000000001661026b565b604080519115158252519081900360200190f35b6101b8600480360360808110156100ba57600080fd5b8101906020810181356401000000008111156100d557600080fd5b8201836020820111156100e757600080fd5b8035906020019184600183028401116401000000008311171561010957600080fd5b91939092909160208101903564010000000081111561012757600080fd5b82018360208201111561013957600080fd5b8035906020019184600183028401116401000000008311171561015b57600080fd5b91939092909160208101903564010000000081111561017957600080fd5b82018360208201111561018b57600080fd5b803590602001918460018302840111640100000000831117156101ad57600080fd5b919350915035610304565b005b3480156101c657600080fd5b506101cf6110b5565b6040805160208082528351818301528351919283929083019185019080838360005b838110156102095781810151838201526020016101f1565b50505050905090810190601f1680156102365780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561025057600080fd5b506102596110c7565b60408051918252519081900360200190f35b60007fffffffff0000000000000000000000000000000000000000000000000000000082167f01ffc9a70000000000000000000000000000000000000000000000000000000014806102fe57507fffffffff0000000000000000000000000000000000000000000000000000000082167f8564090700000000000000000000000000000000000000000000000000000000145b92915050565b6030861461035d576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806118056026913960400191505060405180910390fd5b602084146103b6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603681526020018061179c6036913960400191505060405180910390fd5b6060821461040f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806118786029913960400191505060405180910390fd5b670de0b6b3a7640000341015610470576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806118526026913960400191505060405180910390fd5b633b9aca003406156104cd576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260338152602001806117d26033913960400191505060405180910390fd5b633b9aca00340467ffffffffffffffff811115610535576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602781526020018061182b6027913960400191505060405180910390fd5b6060610540826114ba565b90507f649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c589898989858a8a6105756020546114ba565b6040805160a0808252810189905290819060208201908201606083016080840160c085018e8e80828437600083820152601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01690910187810386528c815260200190508c8c808284376000838201819052601f9091017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01690920188810386528c5181528c51602091820193918e019250908190849084905b83811015610648578181015183820152602001610630565b50505050905090810190601f1680156106755780820380516001836020036101000a031916815260200191505b5086810383528881526020018989808284376000838201819052601f9091017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169092018881038452895181528951602091820193918b019250908190849084905b838110156106ef5781810151838201526020016106d7565b50505050905090810190601f16801561071c5780820380516001836020036101000a031916815260200191505b509d505050505050505050505050505060405180910390a1600060028a8a600060801b604051602001808484808284377fffffffffffffffffffffffffffffffff0000000000000000000000000000000090941691909301908152604080517ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0818403018152601090920190819052815191955093508392506020850191508083835b602083106107fc57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016107bf565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610859573d6000803e3d6000fd5b5050506040513d602081101561086e57600080fd5b5051905060006002806108846040848a8c6116fe565b6040516020018083838082843780830192505050925050506040516020818303038152906040526040518082805190602001908083835b602083106108f857805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016108bb565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610955573d6000803e3d6000fd5b5050506040513d602081101561096a57600080fd5b5051600261097b896040818d6116fe565b60405160009060200180848480828437919091019283525050604080518083038152602092830191829052805190945090925082918401908083835b602083106109f457805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016109b7565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610a51573d6000803e3d6000fd5b5050506040513d6020811015610a6657600080fd5b5051604080516020818101949094528082019290925280518083038201815260609092019081905281519192909182918401908083835b60208310610ada57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610a9d565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610b37573d6000803e3d6000fd5b5050506040513d6020811015610b4c57600080fd5b50516040805160208101858152929350600092600292839287928f928f92018383808284378083019250505093505050506040516020818303038152906040526040518082805190602001908083835b60208310610bd957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610b9c565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610c36573d6000803e3d6000fd5b5050506040513d6020811015610c4b57600080fd5b50516040518651600291889160009188916020918201918291908601908083835b60208310610ca957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610c6c565b6001836020036101000a0380198251168184511680821785525050505050509050018367ffffffffffffffff191667ffffffffffffffff1916815260180182815260200193505050506040516020818303038152906040526040518082805190602001908083835b60208310610d4e57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610d11565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610dab573d6000803e3d6000fd5b5050506040513d6020811015610dc057600080fd5b5051604080516020818101949094528082019290925280518083038201815260609092019081905281519192909182918401908083835b60208310610e3457805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610df7565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610e91573d6000803e3d6000fd5b5050506040513d6020811015610ea657600080fd5b50519050858114610f02576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260548152602001806117486054913960600191505060405180910390fd5b60205463ffffffff11610f60576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806117276021913960400191505060405180910390fd5b602080546001019081905560005b60208110156110a9578160011660011415610fa0578260008260208110610f9157fe5b0155506110ac95505050505050565b600260008260208110610faf57fe5b01548460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061102557805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610fe8565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015611082573d6000803e3d6000fd5b5050506040513d602081101561109757600080fd5b50519250600282049150600101610f6e565b50fe5b50505050505050565b60606110c26020546114ba565b905090565b6020546000908190815b60208110156112f05781600116600114156111e6576002600082602081106110f557fe5b01548460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061116b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161112e565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa1580156111c8573d6000803e3d6000fd5b5050506040513d60208110156111dd57600080fd5b505192506112e2565b600283602183602081106111f657fe5b015460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061126b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161122e565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa1580156112c8573d6000803e3d6000fd5b5050506040513d60208110156112dd57600080fd5b505192505b6002820491506001016110d1565b506002826112ff6020546114ba565b600060401b6040516020018084815260200183805190602001908083835b6020831061135a57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161131d565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790527fffffffffffffffffffffffffffffffffffffffffffffffff000000000000000095909516920191825250604080518083037ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8018152601890920190819052815191955093508392850191508083835b6020831061143f57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101611402565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa15801561149c573d6000803e3d6000fd5b5050506040513d60208110156114b157600080fd5b50519250505090565b60408051600880825281830190925260609160208201818036833701905050905060c082901b8060071a60f81b826000815181106114f457fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060061a60f81b8260018151811061153757fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060051a60f81b8260028151811061157a57fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060041a60f81b826003815181106115bd57fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060031a60f81b8260048151811061160057fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060021a60f81b8260058151811061164357fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060011a60f81b8260068151811061168657fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060001a60f81b826007815181106116c957fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a90535050919050565b6000808585111561170d578182fd5b83861115611719578182fd5b505082019391909203915056fe4465706f736974436f6e74726163743a206d65726b6c6520747265652066756c6c4465706f736974436f6e74726163743a207265636f6e7374727563746564204465706f7369744461746120646f6573206e6f74206d6174636820737570706c696564206465706f7369745f646174615f726f6f744465706f736974436f6e74726163743a20696e76616c6964207769746864726177616c5f63726564656e7469616c73206c656e6774684465706f736974436f6e74726163743a206465706f7369742076616c7565206e6f74206d756c7469706c65206f6620677765694465706f736974436f6e74726163743a20696e76616c6964207075626b6579206c656e6774684465706f736974436f6e74726163743a206465706f7369742076616c756520746f6f20686967684465706f736974436f6e74726163743a206465706f7369742076616c756520746f6f206c6f774465706f736974436f6e74726163743a20696e76616c6964207369676e6174757265206c656e677468a26469706673582212201dd26f37a621703009abf16e77e69c93dc50c79db7f6cc37543e3e0e3decdc9764736f6c634300060b0033", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000022": "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", + "0x0000000000000000000000000000000000000000000000000000000000000023": "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71", + "0x0000000000000000000000000000000000000000000000000000000000000024": "0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c", + "0x0000000000000000000000000000000000000000000000000000000000000025": "0x536d98837f2dd165a55d5eeae91485954472d56f246df256bf3cae19352a123c", + "0x0000000000000000000000000000000000000000000000000000000000000026": "0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30", + "0x0000000000000000000000000000000000000000000000000000000000000027": "0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1", + "0x0000000000000000000000000000000000000000000000000000000000000028": "0x87eb0ddba57e35f6d286673802a4af5975e22506c7cf4c64bb6be5ee11527f2c", + "0x0000000000000000000000000000000000000000000000000000000000000029": "0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193", + "0x000000000000000000000000000000000000000000000000000000000000002a": "0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1", + "0x000000000000000000000000000000000000000000000000000000000000002b": "0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b", + "0x000000000000000000000000000000000000000000000000000000000000002c": "0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220", + "0x000000000000000000000000000000000000000000000000000000000000002d": "0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f", + "0x000000000000000000000000000000000000000000000000000000000000002e": "0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e", + "0x000000000000000000000000000000000000000000000000000000000000002f": "0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784", + "0x0000000000000000000000000000000000000000000000000000000000000030": "0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb", + "0x0000000000000000000000000000000000000000000000000000000000000031": "0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb", + "0x0000000000000000000000000000000000000000000000000000000000000032": "0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab", + "0x0000000000000000000000000000000000000000000000000000000000000033": "0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4", + "0x0000000000000000000000000000000000000000000000000000000000000034": "0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f", + "0x0000000000000000000000000000000000000000000000000000000000000035": "0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa", + "0x0000000000000000000000000000000000000000000000000000000000000036": "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", + "0x0000000000000000000000000000000000000000000000000000000000000037": "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", + "0x0000000000000000000000000000000000000000000000000000000000000038": "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", + "0x0000000000000000000000000000000000000000000000000000000000000039": "0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0", + "0x000000000000000000000000000000000000000000000000000000000000003a": "0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544", + "0x000000000000000000000000000000000000000000000000000000000000003b": "0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765", + "0x000000000000000000000000000000000000000000000000000000000000003c": "0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4", + "0x000000000000000000000000000000000000000000000000000000000000003d": "0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1", + "0x000000000000000000000000000000000000000000000000000000000000003e": "0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636", + "0x000000000000000000000000000000000000000000000000000000000000003f": "0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c", + "0x0000000000000000000000000000000000000000000000000000000000000040": "0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7" + } + }, + "0x9a4aa7d9C2F6386e5F24d790eB2FFB9fd543A170": { + "balance": "1000000000000000000000000000" + }, + "0x5E3141B900ac5f5608b0d057D10d45a0e4927cD9": { + "balance": "1000000000000000000000000000" + }, + "0x7cF5Dbc49F0904065664b5B6C0d69CaB55F33988": { + "balance": "1000000000000000000000000000" + }, + "0x8D12b071A6F3823A535D38C4a583a2FA1859e822": { + "balance": "1000000000000000000000000000" + }, + "0x3B575D3cda6b30736A38B031E0d245E646A21135": { + "balance": "1000000000000000000000000000" + }, + "0x53bDe6CF93461674F590E532006b4022dA57A724": { + "balance": "1000000000000000000000000000" + } + }, + "coinbase": "0x0000000000000000000000000000000000000000", + "difficulty": "0x01", + "extraData": "", + "gasLimit": "0x400000", + "nonce": "0x1234", + "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "timestamp": "1662465600" +} \ No newline at end of file diff --git a/scripts/tests/vars.env b/scripts/tests/vars.env index 7429a35eb6..a7e696ec0a 100644 --- a/scripts/tests/vars.env +++ b/scripts/tests/vars.env @@ -1,17 +1,23 @@ +# Path to the geth binary +GETH_BINARY=geth +EL_BOOTNODE_BINARY=bootnode + # Base directories for the validator keys and secrets DATADIR=~/.lighthouse/local-testnet # Directory for the eth2 config TESTNET_DIR=$DATADIR/testnet -# Mnemonic for the anvil test network -ETH1_NETWORK_MNEMONIC="vast thought differ pull jewel broom cook wrist tribe word before omit" +EL_BOOTNODE_ENODE="enode://51ea9bb34d31efc3491a842ed13b8cab70e753af108526b57916d716978b380ed713f4336a80cdb85ec2a115d5a8c0ae9f3247bed3c84d3cb025c6bab311062c@127.0.0.1:0?discport=30301" -# Hardcoded deposit contract based on ETH1_NETWORK_MNEMONIC -DEPOSIT_CONTRACT_ADDRESS=8c594691c0e592ffa21f153a16ae41db5befcaaa +# Hardcoded deposit contract +DEPOSIT_CONTRACT_ADDRESS=4242424242424242424242424242424242424242 GENESIS_FORK_VERSION=0x42424242 +# Block hash generated from genesis.json in directory +ETH1_BLOCK_HASH=16ef16304456fdacdeb272bd70207021031db355ed6c5e44ebd34c1ab757e221 + VALIDATOR_COUNT=80 GENESIS_VALIDATOR_COUNT=80 @@ -33,7 +39,12 @@ BOOTNODE_PORT=4242 CHAIN_ID=4242 # Hard fork configuration -ALTAIR_FORK_EPOCH=18446744073709551615 +ALTAIR_FORK_EPOCH=0 +BELLATRIX_FORK_EPOCH=0 +CAPELLA_FORK_EPOCH=18446744073709551615 +DENEB_FORK_EPOCH=18446744073709551615 + +TTD=0 # Spec version (mainnet or minimal) SPEC_PRESET=mainnet @@ -45,7 +56,7 @@ SECONDS_PER_SLOT=3 SECONDS_PER_ETH1_BLOCK=1 # Proposer score boost percentage -PROPOSER_SCORE_BOOST=40 +PROPOSER_SCORE_BOOST=70 # Enable doppelganger detection -VC_ARGS=" --enable-doppelganger-protection " +VC_ARGS=" --enable-doppelganger-protection " \ No newline at end of file From aaa118ff0e7a0bfeb119a0551958e9a87136614e Mon Sep 17 00:00:00 2001 From: ethDreamer Date: Wed, 17 May 2023 05:51:55 +0000 Subject: [PATCH 06/63] Fix PERSIST_ETH1_CACHE / PERSIST_OP_POOL Metrics (#4278) Do these metrics ever get read? As far as I'm aware, they're only ever updated when lighthouse is shutting down? --- beacon_node/beacon_chain/src/beacon_chain.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 70853998e3..030137246a 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -555,7 +555,7 @@ impl BeaconChain { /// Persists `self.eth1_chain` and its caches to disk. pub fn persist_eth1_cache(&self) -> Result<(), Error> { - let _timer = metrics::start_timer(&metrics::PERSIST_OP_POOL); + let _timer = metrics::start_timer(&metrics::PERSIST_ETH1_CACHE); if let Some(eth1_chain) = self.eth1_chain.as_ref() { self.store From 3052db29fe284987b0b2c1ada4a8b309beb001a2 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Wed, 17 May 2023 05:51:56 +0000 Subject: [PATCH 07/63] Implement `el_offline` and use it in the VC (#4295) ## Issue Addressed Closes https://github.com/sigp/lighthouse/issues/4291, part of #3613. ## Proposed Changes - Implement the `el_offline` field on `/eth/v1/node/syncing`. We set `el_offline=true` if: - The EL's internal status is `Offline` or `AuthFailed`, _or_ - The most recent call to `newPayload` resulted in an error (more on this in a moment). - Use the `el_offline` field in the VC to mark nodes with offline ELs as _unsynced_. These nodes will still be used, but only after synced nodes. - Overhaul the usage of `RequireSynced` so that `::No` is used almost everywhere. The `--allow-unsynced` flag was broken and had the opposite effect to intended, so it has been deprecated. - Add tests for the EL being offline on the upcheck call, and being offline due to the newPayload check. ## Why track `newPayload` errors? Tracking the EL's online/offline status is too coarse-grained to be useful in practice, because: - If the EL is timing out to some calls, it's unlikely to timeout on the `upcheck` call, which is _just_ `eth_syncing`. Every failed call is followed by an upcheck [here](https://github.com/sigp/lighthouse/blob/693886b94176faa4cb450f024696cb69cda2fe58/beacon_node/execution_layer/src/engines.rs#L372-L380), which would have the effect of masking the failure and keeping the status _online_. - The `newPayload` call is the most likely to time out. It's the call in which ELs tend to do most of their work (often 1-2 seconds), with `forkchoiceUpdated` usually returning much faster (<50ms). - If `newPayload` is failing consistently (e.g. timing out) then this is a good indication that either the node's EL is in trouble, or the network as a whole is. In the first case validator clients _should_ prefer other BNs if they have one available. In the second case, all of their BNs will likely report `el_offline` and they'll just have to proceed with trying to use them. ## Additional Changes - Add utility method `ForkName::latest` which is quite convenient for test writing, but probably other things too. - Delete some stale comments from when we used to support multiple execution nodes. --- .../tests/payload_invalidation.rs | 3 + beacon_node/execution_layer/src/engines.rs | 5 + beacon_node/execution_layer/src/lib.rs | 45 +++--- .../src/test_utils/handle_rpc.rs | 25 ++- .../execution_layer/src/test_utils/mod.rs | 27 +++- beacon_node/http_api/src/lib.rs | 52 ++++--- beacon_node/http_api/tests/main.rs | 1 + beacon_node/http_api/tests/status_tests.rs | 145 ++++++++++++++++++ beacon_node/http_api/tests/tests.rs | 2 + common/eth2/src/types.rs | 1 + consensus/types/src/fork_name.rs | 18 ++- lighthouse/tests/validator_client.rs | 6 +- validator_client/src/beacon_node_fallback.rs | 42 ++--- validator_client/src/check_synced.rs | 6 +- validator_client/src/cli.rs | 5 +- validator_client/src/config.rs | 8 +- validator_client/src/duties_service.rs | 13 +- validator_client/src/duties_service/sync.rs | 4 +- validator_client/src/lib.rs | 9 +- validator_client/src/preparation_service.rs | 4 +- .../src/sync_committee_service.rs | 2 +- 21 files changed, 307 insertions(+), 116 deletions(-) create mode 100644 beacon_node/http_api/tests/status_tests.rs diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index 54d7734471..f88c2ee6fd 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -910,6 +910,9 @@ async fn invalid_after_optimistic_sync() { .await, ); + // EL status should still be online, no errors. + assert!(!rig.execution_layer().is_offline_or_erroring().await); + // Running fork choice is necessary since a block has been invalidated. rig.recompute_head().await; diff --git a/beacon_node/execution_layer/src/engines.rs b/beacon_node/execution_layer/src/engines.rs index ce413cb113..362f5b0b2b 100644 --- a/beacon_node/execution_layer/src/engines.rs +++ b/beacon_node/execution_layer/src/engines.rs @@ -238,6 +238,11 @@ impl Engine { **self.state.read().await == EngineStateInternal::Synced } + /// Returns `true` if the engine has a status other than synced or syncing. + pub async fn is_offline(&self) -> bool { + EngineState::from(**self.state.read().await) == EngineState::Offline + } + /// Run the `EngineApi::upcheck` function if the node's last known state is not synced. This /// might be used to recover the node if offline. pub async fn upcheck(&self) { diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 16a7f3665f..19fa91b129 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -222,6 +222,11 @@ struct Inner { builder_profit_threshold: Uint256, log: Logger, always_prefer_builder_payload: bool, + /// Track whether the last `newPayload` call errored. + /// + /// This is used *only* in the informational sync status endpoint, so that a VC using this + /// node can prefer another node with a healthier EL. + last_new_payload_errored: RwLock, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] @@ -350,6 +355,7 @@ impl ExecutionLayer { builder_profit_threshold: Uint256::from(builder_profit_threshold), log, always_prefer_builder_payload, + last_new_payload_errored: RwLock::new(false), }; Ok(Self { @@ -542,6 +548,15 @@ impl ExecutionLayer { synced } + /// Return `true` if the execution layer is offline or returning errors on `newPayload`. + /// + /// This function should never be used to prevent any operation in the beacon node, but can + /// be used to give an indication on the HTTP API that the node's execution layer is struggling, + /// which can in turn be used by the VC. + pub async fn is_offline_or_erroring(&self) -> bool { + self.engine().is_offline().await || *self.inner.last_new_payload_errored.read().await + } + /// Updates the proposer preparation data provided by validators pub async fn update_proposer_preparation( &self, @@ -1116,18 +1131,6 @@ impl ExecutionLayer { } /// Maps to the `engine_newPayload` JSON-RPC call. - /// - /// ## Fallback Behaviour - /// - /// The request will be broadcast to all nodes, simultaneously. It will await a response (or - /// failure) from all nodes and then return based on the first of these conditions which - /// returns true: - /// - /// - Error::ConsensusFailure if some nodes return valid and some return invalid - /// - Valid, if any nodes return valid. - /// - Invalid, if any nodes return invalid. - /// - Syncing, if any nodes return syncing. - /// - An error, if all nodes return an error. pub async fn notify_new_payload( &self, execution_payload: &ExecutionPayload, @@ -1156,12 +1159,18 @@ impl ExecutionLayer { &["new_payload", status.status.into()], ); } + *self.inner.last_new_payload_errored.write().await = result.is_err(); process_payload_status(execution_payload.block_hash(), result, self.log()) .map_err(Box::new) .map_err(Error::EngineError) } + /// Update engine sync status. + pub async fn upcheck(&self) { + self.engine().upcheck().await; + } + /// Register that the given `validator_index` is going to produce a block at `slot`. /// /// The block will be built atop `head_block_root` and the EL will need to prepare an @@ -1221,18 +1230,6 @@ impl ExecutionLayer { } /// Maps to the `engine_consensusValidated` JSON-RPC call. - /// - /// ## Fallback Behaviour - /// - /// The request will be broadcast to all nodes, simultaneously. It will await a response (or - /// failure) from all nodes and then return based on the first of these conditions which - /// returns true: - /// - /// - Error::ConsensusFailure if some nodes return valid and some return invalid - /// - Valid, if any nodes return valid. - /// - Invalid, if any nodes return invalid. - /// - Syncing, if any nodes return syncing. - /// - An error, if all nodes return an error. pub async fn notify_forkchoice_updated( &self, head_block_hash: ExecutionBlockHash, 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 e3c58cfc27..79468b2116 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -30,7 +30,12 @@ pub async fn handle_rpc( .map_err(|s| (s, GENERIC_ERROR_CODE))?; match method { - ETH_SYNCING => Ok(JsonValue::Bool(false)), + ETH_SYNCING => ctx + .syncing_response + .lock() + .clone() + .map(JsonValue::Bool) + .map_err(|message| (message, GENERIC_ERROR_CODE)), ETH_GET_BLOCK_BY_NUMBER => { let tag = params .get(0) @@ -145,7 +150,9 @@ pub async fn handle_rpc( // Canned responses set by block hash take priority. if let Some(status) = ctx.get_new_payload_status(request.block_hash()) { - return Ok(serde_json::to_value(JsonPayloadStatusV1::from(status)).unwrap()); + return status + .map(|status| serde_json::to_value(JsonPayloadStatusV1::from(status)).unwrap()) + .map_err(|message| (message, GENERIC_ERROR_CODE)); } let (static_response, should_import) = @@ -320,11 +327,15 @@ pub async fn handle_rpc( // Canned responses set by block hash take priority. if let Some(status) = ctx.get_fcu_payload_status(&head_block_hash) { - let response = JsonForkchoiceUpdatedV1Response { - payload_status: JsonPayloadStatusV1::from(status), - payload_id: None, - }; - return Ok(serde_json::to_value(response).unwrap()); + return status + .map(|status| { + let response = JsonForkchoiceUpdatedV1Response { + payload_status: JsonPayloadStatusV1::from(status), + payload_id: None, + }; + serde_json::to_value(response).unwrap() + }) + .map_err(|message| (message, GENERIC_ERROR_CODE)); } let mut response = ctx diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index 9379a3c238..a8e7bab270 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -126,6 +126,7 @@ impl MockServer { hook: <_>::default(), new_payload_statuses: <_>::default(), fcu_payload_statuses: <_>::default(), + syncing_response: Arc::new(Mutex::new(Ok(false))), engine_capabilities: Arc::new(RwLock::new(DEFAULT_ENGINE_CAPABILITIES)), _phantom: PhantomData, }); @@ -414,14 +415,25 @@ impl MockServer { self.ctx .new_payload_statuses .lock() - .insert(block_hash, status); + .insert(block_hash, Ok(status)); } pub fn set_fcu_payload_status(&self, block_hash: ExecutionBlockHash, status: PayloadStatusV1) { self.ctx .fcu_payload_statuses .lock() - .insert(block_hash, status); + .insert(block_hash, Ok(status)); + } + + pub fn set_new_payload_error(&self, block_hash: ExecutionBlockHash, error: String) { + self.ctx + .new_payload_statuses + .lock() + .insert(block_hash, Err(error)); + } + + pub fn set_syncing_response(&self, res: Result) { + *self.ctx.syncing_response.lock() = res; } } @@ -478,8 +490,11 @@ pub struct Context { // // This is a more flexible and less stateful alternative to `static_new_payload_response` // and `preloaded_responses`. - pub new_payload_statuses: Arc>>, - pub fcu_payload_statuses: Arc>>, + pub new_payload_statuses: + Arc>>>, + pub fcu_payload_statuses: + Arc>>>, + pub syncing_response: Arc>>, pub engine_capabilities: Arc>, pub _phantom: PhantomData, @@ -489,14 +504,14 @@ impl Context { pub fn get_new_payload_status( &self, block_hash: &ExecutionBlockHash, - ) -> Option { + ) -> Option> { self.new_payload_statuses.lock().get(block_hash).cloned() } pub fn get_fcu_payload_status( &self, block_hash: &ExecutionBlockHash, - ) -> Option { + ) -> Option> { self.fcu_payload_statuses.lock().get(block_hash).cloned() } } diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 096d99f3f1..be1463f0c3 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -2285,28 +2285,40 @@ pub fn serve( .and(chain_filter.clone()) .and_then( |network_globals: Arc>, chain: Arc>| { - blocking_json_task(move || { - let head_slot = chain.canonical_head.cached_head().head_slot(); - let current_slot = chain.slot_clock.now_or_genesis().ok_or_else(|| { - warp_utils::reject::custom_server_error("Unable to read slot clock".into()) - })?; - - // Taking advantage of saturating subtraction on slot. - let sync_distance = current_slot - head_slot; - - let is_optimistic = chain - .is_optimistic_or_invalid_head() - .map_err(warp_utils::reject::beacon_chain_error)?; - - let syncing_data = api_types::SyncingData { - is_syncing: network_globals.sync_state.read().is_syncing(), - is_optimistic: Some(is_optimistic), - head_slot, - sync_distance, + async move { + let el_offline = if let Some(el) = &chain.execution_layer { + el.is_offline_or_erroring().await + } else { + true }; - Ok(api_types::GenericResponse::from(syncing_data)) - }) + blocking_json_task(move || { + let head_slot = chain.canonical_head.cached_head().head_slot(); + let current_slot = chain.slot_clock.now_or_genesis().ok_or_else(|| { + warp_utils::reject::custom_server_error( + "Unable to read slot clock".into(), + ) + })?; + + // Taking advantage of saturating subtraction on slot. + let sync_distance = current_slot - head_slot; + + let is_optimistic = chain + .is_optimistic_or_invalid_head() + .map_err(warp_utils::reject::beacon_chain_error)?; + + let syncing_data = api_types::SyncingData { + is_syncing: network_globals.sync_state.read().is_syncing(), + is_optimistic: Some(is_optimistic), + el_offline: Some(el_offline), + head_slot, + sync_distance, + }; + + Ok(api_types::GenericResponse::from(syncing_data)) + }) + .await + } }, ); diff --git a/beacon_node/http_api/tests/main.rs b/beacon_node/http_api/tests/main.rs index 342b72cc7d..f5916d8506 100644 --- a/beacon_node/http_api/tests/main.rs +++ b/beacon_node/http_api/tests/main.rs @@ -2,4 +2,5 @@ pub mod fork_tests; pub mod interactive_tests; +pub mod status_tests; pub mod tests; diff --git a/beacon_node/http_api/tests/status_tests.rs b/beacon_node/http_api/tests/status_tests.rs new file mode 100644 index 0000000000..ce725b75a9 --- /dev/null +++ b/beacon_node/http_api/tests/status_tests.rs @@ -0,0 +1,145 @@ +//! Tests related to the beacon node's sync status +use beacon_chain::{ + test_utils::{AttestationStrategy, BlockStrategy, SyncCommitteeStrategy}, + BlockError, +}; +use execution_layer::{PayloadStatusV1, PayloadStatusV1Status}; +use http_api::test_utils::InteractiveTester; +use types::{EthSpec, ExecPayload, ForkName, MinimalEthSpec, Slot}; + +type E = MinimalEthSpec; + +/// Create a new test environment that is post-merge with `chain_depth` blocks. +async fn post_merge_tester(chain_depth: u64, validator_count: u64) -> InteractiveTester { + // Test using latest fork so that we simulate conditions as similar to mainnet as possible. + let mut spec = ForkName::latest().make_genesis_spec(E::default_spec()); + spec.terminal_total_difficulty = 1.into(); + + let tester = InteractiveTester::::new(Some(spec), validator_count as usize).await; + let harness = &tester.harness; + let mock_el = harness.mock_execution_layer.as_ref().unwrap(); + let execution_ctx = mock_el.server.ctx.clone(); + + // Move to terminal block. + mock_el.server.all_payloads_valid(); + execution_ctx + .execution_block_generator + .write() + .move_to_terminal_block() + .unwrap(); + + // Create some chain depth. + harness.advance_slot(); + harness + .extend_chain_with_sync( + chain_depth as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + SyncCommitteeStrategy::AllValidators, + ) + .await; + tester +} + +/// Check `syncing` endpoint when the EL is syncing. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn el_syncing_then_synced() { + let num_blocks = E::slots_per_epoch() / 2; + let num_validators = E::slots_per_epoch(); + let tester = post_merge_tester(num_blocks, num_validators).await; + let harness = &tester.harness; + let mock_el = harness.mock_execution_layer.as_ref().unwrap(); + + // EL syncing + mock_el.server.set_syncing_response(Ok(true)); + mock_el.el.upcheck().await; + + let api_response = tester.client.get_node_syncing().await.unwrap().data; + assert_eq!(api_response.el_offline, Some(false)); + assert_eq!(api_response.is_optimistic, Some(false)); + assert_eq!(api_response.is_syncing, false); + + // EL synced + mock_el.server.set_syncing_response(Ok(false)); + mock_el.el.upcheck().await; + + let api_response = tester.client.get_node_syncing().await.unwrap().data; + assert_eq!(api_response.el_offline, Some(false)); + assert_eq!(api_response.is_optimistic, Some(false)); + assert_eq!(api_response.is_syncing, false); +} + +/// Check `syncing` endpoint when the EL is offline (errors on upcheck). +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn el_offline() { + let num_blocks = E::slots_per_epoch() / 2; + let num_validators = E::slots_per_epoch(); + let tester = post_merge_tester(num_blocks, num_validators).await; + let harness = &tester.harness; + let mock_el = harness.mock_execution_layer.as_ref().unwrap(); + + // EL offline + mock_el.server.set_syncing_response(Err("offline".into())); + mock_el.el.upcheck().await; + + let api_response = tester.client.get_node_syncing().await.unwrap().data; + assert_eq!(api_response.el_offline, Some(true)); + assert_eq!(api_response.is_optimistic, Some(false)); + assert_eq!(api_response.is_syncing, false); +} + +/// Check `syncing` endpoint when the EL errors on newPaylod but is not fully offline. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn el_error_on_new_payload() { + let num_blocks = E::slots_per_epoch() / 2; + let num_validators = E::slots_per_epoch(); + let tester = post_merge_tester(num_blocks, num_validators).await; + let harness = &tester.harness; + let mock_el = harness.mock_execution_layer.as_ref().unwrap(); + + // Make a block. + let pre_state = harness.get_current_state(); + let (block, _) = harness + .make_block(pre_state, Slot::new(num_blocks + 1)) + .await; + let block_hash = block + .message() + .body() + .execution_payload() + .unwrap() + .block_hash(); + + // Make sure `newPayload` errors for the new block. + mock_el + .server + .set_new_payload_error(block_hash, "error".into()); + + // Attempt to process the block, which should error. + harness.advance_slot(); + assert!(matches!( + harness.process_block_result(block.clone()).await, + Err(BlockError::ExecutionPayloadError(_)) + )); + + // The EL should now be *offline* according to the API. + let api_response = tester.client.get_node_syncing().await.unwrap().data; + assert_eq!(api_response.el_offline, Some(true)); + assert_eq!(api_response.is_optimistic, Some(false)); + assert_eq!(api_response.is_syncing, false); + + // Processing a block successfully should remove the status. + mock_el.server.set_new_payload_status( + block_hash, + PayloadStatusV1 { + status: PayloadStatusV1Status::Valid, + latest_valid_hash: Some(block_hash), + validation_error: None, + }, + ); + harness.process_block_result(block).await.unwrap(); + + let api_response = tester.client.get_node_syncing().await.unwrap().data; + assert_eq!(api_response.el_offline, Some(false)); + assert_eq!(api_response.is_optimistic, Some(false)); + assert_eq!(api_response.is_syncing, false); +} diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index fc78b2a9bf..a6c49ddaee 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -1721,6 +1721,8 @@ impl ApiTester { let expected = SyncingData { is_syncing: false, is_optimistic: Some(false), + // these tests run without the Bellatrix fork enabled + el_offline: Some(true), head_slot, sync_distance, }; diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index f58dc8e2a4..d7150bff71 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -577,6 +577,7 @@ pub struct VersionData { pub struct SyncingData { pub is_syncing: bool, pub is_optimistic: Option, + pub el_offline: Option, pub head_slot: Slot, pub sync_distance: Slot, } diff --git a/consensus/types/src/fork_name.rs b/consensus/types/src/fork_name.rs index 007d4c4daa..85144a6137 100644 --- a/consensus/types/src/fork_name.rs +++ b/consensus/types/src/fork_name.rs @@ -24,6 +24,11 @@ impl ForkName { ] } + pub fn latest() -> ForkName { + // This unwrap is safe as long as we have 1+ forks. It is tested below. + *ForkName::list_all().last().unwrap() + } + /// Set the activation slots in the given `ChainSpec` so that the fork named by `self` /// is the only fork in effect from genesis. pub fn make_genesis_spec(&self, mut spec: ChainSpec) -> ChainSpec { @@ -178,7 +183,7 @@ mod test { #[test] fn previous_and_next_fork_consistent() { - assert_eq!(ForkName::Capella.next_fork(), None); + assert_eq!(ForkName::latest().next_fork(), None); assert_eq!(ForkName::Base.previous_fork(), None); for (prev_fork, fork) in ForkName::list_all().into_iter().tuple_windows() { @@ -211,4 +216,15 @@ mod test { assert_eq!(ForkName::from_str("merge"), Ok(ForkName::Merge)); assert_eq!(ForkName::Merge.to_string(), "bellatrix"); } + + #[test] + fn fork_name_latest() { + assert_eq!(ForkName::latest(), *ForkName::list_all().last().unwrap()); + + let mut fork = ForkName::Base; + while let Some(next_fork) = fork.next_fork() { + fork = next_fork; + } + assert_eq!(ForkName::latest(), fork); + } } diff --git a/lighthouse/tests/validator_client.rs b/lighthouse/tests/validator_client.rs index 45cd989a44..8c1f0477c4 100644 --- a/lighthouse/tests/validator_client.rs +++ b/lighthouse/tests/validator_client.rs @@ -103,10 +103,8 @@ fn beacon_nodes_flag() { #[test] fn allow_unsynced_flag() { - CommandLineTest::new() - .flag("allow-unsynced", None) - .run() - .with_config(|config| assert!(config.allow_unsynced_beacon_node)); + // No-op, but doesn't crash. + CommandLineTest::new().flag("allow-unsynced", None).run(); } #[test] diff --git a/validator_client/src/beacon_node_fallback.rs b/validator_client/src/beacon_node_fallback.rs index 3e667429b4..2cbab3b218 100644 --- a/validator_client/src/beacon_node_fallback.rs +++ b/validator_client/src/beacon_node_fallback.rs @@ -28,7 +28,7 @@ const UPDATE_REQUIRED_LOG_HINT: &str = "this VC or the remote BN may need updati /// too early, we risk switching nodes between the time of publishing an attestation and publishing /// an aggregate; this may result in a missed aggregation. If we set this time too late, we risk not /// having the correct nodes up and running prior to the start of the slot. -const SLOT_LOOKAHEAD: Duration = Duration::from_secs(1); +const SLOT_LOOKAHEAD: Duration = Duration::from_secs(2); /// Indicates a measurement of latency between the VC and a BN. pub struct LatencyMeasurement { @@ -52,7 +52,7 @@ pub fn start_fallback_updater_service( let future = async move { loop { - beacon_nodes.update_unready_candidates().await; + beacon_nodes.update_all_candidates().await; let sleep_time = beacon_nodes .slot_clock @@ -385,33 +385,21 @@ impl BeaconNodeFallback { n } - /// Loop through any `self.candidates` that we don't think are online, compatible or synced and - /// poll them to see if their status has changed. + /// Loop through ALL candidates in `self.candidates` and update their sync status. /// - /// We do not poll nodes that are synced to avoid sending additional requests when everything is - /// going smoothly. - pub async fn update_unready_candidates(&self) { - let mut futures = Vec::new(); - for candidate in &self.candidates { - // There is a potential race condition between having the read lock and the write - // lock. The worst case of this race is running `try_become_ready` twice, which is - // acceptable. - // - // Note: `RequireSynced` is always set to false here. This forces us to recheck the sync - // status of nodes that were previously not-synced. - if candidate.status(RequireSynced::Yes).await.is_err() { - // There exists a race-condition that could result in `refresh_status` being called - // when the status does not require refreshing anymore. This is deemed an - // acceptable inefficiency. - futures.push(candidate.refresh_status( - self.slot_clock.as_ref(), - &self.spec, - &self.log, - )); - } - } + /// 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) { + let futures = self + .candidates + .iter() + .map(|candidate| { + candidate.refresh_status(self.slot_clock.as_ref(), &self.spec, &self.log) + }) + .collect::>(); - //run all updates concurrently and ignore results + // run all updates concurrently and ignore errors let _ = future::join_all(futures).await; } diff --git a/validator_client/src/check_synced.rs b/validator_client/src/check_synced.rs index c31457e288..fb88d33dae 100644 --- a/validator_client/src/check_synced.rs +++ b/validator_client/src/check_synced.rs @@ -36,7 +36,10 @@ pub async fn check_synced( } }; - let is_synced = !resp.data.is_syncing || (resp.data.sync_distance.as_u64() < SYNC_TOLERANCE); + // Default EL status to "online" for backwards-compatibility with BNs that don't include it. + let el_offline = resp.data.el_offline.unwrap_or(false); + let bn_is_synced = !resp.data.is_syncing || (resp.data.sync_distance.as_u64() < SYNC_TOLERANCE); + let is_synced = bn_is_synced && !el_offline; if let Some(log) = log_opt { if !is_synced { @@ -52,6 +55,7 @@ pub async fn check_synced( "sync_distance" => resp.data.sync_distance.as_u64(), "head_slot" => resp.data.head_slot.as_u64(), "endpoint" => %beacon_node, + "el_offline" => el_offline, ); } diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index 41ef85dfcd..6e199cb173 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -109,10 +109,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .arg( Arg::with_name("allow-unsynced") .long("allow-unsynced") - .help( - "If present, the validator client will still poll for duties if the beacon - node is not synced.", - ), + .help("DEPRECATED: this flag does nothing"), ) .arg( Arg::with_name("use-long-timeouts") diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index b6e808a86b..fa297dcfed 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -205,7 +205,13 @@ impl Config { ); } - config.allow_unsynced_beacon_node = cli_args.is_present("allow-unsynced"); + if cli_args.is_present("allow-unsynced") { + warn!( + log, + "The --allow-unsynced flag is deprecated"; + "msg" => "it no longer has any effect", + ); + } config.disable_run_on_all = cli_args.is_present("disable-run-on-all"); config.disable_auto_discover = cli_args.is_present("disable-auto-discover"); config.init_slashing_protection = cli_args.is_present("init-slashing-protection"); diff --git a/validator_client/src/duties_service.rs b/validator_client/src/duties_service.rs index 3cab6e7821..83cdb936aa 100644 --- a/validator_client/src/duties_service.rs +++ b/validator_client/src/duties_service.rs @@ -147,11 +147,6 @@ pub struct DutiesService { pub slot_clock: T, /// Provides HTTP access to remote beacon nodes. pub beacon_nodes: Arc>, - /// Controls whether or not this function will refuse to interact with non-synced beacon nodes. - /// - /// This functionality is a little redundant since most BNs will likely reject duties when they - /// aren't synced, but we keep it around for an emergency. - pub require_synced: RequireSynced, pub enable_high_validator_count_metrics: bool, pub context: RuntimeContext, pub spec: ChainSpec, @@ -421,7 +416,7 @@ async fn poll_validator_indices( let download_result = duties_service .beacon_nodes .first_success( - duties_service.require_synced, + RequireSynced::No, OfflineOnFailure::Yes, |beacon_node| async move { let _timer = metrics::start_timer_vec( @@ -618,7 +613,7 @@ async fn poll_beacon_attesters( if let Err(e) = duties_service .beacon_nodes .run( - duties_service.require_synced, + RequireSynced::No, OfflineOnFailure::Yes, |beacon_node| async move { let _timer = metrics::start_timer_vec( @@ -856,7 +851,7 @@ async fn post_validator_duties_attester( duties_service .beacon_nodes .first_success( - duties_service.require_synced, + RequireSynced::No, OfflineOnFailure::Yes, |beacon_node| async move { let _timer = metrics::start_timer_vec( @@ -1063,7 +1058,7 @@ async fn poll_beacon_proposers( let download_result = duties_service .beacon_nodes .first_success( - duties_service.require_synced, + RequireSynced::No, OfflineOnFailure::Yes, |beacon_node| async move { let _timer = metrics::start_timer_vec( diff --git a/validator_client/src/duties_service/sync.rs b/validator_client/src/duties_service/sync.rs index b9d4d70306..7a852091aa 100644 --- a/validator_client/src/duties_service/sync.rs +++ b/validator_client/src/duties_service/sync.rs @@ -1,4 +1,4 @@ -use crate::beacon_node_fallback::OfflineOnFailure; +use crate::beacon_node_fallback::{OfflineOnFailure, RequireSynced}; use crate::{ doppelganger_service::DoppelgangerStatus, duties_service::{DutiesService, Error}, @@ -422,7 +422,7 @@ pub async fn poll_sync_committee_duties_for_period ProductionValidatorClient { slot_clock: slot_clock.clone(), beacon_nodes: beacon_nodes.clone(), validator_store: validator_store.clone(), - require_synced: if config.allow_unsynced_beacon_node { - RequireSynced::Yes - } else { - RequireSynced::No - }, spec: context.eth2_config.spec.clone(), context: duties_context, enable_high_validator_count_metrics: config.enable_high_validator_count_metrics, @@ -620,8 +615,8 @@ async fn init_from_beacon_node( context: &RuntimeContext, ) -> Result<(u64, Hash256), String> { loop { - beacon_nodes.update_unready_candidates().await; - proposer_nodes.update_unready_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(); diff --git a/validator_client/src/preparation_service.rs b/validator_client/src/preparation_service.rs index fc80f2ded0..5bd93a5053 100644 --- a/validator_client/src/preparation_service.rs +++ b/validator_client/src/preparation_service.rs @@ -332,7 +332,7 @@ impl PreparationService { match self .beacon_nodes .run( - RequireSynced::Yes, + RequireSynced::No, OfflineOnFailure::Yes, |beacon_node| async move { beacon_node @@ -451,7 +451,7 @@ impl PreparationService { match self .beacon_nodes .first_success( - RequireSynced::Yes, + RequireSynced::No, OfflineOnFailure::No, |beacon_node| async move { beacon_node.post_validator_register_validator(batch).await diff --git a/validator_client/src/sync_committee_service.rs b/validator_client/src/sync_committee_service.rs index 3647396ed5..cc20cedfc6 100644 --- a/validator_client/src/sync_committee_service.rs +++ b/validator_client/src/sync_committee_service.rs @@ -178,7 +178,7 @@ impl SyncCommitteeService { let response = self .beacon_nodes .first_success( - RequireSynced::Yes, + RequireSynced::No, OfflineOnFailure::Yes, |beacon_node| async move { match beacon_node.get_beacon_blocks_root(BlockId::Head).await { From 75aea7054ccc1741628867d61036a9f34a3a8ff6 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Fri, 19 May 2023 05:13:05 +0000 Subject: [PATCH 08/63] Enshrine head state shuffling in the `shuffling_cache` (#4296) ## Issue Addressed #4281 ## Proposed Changes - Change `ShufflingCache` implementation from using `LruCache` to a custom cache that removes entry with lowest epoch instead of oldest insertion time. - Protect the "enshrined" head shufflings when inserting new committee cache entries. The shuffling ids matching the head's previous, current, and future epochs will never be ejected from the cache during `Self::insert_cache_item`. ## Additional Info There is a bonus point on shuffling preferences in the issue description that hasn't been implemented yet, as I haven't figured out a good way to do this: > However I'm not convinced since there are some complexities around tie-breaking when two entries have the same epoch. Perhaps preferring entries in the canonical chain is best? We should be able to check if a block is on the canonical chain by: ```rust canonical_head .fork_choice_read_lock() .contains_block(root) ``` However we need to interleave the shuffling and fork choice locks, which may cause deadlocks if we're not careful (mentioned by @paulhauner). Alternatively, we could use the `state.block_roots` field of the `chain.canonical_head.snapshot.beacon_state`, which avoids deadlock but requires more work. I'd like to get some feedback on review & testing before I dig deeper into the preferences stuff, as having the canonical head preference may already be quite useful in preventing the issue raised. Co-authored-by: Jimmy Chen --- beacon_node/beacon_chain/src/beacon_chain.rs | 1 + beacon_node/beacon_chain/src/builder.rs | 10 +- .../beacon_chain/src/canonical_head.rs | 31 ++ .../beacon_chain/src/shuffling_cache.rs | 274 +++++++++++++++--- 4 files changed, 280 insertions(+), 36 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 030137246a..2fa04304f5 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -5476,6 +5476,7 @@ impl BeaconChain { let shuffling_id = BlockShufflingIds { current: head_block.current_epoch_shuffling_id.clone(), next: head_block.next_epoch_shuffling_id.clone(), + previous: None, block_root: head_block.root, } .id_for_epoch(shuffling_epoch) diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index ca377635d6..b0f0015b9a 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -6,7 +6,7 @@ use crate::fork_revert::{reset_fork_choice_to_finalization, revert_to_fork_bound use crate::head_tracker::HeadTracker; use crate::migrate::{BackgroundMigrator, MigratorConfig}; use crate::persisted_beacon_chain::PersistedBeaconChain; -use crate::shuffling_cache::ShufflingCache; +use crate::shuffling_cache::{BlockShufflingIds, ShufflingCache}; use crate::snapshot_cache::{SnapshotCache, DEFAULT_SNAPSHOT_CACHE_SIZE}; use crate::timeout_rw_lock::TimeoutRwLock; use crate::validator_monitor::ValidatorMonitor; @@ -691,6 +691,8 @@ where )?; } + let head_shuffling_ids = BlockShufflingIds::try_from_head(head_block_root, &head_state)?; + let mut head_snapshot = BeaconSnapshot { beacon_block_root: head_block_root, beacon_block: Arc::new(head_block), @@ -847,7 +849,11 @@ where DEFAULT_SNAPSHOT_CACHE_SIZE, head_for_snapshot_cache, )), - shuffling_cache: TimeoutRwLock::new(ShufflingCache::new(shuffling_cache_size)), + shuffling_cache: TimeoutRwLock::new(ShufflingCache::new( + shuffling_cache_size, + head_shuffling_ids, + log.clone(), + )), eth1_finalization_cache: TimeoutRwLock::new(Eth1FinalizationCache::new(log.clone())), beacon_proposer_cache: <_>::default(), block_times_cache: <_>::default(), diff --git a/beacon_node/beacon_chain/src/canonical_head.rs b/beacon_node/beacon_chain/src/canonical_head.rs index 0e1c8a5305..2b1f714362 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -31,7 +31,9 @@ //! the head block root. This is unacceptable for fast-responding functions like the networking //! stack. +use crate::beacon_chain::ATTESTATION_CACHE_LOCK_TIMEOUT; use crate::persisted_fork_choice::PersistedForkChoice; +use crate::shuffling_cache::BlockShufflingIds; use crate::{ beacon_chain::{ BeaconForkChoice, BeaconStore, OverrideForkchoiceUpdate, @@ -846,6 +848,35 @@ impl BeaconChain { ); }); + match BlockShufflingIds::try_from_head( + new_snapshot.beacon_block_root, + &new_snapshot.beacon_state, + ) { + Ok(head_shuffling_ids) => { + self.shuffling_cache + .try_write_for(ATTESTATION_CACHE_LOCK_TIMEOUT) + .map(|mut shuffling_cache| { + shuffling_cache.update_head_shuffling_ids(head_shuffling_ids) + }) + .unwrap_or_else(|| { + error!( + self.log, + "Failed to obtain cache write lock"; + "lock" => "shuffling_cache", + "task" => "update head shuffling decision root" + ); + }); + } + Err(e) => { + error!( + self.log, + "Failed to get head shuffling ids"; + "error" => ?e, + "head_block_root" => ?new_snapshot.beacon_block_root + ); + } + } + observe_head_block_delays( &mut self.block_times_cache.write(), &new_head_proto_block, diff --git a/beacon_node/beacon_chain/src/shuffling_cache.rs b/beacon_node/beacon_chain/src/shuffling_cache.rs index 91a1e24d82..086e1c0949 100644 --- a/beacon_node/beacon_chain/src/shuffling_cache.rs +++ b/beacon_node/beacon_chain/src/shuffling_cache.rs @@ -1,10 +1,18 @@ -use crate::{metrics, BeaconChainError}; -use lru::LruCache; -use oneshot_broadcast::{oneshot, Receiver, Sender}; +use std::collections::HashMap; use std::sync::Arc; -use types::{beacon_state::CommitteeCache, AttestationShufflingId, Epoch, Hash256}; -/// The size of the LRU cache that stores committee caches for quicker verification. +use itertools::Itertools; +use slog::{debug, Logger}; + +use oneshot_broadcast::{oneshot, Receiver, Sender}; +use types::{ + beacon_state::CommitteeCache, AttestationShufflingId, BeaconState, Epoch, EthSpec, Hash256, + RelativeEpoch, +}; + +use crate::{metrics, BeaconChainError}; + +/// The size of the cache that stores committee caches for quicker verification. /// /// Each entry should be `8 + 800,000 = 800,008` bytes in size with 100k validators. (8-byte hash + /// 100k indices). Therefore, this cache should be approx `16 * 800,008 = 12.8 MB`. (Note: this @@ -45,18 +53,24 @@ impl CacheItem { } } -/// Provides an LRU cache for `CommitteeCache`. +/// Provides a cache for `CommitteeCache`. /// /// It has been named `ShufflingCache` because `CommitteeCacheCache` is a bit weird and looks like /// a find/replace error. pub struct ShufflingCache { - cache: LruCache, + cache: HashMap, + cache_size: usize, + head_shuffling_ids: BlockShufflingIds, + logger: Logger, } impl ShufflingCache { - pub fn new(cache_size: usize) -> Self { + pub fn new(cache_size: usize, head_shuffling_ids: BlockShufflingIds, logger: Logger) -> Self { Self { - cache: LruCache::new(cache_size), + cache: HashMap::new(), + cache_size, + head_shuffling_ids, + logger, } } @@ -76,7 +90,7 @@ impl ShufflingCache { metrics::inc_counter(&metrics::SHUFFLING_CACHE_PROMISE_HITS); metrics::inc_counter(&metrics::SHUFFLING_CACHE_HITS); let ready = CacheItem::Committee(committee); - self.cache.put(key.clone(), ready.clone()); + self.insert_cache_item(key.clone(), ready.clone()); Some(ready) } // The promise has not yet been resolved. Return the promise so the caller can await @@ -93,13 +107,12 @@ impl ShufflingCache { // It's worth noting that this is the only place where we removed unresolved // promises from the cache. This means unresolved promises will only be removed if // we try to access them again. This is OK, since the promises don't consume much - // memory and the nature of the LRU cache means that future, relevant entries will - // still be added to the cache. We expect that *all* promises should be resolved, - // unless there is a programming or database error. + // memory. We expect that *all* promises should be resolved, unless there is a + // programming or database error. Err(oneshot_broadcast::Error::SenderDropped) => { metrics::inc_counter(&metrics::SHUFFLING_CACHE_PROMISE_FAILS); metrics::inc_counter(&metrics::SHUFFLING_CACHE_MISSES); - self.cache.pop(key); + self.cache.remove(key); None } }, @@ -112,13 +125,13 @@ impl ShufflingCache { } pub fn contains(&self, key: &AttestationShufflingId) -> bool { - self.cache.contains(key) + self.cache.contains_key(key) } - pub fn insert_committee_cache( + pub fn insert_committee_cache( &mut self, key: AttestationShufflingId, - committee_cache: &T, + committee_cache: &C, ) { if self .cache @@ -127,13 +140,55 @@ impl ShufflingCache { // worth two in the promise-bush! .map_or(true, CacheItem::is_promise) { - self.cache.put( + self.insert_cache_item( key, CacheItem::Committee(committee_cache.to_arc_committee_cache()), ); } } + /// Prunes the cache first before inserting a new cache item. + fn insert_cache_item(&mut self, key: AttestationShufflingId, cache_item: CacheItem) { + self.prune_cache(); + self.cache.insert(key, cache_item); + } + + /// Prunes the `cache` to keep the size below the `cache_size` limit, based on the following + /// preferences: + /// - Entries from more recent epochs are preferred over older ones. + /// - Entries with shuffling ids matching the head's previous, current, and future epochs must + /// not be pruned. + fn prune_cache(&mut self) { + let target_cache_size = self.cache_size.saturating_sub(1); + if let Some(prune_count) = self.cache.len().checked_sub(target_cache_size) { + let shuffling_ids_to_prune = self + .cache + .keys() + .sorted_by_key(|key| key.shuffling_epoch) + .filter(|shuffling_id| { + Some(shuffling_id) + != self + .head_shuffling_ids + .id_for_epoch(shuffling_id.shuffling_epoch) + .as_ref() + .as_ref() + }) + .take(prune_count) + .cloned() + .collect::>(); + + 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 + ); + self.cache.remove(shuffling_id); + } + } + } + pub fn create_promise( &mut self, key: AttestationShufflingId, @@ -148,9 +203,17 @@ impl ShufflingCache { } let (sender, receiver) = oneshot(); - self.cache.put(key, CacheItem::Promise(receiver)); + self.insert_cache_item(key, CacheItem::Promise(receiver)); Ok(sender) } + + /// Inform the cache that the shuffling decision roots for the head has changed. + /// + /// The shufflings for the head's previous, current, and future epochs will never be ejected from + /// the cache during `Self::insert_cache_item`. + pub fn update_head_shuffling_ids(&mut self, head_shuffling_ids: BlockShufflingIds) { + self.head_shuffling_ids = head_shuffling_ids; + } } /// A helper trait to allow lazy-cloning of the committee cache when inserting into the cache. @@ -170,26 +233,29 @@ impl ToArcCommitteeCache for Arc { } } -impl Default for ShufflingCache { - fn default() -> Self { - Self::new(DEFAULT_CACHE_SIZE) - } -} - /// Contains the shuffling IDs for a beacon block. +#[derive(Clone)] pub struct BlockShufflingIds { pub current: AttestationShufflingId, pub next: AttestationShufflingId, + pub previous: Option, pub block_root: Hash256, } impl BlockShufflingIds { /// Returns the shuffling ID for the given epoch. /// - /// Returns `None` if `epoch` is prior to `self.current.shuffling_epoch`. + /// Returns `None` if `epoch` is prior to `self.previous?.shuffling_epoch` or + /// `self.current.shuffling_epoch` (if `previous` is `None`). pub fn id_for_epoch(&self, epoch: Epoch) -> Option { if epoch == self.current.shuffling_epoch { Some(self.current.clone()) + } else if self + .previous + .as_ref() + .map_or(false, |id| id.shuffling_epoch == epoch) + { + self.previous.clone() } else if epoch == self.next.shuffling_epoch { Some(self.next.clone()) } else if epoch > self.next.shuffling_epoch { @@ -201,18 +267,57 @@ impl BlockShufflingIds { None } } + + pub fn try_from_head( + head_block_root: Hash256, + head_state: &BeaconState, + ) -> Result { + let get_shuffling_id = |relative_epoch| { + AttestationShufflingId::new(head_block_root, head_state, relative_epoch).map_err(|e| { + format!( + "Unable to get attester shuffling decision slot for the epoch {:?}: {:?}", + relative_epoch, e + ) + }) + }; + + Ok(Self { + current: get_shuffling_id(RelativeEpoch::Current)?, + next: get_shuffling_id(RelativeEpoch::Next)?, + previous: Some(get_shuffling_id(RelativeEpoch::Previous)?), + block_root: head_block_root, + }) + } } // Disable tests in debug since the beacon chain harness is slow unless in release. #[cfg(not(debug_assertions))] #[cfg(test)] mod test { - use super::*; - use crate::test_utils::EphemeralHarnessType; + use task_executor::test_utils::null_logger; use types::*; - type BeaconChainHarness = - crate::test_utils::BeaconChainHarness>; + use crate::test_utils::EphemeralHarnessType; + + use super::*; + + type E = MinimalEthSpec; + type TestBeaconChainType = EphemeralHarnessType; + type BeaconChainHarness = crate::test_utils::BeaconChainHarness; + const TEST_CACHE_SIZE: usize = 5; + + // Creates a new shuffling cache for testing + fn new_shuffling_cache() -> ShufflingCache { + let current_epoch = 8; + let head_shuffling_ids = BlockShufflingIds { + current: shuffling_id(current_epoch), + next: shuffling_id(current_epoch + 1), + previous: Some(shuffling_id(current_epoch - 1)), + block_root: Hash256::from_low_u64_le(0), + }; + let logger = null_logger().unwrap(); + ShufflingCache::new(TEST_CACHE_SIZE, head_shuffling_ids, logger) + } /// Returns two different committee caches for testing. fn committee_caches() -> (Arc, Arc) { @@ -249,7 +354,7 @@ mod test { fn resolved_promise() { let (committee_a, _) = committee_caches(); let id_a = shuffling_id(1); - let mut cache = ShufflingCache::default(); + let mut cache = new_shuffling_cache(); // Create a promise. let sender = cache.create_promise(id_a.clone()).unwrap(); @@ -276,7 +381,7 @@ mod test { #[test] fn unresolved_promise() { let id_a = shuffling_id(1); - let mut cache = ShufflingCache::default(); + let mut cache = new_shuffling_cache(); // Create a promise. let sender = cache.create_promise(id_a.clone()).unwrap(); @@ -301,7 +406,7 @@ mod test { fn two_promises() { let (committee_a, committee_b) = committee_caches(); let (id_a, id_b) = (shuffling_id(1), shuffling_id(2)); - let mut cache = ShufflingCache::default(); + let mut cache = new_shuffling_cache(); // Create promise A. let sender_a = cache.create_promise(id_a.clone()).unwrap(); @@ -355,7 +460,7 @@ mod test { #[test] fn too_many_promises() { - let mut cache = ShufflingCache::default(); + let mut cache = new_shuffling_cache(); for i in 0..MAX_CONCURRENT_PROMISES { cache.create_promise(shuffling_id(i as u64)).unwrap(); @@ -375,4 +480,105 @@ mod test { "the cache should have two entries" ); } + + #[test] + fn should_insert_committee_cache() { + let mut cache = new_shuffling_cache(); + let id_a = shuffling_id(1); + let committee_cache_a = Arc::new(CommitteeCache::default()); + cache.insert_committee_cache(id_a.clone(), &committee_cache_a); + assert!( + matches!(cache.get(&id_a).unwrap(), CacheItem::Committee(committee_cache) if committee_cache == committee_cache_a), + "should insert committee cache" + ); + } + + #[test] + fn should_prune_committee_cache_with_lowest_epoch() { + let mut cache = new_shuffling_cache(); + let shuffling_id_and_committee_caches = (0..(TEST_CACHE_SIZE + 1)) + .map(|i| (shuffling_id(i as u64), Arc::new(CommitteeCache::default()))) + .collect::>(); + + for (shuffling_id, committee_cache) in shuffling_id_and_committee_caches.iter() { + cache.insert_committee_cache(shuffling_id.clone(), committee_cache); + } + + for i in 1..(TEST_CACHE_SIZE + 1) { + assert!( + cache.contains(&shuffling_id_and_committee_caches.get(i).unwrap().0), + "should contain recent epoch shuffling ids" + ); + } + + assert!( + !cache.contains(&shuffling_id_and_committee_caches.get(0).unwrap().0), + "should not contain oldest epoch shuffling id" + ); + assert_eq!( + cache.cache.len(), + cache.cache_size, + "should limit cache size" + ); + } + + #[test] + fn should_retain_head_state_shufflings() { + let mut cache = new_shuffling_cache(); + let current_epoch = 10; + let committee_cache = Arc::new(CommitteeCache::default()); + + // Insert a few entries for next the epoch with different decision roots. + for i in 0..TEST_CACHE_SIZE { + let shuffling_id = AttestationShufflingId { + shuffling_epoch: (current_epoch + 1).into(), + shuffling_decision_block: Hash256::from_low_u64_be(current_epoch + i as u64), + }; + cache.insert_committee_cache(shuffling_id, &committee_cache); + } + + // Now, update the head shuffling ids + let head_shuffling_ids = BlockShufflingIds { + current: shuffling_id(current_epoch), + next: shuffling_id(current_epoch + 1), + previous: Some(shuffling_id(current_epoch - 1)), + block_root: Hash256::from_low_u64_le(42), + }; + cache.update_head_shuffling_ids(head_shuffling_ids.clone()); + + // Insert head state shuffling ids. Should not be overridden by other shuffling ids. + cache.insert_committee_cache(head_shuffling_ids.current.clone(), &committee_cache); + cache.insert_committee_cache(head_shuffling_ids.next.clone(), &committee_cache); + cache.insert_committee_cache( + head_shuffling_ids.previous.clone().unwrap(), + &committee_cache, + ); + + // Insert a few entries for older epochs. + for i in 0..TEST_CACHE_SIZE { + let shuffling_id = AttestationShufflingId { + shuffling_epoch: Epoch::from(i), + shuffling_decision_block: Hash256::from_low_u64_be(i as u64), + }; + cache.insert_committee_cache(shuffling_id, &committee_cache); + } + + assert!( + cache.contains(&head_shuffling_ids.current), + "should retain head shuffling id for the current epoch." + ); + assert!( + cache.contains(&head_shuffling_ids.next), + "should retain head shuffling id for the next epoch." + ); + assert!( + cache.contains(&head_shuffling_ids.previous.unwrap()), + "should retain head shuffling id for previous epoch." + ); + assert_eq!( + cache.cache.len(), + cache.cache_size, + "should limit cache size" + ); + } } From 01ae37ac37c746f3275894b454701b00d1fbd65b Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Fri, 19 May 2023 05:13:07 +0000 Subject: [PATCH 09/63] Add more metrics for tracking sync messages (#4308) ## Issue Addressed NA ## Proposed Changes Adds metrics to track validators that are submitting equivocating (but not slashable) sync messages. This follows on from some research we've been doing in a separate fork of LH. ## Additional Info @jimmygchen and @michaelsproul have already run their eyes over this so it should be easy to get into v4.2.0, IMO. --- beacon_node/beacon_chain/src/metrics.rs | 8 + .../beacon_chain/src/observed_attesters.rs | 289 ++++++++++++++---- .../src/sync_committee_verification.rs | 61 +++- .../tests/sync_committee_verification.rs | 177 ++++++++++- beacon_node/http_api/src/sync_committees.rs | 4 + .../beacon_processor/worker/gossip_methods.rs | 19 ++ 6 files changed, 477 insertions(+), 81 deletions(-) diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index b52c4258fe..d0f695062f 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -874,6 +874,14 @@ lazy_static! { "beacon_sync_committee_message_gossip_verification_seconds", "Full runtime of sync contribution gossip verification" ); + pub static ref SYNC_MESSAGE_EQUIVOCATIONS: Result = try_create_int_counter( + "sync_message_equivocations_total", + "Number of sync messages with the same validator index for different blocks" + ); + pub static ref SYNC_MESSAGE_EQUIVOCATIONS_TO_HEAD: Result = try_create_int_counter( + "sync_message_equivocations_to_head_total", + "Number of sync message which conflict with a previous message but elect the head" + ); /* * Sync Committee Contribution Verification diff --git a/beacon_node/beacon_chain/src/observed_attesters.rs b/beacon_node/beacon_chain/src/observed_attesters.rs index ed22beaec6..59c67bd1b9 100644 --- a/beacon_node/beacon_chain/src/observed_attesters.rs +++ b/beacon_node/beacon_chain/src/observed_attesters.rs @@ -20,7 +20,7 @@ use std::collections::{HashMap, HashSet}; use std::hash::Hash; use std::marker::PhantomData; use types::slot_data::SlotData; -use types::{Epoch, EthSpec, Slot, Unsigned}; +use types::{Epoch, EthSpec, Hash256, Slot, Unsigned}; /// The maximum capacity of the `AutoPruningEpochContainer`. /// @@ -39,10 +39,10 @@ pub const MAX_CACHED_EPOCHS: u64 = 3; pub type ObservedAttesters = AutoPruningEpochContainer; pub type ObservedSyncContributors = - AutoPruningSlotContainer, E>; + AutoPruningSlotContainer, E>; pub type ObservedAggregators = AutoPruningEpochContainer; pub type ObservedSyncAggregators = - AutoPruningSlotContainer; + AutoPruningSlotContainer; #[derive(Debug, PartialEq)] pub enum Error { @@ -62,7 +62,7 @@ pub enum Error { } /// Implemented on an item in an `AutoPruningContainer`. -pub trait Item { +pub trait Item { /// Instantiate `Self` with the given `capacity`. fn with_capacity(capacity: usize) -> Self; @@ -75,11 +75,11 @@ pub trait Item { /// Returns the number of validators that have been observed by `self`. fn validator_count(&self) -> usize; - /// Store `validator_index` in `self`. - fn insert(&mut self, validator_index: usize) -> bool; + /// Store `validator_index` and `value` in `self`. + fn insert(&mut self, validator_index: usize, value: T) -> bool; - /// Returns `true` if `validator_index` has been stored in `self`. - fn contains(&self, validator_index: usize) -> bool; + /// Returns `Some(T)` if there is an entry for `validator_index`. + fn get(&self, validator_index: usize) -> Option; } /// Stores a `BitVec` that represents which validator indices have attested or sent sync committee @@ -88,7 +88,7 @@ pub struct EpochBitfield { bitfield: BitVec, } -impl Item for EpochBitfield { +impl Item<()> for EpochBitfield { fn with_capacity(capacity: usize) -> Self { Self { bitfield: BitVec::with_capacity(capacity), @@ -108,7 +108,7 @@ impl Item for EpochBitfield { self.bitfield.iter().filter(|bit| **bit).count() } - fn insert(&mut self, validator_index: usize) -> bool { + fn insert(&mut self, validator_index: usize, _value: ()) -> bool { self.bitfield .get_mut(validator_index) .map(|mut bit| { @@ -129,8 +129,11 @@ impl Item for EpochBitfield { }) } - fn contains(&self, validator_index: usize) -> bool { - self.bitfield.get(validator_index).map_or(false, |bit| *bit) + fn get(&self, validator_index: usize) -> Option<()> { + self.bitfield + .get(validator_index) + .map_or(false, |bit| *bit) + .then_some(()) } } @@ -140,7 +143,7 @@ pub struct EpochHashSet { set: HashSet, } -impl Item for EpochHashSet { +impl Item<()> for EpochHashSet { fn with_capacity(capacity: usize) -> Self { Self { set: HashSet::with_capacity(capacity), @@ -163,27 +166,27 @@ impl Item for EpochHashSet { /// Inserts the `validator_index` in the set. Returns `true` if the `validator_index` was /// already in the set. - fn insert(&mut self, validator_index: usize) -> bool { + fn insert(&mut self, validator_index: usize, _value: ()) -> bool { !self.set.insert(validator_index) } /// Returns `true` if the `validator_index` is in the set. - fn contains(&self, validator_index: usize) -> bool { - self.set.contains(&validator_index) + fn get(&self, validator_index: usize) -> Option<()> { + self.set.contains(&validator_index).then_some(()) } } /// Stores a `HashSet` of which validator indices have created a sync aggregate during a /// slot. pub struct SyncContributorSlotHashSet { - set: HashSet, + map: HashMap, phantom: PhantomData, } -impl Item for SyncContributorSlotHashSet { +impl Item for SyncContributorSlotHashSet { fn with_capacity(capacity: usize) -> Self { Self { - set: HashSet::with_capacity(capacity), + map: HashMap::with_capacity(capacity), phantom: PhantomData, } } @@ -194,22 +197,24 @@ impl Item for SyncContributorSlotHashSet { } fn len(&self) -> usize { - self.set.len() + self.map.len() } fn validator_count(&self) -> usize { - self.set.len() + self.map.len() } /// Inserts the `validator_index` in the set. Returns `true` if the `validator_index` was /// already in the set. - fn insert(&mut self, validator_index: usize) -> bool { - !self.set.insert(validator_index) + fn insert(&mut self, validator_index: usize, beacon_block_root: Hash256) -> bool { + self.map + .insert(validator_index, beacon_block_root) + .is_some() } /// Returns `true` if the `validator_index` is in the set. - fn contains(&self, validator_index: usize) -> bool { - self.set.contains(&validator_index) + fn get(&self, validator_index: usize) -> Option { + self.map.get(&validator_index).copied() } } @@ -219,7 +224,7 @@ pub struct SyncAggregatorSlotHashSet { set: HashSet, } -impl Item for SyncAggregatorSlotHashSet { +impl Item<()> for SyncAggregatorSlotHashSet { fn with_capacity(capacity: usize) -> Self { Self { set: HashSet::with_capacity(capacity), @@ -241,13 +246,13 @@ impl Item for SyncAggregatorSlotHashSet { /// Inserts the `validator_index` in the set. Returns `true` if the `validator_index` was /// already in the set. - fn insert(&mut self, validator_index: usize) -> bool { + fn insert(&mut self, validator_index: usize, _value: ()) -> bool { !self.set.insert(validator_index) } /// Returns `true` if the `validator_index` is in the set. - fn contains(&self, validator_index: usize) -> bool { - self.set.contains(&validator_index) + fn get(&self, validator_index: usize) -> Option<()> { + self.set.contains(&validator_index).then_some(()) } } @@ -275,7 +280,7 @@ impl Default for AutoPruningEpochContainer { } } -impl AutoPruningEpochContainer { +impl, E: EthSpec> AutoPruningEpochContainer { /// Observe that `validator_index` has produced attestation `a`. Returns `Ok(true)` if `a` has /// previously been observed for `validator_index`. /// @@ -293,7 +298,7 @@ impl AutoPruningEpochContainer { self.prune(epoch); if let Some(item) = self.items.get_mut(&epoch) { - Ok(item.insert(validator_index)) + Ok(item.insert(validator_index, ())) } else { // To avoid re-allocations, try and determine a rough initial capacity for the new item // by obtaining the mean size of all items in earlier epoch. @@ -309,7 +314,7 @@ impl AutoPruningEpochContainer { let initial_capacity = sum.checked_div(count).unwrap_or_else(T::default_capacity); let mut item = T::with_capacity(initial_capacity); - item.insert(validator_index); + item.insert(validator_index, ()); self.items.insert(epoch, item); Ok(false) @@ -333,7 +338,7 @@ impl AutoPruningEpochContainer { let exists = self .items .get(&epoch) - .map_or(false, |item| item.contains(validator_index)); + .map_or(false, |item| item.get(validator_index).is_some()); Ok(exists) } @@ -392,7 +397,7 @@ impl AutoPruningEpochContainer { pub fn index_seen_at_epoch(&self, index: usize, epoch: Epoch) -> bool { self.items .get(&epoch) - .map(|item| item.contains(index)) + .map(|item| item.get(index).is_some()) .unwrap_or(false) } } @@ -405,23 +410,63 @@ impl AutoPruningEpochContainer { /// sync contributions with an epoch prior to `data.slot - 3` will be cleared from the cache. /// /// `V` should be set to a `SyncAggregatorSlotHashSet` or a `SyncContributorSlotHashSet`. -pub struct AutoPruningSlotContainer { +pub struct AutoPruningSlotContainer { lowest_permissible_slot: Slot, items: HashMap, - _phantom: PhantomData, + _phantom_e: PhantomData, + _phantom_s: PhantomData, } -impl Default for AutoPruningSlotContainer { +impl Default for AutoPruningSlotContainer { fn default() -> Self { Self { lowest_permissible_slot: Slot::new(0), items: HashMap::new(), - _phantom: PhantomData, + _phantom_e: PhantomData, + _phantom_s: PhantomData, } } } -impl AutoPruningSlotContainer { +impl, E: EthSpec> + AutoPruningSlotContainer +{ + /// Observes the given `value` for the given `validator_index`. + /// + /// The `override_observation` function is supplied `previous_observation` + /// and `value`. If it returns `true`, then any existing observation will be + /// overridden. + /// + /// This function returns `Some` if: + /// - An observation already existed for the validator, AND, + /// - The `override_observation` function returned `false`. + /// + /// Alternatively, it returns `None` if: + /// - An observation did not already exist for the given validator, OR, + /// - The `override_observation` function returned `true`. + pub fn observe_validator_with_override( + &mut self, + key: K, + validator_index: usize, + value: S, + override_observation: F, + ) -> Result, Error> + where + F: Fn(&S, &S) -> bool, + { + if let Some(prev_observation) = self.observation_for_validator(key, validator_index)? { + if override_observation(&prev_observation, &value) { + self.observe_validator(key, validator_index, value)?; + Ok(None) + } else { + Ok(Some(prev_observation)) + } + } else { + self.observe_validator(key, validator_index, value)?; + Ok(None) + } + } + /// Observe that `validator_index` has produced a sync committee message. Returns `Ok(true)` if /// the sync committee message has previously been observed for `validator_index`. /// @@ -429,14 +474,19 @@ impl AutoPruningSlotContainer Result { + pub fn observe_validator( + &mut self, + key: K, + validator_index: usize, + value: S, + ) -> Result { let slot = key.get_slot(); self.sanitize_request(slot, validator_index)?; self.prune(slot); if let Some(item) = self.items.get_mut(&key) { - Ok(item.insert(validator_index)) + Ok(item.insert(validator_index, value)) } else { // To avoid re-allocations, try and determine a rough initial capacity for the new item // by obtaining the mean size of all items in earlier slot. @@ -452,32 +502,45 @@ impl AutoPruningSlotContainer Result { + self.observation_for_validator(key, validator_index) + .map(|observation| observation.is_some()) + } + + /// Returns `Ok(Some)` if the `validator_index` has already produced a + /// conflicting sync committee message. + /// + /// ## Errors + /// + /// - `validator_index` is higher than `VALIDATOR_REGISTRY_LIMIT`. + /// - `key.slot` is earlier than `self.lowest_permissible_slot`. + pub fn observation_for_validator( + &self, + key: K, + validator_index: usize, + ) -> Result, Error> { self.sanitize_request(key.get_slot(), validator_index)?; - let exists = self + let observation = self .items .get(&key) - .map_or(false, |item| item.contains(validator_index)); + .and_then(|item| item.get(validator_index)); - Ok(exists) + Ok(observation) } /// Returns the number of validators that have been observed at the given `slot`. Returns @@ -561,6 +624,116 @@ mod tests { type E = types::MainnetEthSpec; + #[test] + fn value_storage() { + type Container = AutoPruningSlotContainer, E>; + + let mut store: Container = <_>::default(); + let key = Slot::new(0); + let validator_index = 0; + let value = Hash256::zero(); + + // Assert there is no entry. + assert!(store + .observation_for_validator(key, validator_index) + .unwrap() + .is_none()); + assert!(!store + .validator_has_been_observed(key, validator_index) + .unwrap()); + + // Add an entry. + assert!(!store + .observe_validator(key, validator_index, value) + .unwrap()); + + // Assert there is a correct entry. + assert_eq!( + store + .observation_for_validator(key, validator_index) + .unwrap(), + Some(value) + ); + assert!(store + .validator_has_been_observed(key, validator_index) + .unwrap()); + + let alternate_value = Hash256::from_low_u64_be(1); + + // Assert that override false does not override. + assert_eq!( + store + .observe_validator_with_override(key, validator_index, alternate_value, |_, _| { + false + }) + .unwrap(), + Some(value) + ); + + // Assert that override true overrides and acts as if there was never an + // entry there. + assert_eq!( + store + .observe_validator_with_override(key, validator_index, alternate_value, |_, _| { + true + }) + .unwrap(), + None + ); + assert_eq!( + store + .observation_for_validator(key, validator_index) + .unwrap(), + Some(alternate_value) + ); + + // Reset the store. + let mut store: Container = <_>::default(); + + // Asset that a new entry with override = false is inserted + assert_eq!( + store + .observation_for_validator(key, validator_index) + .unwrap(), + None + ); + assert_eq!( + store + .observe_validator_with_override(key, validator_index, value, |_, _| { false }) + .unwrap(), + None, + ); + assert_eq!( + store + .observation_for_validator(key, validator_index) + .unwrap(), + Some(value) + ); + + // Reset the store. + let mut store: Container = <_>::default(); + + // Asset that a new entry with override = true is inserted + assert_eq!( + store + .observation_for_validator(key, validator_index) + .unwrap(), + None + ); + assert_eq!( + store + .observe_validator_with_override(key, validator_index, value, |_, _| { true }) + .unwrap(), + None, + ); + assert_eq!( + store + .observation_for_validator(key, validator_index) + .unwrap(), + Some(value) + ); + } + macro_rules! test_suite_epoch { ($mod_name: ident, $type: ident) => { #[cfg(test)] @@ -722,7 +895,7 @@ mod tests { test_suite_epoch!(observed_aggregators, ObservedAggregators); macro_rules! test_suite_slot { - ($mod_name: ident, $type: ident) => { + ($mod_name: ident, $type: ident, $value: expr) => { #[cfg(test)] mod $mod_name { use super::*; @@ -737,7 +910,7 @@ mod tests { "should indicate an unknown item is unknown" ); assert_eq!( - store.observe_validator(key, i), + store.observe_validator(key, i, $value), Ok(false), "should observe new item" ); @@ -750,7 +923,7 @@ mod tests { "should indicate a known item is known" ); assert_eq!( - store.observe_validator(key, i), + store.observe_validator(key, i, $value), Ok(true), "should acknowledge an existing item" ); @@ -997,6 +1170,10 @@ mod tests { } }; } - test_suite_slot!(observed_sync_contributors, ObservedSyncContributors); - test_suite_slot!(observed_sync_aggregators, ObservedSyncAggregators); + test_suite_slot!( + observed_sync_contributors, + ObservedSyncContributors, + Hash256::zero() + ); + test_suite_slot!(observed_sync_aggregators, ObservedSyncAggregators, ()); } diff --git a/beacon_node/beacon_chain/src/sync_committee_verification.rs b/beacon_node/beacon_chain/src/sync_committee_verification.rs index 4b4228e71d..14cdc2400d 100644 --- a/beacon_node/beacon_chain/src/sync_committee_verification.rs +++ b/beacon_node/beacon_chain/src/sync_committee_verification.rs @@ -153,7 +153,21 @@ pub enum Error { /// It's unclear if this sync message is valid, however we have already observed a /// signature from this validator for this slot and should not observe /// another. - PriorSyncCommitteeMessageKnown { validator_index: u64, slot: Slot }, + PriorSyncCommitteeMessageKnown { + validator_index: u64, + slot: Slot, + prev_root: Hash256, + new_root: Hash256, + }, + /// We have already observed a contribution for the aggregator and refuse to + /// process another. + /// + /// ## Peer scoring + /// + /// It's unclear if this sync message is valid, however we have already observed a + /// signature from this validator for this slot and should not observe + /// another. + PriorSyncContributionMessageKnown { validator_index: u64, slot: Slot }, /// The sync committee message was received on an invalid sync committee message subnet. /// /// ## Peer scoring @@ -378,10 +392,10 @@ impl VerifiedSyncContribution { if chain .observed_sync_aggregators .write() - .observe_validator(observed_key, aggregator_index as usize) + .observe_validator(observed_key, aggregator_index as usize, ()) .map_err(BeaconChainError::from)? { - return Err(Error::PriorSyncCommitteeMessageKnown { + return Err(Error::PriorSyncContributionMessageKnown { validator_index: aggregator_index, slot: contribution.slot, }); @@ -450,19 +464,40 @@ impl VerifiedSyncCommitteeMessage { // The sync committee message is the first valid message received for the participating validator // for the slot, sync_message.slot. let validator_index = sync_message.validator_index; - if chain + let head_root = chain.canonical_head.cached_head().head_block_root(); + let new_root = sync_message.beacon_block_root; + let should_override_prev = |prev_root: &Hash256, new_root: &Hash256| { + let roots_differ = new_root != prev_root; + let new_elects_head = new_root == &head_root; + + if roots_differ { + // Track sync committee messages that differ from each other. + metrics::inc_counter(&metrics::SYNC_MESSAGE_EQUIVOCATIONS); + if new_elects_head { + // Track sync committee messages that swap from an old block to a new block. + metrics::inc_counter(&metrics::SYNC_MESSAGE_EQUIVOCATIONS_TO_HEAD); + } + } + + roots_differ && new_elects_head + }; + if let Some(prev_root) = chain .observed_sync_contributors .read() - .validator_has_been_observed( + .observation_for_validator( SlotSubcommitteeIndex::new(sync_message.slot, subnet_id.into()), validator_index as usize, ) .map_err(BeaconChainError::from)? { - return Err(Error::PriorSyncCommitteeMessageKnown { - validator_index, - slot: sync_message.slot, - }); + if !should_override_prev(&prev_root, &new_root) { + return Err(Error::PriorSyncCommitteeMessageKnown { + validator_index, + slot: sync_message.slot, + prev_root, + new_root, + }); + } } // The aggregate signature of the sync committee message is valid. @@ -474,18 +509,22 @@ impl VerifiedSyncCommitteeMessage { // It's important to double check that the sync committee message still hasn't been observed, since // there can be a race-condition if we receive two sync committee messages at the same time and // process them in different threads. - if chain + if let Some(prev_root) = chain .observed_sync_contributors .write() - .observe_validator( + .observe_validator_with_override( SlotSubcommitteeIndex::new(sync_message.slot, subnet_id.into()), validator_index as usize, + sync_message.beacon_block_root, + should_override_prev, ) .map_err(BeaconChainError::from)? { return Err(Error::PriorSyncCommitteeMessageKnown { validator_index, slot: sync_message.slot, + prev_root, + new_root, }); } diff --git a/beacon_node/beacon_chain/tests/sync_committee_verification.rs b/beacon_node/beacon_chain/tests/sync_committee_verification.rs index 239f55e7d3..4204a51212 100644 --- a/beacon_node/beacon_chain/tests/sync_committee_verification.rs +++ b/beacon_node/beacon_chain/tests/sync_committee_verification.rs @@ -5,12 +5,16 @@ use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType, Relativ use int_to_bytes::int_to_bytes32; use lazy_static::lazy_static; use safe_arith::SafeArith; +use state_processing::{ + per_block_processing::{altair::sync_committee::process_sync_aggregate, VerifySignatures}, + state_advance::complete_state_advance, +}; use store::{SignedContributionAndProof, SyncCommitteeMessage}; use tree_hash::TreeHash; use types::consts::altair::SYNC_COMMITTEE_SUBNET_COUNT; use types::{ AggregateSignature, Epoch, EthSpec, Hash256, Keypair, MainnetEthSpec, SecretKey, Slot, - SyncSelectionProof, SyncSubnetId, Unsigned, + SyncContributionData, SyncSelectionProof, SyncSubnetId, Unsigned, }; pub type E = MainnetEthSpec; @@ -47,10 +51,29 @@ fn get_valid_sync_committee_message( relative_sync_committee: RelativeSyncCommittee, message_index: usize, ) -> (SyncCommitteeMessage, usize, SecretKey, SyncSubnetId) { - let head_state = harness.chain.head_beacon_state_cloned(); let head_block_root = harness.chain.head_snapshot().beacon_block_root; + get_valid_sync_committee_message_for_block( + harness, + slot, + relative_sync_committee, + message_index, + head_block_root, + ) +} + +/// Returns a sync message that is valid for some slot in the given `chain`. +/// +/// Also returns some info about who created it. +fn get_valid_sync_committee_message_for_block( + harness: &BeaconChainHarness>, + slot: Slot, + relative_sync_committee: RelativeSyncCommittee, + message_index: usize, + block_root: Hash256, +) -> (SyncCommitteeMessage, usize, SecretKey, SyncSubnetId) { + let head_state = harness.chain.head_beacon_state_cloned(); let (signature, _) = harness - .make_sync_committee_messages(&head_state, head_block_root, slot, relative_sync_committee) + .make_sync_committee_messages(&head_state, block_root, slot, relative_sync_committee) .get(0) .expect("sync messages should exist") .get(message_index) @@ -119,7 +142,7 @@ fn get_non_aggregator( subcommittee.iter().find_map(|pubkey| { let validator_index = harness .chain - .validator_index(&pubkey) + .validator_index(pubkey) .expect("should get validator index") .expect("pubkey should exist in beacon chain"); @@ -376,7 +399,7 @@ async fn aggregated_gossip_verification() { SyncCommitteeError::AggregatorNotInCommittee { aggregator_index } - if aggregator_index == valid_aggregate.message.aggregator_index as u64 + if aggregator_index == valid_aggregate.message.aggregator_index ); /* @@ -472,7 +495,7 @@ async fn aggregated_gossip_verification() { assert_invalid!( "sync contribution created with incorrect sync committee", - next_valid_contribution.clone(), + next_valid_contribution, SyncCommitteeError::InvalidSignature | SyncCommitteeError::AggregatorNotInCommittee { .. } ); } @@ -496,6 +519,30 @@ async fn unaggregated_gossip_verification() { let (valid_sync_committee_message, expected_validator_index, validator_sk, subnet_id) = get_valid_sync_committee_message(&harness, current_slot, RelativeSyncCommittee::Current, 0); + let parent_root = harness.chain.head_snapshot().beacon_block.parent_root(); + let (valid_sync_committee_message_to_parent, _, _, _) = + get_valid_sync_committee_message_for_block( + &harness, + current_slot, + RelativeSyncCommittee::Current, + 0, + parent_root, + ); + + assert_eq!( + valid_sync_committee_message.slot, valid_sync_committee_message_to_parent.slot, + "test pre-condition: same slot" + ); + assert_eq!( + valid_sync_committee_message.validator_index, + valid_sync_committee_message_to_parent.validator_index, + "test pre-condition: same validator index" + ); + assert!( + valid_sync_committee_message.beacon_block_root + != valid_sync_committee_message_to_parent.beacon_block_root, + "test pre-condition: differing roots" + ); macro_rules! assert_invalid { ($desc: tt, $attn_getter: expr, $subnet_getter: expr, $($error: pat_param) |+ $( if $guard: expr )?) => { @@ -602,28 +649,130 @@ async fn unaggregated_gossip_verification() { SyncCommitteeError::InvalidSignature ); + let head_root = valid_sync_committee_message.beacon_block_root; + let parent_root = valid_sync_committee_message_to_parent.beacon_block_root; + + let verifed_message_to_parent = harness + .chain + .verify_sync_committee_message_for_gossip( + valid_sync_committee_message_to_parent.clone(), + subnet_id, + ) + .expect("valid sync message to parent should be verified"); + // Add the aggregate to the pool. harness .chain - .verify_sync_committee_message_for_gossip(valid_sync_committee_message.clone(), subnet_id) - .expect("valid sync message should be verified"); + .add_to_naive_sync_aggregation_pool(verifed_message_to_parent) + .unwrap(); /* * The following test ensures that: * - * There has been no other valid sync committee message for the declared slot for the - * validator referenced by sync_committee_message.validator_index. + * A sync committee message from the same validator to the same block will + * be rejected. */ assert_invalid!( - "sync message that has already been seen", - valid_sync_committee_message, + "sync message to parent block that has already been seen", + valid_sync_committee_message_to_parent.clone(), subnet_id, SyncCommitteeError::PriorSyncCommitteeMessageKnown { validator_index, slot, + prev_root, + new_root } - if validator_index == expected_validator_index as u64 && slot == current_slot + if validator_index == expected_validator_index as u64 && slot == current_slot && prev_root == parent_root && new_root == parent_root ); + let verified_message_to_head = harness + .chain + .verify_sync_committee_message_for_gossip(valid_sync_committee_message.clone(), subnet_id) + .expect("valid sync message to the head should be verified"); + // Add the aggregate to the pool. + harness + .chain + .add_to_naive_sync_aggregation_pool(verified_message_to_head) + .unwrap(); + + /* + * The following test ensures that: + * + * A sync committee message from the same validator to the same block will + * be rejected. + */ + assert_invalid!( + "sync message to the head that has already been seen", + valid_sync_committee_message.clone(), + subnet_id, + SyncCommitteeError::PriorSyncCommitteeMessageKnown { + validator_index, + slot, + prev_root, + new_root + } + if validator_index == expected_validator_index as u64 && slot == current_slot && prev_root == head_root && new_root == head_root + ); + + /* + * The following test ensures that: + * + * A sync committee message from the same validator to a non-head block will + * be rejected. + */ + assert_invalid!( + "sync message to parent after message to head has already been seen", + valid_sync_committee_message_to_parent.clone(), + subnet_id, + SyncCommitteeError::PriorSyncCommitteeMessageKnown { + validator_index, + slot, + prev_root, + new_root + } + if validator_index == expected_validator_index as u64 && slot == current_slot && prev_root == head_root && new_root == parent_root + ); + + // Ensure that the sync aggregates in the op pool for both the parent block and head block are valid. + let chain = &harness.chain; + let check_sync_aggregate = |root: Hash256| async move { + // Generate an aggregate sync message from the naive aggregation pool. + let aggregate = chain + .get_aggregated_sync_committee_contribution(&SyncContributionData { + // It's a test pre-condition that both sync messages have the same slot. + slot: valid_sync_committee_message.slot, + beacon_block_root: root, + subcommittee_index: subnet_id.into(), + }) + .unwrap() + .unwrap(); + + // Insert the aggregate into the op pool. + chain.op_pool.insert_sync_contribution(aggregate).unwrap(); + + // Load the block and state for the given root. + let block = chain.get_block(&root).await.unwrap().unwrap(); + let mut state = chain.get_state(&block.state_root(), None).unwrap().unwrap(); + + // Advance the state to simulate a pre-state for block production. + let slot = valid_sync_committee_message.slot + 1; + complete_state_advance(&mut state, Some(block.state_root()), slot, &chain.spec).unwrap(); + + // Get an aggregate that would be included in a block. + let aggregate_for_inclusion = chain.op_pool.get_sync_aggregate(&state).unwrap().unwrap(); + + // Validate the retrieved aggregate against the state. + process_sync_aggregate( + &mut state, + &aggregate_for_inclusion, + 0, + VerifySignatures::True, + &chain.spec, + ) + .unwrap(); + }; + check_sync_aggregate(valid_sync_committee_message.beacon_block_root).await; + check_sync_aggregate(valid_sync_committee_message_to_parent.beacon_block_root).await; + /* * The following test ensures that: * @@ -649,7 +798,7 @@ async fn unaggregated_gossip_verification() { assert_invalid!( "sync message on incorrect subnet", - next_valid_sync_committee_message.clone(), + next_valid_sync_committee_message, next_subnet_id, SyncCommitteeError::InvalidSubnetId { received, diff --git a/beacon_node/http_api/src/sync_committees.rs b/beacon_node/http_api/src/sync_committees.rs index a6acf308fa..c728fbeb14 100644 --- a/beacon_node/http_api/src/sync_committees.rs +++ b/beacon_node/http_api/src/sync_committees.rs @@ -199,10 +199,14 @@ pub fn process_sync_committee_signatures( Err(SyncVerificationError::PriorSyncCommitteeMessageKnown { validator_index, slot, + prev_root, + new_root, }) => { debug!( log, "Ignoring already-known sync message"; + "new_root" => ?new_root, + "prev_root" => ?prev_root, "slot" => slot, "validator_index" => validator_index, ); diff --git a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs b/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs index 9d85bc545e..cb4533f5ae 100644 --- a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs +++ b/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs @@ -2343,6 +2343,25 @@ impl Worker { "peer_id" => %peer_id, "type" => ?message_type, ); + + // Do not penalize the peer. + + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); + + return; + } + SyncCommitteeError::PriorSyncContributionMessageKnown { .. } => { + /* + * We have already seen a sync contribution message from this validator for this epoch. + * + * The peer is not necessarily faulty. + */ + debug!( + self.log, + "Prior sync contribution message known"; + "peer_id" => %peer_id, + "type" => ?message_type, + ); // We still penalize the peer slightly. We don't want this to be a recurring // behaviour. self.gossip_penalize_peer( From 283c327f85c1424040b124f077e67cf8e06c13e0 Mon Sep 17 00:00:00 2001 From: int88 Date: Fri, 19 May 2023 07:40:35 +0000 Subject: [PATCH 10/63] add more sepolia bootnodes (#4297) ## Issue Addressed #4288 ## Proposed Changes add more sepolia bootnodes ## Additional Info Before add this fix: ```bash May 16 08:13:59.161 INFO ENR Initialised tcp4: None, udp4: None, ip4: None, id: 0xf5b4..a912, seq: 1, enr: enr:-K24QADMSAVTKfbfTeiHkxDNMoW8OzVTsROE_FkYbo1Bny9kIEj74Q8Rqkz5c1q_PIWG9_3GvtSMfsjPI6h5S6z_wLsBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpBH63KzkAAAcv__________gmlkgnY0iXNlY3AyNTZrMaECD_KuAkUG3mbySM7ifutb6k6iRuG_q6-aUUHevTw7mayIc3luY25ldHMAg3RjcIIjKA, service: libp2p May 16 08:13:59.162 DEBG Adding node to routing table tcp: None, udp: Some(9001), ip: Some(178.128.150.254), peer_id: 16Uiu2HAm7xtvD82P5sCTV3N8acdrNpYKFsoY5Da3PShxaJ4zkquH, node_id: 0x2335..612e, service: libp2p May 16 08:13:59.162 DEBG Discovery service started service: libp2p May 16 08:13:59.162 DEBG Starting a peer discovery request target_peers: 16, service: libp2p ``` After add this fix: ```bash May 16 08:52:04.263 INFO ENR Initialised udp6: None, tcp6: None, tcp4: Some(9000), udp4: None, ip4: None, id: 0x5065..83f0, seq: 1, enr: enr:-K24QIBmjZ2bYWd30jnYRoBGUvdGV7O0zd90FejaQWNQGIPbCZzY1VfOrdfvVv-CQdwZlCa3izxURu9BbCvZ2o_anJQBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpBH63KzkAAAcv__________gmlkgnY0iXNlY3AyNTZrMaECo-FsxPuF0qVXaGKkNvaWCMTtpscPwFzJyfSb9F7LNtiIc3luY25ldHMAg3RjcIIjKA, service: libp2p May 16 08:52:04.263 DEBG Adding node to routing table tcp: None, udp: Some(9001), ip: Some(178.128.150.254), peer_id: 16Uiu2HAm7xtvD82P5sCTV3N8acdrNpYKFsoY5Da3PShxaJ4zkquH, node_id: 0x2335..612e, service: libp2p May 16 08:52:04.264 DEBG Adding node to routing table tcp: Some(9000), udp: Some(9000), ip: Some(165.22.196.173), peer_id: 16Uiu2HAmCC5FvoVuvrTgaQyUzf8kf86D3XMe8PshNCLyiQAfzQGi, node_id: 0xfcac..7b09, service: libp2p May 16 08:52:04.264 DEBG Adding node to routing table tcp: Some(9000), udp: Some(9000), ip: Some(18.216.38.49), peer_id: 16Uiu2HAkxxexUkjjh9rzPTkoqvT2jvdegLpC8GXp1s6euFeiHpmW, node_id: 0x1977..3ea6, service: libp2p May 16 08:52:04.264 DEBG Discovery service started service: libp2p May 16 08:52:04.264 DEBG Starting a peer discovery request target_peers: 16, service: libp2p ``` And the nodes add to the route table is the same as the results decoded from `boot_enr.yaml` which means the fix is working! --- .../built_in_network_configs/sepolia/boot_enr.yaml | 4 ++++ 1 file changed, 4 insertions(+) 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/boot_enr.yaml index abb3b1250e..f88fbc765a 100644 --- a/common/eth2_network_config/built_in_network_configs/sepolia/boot_enr.yaml +++ b/common/eth2_network_config/built_in_network_configs/sepolia/boot_enr.yaml @@ -1 +1,5 @@ +# EF Team - enr:-Iq4QMCTfIMXnow27baRUb35Q8iiFHSIDBJh6hQM5Axohhf4b6Kr_cOCu0htQ5WvVqKvFgY28893DHAg8gnBAXsAVqmGAX53x8JggmlkgnY0gmlwhLKAlv6Jc2VjcDI1NmsxoQK6S-Cii_KmfFdUJL2TANL3ksaKUnNXvTCv1tLwXs0QgIN1ZHCCIyk +- enr:-KG4QE5OIg5ThTjkzrlVF32WT_-XT14WeJtIz2zoTqLLjQhYAmJlnk4ItSoH41_2x0RX0wTFIe5GgjRzU2u7Q1fN4vADhGV0aDKQqP7o7pAAAHAyAAAAAAAAAIJpZIJ2NIJpcISlFsStiXNlY3AyNTZrMaEC-Rrd_bBZwhKpXzFCrStKp1q_HmGOewxY3KwM8ofAj_ODdGNwgiMog3VkcIIjKA +# Teku team (Consensys) +- enr:-Ly4QFoZTWR8ulxGVsWydTNGdwEESueIdj-wB6UmmjUcm-AOPxnQi7wprzwcdo7-1jBW_JxELlUKJdJES8TDsbl1EdNlh2F0dG5ldHOI__78_v2bsV-EZXRoMpA2-lATkAAAcf__________gmlkgnY0gmlwhBLYJjGJc2VjcDI1NmsxoQI0gujXac9rMAb48NtMqtSTyHIeNYlpjkbYpWJw46PmYYhzeW5jbmV0cw-DdGNwgiMog3VkcIIjKA From 981c72e863f37113f45306246775dcf715705a1c Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 19 May 2023 10:07:12 +0000 Subject: [PATCH 11/63] Update gnosis capella preset (#4302) ## Issue Addressed N/A ## Proposed Changes Gnosis preset values have been updated from previous placeholder values. This changes are required for chiado since it inherits the preset from gnosis mainnet. - preset values update PR ref: https://github.com/gnosischain/configs/pull/11 ## Additional Info N/A --- consensus/types/presets/gnosis/capella.yaml | 4 ++-- consensus/types/src/chain_spec.rs | 2 +- consensus/types/src/eth_spec.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/consensus/types/presets/gnosis/capella.yaml b/consensus/types/presets/gnosis/capella.yaml index 913c2956ba..fb36f94634 100644 --- a/consensus/types/presets/gnosis/capella.yaml +++ b/consensus/types/presets/gnosis/capella.yaml @@ -9,9 +9,9 @@ MAX_BLS_TO_EXECUTION_CHANGES: 16 # Execution # --------------------------------------------------------------- # 2**4 (= 16) withdrawals -MAX_WITHDRAWALS_PER_PAYLOAD: 16 +MAX_WITHDRAWALS_PER_PAYLOAD: 8 # Withdrawals processing # --------------------------------------------------------------- # 2**14 (= 16384) validators -MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP: 16384 +MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP: 8192 \ No newline at end of file diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index 163b07dcd1..2b25cc1d59 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -843,7 +843,7 @@ impl ChainSpec { */ capella_fork_version: [0x03, 0x00, 0x00, 0x64], capella_fork_epoch: None, - max_validators_per_withdrawals_sweep: 16384, + max_validators_per_withdrawals_sweep: 8192, /* * Network specific diff --git a/consensus/types/src/eth_spec.rs b/consensus/types/src/eth_spec.rs index 378e8d34b7..64bfb8da0b 100644 --- a/consensus/types/src/eth_spec.rs +++ b/consensus/types/src/eth_spec.rs @@ -373,7 +373,7 @@ impl EthSpec for GnosisEthSpec { type MaxPendingAttestations = U2048; // 128 max attestations * 16 slots per epoch type SlotsPerEth1VotingPeriod = U1024; // 64 epochs * 16 slots per epoch type MaxBlsToExecutionChanges = U16; - type MaxWithdrawalsPerPayload = U16; + type MaxWithdrawalsPerPayload = U8; fn default_spec() -> ChainSpec { ChainSpec::gnosis() From c27f2bf9c6a33d3b7ac630ca52e884c0e1737e99 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 22 May 2023 02:36:43 +0000 Subject: [PATCH 12/63] Avoid excessive logging of BN online status (#4315) ## Issue Addressed https://github.com/sigp/lighthouse/pull/4309#issuecomment-1556052261 ## Proposed Changes Log the `Connected to beacon node` message only if the node was previously offline. This avoids a regression in logging after #4295, whereby the `Connected to beacon node` message would be logged every slot. The new reduced logging is _slightly different_ from what we had prior to my changes in #4295. The main difference is that we used to log the `Connected` message whenever a node was online and subject to a health check (for being unhealthy in some other way). I think the new behaviour is reasonable, as the `Connected` message isn't particularly helpful if the BN is unhealthy, and the specific reason for unhealthiness will be logged by the warnings for `is_compatible`/`is_synced`. --- validator_client/src/beacon_node_fallback.rs | 21 ++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/validator_client/src/beacon_node_fallback.rs b/validator_client/src/beacon_node_fallback.rs index 2cbab3b218..531cec08ac 100644 --- a/validator_client/src/beacon_node_fallback.rs +++ b/validator_client/src/beacon_node_fallback.rs @@ -182,7 +182,10 @@ impl CandidateBeaconNode { spec: &ChainSpec, log: &Logger, ) -> Result<(), CandidateError> { - let new_status = if let Err(e) = self.is_online(log).await { + let previous_status = self.status(RequireSynced::Yes).await; + let was_offline = matches!(previous_status, Err(CandidateError::Offline)); + + let new_status = if let Err(e) = self.is_online(was_offline, log).await { Err(e) } else if let Err(e) = self.is_compatible(spec, log).await { Err(e) @@ -202,7 +205,7 @@ impl CandidateBeaconNode { } /// Checks if the node is reachable. - async fn is_online(&self, log: &Logger) -> Result<(), CandidateError> { + async fn is_online(&self, was_offline: bool, log: &Logger) -> Result<(), CandidateError> { let result = self .beacon_node .get_node_version() @@ -211,12 +214,14 @@ impl CandidateBeaconNode { match result { Ok(version) => { - info!( - log, - "Connected to beacon node"; - "version" => version, - "endpoint" => %self.beacon_node, - ); + if was_offline { + info!( + log, + "Connected to beacon node"; + "version" => version, + "endpoint" => %self.beacon_node, + ); + } Ok(()) } Err(e) => { From aa1ed787e9bb65659e043cc9ca9d26cacbaf0bc9 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Mon, 22 May 2023 05:57:08 +0000 Subject: [PATCH 13/63] Logging via the HTTP API (#4074) This PR adds the ability to read the Lighthouse logs from the HTTP API for both the BN and the VC. This is done in such a way to as minimize any kind of performance hit by adding this feature. The current design creates a tokio broadcast channel and mixes is into a form of slog drain that combines with our main global logger drain, only if the http api is enabled. The drain gets the logs, checks the log level and drops them if they are below INFO. If they are INFO or higher, it sends them via a broadcast channel only if there are users subscribed to the HTTP API channel. If not, it drops the logs. If there are more than one subscriber, the channel clones the log records and converts them to json in their independent HTTP API tasks. Co-authored-by: Michael Sproul --- Cargo.lock | 800 ++++++++++--------- beacon_node/client/Cargo.toml | 2 +- beacon_node/client/src/builder.rs | 2 + beacon_node/http_api/Cargo.toml | 4 +- beacon_node/http_api/src/lib.rs | 44 + beacon_node/http_api/src/test_utils.rs | 1 + beacon_node/src/cli.rs | 2 +- book/src/api-lighthouse.md | 28 + book/src/api-vc-endpoints.md | 30 + common/logging/Cargo.toml | 7 + common/logging/src/async_record.rs | 309 +++++++ common/logging/src/lib.rs | 4 + common/logging/src/sse_logging_components.rs | 46 ++ lcli/src/main.rs | 1 + lighthouse/environment/src/lib.rs | 96 ++- lighthouse/src/main.rs | 11 + testing/simulator/src/eth1_sim.rs | 1 + testing/simulator/src/no_eth1_sim.rs | 1 + testing/simulator/src/sync_sim.rs | 1 + validator_client/Cargo.toml | 2 + validator_client/src/http_api/mod.rs | 55 +- validator_client/src/http_api/tests.rs | 3 +- validator_client/src/lib.rs | 1 + 23 files changed, 1040 insertions(+), 411 deletions(-) create mode 100644 common/logging/src/async_record.rs create mode 100644 common/logging/src/sse_logging_components.rs diff --git a/Cargo.lock b/Cargo.lock index d6e21d4d22..3114723a73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,9 +159,9 @@ dependencies = [ [[package]] name = "aes-gcm" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e1366e0c69c9f927b1fa5ce2c7bf9eafc8f9268c0b9800729e8b267612447c" +checksum = "209b47e8954a928e1d72e86eca7000ebb6655fe1436d33eefc2201cad027e237" dependencies = [ "aead 0.5.2", "aes 0.8.2", @@ -197,16 +197,27 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom 0.2.8", + "getrandom 0.2.9", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", "once_cell", "version_check", ] [[package]] name = "aho-corasick" -version = "0.7.20" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" dependencies = [ "memchr", ] @@ -236,9 +247,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.70" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" [[package]] name = "arbitrary" @@ -279,7 +290,7 @@ dependencies = [ "num-traits", "rusticata-macros", "thiserror", - "time 0.3.20", + "time 0.3.21", ] [[package]] @@ -295,7 +306,7 @@ dependencies = [ "num-traits", "rusticata-macros", "thiserror", - "time 0.3.20", + "time 0.3.21", ] [[package]] @@ -335,9 +346,9 @@ dependencies = [ [[package]] name = "asn1_der" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22d1f4b888c298a027c99dc9048015fac177587de20fc30232a057dfbe24a21" +checksum = "155a5a185e42c6b77ac7b88a15143d930a9e9727a5b7b77eed417404ab15c247" [[package]] name = "async-io" @@ -370,9 +381,9 @@ dependencies = [ [[package]] name = "async-stream" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad445822218ce64be7a341abfb0b1ea43b5c23aa83902542a4542e78309d8e5e" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ "async-stream-impl", "futures-core", @@ -381,13 +392,13 @@ dependencies = [ [[package]] name = "async-stream-impl" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4655ae1a7b0cdf149156f780c5bf3f1352bc53cbd9e0a361a7ef7b22947e965" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.16", ] [[package]] @@ -398,7 +409,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.16", ] [[package]] @@ -427,9 +438,9 @@ dependencies = [ [[package]] name = "atomic-waker" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "debc29dde2e69f9e47506b525f639ed42300fc014a3e007832592448fa8e4599" +checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" [[package]] name = "attohttpc" @@ -456,9 +467,9 @@ dependencies = [ [[package]] name = "auto_impl" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a8c1df849285fbacd587de7818cc7d13be6cd2cbcd47a04fb1801b0e2706e33" +checksum = "fee3da8ef1276b0bee5dd1c7258010d8fffd31801447323115a25560e1327b89" dependencies = [ "proc-macro-error", "proc-macro2", @@ -538,7 +549,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.6.2", "object", "rustc-demangle", ] @@ -563,9 +574,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +checksum = "3f1e31e207a6b8fb791a38ea3105e6cb541f55e4d029902d3039a4ad07cc4105" [[package]] name = "base64ct" @@ -746,7 +757,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -883,9 +894,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.12.0" +version = "3.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b" [[package]] name = "byte-slice-cast" @@ -964,9 +975,9 @@ dependencies = [ [[package]] name = "cargo_metadata" -version = "0.15.3" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a1ec454bc3eead8719cb56e15dbbfecdbc14e4b3a3ae4936cc6e31f5fc0d07" +checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" dependencies = [ "camino", "cargo-platform", @@ -1160,7 +1171,7 @@ dependencies = [ "state_processing", "store", "task_executor", - "time 0.3.20", + "time 0.3.21", "timer", "tokio", "types", @@ -1175,16 +1186,6 @@ dependencies = [ "cc", ] -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] - [[package]] name = "compare_fields" version = "0.2.0" @@ -1202,9 +1203,9 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e" +checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" dependencies = [ "crossbeam-utils", ] @@ -1248,9 +1249,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181" +checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" dependencies = [ "libc", ] @@ -1317,9 +1318,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf2b3e8478797446514c91ef04bafcb59faba183e621ad488df88983cc14128c" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ "cfg-if", "crossbeam-utils", @@ -1448,12 +1449,12 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.2.5" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbcf33c2a618cbe41ee43ae6e9f2e48368cd9f9db2896f10167d8d762679f639" +checksum = "04d778600249295e82b6ab12e291ed9029407efee0cfb7baf67157edc65964df" dependencies = [ "nix 0.26.2", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -1483,50 +1484,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "cxx" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" -dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn 2.0.13", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.13", -] - [[package]] name = "darling" version = "0.13.4" @@ -1619,15 +1576,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "data-encoding-macro" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86927b7cd2fe88fa698b87404b287ab98d1a0063a34071d92e575b72d3029aca" +checksum = "c904b33cc60130e1aeea4956ab803d08a3f4a0ca82d64ed757afac3891f2bb99" dependencies = [ "data-encoding", "data-encoding-macro-internal", @@ -1635,9 +1592,9 @@ dependencies = [ [[package]] name = "data-encoding-macro-internal" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5bbed42daaa95e780b60a50546aa345b8413a1e46f9a40a12907d3598f038db" +checksum = "8fdf3fce3ce863539ec1d7fd1b6dcc3c645663376b43ed376bbf887733e4f772" dependencies = [ "data-encoding", "syn 1.0.109", @@ -1674,7 +1631,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4355c25cbf99edcb6b4a0e906f6bdc6956eda149e84455bea49696429b2f8e8" dependencies = [ "futures", - "tokio-util 0.7.7", + "tokio-util 0.7.8", ] [[package]] @@ -1797,9 +1754,9 @@ dependencies = [ [[package]] name = "diesel" -version = "2.0.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4391a22b19c916e50bec4d6140f29bdda3e3bb187223fe6e3ea0b6e4d1021c04" +checksum = "72eb77396836a4505da85bae0712fa324b74acfe1876d7c2f7e694ef3d0ee373" dependencies = [ "bitflags", "byteorder", @@ -1843,9 +1800,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "crypto-common", @@ -1938,13 +1895,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.16", ] [[package]] @@ -1955,9 +1912,9 @@ checksum = "65d09067bfacaa79114679b279d7f5885b53295b1e2cfb4e79c8e4bd3d633169" [[package]] name = "dunce" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bd4b30a6560bbd9b4620f4de34c3f14f60848e58a9b7216801afcb4c7b31c3c" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" [[package]] name = "ecdsa" @@ -2040,7 +1997,7 @@ dependencies = [ "base16ct", "crypto-bigint", "der", - "digest 0.10.6", + "digest 0.10.7", "ff", "generic-array", "group", @@ -2077,7 +2034,7 @@ dependencies = [ "rand 0.8.5", "rlp", "serde", - "sha3 0.10.6", + "sha3 0.10.8", "zeroize", ] @@ -2097,7 +2054,7 @@ dependencies = [ "rand 0.8.5", "rlp", "serde", - "sha3 0.10.6", + "sha3 0.10.8", "zeroize", ] @@ -2160,13 +2117,13 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ "errno-dragonfly", "libc", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -2385,7 +2342,7 @@ dependencies = [ "regex", "serde", "serde_json", - "sha3 0.10.6", + "sha3 0.10.8", "thiserror", "uint", ] @@ -2549,7 +2506,7 @@ dependencies = [ "dunce", "ethers-core", "eyre", - "getrandom 0.2.8", + "getrandom 0.2.9", "hex", "proc-macro2", "quote", @@ -2621,7 +2578,7 @@ dependencies = [ "futures-core", "futures-timer", "futures-util", - "getrandom 0.2.8", + "getrandom 0.2.9", "hashers", "hex", "http", @@ -2837,13 +2794,13 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" dependencies = [ "crc32fast", "libz-sys", - "miniz_oxide", + "miniz_oxide 0.7.1", ] [[package]] @@ -2964,9 +2921,9 @@ checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" [[package]] name = "futures-lite" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ "fastrand", "futures-core", @@ -2985,7 +2942,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.16", ] [[package]] @@ -3090,9 +3047,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" dependencies = [ "cfg-if", "js-sys", @@ -3168,9 +3125,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f8a914c2987b688368b5138aa05321db91f4090cf26118185672ad588bce21" +checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" dependencies = [ "bytes", "fnv", @@ -3181,7 +3138,7 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util 0.7.7", + "tokio-util 0.7.8", "tracing", ] @@ -3212,7 +3169,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" dependencies = [ - "ahash", + "ahash 0.7.6", ] [[package]] @@ -3221,7 +3178,16 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash", + "ahash 0.7.6", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.3", ] [[package]] @@ -3244,11 +3210,11 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" +checksum = "0761a1b9491c4f2e3d66aa0f62d0fba0af9a0e2852e4d48ea506632a4b56e6aa" dependencies = [ - "hashbrown 0.12.3", + "hashbrown 0.13.2", ] [[package]] @@ -3353,7 +3319,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -3493,9 +3459,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.25" +version = "0.14.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899" +checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" dependencies = [ "bytes", "futures-channel", @@ -3517,15 +3483,15 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.23.2" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +checksum = "0646026eb1b3eea4cd9ba47912ea5ce9cc07713d105b1a14698f4e6433d348b7" dependencies = [ "http", "hyper", - "rustls 0.20.8", + "rustls 0.21.1", "tokio", - "tokio-rustls 0.23.4", + "tokio-rustls 0.24.0", ] [[package]] @@ -3543,26 +3509,25 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.54" +version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c17cc76786e99f8d2f055c11159e7f0091c42474dcc3189fbab96072e873e6d" +checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows 0.46.0", + "windows 0.48.0", ] [[package]] name = "iana-time-zone-haiku" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "cxx", - "cxx-build", + "cc", ] [[package]] @@ -3670,7 +3635,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" dependencies = [ - "parity-scale-codec 3.4.0", + "parity-scale-codec 3.5.0", ] [[package]] @@ -3787,13 +3752,13 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" +checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" dependencies = [ "hermit-abi 0.3.1", "libc", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -3862,9 +3827,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" dependencies = [ "wasm-bindgen", ] @@ -3875,7 +3840,7 @@ version = "8.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" dependencies = [ - "base64 0.21.0", + "base64 0.21.1", "pem", "ring", "serde", @@ -3893,14 +3858,14 @@ dependencies = [ "ecdsa", "elliptic-curve", "sha2 0.10.6", - "sha3 0.10.6", + "sha3 0.10.8", ] [[package]] name = "keccak" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3afef3b6eff9ce9d8ff9b3601125eec7f0c8cbac7abd14f355d053fa56c98768" +checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" dependencies = [ "cpufeatures", ] @@ -3993,15 +3958,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.140" +version = "0.2.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" +checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" [[package]] name = "libflate" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97822bf791bd4d5b403713886a5fbe8bf49520fe78e323b0dc480ca1a03e50b0" +checksum = "5ff4ae71b685bbad2f2f391fe74f6b7659a34871c08b210fdc039e43bee07d18" dependencies = [ "adler32", "crc32fast", @@ -4035,9 +4000,9 @@ checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" [[package]] name = "libm" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" [[package]] name = "libmdbx" @@ -4063,7 +4028,7 @@ dependencies = [ "bytes", "futures", "futures-timer", - "getrandom 0.2.8", + "getrandom 0.2.9", "instant", "libp2p-core 0.38.0", "libp2p-dns", @@ -4159,9 +4124,9 @@ dependencies = [ [[package]] name = "libp2p-core" -version = "0.39.1" +version = "0.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7f8b7d65c070a5a1b5f8f0510648189da08f787b8963f8e21219e0710733af" +checksum = "3c1df63c0b582aa434fb09b2d86897fa2b419ffeccf934b36f87fcedc8e835c2" dependencies = [ "either", "fnv", @@ -4252,18 +4217,18 @@ dependencies = [ [[package]] name = "libp2p-identity" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a8ea433ae0cea7e3315354305237b9897afe45278b2118a7a57ca744e70fd27" +checksum = "9e2d584751cecb2aabaa56106be6be91338a60a0f4e420cf2af639204f596fc1" dependencies = [ "bs58", "ed25519-dalek", "log", "multiaddr 0.17.1", "multihash 0.17.0", - "prost", "quick-protobuf", "rand 0.8.5", + "sha2 0.10.6", "thiserror", "zeroize", ] @@ -4437,7 +4402,7 @@ checksum = "ff08d13d0dc66e5e9ba6279c1de417b84fa0d0adc3b03e5732928c180ec02781" dependencies = [ "futures", "futures-rustls", - "libp2p-core 0.39.1", + "libp2p-core 0.39.2", "libp2p-identity", "rcgen 0.10.0", "ring", @@ -4475,7 +4440,7 @@ dependencies = [ "thiserror", "tinytemplate", "tokio", - "tokio-util 0.7.7", + "tokio-util 0.7.8", "webrtc", ] @@ -4573,9 +4538,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" +checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db" dependencies = [ "cc", "pkg-config", @@ -4691,15 +4656,6 @@ dependencies = [ "target_info", ] -[[package]] -name = "link-cplusplus" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" -dependencies = [ - "cc", -] - [[package]] name = "linked-hash-map" version = "0.5.6" @@ -4708,9 +4664,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.3.1" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "lmdb-rkv" @@ -4764,11 +4720,18 @@ dependencies = [ name = "logging" version = "0.2.0" dependencies = [ + "chrono", "lazy_static", "lighthouse_metrics", + "parking_lot 0.12.1", + "serde", + "serde_json", "slog", + "slog-async", "slog-term", "sloggers", + "take_mut", + "tokio", ] [[package]] @@ -4865,7 +4828,7 @@ version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -5019,6 +4982,15 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + [[package]] name = "mio" version = "0.8.6" @@ -5130,7 +5102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c346cf9999c631f002d8f977c4eaeaa0e6386f16007202308d0b3757522c2cc" dependencies = [ "core2", - "digest 0.10.6", + "digest 0.10.7", "multihash-derive", "sha2 0.10.6", "unsigned-varint 0.7.1", @@ -5143,9 +5115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835d6ff01d610179fbce3de1694d007e500bf33a7f29689838941d6bf783ae40" dependencies = [ "core2", - "digest 0.10.6", "multihash-derive", - "sha2 0.10.6", "unsigned-varint 0.7.1", ] @@ -5421,9 +5391,9 @@ dependencies = [ [[package]] name = "ntapi" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" dependencies = [ "winapi", ] @@ -5458,7 +5428,7 @@ dependencies = [ "autocfg 0.1.8", "byteorder", "lazy_static", - "libm 0.2.6", + "libm 0.2.7", "num-integer", "num-iter", "num-traits", @@ -5596,9 +5566,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.49" +version = "0.10.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d2f106ab837a24e03672c59b1239669a0596406ff657c3c0835b6b7f0f35a33" +checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56" dependencies = [ "bitflags", "cfg-if", @@ -5617,7 +5587,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.16", ] [[package]] @@ -5628,18 +5598,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "111.25.2+1.1.1t" +version = "111.25.3+1.1.1t" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320708a054ad9b3bf314688b5db87cf4d6683d64cfc835e2337924ae62bf4431" +checksum = "924757a6a226bf60da5f7dd0311a34d2b52283dd82ddeb103208ddc66362f80c" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.84" +version = "0.9.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a20eace9dc2d82904039cb76dcf50fb1a0bba071cfd1629720b5d6f1ddba0fa" +checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e" dependencies = [ "cc", "libc", @@ -5726,9 +5696,9 @@ dependencies = [ [[package]] name = "parity-scale-codec" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "637935964ff85a605d114591d4d2c13c5d1ba2806dae97cea6bf180238a749ac" +checksum = "5ddb756ca205bd108aee3c62c6d3c994e1df84a59b9d6d4a5ea42ee1fd5a9a28" dependencies = [ "arrayvec", "bitvec 1.0.1", @@ -5764,9 +5734,9 @@ dependencies = [ [[package]] name = "parking" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" +checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" [[package]] name = "parking_lot" @@ -5910,22 +5880,22 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.0.12" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.12" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.16", ] [[package]] @@ -5958,9 +5928,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "platforms" @@ -6004,9 +5974,9 @@ dependencies = [ [[package]] name = "polling" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e1f879b2998099c2d69ab9605d145d5b661195627eccc680002c4918a7fb6fa" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ "autocfg 1.1.0", "bitflags", @@ -6015,7 +5985,7 @@ dependencies = [ "libc", "log", "pin-project-lite 0.2.9", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -6050,7 +6020,7 @@ dependencies = [ "cfg-if", "cpufeatures", "opaque-debug", - "universal-hash 0.5.0", + "universal-hash 0.5.1", ] [[package]] @@ -6059,7 +6029,7 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b7fa9f396f51dffd61546fd8573ee20592287996568e6175ceb0f8699ad75d" dependencies = [ - "base64 0.21.0", + "base64 0.21.1", "byteorder", "bytes", "fallible-iterator", @@ -6090,9 +6060,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "pq-sys" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b845d6d8ec554f972a2c5298aad68953fd64e7441e846075450b44656a016d1" +checksum = "31c0052426df997c0cbd30789eb44ca097e3541717a7b8fa36b1c464ee7edebd" dependencies = [ "vcpkg", ] @@ -6176,9 +6146,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.55" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d0dd4be24fcdcfeaa12a432d588dc59bbad6cad3510c67e74a2b6b2fc950564" +checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" dependencies = [ "unicode-ident", ] @@ -6235,9 +6205,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48e50df39172a3e7eb17e14642445da64996989bc212b583015435d39a58537" +checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" dependencies = [ "bytes", "prost-derive", @@ -6245,9 +6215,9 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c828f93f5ca4826f97fedcbd3f9a536c16b12cff3dbbb4a007f932bbad95b12" +checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270" dependencies = [ "bytes", "heck", @@ -6280,9 +6250,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea9b0f8cbe5e15a8a042d030bd96668db28ecb567ec37d691971ff5731d2b1b" +checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" dependencies = [ "anyhow", "itertools", @@ -6293,9 +6263,9 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "379119666929a1afd7a043aa6cf96fa67a6dce9af60c88095a4686dbce4c9c88" +checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" dependencies = [ "prost", ] @@ -6408,9 +6378,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" dependencies = [ "proc-macro2", ] @@ -6507,7 +6477,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.8", + "getrandom 0.2.9", ] [[package]] @@ -6558,7 +6528,7 @@ checksum = "6413f3de1edee53342e6138e75b56d32e7bc6e332b3bd62d497b1929d4cfbcdd" dependencies = [ "pem", "ring", - "time 0.3.20", + "time 0.3.21", "x509-parser 0.13.2", "yasna", ] @@ -6571,7 +6541,7 @@ checksum = "ffbe84efe2f38dea12e9bfc1f65377fdf03e53a18cb3b995faedf7934c7e785b" dependencies = [ "pem", "ring", - "time 0.3.20", + "time 0.3.21", "yasna", ] @@ -6599,20 +6569,20 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom 0.2.8", + "getrandom 0.2.9", "redox_syscall 0.2.16", "thiserror", ] [[package]] name = "regex" -version = "1.7.3" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" +checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.7.1", ] [[package]] @@ -6621,7 +6591,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" dependencies = [ - "regex-syntax", + "regex-syntax 0.6.29", ] [[package]] @@ -6631,12 +6601,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] -name = "reqwest" -version = "0.11.16" +name = "regex-syntax" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254" +checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" + +[[package]] +name = "reqwest" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" dependencies = [ - "base64 0.21.0", + "base64 0.21.1", "bytes", "encoding_rs", "futures-core", @@ -6655,15 +6631,15 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite 0.2.9", - "rustls 0.20.8", + "rustls 0.21.1", "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "tokio", "tokio-native-tls", - "tokio-rustls 0.23.4", - "tokio-util 0.7.7", + "tokio-rustls 0.24.0", + "tokio-util 0.7.8", "tower-service", "url", "wasm-bindgen", @@ -6796,16 +6772,16 @@ dependencies = [ "bitflags", "fallible-iterator", "fallible-streaming-iterator", - "hashlink 0.8.1", + "hashlink 0.8.2", "libsqlite3-sys", "smallvec", ] [[package]] name = "rustc-demangle" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a36c42d1873f9a77c53bde094f9664d9891bc604a45b4798fd2c389ed12e5b" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustc-hash" @@ -6848,16 +6824,16 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.6" +version = "0.37.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d097081ed288dfe45699b72f5b5d648e5f15d64d900c7080273baa20c16a6849" +checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" dependencies = [ "bitflags", "errno", "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -6885,13 +6861,35 @@ dependencies = [ "webpki 0.22.0", ] +[[package]] +name = "rustls" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c911ba11bc8433e811ce56fde130ccf32f5127cab0e0194e9c68c5a5b671791e" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct 0.7.0", +] + [[package]] name = "rustls-pemfile" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" dependencies = [ - "base64 0.21.0", + "base64 0.21.1", +] + +[[package]] +name = "rustls-webpki" +version = "0.100.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +dependencies = [ + "ring", + "untrusted", ] [[package]] @@ -6947,21 +6945,21 @@ dependencies = [ [[package]] name = "scale-info" -version = "2.5.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cfdffd972d76b22f3d7f81c8be34b2296afd3a25e0a547bd9abe340a4dbbe97" +checksum = "b569c32c806ec3abdf3b5869fb8bf1e0d275a7c1c9b0b05603d9464632649edf" dependencies = [ "cfg-if", "derive_more", - "parity-scale-codec 3.4.0", + "parity-scale-codec 3.5.0", "scale-info-derive", ] [[package]] name = "scale-info-derive" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61fa974aea2d63dd18a4ec3a49d59af9f34178c73a4f56d2f18205628d00681e" +checksum = "53012eae69e5aa5c14671942a5dd47de59d4cdcff8532a6dd0e081faf1119482" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -6999,12 +6997,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" -[[package]] -name = "scratch" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" - [[package]] name = "scrypt" version = "0.7.0" @@ -7065,9 +7057,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.8.2" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" dependencies = [ "bitflags", "core-foundation", @@ -7078,9 +7070,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" dependencies = [ "core-foundation-sys", "libc", @@ -7126,9 +7118,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.159" +version = "1.0.163" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" +checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" dependencies = [ "serde_derive", ] @@ -7155,20 +7147,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.159" +version = "1.0.163" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" +checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.16", ] [[package]] name = "serde_json" -version = "1.0.95" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" dependencies = [ "itoa", "ryu", @@ -7183,7 +7175,7 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.16", ] [[package]] @@ -7253,7 +7245,7 @@ checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -7264,7 +7256,7 @@ checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -7288,7 +7280,7 @@ checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -7305,11 +7297,11 @@ dependencies = [ [[package]] name = "sha3" -version = "0.10.6" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdf0c33fae925bdc080598b84bc15c55e7b9a4a43b3c704da051f977469691c9" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", "keccak", ] @@ -7343,7 +7335,7 @@ version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", "rand_core 0.6.4", ] @@ -7356,7 +7348,7 @@ dependencies = [ "num-bigint", "num-traits", "thiserror", - "time 0.3.20", + "time 0.3.21", ] [[package]] @@ -7488,7 +7480,7 @@ dependencies = [ "serde", "serde_json", "slog", - "time 0.3.20", + "time 0.3.21", ] [[package]] @@ -7533,7 +7525,7 @@ dependencies = [ "slog", "term", "thread_local", - "time 0.3.20", + "time 0.3.21", ] [[package]] @@ -7611,12 +7603,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc8d618c6641ae355025c449427f9e96b98abf99a772be3cef6708d15c77147a" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" dependencies = [ "libc", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -7891,9 +7883,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.13" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c9da457c5285ac1f936ebd076af6dac17a61cfe7826f2076b4d015cf47bc8ec" +checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" dependencies = [ "proc-macro2", "quote", @@ -7935,9 +7927,9 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75182f12f490e953596550b65ee31bda7c8e043d9386174b353bda50838c3fd" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags", "core-foundation", @@ -8097,7 +8089,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.16", ] [[package]] @@ -8132,9 +8124,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" dependencies = [ "itoa", "libc", @@ -8146,15 +8138,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" dependencies = [ "time-core", ] @@ -8225,9 +8217,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.27.0" +version = "1.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001" +checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105" dependencies = [ "autocfg 1.1.0", "bytes", @@ -8239,7 +8231,7 @@ dependencies = [ "signal-hook-registry", "socket2 0.4.9", "tokio-macros", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -8254,13 +8246,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.16", ] [[package]] @@ -8292,9 +8284,9 @@ dependencies = [ "pin-project-lite 0.2.9", "postgres-protocol", "postgres-types", - "socket2 0.5.1", + "socket2 0.5.3", "tokio", - "tokio-util 0.7.7", + "tokio-util 0.7.8", ] [[package]] @@ -8320,15 +8312,25 @@ dependencies = [ ] [[package]] -name = "tokio-stream" -version = "0.1.12" +name = "tokio-rustls" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313" +checksum = "e0d409377ff5b1e3ca6437aa86c1eb7d40c134bfec254e44c830defa92669db5" +dependencies = [ + "rustls 0.21.1", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", "pin-project-lite 0.2.9", "tokio", - "tokio-util 0.7.7", + "tokio-util 0.7.8", ] [[package]] @@ -8378,9 +8380,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ "bytes", "futures-core", @@ -8463,20 +8465,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.16", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", "valuable", @@ -8505,9 +8507,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" dependencies = [ "matchers", "nu-ansi-term", @@ -8523,9 +8525,9 @@ dependencies = [ [[package]] name = "trackable" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "017e2a1a93718e4e8386d037cfb8add78f1d690467f4350fb582f55af1203167" +checksum = "b15bd114abb99ef8cee977e517c8f37aee63f184f2d08e3e6ceca092373369ae" dependencies = [ "trackable_derive", ] @@ -8822,9 +8824,9 @@ dependencies = [ [[package]] name = "universal-hash" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d3160b73c9a19f7e2939a2fdad446c57c1bbbbf4d919d3213ff1267a580d8b5" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ "crypto-common", "subtle", @@ -8888,17 +8890,17 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom 0.2.8", + "getrandom 0.2.9", "serde", ] [[package]] name = "uuid" -version = "1.3.0" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" +checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" dependencies = [ - "getrandom 0.2.8", + "getrandom 0.2.9", ] [[package]] @@ -8948,6 +8950,7 @@ dependencies = [ "task_executor", "tempfile", "tokio", + "tokio-stream", "tree_hash", "types", "url", @@ -9106,9 +9109,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -9116,24 +9119,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.16", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.34" +version = "0.4.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" +checksum = "2d1985d03709c53167ce907ff394f5316aa22cb4e12761295c5dc57dacb6297e" dependencies = [ "cfg-if", "js-sys", @@ -9143,9 +9146,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -9153,22 +9156,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.16", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" [[package]] name = "wasm-streams" @@ -9233,9 +9236,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.61" +version = "0.3.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" dependencies = [ "js-sys", "wasm-bindgen", @@ -9323,7 +9326,7 @@ dependencies = [ "sha2 0.10.6", "stun", "thiserror", - "time 0.3.20", + "time 0.3.21", "tokio", "turn", "url", @@ -9360,7 +9363,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "942be5bd85f072c3128396f6e5a9bfb93ca8c1939ded735d177b7bcba9a13d05" dependencies = [ "aes 0.6.0", - "aes-gcm 0.10.1", + "aes-gcm 0.10.2", "async-trait", "bincode", "block-modes", @@ -9412,7 +9415,7 @@ dependencies = [ "tokio", "turn", "url", - "uuid 1.3.0", + "uuid 1.3.3", "waitgroup", "webrtc-mdns", "webrtc-util", @@ -9433,18 +9436,15 @@ dependencies = [ [[package]] name = "webrtc-media" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2a3c157a040324e5049bcbd644ffc9079e6738fa2cfab2bcff64e5cc4c00d7" +checksum = "f72e1650a8ae006017d1a5280efb49e2610c19ccc3c0905b03b648aee9554991" dependencies = [ "byteorder", "bytes", - "derive_builder", - "displaydoc", "rand 0.8.5", "rtp", "thiserror", - "webrtc-util", ] [[package]] @@ -9584,11 +9584,11 @@ dependencies = [ [[package]] name = "windows" -version = "0.46.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdacb41e6a96a052c6cb63a144f24900236121c6f63f4f8219fef5977ecb0c25" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets", + "windows-targets 0.48.0", ] [[package]] @@ -9609,12 +9609,12 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm", + "windows_aarch64_gnullvm 0.42.2", "windows_aarch64_msvc 0.42.2", "windows_i686_gnu 0.42.2", "windows_i686_msvc 0.42.2", "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm", + "windows_x86_64_gnullvm 0.42.2", "windows_x86_64_msvc 0.42.2", ] @@ -9624,7 +9624,16 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets", + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", ] [[package]] @@ -9633,21 +9642,42 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ - "windows_aarch64_gnullvm", + "windows_aarch64_gnullvm 0.42.2", "windows_aarch64_msvc 0.42.2", "windows_i686_gnu 0.42.2", "windows_i686_msvc 0.42.2", "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm", + "windows_x86_64_gnullvm 0.42.2", "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + [[package]] name = "windows_aarch64_msvc" version = "0.34.0" @@ -9660,6 +9690,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + [[package]] name = "windows_i686_gnu" version = "0.34.0" @@ -9672,6 +9708,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + [[package]] name = "windows_i686_msvc" version = "0.34.0" @@ -9684,6 +9726,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + [[package]] name = "windows_x86_64_gnu" version = "0.34.0" @@ -9696,12 +9744,24 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + [[package]] name = "windows_x86_64_msvc" version = "0.34.0" @@ -9714,6 +9774,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + [[package]] name = "winreg" version = "0.10.1" @@ -9795,7 +9861,7 @@ dependencies = [ "ring", "rusticata-macros", "thiserror", - "time 0.3.20", + "time 0.3.21", ] [[package]] @@ -9813,14 +9879,14 @@ dependencies = [ "oid-registry 0.6.1", "rusticata-macros", "thiserror", - "time 0.3.20", + "time 0.3.21", ] [[package]] name = "xml-rs" -version = "0.8.4" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" +checksum = "1690519550bfa95525229b9ca2350c63043a4857b3b0013811b2ccf4a2420b01" [[package]] name = "xmltree" @@ -9856,11 +9922,11 @@ dependencies = [ [[package]] name = "yasna" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aed2e7a52e3744ab4d0c05c20aa065258e84c49fd4226f5191b2ed29712710b4" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" dependencies = [ - "time 0.3.20", + "time 0.3.21", ] [[package]] @@ -9880,7 +9946,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.13", + "syn 2.0.16", ] [[package]] diff --git a/beacon_node/client/Cargo.toml b/beacon_node/client/Cargo.toml index 876458eea5..d39bb2e3e2 100644 --- a/beacon_node/client/Cargo.toml +++ b/beacon_node/client/Cargo.toml @@ -6,7 +6,6 @@ edition = "2021" [dev-dependencies] serde_yaml = "0.8.13" -logging = { path = "../../common/logging" } state_processing = { path = "../../consensus/state_processing" } operation_pool = { path = "../operation_pool" } tokio = "1.14.0" @@ -17,6 +16,7 @@ store = { path = "../store" } network = { path = "../network" } timer = { path = "../timer" } lighthouse_network = { path = "../lighthouse_network" } +logging = { path = "../../common/logging" } parking_lot = "0.12.0" types = { path = "../../consensus/types" } eth2_config = { path = "../../common/eth2_config" } diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index 5ef1f28fb4..215244b9ba 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -478,6 +478,7 @@ where network_globals: None, eth1_service: Some(genesis_service.eth1_service.clone()), log: context.log().clone(), + sse_logging_components: runtime_context.sse_logging_components.clone(), }); // Discard the error from the oneshot. @@ -698,6 +699,7 @@ where network_senders: self.network_senders.clone(), network_globals: self.network_globals.clone(), eth1_service: self.eth1_service.clone(), + sse_logging_components: runtime_context.sse_logging_components.clone(), log: log.clone(), }); diff --git a/beacon_node/http_api/Cargo.toml b/beacon_node/http_api/Cargo.toml index 8f253e2f24..2b117b26ce 100644 --- a/beacon_node/http_api/Cargo.toml +++ b/beacon_node/http_api/Cargo.toml @@ -36,11 +36,11 @@ tree_hash = "0.5.0" sysinfo = "0.26.5" system_health = { path = "../../common/system_health" } directory = { path = "../../common/directory" } +logging = { path = "../../common/logging" } ethereum_serde_utils = "0.5.0" operation_pool = { path = "../operation_pool" } sensitive_url = { path = "../../common/sensitive_url" } unused_port = {path = "../../common/unused_port"} -logging = { path = "../../common/logging" } store = { path = "../store" } [dev-dependencies] @@ -51,4 +51,4 @@ genesis = { path = "../genesis" } [[test]] name = "bn_http_api_tests" -path = "tests/main.rs" +path = "tests/main.rs" \ No newline at end of file diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index be1463f0c3..55e00bab34 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -36,6 +36,7 @@ use eth2::types::{ }; use lighthouse_network::{types::SyncState, EnrExt, NetworkGlobals, PeerId, PubsubMessage}; use lighthouse_version::version_with_platform; +use logging::SSELoggingComponents; use network::{NetworkMessage, NetworkSenders, ValidatorSubscriptionMessage}; use operation_pool::ReceivedPreCapella; use parking_lot::RwLock; @@ -108,6 +109,7 @@ pub struct Context { pub network_senders: Option>, pub network_globals: Option>>, pub eth1_service: Option, + pub sse_logging_components: Option, pub log: Logger, } @@ -448,6 +450,9 @@ pub fn serve( let inner_ctx = ctx.clone(); let log_filter = warp::any().map(move || inner_ctx.log.clone()); + let inner_components = ctx.sse_logging_components.clone(); + let sse_component_filter = warp::any().map(move || inner_components.clone()); + // Create a `warp` filter that provides access to local system information. let system_info = Arc::new(RwLock::new(sysinfo::System::new())); { @@ -3729,6 +3734,44 @@ pub fn serve( }, ); + // Subscribe to logs via Server Side Events + // /lighthouse/logs + let lighthouse_log_events = warp::path("lighthouse") + .and(warp::path("logs")) + .and(warp::path::end()) + .and(sse_component_filter) + .and_then(|sse_component: Option| { + blocking_response_task(move || { + if let Some(logging_components) = sse_component { + // Build a JSON stream + let s = + BroadcastStream::new(logging_components.sender.subscribe()).map(|msg| { + match msg { + Ok(data) => { + // Serialize to json + match data.to_json_string() { + // Send the json as a Server Side Event + Ok(json) => Ok(Event::default().data(json)), + Err(e) => Err(warp_utils::reject::server_sent_event_error( + format!("Unable to serialize to JSON {}", e), + )), + } + } + Err(e) => Err(warp_utils::reject::server_sent_event_error( + format!("Unable to receive event {}", e), + )), + } + }); + + Ok::<_, warp::Rejection>(warp::sse::reply(warp::sse::keep_alive().stream(s))) + } else { + Err(warp_utils::reject::custom_server_error( + "SSE Logging is not enabled".to_string(), + )) + } + }) + }); + // Define the ultimate set of routes that will be provided to the server. // Use `uor` rather than `or` in order to simplify types (see `UnifyingOrFilter`). let routes = warp::get() @@ -3796,6 +3839,7 @@ pub fn serve( .uor(get_lighthouse_block_packing_efficiency) .uor(get_lighthouse_merge_readiness) .uor(get_events) + .uor(lighthouse_log_events.boxed()) .recover(warp_utils::reject::handle_rejection), ) .boxed() diff --git a/beacon_node/http_api/src/test_utils.rs b/beacon_node/http_api/src/test_utils.rs index 8dc9be7dd4..9880a8ca61 100644 --- a/beacon_node/http_api/src/test_utils.rs +++ b/beacon_node/http_api/src/test_utils.rs @@ -198,6 +198,7 @@ pub async fn create_api_server_on_port( network_senders: Some(network_senders), network_globals: Some(network_globals), eth1_service: Some(eth1_service), + sse_logging_components: None, log, }); diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index b20b5c0a95..59a5f4b2e0 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -1081,7 +1081,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .long("gui") .hidden(true) .help("Enable the graphical user interface and all its requirements. \ - This is equivalent to --http and --validator-monitor-auto.") + This enables --http and --validator-monitor-auto and enables SSE logging.") .takes_value(false) ) .arg( diff --git a/book/src/api-lighthouse.md b/book/src/api-lighthouse.md index e67a79c8f0..47fe62f50b 100644 --- a/book/src/api-lighthouse.md +++ b/book/src/api-lighthouse.md @@ -679,3 +679,31 @@ Caveats: This is because the state _prior_ to the `start_epoch` needs to be loaded from the database, and loading a state on a boundary is most efficient. + +### `/lighthouse/logs` + +This is a Server Side Event subscription endpoint. This allows a user to read +the Lighthouse logs directly from the HTTP API endpoint. This currently +exposes INFO and higher level logs. It is only enabled when the `--gui` flag is set in the CLI. + +Example: + +```bash +curl -N "http://localhost:5052/lighthouse/logs" +``` + +Should provide an output that emits log events as they occur: +```json +{ +"data": { + "time": "Mar 13 15:28:41", + "level": "INFO", + "msg": "Syncing", + "service": "slot_notifier", + "est_time": "1 hr 27 mins", + "speed": "5.33 slots/sec", + "distance": "28141 slots (3 days 21 hrs)", + "peers": "8" + } +} +``` diff --git a/book/src/api-vc-endpoints.md b/book/src/api-vc-endpoints.md index 80a14ae771..d5d76e4ef4 100644 --- a/book/src/api-vc-endpoints.md +++ b/book/src/api-vc-endpoints.md @@ -578,3 +578,33 @@ The following fields may be omitted or nullified to obtain default values: ### Example Response Body *No data is included in the response body.* + +## `GET /lighthouse/logs` + +Provides a subscription to receive logs as Server Side Events. Currently the +logs emitted are INFO level or higher. + +### HTTP Specification + +| Property | Specification | +|-------------------|--------------------------------------------| +| Path | `/lighthouse/logs` | +| Method | GET | +| Required Headers | None | +| Typical Responses | 200 | + +### Example Response Body + +```json +{ + "data": { + "time": "Mar 13 15:26:53", + "level": "INFO", + "msg": "Connected to beacon node(s)", + "service": "notifier", + "synced": 1, + "available": 1, + "total": 1 + } +} +``` \ No newline at end of file diff --git a/common/logging/Cargo.toml b/common/logging/Cargo.toml index e56a1a2358..b6179d9e78 100644 --- a/common/logging/Cargo.toml +++ b/common/logging/Cargo.toml @@ -10,6 +10,13 @@ test_logger = [] # Print log output to stderr when running tests instead of drop [dependencies] slog = "2.5.2" slog-term = "2.6.0" +tokio = { version = "1.26.0", features = ["sync"] } lighthouse_metrics = { path = "../lighthouse_metrics" } lazy_static = "1.4.0" sloggers = { version = "2.1.1", features = ["json"] } +slog-async = "2.7.0" +take_mut = "0.2.2" +parking_lot = "0.12.1" +serde = "1.0.153" +serde_json = "1.0.94" +chrono = "0.4.23" diff --git a/common/logging/src/async_record.rs b/common/logging/src/async_record.rs new file mode 100644 index 0000000000..6f998c6191 --- /dev/null +++ b/common/logging/src/async_record.rs @@ -0,0 +1,309 @@ +//! 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(()) + } + #[cfg(integer128)] + fn emit_u128(&mut self, key: Key, val: u128) -> slog::Result { + take(&mut self.kv, |kv| Box::new((kv, SingleKV(key, val)))); + Ok(()) + } + #[cfg(integer128)] + 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 85c4255744..a9ad25f3f3 100644 --- a/common/logging/src/lib.rs +++ b/common/logging/src/lib.rs @@ -11,6 +11,10 @@ use std::time::{Duration, Instant}; pub const MAX_MESSAGE_WIDTH: usize = 40; +pub mod async_record; +mod sse_logging_components; +pub use sse_logging_components::SSELoggingComponents; + /// The minimum interval between log messages indicating that a queue is full. const LOG_DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30); diff --git a/common/logging/src/sse_logging_components.rs b/common/logging/src/sse_logging_components.rs new file mode 100644 index 0000000000..244d09fbd1 --- /dev/null +++ b/common/logging/src/sse_logging_components.rs @@ -0,0 +1,46 @@ +//! This module provides an implementation of `slog::Drain` 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 std::sync::Arc; +use tokio::sync::broadcast::Sender; + +/// 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; + +/// 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>>, +} + +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 } + } +} + +impl Drain for SSELoggingComponents { + type Ok = (); + type Err = &'static str; + + 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 + } + } + Ok(()) + } +} diff --git a/lcli/src/main.rs b/lcli/src/main.rs index bc39c34e26..d072beaa4e 100644 --- a/lcli/src/main.rs +++ b/lcli/src/main.rs @@ -881,6 +881,7 @@ fn run( 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() diff --git a/lighthouse/environment/src/lib.rs b/lighthouse/environment/src/lib.rs index 8ef67e82dd..53915b52d9 100644 --- a/lighthouse/environment/src/lib.rs +++ b/lighthouse/environment/src/lib.rs @@ -12,6 +12,7 @@ use eth2_network_config::Eth2NetworkConfig; use futures::channel::mpsc::{channel, Receiver, Sender}; use futures::{future, StreamExt}; +use logging::SSELoggingComponents; use serde_derive::{Deserialize, Serialize}; use slog::{error, info, o, warn, Drain, Duplicate, Level, Logger}; use sloggers::{file::FileLoggerBuilder, types::Format, types::Severity, Build}; @@ -36,6 +37,7 @@ use {futures::channel::oneshot, std::cell::RefCell}; pub use task_executor::test_utils::null_logger; const LOG_CHANNEL_SIZE: usize = 2048; +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; @@ -57,6 +59,7 @@ pub struct LoggerConfig { pub max_log_number: usize, pub compression: bool, pub is_restricted: bool, + pub sse_logging: bool, } impl Default for LoggerConfig { fn default() -> Self { @@ -72,14 +75,54 @@ impl Default for LoggerConfig { max_log_number: 5, compression: false, is_restricted: true, + sse_logging: false, } } } +/// 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 +/// `Runtime`, instead it only has access to a `Runtime`. +#[derive(Clone)] +pub struct RuntimeContext { + pub executor: TaskExecutor, + pub eth_spec_instance: E, + pub eth2_config: Eth2Config, + pub eth2_network_config: Option>, + pub sse_logging_components: Option, +} + +impl RuntimeContext { + /// Returns a sub-context of this context. + /// + /// The generated service will have the `service_name` in all it's logs. + pub fn service_context(&self, service_name: String) -> Self { + Self { + executor: self.executor.clone_with_name(service_name), + eth_spec_instance: self.eth_spec_instance.clone(), + eth2_config: self.eth2_config.clone(), + eth2_network_config: self.eth2_network_config.clone(), + sse_logging_components: self.sse_logging_components.clone(), + } + } + + /// Returns the `eth2_config` for this service. + 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, eth2_network_config: Option, @@ -91,6 +134,7 @@ impl EnvironmentBuilder { Self { runtime: None, log: None, + sse_logging_components: None, eth_spec_instance: MinimalEthSpec, eth2_config: Eth2Config::minimal(), eth2_network_config: None, @@ -104,6 +148,7 @@ impl EnvironmentBuilder { Self { runtime: None, log: None, + sse_logging_components: None, eth_spec_instance: MainnetEthSpec, eth2_config: Eth2Config::mainnet(), eth2_network_config: None, @@ -117,6 +162,7 @@ impl EnvironmentBuilder { Self { runtime: None, log: None, + sse_logging_components: None, eth_spec_instance: GnosisEthSpec, eth2_config: Eth2Config::gnosis(), eth2_network_config: None, @@ -265,7 +311,7 @@ impl EnvironmentBuilder { .build() .map_err(|e| format!("Unable to build file logger: {}", e))?; - let log = Logger::root(Duplicate::new(stdout_logger, file_logger).fuse(), o!()); + let mut log = Logger::root(Duplicate::new(stdout_logger, file_logger).fuse(), o!()); info!( log, @@ -273,6 +319,14 @@ impl EnvironmentBuilder { "path" => format!("{:?}", path) ); + // 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()); + + log = Logger::root(Duplicate::new(log, sse_logger).fuse(), o!()); + } + self.log = Some(log); Ok(self) @@ -315,6 +369,7 @@ impl EnvironmentBuilder { 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, eth2_network_config: self.eth2_network_config.map(Arc::new), @@ -322,42 +377,6 @@ impl EnvironmentBuilder { } } -/// 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 -/// `Runtime`, instead it only has access to a `Runtime`. -#[derive(Clone)] -pub struct RuntimeContext { - pub executor: TaskExecutor, - pub eth_spec_instance: E, - pub eth2_config: Eth2Config, - pub eth2_network_config: Option>, -} - -impl RuntimeContext { - /// Returns a sub-context of this context. - /// - /// The generated service will have the `service_name` in all it's logs. - pub fn service_context(&self, service_name: String) -> Self { - Self { - executor: self.executor.clone_with_name(service_name), - eth_spec_instance: self.eth_spec_instance.clone(), - eth2_config: self.eth2_config.clone(), - eth2_network_config: self.eth2_network_config.clone(), - } - } - - /// Returns the `eth2_config` for this service. - 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() - } -} - /// An environment where Lighthouse services can run. Used to start a production beacon node or /// validator client, or to run tests that involve logging and async task execution. pub struct Environment { @@ -369,6 +388,7 @@ pub struct Environment { signal: Option, exit: exit_future::Exit, log: Logger, + sse_logging_components: Option, eth_spec_instance: E, pub eth2_config: Eth2Config, pub eth2_network_config: Option>, @@ -395,6 +415,7 @@ impl Environment { eth_spec_instance: self.eth_spec_instance.clone(), eth2_config: self.eth2_config.clone(), eth2_network_config: self.eth2_network_config.clone(), + sse_logging_components: self.sse_logging_components.clone(), } } @@ -410,6 +431,7 @@ impl Environment { eth_spec_instance: self.eth_spec_instance.clone(), eth2_config: self.eth2_config.clone(), eth2_network_config: self.eth2_network_config.clone(), + sse_logging_components: self.sse_logging_components.clone(), } } diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index f55e39bfb7..b814639ceb 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -483,6 +483,16 @@ fn run( }; } + let sse_logging = { + if let Some(bn_matches) = matches.subcommand_matches("beacon_node") { + bn_matches.is_present("gui") + } else if let Some(vc_matches) = matches.subcommand_matches("validator_client") { + vc_matches.is_present("http") + } else { + false + } + }; + let logger_config = LoggerConfig { path: log_path, debug_level: String::from(debug_level), @@ -495,6 +505,7 @@ fn run( max_log_number: logfile_max_number, compression: logfile_compress, is_restricted: logfile_restricted, + sse_logging, }; let builder = environment_builder.initialize_logger(logger_config.clone())?; diff --git a/testing/simulator/src/eth1_sim.rs b/testing/simulator/src/eth1_sim.rs index a5462da396..3e764d27d0 100644 --- a/testing/simulator/src/eth1_sim.rs +++ b/testing/simulator/src/eth1_sim.rs @@ -72,6 +72,7 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> { max_log_number: 0, compression: false, is_restricted: true, + sse_logging: false, })? .multi_threaded_tokio_runtime()? .build()?; diff --git a/testing/simulator/src/no_eth1_sim.rs b/testing/simulator/src/no_eth1_sim.rs index b7598f9fa7..fc18b1cd48 100644 --- a/testing/simulator/src/no_eth1_sim.rs +++ b/testing/simulator/src/no_eth1_sim.rs @@ -54,6 +54,7 @@ pub fn run_no_eth1_sim(matches: &ArgMatches) -> Result<(), String> { max_log_number: 0, compression: false, is_restricted: true, + sse_logging: false, })? .multi_threaded_tokio_runtime()? .build()?; diff --git a/testing/simulator/src/sync_sim.rs b/testing/simulator/src/sync_sim.rs index 5eaed809df..78f7e1ee9f 100644 --- a/testing/simulator/src/sync_sim.rs +++ b/testing/simulator/src/sync_sim.rs @@ -58,6 +58,7 @@ fn syncing_sim( max_log_number: 0, compression: false, is_restricted: true, + sse_logging: false, })? .multi_threaded_tokio_runtime()? .build()?; diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index e0172afd2a..494ebcb3df 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -25,6 +25,7 @@ bincode = "1.3.1" serde_json = "1.0.58" slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_trace"] } tokio = { version = "1.14.0", features = ["time"] } +tokio-stream = { version = "0.1.3", features = ["sync"] } futures = "0.3.7" dirs = "3.0.1" directory = { path = "../common/directory" } @@ -61,4 +62,5 @@ url = "2.2.2" malloc_utils = { path = "../common/malloc_utils" } sysinfo = "0.26.5" system_health = { path = "../common/system_health" } +logging = { path = "../common/logging" } diff --git a/validator_client/src/http_api/mod.rs b/validator_client/src/http_api/mod.rs index 15b3f9fe09..fa6cde3ed5 100644 --- a/validator_client/src/http_api/mod.rs +++ b/validator_client/src/http_api/mod.rs @@ -18,6 +18,7 @@ use eth2::lighthouse_vc::{ types::{self as api_types, GenericResponse, Graffiti, PublicKey, PublicKeyBytes}, }; use lighthouse_version::version_with_platform; +use logging::SSELoggingComponents; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use slog::{crit, info, warn, Logger}; @@ -31,6 +32,7 @@ use std::sync::Arc; use sysinfo::{System, SystemExt}; use system_health::observe_system_health_vc; use task_executor::TaskExecutor; +use tokio_stream::{wrappers::BroadcastStream, StreamExt}; use types::{ChainSpec, ConfigAndPreset, EthSpec}; use validator_dir::Builder as ValidatorDirBuilder; use warp::{ @@ -39,6 +41,7 @@ use warp::{ response::Response, StatusCode, }, + sse::Event, Filter, }; @@ -73,6 +76,7 @@ pub struct Context { pub spec: ChainSpec, pub config: Config, pub log: Logger, + pub sse_logging_components: Option, pub slot_clock: T, pub _phantom: PhantomData, } @@ -201,6 +205,10 @@ pub fn serve( let api_token_path_inner = api_token_path.clone(); let api_token_path_filter = warp::any().map(move || api_token_path_inner.clone()); + // Filter for SEE Logging events + let inner_components = ctx.sse_logging_components.clone(); + let sse_component_filter = warp::any().map(move || inner_components.clone()); + // Create a `warp` filter that provides access to local system information. let system_info = Arc::new(RwLock::new(sysinfo::System::new())); { @@ -1021,6 +1029,49 @@ pub fn serve( }) }); + // Subscribe to get VC logs via Server side events + // /lighthouse/logs + let get_log_events = warp::path("lighthouse") + .and(warp::path("logs")) + .and(warp::path::end()) + .and(sse_component_filter) + .and_then(|sse_component: Option| { + warp_utils::task::blocking_task(move || { + if let Some(logging_components) = sse_component { + // Build a JSON stream + let s = + BroadcastStream::new(logging_components.sender.subscribe()).map(|msg| { + match msg { + Ok(data) => { + // Serialize to json + match data.to_json_string() { + // Send the json as a Server Sent Event + Ok(json) => Event::default().json_data(json).map_err(|e| { + warp_utils::reject::server_sent_event_error(format!( + "{:?}", + e + )) + }), + Err(e) => Err(warp_utils::reject::server_sent_event_error( + format!("Unable to serialize to JSON {}", e), + )), + } + } + Err(e) => Err(warp_utils::reject::server_sent_event_error( + format!("Unable to receive event {}", e), + )), + } + }); + + Ok::<_, warp::Rejection>(warp::sse::reply(warp::sse::keep_alive().stream(s))) + } else { + Err(warp_utils::reject::custom_server_error( + "SSE Logging is not enabled".to_string(), + )) + } + }) + }); + let routes = warp::any() .and(authorization_header_filter) // Note: it is critical that the `authorization_header_filter` is applied to all routes. @@ -1061,8 +1112,8 @@ pub fn serve( .or(delete_std_remotekeys), )), ) - // The auth route is the only route that is allowed to be accessed without the API token. - .or(warp::get().and(get_auth)) + // The auth route and logs are the only routes that are allowed to be accessed without the API token. + .or(warp::get().and(get_auth.or(get_log_events.boxed()))) // Maps errors into HTTP responses. .recover(warp_utils::reject::handle_rejection) // Add a `Server` header. diff --git a/validator_client/src/http_api/tests.rs b/validator_client/src/http_api/tests.rs index 1c593b1a4e..84d2fe437d 100644 --- a/validator_client/src/http_api/tests.rs +++ b/validator_client/src/http_api/tests.rs @@ -134,7 +134,8 @@ impl ApiTester { listen_port: 0, allow_origin: None, }, - log: log.clone(), + sse_logging_components: None, + log, slot_clock: slot_clock.clone(), _phantom: PhantomData, }); diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 6563d2fea5..3dde49f227 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -576,6 +576,7 @@ impl ProductionValidatorClient { graffiti_flag: self.config.graffiti, spec: self.context.eth2_config.spec.clone(), config: self.config.http_api.clone(), + sse_logging_components: self.context.sse_logging_components.clone(), slot_clock: self.slot_clock.clone(), log: log.clone(), _phantom: PhantomData, From c547a11b0da48db6fdd03bca2c6ce2448bbcc3a9 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 23 May 2023 00:17:10 +0000 Subject: [PATCH 14/63] v4.2.0 (#4309) ## Issue Addressed NA ## Proposed Changes Bump versions ## Additional Info NA --- Cargo.lock | 8 ++++---- beacon_node/Cargo.toml | 2 +- boot_node/Cargo.toml | 2 +- common/lighthouse_version/src/lib.rs | 4 ++-- lcli/Cargo.toml | 2 +- lighthouse/Cargo.toml | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3114723a73..6f40e53c34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -661,7 +661,7 @@ dependencies = [ [[package]] name = "beacon_node" -version = "4.1.0" +version = "4.2.0" dependencies = [ "beacon_chain", "clap", @@ -840,7 +840,7 @@ dependencies = [ [[package]] name = "boot_node" -version = "4.1.0" +version = "4.2.0" dependencies = [ "beacon_node", "clap", @@ -3897,7 +3897,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lcli" -version = "4.1.0" +version = "4.2.0" dependencies = [ "account_utils", "beacon_chain", @@ -4549,7 +4549,7 @@ dependencies = [ [[package]] name = "lighthouse" -version = "4.1.0" +version = "4.2.0" dependencies = [ "account_manager", "account_utils", diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index 95f145a557..acf373070e 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "beacon_node" -version = "4.1.0" +version = "4.2.0" authors = ["Paul Hauner ", "Age Manning "] edition = "2021" diff --git a/common/lighthouse_version/src/lib.rs b/common/lighthouse_version/src/lib.rs index d30f45ca29..3f2745bf90 100644 --- a/common/lighthouse_version/src/lib.rs +++ b/common/lighthouse_version/src/lib.rs @@ -17,8 +17,8 @@ pub const VERSION: &str = git_version!( // NOTE: using --match instead of --exclude for compatibility with old Git "--match=thiswillnevermatchlol" ], - prefix = "Lighthouse/v4.1.0-", - fallback = "Lighthouse/v4.1.0" + prefix = "Lighthouse/v4.2.0-", + fallback = "Lighthouse/v4.2.0" ); /// Returns `VERSION`, but with platform information appended to the end. diff --git a/lcli/Cargo.toml b/lcli/Cargo.toml index 3d875f54bb..b4d1baba4a 100644 --- a/lcli/Cargo.toml +++ b/lcli/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "lcli" description = "Lighthouse CLI (modeled after zcli)" -version = "4.1.0" +version = "4.2.0" authors = ["Paul Hauner "] edition = "2021" diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 2afad1b582..5d2b5e092f 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lighthouse" -version = "4.1.0" +version = "4.2.0" authors = ["Sigma Prime "] edition = "2021" autotests = false From 744b1950e54a4cf80c91e1f3a8dc26128deab062 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Tue, 30 May 2023 01:38:45 +0000 Subject: [PATCH 15/63] Keep payload cache idempotent (#4256) ## Issue Addressed [#4239](https://github.com/sigp/lighthouse/issues/4239) ## Proposed Changes keep the payload cache entry intact after fetching it ## Additional Info --- beacon_node/execution_layer/src/lib.rs | 2 +- beacon_node/execution_layer/src/payload_cache.rs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 19fa91b129..51b681b219 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -380,7 +380,7 @@ impl ExecutionLayer { /// Attempt to retrieve a full payload from the payload cache by the payload root pub fn get_payload_by_root(&self, root: &Hash256) -> Option> { - self.inner.payload_cache.pop(root) + self.inner.payload_cache.get(root) } pub fn executor(&self) -> &TaskExecutor { diff --git a/beacon_node/execution_layer/src/payload_cache.rs b/beacon_node/execution_layer/src/payload_cache.rs index 60a8f2a95c..1722edff46 100644 --- a/beacon_node/execution_layer/src/payload_cache.rs +++ b/beacon_node/execution_layer/src/payload_cache.rs @@ -30,4 +30,8 @@ impl PayloadCache { pub fn pop(&self, root: &Hash256) -> Option> { self.payloads.lock().pop(&PayloadCacheId(*root)) } + + pub fn get(&self, hash: &Hash256) -> Option> { + self.payloads.lock().get(&PayloadCacheId(*hash)).cloned() + } } From 02ef7ae01699433542a56713e72049c0ef57beff Mon Sep 17 00:00:00 2001 From: Philippe Schommers Date: Tue, 30 May 2023 01:38:46 +0000 Subject: [PATCH 16/63] chore: Bellatrix occurred for Gnosis (#4301) ## Issue Addressed None ## Proposed Changes Tiny change to the documentation: Bellatrix happened for Gnosis so also needs a merge-ready config. ## Additional Info None --- book/src/merge-migration.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/book/src/merge-migration.md b/book/src/merge-migration.md index ec9aeaaee8..08107695f3 100644 --- a/book/src/merge-migration.md +++ b/book/src/merge-migration.md @@ -26,8 +26,7 @@ engine to a merge-ready version. You must configure your node to be merge-ready before the Bellatrix fork occurs on the network on which your node is operating. -* **Gnosis**: the Bellatrix fork has not yet been scheduled. -* **Mainnet**, **Goerli (Prater)**, **Ropsten**, **Sepolia**, **Kiln**: the Bellatrix fork has +* **Mainnet**, **Goerli (Prater)**, **Ropsten**, **Sepolia**, **Kiln**, **Gnosis**: the Bellatrix fork has already occurred. You must have a merge-ready configuration right now. ## Connecting to an execution engine From 10318fa34b909e4665a243bcc6843ab1932ecca3 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Tue, 30 May 2023 01:38:47 +0000 Subject: [PATCH 17/63] Update blog link in README (#4322) ## Issue Addressed Update blog link in README.md Co-authored-by: Jimmy Chen --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3565882d6e..ade3bc2aba 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ An open-source Ethereum consensus client, written in Rust and maintained by Sigm [Book Link]: https://lighthouse-book.sigmaprime.io [stable]: https://github.com/sigp/lighthouse/tree/stable [unstable]: https://github.com/sigp/lighthouse/tree/unstable -[blog]: https://lighthouse.sigmaprime.io +[blog]: https://lighthouse-blog.sigmaprime.io [Documentation](https://lighthouse-book.sigmaprime.io) From 88abaaae05cc08ff8151054a162b575e1072b74c Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Tue, 30 May 2023 01:38:48 +0000 Subject: [PATCH 18/63] Add `db inspect --output values` option to support dumping raw db values (#4324) ## Issue Addressed Add a new `--output values` option to `db inspect` for dumping raw database values to SSZ files. This could be useful for inspecting the database when we're unable to start the beacon node. Example usage: ``` # Output the `ForkChoice` column to an SSZ file lighthouse db inspect --column frk --output values ``` By default, it stores the output files in the current directory, and can be overriden with `--ouput-dir`. List of columns can be found here: https://github.com/sigp/lighthouse/blob/c547a11b0da48db6fdd03bca2c6ce2448bbcc3a9/beacon_node/store/src/lib.rs#L169-L216 --- database_manager/src/lib.rs | 73 +++++++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/database_manager/src/lib.rs b/database_manager/src/lib.rs index 33accfc057..ce0b094b77 100644 --- a/database_manager/src/lib.rs +++ b/database_manager/src/lib.rs @@ -6,6 +6,9 @@ use beacon_node::{get_data_dir, get_slots_per_restore_point, ClientConfig}; use clap::{App, Arg, ArgMatches}; use environment::{Environment, RuntimeContext}; use slog::{info, Logger}; +use std::fs; +use std::io::Write; +use std::path::PathBuf; use store::{ errors::Error, metadata::{SchemaVersion, CURRENT_SCHEMA_VERSION}, @@ -57,6 +60,13 @@ pub fn inspect_cli_app<'a, 'b>() -> App<'a, 'b> { .default_value("sizes") .possible_values(InspectTarget::VARIANTS), ) + .arg( + Arg::with_name("output-dir") + .long("output-dir") + .value_name("DIR") + .help("Base directory for the output files. Defaults to the current directory") + .takes_value(true), + ) } pub fn prune_payloads_app<'a, 'b>() -> App<'a, 'b> { @@ -154,18 +164,27 @@ pub enum InspectTarget { ValueSizes, #[strum(serialize = "total")] ValueTotal, + #[strum(serialize = "values")] + Values, } pub struct InspectConfig { column: DBColumn, target: InspectTarget, + /// Configures where the inspect output should be stored. + output_dir: PathBuf, } fn parse_inspect_config(cli_args: &ArgMatches) -> Result { let column = clap_utils::parse_required(cli_args, "column")?; let target = clap_utils::parse_required(cli_args, "output")?; - - Ok(InspectConfig { column, target }) + let output_dir: PathBuf = + clap_utils::parse_optional(cli_args, "output-dir")?.unwrap_or_else(PathBuf::new); + Ok(InspectConfig { + column, + target, + output_dir, + }) } pub fn inspect_db( @@ -173,7 +192,7 @@ pub fn inspect_db( client_config: ClientConfig, runtime_context: &RuntimeContext, log: Logger, -) -> Result<(), Error> { +) -> Result<(), String> { let spec = runtime_context.eth2_config.spec.clone(); let hot_path = client_config.get_db_path(); let cold_path = client_config.get_freezer_db_path(); @@ -185,12 +204,19 @@ pub fn inspect_db( client_config.store, spec, log, - )?; + ) + .map_err(|e| format!("{:?}", e))?; let mut total = 0; + let base_path = &inspect_config.output_dir; + + if let InspectTarget::Values = inspect_config.target { + fs::create_dir_all(base_path) + .map_err(|e| format!("Unable to create import directory: {:?}", e))?; + } for res in db.hot_db.iter_column(inspect_config.column) { - let (key, value) = res?; + let (key, value) = res.map_err(|e| format!("{:?}", e))?; match inspect_config.target { InspectTarget::ValueSizes => { @@ -200,11 +226,32 @@ pub fn inspect_db( InspectTarget::ValueTotal => { total += value.len(); } + InspectTarget::Values => { + let file_path = + base_path.join(format!("{}_{}.ssz", inspect_config.column.as_str(), key)); + + let write_result = fs::OpenOptions::new() + .create(true) + .write(true) + .open(&file_path) + .map_err(|e| format!("Failed to open file: {:?}", e)) + .map(|mut file| { + file.write_all(&value) + .map_err(|e| format!("Failed to write file: {:?}", e)) + }); + if let Err(e) = write_result { + println!("Error writing values to file {:?}: {:?}", file_path, e); + } else { + println!("Successfully saved values to file: {:?}", file_path); + } + + total += value.len(); + } } } match inspect_config.target { - InspectTarget::ValueSizes | InspectTarget::ValueTotal => { + InspectTarget::ValueSizes | InspectTarget::ValueTotal | InspectTarget::Values => { println!("Total: {} bytes", total); } } @@ -292,21 +339,23 @@ pub fn run(cli_args: &ArgMatches<'_>, env: Environment) -> Result let client_config = parse_client_config(cli_args, &env)?; let context = env.core_context(); let log = context.log().clone(); + let format_err = |e| format!("Fatal error: {:?}", e); match cli_args.subcommand() { - ("version", Some(_)) => display_db_version(client_config, &context, log), + ("version", Some(_)) => { + display_db_version(client_config, &context, log).map_err(format_err) + } ("migrate", Some(cli_args)) => { let migrate_config = parse_migrate_config(cli_args)?; - migrate_db(migrate_config, client_config, &context, log) + migrate_db(migrate_config, client_config, &context, log).map_err(format_err) } ("inspect", Some(cli_args)) => { let inspect_config = parse_inspect_config(cli_args)?; inspect_db(inspect_config, client_config, &context, log) } - ("prune_payloads", Some(_)) => prune_payloads(client_config, &context, log), - _ => { - return Err("Unknown subcommand, for help `lighthouse database_manager --help`".into()) + ("prune_payloads", Some(_)) => { + prune_payloads(client_config, &context, log).map_err(format_err) } + _ => Err("Unknown subcommand, for help `lighthouse database_manager --help`".into()), } - .map_err(|e| format!("Fatal error: {:?}", e)) } From 2a7e54d8bdfad9d05997660d5ad5ee81a4291821 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Tue, 30 May 2023 01:38:49 +0000 Subject: [PATCH 19/63] swap unnecessary write lock to read lock in block_verification (#4340) ## Issue Addressed [#4334](https://github.com/sigp/lighthouse/issues/4334) ## Proposed Changes swap unnecessary write lock to read lock ## Additional Info N/A Co-authored-by: Michael Sproul --- beacon_node/beacon_chain/src/block_verification.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index ca4df864db..dba38af9bd 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -747,7 +747,7 @@ impl GossipVerifiedBlock { // We check this *before* we load the parent so that we can return a more detailed error. check_block_is_finalized_checkpoint_or_descendant( chain, - &chain.canonical_head.fork_choice_write_lock(), + &chain.canonical_head.fork_choice_read_lock(), &block, )?; From a7da331f6a459c574cfe601b61da6903d68c3a22 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Tue, 30 May 2023 01:38:50 +0000 Subject: [PATCH 20/63] Fix geth scripts (#4342) ## Issue Addressed N/A ## Proposed Changes Geth's latest release breaks our CI with the following message ``` Fatal: Failed to register the Ethereum service: ethash is only supported as a historical component of already merged networks Shutting down ``` Latest geth version has removed support for PoW networks. Hence, we need to add an extra `terminalTotalDifficultyPassed ` parameter in the genesis config to start from a merged network. --- scripts/local_testnet/genesis.json | 3 ++- scripts/tests/genesis.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/local_testnet/genesis.json b/scripts/local_testnet/genesis.json index f92a5f5d00..3ac553e55b 100644 --- a/scripts/local_testnet/genesis.json +++ b/scripts/local_testnet/genesis.json @@ -13,7 +13,8 @@ "londonBlock": 0, "mergeNetsplitBlock": 0, "shanghaiTime": 0, - "terminalTotalDifficulty": 0 + "terminalTotalDifficulty": 0, + "terminalTotalDifficultyPassed": true }, "alloc": { "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { diff --git a/scripts/tests/genesis.json b/scripts/tests/genesis.json index 985bb9cef8..ec3cd1e813 100644 --- a/scripts/tests/genesis.json +++ b/scripts/tests/genesis.json @@ -12,7 +12,8 @@ "berlinBlock": 0, "londonBlock": 0, "mergeForkBlock": 0, - "terminalTotalDifficulty": 0 + "terminalTotalDifficulty": 0, + "terminalTotalDifficultyPassed": true }, "alloc": { "0x0000000000000000000000000000000000000000": { From baad729fa7afb637af750b04ea005aa3f22eb1ee Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 30 May 2023 01:38:51 +0000 Subject: [PATCH 21/63] Fix Rust 1.71.0 warnings (#4348) ## Issue Addressed The Rust 1.70 release is imminent, so CI is using 1.71 for the Beta compiler, which is failing with a warning. --- beacon_node/lighthouse_network/src/peer_manager/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/lighthouse_network/src/peer_manager/mod.rs b/beacon_node/lighthouse_network/src/peer_manager/mod.rs index b2096013bf..c6c737caed 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/mod.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/mod.rs @@ -1266,7 +1266,7 @@ impl PeerManager { ); } - let mut score_peers: &mut (f64, usize) = avg_score_per_client + let score_peers: &mut (f64, usize) = avg_score_per_client .entry(peer_info.client().kind.to_string()) .or_default(); score_peers.0 += peer_info.score().score(); From d150ccbee53c55b582d82bec6d8011167d22a93a Mon Sep 17 00:00:00 2001 From: Daniel Ramirez Chiquillo Date: Tue, 30 May 2023 06:15:54 +0000 Subject: [PATCH 22/63] Add `libpq-dev` and `docker` to the to the list of additional requirements for developers in the Book (#4282) ## Issue Addressed Realized this was missing while discussing #4280 ## Proposed Changes Add an Item to the list of additional requirements for developers. --- book/src/setup.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/book/src/setup.md b/book/src/setup.md index 62580ac1be..ea3c5664ac 100644 --- a/book/src/setup.md +++ b/book/src/setup.md @@ -18,7 +18,9 @@ The additional requirements for developers are: the networking stack. - [`java 11 runtime`](https://openjdk.java.net/projects/jdk/). 11 is the minimum, used by web3signer_tests. - +- [`libpq-dev`](https://www.postgresql.org/docs/devel/libpq.html). Also know as + `libpq-devel` on some systems. +- [`docker`](https://www.docker.com/). Some tests need docker installed and **running**. ## Using `make` Commands to run the test suite are available via the `Makefile` in the From fdea8f2b27c2e682029eea1ae374ebd54b578ecf Mon Sep 17 00:00:00 2001 From: Age Manning Date: Tue, 30 May 2023 06:15:56 +0000 Subject: [PATCH 23/63] Shift subnet backbone structure (attnets revamp) (#4304) This PR address the following spec change: https://github.com/ethereum/consensus-specs/pull/3312 Instead of subscribing to a long-lived subnet for every attached validator to a beacon node, all beacon nodes will subscribe to `SUBNETS_PER_NODE` long-lived subnets. This is currently set to 2 for mainnet. This PR does not include any scoring or advanced discovery mechanisms. A future PR will improve discovery and we can implement scoring after the next hard fork when we expect all client teams and all implementations to respect this spec change. This will be a significant change in the subnet network structure for consensus clients and we will likely have to monitor and tweak our peer management logic. --- beacon_node/network/Cargo.toml | 6 +- beacon_node/network/src/service.rs | 3 +- .../src/subnet_service/attestation_subnets.rs | 388 +++--------------- .../network/src/subnet_service/tests/mod.rs | 157 ++++--- .../gnosis/config.yaml | 4 + .../mainnet/config.yaml | 4 + .../prater/config.yaml | 4 + .../sepolia/config.yaml | 4 + consensus/types/src/chain_spec.rs | 38 +- consensus/types/src/config_and_preset.rs | 4 - consensus/types/src/subnet_id.rs | 93 ++++- 11 files changed, 277 insertions(+), 428 deletions(-) diff --git a/beacon_node/network/Cargo.toml b/beacon_node/network/Cargo.toml index a234165d11..c991728994 100644 --- a/beacon_node/network/Cargo.toml +++ b/beacon_node/network/Cargo.toml @@ -46,8 +46,4 @@ derivative = "2.2.0" delay_map = "0.3.0" ethereum-types = { version = "0.14.1", optional = true } operation_pool = { path = "../operation_pool" } -execution_layer = { path = "../execution_layer" } - -[features] -deterministic_long_lived_attnets = [ "ethereum-types" ] -# default = ["deterministic_long_lived_attnets"] +execution_layer = { path = "../execution_layer" } \ No newline at end of file diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index edc1d5c2ef..2c919233fc 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -317,8 +317,7 @@ impl NetworkService { // attestation subnet service let attestation_service = AttestationService::new( beacon_chain.clone(), - #[cfg(feature = "deterministic_long_lived_attnets")] - network_globals.local_enr().node_id().raw().into(), + network_globals.local_enr().node_id(), config, &network_log, ); diff --git a/beacon_node/network/src/subnet_service/attestation_subnets.rs b/beacon_node/network/src/subnet_service/attestation_subnets.rs index e46a52cfb2..b4f52df39d 100644 --- a/beacon_node/network/src/subnet_service/attestation_subnets.rs +++ b/beacon_node/network/src/subnet_service/attestation_subnets.rs @@ -3,7 +3,6 @@ //! determines whether attestations should be aggregated and/or passed to the beacon node. use super::SubnetServiceMessage; -#[cfg(any(test, feature = "deterministic_long_lived_attnets"))] use std::collections::HashSet; use std::collections::{HashMap, VecDeque}; use std::pin::Pin; @@ -14,10 +13,8 @@ use std::time::Duration; use beacon_chain::{BeaconChain, BeaconChainTypes}; use delay_map::{HashMapDelay, HashSetDelay}; use futures::prelude::*; -use lighthouse_network::{NetworkConfig, Subnet, SubnetDiscovery}; -#[cfg(not(feature = "deterministic_long_lived_attnets"))] -use rand::seq::SliceRandom; -use slog::{debug, error, o, trace, warn}; +use lighthouse_network::{discv5::enr::NodeId, NetworkConfig, Subnet, SubnetDiscovery}; +use slog::{debug, error, info, o, trace, warn}; use slot_clock::SlotClock; use types::{Attestation, EthSpec, Slot, SubnetId, ValidatorSubscription}; @@ -27,10 +24,6 @@ use crate::metrics; /// slot is less than this number, skip the peer discovery process. /// Subnet discovery query takes at most 30 secs, 2 slots take 24s. pub(crate) const MIN_PEER_DISCOVERY_SLOT_LOOK_AHEAD: u64 = 2; -/// The time (in slots) before a last seen validator is considered absent and we unsubscribe from -/// the random gossip topics that we subscribed to due to the validator connection. -#[cfg(not(feature = "deterministic_long_lived_attnets"))] -const LAST_SEEN_VALIDATOR_TIMEOUT_SLOTS: u32 = 150; /// The fraction of a slot that we subscribe to a subnet before the required slot. /// /// Currently a whole slot ahead. @@ -67,30 +60,23 @@ pub struct AttestationService { /// Subnets we are currently subscribed to as short lived subscriptions. /// /// Once they expire, we unsubscribe from these. + /// We subscribe to subnets when we are an aggregator for an exact subnet. short_lived_subscriptions: HashMapDelay, /// Subnets we are currently subscribed to as long lived subscriptions. /// /// We advertise these in our ENR. When these expire, the subnet is removed from our ENR. - #[cfg(feature = "deterministic_long_lived_attnets")] + /// These are required of all beacon nodes. The exact number is determined by the chain + /// specification. long_lived_subscriptions: HashSet, - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - long_lived_subscriptions: HashMapDelay, - /// Short lived subscriptions that need to be done in the future. + /// Short lived subscriptions that need to be executed in the future. scheduled_short_lived_subscriptions: HashSetDelay, /// A collection timeouts to track the existence of aggregate validator subscriptions at an /// `ExactSubnet`. aggregate_validators_on_subnet: Option>, - /// A collection of seen validators. These dictate how many random subnets we should be - /// subscribed to. As these time out, we unsubscribe for the required random subnets and update - /// our ENR. - /// This is a set of validator indices. - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - known_validators: HashSetDelay, - /// The waker for the current thread. waker: Option, @@ -100,16 +86,10 @@ pub struct AttestationService { /// We are always subscribed to all subnets. subscribe_all_subnets: bool, - /// For how many slots we subscribe to long lived subnets. - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - long_lived_subnet_subscription_slots: u64, - /// Our Discv5 node_id. - #[cfg(feature = "deterministic_long_lived_attnets")] - node_id: ethereum_types::U256, + node_id: NodeId, /// Future used to manage subscribing and unsubscribing from long lived subnets. - #[cfg(feature = "deterministic_long_lived_attnets")] next_long_lived_subscription_event: Pin>, /// Whether this node is a block proposer-only node. @@ -122,62 +102,22 @@ pub struct AttestationService { impl AttestationService { /* Public functions */ - #[cfg(not(feature = "deterministic_long_lived_attnets"))] + /// Establish the service based on the passed configuration. pub fn new( beacon_chain: Arc>, + node_id: NodeId, config: &NetworkConfig, log: &slog::Logger, ) -> Self { let log = log.new(o!("service" => "attestation_service")); - // Calculate the random subnet duration from the spec constants. - let spec = &beacon_chain.spec; let slot_duration = beacon_chain.slot_clock.slot_duration(); - let long_lived_subnet_subscription_slots = spec - .epochs_per_random_subnet_subscription - .saturating_mul(T::EthSpec::slots_per_epoch()); - let long_lived_subscription_duration = Duration::from_millis( - slot_duration.as_millis() as u64 * long_lived_subnet_subscription_slots, - ); - // Panics on overflow. Ensure LAST_SEEN_VALIDATOR_TIMEOUT_SLOTS is not too large. - let last_seen_val_timeout = slot_duration - .checked_mul(LAST_SEEN_VALIDATOR_TIMEOUT_SLOTS) - .expect("LAST_SEEN_VALIDATOR_TIMEOUT must not be ridiculously large"); - - let track_validators = !config.import_all_attestations; - let aggregate_validators_on_subnet = - track_validators.then(|| HashSetDelay::new(slot_duration)); - AttestationService { - events: VecDeque::with_capacity(10), - beacon_chain, - short_lived_subscriptions: HashMapDelay::new(slot_duration), - long_lived_subscriptions: HashMapDelay::new(long_lived_subscription_duration), - scheduled_short_lived_subscriptions: HashSetDelay::default(), - aggregate_validators_on_subnet, - known_validators: HashSetDelay::new(last_seen_val_timeout), - waker: None, - discovery_disabled: config.disable_discovery, - proposer_only: config.proposer_only, - subscribe_all_subnets: config.subscribe_all_subnets, - long_lived_subnet_subscription_slots, - log, + if config.subscribe_all_subnets { + slog::info!(log, "Subscribing to all subnets"); + } else { + slog::info!(log, "Deterministic long lived subnets enabled"; "subnets_per_node" => beacon_chain.spec.subnets_per_node, "subscription_duration_in_epochs" => beacon_chain.spec.epochs_per_subnet_subscription); } - } - - #[cfg(feature = "deterministic_long_lived_attnets")] - pub fn new( - beacon_chain: Arc>, - node_id: ethereum_types::U256, - config: &NetworkConfig, - log: &slog::Logger, - ) -> Self { - let log = log.new(o!("service" => "attestation_service")); - - // Calculate the random subnet duration from the spec constants. - let slot_duration = beacon_chain.slot_clock.slot_duration(); - - slog::info!(log, "Deterministic long lived subnets enabled"; "subnets_per_node" => beacon_chain.spec.subnets_per_node); let track_validators = !config.import_all_attestations; let aggregate_validators_on_subnet = @@ -198,9 +138,15 @@ impl AttestationService { // value with a smarter timing Box::pin(tokio::time::sleep(Duration::from_secs(1))) }, + proposer_only: config.proposer_only, log, }; - service.recompute_long_lived_subnets(); + + // If we are not subscribed to all subnets, handle the deterministic set of subnets + if !config.subscribe_all_subnets { + service.recompute_long_lived_subnets(); + } + service } @@ -210,20 +156,12 @@ impl AttestationService { if self.subscribe_all_subnets { self.beacon_chain.spec.attestation_subnet_count as usize } else { - #[cfg(feature = "deterministic_long_lived_attnets")] let count = self .short_lived_subscriptions .keys() .chain(self.long_lived_subscriptions.iter()) .collect::>() .len(); - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - let count = self - .short_lived_subscriptions - .keys() - .chain(self.long_lived_subscriptions.keys()) - .collect::>() - .len(); count } } @@ -236,20 +174,20 @@ impl AttestationService { subscription_kind: SubscriptionKind, ) -> bool { match subscription_kind { - #[cfg(feature = "deterministic_long_lived_attnets")] SubscriptionKind::LongLived => self.long_lived_subscriptions.contains(subnet_id), - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - SubscriptionKind::LongLived => self.long_lived_subscriptions.contains_key(subnet_id), SubscriptionKind::ShortLived => self.short_lived_subscriptions.contains_key(subnet_id), } } + #[cfg(test)] + pub(crate) fn long_lived_subscriptions(&self) -> &HashSet { + &self.long_lived_subscriptions + } + /// Processes a list of validator subscriptions. /// /// This will: /// - Register new validators as being known. - /// - Subscribe to the required number of random subnets. - /// - Update the local ENR for new random subnets due to seeing new validators. /// - Search for peers for required subnets. /// - Request subscriptions for subnets on specific slots when required. /// - Build the timeouts for each of these events. @@ -267,18 +205,17 @@ impl AttestationService { // Maps each subnet_id subscription to it's highest slot let mut subnets_to_discover: HashMap = HashMap::new(); + + // Registers the validator with the attestation service. for subscription in subscriptions { metrics::inc_counter(&metrics::SUBNET_SUBSCRIPTION_REQUESTS); - // Registers the validator with the attestation service. - // This will subscribe to long-lived random subnets if required. trace!(self.log, "Validator subscription"; "subscription" => ?subscription, ); - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - self.add_known_validator(subscription.validator_index); + // Compute the subnet that is associated with this subscription let subnet_id = match SubnetId::compute_subnet::( subscription.slot, subscription.attestation_committee_index, @@ -316,7 +253,7 @@ impl AttestationService { if subscription.is_aggregator { metrics::inc_counter(&metrics::SUBNET_SUBSCRIPTION_AGGREGATOR_REQUESTS); - if let Err(e) = self.subscribe_to_subnet(exact_subnet) { + if let Err(e) = self.subscribe_to_short_lived_subnet(exact_subnet) { warn!(self.log, "Subscription to subnet error"; "error" => e, @@ -347,14 +284,13 @@ impl AttestationService { Ok(()) } - #[cfg(feature = "deterministic_long_lived_attnets")] fn recompute_long_lived_subnets(&mut self) { // Ensure the next computation is scheduled even if assigning subnets fails. let next_subscription_event = self .recompute_long_lived_subnets_inner() .unwrap_or_else(|_| self.beacon_chain.slot_clock.slot_duration()); - debug!(self.log, "Recomputing deterministic long lived attnets"); + debug!(self.log, "Recomputing deterministic long lived subnets"); self.next_long_lived_subscription_event = Box::pin(tokio::time::sleep(next_subscription_event)); @@ -365,14 +301,13 @@ impl AttestationService { /// Gets the long lived subnets the node should be subscribed to during the current epoch and /// the remaining duration for which they remain valid. - #[cfg(feature = "deterministic_long_lived_attnets")] fn recompute_long_lived_subnets_inner(&mut self) -> Result { let current_epoch = self.beacon_chain.epoch().map_err( |e| error!(self.log, "Failed to get the current epoch from clock"; "err" => ?e), )?; let (subnets, next_subscription_epoch) = SubnetId::compute_subnets_for_epoch::( - self.node_id, + self.node_id.raw().into(), current_epoch, &self.beacon_chain.spec, ) @@ -396,17 +331,12 @@ impl AttestationService { Ok(next_subscription_event) } - #[cfg(all(test, feature = "deterministic_long_lived_attnets"))] - pub fn update_long_lived_subnets_testing(&mut self, subnets: HashSet) { - self.update_long_lived_subnets(subnets) - } - /// Updates the long lived subnets. /// /// New subnets are registered as subscribed, removed subnets as unsubscribed and the Enr /// updated accordingly. - #[cfg(feature = "deterministic_long_lived_attnets")] fn update_long_lived_subnets(&mut self, mut subnets: HashSet) { + info!(self.log, "Subscribing to long-lived subnets"; "subnets" => ?subnets.iter().collect::>()); for subnet in &subnets { // Add the events for those subnets that are new as long lived subscriptions. if !self.long_lived_subscriptions.contains(subnet) { @@ -430,28 +360,15 @@ impl AttestationService { } } - // Check for subnets that are being removed + // Update the long_lived_subnets set and check for subnets that are being removed std::mem::swap(&mut self.long_lived_subscriptions, &mut subnets); for subnet in subnets { if !self.long_lived_subscriptions.contains(&subnet) { - if !self.short_lived_subscriptions.contains_key(&subnet) { - debug!(self.log, "Unsubscribing from subnet"; "subnet" => ?subnet, "subscription_kind" => ?SubscriptionKind::LongLived); - self.queue_event(SubnetServiceMessage::Unsubscribe(Subnet::Attestation( - subnet, - ))); - } - - self.queue_event(SubnetServiceMessage::EnrRemove(Subnet::Attestation(subnet))); + self.handle_removed_subnet(subnet, SubscriptionKind::LongLived); } } } - /// Overwrites the long lived subscriptions for testing. - #[cfg(all(test, feature = "deterministic_long_lived_attnets"))] - pub fn set_long_lived_subscriptions(&mut self, subnets: HashSet) { - self.long_lived_subscriptions = subnets - } - /// Checks if we have subscribed aggregate validators for the subnet. If not, checks the gossip /// verification, re-propagates and returns false. pub fn should_process_attestation( @@ -535,7 +452,7 @@ impl AttestationService { } // Subscribes to the subnet if it should be done immediately, or schedules it if required. - fn subscribe_to_subnet( + fn subscribe_to_short_lived_subnet( &mut self, ExactSubnet { subnet_id, slot }: ExactSubnet, ) -> Result<(), &'static str> { @@ -564,12 +481,7 @@ impl AttestationService { // immediately. if time_to_subscription_start.is_zero() { // This is a current or past slot, we subscribe immediately. - self.subscribe_to_subnet_immediately( - subnet_id, - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - SubscriptionKind::ShortLived, - slot + 1, - )?; + self.subscribe_to_short_lived_subnet_immediately(subnet_id, slot + 1)?; } else { // This is a future slot, schedule subscribing. trace!(self.log, "Scheduling subnet subscription"; "subnet" => ?subnet_id, "time_to_subscription_start" => ?time_to_subscription_start); @@ -580,79 +492,6 @@ impl AttestationService { Ok(()) } - /// Updates the `known_validators` mapping and subscribes to long lived subnets if required. - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - fn add_known_validator(&mut self, validator_index: u64) { - let previously_known = self.known_validators.contains_key(&validator_index); - // Add the new validator or update the current timeout for a known validator. - self.known_validators.insert(validator_index); - if !previously_known { - // New validator has subscribed. - // Subscribe to random topics and update the ENR if needed. - self.subscribe_to_random_subnets(); - } - } - - /// Subscribe to long-lived random subnets and update the local ENR bitfield. - /// The number of subnets to subscribe depends on the number of active validators and number of - /// current subscriptions. - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - fn subscribe_to_random_subnets(&mut self) { - if self.subscribe_all_subnets { - // This case is not handled by this service. - return; - } - - let max_subnets = self.beacon_chain.spec.attestation_subnet_count; - // Calculate how many subnets we need, - let required_long_lived_subnets = { - let subnets_for_validators = self - .known_validators - .len() - .saturating_mul(self.beacon_chain.spec.random_subnets_per_validator as usize); - subnets_for_validators // How many subnets we need - .min(max_subnets as usize) // Capped by the max - .saturating_sub(self.long_lived_subscriptions.len()) // Minus those we have - }; - - if required_long_lived_subnets == 0 { - // Nothing to do. - return; - } - - // Build a list of the subnets that we are not currently advertising. - let available_subnets = (0..max_subnets) - .map(SubnetId::new) - .filter(|subnet_id| !self.long_lived_subscriptions.contains_key(subnet_id)) - .collect::>(); - - let subnets_to_subscribe: Vec<_> = available_subnets - .choose_multiple(&mut rand::thread_rng(), required_long_lived_subnets) - .cloned() - .collect(); - - // Calculate in which slot does this subscription end. - let end_slot = match self.beacon_chain.slot_clock.now() { - Some(slot) => slot + self.long_lived_subnet_subscription_slots, - None => { - return debug!( - self.log, - "Failed to calculate end slot of long lived subnet subscriptions." - ) - } - }; - - for subnet_id in &subnets_to_subscribe { - if let Err(e) = self.subscribe_to_subnet_immediately( - *subnet_id, - SubscriptionKind::LongLived, - end_slot, - ) { - debug!(self.log, "Failed to subscribe to long lived subnet"; "subnet" => ?subnet_id, "err" => e); - } - } - } - /* A collection of functions that handle the various timeouts */ /// Registers a subnet as subscribed. @@ -662,11 +501,9 @@ impl AttestationService { /// out the appropriate events. /// /// On determinist long lived subnets, this is only used for short lived subscriptions. - fn subscribe_to_subnet_immediately( + fn subscribe_to_short_lived_subnet_immediately( &mut self, subnet_id: SubnetId, - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - subscription_kind: SubscriptionKind, end_slot: Slot, ) -> Result<(), &'static str> { if self.subscribe_all_subnets { @@ -685,25 +522,12 @@ impl AttestationService { return Err("Time when subscription would end has already passed."); } - #[cfg(feature = "deterministic_long_lived_attnets")] let subscription_kind = SubscriptionKind::ShortLived; // We need to check and add a subscription for the right kind, regardless of the presence // of the subnet as a subscription of the other kind. This is mainly since long lived // subscriptions can be removed at any time when a validator goes offline. - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - let (subscriptions, already_subscribed_as_other_kind) = match subscription_kind { - SubscriptionKind::ShortLived => ( - &mut self.short_lived_subscriptions, - self.long_lived_subscriptions.contains_key(&subnet_id), - ), - SubscriptionKind::LongLived => ( - &mut self.long_lived_subscriptions, - self.short_lived_subscriptions.contains_key(&subnet_id), - ), - }; - #[cfg(feature = "deterministic_long_lived_attnets")] let (subscriptions, already_subscribed_as_other_kind) = ( &mut self.short_lived_subscriptions, self.long_lived_subscriptions.contains(&subnet_id), @@ -738,57 +562,19 @@ impl AttestationService { subnet_id, ))); } - - // If this is a new long lived subscription, send out the appropriate events. - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - if SubscriptionKind::LongLived == subscription_kind { - let subnet = Subnet::Attestation(subnet_id); - // Advertise this subnet in our ENR. - self.long_lived_subscriptions.insert_at( - subnet_id, - end_slot, - time_to_subscription_end, - ); - self.queue_event(SubnetServiceMessage::EnrAdd(subnet)); - - if !self.discovery_disabled { - self.queue_event(SubnetServiceMessage::DiscoverPeers(vec![ - SubnetDiscovery { - subnet, - min_ttl: None, - }, - ])) - } - } } } Ok(()) } - /// A random subnet has expired. - /// - /// This function selects a new subnet to join, or extends the expiry if there are no more - /// available subnets to choose from. - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - fn handle_random_subnet_expiry(&mut self, subnet_id: SubnetId) { - self.handle_removed_subnet(subnet_id, SubscriptionKind::LongLived); - - // Remove the ENR bitfield bit and choose a new random on from the available subnets - // Subscribe to a new random subnet. - self.subscribe_to_random_subnets(); - } - // Unsubscribes from a subnet that was removed if it does not continue to exist as a // subscription of the other kind. For long lived subscriptions, it also removes the // advertisement from our ENR. fn handle_removed_subnet(&mut self, subnet_id: SubnetId, subscription_kind: SubscriptionKind) { let exists_in_other_subscriptions = match subscription_kind { SubscriptionKind::LongLived => self.short_lived_subscriptions.contains_key(&subnet_id), - #[cfg(feature = "deterministic_long_lived_attnets")] SubscriptionKind::ShortLived => self.long_lived_subscriptions.contains(&subnet_id), - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - SubscriptionKind::ShortLived => self.long_lived_subscriptions.contains_key(&subnet_id), }; if !exists_in_other_subscriptions { @@ -806,48 +592,6 @@ impl AttestationService { ))); } } - - /// A known validator has not sent a subscription in a while. They are considered offline and the - /// beacon node no longer needs to be subscribed to the allocated random subnets. - /// - /// We don't keep track of a specific validator to random subnet, rather the ratio of active - /// validators to random subnets. So when a validator goes offline, we can simply remove the - /// allocated amount of random subnets. - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - fn handle_known_validator_expiry(&mut self) { - // Calculate how many subnets should we remove. - let extra_subnet_count = { - let max_subnets = self.beacon_chain.spec.attestation_subnet_count; - let subnets_for_validators = self - .known_validators - .len() - .saturating_mul(self.beacon_chain.spec.random_subnets_per_validator as usize) - .min(max_subnets as usize); - - self.long_lived_subscriptions - .len() - .saturating_sub(subnets_for_validators) - }; - - if extra_subnet_count == 0 { - // Nothing to do - return; - } - - let advertised_subnets = self - .long_lived_subscriptions - .keys() - .cloned() - .collect::>(); - let to_remove_subnets = advertised_subnets - .choose_multiple(&mut rand::thread_rng(), extra_subnet_count) - .cloned(); - - for subnet_id in to_remove_subnets { - self.long_lived_subscriptions.remove(&subnet_id); - self.handle_removed_subnet(subnet_id, SubscriptionKind::LongLived); - } - } } impl Stream for AttestationService { @@ -868,37 +612,34 @@ impl Stream for AttestationService { return Poll::Ready(Some(event)); } - // Process first any known validator expiries, since these affect how many long lived - // subnets we need. - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - match self.known_validators.poll_next_unpin(cx) { - Poll::Ready(Some(Ok(_validator_index))) => { - self.handle_known_validator_expiry(); + // If we aren't subscribed to all subnets, handle the deterministic long-lived subnets + if !self.subscribe_all_subnets { + match self.next_long_lived_subscription_event.as_mut().poll(cx) { + Poll::Ready(_) => { + self.recompute_long_lived_subnets(); + // We re-wake the task as there could be other subscriptions to process + self.waker + .as_ref() + .expect("Waker has been set") + .wake_by_ref(); + } + Poll::Pending => {} } - Poll::Ready(Some(Err(e))) => { - error!(self.log, "Failed to check for random subnet cycles"; "error"=> e); - } - Poll::Ready(None) | Poll::Pending => {} - } - - #[cfg(feature = "deterministic_long_lived_attnets")] - match self.next_long_lived_subscription_event.as_mut().poll(cx) { - Poll::Ready(_) => self.recompute_long_lived_subnets(), - Poll::Pending => {} } // Process scheduled subscriptions that might be ready, since those can extend a soon to // expire subscription. match self.scheduled_short_lived_subscriptions.poll_next_unpin(cx) { Poll::Ready(Some(Ok(ExactSubnet { subnet_id, slot }))) => { - if let Err(e) = self.subscribe_to_subnet_immediately( - subnet_id, - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - SubscriptionKind::ShortLived, - slot + 1, - ) { + if let Err(e) = + self.subscribe_to_short_lived_subnet_immediately(subnet_id, slot + 1) + { debug!(self.log, "Failed to subscribe to short lived subnet"; "subnet" => ?subnet_id, "err" => e); } + self.waker + .as_ref() + .expect("Waker has been set") + .wake_by_ref(); } Poll::Ready(Some(Err(e))) => { error!(self.log, "Failed to check for scheduled subnet subscriptions"; "error"=> e); @@ -910,6 +651,11 @@ impl Stream for AttestationService { match self.short_lived_subscriptions.poll_next_unpin(cx) { Poll::Ready(Some(Ok((subnet_id, _end_slot)))) => { self.handle_removed_subnet(subnet_id, SubscriptionKind::ShortLived); + // We re-wake the task as there could be other subscriptions to process + self.waker + .as_ref() + .expect("Waker has been set") + .wake_by_ref(); } Poll::Ready(Some(Err(e))) => { error!(self.log, "Failed to check for subnet unsubscription times"; "error"=> e); @@ -917,18 +663,6 @@ impl Stream for AttestationService { Poll::Ready(None) | Poll::Pending => {} } - // Process any random subnet expiries. - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - match self.long_lived_subscriptions.poll_next_unpin(cx) { - Poll::Ready(Some(Ok((subnet_id, _end_slot)))) => { - self.handle_random_subnet_expiry(subnet_id) - } - Poll::Ready(Some(Err(e))) => { - error!(self.log, "Failed to check for random subnet cycles"; "error"=> e); - } - Poll::Ready(None) | Poll::Pending => {} - } - // Poll to remove entries on expiration, no need to act on expiration events. if let Some(tracked_vals) = self.aggregate_validators_on_subnet.as_mut() { if let Poll::Ready(Some(Err(e))) = tracked_vals.poll_next_unpin(cx) { diff --git a/beacon_node/network/src/subnet_service/tests/mod.rs b/beacon_node/network/src/subnet_service/tests/mod.rs index a407fe1bcf..3b8c89a442 100644 --- a/beacon_node/network/src/subnet_service/tests/mod.rs +++ b/beacon_node/network/src/subnet_service/tests/mod.rs @@ -126,10 +126,7 @@ fn get_attestation_service( AttestationService::new( beacon_chain, - #[cfg(feature = "deterministic_long_lived_attnets")] - lighthouse_network::discv5::enr::NodeId::random() - .raw() - .into(), + lighthouse_network::discv5::enr::NodeId::random(), &config, &log, ) @@ -179,9 +176,6 @@ async fn get_events + Unpin>( mod attestation_service { - #[cfg(feature = "deterministic_long_lived_attnets")] - use std::collections::HashSet; - #[cfg(not(windows))] use crate::subnet_service::attestation_subnets::MIN_PEER_DISCOVERY_SLOT_LOOK_AHEAD; @@ -192,8 +186,8 @@ mod attestation_service { attestation_committee_index: CommitteeIndex, slot: Slot, committee_count_at_slot: u64, + is_aggregator: bool, ) -> ValidatorSubscription { - let is_aggregator = true; ValidatorSubscription { validator_index, attestation_committee_index, @@ -203,11 +197,11 @@ mod attestation_service { } } - #[cfg(not(feature = "deterministic_long_lived_attnets"))] fn get_subscriptions( validator_count: u64, slot: Slot, committee_count_at_slot: u64, + is_aggregator: bool, ) -> Vec { (0..validator_count) .map(|validator_index| { @@ -216,6 +210,7 @@ mod attestation_service { validator_index, slot, committee_count_at_slot, + is_aggregator, ) }) .collect() @@ -229,6 +224,7 @@ mod attestation_service { // Keep a low subscription slot so that there are no additional subnet discovery events. let subscription_slot = 0; let committee_count = 1; + let subnets_per_node = MainnetEthSpec::default_spec().subnets_per_node as usize; // create the attestation service and subscriptions let mut attestation_service = get_attestation_service(None); @@ -243,6 +239,7 @@ mod attestation_service { committee_index, current_slot + Slot::new(subscription_slot), committee_count, + true, )]; // submit the subscriptions @@ -266,16 +263,19 @@ mod attestation_service { // Wait for 1 slot duration to get the unsubscription event let events = get_events( &mut attestation_service, - Some(5), + Some(subnets_per_node * 3 + 2), (MainnetEthSpec::slots_per_epoch() * 3) as u32, ) .await; matches::assert_matches!( - events[..3], + events[..6], [ SubnetServiceMessage::Subscribe(_any1), SubnetServiceMessage::EnrAdd(_any3), SubnetServiceMessage::DiscoverPeers(_), + SubnetServiceMessage::Subscribe(_), + SubnetServiceMessage::EnrAdd(_), + SubnetServiceMessage::DiscoverPeers(_), ] ); @@ -284,10 +284,10 @@ mod attestation_service { if !attestation_service .is_subscribed(&subnet_id, attestation_subnets::SubscriptionKind::LongLived) { - assert_eq!(expected[..], events[3..]); + assert_eq!(expected[..], events[subnets_per_node * 3..]); } - // Should be subscribed to only 1 long lived subnet after unsubscription. - assert_eq!(attestation_service.subscription_count(), 1); + // Should be subscribed to only subnets_per_node long lived subnet after unsubscription. + assert_eq!(attestation_service.subscription_count(), subnets_per_node); } /// Test to verify that we are not unsubscribing to a subnet before a required subscription. @@ -297,6 +297,7 @@ mod attestation_service { // subscription config let validator_index = 1; let committee_count = 1; + let subnets_per_node = MainnetEthSpec::default_spec().subnets_per_node as usize; // Makes 2 validator subscriptions to the same subnet but at different slots. // There should be just 1 unsubscription event for the later slot subscription (subscription_slot2). @@ -318,6 +319,7 @@ mod attestation_service { com1, current_slot + Slot::new(subscription_slot1), committee_count, + true, ); let sub2 = get_subscription( @@ -325,6 +327,7 @@ mod attestation_service { com2, current_slot + Slot::new(subscription_slot2), committee_count, + true, ); let subnet_id1 = SubnetId::compute_subnet::( @@ -366,16 +369,22 @@ mod attestation_service { let expected = SubnetServiceMessage::Subscribe(Subnet::Attestation(subnet_id1)); - // Should be still subscribed to 1 long lived and 1 short lived subnet if both are + // Should be still subscribed to 2 long lived and up to 1 short lived subnet if both are // different. if !attestation_service.is_subscribed( &subnet_id1, attestation_subnets::SubscriptionKind::LongLived, ) { - assert_eq!(expected, events[3]); - assert_eq!(attestation_service.subscription_count(), 2); + // The index is 3*subnets_per_node (because we subscribe + discover + enr per long lived + // subnet) + 1 + let index = 3 * subnets_per_node; + assert_eq!(expected, events[index]); + assert_eq!( + attestation_service.subscription_count(), + subnets_per_node + 1 + ); } else { - assert_eq!(attestation_service.subscription_count(), 1); + assert!(attestation_service.subscription_count() == subnets_per_node); } // Get event for 1 more slot duration, we should get the unsubscribe event now. @@ -395,17 +404,17 @@ mod attestation_service { ); } - // Should be subscribed to only 1 long lived subnet after unsubscription. - assert_eq!(attestation_service.subscription_count(), 1); + // Should be subscribed 2 long lived subnet after unsubscription. + assert_eq!(attestation_service.subscription_count(), subnets_per_node); } - #[cfg(not(feature = "deterministic_long_lived_attnets"))] #[tokio::test] - async fn subscribe_all_random_subnets() { + async fn subscribe_all_subnets() { let attestation_subnet_count = MainnetEthSpec::default_spec().attestation_subnet_count; - let subscription_slot = 10; + let subscription_slot = 3; let subscription_count = attestation_subnet_count; let committee_count = 1; + let subnets_per_node = MainnetEthSpec::default_spec().subnets_per_node as usize; // create the attestation service and subscriptions let mut attestation_service = get_attestation_service(None); @@ -419,6 +428,7 @@ mod attestation_service { subscription_count, current_slot + subscription_slot, committee_count, + true, ); // submit the subscriptions @@ -426,42 +436,52 @@ mod attestation_service { .validator_subscriptions(subscriptions) .unwrap(); - let events = get_events(&mut attestation_service, None, 3).await; + let events = get_events(&mut attestation_service, Some(131), 10).await; let mut discover_peer_count = 0; let mut enr_add_count = 0; let mut unexpected_msg_count = 0; + let mut unsubscribe_event_count = 0; for event in &events { match event { SubnetServiceMessage::DiscoverPeers(_) => discover_peer_count += 1, SubnetServiceMessage::Subscribe(_any_subnet) => {} SubnetServiceMessage::EnrAdd(_any_subnet) => enr_add_count += 1, + SubnetServiceMessage::Unsubscribe(_) => unsubscribe_event_count += 1, _ => unexpected_msg_count += 1, } } + // There should be a Subscribe Event, and Enr Add event and a DiscoverPeers event for each + // long-lived subnet initially. The next event should be a bulk discovery event. + let bulk_discovery_index = 3 * subnets_per_node; // The bulk discovery request length should be equal to validator_count - let bulk_discovery_event = events.last().unwrap(); + let bulk_discovery_event = &events[bulk_discovery_index]; if let SubnetServiceMessage::DiscoverPeers(d) = bulk_discovery_event { assert_eq!(d.len(), attestation_subnet_count as usize); } else { panic!("Unexpected event {:?}", bulk_discovery_event); } - // 64 `DiscoverPeer` requests of length 1 corresponding to random subnets + // 64 `DiscoverPeer` requests of length 1 corresponding to deterministic subnets // and 1 `DiscoverPeer` request corresponding to bulk subnet discovery. - assert_eq!(discover_peer_count, subscription_count + 1); - assert_eq!(attestation_service.subscription_count(), 64); - assert_eq!(enr_add_count, 64); + assert_eq!(discover_peer_count, subnets_per_node + 1); + assert_eq!(attestation_service.subscription_count(), subnets_per_node); + assert_eq!(enr_add_count, subnets_per_node); + assert_eq!( + unsubscribe_event_count, + attestation_subnet_count - subnets_per_node as u64 + ); assert_eq!(unexpected_msg_count, 0); // test completed successfully } - #[cfg(not(feature = "deterministic_long_lived_attnets"))] #[tokio::test] - async fn subscribe_all_random_subnets_plus_one() { + async fn subscribe_correct_number_of_subnets() { let attestation_subnet_count = MainnetEthSpec::default_spec().attestation_subnet_count; let subscription_slot = 10; + let subnets_per_node = MainnetEthSpec::default_spec().subnets_per_node as usize; + // the 65th subscription should result in no more messages than the previous scenario let subscription_count = attestation_subnet_count + 1; let committee_count = 1; @@ -478,6 +498,7 @@ mod attestation_service { subscription_count, current_slot + subscription_slot, committee_count, + true, ); // submit the subscriptions @@ -506,12 +527,12 @@ mod attestation_service { } else { panic!("Unexpected event {:?}", bulk_discovery_event); } - // 64 `DiscoverPeer` requests of length 1 corresponding to random subnets + // subnets_per_node `DiscoverPeer` requests of length 1 corresponding to long-lived subnets // and 1 `DiscoverPeer` request corresponding to the bulk subnet discovery. - // For the 65th subscription, the call to `subscribe_to_random_subnets` is not made because we are at capacity. - assert_eq!(discover_peer_count, 64 + 1); - assert_eq!(attestation_service.subscription_count(), 64); - assert_eq!(enr_add_count, 64); + + assert_eq!(discover_peer_count, subnets_per_node + 1); + assert_eq!(attestation_service.subscription_count(), subnets_per_node); + assert_eq!(enr_add_count, subnets_per_node); assert_eq!(unexpected_msg_count, 0); } @@ -521,6 +542,7 @@ mod attestation_service { // subscription config let validator_index = 1; let committee_count = 1; + let subnets_per_node = MainnetEthSpec::default_spec().subnets_per_node as usize; // Makes 2 validator subscriptions to the same subnet but at different slots. // There should be just 1 unsubscription event for the later slot subscription (subscription_slot2). @@ -542,6 +564,7 @@ mod attestation_service { com1, current_slot + Slot::new(subscription_slot1), committee_count, + true, ); let sub2 = get_subscription( @@ -549,6 +572,7 @@ mod attestation_service { com2, current_slot + Slot::new(subscription_slot2), committee_count, + true, ); let subnet_id1 = SubnetId::compute_subnet::( @@ -596,11 +620,10 @@ mod attestation_service { &subnet_id1, attestation_subnets::SubscriptionKind::LongLived, ) { - assert_eq!(expected_subscription, events[3]); - // fourth is a discovery event - assert_eq!(expected_unsubscription, events[5]); + assert_eq!(expected_subscription, events[subnets_per_node * 3]); + assert_eq!(expected_unsubscription, events[subnets_per_node * 3 + 2]); } - assert_eq!(attestation_service.subscription_count(), 1); + assert_eq!(attestation_service.subscription_count(), 2); println!("{events:?}"); let subscription_slot = current_slot + subscription_slot2 - 1; // one less do to the @@ -633,40 +656,44 @@ mod attestation_service { } #[tokio::test] - #[cfg(feature = "deterministic_long_lived_attnets")] async fn test_update_deterministic_long_lived_subnets() { let mut attestation_service = get_attestation_service(None); - let new_subnet = SubnetId::new(1); - let maintained_subnet = SubnetId::new(2); - let removed_subnet = SubnetId::new(3); + let subnets_per_node = MainnetEthSpec::default_spec().subnets_per_node as usize; + let current_slot = attestation_service + .beacon_chain + .slot_clock + .now() + .expect("Could not get current slot"); + + let subscriptions = get_subscriptions(20, current_slot, 30, false); + + // submit the subscriptions attestation_service - .set_long_lived_subscriptions(HashSet::from([removed_subnet, maintained_subnet])); - // clear initial events - let _events = get_events(&mut attestation_service, None, 1).await; + .validator_subscriptions(subscriptions) + .unwrap(); - attestation_service - .update_long_lived_subnets_testing(HashSet::from([maintained_subnet, new_subnet])); - - let events = get_events(&mut attestation_service, None, 1).await; - let new_subnet = Subnet::Attestation(new_subnet); - let removed_subnet = Subnet::Attestation(removed_subnet); + // There should only be the same subscriptions as there are in the specification, + // regardless of subscriptions assert_eq!( - events, + attestation_service.long_lived_subscriptions().len(), + subnets_per_node + ); + + let events = get_events(&mut attestation_service, None, 4).await; + + // Check that we attempt to subscribe and register ENRs + matches::assert_matches!( + events[..6], [ - // events for the new subnet - SubnetServiceMessage::Subscribe(new_subnet), - SubnetServiceMessage::EnrAdd(new_subnet), - SubnetServiceMessage::DiscoverPeers(vec![SubnetDiscovery { - subnet: new_subnet, - min_ttl: None - }]), - // events for the removed subnet - SubnetServiceMessage::Unsubscribe(removed_subnet), - SubnetServiceMessage::EnrRemove(removed_subnet), + SubnetServiceMessage::Subscribe(_), + SubnetServiceMessage::EnrAdd(_), + SubnetServiceMessage::DiscoverPeers(_), + SubnetServiceMessage::Subscribe(_), + SubnetServiceMessage::EnrAdd(_), + SubnetServiceMessage::DiscoverPeers(_), ] ); - println!("{events:?}") } } diff --git a/common/eth2_network_config/built_in_network_configs/gnosis/config.yaml b/common/eth2_network_config/built_in_network_configs/gnosis/config.yaml index ca1d1e88a8..95ca9d0108 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 @@ -86,3 +86,7 @@ PROPOSER_SCORE_BOOST: 40 DEPOSIT_CHAIN_ID: 100 DEPOSIT_NETWORK_ID: 100 DEPOSIT_CONTRACT_ADDRESS: 0x0B98057eA310F4d31F2a452B414647007d1645d9 + +# Network +# --------------------------------------------------------------- +SUBNETS_PER_NODE: 4 \ 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 0bbf873a3f..7b26b30a6c 100644 --- a/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml @@ -86,3 +86,7 @@ PROPOSER_SCORE_BOOST: 40 DEPOSIT_CHAIN_ID: 1 DEPOSIT_NETWORK_ID: 1 DEPOSIT_CONTRACT_ADDRESS: 0x00000000219ab540356cBB839Cbe05303d7705Fa + +# Network +# --------------------------------------------------------------- +SUBNETS_PER_NODE: 2 \ No newline at end of file diff --git a/common/eth2_network_config/built_in_network_configs/prater/config.yaml b/common/eth2_network_config/built_in_network_configs/prater/config.yaml index 69d65ca8fc..63b3d45db9 100644 --- a/common/eth2_network_config/built_in_network_configs/prater/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/prater/config.yaml @@ -86,3 +86,7 @@ DEPOSIT_CHAIN_ID: 5 DEPOSIT_NETWORK_ID: 5 # Prater test deposit contract on Goerli Testnet DEPOSIT_CONTRACT_ADDRESS: 0xff50ed3d0ec03aC01D4C79aAd74928BFF48a7b2b + +# Network +# --------------------------------------------------------------- +SUBNETS_PER_NODE: 2 \ No newline at end of file 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 2946572899..8489f085f4 100644 --- a/common/eth2_network_config/built_in_network_configs/sepolia/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/sepolia/config.yaml @@ -74,3 +74,7 @@ PROPOSER_SCORE_BOOST: 40 DEPOSIT_CHAIN_ID: 11155111 DEPOSIT_NETWORK_ID: 11155111 DEPOSIT_CONTRACT_ADDRESS: 0x7f02C3E3c98b133055B8B348B2Ac625669Ed295D + +# Network +# --------------------------------------------------------------- +SUBNETS_PER_NODE: 2 \ No newline at end of file diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index 2b25cc1d59..5253dcc4b0 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -168,11 +168,9 @@ pub struct ChainSpec { pub maximum_gossip_clock_disparity_millis: u64, pub target_aggregators_per_committee: u64, pub attestation_subnet_count: u64, - pub random_subnets_per_validator: u64, - pub epochs_per_random_subnet_subscription: u64, pub subnets_per_node: u8, pub epochs_per_subnet_subscription: u64, - attestation_subnet_extra_bits: u8, + pub attestation_subnet_extra_bits: u8, /* * Application params @@ -455,17 +453,7 @@ impl ChainSpec { #[allow(clippy::integer_arithmetic)] pub const fn attestation_subnet_prefix_bits(&self) -> u32 { - // maybe use log2 when stable https://github.com/rust-lang/rust/issues/70887 - - // NOTE: this line is here simply to guarantee that if self.attestation_subnet_count type - // is changed, a compiler warning will be raised. This code depends on the type being u64. - let attestation_subnet_count: u64 = self.attestation_subnet_count; - let attestation_subnet_count_bits = if attestation_subnet_count == 0 { - 0 - } else { - 63 - attestation_subnet_count.leading_zeros() - }; - + let attestation_subnet_count_bits = self.attestation_subnet_count.ilog2(); self.attestation_subnet_extra_bits as u32 + attestation_subnet_count_bits } @@ -625,13 +613,11 @@ impl ChainSpec { network_id: 1, // mainnet network id attestation_propagation_slot_range: 32, attestation_subnet_count: 64, - random_subnets_per_validator: 1, - subnets_per_node: 1, + subnets_per_node: 2, maximum_gossip_clock_disparity_millis: 500, target_aggregators_per_committee: 16, - epochs_per_random_subnet_subscription: 256, epochs_per_subnet_subscription: 256, - attestation_subnet_extra_bits: 6, + attestation_subnet_extra_bits: 0, /* * Application specific @@ -852,13 +838,11 @@ impl ChainSpec { network_id: 100, // Gnosis Chain network id attestation_propagation_slot_range: 32, attestation_subnet_count: 64, - random_subnets_per_validator: 1, - subnets_per_node: 1, + subnets_per_node: 4, // Make this larger than usual to avoid network damage maximum_gossip_clock_disparity_millis: 500, target_aggregators_per_committee: 16, - epochs_per_random_subnet_subscription: 256, epochs_per_subnet_subscription: 256, - attestation_subnet_extra_bits: 6, + attestation_subnet_extra_bits: 0, /* * Application specific @@ -946,6 +930,9 @@ pub struct Config { shard_committee_period: u64, #[serde(with = "serde_utils::quoted_u64")] eth1_follow_distance: u64, + #[serde(default = "default_subnets_per_node")] + #[serde(with = "serde_utils::quoted_u8")] + subnets_per_node: u8, #[serde(with = "serde_utils::quoted_u64")] inactivity_score_bias: u64, @@ -1002,6 +989,10 @@ fn default_safe_slots_to_import_optimistically() -> u64 { 128u64 } +fn default_subnets_per_node() -> u8 { + 2u8 +} + impl Default for Config { fn default() -> Self { let chain_spec = MainnetEthSpec::default_spec(); @@ -1084,6 +1075,7 @@ impl Config { min_validator_withdrawability_delay: spec.min_validator_withdrawability_delay, shard_committee_period: spec.shard_committee_period, eth1_follow_distance: spec.eth1_follow_distance, + subnets_per_node: spec.subnets_per_node, inactivity_score_bias: spec.inactivity_score_bias, inactivity_score_recovery_rate: spec.inactivity_score_recovery_rate, @@ -1130,6 +1122,7 @@ impl Config { min_validator_withdrawability_delay, shard_committee_period, eth1_follow_distance, + subnets_per_node, inactivity_score_bias, inactivity_score_recovery_rate, ejection_balance, @@ -1162,6 +1155,7 @@ impl Config { min_validator_withdrawability_delay, shard_committee_period, eth1_follow_distance, + subnets_per_node, inactivity_score_bias, inactivity_score_recovery_rate, ejection_balance, diff --git a/consensus/types/src/config_and_preset.rs b/consensus/types/src/config_and_preset.rs index b10ad7557b..01f86d3480 100644 --- a/consensus/types/src/config_and_preset.rs +++ b/consensus/types/src/config_and_preset.rs @@ -86,10 +86,6 @@ pub fn get_extra_fields(spec: &ChainSpec) -> HashMap { "domain_application_mask".to_uppercase()=> u32_hex(spec.domain_application_mask), "target_aggregators_per_committee".to_uppercase() => spec.target_aggregators_per_committee.to_string().into(), - "random_subnets_per_validator".to_uppercase() => - spec.random_subnets_per_validator.to_string().into(), - "epochs_per_random_subnet_subscription".to_uppercase() => - spec.epochs_per_random_subnet_subscription.to_string().into(), "domain_contribution_and_proof".to_uppercase() => u32_hex(spec.domain_contribution_and_proof), "domain_sync_committee".to_uppercase() => u32_hex(spec.domain_sync_committee), diff --git a/consensus/types/src/subnet_id.rs b/consensus/types/src/subnet_id.rs index b885f89f7d..6793fe5574 100644 --- a/consensus/types/src/subnet_id.rs +++ b/consensus/types/src/subnet_id.rs @@ -80,15 +80,26 @@ impl SubnetId { epoch: Epoch, spec: &ChainSpec, ) -> Result<(impl Iterator, Epoch), &'static str> { + // Simplify the variable name + let subscription_duration = spec.epochs_per_subnet_subscription; + let node_id_prefix = (node_id >> (256 - spec.attestation_subnet_prefix_bits() as usize)).as_usize(); - let subscription_event_idx = epoch.as_u64() / spec.epochs_per_subnet_subscription; + // NOTE: The as_u64() panics if the number is larger than u64::max_value(). This cannot be + // true as spec.epochs_per_subnet_subscription is a u64. + let node_offset = (node_id % ethereum_types::U256::from(subscription_duration)).as_u64(); + + // Calculate at which epoch this node needs to re-evaluate + let valid_until_epoch = epoch.as_u64() + + subscription_duration + .saturating_sub((epoch.as_u64() + node_offset) % subscription_duration); + + let subscription_event_idx = (epoch.as_u64() + node_offset) / subscription_duration; let permutation_seed = ethereum_hashing::hash(&int_to_bytes::int_to_bytes8(subscription_event_idx)); let num_subnets = 1 << spec.attestation_subnet_prefix_bits(); - let permutated_prefix = compute_shuffled_index( node_id_prefix, num_subnets, @@ -107,7 +118,6 @@ impl SubnetId { let subnet_set_generator = (0..subnets_per_node).map(move |idx| { SubnetId::new((permutated_prefix + idx as u64) % attestation_subnet_count) }); - let valid_until_epoch = (subscription_event_idx + 1) * spec.epochs_per_subnet_subscription; Ok((subnet_set_generator, valid_until_epoch.into())) } } @@ -149,3 +159,80 @@ impl AsRef for SubnetId { subnet_id_to_string(self.0) } } + +#[cfg(test)] +mod tests { + use super::*; + + /// A set of tests compared to the python specification + #[test] + fn compute_subnets_for_epoch_unit_test() { + // Randomized variables used generated with the python specification + let node_ids = [ + "0", + "88752428858350697756262172400162263450541348766581994718383409852729519486397", + "18732750322395381632951253735273868184515463718109267674920115648614659369468", + "27726842142488109545414954493849224833670205008410190955613662332153332462900", + "39755236029158558527862903296867805548949739810920318269566095185775868999998", + "31899136003441886988955119620035330314647133604576220223892254902004850516297", + "58579998103852084482416614330746509727562027284701078483890722833654510444626", + "28248042035542126088870192155378394518950310811868093527036637864276176517397", + "60930578857433095740782970114409273483106482059893286066493409689627770333527", + "103822458477361691467064888613019442068586830412598673713899771287914656699997", + ] + .into_iter() + .map(|v| ethereum_types::U256::from_dec_str(v).unwrap()) + .collect::>(); + + let epochs = [ + 54321u64, 1017090249, 1827566880, 846255942, 766597383, 1204990115, 1616209495, + 1774367616, 1484598751, 3525502229, + ] + .into_iter() + .map(Epoch::from) + .collect::>(); + + // Test mainnet + let spec = ChainSpec::mainnet(); + + // Calculated by hand + let expected_valid_time: Vec = [ + 54528, 1017090371, 1827567108, 846256076, 766597570, 1204990135, 1616209582, + 1774367723, 1484598953, 3525502371, + ] + .into(); + + // Calculated from pyspec + let expected_subnets = vec![ + vec![4u64, 5u64], + vec![61, 62], + vec![23, 24], + vec![38, 39], + vec![53, 54], + vec![39, 40], + vec![48, 49], + vec![39, 40], + vec![34, 35], + vec![37, 38], + ]; + + for x in 0..node_ids.len() { + println!("Test: {}", x); + println!( + "NodeId: {}\n Epoch: {}\n, expected_update_time: {}\n, expected_subnets: {:?}", + node_ids[x], epochs[x], expected_valid_time[x], expected_subnets[x] + ); + + let (computed_subnets, valid_time) = SubnetId::compute_subnets_for_epoch::< + crate::MainnetEthSpec, + >(node_ids[x], epochs[x], &spec) + .unwrap(); + + assert_eq!(Epoch::from(expected_valid_time[x]), valid_time); + assert_eq!( + expected_subnets[x], + computed_subnets.map(SubnetId::into).collect::>() + ); + } + } +} From f5369329351d17658595f4fd50003b85ae056aa3 Mon Sep 17 00:00:00 2001 From: chonghe Date: Tue, 30 May 2023 06:15:57 +0000 Subject: [PATCH 24/63] Add SSH tunneling in Lighthouse UI Siren (#4328) ## Issue Addressed - ## Proposed Changes Add information to use SSH to connect to Siren beyond the local computer ## Additional Info Please provide any additional information. For example, future considerations or information useful for reviewers. Co-authored-by: chonghe <44791194+chong-he@users.noreply.github.com> Co-authored-by: Michael Sproul --- book/src/ui-configuration.md | 19 +++++++++++++++---- book/src/ui-faqs.md | 6 +++--- book/src/ui-usage.md | 2 +- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/book/src/ui-configuration.md b/book/src/ui-configuration.md index 5b67b03b37..98f3041391 100644 --- a/book/src/ui-configuration.md +++ b/book/src/ui-configuration.md @@ -12,9 +12,20 @@ following configuration screen. This allows you to enter the address and ports of the associated Lighthouse Beacon node and Lighthouse Validator client. -> The Beacon Node must be run with the `--gui` flag set. To allow the browser -> to access the node beyond your local computer you also need to allow CORS in -> the http API. This can be done via `--http-allow-origin "*"`. +> The Beacon Node must be run with the `--gui` flag set. + +If you run Siren in the browser (by entering `localhost` in the browser), you will need to allow CORS in the HTTP API. This can be done by adding the flag `--http-allow-origin "*"` for both beacon node and validator client. If you would like to access Siren beyond the local computer, we recommend using an SSH tunnel. This requires a tunnel for 3 ports: `80` (assuming the port is unchanged as per the [installation guide](./ui-installation.md#docker-recommended), `5052` (for beacon node) and `5062` (for validator client). You can use the command below to perform SSH tunneling: +```bash +ssh -N -L 80:127.0.0.1:80 -L 5052:127.0.0.1:5052 -L 5062:127.0.0.1:5062 username@local_ip +``` + +where `username` is the username of the server and `local_ip` is the local IP address of the server. Note that with the `-N` option in an SSH session, you will not be able to execute commands in the CLI to avoid confusion with ordinary shell sessions. The connection will appear to be "hung" upon a successful connection, but that is normal. Once you have successfully connected to the server via SSH tunneling, you should be able to access Siren by entering `localhost` in a web browser. + +You can also access Siren using the app downloaded in the [Siren release page](https://github.com/sigp/siren/releases). To access Siren beyond the local computer, you can use SSH tunneling for ports `5052` and `5062` using the command: + +```bash +ssh -N -L 5052:127.0.0.1:5052 -L 5062:127.0.0.1:5062 username@local_ip +``` A green tick will appear once Siren is able to connect to both clients. You can specify different ports for each client by clicking on the advanced tab. @@ -33,7 +44,7 @@ The token is located in the default data directory of the validator client. The default path is `~/.lighthouse//validators/api-token.txt`. -The contents of this file for the desired valdiator client needs to be +The contents of this file for the desired validator client needs to be entered. ## Name diff --git a/book/src/ui-faqs.md b/book/src/ui-faqs.md index 51aa9385a4..d2e60d42da 100644 --- a/book/src/ui-faqs.md +++ b/book/src/ui-faqs.md @@ -4,13 +4,13 @@ Yes, Siren requires Lighthouse v3.5.1 or higher to function properly. These releases can be found on the [releases](https://github.com/sigp/lighthouse/releases) page of the Lighthouse repository. ## 2. Where can I find my API token? -The required Api token may be found in the default data directory of the validator client. For more information please refer to the lighthouse ui configuration [`api token section`](./ui-configuration.md#api-token). +The required Api token may be found in the default data directory of the validator client. For more information please refer to the lighthouse ui configuration [`api token section`](./api-vc-auth-header.md). ## 3. How do I fix the Node Network Errors? -If you recieve a red notification with a BEACON or VALIDATOR NODE NETWORK ERROR you can refer to the lighthouse ui configuration and [`connecting to clients section`](./ui-configuration.md#connecting-to-the-clients). +If you receive a red notification with a BEACON or VALIDATOR NODE NETWORK ERROR you can refer to the lighthouse ui configuration and [`connecting to clients section`](./ui-configuration.md#connecting-to-the-clients). ## 4. How do I change my Beacon or Validator address after logging in? -Once you have successfully arrived to the main dashboard, use the sidebar to access the settings view. In the top right hand corner there is a `Configurtion` action button that will redirect you back to the configuration screen where you can make appropriate changes. +Once you have successfully arrived to the main dashboard, use the sidebar to access the settings view. In the top right hand corner there is a `Configuration` action button that will redirect you back to the configuration screen where you can make appropriate changes. ## 5. Why doesn't my validator balance graph show any data? If your graph is not showing data, it usually means your validator node is still caching data. The application must wait at least 3 epochs before it can render any graphical visualizations. This could take up to 20min. diff --git a/book/src/ui-usage.md b/book/src/ui-usage.md index e88c4677a8..010f5d491b 100644 --- a/book/src/ui-usage.md +++ b/book/src/ui-usage.md @@ -10,7 +10,7 @@ Siren's dashboard view provides a summary of all performance and key validator m The account earnings component accumulates reward data from all registered validators providing a summation of total rewards earned while staking. Given current conversion rates, this component also converts your balance into your selected fiat currency. -Below in the earning section, you can also view your total earnings or click the adjacent buttons to view your estimated earnings given a specific timeframe based on current device and network conditions. +Below in the earning section, you can also view your total earnings or click the adjacent buttons to view your estimated earnings given a specific time frame based on current device and network conditions. ![](imgs/ui-account-earnings.png) From e6deaad91e83cce70b5b57a91428acc88d91df95 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Tue, 30 May 2023 06:15:58 +0000 Subject: [PATCH 25/63] Remove unused crate publishing Github action and script (#4347) ## Issue Addressed `publish-crate` action and its script no longer used after https://github.com/sigp/lighthouse/pull/3890. --- .github/workflows/publish-crate.yml | 66 ----------------- scripts/ci/publish.sh | 109 ---------------------------- 2 files changed, 175 deletions(-) delete mode 100644 .github/workflows/publish-crate.yml delete mode 100755 scripts/ci/publish.sh diff --git a/.github/workflows/publish-crate.yml b/.github/workflows/publish-crate.yml deleted file mode 100644 index 736057f785..0000000000 --- a/.github/workflows/publish-crate.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Publish Crate - -on: - push: - tags: - - tree-hash-v* - - tree-hash-derive-v* - - eth2-ssz-v* - - eth2-ssz-derive-v* - - eth2-ssz-types-v* - - eth2-serde-util-v* - - eth2-hashing-v* - -env: - CARGO_API_TOKEN: ${{ secrets.CARGO_API_TOKEN }} - -jobs: - extract-tag: - runs-on: ubuntu-latest - steps: - - name: Extract tag - run: echo "TAG=$(echo ${GITHUB_REF#refs/tags/})" >> $GITHUB_OUTPUT - id: extract_tag - outputs: - TAG: ${{ steps.extract_tag.outputs.TAG }} - - publish-crate: - runs-on: ubuntu-latest - needs: [extract-tag] - env: - TAG: ${{ needs.extract-tag.outputs.TAG }} - steps: - - uses: actions/checkout@v3 - - name: Update Rust - run: rustup update stable - - name: Cargo login - run: | - echo "${CARGO_API_TOKEN}" | cargo login - - name: publish eth2 ssz derive - if: startsWith(env.TAG, 'eth2-ssz-derive-v') - run: | - ./scripts/ci/publish.sh consensus/ssz_derive eth2_ssz_derive "$TAG" - - name: publish eth2 ssz - if: startsWith(env.TAG, 'eth2-ssz-v') - run: | - ./scripts/ci/publish.sh consensus/ssz eth2_ssz "$TAG" - - name: publish eth2 hashing - if: startsWith(env.TAG, 'eth2-hashing-v') - run: | - ./scripts/ci/publish.sh crypto/eth2_hashing eth2_hashing "$TAG" - - name: publish tree hash derive - if: startsWith(env.TAG, 'tree-hash-derive-v') - run: | - ./scripts/ci/publish.sh consensus/tree_hash_derive tree_hash_derive "$TAG" - - name: publish tree hash - if: startsWith(env.TAG, 'tree-hash-v') - run: | - ./scripts/ci/publish.sh consensus/tree_hash tree_hash "$TAG" - - name: publish ssz types - if: startsWith(env.TAG, 'eth2-ssz-types-v') - run: | - ./scripts/ci/publish.sh consensus/ssz_types eth2_ssz_types "$TAG" - - name: publish serde util - if: startsWith(env.TAG, 'eth2-serde-util-v') - run: | - ./scripts/ci/publish.sh consensus/serde_utils eth2_serde_utils "$TAG" diff --git a/scripts/ci/publish.sh b/scripts/ci/publish.sh deleted file mode 100755 index f2cea95b7d..0000000000 --- a/scripts/ci/publish.sh +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env bash - -# Based on: https://github.com/tokio-rs/tokio/blob/master/bin/publish - -set -e -USAGE="Publish a new release of a lighthouse crate -USAGE: - $(basename "$0") [OPTIONS] [CRATE_PATH] [CRATE] [TAG_NAME] -OPTIONS: - -v, --verbose Use verbose Cargo output - -d, --dry-run Perform a dry run (do not publish the release) - -h, --help Show this help text and exit - --allow-dirty Allow dirty working directories to be packaged" - -DRY_RUN="" -DIRTY="" -VERBOSE="" - -verify() { - echo "Verifying if $CRATE v$VERSION can be released" - - # `cargo pkgid` has different formats based on whether the `[lib]` name and `[package]` name - # are the same, necessitating the following logic. - # - # Try to match on `#` - ACTUAL=$(cargo pkgid | sed -n 's/.*#\([0-9]\)/\1/p' ) - if [ -z "$ACTUAL" ]; then - # Match on the final `:` - ACTUAL=$(cargo pkgid | sed -n 's/.*:\(.*\)/\1/p') - fi - - if [ "$ACTUAL" != "$VERSION" ]; then - echo "expected to release version $VERSION, but Cargo.toml contained $ACTUAL" - exit 1 - fi -} - -release() { - echo "Releasing $CRATE v$VERSION" - cargo package $VERBOSE $DIRTY - cargo publish $VERBOSE $DRY_RUN $DIRTY -} - -while [[ $# -gt 0 ]] -do - -case "$1" in - -h|--help) - echo "$USAGE" - exit 0 - ;; - -v|--verbose) - VERBOSE="--verbose" - set +x - shift - ;; - --allow-dirty) - DIRTY="--allow-dirty" - shift - ;; - -d|--dry-run) - DRY_RUN="--dry-run" - shift - ;; - -*) - echo "unknown flag \"$1\"" - echo "$USAGE" - exit 1 - ;; - *) # crate, crate path, or version - if [ -z "$CRATE_PATH" ]; then - CRATE_PATH="$1" - elif [ -z "$CRATE" ]; then - CRATE="$1" - elif [ -z "$TAG_NAME" ]; then - TAG_NAME="$1" - VERSION=$(sed -e 's#.*-v\([0-9]\)#\1#' <<< "$TAG_NAME") - else - echo "unknown positional argument \"$1\"" - echo "$USAGE" - exit 1 - fi - shift - ;; -esac -done -# set -- "${POSITIONAL[@]}" - -if [ -z "$VERSION" ]; then - echo "no version specified!" - HELP=1 -fi - -if [ -z "$CRATE" ]; then - echo "no crate specified!" - HELP=1 -fi - -if [ -n "$HELP" ]; then - echo "$USAGE" - exit 1 -fi - -if [ -d "$CRATE_PATH" ]; then - (cd "$CRATE_PATH" && verify && release ) -else - echo "no such dir \"$CRATE_PATH\"" - exit 1 -fi From 7b1a0d4bba4d6ad8c85273745e273fd8cdf74cc6 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 31 May 2023 07:16:20 +0000 Subject: [PATCH 26/63] Fix `libpq` typo in beacon.watch README (#4356) ## Issue Addressed Fix README typo (`libpg` -> `libpq`). Co-authored-by: Jimmy Chen --- watch/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watch/README.md b/watch/README.md index 18bf393946..34519e52e5 100644 --- a/watch/README.md +++ b/watch/README.md @@ -12,7 +12,7 @@ data which is: ### Requirements - `git` - `rust` : https://rustup.rs/ -- `libpg` : https://www.postgresql.org/download/ +- `libpq` : https://www.postgresql.org/download/ - `diesel_cli` : ``` cargo install diesel_cli --no-default-features --features postgres From 749a242b0f983963068aff8c1fa5529f9f601dea Mon Sep 17 00:00:00 2001 From: chonghe Date: Fri, 2 Jun 2023 03:17:35 +0000 Subject: [PATCH 27/63] Addition to Lighthouse Book faq.md (#4273) ## Issue Addressed Added some frequently asked questions in the Lighthouse Book ## Proposed Changes Created another file faqV2.md which categorises the FAQs into different sections for better organisation. For review purpose, a review on faq.md will suffice. Then, if faqV2.md looks better, can delete faq.md; otherwise if the changes to the faqV2.md is too much, can keep faq.md and delete faqv2.md. ## Additional Info Please provide any additional information. For example, future considerations or information useful for reviewers. Co-authored-by: chonghe <44791194+chong-he@users.noreply.github.com> --- book/src/faq.md | 535 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 394 insertions(+), 141 deletions(-) diff --git a/book/src/faq.md b/book/src/faq.md index b42e197a00..404ae26671 100644 --- a/book/src/faq.md +++ b/book/src/faq.md @@ -1,29 +1,184 @@ # Frequently Asked Questions -- [Why does it take so long for a validator to be activated?](#why-does-it-take-so-long-for-a-validator-to-be-activated) -- [Do I need to set up any port mappings?](#do-i-need-to-set-up-any-port-mappings) -- [I have a low peer count and it is not increasing](#i-have-a-low-peer-count-and-it-is-not-increasing) -- [What should I do if I lose my slashing protection database?](#what-should-i-do-if-i-lose-my-slashing-protection-database) -- [How do I update lighthouse?](#how-do-i-update-lighthouse) -- [I can't compile lighthouse](#i-cant-compile-lighthouse) -- [What is "Syncing deposit contract block cache"?](#what-is-syncing-deposit-contract-block-cache) -- [Can I use redundancy in my staking setup?](#can-i-use-redundancy-in-my-staking-setup) -- [How can I monitor my validators?](#how-can-i-monitor-my-validators) -- [I see beacon logs showing `WARN: Execution engine called failed`, what should I do?](#i-see-beacon-logs-showing-warn-execution-engine-called-failed-what-should-i-do) -- [How do I check or update my withdrawal credentials?](#how-do-i-check-or-update-my-withdrawal-credentials) -- [I am missing attestations. Why?](#i-am-missing-attestations-why) -- [Sometimes I miss the attestation head vote, resulting in penalty. Is this normal?](#sometimes-i-miss-the-attestation-head-vote-resulting-in-penalty-is-this-normal) -- [My beacon node is stuck at downloading historical block using checkpoing sync. What can I do?](#my-beacon-node-is-stuck-at-downloading-historical-block-using-checkpoing-sync-what-can-i-do) +## [Beacon Node](#beacon-node-1) +- [I see a warning about "Syncing deposit contract block cache" or an error about "updating deposit contract cache", what should I do?](#bn-deposit-contract) +- [I see beacon logs showing `WARN: Execution engine called failed`, what should I do?](#bn-ee) +- [My beacon node is stuck at downloading historical block using checkpoint sync. What should I do?](#bn-download-historical) +- [I proposed a block but the beacon node shows `could not publish message` with error `duplicate` as below, should I be worried?](#bn-duplicate) +- [I see beacon node logs `Head is optimistic` and I am missing attestations. What should I do?](#bn-optimistic) +- [My beacon node logs `CRIT Beacon block processing error error: ValidatorPubkeyCacheLockTimeout`, what should I do?](#bn-timeout) +- [My beacon node logs `WARN BlockProcessingFailure outcome: MissingBeaconBlock`, what should I do?](#bn-missing-beacon) +- [After checkpoint sync, the progress of `downloading historical blocks` is slow. Why?](#bn-download-slow) +- [My beacon node logs `WARN Error processing HTTP API request`, what should I do?](#bn-http) -### Why does it take so long for a validator to be activated? +## [Validator](#validator-1) +- [Why does it take so long for a validator to be activated?](#vc-activation) +- [Can I use redundancy in my staking setup?](#vc-redundancy) +- [I am missing attestations. Why?](#vc-missed-attestations) +- [Sometimes I miss the attestation head vote, resulting in penalty. Is this normal?](#vc-head-vote) +- [Can I submit a voluntary exit message without a beacon node?](#vc-exit) +- [Does increasing the number of validators increase the CPU and other computer resources used?](#vc-resource) +- [I want to add new validators. Do I have to reimport the existing keys?](#vc-reimport) +- [Do I have to stop `lighthouse vc` the when importing new validator keys?](#vc-import) + + +## [Network, Monitoring and Maintenance](#network-monitoring-and-maintenance-1) +- [I have a low peer count and it is not increasing](#net-peer) +- [How do I update lighthouse?](#net-update) +- [Do I need to set up any port mappings (port forwarding)?](#net-port) +- [How can I monitor my validators?](#net-monitor) +- [My beacon node and validator client are on different servers. How can I point the validator client to the beacon node?](#net-bn-vc) +- [Should I do anything to the beacon node or validator client settings if I have a relocation of the node / change of IP address?](#net-ip) + + +## [Miscellaneous](#miscellaneous-1) +- [What should I do if I lose my slashing protection database?](#misc-slashing) +- [I can't compile lighthouse](#misc-compile) +- [How do I check the version of Lighthouse that is running?](#misc-version) +- [Does Lighthouse have pruning function like the execution client to save disk space?](#misc-prune) +- [Can I use a HDD for the freezer database and only have the hot db on SSD?](#misc-freezer) + +## Beacon Node + + + +### I see a warning about "Syncing deposit contract block cache" or an error about "updating deposit contract cache", what should I do? + +The error can be a warning: + +``` +Nov 30 21:04:28.268 WARN Syncing deposit contract block cache est_blocks_remaining: initializing deposits, service: slot_notifier +``` + +or an error: + +``` +ERRO Error updating deposit contract cache error: Failed to get remote head and new block ranges: EndpointError(FarBehind), retry_millis: 60000, service: deposit_contract_rpc +``` + +This log indicates that your beacon node is downloading blocks and deposits +from your execution node. When the `est_blocks_remaining` is +`initializing_deposits`, your node is downloading deposit logs. It may stay in +this stage for several minutes. Once the deposits logs are finished +downloading, the `est_blocks_remaining` value will start decreasing. + +It is perfectly normal to see this log when starting a node for the first time +or after being off for more than several minutes. + +If this log continues appearing during operation, it means your execution client is still syncing and it cannot provide Lighthouse the information about the deposit contract yet. What you need to do is to make sure that the execution client is up and syncing. Once the execution client is synced, the error will disappear. + +### I see beacon logs showing `WARN: Execution engine called failed`, what should I do? + +The `WARN Execution engine called failed` log is shown when the beacon node cannot reach the execution engine. When this warning occurs, it will be followed by a detailed message. A frequently encountered example of the error message is: + +`error: Reqwest(reqwest::Error { kind: Request, url: Url { scheme: "http", cannot_be_a_base: false, username: "", password: None, host: Some(Ipv4(127.0.0.1)), port: Some(8551), path: "/", query: None, fragment: None }, source: TimedOut }), service: exec` + +which says `TimedOut` at the end of the message. This means that the execution engine has not responded in time to the beacon node. One option is to add the flag `--execution-timeout-multiplier 3` to the beacon node. However, if the error persists, it is worth digging further to find out the cause. There are a few reasons why this can occur: +1. The execution engine is not synced. Check the log of the execution engine to make sure that it is synced. If it is syncing, wait until it is synced and the error will disappear. You will see the beacon node logs `INFO Execution engine online` when it is synced. +1. The computer is overloaded. Check the CPU and RAM usage to see if it has overloaded. You can use `htop` to check for CPU and RAM usage. +1. Your SSD is slow. Check if your SSD is in "The Bad" list [here](https://gist.github.com/yorickdowne/f3a3e79a573bf35767cd002cc977b038). If your SSD is in "The Bad" list, it means it cannot keep in sync to the network and you may want to consider upgrading to a better SSD. + +If the reason for the error message is caused by no. 1 above, you may want to look further. If the execution engine is out of sync suddenly, it is usually caused by ungraceful shutdown. The common causes for ungraceful shutdown are: +- Power outage. If power outages are an issue at your place, consider getting a UPS to avoid ungraceful shutdown of services. +- The service file is not stopped properly. To overcome this, make sure that the process is stopped properly, e.g., during client updates. +- Out of memory (oom) error. This can happen when the system memory usage has reached its maximum and causes the execution engine to be killed. When this occurs, the log file will show `Main process exited, code=killed, status=9/KILL`. You can also run `sudo journalctl -a --since "18 hours ago" | grep -i "killed process` to confirm that the execution client has been killed due to oom. If you are using geth as the execution client, a short term solution is to reduce the resources used. For example, you can reduce the cache by adding the flag `--cache 2048`. If the oom occurs rather frequently, a long term solution is to increase the memory capacity of the computer. + +### My beacon node is stuck at downloading historical block using checkpoint sync. What should I do? + +After checkpoint forwards sync completes, the beacon node will start to download historical blocks. The log will look like: + +```bash +INFO Downloading historical blocks est_time: --, distance: 4524545 slots (89 weeks 5 days), service: slot_notifier +``` + +If the same log appears every minute and you do not see progress in downloading historical blocks, you can try one of the followings: + + - Check the number of peers you are connected to. If you have low peers (less than 50), try to do port forwarding on the port 9000 TCP/UDP to increase peer count. + - Restart the beacon node. + + +### I proposed a block but the beacon node shows `could not publish message` with error `duplicate` as below, should I be worried? + +``` +INFO Block from HTTP API already known` +WARN Could not publish message error: Duplicate, service: libp2p +``` + +This error usually happens when users are running mev-boost. The relay will publish the block on the network before returning it back to you. After the relay published the block on the network, it will propagate through nodes, and it happens quite often that your node will receive the block from your connected peers via gossip first, before getting the block from the relay, hence the message `duplicate`. + +In short, it is nothing to worry about. + +### I see beacon node logs `Head is optimistic`, and I am missing attestations. What should I do? + +The log looks like: + +``` +WARN Head is optimistic execution_block_hash: 0x47e7555f1d4215d1ad409b1ac188b008fcb286ed8f38d3a5e8078a0af6cbd6e1, info: chain not fully verified, block and attestation production disabled until execution engine syncs, service: slot_notifier +``` + +It means the beacon node will follow the chain, but it will not be able to attest or produce blocks. This is because the execution client is not synced, so the beacon chain cannot verify the authenticity of the chain head, hence the word `optimistic`. What you need to do is to make sure that the execution client is up and syncing. Once the execution client is synced, the error will disappear. + +### My beacon node logs `CRIT Beacon block processing error error: ValidatorPubkeyCacheLockTimeout, service: beacon`, what should I do? + +An example of the log is shown below: + +``` +CRIT Beacon block processing error error: ValidatorPubkeyCacheLockTimeout, service: beacon +WARN BlockProcessingFailure outcome: ValidatorPubkeyCacheLockTimeout, msg: unexpected condition in processing block. +``` + +A `Timeout` error suggests that the computer may be overloaded at the moment, for example, the execution client is still syncing. You may use the flag `--disable-lock-timeouts` to silence this error, although it will not fix the underlying slowness. Nevertheless, this is a relatively harmless log, and the error should go away once the resources used are back to normal. + +### My beacon node logs `WARN BlockProcessingFailure outcome: MissingBeaconBlock`, what should I do? + +An example of the full log is shown below: + +``` +WARN BlockProcessingFailure outcome: MissingBeaconBlock(0xbdba211f8d72029554e405d8e4906690dca807d1d7b1bc8c9b88d7970f1648bc), msg: unexpected condition in processing block. +``` + +`MissingBeaconBlock` suggests that the database has corrupted. You should wipe the database and use [Checkpoint Sync](./checkpoint-sync.md) to resync the beacon chain. + +### After checkpoint sync, the progress of `downloading historical blocks` is slow. Why? + +This is a normal behaviour. Since [v4.1.0](https://github.com/sigp/lighthouse/releases/tag/v4.1.0), Lighthouse implements rate-limited backfill sync to mitigate validator performance issues after a checkpoint sync. This is not something to worry about since backfill sync / historical data is not required for staking. However, if you opt to sync the chain as fast as possible, you can add the flag `--disable-backfill-rate-limiting` to the beacon node. + +### My beacon node logs `WARN Error processing HTTP API request`, what should I do? + +This warning usually comes with an http error code. Some examples are given below: + +1. The log shows: + +``` +WARN Error processing HTTP API request method: GET, path: /eth/v1/validator/attestation_data, status: 500 Internal Server Error, elapsed: 305.65µs +``` + +The error is `500 Internal Server Error`. This suggests that the execution client is not synced. Once the execution client is synced, the error will disappear. + +2. The log shows: + +``` +WARN Error processing HTTP API request method: POST, path: /eth/v1/validator/duties/attester/199565, status: 503 Service Unavailable, elapsed: 96.787µs +``` + +The error is `503 Service Unavailable`. This means that the beacon node is still syncing. When this happens, the validator client will log: + +``` +ERRO Failed to download attester duties err: FailedToDownloadAttesters("Some endpoints failed, num_failed: 2 http://localhost:5052/ => Unavailable(NotSynced), http://localhost:5052/ => RequestFailed(ServerMessage(ErrorMessage { code: 503, message: \"SERVICE_UNAVAILABLE: beacon node is syncing +``` + +This means that the validator client is sending requests to the beacon node. However, as the beacon node is still syncing, it is therefore unable to fulfil the request. The error will disappear once the beacon node is synced. + +## Validator + +### Why does it take so long for a validator to be activated? After validators create their execution layer deposit transaction there are two waiting periods before they can start producing blocks and attestations: 1. Waiting for the beacon chain to recognise the execution layer block containing the - deposit (generally 4 to 7.4 hours). -1. Waiting in the queue for validator activation (generally 6.4 minutes for - every 4 validators in the queue). + deposit (generally takes ~13.6 hours). +1. Waiting in the queue for validator activation. Detailed answers below: @@ -32,33 +187,33 @@ Detailed answers below: Since the beacon chain uses the execution layer for validator on-boarding, beacon chain validators must listen to event logs from the deposit contract. Since the latest blocks of the execution chain are vulnerable to re-orgs due to minor network -partitions, beacon nodes follow the execution chain at a distance of 1,024 blocks -(~4 hours) (see -[`ETH1_FOLLOW_DISTANCE`](https://github.com/ethereum/consensus-specs/blob/v0.12.1/specs/phase0/validator.md#misc)). +partitions, beacon nodes follow the execution chain at a distance of 2048 blocks +(~6.8 hours) (see +[`ETH1_FOLLOW_DISTANCE`](https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/validator.md#process-deposit)). This follow distance protects the beacon chain from on-boarding validators that are likely to be removed due to an execution chain re-org. -Now we know there's a 4 hours delay before the beacon nodes even _consider_ an +Now we know there's a 6.8 hours delay before the beacon nodes even _consider_ an execution layer block. Once they _are_ considering these blocks, there's a voting period where beacon validators vote on which execution block hash to include in the beacon chain. This -period is defined as 32 epochs (~3.4 hours, see -[`ETH1_VOTING_PERIOD`](https://github.com/ethereum/consensus-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#time-parameters)). +period is defined as 64 epochs (~6.8 hours, see +[`ETH1_VOTING_PERIOD`](https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/beacon-chain.md#time-parameters)). During this voting period, each beacon block producer includes an -[`Eth1Data`](https://github.com/ethereum/consensus-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#eth1data) +[`Eth1Data`](https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/beacon-chain.md#eth1data) in their block which counts as a vote towards what that validator considers to be the head of the execution chain at the start of the voting period (with respect to `ETH1_FOLLOW_DISTANCE`, of course). You can see the exact voting logic -[here](https://github.com/ethereum/consensus-specs/blob/v0.12.1/specs/phase0/validator.md#eth1-data). +[here](https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/validator.md#eth1-data). These two delays combined represent the time between an execution layer deposit being included in an execution data vote and that validator appearing in the beacon chain. -The `ETH1_FOLLOW_DISTANCE` delay causes a minimum delay of ~4 hours and +The `ETH1_FOLLOW_DISTANCE` delay causes a minimum delay of ~6.8 hours and `ETH1_VOTING_PERIOD` means that if a validator deposit happens just _before_ the start of a new voting period then they might not notice this delay at all. However, if the validator deposit happens just _after_ the start of the new -voting period the validator might have to wait ~3.4 hours for next voting -period. In times of very, very severe network issues, the network may even fail -to vote in new execution layer blocks, stopping all new validator deposits! +voting period the validator might have to wait ~6.8 hours for next voting +period. In times of very severe network issues, the network may even fail +to vote in new execution layer blocks, thus stopping all new validator deposits and causing the wait to be longer. #### 2. Waiting for a validator to be activated @@ -68,30 +223,144 @@ They will simply be forgotten by the beacon chain! But, if those parameters were correct, once the execution layer delays have elapsed and the validator appears in the beacon chain, there's _another_ delay before the validator becomes "active" (canonical definition -[here](https://github.com/ethereum/consensus-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#is_active_validator)) and can start producing blocks and attestations. +[here](https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/beacon-chain.md#is_active_validator)) and can start producing blocks and attestations. Firstly, the validator won't become active until their beacon chain balance is equal to or greater than -[`MAX_EFFECTIVE_BALANCE`](https://github.com/ethereum/consensus-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#gwei-values) +[`MAX_EFFECTIVE_BALANCE`](https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/beacon-chain.md#gwei-values) (32 ETH on mainnet, usually 3.2 ETH on testnets). Once this balance is reached, the validator must wait until the start of the next epoch (up to 6.4 minutes) for the -[`process_registry_updates`](https://github.com/ethereum/consensus-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#registry-updates) +[`process_registry_updates`](https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/beacon-chain.md#registry-updates) routine to run. This routine activates validators with respect to a [churn -limit](https://github.com/ethereum/consensus-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#get_validator_churn_limit); +limit](https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/beacon-chain.md#get_validator_churn_limit); it will only allow the number of validators to increase (churn) by a certain -amount. Up until there are about 330,000 validators this churn limit is set to -4 and it starts to very slowly increase as the number of validators increases -from there. - -If a new validator isn't within the churn limit from the front of the queue, +amount. If a new validator isn't within the churn limit from the front of the queue, they will need to wait another epoch (6.4 minutes) for their next chance. This -repeats until the queue is cleared. +repeats until the queue is cleared. The churn limit is summarised in the table below: -Once a validator has been activated, there's no more waiting! It's time to +
+ +| Number of active validators | Validators activated per epoch | Validators activated per day | +|-------------------|--------------------------------------------|----| +| 327679 or less | 4 | 900 | +| 327680-393215 | 5 | 1125 | +| 393216-458751 | 6 | 1350 +| 458752-524287 | 7 | 1575 +| 524288-589823 | 8| 1800 | +| 589824-655359 | 9| 2025 | +| 655360-720895 | 10 | 2250| +| 720896-786431 | 11 | 2475 | +| 786432-851967 | 12 | 2700 | +| 851968-917503 | 13 | 2925 | +| 917504-983039 | 14 | 3150 | +| 983040-1048575 | 15 | 3375 | + +
+ +For example, the number of active validators on Mainnet is about 574000 on May 2023. This means that 8 validators can be activated per epoch or 1800 per day (it is noted that the same applies to the exit queue). If, for example, there are 9000 validators waiting to be activated, this means that the waiting time can take up to 5 days. + +Once a validator has been activated, congratulations! It's time to produce blocks and attestations! -### Do I need to set up any port mappings? +### Can I use redundancy in my staking setup? + +You should **never** use duplicate/redundant validator keypairs or validator clients (i.e., don't +duplicate your JSON keystores and don't run `lighthouse vc` twice). This will lead to slashing. + +However, there are some components which can be configured with redundancy. See the +[Redundancy](./redundancy.md) guide for more information. + +### I am missing attestations. Why? +The first thing is to ensure both consensus and execution clients are synced with the network. If they are synced, there may still be some issues with the node setup itself that is causing the missed attestations. Check the setup to ensure that: +- the clock is synced +- the computer has sufficient resources and is not overloaded +- the internet is working well +- you have sufficient peers + +You can see more information on the [Ethstaker KB](https://ethstaker.gitbook.io/ethstaker-knowledge-base/help/missed-attestations). Once the above points are good, missing attestation should be a rare occurrence. + +### Sometimes I miss the attestation head vote, resulting in penalty. Is this normal? + +In general, it is unavoidable to have some penalties occasionally. This is particularly the case when you are assigned to attest on the first slot of an epoch and if the proposer of that slot releases the block late, then you will get penalised for missing the target and head votes. Your attestation performance does not only depend on your own setup, but also on everyone elses performance. + +### Can I submit a voluntary exit message without running a beacon node? + +Yes. Beaconcha.in provides the tool to broadcast the message. You can create the voluntary exit message file with [ethdo](https://github.com/wealdtech/ethdo/releases/tag/v1.30.0) and submit the message via the [beaconcha.in](https://beaconcha.in/tools/broadcast) website. A guide on how to use `ethdo` to perform voluntary exit can be found [here](https://github.com/eth-educators/ethstaker-guides/blob/main/voluntary-exit.md). + +It is also noted that you can submit your BLS-to-execution-change message to update your withdrawal credentials from type `0x00` to `0x01` using the same link. + +If you would like to still use Lighthouse to submit the message, you will need to run a beacon node and an execution client. For the beacon node, you can use checkpoint sync to quickly sync the chain under a minute. On the other hand, the execution client can be syncing and *needs not be synced*. This implies that it is possible to broadcast a voluntary exit message within a short time by quickly spinning up a node. + +### Does increasing the number of validators increase the CPU and other computer resources used? + +A computer with hardware specifications stated in the [Recommended System Requirements](./installation.md#recommended-system-requirements) can run hundreds validators with only marginal increase in cpu usage. When validators are active, there is a bit of an increase in resources used from validators 0-64, because you end up subscribed to more subnets. After that, the increase in resources plateaus when the number of validators go from 64 to ~500. + +### I want to add new validators. Do I have to reimport the existing keys? + +No. You can just import new validator keys to the destination directory. If the `validator_keys` folder contains existing keys, that's fine as well because Lighthouse will skip importing existing keys. + +### Do I have to stop `lighthouse vc` when importing new validator keys? + +Generally yes. + +If you do not want to stop `lighthouse vc`, you can use the [key manager API](./api-vc-endpoints.md) to import keys. + +## Network, Monitoring and Maintenance + +### I have a low peer count and it is not increasing + +If you cannot find *ANY* peers at all, it is likely that you have incorrect +network configuration settings. Ensure that the network you wish to connect to +is correct (the beacon node outputs the network it is connecting to in the +initial boot-up log lines). On top of this, ensure that you are not using the +same `datadir` as a previous network, i.e., if you have been running the +`Goerli` testnet and are now trying to join a new network but using the same +`datadir` (the `datadir` is also printed out in the beacon node's logs on +boot-up). + +If you find yourself with a low peer count and it's not reaching the target you +expect, there are a few things to check on: + +1. Ensure that port forward was correctly set up as described [here](./advanced_networking.md#nat-traversal-port-forwarding). + +To check that the ports are forwarded, run the command: + + ```bash + curl http://localhost:5052/lighthouse/nat + ``` + +It should return `{"data":true}`. If it returns `{"data":false}`, you may want to double check if the port forward was correctly set up. + +If the ports are open, you should have incoming peers. To check that you have incoming peers, run the command: + + ```bash + curl localhost:5052/lighthouse/peers | jq '.[] | select(.peer_info.connection_direction=="Incoming")' + ``` + +If you have incoming peers, it should return a lot of data containing information of peers. If the response is empty, it means that you have no incoming peers and there the ports are not open. You may want to double check if the port forward was correctly set up. + +2. Check that you do not lower the number of peers using the flag `--target-peers`. The default is 80. A lower value set will lower the maximum number of peers your node can connect to, which may potentially interrupt the validator performance. We recommend users to leave the `--target peers` untouched to keep a diverse set of peers. + +3. Ensure that you have a quality router for the internet connection. For example, if you connect the router to many devices including the node, it may be possible that the router cannot handle all routing tasks, hence struggling to keep up the number of peers. Therefore, using a quality router for the node is important to keep a healthy number of peers. + + +### How do I update lighthouse? + +If you are updating to new release binaries, it will be the same process as described [here.](./installation-binaries.md) + +If you are updating by rebuilding from source, see [here.](./installation-source.md#update-lighthouse) + +If you are running the docker image provided by Sigma Prime on Dockerhub, you can update to specific versions, for example: + +```bash +$ docker pull sigp/lighthouse:v1.0.0 +``` + +If you are building a docker image, the process will be similar to the one described [here.](./docker.md#building-the-docker-image) +You just need to make sure the code you have checked out is up to date. + +### Do I need to set up any port mappings (port forwarding)? It is not strictly required to open any ports for Lighthouse to connect and participate in the network. Lighthouse should work out-of-the-box. However, if @@ -116,121 +385,105 @@ peers to join and degrades the overall connectivity of the global network. For these reasons, we recommend that you make your node publicly accessible. Lighthouse supports UPnP. If you are behind a NAT with a router that supports -UPnP you can simply ensure UPnP is enabled (Lighthouse will inform you in its -initial logs if a route has been established). You can also manually set up -port mappings in your router to your local Lighthouse instance. By default, +UPnP, you can simply ensure UPnP is enabled (Lighthouse will inform you in its +initial logs if a route has been established). You can also manually [set up port mappings](./advanced_networking.md) in your router to your local Lighthouse instance. By default, Lighthouse uses port 9000 for both TCP and UDP. Opening both these ports will make your Lighthouse node maximally contactable. -### I have a low peer count and it is not increasing - -If you cannot find *ANY* peers at all. It is likely that you have incorrect -testnet configuration settings. Ensure that the network you wish to connect to -is correct (the beacon node outputs the network it is connecting to in the -initial boot-up log lines). On top of this, ensure that you are not using the -same `datadir` as a previous network. I.e if you have been running the -`prater` testnet and are now trying to join a new testnet but using the same -`datadir` (the `datadir` is also printed out in the beacon node's logs on -boot-up). - -If you find yourself with a low peer count and it's not reaching the target you -expect. Try setting up the correct port forwards as described -[here](./advanced_networking.md#nat-traversal-port-forwarding). - -### What should I do if I lose my slashing protection database? - -See [here](./slashing-protection.md#misplaced-slashing-database). - -### How do I update lighthouse? - -If you are updating to new release binaries, it will be the same process as described [here.](./installation-binaries.md) - -If you are updating by rebuilding from source, see [here.](./installation-source.md#update-lighthouse) - -If you are running the docker image provided by Sigma Prime on Dockerhub, you can update to specific versions, for example: - -```bash -$ docker pull sigp/lighthouse:v1.0.0 -``` - -If you are building a docker image, the process will be similar to the one described [here.](./docker.md#building-the-docker-image) -You will just also need to make sure the code you have checked out is up to date. - -### I can't compile lighthouse - -See [here.](./installation-source.md#troubleshooting) - -### What is "Syncing deposit contract block cache"? - -``` -Nov 30 21:04:28.268 WARN Syncing deposit contract block cache est_blocks_remaining: initializing deposits, service: slot_notifier -``` - -This log indicates that your beacon node is downloading blocks and deposits -from your execution node. When the `est_blocks_remaining` is -`initializing_deposits`, your node is downloading deposit logs. It may stay in -this stage for several minutes. Once the deposits logs are finished -downloading, the `est_blocks_remaining` value will start decreasing. - -It is perfectly normal to see this log when starting a node for the first time -or after being off for more than several minutes. - -If this log continues appearing sporadically during operation, there may be an -issue with your execution client endpoint. - -### Can I use redundancy in my staking setup? - -You should **never** use duplicate/redundant validator keypairs or validator clients (i.e., don't -duplicate your JSON keystores and don't run `lighthouse vc` twice). This will lead to slashing. - -However, there are some components which can be configured with redundancy. See the -[Redundancy](./redundancy.md) guide for more information. - -### How can I monitor my validators? +### How can I monitor my validators? Apart from using block explorers, you may use the "Validator Monitor" built into Lighthouse which provides logging and Prometheus/Grafana metrics for individual validators. See [Validator Monitoring](./validator-monitoring.md) for more information. Lighthouse has also developed Lighthouse UI (Siren) to monitor performance, see [Lighthouse UI (Siren)](./lighthouse-ui.md). -### I see beacon logs showing `WARN: Execution engine called failed`, what should I do? +### My beacon node and validator client are on different servers. How can I point the validator client to the beacon node? + +The settings are as follows: + +1. On the beacon node: + + Specify `lighthouse bn --http-address local_IP` so that the beacon node is listening on the local network rather than on the `localhost`. + +1. On the validator client: + + Use the flag `--beacon-nodes` to point to the beacon node. For example, `lighthouse vc --beacon-nodes http://local_IP:5052` where `local_IP` is the local IP address of the beacon node and `5052` is the default `http-port` of the beacon node. + + You can test that the setup is working with by running the following command on the validator client host: + + ```bash + curl "http://local_IP:5052/eth/v1/node/version" + ``` + + You can refer to [Redundancy](./redundancy.md) for more information. + + It is also worth noting that the `--beacon-nodes` flag can also be used for redundancy of beacon nodes. For example, let's say you have a beacon node and a validator client running on the same host, and a second beacon node on another server as a backup. In this case, you can use `lighthouse vc --beacon-nodes http://localhost:5052, http://local_IP:5052` on the validator client. + +### Should I do anything to the beacon node or validator client settings if I have a relocation of the node / change of IP address? +No. Lighthouse will auto-detect the change and update your Ethereum Node Record (ENR). You just need to make sure you are not manually setting the ENR with `--enr-address` (which, for common use cases, this flag is not used). + +## Miscellaneous + +### What should I do if I lose my slashing protection database? + +See [here](./slashing-protection.md#misplaced-slashing-database). + +### I can't compile lighthouse + +See [here.](./installation-source.md#troubleshooting) + +### How do I check the version of Lighthouse that is running? + +If you build Lighthouse from source, run `lighthouse --version`. Example of output: + +```bash +Lighthouse v4.1.0-693886b +BLS library: blst-modern +SHA256 hardware acceleration: false +Allocator: jemalloc +Specs: mainnet (true), minimal (false), gnosis (true) +``` + +If you download the binary file, navigate to the location of the directory, for example, the binary file is in `/usr/local/bin`, run `/usr/local/bin/lighthouse --version`, the example of output is the same as above. + +Alternatively, if you have Lighthouse running, on the same computer, you can run: +```bash +curl "http://127.0.0.1:5052/eth/v1/node/version" +``` + +Example of output: +```bash +{"data":{"version":"Lighthouse/v4.1.0-693886b/x86_64-linux"}} +``` +which says that the version is v4.1.0. + +### Does Lighthouse have pruning function like the execution client to save disk space? + +There is no pruning of Lighthouse database for now. However, since v4.2.0, a feature to only sync back to the weak subjectivity point (approximately 5 months) when syncing via a checkpoint sync was added. This will help to save disk space since the previous behaviour will sync back to the genesis by default. + +### Can I use a HDD for the freezer database and only have the hot db on SSD? + +Yes, you can do so by using the flag `--freezer-dir /path/to/freezer_db` in the beacon node. + + + + + + + + + + + + + -The `WARN Execution engine called failed` log is shown when the beacon node cannot reach the execution engine. When this warning occurs, it will be followed by a detailed message. A frequently encountered example of the error message is: -`error: Reqwest(reqwest::Error { kind: Request, url: Url { scheme: "http", cannot_be_a_base: false, username: "", password: None, host: Some(Ipv4(127.0.0.1)), port: Some(8551), path: "/", query: None, fragment: None }, source: TimedOut }), service: exec` -which says `TimedOut` at the end of the message. This means that the execution engine has not responded in time to the beacon node. There are a few reasons why this can occur: -1. The execution engine is not synced. Check the log of the execution engine to make sure that it is synced. If it is syncing, wait until it is synced and the error will disappear. You will see the beacon node logs `INFO Execution engine online` when it is synced. -1. The computer is overloaded. Check the CPU and RAM usage to see if it has overloaded. You can use `htop` to check for CPU and RAM usage. -1. Your SSD is slow. Check if your SSD is in "The Bad" list [here](https://gist.github.com/yorickdowne/f3a3e79a573bf35767cd002cc977b038). If your SSD is in "The Bad" list, it means it cannot keep in sync to the network and you may want to consider upgrading to a better SSD. -If the reason for the error message is caused by no. 1 above, you may want to look further. If the execution engine is out of sync suddenly, it is usually caused by ungraceful shutdown. The common causes for ungraceful shutdown are: -- Power outage. If power outages are an issue at your place, consider getting a UPS to avoid ungraceful shutdown of services. -- The service file is not stopped properly. To overcome this, make sure that the process is stop properly, e.g., during client updates. -- Out of memory (oom) error. This can happen when the system memory usage has reached its maximum and causes the execution engine to be killed. When this occurs, the log file will show `Main process exited, code=killed, status=9/KILL`. You can also run `sudo journalctl -a --since "18 hours ago" | grep -i "killed process` to confirm that the execution client has been killed due to oom. If you are using geth as the execution client, a short term solution is to reduce the resources used, for example: (1) reduce the cache by adding the flag `--cache 2048` (2) connect to less peers using the flag `--maxpeers 10`. If the oom occurs rather frequently, a long term solution is to increase the memory capacity of the computer. -### How do I check or update my withdrawal credentials? -Withdrawals will be available after the Capella/Shanghai upgrades on 12th April 2023. To check that if you are eligible for withdrawals, go to [Staking launchpad](https://launchpad.ethereum.org/en/withdrawals), enter your validator index and click `verify on mainnet`: -- `withdrawals enabled` means you will automatically receive withdrawals to the withdrawal address that you set. -- `withdrawals not enabled` means you will need to update your withdrawal credentials from `0x00` type to `0x01` type. The common way to do this is using `Staking deposit CLI` or `ethdo`, with the instructions available [here](https://launchpad.ethereum.org/en/withdrawals#update-your-keys). - -For the case of `withdrawals not enabled`, you can update your withdrawal credentials **anytime**, and there is no deadline for that. The catch is that as long as you do not update your withdrawal credentials, your rewards in the beacon chain will continue to be locked in the beacon chain. Only after you update the withdrawal credentials, will the rewards be withdrawn to the withdrawal address. -### I am missing attestations. Why? -The first thing is to ensure both consensus and execution clients are synced with the network. If they are synced, there may still be some issues with the node setup itself that is causing the missed attestations. Check the setup to ensure that: -- the clock is synced -- the computer has sufficient resources and is not overloaded -- the internet is working well -- you have sufficient peers - -You can see more information on the [Ethstaker KB](https://ethstaker.gitbook.io/ethstaker-knowledge-base/help/missed-attestations). Once the above points are good, missing attestation should be a rare occurance. - -### Sometimes I miss the attestation head vote, resulting in penalty. Is this normal? - -In general it is unavoiadable to have some penalties occasionally. This is particularly the case when you are assigned to attest on the first slot of an epoch and if the proposer of that slot releases the block late, then you will get penalised for missing the target and head votes. Your attestation performance does not only depend on your own setup, but also on everyone else's performance. -### My beacon node is stuck at downloading historical block using checkpoing sync. What can I do? -Check the number of peers you are connected to. If you have low peers (less than 50), try to do port forwarding on the port 9000 TCP/UDP to increase peer count. \ No newline at end of file From 6c769ed86c4512e72f27ac60a62c0805e2c2dab4 Mon Sep 17 00:00:00 2001 From: chonghe Date: Fri, 2 Jun 2023 03:17:36 +0000 Subject: [PATCH 28/63] Update Lighthouse Book API and Advanced Usage section (#4300) ## Issue Addressed Update Information in Lighthouse Book ## Proposed Changes - move Validator Graffiti from Advanced Usage to Validator Management - update API response and command - some items that aren't too sure I put it in comment, which can be seen in raw/review format but not live ## Additional Info Please provide any additional information. For example, future considerations or information useful for reviewers. Co-authored-by: chonghe <44791194+chong-he@users.noreply.github.com> --- book/src/SUMMARY.md | 9 +- book/src/advanced-pre-releases.md | 4 - book/src/advanced-release-candidates.md | 7 +- book/src/advanced.md | 16 +- book/src/advanced_database.md | 26 +- book/src/advanced_networking.md | 26 +- book/src/api-bn.md | 56 ++-- book/src/api-lighthouse.md | 290 +++++++++++------ book/src/api-vc-endpoints.md | 400 ++++++++++++++++++------ book/src/api-vc.md | 6 +- book/src/builders.md | 37 ++- book/src/checkpoint-sync.md | 6 +- book/src/database-migrations.md | 25 +- book/src/key-management.md | 2 +- book/src/merge-migration.md | 35 ++- book/src/redundancy.md | 12 +- book/src/slasher.md | 9 +- book/src/suggested-fee-recipient.md | 10 + book/src/validator-inclusion.md | 4 +- book/src/validator-web3signer.md | 4 +- 20 files changed, 706 insertions(+), 278 deletions(-) delete mode 100644 book/src/advanced-pre-releases.md diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index bfd5a02a6f..8fc2c2f836 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -18,10 +18,11 @@ * [Validator Monitoring](./validator-monitoring.md) * [Doppelganger Protection](./validator-doppelganger.md) * [Suggested Fee Recipient](./suggested-fee-recipient.md) + * [Validator Graffiti](./graffiti.md) * [APIs](./api.md) * [Beacon Node API](./api-bn.md) - * [/lighthouse](./api-lighthouse.md) - * [Validator Inclusion APIs](./validator-inclusion.md) + * [Lighthouse API](./api-lighthouse.md) + * [Validator Inclusion APIs](./validator-inclusion.md) * [Validator Client API](./api-vc.md) * [Endpoints](./api-vc-endpoints.md) * [Authorization Header](./api-vc-auth-header.md) @@ -36,7 +37,6 @@ * [Advanced Usage](./advanced.md) * [Checkpoint Sync](./checkpoint-sync.md) * [Custom Data Directories](./advanced-datadir.md) - * [Validator Graffiti](./graffiti.md) * [Proposer Only Beacon Nodes](./advanced-proposer-only.md) * [Remote Signing with Web3Signer](./validator-web3signer.md) * [Database Configuration](./advanced_database.md) @@ -46,9 +46,8 @@ * [Advanced Networking](./advanced_networking.md) * [Running a Slasher](./slasher.md) * [Redundancy](./redundancy.md) - * [Pre-Releases](./advanced-pre-releases.md) * [Release Candidates](./advanced-release-candidates.md) - * [MEV and Lighthouse](./builders.md) + * [Maximal Extractable Value (MEV)](./builders.md) * [Merge Migration](./merge-migration.md) * [Late Block Re-orgs](./late-block-re-orgs.md) * [Contributing](./contributing.md) diff --git a/book/src/advanced-pre-releases.md b/book/src/advanced-pre-releases.md deleted file mode 100644 index f3f4a52304..0000000000 --- a/book/src/advanced-pre-releases.md +++ /dev/null @@ -1,4 +0,0 @@ -# Pre-Releases - -Pre-releases are now referred to as [Release Candidates](./advanced-release-candidates.md). The terms may -be used interchangeably. diff --git a/book/src/advanced-release-candidates.md b/book/src/advanced-release-candidates.md index 842bc48404..b2ff021365 100644 --- a/book/src/advanced-release-candidates.md +++ b/book/src/advanced-release-candidates.md @@ -7,7 +7,7 @@ [`v1.4.0`]: https://github.com/sigp/lighthouse/releases/tag/v1.4.0 From time-to-time, Lighthouse *release candidates* will be published on the [sigp/lighthouse] -repository. These releases have passed the usual automated testing, however the developers would +repository. Release candidates are previously known as Pre-Releases. These releases have passed the usual automated testing, however the developers would like to see it running "in the wild" in a variety of configurations before declaring it an official, stable release. Release candidates are also used by developers to get feedback from users regarding the ergonomics of new features or changes. @@ -36,8 +36,9 @@ Users may wish to try a release candidate for the following reasons: - To help detect bugs and regressions before they reach production. - To provide feedback on annoyances before they make it into a release and become harder to change or revert. +There can also be a scenario that a bug has been found and requires an urgent fix. An example of incidence is [v4.0.2-rc.0](https://github.com/sigp/lighthouse/releases/tag/v4.0.2-rc.0) which contains a hot-fix to address high CPU usage experienced after the [Capella](https://ethereum.org/en/history/#capella) upgrade on 12th April 2023. In this scenario, we will announce the release candidate on [Github](https://github.com/sigp/lighthouse/releases) and also on [Discord](https://discord.gg/cyAszAh) to recommend users to update to the release candidate version. + ## When *not* to use a release candidate -It is not recommended to use release candidates for any critical tasks on mainnet (e.g., staking). -To test critical features, try one of the testnets (e.g., Prater). +Other than the above scenarios, it is generally not recommended to use release candidates for any critical tasks on mainnet (e.g., staking). To test new release candidate features, try one of the testnets (e.g., Goerli). diff --git a/book/src/advanced.md b/book/src/advanced.md index d46cae6990..51416a3b73 100644 --- a/book/src/advanced.md +++ b/book/src/advanced.md @@ -6,4 +6,18 @@ elsewhere? This section provides detailed information about configuring Lighthouse for specific use cases, and tips about how things work under the hood. -* [Advanced Database Configuration](./advanced_database.md): understanding space-time trade-offs in the database. +* [Checkpoint Sync](./checkpoint-sync.md): quickly sync the beacon chain to perform validator duties. +* [Custom Data Directories](./advanced-datadir.md): modify the data directory to your preferred location. +* [Proposer Only Beacon Nodes](./advanced-proposer-only.md): beacon node only for proposer duty for increased anonymity. +* [Remote Signing with Web3Signer](./validator-web3signer.md): don't want to store your keystore in local node? Use web3signer. +* [Database Configuration](./advanced_database.md): understanding space-time trade-offs in the database. +* [Database Migrations](./database-migrations.md): have a look at all previous Lighthouse database scheme versions. +* [Key Management](./key-management.md): explore how to generate wallet with Lighthouse. +* [Key Recovery](./key-recovery.md): explore how to recover wallet and validator with Lighthouse. +* [Advanced Networking](./advanced_networking.md): open your ports to have a diverse and healthy set of peers. +* [Running a Slasher](./slasher.md): contribute to the health of the network by running a slasher. +* [Redundancy](./redundancy.md): want to have more than one beacon node as backup? This is for you. +* [Release Candidates](./advanced-release-candidates.md): latest release of Lighthouse to get feedback from users. +* [Maximal Extractable Value](./builders.md): use external builders for a potential higher rewards during block proposals +* [Merge Migration](./merge-migration.md): look at what you need to do during a significant network upgrade: The Merge +* [Late Block Re-orgs](./late-block-re-orgs.md): read information about Lighthouse late block re-orgs. diff --git a/book/src/advanced_database.md b/book/src/advanced_database.md index 57e49531ca..f9996ec65d 100644 --- a/book/src/advanced_database.md +++ b/book/src/advanced_database.md @@ -23,13 +23,17 @@ states to slow down dramatically. A lower _slots per restore point_ value (SPRP) frequent restore points, while a higher SPRP corresponds to less frequent. The table below shows some example values. -| Use Case | SPRP | Yearly Disk Usage | Load Historical State | -|--------------------------|------|-------------------|-----------------------| -| Block explorer/analysis | 32 | 1.4 TB | 155 ms | -| Hobbyist (prev. default) | 2048 | 23.1 GB | 10.2 s | -| Validator only (default) | 8192 | 5.7 GB | 41 s | +| Use Case | SPRP | Yearly Disk Usage* | Load Historical State | +|----------------------------|------|-------------------|-----------------------| +| Research | 32 | 3.4 TB | 155 ms | +| Block explorer/analysis | 128 | 851 GB | 620 ms | +| Enthusiast (prev. default) | 2048 | 53.6 GB | 10.2 s | +| EHobbyist | 4096 | 26.8 GB | 20.5 s | +| Validator only (default) | 8192 | 8.1 GB | 41 s | -As you can see, it's a high-stakes trade-off! The relationships to disk usage and historical state +*Last update: May 2023. + +As we can see, it's a high-stakes trade-off! The relationships to disk usage and historical state load time are both linear – doubling SPRP halves disk usage and doubles load time. The minimum SPRP is 32, and the maximum is 8192. @@ -38,9 +42,11 @@ The default value is 8192 for databases synced from scratch using Lighthouse v2. The values shown in the table are approximate, calculated using a simple heuristic: each `BeaconState` consumes around 18MB of disk space, and each block replayed takes around 5ms. The -**Yearly Disk Usage** column shows the approx size of the freezer DB _alone_ (hot DB not included), -and the **Load Historical State** time is the worst-case load time for a state in the last slot -before a restore point. +**Yearly Disk Usage** column shows the approximate size of the freezer DB _alone_ (hot DB not included), calculated proportionally using the total freezer database disk usage. +The **Load Historical State** time is the worst-case load time for a state in the last slot +before a restore point. + +As an example, we use an SPRP of 4096 to calculate the total size of the freezer database until May 2023. It has been about 900 days since the genesis, the total disk usage by the freezer database is therefore: 900/365*26.8 GB = 66 GB. ### Defaults @@ -68,6 +74,8 @@ The historical state cache size can be specified with the flag `--historic-state lighthouse beacon_node --historic-state-cache-size 4 ``` +> Note: This feature will cause high memory usage. + ## Glossary * _Freezer DB_: part of the database storing finalized states. States are stored in a sparser diff --git a/book/src/advanced_networking.md b/book/src/advanced_networking.md index 08d276ba35..59f3da376f 100644 --- a/book/src/advanced_networking.md +++ b/book/src/advanced_networking.md @@ -51,7 +51,7 @@ peers for your node and overall improve the Ethereum consensus network. Lighthouse currently supports UPnP. If UPnP is enabled on your router, Lighthouse will automatically establish the port mappings for you (the beacon node will inform you of established routes in this case). If UPnP is not -enabled, we recommend you manually set up port mappings to both of Lighthouse's +enabled, we recommend you to manually set up port mappings to both of Lighthouse's TCP and UDP ports (9000 by default). > Note: Lighthouse needs to advertise its publicly accessible ports in @@ -63,6 +63,28 @@ TCP and UDP ports (9000 by default). > explicitly specify them using the `--enr-tcp-port` and `--enr-udp-port` as > explained in the following section. +### How to Open Ports + +The steps to do port forwarding depends on the router, but the general steps are given below: +1. Determine the default gateway IP: +- On Linux: open a terminal and run `ip route | grep default`, the result should look something similar to `default via 192.168.50.1 dev wlp2s0 proto dhcp metric 600`. The `192.168.50.1` is your router management default gateway IP. +- On MacOS: open a terminal and run `netstat -nr|grep default` and it should return the default gateway IP. +- On Windows: open a command prompt and run `ipconfig` and look for the `Default Gateway` which will show you the gateway IP. + + The default gateway IP usually looks like 192.168.X.X. Once you obtain the IP, enter it to a web browser and it will lead you to the router management page. + +2. Login to the router management page. The login credentials are usually available in the manual or the router, or it can be found on a sticker underneath the router. You can also try the login credentials for some common router brands listed [here](https://www.noip.com/support/knowledgebase/general-port-forwarding-guide/). + +3. Navigate to the port forward settings in your router. The exact step depends on the router, but typically it will fall under the "Advanced" section, under the name "port forwarding" or "virtual server". + +4. Configure a port forwarding rule as below: +- Protocol: select `TCP/UDP` or `BOTH` +- External port: `9000` +- Internal port: `9000` +- IP address: Usually there is a dropdown list for you to select the device. Choose the device that is running Lighthouse + +5. To check that you have successfully open the ports, go to [yougetsignal](https://www.yougetsignal.com/tools/open-ports/) and enter `9000` in the `port number`. If it shows "open", then you have successfully set up port forwarding. If it shows "closed", double check your settings, and also check that you have allowed firewall rules on port 9000. + ### ENR Configuration @@ -81,7 +103,7 @@ and if it is, it will update your ENR to the correct public IP and port address (meaning you do not need to set it manually). Lighthouse persists its ENR, so on reboot it will re-load the settings it had discovered previously. -Modifying the ENR settings can degrade the discovery of your node making it +Modifying the ENR settings can degrade the discovery of your node, making it harder for peers to find you or potentially making it harder for other peers to find each other. We recommend not touching these settings unless for a more advanced use case. diff --git a/book/src/api-bn.md b/book/src/api-bn.md index b86e593bf1..11a006493a 100644 --- a/book/src/api-bn.md +++ b/book/src/api-bn.md @@ -5,7 +5,7 @@ specification][OpenAPI]. Please follow that link for a full description of each ## Starting the server -A Lighthouse beacon node can be configured to expose a HTTP server by supplying the `--http` flag. The default listen address is `127.0.0.1:5052`. +A Lighthouse beacon node can be configured to expose an HTTP server by supplying the `--http` flag. The default listen address is `http://127.0.0.1:5052`. The following CLI flags control the HTTP server: @@ -55,11 +55,8 @@ Additional risks to be aware of include: ## CLI Example -Start the beacon node with the HTTP server listening on [http://localhost:5052](http://localhost:5052): +Start a beacon node and an execution node according to [Run a node](./run_a_node.md). Note that since [The Merge](https://ethereum.org/en/roadmap/merge/), an execution client is required to be running along with a beacon node. Hence, the query on Beacon Node APIs requires users to run both. While there are some Beacon Node APIs that you can query with only the beacon node, such as the [node version](https://ethereum.github.io/beacon-APIs/#/Node/getNodeVersion), in general an execution client is required to get the updated information about the beacon chain, such as [state root](https://ethereum.github.io/beacon-APIs/#/Beacon/getStateRoot), [headers](https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockHeaders) and many others, which are dynamically progressing with time. -```bash -lighthouse bn --http -``` ## HTTP Request/Response Examples @@ -77,40 +74,46 @@ curl -X GET "http://localhost:5052/eth/v1/beacon/headers/head" -H "accept: appl ```json { + "execution_optimistic": false, + "finalized": false, "data": { - "root": "0x4381454174fc28c7095077e959dcab407ae5717b5dca447e74c340c1b743d7b2", + "root": "0x9059bbed6b8891e0ba2f656dbff93fc40f8c7b2b7af8fea9df83cfce5ee5e3d8", "canonical": true, "header": { "message": { - "slot": "3199", - "proposer_index": "19077", - "parent_root": "0xf1934973041c5896d0d608e52847c3cd9a5f809c59c64e76f6020e3d7cd0c7cd", - "state_root": "0xe8e468f9f5961655dde91968f66480868dab8d4147de9498111df2b7e4e6fe60", - "body_root": "0x6f183abc6c4e97f832900b00d4e08d4373bfdc819055d76b0f4ff850f559b883" + "slot": "6271829", + "proposer_index": "114398", + "parent_root": "0x1d2b4fa8247f754a7a86d36e1d0283a5e425491c431533716764880a7611d225", + "state_root": "0x2b48adea290712f56b517658dde2da5d36ee01c41aebe7af62b7873b366de245", + "body_root": "0x6fa74c995ce6f397fa293666cde054d6a9741f7ec280c640bee51220b4641e2d" }, - "signature": "0x988064a2f9cf13fe3aae051a3d85f6a4bca5a8ff6196f2f504e32f1203b549d5f86a39c6509f7113678880701b1881b50925a0417c1c88a750c8da7cd302dda5aabae4b941e3104d0cf19f5043c4f22a7d75d0d50dad5dbdaf6991381dc159ab" + "signature": "0x8258e64fea426033676a0045c50543978bf173114ba94822b12188e23cbc8d8e89e0b5c628a881bf3075d325bc11341105a4e3f9332ac031d89a93b422525b79e99325928a5262f17dfa6cc3ddf84ca2466fcad86a3c168af0d045f79ef52036" } } } ``` +The `jq` tool is used to format the JSON data properly. If it returns `jq: command not found`, then you can install `jq` with `sudo apt install -y jq`. After that, run the command again, and it should return the head state of the beacon chain. + ### View the status of a validator Shows the status of validator at index `1` at the `head` state. ```bash -curl -X GET "http://localhost:5052/eth/v1/beacon/states/head/validators/1" -H "accept: application/json" | jq +curl -X GET "http://localhost:5052/eth/v1/beacon/states/head/validators/1" -H "accept: application/json" ``` ```json { + "execution_optimistic": false, + "finalized": false, "data": { "index": "1", - "balance": "63985937939", - "status": "Active", + "balance": "32004587169", + "status": "active_ongoing", "validator": { - "pubkey": "0x873e73ee8b3e4fcf1d2fb0f1036ba996ac9910b5b348f6438b5f8ef50857d4da9075d0218a9d1b99a9eae235a39703e1", - "withdrawal_credentials": "0x00b8cdcf79ba7e74300a07e9d8f8121dd0d8dd11dcfd6d3f2807c45b426ac968", + "pubkey": "0xa1d1ad0714035353258038e964ae9675dc0252ee22cea896825c01458e1807bfad2f9969338798548d9858a571f7425c", + "withdrawal_credentials": "0x01000000000000000000000015f4b914a0ccd14333d850ff311d6dafbfbaa32b", "effective_balance": "32000000000", "slashed": false, "activation_eligibility_epoch": "0", @@ -121,6 +124,7 @@ curl -X GET "http://localhost:5052/eth/v1/beacon/states/head/validators/1" -H " } } ``` +You can replace `1` in the above command with the validator index that you would like to query. Other API query can be done similarly by changing the link according to the Beacon API. ## Serving the HTTP API over TLS > **Warning**: This feature is currently experimental. @@ -147,9 +151,18 @@ openssl req -x509 -nodes -newkey rsa:4096 -keyout key.pem -out cert.pem -days 36 Note that currently Lighthouse only accepts keys that are not password protected. This means we need to run with the `-nodes` flag (short for 'no DES'). -Once generated, we can run Lighthouse: +Once generated, we can run Lighthouse and an execution node according to [Run a node](./run_a_node.md). In addition, add the flags `--http-enable-tls --http-tls-cert cert.pem --http-tls-key key.pem` to Lighthouse, the command should look like: + ```bash -lighthouse bn --http --http-enable-tls --http-tls-cert cert.pem --http-tls-key key.pem +lighthouse bn \ + --network mainnet \ + --execution-endpoint http://localhost:8551 \ + --execution-jwt /secrets/jwt.hex \ + --checkpoint-sync-url https://mainnet.checkpoint.sigp.io \ + --http \ + --http-enable-tls \ + --http-tls-cert cert.pem \ + --http-tls-key key.pem ``` Note that the user running Lighthouse must have permission to read the certificate and key. @@ -159,6 +172,7 @@ The API is now being served at `https://localhost:5052`. To test connectivity, you can run the following: ```bash curl -X GET "https://localhost:5052/eth/v1/node/version" -H "accept: application/json" --cacert cert.pem | jq + ``` ### Connecting a validator client In order to connect a validator client to a beacon node over TLS, the validator @@ -201,13 +215,13 @@ Ensure the `--http` flag has been supplied at the CLI. You can quickly check that the HTTP endpoint is up using `curl`: ```bash -curl -X GET "http://localhost:5052/eth/v1/node/version" -H "accept: application/json" | jq +curl -X GET "http://localhost:5052/eth/v1/node/version" -H "accept:application/json" ``` The beacon node should respond with its version: ```json -{"data":{"version":"Lighthouse/v0.2.9-6f7b4768a/x86_64-linux"}} +{"data":{"version":"Lighthouse/v4.1.0-693886b/x86_64-linux"} ``` If this doesn't work, the server might not be started or there might be a diff --git a/book/src/api-lighthouse.md b/book/src/api-lighthouse.md index 47fe62f50b..7626d64013 100644 --- a/book/src/api-lighthouse.md +++ b/book/src/api-lighthouse.md @@ -1,8 +1,8 @@ # Lighthouse Non-Standard APIs Lighthouse fully supports the standardization efforts at -[github.com/ethereum/beacon-APIs](https://github.com/ethereum/beacon-APIs), -however sometimes development requires additional endpoints that shouldn't +[github.com/ethereum/beacon-APIs](https://github.com/ethereum/beacon-APIs). +However, sometimes development requires additional endpoints that shouldn't necessarily be defined as a broad-reaching standard. Such endpoints are placed behind the `/lighthouse` path. @@ -16,10 +16,12 @@ Although we don't recommend that users rely on these endpoints, we document them briefly so they can be utilized by developers and researchers. + + ### `/lighthouse/health` +*Note: This endpoint is presently only available on Linux.* -*Presently only available on Linux.* - +Returns information regarding the health of the host machine. ```bash curl -X GET "http://localhost:5052/lighthouse/health" -H "accept: application/json" | jq ``` @@ -63,7 +65,7 @@ curl -X GET "http://localhost:5052/lighthouse/health" -H "accept: application/j ``` ### `/lighthouse/ui/health` - +Returns information regarding the health of the host machine. ```bash curl -X GET "http://localhost:5052/lighthouse/ui/health" -H "accept: application/json" | jq @@ -83,24 +85,24 @@ curl -X GET "http://localhost:5052/lighthouse/ui/health" -H "accept: applicatio "global_cpu_frequency": 3.4, "disk_bytes_total": 502390845440, "disk_bytes_free": 9981386752, - "network_name": "wlp0s20f3", - "network_bytes_total_received": 14105556611, - "network_bytes_total_transmit": 3649489389, - "nat_open": true, - "connected_peers": 80, - "sync_state": "Synced", "system_uptime": 660706, "app_uptime": 105, "system_name": "Arch Linux", "kernel_version": "5.19.13-arch1-1", "os_version": "Linux rolling Arch Linux", "host_name": "Computer1" + "network_name": "wlp0s20f3", + "network_bytes_total_received": 14105556611, + "network_bytes_total_transmit": 3649489389, + "nat_open": true, + "connected_peers": 80, + "sync_state": "Synced", } } ``` ### `/lighthouse/ui/validator_count` - +Returns an overview of validators. ```bash curl -X GET "http://localhost:5052/lighthouse/ui/validator_count" -H "accept: application/json" | jq ``` @@ -121,9 +123,9 @@ curl -X GET "http://localhost:5052/lighthouse/ui/validator_count" -H "accept: ap } ``` + ### `/lighthouse/ui/validator_metrics` -Re-exposes certain metrics from the validator monitor to the HTTP API. -Will only return metrics for the validators currently being monitored and are present in the POST data. +Re-exposes certain metrics from the validator monitor to the HTTP API. This API requires that the beacon node to have the flag `--validator-monitor-auto`. This API will only return metrics for the validators currently being monitored and present in the POST data, or the validators running in the validator client. ```bash curl -X POST "http://localhost:5052/lighthouse/ui/validator_metrics" -d '{"indices": [12345]}' -H "Content-Type: application/json" | jq ``` @@ -148,24 +150,40 @@ curl -X POST "http://localhost:5052/lighthouse/ui/validator_metrics" -d '{"indic } } ``` +Running this API without the flag `--validator-monitor-auto` in the beacon node will return null: +```json +{ + "data": { + "validators": {} + } +} +``` ### `/lighthouse/syncing` - +Returns the sync status of the beacon node. ```bash curl -X GET "http://localhost:5052/lighthouse/syncing" -H "accept: application/json" | jq ``` -```json -{ - "data": { - "SyncingFinalized": { - "start_slot": 3104, - "head_slot": 343744, - "head_root": "0x1b434b5ed702338df53eb5e3e24336a90373bb51f74b83af42840be7421dd2bf" +There are two possible outcomes, depending on whether the beacon node is syncing or synced. + +1. Syncing: + ```json + { + "data": { + "SyncingFinalized": { + "start_slot": "5478848", + "target_slot": "5478944" + } + } } - } -} -``` + ``` +1. Synced: + ```json + { + "data": "Synced" + } + ``` ### `/lighthouse/peers` @@ -173,96 +191,137 @@ curl -X GET "http://localhost:5052/lighthouse/syncing" -H "accept: application/ curl -X GET "http://localhost:5052/lighthouse/peers" -H "accept: application/json" | jq ``` + ```json [ { - "peer_id": "16Uiu2HAmA9xa11dtNv2z5fFbgF9hER3yq35qYNTPvN7TdAmvjqqv", + "peer_id": "16Uiu2HAm2ZoWQ2zkzsMFvf5o7nXa7R5F7H1WzZn2w7biU3afhgov", "peer_info": { - "_status": "Healthy", "score": { - "score": 0 + "Real": { + "lighthouse_score": 0, + "gossipsub_score": -18371.409037358582, + "ignore_negative_gossipsub_score": false, + "score": -21.816048231863316 + } }, "client": { "kind": "Lighthouse", - "version": "v0.2.9-1c9a055c", - "os_version": "aarch64-linux", - "protocol_version": "lighthouse/libp2p", - "agent_string": "Lighthouse/v0.2.9-1c9a055c/aarch64-linux" + "version": "v4.1.0-693886b", + "os_version": "x86_64-linux", + "protocol_version": "eth2/1.0.0", + "agent_string": "Lighthouse/v4.1.0-693886b/x86_64-linux" }, "connection_status": { "status": "disconnected", "connections_in": 0, "connections_out": 0, - "last_seen": 1082, + "last_seen": 9028, "banned_ips": [] }, "listening_addresses": [ - "/ip4/80.109.35.174/tcp/9000", - "/ip4/127.0.0.1/tcp/9000", - "/ip4/192.168.0.73/tcp/9000", - "/ip4/172.17.0.1/tcp/9000", - "/ip6/::1/tcp/9000" + "/ip4/212.102.59.173/tcp/23452", + "/ip4/23.124.84.197/tcp/23452", + "/ip4/127.0.0.1/tcp/23452", + "/ip4/192.168.0.2/tcp/23452", + "/ip4/192.168.122.1/tcp/23452" + ], + "seen_addresses": [ + "23.124.84.197:23452" ], "sync_status": { - "Advanced": { + "Synced": { "info": { - "status_head_slot": 343829, - "status_head_root": "0xe34e43efc2bb462d9f364bc90e1f7f0094e74310fd172af698b5a94193498871", - "status_finalized_epoch": 10742, - "status_finalized_root": "0x1b434b5ed702338df53eb5e3e24336a90373bb51f74b83af42840be7421dd2bf" + "head_slot": "5468141", + "head_root": "0x7acc017a199c0cf0693a19e0ed3a445a02165c03ea6f46cb5ffb8f60bf0ebf35", + "finalized_epoch": "170877", + "finalized_root": "0xbbc3541637976bd03b526de73e60a064e452a4b873b65f43fa91fefbba140410" } } }, "meta_data": { - "seq_number": 160, - "attnets": "0x0000000800000080" - } + "V2": { + "seq_number": 501, + "attnets": "0x0000020000000000", + "syncnets": "0x00" + } + }, + "subnets": [], + "is_trusted": false, + "connection_direction": "Outgoing", + "enr": "enr:-L64QI37ReMIki2Uqln3pcgQyAH8Y3ceSYrtJp1FlDEGSM37F7ngCpS9k-SKQ1bOHp0zFCkNxpvFlf_3o5OUkBRw0qyCAfqHYXR0bmV0c4gAAAIAAAAAAIRldGgykGKJQe8DABAg__________-CaWSCdjSCaXCEF3xUxYlzZWNwMjU2azGhAmoW921eIvf8pJhOvOwuxLSxKnpLY2inE_bUILdlZvhdiHN5bmNuZXRzAIN0Y3CCW5yDdWRwgluc" } } ] ``` ### `/lighthouse/peers/connected` - +Returns information about connected peers. ```bash curl -X GET "http://localhost:5052/lighthouse/peers/connected" -H "accept: application/json" | jq ``` + + ```json [ - { - "peer_id": "16Uiu2HAkzJC5TqDSKuLgVUsV4dWat9Hr8EjNZUb6nzFb61mrfqBv", + { + "peer_id": "16Uiu2HAmCAvpoYE6ABGdQJaW4iufVqNCTJU5AqzyZPB2D9qba7ZU", "peer_info": { - "_status": "Healthy", "score": { - "score": 0 + "Real": { + "lighthouse_score": 0, + "gossipsub_score": 0, + "ignore_negative_gossipsub_score": false, + "score": 0 + } }, "client": { "kind": "Lighthouse", - "version": "v0.2.8-87181204+", + "version": "v3.5.1-319cc61", "os_version": "x86_64-linux", - "protocol_version": "lighthouse/libp2p", - "agent_string": "Lighthouse/v0.2.8-87181204+/x86_64-linux" + "protocol_version": "eth2/1.0.0", + "agent_string": "Lighthouse/v3.5.1-319cc61/x86_64-linux" }, "connection_status": { "status": "connected", - "connections_in": 1, - "connections_out": 0, - "last_seen": 0, - "banned_ips": [] + "connections_in": 0, + "connections_out": 1, + "last_seen": 0 }, "listening_addresses": [ - "/ip4/34.204.178.218/tcp/9000", + "/ip4/144.91.92.17/tcp/9000", "/ip4/127.0.0.1/tcp/9000", - "/ip4/172.31.67.58/tcp/9000", - "/ip4/172.17.0.1/tcp/9000", - "/ip6/::1/tcp/9000" + "/ip4/172.19.0.3/tcp/9000" ], - "sync_status": "Unknown", + "seen_addresses": [ + "144.91.92.17:9000" + ], + "sync_status": { + "Synced": { + "info": { + "head_slot": "5468930", + "head_root": "0x25409073c65d2f6f5cee20ac2eff5ab980b576ca7053111456063f8ff8f67474", + "finalized_epoch": "170902", + "finalized_root": "0xab59473289e2f708341d8e5aafd544dd88e09d56015c90550ea8d16c50b4436f" + } + } + }, "meta_data": { - "seq_number": 1819, - "attnets": "0xffffffffffffffff" - } + "V2": { + "seq_number": 67, + "attnets": "0x0000000080000000", + "syncnets": "0x00" + } + }, + "subnets": [ + { + "Attestation": "39" + } + ], + "is_trusted": false, + "connection_direction": "Outgoing", + "enr": "enr:-Ly4QHd3RHJdkuR1iE6MtVtibC5S-aiWGPbwi4cG3wFGbqxRAkAgLDseTzPFQQIehQ7LmO7KIAZ5R1fotjMQ_LjA8n1Dh2F0dG5ldHOIAAAAAAAQAACEZXRoMpBiiUHvAwAQIP__________gmlkgnY0gmlwhJBbXBGJc2VjcDI1NmsxoQL4z8A7B-NS29zOgvkTX1YafKandwOtrqQ1XRnUJj3se4hzeW5jbmV0cwCDdGNwgiMog3VkcIIjKA" } } ] @@ -297,7 +356,8 @@ health of the execution node that the beacon node is connected to. - `latest_cached_block_number` & `latest_cached_block_timestamp`: the block number and timestamp of the latest block we have in our block cache. - For correct execution client voting this timestamp should be later than the -`voting_period_start_timestamp`. +`voting_target_timestamp`. + - `voting_target_timestamp`: The latest timestamp allowed for an execution layer block in this voting period. - `eth1_node_sync_status_percentage` (float): An estimate of how far the head of the execution node is from the head of the execution chain. @@ -420,11 +480,11 @@ curl -X GET "http://localhost:5052/lighthouse/beacon/states/0/ssz" | jq ### `/lighthouse/liveness` POST request that checks if any of the given validators have attested in the given epoch. Returns a list -of objects, each including the validator index, epoch, and `is_live` status of a requested validator. +of objects, each including the validator index, epoch, and `is_live` status of a requested validator. -This endpoint is used in doppelganger detection, and will only provide accurate information for the -current, previous, or next epoch. +This endpoint is used in doppelganger detection, and can only provide accurate information for the current, previous, or next epoch. +> Note that for this API, if you insert an arbitrary epoch other than the previous, current or next epoch of the network, it will return `"code:400"` and `BAD_REQUEST`. ```bash curl -X POST "http://localhost:5052/lighthouse/liveness" -d '{"indices":["0","1"],"epoch":"1"}' -H "content-type: application/json" | jq @@ -442,6 +502,8 @@ curl -X POST "http://localhost:5052/lighthouse/liveness" -d '{"indices":["0","1" } ``` + + ### `/lighthouse/database/info` Information about the database's split point and anchor info. @@ -450,26 +512,29 @@ Information about the database's split point and anchor info. curl "http://localhost:5052/lighthouse/database/info" | jq ``` + ```json { - "schema_version": 5, + "schema_version": 16, "config": { - "slots_per_restore_point": 2048, + "slots_per_restore_point": 8192, + "slots_per_restore_point_set_explicitly": false, "block_cache_size": 5, "historic_state_cache_size": 1, "compact_on_init": false, - "compact_on_prune": true + "compact_on_prune": true, + "prune_payloads": true }, "split": { - "slot": "2034912", - "state_root": "0x11c8516aa7d4d1613e84121e3a557ceca34618b4c1a38f05b66ad045ff82b33b" + "slot": "5485952", + "state_root": "0xcfe5d41e6ab5a9dab0de00d89d97ae55ecaeed3b08e4acda836e69b2bef698b4" }, "anchor": { - "anchor_slot": "2034720", - "oldest_block_slot": "1958881", - "oldest_block_parent": "0x1fd3d855d03e9df28d8a41a0f9cb9d4c540832b3ca1c3e1d7e09cd75b874cc87", - "state_upper_limit": "2035712", - "state_lower_limit": "0" + "anchor_slot": "5414688", + "oldest_block_slot": "0", + "oldest_block_parent": "0x0000000000000000000000000000000000000000000000000000000000000000", + "state_upper_limit": "5414912", + "state_lower_limit": "8192" } } ``` @@ -504,12 +569,12 @@ Manually provide `SignedBeaconBlock`s to backfill the database. This is intended for use by Lighthouse developers during testing only. ### `/lighthouse/merge_readiness` - +Returns the current difficulty and terminal total difficulty of the network. Before [The Merge](https://ethereum.org/en/roadmap/merge/) on 15th September 2022, you will see that the current difficulty is less than the terminal total difficulty, An example is shown below: ```bash curl -X GET "http://localhost:5052/lighthouse/merge_readiness" | jq ``` -``` +```json { "data":{ "type":"ready", @@ -521,6 +586,21 @@ curl -X GET "http://localhost:5052/lighthouse/merge_readiness" | jq } ``` +As all testnets and Mainnet have been merged, both values will be the same after The Merge. An example of response on the Goerli testnet: + +```json +{ + "data": { + "type": "ready", + "config": { + "terminal_total_difficulty": "10790000" + }, + "current_difficulty": "10790000" + } +} +``` + + ### `/lighthouse/analysis/attestation_performance/{index}` Fetch information about the attestation performance of a validator index or all validators for a @@ -611,20 +691,35 @@ Two query parameters are required: Example: ```bash -curl -X GET "http://localhost:5052/lighthouse/analysis/block_rewards?start_slot=1&end_slot=32" | jq +curl -X GET "http://localhost:5052/lighthouse/analysis/block_rewards?start_slot=1&end_slot=1" | jq ``` + +The first few lines of the response would look like: + ```json [ { - "block_root": "0x51576c2fcf0ab68d7d93c65e6828e620efbb391730511ffa35584d6c30e51410", - "attestation_rewards": { - "total": 4941156, + "total": 637260, + "block_root": "0x4a089c5e390bb98e66b27358f157df825128ea953cee9d191229c0bcf423a4f6", + "meta": { + "slot": "1", + "parent_slot": "0", + "proposer_index": 93, + "graffiti": "EF #vm-eth2-raw-iron-prater-101" }, - .. - }, - .. -] + "attestation_rewards": { + "total": 637260, + "prev_epoch_total": 0, + "curr_epoch_total": 637260, + "per_attestation_rewards": [ + { + "50102": 780, + } + ] + } + } +] ``` Caveats: @@ -653,6 +748,8 @@ Two query parameters are required: curl -X GET "http://localhost:5052/lighthouse/analysis/block_packing_efficiency?start_epoch=1&end_epoch=1" | jq ``` +An excerpt of the response looks like: + ```json [ { @@ -707,3 +804,16 @@ Should provide an output that emits log events as they occur: } } ``` + +### `/lighthouse/nat` +Checks if the ports are open. + +```bash +curl -X GET "http://localhost:5052/lighthouse/nat" | jq +``` + +An open port will return: +```json +{ + "data": true +} \ No newline at end of file diff --git a/book/src/api-vc-endpoints.md b/book/src/api-vc-endpoints.md index d5d76e4ef4..406c5b1f0e 100644 --- a/book/src/api-vc-endpoints.md +++ b/book/src/api-vc-endpoints.md @@ -17,8 +17,11 @@ HTTP Path | Description | [`POST /lighthouse/validators/mnemonic`](#post-lighthousevalidatorsmnemonic) | Create a new validator from an existing mnemonic. [`POST /lighthouse/validators/web3signer`](#post-lighthousevalidatorsweb3signer) | Add web3signer validators. +The query to Lighthouse API endpoints requires authorization, see [Authorization Header](./api-vc-auth-header.md). + In addition to the above endpoints Lighthouse also supports all of the [standard keymanager APIs](https://ethereum.github.io/keymanager-APIs/). + ## `GET /lighthouse/version` Returns the software version and `git` commit hash for the Lighthouse binary. @@ -32,15 +35,28 @@ Returns the software version and `git` commit hash for the Lighthouse binary. | Required Headers | [`Authorization`](./api-vc-auth-header.md) | | Typical Responses | 200 | -### Example Response Body +Command: +```bash +DATADIR=/var/lib/lighthouse +curl -X GET "http://localhost:5062/lighthouse/version" -H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" | jq +``` + +Example Response Body: + ```json { "data": { - "version": "Lighthouse/v0.2.11-fc0654fbe+/x86_64-linux" + "version": "Lighthouse/v4.1.0-693886b/x86_64-linux" } } ``` +> Note: The command provided in this documentation links to the API token file. In this documentation, it is assumed that the API token file is located in `/var/lib/lighthouse/validators/API-token.txt`. If your database is saved in another directory, modify the `DATADIR` accordingly. If you are having permission issue with accessing the API token file, you can modify the header to become `-H "Authorization: Bearer $(sudo cat ${DATADIR}/validators/api-token.txt)"`. + +> As an alternative, you can also provide the API token directly, for example, `-H "Authorization: Bearer api-token-0x02dc2a13115cc8c83baf170f597f22b1eb2930542941ab902df3daadebcb8f8176`. In this case, you obtain the token from the file `API token.txt` and the command becomes: +```bash +curl -X GET "http://localhost:5062/lighthouse/version" -H "Authorization: Bearer api-token-0x02dc2a13115cc8c83baf170f597f22b1eb2930542941ab902df3daadebcb8f8176" | jq +``` ## `GET /lighthouse/health` @@ -57,24 +73,48 @@ Returns information regarding the health of the host machine. *Note: this endpoint is presently only available on Linux.* -### Example Response Body +Command: +```bash +DATADIR=/var/lib/lighthouse +curl -X GET "http://localhost:5062/lighthouse/health" -H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" | jq +``` + +Example Response Body: ```json { - "data": { - "pid": 1476293, - "pid_num_threads": 19, - "pid_mem_resident_set_size": 4009984, - "pid_mem_virtual_memory_size": 1306775552, - "sys_virt_mem_total": 33596100608, - "sys_virt_mem_available": 23073017856, - "sys_virt_mem_used": 9346957312, - "sys_virt_mem_free": 22410510336, - "sys_virt_mem_percent": 31.322334, - "sys_loadavg_1": 0.98, - "sys_loadavg_5": 0.98, - "sys_loadavg_15": 1.01 - } + "data": { + "sys_virt_mem_total": 8184274944, + "sys_virt_mem_available": 1532280832, + "sys_virt_mem_used": 6248341504, + "sys_virt_mem_free": 648790016, + "sys_virt_mem_percent": 81.27775, + "sys_virt_mem_cached": 1244770304, + "sys_virt_mem_buffers": 42373120, + "sys_loadavg_1": 2.33, + "sys_loadavg_5": 2.11, + "sys_loadavg_15": 2.47, + "cpu_cores": 4, + "cpu_threads": 8, + "system_seconds_total": 103095, + "user_seconds_total": 750734, + "iowait_seconds_total": 60671, + "idle_seconds_total": 3922305, + "cpu_time_total": 4794222, + "disk_node_bytes_total": 982820896768, + "disk_node_bytes_free": 521943703552, + "disk_node_reads_total": 376287830, + "disk_node_writes_total": 48232652, + "network_node_bytes_total_received": 143003442144, + "network_node_bytes_total_transmit": 185348289905, + "misc_node_boot_ts_seconds": 1681740973, + "misc_os": "linux", + "pid": 144072, + "pid_num_threads": 27, + "pid_mem_resident_set_size": 15835136, + "pid_mem_virtual_memory_size": 2179018752, + "pid_process_seconds_total": 54 + } } ``` @@ -91,7 +131,13 @@ Returns information regarding the health of the host machine. | Required Headers | [`Authorization`](./api-vc-auth-header.md) | | Typical Responses | 200 | -### Example Response Body +Command: +```bash +DATADIR=/var/lib/lighthouse +curl -X GET "http://localhost:5062/lighthouse/ui/health" -H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" | jq + ``` + +Example Response Body ```json { @@ -130,7 +176,12 @@ Returns the graffiti that will be used for the next block proposal of each valid | Required Headers | [`Authorization`](./api-vc-auth-header.md) | | Typical Responses | 200 | -### Example Response Body +Command: +```bash +DATADIR=/var/lib/lighthouse +curl -X GET "http://localhost:5062/lighthouse/ui/graffiti" -H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" | jq + ``` +Example Response Body ```json { @@ -155,71 +206,115 @@ Returns the Ethereum proof-of-stake consensus specification loaded for this vali | Required Headers | [`Authorization`](./api-vc-auth-header.md) | | Typical Responses | 200 | -### Example Response Body +Command: + +```bash +DATADIR=/var/lib/lighthouse +curl -X GET "http://localhost:5062/lighthouse/spec" -H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" | jq +``` + +Example Response Body ```json { - "data": { - "CONFIG_NAME": "mainnet", - "MAX_COMMITTEES_PER_SLOT": "64", - "TARGET_COMMITTEE_SIZE": "128", - "MIN_PER_EPOCH_CHURN_LIMIT": "4", - "CHURN_LIMIT_QUOTIENT": "65536", - "SHUFFLE_ROUND_COUNT": "90", - "MIN_GENESIS_ACTIVE_VALIDATOR_COUNT": "1024", - "MIN_GENESIS_TIME": "1601380800", - "GENESIS_DELAY": "172800", - "MIN_DEPOSIT_AMOUNT": "1000000000", - "MAX_EFFECTIVE_BALANCE": "32000000000", - "EJECTION_BALANCE": "16000000000", - "EFFECTIVE_BALANCE_INCREMENT": "1000000000", - "HYSTERESIS_QUOTIENT": "4", - "HYSTERESIS_DOWNWARD_MULTIPLIER": "1", - "HYSTERESIS_UPWARD_MULTIPLIER": "5", - "PROPORTIONAL_SLASHING_MULTIPLIER": "3", - "GENESIS_FORK_VERSION": "0x00000002", - "BLS_WITHDRAWAL_PREFIX": "0x00", - "SECONDS_PER_SLOT": "12", - "MIN_ATTESTATION_INCLUSION_DELAY": "1", - "MIN_SEED_LOOKAHEAD": "1", - "MAX_SEED_LOOKAHEAD": "4", - "MIN_EPOCHS_TO_INACTIVITY_PENALTY": "4", - "MIN_VALIDATOR_WITHDRAWABILITY_DELAY": "256", - "SHARD_COMMITTEE_PERIOD": "256", - "BASE_REWARD_FACTOR": "64", - "WHISTLEBLOWER_REWARD_QUOTIENT": "512", - "PROPOSER_REWARD_QUOTIENT": "8", - "INACTIVITY_PENALTY_QUOTIENT": "16777216", - "MIN_SLASHING_PENALTY_QUOTIENT": "32", - "SAFE_SLOTS_TO_UPDATE_JUSTIFIED": "8", - "DOMAIN_BEACON_PROPOSER": "0x00000000", - "DOMAIN_BEACON_ATTESTER": "0x01000000", - "DOMAIN_RANDAO": "0x02000000", - "DOMAIN_DEPOSIT": "0x03000000", - "DOMAIN_VOLUNTARY_EXIT": "0x04000000", - "DOMAIN_SELECTION_PROOF": "0x05000000", - "DOMAIN_AGGREGATE_AND_PROOF": "0x06000000", - "DOMAIN_APPLICATION_MASK": "0x00000001", - "MAX_VALIDATORS_PER_COMMITTEE": "2048", - "SLOTS_PER_EPOCH": "32", - "EPOCHS_PER_ETH1_VOTING_PERIOD": "32", - "SLOTS_PER_HISTORICAL_ROOT": "8192", - "EPOCHS_PER_HISTORICAL_VECTOR": "65536", - "EPOCHS_PER_SLASHINGS_VECTOR": "8192", - "HISTORICAL_ROOTS_LIMIT": "16777216", - "VALIDATOR_REGISTRY_LIMIT": "1099511627776", - "MAX_PROPOSER_SLASHINGS": "16", - "MAX_ATTESTER_SLASHINGS": "2", - "MAX_ATTESTATIONS": "128", - "MAX_DEPOSITS": "16", - "MAX_VOLUNTARY_EXITS": "16", - "ETH1_FOLLOW_DISTANCE": "1024", - "TARGET_AGGREGATORS_PER_COMMITTEE": "16", - "RANDOM_SUBNETS_PER_VALIDATOR": "1", - "EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION": "256", - "SECONDS_PER_ETH1_BLOCK": "14", - "DEPOSIT_CONTRACT_ADDRESS": "0x48b597f4b53c21b48ad95c7256b49d1779bd5890" - } + "data": { + "CONFIG_NAME": "prater", + "PRESET_BASE": "mainnet", + "TERMINAL_TOTAL_DIFFICULTY": "10790000", + "TERMINAL_BLOCK_HASH": "0x0000000000000000000000000000000000000000000000000000000000000000", + "TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH": "18446744073709551615", + "SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY": "128", + "MIN_GENESIS_ACTIVE_VALIDATOR_COUNT": "16384", + "MIN_GENESIS_TIME": "1614588812", + "GENESIS_FORK_VERSION": "0x00001020", + "GENESIS_DELAY": "1919188", + "ALTAIR_FORK_VERSION": "0x01001020", + "ALTAIR_FORK_EPOCH": "36660", + "BELLATRIX_FORK_VERSION": "0x02001020", + "BELLATRIX_FORK_EPOCH": "112260", + "CAPELLA_FORK_VERSION": "0x03001020", + "CAPELLA_FORK_EPOCH": "162304", + "SECONDS_PER_SLOT": "12", + "SECONDS_PER_ETH1_BLOCK": "14", + "MIN_VALIDATOR_WITHDRAWABILITY_DELAY": "256", + "SHARD_COMMITTEE_PERIOD": "256", + "ETH1_FOLLOW_DISTANCE": "2048", + "INACTIVITY_SCORE_BIAS": "4", + "INACTIVITY_SCORE_RECOVERY_RATE": "16", + "EJECTION_BALANCE": "16000000000", + "MIN_PER_EPOCH_CHURN_LIMIT": "4", + "CHURN_LIMIT_QUOTIENT": "65536", + "PROPOSER_SCORE_BOOST": "40", + "DEPOSIT_CHAIN_ID": "5", + "DEPOSIT_NETWORK_ID": "5", + "DEPOSIT_CONTRACT_ADDRESS": "0xff50ed3d0ec03ac01d4c79aad74928bff48a7b2b", + "MAX_COMMITTEES_PER_SLOT": "64", + "TARGET_COMMITTEE_SIZE": "128", + "MAX_VALIDATORS_PER_COMMITTEE": "2048", + "SHUFFLE_ROUND_COUNT": "90", + "HYSTERESIS_QUOTIENT": "4", + "HYSTERESIS_DOWNWARD_MULTIPLIER": "1", + "HYSTERESIS_UPWARD_MULTIPLIER": "5", + "SAFE_SLOTS_TO_UPDATE_JUSTIFIED": "8", + "MIN_DEPOSIT_AMOUNT": "1000000000", + "MAX_EFFECTIVE_BALANCE": "32000000000", + "EFFECTIVE_BALANCE_INCREMENT": "1000000000", + "MIN_ATTESTATION_INCLUSION_DELAY": "1", + "SLOTS_PER_EPOCH": "32", + "MIN_SEED_LOOKAHEAD": "1", + "MAX_SEED_LOOKAHEAD": "4", + "EPOCHS_PER_ETH1_VOTING_PERIOD": "64", + "SLOTS_PER_HISTORICAL_ROOT": "8192", + "MIN_EPOCHS_TO_INACTIVITY_PENALTY": "4", + "EPOCHS_PER_HISTORICAL_VECTOR": "65536", + "EPOCHS_PER_SLASHINGS_VECTOR": "8192", + "HISTORICAL_ROOTS_LIMIT": "16777216", + "VALIDATOR_REGISTRY_LIMIT": "1099511627776", + "BASE_REWARD_FACTOR": "64", + "WHISTLEBLOWER_REWARD_QUOTIENT": "512", + "PROPOSER_REWARD_QUOTIENT": "8", + "INACTIVITY_PENALTY_QUOTIENT": "67108864", + "MIN_SLASHING_PENALTY_QUOTIENT": "128", + "PROPORTIONAL_SLASHING_MULTIPLIER": "1", + "MAX_PROPOSER_SLASHINGS": "16", + "MAX_ATTESTER_SLASHINGS": "2", + "MAX_ATTESTATIONS": "128", + "MAX_DEPOSITS": "16", + "MAX_VOLUNTARY_EXITS": "16", + "INACTIVITY_PENALTY_QUOTIENT_ALTAIR": "50331648", + "MIN_SLASHING_PENALTY_QUOTIENT_ALTAIR": "64", + "PROPORTIONAL_SLASHING_MULTIPLIER_ALTAIR": "2", + "SYNC_COMMITTEE_SIZE": "512", + "EPOCHS_PER_SYNC_COMMITTEE_PERIOD": "256", + "MIN_SYNC_COMMITTEE_PARTICIPANTS": "1", + "INACTIVITY_PENALTY_QUOTIENT_BELLATRIX": "16777216", + "MIN_SLASHING_PENALTY_QUOTIENT_BELLATRIX": "32", + "PROPORTIONAL_SLASHING_MULTIPLIER_BELLATRIX": "3", + "MAX_BYTES_PER_TRANSACTION": "1073741824", + "MAX_TRANSACTIONS_PER_PAYLOAD": "1048576", + "BYTES_PER_LOGS_BLOOM": "256", + "MAX_EXTRA_DATA_BYTES": "32", + "MAX_BLS_TO_EXECUTION_CHANGES": "16", + "MAX_WITHDRAWALS_PER_PAYLOAD": "16", + "MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP": "16384", + "DOMAIN_DEPOSIT": "0x03000000", + "BLS_WITHDRAWAL_PREFIX": "0x00", + "RANDOM_SUBNETS_PER_VALIDATOR": "1", + "DOMAIN_SYNC_COMMITTEE": "0x07000000", + "TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE": "16", + "DOMAIN_BEACON_ATTESTER": "0x01000000", + "DOMAIN_VOLUNTARY_EXIT": "0x04000000", + "DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF": "0x08000000", + "DOMAIN_CONTRIBUTION_AND_PROOF": "0x09000000", + "EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION": "256", + "TARGET_AGGREGATORS_PER_COMMITTEE": "16", + "DOMAIN_APPLICATION_MASK": "0x00000001", + "DOMAIN_AGGREGATE_AND_PROOF": "0x06000000", + "DOMAIN_RANDAO": "0x02000000", + "DOMAIN_SELECTION_PROOF": "0x05000000", + "DOMAIN_BEACON_PROPOSER": "0x00000000", + "SYNC_COMMITTEE_SUBNET_COUNT": "4" + } } ``` @@ -240,13 +335,13 @@ file may be read by a local user with access rights. | Required Headers | - | | Typical Responses | 200 | -### Example Path +Command: -``` -localhost:5062/lighthouse/auth +```bash +curl http://localhost:5062/lighthouse/auth | jq ``` -### Example Response Body +Example Response Body ```json { @@ -267,7 +362,14 @@ Lists all validators managed by this validator client. | Required Headers | [`Authorization`](./api-vc-auth-header.md) | | Typical Responses | 200 | -### Example Response Body +Command: + +```bash +DATADIR=/var/lib/lighthouse +curl -X GET "http://localhost:5062/lighthouse/validators/" -H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" | jq +``` + +Example Response Body ```json { @@ -304,13 +406,14 @@ Get a validator by their `voting_pubkey`. | Required Headers | [`Authorization`](./api-vc-auth-header.md) | | Typical Responses | 200, 400 | -### Example Path +Command: -``` -localhost:5062/lighthouse/validators/0xb0148e6348264131bf47bcd1829590e870c836dc893050fd0dadc7a28949f9d0a72f2805d027521b45441101f0cc1cde +```bash +DATADIR=/var/lib/lighthouse +curl -X GET "http://localhost:5062/lighthouse/validators/0xb0148e6348264131bf47bcd1829590e870c836dc893050fd0dadc7a28949f9d0a72f2805d027521b45441101f0cc1cde" -H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" | jq ``` -### Example Response Body +Example Response Body ```json { @@ -323,7 +426,7 @@ localhost:5062/lighthouse/validators/0xb0148e6348264131bf47bcd1829590e870c836dc8 ## `PATCH /lighthouse/validators/:voting_pubkey` -Update some values for the validator with `voting_pubkey`. +Update some values for the validator with `voting_pubkey`. The following example updates a validator from `enabled: true` to `enabled: false` ### HTTP Specification @@ -334,13 +437,8 @@ Update some values for the validator with `voting_pubkey`. | Required Headers | [`Authorization`](./api-vc-auth-header.md) | | Typical Responses | 200, 400 | -### Example Path -``` -localhost:5062/lighthouse/validators/0xb0148e6348264131bf47bcd1829590e870c836dc893050fd0dadc7a28949f9d0a72f2805d027521b45441101f0cc1cde -``` - -### Example Request Body +Example Request Body ```json { @@ -348,12 +446,29 @@ localhost:5062/lighthouse/validators/0xb0148e6348264131bf47bcd1829590e870c836dc8 } ``` +Command: + +```bash +DATADIR=/var/lib/lighthouse +curl -X PATCH "http://localhost:5062/lighthouse/validators/0xb0148e6348264131bf47bcd1829590e870c836dc893050fd0dadc7a28949f9d0a72f2805d027521b45441101f0cc1cde" \ +-H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" \ +-H "Content-Type: application/json" \ +-d "{\"enabled\":false}" | jq +``` ### Example Response Body ```json null ``` +A `null` response indicates that the request is successful. At the same time, `lighthouse vc` will log: + +``` +INFO Disabled validator voting_pubkey: 0xb0148e6348264131bf47bcd1829590e870c836dc893050fd0dadc7a28949f9d0a72f2805d027521b45441101f0cc1cde +INFO Modified key_cache saved successfully +``` + + ## `POST /lighthouse/validators/` Create any number of new validators, all of which will share a common mnemonic @@ -392,6 +507,28 @@ Validators are generated from the mnemonic according to ] ``` +Command: +```bash +DATADIR=/var/lib/lighthouse +curl -X POST http://localhost:5062/lighthouse/validators \ +-H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" \ +-H "Content-Type: application/json" \ +-d '[ + { + "enable": true, + "description": "validator_one", + "deposit_gwei": "32000000000", + "graffiti": "Mr F was here", + "suggested_fee_recipient": "0xa2e334e71511686bcfe38bb3ee1ad8f6babcc03d" + }, + { + "enable": false, + "description": "validator two", + "deposit_gwei": "34000000000" + } +]' | jq +``` + ### Example Response Body ```json @@ -416,6 +553,14 @@ Validators are generated from the mnemonic according to ] } } +``` + + `lighthouse vc` will log: + +``` +INFO Enabled validator voting_pubkey: 0x8ffbc881fb60841a4546b4b385ec5e9b5090fd1c4395e568d98b74b94b41a912c6101113da39d43c101369eeb9b48e50, signing_method: local_keystore +INFO Modified key_cache saved successfully +INFO Disabled validator voting_pubkey: 0xa9fadd620dc68e9fe0d6e1a69f6c54a0271ad65ab5a509e645e45c6e60ff8f4fc538f301781193a08b55821444801502 ``` ## `POST /lighthouse/validators/keystore` @@ -474,6 +619,19 @@ Import a keystore into the validator client. } ``` +We can use [JSON to String Converter](https://jsontostring.com/) so that the above data can be properly presented as a command. The command is as below: + +Command: +```bash +DATADIR=/var/lib/lighthouse +curl -X POST http://localhost:5062/lighthouse/validators/keystore \ +-H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" \ +-H "Content-Type: application/json" \ +-d "{\"enable\":true,\"password\":\"mypassword\",\"keystore\":{\"crypto\":{\"kdf\":{\"function\":\"scrypt\",\"params\":{\"dklen\":32,\"n\":262144,\"r\":8,\"p\":1,\"salt\":\"445989ec2f332bb6099605b4f1562c0df017488d8d7fb3709f99ebe31da94b49\"},\"message\":\"\"},\"checksum\":{\"function\":\"sha256\",\"params\":{},\"message\":\"abadc1285fd38b24a98ac586bda5b17a8f93fc1ff0778803dc32049578981236\"},\"cipher\":{\"function\":\"aes-128-ctr\",\"params\":{\"iv\":\"65abb7e1d02eec9910d04299cc73efbe\"},\"message\":\"6b7931a4447be727a3bb5dc106d9f3c1ba50671648e522f213651d13450b6417\"}},\"uuid\":\"5cf2a1fb-dcd6-4095-9ebf-7e4ee0204cab\",\"path\":\"m/12381/3600/0/0/0\",\"pubkey\":\"b0d2f05014de27c6d7981e4a920799db1c512ee7922932be6bf55729039147cf35a090bd4ab378fe2d133c36cbbc9969\",\"version\":4,\"description\":\"\"}}" | jq +``` + +As this is an example for demonstration, the above command will return `InvalidPassword`. However, with a keystore file and correct password, running the above command will import the keystore to the validator client. An example of a success message is shown below: + ### Example Response Body ```json { @@ -484,6 +642,13 @@ Import a keystore into the validator client. } } +``` + + `lighthouse vc` will log: + +```bash +INFO Enabled validator voting_pubkey: 0xb0d2f05014de27c6d7981e4a920799db1c512ee7922932be6bf55729039147cf35a090bd4ab378fe2d133c36cbb, signing_method: local_keystore +INFO Modified key_cache saved successfully ``` ## `POST /lighthouse/validators/mnemonic` @@ -521,6 +686,16 @@ generated with the path `m/12381/3600/i/42`. } ``` +Command: + +```bash +DATADIR=/var/lib/lighthouse +curl -X POST http://localhost:5062/lighthouse/validators/mnemonic \ +-H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" \ +-H "Content-Type: application/json" \ +-d '{"mnemonic":" theme onion deal plastic claim silver fancy youth lock ordinary hotel elegant balance ridge web skill burger survey demand distance legal fish salad cloth","key_derivation_path_offset":0,"validators":[{"enable":true,"description":"validator_one","deposit_gwei":"32000000000"}]}' | jq +``` + ### Example Response Body ```json @@ -537,6 +712,13 @@ generated with the path `m/12381/3600/i/42`. } ``` +`lighthouse vc` will log: + +``` +INFO Enabled validator voting_pubkey: 0xa062f95fee747144d5e511940624bc6546509eeaeae9383257a9c43e7ddc58c17c2bab4ae62053122184c381b90db380, signing_method: local_keystore +INFO Modified key_cache saved successfully +``` + ## `POST /lighthouse/validators/web3signer` Create any number of new validators, all of which will refer to a @@ -575,9 +757,29 @@ The following fields may be omitted or nullified to obtain default values: - `root_certificate_path` - `request_timeout_ms` +Command: +```bash +DATADIR=/var/lib/lighthouse +curl -X POST http://localhost:5062/lighthouse/validators/web3signer \ +-H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" \ +-H "Content-Type: application/json" \ +-d "[{\"enable\":true,\"description\":\"validator_one\",\"graffiti\":\"Mr F was here\",\"suggested_fee_recipient\":\"0xa2e334e71511686bcfe38bb3ee1ad8f6babcc03d\",\"voting_public_key\":\"0xa062f95fee747144d5e511940624bc6546509eeaeae9383257a9c43e7ddc58c17c2bab4ae62053122184c381b90db380\",\"url\":\"http://path-to-web3signer.com\",\"request_timeout_ms\":12000}]" +``` + + ### Example Response Body -*No data is included in the response body.* + +```json +null +``` + +A `null` response indicates that the request is successful. At the same time, `lighthouse vc` will log: + +``` +INFO Enabled validator voting_pubkey: 0xa062f95fee747144d5e511940624bc6546509eeaeae9383257a9c43e7ddc58c17c2bab4ae62053122184c381b90db380, signing_method: remote_signer +``` + ## `GET /lighthouse/logs` @@ -607,4 +809,4 @@ logs emitted are INFO level or higher. "total": 1 } } -``` \ No newline at end of file +``` diff --git a/book/src/api-vc.md b/book/src/api-vc.md index 74c493ebea..a3400016ec 100644 --- a/book/src/api-vc.md +++ b/book/src/api-vc.md @@ -14,13 +14,13 @@ signers. It also includes some Lighthouse-specific endpoints which are described ## Starting the server -A Lighthouse validator client can be configured to expose a HTTP server by supplying the `--http` flag. The default listen address is `127.0.0.1:5062`. +A Lighthouse validator client can be configured to expose a HTTP server by supplying the `--http` flag. The default listen address is `http://127.0.0.1:5062`. The following CLI flags control the HTTP server: - `--http`: enable the HTTP server (required even if the following flags are provided). -- `--http-address`: specify the listen address of the server. It is almost always unsafe to use a non-default HTTP listen address. Use with caution. See the **Security** section below for more information. +- `--http-address`: specify the listen address of the server. It is almost always unsafe to use a non-default HTTP listen address. Use this with caution. See the **Security** section below for more information. - `--http-port`: specify the listen port of the server. - `--http-allow-origin`: specify the value of the `Access-Control-Allow-Origin` header. The default is to not supply a header. @@ -28,7 +28,7 @@ The following CLI flags control the HTTP server: ## Security The validator client HTTP server is **not encrypted** (i.e., it is **not HTTPS**). For -this reason, it will listen by default on `127.0.0.1`. +this reason, it will listen by default on `http://127.0.0.1`. It is unsafe to expose the validator client to the public Internet without additional transport layer security (e.g., HTTPS via nginx, SSH tunnels, etc.). diff --git a/book/src/builders.md b/book/src/builders.md index fc42f9b743..8d727ef2ce 100644 --- a/book/src/builders.md +++ b/book/src/builders.md @@ -1,4 +1,4 @@ -# MEV and Lighthouse +# Maximal Extractable Value (MEV) Lighthouse is able to interact with servers that implement the [builder API](https://github.com/ethereum/builder-specs), allowing it to produce blocks without having @@ -103,11 +103,31 @@ Each field is optional. } ``` +Command: + +```bash +DATADIR=/var/lib/lighthouse +curl -X PATCH "http://localhost:5062/lighthouse/validators/0xb0148e6348264131bf47bcd1829590e870c836dc893050fd0dadc7a28949f9d0a72f2805d027521b45441101f0cc1cde" \ +-H "Authorization: Bearer $(sudo cat ${DATADIR}/validators/api-token.txt)" \ +-H "Content-Type: application/json" \ +-d '{ + "builder_proposals": true, + "gas_limit": 30000001 +}' | jq +``` + #### Example Response Body ```json null ``` + +A `null` response indicates that the request is successful. At the same time, `lighthouse vc` will show a log which looks like: + +``` +INFO Published validator registrations to the builder network, count: 3, service: preparation +``` + ### Fee Recipient Refer to [suggested fee recipient](suggested-fee-recipient.md) documentation. @@ -167,9 +187,18 @@ consider using it for the chance of out-sized rewards, this flag may be useful: The number provided indicates the minimum reward that an external payload must provide the proposer for it to be considered for inclusion in a proposal. For example, if you'd only like to use an external payload for a reward of >= 0.25 ETH, you would provide your beacon node with `--builder-profit-threshold 250000000000000000`. If it's your turn to propose and the -most valuable payload offered by builders is only 0.1 ETH, the local execution engine's payload will be used. Currently, -this threshold just looks at the value of the external payload. No comparison to the local payload is made, although -this feature will likely be added in the future. +most valuable payload offered by builders is only 0.1 ETH, the local execution engine's payload will be used. + +Since the [Capella](https://ethereum.org/en/history/#capella) upgrade, a comparison of the external payload and local payload will be made according to the [engine_getPayloadV2](https://github.com/ethereum/execution-apis/blob/main/src/engine/shanghai.md#engine_getpayloadv2) API. The logic is as follows: + +``` +if local payload value >= builder payload value: + use local payload +else if builder payload value >= builder_profit_threshold or builder_profit_threshold == 0: + use builder payload +else: + use local payload +``` ## Checking your builder config diff --git a/book/src/checkpoint-sync.md b/book/src/checkpoint-sync.md index 47dc03b20c..d5c8b18e57 100644 --- a/book/src/checkpoint-sync.md +++ b/book/src/checkpoint-sync.md @@ -34,7 +34,7 @@ INFO Loaded checkpoint block and state state_root: 0xe8252c68784a8d5cc7e54 ``` > **Security Note**: You should cross-reference the `block_root` and `slot` of the loaded checkpoint -> against a trusted source like a friend's node, or a block explorer. +> against a trusted source like a friend's node, a block explorer or some [public endpoints](https://eth-clients.github.io/checkpoint-sync-endpoints/). Once the checkpoint is loaded Lighthouse will sync forwards to the head of the chain. @@ -62,6 +62,10 @@ INFO Downloading historical blocks est_time: 5 hrs 0 mins, speed: 111.96 slots/ Once backfill is complete, a `INFO Historical block download complete` log will be emitted. +> Note: Since [v4.1.0](https://github.com/sigp/lighthouse/releases/tag/v4.1.0), Lighthouse implements rate-limited backfilling to mitigate validator performance issues after a recent checkpoint sync. This means that the speed at which historical blocks are downloaded is limited, typically to less than 20 slots/sec. This will not affect validator performance. However, if you would still prefer to sync the chain as fast as possible, you can add the flag `--disable-backfill-rate-limiting` to the beacon node. + +> Note: Since [v4.2.0](https://github.com/sigp/lighthouse/releases/tag/v4.2.0), Lighthouse limits the backfill sync to only sync backwards to the weak subjectivity point (approximately 5 months). This will help to save disk space. However, if you would like to sync back to the genesis, you can add the flag `--genesis-backfill` to the beacon node. + ## FAQ 1. What if I have an existing database? How can I use checkpoint sync? diff --git a/book/src/database-migrations.md b/book/src/database-migrations.md index 5e0b896359..9b60ca2e18 100644 --- a/book/src/database-migrations.md +++ b/book/src/database-migrations.md @@ -29,6 +29,7 @@ validator client or the slasher**. | v3.4.0 | Jan 2023 | v13 | yes | | v3.5.0 | Feb 2023 | v15 | yes before Capella | | v4.0.1 | Mar 2023 | v16 | yes before Capella | +| v4.2.0 | May 2023 | v17 | yes | > **Note**: All point releases (e.g. v2.3.1) are schema-compatible with the prior minor release > (e.g. v2.3.0). @@ -82,24 +83,36 @@ on downgrades above. To check the schema version of a running Lighthouse instance you can use the HTTP API: ```bash -curl "http://localhost:5052/lighthouse/database/info" +curl "http://localhost:5052/lighthouse/database/info" | jq ``` ```json { - "schema_version": 8, + "schema_version": 16, "config": { "slots_per_restore_point": 8192, - "slots_per_restore_point_set_explicitly": true, + "slots_per_restore_point_set_explicitly": false, "block_cache_size": 5, "historic_state_cache_size": 1, "compact_on_init": false, - "compact_on_prune": true + "compact_on_prune": true, + "prune_payloads": true + }, + "split": { + "slot": "5485952", + "state_root": "0xcfe5d41e6ab5a9dab0de00d89d97ae55ecaeed3b08e4acda836e69b2bef698b4" + }, + "anchor": { + "anchor_slot": "5414688", + "oldest_block_slot": "0", + "oldest_block_parent": "0x0000000000000000000000000000000000000000000000000000000000000000", + "state_upper_limit": "5414912", + "state_lower_limit": "8192" } } ``` -The `schema_version` key indicates that this database is using schema version 8. +The `schema_version` key indicates that this database is using schema version 16. Alternatively, you can check the schema version with the `lighthouse db` command. @@ -118,7 +131,7 @@ Several conditions need to be met in order to run `lighthouse db`: 2. The command must run as the user that owns the beacon node database. If you are using systemd then your beacon node might run as a user called `lighthousebeacon`. 3. The `--datadir` flag must be set to the location of the Lighthouse data directory. -4. The `--network` flag must be set to the correct network, e.g. `mainnet`, `prater` or `sepolia`. +4. The `--network` flag must be set to the correct network, e.g. `mainnet`, `goerli` or `sepolia`. The general form for a `lighthouse db` command is: diff --git a/book/src/key-management.md b/book/src/key-management.md index 084b1fbe4c..cebd84649d 100644 --- a/book/src/key-management.md +++ b/book/src/key-management.md @@ -3,7 +3,7 @@ [launchpad]: https://launchpad.ethereum.org/ > -> **Note: While Lighthouse is able to generate the validator keys and the deposit data file to submit to the deposit contract, we strongly recommend using the [staking-deposit-cli](https://github.com/ethereum/staking-deposit-cli) to create validators keys and the deposit data file. This is because the [staking-deposit-cli](https://github.com/ethereum/staking-deposit-cli) which has the option to assign a withdrawal address during the key generation process, while Lighthouse wallet will always generate keys with withdrawal credentials of type 0x00. This means that users who created keys using Lighthouse will have to update their withdrawal credentials in the future to enable withdrawals. In addition, Lighthouse generates the deposit data file in the form of `*.rlp`, which cannot be uploaded to the [Staking launchpad][launchpad] that accepts only `*.json` file. This means that users have to directly interact with the deposit contract to be able to submit the deposit if they were to generate the files using Lighthouse.** +> **Note: While Lighthouse is able to generate the validator keys and the deposit data file to submit to the deposit contract, we strongly recommend using the [staking-deposit-cli](https://github.com/ethereum/staking-deposit-cli) to create validators keys and the deposit data file. This is because the [staking-deposit-cli](https://github.com/ethereum/staking-deposit-cli) has the option to assign a withdrawal address during the key generation process, while Lighthouse wallet will always generate keys with withdrawal credentials of type 0x00. This means that users who created keys using Lighthouse will have to update their withdrawal credentials in the future to enable withdrawals. In addition, Lighthouse generates the deposit data file in the form of `*.rlp`, which cannot be uploaded to the [Staking launchpad][launchpad] that accepts only `*.json` file. This means that users have to directly interact with the deposit contract to be able to submit the deposit if they were to generate the files using Lighthouse.** Lighthouse uses a _hierarchical_ key management system for producing validator keys. It is hierarchical because each validator key can be _derived_ from a diff --git a/book/src/merge-migration.md b/book/src/merge-migration.md index 08107695f3..acca0bbeb3 100644 --- a/book/src/merge-migration.md +++ b/book/src/merge-migration.md @@ -1,15 +1,13 @@ # Merge Migration -This document provides detail for users who want to run a Lighthouse node on post-merge Ethereum. - -> The merge occurred on mainnet in September 2022. +[The Merge](https://ethereum.org/en/roadmap/merge/) has occurred on mainnet on 15th September 2022. This document provides detail of what users need to do in the past (before The Merge) to run a Lighthouse node on a post-merge Ethereum network. This document now serves as a record of the milestone upgrade. ## Necessary Configuration There are two configuration changes required for a Lighthouse node to operate correctly throughout the merge: -1. You *must* run your own execution engine such as Geth or Nethermind alongside Lighthouse. +1. You *must* run your own execution engine such as Besu, Erigon, Geth or Nethermind alongside Lighthouse. You *must* update your `lighthouse bn` configuration to connect to the execution engine using new flags which are documented on this page in the [Connecting to an execution engine](#connecting-to-an-execution-engine) section. @@ -23,11 +21,19 @@ engine to a merge-ready version. ## When? -You must configure your node to be merge-ready before the Bellatrix fork occurs on the network -on which your node is operating. +All networks (**Mainnet**, **Goerli (Prater)**, **Ropsten**, **Sepolia**, **Kiln**, **Gnosis**) have successfully undergone the Bellatrix fork and transitioned to a post-merge Network. Your node must have a merge-ready configuration to continue operating. Table below lists the date at which Bellatrix and The Merge occurred: -* **Mainnet**, **Goerli (Prater)**, **Ropsten**, **Sepolia**, **Kiln**, **Gnosis**: the Bellatrix fork has - already occurred. You must have a merge-ready configuration right now. +
+ +| Network | Bellatrix | The Merge | Remark | +|-------------------|--------------------------------------------|----|----| +| Ropsten | 2nd June 2022 | 8th June 2022 | Deprecated +| Sepolia | 20th June 2022 | 6th July 2022 | | +| Goerli | 4th August 2022 | 10th August 2022 | Previously named `Prater`| +| Mainnet | 6th September 2022 | 15th September 2022 | +| Gnosis| 30th November 2022 | 8th December 2022 + +
## Connecting to an execution engine @@ -41,7 +47,7 @@ present in post-merge blocks. Two new flags are used to configure this connectio If you set up an execution engine with `--execution-endpoint` then you *must* provide a JWT secret using `--execution-jwt`. This is a mandatory form of authentication that ensures that Lighthouse -has authority to control the execution engine. +has the authority to control the execution engine. > Tip: the --execution-jwt-secret-key flag can be used instead of --execution-jwt . > This is useful, for example, for users who wish to inject the value into a Docker container without @@ -88,7 +94,7 @@ lighthouse \ beacon_node \ --http \ --execution-endpoint http://localhost:8551 - --execution-jwt ~/.ethereum/geth/jwtsecret + --execution-jwt /path/to/jwtsecret ``` The changes here are: @@ -104,8 +110,7 @@ The changes here are: not be `8551`, see their documentation for details. 3. Add the `--execution-jwt` flag. - This is the path to a file containing a 32-byte secret for authenticating the BN with the - execution engine. In this example our execution engine is Geth, so we've chosen the default - location for Geth. Your execution engine might have a different path. It is critical that both + execution engine. It is critical that both the BN and execution engine reference a file with the same value, otherwise they'll fail to communicate. @@ -117,7 +122,7 @@ a deprecation warning will be logged and Lighthouse *may* remove these flags in ### The relationship between `--eth1-endpoints` and `--execution-endpoint` Pre-merge users will be familiar with the `--eth1-endpoints` flag. This provides a list of Ethereum -"eth1" nodes (e.g., Geth, Nethermind, etc). Each beacon node (BN) can have multiple eth1 endpoints +"eth1" nodes (Besu, Erigon, Geth or Nethermind). Each beacon node (BN) can have multiple eth1 endpoints and each eth1 endpoint can have many BNs connection (many-to-many relationship). The eth1 node provides a source of truth for the [deposit contract](https://ethereum.org/en/staking/deposit-contract/) and beacon chain proposers include this @@ -128,7 +133,7 @@ achieve this. To progress through the Bellatrix upgrade nodes will need a *new* connection to an "eth1" node; `--execution-endpoint`. This connection has a few different properties. Firstly, the term "eth1 node" has been deprecated and replaced with "execution engine". Whilst "eth1 node" and "execution -engine" still refer to the same projects (Geth, Nethermind, etc) the former refers to the pre-merge +engine" still refer to the same projects (Besu, Erigon, Geth or Nethermind), the former refers to the pre-merge versions and the latter refers to post-merge versions. Secondly, there is a strict one-to-one relationship between Lighthouse and the execution engine; only one Lighthouse node can connect to one execution engine. Thirdly, it is impossible to fully verify the post-merge chain without an @@ -138,7 +143,7 @@ impossible to reliably *propose* blocks without it. Since an execution engine is a hard requirement in the post-merge chain and the execution engine contains the transaction history of the Ethereum chain, there is no longer a need for the `--eth1-endpoints` flag for information about the deposit contract. The `--execution-endpoint` can -be used for all such queries. Therefore we can say that where `--execution-endpoint` is included +be used for all such queries. Therefore we can say that where `--execution-endpoint` is included, `--eth1-endpoints` should be omitted. ## FAQ diff --git a/book/src/redundancy.md b/book/src/redundancy.md index dcd2ecdea1..77cec32537 100644 --- a/book/src/redundancy.md +++ b/book/src/redundancy.md @@ -8,7 +8,7 @@ There are three places in Lighthouse where redundancy is notable: 1. ❌ NOT SUPPORTED: Using a redundant execution node in `lighthouse bn --execution-endpoint` 1. ☠️ BAD: Running redundant `lighthouse vc` instances with overlapping keypairs. -I mention (3) since it is unsafe and should not be confused with the other two +We mention (3) since it is unsafe and should not be confused with the other two uses of redundancy. **Running the same validator keypair in more than one validator client (Lighthouse, or otherwise) will eventually lead to slashing.** See [Slashing Protection](./slashing-protection.md) for more information. @@ -49,6 +49,7 @@ There are a few interesting properties about the list of `--beacon-nodes`: > provided (if it is desired). It will only be used as default if no `--beacon-nodes` flag is > provided at all. + ### Configuring a redundant Beacon Node In our previous example, we listed `http://192.168.1.1:5052` as a redundant @@ -56,10 +57,9 @@ node. Apart from having sufficient resources, the backup node should have the following flags: - `--http`: starts the HTTP API server. -- `--http-address 0.0.0.0`: this allows *any* external IP address to access the - HTTP server (a firewall should be configured to deny unauthorized access to port - `5052`). This is only required if your backup node is on a different host. -- `--execution-endpoint`: see [Merge Migration](./merge-migration.md). +- `--http-address local_IP`: where `local_IP` is the private IP address of the computer running the beacon node. This is only required if your backup beacon node is on a different host. + > Note: You could also use `--http-address 0.0.0.0`, but this allows *any* external IP address to access the HTTP server. As such, a firewall should be configured to deny unauthorized access to port `5052`. + - `--execution-endpoint`: see [Merge Migration](./merge-migration.md). - `--execution-jwt`: see [Merge Migration](./merge-migration.md). For example one could use the following command to provide a backup beacon node: @@ -67,7 +67,7 @@ For example one could use the following command to provide a backup beacon node: ```bash lighthouse bn \ --http \ - --http-address 0.0.0.0 \ + --http-address local_IP \ --execution-endpoint http://localhost:8551 \ --execution-jwt /secrets/jwt.hex ``` diff --git a/book/src/slasher.md b/book/src/slasher.md index 61dc4b327f..ecf9d34efd 100644 --- a/book/src/slasher.md +++ b/book/src/slasher.md @@ -8,10 +8,9 @@ extra income for your validators. However it is currently only recommended for e of the immaturity of the slasher UX and the extra resources required. ## Minimum System Requirements - * Quad-core CPU * 16 GB RAM -* 256 GB solid state storage (in addition to space for the beacon node DB) +* 256 GB solid state storage (in addition to the space requirement for the beacon node DB) ## How to Run @@ -28,7 +27,7 @@ messages are filtered for relevancy, and all relevant messages are checked for s to the slasher database. You **should** run with debug logs, so that you can see the slasher's internal machinations, and -provide logs to the devs should you encounter any bugs. +provide logs to the developers should you encounter any bugs. ## Configuration @@ -97,7 +96,7 @@ Both database backends LMDB and MDBX place a hard limit on the size of the datab file. You can use the `--slasher-max-db-size` flag to set this limit. It can be adjusted after initialization if the limit is reached. -By default the limit is set to accommodate the default history length and around 300K validators but +By default the limit is set to accommodate the default history length and around 600K validators (with about 30% headroom) but you can set it lower if running with a reduced history length. The space required scales approximately linearly in validator count and history length, i.e. if you halve either you can halve the space required. @@ -108,7 +107,7 @@ If you want an estimate of the database size you can use this formula: 4.56 GB * (N / 256) * (V / 250000) ``` -where `V` is the validator count and `N` is the history length. +where `N` is the history length and `V` is the validator count. You should set the maximum size higher than the estimate to allow room for growth in the validator count. diff --git a/book/src/suggested-fee-recipient.md b/book/src/suggested-fee-recipient.md index f3ece85062..44accbd143 100644 --- a/book/src/suggested-fee-recipient.md +++ b/book/src/suggested-fee-recipient.md @@ -103,6 +103,8 @@ client. } ``` +Command: + ```bash DATADIR=$HOME/.lighthouse/mainnet PUBKEY=0xa9735061c84fc0003657e5bd38160762b7ef2d67d280e00347b1781570088c32c06f15418c144949f5d736b1d3a6c591 @@ -115,11 +117,15 @@ curl -X POST \ http://localhost:5062/eth/v1/validator/${PUBKEY}/feerecipient | jq ``` +Note that an authorization header is required to interact with the API. This is specified with the header `-H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)"` which read the API token to supply the authentication. Refer to [Authorization Header](./api-vc-auth-header.md) for more information. If you are having permission issue with accessing the API token file, you can modify the header to become `-H "Authorization: Bearer $(sudo cat ${DATADIR}/validators/api-token.txt)"`. + #### Successful Response (202) ```json null ``` +A `null` response indicates that the request is successful. + ### Querying the fee recipient The same path with a `GET` request can be used to query the fee recipient for a given public key at any time. @@ -131,6 +137,8 @@ The same path with a `GET` request can be used to query the fee recipient for a | Required Headers | [`Authorization`](./api-vc-auth-header.md) | | Typical Responses | 200, 404 | +Command: + ```bash DATADIR=$HOME/.lighthouse/mainnet PUBKEY=0xa9735061c84fc0003657e5bd38160762b7ef2d67d280e00347b1781570088c32c06f15418c144949f5d736b1d3a6c591 @@ -163,6 +171,8 @@ This is useful if you want the fee recipient to fall back to the validator clien | Required Headers | [`Authorization`](./api-vc-auth-header.md) | | Typical Responses | 204, 404 | +Command: + ```bash DATADIR=$HOME/.lighthouse/mainnet PUBKEY=0xa9735061c84fc0003657e5bd38160762b7ef2d67d280e00347b1781570088c32c06f15418c144949f5d736b1d3a6c591 diff --git a/book/src/validator-inclusion.md b/book/src/validator-inclusion.md index 0793af20db..ef81b2b751 100644 --- a/book/src/validator-inclusion.md +++ b/book/src/validator-inclusion.md @@ -8,6 +8,8 @@ These endpoints are not stable or included in the Ethereum consensus standard AP they are subject to change or removal without a change in major release version. +In order to apply these APIs, you need to have historical states information in the database of your node. This means adding the flag `--reconstruct-historic-states` in the beacon node or using the [/lighthouse/database/reconstruct API](./api-lighthouse.md#lighthousedatabasereconstruct). Once the state reconstruction process is completed, you can apply these APIs to any epoch. + ## Endpoints HTTP Path | Description | @@ -29,7 +31,7 @@ is not the case for attestations from the _previous_ epoch. ``` `epoch` query parameter | - | --------- values are calcuated here + | --------- values are calculated here | | v v Epoch: |---previous---|---current---|---next---| diff --git a/book/src/validator-web3signer.md b/book/src/validator-web3signer.md index 103f1ccb3c..00ef9a6b59 100644 --- a/book/src/validator-web3signer.md +++ b/book/src/validator-web3signer.md @@ -5,7 +5,7 @@ [Teku]: https://github.com/consensys/teku [Web3Signer] is a tool by Consensys which allows *remote signing*. Remote signing is when a -Validator Client (VC) out-sources the signing of messages to remote server (e.g., via HTTPS). This +Validator Client (VC) out-sources the signing of messages to a remote server (e.g., via HTTPS). This means that the VC does not hold the validator private keys. ## Warnings @@ -47,7 +47,7 @@ remote signer: client_identity_password: "password" ``` -When using this file, the Lighthouse VC will perform duties for the `0xa5566..` validator and defer +When using this file, the Lighthouse VC will perform duties for the `0xa5566..` validator and refer to the `https://my-remote-signer.com:1234` server to obtain any signatures. It will load a "self-signed" SSL certificate from `/home/paul/my-certificates/my-remote-signer.pem` (on the filesystem of the VC) to encrypt the communications between the VC and Web3Signer. It will use From 04386cfabb94d5e645441dd856f527bf05dcc3dc Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 2 Jun 2023 03:17:37 +0000 Subject: [PATCH 29/63] Expose execution block hash calculation (#4326) ## Proposed Changes This is a light refactor of the execution layer's block hash calculation logic making it easier to use externally. e.g. in `eleel` (https://github.com/sigp/eleel/pull/18). A static method is preferable to a method because the calculation doesn't actually need any data from `self`, and callers may want to compute block hashes without constructing an `ExecutionLayer` (`eleel` only constructs a simpler `Engine` struct). --- beacon_node/execution_layer/src/block_hash.rs | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/beacon_node/execution_layer/src/block_hash.rs b/beacon_node/execution_layer/src/block_hash.rs index e9b7dcc17f..c889fead0a 100644 --- a/beacon_node/execution_layer/src/block_hash.rs +++ b/beacon_node/execution_layer/src/block_hash.rs @@ -12,12 +12,13 @@ use types::{ }; impl ExecutionLayer { - /// Verify `payload.block_hash` locally within Lighthouse. + /// Calculate the block hash of an execution block. /// - /// No remote calls to the execution client will be made, so this is quite a cheap check. - pub fn verify_payload_block_hash(&self, payload: ExecutionPayloadRef) -> Result<(), Error> { - let _timer = metrics::start_timer(&metrics::EXECUTION_LAYER_VERIFY_BLOCK_HASH); - + /// Return `(block_hash, transactions_root)`, where `transactions_root` is the root of the RLP + /// transactions. + pub fn calculate_execution_block_hash( + payload: ExecutionPayloadRef, + ) -> (ExecutionBlockHash, Hash256) { // Calculate the transactions root. // We're currently using a deprecated Parity library for this. We should move to a // better alternative when one appears, possibly following Reth. @@ -46,7 +47,19 @@ impl ExecutionLayer { // Hash the RLP encoding of the block header. let rlp_block_header = rlp_encode_block_header(&exec_block_header); - let header_hash = ExecutionBlockHash::from_root(keccak256(&rlp_block_header)); + ( + ExecutionBlockHash::from_root(keccak256(&rlp_block_header)), + rlp_transactions_root, + ) + } + + /// Verify `payload.block_hash` locally within Lighthouse. + /// + /// No remote calls to the execution client will be made, so this is quite a cheap check. + pub fn verify_payload_block_hash(&self, payload: ExecutionPayloadRef) -> Result<(), Error> { + let _timer = metrics::start_timer(&metrics::EXECUTION_LAYER_VERIFY_BLOCK_HASH); + + let (header_hash, rlp_transactions_root) = Self::calculate_execution_block_hash(payload); if header_hash != payload.block_hash() { return Err(Error::BlockHashMismatch { From d399961e6e45a76cc90a1b0ede839df1cdecb176 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Fri, 2 Jun 2023 03:17:38 +0000 Subject: [PATCH 30/63] Add an option to disable inbound rate limiter (#4327) ## Issue Addressed On deneb devnetv5, lighthouse keeps rate limiting peers which makes it harder to bootstrap new nodes as there are very few peers in the network. This PR adds an option to disable the inbound rate limiter for testnets. Added an option to configure inbound rate limits as well. Co-authored-by: Diva M --- beacon_node/lighthouse_network/src/config.rs | 6 +- .../lighthouse_network/src/rpc/config.rs | 51 ++++++-- beacon_node/lighthouse_network/src/rpc/mod.rs | 123 +++++++++--------- .../src/rpc/rate_limiter.rs | 47 +++---- .../src/rpc/self_limiter.rs | 23 +--- .../lighthouse_network/src/service/mod.rs | 1 + beacon_node/src/cli.rs | 18 ++- beacon_node/src/config.rs | 18 ++- lighthouse/tests/beacon_node.rs | 20 +++ 9 files changed, 189 insertions(+), 118 deletions(-) diff --git a/beacon_node/lighthouse_network/src/config.rs b/beacon_node/lighthouse_network/src/config.rs index 5a11890a26..01bb8569dd 100644 --- a/beacon_node/lighthouse_network/src/config.rs +++ b/beacon_node/lighthouse_network/src/config.rs @@ -1,5 +1,5 @@ use crate::listen_addr::{ListenAddr, ListenAddress}; -use crate::rpc::config::OutboundRateLimiterConfig; +use crate::rpc::config::{InboundRateLimiterConfig, OutboundRateLimiterConfig}; use crate::types::GossipKind; use crate::{Enr, PeerIdSerialized}; use directory::{ @@ -148,6 +148,9 @@ pub struct Config { /// Configures if/where invalid blocks should be stored. pub invalid_block_storage: Option, + + /// Configuration for the inbound rate limiter (requests received by this node). + pub inbound_rate_limiter_config: Option, } impl Config { @@ -333,6 +336,7 @@ impl Default for Config { enable_light_client_server: false, outbound_rate_limiter_config: None, invalid_block_storage: None, + inbound_rate_limiter_config: None, } } } diff --git a/beacon_node/lighthouse_network/src/rpc/config.rs b/beacon_node/lighthouse_network/src/rpc/config.rs index bea0929fb0..a0f3acaf76 100644 --- a/beacon_node/lighthouse_network/src/rpc/config.rs +++ b/beacon_node/lighthouse_network/src/rpc/config.rs @@ -58,18 +58,41 @@ impl FromStr for ProtocolQuota { } } -/// Configurations for the rate limiter applied to outbound requests (made by the node itself). +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug, Default)] +pub struct OutboundRateLimiterConfig(pub RateLimiterConfig); + +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug, Default)] +pub struct InboundRateLimiterConfig(pub RateLimiterConfig); + +impl FromStr for OutboundRateLimiterConfig { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + RateLimiterConfig::from_str(s).map(Self) + } +} + +impl FromStr for InboundRateLimiterConfig { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + RateLimiterConfig::from_str(s).map(Self) + } +} + +/// Configurations for the rate limiter. #[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct OutboundRateLimiterConfig { +pub struct RateLimiterConfig { pub(super) ping_quota: Quota, pub(super) meta_data_quota: Quota, pub(super) status_quota: Quota, pub(super) goodbye_quota: Quota, pub(super) blocks_by_range_quota: Quota, pub(super) blocks_by_root_quota: Quota, + pub(super) light_client_bootstrap_quota: Quota, } -impl OutboundRateLimiterConfig { +impl RateLimiterConfig { pub const DEFAULT_PING_QUOTA: Quota = Quota::n_every(2, 10); pub const DEFAULT_META_DATA_QUOTA: Quota = Quota::n_every(2, 5); pub const DEFAULT_STATUS_QUOTA: Quota = Quota::n_every(5, 15); @@ -77,22 +100,24 @@ impl OutboundRateLimiterConfig { pub const DEFAULT_BLOCKS_BY_RANGE_QUOTA: Quota = Quota::n_every(methods::MAX_REQUEST_BLOCKS, 10); pub const DEFAULT_BLOCKS_BY_ROOT_QUOTA: Quota = Quota::n_every(128, 10); + pub const DEFAULT_LIGHT_CLIENT_BOOTSTRAP_QUOTA: Quota = Quota::one_every(10); } -impl Default for OutboundRateLimiterConfig { +impl Default for RateLimiterConfig { fn default() -> Self { - OutboundRateLimiterConfig { + RateLimiterConfig { ping_quota: Self::DEFAULT_PING_QUOTA, meta_data_quota: Self::DEFAULT_META_DATA_QUOTA, status_quota: Self::DEFAULT_STATUS_QUOTA, goodbye_quota: Self::DEFAULT_GOODBYE_QUOTA, blocks_by_range_quota: Self::DEFAULT_BLOCKS_BY_RANGE_QUOTA, blocks_by_root_quota: Self::DEFAULT_BLOCKS_BY_ROOT_QUOTA, + light_client_bootstrap_quota: Self::DEFAULT_LIGHT_CLIENT_BOOTSTRAP_QUOTA, } } } -impl Debug for OutboundRateLimiterConfig { +impl Debug for RateLimiterConfig { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { macro_rules! fmt_q { ($quota:expr) => { @@ -104,7 +129,7 @@ impl Debug for OutboundRateLimiterConfig { }; } - f.debug_struct("OutboundRateLimiterConfig") + f.debug_struct("RateLimiterConfig") .field("ping", fmt_q!(&self.ping_quota)) .field("metadata", fmt_q!(&self.meta_data_quota)) .field("status", fmt_q!(&self.status_quota)) @@ -119,7 +144,7 @@ impl Debug for OutboundRateLimiterConfig { /// the default values. Protocol specified more than once use only the first given Quota. /// /// The expected format is a ';' separated list of [`ProtocolQuota`]. -impl FromStr for OutboundRateLimiterConfig { +impl FromStr for RateLimiterConfig { type Err = &'static str; fn from_str(s: &str) -> Result { @@ -129,6 +154,8 @@ impl FromStr for OutboundRateLimiterConfig { let mut goodbye_quota = None; let mut blocks_by_range_quota = None; let mut blocks_by_root_quota = None; + let mut light_client_bootstrap_quota = None; + for proto_def in s.split(';') { let ProtocolQuota { protocol, quota } = proto_def.parse()?; let quota = Some(quota); @@ -139,10 +166,12 @@ impl FromStr for OutboundRateLimiterConfig { Protocol::BlocksByRoot => blocks_by_root_quota = blocks_by_root_quota.or(quota), Protocol::Ping => ping_quota = ping_quota.or(quota), Protocol::MetaData => meta_data_quota = meta_data_quota.or(quota), - Protocol::LightClientBootstrap => return Err("Lighthouse does not send LightClientBootstrap requests. Quota should not be set."), + Protocol::LightClientBootstrap => { + light_client_bootstrap_quota = light_client_bootstrap_quota.or(quota) + } } } - Ok(OutboundRateLimiterConfig { + Ok(RateLimiterConfig { ping_quota: ping_quota.unwrap_or(Self::DEFAULT_PING_QUOTA), meta_data_quota: meta_data_quota.unwrap_or(Self::DEFAULT_META_DATA_QUOTA), status_quota: status_quota.unwrap_or(Self::DEFAULT_STATUS_QUOTA), @@ -151,6 +180,8 @@ impl FromStr for OutboundRateLimiterConfig { .unwrap_or(Self::DEFAULT_BLOCKS_BY_RANGE_QUOTA), blocks_by_root_quota: blocks_by_root_quota .unwrap_or(Self::DEFAULT_BLOCKS_BY_ROOT_QUOTA), + light_client_bootstrap_quota: light_client_bootstrap_quota + .unwrap_or(Self::DEFAULT_LIGHT_CLIENT_BOOTSTRAP_QUOTA), }) } } diff --git a/beacon_node/lighthouse_network/src/rpc/mod.rs b/beacon_node/lighthouse_network/src/rpc/mod.rs index 31569b820b..4f7af95cfe 100644 --- a/beacon_node/lighthouse_network/src/rpc/mod.rs +++ b/beacon_node/lighthouse_network/src/rpc/mod.rs @@ -17,7 +17,6 @@ use slog::{crit, debug, o}; use std::marker::PhantomData; use std::sync::Arc; use std::task::{Context, Poll}; -use std::time::Duration; use types::{EthSpec, ForkContext}; pub(crate) use handler::HandlerErr; @@ -32,7 +31,7 @@ pub use methods::{ pub(crate) use outbound::OutboundRequest; pub use protocol::{max_rpc_size, Protocol, RPCError}; -use self::config::OutboundRateLimiterConfig; +use self::config::{InboundRateLimiterConfig, OutboundRateLimiterConfig}; use self::self_limiter::SelfRateLimiter; pub(crate) mod codec; @@ -112,7 +111,7 @@ type BehaviourAction = /// logic. pub struct RPC { /// Rate limiter - limiter: RateLimiter, + limiter: Option, /// Rate limiter for our own requests. self_limiter: Option>, /// Queue of events to be processed. @@ -127,32 +126,24 @@ impl RPC { pub fn new( fork_context: Arc, enable_light_client_server: bool, + inbound_rate_limiter_config: Option, outbound_rate_limiter_config: Option, log: slog::Logger, ) -> Self { let log = log.new(o!("service" => "libp2p_rpc")); - let limiter = RateLimiter::builder() - .n_every(Protocol::MetaData, 2, Duration::from_secs(5)) - .n_every(Protocol::Ping, 2, Duration::from_secs(10)) - .n_every(Protocol::Status, 5, Duration::from_secs(15)) - .one_every(Protocol::Goodbye, Duration::from_secs(10)) - .one_every(Protocol::LightClientBootstrap, Duration::from_secs(10)) - .n_every( - Protocol::BlocksByRange, - methods::MAX_REQUEST_BLOCKS, - Duration::from_secs(10), - ) - .n_every(Protocol::BlocksByRoot, 128, Duration::from_secs(10)) - .build() - .expect("Configuration parameters are valid"); + let inbound_limiter = inbound_rate_limiter_config.map(|config| { + debug!(log, "Using inbound rate limiting params"; "config" => ?config); + RateLimiter::new_with_config(config.0) + .expect("Inbound limiter configuration parameters are valid") + }); let self_limiter = outbound_rate_limiter_config.map(|config| { SelfRateLimiter::new(config, log.clone()).expect("Configuration parameters are valid") }); RPC { - limiter, + limiter: inbound_limiter, self_limiter, events: Vec::new(), fork_context, @@ -242,50 +233,60 @@ where event: ::OutEvent, ) { if let Ok(RPCReceived::Request(ref id, ref req)) = event { - // check if the request is conformant to the quota - match self.limiter.allows(&peer_id, req) { - Ok(()) => { - // send the event to the user - self.events - .push(NetworkBehaviourAction::GenerateEvent(RPCMessage { - peer_id, - conn_id, - event, - })) - } - Err(RateLimitedErr::TooLarge) => { - // we set the batch sizes, so this is a coding/config err for most protocols - let protocol = req.protocol(); - if matches!(protocol, Protocol::BlocksByRange) { - debug!(self.log, "Blocks by range request will never be processed"; "request" => %req); - } else { - crit!(self.log, "Request size too large to ever be processed"; "protocol" => %protocol); + if let Some(limiter) = self.limiter.as_mut() { + // check if the request is conformant to the quota + match limiter.allows(&peer_id, req) { + Ok(()) => { + // send the event to the user + self.events + .push(NetworkBehaviourAction::GenerateEvent(RPCMessage { + peer_id, + conn_id, + event, + })) } - // send an error code to the peer. - // the handler upon receiving the error code will send it back to the behaviour - self.send_response( - peer_id, - (conn_id, *id), - RPCCodedResponse::Error( - RPCResponseErrorCode::RateLimited, - "Rate limited. Request too large".into(), - ), - ); - } - Err(RateLimitedErr::TooSoon(wait_time)) => { - debug!(self.log, "Request exceeds the rate limit"; + Err(RateLimitedErr::TooLarge) => { + // we set the batch sizes, so this is a coding/config err for most protocols + let protocol = req.protocol(); + if matches!(protocol, Protocol::BlocksByRange) { + debug!(self.log, "Blocks by range request will never be processed"; "request" => %req); + } else { + crit!(self.log, "Request size too large to ever be processed"; "protocol" => %protocol); + } + // send an error code to the peer. + // the handler upon receiving the error code will send it back to the behaviour + self.send_response( + peer_id, + (conn_id, *id), + RPCCodedResponse::Error( + RPCResponseErrorCode::RateLimited, + "Rate limited. Request too large".into(), + ), + ); + } + Err(RateLimitedErr::TooSoon(wait_time)) => { + debug!(self.log, "Request exceeds the rate limit"; "request" => %req, "peer_id" => %peer_id, "wait_time_ms" => wait_time.as_millis()); - // send an error code to the peer. - // the handler upon receiving the error code will send it back to the behaviour - self.send_response( - peer_id, - (conn_id, *id), - RPCCodedResponse::Error( - RPCResponseErrorCode::RateLimited, - format!("Wait {:?}", wait_time).into(), - ), - ); + // send an error code to the peer. + // the handler upon receiving the error code will send it back to the behaviour + self.send_response( + peer_id, + (conn_id, *id), + RPCCodedResponse::Error( + RPCResponseErrorCode::RateLimited, + format!("Wait {:?}", wait_time).into(), + ), + ); + } } + } else { + // No rate limiting, send the event to the user + self.events + .push(NetworkBehaviourAction::GenerateEvent(RPCMessage { + peer_id, + conn_id, + event, + })) } } else { self.events @@ -303,7 +304,9 @@ where _: &mut impl PollParameters, ) -> Poll> { // let the rate limiter prune. - let _ = self.limiter.poll_unpin(cx); + if let Some(limiter) = self.limiter.as_mut() { + let _ = limiter.poll_unpin(cx); + } if let Some(self_limiter) = self.self_limiter.as_mut() { if let Poll::Ready(event) = self_limiter.poll_ready(cx) { diff --git a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs index a1f7b89a2f..1fdc6cce3b 100644 --- a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs @@ -1,3 +1,4 @@ +use super::config::RateLimiterConfig; use crate::rpc::Protocol; use fnv::FnvHashMap; use libp2p::PeerId; @@ -141,29 +142,6 @@ impl RPCRateLimiterBuilder { self } - /// Allow one token every `time_period` to be used for this `protocol`. - /// This produces a hard limit. - pub fn one_every(self, protocol: Protocol, time_period: Duration) -> Self { - self.set_quota( - protocol, - Quota { - replenish_all_every: time_period, - max_tokens: 1, - }, - ) - } - - /// Allow `n` tokens to be use used every `time_period` for this `protocol`. - pub fn n_every(self, protocol: Protocol, n: u64, time_period: Duration) -> Self { - self.set_quota( - protocol, - Quota { - max_tokens: n, - replenish_all_every: time_period, - }, - ) - } - pub fn build(self) -> Result { // get our quotas let ping_quota = self.ping_quota.ok_or("Ping quota not specified")?; @@ -232,6 +210,29 @@ impl RateLimiterItem for super::OutboundRequest { } } impl RPCRateLimiter { + pub fn new_with_config(config: RateLimiterConfig) -> Result { + // Destructure to make sure every configuration value is used. + let RateLimiterConfig { + ping_quota, + meta_data_quota, + status_quota, + goodbye_quota, + blocks_by_range_quota, + blocks_by_root_quota, + light_client_bootstrap_quota, + } = config; + + Self::builder() + .set_quota(Protocol::Ping, ping_quota) + .set_quota(Protocol::MetaData, meta_data_quota) + .set_quota(Protocol::Status, status_quota) + .set_quota(Protocol::Goodbye, goodbye_quota) + .set_quota(Protocol::BlocksByRange, blocks_by_range_quota) + .set_quota(Protocol::BlocksByRoot, blocks_by_root_quota) + .set_quota(Protocol::LightClientBootstrap, light_client_bootstrap_quota) + .build() + } + /// Get a builder instance. pub fn builder() -> RPCRateLimiterBuilder { RPCRateLimiterBuilder::default() diff --git a/beacon_node/lighthouse_network/src/rpc/self_limiter.rs b/beacon_node/lighthouse_network/src/rpc/self_limiter.rs index 451c6206f3..6748a1947b 100644 --- a/beacon_node/lighthouse_network/src/rpc/self_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/self_limiter.rs @@ -52,28 +52,7 @@ impl SelfRateLimiter { /// Creates a new [`SelfRateLimiter`] based on configration values. pub fn new(config: OutboundRateLimiterConfig, log: Logger) -> Result { debug!(log, "Using self rate limiting params"; "config" => ?config); - // Destructure to make sure every configuration value is used. - let OutboundRateLimiterConfig { - ping_quota, - meta_data_quota, - status_quota, - goodbye_quota, - blocks_by_range_quota, - blocks_by_root_quota, - } = config; - - let limiter = RateLimiter::builder() - .set_quota(Protocol::Ping, ping_quota) - .set_quota(Protocol::MetaData, meta_data_quota) - .set_quota(Protocol::Status, status_quota) - .set_quota(Protocol::Goodbye, goodbye_quota) - .set_quota(Protocol::BlocksByRange, blocks_by_range_quota) - .set_quota(Protocol::BlocksByRoot, blocks_by_root_quota) - // Manually set the LightClientBootstrap quota, since we use the same rate limiter for - // inbound and outbound requests, and the LightClientBootstrap is an only inbound - // protocol. - .one_every(Protocol::LightClientBootstrap, Duration::from_secs(10)) - .build()?; + let limiter = RateLimiter::new_with_config(config.0)?; Ok(SelfRateLimiter { delayed_requests: Default::default(), diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index f815e3bd36..34d5a56312 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -266,6 +266,7 @@ impl Network { let eth2_rpc = RPC::new( ctx.fork_context.clone(), config.enable_light_client_server, + config.inbound_rate_limiter_config.clone(), config.outbound_rate_limiter_config.clone(), log.clone(), ); diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 59a5f4b2e0..10d9ffafd4 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -282,7 +282,23 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { for a beacon node being referenced by validator client using the --proposer-node flag. This configuration is for enabling more secure setups.") .takes_value(false), ) - + .arg( + Arg::with_name("inbound-rate-limiter") + .long("inbound-rate-limiter") + .help( + "Configures the inbound rate limiter (requests received by this node).\ + \ + Rate limit quotas per protocol can be set in the form of \ + :/. To set quotas for multiple protocols, \ + separate them by ';'. If the inbound rate limiter is enabled and a protocol is not \ + present in the configuration, the default quotas will be used. \ + \ + This is enabled by default, using default quotas. To disable rate limiting pass \ + `disabled` to this option instead." + ) + .takes_value(true) + .hidden(true) + ) .arg( Arg::with_name("disable-backfill-rate-limiting") .long("disable-backfill-rate-limiting") diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 6f626bee8d..92e8228190 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -1232,6 +1232,7 @@ pub fn set_network_config( // Light client server config. config.enable_light_client_server = cli_args.is_present("light-client-server"); + // The self limiter is disabled by default. // This flag can be used both with or without a value. Try to parse it first with a value, if // no value is defined but the flag is present, use the default params. config.outbound_rate_limiter_config = clap_utils::parse_optional(cli_args, "self-limiter")?; @@ -1252,7 +1253,22 @@ pub fn set_network_config( config.proposer_only = true; warn!(log, "Proposer-only mode enabled"; "info"=> "Do not connect a validator client to this node unless via the --proposer-nodes flag"); } - + // The inbound rate limiter is enabled by default unless `disabled` is passed to the + // `inbound-rate-limiter` flag. Any other value should be parsed as a configuration string. + config.inbound_rate_limiter_config = match cli_args.value_of("inbound-rate-limiter") { + None => { + // Enabled by default, with default values + Some(Default::default()) + } + Some("disabled") => { + // Explicitly disabled + None + } + Some(config_str) => { + // Enabled with a custom configuration + Some(config_str.parse()?) + } + }; Ok(()) } diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 75bcccc9de..73520dd6b0 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -1451,6 +1451,26 @@ fn empty_self_limiter_flag() { ) }); } + +#[test] +fn empty_inbound_rate_limiter_flag() { + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| { + assert_eq!( + config.network.inbound_rate_limiter_config, + Some(lighthouse_network::rpc::config::InboundRateLimiterConfig::default()) + ) + }); +} +#[test] +fn disable_inbound_rate_limiter_flag() { + CommandLineTest::new() + .flag("inbound-rate-limiter", Some("disabled")) + .run_with_zero_port() + .with_config(|config| assert_eq!(config.network.inbound_rate_limiter_config, None)); +} + #[test] fn http_allow_origin_flag() { CommandLineTest::new() From 4af4e98c829ce3d27db22dc02842c7b48f22c217 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 2 Jun 2023 03:17:39 +0000 Subject: [PATCH 31/63] Update Nethermind (#4361) ## Issue Addressed Nethermind integration tests are failing with a compilation error: https://github.com/sigp/lighthouse/actions/runs/5138586473/jobs/9248079945 This PR updates Nethermind to the latest release to hopefully fix that --- testing/execution_engine_integration/src/nethermind.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/execution_engine_integration/src/nethermind.rs b/testing/execution_engine_integration/src/nethermind.rs index 485485c6fe..8925f1cc84 100644 --- a/testing/execution_engine_integration/src/nethermind.rs +++ b/testing/execution_engine_integration/src/nethermind.rs @@ -11,7 +11,7 @@ use unused_port::unused_tcp4_port; /// We've pinned the Nethermind version since our method of using the `master` branch to /// find the latest tag isn't working. It appears Nethermind don't always tag on `master`. /// We should fix this so we always pull the latest version of Nethermind. -const NETHERMIND_BRANCH: &str = "release/1.17.1"; +const NETHERMIND_BRANCH: &str = "release/1.18.2"; const NETHERMIND_REPO_URL: &str = "https://github.com/NethermindEth/nethermind"; fn build_result(repo_dir: &Path) -> Output { From d07c78bccf20795de89ed9ba3aae9c4dc81d9e32 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Fri, 2 Jun 2023 03:17:40 +0000 Subject: [PATCH 32/63] Appease clippy in Rust 1.70 (#4365) ## Issue Addressed NA ## Proposed Changes Fixes some new clippy lints raised after updating to Rust 1.70. ## Additional Info NA --- beacon_node/client/src/builder.rs | 2 +- beacon_node/http_metrics/tests/tests.rs | 2 +- .../lighthouse_network/src/discovery/subnet_predicate.rs | 2 +- common/lru_cache/src/time.rs | 2 +- validator_client/src/lib.rs | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index 215244b9ba..e05b92a277 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -742,7 +742,7 @@ where runtime_context .executor - .spawn_without_exit(async move { server.await }, "http-metrics"); + .spawn_without_exit(server, "http-metrics"); Some(listen_addr) } else { diff --git a/beacon_node/http_metrics/tests/tests.rs b/beacon_node/http_metrics/tests/tests.rs index 89fde32374..b88a790afd 100644 --- a/beacon_node/http_metrics/tests/tests.rs +++ b/beacon_node/http_metrics/tests/tests.rs @@ -38,7 +38,7 @@ async fn returns_200_ok() { }; let (listening_socket, server) = http_metrics::serve(ctx, server_shutdown).unwrap(); - tokio::spawn(async { server.await }); + tokio::spawn(server); let url = format!( "http://{}:{}/metrics", diff --git a/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs b/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs index e324532f7b..f79ff8daf6 100644 --- a/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs +++ b/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs @@ -1,4 +1,4 @@ -///! The subnet predicate used for searching for a particular subnet. +//! The subnet predicate used for searching for a particular subnet. use super::*; use crate::types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield}; use slog::trace; diff --git a/common/lru_cache/src/time.rs b/common/lru_cache/src/time.rs index 7b8e9ba9a8..966741ca4d 100644 --- a/common/lru_cache/src/time.rs +++ b/common/lru_cache/src/time.rs @@ -1,4 +1,4 @@ -///! This implements a time-based LRU cache for fast checking of duplicates +//! This implements a time-based LRU cache for fast checking of duplicates use fnv::FnvHashSet; use std::collections::VecDeque; use std::time::{Duration, Instant}; diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 3dde49f227..6e4a8da6ac 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -146,7 +146,7 @@ impl ProductionValidatorClient { context .clone() .executor - .spawn_without_exit(async move { server.await }, "metrics-api"); + .spawn_without_exit(server, "metrics-api"); Some(ctx) } else { @@ -590,7 +590,7 @@ impl ProductionValidatorClient { self.context .clone() .executor - .spawn_without_exit(async move { server.await }, "http-api"); + .spawn_without_exit(server, "http-api"); Some(listen_addr) } else { From b14d1493cc69d7179bfccac72663e8a67966fff0 Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 7 Jun 2023 01:50:31 +0000 Subject: [PATCH 33/63] Always log the value of relay and local blocks for comparison (#4352) ## Issue Addressed N/A ## Proposed Changes This change will log the value of the relay block and the local block when the relay block is more profitable. ## Additional Info This change will help validators understand the block selection (as it looks like the execution reward sometimes is higher that the MEV-reward). The rationale for this change is to aid operators to better understand why a relay-block was chosen over a local block. Looking at produced blocks (at beaconcha.in for example) it sometimes looks like the builder is making a profit just from the execution reward vs the MEV-reward, and creates the nagging question: "Could i have built this block and made that extra profit?"... The answer is probably "No, not without the extra transactions included by the relay", but by logging the value of the local block-candidate, this will no longer be an issue.. ### Example (Mainnet) https://beaconcha.in/block/17370329 MEV Block Reward: 0.17122 Ether to 0xE35bBaFa0266089f95d745d348b468622805D82B Execution Reward: 0.17528 Ether to 0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326 Difference: 0.00406 Ether ### Examples (Goerli) https://goerli.beaconcha.in/block/9040065 MEV Block Reward: 0.56423 Ether to 0xF5794543CF6055Ae710E9c8E99E31343Cea004a8 Execution Reward: 0.56488 Ether to 0xfC0157aA4F5DB7177830ACddB3D5a9BB5BE9cc5e Difference: 0.00065 Ether https://goerli.beaconcha.in/block/9019921 MEV Block Reward: 1.39440 Ether to 0xF5794543CF6055Ae710E9c8E99E31343Cea004a8 Execution Reward: 1.39469 Ether to 0xfC0157aA4F5DB7177830ACddB3D5a9BB5BE9cc5e Difference: 0.00029 Ether https://goerli.beaconcha.in/block/9015583 MEV Block Reward: 1.04356 Ether to 0xF5794543CF6055Ae710E9c8E99E31343Cea004a8 Execution Reward: 1.04896 Ether to 0xfC0157aA4F5DB7177830ACddB3D5a9BB5BE9cc5e Difference: 0.0054 Ether --- beacon_node/execution_layer/src/lib.rs | 27 ++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 51b681b219..0e9df7a50d 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -826,16 +826,23 @@ impl ExecutionLayer { let relay_value = relay.data.message.value; let local_value = *local.block_value(); - if !self.inner.always_prefer_builder_payload - && local_value >= relay_value - { - info!( - self.log(), - "Local block is more profitable than relay block"; - "local_block_value" => %local_value, - "relay_value" => %relay_value - ); - return Ok(ProvenancedPayload::Local(local)); + if !self.inner.always_prefer_builder_payload { + if local_value >= relay_value { + info!( + self.log(), + "Local block is more profitable than relay block"; + "local_block_value" => %local_value, + "relay_value" => %relay_value + ); + return Ok(ProvenancedPayload::Local(local)); + } else { + info!( + self.log(), + "Relay block is more profitable than local block"; + "local_block_value" => %local_value, + "relay_value" => %relay_value + ); + } } match verify_builder_bid( From 299cfe1fe623a390627a15e7aa36ccb88b993ce9 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Wed, 7 Jun 2023 01:50:33 +0000 Subject: [PATCH 34/63] Switch default slasher backend to LMDB (#4360) ## Issue Addressed Closes #4354 Closes #3987 Replaces #4305, #4283 ## Proposed Changes This switches the default slasher backend _back_ to LMDB. If an MDBX database exists and the MDBX backend is enabled then MDBX will continue to be used. Our release binaries and Docker images will continue to include MDBX for as long as it is practical, so users of these should not notice any difference. The main benefit is to users compiling from source and devs running tests. These users no longer have to struggle to compile MDBX and deal with the compatibility issues that arises. Similarly, devs don't need to worry about toggling feature flags in tests or risk forgetting to run the slasher tests due to backend issues. --- Makefile | 5 ++- beacon_node/Cargo.toml | 2 +- beacon_node/beacon_chain/Cargo.toml | 2 +- beacon_node/client/Cargo.toml | 2 +- beacon_node/src/lib.rs | 24 +++++++++++- beacon_node/store/Cargo.toml | 2 +- book/src/installation-source.md | 12 +++--- book/src/slasher.md | 44 +++++++++++++++++----- lighthouse/Cargo.toml | 4 +- lighthouse/tests/beacon_node.rs | 23 +++++++++--- slasher/Cargo.toml | 2 +- slasher/service/Cargo.toml | 2 +- slasher/src/config.rs | 36 +++++++++++++++++- slasher/src/lib.rs | 2 +- slasher/tests/backend.rs | 57 +++++++++++++++++++++++++++++ 15 files changed, 184 insertions(+), 35 deletions(-) create mode 100644 slasher/tests/backend.rs diff --git a/Makefile b/Makefile index 89362d12d8..8e7f3fc326 100644 --- a/Makefile +++ b/Makefile @@ -145,8 +145,9 @@ test-op-pool-%: # Run the tests in the `slasher` crate for all supported database backends. test-slasher: - cargo test --release -p slasher --features mdbx - cargo test --release -p slasher --no-default-features --features lmdb + cargo test --release -p slasher --features lmdb + cargo test --release -p slasher --no-default-features --features mdbx + cargo test --release -p slasher --features lmdb,mdbx # both backends enabled # Runs only the tests/state_transition_vectors tests. run-state-transition-tests: diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index acf373070e..67bb2e5e1d 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -36,7 +36,7 @@ clap_utils = { path = "../common/clap_utils" } hyper = "0.14.4" lighthouse_version = { path = "../common/lighthouse_version" } hex = "0.4.2" -slasher = { path = "../slasher", default-features = false } +slasher = { path = "../slasher" } monitoring_api = { path = "../common/monitoring_api" } sensitive_url = { path = "../common/sensitive_url" } http_api = { path = "http_api" } diff --git a/beacon_node/beacon_chain/Cargo.toml b/beacon_node/beacon_chain/Cargo.toml index d11d507e48..27d07e3338 100644 --- a/beacon_node/beacon_chain/Cargo.toml +++ b/beacon_node/beacon_chain/Cargo.toml @@ -54,7 +54,7 @@ fork_choice = { path = "../../consensus/fork_choice" } task_executor = { path = "../../common/task_executor" } derivative = "2.1.1" itertools = "0.10.0" -slasher = { path = "../../slasher", default-features = false } +slasher = { path = "../../slasher" } eth2 = { path = "../../common/eth2" } strum = { version = "0.24.0", features = ["derive"] } logging = { path = "../../common/logging" } diff --git a/beacon_node/client/Cargo.toml b/beacon_node/client/Cargo.toml index d39bb2e3e2..64c79ea668 100644 --- a/beacon_node/client/Cargo.toml +++ b/beacon_node/client/Cargo.toml @@ -39,7 +39,7 @@ time = "0.3.5" directory = {path = "../../common/directory"} http_api = { path = "../http_api" } http_metrics = { path = "../http_metrics" } -slasher = { path = "../../slasher", default-features = false } +slasher = { path = "../../slasher" } slasher_service = { path = "../../slasher/service" } monitoring_api = {path = "../../common/monitoring_api"} execution_layer = { path = "../execution_layer" } diff --git a/beacon_node/src/lib.rs b/beacon_node/src/lib.rs index 650763dcaf..47694825ca 100644 --- a/beacon_node/src/lib.rs +++ b/beacon_node/src/lib.rs @@ -16,7 +16,7 @@ pub use client::{Client, ClientBuilder, ClientConfig, ClientGenesis}; pub use config::{get_config, get_data_dir, get_slots_per_restore_point, set_network_config}; use environment::RuntimeContext; pub use eth2_config::Eth2Config; -use slasher::Slasher; +use slasher::{DatabaseBackendOverride, Slasher}; use slog::{info, warn}; use std::ops::{Deref, DerefMut}; use std::sync::Arc; @@ -86,7 +86,27 @@ impl ProductionBeaconNode { .http_api_config(client_config.http_api.clone()) .disk_store(&db_path, &freezer_db_path, store_config, log.clone())?; - let builder = if let Some(slasher_config) = client_config.slasher.clone() { + let builder = if let Some(mut slasher_config) = client_config.slasher.clone() { + match slasher_config.override_backend() { + DatabaseBackendOverride::Success(old_backend) => { + info!( + log, + "Slasher backend overriden"; + "reason" => "database exists", + "configured_backend" => %old_backend, + "override_backend" => %slasher_config.backend, + ); + } + DatabaseBackendOverride::Failure(path) => { + warn!( + log, + "Slasher backend override failed"; + "advice" => "delete old MDBX database or enable MDBX backend", + "path" => path.display() + ); + } + _ => {} + } let slasher = Arc::new( Slasher::open(slasher_config, log.new(slog::o!("service" => "slasher"))) .map_err(|e| format!("Slasher open error: {:?}", e))?, diff --git a/beacon_node/store/Cargo.toml b/beacon_node/store/Cargo.toml index a1c65bd26d..a952f1b2ff 100644 --- a/beacon_node/store/Cargo.toml +++ b/beacon_node/store/Cargo.toml @@ -10,7 +10,7 @@ beacon_chain = {path = "../beacon_chain"} [dependencies] db-key = "0.0.5" -leveldb = { version = "0.8.6", default-features = false } +leveldb = { version = "0.8.6" } parking_lot = "0.12.0" itertools = "0.10.0" ethereum_ssz = "0.5.0" diff --git a/book/src/installation-source.md b/book/src/installation-source.md index b9c9df163d..1504b7ff0f 100644 --- a/book/src/installation-source.md +++ b/book/src/installation-source.md @@ -5,7 +5,7 @@ the instructions below, and then proceed to [Building Lighthouse](#build-lightho ## Dependencies -First, **install Rust** using [rustup](https://rustup.rs/): +First, **install Rust** using [rustup](https://rustup.rs/): ```bash curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh @@ -64,10 +64,10 @@ After this, you are ready to [build Lighthouse](#build-lighthouse). 1. Install [Git](https://git-scm.com/download/win). 1. Install the [Chocolatey](https://chocolatey.org/install) package manager for Windows. - > Tips: + > Tips: > - Use PowerShell to install. In Windows, search for PowerShell and run as administrator. > - You must ensure `Get-ExecutionPolicy` is not Restricted. To test this, run `Get-ExecutionPolicy` in PowerShell. If it returns `restricted`, then run `Set-ExecutionPolicy AllSigned`, and then run - ```bash + ```bash Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) ``` > - To verify that Chocolatey is ready, run `choco` and it should return the version. @@ -159,13 +159,13 @@ Commonly used features include: * `gnosis`: support for the Gnosis Beacon Chain. * `portable`: support for legacy hardware. * `modern`: support for exclusively modern hardware. -* `slasher-mdbx`: support for the MDBX slasher backend. Enabled by default. -* `slasher-lmdb`: support for the LMDB slasher backend. +* `slasher-lmdb`: support for the LMDB slasher backend. Enabled by default. +* `slasher-mdbx`: support for the MDBX slasher backend. * `jemalloc`: use [`jemalloc`][jemalloc] to allocate memory. Enabled by default on Linux and macOS. Not supported on Windows. * `spec-minimal`: support for the minimal preset (useful for testing). -Default features (e.g. `slasher-mdbx`) may be opted out of using the `--no-default-features` +Default features (e.g. `slasher-lmdb`) may be opted out of using the `--no-default-features` argument for `cargo`, which can be plumbed in via the `CARGO_INSTALL_EXTRA_FLAGS` environment variable. E.g. diff --git a/book/src/slasher.md b/book/src/slasher.md index ecf9d34efd..41bc3baf7e 100644 --- a/book/src/slasher.md +++ b/book/src/slasher.md @@ -46,23 +46,49 @@ directory. * Flag: `--slasher-backend NAME` * Argument: one of `mdbx`, `lmdb` or `disabled` -* Default: `mdbx` +* Default: `lmdb` for new installs, `mdbx` if an MDBX database already exists -Since Lighthouse v2.6.0 it is possible to use one of several database backends with the slasher: +It is possible to use one of several database backends with the slasher: -- MDBX (default) -- LMDB +- LMDB (default) +- MDBX The advantage of MDBX is that it performs compaction, resulting in less disk usage over time. The -disadvantage is that upstream MDBX has removed support for Windows and macOS, so Lighthouse is stuck -on an older version. If bugs are found in our pinned version of MDBX it may be deprecated in future. +disadvantage is that upstream MDBX is unstable, so Lighthouse is pinned to a specific version. +If bugs are found in our pinned version of MDBX it may be deprecated in future. -LMDB does not have compaction but is more stable upstream than MDBX. It is not currently recommended -to use the LMDB backend on Windows. +LMDB does not have compaction but is more stable upstream than MDBX. If running with the LMDB +backend on Windows it is recommended to allow extra space due to this issue: +[sigp/lighthouse#2342](https://github.com/sigp/lighthouse/issues/2342). More backends may be added in future. -### Switching Backends +#### Backend Override + +The default backend was changed from MDBX to LMDB in Lighthouse v4.3.0. + +If an MDBX database is already found on disk, then Lighthouse will try to use it. This will result +in a log at start-up: + +``` +INFO Slasher backend overriden reason: database exists, configured_backend: lmdb, overriden_backend: mdbx +``` + +If the running Lighthouse binary doesn't have the MDBX backend enabled but an existing database is +found, then a warning will be logged and Lighthouse will use the LMDB backend and create a new database: + +``` +WARN Slasher backend override failed advice: delete old MDBX database or enable MDBX backend, path: /home/user/.lighthouse/mainnet/beacon/slasher_db/mdbx.dat +``` + +In this case you should either obtain a Lighthouse binary with the MDBX backend enabled, or delete +the files for the old backend. The pre-built Lighthouse binaries and Docker images have MDBX enabled, +or if you're [building from source](./installation-source.md) you can enable the `slasher-mdbx` feature. + +To delete the files, use the `path` from the `WARN` log, and then delete the `mbdx.dat` and +`mdbx.lck` files. + +#### Switching Backends If you change database backends and want to reclaim the space used by the old backend you can delete the following files from your `slasher_db` directory: diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 5d2b5e092f..bbde006efc 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -7,7 +7,7 @@ autotests = false rust-version = "1.68.2" [features] -default = ["slasher-mdbx"] +default = ["slasher-lmdb"] # Writes debugging .ssz files to /tmp during block processing. write_ssz_files = ["beacon_node/write_ssz_files"] # Compiles the BLS crypto code so that the binary is portable across machines. @@ -55,7 +55,7 @@ malloc_utils = { path = "../common/malloc_utils" } directory = { path = "../common/directory" } unused_port = { path = "../common/unused_port" } database_manager = { path = "../database_manager" } -slasher = { path = "../slasher", default-features = false } +slasher = { path = "../slasher" } [dev-dependencies] tempfile = "3.1.0" diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 73520dd6b0..a71a27bdba 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -1767,10 +1767,12 @@ fn no_reconstruct_historic_states_flag() { } // Tests for Slasher flags. +// Using `--slasher-max-db-size` to work around https://github.com/sigp/lighthouse/issues/2342 #[test] fn slasher_flag() { CommandLineTest::new() .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) .run_with_zero_port() .with_config_and_dir(|config, dir| { if let Some(slasher_config) = &config.slasher { @@ -1788,6 +1790,7 @@ fn slasher_dir_flag() { let dir = TempDir::new().expect("Unable to create temporary directory"); CommandLineTest::new() .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) .flag("slasher-dir", dir.path().as_os_str().to_str()) .run_with_zero_port() .with_config(|config| { @@ -1802,6 +1805,7 @@ fn slasher_dir_flag() { fn slasher_update_period_flag() { CommandLineTest::new() .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) .flag("slasher-update-period", Some("100")) .run_with_zero_port() .with_config(|config| { @@ -1816,6 +1820,7 @@ fn slasher_update_period_flag() { fn slasher_slot_offset_flag() { CommandLineTest::new() .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) .flag("slasher-slot-offset", Some("11.25")) .run_with_zero_port() .with_config(|config| { @@ -1828,6 +1833,7 @@ fn slasher_slot_offset_flag() { fn slasher_slot_offset_nan_flag() { CommandLineTest::new() .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) .flag("slasher-slot-offset", Some("NaN")) .run_with_zero_port(); } @@ -1835,6 +1841,7 @@ fn slasher_slot_offset_nan_flag() { fn slasher_history_length_flag() { CommandLineTest::new() .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) .flag("slasher-history-length", Some("2048")) .run_with_zero_port() .with_config(|config| { @@ -1849,20 +1856,21 @@ fn slasher_history_length_flag() { fn slasher_max_db_size_flag() { CommandLineTest::new() .flag("slasher", None) - .flag("slasher-max-db-size", Some("10")) + .flag("slasher-max-db-size", Some("2")) .run_with_zero_port() .with_config(|config| { let slasher_config = config .slasher .as_ref() .expect("Unable to parse Slasher config"); - assert_eq!(slasher_config.max_db_size_mbs, 10240); + assert_eq!(slasher_config.max_db_size_mbs, 2048); }); } #[test] fn slasher_attestation_cache_size_flag() { CommandLineTest::new() .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) .flag("slasher-att-cache-size", Some("10000")) .run_with_zero_port() .with_config(|config| { @@ -1877,6 +1885,7 @@ fn slasher_attestation_cache_size_flag() { fn slasher_chunk_size_flag() { CommandLineTest::new() .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) .flag("slasher-chunk-size", Some("32")) .run_with_zero_port() .with_config(|config| { @@ -1891,6 +1900,7 @@ fn slasher_chunk_size_flag() { fn slasher_validator_chunk_size_flag() { CommandLineTest::new() .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) .flag("slasher-validator-chunk-size", Some("512")) .run_with_zero_port() .with_config(|config| { @@ -1905,6 +1915,7 @@ fn slasher_validator_chunk_size_flag() { fn slasher_broadcast_flag() { CommandLineTest::new() .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) .flag("slasher-broadcast", None) .run_with_zero_port() .with_config(|config| { @@ -1920,10 +1931,11 @@ fn slasher_broadcast_flag() { fn slasher_backend_default() { CommandLineTest::new() .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) .run_with_zero_port() .with_config(|config| { let slasher_config = config.slasher.as_ref().unwrap(); - assert_eq!(slasher_config.backend, slasher::DatabaseBackend::Mdbx); + assert_eq!(slasher_config.backend, slasher::DatabaseBackend::Lmdb); }); } @@ -1933,11 +1945,12 @@ fn slasher_backend_override_to_default() { // called "disabled" results in a panic. CommandLineTest::new() .flag("slasher", None) - .flag("slasher-backend", Some("mdbx")) + .flag("slasher-max-db-size", Some("1")) + .flag("slasher-backend", Some("lmdb")) .run_with_zero_port() .with_config(|config| { let slasher_config = config.slasher.as_ref().unwrap(); - assert_eq!(slasher_config.backend, slasher::DatabaseBackend::Mdbx); + assert_eq!(slasher_config.backend, slasher::DatabaseBackend::Lmdb); }); } diff --git a/slasher/Cargo.toml b/slasher/Cargo.toml index 7f2ac456b5..bfa7b5f64c 100644 --- a/slasher/Cargo.toml +++ b/slasher/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Michael Sproul "] edition = "2021" [features] -default = ["mdbx"] +default = ["lmdb"] mdbx = ["dep:mdbx"] lmdb = ["lmdb-rkv", "lmdb-rkv-sys"] diff --git a/slasher/service/Cargo.toml b/slasher/service/Cargo.toml index 0a787defa2..63cf1e4649 100644 --- a/slasher/service/Cargo.toml +++ b/slasher/service/Cargo.toml @@ -9,7 +9,7 @@ beacon_chain = { path = "../../beacon_node/beacon_chain" } directory = { path = "../../common/directory" } lighthouse_network = { path = "../../beacon_node/lighthouse_network" } network = { path = "../../beacon_node/network" } -slasher = { path = "..", default-features = false } +slasher = { path = ".." } slog = "2.5.2" slot_clock = { path = "../../common/slot_clock" } state_processing = { path = "../../consensus/state_processing" } diff --git a/slasher/src/config.rs b/slasher/src/config.rs index e2a58a406a..361621d176 100644 --- a/slasher/src/config.rs +++ b/slasher/src/config.rs @@ -13,15 +13,16 @@ pub const DEFAULT_MAX_DB_SIZE: usize = 256 * 1024; // 256 GiB pub const DEFAULT_ATTESTATION_ROOT_CACHE_SIZE: usize = 100_000; pub const DEFAULT_BROADCAST: bool = false; -#[cfg(feature = "mdbx")] +#[cfg(all(feature = "mdbx", not(feature = "lmdb")))] pub const DEFAULT_BACKEND: DatabaseBackend = DatabaseBackend::Mdbx; -#[cfg(all(feature = "lmdb", not(feature = "mdbx")))] +#[cfg(feature = "lmdb")] pub const DEFAULT_BACKEND: DatabaseBackend = DatabaseBackend::Lmdb; #[cfg(not(any(feature = "mdbx", feature = "lmdb")))] pub const DEFAULT_BACKEND: DatabaseBackend = DatabaseBackend::Disabled; pub const MAX_HISTORY_LENGTH: usize = 1 << 16; pub const MEGABYTE: usize = 1 << 20; +pub const MDBX_DATA_FILENAME: &str = "mdbx.dat"; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { @@ -64,6 +65,13 @@ pub enum DatabaseBackend { Disabled, } +#[derive(Debug, PartialEq)] +pub enum DatabaseBackendOverride { + Success(DatabaseBackend), + Failure(PathBuf), + Noop, +} + impl Config { pub fn new(database_path: PathBuf) -> Self { Self { @@ -161,4 +169,28 @@ impl Config { .filter(move |v| self.validator_chunk_index(**v) == validator_chunk_index) .copied() } + + pub fn override_backend(&mut self) -> DatabaseBackendOverride { + let mdbx_path = self.database_path.join(MDBX_DATA_FILENAME); + + #[cfg(feature = "mdbx")] + let already_mdbx = self.backend == DatabaseBackend::Mdbx; + #[cfg(not(feature = "mdbx"))] + let already_mdbx = false; + + if !already_mdbx && mdbx_path.exists() { + #[cfg(feature = "mdbx")] + { + let old_backend = self.backend; + self.backend = DatabaseBackend::Mdbx; + DatabaseBackendOverride::Success(old_backend) + } + #[cfg(not(feature = "mdbx"))] + { + DatabaseBackendOverride::Failure(mdbx_path) + } + } else { + DatabaseBackendOverride::Noop + } + } } diff --git a/slasher/src/lib.rs b/slasher/src/lib.rs index 132ce8b235..45cbef84f2 100644 --- a/slasher/src/lib.rs +++ b/slasher/src/lib.rs @@ -21,7 +21,7 @@ pub use crate::slasher::Slasher; pub use attestation_queue::{AttestationBatch, AttestationQueue, SimpleBatch}; pub use attester_record::{AttesterRecord, CompactAttesterRecord, IndexedAttesterRecord}; pub use block_queue::BlockQueue; -pub use config::{Config, DatabaseBackend}; +pub use config::{Config, DatabaseBackend, DatabaseBackendOverride}; pub use database::{ interface::{Database, Environment, RwTransaction}, IndexedAttestationId, SlasherDB, diff --git a/slasher/tests/backend.rs b/slasher/tests/backend.rs new file mode 100644 index 0000000000..9e68107de7 --- /dev/null +++ b/slasher/tests/backend.rs @@ -0,0 +1,57 @@ +#![cfg(all(feature = "lmdb"))] + +use slasher::{config::MDBX_DATA_FILENAME, Config, DatabaseBackend, DatabaseBackendOverride}; +use std::fs::File; +use tempfile::tempdir; + +#[test] +#[cfg(all(feature = "mdbx", feature = "lmdb"))] +fn override_no_existing_db() { + let tempdir = tempdir().unwrap(); + let mut config = Config::new(tempdir.path().into()); + assert_eq!(config.override_backend(), DatabaseBackendOverride::Noop); +} + +#[test] +#[cfg(all(feature = "mdbx", feature = "lmdb"))] +fn override_with_existing_mdbx_db() { + let tempdir = tempdir().unwrap(); + let mut config = Config::new(tempdir.path().into()); + + File::create(config.database_path.join(MDBX_DATA_FILENAME)).unwrap(); + + assert_eq!( + config.override_backend(), + DatabaseBackendOverride::Success(DatabaseBackend::Lmdb) + ); + assert_eq!(config.backend, DatabaseBackend::Mdbx); +} + +#[test] +#[cfg(all(feature = "mdbx", feature = "lmdb"))] +fn no_override_with_existing_mdbx_db() { + let tempdir = tempdir().unwrap(); + let mut config = Config::new(tempdir.path().into()); + config.backend = DatabaseBackend::Mdbx; + + File::create(config.database_path.join(MDBX_DATA_FILENAME)).unwrap(); + + assert_eq!(config.override_backend(), DatabaseBackendOverride::Noop); + assert_eq!(config.backend, DatabaseBackend::Mdbx); +} + +#[test] +#[cfg(all(not(feature = "mdbx"), feature = "lmdb"))] +fn failed_override_with_existing_mdbx_db() { + let tempdir = tempdir().unwrap(); + let mut config = Config::new(tempdir.path().into()); + + let filename = config.database_path.join(MDBX_DATA_FILENAME); + File::create(&filename).unwrap(); + + assert_eq!( + config.override_backend(), + DatabaseBackendOverride::Failure(filename) + ); + assert_eq!(config.backend, DatabaseBackend::Lmdb); +} From 5e3fb13cfe71c4c1aff6c82839a5b9a13db850ca Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 7 Jun 2023 01:50:34 +0000 Subject: [PATCH 35/63] Downgrade a `CRIT` in the VC for builder timeouts (#4366) ## Issue Addressed NA ## Proposed Changes Downgrade a `CRIT` to an `ERRO` when there's an `Irrecoverable` error whilst publishing a blinded block. It's quite common for builders successfully broadcast a block to the network whilst failing to respond to the BN when it publishes a signed, blinded block. The VC is currently raising a `CRIT` when this happens and I think that's excessive. These changes have the same intent as #4073. In that PR I only managed to remove the `CRIT`s in the BN but missed this one in the VC. I've also tidied the log messages to: - Give them all the same title (*"Error whilst producing block"*) to help with grepping. - Include the `block_slot` so it's easy to look up the slot in an explorer and see if it was actually skipped. ## Additional Info This PR should not change any logic beyond logging. --- validator_client/src/block_service.rs | 68 ++++++++++++++++++--------- 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/validator_client/src/block_service.rs b/validator_client/src/block_service.rs index 61a5a094cd..d22e6c95f3 100644 --- a/validator_client/src/block_service.rs +++ b/validator_client/src/block_service.rs @@ -338,35 +338,61 @@ impl BlockService { let log = log.clone(); self.inner.context.executor.spawn( async move { - let publish_result = if builder_proposals { - let mut result = service.clone() + if builder_proposals { + let result = service + .clone() .publish_block::>(slot, validator_pubkey) .await; - match result.as_ref() { + match result { Err(BlockError::Recoverable(e)) => { - error!(log, "Error whilst producing a blinded block, attempting to \ - publish full block"; "error" => ?e); - result = service + error!( + log, + "Error whilst producing block"; + "error" => ?e, + "block_slot" => ?slot, + "info" => "blinded proposal failed, attempting full block" + ); + if let Err(e) = service .publish_block::>(slot, validator_pubkey) - .await; - }, - Err(BlockError::Irrecoverable(e)) => { - error!(log, "Error whilst producing a blinded block, cannot fallback \ - because the block was signed"; "error" => ?e); - }, - _ => {}, + .await + { + // Log a `crit` since a full block + // (non-builder) proposal failed. + crit!( + log, + "Error whilst producing block"; + "error" => ?e, + "block_slot" => ?slot, + "info" => "full block attempted after a blinded failure", + ); + } + } + Err(BlockError::Irrecoverable(e)) => { + // Only log an `error` since it's common for + // builders to timeout on their response, only + // to publish the block successfully themselves. + error!( + log, + "Error whilst producing block"; + "error" => ?e, + "block_slot" => ?slot, + "info" => "this error may or may not result in a missed block", + ) + } + Ok(_) => {} }; - result - } else { - service - .publish_block::>(slot, validator_pubkey) - .await - }; - if let Err(e) = publish_result { + } else if let Err(e) = service + .publish_block::>(slot, validator_pubkey) + .await + { + // Log a `crit` since a full block (non-builder) + // proposal failed. crit!( log, "Error whilst producing block"; - "message" => ?e + "message" => ?e, + "block_slot" => ?slot, + "info" => "proposal did not use a builder", ); } }, From 186d0af873ca59fb5cf15341e6b8b2a5ec772384 Mon Sep 17 00:00:00 2001 From: Ricki Moore Date: Wed, 7 Jun 2023 01:50:35 +0000 Subject: [PATCH 36/63] feat: added new info about logs and config features (#4378) ## Proposed Changes Add additional information about Siren's new configuration, dashboard and logs view features. --- book/src/imgs/ui-dash-logs.png | Bin 0 -> 52479 bytes book/src/imgs/ui-logs.png | Bin 0 -> 201227 bytes book/src/ui-faqs.md | 10 ++++++++++ book/src/ui-usage.md | 16 ++++++++++++++++ 4 files changed, 26 insertions(+) create mode 100644 book/src/imgs/ui-dash-logs.png create mode 100644 book/src/imgs/ui-logs.png diff --git a/book/src/imgs/ui-dash-logs.png b/book/src/imgs/ui-dash-logs.png new file mode 100644 index 0000000000000000000000000000000000000000..3656ed5b2052c82e13cb0b00b6e91c0637210dff GIT binary patch literal 52479 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 c=b9DNcH{Ux{RAn7ulm! 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

7GdXHNl#EYN-w|mP`IzU z>UNsB5nX@T7d*o`eke5~p63FonBj9=A8K>PaaH=n1ojX36#8Y>GteQd-S-Odeg-_P z>8Z)U*qFSe_@8anEpJ+dMlqGPh9$HBPy0WaRYnX6=d5QgD1*KU|-VFm`RD z>+?s+W+l8od_SY;dC`{o4h=_*i5!nSFNxWfZ_FMAhsUw^qh4280%cdrZjUoCZ0=cecq0r zfZyn}lVT`hT3e0_sh zijk9?z)Cvvb1$mhX1y_3-v5G%3F`&@hKlL$BB5fYZ^+19rMZ)H74zKZL~cSHi}=LA zaLQ*F7mdi(KMUvQ8^)!uI6xJX(&}NYp3k0=Q`=o((H|1K;hJ$f^Fk8Ew}lXmGK4bE zJmK}MXfz)Y;Ro{@E)|6hr|Uak7Ua>;m@TjM^KUjItEI1(i}OCec-*+eQDq5bQ>s7l zI|udLYURYeE4cHg!t_%58)~bJeZr2!rQBphv|ca1)aN3DJTNnMDJ&|%&h=#EwmI@| z7YjKCL*yIeqKdb85+W50$UL^WDW%S0of>DgSI+{UJ!wBo9GGU0Vg8sLiNNJJ9_|E_ zY!Y=ON$jnP@*35o%R*Qh6a?J4er&c)YI`pE^uvfv_#uj63}a6#J&oy|?E|T9O644u zJ?aSUKhwv5apvP9BxIsRY-u?q6X7dk}*qwi%#6od~wKM-fiA~PO72%FjemL`b zZ@h?xCKAFEb8)PA;7#PKcu7GqAArhm$5*U7KZTpZ~b>|@+)Ff%Y*LW>r}&9jwj!jSyvj5~1hA+@j!!st7sx_;AT++_BuXwH0v-;M!P zOzRg^%+Mzgk;(DucDH@9P*?}<*}=&cP-Ei z5Ak2G6ST#U)}Aeph{tW;eP2P4G9kKBMl8A~=c5+=jJ+(S$}bovS`U&0?{Ps-plIzB zlpicJjTZ%iEbsoN&27ifqGC2JX2NqavjqRzWiqhE~#iY1dPv(9)D9|dk?%x}U6 zY8gHz?`$nboXL9Z2tS-sAf7%^d|(jMl6jxF^4p&rv9<-rriFiS#6G>luzk3pS8!CL zeA7iNKkt3p?M;Qf35FTB)wzEnZlKrO6O}deX=2q-ZUCJ2QtGO7aaa+_@LQ?U2e(s( zAX%zU)A+sskXU*P9SRO7n>pp91?+jyx(i3gI%B(%@m`^gl(Aj!>9Un+q2pP{PYx}D z$xu?kc!OYrFOL>Fag`rXI zI!4~y%hF>ho0(Ud+80NtiS--f6(4#_XqWf78Z3G2(#z|AS|(0cCSHuX8Ru5o6!RmN zSj{~YWCU4g8a`D{PJk>{e=PDzHk%n0p))~r;N83x^G$C%z9_h$B4~w4uW{iwMr`2^ zjF^B0Ol7FndK}rT(0MF|R2xXu8~;fWi>C3lDPSu6O%Xe}CYl`R;*NBR79lBO{GzgQ zBY#rFbOJ%4kE1@nnBnU8FN&BAk|Or=7e$QAKms#*zmR9z6K_t1I;;H;irDTiidbeW zz6@RTEyZCK)1rK0yF!xUMcvG5Jkjq zj@XnR!!M55?xn0KZtfG809RRJa=;PG7Hf6K@WE^N!D^XQAUp zS$owR$vNff$TF8YL}Xm$hxVG4uF5|+VrYNkh(Q$8Z_H~uIfgxZueJAD*L9w+Q~tPz1K3q!c#0zN z+EOCZKo@`R!hc#M1yJx(;KQV>tPp4{Ufzu@a)P?+oIY?^+953?`l4j#DOsvY7{Up@ ze0W{OgO7N%!(sLbmXc#Yq9&ntVtf#mci51mjssZIc@y?-PMeoWFviEnB0vRvB}uvN zu&3GfH?h!l4L|f1i@aN$HYT~e>o%%5w$yQ|*<^8U4rV$XZad-Q`5_Z;3jqdv@*JAHl&se;taCfeBBn!I!SRI~TBEQpxRbz{xU zDtY(is<5lS@`4R4X1U?eCf>-FFzf3e>-M}UwexxpAjO8=dqr@+cawYG_a=c0e}Lts zHk1<%)$wc}5sEFPrz64*B7OAiWz%_xr>T{ECh@ETc4o8{XyfC6U@VN|R_09qSlQ!O zf1bM{%X*-NBUNwH&ycVzzp~Nhl7FBS!F7?k#kYO)Ok_OkP`x-x)28%wczlwg){=No zMN+-Y4PBZ&$~nJQuZb+mfWo*4`Pz+zYIO0#u=-JwnsTbPHUPdDH{!l6J90Au=h=a@K~|NCG3MQyYEU#R^207ZuBh{|F7xJ&)2#9zSRD~)CxP1(2;wwsg67tlnClQgi zDtRowCHzQ_-7o)8I$72(m{!cV=%*23$FBBs6B$UObnRGG>keL~-M=@9rL{>O50>S+ z+xSM^vMsZ0?9FH}@y=dvno7Yi8AVuEjzv{v;zi3-{@NxCNP+8Sa=P(B>9?fdU&90W zZ&3>RioT!WCwIs3^yUy*_Jz6j%n>%8*&7bO#6!~GS%Ji)+V4`^=kG1$V>G_2anAqx zSsz)SXkF;$UYNvlE@U3xol&RBV)^4tCnL{v@}Y4y#cw<9zYZgexFLgH`MCA!SX)Y8pYAF!0i!6{KXz5D(^)D1|2!8QEJ zw}9kdufb>RGMlkmnKz&ikFq_eVrgC0Z@_puWG z`4LEQaJ6fpw${IX1o{&9eGK9Semj^G7EwH)M@9?BF78up?>zi-a{0w^<>?qX8+L%;l;^u`zfcn zn~sYNJqzI@k4KKx&p0>j#|CtO6aQ$&!oSjfd`QQ@I@=<8^_PM~dY>f6q`od6pvQ)Q z_RU5W0RUD*AOXil^1M87BgfI#Jox~0^EBx`zwRFH|^?Bcl;g<)ORF4 z-hbLz$&(jmRjAl30PbU&4?e!PgpebzTuu$T&e^c)3eTwx`*Fp%gDptzdUg&9vH2x? zAmHiI_Zm-O%mV9ekPMYt(JMzg3YyvLAQ-AlGgEGqH{e+L6xai6RHWph>8z0$ppZEb zI$UiJB*09Ypt(}_X{Uoz2(#EK9+wxPRad;kOn6Z`y$$`uz zEh(kv212Wqw&T1!G?uQy#)Iz|zjOO`d5)PJ1r1(0r*a$RgQ!zq^W?+TgCVC0H(P$M zs9S!X8Caah9&g--aNHfg-5M?nCuO5KblaJ4XHoa`CIQt@dOJX@DOTpG`V9KdmLOOn z`#Yf7b>8&`5V~<7(|(9-fhLGjN;bI4`@x5gAHQ0euzK-rYPKcvPD%ag_f5O56w#T< z`&LqM2|xnmO^VFGeGCMxM*6jmnd=b0KmbV_1N-ksf>vGl>w6%+i|RzA%pM3cGm#Lz z!7GO#`nilT;H=jHoXwQwmjR#0zn3-a4I-O&xbLy>sZ4=}L17lX0a-#IS_I%Zd%(f< z+*UgQ5nP;~10Svf&J?oX&;b}`Bwf9m8Rzo)xP?eW5)gyy&Quo|*NlMxuScL2hnttl zwk%u$h$wv}@`WFw&w&E;yO`&O9nkW+-)?_oJAaMz|O)=L02st#qN^XnH zjxPz4Kq=4ZH_`5P%N~AUL=wwD$v7a%m+`9Z7ymu)_QAt1l>xG!9+W-SWYX6wC;}HF z$-Z{Gwe>s#9-P2AJJPa!@KCn0rMJ2P{jov4$dncDR5eX=i4j!%d_^#sZ-M31Y$Ccs zYtDT)!glDadu?~KuvF+b})6pGNE%*5Z6I$MH|rG)I?^U@|i{w z4oEyV_84WsmjkFg*JMfG3VKjo2TlCk(Kz;d_$;-TNX`6=7tC}tOOf;xuvCfiLgk~V zh%w`_=$xd(QUeKsZ_lz^_#KS4Q7yHExCj!N^(+Mxj2spL#@T%ZD5I@q{X13%%DnGG z6a^DpkNh?Y#YevS31j1W@pcB?G90aaJ|3C|nZ%h|`&P(#HfV?#p{TTb7+bJvz3V?X z`I_*&FHaNg^kVjn{exj`iH|W3)U{`z>?^#|2HLvjNCeIrz_Q^eJ&9p83V8`OJKq6g zadjbL8)P2YzA7^)Q24m@khSMaEFLKfK#;w;9Qc{pdA9I%HH6fr}nac|+@BKpOCW2*zw2VNDAyS#QodHQ^>(xD@u zvA*y8d0fK|*Us@nQ?4zb zHQ<)O%KrMkO*Nnb2s+JV7m)G8PKp)V2N@}2vo5QY-G;Byin|E~q2-c?-@#3J&N*3h zGgfJn)=VnZ?d3OPk=`F%R3_G5SLTxwYp>tC_VZB~Si;&!HQ`TWS0*j*^PXr=B7_FU_iHUH9=f2r(Zt;_su0hfpO~kb|j(>k=8C^KURvCXN@RHma#GjF|IVc_h92a#&tjYO` zZX+(ftmHnvt>g+y05<0>5xH}V{Vza}LriumK{yatIpr^YszQ`{(rH7uvhq`mAWO*# z6S)NODxr5EOgWXvvdR})XMyR~9r}f*oIYnh6+mwB?c(!mVhl{utD22AoD7u5)1oZ` z1f8c~TrtJ*=TGQ$n^kq0rjav2^dSHgH`fLyR0@3jK*G<2_>NDW!fmsVbT6j*JcF$r zB+-+JcitMH&nf{K^JZL|msvo_W5*Q8rZ2iwZPX{U%9Rs&W{lO3C?9w+mAUI(^yxfwW+Wf;Bz7%zy_n4y z3p76^1sVD(6KyTj+sz~_qU)uJP#@HFVGHRc&fd^*=QjG_8PqlpkUZ4z7_c_5J9Kqo zWJ5d|{pyaCxHfX}D7yEGi{3q>J=_M=Prg4`aU1+Wy=b}onVW>HC78m>X7RK$%$J*a^iZWP`Y12JtauXNb|0IP(d1&Y$t2Ia~7Yn zTtaG zSS0%60zHxznK`x*dNXm3#w-kffZ}jD-_32-ct^4tx~`I_3OQfabXSjCy@6J6S8iBd zGB}6lPhDKG9etGfxd_^I+S3a2OyV{W7=C@zsHM70T$JDF?AmV&(*0FAer+Xez8uph zD>7_}w=sj7YSdesLId&X855FQF|s8AGOnI0IIF*NT8!s2@%m$bFjm7UAo!5wqW&hH`ET~$V^qT7TGMdbw%bnetJJw=+Stx2FB>bm$;CH2y6zNFiMz9ZOhgt;XKW8NrEv| zoeajs;<&AdpA+oB`Iz9g*ZbzYGXhjjj>Di>7yjz2wqC|J)TXRhIQ?i(4kUB|Z`eml z@mY1d;JiD{0*}(Yl{F+Os+s7?&Bx+7rOw-RSZ#^#0~Rd0wUJY?HteH_+0PB75Rslvt^uu5wg8!|>DF{3kZM+r zg2e(oCMrI9#%zbZm?orIv#rUd|EdZ75K(iGX~Pc#>en! ztV?~}DvoKn-OcU=-jQpUnyGs*TE8sMyH3S>BI{}^R{fBzcsb0mIU7&+gPtzOXI)t+ z;t*M~r&=;&?-npk_{^L)Z|d}99Q6#<{3_LcuCm`5NNOX_i_I~Iz(E+$Grq?y*P{bT zgJ~*MMz-iCfHc-jV=4GIWo87)R&lU=>Ju;3qxjJ?Gu@I(m>p z&*g30+dK(zNV`{^=^atc`{^(yUJ1u}Zn56@Ru0ClR7RvH`eP!>I*C1d_X%+NF>>{I zAMSSjNO~{%)=xT+N?WAY;S693@pYmV?H`qqS+C>x5ybK8z39J~?xzjm?+}fvKxW)9datsqrs#(Mi6JD{jDeEX&brxK7nywbHsEO!aW=0X)kQ5~$dtnl9VM5!B4G$u zgdkapvsp&MABWy&OJdASoyNyBuH6v{WM>M+!Wl@$?C#e(93`n2*a`lXi3N z?dQy{MkWVT`sYw<=0j%BZuky#=(%Lf3e|9y^P*8q z12(5I*^mm^+pw&)c*O$$TWd+Rf!u!APiiEDd4OX!;f-sT=!{ff;RhAmW!s+3@I%y` zL|{79UBc_tO)rFB0RU7fQTwaz&q|5}V#06fAB@6RRRL%e_+8Z&YxLNZ|HGrYM zb(iu|($mBI)N(KS_Dr2udu^09EFf9R^N|qMDczjWTP5%7jq4y~>yHA@QPrZ3a7V7z zk89I9;&s`AYr&j6yHjpu#Bt7U=yRP9mj_O-MLXdWW#EcOn^m=F4J-+mMazev;!!Yi zO|>o%1l7=p9dUKaEq9p`a=!gj#iCqE-7~Eg+I4-%$Zei#y2d@kk@K!bXF!?P8fo&N ztMTbY5Pe=tf0{0reXFW!jqQU@23wJtwjvY9gDZwT%T~D#1Uzda0x7n9=T?VYGt%J~ z{6%da;lAyCzIg=HdHe{*WKEJ9J_IYiJB4!y+UY=GPRW~aW-?io>1%pE+o2l5MbTE* zmV#s=qM>Nl;yCf8KkVYKz2cjlbN`%{`@oqXa&KwJN7sgoMAu@$V^wx1x`sYA9@*1S zao85WFe-7Q>eM2;g-_GYh9=K=fA{P4xcKn0jvL|Hbt#;YhKtBKm5PGP7xyyH;Ew1* zLcJ7qco1QD#oMz3f3#D@Zpk*|zA@6-EMwWEr-fEeg|JyP-N;kh<_yWWA^pT;GG(WA zA8}fkWKKJtqWWPwwl#%ITrPCbZuAoMb;&>WbfyiCp~YZ6PxgZ1T8M43aP=F15%%Np zBwcEAwNFxTp79I3jkXE7{F6fGaa;2%l0tV*AJ{ki-H*>YxCzR!1y|cUkvhMtb8m~l zlBDsf1fq~H8{U?M;^0ygBP-J8%a=G`&1gjV4auTge25z`>D`sQV+5F#On53vY=gx{ z$I9Z1_(^eQO>}6KI0ht5zbQ z0U-y485ZwNWTAt)FEoU>F1`;wzG(-8KIbKU6*XF0uT0x)!lP)u!Skwb?9?kItN9T_ z?yVv>tBGe>3%BxlOnYW|N)b{tJls4@ve_loG=2%Olb+^m{`U4d(1sO#e~y@E`{Nj# zM{w3lmN`=Y7(f1f#?iH+`bX`v8F4$xc5hXacT4dK?6SuGs6xl1@A$3`bYu*7$(X6i zzHb(v$Ja2Pk!KUOk-Y~&MMCS0slrav8{ow)G)cFg;Nc#qL(c~S3>J|x$RG5c6E7tx zrv9MToqlA&M^ft?+T}zub$l5J>F@4DUU}(~3CnI)2u)L~@f5;=2dM2N!%g(y^+I;K zb&p=D)><>3I&bbLu&eb~vT?cQZBs%YNu7g|BUiF{; z$j9NtTwtUkDo@o^oqqQ{mo~e@Z|J%cdBfN~W=6$xn;n(3sa;i3FP#s<6KE$g5^xAY z5p6-@@I@KkOPLcIm5Qye4^f&EPLm4Ld$Kn_>xIM%&Xaa}uP_W<=zW;W;2johyXB;x zdZArV9dkbccB0$6irp>pB}7p&H3W9!Ij{YLJ<*4QsO}@DcGk{p{JdG8*dDNMP?7ef z_OxZfqrpf|Q8|*Q%NtOPfz5Gb0d!rVoi_d4#E?T(usz>9QQ#xBM5_%1EGHS*gMUjUsc57E_X`XZY7O6Z&s2!W+@)-7&AZ z4X|?#kvrtKUVFMwi>+g-xB8-jx1}`#zR-TeatOt&LeOu++n+_QeyM*JYpI{>J6X!| zjb=f6HUSQM6lr*~Vp-m;T-H8$Y%f!&Sky4jlKUA~ImK7bm<8;+pDY@_oo7XBgnl4} zkWry)u;n;HNVbZ@`w&Ddm>~1eT*aa4I@^kGe`(hf!bpalnF6#gf6(px0Nqa2%cDq* zZA#DcPpz2jd?K9yHyOUVN>$lx0$}xyV zTy>LIzH$MHzA`?wj%M-7F8jId!kh*W-fd#ujWY!-7NK+JRI@W1L3Rq=4%fmQJm+vJ z=Bg8#iQLDGPyBS6wT5#DvHe0|Pft`YG-}P4o_N)#9z5{~ZNJ+0g{5V$u(r$ll0(-x z{QaAudL`S+CGn;dq36{@qUJ?+4T<8*eA2-3h+*+@;s-QHi9S={=kdtLVv1k!tT!cj zR_GRHs`jMZ3?7`nK1$HOu@_!k{r>yA@2>6Sx^?b@b3689rw%I6 z0Qbi$DAtO{@AAa)4_J_maNvYw@|f{~fxg%TEPWnmm-t@ou!L}pim&8kHc5vC0@R%m z$!2Z8bxRXoCpnobEbNxGbSMT9y(-rvG3MR66gp*YR{X61DsryCBL!L|@wjz-k>h5Y zj*zmVvnCtHRRPQmHx%j#CWpo-s6JA$geDTxwvOzLb@J!*XU6y6}#0C?k>2G%NbFG{lf) zr|~l?$j0WMO*rk9IFeGrW_{tMqj}9+tZFu+E|x9t8uix8sWKP&T_oqO1aR)e;N{ns z`XnAYPgyD9ph9FX&rTUb0Y4<{+j&MJqN>5wYc-pBz@wMjf04o0 z7L?>6)X}&5lXO=qExO8yz1+X`lXORq=W(KNfvT$>d8$ZrqyDUW)F+M;yH0*VpIs9WqH`Z|lCwAe1&IVS%-b;<1K9e7!O$(|<2&>OCxhiL{ zJ?i$s;jqKMDR&D$DR&__v51B%AyRV~skDWO^Jubc1$gX?GZ{mGDqd$|cNuZ;j2ypA zy=p~CV$=GZla=)qS9qnNO7ynGE|Xy@4y&%*a~f@?v7ggRG0m*`S%@N6s(mVX9aa!1 z4|tOP2yf)-N+~wi@+CYxx`B82C*w}p;K3)Jqw^i^W7BL3Kh@c6g54A*YiG?3*eb`i zm%-ZmPn&xBVaS%?I#(1p0f&AHR>MB)e>femht}d zMs1Ozo|M+k!Q3fBmlwDEn}|^9AF9UrP_sCgty0@QWEb>q9jg$Cj9cKQp=YY(Gn_;u z9Qsr~kj0^(G_j$BrfxcNNNvkwDJTv<%vU>6^j5%Cua$bNEXF7)rc9n|?)lsI5p17np$u1J%S^Zyvk*vX zN%{(Q59Zd!flw>Zx?lLblBrN!#~F^5vX(z34A~>j@J@g8mXvW5>+^W;(ygk4*qsCl zfWgBgK!`*t*KsMrJ(bQr$w2X0Yd1N}?=Zd!AJD?fW$=blp74B$=RFyMSYGZV*5AO4 zoFa^p0Bu4#qx&x?D<_{nNNJ7Z3mJ&)K*;w;ZBn|be=!7wU{^&yoE2({0mbv{Q4fz& zQ5f5w7d+grZPm{LAkq^QJ~gG-+VnCwUh5qdqAW!NnDvZkYU>T%%Lt)xshCPiFx=DEqW^lViMf9KxEP5qsST-g{5$l;U|!k8k4Y zs6r$sXbOHy+$6O+^BeZaR<-{ge`r7Sx4CvLDCA=nh#I5`8PygzUeyaA8;9G@%J_R<9hm9LDl%4rps z**?wfDhlSm39rmN{w(_{t2Wy>5=2~|-)K*?F!0gDu(z=Lc!yn`T2tWN_x>B0$8^p6 zF9U7}vcqT?n^@TwL{nE^%dE&+Oek`yMpUs??Id_a`y}W**r3!IQdIKZp@$3R7rq?8 zQcxz%ykbO2&SAa15W$ujK$x5xKwelMl9fC#H2;xVbMAU~vSVca;Y5EH3`|w^%o6+B z=Nvzp8?!XA?=&s?2=&cIOs$qxE1fwjM!1gUdD_b~W7+KwVYLM(?j1f|CX7>IH|dMw zbU`-EAWW!};iI00!%?d%p9H5qtxdKIJm2fPU*Dd0-^Vv)behnvcL~Rgg>qBUQ(d@;9joa)~j=H4}zWYlp`@R`_z1>!Y zCI5P#F-CiSm}~*IU(oI1^bZ#rQc@yKPBL91cd?$t5v8LET+0LeR}k^Xu#SD9fL>1) zUQz286xKr+GtHwgPaYmb((K#_f>MdJ?sR5qYnHhK#wDV9#6!=+%|R+v(AAcmE8~>i zng*&PrpF*JhToPwz`XELHcWy#ZujXo?BRF$sZk~s83D@t^%$E0-?PE)KEo- zY^mmYrHM*m$u|{p3$PiFC705J6WBN2B|FeaMLnm@zBx13_w>#P2#ay^ypvT7_~7qk z<0b1rzQZUGgCw7Sk`O<7`$HXYVTxerkFiRouv@hJlX;Hi74!<)v7P5_?(M+K%9yfW z@p7BA{0`CkONgY=jGfNMD0r-OYI$<5ZPOd=F5yh#X{^=ZdJ0_Q?sIefFceV^AYLv9 zMg1-pzW^qhiv&Z5Vo>B`^-(|u@svMBnIgcMvTl42i#dJW(b0Y@$u^w%f0~t*zfaHzK(b_*kR37hi@~#7Eg|O5Qu*%vTHn8QU z6>%~ZnaI`UZB|tSDvGu9!znmsIiUW8iio?fkmXq=(Rl&8^g1x0S7_@R-$Ilu59KGH z?-vGK9R~f&nP-v7pt()U=u!^|W5F(t05~Q%vHU`QVw*_H;@s|QLe=`C*+{dP zrozKPl=(}bpe_R~@MnZy2pOd&{6R$P7zn^3Wfg3Vrl*yPNG)eQz%C;XG>}2TJ6sE` zI_|M8ZYocLHXmTT^OkpCeQufGuIJm7 zAWpDFB#4CN7T{9krNMR-HNJ|Y>AF2&U{wH6LJvr= zv;_HKADjt94@S)ML5I}?bihJ!P|23xr~24vth{8!b?tBh+EReXXHyE8)jeEZV;JnLrde8o(Hp{(0K3e7@^=4wM??I$z7P6f3K zdCuHT7Ftv_EP8A}{9&+5xhM__lmNjjr$FdnAIV)&gcMNdm72eezH$P119-R|d7YYZ zizIP7K*;m)ITinP^tlr-Sa^Ng0nWl2Mh6&b9cKV;ISx!`w#YVt)0;aC3Y3RpD(LB1 z^5N0x3Y)`)Y5B#iG^0lvsf(*Fp^;l=>fW!grCC*85#{Z9j+kN5in?UkgI@FjFy@aS zu>f7M0eyGR%n^D^=G@)5sI26`{FaQfm4~VS$$h)!Yr^Sdcm%Xq6kM&NZRN@n&s8Pr0J#$La?hVmm{)JQ zW}0L%O0WxC$)T&aEzDrRS46ii7MhqOXhlO&`9|E6AQVY)c>!$YMNirS{O@a=6#{&~ zj0T8NwL3f79_`Oswnzo6Wh668?|8GUUu{N&6cY>1zS=1G!oT5DyGf@6$!Un+7dzYa}@+G~>uqI=49yDQ(JpO3)k?L=` zYPuYW>2z4yJ+WdgJgenf!Lm|)Lcm_F!75cz0WhWLcsvPag4Znd_ko0hjO(Xt(NyyJ z;2w~MHIuj%w9T#|X%uLh36C7h2Xz_?Ih99>$(n7VvlotxETq3l(*zV-C+y-Y9aGWN z8E)&I4jDRzoSA+4wh5AJZ_FV4vbKmfb=W}| zlAGqb)u*zxyTD7gxV5o(4x4`|fAS$O=oIK)2>odDwlgEd`euw((t?f@SCKkBFq=j7 z-j}KeSlYElq{2IOSU;C`aO6G;?mcXjy-G^3m*izNcPFimfH7Sbh=C z6U#p;T>R8kj--Wp6`rDF#$p(}NodJ>V<7kB|z)?C4h2vAJqyvCeqQXps_)<>@q)UKGg$!d<z=0&~S9AV;Kcnu8_;ilWT{ zA*dAOSW_Xhf%W_b25-U`vY)Ks)cyJvC#1IcAv0mtVXnzJ2P8olkn_^R*i=M zq6wuALOtD6>%E(9XXVr_in$A+M?7nUs%Wk>HzsLFCDdkKo6hurkj8AFEvMseoywlD zrFuPl<#nwd=0vc7v-p8fuO&3FPdy|QD;6CoNfK%L6vW9`ai@{#S7{zX_y-y&1#=U7 z(n{q_K=Ye8!3?PF!aaFom-aB0%%_9jgu6|8Ll&L)gKkN*Epu+A{$MVy?e{X--FT#x z3}ry4p~YA8D~zSqyPuko@lOZ7s}M=s5^3$?^Mx zKZ<4Hg`INj3HTUT7-G1#Tn)w8<~_;}&X}zkgsNqGCxGho1wqHI-{E&*qfH%8BXI!n zHLVIjmLD4>ah+x@sl_8~^FC?nnAg74@u<8TuvXj_)FOtqXbzVYa@(OkdatN;!f=2z z`A0bB9cW-10YTyn87Df`mrQK=Fh(hbw5W1A(rYT%4ICY3Akq+5CuE+OHBYI5kvj^u zdYu|A9$hmjI5-XnFZc7z}qna}JBmJ#k}j27sOaf%`gWq$E5dm&rHDIZVmq8DLC z)Q@vAavkSwO3FAdNVj)drWc&{5t!d^^gEB2#3Q{jatDu|3v7W&fjy%+dHfW0QLiOY z`OBTxwV>tO%1BDQSDq0hz`!Em$9|#|cb?+fRSa0kLg>DLZ4w1#rPWJf1xpgOC)H|~ z{$R^vNf>=|buho^x~u9B1J?bp+zHu;W0(3n9ch1FAfQ=8LybddvPhumqjwe?%}QxW z;-gt(w&O<^A6d2w)oBuARzad9>B;7Sbw0gI7l32&jfndqA_#xz_jRGXx#{;})B2%e zG~&wm?q_~p0F71d%rq1`E_h17_V6;MZ}E;}SWRPN{o2SEtRH}vL(eJ$Lm}42G`$+; z@&*+%*e9W;#?-n^(g)n#f=-$8E{f_FY2x;)Hqe}p&L5_QU)7I>k{!ZDy#b2_2>c{IS zB6%a+@&vevNyHS;y9a%o{aczJNF!$o7IBSD6Q$3!=sUT^I@}({ihW}cvX1OauyrP% z)0uvH=k2$hy#?a9Auira5HeVY5t7M&5>HPA}13jV5h4j)csUc@;3%I)2-MKC+ z=xcw8Gr*Dx9=`_d>Vw_*_~OtN5n~n#K1hkQGsK7Ad{S;CY8DX|Xv^@5a0D zSVm;BAqb=&e7YJFPs)h=B@e}k;-3gw?t(y~G?+rQT-&dW>Ce6C6I=1R!JxO&$wKx` z!hS{biDA;ltWVzJl0GsMSe?iEcCv0zwIGqO17%;}f8=bQ=(4ESTbncCntS&5qz2fO zrzoAx+s~_k9vCZR8$h9nVVyNC6b1K}=1=71ciYi*hL5y<5^FC%XlA=;(LsWeh`w0d z29C1LP?2_0C3jf<+`aE1b4GBePTVHGQ_?kQO(eM$Q`ujtFQSP?xxB2z7Z&0W+Xrt8 zG%WpK*3~uO*@QaqCol z2Lefdbp&jPP1!JcZ1QQfhYS3Qa=T zd;kRgea+k2zuX~V4Y=2z_{IS!@z}|uvm|CcH$jC*%QqvO&YovFxmWTqX*5COzVje8 z7+dZBV220vt6{|tx(D@J*(4dCwJ@F|Asp#?hesn1!@K1l6yq<>WdWp&-xtaPBcr39 zabyNpdac!y)y{grvK}>6jJ}eD^8^WH*lqzPR>bAOl4!@XtZc@sk>p|CGS@QeNx$v?A2?e`*pots zh3l>ORAG<&y5>BkWckn^%GwQRr6M|vSKOET78ZN&RV{jm?hNPYmzp+~(rMoJVb?Kr zFureXit(3aiYQRvhV)`$GT~wnSQ}^duP?NNLw~XS0ADMa1cz$v9kaglF zGey$39F+{od4aO~#?@5?)Q#`sp#OBHQ8g5r0~@ zu?E%Owx=9s^rbac_MSQ14H>7q@mX;#ycAOo`)}OnXEKz4HzZ5IsjkGfuOPDZ zxp-2?S^W=ELT6U?;Y@25P?bw-y{UWLR}lU#k;3z}R_d0@kODj7`<$uY_vkG|(r?w$ zD7O8`HVa_tk)<4D+t$vBD+sdP?CZH+f3MzwCgcsuhEc|b&PQ=H?Uxr{UNYN*ozCDJ z_w0YtaV>5)kncQdCEqB>gIo1AMt>0p9mtA-unT#()3p9xDA>MsnSYB&n>( zx-E7#XTyBZhQI#!N3s=k`j*$0=_x3+xPK@Aw3E^U0U5mH`YVq z_5RHT-?8nba!!}}d5YcK{0f5#Snv5-<5#t@?Z;IAiL?HDvj1H045(ePj&O@!JW1U{qg&-|N988sY#ziix49}8A2Y2=zJ~H|A9SWe;;6E zOR{>J7HFk5`Y;)HA(P@BYg8Fw)isUfsuMv{Q*Jvr;4UTzg2d&dZj;OXpTkZXT}cjk zT-H^x#Ejal^3CyD?2M}Nf98h&_el8lO3`(kIKU6VXi`;uO(ON70Xe+>a5Mk!D?t=! zAe;Uu!kcUQJFZkUV|J)5EhXV<{a*{zzwbnUpEhJ!WlSOc_IQOoFNby`D8iVFj0b?# z`M1OK;KHN0MXC<(CO}EAy+4Y*Eo3wB1?dRuyGiyR0W5!Q1^)cuzFhhvirrKvDcnO? zlNap23gLgg@6R2CDyv2zDB>yGxS;NQ^92M`J8&q+n7ShWzjRsM63*+7tv+ok9CD*k;m{PjLo3Lsw$%)tH)|4Ttl)rB<{^bazoq ztpKbW%~!;Q@uLi&*j~*wsm~ZA>tcY817Pk52O#9Ch^W^C5-8JGCC>c$P%i%{u{mdw z9N9>Up^w_Rc3$fP4p+!+^};)Fu$WC&2`I?`f`tV)s=+Jm;p8^9O$0RG}pu#_-Iio21nw1m~ypTn+W!Npc` zz7&$z`!2l=HCP?WFQsm8aRugI!MI(g35S~Xl3}vaU@aK%YnB6nVed3uJcz)ZBoLi{ z;S_fIz0^t_$vod*9qvXhQyy!WUk^NOk`$eb5^UPcGKU<5cVf`50RHF_v^;+SohG3g z_H%jD0UAlQ1B&ea|8wksZX%M|zFv2_(fH8^P$CaN&d)e#5YomKUZoW(1IQwa-Dv@)*>iP>`>B4d zSMeu+Lp%U0zEIoZNh#nJT?1pF++}bM2_vg;nvt=cKold1+kxKva+F(AOP5lZgk|}Mt7a%>XgtzXf38W%Uje^Ds?G7>Y092-}=K9sX zGizu?dRN9>f}>q!(2v*q9}9)_E+T1B7_j7oLdn_?+@nah6etZ90Tc8X(!Q7e83~9t z26ROsw+SQhWqwP7copQii za}o~5qpJXF{VHyd(*m-c9_ku7hk$mOBxJHUZ}AYwhKac*!-p3@CzG(ll(Bs(=nocDz>FT>(mrqI&k`sBkX*k{fh)G+{rItS~K(0Q8?`)xL~;SYkFD08E=;93c@h6C|l=eI3fyov>mV8M} zKWRFax_9-}Rv5_JiS`XrrqfYWc_VfU9nNT8j^<4V09Yb$&Qt{68z-4d8-fvQH0e4K6;-6kUj)k zSOnS9dd=)ISf)x1^RoxlTqC>mpAuD*Iwn9NWkKL6iMKkl64g%53u+Qw@2@wwja>UV z%SxDdCMYKxXjQr z=(?4kHccb-YL$~>i!|*ejZc;PKE{tZj27xFKY9FVlfICo#!gDK)`Rjo_kp`0>sA2jtl)R@B&x)-V3$x%X>5 zEbUI*@R8sv%Bd=Q|i>C$9+$^)vbKYEE!Di%TWYljNdo$PB%*){D9w%!Z z2SqiKQ>6w-`Y0+cq&Vyui&DR?Pz98 zEIeSyc8N2zQ1WhK%}#UUqH(NRYwHYsNM?Jmus{i~H0m%U-EF)fZxj>V(i}IwNvbBo zgrFDu@ya}imPHEjm60bNDK6|He#ephb2IU=16#5H38fe^3Gln5uHZJTxEZ(0^*zU- z)7$cTrc^e^BNga>9L(>(cD$CBlA7tPMk&AH3LEhP54Te2`LenP!1InS^=4j8yF>^q zCZoV54_?`W_TkK(ilrxuUz6?)vB=L%7Mj#6?S84GccR}1332yngwjx0TrKX*DsI?i zB+a&NEnX~*B9otPD#WjM#;eA^y?^&V7dF4`%-mm~3~k+chL1BP`sMvmp34ZBg`DnH zaq33d*s)o0i-tsijcmZjGX?Cg_~yGgpnnU6Nr?F5y*iKK_@Rl&OzD}TiZft0 z(LWLUC1m{V`~CjawG~Z&d`&t4clB{3(*ab3X5fC&&IRujk+Kzdz?pgDDDtt}gyoA@ zSM-^3>+LY&Y!P}{%v~{Z9Wmz(N}R!I$6(LT%)kY^;gJ-K!Js!J9uP=;pMs9l>tc%= zj^Q_8e|6Y}0OR*&;_p}f*UvU$sBs&DmR}XzrkX;njsaT31G^bZ4y3SXGDp;Y6Bn7Gii;GR30%)fZD~ z`CV&&&rgEIfB(r}ujr5kTKB)huy9bQw{80>FCr|oI@u01Y09O0-`q+o+mfzcG*CpR zn%;Y_R*r#3p$B%(y7v=y)sW$#g+vEnaan?l>f`~88vsC&7dZrN=n;@YVGnvp^+ z=oTnTaEZ5^feHKaTiRw32g)(XYFsaBCJtXNi)xC-qw`5v82=GZ z`Hxj`AdPBzkXGGPfn}uyS@oytB=-);cUsGVqO;;|{w`uxW<3j6`>IG^8K$(|#ctt= zy6M+Q1k5+f^MaZU+%|B7e@E1+ZdcTZS!c*xan#$^ebx|&mJiMWOM5BVE=%J_h5utC z4DKHj!|A4kF6M>V9fuvb@SxNp>X|W@L-8pf(Xk5$@o9EI-C^%^8k9_v2v!aJNW-ns z5MaJdaVM_>IK`XSiS9%CZAhTX{i&m>0@dd?Ju=3OMnCrrs{fD2#?PHk&Ng}im;S^4 z_f|#&*Yp}4`z@CJr7m$gZ=Hue6+v>5^5DBLp299Z%@&E}9nJe^MV@P~oJV(Kr~kjY z&O9FKeU0M?$C4#UlF&j3UE3gpa2YC-vXuQ~sf@A{V=NO+gj)@cZIG1Gu}#c`tZA;D z&@?DyJ(e0|ALc%D&+GnkZsw2QAM?leH8Zc@Z=UD#ecs>CXNQY~xHK1bQ4_W^aQMzu zX#dKR5QFln2IPg-$&xtlbUSy|`dZGQx4Vj|E9}wm+@08zFnd8Ov^-es68S<=Xwt_K znTf|-!yw(pDeI%Mwp|2I2F7OFnV@HDBbBB_0O3aiW2O$eLdpS5eiI8%!;#x;o&?MeI6rYEkc(zLE=XWyVZ!9r0W$ z7TV5pg1u2=Tjqb|vHk3so-}7OU58LvS5|^W?0o)=9$-X1;G2NJGD7D7S}Xgp4b++9 zy+o#Y;%O&;^8~gUmsBH`b7bK4y6qdl%B7xTv!7>aE|wI>Wu5x&7S- zyjPCymEJZnvOFXINlA3bni5%O*T|QIXIR`M?qPxJsRbb@*MH39yy#n=Rz_wQY}wY{C{y zQMf%;Au(zn!@eMpvKh+i_RHTZPdQZHI-nE^;32<*N2<*^M>4T7Oec4pzLxYPTf_GV zu~jaMQ+gBvy3|Bx!5hm1^(5>eL>qfS9;Mp9PU3&aPa}mUSuw8YZoTCPEAzBIIGE0T zoQaR+Pow&e?-6&fn>Mhi;fNqIJIG zw!WRPQ3%IaZjD33sHHR(;osD~kru$>U3lgwZ9%%2b-GFWMkux%A7YASII7l9Uau3s zRlWNTF7{YYg@@%9W4Rl*vIQ)PL~_-Q-X=_Bf6*nQ7oC(7EGMTfVv8(~#WD-WlQd>0 zSQ4lp^-T^i*5G!W*mfNN|_I23XOVp*|e{VC6M!&lKYfF&+K z)fSLmn+F@UvLdfv*-drRdU(Wk$mGVhTy2GJ7N*tp7JB-|N~HB*EMCXtS)>P2CVgbx zak{PiAM(Q)zSRIu8gD;yUN&>`I+qw=^8z0q?MTC4zknj_R_bo;Wf$#Vf3gFCMKS&o zi2=Zw^)$*^5hP!Al?PKeB(zlRa|%Rr)jTXvJNdO!nO!-$(@N@obzo^#HDK#N0~59w z>F=1Y%_DFKlNsJDptbko{9$yNk*}HY4xPCT(Og``Zfa4*nb5Tlvu3$@zfUw1cT}xJ z@#H2W$BkLrzNffY{mEuUX_6B1=6;tt4?*74F@w|USDS56u8UA`FZv|5D^qtw)6G!H z-_Uw@D*f8k{b7IF&Y>5~^bq|Pe;MWde>atf5NU`Ns2jhOJ$_0mb{2FYI$~>8fPzA? z7~0094wwoG<3FwwD3knrpIO@^2vGmNQiZF~avi}B$$xlQBqAR9g-kG_v*%i29F4V= zzGO%?qzZnshMg4QEQ3faiZCuhnFJN^8Ygu2QzUnY9-~1YnIfg%VyYO@`*n9hPymQWoQzU&Zgzm!lKl>2vy7+is&AR3|6PpMqXaT^y&x3*~%u*n%msz#BvM zde_DnKAo5I; z8Y|Ls>U2W(WqM8DpO_Ju>7p5ZG2Bw|=GWWlK6dJxmGoEEUnYELVI}A*f@A( zqJDBIBN&RuAzR#Y+ULTFH7ALQmx;vd!~)yFnfBrgv9JNn!n*rfT$4_pUkgVTtMuH^ z=;*^q3&tCWVT(3L4@vK~Dl*!(*rN@lCXqDn6uowr9zoN`U#_E}AK76KU5xF%d2~XC zr-Oey6-yZ}U)kE`6H3oN`m>J*?`7>neG5tV!YBkvW_l;uOpt zwify2>|uQOsrD6rKKbyZx<@lB)l-^l$BPHvPTY$#_N~hnN_9T|({&n{{A9M8tV{JT s%}Rz}&SjUTMSY;o*fB$iI9F;No1vWvr_-8TB^$hurWZ|0&bh|?2fYT&e*gdg literal 0 HcmV?d00001 diff --git a/book/src/ui-faqs.md b/book/src/ui-faqs.md index d2e60d42da..f4cbf1c40a 100644 --- a/book/src/ui-faqs.md +++ b/book/src/ui-faqs.md @@ -14,3 +14,13 @@ Once you have successfully arrived to the main dashboard, use the sidebar to acc ## 5. Why doesn't my validator balance graph show any data? If your graph is not showing data, it usually means your validator node is still caching data. The application must wait at least 3 epochs before it can render any graphical visualizations. This could take up to 20min. + +## 4. Does Siren support reverse proxy or DNS named addresses? +Yes, if you need to access your beacon or validator from an address such as `https://merp-server:9909/eth2-vc` you should follow the following steps for configuration: +1. Toggle `https` as your protocol +2. Add your address as `merp-server/eth2-vc` +3. Add your Beacon and Validator ports as `9909` + +If you have configured it correctly you should see a green checkmark indicating Siren is now connected to your Validator Client and Beacon Node. + +If you have separate address setups for your Validator Client and Beacon Node respectively you should access the `Advance Settings` on the configuration and repeat the steps above for each address. diff --git a/book/src/ui-usage.md b/book/src/ui-usage.md index 010f5d491b..867a49a91f 100644 --- a/book/src/ui-usage.md +++ b/book/src/ui-usage.md @@ -33,6 +33,7 @@ By clicking on the chart component you can filter selected validators in the ren + ## Hardware Usage and Device Diagnostics The hardware usage component gathers information about the device the Beacon Node is currently running. It displays the Disk usage, CPU metrics and memory usage of the Beacon Node device. The device diagnostics component provides the sync status of the execution client and beacon node. @@ -42,6 +43,12 @@ The hardware usage component gathers information about the device the Beacon Nod +## Log Statistics + +The log statistics present an hourly combined rate of critical, warning, and error logs from the validator client and beacon node. This analysis enables informed decision-making, troubleshooting, and proactive maintenance for optimal system performance. + + + # Validator Management Siren's validator management view provides a detailed overview of all validators with options to deposit to and/or add new validators. Each validator table row displays the validator name, index, balance, rewards, status and all available actions per validator. @@ -59,3 +66,12 @@ Clicking the validator icon activates a detailed validator modal component. This Siren's settings view provides access to the application theme, version, name, device name and important external links. From the settings page users can also access the configuration screen to adjust any beacon or validator node parameters. ![](imgs/ui-settings.png) + + +# Validator and Beacon Logs + +The logs page provides users with the functionality to access and review recorded logs for both validators and beacons. Users can conveniently observe log severity, messages, timestamps, and any additional data associated with each log entry. The interface allows for seamless switching between validator and beacon log outputs, and incorporates useful features such as built-in text search and the ability to pause log feeds. + +Additionally, users can obtain log statistics, which are also available on the main dashboard, thereby facilitating a comprehensive overview of the system's log data. Please note that Siren is limited to storing and displaying only the previous 1000 log messages. This also means the text search is limited to the logs that are currently stored within Siren's limit. + +![](imgs/ui-logs.png) \ No newline at end of file From f16795183564a8487c32890f511a24d6abac82e4 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Wed, 7 Jun 2023 01:50:36 +0000 Subject: [PATCH 37/63] Fix Anvil compilation on Windows (#4381) ## Issue Addressed Workaround for https://github.com/foundry-rs/foundry/issues/5115. ## Proposed Changes Allow Anvil to be installed on Windows without errors by enabling the IPC features (which we don't use, but Anvil expects to exist). --- .github/workflows/test-suite.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 32643b147b..e6b75ea7b1 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -79,7 +79,8 @@ jobs: choco install python protoc visualstudio2019-workload-vctools -y npm config set msvs_version 2019 - name: Install anvil - run: cargo install --git https://github.com/foundry-rs/foundry --locked anvil + # Extra feature to work around https://github.com/foundry-rs/foundry/issues/5115 + run: cargo install --git https://github.com/foundry-rs/foundry --locked anvil --features ethers/ipc - name: Install make run: choco install -y make - uses: KyleMayes/install-llvm-action@v1 From 62a2413ade349abeb4ee37de045a94e50ecfb25b Mon Sep 17 00:00:00 2001 From: Gua00va Date: Thu, 8 Jun 2023 13:47:56 +0000 Subject: [PATCH 38/63] Enable slasher broadcast by default (#4368) ## Issue Addressed This PR addresses issue https://github.com/sigp/lighthouse/issues/4350 ## Proposed Changes This change will enable slasher broadcast in the following cases: No flag is passed, `--slasher-broadcast` is passed and, `--slasher-broadcast=true` is passed. Only when an explicit false value is passed the slasher does not broadcast.(`--slasher-broadcast=false`). ## Additional Info TODO - [x] Modify CLI parsing logic - [x] Write test Refer to #4353 Co-authored-by: Rahul Dogra Co-authored-by: Gua00va <105484243+Gua00va@users.noreply.github.com> --- beacon_node/src/cli.rs | 5 ++-- beacon_node/src/config.rs | 4 ++- lighthouse/tests/beacon_node.rs | 43 ++++++++++++++++++++++++++++----- 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 10d9ffafd4..379eb8e338 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -792,8 +792,9 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { Arg::with_name("slasher-broadcast") .long("slasher-broadcast") .help("Broadcast slashings found by the slasher to the rest of the network \ - [disabled by default].") - .requires("slasher") + [Enabled by default].") + .takes_value(true) + .default_value("true") ) .arg( Arg::with_name("slasher-backend") diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 92e8228190..c59b297c1b 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -633,7 +633,9 @@ pub fn get_config( slasher_config.validator_chunk_size = validator_chunk_size; } - slasher_config.broadcast = cli_args.is_present("slasher-broadcast"); + if let Some(broadcast) = clap_utils::parse_optional(cli_args, "slasher-broadcast")? { + slasher_config.broadcast = broadcast; + } if let Some(backend) = clap_utils::parse_optional(cli_args, "slasher-backend")? { slasher_config.backend = backend; diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index a71a27bdba..65d7bd08b2 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -1912,7 +1912,7 @@ fn slasher_validator_chunk_size_flag() { }); } #[test] -fn slasher_broadcast_flag() { +fn slasher_broadcast_flag_no_args() { CommandLineTest::new() .flag("slasher", None) .flag("slasher-max-db-size", Some("1")) @@ -1926,19 +1926,50 @@ fn slasher_broadcast_flag() { assert!(slasher_config.broadcast); }); } - #[test] -fn slasher_backend_default() { +fn slasher_broadcast_flag_no_default() { CommandLineTest::new() .flag("slasher", None) .flag("slasher-max-db-size", Some("1")) .run_with_zero_port() .with_config(|config| { - let slasher_config = config.slasher.as_ref().unwrap(); - assert_eq!(slasher_config.backend, slasher::DatabaseBackend::Lmdb); + let slasher_config = config + .slasher + .as_ref() + .expect("Unable to parse Slasher config"); + assert!(slasher_config.broadcast); + }); +} +#[test] +fn slasher_broadcast_flag_true() { + CommandLineTest::new() + .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) + .flag("slasher-broadcast", Some("true")) + .run_with_zero_port() + .with_config(|config| { + let slasher_config = config + .slasher + .as_ref() + .expect("Unable to parse Slasher config"); + assert!(slasher_config.broadcast); + }); +} +#[test] +fn slasher_broadcast_flag_false() { + CommandLineTest::new() + .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) + .flag("slasher-broadcast", Some("false")) + .run_with_zero_port() + .with_config(|config| { + let slasher_config = config + .slasher + .as_ref() + .expect("Unable to parse Slasher config"); + assert!(!slasher_config.broadcast); }); } - #[test] fn slasher_backend_override_to_default() { // Hard to test this flag because all but one backend is disabled by default and the backend From 2639e67e908b45d2b23208ca59f0ebdbbdbaff97 Mon Sep 17 00:00:00 2001 From: Divma <26765164+divagant-martian@users.noreply.github.com> Date: Tue, 13 Jun 2023 01:25:05 +0000 Subject: [PATCH 39/63] Update discv5 to expand ipv6 support (#4319) Done in different PRs so that they can reviewed independently, as it's likely this won't be merged before I leave Includes resolution for #4080 - [ ] #4299 - [ ] #4318 - [ ] #4320 Co-authored-by: Diva M Co-authored-by: Age Manning --- Cargo.lock | 294 ++++++++++++++---- beacon_node/lighthouse_network/Cargo.toml | 4 +- beacon_node/lighthouse_network/src/config.rs | 30 +- .../src/discovery/enr_ext.rs | 28 +- .../lighthouse_network/src/discovery/mod.rs | 39 +-- beacon_node/src/cli.rs | 2 - book/src/advanced_networking.md | 78 ++++- boot_node/src/cli.rs | 37 ++- boot_node/src/config.rs | 57 ++-- boot_node/src/server.rs | 6 +- common/eth2_network_config/Cargo.toml | 2 +- lighthouse/tests/boot_node.rs | 18 +- 12 files changed, 425 insertions(+), 170 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6f40e53c34..f53171aafb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -566,6 +566,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.13.1" @@ -1377,6 +1383,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "crypto-bigint" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4c2f4e1afd912bc40bfd6fed5d9dc1f288e0ba01bfcc835cc5bc3eb13efe15" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -1472,11 +1490,12 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.0.0-rc.1" +version = "4.0.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d4ba9852b42210c7538b75484f9daa0655e9a3ac04f693747bb0f02cf3cfe16" +checksum = "03d928d978dbec61a1167414f5ec534f24bea0d7a0d24dd9b6233d3d8223e585" dependencies = [ "cfg-if", + "digest 0.10.7", "fiat-crypto", "packed_simd_2", "platforms 3.0.2", @@ -1659,6 +1678,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56acb310e15652100da43d130af8d97b509e95af61aab1c5a7939ef24337ee17" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "der-parser" version = "7.0.0" @@ -1805,6 +1834,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", + "const-oid", "crypto-common", "subtle", ] @@ -1861,15 +1891,15 @@ dependencies = [ [[package]] name = "discv5" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b009a99b85b58900df46435307fc5c4c845af7e182582b1fbf869572fa9fce69" +checksum = "77f32d27968ba86689e3f0eccba0383414348a6fc5918b0a639c98dd81e20ed6" dependencies = [ "aes 0.7.5", "aes-gcm 0.9.4", "arrayvec", "delay_map", - "enr 0.7.0", + "enr 0.8.1", "fnv", "futures", "hashlink 0.7.0", @@ -1885,8 +1915,6 @@ dependencies = [ "smallvec", "socket2 0.4.9", "tokio", - "tokio-stream", - "tokio-util 0.6.10", "tracing", "tracing-subscriber", "uint", @@ -1922,10 +1950,24 @@ version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" dependencies = [ - "der", - "elliptic-curve", - "rfc6979", - "signature", + "der 0.6.1", + "elliptic-curve 0.12.3", + "rfc6979 0.3.1", + "signature 1.6.4", +] + +[[package]] +name = "ecdsa" +version = "0.16.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0997c976637b606099b9985693efa3581e84e41f5c11ba5255f88711058ad428" +dependencies = [ + "der 0.7.6", + "digest 0.10.7", + "elliptic-curve 0.13.5", + "rfc6979 0.4.0", + "signature 2.1.0", + "spki 0.7.2", ] [[package]] @@ -1934,7 +1976,17 @@ version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" dependencies = [ - "signature", + "signature 1.6.4", +] + +[[package]] +name = "ed25519" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fb04eee5d9d907f29e80ee6b0e78f7e2c82342c63e3580d8c4f69d9d5aad963" +dependencies = [ + "pkcs8 0.10.2", + "signature 2.1.0", ] [[package]] @@ -1944,13 +1996,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ "curve25519-dalek 3.2.0", - "ed25519", + "ed25519 1.5.3", "rand 0.7.3", "serde", "sha2 0.9.9", "zeroize", ] +[[package]] +name = "ed25519-dalek" +version = "2.0.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "798f704d128510932661a3489b08e3f4c934a01d61c5def59ae7b8e48f19665a" +dependencies = [ + "curve25519-dalek 4.0.0-rc.2", + "ed25519 2.2.1", + "rand_core 0.6.4", + "serde", + "sha2 0.10.6", + "zeroize", +] + [[package]] name = "ef_tests" version = "0.2.0" @@ -1994,18 +2060,37 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" dependencies = [ - "base16ct", - "crypto-bigint", - "der", + "base16ct 0.1.1", + "crypto-bigint 0.4.9", + "der 0.6.1", "digest 0.10.7", - "ff", + "ff 0.12.1", "generic-array", - "group", + "group 0.12.1", "hkdf", "pem-rfc7468", - "pkcs8", + "pkcs8 0.9.0", "rand_core 0.6.4", - "sec1", + "sec1 0.3.0", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b" +dependencies = [ + "base16ct 0.2.0", + "crypto-bigint 0.5.2", + "digest 0.10.7", + "ff 0.13.0", + "generic-array", + "group 0.13.0", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1 0.7.2", "subtle", "zeroize", ] @@ -2029,7 +2114,7 @@ dependencies = [ "bs58", "bytes", "hex", - "k256", + "k256 0.11.6", "log", "rand 0.8.5", "rlp", @@ -2040,16 +2125,15 @@ dependencies = [ [[package]] name = "enr" -version = "0.7.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "492a7e5fc2504d5fdce8e124d3e263b244a68b283cac67a69eda0cd43e0aebad" +checksum = "cf56acd72bb22d2824e66ae8e9e5ada4d0de17a69c7fd35569dde2ada8ec9116" dependencies = [ "base64 0.13.1", - "bs58", "bytes", - "ed25519-dalek", + "ed25519-dalek 2.0.0-rc.2", "hex", - "k256", + "k256 0.13.1", "log", "rand 0.8.5", "rlp", @@ -2545,11 +2629,11 @@ dependencies = [ "bytes", "cargo_metadata", "chrono", - "elliptic-curve", + "elliptic-curve 0.12.3", "ethabi 18.0.0", "generic-array", "hex", - "k256", + "k256 0.11.6", "once_cell", "open-fastrlp", "rand 0.8.5", @@ -2731,6 +2815,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "ffi-opaque" version = "2.0.1" @@ -3009,6 +3103,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -3118,7 +3213,18 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" dependencies = [ - "ff", + "ff 0.12.1", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff 0.13.0", "rand_core 0.6.4", "subtle", ] @@ -3855,12 +3961,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72c1e0b51e7ec0a97369623508396067a486bd0cbed95a2659a4b863d28cfc8b" dependencies = [ "cfg-if", - "ecdsa", - "elliptic-curve", + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", "sha2 0.10.6", "sha3 0.10.8", ] +[[package]] +name = "k256" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +dependencies = [ + "cfg-if", + "ecdsa 0.16.7", + "elliptic-curve 0.13.5", + "once_cell", + "sha2 0.10.6", + "signature 2.1.0", +] + [[package]] name = "keccak" version = "0.1.4" @@ -4059,7 +4179,7 @@ checksum = "b1fff5bd889c82a0aec668f2045edd066f559d4e5c40354e5a4c77ac00caac38" dependencies = [ "asn1_der", "bs58", - "ed25519-dalek", + "ed25519-dalek 1.0.1", "either", "fnv", "futures", @@ -4094,7 +4214,7 @@ checksum = "b6a8fcd392ff67af6cc3f03b1426c41f7f26b6b9aff2dc632c1c56dd649e571f" dependencies = [ "asn1_der", "bs58", - "ed25519-dalek", + "ed25519-dalek 1.0.1", "either", "fnv", "futures", @@ -4113,7 +4233,7 @@ dependencies = [ "prost-build", "rand 0.8.5", "rw-stream-sink", - "sec1", + "sec1 0.3.0", "sha2 0.10.6", "smallvec", "thiserror", @@ -4222,7 +4342,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e2d584751cecb2aabaa56106be6be91338a60a0f4e420cf2af639204f596fc1" dependencies = [ "bs58", - "ed25519-dalek", + "ed25519-dalek 1.0.1", "log", "multiaddr 0.17.1", "multihash 0.17.0", @@ -5654,8 +5774,8 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" dependencies = [ - "ecdsa", - "elliptic-curve", + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", "sha2 0.10.6", ] @@ -5665,8 +5785,8 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc8c5bf642dde52bb9e87c0ecd8ca5a76faac2eeed98dedb7c717997e1080aa" dependencies = [ - "ecdsa", - "elliptic-curve", + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", "sha2 0.10.6", ] @@ -5922,8 +6042,18 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" dependencies = [ - "der", - "spki", + "der 0.6.1", + "spki 0.6.0", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.6", + "spki 0.7.2", ] [[package]] @@ -6666,11 +6796,21 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" dependencies = [ - "crypto-bigint", + "crypto-bigint 0.4.9", "hmac 0.12.1", "zeroize", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + [[package]] name = "ring" version = "0.16.20" @@ -7047,10 +7187,24 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" dependencies = [ - "base16ct", - "der", + "base16ct 0.1.1", + "der 0.6.1", "generic-array", - "pkcs8", + "pkcs8 0.9.0", + "subtle", + "zeroize", +] + +[[package]] +name = "sec1" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0aec48e813d6b90b15f0b8948af3c63483992dee44c03e9930b3eebdabe046e" +dependencies = [ + "base16ct 0.2.0", + "der 0.7.6", + "generic-array", + "pkcs8 0.10.2", "subtle", "zeroize", ] @@ -7339,6 +7493,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + [[package]] name = "simple_asn1" version = "0.6.2" @@ -7576,14 +7740,14 @@ checksum = "5e9f0ab6ef7eb7353d9119c170a436d1bf248eea575ac42d19d12f4e34130831" [[package]] name = "snow" -version = "0.9.2" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ccba027ba85743e09d15c03296797cad56395089b832b48b5a5217880f57733" +checksum = "774d05a3edae07ce6d68ea6984f3c05e9bba8927e3dd591e3b479e5b03213d0d" dependencies = [ "aes-gcm 0.9.4", "blake2", "chacha20poly1305", - "curve25519-dalek 4.0.0-rc.1", + "curve25519-dalek 4.0.0-rc.2", "rand_core 0.6.4", "ring", "rustc_version 0.4.0", @@ -7640,7 +7804,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" dependencies = [ "base64ct", - "der", + "der 0.6.1", +] + +[[package]] +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der 0.7.6", ] [[package]] @@ -9358,9 +9532,9 @@ dependencies = [ [[package]] name = "webrtc-dtls" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942be5bd85f072c3128396f6e5a9bfb93ca8c1939ded735d177b7bcba9a13d05" +checksum = "c4a00f4242f2db33307347bd5be53263c52a0331c96c14292118c9a6bb48d267" dependencies = [ "aes 0.6.0", "aes-gcm 0.10.2", @@ -9371,29 +9545,28 @@ dependencies = [ "ccm", "curve25519-dalek 3.2.0", "der-parser 8.2.0", - "elliptic-curve", + "elliptic-curve 0.12.3", "hkdf", "hmac 0.12.1", "log", - "oid-registry 0.6.1", "p256", "p384", "rand 0.8.5", "rand_core 0.6.4", - "rcgen 0.9.3", + "rcgen 0.10.0", "ring", "rustls 0.19.1", - "sec1", + "sec1 0.3.0", "serde", "sha1", "sha2 0.10.6", - "signature", + "signature 1.6.4", "subtle", "thiserror", "tokio", "webpki 0.21.4", "webrtc-util", - "x25519-dalek 2.0.0-pre.1", + "x25519-dalek 2.0.0-rc.2", "x509-parser 0.13.2", ] @@ -9836,12 +10009,13 @@ dependencies = [ [[package]] name = "x25519-dalek" -version = "2.0.0-pre.1" +version = "2.0.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5da623d8af10a62342bcbbb230e33e58a63255a58012f8653c578e54bab48df" +checksum = "fabd6e16dd08033932fc3265ad4510cc2eab24656058a6dcb107ffe274abcc95" dependencies = [ - "curve25519-dalek 3.2.0", + "curve25519-dalek 4.0.0-rc.2", "rand_core 0.6.4", + "serde", "zeroize", ] diff --git a/beacon_node/lighthouse_network/Cargo.toml b/beacon_node/lighthouse_network/Cargo.toml index c1b4d72174..ca15b5ef2c 100644 --- a/beacon_node/lighthouse_network/Cargo.toml +++ b/beacon_node/lighthouse_network/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Sigma Prime "] edition = "2021" [dependencies] -discv5 = { version = "0.2.2", features = ["libp2p"] } +discv5 = { version = "0.3.0", features = ["libp2p"]} unsigned-varint = { version = "0.6.0", features = ["codec"] } types = { path = "../../consensus/types" } ssz_types = "0.5.0" @@ -60,4 +60,4 @@ quickcheck = "0.9.2" quickcheck_macros = "0.9.1" [features] -libp2p-websocket = [] +libp2p-websocket = [] \ No newline at end of file diff --git a/beacon_node/lighthouse_network/src/config.rs b/beacon_node/lighthouse_network/src/config.rs index 01bb8569dd..9467526458 100644 --- a/beacon_node/lighthouse_network/src/config.rs +++ b/beacon_node/lighthouse_network/src/config.rs @@ -163,7 +163,7 @@ impl Config { udp_port, tcp_port, }); - self.discv5_config.ip_mode = discv5::IpMode::Ip4; + self.discv5_config.listen_config = discv5::ListenConfig::from_ip(addr.into(), udp_port); self.discv5_config.table_filter = |enr| enr.ip4().as_ref().map_or(false, is_global_ipv4) } @@ -176,9 +176,8 @@ impl Config { udp_port, tcp_port, }); - self.discv5_config.ip_mode = discv5::IpMode::Ip6 { - enable_mapped_addresses: false, - }; + + self.discv5_config.listen_config = discv5::ListenConfig::from_ip(addr.into(), udp_port); self.discv5_config.table_filter = |enr| enr.ip6().as_ref().map_or(false, is_global_ipv6) } @@ -206,10 +205,10 @@ impl Config { tcp_port: tcp6_port, }, ); + self.discv5_config.listen_config = discv5::ListenConfig::default() + .with_ipv4(v4_addr, udp4_port) + .with_ipv6(v6_addr, udp6_port); - self.discv5_config.ip_mode = discv5::IpMode::Ip6 { - enable_mapped_addresses: true, - }; self.discv5_config.table_filter = |enr| match (&enr.ip4(), &enr.ip6()) { (None, None) => false, (None, Some(ip6)) => is_global_ipv6(ip6), @@ -279,9 +278,17 @@ impl Default for Config { .build() .expect("The total rate limit has been specified"), ); + let listen_addresses = ListenAddress::V4(ListenAddr { + addr: Ipv4Addr::UNSPECIFIED, + udp_port: 9000, + tcp_port: 9000, + }); + + let discv5_listen_config = + discv5::ListenConfig::from_ip(Ipv4Addr::UNSPECIFIED.into(), 9000); // discv5 configuration - let discv5_config = Discv5ConfigBuilder::new() + let discv5_config = Discv5ConfigBuilder::new(discv5_listen_config) .enable_packet_filter() .session_cache_capacity(5000) .request_timeout(Duration::from_secs(1)) @@ -304,12 +311,9 @@ impl Default for Config { // NOTE: Some of these get overridden by the corresponding CLI default values. Config { network_dir, - listen_addresses: ListenAddress::V4(ListenAddr { - addr: Ipv4Addr::UNSPECIFIED, - udp_port: 9000, - tcp_port: 9000, - }), + listen_addresses, enr_address: (None, None), + enr_udp4_port: None, enr_tcp4_port: None, enr_udp6_port: None, diff --git a/beacon_node/lighthouse_network/src/discovery/enr_ext.rs b/beacon_node/lighthouse_network/src/discovery/enr_ext.rs index e9cca6667a..3df7f7c16f 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr_ext.rs +++ b/beacon_node/lighthouse_network/src/discovery/enr_ext.rs @@ -198,7 +198,7 @@ impl CombinedKeyPublicExt for CombinedPublicKey { fn as_peer_id(&self) -> PeerId { match self { Self::Secp256k1(pk) => { - let pk_bytes = pk.to_bytes(); + let pk_bytes = pk.to_sec1_bytes(); let libp2p_pk = libp2p::core::PublicKey::Secp256k1( libp2p::core::identity::secp256k1::PublicKey::decode(&pk_bytes) .expect("valid public key"), @@ -222,14 +222,16 @@ impl CombinedKeyExt for CombinedKey { match key { Keypair::Secp256k1(key) => { let secret = - discv5::enr::k256::ecdsa::SigningKey::from_bytes(&key.secret().to_bytes()) + discv5::enr::k256::ecdsa::SigningKey::from_slice(&key.secret().to_bytes()) .expect("libp2p key must be valid"); Ok(CombinedKey::Secp256k1(secret)) } Keypair::Ed25519(key) => { - let ed_keypair = - discv5::enr::ed25519_dalek::SecretKey::from_bytes(&key.encode()[..32]) - .expect("libp2p key must be valid"); + let ed_keypair = discv5::enr::ed25519_dalek::SigningKey::from_bytes( + &(key.encode()[..32]) + .try_into() + .expect("libp2p key must be valid"), + ); Ok(CombinedKey::from(ed_keypair)) } Keypair::Ecdsa(_) => Err("Ecdsa keypairs not supported"), @@ -281,7 +283,7 @@ mod tests { fn test_secp256k1_peer_id_conversion() { let sk_hex = "df94a73d528434ce2309abb19c16aedb535322797dbd59c157b1e04095900f48"; let sk_bytes = hex::decode(sk_hex).unwrap(); - let secret_key = discv5::enr::k256::ecdsa::SigningKey::from_bytes(&sk_bytes).unwrap(); + let secret_key = discv5::enr::k256::ecdsa::SigningKey::from_slice(&sk_bytes).unwrap(); let libp2p_sk = libp2p::identity::secp256k1::SecretKey::from_bytes(sk_bytes).unwrap(); let secp256k1_kp: libp2p::identity::secp256k1::Keypair = libp2p_sk.into(); @@ -300,16 +302,18 @@ mod tests { fn test_ed25519_peer_conversion() { let sk_hex = "4dea8a5072119927e9d243a7d953f2f4bc95b70f110978e2f9bc7a9000e4b261"; let sk_bytes = hex::decode(sk_hex).unwrap(); - let secret = discv5::enr::ed25519_dalek::SecretKey::from_bytes(&sk_bytes).unwrap(); - let public = discv5::enr::ed25519_dalek::PublicKey::from(&secret); - let keypair = discv5::enr::ed25519_dalek::Keypair { secret, public }; + let secret_key = discv5::enr::ed25519_dalek::SigningKey::from_bytes( + &sk_bytes.clone().try_into().unwrap(), + ); let libp2p_sk = libp2p::identity::ed25519::SecretKey::from_bytes(sk_bytes).unwrap(); - let ed25519_kp: libp2p::identity::ed25519::Keypair = libp2p_sk.into(); - let libp2p_kp = Keypair::Ed25519(ed25519_kp); + let secp256k1_kp: libp2p::identity::ed25519::Keypair = libp2p_sk.into(); + let libp2p_kp = Keypair::Ed25519(secp256k1_kp); let peer_id = libp2p_kp.public().to_peer_id(); - let enr = discv5::enr::EnrBuilder::new("v4").build(&keypair).unwrap(); + let enr = discv5::enr::EnrBuilder::new("v4") + .build(&secret_key) + .unwrap(); let node_id = peer_id_to_node_id(&peer_id).unwrap(); assert_eq!(enr.node_id(), node_id); diff --git a/beacon_node/lighthouse_network/src/discovery/mod.rs b/beacon_node/lighthouse_network/src/discovery/mod.rs index 13fdf8ed57..3ee74ebf01 100644 --- a/beacon_node/lighthouse_network/src/discovery/mod.rs +++ b/beacon_node/lighthouse_network/src/discovery/mod.rs @@ -209,13 +209,6 @@ impl Discovery { 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() ); - let listen_socket = match config.listen_addrs() { - crate::listen_addr::ListenAddress::V4(v4_addr) => v4_addr.udp_socket_addr(), - crate::listen_addr::ListenAddress::V6(v6_addr) => v6_addr.udp_socket_addr(), - crate::listen_addr::ListenAddress::DualStack(_v4_addr, v6_addr) => { - v6_addr.udp_socket_addr() - } - }; // convert the keypair into an ENR key let enr_key: CombinedKey = CombinedKey::from_libp2p(local_key)?; @@ -251,10 +244,7 @@ impl Discovery { // Start the discv5 service and obtain an event stream let event_stream = if !config.disable_discovery { - discv5 - .start(listen_socket) - .map_err(|e| e.to_string()) - .await?; + discv5.start().map_err(|e| e.to_string()).await?; debug!(log, "Discovery service started"); EventStream::Awaiting(Box::pin(discv5.event_stream())) } else { @@ -413,7 +403,7 @@ impl Discovery { /// If the external address needs to be modified, use `update_enr_udp_socket. pub fn update_enr_tcp_port(&mut self, port: u16) -> Result<(), String> { self.discv5 - .enr_insert("tcp", &port.to_be_bytes()) + .enr_insert("tcp", &port) .map_err(|e| format!("{:?}", e))?; // replace the global version @@ -428,29 +418,12 @@ impl Discovery { /// This is with caution. Discovery should automatically maintain this. This should only be /// used when automatic discovery is disabled. pub fn update_enr_udp_socket(&mut self, socket_addr: SocketAddr) -> Result<(), String> { - match socket_addr { - SocketAddr::V4(socket) => { - self.discv5 - .enr_insert("ip", &socket.ip().octets()) - .map_err(|e| format!("{:?}", e))?; - self.discv5 - .enr_insert("udp", &socket.port().to_be_bytes()) - .map_err(|e| format!("{:?}", e))?; - } - SocketAddr::V6(socket) => { - self.discv5 - .enr_insert("ip6", &socket.ip().octets()) - .map_err(|e| format!("{:?}", e))?; - self.discv5 - .enr_insert("udp6", &socket.port().to_be_bytes()) - .map_err(|e| format!("{:?}", e))?; - } + const IS_TCP: bool = false; + if self.discv5.update_local_enr_socket(socket_addr, IS_TCP) { + // persist modified enr to disk + enr::save_enr_to_disk(Path::new(&self.enr_dir), &self.local_enr(), &self.log); } - - // replace the global version *self.network_globals.local_enr.write() = self.discv5.local_enr(); - // persist modified enr to disk - enr::save_enr_to_disk(Path::new(&self.enr_dir), &self.local_enr(), &self.log); Ok(()) } diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 379eb8e338..e763d93f82 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -116,7 +116,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .value_name("PORT") .help("The UDP port that discovery will listen on over IpV6 if listening over \ both Ipv4 and IpV6. Defaults to `port6`") - .hidden(true) // TODO: implement dual stack via two sockets in discv5. .takes_value(true), ) .arg( @@ -198,7 +197,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { discovery. Set this only if you are sure other nodes can connect to your \ local node on this address. This will update the `ip4` or `ip6` ENR fields \ accordingly. To update both, set this flag twice with the different values.") - .requires("enr-udp-port") .multiple(true) .max_values(2) .takes_value(true), diff --git a/book/src/advanced_networking.md b/book/src/advanced_networking.md index 59f3da376f..586503cb96 100644 --- a/book/src/advanced_networking.md +++ b/book/src/advanced_networking.md @@ -38,7 +38,6 @@ large peer count will not speed up sync. For these reasons, we recommend users do not modify the `--target-peers` count drastically and use the (recommended) default. - ### NAT Traversal (Port Forwarding) Lighthouse, by default, uses port 9000 for both TCP and UDP. Lighthouse will @@ -55,7 +54,7 @@ enabled, we recommend you to manually set up port mappings to both of Lighthouse TCP and UDP ports (9000 by default). > Note: Lighthouse needs to advertise its publicly accessible ports in -> order to inform its peers that it is contactable and how to connect to it. +> order to inform its peers that it is contactable and how to connect to it. > Lighthouse has an automated way of doing this for the UDP port. This means > Lighthouse can detect its external UDP port. There is no such mechanism for the > TCP port. As such, we assume that the external UDP and external TCP port is the @@ -107,3 +106,78 @@ Modifying the ENR settings can degrade the discovery of your node, making it harder for peers to find you or potentially making it harder for other peers to find each other. We recommend not touching these settings unless for a more advanced use case. + + +### IPv6 support + +As noted in the previous sections, two fundamental parts to ensure good +connectivity are: The parameters that configure the sockets over which +Lighthouse listens for connections, and the parameters used to tell other peers +how to connect to your node. This distinction is relevant and applies to most +nodes that do not run directly on a public network. + +#### Configuring Lighthouse to listen over IPv4/IPv6/Dual stack + +To listen over only IPv6 use the same parameters as done when listening over +IPv4 only: + +- `--listen-addresses :: --port 9909` will listen over IPv6 using port `9909` for +TCP and UDP. +- `--listen-addresses :: --port 9909 --discovery-port 9999` will listen over + IPv6 using port `9909` for TCP and port `9999` for UDP. + +To listen over both IPv4 and IPv6: +- Set two listening addresses using the `--listen-addresses` flag twice ensuring + the two addresses are one IPv4, and the other IPv6. When doing so, the + `--port` and `--discovery-port` flags will apply exclusively to IPv4. Note + that this behaviour differs from the Ipv6 only case described above. +- If necessary, set the `--port6` flag to configure the port used for TCP and + UDP over IPv6. This flag has no effect when listening over IPv6 only. +- If necessary, set the `--discovery-port6` flag to configure the IPv6 UDP + port. This will default to the value given to `--port6` if not set. This flag + has no effect when listening over IPv6 only. + +##### Configuration Examples + +- `--listen-addresses :: --listen-addresses 0.0.0.0 --port 9909` will listen + over IPv4 using port `9909` for TCP and UDP. It will also listen over IPv6 but + using the default value for `--port6` for UDP and TCP (`9090`). +- `--listen-addresses :: --listen-addresses --port 9909 --discovery-port6 9999` + will have the same configuration as before except for the IPv6 UDP socket, + which will use port `9999`. + +#### Configuring Lighthouse to advertise IPv6 reachable addresses +Lighthouse supports IPv6 to connect to other nodes both over IPv6 exclusively, +and dual stack using one socket for IPv6 and another socket for IPv6. In both +scenarios, the previous sections still apply. In summary: + +> Beacon nodes must advertise their publicly reachable socket address + +In order to do so, lighthouse provides the following CLI options/parameters. + +- `--enr-udp-port` Use this to advertise the port that is publicly reachable + over UDP with a publicly reachable IPv4 address. This might differ from the + IPv4 port used to listen. +- `--enr-udp6-port` Use this to advertise the port that is publicly reachable + over UDP with a publicly reachable IPv6 address. This might differ from the + IPv6 port used to listen. +- `--enr-tcp-port` Use this to advertise the port that is publicly reachable + over TCP with a publicly reachable IPv4 address. This might differ from the + IPv4 port used to listen. +- `--enr-tcp6-port` Use this to advertise the port that is publicly reachable + over TCP with a publicly reachable IPv6 address. This might differ from the + IPv6 port used to listen. +- `--enr-addresses` Use this to advertise publicly reachable addresses. Takes at + most two values, one for IPv4 and one for IPv6. Note that a beacon node that + advertises some address, must be + reachable both over UDP and TCP. + +In the general case, an user will not require to set these explicitly. Update +these options only if you can guarantee your node is reachable with these +values. + +#### Known caveats + +IPv6 link local addresses are likely to have poor connectivity if used in +topologies with more than one interface. Use global addresses for the general +case. diff --git a/boot_node/src/cli.rs b/boot_node/src/cli.rs index c3d7ac48a9..b13f47f752 100644 --- a/boot_node/src/cli.rs +++ b/boot_node/src/cli.rs @@ -13,13 +13,19 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .settings(&[clap::AppSettings::ColoredHelp]) .arg( Arg::with_name("enr-address") - .value_name("IP-ADDRESS") - .help("The external IP address/ DNS address to broadcast to other peers on how to reach this node. \ - If a DNS address is provided, the enr-address is set to the IP address it resolves to and \ - does not auto-update based on PONG responses in discovery.") + .long("enr-address") + .value_name("ADDRESS") + .help("The IP address/ DNS address to broadcast to other peers on how to reach \ + this node. If a DNS address is provided, the enr-address is set to the IP \ + address it resolves to and does not auto-update based on PONG responses in \ + discovery. Set this only if you are sure other nodes can connect to your \ + local node on this address. This will update the `ip4` or `ip6` ENR fields \ + accordingly. To update both, set this flag twice with the different values.") + .multiple(true) + .max_values(2) .required(true) - .takes_value(true) .conflicts_with("network-dir") + .takes_value(true), ) .arg( Arg::with_name("port") @@ -29,11 +35,29 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .default_value("9000") .takes_value(true) ) + .arg( + Arg::with_name("port6") + .long("port6") + .value_name("PORT") + .help("The UDP port to listen on over IpV6 when listening over both Ipv4 and \ + Ipv6. Defaults to 9090 when required.") + .default_value("9090") + .takes_value(true), + ) .arg( Arg::with_name("listen-address") .long("listen-address") .value_name("ADDRESS") - .help("The address the bootnode will listen for UDP connections.") + .help("The address the bootnode will listen for UDP communications. To listen \ + over IpV4 and IpV6 set this flag twice with the different values.\n\ + Examples:\n\ + - --listen-address '0.0.0.0' will listen over Ipv4.\n\ + - --listen-address '::' will listen over Ipv6.\n\ + - --listen-address '0.0.0.0' --listen-address '::' will listen over both \ + Ipv4 and Ipv6. The order of the given addresses is not relevant. However, \ + multiple Ipv4, or multiple Ipv6 addresses will not be accepted.") + .multiple(true) + .max_values(2) .default_value("0.0.0.0") .takes_value(true) ) @@ -59,6 +83,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .value_name("PORT") .help("The UDP6 port of the local ENR. Set this only if you are sure other nodes \ can connect to your local node on this port over IpV6.") + .conflicts_with("network-dir") .takes_value(true), ) .arg( diff --git a/boot_node/src/config.rs b/boot_node/src/config.rs index d3ee58a907..c4e36022a8 100644 --- a/boot_node/src/config.rs +++ b/boot_node/src/config.rs @@ -2,7 +2,6 @@ use beacon_node::{get_data_dir, set_network_config}; use clap::ArgMatches; use eth2_network_config::Eth2NetworkConfig; use lighthouse_network::discovery::create_enr_builder_from_config; -use lighthouse_network::discv5::IpMode; use lighthouse_network::discv5::{enr::CombinedKey, Discv5Config, Enr}; use lighthouse_network::{ discovery::{load_enr_from_disk, use_or_load_enr}, @@ -10,13 +9,12 @@ use lighthouse_network::{ }; use serde_derive::{Deserialize, Serialize}; use ssz::Encode; -use std::net::SocketAddr; +use std::net::{SocketAddrV4, SocketAddrV6}; use std::{marker::PhantomData, path::PathBuf}; use types::EthSpec; /// A set of configuration parameters for the bootnode, established from CLI arguments. pub struct BootNodeConfig { - pub listen_socket: SocketAddr, // TODO: Generalise to multiaddr pub boot_nodes: Vec, pub local_enr: Enr, @@ -81,31 +79,6 @@ impl BootNodeConfig { network_config.discv5_config.enr_update = false; } - // the address to listen on - let listen_socket = match network_config.listen_addrs().clone() { - lighthouse_network::ListenAddress::V4(v4_addr) => { - // Set explicitly as ipv4 otherwise - network_config.discv5_config.ip_mode = IpMode::Ip4; - v4_addr.udp_socket_addr() - } - lighthouse_network::ListenAddress::V6(v6_addr) => { - // create ipv6 sockets and enable ipv4 mapped addresses. - network_config.discv5_config.ip_mode = IpMode::Ip6 { - enable_mapped_addresses: false, - }; - - v6_addr.udp_socket_addr() - } - lighthouse_network::ListenAddress::DualStack(_v4_addr, v6_addr) => { - // create ipv6 sockets and enable ipv4 mapped addresses. - network_config.discv5_config.ip_mode = IpMode::Ip6 { - enable_mapped_addresses: true, - }; - - v6_addr.udp_socket_addr() - } - }; - let private_key = load_private_key(&network_config, &logger); let local_key = CombinedKey::from_libp2p(&private_key)?; @@ -143,7 +116,7 @@ impl BootNodeConfig { let mut builder = create_enr_builder_from_config(&network_config, enable_tcp); // If we know of the ENR field, add it to the initial construction if let Some(enr_fork_bytes) = enr_fork { - builder.add_value("eth2", enr_fork_bytes.as_slice()); + builder.add_value("eth2", &enr_fork_bytes); } builder .build(&local_key) @@ -155,7 +128,6 @@ impl BootNodeConfig { }; Ok(BootNodeConfig { - listen_socket, boot_nodes, local_enr, local_key, @@ -170,7 +142,8 @@ impl BootNodeConfig { /// Its fields are a subset of the fields of `BootNodeConfig`, some of them are copied from `Discv5Config`. #[derive(Serialize, Deserialize)] pub struct BootNodeConfigSerialization { - pub listen_socket: SocketAddr, + pub ipv4_listen_socket: Option, + pub ipv6_listen_socket: Option, // TODO: Generalise to multiaddr pub boot_nodes: Vec, pub local_enr: Enr, @@ -183,7 +156,6 @@ impl BootNodeConfigSerialization { /// relevant fields of `config` pub fn from_config_ref(config: &BootNodeConfig) -> Self { let BootNodeConfig { - listen_socket, boot_nodes, local_enr, local_key: _, @@ -191,8 +163,27 @@ impl BootNodeConfigSerialization { phantom: _, } = config; + let (ipv4_listen_socket, ipv6_listen_socket) = match discv5_config.listen_config { + lighthouse_network::discv5::ListenConfig::Ipv4 { ip, port } => { + (Some(SocketAddrV4::new(ip, port)), None) + } + lighthouse_network::discv5::ListenConfig::Ipv6 { ip, port } => { + (None, Some(SocketAddrV6::new(ip, port, 0, 0))) + } + lighthouse_network::discv5::ListenConfig::DualStack { + ipv4, + ipv4_port, + ipv6, + ipv6_port, + } => ( + Some(SocketAddrV4::new(ipv4, ipv4_port)), + Some(SocketAddrV6::new(ipv6, ipv6_port, 0, 0)), + ), + }; + BootNodeConfigSerialization { - listen_socket: *listen_socket, + ipv4_listen_socket, + ipv6_listen_socket, boot_nodes: boot_nodes.clone(), local_enr: local_enr.clone(), disable_packet_filter: !discv5_config.enable_packet_filter, diff --git a/boot_node/src/server.rs b/boot_node/src/server.rs index 3f5419c2c6..3823b28726 100644 --- a/boot_node/src/server.rs +++ b/boot_node/src/server.rs @@ -10,7 +10,6 @@ use types::EthSpec; pub async fn run(config: BootNodeConfig, log: slog::Logger) { let BootNodeConfig { - listen_socket, boot_nodes, local_enr, local_key, @@ -31,7 +30,7 @@ pub async fn run(config: BootNodeConfig, log: slog::Logger) { let pretty_v6_socket = enr_v6_socket.as_ref().map(|addr| addr.to_string()); info!( log, "Configuration parameters"; - "listening_address" => %listen_socket, + "listening_address" => ?discv5_config.listen_config, "advertised_v4_address" => ?pretty_v4_socket, "advertised_v6_address" => ?pretty_v6_socket, "eth2" => eth2_field @@ -41,6 +40,7 @@ pub async fn run(config: BootNodeConfig, log: slog::Logger) { // build the contactable multiaddr list, adding the p2p protocol info!(log, "Contact information"; "enr" => local_enr.to_base64()); + info!(log, "Enr details"; "enr" => ?local_enr); info!(log, "Contact information"; "multiaddrs" => ?local_enr.multiaddr_p2p()); // construct the discv5 server @@ -64,7 +64,7 @@ pub async fn run(config: BootNodeConfig, log: slog::Logger) { } // start the server - if let Err(e) = discv5.start(listen_socket).await { + if let Err(e) = discv5.start().await { slog::crit!(log, "Could not start discv5 server"; "error" => %e); return; } diff --git a/common/eth2_network_config/Cargo.toml b/common/eth2_network_config/Cargo.toml index f8382c95d3..296d43b1a2 100644 --- a/common/eth2_network_config/Cargo.toml +++ b/common/eth2_network_config/Cargo.toml @@ -18,4 +18,4 @@ serde_yaml = "0.8.13" types = { path = "../../consensus/types"} ethereum_ssz = "0.5.0" eth2_config = { path = "../eth2_config"} -discv5 = "0.2.2" +discv5 = "0.3.0" \ No newline at end of file diff --git a/lighthouse/tests/boot_node.rs b/lighthouse/tests/boot_node.rs index 4dd5ad95dd..659dea468d 100644 --- a/lighthouse/tests/boot_node.rs +++ b/lighthouse/tests/boot_node.rs @@ -39,7 +39,7 @@ impl CommandLineTest { } fn run_with_ip(&mut self) -> CompletedTest { - self.cmd.arg(IP_ADDRESS); + self.cmd.arg("--enr-address").arg(IP_ADDRESS); self.run() } } @@ -67,7 +67,13 @@ fn port_flag() { .flag("port", Some(port.to_string().as_str())) .run_with_ip() .with_config(|config| { - assert_eq!(config.listen_socket.port(), port); + assert_eq!( + config + .ipv4_listen_socket + .expect("Bootnode should be listening on IPv4") + .port(), + port + ); }) } @@ -78,7 +84,13 @@ fn listen_address_flag() { .flag("listen-address", Some("127.0.0.2")) .run_with_ip() .with_config(|config| { - assert_eq!(config.listen_socket.ip(), addr); + assert_eq!( + config + .ipv4_listen_socket + .expect("Bootnode should be listening on IPv4") + .ip(), + &addr + ); }); } From a227ee7478a124ae4fe0d88e8f070ed51a26063c Mon Sep 17 00:00:00 2001 From: AMIT SINGH Date: Tue, 13 Jun 2023 10:25:27 +0000 Subject: [PATCH 40/63] Use MediaType accept header parser (#4216) ## Issue Addressed #3510 ## Proposed Changes - Replace mime with MediaTypeList - Remove parse_accept fn as MediaTypeList does it built-in - Get the supported media type of the highest q-factor in a single list iteration without sorting ## Additional Info I have addressed the suggested changes in previous [PR](https://github.com/sigp/lighthouse/pull/3520#discussion_r959048633) --- Cargo.lock | 8 +++- common/eth2/Cargo.toml | 12 ++++-- common/eth2/src/types.rs | 83 ++++++++++++++++++++++++++-------------- 3 files changed, 71 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f53171aafb..18276b3ea3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2290,7 +2290,7 @@ dependencies = [ "futures-util", "libsecp256k1", "lighthouse_network", - "mime", + "mediatype", "procinfo", "proto_array", "psutil", @@ -4962,6 +4962,12 @@ dependencies = [ "libc", ] +[[package]] +name = "mediatype" +version = "0.19.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea6e62614ab2fc0faa58bb15102a0382d368f896a9fa4776592589ab55c4de7" + [[package]] name = "memchr" version = "2.5.0" diff --git a/common/eth2/Cargo.toml b/common/eth2/Cargo.toml index 2c5e7060b2..4eabd3ff86 100644 --- a/common/eth2/Cargo.toml +++ b/common/eth2/Cargo.toml @@ -10,7 +10,7 @@ edition = "2021" serde = { version = "1.0.116", features = ["derive"] } serde_json = "1.0.58" types = { path = "../../consensus/types" } -reqwest = { version = "0.11.0", features = ["json","stream"] } +reqwest = { version = "0.11.0", features = ["json", "stream"] } lighthouse_network = { path = "../../beacon_node/lighthouse_network" } proto_array = { path = "../../consensus/proto_array", optional = true } ethereum_serde_utils = "0.5.0" @@ -26,7 +26,7 @@ futures-util = "0.3.8" futures = "0.3.8" store = { path = "../../beacon_node/store", optional = true } slashing_protection = { path = "../../validator_client/slashing_protection", optional = true } -mime = "0.3.16" +mediatype = "0.19.13" [target.'cfg(target_os = "linux")'.dependencies] psutil = { version = "3.2.2", optional = true } @@ -34,4 +34,10 @@ procinfo = { version = "0.4.2", optional = true } [features] default = ["lighthouse"] -lighthouse = ["proto_array", "psutil", "procinfo", "store", "slashing_protection"] +lighthouse = [ + "proto_array", + "psutil", + "procinfo", + "store", + "slashing_protection", +] diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index d7150bff71..55759a2e15 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -3,9 +3,8 @@ use crate::Error as ServerError; use lighthouse_network::{ConnectionDirection, Enr, Multiaddr, PeerConnectionStatus}; -use mime::{Mime, APPLICATION, JSON, OCTET_STREAM, STAR}; +use mediatype::{names, MediaType, MediaTypeList}; use serde::{Deserialize, Serialize}; -use std::cmp::Reverse; use std::convert::TryFrom; use std::fmt; use std::str::{from_utf8, FromStr}; @@ -1172,35 +1171,58 @@ impl FromStr for Accept { type Err = String; fn from_str(s: &str) -> Result { - let mut mimes = parse_accept(s)?; + let media_type_list = MediaTypeList::new(s); // [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 // find the highest q-factor supported accept type - mimes.sort_by_key(|m| { - Reverse(m.get_param("q").map_or(1000_u16, |n| { - (n.as_ref().parse::().unwrap_or(0_f32) * 1000_f32) as u16 - })) - }); - mimes - .into_iter() - .find_map(|m| match (m.type_(), m.subtype()) { - (APPLICATION, OCTET_STREAM) => Some(Accept::Ssz), - (APPLICATION, JSON) => Some(Accept::Json), - (STAR, STAR) => Some(Accept::Any), - _ => None, - }) - .ok_or_else(|| "accept header is not supported".to_string()) - } -} + let mut highest_q = 0_u16; + let mut accept_type = None; -fn parse_accept(accept: &str) -> Result, String> { - accept - .split(',') - .map(|part| { - part.parse() - .map_err(|e| format!("error parsing Accept header: {}", e)) - }) - .collect() + const APPLICATION: &str = names::APPLICATION.as_str(); + const OCTET_STREAM: &str = names::OCTET_STREAM.as_str(); + const JSON: &str = names::JSON.as_str(); + const STAR: &str = names::_STAR.as_str(); + const Q: &str = names::Q.as_str(); + + media_type_list.into_iter().for_each(|item| { + if let Ok(MediaType { + ty, + subty, + suffix: _, + params, + }) = item + { + let q_accept = match (ty.as_str(), subty.as_str()) { + (APPLICATION, OCTET_STREAM) => Some(Accept::Ssz), + (APPLICATION, JSON) => Some(Accept::Json), + (STAR, STAR) => Some(Accept::Any), + _ => None, + } + .map(|item_accept_type| { + let q_val = params + .iter() + .find_map(|(n, v)| match n.as_str() { + Q => { + Some((v.as_str().parse::().unwrap_or(0_f32) * 1000_f32) as u16) + } + _ => None, + }) + .or(Some(1000_u16)); + + (q_val.unwrap(), item_accept_type) + }); + + match q_accept { + Some((q, accept)) if q > highest_q => { + highest_q = q; + accept_type = Some(accept); + } + _ => (), + } + } + }); + accept_type.ok_or_else(|| "accept header is not supported".to_string()) + } } #[derive(Debug, Serialize, Deserialize)] @@ -1268,6 +1290,11 @@ mod tests { assert_eq!( Accept::from_str("text/plain"), Err("accept header is not supported".to_string()) - ) + ); + + assert_eq!( + Accept::from_str("application/json;message=\"Hello, world!\";q=0.3,*/*;q=0.6").unwrap(), + Accept::Any + ); } } From 2548be3e661f8f930b46a796c1707fc1bf48c06b Mon Sep 17 00:00:00 2001 From: chonghe Date: Tue, 13 Jun 2023 13:12:56 +0000 Subject: [PATCH 41/63] Minor revision in Lighthouse book (#4385) ## Proposed Changes Correct some typos in the book, also update information about withdrawals since the Mainnet will be having 700K validators in about a month --- book/src/LaTeX/full-withdrawal.tex | 2 +- book/src/LaTeX/partial-withdrawal.tex | 2 +- book/src/SUMMARY.md | 2 +- book/src/advanced_database.md | 2 +- book/src/builders.md | 3 ++- book/src/imgs/full-withdrawal.png | Bin 263209 -> 263064 bytes book/src/imgs/partial-withdrawal.png | Bin 175823 -> 175937 bytes book/src/partial-withdrawal.md | 2 +- book/src/voluntary-exit.md | 20 +++++++++++++++++++- 9 files changed, 26 insertions(+), 7 deletions(-) diff --git a/book/src/LaTeX/full-withdrawal.tex b/book/src/LaTeX/full-withdrawal.tex index 2447ba0974..a4b384872b 100644 --- a/book/src/LaTeX/full-withdrawal.tex +++ b/book/src/LaTeX/full-withdrawal.tex @@ -37,7 +37,7 @@ \rput[bl](9.0,-3.49){27.3 hours} \rput[bl](8.8,-5.49){Varying time} \rput[bl](8.7,-5.99){validator sweep} - \rput[bl](8.9,-6.59){up to 5 days} + \rput[bl](8.9,-6.59){up to \textit{n} days} \psframe[linecolor=black, linewidth=0.04, dimen=outer](11.6,-2.19)(8.0,-3.89) \psframe[linecolor=black, linewidth=0.04, dimen=outer](11.7,-4.79)(7.9,-6.89) \psframe[linecolor=black, linewidth=0.04, dimen=outer](3.7,-2.49)(0.0,-4.29) diff --git a/book/src/LaTeX/partial-withdrawal.tex b/book/src/LaTeX/partial-withdrawal.tex index 05db3b6888..4d1d0b5f0a 100644 --- a/book/src/LaTeX/partial-withdrawal.tex +++ b/book/src/LaTeX/partial-withdrawal.tex @@ -31,7 +31,7 @@ \rput[bl](0.9,-1.59){Beacon chain} \psframe[linecolor=black, linewidth=0.04, dimen=outer](10.7,-3.29)(6.8,-5.09) \rput[bl](7.6,-3.99){validator sweep} - \rput[bl](7.5,-4.69){$\sim$ every 5 days} + \rput[bl](7.82,-4.73){every \textit{n} days} \psframe[linecolor=black, linewidth=0.04, dimen=outer](3.7,-3.29)(0.0,-5.09) \rput[bl](1.3,-4.09){BLS to} \rput[bl](0.5,-4.69){execution change} diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 8fc2c2f836..7431d22387 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -47,7 +47,7 @@ * [Running a Slasher](./slasher.md) * [Redundancy](./redundancy.md) * [Release Candidates](./advanced-release-candidates.md) - * [Maximal Extractable Value (MEV)](./builders.md) + * [MEV](./builders.md) * [Merge Migration](./merge-migration.md) * [Late Block Re-orgs](./late-block-re-orgs.md) * [Contributing](./contributing.md) diff --git a/book/src/advanced_database.md b/book/src/advanced_database.md index f9996ec65d..d951104054 100644 --- a/book/src/advanced_database.md +++ b/book/src/advanced_database.md @@ -28,7 +28,7 @@ some example values. | Research | 32 | 3.4 TB | 155 ms | | Block explorer/analysis | 128 | 851 GB | 620 ms | | Enthusiast (prev. default) | 2048 | 53.6 GB | 10.2 s | -| EHobbyist | 4096 | 26.8 GB | 20.5 s | +| Hobbyist | 4096 | 26.8 GB | 20.5 s | | Validator only (default) | 8192 | 8.1 GB | 41 s | *Last update: May 2023. diff --git a/book/src/builders.md b/book/src/builders.md index 8d727ef2ce..6db360d70e 100644 --- a/book/src/builders.md +++ b/book/src/builders.md @@ -108,13 +108,14 @@ Command: ```bash DATADIR=/var/lib/lighthouse curl -X PATCH "http://localhost:5062/lighthouse/validators/0xb0148e6348264131bf47bcd1829590e870c836dc893050fd0dadc7a28949f9d0a72f2805d027521b45441101f0cc1cde" \ --H "Authorization: Bearer $(sudo cat ${DATADIR}/validators/api-token.txt)" \ +-H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" \ -H "Content-Type: application/json" \ -d '{ "builder_proposals": true, "gas_limit": 30000001 }' | jq ``` +If you are having permission issue with accessing the API token file, you can modify the header to become `-H "Authorization: Bearer $(sudo cat ${DATADIR}/validators/api-token.txt)"` #### Example Response Body diff --git a/book/src/imgs/full-withdrawal.png b/book/src/imgs/full-withdrawal.png index 6fa2db6a9137da077a72c658bb8f8ca514bcf0f6..c16d8352696cc399d6368bf4e79ac42bb0f41dea 100644 GIT binary patch delta 47018 zcma&OcT`jB_64dKH9|_LR!a2edS=REB5CLj&Sjl@qm2 zy;OeF%}2w*5lqKTe01yOD-TI7j^yGG11Be<=q?Gwqu<-ImiBR|wV|z^_}R3DjMc@( z^1_&j$Eys2`({FpTf|pRclCzMc76^;lHUM=;nh>WxF)B41GiPltWhZs#bE2h0Qn}g zbSt3B&)o`Yf8by>(6h;$fxpbrI=3>Dve|Lp$Q#euxJvqiyN4>+|shG!>Y-~@-`UDV<^ zuaeWO%1l{QxKg{H1{tR6xBG(5voW2dCwAtF_^}Rrw?c9w>l9961N3S;;q&T8w^L~a zX-|DW-loS4%v)~ z5`2N|PwE!(%P9-}bDk#z>Lg5j4ySLsWd4M9H2jEuc+)Czzh`f7wSDhWz>6cgjPH%Y zRVhLY0~_h;cQo9(8rlMDPHOHs>Lw(`*l9-DFFK2m_9BkQC|1`YA$i!q^P0e2z1xMV z$t|ge?%%Jru zh9E-~T->iCFJk<#{K-`lY@K73>l%`QufB4a5uSy{%-0e1?20 zyr$8sQKk`MEL~*1!BXAMXsysF_47fz50Ez*-5k587k=kOM_*+<6?}ivBwLuG~ z^)$i}j6aP=)qAyq1z4M6s;yoy+p<(C>)1Pc`k}R0BJZ3Dj2}jQAWzI4mw85z2(l$_ zfG(S;CHP5C-222x#LEX|Mlo`-YQ6ZadDd%N3xo81#D0`oJ{=Grr%dV#Jl?V0g~L>(Tc^?xP-F{>Q1PG8W!ohIk2^-bpQs!JNB1P(HxH z$%VC*Bmjcfn4yf%((Mb6HpvjkW$LAypXSVh&F!Oy|U;89&QZF|4op*RI zrwmwP3a5RB?|rhzWJwmzG$2!i&&u)Vk@w@#0Q{Uc(u7+r3fdM*8JQ(E6eh)0Gm{dm zFdB6&CN_2boh8Rl!SU{Go8X>{_2m?T=&u3V^J?rI!n62=M!N&^)+$P=VGivK1=(Nm zj%ehAZE?(qS@BBT(~^aL-cm1AAWq!v34knJAO>-~wZ|nOt{3K*rOcW6@^KP53v*V8 zTLD>%o9^1r2lZW@{t?ba!Tu`6pFbzQ4e9j5JTCO+o;35B3TZ2V?-aT9-<59fe8jh( zl9{i$L>jBvESsc%j zksfw~POewS|C}%Cq=rYS5T+I^1->IVNj-{JkuU8()stUkB zyjDgo8ojK$2>;IS(0y?>caElLQ>&ZPfLCqjN5kGPc_$*qCxa8KlE*}xPI)Zco~$G$ zl42fQSGoOAPKO}0qAvg8sQ@s9x|zC!J5qk3#yeu5CBl$|%>`)Obd#@T;d&CLd@M7zZB~q_y}$w;WVOT+@Y?O90)6ykS#^O8FJJWDuf9q1EZ~KVw5a%zCsxac z;QavnZR9%IzKwlhuSq+9Evx6l8zi^(dPOC-R`tfA1mBKf)m?dQ4E-kU)eRPLz8}&P z{#$Mv?Tpxn_uhlL0cX8R7|bCTN^nby;3|v54`$=tf9hb;p^JrVwg-S>D;9H(Zh*F# zrdluh%hsnt!R)>Wr_WWLXy(MH7r>-KSKmwJJ8T37X9eu3EEOiBaF}f9V4(nw>ZYI* z{y>eHg5qS7jwek_MPu*Bair{8+k`OKP7W-L;S)1a)!>P!iQ~hngosmC`C5AE+}jyuJWmrV*p2=(M*|v|5SWrx+>Q6${FR#8Pl`;F-|?NYv(3P~ z-K)ndiVxpE=pJ?XBvQZKrzKt{d%Qnm*G^$$&X44S@Cf^=XWq(GNpAP7D6DzRXpku> zd^n7SOF4Ncob_5xntb(GgztAsjqY=Xf$utql60Tx@N!J#Ll{gC`hj3ZUTFlplmk>) zz7ccdbt2H}2^WLNTl!y8!-u7w+!Q!2CsRwLnzJa@WN=Iq>JHdElp4vH2KPN(&34~37eg#Rd^J-8$ zov%10?a{Pf*Bb$wrIL@R?p^((%1*x2&k<~V&GI3d0@rd>qMC*8q6#ua&oUyO&|a@R zcdmha@#dFS=?FZ&1#;lRs~0@^CImvyM_f!Cl~V^MNICYy$@4P?kw}?C zU&2BG7H{N*Ij4cs|8(#>)h@Huz!fP0d|LjXMd4qkfdlMpQ+X*DUV_j=nT38&L1^g} zH|mx0t-kKe(w$nTdbGy=;5}X7>ZPQTU7Ul>hKeiUsE#`2T^^1RuSd|}ngJ05j?s`^uP92UC5YH(3 zSVh8~jyLxK{O=QYERM&~DgnTgaHs@rv*LR58F$+Z-YtW~@O?HHFTdN2#gkB#_^h2rnubp>8>Y)giVZB$<|TvHNX5epO!dV9 z+B>wfGzhQwdz(8TAT~ve(36A6vD%$1uqZDvBI}D?jG}Q8+UN`eKrD*29TjBIsifCR zrmJ^z=@zcgruZEzxFfSGR9a(OjbEnCrFGD&?ZA7Kf~)8{#*dGsc~ZtAFqD21wdHV| z<4d5y{XKk@26mE_t~7zYdkHN?K-g7cbuqY%%#sfnb<@e0imf^n$Drf}(tu zHG#*m6RvV5aKa@!qth3jRQAe#cH<}H0^~a6Jdzw~htw3_1MZk$Z^;oggD%qVBZCkk z-+`a!<2QuIllTbcN~ui+nhF{2w|#C8GH0DrK1;N1T1i}5Xw} zsrbs&?WPFr1fW-C)JEQrOOT!2-RY6Q)jPA-hn+%^s|ij)e-_eoB`M`m8ouf>DDthk zv~v6AM*5mRe+Iu+u}k&ON9EJ6am&RGH9wF%NY?G}^k=_h&&j4)2d*yEZKkmJkqa0; z#8XO-(EwP#O>Z_~fx?;kB1n!w^c;0<={Owi{m@sf6;LA+Aeeu|t5$0or9!eHw;^lW z{a>Xg6vlk<>CR^iTJI6g9EA|WV$*pIVJ5}KTz&c5lmz-c#TLPC2M?A*^eLGF9ehu9 zB?G#BGc8SQ%5RCdAhdlxagOH}_f4&Ua3aRsS~CHSJQ3#By8pA3s{*VcCs`Ba+YU<= zM;`vbIXo@^yV+Ffi_w{FT&ONt?8b`oc1Y}0Cz8%9C6r$I02tel(0F@qZo-$;uvkFu zYOB#h8cvSZsJC!R00krTz8U($5LNsXB9exNCRVTW^Ysu&0KyDxuW537qhL;aKTJhq zH2D*IW|$om`ca)L)c%ua#BXJjh3WyJ;X9QqY(!g?^qGf#m=>ri^z3{Si@cE{G7_<= zXY(d;MGLE;r!uMLqspWr(^8GRpxsB$MaxoI9H==pP~$#hd-Wh(*PqMy$#-w$X-9#^ zAD+BSInIUJxm~djSq>C3tZ#E{g6+0qs70C(wmVlz#(3n#B438A+P+2}RX2d7yY69C zf=(EDQ?~KOP$!(o%aKDDvtkeJx?z;gw$QjsY{ptF{i@_2+$DU(o7Awgl|GyRM~sRjPWmQFxO zju7ile^Li@L>3Fypl~H4Qpr4@9wY$gw2x6NV&YzT+$m5phO1J!BrKli-P@VUP z7}x3$ytGn1DthkC82gxzuxq#EabmM^W}$vNfbJ#G^8))C!m|ly!A@)xUTMG?=HYCU9e~5bVvU6P{p3lhYL-b@Qpy3(J zl-SGt^op##@977ltZXos&!114077O6El`gjEU+6>z_Hihq{Rq5!?#5Ko{Z{%#N4*H(G3`_(@8R8zXQ zGV5RnB;TKHPhQJ+lNszT8@Zu8xif*;!3lG~fxMCvqYS3rp9L_qRWSUYR=(KlK$DD> zFBRs<7O?%jORHKRrp9#B#AChXJod$lW+d-oA=%6cMD)M}j)y^aousUewkGmK~=nQO#iUrC(Qx6^&Dq*RQ&L)pz!_ zdWR?1MJ{bSS^dP=2XQBu;?jutz4I(?Hz{I$ei%RZ9>pphZ}{|p3E9>xJ9hoW{g>BX zO3ivg{Y|=Juc4&!5O#ogh3 ziA0q-MHcy^;F%{MpCkoZonZrh`sdLOe4nT(MQT{EgJkBJq1FAFn3$m$v^yni&cSIU znfT(daBIFdew=a*pOG!%c;Q>ZspJ_){*WRh5>hyJM%YV}>6fEMr;7oHj$V&1OkOVa_e2ZUfw_ryHeB8siz) z^5RfZcNmIm!2p>MIAlLtgR6xj2%AsTcd?4!0e0!xz9z%_FDPbb4*GM7}yzQr`7luSRop1V-Q zmaQ2x8Jova)SA;5Sr#t)wJ!4Fk5}H_6CGg05Yy5|}#OVr;e< zuLLvq)g^!BP>eVl5`j#6!qgmIE%1_~hI*^?tJ+)|@?RKfABJ zo0`bNdn#Kc(rNhKe!m9!N}q2p8#3iEy$qR|qlA@_)(m1EN`=SfKEADTr<4`v_@Kz> z7Z%s=NwqZj);+y;4|C3C`*r05o}oJPhvq=8)7OUBQc1k_AoEfA>OuYtAM~N4*6!U; zbb4E?21YwTr*MIy*>%RfwM>{_dS_;`~+HXI$9(71u3SGt#9&TBvM>Ef1A*`b~%PPe(RwNd%tE#_NVE9ZpdnxZDW)C*$^Cq6pY| zJQw(z+l?F0`p*T=HOmMjEbN2ioaH~rIWUZpK!G59L%X4bm!uG!Y26Y4et_F*dSZi} zI7s37TD%D>p`$kk|F7Xsp?EnK?3R0W*`FP1$DJDmnU2>dh5Vaq^?&`@c2J0`KB<|p zkGnA}S~D4Z?a6T2K0bOSWlfcP$BMQL?Eg@;m# ztZuuZqM7Uha;hyX3`Wm~Op&nv?JDvQ$_l*s|9aHZ%u0cWPyRuklvROx=lk22OnIW% z6y>zv-Vmq<39N%hRdeWjAVv9Ks{nw@;0;odq+n}j1d?*C<}VIZHIm}6BZ`?mUnl*o zEByX}{hQ=ra8(xb{+y>JH4{L(OY-S_?=_dPnri`sZU1{macj~I;6AZNAXn%VK*@ZaCyE)6 z;L`?snUmq4zf!HhC%pUmKU2`jHM<^kkEEu}FpoJU<*e+*=?8#Kk|wzVgmc)3K!R$1x9AVodW{MvH3- zn_svO%!za0Zv~k6Z?sQTTOWVLEdi4?GL1iuJPy;;Ss_;s0E+hwyCYSqmsmGKw~K9l+`TYp zasUT4#p98wp7Zeapr5EalG1Opjpo|o)b6^s; zy+z7=n;*BY*mOD;q+d$3^!{1)nNABCu=^9>uixwvIv6FatN|x}GSjcPTVpv@N%^p% zPDTp#+HNjccDF8w+CgjNO?6(h;}YHSr;{5WcgC&GdZlfFBuDG^LjMf@ca!d4o(zgd zdg|8{L7M-X*W$pToxPvy^FJRr()rVfyPWq_PyOul{P&!;T#fivumGh1W5NgC{~K96 z8js)5DUXv~5T+s3D?aUHnn&N#Qh;0L2k<^pauN7bij5n%>J?mP(-zvkV&9YedaYXb zpz%*11OAbU!OtC0E{0OEUhas#!kDj%&RD}(Zyx0*fr--a&rqmH9~4tnLn&Z(9Z@jD z0-dkY>}7`%pcn=|$&SH)XA(obk6*YVk))ajG7BM~+9)ycuKIB6U!Mp5`wNnV=J$in zwysaLwIN~jtvx9cTOTha{NaoEs{}}{JYHtgHjS2e)bj;UboiV45tpmxI}R${>vU2UPm+0{j2tnSeN+1TZ z?7xw6l-Yi_^)a2*^M$;964NoW#!LC_e>*`914Y-ygGUteR^SO-|4gro6fn$3ep+jk zAM@xInW|eV1uoF@>rjA#uC5Qrv~1nY12q0-MGO>A_J9+{z^&8?q*G}vA1PtmG#%|; z0J|{tVUUw;qmo~yIZ@adFl>7txKr$JxM2NX;rQ=`LQrd2knAs0^FU4v6`C+m(7M?z z#-3>5(OK53av=mP*kXsorW3zm=a1vlemsBqY;zm+AEbZza39XFpS%J#UB7ozPi^X^d7m;Ul69w=F6jSa=0a4 z)?XiEKUhw|hr@gV5n!9W67eTfAM=2Q>wk*T;CcI}VJHr9iQy?oYCWj|S;bb~BnvuF zRBBuwae6=J+OM$e3J}d}Kp@)&%D7h&t)4r+loN>mXKDrhY&`<6H0X)wvjq2bs%8dt z0W4rz@5=g{P)#7IJO%ovVCUIxWBp>-R`cV4Da-0odh zI@wa10`dO0iF{wCfJt>Fg8u$yQWHy}=_7#oKW^rK?ge-C(?Wh~D@lgYv(wvqkImFs z{A&jrZ&4Zuw1w&ki^WsZkz!QxE0u=(t&ENL_4dTR{J(q)V8JYa>5i>&x9WWeiOF0B zgYul$NifA??&{w?U*_9Y5QwPOfCs0oP(P;?lxZ&&FH$TX?=TH$jQ=%q&NW8fC7lJI zqn)9dCH9kjbqdzLNC4Q%@ZLWF5v{n>Cx7$6UoNBDDJ7%%&c|S{(GF}8D@B0(y$#~_ z^u%(`xIIPw(_g~AzI)@(!UFcjrQemcl25g_rTH8yCE=*+Q~nCjF;uy;YA7UoDNKMT6wlmhwB8KM()=A8YftXXNp#75LilG z>wZ`ez4z3l9RVY&`j6xA;^myt>Pp?kPieYK#Z{Ds=!M;YoJW7tX~5`bk*5$Lid#Jr ze{^Hi@csMypaM>(dfw`ASqwPPYIID~13;+V3Mv%rfg3=iHBy~D;06fm$Kc zv|SzO>#)7KyM;bcIQ%W$6pqE-0>o&zXygt-QJnhOFJF5Z!hB8}7QS1)p0QUC@Kb;| zl!6_uU=er#o@j^EAB-zKwFucC7h~OA(um+PGrEA@q;-B>?SprswA!JD~NbbFVX6`WI6v2rbt^-;s19$)gc6>e{4O4N&i$J9qiXR4U zhiDY5ycCQ7ddtQdB9Rs{n>^2-Y&U(}dO?hWA3*zi$ox*WYrWUFnZWeaf606T6oF5U z*G@WSK*G3E2F!NovKl6NutS&6wD@hQrXV5kTh3DRgPA6z5}I`16&vw&N8CXo%Uelsa6R7IA@oQUgQdf zk;T8}Ru3f^88f!BBSkO&LFJGNI!V?nXZ;8oYRbM!jrKfu27yC)Non08fH8?4f^l9M z3VB=tI%(yX!Q^7IJ^x+2hEtg9M)uL}mkQx^C%A@f0mwYE_YhT< zH1R#gfnCK`P#9J*YU!ln6f8*w$0$gTxwVz;jyQ$s`cKo_kPNqbOK+V@nO{C!6|?v_2sW+40h2_raKw33ab8oX?6_Bbh}HErKDzr!k%;sMyDueW0p$ArTEq zOIxrBnR}Q;kp3yJq2SA+8hmgFZz#N&WA@53lMchGazzjat%tty9&^VsHM=`Ih$Wr} zayKI)c#mflV4lN!qMw%OR9Qw|ya?5c+5<@-u91+N5liWqYqGkfqa+YE42QTPy(&*n zT?z<&tf>M@D6r?yT!)V&tC34v75~6j)xs7PBE1eR(m+Igz8ZvM9ZMQCJ(3*vvkju1 zlwaM>&#>YUeB#$O-&0jicuE!4^|BQFKd5_7$wh~}6)au^}G2N?SFYf!S#={A#c6=nQ! zq*$ZRJMEkgE;-Rs5eW~Fw%YR_Q)>z~Lpg#Ys`8So!b-pVv2Y*pD1cTpM<#=^`s_3OuLqkSoZHqQ&s$u(Ft& zW&PfHxo(k*qAG@htS|tU1bqUfsg*)Jb$5o+o3h3AqMD^NrYO6`4J>z7pEb!p0dt}F z`V9VM09#QUDp32X`Y2#%S_d``pC2m)7kYPV_lcQG#Wg<$36l%O?>=Z!v|!G8S(qn2 z&9j7lgUS#+`V-tLrgRkWkD*9>UyaKOP@h4o9-{1i^l?_FXx zOYjiefUjy0WI0)fQW|yHwX{n509kK>7?JNjXwW*8(=fsS)`Y$!+uP~N(4C;=)SK~AQQZMK>1KVH-(jSrI zK5mwDorMBE z)*s7Z#tU7zX8|kwbT3=Lg;B>d#zRTJ1snH%TeelCb!-1$h?i5uM+$=1D1Yb%s0Yu{B`JhHbpLkG@B&noh38E-0nRC-{J& zoc}kW3=5(jR7LCoG-L?M#hXT~MwXDmH z@HrO~_d4g*saiWEM}V{S`4z2GpR0bq-53)O7D9#1;1%WpYUD?Sm{^*Kh)>=_Av3vk z5zd8Ovnh`e2m{}t(zhi%Bk23dFOu=XbZ_Xq4Mc`kP0aJkP_e18ue*9dhwo7sT-B%J z!+XdqBH_E*3Yn;iWUKb>S3h64EqOkX{3#4kt0SD=xuJigUi0hT7p&qJ_szqb8PV7e z6)Tu^=nYd~_f00cEX97+oo&3;c2}49!9s|W!v+tsTJmf$f2a9)wIbzg&f3ssnS!Qd z!2pv>@0FBU>30y?`gv`5+TOBRPF$nkADkO5lqP6>8Z1fSEmX?v48FpqJ|sLlLHS6} zNV;%l7=UI$%}nUxB5tZ=SLSk_)?+}*38%_>FAtZ2oiBD#C6Ov`Go0f=*~kmG`48Qq zkyst5`a|X*vB4BDaEvJ|i>`4W%1ypFGffw@(mI5wZSPC6z&>l-N-r0)1fhC4d*y`^ z%{yqVQFs4JC1rkwxAc744p;6}wKcSDq7m`H_n6{yUdWy?J7f>URhZb-aPv#rJ(l3z zz{R41X$cnCo(b0*x7Tx4L^)$JUDRu0Dvt0GAbzZw<-z|FnAPIEHpOzZKmt<~z^J9F z;FL*6eC7@}`@x-e7js)ti#JCK#-(6K=v{liUlS`0kN-Oyfq#-L1D&9o*BLB2XZpkNlKWz>@7@1p)jfcuzS%bx?L1mZLiTz!Z+t472;h$Wnm*3W{!dr=7Aa{ zkMcMP$tAsBzSx-Gr8z=^5zsXsth}w!C1q-xx%+3O5ia_7)vqal+5wY)3D|UsLuY=5 zr&%+M$)}x^ep(%QpNX2&fu$5ha9jNLl(i@OeVArwT|ik>mxI$+);uemP*xcr%`zi_ zQ=tDicRX3A=c~cM;@nGh6anj6{W`j0{WbAFY2p7PLF8=6NFMDN8$-k3I;5s(V$&?MFH!_aLxNit ze6fqAEe6ts+&)=d%Or=zpvBk(I}sShJ`%YyJoF(F7IgkJRn~<;?d7n{2R+H+;-6;; zZ#t@SaAn)PLqbMD?yCysF?n>3jETVt`;#wcwua@-#y}n+?;xhidALtsoMOZ3T%j~} zYinc&Tb-IG{6@}|R|4k2ju17JH~JyEq(%o(MLVGX)kG4UoQd{B@~WB=-T=p425**Q zoVp^br`mD+1$KU?PQ5s#aZ4F7-(9by*#Fwww)2i4wXd3m^m62iDP6+DJGG+QvIMA> z|L+zKnG$}2dx9J<%`rAE#tLFc?=N8$tA z8=zLQ=Lk|+BDmz7>(R$5(65^bV`9Snom(4nMncuo{ytP3T2=-tcal4fyz%MJI(o|_ z-_5Lzr7~JR)weK6Xhw0A^l#1V%QoSUNbYE#HNS}~zd=>QgtgNkpG7PnK4KimLCFb< zrf6Q%Xx5UvKUi%n@i+91&RdoNMUbZye|KL%DWjpiF9tyg0xjQi1N$=YP?{xO;Z2)9 zKAz=QVotj7;NT0U+dIEGikAlVc9@#MP?%lgQ`wv8`<$6sYx4q9Ydm=O{g)gC~}2jBZNapXc0;V9w^h@SOFjwf}>tXU@H^Mx)s zA`Wfu{pW;iKr%L>g{9lp;9!qrru<_r5xocIRx{e;4PhBUF<9Tcn6Nb=()w`PnIUbg zUx7L%Hh~!5)#fr7eF&tLMLQ2GVcQKVA$SFOaE=l*K+in8!0=3XqmHDod2%QJ-l}C= z5hM=y|1~pP6wi0w3*l}F%JVj(&L5-Ex_a31H182d@nj}pua|PcFwCTkeX!)}TGbCT zA!9PSdIV*JgL>)dZhnK;05;Ian0+XS_X6>Gps5DZP#CD`(-dmYWAN2s24^?ISKO9` zQ@^;AxC`-X#2$M%edE!JE4J*(weQEs?9?Y8G$>v>NTY(*QTFGJEknz7^whzEn6Nhi z019}L%x-M5`f(y~s;rIb!c0eWB#v$NnF0t%(Z2$5hWY$y=&c%KiuH&uBB`4{b$g_${& zupX0$IDGZoipk}yK9Z?I3hW63y|0X zhc1fGU8>|pYw!+(;vel^n-|A+4qc>u7-co$t`{ipbUYNRJqOVx@o!IT423dW_rUJq zs1?UkpwcntsIGAY8J!hI8l9<9)|nGsEmt3B!AWtleh}JmbiO zS7C<9mwG7WMf_6x9@(BaIJQ9b>)Av=Ik0Z2JjmOfEebYe;$=A}o+mVM244$(wcse= z|0Xd0&-1dFD_O`5nRnGwpSv_@Q6!h21Wvh}>OMn9qpwj7(i!c%dr)pd1^O6sd4Sfc ztdcWp3>Uat!IVb(h~`zdO;fdl<-PDs2pJ7 zT6$gRv{4W*#Jgfb|Av!HzgDjM<(6CTL?-I~i_ECJODWrHUV3|AZN~}#5;oC(aU>nbP)tT7KMHJj+LOw6c&U6Cs>FP17x=lPV<=YMYmPD&g_qZ(+>Km<&Q{{z zbU_*bhMaJSS7I*?2D?k70HU9!N1yqYj~;)a9dqk3#LFL^b-o44URyO(M1X~H3kfq6K*5oM9P}UZ1NI}$-`K^ z!HG7()3D9vHIPTQA7fX((CutYnYK6yNVtGhTp4IZY~Qs8ZDG@!p%B!H+luU}{93(4 zXoc{g#vSiI4gU7BS2SUn;r74Ekpf!Rj`y)jrDJ?Xxi8{hMBy?(U1i#H)W>Sz_3^^ueosye zT+TS&!@62^bW65mPafOiIqL%YMwn{8pWB`rxJOhe>y#vrR4Y|DX0dLH$pIi!Oe8$` zX_45(v(J}2SpPRRF0Z~i3hwSHS_q=Z#ikXl@f?#q99kxD)+<27W=Iq$1P5Tg{-N0= z$t_<9I)|%x3mR_x`B*u}&nxY>Fv;k(7(eW6l!JOYaF0Z~Mc97@@88QAEUWabs;$D{ zD zaV%)(c*k1G+HL+}0N@^cAbhV59KP<@)zW%N*G21~zoBNL4jpvSXb;Lh0@=;Q*x2aC zHp$Et=r2q)fJx4sa9@RZLGGsq%fYumJ|0S9N)*-_gBy^%La=flZKxRggVGF5-hDp z`=|>iM<1m++tz6DKCxcO{N4H2Qh0jqA}`-hSR{e`3SH|dj2krk4+VEJ_(BH^P$>da z)RRx&{jS!octaiwlXESqUY=_n3~w8qhq3#i!DCRH-Ih(n@3l4mZjpVx0@`fMWnhv^@Go z8X*5qmjSVd_ykm8Z?5;;AoP%!lr#5&RfAhFE4my#p1V>YJ~T;Dd&sn>THqI!iF&A4 z`vWq%olesKs@0_{)y*;ziSe2WgfUX>f9X6Dorx-o#8kU<>uy>RID^)oFO@K^v94NY zY<=2=W2?aHCy2${6uAj(Y$&{Fj0w}<2S92%>-U+X1ZuW6IFPpI{DEoBsZYXp~6~Z zbJ&a~J~fynI36@pc5p=4xjIv$67-PXxpdLq4`3RCEU*H(^z%yg6T&UNlR@V@Kx#{M zB;!=YVqdhEJ6z#eO!J%G@J7`TjsydXHgBkYtqzIk_RnX z_G#NrrGi9`TfS^$A?5!hzo5)`y|>;|gpcy!p%>)1iYd@CqnQnj_BXb3O$%#g0jV5T1G;G35)mOgj~{hOdfZ@cGs=iPjGenn9>1 zdk%{5FITW*Q<+s}#X)nj!y{P8V0;7srqXx)^?|)%c{~1HBVlnPiW5<2!`FA_ zcuxyWQ5ijgBqF6NIkoHqSDdUPjUtQ^Ramc%gbPXws={ia7U0WV#(k(jNcC)4x8w-T z7!b!1MJ`2H^d5C)Y?Hu@L6-_$ay8@DQ!$3EwUyMRl1HH!c><*kYk~u!qa5L2=JKJc zNf!-QG-mL4uNQ#zJOWHG@rzo4YOk=>h_P~i-o1wjdGbYkBpb%eha5=gSlG5rV*Q*< zv8M7kD0MNcP3Z!J{_^XIPTHgSjib-4meDWXJd}ftI{Fd(5@w7m{@Un~>yoU%kKkJ7 zR511YDrx8Tx#Am?9LnND3%sQ%TvremT>qx9)w2)2kO7~vtlMpEkoEMdU0rAe{dN3k)mIWC!XbibqFPub2$!<^YtuolqbkLgZH#hC?!u^ zuCP0i1HI37H5ymdc7A~p@c2F(W=XVuz18HQX**I}7P=sWXekm&KA28*?e4 z?_fAbMpQy?LWI`0@S{G|VXEIOiAQYV-c!Jh%mB~EF1-Etx0}Xc&z0UK+L{e^hFm)J zYoOjL9#sdv++pUk{8Z<6b#~S>Qpb0b)tK5nI}7veV2Z6_gU03}q9Is==l3k!Z~MEj zOc^iXK}Rrxi`If>_?n~cZK^?Kgz<(fIM`=I!9@jtzHXBib%tI>(0b`(0a1X6ysT2N zl}E}WIl=eENHQ6@L81}&@@9UIL1|^xTBBTyG$AqwdfEY+VZHCS_iuCfe&G!Jd_?}g1rThgA|xnYTb=Ia?^vWJxKWe1OC4^P|M2pftt2TVwrwWvDL&u*@DDG;EfV4A5q@bG06I9ZoWa$MP`W9ve4}}8M zoC2)Tf%=y&huT9+xo4#k;QW~kgAcNyKIug&ucf15>th043(Lvx%k;x^Y-~?^mEBJ& zWe49Q=H1Z-;bxNW=F(t+vP!`=iF)8WH~@J$QPU9(GpT1FNPIQ)`c%AK0X48wY#lxH z#`_cK=9y`@8zOL&c+DF`ms9zvE^m$Q7XCl%eR({T>)(H&46;vlAqizoX^fppWkN`@ zh9k+6WoXKB7lX2lO182_Xp=25*)=nvjI9u|42>;oS+f1ETb=XW&i8qq@3Z~>dA!bf zo%0%F=DM%@y07c=d4JyT_vfndKS{IMc4}kh%({;*9Xqbm^=2*@fI@aVl-i<{#^~A` zS9csnEG9+gMuwk{y&bw`_cn~2s{!O^GGTi2E2yA+qxp6#I=qQLYbW^B6LA7J#Yd^3 zbJ+kD;@o|?&~EuH!Wh@9hJ-|?Jp9s3)9K+M*;iiw063RYQ z{F8I&J9FH3yqDZ*gAv{vTun+vDU!VNOzfCvymH*AnUn(z>Q6E($75%1wp(M7 zMh-3et~R)E`8;w{lJ@>=BVG3#rVA}3Kgl#i8>BfOS2DP$-+zL-Hzf}7y3GHZQ@)gh zK$B(Lg2?4-91@E8>YX>&jYmYF+>m>2M~;dwn!Z{%zW6E5SW7T5a-_Jd8PeEK7!mLB zpjNAGuft3g2fB9eyXaIZbx`vB7Y*jN+>6pxj2D@I=so!&sc~V$T-Il!4Px;d+uyr# zX64p>`9D9P!Njba1KGP#yy%TBTR{R+!4n#aWL0Wy#%pN1%%z?WX}R7I@VV3VKoxY- zgo4+u-MaFnxSc*fftN1a7mQ=$?6n9bt4 zTxD{zvaQpPbI_u)W!qJ6_OaZnJ@cy*sAAYyWbaqCC&77 zLhhOMX&bjgd(MQKulfCW=DFnF{PLwOR8uv#Zn)B-np(wwaQdIb*}lAc$6jyqySqI+ zAWA%XOUWT~>gn&4wtE^`1gZUxZ^oVcCmFggZz%e)#?<~NTh95%JyN-9CvNzmRI6U; z%vRG=l;S;rjq?U7(~yeyGW94vB_%gStb6`>wduS z8dUFJHtGtPuB<=GZ;K4A{N}FqIQ!4D-Gi@=Q#bA_J006(fUSV?oYcdl`hUJ5-EYTP@EOh&zWEGF;sR-aY2?+S%Nzz6ng`^7FvS7zzdAWx(ym^>fw?!1ZRzm*$KX+NQFN?JgH%nKCK!ukPGu#m@C zXvaU9KqG+4cGkg#m!|=q0O;A|WWf|1Ie>K*KJtMWy6ev=i&x|bSQ@Ugtopiq6i7X6m-bHS&jZCy*Oqu zaXSFD{tVN)-@XaD)Yjh(fQjQEyZ04Pb+?3eAQ590)*delU>hF#1yTPlhd}@>uyJaB zGkKPuC1h-RMNYP9e$-OW|IJNq*tLhle!1~%-OjT%+>As(BGiU;RI}w+fE!&7G|E1h z^yj%tTgtIReJ}?rv+|WHS&%_2$0mUo%Sva_>W)Izi>^^FD>BzpVeo$4E?Es{m<^yzi?6i!ZiW?-`|V+{ufu@TWJWkvHKI{ z4qUk!X-ZVIuL@kAJM^MrJnyGX)XyTtf8TGQ|I8QdIpKRFaH!%96A?%?s~>@NyZKK{ zA~SZo-`)E)mt}vwVoDp?nyQeB3i>DG`p=FheCT&Yb!4qs`|Je%s5$?eN1pUi13i z=8S*;J4h}^TAFCeo&th;Hhyk9N&~o|k{Qud>anoUc^=L~-O_{U%0CJ2>aMd}m#$l&gg@ ztX<#Sa@b1%wWvA?NXK1RyPqhy$9WjGJp(AkudPN1mt_!}CVTNIvEbB+`|;pn%E(;fsbG7O82;0fPM4V zM}DXSTJa04y+qrtggx&sfz9*hFS4zczO@@ih4EQk%xQVv9_CRXf8zd!zl`LiG5`;f zLtYv2mr9(t5L%OA^y?4Jg{hzjfvL65W$U?}t$Y5Di+U0dQs!o4RlKd`*}L1r{+Oi4 z7Dni?>|}Z5c{`8CoMyDn$bIi$@NZXIYq^W#aS4i~`WdJK4H_nW`tLup=Mx z4g0GC!X9=Wu!;X1hw0Y}kYSU6v!0fjK~3UzQW?}_L#qnGC>O%F*`P^LG%>adxrdW^=?wGEkLRH>#{Eak=4R|*(C5A z@I^o@`ulWP#{uZ$&t6Xt{^42(csTEo&7?4hcIfWGn}0REs_^tO5X)vR-#wW3SFbNX zKp6H8*#iC~E?d|6FSA>@IlFDMSMDBs`&aM30%#BKksW{k@?nopC62)3QkZpY>GHbK z-R;SLTV~{!OGdnI3)l^5;pM&C?c=5cTt? ziSGRP;ipf(ZUKv{ZWHO}&&Yg{`DqW2pR@HCUy?C0^z&!#>bWO*rKLX?ow=S>i;wru zpXCawkwXdf_eEe@kTzQpvYf;LgVSpbUgQ4 z0ERHF^5J7*Yd)ip@LvsTzkvg6a**COt*ll>1z#fZf>B7s?U!@ERQ9&#B4OYgvx84o zQ-+tc%sf02mEkY>zhV5}Z2aHa_;;R^TZN3u+Y%E#MtLMw7c!bdfBRTHH#*%b*|iKH z-*Xi}aY!;$tUTz|1Otw&1W<{cjs%iTxga)1NxqZ)K&|NrJl077CqJhFHeV7T;@YeN z(v|{jOfUpQZHh3Vcc{%!Q65+;i|vGcXWHJs%i!<1SM%~njXH2k8G?W*m%{pY`;C{l zZ>K4aA8jwh3eJ0Apq0w2h_)O_SHj{WSCbmQw8LsBSXn9X@b}K zVfi_(-$uV;8V1yDtYLvQLiW+Q7%C%Kad8Q_138S&58N@>#X?J@h5|ROtTMoPu5)47 zc<*u%(hj~V`TJh=mkrZ`P-l_5bSgkkTrVBK&($cv|ETw=W0A&|y!nnk*XB`T1;FqW zE+MWxrr9FqOafTNf)^3Hu3ZEY>i{G$y!0pp=M+CUbo>i}9(5;N#E1e_$8t*`u8yi+ z>#3H9k#ejGq%ulUjkn)nE_<{64k>+gL;Om*w>)>Fw>&(FB;a;o;ttLM5-=>VhN&AQ zn660v0GO*L0Lxb(0mxny?Kl3}mK=Z^&5Vfjrjd8T+>SyZ$>xLEzlFemHEdDzMeZ4k zbB|!Bi%3wZI?yY*IX}UV6FEy5l_0b3NBvGdf$5L`Zl4}U9tCzBi;yCW|3}Uu$7E+n zA^pG&;gdo|cxMsE1gzuq;Ex=H);b~EXN6=T(yOZP&rNn1ze-lXn*!APx6$2BRxfmq zjxN_Siy`^#1HBO%i01Vn#r20tK(Y}9g15>f$fLA7ym?k^tyOF%G=CT}TkWY{vnD1B zesg@#80C8;S1{ETM*c_1yE}f28AC$o!tIBiNt#rELUtrv$Y!d7t0>CT74v++{N6yo zFlKR2{uv@R$tr$JQ8S`%luHAUbn1dGg6aY(aQ^+-K%j0*84TKJNMAis_6k~~X|FrY zifzn_ja|sDqS?&@O>wxJcW$cxJ_{??M?;DmbBYgwcdd@4ue&ZiTg!!h@wsh^Zsy*e^vX#c|$ zvit(XQ$y5^RjPiA>a}m91soGoATsjcx6@bBx#JD$AquK}*mkag)`lXWnBs?RxMY;( z=7*?OfXN>Prf)B@bEQ?V<$2fIW;3MoccxT{_deSAzMrW=1uG+Dgr9r|b2)Lg`p221 z5JIyt`EcJktRwe~m%`FwFr*>*#B5V`if%Ge8@b12#ogg64-=U)XEwqV106=wzFlP< zAI+2e2DjyiV&v3NO4&c<2b;Cees~e=Co`#(*Pd}p)Q?kRMrRa1GJl<55`5x9A&HMI zo!NU;==U(}=h^+-sP*eS;8lbv#DN8&tPu;zYQ4HO#up1r(j9?%lg(B&ruw-y;J$`W z<-?Rs4SqVX8DX7(g%_^nGt+Xg4GL~ApTu_RhjQT05yhrZmVp|+tse>ezLl7Knf#Qm zKO8ttv8!e!^bJ|IWxkyc9W~9)8owo-CIdHA;=;^T$*j+(HHKfR?8=#gh6GJ8t&IY< zz`&*+C)JyCJQx&+!jz2{t)LTanjB7iH{?=MB8>%N?|yDEk8eYck8()tx>6f$2Enu$y0 zuD?fy?2_^qxkqH&M|6~LbBLpFOo8&%jBsMZD@J`NZG)nM$0>?b@5{7OEPXu1<*y0s z7zVA9;A-Nip7q7du)6synTKb;GL2XbmS1s?2-}zfODSSekBk4s)z8L(is_)q1<4z@ z;BeL#S0vdbTo0YUXp)N_m>&Qft^EqX$bTkE0bB3${_pRRjiV)GAl(a`%+?%%CjvXw zD{|$nZ(CC1-1^Eirha${Fbj z?S*8Q#MZ6+^|8OMz$-?HUDS;F$Tk;RaDn)8?!u>x{;2J?PK4-rEYT;pA21oym@Z(~ z!N~!+Wj^&>U#5#)Q%t94aHJUIC_3-cCEi7h?HD&_F5I6+4TXtjt32$T&wdY&;Sd!B zn2qvA6NfK#S$7hLIX-37QGnE8EB zxbsh1zjt2!6-x9A*#CudrlUJo=$d(q?TtJpu)7-`n9V$4%kdp4kJ9kHbA=SZw!w<9cY+f8UNgtGGe;H; zQ(L^>sw|Iw%xsGMeYm1I5}Lq%BI?jbOQ(yUju3U0p^!sF)2CWl1cG2r*xKvrmDfW+ zE3aW*S8R_pTjPck@|7(fM|C9@V5b`U8t#}*tLtL?^* z=7DJEiMob1!D|Az|8P>$+(Nv60LBJ>{>bsOengA>LZI(>1^XH*Uw9`FO5CNCXJ*UY z0nIis{GMN#M*M!ope5GRDbO%&Gq`aC-Dmc}PMtF;3Tade=}N?hYlWxI=nEKJTm*$+ zywcN4D1Pp5v%RSNdazL`+FaT0w=CH4IREz)Fk9gf9I`&|z2I!Z91;tRmomD5W+e)o zYL!qKC{gR<&i?+X1}OeSn^EypwBIFfz%bbP^m(ZjivCYO(9a}7Iz8|JCZHT_mq@sk z$1I2`aufwG{v0LyUb6bu?|^k+_6%%^gH}p;P1Fn{zdv$TKS);oBD^!HB6Rye5M;mU zdqO`2`e4DPh(GS$_!tG>*^cwTNGeg!OBFnCj^O`P^Cq~#!ioR=k?eR>k`bR(5Z@!t zt0E4b6|cf~!OP)rc3G zAQFrMqN}Gk;E2|qxTECP7cfhR(w9KP9C?^UG2~{yVuhjrXTm&P+gO1=yWiKiJJvuA zUmn)y4&`ML_N_0cZJ?EZ5%v?igw^$%oy^lHkj9mq>E@zaB@?k3dxo(y}lX{ z2=>EhP!nBxgW9jc7vLB8$IkQn^8)4%-s2?P#eX9LTLAsjL{ZkR)o9=k_7n%m*?P4P z>JF2f*VDOTTgE}`FT2+$#Zm*DgCr1>e=HK;MRK`*S|m4V7u$1Wk?bdJVCp0xm-%2C zrhBr=qUB2#cAG-W8-dsL$|RgQ`E8RbXHYtL8uI&G6Sr0a*n3+v@4IbV3}@E=lL zdWG^75JnE$1ItgPD#61x!qLKdyI9Abzxhg9Y(PohUI)e$!g^U?y{n{HH!B}qg3m3c z-c<}4_f%?ElYggUM!$?JS&`N!UEn zzZYP0Z+zRjXt1X68@0$_Dl83}AX9|23FdcEU1rw@Kzza>*-dRwaZHL#qC zGBZ+p1oyBofn5Vhz)voUkpq>{b-8*i5-@X$yMQfdq!U4HFVadDam7k$dh=ZPQm6dT zD+uI__}$-uXN`#s2&hT01MSdV$WVcWZE%a>DzqNc=@rak-49k47j7m{PcNYi#x)~0 zJbR5+FR!P0q<~~j!hAPQyv0MV3}C$I_>8Dd->G&FvPYqDqOJcqIED}35~dV3gofre zU|z5cSqx}J9OO**Y`~5i2R)OD=71aD{#`&=p3sTtkwil0(*ZVVZUbv>H#1_=T|))I z{BnV|LX$vy;3E(Tjv?&|Ld~P@I%FZ!cBb%DzOg1IKO4prF@iE=JW$_J^30%%l*-J-$5@>W{O!^h)ZAH zV`DvOT^w+_cb9e?6aX|L;cKe{B1N`j?ZvGGSxVwO6-8T}SF5Ao__>99B;ats8zmIz+h zc8bg5J>a-yq8h5r+#hve5N$icu0|#zT@7M)**+_3+(!cKBhSbcz1>X$GFw!=Djftw zj$n7%4YAJxVQP{meSLPxw zUrYS4>$)hzyds#m<_kL`tM+l+XoQ`h)>FSKsX_o_3G+C}*P}+q@|`w!3BcRoO`fHD z#(I~)gl`>5qeBhl^e*0KTvY{Au%b7T7EM|P2EZ44m9!26g_;uCI!w+zb|S(pWFq?Z zL^z31z^v$aF>-qXx_u>LEX0KzqmQ#I+VvhVW!uYh$}QU6 z_AB)A3Ou!OVj8k}iZ5eYo#ds*LYJ>h58S8sca!0E$9h48)5Obx{z`lTu_DF^*y<%g z7y}Yek7m=;*1BTq9-DtwYXz}K&7R4J=@~v;#6IL$h;H3khSwm6&$0CTaHihxO=Yxl z%@43^LHgKAY>brBY8fmFqVU1+pG}e?V81gkhy3cgtc;Q$&G< zJfd|SI8WhZ-kv`PASZo(*7nMmY~zS~U5UUcv?mi~C=tJ#4*T&55|b=t5N0i6`6iz+ zn_-v=|3b`^P~=AlYkv9&y^ANL8El_}OXLyYzIX|tf_N?Skk`ERL)|@yNeP?Ar8=wS zRJbw$e7tt_I0ysf%h@iiFt28*!5xD_Jr{T2QFQ$EXxBO>vhp^MoJ99tz8k_cH#k}B zQ^u~g#2DzNibsvH2u3lfAeUIoyS6(i6Os>v%?YeR%B5pDyGI9-xP7=$+~Qt=7eJ6x zaB3lz>!vtCCu}COyH0hKLsCiHX5z?epbx$=5$Sob)VNb9H2}-*)67y~IU2cpG;`uI zaO33c84B|1Ywsq_4%4y1CU3@e0L@nmi#{qH=KX4iDyR~(M`eDqFN-bgkCtxMp5Qvh z4Jr9NLlhv)Spr})X+kIwPUU-jmyU+O+*-U{v{{1NUys@G|k#;EqOH_Kl zdvSO3<&^P+6+yb@XJa(WII5b-u2&MVA!v=?i>Fr5QJua$Zs9i9C;e&y&>lY723g)j=LCvCs#90t3tiSUQ$ z=TrPeZ};-59b@XUsgJ&zpAM&2D9Win67jE>uFqei?Iu5kHp1WD;9h6 z^mm+)acp-zS{U!BPO?HP3iZDLODbi5vv$~(g*e}%7xikB20}Q57vjSnY7TBW>aVBP zEbkQlT}^wnVT1COOUA1?QM-vz7Mhn+j{J^ijy`4AgjOECX{IozB5-)}@Cqr(DP?9I zCFaaF;w@67`Psz=;V}xpXgrI1+-?msE7NcE`a-=$fsX($?P)AN!x?Ul8g^v zt=%Vj0h8MBz2Rl|zDdSTCdi4|4S8YMWzAkubw!$FfbXw*T2-3sS z1@-Ge;Oo)MPKhr_;Yzhf3@oXB7BijIoV8=oXZa@R)GIv%dLKY`)~8~v!~Y&Auzm^> zneE7AS$`GMc^es1vwPVxC#4xlr_j4(FchGv%ui+VdvEe?L`@GHG=nKGyX+iFywG(y zvKmR0eM0dN>Nuk>@qp{z;?K`X%w4m5Cnh`%cX%Qg7kT zEcKA_l76mL%EOXq@|H01XgUK#c46@6>3D=l6|nAI#Py5lmo{7-zAyD{m|qkZE|^+Z z$QJGWCMas)cF-VBnLM_IHjQ{?<#*iYg;dMXu9|P@*q~NhgQbpV#Y&{|VRSa*UAKEH zEQ_wes+0ks=)EYs^a!_sB`b$XEz$+hK2ujWzNSfhx_Lb&fSD5G7PSTqmCdk;XtndA`3R$vA!6j+x~Fn$IcYRtS;IESYnGM&JVRmljZ|9!Vf+hFRj27Zzx2i6)=xt|^eJpBRj;5dVNfFd%vJ$zH z6E@u6G-)O|B^fPTCy8ce83pDU=b_}icRUI-`KB_XA%>k())o9Ac{hUwcfC<$n~6q| zQHw@;=$wO(LJQ9%9Z4c78Y^v+*^08Q)!2yH@__z?J%UD_&9aUQ8vZn7_g)R61s`|$ zjaJ1Gu{OMpS(Ks%4C4+YAhs??pN9l#^@B+>g1R+F^NxBo9HRQwDn^z(@IOEb*t26q z)5kbhg;3ZKTv{I#;{7goKKeKswX8wAcK*H@L-PR#8G3@tVIPh1%PPTY;FxbsO*E3Q zPWXrC40@VD$cS-N@8bg%-fN=+7j0xh<`zK}^%9PqR4?$vq@WpinYCnZX;6ty(o>$Y z1_*=yWiAEsmS|E&K02}k+tpHAR}ws%>=n(Ajtvjsuy_+d7vZ+#UL_Ai3>~JD+zzwb zMn2Ret;3y?=mxDf-X!gN9egA+W$~C&>Ql%>>o{=2)iT2zI4a)$&Ce%#w1W02)B299 zqSJZO`~b?{`BiOo%j?M2Pb{Vm1shatl+BqXuBm62yFHF2_)aS!+pcN?P!bUE!DsejhJ@W8e!k!{G5 zv993?$`(8UmGj#kE;(OdAVmfdz?`hS0yQoxU5ZODAhCHml3$5)H=yr$@twYJGyxbKw z1-G)8;zQ3n%G8$NfVCJ~*wTUamRX@3jqKOX05#g7o$eSQEn(gfrME1;%@3<^pcJl& zbK(6%ZihP6W8-7m8|JF^8mDQv*O05ZZ3@{#cnDi1gxK2E(}1Q-;?2{@iCa!RWox~< zqSe_oYf$X@B$J%UZ<+ND!dPtEV{%!PXyB?IlkG(j`T^=`Mh6jH9;d|X2|+ICQy{+m zG|tr7255M73y5npBC#L^o(uuqiGChy&dLi`A z7ZcdO<&FWG*8W!@G$y%6eu1m=GIveiV)$7J$TeB~fi+R;Y-Uu5YX@Sh4>+v#wuKU> zO1n8zIML8TKeRCf(#31w#Qub5v1T0FRRc!O?Ha}lO0z<^`}>O6ZLXVNCiep zAlqGd23q_qK7dlXaDFao#0g;g5^3j5%lF9gIc6?*b6IZ-KWgx;MA*% zaKhP~o7q_eql3+y8>0RFUS`MU)|cfRBTr0e!w#ln9I8`nfkxgoNxR}XP&rB?y|5dN z1VUc8K>2er$G!O@F4rom5SN=ibG$aKEZe~Dv+Ep#nFWalJ~Arrf3T(#!M=4{>SYYX zukJqDm6+F2HsRruqO-hhios!#p3ig0yUBFkx>dtqsL!TwK#|9gGnS_8yER^ zlg3z$LAwk`(0^TMJYzWc2<|xDli;LlVsO2^DTCnQqd3UV_Zd(lzKz6tr6d#Mrxem? z2G@^JRNA8w#)P7}6iaJ;M+C0)o*lJ$G;#0il}O*FvOybfk2g+D^M-1<*xr}*INGYp z2l;k+6$|IzdO1WP4Ag)UniBN3$%5f%(JoAVqwfd;*Mx2Hu^*^-Fdfm{0esutFYfC?#n545OS4n%HSdiv^NRfTN0FD@5^r`g?$uUZxe;=B1?b~n zMQjPpmsxngkt}+pS6PJSf7Cg0FjniwMFhJg!a}B3C4OAQ_Z@f6qb%!NzQ+8R{gcd; zSbE~uPDVi}ZLrh*S&yuqPpFQkE^d(cxN+3Lgd#8^G;%HgxLJ7q zm^+CIPZ17LBz`Rm3&#!j`Cf{z&7yXZl`4LQxQtku7i0i(38KTH^Ly$jT!vy8!qf$LDw@9pgx>8 z=?t@TFSbVoBjXoAQIhMKvgr98M@LKCc1D^E=X5n=uOcVMY9vjd&3=L^B?w)_6r%6tF%9Or9hM*2z5TULpCzY<~b3Mg(MVFH{3eFbO9Rf*L>MJ>72^p6be0# z#Bnn1&@pDq?u_izVBi(iZtYp_JhEc9Uxb;w+bLr9-I}5nbFB8Vday#fwa;bgJx-In zXyqWum8L4FwUTdox%Vt;3LEGr+%c=7*4aDX&DS$*2{xnYp0Hbr)vG?HBb0D^}+iS zhB%5kj1g>ru0C{2xK3E3t=AsBD>7k0RUZ!4lWD+aU|ZT6QnRv@w_m|GFkySs%T!+w z$Db&rDpWJ+bc)kc0{EH$8BbWt7{$^1LVzmbX6IybIP22ikqCQ1aZcGoAnYtd1OUmq zGxFIgyDn&R=WkZC)O_dPK`K;}o3&Uk*ub-Wj7u#Mkg!>84^>QqB_yTcG6kSiM6F_& zE5jYQ#RulHL2Jdi0fW;I3X<~g15&=UhHs=%<-TjDWyDG{JS}X^unM!qXDuDVGS7$3 zzw5Y#KXqnGEanGyhd1Y=xDs26i-L$5;k}u$=FS}dNBh4HghHGO{(#ida-#82eNd9; zL54?Ss{_m=Kr*u0EOPhb2p3cCmtHcVynLrJ3~TWPwG2PS&{3!MEV7^rx(zByONTN6*8aN$`l6oxtf09c5kA(o%$R}S%mAev~UIcVDIaXEt7ZO8;GMF zJ#N4ZiLEqykEGQnjU<{U8L94K4kdp1hvyrvj)C?mNZ8E~!H95{@%MLY(F?n9-=p{v z%xGBP9c1g%M3#&;}@+Jv3-Qux&mYg5fe8RR6a_Y zSOdp2eP$;@28T@)(`k(;4;ZQ^9z$tClgy$}Of6cpM5Bi=Wl3B^abl}{p7Rj7L%2T{ zrNT`e16a&3Ih_ZbqNc3!^-J}o72D4jt$BJqFMgr%ViwR%*eQ3m+@4*1emUrA7w9dn zC(!gQA<@R?gaC~xy0`r^h$3fpjdG1>%owsxR_ogOe)yR1lf-@!@I@e)F!FtOUY5YS zZ*Ce7E`nma%1#S)<%^kmD(_3rZIXUgZaE#FO;SMSRA)UPEY@KCu^C*y9VF~$Z3UeQ zEKr?TCT<;*Z(pWm-8ao8?+e{&iPaf`jR+@{4%yPE&XH(hluXXNP+Dia{~-1Wf7l%G za?dodK`UQ`I+3*NIs~Qgnkc^skq&MpO;DMxPpdZaP7{p4n_^@e55{5`Uw4=qhAB5_ z1_JYB_EZ;qJ2qv`ioDk;W+2i71N^NpeaEltrzwEiyWerd+md)}C3*h%aX&z*iJ0ml z&qRBZXC)C?^RX`U*5yY3t}8juMk>8+uG)##Li~$iCF%)Ib((iN+Z)IMv|TM@ep?q} zsfzj9=wa2*IGW?|+dG3x`M{7M^8w*@+}fAo7^eoshckkd;CYn?n!c?W`Q?B1*q=u3 z)p2k@mw2iREB(!E#)KRamDMPgtzu^FoPhFQdwK)@1l7bQXQ>(Mhv_qDP0P43HtERB zovB#c1FB^LrTU8>xD7;31OE_S6{gzgL^u?8ac=E?Q?uJGSJ@Hgn$CtZmh)e3Swo_022x>lQCxY7)6BQ2Iwac2USlJn-!Hj zfnU7qe_wU{41iuG&7C(a&N;_tI@J|RJ@6Bp3&zQ(t4OEe9Lv^eFB{jcyT-B5lR4ru z(r02_rLICTv|EdfZpj>p3Wu9dMJ=m-9xgCZrba|Jr$ z&;GTzA|3*C7?^0nnuO&qFfoxA7C)%+x3e{Lz^qObG`i1>BP{B)rWTTtvSfDY+Ymn| z?TS@4h&V-?rwbUsOaV(e+-*o^oww^Wg5GTtBdUt8Ulp|tShe}+cE)7-h$8FA84&f# zOvfwNDI2M?^$3=@0Ymms!ik)n(Tn2E%I({W(yeLu%Ww+PK(3v(;5=_ z1SUszUq%+ol6h;qn-K?aoElC9rAYcI0)lCsnzLbL|AQKK3&!Gj8rs1^O&-k`YwA7G&xEln4hz57y?b3f*T zQNApJQ8WR#@;M-!hJJ|frky%|v&VC-Xk4g6xfFu!M#8kQLPgW7ayb`%OW_@QIq}e~ z;{-TJ8dTNRolD^Ut>A3mW>BLFx}GI3q!*-l zH{OfGb`(B`i=BAa!lt{*RF%_X8P1=Y;3k_iy9@S`3&|_UJ|EcnUO)^VC8x0%>lPg( z`?}94goq2IwVhO)(oT)+bSD0-wkJLd0&FZXE5P)_%dpNrlVDBte&DPRPL^#u&*%dl zmaj#IS=i7rFyazoAa*(E*VN3@WN!O%u;j@6CI-B|93nM7Ly~B(r4>-LR?v76nGr&G z3X_6Ga+h`}cpf5Zh{(VBp$I&lk z*{G5t>i+IHKFi6HClB*=Md_jG`b!mXU(s~^izM^gOv!jRyuNch`}mN`tU!;5*i@=> zj`Cg+7aQLg%2(c+1KSh^HRun%a+l2t`gWW)2)nNN^?vrsRD!jqY$@CmN{jm18c>ev zlc4X=hqLu=We^;XWQHcVEVQ0EJ`nHuG=6DUc`iXvWiL}?Z#pVn#J@L#mE0yVerh36;pbX-_dd66-~udg(Q zv%Wvbwhwv&6OX?<)A3#bbYC`Vnwwn6e4sqTUOfTUIhd(8sw1J}I8MY08ICT8ols`!T+E-keXIK%ZK&Z$IiVgkf*QCW;6X$3ebTJOsoAY-ZY}6XcV#c4Iyx zc5MoIP7j#+L=LBIr68Y1cMs0knC9;!E3Y&xu#AowW%=Ia=)zSkM?0Lv)YT&?u>&zJ zsTomDu%BYYW7%?MpHn^?1Lc1BBz_w&-O(yJ;XDcd{YCb`lRkiTMHosJhx`})i%x~0 zo7r?_sxdXomv(k|_lqNj6In-GLUTMlHz>~!3yEX z$Xfm4Iuz9*i!H)vCvkqXz`;s`%xt#7Jq#QEXNm{Iu@pm~^wwkQl`{9vvX8?Z3Av8* z;M#0Er@qH*m(Qd1Bis~0Gc-ro{g@W|eN2dU)gAE9+;FOvXYl%yPQW{Nsxrpmj0Q0G zYBawa-B4tfb`12o-cL?+{ldr&T^$TQaYZ228Gp_hZ4Nf{GKFsAe6xkCAV+7_RbP46 z)#sdOtH&?wRC!K^M9poAp4XQarQPuGuIvR~m=ZPFp$WA2>&hQ-LDB%LQ&>5ATtwRm zaYS%`pNM_F8=9}Y-y48a&qIh^=Wufk(h)0&#j*>3NvIh>M!oojQ53={@DE~gGa34< zkilb9H~VDe{&q9SETi7L0@#LEaEi!!7FE<240Pfqkd9JXjo z{9E5gFc=9?&*!6`Ur*$}K6xc69t4$Fi(QJ9n*p7&KhwqIWu18-a_wq0X4~=e<_l<1 zs3e16lAy4MK^Sd2cHc3OD;tn%tsJWF``P$KHcRP==EOcTG%*k$m1N*G3f1?gDeAdS zxum8kev}ZPR~A=;Is%(M-8q`!`~w5p=J>VV62Kicdfj++*pl~i zB?zA%<*c{^WLq;IY$W>W%HSJwX24>_S@Z+`jF?`>&sh}-*u-r9Rz>6)x~pThIMHvE zaldX)${Cph?aBHX2OZzs^x7pG-tJIe-UayMdRN%Dx#bkO`5DP3sgR`1`sBgkFu2NH z+9G(Q@P4)QFdFz`st0=}4vqrw;y2rz)!!goKUON?>#AT$1_xHlM>8E%KlHaU5AxH( zxI#Tol(s%=FWT`@O71fNGcO<*m^JgFY_l=Gf{coMQ=Lo>n5HBnUUd-$`7}ED(2+zd zz@HGmWfGGy1u_n~ogJC8<=3$K#ncqDsn<*ktAWkiZtyw$_H!=1np>LTvvr-pJUsyx_gVQr+e{-szbc5w-{@$eS?EZhv>rh{<@-+ z4}UNT?);(!XE6H#@{o@bN#dyULJWbmjwN#Oxn8RI&DjCZ`u({uC*zB?9pN$U8go^^%Ta=xdjB;*yz7y;=LAS2f{7pY1eY=A zC75tY&t%O|vG;)jBLIO!f5p2-=&9KTsIiSi+mb~jq-wYHWIv4)z^ngR@p>V9{ zk(|}68sjBX)g+NO85|<@K$;d&5bt8j0|hHvjOIj|-AL#*iuA76nsGl#wh!bGlwi#q z0Q@es0$tIqhpBOfx#P~&V|$&hbsB}B31T!HP!q8_#jJg?R7wCl3_Te{WsD9&ojY}} zb!RAq2V%8&5kBsq(*JQb@MN>BQ=(gyt@!(O9t$46IW{5p2wIKmXw`6Xs5=zlR@L{t zjkWU~V9|JB(M3Y#pJ2I%iIzsFLA+!98H;3zysUuCcg025V(REHA&~V$;q8;sA2-zvP?2PT zOjhA2rfcWxOG7Kp`!Hz%M{0}Pm9E!Kz*@jbFvt43+qOC99L#2W7)OZC@mt6 zqCT2`RA^#!T;Ij)aF|c&G6ayV_XVeNYWCnII^4fk$P>d6QV8AAW7#IgH>y8;yAgxh z8!b;177j({#A!}EE7tR}S6wCCk2#ZI6pCI*=h&NORtAjd@HE4lM{XWZS^!4R{uP(S z-#&~>aOJytL8xcgYWHo2-WF2llQ>t@>TbsAIGw%Q{i2;2C?boe(x5&A3cs#@%C7h@ zdz%3@U(9>UFBPG_Ix|P|j-uWrW$llr)fSH-2^?l+?<-xH?aLrJ3s<5unj-=F@+{c1jq_KZj8Z9t+Wl2{7oUh8$aUCooG0E$$}B z)9(VS)j{Gu6@N)Gq}U^=v&S+z{qb`KT>jyO^c6v~Xr6bf?c?k99R*AaeK1;2vHORh zuOMDH%ruStScKlBrBi0s_53L8ll)-G0oYkv@0C%CUW}e|G97mmnbHq7dlQ|JWIJpH z7pCnTZ@=P(F-kA9#C=fxA?_69@*cp-yCQSFA7G~orjeoQsds@40NmmMMA%hTK2a~^ z)snxsYx+BGF*NJgKJFMJp9B~8yo2%@Szjx>olpEeNT z&97d9cr{kHucw>>b%~<_fYwrPh8%R;K3H65cfp?3Pk(2sSg z=Len(qj_vL4H!9mEie&qeqFgE8Zi0%i4l%Tx}6+1cBV2RWqNjYK{S<683C0(F_InY z2jppC{cUxUdb{ill+jhjpPgt6%a=<9W2Gqgft$Pci}J=$?>Jf+5VxuYBfV*=?fHcu zy=YN7h6n^G%15q&HxD&yxA*C3Tz-xzRP*oR!aoBMlzzK_>8i^QG_l8O=j*0OdZ@1$ zmDwqT-q7lYRKE<3CS<-=(W1%Oi9I{}<^BMji=oBu7_(rl7$r=5>|T~qx)Gh?&mv_R z;dP@X?EKRY%a=vY%Gt1CALg{1^ITU76W5OGeAAgb{V1;RWG8>S6i>>WmbI-}jN?G9 zAMTwp1uuSY>MT)utczALk14j8=3muZc?UQzyac5fMbUE*mV*aK{v;+dpL&5z7jXk= zwpxmg7}LDBB5^}{QM^>JUqyoF27l%T=ttH1Ev@@f^@G9hDVK>J>I*Ti4 zNB&tgJ5oZl&l+C`_n1YzDiiYwq^VxDv#)psxJNFS_t$_TI>NX0)RjY9WkjQTUzOP{ zso7}vLJ-T##$B9f=H#LHDUF510On4XR4*nEk)8uRC%oNp&0EH)HZ4?zpv{E@jLhO8@CZf}fC zLh)sMkjSgDQww*v?oBDCoqFS)n}?}QQF?f`N5njZ6VN?ITO0JZJ|M`l_M~E*8kGZp z53Ancb17A^%DPK$7M^OD-SH?E!;7s(g4rQ~Dz#a(VP;as zE8uys>?H4_Gxh!6+TvK+%jgZz3rT?~Pq}RTw`Xn3`UN!rxij`=FBZejmJ%;WPRU}v z8zlCFCU;^ChX|VV>jB)gOb({SG($^W%O>xk3dk#^EdN~6zB?^1lx4oGQ&dQ!U6P35 zpyF7j5algSkQ%>@TZ)q%r6bNJs<2*`z``I%951`vMiP#dTEt9A<6k)IBR`8xM|ht) zPc9SB*quEnG-S0K8_!cLQ|Q{$`Ym2u9~7`G@73xE{S5LPC4#H+Dy5SEox5X25OUN zPNBJ_=!j`1(*g~q^Eu~q@11jJ?!C=&?&UAecRn88kN5Zf4)5>%J?iqeYBp zVJ%*nF3NfqNY${`+`Quij(({^04d@E8~9QhYtIz@>pz%l1lSAVyK?`oI^o4l+1+|L zRR`j7|GK)rj|Z#6P)DhmZnWIHPgT`PMz{q$JqovgnV}psZM=!I;MO0i4wa*ZtR{R_ z1xn=x{pixjpx)M$V#Nrzy-K~5N$>9}1xYkiJ04IPSr`NfQ~C}k^p$5}z97A0pr_y# z=x(!m*;RbGYLr&GDszUO#(qeLBndo3rDtifF#<9xqsy8toG35ju`d^ww)oWMqN~5y@IR zL5yJ67@$jLIMTIArm*q-$LJwjU5QzDO_)hnW9wcs7ZK!~HQUmGH3$xS+#g?CI z&EiLK7ah*mW=P!yr>&h`vZY$ZBsq(wsJQ~leloCraOcW74S*h>r7_pA5HKfGgs!ld z3|0R^^9Xq>*u?Lz?x)!V)_)kQ4B|POgQPD5=is1Cw41J4R^;s+k{u_l_$cYOCm!^Z zRPt8on&QmgrAjx-pafYW91)+f&u?~ULK0h5hv?QRW>B{Hfu1AO=zw}15WB`8kRwmZ znMoq%@&S%o8aqOL-UCTQ_baUj(|B-OY}NBW+#M#jSu=zs5qe z0TeY5n8;4{J|}GjDVsXZ%1~E5>mh<71`TfJqXX>sQKrc%x@fSQ@D&E}w@M_!KRvwg zxOF!?2r;G=Kkk*^5!QLS?8%4p zncGi3RFbc^^Nv=paQN|SicaN6d8J=g*iK~1u1S^|)~1wuKdB}Aju5y09P%7RIdtx64>vTjd+O@tn&bnmEjI{2y z@2;xOr8-kr)tL1g<1-HJuEO1fm9LWN`dOA;z>O0!kr`_ukQdA2&zFH-a^XqytV3dB zB(hwcfZ(tn9}lo)R#CHKi+i2g$djQb9Q+C+b6s!c&h1IuG^T(ErmtJTSD&gT08EAZ z7OtTi#VfRFFs6ohEv-tjTQ1A-9ixYyggGoWAj!U`4%a!2*twurB4*KI4e*`I>CtTQ zBR??zxOHgXb#_#I@nFWjGAq8jdD+pa#G0EJ`)b^&r-6$`MsJlIm0v6l&kl@!J{4mK zUtz~wb*O_rRkUZs1*?yQ0fJIXOq$2AN!}Wp@gYm5V$OT~`uNE05$R+edyU8B2!0KqWE*?irp!q=Ur9=&fUO3djLovwKH?4z0~C?5v4fL(*b zlu^dM5SDOPQ0)^OcYnZRAUvR7wWd7D%c{2TQtX{|UA_TpS!G1ej@CCMJi?uPe%cE4 z{c9D}Vo9(WLHR}w42#N$u@D}<)R)|oM5OZxHdibkS#yM2%X8DkJ4IW9;gsn4k%Neb zaa^=v9S0bXLEpB&O0A*G<9l~hs$zGUC;PR7k=O;e7S*fDLTxpTl>ajzs%zouAs z&1ZVouD41z^c~X`#oBK%AUrk0gf8?P<=XN@4Zw6oVT&#PL5u%$+pG=DP!+F!bEZ+v zq;s=qjq4e{Q=enZaBRW=kK@hwa5TPZWM2ZW^_!E-(8^&-wtvoBdxfk$b3SzTRzj{+ zmrE17d)R{~KI&UEL)w`goU^J6E>m`1shw;_-YzBbW<8xuO^ogZ*Y={ zmNAwZRGLpN6&~Uaj{xH>pMN6>@cO8q_7B}Rkf?uU$$TXoR)zZ7m&GEaOs_`y`O0}R z#@RyN{dQt1-xmnGQol1}hem;kxqRQv`LADK6kwN7hfivSxn&QQ6Gvaht@Kgh>+!RE zI5vX20~Z&!X@O2Aj$Arvn>uVoCZ@vIDk5CPM!!bdPd->V!ZAc{e1;hjN7+JIE0izj0e2KF&GlOApw*oy>|f>LN5ve(mN!G6lo&Tq)C%<1OiwfAVpBRv=ES@^eRP~ zQlv|-A|M2$hjve#-^_f@%y*yX-amZ^c+TGYUGI9=T02P*l=)97pP?kg8@<^&x(>Wf#C=e!lNJ&OHYbwOjGse+_c}rVfmmqfccd!+v0(pNk%uR0Khb`;+RibNCOKJ){8;eP=cVo{)ujO$>&&W<2 ze+b)pTNCed-lKQZLUH;_0(9jg{KEyo{ZaPu+1e<#>dEcqmBu5P+}17a`f^^4SRReo zY|>w6-%+14TVW#)F?y}S&@v-$^nH_!U+~GVrpNMHpD2)Wb*F+9XWnq$vYttHfdl*9 zR)>uCDOBfRyUHRr-8@IFBR9mHx9NER?5dxt-Zp{9(Br%rdJ@x{~A(w`B#8cIj%3yx8k7i0Uf8Kc-3~OQeQ3cvBQ>YvI`~ z(eilp9erCQBh9p)8gycXh1tN%d-Yvqc&TlBo0HDY+#QR6Axt+Qf_ooR8189NG>qf3FzxRSCKP2F0LF>m?H6)?^5F|<##Yi3` zR-)Z8B>j%JiJVp#Vy_J-v}XjmGJLTwd6O~o_Qkd5L-6^ws2A4MnJhyhSbfcpvaoZdK?|Lf!2>(z5$bk zN#^%C^6v;uwNxDa`rK|eA0>s}yG=R;B|jifcnW!-t@HU;j&PuW@n!1w1`;C-*Ykpa zqs{jAnAF27?e@~DK#@z$a6lqu;bx%X&2Qt)W!Wr3vpln$!2RSh<|SQ#PhUHeKA3d? zQr66n@d4+FLx%&LI0c79z2sd@JUI`blxF)*%dY-IpNkK;$I4Pa*oJASKvPH zg<(IZ+x8o({TH%aBcC!tDwi{#9TEL}g+~5lGdjYlxL)$2q}a#aGZIMC^n)+<%@QN# z1G}tR5KZzlqf(^~<(SsN2s7tWl|si%rYrUSLg#~#p|WUR0N}rq7b1&J>9bG7fNKWUE7L}637hh(*tHJ`Q|5<5h+?dDStN;nxIF02^t-%8K8 zY7`TftDM~>tP`Q0z%XPskgJni8HB9sjj_!%jC9%5mB_XEGM)H}{1u31fv?eVCz^*} z(b^VW<=lPW0lX(bnPKp(qF+rs7wZQ2WPdL2;t{ zyBeo&Ykn@o>7k8GL|OR*!;d|I_riGbv!ii(@{G}m`Y+|%E@8JD@8iDAPe6BJiv!|$ z=IJMQS6KpIx&a_=p#v{Z&Ij=Cct`Ic^E1$HFP(+~=^fq?6!um{@vmP?No9iu|Aie| zOi6q^GpU9US@O$bS9xcl=Z>`3I^tdGP9qhM5iSGdp)05Wh-RHyWbn>QZnf}dH2iJP zi|-HV)t~o@qBVn%Md4Og5c=(K`Tdzt_c2{;Ds%XW50E1K?DJ%4&Iu2_w zf^YkuJGGW~PBXqLBU>>{rmpL|=r@X#z}QYhYFJ2+dI;y>)$8^>Dbn2>O$|?E5f1jU zb7thGOF239JnMrax7^?5|CpcRkp3CncC6q1c&l5g-FD{j`@kTN>0hiv?C*$H3>7VG zy}0+JaY%EQ@}W8Zvxo0lk%5_tse?(a>2Td|=dgFw zgWF0rLfMaI>Y6q$4VGGjaAV6ElI&;Xml67Eat7tz%`pB}?6q88o|~K$Vnxfd7B3p8mRQiKKqf1OB@2q|pZP0FQ+5jQSKDZ-J%Z{xC=1^7BVid4IX@M3 zjIwg8U8^8dIGUxfrR6tz)V~Kkg>JveqyXE?T5S_CbL(N6&)xpw>XX1vyH$A(B)0(jwq9 z9tcy!vU-nlUK}jlSPGcYjHToIKfGoqZec{_nMPV!<>$O2Wj`Ekw~?R=Cc)j3JK3MN z_0leAawN`U+)tAad_3w{@NTXw5TW2wx7k+uHsb7tai$^Z7Yrr0h*s#l07k3nJgpgu zJStcb#2U)?B324Etxru@U**!>10FZF2{w8|OQ3QNPH2hqUMP|75LqRS5PEDJ!_Pd6 zZ~O_4Wy%GT=bN}VKdUL=zaE6th+A+Duo$kXd@{|tG#JGw#>d8QeOpgYFJ-t4{$M8H z2MwYVPRlz2WuYHQ5xg=rpi-sb?^3<^%3brLcl+eiwSjB5Ed#w$ViTNhjRQud--=9~ zz=GLk#B*_SThk{*)MpA)#ZtY3Pdby<7uBp_kFrjKC3EUAued3BdANMDx~n6;O&O@M z(&pv36yDM{x^D>oT(2j=#z_+_dK@9&a;(BIqCFKsmKRN&=Hsv%-O%|v9c!?_pQ=!CA7heUs%pQcVLB#FHs?&K3&MTIRC z3fGCLH26bn+V+Yz{fK7?{jeRIH6@rs9dZU6igBJZ@h1Zdq-hvqkR=${K$YN&koq-M zdL5J1jd{=Ylv6!t3~ONP+Y*$&BD?YNoNcnCF_*zwTiJ7#-qLn9=JB^wO>BqRdVP1b zfo~fAQVm%psp30=$I%shXKWr1diKcVt<^RV8|nM(ezR}KDiz7lB9Tf3mpnXh?(9@g zcD&hnA!U$NgquNdNBymY33xjTjnL9Mh!Lmz*GFQko;_FJ{Z!vL9d!7~EnWZ)>sxk; zN{H_kH~wB~xo)>kOH^b$R2KDsez&O%-YiRdQ#VjnZ_V`c(_~f~{MMqHhQrw#!$FO_3F87Bysvx^X0jbP>RC z$8dFUVjTI47gn!gY%LLEG^7tf*l<4+l#^imJhS6?Z`xAeO2;{o5GiafhK?)#3sn#` zlm5$3OZhQ2lY+FmWi&=W5_O&1L!sh25AyFZf;-Z?FRs-*XxRHnl}+_Pzq&0*W+ZU; z8~?*NPj#qd5;}fWm!sWJoQv|FoPgWg`lSAs9PhpjhDI5B(HhwqM%|jZ1=aeYYBg3Q z`pi)oT?xmmevVJh$<9CvSiqWk<#2VrQ)_6}Be~$OnGtMELW58Y@UZcC_sCY~#_7=( zeBrI7yRboUTY295mwlCYo^-o=I8Tt&-HzR3qW(o)M&pQVI=(gYxvZ#6CLtOU$s#q7 zwd)P^*;h})0uR{c*d7+P*)kb~%zVvKYj?D0;6`8n)xn@$fmfgue=FSYis|A-^c)Jl zIqwy~Z}2kdcB*8H7m($;b>ou+f62WX-P{X31qMjtKljqwO87KdE{I=)tl&tlm@rJx zoSTEVAg;kNdiw2Fa!8rFD1&a2Os9qg^Vd4O}no1Bp$!*PgZrNGrydn7vKtpU;e(ThZnY>NR%aZAb>x2}bo8VhF|9s;) zR!r$a2x-M9?_`~C?S5|BD1XT7GO8g_#3_|v>0L4B2+Ak6bE$!dSMG({BpT%tw71Qv z#yO|?mtN)GWHmec|;LU3HlqEHhKr-YlLPqk zT>g(er&pv2{R&aU1uV(6*u1z(*K(dHsco-WqQy#jhzkON@E}CtI-H8^3C1W*2t+{K z_T!siuiVdB{m%rpd`XyaWwvhqVYJ3iIjLzyP@?38dCME4bmvxBgn?C~3#trCmv5df zj^?j$nrFvGkY?fTeRgj{ZV(coCPkFpIT z9F$;;GR19yIg-52*MWnDxOs0_2d33FFxQf&b#B7JNH7sf_aINz`NbM#NQuH&JV(5K z+{er3Z3@*oI=m0Q6{fpIBDf7+t4ItIX&ilE$!18`MnVFKV0LEr6A(Ho`eR-MmBwyvh!{BcFjJfc^{pr+qKZItYG-N;8nB zi`r=}3XDS^GKlj(wI;oxR0+3jLjmlk`|a*;G>EHT_q+swo`JZ5F(uCwy)4Xliydxr zD`h>}MIYHN9t|PZ4pSRdS!A;&w`M$thj8XoKXrS#mk%$|#k9^SB*;M6Iby6DQNAoR zhvQbE7i^w3*P!koKI;3mlv2r3LcjO>Z$UYr^e}GB*xV!nrHB~rqdU3>#H2kzWjt35 z$c5i?R9u(X)sQQ_aMG(-s7KjUBj7d@0AqxvK?9)O&{}C#B9RawdA)Iuv@&|m3kllV zIJJGH^X#P`%vb_m$XOVJtnP z^p1kYS+3cytEBLb

Z$lE6fdjEdCpu|t-G7ONz(3T& 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 E35HTH8~^|S diff --git a/book/src/imgs/partial-withdrawal.png b/book/src/imgs/partial-withdrawal.png index 0bf90b91db0270dc8d7a0618905f21301728a5d3..5d318b4e62e0663a51718fe8070ca15d1111efb2 100644 GIT binary patch delta 61109 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

hUK8mSz& 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 delta 60770 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

  • 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 3. Do I have to do anything to get my rewards after I update the withdrawal credentials to type `0x01`? - No. The "validator sweep" occurs automatically and you can expect to receive the rewards every few days. + No. The "validator sweep" occurs automatically and you can expect to receive the rewards every *n* days, [more information here](./voluntary-exit.md#4-when-will-i-get-my-staked-fund-after-voluntary-exit-if-my-validator-is-of-type-0x01). Figure below summarizes partial withdrawals. diff --git a/book/src/voluntary-exit.md b/book/src/voluntary-exit.md index d90395c07f..d298d13f2d 100644 --- a/book/src/voluntary-exit.md +++ b/book/src/voluntary-exit.md @@ -97,7 +97,25 @@ There are two types of withdrawal credentials, `0x00` and `0x01`. To check which - A fixed waiting period of 256 epochs (27.3 hours) for the validator's status to become withdrawable. - - A varying time of "validator sweep" that can take up to 5 days (at the time of writing with ~560,000 validators on the mainnet). The "validator sweep" is the process of skimming through all validators by index number for eligible withdrawals (those with type `0x01` and balance above 32ETH). Once the "validator sweep" reaches your validator's index, your staked fund will be fully withdrawn to the withdrawal address set. + - A varying time of "validator sweep" that can take up to *n* days with *n* listed in the table below. The "validator sweep" is the process of skimming through all eligible validators by index number for withdrawals (those with type `0x01` and balance above 32ETH). Once the "validator sweep" reaches your validator's index, your staked fund will be fully withdrawn to the withdrawal address set. + +
    + + | Number of eligible validators | Ideal scenario *n* | Practical scenario *n* | + |-------------------------------|--------------------| ---------------------- | + | 300000 | 2.60 | 2.63 | + | 400000 | 3.47 | 3.51 | + | 500000 | 4.34 | 4.38 | + | 600000 | 5.21 | 5.26 | + | 700000 | 6.08 | 6.14 | + | 800000 | 6.94 | 7.01 | + | 900000 | 7.81 | 7.89 | + | 1000000 | 8.68 | 8.77 | +
    + +> Note: Ideal scenario assumes no block proposals are missed. This means a total of withdrawals of 7200 blocks/day * 16 withdrawals/block = 115200 withdrawals/day. Practical scenario assumes 1% of blocks are missed per day. As an example, if there are 700000 eligible validators, one would expect a waiting time of slightly more than 6 days. + + The total time taken is the summation of the above 3 waiting periods. After these waiting periods, you will receive the staked funds in your withdrawal address. From 0caaad4c0346f8c1235fbfe43886f4a1a836ef1d Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Wed, 14 Jun 2023 02:29:51 +0000 Subject: [PATCH 42/63] Re-enable maxperf for Windows releases (#4371) ## Issue Addressed Closes #3964 ## Proposed Changes Use the `maxperf` profile to build Windows binaries, now that Rust 1.70.0 fixed the underlying issue. --- .github/workflows/release.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e6d79bd5ef..8142184415 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -134,17 +134,11 @@ jobs: - name: Build Lighthouse for Windows portable if: matrix.arch == 'x86_64-windows-portable' - # NOTE: profile set to release until this rustc issue is fixed: - # - # https://github.com/rust-lang/rust/issues/107781 - # - # tracked at: https://github.com/sigp/lighthouse/issues/3964 - run: cargo install --path lighthouse --force --locked --features portable,gnosis --profile release + run: cargo install --path lighthouse --force --locked --features portable,gnosis --profile ${{ matrix.profile }} - name: Build Lighthouse for Windows modern if: matrix.arch == 'x86_64-windows' - # NOTE: profile set to release (see above) - run: cargo install --path lighthouse --force --locked --features modern,gnosis --profile release + run: cargo install --path lighthouse --force --locked --features modern,gnosis --profile ${{ matrix.profile }} - name: Configure GPG and create artifacts if: startsWith(matrix.arch, 'x86_64-windows') != true From 0ecca1dcb086a440ac4663e3d7aceb74e4101959 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Wed, 14 Jun 2023 05:08:50 +0000 Subject: [PATCH 43/63] Rework internal rpc protocol handling (#4290) ## Issue Addressed Resolves #3980. Builds on work by @GeemoCandama in #4084 ## Proposed Changes Extends the `SupportedProtocol` abstraction added in Geemo's PR and attempts to fix internal versioning of requests that are mentioned in this comment https://github.com/sigp/lighthouse/pull/4084#issuecomment-1496380033 Co-authored-by: geemo --- .../lighthouse_network/src/rpc/codec/base.rs | 9 +- .../src/rpc/codec/ssz_snappy.rs | 591 +++++++----------- .../lighthouse_network/src/rpc/handler.rs | 14 +- .../lighthouse_network/src/rpc/methods.rs | 125 +++- beacon_node/lighthouse_network/src/rpc/mod.rs | 4 +- .../lighthouse_network/src/rpc/outbound.rs | 68 +- .../lighthouse_network/src/rpc/protocol.rs | 187 +++--- .../src/rpc/rate_limiter.rs | 4 +- .../src/rpc/self_limiter.rs | 8 +- .../src/service/api_types.rs | 28 +- .../lighthouse_network/src/service/mod.rs | 49 +- .../lighthouse_network/src/service/utils.rs | 8 +- .../lighthouse_network/tests/rpc_tests.rs | 34 +- .../beacon_processor/worker/rpc_methods.rs | 54 +- .../sync/block_lookups/single_block_lookup.rs | 4 +- .../network/src/sync/network_context.rs | 8 +- .../network/src/sync/range_sync/batch.rs | 8 +- 17 files changed, 619 insertions(+), 584 deletions(-) diff --git a/beacon_node/lighthouse_network/src/rpc/codec/base.rs b/beacon_node/lighthouse_network/src/rpc/codec/base.rs index 6c6ce2da32..d568f27897 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec/base.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec/base.rs @@ -214,8 +214,7 @@ mod tests { let mut buf = BytesMut::new(); buf.extend_from_slice(&message); - let snappy_protocol_id = - ProtocolId::new(Protocol::Status, Version::V1, Encoding::SSZSnappy); + let snappy_protocol_id = ProtocolId::new(SupportedProtocol::StatusV1, Encoding::SSZSnappy); let fork_context = Arc::new(fork_context(ForkName::Base)); let mut snappy_outbound_codec = SSZSnappyOutboundCodec::::new( @@ -249,8 +248,7 @@ mod tests { // Insert length-prefix uvi_codec.encode(len, &mut dst).unwrap(); - let snappy_protocol_id = - ProtocolId::new(Protocol::Status, Version::V1, Encoding::SSZSnappy); + let snappy_protocol_id = ProtocolId::new(SupportedProtocol::StatusV1, Encoding::SSZSnappy); let fork_context = Arc::new(fork_context(ForkName::Base)); let mut snappy_outbound_codec = SSZSnappyOutboundCodec::::new( @@ -277,8 +275,7 @@ mod tests { dst } - let protocol_id = - ProtocolId::new(Protocol::BlocksByRange, Version::V1, Encoding::SSZSnappy); + let protocol_id = ProtocolId::new(SupportedProtocol::BlocksByRangeV1, Encoding::SSZSnappy); // Response limits let fork_context = Arc::new(fork_context(ForkName::Base)); diff --git a/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs b/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs index 28fea40a20..39cf8b3eb2 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs @@ -1,9 +1,9 @@ +use crate::rpc::methods::*; use crate::rpc::{ codec::base::OutboundCodec, - protocol::{Encoding, Protocol, ProtocolId, RPCError, Version, ERROR_TYPE_MAX, ERROR_TYPE_MIN}, + protocol::{Encoding, ProtocolId, RPCError, SupportedProtocol, ERROR_TYPE_MAX, ERROR_TYPE_MIN}, }; use crate::rpc::{InboundRequest, OutboundRequest, RPCCodedResponse, RPCResponse}; -use crate::{rpc::methods::*, EnrSyncCommitteeBitfield}; use libp2p::bytes::BytesMut; use snap::read::FrameDecoder; use snap::write::FrameEncoder; @@ -76,27 +76,14 @@ impl Encoder> for SSZSnappyInboundCodec< RPCResponse::MetaData(res) => // Encode the correct version of the MetaData response based on the negotiated version. { - match self.protocol.version { - Version::V1 => MetaData::::V1(MetaDataV1 { - seq_number: *res.seq_number(), - attnets: res.attnets().clone(), - }) - .as_ssz_bytes(), - Version::V2 => { - // `res` is of type MetaDataV2, return the ssz bytes - if res.syncnets().is_ok() { - res.as_ssz_bytes() - } else { - // `res` is of type MetaDataV1, create a MetaDataV2 by adding a default syncnets field - // Note: This code path is redundant as `res` would be always of type MetaDataV2 - MetaData::::V2(MetaDataV2 { - seq_number: *res.seq_number(), - attnets: res.attnets().clone(), - syncnets: EnrSyncCommitteeBitfield::::default(), - }) - .as_ssz_bytes() - } - } + match self.protocol.versioned_protocol { + SupportedProtocol::MetaDataV1 => res.metadata_v1().as_ssz_bytes(), + // We always send V2 metadata responses from the behaviour + // No change required. + SupportedProtocol::MetaDataV2 => res.metadata_v2().as_ssz_bytes(), + _ => unreachable!( + "We only send metadata responses on negotiating metadata requests" + ), } } }, @@ -139,8 +126,11 @@ impl Decoder for SSZSnappyInboundCodec { type Error = RPCError; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { - if self.protocol.message_name == Protocol::MetaData { - return Ok(Some(InboundRequest::MetaData(PhantomData))); + if self.protocol.versioned_protocol == SupportedProtocol::MetaDataV1 { + return Ok(Some(InboundRequest::MetaData(MetadataRequest::new_v1()))); + } + if self.protocol.versioned_protocol == SupportedProtocol::MetaDataV2 { + return Ok(Some(InboundRequest::MetaData(MetadataRequest::new_v2()))); } let length = match handle_length(&mut self.inner, &mut self.len, src)? { Some(len) => len, @@ -152,8 +142,8 @@ impl Decoder for SSZSnappyInboundCodec { let ssz_limits = self.protocol.rpc_request_limits(); if ssz_limits.is_out_of_bounds(length, self.max_packet_size) { return Err(RPCError::InvalidData(format!( - "RPC request length is out of bounds, length {}", - length + "RPC request length for protocol {:?} is out of bounds, length {}", + self.protocol.versioned_protocol, length ))); } // Calculate worst case compression length for given uncompressed length @@ -170,11 +160,7 @@ impl Decoder for SSZSnappyInboundCodec { let n = reader.get_ref().get_ref().position(); self.len = None; let _read_bytes = src.split_to(n as usize); - - match self.protocol.version { - Version::V1 => handle_v1_request(self.protocol.message_name, &decoded_buffer), - Version::V2 => handle_v2_request(self.protocol.message_name, &decoded_buffer), - } + handle_rpc_request(self.protocol.versioned_protocol, &decoded_buffer) } Err(e) => handle_error(e, reader.get_ref().get_ref().position(), max_compressed_len), } @@ -228,11 +214,16 @@ impl Encoder> for SSZSnappyOutboundCodec< let bytes = match item { OutboundRequest::Status(req) => req.as_ssz_bytes(), OutboundRequest::Goodbye(req) => req.as_ssz_bytes(), - OutboundRequest::BlocksByRange(req) => req.as_ssz_bytes(), - OutboundRequest::BlocksByRoot(req) => req.block_roots.as_ssz_bytes(), + OutboundRequest::BlocksByRange(r) => match r { + OldBlocksByRangeRequest::V1(req) => req.as_ssz_bytes(), + OldBlocksByRangeRequest::V2(req) => req.as_ssz_bytes(), + }, + OutboundRequest::BlocksByRoot(r) => match r { + BlocksByRootRequest::V1(req) => req.block_roots.as_ssz_bytes(), + BlocksByRootRequest::V2(req) => req.block_roots.as_ssz_bytes(), + }, OutboundRequest::Ping(req) => req.as_ssz_bytes(), OutboundRequest::MetaData(_) => return Ok(()), // no metadata to encode - OutboundRequest::LightClientBootstrap(req) => req.as_ssz_bytes(), }; // SSZ encoded bytes should be within `max_packet_size` if bytes.len() > self.max_packet_size { @@ -311,15 +302,10 @@ impl Decoder for SSZSnappyOutboundCodec { let n = reader.get_ref().get_ref().position(); self.len = None; let _read_bytes = src.split_to(n as usize); - - match self.protocol.version { - Version::V1 => handle_v1_response(self.protocol.message_name, &decoded_buffer), - Version::V2 => handle_v2_response( - self.protocol.message_name, - &decoded_buffer, - &mut self.fork_name, - ), - } + // Safe to `take` from `self.fork_name` as we have all the bytes we need to + // decode an ssz object at this point. + let fork_name = self.fork_name.take(); + handle_rpc_response(self.protocol.versioned_protocol, &decoded_buffer, fork_name) } Err(e) => handle_error(e, reader.get_ref().get_ref().position(), max_compressed_len), } @@ -456,181 +442,150 @@ fn handle_length( } } -/// Decodes a `Version::V1` `InboundRequest` from the byte stream. +/// Decodes an `InboundRequest` from the byte stream. /// `decoded_buffer` should be an ssz-encoded bytestream with // length = length-prefix received in the beginning of the stream. -fn handle_v1_request( - protocol: Protocol, +fn handle_rpc_request( + versioned_protocol: SupportedProtocol, decoded_buffer: &[u8], ) -> Result>, RPCError> { - match protocol { - Protocol::Status => Ok(Some(InboundRequest::Status(StatusMessage::from_ssz_bytes( - decoded_buffer, - )?))), - Protocol::Goodbye => Ok(Some(InboundRequest::Goodbye( + match versioned_protocol { + SupportedProtocol::StatusV1 => Ok(Some(InboundRequest::Status( + StatusMessage::from_ssz_bytes(decoded_buffer)?, + ))), + SupportedProtocol::GoodbyeV1 => Ok(Some(InboundRequest::Goodbye( GoodbyeReason::from_ssz_bytes(decoded_buffer)?, ))), - Protocol::BlocksByRange => Ok(Some(InboundRequest::BlocksByRange( - OldBlocksByRangeRequest::from_ssz_bytes(decoded_buffer)?, + SupportedProtocol::BlocksByRangeV2 => Ok(Some(InboundRequest::BlocksByRange( + OldBlocksByRangeRequest::V2(OldBlocksByRangeRequestV2::from_ssz_bytes(decoded_buffer)?), ))), - Protocol::BlocksByRoot => Ok(Some(InboundRequest::BlocksByRoot(BlocksByRootRequest { - block_roots: VariableList::from_ssz_bytes(decoded_buffer)?, - }))), - Protocol::Ping => Ok(Some(InboundRequest::Ping(Ping { + SupportedProtocol::BlocksByRangeV1 => Ok(Some(InboundRequest::BlocksByRange( + OldBlocksByRangeRequest::V1(OldBlocksByRangeRequestV1::from_ssz_bytes(decoded_buffer)?), + ))), + SupportedProtocol::BlocksByRootV2 => Ok(Some(InboundRequest::BlocksByRoot( + BlocksByRootRequest::V2(BlocksByRootRequestV2 { + block_roots: VariableList::from_ssz_bytes(decoded_buffer)?, + }), + ))), + SupportedProtocol::BlocksByRootV1 => Ok(Some(InboundRequest::BlocksByRoot( + BlocksByRootRequest::V1(BlocksByRootRequestV1 { + block_roots: VariableList::from_ssz_bytes(decoded_buffer)?, + }), + ))), + SupportedProtocol::PingV1 => Ok(Some(InboundRequest::Ping(Ping { data: u64::from_ssz_bytes(decoded_buffer)?, }))), - Protocol::LightClientBootstrap => Ok(Some(InboundRequest::LightClientBootstrap( - LightClientBootstrapRequest { + SupportedProtocol::LightClientBootstrapV1 => Ok(Some( + InboundRequest::LightClientBootstrap(LightClientBootstrapRequest { root: Hash256::from_ssz_bytes(decoded_buffer)?, - }, - ))), + }), + )), // MetaData requests return early from InboundUpgrade and do not reach the decoder. // Handle this case just for completeness. - Protocol::MetaData => { + SupportedProtocol::MetaDataV2 => { if !decoded_buffer.is_empty() { Err(RPCError::InternalError( "Metadata requests shouldn't reach decoder", )) } else { - Ok(Some(InboundRequest::MetaData(PhantomData))) + Ok(Some(InboundRequest::MetaData(MetadataRequest::new_v2()))) } } - } -} - -/// Decodes a `Version::V2` `InboundRequest` from the byte stream. -/// `decoded_buffer` should be an ssz-encoded bytestream with -// length = length-prefix received in the beginning of the stream. -fn handle_v2_request( - protocol: Protocol, - decoded_buffer: &[u8], -) -> Result>, RPCError> { - match protocol { - Protocol::BlocksByRange => Ok(Some(InboundRequest::BlocksByRange( - OldBlocksByRangeRequest::from_ssz_bytes(decoded_buffer)?, - ))), - Protocol::BlocksByRoot => Ok(Some(InboundRequest::BlocksByRoot(BlocksByRootRequest { - block_roots: VariableList::from_ssz_bytes(decoded_buffer)?, - }))), - // MetaData requests return early from InboundUpgrade and do not reach the decoder. - // Handle this case just for completeness. - Protocol::MetaData => { + SupportedProtocol::MetaDataV1 => { if !decoded_buffer.is_empty() { Err(RPCError::InvalidData("Metadata request".to_string())) } else { - Ok(Some(InboundRequest::MetaData(PhantomData))) + Ok(Some(InboundRequest::MetaData(MetadataRequest::new_v1()))) } } - _ => Err(RPCError::ErrorResponse( - RPCResponseErrorCode::InvalidRequest, - format!("{} does not support version 2", protocol), - )), } } -/// Decodes a `Version::V1` `RPCResponse` from the byte stream. +/// Decodes a `RPCResponse` from the byte stream. /// `decoded_buffer` should be an ssz-encoded bytestream with -// length = length-prefix received in the beginning of the stream. -fn handle_v1_response( - protocol: Protocol, - decoded_buffer: &[u8], -) -> Result>, RPCError> { - match protocol { - Protocol::Status => Ok(Some(RPCResponse::Status(StatusMessage::from_ssz_bytes( - decoded_buffer, - )?))), - // This case should be unreachable as `Goodbye` has no response. - Protocol::Goodbye => Err(RPCError::InvalidData( - "Goodbye RPC message has no valid response".to_string(), - )), - Protocol::BlocksByRange => Ok(Some(RPCResponse::BlocksByRange(Arc::new( - SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), - )))), - Protocol::BlocksByRoot => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( - SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), - )))), - Protocol::Ping => Ok(Some(RPCResponse::Pong(Ping { - data: u64::from_ssz_bytes(decoded_buffer)?, - }))), - Protocol::MetaData => Ok(Some(RPCResponse::MetaData(MetaData::V1( - MetaDataV1::from_ssz_bytes(decoded_buffer)?, - )))), - Protocol::LightClientBootstrap => Ok(Some(RPCResponse::LightClientBootstrap( - LightClientBootstrap::from_ssz_bytes(decoded_buffer)?, - ))), - } -} - -/// Decodes a `Version::V2` `RPCResponse` from the byte stream. -/// `decoded_buffer` should be an ssz-encoded bytestream with -// length = length-prefix received in the beginning of the stream. +/// length = length-prefix received in the beginning of the stream. /// /// For BlocksByRange/BlocksByRoot reponses, decodes the appropriate response /// according to the received `ForkName`. -fn handle_v2_response( - protocol: Protocol, +fn handle_rpc_response( + versioned_protocol: SupportedProtocol, decoded_buffer: &[u8], - fork_name: &mut Option, + fork_name: Option, ) -> Result>, RPCError> { - // MetaData does not contain context_bytes - if let Protocol::MetaData = protocol { - Ok(Some(RPCResponse::MetaData(MetaData::V2( + match versioned_protocol { + SupportedProtocol::StatusV1 => Ok(Some(RPCResponse::Status( + StatusMessage::from_ssz_bytes(decoded_buffer)?, + ))), + // This case should be unreachable as `Goodbye` has no response. + SupportedProtocol::GoodbyeV1 => Err(RPCError::InvalidData( + "Goodbye RPC message has no valid response".to_string(), + )), + SupportedProtocol::BlocksByRangeV1 => Ok(Some(RPCResponse::BlocksByRange(Arc::new( + SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), + )))), + SupportedProtocol::BlocksByRootV1 => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( + SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), + )))), + SupportedProtocol::PingV1 => Ok(Some(RPCResponse::Pong(Ping { + data: u64::from_ssz_bytes(decoded_buffer)?, + }))), + SupportedProtocol::MetaDataV1 => Ok(Some(RPCResponse::MetaData(MetaData::V1( + MetaDataV1::from_ssz_bytes(decoded_buffer)?, + )))), + SupportedProtocol::LightClientBootstrapV1 => Ok(Some(RPCResponse::LightClientBootstrap( + LightClientBootstrap::from_ssz_bytes(decoded_buffer)?, + ))), + // MetaData V2 responses have no context bytes, so behave similarly to V1 responses + SupportedProtocol::MetaDataV2 => Ok(Some(RPCResponse::MetaData(MetaData::V2( MetaDataV2::from_ssz_bytes(decoded_buffer)?, - )))) - } else { - let fork_name = fork_name.take().ok_or_else(|| { - RPCError::ErrorResponse( - RPCResponseErrorCode::InvalidRequest, - format!("No context bytes provided for {} response", protocol), - ) - })?; - match protocol { - Protocol::BlocksByRange => match fork_name { - ForkName::Altair => Ok(Some(RPCResponse::BlocksByRange(Arc::new( - SignedBeaconBlock::Altair(SignedBeaconBlockAltair::from_ssz_bytes( - decoded_buffer, - )?), - )))), + )))), + SupportedProtocol::BlocksByRangeV2 => match fork_name { + Some(ForkName::Altair) => Ok(Some(RPCResponse::BlocksByRange(Arc::new( + SignedBeaconBlock::Altair(SignedBeaconBlockAltair::from_ssz_bytes(decoded_buffer)?), + )))), - ForkName::Base => Ok(Some(RPCResponse::BlocksByRange(Arc::new( - SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), - )))), - ForkName::Merge => Ok(Some(RPCResponse::BlocksByRange(Arc::new( - SignedBeaconBlock::Merge(SignedBeaconBlockMerge::from_ssz_bytes( - decoded_buffer, - )?), - )))), - ForkName::Capella => Ok(Some(RPCResponse::BlocksByRange(Arc::new( - SignedBeaconBlock::Capella(SignedBeaconBlockCapella::from_ssz_bytes( - decoded_buffer, - )?), - )))), - }, - Protocol::BlocksByRoot => match fork_name { - ForkName::Altair => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( - SignedBeaconBlock::Altair(SignedBeaconBlockAltair::from_ssz_bytes( - decoded_buffer, - )?), - )))), - ForkName::Base => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( - SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), - )))), - ForkName::Merge => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( - SignedBeaconBlock::Merge(SignedBeaconBlockMerge::from_ssz_bytes( - decoded_buffer, - )?), - )))), - ForkName::Capella => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( - SignedBeaconBlock::Capella(SignedBeaconBlockCapella::from_ssz_bytes( - decoded_buffer, - )?), - )))), - }, - _ => Err(RPCError::ErrorResponse( + Some(ForkName::Base) => Ok(Some(RPCResponse::BlocksByRange(Arc::new( + SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), + )))), + Some(ForkName::Merge) => Ok(Some(RPCResponse::BlocksByRange(Arc::new( + SignedBeaconBlock::Merge(SignedBeaconBlockMerge::from_ssz_bytes(decoded_buffer)?), + )))), + Some(ForkName::Capella) => Ok(Some(RPCResponse::BlocksByRange(Arc::new( + SignedBeaconBlock::Capella(SignedBeaconBlockCapella::from_ssz_bytes( + decoded_buffer, + )?), + )))), + None => Err(RPCError::ErrorResponse( RPCResponseErrorCode::InvalidRequest, - "Invalid v2 request".to_string(), + format!( + "No context bytes provided for {:?} response", + versioned_protocol + ), )), - } + }, + SupportedProtocol::BlocksByRootV2 => match fork_name { + Some(ForkName::Altair) => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( + SignedBeaconBlock::Altair(SignedBeaconBlockAltair::from_ssz_bytes(decoded_buffer)?), + )))), + Some(ForkName::Base) => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( + SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), + )))), + Some(ForkName::Merge) => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( + SignedBeaconBlock::Merge(SignedBeaconBlockMerge::from_ssz_bytes(decoded_buffer)?), + )))), + Some(ForkName::Capella) => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( + SignedBeaconBlock::Capella(SignedBeaconBlockCapella::from_ssz_bytes( + decoded_buffer, + )?), + )))), + None => Err(RPCError::ErrorResponse( + RPCResponseErrorCode::InvalidRequest, + format!( + "No context bytes provided for {:?} response", + versioned_protocol + ), + )), + }, } } @@ -742,18 +697,20 @@ mod tests { } } - fn bbrange_request() -> OldBlocksByRangeRequest { - OldBlocksByRangeRequest { - start_slot: 0, - count: 10, - step: 1, - } + fn bbrange_request_v1() -> OldBlocksByRangeRequest { + OldBlocksByRangeRequest::new_v1(0, 10, 1) } - fn bbroot_request() -> BlocksByRootRequest { - BlocksByRootRequest { - block_roots: VariableList::from(vec![Hash256::zero()]), - } + fn bbrange_request_v2() -> OldBlocksByRangeRequest { + OldBlocksByRangeRequest::new(0, 10, 1) + } + + fn bbroot_request_v1() -> BlocksByRootRequest { + BlocksByRootRequest::new_v1(vec![Hash256::zero()].into()) + } + + fn bbroot_request_v2() -> BlocksByRootRequest { + BlocksByRootRequest::new(vec![Hash256::zero()].into()) } fn ping_message() -> Ping { @@ -777,12 +734,11 @@ mod tests { /// Encodes the given protocol response as bytes. fn encode_response( - protocol: Protocol, - version: Version, + protocol: SupportedProtocol, message: RPCCodedResponse, fork_name: ForkName, ) -> Result { - let snappy_protocol_id = ProtocolId::new(protocol, version, Encoding::SSZSnappy); + let snappy_protocol_id = ProtocolId::new(protocol, Encoding::SSZSnappy); let fork_context = Arc::new(fork_context(fork_name)); let max_packet_size = max_rpc_size(&fork_context); @@ -824,12 +780,11 @@ mod tests { /// Attempts to decode the given protocol bytes as an rpc response fn decode_response( - protocol: Protocol, - version: Version, + protocol: SupportedProtocol, message: &mut BytesMut, fork_name: ForkName, ) -> Result>, RPCError> { - let snappy_protocol_id = ProtocolId::new(protocol, version, Encoding::SSZSnappy); + let snappy_protocol_id = ProtocolId::new(protocol, Encoding::SSZSnappy); let fork_context = Arc::new(fork_context(fork_name)); let max_packet_size = max_rpc_size(&fork_context); let mut snappy_outbound_codec = @@ -840,63 +795,55 @@ mod tests { /// Encodes the provided protocol message as bytes and tries to decode the encoding bytes. fn encode_then_decode_response( - protocol: Protocol, - version: Version, + protocol: SupportedProtocol, message: RPCCodedResponse, fork_name: ForkName, ) -> Result>, RPCError> { - let mut encoded = encode_response(protocol, version.clone(), message, fork_name)?; - decode_response(protocol, version, &mut encoded, fork_name) + let mut encoded = encode_response(protocol, message, fork_name)?; + decode_response(protocol, &mut encoded, fork_name) } /// Verifies that requests we send are encoded in a way that we would correctly decode too. fn encode_then_decode_request(req: OutboundRequest, fork_name: ForkName) { let fork_context = Arc::new(fork_context(fork_name)); let max_packet_size = max_rpc_size(&fork_context); - for protocol in req.supported_protocols() { - // Encode a request we send - let mut buf = BytesMut::new(); - let mut outbound_codec = SSZSnappyOutboundCodec::::new( - protocol.clone(), - max_packet_size, - fork_context.clone(), - ); - outbound_codec.encode(req.clone(), &mut buf).unwrap(); + let protocol = ProtocolId::new(req.versioned_protocol(), Encoding::SSZSnappy); + // Encode a request we send + let mut buf = BytesMut::new(); + let mut outbound_codec = SSZSnappyOutboundCodec::::new( + protocol.clone(), + max_packet_size, + fork_context.clone(), + ); + outbound_codec.encode(req.clone(), &mut buf).unwrap(); - let mut inbound_codec = SSZSnappyInboundCodec::::new( - protocol.clone(), - max_packet_size, - fork_context.clone(), - ); + let mut inbound_codec = + SSZSnappyInboundCodec::::new(protocol.clone(), max_packet_size, fork_context); - let decoded = inbound_codec.decode(&mut buf).unwrap().unwrap_or_else(|| { - panic!( - "Should correctly decode the request {} over protocol {:?} and fork {}", - req, protocol, fork_name - ) - }); - match req.clone() { - OutboundRequest::Status(status) => { - assert_eq!(decoded, InboundRequest::Status(status)) - } - OutboundRequest::Goodbye(goodbye) => { - assert_eq!(decoded, InboundRequest::Goodbye(goodbye)) - } - OutboundRequest::BlocksByRange(bbrange) => { - assert_eq!(decoded, InboundRequest::BlocksByRange(bbrange)) - } - OutboundRequest::BlocksByRoot(bbroot) => { - assert_eq!(decoded, InboundRequest::BlocksByRoot(bbroot)) - } - OutboundRequest::Ping(ping) => { - assert_eq!(decoded, InboundRequest::Ping(ping)) - } - OutboundRequest::MetaData(metadata) => { - assert_eq!(decoded, InboundRequest::MetaData(metadata)) - } - OutboundRequest::LightClientBootstrap(bootstrap) => { - assert_eq!(decoded, InboundRequest::LightClientBootstrap(bootstrap)) - } + let decoded = inbound_codec.decode(&mut buf).unwrap().unwrap_or_else(|| { + panic!( + "Should correctly decode the request {} over protocol {:?} and fork {}", + req, protocol, fork_name + ) + }); + match req { + OutboundRequest::Status(status) => { + assert_eq!(decoded, InboundRequest::Status(status)) + } + OutboundRequest::Goodbye(goodbye) => { + assert_eq!(decoded, InboundRequest::Goodbye(goodbye)) + } + OutboundRequest::BlocksByRange(bbrange) => { + assert_eq!(decoded, InboundRequest::BlocksByRange(bbrange)) + } + OutboundRequest::BlocksByRoot(bbroot) => { + assert_eq!(decoded, InboundRequest::BlocksByRoot(bbroot)) + } + OutboundRequest::Ping(ping) => { + assert_eq!(decoded, InboundRequest::Ping(ping)) + } + OutboundRequest::MetaData(metadata) => { + assert_eq!(decoded, InboundRequest::MetaData(metadata)) } } } @@ -906,8 +853,7 @@ mod tests { fn test_encode_then_decode_v1() { assert_eq!( encode_then_decode_response( - Protocol::Status, - Version::V1, + SupportedProtocol::StatusV1, RPCCodedResponse::Success(RPCResponse::Status(status_message())), ForkName::Base, ), @@ -916,8 +862,7 @@ mod tests { assert_eq!( encode_then_decode_response( - Protocol::Ping, - Version::V1, + SupportedProtocol::PingV1, RPCCodedResponse::Success(RPCResponse::Pong(ping_message())), ForkName::Base, ), @@ -926,8 +871,7 @@ mod tests { assert_eq!( encode_then_decode_response( - Protocol::BlocksByRange, - Version::V1, + SupportedProtocol::BlocksByRangeV1, RPCCodedResponse::Success(RPCResponse::BlocksByRange(Arc::new(empty_base_block()))), ForkName::Base, ), @@ -939,8 +883,7 @@ mod tests { assert!( matches!( encode_then_decode_response( - Protocol::BlocksByRange, - Version::V1, + SupportedProtocol::BlocksByRangeV1, RPCCodedResponse::Success(RPCResponse::BlocksByRange(Arc::new(altair_block()))), ForkName::Altair, ) @@ -952,8 +895,7 @@ mod tests { assert_eq!( encode_then_decode_response( - Protocol::BlocksByRoot, - Version::V1, + SupportedProtocol::BlocksByRootV1, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(empty_base_block()))), ForkName::Base, ), @@ -965,8 +907,7 @@ mod tests { assert!( matches!( encode_then_decode_response( - Protocol::BlocksByRoot, - Version::V1, + SupportedProtocol::BlocksByRootV1, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(altair_block()))), ForkName::Altair, ) @@ -978,18 +919,7 @@ mod tests { assert_eq!( encode_then_decode_response( - Protocol::MetaData, - Version::V1, - RPCCodedResponse::Success(RPCResponse::MetaData(metadata())), - ForkName::Base, - ), - Ok(Some(RPCResponse::MetaData(metadata()))), - ); - - assert_eq!( - encode_then_decode_response( - Protocol::MetaData, - Version::V1, + SupportedProtocol::MetaDataV1, RPCCodedResponse::Success(RPCResponse::MetaData(metadata())), ForkName::Base, ), @@ -999,8 +929,7 @@ mod tests { // A MetaDataV2 still encodes as a MetaDataV1 since version is Version::V1 assert_eq!( encode_then_decode_response( - Protocol::MetaData, - Version::V1, + SupportedProtocol::MetaDataV1, RPCCodedResponse::Success(RPCResponse::MetaData(metadata_v2())), ForkName::Base, ), @@ -1011,38 +940,9 @@ mod tests { // Test RPCResponse encoding/decoding for V1 messages #[test] fn test_encode_then_decode_v2() { - assert!( - matches!( - encode_then_decode_response( - Protocol::Status, - Version::V2, - RPCCodedResponse::Success(RPCResponse::Status(status_message())), - ForkName::Base, - ) - .unwrap_err(), - RPCError::ErrorResponse(RPCResponseErrorCode::InvalidRequest, _), - ), - "status does not have V2 message" - ); - - assert!( - matches!( - encode_then_decode_response( - Protocol::Ping, - Version::V2, - RPCCodedResponse::Success(RPCResponse::Pong(ping_message())), - ForkName::Base, - ) - .unwrap_err(), - RPCError::ErrorResponse(RPCResponseErrorCode::InvalidRequest, _), - ), - "ping does not have V2 message" - ); - assert_eq!( encode_then_decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, RPCCodedResponse::Success(RPCResponse::BlocksByRange(Arc::new(empty_base_block()))), ForkName::Base, ), @@ -1056,8 +956,7 @@ mod tests { // the current_fork's rpc limit assert_eq!( encode_then_decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, RPCCodedResponse::Success(RPCResponse::BlocksByRange(Arc::new(empty_base_block()))), ForkName::Altair, ), @@ -1068,8 +967,7 @@ mod tests { assert_eq!( encode_then_decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, RPCCodedResponse::Success(RPCResponse::BlocksByRange(Arc::new(altair_block()))), ForkName::Altair, ), @@ -1081,8 +979,7 @@ mod tests { assert_eq!( encode_then_decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, RPCCodedResponse::Success(RPCResponse::BlocksByRange(Arc::new( merge_block_small.clone() ))), @@ -1100,8 +997,7 @@ mod tests { assert!( matches!( decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, &mut encoded, ForkName::Merge, ) @@ -1113,8 +1009,7 @@ mod tests { assert_eq!( encode_then_decode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(empty_base_block()))), ForkName::Base, ), @@ -1128,8 +1023,7 @@ mod tests { // the current_fork's rpc limit assert_eq!( encode_then_decode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(empty_base_block()))), ForkName::Altair, ), @@ -1140,8 +1034,7 @@ mod tests { assert_eq!( encode_then_decode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(altair_block()))), ForkName::Altair, ), @@ -1150,8 +1043,7 @@ mod tests { assert_eq!( encode_then_decode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new( merge_block_small.clone() ))), @@ -1167,8 +1059,7 @@ mod tests { assert!( matches!( decode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, &mut encoded, ForkName::Merge, ) @@ -1181,8 +1072,7 @@ mod tests { // A MetaDataV1 still encodes as a MetaDataV2 since version is Version::V2 assert_eq!( encode_then_decode_response( - Protocol::MetaData, - Version::V2, + SupportedProtocol::MetaDataV2, RPCCodedResponse::Success(RPCResponse::MetaData(metadata())), ForkName::Base, ), @@ -1191,8 +1081,7 @@ mod tests { assert_eq!( encode_then_decode_response( - Protocol::MetaData, - Version::V2, + SupportedProtocol::MetaDataV2, RPCCodedResponse::Success(RPCResponse::MetaData(metadata_v2())), ForkName::Altair, ), @@ -1207,8 +1096,7 @@ mod tests { // Removing context bytes for v2 messages should error let mut encoded_bytes = encode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, RPCCodedResponse::Success(RPCResponse::BlocksByRange(Arc::new(empty_base_block()))), ForkName::Base, ) @@ -1218,8 +1106,7 @@ mod tests { assert!(matches!( decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, &mut encoded_bytes, ForkName::Base ) @@ -1228,8 +1115,7 @@ mod tests { )); let mut encoded_bytes = encode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(empty_base_block()))), ForkName::Base, ) @@ -1239,8 +1125,7 @@ mod tests { assert!(matches!( decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, &mut encoded_bytes, ForkName::Base ) @@ -1250,8 +1135,7 @@ mod tests { // Trying to decode a base block with altair context bytes should give ssz decoding error let mut encoded_bytes = encode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, RPCCodedResponse::Success(RPCResponse::BlocksByRange(Arc::new(empty_base_block()))), ForkName::Altair, ) @@ -1264,8 +1148,7 @@ mod tests { assert!(matches!( decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, &mut wrong_fork_bytes, ForkName::Altair ) @@ -1275,8 +1158,7 @@ mod tests { // Trying to decode an altair block with base context bytes should give ssz decoding error let mut encoded_bytes = encode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(altair_block()))), ForkName::Altair, ) @@ -1288,8 +1170,7 @@ mod tests { assert!(matches!( decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, &mut wrong_fork_bytes, ForkName::Altair ) @@ -1302,8 +1183,7 @@ mod tests { encoded_bytes.extend_from_slice(&fork_context.to_context_bytes(ForkName::Altair).unwrap()); encoded_bytes.extend_from_slice( &encode_response( - Protocol::MetaData, - Version::V2, + SupportedProtocol::MetaDataV2, RPCCodedResponse::Success(RPCResponse::MetaData(metadata())), ForkName::Altair, ) @@ -1311,8 +1191,7 @@ mod tests { ); assert!(decode_response( - Protocol::MetaData, - Version::V2, + SupportedProtocol::MetaDataV2, &mut encoded_bytes, ForkName::Altair ) @@ -1320,8 +1199,7 @@ mod tests { // Sending context bytes which do not correspond to any fork should return an error let mut encoded_bytes = encode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(empty_base_block()))), ForkName::Altair, ) @@ -1333,8 +1211,7 @@ mod tests { assert!(matches!( decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, &mut wrong_fork_bytes, ForkName::Altair ) @@ -1344,8 +1221,7 @@ mod tests { // Sending bytes less than context bytes length should wait for more bytes by returning `Ok(None)` let mut encoded_bytes = encode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(empty_base_block()))), ForkName::Altair, ) @@ -1355,8 +1231,7 @@ mod tests { assert_eq!( decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, &mut part, ForkName::Altair ), @@ -1370,9 +1245,12 @@ mod tests { OutboundRequest::Ping(ping_message()), OutboundRequest::Status(status_message()), OutboundRequest::Goodbye(GoodbyeReason::Fault), - OutboundRequest::BlocksByRange(bbrange_request()), - OutboundRequest::BlocksByRoot(bbroot_request()), - OutboundRequest::MetaData(PhantomData::), + OutboundRequest::BlocksByRange(bbrange_request_v1()), + OutboundRequest::BlocksByRange(bbrange_request_v2()), + OutboundRequest::BlocksByRoot(bbroot_request_v1()), + OutboundRequest::BlocksByRoot(bbroot_request_v2()), + OutboundRequest::MetaData(MetadataRequest::new_v1()), + OutboundRequest::MetaData(MetadataRequest::new_v2()), ]; for req in requests.iter() { for fork_name in ForkName::list_all() { @@ -1432,7 +1310,7 @@ mod tests { // 10 (for stream identifier) + 80 + 42 = 132 > `max_compressed_len`. Hence, decoding should fail with `InvalidData`. assert!(matches!( - decode_response(Protocol::Status, Version::V1, &mut dst, ForkName::Base).unwrap_err(), + decode_response(SupportedProtocol::StatusV1, &mut dst, ForkName::Base).unwrap_err(), RPCError::InvalidData(_) )); } @@ -1490,8 +1368,7 @@ mod tests { // 10 (for stream identifier) + 176156 + 8103 = 184269 > `max_compressed_len`. Hence, decoding should fail with `InvalidData`. assert!(matches!( decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, &mut dst, ForkName::Altair ) @@ -1534,7 +1411,7 @@ mod tests { dst.extend_from_slice(writer.get_ref()); assert!(matches!( - decode_response(Protocol::Status, Version::V1, &mut dst, ForkName::Base).unwrap_err(), + decode_response(SupportedProtocol::StatusV1, &mut dst, ForkName::Base).unwrap_err(), RPCError::InvalidData(_) )); } diff --git a/beacon_node/lighthouse_network/src/rpc/handler.rs b/beacon_node/lighthouse_network/src/rpc/handler.rs index a1743c15fb..8199bee2a7 100644 --- a/beacon_node/lighthouse_network/src/rpc/handler.rs +++ b/beacon_node/lighthouse_network/src/rpc/handler.rs @@ -245,7 +245,7 @@ where while let Some((id, req)) = self.dial_queue.pop() { self.events_out.push(Err(HandlerErr::Outbound { error: RPCError::Disconnected, - proto: req.protocol(), + proto: req.versioned_protocol().protocol(), id, })); } @@ -269,7 +269,7 @@ where } _ => self.events_out.push(Err(HandlerErr::Outbound { error: RPCError::Disconnected, - proto: req.protocol(), + proto: req.versioned_protocol().protocol(), id, })), } @@ -334,7 +334,7 @@ where ) { self.dial_negotiated -= 1; let (id, request) = request_info; - let proto = request.protocol(); + let proto = request.versioned_protocol().protocol(); // accept outbound connections only if the handler is not deactivated if matches!(self.state, HandlerState::Deactivated) { @@ -414,7 +414,7 @@ where 128, ) as usize), delay_key: Some(delay_key), - protocol: req.protocol(), + protocol: req.versioned_protocol().protocol(), request_start_time: Instant::now(), remaining_chunks: expected_responses, }, @@ -422,7 +422,7 @@ where } else { self.events_out.push(Err(HandlerErr::Inbound { id: self.current_inbound_substream_id, - proto: req.protocol(), + proto: req.versioned_protocol().protocol(), error: RPCError::HandlerRejected, })); return self.shutdown(None); @@ -498,7 +498,7 @@ where }; self.events_out.push(Err(HandlerErr::Outbound { error, - proto: req.protocol(), + proto: req.versioned_protocol().protocol(), id, })); } @@ -895,7 +895,7 @@ where // else we return an error, stream should not have closed early. let outbound_err = HandlerErr::Outbound { id: request_id, - proto: request.protocol(), + proto: request.versioned_protocol().protocol(), error: RPCError::IncompleteStream, }; return Poll::Ready(ConnectionHandlerEvent::Custom(Err(outbound_err))); diff --git a/beacon_node/lighthouse_network/src/rpc/methods.rs b/beacon_node/lighthouse_network/src/rpc/methods.rs index 5da595c3db..af0ba2510b 100644 --- a/beacon_node/lighthouse_network/src/rpc/methods.rs +++ b/beacon_node/lighthouse_network/src/rpc/methods.rs @@ -3,11 +3,13 @@ use crate::types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield}; use regex::bytes::Regex; use serde::Serialize; +use ssz::Encode; use ssz_derive::{Decode, Encode}; use ssz_types::{ typenum::{U1024, U256}, VariableList, }; +use std::marker::PhantomData; use std::ops::Deref; use std::sync::Arc; use strum::IntoStaticStr; @@ -85,6 +87,30 @@ pub struct Ping { pub data: u64, } +/// The METADATA request structure. +#[superstruct( + variants(V1, V2), + variant_attributes(derive(Clone, Debug, PartialEq, Serialize),) +)] +#[derive(Clone, Debug, PartialEq)] +pub struct MetadataRequest { + _phantom_data: PhantomData, +} + +impl MetadataRequest { + pub fn new_v1() -> Self { + Self::V1(MetadataRequestV1 { + _phantom_data: PhantomData, + }) + } + + pub fn new_v2() -> Self { + Self::V2(MetadataRequestV2 { + _phantom_data: PhantomData, + }) + } +} + /// The METADATA response structure. #[superstruct( variants(V1, V2), @@ -93,9 +119,8 @@ pub struct Ping { serde(bound = "T: EthSpec", deny_unknown_fields), ) )] -#[derive(Clone, Debug, PartialEq, Serialize, Encode)] +#[derive(Clone, Debug, PartialEq, Serialize)] #[serde(bound = "T: EthSpec")] -#[ssz(enum_behaviour = "transparent")] pub struct MetaData { /// A sequential counter indicating when data gets modified. pub seq_number: u64, @@ -106,6 +131,38 @@ pub struct MetaData { pub syncnets: EnrSyncCommitteeBitfield, } +impl MetaData { + /// Returns a V1 MetaData response from self. + pub fn metadata_v1(&self) -> Self { + match self { + md @ MetaData::V1(_) => md.clone(), + MetaData::V2(metadata) => MetaData::V1(MetaDataV1 { + seq_number: metadata.seq_number, + attnets: metadata.attnets.clone(), + }), + } + } + + /// Returns a V2 MetaData response from self by filling unavailable fields with default. + pub fn metadata_v2(&self) -> Self { + match self { + MetaData::V1(metadata) => MetaData::V2(MetaDataV2 { + seq_number: metadata.seq_number, + attnets: metadata.attnets.clone(), + syncnets: Default::default(), + }), + md @ MetaData::V2(_) => md.clone(), + } + } + + pub fn as_ssz_bytes(&self) -> Vec { + match self { + MetaData::V1(md) => md.as_ssz_bytes(), + MetaData::V2(md) => md.as_ssz_bytes(), + } + } +} + /// The reason given for a `Goodbye` message. /// /// Note: any unknown `u64::into(n)` will resolve to `Goodbye::Unknown` for any unknown `n`, @@ -197,7 +254,11 @@ impl ssz::Decode for GoodbyeReason { } /// Request a number of beacon block roots from a peer. -#[derive(Encode, Decode, Clone, Debug, PartialEq)] +#[superstruct( + variants(V1, V2), + variant_attributes(derive(Encode, Decode, Clone, Debug, PartialEq)) +)] +#[derive(Clone, Debug, PartialEq)] pub struct BlocksByRangeRequest { /// The starting slot to request blocks. pub start_slot: u64, @@ -206,8 +267,23 @@ pub struct BlocksByRangeRequest { pub count: u64, } +impl BlocksByRangeRequest { + /// The default request is V2 + pub fn new(start_slot: u64, count: u64) -> Self { + Self::V2(BlocksByRangeRequestV2 { start_slot, count }) + } + + pub fn new_v1(start_slot: u64, count: u64) -> Self { + Self::V1(BlocksByRangeRequestV1 { start_slot, count }) + } +} + /// Request a number of beacon block roots from a peer. -#[derive(Encode, Decode, Clone, Debug, PartialEq)] +#[superstruct( + variants(V1, V2), + variant_attributes(derive(Encode, Decode, Clone, Debug, PartialEq)) +)] +#[derive(Clone, Debug, PartialEq)] pub struct OldBlocksByRangeRequest { /// The starting slot to request blocks. pub start_slot: u64, @@ -223,13 +299,43 @@ pub struct OldBlocksByRangeRequest { pub step: u64, } +impl OldBlocksByRangeRequest { + /// The default request is V2 + pub fn new(start_slot: u64, count: u64, step: u64) -> Self { + Self::V2(OldBlocksByRangeRequestV2 { + start_slot, + count, + step, + }) + } + + pub fn new_v1(start_slot: u64, count: u64, step: u64) -> Self { + Self::V1(OldBlocksByRangeRequestV1 { + start_slot, + count, + step, + }) + } +} + /// Request a number of beacon block bodies from a peer. +#[superstruct(variants(V1, V2), variant_attributes(derive(Clone, Debug, PartialEq)))] #[derive(Clone, Debug, PartialEq)] pub struct BlocksByRootRequest { /// The list of beacon block bodies being requested. pub block_roots: VariableList, } +impl BlocksByRootRequest { + pub fn new(block_roots: VariableList) -> Self { + Self::V2(BlocksByRootRequestV2 { block_roots }) + } + + pub fn new_v1(block_roots: VariableList) -> Self { + Self::V1(BlocksByRootRequestV1 { block_roots }) + } +} + /* RPC Handling and Grouping */ // Collection of enums and structs used by the Codecs to encode/decode RPC messages @@ -438,7 +544,12 @@ impl std::fmt::Display for GoodbyeReason { impl std::fmt::Display for BlocksByRangeRequest { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Start Slot: {}, Count: {}", self.start_slot, self.count) + write!( + f, + "Start Slot: {}, Count: {}", + self.start_slot(), + self.count() + ) } } @@ -447,7 +558,9 @@ impl std::fmt::Display for OldBlocksByRangeRequest { write!( f, "Start Slot: {}, Count: {}, Step: {}", - self.start_slot, self.count, self.step + self.start_slot(), + self.count(), + self.step() ) } } diff --git a/beacon_node/lighthouse_network/src/rpc/mod.rs b/beacon_node/lighthouse_network/src/rpc/mod.rs index 4f7af95cfe..ffdc193bbb 100644 --- a/beacon_node/lighthouse_network/src/rpc/mod.rs +++ b/beacon_node/lighthouse_network/src/rpc/mod.rs @@ -247,7 +247,7 @@ where } Err(RateLimitedErr::TooLarge) => { // we set the batch sizes, so this is a coding/config err for most protocols - let protocol = req.protocol(); + let protocol = req.versioned_protocol().protocol(); if matches!(protocol, Protocol::BlocksByRange) { debug!(self.log, "Blocks by range request will never be processed"; "request" => %req); } else { @@ -335,7 +335,7 @@ where serializer.emit_arguments("peer_id", &format_args!("{}", self.peer_id))?; let (msg_kind, protocol) = match &self.event { Ok(received) => match received { - RPCReceived::Request(_, req) => ("request", req.protocol()), + RPCReceived::Request(_, req) => ("request", req.versioned_protocol().protocol()), RPCReceived::Response(_, res) => ("response", res.protocol()), RPCReceived::EndOfStream(_, end) => ( "end_of_stream", diff --git a/beacon_node/lighthouse_network/src/rpc/outbound.rs b/beacon_node/lighthouse_network/src/rpc/outbound.rs index 774303800e..d12f366861 100644 --- a/beacon_node/lighthouse_network/src/rpc/outbound.rs +++ b/beacon_node/lighthouse_network/src/rpc/outbound.rs @@ -1,11 +1,8 @@ -use std::marker::PhantomData; - use super::methods::*; -use super::protocol::Protocol; use super::protocol::ProtocolId; +use super::protocol::SupportedProtocol; use super::RPCError; use crate::rpc::protocol::Encoding; -use crate::rpc::protocol::Version; use crate::rpc::{ codec::{base::BaseOutboundCodec, ssz_snappy::SSZSnappyOutboundCodec, OutboundCodec}, methods::ResponseTermination, @@ -38,9 +35,8 @@ pub enum OutboundRequest { Goodbye(GoodbyeReason), BlocksByRange(OldBlocksByRangeRequest), BlocksByRoot(BlocksByRootRequest), - LightClientBootstrap(LightClientBootstrapRequest), Ping(Ping), - MetaData(PhantomData), + MetaData(MetadataRequest), } impl UpgradeInfo for OutboundRequestContainer { @@ -59,36 +55,29 @@ impl OutboundRequest { match self { // add more protocols when versions/encodings are supported OutboundRequest::Status(_) => vec![ProtocolId::new( - Protocol::Status, - Version::V1, + SupportedProtocol::StatusV1, Encoding::SSZSnappy, )], OutboundRequest::Goodbye(_) => vec![ProtocolId::new( - Protocol::Goodbye, - Version::V1, + SupportedProtocol::GoodbyeV1, Encoding::SSZSnappy, )], OutboundRequest::BlocksByRange(_) => vec![ - ProtocolId::new(Protocol::BlocksByRange, Version::V2, Encoding::SSZSnappy), - ProtocolId::new(Protocol::BlocksByRange, Version::V1, Encoding::SSZSnappy), + ProtocolId::new(SupportedProtocol::BlocksByRangeV2, Encoding::SSZSnappy), + ProtocolId::new(SupportedProtocol::BlocksByRangeV1, Encoding::SSZSnappy), ], OutboundRequest::BlocksByRoot(_) => vec![ - ProtocolId::new(Protocol::BlocksByRoot, Version::V2, Encoding::SSZSnappy), - ProtocolId::new(Protocol::BlocksByRoot, Version::V1, Encoding::SSZSnappy), + ProtocolId::new(SupportedProtocol::BlocksByRootV2, Encoding::SSZSnappy), + ProtocolId::new(SupportedProtocol::BlocksByRootV1, Encoding::SSZSnappy), ], OutboundRequest::Ping(_) => vec![ProtocolId::new( - Protocol::Ping, - Version::V1, + SupportedProtocol::PingV1, Encoding::SSZSnappy, )], OutboundRequest::MetaData(_) => vec![ - ProtocolId::new(Protocol::MetaData, Version::V2, Encoding::SSZSnappy), - ProtocolId::new(Protocol::MetaData, Version::V1, Encoding::SSZSnappy), + ProtocolId::new(SupportedProtocol::MetaDataV2, Encoding::SSZSnappy), + ProtocolId::new(SupportedProtocol::MetaDataV1, Encoding::SSZSnappy), ], - // Note: This match arm is technically unreachable as we only respond to light client requests - // that we generate from the beacon state. - // We do not make light client rpc requests from the beacon node - OutboundRequest::LightClientBootstrap(_) => vec![], } } /* These functions are used in the handler for stream management */ @@ -98,24 +87,31 @@ impl OutboundRequest { match self { OutboundRequest::Status(_) => 1, OutboundRequest::Goodbye(_) => 0, - OutboundRequest::BlocksByRange(req) => req.count, - OutboundRequest::BlocksByRoot(req) => req.block_roots.len() as u64, + OutboundRequest::BlocksByRange(req) => *req.count(), + OutboundRequest::BlocksByRoot(req) => req.block_roots().len() as u64, OutboundRequest::Ping(_) => 1, OutboundRequest::MetaData(_) => 1, - OutboundRequest::LightClientBootstrap(_) => 1, } } - /// Gives the corresponding `Protocol` to this request. - pub fn protocol(&self) -> Protocol { + /// Gives the corresponding `SupportedProtocol` to this request. + pub fn versioned_protocol(&self) -> SupportedProtocol { match self { - OutboundRequest::Status(_) => Protocol::Status, - OutboundRequest::Goodbye(_) => Protocol::Goodbye, - OutboundRequest::BlocksByRange(_) => Protocol::BlocksByRange, - OutboundRequest::BlocksByRoot(_) => Protocol::BlocksByRoot, - OutboundRequest::Ping(_) => Protocol::Ping, - OutboundRequest::MetaData(_) => Protocol::MetaData, - OutboundRequest::LightClientBootstrap(_) => Protocol::LightClientBootstrap, + OutboundRequest::Status(_) => SupportedProtocol::StatusV1, + OutboundRequest::Goodbye(_) => SupportedProtocol::GoodbyeV1, + OutboundRequest::BlocksByRange(req) => match req { + OldBlocksByRangeRequest::V1(_) => SupportedProtocol::BlocksByRangeV1, + OldBlocksByRangeRequest::V2(_) => SupportedProtocol::BlocksByRangeV2, + }, + OutboundRequest::BlocksByRoot(req) => match req { + BlocksByRootRequest::V1(_) => SupportedProtocol::BlocksByRootV1, + BlocksByRootRequest::V2(_) => SupportedProtocol::BlocksByRootV2, + }, + OutboundRequest::Ping(_) => SupportedProtocol::PingV1, + OutboundRequest::MetaData(req) => match req { + MetadataRequest::V1(_) => SupportedProtocol::MetaDataV1, + MetadataRequest::V2(_) => SupportedProtocol::MetaDataV2, + }, } } @@ -127,7 +123,6 @@ impl OutboundRequest { // variants that have `multiple_responses()` can have values. OutboundRequest::BlocksByRange(_) => ResponseTermination::BlocksByRange, OutboundRequest::BlocksByRoot(_) => ResponseTermination::BlocksByRoot, - OutboundRequest::LightClientBootstrap(_) => unreachable!(), OutboundRequest::Status(_) => unreachable!(), OutboundRequest::Goodbye(_) => unreachable!(), OutboundRequest::Ping(_) => unreachable!(), @@ -185,9 +180,6 @@ impl std::fmt::Display for OutboundRequest { OutboundRequest::BlocksByRoot(req) => write!(f, "Blocks by root: {:?}", req), OutboundRequest::Ping(ping) => write!(f, "Ping: {}", ping.data), OutboundRequest::MetaData(_) => write!(f, "MetaData request"), - OutboundRequest::LightClientBootstrap(bootstrap) => { - write!(f, "Lightclient Bootstrap: {}", bootstrap.root) - } } } } diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index a8423e47b0..ea39c1423a 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -179,21 +179,74 @@ pub enum Protocol { LightClientBootstrap, } -/// RPC Versions -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Version { - /// Version 1 of RPC - V1, - /// Version 2 of RPC - V2, -} - /// RPC Encondings supported. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Encoding { SSZSnappy, } +/// All valid protocol name and version combinations. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum SupportedProtocol { + StatusV1, + GoodbyeV1, + BlocksByRangeV1, + BlocksByRangeV2, + BlocksByRootV1, + BlocksByRootV2, + PingV1, + MetaDataV1, + MetaDataV2, + LightClientBootstrapV1, +} + +impl SupportedProtocol { + pub fn version_string(&self) -> &'static str { + match self { + SupportedProtocol::StatusV1 => "1", + SupportedProtocol::GoodbyeV1 => "1", + SupportedProtocol::BlocksByRangeV1 => "1", + SupportedProtocol::BlocksByRangeV2 => "2", + SupportedProtocol::BlocksByRootV1 => "1", + SupportedProtocol::BlocksByRootV2 => "2", + SupportedProtocol::PingV1 => "1", + SupportedProtocol::MetaDataV1 => "1", + SupportedProtocol::MetaDataV2 => "2", + SupportedProtocol::LightClientBootstrapV1 => "1", + } + } + + pub fn protocol(&self) -> Protocol { + match self { + SupportedProtocol::StatusV1 => Protocol::Status, + SupportedProtocol::GoodbyeV1 => Protocol::Goodbye, + SupportedProtocol::BlocksByRangeV1 => Protocol::BlocksByRange, + SupportedProtocol::BlocksByRangeV2 => Protocol::BlocksByRange, + SupportedProtocol::BlocksByRootV1 => Protocol::BlocksByRoot, + SupportedProtocol::BlocksByRootV2 => Protocol::BlocksByRoot, + SupportedProtocol::PingV1 => Protocol::Ping, + SupportedProtocol::MetaDataV1 => Protocol::MetaData, + SupportedProtocol::MetaDataV2 => Protocol::MetaData, + SupportedProtocol::LightClientBootstrapV1 => Protocol::LightClientBootstrap, + } + } + + fn currently_supported() -> Vec { + vec![ + ProtocolId::new(Self::StatusV1, Encoding::SSZSnappy), + ProtocolId::new(Self::GoodbyeV1, Encoding::SSZSnappy), + // V2 variants have higher preference then V1 + ProtocolId::new(Self::BlocksByRangeV2, Encoding::SSZSnappy), + ProtocolId::new(Self::BlocksByRangeV1, Encoding::SSZSnappy), + ProtocolId::new(Self::BlocksByRootV2, Encoding::SSZSnappy), + ProtocolId::new(Self::BlocksByRootV1, Encoding::SSZSnappy), + ProtocolId::new(Self::PingV1, Encoding::SSZSnappy), + ProtocolId::new(Self::MetaDataV2, Encoding::SSZSnappy), + ProtocolId::new(Self::MetaDataV1, Encoding::SSZSnappy), + ] + } +} + impl std::fmt::Display for Encoding { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let repr = match self { @@ -203,16 +256,6 @@ impl std::fmt::Display for Encoding { } } -impl std::fmt::Display for Version { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let repr = match self { - Version::V1 => "1", - Version::V2 => "2", - }; - f.write_str(repr) - } -} - #[derive(Debug, Clone)] pub struct RPCProtocol { pub fork_context: Arc, @@ -227,22 +270,10 @@ impl UpgradeInfo for RPCProtocol { /// The list of supported RPC protocols for Lighthouse. fn protocol_info(&self) -> Self::InfoIter { - let mut supported_protocols = vec![ - ProtocolId::new(Protocol::Status, Version::V1, Encoding::SSZSnappy), - ProtocolId::new(Protocol::Goodbye, Version::V1, Encoding::SSZSnappy), - // V2 variants have higher preference then V1 - ProtocolId::new(Protocol::BlocksByRange, Version::V2, Encoding::SSZSnappy), - ProtocolId::new(Protocol::BlocksByRange, Version::V1, Encoding::SSZSnappy), - ProtocolId::new(Protocol::BlocksByRoot, Version::V2, Encoding::SSZSnappy), - ProtocolId::new(Protocol::BlocksByRoot, Version::V1, Encoding::SSZSnappy), - ProtocolId::new(Protocol::Ping, Version::V1, Encoding::SSZSnappy), - ProtocolId::new(Protocol::MetaData, Version::V2, Encoding::SSZSnappy), - ProtocolId::new(Protocol::MetaData, Version::V1, Encoding::SSZSnappy), - ]; + let mut supported_protocols = SupportedProtocol::currently_supported(); if self.enable_light_client_server { supported_protocols.push(ProtocolId::new( - Protocol::LightClientBootstrap, - Version::V1, + SupportedProtocol::LightClientBootstrapV1, Encoding::SSZSnappy, )); } @@ -272,11 +303,8 @@ impl RpcLimits { /// Tracks the types in a protocol id. #[derive(Clone, Debug)] pub struct ProtocolId { - /// The RPC message type/name. - pub message_name: Protocol, - - /// The version of the RPC. - pub version: Version, + /// The protocol name and version + pub versioned_protocol: SupportedProtocol, /// The encoding of the RPC. pub encoding: Encoding, @@ -288,7 +316,7 @@ pub struct ProtocolId { impl ProtocolId { /// Returns min and max size for messages of given protocol id requests. pub fn rpc_request_limits(&self) -> RpcLimits { - match self.message_name { + match self.versioned_protocol.protocol() { Protocol::Status => RpcLimits::new( ::ssz_fixed_len(), ::ssz_fixed_len(), @@ -297,9 +325,10 @@ impl ProtocolId { ::ssz_fixed_len(), ::ssz_fixed_len(), ), + // V1 and V2 requests are the same Protocol::BlocksByRange => RpcLimits::new( - ::ssz_fixed_len(), - ::ssz_fixed_len(), + ::ssz_fixed_len(), + ::ssz_fixed_len(), ), Protocol::BlocksByRoot => { RpcLimits::new(*BLOCKS_BY_ROOT_REQUEST_MIN, *BLOCKS_BY_ROOT_REQUEST_MAX) @@ -318,7 +347,7 @@ impl ProtocolId { /// Returns min and max size for messages of given protocol id responses. pub fn rpc_response_limits(&self, fork_context: &ForkContext) -> RpcLimits { - match self.message_name { + match self.versioned_protocol.protocol() { Protocol::Status => RpcLimits::new( ::ssz_fixed_len(), ::ssz_fixed_len(), @@ -344,30 +373,34 @@ impl ProtocolId { /// Returns `true` if the given `ProtocolId` should expect `context_bytes` in the /// beginning of the stream, else returns `false`. pub fn has_context_bytes(&self) -> bool { - match self.message_name { - Protocol::BlocksByRange | Protocol::BlocksByRoot => match self.version { - Version::V2 => true, - Version::V1 => false, - }, - Protocol::LightClientBootstrap => match self.version { - Version::V2 | Version::V1 => true, - }, - Protocol::Goodbye | Protocol::Ping | Protocol::Status | Protocol::MetaData => false, + match self.versioned_protocol { + SupportedProtocol::BlocksByRangeV2 + | SupportedProtocol::BlocksByRootV2 + | SupportedProtocol::LightClientBootstrapV1 => true, + SupportedProtocol::StatusV1 + | SupportedProtocol::BlocksByRootV1 + | SupportedProtocol::BlocksByRangeV1 + | SupportedProtocol::PingV1 + | SupportedProtocol::MetaDataV1 + | SupportedProtocol::MetaDataV2 + | SupportedProtocol::GoodbyeV1 => false, } } } /// An RPC protocol ID. impl ProtocolId { - pub fn new(message_name: Protocol, version: Version, encoding: Encoding) -> Self { + pub fn new(versioned_protocol: SupportedProtocol, encoding: Encoding) -> Self { let protocol_id = format!( "{}/{}/{}/{}", - PROTOCOL_PREFIX, message_name, version, encoding + PROTOCOL_PREFIX, + versioned_protocol.protocol(), + versioned_protocol.version_string(), + encoding ); ProtocolId { - message_name, - version, + versioned_protocol, encoding, protocol_id, } @@ -400,7 +433,7 @@ where fn upgrade_inbound(self, socket: TSocket, protocol: ProtocolId) -> Self::Future { async move { - let protocol_name = protocol.message_name; + let versioned_protocol = protocol.versioned_protocol; // convert the socket to tokio compatible socket let socket = socket.compat(); let codec = match protocol.encoding { @@ -419,8 +452,13 @@ where let socket = Framed::new(Box::pin(timed_socket), codec); // MetaData requests should be empty, return the stream - match protocol_name { - Protocol::MetaData => Ok((InboundRequest::MetaData(PhantomData), socket)), + match versioned_protocol { + SupportedProtocol::MetaDataV1 => { + Ok((InboundRequest::MetaData(MetadataRequest::new_v1()), socket)) + } + SupportedProtocol::MetaDataV2 => { + Ok((InboundRequest::MetaData(MetadataRequest::new_v2()), socket)) + } _ => { match tokio::time::timeout( Duration::from_secs(REQUEST_TIMEOUT), @@ -448,7 +486,7 @@ pub enum InboundRequest { BlocksByRoot(BlocksByRootRequest), LightClientBootstrap(LightClientBootstrapRequest), Ping(Ping), - MetaData(PhantomData), + MetaData(MetadataRequest), } /// Implements the encoding per supported protocol for `RPCRequest`. @@ -460,24 +498,33 @@ impl InboundRequest { match self { InboundRequest::Status(_) => 1, InboundRequest::Goodbye(_) => 0, - InboundRequest::BlocksByRange(req) => req.count, - InboundRequest::BlocksByRoot(req) => req.block_roots.len() as u64, + InboundRequest::BlocksByRange(req) => *req.count(), + InboundRequest::BlocksByRoot(req) => req.block_roots().len() as u64, InboundRequest::Ping(_) => 1, InboundRequest::MetaData(_) => 1, InboundRequest::LightClientBootstrap(_) => 1, } } - /// Gives the corresponding `Protocol` to this request. - pub fn protocol(&self) -> Protocol { + /// Gives the corresponding `SupportedProtocol` to this request. + pub fn versioned_protocol(&self) -> SupportedProtocol { match self { - InboundRequest::Status(_) => Protocol::Status, - InboundRequest::Goodbye(_) => Protocol::Goodbye, - InboundRequest::BlocksByRange(_) => Protocol::BlocksByRange, - InboundRequest::BlocksByRoot(_) => Protocol::BlocksByRoot, - InboundRequest::Ping(_) => Protocol::Ping, - InboundRequest::MetaData(_) => Protocol::MetaData, - InboundRequest::LightClientBootstrap(_) => Protocol::LightClientBootstrap, + InboundRequest::Status(_) => SupportedProtocol::StatusV1, + InboundRequest::Goodbye(_) => SupportedProtocol::GoodbyeV1, + InboundRequest::BlocksByRange(req) => match req { + OldBlocksByRangeRequest::V1(_) => SupportedProtocol::BlocksByRangeV1, + OldBlocksByRangeRequest::V2(_) => SupportedProtocol::BlocksByRangeV2, + }, + InboundRequest::BlocksByRoot(req) => match req { + BlocksByRootRequest::V1(_) => SupportedProtocol::BlocksByRootV1, + BlocksByRootRequest::V2(_) => SupportedProtocol::BlocksByRootV2, + }, + InboundRequest::Ping(_) => SupportedProtocol::PingV1, + InboundRequest::MetaData(req) => match req { + MetadataRequest::V1(_) => SupportedProtocol::MetaDataV1, + MetadataRequest::V2(_) => SupportedProtocol::MetaDataV2, + }, + InboundRequest::LightClientBootstrap(_) => SupportedProtocol::LightClientBootstrapV1, } } diff --git a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs index 1fdc6cce3b..e1634d711b 100644 --- a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs @@ -192,7 +192,7 @@ pub trait RateLimiterItem { impl RateLimiterItem for super::InboundRequest { fn protocol(&self) -> Protocol { - self.protocol() + self.versioned_protocol().protocol() } fn expected_responses(&self) -> u64 { @@ -202,7 +202,7 @@ impl RateLimiterItem for super::InboundRequest { impl RateLimiterItem for super::OutboundRequest { fn protocol(&self) -> Protocol { - self.protocol() + self.versioned_protocol().protocol() } fn expected_responses(&self) -> u64 { diff --git a/beacon_node/lighthouse_network/src/rpc/self_limiter.rs b/beacon_node/lighthouse_network/src/rpc/self_limiter.rs index 6748a1947b..626917d6a7 100644 --- a/beacon_node/lighthouse_network/src/rpc/self_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/self_limiter.rs @@ -72,7 +72,7 @@ impl SelfRateLimiter { request_id: Id, req: OutboundRequest, ) -> Result, Error> { - let protocol = req.protocol(); + let protocol = req.versioned_protocol().protocol(); // First check that there are not already other requests waiting to be sent. if let Some(queued_requests) = self.delayed_requests.get_mut(&(peer_id, protocol)) { queued_requests.push_back(QueuedRequest { req, request_id }); @@ -111,7 +111,7 @@ impl SelfRateLimiter { event: RPCSend::Request(request_id, req), }), Err(e) => { - let protocol = req.protocol(); + let protocol = req.versioned_protocol(); match e { RateLimitedErr::TooLarge => { // this should never happen with default parameters. Let's just send the request. @@ -119,7 +119,7 @@ impl SelfRateLimiter { crit!( log, "Self rate limiting error for a batch that will never fit. Sending request anyway. Check configuration parameters."; - "protocol" => %req.protocol() + "protocol" => %req.versioned_protocol().protocol() ); Ok(BehaviourAction::NotifyHandler { peer_id, @@ -128,7 +128,7 @@ impl SelfRateLimiter { }) } RateLimitedErr::TooSoon(wait_time) => { - debug!(log, "Self rate limiting"; "protocol" => %protocol, "wait_time_ms" => wait_time.as_millis(), "peer_id" => %peer_id); + debug!(log, "Self rate limiting"; "protocol" => %protocol.protocol(), "wait_time_ms" => wait_time.as_millis(), "peer_id" => %peer_id); Err((QueuedRequest { req, request_id }, wait_time)) } } diff --git a/beacon_node/lighthouse_network/src/service/api_types.rs b/beacon_node/lighthouse_network/src/service/api_types.rs index bd3df79769..5ab89fee51 100644 --- a/beacon_node/lighthouse_network/src/service/api_types.rs +++ b/beacon_node/lighthouse_network/src/service/api_types.rs @@ -7,7 +7,8 @@ use types::{EthSpec, SignedBeaconBlock}; use crate::rpc::{ methods::{ BlocksByRangeRequest, BlocksByRootRequest, LightClientBootstrapRequest, - OldBlocksByRangeRequest, RPCCodedResponse, RPCResponse, ResponseTermination, StatusMessage, + OldBlocksByRangeRequest, OldBlocksByRangeRequestV1, OldBlocksByRangeRequestV2, + RPCCodedResponse, RPCResponse, ResponseTermination, StatusMessage, }, OutboundRequest, SubstreamId, }; @@ -43,14 +44,25 @@ impl std::convert::From for OutboundRequest { fn from(req: Request) -> OutboundRequest { match req { Request::BlocksByRoot(r) => OutboundRequest::BlocksByRoot(r), - Request::BlocksByRange(BlocksByRangeRequest { start_slot, count }) => { - OutboundRequest::BlocksByRange(OldBlocksByRangeRequest { - start_slot, - count, - step: 1, - }) + Request::BlocksByRange(r) => match r { + BlocksByRangeRequest::V1(req) => OutboundRequest::BlocksByRange( + OldBlocksByRangeRequest::V1(OldBlocksByRangeRequestV1 { + start_slot: req.start_slot, + count: req.count, + step: 1, + }), + ), + BlocksByRangeRequest::V2(req) => OutboundRequest::BlocksByRange( + OldBlocksByRangeRequest::V2(OldBlocksByRangeRequestV2 { + start_slot: req.start_slot, + count: req.count, + step: 1, + }), + ), + }, + Request::LightClientBootstrap(_) => { + unreachable!("Lighthouse never makes an outbound light client request") } - Request::LightClientBootstrap(b) => OutboundRequest::LightClientBootstrap(b), Request::Status(s) => OutboundRequest::Status(s), } } diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index 34d5a56312..129a4da25b 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -9,6 +9,7 @@ use crate::peer_manager::{ ConnectionDirection, PeerManager, PeerManagerEvent, }; use crate::peer_manager::{MIN_OUTBOUND_ONLY_FACTOR, PEER_EXCESS_FACTOR, PRIORITY_PEER_EXCESS}; +use crate::rpc::methods::MetadataRequest; use crate::rpc::*; use crate::service::behaviour::BehaviourEvent; pub use crate::service::behaviour::Gossipsub; @@ -37,7 +38,6 @@ use slog::{crit, debug, info, o, trace, warn}; use std::path::PathBuf; use std::pin::Pin; use std::{ - marker::PhantomData, sync::Arc, task::{Context, Poll}, }; @@ -944,16 +944,25 @@ impl Network { /// Sends a METADATA request to a peer. fn send_meta_data_request(&mut self, peer_id: PeerId) { - let event = OutboundRequest::MetaData(PhantomData); + // We always prefer sending V2 requests + let event = OutboundRequest::MetaData(MetadataRequest::new_v2()); self.eth2_rpc_mut() .send_request(peer_id, RequestId::Internal, event); } /// Sends a METADATA response to a peer. - fn send_meta_data_response(&mut self, id: PeerRequestId, peer_id: PeerId) { - let event = RPCCodedResponse::Success(RPCResponse::MetaData( - self.network_globals.local_metadata.read().clone(), - )); + fn send_meta_data_response( + &mut self, + req: MetadataRequest, + id: PeerRequestId, + peer_id: PeerId, + ) { + let metadata = self.network_globals.local_metadata.read().clone(); + let metadata = match req { + MetadataRequest::V1(_) => metadata.metadata_v1(), + MetadataRequest::V2(_) => metadata, + }; + let event = RPCCodedResponse::Success(RPCResponse::MetaData(metadata)); self.eth2_rpc_mut().send_response(peer_id, id, event); } @@ -1196,9 +1205,9 @@ impl Network { self.pong(peer_request_id, peer_id); None } - InboundRequest::MetaData(_) => { + InboundRequest::MetaData(req) => { // send the requested meta-data - self.send_meta_data_response((handler_id, id), peer_id); + self.send_meta_data_response(req, (handler_id, id), peer_id); None } InboundRequest::Goodbye(reason) => { @@ -1225,13 +1234,9 @@ impl Network { Some(event) } InboundRequest::BlocksByRange(req) => { - let methods::OldBlocksByRangeRequest { - start_slot, - mut count, - step, - } = req; // Still disconnect the peer if the request is naughty. - if step == 0 { + let mut count = *req.count(); + if *req.step() == 0 { self.peer_manager_mut().handle_rpc_error( &peer_id, Protocol::BlocksByRange, @@ -1243,14 +1248,18 @@ impl Network { return None; } // return just one block in case the step parameter is used. https://github.com/ethereum/consensus-specs/pull/2856 - if step > 1 { + if *req.step() > 1 { count = 1; } - let event = self.build_request( - peer_request_id, - peer_id, - Request::BlocksByRange(BlocksByRangeRequest { start_slot, count }), - ); + let request = match req { + methods::OldBlocksByRangeRequest::V1(req) => Request::BlocksByRange( + BlocksByRangeRequest::new_v1(req.start_slot, count), + ), + methods::OldBlocksByRangeRequest::V2(req) => Request::BlocksByRange( + BlocksByRangeRequest::new(req.start_slot, count), + ), + }; + let event = self.build_request(peer_request_id, peer_id, request); Some(event) } InboundRequest::BlocksByRoot(req) => { diff --git a/beacon_node/lighthouse_network/src/service/utils.rs b/beacon_node/lighthouse_network/src/service/utils.rs index 625df65ee9..ac0dc57d7b 100644 --- a/beacon_node/lighthouse_network/src/service/utils.rs +++ b/beacon_node/lighthouse_network/src/service/utils.rs @@ -272,9 +272,11 @@ pub(crate) fn save_metadata_to_disk( log: &slog::Logger, ) { let _ = std::fs::create_dir_all(dir); - match File::create(dir.join(METADATA_FILENAME)) - .and_then(|mut f| f.write_all(&metadata.as_ssz_bytes())) - { + let metadata_bytes = match metadata { + MetaData::V1(md) => md.as_ssz_bytes(), + MetaData::V2(md) => md.as_ssz_bytes(), + }; + match File::create(dir.join(METADATA_FILENAME)).and_then(|mut f| f.write_all(&metadata_bytes)) { Ok(_) => { debug!(log, "Metadata written to disk"); } diff --git a/beacon_node/lighthouse_network/tests/rpc_tests.rs b/beacon_node/lighthouse_network/tests/rpc_tests.rs index ebdbb67421..656df0c4a1 100644 --- a/beacon_node/lighthouse_network/tests/rpc_tests.rs +++ b/beacon_node/lighthouse_network/tests/rpc_tests.rs @@ -155,10 +155,7 @@ fn test_blocks_by_range_chunked_rpc() { common::build_node_pair(Arc::downgrade(&rt), &log, ForkName::Merge).await; // BlocksByRange Request - let rpc_request = Request::BlocksByRange(BlocksByRangeRequest { - start_slot: 0, - count: messages_to_send, - }); + let rpc_request = Request::BlocksByRange(BlocksByRangeRequest::new(0, messages_to_send)); let spec = E::default_spec(); @@ -282,10 +279,7 @@ fn test_blocks_by_range_over_limit() { common::build_node_pair(Arc::downgrade(&rt), &log, ForkName::Merge).await; // BlocksByRange Request - let rpc_request = Request::BlocksByRange(BlocksByRangeRequest { - start_slot: 0, - count: messages_to_send, - }); + let rpc_request = Request::BlocksByRange(BlocksByRangeRequest::new(0, messages_to_send)); // BlocksByRange Response let full_block = merge_block_large(&common::fork_context(ForkName::Merge)); @@ -367,10 +361,7 @@ fn test_blocks_by_range_chunked_rpc_terminates_correctly() { common::build_node_pair(Arc::downgrade(&rt), &log, ForkName::Base).await; // BlocksByRange Request - let rpc_request = Request::BlocksByRange(BlocksByRangeRequest { - start_slot: 0, - count: messages_to_send, - }); + let rpc_request = Request::BlocksByRange(BlocksByRangeRequest::new(0, messages_to_send)); // BlocksByRange Response let spec = E::default_spec(); @@ -490,10 +481,7 @@ fn test_blocks_by_range_single_empty_rpc() { common::build_node_pair(Arc::downgrade(&rt), &log, ForkName::Base).await; // BlocksByRange Request - let rpc_request = Request::BlocksByRange(BlocksByRangeRequest { - start_slot: 0, - count: 10, - }); + let rpc_request = Request::BlocksByRange(BlocksByRangeRequest::new(0, 10)); // BlocksByRange Response let spec = E::default_spec(); @@ -594,16 +582,15 @@ fn test_blocks_by_root_chunked_rpc() { common::build_node_pair(Arc::downgrade(&rt), &log, ForkName::Merge).await; // BlocksByRoot Request - let rpc_request = Request::BlocksByRoot(BlocksByRootRequest { - block_roots: VariableList::from(vec![ + let rpc_request = + Request::BlocksByRoot(BlocksByRootRequest::new(VariableList::from(vec![ Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), - ]), - }); + ]))); // BlocksByRoot Response let full_block = BeaconBlock::Base(BeaconBlockBase::::full(&spec)); @@ -722,8 +709,8 @@ fn test_blocks_by_root_chunked_rpc_terminates_correctly() { common::build_node_pair(Arc::downgrade(&rt), &log, ForkName::Base).await; // BlocksByRoot Request - let rpc_request = Request::BlocksByRoot(BlocksByRootRequest { - block_roots: VariableList::from(vec![ + let rpc_request = + Request::BlocksByRoot(BlocksByRootRequest::new(VariableList::from(vec![ Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), @@ -734,8 +721,7 @@ fn test_blocks_by_root_chunked_rpc_terminates_correctly() { Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), - ]), - }); + ]))); // BlocksByRoot Response let full_block = BeaconBlock::Base(BeaconBlockBase::::full(&spec)); diff --git a/beacon_node/network/src/beacon_processor/worker/rpc_methods.rs b/beacon_node/network/src/beacon_processor/worker/rpc_methods.rs index 81b163bf7e..83baa0417b 100644 --- a/beacon_node/network/src/beacon_processor/worker/rpc_methods.rs +++ b/beacon_node/network/src/beacon_processor/worker/rpc_methods.rs @@ -131,10 +131,10 @@ impl Worker { request_id: PeerRequestId, request: BlocksByRootRequest, ) { - let requested_blocks = request.block_roots.len(); + let requested_blocks = request.block_roots().len(); let mut block_stream = match self .chain - .get_blocks_checking_early_attester_cache(request.block_roots.into(), &executor) + .get_blocks_checking_early_attester_cache(request.block_roots().to_vec(), &executor) { Ok(block_stream) => block_stream, Err(e) => return error!(self.log, "Error getting block stream"; "error" => ?e), @@ -292,18 +292,18 @@ impl Worker { ) { debug!(self.log, "Received BlocksByRange Request"; "peer_id" => %peer_id, - "count" => req.count, - "start_slot" => req.start_slot, + "count" => req.count(), + "start_slot" => req.start_slot(), ); // Should not send more than max request blocks - if req.count > MAX_REQUEST_BLOCKS { - req.count = MAX_REQUEST_BLOCKS; + if *req.count() > MAX_REQUEST_BLOCKS { + *req.count_mut() = MAX_REQUEST_BLOCKS; } let forwards_block_root_iter = match self .chain - .forwards_iter_block_roots(Slot::from(req.start_slot)) + .forwards_iter_block_roots(Slot::from(*req.start_slot())) { Ok(iter) => iter, Err(BeaconChainError::HistoricalBlockError( @@ -326,18 +326,20 @@ impl Worker { // Pick out the required blocks, ignoring skip-slots. let mut last_block_root = None; let maybe_block_roots = process_results(forwards_block_root_iter, |iter| { - iter.take_while(|(_, slot)| slot.as_u64() < req.start_slot.saturating_add(req.count)) - // map skip slots to None - .map(|(root, _)| { - let result = if Some(root) == last_block_root { - None - } else { - Some(root) - }; - last_block_root = Some(root); - result - }) - .collect::>>() + iter.take_while(|(_, slot)| { + slot.as_u64() < req.start_slot().saturating_add(*req.count()) + }) + // map skip slots to None + .map(|(root, _)| { + let result = if Some(root) == last_block_root { + None + } else { + Some(root) + }; + last_block_root = Some(root); + result + }) + .collect::>>() }); let block_roots = match maybe_block_roots { @@ -364,8 +366,8 @@ impl Worker { Ok(Some(block)) => { // Due to skip slots, blocks could be out of the range, we ensure they // are in the range before sending - if block.slot() >= req.start_slot - && block.slot() < req.start_slot + req.count + if block.slot() >= *req.start_slot() + && block.slot() < req.start_slot() + req.count() { blocks_sent += 1; self.send_network_message(NetworkMessage::SendResponse { @@ -440,15 +442,15 @@ impl Worker { .slot() .unwrap_or_else(|_| self.chain.slot_clock.genesis_slot()); - if blocks_sent < (req.count as usize) { + if blocks_sent < (*req.count() as usize) { debug!( self.log, "BlocksByRange outgoing response processed"; "peer" => %peer_id, "msg" => "Failed to return all requested blocks", - "start_slot" => req.start_slot, + "start_slot" => req.start_slot(), "current_slot" => current_slot, - "requested" => req.count, + "requested" => req.count(), "returned" => blocks_sent ); } else { @@ -456,9 +458,9 @@ impl Worker { self.log, "BlocksByRange outgoing response processed"; "peer" => %peer_id, - "start_slot" => req.start_slot, + "start_slot" => req.start_slot(), "current_slot" => current_slot, - "requested" => req.count, + "requested" => req.count(), "returned" => blocks_sent ); } diff --git a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs index 256a2b4297..62ca68e7bc 100644 --- a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs +++ b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs @@ -156,9 +156,7 @@ impl SingleBlockRequest { cannot_process: self.failed_processing >= self.failed_downloading, }) } else if let Some(&peer_id) = self.available_peers.iter().choose(&mut rand::thread_rng()) { - let request = BlocksByRootRequest { - block_roots: VariableList::from(vec![self.hash]), - }; + let request = BlocksByRootRequest::new(VariableList::from(vec![self.hash])); self.state = State::Downloading { peer_id }; self.used_peers.insert(peer_id); Ok((peer_id, request)) diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index c81fed2443..23d42002f4 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -112,7 +112,7 @@ impl SyncNetworkContext { self.log, "Sending BlocksByRange Request"; "method" => "BlocksByRange", - "count" => request.count, + "count" => request.count(), "peer" => %peer_id, ); let request = Request::BlocksByRange(request); @@ -138,7 +138,7 @@ impl SyncNetworkContext { self.log, "Sending backfill BlocksByRange Request"; "method" => "BlocksByRange", - "count" => request.count, + "count" => request.count(), "peer" => %peer_id, ); let request = Request::BlocksByRange(request); @@ -185,7 +185,7 @@ impl SyncNetworkContext { self.log, "Sending BlocksByRoot Request"; "method" => "BlocksByRoot", - "count" => request.block_roots.len(), + "count" => request.block_roots().len(), "peer" => %peer_id ); let request = Request::BlocksByRoot(request); @@ -209,7 +209,7 @@ impl SyncNetworkContext { self.log, "Sending BlocksByRoot Request"; "method" => "BlocksByRoot", - "count" => request.block_roots.len(), + "count" => request.block_roots().len(), "peer" => %peer_id ); let request = Request::BlocksByRoot(request); diff --git a/beacon_node/network/src/sync/range_sync/batch.rs b/beacon_node/network/src/sync/range_sync/batch.rs index 3eee7223db..723ea9b59d 100644 --- a/beacon_node/network/src/sync/range_sync/batch.rs +++ b/beacon_node/network/src/sync/range_sync/batch.rs @@ -202,10 +202,10 @@ impl BatchInfo { /// Returns a BlocksByRange request associated with the batch. pub fn to_blocks_by_range_request(&self) -> BlocksByRangeRequest { - BlocksByRangeRequest { - start_slot: self.start_slot.into(), - count: self.end_slot.sub(self.start_slot).into(), - } + BlocksByRangeRequest::new( + self.start_slot.into(), + self.end_slot.sub(self.start_slot).into(), + ) } /// After different operations over a batch, this could be in a state that allows it to From 77fc5111706c95b3ed55633355a0616f9a2d2073 Mon Sep 17 00:00:00 2001 From: ethDreamer Date: Thu, 15 Jun 2023 08:42:20 +0000 Subject: [PATCH 44/63] Use JSON by default for Deposit Snapshot Sync (#4397) Checkpointz now supports deposit snapshot but [they only support returning them in JSON](https://github.com/ethpandaops/checkpointz/issues/74) so I've modified lighthouse to request them in JSON by default. There's also `get_opt` & `get_opt_with_timeout` methods which seem to expect responses in JSON but were not adding `Accept: application/json` to the request headers so I fixed that as well. Also the beacon API puts quantities in quotes so I fixed that in the snapshot JSON serialization --- common/eth2/src/lib.rs | 16 +++++++++------- consensus/types/src/deposit_tree_snapshot.rs | 2 ++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index e03cc2e9b0..e871efbc2c 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -218,7 +218,11 @@ impl BeaconNodeHttpClient { /// Perform a HTTP GET request, returning `None` on a 404 error. async fn get_opt(&self, url: U) -> Result, Error> { - match self.get_response(url, |b| b).await.optional()? { + match self + .get_response(url, |b| b.accept(Accept::Json)) + .await + .optional()? + { Some(response) => Ok(Some(response.json().await?)), None => Ok(None), } @@ -231,7 +235,7 @@ impl BeaconNodeHttpClient { timeout: Duration, ) -> Result, Error> { let opt_response = self - .get_response(url, |b| b.timeout(timeout)) + .get_response(url, |b| b.timeout(timeout).accept(Accept::Json)) .await .optional()?; match opt_response { @@ -982,16 +986,14 @@ impl BeaconNodeHttpClient { /// `GET beacon/deposit_snapshot` pub async fn get_deposit_snapshot(&self) -> Result, Error> { - use ssz::Decode; let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") .push("deposit_snapshot"); - self.get_bytes_opt_accept_header(path, Accept::Ssz, self.timeouts.get_deposit_snapshot) - .await? - .map(|bytes| DepositTreeSnapshot::from_ssz_bytes(&bytes).map_err(Error::InvalidSsz)) - .transpose() + self.get_opt_with_timeout::, _>(path, self.timeouts.get_deposit_snapshot) + .await + .map(|opt| opt.map(|r| r.data)) } /// `POST beacon/rewards/sync_committee` diff --git a/consensus/types/src/deposit_tree_snapshot.rs b/consensus/types/src/deposit_tree_snapshot.rs index aea4677f26..12e81d0028 100644 --- a/consensus/types/src/deposit_tree_snapshot.rs +++ b/consensus/types/src/deposit_tree_snapshot.rs @@ -30,8 +30,10 @@ impl From<&DepositTreeSnapshot> for FinalizedExecutionBlock { pub struct DepositTreeSnapshot { pub finalized: Vec, pub deposit_root: Hash256, + #[serde(with = "serde_utils::quoted_u64")] pub deposit_count: u64, pub execution_block_hash: Hash256, + #[serde(with = "serde_utils::quoted_u64")] pub execution_block_height: u64, } From affea585f44c037c1db77fb69a64c543f1721739 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 16 Jun 2023 06:44:31 +0000 Subject: [PATCH 45/63] Remove `CountUnrealized` (#4357) ## Issue Addressed Closes #4332 ## Proposed Changes Remove the `CountUnrealized` type, defaulting unrealized justification to _on_. This fixes the #4332 issue by ensuring that importing the same block to fork choice always results in the same outcome. Finalized sync speed may be slightly impacted by this change, but that is deemed an acceptable trade-off until the optimisation from #4118 is implemented. TODO: - [x] Also check that the block isn't a duplicate before importing --- beacon_node/beacon_chain/src/beacon_chain.rs | 12 +- beacon_node/beacon_chain/src/builder.rs | 3 +- beacon_node/beacon_chain/src/fork_revert.rs | 16 +- beacon_node/beacon_chain/src/lib.rs | 5 +- beacon_node/beacon_chain/src/test_utils.rs | 9 +- .../beacon_chain/tests/block_verification.rs | 161 +++++++++++----- .../tests/payload_invalidation.rs | 18 +- beacon_node/beacon_chain/tests/store_tests.rs | 2 - beacon_node/beacon_chain/tests/tests.rs | 2 - beacon_node/http_api/src/publish_blocks.rs | 11 +- .../beacon_processor/worker/gossip_methods.rs | 11 +- .../beacon_processor/worker/sync_methods.rs | 27 +-- beacon_node/network/src/sync/manager.rs | 2 +- .../network/src/sync/range_sync/chain.rs | 13 +- .../src/sync/range_sync/chain_collection.rs | 7 +- consensus/fork_choice/src/fork_choice.rs | 181 ++++++++---------- consensus/fork_choice/src/lib.rs | 6 +- consensus/fork_choice/tests/tests.rs | 5 +- testing/ef_tests/src/cases/fork_choice.rs | 4 +- 19 files changed, 229 insertions(+), 266 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 2fa04304f5..ceda7222e6 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -63,7 +63,6 @@ use execution_layer::{ BlockProposalContents, BuilderParams, ChainHealth, ExecutionLayer, FailedCondition, PayloadAttributes, PayloadStatus, }; -pub use fork_choice::CountUnrealized; use fork_choice::{ AttestationFromBlock, ExecutionStatus, ForkChoice, ForkchoiceUpdateParameters, InvalidationOperation, PayloadVerificationStatus, ResetPayloadStatuses, @@ -2510,7 +2509,6 @@ impl BeaconChain { pub async fn process_chain_segment( self: &Arc, chain_segment: Vec>>, - count_unrealized: CountUnrealized, notify_execution_layer: NotifyExecutionLayer, ) -> ChainSegmentResult { let mut imported_blocks = 0; @@ -2579,7 +2577,6 @@ impl BeaconChain { .process_block( signature_verified_block.block_root(), signature_verified_block, - count_unrealized, notify_execution_layer, ) .await @@ -2668,7 +2665,6 @@ impl BeaconChain { self: &Arc, block_root: Hash256, unverified_block: B, - count_unrealized: CountUnrealized, notify_execution_layer: NotifyExecutionLayer, ) -> Result> { // Start the Prometheus timer. @@ -2689,7 +2685,7 @@ impl BeaconChain { notify_execution_layer, )?; chain - .import_execution_pending_block(execution_pending, count_unrealized) + .import_execution_pending_block(execution_pending) .await }; @@ -2744,10 +2740,9 @@ impl BeaconChain { /// /// An error is returned if the block was unable to be imported. It may be partially imported /// (i.e., this function is not atomic). - async fn import_execution_pending_block( + pub async fn import_execution_pending_block( self: Arc, execution_pending_block: ExecutionPendingBlock, - count_unrealized: CountUnrealized, ) -> Result> { let ExecutionPendingBlock { block, @@ -2808,7 +2803,6 @@ impl BeaconChain { state, confirmed_state_roots, payload_verification_status, - count_unrealized, parent_block, parent_eth1_finalization_data, consensus_context, @@ -2834,7 +2828,6 @@ impl BeaconChain { mut state: BeaconState, confirmed_state_roots: Vec, payload_verification_status: PayloadVerificationStatus, - count_unrealized: CountUnrealized, parent_block: SignedBlindedBeaconBlock, parent_eth1_finalization_data: Eth1FinalizationData, mut consensus_context: ConsensusContext, @@ -2903,7 +2896,6 @@ impl BeaconChain { &state, payload_verification_status, &self.spec, - count_unrealized, ) .map_err(|e| BlockError::BeaconChainError(e.into()))?; } diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index b0f0015b9a..84148fbfb1 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -18,7 +18,7 @@ use crate::{ }; use eth1::Config as Eth1Config; use execution_layer::ExecutionLayer; -use fork_choice::{CountUnrealized, ForkChoice, ResetPayloadStatuses}; +use fork_choice::{ForkChoice, ResetPayloadStatuses}; use futures::channel::mpsc::Sender; use operation_pool::{OperationPool, PersistedOperationPool}; use parking_lot::RwLock; @@ -687,7 +687,6 @@ where store.clone(), Some(current_slot), &self.spec, - CountUnrealized::True, )?; } diff --git a/beacon_node/beacon_chain/src/fork_revert.rs b/beacon_node/beacon_chain/src/fork_revert.rs index ccd17af243..084ae95e09 100644 --- a/beacon_node/beacon_chain/src/fork_revert.rs +++ b/beacon_node/beacon_chain/src/fork_revert.rs @@ -1,5 +1,5 @@ use crate::{BeaconForkChoiceStore, BeaconSnapshot}; -use fork_choice::{CountUnrealized, ForkChoice, PayloadVerificationStatus}; +use fork_choice::{ForkChoice, PayloadVerificationStatus}; use itertools::process_results; use slog::{info, warn, Logger}; use state_processing::state_advance::complete_state_advance; @@ -100,7 +100,6 @@ pub fn reset_fork_choice_to_finalization, Cold: It store: Arc>, current_slot: Option, spec: &ChainSpec, - count_unrealized_config: CountUnrealized, ) -> Result, E>, String> { // Fetch finalized block. let finalized_checkpoint = head_state.finalized_checkpoint(); @@ -166,8 +165,7 @@ pub fn reset_fork_choice_to_finalization, Cold: It .map_err(|e| format!("Error loading blocks to replay for fork choice: {:?}", e))?; let mut state = finalized_snapshot.beacon_state; - let blocks_len = blocks.len(); - for (i, block) in blocks.into_iter().enumerate() { + for block in blocks { complete_state_advance(&mut state, None, block.slot(), spec) .map_err(|e| format!("State advance failed: {:?}", e))?; @@ -190,15 +188,6 @@ pub fn reset_fork_choice_to_finalization, Cold: It // This scenario is so rare that it seems OK to double-verify some blocks. let payload_verification_status = PayloadVerificationStatus::Optimistic; - // Because we are replaying a single chain of blocks, we only need to calculate unrealized - // justification for the last block in the chain. - let is_last_block = i + 1 == blocks_len; - let count_unrealized = if is_last_block { - count_unrealized_config - } else { - CountUnrealized::False - }; - fork_choice .on_block( block.slot(), @@ -209,7 +198,6 @@ pub fn reset_fork_choice_to_finalization, Cold: It &state, payload_verification_status, spec, - count_unrealized, ) .map_err(|e| format!("Error applying replayed block to fork choice: {:?}", e))?; } diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index be1522a3b8..d672c16828 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -52,8 +52,8 @@ pub mod validator_pubkey_cache; pub use self::beacon_chain::{ AttestationProcessingOutcome, BeaconChain, BeaconChainTypes, BeaconStore, ChainSegmentResult, - CountUnrealized, ForkChoiceError, OverrideForkchoiceUpdate, ProduceBlockVerification, - StateSkipConfig, WhenSlotSkipped, INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON, + ForkChoiceError, OverrideForkchoiceUpdate, ProduceBlockVerification, StateSkipConfig, + WhenSlotSkipped, INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON, INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON, MAXIMUM_GOSSIP_CLOCK_DISPARITY, }; pub use self::beacon_snapshot::BeaconSnapshot; @@ -64,6 +64,7 @@ pub use attestation_verification::Error as AttestationError; pub use beacon_fork_choice_store::{BeaconForkChoiceStore, Error as ForkChoiceStoreError}; pub use block_verification::{ get_block_root, BlockError, ExecutionPayloadError, GossipVerifiedBlock, + IntoExecutionPendingBlock, }; pub use canonical_head::{CachedHead, CanonicalHead, CanonicalHeadRwLock}; pub use eth1_chain::{Eth1Chain, Eth1ChainBackend}; diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index c5615b6185..55ea016fbd 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -22,7 +22,6 @@ use execution_layer::{ }, ExecutionLayer, }; -use fork_choice::CountUnrealized; use futures::channel::mpsc::Receiver; pub use genesis::{interop_genesis_state_with_eth1, DEFAULT_ETH1_BLOCK_HASH}; use int_to_bytes::int_to_bytes32; @@ -1693,12 +1692,7 @@ where self.set_current_slot(slot); let block_hash: SignedBeaconBlockHash = self .chain - .process_block( - block_root, - Arc::new(block), - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_block(block_root, Arc::new(block), NotifyExecutionLayer::Yes) .await? .into(); self.chain.recompute_head_at_current_slot().await; @@ -1714,7 +1708,6 @@ where .process_block( block.canonical_root(), Arc::new(block), - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await? diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index c66ed60a9c..a88931367f 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -3,8 +3,9 @@ use beacon_chain::test_utils::{ AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, }; -use beacon_chain::{BeaconSnapshot, BlockError, ChainSegmentResult, NotifyExecutionLayer}; -use fork_choice::CountUnrealized; +use beacon_chain::{ + BeaconSnapshot, BlockError, ChainSegmentResult, IntoExecutionPendingBlock, NotifyExecutionLayer, +}; use lazy_static::lazy_static; use logging::test_logger; use slasher::{Config as SlasherConfig, Slasher}; @@ -148,18 +149,14 @@ async fn chain_segment_full_segment() { // Sneak in a little check to ensure we can process empty chain segments. harness .chain - .process_chain_segment(vec![], CountUnrealized::True, NotifyExecutionLayer::Yes) + .process_chain_segment(vec![], NotifyExecutionLayer::Yes) .await .into_block_error() .expect("should import empty chain segment"); harness .chain - .process_chain_segment( - blocks.clone(), - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_chain_segment(blocks.clone(), NotifyExecutionLayer::Yes) .await .into_block_error() .expect("should import chain segment"); @@ -188,11 +185,7 @@ async fn chain_segment_varying_chunk_size() { for chunk in blocks.chunks(*chunk_size) { harness .chain - .process_chain_segment( - chunk.to_vec(), - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_chain_segment(chunk.to_vec(), NotifyExecutionLayer::Yes) .await .into_block_error() .unwrap_or_else(|_| panic!("should import chain segment of len {}", chunk_size)); @@ -228,7 +221,7 @@ async fn chain_segment_non_linear_parent_roots() { matches!( harness .chain - .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) .await .into_block_error(), Err(BlockError::NonLinearParentRoots) @@ -248,7 +241,7 @@ async fn chain_segment_non_linear_parent_roots() { matches!( harness .chain - .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) .await .into_block_error(), Err(BlockError::NonLinearParentRoots) @@ -279,7 +272,7 @@ async fn chain_segment_non_linear_slots() { matches!( harness .chain - .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) .await .into_block_error(), Err(BlockError::NonLinearSlots) @@ -300,7 +293,7 @@ async fn chain_segment_non_linear_slots() { matches!( harness .chain - .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) .await .into_block_error(), Err(BlockError::NonLinearSlots) @@ -326,7 +319,7 @@ async fn assert_invalid_signature( matches!( harness .chain - .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) .await .into_block_error(), Err(BlockError::InvalidSignature) @@ -348,11 +341,7 @@ async fn assert_invalid_signature( // imported prior to this test. let _ = harness .chain - .process_chain_segment( - ancestor_blocks, - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_chain_segment(ancestor_blocks, NotifyExecutionLayer::Yes) .await; harness.chain.recompute_head_at_current_slot().await; @@ -361,7 +350,6 @@ async fn assert_invalid_signature( .process_block( snapshots[block_index].beacon_block.canonical_root(), snapshots[block_index].beacon_block.clone(), - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await; @@ -414,11 +402,7 @@ async fn invalid_signature_gossip_block() { .collect(); harness .chain - .process_chain_segment( - ancestor_blocks, - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_chain_segment(ancestor_blocks, NotifyExecutionLayer::Yes) .await .into_block_error() .expect("should import all blocks prior to the one being tested"); @@ -430,7 +414,6 @@ async fn invalid_signature_gossip_block() { .process_block( signed_block.canonical_root(), Arc::new(signed_block), - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await, @@ -465,7 +448,7 @@ async fn invalid_signature_block_proposal() { matches!( harness .chain - .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) .await .into_block_error(), Err(BlockError::InvalidSignature) @@ -663,7 +646,7 @@ async fn invalid_signature_deposit() { !matches!( harness .chain - .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) .await .into_block_error(), Err(BlockError::InvalidSignature) @@ -743,7 +726,6 @@ async fn block_gossip_verification() { .process_block( gossip_verified.block_root, gossip_verified, - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await @@ -1015,7 +997,6 @@ async fn verify_block_for_gossip_slashing_detection() { .process_block( verified_block.block_root, verified_block, - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await @@ -1055,7 +1036,6 @@ async fn verify_block_for_gossip_doppelganger_detection() { .process_block( verified_block.block_root, verified_block, - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await @@ -1203,7 +1183,6 @@ async fn add_base_block_to_altair_chain() { .process_block( base_block.canonical_root(), Arc::new(base_block.clone()), - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await @@ -1219,11 +1198,7 @@ async fn add_base_block_to_altair_chain() { assert!(matches!( harness .chain - .process_chain_segment( - vec![Arc::new(base_block)], - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_chain_segment(vec![Arc::new(base_block)], NotifyExecutionLayer::Yes,) .await, ChainSegmentResult::Failed { imported_blocks: 0, @@ -1342,7 +1317,6 @@ async fn add_altair_block_to_base_chain() { .process_block( altair_block.canonical_root(), Arc::new(altair_block.clone()), - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await @@ -1358,11 +1332,7 @@ async fn add_altair_block_to_base_chain() { assert!(matches!( harness .chain - .process_chain_segment( - vec![Arc::new(altair_block)], - CountUnrealized::True, - NotifyExecutionLayer::Yes - ) + .process_chain_segment(vec![Arc::new(altair_block)], NotifyExecutionLayer::Yes) .await, ChainSegmentResult::Failed { imported_blocks: 0, @@ -1373,3 +1343,100 @@ async fn add_altair_block_to_base_chain() { } )); } + +#[tokio::test] +async fn import_duplicate_block_unrealized_justification() { + let spec = MainnetEthSpec::default_spec(); + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec) + .keypairs(KEYPAIRS[..].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + let chain = &harness.chain; + + // Move out of the genesis slot. + harness.advance_slot(); + + // Build the chain out to the first justification opportunity 2/3rds of the way through epoch 2. + let num_slots = E::slots_per_epoch() as usize * 8 / 3; + harness + .extend_chain( + num_slots, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Move into the next empty slot. + harness.advance_slot(); + + // The store's justified checkpoint must still be at epoch 0, while unrealized justification + // must be at epoch 1. + let fc = chain.canonical_head.fork_choice_read_lock(); + assert_eq!(fc.justified_checkpoint().epoch, 0); + assert_eq!(fc.unrealized_justified_checkpoint().epoch, 1); + drop(fc); + + // Produce a block to justify epoch 2. + let state = harness.get_current_state(); + let slot = harness.get_current_slot(); + let (block, _) = harness.make_block(state.clone(), slot).await; + let block = Arc::new(block); + let block_root = block.canonical_root(); + + // Create two verified variants of the block, representing the same block being processed in + // parallel. + let notify_execution_layer = NotifyExecutionLayer::Yes; + let verified_block1 = block + .clone() + .into_execution_pending_block(block_root, &chain, notify_execution_layer) + .unwrap(); + let verified_block2 = block + .into_execution_pending_block(block_root, &chain, notify_execution_layer) + .unwrap(); + + // Import the first block, simulating a block processed via a finalized chain segment. + chain + .clone() + .import_execution_pending_block(verified_block1) + .await + .unwrap(); + + // Unrealized justification should NOT have updated. + let fc = chain.canonical_head.fork_choice_read_lock(); + assert_eq!(fc.justified_checkpoint().epoch, 0); + let unrealized_justification = fc.unrealized_justified_checkpoint(); + assert_eq!(unrealized_justification.epoch, 2); + + // The fork choice node for the block should have unrealized justification. + let fc_block = fc.get_block(&block_root).unwrap(); + assert_eq!( + fc_block.unrealized_justified_checkpoint, + Some(unrealized_justification) + ); + drop(fc); + + // Import the second verified block, simulating a block processed via RPC. + chain + .clone() + .import_execution_pending_block(verified_block2) + .await + .unwrap(); + + // Unrealized justification should still be updated. + let fc = chain.canonical_head.fork_choice_read_lock(); + assert_eq!(fc.justified_checkpoint().epoch, 0); + assert_eq!( + fc.unrealized_justified_checkpoint(), + unrealized_justification + ); + + // The fork choice node for the block should still have the unrealized justified checkpoint. + let fc_block = fc.get_block(&block_root).unwrap(); + assert_eq!( + fc_block.unrealized_justified_checkpoint, + Some(unrealized_justification) + ); +} diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index f88c2ee6fd..c39bdeaf36 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -17,9 +17,7 @@ use execution_layer::{ test_utils::ExecutionBlockGenerator, ExecutionLayer, ForkchoiceState, PayloadAttributes, }; -use fork_choice::{ - CountUnrealized, Error as ForkChoiceError, InvalidationOperation, PayloadVerificationStatus, -}; +use fork_choice::{Error as ForkChoiceError, InvalidationOperation, PayloadVerificationStatus}; use logging::test_logger; use proto_array::{Error as ProtoArrayError, ExecutionStatus}; use slot_clock::SlotClock; @@ -698,7 +696,6 @@ async fn invalidates_all_descendants() { .process_block( fork_block.canonical_root(), Arc::new(fork_block), - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await @@ -795,7 +792,6 @@ async fn switches_heads() { .process_block( fork_block.canonical_root(), Arc::new(fork_block), - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await @@ -1050,7 +1046,7 @@ async fn invalid_parent() { // Ensure the block built atop an invalid payload is invalid for import. assert!(matches!( - rig.harness.chain.process_block(block.canonical_root(), block.clone(), CountUnrealized::True, NotifyExecutionLayer::Yes).await, + rig.harness.chain.process_block(block.canonical_root(), block.clone(), NotifyExecutionLayer::Yes).await, Err(BlockError::ParentExecutionPayloadInvalid { parent_root: invalid_root }) if invalid_root == parent_root )); @@ -1065,7 +1061,7 @@ async fn invalid_parent() { &state, PayloadVerificationStatus::Optimistic, &rig.harness.chain.spec, - CountUnrealized::True, + ), Err(ForkChoiceError::ProtoArrayStringError(message)) if message.contains(&format!( @@ -1336,12 +1332,7 @@ async fn build_optimistic_chain( for block in blocks { rig.harness .chain - .process_block( - block.canonical_root(), - block, - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_block(block.canonical_root(), block, NotifyExecutionLayer::Yes) .await .unwrap(); } @@ -1900,7 +1891,6 @@ async fn recover_from_invalid_head_by_importing_blocks() { .process_block( fork_block.canonical_root(), fork_block.clone(), - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 2f40443b99..0bc7798a7f 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -12,7 +12,6 @@ use beacon_chain::{ BeaconChainError, BeaconChainTypes, BeaconSnapshot, ChainConfig, NotifyExecutionLayer, ServerSentEventHandler, WhenSlotSkipped, }; -use fork_choice::CountUnrealized; use lazy_static::lazy_static; use logging::test_logger; use maplit::hashset; @@ -2151,7 +2150,6 @@ async fn weak_subjectivity_sync() { .process_block( full_block.canonical_root(), Arc::new(full_block), - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs index b4eabc8093..f97f7069dc 100644 --- a/beacon_node/beacon_chain/tests/tests.rs +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -8,7 +8,6 @@ use beacon_chain::{ }, BeaconChain, NotifyExecutionLayer, StateSkipConfig, WhenSlotSkipped, }; -use fork_choice::CountUnrealized; use lazy_static::lazy_static; use operation_pool::PersistedOperationPool; use state_processing::{ @@ -687,7 +686,6 @@ async fn run_skip_slot_test(skip_slots: u64) { .process_block( harness_a.chain.head_snapshot().beacon_block_root, harness_a.chain.head_snapshot().beacon_block.clone(), - CountUnrealized::True, NotifyExecutionLayer::Yes, ) .await diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 1a5d5175bc..8bcad6ba40 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -1,8 +1,6 @@ use crate::metrics; use beacon_chain::validator_monitor::{get_block_delay_ms, timestamp_now}; -use beacon_chain::{ - BeaconChain, BeaconChainTypes, BlockError, CountUnrealized, NotifyExecutionLayer, -}; +use beacon_chain::{BeaconChain, BeaconChainTypes, BlockError, NotifyExecutionLayer}; use execution_layer::ProvenancedPayload; use lighthouse_network::PubsubMessage; use network::NetworkMessage; @@ -56,12 +54,7 @@ pub async fn publish_block( let block_root = block_root.unwrap_or_else(|| block.canonical_root()); match chain - .process_block( - block_root, - block.clone(), - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_block(block_root, block.clone(), NotifyExecutionLayer::Yes) .await { Ok(root) => { diff --git a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs b/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs index cb4533f5ae..121a27fecf 100644 --- a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs +++ b/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs @@ -8,8 +8,8 @@ use beacon_chain::{ observed_operations::ObservationOutcome, sync_committee_verification::{self, Error as SyncCommitteeError}, validator_monitor::get_block_delay_ms, - BeaconChainError, BeaconChainTypes, BlockError, CountUnrealized, ForkChoiceError, - GossipVerifiedBlock, NotifyExecutionLayer, + BeaconChainError, BeaconChainTypes, BlockError, ForkChoiceError, GossipVerifiedBlock, + NotifyExecutionLayer, }; use lighthouse_network::{Client, MessageAcceptance, MessageId, PeerAction, PeerId, ReportSource}; use operation_pool::ReceivedPreCapella; @@ -949,12 +949,7 @@ impl Worker { let result = self .chain - .process_block( - block_root, - verified_block, - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_block(block_root, verified_block, NotifyExecutionLayer::Yes) .await; match &result { diff --git a/beacon_node/network/src/beacon_processor/worker/sync_methods.rs b/beacon_node/network/src/beacon_processor/worker/sync_methods.rs index 2dbb5a346c..7e8fce3563 100644 --- a/beacon_node/network/src/beacon_processor/worker/sync_methods.rs +++ b/beacon_node/network/src/beacon_processor/worker/sync_methods.rs @@ -7,7 +7,6 @@ use crate::beacon_processor::DuplicateCache; use crate::metrics; use crate::sync::manager::{BlockProcessType, SyncMessage}; use crate::sync::{BatchProcessResult, ChainId}; -use beacon_chain::CountUnrealized; use beacon_chain::{ observed_block_producers::Error as ObserveError, validator_monitor::get_block_delay_ms, BeaconChainError, BeaconChainTypes, BlockError, ChainSegmentResult, HistoricalBlockError, @@ -25,7 +24,7 @@ use types::{Epoch, Hash256, SignedBeaconBlock}; #[derive(Clone, Debug, PartialEq)] pub enum ChainSegmentProcessId { /// Processing Id of a range syncing batch. - RangeBatchId(ChainId, Epoch, CountUnrealized), + RangeBatchId(ChainId, Epoch), /// Processing ID for a backfill syncing batch. BackSyncBatchId(Epoch), /// Processing Id of the parent lookup of a block. @@ -166,12 +165,7 @@ impl Worker { let parent_root = block.message().parent_root(); let result = self .chain - .process_block( - block_root, - block, - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_block(block_root, block, NotifyExecutionLayer::Yes) .await; metrics::inc_counter(&metrics::BEACON_PROCESSOR_RPC_BLOCK_IMPORTED_TOTAL); @@ -220,17 +214,13 @@ impl Worker { ) { let result = match sync_type { // this a request from the range sync - ChainSegmentProcessId::RangeBatchId(chain_id, epoch, count_unrealized) => { + ChainSegmentProcessId::RangeBatchId(chain_id, epoch) => { let start_slot = downloaded_blocks.first().map(|b| b.slot().as_u64()); let end_slot = downloaded_blocks.last().map(|b| b.slot().as_u64()); let sent_blocks = downloaded_blocks.len(); match self - .process_blocks( - downloaded_blocks.iter(), - count_unrealized, - notify_execution_layer, - ) + .process_blocks(downloaded_blocks.iter(), notify_execution_layer) .await { (_, Ok(_)) => { @@ -309,11 +299,7 @@ impl Worker { // parent blocks are ordered from highest slot to lowest, so we need to process in // reverse match self - .process_blocks( - downloaded_blocks.iter().rev(), - CountUnrealized::True, - notify_execution_layer, - ) + .process_blocks(downloaded_blocks.iter().rev(), notify_execution_layer) .await { (imported_blocks, Err(e)) => { @@ -343,13 +329,12 @@ impl Worker { async fn process_blocks<'a>( &self, downloaded_blocks: impl Iterator>>, - count_unrealized: CountUnrealized, notify_execution_layer: NotifyExecutionLayer, ) -> (usize, Result<(), ChainSegmentFailed>) { let blocks: Vec> = downloaded_blocks.cloned().collect(); match self .chain - .process_chain_segment(blocks, count_unrealized, notify_execution_layer) + .process_chain_segment(blocks, notify_execution_layer) .await { ChainSegmentResult::Successful { imported_blocks } => { diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 230c883a93..37b63cdba7 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -556,7 +556,7 @@ impl SyncManager { .parent_block_processed(chain_hash, result, &mut self.network), }, SyncMessage::BatchProcessed { sync_type, result } => match sync_type { - ChainSegmentProcessId::RangeBatchId(chain_id, epoch, _) => { + ChainSegmentProcessId::RangeBatchId(chain_id, epoch) => { self.range_sync.handle_block_process_result( &mut self.network, chain_id, diff --git a/beacon_node/network/src/sync/range_sync/chain.rs b/beacon_node/network/src/sync/range_sync/chain.rs index 4226b600f5..51ca9e2b07 100644 --- a/beacon_node/network/src/sync/range_sync/chain.rs +++ b/beacon_node/network/src/sync/range_sync/chain.rs @@ -3,7 +3,7 @@ use crate::beacon_processor::{ChainSegmentProcessId, WorkEvent as BeaconWorkEven use crate::sync::{ manager::Id, network_context::SyncNetworkContext, BatchOperationOutcome, BatchProcessResult, }; -use beacon_chain::{BeaconChainTypes, CountUnrealized}; +use beacon_chain::BeaconChainTypes; use fnv::FnvHashMap; use lighthouse_network::{PeerAction, PeerId}; use rand::seq::SliceRandom; @@ -101,8 +101,6 @@ pub struct SyncingChain { /// Batches validated by this chain. validated_batches: u64, - is_finalized_segment: bool, - /// The chain's log. log: slog::Logger, } @@ -128,7 +126,6 @@ impl SyncingChain { target_head_slot: Slot, target_head_root: Hash256, peer_id: PeerId, - is_finalized_segment: bool, log: &slog::Logger, ) -> Self { let mut peers = FnvHashMap::default(); @@ -150,7 +147,6 @@ impl SyncingChain { state: ChainSyncingState::Stopped, current_processing_batch: None, validated_batches: 0, - is_finalized_segment, log: log.new(o!("chain" => id)), } } @@ -318,12 +314,7 @@ impl SyncingChain { // for removing chains and checking completion is in the callback. let blocks = batch.start_processing()?; - let count_unrealized = if self.is_finalized_segment { - CountUnrealized::False - } else { - CountUnrealized::True - }; - let process_id = ChainSegmentProcessId::RangeBatchId(self.id, batch_id, count_unrealized); + let process_id = ChainSegmentProcessId::RangeBatchId(self.id, batch_id); self.current_processing_batch = Some(batch_id); if let Err(e) = 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 37a3f13e73..65ddcefe85 100644 --- a/beacon_node/network/src/sync/range_sync/chain_collection.rs +++ b/beacon_node/network/src/sync/range_sync/chain_collection.rs @@ -465,10 +465,10 @@ impl ChainCollection { network: &mut SyncNetworkContext, ) { let id = SyncingChain::::id(&target_head_root, &target_head_slot); - let (collection, is_finalized) = if let RangeSyncType::Finalized = sync_type { - (&mut self.finalized_chains, true) + let collection = if let RangeSyncType::Finalized = sync_type { + &mut self.finalized_chains } else { - (&mut self.head_chains, false) + &mut self.head_chains }; match collection.entry(id) { Entry::Occupied(mut entry) => { @@ -493,7 +493,6 @@ impl ChainCollection { target_head_slot, target_head_root, peer, - is_finalized, &self.log, ); debug_assert_eq!(new_chain.get_id(), id); diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index e6c46e83e7..5d86f99f1a 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -174,21 +174,6 @@ impl From for Error { } } -/// Indicates whether the unrealized justification of a block should be calculated and tracked. -/// If a block has been finalized, this can be set to false. This is useful when syncing finalized -/// portions of the chain. Otherwise this should always be set to true. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum CountUnrealized { - True, - False, -} - -impl CountUnrealized { - pub fn is_true(&self) -> bool { - matches!(self, CountUnrealized::True) - } -} - /// Indicates if a block has been verified by an execution payload. /// /// There is no variant for "invalid", since such a block should never be added to fork choice. @@ -659,8 +644,14 @@ where state: &BeaconState, payload_verification_status: PayloadVerificationStatus, spec: &ChainSpec, - count_unrealized: CountUnrealized, ) -> Result<(), Error> { + // If this block has already been processed we do not need to reprocess it. + // We check this immediately in case re-processing the block mutates some property of the + // global fork choice store, e.g. the justified checkpoints or the proposer boost root. + if self.proto_array.contains_block(&block_root) { + return Ok(()); + } + // Provide the slot (as per the system clock) to the `fc_store` and then return its view of // the current slot. The `fc_store` will ensure that the `current_slot` is never // decreasing, a property which we must maintain. @@ -726,96 +717,84 @@ where )?; // Update unrealized justified/finalized checkpoints. - let (unrealized_justified_checkpoint, unrealized_finalized_checkpoint) = if count_unrealized - .is_true() - { - let block_epoch = block.slot().epoch(E::slots_per_epoch()); + let block_epoch = block.slot().epoch(E::slots_per_epoch()); - // If the parent checkpoints are already at the same epoch as the block being imported, - // it's impossible for the unrealized checkpoints to differ from the parent's. This - // holds true because: - // - // 1. A child block cannot have lower FFG checkpoints than its parent. - // 2. A block in epoch `N` cannot contain attestations which would justify an epoch higher than `N`. - // 3. A block in epoch `N` cannot contain attestations which would finalize an epoch higher than `N - 1`. - // - // This is an optimization. It should reduce the amount of times we run - // `process_justification_and_finalization` by approximately 1/3rd when the chain is - // performing optimally. - let parent_checkpoints = parent_block - .unrealized_justified_checkpoint - .zip(parent_block.unrealized_finalized_checkpoint) - .filter(|(parent_justified, parent_finalized)| { - parent_justified.epoch == block_epoch - && parent_finalized.epoch + 1 >= block_epoch - }); + // If the parent checkpoints are already at the same epoch as the block being imported, + // it's impossible for the unrealized checkpoints to differ from the parent's. This + // holds true because: + // + // 1. A child block cannot have lower FFG checkpoints than its parent. + // 2. A block in epoch `N` cannot contain attestations which would justify an epoch higher than `N`. + // 3. A block in epoch `N` cannot contain attestations which would finalize an epoch higher than `N - 1`. + // + // This is an optimization. It should reduce the amount of times we run + // `process_justification_and_finalization` by approximately 1/3rd when the chain is + // performing optimally. + let parent_checkpoints = parent_block + .unrealized_justified_checkpoint + .zip(parent_block.unrealized_finalized_checkpoint) + .filter(|(parent_justified, parent_finalized)| { + parent_justified.epoch == block_epoch && parent_finalized.epoch + 1 >= block_epoch + }); - let (unrealized_justified_checkpoint, unrealized_finalized_checkpoint) = - if let Some((parent_justified, parent_finalized)) = parent_checkpoints { - (parent_justified, parent_finalized) - } else { - let justification_and_finalization_state = match block { - BeaconBlockRef::Capella(_) - | BeaconBlockRef::Merge(_) - | BeaconBlockRef::Altair(_) => { - let participation_cache = - per_epoch_processing::altair::ParticipationCache::new(state, spec) - .map_err(Error::ParticipationCacheBuild)?; - per_epoch_processing::altair::process_justification_and_finalization( - state, - &participation_cache, - )? - } - BeaconBlockRef::Base(_) => { - let mut validator_statuses = - per_epoch_processing::base::ValidatorStatuses::new(state, spec) - .map_err(Error::ValidatorStatuses)?; - validator_statuses - .process_attestations(state) + let (unrealized_justified_checkpoint, unrealized_finalized_checkpoint) = + if let Some((parent_justified, parent_finalized)) = parent_checkpoints { + (parent_justified, parent_finalized) + } else { + let justification_and_finalization_state = match block { + BeaconBlockRef::Capella(_) + | BeaconBlockRef::Merge(_) + | BeaconBlockRef::Altair(_) => { + let participation_cache = + per_epoch_processing::altair::ParticipationCache::new(state, spec) + .map_err(Error::ParticipationCacheBuild)?; + per_epoch_processing::altair::process_justification_and_finalization( + state, + &participation_cache, + )? + } + BeaconBlockRef::Base(_) => { + let mut validator_statuses = + per_epoch_processing::base::ValidatorStatuses::new(state, spec) .map_err(Error::ValidatorStatuses)?; - per_epoch_processing::base::process_justification_and_finalization( - state, - &validator_statuses.total_balances, - spec, - )? - } - }; - - ( - justification_and_finalization_state.current_justified_checkpoint(), - justification_and_finalization_state.finalized_checkpoint(), - ) + validator_statuses + .process_attestations(state) + .map_err(Error::ValidatorStatuses)?; + per_epoch_processing::base::process_justification_and_finalization( + state, + &validator_statuses.total_balances, + spec, + )? + } }; - // Update best known unrealized justified & finalized checkpoints - if unrealized_justified_checkpoint.epoch - > self.fc_store.unrealized_justified_checkpoint().epoch - { - self.fc_store - .set_unrealized_justified_checkpoint(unrealized_justified_checkpoint); - } - if unrealized_finalized_checkpoint.epoch - > self.fc_store.unrealized_finalized_checkpoint().epoch - { - self.fc_store - .set_unrealized_finalized_checkpoint(unrealized_finalized_checkpoint); - } + ( + justification_and_finalization_state.current_justified_checkpoint(), + justification_and_finalization_state.finalized_checkpoint(), + ) + }; - // If block is from past epochs, try to update store's justified & finalized checkpoints right away - if block.slot().epoch(E::slots_per_epoch()) < current_slot.epoch(E::slots_per_epoch()) { - self.pull_up_store_checkpoints( - unrealized_justified_checkpoint, - unrealized_finalized_checkpoint, - )?; - } + // Update best known unrealized justified & finalized checkpoints + if unrealized_justified_checkpoint.epoch + > self.fc_store.unrealized_justified_checkpoint().epoch + { + self.fc_store + .set_unrealized_justified_checkpoint(unrealized_justified_checkpoint); + } + if unrealized_finalized_checkpoint.epoch + > self.fc_store.unrealized_finalized_checkpoint().epoch + { + self.fc_store + .set_unrealized_finalized_checkpoint(unrealized_finalized_checkpoint); + } - ( - Some(unrealized_justified_checkpoint), - Some(unrealized_finalized_checkpoint), - ) - } else { - (None, None) - }; + // If block is from past epochs, try to update store's justified & finalized checkpoints right away + if block.slot().epoch(E::slots_per_epoch()) < current_slot.epoch(E::slots_per_epoch()) { + self.pull_up_store_checkpoints( + unrealized_justified_checkpoint, + unrealized_finalized_checkpoint, + )?; + } let target_slot = block .slot() @@ -886,8 +865,8 @@ where justified_checkpoint: state.current_justified_checkpoint(), finalized_checkpoint: state.finalized_checkpoint(), execution_status, - unrealized_justified_checkpoint, - unrealized_finalized_checkpoint, + unrealized_justified_checkpoint: Some(unrealized_justified_checkpoint), + unrealized_finalized_checkpoint: Some(unrealized_finalized_checkpoint), }, current_slot, )?; diff --git a/consensus/fork_choice/src/lib.rs b/consensus/fork_choice/src/lib.rs index 397a2ff893..e7ca84efb3 100644 --- a/consensus/fork_choice/src/lib.rs +++ b/consensus/fork_choice/src/lib.rs @@ -2,9 +2,9 @@ mod fork_choice; mod fork_choice_store; pub use crate::fork_choice::{ - AttestationFromBlock, CountUnrealized, Error, ForkChoice, ForkChoiceView, - ForkchoiceUpdateParameters, InvalidAttestation, InvalidBlock, PayloadVerificationStatus, - PersistedForkChoice, QueuedAttestation, ResetPayloadStatuses, + AttestationFromBlock, Error, ForkChoice, ForkChoiceView, ForkchoiceUpdateParameters, + InvalidAttestation, InvalidBlock, PayloadVerificationStatus, PersistedForkChoice, + QueuedAttestation, ResetPayloadStatuses, }; pub use fork_choice_store::ForkChoiceStore; pub use proto_array::{Block as ProtoBlock, ExecutionStatus, InvalidationOperation}; diff --git a/consensus/fork_choice/tests/tests.rs b/consensus/fork_choice/tests/tests.rs index 82bf642f18..ef262b58c0 100644 --- a/consensus/fork_choice/tests/tests.rs +++ b/consensus/fork_choice/tests/tests.rs @@ -12,8 +12,7 @@ use beacon_chain::{ StateSkipConfig, WhenSlotSkipped, }; use fork_choice::{ - CountUnrealized, ForkChoiceStore, InvalidAttestation, InvalidBlock, PayloadVerificationStatus, - QueuedAttestation, + ForkChoiceStore, InvalidAttestation, InvalidBlock, PayloadVerificationStatus, QueuedAttestation, }; use store::MemoryStore; use types::{ @@ -288,7 +287,6 @@ impl ForkChoiceTest { &state, PayloadVerificationStatus::Verified, &self.harness.chain.spec, - CountUnrealized::True, ) .unwrap(); self @@ -331,7 +329,6 @@ impl ForkChoiceTest { &state, PayloadVerificationStatus::Verified, &self.harness.chain.spec, - CountUnrealized::True, ) .err() .expect("on_block did not return an error"); diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 4f5d998301..e0f4043ac2 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -7,7 +7,7 @@ use beacon_chain::{ obtain_indexed_attestation_and_committees_per_slot, VerifiedAttestation, }, test_utils::{BeaconChainHarness, EphemeralHarnessType}, - BeaconChainTypes, CachedHead, CountUnrealized, NotifyExecutionLayer, + BeaconChainTypes, CachedHead, NotifyExecutionLayer, }; use execution_layer::{json_structures::JsonPayloadStatusV1Status, PayloadStatusV1}; use serde::Deserialize; @@ -381,7 +381,6 @@ impl Tester { let result = self.block_on_dangerous(self.harness.chain.process_block( block_root, block.clone(), - CountUnrealized::True, NotifyExecutionLayer::Yes, ))?; if result.is_ok() != valid { @@ -441,7 +440,6 @@ impl Tester { &state, PayloadVerificationStatus::Irrelevant, &self.harness.chain.spec, - CountUnrealized::True, ); if result.is_ok() { From 2bb62b7f7dc636f00018144a09873c1c4ba83ec8 Mon Sep 17 00:00:00 2001 From: chonghe Date: Fri, 16 Jun 2023 06:44:32 +0000 Subject: [PATCH 46/63] Correct table formatting in Lighthouse book (#4407) This is only a minor correction to the table not properly showing up in Lighthouse book. The changes solves the formatting issue. Another change is on the link to do port-forwarding. --- book/src/faq.md | 2 +- book/src/voluntary-exit.md | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/book/src/faq.md b/book/src/faq.md index 404ae26671..5651f108a2 100644 --- a/book/src/faq.md +++ b/book/src/faq.md @@ -386,7 +386,7 @@ For these reasons, we recommend that you make your node publicly accessible. Lighthouse supports UPnP. If you are behind a NAT with a router that supports UPnP, you can simply ensure UPnP is enabled (Lighthouse will inform you in its -initial logs if a route has been established). You can also manually [set up port mappings](./advanced_networking.md) in your router to your local Lighthouse instance. By default, +initial logs if a route has been established). You can also manually [set up port mappings/port forwarding](./advanced_networking.md/#how-to-open-ports) in your router to your local Lighthouse instance. By default, Lighthouse uses port 9000 for both TCP and UDP. Opening both these ports will make your Lighthouse node maximally contactable. diff --git a/book/src/voluntary-exit.md b/book/src/voluntary-exit.md index d298d13f2d..8d61c1770d 100644 --- a/book/src/voluntary-exit.md +++ b/book/src/voluntary-exit.md @@ -101,16 +101,17 @@ There are two types of withdrawal credentials, `0x00` and `0x01`. To check which
    - | Number of eligible validators | Ideal scenario *n* | Practical scenario *n* | - |-------------------------------|--------------------| ---------------------- | - | 300000 | 2.60 | 2.63 | - | 400000 | 3.47 | 3.51 | - | 500000 | 4.34 | 4.38 | - | 600000 | 5.21 | 5.26 | - | 700000 | 6.08 | 6.14 | - | 800000 | 6.94 | 7.01 | - | 900000 | 7.81 | 7.89 | - | 1000000 | 8.68 | 8.77 | +| Number of eligible validators | Ideal scenario *n* | Practical scenario *n* | +|:----------------:|:---------------------:|:----:| +| 300000 | 2.60 | 2.63 | +| 400000 | 3.47 | 3.51 | +| 500000 | 4.34 | 4.38 | +| 600000 | 5.21 | 5.26 | +| 700000 | 6.08 | 6.14 | +| 800000 | 6.94 | 7.01 | +| 900000 | 7.81 | 7.89 | +| 1000000 | 8.68 | 8.77 | +
    > Note: Ideal scenario assumes no block proposals are missed. This means a total of withdrawals of 7200 blocks/day * 16 withdrawals/block = 115200 withdrawals/day. Practical scenario assumes 1% of blocks are missed per day. As an example, if there are 700000 eligible validators, one would expect a waiting time of slightly more than 6 days. From 6621e1d0c5fc8a310bec51a19a7a721f9fb91bb9 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Mon, 19 Jun 2023 23:53:25 +0000 Subject: [PATCH 47/63] Improve ENR logic for ipv6 (#4395) Currently, the ENR of the node may not be correctly updated when specifying ipv6 fields through the CLI if an ENR exists on disk. This remedies a bug where we were not checking for ipv6 fields when comparing whether to use an on-disk ENR or updating based on CLI configuration parameters. --- beacon_node/lighthouse_network/src/discovery/enr.rs | 6 +++++- boot_node/src/cli.rs | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/beacon_node/lighthouse_network/src/discovery/enr.rs b/beacon_node/lighthouse_network/src/discovery/enr.rs index 938e7cfa25..f85c4b3e5c 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr.rs +++ b/beacon_node/lighthouse_network/src/discovery/enr.rs @@ -213,13 +213,17 @@ pub fn build_enr( fn compare_enr(local_enr: &Enr, disk_enr: &Enr) -> bool { // take preference over disk_enr address if one is not specified (local_enr.ip4().is_none() || local_enr.ip4() == disk_enr.ip4()) + && + (local_enr.ip6().is_none() || local_enr.ip6() == disk_enr.ip6()) // tcp ports must match && local_enr.tcp4() == disk_enr.tcp4() + && local_enr.tcp6() == disk_enr.tcp6() // must match on the same fork && local_enr.get(ETH2_ENR_KEY) == disk_enr.get(ETH2_ENR_KEY) // take preference over disk udp port if one is not specified && (local_enr.udp4().is_none() || local_enr.udp4() == disk_enr.udp4()) - // we need the ATTESTATION_BITFIELD_ENR_KEY and SYNC_COMMITTEE_BITFIELD_ENR_KEY key to match, + && (local_enr.udp6().is_none() || local_enr.udp6() == disk_enr.udp6()) + // we need the ATTESTATION_BITFIELD_ENR_KEY and SYNC_COMMITTEE_BITFIELD_ENR_KEY key to match, // otherwise we use a new ENR. This will likely only be true for non-validating nodes && local_enr.get(ATTESTATION_BITFIELD_ENR_KEY) == disk_enr.get(ATTESTATION_BITFIELD_ENR_KEY) && local_enr.get(SYNC_COMMITTEE_BITFIELD_ENR_KEY) == disk_enr.get(SYNC_COMMITTEE_BITFIELD_ENR_KEY) diff --git a/boot_node/src/cli.rs b/boot_node/src/cli.rs index b13f47f752..d7ea5ab0b3 100644 --- a/boot_node/src/cli.rs +++ b/boot_node/src/cli.rs @@ -102,7 +102,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { Arg::with_name("network-dir") .value_name("NETWORK_DIR") .long("network-dir") - .help("The directory which contains the enr and it's assoicated private key") + .help("The directory which contains the enr and it's associated private key") .takes_value(true) ) } From c76afc6630740216aa8f73eecd2e56f9a833719c Mon Sep 17 00:00:00 2001 From: Mac L Date: Tue, 20 Jun 2023 05:20:36 +0000 Subject: [PATCH 48/63] Remove legacy `max-skip-slots` checks (#4403) ## Proposed Changes Remove `max-skip-slots` checks when processing blocks. This was legacy code which was previously used in the Medalla testnet to sync to the correct fork. With the addition of checkpoint sync which allows us to sync to any arbitrary fork, this is no longer a necessary feature, so it has been removed for simplicity. ## Additional Notes The CLI flag and checks for attestation processing have been retained as it still may have uses in DoS protection. --- .../beacon_chain/src/block_verification.rs | 35 ------------------- beacon_node/beacon_chain/src/chain_config.rs | 3 +- .../beacon_processor/worker/gossip_methods.rs | 1 - beacon_node/src/cli.rs | 2 +- 4 files changed, 2 insertions(+), 39 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index dba38af9bd..3cb8fbdb52 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -141,8 +141,6 @@ pub enum BlockError { /// It's unclear if this block is valid, but it cannot be processed without already knowing /// its parent. ParentUnknown(Arc>), - /// The block skips too many slots and is a DoS risk. - TooManySkippedSlots { parent_slot: Slot, block_slot: Slot }, /// The block slot is greater than the present slot. /// /// ## Peer scoring @@ -786,9 +784,6 @@ impl GossipVerifiedBlock { parent_block.root }; - // Reject any block that exceeds our limit on skipped slots. - check_block_skip_slots(chain, parent_block.slot, block.message())?; - // We assign to a variable instead of using `if let Some` directly to ensure we drop the // write lock before trying to acquire it again in the `else` clause. let proposer_opt = chain @@ -942,9 +937,6 @@ impl SignatureVerifiedBlock { let (mut parent, block) = load_parent(block_root, block, chain)?; - // Reject any block that exceeds our limit on skipped slots. - check_block_skip_slots(chain, parent.beacon_block.slot(), block.message())?; - let state = cheap_state_advance_to_obtain_committees( &mut parent.pre_state, parent.beacon_state_root, @@ -1135,9 +1127,6 @@ impl ExecutionPendingBlock { return Err(BlockError::ParentUnknown(block)); } - // Reject any block that exceeds our limit on skipped slots. - check_block_skip_slots(chain, parent.beacon_block.slot(), block.message())?; - /* * Perform cursory checks to see if the block is even worth processing. */ @@ -1492,30 +1481,6 @@ impl ExecutionPendingBlock { } } -/// Check that the count of skip slots between the block and its parent does not exceed our maximum -/// value. -/// -/// Whilst this is not part of the specification, we include this to help prevent us from DoS -/// attacks. In times of dire network circumstance, the user can configure the -/// `import_max_skip_slots` value. -fn check_block_skip_slots( - chain: &BeaconChain, - parent_slot: Slot, - block: BeaconBlockRef<'_, T::EthSpec>, -) -> Result<(), BlockError> { - // Reject any block that exceeds our limit on skipped slots. - if let Some(max_skip_slots) = chain.config.import_max_skip_slots { - if block.slot() > parent_slot + max_skip_slots { - return Err(BlockError::TooManySkippedSlots { - parent_slot, - block_slot: block.slot(), - }); - } - } - - Ok(()) -} - /// Returns `Ok(())` if the block's slot is greater than the anchor block's slot (if any). fn check_block_against_anchor_slot( block: BeaconBlockRef<'_, T::EthSpec>, diff --git a/beacon_node/beacon_chain/src/chain_config.rs b/beacon_node/beacon_chain/src/chain_config.rs index a74fdced1f..34a5c9a4ec 100644 --- a/beacon_node/beacon_chain/src/chain_config.rs +++ b/beacon_node/beacon_chain/src/chain_config.rs @@ -17,8 +17,7 @@ pub const FORK_CHOICE_LOOKAHEAD_FACTOR: u32 = 24; #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] pub struct ChainConfig { - /// Maximum number of slots to skip when importing a consensus message (e.g., block, - /// attestation, etc). + /// Maximum number of slots to skip when importing an attestation. /// /// If `None`, there is no limit. pub import_max_skip_slots: Option, diff --git a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs b/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs index 121a27fecf..e3cff00103 100644 --- a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs +++ b/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs @@ -835,7 +835,6 @@ impl Worker { | Err(e @ BlockError::NonLinearParentRoots) | Err(e @ BlockError::BlockIsNotLaterThanParent { .. }) | Err(e @ BlockError::InvalidSignature) - | Err(e @ BlockError::TooManySkippedSlots { .. }) | Err(e @ BlockError::WeakSubjectivityConflict) | Err(e @ BlockError::InconsistentFork(_)) | Err(e @ BlockError::ExecutionPayloadError(_)) diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index e763d93f82..206cd3c72f 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -685,7 +685,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { Arg::with_name("max-skip-slots") .long("max-skip-slots") .help( - "Refuse to skip more than this many slots when processing a block or attestation. \ + "Refuse to skip more than this many slots when processing an attestation. \ This prevents nodes on minority forks from wasting our time and disk space, \ but could also cause unnecessary consensus failures, so is disabled by default." ) From bd6a015fe73f37c77f365d5baa4d955ff6e61f48 Mon Sep 17 00:00:00 2001 From: chonghe Date: Tue, 20 Jun 2023 05:20:37 +0000 Subject: [PATCH 49/63] Update Lighthouse book on Doppelganger Protection (#4418) Revise the page by removing the info on sync committee delay. Also added an faq on changing the port. --- book/src/faq.md | 10 +++++++--- book/src/validator-doppelganger.md | 5 ++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/book/src/faq.md b/book/src/faq.md index 5651f108a2..d3e25438a7 100644 --- a/book/src/faq.md +++ b/book/src/faq.md @@ -25,10 +25,11 @@ ## [Network, Monitoring and Maintenance](#network-monitoring-and-maintenance-1) - [I have a low peer count and it is not increasing](#net-peer) - [How do I update lighthouse?](#net-update) -- [Do I need to set up any port mappings (port forwarding)?](#net-port) +- [Do I need to set up any port mappings (port forwarding)?](#net-port-forwarding) - [How can I monitor my validators?](#net-monitor) - [My beacon node and validator client are on different servers. How can I point the validator client to the beacon node?](#net-bn-vc) - [Should I do anything to the beacon node or validator client settings if I have a relocation of the node / change of IP address?](#net-ip) +- [How to change the TCP/UDP port 9000 that Lighthouse listens on?](#net-port) ## [Miscellaneous](#miscellaneous-1) @@ -360,7 +361,7 @@ $ docker pull sigp/lighthouse:v1.0.0 If you are building a docker image, the process will be similar to the one described [here.](./docker.md#building-the-docker-image) You just need to make sure the code you have checked out is up to date. -###
    Do I need to set up any port mappings (port forwarding)? +### Do I need to set up any port mappings (port forwarding)? It is not strictly required to open any ports for Lighthouse to connect and participate in the network. Lighthouse should work out-of-the-box. However, if @@ -386,7 +387,7 @@ For these reasons, we recommend that you make your node publicly accessible. Lighthouse supports UPnP. If you are behind a NAT with a router that supports UPnP, you can simply ensure UPnP is enabled (Lighthouse will inform you in its -initial logs if a route has been established). You can also manually [set up port mappings/port forwarding](./advanced_networking.md/#how-to-open-ports) in your router to your local Lighthouse instance. By default, +initial logs if a route has been established). You can also manually [set up port mappings/port forwarding](./advanced_networking.md#how-to-open-ports) in your router to your local Lighthouse instance. By default, Lighthouse uses port 9000 for both TCP and UDP. Opening both these ports will make your Lighthouse node maximally contactable. @@ -421,6 +422,9 @@ The settings are as follows: ### Should I do anything to the beacon node or validator client settings if I have a relocation of the node / change of IP address? No. Lighthouse will auto-detect the change and update your Ethereum Node Record (ENR). You just need to make sure you are not manually setting the ENR with `--enr-address` (which, for common use cases, this flag is not used). +### How to change the TCP/UDP port 9000 that Lighthouse listens on? +Use the flag ```--port ``` in the beacon node. This flag can be useful when you are running two beacon nodes at the same time. You can leave one beacon node as the default port 9000, and configure the second beacon node to listen on, e.g., ```--port 9001```. + ## Miscellaneous ### What should I do if I lose my slashing protection database? diff --git a/book/src/validator-doppelganger.md b/book/src/validator-doppelganger.md index 6eaddcc7b0..7ce2868e9b 100644 --- a/book/src/validator-doppelganger.md +++ b/book/src/validator-doppelganger.md @@ -43,13 +43,12 @@ DP works by staying silent on the network for 2-3 epochs before starting to sign Staying silent and refusing to sign messages will cause the following: - 2-3 missed attestations, incurring penalties and missed rewards. -- 2-3 epochs of missed sync committee contributions (if the validator is in a sync committee, which is unlikely), incurring penalties and missed rewards. - Potentially missed rewards by missing a block proposal (if the validator is an elected block proposer, which is unlikely). The loss of rewards and penalties incurred due to the missed duties will be very small in -dollar-values. Generally, they will equate to around one US dollar (at August 2021 figures) or about -2% of the reward for one validator for one day. Since DP costs so little but can protect a user from +dollar-values. Neglecting block proposals, generally they will equate to around 0.00002 ETH (equivalent to USD 0.04 assuming ETH is trading at USD 2000), or less than +1% of the reward for one validator for one day. Since DP costs so little but can protect a user from slashing, many users will consider this a worthwhile trade-off. The 2-3 epochs of missed duties will be incurred whenever the VC is started (e.g., after an update From 3cac6d9ed50d14e9aec0d90cc0fb1bc484c10605 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 22 Jun 2023 02:14:56 +0000 Subject: [PATCH 50/63] Configure the `validator/register_validator` batch size via the CLI (#4399) ## Issue Addressed NA ## Proposed Changes Adds the `--validator-registration-batch-size` flag to the VC to allow runtime configuration of the number of validators POSTed to the [`validator/register_validator`](https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Validator/registerValidator) endpoint. There are builders (Agnostic and Eden) that are timing out with `regsiterValidator` requests with ~400 validators, even with a 9 second timeout. Exposing the batch size will help tune batch sizes to (hopefully) avoid this. This PR should not change the behavior of Lighthouse when the new flag is not provided (i.e., the same default value is used). ## Additional Info NA --- lighthouse/tests/validator_client.rs | 21 +++++++++++++++++++++ validator_client/src/cli.rs | 10 ++++++++++ validator_client/src/config.rs | 9 +++++++++ validator_client/src/lib.rs | 1 + validator_client/src/preparation_service.rs | 21 ++++++++++++++++----- 5 files changed, 57 insertions(+), 5 deletions(-) diff --git a/lighthouse/tests/validator_client.rs b/lighthouse/tests/validator_client.rs index 8c1f0477c4..27d7c10e96 100644 --- a/lighthouse/tests/validator_client.rs +++ b/lighthouse/tests/validator_client.rs @@ -499,3 +499,24 @@ fn latency_measurement_service() { assert!(!config.enable_latency_measurement_service); }); } + +#[test] +fn validator_registration_batch_size() { + CommandLineTest::new().run().with_config(|config| { + assert_eq!(config.validator_registration_batch_size, 500); + }); + CommandLineTest::new() + .flag("validator-registration-batch-size", Some("100")) + .run() + .with_config(|config| { + assert_eq!(config.validator_registration_batch_size, 100); + }); +} + +#[test] +#[should_panic] +fn validator_registration_batch_size_zero_value() { + CommandLineTest::new() + .flag("validator-registration-batch-size", Some("0")) + .run(); +} diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index 6e199cb173..436b8eb4d5 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -333,6 +333,16 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .default_value("true") .takes_value(true), ) + .arg( + Arg::with_name("validator-registration-batch-size") + .long("validator-registration-batch-size") + .value_name("INTEGER") + .help("Defines the number of validators per \ + validator/register_validator request sent to the BN. This value \ + can be reduced to avoid timeouts from builders.") + .default_value("500") + .takes_value(true), + ) /* * Experimental/development options. */ diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index fa297dcfed..e0dd12e100 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -77,6 +77,8 @@ pub struct Config { pub disable_run_on_all: bool, /// Enables a service which attempts to measure latency between the VC and BNs. pub enable_latency_measurement_service: bool, + /// Defines the number of validators per `validator/register_validator` request sent to the BN. + pub validator_registration_batch_size: usize, } impl Default for Config { @@ -117,6 +119,7 @@ impl Default for Config { gas_limit: None, disable_run_on_all: false, enable_latency_measurement_service: true, + validator_registration_batch_size: 500, } } } @@ -380,6 +383,12 @@ impl Config { config.enable_latency_measurement_service = parse_optional(cli_args, "latency-measurement-service")?.unwrap_or(true); + config.validator_registration_batch_size = + parse_required(cli_args, "validator-registration-batch-size")?; + if config.validator_registration_batch_size == 0 { + return Err("validator-registration-batch-size cannot be 0".to_string()); + } + /* * Experimental */ diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 6e4a8da6ac..60943a260c 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -487,6 +487,7 @@ impl ProductionValidatorClient { .beacon_nodes(beacon_nodes.clone()) .runtime_context(context.service_context("preparation".into())) .builder_registration_timestamp_override(config.builder_registration_timestamp_override) + .validator_registration_batch_size(config.validator_registration_batch_size) .build()?; let sync_committee_service = SyncCommitteeService::new( diff --git a/validator_client/src/preparation_service.rs b/validator_client/src/preparation_service.rs index 5bd93a5053..7d6e1744c8 100644 --- a/validator_client/src/preparation_service.rs +++ b/validator_client/src/preparation_service.rs @@ -23,9 +23,6 @@ const PROPOSER_PREPARATION_LOOKAHEAD_EPOCHS: u64 = 2; /// Number of epochs to wait before re-submitting validator registration. const EPOCHS_PER_VALIDATOR_REGISTRATION_SUBMISSION: u64 = 1; -/// The number of validator registrations to include per request to the beacon node. -const VALIDATOR_REGISTRATION_BATCH_SIZE: usize = 500; - /// Builds an `PreparationService`. pub struct PreparationServiceBuilder { validator_store: Option>>, @@ -33,6 +30,7 @@ pub struct PreparationServiceBuilder { beacon_nodes: Option>>, context: Option>, builder_registration_timestamp_override: Option, + validator_registration_batch_size: Option, } impl PreparationServiceBuilder { @@ -43,6 +41,7 @@ impl PreparationServiceBuilder { beacon_nodes: None, context: None, builder_registration_timestamp_override: None, + validator_registration_batch_size: None, } } @@ -74,6 +73,14 @@ impl PreparationServiceBuilder { self } + pub fn validator_registration_batch_size( + mut self, + validator_registration_batch_size: usize, + ) -> Self { + self.validator_registration_batch_size = Some(validator_registration_batch_size); + self + } + pub fn build(self) -> Result, String> { Ok(PreparationService { inner: Arc::new(Inner { @@ -91,6 +98,9 @@ impl PreparationServiceBuilder { .ok_or("Cannot build PreparationService without runtime_context")?, builder_registration_timestamp_override: self .builder_registration_timestamp_override, + validator_registration_batch_size: self.validator_registration_batch_size.ok_or( + "Cannot build PreparationService without validator_registration_batch_size", + )?, validator_registration_cache: RwLock::new(HashMap::new()), }), }) @@ -107,6 +117,7 @@ pub struct Inner { // Used to track unpublished validator registration changes. validator_registration_cache: RwLock>, + validator_registration_batch_size: usize, } #[derive(Hash, Eq, PartialEq, Debug, Clone)] @@ -447,7 +458,7 @@ impl PreparationService { } if !signed.is_empty() { - for batch in signed.chunks(VALIDATOR_REGISTRATION_BATCH_SIZE) { + for batch in signed.chunks(self.validator_registration_batch_size) { match self .beacon_nodes .first_success( @@ -462,7 +473,7 @@ impl PreparationService { Ok(()) => info!( log, "Published validator registrations to the builder network"; - "count" => registration_data_len, + "count" => batch.len(), ), Err(e) => warn!( log, From 33c942ff035268fb1ff1078707c1e5a4301a902b Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 22 Jun 2023 02:14:57 +0000 Subject: [PATCH 51/63] Add support for updating validator graffiti (#4417) ## Issue Addressed #4386 ## Proposed Changes The original proposal described in the issue adds a new endpoint to support updating validator graffiti, but I realized we already have an endpoint that we use for updating various validator fields in memory and in the validator definitions file, so I think that would be the best place to add this to. ### API endpoint `PATCH lighthouse/validators/{validator_pubkey}` This endpoint updates the graffiti in both the [ validator definition file](https://lighthouse-book.sigmaprime.io/graffiti.html#2-setting-the-graffiti-in-the-validator_definitionsyml) and the in memory `InitializedValidators`. In the next block proposal, the new graffiti will be used. Note that the [`--graffiti-file`](https://lighthouse-book.sigmaprime.io/graffiti.html#1-using-the---graffiti-file-flag-on-the-validator-client) flag has a priority over the validator definitions file, so if the caller attempts to update the graffiti while the `--graffiti-file` flag is present, the endpoint will return an error (Bad request 400). ## Tasks - [x] Add graffiti update support to `PATCH lighthouse/validators/{validator_pubkey}` - [x] Return error if user tries to update graffiti while the `--graffiti-flag` is set - [x] Update Lighthouse book --- book/src/api-vc-endpoints.md | 3 +- book/src/graffiti.md | 24 ++++++ common/eth2/src/lighthouse_vc/http_client.rs | 3 + common/eth2/src/lighthouse_vc/types.rs | 3 + validator_client/src/http_api/mod.rs | 17 +++- validator_client/src/http_api/tests.rs | 84 ++++++++++++++++++- .../src/http_api/tests/keystores.rs | 2 +- .../src/initialized_validators.rs | 18 +++- 8 files changed, 143 insertions(+), 11 deletions(-) diff --git a/book/src/api-vc-endpoints.md b/book/src/api-vc-endpoints.md index 406c5b1f0e..ee0cfd2001 100644 --- a/book/src/api-vc-endpoints.md +++ b/book/src/api-vc-endpoints.md @@ -426,7 +426,8 @@ Example Response Body ## `PATCH /lighthouse/validators/:voting_pubkey` -Update some values for the validator with `voting_pubkey`. The following example updates a validator from `enabled: true` to `enabled: false` +Update some values for the validator with `voting_pubkey`. Possible fields: `enabled`, `gas_limit`, `builder_proposals`, +and `graffiti`. The following example updates a validator from `enabled: true` to `enabled: false`. ### HTTP Specification diff --git a/book/src/graffiti.md b/book/src/graffiti.md index 75c2a86dd5..302f8f9679 100644 --- a/book/src/graffiti.md +++ b/book/src/graffiti.md @@ -29,6 +29,8 @@ Lighthouse will first search for the graffiti corresponding to the public key of ### 2. Setting the graffiti in the `validator_definitions.yml` Users can set validator specific graffitis in `validator_definitions.yml` with the `graffiti` key. This option is recommended for static setups where the graffitis won't change on every new block proposal. +You can also update the graffitis in the `validator_definitions.yml` file using the [Lighthouse API](api-vc-endpoints.html#patch-lighthousevalidatorsvoting_pubkey). See example in [Set Graffiti via HTTP](#set-graffiti-via-http). + Below is an example of the validator_definitions.yml with validator specific graffitis: ``` --- @@ -62,3 +64,25 @@ Usage: `lighthouse bn --graffiti fortytwo` > 3. If graffiti is not specified in `validator_definitions.yml`, load the graffiti passed in the `--graffiti` flag on the validator client. > 4. If the `--graffiti` flag on the validator client is not passed, load the graffiti passed in the `--graffiti` flag on the beacon node. > 4. If the `--graffiti` flag is not passed, load the default Lighthouse graffiti. + +### Set Graffiti via HTTP + +Use the [Lighthouse API](api-vc-endpoints.md) to set graffiti on a per-validator basis. This method updates the graffiti +both in memory and in the `validator_definitions.yml` file. The new graffiti will be used in the next block proposal +without requiring a validator client restart. + +Refer to [Lighthouse API](api-vc-endpoints.html#patch-lighthousevalidatorsvoting_pubkey) for API specification. + +#### Example Command + +```bash +DATADIR=/var/lib/lighthouse +curl -X PATCH "http://localhost:5062/lighthouse/validators/0xb0148e6348264131bf47bcd1829590e870c836dc893050fd0dadc7a28949f9d0a72f2805d027521b45441101f0cc1cde" \ +-H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" \ +-H "Content-Type: application/json" \ +-d '{ + "graffiti": "Mr F was here" +}' | jq +``` + +A `null` response indicates that the request is successful. \ No newline at end of file diff --git a/common/eth2/src/lighthouse_vc/http_client.rs b/common/eth2/src/lighthouse_vc/http_client.rs index e576cfcb36..720d8c7795 100644 --- a/common/eth2/src/lighthouse_vc/http_client.rs +++ b/common/eth2/src/lighthouse_vc/http_client.rs @@ -16,6 +16,7 @@ use std::path::Path; pub use reqwest; pub use reqwest::{Response, StatusCode, Url}; +use types::graffiti::GraffitiString; /// A wrapper around `reqwest::Client` which provides convenience methods for interfacing with a /// Lighthouse Validator Client HTTP server (`validator_client/src/http_api`). @@ -467,6 +468,7 @@ impl ValidatorClientHttpClient { enabled: Option, gas_limit: Option, builder_proposals: Option, + graffiti: Option, ) -> Result<(), Error> { let mut path = self.server.full.clone(); @@ -482,6 +484,7 @@ impl ValidatorClientHttpClient { enabled, gas_limit, builder_proposals, + graffiti, }, ) .await diff --git a/common/eth2/src/lighthouse_vc/types.rs b/common/eth2/src/lighthouse_vc/types.rs index dd2ed03221..7bbe041dbd 100644 --- a/common/eth2/src/lighthouse_vc/types.rs +++ b/common/eth2/src/lighthouse_vc/types.rs @@ -83,6 +83,9 @@ pub struct ValidatorPatchRequest { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub builder_proposals: Option, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub graffiti: Option, } #[derive(Clone, PartialEq, Serialize, Deserialize)] diff --git a/validator_client/src/http_api/mod.rs b/validator_client/src/http_api/mod.rs index fa6cde3ed5..f08c8da1bd 100644 --- a/validator_client/src/http_api/mod.rs +++ b/validator_client/src/http_api/mod.rs @@ -357,7 +357,7 @@ pub fn serve( .and(warp::path("graffiti")) .and(warp::path::end()) .and(validator_store_filter.clone()) - .and(graffiti_file_filter) + .and(graffiti_file_filter.clone()) .and(graffiti_flag_filter) .and(signer.clone()) .and(log_filter.clone()) @@ -617,18 +617,27 @@ pub fn serve( .and(warp::path::end()) .and(warp::body::json()) .and(validator_store_filter.clone()) + .and(graffiti_file_filter) .and(signer.clone()) .and(task_executor_filter.clone()) .and_then( |validator_pubkey: PublicKey, body: api_types::ValidatorPatchRequest, validator_store: Arc>, + graffiti_file: Option, signer, task_executor: TaskExecutor| { blocking_signed_json_task(signer, move || { + if body.graffiti.is_some() && graffiti_file.is_some() { + return Err(warp_utils::reject::custom_bad_request( + "Unable to update graffiti as the \"--graffiti-file\" flag is set" + .to_string(), + )); + } + + let maybe_graffiti = body.graffiti.clone().map(Into::into); let initialized_validators_rw_lock = validator_store.initialized_validators(); let mut initialized_validators = initialized_validators_rw_lock.write(); - match ( initialized_validators.is_enabled(&validator_pubkey), initialized_validators.validator(&validator_pubkey.compress()), @@ -641,7 +650,8 @@ pub fn serve( if Some(is_enabled) == body.enabled && initialized_validator.get_gas_limit() == body.gas_limit && initialized_validator.get_builder_proposals() - == body.builder_proposals => + == body.builder_proposals + && initialized_validator.get_graffiti() == maybe_graffiti => { Ok(()) } @@ -654,6 +664,7 @@ pub fn serve( body.enabled, body.gas_limit, body.builder_proposals, + body.graffiti, ), ) .map_err(|e| { diff --git a/validator_client/src/http_api/tests.rs b/validator_client/src/http_api/tests.rs index 84d2fe437d..dbb9d4d620 100644 --- a/validator_client/src/http_api/tests.rs +++ b/validator_client/src/http_api/tests.rs @@ -28,12 +28,14 @@ use slot_clock::{SlotClock, TestingSlotClock}; use std::future::Future; use std::marker::PhantomData; use std::net::{IpAddr, Ipv4Addr}; +use std::str::FromStr; use std::sync::Arc; use std::time::Duration; use task_executor::TaskExecutor; use tempfile::{tempdir, TempDir}; use tokio::runtime::Runtime; use tokio::sync::oneshot; +use types::graffiti::GraffitiString; const PASSWORD_BYTES: &[u8] = &[42, 50, 37]; pub const TEST_DEFAULT_FEE_RECIPIENT: Address = Address::repeat_byte(42); @@ -533,7 +535,7 @@ impl ApiTester { let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; self.client - .patch_lighthouse_validators(&validator.voting_pubkey, Some(enabled), None, None) + .patch_lighthouse_validators(&validator.voting_pubkey, Some(enabled), None, None, None) .await .unwrap(); @@ -575,7 +577,13 @@ impl ApiTester { let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; self.client - .patch_lighthouse_validators(&validator.voting_pubkey, None, Some(gas_limit), None) + .patch_lighthouse_validators( + &validator.voting_pubkey, + None, + Some(gas_limit), + None, + None, + ) .await .unwrap(); @@ -602,6 +610,7 @@ impl ApiTester { None, None, Some(builder_proposals), + None, ) .await .unwrap(); @@ -620,6 +629,34 @@ impl ApiTester { self } + + pub async fn set_graffiti(self, index: usize, graffiti: &str) -> Self { + let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; + let graffiti_str = GraffitiString::from_str(graffiti).unwrap(); + self.client + .patch_lighthouse_validators( + &validator.voting_pubkey, + None, + None, + None, + Some(graffiti_str), + ) + .await + .unwrap(); + + self + } + + pub async fn assert_graffiti(self, index: usize, graffiti: &str) -> Self { + let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; + let graffiti_str = GraffitiString::from_str(graffiti).unwrap(); + assert_eq!( + self.validator_store.graffiti(&validator.voting_pubkey), + Some(graffiti_str.into()) + ); + + self + } } struct HdValidatorScenario { @@ -723,7 +760,13 @@ fn routes_with_invalid_auth() { .await .test_with_invalid_auth(|client| async move { client - .patch_lighthouse_validators(&PublicKeyBytes::empty(), Some(false), None, None) + .patch_lighthouse_validators( + &PublicKeyBytes::empty(), + Some(false), + None, + None, + None, + ) .await }) .await @@ -931,6 +974,41 @@ fn validator_builder_proposals() { }); } +#[test] +fn validator_graffiti() { + let runtime = build_runtime(); + let weak_runtime = Arc::downgrade(&runtime); + runtime.block_on(async { + ApiTester::new(weak_runtime) + .await + .create_hd_validators(HdValidatorScenario { + count: 2, + specify_mnemonic: false, + key_derivation_path_offset: 0, + disabled: vec![], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(2) + .set_graffiti(0, "Mr F was here") + .await + .assert_graffiti(0, "Mr F was here") + .await + // Test setting graffiti while the validator is disabled + .set_validator_enabled(0, false) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(2) + .set_graffiti(0, "Mr F was here again") + .await + .set_validator_enabled(0, true) + .await + .assert_enabled_validators_count(2) + .assert_graffiti(0, "Mr F was here again") + .await + }); +} + #[test] fn keystore_validator_creation() { let runtime = build_runtime(); diff --git a/validator_client/src/http_api/tests/keystores.rs b/validator_client/src/http_api/tests/keystores.rs index 769d8a1d49..7120ee5f9f 100644 --- a/validator_client/src/http_api/tests/keystores.rs +++ b/validator_client/src/http_api/tests/keystores.rs @@ -468,7 +468,7 @@ fn import_and_delete_conflicting_web3_signer_keystores() { for pubkey in &pubkeys { tester .client - .patch_lighthouse_validators(pubkey, Some(false), None, None) + .patch_lighthouse_validators(pubkey, Some(false), None, None, None) .await .unwrap(); } diff --git a/validator_client/src/initialized_validators.rs b/validator_client/src/initialized_validators.rs index 468fc2b06b..090acbe969 100644 --- a/validator_client/src/initialized_validators.rs +++ b/validator_client/src/initialized_validators.rs @@ -27,6 +27,7 @@ use std::io::{self, Read}; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; +use types::graffiti::GraffitiString; use types::{Address, Graffiti, Keypair, PublicKey, PublicKeyBytes}; use url::{ParseError, Url}; use validator_dir::Builder as ValidatorDirBuilder; @@ -147,6 +148,10 @@ impl InitializedValidator { pub fn get_index(&self) -> Option { self.index } + + pub fn get_graffiti(&self) -> Option { + self.graffiti + } } fn open_keystore(path: &Path) -> Result { @@ -671,8 +676,8 @@ impl InitializedValidators { self.validators.get(public_key) } - /// Sets the `InitializedValidator` and `ValidatorDefinition` `enabled`, `gas_limit`, and `builder_proposals` - /// values. + /// Sets the `InitializedValidator` and `ValidatorDefinition` `enabled`, `gas_limit`, + /// `builder_proposals`, and `graffiti` values. /// /// ## Notes /// @@ -682,7 +687,7 @@ impl InitializedValidators { /// /// If a `gas_limit` is included in the call to this function, it will also be updated and saved /// to disk. If `gas_limit` is `None` the `gas_limit` *will not* be unset in `ValidatorDefinition` - /// or `InitializedValidator`. The same logic applies to `builder_proposals`. + /// or `InitializedValidator`. The same logic applies to `builder_proposals` and `graffiti`. /// /// Saves the `ValidatorDefinitions` to file, even if no definitions were changed. pub async fn set_validator_definition_fields( @@ -691,6 +696,7 @@ impl InitializedValidators { enabled: Option, gas_limit: Option, builder_proposals: Option, + graffiti: Option, ) -> Result<(), Error> { if let Some(def) = self .definitions @@ -708,6 +714,9 @@ impl InitializedValidators { if let Some(builder_proposals) = builder_proposals { def.builder_proposals = Some(builder_proposals); } + if let Some(graffiti) = graffiti.clone() { + def.graffiti = Some(graffiti); + } } self.update_validators().await?; @@ -723,6 +732,9 @@ impl InitializedValidators { if let Some(builder_proposals) = builder_proposals { val.builder_proposals = Some(builder_proposals); } + if let Some(graffiti) = graffiti { + val.graffiti = Some(graffiti.into()); + } } self.definitions From 6d585b5885955db46d8f3ec897033539b04823ad Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 22 Jun 2023 02:14:58 +0000 Subject: [PATCH 52/63] Add `lint-fix` task to automatically fix some Clippy warnings. (#4419) ## Issue Addressed This PR adds a new `lint-fix` task to automatically fix simple Clippy warnings using `cargo clippy --fix`. Usage: ``` make lint-fix ``` --- Makefile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 8e7f3fc326..b833686e1b 100644 --- a/Makefile +++ b/Makefile @@ -170,7 +170,7 @@ test-full: cargo-fmt test-release test-debug test-ef test-exec-engine # Lints the code for bad style and potentially unsafe arithmetic using Clippy. # Clippy lints are opt-in per-crate for now. By default, everything is allowed except for performance and correctness lints. lint: - cargo clippy --workspace --tests -- \ + cargo clippy --workspace --tests $(EXTRA_CLIPPY_OPTS) -- \ -D clippy::fn_to_numeric_cast_any \ -D warnings \ -A clippy::derive_partial_eq_without_eq \ @@ -180,6 +180,10 @@ lint: -A clippy::question-mark \ -A clippy::uninlined-format-args +# Lints the code using Clippy and automatically fix some simple compiler warnings. +lint-fix: + EXTRA_CLIPPY_OPTS="--fix --allow-staged --allow-dirty" $(MAKE) lint + nightly-lint: cp .github/custom/clippy.toml . cargo +$(CLIPPY_PINNED_NIGHTLY) clippy --workspace --tests --release -- \ From cc780aae3e0cb89649086a3b63cb02a4f97f7ae2 Mon Sep 17 00:00:00 2001 From: Mac L Date: Thu, 22 Jun 2023 02:14:59 +0000 Subject: [PATCH 53/63] Bump `openssl` deps (#4421) ## Proposed Changes Bump the `openssl` deps to resolve the `cargo-audit` failure caused by https://rustsec.org/advisories/RUSTSEC-2023-0044.html --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 18276b3ea3..8239183710 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5692,9 +5692,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.52" +version = "0.10.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56" +checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" dependencies = [ "bitflags", "cfg-if", @@ -5733,9 +5733,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.87" +version = "0.9.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e" +checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" dependencies = [ "cc", "libc", From 448d3ec9b3a7c90c5209aaa1d50091f2b7303dcf Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Tue, 27 Jun 2023 01:06:49 +0000 Subject: [PATCH 54/63] Aggregate subsets (#3493) ## Issue Addressed Resolves #3238 ## Proposed Changes Please list or describe the changes introduced by this PR. ## Additional Info Please provide any additional information. For example, future considerations or information useful for reviewers. --- Cargo.lock | 5 +- beacon_node/beacon_chain/Cargo.toml | 3 +- .../src/attestation_verification.rs | 36 ++-- beacon_node/beacon_chain/src/metrics.rs | 11 + .../beacon_chain/src/observed_aggregates.rs | 200 +++++++++++++----- .../src/sync_committee_verification.rs | 38 +++- .../tests/attestation_verification.rs | 4 +- .../tests/sync_committee_verification.rs | 12 +- beacon_node/execution_layer/Cargo.toml | 2 +- beacon_node/http_api/src/lib.rs | 2 +- beacon_node/http_api/src/sync_committees.rs | 2 +- beacon_node/lighthouse_network/Cargo.toml | 2 +- beacon_node/network/Cargo.toml | 2 +- .../beacon_processor/worker/gossip_methods.rs | 4 +- consensus/cached_tree_hash/Cargo.toml | 2 +- consensus/state_processing/Cargo.toml | 2 +- consensus/types/Cargo.toml | 2 +- 17 files changed, 236 insertions(+), 93 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8239183710..700aecb83a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -661,6 +661,7 @@ dependencies = [ "tokio", "tokio-stream", "tree_hash", + "tree_hash_derive", "types", "unused_port", ] @@ -7849,9 +7850,9 @@ dependencies = [ [[package]] name = "ssz_types" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8052a1004e979c0be24b9e55940195553103cc57d0b34f7e2c4e32793325e402" +checksum = "e43767964a80b2fdeda7a79a57a2b6cbca966688d5b81da8fe91140a94f552a1" dependencies = [ "arbitrary", "derivative", diff --git a/beacon_node/beacon_chain/Cargo.toml b/beacon_node/beacon_chain/Cargo.toml index 27d07e3338..7f884f561b 100644 --- a/beacon_node/beacon_chain/Cargo.toml +++ b/beacon_node/beacon_chain/Cargo.toml @@ -32,9 +32,10 @@ sloggers = { version = "2.1.1", features = ["json"] } slot_clock = { path = "../../common/slot_clock" } ethereum_hashing = "1.0.0-beta.2" ethereum_ssz = "0.5.0" -ssz_types = "0.5.0" +ssz_types = "0.5.3" ethereum_ssz_derive = "0.5.0" state_processing = { path = "../../consensus/state_processing" } +tree_hash_derive = "0.5.0" tree_hash = "0.5.0" types = { path = "../../consensus/types" } tokio = "1.14.0" diff --git a/beacon_node/beacon_chain/src/attestation_verification.rs b/beacon_node/beacon_chain/src/attestation_verification.rs index 04f601fad9..6df0758b2e 100644 --- a/beacon_node/beacon_chain/src/attestation_verification.rs +++ b/beacon_node/beacon_chain/src/attestation_verification.rs @@ -117,14 +117,14 @@ pub enum Error { /// /// The peer has sent an invalid message. AggregatorPubkeyUnknown(u64), - /// The attestation has been seen before; either in a block, on the gossip network or from a - /// local validator. + /// The attestation or a superset of this attestation's aggregations bits for the same data + /// has been seen before; either in a block, on the gossip network or from a local validator. /// /// ## Peer scoring /// /// It's unclear if this attestation is valid, however we have already observed it and do not /// need to observe it again. - AttestationAlreadyKnown(Hash256), + AttestationSupersetKnown(Hash256), /// There has already been an aggregation observed for this validator, we refuse to process a /// second. /// @@ -268,7 +268,7 @@ enum CheckAttestationSignature { struct IndexedAggregatedAttestation<'a, T: BeaconChainTypes> { signed_aggregate: &'a SignedAggregateAndProof, indexed_attestation: IndexedAttestation, - attestation_root: Hash256, + attestation_data_root: Hash256, } /// Wraps a `Attestation` that has been verified up until the point that an `IndexedAttestation` can @@ -467,14 +467,17 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { } // Ensure the valid aggregated attestation has not already been seen locally. - let attestation_root = attestation.tree_hash_root(); + let attestation_data = &attestation.data; + let attestation_data_root = attestation_data.tree_hash_root(); + if chain .observed_attestations .write() - .is_known(attestation, attestation_root) + .is_known_subset(attestation, attestation_data_root) .map_err(|e| Error::BeaconChainError(e.into()))? { - return Err(Error::AttestationAlreadyKnown(attestation_root)); + metrics::inc_counter(&metrics::AGGREGATED_ATTESTATION_SUBSETS); + return Err(Error::AttestationSupersetKnown(attestation_data_root)); } let aggregator_index = signed_aggregate.message.aggregator_index; @@ -520,7 +523,7 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { if attestation.aggregation_bits.is_zero() { Err(Error::EmptyAggregationBitfield) } else { - Ok(attestation_root) + Ok(attestation_data_root) } } @@ -533,7 +536,7 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { let attestation = &signed_aggregate.message.aggregate; let aggregator_index = signed_aggregate.message.aggregator_index; - let attestation_root = match Self::verify_early_checks(signed_aggregate, chain) { + let attestation_data_root = match Self::verify_early_checks(signed_aggregate, chain) { Ok(root) => root, Err(e) => return Err(SignatureNotChecked(&signed_aggregate.message.aggregate, e)), }; @@ -568,7 +571,7 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { Ok(IndexedAggregatedAttestation { signed_aggregate, indexed_attestation, - attestation_root, + attestation_data_root, }) } } @@ -577,7 +580,7 @@ impl<'a, T: BeaconChainTypes> VerifiedAggregatedAttestation<'a, T> { /// Run the checks that happen after the indexed attestation and signature have been checked. fn verify_late_checks( signed_aggregate: &SignedAggregateAndProof, - attestation_root: Hash256, + attestation_data_root: Hash256, chain: &BeaconChain, ) -> Result<(), Error> { let attestation = &signed_aggregate.message.aggregate; @@ -587,13 +590,14 @@ impl<'a, T: BeaconChainTypes> VerifiedAggregatedAttestation<'a, T> { // // It's important to double check that the attestation is not already known, otherwise two // attestations processed at the same time could be published. - if let ObserveOutcome::AlreadyKnown = chain + if let ObserveOutcome::Subset = chain .observed_attestations .write() - .observe_item(attestation, Some(attestation_root)) + .observe_item(attestation, Some(attestation_data_root)) .map_err(|e| Error::BeaconChainError(e.into()))? { - return Err(Error::AttestationAlreadyKnown(attestation_root)); + metrics::inc_counter(&metrics::AGGREGATED_ATTESTATION_SUBSETS); + return Err(Error::AttestationSupersetKnown(attestation_data_root)); } // Observe the aggregator so we don't process another aggregate from them. @@ -653,7 +657,7 @@ impl<'a, T: BeaconChainTypes> VerifiedAggregatedAttestation<'a, T> { let IndexedAggregatedAttestation { signed_aggregate, indexed_attestation, - attestation_root, + attestation_data_root, } = signed_aggregate; match check_signature { @@ -677,7 +681,7 @@ impl<'a, T: BeaconChainTypes> VerifiedAggregatedAttestation<'a, T> { CheckAttestationSignature::No => (), }; - if let Err(e) = Self::verify_late_checks(signed_aggregate, attestation_root, chain) { + if let Err(e) = Self::verify_late_checks(signed_aggregate, attestation_data_root, chain) { return Err(SignatureValid(indexed_attestation, e)); } diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index d0f695062f..dff663ded0 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -998,6 +998,17 @@ lazy_static! { "light_client_optimistic_update_verification_success_total", "Number of light client optimistic updates verified for gossip" ); + /* + * Aggregate subset metrics + */ + pub static ref SYNC_CONTRIBUTION_SUBSETS: Result = try_create_int_counter( + "beacon_sync_contribution_subsets_total", + "Count of new sync contributions that are subsets of already known aggregates" + ); + pub static ref AGGREGATED_ATTESTATION_SUBSETS: Result = try_create_int_counter( + "beacon_aggregated_attestation_subsets_total", + "Count of new aggregated attestations that are subsets of already known aggregates" + ); } /// Scrape the `beacon_chain` for metrics that are not constantly updated (e.g., the present slot, diff --git a/beacon_node/beacon_chain/src/observed_aggregates.rs b/beacon_node/beacon_chain/src/observed_aggregates.rs index bb0132f5fe..18a761e29e 100644 --- a/beacon_node/beacon_chain/src/observed_aggregates.rs +++ b/beacon_node/beacon_chain/src/observed_aggregates.rs @@ -1,7 +1,9 @@ //! Provides an `ObservedAggregates` struct which allows us to reject aggregated attestations or //! sync committee contributions if we've already seen them. -use std::collections::HashSet; +use crate::sync_committee_verification::SyncCommitteeData; +use ssz_types::{BitList, BitVector}; +use std::collections::HashMap; use std::marker::PhantomData; use tree_hash::TreeHash; use types::consts::altair::{ @@ -10,8 +12,16 @@ use types::consts::altair::{ use types::slot_data::SlotData; use types::{Attestation, EthSpec, Hash256, Slot, SyncCommitteeContribution}; -pub type ObservedSyncContributions = ObservedAggregates, E>; -pub type ObservedAggregateAttestations = ObservedAggregates, E>; +pub type ObservedSyncContributions = ObservedAggregates< + SyncCommitteeContribution, + E, + BitVector<::SyncSubcommitteeSize>, +>; +pub type ObservedAggregateAttestations = ObservedAggregates< + Attestation, + E, + BitList<::MaxValidatorsPerCommittee>, +>; /// A trait use to associate capacity constants with the type being stored in `ObservedAggregates`. pub trait Consts { @@ -69,10 +79,81 @@ impl Consts for SyncCommitteeContribution { } } +/// A trait for types that implement a behaviour where one object of that type +/// can be a subset/superset of another. +/// This trait allows us to be generic over the aggregate item that we store in the cache that +/// we want to prevent duplicates/subsets for. +pub trait SubsetItem { + /// The item that is stored for later comparison with new incoming aggregate items. + type Item; + + /// Returns `true` if `self` is a non-strict subset of `other` and `false` otherwise. + fn is_subset(&self, other: &Self::Item) -> bool; + + /// Returns `true` if `self` is a non-strict superset of `other` and `false` otherwise. + fn is_superset(&self, other: &Self::Item) -> bool; + + /// Returns the item that gets stored in `ObservedAggregates` for later subset + /// comparison with incoming aggregates. + fn get_item(&self) -> Self::Item; + + /// Returns a unique value that keys the object to the item that is being stored + /// in `ObservedAggregates`. + fn root(&self) -> Hash256; +} + +impl SubsetItem for Attestation { + type Item = BitList; + fn is_subset(&self, other: &Self::Item) -> bool { + self.aggregation_bits.is_subset(other) + } + + fn is_superset(&self, other: &Self::Item) -> bool { + other.is_subset(&self.aggregation_bits) + } + + /// Returns the sync contribution aggregation bits. + fn get_item(&self) -> Self::Item { + self.aggregation_bits.clone() + } + + /// Returns the hash tree root of the attestation data. + fn root(&self) -> Hash256 { + self.data.tree_hash_root() + } +} + +impl SubsetItem for SyncCommitteeContribution { + type Item = BitVector; + fn is_subset(&self, other: &Self::Item) -> bool { + self.aggregation_bits.is_subset(other) + } + + fn is_superset(&self, other: &Self::Item) -> bool { + other.is_subset(&self.aggregation_bits) + } + + /// Returns the sync contribution aggregation bits. + fn get_item(&self) -> Self::Item { + self.aggregation_bits.clone() + } + + /// Returns the hash tree root of the root, slot and subcommittee index + /// of the sync contribution. + fn root(&self) -> Hash256 { + SyncCommitteeData { + root: self.beacon_block_root, + slot: self.slot, + subcommittee_index: self.subcommittee_index, + } + .tree_hash_root() + } +} + #[derive(Debug, PartialEq)] pub enum ObserveOutcome { - /// This item was already known. - AlreadyKnown, + /// This item is a non-strict subset of an already known item. + Subset, /// This was the first time this item was observed. New, } @@ -94,26 +175,28 @@ pub enum Error { }, } -/// A `HashSet` that contains entries related to some `Slot`. -struct SlotHashSet { - set: HashSet, +/// A `HashMap` that contains entries related to some `Slot`. +struct SlotHashSet { + /// Contains a vector of maximally-sized aggregation bitfields/bitvectors + /// such that no bitfield/bitvector is a subset of any other in the list. + map: HashMap>, slot: Slot, max_capacity: usize, } -impl SlotHashSet { +impl SlotHashSet { pub fn new(slot: Slot, initial_capacity: usize, max_capacity: usize) -> Self { Self { slot, - set: HashSet::with_capacity(initial_capacity), + map: HashMap::with_capacity(initial_capacity), max_capacity, } } /// Store the items in self so future observations recognise its existence. - pub fn observe_item( + pub fn observe_item>( &mut self, - item: &T, + item: &S, root: Hash256, ) -> Result { if item.get_slot() != self.slot { @@ -123,29 +206,45 @@ impl SlotHashSet { }); } - if self.set.contains(&root) { - Ok(ObserveOutcome::AlreadyKnown) - } else { - // Here we check to see if this slot has reached the maximum observation count. - // - // The resulting behaviour is that we are no longer able to successfully observe new - // items, however we will continue to return `is_known` values. We could also - // disable `is_known`, however then we would stop forwarding items across the - // gossip network and I think that this is a worse case than sending some invalid ones. - // The underlying libp2p network is responsible for removing duplicate messages, so - // this doesn't risk a broadcast loop. - if self.set.len() >= self.max_capacity { - return Err(Error::ReachedMaxObservationsPerSlot(self.max_capacity)); + if let Some(aggregates) = self.map.get_mut(&root) { + for existing in aggregates { + // Check if `item` is a subset of any of the observed aggregates + if item.is_subset(existing) { + return Ok(ObserveOutcome::Subset); + // Check if `item` is a superset of any of the observed aggregates + // If true, we replace the new item with its existing subset. This allows us + // to hold fewer items in the list. + } else if item.is_superset(existing) { + *existing = item.get_item(); + return Ok(ObserveOutcome::New); + } } - - self.set.insert(root); - - Ok(ObserveOutcome::New) } + + // Here we check to see if this slot has reached the maximum observation count. + // + // The resulting behaviour is that we are no longer able to successfully observe new + // items, however we will continue to return `is_known_subset` values. We could also + // disable `is_known_subset`, however then we would stop forwarding items across the + // gossip network and I think that this is a worse case than sending some invalid ones. + // The underlying libp2p network is responsible for removing duplicate messages, so + // this doesn't risk a broadcast loop. + if self.map.len() >= self.max_capacity { + return Err(Error::ReachedMaxObservationsPerSlot(self.max_capacity)); + } + + let item = item.get_item(); + self.map.entry(root).or_default().push(item); + Ok(ObserveOutcome::New) } - /// Indicates if `item` has been observed before. - pub fn is_known(&self, item: &T, root: Hash256) -> Result { + /// Check if `item` is a non-strict subset of any of the already observed aggregates for + /// the given root and slot. + pub fn is_known_subset>( + &self, + item: &S, + root: Hash256, + ) -> Result { if item.get_slot() != self.slot { return Err(Error::IncorrectSlot { expected: self.slot, @@ -153,25 +252,28 @@ impl SlotHashSet { }); } - Ok(self.set.contains(&root)) + Ok(self + .map + .get(&root) + .map_or(false, |agg| agg.iter().any(|val| item.is_subset(val)))) } /// The number of observed items in `self`. pub fn len(&self) -> usize { - self.set.len() + self.map.len() } } /// Stores the roots of objects for some number of `Slots`, so we can determine if /// these have previously been seen on the network. -pub struct ObservedAggregates { +pub struct ObservedAggregates { lowest_permissible_slot: Slot, - sets: Vec, + sets: Vec>, _phantom_spec: PhantomData, _phantom_tree_hash: PhantomData, } -impl Default for ObservedAggregates { +impl Default for ObservedAggregates { fn default() -> Self { Self { lowest_permissible_slot: Slot::new(0), @@ -182,17 +284,17 @@ impl Default for ObservedAggregates } } -impl ObservedAggregates { - /// Store the root of `item` in `self`. +impl, E: EthSpec, I> ObservedAggregates { + /// Store `item` in `self` keyed at `root`. /// - /// `root` must equal `item.tree_hash_root()`. + /// `root` must equal `item.root::()`. pub fn observe_item( &mut self, item: &T, root_opt: Option, ) -> Result { let index = self.get_set_index(item.get_slot())?; - let root = root_opt.unwrap_or_else(|| item.tree_hash_root()); + let root = root_opt.unwrap_or_else(|| item.root()); self.sets .get_mut(index) @@ -200,17 +302,18 @@ impl ObservedAggregates { .and_then(|set| set.observe_item(item, root)) } - /// Check to see if the `root` of `item` is in self. + /// Check if `item` is a non-strict subset of any of the already observed aggregates for + /// the given root and slot. /// - /// `root` must equal `a.tree_hash_root()`. + /// `root` must equal `item.root::()`. #[allow(clippy::wrong_self_convention)] - pub fn is_known(&mut self, item: &T, root: Hash256) -> Result { + pub fn is_known_subset(&mut self, item: &T, root: Hash256) -> Result { let index = self.get_set_index(item.get_slot())?; self.sets .get(index) .ok_or(Error::InvalidSetIndex(index)) - .and_then(|set| set.is_known(item, root)) + .and_then(|set| set.is_known_subset(item, root)) } /// The maximum number of slots that items are stored for. @@ -296,7 +399,6 @@ impl ObservedAggregates { #[cfg(not(debug_assertions))] mod tests { use super::*; - use tree_hash::TreeHash; use types::{test_utils::test_random_instance, Hash256}; type E = types::MainnetEthSpec; @@ -330,7 +432,7 @@ mod tests { for a in &items { assert_eq!( - store.is_known(a, a.tree_hash_root()), + store.is_known_subset(a, a.root()), Ok(false), "should indicate an unknown attestation is unknown" ); @@ -343,13 +445,13 @@ mod tests { for a in &items { assert_eq!( - store.is_known(a, a.tree_hash_root()), + store.is_known_subset(a, a.root()), Ok(true), "should indicate a known attestation is known" ); assert_eq!( - store.observe_item(a, Some(a.tree_hash_root())), - Ok(ObserveOutcome::AlreadyKnown), + store.observe_item(a, Some(a.root())), + Ok(ObserveOutcome::Subset), "should acknowledge an existing attestation" ); } diff --git a/beacon_node/beacon_chain/src/sync_committee_verification.rs b/beacon_node/beacon_chain/src/sync_committee_verification.rs index 14cdc2400d..246bb12cc0 100644 --- a/beacon_node/beacon_chain/src/sync_committee_verification.rs +++ b/beacon_node/beacon_chain/src/sync_committee_verification.rs @@ -37,6 +37,7 @@ use bls::{verify_signature_sets, PublicKeyBytes}; use derivative::Derivative; use safe_arith::ArithError; use slot_clock::SlotClock; +use ssz_derive::{Decode, Encode}; use state_processing::per_block_processing::errors::SyncCommitteeMessageValidationError; use state_processing::signature_sets::{ signed_sync_aggregate_selection_proof_signature_set, signed_sync_aggregate_signature_set, @@ -47,6 +48,7 @@ use std::borrow::Cow; use std::collections::HashMap; use strum::AsRefStr; use tree_hash::TreeHash; +use tree_hash_derive::TreeHash; use types::consts::altair::SYNC_COMMITTEE_SUBNET_COUNT; use types::slot_data::SlotData; use types::sync_committee::Error as SyncCommitteeError; @@ -110,14 +112,14 @@ pub enum Error { /// /// The peer has sent an invalid message. AggregatorPubkeyUnknown(u64), - /// The sync contribution has been seen before; either in a block, on the gossip network or from a - /// local validator. + /// The sync contribution or a superset of this sync contribution's aggregation bits for the same data + /// has been seen before; either in a block on the gossip network or from a local validator. /// /// ## Peer scoring /// /// It's unclear if this sync contribution is valid, however we have already observed it and do not /// need to observe it again. - SyncContributionAlreadyKnown(Hash256), + SyncContributionSupersetKnown(Hash256), /// There has already been an aggregation observed for this validator, we refuse to process a /// second. /// @@ -268,6 +270,14 @@ pub struct VerifiedSyncContribution { participant_pubkeys: Vec, } +/// The sync contribution data. +#[derive(Encode, Decode, TreeHash)] +pub struct SyncCommitteeData { + pub slot: Slot, + pub root: Hash256, + pub subcommittee_index: u64, +} + /// Wraps a `SyncCommitteeMessage` that has been verified for propagation on the gossip network. #[derive(Clone)] pub struct VerifiedSyncCommitteeMessage { @@ -314,15 +324,22 @@ impl VerifiedSyncContribution { return Err(Error::AggregatorNotInCommittee { aggregator_index }); }; - // Ensure the valid sync contribution has not already been seen locally. - let contribution_root = contribution.tree_hash_root(); + // Ensure the valid sync contribution or its superset has not already been seen locally. + let contribution_data_root = SyncCommitteeData { + slot: contribution.slot, + root: contribution.beacon_block_root, + subcommittee_index: contribution.subcommittee_index, + } + .tree_hash_root(); + if chain .observed_sync_contributions .write() - .is_known(contribution, contribution_root) + .is_known_subset(contribution, contribution_data_root) .map_err(|e| Error::BeaconChainError(e.into()))? { - return Err(Error::SyncContributionAlreadyKnown(contribution_root)); + metrics::inc_counter(&metrics::SYNC_CONTRIBUTION_SUBSETS); + return Err(Error::SyncContributionSupersetKnown(contribution_data_root)); } // Ensure there has been no other observed aggregate for the given `aggregator_index`. @@ -376,13 +393,14 @@ impl VerifiedSyncContribution { // // It's important to double check that the contribution is not already known, otherwise two // contribution processed at the same time could be published. - if let ObserveOutcome::AlreadyKnown = chain + if let ObserveOutcome::Subset = chain .observed_sync_contributions .write() - .observe_item(contribution, Some(contribution_root)) + .observe_item(contribution, Some(contribution_data_root)) .map_err(|e| Error::BeaconChainError(e.into()))? { - return Err(Error::SyncContributionAlreadyKnown(contribution_root)); + metrics::inc_counter(&metrics::SYNC_CONTRIBUTION_SUBSETS); + return Err(Error::SyncContributionSupersetKnown(contribution_data_root)); } // Observe the aggregator so we don't process another aggregate from them. diff --git a/beacon_node/beacon_chain/tests/attestation_verification.rs b/beacon_node/beacon_chain/tests/attestation_verification.rs index 1040521e5a..5cea51090b 100644 --- a/beacon_node/beacon_chain/tests/attestation_verification.rs +++ b/beacon_node/beacon_chain/tests/attestation_verification.rs @@ -699,8 +699,8 @@ async fn aggregated_gossip_verification() { |tester, err| { assert!(matches!( err, - AttnError::AttestationAlreadyKnown(hash) - if hash == tester.valid_aggregate.message.aggregate.tree_hash_root() + AttnError::AttestationSupersetKnown(hash) + if hash == tester.valid_aggregate.message.aggregate.data.tree_hash_root() )) }, ) diff --git a/beacon_node/beacon_chain/tests/sync_committee_verification.rs b/beacon_node/beacon_chain/tests/sync_committee_verification.rs index 4204a51212..0e4745ff6b 100644 --- a/beacon_node/beacon_chain/tests/sync_committee_verification.rs +++ b/beacon_node/beacon_chain/tests/sync_committee_verification.rs @@ -1,6 +1,6 @@ #![cfg(not(debug_assertions))] -use beacon_chain::sync_committee_verification::Error as SyncCommitteeError; +use beacon_chain::sync_committee_verification::{Error as SyncCommitteeError, SyncCommitteeData}; use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType, RelativeSyncCommittee}; use int_to_bytes::int_to_bytes32; use lazy_static::lazy_static; @@ -444,11 +444,17 @@ async fn aggregated_gossip_verification() { * subcommittee index contribution.subcommittee_index. */ + let contribution = &valid_aggregate.message.contribution; + let sync_committee_data = SyncCommitteeData { + slot: contribution.slot, + root: contribution.beacon_block_root, + subcommittee_index: contribution.subcommittee_index, + }; assert_invalid!( "aggregate that has already been seen", valid_aggregate.clone(), - SyncCommitteeError::SyncContributionAlreadyKnown(hash) - if hash == valid_aggregate.message.contribution.tree_hash_root() + SyncCommitteeError::SyncContributionSupersetKnown(hash) + if hash == sync_committee_data.tree_hash_root() ); /* diff --git a/beacon_node/execution_layer/Cargo.toml b/beacon_node/execution_layer/Cargo.toml index 3ed7ba65d6..a96cfb6cac 100644 --- a/beacon_node/execution_layer/Cargo.toml +++ b/beacon_node/execution_layer/Cargo.toml @@ -23,7 +23,7 @@ bytes = "1.1.0" task_executor = { path = "../../common/task_executor" } hex = "0.4.2" ethereum_ssz = "0.5.0" -ssz_types = "0.5.0" +ssz_types = "0.5.3" eth2 = { path = "../../common/eth2" } state_processing = { path = "../../consensus/state_processing" } superstruct = "0.6.0" diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 55e00bab34..025b54f326 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -2843,7 +2843,7 @@ pub fn serve( // It's reasonably likely that two different validators produce // identical aggregates, especially if they're using the same beacon // node. - Err(AttnError::AttestationAlreadyKnown(_)) => continue, + Err(AttnError::AttestationSupersetKnown(_)) => continue, // If we've already seen this aggregator produce an aggregate, just // skip this one. // diff --git a/beacon_node/http_api/src/sync_committees.rs b/beacon_node/http_api/src/sync_committees.rs index c728fbeb14..07dfb5c988 100644 --- a/beacon_node/http_api/src/sync_committees.rs +++ b/beacon_node/http_api/src/sync_committees.rs @@ -304,7 +304,7 @@ pub fn process_signed_contribution_and_proofs( } // If we already know the contribution, don't broadcast it or attempt to // further verify it. Return success. - Err(SyncVerificationError::SyncContributionAlreadyKnown(_)) => continue, + Err(SyncVerificationError::SyncContributionSupersetKnown(_)) => continue, // If we've already seen this aggregator produce an aggregate, just // skip this one. // diff --git a/beacon_node/lighthouse_network/Cargo.toml b/beacon_node/lighthouse_network/Cargo.toml index ca15b5ef2c..6d056d8350 100644 --- a/beacon_node/lighthouse_network/Cargo.toml +++ b/beacon_node/lighthouse_network/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" discv5 = { version = "0.3.0", features = ["libp2p"]} unsigned-varint = { version = "0.6.0", features = ["codec"] } types = { path = "../../consensus/types" } -ssz_types = "0.5.0" +ssz_types = "0.5.3" serde = { version = "1.0.116", features = ["derive"] } serde_derive = "1.0.116" ethereum_ssz = "0.5.0" diff --git a/beacon_node/network/Cargo.toml b/beacon_node/network/Cargo.toml index c991728994..aa1827787c 100644 --- a/beacon_node/network/Cargo.toml +++ b/beacon_node/network/Cargo.toml @@ -22,7 +22,7 @@ slot_clock = { path = "../../common/slot_clock" } slog = { version = "2.5.2", features = ["max_level_trace"] } hex = "0.4.2" ethereum_ssz = "0.5.0" -ssz_types = "0.5.0" +ssz_types = "0.5.3" futures = "0.3.7" error-chain = "0.12.4" tokio = { version = "1.14.0", features = ["full"] } diff --git a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs b/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs index e3cff00103..185634c308 100644 --- a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs +++ b/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs @@ -1735,7 +1735,7 @@ impl Worker { "attn_agg_not_in_committee", ); } - AttnError::AttestationAlreadyKnown { .. } => { + AttnError::AttestationSupersetKnown { .. } => { /* * The aggregate attestation has already been observed on the network or in * a block. @@ -2244,7 +2244,7 @@ impl Worker { "sync_bad_aggregator", ); } - SyncCommitteeError::SyncContributionAlreadyKnown(_) + SyncCommitteeError::SyncContributionSupersetKnown(_) | SyncCommitteeError::AggregatorAlreadyKnown(_) => { /* * The sync committee message already been observed on the network or in diff --git a/consensus/cached_tree_hash/Cargo.toml b/consensus/cached_tree_hash/Cargo.toml index c2856003bf..0f43c8890f 100644 --- a/consensus/cached_tree_hash/Cargo.toml +++ b/consensus/cached_tree_hash/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] ethereum-types = "0.14.1" -ssz_types = "0.5.0" +ssz_types = "0.5.3" ethereum_hashing = "1.0.0-beta.2" ethereum_ssz_derive = "0.5.0" ethereum_ssz = "0.5.0" diff --git a/consensus/state_processing/Cargo.toml b/consensus/state_processing/Cargo.toml index c16742782c..f19cd1d29d 100644 --- a/consensus/state_processing/Cargo.toml +++ b/consensus/state_processing/Cargo.toml @@ -15,7 +15,7 @@ integer-sqrt = "0.1.5" itertools = "0.10.0" ethereum_ssz = "0.5.0" ethereum_ssz_derive = "0.5.0" -ssz_types = "0.5.0" +ssz_types = "0.5.3" merkle_proof = { path = "../merkle_proof" } safe_arith = { path = "../safe_arith" } tree_hash = "0.5.0" diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index 91ad3089f1..583b940d5f 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -27,7 +27,7 @@ serde_derive = "1.0.116" slog = "2.5.2" ethereum_ssz = { version = "0.5.0", features = ["arbitrary"] } ethereum_ssz_derive = "0.5.0" -ssz_types = { version = "0.5.0", features = ["arbitrary"] } +ssz_types = { version = "0.5.3", features = ["arbitrary"] } swap_or_not_shuffle = { path = "../swap_or_not_shuffle", features = ["arbitrary"] } test_random_derive = { path = "../../common/test_random_derive" } tree_hash = { version = "0.5.0", features = ["arbitrary"] } From 9072acbfa611367e9e88ce1c5ea1bd6b4ca9ea8d Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 27 Jun 2023 01:06:50 +0000 Subject: [PATCH 55/63] Tidy formatting of `Reqwest` errors (#4336) ## Issue Addressed NA ## Proposed Changes Implements the `PrettyReqwestError` to wrap a `reqwest::Error` and give nicer `Debug` formatting. It also wraps the `Url` component in a `SensitiveUrl` to avoid leaking sensitive info in logs. ### Before ``` Reqwest(reqwest::Error { kind: Request, url: Url { scheme: "http", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("localhost")), port: Some(9999), path: "/eth/v1/node/version", query: None, fragment: None }, source: hyper::Error(Connect, ConnectError("tcp connect error", Os { code: 61, kind: ConnectionRefused, message: "Connection refused" })) }) ``` ### After ``` HttpClient(url: http://localhost:9999/, kind: request, detail: error trying to connect: tcp connect error: Connection refused (os error 61)) ``` ## Additional Info I've also renamed the `Reqwest` error enum variants to `HttpClient`, to give people a better chance at knowing what's going on. Reqwest is pretty odd and looks like a typo. I've implemented it in the `eth2` and `execution_layer` crates. This should affect most logs in the VC and EE-related ones in the BN. I think the last crate that could benefit from the is the `beacon_node/eth1` crate. I haven't updated it in this PR since its error type is not so amenable to it (everything goes into a `String`). I don't have a whole lot of time to jig around with that at the moment and I feel that this PR as it stands is a significant enough improvement to merge on its own. Leaving it as-is is fine for the time being and we can always come back for it later (or implement in-protocol deposits!). --- Cargo.lock | 12 ++++ Cargo.toml | 1 + beacon_node/builder_client/src/lib.rs | 6 +- beacon_node/execution_layer/Cargo.toml | 1 + beacon_node/execution_layer/src/engine_api.rs | 5 +- common/eth2/Cargo.toml | 5 ++ common/eth2/src/lib.rs | 13 ++-- common/eth2/src/lighthouse.rs | 4 +- common/eth2/src/lighthouse_vc/http_client.rs | 12 ++-- common/pretty_reqwest_error/Cargo.toml | 10 +++ common/pretty_reqwest_error/src/lib.rs | 62 +++++++++++++++++++ common/sensitive_url/src/lib.rs | 2 +- 12 files changed, 113 insertions(+), 20 deletions(-) create mode 100644 common/pretty_reqwest_error/Cargo.toml create mode 100644 common/pretty_reqwest_error/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 700aecb83a..02922b2d7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2292,6 +2292,8 @@ dependencies = [ "libsecp256k1", "lighthouse_network", "mediatype", + "mime", + "pretty_reqwest_error", "procinfo", "proto_array", "psutil", @@ -2302,6 +2304,7 @@ dependencies = [ "serde_json", "slashing_protection", "store", + "tokio", "types", ] @@ -2742,6 +2745,7 @@ dependencies = [ "lru 0.7.8", "mev-rs", "parking_lot 0.12.1", + "pretty_reqwest_error", "rand 0.8.5", "reqwest", "sensitive_url", @@ -6204,6 +6208,14 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "pretty_reqwest_error" +version = "0.1.0" +dependencies = [ + "reqwest", + "sensitive_url", +] + [[package]] name = "prettyplease" version = "0.1.25" diff --git a/Cargo.toml b/Cargo.toml index bbe77d2096..5c39e01ed1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ members = [ "common/lru_cache", "common/malloc_utils", "common/oneshot_broadcast", + "common/pretty_reqwest_error", "common/sensitive_url", "common/slot_clock", "common/system_health", diff --git a/beacon_node/builder_client/src/lib.rs b/beacon_node/builder_client/src/lib.rs index 255c2fdd19..c78f686d02 100644 --- a/beacon_node/builder_client/src/lib.rs +++ b/beacon_node/builder_client/src/lib.rs @@ -72,7 +72,7 @@ impl BuilderHttpClient { .await? .json() .await - .map_err(Error::Reqwest) + .map_err(Into::into) } /// Perform a HTTP GET request, returning the `Response` for further processing. @@ -85,7 +85,7 @@ impl BuilderHttpClient { if let Some(timeout) = timeout { builder = builder.timeout(timeout); } - let response = builder.send().await.map_err(Error::Reqwest)?; + let response = builder.send().await.map_err(Error::from)?; ok_or_error(response).await } @@ -114,7 +114,7 @@ impl BuilderHttpClient { if let Some(timeout) = timeout { builder = builder.timeout(timeout); } - let response = builder.json(body).send().await.map_err(Error::Reqwest)?; + let response = builder.json(body).send().await.map_err(Error::from)?; ok_or_error(response).await } diff --git a/beacon_node/execution_layer/Cargo.toml b/beacon_node/execution_layer/Cargo.toml index a96cfb6cac..2cb28346f5 100644 --- a/beacon_node/execution_layer/Cargo.toml +++ b/beacon_node/execution_layer/Cargo.toml @@ -50,3 +50,4 @@ keccak-hash = "0.10.0" hash256-std-hasher = "0.15.2" triehash = "0.8.4" hash-db = "0.15.2" +pretty_reqwest_error = { path = "../../common/pretty_reqwest_error" } \ No newline at end of file diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index 4d2eb565e1..826294d5ff 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -10,6 +10,7 @@ pub use ethers_core::types::Transaction; use ethers_core::utils::rlp::{self, Decodable, Rlp}; use http::deposit_methods::RpcError; pub use json_structures::{JsonWithdrawal, TransitionConfigurationV1}; +use pretty_reqwest_error::PrettyReqwestError; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; @@ -32,7 +33,7 @@ pub type PayloadId = [u8; 8]; #[derive(Debug)] pub enum Error { - Reqwest(reqwest::Error), + HttpClient(PrettyReqwestError), Auth(auth::Error), BadResponse(String), RequestFailed(String), @@ -67,7 +68,7 @@ impl From for Error { ) { Error::Auth(auth::Error::InvalidToken) } else { - Error::Reqwest(e) + Error::HttpClient(e.into()) } } } diff --git a/common/eth2/Cargo.toml b/common/eth2/Cargo.toml index 4eabd3ff86..d8e1a375fd 100644 --- a/common/eth2/Cargo.toml +++ b/common/eth2/Cargo.toml @@ -27,6 +27,11 @@ futures = "0.3.8" store = { path = "../../beacon_node/store", optional = true } slashing_protection = { path = "../../validator_client/slashing_protection", optional = true } mediatype = "0.19.13" +mime = "0.3.16" +pretty_reqwest_error = { path = "../../common/pretty_reqwest_error" } + +[dev-dependencies] +tokio = { version = "1.14.0", features = ["full"] } [target.'cfg(target_os = "linux")'.dependencies] psutil = { version = "3.2.2", optional = true } diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index e871efbc2c..217d356968 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -19,6 +19,7 @@ use self::types::{Error as ResponseError, *}; use futures::Stream; use futures_util::StreamExt; use lighthouse_network::PeerId; +use pretty_reqwest_error::PrettyReqwestError; pub use reqwest; use reqwest::{IntoUrl, RequestBuilder, Response}; pub use reqwest::{StatusCode, Url}; @@ -39,7 +40,7 @@ pub const CONSENSUS_VERSION_HEADER: &str = "Eth-Consensus-Version"; #[derive(Debug)] pub enum Error { /// The `reqwest` client raised an error. - Reqwest(reqwest::Error), + HttpClient(PrettyReqwestError), /// The server returned an error message where the body was able to be parsed. ServerMessage(ErrorMessage), /// The server returned an error message with an array of errors. @@ -70,7 +71,7 @@ pub enum Error { impl From for Error { fn from(error: reqwest::Error) -> Self { - Error::Reqwest(error) + Error::HttpClient(error.into()) } } @@ -78,7 +79,7 @@ impl Error { /// If the error has a HTTP status code, return it. pub fn status(&self) -> Option { match self { - Error::Reqwest(error) => error.status(), + Error::HttpClient(error) => error.inner().status(), Error::ServerMessage(msg) => StatusCode::try_from(msg.code).ok(), Error::ServerIndexedMessage(msg) => StatusCode::try_from(msg.code).ok(), Error::StatusCode(status) => Some(*status), @@ -278,7 +279,7 @@ impl BeaconNodeHttpClient { .await? .json() .await - .map_err(Error::Reqwest) + .map_err(Into::into) } /// Perform a HTTP POST request with a custom timeout. @@ -303,7 +304,7 @@ impl BeaconNodeHttpClient { .await? .json() .await - .map_err(Error::Reqwest) + .map_err(Error::from) } /// Generic POST function supporting arbitrary responses and timeouts. @@ -1645,7 +1646,7 @@ impl BeaconNodeHttpClient { .bytes_stream() .map(|next| match next { Ok(bytes) => EventKind::from_sse_bytes(bytes.as_ref()), - Err(e) => Err(Error::Reqwest(e)), + Err(e) => Err(Error::HttpClient(e.into())), })) } diff --git a/common/eth2/src/lighthouse.rs b/common/eth2/src/lighthouse.rs index bb933dbe12..1b4bcc0e39 100644 --- a/common/eth2/src/lighthouse.rs +++ b/common/eth2/src/lighthouse.rs @@ -364,12 +364,12 @@ pub struct DatabaseInfo { impl BeaconNodeHttpClient { /// Perform a HTTP GET request, returning `None` on a 404 error. async fn get_bytes_opt(&self, url: U) -> Result>, Error> { - let response = self.client.get(url).send().await.map_err(Error::Reqwest)?; + let response = self.client.get(url).send().await.map_err(Error::from)?; match ok_or_error(response).await { Ok(resp) => Ok(Some( resp.bytes() .await - .map_err(Error::Reqwest)? + .map_err(Error::from)? .into_iter() .collect::>(), )), diff --git a/common/eth2/src/lighthouse_vc/http_client.rs b/common/eth2/src/lighthouse_vc/http_client.rs index 720d8c7795..cd7873c9b6 100644 --- a/common/eth2/src/lighthouse_vc/http_client.rs +++ b/common/eth2/src/lighthouse_vc/http_client.rs @@ -170,7 +170,7 @@ impl ValidatorClientHttpClient { .map_err(|_| Error::InvalidSignatureHeader)? .to_string(); - let body = response.bytes().await.map_err(Error::Reqwest)?; + let body = response.bytes().await.map_err(Error::from)?; let message = Message::parse_slice(digest(&SHA256, &body).as_ref()).expect("sha256 is 32 bytes"); @@ -222,7 +222,7 @@ impl ValidatorClientHttpClient { .headers(self.headers()?) .send() .await - .map_err(Error::Reqwest)?; + .map_err(Error::from)?; ok_or_error(response).await } @@ -236,7 +236,7 @@ impl ValidatorClientHttpClient { .await? .json() .await - .map_err(Error::Reqwest) + .map_err(Error::from) } /// Perform a HTTP GET request, returning `None` on a 404 error. @@ -266,7 +266,7 @@ impl ValidatorClientHttpClient { .json(body) .send() .await - .map_err(Error::Reqwest)?; + .map_err(Error::from)?; ok_or_error(response).await } @@ -297,7 +297,7 @@ impl ValidatorClientHttpClient { .json(body) .send() .await - .map_err(Error::Reqwest)?; + .map_err(Error::from)?; let response = ok_or_error(response).await?; self.signed_body(response).await?; Ok(()) @@ -316,7 +316,7 @@ impl ValidatorClientHttpClient { .json(body) .send() .await - .map_err(Error::Reqwest)?; + .map_err(Error::from)?; ok_or_error(response).await } diff --git a/common/pretty_reqwest_error/Cargo.toml b/common/pretty_reqwest_error/Cargo.toml new file mode 100644 index 0000000000..ca9f4812b0 --- /dev/null +++ b/common/pretty_reqwest_error/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "pretty_reqwest_error" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +reqwest = { version = "0.11.0", features = ["json","stream"] } +sensitive_url = { path = "../sensitive_url" } diff --git a/common/pretty_reqwest_error/src/lib.rs b/common/pretty_reqwest_error/src/lib.rs new file mode 100644 index 0000000000..4c605f38ae --- /dev/null +++ b/common/pretty_reqwest_error/src/lib.rs @@ -0,0 +1,62 @@ +use sensitive_url::SensitiveUrl; +use std::error::Error as StdError; +use std::fmt; + +pub struct PrettyReqwestError(reqwest::Error); + +impl PrettyReqwestError { + pub fn inner(&self) -> &reqwest::Error { + &self.0 + } +} + +impl fmt::Debug for PrettyReqwestError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(url) = self.0.url() { + if let Ok(url) = SensitiveUrl::new(url.clone()) { + write!(f, "url: {}", url)?; + } else { + write!(f, "url: unable_to_parse")?; + }; + } + + let kind = if self.0.is_builder() { + "builder" + } else if self.0.is_redirect() { + "redirect" + } else if self.0.is_status() { + "status" + } else if self.0.is_timeout() { + "timeout" + } else if self.0.is_request() { + "request" + } else if self.0.is_connect() { + "connect" + } else if self.0.is_body() { + "body" + } else if self.0.is_decode() { + "decode" + } else { + "unknown" + }; + write!(f, ", kind: {}", kind)?; + + if let Some(status) = self.0.status() { + write!(f, ", status_code: {}", status)?; + } + + if let Some(ref source) = self.0.source() { + write!(f, ", detail: {}", source)?; + } else { + write!(f, ", source: unknown")?; + } + + Ok(()) + } +} + +impl From for PrettyReqwestError { + fn from(inner: reqwest::Error) -> Self { + Self(inner) + } +} diff --git a/common/sensitive_url/src/lib.rs b/common/sensitive_url/src/lib.rs index b6705eb602..b6068a2dca 100644 --- a/common/sensitive_url/src/lib.rs +++ b/common/sensitive_url/src/lib.rs @@ -75,7 +75,7 @@ impl SensitiveUrl { SensitiveUrl::new(surl) } - fn new(full: Url) -> Result { + pub fn new(full: Url) -> Result { let mut redacted = full.clone(); redacted .path_segments_mut() From ead4e60a76a47ca9221d3fc383a15131a77f8596 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 27 Jun 2023 01:06:51 +0000 Subject: [PATCH 56/63] Schedule Capella for Gnosis chain (#4433) ## Issue Addressed Closes #4422 Implements https://github.com/gnosischain/configs/pull/12 ## Proposed Changes Schedule the Capella fork for Gnosis chain at epoch 648704, August 1st 2023 11:34:20 UTC. --- .../built_in_network_configs/gnosis/config.yaml | 2 +- consensus/types/src/chain_spec.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 95ca9d0108..0fdc159ec2 100644 --- a/common/eth2_network_config/built_in_network_configs/gnosis/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/gnosis/config.yaml @@ -38,7 +38,7 @@ BELLATRIX_FORK_VERSION: 0x02000064 BELLATRIX_FORK_EPOCH: 385536 # Capella CAPELLA_FORK_VERSION: 0x03000064 -CAPELLA_FORK_EPOCH: 18446744073709551615 +CAPELLA_FORK_EPOCH: 648704 # Sharding SHARDING_FORK_VERSION: 0x03000064 SHARDING_FORK_EPOCH: 18446744073709551615 diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index 5253dcc4b0..5957182230 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -828,7 +828,7 @@ impl ChainSpec { * Capella hard fork params */ capella_fork_version: [0x03, 0x00, 0x00, 0x64], - capella_fork_epoch: None, + capella_fork_epoch: Some(Epoch::new(648704)), max_validators_per_withdrawals_sweep: 8192, /* From 23b06aa51ee2ab9c13a90c7d5e92f8e085b3eafc Mon Sep 17 00:00:00 2001 From: int88 Date: Thu, 29 Jun 2023 09:39:15 +0000 Subject: [PATCH 57/63] avoid relocking head during builder health check (#4323) ## Issue Addressed #4314 ## Proposed Changes avoid relocking head during builder health check ## Additional Info NA --- beacon_node/beacon_chain/src/beacon_chain.rs | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index ceda7222e6..4aea7bc55f 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -5699,13 +5699,9 @@ impl BeaconChain { /// Since we are likely calling this during the slot we are going to propose in, don't take into /// account the current slot when accounting for skips. pub fn is_healthy(&self, parent_root: &Hash256) -> Result { + let cached_head = self.canonical_head.cached_head(); // Check if the merge has been finalized. - if let Some(finalized_hash) = self - .canonical_head - .cached_head() - .forkchoice_update_parameters() - .finalized_hash - { + if let Some(finalized_hash) = cached_head.forkchoice_update_parameters().finalized_hash { if ExecutionBlockHash::zero() == finalized_hash { return Ok(ChainHealth::PreMerge); } @@ -5732,17 +5728,13 @@ impl BeaconChain { // Check slots at the head of the chain. let prev_slot = current_slot.saturating_sub(Slot::new(1)); - let head_skips = prev_slot.saturating_sub(self.canonical_head.cached_head().head_slot()); + let head_skips = prev_slot.saturating_sub(cached_head.head_slot()); let head_skips_check = head_skips.as_usize() <= self.config.builder_fallback_skips; // Check if finalization is advancing. let current_epoch = current_slot.epoch(T::EthSpec::slots_per_epoch()); - let epochs_since_finalization = current_epoch.saturating_sub( - self.canonical_head - .cached_head() - .finalized_checkpoint() - .epoch, - ); + let epochs_since_finalization = + current_epoch.saturating_sub(cached_head.finalized_checkpoint().epoch); let finalization_check = epochs_since_finalization.as_usize() <= self.config.builder_fallback_epochs_since_finalization; From 1aff082eeaf7ccb66e06153a6db24bb530a4e2af Mon Sep 17 00:00:00 2001 From: Jack McPherson Date: Thu, 29 Jun 2023 12:02:38 +0000 Subject: [PATCH 58/63] Add broadcast validation routes to Beacon Node HTTP API (#4316) ## Issue Addressed - #4293 - #4264 ## Proposed Changes *Changes largely follow those suggested in the main issue*. - Add new routes to HTTP API - `post_beacon_blocks_v2` - `post_blinded_beacon_blocks_v2` - Add new routes to `BeaconNodeHttpClient` - `post_beacon_blocks_v2` - `post_blinded_beacon_blocks_v2` - Define new Eth2 common types - `BroadcastValidation`, enum representing the level of validation to apply to blocks prior to broadcast - `BroadcastValidationQuery`, the corresponding HTTP query string type for the above type - ~~Define `_checked` variants of both `publish_block` and `publish_blinded_block` that enforce a validation level at a type level~~ - Add interactive tests to the `bn_http_api_tests` test target covering each validation level (to their own test module, `broadcast_validation_tests`) - `beacon/blocks` - `broadcast_validation=gossip` - Invalid (400) - Full Pass (200) - Partial Pass (202) - `broadcast_validation=consensus` - Invalid (400) - Only gossip (400) - Only consensus pass (i.e., equivocates) (200) - Full pass (200) - `broadcast_validation=consensus_and_equivocation` - Invalid (400) - Invalid due to early equivocation (400) - Only gossip (400) - Only consensus (400) - Pass (200) - `beacon/blinded_blocks` - `broadcast_validation=gossip` - Invalid (400) - Full Pass (200) - Partial Pass (202) - `broadcast_validation=consensus` - Invalid (400) - Only gossip (400) - ~~Only consensus pass (i.e., equivocates) (200)~~ - Full pass (200) - `broadcast_validation=consensus_and_equivocation` - Invalid (400) - Invalid due to early equivocation (400) - Only gossip (400) - Only consensus (400) - Pass (200) - Add a new trait, `IntoGossipVerifiedBlock`, which allows type-level guarantees to be made as to gossip validity - Modify the structure of the `ObservedBlockProducers` cache from a `(slot, validator_index)` mapping to a `((slot, validator_index), block_root)` mapping - Modify `ObservedBlockProducers::proposer_has_been_observed` to return a `SeenBlock` rather than a boolean on success - Punish gossip peer (low) for submitting equivocating blocks - Rename `BlockError::SlashablePublish` to `BlockError::SlashableProposal` ## Additional Info This PR contains changes that directly modify how blocks are verified within the client. For more context, consult [comments in-thread](https://github.com/sigp/lighthouse/pull/4316#discussion_r1234724202). Co-authored-by: Michael Sproul --- beacon_node/beacon_chain/src/beacon_chain.rs | 5 +- .../beacon_chain/src/block_verification.rs | 81 +- beacon_node/beacon_chain/src/builder.rs | 1 - beacon_node/beacon_chain/src/errors.rs | 1 + beacon_node/beacon_chain/src/lib.rs | 2 +- .../src/observed_block_producers.rs | 221 ++- beacon_node/beacon_chain/src/test_utils.rs | 17 +- .../beacon_chain/tests/block_verification.rs | 19 +- .../tests/payload_invalidation.rs | 14 +- beacon_node/beacon_chain/tests/store_tests.rs | 1 + beacon_node/beacon_chain/tests/tests.rs | 1 + beacon_node/http_api/src/lib.rs | 101 +- beacon_node/http_api/src/publish_blocks.rs | 201 ++- .../tests/broadcast_validation_tests.rs | 1270 +++++++++++++++++ beacon_node/http_api/tests/main.rs | 1 + beacon_node/http_api/tests/tests.rs | 23 +- .../beacon_processor/worker/gossip_methods.rs | 22 +- .../beacon_processor/worker/sync_methods.rs | 24 +- common/eth2/src/lib.rs | 90 ++ common/eth2/src/types.rs | 46 +- consensus/types/src/beacon_block_body.rs | 2 +- testing/ef_tests/src/cases/fork_choice.rs | 1 + 22 files changed, 1963 insertions(+), 181 deletions(-) create mode 100644 beacon_node/http_api/tests/broadcast_validation_tests.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 4aea7bc55f..772e4c1529 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -2578,6 +2578,7 @@ impl BeaconChain { signature_verified_block.block_root(), signature_verified_block, notify_execution_layer, + || Ok(()), ) .await { @@ -2666,6 +2667,7 @@ impl BeaconChain { block_root: Hash256, unverified_block: B, notify_execution_layer: NotifyExecutionLayer, + publish_fn: impl FnOnce() -> Result<(), BlockError> + Send + 'static, ) -> Result> { // Start the Prometheus timer. let _full_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_TIMES); @@ -2684,6 +2686,7 @@ impl BeaconChain { &chain, notify_execution_layer, )?; + publish_fn()?; chain .import_execution_pending_block(execution_pending) .await @@ -2725,7 +2728,7 @@ impl BeaconChain { } // The block failed verification. Err(other) => { - trace!( + debug!( self.log, "Beacon block rejected"; "reason" => other.to_string(), diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 3cb8fbdb52..492f492521 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -52,6 +52,7 @@ use crate::execution_payload::{ is_optimistic_candidate_block, validate_execution_payload_for_gossip, validate_merge_block, AllowOptimisticImport, NotifyExecutionLayer, PayloadNotifier, }; +use crate::observed_block_producers::SeenBlock; use crate::snapshot_cache::PreProcessingSnapshot; use crate::validator_monitor::HISTORIC_EPOCHS as VALIDATOR_MONITOR_HISTORIC_EPOCHS; use crate::validator_pubkey_cache::ValidatorPubkeyCache; @@ -181,13 +182,6 @@ pub enum BlockError { /// /// The block is valid and we have already imported a block with this hash. BlockIsAlreadyKnown, - /// A block for this proposer and slot has already been observed. - /// - /// ## Peer scoring - /// - /// The `proposer` has already proposed a block at this slot. The existing block may or may not - /// be equal to the given block. - RepeatProposal { proposer: u64, slot: Slot }, /// The block slot exceeds the MAXIMUM_BLOCK_SLOT_NUMBER. /// /// ## Peer scoring @@ -283,6 +277,13 @@ pub enum BlockError { /// problems to worry about than losing peers, and we're doing the network a favour by /// disconnecting. ParentExecutionPayloadInvalid { parent_root: Hash256 }, + /// The block is a slashable equivocation from the proposer. + /// + /// ## Peer scoring + /// + /// Honest peers shouldn't forward more than 1 equivocating block from the same proposer, so + /// we penalise them with a mid-tolerance error. + Slashable, } /// Returned when block validation failed due to some issue verifying @@ -631,6 +632,40 @@ pub struct ExecutionPendingBlock { pub payload_verification_handle: PayloadVerificationHandle, } +pub trait IntoGossipVerifiedBlock: Sized { + fn into_gossip_verified_block( + self, + chain: &BeaconChain, + ) -> Result, BlockError>; + fn inner(&self) -> Arc>; +} + +impl IntoGossipVerifiedBlock for GossipVerifiedBlock { + fn into_gossip_verified_block( + self, + _chain: &BeaconChain, + ) -> Result, BlockError> { + Ok(self) + } + + fn inner(&self) -> Arc> { + self.block.clone() + } +} + +impl IntoGossipVerifiedBlock for Arc> { + fn into_gossip_verified_block( + self, + chain: &BeaconChain, + ) -> Result, BlockError> { + GossipVerifiedBlock::new(self, chain) + } + + fn inner(&self) -> Arc> { + self.clone() + } +} + /// Implemented on types that can be converted into a `ExecutionPendingBlock`. /// /// Used to allow functions to accept blocks at various stages of verification. @@ -727,19 +762,6 @@ impl GossipVerifiedBlock { return Err(BlockError::BlockIsAlreadyKnown); } - // Check that we have not already received a block with a valid signature for this slot. - if chain - .observed_block_producers - .read() - .proposer_has_been_observed(block.message()) - .map_err(|e| BlockError::BeaconChainError(e.into()))? - { - return Err(BlockError::RepeatProposal { - proposer: block.message().proposer_index(), - slot: block.slot(), - }); - } - // Do not process a block that doesn't descend from the finalized root. // // We check this *before* we load the parent so that we can return a more detailed error. @@ -855,17 +877,16 @@ impl GossipVerifiedBlock { // // It's important to double-check that the proposer still hasn't been observed so we don't // have a race-condition when verifying two blocks simultaneously. - if chain + match chain .observed_block_producers .write() - .observe_proposer(block.message()) + .observe_proposal(block_root, block.message()) .map_err(|e| BlockError::BeaconChainError(e.into()))? { - return Err(BlockError::RepeatProposal { - proposer: block.message().proposer_index(), - slot: block.slot(), - }); - } + SeenBlock::Slashable => return Err(BlockError::Slashable), + SeenBlock::Duplicate => return Err(BlockError::BlockIsAlreadyKnown), + SeenBlock::UniqueNonSlashable => {} + }; if block.message().proposer_index() != expected_proposer as u64 { return Err(BlockError::IncorrectBlockProposer { @@ -1101,6 +1122,12 @@ impl ExecutionPendingBlock { chain: &Arc>, notify_execution_layer: NotifyExecutionLayer, ) -> Result> { + chain + .observed_block_producers + .write() + .observe_proposal(block_root, block.message()) + .map_err(|e| BlockError::BeaconChainError(e.into()))?; + if let Some(parent) = chain .canonical_head .fork_choice_read_lock() diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 84148fbfb1..9bb3939632 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -826,7 +826,6 @@ where observed_sync_aggregators: <_>::default(), // TODO: allow for persisting and loading the pool from disk. observed_block_producers: <_>::default(), - // TODO: allow for persisting and loading the pool from disk. observed_voluntary_exits: <_>::default(), observed_proposer_slashings: <_>::default(), observed_attester_slashings: <_>::default(), diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index e789b54a21..50bcf42653 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -213,6 +213,7 @@ pub enum BeaconChainError { BlsToExecutionConflictsWithPool, InconsistentFork(InconsistentFork), ProposerHeadForkChoiceError(fork_choice::Error), + UnableToPublish, } easy_from_to!(SlotProcessingError, BeaconChainError); diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index d672c16828..c5cf74e179 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -64,7 +64,7 @@ pub use attestation_verification::Error as AttestationError; pub use beacon_fork_choice_store::{BeaconForkChoiceStore, Error as ForkChoiceStoreError}; pub use block_verification::{ get_block_root, BlockError, ExecutionPayloadError, GossipVerifiedBlock, - IntoExecutionPendingBlock, + IntoExecutionPendingBlock, IntoGossipVerifiedBlock, }; pub use canonical_head::{CachedHead, CanonicalHead, CanonicalHeadRwLock}; pub use eth1_chain::{Eth1Chain, Eth1ChainBackend}; diff --git a/beacon_node/beacon_chain/src/observed_block_producers.rs b/beacon_node/beacon_chain/src/observed_block_producers.rs index b5995121b9..f76fc53796 100644 --- a/beacon_node/beacon_chain/src/observed_block_producers.rs +++ b/beacon_node/beacon_chain/src/observed_block_producers.rs @@ -1,9 +1,10 @@ //! Provides the `ObservedBlockProducers` struct which allows for rejecting gossip blocks from //! validators that have already produced a block. +use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet}; use std::marker::PhantomData; -use types::{BeaconBlockRef, Epoch, EthSpec, Slot, Unsigned}; +use types::{BeaconBlockRef, Epoch, EthSpec, Hash256, Slot, Unsigned}; #[derive(Debug, PartialEq)] pub enum Error { @@ -14,6 +15,12 @@ pub enum Error { ValidatorIndexTooHigh(u64), } +#[derive(Eq, Hash, PartialEq, Debug, Default)] +struct ProposalKey { + slot: Slot, + proposer: u64, +} + /// Maintains a cache of observed `(block.slot, block.proposer)`. /// /// The cache supports pruning based upon the finalized epoch. It does not automatically prune, you @@ -27,7 +34,7 @@ pub enum Error { /// known_distinct_shufflings` which is much smaller. pub struct ObservedBlockProducers { finalized_slot: Slot, - items: HashMap>, + items: HashMap>, _phantom: PhantomData, } @@ -42,6 +49,24 @@ impl Default for ObservedBlockProducers { } } +pub enum SeenBlock { + Duplicate, + Slashable, + UniqueNonSlashable, +} + +impl SeenBlock { + pub fn proposer_previously_observed(self) -> bool { + match self { + Self::Duplicate | Self::Slashable => true, + Self::UniqueNonSlashable => false, + } + } + pub fn is_slashable(&self) -> bool { + matches!(self, Self::Slashable) + } +} + impl ObservedBlockProducers { /// Observe that the `block` was produced by `block.proposer_index` at `block.slot`. This will /// update `self` so future calls to it indicate that this block is known. @@ -52,16 +77,44 @@ impl ObservedBlockProducers { /// /// - `block.proposer_index` is greater than `VALIDATOR_REGISTRY_LIMIT`. /// - `block.slot` is equal to or less than the latest pruned `finalized_slot`. - pub fn observe_proposer(&mut self, block: BeaconBlockRef<'_, E>) -> Result { + pub fn observe_proposal( + &mut self, + block_root: Hash256, + block: BeaconBlockRef<'_, E>, + ) -> Result { self.sanitize_block(block)?; - let did_not_exist = self - .items - .entry(block.slot()) - .or_insert_with(|| HashSet::with_capacity(E::SlotsPerEpoch::to_usize())) - .insert(block.proposer_index()); + let key = ProposalKey { + slot: block.slot(), + proposer: block.proposer_index(), + }; - Ok(!did_not_exist) + let entry = self.items.entry(key); + + let slashable_proposal = match entry { + Entry::Occupied(mut occupied_entry) => { + let block_roots = occupied_entry.get_mut(); + let newly_inserted = block_roots.insert(block_root); + + let is_equivocation = block_roots.len() > 1; + + if is_equivocation { + SeenBlock::Slashable + } else if !newly_inserted { + SeenBlock::Duplicate + } else { + SeenBlock::UniqueNonSlashable + } + } + Entry::Vacant(vacant_entry) => { + let block_roots = HashSet::from([block_root]); + vacant_entry.insert(block_roots); + + SeenBlock::UniqueNonSlashable + } + }; + + Ok(slashable_proposal) } /// Returns `Ok(true)` if the `block` has been observed before, `Ok(false)` if not. Does not @@ -72,15 +125,33 @@ impl ObservedBlockProducers { /// /// - `block.proposer_index` is greater than `VALIDATOR_REGISTRY_LIMIT`. /// - `block.slot` is equal to or less than the latest pruned `finalized_slot`. - pub fn proposer_has_been_observed(&self, block: BeaconBlockRef<'_, E>) -> Result { + pub fn proposer_has_been_observed( + &self, + block: BeaconBlockRef<'_, E>, + block_root: Hash256, + ) -> Result { self.sanitize_block(block)?; - let exists = self - .items - .get(&block.slot()) - .map_or(false, |set| set.contains(&block.proposer_index())); + let key = ProposalKey { + slot: block.slot(), + proposer: block.proposer_index(), + }; - Ok(exists) + if let Some(block_roots) = self.items.get(&key) { + let block_already_known = block_roots.contains(&block_root); + let no_prev_known_blocks = + block_roots.difference(&HashSet::from([block_root])).count() == 0; + + if !no_prev_known_blocks { + Ok(SeenBlock::Slashable) + } else if block_already_known { + Ok(SeenBlock::Duplicate) + } else { + Ok(SeenBlock::UniqueNonSlashable) + } + } else { + Ok(SeenBlock::UniqueNonSlashable) + } } /// Returns `Ok(())` if the given `block` is sane. @@ -112,15 +183,15 @@ impl ObservedBlockProducers { } self.finalized_slot = finalized_slot; - self.items.retain(|slot, _set| *slot > finalized_slot); + self.items.retain(|key, _| key.slot > finalized_slot); } /// Returns `true` if the given `validator_index` has been stored in `self` at `epoch`. /// /// This is useful for doppelganger detection. pub fn index_seen_at_epoch(&self, validator_index: u64, epoch: Epoch) -> bool { - self.items.iter().any(|(slot, producers)| { - slot.epoch(E::slots_per_epoch()) == epoch && producers.contains(&validator_index) + self.items.iter().any(|(key, _)| { + key.slot.epoch(E::slots_per_epoch()) == epoch && key.proposer == validator_index }) } } @@ -148,9 +219,12 @@ mod tests { // Slot 0, proposer 0 let block_a = get_block(0, 0); + let block_root = block_a.canonical_root(); assert_eq!( - cache.observe_proposer(block_a.to_ref()), + cache + .observe_proposal(block_root, block_a.to_ref()) + .map(SeenBlock::proposer_previously_observed), Ok(false), "can observe proposer, indicates proposer unobserved" ); @@ -164,7 +238,10 @@ mod tests { assert_eq!( cache .items - .get(&Slot::new(0)) + .get(&ProposalKey { + slot: Slot::new(0), + proposer: 0 + }) .expect("slot zero should be present") .len(), 1, @@ -182,7 +259,10 @@ mod tests { assert_eq!( cache .items - .get(&Slot::new(0)) + .get(&ProposalKey { + slot: Slot::new(0), + proposer: 0 + }) .expect("slot zero should be present") .len(), 1, @@ -207,9 +287,12 @@ mod tests { // First slot of finalized epoch, proposer 0 let block_b = get_block(E::slots_per_epoch(), 0); + let block_root_b = block_b.canonical_root(); assert_eq!( - cache.observe_proposer(block_b.to_ref()), + cache + .observe_proposal(block_root_b, block_b.to_ref()) + .map(SeenBlock::proposer_previously_observed), Err(Error::FinalizedBlock { slot: E::slots_per_epoch().into(), finalized_slot: E::slots_per_epoch().into(), @@ -229,7 +312,9 @@ mod tests { let block_b = get_block(three_epochs, 0); assert_eq!( - cache.observe_proposer(block_b.to_ref()), + cache + .observe_proposal(block_root_b, block_b.to_ref()) + .map(SeenBlock::proposer_previously_observed), Ok(false), "can insert non-finalized block" ); @@ -238,7 +323,10 @@ mod tests { assert_eq!( cache .items - .get(&Slot::new(three_epochs)) + .get(&ProposalKey { + slot: Slot::new(three_epochs), + proposer: 0 + }) .expect("the three epochs slot should be present") .len(), 1, @@ -262,7 +350,10 @@ mod tests { assert_eq!( cache .items - .get(&Slot::new(three_epochs)) + .get(&ProposalKey { + slot: Slot::new(three_epochs), + proposer: 0 + }) .expect("the three epochs slot should be present") .len(), 1, @@ -276,24 +367,33 @@ mod tests { // Slot 0, proposer 0 let block_a = get_block(0, 0); + let block_root_a = block_a.canonical_root(); assert_eq!( - cache.proposer_has_been_observed(block_a.to_ref()), + cache + .proposer_has_been_observed(block_a.to_ref(), block_a.canonical_root()) + .map(|x| x.proposer_previously_observed()), Ok(false), "no observation in empty cache" ); assert_eq!( - cache.observe_proposer(block_a.to_ref()), + cache + .observe_proposal(block_root_a, block_a.to_ref()) + .map(SeenBlock::proposer_previously_observed), Ok(false), "can observe proposer, indicates proposer unobserved" ); assert_eq!( - cache.proposer_has_been_observed(block_a.to_ref()), + cache + .proposer_has_been_observed(block_a.to_ref(), block_a.canonical_root()) + .map(|x| x.proposer_previously_observed()), Ok(true), "observed block is indicated as true" ); assert_eq!( - cache.observe_proposer(block_a.to_ref()), + cache + .observe_proposal(block_root_a, block_a.to_ref()) + .map(SeenBlock::proposer_previously_observed), Ok(true), "observing again indicates true" ); @@ -303,7 +403,10 @@ mod tests { assert_eq!( cache .items - .get(&Slot::new(0)) + .get(&ProposalKey { + slot: Slot::new(0), + proposer: 0 + }) .expect("slot zero should be present") .len(), 1, @@ -312,24 +415,33 @@ mod tests { // Slot 1, proposer 0 let block_b = get_block(1, 0); + let block_root_b = block_b.canonical_root(); assert_eq!( - cache.proposer_has_been_observed(block_b.to_ref()), + cache + .proposer_has_been_observed(block_b.to_ref(), block_b.canonical_root()) + .map(|x| x.proposer_previously_observed()), Ok(false), "no observation for new slot" ); assert_eq!( - cache.observe_proposer(block_b.to_ref()), + cache + .observe_proposal(block_root_b, block_b.to_ref()) + .map(SeenBlock::proposer_previously_observed), Ok(false), "can observe proposer for new slot, indicates proposer unobserved" ); assert_eq!( - cache.proposer_has_been_observed(block_b.to_ref()), + cache + .proposer_has_been_observed(block_b.to_ref(), block_b.canonical_root()) + .map(|x| x.proposer_previously_observed()), Ok(true), "observed block in slot 1 is indicated as true" ); assert_eq!( - cache.observe_proposer(block_b.to_ref()), + cache + .observe_proposal(block_root_b, block_b.to_ref()) + .map(SeenBlock::proposer_previously_observed), Ok(true), "observing slot 1 again indicates true" ); @@ -339,7 +451,10 @@ mod tests { assert_eq!( cache .items - .get(&Slot::new(0)) + .get(&ProposalKey { + slot: Slot::new(0), + proposer: 0 + }) .expect("slot zero should be present") .len(), 1, @@ -348,7 +463,10 @@ mod tests { assert_eq!( cache .items - .get(&Slot::new(1)) + .get(&ProposalKey { + slot: Slot::new(1), + proposer: 0 + }) .expect("slot zero should be present") .len(), 1, @@ -357,45 +475,54 @@ mod tests { // Slot 0, proposer 1 let block_c = get_block(0, 1); + let block_root_c = block_c.canonical_root(); assert_eq!( - cache.proposer_has_been_observed(block_c.to_ref()), + cache + .proposer_has_been_observed(block_c.to_ref(), block_c.canonical_root()) + .map(|x| x.proposer_previously_observed()), Ok(false), "no observation for new proposer" ); assert_eq!( - cache.observe_proposer(block_c.to_ref()), + cache + .observe_proposal(block_root_c, block_c.to_ref()) + .map(SeenBlock::proposer_previously_observed), Ok(false), "can observe new proposer, indicates proposer unobserved" ); assert_eq!( - cache.proposer_has_been_observed(block_c.to_ref()), + cache + .proposer_has_been_observed(block_c.to_ref(), block_c.canonical_root()) + .map(|x| x.proposer_previously_observed()), Ok(true), "observed new proposer block is indicated as true" ); assert_eq!( - cache.observe_proposer(block_c.to_ref()), + cache + .observe_proposal(block_root_c, block_c.to_ref()) + .map(SeenBlock::proposer_previously_observed), Ok(true), "observing new proposer again indicates true" ); assert_eq!(cache.finalized_slot, 0, "finalized slot is zero"); - assert_eq!(cache.items.len(), 2, "two slots should be present"); + assert_eq!(cache.items.len(), 3, "three slots should be present"); assert_eq!( cache .items - .get(&Slot::new(0)) - .expect("slot zero should be present") - .len(), + .iter() + .filter(|(k, _)| k.slot == cache.finalized_slot) + .count(), 2, "two proposers should be present in slot 0" ); assert_eq!( cache .items - .get(&Slot::new(1)) - .expect("slot zero should be present") - .len(), + .iter() + .filter(|(k, _)| k.slot == Slot::new(1)) + .count(), 1, "only one proposer should be present in slot 1" ); diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 55ea016fbd..21f7248cee 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -733,6 +733,15 @@ where state.get_block_root(slot).unwrap() == state.get_block_root(slot - 1).unwrap() } + pub async fn make_blinded_block( + &self, + state: BeaconState, + slot: Slot, + ) -> (SignedBlindedBeaconBlock, BeaconState) { + let (unblinded, new_state) = self.make_block(state, slot).await; + (unblinded.into(), new_state) + } + /// Returns a newly created block, signed by the proposer for the given slot. pub async fn make_block( &self, @@ -1692,7 +1701,12 @@ where self.set_current_slot(slot); let block_hash: SignedBeaconBlockHash = self .chain - .process_block(block_root, Arc::new(block), NotifyExecutionLayer::Yes) + .process_block( + block_root, + Arc::new(block), + NotifyExecutionLayer::Yes, + || Ok(()), + ) .await? .into(); self.chain.recompute_head_at_current_slot().await; @@ -1709,6 +1723,7 @@ where block.canonical_root(), Arc::new(block), NotifyExecutionLayer::Yes, + || Ok(()), ) .await? .into(); diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index a88931367f..75b00b2b44 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -351,6 +351,7 @@ async fn assert_invalid_signature( snapshots[block_index].beacon_block.canonical_root(), snapshots[block_index].beacon_block.clone(), NotifyExecutionLayer::Yes, + || Ok(()), ) .await; assert!( @@ -415,6 +416,7 @@ async fn invalid_signature_gossip_block() { signed_block.canonical_root(), Arc::new(signed_block), NotifyExecutionLayer::Yes, + || Ok(()), ) .await, Err(BlockError::InvalidSignature) @@ -727,6 +729,7 @@ async fn block_gossip_verification() { gossip_verified.block_root, gossip_verified, NotifyExecutionLayer::Yes, + || Ok(()), ) .await .expect("should import valid gossip verified block"); @@ -923,11 +926,7 @@ async fn block_gossip_verification() { assert!( matches!( unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(block.clone())).await), - BlockError::RepeatProposal { - proposer, - slot, - } - if proposer == other_proposer && slot == block.message().slot() + BlockError::BlockIsAlreadyKnown, ), "should register any valid signature against the proposer, even if the block failed later verification" ); @@ -956,11 +955,7 @@ async fn block_gossip_verification() { .await .err() .expect("should error when processing known block"), - BlockError::RepeatProposal { - proposer, - slot, - } - if proposer == block.message().proposer_index() && slot == block.message().slot() + BlockError::BlockIsAlreadyKnown ), "the second proposal by this validator should be rejected" ); @@ -998,6 +993,7 @@ async fn verify_block_for_gossip_slashing_detection() { verified_block.block_root, verified_block, NotifyExecutionLayer::Yes, + || Ok(()), ) .await .unwrap(); @@ -1037,6 +1033,7 @@ async fn verify_block_for_gossip_doppelganger_detection() { verified_block.block_root, verified_block, NotifyExecutionLayer::Yes, + || Ok(()), ) .await .unwrap(); @@ -1184,6 +1181,7 @@ async fn add_base_block_to_altair_chain() { base_block.canonical_root(), Arc::new(base_block.clone()), NotifyExecutionLayer::Yes, + || Ok(()), ) .await .err() @@ -1318,6 +1316,7 @@ async fn add_altair_block_to_base_chain() { altair_block.canonical_root(), Arc::new(altair_block.clone()), NotifyExecutionLayer::Yes, + || Ok(()), ) .await .err() diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index c39bdeaf36..018defd2f0 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -697,6 +697,7 @@ async fn invalidates_all_descendants() { fork_block.canonical_root(), Arc::new(fork_block), NotifyExecutionLayer::Yes, + || Ok(()), ) .await .unwrap(); @@ -793,6 +794,7 @@ async fn switches_heads() { fork_block.canonical_root(), Arc::new(fork_block), NotifyExecutionLayer::Yes, + || Ok(()), ) .await .unwrap(); @@ -1046,7 +1048,9 @@ async fn invalid_parent() { // Ensure the block built atop an invalid payload is invalid for import. assert!(matches!( - rig.harness.chain.process_block(block.canonical_root(), block.clone(), NotifyExecutionLayer::Yes).await, + rig.harness.chain.process_block(block.canonical_root(), block.clone(), NotifyExecutionLayer::Yes, + || Ok(()), + ).await, Err(BlockError::ParentExecutionPayloadInvalid { parent_root: invalid_root }) if invalid_root == parent_root )); @@ -1332,7 +1336,12 @@ async fn build_optimistic_chain( for block in blocks { rig.harness .chain - .process_block(block.canonical_root(), block, NotifyExecutionLayer::Yes) + .process_block( + block.canonical_root(), + block, + NotifyExecutionLayer::Yes, + || Ok(()), + ) .await .unwrap(); } @@ -1892,6 +1901,7 @@ async fn recover_from_invalid_head_by_importing_blocks() { fork_block.canonical_root(), fork_block.clone(), NotifyExecutionLayer::Yes, + || Ok(()), ) .await .unwrap(); diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 0bc7798a7f..2902774825 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -2151,6 +2151,7 @@ async fn weak_subjectivity_sync() { full_block.canonical_root(), Arc::new(full_block), NotifyExecutionLayer::Yes, + || Ok(()), ) .await .unwrap(); diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs index f97f7069dc..c5b2892cbd 100644 --- a/beacon_node/beacon_chain/tests/tests.rs +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -687,6 +687,7 @@ async fn run_skip_slot_test(skip_slots: u64) { harness_a.chain.head_snapshot().beacon_block_root, harness_a.chain.head_snapshot().beacon_block.clone(), NotifyExecutionLayer::Yes, + || Ok(()) ) .await .unwrap(), diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 025b54f326..93bfe524bc 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -31,8 +31,8 @@ use beacon_chain::{ pub use block_id::BlockId; use directory::DEFAULT_ROOT_DIR; use eth2::types::{ - self as api_types, EndpointVersion, ForkChoice, ForkChoiceNode, SkipRandaoVerification, - ValidatorId, ValidatorStatus, + self as api_types, BroadcastValidation, EndpointVersion, ForkChoice, ForkChoiceNode, + SkipRandaoVerification, ValidatorId, ValidatorStatus, }; use lighthouse_network::{types::SyncState, EnrExt, NetworkGlobals, PeerId, PubsubMessage}; use lighthouse_version::version_with_platform; @@ -40,7 +40,9 @@ use logging::SSELoggingComponents; use network::{NetworkMessage, NetworkSenders, ValidatorSubscriptionMessage}; use operation_pool::ReceivedPreCapella; use parking_lot::RwLock; -use publish_blocks::ProvenancedBlock; +pub use publish_blocks::{ + publish_blinded_block, publish_block, reconstruct_block, ProvenancedBlock, +}; use serde::{Deserialize, Serialize}; use slog::{crit, debug, error, info, warn, Logger}; use slot_clock::SlotClock; @@ -324,6 +326,7 @@ pub fn serve( }; let eth_v1 = single_version(V1); + let eth_v2 = single_version(V2); // Create a `warp` filter that provides access to the network globals. let inner_network_globals = ctx.network_globals.clone(); @@ -1222,16 +1225,55 @@ pub fn serve( log: Logger| async move { publish_blocks::publish_block( None, - ProvenancedBlock::Local(block), + ProvenancedBlock::local(block), chain, &network_tx, log, + BroadcastValidation::default(), ) .await .map(|()| warp::reply().into_response()) }, ); + let post_beacon_blocks_v2 = eth_v2 + .and(warp::path("beacon")) + .and(warp::path("blocks")) + .and(warp::query::()) + .and(warp::path::end()) + .and(warp::body::json()) + .and(chain_filter.clone()) + .and(network_tx_filter.clone()) + .and(log_filter.clone()) + .then( + |validation_level: api_types::BroadcastValidationQuery, + block: Arc>, + chain: Arc>, + network_tx: UnboundedSender>, + log: Logger| async move { + match publish_blocks::publish_block( + None, + ProvenancedBlock::local(block), + chain, + &network_tx, + log, + validation_level.broadcast_validation, + ) + .await + { + Ok(()) => warp::reply().into_response(), + Err(e) => match warp_utils::reject::handle_rejection(e).await { + Ok(reply) => reply.into_response(), + Err(_) => warp::reply::with_status( + StatusCode::INTERNAL_SERVER_ERROR, + eth2::StatusCode::INTERNAL_SERVER_ERROR, + ) + .into_response(), + }, + } + }, + ); + /* * beacon/blocks */ @@ -1250,9 +1292,52 @@ pub fn serve( chain: Arc>, network_tx: UnboundedSender>, log: Logger| async move { - publish_blocks::publish_blinded_block(block, chain, &network_tx, log) - .await - .map(|()| warp::reply().into_response()) + publish_blocks::publish_blinded_block( + block, + chain, + &network_tx, + log, + BroadcastValidation::default(), + ) + .await + .map(|()| warp::reply().into_response()) + }, + ); + + let post_beacon_blinded_blocks_v2 = eth_v2 + .and(warp::path("beacon")) + .and(warp::path("blinded_blocks")) + .and(warp::query::()) + .and(warp::path::end()) + .and(warp::body::json()) + .and(chain_filter.clone()) + .and(network_tx_filter.clone()) + .and(log_filter.clone()) + .then( + |validation_level: api_types::BroadcastValidationQuery, + block: SignedBeaconBlock>, + chain: Arc>, + network_tx: UnboundedSender>, + log: Logger| async move { + match publish_blocks::publish_blinded_block( + block, + chain, + &network_tx, + log, + validation_level.broadcast_validation, + ) + .await + { + Ok(()) => warp::reply().into_response(), + Err(e) => match warp_utils::reject::handle_rejection(e).await { + Ok(reply) => reply.into_response(), + Err(_) => warp::reply::with_status( + StatusCode::INTERNAL_SERVER_ERROR, + eth2::StatusCode::INTERNAL_SERVER_ERROR, + ) + .into_response(), + }, + } }, ); @@ -3847,6 +3932,8 @@ pub fn serve( warp::post().and( post_beacon_blocks .uor(post_beacon_blinded_blocks) + .uor(post_beacon_blocks_v2) + .uor(post_beacon_blinded_blocks_v2) .uor(post_beacon_pool_attestations) .uor(post_beacon_pool_attester_slashings) .uor(post_beacon_pool_proposer_slashings) diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 8bcad6ba40..0f2f7b361c 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -1,11 +1,16 @@ use crate::metrics; use beacon_chain::validator_monitor::{get_block_delay_ms, timestamp_now}; -use beacon_chain::{BeaconChain, BeaconChainTypes, BlockError, NotifyExecutionLayer}; +use beacon_chain::{ + BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, IntoGossipVerifiedBlock, + NotifyExecutionLayer, +}; +use eth2::types::BroadcastValidation; use execution_layer::ProvenancedPayload; use lighthouse_network::PubsubMessage; use network::NetworkMessage; use slog::{debug, error, info, warn, Logger}; use slot_clock::SlotClock; +use std::marker::PhantomData; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc::UnboundedSender; @@ -16,45 +21,115 @@ use types::{ }; use warp::Rejection; -pub enum ProvenancedBlock { +pub enum ProvenancedBlock> { /// The payload was built using a local EE. - Local(Arc>>), + Local(B, PhantomData), /// The payload was build using a remote builder (e.g., via a mev-boost /// compatible relay). - Builder(Arc>>), + Builder(B, PhantomData), +} + +impl> ProvenancedBlock { + pub fn local(block: B) -> Self { + Self::Local(block, PhantomData) + } + + pub fn builder(block: B) -> Self { + Self::Builder(block, PhantomData) + } } /// Handles a request from the HTTP API for full blocks. -pub async fn publish_block( +pub async fn publish_block>( block_root: Option, - provenanced_block: ProvenancedBlock, + provenanced_block: ProvenancedBlock, chain: Arc>, network_tx: &UnboundedSender>, log: Logger, + validation_level: BroadcastValidation, ) -> Result<(), Rejection> { let seen_timestamp = timestamp_now(); let (block, is_locally_built_block) = match provenanced_block { - ProvenancedBlock::Local(block) => (block, true), - ProvenancedBlock::Builder(block) => (block, false), + ProvenancedBlock::Local(block, _) => (block, true), + ProvenancedBlock::Builder(block, _) => (block, false), }; - let delay = get_block_delay_ms(seen_timestamp, block.message(), &chain.slot_clock); + let beacon_block = block.inner(); + let delay = get_block_delay_ms(seen_timestamp, beacon_block.message(), &chain.slot_clock); + debug!(log, "Signed block received in HTTP API"; "slot" => beacon_block.slot()); - debug!( - log, - "Signed block published to HTTP API"; - "slot" => block.slot() - ); + /* actually publish a block */ + let publish_block = move |block: Arc>, + sender, + log, + seen_timestamp| { + let publish_timestamp = timestamp_now(); + let publish_delay = publish_timestamp + .checked_sub(seen_timestamp) + .unwrap_or_else(|| Duration::from_secs(0)); - // Send the block, regardless of whether or not it is valid. The API - // specification is very clear that this is the desired behaviour. + info!(log, "Signed block published to network via HTTP API"; "slot" => block.slot(), "publish_delay" => ?publish_delay); - let message = PubsubMessage::BeaconBlock(block.clone()); - crate::publish_pubsub_message(network_tx, message)?; + let message = PubsubMessage::BeaconBlock(block); + crate::publish_pubsub_message(&sender, message) + .map_err(|_| BeaconChainError::UnableToPublish.into()) + }; - let block_root = block_root.unwrap_or_else(|| block.canonical_root()); + /* if we can form a `GossipVerifiedBlock`, we've passed our basic gossip checks */ + let gossip_verified_block = block.into_gossip_verified_block(&chain).map_err(|e| { + warn!(log, "Not publishing block, not gossip verified"; "slot" => beacon_block.slot(), "error" => ?e); + warp_utils::reject::custom_bad_request(e.to_string()) + })?; + + let block_root = block_root.unwrap_or(gossip_verified_block.block_root); + + if let BroadcastValidation::Gossip = validation_level { + publish_block( + beacon_block.clone(), + network_tx.clone(), + log.clone(), + seen_timestamp, + ) + .map_err(|_| warp_utils::reject::custom_server_error("unable to publish".into()))?; + } + + /* only publish if gossip- and consensus-valid and equivocation-free */ + let chain_clone = chain.clone(); + let block_clone = beacon_block.clone(); + let log_clone = log.clone(); + let sender_clone = network_tx.clone(); + + let publish_fn = move || match validation_level { + BroadcastValidation::Gossip => Ok(()), + BroadcastValidation::Consensus => { + publish_block(block_clone, sender_clone, log_clone, seen_timestamp) + } + BroadcastValidation::ConsensusAndEquivocation => { + if chain_clone + .observed_block_producers + .read() + .proposer_has_been_observed(block_clone.message(), block_root) + .map_err(|e| BlockError::BeaconChainError(e.into()))? + .is_slashable() + { + warn!( + log_clone, + "Not publishing equivocating block"; + "slot" => block_clone.slot() + ); + Err(BlockError::Slashable) + } else { + publish_block(block_clone, sender_clone, log_clone, seen_timestamp) + } + } + }; match chain - .process_block(block_root, block.clone(), NotifyExecutionLayer::Yes) + .process_block( + block_root, + gossip_verified_block, + NotifyExecutionLayer::Yes, + publish_fn, + ) .await { Ok(root) => { @@ -63,14 +138,14 @@ pub async fn publish_block( "Valid block from HTTP API"; "block_delay" => ?delay, "root" => format!("{}", root), - "proposer_index" => block.message().proposer_index(), - "slot" => block.slot(), + "proposer_index" => beacon_block.message().proposer_index(), + "slot" => beacon_block.slot(), ); // Notify the validator monitor. chain.validator_monitor.read().register_api_block( seen_timestamp, - block.message(), + beacon_block.message(), root, &chain.slot_clock, ); @@ -83,40 +158,44 @@ pub async fn publish_block( // blocks built with builders we consider the broadcast time to be // when the blinded block is published to the builder. if is_locally_built_block { - late_block_logging(&chain, seen_timestamp, block.message(), root, "local", &log) + late_block_logging( + &chain, + seen_timestamp, + beacon_block.message(), + root, + "local", + &log, + ) } Ok(()) } - Err(BlockError::BlockIsAlreadyKnown) => { - info!( - log, - "Block from HTTP API already known"; - "block" => ?block.canonical_root(), - "slot" => block.slot(), - ); - Ok(()) + Err(BlockError::BeaconChainError(BeaconChainError::UnableToPublish)) => { + Err(warp_utils::reject::custom_server_error( + "unable to publish to network channel".to_string(), + )) } - Err(BlockError::RepeatProposal { proposer, slot }) => { - warn!( - log, - "Block ignored due to repeat proposal"; - "msg" => "this can happen when a VC uses fallback BNs. \ - whilst this is not necessarily an error, it can indicate issues with a BN \ - or between the VC and BN.", - "slot" => slot, - "proposer" => proposer, - ); + Err(BlockError::Slashable) => Err(warp_utils::reject::custom_bad_request( + "proposal for this slot and proposer has already been seen".to_string(), + )), + Err(BlockError::BlockIsAlreadyKnown) => { + info!(log, "Block from HTTP API already known"; "block" => ?block_root); Ok(()) } Err(e) => { - let msg = format!("{:?}", e); - error!( - log, - "Invalid block provided to HTTP API"; - "reason" => &msg - ); - Err(warp_utils::reject::broadcast_without_import(msg)) + if let BroadcastValidation::Gossip = validation_level { + Err(warp_utils::reject::broadcast_without_import(format!("{e}"))) + } else { + let msg = format!("{:?}", e); + error!( + log, + "Invalid block provided to HTTP API"; + "reason" => &msg + ); + Err(warp_utils::reject::custom_bad_request(format!( + "Invalid block: {e}" + ))) + } } } } @@ -128,21 +207,31 @@ pub async fn publish_blinded_block( chain: Arc>, network_tx: &UnboundedSender>, log: Logger, + validation_level: BroadcastValidation, ) -> Result<(), Rejection> { let block_root = block.canonical_root(); - let full_block = reconstruct_block(chain.clone(), block_root, block, log.clone()).await?; - publish_block::(Some(block_root), full_block, chain, network_tx, log).await + let full_block: ProvenancedBlock>> = + reconstruct_block(chain.clone(), block_root, block, log.clone()).await?; + publish_block::( + Some(block_root), + full_block, + chain, + network_tx, + log, + validation_level, + ) + .await } /// Deconstruct the given blinded block, and construct a full block. This attempts to use the /// execution layer's payload cache, and if that misses, attempts a blind block proposal to retrieve /// the full payload. -async fn reconstruct_block( +pub async fn reconstruct_block( chain: Arc>, block_root: Hash256, block: SignedBeaconBlock>, log: Logger, -) -> Result, Rejection> { +) -> Result>>, Rejection> { let full_payload_opt = if let Ok(payload_header) = block.message().body().execution_payload() { let el = chain.execution_layer.as_ref().ok_or_else(|| { warp_utils::reject::custom_server_error("Missing execution layer".to_string()) @@ -208,15 +297,15 @@ async fn reconstruct_block( None => block .try_into_full_block(None) .map(Arc::new) - .map(ProvenancedBlock::Local), + .map(ProvenancedBlock::local), Some(ProvenancedPayload::Local(full_payload)) => block .try_into_full_block(Some(full_payload)) .map(Arc::new) - .map(ProvenancedBlock::Local), + .map(ProvenancedBlock::local), Some(ProvenancedPayload::Builder(full_payload)) => block .try_into_full_block(Some(full_payload)) .map(Arc::new) - .map(ProvenancedBlock::Builder), + .map(ProvenancedBlock::builder), } .ok_or_else(|| { warp_utils::reject::custom_server_error("Unable to add payload to block".to_string()) diff --git a/beacon_node/http_api/tests/broadcast_validation_tests.rs b/beacon_node/http_api/tests/broadcast_validation_tests.rs new file mode 100644 index 0000000000..4819dd99e7 --- /dev/null +++ b/beacon_node/http_api/tests/broadcast_validation_tests.rs @@ -0,0 +1,1270 @@ +use beacon_chain::{ + test_utils::{AttestationStrategy, BlockStrategy}, + GossipVerifiedBlock, +}; +use eth2::types::{BroadcastValidation, SignedBeaconBlock, SignedBlindedBeaconBlock}; +use http_api::test_utils::InteractiveTester; +use http_api::{publish_blinded_block, publish_block, reconstruct_block, ProvenancedBlock}; +use tree_hash::TreeHash; +use types::{Hash256, MainnetEthSpec, Slot}; +use warp::Rejection; +use warp_utils::reject::CustomBadRequest; + +use eth2::reqwest::StatusCode; + +type E = MainnetEthSpec; + +/* + * We have the following test cases, which are duplicated for the blinded variant of the route: + * + * - `broadcast_validation=gossip` + * - Invalid (400) + * - Full Pass (200) + * - Partial Pass (202) + * - `broadcast_validation=consensus` + * - Invalid (400) + * - Only gossip (400) + * - Only consensus pass (i.e., equivocates) (200) + * - Full pass (200) + * - `broadcast_validation=consensus_and_equivocation` + * - Invalid (400) + * - Invalid due to early equivocation (400) + * - Only gossip (400) + * - Only consensus (400) + * - Pass (200) + * + */ + +/// This test checks that a block that is **invalid** from a gossip perspective gets rejected when using `broadcast_validation=gossip`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn gossip_invalid() { + /* this test targets gossip-level validation */ + let validation_level: Option = Some(BroadcastValidation::Gossip); + + // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing + // `validator_count // 32`. + let validator_count = 64; + let num_initial: u64 = 31; + let tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let chain_state_before = tester.harness.get_current_state(); + let slot = chain_state_before.slot() + 1; + + tester.harness.advance_slot(); + + let (block, _): (SignedBeaconBlock, _) = tester + .harness + .make_block_with_modifier(chain_state_before, slot, |b| { + *b.state_root_mut() = Hash256::zero(); + *b.parent_root_mut() = Hash256::zero(); + }) + .await; + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blocks_v2(&block, validation_level) + .await; + assert!(response.is_err()); + + let error_response: eth2::Error = response.err().unwrap(); + + /* mandated by Beacon API spec */ + assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); + + assert!( + matches!(error_response, eth2::Error::ServerMessage(err) if err.message == "BAD_REQUEST: NotFinalizedDescendant { block_parent_root: 0x0000000000000000000000000000000000000000000000000000000000000000 }".to_string()) + ); +} + +/// This test checks that a block that is valid from a gossip perspective is accepted when using `broadcast_validation=gossip`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn gossip_partial_pass() { + /* this test targets gossip-level validation */ + let validation_level: Option = Some(BroadcastValidation::Gossip); + + // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing + // `validator_count // 32`. + let validator_count = 64; + let num_initial: u64 = 31; + let tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let chain_state_before = tester.harness.get_current_state(); + let slot = chain_state_before.slot() + 1; + + tester.harness.advance_slot(); + + let (block, _): (SignedBeaconBlock, _) = tester + .harness + .make_block_with_modifier(chain_state_before, slot, |b| { + *b.state_root_mut() = Hash256::random() + }) + .await; + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blocks_v2(&block, validation_level) + .await; + assert!(response.is_err()); + + let error_response = response.unwrap_err(); + + assert_eq!(error_response.status(), Some(StatusCode::ACCEPTED)); +} + +// This test checks that a block that is valid from both a gossip and consensus perspective is accepted when using `broadcast_validation=gossip`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn gossip_full_pass() { + /* this test targets gossip-level validation */ + let validation_level: Option = Some(BroadcastValidation::Gossip); + + // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing + // `validator_count // 32`. + let validator_count = 64; + let num_initial: u64 = 31; + let tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block, _): (SignedBeaconBlock, _) = tester.harness.make_block(state_a, slot_b).await; + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blocks_v2(&block, validation_level) + .await; + + assert!(response.is_ok()); + assert!(tester + .harness + .chain + .block_is_known_to_fork_choice(&block.canonical_root())); +} + +/// This test checks that a block that is **invalid** from a gossip perspective gets rejected when using `broadcast_validation=consensus`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn consensus_invalid() { + let validation_level: Option = Some(BroadcastValidation::Consensus); + + // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing + // `validator_count // 32`. + let validator_count = 64; + let num_initial: u64 = 31; + let tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let chain_state_before = tester.harness.get_current_state(); + let slot = chain_state_before.slot() + 1; + + tester.harness.advance_slot(); + + let (block, _): (SignedBeaconBlock, _) = tester + .harness + .make_block_with_modifier(chain_state_before, slot, |b| { + *b.state_root_mut() = Hash256::zero(); + *b.parent_root_mut() = Hash256::zero(); + }) + .await; + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blocks_v2(&block, validation_level) + .await; + assert!(response.is_err()); + + let error_response: eth2::Error = response.err().unwrap(); + + /* mandated by Beacon API spec */ + assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); + + assert!( + matches!(error_response, eth2::Error::ServerMessage(err) if err.message == "BAD_REQUEST: NotFinalizedDescendant { block_parent_root: 0x0000000000000000000000000000000000000000000000000000000000000000 }".to_string()) + ); +} + +/// This test checks that a block that is only valid from a gossip perspective is rejected when using `broadcast_validation=consensus`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn consensus_gossip() { + /* this test targets gossip-level validation */ + let validation_level: Option = Some(BroadcastValidation::Consensus); + + // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing + // `validator_count // 32`. + let validator_count = 64; + let num_initial: u64 = 31; + let tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block, _): (SignedBeaconBlock, _) = tester + .harness + .make_block_with_modifier(state_a, slot_b, |b| *b.state_root_mut() = Hash256::zero()) + .await; + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blocks_v2(&block, validation_level) + .await; + assert!(response.is_err()); + + let error_response: eth2::Error = response.err().unwrap(); + + /* mandated by Beacon API spec */ + assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); + + assert!( + matches!(error_response, eth2::Error::ServerMessage(err) if err.message == "BAD_REQUEST: Invalid block: StateRootMismatch { block: 0x0000000000000000000000000000000000000000000000000000000000000000, local: 0xfc675d642ff7a06458eb33c7d7b62a5813e34d1b2bb1aee3e395100b579da026 }".to_string()) + ); +} + +/// This test checks that a block that is valid from both a gossip and consensus perspective, but nonetheless equivocates, is accepted when using `broadcast_validation=consensus`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn consensus_partial_pass_only_consensus() { + /* this test targets gossip-level validation */ + let validation_level: Option = Some(BroadcastValidation::Consensus); + + // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing + // `validator_count // 32`. + let validator_count = 64; + let num_initial: u64 = 31; + let tester = InteractiveTester::::new(None, validator_count).await; + let test_logger = tester.harness.logger().clone(); + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block_a, state_after_a): (SignedBeaconBlock, _) = + tester.harness.make_block(state_a.clone(), slot_b).await; + let (block_b, state_after_b): (SignedBeaconBlock, _) = + tester.harness.make_block(state_a, slot_b).await; + + /* check for `make_block` curios */ + assert_eq!(block_a.state_root(), state_after_a.tree_hash_root()); + assert_eq!(block_b.state_root(), state_after_b.tree_hash_root()); + assert_ne!(block_a.state_root(), block_b.state_root()); + + let gossip_block_b = GossipVerifiedBlock::new(block_b.clone().into(), &tester.harness.chain); + assert!(gossip_block_b.is_ok()); + let gossip_block_a = GossipVerifiedBlock::new(block_a.clone().into(), &tester.harness.chain); + assert!(gossip_block_a.is_err()); + + /* submit `block_b` which should induce equivocation */ + let channel = tokio::sync::mpsc::unbounded_channel(); + + let publication_result: Result<(), Rejection> = publish_block( + None, + ProvenancedBlock::local(gossip_block_b.unwrap()), + tester.harness.chain.clone(), + &channel.0, + test_logger, + validation_level.unwrap(), + ) + .await; + + assert!(publication_result.is_ok()); + assert!(tester + .harness + .chain + .block_is_known_to_fork_choice(&block_b.canonical_root())); +} + +/// This test checks that a block that is valid from both a gossip and consensus perspective is accepted when using `broadcast_validation=consensus`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn consensus_full_pass() { + /* this test targets gossip-level validation */ + let validation_level: Option = Some(BroadcastValidation::Consensus); + + // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing + // `validator_count // 32`. + let validator_count = 64; + let num_initial: u64 = 31; + let tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block, _): (SignedBeaconBlock, _) = tester.harness.make_block(state_a, slot_b).await; + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blocks_v2(&block, validation_level) + .await; + + assert!(response.is_ok()); + assert!(tester + .harness + .chain + .block_is_known_to_fork_choice(&block.canonical_root())); +} + +/// This test checks that a block that is **invalid** from a gossip perspective gets rejected when using `broadcast_validation=consensus_and_equivocation`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn equivocation_invalid() { + /* this test targets gossip-level validation */ + let validation_level: Option = + Some(BroadcastValidation::ConsensusAndEquivocation); + + // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing + // `validator_count // 32`. + let validator_count = 64; + let num_initial: u64 = 31; + let tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let chain_state_before = tester.harness.get_current_state(); + let slot = chain_state_before.slot() + 1; + + tester.harness.advance_slot(); + + let (block, _): (SignedBeaconBlock, _) = tester + .harness + .make_block_with_modifier(chain_state_before, slot, |b| { + *b.state_root_mut() = Hash256::zero(); + *b.parent_root_mut() = Hash256::zero(); + }) + .await; + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blocks_v2(&block, validation_level) + .await; + assert!(response.is_err()); + + let error_response: eth2::Error = response.err().unwrap(); + + /* mandated by Beacon API spec */ + assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); + + assert!( + matches!(error_response, eth2::Error::ServerMessage(err) if err.message == "BAD_REQUEST: NotFinalizedDescendant { block_parent_root: 0x0000000000000000000000000000000000000000000000000000000000000000 }".to_string()) + ); +} + +/// This test checks that a block that is valid from both a gossip and consensus perspective is rejected when using `broadcast_validation=consensus_and_equivocation`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn equivocation_consensus_early_equivocation() { + /* this test targets gossip-level validation */ + let validation_level: Option = + Some(BroadcastValidation::ConsensusAndEquivocation); + + // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing + // `validator_count // 32`. + let validator_count = 64; + let num_initial: u64 = 31; + let tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block_a, state_after_a): (SignedBeaconBlock, _) = + tester.harness.make_block(state_a.clone(), slot_b).await; + let (block_b, state_after_b): (SignedBeaconBlock, _) = + tester.harness.make_block(state_a, slot_b).await; + + /* check for `make_block` curios */ + assert_eq!(block_a.state_root(), state_after_a.tree_hash_root()); + assert_eq!(block_b.state_root(), state_after_b.tree_hash_root()); + assert_ne!(block_a.state_root(), block_b.state_root()); + + /* submit `block_a` as valid */ + assert!(tester + .client + .post_beacon_blocks_v2(&block_a, validation_level) + .await + .is_ok()); + assert!(tester + .harness + .chain + .block_is_known_to_fork_choice(&block_a.canonical_root())); + + /* submit `block_b` which should induce equivocation */ + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blocks_v2(&block_b, validation_level) + .await; + assert!(response.is_err()); + + let error_response: eth2::Error = response.err().unwrap(); + + assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); + + assert!( + matches!(error_response, eth2::Error::ServerMessage(err) if err.message == "BAD_REQUEST: Slashable".to_string()) + ); +} + +/// This test checks that a block that is only valid from a gossip perspective is rejected when using `broadcast_validation=consensus_and_equivocation`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn equivocation_gossip() { + /* this test targets gossip-level validation */ + let validation_level: Option = + Some(BroadcastValidation::ConsensusAndEquivocation); + + // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing + // `validator_count // 32`. + let validator_count = 64; + let num_initial: u64 = 31; + let tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block, _): (SignedBeaconBlock, _) = tester + .harness + .make_block_with_modifier(state_a, slot_b, |b| *b.state_root_mut() = Hash256::zero()) + .await; + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blocks_v2(&block, validation_level) + .await; + assert!(response.is_err()); + + let error_response: eth2::Error = response.err().unwrap(); + + /* mandated by Beacon API spec */ + assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); + + assert!( + matches!(error_response, eth2::Error::ServerMessage(err) if err.message == "BAD_REQUEST: Invalid block: StateRootMismatch { block: 0x0000000000000000000000000000000000000000000000000000000000000000, local: 0xfc675d642ff7a06458eb33c7d7b62a5813e34d1b2bb1aee3e395100b579da026 }".to_string()) + ); +} + +/// This test checks that a block that is valid from both a gossip and consensus perspective but that equivocates **late** is rejected when using `broadcast_validation=consensus_and_equivocation`. +/// +/// This test is unique in that we can't actually test the HTTP API directly, but instead have to hook into the `publish_blocks` code manually. This is in order to handle the late equivocation case. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn equivocation_consensus_late_equivocation() { + /* this test targets gossip-level validation */ + let validation_level: Option = + Some(BroadcastValidation::ConsensusAndEquivocation); + + // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing + // `validator_count // 32`. + let validator_count = 64; + let num_initial: u64 = 31; + let tester = InteractiveTester::::new(None, validator_count).await; + let test_logger = tester.harness.logger().clone(); + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block_a, state_after_a): (SignedBeaconBlock, _) = + tester.harness.make_block(state_a.clone(), slot_b).await; + let (block_b, state_after_b): (SignedBeaconBlock, _) = + tester.harness.make_block(state_a, slot_b).await; + + /* check for `make_block` curios */ + assert_eq!(block_a.state_root(), state_after_a.tree_hash_root()); + assert_eq!(block_b.state_root(), state_after_b.tree_hash_root()); + assert_ne!(block_a.state_root(), block_b.state_root()); + + let gossip_block_b = GossipVerifiedBlock::new(block_b.clone().into(), &tester.harness.chain); + assert!(gossip_block_b.is_ok()); + let gossip_block_a = GossipVerifiedBlock::new(block_a.clone().into(), &tester.harness.chain); + assert!(gossip_block_a.is_err()); + + let channel = tokio::sync::mpsc::unbounded_channel(); + + let publication_result: Result<(), Rejection> = publish_block( + None, + ProvenancedBlock::local(gossip_block_b.unwrap()), + tester.harness.chain, + &channel.0, + test_logger, + validation_level.unwrap(), + ) + .await; + + assert!(publication_result.is_err()); + + let publication_error = publication_result.unwrap_err(); + + assert!(publication_error.find::().is_some()); + + assert_eq!( + *publication_error.find::().unwrap().0, + "proposal for this slot and proposer has already been seen".to_string() + ); +} + +/// This test checks that a block that is valid from both a gossip and consensus perspective (and does not equivocate) is accepted when using `broadcast_validation=consensus_and_equivocation`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn equivocation_full_pass() { + /* this test targets gossip-level validation */ + let validation_level: Option = + Some(BroadcastValidation::ConsensusAndEquivocation); + + // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing + // `validator_count // 32`. + let validator_count = 64; + let num_initial: u64 = 31; + let tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block, _): (SignedBeaconBlock, _) = tester.harness.make_block(state_a, slot_b).await; + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blocks_v2(&block, validation_level) + .await; + + assert!(response.is_ok()); + assert!(tester + .harness + .chain + .block_is_known_to_fork_choice(&block.canonical_root())); +} + +/// This test checks that a block that is **invalid** from a gossip perspective gets rejected when using `broadcast_validation=gossip`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn blinded_gossip_invalid() { + /* this test targets gossip-level validation */ + let validation_level: Option = Some(BroadcastValidation::Gossip); + + // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing + // `validator_count // 32`. + let validator_count = 64; + let num_initial: u64 = 31; + let tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let chain_state_before = tester.harness.get_current_state(); + let slot = chain_state_before.slot() + 1; + + tester.harness.advance_slot(); + + let (block, _): (SignedBeaconBlock, _) = tester + .harness + .make_block_with_modifier(chain_state_before, slot, |b| { + *b.state_root_mut() = Hash256::zero(); + *b.parent_root_mut() = Hash256::zero(); + }) + .await; + + let blinded_block: SignedBlindedBeaconBlock = block.into(); + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blinded_blocks_v2(&blinded_block, validation_level) + .await; + assert!(response.is_err()); + + let error_response: eth2::Error = response.err().unwrap(); + + /* mandated by Beacon API spec */ + assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); + + assert!( + matches!(error_response, eth2::Error::ServerMessage(err) if err.message == "BAD_REQUEST: NotFinalizedDescendant { block_parent_root: 0x0000000000000000000000000000000000000000000000000000000000000000 }".to_string()) + ); +} + +/// This test checks that a block that is valid from a gossip perspective is accepted when using `broadcast_validation=gossip`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn blinded_gossip_partial_pass() { + /* this test targets gossip-level validation */ + let validation_level: Option = Some(BroadcastValidation::Gossip); + + // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing + // `validator_count // 32`. + let validator_count = 64; + let num_initial: u64 = 31; + let tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let chain_state_before = tester.harness.get_current_state(); + let slot = chain_state_before.slot() + 1; + + tester.harness.advance_slot(); + + let (block, _): (SignedBeaconBlock, _) = tester + .harness + .make_block_with_modifier(chain_state_before, slot, |b| { + *b.state_root_mut() = Hash256::zero() + }) + .await; + + let blinded_block: SignedBlindedBeaconBlock = block.into(); + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blinded_blocks_v2(&blinded_block, validation_level) + .await; + assert!(response.is_err()); + + let error_response = response.unwrap_err(); + + assert_eq!(error_response.status(), Some(StatusCode::ACCEPTED)); +} + +// This test checks that a block that is valid from both a gossip and consensus perspective is accepted when using `broadcast_validation=gossip`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn blinded_gossip_full_pass() { + /* this test targets gossip-level validation */ + let validation_level: Option = Some(BroadcastValidation::Gossip); + + // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing + // `validator_count // 32`. + let validator_count = 64; + let num_initial: u64 = 31; + let tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block, _): (SignedBlindedBeaconBlock, _) = + tester.harness.make_blinded_block(state_a, slot_b).await; + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blinded_blocks_v2(&block, validation_level) + .await; + + assert!(response.is_ok()); + assert!(tester + .harness + .chain + .block_is_known_to_fork_choice(&block.canonical_root())); +} + +/// This test checks that a block that is **invalid** from a gossip perspective gets rejected when using `broadcast_validation=consensus`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn blinded_consensus_invalid() { + /* this test targets gossip-level validation */ + let validation_level: Option = Some(BroadcastValidation::Consensus); + + // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing + // `validator_count // 32`. + let validator_count = 64; + let num_initial: u64 = 31; + let tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let chain_state_before = tester.harness.get_current_state(); + let slot = chain_state_before.slot() + 1; + + tester.harness.advance_slot(); + + let (block, _): (SignedBeaconBlock, _) = tester + .harness + .make_block_with_modifier(chain_state_before, slot, |b| { + *b.state_root_mut() = Hash256::zero(); + *b.parent_root_mut() = Hash256::zero(); + }) + .await; + + let blinded_block: SignedBlindedBeaconBlock = block.into(); + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blinded_blocks_v2(&blinded_block, validation_level) + .await; + assert!(response.is_err()); + + let error_response: eth2::Error = response.err().unwrap(); + + /* mandated by Beacon API spec */ + assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); + + assert!( + matches!(error_response, eth2::Error::ServerMessage(err) if err.message == "BAD_REQUEST: NotFinalizedDescendant { block_parent_root: 0x0000000000000000000000000000000000000000000000000000000000000000 }".to_string()) + ); +} + +/// This test checks that a block that is only valid from a gossip perspective is rejected when using `broadcast_validation=consensus`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn blinded_consensus_gossip() { + /* this test targets gossip-level validation */ + let validation_level: Option = Some(BroadcastValidation::Consensus); + + // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing + // `validator_count // 32`. + let validator_count = 64; + let num_initial: u64 = 31; + let tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block, _): (SignedBeaconBlock, _) = tester + .harness + .make_block_with_modifier(state_a, slot_b, |b| *b.state_root_mut() = Hash256::zero()) + .await; + + let blinded_block: SignedBlindedBeaconBlock = block.into(); + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blinded_blocks_v2(&blinded_block, validation_level) + .await; + assert!(response.is_err()); + + let error_response: eth2::Error = response.err().unwrap(); + + /* mandated by Beacon API spec */ + assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); + + assert!( + matches!(error_response, eth2::Error::ServerMessage(err) if err.message == "BAD_REQUEST: Invalid block: StateRootMismatch { block: 0x0000000000000000000000000000000000000000000000000000000000000000, local: 0xfc675d642ff7a06458eb33c7d7b62a5813e34d1b2bb1aee3e395100b579da026 }".to_string()) + ); +} + +/// This test checks that a block that is valid from both a gossip and consensus perspective is accepted when using `broadcast_validation=consensus`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn blinded_consensus_full_pass() { + /* this test targets gossip-level validation */ + let validation_level: Option = Some(BroadcastValidation::Consensus); + + // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing + // `validator_count // 32`. + let validator_count = 64; + let num_initial: u64 = 31; + let tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block, _): (SignedBlindedBeaconBlock, _) = + tester.harness.make_blinded_block(state_a, slot_b).await; + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blinded_blocks_v2(&block, validation_level) + .await; + + assert!(response.is_ok()); + assert!(tester + .harness + .chain + .block_is_known_to_fork_choice(&block.canonical_root())); +} + +/// This test checks that a block that is **invalid** from a gossip perspective gets rejected when using `broadcast_validation=consensus_and_equivocation`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn blinded_equivocation_invalid() { + /* this test targets gossip-level validation */ + let validation_level: Option = + Some(BroadcastValidation::ConsensusAndEquivocation); + + // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing + // `validator_count // 32`. + let validator_count = 64; + let num_initial: u64 = 31; + let tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let chain_state_before = tester.harness.get_current_state(); + let slot = chain_state_before.slot() + 1; + + tester.harness.advance_slot(); + + let (block, _): (SignedBeaconBlock, _) = tester + .harness + .make_block_with_modifier(chain_state_before, slot, |b| { + *b.state_root_mut() = Hash256::zero(); + *b.parent_root_mut() = Hash256::zero(); + }) + .await; + + let blinded_block: SignedBlindedBeaconBlock = block.into(); + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blinded_blocks_v2(&blinded_block, validation_level) + .await; + assert!(response.is_err()); + + let error_response: eth2::Error = response.err().unwrap(); + + /* mandated by Beacon API spec */ + assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); + + assert!( + matches!(error_response, eth2::Error::ServerMessage(err) if err.message == "BAD_REQUEST: NotFinalizedDescendant { block_parent_root: 0x0000000000000000000000000000000000000000000000000000000000000000 }".to_string()) + ); +} + +/// This test checks that a block that is valid from both a gossip and consensus perspective is rejected when using `broadcast_validation=consensus_and_equivocation`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn blinded_equivocation_consensus_early_equivocation() { + /* this test targets gossip-level validation */ + let validation_level: Option = + Some(BroadcastValidation::ConsensusAndEquivocation); + + // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing + // `validator_count // 32`. + let validator_count = 64; + let num_initial: u64 = 31; + let tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block_a, state_after_a): (SignedBlindedBeaconBlock, _) = tester + .harness + .make_blinded_block(state_a.clone(), slot_b) + .await; + let (block_b, state_after_b): (SignedBlindedBeaconBlock, _) = + tester.harness.make_blinded_block(state_a, slot_b).await; + + /* check for `make_blinded_block` curios */ + assert_eq!(block_a.state_root(), state_after_a.tree_hash_root()); + assert_eq!(block_b.state_root(), state_after_b.tree_hash_root()); + assert_ne!(block_a.state_root(), block_b.state_root()); + + /* submit `block_a` as valid */ + assert!(tester + .client + .post_beacon_blinded_blocks_v2(&block_a, validation_level) + .await + .is_ok()); + assert!(tester + .harness + .chain + .block_is_known_to_fork_choice(&block_a.canonical_root())); + + /* submit `block_b` which should induce equivocation */ + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blinded_blocks_v2(&block_b, validation_level) + .await; + assert!(response.is_err()); + + let error_response: eth2::Error = response.err().unwrap(); + + assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); + + assert!( + matches!(error_response, eth2::Error::ServerMessage(err) if err.message == "BAD_REQUEST: Slashable".to_string()) + ); +} + +/// This test checks that a block that is only valid from a gossip perspective is rejected when using `broadcast_validation=consensus_and_equivocation`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn blinded_equivocation_gossip() { + /* this test targets gossip-level validation */ + let validation_level: Option = + Some(BroadcastValidation::ConsensusAndEquivocation); + + // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing + // `validator_count // 32`. + let validator_count = 64; + let num_initial: u64 = 31; + let tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block, _): (SignedBeaconBlock, _) = tester + .harness + .make_block_with_modifier(state_a, slot_b, |b| *b.state_root_mut() = Hash256::zero()) + .await; + + let blinded_block: SignedBlindedBeaconBlock = block.into(); + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blinded_blocks_v2(&blinded_block, validation_level) + .await; + assert!(response.is_err()); + + let error_response: eth2::Error = response.err().unwrap(); + + /* mandated by Beacon API spec */ + assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); + + assert!( + matches!(error_response, eth2::Error::ServerMessage(err) if err.message == "BAD_REQUEST: Invalid block: StateRootMismatch { block: 0x0000000000000000000000000000000000000000000000000000000000000000, local: 0xfc675d642ff7a06458eb33c7d7b62a5813e34d1b2bb1aee3e395100b579da026 }".to_string()) + ); +} + +/// This test checks that a block that is valid from both a gossip and consensus perspective but that equivocates **late** is rejected when using `broadcast_validation=consensus_and_equivocation`. +/// +/// This test is unique in that we can't actually test the HTTP API directly, but instead have to hook into the `publish_blocks` code manually. This is in order to handle the late equivocation case. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn blinded_equivocation_consensus_late_equivocation() { + /* this test targets gossip-level validation */ + let validation_level: Option = + Some(BroadcastValidation::ConsensusAndEquivocation); + + // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing + // `validator_count // 32`. + let validator_count = 64; + let num_initial: u64 = 31; + let tester = InteractiveTester::::new(None, validator_count).await; + let test_logger = tester.harness.logger().clone(); + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block_a, state_after_a): (SignedBlindedBeaconBlock, _) = tester + .harness + .make_blinded_block(state_a.clone(), slot_b) + .await; + let (block_b, state_after_b): (SignedBlindedBeaconBlock, _) = + tester.harness.make_blinded_block(state_a, slot_b).await; + + /* check for `make_blinded_block` curios */ + assert_eq!(block_a.state_root(), state_after_a.tree_hash_root()); + assert_eq!(block_b.state_root(), state_after_b.tree_hash_root()); + assert_ne!(block_a.state_root(), block_b.state_root()); + + let unblinded_block_a = reconstruct_block( + tester.harness.chain.clone(), + block_a.state_root(), + block_a, + test_logger.clone(), + ) + .await + .unwrap(); + let unblinded_block_b = reconstruct_block( + tester.harness.chain.clone(), + block_b.clone().state_root(), + block_b.clone(), + test_logger.clone(), + ) + .await + .unwrap(); + + let inner_block_a = match unblinded_block_a { + ProvenancedBlock::Local(a, _) => a, + ProvenancedBlock::Builder(a, _) => a, + }; + let inner_block_b = match unblinded_block_b { + ProvenancedBlock::Local(b, _) => b, + ProvenancedBlock::Builder(b, _) => b, + }; + + let gossip_block_b = GossipVerifiedBlock::new(inner_block_b, &tester.harness.chain); + assert!(gossip_block_b.is_ok()); + let gossip_block_a = GossipVerifiedBlock::new(inner_block_a, &tester.harness.chain); + assert!(gossip_block_a.is_err()); + + let channel = tokio::sync::mpsc::unbounded_channel(); + + let publication_result: Result<(), Rejection> = publish_blinded_block( + block_b, + tester.harness.chain, + &channel.0, + test_logger, + validation_level.unwrap(), + ) + .await; + + assert!(publication_result.is_err()); + + let publication_error: Rejection = publication_result.unwrap_err(); + + assert!(publication_error.find::().is_some()); +} + +/// This test checks that a block that is valid from both a gossip and consensus perspective (and does not equivocate) is accepted when using `broadcast_validation=consensus_and_equivocation`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn blinded_equivocation_full_pass() { + /* this test targets gossip-level validation */ + let validation_level: Option = + Some(BroadcastValidation::ConsensusAndEquivocation); + + // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing + // `validator_count // 32`. + let validator_count = 64; + let num_initial: u64 = 31; + let tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block, _): (SignedBlindedBeaconBlock, _) = + tester.harness.make_blinded_block(state_a, slot_b).await; + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blocks_v2(&block, validation_level) + .await; + + assert!(response.is_ok()); + assert!(tester + .harness + .chain + .block_is_known_to_fork_choice(&block.canonical_root())); +} diff --git a/beacon_node/http_api/tests/main.rs b/beacon_node/http_api/tests/main.rs index f5916d8506..e0636424e4 100644 --- a/beacon_node/http_api/tests/main.rs +++ b/beacon_node/http_api/tests/main.rs @@ -1,5 +1,6 @@ #![cfg(not(debug_assertions))] // Tests are too slow in debug. +pub mod broadcast_validation_tests; pub mod fork_tests; pub mod interactive_tests; pub mod status_tests; diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index a6c49ddaee..bcd192c699 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -159,7 +159,7 @@ impl ApiTester { // `make_block` adds random graffiti, so this will produce an alternate block let (reorg_block, _reorg_state) = harness - .make_block(head.beacon_state.clone(), harness.chain.slot().unwrap()) + .make_block(head.beacon_state.clone(), harness.chain.slot().unwrap() + 1) .await; let head_state_root = head.beacon_state_root(); @@ -1248,14 +1248,23 @@ impl ApiTester { } pub async fn test_post_beacon_blocks_invalid(mut self) -> Self { - let mut next_block = self.next_block.clone(); - *next_block.message_mut().proposer_index_mut() += 1; + let block = self + .harness + .make_block_with_modifier( + self.harness.get_current_state(), + self.harness.get_current_slot(), + |b| { + *b.state_root_mut() = Hash256::zero(); + }, + ) + .await + .0; - assert!(self.client.post_beacon_blocks(&next_block).await.is_err()); + assert!(self.client.post_beacon_blocks(&block).await.is_err()); assert!( self.network_rx.network_recv.recv().await.is_some(), - "invalid blocks should be sent to network" + "gossip valid blocks should be sent to network" ); self @@ -4126,7 +4135,7 @@ impl ApiTester { .unwrap(); let expected_reorg = EventKind::ChainReorg(SseChainReorg { - slot: self.next_block.slot(), + slot: self.reorg_block.slot(), depth: 1, old_head_block: self.next_block.canonical_root(), old_head_state: self.next_block.state_root(), @@ -4136,6 +4145,8 @@ impl ApiTester { execution_optimistic: false, }); + self.harness.advance_slot(); + self.client .post_beacon_blocks(&self.reorg_block) .await diff --git a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs b/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs index 185634c308..91ec81b18d 100644 --- a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs +++ b/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs @@ -785,6 +785,20 @@ impl Worker { verified_block } + Err(e @ BlockError::Slashable) => { + warn!( + self.log, + "Received equivocating block from peer"; + "error" => ?e + ); + /* punish peer for submitting an equivocation, but not too harshly as honest peers may conceivably forward equivocating blocks to us from time to time */ + self.gossip_penalize_peer( + peer_id, + PeerAction::MidToleranceError, + "gossip_block_mid", + ); + return None; + } Err(BlockError::ParentUnknown(block)) => { debug!( self.log, @@ -806,7 +820,6 @@ impl Worker { Err(e @ BlockError::FutureSlot { .. }) | Err(e @ BlockError::WouldRevertFinalizedSlot { .. }) | Err(e @ BlockError::BlockIsAlreadyKnown) - | Err(e @ BlockError::RepeatProposal { .. }) | Err(e @ BlockError::NotFinalizedDescendant { .. }) => { debug!(self.log, "Could not verify block for gossip. Ignoring the block"; "error" => %e); @@ -948,7 +961,12 @@ impl Worker { let result = self .chain - .process_block(block_root, verified_block, NotifyExecutionLayer::Yes) + .process_block( + block_root, + verified_block, + NotifyExecutionLayer::Yes, + || Ok(()), + ) .await; match &result { diff --git a/beacon_node/network/src/beacon_processor/worker/sync_methods.rs b/beacon_node/network/src/beacon_processor/worker/sync_methods.rs index 7e8fce3563..ac59b1daa9 100644 --- a/beacon_node/network/src/beacon_processor/worker/sync_methods.rs +++ b/beacon_node/network/src/beacon_processor/worker/sync_methods.rs @@ -98,33 +98,21 @@ impl Worker { }); // Checks if a block from this proposer is already known. - let proposal_already_known = || { + let block_equivocates = || { match self .chain .observed_block_producers .read() - .proposer_has_been_observed(block.message()) + .proposer_has_been_observed(block.message(), block.canonical_root()) { - Ok(is_observed) => is_observed, - // Both of these blocks will be rejected, so reject them now rather + Ok(seen_status) => seen_status.is_slashable(), + //Both of these blocks will be rejected, so reject them now rather // than re-queuing them. Err(ObserveError::FinalizedBlock { .. }) | Err(ObserveError::ValidatorIndexTooHigh { .. }) => false, } }; - // Returns `true` if the block is already known to fork choice. Notably, - // this will return `false` for blocks that we've already imported but - // ancestors of the finalized checkpoint. That should not be an issue - // for our use here since finalized blocks will always be late and won't - // be requeued anyway. - let block_is_already_known = || { - self.chain - .canonical_head - .fork_choice_read_lock() - .contains_block(&block_root) - }; - // If we've already seen a block from this proposer *and* the block // arrived before the attestation deadline, requeue it to ensure it is // imported late enough that it won't receive a proposer boost. @@ -132,7 +120,7 @@ impl Worker { // Don't requeue blocks if they're already known to fork choice, just // push them through to block processing so they can be handled through // the normal channels. - if !block_is_late && proposal_already_known() && !block_is_already_known() { + if !block_is_late && block_equivocates() { debug!( self.log, "Delaying processing of duplicate RPC block"; @@ -165,7 +153,7 @@ impl Worker { let parent_root = block.message().parent_root(); let result = self .chain - .process_block(block_root, block, NotifyExecutionLayer::Yes) + .process_block(block_root, block, NotifyExecutionLayer::Yes, || Ok(())) .await; metrics::inc_counter(&metrics::BEACON_PROCESSOR_RPC_BLOCK_IMPORTED_TOTAL); diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 217d356968..e34916beba 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -322,6 +322,26 @@ impl BeaconNodeHttpClient { ok_or_error(response).await } + /// Generic POST function supporting arbitrary responses and timeouts. + async fn post_generic_with_consensus_version( + &self, + url: U, + body: &T, + timeout: Option, + fork: ForkName, + ) -> Result { + let mut builder = self.client.post(url); + if let Some(timeout) = timeout { + builder = builder.timeout(timeout); + } + let response = builder + .header(CONSENSUS_VERSION_HEADER, fork.to_string()) + .json(body) + .send() + .await?; + ok_or_error(response).await + } + /// `GET beacon/genesis` /// /// ## Errors @@ -654,6 +674,76 @@ impl BeaconNodeHttpClient { Ok(()) } + pub fn post_beacon_blocks_v2_path( + &self, + validation_level: Option, + ) -> Result { + let mut path = self.eth_path(V2)?; + path.path_segments_mut() + .map_err(|_| Error::InvalidUrl(self.server.clone()))? + .extend(&["beacon", "blocks"]); + + path.set_query( + validation_level + .map(|v| format!("broadcast_validation={}", v)) + .as_deref(), + ); + + Ok(path) + } + + pub fn post_beacon_blinded_blocks_v2_path( + &self, + validation_level: Option, + ) -> Result { + let mut path = self.eth_path(V2)?; + path.path_segments_mut() + .map_err(|_| Error::InvalidUrl(self.server.clone()))? + .extend(&["beacon", "blinded_blocks"]); + + path.set_query( + validation_level + .map(|v| format!("broadcast_validation={}", v)) + .as_deref(), + ); + + Ok(path) + } + + /// `POST v2/beacon/blocks` + pub async fn post_beacon_blocks_v2>( + &self, + block: &SignedBeaconBlock, + validation_level: Option, + ) -> Result<(), Error> { + self.post_generic_with_consensus_version( + self.post_beacon_blocks_v2_path(validation_level)?, + block, + Some(self.timeouts.proposal), + block.message().body().fork_name(), + ) + .await?; + + Ok(()) + } + + /// `POST v2/beacon/blinded_blocks` + pub async fn post_beacon_blinded_blocks_v2( + &self, + block: &SignedBlindedBeaconBlock, + validation_level: Option, + ) -> Result<(), Error> { + self.post_generic_with_consensus_version( + self.post_beacon_blinded_blocks_v2_path(validation_level)?, + block, + Some(self.timeouts.proposal), + block.message().body().fork_name(), + ) + .await?; + + Ok(()) + } + /// Path for `v2/beacon/blocks` pub fn get_beacon_blocks_path(&self, block_id: BlockId) -> Result { let mut path = self.eth_path(V2)?; diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index 55759a2e15..5f2e1ada7b 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -6,7 +6,7 @@ use lighthouse_network::{ConnectionDirection, Enr, Multiaddr, PeerConnectionStat use mediatype::{names, MediaType, MediaTypeList}; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; -use std::fmt; +use std::fmt::{self, Display}; use std::str::{from_utf8, FromStr}; use std::time::Duration; pub use types::*; @@ -1260,6 +1260,50 @@ pub struct ForkChoiceNode { pub execution_block_hash: Option, } +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum BroadcastValidation { + Gossip, + Consensus, + ConsensusAndEquivocation, +} + +impl Default for BroadcastValidation { + fn default() -> Self { + Self::Gossip + } +} + +impl Display for BroadcastValidation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Gossip => write!(f, "gossip"), + Self::Consensus => write!(f, "consensus"), + Self::ConsensusAndEquivocation => write!(f, "consensus_and_equivocation"), + } + } +} + +impl FromStr for BroadcastValidation { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "gossip" => Ok(Self::Gossip), + "consensus" => Ok(Self::Consensus), + "consensus_and_equivocation" => Ok(Self::ConsensusAndEquivocation), + _ => Err("Invalid broadcast validation level"), + } + } +} + +#[derive(Default, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct BroadcastValidationQuery { + #[serde(default)] + pub broadcast_validation: BroadcastValidation, +} + #[cfg(test)] mod tests { use super::*; diff --git a/consensus/types/src/beacon_block_body.rs b/consensus/types/src/beacon_block_body.rs index c0ba869410..dce1be742f 100644 --- a/consensus/types/src/beacon_block_body.rs +++ b/consensus/types/src/beacon_block_body.rs @@ -89,7 +89,7 @@ impl<'a, T: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, T, } } -impl<'a, T: EthSpec> BeaconBlockBodyRef<'a, T> { +impl<'a, T: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, T, Payload> { /// Get the fork_name of this object pub fn fork_name(self) -> ForkName { match self { diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index e0f4043ac2..65528de175 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -382,6 +382,7 @@ impl Tester { block_root, block.clone(), NotifyExecutionLayer::Yes, + || Ok(()), ))?; if result.is_ok() != valid { return Err(Error::DidntFail(format!( From edd093293a34b68e5fc32382f7fa0c374f8543ed Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 30 Jun 2023 01:13:03 +0000 Subject: [PATCH 59/63] added debounce to log (#4269) ## Issue Addressed [#4259](https://github.com/sigp/lighthouse/issues/4259) ## Proposed Changes debounce spammy `Unable to send message to the beacon processor` log messages ## Additional Info We could potentially debounce other logs that have the potential to be "spammy". After some feedback we decided to additionally add the following change: create a newtype wrapper around `mpsc::Sender>`. When there is an error on the try_send method on the wrapper, we increase a counter metric with one label per work type. --- .../network/src/beacon_processor/mod.rs | 18 +++++++++++++++ beacon_node/network/src/metrics.rs | 6 +++++ beacon_node/network/src/router.rs | 22 +++++++++++++------ .../network/src/sync/block_lookups/tests.rs | 3 ++- beacon_node/network/src/sync/manager.rs | 4 ++-- .../network/src/sync/network_context.rs | 10 ++++----- .../network/src/sync/range_sync/range.rs | 4 ++-- 7 files changed, 50 insertions(+), 17 deletions(-) diff --git a/beacon_node/network/src/beacon_processor/mod.rs b/beacon_node/network/src/beacon_processor/mod.rs index 26d2c19b51..84d8e1b07a 100644 --- a/beacon_node/network/src/beacon_processor/mod.rs +++ b/beacon_node/network/src/beacon_processor/mod.rs @@ -750,6 +750,24 @@ impl std::convert::From> for WorkEvent { } } +pub struct BeaconProcessorSend(pub mpsc::Sender>); + +impl BeaconProcessorSend { + pub fn try_send(&self, message: WorkEvent) -> Result<(), Box>>> { + let work_type = message.work_type(); + match self.0.try_send(message) { + Ok(res) => Ok(res), + Err(e) => { + metrics::inc_counter_vec( + &metrics::BEACON_PROCESSOR_SEND_ERROR_PER_WORK_TYPE, + &[work_type], + ); + Err(Box::new(e)) + } + } + } +} + /// A consensus message (or multiple) from the network that requires processing. #[derive(Derivative)] #[derivative(Debug(bound = "T: BeaconChainTypes"))] diff --git a/beacon_node/network/src/metrics.rs b/beacon_node/network/src/metrics.rs index 09caaaa11e..27d7dc9625 100644 --- a/beacon_node/network/src/metrics.rs +++ b/beacon_node/network/src/metrics.rs @@ -279,6 +279,12 @@ lazy_static! { "Gossipsub light_client_optimistic_update errors per error type", &["type"] ); + pub static ref BEACON_PROCESSOR_SEND_ERROR_PER_WORK_TYPE: Result = + try_create_int_counter_vec( + "beacon_processor_send_error_per_work_type", + "Total number of beacon processor send error per work type", + &["type"] + ); /* diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index 1b0f1fb41e..7a91f2d0b1 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -6,7 +6,8 @@ #![allow(clippy::unit_arg)] use crate::beacon_processor::{ - BeaconProcessor, InvalidBlockStorage, WorkEvent as BeaconWorkEvent, MAX_WORK_EVENT_QUEUE_LEN, + BeaconProcessor, BeaconProcessorSend, InvalidBlockStorage, WorkEvent as BeaconWorkEvent, + MAX_WORK_EVENT_QUEUE_LEN, }; use crate::error; use crate::service::{NetworkMessage, RequestId}; @@ -19,6 +20,7 @@ use lighthouse_network::rpc::*; use lighthouse_network::{ MessageId, NetworkGlobals, PeerId, PeerRequestId, PubsubMessage, Request, Response, }; +use logging::TimeLatch; use slog::{debug, o, trace}; use slog::{error, warn}; use std::cmp; @@ -39,9 +41,11 @@ pub struct Router { /// A network context to return and handle RPC requests. network: HandlerNetworkContext, /// A multi-threaded, non-blocking processor for applying messages to the beacon chain. - beacon_processor_send: mpsc::Sender>, + beacon_processor_send: BeaconProcessorSend, /// The `Router` logger. log: slog::Logger, + /// Provides de-bounce functionality for logging. + logger_debounce: TimeLatch, } /// Types of messages the router can receive. @@ -100,7 +104,7 @@ impl Router { beacon_chain.clone(), network_globals.clone(), network_send.clone(), - beacon_processor_send.clone(), + BeaconProcessorSend(beacon_processor_send.clone()), sync_logger, ); @@ -124,8 +128,9 @@ impl Router { chain: beacon_chain, sync_send, network: HandlerNetworkContext::new(network_send, log.clone()), - beacon_processor_send, + beacon_processor_send: BeaconProcessorSend(beacon_processor_send), log: message_handler_log, + logger_debounce: TimeLatch::default(), }; // spawn handler task and move the message handler instance into the spawned thread @@ -479,12 +484,15 @@ impl Router { self.beacon_processor_send .try_send(work) .unwrap_or_else(|e| { - let work_type = match &e { + let work_type = match &*e { mpsc::error::TrySendError::Closed(work) | mpsc::error::TrySendError::Full(work) => work.work_type(), }; - error!(&self.log, "Unable to send message to the beacon processor"; - "error" => %e, "type" => work_type) + + if self.logger_debounce.elapsed() { + error!(&self.log, "Unable to send message to the beacon processor"; + "error" => %e, "type" => work_type) + } }) } } diff --git a/beacon_node/network/src/sync/block_lookups/tests.rs b/beacon_node/network/src/sync/block_lookups/tests.rs index 5a70944f6c..82334db0f8 100644 --- a/beacon_node/network/src/sync/block_lookups/tests.rs +++ b/beacon_node/network/src/sync/block_lookups/tests.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use crate::beacon_processor::BeaconProcessorSend; use crate::service::RequestId; use crate::sync::manager::RequestId as SyncId; use crate::NetworkMessage; @@ -54,7 +55,7 @@ impl TestRig { SyncNetworkContext::new( network_tx, globals, - beacon_processor_tx, + BeaconProcessorSend(beacon_processor_tx), log.new(slog::o!("component" => "network_context")), ) }; diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 37b63cdba7..c24d4c192b 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -38,7 +38,7 @@ use super::block_lookups::BlockLookups; use super::network_context::SyncNetworkContext; use super::peer_sync_info::{remote_sync_type, PeerSyncType}; use super::range_sync::{RangeSync, RangeSyncType, EPOCHS_PER_BATCH}; -use crate::beacon_processor::{ChainSegmentProcessId, WorkEvent as BeaconWorkEvent}; +use crate::beacon_processor::{BeaconProcessorSend, ChainSegmentProcessId}; use crate::service::NetworkMessage; use crate::status::ToStatusMessage; use beacon_chain::{BeaconChain, BeaconChainTypes, BlockError, EngineState}; @@ -188,7 +188,7 @@ pub fn spawn( beacon_chain: Arc>, network_globals: Arc>, network_send: mpsc::UnboundedSender>, - beacon_processor_send: mpsc::Sender>, + beacon_processor_send: BeaconProcessorSend, log: slog::Logger, ) -> mpsc::UnboundedSender> { assert!( diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 23d42002f4..03c466eece 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -3,7 +3,7 @@ use super::manager::{Id, RequestId as SyncRequestId}; use super::range_sync::{BatchId, ChainId}; -use crate::beacon_processor::WorkEvent; +use crate::beacon_processor::BeaconProcessorSend; use crate::service::{NetworkMessage, RequestId}; use crate::status::ToStatusMessage; use beacon_chain::{BeaconChainTypes, EngineState}; @@ -37,7 +37,7 @@ pub struct SyncNetworkContext { execution_engine_state: EngineState, /// Channel to send work to the beacon processor. - beacon_processor_send: mpsc::Sender>, + beacon_processor_send: BeaconProcessorSend, /// Logger for the `SyncNetworkContext`. log: slog::Logger, @@ -47,7 +47,7 @@ impl SyncNetworkContext { pub fn new( network_send: mpsc::UnboundedSender>, network_globals: Arc>, - beacon_processor_send: mpsc::Sender>, + beacon_processor_send: BeaconProcessorSend, log: slog::Logger, ) -> Self { Self { @@ -278,12 +278,12 @@ impl SyncNetworkContext { }) } - pub fn processor_channel_if_enabled(&self) -> Option<&mpsc::Sender>> { + pub fn processor_channel_if_enabled(&self) -> Option<&BeaconProcessorSend> { self.is_execution_engine_online() .then_some(&self.beacon_processor_send) } - pub fn processor_channel(&self) -> &mpsc::Sender> { + pub fn processor_channel(&self) -> &BeaconProcessorSend { &self.beacon_processor_send } diff --git a/beacon_node/network/src/sync/range_sync/range.rs b/beacon_node/network/src/sync/range_sync/range.rs index 0f1c00e509..2c35c57d9e 100644 --- a/beacon_node/network/src/sync/range_sync/range.rs +++ b/beacon_node/network/src/sync/range_sync/range.rs @@ -375,7 +375,7 @@ mod tests { use crate::NetworkMessage; use super::*; - use crate::beacon_processor::WorkEvent as BeaconWorkEvent; + use crate::beacon_processor::{BeaconProcessorSend, WorkEvent as BeaconWorkEvent}; use beacon_chain::builder::Witness; use beacon_chain::eth1_chain::CachingEth1Backend; use beacon_chain::parking_lot::RwLock; @@ -603,7 +603,7 @@ mod tests { let cx = SyncNetworkContext::new( network_tx, globals.clone(), - beacon_processor_tx, + BeaconProcessorSend(beacon_processor_tx), log.new(o!("component" => "network_context")), ); let test_rig = TestRig { From 826e090f50da93a0cca297bc4e57a4300f0db9f2 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 30 Jun 2023 01:13:04 +0000 Subject: [PATCH 60/63] Update node health endpoint (#4310) ## Issue Addressed [#4292](https://github.com/sigp/lighthouse/issues/4292) ## Proposed Changes Updated the node health endpoint will return a 200 status code if `!syncing && !el_offline && !optimistic` wil return a 206 if `(syncing || optimistic) && !el_offline` will return a 503 if `el_offline` ## Additional Info --- beacon_node/http_api/src/lib.rs | 53 +++++++++----- beacon_node/http_api/tests/status_tests.rs | 80 ++++++++++++++++++++++ beacon_node/http_api/tests/tests.rs | 14 ++-- 3 files changed, 125 insertions(+), 22 deletions(-) diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 93bfe524bc..27bcc4d8a1 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -2418,24 +2418,41 @@ pub fn serve( .and(warp::path("health")) .and(warp::path::end()) .and(network_globals.clone()) - .and_then(|network_globals: Arc>| { - blocking_response_task(move || match *network_globals.sync_state.read() { - SyncState::SyncingFinalized { .. } - | SyncState::SyncingHead { .. } - | SyncState::SyncTransition - | SyncState::BackFillSyncing { .. } => Ok(warp::reply::with_status( - warp::reply(), - warp::http::StatusCode::PARTIAL_CONTENT, - )), - SyncState::Synced => Ok(warp::reply::with_status( - warp::reply(), - warp::http::StatusCode::OK, - )), - SyncState::Stalled => Err(warp_utils::reject::not_synced( - "sync stalled, beacon chain may not yet be initialized.".to_string(), - )), - }) - }); + .and(chain_filter.clone()) + .and_then( + |network_globals: Arc>, chain: Arc>| { + async move { + let el_offline = if let Some(el) = &chain.execution_layer { + el.is_offline_or_erroring().await + } else { + true + }; + + blocking_response_task(move || { + let is_optimistic = chain + .is_optimistic_or_invalid_head() + .map_err(warp_utils::reject::beacon_chain_error)?; + + let is_syncing = !network_globals.sync_state.read().is_synced(); + + if el_offline { + Err(warp_utils::reject::not_synced("execution layer is offline".to_string())) + } else if is_syncing || is_optimistic { + Ok(warp::reply::with_status( + warp::reply(), + warp::http::StatusCode::PARTIAL_CONTENT, + )) + } else { + Ok(warp::reply::with_status( + warp::reply(), + warp::http::StatusCode::OK, + )) + } + }) + .await + } + }, + ); // GET node/peers/{peer_id} let get_node_peers_by_id = eth_v1 diff --git a/beacon_node/http_api/tests/status_tests.rs b/beacon_node/http_api/tests/status_tests.rs index ce725b75a9..95f885faa5 100644 --- a/beacon_node/http_api/tests/status_tests.rs +++ b/beacon_node/http_api/tests/status_tests.rs @@ -3,6 +3,7 @@ use beacon_chain::{ test_utils::{AttestationStrategy, BlockStrategy, SyncCommitteeStrategy}, BlockError, }; +use eth2::StatusCode; use execution_layer::{PayloadStatusV1, PayloadStatusV1Status}; use http_api::test_utils::InteractiveTester; use types::{EthSpec, ExecPayload, ForkName, MinimalEthSpec, Slot}; @@ -143,3 +144,82 @@ async fn el_error_on_new_payload() { assert_eq!(api_response.is_optimistic, Some(false)); assert_eq!(api_response.is_syncing, false); } + +/// Check `node health` endpoint when the EL is offline. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn node_health_el_offline() { + let num_blocks = E::slots_per_epoch() / 2; + let num_validators = E::slots_per_epoch(); + let tester = post_merge_tester(num_blocks, num_validators).await; + let harness = &tester.harness; + let mock_el = harness.mock_execution_layer.as_ref().unwrap(); + + // EL offline + mock_el.server.set_syncing_response(Err("offline".into())); + mock_el.el.upcheck().await; + + let status = tester.client.get_node_health().await; + match status { + Ok(_) => { + panic!("should return 503 error status code"); + } + Err(e) => { + assert_eq!(e.status().unwrap(), 503); + } + } +} + +/// Check `node health` endpoint when the EL is online and synced. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn node_health_el_online_and_synced() { + let num_blocks = E::slots_per_epoch() / 2; + let num_validators = E::slots_per_epoch(); + let tester = post_merge_tester(num_blocks, num_validators).await; + let harness = &tester.harness; + let mock_el = harness.mock_execution_layer.as_ref().unwrap(); + + // EL synced + mock_el.server.set_syncing_response(Ok(false)); + mock_el.el.upcheck().await; + + let status = tester.client.get_node_health().await; + match status { + Ok(response) => { + assert_eq!(response, StatusCode::OK); + } + Err(_) => { + panic!("should return 200 status code"); + } + } +} + +/// Check `node health` endpoint when the EL is online but not synced. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn node_health_el_online_and_not_synced() { + let num_blocks = E::slots_per_epoch() / 2; + let num_validators = E::slots_per_epoch(); + let tester = post_merge_tester(num_blocks, num_validators).await; + let harness = &tester.harness; + let mock_el = harness.mock_execution_layer.as_ref().unwrap(); + + // EL not synced + harness.advance_slot(); + mock_el.server.all_payloads_syncing(true); + harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let status = tester.client.get_node_health().await; + match status { + Ok(response) => { + assert_eq!(response, StatusCode::PARTIAL_CONTENT); + } + Err(_) => { + panic!("should return 206 status code"); + } + } +} diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index bcd192c699..741ee1ffc0 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -8,7 +8,7 @@ use eth2::{ mixin::{RequestAccept, ResponseForkName, ResponseOptional}, reqwest::RequestBuilder, types::{BlockId as CoreBlockId, ForkChoiceNode, StateId as CoreStateId, *}, - BeaconNodeHttpClient, Error, StatusCode, Timeouts, + BeaconNodeHttpClient, Error, Timeouts, }; use execution_layer::test_utils::TestingBuilder; use execution_layer::test_utils::DEFAULT_BUILDER_THRESHOLD_WEI; @@ -1762,9 +1762,15 @@ impl ApiTester { } pub async fn test_get_node_health(self) -> Self { - let status = self.client.get_node_health().await.unwrap(); - assert_eq!(status, StatusCode::OK); - + let status = self.client.get_node_health().await; + match status { + Ok(_) => { + panic!("should return 503 error status code"); + } + Err(e) => { + assert_eq!(e.status().unwrap(), 503); + } + } self } From 46be05f7280c3afdfbd614dbc8f6a9b3985ab821 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Fri, 30 Jun 2023 01:13:06 +0000 Subject: [PATCH 61/63] Cache target attester balances for unrealized FFG progression calculation (#4362) ## Issue Addressed #4118 ## Proposed Changes This PR introduces a "progressive balances" cache on the `BeaconState`, which keeps track of the accumulated target attestation balance for the current & previous epochs. The cached values are utilised by fork choice to calculate unrealized justification and finalization (instead of converting epoch participation arrays to balances for each block we receive). This optimization will be rolled out gradually to allow for more testing. A new `--progressive-balances disabled|checked|strict|fast` flag is introduced to support this: - `checked`: enabled with checks against participation cache, and falls back to the existing epoch processing calculation if there is a total target attester balance mismatch. There is no performance gain from this as the participation cache still needs to be computed. **This is the default mode for now.** - `strict`: enabled with checks against participation cache, returns error if there is a mismatch. **Used for testing only**. - `fast`: enabled with no comparative checks and without computing the participation cache. This mode gives us the performance gains from the optimization. This is still experimental and not currently recommended for production usage, but will become the default mode in a future release. - `disabled`: disable the usage of progressive cache, and use the existing method for FFG progression calculation. This mode may be useful if we find a bug and want to stop the frequent error logs. ### Tasks - [x] Initial cache implementation in `BeaconState` - [x] Perform checks in fork choice to compare the progressive balances cache against results from `ParticipationCache` - [x] Add CLI flag, and disable the optimization by default - [x] Testing on Goerli & Benchmarking - [x] Move caching logic from state processing to the `ProgressiveBalancesCache` (see [this comment](https://github.com/sigp/lighthouse/pull/4362#discussion_r1230877001)) - [x] Add attesting balance metrics Co-authored-by: Jimmy Chen --- Cargo.lock | 1 + beacon_node/beacon_chain/src/beacon_chain.rs | 2 + beacon_node/beacon_chain/src/builder.rs | 8 +- beacon_node/beacon_chain/src/chain_config.rs | 5 +- beacon_node/beacon_chain/src/fork_revert.rs | 9 +- beacon_node/beacon_chain/src/test_utils.rs | 38 +++- beacon_node/beacon_chain/tests/capella.rs | 18 +- .../tests/payload_invalidation.rs | 3 +- beacon_node/http_api/src/block_rewards.rs | 2 +- beacon_node/src/cli.rs | 14 ++ beacon_node/src/config.rs | 6 + beacon_node/store/src/partial_beacon_state.rs | 1 + beacon_node/store/src/reconstruct.rs | 2 +- consensus/fork_choice/src/fork_choice.rs | 209 +++++++++++++++--- consensus/fork_choice/tests/tests.rs | 106 ++++++++- consensus/state_processing/src/common/mod.rs | 1 + .../src/common/slash_validator.rs | 3 + .../update_progressive_balances_cache.rs | 142 ++++++++++++ consensus/state_processing/src/genesis.rs | 4 +- consensus/state_processing/src/metrics.rs | 11 + .../src/per_block_processing.rs | 9 + .../src/per_block_processing/errors.rs | 9 + .../process_operations.rs | 11 + .../src/per_epoch_processing/altair.rs | 8 +- .../altair/participation_cache.rs | 54 ++--- .../src/per_epoch_processing/base.rs | 2 +- .../src/per_epoch_processing/capella.rs | 8 +- .../effective_balance_updates.rs | 41 +++- .../src/per_epoch_processing/slashings.rs | 2 +- .../state_processing/src/upgrade/altair.rs | 4 + .../state_processing/src/upgrade/capella.rs | 1 + .../state_processing/src/upgrade/merge.rs | 1 + consensus/types/Cargo.toml | 1 + consensus/types/benches/benches.rs | 2 +- consensus/types/src/beacon_state.rs | 55 ++++- consensus/types/src/beacon_state/balance.rs | 33 +++ .../types/src/beacon_state/clone_config.rs | 2 + .../progressive_balances_cache.rs | 184 +++++++++++++++ consensus/types/src/beacon_state/tests.rs | 5 +- lcli/src/new_testnet.rs | 2 +- lcli/src/skip_slots.rs | 2 +- lcli/src/transition_blocks.rs | 6 +- lighthouse/tests/beacon_node.rs | 30 ++- .../ef_tests/src/cases/epoch_processing.rs | 4 +- testing/ef_tests/src/cases/fork_choice.rs | 5 +- testing/ef_tests/src/cases/operations.rs | 4 + testing/ef_tests/src/cases/sanity_blocks.rs | 2 +- testing/ef_tests/src/cases/sanity_slots.rs | 2 +- 48 files changed, 953 insertions(+), 121 deletions(-) create mode 100644 consensus/state_processing/src/common/update_progressive_balances_cache.rs create mode 100644 consensus/types/src/beacon_state/balance.rs create mode 100644 consensus/types/src/beacon_state/progressive_balances_cache.rs diff --git a/Cargo.lock b/Cargo.lock index 02922b2d7e..efc6a5d6ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8935,6 +8935,7 @@ dependencies = [ "smallvec", "ssz_types", "state_processing", + "strum", "superstruct 0.6.0", "swap_or_not_shuffle", "tempfile", diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 772e4c1529..01343ff3b1 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -2898,7 +2898,9 @@ impl BeaconChain { block_delay, &state, payload_verification_status, + self.config.progressive_balances_mode, &self.spec, + &self.log, ) .map_err(|e| BlockError::BeaconChainError(e.into()))?; } diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 9bb3939632..044391c415 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -338,7 +338,7 @@ where let beacon_block = genesis_block(&mut beacon_state, &self.spec)?; beacon_state - .build_all_caches(&self.spec) + .build_caches(&self.spec) .map_err(|e| format!("Failed to build genesis state caches: {:?}", e))?; let beacon_state_root = beacon_block.message().state_root(); @@ -437,7 +437,7 @@ where // Prime all caches before storing the state in the database and computing the tree hash // root. weak_subj_state - .build_all_caches(&self.spec) + .build_caches(&self.spec) .map_err(|e| format!("Error building caches on checkpoint state: {e:?}"))?; let computed_state_root = weak_subj_state @@ -687,6 +687,8 @@ where store.clone(), Some(current_slot), &self.spec, + self.chain_config.progressive_balances_mode, + &log, )?; } @@ -700,7 +702,7 @@ where head_snapshot .beacon_state - .build_all_caches(&self.spec) + .build_caches(&self.spec) .map_err(|e| format!("Failed to build state caches: {:?}", e))?; // Perform a check to ensure that the finalization points of the head and fork choice are diff --git a/beacon_node/beacon_chain/src/chain_config.rs b/beacon_node/beacon_chain/src/chain_config.rs index 34a5c9a4ec..cc7a957ecc 100644 --- a/beacon_node/beacon_chain/src/chain_config.rs +++ b/beacon_node/beacon_chain/src/chain_config.rs @@ -1,7 +1,7 @@ pub use proto_array::{DisallowedReOrgOffsets, ReOrgThreshold}; use serde_derive::{Deserialize, Serialize}; use std::time::Duration; -use types::{Checkpoint, Epoch}; +use types::{Checkpoint, Epoch, ProgressiveBalancesMode}; pub const DEFAULT_RE_ORG_THRESHOLD: ReOrgThreshold = ReOrgThreshold(20); pub const DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION: Epoch = Epoch::new(2); @@ -81,6 +81,8 @@ pub struct ChainConfig { pub always_prepare_payload: bool, /// Whether backfill sync processing should be rate-limited. pub enable_backfill_rate_limiting: bool, + /// Whether to use `ProgressiveBalancesCache` in unrealized FFG progression calculation. + pub progressive_balances_mode: ProgressiveBalancesMode, } impl Default for ChainConfig { @@ -111,6 +113,7 @@ impl Default for ChainConfig { genesis_backfill: false, always_prepare_payload: false, enable_backfill_rate_limiting: true, + progressive_balances_mode: ProgressiveBalancesMode::Checked, } } } diff --git a/beacon_node/beacon_chain/src/fork_revert.rs b/beacon_node/beacon_chain/src/fork_revert.rs index 084ae95e09..dc0e34277c 100644 --- a/beacon_node/beacon_chain/src/fork_revert.rs +++ b/beacon_node/beacon_chain/src/fork_revert.rs @@ -10,7 +10,10 @@ use state_processing::{ use std::sync::Arc; use std::time::Duration; use store::{iter::ParentRootBlockIterator, HotColdDB, ItemStore}; -use types::{BeaconState, ChainSpec, EthSpec, ForkName, Hash256, SignedBeaconBlock, Slot}; +use types::{ + BeaconState, ChainSpec, EthSpec, ForkName, Hash256, ProgressiveBalancesMode, SignedBeaconBlock, + Slot, +}; const CORRUPT_DB_MESSAGE: &str = "The database could be corrupt. Check its file permissions or \ consider deleting it by running with the --purge-db flag."; @@ -100,6 +103,8 @@ pub fn reset_fork_choice_to_finalization, Cold: It store: Arc>, current_slot: Option, spec: &ChainSpec, + progressive_balances_mode: ProgressiveBalancesMode, + log: &Logger, ) -> Result, E>, String> { // Fetch finalized block. let finalized_checkpoint = head_state.finalized_checkpoint(); @@ -197,7 +202,9 @@ pub fn reset_fork_choice_to_finalization, Cold: It Duration::from_secs(0), &state, payload_verification_status, + progressive_balances_mode, spec, + log, ) .map_err(|e| format!("Error applying replayed block to fork choice: {:?}", e))?; } diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 21f7248cee..6520c9ba9c 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -754,9 +754,7 @@ where complete_state_advance(&mut state, None, slot, &self.spec) .expect("should be able to advance state to slot"); - state - .build_all_caches(&self.spec) - .expect("should build caches"); + state.build_caches(&self.spec).expect("should build caches"); let proposer_index = state.get_beacon_proposer_index(slot, &self.spec).unwrap(); @@ -803,9 +801,7 @@ where complete_state_advance(&mut state, None, slot, &self.spec) .expect("should be able to advance state to slot"); - state - .build_all_caches(&self.spec) - .expect("should build caches"); + state.build_caches(&self.spec).expect("should build caches"); let proposer_index = state.get_beacon_proposer_index(slot, &self.spec).unwrap(); @@ -1523,6 +1519,36 @@ where .sign(sk, &fork, genesis_validators_root, &self.chain.spec) } + pub fn add_proposer_slashing(&self, validator_index: u64) -> Result<(), String> { + let propposer_slashing = self.make_proposer_slashing(validator_index); + if let ObservationOutcome::New(verified_proposer_slashing) = self + .chain + .verify_proposer_slashing_for_gossip(propposer_slashing) + .expect("should verify proposer slashing for gossip") + { + self.chain + .import_proposer_slashing(verified_proposer_slashing); + Ok(()) + } else { + Err("should observe new proposer slashing".to_string()) + } + } + + pub fn add_attester_slashing(&self, validator_indices: Vec) -> Result<(), String> { + let attester_slashing = self.make_attester_slashing(validator_indices); + if let ObservationOutcome::New(verified_attester_slashing) = self + .chain + .verify_attester_slashing_for_gossip(attester_slashing) + .expect("should verify attester slashing for gossip") + { + self.chain + .import_attester_slashing(verified_attester_slashing); + Ok(()) + } else { + Err("should observe new attester slashing".to_string()) + } + } + pub fn add_bls_to_execution_change( &self, validator_index: u64, diff --git a/beacon_node/beacon_chain/tests/capella.rs b/beacon_node/beacon_chain/tests/capella.rs index e910e8134f..f0b799ec9f 100644 --- a/beacon_node/beacon_chain/tests/capella.rs +++ b/beacon_node/beacon_chain/tests/capella.rs @@ -133,13 +133,8 @@ async fn base_altair_merge_capella() { for _ in (merge_fork_slot.as_u64() + 3)..capella_fork_slot.as_u64() { harness.extend_slots(1).await; let block = &harness.chain.head_snapshot().beacon_block; - let full_payload: FullPayload = block - .message() - .body() - .execution_payload() - .unwrap() - .clone() - .into(); + let full_payload: FullPayload = + block.message().body().execution_payload().unwrap().into(); // pre-capella shouldn't have withdrawals assert!(full_payload.withdrawals_root().is_err()); execution_payloads.push(full_payload); @@ -151,13 +146,8 @@ async fn base_altair_merge_capella() { for _ in 0..16 { harness.extend_slots(1).await; let block = &harness.chain.head_snapshot().beacon_block; - let full_payload: FullPayload = block - .message() - .body() - .execution_payload() - .unwrap() - .clone() - .into(); + let full_payload: FullPayload = + block.message().body().execution_payload().unwrap().into(); // post-capella should have withdrawals assert!(full_payload.withdrawals_root().is_ok()); execution_payloads.push(full_payload); diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index 018defd2f0..9a8c324d09 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -1064,8 +1064,9 @@ async fn invalid_parent() { Duration::from_secs(0), &state, PayloadVerificationStatus::Optimistic, + rig.harness.chain.config.progressive_balances_mode, &rig.harness.chain.spec, - + rig.harness.logger() ), Err(ForkChoiceError::ProtoArrayStringError(message)) if message.contains(&format!( diff --git a/beacon_node/http_api/src/block_rewards.rs b/beacon_node/http_api/src/block_rewards.rs index 828be8e576..299bc019c4 100644 --- a/beacon_node/http_api/src/block_rewards.rs +++ b/beacon_node/http_api/src/block_rewards.rs @@ -49,7 +49,7 @@ pub fn get_block_rewards( .map_err(beacon_chain_error)?; state - .build_all_caches(&chain.spec) + .build_caches(&chain.spec) .map_err(beacon_state_error)?; let mut reward_cache = Default::default(); diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 206cd3c72f..646356b6cb 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -1,5 +1,6 @@ use clap::{App, Arg}; use strum::VariantNames; +use types::ProgressiveBalancesMode; pub fn cli_app<'a, 'b>() -> App<'a, 'b> { App::new("beacon_node") @@ -1117,4 +1118,17 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { developers. This directory is not pruned, users should be careful to avoid \ filling up their disks.") ) + .arg( + Arg::with_name("progressive-balances") + .long("progressive-balances") + .value_name("MODE") + .help("Options to enable or disable the progressive balances cache for \ + unrealized FFG progression calculation. The default `checked` mode compares \ + the progressive balances from the cache against results from the existing \ + method. If there is a mismatch, it falls back to the existing method. The \ + optimized mode (`fast`) is faster but is still experimental, and is \ + not recommended for mainnet usage at this time.") + .takes_value(true) + .possible_values(ProgressiveBalancesMode::VARIANTS) + ) } diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index c59b297c1b..948c70dd41 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -800,6 +800,12 @@ pub fn get_config( client_config.network.invalid_block_storage = Some(path); } + if let Some(progressive_balances_mode) = + clap_utils::parse_optional(cli_args, "progressive-balances")? + { + client_config.chain.progressive_balances_mode = progressive_balances_mode; + } + Ok(client_config) } diff --git a/beacon_node/store/src/partial_beacon_state.rs b/beacon_node/store/src/partial_beacon_state.rs index cd923da40d..9f2532d0a7 100644 --- a/beacon_node/store/src/partial_beacon_state.rs +++ b/beacon_node/store/src/partial_beacon_state.rs @@ -373,6 +373,7 @@ macro_rules! impl_try_into_beacon_state { // Caching total_active_balance: <_>::default(), + progressive_balances_cache: <_>::default(), committee_caches: <_>::default(), pubkey_cache: <_>::default(), exit_cache: <_>::default(), diff --git a/beacon_node/store/src/reconstruct.rs b/beacon_node/store/src/reconstruct.rs index cd50babdb0..bac5d3cc82 100644 --- a/beacon_node/store/src/reconstruct.rs +++ b/beacon_node/store/src/reconstruct.rs @@ -63,7 +63,7 @@ where .load_cold_state_by_slot(lower_limit_slot)? .ok_or(HotColdDBError::MissingLowerLimitState(lower_limit_slot))?; - state.build_all_caches(&self.spec)?; + state.build_caches(&self.spec)?; process_results(block_root_iter, |iter| -> Result<(), Error> { let mut io_batch = vec![]; diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 5d86f99f1a..e60774fc86 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -1,10 +1,15 @@ use crate::{ForkChoiceStore, InvalidationOperation}; +use per_epoch_processing::altair::participation_cache::Error as ParticipationCacheError; use proto_array::{ Block as ProtoBlock, DisallowedReOrgOffsets, ExecutionStatus, ProposerHeadError, ProposerHeadInfo, ProtoArrayForkChoice, ReOrgThreshold, }; -use slog::{crit, debug, warn, Logger}; +use slog::{crit, debug, error, warn, Logger}; use ssz_derive::{Decode, Encode}; +use state_processing::per_epoch_processing::altair::ParticipationCache; +use state_processing::per_epoch_processing::{ + weigh_justification_and_finalization, JustificationAndFinalizationState, +}; use state_processing::{ per_block_processing::errors::AttesterSlashingValidationError, per_epoch_processing, }; @@ -18,6 +23,7 @@ use types::{ EthSpec, ExecPayload, ExecutionBlockHash, Hash256, IndexedAttestation, RelativeEpoch, SignedBeaconBlock, Slot, }; +use types::{ProgressiveBalancesCache, ProgressiveBalancesMode}; #[derive(Debug)] pub enum Error { @@ -72,7 +78,9 @@ pub enum Error { }, UnrealizedVoteProcessing(state_processing::EpochProcessingError), ParticipationCacheBuild(BeaconStateError), + ParticipationCacheError(ParticipationCacheError), ValidatorStatuses(BeaconStateError), + ProgressiveBalancesCacheCheckFailed(String), } impl From for Error { @@ -93,6 +101,18 @@ impl From for Error { } } +impl From for Error { + fn from(e: BeaconStateError) -> Self { + Error::BeaconStateError(e) + } +} + +impl From for Error { + fn from(e: ParticipationCacheError) -> Self { + Error::ParticipationCacheError(e) + } +} + #[derive(Debug, Clone, Copy)] /// Controls how fork choice should behave when restoring from a persisted fork choice. pub enum ResetPayloadStatuses { @@ -643,7 +663,9 @@ where block_delay: Duration, state: &BeaconState, payload_verification_status: PayloadVerificationStatus, + progressive_balances_mode: ProgressiveBalancesMode, spec: &ChainSpec, + log: &Logger, ) -> Result<(), Error> { // If this block has already been processed we do not need to reprocess it. // We check this immediately in case re-processing the block mutates some property of the @@ -737,43 +759,84 @@ where parent_justified.epoch == block_epoch && parent_finalized.epoch + 1 >= block_epoch }); - let (unrealized_justified_checkpoint, unrealized_finalized_checkpoint) = - if let Some((parent_justified, parent_finalized)) = parent_checkpoints { - (parent_justified, parent_finalized) - } else { - let justification_and_finalization_state = match block { - BeaconBlockRef::Capella(_) - | BeaconBlockRef::Merge(_) - | BeaconBlockRef::Altair(_) => { - let participation_cache = - per_epoch_processing::altair::ParticipationCache::new(state, spec) - .map_err(Error::ParticipationCacheBuild)?; + let (unrealized_justified_checkpoint, unrealized_finalized_checkpoint) = if let Some(( + parent_justified, + parent_finalized, + )) = + parent_checkpoints + { + (parent_justified, parent_finalized) + } else { + let justification_and_finalization_state = match block { + BeaconBlockRef::Capella(_) + | BeaconBlockRef::Merge(_) + | BeaconBlockRef::Altair(_) => match progressive_balances_mode { + ProgressiveBalancesMode::Disabled => { + let participation_cache = ParticipationCache::new(state, spec) + .map_err(Error::ParticipationCacheBuild)?; per_epoch_processing::altair::process_justification_and_finalization( state, &participation_cache, )? } - BeaconBlockRef::Base(_) => { - let mut validator_statuses = - per_epoch_processing::base::ValidatorStatuses::new(state, spec) - .map_err(Error::ValidatorStatuses)?; - validator_statuses - .process_attestations(state) - .map_err(Error::ValidatorStatuses)?; - per_epoch_processing::base::process_justification_and_finalization( - state, - &validator_statuses.total_balances, - spec, - )? - } - }; + ProgressiveBalancesMode::Fast + | ProgressiveBalancesMode::Checked + | ProgressiveBalancesMode::Strict => { + let maybe_participation_cache = progressive_balances_mode + .perform_comparative_checks() + .then(|| { + ParticipationCache::new(state, spec) + .map_err(Error::ParticipationCacheBuild) + }) + .transpose()?; - ( - justification_and_finalization_state.current_justified_checkpoint(), - justification_and_finalization_state.finalized_checkpoint(), - ) + process_justification_and_finalization_from_progressive_cache::( + state, + maybe_participation_cache.as_ref(), + ) + .or_else(|e| { + if progressive_balances_mode != ProgressiveBalancesMode::Strict { + error!( + log, + "Processing with progressive balances cache failed"; + "info" => "falling back to the non-optimized processing method", + "error" => ?e, + ); + let participation_cache = maybe_participation_cache + .map(Ok) + .unwrap_or_else(|| ParticipationCache::new(state, spec)) + .map_err(Error::ParticipationCacheBuild)?; + per_epoch_processing::altair::process_justification_and_finalization( + state, + &participation_cache, + ).map_err(Error::from) + } else { + Err(e) + } + })? + } + }, + BeaconBlockRef::Base(_) => { + let mut validator_statuses = + per_epoch_processing::base::ValidatorStatuses::new(state, spec) + .map_err(Error::ValidatorStatuses)?; + validator_statuses + .process_attestations(state) + .map_err(Error::ValidatorStatuses)?; + per_epoch_processing::base::process_justification_and_finalization( + state, + &validator_statuses.total_balances, + spec, + )? + } }; + ( + justification_and_finalization_state.current_justified_checkpoint(), + justification_and_finalization_state.finalized_checkpoint(), + ) + }; + // Update best known unrealized justified & finalized checkpoints if unrealized_justified_checkpoint.epoch > self.fc_store.unrealized_justified_checkpoint().epoch @@ -1499,6 +1562,92 @@ where } } +/// Process justification and finalization using progressive cache. Also performs a comparative +/// check against the `ParticipationCache` if it is supplied. +/// +/// Returns an error if the cache is not initialized or if there is a mismatch on the comparative check. +fn process_justification_and_finalization_from_progressive_cache( + state: &BeaconState, + maybe_participation_cache: Option<&ParticipationCache>, +) -> Result, Error> +where + E: EthSpec, + T: ForkChoiceStore, +{ + let justification_and_finalization_state = JustificationAndFinalizationState::new(state); + if state.current_epoch() <= E::genesis_epoch() + 1 { + return Ok(justification_and_finalization_state); + } + + // Load cached balances + let progressive_balances_cache: &ProgressiveBalancesCache = state.progressive_balances_cache(); + let previous_target_balance = + progressive_balances_cache.previous_epoch_target_attesting_balance()?; + let current_target_balance = + progressive_balances_cache.current_epoch_target_attesting_balance()?; + let total_active_balance = state.get_total_active_balance()?; + + if let Some(participation_cache) = maybe_participation_cache { + check_progressive_balances::( + state, + participation_cache, + previous_target_balance, + current_target_balance, + total_active_balance, + )?; + } + + weigh_justification_and_finalization( + justification_and_finalization_state, + total_active_balance, + previous_target_balance, + current_target_balance, + ) + .map_err(Error::from) +} + +/// Perform comparative checks against `ParticipationCache`, will return error if there's a mismatch. +fn check_progressive_balances( + state: &BeaconState, + participation_cache: &ParticipationCache, + cached_previous_target_balance: u64, + cached_current_target_balance: u64, + cached_total_active_balance: u64, +) -> Result<(), Error> +where + E: EthSpec, + T: ForkChoiceStore, +{ + let slot = state.slot(); + let epoch = state.current_epoch(); + + // Check previous epoch target balances + let previous_target_balance = participation_cache.previous_epoch_target_attesting_balance()?; + if previous_target_balance != cached_previous_target_balance { + return Err(Error::ProgressiveBalancesCacheCheckFailed( + format!("Previous epoch target attesting balance mismatch, slot: {}, epoch: {}, actual: {}, cached: {}", slot, epoch, previous_target_balance, cached_previous_target_balance) + )); + } + + // Check current epoch target balances + let current_target_balance = participation_cache.current_epoch_target_attesting_balance()?; + if current_target_balance != cached_current_target_balance { + return Err(Error::ProgressiveBalancesCacheCheckFailed( + format!("Current epoch target attesting balance mismatch, slot: {}, epoch: {}, actual: {}, cached: {}", slot, epoch, current_target_balance, cached_current_target_balance) + )); + } + + // Check current epoch total balances + let total_active_balance = participation_cache.current_epoch_total_active_balance(); + if total_active_balance != cached_total_active_balance { + return Err(Error::ProgressiveBalancesCacheCheckFailed( + format!("Current epoch total active balance mismatch, slot: {}, epoch: {}, actual: {}, cached: {}", slot, epoch, total_active_balance, cached_total_active_balance) + )); + } + + Ok(()) +} + /// Helper struct that is used to encode/decode the state of the `ForkChoice` as SSZ bytes. /// /// This is used when persisting the state of the fork choice to disk. diff --git a/consensus/fork_choice/tests/tests.rs b/consensus/fork_choice/tests/tests.rs index ef262b58c0..d28210aa1b 100644 --- a/consensus/fork_choice/tests/tests.rs +++ b/consensus/fork_choice/tests/tests.rs @@ -17,12 +17,13 @@ use fork_choice::{ use store::MemoryStore; use types::{ test_utils::generate_deterministic_keypair, BeaconBlockRef, BeaconState, ChainSpec, Checkpoint, - Epoch, EthSpec, Hash256, IndexedAttestation, MainnetEthSpec, SignedBeaconBlock, Slot, SubnetId, + Epoch, EthSpec, ForkName, Hash256, IndexedAttestation, MainnetEthSpec, ProgressiveBalancesMode, + RelativeEpoch, SignedBeaconBlock, Slot, SubnetId, }; pub type E = MainnetEthSpec; -pub const VALIDATOR_COUNT: usize = 32; +pub const VALIDATOR_COUNT: usize = 64; /// Defines some delay between when an attestation is created and when it is mutated. pub enum MutationDelay { @@ -68,6 +69,24 @@ impl ForkChoiceTest { Self { harness } } + /// Creates a new tester with the specified `ProgressiveBalancesMode` and genesis from latest fork. + fn new_with_progressive_balances_mode(mode: ProgressiveBalancesMode) -> ForkChoiceTest { + // genesis with latest fork (at least altair required to test the cache) + let spec = ForkName::latest().make_genesis_spec(ChainSpec::default()); + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec) + .chain_config(ChainConfig { + progressive_balances_mode: mode, + ..ChainConfig::default() + }) + .deterministic_keypairs(VALIDATOR_COUNT) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + Self { harness } + } + /// Get a value from the `ForkChoice` instantiation. fn get(&self, func: T) -> U where @@ -212,6 +231,39 @@ impl ForkChoiceTest { self } + /// Slash a validator from the previous epoch committee. + pub async fn add_previous_epoch_attester_slashing(self) -> Self { + let state = self.harness.get_current_state(); + let previous_epoch_shuffling = state.get_shuffling(RelativeEpoch::Previous).unwrap(); + let validator_indices = previous_epoch_shuffling + .iter() + .map(|idx| *idx as u64) + .take(1) + .collect(); + + self.harness + .add_attester_slashing(validator_indices) + .unwrap(); + + self + } + + /// Slash the proposer of a block in the previous epoch. + pub async fn add_previous_epoch_proposer_slashing(self, slots_per_epoch: u64) -> Self { + let previous_epoch_slot = self.harness.get_current_slot() - slots_per_epoch; + let previous_epoch_block = self + .harness + .chain + .block_at_slot(previous_epoch_slot, WhenSlotSkipped::None) + .unwrap() + .unwrap(); + let proposer_index: u64 = previous_epoch_block.message().proposer_index(); + + self.harness.add_proposer_slashing(proposer_index).unwrap(); + + self + } + /// Apply `count` blocks to the chain (without attestations). pub async fn apply_blocks_without_new_attestations(self, count: usize) -> Self { self.harness.advance_slot(); @@ -286,7 +338,9 @@ impl ForkChoiceTest { Duration::from_secs(0), &state, PayloadVerificationStatus::Verified, + self.harness.chain.config.progressive_balances_mode, &self.harness.chain.spec, + self.harness.logger(), ) .unwrap(); self @@ -328,7 +382,9 @@ impl ForkChoiceTest { Duration::from_secs(0), &state, PayloadVerificationStatus::Verified, + self.harness.chain.config.progressive_balances_mode, &self.harness.chain.spec, + self.harness.logger(), ) .err() .expect("on_block did not return an error"); @@ -1287,3 +1343,49 @@ async fn weak_subjectivity_check_epoch_boundary_is_skip_slot_failure() { .assert_finalized_epoch_is_less_than(checkpoint.epoch) .assert_shutdown_signal_sent(); } + +/// Checks that `ProgressiveBalancesCache` is updated correctly after an attester slashing event, +/// where the slashed validator is a target attester in previous / current epoch. +#[tokio::test] +async fn progressive_balances_cache_attester_slashing() { + ForkChoiceTest::new_with_progressive_balances_mode(ProgressiveBalancesMode::Strict) + // first two epochs + .apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0) + .await + .unwrap() + .add_previous_epoch_attester_slashing() + .await + // expect fork choice to import blocks successfully after a previous epoch attester is + // slashed, i.e. the slashed attester's balance is correctly excluded from + // the previous epoch total balance in `ProgressiveBalancesCache`. + .apply_blocks(1) + .await + // expect fork choice to import another epoch of blocks successfully - the slashed + // attester's balance should be excluded from the current epoch total balance in + // `ProgressiveBalancesCache` as well. + .apply_blocks(MainnetEthSpec::slots_per_epoch() as usize) + .await; +} + +/// Checks that `ProgressiveBalancesCache` is updated correctly after a proposer slashing event, +/// where the slashed validator is a target attester in previous / current epoch. +#[tokio::test] +async fn progressive_balances_cache_proposer_slashing() { + ForkChoiceTest::new_with_progressive_balances_mode(ProgressiveBalancesMode::Strict) + // first two epochs + .apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0) + .await + .unwrap() + .add_previous_epoch_proposer_slashing(MainnetEthSpec::slots_per_epoch()) + .await + // expect fork choice to import blocks successfully after a previous epoch proposer is + // slashed, i.e. the slashed proposer's balance is correctly excluded from + // the previous epoch total balance in `ProgressiveBalancesCache`. + .apply_blocks(1) + .await + // expect fork choice to import another epoch of blocks successfully - the slashed + // proposer's balance should be excluded from the current epoch total balance in + // `ProgressiveBalancesCache` as well. + .apply_blocks(MainnetEthSpec::slots_per_epoch() as usize) + .await; +} diff --git a/consensus/state_processing/src/common/mod.rs b/consensus/state_processing/src/common/mod.rs index 8a2e2439bb..ffe8be3a04 100644 --- a/consensus/state_processing/src/common/mod.rs +++ b/consensus/state_processing/src/common/mod.rs @@ -7,6 +7,7 @@ mod slash_validator; pub mod altair; pub mod base; +pub mod update_progressive_balances_cache; pub use deposit_data_tree::DepositDataTree; pub use get_attestation_participation::get_attestation_participation_flag_indices; diff --git a/consensus/state_processing/src/common/slash_validator.rs b/consensus/state_processing/src/common/slash_validator.rs index d4675f5ef5..d54da43a04 100644 --- a/consensus/state_processing/src/common/slash_validator.rs +++ b/consensus/state_processing/src/common/slash_validator.rs @@ -1,3 +1,4 @@ +use crate::common::update_progressive_balances_cache::update_progressive_balances_on_slashing; use crate::{ common::{decrease_balance, increase_balance, initiate_validator_exit}, per_block_processing::errors::BlockProcessingError, @@ -43,6 +44,8 @@ pub fn slash_validator( .safe_div(spec.min_slashing_penalty_quotient_for_state(state))?, )?; + update_progressive_balances_on_slashing(state, slashed_index)?; + // Apply proposer and whistleblower rewards let proposer_index = ctxt.get_proposer_index(state, spec)? as usize; let whistleblower_index = opt_whistleblower_index.unwrap_or(proposer_index); diff --git a/consensus/state_processing/src/common/update_progressive_balances_cache.rs b/consensus/state_processing/src/common/update_progressive_balances_cache.rs new file mode 100644 index 0000000000..45b5d657a6 --- /dev/null +++ b/consensus/state_processing/src/common/update_progressive_balances_cache.rs @@ -0,0 +1,142 @@ +/// A collection of all functions that mutates the `ProgressiveBalancesCache`. +use crate::metrics::{ + PARTICIPATION_CURR_EPOCH_TARGET_ATTESTING_GWEI_PROGRESSIVE_TOTAL, + PARTICIPATION_PREV_EPOCH_TARGET_ATTESTING_GWEI_PROGRESSIVE_TOTAL, +}; +use crate::per_epoch_processing::altair::ParticipationCache; +use crate::{BlockProcessingError, EpochProcessingError}; +use lighthouse_metrics::set_gauge; +use ssz_types::VariableList; +use std::borrow::Cow; +use types::consts::altair::TIMELY_TARGET_FLAG_INDEX; +use types::{ + is_progressive_balances_enabled, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, + ParticipationFlags, ProgressiveBalancesCache, +}; + +/// Initializes the `ProgressiveBalancesCache` cache using balance values from the +/// `ParticipationCache`. If the optional `&ParticipationCache` is not supplied, it will be computed +/// from the `BeaconState`. +pub fn initialize_progressive_balances_cache( + state: &mut BeaconState, + maybe_participation_cache: Option<&ParticipationCache>, + spec: &ChainSpec, +) -> Result<(), BeaconStateError> { + if !is_progressive_balances_enabled(state) + || state.progressive_balances_cache().is_initialized() + { + return Ok(()); + } + + let participation_cache = match maybe_participation_cache { + Some(cache) => Cow::Borrowed(cache), + None => Cow::Owned(ParticipationCache::new(state, spec)?), + }; + + let previous_epoch_target_attesting_balance = participation_cache + .previous_epoch_target_attesting_balance_raw() + .map_err(|e| BeaconStateError::ParticipationCacheError(format!("{e:?}")))?; + + let current_epoch_target_attesting_balance = participation_cache + .current_epoch_target_attesting_balance_raw() + .map_err(|e| BeaconStateError::ParticipationCacheError(format!("{e:?}")))?; + + let current_epoch = state.current_epoch(); + state.progressive_balances_cache_mut().initialize( + current_epoch, + previous_epoch_target_attesting_balance, + current_epoch_target_attesting_balance, + ); + + update_progressive_balances_metrics(state.progressive_balances_cache())?; + + Ok(()) +} + +/// Updates the `ProgressiveBalancesCache` when a new target attestation has been processed. +pub fn update_progressive_balances_on_attestation( + state: &mut BeaconState, + epoch: Epoch, + validator_index: usize, +) -> Result<(), BlockProcessingError> { + if is_progressive_balances_enabled(state) { + let validator = state.get_validator(validator_index)?; + if !validator.slashed { + let validator_effective_balance = validator.effective_balance; + state + .progressive_balances_cache_mut() + .on_new_target_attestation(epoch, validator_effective_balance)?; + } + } + Ok(()) +} + +/// Updates the `ProgressiveBalancesCache` when a target attester has been slashed. +pub fn update_progressive_balances_on_slashing( + state: &mut BeaconState, + validator_index: usize, +) -> Result<(), BlockProcessingError> { + if is_progressive_balances_enabled(state) { + let previous_epoch_participation = state.previous_epoch_participation()?; + let is_previous_epoch_target_attester = + is_target_attester_in_epoch::(previous_epoch_participation, validator_index)?; + + let current_epoch_participation = state.current_epoch_participation()?; + let is_current_epoch_target_attester = + is_target_attester_in_epoch::(current_epoch_participation, validator_index)?; + + let validator_effective_balance = state.get_effective_balance(validator_index)?; + + state.progressive_balances_cache_mut().on_slashing( + is_previous_epoch_target_attester, + is_current_epoch_target_attester, + validator_effective_balance, + )?; + } + + Ok(()) +} + +/// Updates the `ProgressiveBalancesCache` on epoch transition. +pub fn update_progressive_balances_on_epoch_transition( + state: &mut BeaconState, + spec: &ChainSpec, +) -> Result<(), EpochProcessingError> { + if is_progressive_balances_enabled(state) { + state + .progressive_balances_cache_mut() + .on_epoch_transition(spec)?; + + update_progressive_balances_metrics(state.progressive_balances_cache())?; + } + + Ok(()) +} + +pub fn update_progressive_balances_metrics( + cache: &ProgressiveBalancesCache, +) -> Result<(), BeaconStateError> { + set_gauge( + &PARTICIPATION_PREV_EPOCH_TARGET_ATTESTING_GWEI_PROGRESSIVE_TOTAL, + cache.previous_epoch_target_attesting_balance()? as i64, + ); + + set_gauge( + &PARTICIPATION_CURR_EPOCH_TARGET_ATTESTING_GWEI_PROGRESSIVE_TOTAL, + cache.current_epoch_target_attesting_balance()? as i64, + ); + + Ok(()) +} + +fn is_target_attester_in_epoch( + epoch_participation: &VariableList, + validator_index: usize, +) -> Result { + let participation_flags = epoch_participation + .get(validator_index) + .ok_or(BeaconStateError::UnknownValidator(validator_index))?; + participation_flags + .has_flag(TIMELY_TARGET_FLAG_INDEX) + .map_err(|e| e.into()) +} diff --git a/consensus/state_processing/src/genesis.rs b/consensus/state_processing/src/genesis.rs index 68f04b554e..ebbc8f9f31 100644 --- a/consensus/state_processing/src/genesis.rs +++ b/consensus/state_processing/src/genesis.rs @@ -92,7 +92,7 @@ pub fn initialize_beacon_state_from_eth1( } // Now that we have our validators, initialize the caches (including the committees) - state.build_all_caches(spec)?; + state.build_caches(spec)?; // Set genesis validators root for domain separation and chain versioning *state.genesis_validators_root_mut() = state.update_validators_tree_hash_cache()?; @@ -115,7 +115,7 @@ pub fn process_activations( state: &mut BeaconState, spec: &ChainSpec, ) -> Result<(), Error> { - let (validators, balances) = state.validators_and_balances_mut(); + let (validators, balances, _) = state.validators_and_balances_and_progressive_balances_mut(); for (index, validator) in validators.iter_mut().enumerate() { let balance = balances .get(index) diff --git a/consensus/state_processing/src/metrics.rs b/consensus/state_processing/src/metrics.rs index ddfaae5640..360b007678 100644 --- a/consensus/state_processing/src/metrics.rs +++ b/consensus/state_processing/src/metrics.rs @@ -23,4 +23,15 @@ lazy_static! { "beacon_participation_prev_epoch_active_gwei_total", "Total effective balance (gwei) of validators active in the previous epoch" ); + /* + * Participation Metrics (progressive balances) + */ + pub static ref PARTICIPATION_PREV_EPOCH_TARGET_ATTESTING_GWEI_PROGRESSIVE_TOTAL: Result = try_create_int_gauge( + "beacon_participation_prev_epoch_target_attesting_gwei_progressive_total", + "Progressive total effective balance (gwei) of validators who attested to the target in the previous epoch" + ); + pub static ref PARTICIPATION_CURR_EPOCH_TARGET_ATTESTING_GWEI_PROGRESSIVE_TOTAL: Result = try_create_int_gauge( + "beacon_participation_curr_epoch_target_attesting_gwei_progressive_total", + "Progressive total effective balance (gwei) of validators who attested to the target in the current epoch" + ); } diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index 124fdf6500..b8b76a499d 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -41,6 +41,9 @@ mod verify_proposer_slashing; use crate::common::decrease_balance; use crate::StateProcessingStrategy; +use crate::common::update_progressive_balances_cache::{ + initialize_progressive_balances_cache, update_progressive_balances_metrics, +}; #[cfg(feature = "arbitrary-fuzz")] use arbitrary::Arbitrary; @@ -114,6 +117,8 @@ pub fn per_block_processing>( .fork_name(spec) .map_err(BlockProcessingError::InconsistentStateFork)?; + initialize_progressive_balances_cache(state, None, spec)?; + let verify_signatures = match block_signature_strategy { BlockSignatureStrategy::VerifyBulk => { // Verify all signatures in the block at once. @@ -182,6 +187,10 @@ pub fn per_block_processing>( )?; } + if is_progressive_balances_enabled(state) { + update_progressive_balances_metrics(state.progressive_balances_cache())?; + } + Ok(()) } diff --git a/consensus/state_processing/src/per_block_processing/errors.rs b/consensus/state_processing/src/per_block_processing/errors.rs index 1aaf298d69..0aba1d83fa 100644 --- a/consensus/state_processing/src/per_block_processing/errors.rs +++ b/consensus/state_processing/src/per_block_processing/errors.rs @@ -1,6 +1,8 @@ use super::signature_sets::Error as SignatureSetError; +use crate::per_epoch_processing::altair::participation_cache; use crate::ContextError; use merkle_proof::MerkleTreeError; +use participation_cache::Error as ParticipationCacheError; use safe_arith::ArithError; use ssz::DecodeError; use types::*; @@ -83,6 +85,7 @@ pub enum BlockProcessingError { found: Hash256, }, WithdrawalCredentialsInvalid, + ParticipationCacheError(ParticipationCacheError), } impl From for BlockProcessingError { @@ -140,6 +143,12 @@ impl From> for BlockProcessingError { } } +impl From for BlockProcessingError { + fn from(e: ParticipationCacheError) -> Self { + BlockProcessingError::ParticipationCacheError(e) + } +} + /// A conversion that consumes `self` and adds an `index` variable to resulting struct. /// /// Used here to allow converting an error into an upstream error that points to the object that diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index 4bee596615..1dbcb7fb8f 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -97,6 +97,8 @@ pub mod base { pub mod altair { use super::*; + use crate::common::update_progressive_balances_cache::update_progressive_balances_on_attestation; + use types::consts::altair::TIMELY_TARGET_FLAG_INDEX; pub fn process_attestations( state: &mut BeaconState, @@ -163,6 +165,14 @@ pub mod altair { get_base_reward(state, index, base_reward_per_increment, spec)? .safe_mul(weight)?, )?; + + if flag_index == TIMELY_TARGET_FLAG_INDEX { + update_progressive_balances_on_attestation( + state, + data.target.epoch, + index, + )?; + } } } } @@ -235,6 +245,7 @@ pub fn process_attester_slashings( Ok(()) } + /// Wrapper function to handle calling the correct version of `process_attestations` based on /// the fork. pub fn process_attestations>( diff --git a/consensus/state_processing/src/per_epoch_processing/altair.rs b/consensus/state_processing/src/per_epoch_processing/altair.rs index d5df2fc975..0abbd16a98 100644 --- a/consensus/state_processing/src/per_epoch_processing/altair.rs +++ b/consensus/state_processing/src/per_epoch_processing/altair.rs @@ -1,4 +1,7 @@ use super::{process_registry_updates, process_slashings, EpochProcessingSummary, Error}; +use crate::common::update_progressive_balances_cache::{ + initialize_progressive_balances_cache, update_progressive_balances_on_epoch_transition, +}; use crate::per_epoch_processing::{ effective_balance_updates::process_effective_balance_updates, historical_roots_update::process_historical_roots_update, @@ -31,6 +34,7 @@ pub fn process_epoch( // Pre-compute participating indices and total balances. let participation_cache = ParticipationCache::new(state, spec)?; let sync_committee = state.current_sync_committee()?.clone(); + initialize_progressive_balances_cache::(state, Some(&participation_cache), spec)?; // Justification and finalization. let justification_and_finalization_state = @@ -56,7 +60,7 @@ pub fn process_epoch( process_eth1_data_reset(state)?; // Update effective balances with hysteresis (lag). - process_effective_balance_updates(state, spec)?; + process_effective_balance_updates(state, Some(&participation_cache), spec)?; // Reset slashings process_slashings_reset(state)?; @@ -75,6 +79,8 @@ pub fn process_epoch( // Rotate the epoch caches to suit the epoch transition. state.advance_caches(spec)?; + update_progressive_balances_on_epoch_transition(state, spec)?; + Ok(EpochProcessingSummary::Altair { participation_cache, sync_committee, diff --git a/consensus/state_processing/src/per_epoch_processing/altair/participation_cache.rs b/consensus/state_processing/src/per_epoch_processing/altair/participation_cache.rs index 004726923e..a5caddd045 100644 --- a/consensus/state_processing/src/per_epoch_processing/altair/participation_cache.rs +++ b/consensus/state_processing/src/per_epoch_processing/altair/participation_cache.rs @@ -11,49 +11,23 @@ //! Additionally, this cache is returned from the `altair::process_epoch` function and can be used //! to get useful summaries about the validator participation in an epoch. -use safe_arith::{ArithError, SafeArith}; use types::{ consts::altair::{ NUM_FLAG_INDICES, TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX, }, - BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, ParticipationFlags, RelativeEpoch, + Balance, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, ParticipationFlags, + RelativeEpoch, }; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub enum Error { InvalidFlagIndex(usize), InvalidValidatorIndex(usize), } -/// A balance which will never be below the specified `minimum`. -/// -/// This is an effort to ensure the `EFFECTIVE_BALANCE_INCREMENT` minimum is always respected. -#[derive(PartialEq, Debug, Clone, Copy)] -struct Balance { - raw: u64, - minimum: u64, -} - -impl Balance { - /// Initialize the balance to `0`, or the given `minimum`. - pub fn zero(minimum: u64) -> Self { - Self { raw: 0, minimum } - } - - /// Returns the balance with respect to the initialization `minimum`. - pub fn get(&self) -> u64 { - std::cmp::max(self.raw, self.minimum) - } - - /// Add-assign to the balance. - pub fn safe_add_assign(&mut self, other: u64) -> Result<(), ArithError> { - self.raw.safe_add_assign(other) - } -} - /// Caches the participation values for one epoch (either the previous or current). -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Clone)] struct SingleEpochParticipationCache { /// Maps an active validator index to their participation flags. /// @@ -95,6 +69,14 @@ impl SingleEpochParticipationCache { .ok_or(Error::InvalidFlagIndex(flag_index)) } + /// Returns the raw total balance of attesters who have `flag_index` set. + fn total_flag_balance_raw(&self, flag_index: usize) -> Result { + self.total_flag_balances + .get(flag_index) + .copied() + .ok_or(Error::InvalidFlagIndex(flag_index)) + } + /// Returns `true` if `val_index` is active, unslashed and has `flag_index` set. /// /// ## Errors @@ -173,7 +155,7 @@ impl SingleEpochParticipationCache { } /// Maintains a cache to be used during `altair::process_epoch`. -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Clone)] pub struct ParticipationCache { current_epoch: Epoch, /// Caches information about active validators pertaining to `self.current_epoch`. @@ -291,6 +273,11 @@ impl ParticipationCache { .total_flag_balance(TIMELY_TARGET_FLAG_INDEX) } + pub fn current_epoch_target_attesting_balance_raw(&self) -> Result { + self.current_epoch_participation + .total_flag_balance_raw(TIMELY_TARGET_FLAG_INDEX) + } + pub fn previous_epoch_total_active_balance(&self) -> u64 { self.previous_epoch_participation.total_active_balance.get() } @@ -300,6 +287,11 @@ impl ParticipationCache { .total_flag_balance(TIMELY_TARGET_FLAG_INDEX) } + pub fn previous_epoch_target_attesting_balance_raw(&self) -> Result { + self.previous_epoch_participation + .total_flag_balance_raw(TIMELY_TARGET_FLAG_INDEX) + } + pub fn previous_epoch_source_attesting_balance(&self) -> Result { self.previous_epoch_participation .total_flag_balance(TIMELY_SOURCE_FLAG_INDEX) diff --git a/consensus/state_processing/src/per_epoch_processing/base.rs b/consensus/state_processing/src/per_epoch_processing/base.rs index cb7e7d4b30..680563ce74 100644 --- a/consensus/state_processing/src/per_epoch_processing/base.rs +++ b/consensus/state_processing/src/per_epoch_processing/base.rs @@ -52,7 +52,7 @@ pub fn process_epoch( process_eth1_data_reset(state)?; // Update effective balances with hysteresis (lag). - process_effective_balance_updates(state, spec)?; + process_effective_balance_updates(state, None, spec)?; // Reset slashings process_slashings_reset(state)?; diff --git a/consensus/state_processing/src/per_epoch_processing/capella.rs b/consensus/state_processing/src/per_epoch_processing/capella.rs index aaf301f29e..911510ed0c 100644 --- a/consensus/state_processing/src/per_epoch_processing/capella.rs +++ b/consensus/state_processing/src/per_epoch_processing/capella.rs @@ -11,6 +11,9 @@ use crate::per_epoch_processing::{ }; use types::{BeaconState, ChainSpec, EthSpec, RelativeEpoch}; +use crate::common::update_progressive_balances_cache::{ + initialize_progressive_balances_cache, update_progressive_balances_on_epoch_transition, +}; pub use historical_summaries_update::process_historical_summaries_update; mod historical_summaries_update; @@ -27,6 +30,7 @@ pub fn process_epoch( // Pre-compute participating indices and total balances. let participation_cache = ParticipationCache::new(state, spec)?; let sync_committee = state.current_sync_committee()?.clone(); + initialize_progressive_balances_cache(state, Some(&participation_cache), spec)?; // Justification and finalization. let justification_and_finalization_state = @@ -52,7 +56,7 @@ pub fn process_epoch( process_eth1_data_reset(state)?; // Update effective balances with hysteresis (lag). - process_effective_balance_updates(state, spec)?; + process_effective_balance_updates(state, Some(&participation_cache), spec)?; // Reset slashings process_slashings_reset(state)?; @@ -71,6 +75,8 @@ pub fn process_epoch( // Rotate the epoch caches to suit the epoch transition. state.advance_caches(spec)?; + update_progressive_balances_on_epoch_transition(state, spec)?; + Ok(EpochProcessingSummary::Altair { participation_cache, sync_committee, diff --git a/consensus/state_processing/src/per_epoch_processing/effective_balance_updates.rs b/consensus/state_processing/src/per_epoch_processing/effective_balance_updates.rs index c166667b5a..1759f7e140 100644 --- a/consensus/state_processing/src/per_epoch_processing/effective_balance_updates.rs +++ b/consensus/state_processing/src/per_epoch_processing/effective_balance_updates.rs @@ -1,11 +1,13 @@ use super::errors::EpochProcessingError; +use crate::per_epoch_processing::altair::ParticipationCache; use safe_arith::SafeArith; use types::beacon_state::BeaconState; use types::chain_spec::ChainSpec; -use types::{BeaconStateError, EthSpec}; +use types::{BeaconStateError, EthSpec, ProgressiveBalancesCache}; pub fn process_effective_balance_updates( state: &mut BeaconState, + maybe_participation_cache: Option<&ParticipationCache>, spec: &ChainSpec, ) -> Result<(), EpochProcessingError> { let hysteresis_increment = spec @@ -13,7 +15,8 @@ pub fn process_effective_balance_updates( .safe_div(spec.hysteresis_quotient)?; let downward_threshold = hysteresis_increment.safe_mul(spec.hysteresis_downward_multiplier)?; let upward_threshold = hysteresis_increment.safe_mul(spec.hysteresis_upward_multiplier)?; - let (validators, balances) = state.validators_and_balances_mut(); + let (validators, balances, progressive_balances_cache) = + state.validators_and_balances_and_progressive_balances_mut(); for (index, validator) in validators.iter_mut().enumerate() { let balance = balances .get(index) @@ -23,11 +26,43 @@ pub fn process_effective_balance_updates( if balance.safe_add(downward_threshold)? < validator.effective_balance || validator.effective_balance.safe_add(upward_threshold)? < balance { - validator.effective_balance = std::cmp::min( + let old_effective_balance = validator.effective_balance; + let new_effective_balance = std::cmp::min( balance.safe_sub(balance.safe_rem(spec.effective_balance_increment)?)?, spec.max_effective_balance, ); + + if let Some(participation_cache) = maybe_participation_cache { + update_progressive_balances( + participation_cache, + progressive_balances_cache, + index, + old_effective_balance, + new_effective_balance, + )?; + } + + validator.effective_balance = new_effective_balance; } } Ok(()) } + +fn update_progressive_balances( + participation_cache: &ParticipationCache, + progressive_balances_cache: &mut ProgressiveBalancesCache, + index: usize, + old_effective_balance: u64, + new_effective_balance: u64, +) -> Result<(), EpochProcessingError> { + if old_effective_balance != new_effective_balance { + let is_current_epoch_target_attester = + participation_cache.is_current_epoch_timely_target_attester(index)?; + progressive_balances_cache.on_effective_balance_change( + is_current_epoch_target_attester, + old_effective_balance, + new_effective_balance, + )?; + } + Ok(()) +} diff --git a/consensus/state_processing/src/per_epoch_processing/slashings.rs b/consensus/state_processing/src/per_epoch_processing/slashings.rs index 6d5342cd36..2d595491c1 100644 --- a/consensus/state_processing/src/per_epoch_processing/slashings.rs +++ b/consensus/state_processing/src/per_epoch_processing/slashings.rs @@ -16,7 +16,7 @@ pub fn process_slashings( total_balance, ); - let (validators, balances) = state.validators_and_balances_mut(); + let (validators, balances, _) = state.validators_and_balances_and_progressive_balances_mut(); for (index, validator) in validators.iter().enumerate() { if validator.slashed && epoch.safe_add(T::EpochsPerSlashingsVector::to_u64().safe_div(2)?)? diff --git a/consensus/state_processing/src/upgrade/altair.rs b/consensus/state_processing/src/upgrade/altair.rs index 176f1af15c..26b1192bc1 100644 --- a/consensus/state_processing/src/upgrade/altair.rs +++ b/consensus/state_processing/src/upgrade/altair.rs @@ -1,3 +1,4 @@ +use crate::common::update_progressive_balances_cache::initialize_progressive_balances_cache; use crate::common::{get_attestation_participation_flag_indices, get_attesting_indices}; use std::mem; use std::sync::Arc; @@ -101,6 +102,7 @@ pub fn upgrade_to_altair( next_sync_committee: temp_sync_committee, // not read // Caches total_active_balance: pre.total_active_balance, + progressive_balances_cache: mem::take(&mut pre.progressive_balances_cache), committee_caches: mem::take(&mut pre.committee_caches), pubkey_cache: mem::take(&mut pre.pubkey_cache), exit_cache: mem::take(&mut pre.exit_cache), @@ -110,6 +112,8 @@ pub fn upgrade_to_altair( // Fill in previous epoch participation from the pre state's pending attestations. translate_participation(&mut post, &pre.previous_epoch_attestations, spec)?; + initialize_progressive_balances_cache(&mut post, None, spec)?; + // Fill in sync committees // Note: A duplicate committee is assigned for the current and next committee at the fork // boundary diff --git a/consensus/state_processing/src/upgrade/capella.rs b/consensus/state_processing/src/upgrade/capella.rs index 3b933fac37..5153e35f44 100644 --- a/consensus/state_processing/src/upgrade/capella.rs +++ b/consensus/state_processing/src/upgrade/capella.rs @@ -62,6 +62,7 @@ pub fn upgrade_to_capella( historical_summaries: VariableList::default(), // Caches total_active_balance: pre.total_active_balance, + progressive_balances_cache: mem::take(&mut pre.progressive_balances_cache), committee_caches: mem::take(&mut pre.committee_caches), pubkey_cache: mem::take(&mut pre.pubkey_cache), exit_cache: mem::take(&mut pre.exit_cache), diff --git a/consensus/state_processing/src/upgrade/merge.rs b/consensus/state_processing/src/upgrade/merge.rs index c172466248..eb74450107 100644 --- a/consensus/state_processing/src/upgrade/merge.rs +++ b/consensus/state_processing/src/upgrade/merge.rs @@ -60,6 +60,7 @@ pub fn upgrade_to_bellatrix( latest_execution_payload_header: >::default(), // Caches total_active_balance: pre.total_active_balance, + progressive_balances_cache: mem::take(&mut pre.progressive_balances_cache), committee_caches: mem::take(&mut pre.committee_caches), pubkey_cache: mem::take(&mut pre.pubkey_cache), exit_cache: mem::take(&mut pre.exit_cache), diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index 583b940d5f..ba15f6d488 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -52,6 +52,7 @@ serde_json = "1.0.74" smallvec = "1.8.0" serde_with = "1.13.0" maplit = "1.0.2" +strum = { version = "0.24.0", features = ["derive"] } [dev-dependencies] criterion = "0.3.3" diff --git a/consensus/types/benches/benches.rs b/consensus/types/benches/benches.rs index 28f57e7080..bb2b527109 100644 --- a/consensus/types/benches/benches.rs +++ b/consensus/types/benches/benches.rs @@ -51,7 +51,7 @@ fn all_benches(c: &mut Criterion) { let spec = Arc::new(MainnetEthSpec::default_spec()); let mut state = get_state::(validator_count); - state.build_all_caches(&spec).expect("should build caches"); + state.build_caches(&spec).expect("should build caches"); let state_bytes = state.as_ssz_bytes(); let inner_state = state.clone(); diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 4a9da36404..1fa4dee3a0 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -26,6 +26,8 @@ pub use self::committee_cache::{ compute_committee_index_in_epoch, compute_committee_range_in_epoch, epoch_committee_count, CommitteeCache, }; +pub use crate::beacon_state::balance::Balance; +pub use crate::beacon_state::progressive_balances_cache::*; use crate::historical_summary::HistoricalSummary; pub use clone_config::CloneConfig; pub use eth_spec::*; @@ -34,9 +36,11 @@ pub use tree_hash_cache::BeaconTreeHashCache; #[macro_use] mod committee_cache; +mod balance; mod clone_config; mod exit_cache; mod iter; +mod progressive_balances_cache; mod pubkey_cache; mod tests; mod tree_hash_cache; @@ -101,6 +105,9 @@ pub enum Error { SszTypesError(ssz_types::Error), TreeHashCacheNotInitialized, NonLinearTreeHashCacheHistory, + ParticipationCacheError(String), + ProgressiveBalancesCacheNotInitialized, + ProgressiveBalancesCacheInconsistent, TreeHashCacheSkippedSlot { cache: Slot, state: Slot, @@ -317,6 +324,12 @@ where #[tree_hash(skip_hashing)] #[test_random(default)] #[derivative(Clone(clone_with = "clone_default"))] + pub progressive_balances_cache: ProgressiveBalancesCache, + #[serde(skip_serializing, skip_deserializing)] + #[ssz(skip_serializing, skip_deserializing)] + #[tree_hash(skip_hashing)] + #[test_random(default)] + #[derivative(Clone(clone_with = "clone_default"))] pub committee_caches: [CommitteeCache; CACHED_EPOCHS], #[serde(skip_serializing, skip_deserializing)] #[ssz(skip_serializing, skip_deserializing)] @@ -393,6 +406,7 @@ impl BeaconState { // Caching (not in spec) total_active_balance: None, + progressive_balances_cache: <_>::default(), committee_caches: [ CommitteeCache::default(), CommitteeCache::default(), @@ -757,7 +771,7 @@ impl BeaconState { Ok(signature_hash_int.safe_rem(modulo)? == 0) } - /// Returns the beacon proposer index for the `slot` in the given `relative_epoch`. + /// Returns the beacon proposer index for the `slot` in `self.current_epoch()`. /// /// Spec v0.12.1 pub fn get_beacon_proposer_index(&self, slot: Slot, spec: &ChainSpec) -> Result { @@ -1150,12 +1164,30 @@ impl BeaconState { } /// Convenience accessor for validators and balances simultaneously. - pub fn validators_and_balances_mut(&mut self) -> (&mut [Validator], &mut [u64]) { + pub fn validators_and_balances_and_progressive_balances_mut( + &mut self, + ) -> (&mut [Validator], &mut [u64], &mut ProgressiveBalancesCache) { match self { - BeaconState::Base(state) => (&mut state.validators, &mut state.balances), - BeaconState::Altair(state) => (&mut state.validators, &mut state.balances), - BeaconState::Merge(state) => (&mut state.validators, &mut state.balances), - BeaconState::Capella(state) => (&mut state.validators, &mut state.balances), + BeaconState::Base(state) => ( + &mut state.validators, + &mut state.balances, + &mut state.progressive_balances_cache, + ), + BeaconState::Altair(state) => ( + &mut state.validators, + &mut state.balances, + &mut state.progressive_balances_cache, + ), + BeaconState::Merge(state) => ( + &mut state.validators, + &mut state.balances, + &mut state.progressive_balances_cache, + ), + BeaconState::Capella(state) => ( + &mut state.validators, + &mut state.balances, + &mut state.progressive_balances_cache, + ), } } @@ -1380,7 +1412,7 @@ impl BeaconState { } /// Build all caches (except the tree hash cache), if they need to be built. - pub fn build_all_caches(&mut self, spec: &ChainSpec) -> Result<(), Error> { + pub fn build_caches(&mut self, spec: &ChainSpec) -> Result<(), Error> { self.build_all_committee_caches(spec)?; self.update_pubkey_cache()?; self.build_exit_cache(spec)?; @@ -1412,6 +1444,7 @@ impl BeaconState { self.drop_committee_cache(RelativeEpoch::Next)?; self.drop_pubkey_cache(); self.drop_tree_hash_cache(); + self.drop_progressive_balances_cache(); *self.exit_cache_mut() = ExitCache::default(); Ok(()) } @@ -1608,6 +1641,11 @@ impl BeaconState { *self.pubkey_cache_mut() = PubkeyCache::default() } + /// Completely drops the `progressive_balances_cache` cache, replacing it with a new, empty cache. + fn drop_progressive_balances_cache(&mut self) { + *self.progressive_balances_cache_mut() = ProgressiveBalancesCache::default(); + } + /// Initialize but don't fill the tree hash cache, if it isn't already initialized. pub fn initialize_tree_hash_cache(&mut self) { if !self.tree_hash_cache().is_initialized() { @@ -1679,6 +1717,9 @@ impl BeaconState { if config.tree_hash_cache { *res.tree_hash_cache_mut() = self.tree_hash_cache().clone(); } + if config.progressive_balances_cache { + *res.progressive_balances_cache_mut() = self.progressive_balances_cache().clone(); + } res } diff --git a/consensus/types/src/beacon_state/balance.rs b/consensus/types/src/beacon_state/balance.rs new file mode 100644 index 0000000000..e537a5b984 --- /dev/null +++ b/consensus/types/src/beacon_state/balance.rs @@ -0,0 +1,33 @@ +use arbitrary::Arbitrary; +use safe_arith::{ArithError, SafeArith}; + +/// A balance which will never be below the specified `minimum`. +/// +/// This is an effort to ensure the `EFFECTIVE_BALANCE_INCREMENT` minimum is always respected. +#[derive(PartialEq, Debug, Clone, Copy, Arbitrary)] +pub struct Balance { + raw: u64, + minimum: u64, +} + +impl Balance { + /// Initialize the balance to `0`, or the given `minimum`. + pub fn zero(minimum: u64) -> Self { + Self { raw: 0, minimum } + } + + /// Returns the balance with respect to the initialization `minimum`. + pub fn get(&self) -> u64 { + std::cmp::max(self.raw, self.minimum) + } + + /// Add-assign to the balance. + pub fn safe_add_assign(&mut self, other: u64) -> Result<(), ArithError> { + self.raw.safe_add_assign(other) + } + + /// Sub-assign to the balance. + pub fn safe_sub_assign(&mut self, other: u64) -> Result<(), ArithError> { + self.raw.safe_sub_assign(other) + } +} diff --git a/consensus/types/src/beacon_state/clone_config.rs b/consensus/types/src/beacon_state/clone_config.rs index e5f050aee6..c6e7f47421 100644 --- a/consensus/types/src/beacon_state/clone_config.rs +++ b/consensus/types/src/beacon_state/clone_config.rs @@ -5,6 +5,7 @@ pub struct CloneConfig { pub pubkey_cache: bool, pub exit_cache: bool, pub tree_hash_cache: bool, + pub progressive_balances_cache: bool, } impl CloneConfig { @@ -14,6 +15,7 @@ impl CloneConfig { pubkey_cache: true, exit_cache: true, tree_hash_cache: true, + progressive_balances_cache: true, } } diff --git a/consensus/types/src/beacon_state/progressive_balances_cache.rs b/consensus/types/src/beacon_state/progressive_balances_cache.rs new file mode 100644 index 0000000000..9f5c223d57 --- /dev/null +++ b/consensus/types/src/beacon_state/progressive_balances_cache.rs @@ -0,0 +1,184 @@ +use crate::beacon_state::balance::Balance; +use crate::{BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec}; +use arbitrary::Arbitrary; +use safe_arith::SafeArith; +use serde_derive::{Deserialize, Serialize}; +use strum::{Display, EnumString, EnumVariantNames}; + +/// This cache keeps track of the accumulated target attestation balance for the current & previous +/// epochs. The cached values can be utilised by fork choice to calculate unrealized justification +/// and finalization instead of converting epoch participation arrays to balances for each block we +/// process. +#[derive(Default, Debug, PartialEq, Arbitrary, Clone)] +pub struct ProgressiveBalancesCache { + inner: Option, +} + +#[derive(Debug, PartialEq, Arbitrary, Clone)] +struct Inner { + pub current_epoch: Epoch, + pub previous_epoch_target_attesting_balance: Balance, + pub current_epoch_target_attesting_balance: Balance, +} + +impl ProgressiveBalancesCache { + pub fn initialize( + &mut self, + current_epoch: Epoch, + previous_epoch_target_attesting_balance: Balance, + current_epoch_target_attesting_balance: Balance, + ) { + self.inner = Some(Inner { + current_epoch, + previous_epoch_target_attesting_balance, + current_epoch_target_attesting_balance, + }); + } + + pub fn is_initialized(&self) -> bool { + self.inner.is_some() + } + + /// When a new target attestation has been processed, we update the cached + /// `current_epoch_target_attesting_balance` to include the validator effective balance. + /// If the epoch is neither the current epoch nor the previous epoch, an error is returned. + pub fn on_new_target_attestation( + &mut self, + epoch: Epoch, + validator_effective_balance: u64, + ) -> Result<(), BeaconStateError> { + let cache = self.get_inner_mut()?; + + if epoch == cache.current_epoch { + cache + .current_epoch_target_attesting_balance + .safe_add_assign(validator_effective_balance)?; + } else if epoch.safe_add(1)? == cache.current_epoch { + cache + .previous_epoch_target_attesting_balance + .safe_add_assign(validator_effective_balance)?; + } else { + return Err(BeaconStateError::ProgressiveBalancesCacheInconsistent); + } + + Ok(()) + } + + /// When a validator is slashed, we reduce the `current_epoch_target_attesting_balance` by the + /// validator's effective balance to exclude the validator weight. + pub fn on_slashing( + &mut self, + is_previous_epoch_target_attester: bool, + is_current_epoch_target_attester: bool, + effective_balance: u64, + ) -> Result<(), BeaconStateError> { + let cache = self.get_inner_mut()?; + if is_previous_epoch_target_attester { + cache + .previous_epoch_target_attesting_balance + .safe_sub_assign(effective_balance)?; + } + if is_current_epoch_target_attester { + cache + .current_epoch_target_attesting_balance + .safe_sub_assign(effective_balance)?; + } + Ok(()) + } + + /// When a current epoch target attester has its effective balance changed, we adjust the + /// its share of the target attesting balance in the cache. + pub fn on_effective_balance_change( + &mut self, + is_current_epoch_target_attester: bool, + old_effective_balance: u64, + new_effective_balance: u64, + ) -> Result<(), BeaconStateError> { + let cache = self.get_inner_mut()?; + if is_current_epoch_target_attester { + if new_effective_balance > old_effective_balance { + cache + .current_epoch_target_attesting_balance + .safe_add_assign(new_effective_balance.safe_sub(old_effective_balance)?)?; + } else { + cache + .current_epoch_target_attesting_balance + .safe_sub_assign(old_effective_balance.safe_sub(new_effective_balance)?)?; + } + } + Ok(()) + } + + /// On epoch transition, the balance from current epoch is shifted to previous epoch, and the + /// current epoch balance is reset to 0. + pub fn on_epoch_transition(&mut self, spec: &ChainSpec) -> Result<(), BeaconStateError> { + let cache = self.get_inner_mut()?; + cache.current_epoch.safe_add_assign(1)?; + cache.previous_epoch_target_attesting_balance = + cache.current_epoch_target_attesting_balance; + cache.current_epoch_target_attesting_balance = + Balance::zero(spec.effective_balance_increment); + Ok(()) + } + + pub fn previous_epoch_target_attesting_balance(&self) -> Result { + Ok(self + .get_inner()? + .previous_epoch_target_attesting_balance + .get()) + } + + pub fn current_epoch_target_attesting_balance(&self) -> Result { + Ok(self + .get_inner()? + .current_epoch_target_attesting_balance + .get()) + } + + fn get_inner_mut(&mut self) -> Result<&mut Inner, BeaconStateError> { + self.inner + .as_mut() + .ok_or(BeaconStateError::ProgressiveBalancesCacheNotInitialized) + } + + fn get_inner(&self) -> Result<&Inner, BeaconStateError> { + self.inner + .as_ref() + .ok_or(BeaconStateError::ProgressiveBalancesCacheNotInitialized) + } +} + +#[derive( + Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize, Display, EnumString, EnumVariantNames, +)] +#[strum(serialize_all = "lowercase")] +pub enum ProgressiveBalancesMode { + /// Disable the usage of progressive cache, and use the existing `ParticipationCache` calculation. + Disabled, + /// Enable the usage of progressive cache, with checks against the `ParticipationCache` and falls + /// back to the existing calculation if there is a balance mismatch. + Checked, + /// Enable the usage of progressive cache, with checks against the `ParticipationCache`. Errors + /// if there is a balance mismatch. Used in testing only. + Strict, + /// Enable the usage of progressive cache, with no comparative checks against the + /// `ParticipationCache`. This is fast but an experimental mode, use with caution. + Fast, +} + +impl ProgressiveBalancesMode { + pub fn perform_comparative_checks(&self) -> bool { + match self { + Self::Disabled | Self::Fast => false, + Self::Checked | Self::Strict => true, + } + } +} + +/// `ProgressiveBalancesCache` is only enabled from `Altair` as it requires `ParticipationCache`. +pub fn is_progressive_balances_enabled(state: &BeaconState) -> bool { + match state { + BeaconState::Base(_) => false, + BeaconState::Altair(_) | BeaconState::Merge(_) | BeaconState::Capella(_) => true, + } +} diff --git a/consensus/types/src/beacon_state/tests.rs b/consensus/types/src/beacon_state/tests.rs index d63eaafc4b..6cd9c1dbf8 100644 --- a/consensus/types/src/beacon_state/tests.rs +++ b/consensus/types/src/beacon_state/tests.rs @@ -219,17 +219,18 @@ async fn clone_config() { let mut state = build_state::(16).await; - state.build_all_caches(&spec).unwrap(); + state.build_caches(&spec).unwrap(); state .update_tree_hash_cache() .expect("should update tree hash cache"); - let num_caches = 4; + let num_caches = 5; let all_configs = (0..2u8.pow(num_caches)).map(|i| CloneConfig { committee_caches: (i & 1) != 0, pubkey_cache: ((i >> 1) & 1) != 0, exit_cache: ((i >> 2) & 1) != 0, tree_hash_cache: ((i >> 3) & 1) != 0, + progressive_balances_cache: ((i >> 4) & 1) != 0, }); for config in all_configs { diff --git a/lcli/src/new_testnet.rs b/lcli/src/new_testnet.rs index aa5f52eef8..01a44cabef 100644 --- a/lcli/src/new_testnet.rs +++ b/lcli/src/new_testnet.rs @@ -303,7 +303,7 @@ fn initialize_state_with_validators( } // Now that we have our validators, initialize the caches (including the committees) - state.build_all_caches(spec).unwrap(); + state.build_caches(spec).unwrap(); // Set genesis validators root for domain separation and chain versioning *state.genesis_validators_root_mut() = state.update_validators_tree_hash_cache().unwrap(); diff --git a/lcli/src/skip_slots.rs b/lcli/src/skip_slots.rs index 49d1dd424d..e3b2a5acbf 100644 --- a/lcli/src/skip_slots.rs +++ b/lcli/src/skip_slots.rs @@ -109,7 +109,7 @@ pub fn run(env: Environment, matches: &ArgMatches) -> Result<(), let target_slot = initial_slot + slots; state - .build_all_caches(spec) + .build_caches(spec) .map_err(|e| format!("Unable to build caches: {:?}", e))?; let state_root = if let Some(root) = cli_state_root.or(state_root) { diff --git a/lcli/src/transition_blocks.rs b/lcli/src/transition_blocks.rs index cf971c69f0..34a4560761 100644 --- a/lcli/src/transition_blocks.rs +++ b/lcli/src/transition_blocks.rs @@ -205,7 +205,7 @@ pub fn run(env: Environment, matches: &ArgMatches) -> Result<(), if config.exclude_cache_builds { pre_state - .build_all_caches(spec) + .build_caches(spec) .map_err(|e| format!("Unable to build caches: {:?}", e))?; let state_root = pre_state .update_tree_hash_cache() @@ -303,7 +303,7 @@ fn do_transition( if !config.exclude_cache_builds { let t = Instant::now(); pre_state - .build_all_caches(spec) + .build_caches(spec) .map_err(|e| format!("Unable to build caches: {:?}", e))?; debug!("Build caches: {:?}", t.elapsed()); @@ -335,7 +335,7 @@ fn do_transition( let t = Instant::now(); pre_state - .build_all_caches(spec) + .build_caches(spec) .map_err(|e| format!("Unable to build caches: {:?}", e))?; debug!("Build all caches (again): {:?}", t.elapsed()); diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 65d7bd08b2..ac0780015f 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -16,7 +16,10 @@ use std::str::FromStr; use std::string::ToString; use std::time::Duration; use tempfile::TempDir; -use types::{Address, Checkpoint, Epoch, ExecutionBlockHash, ForkName, Hash256, MainnetEthSpec}; +use types::{ + Address, Checkpoint, Epoch, ExecutionBlockHash, ForkName, Hash256, MainnetEthSpec, + ProgressiveBalancesMode, +}; use unused_port::{unused_tcp4_port, unused_tcp6_port, unused_udp4_port, unused_udp6_port}; const DEFAULT_ETH1_ENDPOINT: &str = "http://localhost:8545/"; @@ -2284,3 +2287,28 @@ fn invalid_gossip_verified_blocks_path() { ) }); } + +#[test] +fn progressive_balances_default() { + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| { + assert_eq!( + config.chain.progressive_balances_mode, + ProgressiveBalancesMode::Checked + ) + }); +} + +#[test] +fn progressive_balances_fast() { + CommandLineTest::new() + .flag("progressive-balances", Some("fast")) + .run_with_zero_port() + .with_config(|config| { + assert_eq!( + config.chain.progressive_balances_mode, + ProgressiveBalancesMode::Fast + ) + }); +} diff --git a/testing/ef_tests/src/cases/epoch_processing.rs b/testing/ef_tests/src/cases/epoch_processing.rs index 6095e1be6b..31542ba447 100644 --- a/testing/ef_tests/src/cases/epoch_processing.rs +++ b/testing/ef_tests/src/cases/epoch_processing.rs @@ -6,9 +6,9 @@ use crate::type_name; use crate::type_name::TypeName; use serde_derive::Deserialize; use state_processing::per_epoch_processing::capella::process_historical_summaries_update; +use state_processing::per_epoch_processing::effective_balance_updates::process_effective_balance_updates; use state_processing::per_epoch_processing::{ altair, base, - effective_balance_updates::process_effective_balance_updates, historical_roots_update::process_historical_roots_update, process_registry_updates, process_slashings, resets::{process_eth1_data_reset, process_randao_mixes_reset, process_slashings_reset}, @@ -173,7 +173,7 @@ impl EpochTransition for Eth1DataReset { impl EpochTransition for EffectiveBalanceUpdates { fn run(state: &mut BeaconState, spec: &ChainSpec) -> Result<(), EpochProcessingError> { - process_effective_balance_updates(state, spec) + process_effective_balance_updates(state, None, spec) } } diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 65528de175..9627d2cde0 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -18,7 +18,8 @@ use std::sync::Arc; use std::time::Duration; use types::{ Attestation, AttesterSlashing, BeaconBlock, BeaconState, Checkpoint, EthSpec, - ExecutionBlockHash, ForkName, Hash256, IndexedAttestation, SignedBeaconBlock, Slot, Uint256, + ExecutionBlockHash, ForkName, Hash256, IndexedAttestation, ProgressiveBalancesMode, + SignedBeaconBlock, Slot, Uint256, }; #[derive(Default, Debug, PartialEq, Clone, Deserialize, Decode)] @@ -440,7 +441,9 @@ impl Tester { block_delay, &state, PayloadVerificationStatus::Irrelevant, + ProgressiveBalancesMode::Strict, &self.harness.chain.spec, + self.harness.logger(), ); if result.is_ok() { diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index 5fd00285aa..21a56dcf2a 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -4,6 +4,7 @@ use crate::case_result::compare_beacon_state_results_without_caches; use crate::decode::{ssz_decode_file, ssz_decode_file_with, ssz_decode_state, yaml_decode_file}; use crate::testing_spec; use serde_derive::Deserialize; +use state_processing::common::update_progressive_balances_cache::initialize_progressive_balances_cache; use state_processing::{ per_block_processing::{ errors::BlockProcessingError, @@ -96,6 +97,7 @@ impl Operation for Attestation { spec, ), BeaconState::Altair(_) | BeaconState::Merge(_) | BeaconState::Capella(_) => { + initialize_progressive_balances_cache(state, None, spec)?; altair::process_attestation(state, self, 0, &mut ctxt, VerifySignatures::True, spec) } } @@ -118,6 +120,7 @@ impl Operation for AttesterSlashing { _: &Operations, ) -> Result<(), BlockProcessingError> { let mut ctxt = ConsensusContext::new(state.slot()); + initialize_progressive_balances_cache(state, None, spec)?; process_attester_slashings( state, &[self.clone()], @@ -168,6 +171,7 @@ impl Operation for ProposerSlashing { _: &Operations, ) -> Result<(), BlockProcessingError> { let mut ctxt = ConsensusContext::new(state.slot()); + initialize_progressive_balances_cache(state, None, spec)?; process_proposer_slashings( state, &[self.clone()], diff --git a/testing/ef_tests/src/cases/sanity_blocks.rs b/testing/ef_tests/src/cases/sanity_blocks.rs index e51fed1907..191b45c33a 100644 --- a/testing/ef_tests/src/cases/sanity_blocks.rs +++ b/testing/ef_tests/src/cases/sanity_blocks.rs @@ -67,7 +67,7 @@ impl Case for SanityBlocks { let spec = &testing_spec::(fork_name); // Processing requires the epoch cache. - bulk_state.build_all_caches(spec).unwrap(); + bulk_state.build_caches(spec).unwrap(); // Spawning a second state to call the VerifyIndiviual strategy to avoid bitrot. // See https://github.com/sigp/lighthouse/issues/742. diff --git a/testing/ef_tests/src/cases/sanity_slots.rs b/testing/ef_tests/src/cases/sanity_slots.rs index a38a8930a0..dd385d13f4 100644 --- a/testing/ef_tests/src/cases/sanity_slots.rs +++ b/testing/ef_tests/src/cases/sanity_slots.rs @@ -61,7 +61,7 @@ impl Case for SanitySlots { let spec = &testing_spec::(fork_name); // Processing requires the epoch cache. - state.build_all_caches(spec).unwrap(); + state.build_caches(spec).unwrap(); let mut result = (0..self.slots) .try_for_each(|_| per_slot_processing(&mut state, None, spec).map(|_| ())) From 8e65419455577bb4d4f3fdeb045085a58874d24d Mon Sep 17 00:00:00 2001 From: Age Manning Date: Mon, 3 Jul 2023 03:20:21 +0000 Subject: [PATCH 62/63] Ipv6 bootnodes update (#4394) We now officially have ipv6 support. The mainnet bootnodes have been updated to support ipv6. This PR updates lighthouse's internal bootnodes for mainnet to avoid fetching them on initial load. --- .../mainnet/boot_enr.yaml | 8 ++-- lighthouse/tests/beacon_node.rs | 39 +------------------ 2 files changed, 7 insertions(+), 40 deletions(-) diff --git a/common/eth2_network_config/built_in_network_configs/mainnet/boot_enr.yaml b/common/eth2_network_config/built_in_network_configs/mainnet/boot_enr.yaml index 196629cb8d..428a082cc0 100644 --- a/common/eth2_network_config/built_in_network_configs/mainnet/boot_enr.yaml +++ b/common/eth2_network_config/built_in_network_configs/mainnet/boot_enr.yaml @@ -1,6 +1,8 @@ # Lighthouse Team (Sigma Prime) -- enr:-Jq4QItoFUuug_n_qbYbU0OY04-np2wT8rUCauOOXNi0H3BWbDj-zbfZb7otA7jZ6flbBpx1LNZK2TDebZ9dEKx84LYBhGV0aDKQtTA_KgEAAAD__________4JpZIJ2NIJpcISsaa0ZiXNlY3AyNTZrMaEDHAD2JKYevx89W0CcFJFiskdcEzkH_Wdv9iW42qLK79ODdWRwgiMo -- enr:-Jq4QN_YBsUOqQsty1OGvYv48PMaiEt1AzGD1NkYQHaxZoTyVGqMYXg0K9c0LPNWC9pkXmggApp8nygYLsQwScwAgfgBhGV0aDKQtTA_KgEAAAD__________4JpZIJ2NIJpcISLosQxiXNlY3AyNTZrMaEDBJj7_dLFACaxBfaI8KZTh_SSJUjhyAyfshimvSqo22WDdWRwgiMo +- enr:-Le4QPUXJS2BTORXxyx2Ia-9ae4YqA_JWX3ssj4E_J-3z1A-HmFGrU8BpvpqhNabayXeOZ2Nq_sbeDgtzMJpLLnXFgAChGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISsaa0Zg2lwNpAkAIkHAAAAAPA8kv_-awoTiXNlY3AyNTZrMaEDHAD2JKYevx89W0CcFJFiskdcEzkH_Wdv9iW42qLK79ODdWRwgiMohHVkcDaCI4I +- enr:-Le4QLHZDSvkLfqgEo8IWGG96h6mxwe_PsggC20CL3neLBjfXLGAQFOPSltZ7oP6ol54OvaNqO02Rnvb8YmDR274uq8ChGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISLosQxg2lwNpAqAX4AAAAAAPA8kv_-ax65iXNlY3AyNTZrMaEDBJj7_dLFACaxBfaI8KZTh_SSJUjhyAyfshimvSqo22WDdWRwgiMohHVkcDaCI4I +- enr:-Le4QH6LQrusDbAHPjU_HcKOuMeXfdEB5NJyXgHWFadfHgiySqeDyusQMvfphdYWOzuSZO9Uq2AMRJR5O4ip7OvVma8BhGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISLY9ncg2lwNpAkAh8AgQIBAAAAAAAAAAmXiXNlY3AyNTZrMaECDYCZTZEksF-kmgPholqgVt8IXr-8L7Nu7YrZ7HUpgxmDdWRwgiMohHVkcDaCI4I +- enr:-Le4QIqLuWybHNONr933Lk0dcMmAB5WgvGKRyDihy1wHDIVlNuuztX62W51voT4I8qD34GcTEOTmag1bcdZ_8aaT4NUBhGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISLY04ng2lwNpAkAh8AgAIBAAAAAAAAAA-fiXNlY3AyNTZrMaEDscnRV6n1m-D9ID5UsURk0jsoKNXt1TIrj8uKOGW6iluDdWRwgiMohHVkcDaCI4I # EF Team - enr:-Ku4QHqVeJ8PPICcWk1vSn_XcSkjOkNiTg6Fmii5j6vUQgvzMc9L1goFnLKgXqBJspJjIsB91LTOleFmyWWrFVATGngBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhAMRHkWJc2VjcDI1NmsxoQKLVXFOhp2uX6jeT0DvvDpPcU8FWMjQdR4wMuORMhpX24N1ZHCCIyg - enr:-Ku4QG-2_Md3sZIAUebGYT6g0SMskIml77l6yR-M_JXc-UdNHCmHQeOiMLbylPejyJsdAPsTHJyjJB2sYGDLe0dn8uYBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhBLY-NyJc2VjcDI1NmsxoQORcM6e19T1T9gi7jxEZjk_sjVLGFscUNqAY9obgZaxbIN1ZHCCIyg @@ -15,4 +17,4 @@ - enr:-Ku4QPp9z1W4tAO8Ber_NQierYaOStqhDqQdOPY3bB3jDgkjcbk6YrEnVYIiCBbTxuar3CzS528d2iE7TdJsrL-dEKoBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQMw5fqqkw2hHC4F5HZZDPsNmPdB1Gi8JPQK7pRc9XHh-oN1ZHCCKvg # Nimbus team - enr:-LK4QA8FfhaAjlb_BXsXxSfiysR7R52Nhi9JBt4F8SPssu8hdE1BXQQEtVDC3qStCW60LSO7hEsVHv5zm8_6Vnjhcn0Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhAN4aBKJc2VjcDI1NmsxoQJerDhsJ-KxZ8sHySMOCmTO6sHM3iCFQ6VMvLTe948MyYN0Y3CCI4yDdWRwgiOM -- enr:-LK4QKWrXTpV9T78hNG6s8AM6IO4XH9kFT91uZtFg1GcsJ6dKovDOr1jtAAFPnS2lvNltkOGA9k29BUN7lFh_sjuc9QBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhANAdd-Jc2VjcDI1NmsxoQLQa6ai7y9PMN5hpLe5HmiJSlYzMuzP7ZhwRiwHvqNXdoN0Y3CCI4yDdWRwgiOM +- enr:-LK4QKWrXTpV9T78hNG6s8AM6IO4XH9kFT91uZtFg1GcsJ6dKovDOr1jtAAFPnS2lvNltkOGA9k29BUN7lFh_sjuc9QBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhANAdd-Jc2VjcDI1NmsxoQLQa6ai7y9PMN5hpLe5HmiJSlYzMuzP7ZhwRiwHvqNXdoN0Y3CCI4yDdWRwgiOM \ No newline at end of file diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index ac0780015f..9b6d23ddcf 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -1128,48 +1128,13 @@ fn default_backfill_rate_limiting_flag() { } #[test] fn default_boot_nodes() { - let mainnet = vec![ - // Lighthouse Team (Sigma Prime) - "enr:-Jq4QItoFUuug_n_qbYbU0OY04-np2wT8rUCauOOXNi0H3BWbDj-zbfZb7otA7jZ6flbBpx1LNZK2TDebZ9dEKx84LYBhGV0aDKQtTA_KgEAAAD__________4JpZIJ2NIJpcISsaa0ZiXNlY3AyNTZrMaEDHAD2JKYevx89W0CcFJFiskdcEzkH_Wdv9iW42qLK79ODdWRwgiMo", - "enr:-Jq4QN_YBsUOqQsty1OGvYv48PMaiEt1AzGD1NkYQHaxZoTyVGqMYXg0K9c0LPNWC9pkXmggApp8nygYLsQwScwAgfgBhGV0aDKQtTA_KgEAAAD__________4JpZIJ2NIJpcISLosQxiXNlY3AyNTZrMaEDBJj7_dLFACaxBfaI8KZTh_SSJUjhyAyfshimvSqo22WDdWRwgiMo", - // EF Team - "enr:-Ku4QHqVeJ8PPICcWk1vSn_XcSkjOkNiTg6Fmii5j6vUQgvzMc9L1goFnLKgXqBJspJjIsB91LTOleFmyWWrFVATGngBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhAMRHkWJc2VjcDI1NmsxoQKLVXFOhp2uX6jeT0DvvDpPcU8FWMjQdR4wMuORMhpX24N1ZHCCIyg", - "enr:-Ku4QG-2_Md3sZIAUebGYT6g0SMskIml77l6yR-M_JXc-UdNHCmHQeOiMLbylPejyJsdAPsTHJyjJB2sYGDLe0dn8uYBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhBLY-NyJc2VjcDI1NmsxoQORcM6e19T1T9gi7jxEZjk_sjVLGFscUNqAY9obgZaxbIN1ZHCCIyg", - "enr:-Ku4QPn5eVhcoF1opaFEvg1b6JNFD2rqVkHQ8HApOKK61OIcIXD127bKWgAtbwI7pnxx6cDyk_nI88TrZKQaGMZj0q0Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhDayLMaJc2VjcDI1NmsxoQK2sBOLGcUb4AwuYzFuAVCaNHA-dy24UuEKkeFNgCVCsIN1ZHCCIyg", - "enr:-Ku4QEWzdnVtXc2Q0ZVigfCGggOVB2Vc1ZCPEc6j21NIFLODSJbvNaef1g4PxhPwl_3kax86YPheFUSLXPRs98vvYsoBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhDZBrP2Jc2VjcDI1NmsxoQM6jr8Rb1ktLEsVcKAPa08wCsKUmvoQ8khiOl_SLozf9IN1ZHCCIyg", - // Teku team (Consensys) - "enr:-KG4QOtcP9X1FbIMOe17QNMKqDxCpm14jcX5tiOE4_TyMrFqbmhPZHK_ZPG2Gxb1GE2xdtodOfx9-cgvNtxnRyHEmC0ghGV0aDKQ9aX9QgAAAAD__________4JpZIJ2NIJpcIQDE8KdiXNlY3AyNTZrMaEDhpehBDbZjM_L9ek699Y7vhUJ-eAdMyQW_Fil522Y0fODdGNwgiMog3VkcIIjKA", - "enr:-KG4QDyytgmE4f7AnvW-ZaUOIi9i79qX4JwjRAiXBZCU65wOfBu-3Nb5I7b_Rmg3KCOcZM_C3y5pg7EBU5XGrcLTduQEhGV0aDKQ9aX9QgAAAAD__________4JpZIJ2NIJpcIQ2_DUbiXNlY3AyNTZrMaEDKnz_-ps3UUOfHWVYaskI5kWYO_vtYMGYCQRAR3gHDouDdGNwgiMog3VkcIIjKA", - // Prysm team (Prysmatic Labs) - "enr:-Ku4QImhMc1z8yCiNJ1TyUxdcfNucje3BGwEHzodEZUan8PherEo4sF7pPHPSIB1NNuSg5fZy7qFsjmUKs2ea1Whi0EBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQOVphkDqal4QzPMksc5wnpuC3gvSC8AfbFOnZY_On34wIN1ZHCCIyg", - "enr:-Ku4QP2xDnEtUXIjzJ_DhlCRN9SN99RYQPJL92TMlSv7U5C1YnYLjwOQHgZIUXw6c-BvRg2Yc2QsZxxoS_pPRVe0yK8Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQMeFF5GrS7UZpAH2Ly84aLK-TyvH-dRo0JM1i8yygH50YN1ZHCCJxA", - "enr:-Ku4QPp9z1W4tAO8Ber_NQierYaOStqhDqQdOPY3bB3jDgkjcbk6YrEnVYIiCBbTxuar3CzS528d2iE7TdJsrL-dEKoBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQMw5fqqkw2hHC4F5HZZDPsNmPdB1Gi8JPQK7pRc9XHh-oN1ZHCCKvg", - // Nimbus team - "enr:-LK4QA8FfhaAjlb_BXsXxSfiysR7R52Nhi9JBt4F8SPssu8hdE1BXQQEtVDC3qStCW60LSO7hEsVHv5zm8_6Vnjhcn0Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhAN4aBKJc2VjcDI1NmsxoQJerDhsJ-KxZ8sHySMOCmTO6sHM3iCFQ6VMvLTe948MyYN0Y3CCI4yDdWRwgiOM", - "enr:-LK4QKWrXTpV9T78hNG6s8AM6IO4XH9kFT91uZtFg1GcsJ6dKovDOr1jtAAFPnS2lvNltkOGA9k29BUN7lFh_sjuc9QBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhANAdd-Jc2VjcDI1NmsxoQLQa6ai7y9PMN5hpLe5HmiJSlYzMuzP7ZhwRiwHvqNXdoN0Y3CCI4yDdWRwgiOM" - ]; + let number_of_boot_nodes = 15; CommandLineTest::new() .run_with_zero_port() .with_config(|config| { // Lighthouse Team (Sigma Prime) - assert_eq!(config.network.boot_nodes_enr[0].to_base64(), mainnet[0]); - assert_eq!(config.network.boot_nodes_enr[1].to_base64(), mainnet[1]); - // EF Team - assert_eq!(config.network.boot_nodes_enr[2].to_base64(), mainnet[2]); - assert_eq!(config.network.boot_nodes_enr[3].to_base64(), mainnet[3]); - assert_eq!(config.network.boot_nodes_enr[4].to_base64(), mainnet[4]); - assert_eq!(config.network.boot_nodes_enr[5].to_base64(), mainnet[5]); - // Teku team (Consensys) - assert_eq!(config.network.boot_nodes_enr[6].to_base64(), mainnet[6]); - assert_eq!(config.network.boot_nodes_enr[7].to_base64(), mainnet[7]); - // Prysm team (Prysmatic Labs) - assert_eq!(config.network.boot_nodes_enr[8].to_base64(), mainnet[8]); - assert_eq!(config.network.boot_nodes_enr[9].to_base64(), mainnet[9]); - assert_eq!(config.network.boot_nodes_enr[10].to_base64(), mainnet[10]); - // Nimbus team - assert_eq!(config.network.boot_nodes_enr[11].to_base64(), mainnet[11]); - assert_eq!(config.network.boot_nodes_enr[12].to_base64(), mainnet[12]); + assert_eq!(config.network.boot_nodes_enr.len(), number_of_boot_nodes); }); } #[test] From dfcb3363c757671eb19d5f8e519b4b94ac74677a Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 4 Jul 2023 13:29:55 +0000 Subject: [PATCH 63/63] Release v4.3.0 (#4452) ## Issue Addressed NA ## Proposed Changes Bump versions ## Additional Info NA --- Cargo.lock | 8 ++++---- beacon_node/Cargo.toml | 2 +- boot_node/Cargo.toml | 2 +- common/lighthouse_version/src/lib.rs | 4 ++-- lcli/Cargo.toml | 2 +- lighthouse/Cargo.toml | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index efc6a5d6ab..e360bdd62e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -668,7 +668,7 @@ dependencies = [ [[package]] name = "beacon_node" -version = "4.2.0" +version = "4.3.0" dependencies = [ "beacon_chain", "clap", @@ -847,7 +847,7 @@ dependencies = [ [[package]] name = "boot_node" -version = "4.2.0" +version = "4.3.0" dependencies = [ "beacon_node", "clap", @@ -4022,7 +4022,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lcli" -version = "4.2.0" +version = "4.3.0" dependencies = [ "account_utils", "beacon_chain", @@ -4674,7 +4674,7 @@ dependencies = [ [[package]] name = "lighthouse" -version = "4.2.0" +version = "4.3.0" dependencies = [ "account_manager", "account_utils", diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index 67bb2e5e1d..7c74365418 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "beacon_node" -version = "4.2.0" +version = "4.3.0" authors = ["Paul Hauner ", "Age Manning "] edition = "2021" diff --git a/common/lighthouse_version/src/lib.rs b/common/lighthouse_version/src/lib.rs index 3f2745bf90..e874432fbc 100644 --- a/common/lighthouse_version/src/lib.rs +++ b/common/lighthouse_version/src/lib.rs @@ -17,8 +17,8 @@ pub const VERSION: &str = git_version!( // NOTE: using --match instead of --exclude for compatibility with old Git "--match=thiswillnevermatchlol" ], - prefix = "Lighthouse/v4.2.0-", - fallback = "Lighthouse/v4.2.0" + prefix = "Lighthouse/v4.3.0-", + fallback = "Lighthouse/v4.3.0" ); /// Returns `VERSION`, but with platform information appended to the end. diff --git a/lcli/Cargo.toml b/lcli/Cargo.toml index b4d1baba4a..f9d0a6a31c 100644 --- a/lcli/Cargo.toml +++ b/lcli/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "lcli" description = "Lighthouse CLI (modeled after zcli)" -version = "4.2.0" +version = "4.3.0" authors = ["Paul Hauner "] edition = "2021" diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index bbde006efc..e7746a2db9 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lighthouse" -version = "4.2.0" +version = "4.3.0" authors = ["Sigma Prime "] edition = "2021" autotests = false