mirror of
https://github.com/sigp/lighthouse.git
synced 2026-07-05 05:44:30 +00:00
Implement weak subjectivity safety checks (#7347)
Closes #7273 https://github.com/ethereum/consensus-specs/pull/4179 Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu> Co-Authored-By: Eitan Seri- Levi <eserilev@gmail.com> Co-Authored-By: Michael Sproul <michaelsproul@users.noreply.github.com> Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com>
This commit is contained in:
@@ -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 tree_hash::TreeHash;
|
||||
use types::data::CustodyIndex;
|
||||
use types::{
|
||||
@@ -848,6 +848,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| {
|
||||
|
||||
@@ -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.
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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::<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 {
|
||||
|
||||
@@ -117,6 +117,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()
|
||||
};
|
||||
|
||||
@@ -15,7 +15,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,
|
||||
};
|
||||
|
||||
@@ -38,6 +39,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,
|
||||
@@ -1083,3 +1105,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);
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -780,6 +780,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.
|
||||
*/
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -509,6 +509,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
|
||||
|
||||
@@ -55,9 +55,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>;
|
||||
|
||||
@@ -3007,6 +3019,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))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the payload timeliness committee for the given `slot`.
|
||||
///
|
||||
/// Requires the committee cache to be initialized.
|
||||
@@ -3382,3 +3414,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user