diff --git a/README.md b/README.md index 1c4a802bc..20f427f4f 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ An open-source Ethereum 2.0 client, written in Rust and maintained by Sigma Prim [Swagger Badge]: https://img.shields.io/badge/Open%20API-0.2.0-success [Swagger Link]: https://app.swaggerhub.com/apis-docs/spble/lighthouse_rest_api/0.2.0 +[Documentation](http://lighthouse-book.sigmaprime.io/) + ![terminalize](https://i.postimg.cc/kG11dpCW/lighthouse-cli-png.gif) ## Overview diff --git a/account_manager/src/cli.rs b/account_manager/src/cli.rs index d56898ebd..07685fb70 100644 --- a/account_manager/src/cli.rs +++ b/account_manager/src/cli.rs @@ -16,8 +16,8 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .long("deposit-value") .value_name("GWEI") .takes_value(true) - .default_value("32000000000") - .help("The deposit amount in Gwei (not Wei). Default is 32 ETH."), + .default_value("3200000000") + .help("The deposit amount in Gwei (not Wei). Default is 3.2 ETH."), ) .arg( Arg::with_name("send-deposits") diff --git a/beacon_node/eth1/src/service.rs b/beacon_node/eth1/src/service.rs index efa402884..80fd6c479 100644 --- a/beacon_node/eth1/src/service.rs +++ b/beacon_node/eth1/src/service.rs @@ -306,8 +306,7 @@ impl Service { let log = self.log.clone(); let update_interval = Duration::from_millis(self.config().auto_update_interval_millis); - loop_fn((), move |()| { - let exit = exit.clone(); + let loop_future = loop_fn((), move |()| { let service = service.clone(); let log_a = log.clone(); let log_b = log.clone(); @@ -344,16 +343,11 @@ impl Service { ); } // Do not break the loop if there is an timer failure. - Ok(()) + Ok(Loop::Continue(())) }) - .map(move |_| { - if exit.is_live() { - Loop::Continue(()) - } else { - Loop::Break(()) - } - }) - }) + }); + + exit.until(loop_future).map(|_: Option<()>| ()) } /// Contacts the remote eth1 node and attempts to import deposit logs up to the configured diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 7c474ca1f..de0f7ff29 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -6,7 +6,9 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .visible_aliases(&["b", "bn", "beacon"]) .version(crate_version!()) .author("Sigma Prime ") - .about("Eth 2.0 Client") + .about("The primary component which connects to the Ethereum 2.0 P2P network and \ + downloads, verifies and stores blocks. Provides a HTTP API for querying \ + the beacon chain and publishing messages to the network.") /* * Configuration directory locations. */ @@ -187,7 +189,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .value_name("HTTP-ENDPOINT") .help("Specifies the server for a web3 connection to the Eth1 chain.") .takes_value(true) - .default_value("http://localhost:8545") + .default_value("https://goerli.public.sigp.io") ) .arg( Arg::with_name("slots-per-restore-point") diff --git a/book/README.md b/book/README.md index 3a5396283..45448c4ef 100644 --- a/book/README.md +++ b/book/README.md @@ -3,7 +3,7 @@ Contains an [mdBook](https://github.com/rust-lang-nursery/mdBook) that serves as the primary source of Lighthouse user documentation. -The book is hosted at [lighthouse-book.sigmaprime.io](http://lighthouse-book.sigmaprime.io). +The book is hosted at [lighthouse-book.sigmaprime.io](http://lighthouse-book.sigmaprime.io).i ## Usage @@ -14,4 +14,4 @@ best source of information for building the book. 1. Install mdBook: `$ cargo install mdbook` 1. Build the book, open it in a browser and build after file changes: `$ mdbook - watch --open` + serve --open` diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 8882bead7..8ca5d49f6 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -1,7 +1,8 @@ # Summary * [Introduction](./intro.md) -* [Installation](./installation.md) +* [Become a Validator](./become-a-validator.md) +* [Introduction](./intro.md) * [Docker](./docker.md) * [CLI](./cli.md) * [Testnets](./testnets.md) diff --git a/book/src/become-a-validator.md b/book/src/become-a-validator.md new file mode 100644 index 000000000..b12d2bea4 --- /dev/null +++ b/book/src/become-a-validator.md @@ -0,0 +1,179 @@ +# Become an Ethereum 2.0 Validator* + +_* Testnet validator_ + +Running Lighthouse validator is easy if you're familiar with the terminal. It +runs on Linux, MacOS and Windows. + +Before you start, you'll need [Metamask](https://metamask.io/) and 3.2 gETH +(Goerli ETH). We recommend the [mudit.blog +faucet](https://faucet.goerli.mudit.blog/) for those familiar with Goerli, or +[goerli.net](https://goerli.net/) for an overview of the testnet. + +### 1. Download and install Lighthouse + +If you already have Rust installed, you can install Lighthouse with the +following three commands: + +- `$ git clone https://github.com/sigp/lighthouse.git` +- `$ cd lighthouse` +- `$ make` + +You've completed this step when you can run `$ lighthouse --help` and see the +help menu. + +> - If you're not familiar with Rust or you'd like more detailed instructions, see +> the [Installation Guide](./installation.md). +> - The [Docker Guide](./docker.md) is great if you have Docker installed and would +> like to avoid installing Rust. + +### 2. Start your Beacon Node + +The beacon node is the core component of Eth2, it connects to other peers over +the Internet and maintains a view of the chain. + +Start your beacon node with: + +```bash +$ lighthouse beacon --eth1 --http +``` + +You're beacon node has started syncing when you see the following (truncated) +log: + +``` +Dec 09 12:57:18.026 INFO Syncing distance: 16837 slots (2 days 8 hrs), ... +``` + +It has finished syncing once you see the following (truncated) log: + +``` +Dec 09 12:27:06.010 INFO Synced slot: 16835, ... +``` + +> - The `--http` flag enables the HTTP API for the validator client. +> - The `--eth1` flag tells the beacon node that it should sync with an Ethereum +> 1 node (e.g., Geth). This is only required if you wish to run a validator. +> - We are hosting a public Goerli archive node and have set this as the +> default, but you can specify your own Eth1 node using the `--eth1-endpoint` +> flag. Presently we require the node to be a full archive node, but we're +> working to [fix](https://github.com/sigp/lighthouse/issues/637) this. + +### 3. Generate your validator key + +Generate new validator BLS keypairs using: + +```shell +$ lighthouse account validator new random +``` + + +You've completed this step when you see the equivalent line: + +``` +Dec 02 21:42:01.337 INFO Generated validator directories count: 1, base_path: "/home/karl/.lighthouse/validators" +``` + +> - This will generate a new _validator directory_ in the `.lighthouse/validators` +> directory. Your validator directory will be identified by it's public key, +> which looks something like `0xc483de...`. You'll need to find this directory +> for the next step. +> - These keys are good enough for the Lighthouse testnet, however they shouldn't +> be considered secure until we've undergone a security audit (planned Jan +> 2020). + +### 4. Start your validator client + +For security reasons, the validator client runs separately to the beacon node. +The validator client stores private keys and signs messages generated by the +beacon node. + +You'll need both your beacon node _and_ validator client running if you want to +stake. + +Start the validator client with: + +```bash +$ lighthouse validator +``` + +The validator client is running and has found your validator keys from step 3 +when you see the following log: + +``` +Dec 09 13:08:59.171 INFO Loaded validator keypair store voting_validators: 1 +Dec 09 13:09:09.000 INFO Awaiting activation slot: 17787, ... +``` + +If your beacon node hasn't finished syncing yet, you'll see some `ERRO` +messages indicating that your node isn't synced yet. It is safest to wait for +your node to sync before moving onto the next step, otherwise your validator +may active before you're able to produce blocks and attestations. However, it +generally takes 4-8+ hours after deposit for a validator to become active. If +your `est_time` is less than 4 hours, you _should_ be fine to just move to the +next step. After all, this is a testnet and you're only risking Goerli ETH. + +### 5. Submit your deposit + +
+

Upload the eth1_deposit_data.rlp file from your validator + directory (created in step 3) to submit your 3.2 Goerli-ETH + deposit using Metamask.

+

Hint: it's generally in the $HOME/.lighthouse/validators/0x... directory

+ + +
+ + + + + +> This deposit is using gETH (Goerli ETH) which has no real value. Don't ever +> send _real_ ETH to our deposit contract! + +## Next steps + +Leave your beacon node and validator client running and you'll see logs as the +beacon node keeps synced with the network and the validator client produces +blocks and attestations. + +It will take some time (minutes to hours) for the beacon chain to process and +activate your validator, however you'll know you're active when the validator +client starts successfully publishing attestations each slot: + +``` +Dec 03 08:49:40.053 INFO Successfully published attestation slot: 98, committee_index: 0, head_block: 0xa208…7fd5, +``` + +Although you'll produce an attestation each slot, it's less common to produce a +block. Watch for the block production logs too: + +``` +Dec 03 08:49:36.225 INFO Successfully published block slot: 98, attestations: 2, deposits: 0, service: block +``` + +If you see any `ERRO` (error) logs, please reach out on +[Discord](https://discord.gg/cyAszAh) or [create an +issue](https://github.com/sigp/lighthouse/issues/new). + +Happy staking! + + + + + + + + diff --git a/book/src/js/deposit.js b/book/src/js/deposit.js new file mode 100644 index 000000000..6701c979f --- /dev/null +++ b/book/src/js/deposit.js @@ -0,0 +1,127 @@ +const NETWORK = "5"; +const NETWORK_NAME = "Goerli Test Network"; +const DEPOSIT_CONTRACT = "0x13e4d66c7215d7b63fec7b52fc65e6655093d906"; +const DEPOSIT_AMOUNT_ETH = "3.2"; +const GAS_LIMIT = "4000000"; +const DEPOSIT_DATA_BYTES = 420; + +let PREVIOUS_NON_ERROR_STATE = ""; + +$(document).ready(function(){ + if (typeof window.ethereum !== 'undefined') { + ethereum.on('networkChanged', function (accounts) { + checkNetwork() + }) + + PREVIOUS_NON_ERROR_STATE = "upload"; + checkNetwork() + } else { + console.error("No metamask detected!") + triggerError("Metamask is not installed.
Get Metamask.") + } + + $("#fileInput").change(function() { + openFile(this.files[0]) + }); + + $("#uploadButton").on("click", function() { + $("#fileInput").trigger("click"); + }); +}); + +function checkNetwork() { + if (window.ethereum.networkVersion === NETWORK) { + setUiState(PREVIOUS_NON_ERROR_STATE) + } else { + triggerError("Please set Metamask to use " + NETWORK_NAME + ".") + } +} + +function doDeposit(deposit_data) { + const ethereum = window.ethereum; + const utils = ethers.utils; + + let wei = utils.parseEther(DEPOSIT_AMOUNT_ETH); + let gasLimit = utils.bigNumberify(GAS_LIMIT); + + ethereum.enable() + .then(function (accounts) { + let params = [{ + "from": accounts[0], + "to": DEPOSIT_CONTRACT, + "gas": utils.hexlify(gasLimit), + "value": utils.hexlify(wei), + "data": deposit_data + }] + + ethereum.sendAsync({ + method: 'eth_sendTransaction', + params: params, + from: accounts[0], // Provide the user's account to use. + }, function (err, result) { + if (err !== null) { + triggerError("

" + err.message + "

Reload the window to try again.

") + } else { + let tx_hash = result.result; + $("#txLink").attr("href", "https://goerli.etherscan.io/tx/" + tx_hash); + setUiState("waiting"); + } + }) + }) + .catch(function (error) { + triggerError("Unable to get Metamask accounts.
Reload page to try again.") + }) + +} + +function openFile(file) { + var reader = new FileReader(); + + reader.onload = function () { + let data = reader.result; + if (data.startsWith("0x")) { + if (data.length === DEPOSIT_DATA_BYTES * 2 + 2) { + doDeposit(data) + } else { + triggerError("Invalid eth1_deposit_file. Bad length.") + } + } else { + triggerError("Invalid eth1_deposit_file. Did not start with 0x.") + } + } + + reader.readAsBinaryString(file); +} + +function triggerError(text) { + $("#errorText").html(text); + setUiState("error"); +} + +function setUiState(state) { + if (state === "upload") { + $('#uploadDiv').show(); + $('#depositDiv').hide(); + $('#waitingDiv').hide(); + $('#errorDiv').hide(); + } else if (state == "deposit") { + $('#uploadDiv').hide(); + $('#depositDiv').show(); + $('#waitingDiv').hide(); + $('#errorDiv').hide(); + } else if (state == "error") { + $('#uploadDiv').hide(); + $('#depositDiv').hide(); + $('#waitingDiv').hide(); + $('#errorDiv').show(); + } else if (state == "waiting") { + $('#uploadDiv').hide(); + $('#depositDiv').hide(); + $('#waitingDiv').show(); + $('#errorDiv').hide(); + } + + if (state !== "error") { + PREVIOUS_NON_ERROR_STATE = state; + } +} diff --git a/eth2/utils/eth2_testnet_config/src/lib.rs b/eth2/utils/eth2_testnet_config/src/lib.rs index 8ef0af5de..57575fead 100644 --- a/eth2/utils/eth2_testnet_config/src/lib.rs +++ b/eth2/utils/eth2_testnet_config/src/lib.rs @@ -198,9 +198,9 @@ impl Eth2TestnetConfig { mod tests { use super::*; use tempdir::TempDir; - use types::{Eth1Data, Hash256, MinimalEthSpec, YamlConfig}; + use types::{Eth1Data, Hash256, MainnetEthSpec, YamlConfig}; - type E = MinimalEthSpec; + type E = MainnetEthSpec; #[test] fn hard_coded_works() { diff --git a/eth2/utils/eth2_testnet_config/testnet/config.yaml b/eth2/utils/eth2_testnet_config/testnet/config.yaml index 1693cdf7a..c9e4bd3ff 100644 --- a/eth2/utils/eth2_testnet_config/testnet/config.yaml +++ b/eth2/utils/eth2_testnet_config/testnet/config.yaml @@ -1,13 +1,13 @@ FAR_FUTURE_EPOCH: 18446744073709551615 BASE_REWARDS_PER_EPOCH: 4 DEPOSIT_CONTRACT_TREE_DEPTH: 32 -SECONDS_PER_DAY: 480 -MAX_COMMITTEES_PER_SLOT: 4 -TARGET_COMMITTEE_SIZE: 4 +SECONDS_PER_DAY: 2400 +MAX_COMMITTEES_PER_SLOT: 64 +TARGET_COMMITTEE_SIZE: 128 MIN_PER_EPOCH_CHURN_LIMIT: 4 CHURN_LIMIT_QUOTIENT: 65536 -SHUFFLE_ROUND_COUNT: 10 -MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 64 +SHUFFLE_ROUND_COUNT: 90 +MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 16384 MIN_GENESIS_TIME: 0 MIN_DEPOSIT_AMOUNT: 100 MAX_EFFECTIVE_BALANCE: 3200000000 @@ -35,15 +35,16 @@ DOMAIN_VOLUNTARY_EXIT: 0x04000000 JUSTIFICATION_BITS_LENGTH: 0x04000000 MAX_VALIDATORS_PER_COMMITTEE: 2048 GENESIS_EPOCH: 0 -SLOTS_PER_EPOCH: 8 -SLOTS_PER_ETH1_VOTING_PERIOD: 16 -SLOTS_PER_HISTORICAL_ROOT: 64 -EPOCHS_PER_HISTORICAL_VECTOR: 64 -EPOCHS_PER_SLASHINGS_VECTOR: 64 +SLOTS_PER_EPOCH: 32 +SLOTS_PER_ETH1_VOTING_PERIOD: 1024 +SLOTS_PER_HISTORICAL_ROOT: 8192 +EPOCHS_PER_HISTORICAL_VECTOR: 65536 +EPOCHS_PER_SLASHINGS_VECTOR: 8192 HISTORICAL_ROOTS_LIMIT: 16777216 VALIDATOR_REGISTRY_LIMIT: 1099511627776 MAX_PROPOSER_SLASHINGS: 16 MAX_ATTESTER_SLASHINGS: 1 MAX_ATTESTATIONS: 128 MAX_DEPOSITS: 16 -MAX_VOLUNTARY_EXITS: 16 \ No newline at end of file +MAX_VOLUNTARY_EXITS: 16 +ETH1_FOLLOW_DISTANCE: 16 \ No newline at end of file diff --git a/eth2/utils/eth2_testnet_config/testnet/deploy_block.txt b/eth2/utils/eth2_testnet_config/testnet/deploy_block.txt index 9272e5890..961f2241d 100644 --- a/eth2/utils/eth2_testnet_config/testnet/deploy_block.txt +++ b/eth2/utils/eth2_testnet_config/testnet/deploy_block.txt @@ -1 +1 @@ -1743571 \ No newline at end of file +1773705 \ No newline at end of file diff --git a/eth2/utils/eth2_testnet_config/testnet/deposit_contract.txt b/eth2/utils/eth2_testnet_config/testnet/deposit_contract.txt index 55ea9dc71..187fd074f 100644 --- a/eth2/utils/eth2_testnet_config/testnet/deposit_contract.txt +++ b/eth2/utils/eth2_testnet_config/testnet/deposit_contract.txt @@ -1 +1 @@ -0xf382356688ae7dd3c2d6deb7e79c3ffe68816251 \ No newline at end of file +0x13e4d66c7215d7b63fec7b52fc65e6655093d906 \ No newline at end of file diff --git a/eth2/utils/eth2_testnet_config/testnet/genesis.ssz b/eth2/utils/eth2_testnet_config/testnet/genesis.ssz index 90edf24e4..d4457d6e4 100644 Binary files a/eth2/utils/eth2_testnet_config/testnet/genesis.ssz and b/eth2/utils/eth2_testnet_config/testnet/genesis.ssz differ diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index 7e5cf987a..d5d76a244 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -23,7 +23,10 @@ fn main() { let matches = App::new("Lighthouse") .version(crate_version!()) .author("Sigma Prime ") - .about("Eth 2.0 Client") + .about( + "Ethereum 2.0 client by Sigma Prime. Provides a full-featured beacon \ + node, a validator client and utilities for managing validator accounts.", + ) .arg( Arg::with_name("spec") .short("s") diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index 1173588ff..fd1d46fb9 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -4,7 +4,8 @@ use clap::{App, Arg, SubCommand}; pub fn cli_app<'a, 'b>() -> App<'a, 'b> { App::new("validator_client") .visible_aliases(&["v", "vc", "validator"]) - .about("Ethereum 2.0 Validator Client") + .about("When connected to a beacon node, performs the duties of a staked \ + validator (e.g., proposing blocks and attestations).") .arg( Arg::with_name("server") .long("server") diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index 3e6f4ee49..76c9bf814 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -80,10 +80,13 @@ impl Config { .into(), ); } - process_testnet_subcommand(sub_cli_args, config) + process_testnet_subcommand(sub_cli_args, config)? } - _ => return Err("You must use the testnet command. See '--help'.".into()), - }?; + _ => { + config.key_source = KeySource::Disk; + config + } + }; Ok(config) } diff --git a/validator_client/src/duties_service.rs b/validator_client/src/duties_service.rs index 0146c1e04..60d3e53d3 100644 --- a/validator_client/src/duties_service.rs +++ b/validator_client/src/duties_service.rs @@ -1,10 +1,10 @@ use crate::validator_store::ValidatorStore; use environment::RuntimeContext; use exit_future::Signal; -use futures::{Future, IntoFuture, Stream}; +use futures::{future, Future, IntoFuture, Stream}; use parking_lot::RwLock; use remote_beacon_node::RemoteBeaconNode; -use slog::{crit, error, info, trace, warn}; +use slog::{crit, debug, error, info, trace, warn}; use slot_clock::SlotClock; use std::collections::HashMap; use std::convert::TryInto; @@ -74,6 +74,34 @@ pub struct DutiesStore { } impl DutiesStore { + /// Returns the total number of validators that should propose in the given epoch. + fn proposer_count(&self, epoch: Epoch) -> usize { + self.store + .read() + .iter() + .filter(|(_validator_pubkey, validator_map)| { + validator_map + .get(&epoch) + .map(|duties| !duties.block_proposal_slots.is_empty()) + .unwrap_or_else(|| false) + }) + .count() + } + + /// Returns the total number of validators that should attest in the given epoch. + fn attester_count(&self, epoch: Epoch) -> usize { + self.store + .read() + .iter() + .filter(|(_validator_pubkey, validator_map)| { + validator_map + .get(&epoch) + .map(|duties| duties.attestation_slot.is_some()) + .unwrap_or_else(|| false) + }) + .count() + } + fn block_producers(&self, slot: Slot, slots_per_epoch: u64) -> Vec { self.store .read() @@ -219,7 +247,7 @@ impl DutiesServiceBuilder { pub struct Inner { store: Arc, validator_store: ValidatorStore, - slot_clock: T, + pub(crate) slot_clock: T, beacon_node: RemoteBeaconNode, context: RuntimeContext, } @@ -249,6 +277,21 @@ impl Deref for DutiesService { } impl DutiesService { + /// Returns the total number of validators known to the duties service. + pub fn total_validator_count(&self) -> usize { + self.validator_store.num_voting_validators() + } + + /// Returns the total number of validators that should propose in the given epoch. + pub fn proposer_count(&self, epoch: Epoch) -> usize { + self.store.proposer_count(epoch) + } + + /// Returns the total number of validators that should attest in the given epoch. + pub fn attester_count(&self, epoch: Epoch) -> usize { + self.store.attester_count(epoch) + } + /// Returns the pubkeys of the validators which are assigned to propose in the given slot. /// /// In normal cases, there should be 0 or 1 validators returned. In extreme cases (i.e., deep forking) @@ -313,6 +356,7 @@ impl DutiesService { let service_1 = self.clone(); let service_2 = self.clone(); let service_3 = self.clone(); + let service_4 = self.clone(); let log_1 = self.context.log.clone(); let log_2 = self.context.log.clone(); @@ -342,24 +386,56 @@ impl DutiesService { }) .and_then(move |epoch| { let log = service_2.context.log.clone(); - service_2.update_epoch(epoch).then(move |result| { - if let Err(e) = result { - error!( - log, - "Failed to get current epoch duties"; - "http_error" => format!("{:?}", e) - ); - } - let log = service_3.context.log.clone(); - service_3.update_epoch(epoch + 1).map_err(move |e| { + service_2 + .beacon_node + .http + .beacon() + .get_head() + .map(move |head| (epoch, head.slot.epoch(E::slots_per_epoch()))) + .map_err(move |e| { + error!( + log, + "Failed to contact beacon node"; + "error" => format!("{:?}", e) + ) + }) + }) + .and_then(move |(current_epoch, beacon_head_epoch)| { + let log = service_3.context.log.clone(); + + let future: Box + Send> = + if beacon_head_epoch + 1 < current_epoch { error!( log, - "Failed to get next epoch duties"; - "http_error" => format!("{:?}", e) + "Beacon node is not synced"; + "node_head_epoch" => format!("{}", beacon_head_epoch), + "current_epoch" => format!("{}", current_epoch), ); - }) - }) + + Box::new(future::ok(())) + } else { + Box::new(service_3.update_epoch(current_epoch).then(move |result| { + if let Err(e) = result { + error!( + log, + "Failed to get current epoch duties"; + "http_error" => format!("{:?}", e) + ); + } + + let log = service_4.context.log.clone(); + service_4.update_epoch(current_epoch + 1).map_err(move |e| { + error!( + log, + "Failed to get next epoch duties"; + "http_error" => format!("{:?}", e) + ); + }) + })) + }; + + future }) .map(|_| ()) } @@ -394,7 +470,7 @@ impl DutiesService { .insert(epoch, duties.clone(), E::slots_per_epoch()) { InsertOutcome::NewValidator => { - info!( + debug!( log, "First duty assignment for validator"; "proposal_slots" => format!("{:?}", &duties.block_proposal_slots), diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index b727dcb16..a9660ef85 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -4,6 +4,7 @@ mod cli; mod config; mod duties_service; mod fork_service; +mod notifier; mod validator_store; pub mod validator_directory; @@ -22,6 +23,7 @@ use futures::{ future::{self, loop_fn, Loop}, Future, IntoFuture, }; +use notifier::spawn_notifier; use remote_beacon_node::RemoteBeaconNode; use slog::{error, info, Logger}; use slot_clock::SlotClock; @@ -258,7 +260,16 @@ impl ProductionValidatorClient { .start_update_service(&self.context.eth2_config.spec) .map_err(|e| format!("Unable to start attestation service: {}", e))?; - self.exit_signals = vec![duties_exit, fork_exit, block_exit, attestation_exit]; + let notifier_exit = + spawn_notifier(self).map_err(|e| format!("Failed to start notifier: {}", e))?; + + self.exit_signals = vec![ + duties_exit, + fork_exit, + block_exit, + attestation_exit, + notifier_exit, + ]; Ok(()) } diff --git a/validator_client/src/notifier.rs b/validator_client/src/notifier.rs new file mode 100644 index 000000000..e60bf8cc6 --- /dev/null +++ b/validator_client/src/notifier.rs @@ -0,0 +1,91 @@ +use crate::ProductionValidatorClient; +use exit_future::Signal; +use futures::{Future, Stream}; +use slog::{error, info}; +use slot_clock::SlotClock; +use std::time::{Duration, Instant}; +use tokio::timer::Interval; +use types::EthSpec; + +/// Spawns a notifier service which periodically logs information about the node. +pub fn spawn_notifier(client: &ProductionValidatorClient) -> Result { + let context = client.context.service_context("notifier".into()); + + let slot_duration = Duration::from_millis(context.eth2_config.spec.milliseconds_per_slot); + let duration_to_next_slot = client + .duties_service + .slot_clock + .duration_to_next_slot() + .ok_or_else(|| "slot_notifier unable to determine time to next slot")?; + + // Run this half way through each slot. + let start_instant = Instant::now() + duration_to_next_slot + (slot_duration / 2); + + // Run this each slot. + let interval_duration = slot_duration; + + let duties_service = client.duties_service.clone(); + let log_1 = context.log.clone(); + let log_2 = context.log.clone(); + + let interval_future = Interval::new(start_instant, interval_duration) + .map_err( + move |e| error!(log_1, "Slot notifier timer failed"; "error" => format!("{:?}", e)), + ) + .for_each(move |_| { + let log = log_2.clone(); + + if let Some(slot) = duties_service.slot_clock.now() { + let epoch = slot.epoch(T::slots_per_epoch()); + + let total_validators = duties_service.total_validator_count(); + let proposing_validators = duties_service.proposer_count(epoch); + let attesting_validators = duties_service.attester_count(epoch); + + if total_validators == 0 { + error!(log, "No validators present") + } else if total_validators == attesting_validators { + info!( + log_2, + "All validators active"; + "proposers" => proposing_validators, + "active_validators" => attesting_validators, + "total_validators" => total_validators, + "epoch" => format!("{}", epoch), + "slot" => format!("{}", slot), + ); + } else if attesting_validators > 0 { + info!( + log_2, + "Some validators active"; + "proposers" => proposing_validators, + "active_validators" => attesting_validators, + "total_validators" => total_validators, + "epoch" => format!("{}", epoch), + "slot" => format!("{}", slot), + ); + } else { + info!( + log_2, + "Awaiting activation"; + "validators" => total_validators, + "epoch" => format!("{}", epoch), + "slot" => format!("{}", slot), + ); + } + } else { + error!(log, "Unable to read slot clock"); + } + + Ok(()) + }); + + let (exit_signal, exit) = exit_future::signal(); + let log = context.log.clone(); + client.context.executor.spawn( + exit.until(interval_future) + .map(move |_| info!(log, "Shutdown complete")), + ); + + Ok(exit_signal) +}