Update SAMPLES_PER_SLOT to be number of custody groups instead of data columns (#7683)

Update `SAMPLES_PER_SLOT` to be number of custody groups instead of data columns. This should have no impact on the current implementation as config currently maintains a `group:subnet:column` ratio of `1:1:1`.  **In short, this PR doesn't change anything for Fusaka, but ensures compliance with the spec and potential future changes.**

I've added separate methods to compute sampling columns and custody groups for clarity: `spec.sampling_size_columns` and `spec.sampling_size_custod_groups`

See the clarifications in this PR for more details: https://github.com/ethereum/consensus-specs/pull/4251
This commit is contained in:
Jimmy Chen
2025-07-02 10:08:40 +10:00
committed by GitHub
parent e305cb1b92
commit 41742ce2bd
5 changed files with 71 additions and 30 deletions

View File

@@ -481,7 +481,8 @@ impl<T: BeaconChainTypes> DataAvailabilityCheckerInner<T> {
if let Some(available_block) = pending_components.make_available( if let Some(available_block) = pending_components.make_available(
&self.spec, &self.spec,
self.custody_context.sampling_size(Some(epoch), &self.spec), self.custody_context
.num_of_data_columns_to_sample(Some(epoch), &self.spec),
|block| self.state_cache.recover_pending_executed_block(block), |block| self.state_cache.recover_pending_executed_block(block),
)? { )? {
// We keep the pending components in the availability cache during block import (#5845). // We keep the pending components in the availability cache during block import (#5845).
@@ -526,7 +527,9 @@ impl<T: BeaconChainTypes> DataAvailabilityCheckerInner<T> {
// Merge in the data columns. // Merge in the data columns.
pending_components.merge_data_columns(kzg_verified_data_columns)?; pending_components.merge_data_columns(kzg_verified_data_columns)?;
let num_expected_columns = self.custody_context.sampling_size(Some(epoch), &self.spec); let num_expected_columns = self
.custody_context
.num_of_data_columns_to_sample(Some(epoch), &self.spec);
debug!( debug!(
component = "data_columns", component = "data_columns",
?block_root, ?block_root,
@@ -622,7 +625,9 @@ impl<T: BeaconChainTypes> DataAvailabilityCheckerInner<T> {
// Merge in the block. // Merge in the block.
pending_components.merge_block(diet_executed_block); pending_components.merge_block(diet_executed_block);
let num_expected_columns = self.custody_context.sampling_size(Some(epoch), &self.spec); let num_expected_columns = self
.custody_context
.num_of_data_columns_to_sample(Some(epoch), &self.spec);
debug!( debug!(
component = "block", component = "block",
?block_root, ?block_root,
@@ -631,11 +636,11 @@ impl<T: BeaconChainTypes> DataAvailabilityCheckerInner<T> {
); );
// Check if we have all components and entire set is consistent. // Check if we have all components and entire set is consistent.
if let Some(available_block) = pending_components.make_available( if let Some(available_block) =
&self.spec, pending_components.make_available(&self.spec, num_expected_columns, |block| {
self.custody_context.sampling_size(Some(epoch), &self.spec), self.state_cache.recover_pending_executed_block(block)
|block| self.state_cache.recover_pending_executed_block(block), })?
)? { {
// We keep the pending components in the availability cache during block import (#5845). // We keep the pending components in the availability cache during block import (#5845).
write_lock.put(block_root, pending_components); write_lock.put(block_root, pending_components);
drop(write_lock); drop(write_lock);

View File

@@ -777,7 +777,7 @@ where
self.chain self.chain
.data_availability_checker .data_availability_checker
.custody_context() .custody_context()
.sampling_size(None, &self.chain.spec) as usize .num_of_data_columns_to_sample(None, &self.chain.spec) as usize
} }
pub fn slots_per_epoch(&self) -> u64 { pub fn slots_per_epoch(&self) -> u64 {

View File

@@ -215,7 +215,8 @@ impl CustodyContext {
); );
return Some(CustodyCountChanged { return Some(CustodyCountChanged {
new_custody_group_count: updated_cgc, new_custody_group_count: updated_cgc,
sampling_count: self.sampling_size(Some(effective_epoch), spec), sampling_count: self
.num_of_custody_groups_to_sample(Some(effective_epoch), spec),
}); });
} }
} }
@@ -240,9 +241,13 @@ impl CustodyContext {
} }
} }
/// Returns the count of custody columns this node must sample for a block at `epoch` to import. /// This function is used to determine the custody group count at a given epoch.
/// If an `epoch` is not specified, returns the *current* validator custody requirement. ///
pub fn sampling_size(&self, epoch_opt: Option<Epoch>, spec: &ChainSpec) -> u64 { /// 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`].
fn custody_group_count_at_epoch(&self, epoch_opt: Option<Epoch>, spec: &ChainSpec) -> u64 {
let custody_group_count = if self.current_is_supernode { let custody_group_count = if self.current_is_supernode {
spec.number_of_custody_groups spec.number_of_custody_groups
} else if let Some(epoch) = epoch_opt { } else if let Some(epoch) = epoch_opt {
@@ -253,8 +258,26 @@ impl CustodyContext {
} else { } else {
self.custody_group_count_at_head(spec) self.custody_group_count_at_head(spec)
}; };
custody_group_count
}
spec.sampling_size(custody_group_count) /// Returns the count of custody groups this node must _sample_ for a block at `epoch` to import.
/// If an `epoch` is not specified, returns the *current* validator custody requirement.
pub fn num_of_custody_groups_to_sample(
&self,
epoch_opt: Option<Epoch>,
spec: &ChainSpec,
) -> u64 {
let custody_group_count = self.custody_group_count_at_epoch(epoch_opt, 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.
/// If an `epoch` is not specified, returns the *current* validator custody requirement.
pub fn num_of_data_columns_to_sample(&self, epoch_opt: Option<Epoch>, spec: &ChainSpec) -> u64 {
let custody_group_count = self.custody_group_count_at_epoch(epoch_opt, spec);
spec.sampling_size_columns(custody_group_count)
.expect("should compute node sampling size from valid chain spec") .expect("should compute node sampling size from valid chain spec")
} }
} }
@@ -307,7 +330,7 @@ mod tests {
spec.number_of_custody_groups spec.number_of_custody_groups
); );
assert_eq!( assert_eq!(
custody_context.sampling_size(None, &spec), custody_context.num_of_custody_groups_to_sample(None, &spec),
spec.number_of_custody_groups spec.number_of_custody_groups
); );
} }
@@ -322,7 +345,7 @@ mod tests {
"head custody count should be minimum spec custody requirement" "head custody count should be minimum spec custody requirement"
); );
assert_eq!( assert_eq!(
custody_context.sampling_size(None, &spec), custody_context.num_of_custody_groups_to_sample(None, &spec),
spec.samples_per_slot spec.samples_per_slot
); );
} }
@@ -412,7 +435,7 @@ mod tests {
register_validators_and_assert_cgc(&custody_context, validators_and_expected_cgc, &spec); register_validators_and_assert_cgc(&custody_context, validators_and_expected_cgc, &spec);
assert_eq!( assert_eq!(
custody_context.sampling_size(None, &spec), custody_context.num_of_custody_groups_to_sample(None, &spec),
spec.number_of_custody_groups spec.number_of_custody_groups
); );
} }
@@ -423,7 +446,7 @@ mod tests {
let spec = E::default_spec(); let spec = E::default_spec();
let current_slot = Slot::new(10); let current_slot = Slot::new(10);
let current_epoch = current_slot.epoch(E::slots_per_epoch()); let current_epoch = current_slot.epoch(E::slots_per_epoch());
let default_sampling_size = custody_context.sampling_size(None, &spec); let default_sampling_size = custody_context.num_of_custody_groups_to_sample(None, &spec);
let validator_custody_units = 10; let validator_custody_units = 10;
let _cgc_changed = custody_context.register_validators::<E>( let _cgc_changed = custody_context.register_validators::<E>(
@@ -437,12 +460,12 @@ mod tests {
// CGC update is not applied for `current_epoch`. // CGC update is not applied for `current_epoch`.
assert_eq!( assert_eq!(
custody_context.sampling_size(Some(current_epoch), &spec), custody_context.num_of_custody_groups_to_sample(Some(current_epoch), &spec),
default_sampling_size default_sampling_size
); );
// CGC update is applied for the next epoch. // CGC update is applied for the next epoch.
assert_eq!( assert_eq!(
custody_context.sampling_size(Some(current_epoch + 1), &spec), custody_context.num_of_custody_groups_to_sample(Some(current_epoch + 1), &spec),
validator_custody_units validator_custody_units
); );
} }

View File

@@ -66,7 +66,7 @@ impl<E: EthSpec> NetworkGlobals<E> {
// The below `expect` calls will panic on start up if the chain spec config values used // The below `expect` calls will panic on start up if the chain spec config values used
// are invalid // are invalid
let sampling_size = spec let sampling_size = spec
.sampling_size(custody_group_count) .sampling_size_custody_groups(custody_group_count)
.expect("should compute node sampling size from valid chain spec"); .expect("should compute node sampling size from valid chain spec");
let custody_groups = get_custody_groups(node_id, sampling_size, &spec) let custody_groups = get_custody_groups(node_id, sampling_size, &spec)
.expect("should compute node custody groups"); .expect("should compute node custody groups");
@@ -114,7 +114,7 @@ impl<E: EthSpec> NetworkGlobals<E> {
// are invalid // are invalid
let sampling_size = self let sampling_size = self
.spec .spec
.sampling_size(custody_group_count) .sampling_size_custody_groups(custody_group_count)
.expect("should compute node sampling size from valid chain spec"); .expect("should compute node sampling size from valid chain spec");
let custody_groups = let custody_groups =
get_custody_groups(self.local_enr().node_id().raw(), sampling_size, &self.spec) get_custody_groups(self.local_enr().node_id().raw(), sampling_size, &self.spec)
@@ -298,7 +298,13 @@ mod test {
spec.fulu_fork_epoch = Some(Epoch::new(0)); spec.fulu_fork_epoch = Some(Epoch::new(0));
let custody_group_count = spec.number_of_custody_groups / 2; let custody_group_count = spec.number_of_custody_groups / 2;
let subnet_sampling_size = spec.sampling_size(custody_group_count).unwrap(); let sampling_size_custody_groups = spec
.sampling_size_custody_groups(custody_group_count)
.unwrap();
let expected_sampling_subnet_count = sampling_size_custody_groups
* spec.data_column_sidecar_subnet_count
/ spec.number_of_custody_groups;
let metadata = get_metadata(custody_group_count); let metadata = get_metadata(custody_group_count);
let config = Arc::new(NetworkConfig::default()); let config = Arc::new(NetworkConfig::default());
@@ -310,7 +316,7 @@ mod test {
); );
assert_eq!( assert_eq!(
globals.sampling_subnets.read().len(), globals.sampling_subnets.read().len(),
subnet_sampling_size as usize expected_sampling_subnet_count as usize
); );
} }
@@ -321,7 +327,7 @@ mod test {
spec.fulu_fork_epoch = Some(Epoch::new(0)); spec.fulu_fork_epoch = Some(Epoch::new(0));
let custody_group_count = spec.number_of_custody_groups / 2; let custody_group_count = spec.number_of_custody_groups / 2;
let subnet_sampling_size = spec.sampling_size(custody_group_count).unwrap(); let expected_sampling_columns = spec.sampling_size_columns(custody_group_count).unwrap();
let metadata = get_metadata(custody_group_count); let metadata = get_metadata(custody_group_count);
let config = Arc::new(NetworkConfig::default()); let config = Arc::new(NetworkConfig::default());
@@ -333,7 +339,7 @@ mod test {
); );
assert_eq!( assert_eq!(
globals.sampling_columns.read().len(), globals.sampling_columns.read().len(),
subnet_sampling_size as usize expected_sampling_columns as usize
); );
} }

View File

@@ -720,17 +720,24 @@ impl ChainSpec {
} }
/// Returns the number of column sidecars to sample per slot. /// Returns the number of column sidecars to sample per slot.
pub fn sampling_size(&self, custody_group_count: u64) -> Result<u64, String> { pub fn sampling_size_columns(&self, custody_group_count: u64) -> Result<u64, String> {
let sampling_size_groups = self.sampling_size_custody_groups(custody_group_count)?;
let columns_per_custody_group = self let columns_per_custody_group = self
.number_of_columns .number_of_columns
.safe_div(self.number_of_custody_groups) .safe_div(self.number_of_custody_groups)
.map_err(|_| "number_of_custody_groups must be greater than 0")?; .map_err(|_| "number_of_custody_groups must be greater than 0")?;
let custody_column_count = columns_per_custody_group let sampling_size_columns = columns_per_custody_group
.safe_mul(custody_group_count) .safe_mul(sampling_size_groups)
.map_err(|_| "Computing sampling size should not overflow")?; .map_err(|_| "Computing sampling size should not overflow")?;
Ok(std::cmp::max(custody_column_count, self.samples_per_slot)) Ok(sampling_size_columns)
}
/// Returns the number of custody groups to sample per slot.
pub fn sampling_size_custody_groups(&self, custody_group_count: u64) -> Result<u64, String> {
Ok(std::cmp::max(custody_group_count, self.samples_per_slot))
} }
pub fn all_data_column_sidecar_subnets(&self) -> impl Iterator<Item = DataColumnSubnetId> { pub fn all_data_column_sidecar_subnets(&self) -> impl Iterator<Item = DataColumnSubnetId> {