From 448f36d2c1e0727ad29637229e094ec9e7d98b50 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Tue, 22 Apr 2025 12:46:31 -0700 Subject: [PATCH 01/18] Add initial weak subjectivity check --- beacon_node/beacon_chain/src/builder.rs | 18 +++++++++++++++ beacon_node/src/lib.rs | 7 +----- beacon_node/store/src/hot_cold_store.rs | 29 +++++++++++++++++++++++++ consensus/types/src/beacon_state.rs | 24 ++++++++++++++++++++ 4 files changed, 72 insertions(+), 6 deletions(-) diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 812dcbeda7d..286b29d75e8 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -816,6 +816,24 @@ where )); } + // Check if the store is within the weak subjectivity period + if let Some(ws_checkpoint) = self.chain_config.weak_subjectivity_checkpoint { + let Ok(finalized_block) = fork_choice.get_finalized_block() else { + panic!("TODO(ws)") + }; + if !store.is_within_weak_subjectivity_period( + ws_checkpoint, + finalized_block.slot, + finalized_block.state_root, + head_snapshot + .beacon_state + .slot() + .epoch(E::slots_per_epoch()), + ) { + panic!("TODO(ws)") + } + }; + let validator_pubkey_cache = self .validator_pubkey_cache .map(|mut validator_pubkey_cache| { diff --git a/beacon_node/src/lib.rs b/beacon_node/src/lib.rs index a7f92434ce3..7ead7ba52b1 100644 --- a/beacon_node/src/lib.rs +++ b/beacon_node/src/lib.rs @@ -29,14 +29,9 @@ pub type ProductionClient = Client< >, >; -/// The beacon node `Client` that will be used in production. +/// The beacon node `Client` that is used in production. /// /// Generic over some `EthSpec`. -/// -/// ## Notes: -/// -/// Despite being titled `Production...`, this code is not ready for production. The name -/// demonstrates an intention, not a promise. pub struct ProductionBeaconNode(ProductionClient); impl ProductionBeaconNode { diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 362c5d8014e..26b19eb9a27 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -2944,6 +2944,35 @@ impl, Cold: ItemStore> HotColdDB Ok(()) } + + pub fn is_within_weak_subjectivity_period( + &self, + ws_checkpoint: Checkpoint, + fc_slot: Slot, + fc_state_root: Hash256, + current_epoch: Epoch, + ) -> bool { + let Ok(Some(finalized_state)) = self.get_state(&fc_state_root, Some(fc_slot), true) else { + // we cant get a finalized state from the db, we should force exit here? + return true; + }; + + if finalized_state.latest_block_header().state_root != ws_checkpoint.root { + return true; + } + + let finalized_epoch = finalized_state.slot().epoch(E::slots_per_epoch()); + + if finalized_epoch != ws_checkpoint.epoch { + return false; + } + + let Ok(ws_period) = finalized_state.compute_weak_subjectivity_period(&self.spec) else { + // TODO(ws) failed to calculate ws period, log and continue? + return true; + }; + current_epoch <= finalized_epoch + ws_period + } } /// Advance the split point of the store, copying new finalized states to the freezer. diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 4aed79898d3..4bf857f0319 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -2505,6 +2505,30 @@ impl BeaconState { Ok(()) } + + /// Returns the weak subjectivity period for `self`. This computation takes into + /// account the effect of validator set churn (bounded by `get_balance_churn_limit()` per epoch) + /// A detailed calculation can be found at: + /// https://notes.ethereum.org/@CarlBeek/electra_weak_subjectivity + pub fn compute_weak_subjectivity_period(&self, spec: &ChainSpec) -> Result { + // `SAFETY_DECAY` is defined as the maximum percentage tolerable loss in the one-third + // safety margin of FFG finality. Thus, any attack exploiting the Weak Subjectivity Period has + // a safety margin of at least `1/3 - SAFETY_DECAY/100`. + // Spec: https://github.com/ethereum/consensus-specs/blob/1937aff86b41b5171a9bc3972515986f1bbbf303/specs/phase0/weak-subjectivity.md?plain=1#L50-L71 + // TODO(ws) move this to config + const SAFETY_DECAY: u64 = 10; + + let total_active_balance = self.get_total_active_balance()?; + let balance_churn_limit = self.get_balance_churn_limit(spec)?; + let epochs_for_validator_set_churn = SAFETY_DECAY + .safe_mul(total_active_balance)? + .safe_div(balance_churn_limit.safe_mul(200)?)?; + let weak_subjectivity_period = spec + .min_validator_withdrawability_delay + .safe_mul(epochs_for_validator_set_churn)?; + + Ok(weak_subjectivity_period) + } } impl BeaconState { From 48ee2629d08ebfa8fed1fd62f47342f314b9d6a6 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Wed, 23 Apr 2025 10:27:08 -0700 Subject: [PATCH 02/18] Add ignore-ws-check flag and changes based on feedback --- beacon_node/beacon_chain/src/builder.rs | 41 ++++++++++++-------- beacon_node/beacon_chain/src/chain_config.rs | 4 ++ beacon_node/src/cli.rs | 11 ++++++ beacon_node/src/config.rs | 2 + beacon_node/store/src/hot_cold_store.rs | 29 -------------- lighthouse/tests/beacon_node.rs | 15 +++++++ 6 files changed, 56 insertions(+), 46 deletions(-) diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 286b29d75e8..8e517b9cd1a 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -40,7 +40,7 @@ use std::sync::Arc; use std::time::Duration; use store::{Error as StoreError, HotColdDB, ItemStore, KeyValueStoreOp}; use task_executor::{ShutdownReason, TaskExecutor}; -use tracing::{debug, error, info}; +use tracing::{debug, error, info, warn}; use types::{ BeaconBlock, BeaconState, BlobSidecarList, ChainSpec, Checkpoint, DataColumnSidecarList, Epoch, EthSpec, FixedBytesExtended, Hash256, Signature, SignedBeaconBlock, Slot, @@ -816,23 +816,30 @@ where )); } - // Check if the store is within the weak subjectivity period - if let Some(ws_checkpoint) = self.chain_config.weak_subjectivity_checkpoint { - let Ok(finalized_block) = fork_choice.get_finalized_block() else { - panic!("TODO(ws)") - }; - if !store.is_within_weak_subjectivity_period( - ws_checkpoint, - finalized_block.slot, - finalized_block.state_root, - head_snapshot - .beacon_state - .slot() - .epoch(E::slots_per_epoch()), - ) { - panic!("TODO(ws)") - } + // Check if the head snapshot is within the weak subjectivity period + let head_state = &head_snapshot.beacon_state; + let Ok(ws_period) = head_state.compute_weak_subjectivity_period(&self.spec) else { + return Err(format!( + "Unable to compute the weak subjectivity period at the head snapshot slot: {:?}", + head_state.slot() + )); }; + if current_slot.epoch(E::slots_per_epoch()) + > head_state.slot().epoch(E::slots_per_epoch()) + ws_period + { + if self.chain_config.ignore_ws_check { + warn!( + head_slot=%head_state.slot(), + %current_slot, + "The current head state is outside the weak subjectivity period. It is highly recommended to purge your db and \ + checkpoint sync." + ) + } + return Err( + "The current head state is outside the weak subjectivity period. It is highly recommended to purge your db and \ + checkpoint sync. Alternatively you can accept the risks and ignore this error with the --ignore-ws-check flag.".to_string() + ); + } let validator_pubkey_cache = self .validator_pubkey_cache diff --git a/beacon_node/beacon_chain/src/chain_config.rs b/beacon_node/beacon_chain/src/chain_config.rs index 808c96d9650..e5dc499cdf1 100644 --- a/beacon_node/beacon_chain/src/chain_config.rs +++ b/beacon_node/beacon_chain/src/chain_config.rs @@ -116,6 +116,9 @@ pub struct ChainConfig { /// 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, + + /// When set to true, the beacon node can be started even if the head state is outside the weak subjectivity period. + pub ignore_ws_check: bool, } impl Default for ChainConfig { @@ -155,6 +158,7 @@ impl Default for ChainConfig { block_publishing_delay: None, data_column_publishing_delay: None, invalid_block_roots: HashSet::new(), + ignore_ws_check: false, } } } diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 7d086dcc326..f5d858b6670 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -1414,6 +1414,17 @@ pub fn cli_app() -> Command { .help_heading(FLAG_HEADER) .display_order(0) ) + .arg( + Arg::new("ignore-ws-check") + .long("ignore-ws-check") + .help("The Weak Subjectivity Period is the the maximum time a node can be offline and still \ + safely sync back to the canonical chain without the risk of falling victim to long-range attacks. \ + This flag disables the Weak Subjectivity check at startup, allowing users to run a node whose current head snapshot \ + is outside the Weak Subjectivity Period. It is unsafe to disable the Weak Subjectivity check at startup.") + .action(ArgAction::SetTrue) + .help_heading(FLAG_HEADER) + .display_order(0) + ) .arg( Arg::new("builder-fallback-skips") .long("builder-fallback-skips") diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index e887aa9abce..e18d0188a34 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -834,6 +834,8 @@ pub fn get_config( client_config.chain.paranoid_block_proposal = cli_args.get_flag("paranoid-block-proposal"); + client_config.chain.ignore_ws_check = cli_args.get_flag("ignore-ws-check"); + /* * Builder fallback configs. */ diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 26b19eb9a27..362c5d8014e 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -2944,35 +2944,6 @@ impl, Cold: ItemStore> HotColdDB Ok(()) } - - pub fn is_within_weak_subjectivity_period( - &self, - ws_checkpoint: Checkpoint, - fc_slot: Slot, - fc_state_root: Hash256, - current_epoch: Epoch, - ) -> bool { - let Ok(Some(finalized_state)) = self.get_state(&fc_state_root, Some(fc_slot), true) else { - // we cant get a finalized state from the db, we should force exit here? - return true; - }; - - if finalized_state.latest_block_header().state_root != ws_checkpoint.root { - return true; - } - - let finalized_epoch = finalized_state.slot().epoch(E::slots_per_epoch()); - - if finalized_epoch != ws_checkpoint.epoch { - return false; - } - - let Ok(ws_period) = finalized_state.compute_weak_subjectivity_period(&self.spec) else { - // TODO(ws) failed to calculate ws period, log and continue? - return true; - }; - current_epoch <= finalized_epoch + ws_period - } } /// Advance the split point of the store, copying new finalized states to the freezer. diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index ea4716c0103..009a6d6fcd2 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -297,6 +297,21 @@ fn paranoid_block_proposal_on() { .with_config(|config| assert!(config.chain.paranoid_block_proposal)); } +#[test] +fn ignore_ws_check_enabled() { + CommandLineTest::new() + .flag("ignore-ws-check", None) + .run_with_zero_port() + .with_config(|config| assert!(config.chain.ignore_ws_check)); +} + +#[test] +fn ignore_ws_check_default() { + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| assert!(!config.chain.ignore_ws_check)); +} + #[test] fn reset_payload_statuses_default() { CommandLineTest::new() From 6ac8d9964249e6ed5c596afcb6d18d2775ce5171 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Wed, 23 Apr 2025 11:19:15 -0700 Subject: [PATCH 03/18] Add pre-electra calculations --- consensus/types/src/beacon_state.rs | 101 +++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 11 deletions(-) diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 4bf857f0319..1cf2c9f2bc7 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -2515,19 +2515,98 @@ impl BeaconState { // safety margin of FFG finality. Thus, any attack exploiting the Weak Subjectivity Period has // a safety margin of at least `1/3 - SAFETY_DECAY/100`. // Spec: https://github.com/ethereum/consensus-specs/blob/1937aff86b41b5171a9bc3972515986f1bbbf303/specs/phase0/weak-subjectivity.md?plain=1#L50-L71 - // TODO(ws) move this to config const SAFETY_DECAY: u64 = 10; - let total_active_balance = self.get_total_active_balance()?; - let balance_churn_limit = self.get_balance_churn_limit(spec)?; - let epochs_for_validator_set_churn = SAFETY_DECAY - .safe_mul(total_active_balance)? - .safe_div(balance_churn_limit.safe_mul(200)?)?; - let weak_subjectivity_period = spec - .min_validator_withdrawability_delay - .safe_mul(epochs_for_validator_set_churn)?; - - Ok(weak_subjectivity_period) + let fork_name = self.fork_name_unchecked(); + + if fork_name.electra_enabled() { + // spec: https://github.com/ethereum/consensus-specs/blob/1937aff86b41b5171a9bc3972515986f1bbbf303/specs/electra/weak-subjectivity.md?plain=1#L30 + // labeled delta in the spec + let balance_churn_limit = self.get_balance_churn_limit(spec)?; + let epochs_for_validator_set_churn = SAFETY_DECAY + .safe_mul(total_active_balance)? + .safe_div(balance_churn_limit.safe_mul(200)?)?; + let ws_period = spec + .min_validator_withdrawability_delay + .safe_mul(epochs_for_validator_set_churn)?; + + Ok(ws_period) + } else { + // spec: https://github.com/ethereum/consensus-specs/blob/1937aff86b41b5171a9bc3972515986f1bbbf303/specs/phase0/weak-subjectivity.md?plain=1#L82 + let mut ws_period = spec.min_validator_withdrawability_delay; + // labeled N in the spec + let active_validator_count = self + .get_active_validator_indices(self.slot().epoch(E::slots_per_epoch()), spec)? + .len() as u64; + // labeled t in the spec + let total_active_balance_per_validator = + total_active_balance.safe_div(active_validator_count)?; + // labeled T in the spec + let max_effective_balance = spec.max_effective_balance_for_fork(fork_name); + // labled delta in the spec + let validator_churn_limit = self.get_validator_churn_limit(spec)?; + // labeled Delta in the spec + let max_deposits_per_epoch = E::MaxDeposits::to_u64().safe_mul(E::slots_per_epoch())?; + + // T * (200 + 3 * D) < t * (200 + 12 * D) + if max_effective_balance.safe_mul(200.safe_add(SAFETY_DECAY.safe_mul(3)?)?)? + < total_active_balance_per_validator + .safe_mul(SAFETY_DECAY.safe_mul(12)?.safe_add(200)?)? + { + // N * (t * (200 + 12 * D) - T * (200 + 3 * D)) + let epochs_for_validator_set_churn_numerator = active_validator_count + .safe_mul(total_active_balance_per_validator)? + .safe_mul(200.safe_add(SAFETY_DECAY.safe_mul(12)?)?)? + .safe_sub( + max_effective_balance.safe_mul(SAFETY_DECAY.safe_mul(3)?.safe_add(200)?)?, + )?; + + // (600 * delta * (2 * t + T)) + let epochs_for_validator_set_churn_denominator = + validator_churn_limit.safe_mul(600)?.safe_mul( + total_active_balance_per_validator + .safe_mul(2)? + .safe_add(max_effective_balance)?, + )?; + + // N * (t * (200 + 12 * D) - T * (200 + 3 * D)) // (600 * delta * (2 * t + T)) + let epochs_for_validator_set_churn = epochs_for_validator_set_churn_numerator + .safe_div(epochs_for_validator_set_churn_denominator)?; + + // N * (200 + 3 * D) + let epochs_for_balance_top_ups_numerator = + active_validator_count.safe_mul(SAFETY_DECAY.safe_mul(3)?.safe_add(200)?)?; + + // (600 * Delta) + let epochs_for_balance_top_ups_denominator = + max_deposits_per_epoch.safe_mul(600)?; + + // N * (200 + 3 * D) // (600 * Delta) + let epochs_for_balance_top_ups = epochs_for_balance_top_ups_numerator + .safe_div(epochs_for_balance_top_ups_denominator)?; + + ws_period.safe_add_assign(std::cmp::max( + epochs_for_validator_set_churn, + epochs_for_balance_top_ups, + ))?; + } else { + // 3 * N * D * t + let numerator = active_validator_count + .safe_mul(3)? + .safe_mul(SAFETY_DECAY)? + .safe_mul(total_active_balance_per_validator)?; + + // 200 * (Delta * (T - t)) + let denomenator = max_deposits_per_epoch + .safe_mul(max_effective_balance.safe_sub(total_active_balance_per_validator)?)? + .safe_mul(200)?; + + // 3 * N * D * t // (200 * Delta * (T - t)) + ws_period.safe_add_assign(numerator.safe_div(denomenator)?)?; + } + + Ok(ws_period) + } } } From c2b9f9fe90cf83359e0465fba689bf34ced8f80e Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Wed, 23 Apr 2025 12:05:53 -0700 Subject: [PATCH 04/18] fix ci --- beacon_node/beacon_chain/tests/store_tests.rs | 1 + book/src/help_bn.md | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 3343dc101b5..16116df8f7c 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -97,6 +97,7 @@ fn get_harness_import_all_data_columns( ) -> TestHarness { // Most tests expect to retain historic states, so we use this as the default. let chain_config = ChainConfig { + ignore_ws_check: true, reconstruct_historic_states: true, ..ChainConfig::default() }; diff --git a/book/src/help_bn.md b/book/src/help_bn.md index 35ad020b74f..80c34180562 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -508,6 +508,13 @@ Flags: --http-enable-tls Serves the RESTful HTTP API server over TLS. This feature is currently experimental. + --ignore-ws-check + The Weak Subjectivity Period is the the maximum time a node can be + offline and still safely sync back to the canonical chain without the + risk of falling victim to long-range attacks. This flag disables the + Weak Subjectivity check at startup, allowing users to run a node whose + current head snapshot is outside the Weak Subjectivity Period. It is + unsafe to disable the Weak Subjectivity check at startup. --import-all-attestations Import and aggregate all attestations, regardless of validator subscriptions. This will only import attestations from From 172716733c93312ad4daedabf3e8b6275ac89ae3 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 24 Apr 2025 10:24:31 -0700 Subject: [PATCH 05/18] Added tests --- consensus/types/src/beacon_state.rs | 313 ++++++++++++++++++++-------- 1 file changed, 224 insertions(+), 89 deletions(-) diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 1cf2c9f2bc7..ff9d5ffc4c7 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -48,6 +48,13 @@ pub const CACHED_EPOCHS: usize = 3; const MAX_RANDOM_BYTE: u64 = (1 << 8) - 1; const MAX_RANDOM_VALUE: u64 = (1 << 16) - 1; +// `SAFETY_DECAY` is defined as the maximum percentage tolerable loss in the one-third +// safety margin of FFG finality. Thus, any attack exploiting the Weak Subjectivity Period has +// a safety margin of at least `1/3 - SAFETY_DECAY/100`. +// Spec: https://github.com/ethereum/consensus-specs/blob/1937aff86b41b5171a9bc3972515986f1bbbf303/specs/phase0/weak-subjectivity.md?plain=1#L50-L71 +const SAFETY_DECAY: u64 = 10; +const GWEI_PER_ETH: u64 = 1_000_000_000; + pub type Validators = List::ValidatorRegistryLimit>; pub type Balances = List::ValidatorRegistryLimit>; @@ -2506,106 +2513,33 @@ impl BeaconState { Ok(()) } - /// Returns the weak subjectivity period for `self`. This computation takes into - /// account the effect of validator set churn (bounded by `get_balance_churn_limit()` per epoch) - /// A detailed calculation can be found at: - /// https://notes.ethereum.org/@CarlBeek/electra_weak_subjectivity + /// Returns the weak subjectivity period for `self` pub fn compute_weak_subjectivity_period(&self, spec: &ChainSpec) -> Result { - // `SAFETY_DECAY` is defined as the maximum percentage tolerable loss in the one-third - // safety margin of FFG finality. Thus, any attack exploiting the Weak Subjectivity Period has - // a safety margin of at least `1/3 - SAFETY_DECAY/100`. - // Spec: https://github.com/ethereum/consensus-specs/blob/1937aff86b41b5171a9bc3972515986f1bbbf303/specs/phase0/weak-subjectivity.md?plain=1#L50-L71 - const SAFETY_DECAY: u64 = 10; let total_active_balance = self.get_total_active_balance()?; let fork_name = self.fork_name_unchecked(); if fork_name.electra_enabled() { - // spec: https://github.com/ethereum/consensus-specs/blob/1937aff86b41b5171a9bc3972515986f1bbbf303/specs/electra/weak-subjectivity.md?plain=1#L30 - // labeled delta in the spec let balance_churn_limit = self.get_balance_churn_limit(spec)?; - let epochs_for_validator_set_churn = SAFETY_DECAY - .safe_mul(total_active_balance)? - .safe_div(balance_churn_limit.safe_mul(200)?)?; - let ws_period = spec - .min_validator_withdrawability_delay - .safe_mul(epochs_for_validator_set_churn)?; - - Ok(ws_period) + compute_weak_subjectivity_period_electra( + total_active_balance, + balance_churn_limit, + spec, + ) } else { - // spec: https://github.com/ethereum/consensus-specs/blob/1937aff86b41b5171a9bc3972515986f1bbbf303/specs/phase0/weak-subjectivity.md?plain=1#L82 - let mut ws_period = spec.min_validator_withdrawability_delay; - // labeled N in the spec let active_validator_count = self .get_active_validator_indices(self.slot().epoch(E::slots_per_epoch()), spec)? .len() as u64; - // labeled t in the spec - let total_active_balance_per_validator = - total_active_balance.safe_div(active_validator_count)?; - // labeled T in the spec - let max_effective_balance = spec.max_effective_balance_for_fork(fork_name); - // labled delta in the spec let validator_churn_limit = self.get_validator_churn_limit(spec)?; - // labeled Delta in the spec - let max_deposits_per_epoch = E::MaxDeposits::to_u64().safe_mul(E::slots_per_epoch())?; - - // T * (200 + 3 * D) < t * (200 + 12 * D) - if max_effective_balance.safe_mul(200.safe_add(SAFETY_DECAY.safe_mul(3)?)?)? - < total_active_balance_per_validator - .safe_mul(SAFETY_DECAY.safe_mul(12)?.safe_add(200)?)? - { - // N * (t * (200 + 12 * D) - T * (200 + 3 * D)) - let epochs_for_validator_set_churn_numerator = active_validator_count - .safe_mul(total_active_balance_per_validator)? - .safe_mul(200.safe_add(SAFETY_DECAY.safe_mul(12)?)?)? - .safe_sub( - max_effective_balance.safe_mul(SAFETY_DECAY.safe_mul(3)?.safe_add(200)?)?, - )?; - - // (600 * delta * (2 * t + T)) - let epochs_for_validator_set_churn_denominator = - validator_churn_limit.safe_mul(600)?.safe_mul( - total_active_balance_per_validator - .safe_mul(2)? - .safe_add(max_effective_balance)?, - )?; - - // N * (t * (200 + 12 * D) - T * (200 + 3 * D)) // (600 * delta * (2 * t + T)) - let epochs_for_validator_set_churn = epochs_for_validator_set_churn_numerator - .safe_div(epochs_for_validator_set_churn_denominator)?; - - // N * (200 + 3 * D) - let epochs_for_balance_top_ups_numerator = - active_validator_count.safe_mul(SAFETY_DECAY.safe_mul(3)?.safe_add(200)?)?; - - // (600 * Delta) - let epochs_for_balance_top_ups_denominator = - max_deposits_per_epoch.safe_mul(600)?; - - // N * (200 + 3 * D) // (600 * Delta) - let epochs_for_balance_top_ups = epochs_for_balance_top_ups_numerator - .safe_div(epochs_for_balance_top_ups_denominator)?; - - ws_period.safe_add_assign(std::cmp::max( - epochs_for_validator_set_churn, - epochs_for_balance_top_ups, - ))?; - } else { - // 3 * N * D * t - let numerator = active_validator_count - .safe_mul(3)? - .safe_mul(SAFETY_DECAY)? - .safe_mul(total_active_balance_per_validator)?; - - // 200 * (Delta * (T - t)) - let denomenator = max_deposits_per_epoch - .safe_mul(max_effective_balance.safe_sub(total_active_balance_per_validator)?)? - .safe_mul(200)?; - - // 3 * N * D * t // (200 * Delta * (T - t)) - ws_period.safe_add_assign(numerator.safe_div(denomenator)?)?; - } - - Ok(ws_period) + let total_active_balance_per_validator = total_active_balance + .safe_div(active_validator_count)? + .safe_div(GWEI_PER_ETH)?; + compute_weak_subjectivity_period_base::( + active_validator_count, + total_active_balance_per_validator, + validator_churn_limit, + fork_name, + spec, + ) } } } @@ -2866,3 +2800,204 @@ impl ForkVersionDeserialize for BeaconState { )) } } + +/// Spec: https://github.com/ethereum/consensus-specs/blob/1937aff86b41b5171a9bc3972515986f1bbbf303/specs/electra/weak-subjectivity.md?plain=1#L30 +pub fn compute_weak_subjectivity_period_electra( + total_active_balance: u64, + balance_churn_limit: u64, + spec: &ChainSpec, +) -> Result { + let epochs_for_validator_set_churn = SAFETY_DECAY + .safe_mul(total_active_balance)? + .safe_div(balance_churn_limit.safe_mul(200)?)?; + let ws_period = spec + .min_validator_withdrawability_delay + .safe_add(epochs_for_validator_set_churn)?; + + Ok(ws_period) +} + +/// Spec: https://github.com/ethereum/consensus-specs/blob/1937aff86b41b5171a9bc3972515986f1bbbf303/specs/phase0/weak-subjectivity.md?plain=1#L82 +/// N: active_validator_count +/// t: total_active_balance_per_validator +/// T: max_effective_balance +/// delta: validator_churn_limit +/// Delta: max_deposits_per_epoch +/// D: SAFETY_DECAY +/// Note: We denominate balance values in Ether to prevent overflow +pub fn compute_weak_subjectivity_period_base( + active_validator_count: u64, + total_active_balance_per_validator: u64, + validator_churn_limit: u64, + fork_name: ForkName, + spec: &ChainSpec, +) -> Result { + let mut ws_period = spec.min_validator_withdrawability_delay; + let max_effective_balance = spec + .max_effective_balance_for_fork(fork_name) + .safe_div(GWEI_PER_ETH)?; + let max_deposits_per_epoch = E::MaxDeposits::to_u64().safe_mul(E::slots_per_epoch())?; + + // T * (200 + 3 * D) < t * (200 + 12 * D) + if max_effective_balance.safe_mul(SAFETY_DECAY.safe_mul(3)?.safe_add(200)?)? + < total_active_balance_per_validator.safe_mul(SAFETY_DECAY.safe_mul(12)?.safe_add(200)?)? + { + // N * (t * (200 + 12 * D) - T * (200 + 3 * D)) + let epochs_for_validator_set_churn_numerator = active_validator_count.safe_mul( + total_active_balance_per_validator + .safe_mul(SAFETY_DECAY.safe_mul(12)?.safe_add(200)?)? + .safe_sub( + max_effective_balance.safe_mul(SAFETY_DECAY.safe_mul(3)?.safe_add(200)?)?, + )?, + )?; + + // (600 * delta * (2 * t + T)) + let epochs_for_validator_set_churn_denominator = + validator_churn_limit.safe_mul(600)?.safe_mul( + total_active_balance_per_validator + .safe_mul(2)? + .safe_add(max_effective_balance)?, + )?; + + // N * (t * (200 + 12 * D) - T * (200 + 3 * D)) // (600 * delta * (2 * t + T)) + let epochs_for_validator_set_churn = epochs_for_validator_set_churn_numerator + .safe_div(epochs_for_validator_set_churn_denominator)?; + + // N * (200 + 3 * D) + let epochs_for_balance_top_ups_numerator = + active_validator_count.safe_mul(SAFETY_DECAY.safe_mul(3)?.safe_add(200)?)?; + + // (600 * Delta) + let epochs_for_balance_top_ups_denominator = max_deposits_per_epoch.safe_mul(600)?; + + // N * (200 + 3 * D) // (600 * Delta) + let epochs_for_balance_top_ups = epochs_for_balance_top_ups_numerator + .safe_div(epochs_for_balance_top_ups_denominator)?; + + ws_period.safe_add_assign(std::cmp::max( + epochs_for_validator_set_churn, + epochs_for_balance_top_ups, + ))?; + } else { + // 3 * N * D * t + let numerator = active_validator_count + .safe_mul(3)? + .safe_mul(SAFETY_DECAY)? + .safe_mul(total_active_balance_per_validator)?; + + // 200 * (Delta * (T - t)) + let denomenator = max_deposits_per_epoch + .safe_mul(max_effective_balance.safe_sub(total_active_balance_per_validator)?)? + .safe_mul(200)?; + + // 3 * N * D * t // (200 * Delta * (T - t)) + ws_period.safe_add_assign(numerator.safe_div(denomenator)?)?; + } + + Ok(ws_period) +} + +#[cfg(test)] +mod weak_subjectivity_tests { + use super::MainnetEthSpec; + use crate::{ + beacon_state::GWEI_PER_ETH, compute_weak_subjectivity_period_base, + compute_weak_subjectivity_period_electra, eth_spec::EthSpec, ChainSpec, Epoch, ForkName, + }; + + type E = MainnetEthSpec; + + #[test] + fn test_compute_weak_subjectivity_period_electra() { + let mut spec = MainnetEthSpec::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(0)); + spec.deneb_fork_epoch = Some(Epoch::new(0)); + spec.electra_fork_epoch = Some(Epoch::new(0)); + + // A table of some expected values: + // https://github.com/ethereum/consensus-specs/blob/1937aff86b41b5171a9bc3972515986f1bbbf303/specs/electra/weak-subjectivity.md?plain=1#L44-L54 + // (total_active_balance, expected_ws_period) + let expected_values: Vec<(u64, u64)> = vec![ + (1_048_576 * GWEI_PER_ETH, 665), + (2_097_152 * GWEI_PER_ETH, 1_075), + (4_194_304 * GWEI_PER_ETH, 1_894), + (8_388_608 * GWEI_PER_ETH, 3_532), + (16_777_216 * GWEI_PER_ETH, 3_532), + (33_554_432 * GWEI_PER_ETH, 3_532), + ]; + + for (total_active_balance, expected_ws_period) in expected_values { + let balance_churn_limit = get_balance_churn_limit(total_active_balance, &spec); + + let calculated_ws_period = compute_weak_subjectivity_period_electra( + total_active_balance, + balance_churn_limit, + &spec, + ) + .unwrap(); + + assert_eq!(calculated_ws_period, expected_ws_period); + } + } + + // caclulate the balance_churn_limit without dealing with states + // and without initializing the active balance cache + fn get_balance_churn_limit(total_active_balance: u64, spec: &ChainSpec) -> u64 { + let churn = std::cmp::max( + spec.min_per_epoch_churn_limit_electra, + total_active_balance / spec.churn_limit_quotient, + ); + churn - (churn % spec.effective_balance_increment) + } + + // caclulate the validator_churn_limit without dealing with states + // and without initializing the active balance cache + fn get_validator_churn_limit(active_validator_count: u64, spec: &ChainSpec) -> u64 { + std::cmp::max( + spec.min_per_epoch_churn_limit, + active_validator_count / spec.churn_limit_quotient, + ) + } + + #[test] + fn test_compute_weak_subjectivity_period_base() { + let mut spec = MainnetEthSpec::default_spec(); + spec.altair_fork_epoch = Some(Epoch::new(0)); + + // A table of some expected values: + // https://github.com/ethereum/consensus-specs/blob/1937aff86b41b5171a9bc3972515986f1bbbf303/specs/phase0/weak-subjectivity.md?plain=1#L115-L130 + // (total_active_balance_per_validator, active_validator_count, expected_ws_period) + let expected_values: Vec<(u64, u64, u64)> = vec![ + (28, 32_768, 504), + (28, 65_536, 752), + (28, 131_072, 1248), + (28, 262_144, 2241), + (28, 524_288, 2241), + (28, 1_048_576, 2241), + (32, 32_768, 665), + (32, 65_536, 1075), + (32, 131_072, 1894), + (32, 262_144, 3532), + (32, 524_288, 3532), + (32, 1_048_576, 3532), + ]; + + for (total_active_balance_per_validator, active_validator_count, expected_ws_period) in + expected_values + { + let validator_churn_limit = get_validator_churn_limit(active_validator_count, &spec); + let calculated_ws_period = compute_weak_subjectivity_period_base::( + active_validator_count, + total_active_balance_per_validator, + validator_churn_limit, + ForkName::Altair, + &spec, + ) + .unwrap(); + + assert_eq!(calculated_ws_period, expected_ws_period); + } + } +} From e6fdd4d363fc57ee38f7ed37c4edb5c1ca0cbffc Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 24 Apr 2025 12:12:10 -0700 Subject: [PATCH 06/18] added better test coverage --- beacon_node/beacon_chain/tests/tests.rs | 49 ++++++++++++++++++++++++- consensus/types/src/beacon_state.rs | 7 ++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs index c801361fd5f..2968732b2d6 100644 --- a/beacon_node/beacon_chain/tests/tests.rs +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -12,8 +12,8 @@ use operation_pool::PersistedOperationPool; use state_processing::{per_slot_processing, per_slot_processing::Error as SlotProcessingError}; use std::sync::LazyLock; use types::{ - BeaconState, BeaconStateError, BlockImportSource, Checkpoint, EthSpec, Hash256, Keypair, - MinimalEthSpec, RelativeEpoch, Slot, + BeaconState, BeaconStateError, BlockImportSource, ChainSpec, Checkpoint, EthSpec, ForkName, + Hash256, Keypair, MainnetEthSpec, MinimalEthSpec, RelativeEpoch, Slot, }; type E = MinimalEthSpec; @@ -35,6 +35,27 @@ fn get_harness(validator_count: usize) -> BeaconChainHarness BeaconChainHarness> { + let chain_config = ChainConfig { + reconstruct_historic_states: true, + ..Default::default() + }; + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec.clone().into()) + .chain_config(chain_config) + .keypairs(KEYPAIRS[0..validator_count].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + harness.advance_slot(); + + harness +} + fn get_harness_with_config( validator_count: usize, chain_config: ChainConfig, @@ -1043,3 +1064,27 @@ async fn pseudo_finalize_with_lagging_split_update() { let expect_true_migration = false; pseudo_finalize_test_generic(epochs_per_migration, expect_true_migration).await; } + +#[tokio::test] +async fn test_compute_weak_subjectivity_period() { + type E = MainnetEthSpec; + let expected_ws_period = 256; + + // test Base variant + let spec = ForkName::Altair.make_genesis_spec(E::default_spec()); + let harness = get_harness_with_spec(VALIDATOR_COUNT, &spec); + let head_state = harness.get_current_state(); + + let calculated_ws_period = head_state.compute_weak_subjectivity_period(&spec).unwrap(); + + assert_eq!(calculated_ws_period, expected_ws_period); + + // test Electra variant + let spec = ForkName::Electra.make_genesis_spec(E::default_spec()); + let harness = get_harness_with_spec(VALIDATOR_COUNT, &spec); + let head_state = harness.get_current_state(); + + let calculated_ws_period = head_state.compute_weak_subjectivity_period(&spec).unwrap(); + + assert_eq!(calculated_ws_period, expected_ws_period); +} diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index ff9d5ffc4c7..ef4460bc49b 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -2533,6 +2533,7 @@ impl BeaconState { let total_active_balance_per_validator = total_active_balance .safe_div(active_validator_count)? .safe_div(GWEI_PER_ETH)?; + compute_weak_subjectivity_period_base::( active_validator_count, total_active_balance_per_validator, @@ -2926,6 +2927,9 @@ mod weak_subjectivity_tests { (8_388_608 * GWEI_PER_ETH, 3_532), (16_777_216 * GWEI_PER_ETH, 3_532), (33_554_432 * GWEI_PER_ETH, 3_532), + // This value cross referenced w/ + // beacon_chain/tests/tests.rs:test_compute_weak_subjectivity_period + (1536 * GWEI_PER_ETH, 256), ]; for (total_active_balance, expected_ws_period) in expected_values { @@ -2982,6 +2986,9 @@ mod weak_subjectivity_tests { (32, 262_144, 3532), (32, 524_288, 3532), (32, 1_048_576, 3532), + // This value cross referenced w/ + // beacon_chain/tests/tests.rs:test_compute_weak_subjectivity_period + (32, 48, 256), ]; for (total_active_balance_per_validator, active_validator_count, expected_ws_period) in From 1086b50fe8d2c8cee2e04d6ed6ba384e5e58886e Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 24 Apr 2025 12:13:09 -0700 Subject: [PATCH 07/18] update comment --- consensus/types/src/beacon_state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index ef4460bc49b..06c8384a21c 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -2957,7 +2957,7 @@ mod weak_subjectivity_tests { } // caclulate the validator_churn_limit without dealing with states - // and without initializing the active balance cache + // and without initializing any caches fn get_validator_churn_limit(active_validator_count: u64, spec: &ChainSpec) -> u64 { std::cmp::max( spec.min_per_epoch_churn_limit, From c995e6a0324df116edb20dfb501963b0d25d35cf Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 24 Apr 2025 12:23:07 -0700 Subject: [PATCH 08/18] update comment --- beacon_node/beacon_chain/src/builder.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 8e517b9cd1a..19e1cb2f673 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -832,12 +832,15 @@ where head_slot=%head_state.slot(), %current_slot, "The current head state is outside the weak subjectivity period. It is highly recommended to purge your db and \ - checkpoint sync." + checkpoint sync. You are currently running a node that is susceptible to long range attacks. For more information please \ + read this blog post: https://blog.ethereum.org/2014/11/25/proof-stake-learned-love-weak-subjectivity" ) } return Err( "The current head state is outside the weak subjectivity period. It is highly recommended to purge your db and \ - checkpoint sync. Alternatively you can accept the risks and ignore this error with the --ignore-ws-check flag.".to_string() + checkpoint sync. It is possible to ignore this error with the --ignore-ws-check flag, but this could make the node \ + susceptible to long range attacks. For more information please read this blog post: \ + https://blog.ethereum.org/2014/11/25/proof-stake-learned-love-weak-subjectivity".to_string() ); } From c09954a4583876606d10b9940af0460b5bb5f458 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 1 Dec 2025 14:01:25 -0300 Subject: [PATCH 09/18] Fix messaging --- beacon_node/beacon_chain/src/builder.rs | 11 +++++------ beacon_node/src/cli.rs | 7 ++++--- book/src/help_bn.md | 10 ++++++---- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 00731c02443..46057ebb5c7 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -860,16 +860,15 @@ where warn!( head_slot=%head_state.slot(), %current_slot, - "The current head state is outside the weak subjectivity period. It is highly recommended to purge your db and \ - checkpoint sync. You are currently running a node that is susceptible to long range attacks. For more information please \ + "The current head state is outside the weak subjectivity period. You are currently running a node that is susceptible to long range attacks. \ + It is highly recommended to purge your db and checkpoint sync. For more information please \ read this blog post: https://blog.ethereum.org/2014/11/25/proof-stake-learned-love-weak-subjectivity" ) } return Err( - "The current head state is outside the weak subjectivity period. It is highly recommended to purge your db and \ - checkpoint sync. It is possible to ignore this error with the --ignore-ws-check flag, but this could make the node \ - susceptible to long range attacks. For more information please read this blog post: \ - https://blog.ethereum.org/2014/11/25/proof-stake-learned-love-weak-subjectivity".to_string() + "The current head state is outside the weak subjectivity period. A node in this state is susceptible to long range attacks. You should purge your db and \ + checkpoint sync. For more information please read this blog post: https://blog.ethereum.org/2014/11/25/proof-stake-learned-love-weak-subjectivity \ + If you understand the risks, it is possible to ignore this error with the --ignore-ws-check flag.".to_string() ); } diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 1f1c7f9cc79..c36c39112bb 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -1408,9 +1408,10 @@ pub fn cli_app() -> Command { Arg::new("ignore-ws-check") .long("ignore-ws-check") .help("The Weak Subjectivity Period is the the maximum time a node can be offline and still \ - safely sync back to the canonical chain without the risk of falling victim to long-range attacks. \ - This flag disables the Weak Subjectivity check at startup, allowing users to run a node whose current head snapshot \ - is outside the Weak Subjectivity Period. It is unsafe to disable the Weak Subjectivity check at startup.") + safely sync back to the canonical chain without the risk of falling victim to long-range attacks. \ + For more information please read this blog post: https://blog.ethereum.org/2014/11/25/proof-stake-learned-love-weak-subjectivity \ + If you understand the risks, you can use this flag to disable the Weak Subjectivity check at startup, \ + allowing users to run a node whose current head snapshot is outside the Weak Subjectivity Period.") .action(ArgAction::SetTrue) .help_heading(FLAG_HEADER) .display_order(0) diff --git a/book/src/help_bn.md b/book/src/help_bn.md index 76d2f333b38..b1c63537773 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -511,10 +511,12 @@ Flags: --ignore-ws-check The Weak Subjectivity Period is the the maximum time a node can be offline and still safely sync back to the canonical chain without the - risk of falling victim to long-range attacks. This flag disables the - Weak Subjectivity check at startup, allowing users to run a node whose - current head snapshot is outside the Weak Subjectivity Period. It is - unsafe to disable the Weak Subjectivity check at startup. + risk of falling victim to long-range attacks. For more information + please read this blog post: + https://blog.ethereum.org/2014/11/25/proof-stake-learned-love-weak-subjectivity + If you understand the risks, you can use this flag to disable the Weak + Subjectivity check at startup, allowing users to run a node whose + current head snapshot is outside the Weak Subjectivity Period. --import-all-attestations Import and aggregate all attestations, regardless of validator subscriptions. This will only import attestations from From 83f6ac436ba61cd413cb468a7dd3958f3a003365 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 1 Dec 2025 14:09:21 -0300 Subject: [PATCH 10/18] simlplify cli text --- beacon_node/src/cli.rs | 6 ++---- book/src/help_bn.md | 9 +++------ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index c36c39112bb..3f1d11e08aa 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -1407,11 +1407,9 @@ pub fn cli_app() -> Command { .arg( Arg::new("ignore-ws-check") .long("ignore-ws-check") - .help("The Weak Subjectivity Period is the the maximum time a node can be offline and still \ - safely sync back to the canonical chain without the risk of falling victim to long-range attacks. \ + .help("Using this flag allows a node to run in a state that may expose it to long-range attacks. \ For more information please read this blog post: https://blog.ethereum.org/2014/11/25/proof-stake-learned-love-weak-subjectivity \ - If you understand the risks, you can use this flag to disable the Weak Subjectivity check at startup, \ - allowing users to run a node whose current head snapshot is outside the Weak Subjectivity Period.") + If you understand the risks, you can use this flag to disable the Weak Subjectivity check at startup.") .action(ArgAction::SetTrue) .help_heading(FLAG_HEADER) .display_order(0) diff --git a/book/src/help_bn.md b/book/src/help_bn.md index b1c63537773..d4bfac80c79 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -509,14 +509,11 @@ Flags: Serves the RESTful HTTP API server over TLS. This feature is currently experimental. --ignore-ws-check - The Weak Subjectivity Period is the the maximum time a node can be - offline and still safely sync back to the canonical chain without the - risk of falling victim to long-range attacks. For more information - please read this blog post: + Using this flag allows a node to run in a state that may expose it to + long-range attacks. For more information please read this blog post: https://blog.ethereum.org/2014/11/25/proof-stake-learned-love-weak-subjectivity If you understand the risks, you can use this flag to disable the Weak - Subjectivity check at startup, allowing users to run a node whose - current head snapshot is outside the Weak Subjectivity Period. + Subjectivity check at startup. --import-all-attestations Import and aggregate all attestations, regardless of validator subscriptions. This will only import attestations from From 3d919f03055772ed6e935b230a9087e3401f4b21 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Tue, 2 Dec 2025 13:00:04 -0300 Subject: [PATCH 11/18] Remove pre-electra calc --- consensus/types/src/beacon_state.rs | 159 +--------------------------- 1 file changed, 5 insertions(+), 154 deletions(-) diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index faa9f9e46d8..13e531a08ac 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -54,7 +54,6 @@ const MAX_RANDOM_VALUE: u64 = (1 << 16) - 1; // a safety margin of at least `1/3 - SAFETY_DECAY/100`. // Spec: https://github.com/ethereum/consensus-specs/blob/1937aff86b41b5171a9bc3972515986f1bbbf303/specs/phase0/weak-subjectivity.md?plain=1#L50-L71 const SAFETY_DECAY: u64 = 10; -const GWEI_PER_ETH: u64 = 1_000_000_000; pub type Validators = List::ValidatorRegistryLimit>; pub type Balances = List::ValidatorRegistryLimit>; @@ -2736,21 +2735,9 @@ impl BeaconState { spec, ) } else { - let active_validator_count = self - .get_active_validator_indices(self.slot().epoch(E::slots_per_epoch()), spec)? - .len() as u64; - let validator_churn_limit = self.get_validator_churn_limit(spec)?; - let total_active_balance_per_validator = total_active_balance - .safe_div(active_validator_count)? - .safe_div(GWEI_PER_ETH)?; - - compute_weak_subjectivity_period_base::( - active_validator_count, - total_active_balance_per_validator, - validator_churn_limit, - fork_name, - spec, - ) + // We don't support pre-electra WS calculations. This is outside the weak subjectivity + // period anyways, so we return 0. + Ok(Epoch::new(0)) } } } @@ -3041,96 +3028,12 @@ pub fn compute_weak_subjectivity_period_electra( Ok(ws_period) } -/// Spec: https://github.com/ethereum/consensus-specs/blob/1937aff86b41b5171a9bc3972515986f1bbbf303/specs/phase0/weak-subjectivity.md?plain=1#L82 -/// N: active_validator_count -/// t: total_active_balance_per_validator -/// T: max_effective_balance -/// delta: validator_churn_limit -/// Delta: max_deposits_per_epoch -/// D: SAFETY_DECAY -/// Note: We denominate balance values in Ether to prevent overflow -pub fn compute_weak_subjectivity_period_base( - active_validator_count: u64, - total_active_balance_per_validator: u64, - validator_churn_limit: u64, - fork_name: ForkName, - spec: &ChainSpec, -) -> Result { - let mut ws_period = spec.min_validator_withdrawability_delay; - let max_effective_balance = spec - .max_effective_balance_for_fork(fork_name) - .safe_div(GWEI_PER_ETH)?; - let max_deposits_per_epoch = E::MaxDeposits::to_u64().safe_mul(E::slots_per_epoch())?; - - // T * (200 + 3 * D) < t * (200 + 12 * D) - if max_effective_balance.safe_mul(SAFETY_DECAY.safe_mul(3)?.safe_add(200)?)? - < total_active_balance_per_validator.safe_mul(SAFETY_DECAY.safe_mul(12)?.safe_add(200)?)? - { - // N * (t * (200 + 12 * D) - T * (200 + 3 * D)) - let epochs_for_validator_set_churn_numerator = active_validator_count.safe_mul( - total_active_balance_per_validator - .safe_mul(SAFETY_DECAY.safe_mul(12)?.safe_add(200)?)? - .safe_sub( - max_effective_balance.safe_mul(SAFETY_DECAY.safe_mul(3)?.safe_add(200)?)?, - )?, - )?; - - // (600 * delta * (2 * t + T)) - let epochs_for_validator_set_churn_denominator = - validator_churn_limit.safe_mul(600)?.safe_mul( - total_active_balance_per_validator - .safe_mul(2)? - .safe_add(max_effective_balance)?, - )?; - - // N * (t * (200 + 12 * D) - T * (200 + 3 * D)) // (600 * delta * (2 * t + T)) - let epochs_for_validator_set_churn = epochs_for_validator_set_churn_numerator - .safe_div(epochs_for_validator_set_churn_denominator)?; - - // N * (200 + 3 * D) - let epochs_for_balance_top_ups_numerator = - active_validator_count.safe_mul(SAFETY_DECAY.safe_mul(3)?.safe_add(200)?)?; - - // (600 * Delta) - let epochs_for_balance_top_ups_denominator = max_deposits_per_epoch.safe_mul(600)?; - - // N * (200 + 3 * D) // (600 * Delta) - let epochs_for_balance_top_ups = epochs_for_balance_top_ups_numerator - .safe_div(epochs_for_balance_top_ups_denominator)?; - - ws_period.safe_add_assign(std::cmp::max( - epochs_for_validator_set_churn, - epochs_for_balance_top_ups, - ))?; - } else { - // 3 * N * D * t - let numerator = active_validator_count - .safe_mul(3)? - .safe_mul(SAFETY_DECAY)? - .safe_mul(total_active_balance_per_validator)?; - - // 200 * (Delta * (T - t)) - let denomenator = max_deposits_per_epoch - .safe_mul(max_effective_balance.safe_sub(total_active_balance_per_validator)?)? - .safe_mul(200)?; - - // 3 * N * D * t // (200 * Delta * (T - t)) - ws_period.safe_add_assign(numerator.safe_div(denomenator)?)?; - } - - Ok(ws_period) -} - #[cfg(test)] mod weak_subjectivity_tests { use super::MainnetEthSpec; - use crate::{ - ChainSpec, Epoch, ForkName, beacon_state::GWEI_PER_ETH, - compute_weak_subjectivity_period_base, compute_weak_subjectivity_period_electra, - eth_spec::EthSpec, - }; + use crate::{ChainSpec, Epoch, compute_weak_subjectivity_period_electra, eth_spec::EthSpec}; - type E = MainnetEthSpec; + const GWEI_PER_ETH: u64 = 1_000_000_000; #[test] fn test_compute_weak_subjectivity_period_electra() { @@ -3179,56 +3082,4 @@ mod weak_subjectivity_tests { ); churn - (churn % spec.effective_balance_increment) } - - // caclulate the validator_churn_limit without dealing with states - // and without initializing any caches - fn get_validator_churn_limit(active_validator_count: u64, spec: &ChainSpec) -> u64 { - std::cmp::max( - spec.min_per_epoch_churn_limit, - active_validator_count / spec.churn_limit_quotient, - ) - } - - #[test] - fn test_compute_weak_subjectivity_period_base() { - let mut spec = MainnetEthSpec::default_spec(); - spec.altair_fork_epoch = Some(Epoch::new(0)); - - // A table of some expected values: - // https://github.com/ethereum/consensus-specs/blob/1937aff86b41b5171a9bc3972515986f1bbbf303/specs/phase0/weak-subjectivity.md?plain=1#L115-L130 - // (total_active_balance_per_validator, active_validator_count, expected_ws_period) - let expected_values: Vec<(u64, u64, u64)> = vec![ - (28, 32_768, 504), - (28, 65_536, 752), - (28, 131_072, 1248), - (28, 262_144, 2241), - (28, 524_288, 2241), - (28, 1_048_576, 2241), - (32, 32_768, 665), - (32, 65_536, 1075), - (32, 131_072, 1894), - (32, 262_144, 3532), - (32, 524_288, 3532), - (32, 1_048_576, 3532), - // This value cross referenced w/ - // beacon_chain/tests/tests.rs:test_compute_weak_subjectivity_period - (32, 48, 256), - ]; - - for (total_active_balance_per_validator, active_validator_count, expected_ws_period) in - expected_values - { - let validator_churn_limit = get_validator_churn_limit(active_validator_count, &spec); - let calculated_ws_period = compute_weak_subjectivity_period_base::( - active_validator_count, - total_active_balance_per_validator, - validator_churn_limit, - ForkName::Altair, - &spec, - ) - .unwrap(); - - assert_eq!(calculated_ws_period, expected_ws_period); - } - } } From 5db3205ce8de4246c676f2422a5df0b25bdfcb0b Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Wed, 3 Dec 2025 10:32:34 -0300 Subject: [PATCH 12/18] Use test spec in InvalidPayloadRig --- beacon_node/beacon_chain/tests/payload_invalidation.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index 5bd43835e33..f68107b229f 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -6,7 +6,7 @@ use beacon_chain::{ INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON, NotifyExecutionLayer, OverrideForkchoiceUpdate, StateSkipConfig, WhenSlotSkipped, canonical_head::{CachedHead, CanonicalHead}, - test_utils::{BeaconChainHarness, EphemeralHarnessType}, + test_utils::{BeaconChainHarness, EphemeralHarnessType, test_spec}, }; use execution_layer::{ ExecutionLayer, ForkchoiceState, PayloadAttributes, @@ -42,14 +42,11 @@ struct InvalidPayloadRig { impl InvalidPayloadRig { fn new() -> Self { - let spec = E::default_spec(); + let spec = test_spec::(); Self::new_with_spec(spec) } - fn new_with_spec(mut spec: ChainSpec) -> Self { - spec.altair_fork_epoch = Some(Epoch::new(0)); - spec.bellatrix_fork_epoch = Some(Epoch::new(0)); - + fn new_with_spec(spec: ChainSpec) -> Self { let harness = BeaconChainHarness::builder(MainnetEthSpec) .spec(spec.into()) .chain_config(ChainConfig { From bc60964d8da6ba6cf9a8a062381f11c0a44515d0 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Wed, 3 Dec 2025 11:15:50 -0300 Subject: [PATCH 13/18] WS pre-electra set to 5 --- consensus/types/src/beacon_state.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 13e531a08ac..85c704b4641 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -2735,9 +2735,10 @@ impl BeaconState { spec, ) } else { - // We don't support pre-electra WS calculations. This is outside the weak subjectivity - // period anyways, so we return 0. - Ok(Epoch::new(0)) + // We don't support pre-electra WS calculations. On mainnet, pre-electra epochs are the weak subjectivity + // period. We return 5 here just to allow for the test case `revert_minority_fork_on_resume` + // to pass. 5 is a sufficently small number to trigger the WS safety check pre-electra on mainnet. + Ok(Epoch::new(5)) } } } From 5bb7d46d26b42f0c21c525bb5c5d152c1ad1db75 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Wed, 3 Dec 2025 11:27:46 -0300 Subject: [PATCH 14/18] Fix test --- beacon_node/beacon_chain/tests/tests.rs | 12 +++++++----- consensus/types/src/beacon_state.rs | 11 +++++++---- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs index c162a946ed8..1480152359a 100644 --- a/beacon_node/beacon_chain/tests/tests.rs +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -13,8 +13,9 @@ use state_processing::EpochProcessingError; use state_processing::{per_slot_processing, per_slot_processing::Error as SlotProcessingError}; use std::sync::LazyLock; use types::{ - BeaconState, BeaconStateError, BlockImportSource, ChainSpec, Checkpoint, EthSpec, ForkName, - Hash256, Keypair, MainnetEthSpec, MinimalEthSpec, RelativeEpoch, Slot, + BeaconState, BeaconStateError, BlockImportSource, ChainSpec, Checkpoint, + DEFAULT_PRE_ELECTRA_WS_PERIOD, EthSpec, ForkName, Hash256, Keypair, MainnetEthSpec, + MinimalEthSpec, RelativeEpoch, Slot, }; type E = MinimalEthSpec; @@ -1083,7 +1084,8 @@ async fn pseudo_finalize_with_lagging_split_update() { #[tokio::test] async fn test_compute_weak_subjectivity_period() { type E = MainnetEthSpec; - let expected_ws_period = 256; + let expected_ws_period_pre_electra = DEFAULT_PRE_ELECTRA_WS_PERIOD; + let expected_ws_period_post_electra = 256; // test Base variant let spec = ForkName::Altair.make_genesis_spec(E::default_spec()); @@ -1092,7 +1094,7 @@ async fn test_compute_weak_subjectivity_period() { let calculated_ws_period = head_state.compute_weak_subjectivity_period(&spec).unwrap(); - assert_eq!(calculated_ws_period, expected_ws_period); + assert_eq!(calculated_ws_period, expected_ws_period_pre_electra); // test Electra variant let spec = ForkName::Electra.make_genesis_spec(E::default_spec()); @@ -1101,5 +1103,5 @@ async fn test_compute_weak_subjectivity_period() { let calculated_ws_period = head_state.compute_weak_subjectivity_period(&spec).unwrap(); - assert_eq!(calculated_ws_period, expected_ws_period); + assert_eq!(calculated_ws_period, expected_ws_period_post_electra); } diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 85c704b4641..8c022d847df 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -46,6 +46,12 @@ mod slashings_cache; mod tests; pub const CACHED_EPOCHS: usize = 3; + +// Pre-electra WS calculations are not supported. On mainnet, pre-electra epochs are outside the weak subjectivity +// period. The default pre-electra WS value is set to 5 to allow for the test case `revert_minority_fork_on_resume` +// to pass. 5 is a small enough number to trigger the WS safety check pre-electra on mainnet. +pub const DEFAULT_PRE_ELECTRA_WS_PERIOD: u64 = 5; + const MAX_RANDOM_BYTE: u64 = (1 << 8) - 1; const MAX_RANDOM_VALUE: u64 = (1 << 16) - 1; @@ -2735,10 +2741,7 @@ impl BeaconState { spec, ) } else { - // We don't support pre-electra WS calculations. On mainnet, pre-electra epochs are the weak subjectivity - // period. We return 5 here just to allow for the test case `revert_minority_fork_on_resume` - // to pass. 5 is a sufficently small number to trigger the WS safety check pre-electra on mainnet. - Ok(Epoch::new(5)) + Ok(Epoch::new(DEFAULT_PRE_ELECTRA_WS_PERIOD)) } } } From d808c07e52a0e458864bbed8c31aa64f1ffee116 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Wed, 3 Dec 2025 12:59:31 -0300 Subject: [PATCH 15/18] Fi --- consensus/types/src/beacon_state.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 8c022d847df..5d81f9a7cf5 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -48,9 +48,9 @@ mod tests; pub const CACHED_EPOCHS: usize = 3; // Pre-electra WS calculations are not supported. On mainnet, pre-electra epochs are outside the weak subjectivity -// period. The default pre-electra WS value is set to 5 to allow for the test case `revert_minority_fork_on_resume` -// to pass. 5 is a small enough number to trigger the WS safety check pre-electra on mainnet. -pub const DEFAULT_PRE_ELECTRA_WS_PERIOD: u64 = 5; +// period. The default pre-electra WS value is set to 256 to allow for `basic-sim``, `fallback-sim`` test case `revert_minority_fork_on_resume` +// to pass. 256 is a small enough number to trigger the WS safety check pre-electra on mainnet. +pub const DEFAULT_PRE_ELECTRA_WS_PERIOD: u64 = 256; const MAX_RANDOM_BYTE: u64 = (1 << 8) - 1; const MAX_RANDOM_VALUE: u64 = (1 << 16) - 1; From 5243aaf5c5b80e80071ddb535ad00d92f8514702 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 8 Dec 2025 14:54:31 -0300 Subject: [PATCH 16/18] fix --- consensus/types/src/state/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/consensus/types/src/state/mod.rs b/consensus/types/src/state/mod.rs index 309796d3592..ea064fb7ac4 100644 --- a/consensus/types/src/state/mod.rs +++ b/consensus/types/src/state/mod.rs @@ -17,7 +17,7 @@ pub use balance::Balance; pub use beacon_state::{ BeaconState, BeaconStateAltair, BeaconStateBase, BeaconStateBellatrix, BeaconStateCapella, BeaconStateDeneb, BeaconStateElectra, BeaconStateError, BeaconStateFulu, BeaconStateGloas, - BeaconStateHash, BeaconStateRef, CACHED_EPOCHS, + BeaconStateHash, BeaconStateRef, CACHED_EPOCHS, DEFAULT_PRE_ELECTRA_WS_PERIOD, }; pub use committee_cache::{ CommitteeCache, compute_committee_index_in_epoch, compute_committee_range_in_epoch, From 2ba18d31736f4559ae73c72dcbda89b56f91c5b3 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Wed, 14 Jan 2026 16:11:30 -0600 Subject: [PATCH 17/18] Fix --- beacon_node/beacon_chain/tests/tests.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs index 0750cf7ef3f..b8198aa4363 100644 --- a/beacon_node/beacon_chain/tests/tests.rs +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -14,8 +14,9 @@ use state_processing::EpochProcessingError; use state_processing::{per_slot_processing, per_slot_processing::Error as SlotProcessingError}; use std::sync::LazyLock; use types::{ - BeaconState, BeaconStateError, BlockImportSource, Checkpoint, DEFAULT_PRE_ELECTRA_WS_PERIOD, - EthSpec, ForkName, Hash256, MainnetEthSpec, MinimalEthSpec, RelativeEpoch, Slot, + BeaconState, BeaconStateError, BlockImportSource, ChainSpec, Checkpoint, + DEFAULT_PRE_ELECTRA_WS_PERIOD, EthSpec, ForkName, Hash256, MainnetEthSpec, MinimalEthSpec, + RelativeEpoch, Slot, }; type E = MinimalEthSpec; From 0b25b1d8028d1976254c71ed91f8160631ea7fe6 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Tue, 10 Feb 2026 00:57:07 -0700 Subject: [PATCH 18/18] remove accidentally committed .claude/plans file --- .claude/plans/pr_8658.md | 197 --------------------------------------- 1 file changed, 197 deletions(-) delete mode 100644 .claude/plans/pr_8658.md diff --git a/.claude/plans/pr_8658.md b/.claude/plans/pr_8658.md deleted file mode 100644 index 1221b3b6f70..00000000000 --- a/.claude/plans/pr_8658.md +++ /dev/null @@ -1,197 +0,0 @@ -# PR #8658 — Remove merge code - -https://github.com/sigp/lighthouse/pull/8658 - -## Goal - -Remove support for the merge (Bellatrix) transition logic. The Merge happened long -ago — all networks start post-merge now, so the transition codepaths are dead code. -This also removes support for starting new networks prior to Bellatrix. - -## What's been removed - -- **Merge transition checks**: `is_merge_transition_complete`, `is_merge_transition_block`, - `ExecutionBlockHash::zero()` special-casing -- **OTB verification service**: `otb_verification_service.rs` deleted entirely - (was already deleted once in #6631, got revived by the tracing PR) -- **Bellatrix/Capella-specific tests**: `tests/bellatrix.rs`, `tests/capella.rs` removed -- **Execution layer merge logic**: ~350 lines removed from `execution_layer/src/lib.rs` - (terminal block handling, transition config validation, etc.) -- **Notifier merge messaging**: removed "awaiting merge" / "merge complete" log paths -- **Pre-Bellatrix network support**: config validation now requires Bellatrix fields -- **Test scaffolding**: simplified harness setup that assumed pre-merge states - -## Branch history - -The original branch `remove-merge-code` had a messy commit history (19 commits -including 3 real merges and 1 fake merge with unrelated changes). We cleaned this -up by: - -1. Cherry-picking 16 non-merge commits onto a fresh base -2. Squashing into a single commit -3. Cherry-picking the squash onto current `unstable` → branch `remove-merge-code-v2` -4. Resolving 11 conflicts from upstream changes (import path updates, new enum variants) - -The result is commit `ca6a1b667` on `remove-merge-code-v2` (39 files, +224/-2363). - -## Areas touched (39 files, ~2100 net lines removed) - -| Area | Key files | Notes | -|------|-----------|-------| -| beacon_chain core | `beacon_chain.rs`, `execution_payload.rs`, `block_verification.rs`, `canonical_head.rs` | Removed transition guards and fallback paths | -| execution layer | `execution_layer/src/lib.rs`, test utils | Removed terminal block / transition config logic | -| operation pool | `operation_pool/src/lib.rs` | Simplified — removed pre-merge conditional paths | -| http API | `http_api/src/lib.rs`, tests | Removed merge-related status checks | -| validator client | `duties_service.rs`, `attestation_service.rs`, `sync_committee_service.rs` | Removed "pre-merge" early-returns | -| consensus types | `chain_spec.rs`, `consts.rs` | Made Bellatrix config fields required | -| networking | `lighthouse_network/`, `network/` | Minor cleanup of merge-era checks | -| tests/scripts | simulator, EF tests, local testnet configs | Updated for post-merge-only world | - -## Local CI results (2026-02-04) - -- [x] `cargo check` — clean -- [x] `cargo fmt --all -- --check` — clean -- [x] `make lint-full` (clippy with all features) — clean -- [x] `make test-op-pool` — 25/25 passed (electra+fulu) -- [x] `make test-http-api` — 182/182 passed per fork (electra+fulu) -- [x] `make test-debug` — 753/753 passed (4 skipped) -- [x] `FORK_NAME=electra cargo nextest run -p beacon_chain` — passed - (3 SIGABRT from stack overflow are pre-existing on unstable, not caused by our changes) -- [ ] EF tests, simulators, slasher tests — not run locally (CI only) - -## Status - -- [x] Core removal of merge transition logic -- [x] Fix beacon_chain tests -- [x] Fix op pool tests -- [x] Fix API tests across forks -- [x] Lint and format passes -- [x] Rebase onto latest unstable (clean single commit) -- [x] Local CI checks passing -- [ ] Push `remove-merge-code-v2` and force-push to PR -- [ ] Add `bellatrix_produce_and_store_payloads` test (see spec below) -- [ ] Fix `release-tests-ubuntu` CI failure (3 tests in `per_block_processing::tests` fail - because adding `.mock_execution_layer()` to shared `get_harness` changed genesis fork) -- [ ] Address reviewer feedback (label: waiting-on-author) -- [ ] CI passing in GitHub Actions -- [ ] Final review from michaelsproul - -## Review comments - -- michaelsproul noted `otb_verification_service.rs` was already deleted in #6631 and - got resurrected by the tracing PR — just an observation, no action needed. -- michaelsproul (PR #8761): "Have we tested syncing through the merge transition on - e.g. mainnet?" → addressed by the `bellatrix_produce_and_store_payloads` test spec below. - -## Key decisions - -- Networks must now have Bellatrix config fields — no backward compat for pre-merge genesis -- No DB schema migration needed (this only removes runtime logic, not stored data) -- Test harness defaults changed to assume post-merge state -- Excluded `dd8e6ca91` (mislabeled "Merge sigp/unstable" — actually contained unrelated - timing refactor, SSE relocation, early attester cache changes) - -## Test: sync through merge transition (`store_tests::bellatrix_produce_and_store_payloads`) - -Addresses michaelsproul's review comment: "Have we tested syncing through the merge -transition on e.g. mainnet?" - -### Specification - -The test proves that Lighthouse can still import a chain that transitions from pre-merge -(default payloads) to post-merge (real payloads) — the exact sequence a mainnet syncer -would encounter at the Bellatrix boundary. - -**Setup:** -- `MinimalEthSpec`, Bellatrix at epoch 0 (genesis) -- Genesis state has a **default** (zeroed) execution payload header → - `is_merge_transition_complete` = false at genesis -- Total chain length: at least epoch 4 (≥ 32 slots on MinimalEthSpec) to reach finalization - -**Payload invariants — these are the assertions the test MUST verify:** - -| Slot range | Execution payload | Parent hash constraint | -|------------|-------------------|----------------------| -| 1–9 | `is_default_with_empty_roots() == true` | N/A (execution not enabled) | -| 10 | `is_default_with_empty_roots() == false` | None (this is the merge transition block) | -| 11–32+ | `is_default_with_empty_roots() == false` | `payload.parent_hash() == prev_payload.block_hash()` | - -**Finalization:** `finalized_epoch > 0` after the chain completes. - -### Why the normal harness can't do this - -1. `generate_genesis_header` produces a non-default header for Bellatrix genesis → - merge is already complete at slot 0 → every block gets a real payload. -2. `get_execution_payload` / `prepare_execution_payload` always asks the EL for a - payload using `latest_execution_payload_header_block_hash` as parent. There is no - codepath to produce a block with a default payload. -3. The mock EL's `terminal_block_number` / `terminal_total_difficulty` are internal - to `ExecutionBlockGenerator` and are never initialized from the spec's TTD. Setting - `spec.terminal_total_difficulty` has no effect on the mock. - -### Workaround - -Bypass the harness's block production for slots 1–9. Use `make_block` for slots 10+. - -Concretely: - -1. **Genesis state**: create a Bellatrix genesis state with a zeroed execution payload - header (skip `generate_genesis_header`, or pass `None`). This sets - `is_merge_transition_complete = false`. - -2. **Slots 1–9 (pre-merge):** build blocks manually with default execution payloads. - Use `per_slot_processing` to advance the state, construct a `BeaconBlock` with a - default `ExecutionPayloadBellatrix`, compute the state root and block root, sign with - the proposer key, and import via `process_chain_segment` or `process_block`. - `is_execution_enabled` returns false for these blocks so `per_block_processing` - skips all execution payload checks. - -3. **Slot 10 (merge transition block):** produce a block with a real (non-default) - execution payload. The mock EL can provide this via `forkchoice_updated` + - `get_payload`. Since `is_merge_transition_block` returns true, the parent_hash - check in `partially_verify_execution_payload` is skipped. - -4. **Slots 11–32+:** use `extend_chain` normally. The merge is now complete and each - block's payload must satisfy `payload.parent_hash() == state.latest_header.block_hash()`. - -### Alternative simpler workaround - -If manually constructing signed blocks is too involved (proposer selection, signing, -randao reveals), a simpler approach: - -- Use the harness with a custom genesis state builder that sets a **default** execution - payload header (override `generate_genesis_header` to return `None` for Bellatrix). -- Configure the mock EL's `ExecutionBlockGenerator` to have - `terminal_block_number = 10` and a corresponding `terminal_total_difficulty`. - Insert PoW blocks 0–10 so the mock has a terminal block ready at block 10. -- Set the mock EL to `all_payloads_valid()`. -- Use `extend_chain` for the whole run. Block production calls - `prepare_execution_payload` with `parent_hash = ExecutionBlockHash::zero()` (from - the default header). The EL's `forkchoice_updated` with a zero parent_hash should - trigger payload production starting from the terminal block. -- If `forkchoice_updated` rejects the zero parent hash, the fallback is to use - `make_block_return_pre_state` for slot 10, manually set the payload's parent_hash - to the terminal block hash, and import it. - -The implementer should try the simpler approach first and fall back to the manual -block construction if needed. - -## How to continue - -``` -# Read this file for context -cat .claude/plans/8658.md - -# Check current state -git log unstable..HEAD --oneline -gh pr checks 8658 - -# Push to PR (force-push needed since history was rewritten) -git push -f origin remove-merge-code-v2:remove-merge-code - -# Run targeted tests locally -FORK_NAME=electra cargo nextest run -p beacon_chain -make test-op-pool -make test-http-api -make test-debug -```