diff --git a/Cargo.lock b/Cargo.lock index bf0f1101..efb5b13a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5640,6 +5640,7 @@ version = "1.7.1" dependencies = [ "hex", "k256", + "rand 0.8.5", "rand_core 0.6.4", "thiserror 2.0.18", ] diff --git a/crates/cluster/src/manifest/cluster.rs b/crates/cluster/src/manifest/cluster.rs index 8b137891..631b43e2 100644 --- a/crates/cluster/src/manifest/cluster.rs +++ b/crates/cluster/src/manifest/cluster.rs @@ -1 +1,220 @@ +use std::collections::HashSet; +use libp2p::PeerId; +use pluto_crypto::types::{PUBLIC_KEY_LENGTH, PublicKey}; +use pluto_eth2util::enr::Record; +use pluto_p2p::peer::Peer; + +use crate::{ + definition::NodeIdx, + helpers::to_0x_hex, + manifestpb::v1::{Cluster, Validator}, +}; + +use super::error::{ManifestError, Result}; + +impl Cluster { + /// Returns the cluster operators as a slice of p2p peers. + pub fn peers(&self) -> Result> { + if self.operators.is_empty() { + return Err(ManifestError::InvalidCluster); + } + + let mut resp = Vec::new(); + let mut dedup = HashSet::new(); + + for (i, operator) in self.operators.iter().enumerate() { + if dedup.contains(&operator.enr) { + return Err(ManifestError::DuplicatePeerENR { + enr: operator.enr.clone(), + }); + } + dedup.insert(&operator.enr); + + let record = Record::try_from(operator.enr.as_str())?; + + let peer = Peer::from_enr(&record, i)?; + + resp.push(peer); + } + + Ok(resp) + } + + /// Returns the operators p2p peer IDs. + pub fn peer_ids(&self) -> Result> { + let peers = self.peers()?; + Ok(peers.iter().map(|p| p.id).collect()) + } + + /// Returns the node index for the peer in the cluster. + pub fn node_idx(&self, peer_id: &PeerId) -> Result { + let peers = self.peers()?; + + for (i, p) in peers.iter().enumerate() { + if p.id == *peer_id { + return Ok(NodeIdx { + peer_idx: i, // 0-indexed + share_idx: i.saturating_add(1), // 1-indexed + }); + } + } + + Err(ManifestError::PeerNotInDefinition) + } +} + +impl Validator { + /// Returns the validator BLS group public key. + pub fn public_key(&self) -> Result { + let pk_vec = self.public_key.to_vec(); + pk_vec + .try_into() + .map_err(|_| ManifestError::InvalidHexLength { + expect: PUBLIC_KEY_LENGTH, + actual: self.public_key.len(), + }) + } + + /// Returns the validator hex group public key. + pub fn public_key_hex(&self) -> String { + to_0x_hex(&self.public_key) + } + + /// Returns the validator's peerIdx'th BLS public share. + pub fn public_share(&self, peer_idx: usize) -> Result { + let share = self + .pub_shares + .get(peer_idx) + .ok_or(ManifestError::InvalidCluster)?; + + let share_vec = share.to_vec(); + share_vec + .try_into() + .map_err(|_| ManifestError::InvalidHexLength { + expect: PUBLIC_KEY_LENGTH, + actual: share.len(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::manifestpb::v1::Operator; + + #[test] + fn cluster_peers_empty() { + let cluster = Cluster::default(); + let result = cluster.peers(); + assert!(result.is_err()); + } + + #[test] + fn cluster_peers_duplicate_enr() { + let duplicate_enr = "enr:-HW4QIHPUOMb34YoizKGhz7nsDNQ7hCaiuwyscmeaOQ04awdH05gDnGrZhxDfzcfHssCDeB-esi99A2RoZia6UaYBCuAgmlkgnY0iXNlY3AyNTZrMaECTUts0TYQMsqb0q652QCqTUXZ6tgKyUIzdMRRpyVNB2Y".to_string(); + + let cluster = Cluster { + operators: vec![ + Operator { + address: "0x123".to_string(), + enr: duplicate_enr.clone(), + }, + Operator { + address: "0x456".to_string(), + enr: duplicate_enr, // duplicate + }, + ], + ..Default::default() + }; + let result = cluster.peers(); + assert!(matches!( + result.unwrap_err(), + ManifestError::DuplicatePeerENR { .. } + )); + } + + #[test] + fn validator_public_share_test() { + let mut share0 = vec![0u8; PUBLIC_KEY_LENGTH]; + share0[0] = 0x01; + let mut share1 = vec![0u8; PUBLIC_KEY_LENGTH]; + share1[0] = 0x02; + + let validator = Validator { + pub_shares: vec![share0.into(), share1.into()], + ..Default::default() + }; + + let result0 = validator.public_share(0).unwrap(); + assert_eq!(result0[0], 0x01); + assert_eq!(result0.len(), PUBLIC_KEY_LENGTH); + + let result1 = validator.public_share(1).unwrap(); + assert_eq!(result1[0], 0x02); + assert_eq!(result1.len(), PUBLIC_KEY_LENGTH); + + assert!(validator.public_share(5).is_err()); + } + + #[test] + fn cluster_node_idx_test() { + let enr0 = "enr:-HW4QMOF6QNn4DRhSznyqhoRitA0R1P_p-Cf8I_phn-qR5EQEqFVV0_OtVuSWPj_HjGPd8lcXmcTen8j-9VT9hadVFyAgmlkgnY0iXNlY3AyNTZrMaECOx8LaV0436lNYE4XiqbGbVmXrEhUTg73e3M7HdRUWao".to_string(); + let enr1 = "enr:-HW4QKFO6PyCQdVXUdNEn80MJL7O048nRgZvheMhdT4LL9DGPjXlhrP1beyj8OEfZrapZVWNPEjfkUJubybvOPqkEhmAgmlkgnY0iXNlY3AyNTZrMaECGzgOLCm1ShATtBj1sh0VvshUOPkGW20ruTPPo5N_HZM".to_string(); + let enr2 = "enr:-HW4QJV3uqiuCqreW6nn794r-SxTC1fTXCnZQ4smu3l5F4DofbW566Zo8G0A9WL_wfGzkGRPPdGu6vYT7JfskEmbjIKAgmlkgnY0iXNlY3AyNTZrMaECh69y5mTVFNZQSh8Kc_57VwcK39WfY68y2F2WkeLa7EY".to_string(); + + let cluster = Cluster { + operators: vec![ + Operator { + address: "0x123".to_string(), + enr: enr0, + }, + Operator { + address: "0x456".to_string(), + enr: enr1, + }, + Operator { + address: "0x789".to_string(), + enr: enr2, + }, + ], + ..Default::default() + }; + + let peers = cluster.peers().unwrap(); + let peer_id = peers[1].id; + + let node_idx = cluster.node_idx(&peer_id).unwrap(); + assert_eq!(node_idx.peer_idx, 1); + assert_eq!(node_idx.share_idx, 2); + } + + #[test] + fn validator_public_key_test() { + let public_key = vec![0x42u8; PUBLIC_KEY_LENGTH]; + let validator = Validator { + public_key: public_key.clone().into(), + ..Default::default() + }; + + let result = validator.public_key().unwrap(); + assert_eq!(result[0], 0x42); + assert_eq!(result.len(), PUBLIC_KEY_LENGTH); + } + + #[test] + fn validator_public_key_hex_test() { + let mut public_key = vec![0u8; PUBLIC_KEY_LENGTH]; + public_key[0] = 0xab; + public_key[1] = 0xcd; + + let validator = Validator { + public_key: public_key.into(), + ..Default::default() + }; + + let hex = validator.public_key_hex(); + let expected = "0xabcd".to_string() + &"00".repeat(PUBLIC_KEY_LENGTH - 2); + assert_eq!(hex, expected); + } +} diff --git a/crates/cluster/src/manifest/error.rs b/crates/cluster/src/manifest/error.rs new file mode 100644 index 00000000..e3ae58c4 --- /dev/null +++ b/crates/cluster/src/manifest/error.rs @@ -0,0 +1,40 @@ +use thiserror::Error; + +/// Manifest module error type. +#[derive(Debug, Error)] +pub enum ManifestError { + /// Invalid cluster. + #[error("invalid cluster")] + InvalidCluster, + + /// Cluster contains duplicate peer ENRs. + #[error("cluster contains duplicate peer enrs: {enr}")] + DuplicatePeerENR { + /// ENR string. + enr: String, + }, + + /// Peer not in definition. + #[error("peer not in definition")] + PeerNotInDefinition, + + /// Invalid hex length. + #[error("invalid hex length (expect: {expect}, actual: {actual})")] + InvalidHexLength { + /// Expected length. + expect: usize, + /// Actual length. + actual: usize, + }, + + /// ENR parsing error. + #[error("enr parsing error: {0}")] + EnrParse(#[from] pluto_eth2util::enr::RecordError), + + /// P2P error. + #[error("p2p error: {0}")] + P2p(#[from] pluto_p2p::peer::PeerError), +} + +/// Result type alias for manifest operations. +pub type Result = std::result::Result; diff --git a/crates/cluster/src/manifest/helpers.rs b/crates/cluster/src/manifest/helpers.rs index 8b137891..5169cd93 100644 --- a/crates/cluster/src/manifest/helpers.rs +++ b/crates/cluster/src/manifest/helpers.rs @@ -1 +1,2 @@ - +// Link to PR #4130: https://github.com/ObolNetwork/charon/pull/4130 +// The manifest is removed and there is no use in production. diff --git a/crates/cluster/src/manifest/load.rs b/crates/cluster/src/manifest/load.rs index 8b137891..5169cd93 100644 --- a/crates/cluster/src/manifest/load.rs +++ b/crates/cluster/src/manifest/load.rs @@ -1 +1,2 @@ - +// Link to PR #4130: https://github.com/ObolNetwork/charon/pull/4130 +// The manifest is removed and there is no use in production. diff --git a/crates/cluster/src/manifest/materialise.rs b/crates/cluster/src/manifest/materialise.rs index 8b137891..5169cd93 100644 --- a/crates/cluster/src/manifest/materialise.rs +++ b/crates/cluster/src/manifest/materialise.rs @@ -1 +1,2 @@ - +// Link to PR #4130: https://github.com/ObolNetwork/charon/pull/4130 +// The manifest is removed and there is no use in production. diff --git a/crates/cluster/src/manifest/mod.rs b/crates/cluster/src/manifest/mod.rs index c3712bd6..d00b57ac 100644 --- a/crates/cluster/src/manifest/mod.rs +++ b/crates/cluster/src/manifest/mod.rs @@ -1,7 +1,19 @@ -//! # Charon Cluster Manifest +// Link to PR #4130: https://github.com/ObolNetwork/charon/pull/4130 +// The manifest is removed and there is no use in production. +// +// The following modules are no longer required: +// - load +// - materialise +// - mutation +// - mutationaddvalidator +// - mutationlegacylock +// - mutationnodeapproval +// - types /// Cluster manifest management and coordination. pub mod cluster; +/// Cluster manifest error types. +pub mod error; /// Cluster manifest helpers management and coordination. pub mod helpers; /// Cluster manifest load management and coordination. diff --git a/crates/cluster/src/manifest/mutation.rs b/crates/cluster/src/manifest/mutation.rs index 8b137891..5169cd93 100644 --- a/crates/cluster/src/manifest/mutation.rs +++ b/crates/cluster/src/manifest/mutation.rs @@ -1 +1,2 @@ - +// Link to PR #4130: https://github.com/ObolNetwork/charon/pull/4130 +// The manifest is removed and there is no use in production. diff --git a/crates/cluster/src/manifest/mutationaddvalidator.rs b/crates/cluster/src/manifest/mutationaddvalidator.rs index 8b137891..5169cd93 100644 --- a/crates/cluster/src/manifest/mutationaddvalidator.rs +++ b/crates/cluster/src/manifest/mutationaddvalidator.rs @@ -1 +1,2 @@ - +// Link to PR #4130: https://github.com/ObolNetwork/charon/pull/4130 +// The manifest is removed and there is no use in production. diff --git a/crates/cluster/src/manifest/mutationlegacylock.rs b/crates/cluster/src/manifest/mutationlegacylock.rs index 8b137891..5169cd93 100644 --- a/crates/cluster/src/manifest/mutationlegacylock.rs +++ b/crates/cluster/src/manifest/mutationlegacylock.rs @@ -1 +1,2 @@ - +// Link to PR #4130: https://github.com/ObolNetwork/charon/pull/4130 +// The manifest is removed and there is no use in production. diff --git a/crates/cluster/src/manifest/mutationnodeapproval.rs b/crates/cluster/src/manifest/mutationnodeapproval.rs index 8b137891..5169cd93 100644 --- a/crates/cluster/src/manifest/mutationnodeapproval.rs +++ b/crates/cluster/src/manifest/mutationnodeapproval.rs @@ -1 +1,2 @@ - +// Link to PR #4130: https://github.com/ObolNetwork/charon/pull/4130 +// The manifest is removed and there is no use in production. diff --git a/crates/cluster/src/manifest/types.rs b/crates/cluster/src/manifest/types.rs index 8b137891..5169cd93 100644 --- a/crates/cluster/src/manifest/types.rs +++ b/crates/cluster/src/manifest/types.rs @@ -1 +1,2 @@ - +// Link to PR #4130: https://github.com/ObolNetwork/charon/pull/4130 +// The manifest is removed and there is no use in production. diff --git a/crates/testutil/Cargo.toml b/crates/testutil/Cargo.toml index 599bf1d7..14de2a8a 100644 --- a/crates/testutil/Cargo.toml +++ b/crates/testutil/Cargo.toml @@ -8,6 +8,7 @@ publish.workspace = true [dependencies] k256.workspace = true +rand.workspace = true rand_core.workspace = true thiserror.workspace = true hex.workspace = true diff --git a/crates/testutil/src/random.rs b/crates/testutil/src/random.rs index 3b32830b..fd8e9d43 100644 --- a/crates/testutil/src/random.rs +++ b/crates/testutil/src/random.rs @@ -6,6 +6,7 @@ use k256::{ SecretKey, elliptic_curve::rand_core::{CryptoRng, Error, RngCore}, }; +use rand::{Rng, SeedableRng, rngs::StdRng}; /// A deterministic RNG that always returns the same byte value. /// This counter-acts the library's attempt at making ECDSA signatures @@ -45,6 +46,16 @@ pub fn generate_insecure_k1_key(seed: u8) -> SecretKey { SecretKey::random(&mut rng) } +/// Generates a deterministic 32-byte hash for testing using a seed. +pub fn random_bytes32_seed(seed: u8) -> Vec { + let seed_bytes = [seed; 32]; + let mut rng = StdRng::from_seed(seed_bytes); + + let mut bytes = vec![0u8; 32]; + rng.fill(&mut bytes[..]); + bytes +} + #[cfg(test)] mod tests { use super::*; @@ -82,4 +93,24 @@ mod tests { // Verify it's a valid key by deriving public key let _pubkey: PublicKey = key.public_key(); } + + #[test] + fn random_bytes32_deterministic() { + let bytes1 = random_bytes32_seed(42); + let bytes2 = random_bytes32_seed(42); + + assert_eq!(bytes1, bytes2, "Same seed should produce identical bytes"); + assert_eq!(bytes1.len(), 32); + } + + #[test] + fn random_bytes32_different_seeds() { + let bytes1 = random_bytes32_seed(1); + let bytes2 = random_bytes32_seed(2); + + assert_ne!( + bytes1, bytes2, + "Different seeds should produce different bytes" + ); + } }