Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion beacon_node/beacon_chain/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,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::data_column_custody_group::CustodyIndex;
use types::{
BeaconBlock, BeaconState, BlobSidecarList, ChainSpec, ColumnIndex, DataColumnSidecarList,
Expand Down Expand Up @@ -847,6 +847,33 @@ where
));
}

// 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. 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. 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()
);
}

let validator_pubkey_cache = self
.validator_pubkey_cache
.map(|mut validator_pubkey_cache| {
Expand Down
3 changes: 3 additions & 0 deletions beacon_node/beacon_chain/src/chain_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ 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<Hash256>,
/// 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,
/// Disable the getBlobs optimisation to fetch blobs from the EL mempool.
pub disable_get_blobs: bool,
/// The node's custody type, determining how many data columns to custody and sample.
Expand Down Expand Up @@ -160,6 +162,7 @@ impl Default for ChainConfig {
block_publishing_delay: None,
data_column_publishing_delay: None,
invalid_block_roots: HashSet::new(),
ignore_ws_check: false,
disable_get_blobs: false,
node_custody_type: NodeCustodyType::Fullnode,
}
Expand Down
9 changes: 3 additions & 6 deletions beacon_node/beacon_chain/tests/payload_invalidation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -42,14 +42,11 @@ struct InvalidPayloadRig {

impl InvalidPayloadRig {
fn new() -> Self {
let spec = E::default_spec();
let spec = test_spec::<E>();
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 {
Expand Down
1 change: 1 addition & 0 deletions beacon_node/beacon_chain/tests/store_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,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()
};
Expand Down
49 changes: 48 additions & 1 deletion beacon_node/beacon_chain/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ 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, EthSpec, Hash256, MinimalEthSpec,
BeaconState, BeaconStateError, BlockImportSource, ChainSpec, Checkpoint,
DEFAULT_PRE_ELECTRA_WS_PERIOD, EthSpec, ForkName, Hash256, MainnetEthSpec, MinimalEthSpec,
RelativeEpoch, Slot,
};

Expand All @@ -37,6 +38,27 @@ fn get_harness(validator_count: usize) -> BeaconChainHarness<EphemeralHarnessTyp
)
}

fn get_harness_with_spec(
validator_count: usize,
spec: &ChainSpec,
) -> BeaconChainHarness<EphemeralHarnessType<MainnetEthSpec>> {
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,
Expand Down Expand Up @@ -1059,3 +1081,28 @@ 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_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());
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_pre_electra);

// 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_post_electra);
}
10 changes: 10 additions & 0 deletions beacon_node/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1404,6 +1404,16 @@ pub fn cli_app() -> Command {
.help_heading(FLAG_HEADER)
.display_order(0)
)
.arg(
Arg::new("ignore-ws-check")
.long("ignore-ws-check")
.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.")
.action(ArgAction::SetTrue)
.help_heading(FLAG_HEADER)
.display_order(0)
)
.arg(
Arg::new("builder-fallback-skips")
.long("builder-fallback-skips")
Expand Down
2 changes: 2 additions & 0 deletions beacon_node/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,8 @@ pub fn get_config<E: EthSpec>(

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.
*/
Expand Down
7 changes: 1 addition & 6 deletions beacon_node/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,9 @@ use types::{ChainSpec, Epoch, EthSpec, ForkName};
pub type ProductionClient<E> =
Client<Witness<SystemTimeSlotClock, E, BeaconNodeBackend<E>, BeaconNodeBackend<E>>>;

/// 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<E: EthSpec>(ProductionClient<E>);

impl<E: EthSpec> ProductionBeaconNode<E> {
Expand Down
6 changes: 6 additions & 0 deletions book/src/help_bn.md
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,12 @@ Flags:
--http-enable-tls
Serves the RESTful HTTP API server over TLS. This feature is currently
experimental.
--ignore-ws-check
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.
--import-all-attestations
Import and aggregate all attestations, regardless of validator
subscriptions. This will only import attestations from
Expand Down
104 changes: 104 additions & 0 deletions consensus/types/src/state/beacon_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,21 @@ use crate::{
};

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 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;

// `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;

pub type Validators<E> = List<Validator, <E as EthSpec>::ValidatorRegistryLimit>;
pub type Balances<E> = List<u64, <E as EthSpec>::ValidatorRegistryLimit>;

Expand Down Expand Up @@ -2842,6 +2854,26 @@ impl<E: EthSpec> BeaconState<E> {

Ok(())
}

/// Returns the weak subjectivity period for `self`
pub fn compute_weak_subjectivity_period(
&self,
spec: &ChainSpec,
) -> Result<Epoch, BeaconStateError> {
let total_active_balance = self.get_total_active_balance()?;
let fork_name = self.fork_name_unchecked();

if fork_name.electra_enabled() {
let balance_churn_limit = self.get_balance_churn_limit(spec)?;
compute_weak_subjectivity_period_electra(
total_active_balance,
balance_churn_limit,
spec,
)
} else {
Ok(Epoch::new(DEFAULT_PRE_ELECTRA_WS_PERIOD))
}
}
}

impl<E: EthSpec> ForkVersionDecode for BeaconState<E> {
Expand Down Expand Up @@ -3113,3 +3145,75 @@ impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for BeaconState<E> {
))
}
}

/// 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<Epoch, BeaconStateError> {
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)
}

#[cfg(test)]
mod weak_subjectivity_tests {
use crate::state::beacon_state::compute_weak_subjectivity_period_electra;
use crate::{ChainSpec, Epoch, EthSpec, MainnetEthSpec};

const GWEI_PER_ETH: u64 = 1_000_000_000;

#[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),
// 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 {
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)
}
}
2 changes: 1 addition & 1 deletion consensus/types/src/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
15 changes: 15 additions & 0 deletions lighthouse/tests/beacon_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,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()
Expand Down