From f027e46ff7207f3053f79f38c2d80d62fa1358cf Mon Sep 17 00:00:00 2001 From: Daniel Knopik <107140945+dknopik@users.noreply.github.com> Date: Thu, 20 Feb 2025 13:27:51 +0100 Subject: [PATCH] prepare types for network queueing system (#151) --- Cargo.lock | 1 + anchor/common/ssv_types/Cargo.toml | 1 + anchor/common/ssv_types/src/cluster.rs | 9 +++ anchor/common/ssv_types/src/committee.rs | 33 ++++++++ anchor/common/ssv_types/src/consensus.rs | 13 +-- anchor/common/ssv_types/src/lib.rs | 3 + anchor/common/ssv_types/src/message.rs | 4 +- anchor/common/ssv_types/src/msgid.rs | 53 +++++++++--- anchor/common/ssv_types/src/partial_sig.rs | 93 ++++++++++++++++++++++ anchor/qbft_manager/src/lib.rs | 6 +- anchor/qbft_manager/src/tests.rs | 4 +- anchor/subnet_tracker/src/lib.rs | 23 +----- anchor/validator_store/src/lib.rs | 4 +- 13 files changed, 195 insertions(+), 52 deletions(-) create mode 100644 anchor/common/ssv_types/src/committee.rs create mode 100644 anchor/common/ssv_types/src/partial_sig.rs diff --git a/Cargo.lock b/Cargo.lock index 5a1a1bfd..b6629a02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7441,6 +7441,7 @@ dependencies = [ name = "ssv_types" version = "0.1.0" dependencies = [ + "alloy", "base64 0.22.1", "derive_more 1.0.0", "ethereum_ssz", diff --git a/anchor/common/ssv_types/Cargo.toml b/anchor/common/ssv_types/Cargo.toml index 982aa8d2..239eb04d 100644 --- a/anchor/common/ssv_types/Cargo.toml +++ b/anchor/common/ssv_types/Cargo.toml @@ -5,6 +5,7 @@ edition = { workspace = true } authors = ["Sigma Prime "] [dependencies] +alloy = { workspace = true } base64 = { workspace = true } derive_more = { workspace = true } ethereum_ssz = { workspace = true } diff --git a/anchor/common/ssv_types/src/cluster.rs b/anchor/common/ssv_types/src/cluster.rs index 403e51b0..5679f5a9 100644 --- a/anchor/common/ssv_types/src/cluster.rs +++ b/anchor/common/ssv_types/src/cluster.rs @@ -1,3 +1,4 @@ +use crate::committee::CommitteeId; use crate::OperatorId; use derive_more::{Deref, From}; use indexmap::IndexSet; @@ -36,6 +37,14 @@ impl Cluster { pub fn get_f(&self) -> u64 { (self.cluster_members.len().saturating_sub(1) / 3) as u64 } + + pub fn committee_id(&self) -> CommitteeId { + self.cluster_members + .iter() + .cloned() + .collect::>() + .into() + } } /// A member of a Cluster. diff --git a/anchor/common/ssv_types/src/committee.rs b/anchor/common/ssv_types/src/committee.rs new file mode 100644 index 00000000..8ce3f0d7 --- /dev/null +++ b/anchor/common/ssv_types/src/committee.rs @@ -0,0 +1,33 @@ +use crate::OperatorId; +use alloy::primitives::keccak256; +use derive_more::{Deref, From}; + +const COMMITTEE_ID_LEN: usize = 32; + +/// Unique identifier for a committee +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, From, Deref)] +pub struct CommitteeId(pub [u8; COMMITTEE_ID_LEN]); + +impl From> for CommitteeId { + fn from(mut operator_ids: Vec) -> Self { + // Sort the operator IDs + operator_ids.sort(); + let mut data: Vec = Vec::with_capacity(operator_ids.len() * 4); + + // Add the operator IDs as 32 byte values + for id in operator_ids { + data.extend_from_slice(&id.to_le_bytes()); + } + + // Hash it all + keccak256(data).0.into() + } +} + +impl TryFrom<&[u8]> for CommitteeId { + type Error = (); + + fn try_from(value: &[u8]) -> Result { + value.try_into().map(CommitteeId).map_err(|_| ()) + } +} diff --git a/anchor/common/ssv_types/src/consensus.rs b/anchor/common/ssv_types/src/consensus.rs index d20f9085..ac238e0f 100644 --- a/anchor/common/ssv_types/src/consensus.rs +++ b/anchor/common/ssv_types/src/consensus.rs @@ -1,6 +1,6 @@ use crate::message::*; use crate::msgid::MessageId; -use crate::{OperatorId, ValidatorIndex}; +use crate::ValidatorIndex; use sha2::{Digest, Sha256}; use ssz::{Decode, DecodeError, Encode}; use ssz_derive::{Decode, Encode}; @@ -26,7 +26,7 @@ use types::{ // MsgType FullData // --------- ----------- // ConsensusMsg QBFTMessage SSZ -// PartialSigMsg PartialSignatureMessage SSZ +// PartialSigMsg PartialSignatureMessages SSZ pub trait QbftData: Debug + Clone + Encode + Decode { type Hash: Debug + Clone + Eq + Hash; @@ -144,15 +144,6 @@ impl Decode for QbftMessageType { } } -// A partial signature specific message -#[derive(Clone, Debug)] -pub struct PartialSignatureMessage { - pub partial_signature: Signature, - pub signing_root: Hash256, - pub signer: OperatorId, - pub validator_index: ValidatorIndex, -} - #[derive(Clone, Debug, PartialEq, Encode, Decode)] pub struct ValidatorConsensusData { pub duty: ValidatorDuty, diff --git a/anchor/common/ssv_types/src/lib.rs b/anchor/common/ssv_types/src/lib.rs index f3acc55c..e42c658a 100644 --- a/anchor/common/ssv_types/src/lib.rs +++ b/anchor/common/ssv_types/src/lib.rs @@ -1,12 +1,15 @@ pub use cluster::{Cluster, ClusterId, ClusterMember, ValidatorIndex, ValidatorMetadata}; +pub use committee::CommitteeId; pub use operator::{Operator, OperatorId}; pub use share::Share; mod cluster; +mod committee; pub mod consensus; pub mod domain_type; pub mod message; pub mod msgid; mod operator; +pub mod partial_sig; mod share; mod sql_conversions; mod util; diff --git a/anchor/common/ssv_types/src/message.rs b/anchor/common/ssv_types/src/message.rs index e2a4d299..abe232d6 100644 --- a/anchor/common/ssv_types/src/message.rs +++ b/anchor/common/ssv_types/src/message.rs @@ -92,7 +92,7 @@ impl SSVMessage { /// /// ``` /// use ssv_types::message::{MessageId, MsgType, SSVMessage}; - /// let message_id = MessageId::new([0u8; 56]); + /// let message_id = MessageId::from([0u8; 56]); /// let msg = SSVMessage::new(MsgType::SSVConsensusMsgType, message_id, vec![1, 2, 3]); /// ``` pub fn new(msg_type: MsgType, msg_id: MessageId, data: Vec) -> Self { @@ -154,7 +154,7 @@ impl SignedSSVMessage { /// ``` /// use ssv_types::message::{MessageId, MsgType, SSVMessage, SignedSSVMessage}; /// use ssv_types::OperatorId; - /// let ssv_msg = SSVMessage::new(MsgType::SSVConsensusMsgType, MessageId::new([0u8; 56]), vec![1,2,3]); + /// let ssv_msg = SSVMessage::new(MsgType::SSVConsensusMsgType, MessageId::from([0u8; 56]), vec![1,2,3]); /// let signed_msg = SignedSSVMessage::new(vec![vec![0; 256]], vec![OperatorId(1)], ssv_msg, vec![4,5,6]).unwrap(); /// ``` pub fn new( diff --git a/anchor/common/ssv_types/src/msgid.rs b/anchor/common/ssv_types/src/msgid.rs index 0f974260..b57f0e30 100644 --- a/anchor/common/ssv_types/src/msgid.rs +++ b/anchor/common/ssv_types/src/msgid.rs @@ -1,5 +1,8 @@ +use crate::committee::CommitteeId; use crate::domain_type::DomainType; +use derive_more::From; use ssz::{Decode, DecodeError, Encode}; +use types::PublicKeyBytes; const MESSAGE_ID_LEN: usize = 56; @@ -37,26 +40,54 @@ impl TryFrom<&[u8]> for Role { } #[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub enum Executor { - Committee([u8; 32]), - Validator([u8; 48]), +pub enum DutyExecutor { + Committee(CommitteeId), + Validator(PublicKeyBytes), } -#[derive(Debug, Clone, Hash, Eq, PartialEq)] +#[derive(Debug, Clone, Hash, Eq, PartialEq, From)] pub struct MessageId([u8; 56]); impl MessageId { - pub fn new(domain: &DomainType, role: Role, duty_executor: &Executor) -> Self { + pub fn new(domain: &DomainType, role: Role, duty_executor: &DutyExecutor) -> Self { let mut id = [0; 56]; id[0..4].copy_from_slice(&domain.0); id[4..8].copy_from_slice(&<[u8; 4]>::from(role)); match duty_executor { - Executor::Committee(slice) => id[24..].copy_from_slice(slice), - Executor::Validator(slice) => id[8..].copy_from_slice(slice), + DutyExecutor::Committee(committee_id) => { + id[24..].copy_from_slice(committee_id.as_slice()) + } + DutyExecutor::Validator(public_key) => { + id[8..].copy_from_slice(public_key.as_serialized()) + } } MessageId(id) } + + pub fn domain(&self) -> DomainType { + DomainType( + self.0[0..4] + .try_into() + .expect("we know the slice has the correct length"), + ) + } + + pub fn role(&self) -> Option { + self.0[4..8].try_into().ok() + } + + pub fn duty_executor(&self) -> Option { + // which kind of executor we need to get depends on the role + match self.role()? { + Role::Committee => self.0[24..].try_into().ok().map(DutyExecutor::Committee), + Role::Aggregator | Role::Proposer | Role::SyncCommittee => { + PublicKeyBytes::deserialize(&self.0[8..]) + .ok() + .map(DutyExecutor::Validator) + } + } + } } impl AsRef<[u8]> for MessageId { @@ -65,9 +96,11 @@ impl AsRef<[u8]> for MessageId { } } -impl From<[u8; MESSAGE_ID_LEN]> for MessageId { - fn from(value: [u8; MESSAGE_ID_LEN]) -> Self { - MessageId(value) +impl TryFrom<&[u8]> for MessageId { + type Error = (); + + fn try_from(value: &[u8]) -> Result { + value.try_into().map(MessageId).map_err(|_| ()) } } diff --git a/anchor/common/ssv_types/src/partial_sig.rs b/anchor/common/ssv_types/src/partial_sig.rs new file mode 100644 index 00000000..aaa09ee3 --- /dev/null +++ b/anchor/common/ssv_types/src/partial_sig.rs @@ -0,0 +1,93 @@ +use crate::{OperatorId, ValidatorIndex}; +use ssz::{Decode, DecodeError, Encode}; +use ssz_derive::{Decode, Encode}; +use types::{Hash256, Signature, Slot}; + +#[derive(Clone, Copy, Debug)] +pub enum PartialSignatureKind { + // PostConsensusPartialSig is a partial signature over a decided duty (attestation data, block, etc) + PostConsensus = 0, + // RandaoPartialSig is a partial signature over randao reveal + RandaoPartialSig = 1, + // SelectionProofPartialSig is a partial signature for aggregator selection proof + SelectionProofPartialSig = 2, + // ContributionProofs is the partial selection proofs for sync committee contributions (it's an array of sigs) + ContributionProofs = 3, + // ValidatorRegistrationPartialSig is a partial signature over a ValidatorRegistration object + ValidatorRegistration = 4, + // VoluntaryExitPartialSig is a partial signature over a VoluntaryExit object + VoluntaryExit = 5, +} + +impl TryFrom for PartialSignatureKind { + type Error = (); + + fn try_from(value: u64) -> Result { + match value { + 0 => Ok(PartialSignatureKind::PostConsensus), + 1 => Ok(PartialSignatureKind::RandaoPartialSig), + 2 => Ok(PartialSignatureKind::SelectionProofPartialSig), + 3 => Ok(PartialSignatureKind::ContributionProofs), + 4 => Ok(PartialSignatureKind::ValidatorRegistration), + 5 => Ok(PartialSignatureKind::VoluntaryExit), + _ => Err(()), + } + } +} + +const U64_SIZE: usize = 8; // u64 is 8 bytes + +impl Encode for PartialSignatureKind { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_append(&self, buf: &mut Vec) { + buf.extend_from_slice(&(*self as u64).to_le_bytes()); + } + + fn ssz_fixed_len() -> usize { + U64_SIZE + } + + fn ssz_bytes_len(&self) -> usize { + U64_SIZE + } +} + +impl Decode for PartialSignatureKind { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + U64_SIZE + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + if bytes.len() != U64_SIZE { + return Err(DecodeError::InvalidByteLength { + len: bytes.len(), + expected: U64_SIZE, + }); + } + let value = u64::from_le_bytes(bytes.try_into().unwrap()); + value.try_into().map_err(|_| DecodeError::NoMatchingVariant) + } +} + +// A partial signature specific message +#[derive(Clone, Debug, Encode, Decode)] +pub struct PartialSignatureMessages { + pub kind: PartialSignatureKind, + pub slot: Slot, + pub messages: Vec, +} + +#[derive(Clone, Debug, Encode, Decode)] +pub struct PartialSignatureMessage { + pub partial_signature: Signature, + pub signing_root: Hash256, + pub signer: OperatorId, + pub validator_index: ValidatorIndex, +} diff --git a/anchor/qbft_manager/src/lib.rs b/anchor/qbft_manager/src/lib.rs index aeea3f5e..8cd67e14 100644 --- a/anchor/qbft_manager/src/lib.rs +++ b/anchor/qbft_manager/src/lib.rs @@ -15,7 +15,7 @@ use std::error::Error; use ssv_types::message::SignedSSVMessage; use ssv_types::OperatorId as QbftOperatorId; -use ssv_types::{Cluster, ClusterId, OperatorId}; +use ssv_types::{Cluster, CommitteeId, OperatorId}; use ssz::Encode; use std::fmt::Debug; use std::hash::Hash; @@ -40,10 +40,10 @@ const QBFT_SIGNER_NAME: &str = "qbft_signer"; /// Number of slots to keep before the current slot const QBFT_RETAIN_SLOTS: u64 = 1; -// Unique Identifier for a Cluster and its corresponding QBFT instance +// Unique Identifier for a committee and its corresponding QBFT instance #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct CommitteeInstanceId { - pub committee: ClusterId, + pub committee: CommitteeId, pub instance_height: InstanceHeight, } diff --git a/anchor/qbft_manager/src/tests.rs b/anchor/qbft_manager/src/tests.rs index 5bcb783c..30c01170 100644 --- a/anchor/qbft_manager/src/tests.rs +++ b/anchor/qbft_manager/src/tests.rs @@ -6,7 +6,7 @@ use processor::Senders; use slot_clock::{ManualSlotClock, SlotClock}; use ssv_types::consensus::{BeaconVote, QbftMessage, QbftMessageType}; use ssv_types::message::SignedSSVMessage; -use ssv_types::{Cluster, ClusterId, OperatorId}; +use ssv_types::{Cluster, ClusterId, CommitteeId, OperatorId}; use ssz::Decode; use std::collections::HashMap; use std::sync::LazyLock; @@ -550,7 +550,7 @@ mod manager_tests { fn generate_test_data(id: usize) -> (BeaconVote, CommitteeInstanceId) { // setup mock data let id = CommitteeInstanceId { - committee: ClusterId([0; 32]), + committee: CommitteeId([0; 32]), instance_height: id.into(), }; diff --git a/anchor/subnet_tracker/src/lib.rs b/anchor/subnet_tracker/src/lib.rs index ff301719..fd0e0d44 100644 --- a/anchor/subnet_tracker/src/lib.rs +++ b/anchor/subnet_tracker/src/lib.rs @@ -1,9 +1,7 @@ -use alloy::primitives::keccak256; use alloy::primitives::ruint::aliases::U256; use database::{NetworkState, UniqueIndex}; use log::warn; use serde::{Deserialize, Serialize}; -use ssv_types::Cluster; use std::collections::HashSet; use std::ops::Deref; use std::time::Duration; @@ -76,7 +74,7 @@ async fn subnet_tracker( for cluster_id in state.get_own_clusters() { if let Some(cluster) = state.clusters().get_by(cluster_id) { // Derive a numeric "committee ID" and convert to an index in [0..subnet_count]. - let id = get_committee_id(&cluster); + let id = U256::from_be_bytes(*cluster.committee_id()); let index = (id % U256::from(subnet_count)) .try_into() .expect("modulo must be < subnet_count"); @@ -120,25 +118,6 @@ async fn subnet_tracker( } } -fn get_committee_id(cluster: &Cluster) -> U256 { - let mut operator_ids = cluster - .cluster_members - .iter() - .map(|x| **x) - .collect::>(); - // Sort the operator IDs - operator_ids.sort(); - let mut data: Vec = Vec::with_capacity(operator_ids.len() * 4); - - // Add the operator IDs as 32 byte values - for id in operator_ids { - data.extend_from_slice(&id.to_le_bytes()); - } - - // Hash it all - U256::from_be_bytes(keccak256(data).0) -} - /// only useful for testing - introduce feature flag? pub fn test_tracker( executor: TaskExecutor, diff --git a/anchor/validator_store/src/lib.rs b/anchor/validator_store/src/lib.rs index 81fe0147..b7ec7aca 100644 --- a/anchor/validator_store/src/lib.rs +++ b/anchor/validator_store/src/lib.rs @@ -388,7 +388,7 @@ impl AnchorValidatorStore { .qbft_manager .decide_instance( CommitteeInstanceId { - committee: validator.cluster.cluster_id, + committee: validator.cluster.committee_id(), instance_height: slot.as_usize().into(), }, vote, @@ -723,7 +723,7 @@ impl ValidatorStore for AnchorValidatorStore { .qbft_manager .decide_instance( CommitteeInstanceId { - committee: validator.cluster.cluster_id, + committee: validator.cluster.committee_id(), instance_height: attestation.data().slot.as_usize().into(), }, BeaconVote {