From 1fd86f8b595c6115bb235767ab72262a8746c984 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 3 Dec 2024 11:08:53 +1100 Subject: [PATCH 01/23] Add a security section to the book (#6581) * Add a security section to the book * Update book/src/security.md Co-authored-by: chonghe <44791194+chong-he@users.noreply.github.com> * Update book/src/security.md Co-authored-by: chonghe <44791194+chong-he@users.noreply.github.com> --- book/src/SUMMARY.md | 1 + book/src/resources/2020-lh-trail-of-bits.pdf | Bin 0 -> 501738 bytes book/src/security.md | 12 ++++++++++++ 3 files changed, 13 insertions(+) create mode 100644 book/src/resources/2020-lh-trail-of-bits.pdf create mode 100644 book/src/security.md diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index c38ee58e3b..02683a1172 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -66,3 +66,4 @@ * [Development Environment](./setup.md) * [FAQs](./faq.md) * [Protocol Developers](./developers.md) +* [Security Researchers](./security.md) diff --git a/book/src/resources/2020-lh-trail-of-bits.pdf b/book/src/resources/2020-lh-trail-of-bits.pdf new file mode 100644 index 0000000000000000000000000000000000000000..162bef53f0550c677a8919b4d8f3efd5b3f90948 GIT binary patch literal 501738 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

>6`JW~lI3g!Uca`eoG^gP6FY3;pW09yaF3!V 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 OuePz#+u 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 z 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≪gw~}Q$GQsQY$bfB1vDYv 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�}*{csU^ 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 ukr5Hm&ImSJZ;``GtR{ye-PTMcDfJo;` Date: Tue, 3 Dec 2024 03:56:43 -0500 Subject: [PATCH 02/23] Deprecate futures ticker (#6630) * deprecate futures-ticker * Merge branch 'unstable' of github.com:sigp/lighthouse into deprecate-futures-timer * Merge branch 'unstable' into deprecate-futures-timer * making the linter happy * remove unrequired #[allow(unused_imports)] * fixing minor issues * merge commit * minor fix * clippy changes --- Cargo.lock | 36 ++++++++++++------- .../lighthouse_network/gossipsub/Cargo.toml | 3 +- .../gossipsub/src/behaviour.rs | 25 +++++++------ .../gossipsub/src/behaviour/tests.rs | 1 + .../gossipsub/src/peer_score.rs | 2 +- 5 files changed, 38 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1ddeecf711..5cea2d2ec5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3375,22 +3375,15 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" -[[package]] -name = "futures-ticker" -version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9763058047f713632a52e916cc7f6a4b3fc6e9fc1ff8c5b1dc49e5a89041682e" -dependencies = [ - "futures", - "futures-timer", - "instant", -] - [[package]] name = "futures-timer" version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +dependencies = [ + "gloo-timers", + "send_wrapper 0.4.0", +] [[package]] name = "futures-util" @@ -3506,6 +3499,18 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "gossipsub" version = "0.5.0" @@ -3518,7 +3523,6 @@ dependencies = [ "either", "fnv", "futures", - "futures-ticker", "futures-timer", "getrandom", "hashlink 0.9.1", @@ -7736,6 +7740,12 @@ dependencies = [ "pest", ] +[[package]] +name = "send_wrapper" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" + [[package]] name = "send_wrapper" version = "0.6.0" @@ -10349,7 +10359,7 @@ dependencies = [ "log", "pharos", "rustc_version 0.4.1", - "send_wrapper", + "send_wrapper 0.6.0", "thiserror 1.0.69", "wasm-bindgen", "wasm-bindgen-futures", diff --git a/beacon_node/lighthouse_network/gossipsub/Cargo.toml b/beacon_node/lighthouse_network/gossipsub/Cargo.toml index a01d60dae9..6cbe6d3a1c 100644 --- a/beacon_node/lighthouse_network/gossipsub/Cargo.toml +++ b/beacon_node/lighthouse_network/gossipsub/Cargo.toml @@ -10,7 +10,7 @@ keywords = ["peer-to-peer", "libp2p", "networking"] categories = ["network-programming", "asynchronous"] [features] -wasm-bindgen = ["getrandom/js"] +wasm-bindgen = ["getrandom/js", "futures-timer/wasm-bindgen"] rsa = [] [dependencies] @@ -22,7 +22,6 @@ bytes = "1.5" either = "1.9" fnv = "1.0.7" futures = "0.3.30" -futures-ticker = "0.0.3" futures-timer = "3.0.2" getrandom = "0.2.12" hashlink.workspace = true diff --git a/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs b/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs index 5ead0c06a0..aafd869bee 100644 --- a/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs +++ b/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs @@ -29,8 +29,7 @@ use std::{ time::Duration, }; -use futures::StreamExt; -use futures_ticker::Ticker; +use futures::FutureExt; use hashlink::LinkedHashMap; use prometheus_client::registry::Registry; use rand::{seq::SliceRandom, thread_rng}; @@ -74,6 +73,7 @@ use super::{ types::RpcOut, }; use super::{PublishError, SubscriptionError, TopicScoreParams, ValidationError}; +use futures_timer::Delay; use quick_protobuf::{MessageWrite, Writer}; use std::{cmp::Ordering::Equal, fmt::Debug}; @@ -301,7 +301,7 @@ pub struct Behaviour { mcache: MessageCache, /// Heartbeat interval stream. - heartbeat: Ticker, + heartbeat: Delay, /// Number of heartbeats since the beginning of time; this allows us to amortize some resource /// clean up -- eg backoff clean up. @@ -318,7 +318,7 @@ pub struct Behaviour { outbound_peers: HashSet, /// Stores optional peer score data together with thresholds and decay interval. - peer_score: Option<(PeerScore, PeerScoreThresholds, Ticker)>, + peer_score: Option<(PeerScore, PeerScoreThresholds, Delay)>, /// Counts the number of `IHAVE` received from each peer since the last heartbeat. count_received_ihave: HashMap, @@ -466,10 +466,7 @@ where config.backoff_slack(), ), mcache: MessageCache::new(config.history_gossip(), config.history_length()), - heartbeat: Ticker::new_with_next( - config.heartbeat_interval(), - config.heartbeat_initial_delay(), - ), + heartbeat: Delay::new(config.heartbeat_interval() + config.heartbeat_initial_delay()), heartbeat_ticks: 0, px_peers: HashSet::new(), outbound_peers: HashSet::new(), @@ -938,7 +935,7 @@ where return Err("Peer score set twice".into()); } - let interval = Ticker::new(params.decay_interval); + let interval = Delay::new(params.decay_interval); let peer_score = PeerScore::new_with_message_delivery_time_callback(params, callback); self.peer_score = Some((peer_score, threshold, interval)); Ok(()) @@ -1208,7 +1205,7 @@ where } fn score_below_threshold_from_scores( - peer_score: &Option<(PeerScore, PeerScoreThresholds, Ticker)>, + peer_score: &Option<(PeerScore, PeerScoreThresholds, Delay)>, peer_id: &PeerId, threshold: impl Fn(&PeerScoreThresholds) -> f64, ) -> (bool, f64) { @@ -3427,14 +3424,16 @@ where } // update scores - if let Some((peer_score, _, interval)) = &mut self.peer_score { - while let Poll::Ready(Some(_)) = interval.poll_next_unpin(cx) { + if let Some((peer_score, _, delay)) = &mut self.peer_score { + if delay.poll_unpin(cx).is_ready() { peer_score.refresh_scores(); + delay.reset(peer_score.params.decay_interval); } } - while let Poll::Ready(Some(_)) = self.heartbeat.poll_next_unpin(cx) { + if self.heartbeat.poll_unpin(cx).is_ready() { self.heartbeat(); + self.heartbeat.reset(self.config.heartbeat_interval()); } Poll::Pending diff --git a/beacon_node/lighthouse_network/gossipsub/src/behaviour/tests.rs b/beacon_node/lighthouse_network/gossipsub/src/behaviour/tests.rs index 713fe1f266..90b8fe43fb 100644 --- a/beacon_node/lighthouse_network/gossipsub/src/behaviour/tests.rs +++ b/beacon_node/lighthouse_network/gossipsub/src/behaviour/tests.rs @@ -25,6 +25,7 @@ use crate::subscription_filter::WhitelistSubscriptionFilter; use crate::types::RpcReceiver; use crate::{config::ConfigBuilder, types::Rpc, IdentTopic as Topic}; use byteorder::{BigEndian, ByteOrder}; +use futures::StreamExt; use libp2p::core::ConnectedPoint; use rand::Rng; use std::net::Ipv4Addr; diff --git a/beacon_node/lighthouse_network/gossipsub/src/peer_score.rs b/beacon_node/lighthouse_network/gossipsub/src/peer_score.rs index fa02f06f69..ec6fe7bdb6 100644 --- a/beacon_node/lighthouse_network/gossipsub/src/peer_score.rs +++ b/beacon_node/lighthouse_network/gossipsub/src/peer_score.rs @@ -44,7 +44,7 @@ mod tests; const TIME_CACHE_DURATION: u64 = 120; pub(crate) struct PeerScore { - params: PeerScoreParams, + pub(crate) params: PeerScoreParams, /// The score parameters. peer_stats: HashMap, /// Tracking peers per IP. From a2b00090fd8c6c5d6b4ffc88f8d0f937d9165c58 Mon Sep 17 00:00:00 2001 From: Daniel Knopik <107140945+dknopik@users.noreply.github.com> Date: Thu, 12 Dec 2024 00:51:20 +0100 Subject: [PATCH 03/23] Remove `ZeroizeString` in favour of `Zeroizing` (#6661) * Remove ZeroizeString in favour of Zeroizing * cargo fmt * remove unrelated line that slipped in * Update beacon_node/store/Cargo.toml thanks michael! Co-authored-by: Michael Sproul * Merge branch 'unstable' into remove-zeroizedstring --- Cargo.lock | 11 +- Cargo.toml | 2 +- account_manager/Cargo.toml | 1 + account_manager/src/validator/create.rs | 2 +- account_manager/src/validator/import.rs | 16 ++- account_manager/src/wallet/create.rs | 4 +- common/account_utils/src/lib.rs | 108 ++---------------- .../src/validator_definitions.rs | 11 +- common/eth2/Cargo.toml | 5 +- common/eth2/src/lighthouse_vc/http_client.rs | 12 +- common/eth2/src/lighthouse_vc/std_types.rs | 4 +- common/eth2/src/lighthouse_vc/types.rs | 17 ++- crypto/eth2_keystore/src/keystore.rs | 46 +------- lighthouse/Cargo.toml | 1 + lighthouse/tests/account_manager.rs | 9 +- validator_client/http_api/Cargo.toml | 1 + .../http_api/src/create_validator.rs | 7 +- validator_client/http_api/src/keystores.rs | 5 +- validator_client/http_api/src/test_utils.rs | 4 +- validator_client/http_api/src/tests.rs | 5 +- .../http_api/src/tests/keystores.rs | 3 +- .../initialized_validators/Cargo.toml | 1 + .../initialized_validators/src/lib.rs | 16 +-- validator_manager/Cargo.toml | 1 + validator_manager/src/common.rs | 5 +- validator_manager/src/import_validators.rs | 14 +-- validator_manager/src/move_validators.rs | 5 +- 27 files changed, 99 insertions(+), 217 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5cea2d2ec5..00aeaa9af4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,6 +36,7 @@ dependencies = [ "tokio", "types", "validator_dir", + "zeroize", ] [[package]] @@ -2561,8 +2562,6 @@ dependencies = [ name = "eth2" version = "0.1.0" dependencies = [ - "account_utils", - "bytes", "derivative", "eth2_keystore", "ethereum_serde_utils", @@ -2570,7 +2569,6 @@ dependencies = [ "ethereum_ssz_derive", "futures", "futures-util", - "libsecp256k1", "lighthouse_network", "mediatype", "pretty_reqwest_error", @@ -2578,7 +2576,6 @@ dependencies = [ "proto_array", "psutil", "reqwest", - "ring 0.16.20", "sensitive_url", "serde", "serde_json", @@ -2587,6 +2584,7 @@ dependencies = [ "store", "tokio", "types", + "zeroize", ] [[package]] @@ -4433,6 +4431,7 @@ dependencies = [ "url", "validator_dir", "validator_metrics", + "zeroize", ] [[package]] @@ -5287,6 +5286,7 @@ dependencies = [ "validator_client", "validator_dir", "validator_manager", + "zeroize", ] [[package]] @@ -9584,6 +9584,7 @@ dependencies = [ "validator_store", "warp", "warp_utils", + "zeroize", ] [[package]] @@ -9627,6 +9628,7 @@ dependencies = [ "tree_hash", "types", "validator_http_api", + "zeroize", ] [[package]] @@ -10562,6 +10564,7 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" dependencies = [ + "serde", "zeroize_derive", ] diff --git a/Cargo.toml b/Cargo.toml index 0be462754e..9e921190b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -201,7 +201,7 @@ tree_hash_derive = "0.8" url = "2" uuid = { version = "0.8", features = ["serde", "v4"] } warp = { version = "0.3.7", default-features = false, features = ["tls"] } -zeroize = { version = "1", features = ["zeroize_derive"] } +zeroize = { version = "1", features = ["zeroize_derive", "serde"] } zip = "0.6" # Local crates. diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index 7f2fa05a88..48230bb281 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -27,6 +27,7 @@ safe_arith = { workspace = true } slot_clock = { workspace = true } filesystem = { workspace = true } sensitive_url = { workspace = true } +zeroize = { workspace = true } [dev-dependencies] tempfile = { workspace = true } diff --git a/account_manager/src/validator/create.rs b/account_manager/src/validator/create.rs index ec5af1e2ec..73e0ad54d4 100644 --- a/account_manager/src/validator/create.rs +++ b/account_manager/src/validator/create.rs @@ -294,7 +294,7 @@ pub fn read_wallet_password_from_cli( eprintln!(); eprintln!("{}", WALLET_PASSWORD_PROMPT); let password = - PlainText::from(read_password_from_user(stdin_inputs)?.as_ref().to_vec()); + PlainText::from(read_password_from_user(stdin_inputs)?.as_bytes().to_vec()); Ok(password) } } diff --git a/account_manager/src/validator/import.rs b/account_manager/src/validator/import.rs index 19ab5ad60a..4d2353b553 100644 --- a/account_manager/src/validator/import.rs +++ b/account_manager/src/validator/import.rs @@ -7,7 +7,7 @@ use account_utils::{ recursively_find_voting_keystores, PasswordStorage, ValidatorDefinition, ValidatorDefinitions, CONFIG_FILENAME, }, - ZeroizeString, STDIN_INPUTS_FLAG, + STDIN_INPUTS_FLAG, }; use clap::{Arg, ArgAction, ArgMatches, Command}; use clap_utils::FLAG_HEADER; @@ -16,6 +16,7 @@ use std::fs; use std::path::PathBuf; use std::thread::sleep; use std::time::Duration; +use zeroize::Zeroizing; pub const CMD: &str = "import"; pub const KEYSTORE_FLAG: &str = "keystore"; @@ -148,7 +149,7 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin // Skip keystores that already exist, but exit early if any operation fails. // Reuses the same password for all keystores if the `REUSE_PASSWORD_FLAG` flag is set. let mut num_imported_keystores = 0; - let mut previous_password: Option = None; + let mut previous_password: Option> = None; for src_keystore in &keystore_paths { let keystore = Keystore::from_json_file(src_keystore) @@ -182,14 +183,17 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin let password = match keystore_password_path.as_ref() { Some(path) => { - let password_from_file: ZeroizeString = fs::read_to_string(path) + let password_from_file: Zeroizing = fs::read_to_string(path) .map_err(|e| format!("Unable to read {:?}: {:?}", path, e))? .into(); - password_from_file.without_newlines() + password_from_file + .trim_end_matches(['\r', '\n']) + .to_string() + .into() } None => { let password_from_user = read_password_from_user(stdin_inputs)?; - if password_from_user.as_ref().is_empty() { + if password_from_user.is_empty() { eprintln!("Continuing without password."); sleep(Duration::from_secs(1)); // Provides nicer UX. break None; @@ -314,7 +318,7 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin /// Otherwise, returns the keystore error. fn check_password_on_keystore( keystore: &Keystore, - password: &ZeroizeString, + password: &Zeroizing, ) -> Result { match keystore.decrypt_keypair(password.as_ref()) { Ok(_) => { diff --git a/account_manager/src/wallet/create.rs b/account_manager/src/wallet/create.rs index b22007050f..6369646929 100644 --- a/account_manager/src/wallet/create.rs +++ b/account_manager/src/wallet/create.rs @@ -226,14 +226,14 @@ pub fn read_new_wallet_password_from_cli( eprintln!(); eprintln!("{}", NEW_WALLET_PASSWORD_PROMPT); let password = - PlainText::from(read_password_from_user(stdin_inputs)?.as_ref().to_vec()); + PlainText::from(read_password_from_user(stdin_inputs)?.as_bytes().to_vec()); // Ensure the password meets the minimum requirements. match is_password_sufficiently_complex(password.as_bytes()) { Ok(_) => { eprintln!("{}", RETYPE_PASSWORD_PROMPT); let retyped_password = - PlainText::from(read_password_from_user(stdin_inputs)?.as_ref().to_vec()); + PlainText::from(read_password_from_user(stdin_inputs)?.as_bytes().to_vec()); if retyped_password == password { break Ok(password); } else { diff --git a/common/account_utils/src/lib.rs b/common/account_utils/src/lib.rs index c1fa621abb..0f576efb3a 100644 --- a/common/account_utils/src/lib.rs +++ b/common/account_utils/src/lib.rs @@ -8,18 +8,14 @@ use eth2_wallet::{ }; use filesystem::{create_with_600_perms, Error as FsError}; use rand::{distributions::Alphanumeric, Rng}; -use serde::{Deserialize, Serialize}; +use std::fs::{self, File}; use std::io; use std::io::prelude::*; use std::path::{Path, PathBuf}; use std::str::from_utf8; use std::thread::sleep; use std::time::Duration; -use std::{ - fs::{self, File}, - str::FromStr, -}; -use zeroize::Zeroize; +use zeroize::Zeroizing; pub mod validator_definitions; @@ -69,8 +65,8 @@ pub fn read_password>(path: P) -> Result { fs::read(path).map(strip_off_newlines).map(Into::into) } -/// Reads a password file into a `ZeroizeString` struct, with new-lines removed. -pub fn read_password_string>(path: P) -> Result { +/// Reads a password file into a `Zeroizing` struct, with new-lines removed. +pub fn read_password_string>(path: P) -> Result, String> { fs::read(path) .map_err(|e| format!("Error opening file: {:?}", e)) .map(strip_off_newlines) @@ -112,8 +108,8 @@ 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 { +/// Generates a random alphanumeric password of length `DEFAULT_PASSWORD_LEN` as `Zeroizing`. +pub fn random_password_string() -> Zeroizing { random_password_raw_string().into() } @@ -141,7 +137,7 @@ pub fn strip_off_newlines(mut bytes: Vec) -> Vec { } /// Reads a password from TTY or stdin if `use_stdin == true`. -pub fn read_password_from_user(use_stdin: bool) -> Result { +pub fn read_password_from_user(use_stdin: bool) -> Result, String> { let result = if use_stdin { rpassword::prompt_password_stderr("") .map_err(|e| format!("Error reading from stdin: {}", e)) @@ -150,7 +146,7 @@ pub fn read_password_from_user(use_stdin: bool) -> Result .map_err(|e| format!("Error reading from tty: {}", e)) }; - result.map(ZeroizeString::from) + result.map(Zeroizing::from) } /// Reads a mnemonic phrase from TTY or stdin if `use_stdin == true`. @@ -210,46 +206,6 @@ pub fn mnemonic_from_phrase(phrase: &str) -> Result { Mnemonic::from_phrase(phrase, Language::English).map_err(|e| e.to_string()) } -/// Provides a new-type wrapper around `String` that is zeroized on `Drop`. -/// -/// Useful for ensuring that password memory is zeroed-out on drop. -#[derive(Clone, PartialEq, Serialize, Deserialize, Zeroize)] -#[zeroize(drop)] -#[serde(transparent)] -pub struct ZeroizeString(String); - -impl FromStr for ZeroizeString { - type Err = String; - - fn from_str(s: &str) -> Result { - Ok(Self(s.to_owned())) - } -} - -impl From for ZeroizeString { - fn from(s: String) -> Self { - Self(s) - } -} - -impl ZeroizeString { - pub fn as_str(&self) -> &str { - &self.0 - } - - /// Remove any number of newline or carriage returns from the end of a vector of bytes. - pub fn without_newlines(&self) -> ZeroizeString { - let stripped_string = self.0.trim_end_matches(['\r', '\n']).into(); - Self(stripped_string) - } -} - -impl AsRef<[u8]> for ZeroizeString { - fn as_ref(&self) -> &[u8] { - self.0.as_bytes() - } -} - pub fn read_mnemonic_from_cli( mnemonic_path: Option, stdin_inputs: bool, @@ -294,54 +250,6 @@ pub fn read_mnemonic_from_cli( mod test { use super::*; - #[test] - fn test_zeroize_strip_off() { - let expected = "hello world"; - - assert_eq!( - ZeroizeString::from("hello world\n".to_string()) - .without_newlines() - .as_str(), - expected - ); - assert_eq!( - ZeroizeString::from("hello world\n\n\n\n".to_string()) - .without_newlines() - .as_str(), - expected - ); - assert_eq!( - ZeroizeString::from("hello world\r".to_string()) - .without_newlines() - .as_str(), - expected - ); - assert_eq!( - ZeroizeString::from("hello world\r\r\r\r\r".to_string()) - .without_newlines() - .as_str(), - expected - ); - assert_eq!( - ZeroizeString::from("hello world\r\n".to_string()) - .without_newlines() - .as_str(), - expected - ); - assert_eq!( - ZeroizeString::from("hello world\r\n\r\n".to_string()) - .without_newlines() - .as_str(), - expected - ); - assert_eq!( - ZeroizeString::from("hello world".to_string()) - .without_newlines() - .as_str(), - expected - ); - } - #[test] fn test_strip_off() { let expected = b"hello world".to_vec(); diff --git a/common/account_utils/src/validator_definitions.rs b/common/account_utils/src/validator_definitions.rs index f228ce5fdf..a4850fc1c6 100644 --- a/common/account_utils/src/validator_definitions.rs +++ b/common/account_utils/src/validator_definitions.rs @@ -3,9 +3,7 @@ //! Serves as the source-of-truth of which validators this validator client should attempt (or not //! attempt) to load into the `crate::intialized_validators::InitializedValidators` struct. -use crate::{ - default_keystore_password_path, read_password_string, write_file_via_temporary, ZeroizeString, -}; +use crate::{default_keystore_password_path, read_password_string, write_file_via_temporary}; use directory::ensure_dir_exists; use eth2_keystore::Keystore; use regex::Regex; @@ -17,6 +15,7 @@ use std::io; use std::path::{Path, PathBuf}; use types::{graffiti::GraffitiString, Address, PublicKey}; use validator_dir::VOTING_KEYSTORE_FILE; +use zeroize::Zeroizing; /// The file name for the serialized `ValidatorDefinitions` struct. pub const CONFIG_FILENAME: &str = "validator_definitions.yml"; @@ -52,7 +51,7 @@ pub enum Error { /// Defines how a password for a validator keystore will be persisted. pub enum PasswordStorage { /// Store the password in the `validator_definitions.yml` file. - ValidatorDefinitions(ZeroizeString), + ValidatorDefinitions(Zeroizing), /// Store the password in a separate, dedicated file (likely in the "secrets" directory). File(PathBuf), /// Don't store the password at all. @@ -93,7 +92,7 @@ pub enum SigningDefinition { #[serde(skip_serializing_if = "Option::is_none")] voting_keystore_password_path: Option, #[serde(skip_serializing_if = "Option::is_none")] - voting_keystore_password: Option, + voting_keystore_password: Option>, }, /// A validator that defers to a Web3Signer HTTP server for signing. /// @@ -107,7 +106,7 @@ impl SigningDefinition { matches!(self, SigningDefinition::LocalKeystore { .. }) } - pub fn voting_keystore_password(&self) -> Result, Error> { + pub fn voting_keystore_password(&self) -> Result>, Error> { match self { SigningDefinition::LocalKeystore { voting_keystore_password: Some(password), diff --git a/common/eth2/Cargo.toml b/common/eth2/Cargo.toml index d23a4068f1..f735b4c688 100644 --- a/common/eth2/Cargo.toml +++ b/common/eth2/Cargo.toml @@ -16,10 +16,7 @@ lighthouse_network = { workspace = true } proto_array = { workspace = true } ethereum_serde_utils = { workspace = true } eth2_keystore = { workspace = true } -libsecp256k1 = { workspace = true } -ring = { workspace = true } -bytes = { workspace = true } -account_utils = { workspace = true } +zeroize = { workspace = true } sensitive_url = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } diff --git a/common/eth2/src/lighthouse_vc/http_client.rs b/common/eth2/src/lighthouse_vc/http_client.rs index 67fe77a315..1d1abcac79 100644 --- a/common/eth2/src/lighthouse_vc/http_client.rs +++ b/common/eth2/src/lighthouse_vc/http_client.rs @@ -1,6 +1,5 @@ use super::types::*; use crate::Error; -use account_utils::ZeroizeString; use reqwest::{ header::{HeaderMap, HeaderValue}, IntoUrl, @@ -14,6 +13,7 @@ use std::path::Path; pub use reqwest; pub use reqwest::{Response, StatusCode, Url}; use types::graffiti::GraffitiString; +use zeroize::Zeroizing; /// A wrapper around `reqwest::Client` which provides convenience methods for interfacing with a /// Lighthouse Validator Client HTTP server (`validator_client/src/http_api`). @@ -21,7 +21,7 @@ use types::graffiti::GraffitiString; pub struct ValidatorClientHttpClient { client: reqwest::Client, server: SensitiveUrl, - api_token: Option, + api_token: Option>, authorization_header: AuthorizationHeader, } @@ -79,18 +79,18 @@ impl ValidatorClientHttpClient { } /// Get a reference to this client's API token, if any. - pub fn api_token(&self) -> Option<&ZeroizeString> { + pub fn api_token(&self) -> Option<&Zeroizing> { self.api_token.as_ref() } /// Read an API token from the specified `path`, stripping any trailing whitespace. - pub fn load_api_token_from_file(path: &Path) -> Result { + pub fn load_api_token_from_file(path: &Path) -> Result, Error> { let token = fs::read_to_string(path).map_err(|e| Error::TokenReadError(path.into(), e))?; - Ok(ZeroizeString::from(token.trim_end().to_string())) + Ok(token.trim_end().to_string().into()) } /// Add an authentication token to use when making requests. - pub fn add_auth_token(&mut self, token: ZeroizeString) -> Result<(), Error> { + pub fn add_auth_token(&mut self, token: Zeroizing) -> Result<(), Error> { self.api_token = Some(token); self.authorization_header = AuthorizationHeader::Bearer; diff --git a/common/eth2/src/lighthouse_vc/std_types.rs b/common/eth2/src/lighthouse_vc/std_types.rs index ee05c29839..ae192312bd 100644 --- a/common/eth2/src/lighthouse_vc/std_types.rs +++ b/common/eth2/src/lighthouse_vc/std_types.rs @@ -1,7 +1,7 @@ -use account_utils::ZeroizeString; use eth2_keystore::Keystore; use serde::{Deserialize, Serialize}; use types::{Address, Graffiti, PublicKeyBytes}; +use zeroize::Zeroizing; pub use slashing_protection::interchange::Interchange; @@ -41,7 +41,7 @@ pub struct SingleKeystoreResponse { #[serde(deny_unknown_fields)] pub struct ImportKeystoresRequest { pub keystores: Vec, - pub passwords: Vec, + pub passwords: Vec>, pub slashing_protection: Option, } diff --git a/common/eth2/src/lighthouse_vc/types.rs b/common/eth2/src/lighthouse_vc/types.rs index 1921549bcb..d7d5a00df5 100644 --- a/common/eth2/src/lighthouse_vc/types.rs +++ b/common/eth2/src/lighthouse_vc/types.rs @@ -1,13 +1,12 @@ -use account_utils::ZeroizeString; +pub use crate::lighthouse::Health; +pub use crate::lighthouse_vc::std_types::*; +pub use crate::types::{GenericResponse, VersionData}; use eth2_keystore::Keystore; use graffiti::GraffitiString; 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::*; +use zeroize::Zeroizing; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ValidatorData { @@ -44,7 +43,7 @@ pub struct ValidatorRequest { #[derive(Clone, PartialEq, Serialize, Deserialize)] pub struct CreateValidatorsMnemonicRequest { - pub mnemonic: ZeroizeString, + pub mnemonic: Zeroizing, #[serde(with = "serde_utils::quoted_u32")] pub key_derivation_path_offset: u32, pub validators: Vec, @@ -74,7 +73,7 @@ pub struct CreatedValidator { #[derive(Clone, PartialEq, Serialize, Deserialize)] pub struct PostValidatorsResponseData { - pub mnemonic: ZeroizeString, + pub mnemonic: Zeroizing, pub validators: Vec, } @@ -102,7 +101,7 @@ pub struct ValidatorPatchRequest { #[derive(Clone, PartialEq, Serialize, Deserialize)] pub struct KeystoreValidatorsPostRequest { - pub password: ZeroizeString, + pub password: Zeroizing, pub enable: bool, pub keystore: Keystore, #[serde(default)] @@ -191,7 +190,7 @@ pub struct SingleExportKeystoresResponse { #[serde(skip_serializing_if = "Option::is_none")] pub validating_keystore: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub validating_keystore_password: Option, + pub validating_keystore_password: Option>, } #[derive(Serialize, Deserialize, Debug)] diff --git a/crypto/eth2_keystore/src/keystore.rs b/crypto/eth2_keystore/src/keystore.rs index 304ea3ecd6..16a979cf63 100644 --- a/crypto/eth2_keystore/src/keystore.rs +++ b/crypto/eth2_keystore/src/keystore.rs @@ -26,7 +26,7 @@ use std::io::{Read, Write}; use std::path::Path; use std::str; use unicode_normalization::UnicodeNormalization; -use zeroize::Zeroize; +use zeroize::Zeroizing; /// The byte-length of a BLS secret key. const SECRET_KEY_LEN: usize = 32; @@ -60,45 +60,6 @@ pub const HASH_SIZE: usize = 32; /// The default iteraction count, `c`, for PBKDF2. pub const DEFAULT_PBKDF2_C: u32 = 262_144; -/// Provides a new-type wrapper around `String` that is zeroized on `Drop`. -/// -/// Useful for ensuring that password memory is zeroed-out on drop. -#[derive(Clone, PartialEq, Serialize, Deserialize, Zeroize)] -#[zeroize(drop)] -#[serde(transparent)] -struct ZeroizeString(String); - -impl From for ZeroizeString { - fn from(s: String) -> Self { - Self(s) - } -} - -impl AsRef<[u8]> for ZeroizeString { - fn as_ref(&self) -> &[u8] { - self.0.as_bytes() - } -} - -impl std::ops::Deref for ZeroizeString { - type Target = String; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl std::ops::DerefMut for ZeroizeString { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl FromIterator for ZeroizeString { - fn from_iter>(iter: T) -> Self { - ZeroizeString(String::from_iter(iter)) - } -} - #[derive(Debug, PartialEq)] pub enum Error { InvalidSecretKeyLen { len: usize, expected: usize }, @@ -451,11 +412,12 @@ fn is_control_character(c: char) -> bool { /// Takes a slice of bytes and returns a NFKD normalized string representation. /// /// Returns an error if the bytes are not valid utf8. -fn normalize(bytes: &[u8]) -> Result { +fn normalize(bytes: &[u8]) -> Result, Error> { Ok(str::from_utf8(bytes) .map_err(|_| Error::InvalidPasswordBytes)? .nfkd() - .collect::()) + .collect::() + .into()) } /// Generates a checksum to indicate that the `derived_key` is associated with the diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 329519fb54..1fd9e3dac8 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -73,6 +73,7 @@ eth2 = { workspace = true } beacon_processor = { workspace = true } beacon_node_fallback = { workspace = true } initialized_validators = { workspace = true } +zeroize = { workspace = true } [[test]] diff --git a/lighthouse/tests/account_manager.rs b/lighthouse/tests/account_manager.rs index 4d15593714..c7153f48ef 100644 --- a/lighthouse/tests/account_manager.rs +++ b/lighthouse/tests/account_manager.rs @@ -15,7 +15,7 @@ use account_manager::{ use account_utils::{ eth2_keystore::KeystoreBuilder, validator_definitions::{SigningDefinition, ValidatorDefinition, ValidatorDefinitions}, - ZeroizeString, STDIN_INPUTS_FLAG, + STDIN_INPUTS_FLAG, }; use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; use std::env; @@ -27,6 +27,7 @@ use std::str::from_utf8; use tempfile::{tempdir, TempDir}; use types::{Keypair, PublicKey}; use validator_dir::ValidatorDir; +use zeroize::Zeroizing; /// Returns the `lighthouse account` command. fn account_cmd() -> Command { @@ -498,7 +499,7 @@ fn validator_import_launchpad() { signing_definition: SigningDefinition::LocalKeystore { voting_keystore_path, voting_keystore_password_path: None, - voting_keystore_password: Some(ZeroizeString::from(PASSWORD.to_string())), + voting_keystore_password: Some(Zeroizing::from(PASSWORD.to_string())), }, }; @@ -650,7 +651,7 @@ fn validator_import_launchpad_no_password_then_add_password() { signing_definition: SigningDefinition::LocalKeystore { voting_keystore_path: dst_keystore_dir.join(KEYSTORE_NAME), voting_keystore_password_path: None, - voting_keystore_password: Some(ZeroizeString::from(PASSWORD.to_string())), + voting_keystore_password: Some(Zeroizing::from(PASSWORD.to_string())), }, }; @@ -753,7 +754,7 @@ fn validator_import_launchpad_password_file() { signing_definition: SigningDefinition::LocalKeystore { voting_keystore_path, voting_keystore_password_path: None, - voting_keystore_password: Some(ZeroizeString::from(PASSWORD.to_string())), + voting_keystore_password: Some(Zeroizing::from(PASSWORD.to_string())), }, }; diff --git a/validator_client/http_api/Cargo.toml b/validator_client/http_api/Cargo.toml index b83acdc782..18e0604ad5 100644 --- a/validator_client/http_api/Cargo.toml +++ b/validator_client/http_api/Cargo.toml @@ -43,6 +43,7 @@ validator_services = { workspace = true } url = { workspace = true } warp_utils = { workspace = true } warp = { workspace = true } +zeroize = { workspace = true } [dev-dependencies] itertools = { workspace = true } diff --git a/validator_client/http_api/src/create_validator.rs b/validator_client/http_api/src/create_validator.rs index dfd092e8b4..f90a1057a4 100644 --- a/validator_client/http_api/src/create_validator.rs +++ b/validator_client/http_api/src/create_validator.rs @@ -2,7 +2,7 @@ use account_utils::validator_definitions::{PasswordStorage, ValidatorDefinition} use account_utils::{ eth2_keystore::Keystore, eth2_wallet::{bip39::Mnemonic, WalletBuilder}, - random_mnemonic, random_password, ZeroizeString, + random_mnemonic, random_password, }; use eth2::lighthouse_vc::types::{self as api_types}; use slot_clock::SlotClock; @@ -11,6 +11,7 @@ use types::ChainSpec; use types::EthSpec; use validator_dir::{keystore_password_path, Builder as ValidatorDirBuilder}; use validator_store::ValidatorStore; +use zeroize::Zeroizing; /// Create some validator EIP-2335 keystores and store them on disk. Then, enroll the validators in /// this validator client. @@ -59,7 +60,7 @@ pub async fn create_validators_mnemonic, T: 'static + SlotClock, for request in validator_requests { let voting_password = random_password(); let withdrawal_password = random_password(); - let voting_password_string = ZeroizeString::from( + let voting_password_string = Zeroizing::from( String::from_utf8(voting_password.as_bytes().to_vec()).map_err(|e| { warp_utils::reject::custom_server_error(format!( "locally generated password is not utf8: {:?}", @@ -199,7 +200,7 @@ pub async fn create_validators_web3signer( pub fn get_voting_password_storage( secrets_dir: &Option, voting_keystore: &Keystore, - voting_password_string: &ZeroizeString, + voting_password_string: &Zeroizing, ) -> Result { if let Some(secrets_dir) = &secrets_dir { let password_path = keystore_password_path(secrets_dir, voting_keystore); diff --git a/validator_client/http_api/src/keystores.rs b/validator_client/http_api/src/keystores.rs index 5822c89cb8..fd6b4fdae5 100644 --- a/validator_client/http_api/src/keystores.rs +++ b/validator_client/http_api/src/keystores.rs @@ -1,5 +1,5 @@ //! Implementation of the standard keystore management API. -use account_utils::{validator_definitions::PasswordStorage, ZeroizeString}; +use account_utils::validator_definitions::PasswordStorage; use eth2::lighthouse_vc::{ std_types::{ DeleteKeystoreStatus, DeleteKeystoresRequest, DeleteKeystoresResponse, @@ -22,6 +22,7 @@ use validator_dir::{keystore_password_path, Builder as ValidatorDirBuilder}; use validator_store::ValidatorStore; use warp::Rejection; use warp_utils::reject::{custom_bad_request, custom_server_error}; +use zeroize::Zeroizing; pub fn list( validator_store: Arc>, @@ -167,7 +168,7 @@ pub fn import( fn import_single_keystore( keystore: Keystore, - password: ZeroizeString, + password: Zeroizing, validator_dir_path: PathBuf, secrets_dir: Option, validator_store: &ValidatorStore, diff --git a/validator_client/http_api/src/test_utils.rs b/validator_client/http_api/src/test_utils.rs index 931c4ea08e..d033fdbf2d 100644 --- a/validator_client/http_api/src/test_utils.rs +++ b/validator_client/http_api/src/test_utils.rs @@ -2,7 +2,6 @@ use crate::{ApiSecret, Config as HttpConfig, Context}; use account_utils::validator_definitions::ValidatorDefinitions; use account_utils::{ eth2_wallet::WalletBuilder, mnemonic_from_phrase, random_mnemonic, random_password, - ZeroizeString, }; use deposit_contract::decode_eth1_tx_data; use doppelganger_service::DoppelgangerService; @@ -28,6 +27,7 @@ use task_executor::test_utils::TestRuntime; use tempfile::{tempdir, TempDir}; use tokio::sync::oneshot; use validator_store::{Config as ValidatorStoreConfig, ValidatorStore}; +use zeroize::Zeroizing; pub const PASSWORD_BYTES: &[u8] = &[42, 50, 37]; pub const TEST_DEFAULT_FEE_RECIPIENT: Address = Address::repeat_byte(42); @@ -321,7 +321,7 @@ impl ApiTester { .collect::>(); let (response, mnemonic) = if s.specify_mnemonic { - let mnemonic = ZeroizeString::from(random_mnemonic().phrase().to_string()); + let mnemonic = Zeroizing::from(random_mnemonic().phrase().to_string()); let request = CreateValidatorsMnemonicRequest { mnemonic: mnemonic.clone(), key_derivation_path_offset: s.key_derivation_path_offset, diff --git a/validator_client/http_api/src/tests.rs b/validator_client/http_api/src/tests.rs index 76a6952153..262bb64e69 100644 --- a/validator_client/http_api/src/tests.rs +++ b/validator_client/http_api/src/tests.rs @@ -9,7 +9,7 @@ use initialized_validators::{Config as InitializedValidatorsConfig, InitializedV use crate::{ApiSecret, Config as HttpConfig, Context}; use account_utils::{ eth2_wallet::WalletBuilder, mnemonic_from_phrase, random_mnemonic, random_password, - random_password_string, validator_definitions::ValidatorDefinitions, ZeroizeString, + random_password_string, validator_definitions::ValidatorDefinitions, }; use deposit_contract::decode_eth1_tx_data; use eth2::{ @@ -33,6 +33,7 @@ use task_executor::test_utils::TestRuntime; use tempfile::{tempdir, TempDir}; use types::graffiti::GraffitiString; use validator_store::{Config as ValidatorStoreConfig, ValidatorStore}; +use zeroize::Zeroizing; const PASSWORD_BYTES: &[u8] = &[42, 50, 37]; pub const TEST_DEFAULT_FEE_RECIPIENT: Address = Address::repeat_byte(42); @@ -282,7 +283,7 @@ impl ApiTester { .collect::>(); let (response, mnemonic) = if s.specify_mnemonic { - let mnemonic = ZeroizeString::from(random_mnemonic().phrase().to_string()); + let mnemonic = Zeroizing::from(random_mnemonic().phrase().to_string()); let request = CreateValidatorsMnemonicRequest { mnemonic: mnemonic.clone(), key_derivation_path_offset: s.key_derivation_path_offset, diff --git a/validator_client/http_api/src/tests/keystores.rs b/validator_client/http_api/src/tests/keystores.rs index f3f6de548b..2dde087a7f 100644 --- a/validator_client/http_api/src/tests/keystores.rs +++ b/validator_client/http_api/src/tests/keystores.rs @@ -14,8 +14,9 @@ use std::{collections::HashMap, path::Path}; use tokio::runtime::Handle; use types::{attestation::AttestationBase, Address}; use validator_store::DEFAULT_GAS_LIMIT; +use zeroize::Zeroizing; -fn new_keystore(password: ZeroizeString) -> Keystore { +fn new_keystore(password: Zeroizing) -> Keystore { let keypair = Keypair::random(); Keystore( KeystoreBuilder::new(&keypair, password.as_ref(), String::new()) diff --git a/validator_client/initialized_validators/Cargo.toml b/validator_client/initialized_validators/Cargo.toml index 426cb303f6..9c7a3f19d6 100644 --- a/validator_client/initialized_validators/Cargo.toml +++ b/validator_client/initialized_validators/Cargo.toml @@ -24,3 +24,4 @@ tokio = { workspace = true } bincode = { workspace = true } filesystem = { workspace = true } validator_metrics = { workspace = true } +zeroize = { workspace = true } diff --git a/validator_client/initialized_validators/src/lib.rs b/validator_client/initialized_validators/src/lib.rs index 0b36dbd62c..bd64091dae 100644 --- a/validator_client/initialized_validators/src/lib.rs +++ b/validator_client/initialized_validators/src/lib.rs @@ -14,7 +14,6 @@ use account_utils::{ self, SigningDefinition, ValidatorDefinition, ValidatorDefinitions, Web3SignerDefinition, CONFIG_FILENAME, }, - ZeroizeString, }; use eth2_keystore::Keystore; use lockfile::{Lockfile, LockfileError}; @@ -34,6 +33,7 @@ use types::graffiti::GraffitiString; use types::{Address, Graffiti, Keypair, PublicKey, PublicKeyBytes}; use url::{ParseError, Url}; use validator_dir::Builder as ValidatorDirBuilder; +use zeroize::Zeroizing; use key_cache::KeyCache; @@ -74,7 +74,7 @@ pub enum OnDecryptFailure { pub struct KeystoreAndPassword { pub keystore: Keystore, - pub password: Option, + pub password: Option>, } #[derive(Debug)] @@ -262,7 +262,7 @@ impl InitializedValidator { // If the password is supplied, use it and ignore the path // (if supplied). (_, Some(password)) => ( - password.as_ref().to_vec().into(), + password.as_bytes().to_vec().into(), keystore .decrypt_keypair(password.as_ref()) .map_err(Error::UnableToDecryptKeystore)?, @@ -282,7 +282,7 @@ impl InitializedValidator { &keystore, &keystore_path, )?; - (password.as_ref().to_vec().into(), keypair) + (password.as_bytes().to_vec().into(), keypair) } }, ) @@ -455,7 +455,7 @@ fn build_web3_signer_client( fn unlock_keystore_via_stdin_password( keystore: &Keystore, keystore_path: &Path, -) -> Result<(ZeroizeString, Keypair), Error> { +) -> Result<(Zeroizing, Keypair), Error> { eprintln!(); eprintln!( "The {} file does not contain either of the following fields for {:?}:", @@ -1172,14 +1172,14 @@ impl InitializedValidators { voting_keystore_path, } => { let pw = if let Some(p) = voting_keystore_password { - p.as_ref().to_vec().into() + p.as_bytes().to_vec().into() } else if let Some(path) = voting_keystore_password_path { read_password(path).map_err(Error::UnableToReadVotingKeystorePassword)? } else { let keystore = open_keystore(voting_keystore_path)?; unlock_keystore_via_stdin_password(&keystore, voting_keystore_path)? .0 - .as_ref() + .as_bytes() .to_vec() .into() }; @@ -1425,7 +1425,7 @@ impl InitializedValidators { /// This should only be used for testing, it's rather destructive. pub fn delete_passwords_from_validator_definitions( &mut self, - ) -> Result, Error> { + ) -> Result>, Error> { let mut passwords = HashMap::default(); for def in self.definitions.as_mut_slice() { diff --git a/validator_manager/Cargo.toml b/validator_manager/Cargo.toml index 4f367b8f5b..36df256841 100644 --- a/validator_manager/Cargo.toml +++ b/validator_manager/Cargo.toml @@ -21,6 +21,7 @@ eth2 = { workspace = true } hex = { workspace = true } tokio = { workspace = true } derivative = { workspace = true } +zeroize = { workspace = true } [dev-dependencies] tempfile = { workspace = true } diff --git a/validator_manager/src/common.rs b/validator_manager/src/common.rs index 4a35791b32..cc4157990f 100644 --- a/validator_manager/src/common.rs +++ b/validator_manager/src/common.rs @@ -1,5 +1,5 @@ +use account_utils::strip_off_newlines; pub use account_utils::STDIN_INPUTS_FLAG; -use account_utils::{strip_off_newlines, ZeroizeString}; use eth2::lighthouse_vc::std_types::{InterchangeJsonStr, KeystoreJsonStr}; use eth2::{ lighthouse_vc::{ @@ -14,6 +14,7 @@ use std::fs; use std::path::{Path, PathBuf}; use tree_hash::TreeHash; use types::*; +use zeroize::Zeroizing; pub const IGNORE_DUPLICATES_FLAG: &str = "ignore-duplicates"; pub const COUNT_FLAG: &str = "count"; @@ -41,7 +42,7 @@ pub enum UploadError { #[derive(Clone, Serialize, Deserialize)] pub struct ValidatorSpecification { pub voting_keystore: KeystoreJsonStr, - pub voting_keystore_password: ZeroizeString, + pub voting_keystore_password: Zeroizing, pub slashing_protection: Option, pub fee_recipient: Option

, pub gas_limit: Option, diff --git a/validator_manager/src/import_validators.rs b/validator_manager/src/import_validators.rs index 2a819a2a64..2e8821f0db 100644 --- a/validator_manager/src/import_validators.rs +++ b/validator_manager/src/import_validators.rs @@ -1,6 +1,6 @@ use super::common::*; use crate::DumpConfig; -use account_utils::{eth2_keystore::Keystore, ZeroizeString}; +use account_utils::eth2_keystore::Keystore; use clap::{Arg, ArgAction, ArgMatches, Command}; use clap_utils::FLAG_HEADER; use derivative::Derivative; @@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; use types::Address; +use zeroize::Zeroizing; pub const CMD: &str = "import"; pub const VALIDATORS_FILE_FLAG: &str = "validators-file"; @@ -167,7 +168,7 @@ pub struct ImportConfig { pub vc_token_path: PathBuf, pub ignore_duplicates: bool, #[derivative(Debug = "ignore")] - pub password: Option, + pub password: Option>, pub fee_recipient: Option
, pub gas_limit: Option, pub builder_proposals: Option, @@ -184,7 +185,7 @@ impl ImportConfig { vc_url: clap_utils::parse_required(matches, VC_URL_FLAG)?, vc_token_path: clap_utils::parse_required(matches, VC_TOKEN_FLAG)?, ignore_duplicates: matches.get_flag(IGNORE_DUPLICATES_FLAG), - password: clap_utils::parse_optional(matches, PASSWORD)?, + password: clap_utils::parse_optional(matches, PASSWORD)?.map(Zeroizing::new), fee_recipient: clap_utils::parse_optional(matches, FEE_RECIPIENT)?, gas_limit: clap_utils::parse_optional(matches, GAS_LIMIT)?, builder_proposals: clap_utils::parse_optional(matches, BUILDER_PROPOSALS)?, @@ -382,10 +383,7 @@ async fn run<'a>(config: ImportConfig) -> Result<(), String> { pub mod tests { use super::*; use crate::create_validators::tests::TestBuilder as CreateTestBuilder; - use std::{ - fs::{self, File}, - str::FromStr, - }; + use std::fs::{self, File}; use tempfile::{tempdir, TempDir}; use validator_http_api::{test_utils::ApiTester, Config as HttpConfig}; @@ -419,7 +417,7 @@ pub mod tests { vc_url: vc.url.clone(), vc_token_path, ignore_duplicates: false, - password: Some(ZeroizeString::from_str("password").unwrap()), + password: Some(Zeroizing::new("password".into())), fee_recipient: None, builder_boost_factor: None, gas_limit: None, diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index 807a147ca1..c039728e6f 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -1,6 +1,6 @@ use super::common::*; use crate::DumpConfig; -use account_utils::{read_password_from_user, ZeroizeString}; +use account_utils::read_password_from_user; use clap::{Arg, ArgAction, ArgMatches, Command}; use eth2::{ lighthouse_vc::{ @@ -19,6 +19,7 @@ use std::str::FromStr; use std::time::Duration; use tokio::time::sleep; use types::{Address, PublicKeyBytes}; +use zeroize::Zeroizing; pub const MOVE_DIR_NAME: &str = "lighthouse-validator-move"; pub const VALIDATOR_SPECIFICATION_FILE: &str = "validator-specification.json"; @@ -48,7 +49,7 @@ pub enum PasswordSource { } impl PasswordSource { - fn read_password(&mut self, pubkey: &PublicKeyBytes) -> Result { + fn read_password(&mut self, pubkey: &PublicKeyBytes) -> Result, String> { match self { PasswordSource::Interactive { stdin_inputs } => { eprintln!("Please enter a password for keystore {:?}:", pubkey); From 943716b9a22c1c2589739f3f5af4725f69f48c3a Mon Sep 17 00:00:00 2001 From: Shayan Eskandari Date: Fri, 13 Dec 2024 00:07:01 -0500 Subject: [PATCH 04/23] Fix for blank line in graffiti file (#6635) * Fix for blank line in graffiti file Fix as described in https://github.com/sigp/lighthouse/issues/5880 * add graffiti new line tests * cargo fmt --- validator_client/graffiti_file/src/lib.rs | 52 +++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/validator_client/graffiti_file/src/lib.rs b/validator_client/graffiti_file/src/lib.rs index 0328c14eeb..9dab2e7827 100644 --- a/validator_client/graffiti_file/src/lib.rs +++ b/validator_client/graffiti_file/src/lib.rs @@ -66,6 +66,9 @@ impl GraffitiFile { for line in lines { let line = line.map_err(|e| Error::InvalidLine(e.to_string()))?; + if line.trim().is_empty() { + continue; + } let (pk_opt, graffiti) = read_line(&line)?; match pk_opt { Some(pk) => { @@ -133,9 +136,15 @@ mod tests { const CUSTOM_GRAFFITI1: &str = "custom-graffiti1"; const CUSTOM_GRAFFITI2: &str = "graffitiwall:720:641:#ffff00"; const EMPTY_GRAFFITI: &str = ""; + // Newline test cases + const CUSTOM_GRAFFITI4: &str = "newlines-tests"; + const PK1: &str = "0x800012708dc03f611751aad7a43a082142832b5c1aceed07ff9b543cf836381861352aa923c70eeb02018b638aa306aa"; const PK2: &str = "0x80001866ce324de7d80ec73be15e2d064dcf121adf1b34a0d679f2b9ecbab40ce021e03bb877e1a2fe72eaaf475e6e21"; const PK3: &str = "0x9035d41a8bc11b08c17d0d93d876087958c9d055afe86fce558e3b988d92434769c8d50b0b463708db80c6aae1160c02"; + const PK4: &str = "0x8c0fca2cc70f44188a4b79e5623ac85898f1df479e14a1f4ebb615907810b6fb939c3fb4ba2081b7a5b6e33dc73621d2"; + const PK5: &str = "0x87998b0ea4a8826f03d1985e5a5ce7235bd3a56fb7559b02a55b737f4ebc69b0bf35444de5cf2680cb7eb2283eb62050"; + const PK6: &str = "0xa2af9b128255568e2ee5c42af118cc4301198123d210dbdbf2ca7ec0222f8d491f308e85076b09a2f44a75875cd6fa0f"; // Create a graffiti file in the required format and return a path to the file. fn create_graffiti_file() -> PathBuf { @@ -143,6 +152,9 @@ mod tests { let pk1 = PublicKeyBytes::deserialize(&hex::decode(&PK1[2..]).unwrap()).unwrap(); let pk2 = PublicKeyBytes::deserialize(&hex::decode(&PK2[2..]).unwrap()).unwrap(); let pk3 = PublicKeyBytes::deserialize(&hex::decode(&PK3[2..]).unwrap()).unwrap(); + let pk4 = PublicKeyBytes::deserialize(&hex::decode(&PK4[2..]).unwrap()).unwrap(); + let pk5 = PublicKeyBytes::deserialize(&hex::decode(&PK5[2..]).unwrap()).unwrap(); + let pk6 = PublicKeyBytes::deserialize(&hex::decode(&PK6[2..]).unwrap()).unwrap(); let file_name = temp.into_path().join("graffiti.txt"); @@ -160,6 +172,29 @@ mod tests { graffiti_file .write_all(format!("{}:{}\n", pk3.as_hex_string(), EMPTY_GRAFFITI).as_bytes()) .unwrap(); + + // Test Lines with leading newlines - these empty lines will be skipped + graffiti_file.write_all(b"\n").unwrap(); + graffiti_file.write_all(b" \n").unwrap(); + graffiti_file + .write_all(format!("{}: {}\n", pk4.as_hex_string(), CUSTOM_GRAFFITI4).as_bytes()) + .unwrap(); + + // Test Empty lines between entries - these will be skipped + graffiti_file.write_all(b"\n").unwrap(); + graffiti_file.write_all(b" \n").unwrap(); + graffiti_file.write_all(b"\t\n").unwrap(); + graffiti_file + .write_all(format!("{}: {}\n", pk5.as_hex_string(), CUSTOM_GRAFFITI4).as_bytes()) + .unwrap(); + + // Test Trailing empty lines - these will be skipped + graffiti_file + .write_all(format!("{}: {}\n", pk6.as_hex_string(), CUSTOM_GRAFFITI4).as_bytes()) + .unwrap(); + graffiti_file.write_all(b"\n").unwrap(); + graffiti_file.write_all(b" \n").unwrap(); + graffiti_file.flush().unwrap(); file_name } @@ -172,6 +207,9 @@ mod tests { let pk1 = PublicKeyBytes::deserialize(&hex::decode(&PK1[2..]).unwrap()).unwrap(); let pk2 = PublicKeyBytes::deserialize(&hex::decode(&PK2[2..]).unwrap()).unwrap(); let pk3 = PublicKeyBytes::deserialize(&hex::decode(&PK3[2..]).unwrap()).unwrap(); + let pk4 = PublicKeyBytes::deserialize(&hex::decode(&PK4[2..]).unwrap()).unwrap(); + let pk5 = PublicKeyBytes::deserialize(&hex::decode(&PK5[2..]).unwrap()).unwrap(); + let pk6 = PublicKeyBytes::deserialize(&hex::decode(&PK6[2..]).unwrap()).unwrap(); // Read once gf.read_graffiti_file().unwrap(); @@ -190,6 +228,20 @@ mod tests { GraffitiString::from_str(EMPTY_GRAFFITI).unwrap().into() ); + // Test newline cases - all empty lines should be skipped + assert_eq!( + gf.load_graffiti(&pk4).unwrap().unwrap(), + GraffitiString::from_str(CUSTOM_GRAFFITI4).unwrap().into() + ); + assert_eq!( + gf.load_graffiti(&pk5).unwrap().unwrap(), + GraffitiString::from_str(CUSTOM_GRAFFITI4).unwrap().into() + ); + assert_eq!( + gf.load_graffiti(&pk6).unwrap().unwrap(), + GraffitiString::from_str(CUSTOM_GRAFFITI4).unwrap().into() + ); + // Random pk should return the default graffiti let random_pk = Keypair::random().pk.compress(); assert_eq!( From d49e1be35d3776bb6ce074d9446b6ff3663bf7fe Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 13 Dec 2024 16:44:41 +1100 Subject: [PATCH 05/23] Remove heading that isn't rendered correctly (#6650) * Remove heading that isn't rendered correctly --- book/src/security.md | 1 - 1 file changed, 1 deletion(-) diff --git a/book/src/security.md b/book/src/security.md index 43fa0afc8f..0af57db7f9 100644 --- a/book/src/security.md +++ b/book/src/security.md @@ -1,6 +1,5 @@ # Security -======== Lighthouse takes security seriously. Please see our security policy on GitHub for our PGP key and information on reporting vulnerabilities: - [GitHub: Security Policy](https://github.com/sigp/lighthouse/blob/stable/SECURITY.md) From c92c07ff498721d9eea60db8a5acfde399f47eea Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:33:33 +0800 Subject: [PATCH 06/23] Track beacon processor import result metrics (#6541) * Track beacon processor import result metrics * Update metric name --- .../beacon_chain/src/block_verification.rs | 3 +- beacon_node/network/src/metrics.rs | 62 +++++++++++++++- .../gossip_methods.rs | 70 +++++++++---------- .../network_beacon_processor/sync_methods.rs | 7 +- 4 files changed, 99 insertions(+), 43 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 4c5f53248f..ddb7bb614a 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -92,6 +92,7 @@ use std::fs; use std::io::Write; use std::sync::Arc; use store::{Error as DBError, HotStateSummary, KeyValueStore, StoreOp}; +use strum::AsRefStr; use task_executor::JoinHandle; use types::{ data_column_sidecar::DataColumnSidecarError, BeaconBlockRef, BeaconState, BeaconStateError, @@ -137,7 +138,7 @@ const WRITE_BLOCK_PROCESSING_SSZ: bool = cfg!(feature = "write_ssz_files"); /// /// - The block is malformed/invalid (indicated by all results other than `BeaconChainError`. /// - We encountered an error whilst trying to verify the block (a `BeaconChainError`). -#[derive(Debug)] +#[derive(Debug, AsRefStr)] pub enum BlockError { /// The parent block was unknown. /// diff --git a/beacon_node/network/src/metrics.rs b/beacon_node/network/src/metrics.rs index 4b7e8a50a3..154a59eade 100644 --- a/beacon_node/network/src/metrics.rs +++ b/beacon_node/network/src/metrics.rs @@ -2,7 +2,8 @@ use beacon_chain::{ attestation_verification::Error as AttnError, light_client_finality_update_verification::Error as LightClientFinalityUpdateError, light_client_optimistic_update_verification::Error as LightClientOptimisticUpdateError, - sync_committee_verification::Error as SyncCommitteeError, + sync_committee_verification::Error as SyncCommitteeError, AvailabilityProcessingStatus, + BlockError, }; use fnv::FnvHashMap; use lighthouse_network::{ @@ -11,12 +12,19 @@ use lighthouse_network::{ }; pub use metrics::*; use std::sync::{Arc, LazyLock}; +use strum::AsRefStr; use strum::IntoEnumIterator; use types::EthSpec; pub const SUCCESS: &str = "SUCCESS"; pub const FAILURE: &str = "FAILURE"; +#[derive(Debug, AsRefStr)] +pub(crate) enum BlockSource { + Gossip, + Rpc, +} + pub static BEACON_BLOCK_MESH_PEERS_PER_CLIENT: LazyLock> = LazyLock::new(|| { try_create_int_gauge_vec( @@ -59,6 +67,27 @@ pub static SYNC_COMMITTEE_SUBSCRIPTION_REQUESTS: LazyLock> = ) }); +/* + * Beacon processor + */ +pub static BEACON_PROCESSOR_MISSING_COMPONENTS: LazyLock> = LazyLock::new( + || { + try_create_int_counter_vec( + "beacon_processor_missing_components_total", + "Total number of imported individual block components that resulted in missing components", + &["source", "component"], + ) + }, +); +pub static BEACON_PROCESSOR_IMPORT_ERRORS_PER_TYPE: LazyLock> = + LazyLock::new(|| { + try_create_int_counter_vec( + "beacon_processor_import_errors_total", + "Total number of block components that were not verified", + &["source", "component", "type"], + ) + }); + /* * Gossip processor */ @@ -606,6 +635,37 @@ pub fn register_sync_committee_error(error: &SyncCommitteeError) { inc_counter_vec(&GOSSIP_SYNC_COMMITTEE_ERRORS_PER_TYPE, &[error.as_ref()]); } +pub(crate) fn register_process_result_metrics( + result: &std::result::Result, + source: BlockSource, + block_component: &'static str, +) { + match result { + Ok(status) => match status { + AvailabilityProcessingStatus::Imported { .. } => match source { + BlockSource::Gossip => { + inc_counter(&BEACON_PROCESSOR_GOSSIP_BLOCK_IMPORTED_TOTAL); + } + BlockSource::Rpc => { + inc_counter(&BEACON_PROCESSOR_RPC_BLOCK_IMPORTED_TOTAL); + } + }, + AvailabilityProcessingStatus::MissingComponents { .. } => { + inc_counter_vec( + &BEACON_PROCESSOR_MISSING_COMPONENTS, + &[source.as_ref(), block_component], + ); + } + }, + Err(error) => { + inc_counter_vec( + &BEACON_PROCESSOR_IMPORT_ERRORS_PER_TYPE, + &[source.as_ref(), block_component, error.as_ref()], + ); + } + } +} + pub fn from_result(result: &std::result::Result) -> &str { match result { Ok(_) => SUCCESS, diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index 317bfb104b..4fc83b0923 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -1,5 +1,5 @@ use crate::{ - metrics, + metrics::{self, register_process_result_metrics}, network_beacon_processor::{InvalidBlockStorage, NetworkBeaconProcessor}, service::NetworkMessage, sync::SyncMessage, @@ -915,12 +915,11 @@ impl NetworkBeaconProcessor { let blob_index = verified_blob.id().index; let result = self.chain.process_gossip_blob(verified_blob).await; + register_process_result_metrics(&result, metrics::BlockSource::Gossip, "blob"); match &result { Ok(AvailabilityProcessingStatus::Imported(block_root)) => { - // Note: Reusing block imported metric here - metrics::inc_counter(&metrics::BEACON_PROCESSOR_GOSSIP_BLOCK_IMPORTED_TOTAL); - debug!( + info!( self.log, "Gossipsub blob processed - imported fully available block"; "block_root" => %block_root @@ -989,43 +988,39 @@ impl NetworkBeaconProcessor { let data_column_slot = verified_data_column.slot(); let data_column_index = verified_data_column.id().index; - match self + let result = self .chain .process_gossip_data_columns(vec![verified_data_column], || Ok(())) - .await - { - Ok(availability) => { - match availability { - AvailabilityProcessingStatus::Imported(block_root) => { - // Note: Reusing block imported metric here - metrics::inc_counter( - &metrics::BEACON_PROCESSOR_GOSSIP_BLOCK_IMPORTED_TOTAL, - ); - info!( - self.log, - "Gossipsub data column processed, imported fully available block"; - "block_root" => %block_root - ); - self.chain.recompute_head_at_current_slot().await; + .await; + register_process_result_metrics(&result, metrics::BlockSource::Gossip, "data_column"); - metrics::set_gauge( - &metrics::BEACON_BLOB_DELAY_FULL_VERIFICATION, - processing_start_time.elapsed().as_millis() as i64, - ); - } - AvailabilityProcessingStatus::MissingComponents(slot, block_root) => { - trace!( - self.log, - "Processed data column, waiting for other components"; - "slot" => %slot, - "data_column_index" => %data_column_index, - "block_root" => %block_root, - ); + match result { + Ok(availability) => match availability { + AvailabilityProcessingStatus::Imported(block_root) => { + info!( + self.log, + "Gossipsub data column processed, imported fully available block"; + "block_root" => %block_root + ); + self.chain.recompute_head_at_current_slot().await; - self.attempt_data_column_reconstruction(block_root).await; - } + metrics::set_gauge( + &metrics::BEACON_BLOB_DELAY_FULL_VERIFICATION, + processing_start_time.elapsed().as_millis() as i64, + ); } - } + AvailabilityProcessingStatus::MissingComponents(slot, block_root) => { + trace!( + self.log, + "Processed data column, waiting for other components"; + "slot" => %slot, + "data_column_index" => %data_column_index, + "block_root" => %block_root, + ); + + self.attempt_data_column_reconstruction(block_root).await; + } + }, Err(BlockError::DuplicateFullyImported(_)) => { debug!( self.log, @@ -1467,11 +1462,10 @@ impl NetworkBeaconProcessor { NotifyExecutionLayer::Yes, ) .await; + register_process_result_metrics(&result, metrics::BlockSource::Gossip, "block"); match &result { Ok(AvailabilityProcessingStatus::Imported(block_root)) => { - metrics::inc_counter(&metrics::BEACON_PROCESSOR_GOSSIP_BLOCK_IMPORTED_TOTAL); - if reprocess_tx .try_send(ReprocessQueueMessage::BlockImported { block_root: *block_root, diff --git a/beacon_node/network/src/network_beacon_processor/sync_methods.rs b/beacon_node/network/src/network_beacon_processor/sync_methods.rs index 6c6bb26ee0..817e6b6440 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -1,4 +1,4 @@ -use crate::metrics; +use crate::metrics::{self, register_process_result_metrics}; use crate::network_beacon_processor::{NetworkBeaconProcessor, FUTURE_SLOT_TOLERANCE}; use crate::sync::BatchProcessResult; use crate::sync::{ @@ -163,8 +163,7 @@ impl NetworkBeaconProcessor { NotifyExecutionLayer::Yes, ) .await; - - metrics::inc_counter(&metrics::BEACON_PROCESSOR_RPC_BLOCK_IMPORTED_TOTAL); + register_process_result_metrics(&result, metrics::BlockSource::Rpc, "block"); // RPC block imported, regardless of process type match result.as_ref() { @@ -286,6 +285,7 @@ impl NetworkBeaconProcessor { } let result = self.chain.process_rpc_blobs(slot, block_root, blobs).await; + register_process_result_metrics(&result, metrics::BlockSource::Rpc, "blobs"); match &result { Ok(AvailabilityProcessingStatus::Imported(hash)) => { @@ -343,6 +343,7 @@ impl NetworkBeaconProcessor { .chain .process_rpc_custody_columns(custody_columns) .await; + register_process_result_metrics(&result, metrics::BlockSource::Rpc, "custody_columns"); match &result { Ok(availability) => match availability { From 11e1d5bf148784d1ccbaf8b1023e26b3d0fb4cd1 Mon Sep 17 00:00:00 2001 From: Jun Song <87601811+syjn99@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:43:54 +0900 Subject: [PATCH 07/23] Add CLI flag for HTTP API token path (VC) (#6577) * Add cli flag for HTTP API token path (VC) * Add http_token_path_flag test * Add pre-check for directory case & Fix test utils * Update docs * Apply review: move http_token_path into validator_http_api config * Lint * Make diff lesser to replace PK_FILENAME * Merge branch 'unstable' into feature/cli-token-path * Applt review: help_vc.md Co-authored-by: chonghe <44791194+chong-he@users.noreply.github.com> * Fix help for cli * Fix issues on ci * Merge branch 'unstable' into feature/cli-token-path * Merge branch 'unstable' into feature/cli-token-path * Merge branch 'unstable' into feature/cli-token-path * Merge branch 'unstable' into feature/cli-token-path --- Cargo.lock | 2 ++ book/src/api-vc-auth-header.md | 3 +- book/src/api-vc-endpoints.md | 2 +- book/src/help_vc.md | 4 +++ lighthouse/tests/validator_client.rs | 15 +++++++++ validator_client/http_api/Cargo.toml | 2 ++ validator_client/http_api/src/api_secret.rs | 37 ++++++++++++++++----- validator_client/http_api/src/lib.rs | 10 ++++++ validator_client/http_api/src/test_utils.rs | 9 +++-- validator_client/http_api/src/tests.rs | 8 +++-- validator_client/src/cli.rs | 12 +++++++ validator_client/src/config.rs | 8 ++++- validator_client/src/lib.rs | 2 +- 13 files changed, 96 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9d0d38c1ae..2978a3a19f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9552,6 +9552,8 @@ dependencies = [ "beacon_node_fallback", "bls", "deposit_contract", + "directory", + "dirs", "doppelganger_service", "eth2", "eth2_keystore", diff --git a/book/src/api-vc-auth-header.md b/book/src/api-vc-auth-header.md index adde78270a..feb93724c0 100644 --- a/book/src/api-vc-auth-header.md +++ b/book/src/api-vc-auth-header.md @@ -18,7 +18,8 @@ Authorization: Bearer hGut6B8uEujufDXSmZsT0thnxvdvKFBvh ## Obtaining the API token The API token is stored as a file in the `validators` directory. For most users -this is `~/.lighthouse/{network}/validators/api-token.txt`. Here's an +this is `~/.lighthouse/{network}/validators/api-token.txt`, unless overridden using the +`--http-token-path` CLI parameter. Here's an example using the `cat` command to print the token to the terminal, but any text editor will suffice: diff --git a/book/src/api-vc-endpoints.md b/book/src/api-vc-endpoints.md index 80eba7a059..98605a3dcd 100644 --- a/book/src/api-vc-endpoints.md +++ b/book/src/api-vc-endpoints.md @@ -53,7 +53,7 @@ Example Response Body: } ``` -> Note: The command provided in this documentation links to the API token file. In this documentation, it is assumed that the API token file is located in `/var/lib/lighthouse/validators/api-token.txt`. If your database is saved in another directory, modify the `DATADIR` accordingly. If you are having permission issue with accessing the API token file, you can modify the header to become `-H "Authorization: Bearer $(sudo cat ${DATADIR}/validators/api-token.txt)"`. +> Note: The command provided in this documentation links to the API token file. In this documentation, it is assumed that the API token file is located in `/var/lib/lighthouse/validators/api-token.txt`. If your database is saved in another directory, modify the `DATADIR` accordingly. If you've specified a custom token path using `--http-token-path`, use that path instead. If you are having permission issue with accessing the API token file, you can modify the header to become `-H "Authorization: Bearer $(sudo cat ${DATADIR}/validators/api-token.txt)"`. > As an alternative, you can also provide the API token directly, for example, `-H "Authorization: Bearer hGut6B8uEujufDXSmZsT0thnxvdvKFBvh`. In this case, you obtain the token from the file `api-token.txt` and the command becomes: diff --git a/book/src/help_vc.md b/book/src/help_vc.md index 2cfbfbc857..71e21d68c9 100644 --- a/book/src/help_vc.md +++ b/book/src/help_vc.md @@ -69,6 +69,10 @@ Options: this server (e.g., http://localhost:5062). --http-port Set the listen TCP port for the RESTful HTTP API server. + --http-token-path + Path to file containing the HTTP API token for validator client + authentication. If not specified, defaults to + {validators-dir}/api-token.txt. --log-format Specifies the log format used when emitting logs to the terminal. [possible values: JSON] diff --git a/lighthouse/tests/validator_client.rs b/lighthouse/tests/validator_client.rs index 34fe04cc45..587001f77b 100644 --- a/lighthouse/tests/validator_client.rs +++ b/lighthouse/tests/validator_client.rs @@ -344,6 +344,21 @@ fn http_store_keystore_passwords_in_secrets_dir_present() { .with_config(|config| assert!(config.http_api.store_passwords_in_secrets_dir)); } +#[test] +fn http_token_path_flag() { + let dir = TempDir::new().expect("Unable to create temporary directory"); + CommandLineTest::new() + .flag("http", None) + .flag("http-token-path", dir.path().join("api-token.txt").to_str()) + .run() + .with_config(|config| { + assert_eq!( + config.http_api.http_token_path, + dir.path().join("api-token.txt") + ); + }); +} + // Tests for Metrics flags. #[test] fn metrics_flag() { diff --git a/validator_client/http_api/Cargo.toml b/validator_client/http_api/Cargo.toml index 18e0604ad5..96c836f6f3 100644 --- a/validator_client/http_api/Cargo.toml +++ b/validator_client/http_api/Cargo.toml @@ -13,7 +13,9 @@ account_utils = { workspace = true } bls = { workspace = true } beacon_node_fallback = { workspace = true } deposit_contract = { workspace = true } +directory = { workspace = true } doppelganger_service = { workspace = true } +dirs = { workspace = true } graffiti_file = { workspace = true } eth2 = { workspace = true } eth2_keystore = { workspace = true } diff --git a/validator_client/http_api/src/api_secret.rs b/validator_client/http_api/src/api_secret.rs index afcac477ec..bac54dc8b2 100644 --- a/validator_client/http_api/src/api_secret.rs +++ b/validator_client/http_api/src/api_secret.rs @@ -5,7 +5,7 @@ use std::fs; use std::path::{Path, PathBuf}; use warp::Filter; -/// The name of the file which stores the API token. +/// The default name of the file which stores the API token. pub const PK_FILENAME: &str = "api-token.txt"; pub const PK_LEN: usize = 33; @@ -31,14 +31,32 @@ pub struct ApiSecret { impl ApiSecret { /// If the public key is already on-disk, use it. /// - /// The provided `dir` is a directory containing `PK_FILENAME`. + /// The provided `pk_path` is a path containing API token. /// /// If the public key file is missing on disk, create a new key and /// write it to disk (over-writing any existing files). - pub fn create_or_open>(dir: P) -> Result { - let pk_path = dir.as_ref().join(PK_FILENAME); + pub fn create_or_open>(pk_path: P) -> Result { + let pk_path = pk_path.as_ref(); + + // Check if the path is a directory + if pk_path.is_dir() { + return Err(format!( + "API token path {:?} is a directory, not a file", + pk_path + )); + } if !pk_path.exists() { + // Create parent directories if they don't exist + if let Some(parent) = pk_path.parent() { + std::fs::create_dir_all(parent).map_err(|e| { + format!( + "Unable to create parent directories for {:?}: {:?}", + pk_path, e + ) + })?; + } + let length = PK_LEN; let pk: String = thread_rng() .sample_iter(&Alphanumeric) @@ -47,7 +65,7 @@ impl ApiSecret { .collect(); // Create and write the public key to file with appropriate permissions - create_with_600_perms(&pk_path, pk.to_string().as_bytes()).map_err(|e| { + create_with_600_perms(pk_path, pk.to_string().as_bytes()).map_err(|e| { format!( "Unable to create file with permissions for {:?}: {:?}", pk_path, e @@ -55,13 +73,16 @@ impl ApiSecret { })?; } - let pk = fs::read(&pk_path) - .map_err(|e| format!("cannot read {}: {}", PK_FILENAME, e))? + let pk = fs::read(pk_path) + .map_err(|e| format!("cannot read {}: {}", pk_path.display(), e))? .iter() .map(|&c| char::from(c)) .collect(); - Ok(Self { pk, pk_path }) + Ok(Self { + pk, + pk_path: pk_path.to_path_buf(), + }) } /// Returns the API token. diff --git a/validator_client/http_api/src/lib.rs b/validator_client/http_api/src/lib.rs index b58c7ccec0..f3dab3780c 100644 --- a/validator_client/http_api/src/lib.rs +++ b/validator_client/http_api/src/lib.rs @@ -7,6 +7,7 @@ mod remotekeys; mod tests; pub mod test_utils; +pub use api_secret::PK_FILENAME; use graffiti::{delete_graffiti, get_graffiti, set_graffiti}; @@ -23,6 +24,7 @@ use beacon_node_fallback::CandidateInfo; use create_validator::{ create_validators_mnemonic, create_validators_web3signer, get_voting_password_storage, }; +use directory::{DEFAULT_HARDCODED_NETWORK, DEFAULT_ROOT_DIR, DEFAULT_VALIDATOR_DIR}; use eth2::lighthouse_vc::{ std_types::{AuthResponse, GetFeeRecipientResponse, GetGasLimitResponse}, types::{ @@ -99,10 +101,17 @@ pub struct Config { pub allow_origin: Option, pub allow_keystore_export: bool, pub store_passwords_in_secrets_dir: bool, + pub http_token_path: PathBuf, } impl Default for Config { fn default() -> Self { + let http_token_path = dirs::home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join(DEFAULT_ROOT_DIR) + .join(DEFAULT_HARDCODED_NETWORK) + .join(DEFAULT_VALIDATOR_DIR) + .join(PK_FILENAME); Self { enabled: false, listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), @@ -110,6 +119,7 @@ impl Default for Config { allow_origin: None, allow_keystore_export: false, store_passwords_in_secrets_dir: false, + http_token_path, } } } diff --git a/validator_client/http_api/src/test_utils.rs b/validator_client/http_api/src/test_utils.rs index d033fdbf2d..390095eec7 100644 --- a/validator_client/http_api/src/test_utils.rs +++ b/validator_client/http_api/src/test_utils.rs @@ -1,3 +1,4 @@ +use crate::api_secret::PK_FILENAME; use crate::{ApiSecret, Config as HttpConfig, Context}; use account_utils::validator_definitions::ValidatorDefinitions; use account_utils::{ @@ -73,6 +74,7 @@ impl ApiTester { let validator_dir = tempdir().unwrap(); let secrets_dir = tempdir().unwrap(); + let token_path = tempdir().unwrap().path().join(PK_FILENAME); let validator_defs = ValidatorDefinitions::open_or_create(validator_dir.path()).unwrap(); @@ -85,7 +87,7 @@ impl ApiTester { .await .unwrap(); - let api_secret = ApiSecret::create_or_open(validator_dir.path()).unwrap(); + let api_secret = ApiSecret::create_or_open(token_path).unwrap(); let api_pubkey = api_secret.api_token(); let config = ValidatorStoreConfig { @@ -177,6 +179,7 @@ impl ApiTester { allow_origin: None, allow_keystore_export: true, store_passwords_in_secrets_dir: false, + http_token_path: tempdir().unwrap().path().join(PK_FILENAME), } } @@ -199,8 +202,8 @@ impl ApiTester { } pub fn invalid_token_client(&self) -> ValidatorClientHttpClient { - let tmp = tempdir().unwrap(); - let api_secret = ApiSecret::create_or_open(tmp.path()).unwrap(); + let tmp = tempdir().unwrap().path().join("invalid-token.txt"); + let api_secret = ApiSecret::create_or_open(tmp).unwrap(); let invalid_pubkey = api_secret.api_token(); ValidatorClientHttpClient::new(self.url.clone(), invalid_pubkey).unwrap() } diff --git a/validator_client/http_api/src/tests.rs b/validator_client/http_api/src/tests.rs index 262bb64e69..027b10e246 100644 --- a/validator_client/http_api/src/tests.rs +++ b/validator_client/http_api/src/tests.rs @@ -63,6 +63,7 @@ impl ApiTester { let validator_dir = tempdir().unwrap(); let secrets_dir = tempdir().unwrap(); + let token_path = tempdir().unwrap().path().join("api-token.txt"); let validator_defs = ValidatorDefinitions::open_or_create(validator_dir.path()).unwrap(); @@ -75,7 +76,7 @@ impl ApiTester { .await .unwrap(); - let api_secret = ApiSecret::create_or_open(validator_dir.path()).unwrap(); + let api_secret = ApiSecret::create_or_open(&token_path).unwrap(); let api_pubkey = api_secret.api_token(); let spec = Arc::new(E::default_spec()); @@ -127,6 +128,7 @@ impl ApiTester { allow_origin: None, allow_keystore_export: true, store_passwords_in_secrets_dir: false, + http_token_path: token_path, }, sse_logging_components: None, log, @@ -161,8 +163,8 @@ impl ApiTester { } pub fn invalid_token_client(&self) -> ValidatorClientHttpClient { - let tmp = tempdir().unwrap(); - let api_secret = ApiSecret::create_or_open(tmp.path()).unwrap(); + let tmp = tempdir().unwrap().path().join("invalid-token.txt"); + let api_secret = ApiSecret::create_or_open(tmp).unwrap(); let invalid_pubkey = api_secret.api_token(); ValidatorClientHttpClient::new(self.url.clone(), invalid_pubkey.clone()).unwrap() } diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index 209876f07b..b2d1ebb3c2 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -247,6 +247,18 @@ pub fn cli_app() -> Command { .help_heading(FLAG_HEADER) .display_order(0) ) + .arg( + Arg::new("http-token-path") + .long("http-token-path") + .requires("http") + .value_name("HTTP_TOKEN_PATH") + .help( + "Path to file containing the HTTP API token for validator client authentication. \ + If not specified, defaults to {validators-dir}/api-token.txt." + ) + .action(ArgAction::Set) + .display_order(0) + ) /* Prometheus metrics HTTP server related arguments */ .arg( Arg::new("metrics") diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index abdadeb393..0fecb5202d 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -17,7 +17,7 @@ use std::path::PathBuf; use std::str::FromStr; use std::time::Duration; use types::{Address, GRAFFITI_BYTES_LEN}; -use validator_http_api; +use validator_http_api::{self, PK_FILENAME}; use validator_http_metrics; use validator_store::Config as ValidatorStoreConfig; @@ -314,6 +314,12 @@ impl Config { config.http_api.store_passwords_in_secrets_dir = true; } + if cli_args.get_one::("http-token-path").is_some() { + config.http_api.http_token_path = parse_required(cli_args, "http-token-path") + // For backward compatibility, default to the path under the validator dir if not provided. + .unwrap_or_else(|_| config.validator_dir.join(PK_FILENAME)); + } + /* * Prometheus metrics HTTP server */ diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 2cc22357fb..8ebfe98b15 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -551,7 +551,7 @@ impl ProductionValidatorClient { let (block_service_tx, block_service_rx) = mpsc::channel(channel_capacity); let log = self.context.log(); - let api_secret = ApiSecret::create_or_open(&self.config.validator_dir)?; + let api_secret = ApiSecret::create_or_open(&self.config.http_api.http_token_path)?; self.http_api_listen_addr = if self.config.http_api.enabled { let ctx = Arc::new(validator_http_api::Context { From 86891e6d0f111c318660aaea63ed39c58dd716a5 Mon Sep 17 00:00:00 2001 From: ethDreamer <37123614+ethDreamer@users.noreply.github.com> Date: Sun, 15 Dec 2024 21:43:58 -0800 Subject: [PATCH 08/23] builder gas limit & some refactoring (#6583) * Cache gas_limit * Payload Parameters Refactor * Enforce Proposer Gas Limit * Fixed and Added New Tests * Fix Beacon Chain Tests --- .../beacon_chain/src/execution_payload.rs | 24 +- .../tests/payload_invalidation.rs | 22 +- beacon_node/execution_layer/src/lib.rs | 284 +++++++++++------- .../test_utils/execution_block_generator.rs | 35 ++- .../src/test_utils/mock_builder.rs | 63 +++- .../src/test_utils/mock_execution_layer.rs | 29 +- .../execution_layer/src/test_utils/mod.rs | 5 +- beacon_node/http_api/src/lib.rs | 27 +- .../http_api/tests/interactive_tests.rs | 13 +- beacon_node/http_api/tests/tests.rs | 234 +++++++++++---- consensus/types/src/chain_spec.rs | 27 ++ consensus/types/src/payload.rs | 33 ++ testing/ef_tests/src/cases/fork_choice.rs | 11 +- .../src/test_rig.rs | 34 ++- 14 files changed, 598 insertions(+), 243 deletions(-) diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index f2420eea0d..5e13f0624d 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -14,7 +14,7 @@ use crate::{ }; use execution_layer::{ BlockProposalContents, BlockProposalContentsType, BuilderParams, NewPayloadRequest, - PayloadAttributes, PayloadStatus, + PayloadAttributes, PayloadParameters, PayloadStatus, }; use fork_choice::{InvalidationOperation, PayloadVerificationStatus}; use proto_array::{Block as ProtoBlock, ExecutionStatus}; @@ -375,8 +375,9 @@ pub fn get_execution_payload( let timestamp = compute_timestamp_at_slot(state, state.slot(), spec).map_err(BeaconStateError::from)?; let random = *state.get_randao_mix(current_epoch)?; - let latest_execution_payload_header_block_hash = - state.latest_execution_payload_header()?.block_hash(); + let latest_execution_payload_header = state.latest_execution_payload_header()?; + let latest_execution_payload_header_block_hash = latest_execution_payload_header.block_hash(); + let latest_execution_payload_header_gas_limit = latest_execution_payload_header.gas_limit(); let withdrawals = match state { &BeaconState::Capella(_) | &BeaconState::Deneb(_) | &BeaconState::Electra(_) => { Some(get_expected_withdrawals(state, spec)?.0.into()) @@ -406,6 +407,7 @@ pub fn get_execution_payload( random, proposer_index, latest_execution_payload_header_block_hash, + latest_execution_payload_header_gas_limit, builder_params, withdrawals, parent_beacon_block_root, @@ -443,6 +445,7 @@ pub async fn prepare_execution_payload( random: Hash256, proposer_index: u64, latest_execution_payload_header_block_hash: ExecutionBlockHash, + latest_execution_payload_header_gas_limit: u64, builder_params: BuilderParams, withdrawals: Option>, parent_beacon_block_root: Option, @@ -526,13 +529,20 @@ where parent_beacon_block_root, ); + let target_gas_limit = execution_layer.get_proposer_gas_limit(proposer_index).await; + let payload_parameters = PayloadParameters { + parent_hash, + parent_gas_limit: latest_execution_payload_header_gas_limit, + proposer_gas_limit: target_gas_limit, + payload_attributes: &payload_attributes, + forkchoice_update_params: &forkchoice_update_params, + current_fork: fork, + }; + let block_contents = execution_layer .get_payload( - parent_hash, - &payload_attributes, - forkchoice_update_params, + payload_parameters, builder_params, - fork, &chain.spec, builder_boost_factor, block_production_version, diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index 1325875a27..729d88450f 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -986,10 +986,13 @@ async fn payload_preparation() { // Provide preparation data to the EL for `proposer`. el.update_proposer_preparation( Epoch::new(1), - &[ProposerPreparationData { - validator_index: proposer as u64, - fee_recipient, - }], + [( + &ProposerPreparationData { + validator_index: proposer as u64, + fee_recipient, + }, + &None, + )], ) .await; @@ -1119,10 +1122,13 @@ async fn payload_preparation_before_transition_block() { // Provide preparation data to the EL for `proposer`. el.update_proposer_preparation( Epoch::new(0), - &[ProposerPreparationData { - validator_index: proposer as u64, - fee_recipient, - }], + [( + &ProposerPreparationData { + validator_index: proposer as u64, + fee_recipient, + }, + &None, + )], ) .await; diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 08a00d7bf8..ae0dca9833 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -28,7 +28,7 @@ use sensitive_url::SensitiveUrl; use serde::{Deserialize, Serialize}; use slog::{crit, debug, error, info, warn, Logger}; use slot_clock::SlotClock; -use std::collections::HashMap; +use std::collections::{hash_map::Entry, HashMap}; use std::fmt; use std::future::Future; use std::io::Write; @@ -319,10 +319,52 @@ impl> BlockProposalContents { + pub parent_hash: ExecutionBlockHash, + pub parent_gas_limit: u64, + pub proposer_gas_limit: Option, + pub payload_attributes: &'a PayloadAttributes, + pub forkchoice_update_params: &'a ForkchoiceUpdateParameters, + pub current_fork: ForkName, +} + #[derive(Clone, PartialEq)] pub struct ProposerPreparationDataEntry { update_epoch: Epoch, preparation_data: ProposerPreparationData, + gas_limit: Option, +} + +impl ProposerPreparationDataEntry { + pub fn update(&mut self, updated: Self) -> bool { + let mut changed = false; + // Update `gas_limit` if `updated.gas_limit` is `Some` and: + // - `self.gas_limit` is `None`, or + // - both are `Some` but the values differ. + if let Some(updated_gas_limit) = updated.gas_limit { + if self.gas_limit != Some(updated_gas_limit) { + self.gas_limit = Some(updated_gas_limit); + changed = true; + } + } + + // Update `update_epoch` if it differs + if self.update_epoch != updated.update_epoch { + self.update_epoch = updated.update_epoch; + changed = true; + } + + // Update `preparation_data` if it differs + if self.preparation_data != updated.preparation_data { + self.preparation_data = updated.preparation_data; + changed = true; + } + + changed + } } #[derive(Hash, PartialEq, Eq)] @@ -711,23 +753,29 @@ impl ExecutionLayer { } /// Updates the proposer preparation data provided by validators - pub async fn update_proposer_preparation( - &self, - update_epoch: Epoch, - preparation_data: &[ProposerPreparationData], - ) { + pub async fn update_proposer_preparation<'a, I>(&self, update_epoch: Epoch, proposer_data: I) + where + I: IntoIterator)>, + { let mut proposer_preparation_data = self.proposer_preparation_data().await; - for preparation_entry in preparation_data { + + for (preparation_entry, gas_limit) in proposer_data { let new = ProposerPreparationDataEntry { update_epoch, preparation_data: preparation_entry.clone(), + gas_limit: *gas_limit, }; - let existing = - proposer_preparation_data.insert(preparation_entry.validator_index, new.clone()); - - if existing != Some(new) { - metrics::inc_counter(&metrics::EXECUTION_LAYER_PROPOSER_DATA_UPDATED); + match proposer_preparation_data.entry(preparation_entry.validator_index) { + Entry::Occupied(mut entry) => { + if entry.get_mut().update(new) { + metrics::inc_counter(&metrics::EXECUTION_LAYER_PROPOSER_DATA_UPDATED); + } + } + Entry::Vacant(entry) => { + entry.insert(new); + metrics::inc_counter(&metrics::EXECUTION_LAYER_PROPOSER_DATA_UPDATED); + } } } } @@ -809,6 +857,13 @@ impl ExecutionLayer { } } + pub async fn get_proposer_gas_limit(&self, proposer_index: u64) -> Option { + self.proposer_preparation_data() + .await + .get(&proposer_index) + .and_then(|entry| entry.gas_limit) + } + /// Maps to the `engine_getPayload` JSON-RPC call. /// /// However, it will attempt to call `self.prepare_payload` if it cannot find an existing @@ -818,14 +873,10 @@ impl ExecutionLayer { /// /// The result will be returned from the first node that returns successfully. No more nodes /// will be contacted. - #[allow(clippy::too_many_arguments)] pub async fn get_payload( &self, - parent_hash: ExecutionBlockHash, - payload_attributes: &PayloadAttributes, - forkchoice_update_params: ForkchoiceUpdateParameters, + payload_parameters: PayloadParameters<'_>, builder_params: BuilderParams, - current_fork: ForkName, spec: &ChainSpec, builder_boost_factor: Option, block_production_version: BlockProductionVersion, @@ -833,11 +884,8 @@ impl ExecutionLayer { let payload_result_type = match block_production_version { BlockProductionVersion::V3 => match self .determine_and_fetch_payload( - parent_hash, - payload_attributes, - forkchoice_update_params, + payload_parameters, builder_params, - current_fork, builder_boost_factor, spec, ) @@ -857,25 +905,11 @@ impl ExecutionLayer { &metrics::EXECUTION_LAYER_REQUEST_TIMES, &[metrics::GET_BLINDED_PAYLOAD], ); - self.determine_and_fetch_payload( - parent_hash, - payload_attributes, - forkchoice_update_params, - builder_params, - current_fork, - None, - spec, - ) - .await? + self.determine_and_fetch_payload(payload_parameters, builder_params, None, spec) + .await? } BlockProductionVersion::FullV2 => self - .get_full_payload_with( - parent_hash, - payload_attributes, - forkchoice_update_params, - current_fork, - noop, - ) + .get_full_payload_with(payload_parameters, noop) .await .and_then(GetPayloadResponseType::try_into) .map(ProvenancedPayload::Local)?, @@ -922,17 +956,15 @@ impl ExecutionLayer { async fn fetch_builder_and_local_payloads( &self, builder: &BuilderHttpClient, - parent_hash: ExecutionBlockHash, builder_params: &BuilderParams, - payload_attributes: &PayloadAttributes, - forkchoice_update_params: ForkchoiceUpdateParameters, - current_fork: ForkName, + payload_parameters: PayloadParameters<'_>, ) -> ( Result>>, builder_client::Error>, Result, Error>, ) { let slot = builder_params.slot; let pubkey = &builder_params.pubkey; + let parent_hash = payload_parameters.parent_hash; info!( self.log(), @@ -950,17 +982,12 @@ impl ExecutionLayer { .await }), timed_future(metrics::GET_BLINDED_PAYLOAD_LOCAL, async { - self.get_full_payload_caching( - parent_hash, - payload_attributes, - forkchoice_update_params, - current_fork, - ) - .await - .and_then(|local_result_type| match local_result_type { - GetPayloadResponseType::Full(payload) => Ok(payload), - GetPayloadResponseType::Blinded(_) => Err(Error::PayloadTypeMismatch), - }) + self.get_full_payload_caching(payload_parameters) + .await + .and_then(|local_result_type| match local_result_type { + GetPayloadResponseType::Full(payload) => Ok(payload), + GetPayloadResponseType::Blinded(_) => Err(Error::PayloadTypeMismatch), + }) }) ); @@ -984,26 +1011,17 @@ impl ExecutionLayer { (relay_result, local_result) } - #[allow(clippy::too_many_arguments)] async fn determine_and_fetch_payload( &self, - parent_hash: ExecutionBlockHash, - payload_attributes: &PayloadAttributes, - forkchoice_update_params: ForkchoiceUpdateParameters, + payload_parameters: PayloadParameters<'_>, builder_params: BuilderParams, - current_fork: ForkName, builder_boost_factor: Option, spec: &ChainSpec, ) -> Result>, Error> { let Some(builder) = self.builder() else { // no builder.. return local payload return self - .get_full_payload_caching( - parent_hash, - payload_attributes, - forkchoice_update_params, - current_fork, - ) + .get_full_payload_caching(payload_parameters) .await .and_then(GetPayloadResponseType::try_into) .map(ProvenancedPayload::Local); @@ -1034,26 +1052,15 @@ impl ExecutionLayer { ), } return self - .get_full_payload_caching( - parent_hash, - payload_attributes, - forkchoice_update_params, - current_fork, - ) + .get_full_payload_caching(payload_parameters) .await .and_then(GetPayloadResponseType::try_into) .map(ProvenancedPayload::Local); } + let parent_hash = payload_parameters.parent_hash; let (relay_result, local_result) = self - .fetch_builder_and_local_payloads( - builder.as_ref(), - parent_hash, - &builder_params, - payload_attributes, - forkchoice_update_params, - current_fork, - ) + .fetch_builder_and_local_payloads(builder.as_ref(), &builder_params, payload_parameters) .await; match (relay_result, local_result) { @@ -1118,14 +1125,9 @@ impl ExecutionLayer { ); // check relay payload validity - if let Err(reason) = verify_builder_bid( - &relay, - parent_hash, - payload_attributes, - Some(local.block_number()), - current_fork, - spec, - ) { + if let Err(reason) = + verify_builder_bid(&relay, payload_parameters, Some(local.block_number()), spec) + { // relay payload invalid -> return local metrics::inc_counter_vec( &metrics::EXECUTION_LAYER_GET_PAYLOAD_BUILDER_REJECTIONS, @@ -1202,14 +1204,7 @@ impl ExecutionLayer { "parent_hash" => ?parent_hash, ); - match verify_builder_bid( - &relay, - parent_hash, - payload_attributes, - None, - current_fork, - spec, - ) { + match verify_builder_bid(&relay, payload_parameters, None, spec) { Ok(()) => Ok(ProvenancedPayload::try_from(relay.data.message)?), Err(reason) => { metrics::inc_counter_vec( @@ -1234,32 +1229,28 @@ impl ExecutionLayer { /// Get a full payload and cache its result in the execution layer's payload cache. async fn get_full_payload_caching( &self, - parent_hash: ExecutionBlockHash, - payload_attributes: &PayloadAttributes, - forkchoice_update_params: ForkchoiceUpdateParameters, - current_fork: ForkName, + payload_parameters: PayloadParameters<'_>, ) -> Result, Error> { - self.get_full_payload_with( - parent_hash, - payload_attributes, - forkchoice_update_params, - current_fork, - Self::cache_payload, - ) - .await + self.get_full_payload_with(payload_parameters, Self::cache_payload) + .await } async fn get_full_payload_with( &self, - parent_hash: ExecutionBlockHash, - payload_attributes: &PayloadAttributes, - forkchoice_update_params: ForkchoiceUpdateParameters, - current_fork: ForkName, + payload_parameters: PayloadParameters<'_>, cache_fn: fn( &ExecutionLayer, PayloadContentsRefTuple, ) -> Option>, ) -> Result, Error> { + let PayloadParameters { + parent_hash, + payload_attributes, + forkchoice_update_params, + current_fork, + .. + } = payload_parameters; + self.engine() .request(move |engine| async move { let payload_id = if let Some(id) = engine @@ -1984,6 +1975,10 @@ enum InvalidBuilderPayload { payload: Option, expected: Option, }, + GasLimitMismatch { + payload: u64, + expected: u64, + }, } impl fmt::Display for InvalidBuilderPayload { @@ -2022,19 +2017,51 @@ impl fmt::Display for InvalidBuilderPayload { opt_string(expected) ) } + InvalidBuilderPayload::GasLimitMismatch { payload, expected } => { + write!(f, "payload gas limit was {} not {}", payload, expected) + } } } } +/// Calculate the expected gas limit for a block. +pub fn expected_gas_limit( + parent_gas_limit: u64, + target_gas_limit: u64, + spec: &ChainSpec, +) -> Option { + // Calculate the maximum gas limit difference allowed safely + let max_gas_limit_difference = parent_gas_limit + .checked_div(spec.gas_limit_adjustment_factor) + .and_then(|result| result.checked_sub(1)) + .unwrap_or(0); + + // Adjust the gas limit safely + if target_gas_limit > parent_gas_limit { + let gas_diff = target_gas_limit.saturating_sub(parent_gas_limit); + parent_gas_limit.checked_add(std::cmp::min(gas_diff, max_gas_limit_difference)) + } else { + let gas_diff = parent_gas_limit.saturating_sub(target_gas_limit); + parent_gas_limit.checked_sub(std::cmp::min(gas_diff, max_gas_limit_difference)) + } +} + /// Perform some cursory, non-exhaustive validation of the bid returned from the builder. fn verify_builder_bid( bid: &ForkVersionedResponse>, - parent_hash: ExecutionBlockHash, - payload_attributes: &PayloadAttributes, + payload_parameters: PayloadParameters<'_>, block_number: Option, - current_fork: ForkName, spec: &ChainSpec, ) -> Result<(), Box> { + let PayloadParameters { + parent_hash, + payload_attributes, + current_fork, + parent_gas_limit, + proposer_gas_limit, + .. + } = payload_parameters; + let is_signature_valid = bid.data.verify_signature(spec); let header = &bid.data.message.header(); @@ -2050,6 +2077,8 @@ fn verify_builder_bid( .cloned() .map(|withdrawals| Withdrawals::::from(withdrawals).tree_hash_root()); let payload_withdrawals_root = header.withdrawals_root().ok(); + let expected_gas_limit = proposer_gas_limit + .and_then(|target_gas_limit| expected_gas_limit(parent_gas_limit, target_gas_limit, spec)); if header.parent_hash() != parent_hash { Err(Box::new(InvalidBuilderPayload::ParentHash { @@ -2086,6 +2115,14 @@ fn verify_builder_bid( payload: payload_withdrawals_root, expected: expected_withdrawals_root, })) + } else if expected_gas_limit + .map(|gas_limit| header.gas_limit() != gas_limit) + .unwrap_or(false) + { + Err(Box::new(InvalidBuilderPayload::GasLimitMismatch { + payload: header.gas_limit(), + expected: expected_gas_limit.unwrap_or(0), + })) } else { Ok(()) } @@ -2138,6 +2175,27 @@ mod test { .await; } + #[tokio::test] + async fn test_expected_gas_limit() { + let spec = ChainSpec::mainnet(); + assert_eq!( + expected_gas_limit(30_000_000, 30_000_000, &spec), + Some(30_000_000) + ); + assert_eq!( + expected_gas_limit(30_000_000, 40_000_000, &spec), + Some(30_029_295) + ); + assert_eq!( + expected_gas_limit(30_029_295, 40_000_000, &spec), + Some(30_058_619) + ); + assert_eq!( + expected_gas_limit(30_058_619, 30_000_000, &spec), + Some(30_029_266) + ); + } + #[tokio::test] async fn test_forked_terminal_block() { let runtime = TestRuntime::default(); diff --git a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs index 4deb91e056..4fab7150ce 100644 --- a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs +++ b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs @@ -28,8 +28,8 @@ use super::DEFAULT_TERMINAL_BLOCK; const TEST_BLOB_BUNDLE: &[u8] = include_bytes!("fixtures/mainnet/test_blobs_bundle.ssz"); -const GAS_LIMIT: u64 = 16384; -const GAS_USED: u64 = GAS_LIMIT - 1; +pub const DEFAULT_GAS_LIMIT: u64 = 30_000_000; +const GAS_USED: u64 = DEFAULT_GAS_LIMIT - 1; #[derive(Clone, Debug, PartialEq)] #[allow(clippy::large_enum_variant)] // This struct is only for testing. @@ -38,6 +38,10 @@ pub enum Block { PoS(ExecutionPayload), } +pub fn mock_el_extra_data() -> types::VariableList { + "block gen was here".as_bytes().to_vec().into() +} + impl Block { pub fn block_number(&self) -> u64 { match self { @@ -67,6 +71,13 @@ impl Block { } } + pub fn gas_limit(&self) -> u64 { + match self { + Block::PoW(_) => DEFAULT_GAS_LIMIT, + Block::PoS(payload) => payload.gas_limit(), + } + } + pub fn as_execution_block(&self, total_difficulty: Uint256) -> ExecutionBlock { match self { Block::PoW(block) => ExecutionBlock { @@ -570,10 +581,10 @@ impl ExecutionBlockGenerator { logs_bloom: vec![0; 256].into(), prev_randao: pa.prev_randao, block_number: parent.block_number() + 1, - gas_limit: GAS_LIMIT, + gas_limit: DEFAULT_GAS_LIMIT, gas_used: GAS_USED, timestamp: pa.timestamp, - extra_data: "block gen was here".as_bytes().to_vec().into(), + extra_data: mock_el_extra_data::(), base_fee_per_gas: Uint256::from(1u64), block_hash: ExecutionBlockHash::zero(), transactions: vec![].into(), @@ -587,10 +598,10 @@ impl ExecutionBlockGenerator { logs_bloom: vec![0; 256].into(), prev_randao: pa.prev_randao, block_number: parent.block_number() + 1, - gas_limit: GAS_LIMIT, + gas_limit: DEFAULT_GAS_LIMIT, gas_used: GAS_USED, timestamp: pa.timestamp, - extra_data: "block gen was here".as_bytes().to_vec().into(), + extra_data: mock_el_extra_data::(), base_fee_per_gas: Uint256::from(1u64), block_hash: ExecutionBlockHash::zero(), transactions: vec![].into(), @@ -603,10 +614,10 @@ impl ExecutionBlockGenerator { logs_bloom: vec![0; 256].into(), prev_randao: pa.prev_randao, block_number: parent.block_number() + 1, - gas_limit: GAS_LIMIT, + gas_limit: DEFAULT_GAS_LIMIT, gas_used: GAS_USED, timestamp: pa.timestamp, - extra_data: "block gen was here".as_bytes().to_vec().into(), + extra_data: mock_el_extra_data::(), base_fee_per_gas: Uint256::from(1u64), block_hash: ExecutionBlockHash::zero(), transactions: vec![].into(), @@ -623,10 +634,10 @@ impl ExecutionBlockGenerator { logs_bloom: vec![0; 256].into(), prev_randao: pa.prev_randao, block_number: parent.block_number() + 1, - gas_limit: GAS_LIMIT, + gas_limit: DEFAULT_GAS_LIMIT, gas_used: GAS_USED, timestamp: pa.timestamp, - extra_data: "block gen was here".as_bytes().to_vec().into(), + extra_data: mock_el_extra_data::(), base_fee_per_gas: Uint256::from(1u64), block_hash: ExecutionBlockHash::zero(), transactions: vec![].into(), @@ -642,10 +653,10 @@ impl ExecutionBlockGenerator { logs_bloom: vec![0; 256].into(), prev_randao: pa.prev_randao, block_number: parent.block_number() + 1, - gas_limit: GAS_LIMIT, + gas_limit: DEFAULT_GAS_LIMIT, gas_used: GAS_USED, timestamp: pa.timestamp, - extra_data: "block gen was here".as_bytes().to_vec().into(), + extra_data: mock_el_extra_data::(), base_fee_per_gas: Uint256::from(1u64), block_hash: ExecutionBlockHash::zero(), transactions: vec![].into(), diff --git a/beacon_node/execution_layer/src/test_utils/mock_builder.rs b/beacon_node/execution_layer/src/test_utils/mock_builder.rs index 341daedbc8..879b54eb07 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_builder.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_builder.rs @@ -1,5 +1,5 @@ use crate::test_utils::{DEFAULT_BUILDER_PAYLOAD_VALUE_WEI, DEFAULT_JWT_SECRET}; -use crate::{Config, ExecutionLayer, PayloadAttributes}; +use crate::{Config, ExecutionLayer, PayloadAttributes, PayloadParameters}; use eth2::types::{BlobsBundle, BlockId, StateId, ValidatorId}; use eth2::{BeaconNodeHttpClient, Timeouts, CONSENSUS_VERSION_HEADER}; use fork_choice::ForkchoiceUpdateParameters; @@ -54,6 +54,10 @@ impl Operation { } } +pub fn mock_builder_extra_data() -> types::VariableList { + "mock_builder".as_bytes().to_vec().into() +} + #[derive(Debug)] // We don't use the string value directly, but it's used in the Debug impl which is required by `warp::reject::Reject`. struct Custom(#[allow(dead_code)] String); @@ -72,6 +76,8 @@ pub trait BidStuff { fn set_withdrawals_root(&mut self, withdrawals_root: Hash256); fn sign_builder_message(&mut self, sk: &SecretKey, spec: &ChainSpec) -> Signature; + + fn stamp_payload(&mut self); } impl BidStuff for BuilderBid { @@ -203,6 +209,29 @@ impl BidStuff for BuilderBid { let message = self.signing_root(domain); sk.sign(message) } + + // this helps differentiate a builder block from a regular block + fn stamp_payload(&mut self) { + let extra_data = mock_builder_extra_data::(); + match self.to_mut().header_mut() { + ExecutionPayloadHeaderRefMut::Bellatrix(header) => { + header.extra_data = extra_data; + header.block_hash = ExecutionBlockHash::from_root(header.tree_hash_root()); + } + ExecutionPayloadHeaderRefMut::Capella(header) => { + header.extra_data = extra_data; + header.block_hash = ExecutionBlockHash::from_root(header.tree_hash_root()); + } + ExecutionPayloadHeaderRefMut::Deneb(header) => { + header.extra_data = extra_data; + header.block_hash = ExecutionBlockHash::from_root(header.tree_hash_root()); + } + ExecutionPayloadHeaderRefMut::Electra(header) => { + header.extra_data = extra_data; + header.block_hash = ExecutionBlockHash::from_root(header.tree_hash_root()); + } + } + } } #[derive(Clone)] @@ -286,6 +315,7 @@ impl MockBuilder { while let Some(op) = guard.pop() { op.apply(bid); } + bid.stamp_payload(); } } @@ -413,11 +443,12 @@ pub fn serve( let block = head.data.message(); let head_block_root = block.tree_hash_root(); - let head_execution_hash = block + let head_execution_payload = block .body() .execution_payload() - .map_err(|_| reject("pre-merge block"))? - .block_hash(); + .map_err(|_| reject("pre-merge block"))?; + let head_execution_hash = head_execution_payload.block_hash(); + let head_gas_limit = head_execution_payload.gas_limit(); if head_execution_hash != parent_hash { return Err(reject("head mismatch")); } @@ -529,14 +560,24 @@ pub fn serve( finalized_hash: Some(finalized_execution_hash), }; + let proposer_gas_limit = builder + .val_registration_cache + .read() + .get(&pubkey) + .map(|v| v.message.gas_limit); + + let payload_parameters = PayloadParameters { + parent_hash: head_execution_hash, + parent_gas_limit: head_gas_limit, + proposer_gas_limit, + payload_attributes: &payload_attributes, + forkchoice_update_params: &forkchoice_update_params, + current_fork: fork, + }; + let payload_response_type = builder .el - .get_full_payload_caching( - head_execution_hash, - &payload_attributes, - forkchoice_update_params, - fork, - ) + .get_full_payload_caching(payload_parameters) .await .map_err(|_| reject("couldn't get payload"))?; @@ -648,8 +689,6 @@ pub fn serve( } }; - message.set_gas_limit(cached_data.gas_limit); - builder.apply_operations(&mut message); let mut signature = diff --git a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs index a9f1313e46..48372a39be 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs @@ -90,6 +90,7 @@ impl MockExecutionLayer { }; let parent_hash = latest_execution_block.block_hash(); + let parent_gas_limit = latest_execution_block.gas_limit(); let block_number = latest_execution_block.block_number() + 1; let timestamp = block_number; let prev_randao = Hash256::from_low_u64_be(block_number); @@ -131,14 +132,20 @@ impl MockExecutionLayer { let payload_attributes = PayloadAttributes::new(timestamp, prev_randao, suggested_fee_recipient, None, None); + let payload_parameters = PayloadParameters { + parent_hash, + parent_gas_limit, + proposer_gas_limit: None, + payload_attributes: &payload_attributes, + forkchoice_update_params: &forkchoice_update_params, + current_fork: ForkName::Bellatrix, + }; + let block_proposal_content_type = self .el .get_payload( - parent_hash, - &payload_attributes, - forkchoice_update_params, + payload_parameters, builder_params, - ForkName::Bellatrix, &self.spec, None, BlockProductionVersion::FullV2, @@ -171,14 +178,20 @@ impl MockExecutionLayer { let payload_attributes = PayloadAttributes::new(timestamp, prev_randao, suggested_fee_recipient, None, None); + let payload_parameters = PayloadParameters { + parent_hash, + parent_gas_limit, + proposer_gas_limit: None, + payload_attributes: &payload_attributes, + forkchoice_update_params: &forkchoice_update_params, + current_fork: ForkName::Bellatrix, + }; + let block_proposal_content_type = self .el .get_payload( - parent_hash, - &payload_attributes, - forkchoice_update_params, + payload_parameters, builder_params, - ForkName::Bellatrix, &self.spec, None, BlockProductionVersion::BlindedV2, diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index 1e71fde255..faf6d4ef0b 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -25,12 +25,13 @@ use types::{EthSpec, ExecutionBlockHash, Uint256}; use warp::{http::StatusCode, Filter, Rejection}; use crate::EngineCapabilities; +pub use execution_block_generator::DEFAULT_GAS_LIMIT; pub use execution_block_generator::{ generate_blobs, generate_genesis_block, generate_genesis_header, generate_pow_block, - static_valid_tx, Block, ExecutionBlockGenerator, + mock_el_extra_data, static_valid_tx, Block, ExecutionBlockGenerator, }; pub use hook::Hook; -pub use mock_builder::{MockBuilder, Operation}; +pub use mock_builder::{mock_builder_extra_data, MockBuilder, Operation}; pub use mock_execution_layer::MockExecutionLayer; pub const DEFAULT_TERMINAL_DIFFICULTY: u64 = 6400; diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index fe05f55a01..23d177da78 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -3704,7 +3704,10 @@ pub fn serve( ); execution_layer - .update_proposer_preparation(current_epoch, &preparation_data) + .update_proposer_preparation( + current_epoch, + preparation_data.iter().map(|data| (data, &None)), + ) .await; chain @@ -3762,7 +3765,7 @@ pub fn serve( let spec = &chain.spec; let (preparation_data, filtered_registration_data): ( - Vec, + Vec<(ProposerPreparationData, Option)>, Vec, ) = register_val_data .into_iter() @@ -3792,12 +3795,15 @@ pub fn serve( // Filter out validators who are not 'active' or 'pending'. is_active_or_pending.then_some({ ( - ProposerPreparationData { - validator_index: validator_index as u64, - fee_recipient: register_data - .message - .fee_recipient, - }, + ( + ProposerPreparationData { + validator_index: validator_index as u64, + fee_recipient: register_data + .message + .fee_recipient, + }, + Some(register_data.message.gas_limit), + ), register_data, ) }) @@ -3807,7 +3813,10 @@ pub fn serve( // Update the prepare beacon proposer cache based on this request. execution_layer - .update_proposer_preparation(current_epoch, &preparation_data) + .update_proposer_preparation( + current_epoch, + preparation_data.iter().map(|(data, limit)| (data, limit)), + ) .await; // Call prepare beacon proposer blocking with the latest update in order to make diff --git a/beacon_node/http_api/tests/interactive_tests.rs b/beacon_node/http_api/tests/interactive_tests.rs index c3ed334782..627b0d0b17 100644 --- a/beacon_node/http_api/tests/interactive_tests.rs +++ b/beacon_node/http_api/tests/interactive_tests.rs @@ -447,9 +447,14 @@ pub async fn proposer_boost_re_org_test( // Send proposer preparation data for all validators. let proposer_preparation_data = all_validators .iter() - .map(|i| ProposerPreparationData { - validator_index: *i as u64, - fee_recipient: Address::from_low_u64_be(*i as u64), + .map(|i| { + ( + ProposerPreparationData { + validator_index: *i as u64, + fee_recipient: Address::from_low_u64_be(*i as u64), + }, + None, + ) }) .collect::>(); harness @@ -459,7 +464,7 @@ pub async fn proposer_boost_re_org_test( .unwrap() .update_proposer_preparation( head_slot.epoch(E::slots_per_epoch()) + 1, - &proposer_preparation_data, + proposer_preparation_data.iter().map(|(a, b)| (a, b)), ) .await; diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 940f3ae9c0..080a393b4d 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -13,8 +13,10 @@ use eth2::{ Error::ServerMessage, StatusCode, Timeouts, }; +use execution_layer::expected_gas_limit; use execution_layer::test_utils::{ - MockBuilder, Operation, DEFAULT_BUILDER_PAYLOAD_VALUE_WEI, DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI, + mock_builder_extra_data, mock_el_extra_data, MockBuilder, Operation, + DEFAULT_BUILDER_PAYLOAD_VALUE_WEI, DEFAULT_GAS_LIMIT, DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI, }; use futures::stream::{Stream, StreamExt}; use futures::FutureExt; @@ -348,7 +350,6 @@ impl ApiTester { let bls_to_execution_change = harness.make_bls_to_execution_change(4, Address::zero()); let chain = harness.chain.clone(); - let log = test_logger(); let ApiServer { @@ -3755,7 +3756,11 @@ impl ApiTester { self } - pub async fn test_post_validator_register_validator(self) -> Self { + async fn generate_validator_registration_data( + &self, + fee_recipient_generator: impl Fn(usize) -> Address, + gas_limit: u64, + ) -> (Vec, Vec
) { let mut registrations = vec![]; let mut fee_recipients = vec![]; @@ -3766,15 +3771,13 @@ impl ApiTester { epoch: genesis_epoch, }; - let expected_gas_limit = 11_111_111; - for (val_index, keypair) in self.validator_keypairs().iter().enumerate() { let pubkey = keypair.pk.compress(); - let fee_recipient = Address::from_low_u64_be(val_index as u64); + let fee_recipient = fee_recipient_generator(val_index); let data = ValidatorRegistrationData { fee_recipient, - gas_limit: expected_gas_limit, + gas_limit, timestamp: 0, pubkey, }; @@ -3797,6 +3800,17 @@ impl ApiTester { registrations.push(signed); } + (registrations, fee_recipients) + } + + pub async fn test_post_validator_register_validator(self) -> Self { + let (registrations, fee_recipients) = self + .generate_validator_registration_data( + |val_index| Address::from_low_u64_be(val_index as u64), + DEFAULT_GAS_LIMIT, + ) + .await; + self.client .post_validator_register_validator(®istrations) .await @@ -3811,14 +3825,22 @@ impl ApiTester { .zip(fee_recipients.into_iter()) .enumerate() { - let actual = self + let actual_fee_recipient = self .chain .execution_layer .as_ref() .unwrap() .get_suggested_fee_recipient(val_index as u64) .await; - assert_eq!(actual, fee_recipient); + let actual_gas_limit = self + .chain + .execution_layer + .as_ref() + .unwrap() + .get_proposer_gas_limit(val_index as u64) + .await; + assert_eq!(actual_fee_recipient, fee_recipient); + assert_eq!(actual_gas_limit, Some(DEFAULT_GAS_LIMIT)); } self @@ -3839,46 +3861,12 @@ impl ApiTester { ) .await; - let mut registrations = vec![]; - let mut fee_recipients = vec![]; - - let genesis_epoch = self.chain.spec.genesis_slot.epoch(E::slots_per_epoch()); - let fork = Fork { - current_version: self.chain.spec.genesis_fork_version, - previous_version: self.chain.spec.genesis_fork_version, - epoch: genesis_epoch, - }; - - let expected_gas_limit = 11_111_111; - - for (val_index, keypair) in self.validator_keypairs().iter().enumerate() { - let pubkey = keypair.pk.compress(); - let fee_recipient = Address::from_low_u64_be(val_index as u64); - - let data = ValidatorRegistrationData { - fee_recipient, - gas_limit: expected_gas_limit, - timestamp: 0, - pubkey, - }; - - let domain = self.chain.spec.get_domain( - genesis_epoch, - Domain::ApplicationMask(ApplicationDomain::Builder), - &fork, - Hash256::zero(), - ); - let message = data.signing_root(domain); - let signature = keypair.sk.sign(message); - - let signed = SignedValidatorRegistrationData { - message: data, - signature, - }; - - fee_recipients.push(fee_recipient); - registrations.push(signed); - } + let (registrations, fee_recipients) = self + .generate_validator_registration_data( + |val_index| Address::from_low_u64_be(val_index as u64), + DEFAULT_GAS_LIMIT, + ) + .await; self.client .post_validator_register_validator(®istrations) @@ -3911,6 +3899,47 @@ impl ApiTester { self } + pub async fn test_post_validator_register_validator_higher_gas_limit(&self) { + let (registrations, fee_recipients) = self + .generate_validator_registration_data( + |val_index| Address::from_low_u64_be(val_index as u64), + DEFAULT_GAS_LIMIT + 10_000_000, + ) + .await; + + self.client + .post_validator_register_validator(®istrations) + .await + .unwrap(); + + for (val_index, (_, fee_recipient)) in self + .chain + .head_snapshot() + .beacon_state + .validators() + .into_iter() + .zip(fee_recipients.into_iter()) + .enumerate() + { + let actual_fee_recipient = self + .chain + .execution_layer + .as_ref() + .unwrap() + .get_suggested_fee_recipient(val_index as u64) + .await; + let actual_gas_limit = self + .chain + .execution_layer + .as_ref() + .unwrap() + .get_proposer_gas_limit(val_index as u64) + .await; + assert_eq!(actual_fee_recipient, fee_recipient); + assert_eq!(actual_gas_limit, Some(DEFAULT_GAS_LIMIT + 10_000_000)); + } + } + pub async fn test_post_validator_liveness_epoch(self) -> Self { let epoch = self.chain.epoch().unwrap(); let head_state = self.chain.head_beacon_state_cloned(); @@ -4031,7 +4060,7 @@ impl ApiTester { let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); assert_eq!(payload.fee_recipient(), expected_fee_recipient); - assert_eq!(payload.gas_limit(), 11_111_111); + assert_eq!(payload.gas_limit(), DEFAULT_GAS_LIMIT); self } @@ -4058,7 +4087,8 @@ impl ApiTester { let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); assert_eq!(payload.fee_recipient(), expected_fee_recipient); - assert_eq!(payload.gas_limit(), 16_384); + // This is the graffiti of the mock execution layer, not the builder. + assert_eq!(payload.extra_data(), mock_el_extra_data::()); self } @@ -4085,7 +4115,7 @@ impl ApiTester { let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); assert_eq!(payload.fee_recipient(), expected_fee_recipient); - assert_eq!(payload.gas_limit(), 11_111_111); + assert_eq!(payload.gas_limit(), DEFAULT_GAS_LIMIT); self } @@ -4109,7 +4139,7 @@ impl ApiTester { let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); assert_eq!(payload.fee_recipient(), expected_fee_recipient); - assert_eq!(payload.gas_limit(), 11_111_111); + assert_eq!(payload.gas_limit(), DEFAULT_GAS_LIMIT); // If this cache is empty, it indicates fallback was not used, so the payload came from the // mock builder. @@ -4126,10 +4156,16 @@ impl ApiTester { pub async fn test_payload_accepts_mutated_gas_limit(self) -> Self { // Mutate gas limit. + let builder_limit = expected_gas_limit( + DEFAULT_GAS_LIMIT, + DEFAULT_GAS_LIMIT + 10_000_000, + self.chain.spec.as_ref(), + ) + .expect("calculate expected gas limit"); self.mock_builder .as_ref() .unwrap() - .add_operation(Operation::GasLimit(30_000_000)); + .add_operation(Operation::GasLimit(builder_limit as usize)); let slot = self.chain.slot().unwrap(); let epoch = self.chain.epoch().unwrap(); @@ -4149,7 +4185,7 @@ impl ApiTester { let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); assert_eq!(payload.fee_recipient(), expected_fee_recipient); - assert_eq!(payload.gas_limit(), 30_000_000); + assert_eq!(payload.gas_limit(), builder_limit); // This cache should not be populated because fallback should not have been used. assert!(self @@ -4159,6 +4195,49 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_none()); + // Another way is to check for the extra data of the mock builder + assert_eq!(payload.extra_data(), mock_builder_extra_data::()); + + self + } + + pub async fn test_builder_payload_rejected_when_gas_limit_incorrect(self) -> Self { + self.test_post_validator_register_validator_higher_gas_limit() + .await; + + // Mutate gas limit. + self.mock_builder + .as_ref() + .unwrap() + .add_operation(Operation::GasLimit(1)); + + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + + let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; + + let payload: BlindedPayload = self + .client + .get_validator_blinded_blocks::(slot, &randao_reveal, None) + .await + .unwrap() + .data + .body() + .execution_payload() + .unwrap() + .into(); + + // If this cache is populated, it indicates fallback to the local EE was correctly used. + assert!(self + .chain + .execution_layer + .as_ref() + .unwrap() + .get_payload_by_root(&payload.tree_hash_root()) + .is_some()); + // another way is to check for the extra data of the local EE + assert_eq!(payload.extra_data(), mock_el_extra_data::()); + self } @@ -4232,6 +4311,9 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_none()); + // Another way is to check for the extra data of the mock builder + assert_eq!(payload.extra_data(), mock_builder_extra_data::()); + self } @@ -4315,6 +4397,9 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_some()); + // another way is to check for the extra data of the local EE + assert_eq!(payload.extra_data(), mock_el_extra_data::()); + self } @@ -4404,6 +4489,9 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_some()); + // another way is to check for the extra data of the local EE + assert_eq!(payload.extra_data(), mock_el_extra_data::()); + self } @@ -4491,6 +4579,9 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_some()); + // another way is to check for the extra data of the local EE + assert_eq!(payload.extra_data(), mock_el_extra_data::()); + self } @@ -4577,6 +4668,9 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_some()); + // another way is to check for the extra data of the local EE + assert_eq!(payload.extra_data(), mock_el_extra_data::()); + self } @@ -4647,6 +4741,9 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_some()); + // another way is to check for the extra data of the local EE + assert_eq!(payload.extra_data(), mock_el_extra_data::()); + self } @@ -4707,6 +4804,9 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_some()); + // another way is to check for the extra data of the local EE + assert_eq!(payload.extra_data(), mock_el_extra_data::()); + self } @@ -4780,6 +4880,8 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_none()); + // Another way is to check for the extra data of the mock builder + assert_eq!(payload.extra_data(), mock_builder_extra_data::()); // Without proposing, advance into the next slot, this should make us cross the threshold // number of skips, causing us to use the fallback. @@ -4809,6 +4911,8 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_some()); + // another way is to check for the extra data of the local EE + assert_eq!(payload.extra_data(), mock_el_extra_data::()); self } @@ -4915,6 +5019,8 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_some()); + // another way is to check for the extra data of the local EE + assert_eq!(payload.extra_data(), mock_el_extra_data::()); // Fill another epoch with blocks, should be enough to finalize. (Sneaky plus 1 because this // scenario starts at an epoch boundary). @@ -4954,6 +5060,8 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_none()); + // Another way is to check for the extra data of the mock builder + assert_eq!(payload.extra_data(), mock_builder_extra_data::()); self } @@ -5072,6 +5180,8 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_some()); + // another way is to check for the extra data of the local EE + assert_eq!(payload.extra_data(), mock_el_extra_data::()); self } @@ -5149,6 +5259,9 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_none()); + // Another way is to check for the extra data of the mock builder + assert_eq!(payload.extra_data(), mock_builder_extra_data::()); + self } @@ -5214,6 +5327,9 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_some()); + // another way is to check for the extra data of the local EE + assert_eq!(payload.extra_data(), mock_el_extra_data::()); + self } @@ -5279,6 +5395,9 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_some()); + // another way is to check for the extra data of the local EE + assert_eq!(payload.extra_data(), mock_el_extra_data::()); + self } @@ -5343,6 +5462,9 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_none()); + // Another way is to check for the extra data of the mock builder + assert_eq!(payload.extra_data(), mock_builder_extra_data::()); + self } @@ -6682,6 +6804,8 @@ async fn post_validator_register_valid_v3() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn post_validator_register_gas_limit_mutation() { ApiTester::new_mev_tester() + .await + .test_builder_payload_rejected_when_gas_limit_incorrect() .await .test_payload_accepts_mutated_gas_limit() .await; diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index 79dcc65ea3..0b33a76ff1 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -127,6 +127,11 @@ pub struct ChainSpec { pub deposit_network_id: u64, pub deposit_contract_address: Address, + /* + * Execution Specs + */ + pub gas_limit_adjustment_factor: u64, + /* * Altair hard fork params */ @@ -715,6 +720,11 @@ impl ChainSpec { .parse() .expect("chain spec deposit contract address"), + /* + * Execution Specs + */ + gas_limit_adjustment_factor: 1024, + /* * Altair hard fork params */ @@ -1029,6 +1039,11 @@ impl ChainSpec { .parse() .expect("chain spec deposit contract address"), + /* + * Execution Specs + */ + gas_limit_adjustment_factor: 1024, + /* * Altair hard fork params */ @@ -1285,6 +1300,10 @@ pub struct Config { #[serde(with = "serde_utils::address_hex")] deposit_contract_address: Address, + #[serde(default = "default_gas_limit_adjustment_factor")] + #[serde(with = "serde_utils::quoted_u64")] + gas_limit_adjustment_factor: u64, + #[serde(default = "default_gossip_max_size")] #[serde(with = "serde_utils::quoted_u64")] gossip_max_size: u64, @@ -1407,6 +1426,10 @@ const fn default_max_per_epoch_activation_churn_limit() -> u64 { 8 } +const fn default_gas_limit_adjustment_factor() -> u64 { + 1024 +} + const fn default_gossip_max_size() -> u64 { 10485760 } @@ -1659,6 +1682,8 @@ impl Config { deposit_network_id: spec.deposit_network_id, deposit_contract_address: spec.deposit_contract_address, + gas_limit_adjustment_factor: spec.gas_limit_adjustment_factor, + gossip_max_size: spec.gossip_max_size, max_request_blocks: spec.max_request_blocks, min_epochs_for_block_requests: spec.min_epochs_for_block_requests, @@ -1733,6 +1758,7 @@ impl Config { deposit_chain_id, deposit_network_id, deposit_contract_address, + gas_limit_adjustment_factor, gossip_max_size, min_epochs_for_block_requests, max_chunk_size, @@ -1794,6 +1820,7 @@ impl Config { deposit_chain_id, deposit_network_id, deposit_contract_address, + gas_limit_adjustment_factor, terminal_total_difficulty, terminal_block_hash, terminal_block_hash_activation_epoch, diff --git a/consensus/types/src/payload.rs b/consensus/types/src/payload.rs index b82a897da5..e68801840a 100644 --- a/consensus/types/src/payload.rs +++ b/consensus/types/src/payload.rs @@ -32,6 +32,7 @@ pub trait ExecPayload: Debug + Clone + PartialEq + Hash + TreeHash + fn prev_randao(&self) -> Hash256; fn block_number(&self) -> u64; fn timestamp(&self) -> u64; + fn extra_data(&self) -> VariableList; fn block_hash(&self) -> ExecutionBlockHash; fn fee_recipient(&self) -> Address; fn gas_limit(&self) -> u64; @@ -225,6 +226,13 @@ impl ExecPayload for FullPayload { }) } + fn extra_data<'a>(&'a self) -> VariableList { + map_full_payload_ref!(&'a _, self.to_ref(), move |payload, cons| { + cons(payload); + payload.execution_payload.extra_data.clone() + }) + } + fn block_hash<'a>(&'a self) -> ExecutionBlockHash { map_full_payload_ref!(&'a _, self.to_ref(), move |payload, cons| { cons(payload); @@ -357,6 +365,13 @@ impl ExecPayload for FullPayloadRef<'_, E> { }) } + fn extra_data<'a>(&'a self) -> VariableList { + map_full_payload_ref!(&'a _, self, move |payload, cons| { + cons(payload); + payload.execution_payload.extra_data.clone() + }) + } + fn block_hash<'a>(&'a self) -> ExecutionBlockHash { map_full_payload_ref!(&'a _, self, move |payload, cons| { cons(payload); @@ -542,6 +557,13 @@ impl ExecPayload for BlindedPayload { }) } + fn extra_data<'a>(&'a self) -> VariableList::MaxExtraDataBytes> { + map_blinded_payload_ref!(&'a _, self.to_ref(), move |payload, cons| { + cons(payload); + payload.execution_payload_header.extra_data.clone() + }) + } + fn block_hash<'a>(&'a self) -> ExecutionBlockHash { map_blinded_payload_ref!(&'a _, self.to_ref(), move |payload, cons| { cons(payload); @@ -643,6 +665,13 @@ impl<'b, E: EthSpec> ExecPayload for BlindedPayloadRef<'b, E> { }) } + fn extra_data<'a>(&'a self) -> VariableList::MaxExtraDataBytes> { + map_blinded_payload_ref!(&'a _, self, move |payload, cons| { + cons(payload); + payload.execution_payload_header.extra_data.clone() + }) + } + fn block_hash<'a>(&'a self) -> ExecutionBlockHash { map_blinded_payload_ref!(&'a _, self, move |payload, cons| { cons(payload); @@ -745,6 +774,10 @@ macro_rules! impl_exec_payload_common { self.$wrapped_field.timestamp } + fn extra_data(&self) -> VariableList { + self.$wrapped_field.extra_data.clone() + } + fn block_hash(&self) -> ExecutionBlockHash { self.$wrapped_field.block_hash } diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 7d4d229fef..427bcf5e9c 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -809,10 +809,13 @@ impl Tester { if expected_should_override_fcu.validator_is_connected { el.update_proposer_preparation( next_slot_epoch, - &[ProposerPreparationData { - validator_index: dbg!(proposer_index) as u64, - fee_recipient: Default::default(), - }], + [( + &ProposerPreparationData { + validator_index: dbg!(proposer_index) as u64, + fee_recipient: Default::default(), + }, + &None, + )], ) .await; } else { diff --git a/testing/execution_engine_integration/src/test_rig.rs b/testing/execution_engine_integration/src/test_rig.rs index 0289fd4206..f664509304 100644 --- a/testing/execution_engine_integration/src/test_rig.rs +++ b/testing/execution_engine_integration/src/test_rig.rs @@ -3,9 +3,10 @@ use crate::execution_engine::{ }; use crate::transactions::transactions; use ethers_providers::Middleware; +use execution_layer::test_utils::DEFAULT_GAS_LIMIT; use execution_layer::{ BlockProposalContentsType, BuilderParams, ChainHealth, ExecutionLayer, PayloadAttributes, - PayloadStatus, + PayloadParameters, PayloadStatus, }; use fork_choice::ForkchoiceUpdateParameters; use reqwest::{header::CONTENT_TYPE, Client}; @@ -251,6 +252,7 @@ impl TestRig { */ let parent_hash = terminal_pow_block_hash; + let parent_gas_limit = DEFAULT_GAS_LIMIT; let timestamp = timestamp_now(); let prev_randao = Hash256::zero(); let head_root = Hash256::zero(); @@ -324,15 +326,22 @@ impl TestRig { Some(vec![]), None, ); + + let payload_parameters = PayloadParameters { + parent_hash, + parent_gas_limit, + proposer_gas_limit: None, + payload_attributes: &payload_attributes, + forkchoice_update_params: &forkchoice_update_params, + current_fork: TEST_FORK, + }; + let block_proposal_content_type = self .ee_a .execution_layer .get_payload( - parent_hash, - &payload_attributes, - forkchoice_update_params, + payload_parameters, builder_params, - TEST_FORK, &self.spec, None, BlockProductionVersion::FullV2, @@ -476,15 +485,22 @@ impl TestRig { Some(vec![]), None, ); + + let payload_parameters = PayloadParameters { + parent_hash, + parent_gas_limit, + proposer_gas_limit: None, + payload_attributes: &payload_attributes, + forkchoice_update_params: &forkchoice_update_params, + current_fork: TEST_FORK, + }; + let block_proposal_content_type = self .ee_a .execution_layer .get_payload( - parent_hash, - &payload_attributes, - forkchoice_update_params, + payload_parameters, builder_params, - TEST_FORK, &self.spec, None, BlockProductionVersion::FullV2, From 8e891a8bfd139dde3e63a5ed70bc8b76eea896bf Mon Sep 17 00:00:00 2001 From: Akihito Nakano Date: Mon, 16 Dec 2024 14:44:02 +0900 Subject: [PATCH 09/23] Fix web3signer test fails on macOS (#6588) * Add lighthouse/key_legacy.p12 for macOS * Specify `-days 825` to meet Apple's requirements for TLS server certificates * Remove `-aes256` as it's ignored on exporting The following warning will appear: Warning: output encryption option -aes256 ignored with -export * Update certificates and keys --- testing/web3signer_tests/src/lib.rs | 6 +- testing/web3signer_tests/tls/generate.sh | 21 +++- .../web3signer_tests/tls/lighthouse/cert.pem | 58 +++++----- .../web3signer_tests/tls/lighthouse/key.key | 100 +++++++++--------- .../web3signer_tests/tls/lighthouse/key.p12 | Bin 4371 -> 4387 bytes .../tls/lighthouse/key_legacy.p12 | Bin 0 -> 4221 bytes .../tls/lighthouse/web3signer.pem | 58 +++++----- .../web3signer_tests/tls/web3signer/cert.pem | 58 +++++----- .../web3signer_tests/tls/web3signer/key.key | 100 +++++++++--------- .../web3signer_tests/tls/web3signer/key.p12 | Bin 4371 -> 4387 bytes .../tls/web3signer/known_clients.txt | 2 +- 11 files changed, 210 insertions(+), 193 deletions(-) create mode 100644 testing/web3signer_tests/tls/lighthouse/key_legacy.p12 diff --git a/testing/web3signer_tests/src/lib.rs b/testing/web3signer_tests/src/lib.rs index a58dcb5fa0..bebc8fa13b 100644 --- a/testing/web3signer_tests/src/lib.rs +++ b/testing/web3signer_tests/src/lib.rs @@ -130,7 +130,11 @@ mod tests { } fn client_identity_path() -> PathBuf { - tls_dir().join("lighthouse").join("key.p12") + if cfg!(target_os = "macos") { + tls_dir().join("lighthouse").join("key_legacy.p12") + } else { + tls_dir().join("lighthouse").join("key.p12") + } } fn client_identity_password() -> String { diff --git a/testing/web3signer_tests/tls/generate.sh b/testing/web3signer_tests/tls/generate.sh index f918e87cf8..3b14dbddba 100755 --- a/testing/web3signer_tests/tls/generate.sh +++ b/testing/web3signer_tests/tls/generate.sh @@ -1,7 +1,20 @@ #!/bin/bash -openssl req -x509 -sha256 -nodes -days 36500 -newkey rsa:4096 -keyout web3signer/key.key -out web3signer/cert.pem -config web3signer/config && -openssl pkcs12 -export -aes256 -out web3signer/key.p12 -inkey web3signer/key.key -in web3signer/cert.pem -password pass:$(cat web3signer/password.txt) && + +# The lighthouse/key_legacy.p12 file is generated specifically for macOS because the default `openssl pkcs12` encoding +# algorithm in OpenSSL v3 is not compatible with the PKCS algorithm used by the Apple Security Framework. The client +# side (using the reqwest crate) relies on the Apple Security Framework to parse PKCS files. +# We don't need to generate web3signer/key_legacy.p12 because the compatibility issue doesn't occur on the web3signer +# side. It seems that web3signer (Java) uses its own implementation to parse PKCS files. +# See https://github.com/sigp/lighthouse/issues/6442#issuecomment-2469252651 + +# We specify `-days 825` when generating the certificate files because Apple requires TLS server certificates to have a +# validity period of 825 days or fewer. +# See https://github.com/sigp/lighthouse/issues/6442#issuecomment-2474979183 + +openssl req -x509 -sha256 -nodes -days 825 -newkey rsa:4096 -keyout web3signer/key.key -out web3signer/cert.pem -config web3signer/config && +openssl pkcs12 -export -out web3signer/key.p12 -inkey web3signer/key.key -in web3signer/cert.pem -password pass:$(cat web3signer/password.txt) && cp web3signer/cert.pem lighthouse/web3signer.pem && -openssl req -x509 -sha256 -nodes -days 36500 -newkey rsa:4096 -keyout lighthouse/key.key -out lighthouse/cert.pem -config lighthouse/config && -openssl pkcs12 -export -aes256 -out lighthouse/key.p12 -inkey lighthouse/key.key -in lighthouse/cert.pem -password pass:$(cat lighthouse/password.txt) && +openssl req -x509 -sha256 -nodes -days 825 -newkey rsa:4096 -keyout lighthouse/key.key -out lighthouse/cert.pem -config lighthouse/config && +openssl pkcs12 -export -out lighthouse/key.p12 -inkey lighthouse/key.key -in lighthouse/cert.pem -password pass:$(cat lighthouse/password.txt) && +openssl pkcs12 -export -legacy -out lighthouse/key_legacy.p12 -inkey lighthouse/key.key -in lighthouse/cert.pem -password pass:$(cat lighthouse/password.txt) && openssl x509 -noout -fingerprint -sha256 -inform pem -in lighthouse/cert.pem | cut -b 20-| sed "s/^/lighthouse /" > web3signer/known_clients.txt diff --git a/testing/web3signer_tests/tls/lighthouse/cert.pem b/testing/web3signer_tests/tls/lighthouse/cert.pem index 24b0a2e5c0..4aaf66b747 100644 --- a/testing/web3signer_tests/tls/lighthouse/cert.pem +++ b/testing/web3signer_tests/tls/lighthouse/cert.pem @@ -1,33 +1,33 @@ -----BEGIN CERTIFICATE----- -MIIFujCCA6KgAwIBAgIUXZijYo8W4/9dAq58ocFEbZDxohwwDQYJKoZIhvcNAQEL +MIIFuDCCA6CgAwIBAgIUa3O7icWD4W7c5yRMjG/EX422ODUwDQYJKoZIhvcNAQEL BQAwazELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlZBMREwDwYDVQQHDAhTb21lQ2l0 eTESMBAGA1UECgwJTXlDb21wYW55MRMwEQYDVQQLDApNeURpdmlzaW9uMRMwEQYD -VQQDDApsaWdodGhvdXNlMCAXDTIzMDkyMDAyNTYzNloYDzIxMjMwODI3MDI1NjM2 -WjBrMQswCQYDVQQGEwJVUzELMAkGA1UECAwCVkExETAPBgNVBAcMCFNvbWVDaXR5 -MRIwEAYDVQQKDAlNeUNvbXBhbnkxEzARBgNVBAsMCk15RGl2aXNpb24xEzARBgNV -BAMMCmxpZ2h0aG91c2UwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC1 -R1M9NnRwUsqFvJzNWPKuY1PW7llwRRWCixiWNvcxukGTa6AMLZDrYO1Y7qlw5m52 -aHSA2fs2KyeA61yajG/BsLn1vmTtJMZXgLsG0MIqvhgOoh+ZZbl8biO0gQJSRSDE -jf0ogUVM9TCEt6ydbGnzgs8EESqvyXcreaXfmLI7jiX/BkwCdf+Ru+H3MF96QgAw -Oz1d8/fxYJvIpT/DOx4NuMZouSAcUVXgwcVb6JXeTg0xVcL33lluquhYDR0gD5Fe -V0fPth+e9XMAH7udim8E5wn2Ep8CAVoeVq6K9mBM3NqP7+2YmU//jLbkd6UvKPaI -0vps1zF9Bo8QewiRbM0IRse99ikCVZcjOcZSitw3kwTg59NjZ0Vk9R/2YQt/gGWM -VcR//EtbOZGqzGrLPFKOcWO85Ggz746Saj15N+bqT20hXHyiwYL8DLgJkMR2W9Nr -67Vyi9SWSM6rdRQlezlHq/yNEh+JuY7eoC3VeVw9K1ZXP+OKAwbpcnvd3uLwV91f -kpT6kjc6d2h4bK8fhvF16Em42JypQCl0xMhgg/8MFO+6ZLy5otWAdsSYyO5k9CAa -3zLeqd89dS7HNLdLZ0Y5SFWm6y5Kqu89ErIENafX5DxupHWsruiBV7zhDHNPaGcf -TPFe8xuDYsi155veOfEiDh4g+X1qjL8x8OEDjgsM3QIDAQABo1QwUjALBgNVHQ8E -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 +4IRAZKHZaeKYr1UXwaqzpwES+8ZZLjURkvqvnQ== -----END CERTIFICATE----- diff --git a/testing/web3signer_tests/tls/lighthouse/key.key b/testing/web3signer_tests/tls/lighthouse/key.key index d00b6c2122..2b510c6b6d 100644 --- a/testing/web3signer_tests/tls/lighthouse/key.key +++ b/testing/web3signer_tests/tls/lighthouse/key.key @@ -1,52 +1,52 @@ -----BEGIN PRIVATE KEY----- -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 -vc3Za4ClKSLmb3ZA9giTswYMev+3BQ== +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 +UyUzHP+SV/CL1Otbg/HjN6JGIcgY -----END PRIVATE KEY----- diff --git a/testing/web3signer_tests/tls/lighthouse/key.p12 b/testing/web3signer_tests/tls/lighthouse/key.p12 index 73468fa084b6f5f1b036afd643967e361d004fc4..f2ef6d20e27199c5f1d39d25763776470f5a9dd0 100644 GIT binary patch literal 4387 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 diff --git a/testing/web3signer_tests/tls/lighthouse/key_legacy.p12 b/testing/web3signer_tests/tls/lighthouse/key_legacy.p12 new file mode 100644 index 0000000000000000000000000000000000000000..c3394fae9af893142c035e087fa752c5225eefde GIT binary patch literal 4221 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 CpCCy9 diff --git a/testing/web3signer_tests/tls/web3signer/known_clients.txt b/testing/web3signer_tests/tls/web3signer/known_clients.txt index c4722fe587..86d61fba75 100644 --- a/testing/web3signer_tests/tls/web3signer/known_clients.txt +++ b/testing/web3signer_tests/tls/web3signer/known_clients.txt @@ -1 +1 @@ -lighthouse 02:D0:A8:C0:6A:59:90:40:54:67:D4:BD:AE:5A:D4:F5:14:A9:79:38:98:E0:62:93:C1:77:13:FC:B4:60:65:CE +lighthouse 49:99:C9:A4:05:4C:EC:BE:FD:0B:C3:C3:C1:2F:A4:D3:AB:70:96:47:51:F5:5B:3B:37:65:31:56:18:B7:B8:AD From 75d90795be0fe3ddbcb78402d35aab345dc88e2c Mon Sep 17 00:00:00 2001 From: Akihito Nakano Date: Mon, 16 Dec 2024 14:44:06 +0900 Subject: [PATCH 10/23] Remove req_id from CustodyId (#6589) * Remove req_id from CustodyId because it's not used --- beacon_node/lighthouse_network/src/service/api_types.rs | 1 - beacon_node/network/src/sync/network_context.rs | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/beacon_node/lighthouse_network/src/service/api_types.rs b/beacon_node/lighthouse_network/src/service/api_types.rs index cb22815390..85fabbb0c3 100644 --- a/beacon_node/lighthouse_network/src/service/api_types.rs +++ b/beacon_node/lighthouse_network/src/service/api_types.rs @@ -67,7 +67,6 @@ pub struct SamplingRequestId(pub usize); #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] pub struct CustodyId { pub requester: CustodyRequester, - pub req_id: Id, } /// Downstream components that perform custody by root requests. diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index c4d987e858..b6b7b315f3 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -763,8 +763,7 @@ impl SyncNetworkContext { let requester = CustodyRequester(id); let mut request = ActiveCustodyRequest::new( block_root, - // TODO(das): req_id is duplicated here, also present in id - CustodyId { requester, req_id }, + CustodyId { requester }, &custody_indexes_to_fetch, self.log.clone(), ); From 1c5be34def7ea46297524180d3b5a1fd2b4c1ac7 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:44:10 +0800 Subject: [PATCH 11/23] Write range sync tests in external event-driven form (#6618) * Write range sync tests in external event-driven form * Fix remaining test * Drop unused generics * Merge branch 'unstable' into range-sync-tests * Add reference to test author * Use async await * Fix failing test. Not sure how it was passing before without an EL. --- beacon_node/network/src/sync/manager.rs | 10 + .../src/sync/range_sync/block_storage.rs | 13 - .../src/sync/range_sync/chain_collection.rs | 21 +- .../network/src/sync/range_sync/mod.rs | 3 +- .../network/src/sync/range_sync/range.rs | 482 +----------------- .../network/src/sync/range_sync/sync_type.rs | 9 +- beacon_node/network/src/sync/tests/lookups.rs | 30 +- beacon_node/network/src/sync/tests/range.rs | 272 ++++++++++ 8 files changed, 328 insertions(+), 512 deletions(-) delete mode 100644 beacon_node/network/src/sync/range_sync/block_storage.rs diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 344e91711c..5d02be2b4c 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -362,6 +362,16 @@ impl SyncManager { self.sampling.get_request_status(block_root, index) } + #[cfg(test)] + pub(crate) fn range_sync_state(&self) -> super::range_sync::SyncChainStatus { + self.range_sync.state() + } + + #[cfg(test)] + pub(crate) fn update_execution_engine_state(&mut self, state: EngineState) { + self.handle_new_execution_engine_state(state); + } + fn network_globals(&self) -> &NetworkGlobals { self.network.network_globals() } diff --git a/beacon_node/network/src/sync/range_sync/block_storage.rs b/beacon_node/network/src/sync/range_sync/block_storage.rs deleted file mode 100644 index df49543a6b..0000000000 --- a/beacon_node/network/src/sync/range_sync/block_storage.rs +++ /dev/null @@ -1,13 +0,0 @@ -use beacon_chain::{BeaconChain, BeaconChainTypes}; -use types::Hash256; - -/// Trait that helps maintain RangeSync's implementation split from the BeaconChain -pub trait BlockStorage { - fn is_block_known(&self, block_root: &Hash256) -> bool; -} - -impl BlockStorage for BeaconChain { - fn is_block_known(&self, block_root: &Hash256) -> bool { - self.block_is_known_to_fork_choice(block_root) - } -} diff --git a/beacon_node/network/src/sync/range_sync/chain_collection.rs b/beacon_node/network/src/sync/range_sync/chain_collection.rs index 1217fbf8fe..c030d0a19e 100644 --- a/beacon_node/network/src/sync/range_sync/chain_collection.rs +++ b/beacon_node/network/src/sync/range_sync/chain_collection.rs @@ -3,12 +3,11 @@ //! Each chain type is stored in it's own map. A variety of helper functions are given along with //! this struct to simplify the logic of the other layers of sync. -use super::block_storage::BlockStorage; use super::chain::{ChainId, ProcessingResult, RemoveChain, SyncingChain}; use super::sync_type::RangeSyncType; use crate::metrics; use crate::sync::network_context::SyncNetworkContext; -use beacon_chain::BeaconChainTypes; +use beacon_chain::{BeaconChain, BeaconChainTypes}; use fnv::FnvHashMap; use lighthouse_network::PeerId; use lighthouse_network::SyncInfo; @@ -37,10 +36,13 @@ pub enum RangeSyncState { Idle, } +pub type SyncChainStatus = + Result, &'static str>; + /// A collection of finalized and head chains currently being processed. -pub struct ChainCollection { +pub struct ChainCollection { /// The beacon chain for processing. - beacon_chain: Arc, + beacon_chain: Arc>, /// The set of finalized chains being synced. finalized_chains: FnvHashMap>, /// The set of head chains being synced. @@ -51,8 +53,8 @@ pub struct ChainCollection { log: slog::Logger, } -impl ChainCollection { - pub fn new(beacon_chain: Arc, log: slog::Logger) -> Self { +impl ChainCollection { + pub fn new(beacon_chain: Arc>, log: slog::Logger) -> Self { ChainCollection { beacon_chain, finalized_chains: FnvHashMap::default(), @@ -213,9 +215,7 @@ impl ChainCollection { } } - pub fn state( - &self, - ) -> Result, &'static str> { + pub fn state(&self) -> SyncChainStatus { match self.state { RangeSyncState::Finalized(ref syncing_id) => { let chain = self @@ -409,7 +409,8 @@ impl ChainCollection { let log_ref = &self.log; let is_outdated = |target_slot: &Slot, target_root: &Hash256| { - target_slot <= &local_finalized_slot || beacon_chain.is_block_known(target_root) + target_slot <= &local_finalized_slot + || beacon_chain.block_is_known_to_fork_choice(target_root) }; // Retain only head peers that remain relevant diff --git a/beacon_node/network/src/sync/range_sync/mod.rs b/beacon_node/network/src/sync/range_sync/mod.rs index d0f2f9217e..8f881fba90 100644 --- a/beacon_node/network/src/sync/range_sync/mod.rs +++ b/beacon_node/network/src/sync/range_sync/mod.rs @@ -2,7 +2,6 @@ //! peers. mod batch; -mod block_storage; mod chain; mod chain_collection; mod range; @@ -13,5 +12,7 @@ pub use batch::{ ByRangeRequestType, }; pub use chain::{BatchId, ChainId, EPOCHS_PER_BATCH}; +#[cfg(test)] +pub use chain_collection::SyncChainStatus; pub use range::RangeSync; pub use sync_type::RangeSyncType; diff --git a/beacon_node/network/src/sync/range_sync/range.rs b/beacon_node/network/src/sync/range_sync/range.rs index 0ef99838de..78679403bb 100644 --- a/beacon_node/network/src/sync/range_sync/range.rs +++ b/beacon_node/network/src/sync/range_sync/range.rs @@ -39,9 +39,8 @@ //! Each chain is downloaded in batches of blocks. The batched blocks are processed sequentially //! and further batches are requested as current blocks are being processed. -use super::block_storage::BlockStorage; use super::chain::{BatchId, ChainId, RemoveChain, SyncingChain}; -use super::chain_collection::ChainCollection; +use super::chain_collection::{ChainCollection, SyncChainStatus}; use super::sync_type::RangeSyncType; use crate::metrics; use crate::status::ToStatusMessage; @@ -56,7 +55,7 @@ use lru_cache::LRUTimeCache; use slog::{crit, debug, trace, warn}; use std::collections::HashMap; use std::sync::Arc; -use types::{Epoch, EthSpec, Hash256, Slot}; +use types::{Epoch, EthSpec, Hash256}; /// For how long we store failed finalized chains to prevent retries. const FAILED_CHAINS_EXPIRY_SECONDS: u64 = 30; @@ -64,27 +63,26 @@ const FAILED_CHAINS_EXPIRY_SECONDS: u64 = 30; /// The primary object dealing with long range/batch syncing. This contains all the active and /// non-active chains that need to be processed before the syncing is considered complete. This /// holds the current state of the long range sync. -pub struct RangeSync> { +pub struct RangeSync { /// The beacon chain for processing. - beacon_chain: Arc, + beacon_chain: Arc>, /// Last known sync info of our useful connected peers. We use this information to create Head /// chains after all finalized chains have ended. awaiting_head_peers: HashMap, /// A collection of chains that need to be downloaded. This stores any head or finalized chains /// that need to be downloaded. - chains: ChainCollection, + chains: ChainCollection, /// Chains that have failed and are stored to prevent being retried. failed_chains: LRUTimeCache, /// The syncing logger. log: slog::Logger, } -impl RangeSync +impl RangeSync where - C: BlockStorage + ToStatusMessage, T: BeaconChainTypes, { - pub fn new(beacon_chain: Arc, log: slog::Logger) -> Self { + pub fn new(beacon_chain: Arc>, log: slog::Logger) -> Self { RangeSync { beacon_chain: beacon_chain.clone(), chains: ChainCollection::new(beacon_chain, log.clone()), @@ -96,9 +94,7 @@ where } } - pub fn state( - &self, - ) -> Result, &'static str> { + pub fn state(&self) -> SyncChainStatus { self.chains.state() } @@ -382,465 +378,3 @@ where } } } - -#[cfg(test)] -mod tests { - use crate::network_beacon_processor::NetworkBeaconProcessor; - use crate::sync::SyncMessage; - use crate::NetworkMessage; - - use super::*; - use crate::sync::network_context::{BlockOrBlob, RangeRequestId}; - use beacon_chain::builder::Witness; - use beacon_chain::eth1_chain::CachingEth1Backend; - use beacon_chain::parking_lot::RwLock; - use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; - use beacon_chain::EngineState; - use beacon_processor::WorkEvent as BeaconWorkEvent; - use lighthouse_network::service::api_types::SyncRequestId; - use lighthouse_network::{ - rpc::StatusMessage, service::api_types::AppRequestId, NetworkConfig, NetworkGlobals, - }; - use slog::{o, Drain}; - use slot_clock::TestingSlotClock; - use std::collections::HashSet; - use store::MemoryStore; - use tokio::sync::mpsc; - use types::{FixedBytesExtended, ForkName, MinimalEthSpec as E}; - - #[derive(Debug)] - struct FakeStorage { - known_blocks: RwLock>, - status: RwLock, - } - - impl Default for FakeStorage { - fn default() -> Self { - FakeStorage { - known_blocks: RwLock::new(HashSet::new()), - status: RwLock::new(StatusMessage { - fork_digest: [0; 4], - finalized_root: Hash256::zero(), - finalized_epoch: 0usize.into(), - head_root: Hash256::zero(), - head_slot: 0usize.into(), - }), - } - } - } - - impl FakeStorage { - fn remember_block(&self, block_root: Hash256) { - self.known_blocks.write().insert(block_root); - } - - #[allow(dead_code)] - fn forget_block(&self, block_root: &Hash256) { - self.known_blocks.write().remove(block_root); - } - } - - impl BlockStorage for FakeStorage { - fn is_block_known(&self, block_root: &store::Hash256) -> bool { - self.known_blocks.read().contains(block_root) - } - } - - impl ToStatusMessage for FakeStorage { - fn status_message(&self) -> StatusMessage { - self.status.read().clone() - } - } - - type TestBeaconChainType = - Witness, E, MemoryStore, MemoryStore>; - - fn build_log(level: slog::Level, enabled: bool) -> slog::Logger { - let decorator = slog_term::TermDecorator::new().build(); - let drain = slog_term::FullFormat::new(decorator).build().fuse(); - let drain = slog_async::Async::new(drain).build().fuse(); - - if enabled { - slog::Logger::root(drain.filter_level(level).fuse(), o!()) - } else { - slog::Logger::root(drain.filter(|_| false).fuse(), o!()) - } - } - - #[allow(unused)] - struct TestRig { - log: slog::Logger, - /// To check what does sync send to the beacon processor. - beacon_processor_rx: mpsc::Receiver>, - /// To set up different scenarios where sync is told about known/unknown blocks. - chain: Arc, - /// Needed by range to handle communication with the network. - cx: SyncNetworkContext, - /// To check what the network receives from Range. - network_rx: mpsc::UnboundedReceiver>, - /// To modify what the network declares about various global variables, in particular about - /// the sync state of a peer. - globals: Arc>, - } - - impl RangeSync { - fn assert_state(&self, expected_state: RangeSyncType) { - assert_eq!( - self.state() - .expect("State is ok") - .expect("Range is syncing") - .0, - expected_state - ) - } - - #[allow(dead_code)] - fn assert_not_syncing(&self) { - assert!( - self.state().expect("State is ok").is_none(), - "Range should not be syncing." - ); - } - } - - impl TestRig { - fn local_info(&self) -> SyncInfo { - let StatusMessage { - fork_digest: _, - finalized_root, - finalized_epoch, - head_root, - head_slot, - } = self.chain.status.read().clone(); - SyncInfo { - head_slot, - head_root, - finalized_epoch, - finalized_root, - } - } - - /// Reads an BlocksByRange request to a given peer from the network receiver channel. - #[track_caller] - fn grab_request( - &mut self, - expected_peer: &PeerId, - fork_name: ForkName, - ) -> (AppRequestId, Option) { - let block_req_id = if let Ok(NetworkMessage::SendRequest { - peer_id, - request: _, - request_id, - }) = self.network_rx.try_recv() - { - assert_eq!(&peer_id, expected_peer); - request_id - } else { - panic!("Should have sent a batch request to the peer") - }; - let blob_req_id = if fork_name.deneb_enabled() { - if let Ok(NetworkMessage::SendRequest { - peer_id, - request: _, - request_id, - }) = self.network_rx.try_recv() - { - assert_eq!(&peer_id, expected_peer); - Some(request_id) - } else { - panic!("Should have sent a batch request to the peer") - } - } else { - None - }; - (block_req_id, blob_req_id) - } - - fn complete_range_block_and_blobs_response( - &mut self, - block_req: AppRequestId, - blob_req_opt: Option, - ) -> (ChainId, BatchId, Id) { - if blob_req_opt.is_some() { - match block_req { - AppRequestId::Sync(SyncRequestId::RangeBlockAndBlobs { id }) => { - let _ = self - .cx - .range_block_and_blob_response(id, BlockOrBlob::Block(None)); - let response = self - .cx - .range_block_and_blob_response(id, BlockOrBlob::Blob(None)) - .unwrap(); - let (chain_id, batch_id) = - TestRig::unwrap_range_request_id(response.sender_id); - (chain_id, batch_id, id) - } - other => panic!("unexpected request {:?}", other), - } - } else { - match block_req { - AppRequestId::Sync(SyncRequestId::RangeBlockAndBlobs { id }) => { - let response = self - .cx - .range_block_and_blob_response(id, BlockOrBlob::Block(None)) - .unwrap(); - let (chain_id, batch_id) = - TestRig::unwrap_range_request_id(response.sender_id); - (chain_id, batch_id, id) - } - other => panic!("unexpected request {:?}", other), - } - } - } - - fn unwrap_range_request_id(sender_id: RangeRequestId) -> (ChainId, BatchId) { - if let RangeRequestId::RangeSync { chain_id, batch_id } = sender_id { - (chain_id, batch_id) - } else { - panic!("expected RangeSync request: {:?}", sender_id) - } - } - - /// Produce a head peer - fn head_peer( - &self, - ) -> ( - PeerId, - SyncInfo, /* Local info */ - SyncInfo, /* Remote info */ - ) { - let local_info = self.local_info(); - - // Get a peer with an advanced head - let head_root = Hash256::random(); - let head_slot = local_info.head_slot + 1; - let remote_info = SyncInfo { - head_root, - head_slot, - ..local_info - }; - let peer_id = PeerId::random(); - (peer_id, local_info, remote_info) - } - - fn finalized_peer( - &self, - ) -> ( - PeerId, - SyncInfo, /* Local info */ - SyncInfo, /* Remote info */ - ) { - let local_info = self.local_info(); - - let finalized_root = Hash256::random(); - let finalized_epoch = local_info.finalized_epoch + 2; - let head_slot = finalized_epoch.start_slot(E::slots_per_epoch()); - let head_root = Hash256::random(); - let remote_info = SyncInfo { - finalized_epoch, - finalized_root, - head_slot, - head_root, - }; - - let peer_id = PeerId::random(); - (peer_id, local_info, remote_info) - } - - #[track_caller] - fn expect_empty_processor(&mut self) { - match self.beacon_processor_rx.try_recv() { - Ok(work) => { - panic!( - "Expected empty processor. Instead got {}", - work.work_type_str() - ); - } - Err(e) => match e { - mpsc::error::TryRecvError::Empty => {} - mpsc::error::TryRecvError::Disconnected => unreachable!("bad coded test?"), - }, - } - } - - #[track_caller] - fn expect_chain_segment(&mut self) { - match self.beacon_processor_rx.try_recv() { - Ok(work) => { - assert_eq!(work.work_type(), beacon_processor::WorkType::ChainSegment); - } - other => panic!("Expected chain segment process, found {:?}", other), - } - } - } - - fn range(log_enabled: bool) -> (TestRig, RangeSync) { - let log = build_log(slog::Level::Trace, log_enabled); - // Initialise a new beacon chain - let harness = BeaconChainHarness::>::builder(E) - .default_spec() - .logger(log.clone()) - .deterministic_keypairs(1) - .fresh_ephemeral_store() - .build(); - let chain = harness.chain; - - let fake_store = Arc::new(FakeStorage::default()); - let range_sync = RangeSync::::new( - fake_store.clone(), - log.new(o!("component" => "range")), - ); - let (network_tx, network_rx) = mpsc::unbounded_channel(); - let (sync_tx, _sync_rx) = mpsc::unbounded_channel::>(); - let network_config = Arc::new(NetworkConfig::default()); - let globals = Arc::new(NetworkGlobals::new_test_globals( - Vec::new(), - &log, - network_config, - chain.spec.clone(), - )); - let (network_beacon_processor, beacon_processor_rx) = - NetworkBeaconProcessor::null_for_testing( - globals.clone(), - sync_tx, - chain.clone(), - harness.runtime.task_executor.clone(), - log.clone(), - ); - let cx = SyncNetworkContext::new( - network_tx, - Arc::new(network_beacon_processor), - chain, - log.new(o!("component" => "network_context")), - ); - let test_rig = TestRig { - log, - beacon_processor_rx, - chain: fake_store, - cx, - network_rx, - globals, - }; - (test_rig, range_sync) - } - - #[test] - fn head_chain_removed_while_finalized_syncing() { - // NOTE: this is a regression test. - let (mut rig, mut range) = range(false); - - // Get a peer with an advanced head - let (head_peer, local_info, remote_info) = rig.head_peer(); - range.add_peer(&mut rig.cx, local_info, head_peer, remote_info); - range.assert_state(RangeSyncType::Head); - - let fork = rig - .cx - .chain - .spec - .fork_name_at_epoch(rig.cx.chain.epoch().unwrap()); - - // Sync should have requested a batch, grab the request. - let _ = rig.grab_request(&head_peer, fork); - - // Now get a peer with an advanced finalized epoch. - let (finalized_peer, local_info, remote_info) = rig.finalized_peer(); - range.add_peer(&mut rig.cx, local_info, finalized_peer, remote_info); - range.assert_state(RangeSyncType::Finalized); - - // Sync should have requested a batch, grab the request - let _ = rig.grab_request(&finalized_peer, fork); - - // Fail the head chain by disconnecting the peer. - range.remove_peer(&mut rig.cx, &head_peer); - range.assert_state(RangeSyncType::Finalized); - } - - #[test] - fn state_update_while_purging() { - // NOTE: this is a regression test. - let (mut rig, mut range) = range(true); - - // Get a peer with an advanced head - let (head_peer, local_info, head_info) = rig.head_peer(); - let head_peer_root = head_info.head_root; - range.add_peer(&mut rig.cx, local_info, head_peer, head_info); - range.assert_state(RangeSyncType::Head); - - let fork = rig - .cx - .chain - .spec - .fork_name_at_epoch(rig.cx.chain.epoch().unwrap()); - - // Sync should have requested a batch, grab the request. - let _ = rig.grab_request(&head_peer, fork); - - // Now get a peer with an advanced finalized epoch. - let (finalized_peer, local_info, remote_info) = rig.finalized_peer(); - let finalized_peer_root = remote_info.finalized_root; - range.add_peer(&mut rig.cx, local_info, finalized_peer, remote_info); - range.assert_state(RangeSyncType::Finalized); - - // Sync should have requested a batch, grab the request - let _ = rig.grab_request(&finalized_peer, fork); - - // Now the chain knows both chains target roots. - rig.chain.remember_block(head_peer_root); - rig.chain.remember_block(finalized_peer_root); - - // Add an additional peer to the second chain to make range update it's status - let (finalized_peer, local_info, remote_info) = rig.finalized_peer(); - range.add_peer(&mut rig.cx, local_info, finalized_peer, remote_info); - } - - #[test] - fn pause_and_resume_on_ee_offline() { - let (mut rig, mut range) = range(true); - let fork = rig - .cx - .chain - .spec - .fork_name_at_epoch(rig.cx.chain.epoch().unwrap()); - - // add some peers - let (peer1, local_info, head_info) = rig.head_peer(); - range.add_peer(&mut rig.cx, local_info, peer1, head_info); - let (block_req, blob_req_opt) = rig.grab_request(&peer1, fork); - - let (chain1, batch1, id1) = - rig.complete_range_block_and_blobs_response(block_req, blob_req_opt); - - // make the ee offline - rig.cx.update_execution_engine_state(EngineState::Offline); - - // send the response to the request - range.blocks_by_range_response(&mut rig.cx, peer1, chain1, batch1, id1, vec![]); - - // the beacon processor shouldn't have received any work - rig.expect_empty_processor(); - - // while the ee is offline, more peers might arrive. Add a new finalized peer. - let (peer2, local_info, finalized_info) = rig.finalized_peer(); - range.add_peer(&mut rig.cx, local_info, peer2, finalized_info); - let (block_req, blob_req_opt) = rig.grab_request(&peer2, fork); - - let (chain2, batch2, id2) = - rig.complete_range_block_and_blobs_response(block_req, blob_req_opt); - - // send the response to the request - range.blocks_by_range_response(&mut rig.cx, peer2, chain2, batch2, id2, vec![]); - - // the beacon processor shouldn't have received any work - rig.expect_empty_processor(); - - // make the beacon processor available again. - rig.cx.update_execution_engine_state(EngineState::Online); - - // now resume range, we should have two processing requests in the beacon processor. - range.resume(&mut rig.cx); - - rig.expect_chain_segment(); - rig.expect_chain_segment(); - } -} diff --git a/beacon_node/network/src/sync/range_sync/sync_type.rs b/beacon_node/network/src/sync/range_sync/sync_type.rs index d6ffd4a5df..4ff7e39310 100644 --- a/beacon_node/network/src/sync/range_sync/sync_type.rs +++ b/beacon_node/network/src/sync/range_sync/sync_type.rs @@ -1,10 +1,9 @@ //! Contains logic about identifying which Sync to perform given PeerSyncInfo of ourselves and //! of a remote. +use beacon_chain::{BeaconChain, BeaconChainTypes}; use lighthouse_network::SyncInfo; -use super::block_storage::BlockStorage; - /// The type of Range sync that should be done relative to our current state. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RangeSyncType { @@ -17,8 +16,8 @@ pub enum RangeSyncType { impl RangeSyncType { /// Determines the type of sync given our local `PeerSyncInfo` and the remote's /// `PeerSyncInfo`. - pub fn new( - chain: &C, + pub fn new( + chain: &BeaconChain, local_info: &SyncInfo, remote_info: &SyncInfo, ) -> RangeSyncType { @@ -29,7 +28,7 @@ impl RangeSyncType { // not seen the finalized hash before. if remote_info.finalized_epoch > local_info.finalized_epoch - && !chain.is_block_known(&remote_info.finalized_root) + && !chain.block_is_known_to_fork_choice(&remote_info.finalized_root) { RangeSyncType::Finalized } else { diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index 9f2c9ef66f..94aacad3e8 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -83,6 +83,7 @@ impl TestRig { .logger(log.clone()) .deterministic_keypairs(1) .fresh_ephemeral_store() + .mock_execution_layer() .testing_slot_clock(TestingSlotClock::new( Slot::new(0), Duration::from_secs(0), @@ -144,7 +145,7 @@ impl TestRig { } } - fn test_setup() -> Self { + pub fn test_setup() -> Self { Self::test_setup_with_config(None) } @@ -168,11 +169,11 @@ impl TestRig { } } - fn log(&self, msg: &str) { + pub fn log(&self, msg: &str) { info!(self.log, "TEST_RIG"; "msg" => msg); } - fn after_deneb(&self) -> bool { + pub fn after_deneb(&self) -> bool { matches!(self.fork_name, ForkName::Deneb | ForkName::Electra) } @@ -238,7 +239,7 @@ impl TestRig { (parent, block, parent_root, block_root) } - fn send_sync_message(&mut self, sync_message: SyncMessage) { + pub fn send_sync_message(&mut self, sync_message: SyncMessage) { self.sync_manager.handle_message(sync_message); } @@ -369,7 +370,7 @@ impl TestRig { self.expect_empty_network(); } - fn new_connected_peer(&mut self) -> PeerId { + pub fn new_connected_peer(&mut self) -> PeerId { self.network_globals .peers .write() @@ -811,7 +812,7 @@ impl TestRig { } } - fn peer_disconnected(&mut self, peer_id: PeerId) { + pub fn peer_disconnected(&mut self, peer_id: PeerId) { self.send_sync_message(SyncMessage::Disconnect(peer_id)); } @@ -827,7 +828,7 @@ impl TestRig { } } - fn pop_received_network_event) -> Option>( + pub fn pop_received_network_event) -> Option>( &mut self, predicate_transform: F, ) -> Result { @@ -847,7 +848,7 @@ impl TestRig { } } - fn pop_received_processor_event) -> Option>( + pub fn pop_received_processor_event) -> Option>( &mut self, predicate_transform: F, ) -> Result { @@ -871,6 +872,16 @@ impl TestRig { } } + pub fn expect_empty_processor(&mut self) { + self.drain_processor_rx(); + if !self.beacon_processor_rx_queue.is_empty() { + panic!( + "Expected processor to be empty, but has events: {:?}", + self.beacon_processor_rx_queue + ); + } + } + fn find_block_lookup_request( &mut self, for_block: Hash256, @@ -2173,7 +2184,8 @@ fn custody_lookup_happy_path() { mod deneb_only { use super::*; use beacon_chain::{ - block_verification_types::RpcBlock, data_availability_checker::AvailabilityCheckError, + block_verification_types::{AsBlock, RpcBlock}, + data_availability_checker::AvailabilityCheckError, }; use ssz_types::VariableList; use std::collections::VecDeque; diff --git a/beacon_node/network/src/sync/tests/range.rs b/beacon_node/network/src/sync/tests/range.rs index 8b13789179..6faa8b7247 100644 --- a/beacon_node/network/src/sync/tests/range.rs +++ b/beacon_node/network/src/sync/tests/range.rs @@ -1 +1,273 @@ +use super::*; +use crate::status::ToStatusMessage; +use crate::sync::manager::SLOT_IMPORT_TOLERANCE; +use crate::sync::range_sync::RangeSyncType; +use crate::sync::SyncMessage; +use beacon_chain::test_utils::{AttestationStrategy, BlockStrategy}; +use beacon_chain::EngineState; +use lighthouse_network::rpc::{RequestType, StatusMessage}; +use lighthouse_network::service::api_types::{AppRequestId, Id, SyncRequestId}; +use lighthouse_network::{PeerId, SyncInfo}; +use std::time::Duration; +use types::{EthSpec, Hash256, MinimalEthSpec as E, SignedBeaconBlock, Slot}; +const D: Duration = Duration::new(0, 0); + +impl TestRig { + /// Produce a head peer with an advanced head + fn add_head_peer(&mut self) -> PeerId { + self.add_head_peer_with_root(Hash256::random()) + } + + /// Produce a head peer with an advanced head + fn add_head_peer_with_root(&mut self, head_root: Hash256) -> PeerId { + let local_info = self.local_info(); + self.add_peer(SyncInfo { + head_root, + head_slot: local_info.head_slot + 1 + Slot::new(SLOT_IMPORT_TOLERANCE as u64), + ..local_info + }) + } + + // Produce a finalized peer with an advanced finalized epoch + fn add_finalized_peer(&mut self) -> PeerId { + self.add_finalized_peer_with_root(Hash256::random()) + } + + // Produce a finalized peer with an advanced finalized epoch + fn add_finalized_peer_with_root(&mut self, finalized_root: Hash256) -> PeerId { + let local_info = self.local_info(); + let finalized_epoch = local_info.finalized_epoch + 2; + self.add_peer(SyncInfo { + finalized_epoch, + finalized_root, + head_slot: finalized_epoch.start_slot(E::slots_per_epoch()), + head_root: Hash256::random(), + }) + } + + fn local_info(&self) -> SyncInfo { + let StatusMessage { + fork_digest: _, + finalized_root, + finalized_epoch, + head_root, + head_slot, + } = self.harness.chain.status_message(); + SyncInfo { + head_slot, + head_root, + finalized_epoch, + finalized_root, + } + } + + fn add_peer(&mut self, remote_info: SyncInfo) -> PeerId { + // Create valid peer known to network globals + let peer_id = self.new_connected_peer(); + // Send peer to sync + self.send_sync_message(SyncMessage::AddPeer(peer_id, remote_info.clone())); + peer_id + } + + fn assert_state(&self, state: RangeSyncType) { + assert_eq!( + self.sync_manager + .range_sync_state() + .expect("State is ok") + .expect("Range should be syncing") + .0, + state, + "not expected range sync state" + ); + } + + #[track_caller] + fn expect_chain_segment(&mut self) { + self.pop_received_processor_event(|ev| { + (ev.work_type() == beacon_processor::WorkType::ChainSegment).then_some(()) + }) + .unwrap_or_else(|e| panic!("Expect ChainSegment work event: {e:?}")); + } + + fn update_execution_engine_state(&mut self, state: EngineState) { + self.log(&format!("execution engine state updated: {state:?}")); + self.sync_manager.update_execution_engine_state(state); + } + + fn find_blocks_by_range_request(&mut self, target_peer_id: &PeerId) -> (Id, Option) { + let block_req_id = self + .pop_received_network_event(|ev| match ev { + NetworkMessage::SendRequest { + peer_id, + request: RequestType::BlocksByRange(_), + request_id: AppRequestId::Sync(SyncRequestId::RangeBlockAndBlobs { id }), + } if peer_id == target_peer_id => Some(*id), + _ => None, + }) + .expect("Should have a blocks by range request"); + + let blob_req_id = if self.after_deneb() { + Some( + self.pop_received_network_event(|ev| match ev { + NetworkMessage::SendRequest { + peer_id, + request: RequestType::BlobsByRange(_), + request_id: AppRequestId::Sync(SyncRequestId::RangeBlockAndBlobs { id }), + } if peer_id == target_peer_id => Some(*id), + _ => None, + }) + .expect("Should have a blobs by range request"), + ) + } else { + None + }; + + (block_req_id, blob_req_id) + } + + fn find_and_complete_blocks_by_range_request(&mut self, target_peer_id: PeerId) { + let (blocks_req_id, blobs_req_id) = self.find_blocks_by_range_request(&target_peer_id); + + // Complete the request with a single stream termination + self.log(&format!( + "Completing BlocksByRange request {blocks_req_id} with empty stream" + )); + self.send_sync_message(SyncMessage::RpcBlock { + request_id: SyncRequestId::RangeBlockAndBlobs { id: blocks_req_id }, + peer_id: target_peer_id, + beacon_block: None, + seen_timestamp: D, + }); + + if let Some(blobs_req_id) = blobs_req_id { + // Complete the request with a single stream termination + self.log(&format!( + "Completing BlobsByRange request {blobs_req_id} with empty stream" + )); + self.send_sync_message(SyncMessage::RpcBlob { + request_id: SyncRequestId::RangeBlockAndBlobs { id: blobs_req_id }, + peer_id: target_peer_id, + blob_sidecar: None, + seen_timestamp: D, + }); + } + } + + async fn create_canonical_block(&mut self) -> SignedBeaconBlock { + self.harness.advance_slot(); + + let block_root = self + .harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + self.harness + .chain + .store + .get_full_block(&block_root) + .unwrap() + .unwrap() + } + + async fn remember_block(&mut self, block: SignedBeaconBlock) { + self.harness + .process_block(block.slot(), block.canonical_root(), (block.into(), None)) + .await + .unwrap(); + } +} + +#[test] +fn head_chain_removed_while_finalized_syncing() { + // NOTE: this is a regression test. + // Added in PR https://github.com/sigp/lighthouse/pull/2821 + let mut rig = TestRig::test_setup(); + + // Get a peer with an advanced head + let head_peer = rig.add_head_peer(); + rig.assert_state(RangeSyncType::Head); + + // Sync should have requested a batch, grab the request. + let _ = rig.find_blocks_by_range_request(&head_peer); + + // Now get a peer with an advanced finalized epoch. + let finalized_peer = rig.add_finalized_peer(); + rig.assert_state(RangeSyncType::Finalized); + + // Sync should have requested a batch, grab the request + let _ = rig.find_blocks_by_range_request(&finalized_peer); + + // Fail the head chain by disconnecting the peer. + rig.peer_disconnected(head_peer); + rig.assert_state(RangeSyncType::Finalized); +} + +#[tokio::test] +async fn state_update_while_purging() { + // NOTE: this is a regression test. + // Added in PR https://github.com/sigp/lighthouse/pull/2827 + let mut rig = TestRig::test_setup(); + + // Create blocks on a separate harness + let mut rig_2 = TestRig::test_setup(); + // Need to create blocks that can be inserted into the fork-choice and fit the "known + // conditions" below. + let head_peer_block = rig_2.create_canonical_block().await; + let head_peer_root = head_peer_block.canonical_root(); + let finalized_peer_block = rig_2.create_canonical_block().await; + let finalized_peer_root = finalized_peer_block.canonical_root(); + + // Get a peer with an advanced head + let head_peer = rig.add_head_peer_with_root(head_peer_root); + rig.assert_state(RangeSyncType::Head); + + // Sync should have requested a batch, grab the request. + let _ = rig.find_blocks_by_range_request(&head_peer); + + // Now get a peer with an advanced finalized epoch. + let finalized_peer = rig.add_finalized_peer_with_root(finalized_peer_root); + rig.assert_state(RangeSyncType::Finalized); + + // Sync should have requested a batch, grab the request + let _ = rig.find_blocks_by_range_request(&finalized_peer); + + // Now the chain knows both chains target roots. + rig.remember_block(head_peer_block).await; + rig.remember_block(finalized_peer_block).await; + + // Add an additional peer to the second chain to make range update it's status + rig.add_finalized_peer(); +} + +#[test] +fn pause_and_resume_on_ee_offline() { + let mut rig = TestRig::test_setup(); + + // add some peers + let peer1 = rig.add_head_peer(); + // make the ee offline + rig.update_execution_engine_state(EngineState::Offline); + // send the response to the request + rig.find_and_complete_blocks_by_range_request(peer1); + // the beacon processor shouldn't have received any work + rig.expect_empty_processor(); + + // while the ee is offline, more peers might arrive. Add a new finalized peer. + let peer2 = rig.add_finalized_peer(); + + // send the response to the request + rig.find_and_complete_blocks_by_range_request(peer2); + // the beacon processor shouldn't have received any work + rig.expect_empty_processor(); + // make the beacon processor available again. + // update_execution_engine_state implicitly calls resume + // now resume range, we should have two processing requests in the beacon processor. + rig.update_execution_engine_state(EngineState::Online); + + rig.expect_chain_segment(); + rig.expect_chain_segment(); +} From 847c8019c7867e3eaf65168e5259ea33e7e0eb5a Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Mon, 16 Dec 2024 16:44:14 +1100 Subject: [PATCH 12/23] Fix peer down-scoring behaviour when gossip blobs/columns are received after `getBlobs` or reconstruction (#6686) * Fix peer disconnection when gossip blobs/columns are received after they are recieved from the EL or available via column reconstruction. --- .../gossip_methods.rs | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index 4fc83b0923..f3c48e42f0 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -710,8 +710,19 @@ impl NetworkBeaconProcessor { MessageAcceptance::Reject, ); } + GossipDataColumnError::PriorKnown { .. } => { + // Data column is available via either the EL or reconstruction. + // Do not penalise the peer. + // Gossip filter should filter any duplicates received after this. + debug!( + self.log, + "Received already available column sidecar. Ignoring the column sidecar"; + "slot" => %slot, + "block_root" => %block_root, + "index" => %index, + ) + } GossipDataColumnError::FutureSlot { .. } - | GossipDataColumnError::PriorKnown { .. } | GossipDataColumnError::PastFinalizedSlot { .. } => { debug!( self.log, @@ -852,7 +863,18 @@ impl NetworkBeaconProcessor { MessageAcceptance::Reject, ); } - GossipBlobError::FutureSlot { .. } | GossipBlobError::RepeatBlob { .. } => { + GossipBlobError::RepeatBlob { .. } => { + // We may have received the blob from the EL. Do not penalise the peer. + // Gossip filter should filter any duplicates received after this. + debug!( + self.log, + "Received already available blob sidecar. Ignoring the blob sidecar"; + "slot" => %slot, + "root" => %root, + "index" => %index, + ) + } + GossipBlobError::FutureSlot { .. } => { debug!( self.log, "Could not verify blob sidecar for gossip. Ignoring the blob sidecar"; From 02cb2d68ff6f5bc3a4bc34baff9926d6b449e144 Mon Sep 17 00:00:00 2001 From: Daniel Knopik <107140945+dknopik@users.noreply.github.com> Date: Tue, 17 Dec 2024 01:40:35 +0100 Subject: [PATCH 13/23] Enable lints for tests only running optimized (#6664) * enable linting optimized-only tests * fix automatically fixable or obvious lints * fix suspicious_open_options by removing manual options * fix `await_holding_lock`s * avoid failing lint due to now disabled `#[cfg(debug_assertions)]` * reduce future sizes in tests * fix accidently flipped assert logic * restore holding lock for web3signer download * Merge branch 'unstable' into lint-opt-tests --- Makefile | 2 +- account_manager/src/validator/exit.rs | 2 +- .../beacon_chain/src/shuffling_cache.rs | 2 +- .../tests/attestation_production.rs | 4 +- .../tests/attestation_verification.rs | 42 +- beacon_node/beacon_chain/tests/bellatrix.rs | 14 +- beacon_node/beacon_chain/tests/capella.rs | 8 +- .../tests/payload_invalidation.rs | 4 +- beacon_node/beacon_chain/tests/store_tests.rs | 93 ++-- .../tests/sync_committee_verification.rs | 4 +- beacon_node/beacon_chain/tests/tests.rs | 5 +- .../tests/broadcast_validation_tests.rs | 18 +- .../http_api/tests/interactive_tests.rs | 2 +- beacon_node/http_api/tests/status_tests.rs | 30 +- beacon_node/http_api/tests/tests.rs | 129 +++--- .../src/service/gossip_cache.rs | 23 +- .../src/network_beacon_processor/tests.rs | 2 +- beacon_node/network/src/service/tests.rs | 402 +++++++++--------- beacon_node/operation_pool/src/lib.rs | 62 ++- .../eth2_wallet_manager/src/wallet_manager.rs | 32 +- consensus/fork_choice/tests/tests.rs | 26 +- crypto/eth2_keystore/tests/eip2335_vectors.rs | 4 +- crypto/eth2_keystore/tests/tests.rs | 14 +- crypto/eth2_wallet/tests/tests.rs | 13 +- lighthouse/tests/account_manager.rs | 28 +- lighthouse/tests/beacon_node.rs | 51 ++- lighthouse/tests/boot_node.rs | 4 +- lighthouse/tests/validator_client.rs | 8 +- lighthouse/tests/validator_manager.rs | 16 +- testing/web3signer_tests/src/lib.rs | 4 +- validator_client/http_api/src/tests.rs | 15 +- .../http_api/src/tests/keystores.rs | 65 +-- validator_manager/src/import_validators.rs | 4 +- validator_manager/src/move_validators.rs | 14 +- 34 files changed, 572 insertions(+), 574 deletions(-) diff --git a/Makefile b/Makefile index ab239c94d3..958abf8705 100644 --- a/Makefile +++ b/Makefile @@ -204,7 +204,7 @@ test-full: cargo-fmt test-release test-debug test-ef test-exec-engine # Lints the code for bad style and potentially unsafe arithmetic using Clippy. # Clippy lints are opt-in per-crate for now. By default, everything is allowed except for performance and correctness lints. lint: - cargo clippy --workspace --benches --tests $(EXTRA_CLIPPY_OPTS) --features "$(TEST_FEATURES)" -- \ + RUSTFLAGS="-C debug-assertions=no $(RUSTFLAGS)" cargo clippy --workspace --benches --tests $(EXTRA_CLIPPY_OPTS) --features "$(TEST_FEATURES)" -- \ -D clippy::fn_to_numeric_cast_any \ -D clippy::manual_let_else \ -D clippy::large_stack_frames \ diff --git a/account_manager/src/validator/exit.rs b/account_manager/src/validator/exit.rs index 3fb0e50d22..ea1a24da1f 100644 --- a/account_manager/src/validator/exit.rs +++ b/account_manager/src/validator/exit.rs @@ -409,6 +409,6 @@ mod tests { ) .unwrap(); - assert_eq!(expected_pk, kp.pk.into()); + assert_eq!(expected_pk, kp.pk); } } diff --git a/beacon_node/beacon_chain/src/shuffling_cache.rs b/beacon_node/beacon_chain/src/shuffling_cache.rs index a662cc49c9..da1d60db17 100644 --- a/beacon_node/beacon_chain/src/shuffling_cache.rs +++ b/beacon_node/beacon_chain/src/shuffling_cache.rs @@ -512,7 +512,7 @@ mod test { } assert!( - !cache.contains(&shuffling_id_and_committee_caches.get(0).unwrap().0), + !cache.contains(&shuffling_id_and_committee_caches.first().unwrap().0), "should not contain oldest epoch shuffling id" ); assert_eq!( diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index 0b121356b9..87fefe7114 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -70,12 +70,12 @@ async fn produces_attestations_from_attestation_simulator_service() { } // Compare the prometheus metrics that evaluates the performance of the unaggregated attestations - let hit_prometheus_metrics = vec![ + let hit_prometheus_metrics = [ metrics::VALIDATOR_MONITOR_ATTESTATION_SIMULATOR_HEAD_ATTESTER_HIT_TOTAL, metrics::VALIDATOR_MONITOR_ATTESTATION_SIMULATOR_TARGET_ATTESTER_HIT_TOTAL, metrics::VALIDATOR_MONITOR_ATTESTATION_SIMULATOR_SOURCE_ATTESTER_HIT_TOTAL, ]; - let miss_prometheus_metrics = vec![ + let miss_prometheus_metrics = [ metrics::VALIDATOR_MONITOR_ATTESTATION_SIMULATOR_HEAD_ATTESTER_MISS_TOTAL, metrics::VALIDATOR_MONITOR_ATTESTATION_SIMULATOR_TARGET_ATTESTER_MISS_TOTAL, metrics::VALIDATOR_MONITOR_ATTESTATION_SIMULATOR_SOURCE_ATTESTER_MISS_TOTAL, diff --git a/beacon_node/beacon_chain/tests/attestation_verification.rs b/beacon_node/beacon_chain/tests/attestation_verification.rs index e168cbb6f4..dcc63ddf62 100644 --- a/beacon_node/beacon_chain/tests/attestation_verification.rs +++ b/beacon_node/beacon_chain/tests/attestation_verification.rs @@ -431,10 +431,12 @@ impl GossipTester { .chain .verify_aggregated_attestation_for_gossip(&aggregate) .err() - .expect(&format!( - "{} should error during verify_aggregated_attestation_for_gossip", - desc - )); + .unwrap_or_else(|| { + panic!( + "{} should error during verify_aggregated_attestation_for_gossip", + desc + ) + }); inspect_err(&self, err); /* @@ -449,10 +451,12 @@ impl GossipTester { .unwrap(); assert_eq!(results.len(), 2); - let batch_err = results.pop().unwrap().err().expect(&format!( - "{} should error during batch_verify_aggregated_attestations_for_gossip", - desc - )); + let batch_err = results.pop().unwrap().err().unwrap_or_else(|| { + panic!( + "{} should error during batch_verify_aggregated_attestations_for_gossip", + desc + ) + }); inspect_err(&self, batch_err); self @@ -475,10 +479,12 @@ impl GossipTester { .chain .verify_unaggregated_attestation_for_gossip(&attn, Some(subnet_id)) .err() - .expect(&format!( - "{} should error during verify_unaggregated_attestation_for_gossip", - desc - )); + .unwrap_or_else(|| { + panic!( + "{} should error during verify_unaggregated_attestation_for_gossip", + desc + ) + }); inspect_err(&self, err); /* @@ -496,10 +502,12 @@ impl GossipTester { ) .unwrap(); assert_eq!(results.len(), 2); - let batch_err = results.pop().unwrap().err().expect(&format!( - "{} should error during batch_verify_unaggregated_attestations_for_gossip", - desc - )); + let batch_err = results.pop().unwrap().err().unwrap_or_else(|| { + panic!( + "{} should error during batch_verify_unaggregated_attestations_for_gossip", + desc + ) + }); inspect_err(&self, batch_err); self @@ -816,7 +824,7 @@ async fn aggregated_gossip_verification() { let (index, sk) = tester.non_aggregator(); *a = SignedAggregateAndProof::from_aggregate( index as u64, - tester.valid_aggregate.message().aggregate().clone(), + tester.valid_aggregate.message().aggregate(), None, &sk, &chain.canonical_head.cached_head().head_fork(), diff --git a/beacon_node/beacon_chain/tests/bellatrix.rs b/beacon_node/beacon_chain/tests/bellatrix.rs index 5bd3452623..5080b0890b 100644 --- a/beacon_node/beacon_chain/tests/bellatrix.rs +++ b/beacon_node/beacon_chain/tests/bellatrix.rs @@ -82,7 +82,7 @@ async fn merge_with_terminal_block_hash_override() { let block = &harness.chain.head_snapshot().beacon_block; - let execution_payload = block.message().body().execution_payload().unwrap().clone(); + let execution_payload = block.message().body().execution_payload().unwrap(); if i == 0 { assert_eq!(execution_payload.block_hash(), genesis_pow_block_hash); } @@ -133,7 +133,7 @@ async fn base_altair_bellatrix_with_terminal_block_after_fork() { * Do the Bellatrix fork, without a terminal PoW block. */ - harness.extend_to_slot(bellatrix_fork_slot).await; + Box::pin(harness.extend_to_slot(bellatrix_fork_slot)).await; let bellatrix_head = &harness.chain.head_snapshot().beacon_block; assert!(bellatrix_head.as_bellatrix().is_ok()); @@ -207,15 +207,7 @@ async fn base_altair_bellatrix_with_terminal_block_after_fork() { harness.extend_slots(1).await; let block = &harness.chain.head_snapshot().beacon_block; - execution_payloads.push( - block - .message() - .body() - .execution_payload() - .unwrap() - .clone() - .into(), - ); + execution_payloads.push(block.message().body().execution_payload().unwrap().into()); } verify_execution_payload_chain(execution_payloads.as_slice()); diff --git a/beacon_node/beacon_chain/tests/capella.rs b/beacon_node/beacon_chain/tests/capella.rs index ac97a95721..3ce5702f2e 100644 --- a/beacon_node/beacon_chain/tests/capella.rs +++ b/beacon_node/beacon_chain/tests/capella.rs @@ -54,7 +54,7 @@ async fn base_altair_bellatrix_capella() { /* * Do the Altair fork. */ - harness.extend_to_slot(altair_fork_slot).await; + Box::pin(harness.extend_to_slot(altair_fork_slot)).await; let altair_head = &harness.chain.head_snapshot().beacon_block; assert!(altair_head.as_altair().is_ok()); @@ -63,7 +63,7 @@ async fn base_altair_bellatrix_capella() { /* * Do the Bellatrix fork, without a terminal PoW block. */ - harness.extend_to_slot(bellatrix_fork_slot).await; + Box::pin(harness.extend_to_slot(bellatrix_fork_slot)).await; let bellatrix_head = &harness.chain.head_snapshot().beacon_block; assert!(bellatrix_head.as_bellatrix().is_ok()); @@ -81,7 +81,7 @@ async fn base_altair_bellatrix_capella() { /* * Next Bellatrix block shouldn't include an exec payload. */ - harness.extend_slots(1).await; + Box::pin(harness.extend_slots(1)).await; let one_after_bellatrix_head = &harness.chain.head_snapshot().beacon_block; assert!( @@ -112,7 +112,7 @@ async fn base_altair_bellatrix_capella() { terminal_block.timestamp = timestamp; } }); - harness.extend_slots(1).await; + Box::pin(harness.extend_slots(1)).await; let two_after_bellatrix_head = &harness.chain.head_snapshot().beacon_block; assert!( diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index 729d88450f..01b790bb25 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -413,7 +413,7 @@ async fn invalid_payload_invalidates_parent() { rig.import_block(Payload::Valid).await; // Import a valid transition block. rig.move_to_first_justification(Payload::Syncing).await; - let roots = vec![ + let roots = [ rig.import_block(Payload::Syncing).await, rig.import_block(Payload::Syncing).await, rig.import_block(Payload::Syncing).await, @@ -1052,7 +1052,7 @@ async fn invalid_parent() { // Ensure the block built atop an invalid payload is invalid for gossip. assert!(matches!( - rig.harness.chain.clone().verify_block_for_gossip(block.clone().into()).await, + rig.harness.chain.clone().verify_block_for_gossip(block.clone()).await, Err(BlockError::ParentExecutionPayloadInvalid { parent_root: invalid_root }) if invalid_root == parent_root )); diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 522020e476..73805a8525 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -330,7 +330,7 @@ async fn long_skip() { final_blocks as usize, BlockStrategy::ForkCanonicalChainAt { previous_slot: Slot::new(initial_blocks), - first_slot: Slot::new(initial_blocks + skip_slots as u64 + 1), + first_slot: Slot::new(initial_blocks + skip_slots + 1), }, AttestationStrategy::AllValidators, ) @@ -381,8 +381,7 @@ async fn randao_genesis_storage() { .beacon_state .randao_mixes() .iter() - .find(|x| **x == genesis_value) - .is_some()); + .any(|x| *x == genesis_value)); // Then upon adding one more block, it isn't harness.advance_slot(); @@ -393,14 +392,13 @@ async fn randao_genesis_storage() { AttestationStrategy::AllValidators, ) .await; - assert!(harness + assert!(!harness .chain .head_snapshot() .beacon_state .randao_mixes() .iter() - .find(|x| **x == genesis_value) - .is_none()); + .any(|x| *x == genesis_value)); check_finalization(&harness, num_slots); check_split_slot(&harness, store); @@ -1062,7 +1060,7 @@ fn check_shuffling_compatible( let current_epoch_shuffling_is_compatible = harness.chain.shuffling_is_compatible( &block_root, head_state.current_epoch(), - &head_state, + head_state, ); // Check for consistency with the more expensive shuffling lookup. @@ -1102,7 +1100,7 @@ fn check_shuffling_compatible( let previous_epoch_shuffling_is_compatible = harness.chain.shuffling_is_compatible( &block_root, head_state.previous_epoch(), - &head_state, + head_state, ); harness .chain @@ -1130,14 +1128,11 @@ fn check_shuffling_compatible( // Targeting two epochs before the current epoch should always return false if head_state.current_epoch() >= 2 { - assert_eq!( - harness.chain.shuffling_is_compatible( - &block_root, - head_state.current_epoch() - 2, - &head_state - ), - false - ); + assert!(!harness.chain.shuffling_is_compatible( + &block_root, + head_state.current_epoch() - 2, + head_state + )); } } } @@ -1559,14 +1554,13 @@ async fn prunes_fork_growing_past_youngest_finalized_checkpoint() { .map(Into::into) .collect(); let canonical_state_root = canonical_state.update_tree_hash_cache().unwrap(); - let (canonical_blocks, _, _, _) = rig - .add_attested_blocks_at_slots( - canonical_state, - canonical_state_root, - &canonical_slots, - &honest_validators, - ) - .await; + let (canonical_blocks, _, _, _) = Box::pin(rig.add_attested_blocks_at_slots( + canonical_state, + canonical_state_root, + &canonical_slots, + &honest_validators, + )) + .await; // Postconditions let canonical_blocks: HashMap = canonical_blocks_zeroth_epoch @@ -1939,7 +1933,7 @@ async fn prune_single_block_long_skip() { 2 * slots_per_epoch, 1, 2 * slots_per_epoch, - 2 * slots_per_epoch as u64, + 2 * slots_per_epoch, 1, ) .await; @@ -1961,31 +1955,45 @@ async fn prune_shared_skip_states_mid_epoch() { #[tokio::test] async fn prune_shared_skip_states_epoch_boundaries() { let slots_per_epoch = E::slots_per_epoch(); - pruning_test(slots_per_epoch - 1, 1, slots_per_epoch, 2, slots_per_epoch).await; - pruning_test(slots_per_epoch - 1, 2, slots_per_epoch, 1, slots_per_epoch).await; - pruning_test( - 2 * slots_per_epoch + slots_per_epoch / 2, - slots_per_epoch as u64 / 2, + Box::pin(pruning_test( + slots_per_epoch - 1, + 1, slots_per_epoch, - slots_per_epoch as u64 / 2 + 1, + 2, slots_per_epoch, - ) + )) .await; - pruning_test( - 2 * slots_per_epoch + slots_per_epoch / 2, - slots_per_epoch as u64 / 2, + Box::pin(pruning_test( + slots_per_epoch - 1, + 2, slots_per_epoch, - slots_per_epoch as u64 / 2 + 1, + 1, slots_per_epoch, - ) + )) .await; - pruning_test( + Box::pin(pruning_test( + 2 * slots_per_epoch + slots_per_epoch / 2, + slots_per_epoch / 2, + slots_per_epoch, + slots_per_epoch / 2 + 1, + slots_per_epoch, + )) + .await; + Box::pin(pruning_test( + 2 * slots_per_epoch + slots_per_epoch / 2, + slots_per_epoch / 2, + slots_per_epoch, + slots_per_epoch / 2 + 1, + slots_per_epoch, + )) + .await; + Box::pin(pruning_test( 2 * slots_per_epoch - 1, - slots_per_epoch as u64, + slots_per_epoch, 1, 0, 2 * slots_per_epoch, - ) + )) .await; } @@ -2094,7 +2102,7 @@ async fn pruning_test( ); check_chain_dump( &harness, - (num_initial_blocks + num_canonical_middle_blocks + num_finalization_blocks + 1) as u64, + num_initial_blocks + num_canonical_middle_blocks + num_finalization_blocks + 1, ); let all_canonical_states = harness @@ -2613,8 +2621,7 @@ async fn process_blocks_and_attestations_for_unaligned_checkpoint() { harness.advance_slot(); } harness.extend_to_slot(finalizing_slot - 1).await; - harness - .add_block_at_slot(finalizing_slot, harness.get_current_state()) + Box::pin(harness.add_block_at_slot(finalizing_slot, harness.get_current_state())) .await .unwrap(); diff --git a/beacon_node/beacon_chain/tests/sync_committee_verification.rs b/beacon_node/beacon_chain/tests/sync_committee_verification.rs index d1b3139d42..6d30b8a4e3 100644 --- a/beacon_node/beacon_chain/tests/sync_committee_verification.rs +++ b/beacon_node/beacon_chain/tests/sync_committee_verification.rs @@ -73,7 +73,7 @@ fn get_valid_sync_committee_message_for_block( let head_state = harness.chain.head_beacon_state_cloned(); let (signature, _) = harness .make_sync_committee_messages(&head_state, block_root, slot, relative_sync_committee) - .get(0) + .first() .expect("sync messages should exist") .get(message_index) .expect("first sync message should exist") @@ -104,7 +104,7 @@ fn get_valid_sync_contribution( ); let (_, contribution_opt) = sync_contributions - .get(0) + .first() .expect("sync contributions should exist"); let contribution = contribution_opt .as_ref() diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs index 7ae34ccf38..c641f32b82 100644 --- a/beacon_node/beacon_chain/tests/tests.rs +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -170,7 +170,7 @@ async fn find_reorgs() { harness .extend_chain( - num_blocks_produced as usize, + num_blocks_produced, BlockStrategy::OnCanonicalHead, // No need to produce attestations for this test. AttestationStrategy::SomeValidators(vec![]), @@ -203,7 +203,7 @@ async fn find_reorgs() { assert_eq!( find_reorg_slot( &harness.chain, - &head_state, + head_state, harness.chain.head_beacon_block().canonical_root() ), head_slot @@ -503,7 +503,6 @@ async fn unaggregated_attestations_added_to_fork_choice_some_none() { .unwrap(); let validator_slots: Vec<(usize, Slot)> = (0..VALIDATOR_COUNT) - .into_iter() .map(|validator_index| { let slot = state .get_attestation_duties(validator_index, RelativeEpoch::Current) diff --git a/beacon_node/http_api/tests/broadcast_validation_tests.rs b/beacon_node/http_api/tests/broadcast_validation_tests.rs index 1338f4f180..e1ecf2d4fc 100644 --- a/beacon_node/http_api/tests/broadcast_validation_tests.rs +++ b/beacon_node/http_api/tests/broadcast_validation_tests.rs @@ -322,7 +322,7 @@ pub async fn consensus_gossip() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] pub async fn consensus_partial_pass_only_consensus() { /* this test targets gossip-level validation */ - let validation_level: Option = Some(BroadcastValidation::Consensus); + let validation_level = BroadcastValidation::Consensus; // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing // `validator_count // 32`. @@ -378,7 +378,7 @@ pub async fn consensus_partial_pass_only_consensus() { tester.harness.chain.clone(), &channel.0, test_logger, - validation_level.unwrap(), + validation_level, StatusCode::ACCEPTED, network_globals, ) @@ -615,8 +615,7 @@ pub async fn equivocation_gossip() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] pub async fn equivocation_consensus_late_equivocation() { /* this test targets gossip-level validation */ - let validation_level: Option = - Some(BroadcastValidation::ConsensusAndEquivocation); + let validation_level = BroadcastValidation::ConsensusAndEquivocation; // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing // `validator_count // 32`. @@ -671,7 +670,7 @@ pub async fn equivocation_consensus_late_equivocation() { tester.harness.chain, &channel.0, test_logger, - validation_level.unwrap(), + validation_level, StatusCode::ACCEPTED, network_globals, ) @@ -1228,8 +1227,7 @@ pub async fn blinded_equivocation_gossip() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] pub async fn blinded_equivocation_consensus_late_equivocation() { /* this test targets gossip-level validation */ - let validation_level: Option = - Some(BroadcastValidation::ConsensusAndEquivocation); + let validation_level = BroadcastValidation::ConsensusAndEquivocation; // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing // `validator_count // 32`. @@ -1311,7 +1309,7 @@ pub async fn blinded_equivocation_consensus_late_equivocation() { tester.harness.chain, &channel.0, test_logger, - validation_level.unwrap(), + validation_level, StatusCode::ACCEPTED, network_globals, ) @@ -1465,8 +1463,8 @@ pub async fn block_seen_on_gossip_with_some_blobs() { "need at least 2 blobs for partial reveal" ); - let partial_kzg_proofs = vec![blobs.0.get(0).unwrap().clone()]; - let partial_blobs = vec![blobs.1.get(0).unwrap().clone()]; + let partial_kzg_proofs = vec![*blobs.0.first().unwrap()]; + let partial_blobs = vec![blobs.1.first().unwrap().clone()]; // Simulate the block being seen on gossip. block diff --git a/beacon_node/http_api/tests/interactive_tests.rs b/beacon_node/http_api/tests/interactive_tests.rs index 627b0d0b17..e45dcf221c 100644 --- a/beacon_node/http_api/tests/interactive_tests.rs +++ b/beacon_node/http_api/tests/interactive_tests.rs @@ -139,7 +139,7 @@ impl ForkChoiceUpdates { fn insert(&mut self, update: ForkChoiceUpdateMetadata) { self.updates .entry(update.state.head_block_hash) - .or_insert_with(Vec::new) + .or_default() .push(update); } diff --git a/beacon_node/http_api/tests/status_tests.rs b/beacon_node/http_api/tests/status_tests.rs index 01731530d3..dd481f23ba 100644 --- a/beacon_node/http_api/tests/status_tests.rs +++ b/beacon_node/http_api/tests/status_tests.rs @@ -57,18 +57,18 @@ async fn el_syncing_then_synced() { mock_el.el.upcheck().await; let api_response = tester.client.get_node_syncing().await.unwrap().data; - assert_eq!(api_response.el_offline, false); - assert_eq!(api_response.is_optimistic, false); - assert_eq!(api_response.is_syncing, false); + assert!(!api_response.el_offline); + assert!(!api_response.is_optimistic); + assert!(!api_response.is_syncing); // EL synced mock_el.server.set_syncing_response(Ok(false)); mock_el.el.upcheck().await; let api_response = tester.client.get_node_syncing().await.unwrap().data; - assert_eq!(api_response.el_offline, false); - assert_eq!(api_response.is_optimistic, false); - assert_eq!(api_response.is_syncing, false); + assert!(!api_response.el_offline); + assert!(!api_response.is_optimistic); + assert!(!api_response.is_syncing); } /// Check `syncing` endpoint when the EL is offline (errors on upcheck). @@ -85,9 +85,9 @@ async fn el_offline() { mock_el.el.upcheck().await; let api_response = tester.client.get_node_syncing().await.unwrap().data; - assert_eq!(api_response.el_offline, true); - assert_eq!(api_response.is_optimistic, false); - assert_eq!(api_response.is_syncing, false); + assert!(api_response.el_offline); + assert!(!api_response.is_optimistic); + assert!(!api_response.is_syncing); } /// Check `syncing` endpoint when the EL errors on newPaylod but is not fully offline. @@ -128,9 +128,9 @@ async fn el_error_on_new_payload() { // The EL should now be *offline* according to the API. let api_response = tester.client.get_node_syncing().await.unwrap().data; - assert_eq!(api_response.el_offline, true); - assert_eq!(api_response.is_optimistic, false); - assert_eq!(api_response.is_syncing, false); + assert!(api_response.el_offline); + assert!(!api_response.is_optimistic); + assert!(!api_response.is_syncing); // Processing a block successfully should remove the status. mock_el.server.set_new_payload_status( @@ -144,9 +144,9 @@ async fn el_error_on_new_payload() { harness.process_block_result((block, blobs)).await.unwrap(); let api_response = tester.client.get_node_syncing().await.unwrap().data; - assert_eq!(api_response.el_offline, false); - assert_eq!(api_response.is_optimistic, false); - assert_eq!(api_response.is_syncing, false); + assert!(!api_response.el_offline); + assert!(!api_response.is_optimistic); + assert!(!api_response.is_syncing); } /// Check `node health` endpoint when the EL is offline. diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 080a393b4d..7007a14466 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -274,10 +274,10 @@ impl ApiTester { let mock_builder_server = harness.set_mock_builder(beacon_url.clone()); // Start the mock builder service prior to building the chain out. - harness.runtime.task_executor.spawn( - async move { mock_builder_server.await }, - "mock_builder_server", - ); + harness + .runtime + .task_executor + .spawn(mock_builder_server, "mock_builder_server"); let mock_builder = harness.mock_builder.clone(); @@ -641,7 +641,7 @@ impl ApiTester { self } - pub async fn test_beacon_blocks_finalized(self) -> Self { + pub async fn test_beacon_blocks_finalized(self) -> Self { for block_id in self.interesting_block_ids() { let block_root = block_id.root(&self.chain); let block = block_id.full_block(&self.chain).await; @@ -678,7 +678,7 @@ impl ApiTester { self } - pub async fn test_beacon_blinded_blocks_finalized(self) -> Self { + pub async fn test_beacon_blinded_blocks_finalized(self) -> Self { for block_id in self.interesting_block_ids() { let block_root = block_id.root(&self.chain); let block = block_id.full_block(&self.chain).await; @@ -819,7 +819,7 @@ impl ApiTester { let validator_index_ids = validator_indices .iter() .cloned() - .map(|i| ValidatorId::Index(i)) + .map(ValidatorId::Index) .collect::>(); let unsupported_media_response = self @@ -859,7 +859,7 @@ impl ApiTester { let validator_index_ids = validator_indices .iter() .cloned() - .map(|i| ValidatorId::Index(i)) + .map(ValidatorId::Index) .collect::>(); let validator_pubkey_ids = validator_indices .iter() @@ -910,7 +910,7 @@ impl ApiTester { for i in validator_indices { if i < state.balances().len() as u64 { validators.push(ValidatorBalanceData { - index: i as u64, + index: i, balance: *state.balances().get(i as usize).unwrap(), }); } @@ -944,7 +944,7 @@ impl ApiTester { let validator_index_ids = validator_indices .iter() .cloned() - .map(|i| ValidatorId::Index(i)) + .map(ValidatorId::Index) .collect::>(); let validator_pubkey_ids = validator_indices .iter() @@ -1012,7 +1012,7 @@ impl ApiTester { || statuses.contains(&status.superstatus()) { validators.push(ValidatorData { - index: i as u64, + index: i, balance: *state.balances().get(i as usize).unwrap(), status, validator, @@ -1641,11 +1641,7 @@ impl ApiTester { let (block, _, _) = block_id.full_block(&self.chain).await.unwrap(); let num_blobs = block.num_expected_blobs(); let blob_indices = if use_indices { - Some( - (0..num_blobs.saturating_sub(1) as u64) - .into_iter() - .collect::>(), - ) + Some((0..num_blobs.saturating_sub(1) as u64).collect::>()) } else { None }; @@ -1663,7 +1659,7 @@ impl ApiTester { blob_indices.map_or(num_blobs, |indices| indices.len()) ); let expected = block.slot(); - assert_eq!(result.get(0).unwrap().slot(), expected); + assert_eq!(result.first().unwrap().slot(), expected); self } @@ -1701,9 +1697,9 @@ impl ApiTester { break; } } - let test_slot = test_slot.expect(&format!( - "should be able to find a block matching zero_blobs={zero_blobs}" - )); + let test_slot = test_slot.unwrap_or_else(|| { + panic!("should be able to find a block matching zero_blobs={zero_blobs}") + }); match self .client @@ -1772,7 +1768,6 @@ impl ApiTester { .attestations() .map(|att| att.clone_as_attestation()) .collect::>() - .into() }, ); @@ -1909,7 +1904,7 @@ impl ApiTester { let result = match self .client - .get_beacon_light_client_updates::(current_sync_committee_period as u64, 1) + .get_beacon_light_client_updates::(current_sync_committee_period, 1) .await { Ok(result) => result, @@ -1921,7 +1916,7 @@ impl ApiTester { .light_client_server_cache .get_light_client_updates( &self.chain.store, - current_sync_committee_period as u64, + current_sync_committee_period, 1, &self.chain.spec, ) @@ -2314,7 +2309,7 @@ impl ApiTester { .unwrap() .data .is_syncing; - assert_eq!(is_syncing, true); + assert!(is_syncing); // Reset sync state. *self @@ -2364,7 +2359,7 @@ impl ApiTester { pub async fn test_get_node_peers_by_id(self) -> Self { let result = self .client - .get_node_peers_by_id(self.external_peer_id.clone()) + .get_node_peers_by_id(self.external_peer_id) .await .unwrap() .data; @@ -3514,6 +3509,7 @@ impl ApiTester { self } + #[allow(clippy::await_holding_lock)] // This is a test, so it should be fine. pub async fn test_get_validator_aggregate_attestation(self) -> Self { if self .chain @@ -4058,7 +4054,7 @@ impl ApiTester { ProduceBlockV3Response::Full(_) => panic!("Expecting a blinded payload"), }; - let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); + let expected_fee_recipient = Address::from_low_u64_be(proposer_index); assert_eq!(payload.fee_recipient(), expected_fee_recipient); assert_eq!(payload.gas_limit(), DEFAULT_GAS_LIMIT); @@ -4085,7 +4081,7 @@ impl ApiTester { ProduceBlockV3Response::Blinded(_) => panic!("Expecting a full payload"), }; - let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); + let expected_fee_recipient = Address::from_low_u64_be(proposer_index); assert_eq!(payload.fee_recipient(), expected_fee_recipient); // This is the graffiti of the mock execution layer, not the builder. assert_eq!(payload.extra_data(), mock_el_extra_data::()); @@ -4113,7 +4109,7 @@ impl ApiTester { ProduceBlockV3Response::Full(_) => panic!("Expecting a blinded payload"), }; - let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); + let expected_fee_recipient = Address::from_low_u64_be(proposer_index); assert_eq!(payload.fee_recipient(), expected_fee_recipient); assert_eq!(payload.gas_limit(), DEFAULT_GAS_LIMIT); @@ -4137,7 +4133,7 @@ impl ApiTester { .unwrap() .into(); - let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); + let expected_fee_recipient = Address::from_low_u64_be(proposer_index); assert_eq!(payload.fee_recipient(), expected_fee_recipient); assert_eq!(payload.gas_limit(), DEFAULT_GAS_LIMIT); @@ -4183,7 +4179,7 @@ impl ApiTester { .unwrap() .into(); - let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); + let expected_fee_recipient = Address::from_low_u64_be(proposer_index); assert_eq!(payload.fee_recipient(), expected_fee_recipient); assert_eq!(payload.gas_limit(), builder_limit); @@ -4267,7 +4263,7 @@ impl ApiTester { ProduceBlockV3Response::Full(_) => panic!("Expecting a blinded payload"), }; - let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); + let expected_fee_recipient = Address::from_low_u64_be(proposer_index); assert_eq!(payload.fee_recipient(), expected_fee_recipient); assert_eq!(payload.gas_limit(), 30_000_000); @@ -5140,9 +5136,8 @@ impl ApiTester { pub async fn test_builder_chain_health_optimistic_head(self) -> Self { // Make sure the next payload verification will return optimistic before advancing the chain. - self.harness.mock_execution_layer.as_ref().map(|el| { + self.harness.mock_execution_layer.as_ref().inspect(|el| { el.server.all_payloads_syncing(true); - el }); self.harness .extend_chain( @@ -5169,7 +5164,7 @@ impl ApiTester { .unwrap() .into(); - let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); + let expected_fee_recipient = Address::from_low_u64_be(proposer_index); assert_eq!(payload.fee_recipient(), expected_fee_recipient); // If this cache is populated, it indicates fallback to the local EE was correctly used. @@ -5188,9 +5183,8 @@ impl ApiTester { pub async fn test_builder_v3_chain_health_optimistic_head(self) -> Self { // Make sure the next payload verification will return optimistic before advancing the chain. - self.harness.mock_execution_layer.as_ref().map(|el| { + self.harness.mock_execution_layer.as_ref().inspect(|el| { el.server.all_payloads_syncing(true); - el }); self.harness .extend_chain( @@ -5220,7 +5214,7 @@ impl ApiTester { ProduceBlockV3Response::Blinded(_) => panic!("Expecting a full payload"), }; - let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); + let expected_fee_recipient = Address::from_low_u64_be(proposer_index); assert_eq!(payload.fee_recipient(), expected_fee_recipient); self @@ -6101,16 +6095,17 @@ impl ApiTester { assert_eq!(result.execution_optimistic, Some(false)); // Change head to be optimistic. - self.chain + if let Some(head_node) = self + .chain .canonical_head .fork_choice_write_lock() .proto_array_mut() .core_proto_array_mut() .nodes .last_mut() - .map(|head_node| { - head_node.execution_status = ExecutionStatus::Optimistic(ExecutionBlockHash::zero()) - }); + { + head_node.execution_status = ExecutionStatus::Optimistic(ExecutionBlockHash::zero()) + } // Check responses are now optimistic. let result = self @@ -6143,8 +6138,8 @@ async fn poll_events, eth2::Error>> + Unpin }; tokio::select! { - _ = collect_stream_fut => {events} - _ = tokio::time::sleep(timeout) => { return events; } + _ = collect_stream_fut => { events } + _ = tokio::time::sleep(timeout) => { events } } } @@ -6180,31 +6175,31 @@ async fn test_unsupported_media_response() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn beacon_get() { +async fn beacon_get_state_hashes() { + ApiTester::new() + .await + .test_beacon_states_root_finalized() + .await + .test_beacon_states_finality_checkpoints_finalized() + .await + .test_beacon_states_root() + .await + .test_beacon_states_finality_checkpoints() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn beacon_get_state_info() { ApiTester::new() .await .test_beacon_genesis() .await - .test_beacon_states_root_finalized() - .await .test_beacon_states_fork_finalized() .await - .test_beacon_states_finality_checkpoints_finalized() - .await - .test_beacon_headers_block_id_finalized() - .await - .test_beacon_blocks_finalized::() - .await - .test_beacon_blinded_blocks_finalized::() - .await .test_debug_beacon_states_finalized() .await - .test_beacon_states_root() - .await .test_beacon_states_fork() .await - .test_beacon_states_finality_checkpoints() - .await .test_beacon_states_validators() .await .test_beacon_states_validator_balances() @@ -6214,6 +6209,18 @@ async fn beacon_get() { .test_beacon_states_validator_id() .await .test_beacon_states_randao() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn beacon_get_blocks() { + ApiTester::new() + .await + .test_beacon_headers_block_id_finalized() + .await + .test_beacon_blocks_finalized() + .await + .test_beacon_blinded_blocks_finalized() .await .test_beacon_headers_all_slots() .await @@ -6228,6 +6235,12 @@ async fn beacon_get() { .test_beacon_blocks_attestations() .await .test_beacon_blocks_root() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn beacon_get_pools() { + ApiTester::new() .await .test_get_beacon_pool_attestations() .await diff --git a/beacon_node/lighthouse_network/src/service/gossip_cache.rs b/beacon_node/lighthouse_network/src/service/gossip_cache.rs index 0ad31ff2e8..e46c69dc71 100644 --- a/beacon_node/lighthouse_network/src/service/gossip_cache.rs +++ b/beacon_node/lighthouse_network/src/service/gossip_cache.rs @@ -250,18 +250,17 @@ impl futures::stream::Stream for GossipCache { Poll::Ready(Some(expired)) => { let expected_key = expired.key(); let (topic, data) = expired.into_inner(); - match self.topic_msgs.get_mut(&topic) { - Some(msgs) => { - let key = msgs.remove(&data); - debug_assert_eq!(key, Some(expected_key)); - if msgs.is_empty() { - // no more messages for this topic. - self.topic_msgs.remove(&topic); - } - } - None => { - #[cfg(debug_assertions)] - panic!("Topic for registered message is not present.") + let topic_msg = self.topic_msgs.get_mut(&topic); + debug_assert!( + topic_msg.is_some(), + "Topic for registered message is not present." + ); + if let Some(msgs) = topic_msg { + let key = msgs.remove(&data); + debug_assert_eq!(key, Some(expected_key)); + if msgs.is_empty() { + // no more messages for this topic. + self.topic_msgs.remove(&topic); } } Poll::Ready(Some(Ok(topic))) diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index 9d774d97c1..7e27a91bd6 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -527,7 +527,7 @@ impl TestRig { self.assert_event_journal( &expected .iter() - .map(|ev| Into::<&'static str>::into(ev)) + .map(Into::<&'static str>::into) .chain(std::iter::once(WORKER_FREED)) .chain(std::iter::once(NOTHING_TO_DO)) .collect::>(), diff --git a/beacon_node/network/src/service/tests.rs b/beacon_node/network/src/service/tests.rs index c46e46e0fa..32bbfcbcaa 100644 --- a/beacon_node/network/src/service/tests.rs +++ b/beacon_node/network/src/service/tests.rs @@ -1,235 +1,229 @@ -#[cfg(not(debug_assertions))] -#[cfg(test)] -mod tests { - use crate::persisted_dht::load_dht; - use crate::{NetworkConfig, NetworkService}; - use beacon_chain::test_utils::BeaconChainHarness; - use beacon_chain::BeaconChainTypes; - use beacon_processor::{BeaconProcessorChannels, BeaconProcessorConfig}; - use futures::StreamExt; - use lighthouse_network::types::{GossipEncoding, GossipKind}; - use lighthouse_network::{Enr, GossipTopic}; - use slog::{o, Drain, Level, Logger}; - use sloggers::{null::NullLoggerBuilder, Build}; - use std::str::FromStr; - use std::sync::Arc; - use tokio::runtime::Runtime; - use types::{Epoch, EthSpec, ForkName, MinimalEthSpec, SubnetId}; +#![cfg(not(debug_assertions))] +#![cfg(test)] +use crate::persisted_dht::load_dht; +use crate::{NetworkConfig, NetworkService}; +use beacon_chain::test_utils::BeaconChainHarness; +use beacon_chain::BeaconChainTypes; +use beacon_processor::{BeaconProcessorChannels, BeaconProcessorConfig}; +use futures::StreamExt; +use lighthouse_network::types::{GossipEncoding, GossipKind}; +use lighthouse_network::{Enr, GossipTopic}; +use slog::{o, Drain, Level, Logger}; +use sloggers::{null::NullLoggerBuilder, Build}; +use std::str::FromStr; +use std::sync::Arc; +use tokio::runtime::Runtime; +use types::{Epoch, EthSpec, ForkName, MinimalEthSpec, SubnetId}; - impl NetworkService { - fn get_topic_params(&self, topic: GossipTopic) -> Option<&gossipsub::TopicScoreParams> { - self.libp2p.get_topic_params(topic) - } +impl NetworkService { + fn get_topic_params(&self, topic: GossipTopic) -> Option<&gossipsub::TopicScoreParams> { + self.libp2p.get_topic_params(topic) } +} - fn get_logger(actual_log: bool) -> Logger { - if actual_log { - let drain = { - let decorator = slog_term::TermDecorator::new().build(); - let decorator = - logging::AlignedTermDecorator::new(decorator, logging::MAX_MESSAGE_WIDTH); - let drain = slog_term::FullFormat::new(decorator).build().fuse(); - let drain = slog_async::Async::new(drain).chan_size(2048).build(); - drain.filter_level(Level::Debug) - }; +fn get_logger(actual_log: bool) -> Logger { + if actual_log { + let drain = { + let decorator = slog_term::TermDecorator::new().build(); + let decorator = + logging::AlignedTermDecorator::new(decorator, logging::MAX_MESSAGE_WIDTH); + let drain = slog_term::FullFormat::new(decorator).build().fuse(); + let drain = slog_async::Async::new(drain).chan_size(2048).build(); + drain.filter_level(Level::Debug) + }; - Logger::root(drain.fuse(), o!()) - } else { - let builder = NullLoggerBuilder; - builder.build().expect("should build logger") - } + Logger::root(drain.fuse(), o!()) + } else { + let builder = NullLoggerBuilder; + builder.build().expect("should build logger") } +} - #[test] - fn test_dht_persistence() { - let log = get_logger(false); +#[test] +fn test_dht_persistence() { + let log = get_logger(false); - let beacon_chain = BeaconChainHarness::builder(MinimalEthSpec) - .default_spec() - .deterministic_keypairs(8) - .fresh_ephemeral_store() - .build() - .chain; + let beacon_chain = BeaconChainHarness::builder(MinimalEthSpec) + .default_spec() + .deterministic_keypairs(8) + .fresh_ephemeral_store() + .build() + .chain; - let store = beacon_chain.store.clone(); + let store = beacon_chain.store.clone(); - let enr1 = Enr::from_str("enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8").unwrap(); - let enr2 = Enr::from_str("enr:-IS4QJ2d11eu6dC7E7LoXeLMgMP3kom1u3SE8esFSWvaHoo0dP1jg8O3-nx9ht-EO3CmG7L6OkHcMmoIh00IYWB92QABgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQIB_c-jQMOXsbjWkbN-Oj99H57gfId5pfb4wa1qxwV4CIN1ZHCCIyk").unwrap(); - let enrs = vec![enr1, enr2]; + let enr1 = Enr::from_str("enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8").unwrap(); + let enr2 = Enr::from_str("enr:-IS4QJ2d11eu6dC7E7LoXeLMgMP3kom1u3SE8esFSWvaHoo0dP1jg8O3-nx9ht-EO3CmG7L6OkHcMmoIh00IYWB92QABgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQIB_c-jQMOXsbjWkbN-Oj99H57gfId5pfb4wa1qxwV4CIN1ZHCCIyk").unwrap(); + let enrs = vec![enr1, enr2]; - let runtime = Arc::new(Runtime::new().unwrap()); + let runtime = Arc::new(Runtime::new().unwrap()); - let (signal, exit) = async_channel::bounded(1); + let (signal, exit) = async_channel::bounded(1); + let (shutdown_tx, _) = futures::channel::mpsc::channel(1); + let executor = + task_executor::TaskExecutor::new(Arc::downgrade(&runtime), exit, log.clone(), shutdown_tx); + + let mut config = NetworkConfig::default(); + config.set_ipv4_listening_address(std::net::Ipv4Addr::UNSPECIFIED, 21212, 21212, 21213); + config.discv5_config.table_filter = |_| true; // Do not ignore local IPs + config.upnp_enabled = false; + config.boot_nodes_enr = enrs.clone(); + let config = Arc::new(config); + runtime.block_on(async move { + // Create a new network service which implicitly gets dropped at the + // end of the block. + + let BeaconProcessorChannels { + beacon_processor_tx, + beacon_processor_rx: _beacon_processor_rx, + work_reprocessing_tx, + work_reprocessing_rx: _work_reprocessing_rx, + } = <_>::default(); + + let _network_service = NetworkService::start( + beacon_chain.clone(), + config, + executor, + None, + beacon_processor_tx, + work_reprocessing_tx, + ) + .await + .unwrap(); + drop(signal); + }); + + let raw_runtime = Arc::try_unwrap(runtime).unwrap(); + raw_runtime.shutdown_timeout(tokio::time::Duration::from_secs(300)); + + // Load the persisted dht from the store + let persisted_enrs = load_dht(store); + assert!( + persisted_enrs.contains(&enrs[0]), + "should have persisted the first ENR to store" + ); + assert!( + persisted_enrs.contains(&enrs[1]), + "should have persisted the second ENR to store" + ); +} + +// Test removing topic weight on old topics when a fork happens. +#[test] +fn test_removing_topic_weight_on_old_topics() { + let runtime = Arc::new(Runtime::new().unwrap()); + + // Capella spec + let mut spec = MinimalEthSpec::default_spec(); + spec.altair_fork_epoch = Some(Epoch::new(0)); + spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + spec.capella_fork_epoch = Some(Epoch::new(1)); + + // Build beacon chain. + let beacon_chain = BeaconChainHarness::builder(MinimalEthSpec) + .spec(spec.clone().into()) + .deterministic_keypairs(8) + .fresh_ephemeral_store() + .mock_execution_layer() + .build() + .chain; + let (next_fork_name, _) = beacon_chain.duration_to_next_fork().expect("next fork"); + assert_eq!(next_fork_name, ForkName::Capella); + + // Build network service. + let (mut network_service, network_globals, _network_senders) = runtime.block_on(async { + let (_, exit) = async_channel::bounded(1); let (shutdown_tx, _) = futures::channel::mpsc::channel(1); let executor = task_executor::TaskExecutor::new( Arc::downgrade(&runtime), exit, - log.clone(), + get_logger(false), shutdown_tx, ); let mut config = NetworkConfig::default(); - config.set_ipv4_listening_address(std::net::Ipv4Addr::UNSPECIFIED, 21212, 21212, 21213); + config.set_ipv4_listening_address(std::net::Ipv4Addr::UNSPECIFIED, 21214, 21214, 21215); config.discv5_config.table_filter = |_| true; // Do not ignore local IPs config.upnp_enabled = false; - config.boot_nodes_enr = enrs.clone(); let config = Arc::new(config); - runtime.block_on(async move { - // Create a new network service which implicitly gets dropped at the - // end of the block. - let BeaconProcessorChannels { - beacon_processor_tx, - beacon_processor_rx: _beacon_processor_rx, - work_reprocessing_tx, - work_reprocessing_rx: _work_reprocessing_rx, - } = <_>::default(); + let beacon_processor_channels = + BeaconProcessorChannels::new(&BeaconProcessorConfig::default()); + NetworkService::build( + beacon_chain.clone(), + config, + executor.clone(), + None, + beacon_processor_channels.beacon_processor_tx, + beacon_processor_channels.work_reprocessing_tx, + ) + .await + .unwrap() + }); - let _network_service = NetworkService::start( - beacon_chain.clone(), - config, - executor, - None, - beacon_processor_tx, - work_reprocessing_tx, - ) - .await - .unwrap(); - drop(signal); - }); - - let raw_runtime = Arc::try_unwrap(runtime).unwrap(); - raw_runtime.shutdown_timeout(tokio::time::Duration::from_secs(300)); - - // Load the persisted dht from the store - let persisted_enrs = load_dht(store); - assert!( - persisted_enrs.contains(&enrs[0]), - "should have persisted the first ENR to store" - ); - assert!( - persisted_enrs.contains(&enrs[1]), - "should have persisted the second ENR to store" - ); - } - - // Test removing topic weight on old topics when a fork happens. - #[test] - fn test_removing_topic_weight_on_old_topics() { - let runtime = Arc::new(Runtime::new().unwrap()); - - // Capella spec - let mut spec = MinimalEthSpec::default_spec(); - spec.altair_fork_epoch = Some(Epoch::new(0)); - spec.bellatrix_fork_epoch = Some(Epoch::new(0)); - spec.capella_fork_epoch = Some(Epoch::new(1)); - - // Build beacon chain. - let beacon_chain = BeaconChainHarness::builder(MinimalEthSpec) - .spec(spec.clone().into()) - .deterministic_keypairs(8) - .fresh_ephemeral_store() - .mock_execution_layer() - .build() - .chain; - let (next_fork_name, _) = beacon_chain.duration_to_next_fork().expect("next fork"); - assert_eq!(next_fork_name, ForkName::Capella); - - // Build network service. - let (mut network_service, network_globals, _network_senders) = runtime.block_on(async { - let (_, exit) = async_channel::bounded(1); - let (shutdown_tx, _) = futures::channel::mpsc::channel(1); - let executor = task_executor::TaskExecutor::new( - Arc::downgrade(&runtime), - exit, - get_logger(false), - shutdown_tx, - ); - - let mut config = NetworkConfig::default(); - config.set_ipv4_listening_address(std::net::Ipv4Addr::UNSPECIFIED, 21214, 21214, 21215); - config.discv5_config.table_filter = |_| true; // Do not ignore local IPs - config.upnp_enabled = false; - let config = Arc::new(config); - - let beacon_processor_channels = - BeaconProcessorChannels::new(&BeaconProcessorConfig::default()); - NetworkService::build( - beacon_chain.clone(), - config, - executor.clone(), - None, - beacon_processor_channels.beacon_processor_tx, - beacon_processor_channels.work_reprocessing_tx, - ) - .await - .unwrap() - }); - - // Subscribe to the topics. - runtime.block_on(async { - while network_globals.gossipsub_subscriptions.read().len() < 2 { - if let Some(msg) = network_service.subnet_service.next().await { - network_service.on_subnet_service_msg(msg); - } + // Subscribe to the topics. + runtime.block_on(async { + while network_globals.gossipsub_subscriptions.read().len() < 2 { + if let Some(msg) = network_service.subnet_service.next().await { + network_service.on_subnet_service_msg(msg); } - }); - - // Make sure the service is subscribed to the topics. - let (old_topic1, old_topic2) = { - let mut subnets = SubnetId::compute_attestation_subnets( - network_globals.local_enr().node_id().raw(), - &spec, - ) - .collect::>(); - assert_eq!(2, subnets.len()); - - let old_fork_digest = beacon_chain.enr_fork_id().fork_digest; - let old_topic1 = GossipTopic::new( - GossipKind::Attestation(subnets.pop().unwrap()), - GossipEncoding::SSZSnappy, - old_fork_digest, - ); - let old_topic2 = GossipTopic::new( - GossipKind::Attestation(subnets.pop().unwrap()), - GossipEncoding::SSZSnappy, - old_fork_digest, - ); - - (old_topic1, old_topic2) - }; - let subscriptions = network_globals.gossipsub_subscriptions.read().clone(); - assert_eq!(2, subscriptions.len()); - assert!(subscriptions.contains(&old_topic1)); - assert!(subscriptions.contains(&old_topic2)); - let old_topic_params1 = network_service - .get_topic_params(old_topic1.clone()) - .expect("topic score params"); - assert!(old_topic_params1.topic_weight > 0.0); - let old_topic_params2 = network_service - .get_topic_params(old_topic2.clone()) - .expect("topic score params"); - assert!(old_topic_params2.topic_weight > 0.0); - - // Advance slot to the next fork - for _ in 0..MinimalEthSpec::slots_per_epoch() { - beacon_chain.slot_clock.advance_slot(); } + }); - // Run `NetworkService::update_next_fork()`. - runtime.block_on(async { - network_service.update_next_fork(); - }); + // Make sure the service is subscribed to the topics. + let (old_topic1, old_topic2) = { + let mut subnets = SubnetId::compute_attestation_subnets( + network_globals.local_enr().node_id().raw(), + &spec, + ) + .collect::>(); + assert_eq!(2, subnets.len()); - // Check that topic_weight on the old topics has been zeroed. - let old_topic_params1 = network_service - .get_topic_params(old_topic1) - .expect("topic score params"); - assert_eq!(0.0, old_topic_params1.topic_weight); + let old_fork_digest = beacon_chain.enr_fork_id().fork_digest; + let old_topic1 = GossipTopic::new( + GossipKind::Attestation(subnets.pop().unwrap()), + GossipEncoding::SSZSnappy, + old_fork_digest, + ); + let old_topic2 = GossipTopic::new( + GossipKind::Attestation(subnets.pop().unwrap()), + GossipEncoding::SSZSnappy, + old_fork_digest, + ); - let old_topic_params2 = network_service - .get_topic_params(old_topic2) - .expect("topic score params"); - assert_eq!(0.0, old_topic_params2.topic_weight); + (old_topic1, old_topic2) + }; + let subscriptions = network_globals.gossipsub_subscriptions.read().clone(); + assert_eq!(2, subscriptions.len()); + assert!(subscriptions.contains(&old_topic1)); + assert!(subscriptions.contains(&old_topic2)); + let old_topic_params1 = network_service + .get_topic_params(old_topic1.clone()) + .expect("topic score params"); + assert!(old_topic_params1.topic_weight > 0.0); + let old_topic_params2 = network_service + .get_topic_params(old_topic2.clone()) + .expect("topic score params"); + assert!(old_topic_params2.topic_weight > 0.0); + + // Advance slot to the next fork + for _ in 0..MinimalEthSpec::slots_per_epoch() { + beacon_chain.slot_clock.advance_slot(); } + + // Run `NetworkService::update_next_fork()`. + runtime.block_on(async { + network_service.update_next_fork(); + }); + + // Check that topic_weight on the old topics has been zeroed. + let old_topic_params1 = network_service + .get_topic_params(old_topic1) + .expect("topic score params"); + assert_eq!(0.0, old_topic_params1.topic_weight); + + let old_topic_params2 = network_service + .get_topic_params(old_topic2) + .expect("topic score params"); + assert_eq!(0.0, old_topic_params2.topic_weight); } diff --git a/beacon_node/operation_pool/src/lib.rs b/beacon_node/operation_pool/src/lib.rs index 3a002bf870..d01c73118c 100644 --- a/beacon_node/operation_pool/src/lib.rs +++ b/beacon_node/operation_pool/src/lib.rs @@ -877,11 +877,11 @@ mod release_tests { let (harness, ref spec) = attestation_test_state::(1); // Only run this test on the phase0 hard-fork. - if spec.altair_fork_epoch != None { + if spec.altair_fork_epoch.is_some() { return; } - let mut state = get_current_state_initialize_epoch_cache(&harness, &spec); + let mut state = get_current_state_initialize_epoch_cache(&harness, spec); let slot = state.slot(); let committees = state .get_beacon_committees_at_slot(slot) @@ -902,10 +902,10 @@ mod release_tests { ); for (atts, aggregate) in &attestations { - let att2 = aggregate.as_ref().unwrap().message().aggregate().clone(); + let att2 = aggregate.as_ref().unwrap().message().aggregate(); let att1 = atts - .into_iter() + .iter() .map(|(att, _)| att) .take(2) .fold::>, _>(None, |att, new_att| { @@ -946,7 +946,7 @@ mod release_tests { .unwrap(); assert_eq!( - committees.get(0).unwrap().committee.len() - 2, + committees.first().unwrap().committee.len() - 2, earliest_attestation_validators( &att2_split.as_ref(), &state, @@ -963,7 +963,7 @@ mod release_tests { let (harness, ref spec) = attestation_test_state::(1); let op_pool = OperationPool::::new(); - let mut state = get_current_state_initialize_epoch_cache(&harness, &spec); + let mut state = get_current_state_initialize_epoch_cache(&harness, spec); let slot = state.slot(); let committees = state @@ -1020,7 +1020,7 @@ mod release_tests { let agg_att = &block_attestations[0]; assert_eq!( agg_att.num_set_aggregation_bits(), - spec.target_committee_size as usize + spec.target_committee_size ); // Prune attestations shouldn't do anything at this point. @@ -1039,7 +1039,7 @@ mod release_tests { fn attestation_duplicate() { let (harness, ref spec) = attestation_test_state::(1); - let state = get_current_state_initialize_epoch_cache(&harness, &spec); + let state = get_current_state_initialize_epoch_cache(&harness, spec); let op_pool = OperationPool::::new(); @@ -1082,7 +1082,7 @@ mod release_tests { fn attestation_pairwise_overlapping() { let (harness, ref spec) = attestation_test_state::(1); - let state = get_current_state_initialize_epoch_cache(&harness, &spec); + let state = get_current_state_initialize_epoch_cache(&harness, spec); let op_pool = OperationPool::::new(); @@ -1113,19 +1113,17 @@ mod release_tests { let aggs1 = atts1 .chunks_exact(step_size * 2) .map(|chunk| { - let agg = chunk.into_iter().map(|(att, _)| att).fold::, - >, _>( - None, - |att, new_att| { + let agg = chunk + .iter() + .map(|(att, _)| att) + .fold::>, _>(None, |att, new_att| { if let Some(mut a) = att { a.aggregate(new_att.to_ref()); Some(a) } else { Some(new_att.clone()) } - }, - ); + }); agg.unwrap() }) .collect::>(); @@ -1136,19 +1134,17 @@ mod release_tests { .as_slice() .chunks_exact(step_size * 2) .map(|chunk| { - let agg = chunk.into_iter().map(|(att, _)| att).fold::, - >, _>( - None, - |att, new_att| { + let agg = chunk + .iter() + .map(|(att, _)| att) + .fold::>, _>(None, |att, new_att| { if let Some(mut a) = att { a.aggregate(new_att.to_ref()); Some(a) } else { Some(new_att.clone()) } - }, - ); + }); agg.unwrap() }) .collect::>(); @@ -1181,7 +1177,7 @@ mod release_tests { let (harness, ref spec) = attestation_test_state::(num_committees); - let mut state = get_current_state_initialize_epoch_cache(&harness, &spec); + let mut state = get_current_state_initialize_epoch_cache(&harness, spec); let op_pool = OperationPool::::new(); @@ -1194,7 +1190,7 @@ mod release_tests { .collect::>(); let max_attestations = ::MaxAttestations::to_usize(); - let target_committee_size = spec.target_committee_size as usize; + let target_committee_size = spec.target_committee_size; let num_validators = num_committees * MainnetEthSpec::slots_per_epoch() as usize * spec.target_committee_size; @@ -1209,12 +1205,12 @@ mod release_tests { let insert_attestations = |attestations: Vec<(Attestation, SubnetId)>, step_size| { - let att_0 = attestations.get(0).unwrap().0.clone(); + let att_0 = attestations.first().unwrap().0.clone(); let aggs = attestations .chunks_exact(step_size) .map(|chunk| { chunk - .into_iter() + .iter() .map(|(att, _)| att) .fold::, _>( att_0.clone(), @@ -1296,7 +1292,7 @@ mod release_tests { let (harness, ref spec) = attestation_test_state::(num_committees); - let mut state = get_current_state_initialize_epoch_cache(&harness, &spec); + let mut state = get_current_state_initialize_epoch_cache(&harness, spec); let op_pool = OperationPool::::new(); let slot = state.slot(); @@ -1308,7 +1304,7 @@ mod release_tests { .collect::>(); let max_attestations = ::MaxAttestations::to_usize(); - let target_committee_size = spec.target_committee_size as usize; + let target_committee_size = spec.target_committee_size; // Each validator will have a multiple of 1_000_000_000 wei. // Safe from overflow unless there are about 18B validators (2^64 / 1_000_000_000). @@ -1329,12 +1325,12 @@ mod release_tests { let insert_attestations = |attestations: Vec<(Attestation, SubnetId)>, step_size| { - let att_0 = attestations.get(0).unwrap().0.clone(); + let att_0 = attestations.first().unwrap().0.clone(); let aggs = attestations .chunks_exact(step_size) .map(|chunk| { chunk - .into_iter() + .iter() .map(|(att, _)| att) .fold::, _>( att_0.clone(), @@ -1615,7 +1611,6 @@ mod release_tests { let block_root = *state .get_block_root(state.slot() - Slot::new(1)) - .ok() .expect("block root should exist at slot"); let contributions = harness.make_sync_contributions( &state, @@ -1674,7 +1669,6 @@ mod release_tests { let state = harness.get_current_state(); let block_root = *state .get_block_root(state.slot() - Slot::new(1)) - .ok() .expect("block root should exist at slot"); let contributions = harness.make_sync_contributions( &state, @@ -1711,7 +1705,6 @@ mod release_tests { let state = harness.get_current_state(); let block_root = *state .get_block_root(state.slot() - Slot::new(1)) - .ok() .expect("block root should exist at slot"); let contributions = harness.make_sync_contributions( &state, @@ -1791,7 +1784,6 @@ mod release_tests { let state = harness.get_current_state(); let block_root = *state .get_block_root(state.slot() - Slot::new(1)) - .ok() .expect("block root should exist at slot"); let contributions = harness.make_sync_contributions( &state, diff --git a/common/eth2_wallet_manager/src/wallet_manager.rs b/common/eth2_wallet_manager/src/wallet_manager.rs index 3dd419a48b..c988ca4135 100644 --- a/common/eth2_wallet_manager/src/wallet_manager.rs +++ b/common/eth2_wallet_manager/src/wallet_manager.rs @@ -296,10 +296,10 @@ mod tests { ) .expect("should create first wallet"); - let uuid = w.wallet().uuid().clone(); + let uuid = *w.wallet().uuid(); assert_eq!( - load_wallet_raw(&base_dir, &uuid).nextaccount(), + load_wallet_raw(base_dir, &uuid).nextaccount(), 0, "should start wallet with nextaccount 0" ); @@ -308,7 +308,7 @@ mod tests { w.next_validator(WALLET_PASSWORD, &[50; 32], &[51; 32]) .expect("should create validator"); assert_eq!( - load_wallet_raw(&base_dir, &uuid).nextaccount(), + load_wallet_raw(base_dir, &uuid).nextaccount(), i, "should update wallet with nextaccount {}", i @@ -333,54 +333,54 @@ mod tests { let base_dir = dir.path(); let mgr = WalletManager::open(base_dir).unwrap(); - let uuid_a = create_wallet(&mgr, 0).wallet().uuid().clone(); - let uuid_b = create_wallet(&mgr, 1).wallet().uuid().clone(); + let uuid_a = *create_wallet(&mgr, 0).wallet().uuid(); + let uuid_b = *create_wallet(&mgr, 1).wallet().uuid(); - let locked_a = LockedWallet::open(&base_dir, &uuid_a).expect("should open wallet a"); + let locked_a = LockedWallet::open(base_dir, &uuid_a).expect("should open wallet a"); assert!( - lockfile_path(&base_dir, &uuid_a).exists(), + lockfile_path(base_dir, &uuid_a).exists(), "lockfile should exist" ); drop(locked_a); assert!( - !lockfile_path(&base_dir, &uuid_a).exists(), + !lockfile_path(base_dir, &uuid_a).exists(), "lockfile have been cleaned up" ); - let locked_a = LockedWallet::open(&base_dir, &uuid_a).expect("should open wallet a"); - let locked_b = LockedWallet::open(&base_dir, &uuid_b).expect("should open wallet b"); + let locked_a = LockedWallet::open(base_dir, &uuid_a).expect("should open wallet a"); + let locked_b = LockedWallet::open(base_dir, &uuid_b).expect("should open wallet b"); assert!( - lockfile_path(&base_dir, &uuid_a).exists(), + lockfile_path(base_dir, &uuid_a).exists(), "lockfile a should exist" ); assert!( - lockfile_path(&base_dir, &uuid_b).exists(), + lockfile_path(base_dir, &uuid_b).exists(), "lockfile b should exist" ); - match LockedWallet::open(&base_dir, &uuid_a) { + match LockedWallet::open(base_dir, &uuid_a) { Err(Error::LockfileError(_)) => {} _ => panic!("did not get locked error"), }; drop(locked_a); - LockedWallet::open(&base_dir, &uuid_a) + LockedWallet::open(base_dir, &uuid_a) .expect("should open wallet a after previous instance is dropped"); - match LockedWallet::open(&base_dir, &uuid_b) { + match LockedWallet::open(base_dir, &uuid_b) { Err(Error::LockfileError(_)) => {} _ => panic!("did not get locked error"), }; drop(locked_b); - LockedWallet::open(&base_dir, &uuid_b) + LockedWallet::open(base_dir, &uuid_b) .expect("should open wallet a after previous instance is dropped"); } } diff --git a/consensus/fork_choice/tests/tests.rs b/consensus/fork_choice/tests/tests.rs index 29265e34e4..ef017159a0 100644 --- a/consensus/fork_choice/tests/tests.rs +++ b/consensus/fork_choice/tests/tests.rs @@ -1156,18 +1156,20 @@ async fn weak_subjectivity_check_epoch_boundary_is_skip_slot() { }; // recreate the chain exactly - ForkChoiceTest::new_with_chain_config(chain_config.clone()) - .apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0) - .await - .unwrap() - .skip_slots(E::slots_per_epoch() as usize) - .apply_blocks_while(|_, state| state.finalized_checkpoint().epoch < 5) - .await - .unwrap() - .apply_blocks(1) - .await - .assert_finalized_epoch(5) - .assert_shutdown_signal_not_sent(); + Box::pin( + ForkChoiceTest::new_with_chain_config(chain_config.clone()) + .apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0) + .await + .unwrap() + .skip_slots(E::slots_per_epoch() as usize) + .apply_blocks_while(|_, state| state.finalized_checkpoint().epoch < 5) + .await + .unwrap() + .apply_blocks(1), + ) + .await + .assert_finalized_epoch(5) + .assert_shutdown_signal_not_sent(); } #[tokio::test] diff --git a/crypto/eth2_keystore/tests/eip2335_vectors.rs b/crypto/eth2_keystore/tests/eip2335_vectors.rs index 3702a21816..e6852cc608 100644 --- a/crypto/eth2_keystore/tests/eip2335_vectors.rs +++ b/crypto/eth2_keystore/tests/eip2335_vectors.rs @@ -58,7 +58,7 @@ fn eip2335_test_vector_scrypt() { } "#; - let keystore = decode_and_check_sk(&vector); + let keystore = decode_and_check_sk(vector); assert_eq!( *keystore.uuid(), Uuid::parse_str("1d85ae20-35c5-4611-98e8-aa14a633906f").unwrap(), @@ -102,7 +102,7 @@ fn eip2335_test_vector_pbkdf() { } "#; - let keystore = decode_and_check_sk(&vector); + let keystore = decode_and_check_sk(vector); assert_eq!( *keystore.uuid(), Uuid::parse_str("64625def-3331-4eea-ab6f-782f3ed16a83").unwrap(), diff --git a/crypto/eth2_keystore/tests/tests.rs b/crypto/eth2_keystore/tests/tests.rs index 0df884b8a2..20bf9f1653 100644 --- a/crypto/eth2_keystore/tests/tests.rs +++ b/crypto/eth2_keystore/tests/tests.rs @@ -54,25 +54,17 @@ fn file() { let dir = tempdir().unwrap(); let path = dir.path().join("keystore.json"); - let get_file = || { - File::options() - .write(true) - .read(true) - .create(true) - .open(path.clone()) - .expect("should create file") - }; - let keystore = KeystoreBuilder::new(&keypair, GOOD_PASSWORD, "".into()) .unwrap() .build() .unwrap(); keystore - .to_json_writer(&mut get_file()) + .to_json_writer(File::create_new(&path).unwrap()) .expect("should write to file"); - let decoded = Keystore::from_json_reader(&mut get_file()).expect("should read from file"); + let decoded = + Keystore::from_json_reader(File::open(&path).unwrap()).expect("should read from file"); assert_eq!( decoded.decrypt_keypair(BAD_PASSWORD).err().unwrap(), diff --git a/crypto/eth2_wallet/tests/tests.rs b/crypto/eth2_wallet/tests/tests.rs index fe4565e0db..3dc073f764 100644 --- a/crypto/eth2_wallet/tests/tests.rs +++ b/crypto/eth2_wallet/tests/tests.rs @@ -132,20 +132,11 @@ fn file_round_trip() { let dir = tempdir().unwrap(); let path = dir.path().join("keystore.json"); - let get_file = || { - File::options() - .write(true) - .read(true) - .create(true) - .open(path.clone()) - .expect("should create file") - }; - wallet - .to_json_writer(&mut get_file()) + .to_json_writer(File::create_new(&path).unwrap()) .expect("should write to file"); - let decoded = Wallet::from_json_reader(&mut get_file()).unwrap(); + let decoded = Wallet::from_json_reader(File::open(&path).unwrap()).unwrap(); assert_eq!( decoded.decrypt_seed(&[1, 2, 3]).err().unwrap(), diff --git a/lighthouse/tests/account_manager.rs b/lighthouse/tests/account_manager.rs index c7153f48ef..d53d042fa4 100644 --- a/lighthouse/tests/account_manager.rs +++ b/lighthouse/tests/account_manager.rs @@ -115,7 +115,7 @@ fn create_wallet>( .arg(base_dir.as_ref().as_os_str()) .arg(CREATE_CMD) .arg(format!("--{}", NAME_FLAG)) - .arg(&name) + .arg(name) .arg(format!("--{}", PASSWORD_FLAG)) .arg(password.as_ref().as_os_str()) .arg(format!("--{}", MNEMONIC_FLAG)) @@ -273,16 +273,16 @@ impl TestValidator { .expect("stdout is not utf8") .to_string(); - if stdout == "" { + if stdout.is_empty() { return Ok(vec![]); } let pubkeys = stdout[..stdout.len() - 1] .split("\n") - .filter_map(|line| { + .map(|line| { let tab = line.find("\t").expect("line must have tab"); let (_, pubkey) = line.split_at(tab + 1); - Some(pubkey.to_string()) + pubkey.to_string() }) .collect::>(); @@ -446,7 +446,9 @@ fn validator_import_launchpad() { } } - stdin.write(format!("{}\n", PASSWORD).as_bytes()).unwrap(); + stdin + .write_all(format!("{}\n", PASSWORD).as_bytes()) + .unwrap(); child.wait().unwrap(); @@ -504,7 +506,7 @@ fn validator_import_launchpad() { }; assert!( - defs.as_slice() == &[expected_def.clone()], + defs.as_slice() == [expected_def.clone()], "validator defs file should be accurate" ); @@ -525,7 +527,7 @@ fn validator_import_launchpad() { expected_def.enabled = true; assert!( - defs.as_slice() == &[expected_def.clone()], + defs.as_slice() == [expected_def.clone()], "validator defs file should be accurate" ); } @@ -582,7 +584,7 @@ fn validator_import_launchpad_no_password_then_add_password() { let mut child = validator_import_key_cmd(); wait_for_password_prompt(&mut child); let stdin = child.stdin.as_mut().unwrap(); - stdin.write("\n".as_bytes()).unwrap(); + stdin.write_all("\n".as_bytes()).unwrap(); child.wait().unwrap(); assert!( @@ -628,14 +630,16 @@ fn validator_import_launchpad_no_password_then_add_password() { }; assert!( - defs.as_slice() == &[expected_def.clone()], + defs.as_slice() == [expected_def.clone()], "validator defs file should be accurate" ); let mut child = validator_import_key_cmd(); wait_for_password_prompt(&mut child); let stdin = child.stdin.as_mut().unwrap(); - stdin.write(format!("{}\n", PASSWORD).as_bytes()).unwrap(); + stdin + .write_all(format!("{}\n", PASSWORD).as_bytes()) + .unwrap(); child.wait().unwrap(); let expected_def = ValidatorDefinition { @@ -657,7 +661,7 @@ fn validator_import_launchpad_no_password_then_add_password() { let defs = ValidatorDefinitions::open(&dst_dir).unwrap(); assert!( - defs.as_slice() == &[expected_def.clone()], + defs.as_slice() == [expected_def.clone()], "validator defs file should be accurate" ); } @@ -759,7 +763,7 @@ fn validator_import_launchpad_password_file() { }; assert!( - defs.as_slice() == &[expected_def], + defs.as_slice() == [expected_def], "validator defs file should be accurate" ); } diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 80986653c1..88e05dfa12 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -9,7 +9,6 @@ use beacon_node::beacon_chain::graffiti_calculator::GraffitiOrigin; use beacon_processor::BeaconProcessorConfig; use eth1::Eth1Endpoint; use lighthouse_network::PeerId; -use lighthouse_version; use std::fs::File; use std::io::{Read, Write}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; @@ -128,7 +127,7 @@ fn allow_insecure_genesis_sync_default() { CommandLineTest::new() .run_with_zero_port_and_no_genesis_sync() .with_config(|config| { - assert_eq!(config.allow_insecure_genesis_sync, false); + assert!(!config.allow_insecure_genesis_sync); }); } @@ -146,7 +145,7 @@ fn allow_insecure_genesis_sync_enabled() { .flag("allow-insecure-genesis-sync", None) .run_with_zero_port_and_no_genesis_sync() .with_config(|config| { - assert_eq!(config.allow_insecure_genesis_sync, true); + assert!(config.allow_insecure_genesis_sync); }); } @@ -359,11 +358,11 @@ fn default_graffiti() { #[test] fn trusted_peers_flag() { - let peers = vec![PeerId::random(), PeerId::random()]; + let peers = [PeerId::random(), PeerId::random()]; CommandLineTest::new() .flag( "trusted-peers", - Some(format!("{},{}", peers[0].to_string(), peers[1].to_string()).as_str()), + Some(format!("{},{}", peers[0], peers[1]).as_str()), ) .run_with_zero_port() .with_config(|config| { @@ -383,7 +382,7 @@ fn genesis_backfill_flag() { CommandLineTest::new() .flag("genesis-backfill", None) .run_with_zero_port() - .with_config(|config| assert_eq!(config.chain.genesis_backfill, true)); + .with_config(|config| assert!(config.chain.genesis_backfill)); } /// The genesis backfill flag should be enabled if historic states flag is set. @@ -392,7 +391,7 @@ fn genesis_backfill_with_historic_flag() { CommandLineTest::new() .flag("reconstruct-historic-states", None) .run_with_zero_port() - .with_config(|config| assert_eq!(config.chain.genesis_backfill, true)); + .with_config(|config| assert!(config.chain.genesis_backfill)); } // Tests for Eth1 flags. @@ -448,7 +447,7 @@ fn eth1_cache_follow_distance_manual() { // Tests for Bellatrix flags. fn run_bellatrix_execution_endpoints_flag_test(flag: &str) { use sensitive_url::SensitiveUrl; - let urls = vec!["http://sigp.io/no-way:1337", "http://infura.not_real:4242"]; + let urls = ["http://sigp.io/no-way:1337", "http://infura.not_real:4242"]; // we don't support redundancy for execution-endpoints // only the first provided endpoint is parsed. @@ -480,10 +479,10 @@ fn run_bellatrix_execution_endpoints_flag_test(flag: &str) { .run_with_zero_port() .with_config(|config| { let config = config.execution_layer.as_ref().unwrap(); - assert_eq!(config.execution_endpoint.is_some(), true); + assert!(config.execution_endpoint.is_some()); assert_eq!( config.execution_endpoint.as_ref().unwrap().clone(), - SensitiveUrl::parse(&urls[0]).unwrap() + SensitiveUrl::parse(urls[0]).unwrap() ); // Only the first secret file should be used. assert_eq!( @@ -595,7 +594,7 @@ fn run_payload_builder_flag_test(flag: &str, builders: &str) { let config = config.execution_layer.as_ref().unwrap(); // Only first provided endpoint is parsed as we don't support // redundancy. - assert_eq!(config.builder_url, all_builders.get(0).cloned()); + assert_eq!(config.builder_url, all_builders.first().cloned()); }) } fn run_payload_builder_flag_test_with_config( @@ -661,7 +660,7 @@ fn builder_fallback_flags() { Some("builder-fallback-disable-checks"), None, |config| { - assert_eq!(config.chain.builder_fallback_disable_checks, true); + assert!(config.chain.builder_fallback_disable_checks); }, ); } @@ -1657,19 +1656,19 @@ fn http_enable_beacon_processor() { CommandLineTest::new() .flag("http", None) .run_with_zero_port() - .with_config(|config| assert_eq!(config.http_api.enable_beacon_processor, true)); + .with_config(|config| assert!(config.http_api.enable_beacon_processor)); CommandLineTest::new() .flag("http", None) .flag("http-enable-beacon-processor", Some("true")) .run_with_zero_port() - .with_config(|config| assert_eq!(config.http_api.enable_beacon_processor, true)); + .with_config(|config| assert!(config.http_api.enable_beacon_processor)); CommandLineTest::new() .flag("http", None) .flag("http-enable-beacon-processor", Some("false")) .run_with_zero_port() - .with_config(|config| assert_eq!(config.http_api.enable_beacon_processor, false)); + .with_config(|config| assert!(!config.http_api.enable_beacon_processor)); } #[test] fn http_tls_flags() { @@ -2221,7 +2220,7 @@ fn slasher_broadcast_flag_false() { }); } -#[cfg(all(feature = "slasher-lmdb"))] +#[cfg(feature = "slasher-lmdb")] #[test] fn slasher_backend_override_to_default() { // Hard to test this flag because all but one backend is disabled by default and the backend @@ -2429,7 +2428,7 @@ fn logfile_no_restricted_perms_flag() { .flag("logfile-no-restricted-perms", None) .run_with_zero_port() .with_config(|config| { - assert!(config.logger_config.is_restricted == false); + assert!(!config.logger_config.is_restricted); }); } #[test] @@ -2454,7 +2453,7 @@ fn logfile_format_flag() { fn sync_eth1_chain_default() { CommandLineTest::new() .run_with_zero_port() - .with_config(|config| assert_eq!(config.sync_eth1_chain, true)); + .with_config(|config| assert!(config.sync_eth1_chain)); } #[test] @@ -2467,7 +2466,7 @@ fn sync_eth1_chain_execution_endpoints_flag() { dir.path().join("jwt-file").as_os_str().to_str(), ) .run_with_zero_port() - .with_config(|config| assert_eq!(config.sync_eth1_chain, true)); + .with_config(|config| assert!(config.sync_eth1_chain)); } #[test] @@ -2481,7 +2480,7 @@ fn sync_eth1_chain_disable_deposit_contract_sync_flag() { dir.path().join("jwt-file").as_os_str().to_str(), ) .run_with_zero_port() - .with_config(|config| assert_eq!(config.sync_eth1_chain, false)); + .with_config(|config| assert!(!config.sync_eth1_chain)); } #[test] @@ -2504,9 +2503,9 @@ fn light_client_server_default() { CommandLineTest::new() .run_with_zero_port() .with_config(|config| { - assert_eq!(config.network.enable_light_client_server, false); - assert_eq!(config.chain.enable_light_client_server, false); - assert_eq!(config.http_api.enable_light_client_server, false); + assert!(!config.network.enable_light_client_server); + assert!(!config.chain.enable_light_client_server); + assert!(!config.http_api.enable_light_client_server); }); } @@ -2516,8 +2515,8 @@ fn light_client_server_enabled() { .flag("light-client-server", None) .run_with_zero_port() .with_config(|config| { - assert_eq!(config.network.enable_light_client_server, true); - assert_eq!(config.chain.enable_light_client_server, true); + assert!(config.network.enable_light_client_server); + assert!(config.chain.enable_light_client_server); }); } @@ -2528,7 +2527,7 @@ fn light_client_http_server_enabled() { .flag("light-client-server", None) .run_with_zero_port() .with_config(|config| { - assert_eq!(config.http_api.enable_light_client_server, true); + assert!(config.http_api.enable_light_client_server); }); } diff --git a/lighthouse/tests/boot_node.rs b/lighthouse/tests/boot_node.rs index 659dea468d..b243cd6001 100644 --- a/lighthouse/tests/boot_node.rs +++ b/lighthouse/tests/boot_node.rs @@ -149,7 +149,7 @@ fn disable_packet_filter_flag() { .flag("disable-packet-filter", None) .run_with_ip() .with_config(|config| { - assert_eq!(config.disable_packet_filter, true); + assert!(config.disable_packet_filter); }); } @@ -159,7 +159,7 @@ fn enable_enr_auto_update_flag() { .flag("enable-enr-auto-update", None) .run_with_ip() .with_config(|config| { - assert_eq!(config.enable_enr_auto_update, true); + assert!(config.enable_enr_auto_update); }); } diff --git a/lighthouse/tests/validator_client.rs b/lighthouse/tests/validator_client.rs index 587001f77b..c5b303e4d1 100644 --- a/lighthouse/tests/validator_client.rs +++ b/lighthouse/tests/validator_client.rs @@ -136,7 +136,7 @@ fn beacon_nodes_tls_certs_flag() { .flag( "beacon-nodes-tls-certs", Some( - vec![ + [ dir.path().join("certificate.crt").to_str().unwrap(), dir.path().join("certificate2.crt").to_str().unwrap(), ] @@ -205,7 +205,7 @@ fn graffiti_file_with_pk_flag() { let mut file = File::create(dir.path().join("graffiti.txt")).expect("Unable to create file"); let new_key = Keypair::random(); let pubkeybytes = PublicKeyBytes::from(new_key.pk); - let contents = format!("{}:nice-graffiti", pubkeybytes.to_string()); + let contents = format!("{}:nice-graffiti", pubkeybytes); file.write_all(contents.as_bytes()) .expect("Unable to write to file"); CommandLineTest::new() @@ -419,13 +419,13 @@ pub fn malloc_tuning_flag() { CommandLineTest::new() .flag("disable-malloc-tuning", None) .run() - .with_config(|config| assert_eq!(config.http_metrics.allocator_metrics_enabled, false)); + .with_config(|config| assert!(!config.http_metrics.allocator_metrics_enabled)); } #[test] pub fn malloc_tuning_default() { CommandLineTest::new() .run() - .with_config(|config| assert_eq!(config.http_metrics.allocator_metrics_enabled, true)); + .with_config(|config| assert!(config.http_metrics.allocator_metrics_enabled)); } #[test] fn doppelganger_protection_flag() { diff --git a/lighthouse/tests/validator_manager.rs b/lighthouse/tests/validator_manager.rs index 999f3c3141..04e3eafe6e 100644 --- a/lighthouse/tests/validator_manager.rs +++ b/lighthouse/tests/validator_manager.rs @@ -136,7 +136,7 @@ pub fn validator_create_defaults() { count: 1, deposit_gwei: MainnetEthSpec::default_spec().max_effective_balance, mnemonic_path: None, - stdin_inputs: cfg!(windows) || false, + stdin_inputs: cfg!(windows), disable_deposits: false, specify_voting_keystore_password: false, eth1_withdrawal_address: None, @@ -201,7 +201,7 @@ pub fn validator_create_disable_deposits() { .flag("--disable-deposits", None) .flag("--builder-proposals", Some("false")) .assert_success(|config| { - assert_eq!(config.disable_deposits, true); + assert!(config.disable_deposits); assert_eq!(config.builder_proposals, Some(false)); }); } @@ -300,7 +300,7 @@ pub fn validator_move_defaults() { fee_recipient: None, gas_limit: None, password_source: PasswordSource::Interactive { - stdin_inputs: cfg!(windows) || false, + stdin_inputs: cfg!(windows), }, }; assert_eq!(expected, config); @@ -350,7 +350,7 @@ pub fn validator_move_misc_flags_1() { .flag("--src-vc-token", Some("./1.json")) .flag("--dest-vc-url", Some("http://localhost:2")) .flag("--dest-vc-token", Some("./2.json")) - .flag("--validators", Some(&format!("{}", EXAMPLE_PUBKEY_0))) + .flag("--validators", Some(EXAMPLE_PUBKEY_0)) .flag("--builder-proposals", Some("false")) .flag("--prefer-builder-proposals", Some("false")) .assert_success(|config| { @@ -368,7 +368,7 @@ pub fn validator_move_misc_flags_1() { fee_recipient: None, gas_limit: None, password_source: PasswordSource::Interactive { - stdin_inputs: cfg!(windows) || false, + stdin_inputs: cfg!(windows), }, }; assert_eq!(expected, config); @@ -382,7 +382,7 @@ pub fn validator_move_misc_flags_2() { .flag("--src-vc-token", Some("./1.json")) .flag("--dest-vc-url", Some("http://localhost:2")) .flag("--dest-vc-token", Some("./2.json")) - .flag("--validators", Some(&format!("{}", EXAMPLE_PUBKEY_0))) + .flag("--validators", Some(EXAMPLE_PUBKEY_0)) .flag("--builder-proposals", Some("false")) .flag("--builder-boost-factor", Some("100")) .assert_success(|config| { @@ -400,7 +400,7 @@ pub fn validator_move_misc_flags_2() { fee_recipient: None, gas_limit: None, password_source: PasswordSource::Interactive { - stdin_inputs: cfg!(windows) || false, + stdin_inputs: cfg!(windows), }, }; assert_eq!(expected, config); @@ -428,7 +428,7 @@ pub fn validator_move_count() { fee_recipient: None, gas_limit: None, password_source: PasswordSource::Interactive { - stdin_inputs: cfg!(windows) || false, + stdin_inputs: cfg!(windows), }, }; assert_eq!(expected, config); diff --git a/testing/web3signer_tests/src/lib.rs b/testing/web3signer_tests/src/lib.rs index bebc8fa13b..e0dee9ceb4 100644 --- a/testing/web3signer_tests/src/lib.rs +++ b/testing/web3signer_tests/src/lib.rs @@ -173,6 +173,8 @@ mod tests { } impl Web3SignerRig { + // We need to hold that lock as we want to get the binary only once + #[allow(clippy::await_holding_lock)] pub async fn new(network: &str, listen_address: &str, listen_port: u16) -> Self { GET_WEB3SIGNER_BIN .get_or_init(|| async { @@ -210,7 +212,7 @@ mod tests { keystore_password_file: keystore_password_filename.to_string(), }; let key_config_file = - File::create(&keystore_dir.path().join("key-config.yaml")).unwrap(); + File::create(keystore_dir.path().join("key-config.yaml")).unwrap(); serde_yaml::to_writer(key_config_file, &key_config).unwrap(); let tls_keystore_file = tls_dir().join("web3signer").join("key.p12"); diff --git a/validator_client/http_api/src/tests.rs b/validator_client/http_api/src/tests.rs index 027b10e246..7ea3d7ebaa 100644 --- a/validator_client/http_api/src/tests.rs +++ b/validator_client/http_api/src/tests.rs @@ -53,8 +53,10 @@ struct ApiTester { impl ApiTester { pub async fn new() -> Self { - let mut config = ValidatorStoreConfig::default(); - config.fee_recipient = Some(TEST_DEFAULT_FEE_RECIPIENT); + let config = ValidatorStoreConfig { + fee_recipient: Some(TEST_DEFAULT_FEE_RECIPIENT), + ..Default::default() + }; Self::new_with_config(config).await } @@ -139,7 +141,7 @@ impl ApiTester { let (listening_socket, server) = super::serve(ctx, test_runtime.task_executor.exit()).unwrap(); - tokio::spawn(async { server.await }); + tokio::spawn(server); let url = SensitiveUrl::parse(&format!( "http://{}:{}", @@ -345,22 +347,21 @@ impl ApiTester { .set_nextaccount(s.key_derivation_path_offset) .unwrap(); - for i in 0..s.count { + for validator in response.iter().take(s.count) { let keypairs = wallet .next_validator(PASSWORD_BYTES, PASSWORD_BYTES, PASSWORD_BYTES) .unwrap(); let voting_keypair = keypairs.voting.decrypt_keypair(PASSWORD_BYTES).unwrap(); assert_eq!( - response[i].voting_pubkey, + validator.voting_pubkey, voting_keypair.pk.clone().into(), "the locally generated voting pk should match the server response" ); let withdrawal_keypair = keypairs.withdrawal.decrypt_keypair(PASSWORD_BYTES).unwrap(); - let deposit_bytes = - serde_utils::hex::decode(&response[i].eth1_deposit_tx_data).unwrap(); + let deposit_bytes = serde_utils::hex::decode(&validator.eth1_deposit_tx_data).unwrap(); let (deposit_data, _) = decode_eth1_tx_data(&deposit_bytes, E::default_spec().max_effective_balance) diff --git a/validator_client/http_api/src/tests/keystores.rs b/validator_client/http_api/src/tests/keystores.rs index 2dde087a7f..6559a2bb9e 100644 --- a/validator_client/http_api/src/tests/keystores.rs +++ b/validator_client/http_api/src/tests/keystores.rs @@ -130,7 +130,7 @@ fn check_keystore_get_response<'a>( 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)); + assert!(ks1.readonly.is_none() || ks1.readonly == Some(false)); } } @@ -147,7 +147,7 @@ fn check_keystore_import_response( } } -fn check_keystore_delete_response<'a>( +fn check_keystore_delete_response( response: &DeleteKeystoresResponse, expected_statuses: impl IntoIterator, ) { @@ -634,7 +634,7 @@ async fn check_get_set_fee_recipient() { assert_eq!( get_res, GetFeeRecipientResponse { - pubkey: pubkey.clone(), + pubkey: *pubkey, ethaddress: TEST_DEFAULT_FEE_RECIPIENT, } ); @@ -654,7 +654,7 @@ async fn check_get_set_fee_recipient() { .post_fee_recipient( &all_pubkeys[1], &UpdateFeeRecipientRequest { - ethaddress: fee_recipient_public_key_1.clone(), + ethaddress: fee_recipient_public_key_1, }, ) .await @@ -667,14 +667,14 @@ async fn check_get_set_fee_recipient() { .await .expect("should get fee recipient"); let expected = if i == 1 { - fee_recipient_public_key_1.clone() + fee_recipient_public_key_1 } else { TEST_DEFAULT_FEE_RECIPIENT }; assert_eq!( get_res, GetFeeRecipientResponse { - pubkey: pubkey.clone(), + pubkey: *pubkey, ethaddress: expected, } ); @@ -686,7 +686,7 @@ async fn check_get_set_fee_recipient() { .post_fee_recipient( &all_pubkeys[2], &UpdateFeeRecipientRequest { - ethaddress: fee_recipient_public_key_2.clone(), + ethaddress: fee_recipient_public_key_2, }, ) .await @@ -699,16 +699,16 @@ async fn check_get_set_fee_recipient() { .await .expect("should get fee recipient"); let expected = if i == 1 { - fee_recipient_public_key_1.clone() + fee_recipient_public_key_1 } else if i == 2 { - fee_recipient_public_key_2.clone() + fee_recipient_public_key_2 } else { TEST_DEFAULT_FEE_RECIPIENT }; assert_eq!( get_res, GetFeeRecipientResponse { - pubkey: pubkey.clone(), + pubkey: *pubkey, ethaddress: expected, } ); @@ -720,7 +720,7 @@ async fn check_get_set_fee_recipient() { .post_fee_recipient( &all_pubkeys[1], &UpdateFeeRecipientRequest { - ethaddress: fee_recipient_override.clone(), + ethaddress: fee_recipient_override, }, ) .await @@ -732,16 +732,16 @@ async fn check_get_set_fee_recipient() { .await .expect("should get fee recipient"); let expected = if i == 1 { - fee_recipient_override.clone() + fee_recipient_override } else if i == 2 { - fee_recipient_public_key_2.clone() + fee_recipient_public_key_2 } else { TEST_DEFAULT_FEE_RECIPIENT }; assert_eq!( get_res, GetFeeRecipientResponse { - pubkey: pubkey.clone(), + pubkey: *pubkey, ethaddress: expected, } ); @@ -761,14 +761,14 @@ async fn check_get_set_fee_recipient() { .await .expect("should get fee recipient"); let expected = if i == 2 { - fee_recipient_public_key_2.clone() + fee_recipient_public_key_2 } else { TEST_DEFAULT_FEE_RECIPIENT }; assert_eq!( get_res, GetFeeRecipientResponse { - pubkey: pubkey.clone(), + pubkey: *pubkey, ethaddress: expected, } ); @@ -814,7 +814,7 @@ async fn check_get_set_gas_limit() { assert_eq!( get_res, GetGasLimitResponse { - pubkey: pubkey.clone(), + pubkey: *pubkey, gas_limit: DEFAULT_GAS_LIMIT, } ); @@ -843,14 +843,14 @@ async fn check_get_set_gas_limit() { .await .expect("should get gas limit"); let expected = if i == 1 { - gas_limit_public_key_1.clone() + gas_limit_public_key_1 } else { DEFAULT_GAS_LIMIT }; assert_eq!( get_res, GetGasLimitResponse { - pubkey: pubkey.clone(), + pubkey: *pubkey, gas_limit: expected, } ); @@ -884,7 +884,7 @@ async fn check_get_set_gas_limit() { assert_eq!( get_res, GetGasLimitResponse { - pubkey: pubkey.clone(), + pubkey: *pubkey, gas_limit: expected, } ); @@ -917,7 +917,7 @@ async fn check_get_set_gas_limit() { assert_eq!( get_res, GetGasLimitResponse { - pubkey: pubkey.clone(), + pubkey: *pubkey, gas_limit: expected, } ); @@ -944,7 +944,7 @@ async fn check_get_set_gas_limit() { assert_eq!( get_res, GetGasLimitResponse { - pubkey: pubkey.clone(), + pubkey: *pubkey, gas_limit: expected, } ); @@ -1305,7 +1305,7 @@ async fn delete_concurrent_with_signing() { let handle = handle.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() { + for public_key in thread_pubkeys.iter() { let _ = validator_store .sign_attestation(*public_key, 0, &mut att, Epoch::new(j + 1)) .await; @@ -2084,7 +2084,7 @@ async fn import_remotekey_web3signer_disabled() { web3signer_req.enable = false; // Import web3signers. - let _ = tester + tester .client .post_lighthouse_validators_web3signer(&vec![web3signer_req]) .await @@ -2148,8 +2148,11 @@ async fn import_remotekey_web3signer_enabled() { // 1 validator imported. assert_eq!(tester.vals_total(), 1); assert_eq!(tester.vals_enabled(), 1); - let vals = tester.initialized_validators.read(); - let web3_vals = vals.validator_definitions(); + let web3_vals = tester + .initialized_validators + .read() + .validator_definitions() + .to_vec(); // Import remotekeys. let import_res = tester @@ -2166,11 +2169,13 @@ async fn import_remotekey_web3signer_enabled() { assert_eq!(tester.vals_total(), 1); assert_eq!(tester.vals_enabled(), 1); - let vals = tester.initialized_validators.read(); - let remote_vals = vals.validator_definitions(); + { + let vals = tester.initialized_validators.read(); + let remote_vals = vals.validator_definitions(); - // Web3signer should not be overwritten since it is enabled. - assert!(web3_vals == remote_vals); + // Web3signer should not be overwritten since it is enabled. + assert!(web3_vals == remote_vals); + } // Remotekey should not be imported. let expected_responses = vec![SingleListRemotekeysResponse { diff --git a/validator_manager/src/import_validators.rs b/validator_manager/src/import_validators.rs index 2e8821f0db..3cebc10bb3 100644 --- a/validator_manager/src/import_validators.rs +++ b/validator_manager/src/import_validators.rs @@ -520,7 +520,7 @@ pub mod tests { let local_validators: Vec = { let contents = - fs::read_to_string(&self.import_config.validators_file_path.unwrap()) + fs::read_to_string(self.import_config.validators_file_path.unwrap()) .unwrap(); serde_json::from_str(&contents).unwrap() }; @@ -557,7 +557,7 @@ pub mod tests { self.vc.ensure_key_cache_consistency().await; let local_keystore: Keystore = - Keystore::from_json_file(&self.import_config.keystore_file_path.unwrap()) + Keystore::from_json_file(self.import_config.keystore_file_path.unwrap()) .unwrap(); let list_keystores_response = self.vc.client.get_keystores().await.unwrap().data; diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index c039728e6f..4d0820f5a8 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -978,13 +978,13 @@ mod test { }) .unwrap(); // Set all definitions to use the same password path as the primary. - definitions.iter_mut().enumerate().for_each(|(_, def)| { - match &mut def.signing_definition { - SigningDefinition::LocalKeystore { - voting_keystore_password_path: Some(path), - .. - } => *path = primary_path.clone(), - _ => (), + definitions.iter_mut().for_each(|def| { + if let SigningDefinition::LocalKeystore { + voting_keystore_password_path: Some(path), + .. + } = &mut def.signing_definition + { + *path = primary_path.clone() } }) } From d74b2d96f58b97d807eff30e4fb1c0b964e7e6dd Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Tue, 17 Dec 2024 07:44:24 +0530 Subject: [PATCH 14/23] Electra alpha8 spec updates (#6496) * Fix partial withdrawals count * Remove get_active_balance * Remove queue_entire_balance_and_reset_validator * Switch to compounding when consolidating with source==target * Queue deposit requests and apply them during epoch processing * Fix ef tests * Clear todos * Fix engine api formatting issues * Merge branch 'unstable' into electra-alpha7 * Make add_validator_to_registry more in line with the spec * Address some review comments * Cleanup * Update initialize_beacon_state_from_eth1 * Merge branch 'unstable' into electra-alpha7 * Fix rpc decoding for blobs by range/root * Fix block hash computation * Fix process_deposits bug * Merge branch 'unstable' into electra-alpha7 * Fix topup deposit processing bug * Update builder api for electra * Refactor mock builder to separate functionality * Merge branch 'unstable' into electra-alpha7 * Address review comments * Use copied for reference rather than cloned * Optimise and simplify PendingDepositsContext::new * Merge remote-tracking branch 'origin/unstable' into electra-alpha7 * Fix processing of deposits with invalid signatures * Remove redundant code in genesis init * Revert "Refactor mock builder to separate functionality" This reverts commit 6d10456912b3c39b8a8c9089db76e8ead20608a0. * Revert "Update builder api for electra" This reverts commit c5c9aca6db201c09c995756a11e9cb6a03b2ea99. * Simplify pre-activation sorting * Fix stale validators used in upgrade_to_electra * Merge branch 'unstable' into electra-alpha7 --- beacon_node/execution_layer/src/block_hash.rs | 44 +-- .../execution_layer/src/engine_api/http.rs | 2 +- .../src/engine_api/json_structures.rs | 30 +- .../src/engine_api/new_payload_request.rs | 11 +- .../src/test_utils/handle_rpc.rs | 2 +- beacon_node/store/src/partial_beacon_state.rs | 4 +- .../src/per_block_processing.rs | 4 +- .../process_operations.rs | 151 +++++++--- .../src/per_epoch_processing/errors.rs | 1 + .../src/per_epoch_processing/single_pass.rs | 262 +++++++++++++----- .../state_processing/src/upgrade/electra.rs | 47 +++- consensus/types/src/beacon_state.rs | 123 +++----- consensus/types/src/beacon_state/tests.rs | 37 --- consensus/types/src/deposit_request.rs | 8 +- consensus/types/src/eth_spec.rs | 23 +- consensus/types/src/execution_block_header.rs | 9 + consensus/types/src/execution_requests.rs | 40 ++- consensus/types/src/lib.rs | 6 +- ..._balance_deposit.rs => pending_deposit.rs} | 12 +- consensus/types/src/preset.rs | 2 +- consensus/types/src/validator.rs | 21 +- testing/ef_tests/Makefile | 2 +- .../ef_tests/src/cases/epoch_processing.rs | 6 +- testing/ef_tests/src/type_name.rs | 2 +- testing/ef_tests/tests/tests.rs | 7 +- 25 files changed, 519 insertions(+), 337 deletions(-) rename consensus/types/src/{pending_balance_deposit.rs => pending_deposit.rs} (68%) diff --git a/beacon_node/execution_layer/src/block_hash.rs b/beacon_node/execution_layer/src/block_hash.rs index cdc172cff4..d3a32c7929 100644 --- a/beacon_node/execution_layer/src/block_hash.rs +++ b/beacon_node/execution_layer/src/block_hash.rs @@ -7,7 +7,7 @@ use keccak_hash::KECCAK_EMPTY_LIST_RLP; use triehash::ordered_trie_root; use types::{ EncodableExecutionBlockHeader, EthSpec, ExecutionBlockHash, ExecutionBlockHeader, - ExecutionPayloadRef, Hash256, + ExecutionPayloadRef, ExecutionRequests, Hash256, }; /// Calculate the block hash of an execution block. @@ -17,6 +17,7 @@ use types::{ pub fn calculate_execution_block_hash( payload: ExecutionPayloadRef, parent_beacon_block_root: Option, + execution_requests: Option<&ExecutionRequests>, ) -> (ExecutionBlockHash, Hash256) { // Calculate the transactions root. // We're currently using a deprecated Parity library for this. We should move to a @@ -38,6 +39,7 @@ pub fn calculate_execution_block_hash( let rlp_blob_gas_used = payload.blob_gas_used().ok(); let rlp_excess_blob_gas = payload.excess_blob_gas().ok(); + let requests_root = execution_requests.map(|requests| requests.requests_hash()); // Construct the block header. let exec_block_header = ExecutionBlockHeader::from_payload( @@ -48,6 +50,7 @@ pub fn calculate_execution_block_hash( rlp_blob_gas_used, rlp_excess_blob_gas, parent_beacon_block_root, + requests_root, ); // Hash the RLP encoding of the block header. @@ -118,6 +121,7 @@ mod test { blob_gas_used: None, excess_blob_gas: None, parent_beacon_block_root: None, + requests_root: None, }; let expected_rlp = "f90200a0e0a94a7a3c9617401586b1a27025d2d9671332d22d540e0af72b069170380f2aa01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794ba5e000000000000000000000000000000000000a0ec3c94b18b8a1cff7d60f8d258ec723312932928626b4c9355eb4ab3568ec7f7a050f738580ed699f0469702c7ccc63ed2e51bc034be9479b7bff4e68dee84accfa029b0562f7140574dd0d50dee8a271b22e1a0a7b78fca58f7c60370d8317ba2a9b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000830200000188016345785d8a00008301553482079e42a0000000000000000000000000000000000000000000000000000000000000000088000000000000000082036b"; let expected_hash = @@ -149,6 +153,7 @@ mod test { blob_gas_used: None, excess_blob_gas: None, parent_beacon_block_root: None, + requests_root: None, }; let expected_rlp = "f901fda0927ca537f06c783a3a2635b8805eef1c8c2124f7444ad4a3389898dd832f2dbea01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794ba5e000000000000000000000000000000000000a0e97859b065bd8dbbb4519c7cb935024de2484c2b7f881181b4360492f0b06b82a050f738580ed699f0469702c7ccc63ed2e51bc034be9479b7bff4e68dee84accfa029b0562f7140574dd0d50dee8a271b22e1a0a7b78fca58f7c60370d8317ba2a9b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800188016345785d8a00008301553482079e42a0000000000000000000000000000000000000000000000000000000000002000088000000000000000082036b"; let expected_hash = @@ -181,6 +186,7 @@ mod test { blob_gas_used: None, excess_blob_gas: None, parent_beacon_block_root: None, + requests_root: None, }; let expected_hash = Hash256::from_str("6da69709cd5a34079b6604d29cd78fc01dacd7c6268980057ad92a2bede87351") @@ -211,6 +217,7 @@ mod test { blob_gas_used: Some(0x0u64), excess_blob_gas: Some(0x0u64), parent_beacon_block_root: Some(Hash256::from_str("f7d327d2c04e4f12e9cdd492e53d39a1d390f8b1571e3b2a22ac6e1e170e5b1a").unwrap()), + requests_root: None, }; let expected_hash = Hash256::from_str("a7448e600ead0a23d16f96aa46e8dea9eef8a7c5669a5f0a5ff32709afe9c408") @@ -221,29 +228,30 @@ mod test { #[test] fn test_rlp_encode_block_electra() { let header = ExecutionBlockHeader { - parent_hash: Hash256::from_str("172864416698b842f4c92f7b476be294b4ef720202779df194cd225f531053ab").unwrap(), + parent_hash: Hash256::from_str("a628f146df398a339768bd101f7dc41d828be79aca5dd02cc878a51bdbadd761").unwrap(), ommers_hash: Hash256::from_str("1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347").unwrap(), - beneficiary: Address::from_str("878705ba3f8bc32fcf7f4caa1a35e72af65cf766").unwrap(), - state_root: Hash256::from_str("c6457d0df85c84c62d1c68f68138b6e796e8a44fb44de221386fb2d5611c41e0").unwrap(), - transactions_root: Hash256::from_str("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").unwrap(), - receipts_root: Hash256::from_str("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").unwrap(), - logs_bloom:<[u8; 256]>::from_hex("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").unwrap().into(), + beneficiary: Address::from_str("f97e180c050e5ab072211ad2c213eb5aee4df134").unwrap(), + state_root: Hash256::from_str("fdff009f8280bd113ebb4df8ce4e2dcc9322d43184a0b506e70b7f4823ca1253").unwrap(), + transactions_root: Hash256::from_str("452806578b4fa881cafb019c47e767e37e2249accf859159f00cddefb2579bb5").unwrap(), + receipts_root: Hash256::from_str("72ceac0f16a32041c881b3220d39ca506a286bef163c01a4d0821cd4027d31c7").unwrap(), + logs_bloom:<[u8; 256]>::from_hex("10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000").unwrap().into(), difficulty: Uint256::ZERO, - number: Uint256::from(97), - gas_limit: Uint256::from(27482534), - gas_used: Uint256::ZERO, - timestamp: 1692132829u64, - extra_data: hex::decode("d883010d00846765746888676f312e32302e37856c696e7578").unwrap(), - mix_hash: Hash256::from_str("0b493c22d2ad4ca76c77ae6ad916af429b42b1dc98fdcb8e5ddbd049bbc5d623").unwrap(), + number: Uint256::from(8230), + gas_limit: Uint256::from(30000000), + gas_used: Uint256::from(3716848), + timestamp: 1730921268, + extra_data: hex::decode("d883010e0c846765746888676f312e32332e32856c696e7578").unwrap(), + mix_hash: Hash256::from_str("e87ca9a45b2e61bbe9080d897db1d584b5d2367d22e898af901091883b7b96ec").unwrap(), nonce: Hash64::ZERO, - base_fee_per_gas: Uint256::from(2374u64), + base_fee_per_gas: Uint256::from(7u64), withdrawals_root: Some(Hash256::from_str("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").unwrap()), - blob_gas_used: Some(0x0u64), - excess_blob_gas: Some(0x0u64), - parent_beacon_block_root: Some(Hash256::from_str("f7d327d2c04e4f12e9cdd492e53d39a1d390f8b1571e3b2a22ac6e1e170e5b1a").unwrap()), + blob_gas_used: Some(786432), + excess_blob_gas: Some(44695552), + parent_beacon_block_root: Some(Hash256::from_str("f3a888fee010ebb1ae083547004e96c254b240437823326fdff8354b1fc25629").unwrap()), + requests_root: Some(Hash256::from_str("9440d3365f07573919e1e9ac5178c20ec6fe267357ee4baf8b6409901f331b62").unwrap()), }; let expected_hash = - Hash256::from_str("a7448e600ead0a23d16f96aa46e8dea9eef8a7c5669a5f0a5ff32709afe9c408") + Hash256::from_str("61e67afc96bf21be6aab52c1ace1db48de7b83f03119b0644deb4b69e87e09e1") .unwrap(); test_rlp_encoding(&header, None, expected_hash); } diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index d4734be448..33dc60d037 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -812,7 +812,7 @@ impl HttpJsonRpc { new_payload_request_electra.versioned_hashes, new_payload_request_electra.parent_beacon_block_root, new_payload_request_electra - .execution_requests_list + .execution_requests .get_execution_requests_list(), ]); diff --git a/beacon_node/execution_layer/src/engine_api/json_structures.rs b/beacon_node/execution_layer/src/engine_api/json_structures.rs index efd68f1023..1c6639804e 100644 --- a/beacon_node/execution_layer/src/engine_api/json_structures.rs +++ b/beacon_node/execution_layer/src/engine_api/json_structures.rs @@ -6,7 +6,9 @@ use strum::EnumString; use superstruct::superstruct; use types::beacon_block_body::KzgCommitments; use types::blob_sidecar::BlobsList; -use types::execution_requests::{ConsolidationRequests, DepositRequests, WithdrawalRequests}; +use types::execution_requests::{ + ConsolidationRequests, DepositRequests, RequestPrefix, WithdrawalRequests, +}; use types::{Blob, FixedVector, KzgProof, Unsigned}; #[derive(Debug, PartialEq, Serialize, Deserialize)] @@ -339,25 +341,6 @@ impl From> for ExecutionPayload { } } -/// This is used to index into the `execution_requests` array. -#[derive(Debug, Copy, Clone)] -enum RequestPrefix { - Deposit, - Withdrawal, - Consolidation, -} - -impl RequestPrefix { - pub fn from_prefix(prefix: u8) -> Option { - match prefix { - 0 => Some(Self::Deposit), - 1 => Some(Self::Withdrawal), - 2 => Some(Self::Consolidation), - _ => None, - } - } -} - /// Format of `ExecutionRequests` received over the engine api. /// /// Array of ssz-encoded requests list encoded as hex bytes. @@ -379,7 +362,8 @@ impl TryFrom for ExecutionRequests { for (i, request) in value.0.into_iter().enumerate() { // hex string - let decoded_bytes = hex::decode(request).map_err(|e| format!("Invalid hex {:?}", e))?; + let decoded_bytes = hex::decode(request.strip_prefix("0x").unwrap_or(&request)) + .map_err(|e| format!("Invalid hex {:?}", e))?; match RequestPrefix::from_prefix(i as u8) { Some(RequestPrefix::Deposit) => { requests.deposits = DepositRequests::::from_ssz_bytes(&decoded_bytes) @@ -431,7 +415,7 @@ pub struct JsonGetPayloadResponse { #[superstruct(only(V3, V4))] pub should_override_builder: bool, #[superstruct(only(V4))] - pub requests: JsonExecutionRequests, + pub execution_requests: JsonExecutionRequests, } impl TryFrom> for GetPayloadResponse { @@ -464,7 +448,7 @@ impl TryFrom> for GetPayloadResponse { block_value: response.block_value, blobs_bundle: response.blobs_bundle.into(), should_override_builder: response.should_override_builder, - requests: response.requests.try_into()?, + requests: response.execution_requests.try_into()?, })) } } diff --git a/beacon_node/execution_layer/src/engine_api/new_payload_request.rs b/beacon_node/execution_layer/src/engine_api/new_payload_request.rs index 318779b7f3..60bc848974 100644 --- a/beacon_node/execution_layer/src/engine_api/new_payload_request.rs +++ b/beacon_node/execution_layer/src/engine_api/new_payload_request.rs @@ -44,7 +44,7 @@ pub struct NewPayloadRequest<'block, E: EthSpec> { #[superstruct(only(Deneb, Electra))] pub parent_beacon_block_root: Hash256, #[superstruct(only(Electra))] - pub execution_requests_list: &'block ExecutionRequests, + pub execution_requests: &'block ExecutionRequests, } impl<'block, E: EthSpec> NewPayloadRequest<'block, E> { @@ -121,8 +121,11 @@ impl<'block, E: EthSpec> NewPayloadRequest<'block, E> { let _timer = metrics::start_timer(&metrics::EXECUTION_LAYER_VERIFY_BLOCK_HASH); - let (header_hash, rlp_transactions_root) = - calculate_execution_block_hash(payload, parent_beacon_block_root); + let (header_hash, rlp_transactions_root) = calculate_execution_block_hash( + payload, + parent_beacon_block_root, + self.execution_requests().ok().copied(), + ); if header_hash != self.block_hash() { return Err(Error::BlockHashMismatch { @@ -185,7 +188,7 @@ impl<'a, E: EthSpec> TryFrom> for NewPayloadRequest<'a, E> .map(kzg_commitment_to_versioned_hash) .collect(), parent_beacon_block_root: block_ref.parent_root, - execution_requests_list: &block_ref.body.execution_requests, + execution_requests: &block_ref.body.execution_requests, })), } } diff --git a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs index 786ac9ad9c..9365024ffb 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -374,7 +374,7 @@ pub async fn handle_rpc( .into(), should_override_builder: false, // TODO(electra): add EL requests in mock el - requests: Default::default(), + execution_requests: Default::default(), }) .unwrap() } diff --git a/beacon_node/store/src/partial_beacon_state.rs b/beacon_node/store/src/partial_beacon_state.rs index 2eb40f47b1..22eecdcc60 100644 --- a/beacon_node/store/src/partial_beacon_state.rs +++ b/beacon_node/store/src/partial_beacon_state.rs @@ -136,7 +136,7 @@ where pub earliest_consolidation_epoch: Epoch, #[superstruct(only(Electra))] - pub pending_balance_deposits: List, + pub pending_deposits: List, #[superstruct(only(Electra))] pub pending_partial_withdrawals: List, @@ -403,7 +403,7 @@ impl TryInto> for PartialBeaconState { earliest_exit_epoch, consolidation_balance_to_consume, earliest_consolidation_epoch, - pending_balance_deposits, + pending_deposits, pending_partial_withdrawals, pending_consolidations ], diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index f289b6e081..436f4934b9 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -514,6 +514,7 @@ pub fn get_expected_withdrawals( // Consume pending partial withdrawals let partial_withdrawals_count = if let Ok(partial_withdrawals) = state.pending_partial_withdrawals() { + let mut partial_withdrawals_count = 0; for withdrawal in partial_withdrawals { if withdrawal.withdrawable_epoch > epoch || withdrawals.len() == spec.max_pending_partials_per_withdrawals_sweep as usize @@ -546,8 +547,9 @@ pub fn get_expected_withdrawals( }); withdrawal_index.safe_add_assign(1)?; } + partial_withdrawals_count.safe_add_assign(1)?; } - Some(withdrawals.len()) + Some(partial_withdrawals_count) } else { None }; diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index a53dc15126..22d8592364 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -7,7 +7,6 @@ use crate::per_block_processing::errors::{BlockProcessingError, IntoWithIndex}; use crate::VerifySignatures; use types::consts::altair::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}; use types::typenum::U33; -use types::validator::is_compounding_withdrawal_credential; pub fn process_operations>( state: &mut BeaconState, @@ -378,7 +377,7 @@ pub fn process_deposits( if state.eth1_deposit_index() < eth1_deposit_index_limit { let expected_deposit_len = std::cmp::min( E::MaxDeposits::to_u64(), - state.get_outstanding_deposit_len()?, + eth1_deposit_index_limit.safe_sub(state.eth1_deposit_index())?, ); block_verify!( deposits.len() as u64 == expected_deposit_len, @@ -450,39 +449,46 @@ pub fn apply_deposit( if let Some(index) = validator_index { // [Modified in Electra:EIP7251] - if let Ok(pending_balance_deposits) = state.pending_balance_deposits_mut() { - pending_balance_deposits.push(PendingBalanceDeposit { index, amount })?; - - let validator = state - .validators() - .get(index as usize) - .ok_or(BeaconStateError::UnknownValidator(index as usize))?; - - if is_compounding_withdrawal_credential(deposit_data.withdrawal_credentials, spec) - && validator.has_eth1_withdrawal_credential(spec) - && is_valid_deposit_signature(&deposit_data, spec).is_ok() - { - state.switch_to_compounding_validator(index as usize, spec)?; - } + if let Ok(pending_deposits) = state.pending_deposits_mut() { + pending_deposits.push(PendingDeposit { + pubkey: deposit_data.pubkey, + withdrawal_credentials: deposit_data.withdrawal_credentials, + amount, + signature: deposit_data.signature, + slot: spec.genesis_slot, // Use `genesis_slot` to distinguish from a pending deposit request + })?; } else { // Update the existing validator balance. increase_balance(state, index as usize, amount)?; } - } else { + } + // New validator + else { // The signature should be checked for new validators. Return early for a bad // signature. if is_valid_deposit_signature(&deposit_data, spec).is_err() { return Ok(()); } - state.add_validator_to_registry(&deposit_data, spec)?; - let new_validator_index = state.validators().len().safe_sub(1)? as u64; + state.add_validator_to_registry( + deposit_data.pubkey, + deposit_data.withdrawal_credentials, + if state.fork_name_unchecked() >= ForkName::Electra { + 0 + } else { + amount + }, + spec, + )?; // [New in Electra:EIP7251] - if let Ok(pending_balance_deposits) = state.pending_balance_deposits_mut() { - pending_balance_deposits.push(PendingBalanceDeposit { - index: new_validator_index, + if let Ok(pending_deposits) = state.pending_deposits_mut() { + pending_deposits.push(PendingDeposit { + pubkey: deposit_data.pubkey, + withdrawal_credentials: deposit_data.withdrawal_credentials, amount, + signature: deposit_data.signature, + slot: spec.genesis_slot, // Use `genesis_slot` to distinguish from a pending deposit request })?; } } @@ -596,13 +602,18 @@ pub fn process_deposit_requests( if state.deposit_requests_start_index()? == spec.unset_deposit_requests_start_index { *state.deposit_requests_start_index_mut()? = request.index } - let deposit_data = DepositData { - pubkey: request.pubkey, - withdrawal_credentials: request.withdrawal_credentials, - amount: request.amount, - signature: request.signature.clone().into(), - }; - apply_deposit(state, deposit_data, None, false, spec)? + let slot = state.slot(); + + // [New in Electra:EIP7251] + if let Ok(pending_deposits) = state.pending_deposits_mut() { + pending_deposits.push(PendingDeposit { + pubkey: request.pubkey, + withdrawal_credentials: request.withdrawal_credentials, + amount: request.amount, + signature: request.signature.clone(), + slot, + })?; + } } Ok(()) @@ -621,11 +632,84 @@ pub fn process_consolidation_requests( Ok(()) } +fn is_valid_switch_to_compounding_request( + state: &BeaconState, + consolidation_request: &ConsolidationRequest, + spec: &ChainSpec, +) -> Result { + // Switch to compounding requires source and target be equal + if consolidation_request.source_pubkey != consolidation_request.target_pubkey { + return Ok(false); + } + + // Verify pubkey exists + let Some(source_index) = state + .pubkey_cache() + .get(&consolidation_request.source_pubkey) + else { + // source validator doesn't exist + return Ok(false); + }; + + let source_validator = state.get_validator(source_index)?; + // Verify the source withdrawal credentials + // Note: We need to specifically check for eth1 withdrawal credentials here + // If the validator is already compounding, the compounding request is not valid. + if let Some(withdrawal_address) = source_validator + .has_eth1_withdrawal_credential(spec) + .then(|| { + source_validator + .withdrawal_credentials + .as_slice() + .get(12..) + .map(Address::from_slice) + }) + .flatten() + { + if withdrawal_address != consolidation_request.source_address { + return Ok(false); + } + } else { + // Source doesn't have eth1 withdrawal credentials + return Ok(false); + } + + // Verify the source is active + let current_epoch = state.current_epoch(); + if !source_validator.is_active_at(current_epoch) { + return Ok(false); + } + // Verify exits for source has not been initiated + if source_validator.exit_epoch != spec.far_future_epoch { + return Ok(false); + } + + Ok(true) +} + pub fn process_consolidation_request( state: &mut BeaconState, consolidation_request: &ConsolidationRequest, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { + if is_valid_switch_to_compounding_request(state, consolidation_request, spec)? { + let Some(source_index) = state + .pubkey_cache() + .get(&consolidation_request.source_pubkey) + else { + // source validator doesn't exist. This is unreachable as `is_valid_switch_to_compounding_request` + // will return false in that case. + return Ok(()); + }; + state.switch_to_compounding_validator(source_index, spec)?; + return Ok(()); + } + + // Verify that source != target, so a consolidation cannot be used as an exit. + if consolidation_request.source_pubkey == consolidation_request.target_pubkey { + return Ok(()); + } + // If the pending consolidations queue is full, consolidation requests are ignored if state.pending_consolidations()?.len() == E::PendingConsolidationsLimit::to_usize() { return Ok(()); @@ -649,10 +733,6 @@ pub fn process_consolidation_request( // target validator doesn't exist return Ok(()); }; - // Verify that source != target, so a consolidation cannot be used as an exit. - if source_index == target_index { - return Ok(()); - } let source_validator = state.get_validator(source_index)?; // Verify the source withdrawal credentials @@ -699,5 +779,10 @@ pub fn process_consolidation_request( target_index: target_index as u64, })?; + let target_validator = state.get_validator(target_index)?; + // Churn any target excess active balance of target and raise its max + if target_validator.has_eth1_withdrawal_credential(spec) { + state.switch_to_compounding_validator(target_index, spec)?; + } Ok(()) } diff --git a/consensus/state_processing/src/per_epoch_processing/errors.rs b/consensus/state_processing/src/per_epoch_processing/errors.rs index b6c9dbea52..f45c55a7ac 100644 --- a/consensus/state_processing/src/per_epoch_processing/errors.rs +++ b/consensus/state_processing/src/per_epoch_processing/errors.rs @@ -28,6 +28,7 @@ pub enum EpochProcessingError { SinglePassMissingActivationQueue, MissingEarliestExitEpoch, MissingExitBalanceToConsume, + PendingDepositsLogicError, } impl From for EpochProcessingError { diff --git a/consensus/state_processing/src/per_epoch_processing/single_pass.rs b/consensus/state_processing/src/per_epoch_processing/single_pass.rs index fcb480a37c..904e68e368 100644 --- a/consensus/state_processing/src/per_epoch_processing/single_pass.rs +++ b/consensus/state_processing/src/per_epoch_processing/single_pass.rs @@ -4,6 +4,7 @@ use crate::{ update_progressive_balances_cache::initialize_progressive_balances_cache, }, epoch_cache::{initialize_epoch_cache, PreEpochCache}, + per_block_processing::is_valid_deposit_signature, per_epoch_processing::{Delta, Error, ParticipationEpochSummary}, }; use itertools::izip; @@ -16,9 +17,9 @@ use types::{ TIMELY_TARGET_FLAG_INDEX, WEIGHT_DENOMINATOR, }, milhouse::Cow, - ActivationQueue, BeaconState, BeaconStateError, ChainSpec, Checkpoint, Epoch, EthSpec, - ExitCache, ForkName, List, ParticipationFlags, PendingBalanceDeposit, ProgressiveBalancesCache, - RelativeEpoch, Unsigned, Validator, + ActivationQueue, BeaconState, BeaconStateError, ChainSpec, Checkpoint, DepositData, Epoch, + EthSpec, ExitCache, ForkName, List, ParticipationFlags, PendingDeposit, + ProgressiveBalancesCache, RelativeEpoch, Unsigned, Validator, }; pub struct SinglePassConfig { @@ -26,7 +27,7 @@ pub struct SinglePassConfig { pub rewards_and_penalties: bool, pub registry_updates: bool, pub slashings: bool, - pub pending_balance_deposits: bool, + pub pending_deposits: bool, pub pending_consolidations: bool, pub effective_balance_updates: bool, } @@ -44,7 +45,7 @@ impl SinglePassConfig { rewards_and_penalties: true, registry_updates: true, slashings: true, - pending_balance_deposits: true, + pending_deposits: true, pending_consolidations: true, effective_balance_updates: true, } @@ -56,7 +57,7 @@ impl SinglePassConfig { rewards_and_penalties: false, registry_updates: false, slashings: false, - pending_balance_deposits: false, + pending_deposits: false, pending_consolidations: false, effective_balance_updates: false, } @@ -85,15 +86,17 @@ struct SlashingsContext { penalty_per_effective_balance_increment: u64, } -struct PendingBalanceDepositsContext { +struct PendingDepositsContext { /// The value to set `next_deposit_index` to *after* processing completes. next_deposit_index: usize, /// The value to set `deposit_balance_to_consume` to *after* processing completes. deposit_balance_to_consume: u64, /// Total balance increases for each validator due to pending balance deposits. validator_deposits_to_process: HashMap, - /// The deposits to append to `pending_balance_deposits` after processing all applicable deposits. - deposits_to_postpone: Vec, + /// The deposits to append to `pending_deposits` after processing all applicable deposits. + deposits_to_postpone: Vec, + /// New validators to be added to the state *after* processing completes. + new_validator_deposits: Vec, } struct EffectiveBalancesContext { @@ -138,6 +141,7 @@ pub fn process_epoch_single_pass( state.build_exit_cache(spec)?; state.build_committee_cache(RelativeEpoch::Previous, spec)?; state.build_committee_cache(RelativeEpoch::Current, spec)?; + state.update_pubkey_cache()?; let previous_epoch = state.previous_epoch(); let current_epoch = state.current_epoch(); @@ -163,12 +167,11 @@ pub fn process_epoch_single_pass( let slashings_ctxt = &SlashingsContext::new(state, state_ctxt, spec)?; let mut next_epoch_cache = PreEpochCache::new_for_next_epoch(state)?; - let pending_balance_deposits_ctxt = - if fork_name.electra_enabled() && conf.pending_balance_deposits { - Some(PendingBalanceDepositsContext::new(state, spec)?) - } else { - None - }; + let pending_deposits_ctxt = if fork_name.electra_enabled() && conf.pending_deposits { + Some(PendingDepositsContext::new(state, spec, &conf)?) + } else { + None + }; let mut earliest_exit_epoch = state.earliest_exit_epoch().ok(); let mut exit_balance_to_consume = state.exit_balance_to_consume().ok(); @@ -303,9 +306,9 @@ pub fn process_epoch_single_pass( process_single_slashing(&mut balance, &validator, slashings_ctxt, state_ctxt, spec)?; } - // `process_pending_balance_deposits` - if let Some(pending_balance_deposits_ctxt) = &pending_balance_deposits_ctxt { - process_pending_balance_deposits_for_validator( + // `process_pending_deposits` + if let Some(pending_balance_deposits_ctxt) = &pending_deposits_ctxt { + process_pending_deposits_for_validator( &mut balance, validator_info, pending_balance_deposits_ctxt, @@ -342,20 +345,84 @@ pub fn process_epoch_single_pass( // Finish processing pending balance deposits if relevant. // // This *could* be reordered after `process_pending_consolidations` which pushes only to the end - // of the `pending_balance_deposits` list. But we may as well preserve the write ordering used + // of the `pending_deposits` list. But we may as well preserve the write ordering used // by the spec and do this first. - if let Some(ctxt) = pending_balance_deposits_ctxt { - let mut new_pending_balance_deposits = List::try_from_iter( + if let Some(ctxt) = pending_deposits_ctxt { + let mut new_balance_deposits = List::try_from_iter( state - .pending_balance_deposits()? + .pending_deposits()? .iter_from(ctxt.next_deposit_index)? .cloned(), )?; for deposit in ctxt.deposits_to_postpone { - new_pending_balance_deposits.push(deposit)?; + new_balance_deposits.push(deposit)?; } - *state.pending_balance_deposits_mut()? = new_pending_balance_deposits; + *state.pending_deposits_mut()? = new_balance_deposits; *state.deposit_balance_to_consume_mut()? = ctxt.deposit_balance_to_consume; + + // `new_validator_deposits` may contain multiple deposits with the same pubkey where + // the first deposit creates the new validator and the others are topups. + // Each item in the vec is a (pubkey, validator_index) + let mut added_validators = Vec::new(); + for deposit in ctxt.new_validator_deposits { + let deposit_data = DepositData { + pubkey: deposit.pubkey, + withdrawal_credentials: deposit.withdrawal_credentials, + amount: deposit.amount, + signature: deposit.signature, + }; + // Only check the signature if this is the first deposit for the validator, + // following the logic from `apply_pending_deposit` in the spec. + if let Some(validator_index) = state.get_validator_index(&deposit_data.pubkey)? { + state + .get_balance_mut(validator_index)? + .safe_add_assign(deposit_data.amount)?; + } else if is_valid_deposit_signature(&deposit_data, spec).is_ok() { + // Apply the new deposit to the state + let validator_index = state.add_validator_to_registry( + deposit_data.pubkey, + deposit_data.withdrawal_credentials, + deposit_data.amount, + spec, + )?; + added_validators.push((deposit_data.pubkey, validator_index)); + } + } + if conf.effective_balance_updates { + // Re-process effective balance updates for validators affected by top-up of new validators. + let ( + validators, + balances, + _, + current_epoch_participation, + _, + progressive_balances, + _, + _, + ) = state.mutable_validator_fields()?; + for (_, validator_index) in added_validators.iter() { + let balance = *balances + .get(*validator_index) + .ok_or(BeaconStateError::UnknownValidator(*validator_index))?; + let mut validator = validators + .get_cow(*validator_index) + .ok_or(BeaconStateError::UnknownValidator(*validator_index))?; + let validator_current_epoch_participation = *current_epoch_participation + .get(*validator_index) + .ok_or(BeaconStateError::UnknownValidator(*validator_index))?; + process_single_effective_balance_update( + *validator_index, + balance, + &mut validator, + validator_current_epoch_participation, + &mut next_epoch_cache, + progressive_balances, + effective_balances_ctxt, + state_ctxt, + spec, + )?; + } + } } // Process consolidations outside the single-pass loop, as they depend on balances for multiple @@ -819,8 +886,12 @@ fn process_single_slashing( Ok(()) } -impl PendingBalanceDepositsContext { - fn new(state: &BeaconState, spec: &ChainSpec) -> Result { +impl PendingDepositsContext { + fn new( + state: &BeaconState, + spec: &ChainSpec, + config: &SinglePassConfig, + ) -> Result { let available_for_processing = state .deposit_balance_to_consume()? .safe_add(state.get_activation_exit_churn_limit(spec)?)?; @@ -830,10 +901,31 @@ impl PendingBalanceDepositsContext { let mut next_deposit_index = 0; let mut validator_deposits_to_process = HashMap::new(); let mut deposits_to_postpone = vec![]; + let mut new_validator_deposits = vec![]; + let mut is_churn_limit_reached = false; + let finalized_slot = state + .finalized_checkpoint() + .epoch + .start_slot(E::slots_per_epoch()); - let pending_balance_deposits = state.pending_balance_deposits()?; + let pending_deposits = state.pending_deposits()?; - for deposit in pending_balance_deposits.iter() { + for deposit in pending_deposits.iter() { + // Do not process deposit requests if the Eth1 bridge deposits are not yet applied. + if deposit.slot > spec.genesis_slot + && state.eth1_deposit_index() < state.deposit_requests_start_index()? + { + break; + } + // Do not process is deposit slot has not been finalized. + if deposit.slot > finalized_slot { + break; + } + // Do not process if we have reached the limit for the number of deposits + // processed in an epoch. + if next_deposit_index >= E::max_pending_deposits_per_epoch() { + break; + } // We have to do a bit of indexing into `validators` here, but I can't see any way // around that without changing the spec. // @@ -844,48 +936,70 @@ impl PendingBalanceDepositsContext { // take, just whether it is non-default. Nor do we need to know the value of // `withdrawable_epoch`, because `next_epoch <= withdrawable_epoch` will evaluate to // `true` both for the actual value & the default placeholder value (`FAR_FUTURE_EPOCH`). - let validator = state.get_validator(deposit.index as usize)?; - let already_exited = validator.exit_epoch < spec.far_future_epoch; - // In the spec process_registry_updates is called before process_pending_balance_deposits - // so we must account for process_registry_updates ejecting the validator for low balance - // and setting the exit_epoch to < far_future_epoch. Note that in the spec the effective - // balance update does not happen until *after* the registry update, so we don't need to - // account for changes to the effective balance that would push it below the ejection - // balance here. - let will_be_exited = validator.is_active_at(current_epoch) - && validator.effective_balance <= spec.ejection_balance; - if already_exited || will_be_exited { - if next_epoch <= validator.withdrawable_epoch { - deposits_to_postpone.push(deposit.clone()); - } else { - // Deposited balance will never become active. Increase balance but do not - // consume churn. - validator_deposits_to_process - .entry(deposit.index as usize) - .or_insert(0) - .safe_add_assign(deposit.amount)?; - } - } else { - // Deposit does not fit in the churn, no more deposit processing in this epoch. - if processed_amount.safe_add(deposit.amount)? > available_for_processing { - break; - } - // Deposit fits in the churn, process it. Increase balance and consume churn. + let mut is_validator_exited = false; + let mut is_validator_withdrawn = false; + let opt_validator_index = state.pubkey_cache().get(&deposit.pubkey); + if let Some(validator_index) = opt_validator_index { + let validator = state.get_validator(validator_index)?; + let already_exited = validator.exit_epoch < spec.far_future_epoch; + // In the spec process_registry_updates is called before process_pending_deposits + // so we must account for process_registry_updates ejecting the validator for low balance + // and setting the exit_epoch to < far_future_epoch. Note that in the spec the effective + // balance update does not happen until *after* the registry update, so we don't need to + // account for changes to the effective balance that would push it below the ejection + // balance here. + // Note: we only consider this if registry_updates are enabled in the config. + // EF tests require us to run epoch_processing functions in isolation. + let will_be_exited = config.registry_updates + && (validator.is_active_at(current_epoch) + && validator.effective_balance <= spec.ejection_balance); + is_validator_exited = already_exited || will_be_exited; + is_validator_withdrawn = validator.withdrawable_epoch < next_epoch; + } + + if is_validator_withdrawn { + // Deposited balance will never become active. Queue a balance increase but do not + // consume churn. Validator index must be known if the validator is known to be + // withdrawn (see calculation of `is_validator_withdrawn` above). + let validator_index = + opt_validator_index.ok_or(Error::PendingDepositsLogicError)?; validator_deposits_to_process - .entry(deposit.index as usize) + .entry(validator_index) .or_insert(0) .safe_add_assign(deposit.amount)?; + } else if is_validator_exited { + // Validator is exiting, postpone the deposit until after withdrawable epoch + deposits_to_postpone.push(deposit.clone()); + } else { + // Check if deposit fits in the churn, otherwise, do no more deposit processing in this epoch. + is_churn_limit_reached = + processed_amount.safe_add(deposit.amount)? > available_for_processing; + if is_churn_limit_reached { + break; + } processed_amount.safe_add_assign(deposit.amount)?; + + // Deposit fits in the churn, process it. Increase balance and consume churn. + if let Some(validator_index) = state.pubkey_cache().get(&deposit.pubkey) { + validator_deposits_to_process + .entry(validator_index) + .or_insert(0) + .safe_add_assign(deposit.amount)?; + } else { + // The `PendingDeposit` is for a new validator + new_validator_deposits.push(deposit.clone()); + } } // Regardless of how the deposit was handled, we move on in the queue. next_deposit_index.safe_add_assign(1)?; } - let deposit_balance_to_consume = if next_deposit_index == pending_balance_deposits.len() { - 0 - } else { + // Accumulate churn only if the churn limit has been hit. + let deposit_balance_to_consume = if is_churn_limit_reached { available_for_processing.safe_sub(processed_amount)? + } else { + 0 }; Ok(Self { @@ -893,14 +1007,15 @@ impl PendingBalanceDepositsContext { deposit_balance_to_consume, validator_deposits_to_process, deposits_to_postpone, + new_validator_deposits, }) } } -fn process_pending_balance_deposits_for_validator( +fn process_pending_deposits_for_validator( balance: &mut Cow, validator_info: &ValidatorInfo, - pending_balance_deposits_ctxt: &PendingBalanceDepositsContext, + pending_balance_deposits_ctxt: &PendingDepositsContext, ) -> Result<(), Error> { if let Some(deposit_amount) = pending_balance_deposits_ctxt .validator_deposits_to_process @@ -941,21 +1056,20 @@ fn process_pending_consolidations( break; } - // Calculate the active balance while we have the source validator loaded. This is a safe - // reordering. - let source_balance = *state - .balances() - .get(source_index) - .ok_or(BeaconStateError::UnknownValidator(source_index))?; - let active_balance = - source_validator.get_active_balance(source_balance, spec, state_ctxt.fork_name); - - // Churn any target excess active balance of target and raise its max. - state.switch_to_compounding_validator(target_index, spec)?; + // Calculate the consolidated balance + let max_effective_balance = + source_validator.get_max_effective_balance(spec, state_ctxt.fork_name); + let source_effective_balance = std::cmp::min( + *state + .balances() + .get(source_index) + .ok_or(BeaconStateError::UnknownValidator(source_index))?, + max_effective_balance, + ); // Move active balance to target. Excess balance is withdrawable. - decrease_balance(state, source_index, active_balance)?; - increase_balance(state, target_index, active_balance)?; + decrease_balance(state, source_index, source_effective_balance)?; + increase_balance(state, target_index, source_effective_balance)?; affected_validators.insert(source_index); affected_validators.insert(target_index); diff --git a/consensus/state_processing/src/upgrade/electra.rs b/consensus/state_processing/src/upgrade/electra.rs index 1e532d9f10..1e64ef2897 100644 --- a/consensus/state_processing/src/upgrade/electra.rs +++ b/consensus/state_processing/src/upgrade/electra.rs @@ -1,8 +1,10 @@ +use bls::Signature; +use itertools::Itertools; use safe_arith::SafeArith; use std::mem; use types::{ BeaconState, BeaconStateElectra, BeaconStateError as Error, ChainSpec, Epoch, EpochCache, - EthSpec, Fork, + EthSpec, Fork, PendingDeposit, }; /// Transform a `Deneb` state into an `Electra` state. @@ -38,29 +40,44 @@ pub fn upgrade_to_electra( // Add validators that are not yet active to pending balance deposits let validators = post.validators().clone(); - let mut pre_activation = validators + let pre_activation = validators .iter() .enumerate() .filter(|(_, validator)| validator.activation_epoch == spec.far_future_epoch) + .sorted_by_key(|(index, validator)| (validator.activation_eligibility_epoch, *index)) .collect::>(); - // Sort the indices by activation_eligibility_epoch and then by index - pre_activation.sort_by(|(index_a, val_a), (index_b, val_b)| { - if val_a.activation_eligibility_epoch == val_b.activation_eligibility_epoch { - index_a.cmp(index_b) - } else { - val_a - .activation_eligibility_epoch - .cmp(&val_b.activation_eligibility_epoch) - } - }); - // Process validators to queue entire balance and reset them for (index, _) in pre_activation { - post.queue_entire_balance_and_reset_validator(index, spec)?; + let balance = post + .balances_mut() + .get_mut(index) + .ok_or(Error::UnknownValidator(index))?; + let balance_copy = *balance; + *balance = 0_u64; + + let validator = post + .validators_mut() + .get_mut(index) + .ok_or(Error::UnknownValidator(index))?; + validator.effective_balance = 0; + validator.activation_eligibility_epoch = spec.far_future_epoch; + let pubkey = validator.pubkey; + let withdrawal_credentials = validator.withdrawal_credentials; + + post.pending_deposits_mut()? + .push(PendingDeposit { + pubkey, + withdrawal_credentials, + amount: balance_copy, + signature: Signature::infinity()?.into(), + slot: spec.genesis_slot, + }) + .map_err(Error::MilhouseError)?; } // Ensure early adopters of compounding credentials go through the activation churn + let validators = post.validators().clone(); for (index, validator) in validators.iter().enumerate() { if validator.has_compounding_withdrawal_credential(spec) { post.queue_excess_active_balance(index, spec)?; @@ -137,7 +154,7 @@ pub fn upgrade_state_to_electra( earliest_exit_epoch, consolidation_balance_to_consume: 0, earliest_consolidation_epoch, - pending_balance_deposits: Default::default(), + pending_deposits: Default::default(), pending_partial_withdrawals: Default::default(), pending_consolidations: Default::default(), // Caches diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 833231dca3..77b72b209c 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -509,7 +509,7 @@ where #[compare_fields(as_iter)] #[test_random(default)] #[superstruct(only(Electra))] - pub pending_balance_deposits: List, + pub pending_deposits: List, #[compare_fields(as_iter)] #[test_random(default)] #[superstruct(only(Electra))] @@ -1547,19 +1547,23 @@ impl BeaconState { .ok_or(Error::UnknownValidator(validator_index)) } + /// Add a validator to the registry and return the validator index that was allocated for it. pub fn add_validator_to_registry( &mut self, - deposit_data: &DepositData, + pubkey: PublicKeyBytes, + withdrawal_credentials: Hash256, + amount: u64, spec: &ChainSpec, - ) -> Result<(), Error> { - let fork = self.fork_name_unchecked(); - let amount = if fork.electra_enabled() { - 0 - } else { - deposit_data.amount - }; - self.validators_mut() - .push(Validator::from_deposit(deposit_data, amount, fork, spec))?; + ) -> Result { + let index = self.validators().len(); + let fork_name = self.fork_name_unchecked(); + self.validators_mut().push(Validator::from_deposit( + pubkey, + withdrawal_credentials, + amount, + fork_name, + spec, + ))?; self.balances_mut().push(amount)?; // Altair or later initializations. @@ -1573,7 +1577,20 @@ impl BeaconState { inactivity_scores.push(0)?; } - Ok(()) + // Keep the pubkey cache up to date if it was up to date prior to this call. + // + // Doing this here while we know the pubkey and index is marginally quicker than doing it in + // a call to `update_pubkey_cache` later because we don't need to index into the validators + // tree again. + let pubkey_cache = self.pubkey_cache_mut(); + if pubkey_cache.len() == index { + let success = pubkey_cache.insert(pubkey, index); + if !success { + return Err(Error::PubkeyCacheInconsistent); + } + } + + Ok(index) } /// Safe copy-on-write accessor for the `validators` list. @@ -1780,19 +1797,6 @@ impl BeaconState { } } - /// Get the number of outstanding deposits. - /// - /// Returns `Err` if the state is invalid. - pub fn get_outstanding_deposit_len(&self) -> Result { - self.eth1_data() - .deposit_count - .checked_sub(self.eth1_deposit_index()) - .ok_or(Error::InvalidDepositState { - deposit_count: self.eth1_data().deposit_count, - deposit_index: self.eth1_deposit_index(), - }) - } - /// Build all caches (except the tree hash cache), if they need to be built. pub fn build_caches(&mut self, spec: &ChainSpec) -> Result<(), Error> { self.build_all_committee_caches(spec)?; @@ -2149,27 +2153,6 @@ impl BeaconState { .map_err(Into::into) } - /// Get active balance for the given `validator_index`. - pub fn get_active_balance( - &self, - validator_index: usize, - spec: &ChainSpec, - current_fork: ForkName, - ) -> Result { - let max_effective_balance = self - .validators() - .get(validator_index) - .map(|validator| validator.get_max_effective_balance(spec, current_fork)) - .ok_or(Error::UnknownValidator(validator_index))?; - Ok(std::cmp::min( - *self - .balances() - .get(validator_index) - .ok_or(Error::UnknownValidator(validator_index))?, - max_effective_balance, - )) - } - pub fn get_pending_balance_to_withdraw(&self, validator_index: usize) -> Result { let mut pending_balance = 0; for withdrawal in self @@ -2196,42 +2179,18 @@ impl BeaconState { if *balance > spec.min_activation_balance { let excess_balance = balance.safe_sub(spec.min_activation_balance)?; *balance = spec.min_activation_balance; - self.pending_balance_deposits_mut()? - .push(PendingBalanceDeposit { - index: validator_index as u64, - amount: excess_balance, - })?; + let validator = self.get_validator(validator_index)?.clone(); + self.pending_deposits_mut()?.push(PendingDeposit { + pubkey: validator.pubkey, + withdrawal_credentials: validator.withdrawal_credentials, + amount: excess_balance, + signature: Signature::infinity()?.into(), + slot: spec.genesis_slot, + })?; } Ok(()) } - pub fn queue_entire_balance_and_reset_validator( - &mut self, - validator_index: usize, - spec: &ChainSpec, - ) -> Result<(), Error> { - let balance = self - .balances_mut() - .get_mut(validator_index) - .ok_or(Error::UnknownValidator(validator_index))?; - let balance_copy = *balance; - *balance = 0_u64; - - let validator = self - .validators_mut() - .get_mut(validator_index) - .ok_or(Error::UnknownValidator(validator_index))?; - validator.effective_balance = 0; - validator.activation_eligibility_epoch = spec.far_future_epoch; - - self.pending_balance_deposits_mut()? - .push(PendingBalanceDeposit { - index: validator_index as u64, - amount: balance_copy, - }) - .map_err(Into::into) - } - /// Change the withdrawal prefix of the given `validator_index` to the compounding withdrawal validator prefix. pub fn switch_to_compounding_validator( &mut self, @@ -2242,12 +2201,10 @@ impl BeaconState { .validators_mut() .get_mut(validator_index) .ok_or(Error::UnknownValidator(validator_index))?; - if validator.has_eth1_withdrawal_credential(spec) { - AsMut::<[u8; 32]>::as_mut(&mut validator.withdrawal_credentials)[0] = - spec.compounding_withdrawal_prefix_byte; + AsMut::<[u8; 32]>::as_mut(&mut validator.withdrawal_credentials)[0] = + spec.compounding_withdrawal_prefix_byte; - self.queue_excess_active_balance(validator_index, spec)?; - } + self.queue_excess_active_balance(validator_index, spec)?; Ok(()) } diff --git a/consensus/types/src/beacon_state/tests.rs b/consensus/types/src/beacon_state/tests.rs index 3ad3ccf561..bfa7bb86d2 100644 --- a/consensus/types/src/beacon_state/tests.rs +++ b/consensus/types/src/beacon_state/tests.rs @@ -307,43 +307,6 @@ mod committees { } } -mod get_outstanding_deposit_len { - use super::*; - - async fn state() -> BeaconState { - get_harness(16, Slot::new(0)) - .await - .chain - .head_beacon_state_cloned() - } - - #[tokio::test] - async fn returns_ok() { - let mut state = state().await; - assert_eq!(state.get_outstanding_deposit_len(), Ok(0)); - - state.eth1_data_mut().deposit_count = 17; - *state.eth1_deposit_index_mut() = 16; - assert_eq!(state.get_outstanding_deposit_len(), Ok(1)); - } - - #[tokio::test] - async fn returns_err_if_the_state_is_invalid() { - let mut state = state().await; - // The state is invalid, deposit count is lower than deposit index. - state.eth1_data_mut().deposit_count = 16; - *state.eth1_deposit_index_mut() = 17; - - assert_eq!( - state.get_outstanding_deposit_len(), - Err(BeaconStateError::InvalidDepositState { - deposit_count: 16, - deposit_index: 17, - }) - ); - } -} - #[test] fn decode_base_and_altair() { type E = MainnetEthSpec; diff --git a/consensus/types/src/deposit_request.rs b/consensus/types/src/deposit_request.rs index 7af949fef3..a21760551b 100644 --- a/consensus/types/src/deposit_request.rs +++ b/consensus/types/src/deposit_request.rs @@ -1,5 +1,6 @@ use crate::test_utils::TestRandom; -use crate::{Hash256, PublicKeyBytes, Signature}; +use crate::{Hash256, PublicKeyBytes}; +use bls::SignatureBytes; use serde::{Deserialize, Serialize}; use ssz::Encode; use ssz_derive::{Decode, Encode}; @@ -10,7 +11,6 @@ use tree_hash_derive::TreeHash; arbitrary::Arbitrary, Debug, PartialEq, - Eq, Hash, Clone, Serialize, @@ -25,7 +25,7 @@ pub struct DepositRequest { pub withdrawal_credentials: Hash256, #[serde(with = "serde_utils::quoted_u64")] pub amount: u64, - pub signature: Signature, + pub signature: SignatureBytes, #[serde(with = "serde_utils::quoted_u64")] pub index: u64, } @@ -36,7 +36,7 @@ impl DepositRequest { pubkey: PublicKeyBytes::empty(), withdrawal_credentials: Hash256::ZERO, amount: 0, - signature: Signature::empty(), + signature: SignatureBytes::empty(), index: 0, } .as_ssz_bytes() diff --git a/consensus/types/src/eth_spec.rs b/consensus/types/src/eth_spec.rs index 09ef8e3c1a..23e8276209 100644 --- a/consensus/types/src/eth_spec.rs +++ b/consensus/types/src/eth_spec.rs @@ -151,7 +151,7 @@ pub trait EthSpec: /* * New in Electra */ - type PendingBalanceDepositsLimit: Unsigned + Clone + Sync + Send + Debug + PartialEq; + type PendingDepositsLimit: Unsigned + Clone + Sync + Send + Debug + PartialEq; type PendingPartialWithdrawalsLimit: Unsigned + Clone + Sync + Send + Debug + PartialEq; type PendingConsolidationsLimit: Unsigned + Clone + Sync + Send + Debug + PartialEq; type MaxConsolidationRequestsPerPayload: Unsigned + Clone + Sync + Send + Debug + PartialEq; @@ -159,6 +159,7 @@ pub trait EthSpec: type MaxAttesterSlashingsElectra: Unsigned + Clone + Sync + Send + Debug + PartialEq; type MaxAttestationsElectra: Unsigned + Clone + Sync + Send + Debug + PartialEq; type MaxWithdrawalRequestsPerPayload: Unsigned + Clone + Sync + Send + Debug + PartialEq; + type MaxPendingDepositsPerEpoch: Unsigned + Clone + Sync + Send + Debug + PartialEq; fn default_spec() -> ChainSpec; @@ -331,9 +332,9 @@ pub trait EthSpec: .expect("Preset values are not configurable and never result in non-positive block body depth") } - /// Returns the `PENDING_BALANCE_DEPOSITS_LIMIT` constant for this specification. - fn pending_balance_deposits_limit() -> usize { - Self::PendingBalanceDepositsLimit::to_usize() + /// Returns the `PENDING_DEPOSITS_LIMIT` constant for this specification. + fn pending_deposits_limit() -> usize { + Self::PendingDepositsLimit::to_usize() } /// Returns the `PENDING_PARTIAL_WITHDRAWALS_LIMIT` constant for this specification. @@ -371,6 +372,11 @@ pub trait EthSpec: Self::MaxWithdrawalRequestsPerPayload::to_usize() } + /// Returns the `MAX_PENDING_DEPOSITS_PER_EPOCH` constant for this specification. + fn max_pending_deposits_per_epoch() -> usize { + Self::MaxPendingDepositsPerEpoch::to_usize() + } + fn kzg_commitments_inclusion_proof_depth() -> usize { Self::KzgCommitmentsInclusionProofDepth::to_usize() } @@ -430,7 +436,7 @@ impl EthSpec for MainnetEthSpec { type SlotsPerEth1VotingPeriod = U2048; // 64 epochs * 32 slots per epoch type MaxBlsToExecutionChanges = U16; type MaxWithdrawalsPerPayload = U16; - type PendingBalanceDepositsLimit = U134217728; + type PendingDepositsLimit = U134217728; type PendingPartialWithdrawalsLimit = U134217728; type PendingConsolidationsLimit = U262144; type MaxConsolidationRequestsPerPayload = U1; @@ -438,6 +444,7 @@ impl EthSpec for MainnetEthSpec { type MaxAttesterSlashingsElectra = U1; type MaxAttestationsElectra = U8; type MaxWithdrawalRequestsPerPayload = U16; + type MaxPendingDepositsPerEpoch = U16; fn default_spec() -> ChainSpec { ChainSpec::mainnet() @@ -500,7 +507,8 @@ impl EthSpec for MinimalEthSpec { MaxBlsToExecutionChanges, MaxBlobsPerBlock, BytesPerFieldElement, - PendingBalanceDepositsLimit, + PendingDepositsLimit, + MaxPendingDepositsPerEpoch, MaxConsolidationRequestsPerPayload, MaxAttesterSlashingsElectra, MaxAttestationsElectra @@ -557,7 +565,7 @@ impl EthSpec for GnosisEthSpec { type BytesPerFieldElement = U32; type BytesPerBlob = U131072; type KzgCommitmentInclusionProofDepth = U17; - type PendingBalanceDepositsLimit = U134217728; + type PendingDepositsLimit = U134217728; type PendingPartialWithdrawalsLimit = U134217728; type PendingConsolidationsLimit = U262144; type MaxConsolidationRequestsPerPayload = U1; @@ -565,6 +573,7 @@ impl EthSpec for GnosisEthSpec { type MaxAttesterSlashingsElectra = U1; type MaxAttestationsElectra = U8; type MaxWithdrawalRequestsPerPayload = U16; + type MaxPendingDepositsPerEpoch = U16; type FieldElementsPerCell = U64; type FieldElementsPerExtBlob = U8192; type BytesPerCell = U2048; diff --git a/consensus/types/src/execution_block_header.rs b/consensus/types/src/execution_block_header.rs index 694162d6ff..60f2960afb 100644 --- a/consensus/types/src/execution_block_header.rs +++ b/consensus/types/src/execution_block_header.rs @@ -52,9 +52,11 @@ pub struct ExecutionBlockHeader { pub blob_gas_used: Option, pub excess_blob_gas: Option, pub parent_beacon_block_root: Option, + pub requests_root: Option, } impl ExecutionBlockHeader { + #[allow(clippy::too_many_arguments)] pub fn from_payload( payload: ExecutionPayloadRef, rlp_empty_list_root: Hash256, @@ -63,6 +65,7 @@ impl ExecutionBlockHeader { rlp_blob_gas_used: Option, rlp_excess_blob_gas: Option, rlp_parent_beacon_block_root: Option, + rlp_requests_root: Option, ) -> Self { // Most of these field mappings are defined in EIP-3675 except for `mixHash`, which is // defined in EIP-4399. @@ -87,6 +90,7 @@ impl ExecutionBlockHeader { blob_gas_used: rlp_blob_gas_used, excess_blob_gas: rlp_excess_blob_gas, parent_beacon_block_root: rlp_parent_beacon_block_root, + requests_root: rlp_requests_root, } } } @@ -114,6 +118,7 @@ pub struct EncodableExecutionBlockHeader<'a> { pub blob_gas_used: Option, pub excess_blob_gas: Option, pub parent_beacon_block_root: Option<&'a [u8]>, + pub requests_root: Option<&'a [u8]>, } impl<'a> From<&'a ExecutionBlockHeader> for EncodableExecutionBlockHeader<'a> { @@ -139,6 +144,7 @@ impl<'a> From<&'a ExecutionBlockHeader> for EncodableExecutionBlockHeader<'a> { blob_gas_used: header.blob_gas_used, excess_blob_gas: header.excess_blob_gas, parent_beacon_block_root: None, + requests_root: None, }; if let Some(withdrawals_root) = &header.withdrawals_root { encodable.withdrawals_root = Some(withdrawals_root.as_slice()); @@ -146,6 +152,9 @@ impl<'a> From<&'a ExecutionBlockHeader> for EncodableExecutionBlockHeader<'a> { if let Some(parent_beacon_block_root) = &header.parent_beacon_block_root { encodable.parent_beacon_block_root = Some(parent_beacon_block_root.as_slice()) } + if let Some(requests_root) = &header.requests_root { + encodable.requests_root = Some(requests_root.as_slice()) + } encodable } } diff --git a/consensus/types/src/execution_requests.rs b/consensus/types/src/execution_requests.rs index 778260dd84..96a3905420 100644 --- a/consensus/types/src/execution_requests.rs +++ b/consensus/types/src/execution_requests.rs @@ -1,7 +1,8 @@ use crate::test_utils::TestRandom; -use crate::{ConsolidationRequest, DepositRequest, EthSpec, WithdrawalRequest}; +use crate::{ConsolidationRequest, DepositRequest, EthSpec, Hash256, WithdrawalRequest}; use alloy_primitives::Bytes; use derivative::Derivative; +use ethereum_hashing::{DynamicContext, Sha256Context}; use serde::{Deserialize, Serialize}; use ssz::Encode; use ssz_derive::{Decode, Encode}; @@ -47,6 +48,43 @@ impl ExecutionRequests { let consolidation_bytes = Bytes::from(self.consolidations.as_ssz_bytes()); vec![deposit_bytes, withdrawal_bytes, consolidation_bytes] } + + /// Generate the execution layer `requests_hash` based on EIP-7685. + /// + /// `sha256(sha256(requests_0) ++ sha256(requests_1) ++ ...)` + pub fn requests_hash(&self) -> Hash256 { + let mut hasher = DynamicContext::new(); + + for (i, request) in self.get_execution_requests_list().iter().enumerate() { + let mut request_hasher = DynamicContext::new(); + request_hasher.update(&[i as u8]); + request_hasher.update(request); + let request_hash = request_hasher.finalize(); + + hasher.update(&request_hash); + } + + hasher.finalize().into() + } +} + +/// This is used to index into the `execution_requests` array. +#[derive(Debug, Copy, Clone)] +pub enum RequestPrefix { + Deposit, + Withdrawal, + Consolidation, +} + +impl RequestPrefix { + pub fn from_prefix(prefix: u8) -> Option { + match prefix { + 0 => Some(Self::Deposit), + 1 => Some(Self::Withdrawal), + 2 => Some(Self::Consolidation), + _ => None, + } + } } #[cfg(test)] diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index eff5237834..dd304c6296 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -54,8 +54,8 @@ pub mod light_client_finality_update; pub mod light_client_optimistic_update; pub mod light_client_update; pub mod pending_attestation; -pub mod pending_balance_deposit; pub mod pending_consolidation; +pub mod pending_deposit; pub mod pending_partial_withdrawal; pub mod proposer_preparation_data; pub mod proposer_slashing; @@ -170,7 +170,7 @@ pub use crate::execution_payload_header::{ ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderElectra, ExecutionPayloadHeaderRef, ExecutionPayloadHeaderRefMut, }; -pub use crate::execution_requests::ExecutionRequests; +pub use crate::execution_requests::{ExecutionRequests, RequestPrefix}; pub use crate::fork::Fork; pub use crate::fork_context::ForkContext; pub use crate::fork_data::ForkData; @@ -210,8 +210,8 @@ pub use crate::payload::{ FullPayloadRef, OwnedExecPayload, }; pub use crate::pending_attestation::PendingAttestation; -pub use crate::pending_balance_deposit::PendingBalanceDeposit; pub use crate::pending_consolidation::PendingConsolidation; +pub use crate::pending_deposit::PendingDeposit; pub use crate::pending_partial_withdrawal::PendingPartialWithdrawal; pub use crate::preset::{ AltairPreset, BasePreset, BellatrixPreset, CapellaPreset, DenebPreset, ElectraPreset, diff --git a/consensus/types/src/pending_balance_deposit.rs b/consensus/types/src/pending_deposit.rs similarity index 68% rename from consensus/types/src/pending_balance_deposit.rs rename to consensus/types/src/pending_deposit.rs index a2bce577f8..3bee86417d 100644 --- a/consensus/types/src/pending_balance_deposit.rs +++ b/consensus/types/src/pending_deposit.rs @@ -1,4 +1,5 @@ use crate::test_utils::TestRandom; +use crate::*; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; @@ -8,7 +9,6 @@ use tree_hash_derive::TreeHash; arbitrary::Arbitrary, Debug, PartialEq, - Eq, Hash, Clone, Serialize, @@ -18,16 +18,18 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] -pub struct PendingBalanceDeposit { - #[serde(with = "serde_utils::quoted_u64")] - pub index: u64, +pub struct PendingDeposit { + pub pubkey: PublicKeyBytes, + pub withdrawal_credentials: Hash256, #[serde(with = "serde_utils::quoted_u64")] pub amount: u64, + pub signature: SignatureBytes, + pub slot: Slot, } #[cfg(test)] mod tests { use super::*; - ssz_and_tree_hash_tests!(PendingBalanceDeposit); + ssz_and_tree_hash_tests!(PendingDeposit); } diff --git a/consensus/types/src/preset.rs b/consensus/types/src/preset.rs index 435a74bdc3..b469b7b777 100644 --- a/consensus/types/src/preset.rs +++ b/consensus/types/src/preset.rs @@ -263,7 +263,7 @@ impl ElectraPreset { whistleblower_reward_quotient_electra: spec.whistleblower_reward_quotient_electra, max_pending_partials_per_withdrawals_sweep: spec .max_pending_partials_per_withdrawals_sweep, - pending_balance_deposits_limit: E::pending_balance_deposits_limit() as u64, + pending_balance_deposits_limit: E::pending_deposits_limit() as u64, pending_partial_withdrawals_limit: E::pending_partial_withdrawals_limit() as u64, pending_consolidations_limit: E::pending_consolidations_limit() as u64, max_consolidation_requests_per_payload: E::max_consolidation_requests_per_payload() diff --git a/consensus/types/src/validator.rs b/consensus/types/src/validator.rs index 275101ddbe..222b9292a2 100644 --- a/consensus/types/src/validator.rs +++ b/consensus/types/src/validator.rs @@ -1,6 +1,6 @@ use crate::{ - test_utils::TestRandom, Address, BeaconState, ChainSpec, Checkpoint, DepositData, Epoch, - EthSpec, FixedBytesExtended, ForkName, Hash256, PublicKeyBytes, + test_utils::TestRandom, Address, BeaconState, ChainSpec, Checkpoint, Epoch, EthSpec, + FixedBytesExtended, ForkName, Hash256, PublicKeyBytes, }; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; @@ -38,14 +38,15 @@ pub struct Validator { impl Validator { #[allow(clippy::arithmetic_side_effects)] pub fn from_deposit( - deposit_data: &DepositData, + pubkey: PublicKeyBytes, + withdrawal_credentials: Hash256, amount: u64, fork_name: ForkName, spec: &ChainSpec, ) -> Self { let mut validator = Validator { - pubkey: deposit_data.pubkey, - withdrawal_credentials: deposit_data.withdrawal_credentials, + pubkey, + withdrawal_credentials, activation_eligibility_epoch: spec.far_future_epoch, activation_epoch: spec.far_future_epoch, exit_epoch: spec.far_future_epoch, @@ -291,16 +292,6 @@ impl Validator { spec.max_effective_balance } } - - pub fn get_active_balance( - &self, - validator_balance: u64, - spec: &ChainSpec, - current_fork: ForkName, - ) -> u64 { - let max_effective_balance = self.get_max_effective_balance(spec, current_fork); - std::cmp::min(validator_balance, max_effective_balance) - } } impl Default for Validator { diff --git a/testing/ef_tests/Makefile b/testing/ef_tests/Makefile index 390711079f..d5f4997bb7 100644 --- a/testing/ef_tests/Makefile +++ b/testing/ef_tests/Makefile @@ -1,4 +1,4 @@ -TESTS_TAG := v1.5.0-alpha.6 +TESTS_TAG := v1.5.0-alpha.8 TESTS = general minimal mainnet TARBALLS = $(patsubst %,%-$(TESTS_TAG).tar.gz,$(TESTS)) diff --git a/testing/ef_tests/src/cases/epoch_processing.rs b/testing/ef_tests/src/cases/epoch_processing.rs index dfd782a22b..c1adf10770 100644 --- a/testing/ef_tests/src/cases/epoch_processing.rs +++ b/testing/ef_tests/src/cases/epoch_processing.rs @@ -86,7 +86,7 @@ type_name!(RewardsAndPenalties, "rewards_and_penalties"); type_name!(RegistryUpdates, "registry_updates"); type_name!(Slashings, "slashings"); type_name!(Eth1DataReset, "eth1_data_reset"); -type_name!(PendingBalanceDeposits, "pending_balance_deposits"); +type_name!(PendingBalanceDeposits, "pending_deposits"); type_name!(PendingConsolidations, "pending_consolidations"); type_name!(EffectiveBalanceUpdates, "effective_balance_updates"); type_name!(SlashingsReset, "slashings_reset"); @@ -193,7 +193,7 @@ impl EpochTransition for PendingBalanceDeposits { state, spec, SinglePassConfig { - pending_balance_deposits: true, + pending_deposits: true, ..SinglePassConfig::disable_all() }, ) @@ -363,7 +363,7 @@ impl> Case for EpochProcessing { } if !fork_name.electra_enabled() - && (T::name() == "pending_consolidations" || T::name() == "pending_balance_deposits") + && (T::name() == "pending_consolidations" || T::name() == "pending_deposits") { return false; } diff --git a/testing/ef_tests/src/type_name.rs b/testing/ef_tests/src/type_name.rs index a9322e5dd5..c50032a63d 100644 --- a/testing/ef_tests/src/type_name.rs +++ b/testing/ef_tests/src/type_name.rs @@ -134,7 +134,7 @@ type_name_generic!(LightClientUpdateElectra, "LightClientUpdate"); type_name_generic!(PendingAttestation); type_name!(PendingConsolidation); type_name!(PendingPartialWithdrawal); -type_name!(PendingBalanceDeposit); +type_name!(PendingDeposit); type_name!(ProposerSlashing); type_name_generic!(SignedAggregateAndProof); type_name_generic!(SignedAggregateAndProofBase, "SignedAggregateAndProof"); diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 3f802d8944..292625a371 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -243,8 +243,7 @@ mod ssz_static { use types::historical_summary::HistoricalSummary; use types::{ AttesterSlashingBase, AttesterSlashingElectra, ConsolidationRequest, DepositRequest, - LightClientBootstrapAltair, PendingBalanceDeposit, PendingPartialWithdrawal, - WithdrawalRequest, *, + LightClientBootstrapAltair, PendingDeposit, PendingPartialWithdrawal, WithdrawalRequest, *, }; ssz_static_test!(attestation_data, AttestationData); @@ -661,8 +660,8 @@ mod ssz_static { #[test] fn pending_balance_deposit() { - SszStaticHandler::::electra_and_later().run(); - SszStaticHandler::::electra_and_later().run(); + SszStaticHandler::::electra_and_later().run(); + SszStaticHandler::::electra_and_later().run(); } #[test] From 1de498340c5166e4abbc3de12dae4af6dab7c6c3 Mon Sep 17 00:00:00 2001 From: chonghe <44791194+chong-he@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:26:59 +0800 Subject: [PATCH 15/23] Add spell check and update Lighthouse book (#6627) * spellcheck config * Merge remote-tracking branch 'origin/unstable' into spellcheck * spellcheck update * update spellcheck * spell check passes * Remove ignored and add other md files * Remove some words in wordlist * CI * test spell check CI * correct spell check * Merge branch 'unstable' into spellcheck * minor fix * Merge branch 'spellcheck' of https://github.com/chong-he/lighthouse into spellcheck * Update book * mdlint * delete previous_epoch_active_gwei * Merge branch 'unstable' into spellcheck * Tweak "container runtime" wording * Try `BeaconState`s --- .github/workflows/test-suite.yml | 2 + .spellcheck.yml | 35 +++++ CONTRIBUTING.md | 2 +- README.md | 2 +- book/src/advanced_database.md | 2 +- book/src/advanced_networking.md | 6 +- book/src/api-lighthouse.md | 35 +++-- book/src/faq.md | 6 +- book/src/graffiti.md | 2 +- book/src/homebrew.md | 2 +- book/src/late-block-re-orgs.md | 19 ++- book/src/ui-faqs.md | 2 +- book/src/ui-installation.md | 2 +- book/src/validator-inclusion.md | 1 - book/src/validator-manager.md | 1 + book/src/validator-monitoring.md | 2 +- scripts/local_testnet/README.md | 8 +- wordlist.txt | 235 +++++++++++++++++++++++++++++++ 18 files changed, 331 insertions(+), 33 deletions(-) create mode 100644 .spellcheck.yml create mode 100644 wordlist.txt diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 8da46ed8ee..bba670cc22 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -363,6 +363,8 @@ jobs: run: CARGO_HOME=$(readlink -f $HOME) make vendor - name: Markdown-linter run: make mdlint + - name: Spell-check + uses: rojopolis/spellcheck-github-actions@v0 check-msrv: name: check-msrv runs-on: ubuntu-latest diff --git a/.spellcheck.yml b/.spellcheck.yml new file mode 100644 index 0000000000..692bc4d176 --- /dev/null +++ b/.spellcheck.yml @@ -0,0 +1,35 @@ +matrix: +- name: Markdown + sources: + - './book/**/*.md' + - 'README.md' + - 'CONTRIBUTING.md' + - 'SECURITY.md' + - './scripts/local_testnet/README.md' + default_encoding: utf-8 + aspell: + lang: en + dictionary: + wordlists: + - wordlist.txt + encoding: utf-8 + pipeline: + - pyspelling.filters.url: + - pyspelling.filters.markdown: + markdown_extensions: + - pymdownx.superfences: + - pymdownx.highlight: + - pymdownx.striphtml: + - pymdownx.magiclink: + - pyspelling.filters.html: + comments: false + ignores: + - code + - pre + - pyspelling.filters.context: + context_visible_first: true + delimiters: + # Ignore hex strings + - open: '0x[a-fA-F0-9]' + close: '[^a-fA-F0-9]' + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3c53558a10..4cad219c89 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,7 +85,7 @@ steps: 5. Commit your changes and push them to your fork with `$ git push origin your_feature_name`. 6. Go to your fork on github.com and use the web interface to create a pull - request into the sigp/lighthouse repo. + request into the sigp/lighthouse repository. From there, the repository maintainers will review the PR and either accept it or provide some constructive feedback. diff --git a/README.md b/README.md index 4b22087bcd..147a06e504 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Lighthouse is: - Built in [Rust](https://www.rust-lang.org), a modern language providing unique safety guarantees and excellent performance (comparable to C++). - Funded by various organisations, including Sigma Prime, the - Ethereum Foundation, ConsenSys, the Decentralization Foundation and private individuals. + Ethereum Foundation, Consensys, the Decentralization Foundation and private individuals. - Actively involved in the specification and security analysis of the Ethereum proof-of-stake consensus specification. diff --git a/book/src/advanced_database.md b/book/src/advanced_database.md index d8d6ea61a1..b558279730 100644 --- a/book/src/advanced_database.md +++ b/book/src/advanced_database.md @@ -56,7 +56,7 @@ that we have observed are: _a lot_ of space. It's even possible to push beyond that with `--hierarchy-exponents 0` which would store a full state every single slot (NOT RECOMMENDED). - **Less diff layers are not necessarily faster**. One might expect that the fewer diff layers there - are, the less work Lighthouse would have to do to reconstruct any particular state. In practise + are, the less work Lighthouse would have to do to reconstruct any particular state. In practice this seems to be offset by the increased size of diffs in each layer making the diffs take longer to apply. We observed no significant performance benefit from `--hierarchy-exponents 5,7,11`, and a substantial increase in space consumed. diff --git a/book/src/advanced_networking.md b/book/src/advanced_networking.md index 732b4f51e6..c0f6b5485e 100644 --- a/book/src/advanced_networking.md +++ b/book/src/advanced_networking.md @@ -68,7 +68,7 @@ The steps to do port forwarding depends on the router, but the general steps are 1. Determine the default gateway IP: - On Linux: open a terminal and run `ip route | grep default`, the result should look something similar to `default via 192.168.50.1 dev wlp2s0 proto dhcp metric 600`. The `192.168.50.1` is your router management default gateway IP. - - On MacOS: open a terminal and run `netstat -nr|grep default` and it should return the default gateway IP. + - On macOS: open a terminal and run `netstat -nr|grep default` and it should return the default gateway IP. - On Windows: open a command prompt and run `ipconfig` and look for the `Default Gateway` which will show you the gateway IP. The default gateway IP usually looks like 192.168.X.X. Once you obtain the IP, enter it to a web browser and it will lead you to the router management page. @@ -91,7 +91,7 @@ The steps to do port forwarding depends on the router, but the general steps are - Internal port: `9001` - IP address: Choose the device that is running Lighthouse. -1. To check that you have successfully opened the ports, go to [yougetsignal](https://www.yougetsignal.com/tools/open-ports/) and enter `9000` in the `port number`. If it shows "open", then you have successfully set up port forwarding. If it shows "closed", double check your settings, and also check that you have allowed firewall rules on port 9000. Note: this will only confirm if port 9000/TCP is open. You will need to ensure you have correctly setup port forwarding for the UDP ports (`9000` and `9001` by default). +1. To check that you have successfully opened the ports, go to [`yougetsignal`](https://www.yougetsignal.com/tools/open-ports/) and enter `9000` in the `port number`. If it shows "open", then you have successfully set up port forwarding. If it shows "closed", double check your settings, and also check that you have allowed firewall rules on port 9000. Note: this will only confirm if port 9000/TCP is open. You will need to ensure you have correctly setup port forwarding for the UDP ports (`9000` and `9001` by default). ## ENR Configuration @@ -141,7 +141,7 @@ To listen over both IPv4 and IPv6: - Set two listening addresses using the `--listen-address` flag twice ensuring the two addresses are one IPv4, and the other IPv6. When doing so, the `--port` and `--discovery-port` flags will apply exclusively to IPv4. Note - that this behaviour differs from the Ipv6 only case described above. + that this behaviour differs from the IPv6 only case described above. - If necessary, set the `--port6` flag to configure the port used for TCP and UDP over IPv6. This flag has no effect when listening over IPv6 only. - If necessary, set the `--discovery-port6` flag to configure the IPv6 UDP diff --git a/book/src/api-lighthouse.md b/book/src/api-lighthouse.md index b63505c490..5428ab8f9a 100644 --- a/book/src/api-lighthouse.md +++ b/book/src/api-lighthouse.md @@ -508,23 +508,31 @@ curl "http://localhost:5052/lighthouse/database/info" | jq ```json { - "schema_version": 18, + "schema_version": 22, "config": { - "slots_per_restore_point": 8192, - "slots_per_restore_point_set_explicitly": false, "block_cache_size": 5, + "state_cache_size": 128, + "compression_level": 1, "historic_state_cache_size": 1, + "hdiff_buffer_cache_size": 16, "compact_on_init": false, "compact_on_prune": true, "prune_payloads": true, + "hierarchy_config": { + "exponents": [ + 5, + 7, + 11 + ] + }, "prune_blobs": true, "epochs_per_blob_prune": 1, "blob_prune_margin_epochs": 0 }, "split": { - "slot": "7454656", - "state_root": "0xbecfb1c8ee209854c611ebc967daa77da25b27f1a8ef51402fdbe060587d7653", - "block_root": "0x8730e946901b0a406313d36b3363a1b7091604e1346a3410c1a7edce93239a68" + "slot": "10530592", + "state_root": "0xd27e6ce699637cf9b5c7ca632118b7ce12c2f5070bb25a27ac353ff2799d4466", + "block_root": "0x71509a1cb374773d680cd77148c73ab3563526dacb0ab837bb0c87e686962eae" }, "anchor": { "anchor_slot": "7451168", @@ -543,8 +551,19 @@ curl "http://localhost:5052/lighthouse/database/info" | jq For more information about the split point, see the [Database Configuration](./advanced_database.md) docs. -The `anchor` will be `null` unless the node has been synced with checkpoint sync and state -reconstruction has yet to be completed. For more information +For archive nodes, the `anchor` will be: + +```json +"anchor": { + "anchor_slot": "0", + "oldest_block_slot": "0", + "oldest_block_parent": "0x0000000000000000000000000000000000000000000000000000000000000000", + "state_upper_limit": "0", + "state_lower_limit": "0" + }, +``` + +indicating that all states with slots `>= 0` are available, i.e., full state history. For more information on the specific meanings of these fields see the docs on [Checkpoint Sync](./checkpoint-sync.md#reconstructing-states). diff --git a/book/src/faq.md b/book/src/faq.md index 04e5ce5bc8..d23951c8c7 100644 --- a/book/src/faq.md +++ b/book/src/faq.md @@ -92,7 +92,7 @@ If the reason for the error message is caused by no. 1 above, you may want to lo - Power outage. If power outages are an issue at your place, consider getting a UPS to avoid ungraceful shutdown of services. - The service file is not stopped properly. To overcome this, make sure that the process is stopped properly, e.g., during client updates. -- Out of memory (oom) error. This can happen when the system memory usage has reached its maximum and causes the execution engine to be killed. To confirm that the error is due to oom, run `sudo dmesg -T | grep killed` to look for killed processes. If you are using geth as the execution client, a short term solution is to reduce the resources used. For example, you can reduce the cache by adding the flag `--cache 2048`. If the oom occurs rather frequently, a long term solution is to increase the memory capacity of the computer. +- Out of memory (oom) error. This can happen when the system memory usage has reached its maximum and causes the execution engine to be killed. To confirm that the error is due to oom, run `sudo dmesg -T | grep killed` to look for killed processes. If you are using Geth as the execution client, a short term solution is to reduce the resources used. For example, you can reduce the cache by adding the flag `--cache 2048`. If the oom occurs rather frequently, a long term solution is to increase the memory capacity of the computer. ### I see beacon logs showing `Error during execution engine upcheck`, what should I do? @@ -302,7 +302,7 @@ An example of the log: (debug logs can be found under `$datadir/beacon/logs`): Delayed head block, set_as_head_time_ms: 27, imported_time_ms: 168, attestable_delay_ms: 4209, available_delay_ms: 4186, execution_time_ms: 201, blob_delay_ms: 3815, observed_delay_ms: 3984, total_delay_ms: 4381, slot: 1886014, proposer_index: 733, block_root: 0xa7390baac88d50f1cbb5ad81691915f6402385a12521a670bbbd4cd5f8bf3934, service: beacon, module: beacon_chain::canonical_head:1441 ``` -The field to look for is `attestable_delay`, which defines the time when a block is ready for the validator to attest. If the `attestable_delay` is greater than 4s which has past the window of attestation, the attestation wil fail. In the above example, the delay is mostly caused by late block observed by the node, as shown in `observed_delay`. The `observed_delay` is determined mostly by the proposer and partly by your networking setup (e.g., how long it took for the node to receive the block). Ideally, `observed_delay` should be less than 3 seconds. In this example, the validator failed to attest the block due to the block arriving late. +The field to look for is `attestable_delay`, which defines the time when a block is ready for the validator to attest. If the `attestable_delay` is greater than 4s which has past the window of attestation, the attestation will fail. In the above example, the delay is mostly caused by late block observed by the node, as shown in `observed_delay`. The `observed_delay` is determined mostly by the proposer and partly by your networking setup (e.g., how long it took for the node to receive the block). Ideally, `observed_delay` should be less than 3 seconds. In this example, the validator failed to attest the block due to the block arriving late. Another example of log: @@ -315,7 +315,7 @@ In this example, we see that the `execution_time_ms` is 4694ms. The `execution_t ### Sometimes I miss the attestation head vote, resulting in penalty. Is this normal? -In general, it is unavoidable to have some penalties occasionally. This is particularly the case when you are assigned to attest on the first slot of an epoch and if the proposer of that slot releases the block late, then you will get penalised for missing the target and head votes. Your attestation performance does not only depend on your own setup, but also on everyone elses performance. +In general, it is unavoidable to have some penalties occasionally. This is particularly the case when you are assigned to attest on the first slot of an epoch and if the proposer of that slot releases the block late, then you will get penalised for missing the target and head votes. Your attestation performance does not only depend on your own setup, but also on everyone else's performance. You could also check for the sync aggregate participation percentage on block explorers such as [beaconcha.in](https://beaconcha.in/). A low sync aggregate participation percentage (e.g., 60-70%) indicates that the block that you are assigned to attest to may be published late. As a result, your validator fails to correctly attest to the block. diff --git a/book/src/graffiti.md b/book/src/graffiti.md index ba9c7d05d7..7b402ea866 100644 --- a/book/src/graffiti.md +++ b/book/src/graffiti.md @@ -4,7 +4,7 @@ Lighthouse provides four options for setting validator graffiti. ## 1. Using the "--graffiti-file" flag on the validator client -Users can specify a file with the `--graffiti-file` flag. This option is useful for dynamically changing graffitis for various use cases (e.g. drawing on the beaconcha.in graffiti wall). This file is loaded once on startup and reloaded everytime a validator is chosen to propose a block. +Users can specify a file with the `--graffiti-file` flag. This option is useful for dynamically changing graffitis for various use cases (e.g. drawing on the beaconcha.in graffiti wall). This file is loaded once on startup and reloaded every time a validator is chosen to propose a block. Usage: `lighthouse vc --graffiti-file graffiti_file.txt` diff --git a/book/src/homebrew.md b/book/src/homebrew.md index da92dcb26c..f94764889e 100644 --- a/book/src/homebrew.md +++ b/book/src/homebrew.md @@ -31,6 +31,6 @@ Alternatively, you can find the `lighthouse` binary at: The [formula][] is kept up-to-date by the Homebrew community and a bot that lists for new releases. -The package source can be found in the [homebrew-core](https://github.com/Homebrew/homebrew-core/blob/master/Formula/l/lighthouse.rb) repo. +The package source can be found in the [homebrew-core](https://github.com/Homebrew/homebrew-core/blob/master/Formula/l/lighthouse.rb) repository. [formula]: https://formulae.brew.sh/formula/lighthouse diff --git a/book/src/late-block-re-orgs.md b/book/src/late-block-re-orgs.md index 4a00f33aa4..fca156bda3 100644 --- a/book/src/late-block-re-orgs.md +++ b/book/src/late-block-re-orgs.md @@ -46,24 +46,31 @@ You can track the reasons for re-orgs being attempted (or not) via Lighthouse's A pair of messages at `INFO` level will be logged if a re-org opportunity is detected: -> INFO Attempting re-org due to weak head threshold_weight: 45455983852725, head_weight: 0, parent: 0x09d953b69041f280758400c671130d174113bbf57c2d26553a77fb514cad4890, weak_head: 0xf64f8e5ed617dc18c1e759dab5d008369767c3678416dac2fe1d389562842b49 - -> INFO Proposing block to re-org current head head_to_reorg: 0xf64f…2b49, slot: 1105320 +```text +INFO Attempting re-org due to weak head threshold_weight: 45455983852725, head_weight: 0, parent: 0x09d953b69041f280758400c671130d174113bbf57c2d26553a77fb514cad4890, weak_head: 0xf64f8e5ed617dc18c1e759dab5d008369767c3678416dac2fe1d389562842b49 +INFO Proposing block to re-org current head head_to_reorg: 0xf64f…2b49, slot: 1105320 +``` This should be followed shortly after by a `INFO` log indicating that a re-org occurred. This is expected and normal: -> INFO Beacon chain re-org reorg_distance: 1, new_slot: 1105320, new_head: 0x72791549e4ca792f91053bc7cf1e55c6fbe745f78ce7a16fc3acb6f09161becd, previous_slot: 1105319, previous_head: 0xf64f8e5ed617dc18c1e759dab5d008369767c3678416dac2fe1d389562842b49 +```text +INFO Beacon chain re-org reorg_distance: 1, new_slot: 1105320, new_head: 0x72791549e4ca792f91053bc7cf1e55c6fbe745f78ce7a16fc3acb6f09161becd, previous_slot: 1105319, previous_head: 0xf64f8e5ed617dc18c1e759dab5d008369767c3678416dac2fe1d389562842b49 +``` In case a re-org is not viable (which should be most of the time), Lighthouse will just propose a block as normal and log the reason the re-org was not attempted at debug level: -> DEBG Not attempting re-org reason: head not late +```text +DEBG Not attempting re-org reason: head not late +``` If you are interested in digging into the timing of `forkchoiceUpdated` messages sent to the execution layer, there is also a debug log for the suppression of `forkchoiceUpdated` messages when Lighthouse thinks that a re-org is likely: -> DEBG Fork choice update overridden slot: 1105320, override: 0x09d953b69041f280758400c671130d174113bbf57c2d26553a77fb514cad4890, canonical_head: 0xf64f8e5ed617dc18c1e759dab5d008369767c3678416dac2fe1d389562842b49 +```text +DEBG Fork choice update overridden slot: 1105320, override: 0x09d953b69041f280758400c671130d174113bbf57c2d26553a77fb514cad4890, canonical_head: 0xf64f8e5ed617dc18c1e759dab5d008369767c3678416dac2fe1d389562842b49 +``` [the spec]: https://github.com/ethereum/consensus-specs/pull/3034 diff --git a/book/src/ui-faqs.md b/book/src/ui-faqs.md index efa6d3d4ab..0887875316 100644 --- a/book/src/ui-faqs.md +++ b/book/src/ui-faqs.md @@ -6,7 +6,7 @@ Yes, the most current Siren version requires Lighthouse v4.3.0 or higher to func ## 2. Where can I find my API token? -The required Api token may be found in the default data directory of the validator client. For more information please refer to the lighthouse ui configuration [`api token section`](./api-vc-auth-header.md). +The required API token may be found in the default data directory of the validator client. For more information please refer to the lighthouse ui configuration [`api token section`](./api-vc-auth-header.md). ## 3. How do I fix the Node Network Errors? diff --git a/book/src/ui-installation.md b/book/src/ui-installation.md index 1444c0d633..9cd84e5160 100644 --- a/book/src/ui-installation.md +++ b/book/src/ui-installation.md @@ -1,6 +1,6 @@ # 📦 Installation -Siren supports any operating system that supports container runtimes and/or NodeJS 18, this includes Linux, MacOS, and Windows. The recommended way of running Siren is by launching the [docker container](https://hub.docker.com/r/sigp/siren) , but running the application directly is also possible. +Siren supports any operating system that supports containers and/or NodeJS 18, this includes Linux, macOS, and Windows. The recommended way of running Siren is by launching the [docker container](https://hub.docker.com/r/sigp/siren) , but running the application directly is also possible. ## Version Requirement diff --git a/book/src/validator-inclusion.md b/book/src/validator-inclusion.md index 092c813a1e..eef563dcdb 100644 --- a/book/src/validator-inclusion.md +++ b/book/src/validator-inclusion.md @@ -56,7 +56,6 @@ The following fields are returned: able to vote) during the current epoch. - `current_epoch_target_attesting_gwei`: the total staked gwei that attested to the majority-elected Casper FFG target epoch during the current epoch. -- `previous_epoch_active_gwei`: as per `current_epoch_active_gwei`, but during the previous epoch. - `previous_epoch_target_attesting_gwei`: see `current_epoch_target_attesting_gwei`. - `previous_epoch_head_attesting_gwei`: the total staked gwei that attested to a head beacon block that is in the canonical chain. diff --git a/book/src/validator-manager.md b/book/src/validator-manager.md index a71fab1e3a..11df2af037 100644 --- a/book/src/validator-manager.md +++ b/book/src/validator-manager.md @@ -32,3 +32,4 @@ The `validator-manager` boasts the following features: - [Creating and importing validators using the `create` and `import` commands.](./validator-manager-create.md) - [Moving validators between two VCs using the `move` command.](./validator-manager-move.md) +- [Managing validators such as delete, import and list validators.](./validator-manager-api.md) diff --git a/book/src/validator-monitoring.md b/book/src/validator-monitoring.md index 6439ea83a3..bbc95460ec 100644 --- a/book/src/validator-monitoring.md +++ b/book/src/validator-monitoring.md @@ -134,7 +134,7 @@ validator_monitor_attestation_simulator_source_attester_hit_total validator_monitor_attestation_simulator_source_attester_miss_total ``` -A grafana dashboard to view the metrics for attestation simulator is available [here](https://github.com/sigp/lighthouse-metrics/blob/master/dashboards/AttestationSimulator.json). +A Grafana dashboard to view the metrics for attestation simulator is available [here](https://github.com/sigp/lighthouse-metrics/blob/master/dashboards/AttestationSimulator.json). The attestation simulator provides an insight into the attestation performance of a beacon node. It can be used as an indication of how expediently the beacon node has completed importing blocks within the 4s time frame for an attestation to be made. diff --git a/scripts/local_testnet/README.md b/scripts/local_testnet/README.md index ca701eb7e9..159c89badb 100644 --- a/scripts/local_testnet/README.md +++ b/scripts/local_testnet/README.md @@ -1,6 +1,6 @@ # Simple Local Testnet -These scripts allow for running a small local testnet with a default of 4 beacon nodes, 4 validator clients and 4 geth execution clients using Kurtosis. +These scripts allow for running a small local testnet with a default of 4 beacon nodes, 4 validator clients and 4 Geth execution clients using Kurtosis. This setup can be useful for testing and development. ## Installation @@ -9,7 +9,7 @@ This setup can be useful for testing and development. 1. Install [Kurtosis](https://docs.kurtosis.com/install/). Verify that Kurtosis has been successfully installed by running `kurtosis version` which should display the version. -1. Install [yq](https://github.com/mikefarah/yq). If you are on Ubuntu, you can install `yq` by running `snap install yq`. +1. Install [`yq`](https://github.com/mikefarah/yq). If you are on Ubuntu, you can install `yq` by running `snap install yq`. ## Starting the testnet @@ -22,7 +22,7 @@ cd ./scripts/local_testnet It will build a Lighthouse docker image from the root of the directory and will take an approximately 12 minutes to complete. Once built, the testing will be started automatically. You will see a list of services running and "Started!" at the end. You can also select your own Lighthouse docker image to use by specifying it in `network_params.yml` under the `cl_image` key. -Full configuration reference for kurtosis is specified [here](https://github.com/ethpandaops/ethereum-package?tab=readme-ov-file#configuration). +Full configuration reference for Kurtosis is specified [here](https://github.com/ethpandaops/ethereum-package?tab=readme-ov-file#configuration). To view all running services: @@ -36,7 +36,7 @@ To view the logs: kurtosis service logs local-testnet $SERVICE_NAME ``` -where `$SERVICE_NAME` is obtained by inspecting the running services above. For example, to view the logs of the first beacon node, validator client and geth: +where `$SERVICE_NAME` is obtained by inspecting the running services above. For example, to view the logs of the first beacon node, validator client and Geth: ```bash kurtosis service logs local-testnet -f cl-1-lighthouse-geth diff --git a/wordlist.txt b/wordlist.txt new file mode 100644 index 0000000000..f06c278866 --- /dev/null +++ b/wordlist.txt @@ -0,0 +1,235 @@ +APIs +ARMv +AUR +Backends +Backfilling +Beaconcha +Besu +Broadwell +BIP +BLS +BN +BNs +BTC +BTEC +Casper +CentOS +Chiado +CMake +CoinCashew +Consensys +CORS +CPUs +DBs +DES +DHT +DNS +Dockerhub +DoS +EIP +ENR +Erigon +Esat's +ETH +EthDocker +Ethereum +Ethstaker +Exercism +Extractable +FFG +Geth +Gitcoin +Gnosis +Goerli +Grafana +Holesky +Homebrew +Infura +IPs +IPv +JSON +KeyManager +Kurtosis +LMDB +LLVM +LRU +LTO +Mainnet +MDBX +Merkle +MEV +MSRV +NAT's +Nethermind +NodeJS +NullLogger +PathBuf +PowerShell +PPA +Pre +Proto +PRs +Prysm +QUIC +RasPi +README +RESTful +Reth +RHEL +Ropsten +RPC +Ryzen +Sepolia +Somer +SSD +SSL +SSZ +Styleguide +TCP +Teku +TLS +TODOs +UDP +UI +UPnP +USD +UX +Validator +VC +VCs +VPN +Withdrawable +WSL +YAML +aarch +anonymize +api +attester +backend +backends +backfill +backfilling +beaconcha +bitfield +blockchain +bn +cli +clippy +config +cpu +cryptocurrencies +cryptographic +danksharding +datadir +datadirs +de +decrypt +decrypted +dest +dir +disincentivise +doppelgänger +dropdown +else's +env +eth +ethdo +ethereum +ethstaker +filesystem +frontend +gapped +github +graffitis +gwei +hdiffs +homebrew +hostname +html +http +https +hDiff +implementers +interoperable +io +iowait +jemalloc +json +jwt +kb +keymanager +keypair +keypairs +keystore +keystores +linter +linux +localhost +lossy +macOS +mainnet +makefile +mdBook +mev +misconfiguration +mkcert +namespace +natively +nd +ness +nginx +nitty +oom +orging +orgs +os +paul +pem +performant +pid +pre +pubkey +pubkeys +rc +reimport +resync +roadmap +runtime +rustfmt +rustup +schemas +sigmaprime +sigp +slashable +slashings +spec'd +src +stakers +subnet +subnets +systemd +testnet +testnets +th +toml +topologies +tradeoffs +transactional +tweakers +ui +unadvanced +unaggregated +unencrypted +unfinalized +untrusted +uptimes +url +validator +validators +validator's +vc +virt +webapp +withdrawable +yaml +yml From 1315c94adbc929df39c4ebbd17d627129903e3b6 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Wed, 18 Dec 2024 07:10:53 +1100 Subject: [PATCH 16/23] Unsaturate dial negotiation queue (#6711) * Unsaturate dial-negotiation count --- beacon_node/lighthouse_network/src/rpc/handler.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/beacon_node/lighthouse_network/src/rpc/handler.rs b/beacon_node/lighthouse_network/src/rpc/handler.rs index e76d6d2786..0a0a6ca754 100644 --- a/beacon_node/lighthouse_network/src/rpc/handler.rs +++ b/beacon_node/lighthouse_network/src/rpc/handler.rs @@ -964,6 +964,9 @@ where request_info: (Id, RequestType), error: StreamUpgradeError, ) { + // This dialing is now considered failed + self.dial_negotiated -= 1; + let (id, req) = request_info; // map the error @@ -989,9 +992,6 @@ where StreamUpgradeError::Apply(other) => other, }; - // This dialing is now considered failed - self.dial_negotiated -= 1; - self.outbound_io_error_retries = 0; self.events_out .push(HandlerEvent::Err(HandlerErr::Outbound { From 2662dc7f8fba71a5682f8906a6f6a71374757c23 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Wed, 18 Dec 2024 05:35:58 +0530 Subject: [PATCH 17/23] Fix Sse client api (#6685) * Use reqwest eventsource for get_events api * await for Event::Open before returning stream * fmt * Merge branch 'unstable' into sse-client-fix * Ignore lint --- Cargo.lock | 28 ++++++++++++ beacon_node/beacon_chain/tests/store_tests.rs | 1 + common/eth2/Cargo.toml | 1 + common/eth2/src/lib.rs | 43 ++++++++++++++----- common/eth2/src/types.rs | 21 +-------- 5 files changed, 65 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2978a3a19f..c62e9fbc87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2576,6 +2576,7 @@ dependencies = [ "proto_array", "psutil", "reqwest", + "reqwest-eventsource", "sensitive_url", "serde", "serde_json", @@ -2977,6 +2978,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "eventsource-stream" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" +dependencies = [ + "futures-core", + "nom", + "pin-project-lite", +] + [[package]] name = "execution_engine_integration" version = "0.1.0" @@ -7179,6 +7191,22 @@ dependencies = [ "winreg", ] +[[package]] +name = "reqwest-eventsource" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f529a5ff327743addc322af460761dff5b50e0c826b9e6ac44c3195c50bb2026" +dependencies = [ + "eventsource-stream", + "futures-core", + "futures-timer", + "mime", + "nom", + "pin-project-lite", + "reqwest", + "thiserror 1.0.69", +] + [[package]] name = "resolv-conf" version = "0.7.0" diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 73805a8525..e1258ccdea 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -2796,6 +2796,7 @@ async fn finalizes_after_resuming_from_db() { ); } +#[allow(clippy::large_stack_frames)] #[tokio::test] async fn revert_minority_fork_on_resume() { let validator_count = 16; diff --git a/common/eth2/Cargo.toml b/common/eth2/Cargo.toml index f735b4c688..912051da36 100644 --- a/common/eth2/Cargo.toml +++ b/common/eth2/Cargo.toml @@ -27,6 +27,7 @@ slashing_protection = { workspace = true } mediatype = "0.19.13" pretty_reqwest_error = { workspace = true } derivative = { workspace = true } +reqwest-eventsource = "0.5.0" [dev-dependencies] tokio = { workspace = true } diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 522c6414ea..12b1538984 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -27,6 +27,7 @@ use reqwest::{ Body, IntoUrl, RequestBuilder, Response, }; pub use reqwest::{StatusCode, Url}; +use reqwest_eventsource::{Event, EventSource}; pub use sensitive_url::{SensitiveError, SensitiveUrl}; use serde::{de::DeserializeOwned, Serialize}; use ssz::Encode; @@ -52,6 +53,8 @@ pub const SSZ_CONTENT_TYPE_HEADER: &str = "application/octet-stream"; pub enum Error { /// The `reqwest` client raised an error. HttpClient(PrettyReqwestError), + /// The `reqwest_eventsource` client raised an error. + SseClient(reqwest_eventsource::Error), /// The server returned an error message where the body was able to be parsed. ServerMessage(ErrorMessage), /// The server returned an error message with an array of errors. @@ -93,6 +96,13 @@ impl Error { pub fn status(&self) -> Option { match self { Error::HttpClient(error) => error.inner().status(), + Error::SseClient(error) => { + if let reqwest_eventsource::Error::InvalidStatusCode(status, _) = error { + Some(*status) + } else { + None + } + } Error::ServerMessage(msg) => StatusCode::try_from(msg.code).ok(), Error::ServerIndexedMessage(msg) => StatusCode::try_from(msg.code).ok(), Error::StatusCode(status) => Some(*status), @@ -2592,16 +2602,29 @@ impl BeaconNodeHttpClient { .join(","); path.query_pairs_mut().append_pair("topics", &topic_string); - Ok(self - .client - .get(path) - .send() - .await? - .bytes_stream() - .map(|next| match next { - Ok(bytes) => EventKind::from_sse_bytes(bytes.as_ref()), - Err(e) => Err(Error::HttpClient(e.into())), - })) + let mut es = EventSource::get(path); + // If we don't await `Event::Open` here, then the consumer + // will not get any Message events until they start awaiting the stream. + // This is a way to register the stream with the sse server before + // message events start getting emitted. + while let Some(event) = es.next().await { + match event { + Ok(Event::Open) => break, + Err(err) => return Err(Error::SseClient(err)), + // This should never happen as we are guaranteed to get the + // Open event before any message starts coming through. + Ok(Event::Message(_)) => continue, + } + } + Ok(Box::pin(es.filter_map(|event| async move { + match event { + Ok(Event::Open) => None, + Ok(Event::Message(message)) => { + Some(EventKind::from_sse_bytes(&message.event, &message.data)) + } + Err(err) => Some(Err(Error::SseClient(err))), + } + }))) } /// `POST validator/duties/sync/{epoch}` diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index c187399ebd..a303953a86 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -13,7 +13,7 @@ use serde_json::Value; use ssz::{Decode, DecodeError}; use ssz_derive::{Decode, Encode}; use std::fmt::{self, Display}; -use std::str::{from_utf8, FromStr}; +use std::str::FromStr; use std::sync::Arc; use std::time::Duration; use types::beacon_block_body::KzgCommitments; @@ -1153,24 +1153,7 @@ impl EventKind { } } - pub fn from_sse_bytes(message: &[u8]) -> Result { - let s = from_utf8(message) - .map_err(|e| ServerError::InvalidServerSentEvent(format!("{:?}", e)))?; - - let mut split = s.split('\n'); - let event = split - .next() - .ok_or_else(|| { - ServerError::InvalidServerSentEvent("Could not parse event tag".to_string()) - })? - .trim_start_matches("event:"); - let data = split - .next() - .ok_or_else(|| { - ServerError::InvalidServerSentEvent("Could not parse data tag".to_string()) - })? - .trim_start_matches("data:"); - + pub fn from_sse_bytes(event: &str, data: &str) -> Result { match event { "attestation" => Ok(EventKind::Attestation(serde_json::from_str(data).map_err( |e| ServerError::InvalidServerSentEvent(format!("Attestation: {:?}", e)), From 10c96f8631d7db0d875dd60f1b9828712b96d01a Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 19 Dec 2024 16:45:59 +1100 Subject: [PATCH 18/23] Fix anvil 404 link in docs (#6724) * Fix anvil 404 link in docs --- testing/eth1_test_rig/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/eth1_test_rig/src/lib.rs b/testing/eth1_test_rig/src/lib.rs index 015a632ff4..3cba908261 100644 --- a/testing/eth1_test_rig/src/lib.rs +++ b/testing/eth1_test_rig/src/lib.rs @@ -1,6 +1,6 @@ //! Provides utilities for deploying and manipulating the eth2 deposit contract on the eth1 chain. //! -//! Presently used with [`anvil`](https://github.com/foundry-rs/foundry/tree/master/anvil) to simulate +//! Presently used with [`anvil`](https://github.com/foundry-rs/foundry/tree/master/crates/anvil) to simulate //! the deposit contract for testing beacon node eth1 integration. //! //! Not tested to work with actual clients (e.g., geth). It should work fine, however there may be From b2b1faad4e32e710eab905d6d227543c44335986 Mon Sep 17 00:00:00 2001 From: Mac L Date: Thu, 19 Dec 2024 09:46:03 +0400 Subject: [PATCH 19/23] Enforce alphabetically ordered cargo deps (#6678) * Enforce alphabetically ordered cargo deps * Fix test-suite * Another CI fix * Merge branch 'unstable' into cargo-sort * Fix conflicts * Merge remote-tracking branch 'origin/unstable' into cargo-sort --- .github/workflows/test-suite.yml | 16 ++++ Cargo.toml | 49 ++++++++---- account_manager/Cargo.toml | 22 +++--- beacon_node/Cargo.toml | 36 ++++----- beacon_node/beacon_chain/Cargo.toml | 4 +- beacon_node/beacon_processor/Cargo.toml | 24 +++--- beacon_node/builder_client/Cargo.toml | 4 +- beacon_node/client/Cargo.toml | 48 +++++------ beacon_node/eth1/Cargo.toml | 28 +++---- beacon_node/execution_layer/Cargo.toml | 79 +++++++++---------- beacon_node/http_api/Cargo.toml | 66 ++++++++-------- beacon_node/http_metrics/Cargo.toml | 21 +++-- beacon_node/lighthouse_network/Cargo.toml | 66 ++++++++-------- .../lighthouse_network/gossipsub/Cargo.toml | 4 +- beacon_node/network/Cargo.toml | 64 +++++++-------- beacon_node/store/Cargo.toml | 32 ++++---- beacon_node/timer/Cargo.toml | 4 +- boot_node/Cargo.toml | 18 ++--- common/account_utils/Cargo.toml | 13 ++- common/clap_utils/Cargo.toml | 3 +- common/compare_fields_derive/Cargo.toml | 2 +- common/deposit_contract/Cargo.toml | 6 +- common/directory/Cargo.toml | 1 - common/eth2/Cargo.toml | 29 ++++--- common/eth2_config/Cargo.toml | 2 +- common/eth2_interop_keypairs/Cargo.toml | 7 +- common/eth2_network_config/Cargo.toml | 26 +++--- common/eth2_wallet_manager/Cargo.toml | 1 - common/lighthouse_version/Cargo.toml | 1 - common/logging/Cargo.toml | 2 +- common/malloc_utils/Cargo.toml | 2 +- common/monitoring_api/Cargo.toml | 15 ++-- common/oneshot_broadcast/Cargo.toml | 1 - common/pretty_reqwest_error/Cargo.toml | 1 - common/sensitive_url/Cargo.toml | 3 +- common/slot_clock/Cargo.toml | 2 +- common/system_health/Cargo.toml | 6 +- common/task_executor/Cargo.toml | 8 +- common/test_random_derive/Cargo.toml | 2 +- common/unused_port/Cargo.toml | 1 - common/validator_dir/Cargo.toml | 13 ++- common/warp_utils/Cargo.toml | 19 +++-- consensus/fixed_bytes/Cargo.toml | 1 - consensus/fork_choice/Cargo.toml | 7 +- consensus/int_to_bytes/Cargo.toml | 2 +- consensus/proto_array/Cargo.toml | 4 +- consensus/safe_arith/Cargo.toml | 1 - consensus/state_processing/Cargo.toml | 26 +++--- crypto/bls/Cargo.toml | 18 ++--- crypto/eth2_key_derivation/Cargo.toml | 7 +- crypto/eth2_keystore/Cargo.toml | 19 +++-- crypto/eth2_wallet/Cargo.toml | 9 +-- crypto/kzg/Cargo.toml | 13 ++- database_manager/Cargo.toml | 8 +- lcli/Cargo.toml | 46 +++++------ lighthouse/Cargo.toml | 53 ++++++------- lighthouse/environment/Cargo.toml | 18 ++--- slasher/Cargo.toml | 30 +++---- testing/ef_tests/Cargo.toml | 22 +++--- testing/eth1_test_rig/Cargo.toml | 12 +-- .../execution_engine_integration/Cargo.toml | 22 +++--- testing/node_test_rig/Cargo.toml | 14 ++-- testing/simulator/Cargo.toml | 19 +++-- testing/state_transition_vectors/Cargo.toml | 9 +-- testing/test-test_logger/Cargo.toml | 1 - testing/web3signer_tests/Cargo.toml | 33 ++++---- validator_client/Cargo.toml | 10 +-- .../doppelganger_service/Cargo.toml | 2 +- validator_client/graffiti_file/Cargo.toml | 6 +- validator_client/http_api/Cargo.toml | 20 ++--- validator_client/http_metrics/Cargo.toml | 12 +-- .../initialized_validators/Cargo.toml | 18 ++--- validator_client/signing_method/Cargo.toml | 4 +- .../slashing_protection/Cargo.toml | 16 ++-- .../validator_services/Cargo.toml | 10 +-- validator_manager/Cargo.toml | 25 +++--- watch/Cargo.toml | 41 +++++----- 77 files changed, 655 insertions(+), 654 deletions(-) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index bba670cc22..65663e0cf4 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -428,6 +428,21 @@ jobs: cache-target: release - name: Run Makefile to trigger the bash script run: make cli-local + cargo-sort: + name: cargo-sort + needs: [check-labels] + if: needs.check-labels.outputs.skip_ci != 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: + channel: stable + cache-target: release + bins: cargo-sort + - name: Run cargo sort to check if Cargo.toml files are sorted + run: cargo sort --check --workspace # This job succeeds ONLY IF all others succeed. It is used by the merge queue to determine whether # a PR is safe to merge. New jobs should be added here. test-suite-success: @@ -455,6 +470,7 @@ jobs: 'compile-with-beta-compiler', 'cli-check', 'lockbud', + 'cargo-sort', ] steps: - uses: actions/checkout@v4 diff --git a/Cargo.toml b/Cargo.toml index 9e921190b8..23e52a306b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,11 +8,11 @@ members = [ "beacon_node/builder_client", "beacon_node/client", "beacon_node/eth1", - "beacon_node/lighthouse_network", - "beacon_node/lighthouse_network/gossipsub", "beacon_node/execution_layer", "beacon_node/http_api", "beacon_node/http_metrics", + "beacon_node/lighthouse_network", + "beacon_node/lighthouse_network/gossipsub", "beacon_node/network", "beacon_node/store", "beacon_node/timer", @@ -30,40 +30,40 @@ members = [ "common/eth2_interop_keypairs", "common/eth2_network_config", "common/eth2_wallet_manager", - "common/metrics", "common/lighthouse_version", "common/lockfile", "common/logging", "common/lru_cache", "common/malloc_utils", + "common/metrics", + "common/monitoring_api", "common/oneshot_broadcast", "common/pretty_reqwest_error", "common/sensitive_url", "common/slot_clock", "common/system_health", - "common/task_executor", "common/target_check", + "common/task_executor", "common/test_random_derive", "common/unused_port", "common/validator_dir", "common/warp_utils", - "common/monitoring_api", - - "database_manager", - - "consensus/int_to_bytes", "consensus/fixed_bytes", "consensus/fork_choice", + + "consensus/int_to_bytes", "consensus/proto_array", "consensus/safe_arith", "consensus/state_processing", "consensus/swap_or_not_shuffle", "crypto/bls", - "crypto/kzg", "crypto/eth2_key_derivation", "crypto/eth2_keystore", "crypto/eth2_wallet", + "crypto/kzg", + + "database_manager", "lcli", @@ -78,8 +78,8 @@ members = [ "testing/execution_engine_integration", "testing/node_test_rig", "testing/simulator", - "testing/test-test_logger", "testing/state_transition_vectors", + "testing/test-test_logger", "testing/web3signer_tests", "validator_client", @@ -126,8 +126,8 @@ delay_map = "0.4" derivative = "2" dirs = "3" either = "1.9" - # TODO: rust_eth_kzg is pinned for now while a perf regression is investigated - # The crate_crypto_* dependencies can be removed from this file completely once we update +# TODO: rust_eth_kzg is pinned for now while a perf regression is investigated +# The crate_crypto_* dependencies can be removed from this file completely once we update rust_eth_kzg = "=0.5.1" crate_crypto_internal_eth_kzg_bls12_381 = "=0.5.1" crate_crypto_internal_eth_kzg_erasure_codes = "=0.5.1" @@ -167,7 +167,13 @@ r2d2 = "0.8" rand = "0.8" rayon = "1.7" regex = "1" -reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "stream", "rustls-tls", "native-tls-vendored"] } +reqwest = { version = "0.11", default-features = false, features = [ + "blocking", + "json", + "stream", + "rustls-tls", + "native-tls-vendored", +] } ring = "0.16" rpds = "0.11" rusqlite = { version = "0.28", features = ["bundled"] } @@ -176,7 +182,11 @@ serde_json = "1" serde_repr = "0.1" serde_yaml = "0.9" sha2 = "0.9" -slog = { version = "2", features = ["max_level_debug", "release_max_level_debug", "nested-values"] } +slog = { version = "2", features = [ + "max_level_debug", + "release_max_level_debug", + "nested-values", +] } slog-async = "2" slog-term = "2" sloggers = { version = "2", features = ["json"] } @@ -188,7 +198,12 @@ superstruct = "0.8" syn = "1" sysinfo = "0.26" tempfile = "3" -tokio = { version = "1", features = ["rt-multi-thread", "sync", "signal", "macros"] } +tokio = { version = "1", features = [ + "rt-multi-thread", + "sync", + "signal", + "macros", +] } tokio-stream = { version = "0.1", features = ["sync"] } tokio-util = { version = "0.7", features = ["codec", "compat", "time"] } tracing = "0.1.40" @@ -267,7 +282,7 @@ validator_dir = { path = "common/validator_dir" } validator_http_api = { path = "validator_client/http_api" } validator_http_metrics = { path = "validator_client/http_metrics" } validator_metrics = { path = "validator_client/validator_metrics" } -validator_store= { path = "validator_client/validator_store" } +validator_store = { path = "validator_client/validator_store" } warp_utils = { path = "common/warp_utils" } xdelta3 = { git = "http://github.com/sigp/xdelta3-rs", rev = "50d63cdf1878e5cf3538e9aae5eed34a22c64e4a" } zstd = "0.13" diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index 48230bb281..a7752d621f 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -8,25 +8,25 @@ authors = [ edition = { workspace = true } [dependencies] +account_utils = { workspace = true } bls = { workspace = true } clap = { workspace = true } -types = { workspace = true } -environment = { workspace = true } -eth2_network_config = { workspace = true } clap_utils = { workspace = true } directory = { workspace = true } +environment = { workspace = true } +eth2 = { workspace = true } +eth2_keystore = { workspace = true } +eth2_network_config = { workspace = true } eth2_wallet = { workspace = true } eth2_wallet_manager = { path = "../common/eth2_wallet_manager" } -validator_dir = { workspace = true } -tokio = { workspace = true } -eth2_keystore = { workspace = true } -account_utils = { workspace = true } -slashing_protection = { workspace = true } -eth2 = { workspace = true } -safe_arith = { workspace = true } -slot_clock = { workspace = true } filesystem = { workspace = true } +safe_arith = { workspace = true } sensitive_url = { workspace = true } +slashing_protection = { workspace = true } +slot_clock = { workspace = true } +tokio = { workspace = true } +types = { workspace = true } +validator_dir = { workspace = true } zeroize = { workspace = true } [dev-dependencies] diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index 15cdf15dc5..7da65ad742 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -20,28 +20,28 @@ write_ssz_files = [ ] # Writes debugging .ssz files to /tmp during block processing. [dependencies] -eth2_config = { workspace = true } +account_utils = { workspace = true } beacon_chain = { workspace = true } -types = { workspace = true } -store = { workspace = true } -client = { path = "client" } clap = { workspace = true } -slog = { workspace = true } -dirs = { workspace = true } -directory = { workspace = true } -environment = { workspace = true } -task_executor = { workspace = true } -genesis = { workspace = true } -execution_layer = { workspace = true } -lighthouse_network = { workspace = true } -serde_json = { workspace = true } clap_utils = { workspace = true } -hyper = { workspace = true } +client = { path = "client" } +directory = { workspace = true } +dirs = { workspace = true } +environment = { workspace = true } +eth2_config = { workspace = true } +execution_layer = { workspace = true } +genesis = { workspace = true } hex = { workspace = true } -slasher = { workspace = true } +http_api = { workspace = true } +hyper = { workspace = true } +lighthouse_network = { workspace = true } monitoring_api = { workspace = true } sensitive_url = { workspace = true } -http_api = { workspace = true } -unused_port = { workspace = true } +serde_json = { workspace = true } +slasher = { workspace = true } +slog = { workspace = true } +store = { workspace = true } strum = { workspace = true } -account_utils = { workspace = true } +task_executor = { workspace = true } +types = { workspace = true } +unused_port = { workspace = true } diff --git a/beacon_node/beacon_chain/Cargo.toml b/beacon_node/beacon_chain/Cargo.toml index b0fa013180..7b725d3519 100644 --- a/beacon_node/beacon_chain/Cargo.toml +++ b/beacon_node/beacon_chain/Cargo.toml @@ -18,9 +18,9 @@ portable = ["bls/supranational-portable"] test_backfill = [] [dev-dependencies] +criterion = { workspace = true } maplit = { workspace = true } serde_json = { workspace = true } -criterion = { workspace = true } [dependencies] alloy-primitives = { workspace = true } @@ -42,11 +42,11 @@ hex = { workspace = true } int_to_bytes = { workspace = true } itertools = { workspace = true } kzg = { workspace = true } -metrics = { workspace = true } lighthouse_version = { workspace = true } logging = { workspace = true } lru = { workspace = true } merkle_proof = { workspace = true } +metrics = { workspace = true } oneshot_broadcast = { path = "../../common/oneshot_broadcast/" } operation_pool = { workspace = true } parking_lot = { workspace = true } diff --git a/beacon_node/beacon_processor/Cargo.toml b/beacon_node/beacon_processor/Cargo.toml index 9273137bf6..c96e0868d7 100644 --- a/beacon_node/beacon_processor/Cargo.toml +++ b/beacon_node/beacon_processor/Cargo.toml @@ -4,22 +4,22 @@ version = "0.1.0" edition = { workspace = true } [dependencies] -slog = { workspace = true } -itertools = { workspace = true } -logging = { workspace = true } -tokio = { workspace = true } -tokio-util = { workspace = true } -futures = { workspace = true } fnv = { workspace = true } +futures = { workspace = true } +itertools = { workspace = true } +lighthouse_network = { workspace = true } +logging = { workspace = true } +metrics = { workspace = true } +num_cpus = { workspace = true } +parking_lot = { workspace = true } +serde = { workspace = true } +slog = { workspace = true } +slot_clock = { workspace = true } strum = { workspace = true } task_executor = { workspace = true } -slot_clock = { workspace = true } -lighthouse_network = { workspace = true } +tokio = { workspace = true } +tokio-util = { workspace = true } types = { workspace = true } -metrics = { workspace = true } -parking_lot = { workspace = true } -num_cpus = { workspace = true } -serde = { workspace = true } [dev-dependencies] tokio = { workspace = true, features = ["test-util"] } diff --git a/beacon_node/builder_client/Cargo.toml b/beacon_node/builder_client/Cargo.toml index c3658f45c7..3531e81c84 100644 --- a/beacon_node/builder_client/Cargo.toml +++ b/beacon_node/builder_client/Cargo.toml @@ -5,8 +5,8 @@ edition = { workspace = true } authors = ["Sean Anderson "] [dependencies] +eth2 = { workspace = true } +lighthouse_version = { workspace = true } reqwest = { workspace = true } sensitive_url = { workspace = true } -eth2 = { workspace = true } serde = { workspace = true } -lighthouse_version = { workspace = true } diff --git a/beacon_node/client/Cargo.toml b/beacon_node/client/Cargo.toml index 4df13eb3d4..614115eb58 100644 --- a/beacon_node/client/Cargo.toml +++ b/beacon_node/client/Cargo.toml @@ -5,41 +5,41 @@ authors = ["Sigma Prime "] edition = { workspace = true } [dev-dependencies] +operation_pool = { workspace = true } serde_yaml = { workspace = true } state_processing = { workspace = true } -operation_pool = { workspace = true } tokio = { workspace = true } [dependencies] beacon_chain = { workspace = true } -store = { workspace = true } -network = { workspace = true } -timer = { path = "../timer" } -lighthouse_network = { workspace = true } -types = { workspace = true } -eth2_config = { workspace = true } -slot_clock = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -slog = { workspace = true } -tokio = { workspace = true } -futures = { workspace = true } +beacon_processor = { workspace = true } +directory = { workspace = true } dirs = { workspace = true } +environment = { workspace = true } eth1 = { workspace = true } eth2 = { workspace = true } -kzg = { workspace = true } -sensitive_url = { workspace = true } +eth2_config = { workspace = true } +ethereum_ssz = { workspace = true } +execution_layer = { workspace = true } +futures = { workspace = true } genesis = { workspace = true } -task_executor = { workspace = true } -environment = { workspace = true } -metrics = { workspace = true } -time = "0.3.5" -directory = { workspace = true } http_api = { workspace = true } http_metrics = { path = "../http_metrics" } +kzg = { workspace = true } +lighthouse_network = { workspace = true } +metrics = { workspace = true } +monitoring_api = { workspace = true } +network = { workspace = true } +sensitive_url = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } slasher = { workspace = true } slasher_service = { path = "../../slasher/service" } -monitoring_api = { workspace = true } -execution_layer = { workspace = true } -beacon_processor = { workspace = true } -ethereum_ssz = { workspace = true } +slog = { workspace = true } +slot_clock = { workspace = true } +store = { workspace = true } +task_executor = { workspace = true } +time = "0.3.5" +timer = { path = "../timer" } +tokio = { workspace = true } +types = { workspace = true } diff --git a/beacon_node/eth1/Cargo.toml b/beacon_node/eth1/Cargo.toml index 50400a77e0..8ccd50aad8 100644 --- a/beacon_node/eth1/Cargo.toml +++ b/beacon_node/eth1/Cargo.toml @@ -5,27 +5,27 @@ authors = ["Paul Hauner "] edition = { workspace = true } [dev-dependencies] +environment = { workspace = true } eth1_test_rig = { workspace = true } serde_yaml = { workspace = true } sloggers = { workspace = true } -environment = { workspace = true } [dependencies] -execution_layer = { workspace = true } -futures = { workspace = true } -serde = { workspace = true } -types = { workspace = true } -merkle_proof = { workspace = true } +eth2 = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } -tree_hash = { workspace = true } -parking_lot = { workspace = true } -slog = { workspace = true } +execution_layer = { workspace = true } +futures = { workspace = true } logging = { workspace = true } -superstruct = { workspace = true } -tokio = { workspace = true } -state_processing = { workspace = true } +merkle_proof = { workspace = true } metrics = { workspace = true } -task_executor = { workspace = true } -eth2 = { workspace = true } +parking_lot = { workspace = true } sensitive_url = { workspace = true } +serde = { workspace = true } +slog = { workspace = true } +state_processing = { workspace = true } +superstruct = { workspace = true } +task_executor = { workspace = true } +tokio = { workspace = true } +tree_hash = { workspace = true } +types = { workspace = true } diff --git a/beacon_node/execution_layer/Cargo.toml b/beacon_node/execution_layer/Cargo.toml index 0ef101fae7..7eb7b4a15e 100644 --- a/beacon_node/execution_layer/Cargo.toml +++ b/beacon_node/execution_layer/Cargo.toml @@ -2,54 +2,53 @@ name = "execution_layer" version = "0.1.0" edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +alloy-consensus = { workspace = true } alloy-primitives = { workspace = true } -types = { workspace = true } -tokio = { workspace = true } -slog = { workspace = true } -logging = { workspace = true } -sensitive_url = { workspace = true } -reqwest = { workspace = true } -ethereum_serde_utils = { workspace = true } -serde_json = { workspace = true } -serde = { workspace = true } -warp = { workspace = true } -jsonwebtoken = "9" +alloy-rlp = { workspace = true } +arc-swap = "1.6.0" +builder_client = { path = "../builder_client" } bytes = { workspace = true } -task_executor = { workspace = true } -hex = { workspace = true } -ethereum_ssz = { workspace = true } -ssz_types = { workspace = true } eth2 = { workspace = true } +eth2_network_config = { workspace = true } +ethereum_serde_utils = { workspace = true } +ethereum_ssz = { workspace = true } +ethers-core = { workspace = true } +fixed_bytes = { workspace = true } +fork_choice = { workspace = true } +hash-db = "0.15.2" +hash256-std-hasher = "0.15.2" +hex = { workspace = true } +jsonwebtoken = "9" +keccak-hash = "0.10.0" kzg = { workspace = true } -state_processing = { workspace = true } -superstruct = { workspace = true } +lighthouse_version = { workspace = true } +logging = { workspace = true } lru = { workspace = true } +metrics = { workspace = true } +parking_lot = { workspace = true } +pretty_reqwest_error = { workspace = true } +rand = { workspace = true } +reqwest = { workspace = true } +sensitive_url = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +sha2 = { workspace = true } +slog = { workspace = true } +slot_clock = { workspace = true } +ssz_types = { workspace = true } +state_processing = { workspace = true } +strum = { workspace = true } +superstruct = { workspace = true } +task_executor = { workspace = true } +tempfile = { workspace = true } +tokio = { workspace = true } +tokio-stream = { workspace = true } tree_hash = { workspace = true } tree_hash_derive = { workspace = true } -parking_lot = { workspace = true } -slot_clock = { workspace = true } -tempfile = { workspace = true } -rand = { workspace = true } -zeroize = { workspace = true } -metrics = { workspace = true } -ethers-core = { workspace = true } -builder_client = { path = "../builder_client" } -fork_choice = { workspace = true } -tokio-stream = { workspace = true } -strum = { workspace = true } -keccak-hash = "0.10.0" -hash256-std-hasher = "0.15.2" triehash = "0.8.4" -hash-db = "0.15.2" -pretty_reqwest_error = { workspace = true } -arc-swap = "1.6.0" -eth2_network_config = { workspace = true } -alloy-rlp = { workspace = true } -alloy-consensus = { workspace = true } -lighthouse_version = { workspace = true } -fixed_bytes = { workspace = true } -sha2 = { workspace = true } +types = { workspace = true } +warp = { workspace = true } +zeroize = { workspace = true } diff --git a/beacon_node/http_api/Cargo.toml b/beacon_node/http_api/Cargo.toml index 638fe0f219..5d601008bc 100644 --- a/beacon_node/http_api/Cargo.toml +++ b/beacon_node/http_api/Cargo.toml @@ -6,49 +6,49 @@ edition = { workspace = true } autotests = false # using a single test binary compiles faster [dependencies] -warp = { workspace = true } -serde = { workspace = true } -tokio = { workspace = true } -tokio-stream = { workspace = true } -types = { workspace = true } -hex = { workspace = true } beacon_chain = { workspace = true } -eth2 = { workspace = true } -slog = { workspace = true } -network = { workspace = true } -lighthouse_network = { workspace = true } -eth1 = { workspace = true } -state_processing = { workspace = true } -lighthouse_version = { workspace = true } -metrics = { workspace = true } -warp_utils = { workspace = true } -slot_clock = { workspace = true } -ethereum_ssz = { workspace = true } +beacon_processor = { workspace = true } bs58 = "0.4.0" -futures = { workspace = true } +bytes = { workspace = true } +directory = { workspace = true } +eth1 = { workspace = true } +eth2 = { workspace = true } +ethereum_serde_utils = { workspace = true } +ethereum_ssz = { workspace = true } execution_layer = { workspace = true } -parking_lot = { workspace = true } -safe_arith = { workspace = true } -task_executor = { workspace = true } +futures = { workspace = true } +hex = { workspace = true } +lighthouse_network = { workspace = true } +lighthouse_version = { workspace = true } +logging = { workspace = true } lru = { workspace = true } -tree_hash = { workspace = true } +metrics = { workspace = true } +network = { workspace = true } +operation_pool = { workspace = true } +parking_lot = { workspace = true } +rand = { workspace = true } +safe_arith = { workspace = true } +sensitive_url = { workspace = true } +serde = { workspace = true } +slog = { workspace = true } +slot_clock = { workspace = true } +state_processing = { workspace = true } +store = { workspace = true } sysinfo = { workspace = true } system_health = { path = "../../common/system_health" } -directory = { workspace = true } -logging = { workspace = true } -ethereum_serde_utils = { workspace = true } -operation_pool = { workspace = true } -sensitive_url = { workspace = true } -store = { workspace = true } -bytes = { workspace = true } -beacon_processor = { workspace = true } -rand = { workspace = true } +task_executor = { workspace = true } +tokio = { workspace = true } +tokio-stream = { workspace = true } +tree_hash = { workspace = true } +types = { workspace = true } +warp = { workspace = true } +warp_utils = { workspace = true } [dev-dependencies] -serde_json = { workspace = true } -proto_array = { workspace = true } genesis = { workspace = true } logging = { workspace = true } +proto_array = { workspace = true } +serde_json = { workspace = true } [[test]] name = "bn_http_api_tests" diff --git a/beacon_node/http_metrics/Cargo.toml b/beacon_node/http_metrics/Cargo.toml index 97ba72a2ac..d92f986440 100644 --- a/beacon_node/http_metrics/Cargo.toml +++ b/beacon_node/http_metrics/Cargo.toml @@ -3,24 +3,23 @@ name = "http_metrics" version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -warp = { workspace = true } +beacon_chain = { workspace = true } +lighthouse_network = { workspace = true } +lighthouse_version = { workspace = true } +malloc_utils = { workspace = true } +metrics = { workspace = true } serde = { workspace = true } slog = { workspace = true } -beacon_chain = { workspace = true } -store = { workspace = true } -lighthouse_network = { workspace = true } slot_clock = { workspace = true } -metrics = { workspace = true } -lighthouse_version = { workspace = true } +store = { workspace = true } +warp = { workspace = true } warp_utils = { workspace = true } -malloc_utils = { workspace = true } [dev-dependencies] -tokio = { workspace = true } -reqwest = { workspace = true } -types = { workspace = true } logging = { workspace = true } +reqwest = { workspace = true } +tokio = { workspace = true } +types = { workspace = true } diff --git a/beacon_node/lighthouse_network/Cargo.toml b/beacon_node/lighthouse_network/Cargo.toml index eccc244d59..485f32b37a 100644 --- a/beacon_node/lighthouse_network/Cargo.toml +++ b/beacon_node/lighthouse_network/Cargo.toml @@ -5,49 +5,49 @@ authors = ["Sigma Prime "] edition = { workspace = true } [dependencies] -alloy-primitives = { workspace = true} +alloy-primitives = { workspace = true } +alloy-rlp = { workspace = true } +bytes = { workspace = true } +delay_map = { workspace = true } +directory = { workspace = true } +dirs = { workspace = true } discv5 = { workspace = true } -gossipsub = { workspace = true } -unsigned-varint = { version = "0.8", features = ["codec"] } -ssz_types = { workspace = true } -types = { workspace = true } -serde = { workspace = true } +either = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } -slog = { workspace = true } -lighthouse_version = { workspace = true } -tokio = { workspace = true } -futures = { workspace = true } -dirs = { workspace = true } fnv = { workspace = true } -metrics = { workspace = true } -smallvec = { workspace = true } -tokio-io-timeout = "1" +futures = { workspace = true } +gossipsub = { workspace = true } +hex = { workspace = true } +itertools = { workspace = true } +libp2p-mplex = "0.42" +lighthouse_version = { workspace = true } lru = { workspace = true } lru_cache = { workspace = true } +metrics = { workspace = true } parking_lot = { workspace = true } -sha2 = { workspace = true } -snap = { workspace = true } -hex = { workspace = true } -tokio-util = { workspace = true } -tiny-keccak = "2" -task_executor = { workspace = true } +prometheus-client = "0.22.0" rand = { workspace = true } -directory = { workspace = true } regex = { workspace = true } +serde = { workspace = true } +sha2 = { workspace = true } +slog = { workspace = true } +smallvec = { workspace = true } +snap = { workspace = true } +ssz_types = { workspace = true } strum = { workspace = true } superstruct = { workspace = true } -prometheus-client = "0.22.0" +task_executor = { workspace = true } +tiny-keccak = "2" +tokio = { workspace = true } +tokio-io-timeout = "1" +tokio-util = { workspace = true } +types = { workspace = true } +unsigned-varint = { version = "0.8", features = ["codec"] } unused_port = { workspace = true } -delay_map = { workspace = true } -bytes = { workspace = true } -either = { workspace = true } -itertools = { workspace = true } -alloy-rlp = { workspace = true } # Local dependencies void = "1.0.2" -libp2p-mplex = "0.42" [dependencies.libp2p] version = "0.54" @@ -55,13 +55,13 @@ default-features = false features = ["identify", "yamux", "noise", "dns", "tcp", "tokio", "plaintext", "secp256k1", "macros", "ecdsa", "metrics", "quic", "upnp"] [dev-dependencies] -slog-term = { workspace = true } -slog-async = { workspace = true } -tempfile = { workspace = true } -quickcheck = { workspace = true } -quickcheck_macros = { workspace = true } async-channel = { workspace = true } logging = { workspace = true } +quickcheck = { workspace = true } +quickcheck_macros = { workspace = true } +slog-async = { workspace = true } +slog-term = { workspace = true } +tempfile = { workspace = true } [features] libp2p-websocket = [] diff --git a/beacon_node/lighthouse_network/gossipsub/Cargo.toml b/beacon_node/lighthouse_network/gossipsub/Cargo.toml index 6cbe6d3a1c..61f5730c08 100644 --- a/beacon_node/lighthouse_network/gossipsub/Cargo.toml +++ b/beacon_node/lighthouse_network/gossipsub/Cargo.toml @@ -24,9 +24,10 @@ fnv = "1.0.7" futures = "0.3.30" futures-timer = "3.0.2" getrandom = "0.2.12" -hashlink.workspace = true +hashlink = { workspace = true } hex_fmt = "0.3.0" libp2p = { version = "0.54", default-features = false } +prometheus-client = "0.22.0" quick-protobuf = "0.8" quick-protobuf-codec = "0.3" rand = "0.8" @@ -35,7 +36,6 @@ serde = { version = "1", optional = true, features = ["derive"] } sha2 = "0.10.8" tracing = "0.1.37" void = "1.0.2" -prometheus-client = "0.22.0" web-time = "1.1.0" [dev-dependencies] diff --git a/beacon_node/network/Cargo.toml b/beacon_node/network/Cargo.toml index 6fc818e9c9..44f6c54bbc 100644 --- a/beacon_node/network/Cargo.toml +++ b/beacon_node/network/Cargo.toml @@ -5,51 +5,51 @@ authors = ["Sigma Prime "] edition = { workspace = true } [dev-dependencies] -sloggers = { workspace = true } +bls = { workspace = true } +eth2 = { workspace = true } +eth2_network_config = { workspace = true } genesis = { workspace = true } +gossipsub = { workspace = true } +kzg = { workspace = true } matches = "0.1.8" serde_json = { workspace = true } -slog-term = { workspace = true } slog-async = { workspace = true } -eth2 = { workspace = true } -gossipsub = { workspace = true } -eth2_network_config = { workspace = true } -kzg = { workspace = true } -bls = { workspace = true } +slog-term = { workspace = true } +sloggers = { workspace = true } [dependencies] alloy-primitives = { workspace = true } -async-channel = { workspace = true } -anyhow = { workspace = true } -beacon_chain = { workspace = true } -store = { workspace = true } -lighthouse_network = { workspace = true } -types = { workspace = true } -slot_clock = { workspace = true } -slog = { workspace = true } -hex = { workspace = true } -ethereum_ssz = { workspace = true } -ssz_types = { workspace = true } -futures = { workspace = true } -tokio = { workspace = true } -tokio-stream = { workspace = true } -smallvec = { workspace = true } -rand = { workspace = true } -fnv = { workspace = true } alloy-rlp = { workspace = true } -metrics = { workspace = true } -logging = { workspace = true } -task_executor = { workspace = true } +anyhow = { workspace = true } +async-channel = { workspace = true } +beacon_chain = { workspace = true } +beacon_processor = { workspace = true } +delay_map = { workspace = true } +derivative = { workspace = true } +ethereum_ssz = { workspace = true } +execution_layer = { workspace = true } +fnv = { workspace = true } +futures = { workspace = true } +hex = { workspace = true } igd-next = "0.14" itertools = { workspace = true } +lighthouse_network = { workspace = true } +logging = { workspace = true } lru_cache = { workspace = true } -strum = { workspace = true } -derivative = { workspace = true } -delay_map = { workspace = true } +metrics = { workspace = true } operation_pool = { workspace = true } -execution_layer = { workspace = true } -beacon_processor = { workspace = true } parking_lot = { workspace = true } +rand = { workspace = true } +slog = { workspace = true } +slot_clock = { workspace = true } +smallvec = { workspace = true } +ssz_types = { workspace = true } +store = { workspace = true } +strum = { workspace = true } +task_executor = { workspace = true } +tokio = { workspace = true } +tokio-stream = { workspace = true } +types = { workspace = true } [features] # NOTE: This can be run via cargo build --bin lighthouse --features network/disable-backfill diff --git a/beacon_node/store/Cargo.toml b/beacon_node/store/Cargo.toml index 7cee16c353..21d0cf8dec 100644 --- a/beacon_node/store/Cargo.toml +++ b/beacon_node/store/Cargo.toml @@ -5,34 +5,34 @@ authors = ["Paul Hauner "] edition = { workspace = true } [dev-dependencies] -tempfile = { workspace = true } beacon_chain = { workspace = true } criterion = { workspace = true } rand = { workspace = true, features = ["small_rng"] } +tempfile = { workspace = true } [dependencies] +bls = { workspace = true } db-key = "0.0.5" -leveldb = { version = "0.8" } -parking_lot = { workspace = true } -itertools = { workspace = true } +directory = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } +itertools = { workspace = true } +leveldb = { version = "0.8" } +logging = { workspace = true } +lru = { workspace = true } +metrics = { workspace = true } +parking_lot = { workspace = true } +safe_arith = { workspace = true } +serde = { workspace = true } +slog = { workspace = true } +sloggers = { workspace = true } +smallvec = { workspace = true } +state_processing = { workspace = true } +strum = { workspace = true } superstruct = { workspace = true } types = { workspace = true } -safe_arith = { workspace = true } -state_processing = { workspace = true } -slog = { workspace = true } -serde = { workspace = true } -metrics = { workspace = true } -lru = { workspace = true } -sloggers = { workspace = true } -directory = { workspace = true } -strum = { workspace = true } xdelta3 = { workspace = true } zstd = { workspace = true } -bls = { workspace = true } -smallvec = { workspace = true } -logging = { workspace = true } [[bench]] name = "hdiff" diff --git a/beacon_node/timer/Cargo.toml b/beacon_node/timer/Cargo.toml index afb93f3657..546cc2ed41 100644 --- a/beacon_node/timer/Cargo.toml +++ b/beacon_node/timer/Cargo.toml @@ -6,7 +6,7 @@ edition = { workspace = true } [dependencies] beacon_chain = { workspace = true } -slot_clock = { workspace = true } -tokio = { workspace = true } slog = { workspace = true } +slot_clock = { workspace = true } task_executor = { workspace = true } +tokio = { workspace = true } diff --git a/boot_node/Cargo.toml b/boot_node/Cargo.toml index c60d308cbb..7c8d2b16fd 100644 --- a/boot_node/Cargo.toml +++ b/boot_node/Cargo.toml @@ -6,19 +6,19 @@ edition = { workspace = true } [dependencies] beacon_node = { workspace = true } +bytes = { workspace = true } clap = { workspace = true } clap_utils = { workspace = true } -lighthouse_network = { workspace = true } -types = { workspace = true } +eth2_network_config = { workspace = true } ethereum_ssz = { workspace = true } -slog = { workspace = true } -tokio = { workspace = true } +hex = { workspace = true } +lighthouse_network = { workspace = true } log = { workspace = true } -slog-term = { workspace = true } logging = { workspace = true } +serde = { workspace = true } +slog = { workspace = true } slog-async = { workspace = true } slog-scope = "4.3.0" -hex = { workspace = true } -serde = { workspace = true } -eth2_network_config = { workspace = true } -bytes = { workspace = true } +slog-term = { workspace = true } +tokio = { workspace = true } +types = { workspace = true } diff --git a/common/account_utils/Cargo.toml b/common/account_utils/Cargo.toml index e66bf14233..dece975d37 100644 --- a/common/account_utils/Cargo.toml +++ b/common/account_utils/Cargo.toml @@ -3,20 +3,19 @@ name = "account_utils" version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rand = { workspace = true } -eth2_wallet = { workspace = true } +directory = { workspace = true } eth2_keystore = { workspace = true } +eth2_wallet = { workspace = true } filesystem = { workspace = true } -zeroize = { workspace = true } +rand = { workspace = true } +regex = { workspace = true } +rpassword = "5.0.0" serde = { workspace = true } serde_yaml = { workspace = true } slog = { workspace = true } types = { workspace = true } validator_dir = { workspace = true } -regex = { workspace = true } -rpassword = "5.0.0" -directory = { workspace = true } +zeroize = { workspace = true } diff --git a/common/clap_utils/Cargo.toml b/common/clap_utils/Cargo.toml index 73823ae24e..f3c166bda9 100644 --- a/common/clap_utils/Cargo.toml +++ b/common/clap_utils/Cargo.toml @@ -3,16 +3,15 @@ name = "clap_utils" version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] alloy-primitives = { workspace = true } clap = { workspace = true } -hex = { workspace = true } dirs = { workspace = true } eth2_network_config = { workspace = true } ethereum_ssz = { workspace = true } +hex = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde_yaml = { workspace = true } diff --git a/common/compare_fields_derive/Cargo.toml b/common/compare_fields_derive/Cargo.toml index b4bbbaa436..19682bf367 100644 --- a/common/compare_fields_derive/Cargo.toml +++ b/common/compare_fields_derive/Cargo.toml @@ -8,5 +8,5 @@ edition = { workspace = true } proc-macro = true [dependencies] -syn = { workspace = true } quote = { workspace = true } +syn = { workspace = true } diff --git a/common/deposit_contract/Cargo.toml b/common/deposit_contract/Cargo.toml index a03ac2178f..953fde1af7 100644 --- a/common/deposit_contract/Cargo.toml +++ b/common/deposit_contract/Cargo.toml @@ -7,13 +7,13 @@ edition = { workspace = true } build = "build.rs" [build-dependencies] +hex = { workspace = true } reqwest = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } -hex = { workspace = true } [dependencies] -types = { workspace = true } +ethabi = "16.0.0" ethereum_ssz = { workspace = true } tree_hash = { workspace = true } -ethabi = "16.0.0" +types = { workspace = true } diff --git a/common/directory/Cargo.toml b/common/directory/Cargo.toml index f724337261..9c3ced9097 100644 --- a/common/directory/Cargo.toml +++ b/common/directory/Cargo.toml @@ -3,7 +3,6 @@ name = "directory" version = "0.1.0" authors = ["pawan "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/common/eth2/Cargo.toml b/common/eth2/Cargo.toml index 912051da36..9d6dea100d 100644 --- a/common/eth2/Cargo.toml +++ b/common/eth2/Cargo.toml @@ -3,31 +3,30 @@ name = "eth2" version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -serde = { workspace = true } -serde_json = { workspace = true } -ssz_types = { workspace = true } -types = { workspace = true } -reqwest = { workspace = true } -lighthouse_network = { workspace = true } -proto_array = { workspace = true } -ethereum_serde_utils = { workspace = true } +derivative = { workspace = true } eth2_keystore = { workspace = true } -zeroize = { workspace = true } -sensitive_url = { workspace = true } +ethereum_serde_utils = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } -futures-util = "0.3.8" futures = { workspace = true } -store = { workspace = true } -slashing_protection = { workspace = true } +futures-util = "0.3.8" +lighthouse_network = { workspace = true } mediatype = "0.19.13" pretty_reqwest_error = { workspace = true } -derivative = { workspace = true } +proto_array = { workspace = true } +reqwest = { workspace = true } reqwest-eventsource = "0.5.0" +sensitive_url = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +slashing_protection = { workspace = true } +ssz_types = { workspace = true } +store = { workspace = true } +types = { workspace = true } +zeroize = { workspace = true } [dev-dependencies] tokio = { workspace = true } diff --git a/common/eth2_config/Cargo.toml b/common/eth2_config/Cargo.toml index 20c3b0b6f2..509f5ff87e 100644 --- a/common/eth2_config/Cargo.toml +++ b/common/eth2_config/Cargo.toml @@ -5,5 +5,5 @@ authors = ["Paul Hauner "] edition = { workspace = true } [dependencies] -types = { workspace = true } paste = { workspace = true } +types = { workspace = true } diff --git a/common/eth2_interop_keypairs/Cargo.toml b/common/eth2_interop_keypairs/Cargo.toml index 5971b934e0..c19b32014e 100644 --- a/common/eth2_interop_keypairs/Cargo.toml +++ b/common/eth2_interop_keypairs/Cargo.toml @@ -3,16 +3,15 @@ name = "eth2_interop_keypairs" version = "0.2.0" authors = ["Paul Hauner "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -num-bigint = "0.4.2" +bls = { workspace = true } ethereum_hashing = { workspace = true } hex = { workspace = true } -serde_yaml = { workspace = true } +num-bigint = "0.4.2" serde = { workspace = true } -bls = { workspace = true } +serde_yaml = { workspace = true } [dev-dependencies] base64 = "0.13.0" diff --git a/common/eth2_network_config/Cargo.toml b/common/eth2_network_config/Cargo.toml index 09cf2072d2..a255e04229 100644 --- a/common/eth2_network_config/Cargo.toml +++ b/common/eth2_network_config/Cargo.toml @@ -7,25 +7,25 @@ edition = { workspace = true } build = "build.rs" [build-dependencies] -zip = { workspace = true } eth2_config = { workspace = true } +zip = { workspace = true } [dev-dependencies] +ethereum_ssz = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true } -ethereum_ssz = { workspace = true } [dependencies] -serde_yaml = { workspace = true } -types = { workspace = true } -eth2_config = { workspace = true } -discv5 = { workspace = true } -reqwest = { workspace = true } -pretty_reqwest_error = { workspace = true } -sha2 = { workspace = true } -url = { workspace = true } -sensitive_url = { workspace = true } -slog = { workspace = true } -logging = { workspace = true } bytes = { workspace = true } +discv5 = { workspace = true } +eth2_config = { workspace = true } kzg = { workspace = true } +logging = { workspace = true } +pretty_reqwest_error = { workspace = true } +reqwest = { workspace = true } +sensitive_url = { workspace = true } +serde_yaml = { workspace = true } +sha2 = { workspace = true } +slog = { workspace = true } +types = { workspace = true } +url = { workspace = true } diff --git a/common/eth2_wallet_manager/Cargo.toml b/common/eth2_wallet_manager/Cargo.toml index f471757065..a6eb24c78c 100644 --- a/common/eth2_wallet_manager/Cargo.toml +++ b/common/eth2_wallet_manager/Cargo.toml @@ -3,7 +3,6 @@ name = "eth2_wallet_manager" version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/common/lighthouse_version/Cargo.toml b/common/lighthouse_version/Cargo.toml index 3c4f9fe50c..164e3e47a7 100644 --- a/common/lighthouse_version/Cargo.toml +++ b/common/lighthouse_version/Cargo.toml @@ -3,7 +3,6 @@ name = "lighthouse_version" version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/common/logging/Cargo.toml b/common/logging/Cargo.toml index 73cbdf44d4..b2829a48d8 100644 --- a/common/logging/Cargo.toml +++ b/common/logging/Cargo.toml @@ -19,7 +19,7 @@ sloggers = { workspace = true } take_mut = "0.2.2" tokio = { workspace = true, features = [ "time" ] } tracing = "0.1" +tracing-appender = { workspace = true } tracing-core = { workspace = true } tracing-log = { workspace = true } tracing-subscriber = { workspace = true } -tracing-appender = { workspace = true } diff --git a/common/malloc_utils/Cargo.toml b/common/malloc_utils/Cargo.toml index 79a07eed16..64fb7b9aad 100644 --- a/common/malloc_utils/Cargo.toml +++ b/common/malloc_utils/Cargo.toml @@ -5,8 +5,8 @@ authors = ["Paul Hauner "] edition = { workspace = true } [dependencies] -metrics = { workspace = true } libc = "0.2.79" +metrics = { workspace = true } parking_lot = { workspace = true } tikv-jemalloc-ctl = { version = "0.6.0", optional = true, features = ["stats"] } diff --git a/common/monitoring_api/Cargo.toml b/common/monitoring_api/Cargo.toml index 2da32c307e..5008c86e85 100644 --- a/common/monitoring_api/Cargo.toml +++ b/common/monitoring_api/Cargo.toml @@ -3,19 +3,18 @@ name = "monitoring_api" version = "0.1.0" authors = ["pawan "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -reqwest = { workspace = true } -task_executor = { workspace = true } -tokio = { workspace = true } eth2 = { workspace = true } -serde_json = { workspace = true } -serde = { workspace = true } lighthouse_version = { workspace = true } metrics = { workspace = true } +regex = { workspace = true } +reqwest = { workspace = true } +sensitive_url = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } slog = { workspace = true } store = { workspace = true } -regex = { workspace = true } -sensitive_url = { workspace = true } +task_executor = { workspace = true } +tokio = { workspace = true } diff --git a/common/oneshot_broadcast/Cargo.toml b/common/oneshot_broadcast/Cargo.toml index 12c9b40bc8..8a358ef851 100644 --- a/common/oneshot_broadcast/Cargo.toml +++ b/common/oneshot_broadcast/Cargo.toml @@ -2,7 +2,6 @@ name = "oneshot_broadcast" version = "0.1.0" edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/common/pretty_reqwest_error/Cargo.toml b/common/pretty_reqwest_error/Cargo.toml index dc79832cd3..4311601bcd 100644 --- a/common/pretty_reqwest_error/Cargo.toml +++ b/common/pretty_reqwest_error/Cargo.toml @@ -2,7 +2,6 @@ name = "pretty_reqwest_error" version = "0.1.0" edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/common/sensitive_url/Cargo.toml b/common/sensitive_url/Cargo.toml index d218c8d93a..ff56209722 100644 --- a/common/sensitive_url/Cargo.toml +++ b/common/sensitive_url/Cargo.toml @@ -3,9 +3,8 @@ name = "sensitive_url" version = "0.1.0" authors = ["Mac L "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -url = { workspace = true } serde = { workspace = true } +url = { workspace = true } diff --git a/common/slot_clock/Cargo.toml b/common/slot_clock/Cargo.toml index c2f330cd50..2e1982efb1 100644 --- a/common/slot_clock/Cargo.toml +++ b/common/slot_clock/Cargo.toml @@ -5,6 +5,6 @@ authors = ["Paul Hauner "] edition = { workspace = true } [dependencies] -types = { workspace = true } metrics = { workspace = true } parking_lot = { workspace = true } +types = { workspace = true } diff --git a/common/system_health/Cargo.toml b/common/system_health/Cargo.toml index be339f2779..034683f72e 100644 --- a/common/system_health/Cargo.toml +++ b/common/system_health/Cargo.toml @@ -5,7 +5,7 @@ edition = { workspace = true } [dependencies] lighthouse_network = { workspace = true } -types = { workspace = true } -sysinfo = { workspace = true } -serde = { workspace = true } parking_lot = { workspace = true } +serde = { workspace = true } +sysinfo = { workspace = true } +types = { workspace = true } diff --git a/common/task_executor/Cargo.toml b/common/task_executor/Cargo.toml index 26bcd7b339..c1ac4b55a9 100644 --- a/common/task_executor/Cargo.toml +++ b/common/task_executor/Cargo.toml @@ -11,10 +11,10 @@ tracing = ["dep:tracing"] [dependencies] async-channel = { workspace = true } -tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } -slog = { workspace = true, optional = true } futures = { workspace = true } -metrics = { workspace = true } -sloggers = { workspace = true, optional = true } logging = { workspace = true, optional = true } +metrics = { workspace = true } +slog = { workspace = true, optional = true } +sloggers = { workspace = true, optional = true } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } tracing = { workspace = true, optional = true } diff --git a/common/test_random_derive/Cargo.toml b/common/test_random_derive/Cargo.toml index 79308797a4..b38d5ef63a 100644 --- a/common/test_random_derive/Cargo.toml +++ b/common/test_random_derive/Cargo.toml @@ -9,5 +9,5 @@ description = "Procedural derive macros for implementation of TestRandom trait" proc-macro = true [dependencies] -syn = { workspace = true } quote = { workspace = true } +syn = { workspace = true } diff --git a/common/unused_port/Cargo.toml b/common/unused_port/Cargo.toml index 95dbf59186..2d771cd600 100644 --- a/common/unused_port/Cargo.toml +++ b/common/unused_port/Cargo.toml @@ -2,7 +2,6 @@ name = "unused_port" version = "0.1.0" edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/common/validator_dir/Cargo.toml b/common/validator_dir/Cargo.toml index ae8742fe07..773431c93c 100644 --- a/common/validator_dir/Cargo.toml +++ b/common/validator_dir/Cargo.toml @@ -6,21 +6,20 @@ edition = { workspace = true } [features] insecure_keys = [] - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] bls = { workspace = true } +deposit_contract = { workspace = true } +derivative = { workspace = true } +directory = { workspace = true } eth2_keystore = { workspace = true } filesystem = { workspace = true } -types = { workspace = true } -rand = { workspace = true } -deposit_contract = { workspace = true } -tree_hash = { workspace = true } hex = { workspace = true } -derivative = { workspace = true } lockfile = { workspace = true } -directory = { workspace = true } +rand = { workspace = true } +tree_hash = { workspace = true } +types = { workspace = true } [dev-dependencies] tempfile = { workspace = true } diff --git a/common/warp_utils/Cargo.toml b/common/warp_utils/Cargo.toml index a9407c392d..4a3cde54a9 100644 --- a/common/warp_utils/Cargo.toml +++ b/common/warp_utils/Cargo.toml @@ -3,20 +3,19 @@ name = "warp_utils" version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -warp = { workspace = true } -eth2 = { workspace = true } -types = { workspace = true } beacon_chain = { workspace = true } -state_processing = { workspace = true } -safe_arith = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -tokio = { workspace = true } +bytes = { workspace = true } +eth2 = { workspace = true } headers = "0.3.2" metrics = { workspace = true } +safe_arith = { workspace = true } +serde = { workspace = true } serde_array_query = "0.1.0" -bytes = { workspace = true } +serde_json = { workspace = true } +state_processing = { workspace = true } +tokio = { workspace = true } +types = { workspace = true } +warp = { workspace = true } diff --git a/consensus/fixed_bytes/Cargo.toml b/consensus/fixed_bytes/Cargo.toml index e5201a0455..ab29adfb1b 100644 --- a/consensus/fixed_bytes/Cargo.toml +++ b/consensus/fixed_bytes/Cargo.toml @@ -3,7 +3,6 @@ name = "fixed_bytes" version = "0.1.0" authors = ["Eitan Seri-Levi "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/consensus/fork_choice/Cargo.toml b/consensus/fork_choice/Cargo.toml index b32e0aa665..3bd18e922a 100644 --- a/consensus/fork_choice/Cargo.toml +++ b/consensus/fork_choice/Cargo.toml @@ -3,17 +3,16 @@ name = "fork_choice" version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -types = { workspace = true } -state_processing = { workspace = true } -proto_array = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } metrics = { workspace = true } +proto_array = { workspace = true } slog = { workspace = true } +state_processing = { workspace = true } +types = { workspace = true } [dev-dependencies] beacon_chain = { workspace = true } diff --git a/consensus/int_to_bytes/Cargo.toml b/consensus/int_to_bytes/Cargo.toml index e99d1af8e5..c639dfce8d 100644 --- a/consensus/int_to_bytes/Cargo.toml +++ b/consensus/int_to_bytes/Cargo.toml @@ -8,5 +8,5 @@ edition = { workspace = true } bytes = { workspace = true } [dev-dependencies] -yaml-rust2 = "0.8" hex = { workspace = true } +yaml-rust2 = "0.8" diff --git a/consensus/proto_array/Cargo.toml b/consensus/proto_array/Cargo.toml index 99f98cf545..bd6757c0fa 100644 --- a/consensus/proto_array/Cargo.toml +++ b/consensus/proto_array/Cargo.toml @@ -9,10 +9,10 @@ name = "proto_array" path = "src/bin.rs" [dependencies] -types = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } +safe_arith = { workspace = true } serde = { workspace = true } serde_yaml = { workspace = true } -safe_arith = { workspace = true } superstruct = { workspace = true } +types = { workspace = true } diff --git a/consensus/safe_arith/Cargo.toml b/consensus/safe_arith/Cargo.toml index 6f2e4b811c..9ac9fe28d3 100644 --- a/consensus/safe_arith/Cargo.toml +++ b/consensus/safe_arith/Cargo.toml @@ -3,7 +3,6 @@ name = "safe_arith" version = "0.1.0" authors = ["Michael Sproul "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/consensus/state_processing/Cargo.toml b/consensus/state_processing/Cargo.toml index b7f6ef7b2a..502ffe3cf6 100644 --- a/consensus/state_processing/Cargo.toml +++ b/consensus/state_processing/Cargo.toml @@ -5,30 +5,30 @@ authors = ["Paul Hauner ", "Michael Sproul "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -sha2 = { workspace = true } -zeroize = { workspace = true } +bls = { workspace = true } num-bigint-dig = { version = "0.8.4", features = ["zeroize"] } ring = { workspace = true } -bls = { workspace = true } +sha2 = { workspace = true } +zeroize = { workspace = true } [dev-dependencies] hex = { workspace = true } diff --git a/crypto/eth2_keystore/Cargo.toml b/crypto/eth2_keystore/Cargo.toml index bb6222807b..61d2722efb 100644 --- a/crypto/eth2_keystore/Cargo.toml +++ b/crypto/eth2_keystore/Cargo.toml @@ -3,25 +3,24 @@ name = "eth2_keystore" version = "0.1.0" authors = ["Pawan Dhananjay "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rand = { workspace = true } +aes = { version = "0.7", features = ["ctr"] } +bls = { workspace = true } +eth2_key_derivation = { workspace = true } +hex = { workspace = true } hmac = "0.11.0" pbkdf2 = { version = "0.8.0", default-features = false } +rand = { workspace = true } scrypt = { version = "0.7.0", default-features = false } +serde = { workspace = true } +serde_json = { workspace = true } +serde_repr = { workspace = true } sha2 = { workspace = true } +unicode-normalization = "0.1.16" uuid = { workspace = true } zeroize = { workspace = true } -serde = { workspace = true } -serde_repr = { workspace = true } -hex = { workspace = true } -bls = { workspace = true } -serde_json = { workspace = true } -eth2_key_derivation = { workspace = true } -unicode-normalization = "0.1.16" -aes = { version = "0.7", features = ["ctr"] } [dev-dependencies] tempfile = { workspace = true } diff --git a/crypto/eth2_wallet/Cargo.toml b/crypto/eth2_wallet/Cargo.toml index f3af6aab59..5327bdc163 100644 --- a/crypto/eth2_wallet/Cargo.toml +++ b/crypto/eth2_wallet/Cargo.toml @@ -3,18 +3,17 @@ name = "eth2_wallet" version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +eth2_key_derivation = { workspace = true } +eth2_keystore = { workspace = true } +rand = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde_repr = { workspace = true } -uuid = { workspace = true } -rand = { workspace = true } -eth2_keystore = { workspace = true } -eth2_key_derivation = { workspace = true } tiny-bip39 = "1" +uuid = { workspace = true } [dev-dependencies] hex = { workspace = true } diff --git a/crypto/kzg/Cargo.toml b/crypto/kzg/Cargo.toml index ce55f83639..bfe0f19cd0 100644 --- a/crypto/kzg/Cargo.toml +++ b/crypto/kzg/Cargo.toml @@ -3,22 +3,21 @@ name = "kzg" version = "0.1.0" authors = ["Pawan Dhananjay "] edition = "2021" - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] arbitrary = { workspace = true } +c-kzg = { workspace = true } +derivative = { workspace = true } +ethereum_hashing = { workspace = true } +ethereum_serde_utils = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } -tree_hash = { workspace = true } -derivative = { workspace = true } -serde = { workspace = true } -ethereum_serde_utils = { workspace = true } hex = { workspace = true } -ethereum_hashing = { workspace = true } -c-kzg = { workspace = true } rust_eth_kzg = { workspace = true } +serde = { workspace = true } serde_json = { workspace = true } +tree_hash = { workspace = true } [dev-dependencies] criterion = { workspace = true } diff --git a/database_manager/Cargo.toml b/database_manager/Cargo.toml index 96176f3fba..a7a54b1416 100644 --- a/database_manager/Cargo.toml +++ b/database_manager/Cargo.toml @@ -10,8 +10,8 @@ clap = { workspace = true } clap_utils = { workspace = true } environment = { workspace = true } hex = { workspace = true } -store = { workspace = true } -types = { workspace = true } -slog = { workspace = true } -strum = { workspace = true } serde = { workspace = true } +slog = { workspace = true } +store = { workspace = true } +strum = { workspace = true } +types = { workspace = true } diff --git a/lcli/Cargo.toml b/lcli/Cargo.toml index 9612bded47..72be77a70b 100644 --- a/lcli/Cargo.toml +++ b/lcli/Cargo.toml @@ -11,36 +11,36 @@ fake_crypto = ['bls/fake_crypto'] jemalloc = ["malloc_utils/jemalloc"] [dependencies] +account_utils = { workspace = true } +beacon_chain = { workspace = true } bls = { workspace = true } clap = { workspace = true } -log = { workspace = true } -sloggers = { workspace = true } -serde = { workspace = true } -serde_yaml = { workspace = true } -serde_json = { workspace = true } +clap_utils = { workspace = true } +deposit_contract = { workspace = true } env_logger = { workspace = true } -types = { workspace = true } -state_processing = { workspace = true } +environment = { workspace = true } +eth2 = { workspace = true } +eth2_network_config = { workspace = true } +eth2_wallet = { workspace = true } ethereum_hashing = { workspace = true } ethereum_ssz = { workspace = true } -environment = { workspace = true } -eth2_network_config = { workspace = true } -deposit_contract = { workspace = true } -tree_hash = { workspace = true } -clap_utils = { workspace = true } -lighthouse_network = { workspace = true } -validator_dir = { workspace = true } -lighthouse_version = { workspace = true } -account_utils = { workspace = true } -eth2_wallet = { workspace = true } -eth2 = { workspace = true } -snap = { workspace = true } -beacon_chain = { workspace = true } -store = { workspace = true } -malloc_utils = { workspace = true } -rayon = { workspace = true } execution_layer = { workspace = true } hex = { workspace = true } +lighthouse_network = { workspace = true } +lighthouse_version = { workspace = true } +log = { workspace = true } +malloc_utils = { workspace = true } +rayon = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +serde_yaml = { workspace = true } +sloggers = { workspace = true } +snap = { workspace = true } +state_processing = { workspace = true } +store = { workspace = true } +tree_hash = { workspace = true } +types = { workspace = true } +validator_dir = { workspace = true } [package.metadata.cargo-udeps.ignore] normal = ["malloc_utils"] diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 1c91b18e9c..eda9a2ebf2 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -34,48 +34,47 @@ malloc_utils = { workspace = true, features = ["jemalloc"] } malloc_utils = { workspace = true } [dependencies] -beacon_node = { workspace = true } -slog = { workspace = true } -types = { workspace = true } -bls = { workspace = true } -ethereum_hashing = { workspace = true } -clap = { workspace = true } -environment = { workspace = true } -boot_node = { path = "../boot_node" } -futures = { workspace = true } -validator_client = { workspace = true } account_manager = { "path" = "../account_manager" } -clap_utils = { workspace = true } -eth2_network_config = { workspace = true } -lighthouse_version = { workspace = true } account_utils = { workspace = true } +beacon_node = { workspace = true } +bls = { workspace = true } +boot_node = { path = "../boot_node" } +clap = { workspace = true } +clap_utils = { workspace = true } +database_manager = { path = "../database_manager" } +directory = { workspace = true } +environment = { workspace = true } +eth2_network_config = { workspace = true } +ethereum_hashing = { workspace = true } +futures = { workspace = true } +lighthouse_version = { workspace = true } +logging = { workspace = true } +malloc_utils = { workspace = true } metrics = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde_yaml = { workspace = true } -task_executor = { workspace = true } -malloc_utils = { workspace = true } -directory = { workspace = true } -unused_port = { workspace = true } -database_manager = { path = "../database_manager" } slasher = { workspace = true } +slog = { workspace = true } +task_executor = { workspace = true } +types = { workspace = true } +unused_port = { workspace = true } +validator_client = { workspace = true } validator_manager = { path = "../validator_manager" } -logging = { workspace = true } [dev-dependencies] -tempfile = { workspace = true } -validator_dir = { workspace = true } -slashing_protection = { workspace = true } -lighthouse_network = { workspace = true } -sensitive_url = { workspace = true } +beacon_node_fallback = { workspace = true } +beacon_processor = { workspace = true } eth1 = { workspace = true } eth2 = { workspace = true } -beacon_processor = { workspace = true } -beacon_node_fallback = { workspace = true } initialized_validators = { workspace = true } +lighthouse_network = { workspace = true } +sensitive_url = { workspace = true } +slashing_protection = { workspace = true } +tempfile = { workspace = true } +validator_dir = { workspace = true } zeroize = { workspace = true } - [[test]] name = "lighthouse_tests" path = "tests/main.rs" diff --git a/lighthouse/environment/Cargo.toml b/lighthouse/environment/Cargo.toml index f95751392c..02b8e0b655 100644 --- a/lighthouse/environment/Cargo.toml +++ b/lighthouse/environment/Cargo.toml @@ -6,19 +6,19 @@ edition = { workspace = true } [dependencies] async-channel = { workspace = true } -tokio = { workspace = true } -slog = { workspace = true } -sloggers = { workspace = true } -types = { workspace = true } eth2_config = { workspace = true } -task_executor = { workspace = true } eth2_network_config = { workspace = true } -logging = { workspace = true } -slog-term = { workspace = true } -slog-async = { workspace = true } futures = { workspace = true } -slog-json = "2.3.0" +logging = { workspace = true } serde = { workspace = true } +slog = { workspace = true } +slog-async = { workspace = true } +slog-json = "2.3.0" +slog-term = { workspace = true } +sloggers = { workspace = true } +task_executor = { workspace = true } +tokio = { workspace = true } +types = { workspace = true } [target.'cfg(not(target_family = "unix"))'.dependencies] ctrlc = { version = "3.1.6", features = ["termination"] } diff --git a/slasher/Cargo.toml b/slasher/Cargo.toml index 56a023df0b..fcecc2fc23 100644 --- a/slasher/Cargo.toml +++ b/slasher/Cargo.toml @@ -17,31 +17,31 @@ byteorder = { workspace = true } derivative = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } -flate2 = { version = "1.0.14", features = ["zlib"], default-features = false } -metrics = { workspace = true } filesystem = { workspace = true } +flate2 = { version = "1.0.14", features = ["zlib"], default-features = false } +lmdb-rkv = { git = "https://github.com/sigp/lmdb-rs", rev = "f33845c6469b94265319aac0ed5085597862c27e", optional = true } +lmdb-rkv-sys = { git = "https://github.com/sigp/lmdb-rs", rev = "f33845c6469b94265319aac0ed5085597862c27e", optional = true } lru = { workspace = true } -parking_lot = { workspace = true } -rand = { workspace = true } -safe_arith = { workspace = true } -serde = { workspace = true } -slog = { workspace = true } -tree_hash = { workspace = true } -tree_hash_derive = { workspace = true } -types = { workspace = true } -strum = { workspace = true } -ssz_types = { workspace = true } # MDBX is pinned at the last version with Windows and macOS support. mdbx = { package = "libmdbx", git = "https://github.com/sigp/libmdbx-rs", rev = "e6ff4b9377c1619bcf0bfdf52bee5a980a432a1a", optional = true } -lmdb-rkv = { git = "https://github.com/sigp/lmdb-rs", rev = "f33845c6469b94265319aac0ed5085597862c27e", optional = true } -lmdb-rkv-sys = { git = "https://github.com/sigp/lmdb-rs", rev = "f33845c6469b94265319aac0ed5085597862c27e", optional = true } +metrics = { workspace = true } +parking_lot = { workspace = true } +rand = { workspace = true } redb = { version = "2.1.4", optional = true } +safe_arith = { workspace = true } +serde = { workspace = true } +slog = { workspace = true } +ssz_types = { workspace = true } +strum = { workspace = true } +tree_hash = { workspace = true } +tree_hash_derive = { workspace = true } +types = { workspace = true } [dev-dependencies] +logging = { workspace = true } maplit = { workspace = true } rayon = { workspace = true } tempfile = { workspace = true } -logging = { workspace = true } diff --git a/testing/ef_tests/Cargo.toml b/testing/ef_tests/Cargo.toml index 6012283e11..d93f3a5578 100644 --- a/testing/ef_tests/Cargo.toml +++ b/testing/ef_tests/Cargo.toml @@ -12,28 +12,28 @@ portable = ["beacon_chain/portable"] [dependencies] alloy-primitives = { workspace = true } +beacon_chain = { workspace = true } bls = { workspace = true } compare_fields = { workspace = true } compare_fields_derive = { workspace = true } derivative = { workspace = true } +eth2_network_config = { workspace = true } +ethereum_ssz = { workspace = true } +ethereum_ssz_derive = { workspace = true } +execution_layer = { workspace = true } +fork_choice = { workspace = true } +fs2 = { workspace = true } hex = { workspace = true } kzg = { workspace = true } +logging = { workspace = true } rayon = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde_repr = { workspace = true } serde_yaml = { workspace = true } -eth2_network_config = { workspace = true } -ethereum_ssz = { workspace = true } -ethereum_ssz_derive = { workspace = true } -tree_hash = { workspace = true } -tree_hash_derive = { workspace = true } +snap = { workspace = true } state_processing = { workspace = true } swap_or_not_shuffle = { workspace = true } +tree_hash = { workspace = true } +tree_hash_derive = { workspace = true } types = { workspace = true } -snap = { workspace = true } -fs2 = { workspace = true } -beacon_chain = { workspace = true } -fork_choice = { workspace = true } -execution_layer = { workspace = true } -logging = { workspace = true } diff --git a/testing/eth1_test_rig/Cargo.toml b/testing/eth1_test_rig/Cargo.toml index c76ef91183..9b0ac5ec9b 100644 --- a/testing/eth1_test_rig/Cargo.toml +++ b/testing/eth1_test_rig/Cargo.toml @@ -5,12 +5,12 @@ authors = ["Paul Hauner "] edition = { workspace = true } [dependencies] -tokio = { workspace = true } +deposit_contract = { workspace = true } +ethers-contract = "1.0.2" ethers-core = { workspace = true } ethers-providers = { workspace = true } -ethers-contract = "1.0.2" -types = { workspace = true } -serde_json = { workspace = true } -deposit_contract = { workspace = true } -unused_port = { workspace = true } hex = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +types = { workspace = true } +unused_port = { workspace = true } diff --git a/testing/execution_engine_integration/Cargo.toml b/testing/execution_engine_integration/Cargo.toml index 159561d5dd..28ff944799 100644 --- a/testing/execution_engine_integration/Cargo.toml +++ b/testing/execution_engine_integration/Cargo.toml @@ -5,22 +5,22 @@ edition = { workspace = true } [dependencies] async-channel = { workspace = true } -tempfile = { workspace = true } +deposit_contract = { workspace = true } +ethers-core = { workspace = true } +ethers-providers = { workspace = true } +execution_layer = { workspace = true } +fork_choice = { workspace = true } +futures = { workspace = true } +hex = { workspace = true } +logging = { workspace = true } +reqwest = { workspace = true } +sensitive_url = { workspace = true } serde_json = { workspace = true } task_executor = { workspace = true } +tempfile = { workspace = true } tokio = { workspace = true } -futures = { workspace = true } -execution_layer = { workspace = true } -sensitive_url = { workspace = true } types = { workspace = true } unused_port = { workspace = true } -ethers-providers = { workspace = true } -ethers-core = { workspace = true } -deposit_contract = { workspace = true } -reqwest = { workspace = true } -hex = { workspace = true } -fork_choice = { workspace = true } -logging = { workspace = true } [features] portable = ["types/portable"] diff --git a/testing/node_test_rig/Cargo.toml b/testing/node_test_rig/Cargo.toml index 97e73b8a2f..0d9db528da 100644 --- a/testing/node_test_rig/Cargo.toml +++ b/testing/node_test_rig/Cargo.toml @@ -5,14 +5,14 @@ authors = ["Paul Hauner "] edition = { workspace = true } [dependencies] -environment = { workspace = true } beacon_node = { workspace = true } -types = { workspace = true } -tempfile = { workspace = true } -eth2 = { workspace = true } -validator_client = { workspace = true } beacon_node_fallback = { workspace = true } -validator_dir = { workspace = true, features = ["insecure_keys"] } -sensitive_url = { workspace = true } +environment = { workspace = true } +eth2 = { workspace = true } execution_layer = { workspace = true } +sensitive_url = { workspace = true } +tempfile = { workspace = true } tokio = { workspace = true } +types = { workspace = true } +validator_client = { workspace = true } +validator_dir = { workspace = true, features = ["insecure_keys"] } diff --git a/testing/simulator/Cargo.toml b/testing/simulator/Cargo.toml index 7772523284..77645dba45 100644 --- a/testing/simulator/Cargo.toml +++ b/testing/simulator/Cargo.toml @@ -3,20 +3,19 @@ name = "simulator" version = "0.2.0" authors = ["Paul Hauner "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -node_test_rig = { path = "../node_test_rig" } -execution_layer = { workspace = true } -types = { workspace = true } -parking_lot = { workspace = true } -futures = { workspace = true } -tokio = { workspace = true } -env_logger = { workspace = true } clap = { workspace = true } +env_logger = { workspace = true } +eth2_network_config = { workspace = true } +execution_layer = { workspace = true } +futures = { workspace = true } +kzg = { workspace = true } +node_test_rig = { path = "../node_test_rig" } +parking_lot = { workspace = true } rayon = { workspace = true } sensitive_url = { path = "../../common/sensitive_url" } -eth2_network_config = { workspace = true } serde_json = { workspace = true } -kzg = { workspace = true } +tokio = { workspace = true } +types = { workspace = true } diff --git a/testing/state_transition_vectors/Cargo.toml b/testing/state_transition_vectors/Cargo.toml index 142a657f07..7c29715346 100644 --- a/testing/state_transition_vectors/Cargo.toml +++ b/testing/state_transition_vectors/Cargo.toml @@ -3,15 +3,14 @@ name = "state_transition_vectors" version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -state_processing = { workspace = true } -types = { workspace = true } -ethereum_ssz = { workspace = true } beacon_chain = { workspace = true } +ethereum_ssz = { workspace = true } +state_processing = { workspace = true } tokio = { workspace = true } +types = { workspace = true } [features] -portable = ["beacon_chain/portable"] \ No newline at end of file +portable = ["beacon_chain/portable"] diff --git a/testing/test-test_logger/Cargo.toml b/testing/test-test_logger/Cargo.toml index 63bb87c06e..d2d705f714 100644 --- a/testing/test-test_logger/Cargo.toml +++ b/testing/test-test_logger/Cargo.toml @@ -2,7 +2,6 @@ name = "test-test_logger" version = "0.1.0" edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/testing/web3signer_tests/Cargo.toml b/testing/web3signer_tests/Cargo.toml index 0096d74f64..376aa13406 100644 --- a/testing/web3signer_tests/Cargo.toml +++ b/testing/web3signer_tests/Cargo.toml @@ -2,31 +2,30 @@ name = "web3signer_tests" version = "0.1.0" edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] [dev-dependencies] +account_utils = { workspace = true } async-channel = { workspace = true } +environment = { workspace = true } eth2_keystore = { workspace = true } -types = { workspace = true } +eth2_network_config = { workspace = true } +futures = { workspace = true } +initialized_validators = { workspace = true } +logging = { workspace = true } +parking_lot = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +serde_yaml = { workspace = true } +slashing_protection = { workspace = true } +slot_clock = { workspace = true } +task_executor = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true } -reqwest = { workspace = true } +types = { workspace = true } url = { workspace = true } -slot_clock = { workspace = true } -futures = { workspace = true } -task_executor = { workspace = true } -environment = { workspace = true } -account_utils = { workspace = true } -serde = { workspace = true } -serde_yaml = { workspace = true } -eth2_network_config = { workspace = true } -serde_json = { workspace = true } -zip = { workspace = true } -parking_lot = { workspace = true } -logging = { workspace = true } -initialized_validators = { workspace = true } -slashing_protection = { workspace = true } validator_store = { workspace = true } +zip = { workspace = true } diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index 044a622d54..504d96ae1c 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -17,10 +17,11 @@ beacon_node_fallback = { workspace = true } clap = { workspace = true } clap_utils = { workspace = true } directory = { workspace = true } -doppelganger_service = { workspace = true } dirs = { workspace = true } -eth2 = { workspace = true } +doppelganger_service = { workspace = true } environment = { workspace = true } +eth2 = { workspace = true } +fdlimit = "0.3.0" graffiti_file = { workspace = true } hyper = { workspace = true } initialized_validators = { workspace = true } @@ -29,15 +30,14 @@ monitoring_api = { workspace = true } parking_lot = { workspace = true } reqwest = { workspace = true } sensitive_url = { workspace = true } -slashing_protection = { workspace = true } serde = { workspace = true } +slashing_protection = { workspace = true } slog = { workspace = true } slot_clock = { workspace = true } +tokio = { workspace = true } types = { workspace = true } validator_http_api = { workspace = true } validator_http_metrics = { workspace = true } validator_metrics = { workspace = true } validator_services = { workspace = true } validator_store = { workspace = true } -tokio = { workspace = true } -fdlimit = "0.3.0" diff --git a/validator_client/doppelganger_service/Cargo.toml b/validator_client/doppelganger_service/Cargo.toml index e5f7d3f2ba..66b61a411b 100644 --- a/validator_client/doppelganger_service/Cargo.toml +++ b/validator_client/doppelganger_service/Cargo.toml @@ -17,4 +17,4 @@ types = { workspace = true } [dev-dependencies] futures = { workspace = true } -logging = {workspace = true } +logging = { workspace = true } diff --git a/validator_client/graffiti_file/Cargo.toml b/validator_client/graffiti_file/Cargo.toml index 02e48849d1..8868f5aec8 100644 --- a/validator_client/graffiti_file/Cargo.toml +++ b/validator_client/graffiti_file/Cargo.toml @@ -9,11 +9,11 @@ name = "graffiti_file" path = "src/lib.rs" [dependencies] -serde = { workspace = true } bls = { workspace = true } -types = { workspace = true } +serde = { workspace = true } slog = { workspace = true } +types = { workspace = true } [dev-dependencies] -tempfile = { workspace = true } hex = { workspace = true } +tempfile = { workspace = true } diff --git a/validator_client/http_api/Cargo.toml b/validator_client/http_api/Cargo.toml index 96c836f6f3..76a021ab8c 100644 --- a/validator_client/http_api/Cargo.toml +++ b/validator_client/http_api/Cargo.toml @@ -10,25 +10,25 @@ path = "src/lib.rs" [dependencies] account_utils = { workspace = true } -bls = { workspace = true } beacon_node_fallback = { workspace = true } +bls = { workspace = true } deposit_contract = { workspace = true } directory = { workspace = true } -doppelganger_service = { workspace = true } dirs = { workspace = true } -graffiti_file = { workspace = true } +doppelganger_service = { workspace = true } eth2 = { workspace = true } eth2_keystore = { workspace = true } ethereum_serde_utils = { workspace = true } +filesystem = { workspace = true } +graffiti_file = { workspace = true } initialized_validators = { workspace = true } lighthouse_version = { workspace = true } logging = { workspace = true } parking_lot = { workspace = true } -filesystem = { workspace = true } rand = { workspace = true } +sensitive_url = { workspace = true } serde = { workspace = true } signing_method = { workspace = true } -sensitive_url = { workspace = true } slashing_protection = { workspace = true } slog = { workspace = true } slot_clock = { workspace = true } @@ -39,15 +39,15 @@ tempfile = { workspace = true } tokio = { workspace = true } tokio-stream = { workspace = true } types = { workspace = true } -validator_dir = { workspace = true } -validator_store = { workspace = true } -validator_services = { workspace = true } url = { workspace = true } -warp_utils = { workspace = true } +validator_dir = { workspace = true } +validator_services = { workspace = true } +validator_store = { workspace = true } warp = { workspace = true } +warp_utils = { workspace = true } zeroize = { workspace = true } [dev-dependencies] -itertools = { workspace = true } futures = { workspace = true } +itertools = { workspace = true } rand = { workspace = true, features = ["small_rng"] } diff --git a/validator_client/http_metrics/Cargo.toml b/validator_client/http_metrics/Cargo.toml index a9de26a55b..c29a4d18fa 100644 --- a/validator_client/http_metrics/Cargo.toml +++ b/validator_client/http_metrics/Cargo.toml @@ -5,16 +5,16 @@ edition = { workspace = true } authors = ["Sigma Prime "] [dependencies] +lighthouse_version = { workspace = true } malloc_utils = { workspace = true } -slot_clock = { workspace = true } metrics = { workspace = true } parking_lot = { workspace = true } serde = { workspace = true } slog = { workspace = true } -warp_utils = { workspace = true } -warp = { workspace = true } -lighthouse_version = { workspace = true } +slot_clock = { workspace = true } +types = { workspace = true } +validator_metrics = { workspace = true } validator_services = { workspace = true } validator_store = { workspace = true } -validator_metrics = { workspace = true } -types = { workspace = true } +warp = { workspace = true } +warp_utils = { workspace = true } diff --git a/validator_client/initialized_validators/Cargo.toml b/validator_client/initialized_validators/Cargo.toml index 9c7a3f19d6..05e85261f9 100644 --- a/validator_client/initialized_validators/Cargo.toml +++ b/validator_client/initialized_validators/Cargo.toml @@ -5,23 +5,23 @@ edition = { workspace = true } authors = ["Sigma Prime "] [dependencies] -signing_method = { workspace = true } account_utils = { workspace = true } +bincode = { workspace = true } +bls = { workspace = true } eth2_keystore = { workspace = true } -metrics = { workspace = true } +filesystem = { workspace = true } lockfile = { workspace = true } +metrics = { workspace = true } parking_lot = { workspace = true } +rand = { workspace = true } reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +signing_method = { workspace = true } slog = { workspace = true } +tokio = { workspace = true } types = { workspace = true } url = { workspace = true } validator_dir = { workspace = true } -rand = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -bls = { workspace = true } -tokio = { workspace = true } -bincode = { workspace = true } -filesystem = { workspace = true } validator_metrics = { workspace = true } zeroize = { workspace = true } diff --git a/validator_client/signing_method/Cargo.toml b/validator_client/signing_method/Cargo.toml index 0f3852eff6..3e1a48142f 100644 --- a/validator_client/signing_method/Cargo.toml +++ b/validator_client/signing_method/Cargo.toml @@ -6,12 +6,12 @@ authors = ["Sigma Prime "] [dependencies] eth2_keystore = { workspace = true } +ethereum_serde_utils = { workspace = true } lockfile = { workspace = true } parking_lot = { workspace = true } reqwest = { workspace = true } +serde = { workspace = true } task_executor = { workspace = true } types = { workspace = true } url = { workspace = true } validator_metrics = { workspace = true } -serde = { workspace = true } -ethereum_serde_utils = { workspace = true } diff --git a/validator_client/slashing_protection/Cargo.toml b/validator_client/slashing_protection/Cargo.toml index 6982958bd5..1a098742d8 100644 --- a/validator_client/slashing_protection/Cargo.toml +++ b/validator_client/slashing_protection/Cargo.toml @@ -10,16 +10,16 @@ name = "slashing_protection_tests" path = "tests/main.rs" [dependencies] -tempfile = { workspace = true } -types = { workspace = true } -rusqlite = { workspace = true } -r2d2 = { workspace = true } -r2d2_sqlite = "0.21.0" -serde = { workspace = true } -serde_json = { workspace = true } +arbitrary = { workspace = true, features = ["derive"] } ethereum_serde_utils = { workspace = true } filesystem = { workspace = true } -arbitrary = { workspace = true, features = ["derive"] } +r2d2 = { workspace = true } +r2d2_sqlite = "0.21.0" +rusqlite = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tempfile = { workspace = true } +types = { workspace = true } [dev-dependencies] rayon = { workspace = true } diff --git a/validator_client/validator_services/Cargo.toml b/validator_client/validator_services/Cargo.toml index 7dcd815541..21f0ae2d77 100644 --- a/validator_client/validator_services/Cargo.toml +++ b/validator_client/validator_services/Cargo.toml @@ -6,18 +6,18 @@ authors = ["Sigma Prime "] [dependencies] beacon_node_fallback = { workspace = true } -validator_metrics = { workspace = true } -validator_store = { workspace = true } -graffiti_file = { workspace = true } +bls = { workspace = true } doppelganger_service = { workspace = true } environment = { workspace = true } eth2 = { workspace = true } futures = { workspace = true } +graffiti_file = { workspace = true } parking_lot = { workspace = true } safe_arith = { workspace = true } slog = { workspace = true } slot_clock = { workspace = true } tokio = { workspace = true } -types = { workspace = true } tree_hash = { workspace = true } -bls = { workspace = true } +types = { workspace = true } +validator_metrics = { workspace = true } +validator_store = { workspace = true } diff --git a/validator_manager/Cargo.toml b/validator_manager/Cargo.toml index 36df256841..7cb05616f4 100644 --- a/validator_manager/Cargo.toml +++ b/validator_manager/Cargo.toml @@ -2,28 +2,27 @@ name = "validator_manager" version = "0.1.0" edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -clap = { workspace = true } -types = { workspace = true } -environment = { workspace = true } -eth2_network_config = { workspace = true } -clap_utils = { workspace = true } -eth2_wallet = { workspace = true } account_utils = { workspace = true } +clap = { workspace = true } +clap_utils = { workspace = true } +derivative = { workspace = true } +environment = { workspace = true } +eth2 = { workspace = true } +eth2_network_config = { workspace = true } +eth2_wallet = { workspace = true } +ethereum_serde_utils = { workspace = true } +hex = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -ethereum_serde_utils = { workspace = true } -tree_hash = { workspace = true } -eth2 = { workspace = true } -hex = { workspace = true } tokio = { workspace = true } -derivative = { workspace = true } +tree_hash = { workspace = true } +types = { workspace = true } zeroize = { workspace = true } [dev-dependencies] -tempfile = { workspace = true } regex = { workspace = true } +tempfile = { workspace = true } validator_http_api = { workspace = true } diff --git a/watch/Cargo.toml b/watch/Cargo.toml index 9e8da3b293..41cfb58e28 100644 --- a/watch/Cargo.toml +++ b/watch/Cargo.toml @@ -10,37 +10,36 @@ path = "src/lib.rs" [[bin]] name = "watch" path = "src/main.rs" - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +axum = "0.7" +beacon_node = { workspace = true } +bls = { workspace = true } clap = { workspace = true } clap_utils = { workspace = true } -log = { workspace = true } -env_logger = { workspace = true } -types = { workspace = true } -eth2 = { workspace = true } -beacon_node = { workspace = true } -tokio = { workspace = true } -axum = "0.7" -hyper = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -reqwest = { workspace = true } -url = { workspace = true } -rand = { workspace = true } diesel = { version = "2.0.2", features = ["postgres", "r2d2"] } diesel_migrations = { version = "2.0.0", features = ["postgres"] } -bls = { workspace = true } +env_logger = { workspace = true } +eth2 = { workspace = true } +hyper = { workspace = true } +log = { workspace = true } r2d2 = { workspace = true } +rand = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } serde_yaml = { workspace = true } +tokio = { workspace = true } +types = { workspace = true } +url = { workspace = true } [dev-dependencies] -tokio-postgres = "0.7.5" -http_api = { workspace = true } beacon_chain = { workspace = true } -network = { workspace = true } -testcontainers = "0.15" -unused_port = { workspace = true } -task_executor = { workspace = true } +http_api = { workspace = true } logging = { workspace = true } +network = { workspace = true } +task_executor = { workspace = true } +testcontainers = "0.15" +tokio-postgres = "0.7.5" +unused_port = { workspace = true } From 07e82dabc06f8e0f7d92ca3edf4ca061899e20da Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 19 Dec 2024 16:46:06 +1100 Subject: [PATCH 20/23] Delete OTB verification service (#6631) * Delete OTB verification service * Merge branch 'unstable' into delete-otb --- .../beacon_chain/src/execution_payload.rs | 4 - beacon_node/beacon_chain/src/lib.rs | 1 - .../src/otb_verification_service.rs | 381 ------------------ beacon_node/client/src/builder.rs | 2 - beacon_node/store/src/lib.rs | 2 +- 5 files changed, 1 insertion(+), 389 deletions(-) delete mode 100644 beacon_node/beacon_chain/src/otb_verification_service.rs diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index 5e13f0624d..92d24c53c0 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -7,7 +7,6 @@ //! So, this module contains functions that one might expect to find in other crates, but they live //! here for good reason. -use crate::otb_verification_service::OptimisticTransitionBlock; use crate::{ BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, BlockProductionError, ExecutionPayloadError, @@ -284,9 +283,6 @@ pub async fn validate_merge_block<'a, T: BeaconChainTypes>( "block_hash" => ?execution_payload.parent_hash(), "msg" => "the terminal block/parent was unavailable" ); - // Store Optimistic Transition Block in Database for later Verification - OptimisticTransitionBlock::from_block(block) - .persist_in_store::(&chain.store)?; Ok(()) } else { Err(ExecutionPayloadError::UnverifiedNonOptimisticCandidate.into()) diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 2953516fb1..d9728b9fd4 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -47,7 +47,6 @@ pub mod observed_block_producers; pub mod observed_data_sidecars; pub mod observed_operations; mod observed_slashable; -pub mod otb_verification_service; mod persisted_beacon_chain; mod persisted_fork_choice; mod pre_finalization_cache; diff --git a/beacon_node/beacon_chain/src/otb_verification_service.rs b/beacon_node/beacon_chain/src/otb_verification_service.rs deleted file mode 100644 index 31034a7d59..0000000000 --- a/beacon_node/beacon_chain/src/otb_verification_service.rs +++ /dev/null @@ -1,381 +0,0 @@ -use crate::execution_payload::{validate_merge_block, AllowOptimisticImport}; -use crate::{ - BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, ExecutionPayloadError, - INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON, -}; -use itertools::process_results; -use proto_array::InvalidationOperation; -use slog::{crit, debug, error, info, warn}; -use slot_clock::SlotClock; -use ssz::{Decode, Encode}; -use ssz_derive::{Decode, Encode}; -use state_processing::per_block_processing::is_merge_transition_complete; -use std::sync::Arc; -use store::{DBColumn, Error as StoreError, HotColdDB, KeyValueStore, StoreItem}; -use task_executor::{ShutdownReason, TaskExecutor}; -use tokio::time::sleep; -use tree_hash::TreeHash; -use types::{BeaconBlockRef, EthSpec, Hash256, Slot}; -use DBColumn::OptimisticTransitionBlock as OTBColumn; - -#[derive(Clone, Debug, Decode, Encode, PartialEq)] -pub struct OptimisticTransitionBlock { - root: Hash256, - slot: Slot, -} - -impl OptimisticTransitionBlock { - // types::BeaconBlockRef<'_, ::EthSpec> - pub fn from_block(block: BeaconBlockRef) -> Self { - Self { - root: block.tree_hash_root(), - slot: block.slot(), - } - } - - pub fn root(&self) -> &Hash256 { - &self.root - } - - pub fn slot(&self) -> &Slot { - &self.slot - } - - pub fn persist_in_store(&self, store: A) -> Result<(), StoreError> - where - T: BeaconChainTypes, - A: AsRef>, - { - if store - .as_ref() - .item_exists::(&self.root)? - { - Ok(()) - } else { - store.as_ref().put_item(&self.root, self) - } - } - - pub fn remove_from_store(&self, store: A) -> Result<(), StoreError> - where - T: BeaconChainTypes, - A: AsRef>, - { - store - .as_ref() - .hot_db - .key_delete(OTBColumn.into(), self.root.as_slice()) - } - - fn is_canonical( - &self, - chain: &BeaconChain, - ) -> Result { - Ok(chain - .forwards_iter_block_roots_until(self.slot, self.slot)? - .next() - .transpose()? - .map(|(root, _)| root) - == Some(self.root)) - } -} - -impl StoreItem for OptimisticTransitionBlock { - fn db_column() -> DBColumn { - OTBColumn - } - - fn as_store_bytes(&self) -> Vec { - self.as_ssz_bytes() - } - - fn from_store_bytes(bytes: &[u8]) -> Result { - Ok(Self::from_ssz_bytes(bytes)?) - } -} - -/// The routine is expected to run once per epoch, 1/4th through the epoch. -pub const EPOCH_DELAY_FACTOR: u32 = 4; - -/// Spawns a routine which checks the validity of any optimistically imported transition blocks -/// -/// This routine will run once per epoch, at `epoch_duration / EPOCH_DELAY_FACTOR` after -/// the start of each epoch. -/// -/// The service will not be started if there is no `execution_layer` on the `chain`. -pub fn start_otb_verification_service( - executor: TaskExecutor, - chain: Arc>, -) { - // Avoid spawning the service if there's no EL, it'll just error anyway. - if chain.execution_layer.is_some() { - executor.spawn( - async move { otb_verification_service(chain).await }, - "otb_verification_service", - ); - } -} - -pub fn load_optimistic_transition_blocks( - chain: &BeaconChain, -) -> Result, StoreError> { - process_results( - chain.store.hot_db.iter_column::(OTBColumn), - |iter| { - iter.map(|(_, bytes)| OptimisticTransitionBlock::from_store_bytes(&bytes)) - .collect() - }, - )? -} - -#[derive(Debug)] -pub enum Error { - ForkChoice(String), - BeaconChain(BeaconChainError), - StoreError(StoreError), - NoBlockFound(OptimisticTransitionBlock), -} - -pub async fn validate_optimistic_transition_blocks( - chain: &Arc>, - otbs: Vec, -) -> Result<(), Error> { - let finalized_slot = chain - .canonical_head - .fork_choice_read_lock() - .get_finalized_block() - .map_err(|e| Error::ForkChoice(format!("{:?}", e)))? - .slot; - - // separate otbs into - // non-canonical - // finalized canonical - // unfinalized canonical - let mut non_canonical_otbs = vec![]; - let (finalized_canonical_otbs, unfinalized_canonical_otbs) = process_results( - otbs.into_iter().map(|otb| { - otb.is_canonical(chain) - .map(|is_canonical| (otb, is_canonical)) - }), - |pair_iter| { - pair_iter - .filter_map(|(otb, is_canonical)| { - if is_canonical { - Some(otb) - } else { - non_canonical_otbs.push(otb); - None - } - }) - .partition::, _>(|otb| *otb.slot() <= finalized_slot) - }, - ) - .map_err(Error::BeaconChain)?; - - // remove non-canonical blocks that conflict with finalized checkpoint from the database - for otb in non_canonical_otbs { - if *otb.slot() <= finalized_slot { - otb.remove_from_store::(&chain.store) - .map_err(Error::StoreError)?; - } - } - - // ensure finalized canonical otb are valid, otherwise kill client - for otb in finalized_canonical_otbs { - match chain.get_block(otb.root()).await { - Ok(Some(block)) => { - match validate_merge_block(chain, block.message(), AllowOptimisticImport::No).await - { - Ok(()) => { - // merge transition block is valid, remove it from OTB - otb.remove_from_store::(&chain.store) - .map_err(Error::StoreError)?; - info!( - chain.log, - "Validated merge transition block"; - "block_root" => ?otb.root(), - "type" => "finalized" - ); - } - // The block was not able to be verified by the EL. Leave the OTB in the - // database since the EL is likely still syncing and may verify the block - // later. - Err(BlockError::ExecutionPayloadError( - ExecutionPayloadError::UnverifiedNonOptimisticCandidate, - )) => (), - Err(BlockError::ExecutionPayloadError( - ExecutionPayloadError::InvalidTerminalPoWBlock { .. }, - )) => { - // Finalized Merge Transition Block is Invalid! Kill the Client! - crit!( - chain.log, - "Finalized merge transition block is invalid!"; - "msg" => "You must use the `--purge-db` flag to clear the database and restart sync. \ - You may be on a hostile network.", - "block_hash" => ?block.canonical_root() - ); - let mut shutdown_sender = chain.shutdown_sender(); - if let Err(e) = shutdown_sender.try_send(ShutdownReason::Failure( - INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON, - )) { - crit!( - chain.log, - "Failed to shut down client"; - "error" => ?e, - "shutdown_reason" => INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON - ); - } - } - _ => {} - } - } - Ok(None) => return Err(Error::NoBlockFound(otb)), - // Our database has pruned the payload and the payload was unavailable on the EL since - // the EL is still syncing or the payload is non-canonical. - Err(BeaconChainError::BlockHashMissingFromExecutionLayer(_)) => (), - Err(e) => return Err(Error::BeaconChain(e)), - } - } - - // attempt to validate any non-finalized canonical otb blocks - for otb in unfinalized_canonical_otbs { - match chain.get_block(otb.root()).await { - Ok(Some(block)) => { - match validate_merge_block(chain, block.message(), AllowOptimisticImport::No).await - { - Ok(()) => { - // merge transition block is valid, remove it from OTB - otb.remove_from_store::(&chain.store) - .map_err(Error::StoreError)?; - info!( - chain.log, - "Validated merge transition block"; - "block_root" => ?otb.root(), - "type" => "not finalized" - ); - } - // The block was not able to be verified by the EL. Leave the OTB in the - // database since the EL is likely still syncing and may verify the block - // later. - Err(BlockError::ExecutionPayloadError( - ExecutionPayloadError::UnverifiedNonOptimisticCandidate, - )) => (), - Err(BlockError::ExecutionPayloadError( - ExecutionPayloadError::InvalidTerminalPoWBlock { .. }, - )) => { - // Unfinalized Merge Transition Block is Invalid -> Run process_invalid_execution_payload - warn!( - chain.log, - "Merge transition block invalid"; - "block_root" => ?otb.root() - ); - chain - .process_invalid_execution_payload( - &InvalidationOperation::InvalidateOne { - block_root: *otb.root(), - }, - ) - .await - .map_err(|e| { - warn!( - chain.log, - "Error checking merge transition block"; - "error" => ?e, - "location" => "process_invalid_execution_payload" - ); - Error::BeaconChain(e) - })?; - } - _ => {} - } - } - Ok(None) => return Err(Error::NoBlockFound(otb)), - // Our database has pruned the payload and the payload was unavailable on the EL since - // the EL is still syncing or the payload is non-canonical. - Err(BeaconChainError::BlockHashMissingFromExecutionLayer(_)) => (), - Err(e) => return Err(Error::BeaconChain(e)), - } - } - - Ok(()) -} - -/// Loop until any optimistically imported merge transition blocks have been verified and -/// the merge has been finalized. -async fn otb_verification_service(chain: Arc>) { - let epoch_duration = chain.slot_clock.slot_duration() * T::EthSpec::slots_per_epoch() as u32; - loop { - match chain - .slot_clock - .duration_to_next_epoch(T::EthSpec::slots_per_epoch()) - { - Some(duration) => { - let additional_delay = epoch_duration / EPOCH_DELAY_FACTOR; - sleep(duration + additional_delay).await; - - debug!( - chain.log, - "OTB verification service firing"; - ); - - if !is_merge_transition_complete( - &chain.canonical_head.cached_head().snapshot.beacon_state, - ) { - // We are pre-merge. Nothing to do yet. - continue; - } - - // load all optimistically imported transition blocks from the database - match load_optimistic_transition_blocks(chain.as_ref()) { - Ok(otbs) => { - if otbs.is_empty() { - if chain - .canonical_head - .fork_choice_read_lock() - .get_finalized_block() - .map_or(false, |block| { - block.execution_status.is_execution_enabled() - }) - { - // there are no optimistic blocks in the database, we can exit - // the service since the merge transition is finalized and we'll - // never see another transition block - break; - } else { - debug!( - chain.log, - "No optimistic transition blocks"; - "info" => "waiting for the merge transition to finalize" - ) - } - } - if let Err(e) = validate_optimistic_transition_blocks(&chain, otbs).await { - warn!( - chain.log, - "Error while validating optimistic transition blocks"; - "error" => ?e - ); - } - } - Err(e) => { - error!( - chain.log, - "Error loading optimistic transition blocks"; - "error" => ?e - ); - } - }; - } - None => { - error!(chain.log, "Failed to read slot clock"); - // If we can't read the slot clock, just wait another slot. - sleep(chain.slot_clock.slot_duration()).await; - } - }; - } - debug!( - chain.log, - "No optimistic transition blocks in database"; - "msg" => "shutting down OTB verification service" - ); -} diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index 961f5140f9..7c6a253aca 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -7,7 +7,6 @@ use crate::Client; use beacon_chain::attestation_simulator::start_attestation_simulator_service; use beacon_chain::data_availability_checker::start_availability_cache_maintenance_service; use beacon_chain::graffiti_calculator::start_engine_version_cache_refresh_service; -use beacon_chain::otb_verification_service::start_otb_verification_service; use beacon_chain::proposer_prep_service::start_proposer_prep_service; use beacon_chain::schema_change::migrate_schema; use beacon_chain::{ @@ -970,7 +969,6 @@ where } start_proposer_prep_service(runtime_context.executor.clone(), beacon_chain.clone()); - start_otb_verification_service(runtime_context.executor.clone(), beacon_chain.clone()); start_availability_cache_maintenance_service( runtime_context.executor.clone(), beacon_chain.clone(), diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index 0498c7c1e2..09ae9a32dd 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -332,7 +332,7 @@ pub enum DBColumn { BeaconRandaoMixes, #[strum(serialize = "dht")] DhtEnrs, - /// For Optimistically Imported Merge Transition Blocks + /// DEPRECATED. For Optimistically Imported Merge Transition Blocks #[strum(serialize = "otb")] OptimisticTransitionBlock, /// DEPRECATED. Can be removed once schema v22 is buried by a hard fork. From 502239871508b6a39f27a6fc54c82d46d59f9bca Mon Sep 17 00:00:00 2001 From: chonghe <44791194+chong-he@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:46:10 +0800 Subject: [PATCH 21/23] Revise Siren documentation (#6553) * revise Siren doc * Fix broken links * Fix broken links * broken links * mdlint * mdlint * mdlint again * Merge branch 'unstable' into book-siren * test whether I have the required privs :-) * revise * some minor siren related changes for the book * updates re: `--net=host` * lint * Minor revision * Add note * mdlint * Merge branch 'unstable' into book-siren * Merge branch 'unstable' into book-siren * Merge remote-tracking branch 'origin/unstable' into book-siren * Fix spellcheck * Capital letters SSL --- book/src/SUMMARY.md | 3 +- book/src/api-vc-auth-header.md | 4 +- book/src/lighthouse-ui.md | 1 - book/src/ui-authentication.md | 4 +- book/src/ui-configuration.md | 121 +++++++++++++++++++++++++++------ book/src/ui-faqs.md | 7 +- wordlist.txt | 1 - 7 files changed, 109 insertions(+), 32 deletions(-) diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 02683a1172..44d7702e5f 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -33,9 +33,8 @@ * [Signature Header](./api-vc-sig-header.md) * [Prometheus Metrics](./advanced_metrics.md) * [Lighthouse UI (Siren)](./lighthouse-ui.md) - * [Installation](./ui-installation.md) - * [Authentication](./ui-authentication.md) * [Configuration](./ui-configuration.md) + * [Authentication](./ui-authentication.md) * [Usage](./ui-usage.md) * [FAQs](./ui-faqs.md) * [Advanced Usage](./advanced.md) diff --git a/book/src/api-vc-auth-header.md b/book/src/api-vc-auth-header.md index feb93724c0..f792ee870e 100644 --- a/book/src/api-vc-auth-header.md +++ b/book/src/api-vc-auth-header.md @@ -20,11 +20,11 @@ Authorization: Bearer hGut6B8uEujufDXSmZsT0thnxvdvKFBvh The API token is stored as a file in the `validators` directory. For most users this is `~/.lighthouse/{network}/validators/api-token.txt`, unless overridden using the `--http-token-path` CLI parameter. Here's an -example using the `cat` command to print the token to the terminal, but any +example using the `cat` command to print the token for mainnet to the terminal, but any text editor will suffice: ```bash -cat api-token.txt +cat ~/.lighthouse/mainnet/validators/api-token.txt hGut6B8uEujufDXSmZsT0thnxvdvKFBvh ``` diff --git a/book/src/lighthouse-ui.md b/book/src/lighthouse-ui.md index 106a5e8947..f2662f4a69 100644 --- a/book/src/lighthouse-ui.md +++ b/book/src/lighthouse-ui.md @@ -21,7 +21,6 @@ The UI is currently in active development. It resides in the See the following Siren specific topics for more context-specific information: -- [Installation Guide](./ui-installation.md) - Information to install and run the Lighthouse UI. - [Configuration Guide](./ui-configuration.md) - Explanation of how to setup and configure Siren. - [Authentication Guide](./ui-authentication.md) - Explanation of how Siren authentication works and protects validator actions. diff --git a/book/src/ui-authentication.md b/book/src/ui-authentication.md index 9e3a94db78..81b867bae2 100644 --- a/book/src/ui-authentication.md +++ b/book/src/ui-authentication.md @@ -2,12 +2,12 @@ ## Siren Session -For enhanced security, Siren will require users to authenticate with their session password to access the dashboard. This is crucial because Siren now includes features that can permanently alter the status of user validators. The session password must be set during the [installation](./ui-installation.md) process before running the Docker or local build, either in an `.env` file or via Docker flags. +For enhanced security, Siren will require users to authenticate with their session password to access the dashboard. This is crucial because Siren now includes features that can permanently alter the status of the user's validators. The session password must be set during the [configuration](./ui-configuration.md) process before running the Docker or local build, either in an `.env` file or via Docker flags. ![exit](imgs/ui-session.png) ## Protected Actions -Prior to executing any sensitive validator action, Siren will request authentication of the session password. If you wish to update your password please refer to the Siren [installation process](./ui-installation.md). +Prior to executing any sensitive validator action, Siren will request authentication of the session password. If you wish to update your password please refer to the Siren [configuration process](./ui-configuration.md). ![exit](imgs/ui-auth.png) diff --git a/book/src/ui-configuration.md b/book/src/ui-configuration.md index eeb2c9a51c..34cc9fe7ca 100644 --- a/book/src/ui-configuration.md +++ b/book/src/ui-configuration.md @@ -1,37 +1,116 @@ -# Configuration +# 📦 Installation + +Siren supports any operating system that supports containers and/or NodeJS 18, this includes Linux, MacOS, and Windows. The recommended way of running Siren is by launching the [docker container](https://hub.docker.com/r/sigp/siren). + +## Version Requirement + +To ensure proper functionality, the Siren app requires Lighthouse v4.3.0 or higher. You can find these versions on the [releases](https://github.com/sigp/lighthouse/releases) page of the Lighthouse repository. + +## Configuration Siren requires a connection to both a Lighthouse Validator Client and a Lighthouse Beacon Node. -To enable connection, you must generate .env file based on the provided .env.example - -## Connecting to the Clients Both the Beacon node and the Validator client need to have their HTTP APIs enabled. -These ports should be accessible from Siren. +These ports should be accessible from Siren. This means adding the flag `--http` on both beacon node and validator client. To enable the HTTP API for the beacon node, utilize the `--gui` CLI flag. This action ensures that the HTTP API can be accessed by other software on the same machine. > The Beacon Node must be run with the `--gui` flag set. -If you require accessibility from another machine within the network, configure the `--http-address` to match the local LAN IP of the system running the Beacon Node and Validator Client. +## Running the Docker container (Recommended) -> To access from another machine on the same network (192.168.0.200) set the Beacon Node and Validator Client `--http-address` as `192.168.0.200`. When this is set, the validator client requires the flag `--beacon-nodes http://192.168.0.200:5052` to connect to the beacon node. +We recommend running Siren's container next to your beacon node (on the same server), as it's essentially a webapp that you can access with any browser. -In a similar manner, the validator client requires activation of the `--http` flag, along with the optional consideration of configuring the `--http-address` flag. If `--http-address` flag is set on the Validator Client, then the `--unencrypted-http-transport` flag is required as well. These settings will ensure compatibility with Siren's connectivity requirements. + 1. Create a directory to run Siren: -If you run the Docker container, it will fail to startup if your BN/VC are not accessible, or if you provided a wrong API token. + ```bash + cd ~ + mkdir Siren + cd Siren + ``` -## API Token + 1. Create a configuration file in the `Siren` directory: `nano .env` and insert the following fields to the `.env` file. The field values are given here as an example, modify the fields as necessary. For example, the `API_TOKEN` can be obtained from [`Validator Client Authorization Header`](./api-vc-auth-header.md) -The API Token is a secret key that allows you to connect to the validator -client. The validator client's HTTP API is guarded by this key because it -contains sensitive validator information and the ability to modify -validators. Please see [`Validator Authorization`](./api-vc-auth-header.md) -for further details. + A full example with all possible configuration options can be found [here](https://github.com/sigp/siren/blob/stable/.env.example). -Siren requires this token in order to connect to the Validator client. -The token is located in the default data directory of the validator -client. The default path is -`~/.lighthouse//validators/api-token.txt`. + ``` + BEACON_URL=http://localhost:5052 + VALIDATOR_URL=http://localhost:5062 + API_TOKEN=R6YhbDO6gKjNMydtZHcaCovFbQ0izq5Hk + SESSION_PASSWORD=your_password + ``` -The contents of this file for the desired validator client needs to be -entered. + 1. You can now start Siren with: + + ```bash + docker run --rm -ti --name siren --env-file $PWD/.env --net host sigp/siren + ``` + + Note that, due to the `--net=host` flag, this will expose Siren on ports 3000, 80, and 443. Preferably, only the latter should be accessible. Adjust your firewall and/or skip the flag wherever possible. + + If it fails to start, an error message will be shown. For example, the error + + ``` + http://localhost:5062 unreachable, check settings and connection + ``` + + means that the validator client is not running, or the `--http` flag is not provided, or otherwise inaccessible from within the container. Another common error is: + + ``` + validator api issue, server response: 403 + ``` + + which means that the API token is incorrect. Check that you have provided the correct token in the field `API_TOKEN` in `.env`. + + When Siren has successfully started, you should see the log `LOG [NestApplication] Nest application successfully started +118ms`, indicating that Siren has started. + + 1. Siren is now accessible at `https://` (when used with `--net=host`). You will get a warning about an invalid certificate, this can be safely ignored. + + > Note: We recommend setting a strong password when running Siren to protect it from unauthorized access. + +Advanced users can mount their own certificates or disable SSL altogether, see the `SSL Certificates` section below. + +## Building From Source + +### Docker + +The docker image can be built with the following command: +`docker build -f Dockerfile -t siren .` + +### Building locally + +To build from source, ensure that your system has `Node v18.18` and `yarn` installed. + +#### Build and run the backend + +Navigate to the backend directory `cd backend`. Install all required Node packages by running `yarn`. Once the installation is complete, compile the backend with `yarn build`. Deploy the backend in a production environment, `yarn start:production`. This ensures optimal performance. + +#### Build and run the frontend + +After initializing the backend, return to the root directory. Install all frontend dependencies by executing `yarn`. Build the frontend using `yarn build`. Start the frontend production server with `yarn start`. + +This will allow you to access siren at `http://localhost:3000` by default. + +## Advanced configuration + +### About self-signed SSL certificates + +By default, internally, Siren is running on port 80 (plain, behind nginx), port 3000 (plain, direct) and port 443 (with SSL, behind nginx)). Siren will generate and use a self-signed certificate on startup. This will generate a security warning when you try to access the interface. We recommend to only disable SSL if you would access Siren over a local LAN or otherwise highly trusted or encrypted network (i.e. VPN). + +#### Generating persistent SSL certificates and installing them to your system + +[mkcert](https://github.com/FiloSottile/mkcert) is a tool that makes it super easy to generate a self-signed certificate that is trusted by your browser. + +To use it for `siren`, install it following the instructions. Then, run `mkdir certs; mkcert -cert-file certs/cert.pem -key-file certs/key.pem 127.0.0.1 localhost` (add or replace any IP or hostname that you would use to access it at the end of this command). +To use these generated certificates, add this to to your `docker run` command: `-v $PWD/certs:/certs` + +The nginx SSL config inside Siren's container expects 3 files: `/certs/cert.pem` `/certs/key.pem` `/certs/key.pass`. If `/certs/cert.pem` does not exist, it will generate a self-signed certificate as mentioned above. If `/certs/cert.pem` does exist, it will attempt to use your provided or persisted certificates. + +### Configuration through environment variables + +For those who prefer to use environment variables to configure Siren instead of using an `.env` file, this is fully supported. In some cases this may even be preferred. + +#### Docker installed through `snap` + +If you installed Docker through a snap (i.e. on Ubuntu), Docker will have trouble accessing the `.env` file. In this case it is highly recommended to pass the config to the container with environment variables. +Note that the defaults in `.env.example` will be used as fallback, if no other value is provided. diff --git a/book/src/ui-faqs.md b/book/src/ui-faqs.md index 0887875316..29de889e5f 100644 --- a/book/src/ui-faqs.md +++ b/book/src/ui-faqs.md @@ -10,15 +10,16 @@ The required API token may be found in the default data directory of the validat ## 3. How do I fix the Node Network Errors? -If you receive a red notification with a BEACON or VALIDATOR NODE NETWORK ERROR you can refer to the lighthouse ui configuration and [`connecting to clients section`](./ui-configuration.md#connecting-to-the-clients). +If you receive a red notification with a BEACON or VALIDATOR NODE NETWORK ERROR you can refer to the lighthouse ui [`configuration`](./ui-configuration.md#configuration). ## 4. How do I connect Siren to Lighthouse from a different computer on the same network? -Siren is a webapp, you can access it like any other website. We don't recommend exposing it to the internet; if you require remote access a VPN or (authenticated) reverse proxy is highly recommended. +Siren is a webapp, you can access it like any other website. We don't recommend exposing it to the internet; if you require remote access a VPN or (authenticated) reverse proxy is highly recommended. +That being said, it is entirely possible to have it published over the internet, how to do that goes well beyond the scope of this document but we want to emphasize once more the need for *at least* SSL encryption if you choose to do so. ## 5. How can I use Siren to monitor my validators remotely when I am not at home? -Most contemporary home routers provide options for VPN access in various ways. A VPN permits a remote computer to establish a connection with internal computers within a home network. With a VPN configuration in place, connecting to the VPN enables you to treat your computer as if it is part of your local home network. The connection process involves following the setup steps for connecting via another machine on the same network on the Siren configuration page and [`connecting to clients section`](./ui-configuration.md#connecting-to-the-clients). +Most contemporary home routers provide options for VPN access in various ways. A VPN permits a remote computer to establish a connection with internal computers within a home network. With a VPN configuration in place, connecting to the VPN enables you to treat your computer as if it is part of your local home network. The connection process involves following the setup steps for connecting via another machine on the same network on the Siren configuration page and [`configuration`](./ui-configuration.md#configuration). ## 6. Does Siren support reverse proxy or DNS named addresses? diff --git a/wordlist.txt b/wordlist.txt index f06c278866..6287366cbc 100644 --- a/wordlist.txt +++ b/wordlist.txt @@ -194,7 +194,6 @@ rc reimport resync roadmap -runtime rustfmt rustup schemas From 42c64a2744759b7a0ef9852b0e8caf3b3cb4e7db Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 19 Dec 2024 14:09:44 +0700 Subject: [PATCH 22/23] Ensure non-zero bits for each committee bitfield comprising an aggregate (#6603) * add new validation --- .../src/common/get_attesting_indices.rs | 16 +++++++++++----- consensus/types/src/beacon_state.rs | 1 + 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/consensus/state_processing/src/common/get_attesting_indices.rs b/consensus/state_processing/src/common/get_attesting_indices.rs index b131f7679a..842adce431 100644 --- a/consensus/state_processing/src/common/get_attesting_indices.rs +++ b/consensus/state_processing/src/common/get_attesting_indices.rs @@ -103,14 +103,14 @@ pub mod attesting_indices_electra { let committee_count_per_slot = committees.len() as u64; let mut participant_count = 0; - for index in committee_indices { + for committee_index in committee_indices { let beacon_committee = committees - .get(index as usize) - .ok_or(Error::NoCommitteeFound(index))?; + .get(committee_index as usize) + .ok_or(Error::NoCommitteeFound(committee_index))?; // This check is new to the spec's `process_attestation` in Electra. - if index >= committee_count_per_slot { - return Err(BeaconStateError::InvalidCommitteeIndex(index)); + if committee_index >= committee_count_per_slot { + return Err(BeaconStateError::InvalidCommitteeIndex(committee_index)); } participant_count.safe_add_assign(beacon_committee.committee.len() as u64)?; let committee_attesters = beacon_committee @@ -127,6 +127,12 @@ pub mod attesting_indices_electra { }) .collect::>(); + // Require at least a single non-zero bit for each attesting committee bitfield. + // This check is new to the spec's `process_attestation` in Electra. + if committee_attesters.is_empty() { + return Err(BeaconStateError::EmptyCommittee); + } + attesting_indices.extend(committee_attesters); committee_offset.safe_add_assign(beacon_committee.committee.len())?; } diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 77b72b209c..ad4484b86a 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -59,6 +59,7 @@ pub enum Error { UnknownValidator(usize), UnableToDetermineProducer, InvalidBitfield, + EmptyCommittee, ValidatorIsWithdrawable, ValidatorIsInactive { val_index: usize, From 7e0cddef321c2a069582c65b58e5f46590d60c49 Mon Sep 17 00:00:00 2001 From: Akihito Nakano Date: Tue, 24 Dec 2024 10:38:56 +0900 Subject: [PATCH 23/23] Make sure we have fanout peers when publish (#6738) * Ensure that `fanout_peers` is always non-empty if it's `Some` --- .../lighthouse_network/gossipsub/src/behaviour.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs b/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs index aafd869bee..c4e20e4397 100644 --- a/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs +++ b/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs @@ -679,9 +679,15 @@ where // Gossipsub peers None => { tracing::debug!(topic=%topic_hash, "Topic not in the mesh"); + // `fanout_peers` is always non-empty if it's `Some`. + let fanout_peers = self + .fanout + .get(&topic_hash) + .map(|peers| if peers.is_empty() { None } else { Some(peers) }) + .unwrap_or(None); // If we have fanout peers add them to the map. - if self.fanout.contains_key(&topic_hash) { - for peer in self.fanout.get(&topic_hash).expect("Topic must exist") { + if let Some(peers) = fanout_peers { + for peer in peers { recipient_peers.insert(*peer); } } else {