diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 891a32a6d04..6ce0c00d10f 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -2857,6 +2857,15 @@ impl BeaconChain { chain_segment: Vec>, notify_execution_layer: NotifyExecutionLayer, ) -> ChainSegmentResult { + for block in chain_segment.iter() { + if let Err(error) = self.check_invalid_block_roots(block.block_root()) { + return ChainSegmentResult::Failed { + imported_blocks: vec![], + error, + }; + } + } + let mut imported_blocks = vec![]; // Filter uninteresting blocks from the chain segment in a blocking task. @@ -3340,6 +3349,15 @@ impl BeaconChain { self.remove_notified(&block_root, r) } + /// Check for known and configured invalid block roots before processing. + pub fn check_invalid_block_roots(&self, block_root: Hash256) -> Result<(), BlockError> { + if self.config.invalid_block_roots.contains(&block_root) { + Err(BlockError::KnownInvalidExecutionPayload(block_root)) + } else { + Ok(()) + } + } + /// Returns `Ok(block_root)` if the given `unverified_block` was successfully verified and /// imported into the chain. /// diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 12652763763..1bac6cac0aa 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -282,6 +282,9 @@ pub enum BlockError { /// problems to worry about than losing peers, and we're doing the network a favour by /// disconnecting. ParentExecutionPayloadInvalid { parent_root: Hash256 }, + /// This is a known invalid block that was listed in Lighthouses configuration. + /// At the moment this error is only relevant as part of the Holesky network recovery efforts. + KnownInvalidExecutionPayload(Hash256), /// The block is a slashable equivocation from the proposer. /// /// ## Peer scoring @@ -862,6 +865,9 @@ impl GossipVerifiedBlock { return Err(BlockError::DuplicateFullyImported(block_root)); } + // Do not process a block that is known to be invalid. + chain.check_invalid_block_roots(block_root)?; + // Do not process a block that doesn't descend from the finalized root. // // We check this *before* we load the parent so that we can return a more detailed error. @@ -1081,6 +1087,9 @@ impl SignatureVerifiedBlock { .fork_name(&chain.spec) .map_err(BlockError::InconsistentFork)?; + // Check whether the block is a banned block prior to loading the parent. + chain.check_invalid_block_roots(block_root)?; + let (mut parent, block) = load_parent(block, chain)?; let state = cheap_state_advance_to_obtain_committees::<_, BlockError>( diff --git a/beacon_node/beacon_chain/src/chain_config.rs b/beacon_node/beacon_chain/src/chain_config.rs index 4ca2db2aaaa..d45f0b5cc87 100644 --- a/beacon_node/beacon_chain/src/chain_config.rs +++ b/beacon_node/beacon_chain/src/chain_config.rs @@ -1,7 +1,8 @@ pub use proto_array::{DisallowedReOrgOffsets, ReOrgThreshold}; use serde::{Deserialize, Serialize}; -use std::time::Duration; -use types::{Checkpoint, Epoch}; +use std::str::FromStr; +use std::{collections::HashSet, sync::LazyLock, time::Duration}; +use types::{Checkpoint, Epoch, Hash256}; pub const DEFAULT_RE_ORG_HEAD_THRESHOLD: ReOrgThreshold = ReOrgThreshold(20); pub const DEFAULT_RE_ORG_PARENT_THRESHOLD: ReOrgThreshold = ReOrgThreshold(160); @@ -19,6 +20,12 @@ pub const FORK_CHOICE_LOOKAHEAD_FACTOR: u32 = 24; /// Default sync tolerance epochs. pub const DEFAULT_SYNC_TOLERANCE_EPOCHS: u64 = 2; +/// Invalid block root to be banned from processing and importing on Holesky network by default. +pub static INVALID_HOLESKY_BLOCK_ROOT: LazyLock = LazyLock::new(|| { + Hash256::from_str("2db899881ed8546476d0b92c6aa9110bea9a4cd0dbeb5519eb0ea69575f1f359") + .expect("valid block root") +}); + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] pub struct ChainConfig { /// Maximum number of slots to skip when importing an attestation. @@ -100,6 +107,11 @@ pub struct ChainConfig { /// The max distance between the head block and the current slot at which Lighthouse will /// consider itself synced and still serve validator-related requests. pub sync_tolerance_epochs: u64, + /// Block roots of "banned" blocks which Lighthouse will refuse to import. + /// + /// On Holesky there is a block which is added to this set by default but which can be removed + /// by using `--invalid-block-roots ""`. + pub invalid_block_roots: HashSet, } impl Default for ChainConfig { @@ -136,6 +148,7 @@ impl Default for ChainConfig { blob_publication_batches: 4, blob_publication_batch_interval: Duration::from_millis(300), sync_tolerance_epochs: DEFAULT_SYNC_TOLERANCE_EPOCHS, + invalid_block_roots: HashSet::new(), } } } 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 090b963cbc2..0956c153a68 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -1457,6 +1457,7 @@ impl NetworkBeaconProcessor { | Err(e @ BlockError::InconsistentFork(_)) | Err(e @ BlockError::ExecutionPayloadError(_)) | Err(e @ BlockError::ParentExecutionPayloadInvalid { .. }) + | Err(e @ BlockError::KnownInvalidExecutionPayload(_)) | Err(e @ BlockError::GenesisBlock) => { warn!(self.log, "Could not verify block for gossip. Rejecting the block"; "error" => %e); 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 338f2bc4c8b..eb03d3c01bc 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -800,6 +800,18 @@ impl NetworkBeaconProcessor { peer_action: Some(PeerAction::LowToleranceError), }) } + // Penalise peers for sending us banned blocks. + BlockError::KnownInvalidExecutionPayload(block_root) => { + warn!( + self.log, + "Received block known to be invalid"; + "block_root" => ?block_root, + ); + Err(ChainSegmentFailed { + message: format!("Banned block: {block_root:?}"), + peer_action: Some(PeerAction::Fatal), + }) + } other => { debug!( self.log, "Invalid block received"; diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index a8f626f0647..8fd8ea9c7bc 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -1621,5 +1621,13 @@ pub fn cli_app() -> Command { .action(ArgAction::Set) .display_order(0) ) + .arg( + Arg::new("invalid-block-roots") + .long("invalid-block-roots") + .value_name("FILE") + .help("Path to a comma separated file containing block roots that should be treated as invalid during block verification.") + .action(ArgAction::Set) + .hide(true) + ) .group(ArgGroup::new("enable_http").args(["http", "gui", "staking"]).multiple(true)) } diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 494f8e88828..f1db7590e89 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -2,7 +2,7 @@ use account_utils::{read_input_from_user, STDIN_INPUTS_FLAG}; use beacon_chain::chain_config::{ DisallowedReOrgOffsets, ReOrgThreshold, DEFAULT_PREPARE_PAYLOAD_LOOKAHEAD_FACTOR, DEFAULT_RE_ORG_HEAD_THRESHOLD, DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, - DEFAULT_RE_ORG_PARENT_THRESHOLD, + DEFAULT_RE_ORG_PARENT_THRESHOLD, INVALID_HOLESKY_BLOCK_ROOT, }; use beacon_chain::graffiti_calculator::GraffitiOrigin; use beacon_chain::TrustedSetup; @@ -20,9 +20,10 @@ use lighthouse_network::{multiaddr::Protocol, Enr, Multiaddr, NetworkConfig, Pee use sensitive_url::SensitiveUrl; use slog::{info, warn, Logger}; use std::cmp::max; +use std::collections::HashSet; use std::fmt::Debug; use std::fs; -use std::io::IsTerminal; +use std::io::{IsTerminal, Read}; use std::net::Ipv6Addr; use std::net::{IpAddr, Ipv4Addr, ToSocketAddrs}; use std::num::NonZeroU16; @@ -903,6 +904,40 @@ pub fn get_config( .max_gossip_aggregate_batch_size = clap_utils::parse_required(cli_args, "beacon-processor-aggregate-batch-size")?; + if let Some(invalid_block_roots_file_path) = + clap_utils::parse_optional::(cli_args, "invalid-block-roots")? + { + let mut file = std::fs::File::open(invalid_block_roots_file_path) + .map_err(|e| format!("Failed to open invalid-block-roots file: {}", e))?; + let mut contents = String::new(); + file.read_to_string(&mut contents) + .map_err(|e| format!("Failed to read invalid-block-roots file {}", e))?; + let invalid_block_roots: HashSet = contents + .split(',') + .filter_map( + |s| match Hash256::from_str(s.strip_prefix("0x").unwrap_or(s).trim()) { + Ok(block_root) => Some(block_root), + Err(e) => { + warn!( + log, + "Unable to parse invalid block root"; + "block_root" => s, + "error" => ?e, + ); + None + } + }, + ) + .collect(); + client_config.chain.invalid_block_roots = invalid_block_roots; + } else if spec + .config_name + .as_ref() + .is_some_and(|network_name| network_name == "holesky") + { + client_config.chain.invalid_block_roots = HashSet::from([*INVALID_HOLESKY_BLOCK_ROOT]); + } + Ok(client_config) } diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 3cbb7d4e15c..cc4262a00f9 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -2773,3 +2773,43 @@ fn beacon_node_backend_override() { assert_eq!(config.store.backend, BeaconNodeBackend::LevelDb); }); } + +#[test] +fn invalid_block_roots_flag() { + let dir = TempDir::new().expect("Unable to create temporary directory"); + let mut file = + File::create(dir.path().join("invalid-block-roots")).expect("Unable to create file"); + file.write_all(b"2db899881ed8546476d0b92c6aa9110bea9a4cd0dbeb5519eb0ea69575f1f359, 2db899881ed8546476d0b92c6aa9110bea9a4cd0dbeb5519eb0ea69575f1f358, 0x3db899881ed8546476d0b92c6aa9110bea9a4cd0dbeb5519eb0ea69575f1f358") + .expect("Unable to write to file"); + CommandLineTest::new() + .flag( + "invalid-block-roots", + dir.path().join("invalid-block-roots").as_os_str().to_str(), + ) + .run_with_zero_port() + .with_config(|config| assert_eq!(config.chain.invalid_block_roots.len(), 3)) +} + +#[test] +fn invalid_block_roots_default_holesky() { + use beacon_node::beacon_chain::chain_config::INVALID_HOLESKY_BLOCK_ROOT; + CommandLineTest::new() + .flag("network", Some("holesky")) + .run_with_zero_port() + .with_config(|config| { + assert_eq!(config.chain.invalid_block_roots.len(), 1); + assert!(config + .chain + .invalid_block_roots + .contains(&*INVALID_HOLESKY_BLOCK_ROOT)); + }) +} + +#[test] +fn invalid_block_roots_default_mainnet() { + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| { + assert!(config.chain.invalid_block_roots.is_empty()); + }) +}