Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

219 changes: 219 additions & 0 deletions crates/cluster/src/manifest/cluster.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<Peer>> {
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<Vec<PeerId>> {
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<NodeIdx> {
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<PublicKey> {
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<PublicKey> {
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);
}
}
40 changes: 40 additions & 0 deletions crates/cluster/src/manifest/error.rs
Original file line number Diff line number Diff line change
@@ -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<T> = std::result::Result<T, ManifestError>;
3 changes: 2 additions & 1 deletion crates/cluster/src/manifest/helpers.rs
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 2 additions & 1 deletion crates/cluster/src/manifest/load.rs
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 2 additions & 1 deletion crates/cluster/src/manifest/materialise.rs
Original file line number Diff line number Diff line change
@@ -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.
14 changes: 13 additions & 1 deletion crates/cluster/src/manifest/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
//! # Charon Cluster Manifest
// Link to PR #4130: https://github.com/ObolNetwork/charon/pull/4130
Comment thread
iamquang95 marked this conversation as resolved.
// 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.
Expand Down
3 changes: 2 additions & 1 deletion crates/cluster/src/manifest/mutation.rs
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 2 additions & 1 deletion crates/cluster/src/manifest/mutationaddvalidator.rs
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 2 additions & 1 deletion crates/cluster/src/manifest/mutationlegacylock.rs
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 2 additions & 1 deletion crates/cluster/src/manifest/mutationnodeapproval.rs
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 2 additions & 1 deletion crates/cluster/src/manifest/types.rs
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions crates/testutil/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ publish.workspace = true

[dependencies]
k256.workspace = true
rand.workspace = true
rand_core.workspace = true
thiserror.workspace = true
hex.workspace = true
Expand Down
31 changes: 31 additions & 0 deletions crates/testutil/src/random.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<u8> {
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::*;
Expand Down Expand Up @@ -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"
);
}
}