Skip to content

Commit 7108c74

Browse files
feat: add peerdas custody field to ENR (#5409)
* feat: add peerdas custody field to ENR * add hash prefix step in subnet computation * refactor test and fix possible u64 overflow * default to min custody value if not present in ENR
1 parent 1317f70 commit 7108c74

File tree

7 files changed

+126
-46
lines changed

7 files changed

+126
-46
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

beacon_node/lighthouse_network/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ tracing = { workspace = true }
4747
byteorder = { workspace = true }
4848
bytes = { workspace = true }
4949
either = { workspace = true }
50+
itertools = { workspace = true }
5051

5152
# Local dependencies
5253
futures-ticker = "0.0.3"

beacon_node/lighthouse_network/src/discovery/enr.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ pub const ETH2_ENR_KEY: &str = "eth2";
2424
pub const ATTESTATION_BITFIELD_ENR_KEY: &str = "attnets";
2525
/// The ENR field specifying the sync committee subnet bitfield.
2626
pub const SYNC_COMMITTEE_BITFIELD_ENR_KEY: &str = "syncnets";
27+
/// The ENR field specifying the peerdas custody subnet count.
28+
pub const PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY: &str = "custody_subnet_count";
2729

2830
/// Extension trait for ENR's within Eth2.
2931
pub trait Eth2Enr {
@@ -37,6 +39,9 @@ pub trait Eth2Enr {
3739
&self,
3840
) -> Result<EnrSyncCommitteeBitfield<TSpec>, &'static str>;
3941

42+
/// The peerdas custody subnet count associated with the ENR.
43+
fn custody_subnet_count<TSpec: EthSpec>(&self) -> Result<u64, &'static str>;
44+
4045
fn eth2(&self) -> Result<EnrForkId, &'static str>;
4146
}
4247

@@ -63,6 +68,17 @@ impl Eth2Enr for Enr {
6368
.map_err(|_| "Could not decode the ENR syncnets bitfield")
6469
}
6570

71+
fn custody_subnet_count<TSpec: EthSpec>(&self) -> Result<u64, &'static str> {
72+
// NOTE: if the custody value is non-existent in the ENR, then we assume the minimum
73+
// custody value defined in the spec.
74+
let min_custody_bytes = TSpec::min_custody_requirement().as_ssz_bytes();
75+
let custody_bytes = self
76+
.get(PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY)
77+
.unwrap_or(&min_custody_bytes);
78+
u64::from_ssz_bytes(custody_bytes)
79+
.map_err(|_| "Could not decode the ENR custody subnet count")
80+
}
81+
6682
fn eth2(&self) -> Result<EnrForkId, &'static str> {
6783
let eth2_bytes = self.get(ETH2_ENR_KEY).ok_or("ENR has no eth2 field")?;
6884

@@ -225,6 +241,14 @@ pub fn build_enr<T: EthSpec>(
225241

226242
builder.add_value(SYNC_COMMITTEE_BITFIELD_ENR_KEY, &bitfield.as_ssz_bytes());
227243

244+
// set the "custody_subnet_count" field on our ENR
245+
let custody_subnet_count = T::min_custody_requirement() as u64;
246+
247+
builder.add_value(
248+
PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY,
249+
&custody_subnet_count.as_ssz_bytes(),
250+
);
251+
228252
builder
229253
.build(enr_key)
230254
.map_err(|e| format!("Could not build Local ENR: {:?}", e))
@@ -248,10 +272,12 @@ fn compare_enr(local_enr: &Enr, disk_enr: &Enr) -> bool {
248272
// take preference over disk udp port if one is not specified
249273
&& (local_enr.udp4().is_none() || local_enr.udp4() == disk_enr.udp4())
250274
&& (local_enr.udp6().is_none() || local_enr.udp6() == disk_enr.udp6())
251-
// we need the ATTESTATION_BITFIELD_ENR_KEY and SYNC_COMMITTEE_BITFIELD_ENR_KEY key to match,
252-
// otherwise we use a new ENR. This will likely only be true for non-validating nodes
275+
// we need the ATTESTATION_BITFIELD_ENR_KEY and SYNC_COMMITTEE_BITFIELD_ENR_KEY and
276+
// PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY key to match, otherwise we use a new ENR. This will
277+
// likely only be true for non-validating nodes.
253278
&& local_enr.get(ATTESTATION_BITFIELD_ENR_KEY) == disk_enr.get(ATTESTATION_BITFIELD_ENR_KEY)
254279
&& local_enr.get(SYNC_COMMITTEE_BITFIELD_ENR_KEY) == disk_enr.get(SYNC_COMMITTEE_BITFIELD_ENR_KEY)
280+
&& local_enr.get(PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY) == disk_enr.get(PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY)
255281
}
256282

257283
/// Loads enr from the given directory

beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
//! The subnet predicate used for searching for a particular subnet.
22
use super::*;
33
use crate::types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield};
4+
use itertools::Itertools;
45
use slog::trace;
56
use std::ops::Deref;
7+
use types::DataColumnSubnetId;
68

79
/// Returns the predicate for a given subnet.
810
pub fn subnet_predicate<TSpec>(
@@ -22,19 +24,30 @@ where
2224
};
2325

2426
// Pre-fork/fork-boundary enrs may not contain a syncnets field.
25-
// Don't return early here
27+
// Don't return early here.
2628
let sync_committee_bitfield: Result<EnrSyncCommitteeBitfield<TSpec>, _> =
2729
enr.sync_committee_bitfield::<TSpec>();
2830

31+
// Pre-fork/fork-boundary enrs may not contain a peerdas custody field.
32+
// Don't return early here.
33+
//
34+
// NOTE: we could map to minimum custody requirement here.
35+
let custody_subnet_count: Result<u64, _> = enr.custody_subnet_count::<TSpec>();
36+
2937
let predicate = subnets.iter().any(|subnet| match subnet {
3038
Subnet::Attestation(s) => attestation_bitfield
3139
.get(*s.deref() as usize)
3240
.unwrap_or(false),
3341
Subnet::SyncCommittee(s) => sync_committee_bitfield
3442
.as_ref()
3543
.map_or(false, |b| b.get(*s.deref() as usize).unwrap_or(false)),
36-
// TODO(das) discovery to be implemented at a later phase. Initially we just use a large peer count.
37-
Subnet::DataColumn(_) => false,
44+
Subnet::DataColumn(s) => custody_subnet_count.map_or(false, |count| {
45+
let mut subnets = DataColumnSubnetId::compute_custody_subnets::<TSpec>(
46+
enr.node_id().raw().into(),
47+
count,
48+
);
49+
subnets.contains(s)
50+
}),
3851
});
3952

4053
if !predicate {

beacon_node/network/src/service.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use futures::prelude::*;
1717
use futures::StreamExt;
1818
use lighthouse_network::service::Network;
1919
use lighthouse_network::types::GossipKind;
20+
use lighthouse_network::Eth2Enr;
2021
use lighthouse_network::{prometheus_client::registry::Registry, MessageAcceptance};
2122
use lighthouse_network::{
2223
rpc::{GoodbyeReason, RPCResponseErrorCode},
@@ -733,12 +734,13 @@ impl<T: BeaconChainTypes> NetworkService<T> {
733734
}
734735

735736
if !self.subscribe_all_subnets {
736-
for column_subnet in
737-
DataColumnSubnetId::compute_subnets_for_data_column::<T::EthSpec>(
738-
self.network_globals.local_enr().node_id().raw().into(),
739-
&self.beacon_chain.spec,
740-
)
741-
{
737+
for column_subnet in DataColumnSubnetId::compute_custody_subnets::<T::EthSpec>(
738+
self.network_globals.local_enr().node_id().raw().into(),
739+
self.network_globals
740+
.local_enr()
741+
.custody_subnet_count::<<T as BeaconChainTypes>::EthSpec>()
742+
.unwrap_or(self.beacon_chain.spec.custody_requirement),
743+
) {
742744
for fork_digest in self.required_gossip_fork_digests() {
743745
let gossip_kind = Subnet::DataColumn(column_subnet).into();
744746
let topic = GossipTopic::new(

consensus/types/src/data_column_subnet_id.rs

Lines changed: 63 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
//! Identifies each data column subnet by an integer identifier.
2-
use crate::{ChainSpec, EthSpec};
2+
use crate::EthSpec;
33
use ethereum_types::U256;
44
use safe_arith::{ArithError, SafeArith};
55
use serde::{Deserialize, Serialize};
6+
use smallvec::SmallVec;
67
use std::fmt::{self, Display};
78
use std::ops::{Deref, DerefMut};
89

@@ -44,20 +45,50 @@ impl DataColumnSubnetId {
4445
}
4546

4647
#[allow(clippy::arithmetic_side_effects)]
48+
pub fn columns<T: EthSpec>(&self) -> impl Iterator<Item = u64> {
49+
let subnet = self.0;
50+
let data_column_subnet_count = T::data_column_subnet_count() as u64;
51+
let columns_per_subnet = (T::number_of_columns() as u64) / data_column_subnet_count;
52+
(0..columns_per_subnet).map(move |i| data_column_subnet_count * i + subnet)
53+
}
54+
4755
/// Compute required subnets to subscribe to given the node id.
4856
/// TODO(das): Add epoch param
49-
/// TODO(das): Add num of subnets (from ENR)
50-
pub fn compute_subnets_for_data_column<T: EthSpec>(
57+
#[allow(clippy::arithmetic_side_effects)]
58+
pub fn compute_custody_subnets<T: EthSpec>(
5159
node_id: U256,
52-
spec: &ChainSpec,
60+
custody_subnet_count: u64,
5361
) -> impl Iterator<Item = DataColumnSubnetId> {
54-
let num_of_column_subnets = T::data_column_subnet_count() as u64;
55-
(0..spec.custody_requirement)
56-
.map(move |i| {
57-
let node_offset = (node_id % U256::from(num_of_column_subnets)).as_u64();
58-
node_offset.saturating_add(i) % num_of_column_subnets
59-
})
60-
.map(DataColumnSubnetId::new)
62+
// NOTE: we could perform check on `custody_subnet_count` here to ensure that it is a valid
63+
// value, but here we assume it is valid.
64+
65+
let mut subnets = SmallVec::<[u64; 32]>::new();
66+
let mut offset = 0;
67+
while (subnets.len() as u64) < custody_subnet_count {
68+
let offset_node_id = node_id + U256::from(offset);
69+
let offset_node_id = offset_node_id.low_u64().to_le_bytes();
70+
let hash: [u8; 32] = ethereum_hashing::hash_fixed(&offset_node_id);
71+
let hash_prefix = [
72+
hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7],
73+
];
74+
let hash_prefix_u64 = u64::from_le_bytes(hash_prefix);
75+
let subnet = hash_prefix_u64 % (T::data_column_subnet_count() as u64);
76+
77+
if !subnets.contains(&subnet) {
78+
subnets.push(subnet);
79+
}
80+
81+
offset += 1
82+
}
83+
subnets.into_iter().map(DataColumnSubnetId::new)
84+
}
85+
86+
pub fn compute_custody_columns<T: EthSpec>(
87+
node_id: U256,
88+
custody_subnet_count: u64,
89+
) -> impl Iterator<Item = u64> {
90+
Self::compute_custody_subnets::<T>(node_id, custody_subnet_count)
91+
.flat_map(|subnet| subnet.columns::<T>())
6192
}
6293
}
6394

@@ -120,6 +151,7 @@ impl From<ArithError> for Error {
120151
mod test {
121152
use crate::data_column_subnet_id::DataColumnSubnetId;
122153
use crate::ChainSpec;
154+
use crate::EthSpec;
123155

124156
#[test]
125157
fn test_compute_subnets_for_data_column() {
@@ -139,32 +171,29 @@ mod test {
139171
.map(|v| ethereum_types::U256::from_dec_str(v).unwrap())
140172
.collect::<Vec<_>>();
141173

142-
let expected_subnets = vec![
143-
vec![0],
144-
vec![29],
145-
vec![28],
146-
vec![20],
147-
vec![30],
148-
vec![9],
149-
vec![18],
150-
vec![21],
151-
vec![23],
152-
vec![29],
153-
];
154-
155174
let spec = ChainSpec::mainnet();
156175

157-
for x in 0..node_ids.len() {
158-
let computed_subnets = DataColumnSubnetId::compute_subnets_for_data_column::<
176+
for node_id in node_ids {
177+
let computed_subnets = DataColumnSubnetId::compute_custody_subnets::<
159178
crate::MainnetEthSpec,
160-
>(node_ids[x], &spec);
161-
162-
assert_eq!(
163-
expected_subnets[x],
164-
computed_subnets
165-
.map(DataColumnSubnetId::into)
166-
.collect::<Vec<u64>>()
167-
);
179+
>(node_id, spec.custody_requirement);
180+
let computed_subnets: Vec<_> = computed_subnets.collect();
181+
182+
// the number of subnets is equal to the custody requirement
183+
assert_eq!(computed_subnets.len() as u64, spec.custody_requirement);
184+
185+
let subnet_count = crate::MainnetEthSpec::data_column_subnet_count();
186+
let columns_per_subnet = crate::MainnetEthSpec::number_of_columns() / subnet_count;
187+
for subnet in computed_subnets {
188+
let columns: Vec<_> = subnet.columns::<crate::MainnetEthSpec>().collect();
189+
// the number of columns is equal to the specified number of columns per subnet
190+
assert_eq!(columns.len(), columns_per_subnet);
191+
192+
for pair in columns.windows(2) {
193+
// each successive column index is offset by the number of subnets
194+
assert_eq!(pair[1] - pair[0], subnet_count as u64);
195+
}
196+
}
168197
}
169198
}
170199
}

consensus/types/src/eth_spec.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use ssz_types::typenum::{
66
bit::B0, UInt, Unsigned, U0, U1024, U1048576, U1073741824, U1099511627776, U128, U131072, U16,
77
U16777216, U2, U2048, U256, U32, U4, U4096, U512, U6, U625, U64, U65536, U8, U8192,
88
};
9-
use ssz_types::typenum::{U17, U9};
9+
use ssz_types::typenum::{U1, U17, U9};
1010
use std::fmt::{self, Debug};
1111
use std::str::FromStr;
1212

@@ -115,6 +115,7 @@ pub trait EthSpec:
115115
/*
116116
* New in PeerDAS
117117
*/
118+
type MinCustodyRequirement: Unsigned + Clone + Sync + Send + Debug + PartialEq;
118119
type DataColumnSubnetCount: Unsigned + Clone + Sync + Send + Debug + PartialEq;
119120
type DataColumnCount: Unsigned + Clone + Sync + Send + Debug + PartialEq;
120121
type MaxBytesPerColumn: Unsigned + Clone + Sync + Send + Debug + PartialEq;
@@ -296,6 +297,10 @@ pub trait EthSpec:
296297
Self::DataColumnCount::to_usize()
297298
}
298299

300+
fn min_custody_requirement() -> usize {
301+
Self::MinCustodyRequirement::to_usize()
302+
}
303+
299304
fn data_column_subnet_count() -> usize {
300305
Self::DataColumnSubnetCount::to_usize()
301306
}
@@ -353,6 +358,7 @@ impl EthSpec for MainnetEthSpec {
353358
type FieldElementsPerCell = U64;
354359
type BytesPerBlob = U131072;
355360
type KzgCommitmentInclusionProofDepth = U17;
361+
type MinCustodyRequirement = U1;
356362
type DataColumnSubnetCount = U32;
357363
type DataColumnCount = U128;
358364
// Column samples are entire columns in 1D DAS.
@@ -396,6 +402,7 @@ impl EthSpec for MinimalEthSpec {
396402
type MaxBlobCommitmentsPerBlock = U16;
397403
type KzgCommitmentInclusionProofDepth = U9;
398404
// DAS spec values copied from `MainnetEthSpec`
405+
type MinCustodyRequirement = U1;
399406
type DataColumnSubnetCount = U32;
400407
type DataColumnCount = U128;
401408
type MaxBytesPerColumn = U65536;
@@ -476,6 +483,7 @@ impl EthSpec for GnosisEthSpec {
476483
type BytesPerBlob = U131072;
477484
type KzgCommitmentInclusionProofDepth = U17;
478485
// DAS spec values copied from `MainnetEthSpec`
486+
type MinCustodyRequirement = U1;
479487
type DataColumnSubnetCount = U32;
480488
type DataColumnCount = U128;
481489
type MaxBytesPerColumn = U65536;

0 commit comments

Comments
 (0)