From b9b3ea70de6e78a586a6860ac32f4f3a9ccb62a6 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Mon, 24 Jan 2022 22:33:02 +0000 Subject: [PATCH 01/23] Fix metric name for monitoring (#2950) ## Issue Addressed Resolves #2949 ## Proposed Changes Fix metric naming for libp2p peer count. --- common/monitoring_api/src/gather.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/common/monitoring_api/src/gather.rs b/common/monitoring_api/src/gather.rs index 16965f43cd..8699a8cf2c 100644 --- a/common/monitoring_api/src/gather.rs +++ b/common/monitoring_api/src/gather.rs @@ -67,11 +67,7 @@ const BEACON_PROCESS_METRICS: &[JsonMetric] = &[ "disk_beaconchain_bytes_total", JsonType::Integer, ), - JsonMetric::new( - "libp2p_peer_connected_peers_total", - "network_peers_connected", - JsonType::Integer, - ), + JsonMetric::new("libp2p_peers", "network_peers_connected", JsonType::Integer), JsonMetric::new( "libp2p_outbound_bytes", "network_libp2p_bytes_total_transmit", From 69288f6164154c870bfeff69ff27dfc6f9fbadb3 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 24 Jan 2022 22:33:04 +0000 Subject: [PATCH 02/23] VC: don't warn if BN config doesn't match exactly (#2952) ## Proposed Changes Remove the check for exact equality on the beacon node spec when polling `/config/spec` from the VC. This check was always overzealous, and mostly served to check that the BN was configured for upcoming forks. I've replaced it by explicit checks of the `altair_fork_epoch` and `bellatrix_fork_epoch` instead. ## Additional Info We should come back to this and clean it up so that we can retain compatibility while removing the field `default`s we installed. --- Cargo.lock | 4 ++-- validator_client/src/beacon_node_fallback.rs | 19 ++++++++----------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bbf8de27e0..e16f4996ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5994,9 +5994,9 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" dependencies = [ "once_cell", ] diff --git a/validator_client/src/beacon_node_fallback.rs b/validator_client/src/beacon_node_fallback.rs index 487b5744d0..18780c3092 100644 --- a/validator_client/src/beacon_node_fallback.rs +++ b/validator_client/src/beacon_node_fallback.rs @@ -253,22 +253,19 @@ impl CandidateBeaconNode { "our_genesis_fork" => ?spec.genesis_fork_version, ); return Err(CandidateError::Incompatible); - } else if *spec != beacon_node_spec { + } else if beacon_node_spec.altair_fork_epoch != spec.altair_fork_epoch { warn!( log, - "Beacon node config does not match exactly"; + "Beacon node has mismatched Altair fork epoch"; "endpoint" => %self.beacon_node, - "advice" => "check that the BN is updated and configured for any upcoming forks", + "endpoint_altair_fork_epoch" => ?beacon_node_spec.altair_fork_epoch, ); - debug!( + } else if beacon_node_spec.bellatrix_fork_epoch != spec.bellatrix_fork_epoch { + warn!( log, - "Beacon node config"; - "config" => ?beacon_node_spec, - ); - debug!( - log, - "Our config"; - "config" => ?spec, + "Beacon node has mismatched Bellatrix fork epoch"; + "endpoint" => %self.beacon_node, + "endpoint_bellatrix_fork_epoch" => ?beacon_node_spec.bellatrix_fork_epoch, ); } From 5f628a71d4b2a7e7761b30e726338bba07617cd2 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 25 Jan 2022 00:46:24 +0000 Subject: [PATCH 03/23] v2.1.1 (#2951) ## Issue Addressed NA ## Proposed Changes - Bump Lighthouse version to v2.1.1 - Update `thread_local` from v1.1.3 to v1.1.4 to address https://rustsec.org/advisories/RUSTSEC-2022-0006 ## Additional Info - ~~Blocked on #2950~~ - ~~Blocked on #2952~~ --- Cargo.lock | 8 ++++---- beacon_node/Cargo.toml | 2 +- boot_node/Cargo.toml | 2 +- common/lighthouse_version/src/lib.rs | 2 +- lcli/Cargo.toml | 2 +- lighthouse/Cargo.toml | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e16f4996ff..586cdaf181 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -331,7 +331,7 @@ dependencies = [ [[package]] name = "beacon_node" -version = "2.1.0" +version = "2.1.1" dependencies = [ "beacon_chain", "clap", @@ -497,7 +497,7 @@ dependencies = [ [[package]] name = "boot_node" -version = "2.1.0" +version = "2.1.1" dependencies = [ "beacon_node", "clap", @@ -2825,7 +2825,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lcli" -version = "2.1.0" +version = "2.1.1" dependencies = [ "account_utils", "bls", @@ -3350,7 +3350,7 @@ dependencies = [ [[package]] name = "lighthouse" -version = "2.1.0" +version = "2.1.1" dependencies = [ "account_manager", "account_utils", diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index eecef0349e..c8cd5152af 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "beacon_node" -version = "2.1.0" +version = "2.1.1" authors = ["Paul Hauner ", "Age Manning "] edition = "2018" diff --git a/common/lighthouse_version/src/lib.rs b/common/lighthouse_version/src/lib.rs index 6f2baf132c..a66ff66e5c 100644 --- a/common/lighthouse_version/src/lib.rs +++ b/common/lighthouse_version/src/lib.rs @@ -16,7 +16,7 @@ pub const VERSION: &str = git_version!( // NOTE: using --match instead of --exclude for compatibility with old Git "--match=thiswillnevermatchlol" ], - prefix = "Lighthouse/v2.1.0-", + prefix = "Lighthouse/v2.1.1-", fallback = "unknown" ); diff --git a/lcli/Cargo.toml b/lcli/Cargo.toml index a6062e5b8c..2b9541de3f 100644 --- a/lcli/Cargo.toml +++ b/lcli/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "lcli" description = "Lighthouse CLI (modeled after zcli)" -version = "2.1.0" +version = "2.1.1" authors = ["Paul Hauner "] edition = "2018" diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 787b992a22..130322e0e9 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lighthouse" -version = "2.1.0" +version = "2.1.1" authors = ["Sigma Prime "] edition = "2018" autotests = false From 150931950dd2dd531d0643314ae9e3c46503cdf3 Mon Sep 17 00:00:00 2001 From: Akihito Nakano Date: Wed, 26 Jan 2022 23:14:20 +0000 Subject: [PATCH 04/23] Fix errors from local testnet scripts on MacOS (#2919) ## Issue Addressed Resolves https://github.com/sigp/lighthouse/issues/2763 ## Proposed Changes - Add a workflow which tests that local testnet starts successfully - Added `set` option into the scripts in order to fail fast so that we can notice errors during starting local testnet. - Fix errors on MacOS - The redirect `&>>` is supported since bash v4 but the version bundled in macOS(11.6.1) is v3. https://github.com/sigp/lighthouse/pull/2919/commits/a54f119c9b1839fd0909792d219858e727e120a2 --- .github/workflows/local-testnet.yml | 50 ++++++++++++++++++++ scripts/local_testnet/beacon_node.sh | 2 + scripts/local_testnet/bootnode.sh | 2 + scripts/local_testnet/clean.sh | 2 + scripts/local_testnet/ganache_test_node.sh | 2 + scripts/local_testnet/kill_processes.sh | 2 + scripts/local_testnet/print_logs.sh | 17 +++++++ scripts/local_testnet/reset_genesis_time.sh | 2 + scripts/local_testnet/start_local_testnet.sh | 8 ++-- scripts/local_testnet/stop_local_testnet.sh | 2 + scripts/local_testnet/validator_client.sh | 2 + scripts/local_testnet/vars.env | 3 ++ 12 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/local-testnet.yml create mode 100755 scripts/local_testnet/print_logs.sh diff --git a/.github/workflows/local-testnet.yml b/.github/workflows/local-testnet.yml new file mode 100644 index 0000000000..f97b271c35 --- /dev/null +++ b/.github/workflows/local-testnet.yml @@ -0,0 +1,50 @@ +# Test that local testnet starts successfully. +name: local testnet + +on: + push: + branches: + - unstable + pull_request: + +jobs: + run-local-testnet: + strategy: + matrix: + os: + - ubuntu-18.04 + - macos-latest + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v1 + + - name: Install ganache + run: npm install ganache-cli@latest --global + + # https://github.com/actions/cache/blob/main/examples.md#rust---cargo + - uses: actions/cache@v2 + id: cache-cargo + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Install lighthouse + if: steps.cache-cargo.outputs.cache-hit != 'true' + run: make && make install-lcli + + - name: Start local testnet + run: ./start_local_testnet.sh + working-directory: scripts/local_testnet + + - name: Print logs + run: ./print_logs.sh + working-directory: scripts/local_testnet + + - name: Stop local testnet + run: ./stop_local_testnet.sh + working-directory: scripts/local_testnet diff --git a/scripts/local_testnet/beacon_node.sh b/scripts/local_testnet/beacon_node.sh index 883c666029..8151aac249 100755 --- a/scripts/local_testnet/beacon_node.sh +++ b/scripts/local_testnet/beacon_node.sh @@ -4,6 +4,8 @@ # Starts a beacon node based upon a genesis state created by `./setup.sh`. # +set -Eeuo pipefail + source ./vars.env SUBSCRIBE_ALL_SUBNETS= diff --git a/scripts/local_testnet/bootnode.sh b/scripts/local_testnet/bootnode.sh index bef207a694..ca02a24140 100755 --- a/scripts/local_testnet/bootnode.sh +++ b/scripts/local_testnet/bootnode.sh @@ -5,6 +5,8 @@ # Starts a bootnode from the generated enr. # +set -Eeuo pipefail + source ./vars.env echo "Generating bootnode enr" diff --git a/scripts/local_testnet/clean.sh b/scripts/local_testnet/clean.sh index bc4db74c61..b01b1a2dff 100755 --- a/scripts/local_testnet/clean.sh +++ b/scripts/local_testnet/clean.sh @@ -4,6 +4,8 @@ # Deletes all files associated with the local testnet. # +set -Eeuo pipefail + source ./vars.env if [ -d $DATADIR ]; then diff --git a/scripts/local_testnet/ganache_test_node.sh b/scripts/local_testnet/ganache_test_node.sh index 762700dbd6..69edc1e770 100755 --- a/scripts/local_testnet/ganache_test_node.sh +++ b/scripts/local_testnet/ganache_test_node.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +set -Eeuo pipefail + source ./vars.env exec ganache-cli \ diff --git a/scripts/local_testnet/kill_processes.sh b/scripts/local_testnet/kill_processes.sh index c729a1645a..4f52a5f256 100755 --- a/scripts/local_testnet/kill_processes.sh +++ b/scripts/local_testnet/kill_processes.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash # Kill processes +set -Eeuo pipefail + # First parameter is the file with # one pid per line. if [ -f "$1" ]; then diff --git a/scripts/local_testnet/print_logs.sh b/scripts/local_testnet/print_logs.sh new file mode 100755 index 0000000000..2a9e7822a6 --- /dev/null +++ b/scripts/local_testnet/print_logs.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# Print the tail of all the logs output from local testnet + +set -Eeuo pipefail + +source ./vars.env + +for f in "$TESTNET_DIR"/*.log +do + [[ -e "$f" ]] || break # handle the case of no *.log files + echo "=============================================================================" + echo "$f" + echo "=============================================================================" + tail "$f" + echo "" +done diff --git a/scripts/local_testnet/reset_genesis_time.sh b/scripts/local_testnet/reset_genesis_time.sh index c7332e327e..68c8fb6b4c 100755 --- a/scripts/local_testnet/reset_genesis_time.sh +++ b/scripts/local_testnet/reset_genesis_time.sh @@ -4,6 +4,8 @@ # Resets the beacon state genesis time to now. # +set -Eeuo pipefail + source ./vars.env NOW=$(date +%s) diff --git a/scripts/local_testnet/start_local_testnet.sh b/scripts/local_testnet/start_local_testnet.sh index cdae9b2ba2..7126e4c5dc 100755 --- a/scripts/local_testnet/start_local_testnet.sh +++ b/scripts/local_testnet/start_local_testnet.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash # Start all processes necessary to create a local testnet +set -Eeuo pipefail + source ./vars.env # VC_COUNT is defaulted in vars.env @@ -49,7 +51,7 @@ for (( bn=1; bn<=$BN_COUNT; bn++ )); do done for (( vc=1; vc<=$VC_COUNT; vc++ )); do touch $LOG_DIR/validator_node_$vc.log -done +done # Sleep with a message sleeping() { @@ -67,7 +69,7 @@ execute_command() { EX_NAME=$2 shift shift - CMD="$EX_NAME $@ &>> $LOG_DIR/$LOG_NAME" + CMD="$EX_NAME $@ >> $LOG_DIR/$LOG_NAME 2>&1" echo "executing: $CMD" echo "$CMD" > "$LOG_DIR/$LOG_NAME" eval "$CMD &" @@ -89,7 +91,7 @@ execute_command_add_PID() { # Delay to let ganache-cli to get started execute_command_add_PID ganache_test_node.log ./ganache_test_node.sh -sleeping 2 +sleeping 10 # Delay to get data setup execute_command setup.log ./setup.sh diff --git a/scripts/local_testnet/stop_local_testnet.sh b/scripts/local_testnet/stop_local_testnet.sh index 47f390ba76..b1c3188ee3 100755 --- a/scripts/local_testnet/stop_local_testnet.sh +++ b/scripts/local_testnet/stop_local_testnet.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash # Stop all processes that were started with start_local_testnet.sh +set -Eeuo pipefail + source ./vars.env PID_FILE=$TESTNET_DIR/PIDS.pid diff --git a/scripts/local_testnet/validator_client.sh b/scripts/local_testnet/validator_client.sh index 6755384be5..5aa75dfe2d 100755 --- a/scripts/local_testnet/validator_client.sh +++ b/scripts/local_testnet/validator_client.sh @@ -6,6 +6,8 @@ # # Usage: ./validator_client.sh +set -Eeuo pipefail + source ./vars.env DEBUG_LEVEL=${3:-info} diff --git a/scripts/local_testnet/vars.env b/scripts/local_testnet/vars.env index f88e9eb716..208fbb6d85 100644 --- a/scripts/local_testnet/vars.env +++ b/scripts/local_testnet/vars.env @@ -43,3 +43,6 @@ SECONDS_PER_SLOT=3 # Seconds per Eth1 block SECONDS_PER_ETH1_BLOCK=1 + +# Command line arguments for validator client +VC_ARGS="" From 9964f5afe5d810025fe6c3901674f6c92f31ce52 Mon Sep 17 00:00:00 2001 From: Divma Date: Wed, 26 Jan 2022 23:14:22 +0000 Subject: [PATCH 05/23] Document why we hash downloaded blocks for both sync algs (#2927) ## Proposed Changes Initially the idea was to remove hashing of blocks in backfill sync. After considering it more, we conclude that we need to do it in both (forward and backfill) anyway. But since we forgot why we were doing it in the first place, this PR documents this logic. Future us should find it useful Co-authored-by: Divma <26765164+divagant-martian@users.noreply.github.com> --- .../network/src/sync/backfill_sync/mod.rs | 13 ++++-- beacon_node/network/src/sync/manager.rs | 2 +- .../network/src/sync/range_sync/batch.rs | 42 ++++++++++++++++--- 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index 610081319d..0c34eef274 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -54,6 +54,13 @@ impl BatchConfig for BackFillBatchConfig { fn max_batch_processing_attempts() -> u8 { MAX_BATCH_PROCESSING_ATTEMPTS } + fn batch_attempt_hash(blocks: &[SignedBeaconBlock]) -> u64 { + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + let mut hasher = DefaultHasher::new(); + blocks.hash(&mut hasher); + hasher.finish() + } } /// Return type when attempting to start the backfill sync process. @@ -119,7 +126,7 @@ pub struct BackFillSync { /// Batches validated by this chain. validated_batches: u64, - /// We keep track of peer that are participating in the backfill sync. Unlike RangeSync, + /// We keep track of peers that are participating in the backfill sync. Unlike RangeSync, /// BackFillSync uses all synced peers to download the chain from. If BackFillSync fails, we don't /// want to penalize all our synced peers, so we use this variable to keep track of peers that /// have participated and only penalize these peers if backfill sync fails. @@ -539,7 +546,7 @@ impl BackFillSync { "error" => %e, "batch" => self.processing_target); // This is unlikely to happen but it would stall syncing since the batch now has no // blocks to continue, and the chain is expecting a processing result that won't - // arrive. To mitigate this, (fake) fail this processing so that the batch is + // arrive. To mitigate this, (fake) fail this processing so that the batch is // re-downloaded. self.on_batch_process_result( network, @@ -795,7 +802,7 @@ impl BackFillSync { for attempt in batch.attempts() { // The validated batch has been re-processed if attempt.hash != processed_attempt.hash { - // The re-downloaded version was different + // The re-downloaded version was different. if processed_attempt.peer_id != attempt.peer_id { // A different peer sent the correct batch, the previous peer did not // We negatively score the original peer. diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 32f2a26367..960dd12afc 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -131,7 +131,7 @@ pub enum SyncRequestType { RangeSync(Epoch, ChainId), } -/// The result of processing a multiple blocks (a chain segment). +/// The result of processing multiple blocks (a chain segment). #[derive(Debug)] pub enum BatchProcessResult { /// The batch was completed successfully. It carries whether the sent batch contained blocks. diff --git a/beacon_node/network/src/sync/range_sync/batch.rs b/beacon_node/network/src/sync/range_sync/batch.rs index e0b15cb498..7239081ad1 100644 --- a/beacon_node/network/src/sync/range_sync/batch.rs +++ b/beacon_node/network/src/sync/range_sync/batch.rs @@ -19,6 +19,34 @@ pub trait BatchConfig { fn max_batch_download_attempts() -> u8; /// The max batch processing attempts. fn max_batch_processing_attempts() -> u8; + /// Hashing function of a batch's attempt. Used for scoring purposes. + /// + /// When a batch fails processing, it is possible that the batch is wrong (faulty or + /// incomplete) or that a previous one is wrong. For this reason we need to re-download and + /// re-process the batches awaiting validation and the current one. Consider this scenario: + /// + /// ```ignore + /// BatchA BatchB BatchC BatchD + /// -----X Empty Empty Y----- + /// ``` + /// + /// BatchA declares that it refers X, but BatchD declares that it's first block is Y. There is no + /// way to know if BatchD is faulty/incomplete or if batches B and/or C are missing blocks. It is + /// also possible that BatchA belongs to a different chain to the rest starting in some block + /// midway in the batch's range. For this reason, the four batches would need to be re-downloaded + /// and re-processed. + /// + /// If batchD was actually good, it will still register two processing attempts for the same set of + /// blocks. In this case, we don't want to penalize the peer that provided the first version, since + /// it's equal to the successfully processed one. + /// + /// The function `batch_attempt_hash` provides a way to compare two batch attempts without + /// storing the full set of blocks. + /// + /// Note that simpler hashing functions considered in the past (hash of first block, hash of last + /// block, number of received blocks) are not good enough to differentiate attempts. For this + /// reason, we hash the complete set of blocks both in RangeSync and BackFillSync. + fn batch_attempt_hash(blocks: &[SignedBeaconBlock]) -> u64; } pub struct RangeSyncBatchConfig {} @@ -30,6 +58,11 @@ impl BatchConfig for RangeSyncBatchConfig { fn max_batch_processing_attempts() -> u8 { MAX_BATCH_PROCESSING_ATTEMPTS } + fn batch_attempt_hash(blocks: &[SignedBeaconBlock]) -> u64 { + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + blocks.hash(&mut hasher); + hasher.finish() + } } /// Error type of a batch in a wrong state. @@ -300,7 +333,7 @@ impl BatchInfo { pub fn start_processing(&mut self) -> Result>, WrongState> { match self.state.poison() { BatchState::AwaitingProcessing(peer, blocks) => { - self.state = BatchState::Processing(Attempt::new(peer, &blocks)); + self.state = BatchState::Processing(Attempt::new::(peer, &blocks)); Ok(blocks) } BatchState::Poisoned => unreachable!("Poisoned batch"), @@ -386,11 +419,8 @@ pub struct Attempt { } impl Attempt { - #[allow(clippy::ptr_arg)] - fn new(peer_id: PeerId, blocks: &Vec>) -> Self { - let mut hasher = std::collections::hash_map::DefaultHasher::new(); - blocks.hash(&mut hasher); - let hash = hasher.finish(); + fn new(peer_id: PeerId, blocks: &[SignedBeaconBlock]) -> Self { + let hash = B::batch_attempt_hash(blocks); Attempt { peer_id, hash } } } From f2b1e096b2d57abe03e002dea71dab6679a49765 Mon Sep 17 00:00:00 2001 From: Divma Date: Wed, 26 Jan 2022 23:14:23 +0000 Subject: [PATCH 06/23] Code quality improvents to the network service (#2932) Checking how to priorize the polling of the network I moved most of the service code to functions. This change I think it's worth on it's own for code quality since inside the `tokio::select` many tools don't work (cargo fmt, sometimes clippy, and sometimes even the compiler's errors get wack). This is functionally equivalent to the previous code, just better organized --- beacon_node/network/src/service.rs | 902 ++++++++++++++++------------- 1 file changed, 503 insertions(+), 399 deletions(-) diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index 35cf3fa90e..c6f68d5faa 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -7,6 +7,7 @@ use crate::{ NetworkConfig, }; use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes}; +use futures::channel::mpsc::Sender; use futures::future::OptionFuture; use futures::prelude::*; use lighthouse_network::{ @@ -279,7 +280,7 @@ impl NetworkService { log: network_log, }; - spawn_service(executor, network_service); + network_service.spawn_service(executor); Ok((network_globals, network_send)) } @@ -320,428 +321,531 @@ impl NetworkService { result } -} -fn spawn_service( - executor: task_executor::TaskExecutor, - mut service: NetworkService, -) { - let mut shutdown_sender = executor.shutdown_sender(); + fn send_to_router(&mut self, msg: RouterMessage) { + if let Err(mpsc::error::SendError(msg)) = self.router_send.send(msg) { + debug!(self.log, "Failed to send msg to router"; "msg" => ?msg); + } + } - // spawn on the current executor - executor.spawn(async move { + fn spawn_service(mut self, executor: task_executor::TaskExecutor) { + let mut shutdown_sender = executor.shutdown_sender(); - loop { - // build the futures to check simultaneously - tokio::select! { - _ = service.metrics_update.tick(), if service.metrics_enabled => { - // update various network metrics - metrics::update_gossip_metrics::( - service.libp2p.swarm.behaviour().gs(), - &service.network_globals, - ); - // update sync metrics - metrics::update_sync_metrics(&service.network_globals); + // spawn on the current executor + let service_fut = async move { + loop { + tokio::select! { + _ = self.metrics_update.tick(), if self.metrics_enabled => { + // update various network metrics + metrics::update_gossip_metrics::( + self.libp2p.swarm.behaviour().gs(), + &self.network_globals, + ); + // update sync metrics + metrics::update_sync_metrics(&self.network_globals); + } - } - _ = service.gossipsub_parameter_update.tick() => { - if let Ok(slot) = service.beacon_chain.slot() { - if let Some(active_validators) = service.beacon_chain.with_head(|head| { - Ok::<_, BeaconChainError>( - head - .beacon_state - .get_cached_active_validator_indices(RelativeEpoch::Current) - .map(|indices| indices.len()) - .ok() - .or_else(|| { - // if active validator cached was not build we count the - // active validators - service - .beacon_chain - .epoch() - .ok() - .map(|current_epoch| { - head - .beacon_state - .validators() - .iter() - .filter(|validator| - validator.is_active_at(current_epoch) - ) - .count() - }) - }) - ) - }).unwrap_or(None) { - if service.libp2p.swarm.behaviour_mut().update_gossipsub_parameters(active_validators, slot).is_err() { - error!( - service.log, - "Failed to update gossipsub parameters"; - "active_validators" => active_validators - ); - } + _ = self.gossipsub_parameter_update.tick() => self.update_gossipsub_parameters(), + + // handle a message sent to the network + Some(msg) = self.network_recv.recv() => self.on_network_msg(msg, &mut shutdown_sender).await, + + // process any attestation service events + Some(msg) = self.attestation_service.next() => self.on_attestation_service_msg(msg), + + // process any sync committee service events + Some(msg) = self.sync_committee_service.next() => self.on_sync_commitee_service_message(msg), + + event = self.libp2p.next_event() => self.on_libp2p_event(event, &mut shutdown_sender).await, + + Some(_) = &mut self.next_fork_update => self.update_next_fork(), + + Some(_) = &mut self.next_unsubscribe => { + let new_enr_fork_id = self.beacon_chain.enr_fork_id(); + self.libp2p.swarm.behaviour_mut().unsubscribe_from_fork_topics_except(new_enr_fork_id.fork_digest); + info!(self.log, "Unsubscribed from old fork topics"); + self.next_unsubscribe = Box::pin(None.into()); + } + + Some(_) = &mut self.next_fork_subscriptions => { + if let Some((fork_name, _)) = self.beacon_chain.duration_to_next_fork() { + let fork_version = self.beacon_chain.spec.fork_version_for_name(fork_name); + let fork_digest = ChainSpec::compute_fork_digest(fork_version, self.beacon_chain.genesis_validators_root); + info!(self.log, "Subscribing to new fork topics"); + self.libp2p.swarm.behaviour_mut().subscribe_new_fork_topics(fork_digest); + self.next_fork_subscriptions = Box::pin(None.into()); + } + else { + error!(self.log, "Fork subscription scheduled but no fork scheduled"); } } } - // handle a message sent to the network - Some(message) = service.network_recv.recv() => { + metrics::update_bandwidth_metrics(self.libp2p.bandwidth.clone()); + } + }; + executor.spawn(service_fut, "network"); + } + + /// Handle an event received from the network. + async fn on_libp2p_event( + &mut self, + ev: Libp2pEvent, + shutdown_sender: &mut Sender, + ) { + match ev { + Libp2pEvent::Behaviour(event) => match event { + BehaviourEvent::PeerConnectedOutgoing(peer_id) => { + self.send_to_router(RouterMessage::PeerDialed(peer_id)); + } + BehaviourEvent::PeerConnectedIncoming(_) + | BehaviourEvent::PeerBanned(_) + | BehaviourEvent::PeerUnbanned(_) => { + // No action required for these events. + } + BehaviourEvent::PeerDisconnected(peer_id) => { + self.send_to_router(RouterMessage::PeerDisconnected(peer_id)); + } + BehaviourEvent::RequestReceived { + peer_id, + id, + request, + } => { + self.send_to_router(RouterMessage::RPCRequestReceived { + peer_id, + id, + request, + }); + } + BehaviourEvent::ResponseReceived { + peer_id, + id, + response, + } => { + self.send_to_router(RouterMessage::RPCResponseReceived { + peer_id, + request_id: id, + response, + }); + } + BehaviourEvent::RPCFailed { id, peer_id } => { + self.send_to_router(RouterMessage::RPCFailed { + peer_id, + request_id: id, + }); + } + BehaviourEvent::StatusPeer(peer_id) => { + self.send_to_router(RouterMessage::StatusPeer(peer_id)); + } + BehaviourEvent::PubsubMessage { + id, + source, + message, + .. + } => { match message { - NetworkMessage::SendRequest{ peer_id, request, request_id } => { - service.libp2p.send_request(peer_id, request_id, request); - } - NetworkMessage::SendResponse{ peer_id, response, id } => { - service.libp2p.send_response(peer_id, id, response); - } - NetworkMessage::SendErrorResponse{ peer_id, error, id, reason } => { - service.libp2p.respond_with_error(peer_id, id, error, reason); - } - NetworkMessage::UPnPMappingEstablished { tcp_socket, udp_socket} => { - service.upnp_mappings = (tcp_socket.map(|s| s.port()), udp_socket.map(|s| s.port())); - // If there is an external TCP port update, modify our local ENR. - if let Some(tcp_socket) = tcp_socket { - if let Err(e) = service.libp2p.swarm.behaviour_mut().discovery_mut().update_enr_tcp_port(tcp_socket.port()) { - warn!(service.log, "Failed to update ENR"; "error" => e); - } - } - // if the discovery service is not auto-updating, update it with the - // UPnP mappings - if !service.discovery_auto_update { - if let Some(udp_socket) = udp_socket { - if let Err(e) = service.libp2p.swarm.behaviour_mut().discovery_mut().update_enr_udp_socket(udp_socket) { - warn!(service.log, "Failed to update ENR"; "error" => e); - } - } - } - }, - NetworkMessage::ValidationResult { - propagation_source, - message_id, - validation_result, - } => { - trace!(service.log, "Propagating gossipsub message"; - "propagation_peer" => ?propagation_source, - "message_id" => %message_id, - "validation_result" => ?validation_result - ); - service - .libp2p - .swarm - .behaviour_mut() - .report_message_validation_result( - &propagation_source, message_id, validation_result - ); - } - NetworkMessage::Publish { messages } => { - let mut topic_kinds = Vec::new(); - for message in &messages { - if !topic_kinds.contains(&message.kind()) { - topic_kinds.push(message.kind()); - } - } - debug!( - service.log, - "Sending pubsub messages"; - "count" => messages.len(), - "topics" => ?topic_kinds - ); - service.libp2p.swarm.behaviour_mut().publish(messages); - } - NetworkMessage::ReportPeer { peer_id, action, source, msg } => service.libp2p.report_peer(&peer_id, action, source, msg), - NetworkMessage::GoodbyePeer { peer_id, reason, source } => service.libp2p.goodbye_peer(&peer_id, reason, source), - NetworkMessage::AttestationSubscribe { subscriptions } => { - if let Err(e) = service + // attestation information gets processed in the attestation service + PubsubMessage::Attestation(ref subnet_and_attestation) => { + let subnet = subnet_and_attestation.0; + let attestation = &subnet_and_attestation.1; + // checks if we have an aggregator for the slot. If so, we should process + // the attestation, else we just just propagate the Attestation. + let should_process = self .attestation_service - .validator_subscriptions(subscriptions) { - warn!(service.log, "Attestation validator subscription failed"; "error" => e); - } - } - NetworkMessage::SyncCommitteeSubscribe { subscriptions } => { - if let Err(e) = service - .sync_committee_service - .validator_subscriptions(subscriptions) { - warn!(service.log, "Sync committee calidator subscription failed"; "error" => e); - } - } - NetworkMessage::SubscribeCoreTopics => { - if service.shutdown_after_sync { - let _ = shutdown_sender - .send(ShutdownReason::Success( - "Beacon node completed sync. Shutting down as --shutdown-after-sync flag is enabled")) - .await - .map_err(|e| warn!( - service.log, - "failed to send a shutdown signal"; - "error" => %e - )); - return; - } - let mut subscribed_topics: Vec = vec![]; - for topic_kind in lighthouse_network::types::CORE_TOPICS.iter() { - for fork_digest in service.required_gossip_fork_digests() { - let topic = GossipTopic::new(topic_kind.clone(), GossipEncoding::default(), fork_digest); - if service.libp2p.swarm.behaviour_mut().subscribe(topic.clone()) { - subscribed_topics.push(topic); - } else { - warn!(service.log, "Could not subscribe to topic"; "topic" => %topic); - } - } - } - - // If we are to subscribe to all subnets we do it here - if service.subscribe_all_subnets { - for subnet_id in 0..<::EthSpec as EthSpec>::SubnetBitfieldLength::to_u64() { - let subnet = Subnet::Attestation(SubnetId::new(subnet_id)); - // Update the ENR bitfield - service.libp2p.swarm.behaviour_mut().update_enr_subnet(subnet, true); - for fork_digest in service.required_gossip_fork_digests() { - let topic = GossipTopic::new(subnet.into(), GossipEncoding::default(), fork_digest); - if service.libp2p.swarm.behaviour_mut().subscribe(topic.clone()) { - subscribed_topics.push(topic); - } else { - warn!(service.log, "Could not subscribe to topic"; "topic" => %topic); - } - } - } - for subnet_id in 0..<::EthSpec as EthSpec>::SyncCommitteeSubnetCount::to_u64() { - let subnet = Subnet::SyncCommittee(SyncSubnetId::new(subnet_id)); - // Update the ENR bitfield - service.libp2p.swarm.behaviour_mut().update_enr_subnet(subnet, true); - for fork_digest in service.required_gossip_fork_digests() { - let topic = GossipTopic::new(subnet.into(), GossipEncoding::default(), fork_digest); - if service.libp2p.swarm.behaviour_mut().subscribe(topic.clone()) { - subscribed_topics.push(topic); - } else { - warn!(service.log, "Could not subscribe to topic"; "topic" => %topic); - } - } - } - } - - if !subscribed_topics.is_empty() { - info!( - service.log, - "Subscribed to topics"; - "topics" => ?subscribed_topics.into_iter().map(|topic| format!("{}", topic)).collect::>() - ); - } - } - } - } - // process any attestation service events - Some(attestation_service_message) = service.attestation_service.next() => { - match attestation_service_message { - SubnetServiceMessage::Subscribe(subnet) => { - for fork_digest in service.required_gossip_fork_digests() { - let topic = GossipTopic::new(subnet.into(), GossipEncoding::default(), fork_digest); - service.libp2p.swarm.behaviour_mut().subscribe(topic); - } - } - SubnetServiceMessage::Unsubscribe(subnet) => { - for fork_digest in service.required_gossip_fork_digests() { - let topic = GossipTopic::new(subnet.into(), GossipEncoding::default(), fork_digest); - service.libp2p.swarm.behaviour_mut().unsubscribe(topic); - } - } - SubnetServiceMessage::EnrAdd(subnet) => { - service.libp2p.swarm.behaviour_mut().update_enr_subnet(subnet, true); - } - SubnetServiceMessage::EnrRemove(subnet) => { - service.libp2p.swarm.behaviour_mut().update_enr_subnet(subnet, false); - } - SubnetServiceMessage::DiscoverPeers(subnets_to_discover) => { - service.libp2p.swarm.behaviour_mut().discover_subnet_peers(subnets_to_discover); - } - } - } - // process any sync committee service events - Some(sync_committee_service_message) = service.sync_committee_service.next() => { - match sync_committee_service_message { - SubnetServiceMessage::Subscribe(subnet) => { - for fork_digest in service.required_gossip_fork_digests() { - let topic = GossipTopic::new(subnet.into(), GossipEncoding::default(), fork_digest); - service.libp2p.swarm.behaviour_mut().subscribe(topic); - } - } - SubnetServiceMessage::Unsubscribe(subnet) => { - for fork_digest in service.required_gossip_fork_digests() { - let topic = GossipTopic::new(subnet.into(), GossipEncoding::default(), fork_digest); - service.libp2p.swarm.behaviour_mut().unsubscribe(topic); - } - } - SubnetServiceMessage::EnrAdd(subnet) => { - service.libp2p.swarm.behaviour_mut().update_enr_subnet(subnet, true); - } - SubnetServiceMessage::EnrRemove(subnet) => { - service.libp2p.swarm.behaviour_mut().update_enr_subnet(subnet, false); - } - SubnetServiceMessage::DiscoverPeers(subnets_to_discover) => { - service.libp2p.swarm.behaviour_mut().discover_subnet_peers(subnets_to_discover); - } - } - } - libp2p_event = service.libp2p.next_event() => { - // poll the swarm - match libp2p_event { - Libp2pEvent::Behaviour(event) => match event { - BehaviourEvent::PeerConnectedOutgoing(peer_id) => { - let _ = service - .router_send - .send(RouterMessage::PeerDialed(peer_id)) - .map_err(|_| { - debug!(service.log, "Failed to send peer dialed to router"); }); - }, - BehaviourEvent::PeerConnectedIncoming(_) | BehaviourEvent::PeerBanned(_) | BehaviourEvent::PeerUnbanned(_) => { - // No action required for these events. - }, - BehaviourEvent::PeerDisconnected(peer_id) => { - let _ = service - .router_send - .send(RouterMessage::PeerDisconnected(peer_id)) - .map_err(|_| { - debug!(service.log, "Failed to send peer disconnect to router"); - }); - }, - BehaviourEvent::RequestReceived{peer_id, id, request} => { - let _ = service - .router_send - .send(RouterMessage::RPCRequestReceived{peer_id, id, request}) - .map_err(|_| { - debug!(service.log, "Failed to send RPC to router"); - }); - } - BehaviourEvent::ResponseReceived{peer_id, id, response} => { - let _ = service - .router_send - .send(RouterMessage::RPCResponseReceived{ peer_id, request_id: id, response }) - .map_err(|_| { - debug!(service.log, "Failed to send RPC to router"); - }); - - } - BehaviourEvent::RPCFailed{id, peer_id} => { - let _ = service - .router_send - .send(RouterMessage::RPCFailed{ peer_id, request_id: id}) - .map_err(|_| { - debug!(service.log, "Failed to send RPC to router"); - }); - - } - BehaviourEvent::StatusPeer(peer_id) => { - let _ = service - .router_send - .send(RouterMessage::StatusPeer(peer_id)) - .map_err(|_| { - debug!(service.log, "Failed to send re-status peer to router"); - }); - } - BehaviourEvent::PubsubMessage { + .should_process_attestation(subnet, attestation); + self.send_to_router(RouterMessage::PubsubMessage( id, source, message, - .. - } => { - match message { - // attestation information gets processed in the attestation service - PubsubMessage::Attestation(ref subnet_and_attestation) => { - let subnet = subnet_and_attestation.0; - let attestation = &subnet_and_attestation.1; - // checks if we have an aggregator for the slot. If so, we should process - // the attestation, else we just just propagate the Attestation. - let should_process = service.attestation_service.should_process_attestation( - subnet, - attestation, - ); - let _ = service - .router_send - .send(RouterMessage::PubsubMessage(id, source, message, should_process)) - .map_err(|_| { - debug!(service.log, "Failed to send pubsub message to router"); - }); - } - _ => { - // all else is sent to the router - let _ = service - .router_send - .send(RouterMessage::PubsubMessage(id, source, message, true)) - .map_err(|_| { - debug!(service.log, "Failed to send pubsub message to router"); - }); - } - } - } + should_process, + )); } - Libp2pEvent::NewListenAddr(multiaddr) => { - service.network_globals.listen_multiaddrs.write().push(multiaddr); - } - Libp2pEvent::ZeroListeners => { - let _ = shutdown_sender - .send(ShutdownReason::Failure("All listeners are closed. Unable to listen")) - .await - .map_err(|e| warn!( - service.log, - "failed to send a shutdown signal"; - "error" => %e - )); + _ => { + // all else is sent to the router + self.send_to_router(RouterMessage::PubsubMessage( + id, source, message, true, + )); } } } - Some(_) = &mut service.next_fork_update => { - let new_enr_fork_id = service.beacon_chain.enr_fork_id(); + }, + Libp2pEvent::NewListenAddr(multiaddr) => { + self.network_globals + .listen_multiaddrs + .write() + .push(multiaddr); + } + Libp2pEvent::ZeroListeners => { + let _ = shutdown_sender + .send(ShutdownReason::Failure( + "All listeners are closed. Unable to listen", + )) + .await + .map_err(|e| { + warn!( + self.log, + "failed to send a shutdown signal"; + "error" => %e + ) + }); + } + } + } - let fork_context = &service.fork_context; - if let Some(new_fork_name) = fork_context.from_context_bytes(new_enr_fork_id.fork_digest) { - info!( - service.log, - "Transitioned to new fork"; - "old_fork" => ?fork_context.current_fork(), - "new_fork" => ?new_fork_name, - ); - fork_context.update_current_fork(*new_fork_name); - - service + /// Handle a message sent to the network service. + async fn on_network_msg( + &mut self, + msg: NetworkMessage, + shutdown_sender: &mut Sender, + ) { + match msg { + NetworkMessage::SendRequest { + peer_id, + request, + request_id, + } => { + self.libp2p.send_request(peer_id, request_id, request); + } + NetworkMessage::SendResponse { + peer_id, + response, + id, + } => { + self.libp2p.send_response(peer_id, id, response); + } + NetworkMessage::SendErrorResponse { + peer_id, + error, + id, + reason, + } => { + self.libp2p.respond_with_error(peer_id, id, error, reason); + } + NetworkMessage::UPnPMappingEstablished { + tcp_socket, + udp_socket, + } => { + self.upnp_mappings = (tcp_socket.map(|s| s.port()), udp_socket.map(|s| s.port())); + // If there is an external TCP port update, modify our local ENR. + if let Some(tcp_socket) = tcp_socket { + if let Err(e) = self + .libp2p + .swarm + .behaviour_mut() + .discovery_mut() + .update_enr_tcp_port(tcp_socket.port()) + { + warn!(self.log, "Failed to update ENR"; "error" => e); + } + } + // if the discovery service is not auto-updating, update it with the + // UPnP mappings + if !self.discovery_auto_update { + if let Some(udp_socket) = udp_socket { + if let Err(e) = self .libp2p .swarm .behaviour_mut() - .update_fork_version(new_enr_fork_id.clone()); - // Reinitialize the next_fork_update - service.next_fork_update = Box::pin(next_fork_delay(&service.beacon_chain).into()); - - // Set the next_unsubscribe delay. - let epoch_duration = service.beacon_chain.spec.seconds_per_slot * T::EthSpec::slots_per_epoch(); - let unsubscribe_delay = Duration::from_secs(UNSUBSCRIBE_DELAY_EPOCHS * epoch_duration); - - // Update the `next_fork_subscriptions` timer if the next fork is known. - service.next_fork_subscriptions = Box::pin(next_fork_subscriptions_delay(&service.beacon_chain).into()); - service.next_unsubscribe = Box::pin(Some(tokio::time::sleep(unsubscribe_delay)).into()); - info!(service.log, "Network will unsubscribe from old fork gossip topics in a few epochs"; "remaining_epochs" => UNSUBSCRIBE_DELAY_EPOCHS); - } else { - crit!(service.log, "Unknown new enr fork id"; "new_fork_id" => ?new_enr_fork_id); - } - - } - Some(_) = &mut service.next_unsubscribe => { - let new_enr_fork_id = service.beacon_chain.enr_fork_id(); - service.libp2p.swarm.behaviour_mut().unsubscribe_from_fork_topics_except(new_enr_fork_id.fork_digest); - info!(service.log, "Unsubscribed from old fork topics"); - service.next_unsubscribe = Box::pin(None.into()); - } - Some(_) = &mut service.next_fork_subscriptions => { - if let Some((fork_name, _)) = service.beacon_chain.duration_to_next_fork() { - let fork_version = service.beacon_chain.spec.fork_version_for_name(fork_name); - let fork_digest = ChainSpec::compute_fork_digest(fork_version, service.beacon_chain.genesis_validators_root); - info!(service.log, "Subscribing to new fork topics"); - service.libp2p.swarm.behaviour_mut().subscribe_new_fork_topics(fork_digest); - service.next_fork_subscriptions = Box::pin(None.into()); - } - else { - error!(service.log, "Fork subscription scheduled but no fork scheduled"); + .discovery_mut() + .update_enr_udp_socket(udp_socket) + { + warn!(self.log, "Failed to update ENR"; "error" => e); + } } } } - metrics::update_bandwidth_metrics(service.libp2p.bandwidth.clone()); + NetworkMessage::ValidationResult { + propagation_source, + message_id, + validation_result, + } => { + trace!(self.log, "Propagating gossipsub message"; + "propagation_peer" => ?propagation_source, + "message_id" => %message_id, + "validation_result" => ?validation_result + ); + self.libp2p + .swarm + .behaviour_mut() + .report_message_validation_result( + &propagation_source, + message_id, + validation_result, + ); + } + NetworkMessage::Publish { messages } => { + let mut topic_kinds = Vec::new(); + for message in &messages { + if !topic_kinds.contains(&message.kind()) { + topic_kinds.push(message.kind()); + } + } + debug!( + self.log, + "Sending pubsub messages"; + "count" => messages.len(), + "topics" => ?topic_kinds + ); + self.libp2p.swarm.behaviour_mut().publish(messages); + } + NetworkMessage::ReportPeer { + peer_id, + action, + source, + msg, + } => self.libp2p.report_peer(&peer_id, action, source, msg), + NetworkMessage::GoodbyePeer { + peer_id, + reason, + source, + } => self.libp2p.goodbye_peer(&peer_id, reason, source), + NetworkMessage::AttestationSubscribe { subscriptions } => { + if let Err(e) = self + .attestation_service + .validator_subscriptions(subscriptions) + { + warn!(self.log, "Attestation validator subscription failed"; "error" => e); + } + } + NetworkMessage::SyncCommitteeSubscribe { subscriptions } => { + if let Err(e) = self + .sync_committee_service + .validator_subscriptions(subscriptions) + { + warn!(self.log, "Sync committee calidator subscription failed"; "error" => e); + } + } + NetworkMessage::SubscribeCoreTopics => { + if self.shutdown_after_sync { + if let Err(e) = shutdown_sender + .send(ShutdownReason::Success( + "Beacon node completed sync. \ + Shutting down as --shutdown-after-sync flag is enabled", + )) + .await + { + warn!( + self.log, + "failed to send a shutdown signal"; + "error" => %e + ) + } + return; + } + let mut subscribed_topics: Vec = vec![]; + for topic_kind in lighthouse_network::types::CORE_TOPICS.iter() { + for fork_digest in self.required_gossip_fork_digests() { + let topic = GossipTopic::new( + topic_kind.clone(), + GossipEncoding::default(), + fork_digest, + ); + if self.libp2p.swarm.behaviour_mut().subscribe(topic.clone()) { + subscribed_topics.push(topic); + } else { + warn!(self.log, "Could not subscribe to topic"; "topic" => %topic); + } + } + } + + // If we are to subscribe to all subnets we do it here + if self.subscribe_all_subnets { + for subnet_id in 0..<::EthSpec as EthSpec>::SubnetBitfieldLength::to_u64() { + let subnet = Subnet::Attestation(SubnetId::new(subnet_id)); + // Update the ENR bitfield + self.libp2p.swarm.behaviour_mut().update_enr_subnet(subnet, true); + for fork_digest in self.required_gossip_fork_digests() { + let topic = GossipTopic::new(subnet.into(), GossipEncoding::default(), fork_digest); + if self.libp2p.swarm.behaviour_mut().subscribe(topic.clone()) { + subscribed_topics.push(topic); + } else { + warn!(self.log, "Could not subscribe to topic"; "topic" => %topic); + } + } + } + let subnet_max = <::EthSpec as EthSpec>::SyncCommitteeSubnetCount::to_u64(); + for subnet_id in 0..subnet_max { + let subnet = Subnet::SyncCommittee(SyncSubnetId::new(subnet_id)); + // Update the ENR bitfield + self.libp2p + .swarm + .behaviour_mut() + .update_enr_subnet(subnet, true); + for fork_digest in self.required_gossip_fork_digests() { + let topic = GossipTopic::new( + subnet.into(), + GossipEncoding::default(), + fork_digest, + ); + if self.libp2p.swarm.behaviour_mut().subscribe(topic.clone()) { + subscribed_topics.push(topic); + } else { + warn!(self.log, "Could not subscribe to topic"; "topic" => %topic); + } + } + } + } + + if !subscribed_topics.is_empty() { + info!( + self.log, + "Subscribed to topics"; + "topics" => ?subscribed_topics.into_iter().map(|topic| format!("{}", topic)).collect::>() + ); + } + } } - }, "network"); + } + + fn update_gossipsub_parameters(&mut self) { + if let Ok(slot) = self.beacon_chain.slot() { + if let Some(active_validators) = self + .beacon_chain + .with_head(|head| { + Ok::<_, BeaconChainError>( + head.beacon_state + .get_cached_active_validator_indices(RelativeEpoch::Current) + .map(|indices| indices.len()) + .ok() + .or_else(|| { + // if active validator cached was not build we count the + // active validators + self.beacon_chain.epoch().ok().map(|current_epoch| { + head.beacon_state + .validators() + .iter() + .filter(|validator| validator.is_active_at(current_epoch)) + .count() + }) + }), + ) + }) + .unwrap_or(None) + { + if self + .libp2p + .swarm + .behaviour_mut() + .update_gossipsub_parameters(active_validators, slot) + .is_err() + { + error!( + self.log, + "Failed to update gossipsub parameters"; + "active_validators" => active_validators + ); + } + } + } + } + + fn on_attestation_service_msg(&mut self, msg: SubnetServiceMessage) { + match msg { + SubnetServiceMessage::Subscribe(subnet) => { + for fork_digest in self.required_gossip_fork_digests() { + let topic = + GossipTopic::new(subnet.into(), GossipEncoding::default(), fork_digest); + self.libp2p.swarm.behaviour_mut().subscribe(topic); + } + } + SubnetServiceMessage::Unsubscribe(subnet) => { + for fork_digest in self.required_gossip_fork_digests() { + let topic = + GossipTopic::new(subnet.into(), GossipEncoding::default(), fork_digest); + self.libp2p.swarm.behaviour_mut().unsubscribe(topic); + } + } + SubnetServiceMessage::EnrAdd(subnet) => { + self.libp2p + .swarm + .behaviour_mut() + .update_enr_subnet(subnet, true); + } + SubnetServiceMessage::EnrRemove(subnet) => { + self.libp2p + .swarm + .behaviour_mut() + .update_enr_subnet(subnet, false); + } + SubnetServiceMessage::DiscoverPeers(subnets_to_discover) => { + self.libp2p + .swarm + .behaviour_mut() + .discover_subnet_peers(subnets_to_discover); + } + } + } + + fn on_sync_commitee_service_message(&mut self, msg: SubnetServiceMessage) { + match msg { + SubnetServiceMessage::Subscribe(subnet) => { + for fork_digest in self.required_gossip_fork_digests() { + let topic = + GossipTopic::new(subnet.into(), GossipEncoding::default(), fork_digest); + self.libp2p.swarm.behaviour_mut().subscribe(topic); + } + } + SubnetServiceMessage::Unsubscribe(subnet) => { + for fork_digest in self.required_gossip_fork_digests() { + let topic = + GossipTopic::new(subnet.into(), GossipEncoding::default(), fork_digest); + self.libp2p.swarm.behaviour_mut().unsubscribe(topic); + } + } + SubnetServiceMessage::EnrAdd(subnet) => { + self.libp2p + .swarm + .behaviour_mut() + .update_enr_subnet(subnet, true); + } + SubnetServiceMessage::EnrRemove(subnet) => { + self.libp2p + .swarm + .behaviour_mut() + .update_enr_subnet(subnet, false); + } + SubnetServiceMessage::DiscoverPeers(subnets_to_discover) => { + self.libp2p + .swarm + .behaviour_mut() + .discover_subnet_peers(subnets_to_discover); + } + } + } + + fn update_next_fork(&mut self) { + let new_enr_fork_id = self.beacon_chain.enr_fork_id(); + + let fork_context = &self.fork_context; + if let Some(new_fork_name) = fork_context.from_context_bytes(new_enr_fork_id.fork_digest) { + info!( + self.log, + "Transitioned to new fork"; + "old_fork" => ?fork_context.current_fork(), + "new_fork" => ?new_fork_name, + ); + fork_context.update_current_fork(*new_fork_name); + + self.libp2p + .swarm + .behaviour_mut() + .update_fork_version(new_enr_fork_id); + // Reinitialize the next_fork_update + self.next_fork_update = Box::pin(next_fork_delay(&self.beacon_chain).into()); + + // Set the next_unsubscribe delay. + let epoch_duration = + self.beacon_chain.spec.seconds_per_slot * T::EthSpec::slots_per_epoch(); + let unsubscribe_delay = Duration::from_secs(UNSUBSCRIBE_DELAY_EPOCHS * epoch_duration); + + // Update the `next_fork_subscriptions` timer if the next fork is known. + self.next_fork_subscriptions = + Box::pin(next_fork_subscriptions_delay(&self.beacon_chain).into()); + self.next_unsubscribe = Box::pin(Some(tokio::time::sleep(unsubscribe_delay)).into()); + info!(self.log, "Network will unsubscribe from old fork gossip topics in a few epochs"; "remaining_epochs" => UNSUBSCRIBE_DELAY_EPOCHS); + } else { + crit!(self.log, "Unknown new enr fork id"; "new_fork_id" => ?new_enr_fork_id); + } + } } /// Returns a `Sleep` that triggers after the next change in the beacon chain fork version. From 85d73d5443c0000d91e300dbeda6788b9d124e22 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Wed, 26 Jan 2022 23:14:24 +0000 Subject: [PATCH 07/23] Set mmap threshold to 128KB in malloc utils (#2937) ## Issue Addressed Closes https://github.com/sigp/lighthouse/issues/2857 ## Proposed Changes Explicitly set GNU malloc's MMAP_THRESHOLD to 128KB, disabling dynamic adjustments. For rationale see the linked issue. --- common/malloc_utils/src/glibc.rs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/common/malloc_utils/src/glibc.rs b/common/malloc_utils/src/glibc.rs index 402cdc27aa..681849a78c 100644 --- a/common/malloc_utils/src/glibc.rs +++ b/common/malloc_utils/src/glibc.rs @@ -11,22 +11,20 @@ use std::env; use std::os::raw::c_int; use std::result::Result; -/// The value to be provided to `malloc_mmap_threshold`. +/// The optimal mmap threshold for Lighthouse seems to be around 128KB. /// -/// Value chosen so that values of the validators tree hash cache will *not* be allocated via -/// `mmap`. -/// -/// The size of a single chunk is: -/// -/// NODES_PER_VALIDATOR * VALIDATORS_PER_ARENA * 32 = 15 * 4096 * 32 = 1.875 MiB -const OPTIMAL_MMAP_THRESHOLD: c_int = 2 * 1_024 * 1_024; +/// By default GNU malloc will start with a threshold of 128KB and adjust it upwards, but we've +/// found that the upwards adjustments tend to result in heap fragmentation. Explicitly setting the +/// threshold to 128KB disables the dynamic adjustments and encourages `mmap` usage, which keeps the +/// heap size under control. +const OPTIMAL_MMAP_THRESHOLD: c_int = 128 * 1_024; /// Constants used to configure malloc internals. /// /// Source: /// /// https://github.com/lattera/glibc/blob/895ef79e04a953cac1493863bcae29ad85657ee1/malloc/malloc.h#L115-L123 -const M_MMAP_THRESHOLD: c_int = -4; +const M_MMAP_THRESHOLD: c_int = -3; /// Environment variables used to configure malloc. /// @@ -134,8 +132,8 @@ fn env_var_present(name: &str) -> bool { /// ## Resources /// /// - https://man7.org/linux/man-pages/man3/mallopt.3.html -fn malloc_mmap_threshold(num_arenas: c_int) -> Result<(), c_int> { - into_result(mallopt(M_MMAP_THRESHOLD, num_arenas)) +fn malloc_mmap_threshold(threshold: c_int) -> Result<(), c_int> { + into_result(mallopt(M_MMAP_THRESHOLD, threshold)) } fn mallopt(param: c_int, val: c_int) -> c_int { From 013a3cc3e08cd7dd76df27efa70716d5c0155be4 Mon Sep 17 00:00:00 2001 From: Mac L Date: Wed, 26 Jan 2022 23:14:25 +0000 Subject: [PATCH 08/23] Add flag to disable confirmation when performing voluntary exits (#2955) ## Issue Addressed Currently performing a voluntary exit prompts for manual confirmation. This prevents automation of exits. ## Proposed Changes Add the flag `--no-confirmation` to the account manager when performing voluntary exits to bypass this manual confirmation. --- account_manager/src/validator/exit.rs | 33 ++++++++++++++++++++------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/account_manager/src/validator/exit.rs b/account_manager/src/validator/exit.rs index 221c31caf6..ca8cab5bd3 100644 --- a/account_manager/src/validator/exit.rs +++ b/account_manager/src/validator/exit.rs @@ -21,6 +21,7 @@ pub const KEYSTORE_FLAG: &str = "keystore"; pub const PASSWORD_FILE_FLAG: &str = "password-file"; pub const BEACON_SERVER_FLAG: &str = "beacon-node"; pub const NO_WAIT: &str = "no-wait"; +pub const NO_CONFIRMATION: &str = "no-confirmation"; pub const PASSWORD_PROMPT: &str = "Enter the keystore password"; pub const DEFAULT_BEACON_NODE: &str = "http://localhost:5052/"; @@ -59,6 +60,11 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .long(NO_WAIT) .help("Exits after publishing the voluntary exit without waiting for confirmation that the exit was included in the beacon chain") ) + .arg( + Arg::with_name(NO_CONFIRMATION) + .long(NO_CONFIRMATION) + .help("Exits without prompting for confirmation that you understand the implications of a voluntary exit. This should be used with caution") + ) .arg( Arg::with_name(STDIN_INPUTS_FLAG) .takes_value(false) @@ -75,6 +81,7 @@ pub fn cli_run(matches: &ArgMatches, env: Environment) -> Result< let stdin_inputs = cfg!(windows) || matches.is_present(STDIN_INPUTS_FLAG); let no_wait = matches.is_present(NO_WAIT); + let no_confirmation = matches.is_present(NO_CONFIRMATION); let spec = env.eth2_config().spec.clone(); let server_url: String = clap_utils::parse_required(matches, BEACON_SERVER_FLAG)?; @@ -97,12 +104,14 @@ pub fn cli_run(matches: &ArgMatches, env: Environment) -> Result< stdin_inputs, ð2_network_config, no_wait, + no_confirmation, ))?; Ok(()) } /// Gets the keypair and validator_index for every validator and calls `publish_voluntary_exit` on it. +#[allow(clippy::too_many_arguments)] async fn publish_voluntary_exit( keystore_path: &Path, password_file_path: Option<&PathBuf>, @@ -111,6 +120,7 @@ async fn publish_voluntary_exit( stdin_inputs: bool, eth2_network_config: &Eth2NetworkConfig, no_wait: bool, + no_confirmation: bool, ) -> Result<(), String> { let genesis_data = get_geneisis_data(client).await?; let testnet_genesis_root = eth2_network_config @@ -149,15 +159,22 @@ async fn publish_voluntary_exit( "Publishing a voluntary exit for validator: {} \n", keypair.pk ); - eprintln!("WARNING: THIS IS AN IRREVERSIBLE OPERATION\n"); - eprintln!("{}\n", PROMPT); - eprintln!( - "PLEASE VISIT {} TO MAKE SURE YOU UNDERSTAND THE IMPLICATIONS OF A VOLUNTARY EXIT.", - WEBSITE_URL - ); - eprintln!("Enter the exit phrase from the above URL to confirm the voluntary exit: "); + if !no_confirmation { + eprintln!("WARNING: THIS IS AN IRREVERSIBLE OPERATION\n"); + eprintln!("{}\n", PROMPT); + eprintln!( + "PLEASE VISIT {} TO MAKE SURE YOU UNDERSTAND THE IMPLICATIONS OF A VOLUNTARY EXIT.", + WEBSITE_URL + ); + eprintln!("Enter the exit phrase from the above URL to confirm the voluntary exit: "); + } + + let confirmation = if !no_confirmation { + account_utils::read_input_from_user(stdin_inputs)? + } else { + CONFIRMATION_PHRASE.to_string() + }; - let confirmation = account_utils::read_input_from_user(stdin_inputs)?; if confirmation == CONFIRMATION_PHRASE { // Sign and publish the voluntary exit to network let signed_voluntary_exit = voluntary_exit.sign( From e70daaa3b6ee70eb6a6f2c0a1759062943e08259 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 27 Jan 2022 01:06:02 +0000 Subject: [PATCH 09/23] Implement API for block rewards (#2628) ## Proposed Changes Add an API endpoint for retrieving detailed information about block rewards. For information on usage see [the docs](https://github.com/sigp/lighthouse/blob/block-rewards-api/book/src/api-lighthouse.md#lighthouseblock_rewards), and the source. --- beacon_node/beacon_chain/src/block_reward.rs | 97 +++++++++++++++++++ .../beacon_chain/src/block_verification.rs | 13 +++ beacon_node/beacon_chain/src/errors.rs | 3 + beacon_node/beacon_chain/src/events.rs | 13 +++ beacon_node/beacon_chain/src/lib.rs | 1 + beacon_node/http_api/src/block_rewards.rs | 80 +++++++++++++++ beacon_node/http_api/src/lib.rs | 15 +++ beacon_node/operation_pool/src/lib.rs | 5 +- book/src/api-lighthouse.md | 42 +++++++- common/eth2/src/lib.rs | 2 + common/eth2/src/lighthouse.rs | 3 + common/eth2/src/lighthouse/block_rewards.rs | 54 +++++++++++ common/eth2/src/types.rs | 17 ++++ .../altair/sync_committee.rs | 37 ++++--- 14 files changed, 366 insertions(+), 16 deletions(-) create mode 100644 beacon_node/beacon_chain/src/block_reward.rs create mode 100644 beacon_node/http_api/src/block_rewards.rs create mode 100644 common/eth2/src/lighthouse/block_rewards.rs diff --git a/beacon_node/beacon_chain/src/block_reward.rs b/beacon_node/beacon_chain/src/block_reward.rs new file mode 100644 index 0000000000..83b204113f --- /dev/null +++ b/beacon_node/beacon_chain/src/block_reward.rs @@ -0,0 +1,97 @@ +use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; +use eth2::lighthouse::{AttestationRewards, BlockReward, BlockRewardMeta}; +use operation_pool::{AttMaxCover, MaxCover}; +use state_processing::per_block_processing::altair::sync_committee::compute_sync_aggregate_rewards; +use types::{BeaconBlockRef, BeaconState, EthSpec, Hash256, RelativeEpoch}; + +impl BeaconChain { + pub fn compute_block_reward( + &self, + block: BeaconBlockRef<'_, T::EthSpec>, + block_root: Hash256, + state: &BeaconState, + ) -> Result { + if block.slot() != state.slot() { + return Err(BeaconChainError::BlockRewardSlotError); + } + + let active_indices = state.get_cached_active_validator_indices(RelativeEpoch::Current)?; + let total_active_balance = state.get_total_balance(active_indices, &self.spec)?; + let mut per_attestation_rewards = block + .body() + .attestations() + .iter() + .map(|att| { + AttMaxCover::new(att, state, total_active_balance, &self.spec) + .ok_or(BeaconChainError::BlockRewardAttestationError) + }) + .collect::, _>>()?; + + // Update the attestation rewards for each previous attestation included. + // This is O(n^2) in the number of attestations n. + for i in 0..per_attestation_rewards.len() { + let (updated, to_update) = per_attestation_rewards.split_at_mut(i + 1); + let latest_att = &updated[i]; + + for att in to_update { + att.update_covering_set(latest_att.object(), latest_att.covering_set()); + } + } + + let mut prev_epoch_total = 0; + let mut curr_epoch_total = 0; + + for cover in &per_attestation_rewards { + for &reward in cover.fresh_validators_rewards.values() { + if cover.att.data.slot.epoch(T::EthSpec::slots_per_epoch()) == state.current_epoch() + { + curr_epoch_total += reward; + } else { + prev_epoch_total += reward; + } + } + } + + let attestation_total = prev_epoch_total + curr_epoch_total; + + // Drop the covers. + let per_attestation_rewards = per_attestation_rewards + .into_iter() + .map(|cover| cover.fresh_validators_rewards) + .collect(); + + let attestation_rewards = AttestationRewards { + total: attestation_total, + prev_epoch_total, + curr_epoch_total, + per_attestation_rewards, + }; + + // Sync committee rewards. + let sync_committee_rewards = if let Ok(sync_aggregate) = block.body().sync_aggregate() { + let (_, proposer_reward_per_bit) = compute_sync_aggregate_rewards(state, &self.spec) + .map_err(|_| BeaconChainError::BlockRewardSyncError)?; + sync_aggregate.sync_committee_bits.num_set_bits() as u64 * proposer_reward_per_bit + } else { + 0 + }; + + // Total, metadata + let total = attestation_total + sync_committee_rewards; + + let meta = BlockRewardMeta { + slot: block.slot(), + parent_slot: state.latest_block_header().slot, + proposer_index: block.proposer_index(), + graffiti: block.body().graffiti().as_utf8_lossy(), + }; + + Ok(BlockReward { + total, + block_root, + meta, + attestation_rewards, + sync_committee_rewards, + }) + } +} diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index c6d937c81e..a4a1dc31b9 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -53,6 +53,7 @@ use crate::{ }, metrics, BeaconChain, BeaconChainError, BeaconChainTypes, }; +use eth2::types::EventKind; use fork_choice::{ForkChoice, ForkChoiceStore, PayloadVerificationStatus}; use parking_lot::RwLockReadGuard; use proto_array::Block as ProtoBlock; @@ -1165,6 +1166,18 @@ impl<'a, T: BeaconChainTypes> FullyVerifiedBlock<'a, T> { metrics::stop_timer(committee_timer); + /* + * If we have block reward listeners, compute the block reward and push it to the + * event handler. + */ + if let Some(ref event_handler) = chain.event_handler { + if event_handler.has_block_reward_subscribers() { + let block_reward = + chain.compute_block_reward(block.message(), block_root, &state)?; + event_handler.register(EventKind::BlockReward(block_reward)); + } + } + /* * Perform `per_block_processing` on the block and state, returning early if the block is * invalid. diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 70e288ec26..6920c06039 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -137,6 +137,9 @@ pub enum BeaconChainError { AltairForkDisabled, ExecutionLayerMissing, ExecutionForkChoiceUpdateFailed(execution_layer::Error), + BlockRewardSlotError, + BlockRewardAttestationError, + BlockRewardSyncError, HeadMissingFromForkChoice(Hash256), FinalizedBlockMissingFromForkChoice(Hash256), InvalidFinalizedPayloadShutdownError(TrySendError), diff --git a/beacon_node/beacon_chain/src/events.rs b/beacon_node/beacon_chain/src/events.rs index 459ccb457f..6f4415ef4f 100644 --- a/beacon_node/beacon_chain/src/events.rs +++ b/beacon_node/beacon_chain/src/events.rs @@ -15,6 +15,7 @@ pub struct ServerSentEventHandler { chain_reorg_tx: Sender>, contribution_tx: Sender>, late_head: Sender>, + block_reward_tx: Sender>, log: Logger, } @@ -32,6 +33,7 @@ impl ServerSentEventHandler { let (chain_reorg_tx, _) = broadcast::channel(capacity); let (contribution_tx, _) = broadcast::channel(capacity); let (late_head, _) = broadcast::channel(capacity); + let (block_reward_tx, _) = broadcast::channel(capacity); Self { attestation_tx, @@ -42,6 +44,7 @@ impl ServerSentEventHandler { chain_reorg_tx, contribution_tx, late_head, + block_reward_tx, log, } } @@ -67,6 +70,8 @@ impl ServerSentEventHandler { .map(|count| trace!(self.log, "Registering server-sent contribution and proof event"; "receiver_count" => count)), EventKind::LateHead(late_head) => self.late_head.send(EventKind::LateHead(late_head)) .map(|count| trace!(self.log, "Registering server-sent late head event"; "receiver_count" => count)), + EventKind::BlockReward(block_reward) => self.block_reward_tx.send(EventKind::BlockReward(block_reward)) + .map(|count| trace!(self.log, "Registering server-sent contribution and proof event"; "receiver_count" => count)), }; if let Err(SendError(event)) = result { trace!(self.log, "No receivers registered to listen for event"; "event" => ?event); @@ -105,6 +110,10 @@ impl ServerSentEventHandler { self.late_head.subscribe() } + pub fn subscribe_block_reward(&self) -> Receiver> { + self.block_reward_tx.subscribe() + } + pub fn has_attestation_subscribers(&self) -> bool { self.attestation_tx.receiver_count() > 0 } @@ -136,4 +145,8 @@ impl ServerSentEventHandler { pub fn has_late_head_subscribers(&self) -> bool { self.late_head.receiver_count() > 0 } + + pub fn has_block_reward_subscribers(&self) -> bool { + self.block_reward_tx.receiver_count() > 0 + } } diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 768a869551..aff8657e86 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -5,6 +5,7 @@ mod beacon_chain; mod beacon_fork_choice_store; mod beacon_proposer_cache; mod beacon_snapshot; +pub mod block_reward; mod block_times_cache; mod block_verification; pub mod builder; diff --git a/beacon_node/http_api/src/block_rewards.rs b/beacon_node/http_api/src/block_rewards.rs new file mode 100644 index 0000000000..154773aa95 --- /dev/null +++ b/beacon_node/http_api/src/block_rewards.rs @@ -0,0 +1,80 @@ +use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes, WhenSlotSkipped}; +use eth2::lighthouse::{BlockReward, BlockRewardsQuery}; +use slog::{warn, Logger}; +use state_processing::BlockReplayer; +use std::sync::Arc; +use warp_utils::reject::{beacon_chain_error, beacon_state_error, custom_bad_request}; + +pub fn get_block_rewards( + query: BlockRewardsQuery, + chain: Arc>, + log: Logger, +) -> Result, warp::Rejection> { + let start_slot = query.start_slot; + let end_slot = query.end_slot; + let prior_slot = start_slot - 1; + + if start_slot > end_slot || start_slot == 0 { + return Err(custom_bad_request(format!( + "invalid start and end: {}, {}", + start_slot, end_slot + ))); + } + + let end_block_root = chain + .block_root_at_slot(end_slot, WhenSlotSkipped::Prev) + .map_err(beacon_chain_error)? + .ok_or_else(|| custom_bad_request(format!("block at end slot {} unknown", end_slot)))?; + + let blocks = chain + .store + .load_blocks_to_replay(start_slot, end_slot, end_block_root) + .map_err(|e| beacon_chain_error(e.into()))?; + + let state_root = chain + .state_root_at_slot(prior_slot) + .map_err(beacon_chain_error)? + .ok_or_else(|| custom_bad_request(format!("prior state at slot {} unknown", prior_slot)))?; + + let mut state = chain + .get_state(&state_root, Some(prior_slot)) + .and_then(|maybe_state| maybe_state.ok_or(BeaconChainError::MissingBeaconState(state_root))) + .map_err(beacon_chain_error)?; + + state + .build_all_caches(&chain.spec) + .map_err(beacon_state_error)?; + + let mut block_rewards = Vec::with_capacity(blocks.len()); + + let block_replayer = BlockReplayer::new(state, &chain.spec) + .pre_block_hook(Box::new(|state, block| { + // Compute block reward. + let block_reward = + chain.compute_block_reward(block.message(), block.canonical_root(), state)?; + block_rewards.push(block_reward); + Ok(()) + })) + .state_root_iter( + chain + .forwards_iter_state_roots_until(prior_slot, end_slot) + .map_err(beacon_chain_error)?, + ) + .no_signature_verification() + .minimal_block_root_verification() + .apply_blocks(blocks, None) + .map_err(beacon_chain_error)?; + + if block_replayer.state_root_miss() { + warn!( + log, + "Block reward state root miss"; + "start_slot" => start_slot, + "end_slot" => end_slot, + ); + } + + drop(block_replayer); + + Ok(block_rewards) +} diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index b0907a30c1..deadf68543 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -7,6 +7,7 @@ mod attester_duties; mod block_id; +mod block_rewards; mod database; mod metrics; mod proposer_duties; @@ -2540,6 +2541,16 @@ pub fn serve( }, ); + let get_lighthouse_block_rewards = warp::path("lighthouse") + .and(warp::path("block_rewards")) + .and(warp::query::()) + .and(warp::path::end()) + .and(chain_filter.clone()) + .and(log_filter.clone()) + .and_then(|query, chain, log| { + blocking_json_task(move || block_rewards::get_block_rewards(query, chain, log)) + }); + let get_events = eth1_v1 .and(warp::path("events")) .and(warp::path::end()) @@ -2576,6 +2587,9 @@ pub fn serve( api_types::EventTopic::LateHead => { event_handler.subscribe_late_head() } + api_types::EventTopic::BlockReward => { + event_handler.subscribe_block_reward() + } }; receivers.push(BroadcastStream::new(receiver).map(|msg| { @@ -2661,6 +2675,7 @@ pub fn serve( .or(get_lighthouse_beacon_states_ssz.boxed()) .or(get_lighthouse_staking.boxed()) .or(get_lighthouse_database_info.boxed()) + .or(get_lighthouse_block_rewards.boxed()) .or(get_events.boxed()), ) .or(warp::post().and( diff --git a/beacon_node/operation_pool/src/lib.rs b/beacon_node/operation_pool/src/lib.rs index 2cc3ffaf6b..c9b252ca11 100644 --- a/beacon_node/operation_pool/src/lib.rs +++ b/beacon_node/operation_pool/src/lib.rs @@ -6,15 +6,16 @@ mod metrics; mod persistence; mod sync_aggregate_id; +pub use attestation::AttMaxCover; +pub use max_cover::MaxCover; pub use persistence::{ PersistedOperationPool, PersistedOperationPoolAltair, PersistedOperationPoolBase, }; use crate::sync_aggregate_id::SyncAggregateId; -use attestation::AttMaxCover; use attestation_id::AttestationId; use attester_slashing::AttesterSlashingMaxCover; -use max_cover::{maximum_cover, MaxCover}; +use max_cover::maximum_cover; use parking_lot::RwLock; use state_processing::per_block_processing::errors::AttestationValidationError; use state_processing::per_block_processing::{ diff --git a/book/src/api-lighthouse.md b/book/src/api-lighthouse.md index 8ea35f7348..7836ac14a4 100644 --- a/book/src/api-lighthouse.md +++ b/book/src/api-lighthouse.md @@ -407,4 +407,44 @@ The endpoint will return immediately. See the beacon node logs for an indication ### `/lighthouse/database/historical_blocks` Manually provide `SignedBeaconBlock`s to backfill the database. This is intended -for use by Lighthouse developers during testing only. \ No newline at end of file +for use by Lighthouse developers during testing only. + +### `/lighthouse/block_rewards` + +Fetch information about the block rewards paid to proposers for a range of consecutive blocks. + +Two query parameters are required: + +* `start_slot` (inclusive): the slot of the first block to compute rewards for. +* `end_slot` (inclusive): the slot of the last block to compute rewards for. + +Example: + +```bash +curl "http://localhost:5052/lighthouse/block_rewards?start_slot=1&end_slot=32" | jq +``` + +```json +[ + { + "block_root": "0x51576c2fcf0ab68d7d93c65e6828e620efbb391730511ffa35584d6c30e51410", + "attestation_rewards": { + "total": 4941156, + }, + .. + }, + .. +] +``` + +Caveats: + +* Presently only attestation rewards are computed. +* The output format is verbose and subject to change. Please see [`BlockReward`][block_reward_src] + in the source. +* For maximum efficiency the `start_slot` should satisfy `start_slot % slots_per_restore_point == 1`. + This is because the state _prior_ to the `start_slot` needs to be loaded from the database, and + loading a state on a boundary is most efficient. + +[block_reward_src]: +https://github.com/sigp/lighthouse/tree/unstable/common/eth2/src/lighthouse/block_reward.rs \ No newline at end of file diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 153667d7e9..8dc808c265 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -9,6 +9,7 @@ #[cfg(feature = "lighthouse")] pub mod lighthouse; +#[cfg(feature = "lighthouse")] pub mod lighthouse_vc; pub mod mixin; pub mod types; @@ -245,6 +246,7 @@ impl BeaconNodeHttpClient { } /// Perform a HTTP POST request, returning a JSON response. + #[cfg(feature = "lighthouse")] async fn post_with_response( &self, url: U, diff --git a/common/eth2/src/lighthouse.rs b/common/eth2/src/lighthouse.rs index a8993a39c5..10601556fa 100644 --- a/common/eth2/src/lighthouse.rs +++ b/common/eth2/src/lighthouse.rs @@ -1,5 +1,7 @@ //! This module contains endpoints that are non-standard and only available on Lighthouse servers. +mod block_rewards; + use crate::{ ok_or_error, types::{BeaconState, ChainSpec, Epoch, EthSpec, GenericResponse, ValidatorId}, @@ -12,6 +14,7 @@ use ssz::four_byte_option_impl; use ssz_derive::{Decode, Encode}; use store::{AnchorInfo, Split}; +pub use block_rewards::{AttestationRewards, BlockReward, BlockRewardMeta, BlockRewardsQuery}; pub use lighthouse_network::{types::SyncState, PeerInfo}; // Define "legacy" implementations of `Option` which use four bytes for encoding the union diff --git a/common/eth2/src/lighthouse/block_rewards.rs b/common/eth2/src/lighthouse/block_rewards.rs new file mode 100644 index 0000000000..186cbd888c --- /dev/null +++ b/common/eth2/src/lighthouse/block_rewards.rs @@ -0,0 +1,54 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use types::{Hash256, Slot}; + +/// Details about the rewards paid to a block proposer for proposing a block. +/// +/// All rewards in GWei. +/// +/// Presently this only counts attestation rewards, but in future should be expanded +/// to include information on slashings and sync committee aggregates too. +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct BlockReward { + /// Sum of all reward components. + pub total: u64, + /// Block root of the block that these rewards are for. + pub block_root: Hash256, + /// Metadata about the block, particularly reward-relevant metadata. + pub meta: BlockRewardMeta, + /// Rewards due to attestations. + pub attestation_rewards: AttestationRewards, + /// Sum of rewards due to sync committee signatures. + pub sync_committee_rewards: u64, +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct BlockRewardMeta { + pub slot: Slot, + pub parent_slot: Slot, + pub proposer_index: u64, + pub graffiti: String, +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct AttestationRewards { + /// Total block reward from attestations included. + pub total: u64, + /// Total rewards from previous epoch attestations. + pub prev_epoch_total: u64, + /// Total rewards from current epoch attestations. + pub curr_epoch_total: u64, + /// Vec of attestation rewards for each attestation included. + /// + /// Each element of the vec is a map from validator index to reward. + pub per_attestation_rewards: Vec>, +} + +/// Query parameters for the `/lighthouse/block_rewards` endpoint. +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct BlockRewardsQuery { + /// Lower slot limit for block rewards returned (inclusive). + pub start_slot: Slot, + /// Upper slot limit for block rewards returned (inclusive). + pub end_slot: Slot, +} diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index a761b9ed12..78567ad83c 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -10,6 +10,9 @@ use std::str::{from_utf8, FromStr}; use std::time::Duration; pub use types::*; +#[cfg(feature = "lighthouse")] +use crate::lighthouse::BlockReward; + /// An API error serializable to JSON. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(untagged)] @@ -839,6 +842,8 @@ pub enum EventKind { ChainReorg(SseChainReorg), ContributionAndProof(Box>), LateHead(SseLateHead), + #[cfg(feature = "lighthouse")] + BlockReward(BlockReward), } impl EventKind { @@ -852,6 +857,8 @@ impl EventKind { EventKind::ChainReorg(_) => "chain_reorg", EventKind::ContributionAndProof(_) => "contribution_and_proof", EventKind::LateHead(_) => "late_head", + #[cfg(feature = "lighthouse")] + EventKind::BlockReward(_) => "block_reward", } } @@ -904,6 +911,10 @@ impl EventKind { ServerError::InvalidServerSentEvent(format!("Contribution and Proof: {:?}", e)) })?, ))), + #[cfg(feature = "lighthouse")] + "block_reward" => Ok(EventKind::BlockReward(serde_json::from_str(data).map_err( + |e| ServerError::InvalidServerSentEvent(format!("Block Reward: {:?}", e)), + )?)), _ => Err(ServerError::InvalidServerSentEvent( "Could not parse event tag".to_string(), )), @@ -929,6 +940,8 @@ pub enum EventTopic { ChainReorg, ContributionAndProof, LateHead, + #[cfg(feature = "lighthouse")] + BlockReward, } impl FromStr for EventTopic { @@ -944,6 +957,8 @@ impl FromStr for EventTopic { "chain_reorg" => Ok(EventTopic::ChainReorg), "contribution_and_proof" => Ok(EventTopic::ContributionAndProof), "late_head" => Ok(EventTopic::LateHead), + #[cfg(feature = "lighthouse")] + "block_reward" => Ok(EventTopic::BlockReward), _ => Err("event topic cannot be parsed.".to_string()), } } @@ -960,6 +975,8 @@ impl fmt::Display for EventTopic { EventTopic::ChainReorg => write!(f, "chain_reorg"), EventTopic::ContributionAndProof => write!(f, "contribution_and_proof"), EventTopic::LateHead => write!(f, "late_head"), + #[cfg(feature = "lighthouse")] + EventTopic::BlockReward => write!(f, "block_reward"), } } } diff --git a/consensus/state_processing/src/per_block_processing/altair/sync_committee.rs b/consensus/state_processing/src/per_block_processing/altair/sync_committee.rs index 31386a8fb1..8358003e4b 100644 --- a/consensus/state_processing/src/per_block_processing/altair/sync_committee.rs +++ b/consensus/state_processing/src/per_block_processing/altair/sync_committee.rs @@ -42,19 +42,7 @@ pub fn process_sync_aggregate( } // Compute participant and proposer rewards - let total_active_balance = state.get_total_active_balance()?; - let total_active_increments = - total_active_balance.safe_div(spec.effective_balance_increment)?; - let total_base_rewards = get_base_reward_per_increment(total_active_balance, spec)? - .safe_mul(total_active_increments)?; - let max_participant_rewards = total_base_rewards - .safe_mul(SYNC_REWARD_WEIGHT)? - .safe_div(WEIGHT_DENOMINATOR)? - .safe_div(T::slots_per_epoch())?; - let participant_reward = max_participant_rewards.safe_div(T::SyncCommitteeSize::to_u64())?; - let proposer_reward = participant_reward - .safe_mul(PROPOSER_WEIGHT)? - .safe_div(WEIGHT_DENOMINATOR.safe_sub(PROPOSER_WEIGHT)?)?; + let (participant_reward, proposer_reward) = compute_sync_aggregate_rewards(state, spec)?; // Apply participant and proposer rewards let committee_indices = state.get_sync_committee_indices(¤t_sync_committee)?; @@ -73,3 +61,26 @@ pub fn process_sync_aggregate( Ok(()) } + +/// Compute the `(participant_reward, proposer_reward)` for a sync aggregate. +/// +/// The `state` should be the pre-state from the same slot as the block containing the aggregate. +pub fn compute_sync_aggregate_rewards( + state: &BeaconState, + spec: &ChainSpec, +) -> Result<(u64, u64), BlockProcessingError> { + let total_active_balance = state.get_total_active_balance()?; + let total_active_increments = + total_active_balance.safe_div(spec.effective_balance_increment)?; + let total_base_rewards = get_base_reward_per_increment(total_active_balance, spec)? + .safe_mul(total_active_increments)?; + let max_participant_rewards = total_base_rewards + .safe_mul(SYNC_REWARD_WEIGHT)? + .safe_div(WEIGHT_DENOMINATOR)? + .safe_div(T::slots_per_epoch())?; + let participant_reward = max_participant_rewards.safe_div(T::SyncCommitteeSize::to_u64())?; + let proposer_reward = participant_reward + .safe_mul(PROPOSER_WEIGHT)? + .safe_div(WEIGHT_DENOMINATOR.safe_sub(PROPOSER_WEIGHT)?)?; + Ok((participant_reward, proposer_reward)) +} From 782abdcab5e79294b1e7e41544f23d2a9936aead Mon Sep 17 00:00:00 2001 From: tim gretler Date: Thu, 27 Jan 2022 01:06:04 +0000 Subject: [PATCH 10/23] Outaded flag in lighthouse book (#2965) ## Proposed Changes Outdated flag. Need to use `--wallet-name` instead. --- book/src/validator-create.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/src/validator-create.md b/book/src/validator-create.md index 73fff42dfe..91af60078a 100644 --- a/book/src/validator-create.md +++ b/book/src/validator-create.md @@ -75,7 +75,7 @@ The example assumes that the `wally` wallet was generated from the [wallet](./wallet-create.md) example. ```bash -lighthouse --network pyrmont account validator create --name wally --wallet-password wally.pass --count 1 +lighthouse --network pyrmont account validator create --wallet-name wally --wallet-password wally.pass --count 1 ``` This command will: From e05142b798759ea92ad028e7454dd5af572d8e8b Mon Sep 17 00:00:00 2001 From: Mac L Date: Thu, 27 Jan 2022 22:58:31 +0000 Subject: [PATCH 11/23] Add API to compute discrete validator attestation performance (#2874) ## Issue Addressed N/A ## Proposed Changes Add a HTTP API which can be used to compute the attestation performances of a validator (or all validators) over a discrete range of epochs. Performances can be computed for a single validator, or for the global validator set. ## Usage ### Request The API can be used as follows: ``` curl "http://localhost:5052/lighthouse/analysis/attestation_performance/{validator_index}?start_epoch=57730&end_epoch=57732" ``` Alternatively, to compute performances for the global validator set: ``` curl "http://localhost:5052/lighthouse/analysis/attestation_performance/global?start_epoch=57730&end_epoch=57732" ``` ### Response The response is JSON formatted as follows: ``` [ { "index": 72, "epochs": { "57730": { "active": true, "head": false, "target": false, "source": false }, "57731": { "active": true, "head": true, "target": true, "source": true, "delay": 1 }, "57732": { "active": true, "head": true, "target": true, "source": true, "delay": 1 }, } } ] ``` > Note that the `"epochs"` are not guaranteed to be in ascending order. ## Additional Info - This API is intended to be used in our upcoming validator analysis tooling (#2873) and will likely not be very useful for regular users. Some advanced users or block explorers may find this API useful however. - The request range is limited to 100 epochs (since the range is inclusive and it also computes the `end_epoch` it's actually 101 epochs) to prevent Lighthouse using exceptionally large amounts of memory. --- .../http_api/src/attestation_performance.rs | 216 ++++++++++++++++++ beacon_node/http_api/src/lib.rs | 18 ++ common/eth2/src/lighthouse.rs | 4 + .../src/lighthouse/attestation_performance.rs | 39 ++++ 4 files changed, 277 insertions(+) create mode 100644 beacon_node/http_api/src/attestation_performance.rs create mode 100644 common/eth2/src/lighthouse/attestation_performance.rs diff --git a/beacon_node/http_api/src/attestation_performance.rs b/beacon_node/http_api/src/attestation_performance.rs new file mode 100644 index 0000000000..5cd9894ade --- /dev/null +++ b/beacon_node/http_api/src/attestation_performance.rs @@ -0,0 +1,216 @@ +use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes}; +use eth2::lighthouse::{ + AttestationPerformance, AttestationPerformanceQuery, AttestationPerformanceStatistics, +}; +use state_processing::{ + per_epoch_processing::altair::participation_cache::Error as ParticipationCacheError, + per_epoch_processing::EpochProcessingSummary, BlockReplayError, BlockReplayer, +}; +use std::sync::Arc; +use types::{BeaconState, BeaconStateError, EthSpec, Hash256, SignedBeaconBlock}; +use warp_utils::reject::{beacon_chain_error, custom_bad_request, custom_server_error}; + +const MAX_REQUEST_RANGE_EPOCHS: usize = 100; +const BLOCK_ROOT_CHUNK_SIZE: usize = 100; + +#[derive(Debug)] +enum AttestationPerformanceError { + BlockReplay(BlockReplayError), + BeaconState(BeaconStateError), + ParticipationCache(ParticipationCacheError), + UnableToFindValidator(usize), +} + +impl From for AttestationPerformanceError { + fn from(e: BlockReplayError) -> Self { + Self::BlockReplay(e) + } +} + +impl From for AttestationPerformanceError { + fn from(e: BeaconStateError) -> Self { + Self::BeaconState(e) + } +} + +impl From for AttestationPerformanceError { + fn from(e: ParticipationCacheError) -> Self { + Self::ParticipationCache(e) + } +} + +pub fn get_attestation_performance( + target: String, + query: AttestationPerformanceQuery, + chain: Arc>, +) -> Result, warp::Rejection> { + let spec = &chain.spec; + // We increment by 2 here so that when we build the state from the `prior_slot` it is + // still 1 epoch ahead of the first epoch we want to analyse. + // This ensures the `.is_previous_epoch_X` functions on `EpochProcessingSummary` return results + // for the correct epoch. + let start_epoch = query.start_epoch + 2; + let start_slot = start_epoch.start_slot(T::EthSpec::slots_per_epoch()); + let prior_slot = start_slot - 1; + + let end_epoch = query.end_epoch + 2; + let end_slot = end_epoch.end_slot(T::EthSpec::slots_per_epoch()); + + // Ensure end_epoch is smaller than the current epoch - 1. + let current_epoch = chain.epoch().map_err(beacon_chain_error)?; + if query.end_epoch >= current_epoch - 1 { + return Err(custom_bad_request(format!( + "end_epoch must be less than the current epoch - 1. current: {}, end: {}", + current_epoch, query.end_epoch + ))); + } + + // Check query is valid. + if start_epoch > end_epoch { + return Err(custom_bad_request(format!( + "start_epoch must not be larger than end_epoch. start: {}, end: {}", + query.start_epoch, query.end_epoch + ))); + } + + // The response size can grow exceptionally large therefore we should check that the + // query is within permitted bounds to prevent potential OOM errors. + if (end_epoch - start_epoch).as_usize() > MAX_REQUEST_RANGE_EPOCHS { + return Err(custom_bad_request(format!( + "end_epoch must not exceed start_epoch by more than 100 epochs. start: {}, end: {}", + query.start_epoch, query.end_epoch + ))); + } + + // Either use the global validator set, or the specified index. + let index_range = if target.to_lowercase() == "global" { + chain + .with_head(|head| Ok((0..head.beacon_state.validators().len() as u64).collect())) + .map_err(beacon_chain_error)? + } else { + vec![target.parse::().map_err(|_| { + custom_bad_request(format!( + "Invalid validator index: {:?}", + target.to_lowercase() + )) + })?] + }; + + // Load block roots. + let mut block_roots: Vec = chain + .forwards_iter_block_roots_until(start_slot, end_slot) + .map_err(beacon_chain_error)? + .map(|res| res.map(|(root, _)| root)) + .collect::, _>>() + .map_err(beacon_chain_error)?; + block_roots.dedup(); + + // Load first block so we can get its parent. + let first_block_root = block_roots.first().ok_or_else(|| { + custom_server_error( + "No blocks roots could be loaded. Ensure the beacon node is synced.".to_string(), + ) + })?; + let first_block = chain + .get_block(first_block_root) + .and_then(|maybe_block| { + maybe_block.ok_or(BeaconChainError::MissingBeaconBlock(*first_block_root)) + }) + .map_err(beacon_chain_error)?; + + // Load the block of the prior slot which will be used to build the starting state. + let prior_block = chain + .get_block(&first_block.parent_root()) + .and_then(|maybe_block| { + maybe_block + .ok_or_else(|| BeaconChainError::MissingBeaconBlock(first_block.parent_root())) + }) + .map_err(beacon_chain_error)?; + + // Load state for block replay. + let state_root = prior_block.state_root(); + let state = chain + .get_state(&state_root, Some(prior_slot)) + .and_then(|maybe_state| maybe_state.ok_or(BeaconChainError::MissingBeaconState(state_root))) + .map_err(beacon_chain_error)?; + + // Allocate an AttestationPerformance vector for each validator in the range. + let mut perfs: Vec = + AttestationPerformance::initialize(index_range.clone()); + + let post_slot_hook = |state: &mut BeaconState, + summary: Option>, + _is_skip_slot: bool| + -> Result<(), AttestationPerformanceError> { + // If a `summary` was not output then an epoch boundary was not crossed + // so we move onto the next slot. + if let Some(summary) = summary { + for (position, i) in index_range.iter().enumerate() { + let index = *i as usize; + + let val = perfs + .get_mut(position) + .ok_or(AttestationPerformanceError::UnableToFindValidator(index))?; + + // We are two epochs ahead since the summary is generated for + // `state.previous_epoch()` then `summary.is_previous_epoch_X` functions return + // data for the epoch before that. + let epoch = state.previous_epoch().as_u64() - 1; + + let is_active = summary.is_active_unslashed_in_previous_epoch(index); + + let received_source_reward = summary.is_previous_epoch_source_attester(index)?; + + let received_head_reward = summary.is_previous_epoch_head_attester(index)?; + + let received_target_reward = summary.is_previous_epoch_target_attester(index)?; + + let inclusion_delay = summary + .previous_epoch_inclusion_info(index) + .map(|info| info.delay); + + let perf = AttestationPerformanceStatistics { + active: is_active, + head: received_head_reward, + target: received_target_reward, + source: received_source_reward, + delay: inclusion_delay, + }; + + val.epochs.insert(epoch, perf); + } + } + Ok(()) + }; + + // Initialize block replayer + let mut replayer = BlockReplayer::new(state, spec) + .no_state_root_iter() + .no_signature_verification() + .minimal_block_root_verification() + .post_slot_hook(Box::new(post_slot_hook)); + + // Iterate through block roots in chunks to reduce load on memory. + for block_root_chunks in block_roots.chunks(BLOCK_ROOT_CHUNK_SIZE) { + // Load blocks from the block root chunks. + let blocks = block_root_chunks + .iter() + .map(|root| { + chain + .get_block(root) + .and_then(|maybe_block| { + maybe_block.ok_or(BeaconChainError::MissingBeaconBlock(*root)) + }) + .map_err(beacon_chain_error) + }) + .collect::>, _>>()?; + + replayer = replayer + .apply_blocks(blocks, None) + .map_err(|e| custom_server_error(format!("{:?}", e)))?; + } + + drop(replayer); + + Ok(perfs) +} diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index deadf68543..b30af858f7 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -5,6 +5,7 @@ //! There are also some additional, non-standard endpoints behind the `/lighthouse/` path which are //! used for development. +mod attestation_performance; mod attester_duties; mod block_id; mod block_rewards; @@ -2541,7 +2542,9 @@ pub fn serve( }, ); + // GET lighthouse/analysis/block_rewards let get_lighthouse_block_rewards = warp::path("lighthouse") + .and(warp::path("analysis")) .and(warp::path("block_rewards")) .and(warp::query::()) .and(warp::path::end()) @@ -2551,6 +2554,20 @@ pub fn serve( blocking_json_task(move || block_rewards::get_block_rewards(query, chain, log)) }); + // GET lighthouse/analysis/attestation_performance/{index} + let get_lighthouse_attestation_performance = warp::path("lighthouse") + .and(warp::path("analysis")) + .and(warp::path("attestation_performance")) + .and(warp::path::param::()) + .and(warp::query::()) + .and(warp::path::end()) + .and(chain_filter.clone()) + .and_then(|target, query, chain: Arc>| { + blocking_json_task(move || { + attestation_performance::get_attestation_performance(target, query, chain) + }) + }); + let get_events = eth1_v1 .and(warp::path("events")) .and(warp::path::end()) @@ -2676,6 +2693,7 @@ pub fn serve( .or(get_lighthouse_staking.boxed()) .or(get_lighthouse_database_info.boxed()) .or(get_lighthouse_block_rewards.boxed()) + .or(get_lighthouse_attestation_performance.boxed()) .or(get_events.boxed()), ) .or(warp::post().and( diff --git a/common/eth2/src/lighthouse.rs b/common/eth2/src/lighthouse.rs index 10601556fa..adf73d8b92 100644 --- a/common/eth2/src/lighthouse.rs +++ b/common/eth2/src/lighthouse.rs @@ -1,5 +1,6 @@ //! This module contains endpoints that are non-standard and only available on Lighthouse servers. +mod attestation_performance; mod block_rewards; use crate::{ @@ -14,6 +15,9 @@ use ssz::four_byte_option_impl; use ssz_derive::{Decode, Encode}; use store::{AnchorInfo, Split}; +pub use attestation_performance::{ + AttestationPerformance, AttestationPerformanceQuery, AttestationPerformanceStatistics, +}; pub use block_rewards::{AttestationRewards, BlockReward, BlockRewardMeta, BlockRewardsQuery}; pub use lighthouse_network::{types::SyncState, PeerInfo}; diff --git a/common/eth2/src/lighthouse/attestation_performance.rs b/common/eth2/src/lighthouse/attestation_performance.rs new file mode 100644 index 0000000000..5ce1d90a38 --- /dev/null +++ b/common/eth2/src/lighthouse/attestation_performance.rs @@ -0,0 +1,39 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use types::Epoch; + +#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)] +pub struct AttestationPerformanceStatistics { + pub active: bool, + pub head: bool, + pub target: bool, + pub source: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub delay: Option, +} + +#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)] +pub struct AttestationPerformance { + pub index: u64, + pub epochs: HashMap, +} + +impl AttestationPerformance { + pub fn initialize(indices: Vec) -> Vec { + let mut vec = Vec::with_capacity(indices.len()); + for index in indices { + vec.push(Self { + index, + ..Default::default() + }) + } + vec + } +} + +/// Query parameters for the `/lighthouse/analysis/attestation_performance` endpoint. +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct AttestationPerformanceQuery { + pub start_epoch: Epoch, + pub end_epoch: Epoch, +} From 99d2c33387477398fc11b55319a064f03ab1a646 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 27 Jan 2022 22:58:32 +0000 Subject: [PATCH 12/23] Avoid looking up pre-finalization blocks (#2909) ## Issue Addressed This PR fixes the unnecessary `WARN Single block lookup failed` messages described here: https://github.com/sigp/lighthouse/pull/2866#issuecomment-1008442640 ## Proposed Changes Add a new cache to the `BeaconChain` that tracks the block roots of blocks from before finalization. These could be blocks from the canonical chain (which might need to be read from disk), or old pre-finalization blocks that have been forked out. The cache also stores a set of block roots for in-progress single block lookups, which duplicates some of the information from sync's `single_block_lookups` hashmap: https://github.com/sigp/lighthouse/blob/a836e180f9ad51f31767c5ffb33bebdeff1a9f3f/beacon_node/network/src/sync/manager.rs#L192-L196 On a live node you can confirm that the cache is working by grepping logs for the message: `Rejected attestation to finalized block`. --- .../src/attestation_verification.rs | 18 +++ beacon_node/beacon_chain/src/beacon_chain.rs | 7 ++ .../beacon_chain/src/block_verification.rs | 10 +- beacon_node/beacon_chain/src/builder.rs | 1 + beacon_node/beacon_chain/src/lib.rs | 1 + beacon_node/beacon_chain/src/metrics.rs | 19 +++ .../src/pre_finalization_cache.rs | 119 ++++++++++++++++++ .../tests/attestation_verification.rs | 77 +++++++++++- .../beacon_processor/worker/gossip_methods.rs | 20 +++ 9 files changed, 267 insertions(+), 5 deletions(-) create mode 100644 beacon_node/beacon_chain/src/pre_finalization_cache.rs diff --git a/beacon_node/beacon_chain/src/attestation_verification.rs b/beacon_node/beacon_chain/src/attestation_verification.rs index 6692aa48cd..059b3c4d21 100644 --- a/beacon_node/beacon_chain/src/attestation_verification.rs +++ b/beacon_node/beacon_chain/src/attestation_verification.rs @@ -141,6 +141,14 @@ pub enum Error { /// The attestation points to a block we have not yet imported. It's unclear if the attestation /// is valid or not. UnknownHeadBlock { beacon_block_root: Hash256 }, + /// The `attestation.data.beacon_block_root` block is from before the finalized checkpoint. + /// + /// ## Peer scoring + /// + /// The attestation is not descended from the finalized checkpoint, which is a REJECT according + /// to the spec. We downscore lightly because this could also happen if we are processing + /// attestations extremely slowly. + HeadBlockFinalized { beacon_block_root: Hash256 }, /// The `attestation.data.slot` is not from the same epoch as `data.target.epoch`. /// /// ## Peer scoring @@ -990,7 +998,17 @@ fn verify_head_block_is_known( } Ok(block) + } else if chain.is_pre_finalization_block(attestation.data.beacon_block_root)? { + Err(Error::HeadBlockFinalized { + beacon_block_root: attestation.data.beacon_block_root, + }) } else { + // The block is either: + // + // 1) A pre-finalization block that has been pruned. We'll do one network lookup + // for it and when it fails we will penalise all involved peers. + // 2) A post-finalization block that we don't know about yet. We'll queue + // the attestation until the block becomes available (or we time out). Err(Error::UnknownHeadBlock { beacon_block_root: attestation.data.beacon_block_root, }) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 4e1d54dc13..a65a943b93 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -34,6 +34,7 @@ use crate::observed_block_producers::ObservedBlockProducers; use crate::observed_operations::{ObservationOutcome, ObservedOperations}; use crate::persisted_beacon_chain::{PersistedBeaconChain, DUMMY_CANONICAL_HEAD_BLOCK_ROOT}; use crate::persisted_fork_choice::PersistedForkChoice; +use crate::pre_finalization_cache::PreFinalizationBlockCache; use crate::shuffling_cache::{BlockShufflingIds, ShufflingCache}; use crate::snapshot_cache::SnapshotCache; use crate::sync_committee_verification::{ @@ -336,6 +337,8 @@ pub struct BeaconChain { pub early_attester_cache: EarlyAttesterCache, /// A cache used to keep track of various block timings. pub block_times_cache: Arc>, + /// A cache used to track pre-finalization block roots for quick rejection. + pub pre_finalization_block_cache: PreFinalizationBlockCache, /// Sender given to tasks, so that if they encounter a state in which execution cannot /// continue they can request that everything shuts down. pub shutdown_sender: Sender, @@ -2855,6 +2858,10 @@ impl BeaconChain { ); } + // Inform the unknown block cache, in case it was waiting on this block. + self.pre_finalization_block_cache + .block_processed(block_root); + Ok(block_root) } diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index a4a1dc31b9..c2dc0028e9 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -618,7 +618,7 @@ impl GossipVerifiedBlock { check_block_against_anchor_slot(block.message(), chain)?; // Do not gossip a block from a finalized slot. - check_block_against_finalized_slot(block.message(), chain)?; + check_block_against_finalized_slot(block.message(), block_root, chain)?; // Check if the block is already known. We know it is post-finalization, so it is // sufficient to check the fork choice. @@ -1292,6 +1292,7 @@ fn check_block_against_anchor_slot( /// verifying that condition. fn check_block_against_finalized_slot( block: BeaconBlockRef<'_, T::EthSpec>, + block_root: Hash256, chain: &BeaconChain, ) -> Result<(), BlockError> { let finalized_slot = chain @@ -1301,6 +1302,7 @@ fn check_block_against_finalized_slot( .start_slot(T::EthSpec::slots_per_epoch()); if block.slot() <= finalized_slot { + chain.pre_finalization_block_rejected(block_root); Err(BlockError::WouldRevertFinalizedSlot { block_slot: block.slot(), finalized_slot, @@ -1373,11 +1375,11 @@ pub fn check_block_relevancy( return Err(BlockError::BlockSlotLimitReached); } - // Do not process a block from a finalized slot. - check_block_against_finalized_slot(block, chain)?; - let block_root = block_root.unwrap_or_else(|| get_block_root(signed_block)); + // Do not process a block from a finalized slot. + check_block_against_finalized_slot(block, block_root, chain)?; + // Check if the block is already known. We know it is post-finalization, so it is // sufficient to check the fork choice. if chain.fork_choice.read().contains_block(&block_root) { diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 24a9a916bb..e9860124c0 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -752,6 +752,7 @@ where shuffling_cache: TimeoutRwLock::new(ShufflingCache::new()), beacon_proposer_cache: <_>::default(), block_times_cache: <_>::default(), + pre_finalization_block_cache: <_>::default(), validator_pubkey_cache: TimeoutRwLock::new(validator_pubkey_cache), attester_cache: <_>::default(), early_attester_cache: <_>::default(), diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index aff8657e86..d41c1a5cc5 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -27,6 +27,7 @@ mod observed_block_producers; pub mod observed_operations; mod persisted_beacon_chain; mod persisted_fork_choice; +mod pre_finalization_cache; pub mod schema_change; mod shuffling_cache; mod snapshot_cache; diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index 28eacad559..41b7604532 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -904,6 +904,20 @@ lazy_static! { "beacon_backfill_signature_total_seconds", "Time spent verifying the signature set during backfill sync, including setup" ); + + /* + * Pre-finalization block cache. + */ + pub static ref PRE_FINALIZATION_BLOCK_CACHE_SIZE: Result = + try_create_int_gauge( + "beacon_pre_finalization_block_cache_size", + "Number of pre-finalization block roots cached for quick rejection" + ); + pub static ref PRE_FINALIZATION_BLOCK_LOOKUP_COUNT: Result = + try_create_int_gauge( + "beacon_pre_finalization_block_lookup_count", + "Number of block roots subject to single block lookups" + ); } /// Scrape the `beacon_chain` for metrics that are not constantly updated (e.g., the present slot, @@ -931,6 +945,11 @@ pub fn scrape_for_metrics(beacon_chain: &BeaconChain) { ) } + if let Some((size, num_lookups)) = beacon_chain.pre_finalization_block_cache.metrics() { + set_gauge_by_usize(&PRE_FINALIZATION_BLOCK_CACHE_SIZE, size); + set_gauge_by_usize(&PRE_FINALIZATION_BLOCK_LOOKUP_COUNT, num_lookups); + } + set_gauge_by_usize( &OP_POOL_NUM_ATTESTATIONS, attestation_stats.num_attestations, diff --git a/beacon_node/beacon_chain/src/pre_finalization_cache.rs b/beacon_node/beacon_chain/src/pre_finalization_cache.rs new file mode 100644 index 0000000000..41771b048d --- /dev/null +++ b/beacon_node/beacon_chain/src/pre_finalization_cache.rs @@ -0,0 +1,119 @@ +use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; +use itertools::process_results; +use lru::LruCache; +use parking_lot::Mutex; +use slog::debug; +use std::time::Duration; +use types::Hash256; + +const BLOCK_ROOT_CACHE_LIMIT: usize = 512; +const LOOKUP_LIMIT: usize = 8; +const METRICS_TIMEOUT: Duration = Duration::from_millis(100); + +/// Cache for rejecting attestations to blocks from before finalization. +/// +/// It stores a collection of block roots that are pre-finalization and therefore not known to fork +/// choice in `verify_head_block_is_known` during attestation processing. +#[derive(Default)] +pub struct PreFinalizationBlockCache { + cache: Mutex, +} + +struct Cache { + /// Set of block roots that are known to be pre-finalization. + block_roots: LruCache, + /// Set of block roots that are the subject of single block lookups. + in_progress_lookups: LruCache, +} + +impl Default for Cache { + fn default() -> Self { + Cache { + block_roots: LruCache::new(BLOCK_ROOT_CACHE_LIMIT), + in_progress_lookups: LruCache::new(LOOKUP_LIMIT), + } + } +} + +impl BeaconChain { + /// Check whether the block with `block_root` is known to be pre-finalization. + /// + /// The provided `block_root` is assumed to be unknown to fork choice. I.e., it + /// is not known to be a descendant of the finalized block. + /// + /// Return `true` if the attestation to this block should be rejected outright, + /// return `false` if more information is needed from a single-block-lookup. + pub fn is_pre_finalization_block(&self, block_root: Hash256) -> Result { + let mut cache = self.pre_finalization_block_cache.cache.lock(); + + // Check the cache to see if we already know this pre-finalization block root. + if cache.block_roots.contains(&block_root) { + return Ok(true); + } + + // Avoid repeating the disk lookup for blocks that are already subject to a network lookup. + // Sync will take care of de-duplicating the single block lookups. + if cache.in_progress_lookups.contains(&block_root) { + return Ok(false); + } + + // 1. Check memory for a recent pre-finalization block. + let is_recent_finalized_block = self.with_head(|head| { + process_results( + head.beacon_state.rev_iter_block_roots(&self.spec), + |mut iter| iter.any(|(_, root)| root == block_root), + ) + .map_err(BeaconChainError::BeaconStateError) + })?; + if is_recent_finalized_block { + cache.block_roots.put(block_root, ()); + return Ok(true); + } + + // 2. Check on disk. + if self.store.get_block(&block_root)?.is_some() { + cache.block_roots.put(block_root, ()); + return Ok(true); + } + + // 3. Check the network with a single block lookup. + cache.in_progress_lookups.put(block_root, ()); + if cache.in_progress_lookups.len() == LOOKUP_LIMIT { + // NOTE: we expect this to occur sometimes if a lot of blocks that we look up fail to be + // imported for reasons other than being pre-finalization. The cache will eventually + // self-repair in this case by replacing old entries with new ones until all the failed + // blocks have been flushed out. Solving this issue isn't as simple as hooking the + // beacon processor's functions that handle failed blocks because we need the block root + // and it has been erased from the `BlockError` by that point. + debug!( + self.log, + "Pre-finalization lookup cache is full"; + ); + } + Ok(false) + } + + pub fn pre_finalization_block_rejected(&self, block_root: Hash256) { + // Future requests can know that this block is invalid without having to look it up again. + let mut cache = self.pre_finalization_block_cache.cache.lock(); + cache.in_progress_lookups.pop(&block_root); + cache.block_roots.put(block_root, ()); + } +} + +impl PreFinalizationBlockCache { + pub fn block_processed(&self, block_root: Hash256) { + // Future requests will find this block in fork choice, so no need to cache it in the + // ongoing lookup cache any longer. + self.cache.lock().in_progress_lookups.pop(&block_root); + } + + pub fn contains(&self, block_root: Hash256) -> bool { + self.cache.lock().block_roots.contains(&block_root) + } + + pub fn metrics(&self) -> Option<(usize, usize)> { + let cache = self.cache.try_lock_for(METRICS_TIMEOUT)?; + Some((cache.block_roots.len(), cache.in_progress_lookups.len())) + } +} diff --git a/beacon_node/beacon_chain/tests/attestation_verification.rs b/beacon_node/beacon_chain/tests/attestation_verification.rs index f5942a2be2..3c675ec6a4 100644 --- a/beacon_node/beacon_chain/tests/attestation_verification.rs +++ b/beacon_node/beacon_chain/tests/attestation_verification.rs @@ -5,7 +5,7 @@ use beacon_chain::{ test_utils::{ test_spec, AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, }, - BeaconChain, BeaconChainTypes, WhenSlotSkipped, + BeaconChain, BeaconChainError, BeaconChainTypes, WhenSlotSkipped, }; use int_to_bytes::int_to_bytes32; use lazy_static::lazy_static; @@ -991,6 +991,81 @@ fn attestation_that_skips_epochs() { .expect("should gossip verify attestation that skips slots"); } +#[test] +fn attestation_to_finalized_block() { + let harness = get_harness(VALIDATOR_COUNT); + + // Extend the chain out a few epochs so we have some chain depth to play with. + harness.extend_chain( + MainnetEthSpec::slots_per_epoch() as usize * 4 + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ); + + let finalized_checkpoint = harness + .chain + .with_head(|head| Ok::<_, BeaconChainError>(head.beacon_state.finalized_checkpoint())) + .unwrap(); + assert!(finalized_checkpoint.epoch > 0); + + let current_slot = harness.get_current_slot(); + + let earlier_slot = finalized_checkpoint + .epoch + .start_slot(MainnetEthSpec::slots_per_epoch()) + - 1; + let earlier_block = harness + .chain + .block_at_slot(earlier_slot, WhenSlotSkipped::Prev) + .expect("should not error getting block at slot") + .expect("should find block at slot"); + let earlier_block_root = earlier_block.canonical_root(); + assert_ne!(earlier_block_root, finalized_checkpoint.root); + + let mut state = harness + .chain + .get_state(&earlier_block.state_root(), Some(earlier_slot)) + .expect("should not error getting state") + .expect("should find state"); + + while state.slot() < current_slot { + per_slot_processing(&mut state, None, &harness.spec).expect("should process slot"); + } + + let state_root = state.update_tree_hash_cache().unwrap(); + + let (attestation, subnet_id) = harness + .get_unaggregated_attestations( + &AttestationStrategy::AllValidators, + &state, + state_root, + earlier_block_root, + current_slot, + ) + .first() + .expect("should have at least one committee") + .first() + .cloned() + .expect("should have at least one attestation in committee"); + assert_eq!(attestation.data.beacon_block_root, earlier_block_root); + + // Attestation should be rejected for attesting to a pre-finalization block. + let res = harness + .chain + .verify_unaggregated_attestation_for_gossip(&attestation, Some(subnet_id)); + assert!( + matches!(res, Err(AttnError:: HeadBlockFinalized { beacon_block_root }) + if beacon_block_root == earlier_block_root + ) + ); + + // Pre-finalization block cache should contain the block root. + assert!(harness + .chain + .pre_finalization_block_cache + .contains(earlier_block_root)); +} + #[test] fn verify_aggregate_for_gossip_doppelganger_detection() { let harness = get_harness(VALIDATOR_COUNT); 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 9ece18d02c..132bed1b72 100644 --- a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs +++ b/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs @@ -1701,6 +1701,26 @@ impl Worker { "attn_too_many_skipped_slots", ); } + AttnError::HeadBlockFinalized { beacon_block_root } => { + debug!( + self.log, + "Rejected attestation to finalized block"; + "block_root" => ?beacon_block_root, + "attestation_slot" => failed_att.attestation().data.slot, + ); + + // We have to reject the message as it isn't a descendant of the finalized + // checkpoint. + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); + + // The peer that sent us this could be a lagger, or a spammer, or this failure could + // be due to us processing attestations extremely slowly. Don't be too harsh. + self.gossip_penalize_peer( + peer_id, + PeerAction::HighToleranceError, + "attn_to_finalized_block", + ); + } AttnError::BeaconChainError(BeaconChainError::DBError(Error::HotColdDBError( HotColdDBError::AttestationStateIsFinalized { .. }, ))) => { From ee000d521929fed0b137419b895904e3df739db0 Mon Sep 17 00:00:00 2001 From: Kirill Fedoseev Date: Thu, 27 Jan 2022 22:58:33 +0000 Subject: [PATCH 13/23] Native support for Gnosis Beacon Chain network (#2931) ## Proposed Changes Add a new hardcoded spec for the Gnosis Beacon Chain. Ideally, official Lighthouse executables will be able to connect to the gnosis beacon chain from now on, using `--network gnosis` CLI option. --- .github/workflows/release.yml | 8 +- Makefile | 8 +- boot_node/src/lib.rs | 3 + common/eth2_config/src/lib.rs | 10 +- .../gnosis/boot_enr.yaml | 5 + .../gnosis/config.yaml | 85 ++++++++++ .../gnosis/deploy_block.txt | 1 + .../gnosis/genesis.ssz.zip | Bin 0 -> 213933 bytes common/eth2_network_config/src/lib.rs | 8 +- consensus/types/presets/gnosis/altair.yaml | 24 +++ consensus/types/presets/gnosis/bellatrix.yaml | 21 +++ consensus/types/presets/gnosis/phase0.yaml | 94 +++++++++++ consensus/types/src/chain_spec.rs | 156 ++++++++++++++++++ consensus/types/src/eth_spec.rs | 52 +++++- consensus/types/src/preset.rs | 7 +- lcli/src/main.rs | 3 +- lighthouse/Cargo.toml | 2 + lighthouse/environment/src/lib.rs | 15 +- lighthouse/src/main.rs | 7 +- 19 files changed, 490 insertions(+), 19 deletions(-) create mode 100644 common/eth2_network_config/built_in_network_configs/gnosis/boot_enr.yaml create mode 100644 common/eth2_network_config/built_in_network_configs/gnosis/config.yaml create mode 100644 common/eth2_network_config/built_in_network_configs/gnosis/deploy_block.txt create mode 100644 common/eth2_network_config/built_in_network_configs/gnosis/genesis.ssz.zip create mode 100644 consensus/types/presets/gnosis/altair.yaml create mode 100644 consensus/types/presets/gnosis/bellatrix.yaml create mode 100644 consensus/types/presets/gnosis/phase0.yaml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4c57b8b1e7..87d309dc42 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -181,19 +181,19 @@ jobs: - name: Build Lighthouse for x86_64-apple-darwin portable if: matrix.arch == 'x86_64-apple-darwin-portable' - run: cargo install --path lighthouse --force --locked --features portable + run: cargo install --path lighthouse --force --locked --features portable,gnosis - name: Build Lighthouse for x86_64-apple-darwin modern if: matrix.arch == 'x86_64-apple-darwin' - run: cargo install --path lighthouse --force --locked --features modern + run: cargo install --path lighthouse --force --locked --features modern,gnosis - name: Build Lighthouse for Windows portable if: matrix.arch == 'x86_64-windows-portable' - run: cargo install --path lighthouse --force --locked --features portable + run: cargo install --path lighthouse --force --locked --features portable,gnosis - name: Build Lighthouse for Windows modern if: matrix.arch == 'x86_64-windows' - run: cargo install --path lighthouse --force --locked --features modern + run: cargo install --path lighthouse --force --locked --features modern,gnosis - name: Configure GPG and create artifacts if: startsWith(matrix.arch, 'x86_64-windows') != true diff --git a/Makefile b/Makefile index a4b880b806..c372b9ef8b 100644 --- a/Makefile +++ b/Makefile @@ -50,13 +50,13 @@ endif # optimized CPU functions that may not be available on some systems. This # results in a more portable binary with ~20% slower BLS verification. build-x86_64: - cross build --release --manifest-path lighthouse/Cargo.toml --target x86_64-unknown-linux-gnu --features modern + cross build --release --manifest-path lighthouse/Cargo.toml --target x86_64-unknown-linux-gnu --features modern,gnosis build-x86_64-portable: - cross build --release --manifest-path lighthouse/Cargo.toml --target x86_64-unknown-linux-gnu --features portable + cross build --release --manifest-path lighthouse/Cargo.toml --target x86_64-unknown-linux-gnu --features portable,gnosis build-aarch64: - cross build --release --manifest-path lighthouse/Cargo.toml --target aarch64-unknown-linux-gnu + cross build --release --manifest-path lighthouse/Cargo.toml --target aarch64-unknown-linux-gnu --features gnosis build-aarch64-portable: - cross build --release --manifest-path lighthouse/Cargo.toml --target aarch64-unknown-linux-gnu --features portable + cross build --release --manifest-path lighthouse/Cargo.toml --target aarch64-unknown-linux-gnu --features portable,gnosis # Create a `.tar.gz` containing a binary for a specific target. define tarball_release_binary diff --git a/boot_node/src/lib.rs b/boot_node/src/lib.rs index 2afc063808..6b933013fc 100644 --- a/boot_node/src/lib.rs +++ b/boot_node/src/lib.rs @@ -63,6 +63,9 @@ pub fn run( EthSpecId::Mainnet => { main::(lh_matches, bn_matches, eth2_network_config, log) } + EthSpecId::Gnosis => { + main::(lh_matches, bn_matches, eth2_network_config, log) + } } { slog::crit!(slog_scope::logger(), "{}", e); } diff --git a/common/eth2_config/src/lib.rs b/common/eth2_config/src/lib.rs index b45ad9d1e2..fafa15ef8d 100644 --- a/common/eth2_config/src/lib.rs +++ b/common/eth2_config/src/lib.rs @@ -53,6 +53,13 @@ impl Eth2Config { spec: ChainSpec::minimal(), } } + + pub fn gnosis() -> Self { + Self { + eth_spec_id: EthSpecId::Gnosis, + spec: ChainSpec::gnosis(), + } + } } /// A directory that can be built by downloading files via HTTP. @@ -229,5 +236,6 @@ macro_rules! define_hardcoded_nets { define_hardcoded_nets!( (mainnet, "mainnet", GENESIS_STATE_IS_KNOWN), (pyrmont, "pyrmont", GENESIS_STATE_IS_KNOWN), - (prater, "prater", GENESIS_STATE_IS_KNOWN) + (prater, "prater", GENESIS_STATE_IS_KNOWN), + (gnosis, "gnosis", GENESIS_STATE_IS_KNOWN) ); diff --git a/common/eth2_network_config/built_in_network_configs/gnosis/boot_enr.yaml b/common/eth2_network_config/built_in_network_configs/gnosis/boot_enr.yaml new file mode 100644 index 0000000000..4b232d8b32 --- /dev/null +++ b/common/eth2_network_config/built_in_network_configs/gnosis/boot_enr.yaml @@ -0,0 +1,5 @@ +# Gnosis Chain Team +- enr:-IS4QGmLwm7gFd0L0CEisllrb1op3v-wAGSc7_pwSMGgN3bOS9Fz7m1dWbwuuPHKqeETz9MbhjVuoWk0ohkyRv98kVoBgmlkgnY0gmlwhGjtlgaJc2VjcDI1NmsxoQLMdh0It9fJbuiLydZ9fpF6MRzgNle0vODaDiMqhbC7WIN1ZHCCIyg +- enr:-IS4QFUVG3dvLPCUEI7ycRvFm0Ieg_ITa5tALmJ9LI7dJ6ieT3J4fF9xLRjOoB4ApV-Rjp7HeLKzyTWG1xRdbFBNZPQBgmlkgnY0gmlwhErP5weJc2VjcDI1NmsxoQOBbaJBvx0-w_pyZUhQl9A510Ho2T0grE0K8JevzES99IN1ZHCCIyg +- enr:-Ku4QOQk8V-Hu2gxFzRXmLYIO4AvWDZhoMFwTf3n3DYm_mbsWv0ZitoqiN6JZUUj6Li6e1Jk1w2zFSVHKPMUP1g5tsgBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD5Jd3FAAAAZP__________gmlkgnY0gmlwhC1PTpmJc2VjcDI1NmsxoQL1Ynt5PoA0UOcHa1Rfn98rmnRlLzNuWTePPP4m4qHVroN1ZHCCKvg +- enr:-Ku4QFaTwgoms-EiiRIfHUH3FXprWUFgjHg4UuWvilqoUQtDbmTszVIxUEOwQUmA2qkiP-T9wXjc_rVUuh9cU7WgwbgBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD5Jd3FAAAAZP__________gmlkgnY0gmlwhC0hBmCJc2VjcDI1NmsxoQOpsg1XCrXmCwZKcSTcycLwldoKUMHPUpMEVGeg_EEhuYN1ZHCCKvg 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 new file mode 100644 index 0000000000..c34ebed7d5 --- /dev/null +++ b/common/eth2_network_config/built_in_network_configs/gnosis/config.yaml @@ -0,0 +1,85 @@ +# Gnosis Beacon Chain config + +# Extends the gnosis preset +PRESET_BASE: 'gnosis' + +# Transition +# --------------------------------------------------------------- +# TBD, 2**256-2**10 is a placeholder +TERMINAL_TOTAL_DIFFICULTY: 115792089237316195423570985008687907853269984665640564039457584007913129638912 +# By default, don't use these params +TERMINAL_BLOCK_HASH: 0x0000000000000000000000000000000000000000000000000000000000000000 +TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH: 18446744073709551615 + +# Genesis +# --------------------------------------------------------------- +# `2**12` (= 4,096) +MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 4096 +# Dec 8, 2021, 13:00 UTC +MIN_GENESIS_TIME: 1638968400 +# Gnosis Beacon Chain initial fork version +GENESIS_FORK_VERSION: 0x00000064 +# 6000 seconds (100 minutes) +GENESIS_DELAY: 6000 + + +# Forking +# --------------------------------------------------------------- +# Some forks are disabled for now: +# - These may be re-assigned to another fork-version later +# - Temporarily set to max uint64 value: 2**64 - 1 + +# Altair +ALTAIR_FORK_VERSION: 0x01000064 +ALTAIR_FORK_EPOCH: 512 +# Merge +BELLATRIX_FORK_VERSION: 0x02000064 +BELLATRIX_FORK_EPOCH: 18446744073709551615 +# Sharding +SHARDING_FORK_VERSION: 0x03000064 +SHARDING_FORK_EPOCH: 18446744073709551615 + +# TBD, 2**32 is a placeholder. Merge transition approach is in active R&D. +TRANSITION_TOTAL_DIFFICULTY: 4294967296 + + +# Time parameters +# --------------------------------------------------------------- +# 5 seconds +SECONDS_PER_SLOT: 5 +# 6 (estimate from Gnosis Chain) +SECONDS_PER_ETH1_BLOCK: 6 +# 2**8 (= 256) epochs ~8 hours +MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256 +# 2**8 (= 256) epochs ~8 hours +SHARD_COMMITTEE_PERIOD: 256 +# 2**10 (= 1024) ~1.4 hour +ETH1_FOLLOW_DISTANCE: 1024 + + +# Validator cycle +# --------------------------------------------------------------- +# 2**2 (= 4) +INACTIVITY_SCORE_BIAS: 4 +# 2**4 (= 16) +INACTIVITY_SCORE_RECOVERY_RATE: 16 +# 2**4 * 10**9 (= 16,000,000,000) Gwei +EJECTION_BALANCE: 16000000000 +# 2**2 (= 4) +MIN_PER_EPOCH_CHURN_LIMIT: 4 +# 2**12 (= 4096) +CHURN_LIMIT_QUOTIENT: 4096 + + +# Fork choice +# --------------------------------------------------------------- +# TODO: enable once proposer boosting is desired on mainnet +# 70% +# PROPOSER_SCORE_BOOST: 70 + +# Deposit contract +# --------------------------------------------------------------- +# Gnosis Chain +DEPOSIT_CHAIN_ID: 100 +DEPOSIT_NETWORK_ID: 100 +DEPOSIT_CONTRACT_ADDRESS: 0x0B98057eA310F4d31F2a452B414647007d1645d9 diff --git a/common/eth2_network_config/built_in_network_configs/gnosis/deploy_block.txt b/common/eth2_network_config/built_in_network_configs/gnosis/deploy_block.txt new file mode 100644 index 0000000000..0071371e28 --- /dev/null +++ b/common/eth2_network_config/built_in_network_configs/gnosis/deploy_block.txt @@ -0,0 +1 @@ +19469077 diff --git a/common/eth2_network_config/built_in_network_configs/gnosis/genesis.ssz.zip b/common/eth2_network_config/built_in_network_configs/gnosis/genesis.ssz.zip new file mode 100644 index 0000000000000000000000000000000000000000..3bfb326a244b933270c8646726253cd3b2b4c64e GIT binary patch literal 213933 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ξ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 H5D@().unwrap(); + if net.name == GNOSIS { + config.chain_spec::().unwrap(); + } else { + config.chain_spec::().unwrap(); + } assert_eq!( config.genesis_state_bytes.is_some(), diff --git a/consensus/types/presets/gnosis/altair.yaml b/consensus/types/presets/gnosis/altair.yaml new file mode 100644 index 0000000000..ff6bd98a00 --- /dev/null +++ b/consensus/types/presets/gnosis/altair.yaml @@ -0,0 +1,24 @@ +# Gnosis Beacon Chain preset - Altair + +# Updated penalty values +# --------------------------------------------------------------- +# 3 * 2**24 (= 50,331,648) +INACTIVITY_PENALTY_QUOTIENT_ALTAIR: 50331648 +# 2**6 (= 64) +MIN_SLASHING_PENALTY_QUOTIENT_ALTAIR: 64 +# 2 +PROPORTIONAL_SLASHING_MULTIPLIER_ALTAIR: 2 + + +# Sync committee +# --------------------------------------------------------------- +# 2**9 (= 512) +SYNC_COMMITTEE_SIZE: 512 +# 2**8 (= 256) +EPOCHS_PER_SYNC_COMMITTEE_PERIOD: 512 + + +# Sync protocol +# --------------------------------------------------------------- +# 1 +MIN_SYNC_COMMITTEE_PARTICIPANTS: 1 diff --git a/consensus/types/presets/gnosis/bellatrix.yaml b/consensus/types/presets/gnosis/bellatrix.yaml new file mode 100644 index 0000000000..e938af4792 --- /dev/null +++ b/consensus/types/presets/gnosis/bellatrix.yaml @@ -0,0 +1,21 @@ +# Gnosis Beacon Chain preset - Bellatrix + +# Updated penalty values +# --------------------------------------------------------------- +# 2**24 (= 16,777,216) +INACTIVITY_PENALTY_QUOTIENT_BELLATRIX: 16777216 +# 2**5 (= 32) +MIN_SLASHING_PENALTY_QUOTIENT_BELLATRIX: 32 +# 3 +PROPORTIONAL_SLASHING_MULTIPLIER_BELLATRIX: 3 + +# Execution +# --------------------------------------------------------------- +# 2**30 (= 1,073,741,824) +MAX_BYTES_PER_TRANSACTION: 1073741824 +# 2**20 (= 1,048,576) +MAX_TRANSACTIONS_PER_PAYLOAD: 1048576 +# 2**8 (= 256) +BYTES_PER_LOGS_BLOOM: 256 +# 2**5 (= 32) +MAX_EXTRA_DATA_BYTES: 32 diff --git a/consensus/types/presets/gnosis/phase0.yaml b/consensus/types/presets/gnosis/phase0.yaml new file mode 100644 index 0000000000..87c73e6fb7 --- /dev/null +++ b/consensus/types/presets/gnosis/phase0.yaml @@ -0,0 +1,94 @@ +# Gnosis Beacon Chain preset - Phase0 + +# Misc +# --------------------------------------------------------------- +# 2**6 (= 64) +MAX_COMMITTEES_PER_SLOT: 64 +# 2**7 (= 128) +TARGET_COMMITTEE_SIZE: 128 +# 2**11 (= 2,048) +MAX_VALIDATORS_PER_COMMITTEE: 2048 +# See issue 563 +SHUFFLE_ROUND_COUNT: 90 +# 4 +HYSTERESIS_QUOTIENT: 4 +# 1 (minus 0.25) +HYSTERESIS_DOWNWARD_MULTIPLIER: 1 +# 5 (plus 1.25) +HYSTERESIS_UPWARD_MULTIPLIER: 5 + + +# Fork Choice +# --------------------------------------------------------------- +# 2**3 (= 8) +SAFE_SLOTS_TO_UPDATE_JUSTIFIED: 8 + + +# Gwei values +# --------------------------------------------------------------- +# 2**0 * 10**9 (= 1,000,000,000) Gwei +MIN_DEPOSIT_AMOUNT: 1000000000 +# 2**5 * 10**9 (= 32,000,000,000) Gwei +MAX_EFFECTIVE_BALANCE: 32000000000 +# 2**0 * 10**9 (= 1,000,000,000) Gwei +EFFECTIVE_BALANCE_INCREMENT: 1000000000 + + +# Time parameters +# --------------------------------------------------------------- +# 2**0 (= 1) slots 5 seconds +MIN_ATTESTATION_INCLUSION_DELAY: 1 +# 2**4 (= 16) slots 1.87 minutes +SLOTS_PER_EPOCH: 16 +# 2**0 (= 1) epochs 1.87 minutes +MIN_SEED_LOOKAHEAD: 1 +# 2**2 (= 4) epochs 7.47 minutes +MAX_SEED_LOOKAHEAD: 4 +# 2**6 (= 64) epochs ~2 hours +EPOCHS_PER_ETH1_VOTING_PERIOD: 64 +# 2**13 (= 8,192) slots ~15.9 hours +SLOTS_PER_HISTORICAL_ROOT: 8192 +# 2**2 (= 4) epochs 7.47 minutes +MIN_EPOCHS_TO_INACTIVITY_PENALTY: 4 + + +# State list lengths +# --------------------------------------------------------------- +# 2**16 (= 65,536) epochs ~85 days +EPOCHS_PER_HISTORICAL_VECTOR: 65536 +# 2**13 (= 8,192) epochs ~10.6 days +EPOCHS_PER_SLASHINGS_VECTOR: 8192 +# 2**24 (= 16,777,216) historical roots, ~15,243 years +HISTORICAL_ROOTS_LIMIT: 16777216 +# 2**40 (= 1,099,511,627,776) validator spots +VALIDATOR_REGISTRY_LIMIT: 1099511627776 + + +# Reward and penalty quotients +# --------------------------------------------------------------- +# 2**6 (= 64) +BASE_REWARD_FACTOR: 25 +# 2**9 (= 512) +WHISTLEBLOWER_REWARD_QUOTIENT: 512 +# 2**3 (= 8) +PROPOSER_REWARD_QUOTIENT: 8 +# 2**26 (= 67,108,864) +INACTIVITY_PENALTY_QUOTIENT: 67108864 +# 2**7 (= 128) (lower safety margin at Phase 0 genesis) +MIN_SLASHING_PENALTY_QUOTIENT: 128 +# 1 (lower safety margin at Phase 0 genesis) +PROPORTIONAL_SLASHING_MULTIPLIER: 1 + + +# Max operations per block +# --------------------------------------------------------------- +# 2**4 (= 16) +MAX_PROPOSER_SLASHINGS: 16 +# 2**1 (= 2) +MAX_ATTESTER_SLASHINGS: 2 +# 2**7 (= 128) +MAX_ATTESTATIONS: 128 +# 2**4 (= 16) +MAX_DEPOSITS: 16 +# 2**4 (= 16) +MAX_VOLUNTARY_EXITS: 16 diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index f191eb8671..8f58003572 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -596,6 +596,161 @@ impl ChainSpec { ..ChainSpec::mainnet() } } + + /// Returns a `ChainSpec` compatible with the Gnosis Beacon Chain specification. + pub fn gnosis() -> Self { + Self { + /* + * Constants + */ + genesis_slot: Slot::new(0), + far_future_epoch: Epoch::new(u64::MAX), + base_rewards_per_epoch: 4, + deposit_contract_tree_depth: 32, + + /* + * Misc + */ + max_committees_per_slot: 64, + target_committee_size: 128, + min_per_epoch_churn_limit: 4, + churn_limit_quotient: 4_096, + shuffle_round_count: 90, + min_genesis_active_validator_count: 4_096, + min_genesis_time: 1638968400, // Dec 8, 2020 + hysteresis_quotient: 4, + hysteresis_downward_multiplier: 1, + hysteresis_upward_multiplier: 5, + + /* + * Gwei values + */ + min_deposit_amount: option_wrapper(|| { + u64::checked_pow(2, 0)?.checked_mul(u64::checked_pow(10, 9)?) + }) + .expect("calculation does not overflow"), + max_effective_balance: option_wrapper(|| { + u64::checked_pow(2, 5)?.checked_mul(u64::checked_pow(10, 9)?) + }) + .expect("calculation does not overflow"), + ejection_balance: option_wrapper(|| { + u64::checked_pow(2, 4)?.checked_mul(u64::checked_pow(10, 9)?) + }) + .expect("calculation does not overflow"), + effective_balance_increment: option_wrapper(|| { + u64::checked_pow(2, 0)?.checked_mul(u64::checked_pow(10, 9)?) + }) + .expect("calculation does not overflow"), + + /* + * Initial Values + */ + genesis_fork_version: [0x00, 0x00, 0x00, 0x64], + bls_withdrawal_prefix_byte: 0, + + /* + * Time parameters + */ + genesis_delay: 6000, // 100 minutes + seconds_per_slot: 5, + min_attestation_inclusion_delay: 1, + min_seed_lookahead: Epoch::new(1), + max_seed_lookahead: Epoch::new(4), + min_epochs_to_inactivity_penalty: 4, + min_validator_withdrawability_delay: Epoch::new(256), + shard_committee_period: 256, + + /* + * Reward and penalty quotients + */ + base_reward_factor: 25, + whistleblower_reward_quotient: 512, + proposer_reward_quotient: 8, + inactivity_penalty_quotient: u64::checked_pow(2, 26).expect("pow does not overflow"), + min_slashing_penalty_quotient: 128, + proportional_slashing_multiplier: 1, + + /* + * Signature domains + */ + domain_beacon_proposer: 0, + domain_beacon_attester: 1, + domain_randao: 2, + domain_deposit: 3, + domain_voluntary_exit: 4, + domain_selection_proof: 5, + domain_aggregate_and_proof: 6, + + /* + * Fork choice + */ + safe_slots_to_update_justified: 8, + proposer_score_boost: None, + + /* + * Eth1 + */ + eth1_follow_distance: 1024, + seconds_per_eth1_block: 6, + deposit_chain_id: 100, + deposit_network_id: 100, + deposit_contract_address: "0B98057eA310F4d31F2a452B414647007d1645d9" + .parse() + .expect("chain spec deposit contract address"), + + /* + * Altair hard fork params + */ + inactivity_penalty_quotient_altair: option_wrapper(|| { + u64::checked_pow(2, 24)?.checked_mul(3) + }) + .expect("calculation does not overflow"), + min_slashing_penalty_quotient_altair: u64::checked_pow(2, 6) + .expect("pow does not overflow"), + proportional_slashing_multiplier_altair: 2, + inactivity_score_bias: 4, + inactivity_score_recovery_rate: 16, + min_sync_committee_participants: 1, + epochs_per_sync_committee_period: Epoch::new(512), + domain_sync_committee: 7, + domain_sync_committee_selection_proof: 8, + domain_contribution_and_proof: 9, + altair_fork_version: [0x01, 0x00, 0x00, 0x64], + altair_fork_epoch: Some(Epoch::new(256)), + + /* + * Merge hard fork params + */ + inactivity_penalty_quotient_bellatrix: u64::checked_pow(2, 24) + .expect("pow does not overflow"), + min_slashing_penalty_quotient_bellatrix: u64::checked_pow(2, 5) + .expect("pow does not overflow"), + proportional_slashing_multiplier_bellatrix: 3, + bellatrix_fork_version: [0x02, 0x00, 0x00, 0x64], + bellatrix_fork_epoch: None, + terminal_total_difficulty: Uint256::MAX + .checked_sub(Uint256::from(2u64.pow(10))) + .expect("subtraction does not overflow") + // Add 1 since the spec declares `2**256 - 2**10` and we use + // `Uint256::MAX` which is `2*256- 1`. + .checked_add(Uint256::one()) + .expect("addition does not overflow"), + terminal_block_hash: Hash256::zero(), + terminal_block_hash_activation_epoch: Epoch::new(u64::MAX), + + /* + * Network specific + */ + boot_nodes: vec![], + network_id: 100, // Gnosis Chain network id + attestation_propagation_slot_range: 32, + attestation_subnet_count: 64, + random_subnets_per_validator: 1, + maximum_gossip_clock_disparity_millis: 500, + target_aggregators_per_committee: 16, + epochs_per_random_subnet_subscription: 256, + } + } } impl Default for ChainSpec { @@ -746,6 +901,7 @@ impl Config { match self.preset_base.as_str() { "minimal" => Some(EthSpecId::Minimal), "mainnet" => Some(EthSpecId::Mainnet), + "gnosis" => Some(EthSpecId::Gnosis), _ => None, } } diff --git a/consensus/types/src/eth_spec.rs b/consensus/types/src/eth_spec.rs index ae0cafe1ff..e616976026 100644 --- a/consensus/types/src/eth_spec.rs +++ b/consensus/types/src/eth_spec.rs @@ -3,17 +3,17 @@ use crate::*; use safe_arith::SafeArith; use serde_derive::{Deserialize, Serialize}; use ssz_types::typenum::{ - Unsigned, U0, U1024, U1073741824, U1099511627776, U128, U16, U16777216, U2, U2048, U32, U4, - U4096, U512, U64, U65536, U8, U8192, + bit::B0, UInt, Unsigned, U0, U1024, U1048576, U1073741824, U1099511627776, U128, U16, + U16777216, U2, U2048, U256, U32, U4, U4096, U512, U625, U64, U65536, U8, U8192, }; use std::fmt::{self, Debug}; use std::str::FromStr; -use ssz_types::typenum::{bit::B0, UInt, U1048576, U256, U625}; pub type U5000 = UInt, B0>, B0>; // 625 * 8 = 5000 const MAINNET: &str = "mainnet"; const MINIMAL: &str = "minimal"; +pub const GNOSIS: &str = "gnosis"; /// Used to identify one of the `EthSpec` instances defined here. #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -21,6 +21,7 @@ const MINIMAL: &str = "minimal"; pub enum EthSpecId { Mainnet, Minimal, + Gnosis, } impl FromStr for EthSpecId { @@ -30,6 +31,7 @@ impl FromStr for EthSpecId { match s { MAINNET => Ok(EthSpecId::Mainnet), MINIMAL => Ok(EthSpecId::Minimal), + GNOSIS => Ok(EthSpecId::Gnosis), _ => Err(format!("Unknown eth spec: {}", s)), } } @@ -40,6 +42,7 @@ impl fmt::Display for EthSpecId { let s = match self { EthSpecId::Mainnet => MAINNET, EthSpecId::Minimal => MINIMAL, + EthSpecId::Gnosis => GNOSIS, }; write!(f, "{}", s) } @@ -317,3 +320,46 @@ impl EthSpec for MinimalEthSpec { EthSpecId::Minimal } } + +/// Gnosis Beacon Chain specifications. +#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)] +pub struct GnosisEthSpec; + +impl EthSpec for GnosisEthSpec { + type JustificationBitsLength = U4; + type SubnetBitfieldLength = U64; + type MaxValidatorsPerCommittee = U2048; + type GenesisEpoch = U0; + type SlotsPerEpoch = U16; + type EpochsPerEth1VotingPeriod = U64; + type SlotsPerHistoricalRoot = U8192; + type EpochsPerHistoricalVector = U65536; + type EpochsPerSlashingsVector = U8192; + type HistoricalRootsLimit = U16777216; + type ValidatorRegistryLimit = U1099511627776; + type MaxProposerSlashings = U16; + type MaxAttesterSlashings = U2; + type MaxAttestations = U128; + type MaxDeposits = U16; + type MaxVoluntaryExits = U16; + type SyncCommitteeSize = U512; + type SyncCommitteeSubnetCount = U4; + type MaxBytesPerTransaction = U1073741824; // 1,073,741,824 + type MaxTransactionsPerPayload = U1048576; // 1,048,576 + type BytesPerLogsBloom = U256; + type GasLimitDenominator = U1024; + type MinGasLimit = U5000; + type MaxExtraDataBytes = U32; + type SyncSubcommitteeSize = U128; // 512 committee size / 4 sync committee subnet count + type MaxPendingAttestations = U2048; // 128 max attestations * 16 slots per epoch + type SlotsPerEth1VotingPeriod = U1024; // 64 epochs * 16 slots per epoch + + fn default_spec() -> ChainSpec { + ChainSpec::gnosis() + } + + fn spec_name() -> EthSpecId { + EthSpecId::Gnosis + } +} diff --git a/consensus/types/src/preset.rs b/consensus/types/src/preset.rs index ccda1a06a0..8ee38e46a6 100644 --- a/consensus/types/src/preset.rs +++ b/consensus/types/src/preset.rs @@ -187,7 +187,7 @@ impl BellatrixPreset { #[cfg(test)] mod test { use super::*; - use crate::{MainnetEthSpec, MinimalEthSpec}; + use crate::{GnosisEthSpec, MainnetEthSpec, MinimalEthSpec}; use serde::de::DeserializeOwned; use std::env; use std::fs::File; @@ -226,6 +226,11 @@ mod test { preset_test::(); } + #[test] + fn gnosis_presets_consistent() { + preset_test::(); + } + #[test] fn minimal_presets_consistent() { preset_test::(); diff --git a/lcli/src/main.rs b/lcli/src/main.rs index a494cd3822..9af4b25548 100644 --- a/lcli/src/main.rs +++ b/lcli/src/main.rs @@ -38,7 +38,7 @@ fn main() { .value_name("STRING") .takes_value(true) .required(true) - .possible_values(&["minimal", "mainnet"]) + .possible_values(&["minimal", "mainnet", "gnosis"]) .default_value("mainnet") .global(true), ) @@ -665,6 +665,7 @@ fn main() { .and_then(|eth_spec_id| match eth_spec_id { EthSpecId::Minimal => run(EnvironmentBuilder::minimal(), &matches), EthSpecId::Mainnet => run(EnvironmentBuilder::mainnet(), &matches), + EthSpecId::Gnosis => run(EnvironmentBuilder::gnosis(), &matches), }); match result { diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 130322e0e9..2429b6606d 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -16,6 +16,8 @@ modern = ["bls/supranational-force-adx"] milagro = ["bls/milagro"] # Support minimal spec (used for testing only). spec-minimal = [] +# Support Gnosis spec and Gnosis Beacon Chain. +gnosis = [] [dependencies] beacon_node = { "path" = "../beacon_node" } diff --git a/lighthouse/environment/src/lib.rs b/lighthouse/environment/src/lib.rs index e536d3c95b..448c84b54d 100644 --- a/lighthouse/environment/src/lib.rs +++ b/lighthouse/environment/src/lib.rs @@ -21,7 +21,7 @@ use std::path::PathBuf; use std::sync::Arc; use task_executor::{ShutdownReason, TaskExecutor}; use tokio::runtime::{Builder as RuntimeBuilder, Runtime}; -use types::{EthSpec, MainnetEthSpec, MinimalEthSpec}; +use types::{EthSpec, GnosisEthSpec, MainnetEthSpec, MinimalEthSpec}; #[cfg(target_family = "unix")] use { @@ -87,6 +87,19 @@ impl EnvironmentBuilder { } } +impl EnvironmentBuilder { + /// Creates a new builder using the `gnosis` eth2 specification. + pub fn gnosis() -> Self { + Self { + runtime: None, + log: None, + eth_spec_instance: GnosisEthSpec, + eth2_config: Eth2Config::gnosis(), + eth2_network_config: None, + } + } +} + impl EnvironmentBuilder { /// Specifies that a multi-threaded tokio runtime should be used. Ideal for production uses. /// diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index 693b3de821..51c1075cdb 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -52,11 +52,12 @@ fn main() { "{}\n\ BLS library: {}\n\ SHA256 hardware acceleration: {}\n\ - Specs: mainnet (true), minimal ({})", + Specs: mainnet (true), minimal ({}), gnosis ({})", VERSION.replace("Lighthouse/", ""), bls_library_name(), have_sha_extensions(), cfg!(feature = "spec-minimal"), + cfg!(feature = "gnosis"), ).as_str() ) .arg( @@ -302,9 +303,11 @@ fn main() { match eth_spec_id { EthSpecId::Mainnet => run(EnvironmentBuilder::mainnet(), &matches, eth2_network_config), + #[cfg(feature = "gnosis")] + EthSpecId::Gnosis => run(EnvironmentBuilder::gnosis(), &matches, eth2_network_config), #[cfg(feature = "spec-minimal")] EthSpecId::Minimal => run(EnvironmentBuilder::minimal(), &matches, eth2_network_config), - #[cfg(not(feature = "spec-minimal"))] + #[cfg(not(all(feature = "spec-minimal", feature = "gnosis")))] other => { eprintln!( "Eth spec `{}` is not supported by this build of Lighthouse", From e961ff60b49c17f93ad42597ff119fa60aa8015c Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Sun, 30 Jan 2022 23:22:04 +0000 Subject: [PATCH 14/23] Implement standard keystore API (#2736) ## Issue Addressed Implements the standard key manager API from https://ethereum.github.io/keymanager-APIs/, formerly https://github.com/ethereum/beacon-APIs/pull/151 Related to https://github.com/sigp/lighthouse/issues/2557 ## Proposed Changes - [x] Add all of the new endpoints from the standard API: GET, POST and DELETE. - [x] Add a `validators.enabled` column to the slashing protection database to support atomic disable + export. - [x] Add tests for all the common sequential accesses of the API - [x] Add tests for interactions with remote signer validators - [x] Add end-to-end tests for migration of validators from one VC to another - [x] Implement the authentication scheme from the standard (token bearer auth) ## Additional Info The `enabled` column in the validators SQL database is necessary to prevent a race condition when exporting slashing protection data. Without the slashing protection database having a way of knowing that a key has been disabled, a concurrent request to sign a message could insert a new record into the database. The `delete_concurrent_with_signing` test exercises this code path, and was indeed failing before the `enabled` column was added. The validator client authentication has been modified from basic auth to bearer auth, with basic auth preserved for backwards compatibility. --- Cargo.lock | 12 + book/src/api-vc-auth-header.md | 21 +- book/src/api-vc-endpoints.md | 47 +- book/src/api-vc.md | 9 +- common/account_utils/src/lib.rs | 16 +- .../src/validator_definitions.rs | 14 +- common/eth2/Cargo.toml | 3 +- common/eth2/src/lib.rs | 9 + common/eth2/src/lighthouse_vc/http_client.rs | 232 ++++- common/eth2/src/lighthouse_vc/mod.rs | 1 + common/eth2/src/lighthouse_vc/std_types.rs | 104 ++ common/eth2/src/lighthouse_vc/types.rs | 1 + common/validator_dir/src/builder.rs | 9 +- consensus/serde_utils/Cargo.toml | 4 +- consensus/serde_utils/src/json_str.rs | 25 + consensus/serde_utils/src/lib.rs | 1 + validator_client/Cargo.toml | 3 +- .../slashing_protection/Cargo.toml | 5 + .../v0_no_enabled_column.sqlite | Bin 0 -> 28672 bytes .../slashing_protection/src/lib.rs | 1 + .../src/registration_tests.rs | 42 + .../src/slashing_database.rs | 135 ++- .../slashing_protection/tests/main.rs | 2 + .../slashing_protection/tests/migration.rs | 68 ++ validator_client/src/http_api/api_secret.rs | 25 +- validator_client/src/http_api/keystores.rs | 290 ++++++ validator_client/src/http_api/mod.rs | 106 +- validator_client/src/http_api/tests.rs | 50 +- .../src/http_api/tests/keystores.rs | 977 ++++++++++++++++++ .../src/initialized_validators.rs | 148 ++- validator_client/src/signing_method.rs | 3 +- validator_client/src/validator_store.rs | 48 +- 32 files changed, 2284 insertions(+), 127 deletions(-) create mode 100644 common/eth2/src/lighthouse_vc/std_types.rs create mode 100644 consensus/serde_utils/src/json_str.rs create mode 100644 validator_client/slashing_protection/migration-tests/v0_no_enabled_column.sqlite create mode 100644 validator_client/slashing_protection/tests/main.rs create mode 100644 validator_client/slashing_protection/tests/migration.rs create mode 100644 validator_client/src/http_api/keystores.rs create mode 100644 validator_client/src/http_api/tests/keystores.rs diff --git a/Cargo.lock b/Cargo.lock index 586cdaf181..826c01b09d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1556,6 +1556,7 @@ dependencies = [ "sensitive_url", "serde", "serde_json", + "slashing_protection", "store", "types", ] @@ -4692,6 +4693,7 @@ dependencies = [ "rand_chacha 0.2.2", "rand_core 0.5.1", "rand_hc 0.2.0", + "rand_pcg", ] [[package]] @@ -4762,6 +4764,15 @@ dependencies = [ "rand_core 0.6.3", ] +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + [[package]] name = "rand_xorshift" version = "0.2.0" @@ -6677,6 +6688,7 @@ dependencies = [ "lighthouse_metrics", "lighthouse_version", "lockfile", + "logging", "monitoring_api", "parking_lot", "rand 0.7.3", diff --git a/book/src/api-vc-auth-header.md b/book/src/api-vc-auth-header.md index d09a9e54a2..33f6f6ff7a 100644 --- a/book/src/api-vc-auth-header.md +++ b/book/src/api-vc-auth-header.md @@ -6,13 +6,13 @@ The validator client HTTP server requires that all requests have the following HTTP header: - Name: `Authorization` -- Value: `Basic ` +- Value: `Bearer ` Where `` is a string that can be obtained from the validator client host. Here is an example `Authorization` header: ``` -Authorization Basic api-token-0x03eace4c98e8f77477bb99efb74f9af10d800bd3318f92c33b719a4644254d4123 +Authorization: Bearer api-token-0x03eace4c98e8f77477bb99efb74f9af10d800bd3318f92c33b719a4644254d4123 ``` ## Obtaining the API token @@ -35,12 +35,27 @@ to the file containing the api token. Sep 28 19:17:52.615 INFO HTTP API started api_token_file: "$HOME/prater/validators/api-token.txt", listen_address: 127.0.0.1:5062 ``` +The _path_ to the API token may also be fetched from the HTTP API itself (this endpoint is the only +one accessible without the token): + +```bash +curl http://localhost:5062/lighthouse/auth +``` + +Response: + +```json +{ + "token_path": "/home/karlm/.lighthouse/prater/validators/api-token.txt" +} +``` + ## Example Here is an example `curl` command using the API token in the `Authorization` header: ```bash -curl localhost:5062/lighthouse/version -H "Authorization: Basic api-token-0x03eace4c98e8f77477bb99efb74f9af10d800bd3318f92c33b719a4644254d4123" +curl localhost:5062/lighthouse/version -H "Authorization: Bearer api-token-0x03eace4c98e8f77477bb99efb74f9af10d800bd3318f92c33b719a4644254d4123" ``` The server should respond with its version: diff --git a/book/src/api-vc-endpoints.md b/book/src/api-vc-endpoints.md index 16fd8ff8a7..14d18312e5 100644 --- a/book/src/api-vc-endpoints.md +++ b/book/src/api-vc-endpoints.md @@ -4,15 +4,19 @@ HTTP Path | Description | | --- | -- | -[`GET /lighthouse/version`](#get-lighthouseversion) | Get the Lighthouse software version -[`GET /lighthouse/health`](#get-lighthousehealth) | Get information about the host machine -[`GET /lighthouse/spec`](#get-lighthousespec) | Get the Eth2 specification used by the validator -[`GET /lighthouse/validators`](#get-lighthousevalidators) | List all validators -[`GET /lighthouse/validators/:voting_pubkey`](#get-lighthousevalidatorsvoting_pubkey) | Get a specific validator -[`PATCH /lighthouse/validators/:voting_pubkey`](#patch-lighthousevalidatorsvoting_pubkey) | Update a specific validator +[`GET /lighthouse/version`](#get-lighthouseversion) | Get the Lighthouse software version. +[`GET /lighthouse/health`](#get-lighthousehealth) | Get information about the host machine. +[`GET /lighthouse/spec`](#get-lighthousespec) | Get the Eth2 specification used by the validator. +[`GET /lighthouse/auth`](#get-lighthouseauth) | Get the location of the authorization token. +[`GET /lighthouse/validators`](#get-lighthousevalidators) | List all validators. +[`GET /lighthouse/validators/:voting_pubkey`](#get-lighthousevalidatorsvoting_pubkey) | Get a specific validator. +[`PATCH /lighthouse/validators/:voting_pubkey`](#patch-lighthousevalidatorsvoting_pubkey) | Update a specific validator. [`POST /lighthouse/validators`](#post-lighthousevalidators) | Create a new validator and mnemonic. [`POST /lighthouse/validators/keystore`](#post-lighthousevalidatorskeystore) | Import a keystore. [`POST /lighthouse/validators/mnemonic`](#post-lighthousevalidatorsmnemonic) | Create a new validator from an existing mnemonic. +[`POST /lighthouse/validators/web3signer`](#post-lighthousevalidatorsweb3signer) | Add web3signer validators. + +In addition to the above endpoints Lighthouse also supports all of the [standard keymanager APIs](https://ethereum.github.io/keymanager-APIs/). ## `GET /lighthouse/version` @@ -153,6 +157,37 @@ Typical Responses | 200 } ``` +## `GET /lighthouse/auth` + +Fetch the filesystem path of the [authorization token](./api-vc-auth-header.md). +Unlike the other endpoints this may be called _without_ providing an authorization token. + +This API is intended to be called from the same machine as the validator client, so that the token +file may be read by a local user with access rights. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/lighthouse/auth` +Method | GET +Required Headers | - +Typical Responses | 200 + +### Example Path + +``` +localhost:5062/lighthouse/auth +``` + +### Example Response Body + +```json +{ + "token_path": "/home/karlm/.lighthouse/prater/validators/api-token.txt" +} +``` + ## `GET /lighthouse/validators` Lists all validators managed by this validator client. diff --git a/book/src/api-vc.md b/book/src/api-vc.md index 6ee79d4f72..74c493ebea 100644 --- a/book/src/api-vc.md +++ b/book/src/api-vc.md @@ -1,9 +1,12 @@ # Validator Client API -Lighthouse implements a HTTP/JSON API for the validator client. Since there is -no Eth2 standard validator client API, Lighthouse has defined its own. +Lighthouse implements a JSON HTTP API for the validator client which enables programmatic management +of validators and keys. -A full list of endpoints can be found in [Endpoints](./api-vc-endpoints.md). +The API includes all of the endpoints from the [standard keymanager +API](https://ethereum.github.io/keymanager-APIs/) that is implemented by other clients and remote +signers. It also includes some Lighthouse-specific endpoints which are described in +[Endpoints](./api-vc-endpoints.md). > Note: All requests to the HTTP server must supply an > [`Authorization`](./api-vc-auth-header.md) header. All responses contain a diff --git a/common/account_utils/src/lib.rs b/common/account_utils/src/lib.rs index dc79a1f203..89de380385 100644 --- a/common/account_utils/src/lib.rs +++ b/common/account_utils/src/lib.rs @@ -85,15 +85,23 @@ pub fn write_file_via_temporary( Ok(()) } -/// Generates a random alphanumeric password of length `DEFAULT_PASSWORD_LEN`. +/// Generates a random alphanumeric password of length `DEFAULT_PASSWORD_LEN` as `PlainText`. pub fn random_password() -> PlainText { + random_password_raw_string().into_bytes().into() +} + +/// Generates a random alphanumeric password of length `DEFAULT_PASSWORD_LEN` as `ZeroizeString`. +pub fn random_password_string() -> ZeroizeString { + random_password_raw_string().into() +} + +/// Common implementation for `random_password` and `random_password_string`. +fn random_password_raw_string() -> String { rand::thread_rng() .sample_iter(&Alphanumeric) .take(DEFAULT_PASSWORD_LEN) .map(char::from) - .collect::() - .into_bytes() - .into() + .collect() } /// Remove any number of newline or carriage returns from the end of a vector of bytes. diff --git a/common/account_utils/src/validator_definitions.rs b/common/account_utils/src/validator_definitions.rs index 418c0fb3c6..d66683bee0 100644 --- a/common/account_utils/src/validator_definitions.rs +++ b/common/account_utils/src/validator_definitions.rs @@ -46,9 +46,6 @@ pub enum Error { } /// Defines how the validator client should attempt to sign messages for this validator. -/// -/// Presently there is only a single variant, however we expect more variants to arise (e.g., -/// remote signing). #[derive(Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type")] pub enum SigningDefinition { @@ -78,6 +75,12 @@ pub enum SigningDefinition { }, } +impl SigningDefinition { + pub fn is_local_keystore(&self) -> bool { + matches!(self, SigningDefinition::LocalKeystore { .. }) + } +} + /// A validator that may be initialized by this validator client. /// /// Presently there is only a single variant, however we expect more variants to arise (e.g., @@ -293,6 +296,11 @@ impl ValidatorDefinitions { Ok(()) } + /// Retain only the definitions matching the given predicate. + pub fn retain(&mut self, f: impl FnMut(&ValidatorDefinition) -> bool) { + self.0.retain(f); + } + /// Adds a new `ValidatorDefinition` to `self`. pub fn push(&mut self, def: ValidatorDefinition) { self.0.push(def) diff --git a/common/eth2/Cargo.toml b/common/eth2/Cargo.toml index f1c9f5061e..d039a0c91a 100644 --- a/common/eth2/Cargo.toml +++ b/common/eth2/Cargo.toml @@ -25,6 +25,7 @@ eth2_ssz_derive = "0.3.0" 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 } [target.'cfg(target_os = "linux")'.dependencies] # TODO: update psutil once fix is merged: https://github.com/rust-psutil/rust-psutil/pull/93 @@ -35,4 +36,4 @@ procinfo = { version = "0.4.2", optional = true } [features] default = ["lighthouse"] -lighthouse = ["proto_array", "psutil", "procinfo", "store"] +lighthouse = ["proto_array", "psutil", "procinfo", "store", "slashing_protection"] diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 8dc808c265..608a2c9e22 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -28,6 +28,7 @@ use serde::{de::DeserializeOwned, Serialize}; use std::convert::TryFrom; use std::fmt; use std::iter::Iterator; +use std::path::PathBuf; use std::time::Duration; pub const V1: EndpointVersion = EndpointVersion(1); @@ -59,6 +60,12 @@ pub enum Error { InvalidServerSentEvent(String), /// The server returned an invalid SSZ response. InvalidSsz(ssz::DecodeError), + /// An I/O error occurred while loading an API token from disk. + TokenReadError(PathBuf, std::io::Error), + /// The client has been configured without a server pubkey, but requires one for this request. + NoServerPubkey, + /// The client has been configured without an API token, but requires one for this request. + NoToken, } impl From for Error { @@ -82,6 +89,8 @@ impl Error { Error::InvalidJson(_) => None, Error::InvalidServerSentEvent(_) => None, Error::InvalidSsz(_) => None, + Error::TokenReadError(..) => None, + Error::NoServerPubkey | Error::NoToken => None, } } } diff --git a/common/eth2/src/lighthouse_vc/http_client.rs b/common/eth2/src/lighthouse_vc/http_client.rs index cd640e6158..e7c74668e8 100644 --- a/common/eth2/src/lighthouse_vc/http_client.rs +++ b/common/eth2/src/lighthouse_vc/http_client.rs @@ -10,6 +10,9 @@ use reqwest::{ use ring::digest::{digest, SHA256}; use sensitive_url::SensitiveUrl; use serde::{de::DeserializeOwned, Serialize}; +use std::fmt::{self, Display}; +use std::fs; +use std::path::Path; pub use reqwest; pub use reqwest::{Response, StatusCode, Url}; @@ -20,18 +23,36 @@ pub use reqwest::{Response, StatusCode, Url}; pub struct ValidatorClientHttpClient { client: reqwest::Client, server: SensitiveUrl, - secret: ZeroizeString, - server_pubkey: PublicKey, - send_authorization_header: bool, + secret: Option, + server_pubkey: Option, + authorization_header: AuthorizationHeader, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum AuthorizationHeader { + /// Do not send any Authorization header. + Omit, + /// Send a `Basic` Authorization header (legacy). + Basic, + /// Send a `Bearer` Authorization header. + Bearer, +} + +impl Display for AuthorizationHeader { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // The `Omit` variant should never be `Display`ed, but would result in a harmless rejection. + write!(f, "{:?}", self) + } } /// Parse an API token and return a secp256k1 public key. -pub fn parse_pubkey(secret: &str) -> Result { +/// +/// If the token does not start with the Lighthouse token prefix then `Ok(None)` will be returned. +/// An error will be returned if the token looks like a Lighthouse token but doesn't correspond to a +/// valid public key. +pub fn parse_pubkey(secret: &str) -> Result, Error> { let secret = if !secret.starts_with(SECRET_PREFIX) { - return Err(Error::InvalidSecret(format!( - "secret does not start with {}", - SECRET_PREFIX - ))); + return Ok(None); } else { &secret[SECRET_PREFIX.len()..] }; @@ -52,16 +73,31 @@ pub fn parse_pubkey(secret: &str) -> Result { PublicKey::parse_compressed(&arr) .map_err(|e| Error::InvalidSecret(format!("invalid secp256k1 pubkey: {:?}", e))) }) + .map(Some) } impl ValidatorClientHttpClient { + /// Create a new client pre-initialised with an API token. pub fn new(server: SensitiveUrl, secret: String) -> Result { Ok(Self { client: reqwest::Client::new(), server, server_pubkey: parse_pubkey(&secret)?, - secret: secret.into(), - send_authorization_header: true, + secret: Some(secret.into()), + authorization_header: AuthorizationHeader::Bearer, + }) + } + + /// Create a client without an API token. + /// + /// A token can be fetched by using `self.get_auth`, and then reading the token from disk. + pub fn new_unauthenticated(server: SensitiveUrl) -> Result { + Ok(Self { + client: reqwest::Client::new(), + server, + secret: None, + server_pubkey: None, + authorization_header: AuthorizationHeader::Omit, }) } @@ -74,8 +110,35 @@ impl ValidatorClientHttpClient { client, server, server_pubkey: parse_pubkey(&secret)?, - secret: secret.into(), - send_authorization_header: true, + secret: Some(secret.into()), + authorization_header: AuthorizationHeader::Bearer, + }) + } + + /// Get a reference to this client's API token, if any. + pub fn api_token(&self) -> Option<&ZeroizeString> { + self.secret.as_ref() + } + + /// Read an API token from the specified `path`, stripping any trailing whitespace. + pub fn load_api_token_from_file(path: &Path) -> Result { + let token = fs::read_to_string(path).map_err(|e| Error::TokenReadError(path.into(), e))?; + Ok(ZeroizeString::from(token.trim_end().to_string())) + } + + /// Add an authentication token to use when making requests. + /// + /// If the token is Lighthouse-like, a pubkey derivation will be attempted. In the case + /// of failure the token will still be stored, and the client can continue to be used to + /// communicate with non-Lighthouse nodes. + pub fn add_auth_token(&mut self, token: ZeroizeString) -> Result<(), Error> { + let pubkey_res = parse_pubkey(token.as_str()); + + self.secret = Some(token); + self.authorization_header = AuthorizationHeader::Bearer; + + pubkey_res.map(|opt_pubkey| { + self.server_pubkey = opt_pubkey; }) } @@ -84,10 +147,20 @@ impl ValidatorClientHttpClient { /// Failing to send the `Authorization` header will cause the VC to reject requests with a 403. /// This function is intended only for testing purposes. pub fn send_authorization_header(&mut self, should_send: bool) { - self.send_authorization_header = should_send; + if should_send { + self.authorization_header = AuthorizationHeader::Bearer; + } else { + self.authorization_header = AuthorizationHeader::Omit; + } + } + + /// Use the legacy basic auth style (bearer auth preferred by default now). + pub fn use_basic_auth(&mut self) { + self.authorization_header = AuthorizationHeader::Basic; } async fn signed_body(&self, response: Response) -> Result { + let server_pubkey = self.server_pubkey.as_ref().ok_or(Error::NoServerPubkey)?; let sig = response .headers() .get("Signature") @@ -105,7 +178,7 @@ impl ValidatorClientHttpClient { .ok() .and_then(|bytes| { let sig = Signature::parse_der(&bytes).ok()?; - Some(libsecp256k1::verify(&message, &sig, &self.server_pubkey)) + Some(libsecp256k1::verify(&message, &sig, server_pubkey)) }) .filter(|is_valid| *is_valid) .ok_or(Error::InvalidSignatureHeader)?; @@ -121,11 +194,18 @@ impl ValidatorClientHttpClient { fn headers(&self) -> Result { let mut headers = HeaderMap::new(); - if self.send_authorization_header { - let header_value = HeaderValue::from_str(&format!("Basic {}", self.secret.as_str())) - .map_err(|e| { - Error::InvalidSecret(format!("secret is invalid as a header value: {}", e)) - })?; + if self.authorization_header == AuthorizationHeader::Basic + || self.authorization_header == AuthorizationHeader::Bearer + { + let secret = self.secret.as_ref().ok_or(Error::NoToken)?; + let header_value = HeaderValue::from_str(&format!( + "{} {}", + self.authorization_header, + secret.as_str() + )) + .map_err(|e| { + Error::InvalidSecret(format!("secret is invalid as a header value: {}", e)) + })?; headers.insert("Authorization", header_value); } @@ -133,8 +213,8 @@ impl ValidatorClientHttpClient { Ok(headers) } - /// Perform a HTTP GET request. - async fn get(&self, url: U) -> Result { + /// Perform a HTTP GET request, returning the `Response` for further processing. + async fn get_response(&self, url: U) -> Result { let response = self .client .get(url) @@ -142,20 +222,25 @@ impl ValidatorClientHttpClient { .send() .await .map_err(Error::Reqwest)?; - let response = ok_or_error(response).await?; + ok_or_error(response).await + } + + async fn get(&self, url: U) -> Result { + let response = self.get_response(url).await?; self.signed_json(response).await } + async fn get_unsigned(&self, url: U) -> Result { + self.get_response(url) + .await? + .json() + .await + .map_err(Error::Reqwest) + } + /// Perform a HTTP GET request, returning `None` on a 404 error. async fn get_opt(&self, url: U) -> Result, Error> { - let response = self - .client - .get(url) - .headers(self.headers()?) - .send() - .await - .map_err(Error::Reqwest)?; - match ok_or_error(response).await { + match self.get_response(url).await { Ok(resp) => self.signed_json(resp).await.map(Option::Some), Err(err) => { if err.status() == Some(StatusCode::NOT_FOUND) { @@ -168,11 +253,11 @@ impl ValidatorClientHttpClient { } /// Perform a HTTP POST request. - async fn post( + async fn post_with_raw_response( &self, url: U, body: &T, - ) -> Result { + ) -> Result { let response = self .client .post(url) @@ -181,10 +266,27 @@ impl ValidatorClientHttpClient { .send() .await .map_err(Error::Reqwest)?; - let response = ok_or_error(response).await?; + ok_or_error(response).await + } + + async fn post( + &self, + url: U, + body: &T, + ) -> Result { + let response = self.post_with_raw_response(url, body).await?; self.signed_json(response).await } + async fn post_with_unsigned_response( + &self, + url: U, + body: &T, + ) -> Result { + let response = self.post_with_raw_response(url, body).await?; + Ok(response.json().await?) + } + /// Perform a HTTP PATCH request. async fn patch(&self, url: U, body: &T) -> Result<(), Error> { let response = self @@ -200,6 +302,24 @@ impl ValidatorClientHttpClient { Ok(()) } + /// Perform a HTTP DELETE request. + async fn delete_with_unsigned_response( + &self, + url: U, + body: &T, + ) -> Result { + let response = self + .client + .delete(url) + .headers(self.headers()?) + .json(body) + .send() + .await + .map_err(Error::Reqwest)?; + let response = ok_or_error(response).await?; + Ok(response.json().await?) + } + /// `GET lighthouse/version` pub async fn get_lighthouse_version(&self) -> Result, Error> { let mut path = self.server.full.clone(); @@ -317,7 +437,7 @@ impl ValidatorClientHttpClient { pub async fn post_lighthouse_validators_web3signer( &self, request: &[Web3SignerValidatorRequest], - ) -> Result, Error> { + ) -> Result<(), Error> { let mut path = self.server.full.clone(); path.path_segments_mut() @@ -345,6 +465,50 @@ impl ValidatorClientHttpClient { self.patch(path, &ValidatorPatchRequest { enabled }).await } + + fn make_keystores_url(&self) -> Result { + let mut url = self.server.full.clone(); + url.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("eth") + .push("v1") + .push("keystores"); + Ok(url) + } + + /// `GET lighthouse/auth` + pub async fn get_auth(&self) -> Result { + let mut url = self.server.full.clone(); + url.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("lighthouse") + .push("auth"); + self.get_unsigned(url).await + } + + /// `GET eth/v1/keystores` + pub async fn get_keystores(&self) -> Result { + let url = self.make_keystores_url()?; + self.get_unsigned(url).await + } + + /// `POST eth/v1/keystores` + pub async fn post_keystores( + &self, + req: &ImportKeystoresRequest, + ) -> Result { + let url = self.make_keystores_url()?; + self.post_with_unsigned_response(url, req).await + } + + /// `DELETE eth/v1/keystores` + pub async fn delete_keystores( + &self, + req: &DeleteKeystoresRequest, + ) -> Result { + let url = self.make_keystores_url()?; + self.delete_with_unsigned_response(url, req).await + } } /// Returns `Ok(response)` if the response is a `200 OK` response. Otherwise, creates an diff --git a/common/eth2/src/lighthouse_vc/mod.rs b/common/eth2/src/lighthouse_vc/mod.rs index b7de7c7152..81b4fca283 100644 --- a/common/eth2/src/lighthouse_vc/mod.rs +++ b/common/eth2/src/lighthouse_vc/mod.rs @@ -1,4 +1,5 @@ pub mod http_client; +pub mod std_types; pub mod types; /// The number of bytes in the secp256k1 public key used as the authorization token for the VC API. diff --git a/common/eth2/src/lighthouse_vc/std_types.rs b/common/eth2/src/lighthouse_vc/std_types.rs new file mode 100644 index 0000000000..ebcce3fab0 --- /dev/null +++ b/common/eth2/src/lighthouse_vc/std_types.rs @@ -0,0 +1,104 @@ +use account_utils::ZeroizeString; +use eth2_keystore::Keystore; +use serde::{Deserialize, Serialize}; +use slashing_protection::interchange::Interchange; +use types::PublicKeyBytes; + +#[derive(Debug, Deserialize, Serialize, PartialEq)] +pub struct AuthResponse { + pub token_path: String, +} + +#[derive(Debug, Deserialize, Serialize, PartialEq)] +pub struct ListKeystoresResponse { + pub data: Vec, +} + +#[derive(Debug, Deserialize, Serialize, PartialEq)] +pub struct SingleKeystoreResponse { + pub validating_pubkey: PublicKeyBytes, + pub derivation_path: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub readonly: Option, +} + +#[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ImportKeystoresRequest { + pub keystores: Vec, + pub passwords: Vec, + pub slashing_protection: Option, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[serde(transparent)] +pub struct KeystoreJsonStr(#[serde(with = "eth2_serde_utils::json_str")] pub Keystore); + +impl std::ops::Deref for KeystoreJsonStr { + type Target = Keystore; + fn deref(&self) -> &Keystore { + &self.0 + } +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[serde(transparent)] +pub struct InterchangeJsonStr(#[serde(with = "eth2_serde_utils::json_str")] pub Interchange); + +#[derive(Debug, Deserialize, Serialize)] +pub struct ImportKeystoresResponse { + pub data: Vec>, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Status { + pub status: T, + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, +} + +impl Status { + pub fn ok(status: T) -> Self { + Self { + status, + message: None, + } + } + + pub fn error(status: T, message: String) -> Self { + Self { + status, + message: Some(message), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum ImportKeystoreStatus { + Imported, + Duplicate, + Error, +} + +#[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct DeleteKeystoresRequest { + pub pubkeys: Vec, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct DeleteKeystoresResponse { + pub data: Vec>, + #[serde(with = "eth2_serde_utils::json_str")] + pub slashing_protection: Interchange, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum DeleteKeystoreStatus { + Deleted, + NotActive, + NotFound, + Error, +} diff --git a/common/eth2/src/lighthouse_vc/types.rs b/common/eth2/src/lighthouse_vc/types.rs index 9e311c9d6b..25b3050538 100644 --- a/common/eth2/src/lighthouse_vc/types.rs +++ b/common/eth2/src/lighthouse_vc/types.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use std::path::PathBuf; pub use crate::lighthouse::Health; +pub use crate::lighthouse_vc::std_types::*; pub use crate::types::{GenericResponse, VersionData}; pub use types::*; diff --git a/common/validator_dir/src/builder.rs b/common/validator_dir/src/builder.rs index 4d6de05163..861a6afe96 100644 --- a/common/validator_dir/src/builder.rs +++ b/common/validator_dir/src/builder.rs @@ -134,15 +134,18 @@ impl<'a> Builder<'a> { self } + /// Return the path to the validator dir to be built, i.e. `base_dir/pubkey`. + pub fn get_dir_path(base_validators_dir: &Path, voting_keystore: &Keystore) -> PathBuf { + base_validators_dir.join(format!("0x{}", voting_keystore.pubkey())) + } + /// Consumes `self`, returning a `ValidatorDir` if no error is encountered. pub fn build(self) -> Result { let (voting_keystore, voting_password) = self .voting_keystore .ok_or(Error::UninitializedVotingKeystore)?; - let dir = self - .base_validators_dir - .join(format!("0x{}", voting_keystore.pubkey())); + let dir = Self::get_dir_path(&self.base_validators_dir, &voting_keystore); if dir.exists() { return Err(Error::DirectoryAlreadyExists(dir)); diff --git a/consensus/serde_utils/Cargo.toml b/consensus/serde_utils/Cargo.toml index 965a63c60d..54eb55b8fc 100644 --- a/consensus/serde_utils/Cargo.toml +++ b/consensus/serde_utils/Cargo.toml @@ -9,8 +9,6 @@ license = "Apache-2.0" [dependencies] serde = { version = "1.0.116", features = ["derive"] } serde_derive = "1.0.116" +serde_json = "1.0.58" hex = "0.4.2" ethereum-types = "0.12.1" - -[dev-dependencies] -serde_json = "1.0.58" diff --git a/consensus/serde_utils/src/json_str.rs b/consensus/serde_utils/src/json_str.rs new file mode 100644 index 0000000000..b9a1813915 --- /dev/null +++ b/consensus/serde_utils/src/json_str.rs @@ -0,0 +1,25 @@ +//! Serialize a datatype as a JSON-blob within a single string. +use serde::{ + de::{DeserializeOwned, Error as _}, + ser::Error as _, + Deserialize, Deserializer, Serialize, Serializer, +}; + +/// Serialize as a JSON object within a string. +pub fn serialize(value: &T, serializer: S) -> Result +where + S: Serializer, + T: Serialize, +{ + serializer.serialize_str(&serde_json::to_string(value).map_err(S::Error::custom)?) +} + +/// Deserialize a JSON object embedded in a string. +pub fn deserialize<'de, T, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, + T: DeserializeOwned, +{ + let json_str = String::deserialize(deserializer)?; + serde_json::from_str(&json_str).map_err(D::Error::custom) +} diff --git a/consensus/serde_utils/src/lib.rs b/consensus/serde_utils/src/lib.rs index 87179997e3..81e2bbe963 100644 --- a/consensus/serde_utils/src/lib.rs +++ b/consensus/serde_utils/src/lib.rs @@ -3,6 +3,7 @@ mod quoted_int; pub mod fixed_bytes_hex; pub mod hex; pub mod hex_vec; +pub mod json_str; pub mod list_of_bytes_lists; pub mod quoted_u64_vec; pub mod u32_hex; diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index 4e8aa57a5b..08f5cec07c 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -10,6 +10,7 @@ path = "src/lib.rs" [dev-dependencies] tokio = { version = "1.14.0", features = ["time", "rt-multi-thread", "macros"] } +logging = { path = "../common/logging" } [dependencies] tree_hash = "0.4.1" @@ -48,7 +49,7 @@ hyper = "0.14.4" eth2_serde_utils = "0.1.1" libsecp256k1 = "0.6.0" ring = "0.16.19" -rand = "0.7.3" +rand = { version = "0.7.3", features = ["small_rng"] } lighthouse_metrics = { path = "../common/lighthouse_metrics" } lazy_static = "1.4.0" itertools = "0.10.0" diff --git a/validator_client/slashing_protection/Cargo.toml b/validator_client/slashing_protection/Cargo.toml index 634e49feea..697bd602bf 100644 --- a/validator_client/slashing_protection/Cargo.toml +++ b/validator_client/slashing_protection/Cargo.toml @@ -3,6 +3,11 @@ name = "slashing_protection" version = "0.1.0" authors = ["Michael Sproul ", "pscott "] edition = "2018" +autotests = false + +[[test]] +name = "slashing_protection_tests" +path = "tests/main.rs" [dependencies] tempfile = "3.1.0" diff --git a/validator_client/slashing_protection/migration-tests/v0_no_enabled_column.sqlite b/validator_client/slashing_protection/migration-tests/v0_no_enabled_column.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..5a95fe36e6e4e3058d08851e7148e195984e25c9 GIT binary patch literal 28672 zcmeI)&u$w<7y$77>%@(m;g;)00ymK=G_y0iJA;Ho8f~knZbIBtq9-%6GZUs#g3vVG}P zRKIt3uQDBTr&oP&7?rGVY$fYATdmXK1G%?t2G1Qn>}hv*yR+Bp4U)^Pjmu};`pQPT zT)VoSTq|!a9q;>c(k`!*?Q(s!ypcRP(O>H766zim$ll$ zE$VySU2X35KiYANtd6`p=#BO}6QDa}!oivQtKrPT#f$!D~+Z9X|Uxs&lcJTX0&X;53Zy7;Wo z9rdR}j_Sj;{Os^krQu$2xyZ2N*2oSlp4+}UMOB@!82uO#e_A2%8F{DD{FDC ztt=?bX`U&~WQy}#VIw&a+8E;QK_#=Mph6Q>5gQ(iM^!XWF>%+)l+j!e!_rFY)VfZ) zzl;}z6{%3la>Iq-lxLPGBHb9dDX_$bx$ZHM7g|}G8zu9i5|wg2!#FQ&aL|sCx#9xL zf?HPMtjH^_mFqn(3fBRGQ;}(sD_TijSnDL6jV8uj#-*?}!;}!rDq9F=pE+`q=BW{G zEW9GrSpT3MOe^L3&uo=Z;;J&I{M223&UyVmdj5~z^3X5n zd-Nqzt_lSNKmY_l00ck)1V8`;KmY_l00cnbi9j=se7_aPD^p4{8pcJ@l(3a%IE)L! zsd2$DE(oT@z3xZ#dTUzL==neT)kFWGztNxQH&=xM0w4eaAOHd&00JNY0w4eaAOHd& z@bU=ExbF%4IF3i#D2hhgFbqfAAP6G&{Xl*E{9kzJXY?KV0!buZo&iEnK>!3m00ck) z1V8`;KmY_l00cnb+y$C(?9a}&j{hM3#5SAJC~o+JduNtrIEo9-400#m2;#bHcUoA} zAKfmV7B_zW|IkDKpuf-`=pp)d+7)mf1V8`;KmY_l00ck)1V8`;KmY_l-~|(?MgFWG X#iLCajW$6TNA+1haR0tATaW(-ZhfP; literal 0 HcmV?d00001 diff --git a/validator_client/slashing_protection/src/lib.rs b/validator_client/slashing_protection/src/lib.rs index 858acbfe9b..1610b52372 100644 --- a/validator_client/slashing_protection/src/lib.rs +++ b/validator_client/slashing_protection/src/lib.rs @@ -30,6 +30,7 @@ pub const SLASHING_PROTECTION_FILENAME: &str = "slashing_protection.sqlite"; #[derive(PartialEq, Debug)] pub enum NotSafe { UnregisteredValidator(PublicKeyBytes), + DisabledValidator(PublicKeyBytes), InvalidBlock(InvalidBlock), InvalidAttestation(InvalidAttestation), PermissionsError, diff --git a/validator_client/slashing_protection/src/registration_tests.rs b/validator_client/slashing_protection/src/registration_tests.rs index 40a3d6ee71..472f41577d 100644 --- a/validator_client/slashing_protection/src/registration_tests.rs +++ b/validator_client/slashing_protection/src/registration_tests.rs @@ -2,6 +2,7 @@ use crate::test_utils::*; use crate::*; +use std::iter; use tempfile::tempdir; #[test] @@ -30,3 +31,44 @@ fn double_register_validators() { assert_eq!(slashing_db.num_validator_rows().unwrap(), num_validators); assert_eq!(validator_ids, get_validator_ids()); } + +#[test] +fn reregister_validator() { + let dir = tempdir().unwrap(); + let slashing_db_file = dir.path().join("slashing_protection.sqlite"); + let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap(); + + let pk = pubkey(0); + + // Register validator. + slashing_db.register_validator(pk).unwrap(); + let id = slashing_db.get_validator_id(&pk).unwrap(); + + slashing_db + .with_transaction(|txn| { + // Disable. + slashing_db.update_validator_status(txn, id, false)?; + + // Fetching the validator as "registered" should now fail. + assert_eq!( + slashing_db.get_validator_id_in_txn(txn, &pk).unwrap_err(), + NotSafe::DisabledValidator(pk) + ); + + // Fetching its status should return false. + let (fetched_id, enabled) = + slashing_db.get_validator_id_with_status(txn, &pk)?.unwrap(); + assert_eq!(fetched_id, id); + assert!(!enabled); + + // Re-registering the validator should preserve its ID while changing its status to + // enabled. + slashing_db.register_validators_in_txn(iter::once(&pk), txn)?; + + let re_reg_id = slashing_db.get_validator_id_in_txn(txn, &pk)?; + assert_eq!(re_reg_id, id); + + Ok::<_, NotSafe>(()) + }) + .unwrap(); +} diff --git a/validator_client/slashing_protection/src/slashing_database.rs b/validator_client/slashing_protection/src/slashing_database.rs index 2b187f46ef..9f585c010a 100644 --- a/validator_client/slashing_protection/src/slashing_database.rs +++ b/validator_client/slashing_protection/src/slashing_database.rs @@ -28,6 +28,9 @@ pub const CONNECTION_TIMEOUT: Duration = Duration::from_millis(100); /// Supported version of the interchange format. pub const SUPPORTED_INTERCHANGE_FORMAT_VERSION: u64 = 5; +/// Column ID of the `validators.enabled` column. +pub const VALIDATORS_ENABLED_CID: i64 = 2; + #[derive(Debug, Clone)] pub struct SlashingDatabase { conn_pool: Pool, @@ -55,7 +58,7 @@ impl SlashingDatabase { restrict_file_permissions(path).map_err(|_| NotSafe::PermissionsError)?; let conn_pool = Self::open_conn_pool(path)?; - let conn = conn_pool.get()?; + let mut conn = conn_pool.get()?; conn.execute( "CREATE TABLE validators ( @@ -88,13 +91,55 @@ impl SlashingDatabase { params![], )?; + // The tables created above are for the v0 schema. We immediately update them + // to the latest schema without dropping the connection. + let txn = conn.transaction()?; + Self::apply_schema_migrations(&txn)?; + txn.commit()?; + Ok(Self { conn_pool }) } /// Open an existing `SlashingDatabase` from disk. + /// + /// This will automatically check for and apply the latest schema migrations. pub fn open(path: &Path) -> Result { let conn_pool = Self::open_conn_pool(path)?; - Ok(Self { conn_pool }) + let db = Self { conn_pool }; + db.with_transaction(Self::apply_schema_migrations)?; + Ok(db) + } + + fn apply_schema_migrations(txn: &Transaction) -> Result<(), NotSafe> { + // Add the `enabled` column to the `validators` table if it does not already exist. + let enabled_col_exists = txn + .query_row( + "SELECT cid, name FROM pragma_table_info('validators') WHERE name = 'enabled'", + params![], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .optional()? + .map(|(cid, name): (i64, String)| { + // Check that the enabled column is in the correct position with the right name. + // This is a defensive check that shouldn't do anything in practice unless the + // slashing DB has been manually edited. + if cid == VALIDATORS_ENABLED_CID && name == "enabled" { + Ok(()) + } else { + Err(NotSafe::ConsistencyError) + } + }) + .transpose()? + .is_some(); + + if !enabled_col_exists { + txn.execute( + "ALTER TABLE validators ADD COLUMN enabled BOOL NOT NULL DEFAULT TRUE", + params![], + )?; + } + + Ok(()) } /// Open a new connection pool with all of the necessary settings and tweaks. @@ -166,15 +211,37 @@ impl SlashingDatabase { public_keys: impl Iterator, txn: &Transaction, ) -> Result<(), NotSafe> { - let mut stmt = txn.prepare("INSERT INTO validators (public_key) VALUES (?1)")?; + let mut stmt = + txn.prepare("INSERT INTO validators (public_key, enabled) VALUES (?1, TRUE)")?; for pubkey in public_keys { - if self.get_validator_id_opt(txn, pubkey)?.is_none() { - stmt.execute([pubkey.as_hex_string()])?; + match self.get_validator_id_with_status(txn, pubkey)? { + None => { + stmt.execute([pubkey.as_hex_string()])?; + } + Some((validator_id, false)) => { + self.update_validator_status(txn, validator_id, true)?; + } + Some((_, true)) => { + // Validator already registered and enabled. + } } } Ok(()) } + pub fn update_validator_status( + &self, + txn: &Transaction, + validator_id: i64, + status: bool, + ) -> Result<(), NotSafe> { + txn.execute( + "UPDATE validators SET enabled = ? WHERE id = ?", + params![status, validator_id], + )?; + Ok(()) + } + /// Check that all of the given validators are registered. pub fn check_validator_registrations<'a>( &self, @@ -203,7 +270,7 @@ impl SlashingDatabase { .collect() } - /// Get the database-internal ID for a validator. + /// Get the database-internal ID for an enabled validator. /// /// This is NOT the same as a validator index, and depends on the ordering that validators /// are registered with the slashing protection database (and may vary between machines). @@ -213,26 +280,43 @@ impl SlashingDatabase { self.get_validator_id_in_txn(&txn, public_key) } - fn get_validator_id_in_txn( + pub fn get_validator_id_in_txn( &self, txn: &Transaction, public_key: &PublicKeyBytes, ) -> Result { - self.get_validator_id_opt(txn, public_key)? - .ok_or_else(|| NotSafe::UnregisteredValidator(*public_key)) + let (validator_id, enabled) = self + .get_validator_id_with_status(txn, public_key)? + .ok_or_else(|| NotSafe::UnregisteredValidator(*public_key))?; + if enabled { + Ok(validator_id) + } else { + Err(NotSafe::DisabledValidator(*public_key)) + } } - /// Optional version of `get_validator_id`. - fn get_validator_id_opt( + /// Get validator ID regardless of whether or not it is enabled. + pub fn get_validator_id_ignoring_status( &self, txn: &Transaction, public_key: &PublicKeyBytes, - ) -> Result, NotSafe> { + ) -> Result { + let (validator_id, _) = self + .get_validator_id_with_status(txn, public_key)? + .ok_or_else(|| NotSafe::UnregisteredValidator(*public_key))?; + Ok(validator_id) + } + + pub fn get_validator_id_with_status( + &self, + txn: &Transaction, + public_key: &PublicKeyBytes, + ) -> Result, NotSafe> { Ok(txn .query_row( - "SELECT id FROM validators WHERE public_key = ?1", + "SELECT id, enabled FROM validators WHERE public_key = ?1", params![&public_key.as_hex_string()], - |row| row.get(0), + |row| Ok((row.get(0)?, row.get(1)?)), ) .optional()?) } @@ -722,13 +806,21 @@ impl SlashingDatabase { ) -> Result { let mut conn = self.conn_pool.get()?; let txn = &conn.transaction()?; + self.export_interchange_info_in_txn(genesis_validators_root, selected_pubkeys, txn) + } + pub fn export_interchange_info_in_txn( + &self, + genesis_validators_root: Hash256, + selected_pubkeys: Option<&[PublicKeyBytes]>, + txn: &Transaction, + ) -> Result { // Determine the validator IDs and public keys to export data for. let to_export = if let Some(selected_pubkeys) = selected_pubkeys { selected_pubkeys .iter() .map(|pubkey| { - let id = self.get_validator_id_in_txn(txn, pubkey)?; + let id = self.get_validator_id_ignoring_status(txn, pubkey)?; Ok((id, *pubkey)) }) .collect::>()? @@ -1089,7 +1181,6 @@ impl From for InterchangeError { #[cfg(test)] mod tests { use super::*; - use crate::test_utils::pubkey; use tempfile::tempdir; #[test] @@ -1106,8 +1197,7 @@ mod tests { let file = dir.path().join("db.sqlite"); let _db1 = SlashingDatabase::create(&file).unwrap(); - let db2 = SlashingDatabase::open(&file).unwrap(); - db2.register_validator(pubkey(0)).unwrap_err(); + SlashingDatabase::open(&file).unwrap_err(); } // Attempting to create the same database twice should error. @@ -1152,9 +1242,12 @@ mod tests { fn test_transaction_failure() { let dir = tempdir().unwrap(); let file = dir.path().join("db.sqlite"); - let _db1 = SlashingDatabase::create(&file).unwrap(); + let db = SlashingDatabase::create(&file).unwrap(); - let db2 = SlashingDatabase::open(&file).unwrap(); - db2.test_transaction().unwrap_err(); + db.with_transaction(|_| { + db.test_transaction().unwrap_err(); + Ok::<(), NotSafe>(()) + }) + .unwrap(); } } diff --git a/validator_client/slashing_protection/tests/main.rs b/validator_client/slashing_protection/tests/main.rs new file mode 100644 index 0000000000..5b66bd87e6 --- /dev/null +++ b/validator_client/slashing_protection/tests/main.rs @@ -0,0 +1,2 @@ +mod interop; +mod migration; diff --git a/validator_client/slashing_protection/tests/migration.rs b/validator_client/slashing_protection/tests/migration.rs new file mode 100644 index 0000000000..cd3561f211 --- /dev/null +++ b/validator_client/slashing_protection/tests/migration.rs @@ -0,0 +1,68 @@ +//! Tests for upgrading a previous version of the database to the latest schema. +use slashing_protection::{NotSafe, SlashingDatabase}; +use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; +use tempfile::tempdir; +use types::Hash256; + +fn test_data_dir() -> PathBuf { + Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("migration-tests") +} + +/// Copy `filename` from the test data dir to the temporary `dest` for testing. +fn make_copy(filename: &str, dest: &Path) -> PathBuf { + let source_file = test_data_dir().join(filename); + let dest_file = dest.join(filename); + fs::copy(source_file, &dest_file).unwrap(); + dest_file +} + +#[test] +fn add_enabled_column() { + let tmp = tempdir().unwrap(); + + let path = make_copy("v0_no_enabled_column.sqlite", tmp.path()); + let num_expected_validators = 5; + + // Database should open without errors, indicating successfull application of migrations. + // The input file has no `enabled` column, which should get added when opening it here. + let db = SlashingDatabase::open(&path).unwrap(); + + // Check that exporting an interchange file lists all the validators. + let interchange = db.export_all_interchange_info(Hash256::zero()).unwrap(); + assert_eq!(interchange.data.len(), num_expected_validators); + + db.with_transaction(|txn| { + // Check that all the validators are enabled and unique. + let uniq_validator_ids = interchange + .data + .iter() + .map(|data| { + let (validator_id, enabled) = db + .get_validator_id_with_status(txn, &data.pubkey) + .unwrap() + .unwrap(); + assert!(enabled); + (validator_id, data.pubkey) + }) + .collect::>(); + + assert_eq!(uniq_validator_ids.len(), num_expected_validators); + + // Check that we can disable them all. + for (&validator_id, pubkey) in &uniq_validator_ids { + db.update_validator_status(txn, validator_id, false) + .unwrap(); + let (loaded_id, enabled) = db + .get_validator_id_with_status(txn, pubkey) + .unwrap() + .unwrap(); + assert_eq!(validator_id, loaded_id); + assert!(!enabled); + } + + Ok::<_, NotSafe>(()) + }) + .unwrap(); +} diff --git a/validator_client/src/http_api/api_secret.rs b/validator_client/src/http_api/api_secret.rs index 531180cbad..484ac50bd3 100644 --- a/validator_client/src/http_api/api_secret.rs +++ b/validator_client/src/http_api/api_secret.rs @@ -162,25 +162,32 @@ impl ApiSecret { } /// Returns the path for the API token file - pub fn api_token_path(&self) -> &PathBuf { - &self.pk_path + pub fn api_token_path(&self) -> PathBuf { + self.pk_path.clone() } - /// Returns the value of the `Authorization` header which is used for verifying incoming HTTP - /// requests. - fn auth_header_value(&self) -> String { - format!("Basic {}", self.api_token()) + /// Returns the values of the `Authorization` header which indicate a valid incoming HTTP + /// request. + /// + /// For backwards-compatibility we accept the token in a basic authentication style, but this is + /// technically invalid according to RFC 7617 because the token is not a base64-encoded username + /// and password. As such, bearer authentication should be preferred. + fn auth_header_values(&self) -> Vec { + vec![ + format!("Basic {}", self.api_token()), + format!("Bearer {}", self.api_token()), + ] } /// Returns a `warp` header which filters out request that have a missing or inaccurate /// `Authorization` header. pub fn authorization_header_filter(&self) -> warp::filters::BoxedFilter<()> { - let expected = self.auth_header_value(); + let expected = self.auth_header_values(); warp::any() .map(move || expected.clone()) .and(warp::filters::header::header("Authorization")) - .and_then(move |expected: String, header: String| async move { - if header == expected { + .and_then(move |expected: Vec, header: String| async move { + if expected.contains(&header) { Ok(()) } else { Err(warp_utils::reject::invalid_auth(header)) diff --git a/validator_client/src/http_api/keystores.rs b/validator_client/src/http_api/keystores.rs new file mode 100644 index 0000000000..ce4035581c --- /dev/null +++ b/validator_client/src/http_api/keystores.rs @@ -0,0 +1,290 @@ +//! Implementation of the standard keystore management API. +use crate::{signing_method::SigningMethod, InitializedValidators, ValidatorStore}; +use account_utils::ZeroizeString; +use eth2::lighthouse_vc::std_types::{ + DeleteKeystoreStatus, DeleteKeystoresRequest, DeleteKeystoresResponse, ImportKeystoreStatus, + ImportKeystoresRequest, ImportKeystoresResponse, InterchangeJsonStr, KeystoreJsonStr, + ListKeystoresResponse, SingleKeystoreResponse, Status, +}; +use eth2_keystore::Keystore; +use slog::{info, warn, Logger}; +use slot_clock::SlotClock; +use std::path::PathBuf; +use std::sync::Arc; +use std::sync::Weak; +use tokio::runtime::Runtime; +use types::{EthSpec, PublicKeyBytes}; +use validator_dir::Builder as ValidatorDirBuilder; +use warp::Rejection; +use warp_utils::reject::{custom_bad_request, custom_server_error}; + +pub fn list( + validator_store: Arc>, +) -> ListKeystoresResponse { + let initialized_validators_rwlock = validator_store.initialized_validators(); + let initialized_validators = initialized_validators_rwlock.read(); + + let keystores = initialized_validators + .validator_definitions() + .iter() + .filter(|def| def.enabled) + .map(|def| { + let validating_pubkey = def.voting_public_key.compress(); + + let (derivation_path, readonly) = initialized_validators + .signing_method(&validating_pubkey) + .map_or((None, None), |signing_method| match *signing_method { + SigningMethod::LocalKeystore { + ref voting_keystore, + .. + } => (voting_keystore.path(), None), + SigningMethod::Web3Signer { .. } => (None, Some(true)), + }); + + SingleKeystoreResponse { + validating_pubkey, + derivation_path, + readonly, + } + }) + .collect::>(); + + ListKeystoresResponse { data: keystores } +} + +pub fn import( + request: ImportKeystoresRequest, + validator_dir: PathBuf, + validator_store: Arc>, + runtime: Weak, + log: Logger, +) -> Result { + // Check request validity. This is the only cases in which we should return a 4xx code. + if request.keystores.len() != request.passwords.len() { + return Err(custom_bad_request(format!( + "mismatched numbers of keystores ({}) and passwords ({})", + request.keystores.len(), + request.passwords.len(), + ))); + } + + info!( + log, + "Importing keystores via standard HTTP API"; + "count" => request.keystores.len(), + ); + + // Import slashing protection data before keystores, so that new keystores don't start signing + // without it. Do not return early on failure, propagate the failure to each key. + let slashing_protection_status = + if let Some(InterchangeJsonStr(slashing_protection)) = request.slashing_protection { + // Warn for missing slashing protection. + for KeystoreJsonStr(ref keystore) in &request.keystores { + if let Some(public_key) = keystore.public_key() { + let pubkey_bytes = public_key.compress(); + if !slashing_protection + .data + .iter() + .any(|data| data.pubkey == pubkey_bytes) + { + warn!( + log, + "Slashing protection data not provided"; + "public_key" => ?public_key, + ); + } + } + } + + validator_store.import_slashing_protection(slashing_protection) + } else { + warn!(log, "No slashing protection data provided with keystores"); + Ok(()) + }; + + // Import each keystore. Some keystores may fail to be imported, so we record a status for each. + let mut statuses = Vec::with_capacity(request.keystores.len()); + + for (KeystoreJsonStr(keystore), password) in request + .keystores + .into_iter() + .zip(request.passwords.into_iter()) + { + let pubkey_str = keystore.pubkey().to_string(); + + let status = if let Err(e) = &slashing_protection_status { + // Slashing protection import failed, do not attempt to import the key. Record an + // error status. + Status::error( + ImportKeystoreStatus::Error, + format!("slashing protection import failed: {:?}", e), + ) + } else if let Some(runtime) = runtime.upgrade() { + // Import the keystore. + match import_single_keystore( + keystore, + password, + validator_dir.clone(), + &validator_store, + runtime, + ) { + Ok(status) => Status::ok(status), + Err(e) => { + warn!( + log, + "Error importing keystore, skipped"; + "pubkey" => pubkey_str, + "error" => ?e, + ); + Status::error(ImportKeystoreStatus::Error, e) + } + } + } else { + Status::error( + ImportKeystoreStatus::Error, + "validator client shutdown".into(), + ) + }; + statuses.push(status); + } + + Ok(ImportKeystoresResponse { data: statuses }) +} + +fn import_single_keystore( + keystore: Keystore, + password: ZeroizeString, + validator_dir_path: PathBuf, + validator_store: &ValidatorStore, + runtime: Arc, +) -> Result { + // Check if the validator key already exists, erroring if it is a remote signer validator. + let pubkey = keystore + .public_key() + .ok_or_else(|| format!("invalid pubkey: {}", keystore.pubkey()))?; + if let Some(def) = validator_store + .initialized_validators() + .read() + .validator_definitions() + .iter() + .find(|def| def.voting_public_key == pubkey) + { + if !def.signing_definition.is_local_keystore() { + return Err("cannot import duplicate of existing remote signer validator".into()); + } else if def.enabled { + return Ok(ImportKeystoreStatus::Duplicate); + } + } + + // Check that the password is correct. + // In future we should re-structure to avoid the double decryption here. It's not as simple + // as removing this check because `add_validator_keystore` will break if provided with an + // invalid validator definition (`update_validators` will get stuck trying to decrypt with the + // wrong password indefinitely). + keystore + .decrypt_keypair(password.as_ref()) + .map_err(|e| format!("incorrect password: {:?}", e))?; + + let validator_dir = ValidatorDirBuilder::new(validator_dir_path) + .voting_keystore(keystore, password.as_ref()) + .store_withdrawal_keystore(false) + .build() + .map_err(|e| format!("failed to build validator directory: {:?}", e))?; + + // Drop validator dir so that `add_validator_keystore` can re-lock the keystore. + let voting_keystore_path = validator_dir.voting_keystore_path(); + drop(validator_dir); + + runtime + .block_on(validator_store.add_validator_keystore( + voting_keystore_path, + password, + true, + None, + )) + .map_err(|e| format!("failed to initialize validator: {:?}", e))?; + + Ok(ImportKeystoreStatus::Imported) +} + +pub fn delete( + request: DeleteKeystoresRequest, + validator_store: Arc>, + runtime: Weak, + log: Logger, +) -> Result { + // Remove from initialized validators. + let initialized_validators_rwlock = validator_store.initialized_validators(); + let mut initialized_validators = initialized_validators_rwlock.write(); + + let mut statuses = request + .pubkeys + .iter() + .map(|pubkey_bytes| { + match delete_single_keystore(pubkey_bytes, &mut initialized_validators, runtime.clone()) + { + Ok(status) => Status::ok(status), + Err(error) => { + warn!( + log, + "Error deleting keystore"; + "pubkey" => ?pubkey_bytes, + "error" => ?error, + ); + Status::error(DeleteKeystoreStatus::Error, error) + } + } + }) + .collect::>(); + + // Use `update_validators` to update the key cache. It is safe to let the key cache get a bit out + // of date as it resets when it can't be decrypted. We update it just a single time to avoid + // continually resetting it after each key deletion. + if let Some(runtime) = runtime.upgrade() { + runtime + .block_on(initialized_validators.update_validators()) + .map_err(|e| custom_server_error(format!("unable to update key cache: {:?}", e)))?; + } + + // Export the slashing protection data. + let slashing_protection = validator_store + .export_slashing_protection_for_keys(&request.pubkeys) + .map_err(|e| { + custom_server_error(format!("error exporting slashing protection: {:?}", e)) + })?; + + // Update stasuses based on availability of slashing protection data. + for (pubkey, status) in request.pubkeys.iter().zip(statuses.iter_mut()) { + if status.status == DeleteKeystoreStatus::NotFound + && slashing_protection + .data + .iter() + .any(|interchange_data| interchange_data.pubkey == *pubkey) + { + status.status = DeleteKeystoreStatus::NotActive; + } + } + + Ok(DeleteKeystoresResponse { + data: statuses, + slashing_protection, + }) +} + +fn delete_single_keystore( + pubkey_bytes: &PublicKeyBytes, + initialized_validators: &mut InitializedValidators, + runtime: Weak, +) -> Result { + if let Some(runtime) = runtime.upgrade() { + let pubkey = pubkey_bytes + .decompress() + .map_err(|e| format!("invalid pubkey, {:?}: {:?}", pubkey_bytes, e))?; + + runtime + .block_on(initialized_validators.delete_definition_and_keystore(&pubkey)) + .map_err(|e| format!("unable to disable and delete: {:?}", e)) + } else { + Err("validator client shutdown".into()) + } +} diff --git a/validator_client/src/http_api/mod.rs b/validator_client/src/http_api/mod.rs index 5e0f3443a2..8a5b24f87b 100644 --- a/validator_client/src/http_api/mod.rs +++ b/validator_client/src/http_api/mod.rs @@ -1,14 +1,18 @@ mod api_secret; mod create_validator; +mod keystores; mod tests; use crate::ValidatorStore; use account_utils::mnemonic_from_phrase; use create_validator::{create_validators_mnemonic, create_validators_web3signer}; -use eth2::lighthouse_vc::types::{self as api_types, PublicKey, PublicKeyBytes}; +use eth2::lighthouse_vc::{ + std_types::AuthResponse, + types::{self as api_types, PublicKey, PublicKeyBytes}, +}; use lighthouse_version::version_with_platform; use serde::{Deserialize, Serialize}; -use slog::{crit, info, Logger}; +use slog::{crit, info, warn, Logger}; use slot_clock::SlotClock; use std::future::Future; use std::marker::PhantomData; @@ -106,7 +110,7 @@ pub fn serve( // Configure CORS. let cors_builder = { let builder = warp::cors() - .allow_methods(vec!["GET", "POST", "PATCH"]) + .allow_methods(vec!["GET", "POST", "PATCH", "DELETE"]) .allow_headers(vec!["Content-Type", "Authorization"]); warp_utils::cors::set_builder_origins( @@ -125,7 +129,20 @@ pub fn serve( } let authorization_header_filter = ctx.api_secret.authorization_header_filter(); - let api_token_path = ctx.api_secret.api_token_path(); + let mut api_token_path = ctx.api_secret.api_token_path(); + + // Attempt to convert the path to an absolute path, but don't error if it fails. + match api_token_path.canonicalize() { + Ok(abs_path) => api_token_path = abs_path, + Err(e) => { + warn!( + log, + "Error canonicalizing token path"; + "error" => ?e, + ); + } + }; + let signer = ctx.api_secret.signer(); let signer = warp::any().map(move || signer.clone()); @@ -154,9 +171,15 @@ pub fn serve( }) }); + let inner_ctx = ctx.clone(); + let log_filter = warp::any().map(move || inner_ctx.log.clone()); + let inner_spec = Arc::new(ctx.spec.clone()); let spec_filter = warp::any().map(move || inner_spec.clone()); + let api_token_path_inner = api_token_path.clone(); + let api_token_path_filter = warp::any().map(move || api_token_path_inner.clone()); + // GET lighthouse/version let get_node_version = warp::path("lighthouse") .and(warp::path("version")) @@ -348,7 +371,7 @@ pub fn serve( .and(warp::path("keystore")) .and(warp::path::end()) .and(warp::body::json()) - .and(validator_dir_filter) + .and(validator_dir_filter.clone()) .and(validator_store_filter.clone()) .and(signer.clone()) .and(runtime_filter.clone()) @@ -451,9 +474,9 @@ pub fn serve( .and(warp::path::param::()) .and(warp::path::end()) .and(warp::body::json()) - .and(validator_store_filter) - .and(signer) - .and(runtime_filter) + .and(validator_store_filter.clone()) + .and(signer.clone()) + .and(runtime_filter.clone()) .and_then( |validator_pubkey: PublicKey, body: api_types::ValidatorPatchRequest, @@ -495,6 +518,60 @@ pub fn serve( }, ); + // GET /lighthouse/auth + let get_auth = warp::path("lighthouse").and(warp::path("auth").and(warp::path::end())); + let get_auth = get_auth + .and(signer.clone()) + .and(api_token_path_filter) + .and_then(|signer, token_path: PathBuf| { + blocking_signed_json_task(signer, move || { + Ok(AuthResponse { + token_path: token_path.display().to_string(), + }) + }) + }); + + // Standard key-manager endpoints. + let eth_v1 = warp::path("eth").and(warp::path("v1")); + let std_keystores = eth_v1.and(warp::path("keystores")).and(warp::path::end()); + + // GET /eth/v1/keystores + let get_std_keystores = std_keystores + .and(signer.clone()) + .and(validator_store_filter.clone()) + .and_then(|signer, validator_store: Arc>| { + blocking_signed_json_task(signer, move || Ok(keystores::list(validator_store))) + }); + + // POST /eth/v1/keystores + let post_std_keystores = std_keystores + .and(warp::body::json()) + .and(signer.clone()) + .and(validator_dir_filter) + .and(validator_store_filter.clone()) + .and(runtime_filter.clone()) + .and(log_filter.clone()) + .and_then( + |request, signer, validator_dir, validator_store, runtime, log| { + blocking_signed_json_task(signer, move || { + keystores::import(request, validator_dir, validator_store, runtime, log) + }) + }, + ); + + // DELETE /eth/v1/keystores + let delete_std_keystores = std_keystores + .and(warp::body::json()) + .and(signer) + .and(validator_store_filter) + .and(runtime_filter) + .and(log_filter) + .and_then(|request, signer, validator_store, runtime, log| { + blocking_signed_json_task(signer, move || { + keystores::delete(request, validator_store, runtime, log) + }) + }); + let routes = warp::any() .and(authorization_header_filter) // Note: it is critical that the `authorization_header_filter` is applied to all routes. @@ -508,16 +585,21 @@ pub fn serve( .or(get_lighthouse_health) .or(get_lighthouse_spec) .or(get_lighthouse_validators) - .or(get_lighthouse_validators_pubkey), + .or(get_lighthouse_validators_pubkey) + .or(get_std_keystores), ) .or(warp::post().and( post_validators .or(post_validators_keystore) .or(post_validators_mnemonic) - .or(post_validators_web3signer), + .or(post_validators_web3signer) + .or(post_std_keystores), )) - .or(warp::patch().and(patch_validators)), + .or(warp::patch().and(patch_validators)) + .or(warp::delete().and(delete_std_keystores)), ) + // The auth route is the only route that is allowed to be accessed without the API token. + .or(warp::get().and(get_auth)) // Maps errors into HTTP responses. .recover(warp_utils::reject::handle_rejection) // Add a `Server` header. @@ -550,7 +632,7 @@ pub async fn blocking_signed_json_task( ) -> Result where S: Fn(&[u8]) -> String, - F: Fn() -> Result + Send + 'static, + F: FnOnce() -> Result + Send + 'static, T: Serialize + Send + 'static, { warp_utils::task::blocking_task(func) diff --git a/validator_client/src/http_api/tests.rs b/validator_client/src/http_api/tests.rs index c9ef869be5..fda622901b 100644 --- a/validator_client/src/http_api/tests.rs +++ b/validator_client/src/http_api/tests.rs @@ -1,6 +1,8 @@ #![cfg(test)] #![cfg(not(debug_assertions))] +mod keystores; + use crate::doppelganger_service::DoppelgangerService; use crate::{ http_api::{ApiSecret, Config as HttpConfig, Context}, @@ -9,16 +11,16 @@ use crate::{ }; use account_utils::{ eth2_wallet::WalletBuilder, mnemonic_from_phrase, random_mnemonic, random_password, - ZeroizeString, + random_password_string, ZeroizeString, }; use deposit_contract::decode_eth1_tx_data; -use environment::null_logger; use eth2::{ lighthouse_vc::{http_client::ValidatorClientHttpClient, types::*}, types::ErrorMessage as ApiErrorMessage, Error as ApiError, }; use eth2_keystore::KeystoreBuilder; +use logging::test_logger; use parking_lot::RwLock; use sensitive_url::SensitiveUrl; use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; @@ -40,6 +42,7 @@ type E = MainnetEthSpec; struct ApiTester { client: ValidatorClientHttpClient, initialized_validators: Arc>, + validator_store: Arc>, url: SensitiveUrl, _server_shutdown: oneshot::Sender<()>, _validator_dir: TempDir, @@ -58,7 +61,7 @@ fn build_runtime() -> Arc { impl ApiTester { pub async fn new(runtime: std::sync::Weak) -> Self { - let log = null_logger().unwrap(); + let log = test_logger(); let validator_dir = tempdir().unwrap(); let secrets_dir = tempdir().unwrap(); @@ -92,7 +95,7 @@ impl ApiTester { let (shutdown_tx, _) = futures::channel::mpsc::channel(1); let executor = TaskExecutor::new(runtime.clone(), exit, log.clone(), shutdown_tx); - let validator_store = ValidatorStore::<_, E>::new( + let validator_store = Arc::new(ValidatorStore::<_, E>::new( initialized_validators, slashing_protection, Hash256::repeat_byte(42), @@ -101,7 +104,7 @@ impl ApiTester { slot_clock, executor, log.clone(), - ); + )); validator_store .register_all_in_doppelganger_protection_if_enabled() @@ -113,7 +116,7 @@ impl ApiTester { runtime, api_secret, validator_dir: Some(validator_dir.path().into()), - validator_store: Some(Arc::new(validator_store)), + validator_store: Some(validator_store.clone()), spec: E::default_spec(), config: HttpConfig { enabled: true, @@ -144,11 +147,12 @@ impl ApiTester { let client = ValidatorClientHttpClient::new(url.clone(), api_pubkey).unwrap(); Self { - initialized_validators, - _validator_dir: validator_dir, client, + initialized_validators, + validator_store, url, _server_shutdown: shutdown_tx, + _validator_dir: validator_dir, _runtime_shutdown: runtime_shutdown, } } @@ -456,7 +460,7 @@ impl ApiTester { self.client .post_lighthouse_validators_web3signer(&request) .await - .unwrap_err(); + .unwrap(); assert_eq!(self.vals_total(), initial_vals + s.count); if s.enabled { @@ -608,6 +612,34 @@ fn routes_with_invalid_auth() { .await }) .await + .test_with_invalid_auth(|client| async move { client.get_keystores().await }) + .await + .test_with_invalid_auth(|client| async move { + let password = random_password_string(); + let keypair = Keypair::random(); + let keystore = KeystoreBuilder::new(&keypair, password.as_ref(), String::new()) + .unwrap() + .build() + .map(KeystoreJsonStr) + .unwrap(); + client + .post_keystores(&ImportKeystoresRequest { + keystores: vec![keystore], + passwords: vec![password], + slashing_protection: None, + }) + .await + }) + .await + .test_with_invalid_auth(|client| async move { + let keypair = Keypair::random(); + client + .delete_keystores(&DeleteKeystoresRequest { + pubkeys: vec![keypair.pk.compress()], + }) + .await + }) + .await }); } diff --git a/validator_client/src/http_api/tests/keystores.rs b/validator_client/src/http_api/tests/keystores.rs new file mode 100644 index 0000000000..1b35a0b57b --- /dev/null +++ b/validator_client/src/http_api/tests/keystores.rs @@ -0,0 +1,977 @@ +use super::*; +use account_utils::random_password_string; +use eth2::lighthouse_vc::{ + http_client::ValidatorClientHttpClient as HttpClient, + std_types::{KeystoreJsonStr as Keystore, *}, + types::Web3SignerValidatorRequest, +}; +// use eth2_keystore::Keystore; +use itertools::Itertools; +use rand::{rngs::SmallRng, Rng, SeedableRng}; +use slashing_protection::interchange::{Interchange, InterchangeMetadata}; +use std::collections::HashMap; +use std::path::Path; + +fn new_keystore(password: ZeroizeString) -> Keystore { + let keypair = Keypair::random(); + Keystore( + KeystoreBuilder::new(&keypair, password.as_ref(), String::new()) + .unwrap() + .build() + .unwrap(), + ) +} + +fn web3_signer_url() -> String { + "http://localhost:1/this-url-hopefully-doesnt-exist".into() +} + +fn new_web3signer_validator() -> (Keypair, Web3SignerValidatorRequest) { + let keypair = Keypair::random(); + let pk = keypair.pk.clone(); + (keypair, web3signer_validator_with_pubkey(pk)) +} + +fn web3signer_validator_with_pubkey(pubkey: PublicKey) -> Web3SignerValidatorRequest { + Web3SignerValidatorRequest { + enable: true, + description: "".into(), + graffiti: None, + voting_public_key: pubkey, + url: web3_signer_url(), + root_certificate_path: None, + request_timeout_ms: None, + } +} + +fn run_test(f: F) +where + F: FnOnce(ApiTester) -> V, + V: Future, +{ + let runtime = build_runtime(); + let weak_runtime = Arc::downgrade(&runtime); + runtime.block_on(async { + let tester = ApiTester::new(weak_runtime).await; + f(tester).await + }); +} + +fn run_dual_vc_test(f: F) +where + F: FnOnce(ApiTester, ApiTester) -> V, + V: Future, +{ + let runtime = build_runtime(); + let weak_runtime = Arc::downgrade(&runtime); + runtime.block_on(async { + let tester1 = ApiTester::new(weak_runtime.clone()).await; + let tester2 = ApiTester::new(weak_runtime).await; + f(tester1, tester2).await + }); +} + +fn keystore_pubkey(keystore: &Keystore) -> PublicKeyBytes { + keystore.0.public_key().unwrap().compress() +} + +fn all_with_status(count: usize, status: T) -> impl Iterator { + std::iter::repeat(status).take(count) +} + +fn all_imported(count: usize) -> impl Iterator { + all_with_status(count, ImportKeystoreStatus::Imported) +} + +fn all_duplicate(count: usize) -> impl Iterator { + all_with_status(count, ImportKeystoreStatus::Duplicate) +} + +fn all_import_error(count: usize) -> impl Iterator { + all_with_status(count, ImportKeystoreStatus::Error) +} + +fn all_deleted(count: usize) -> impl Iterator { + all_with_status(count, DeleteKeystoreStatus::Deleted) +} + +fn all_not_active(count: usize) -> impl Iterator { + all_with_status(count, DeleteKeystoreStatus::NotActive) +} + +fn all_not_found(count: usize) -> impl Iterator { + all_with_status(count, DeleteKeystoreStatus::NotFound) +} + +fn all_delete_error(count: usize) -> impl Iterator { + all_with_status(count, DeleteKeystoreStatus::Error) +} + +fn check_get_response<'a>( + response: &ListKeystoresResponse, + expected_keystores: impl IntoIterator, +) { + for (ks1, ks2) in response.data.iter().zip_eq(expected_keystores) { + assert_eq!(ks1.validating_pubkey, keystore_pubkey(ks2)); + assert_eq!(ks1.derivation_path, ks2.path()); + assert!(ks1.readonly == None || ks1.readonly == Some(false)); + } +} + +fn check_import_response( + response: &ImportKeystoresResponse, + expected_statuses: impl IntoIterator, +) { + for (status, expected_status) in response.data.iter().zip_eq(expected_statuses) { + assert_eq!( + expected_status, status.status, + "message: {:?}", + status.message + ); + } +} + +fn check_delete_response<'a>( + response: &DeleteKeystoresResponse, + expected_statuses: impl IntoIterator, +) { + for (status, expected_status) in response.data.iter().zip_eq(expected_statuses) { + assert_eq!( + status.status, expected_status, + "message: {:?}", + status.message + ); + } +} + +#[test] +fn get_auth_no_token() { + run_test(|mut tester| async move { + tester.client.send_authorization_header(false); + let auth_response = tester.client.get_auth().await.unwrap(); + + // Load the file from the returned path. + let token_path = Path::new(&auth_response.token_path); + let token = HttpClient::load_api_token_from_file(token_path).unwrap(); + + // The token should match the one that the client was originally initialised with. + assert!(tester.client.api_token() == Some(&token)); + }) +} + +#[test] +fn get_empty_keystores() { + run_test(|tester| async move { + let res = tester.client.get_keystores().await.unwrap(); + assert_eq!(res, ListKeystoresResponse { data: vec![] }); + }) +} + +#[test] +fn import_new_keystores() { + run_test(|tester| async move { + let password = random_password_string(); + let keystores = (0..3) + .map(|_| new_keystore(password.clone())) + .collect::>(); + + let import_res = tester + .client + .post_keystores(&ImportKeystoresRequest { + keystores: keystores.clone(), + passwords: vec![password.clone(); keystores.len()], + slashing_protection: None, + }) + .await + .unwrap(); + + // All keystores should be imported. + check_import_response(&import_res, all_imported(keystores.len())); + + // Check that GET lists all the imported keystores. + let get_res = tester.client.get_keystores().await.unwrap(); + check_get_response(&get_res, &keystores); + }) +} + +#[test] +fn import_only_duplicate_keystores() { + run_test(|tester| async move { + let password = random_password_string(); + let keystores = (0..3) + .map(|_| new_keystore(password.clone())) + .collect::>(); + + let req = ImportKeystoresRequest { + keystores: keystores.clone(), + passwords: vec![password.clone(); keystores.len()], + slashing_protection: None, + }; + + // All keystores should be imported on first import. + let import_res = tester.client.post_keystores(&req).await.unwrap(); + check_import_response(&import_res, all_imported(keystores.len())); + + // No keystores should be imported on repeat import. + let import_res = tester.client.post_keystores(&req).await.unwrap(); + check_import_response(&import_res, all_duplicate(keystores.len())); + + // Check that GET lists all the imported keystores. + let get_res = tester.client.get_keystores().await.unwrap(); + check_get_response(&get_res, &keystores); + }) +} + +#[test] +fn import_some_duplicate_keystores() { + run_test(|tester| async move { + let password = random_password_string(); + let num_keystores = 5; + let keystores_all = (0..num_keystores) + .map(|_| new_keystore(password.clone())) + .collect::>(); + + // Import even numbered keystores first. + let keystores1 = keystores_all + .iter() + .enumerate() + .filter_map(|(i, keystore)| { + if i % 2 == 0 { + Some(keystore.clone()) + } else { + None + } + }) + .collect::>(); + + let req1 = ImportKeystoresRequest { + keystores: keystores1.clone(), + passwords: vec![password.clone(); keystores1.len()], + slashing_protection: None, + }; + + let req2 = ImportKeystoresRequest { + keystores: keystores_all.clone(), + passwords: vec![password.clone(); keystores_all.len()], + slashing_protection: None, + }; + + let import_res = tester.client.post_keystores(&req1).await.unwrap(); + check_import_response(&import_res, all_imported(keystores1.len())); + + // Check partial import. + let expected = (0..num_keystores).map(|i| { + if i % 2 == 0 { + ImportKeystoreStatus::Duplicate + } else { + ImportKeystoreStatus::Imported + } + }); + let import_res = tester.client.post_keystores(&req2).await.unwrap(); + check_import_response(&import_res, expected); + }) +} + +#[test] +fn import_wrong_number_of_passwords() { + run_test(|tester| async move { + let password = random_password_string(); + let keystores = (0..3) + .map(|_| new_keystore(password.clone())) + .collect::>(); + + let err = tester + .client + .post_keystores(&ImportKeystoresRequest { + keystores: keystores.clone(), + passwords: vec![password.clone()], + slashing_protection: None, + }) + .await + .unwrap_err(); + assert_eq!(err.status().unwrap(), 400); + }) +} + +#[test] +fn get_web3_signer_keystores() { + run_test(|tester| async move { + let num_local = 3; + let num_remote = 2; + + // Add some local validators. + let password = random_password_string(); + let keystores = (0..num_local) + .map(|_| new_keystore(password.clone())) + .collect::>(); + + let import_res = tester + .client + .post_keystores(&ImportKeystoresRequest { + keystores: keystores.clone(), + passwords: vec![password.clone(); keystores.len()], + slashing_protection: None, + }) + .await + .unwrap(); + + // All keystores should be imported. + check_import_response(&import_res, all_imported(keystores.len())); + + // Add some web3signer validators. + let remote_vals = (0..num_remote) + .map(|_| new_web3signer_validator().1) + .collect::>(); + + tester + .client + .post_lighthouse_validators_web3signer(&remote_vals) + .await + .unwrap(); + + // Check that both local and remote validators are returned. + let get_res = tester.client.get_keystores().await.unwrap(); + + let expected_responses = keystores + .iter() + .map(|local_keystore| SingleKeystoreResponse { + validating_pubkey: keystore_pubkey(local_keystore), + derivation_path: local_keystore.path(), + readonly: None, + }) + .chain(remote_vals.iter().map(|remote_val| SingleKeystoreResponse { + validating_pubkey: remote_val.voting_public_key.compress(), + derivation_path: None, + readonly: Some(true), + })) + .collect::>(); + + for response in expected_responses { + assert!(get_res.data.contains(&response), "{:?}", response); + } + }) +} + +#[test] +fn import_and_delete_conflicting_web3_signer_keystores() { + run_test(|tester| async move { + let num_keystores = 3; + + // Create some keystores to be used as both web3signer keystores and local keystores. + let password = random_password_string(); + let keystores = (0..num_keystores) + .map(|_| new_keystore(password.clone())) + .collect::>(); + let pubkeys = keystores.iter().map(keystore_pubkey).collect::>(); + + // Add the validators as web3signer validators. + let remote_vals = pubkeys + .iter() + .map(|pubkey| web3signer_validator_with_pubkey(pubkey.decompress().unwrap())) + .collect::>(); + + tester + .client + .post_lighthouse_validators_web3signer(&remote_vals) + .await + .unwrap(); + + // Attempt to import the same validators as local validators, which should error. + let import_req = ImportKeystoresRequest { + keystores: keystores.clone(), + passwords: vec![password.clone(); keystores.len()], + slashing_protection: None, + }; + let import_res = tester.client.post_keystores(&import_req).await.unwrap(); + check_import_response(&import_res, all_import_error(keystores.len())); + + // Attempt to delete the web3signer validators, which should fail. + let delete_req = DeleteKeystoresRequest { + pubkeys: pubkeys.clone(), + }; + let delete_res = tester.client.delete_keystores(&delete_req).await.unwrap(); + check_delete_response(&delete_res, all_delete_error(keystores.len())); + + // Get should still list all the validators as `readonly`. + let get_res = tester.client.get_keystores().await.unwrap(); + for (ks, pubkey) in get_res.data.iter().zip_eq(&pubkeys) { + assert_eq!(ks.validating_pubkey, *pubkey); + assert_eq!(ks.derivation_path, None); + assert_eq!(ks.readonly, Some(true)); + } + + // Disabling the web3signer validators should *still* prevent them from being + // overwritten. + for pubkey in &pubkeys { + tester + .client + .patch_lighthouse_validators(pubkey, false) + .await + .unwrap(); + } + let import_res = tester.client.post_keystores(&import_req).await.unwrap(); + check_import_response(&import_res, all_import_error(keystores.len())); + let delete_res = tester.client.delete_keystores(&delete_req).await.unwrap(); + check_delete_response(&delete_res, all_delete_error(keystores.len())); + }) +} + +#[test] +fn import_keystores_wrong_password() { + run_test(|tester| async move { + let num_keystores = 4; + let (keystores, correct_passwords): (Vec<_>, Vec<_>) = (0..num_keystores) + .map(|_| { + let password = random_password_string(); + (new_keystore(password.clone()), password) + }) + .unzip(); + + // First import with some incorrect passwords. + let incorrect_passwords = (0..num_keystores) + .map(|i| { + if i % 2 == 0 { + random_password_string() + } else { + correct_passwords[i].clone() + } + }) + .collect::>(); + + let import_res = tester + .client + .post_keystores(&ImportKeystoresRequest { + keystores: keystores.clone(), + passwords: incorrect_passwords.clone(), + slashing_protection: None, + }) + .await + .unwrap(); + + let expected_statuses = (0..num_keystores).map(|i| { + if i % 2 == 0 { + ImportKeystoreStatus::Error + } else { + ImportKeystoreStatus::Imported + } + }); + check_import_response(&import_res, expected_statuses); + + // Import again with the correct passwords and check that the statuses are as expected. + let correct_import_req = ImportKeystoresRequest { + keystores: keystores.clone(), + passwords: correct_passwords.clone(), + slashing_protection: None, + }; + let import_res = tester + .client + .post_keystores(&correct_import_req) + .await + .unwrap(); + let expected_statuses = (0..num_keystores).map(|i| { + if i % 2 == 0 { + ImportKeystoreStatus::Imported + } else { + ImportKeystoreStatus::Duplicate + } + }); + check_import_response(&import_res, expected_statuses); + + // Import one final time, at which point all keys should be duplicates. + let import_res = tester + .client + .post_keystores(&correct_import_req) + .await + .unwrap(); + check_import_response( + &import_res, + (0..num_keystores).map(|_| ImportKeystoreStatus::Duplicate), + ); + }); +} + +#[test] +fn import_invalid_slashing_protection() { + run_test(|tester| async move { + let password = random_password_string(); + let keystores = (0..3) + .map(|_| new_keystore(password.clone())) + .collect::>(); + + // Invalid slashing protection data with mismatched version and mismatched GVR. + let slashing_protection = Interchange { + metadata: InterchangeMetadata { + interchange_format_version: 0, + genesis_validators_root: Hash256::zero(), + }, + data: vec![], + }; + + let import_res = tester + .client + .post_keystores(&ImportKeystoresRequest { + keystores: keystores.clone(), + passwords: vec![password.clone(); keystores.len()], + slashing_protection: Some(InterchangeJsonStr(slashing_protection)), + }) + .await + .unwrap(); + + // All keystores should be imported. + check_import_response(&import_res, all_import_error(keystores.len())); + + // Check that GET lists none of the failed keystores. + let get_res = tester.client.get_keystores().await.unwrap(); + check_get_response(&get_res, &[]); + }) +} + +fn all_indices(count: usize) -> Vec { + (0..count).collect() +} + +#[test] +fn migrate_all_with_slashing_protection() { + let n = 3; + generic_migration_test( + n, + vec![ + (0, make_attestation(1, 2)), + (1, make_attestation(2, 3)), + (2, make_attestation(1, 2)), + ], + all_indices(n), + all_indices(n), + all_indices(n), + vec![ + (0, make_attestation(1, 2), false), + (1, make_attestation(2, 3), false), + (2, make_attestation(1, 2), false), + ], + ); +} + +#[test] +fn migrate_some_with_slashing_protection() { + let n = 3; + generic_migration_test( + n, + vec![ + (0, make_attestation(1, 2)), + (1, make_attestation(2, 3)), + (2, make_attestation(1, 2)), + ], + vec![0, 1], + vec![0, 1], + vec![0, 1], + vec![ + (0, make_attestation(1, 2), false), + (1, make_attestation(2, 3), false), + (0, make_attestation(2, 3), true), + (1, make_attestation(3, 4), true), + ], + ); +} + +#[test] +fn migrate_some_missing_slashing_protection() { + let n = 3; + generic_migration_test( + n, + vec![ + (0, make_attestation(1, 2)), + (1, make_attestation(2, 3)), + (2, make_attestation(1, 2)), + ], + vec![0, 1], + vec![0], + vec![0, 1], + vec![ + (0, make_attestation(1, 2), false), + (1, make_attestation(2, 3), true), + (0, make_attestation(2, 3), true), + ], + ); +} + +#[test] +fn migrate_some_extra_slashing_protection() { + let n = 3; + generic_migration_test( + n, + vec![ + (0, make_attestation(1, 2)), + (1, make_attestation(2, 3)), + (2, make_attestation(1, 2)), + ], + all_indices(n), + all_indices(n), + vec![0, 1], + vec![ + (0, make_attestation(1, 2), false), + (1, make_attestation(2, 3), false), + (0, make_attestation(2, 3), true), + (1, make_attestation(3, 4), true), + (2, make_attestation(2, 3), false), + ], + ); +} + +/// Run a test that creates some validators on one VC, and then migrates them to a second VC. +/// +/// All indices given are in the range 0..`num_validators`. They are *not* validator indices in the +/// ordinary sense. +/// +/// Parameters: +/// +/// - `num_validators`: the total number of validators to create +/// - `first_vc_attestations`: attestations to sign on the first VC as `(validator_idx, att)` +/// - `delete_indices`: validators to delete from the first VC +/// - `slashing_protection_indices`: validators to transfer slashing protection data for. It should +/// be a subset of `delete_indices` or the test will panic. +/// - `import_indices`: validators to transfer. It needn't be a subset of `delete_indices`. +/// - `second_vc_attestations`: attestations to sign on the second VC after the transfer. The bool +/// indicates whether the signing should be successful. +fn generic_migration_test( + num_validators: usize, + first_vc_attestations: Vec<(usize, Attestation)>, + delete_indices: Vec, + slashing_protection_indices: Vec, + import_indices: Vec, + second_vc_attestations: Vec<(usize, Attestation, bool)>, +) { + run_dual_vc_test(move |tester1, tester2| async move { + // Create the validators on VC1. + let (keystores, passwords): (Vec<_>, Vec<_>) = (0..num_validators) + .map(|_| { + let password = random_password_string(); + (new_keystore(password.clone()), password) + }) + .unzip(); + + let import_res = tester1 + .client + .post_keystores(&ImportKeystoresRequest { + keystores: keystores.clone(), + passwords: passwords.clone(), + slashing_protection: None, + }) + .await + .unwrap(); + check_import_response(&import_res, all_imported(keystores.len())); + + // Sign attestations on VC1. + for (validator_index, mut attestation) in first_vc_attestations { + let public_key = keystore_pubkey(&keystores[validator_index]); + let current_epoch = attestation.data.target.epoch; + tester1 + .validator_store + .sign_attestation(public_key, 0, &mut attestation, current_epoch) + .await + .unwrap(); + } + + // Delete the selected keys from VC1. + let delete_res = tester1 + .client + .delete_keystores(&DeleteKeystoresRequest { + pubkeys: delete_indices + .iter() + .copied() + .map(|i| keystore_pubkey(&keystores[i])) + .collect(), + }) + .await + .unwrap(); + check_delete_response(&delete_res, all_deleted(delete_indices.len())); + + // Check that slashing protection data was returned for all selected validators. + assert_eq!( + delete_res.slashing_protection.data.len(), + delete_indices.len() + ); + for &i in &delete_indices { + assert!(delete_res + .slashing_protection + .data + .iter() + .any(|interchange_data| interchange_data.pubkey == keystore_pubkey(&keystores[i]))); + } + + // Filter slashing protection according to `slashing_protection_indices`. + let mut slashing_protection = delete_res.slashing_protection; + let data = std::mem::take(&mut slashing_protection.data); + + for &i in &slashing_protection_indices { + let pubkey = keystore_pubkey(&keystores[i]); + slashing_protection.data.push( + data.iter() + .find(|interchange_data| interchange_data.pubkey == pubkey) + .expect("slashing protection indices should be subset of deleted") + .clone(), + ); + } + assert_eq!( + slashing_protection.data.len(), + slashing_protection_indices.len() + ); + + // Import into the 2nd VC using the slashing protection data. + let import_res = tester2 + .client + .post_keystores(&ImportKeystoresRequest { + keystores: import_indices + .iter() + .copied() + .map(|i| keystores[i].clone()) + .collect(), + passwords: import_indices + .iter() + .copied() + .map(|i| passwords[i].clone()) + .collect(), + slashing_protection: Some(InterchangeJsonStr(slashing_protection)), + }) + .await + .unwrap(); + check_import_response(&import_res, all_imported(import_indices.len())); + + // Sign attestations on the second VC. + for (validator_index, mut attestation, should_succeed) in second_vc_attestations { + let public_key = keystore_pubkey(&keystores[validator_index]); + let current_epoch = attestation.data.target.epoch; + match tester2 + .validator_store + .sign_attestation(public_key, 0, &mut attestation, current_epoch) + .await + { + Ok(()) => assert!(should_succeed), + Err(e) => assert!(!should_succeed, "{:?}", e), + } + } + }); +} + +#[test] +fn delete_keystores_twice() { + run_test(|tester| async move { + let password = random_password_string(); + let keystores = (0..2) + .map(|_| new_keystore(password.clone())) + .collect::>(); + + // 1. Import all keystores. + let import_req = ImportKeystoresRequest { + keystores: keystores.clone(), + passwords: vec![password.clone(); keystores.len()], + slashing_protection: None, + }; + let import_res = tester.client.post_keystores(&import_req).await.unwrap(); + check_import_response(&import_res, all_imported(keystores.len())); + + // 2. Delete all. + let delete_req = DeleteKeystoresRequest { + pubkeys: keystores.iter().map(keystore_pubkey).collect(), + }; + let delete_res = tester.client.delete_keystores(&delete_req).await.unwrap(); + check_delete_response(&delete_res, all_deleted(keystores.len())); + + // 3. Delete again. + let delete_res = tester.client.delete_keystores(&delete_req).await.unwrap(); + check_delete_response(&delete_res, all_not_active(keystores.len())); + }) +} + +#[test] +fn delete_nonexistent_keystores() { + run_test(|tester| async move { + let password = random_password_string(); + let keystores = (0..2) + .map(|_| new_keystore(password.clone())) + .collect::>(); + + // Delete all. + let delete_req = DeleteKeystoresRequest { + pubkeys: keystores.iter().map(keystore_pubkey).collect(), + }; + let delete_res = tester.client.delete_keystores(&delete_req).await.unwrap(); + check_delete_response(&delete_res, all_not_found(keystores.len())); + }) +} + +fn make_attestation(source_epoch: u64, target_epoch: u64) -> Attestation { + Attestation { + aggregation_bits: BitList::with_capacity( + ::MaxValidatorsPerCommittee::to_usize(), + ) + .unwrap(), + data: AttestationData { + source: Checkpoint { + epoch: Epoch::new(source_epoch), + root: Hash256::from_low_u64_le(source_epoch), + }, + target: Checkpoint { + epoch: Epoch::new(target_epoch), + root: Hash256::from_low_u64_le(target_epoch), + }, + ..AttestationData::default() + }, + signature: AggregateSignature::empty(), + } +} + +#[test] +fn delete_concurrent_with_signing() { + let runtime = build_runtime(); + let num_keys = 8; + let num_signing_threads = 8; + let num_attestations = 100; + let num_delete_threads = 8; + let num_delete_attempts = 100; + let delete_prob = 0.01; + + assert!( + num_keys % num_signing_threads == 0, + "num_keys should be divisible by num threads for simplicity" + ); + + let weak_runtime = Arc::downgrade(&runtime); + runtime.block_on(async { + let tester = ApiTester::new(weak_runtime).await; + + // Generate a lot of keys and import them. + let password = random_password_string(); + let keystores = (0..num_keys) + .map(|_| new_keystore(password.clone())) + .collect::>(); + let all_pubkeys = keystores.iter().map(keystore_pubkey).collect::>(); + + let import_res = tester + .client + .post_keystores(&ImportKeystoresRequest { + keystores: keystores.clone(), + passwords: vec![password.clone(); keystores.len()], + slashing_protection: None, + }) + .await + .unwrap(); + check_import_response(&import_res, all_imported(keystores.len())); + + // Start several threads signing attestations at sequential epochs. + let mut join_handles = vec![]; + + for thread_index in 0..num_signing_threads { + let keys_per_thread = num_keys / num_signing_threads; + let validator_store = tester.validator_store.clone(); + let thread_pubkeys = all_pubkeys + [thread_index * keys_per_thread..(thread_index + 1) * keys_per_thread] + .to_vec(); + + let handle = runtime.spawn(async move { + for j in 0..num_attestations { + let mut att = make_attestation(j, j + 1); + for (_validator_id, public_key) in thread_pubkeys.iter().enumerate() { + let _ = validator_store + .sign_attestation(*public_key, 0, &mut att, Epoch::new(j + 1)) + .await; + } + } + }); + join_handles.push(handle); + } + + // Concurrently, delete each validator one at a time. Store the slashing protection + // data so we can ensure it doesn't change after a key is exported. + let mut delete_handles = vec![]; + for _ in 0..num_delete_threads { + let client = tester.client.clone(); + let all_pubkeys = all_pubkeys.clone(); + + let handle = runtime.spawn(async move { + let mut rng = SmallRng::from_entropy(); + + let mut slashing_protection = vec![]; + for _ in 0..num_delete_attempts { + let to_delete = all_pubkeys + .iter() + .filter(|_| rng.gen_bool(delete_prob)) + .copied() + .collect::>(); + + if !to_delete.is_empty() { + let delete_res = client + .delete_keystores(&DeleteKeystoresRequest { pubkeys: to_delete }) + .await + .unwrap(); + + for status in delete_res.data.iter() { + assert_ne!(status.status, DeleteKeystoreStatus::Error); + } + + slashing_protection.push(delete_res.slashing_protection); + } + } + slashing_protection + }); + + delete_handles.push(handle); + } + + // Collect slashing protection. + let mut slashing_protection_map = HashMap::new(); + let collected_slashing_protection = futures::future::join_all(delete_handles).await; + + for interchange in collected_slashing_protection + .into_iter() + .map(Result::unwrap) + .flatten() + { + for validator_data in interchange.data { + slashing_protection_map + .entry(validator_data.pubkey) + .and_modify(|existing| { + assert_eq!( + *existing, validator_data, + "slashing protection data changed after first export" + ) + }) + .or_insert(validator_data); + } + } + + futures::future::join_all(join_handles).await + }); +} + +#[test] +fn delete_then_reimport() { + run_test(|tester| async move { + let password = random_password_string(); + let keystores = (0..2) + .map(|_| new_keystore(password.clone())) + .collect::>(); + + // 1. Import all keystores. + let import_req = ImportKeystoresRequest { + keystores: keystores.clone(), + passwords: vec![password.clone(); keystores.len()], + slashing_protection: None, + }; + let import_res = tester.client.post_keystores(&import_req).await.unwrap(); + check_import_response(&import_res, all_imported(keystores.len())); + + // 2. Delete all. + let delete_res = tester + .client + .delete_keystores(&DeleteKeystoresRequest { + pubkeys: keystores.iter().map(keystore_pubkey).collect(), + }) + .await + .unwrap(); + check_delete_response(&delete_res, all_deleted(keystores.len())); + + // 3. Re-import + let import_res = tester.client.post_keystores(&import_req).await.unwrap(); + check_import_response(&import_res, all_imported(keystores.len())); + }) +} diff --git a/validator_client/src/initialized_validators.rs b/validator_client/src/initialized_validators.rs index 72e651f7d1..5900c8e56b 100644 --- a/validator_client/src/initialized_validators.rs +++ b/validator_client/src/initialized_validators.rs @@ -14,19 +14,22 @@ use account_utils::{ }, ZeroizeString, }; +use eth2::lighthouse_vc::std_types::DeleteKeystoreStatus; use eth2_keystore::Keystore; use lighthouse_metrics::set_gauge; use lockfile::{Lockfile, LockfileError}; +use parking_lot::{MappedMutexGuard, Mutex, MutexGuard}; use reqwest::{Certificate, Client, Error as ReqwestError}; use slog::{debug, error, info, warn, Logger}; use std::collections::{HashMap, HashSet}; -use std::fs::File; +use std::fs::{self, File}; use std::io::{self, Read}; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; use types::{Graffiti, Keypair, PublicKey, PublicKeyBytes}; use url::{ParseError, Url}; +use validator_dir::Builder as ValidatorDirBuilder; use crate::key_cache; use crate::key_cache::KeyCache; @@ -67,6 +70,10 @@ pub enum Error { UnableToSaveDefinitions(validator_definitions::Error), /// It is not legal to try and initialize a disabled validator definition. UnableToInitializeDisabledValidator, + /// There was an error while deleting a keystore file. + UnableToDeleteKeystore(PathBuf, io::Error), + /// There was an error while deleting a validator dir. + UnableToDeleteValidatorDir(PathBuf, io::Error), /// There was an error reading from stdin. UnableToReadPasswordFromUser(String), /// There was an error running a tokio async task. @@ -83,6 +90,8 @@ pub enum Error { InvalidWeb3SignerRootCertificateFile(io::Error), InvalidWeb3SignerRootCertificate(ReqwestError), UnableToBuildWeb3SignerClient(ReqwestError), + /// Unable to apply an action to a validator because it is using a remote signer. + InvalidActionOnRemoteValidator, } impl From for Error { @@ -101,12 +110,15 @@ pub struct InitializedValidator { impl InitializedValidator { /// Return a reference to this validator's lockfile if it has one. - pub fn keystore_lockfile(&self) -> Option<&Lockfile> { + pub fn keystore_lockfile(&self) -> Option> { match self.signing_method.as_ref() { SigningMethod::LocalKeystore { ref voting_keystore_lockfile, .. - } => Some(voting_keystore_lockfile), + } => MutexGuard::try_map(voting_keystore_lockfile.lock(), |option_lockfile| { + option_lockfile.as_mut() + }) + .ok(), // Web3Signer validators do not have any lockfiles. SigningMethod::Web3Signer { .. } => None, } @@ -213,7 +225,7 @@ impl InitializedValidator { let lockfile_path = get_lockfile_path(&voting_keystore_path) .ok_or_else(|| Error::BadVotingKeystorePath(voting_keystore_path.clone()))?; - let voting_keystore_lockfile = Lockfile::new(lockfile_path)?; + let voting_keystore_lockfile = Mutex::new(Some(Lockfile::new(lockfile_path)?)); SigningMethod::LocalKeystore { voting_keystore_path, @@ -381,6 +393,25 @@ impl InitializedValidators { .map(|v| v.signing_method.clone()) } + /// Add a validator definition to `self`, replacing any disabled definition with the same + /// voting public key. + /// + /// The on-disk representation of the validator definitions & the key cache will both be + /// updated. + pub async fn add_definition_replace_disabled( + &mut self, + def: ValidatorDefinition, + ) -> Result<(), Error> { + // Drop any disabled definitions with the same public key. + let delete_def = |existing_def: &ValidatorDefinition| { + !existing_def.enabled && existing_def.voting_public_key == def.voting_public_key + }; + self.definitions.retain(|def| !delete_def(def)); + + // Add the definition. + self.add_definition(def).await + } + /// Add a validator definition to `self`, overwriting the on-disk representation of `self`. pub async fn add_definition(&mut self, def: ValidatorDefinition) -> Result<(), Error> { if self @@ -403,6 +434,91 @@ impl InitializedValidators { Ok(()) } + /// Delete the validator definition and keystore for `pubkey`. + /// + /// The delete is carried out in stages so that the filesystem is never left in an inconsistent + /// state, even in case of errors or crashes. + pub async fn delete_definition_and_keystore( + &mut self, + pubkey: &PublicKey, + ) -> Result { + // 1. Disable the validator definition. + // + // We disable before removing so that in case of a crash the auto-discovery mechanism + // won't re-activate the keystore. + if let Some(def) = self + .definitions + .as_mut_slice() + .iter_mut() + .find(|def| &def.voting_public_key == pubkey) + { + if def.signing_definition.is_local_keystore() { + def.enabled = false; + self.definitions + .save(&self.validators_dir) + .map_err(Error::UnableToSaveDefinitions)?; + } else { + return Err(Error::InvalidActionOnRemoteValidator); + } + } else { + return Ok(DeleteKeystoreStatus::NotFound); + } + + // 2. Delete from `self.validators`, which holds the signing method. + // Delete the keystore files. + if let Some(initialized_validator) = self.validators.remove(&pubkey.compress()) { + if let SigningMethod::LocalKeystore { + ref voting_keystore_path, + ref voting_keystore_lockfile, + ref voting_keystore, + .. + } = *initialized_validator.signing_method + { + // Drop the lock file so that it may be deleted. This is particularly important on + // Windows where the lockfile will fail to be deleted if it is still open. + drop(voting_keystore_lockfile.lock().take()); + + self.delete_keystore_or_validator_dir(voting_keystore_path, voting_keystore)?; + } + } + + // 3. Delete from validator definitions entirely. + self.definitions + .retain(|def| &def.voting_public_key != pubkey); + self.definitions + .save(&self.validators_dir) + .map_err(Error::UnableToSaveDefinitions)?; + + Ok(DeleteKeystoreStatus::Deleted) + } + + /// Attempt to delete the voting keystore file, or its entire validator directory. + /// + /// Some parts of the VC assume the existence of a validator based on the existence of a + /// directory in the validators dir named like a public key. + fn delete_keystore_or_validator_dir( + &self, + voting_keystore_path: &Path, + voting_keystore: &Keystore, + ) -> Result<(), Error> { + // If the parent directory is a `ValidatorDir` within `self.validators_dir`, then + // delete the entire directory so that it may be recreated if the keystore is + // re-imported. + if let Some(validator_dir) = voting_keystore_path.parent() { + if validator_dir + == ValidatorDirBuilder::get_dir_path(&self.validators_dir, voting_keystore) + { + fs::remove_dir_all(validator_dir) + .map_err(|e| Error::UnableToDeleteValidatorDir(validator_dir.into(), e))?; + return Ok(()); + } + } + // Otherwise just delete the keystore file. + fs::remove_file(voting_keystore_path) + .map_err(|e| Error::UnableToDeleteKeystore(voting_keystore_path.into(), e))?; + Ok(()) + } + /// Returns a slice of all defined validators (regardless of their enabled state). pub fn validator_definitions(&self) -> &[ValidatorDefinition] { self.definitions.as_slice() @@ -456,17 +572,24 @@ impl InitializedValidators { /// Tries to decrypt the key cache. /// - /// Returns `Ok(true)` if decryption was successful, `Ok(false)` if it couldn't get decrypted - /// and an error if a needed password couldn't get extracted. + /// Returns the decrypted cache if decryption was successful, or an error if a required password + /// wasn't provided and couldn't be read interactively. /// + /// In the case that the cache contains UUIDs for unknown validator definitions then it cannot + /// be decrypted and will be replaced by a new empty cache. + /// + /// The mutable `key_stores` argument will be used to accelerate decyption by bypassing + /// filesystem accesses for keystores that are already known. In the case that a keystore + /// from the validator definitions is not yet in this map, it will be loaded from disk and + /// inserted into the map. async fn decrypt_key_cache( &self, mut cache: KeyCache, key_stores: &mut HashMap, ) -> Result { - //read relevant key_stores + // Read relevant key stores from the filesystem. let mut definitions_map = HashMap::new(); - for def in self.definitions.as_slice() { + for def in self.definitions.as_slice().iter().filter(|def| def.enabled) { match &def.signing_definition { SigningDefinition::LocalKeystore { voting_keystore_path, @@ -487,10 +610,11 @@ impl InitializedValidators { //check if all paths are in the definitions_map for uuid in cache.uuids() { if !definitions_map.contains_key(uuid) { - warn!( + debug!( self.log, - "Unknown uuid in cache"; - "uuid" => format!("{}", uuid) + "Resetting the key cache"; + "keystore_uuid" => %uuid, + "reason" => "impossible to decrypt due to missing keystore", ); return Ok(KeyCache::new()); } @@ -547,7 +671,7 @@ impl InitializedValidators { /// A validator is considered "already known" and skipped if the public key is already known. /// I.e., if there are two different definitions with the same public key then the second will /// be ignored. - async fn update_validators(&mut self) -> Result<(), Error> { + pub(crate) async fn update_validators(&mut self) -> Result<(), Error> { //use key cache if available let mut key_stores = HashMap::new(); diff --git a/validator_client/src/signing_method.rs b/validator_client/src/signing_method.rs index 7f28700a20..3c12ac1e62 100644 --- a/validator_client/src/signing_method.rs +++ b/validator_client/src/signing_method.rs @@ -6,6 +6,7 @@ use crate::http_metrics::metrics; use eth2_keystore::Keystore; use lockfile::Lockfile; +use parking_lot::Mutex; use reqwest::Client; use std::path::PathBuf; use std::sync::Arc; @@ -75,7 +76,7 @@ pub enum SigningMethod { /// A validator that is defined by an EIP-2335 keystore on the local filesystem. LocalKeystore { voting_keystore_path: PathBuf, - voting_keystore_lockfile: Lockfile, + voting_keystore_lockfile: Mutex>, voting_keystore: Keystore, voting_keypair: Arc, }, diff --git a/validator_client/src/validator_store.rs b/validator_client/src/validator_store.rs index d7efa806ae..884b97694e 100644 --- a/validator_client/src/validator_store.rs +++ b/validator_client/src/validator_store.rs @@ -6,7 +6,9 @@ use crate::{ }; use account_utils::{validator_definitions::ValidatorDefinition, ZeroizeString}; use parking_lot::{Mutex, RwLock}; -use slashing_protection::{NotSafe, Safe, SlashingDatabase}; +use slashing_protection::{ + interchange::Interchange, InterchangeError, NotSafe, Safe, SlashingDatabase, +}; use slog::{crit, error, info, warn, Logger}; use slot_clock::SlotClock; use std::iter::FromIterator; @@ -183,7 +185,7 @@ impl ValidatorStore { self.validators .write() - .add_definition(validator_def.clone()) + .add_definition_replace_disabled(validator_def.clone()) .await .map_err(|e| format!("Unable to add definition: {:?}", e))?; @@ -693,6 +695,48 @@ impl ValidatorStore { Ok(SignedContributionAndProof { message, signature }) } + pub fn import_slashing_protection( + &self, + interchange: Interchange, + ) -> Result<(), InterchangeError> { + self.slashing_protection + .import_interchange_info(interchange, self.genesis_validators_root)?; + Ok(()) + } + + /// Export slashing protection data while also disabling the given keys in the database. + /// + /// If any key is unknown to the slashing protection database it will be silently omitted + /// from the result. It is the caller's responsibility to check whether all keys provided + /// had data returned for them. + pub fn export_slashing_protection_for_keys( + &self, + pubkeys: &[PublicKeyBytes], + ) -> Result { + self.slashing_protection.with_transaction(|txn| { + let known_pubkeys = pubkeys + .iter() + .filter_map(|pubkey| { + let validator_id = self + .slashing_protection + .get_validator_id_ignoring_status(txn, pubkey) + .ok()?; + + Some( + self.slashing_protection + .update_validator_status(txn, validator_id, false) + .map(|()| *pubkey), + ) + }) + .collect::, _>>()?; + self.slashing_protection.export_interchange_info_in_txn( + self.genesis_validators_root, + Some(&known_pubkeys), + txn, + ) + }) + } + /// Prune the slashing protection database so that it remains performant. /// /// This function will only do actual pruning periodically, so it should usually be From 9ff21601983521c1b4257a50c996fe7af49447f9 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Sun, 30 Jan 2022 23:22:05 +0000 Subject: [PATCH 15/23] Parse uint256 as decimal string (#2957) ## Issue Addressed N/A ## Proposed Changes https://github.com/sigp/lighthouse/pull/2940 introduced a bug where we parsed the uint256 terminal total difficulty as a hex string instead of a decimal string. Fixes the bug and adds tests. --- consensus/types/src/chain_spec.rs | 83 +++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 4 deletions(-) diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index 8f58003572..fa74f9d29c 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -843,10 +843,16 @@ fn default_bellatrix_fork_epoch() -> Option> { None } -fn default_terminal_total_difficulty() -> Uint256 { - "115792089237316195423570985008687907853269984665640564039457584007913129638912" - .parse() - .unwrap() +/// Placeholder value: 2^256-2^10 (115792089237316195423570985008687907853269984665640564039457584007913129638912). +/// +/// Taken from https://github.com/ethereum/consensus-specs/blob/d5e4828aecafaf1c57ef67a5f23c4ae7b08c5137/configs/mainnet.yaml#L15-L16 +const fn default_terminal_total_difficulty() -> Uint256 { + ethereum_types::U256([ + 18446744073709550592, + 18446744073709551615, + 18446744073709551615, + 18446744073709551615, + ]) } fn default_terminal_block_hash() -> Hash256 { @@ -1194,4 +1200,73 @@ mod yaml_tests { .expect("should have applied spec"); assert_eq!(new_spec, ChainSpec::minimal()); } + + #[test] + fn test_defaults() { + // Spec yaml string. Fields that serialize/deserialize with a default value are commented out. + let spec = r#" + PRESET_BASE: 'mainnet' + #TERMINAL_TOTAL_DIFFICULTY: 115792089237316195423570985008687907853269984665640564039457584007913129638911 + #TERMINAL_BLOCK_HASH: 0x0000000000000000000000000000000000000000000000000000000000000001 + #TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH: 18446744073709551614 + MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 16384 + MIN_GENESIS_TIME: 1606824000 + GENESIS_FORK_VERSION: 0x00000000 + GENESIS_DELAY: 604800 + ALTAIR_FORK_VERSION: 0x01000000 + ALTAIR_FORK_EPOCH: 74240 + #BELLATRIX_FORK_VERSION: 0x02000000 + #BELLATRIX_FORK_EPOCH: 18446744073709551614 + SHARDING_FORK_VERSION: 0x03000000 + SHARDING_FORK_EPOCH: 18446744073709551615 + 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: 70 + DEPOSIT_CHAIN_ID: 1 + DEPOSIT_NETWORK_ID: 1 + DEPOSIT_CONTRACT_ADDRESS: 0x00000000219ab540356cBB839Cbe05303d7705Fa + "#; + + let chain_spec: Config = serde_yaml::from_str(spec).unwrap(); + assert_eq!( + chain_spec.terminal_total_difficulty, + default_terminal_total_difficulty() + ); + assert_eq!( + chain_spec.terminal_block_hash, + default_terminal_block_hash() + ); + assert_eq!( + chain_spec.terminal_block_hash_activation_epoch, + default_terminal_block_hash_activation_epoch() + ); + + assert_eq!( + chain_spec.bellatrix_fork_epoch, + default_bellatrix_fork_epoch() + ); + + assert_eq!( + chain_spec.bellatrix_fork_version, + default_bellatrix_fork_version() + ); + } + + #[test] + fn test_total_terminal_difficulty() { + assert_eq!( + Ok(default_terminal_total_difficulty()), + Uint256::from_dec_str( + "115792089237316195423570985008687907853269984665640564039457584007913129638912" + ) + ); + } } From bdd70d7aefe6489bf64d0e542783dc26a51a3e93 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Mon, 31 Jan 2022 07:29:41 +0000 Subject: [PATCH 16/23] Reduce gossip history (#2969) The gossipsub history was increased to a good portion of a slot from 2.1 seconds in the last release. Although it shouldn't cause too much issue, it could be related to recieving later messages than usual and interacting with our scoring system penalizing peers. For consistency, this PR reduces the time we gossip messages back to the same values of the previous release. It also adjusts the gossipsub heartbeat time for testing purposes with a developer flag but this should not effect end users. --- beacon_node/lighthouse_network/src/config.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/beacon_node/lighthouse_network/src/config.rs b/beacon_node/lighthouse_network/src/config.rs index 4cafcf62b1..7a2ba61997 100644 --- a/beacon_node/lighthouse_network/src/config.rs +++ b/beacon_node/lighthouse_network/src/config.rs @@ -219,6 +219,7 @@ pub struct NetworkLoad { pub mesh_n_high: usize, pub gossip_lazy: usize, pub history_gossip: usize, + pub heartbeat_interval: Duration, } impl From for NetworkLoad { @@ -231,7 +232,8 @@ impl From for NetworkLoad { mesh_n: 3, mesh_n_high: 4, gossip_lazy: 3, - history_gossip: 12, + history_gossip: 3, + heartbeat_interval: Duration::from_millis(1200), }, 2 => NetworkLoad { name: "Low", @@ -240,7 +242,8 @@ impl From for NetworkLoad { mesh_n: 4, mesh_n_high: 8, gossip_lazy: 3, - history_gossip: 12, + history_gossip: 3, + heartbeat_interval: Duration::from_millis(1000), }, 3 => NetworkLoad { name: "Average", @@ -249,7 +252,8 @@ impl From for NetworkLoad { mesh_n: 5, mesh_n_high: 10, gossip_lazy: 3, - history_gossip: 12, + history_gossip: 3, + heartbeat_interval: Duration::from_millis(700), }, 4 => NetworkLoad { name: "Average", @@ -258,7 +262,8 @@ impl From for NetworkLoad { mesh_n: 8, mesh_n_high: 12, gossip_lazy: 3, - history_gossip: 12, + history_gossip: 3, + heartbeat_interval: Duration::from_millis(700), }, // 5 and above _ => NetworkLoad { @@ -268,7 +273,8 @@ impl From for NetworkLoad { mesh_n: 10, mesh_n_high: 15, gossip_lazy: 5, - history_gossip: 12, + history_gossip: 6, + heartbeat_interval: Duration::from_millis(500), }, } } @@ -322,7 +328,7 @@ pub fn gossipsub_config(network_load: u8, fork_context: Arc) -> Gos GossipsubConfigBuilder::default() .max_transmit_size(gossip_max_size(is_merge_enabled)) - .heartbeat_interval(Duration::from_millis(700)) + .heartbeat_interval(load.heartbeat_interval) .mesh_n(load.mesh_n) .mesh_n_low(load.mesh_n_low) .mesh_outbound_min(load.outbound_min) From 139b44342f9270a12d1165a16d23811bd9dd1a01 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 31 Jan 2022 22:55:03 +0000 Subject: [PATCH 17/23] Optimized Docker images (#2966) ## Issue Addressed Closes #2938 ## Proposed Changes * Build and publish images with a `-modern` suffix which enable CPU optimizations for modern hardware. * Add docs for the plethora of available images! * Unify all the Docker workflows in `docker.yml` (including for tagged releases). ## Additional Info The `Dockerfile` is no longer used by our Docker Hub builds, as we use `cross` and a generic approach for ARM and x86. There's a new CI job `docker-build-from-source` which tests the `Dockerfile` without publishing anything. --- .github/workflows/docker.yml | 135 +++++++++++++--------- .github/workflows/release.yml | 68 ----------- .github/workflows/test-suite.yml | 4 +- Dockerfile | 6 +- Makefile | 44 +++---- book/src/cross-compiling.md | 9 +- book/src/docker.md | 86 ++++++++++---- book/src/installation-binaries.md | 17 ++- lcli/Dockerfile | 2 +- testing/antithesis/Dockerfile.libvoidstar | 2 +- 10 files changed, 187 insertions(+), 186 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 7b606dd0f6..b07f2ad3d4 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -5,6 +5,8 @@ on: branches: - unstable - stable + tags: + - v* env: DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} @@ -13,20 +15,48 @@ env: LCLI_IMAGE_NAME: ${{ github.repository_owner }}/lcli jobs: - extract-branch-name: + # Extract the VERSION which is either `latest` or `vX.Y.Z`, and the VERSION_SUFFIX + # which is either empty or `-unstable`. + # + # It would be nice if the arch didn't get spliced into the version between `latest` and + # `unstable`, but for now we keep the two parts of the version separate for backwards + # compatibility. + extract-version: runs-on: ubuntu-18.04 steps: - - name: Extract branch name - run: echo "::set-output name=BRANCH_NAME::$(echo ${GITHUB_REF#refs/heads/})" - id: extract_branch + - name: Extract version (if stable) + if: github.event.ref == 'refs/heads/stable' + run: | + echo "VERSION=latest" >> $GITHUB_ENV + echo "VERSION_SUFFIX=" >> $GITHUB_ENV + - name: Extract version (if unstable) + if: github.event.ref == 'refs/heads/unstable' + run: | + echo "VERSION=latest" >> $GITHUB_ENV + echo "VERSION_SUFFIX=-unstable" >> $GITHUB_ENV + - name: Extract version (if tagged release) + if: startsWith(github.event.ref, 'refs/tags') + run: | + echo "VERSION=$(echo ${GITHUB_REF#refs/tags/})" >> $GITHUB_ENV + echo "VERSION_SUFFIX=" >> $GITHUB_ENV outputs: - BRANCH_NAME: ${{ steps.extract_branch.outputs.BRANCH_NAME }} - build-docker-arm64: + VERSION: ${{ env.VERSION }} + VERSION_SUFFIX: ${{ env.VERSION_SUFFIX }} + build-docker-single-arch: + name: build-docker-${{ matrix.binary }} runs-on: ubuntu-18.04 - needs: [extract-branch-name] - # We need to enable experimental docker features in order to use `docker buildx` + strategy: + matrix: + binary: [aarch64, + aarch64-portable, + x86_64, + x86_64-portable] + needs: [extract-version] env: + # We need to enable experimental docker features in order to use `docker buildx` DOCKER_CLI_EXPERIMENTAL: enabled + VERSION: ${{ needs.extract-version.outputs.VERSION }} + VERSION_SUFFIX: ${{ needs.extract-version.outputs.VERSION_SUFFIX }} steps: - uses: actions/checkout@v2 - name: Update Rust @@ -34,85 +64,76 @@ jobs: - name: Dockerhub login run: | echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin - - name: Cross build lighthouse binary - run: | + - name: Cross build Lighthouse binary + run: | cargo install cross - make build-aarch64-portable - - name: Move cross-built ARM binary into Docker scope + make build-${{ matrix.binary }} + - name: Move cross-built binary into Docker scope (if ARM) + if: startsWith(matrix.binary, 'aarch64') run: | mkdir ./bin; mv ./target/aarch64-unknown-linux-gnu/release/lighthouse ./bin; - - name: Set Env - if: needs.extract-branch-name.outputs.BRANCH_NAME == 'unstable' + - name: Move cross-built binary into Docker scope (if x86_64) + if: startsWith(matrix.binary, 'x86_64') run: | - echo "TAG_SUFFIX=-unstable" >> $GITHUB_ENV; + mkdir ./bin; + mv ./target/x86_64-unknown-linux-gnu/release/lighthouse ./bin; + - name: Map aarch64 to arm64 short arch + if: startsWith(matrix.binary, 'aarch64') + run: echo "SHORT_ARCH=arm64" >> $GITHUB_ENV + - name: Map x86_64 to amd64 short arch + if: startsWith(matrix.binary, 'x86_64') + run: echo "SHORT_ARCH=amd64" >> $GITHUB_ENV; + - name: Set modernity suffix + if: endsWith(matrix.binary, '-portable') != true + run: echo "MODERNITY_SUFFIX=-modern" >> $GITHUB_ENV; # Install dependencies for emulation. Have to create a new builder to pick up emulation support. - - name: Build ARM64 dockerfile (with push) + - name: Build Dockerfile and push run: | - docker run --privileged --rm tonistiigi/binfmt --install arm64 + docker run --privileged --rm tonistiigi/binfmt --install ${SHORT_ARCH} docker buildx create --use --name cross-builder docker buildx build \ - --platform=linux/arm64 \ + --platform=linux/${SHORT_ARCH} \ --file ./Dockerfile.cross . \ - --tag ${IMAGE_NAME}:latest-arm64${TAG_SUFFIX} \ + --tag ${IMAGE_NAME}:${VERSION}-${SHORT_ARCH}${VERSION_SUFFIX}${MODERNITY_SUFFIX} \ --push - build-docker-amd64: - runs-on: ubuntu-18.04 - needs: [extract-branch-name] - steps: - - uses: actions/checkout@v2 - - name: Update Rust - run: rustup update stable - - name: Dockerhub login - run: | - echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin - - name: Set Env - if: needs.extract-branch-name.outputs.BRANCH_NAME == 'unstable' - run: | - echo "TAG_SUFFIX=-unstable" >> $GITHUB_ENV; - - name: Build AMD64 dockerfile (with push) - run: | - docker build \ - --build-arg PORTABLE=true \ - --tag ${IMAGE_NAME}:latest-amd64${TAG_SUFFIX} \ - --file ./Dockerfile . - docker push ${IMAGE_NAME}:latest-amd64${TAG_SUFFIX} build-docker-multiarch: + name: build-docker-multiarch${{ matrix.modernity }} runs-on: ubuntu-18.04 - needs: [build-docker-arm64, build-docker-amd64, extract-branch-name] - # We need to enable experimental docker features in order to use `docker manifest` + needs: [build-docker-single-arch, extract-version] + strategy: + matrix: + modernity: ["", "-modern"] env: + # We need to enable experimental docker features in order to use `docker manifest` DOCKER_CLI_EXPERIMENTAL: enabled + VERSION: ${{ needs.extract-version.outputs.VERSION }} + VERSION_SUFFIX: ${{ needs.extract-version.outputs.VERSION_SUFFIX }} steps: - name: Dockerhub login run: | echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin - - name: Set Env - if: needs.extract-branch-name.outputs.BRANCH_NAME == 'unstable' - run: | - echo "TAG_SUFFIX=-unstable" >> $GITHUB_ENV; - name: Create and push multiarch manifest run: | - docker manifest create ${IMAGE_NAME}:latest${TAG_SUFFIX} \ - --amend ${IMAGE_NAME}:latest-arm64${TAG_SUFFIX} \ - --amend ${IMAGE_NAME}:latest-amd64${TAG_SUFFIX}; - docker manifest push ${IMAGE_NAME}:latest${TAG_SUFFIX} + docker manifest create ${IMAGE_NAME}:${VERSION}${VERSION_SUFFIX}${{ matrix.modernity }} \ + --amend ${IMAGE_NAME}:${VERSION}-arm64${VERSION_SUFFIX}${{ matrix.modernity }} \ + --amend ${IMAGE_NAME}:${VERSION}-amd64${VERSION_SUFFIX}${{ matrix.modernity }}; + docker manifest push ${IMAGE_NAME}:${VERSION}${VERSION_SUFFIX}${{ matrix.modernity }} build-docker-lcli: runs-on: ubuntu-18.04 - needs: [extract-branch-name] + needs: [extract-version] + env: + VERSION: ${{ needs.extract-version.outputs.VERSION }} + VERSION_SUFFIX: ${{ needs.extract-version.outputs.VERSION_SUFFIX }} steps: - uses: actions/checkout@v2 - name: Dockerhub login run: | echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin - - name: Set Env - if: needs.extract-branch-name.outputs.BRANCH_NAME == 'unstable' - run: | - echo "TAG_SUFFIX=-unstable" >> $GITHUB_ENV; - name: Build lcli dockerfile (with push) run: | docker build \ --build-arg PORTABLE=true \ - --tag ${LCLI_IMAGE_NAME}:latest${TAG_SUFFIX} \ + --tag ${LCLI_IMAGE_NAME}:${VERSION}${VERSION_SUFFIX} \ --file ./lcli/Dockerfile . - docker push ${LCLI_IMAGE_NAME}:latest${TAG_SUFFIX} + docker push ${LCLI_IMAGE_NAME}:${VERSION}${VERSION_SUFFIX} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 87d309dc42..5b28a5ec71 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,74 +20,6 @@ jobs: id: extract_version outputs: VERSION: ${{ steps.extract_version.outputs.VERSION }} - - build-docker-arm64: - runs-on: ubuntu-18.04 - needs: [extract-version] - # We need to enable experimental docker features in order to use `docker buildx` - env: - DOCKER_CLI_EXPERIMENTAL: enabled - VERSION: ${{ needs.extract-version.outputs.VERSION }} - steps: - - uses: actions/checkout@v2 - - name: Update Rust - run: rustup update stable - - name: Dockerhub login - run: | - echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin - - name: Cross build lighthouse binary - run: | - cargo install cross - make build-aarch64-portable - - name: Move cross-built ARM binary into Docker scope - run: | - mkdir ./bin; - mv ./target/aarch64-unknown-linux-gnu/release/lighthouse ./bin; - # Install dependencies for emulation. Have to create a new builder to pick up emulation support. - - name: Build ARM64 dockerfile (with push) - run: | - docker run --privileged --rm tonistiigi/binfmt --install arm64 - docker buildx create --use --name cross-builder - docker buildx build \ - --platform=linux/arm64 \ - --file ./Dockerfile.cross . \ - --tag ${IMAGE_NAME}:${{ env.VERSION }}-arm64 \ - --push - build-docker-amd64: - runs-on: ubuntu-18.04 - needs: [extract-version] - env: - DOCKER_CLI_EXPERIMENTAL: enabled - VERSION: ${{ needs.extract-version.outputs.VERSION }} - steps: - - uses: actions/checkout@v2 - - name: Dockerhub login - run: | - echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin - - name: Build AMD64 dockerfile (with push) - run: | - docker build \ - --build-arg PORTABLE=true \ - --tag ${IMAGE_NAME}:${{ env.VERSION }}-amd64 \ - --file ./Dockerfile . - docker push ${IMAGE_NAME}:${{ env.VERSION }}-amd64 - build-docker-multiarch: - runs-on: ubuntu-18.04 - needs: [build-docker-arm64, build-docker-amd64, extract-version] - # We need to enable experimental docker features in order to use `docker manifest` - env: - DOCKER_CLI_EXPERIMENTAL: enabled - VERSION: ${{ needs.extract-version.outputs.VERSION }} - steps: - - name: Dockerhub login - run: | - echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin - - name: Create and push multiarch manifest - run: | - docker manifest create ${IMAGE_NAME}:${{ env.VERSION }} \ - --amend ${IMAGE_NAME}:${{ env.VERSION }}-arm64 \ - --amend ${IMAGE_NAME}:${{ env.VERSION }}-amd64; - docker manifest push ${IMAGE_NAME}:${{ env.VERSION }} build: name: Build Release strategy: diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index a4e49b1c26..8b590f4e6e 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -123,7 +123,9 @@ jobs: - name: Get latest version of stable Rust run: rustup update stable - name: Build the root Dockerfile - run: docker build . + run: docker build --build-arg FEATURES=portable -t lighthouse:local . + - name: Test the built image + run: docker run -t lighthouse:local lighthouse --version eth1-simulator-ubuntu: name: eth1-simulator-ubuntu runs-on: ubuntu-latest diff --git a/Dockerfile b/Dockerfile index 5ca8cbc964..76347e9bfe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ -FROM rust:1.56.1-bullseye AS builder +FROM rust:1.58.1-bullseye AS builder RUN apt-get update && apt-get -y upgrade && apt-get install -y cmake libclang-dev COPY . lighthouse -ARG PORTABLE -ENV PORTABLE $PORTABLE +ARG FEATURES +ENV FEATURES $FEATURES RUN cd lighthouse && make FROM ubuntu:latest diff --git a/Makefile b/Makefile index c372b9ef8b..f363854c32 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,6 @@ .PHONY: tests EF_TESTS = "testing/ef_tests" -BEACON_CHAIN_CRATE = "beacon_node/beacon_chain" -OP_POOL_CRATE = "beacon_node/operation_pool" STATE_TRANSITION_VECTORS = "testing/state_transition_vectors" GIT_TAG := $(shell git describe --tags --candidates 1) BIN_DIR = "bin" @@ -22,19 +20,11 @@ FORKS=phase0 altair # # Binaries will most likely be found in `./target/release` install: -ifeq ($(PORTABLE), true) - cargo install --path lighthouse --force --locked --features portable -else - cargo install --path lighthouse --force --locked -endif + cargo install --path lighthouse --force --locked --features "$(FEATURES)" # Builds the lcli binary in release (optimized). install-lcli: -ifeq ($(PORTABLE), true) - cargo install --path lcli --force --locked --features portable -else - cargo install --path lcli --force --locked -endif + cargo install --path lcli --force --locked --features "$(FEATURES)" # The following commands use `cross` to build a cross-compile. # @@ -50,13 +40,13 @@ endif # optimized CPU functions that may not be available on some systems. This # results in a more portable binary with ~20% slower BLS verification. build-x86_64: - cross build --release --manifest-path lighthouse/Cargo.toml --target x86_64-unknown-linux-gnu --features modern,gnosis + cross build --release --bin lighthouse --target x86_64-unknown-linux-gnu --features modern,gnosis build-x86_64-portable: - cross build --release --manifest-path lighthouse/Cargo.toml --target x86_64-unknown-linux-gnu --features portable,gnosis + cross build --release --bin lighthouse --target x86_64-unknown-linux-gnu --features portable,gnosis build-aarch64: - cross build --release --manifest-path lighthouse/Cargo.toml --target aarch64-unknown-linux-gnu --features gnosis + cross build --release --bin lighthouse --target aarch64-unknown-linux-gnu --features gnosis build-aarch64-portable: - cross build --release --manifest-path lighthouse/Cargo.toml --target aarch64-unknown-linux-gnu --features portable,gnosis + cross build --release --bin lighthouse --target aarch64-unknown-linux-gnu --features portable,gnosis # Create a `.tar.gz` containing a binary for a specific target. define tarball_release_binary @@ -102,21 +92,21 @@ check-benches: # Typechecks consensus code *without* allowing deprecated legacy arithmetic or metrics. check-consensus: - cargo check --manifest-path=consensus/state_processing/Cargo.toml --no-default-features + cargo check -p state_processing --no-default-features # Runs only the ef-test vectors. run-ef-tests: rm -rf $(EF_TESTS)/.accessed_file_log.txt - cargo test --release --manifest-path=$(EF_TESTS)/Cargo.toml --features "ef_tests" - cargo test --release --manifest-path=$(EF_TESTS)/Cargo.toml --features "ef_tests,fake_crypto" - cargo test --release --manifest-path=$(EF_TESTS)/Cargo.toml --features "ef_tests,milagro" + cargo test --release -p ef_tests --features "ef_tests" + cargo test --release -p ef_tests --features "ef_tests,fake_crypto" + cargo test --release -p ef_tests --features "ef_tests,milagro" ./$(EF_TESTS)/check_all_files_accessed.py $(EF_TESTS)/.accessed_file_log.txt $(EF_TESTS)/consensus-spec-tests # Run the tests in the `beacon_chain` crate for all known forks. test-beacon-chain: $(patsubst %,test-beacon-chain-%,$(FORKS)) test-beacon-chain-%: - env FORK_NAME=$* cargo test --release --features fork_from_env --manifest-path=$(BEACON_CHAIN_CRATE)/Cargo.toml + env FORK_NAME=$* cargo test --release --features fork_from_env -p beacon_chain # Run the tests in the `operation_pool` crate for all known forks. test-op-pool: $(patsubst %,test-op-pool-%,$(FORKS)) @@ -124,7 +114,7 @@ test-op-pool: $(patsubst %,test-op-pool-%,$(FORKS)) test-op-pool-%: env FORK_NAME=$* cargo test --release \ --features 'beacon_chain/fork_from_env'\ - --manifest-path=$(OP_POOL_CRATE)/Cargo.toml + -p operation_pool # Runs only the tests/state_transition_vectors tests. run-state-transition-tests: @@ -144,11 +134,11 @@ test-full: cargo-fmt test-release test-debug test-ef # 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 -- \ - -D clippy::fn_to_numeric_cast_any \ - -D warnings \ - -A clippy::from-over-into \ - -A clippy::upper-case-acronyms \ - -A clippy::vec-init-then-push + -D clippy::fn_to_numeric_cast_any \ + -D warnings \ + -A clippy::from-over-into \ + -A clippy::upper-case-acronyms \ + -A clippy::vec-init-then-push # Runs the makefile in the `ef_tests` repo. # diff --git a/book/src/cross-compiling.md b/book/src/cross-compiling.md index 7dee3320e9..9b458078e2 100644 --- a/book/src/cross-compiling.md +++ b/book/src/cross-compiling.md @@ -19,15 +19,16 @@ project. The `Makefile` in the project contains four targets for cross-compiling: - `build-x86_64`: builds an optimized version for x86_64 processors (suitable for most users). - Supports Intel Broadwell (2014) and newer, and AMD Ryzen (2017) and newer. - `build-x86_64-portable`: builds a version for x86_64 processors which avoids using some modern CPU - instructions that are incompatible with older CPUs. Suitable for pre-Broadwell/Ryzen CPUs. -- `build-aarch64`: builds an optimized version for 64-bit ARM processors - (suitable for Raspberry Pi 4). + instructions that are incompatible with older CPUs. +- `build-aarch64`: builds an optimized version for 64-bit ARM processors (suitable for Raspberry Pi 4). - `build-aarch64-portable`: builds a version for 64-bit ARM processors which avoids using some modern CPU instructions. In practice, very few ARM processors lack the instructions necessary to run the faster non-portable build. +For more information about optimized vs portable builds see +[Portability](./installation-binaries.md#portability). + ### Example ```bash diff --git a/book/src/docker.md b/book/src/docker.md index 965dd7816f..eebbd5dde2 100644 --- a/book/src/docker.md +++ b/book/src/docker.md @@ -1,20 +1,17 @@ # Docker Guide -This repository has a `Dockerfile` in the root which builds an image with the -`lighthouse` binary installed. A pre-built image is available on Docker Hub. +There are two ways to obtain a Lighthouse Docker image: -## Obtaining the Docker image +1. [Docker Hub](#docker-hub), or +2. By [building a Docker image from source](#building-the-docker-image). -There are two ways to obtain the docker image, either via Docker Hub or -building the image from source. Once you have obtained the docker image via one -of these methods, proceed to [Using the Docker image](#using-the-docker-image). +Once you have obtained the docker image via one of these methods, proceed to [Using the Docker +image](#using-the-docker-image). -### Docker Hub +## Docker Hub -Lighthouse maintains the -[sigp/lighthouse](https://hub.docker.com/repository/docker/sigp/lighthouse/) -Docker Hub repository which provides an easy way to run Lighthouse without -building the image yourself. +Lighthouse maintains the [sigp/lighthouse][docker_hub] Docker Hub repository which provides an easy +way to run Lighthouse without building the image yourself. Obtain the latest image with: @@ -28,26 +25,69 @@ Download and test the image with: $ docker run sigp/lighthouse lighthouse --version ``` -If you can see the latest [Lighthouse -release](https://github.com/sigp/lighthouse/releases) version (see example -below), then you've -successfully installed Lighthouse via Docker. +If you can see the latest [Lighthouse release](https://github.com/sigp/lighthouse/releases) version +(see example below), then you've successfully installed Lighthouse via Docker. -#### Example Version Output +> Pro tip: try the `latest-modern` image for a 20-30% speed-up! See [Available Docker +> Images](#available-docker-images) below. + +### Example Version Output ``` Lighthouse vx.x.xx-xxxxxxxxx BLS Library: xxxx-xxxxxxx ``` -> Note: when you're running the Docker Hub image you're relying upon a -> pre-built binary instead of building from source. +### Available Docker Images -> Note: due to the Docker Hub image being compiled to work on arbitrary machines, it isn't as highly -> optimized as an image built from source. We're working to improve this, but for now if you want -> the absolute best performance, please build the image yourself. +There are several images available on Docker Hub. -### Building the Docker Image +Most users should use the `latest-modern` tag, which corresponds to the latest stable release of +Lighthouse with optimizations enabled. If you are running on older hardware then the default +`latest` image bundles a _portable_ version of Lighthouse which is slower but with better hardware +compatibility (see [Portability](./installation-binaries.md#portability)). + +To install a specific tag (in this case `latest-modern`) add the tag name to your `docker` commands +like so: + +``` +$ docker pull sigp/lighthouse:latest-modern +``` + +Image tags follow this format: + +``` +${version}${arch}${stability}${modernity} +``` + +The `version` is: + +* `vX.Y.Z` for a tagged Lighthouse release, e.g. `v2.1.1` +* `latest` for the `stable` branch (latest release) or `unstable` branch + +The `stability` is: + +* `-unstable` for the `unstable` branch +* empty for a tagged release or the `stable` branch + +The `arch` is: + +* `-amd64` for x86_64, e.g. Intel, AMD +* `-arm64` for aarch64, e.g. Rasperry Pi 4 +* empty for a multi-arch image (works on either `amd64` or `arm64` platforms) + +The `modernity` is: + +* `-modern` for optimized builds +* empty for a `portable` unoptimized build + +Examples: + +* `latest-unstable-modern`: most recent `unstable` build for all modern CPUs (x86_64 or ARM) +* `latest-amd64`: most recent Lighthouse release for older x86_64 CPUs +* `latest-amd64-unstable`: most recent `unstable` build for older x86_64 CPUs + +## Building the Docker Image To build the image from source, navigate to the root of the repository and run: @@ -103,3 +143,5 @@ If you use the `--http` flag you may also want to expose the HTTP port with `-p ```bash $ docker run -p 9000:9000 -p 127.0.0.1:5052:5052 sigp/lighthouse lighthouse beacon --http --http-address 0.0.0.0 ``` + +[docker_hub]: https://hub.docker.com/repository/docker/sigp/lighthouse/ diff --git a/book/src/installation-binaries.md b/book/src/installation-binaries.md index 4f092c1e29..7a5aad32bf 100644 --- a/book/src/installation-binaries.md +++ b/book/src/installation-binaries.md @@ -20,13 +20,13 @@ Additionally there is also a `-portable` suffix which indicates if the `portable - Without `portable`: uses modern CPU instructions to provide the fastest signature verification times (may cause `Illegal instruction` error on older CPUs) - With `portable`: approx. 20% slower, but should work on all modern 64-bit processors. +For details, see [Portability](#portability). + ## Usage Each binary is contained in a `.tar.gz` archive. For this example, lets assume the user needs a portable `x86_64` binary. -> Whilst this example uses `v0.2.13` we recommend always using the latest release. - ### Steps 1. Go to the [Releases](https://github.com/sigp/lighthouse/releases) page and @@ -41,6 +41,19 @@ a portable `x86_64` binary. > Windows users will need to execute the commands in Step 3 from PowerShell. +## Portability + +Portable builds of Lighthouse are designed to run on the widest range of hardware possible, but +sacrifice the ability to make use of modern CPU instructions. + +If you have a modern CPU then you should try running a non-portable build to get a 20-30% speed up. + +* For **x86_64**, any CPU supporting the [ADX](https://en.wikipedia.org/wiki/Intel_ADX) instruction set +extension is compatible with the optimized build. This includes Intel Broadwell (2014) +and newer, and AMD Ryzen (2017) and newer. +* For **ARMv8**, most CPUs are compatible with the optimized build, including the Cortex-A72 used by +the Raspberry Pi 4. + ## Troubleshooting If you get a SIGILL (exit code 132), then your CPU is incompatible with the optimized build diff --git a/lcli/Dockerfile b/lcli/Dockerfile index 5a4177ead9..27ec8cc86c 100644 --- a/lcli/Dockerfile +++ b/lcli/Dockerfile @@ -1,7 +1,7 @@ # `lcli` requires the full project to be in scope, so this should be built either: # - from the `lighthouse` dir with the command: `docker build -f ./lcli/Dockerflie .` # - from the current directory with the command: `docker build -f ./Dockerfile ../` -FROM rust:1.56.1-bullseye AS builder +FROM rust:1.58.1-bullseye AS builder RUN apt-get update && apt-get -y upgrade && apt-get install -y cmake COPY . lighthouse ARG PORTABLE diff --git a/testing/antithesis/Dockerfile.libvoidstar b/testing/antithesis/Dockerfile.libvoidstar index d9084af348..61b95397d7 100644 --- a/testing/antithesis/Dockerfile.libvoidstar +++ b/testing/antithesis/Dockerfile.libvoidstar @@ -1,4 +1,4 @@ -FROM rust:1.56.1-bullseye AS builder +FROM rust:1.58.1-bullseye AS builder RUN apt-get update && apt-get -y upgrade && apt-get install -y cmake libclang-dev COPY . lighthouse From 736457b562f0f4aabe775d8f79ec58e50a576647 Mon Sep 17 00:00:00 2001 From: Akihito Nakano Date: Mon, 31 Jan 2022 22:55:04 +0000 Subject: [PATCH 18/23] Run setup.sh foreground in order to avoid timing issues (#2970) ## Issue Addressed Resolves https://github.com/sigp/lighthouse/pull/2919#issuecomment-1022892369 ## Proposed Changes - Run setup.sh foreground in order to avoid timing issues --- scripts/local_testnet/start_local_testnet.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/local_testnet/start_local_testnet.sh b/scripts/local_testnet/start_local_testnet.sh index 7126e4c5dc..69d55660fa 100755 --- a/scripts/local_testnet/start_local_testnet.sh +++ b/scripts/local_testnet/start_local_testnet.sh @@ -93,9 +93,9 @@ execute_command_add_PID() { execute_command_add_PID ganache_test_node.log ./ganache_test_node.sh sleeping 10 -# Delay to get data setup -execute_command setup.log ./setup.sh -sleeping 15 +# Setup data +echo "executing: ./setup.sh >> $LOG_DIR/setup.log" +./setup.sh >> $LOG_DIR/setup.log 2>&1 # Delay to let boot_enr.yaml to be created execute_command_add_PID bootnode.log ./bootnode.sh From 5d26050e97c70445a0e7448c572559fc0f39a94e Mon Sep 17 00:00:00 2001 From: Akihito Nakano Date: Mon, 31 Jan 2022 22:55:06 +0000 Subject: [PATCH 19/23] local testnet: Fix an error on startup (#2973) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Issue Addressed Resolves https://github.com/sigp/lighthouse/issues/2763#issuecomment-1024858187 ## Proposed Changes - Skip if the line is blank. 👌 --- scripts/local_testnet/kill_processes.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/local_testnet/kill_processes.sh b/scripts/local_testnet/kill_processes.sh index 4f52a5f256..be6b7f3d66 100755 --- a/scripts/local_testnet/kill_processes.sh +++ b/scripts/local_testnet/kill_processes.sh @@ -8,6 +8,9 @@ set -Eeuo pipefail if [ -f "$1" ]; then while read pid do + # handle the case of blank lines + [[ -n "$pid" ]] || continue + echo killing $pid kill $pid done < $1 From 286996b0902187a0a5dbc8113e9573ff7adac71d Mon Sep 17 00:00:00 2001 From: Mac L Date: Mon, 31 Jan 2022 22:55:07 +0000 Subject: [PATCH 20/23] Fix small typo in error log (#2975) ## Proposed Changes Fixes a small typo I came across. --- .../lighthouse_network/src/peer_manager/network_behaviour.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs b/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs index d194deffd4..b787c421cf 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs @@ -122,7 +122,7 @@ impl NetworkBehaviour for PeerManager { // TODO: directly emit the ban event? BanResult::BadScore => { // This is a faulty state - error!(self.log, "Connecteded to a banned peer, re-banning"; "peer_id" => %peer_id); + error!(self.log, "Connected to a banned peer, re-banning"; "peer_id" => %peer_id); // Reban the peer self.goodbye_peer(peer_id, GoodbyeReason::Banned, ReportSource::PeerManager); return; From a6da87066b7681cf75691f9f4b4eacd8655082ac Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 1 Feb 2022 01:04:22 +0000 Subject: [PATCH 21/23] Add strict penalties const bool (#2976) ## Issue Addressed NA ## Proposed Changes Adds `STRICT_LATE_MESSAGE_PENALTIES: bool` which allows for toggling penalties for late sync/attn messages. `STRICT_LATE_MESSAGE_PENALTIES` is set to `false`, since we're seeing a lot of late messages on the network which are causing peer drops. We can toggle the bool during testing to try and figure out what/who is the cause of these late messages. In effect, this PR *relaxes* peer downscoring for late attns and sync committee messages. ## Additional Info - ~~Blocked on #2974~~ --- .../network/src/beacon_processor/worker/gossip_methods.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 132bed1b72..d6549db9f3 100644 --- a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs +++ b/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs @@ -30,6 +30,10 @@ use super::{ }; use crate::beacon_processor::DuplicateCache; +/// Set to `true` to introduce stricter penalties for peers who send some types of late consensus +/// messages. +const STRICT_LATE_MESSAGE_PENALTIES: bool = false; + /// An attestation that has been validated by the `BeaconChain`. /// /// Since this struct implements `beacon_chain::VerifiedAttestation`, it would be a logic error to @@ -1311,7 +1315,7 @@ impl Worker { // Only penalize the peer if it would have been invalid at the moment we received // it. - if hindsight_verification.is_err() { + if STRICT_LATE_MESSAGE_PENALTIES && hindsight_verification.is_err() { self.gossip_penalize_peer( peer_id, PeerAction::LowToleranceError, @@ -1832,7 +1836,7 @@ impl Worker { }; // Penalize the peer if the message was more than one slot late - if excessively_late && invalid_in_hindsight() { + if STRICT_LATE_MESSAGE_PENALTIES && excessively_late && invalid_in_hindsight() { self.gossip_penalize_peer( peer_id, PeerAction::HighToleranceError, From fc37d51e1020d20cf642ceaba4034e622e243bb8 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 1 Feb 2022 01:04:24 +0000 Subject: [PATCH 22/23] Add checks to prevent fwding old messages (#2978) ## Issue Addressed NA ## Proposed Changes Checks to see if attestations or sync messages are still valid before "accepting" them for propagation. ## Additional Info NA --- .../beacon_processor/worker/gossip_methods.rs | 74 ++++++++++++++++--- 1 file changed, 62 insertions(+), 12 deletions(-) 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 d6549db9f3..72cb3a7ee1 100644 --- a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs +++ b/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs @@ -350,9 +350,12 @@ impl Worker { &self.chain.slot_clock, ); - // Indicate to the `Network` service that this message is valid and can be - // propagated on the gossip network. - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept); + // If the attestation is still timely, propagate it. + self.propagate_attestation_if_timely( + verified_attestation.attestation(), + message_id, + peer_id, + ); if !should_import { return; @@ -543,9 +546,12 @@ impl Worker { let aggregate = &verified_aggregate.signed_aggregate; let indexed_attestation = &verified_aggregate.indexed_attestation; - // Indicate to the `Network` service that this message is valid and can be - // propagated on the gossip network. - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept); + // If the attestation is still timely, propagate it. + self.propagate_attestation_if_timely( + verified_aggregate.attestation(), + message_id, + peer_id, + ); // Register the attestation with any monitored validators. self.chain @@ -1169,9 +1175,8 @@ impl Worker { } }; - // Indicate to the `Network` service that this message is valid and can be - // propagated on the gossip network. - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept); + // If the message is still timely, propagate it. + self.propagate_sync_message_if_timely(message_slot, message_id, peer_id); // Register the sync signature with any monitored validators. self.chain @@ -1233,9 +1238,8 @@ impl Worker { } }; - // Indicate to the `Network` service that this message is valid and can be - // propagated on the gossip network. - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept); + // If the message is still timely, propagate it. + self.propagate_sync_message_if_timely(contribution_slot, message_id, peer_id); self.chain .validator_monitor @@ -2100,4 +2104,50 @@ impl Worker { "type" => ?message_type, ); } + + /// Propagate (accept) if `is_timely == true`, otherwise ignore. + fn propagate_if_timely(&self, is_timely: bool, message_id: MessageId, peer_id: PeerId) { + if is_timely { + // The message is still relevant, propagate. + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept); + } else { + // The message is not relevant, ignore. It might be that this message became irrelevant + // during the time it took to process it, or it was received invalid. + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); + } + } + + /// If an attestation (agg. or unagg.) is still valid with respect to the current time (i.e., + /// timely), propagate it on gossip. Otherwise, ignore it. + fn propagate_attestation_if_timely( + &self, + attestation: &Attestation, + message_id: MessageId, + peer_id: PeerId, + ) { + let is_timely = attestation_verification::verify_propagation_slot_range( + &self.chain.slot_clock, + attestation, + ) + .is_ok(); + + self.propagate_if_timely(is_timely, message_id, peer_id) + } + + /// If a sync committee signature or sync committee contribution is still valid with respect to + /// the current time (i.e., timely), propagate it on gossip. Otherwise, ignore it. + fn propagate_sync_message_if_timely( + &self, + sync_message_slot: Slot, + message_id: MessageId, + peer_id: PeerId, + ) { + let is_timely = self + .chain + .slot_clock + .now() + .map_or(false, |current_slot| sync_message_slot == current_slot); + + self.propagate_if_timely(is_timely, message_id, peer_id) + } } From 0177b9286edfdeb2782f74fc7fb1392b6459bfaa Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 1 Feb 2022 23:53:53 +0000 Subject: [PATCH 23/23] v2.1.2 (#2980) ## Issue Addressed NA ## Proposed Changes - Bump version to `v2.1.2` - Run `cargo update` ## Additional Info NA --- Cargo.lock | 209 +++++++++++++++------------ beacon_node/Cargo.toml | 2 +- boot_node/Cargo.toml | 2 +- common/lighthouse_version/src/lib.rs | 2 +- lcli/Cargo.toml | 2 +- lighthouse/Cargo.toml | 2 +- 6 files changed, 118 insertions(+), 101 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 826c01b09d..bcea45d619 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,9 +141,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.52" +version = "1.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84450d0b4a8bd1ba4144ce8ce718fbc5d071358b1e5384bace6536b3d1f2d5b3" +checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0" [[package]] name = "arbitrary" @@ -253,9 +253,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "backtrace" -version = "0.3.63" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321629d8ba6513061f26707241fa9bc89524ff1cd7a915a97ef0c62c666ce1b6" +checksum = "5e121dee8023ce33ab248d9ce1493df03c3b38a659b240096fcbd7048ff9c31f" dependencies = [ "addr2line", "cc", @@ -266,6 +266,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + [[package]] name = "base64" version = "0.12.3" @@ -331,7 +337,7 @@ dependencies = [ [[package]] name = "beacon_node" -version = "2.1.1" +version = "2.1.2" dependencies = [ "beacon_chain", "clap", @@ -497,7 +503,7 @@ dependencies = [ [[package]] name = "boot_node" -version = "2.1.1" +version = "2.1.2" dependencies = [ "beacon_node", "clap", @@ -760,7 +766,7 @@ dependencies = [ "slot_clock", "store", "task_executor", - "time 0.3.5", + "time 0.3.7", "timer", "tokio", "toml", @@ -855,9 +861,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "738c290dfaea84fc1ca15ad9c168d083b05a714e1efddd8edaab678dc28d2836" +checksum = "a2209c310e29876f7f0b2721e7e26b84aff178aa3da5d091f9bfbf47669e60e3" dependencies = [ "cfg-if", ] @@ -1272,7 +1278,7 @@ dependencies = [ "tokio-util", "tracing", "tracing-subscriber", - "uint 0.9.1", + "uint 0.9.2", "zeroize", ] @@ -1301,7 +1307,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0d69ae62e0ce582d56380743515fefaf1a8c70cec685d9677636d7e30ae9dc9" dependencies = [ "der 0.5.1", - "elliptic-curve 0.11.6", + "elliptic-curve 0.11.12", "rfc6979", "signature", ] @@ -1383,10 +1389,11 @@ dependencies = [ [[package]] name = "elliptic-curve" -version = "0.11.6" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "decb3a27ea454a5f23f96eb182af0671c12694d64ecc33dada74edd1301f6cfc" +checksum = "25b477563c2bfed38a3b7a60964c49e058b2510ad3f12ba3483fd8f62c2306d6" dependencies = [ + "base16ct", "crypto-bigint", "der 0.5.1", "ff 0.11.0", @@ -1765,7 +1772,7 @@ dependencies = [ "serde_json", "sha3", "thiserror", - "uint 0.9.1", + "uint 0.9.2", ] [[package]] @@ -1819,7 +1826,7 @@ dependencies = [ "impl-rlp 0.3.0", "impl-serde", "primitive-types 0.9.1", - "uint 0.9.1", + "uint 0.9.2", ] [[package]] @@ -1833,7 +1840,7 @@ dependencies = [ "impl-rlp 0.3.0", "impl-serde", "primitive-types 0.10.1", - "uint 0.9.1", + "uint 0.9.2", ] [[package]] @@ -1895,9 +1902,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "779d043b6a0b90cc4c0ed7ee380a6504394cee7efd7db050e3774eee387324b2" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" dependencies = [ "instant", ] @@ -2279,9 +2286,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9de88456263e249e241fcd211d3954e2c9b0ef7ccfc235a444eb367cae3689" +checksum = "d9f1f717ddc7b2ba36df7e871fd88db79326551d3d6f1fc406fbfd28b582ff8e" dependencies = [ "bytes", "fnv", @@ -2331,9 +2338,9 @@ dependencies = [ [[package]] name = "headers" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c4eb0471fcb85846d8b0690695ef354f9afb11cb03cac2e1d7c9253351afb0" +checksum = "c84c647447a07ca16f5fbd05b633e535cc41a08d2d74ab1e08648df53be9cb89" dependencies = [ "base64 0.13.0", "bitflags", @@ -2545,7 +2552,7 @@ dependencies = [ "httpdate", "itoa 0.4.8", "pin-project-lite 0.2.8", - "socket2 0.4.2", + "socket2 0.4.4", "tokio", "tower-service", "tracing", @@ -2769,9 +2776,9 @@ checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" [[package]] name = "js-sys" -version = "0.3.55" +version = "0.3.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" dependencies = [ "wasm-bindgen", ] @@ -2826,7 +2833,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lcli" -version = "2.1.1" +version = "2.1.2" dependencies = [ "account_utils", "bls", @@ -2882,15 +2889,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.112" +version = "0.2.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" +checksum = "565dbd88872dbe4cc8a46e527f26483c1d1f7afa6b884a3bd6cd893d4f98da74" [[package]] name = "libflate" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16364af76ebb39b5869bb32c81fa93573267cd8c62bb3474e28d78fac3fb141e" +checksum = "d2d57e534717ac3e0b8dc459fe338bdfb4e29d7eea8fd0926ba649ddd3f4765f" dependencies = [ "adler32", "crc32fast", @@ -2908,9 +2915,9 @@ dependencies = [ [[package]] name = "libloading" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afe203d669ec979b7128619bae5a63b7b42e9203c1b29146079ee05e2f604b52" +checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" dependencies = [ "cfg-if", "winapi", @@ -3198,7 +3205,7 @@ dependencies = [ "libc", "libp2p-core 0.31.0", "log", - "socket2 0.4.2", + "socket2 0.4.4", "tokio", ] @@ -3351,7 +3358,7 @@ dependencies = [ [[package]] name = "lighthouse" -version = "2.1.1" +version = "2.1.2" dependencies = [ "account_manager", "account_utils", @@ -3454,9 +3461,9 @@ checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" [[package]] name = "lock_api" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" dependencies = [ "scopeguard", ] @@ -4001,6 +4008,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ba99ba6393e2c3734791401b66902d981cb03bf190af674ca69949b6d5fb15" +dependencies = [ + "libc", +] + [[package]] name = "object" version = "0.27.1" @@ -4125,12 +4141,12 @@ dependencies = [ [[package]] name = "p256" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0e0c5310031b5d4528ac6534bccc1446c289ac45c47b277d5aa91089c5f74fa" +checksum = "19736d80675fbe9fe33426268150b951a3fb8f5cfca2a23a17c85ef3adb24e3b" dependencies = [ "ecdsa 0.13.4", - "elliptic-curve 0.11.6", + "elliptic-curve 0.11.12", "sec1", "sha2 0.9.9", ] @@ -4413,7 +4429,7 @@ dependencies = [ "impl-codec 0.5.1", "impl-rlp 0.3.0", "impl-serde", - "uint 0.9.1", + "uint 0.9.2", ] [[package]] @@ -4426,7 +4442,7 @@ dependencies = [ "impl-codec 0.5.1", "impl-rlp 0.3.0", "impl-serde", - "uint 0.9.1", + "uint 0.9.2", ] [[package]] @@ -4572,9 +4588,9 @@ dependencies = [ [[package]] name = "protobuf" -version = "2.25.2" +version = "2.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c327e191621a2158159df97cdbc2e7074bb4e940275e35abf38eb3d2595754" +checksum = "9d613b4fd96c0182e187734b4f8fc5cbc8c940bbf781819f7a52d42dc5922d25" [[package]] name = "psutil" @@ -4636,9 +4652,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" dependencies = [ "proc-macro2", ] @@ -4936,9 +4952,9 @@ dependencies = [ [[package]] name = "rle-decode-fast" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cabe4fa914dec5870285fa7f71f602645da47c486e68486d2b4ceb4a343e90ac" +checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" [[package]] name = "rlp" @@ -5213,9 +5229,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.4.2" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" +checksum = "a57321bf8bc2362081b2599912d2961fe899c0efadf1b4b2f8d48b3e253bb96c" dependencies = [ "core-foundation-sys", "libc", @@ -5270,9 +5286,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.133" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" dependencies = [ "serde_derive", ] @@ -5299,9 +5315,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.133" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" dependencies = [ "proc-macro2", "quote", @@ -5310,9 +5326,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.74" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2bb9cd061c5865d345bb02ca49fcef1391741b672b54a0bf7b679badec3142" +checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085" dependencies = [ "itoa 1.0.1", "ryu", @@ -5332,12 +5348,12 @@ dependencies = [ [[package]] name = "serde_urlencoded" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 0.4.8", + "itoa 1.0.1", "ryu", "serde", ] @@ -5546,9 +5562,9 @@ dependencies = [ [[package]] name = "slog-json" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e9b96fb6b5e80e371423b4aca6656eb537661ce8f82c2697e619f8ca85d043" +checksum = "0f7f7a952ce80fca9da17bf0a53895d11f8aa1ba063668ca53fc72e7869329e9" dependencies = [ "chrono", "serde", @@ -5678,9 +5694,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" dependencies = [ "libc", "winapi", @@ -5866,9 +5882,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.85" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" dependencies = [ "proc-macro2", "quote", @@ -6033,11 +6049,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41effe7cfa8af36f439fac33861b66b049edc6f9a32331e2312660529c1c24ad" +checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d" dependencies = [ "libc", + "num_threads", ] [[package]] @@ -6115,9 +6132,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.15.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838" +checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a" dependencies = [ "bytes", "libc", @@ -6299,9 +6316,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d81bfa81424cc98cb034b837c985b7a290f592e5b4322f353f94a0ab0f9f594" +checksum = "5312f325fe3588e277415f5a6cca1f4ccad0f248c4cd5a4bd33032d7286abc22" dependencies = [ "ansi_term", "lazy_static", @@ -6538,9 +6555,9 @@ dependencies = [ [[package]] name = "uint" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6470ab50f482bde894a037a57064480a246dbfdd5960bd65a44824693f08da5f" +checksum = "1b1b413ebfe8c2c74a69ff124699dd156a7fa41cb1d09ba6df94aa2f2b0a4a3a" dependencies = [ "arbitrary", "byteorder", @@ -6867,9 +6884,9 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "wasm-bindgen" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -6877,9 +6894,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" dependencies = [ "bumpalo", "lazy_static", @@ -6892,9 +6909,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39" +checksum = "2eb6ec270a31b1d3c7e266b999739109abce8b6c87e4b31fcfcd788b65267395" dependencies = [ "cfg-if", "js-sys", @@ -6904,9 +6921,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6914,9 +6931,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" dependencies = [ "proc-macro2", "quote", @@ -6927,15 +6944,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" +checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" [[package]] name = "wasm-bindgen-test" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96f1aa7971fdf61ef0f353602102dbea75a56e225ed036c1e3740564b91e6b7e" +checksum = "45c8d417d87eefa0087e62e3c75ad086be39433449e2961add9a5d9ce5acc2f1" dependencies = [ "console_error_panic_hook", "js-sys", @@ -6947,9 +6964,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6006f79628dfeb96a86d4db51fbf1344cd7fd8408f06fc9aa3c84913a4789688" +checksum = "d0e560d44db5e73b69a9757a15512fe7e1ef93ed2061c928871a4025798293dd" dependencies = [ "proc-macro2", "quote", @@ -6957,9 +6974,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.55" +version = "0.3.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb" dependencies = [ "js-sys", "wasm-bindgen", @@ -7066,9 +7083,9 @@ dependencies = [ [[package]] name = "which" -version = "4.2.2" +version = "4.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea187a8ef279bc014ec368c27a920da2024d2a711109bfbe3440585d5cf27ad9" +checksum = "2a5a7e487e921cf220206864a94a89b6c6905bfc19f1057fa26a4cb360e5c1d2" dependencies = [ "either", "lazy_static", @@ -7199,18 +7216,18 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.4.3" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619" +checksum = "7c88870063c39ee00ec285a2f8d6a966e5b6fb2becc4e8dac77ed0d370ed6006" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.2.2" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65f1a51723ec88c66d5d1fe80c841f17f63587d6691901d66be9bec6c3b51f73" +checksum = "81e8f13fef10b63c06356d65d416b070798ddabcadc10d3ece0c5be9b3c7eddb" dependencies = [ "proc-macro2", "quote", diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index c8cd5152af..c0bc17e118 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "beacon_node" -version = "2.1.1" +version = "2.1.2" authors = ["Paul Hauner ", "Age Manning "] edition = "2018" diff --git a/common/lighthouse_version/src/lib.rs b/common/lighthouse_version/src/lib.rs index a66ff66e5c..3c6b2459ec 100644 --- a/common/lighthouse_version/src/lib.rs +++ b/common/lighthouse_version/src/lib.rs @@ -16,7 +16,7 @@ pub const VERSION: &str = git_version!( // NOTE: using --match instead of --exclude for compatibility with old Git "--match=thiswillnevermatchlol" ], - prefix = "Lighthouse/v2.1.1-", + prefix = "Lighthouse/v2.1.2-", fallback = "unknown" ); diff --git a/lcli/Cargo.toml b/lcli/Cargo.toml index 2b9541de3f..339dfee5b1 100644 --- a/lcli/Cargo.toml +++ b/lcli/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "lcli" description = "Lighthouse CLI (modeled after zcli)" -version = "2.1.1" +version = "2.1.2" authors = ["Paul Hauner "] edition = "2018" diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 2429b6606d..22b2a7645e 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lighthouse" -version = "2.1.1" +version = "2.1.2" authors = ["Sigma Prime "] edition = "2018" autotests = false