-
Notifications
You must be signed in to change notification settings - Fork 48
feat: Implement attestation subnet subscription #616
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
d41dd2c
feat(discv5): Implement attestation subnet subscription
vineetpant Jun 23, 2025
81d81c6
756400d
8dba593
b998589
c33a376
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,6 +1,9 @@ | ||||||||||||||||||||||
use alloy_primitives::B256; | ||||||||||||||||||||||
use alloy_rlp::{BufMut, Decodable, Encodable, bytes::Bytes}; | ||||||||||||||||||||||
use anyhow::{anyhow, ensure}; | ||||||||||||||||||||||
use discv5::Enr; | ||||||||||||||||||||||
use discv5::{Enr, enr::NodeId}; | ||||||||||||||||||||||
use ream_consensus::misc::compute_shuffled_index; | ||||||||||||||||||||||
use sha2::{Digest, Sha256}; | ||||||||||||||||||||||
use ssz::Encode; | ||||||||||||||||||||||
use ssz_types::{ | ||||||||||||||||||||||
BitVector, | ||||||||||||||||||||||
|
@@ -13,6 +16,11 @@ pub const ATTESTATION_SUBNET_COUNT: usize = 64; | |||||||||||||||||||||
pub const SYNC_COMMITTEE_BITFIELD_ENR_KEY: &str = "syncnets"; | ||||||||||||||||||||||
pub const SYNC_COMMITTEE_SUBNET_COUNT: usize = 4; | ||||||||||||||||||||||
|
||||||||||||||||||||||
// Subscription constants | ||||||||||||||||||||||
const SUBNETS_PER_NODE: usize = 2; | ||||||||||||||||||||||
pub const EPOCHS_PER_SUBNET_SUBSCRIPTION: u64 = 256; | ||||||||||||||||||||||
const ATTESTATION_SUBNET_PREFIX_BITS: u32 = 8; | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't this be 6? ATTESTATION_SUBNET_EXTRA_BITS = 0 ceillog2(ATTESTATION_SUBNET_COUNT) = 6 |
||||||||||||||||||||||
|
||||||||||||||||||||||
/// Represents the attestation subnets a node is subscribed to | ||||||||||||||||||||||
/// | ||||||||||||||||||||||
/// This directly wraps a BitVector<U64> for the attestation subnet bitfield | ||||||||||||||||||||||
|
@@ -137,6 +145,38 @@ impl Decodable for SyncCommitteeSubnets { | |||||||||||||||||||||
} | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
/// Compute a single subscribed subnet based on node_id, epoch, and index | ||||||||||||||||||||||
pub fn compute_subscribed_subnet(node_id: NodeId, epoch: u64, index: usize) -> anyhow::Result<u8> { | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
SubnetID is supposed to be a u64 not a u8 |
||||||||||||||||||||||
// Extract prefix from first 8 bytes of node_id | ||||||||||||||||||||||
let mut node_id_prefix_bytes = [0u8; 8]; | ||||||||||||||||||||||
node_id_prefix_bytes.copy_from_slice(&node_id.raw()[..8]); | ||||||||||||||||||||||
let node_id_prefix = | ||||||||||||||||||||||
u64::from_be_bytes(node_id_prefix_bytes) >> (64 - ATTESTATION_SUBNET_PREFIX_BITS); | ||||||||||||||||||||||
Comment on lines
+150
to
+154
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
we should be using B256 here no? instead of of converting this to a u64 |
||||||||||||||||||||||
|
||||||||||||||||||||||
let mut node_offset_bytes = [0u8; 8]; | ||||||||||||||||||||||
node_offset_bytes.copy_from_slice(&node_id.raw()[24..32]); | ||||||||||||||||||||||
let node_offset = u64::from_be_bytes(node_offset_bytes) % EPOCHS_PER_SUBNET_SUBSCRIPTION; | ||||||||||||||||||||||
Comment on lines
+156
to
+158
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
same comment here we should be doing B256 % B256, NodeId is a B256 |
||||||||||||||||||||||
|
||||||||||||||||||||||
let permutation_seed = | ||||||||||||||||||||||
Sha256::digest(((epoch + node_offset) / EPOCHS_PER_SUBNET_SUBSCRIPTION).to_le_bytes()); | ||||||||||||||||||||||
|
||||||||||||||||||||||
let permutated_prefix = compute_shuffled_index( | ||||||||||||||||||||||
node_id_prefix as usize, | ||||||||||||||||||||||
1 << ATTESTATION_SUBNET_PREFIX_BITS, | ||||||||||||||||||||||
B256::from_slice(permutation_seed.as_slice()), | ||||||||||||||||||||||
)?; | ||||||||||||||||||||||
Ok(((permutated_prefix + index) % ATTESTATION_SUBNET_COUNT) as u8) | ||||||||||||||||||||||
} | ||||||||||||||||||||||
Comment on lines
+148
to
+169
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. anyways let me know about what I said above, I might be missing a few insights let me know |
||||||||||||||||||||||
|
||||||||||||||||||||||
/// Compute all subscribed subnets for a node | ||||||||||||||||||||||
pub fn compute_subscribed_subnets(node_id: NodeId, epoch: u64) -> anyhow::Result<Vec<u8>> { | ||||||||||||||||||||||
(0..SUBNETS_PER_NODE).try_fold(Vec::new(), |mut acc, i| { | ||||||||||||||||||||||
let subnet = compute_subscribed_subnet(node_id, epoch, i)?; | ||||||||||||||||||||||
acc.push(subnet); | ||||||||||||||||||||||
Ok(acc) | ||||||||||||||||||||||
}) | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
pub fn attestation_subnet_predicate(subnets: Vec<u8>) -> impl Fn(&Enr) -> bool + Send + Sync { | ||||||||||||||||||||||
move |enr: &Enr| { | ||||||||||||||||||||||
if subnets.is_empty() { | ||||||||||||||||||||||
|
@@ -490,4 +530,46 @@ mod tests { | |||||||||||||||||||||
}; | ||||||||||||||||||||||
assert!(!combined_subnet_predicate_fn(&enr)); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
#[test] | ||||||||||||||||||||||
fn test_compute_subscribed_subnet() { | ||||||||||||||||||||||
let node_id = NodeId::random(); | ||||||||||||||||||||||
let epoch = 1000; | ||||||||||||||||||||||
let index = 0; | ||||||||||||||||||||||
|
||||||||||||||||||||||
// Test valid subnet | ||||||||||||||||||||||
let subnet = compute_subscribed_subnet(node_id, epoch, index).unwrap(); | ||||||||||||||||||||||
assert!( | ||||||||||||||||||||||
subnet < ATTESTATION_SUBNET_COUNT as u8, | ||||||||||||||||||||||
"Subnet ID out of bounds: {}", | ||||||||||||||||||||||
subnet | ||||||||||||||||||||||
); | ||||||||||||||||||||||
|
||||||||||||||||||||||
// Test determinism | ||||||||||||||||||||||
let subnet_same = compute_subscribed_subnet(node_id, epoch, index).unwrap(); | ||||||||||||||||||||||
assert_eq!(subnet, subnet_same, "Non-deterministic subnet"); | ||||||||||||||||||||||
|
||||||||||||||||||||||
// Test different epoch | ||||||||||||||||||||||
let subnet_diff = compute_subscribed_subnet(node_id, epoch + 256, index).unwrap(); | ||||||||||||||||||||||
// Subnets may differ after 256 epochs due to seed change | ||||||||||||||||||||||
if subnet == subnet_diff { | ||||||||||||||||||||||
println!("Note: Same subnet for different epochs (possible but rare)"); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
// Test index | ||||||||||||||||||||||
let subnet_index1 = compute_subscribed_subnet(node_id, epoch, 1).unwrap(); | ||||||||||||||||||||||
// Subnets may be same or different (spec allows either) | ||||||||||||||||||||||
assert!( | ||||||||||||||||||||||
subnet_index1 < ATTESTATION_SUBNET_COUNT as u8, | ||||||||||||||||||||||
"Subnet ID for index 1 out of bounds: {}", | ||||||||||||||||||||||
subnet_index1 | ||||||||||||||||||||||
); | ||||||||||||||||||||||
|
||||||||||||||||||||||
// Test edge cases | ||||||||||||||||||||||
let subnet_epoch_zero = compute_subscribed_subnet(node_id, 0, index).unwrap(); | ||||||||||||||||||||||
assert!( | ||||||||||||||||||||||
subnet_epoch_zero < ATTESTATION_SUBNET_COUNT as u8, | ||||||||||||||||||||||
"Subnet ID for epoch 0 out of bounds" | ||||||||||||||||||||||
); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this code is currently not doing anything. could you make a function called update_attestation_subnets, which takes in a slot and updates the nodes enr, instead of doing this in the new() function
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, update function seems required here, i didn't use it as i remember from my last PR i removed update function , Should i call it from
poll
function or it will be called from consensus when the epoch changes