Files
lighthouse/beacon_node/beacon_chain/src/custody_context.rs
Eitan Seri-Levi 3ecf964385 Replace INTERVALS_PER_SLOT with explicit slot component times (#7944)
https://github.com/ethereum/consensus-specs/pull/4476


  


Co-Authored-By: Barnabas Busa <barnabas.busa@ethereum.org>

Co-Authored-By: Eitan Seri- Levi <eserilev@gmail.com>

Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu>

Co-Authored-By: Michael Sproul <michaelsproul@users.noreply.github.com>

Co-Authored-By: Michael Sproul <michael@sigmaprime.io>
2026-02-02 05:58:42 +00:00

1545 lines
59 KiB
Rust

use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use std::marker::PhantomData;
use std::{
collections::{BTreeMap, HashMap},
sync::atomic::{AtomicU64, Ordering},
};
use tracing::{debug, warn};
use types::{ChainSpec, ColumnIndex, Epoch, EthSpec, Slot};
/// A delay before making the CGC change effective to the data availability checker.
pub const CUSTODY_CHANGE_DA_EFFECTIVE_DELAY_SECONDS: u64 = 30;
/// Number of slots after which a validator's registration is removed if it has not re-registered.
const VALIDATOR_REGISTRATION_EXPIRY_SLOTS: Slot = Slot::new(256);
type ValidatorsAndBalances = Vec<(usize, u64)>;
type SlotAndEffectiveBalance = (Slot, u64);
/// This currently just registers increases in validator count.
/// Does not handle decreasing validator counts
#[derive(Default, Debug)]
struct ValidatorRegistrations {
/// Set of all validators that is registered to this node along with its effective balance
///
/// Key is validator index and value is effective_balance.
validators: HashMap<usize, SlotAndEffectiveBalance>,
/// Maintains the validator custody requirement at a given epoch.
///
/// Note: Only stores the epoch value when there's a change in custody requirement.
/// So if epoch 10 and 11 has the same custody requirement, only 10 is stored.
/// This map is only pruned during custody backfill. If epoch 11 has custody requirements
/// that are then backfilled to epoch 10, the value at epoch 11 will be removed and epoch 10
/// will be added to the map instead. This should keep map size constrained to a maximum
/// value of 128.
///
/// If the node's is started with a cgc override (i.e. supernode/semi-supernode flag), the cgc
/// value is inserted into this map on initialisation with epoch set to 0. For a semi-supernode,
/// this means the custody requirement can still be increased if validator custody exceeds
/// 64 columns.
epoch_validator_custody_requirements: BTreeMap<Epoch, u64>,
}
impl ValidatorRegistrations {
/// Initialise the validator registration with some default custody requirements.
///
/// If a `cgc_override` value is specified, the cgc value is inserted into the registration map
/// and is equivalent to registering validator(s) with the same custody requirement.
///
/// The node will backfill all the way back to either data_availability_boundary or fulu epoch,
/// and because this is a fresh node, setting the epoch to 0 is fine, as backfill will be done via
/// backfill sync instead of column backfill.
fn new(cgc_override: Option<u64>) -> Self {
let mut registrations = ValidatorRegistrations {
validators: Default::default(),
epoch_validator_custody_requirements: Default::default(),
};
if let Some(custody_count) = cgc_override {
registrations
.epoch_validator_custody_requirements
.insert(Epoch::new(0), custody_count);
}
registrations
}
/// Returns the validator custody requirement at the latest epoch.
fn latest_validator_custody_requirement(&self) -> Option<u64> {
self.epoch_validator_custody_requirements
.last_key_value()
.map(|(_, v)| *v)
}
/// Lookup the active custody requirement at the given epoch.
fn custody_requirement_at_epoch(&self, epoch: Epoch) -> Option<u64> {
self.epoch_validator_custody_requirements
.range(..=epoch)
.last()
.map(|(_, custody_count)| *custody_count)
}
/// Register a new validator index and updates the list of validators if required.
/// Returns `Some((effective_epoch, new_cgc))` if the registration results in a CGC update.
pub(crate) fn register_validators<E: EthSpec>(
&mut self,
validators_and_balance: ValidatorsAndBalances,
current_slot: Slot,
spec: &ChainSpec,
) -> Option<(Epoch, u64)> {
for (validator_index, effective_balance) in validators_and_balance {
self.validators
.insert(validator_index, (current_slot, effective_balance));
}
// Drop validators that haven't re-registered with the node for `VALIDATOR_REGISTRATION_EXPIRY_SLOTS`.
self.validators
.retain(|_, (slot, _)| *slot >= current_slot - VALIDATOR_REGISTRATION_EXPIRY_SLOTS);
// Each `BALANCE_PER_ADDITIONAL_CUSTODY_GROUP` effectively contributes one unit of "weight".
let validator_custody_units = self.validators.values().map(|(_, eb)| eb).sum::<u64>()
/ spec.balance_per_additional_custody_group;
let validator_custody_requirement =
get_validators_custody_requirement(validator_custody_units, spec);
debug!(
validator_custody_units,
validator_custody_requirement, "Registered validators"
);
// If registering the new validator increased the total validator "units", then
// add a new entry for the current epoch
if Some(validator_custody_requirement) > self.latest_validator_custody_requirement() {
// Apply the change from the next epoch after adding some delay buffer to ensure
// the node has enough time to subscribe to subnets etc, and to avoid having
// inconsistent column counts within an epoch.
let effective_delay_slots = CUSTODY_CHANGE_DA_EFFECTIVE_DELAY_SECONDS
.checked_div(spec.get_slot_duration().as_secs())
.unwrap_or(1);
let effective_epoch =
(current_slot + effective_delay_slots).epoch(E::slots_per_epoch()) + 1;
self.epoch_validator_custody_requirements
.insert(effective_epoch, validator_custody_requirement);
Some((effective_epoch, validator_custody_requirement))
} else {
None
}
}
/// Updates the `epoch -> cgc` map after custody backfill has been completed for
/// the specified epoch.
///
/// This is done by pruning all values on/after `effective_epoch` and updating the map to store
/// the latest validator custody requirements for the `effective_epoch`.
pub fn backfill_validator_custody_requirements(
&mut self,
effective_epoch: Epoch,
expected_cgc: u64,
) {
if let Some(latest_validator_custody) = self.latest_validator_custody_requirement() {
// If the expected cgc isn't equal to the latest validator custody a very recent cgc change may have occurred.
// We should not update the mapping.
if expected_cgc != latest_validator_custody {
return;
}
// Delete records if
// 1. The epoch is greater than or equal than `effective_epoch`
// 2. the cgc requirements match the latest validator custody requirements
self.epoch_validator_custody_requirements
.retain(|&epoch, custody_requirement| {
!(epoch >= effective_epoch && *custody_requirement == latest_validator_custody)
});
self.epoch_validator_custody_requirements
.insert(effective_epoch, latest_validator_custody);
}
}
/// Updates the `epoch -> cgc` map by pruning records before `effective_epoch`
/// while setting the `cgc` at `effective_epoch` to the latest validator custody requirement.
///
/// This is used to restart custody backfill sync at `effective_epoch`
pub fn reset_validator_custody_requirements(&mut self, effective_epoch: Epoch) {
if let Some(latest_validator_custody_requirements) =
self.latest_validator_custody_requirement()
{
self.epoch_validator_custody_requirements
.retain(|&epoch, _| epoch >= effective_epoch);
self.epoch_validator_custody_requirements
.insert(effective_epoch, latest_validator_custody_requirements);
};
}
}
/// Given the `validator_custody_units`, return the custody requirement based on
/// the spec parameters.
///
/// Note: a `validator_custody_units` here represents the number of 32 eth effective_balance
/// equivalent to `BALANCE_PER_ADDITIONAL_CUSTODY_GROUP`.
///
/// For e.g. a validator with eb 32 eth is 1 unit.
/// a validator with eb 65 eth is 65 // 32 = 2 units.
///
/// See https://github.com/ethereum/consensus-specs/blob/dev/specs/fulu/validator.md#validator-custody
fn get_validators_custody_requirement(validator_custody_units: u64, spec: &ChainSpec) -> u64 {
std::cmp::min(
std::cmp::max(validator_custody_units, spec.validator_custody_requirement),
spec.number_of_custody_groups,
)
}
/// Indicates the different "modes" that a node can run based on the cli
/// parameters that are relevant for computing the custody count.
///
/// The custody count is derived from 2 values:
/// 1. The number of validators attached to the node and the spec parameters
/// that attach custody weight to attached validators.
/// 2. The cli parameters that the current node is running with.
///
/// We always persist the validator custody units to the db across restarts
/// such that we know the validator custody units at any given epoch in the past.
/// However, knowing the cli parameter at any given epoch is a pain to maintain
/// and unnecessary.
///
/// Therefore, the custody count at any point in time is calculated as the max of
/// the validator custody at that time and the current cli params.
///
/// Choosing the max ensures that we always have the minimum required columns, and
/// we can adjust the `status.earliest_available_slot` value to indicate to our peers
/// the columns that we can guarantee to serve.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
pub enum NodeCustodyType {
/// The node is running with cli parameters to indicate that it
/// wants to subscribe to all columns.
Supernode,
/// The node is running with cli parameters to indicate that it
/// wants to subscribe to the minimum number of columns to enable
/// reconstruction (50%) of the full blob data on demand.
SemiSupernode,
/// The node isn't running with any explicit cli parameters
/// or is running with cli parameters to indicate that it wants
/// to only subscribe to the minimal custody requirements.
#[default]
Fullnode,
}
impl NodeCustodyType {
pub fn get_custody_count_override(&self, spec: &ChainSpec) -> Option<u64> {
match self {
Self::Fullnode => None,
Self::SemiSupernode => Some(spec.number_of_custody_groups / 2),
Self::Supernode => Some(spec.number_of_custody_groups),
}
}
}
/// Contains all the information the node requires to calculate the
/// number of columns to be custodied when checking for DA.
#[derive(Debug)]
pub struct CustodyContext<E: EthSpec> {
/// The Number of custody groups required based on the number of validators
/// that is attached to this node.
///
/// This is the number that we use to compute the custody group count that
/// we require for data availability check, and we use to advertise to our peers in the metadata
/// and enr values.
validator_custody_count: AtomicU64,
/// Maintains all the validators that this node is connected to currently
validator_registrations: RwLock<ValidatorRegistrations>,
/// Stores an immutable, ordered list of all data column indices as determined by the node's NodeID
/// on startup. This used to determine the node's custody columns.
ordered_custody_column_indices: Vec<ColumnIndex>,
_phantom_data: PhantomData<E>,
}
impl<E: EthSpec> CustodyContext<E> {
/// Create a new custody default custody context object when no persisted object
/// exists.
///
/// The `node_custody_type` value is based on current cli parameters.
pub fn new(
node_custody_type: NodeCustodyType,
ordered_custody_column_indices: Vec<ColumnIndex>,
spec: &ChainSpec,
) -> Self {
let cgc_override = node_custody_type.get_custody_count_override(spec);
// If there's no override, we initialise `validator_custody_count` to 0. This has been the
// existing behaviour and we maintain this for now to avoid a semantic schema change until
// a later release.
Self {
validator_custody_count: AtomicU64::new(cgc_override.unwrap_or(0)),
validator_registrations: RwLock::new(ValidatorRegistrations::new(cgc_override)),
ordered_custody_column_indices,
_phantom_data: PhantomData,
}
}
/// Restore the custody context from disk.
///
/// # Behavior
/// * If [`NodeCustodyType::get_custody_count_override`] < validator_custody_at_head, it means
/// validators have increased the CGC beyond the derived CGC from cli flags. We ignore the CLI input.
/// * If [`NodeCustodyType::get_custody_count_override`] > validator_custody_at_head, it means the user has
/// changed the node's custody type via either the --supernode or --semi-supernode flags which
/// has resulted in a CGC increase. **The new CGC will be made effective from the next epoch**.
///
/// # Returns
/// A tuple containing:
/// * `Self` - The restored custody context with updated CGC at head
/// * `Option<CustodyCountChanged>` - `Some` if the CLI flag caused a CGC increase (triggering backfill),
/// `None` if no CGC change occurred or reduction was prevented
pub fn new_from_persisted_custody_context(
ssz_context: CustodyContextSsz,
node_custody_type: NodeCustodyType,
head_epoch: Epoch,
ordered_custody_column_indices: Vec<ColumnIndex>,
spec: &ChainSpec,
) -> (Self, Option<CustodyCountChanged>) {
let CustodyContextSsz {
mut validator_custody_at_head,
mut epoch_validator_custody_requirements,
persisted_is_supernode: _,
} = ssz_context;
let mut custody_count_changed = None;
if let Some(cgc_from_cli) = node_custody_type.get_custody_count_override(spec) {
debug!(
?node_custody_type,
persisted_custody_count = validator_custody_at_head,
"Initialising from persisted custody context"
);
if cgc_from_cli > validator_custody_at_head {
// Make the CGC from CLI effective from the next epoch
let effective_epoch = head_epoch + 1;
let old_custody_group_count = validator_custody_at_head;
validator_custody_at_head = cgc_from_cli;
let sampling_count = spec
.sampling_size_custody_groups(cgc_from_cli)
.expect("should compute node sampling size from valid chain spec");
epoch_validator_custody_requirements.push((effective_epoch, cgc_from_cli));
custody_count_changed = Some(CustodyCountChanged {
new_custody_group_count: validator_custody_at_head,
old_custody_group_count,
sampling_count,
effective_epoch,
});
debug!(
info = "new CGC will be effective from the next epoch",
?node_custody_type,
old_cgc = old_custody_group_count,
new_cgc = validator_custody_at_head,
effective_epoch = %effective_epoch,
"Node custody type change caused a custody count increase",
);
} else if cgc_from_cli < validator_custody_at_head {
// We don't currently support reducing CGC for simplicity.
// A common scenario is that user may restart with a CLI flag, but the validators
// are only attached later, and we end up having CGC inconsistency.
warn!(
info = "node will continue to run with the current custody count",
current_custody_count = validator_custody_at_head,
node_custody_type = ?node_custody_type,
"Reducing CGC is currently not supported without a resync and will have no effect",
);
}
}
let custody_context = CustodyContext {
validator_custody_count: AtomicU64::new(validator_custody_at_head),
validator_registrations: RwLock::new(ValidatorRegistrations {
validators: Default::default(),
epoch_validator_custody_requirements: epoch_validator_custody_requirements
.into_iter()
.collect(),
}),
ordered_custody_column_indices,
_phantom_data: PhantomData,
};
(custody_context, custody_count_changed)
}
/// Register a new validator index and updates the list of validators if required.
///
/// Also modifies the internal structures if the validator custody has changed to
/// update the `custody_column_count`.
///
/// Returns `Some` along with the updated custody group count if it has changed, otherwise returns `None`.
pub fn register_validators(
&self,
validators_and_balance: ValidatorsAndBalances,
current_slot: Slot,
spec: &ChainSpec,
) -> Option<CustodyCountChanged> {
let Some((effective_epoch, new_validator_custody)) = self
.validator_registrations
.write()
.register_validators::<E>(validators_and_balance, current_slot, spec)
else {
return None;
};
let current_cgc = self.validator_custody_count.load(Ordering::Relaxed);
if new_validator_custody != current_cgc {
debug!(
old_count = current_cgc,
new_count = new_validator_custody,
"Validator count at head updated"
);
self.validator_custody_count
.store(new_validator_custody, Ordering::Relaxed);
let updated_cgc = self.custody_group_count_at_head(spec);
// Send the message to network only if there are more columns subnets to subscribe to
if updated_cgc > current_cgc {
debug!(
old_cgc = current_cgc,
updated_cgc, "Custody group count updated"
);
return Some(CustodyCountChanged {
new_custody_group_count: updated_cgc,
old_custody_group_count: current_cgc,
sampling_count: self.num_of_custody_groups_to_sample(effective_epoch, spec),
effective_epoch,
});
}
}
None
}
/// This function is used to determine the custody group count at head ONLY.
/// Do NOT use this directly for data availability check, use `self.sampling_size` instead as
/// CGC can change over epochs.
pub fn custody_group_count_at_head(&self, spec: &ChainSpec) -> u64 {
let validator_custody_count_at_head = self.validator_custody_count.load(Ordering::Relaxed);
// If there are no validators, return the minimum custody_requirement
if validator_custody_count_at_head > 0 {
validator_custody_count_at_head
} else {
spec.custody_requirement
}
}
/// This function is used to determine the custody group count at a given epoch.
///
/// This differs from the number of custody groups sampled per slot, as the spec requires a
/// minimum sampling size which may exceed the custody group count (CGC).
///
/// See also: [`Self::num_of_custody_groups_to_sample`].
pub fn custody_group_count_at_epoch(&self, epoch: Epoch, spec: &ChainSpec) -> u64 {
self.validator_registrations
.read()
.custody_requirement_at_epoch(epoch)
.unwrap_or(spec.custody_requirement)
}
/// Returns the count of custody groups this node must _sample_ for a block at `epoch` to import.
pub fn num_of_custody_groups_to_sample(&self, epoch: Epoch, spec: &ChainSpec) -> u64 {
let custody_group_count = self.custody_group_count_at_epoch(epoch, spec);
spec.sampling_size_custody_groups(custody_group_count)
.expect("should compute node sampling size from valid chain spec")
}
/// Returns the count of columns this node must _sample_ for a block at `epoch` to import.
pub fn num_of_data_columns_to_sample(&self, epoch: Epoch, spec: &ChainSpec) -> usize {
let custody_group_count = self.custody_group_count_at_epoch(epoch, spec);
spec.sampling_size_columns::<E>(custody_group_count)
.expect("should compute node sampling size from valid chain spec")
}
/// Returns whether the node should attempt reconstruction at a given epoch.
pub fn should_attempt_reconstruction(&self, epoch: Epoch, spec: &ChainSpec) -> bool {
let min_columns_for_reconstruction = E::number_of_columns() / 2;
// performing reconstruction is not necessary if sampling column count is exactly 50%,
// because the node doesn't need the remaining columns.
self.num_of_data_columns_to_sample(epoch, spec) > min_columns_for_reconstruction
}
/// Returns the ordered list of column indices that should be sampled for data availability checking at the given epoch.
///
/// # Parameters
/// * `epoch` - Epoch to determine sampling columns for
/// * `spec` - Chain specification containing sampling parameters
///
/// # Returns
/// A slice of ordered column indices that should be sampled for this epoch based on the node's custody configuration
pub fn sampling_columns_for_epoch(&self, epoch: Epoch, spec: &ChainSpec) -> &[ColumnIndex] {
let num_of_columns_to_sample = self.num_of_data_columns_to_sample(epoch, spec);
&self.ordered_custody_column_indices[..num_of_columns_to_sample]
}
/// Returns the ordered list of column indices that the node is assigned to custody
/// (and advertised to peers) at the given epoch. If epoch is `None`, this function
/// computes the custody columns at head.
///
/// This method differs from [`self::sampling_columns_for_epoch`] which returns all sampling columns.
/// The columns returned by this method are either identical to or a subset of the sampling columns,
/// representing only those columns that this node is responsible for maintaining custody of.
///
/// # Parameters
/// * `epoch_opt` - Optional epoch to determine custody columns for.
///
/// # Returns
/// A slice of ordered custody column indices for this epoch based on the node's custody configuration
pub fn custody_columns_for_epoch(
&self,
epoch_opt: Option<Epoch>,
spec: &ChainSpec,
) -> &[ColumnIndex] {
let custody_group_count = if let Some(epoch) = epoch_opt {
self.custody_group_count_at_epoch(epoch, spec) as usize
} else {
self.custody_group_count_at_head(spec) as usize
};
// This is an unnecessary conversion for spec compliance, basically just multiplying by 1.
let columns_per_custody_group = spec.data_columns_per_group::<E>() as usize;
let custody_column_count = columns_per_custody_group * custody_group_count;
&self.ordered_custody_column_indices[..custody_column_count]
}
/// The node has completed backfill for this epoch. Update the internal records so the function
/// [`Self::custody_columns_for_epoch()`] returns up-to-date results.
pub fn update_and_backfill_custody_count_at_epoch(
&self,
effective_epoch: Epoch,
expected_cgc: u64,
) {
self.validator_registrations
.write()
.backfill_validator_custody_requirements(effective_epoch, expected_cgc);
}
/// The node is attempting to restart custody backfill. Update the internal records so that
/// custody backfill can start backfilling at `effective_epoch`.
pub fn reset_validator_custody_requirements(&self, effective_epoch: Epoch) {
self.validator_registrations
.write()
.reset_validator_custody_requirements(effective_epoch);
}
}
/// Indicates that the custody group count (CGC) has increased.
///
/// CGC increases can occur due to:
/// 1. Validator registrations increasing effective balance beyond current CGC
/// 2. CLI flag changes (e.g., switching to --supernode or --semi-supernode)
///
/// This struct is used to trigger column backfill and network subnet subscription updates.
pub struct CustodyCountChanged {
pub new_custody_group_count: u64,
pub old_custody_group_count: u64,
pub sampling_count: u64,
pub effective_epoch: Epoch,
}
/// The custody information that gets persisted across runs.
#[derive(Debug, Encode, Decode, Clone)]
pub struct CustodyContextSsz {
pub validator_custody_at_head: u64,
/// DEPRECATED. This field is no longer in used and will be removed in a future release.
pub persisted_is_supernode: bool,
pub epoch_validator_custody_requirements: Vec<(Epoch, u64)>,
}
impl<E: EthSpec> From<&CustodyContext<E>> for CustodyContextSsz {
fn from(context: &CustodyContext<E>) -> Self {
CustodyContextSsz {
validator_custody_at_head: context.validator_custody_count.load(Ordering::Relaxed),
// This field is deprecated and has no effect
persisted_is_supernode: false,
epoch_validator_custody_requirements: context
.validator_registrations
.read()
.epoch_validator_custody_requirements
.iter()
.map(|(epoch, count)| (*epoch, *count))
.collect(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::generate_data_column_indices_rand_order;
use types::MainnetEthSpec;
type E = MainnetEthSpec;
fn setup_custody_context(
spec: &ChainSpec,
head_epoch: Epoch,
epoch_and_cgc_tuples: Vec<(Epoch, u64)>,
) -> CustodyContext<E> {
let cgc_at_head = epoch_and_cgc_tuples.last().unwrap().1;
let ssz_context = CustodyContextSsz {
validator_custody_at_head: cgc_at_head,
persisted_is_supernode: false,
epoch_validator_custody_requirements: epoch_and_cgc_tuples,
};
let (custody_context, _) = CustodyContext::<E>::new_from_persisted_custody_context(
ssz_context,
NodeCustodyType::Fullnode,
head_epoch,
generate_data_column_indices_rand_order::<E>(),
spec,
);
custody_context
}
fn complete_backfill_for_epochs(
custody_context: &CustodyContext<E>,
start_epoch: Epoch,
end_epoch: Epoch,
expected_cgc: u64,
) {
assert!(start_epoch >= end_epoch);
// Call from end_epoch down to start_epoch (inclusive), simulating backfill
for epoch in (end_epoch.as_u64()..=start_epoch.as_u64()).rev() {
custody_context
.update_and_backfill_custody_count_at_epoch(Epoch::new(epoch), expected_cgc);
}
}
/// Helper function to test CGC increases when switching node custody types.
/// Verifies that CustodyCountChanged is returned with correct values and
/// that custody_group_count_at_epoch returns appropriate values for current and next epoch.
fn assert_custody_type_switch_increases_cgc(
persisted_cgc: u64,
target_node_custody_type: NodeCustodyType,
expected_new_cgc: u64,
head_epoch: Epoch,
spec: &ChainSpec,
) {
let ssz_context = CustodyContextSsz {
validator_custody_at_head: persisted_cgc,
persisted_is_supernode: false,
epoch_validator_custody_requirements: vec![(Epoch::new(0), persisted_cgc)],
};
let (custody_context, custody_count_changed) =
CustodyContext::<E>::new_from_persisted_custody_context(
ssz_context,
target_node_custody_type,
head_epoch,
generate_data_column_indices_rand_order::<E>(),
spec,
);
// Verify CGC increased
assert_eq!(
custody_context.custody_group_count_at_head(spec),
expected_new_cgc,
"cgc should increase from {} to {}",
persisted_cgc,
expected_new_cgc
);
// Verify CustodyCountChanged is returned with correct values
let cgc_changed = custody_count_changed.expect("CustodyCountChanged should be returned");
assert_eq!(
cgc_changed.new_custody_group_count, expected_new_cgc,
"new_custody_group_count should be {}",
expected_new_cgc
);
assert_eq!(
cgc_changed.old_custody_group_count, persisted_cgc,
"old_custody_group_count should be {}",
persisted_cgc
);
assert_eq!(
cgc_changed.effective_epoch,
head_epoch + 1,
"effective epoch should be head_epoch + 1"
);
assert_eq!(
cgc_changed.sampling_count,
spec.sampling_size_custody_groups(expected_new_cgc)
.expect("should compute sampling size"),
"sampling_count should match expected value"
);
// Verify custody_group_count_at_epoch returns correct values
assert_eq!(
custody_context.custody_group_count_at_epoch(head_epoch, spec),
persisted_cgc,
"current epoch should still use old cgc ({})",
persisted_cgc
);
assert_eq!(
custody_context.custody_group_count_at_epoch(head_epoch + 1, spec),
expected_new_cgc,
"next epoch should use new cgc ({})",
expected_new_cgc
);
}
/// Helper function to test CGC reduction prevention when switching node custody types.
/// Verifies that CGC stays at the persisted value and CustodyCountChanged is not returned.
fn assert_custody_type_switch_unchanged_cgc(
persisted_cgc: u64,
target_node_custody_type: NodeCustodyType,
head_epoch: Epoch,
spec: &ChainSpec,
) {
let ssz_context = CustodyContextSsz {
validator_custody_at_head: persisted_cgc,
persisted_is_supernode: false,
epoch_validator_custody_requirements: vec![(Epoch::new(0), persisted_cgc)],
};
let (custody_context, custody_count_changed) =
CustodyContext::<E>::new_from_persisted_custody_context(
ssz_context,
target_node_custody_type,
head_epoch,
generate_data_column_indices_rand_order::<E>(),
spec,
);
// Verify CGC stays at persisted value (no reduction)
assert_eq!(
custody_context.custody_group_count_at_head(spec),
persisted_cgc,
"cgc should remain at {} (reduction not supported)",
persisted_cgc
);
// Verify no CustodyCountChanged is returned (no change occurred)
assert!(
custody_count_changed.is_none(),
"CustodyCountChanged should not be returned when CGC doesn't change"
);
}
#[test]
fn no_validators_supernode_default() {
let spec = E::default_spec();
let custody_context = CustodyContext::<E>::new(
NodeCustodyType::Supernode,
generate_data_column_indices_rand_order::<E>(),
&spec,
);
assert_eq!(
custody_context.custody_group_count_at_head(&spec),
spec.number_of_custody_groups
);
assert_eq!(
custody_context.num_of_custody_groups_to_sample(Epoch::new(0), &spec),
spec.number_of_custody_groups
);
}
#[test]
fn no_validators_semi_supernode_default() {
let spec = E::default_spec();
let custody_context = CustodyContext::<E>::new(
NodeCustodyType::SemiSupernode,
generate_data_column_indices_rand_order::<E>(),
&spec,
);
assert_eq!(
custody_context.custody_group_count_at_head(&spec),
spec.number_of_custody_groups / 2
);
assert_eq!(
custody_context.num_of_custody_groups_to_sample(Epoch::new(0), &spec),
spec.number_of_custody_groups / 2
);
}
#[test]
fn no_validators_fullnode_default() {
let spec = E::default_spec();
let custody_context = CustodyContext::<E>::new(
NodeCustodyType::Fullnode,
generate_data_column_indices_rand_order::<E>(),
&spec,
);
assert_eq!(
custody_context.custody_group_count_at_head(&spec),
spec.custody_requirement,
"head custody count should be minimum spec custody requirement"
);
assert_eq!(
custody_context.num_of_custody_groups_to_sample(Epoch::new(0), &spec),
spec.samples_per_slot
);
}
#[test]
fn register_single_validator_should_update_cgc() {
let spec = E::default_spec();
let custody_context = CustodyContext::<E>::new(
NodeCustodyType::Fullnode,
generate_data_column_indices_rand_order::<E>(),
&spec,
);
let bal_per_additional_group = spec.balance_per_additional_custody_group;
let min_val_custody_requirement = spec.validator_custody_requirement;
// One single node increases its balance over 3 epochs.
let validators_and_expected_cgc_change = vec![
(
vec![(0, bal_per_additional_group)],
Some(min_val_custody_requirement),
),
// No CGC change at 8 custody units, as it's the minimum requirement
(vec![(0, 8 * bal_per_additional_group)], None),
(vec![(0, 10 * bal_per_additional_group)], Some(10)),
];
register_validators_and_assert_cgc::<E>(
&custody_context,
validators_and_expected_cgc_change,
&spec,
);
}
#[test]
fn register_multiple_validators_should_update_cgc() {
let spec = E::default_spec();
let custody_context = CustodyContext::<E>::new(
NodeCustodyType::Fullnode,
generate_data_column_indices_rand_order::<E>(),
&spec,
);
let bal_per_additional_group = spec.balance_per_additional_custody_group;
let min_val_custody_requirement = spec.validator_custody_requirement;
// Add 3 validators over 3 epochs.
let validators_and_expected_cgc = vec![
(
vec![(0, bal_per_additional_group)],
Some(min_val_custody_requirement),
),
(
vec![
(0, bal_per_additional_group),
(1, 7 * bal_per_additional_group),
],
// No CGC change at 8 custody units, as it's the minimum requirement
None,
),
(
vec![
(0, bal_per_additional_group),
(1, 7 * bal_per_additional_group),
(2, 2 * bal_per_additional_group),
],
Some(10),
),
];
register_validators_and_assert_cgc::<E>(
&custody_context,
validators_and_expected_cgc,
&spec,
);
}
#[test]
fn register_validators_should_not_update_cgc_for_supernode() {
let spec = E::default_spec();
let custody_context = CustodyContext::<E>::new(
NodeCustodyType::Supernode,
generate_data_column_indices_rand_order::<E>(),
&spec,
);
let bal_per_additional_group = spec.balance_per_additional_custody_group;
// Add 3 validators over 3 epochs.
let validators_and_expected_cgc = vec![
(vec![(0, bal_per_additional_group)], None),
(
vec![
(0, bal_per_additional_group),
(1, 7 * bal_per_additional_group),
],
None,
),
(
vec![
(0, bal_per_additional_group),
(1, 7 * bal_per_additional_group),
(2, 2 * bal_per_additional_group),
],
None,
),
];
register_validators_and_assert_cgc::<E>(
&custody_context,
validators_and_expected_cgc,
&spec,
);
let current_epoch = Epoch::new(2);
assert_eq!(
custody_context.num_of_custody_groups_to_sample(current_epoch, &spec),
spec.number_of_custody_groups
);
}
#[test]
fn cgc_change_should_be_effective_to_sampling_after_delay() {
let spec = E::default_spec();
let custody_context = CustodyContext::<E>::new(
NodeCustodyType::Fullnode,
generate_data_column_indices_rand_order::<E>(),
&spec,
);
let current_slot = Slot::new(10);
let current_epoch = current_slot.epoch(E::slots_per_epoch());
let default_sampling_size =
custody_context.num_of_custody_groups_to_sample(current_epoch, &spec);
let validator_custody_units = 10;
let _cgc_changed = custody_context.register_validators(
vec![(
0,
validator_custody_units * spec.balance_per_additional_custody_group,
)],
current_slot,
&spec,
);
// CGC update is not applied for `current_epoch`.
assert_eq!(
custody_context.num_of_custody_groups_to_sample(current_epoch, &spec),
default_sampling_size
);
// CGC update is applied for the next epoch.
assert_eq!(
custody_context.num_of_custody_groups_to_sample(current_epoch + 1, &spec),
validator_custody_units
);
}
#[test]
fn validator_dropped_after_no_registrations_within_expiry_should_not_reduce_cgc() {
let spec = E::default_spec();
let custody_context = CustodyContext::<E>::new(
NodeCustodyType::Fullnode,
generate_data_column_indices_rand_order::<E>(),
&spec,
);
let current_slot = Slot::new(10);
let val_custody_units_1 = 10;
let val_custody_units_2 = 5;
// GIVEN val_1 and val_2 registered at `current_slot`
let _ = custody_context.register_validators(
vec![
(
1,
val_custody_units_1 * spec.balance_per_additional_custody_group,
),
(
2,
val_custody_units_2 * spec.balance_per_additional_custody_group,
),
],
current_slot,
&spec,
);
// WHEN val_1 re-registered, but val_2 did not re-register after `VALIDATOR_REGISTRATION_EXPIRY_SLOTS + 1` slots
let cgc_changed_opt = custody_context.register_validators(
vec![(
1,
val_custody_units_1 * spec.balance_per_additional_custody_group,
)],
current_slot + VALIDATOR_REGISTRATION_EXPIRY_SLOTS + 1,
&spec,
);
// THEN the reduction from dropping val_2 balance should NOT result in a CGC reduction
assert!(cgc_changed_opt.is_none(), "CGC should remain unchanged");
assert_eq!(
custody_context.custody_group_count_at_head(&spec),
val_custody_units_1 + val_custody_units_2
)
}
#[test]
fn validator_dropped_after_no_registrations_within_expiry() {
let spec = E::default_spec();
let custody_context = CustodyContext::<E>::new(
NodeCustodyType::Fullnode,
generate_data_column_indices_rand_order::<E>(),
&spec,
);
let current_slot = Slot::new(10);
let val_custody_units_1 = 10;
let val_custody_units_2 = 5;
let val_custody_units_3 = 6;
// GIVEN val_1 and val_2 registered at `current_slot`
let _ = custody_context.register_validators(
vec![
(
1,
val_custody_units_1 * spec.balance_per_additional_custody_group,
),
(
2,
val_custody_units_2 * spec.balance_per_additional_custody_group,
),
],
current_slot,
&spec,
);
// WHEN val_1 and val_3 registered, but val_3 did not re-register after `VALIDATOR_REGISTRATION_EXPIRY_SLOTS + 1` slots
let cgc_changed = custody_context.register_validators(
vec![
(
1,
val_custody_units_1 * spec.balance_per_additional_custody_group,
),
(
3,
val_custody_units_3 * spec.balance_per_additional_custody_group,
),
],
current_slot + VALIDATOR_REGISTRATION_EXPIRY_SLOTS + 1,
&spec,
);
// THEN CGC should increase, BUT val_2 balance should NOT be included in CGC
assert_eq!(
cgc_changed
.expect("CGC should change")
.new_custody_group_count,
val_custody_units_1 + val_custody_units_3
);
}
/// Update the validator every epoch and assert cgc against expected values.
fn register_validators_and_assert_cgc<E: EthSpec>(
custody_context: &CustodyContext<E>,
validators_and_expected_cgc_changed: Vec<(ValidatorsAndBalances, Option<u64>)>,
spec: &ChainSpec,
) {
for (idx, (validators_and_balance, expected_cgc_change)) in
validators_and_expected_cgc_changed.into_iter().enumerate()
{
let epoch = Epoch::new(idx as u64);
let updated_custody_count_opt = custody_context
.register_validators(
validators_and_balance,
epoch.start_slot(E::slots_per_epoch()),
spec,
)
.map(|c| c.new_custody_group_count);
assert_eq!(updated_custody_count_opt, expected_cgc_change);
}
}
#[test]
fn custody_columns_for_epoch_no_validators_fullnode() {
let spec = E::default_spec();
let ordered_custody_column_indices = generate_data_column_indices_rand_order::<E>();
let custody_context = CustodyContext::<E>::new(
NodeCustodyType::Fullnode,
ordered_custody_column_indices,
&spec,
);
assert_eq!(
custody_context.custody_columns_for_epoch(None, &spec).len(),
spec.custody_requirement as usize
);
}
#[test]
fn custody_columns_for_epoch_no_validators_supernode() {
let spec = E::default_spec();
let ordered_custody_column_indices = generate_data_column_indices_rand_order::<E>();
let custody_context = CustodyContext::<E>::new(
NodeCustodyType::Supernode,
ordered_custody_column_indices,
&spec,
);
assert_eq!(
custody_context.custody_columns_for_epoch(None, &spec).len(),
spec.number_of_custody_groups as usize
);
}
#[test]
fn custody_columns_for_epoch_with_validators_should_match_cgc() {
let spec = E::default_spec();
let ordered_custody_column_indices = generate_data_column_indices_rand_order::<E>();
let custody_context = CustodyContext::<E>::new(
NodeCustodyType::Fullnode,
ordered_custody_column_indices,
&spec,
);
let val_custody_units = 10;
let _ = custody_context.register_validators(
vec![(
0,
val_custody_units * spec.balance_per_additional_custody_group,
)],
Slot::new(10),
&spec,
);
assert_eq!(
custody_context.custody_columns_for_epoch(None, &spec).len(),
val_custody_units as usize
);
}
#[test]
fn custody_columns_for_epoch_specific_epoch_uses_epoch_cgc() {
let spec = E::default_spec();
let ordered_custody_column_indices = generate_data_column_indices_rand_order::<E>();
let custody_context = CustodyContext::<E>::new(
NodeCustodyType::Fullnode,
ordered_custody_column_indices,
&spec,
);
let test_epoch = Epoch::new(5);
let expected_cgc = custody_context.custody_group_count_at_epoch(test_epoch, &spec);
assert_eq!(
custody_context
.custody_columns_for_epoch(Some(test_epoch), &spec)
.len(),
expected_cgc as usize
);
}
#[test]
fn restore_from_persisted_fullnode_no_validators() {
let spec = E::default_spec();
let ssz_context = CustodyContextSsz {
validator_custody_at_head: 0, // no validators
persisted_is_supernode: false,
epoch_validator_custody_requirements: vec![],
};
let (custody_context, _) = CustodyContext::<E>::new_from_persisted_custody_context(
ssz_context,
NodeCustodyType::Fullnode,
Epoch::new(0),
generate_data_column_indices_rand_order::<E>(),
&spec,
);
assert_eq!(
custody_context.custody_group_count_at_head(&spec),
spec.custody_requirement,
"restored custody group count should match fullnode default"
);
}
/// Tests CLI flag change: Fullnode (CGC=0) → Supernode (CGC=128)
/// CGC should increase and trigger backfill via CustodyCountChanged.
#[test]
fn restore_fullnode_then_switch_to_supernode_increases_cgc() {
let spec = E::default_spec();
let head_epoch = Epoch::new(10);
let supernode_cgc = spec.number_of_custody_groups;
assert_custody_type_switch_increases_cgc(
0,
NodeCustodyType::Supernode,
supernode_cgc,
head_epoch,
&spec,
);
}
/// Tests validator-driven CGC increase: Semi-supernode (CGC=64) → CGC=70
/// Semi-supernode can exceed 64 when validator effective balance increases CGC.
#[test]
fn restore_semi_supernode_with_validators_can_exceed_64() {
let spec = E::default_spec();
let semi_supernode_cgc = spec.number_of_custody_groups / 2; // 64
let custody_context = CustodyContext::<E>::new(
NodeCustodyType::SemiSupernode,
generate_data_column_indices_rand_order::<E>(),
&spec,
);
// Verify initial CGC is 64 (semi-supernode)
assert_eq!(
custody_context.custody_group_count_at_head(&spec),
semi_supernode_cgc,
"initial cgc should be 64"
);
// Register validators with 70 custody units (exceeding semi-supernode default)
let validator_custody_units = 70;
let current_slot = Slot::new(10);
let cgc_changed = custody_context.register_validators(
vec![(
0,
validator_custody_units * spec.balance_per_additional_custody_group,
)],
current_slot,
&spec,
);
// Verify CGC increased from 64 to 70
assert!(
cgc_changed.is_some(),
"CustodyCountChanged should be returned"
);
let cgc_changed = cgc_changed.unwrap();
assert_eq!(
cgc_changed.new_custody_group_count, validator_custody_units,
"cgc should increase to 70"
);
assert_eq!(
cgc_changed.old_custody_group_count, semi_supernode_cgc,
"old cgc should be 64"
);
// Verify the custody context reflects the new CGC
assert_eq!(
custody_context.custody_group_count_at_head(&spec),
validator_custody_units,
"custody_group_count_at_head should be 70"
);
}
/// Tests CLI flag change prevention: Supernode (CGC=128) → Fullnode (CGC stays 128)
/// CGC reduction is not supported - persisted value is retained.
#[test]
fn restore_supernode_then_switch_to_fullnode_uses_persisted() {
let spec = E::default_spec();
let supernode_cgc = spec.number_of_custody_groups;
assert_custody_type_switch_unchanged_cgc(
supernode_cgc,
NodeCustodyType::Fullnode,
Epoch::new(0),
&spec,
);
}
/// Tests CLI flag change prevention: Supernode (CGC=128) → Semi-supernode (CGC stays 128)
/// CGC reduction is not supported - persisted value is retained.
#[test]
fn restore_supernode_then_switch_to_semi_supernode_keeps_supernode_cgc() {
let spec = E::default_spec();
let supernode_cgc = spec.number_of_custody_groups;
let head_epoch = Epoch::new(10);
assert_custody_type_switch_unchanged_cgc(
supernode_cgc,
NodeCustodyType::SemiSupernode,
head_epoch,
&spec,
);
}
/// Tests CLI flag change: Fullnode with validators (CGC=32) → Semi-supernode (CGC=64)
/// CGC should increase and trigger backfill via CustodyCountChanged.
#[test]
fn restore_fullnode_with_validators_then_switch_to_semi_supernode() {
let spec = E::default_spec();
let persisted_cgc = 32u64;
let semi_supernode_cgc = spec.number_of_custody_groups / 2;
let head_epoch = Epoch::new(10);
assert_custody_type_switch_increases_cgc(
persisted_cgc,
NodeCustodyType::SemiSupernode,
semi_supernode_cgc,
head_epoch,
&spec,
);
}
/// Tests CLI flag change: Semi-supernode (CGC=64) → Supernode (CGC=128)
/// CGC should increase and trigger backfill via CustodyCountChanged.
#[test]
fn restore_semi_supernode_then_switch_to_supernode() {
let spec = E::default_spec();
let semi_supernode_cgc = spec.number_of_custody_groups / 2;
let supernode_cgc = spec.number_of_custody_groups;
let head_epoch = Epoch::new(10);
assert_custody_type_switch_increases_cgc(
semi_supernode_cgc,
NodeCustodyType::Supernode,
supernode_cgc,
head_epoch,
&spec,
);
}
/// Tests CLI flag change: Fullnode with validators (CGC=32) → Supernode (CGC=128)
/// CGC should increase and trigger backfill via CustodyCountChanged.
#[test]
fn restore_with_cli_flag_increases_cgc_from_nonzero() {
let spec = E::default_spec();
let persisted_cgc = 32u64;
let supernode_cgc = spec.number_of_custody_groups;
let head_epoch = Epoch::new(10);
assert_custody_type_switch_increases_cgc(
persisted_cgc,
NodeCustodyType::Supernode,
supernode_cgc,
head_epoch,
&spec,
);
}
#[test]
fn restore_with_validator_custody_history_across_epochs() {
let spec = E::default_spec();
let initial_cgc = 8u64;
let increased_cgc = 16u64;
let final_cgc = 32u64;
let ssz_context = CustodyContextSsz {
validator_custody_at_head: final_cgc,
persisted_is_supernode: false,
epoch_validator_custody_requirements: vec![
(Epoch::new(0), initial_cgc),
(Epoch::new(10), increased_cgc),
(Epoch::new(20), final_cgc),
],
};
let (custody_context, _) = CustodyContext::<E>::new_from_persisted_custody_context(
ssz_context,
NodeCustodyType::Fullnode,
Epoch::new(20),
generate_data_column_indices_rand_order::<E>(),
&spec,
);
// Verify head uses latest value
assert_eq!(
custody_context.custody_group_count_at_head(&spec),
final_cgc
);
// Verify historical epoch lookups work correctly
assert_eq!(
custody_context.custody_group_count_at_epoch(Epoch::new(5), &spec),
initial_cgc,
"epoch 5 should use initial cgc"
);
assert_eq!(
custody_context.custody_group_count_at_epoch(Epoch::new(15), &spec),
increased_cgc,
"epoch 15 should use increased cgc"
);
assert_eq!(
custody_context.custody_group_count_at_epoch(Epoch::new(25), &spec),
final_cgc,
"epoch 25 should use final cgc"
);
// Verify sampling size calculation uses correct historical values
assert_eq!(
custody_context.num_of_custody_groups_to_sample(Epoch::new(5), &spec),
spec.samples_per_slot,
"sampling at epoch 5 should use spec minimum since cgc is at minimum"
);
assert_eq!(
custody_context.num_of_custody_groups_to_sample(Epoch::new(25), &spec),
final_cgc,
"sampling at epoch 25 should match final cgc"
);
}
#[test]
fn backfill_single_cgc_increase_updates_past_epochs() {
let spec = E::default_spec();
let final_cgc = 32u64;
let default_cgc = spec.custody_requirement;
// Setup: Node restart after validators were registered, causing CGC increase to 32 at epoch 20
let head_epoch = Epoch::new(20);
let epoch_and_cgc_tuples = vec![(head_epoch, final_cgc)];
let custody_context = setup_custody_context(&spec, head_epoch, epoch_and_cgc_tuples);
assert_eq!(
custody_context.custody_group_count_at_epoch(Epoch::new(15), &spec),
default_cgc,
);
// Backfill from epoch 20 down to 15 (simulating backfill)
complete_backfill_for_epochs(&custody_context, head_epoch, Epoch::new(15), final_cgc);
// After backfilling to epoch 15, it should use latest CGC (32)
assert_eq!(
custody_context.custody_group_count_at_epoch(Epoch::new(15), &spec),
final_cgc,
);
assert_eq!(
custody_context
.custody_columns_for_epoch(Some(Epoch::new(15)), &spec)
.len(),
final_cgc as usize,
);
// Prior epoch should still return the original CGC
assert_eq!(
custody_context.custody_group_count_at_epoch(Epoch::new(14), &spec),
default_cgc,
);
}
#[test]
fn backfill_with_multiple_cgc_increases_prunes_map_correctly() {
let spec = E::default_spec();
let initial_cgc = 8u64;
let mid_cgc = 16u64;
let final_cgc = 32u64;
// Setup: Node restart after multiple validator registrations causing CGC increases
let head_epoch = Epoch::new(20);
let epoch_and_cgc_tuples = vec![
(Epoch::new(0), initial_cgc),
(Epoch::new(10), mid_cgc),
(head_epoch, final_cgc),
];
let custody_context = setup_custody_context(&spec, head_epoch, epoch_and_cgc_tuples);
// Backfill to epoch 15 (between the two CGC increases)
complete_backfill_for_epochs(&custody_context, Epoch::new(20), Epoch::new(15), final_cgc);
// Verify epochs 15 - 20 return latest CGC (32)
for epoch in 15..=20 {
assert_eq!(
custody_context.custody_group_count_at_epoch(Epoch::new(epoch), &spec),
final_cgc,
);
}
// Verify epochs 10-14 still return mid_cgc (16)
for epoch in 10..14 {
assert_eq!(
custody_context.custody_group_count_at_epoch(Epoch::new(epoch), &spec),
mid_cgc,
);
}
}
#[test]
fn attempt_backfill_with_invalid_cgc() {
let spec = E::default_spec();
let initial_cgc = 8u64;
let mid_cgc = 16u64;
let final_cgc = 32u64;
// Setup: Node restart after multiple validator registrations causing CGC increases
let head_epoch = Epoch::new(20);
let epoch_and_cgc_tuples = vec![
(Epoch::new(0), initial_cgc),
(Epoch::new(10), mid_cgc),
(head_epoch, final_cgc),
];
let custody_context = setup_custody_context(&spec, head_epoch, epoch_and_cgc_tuples);
// Backfill to epoch 15 (between the two CGC increases)
complete_backfill_for_epochs(&custody_context, Epoch::new(20), Epoch::new(15), final_cgc);
// Verify epochs 15 - 20 return latest CGC (32)
for epoch in 15..=20 {
assert_eq!(
custody_context.custody_group_count_at_epoch(Epoch::new(epoch), &spec),
final_cgc,
);
}
// Attempt backfill with an incorrect cgc value
complete_backfill_for_epochs(
&custody_context,
Epoch::new(20),
Epoch::new(15),
initial_cgc,
);
// Verify epochs 15 - 20 still return latest CGC (32)
for epoch in 15..=20 {
assert_eq!(
custody_context.custody_group_count_at_epoch(Epoch::new(epoch), &spec),
final_cgc,
);
}
// Verify epochs 10-14 still return mid_cgc (16)
for epoch in 10..14 {
assert_eq!(
custody_context.custody_group_count_at_epoch(Epoch::new(epoch), &spec),
mid_cgc,
);
}
}
#[test]
fn reset_validator_custody_requirements() {
let spec = E::default_spec();
let minimum_cgc = 4u64;
let initial_cgc = 8u64;
let mid_cgc = 16u64;
let final_cgc = 32u64;
// Setup: Node restart after multiple validator registrations causing CGC increases
let head_epoch = Epoch::new(20);
let epoch_and_cgc_tuples = vec![
(Epoch::new(0), initial_cgc),
(Epoch::new(10), mid_cgc),
(head_epoch, final_cgc),
];
let custody_context = setup_custody_context(&spec, head_epoch, epoch_and_cgc_tuples);
// Backfill from epoch 20 to 9
complete_backfill_for_epochs(&custody_context, Epoch::new(20), Epoch::new(9), final_cgc);
// Reset validator custody requirements to the latest cgc requirements at `head_epoch` up to the boundary epoch
custody_context.reset_validator_custody_requirements(head_epoch);
// Verify epochs 0 - 19 return the minimum cgc requirement because of the validator custody requirement reset
for epoch in 0..=19 {
assert_eq!(
custody_context.custody_group_count_at_epoch(Epoch::new(epoch), &spec),
minimum_cgc,
);
}
// Verify epoch 20 returns a CGC of 32
assert_eq!(
custody_context.custody_group_count_at_epoch(head_epoch, &spec),
final_cgc
);
// Rerun Backfill to epoch 20
complete_backfill_for_epochs(&custody_context, Epoch::new(20), Epoch::new(0), final_cgc);
// Verify epochs 0 - 20 return the final cgc requirements
for epoch in 0..=20 {
assert_eq!(
custody_context.custody_group_count_at_epoch(Epoch::new(epoch), &spec),
final_cgc,
);
}
}
}