diff --git a/Cargo.toml b/Cargo.toml index 03f294c0..e7428797 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,12 @@ publish = false missing_docs = "deny" unsafe_code = "forbid" +[workspace.dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "^1.0" } +hex = { version = "^0.4.3" } +chrono = { version = "0.4", features = ["serde"] } + [workspace.lints.clippy] arithmetic_side_effects = "deny" cast_lossless = "deny" diff --git a/crates/charon-core/Cargo.toml b/crates/charon-core/Cargo.toml index 3b92ed1a..baba8427 100644 --- a/crates/charon-core/Cargo.toml +++ b/crates/charon-core/Cargo.toml @@ -7,6 +7,10 @@ license.workspace = true publish.workspace = true [dependencies] +serde.workspace = true +serde_json.workspace = true +hex.workspace = true +chrono.workspace = true [lints] workspace = true diff --git a/crates/charon-core/src/lib.rs b/crates/charon-core/src/lib.rs index a3ed9a9d..7a6c37d5 100644 --- a/crates/charon-core/src/lib.rs +++ b/crates/charon-core/src/lib.rs @@ -4,36 +4,5 @@ //! This crate provides the fundamental building blocks, data structures, and //! core algorithms used throughout the Charon system. -/// Adds two numbers together. -/// -/// # Arguments -/// -/// * `left` - The first number to add -/// * `right` - The second number to add -/// -/// # Returns -/// -/// The sum of the two numbers -/// -/// # Examples -/// -/// ``` -/// use charon_core::add; -/// -/// let result = add(2, 2); -/// assert_eq!(result, 4); -/// ``` -pub fn add(left: u64, right: u64) -> u64 { - left.wrapping_add(right) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +/// Types for the Charon core. +pub mod types; diff --git a/crates/charon-core/src/types.rs b/crates/charon-core/src/types.rs new file mode 100644 index 00000000..9328ded2 --- /dev/null +++ b/crates/charon-core/src/types.rs @@ -0,0 +1,910 @@ +//! Types for the Charon core. + +use std::{collections::HashMap, fmt::Display, iter}; + +use chrono::{DateTime, Duration, Utc}; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug as StdDebug; + +/// The type of duty. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum DutyType { + /// Unknown duty type. + Unknown, + /// Proposer duty type. + Proposer, + /// Attester duty type. + Attester, + /// Signature duty type. + Signature, + /// Exit duty type. + Exit, + /// Builder proposer duty type. + BuilderProposer, + /// Builder registration duty type. + BuilderRegistration, + /// Randao duty type. + Randao, + /// Prepare aggregator duty type. + PrepareAggregator, + /// Aggregator duty type. + Aggregator, + /// Sync message duty type. + SyncMessage, + /// Prepare sync contribution duty type. + PrepareSyncContribution, + /// Sync contribution duty type. + SyncContribution, + /// Info sync duty type. + InfoSync, + /// Duty sentinel duty type. Must always be last. + DutySentinel(Box), +} + +impl Display for DutyType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // safe to unwrap because we know the duty type is valid + let v = serde_json::to_value(self).expect("failed to serialize duty type"); + if let Some(s) = v.as_str() { + write!(f, "{}", s) + } else { + // fallback for non-string variants (structs, numbers, etc.) + write!(f, "{}", v) + } + } +} + +impl DutyType { + /// Returns true if the duty type is valid. + pub fn is_valid(&self) -> bool { + !matches!(self, DutyType::Unknown | DutyType::DutySentinel(_)) + } +} + +/// SlotNumber struct +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct SlotNumber(u64); + +impl Display for SlotNumber { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for SlotNumber { + fn from(slot: u64) -> Self { + Self::new(slot) + } +} + +impl From for u64 { + fn from(slot: SlotNumber) -> Self { + slot.inner() + } +} + +impl SlotNumber { + /// Create a new slot number. + pub fn new(slot: u64) -> Self { + SlotNumber(slot) + } + + /// Inner slot number. + pub fn inner(&self) -> u64 { + self.0 + } + + /// Next slot number. + pub fn next(&self) -> Self { + Self::new(self.inner().saturating_add(1)) + } +} + +/// Duty struct +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Duty { + /// Ethereum consensus layer slot. + pub slot: SlotNumber, + /// Duty type performed in the slot. + pub duty_type: DutyType, +} + +impl Display for Duty { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}/{}", self.slot, self.duty_type) + } +} + +impl Duty { + /// Create a new duty. + pub fn new(slot: SlotNumber, duty_type: DutyType) -> Self { + Self { slot, duty_type } + } + + /// Create a new attester duty. + pub fn new_attester_duty(slot: SlotNumber) -> Self { + Self::new(slot, DutyType::Attester) + } + + /// Create a new randao duty. + pub fn new_randao_duty(slot: SlotNumber) -> Self { + Self::new(slot, DutyType::Randao) + } + + /// Create a new voluntary exit duty. + pub fn new_voluntary_exit_duty(slot: SlotNumber) -> Self { + Self::new(slot, DutyType::Exit) + } + + /// Create a new proposer duty. + pub fn new_proposer_duty(slot: SlotNumber) -> Self { + Self::new(slot, DutyType::Proposer) + } + + /// Create a new builder proposer duty. + pub fn new_builder_proposer_duty(slot: SlotNumber) -> Self { + Self::new(slot, DutyType::BuilderProposer) + } + + /// Create a new builder registration duty. + pub fn new_builder_registration_duty(slot: SlotNumber) -> Self { + Self::new(slot, DutyType::BuilderRegistration) + } + + /// Create a new sync contribution duty. + pub fn new_sync_contribution_duty(slot: SlotNumber) -> Self { + Self::new(slot, DutyType::SyncContribution) + } + + /// Create a new signature duty. + pub fn new_signature_duty(slot: SlotNumber) -> Self { + Self::new(slot, DutyType::Signature) + } + + /// Create a new prepare aggregator duty. + pub fn new_prepare_aggregator_duty(slot: SlotNumber) -> Self { + Self::new(slot, DutyType::PrepareAggregator) + } + + /// Create a new aggregator duty. + pub fn new_aggregator_duty(slot: SlotNumber) -> Self { + Self::new(slot, DutyType::Aggregator) + } + + /// Create a new sync message duty. + pub fn new_sync_message_duty(slot: SlotNumber) -> Self { + Self::new(slot, DutyType::SyncMessage) + } + + /// Create a new prepare sync contribution duty. + pub fn new_prepare_sync_contribution_duty(slot: SlotNumber) -> Self { + Self::new(slot, DutyType::PrepareSyncContribution) + } + + /// Create a new info sync duty. + pub fn new_info_sync_duty(slot: SlotNumber) -> Self { + Self::new(slot, DutyType::InfoSync) + } +} + +/// The type of proposal. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ProposalType { + /// Full proposal type. + Full, + /// Builder proposal type. + Builder, + /// Synthetic proposal type. + Synthetic, +} + +// In golang implementation they use pk_len = 98, which is 0x + [48 bytes] +// We use pk_len = 48, which is [48 bytes], the main difference is that we store +// the pub key as [u8; 48] instead of string. +// [original implementation](https://github.com/ObolNetwork/charon/blob/b3008103c5429b031b63518195f4c49db4e9a68d/core/types.go#L264) +const PK_LEN: usize = 48; +const SIG_LEN: usize = 96; + +/// Public key struct +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct PubKey(pub(crate) [u8; PK_LEN]); + +impl Serialize for PubKey { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl TryFrom for PubKey { + type Error = PubKeyError; + + fn try_from(value: String) -> Result { + let value = value.strip_prefix("0x").unwrap_or(&value); + let hex_value = hex::decode(value).map_err(|_| PubKeyError::InvalidString)?; + PubKey::try_from(hex_value.as_slice()) + } +} + +impl<'de> Deserialize<'de> for PubKey { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let hex_str = String::deserialize(deserializer)?; + let hex_str = hex_str.strip_prefix("0x").unwrap_or(&hex_str); + + let bytes = hex::decode(hex_str).map_err(serde::de::Error::custom)?; + + if bytes.len() != PK_LEN { + return Err(serde::de::Error::custom(format!( + "invalid public key length: got {}, want {}", + bytes.len(), + PK_LEN + ))); + } + + let mut pk = [0u8; PK_LEN]; + pk.copy_from_slice(&bytes); + Ok(PubKey(pk)) + } +} + +impl From<[u8; PK_LEN]> for PubKey { + fn from(pk: [u8; PK_LEN]) -> Self { + PubKey(pk) + } +} + +/// Public key error type +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PubKeyError { + /// Invalid public key length. + InvalidLength, + /// Invalid public key string. + InvalidString, +} + +impl PubKey { + /// Create a new public key. + pub fn new(pk: [u8; PK_LEN]) -> Self { + PubKey(pk) + } + + /// Returns logging-friendly abbreviated form: "b82_97f" + pub fn abbreviated(&self) -> String { + let hex = hex::encode(self.0); + format!("{}_{}", &hex[0..3], &hex[93..96]) + } +} + +impl TryFrom<&[u8]> for PubKey { + type Error = PubKeyError; + + fn try_from(bytes: &[u8]) -> Result { + if bytes.len() != PK_LEN { + return Err(PubKeyError::InvalidLength); + } + let mut arr = [0u8; PK_LEN]; + arr.copy_from_slice(bytes); + Ok(PubKey(arr)) + } +} + +impl Display for PubKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "0x{}", hex::encode(self.0)) + } +} + +/// Implement AsRef<[u8]> for PubKey to allow for easy conversion to bytes. +impl AsRef<[u8]> for PubKey { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +// todo: add toEth2Format for the pub key +// https://github.com/ObolNetwork/charon/blob/b3008103c5429b031b63518195f4c49db4e9a68d/core/types.go#L311 + +/// Duty definition type +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DutyDefinition(T); + +impl DutyDefinition +where + T: Clone + Serialize + StdDebug, +{ + /// Create a new duty definition. + pub fn new(duty_definition: T) -> Self { + Self(duty_definition) + } +} + +/// Duty definition set +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct DutyDefinitionSet(HashMap>) +where + T: Clone + Serialize + StdDebug; + +impl DutyDefinitionSet +where + T: Clone + Serialize + StdDebug, +{ + /// Create a new duty definition set. + pub fn new() -> Self { + Self(HashMap::default()) + } + + /// Get a duty definition by duty type. + pub fn get(&self, duty_type: &DutyType) -> Option<&DutyDefinition> { + self.0.get(duty_type) + } + + /// Insert a duty definition. + pub fn insert(&mut self, duty_type: DutyType, duty_definition: DutyDefinition) { + self.0.insert(duty_type, duty_definition); + } + + /// Remove a duty definition by duty type. + pub fn remove(&mut self, duty_type: &DutyType) -> Option> { + self.0.remove(duty_type) + } + + /// Inner duty definition set. + pub fn inner(&self) -> &HashMap> { + &self.0 + } + + /// Inner duty definition set. + pub fn inner_mut(&mut self) -> &mut HashMap> { + &mut self.0 + } +} + +/// Unsigned data type +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnsignedData(T); + +impl UnsignedData +where + T: Clone + Serialize + StdDebug, +{ + /// Create a new unsigned data. + pub fn new(unsigned_data: T) -> Self { + Self(unsigned_data) + } +} +/// Unsigned data set +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnsignedDataSet(HashMap>) +where + T: Clone + Serialize + StdDebug; + +impl Default for UnsignedDataSet +where + T: Clone + Serialize + StdDebug, +{ + fn default() -> Self { + Self(HashMap::default()) + } +} + +impl UnsignedDataSet +where + T: Clone + Serialize + StdDebug, +{ + /// Create a new unsigned data set. + pub fn new() -> Self { + Self::default() + } + + /// Get an unsigned data by duty type. + pub fn get(&self, duty_type: &DutyType) -> Option<&UnsignedData> { + self.0.get(duty_type) + } + + /// Insert an unsigned data. + pub fn insert(&mut self, duty_type: DutyType, unsigned_data: UnsignedData) { + self.0.insert(duty_type, unsigned_data); + } + + /// Remove an unsigned data by duty type. + pub fn remove(&mut self, duty_type: &DutyType) -> Option> { + self.0.remove(duty_type) + } + + /// Inner unsigned data set. + pub fn inner(&self) -> &HashMap> { + &self.0 + } + + /// Inner unsigned data set. + pub fn inner_mut(&mut self) -> &mut HashMap> { + &mut self.0 + } +} + +// todo: add proper signature type +/// Signature type +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Signature(pub(crate) [u8; SIG_LEN]); + +impl Signature { + /// Create a new signature. + pub fn new(signature: [u8; SIG_LEN]) -> Self { + Signature(signature) + } +} + +/// Signed data type +pub trait SignedData: Clone + Serialize + StdDebug { + /// The error type + type Error: std::error::Error; + + /// signature returns the signed duty data's signature. + fn signature(&self) -> Signature; + + /// set_signature returns a copy of signed duty data with the signature + /// replaced. + fn set_signature(&mut self, signature: Signature) -> Result<(), Self::Error>; + + /// message_root returns the message root for the unsigned data. + fn message_root(&self) -> [u8; 32]; +} + +// todo: add Eth2SignedData type +// https://github.com/ObolNetwork/charon/blob/b3008103c5429b031b63518195f4c49db4e9a68d/core/types.go#L396 + +/// ParSignedData is a partially signed duty data only signed by a single +/// threshold BLS share. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParSignedData { + /// Partially signed duty data. + pub signed_data: T, + + /// Threshold BLS share index. + pub share_idx: u64, +} + +impl ParSignedData +where + T: SignedData, +{ + /// Create a new partially signed data. + pub fn new(partially_signed_data: T, share_idx: u64) -> Self { + Self { + signed_data: partially_signed_data, + share_idx, + } + } +} + +/// ParSignedDataSet is a set of partially signed duty data only signed by a +/// single threshold BLS share. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParSignedDataSet(HashMap>); + +impl Default for ParSignedDataSet +where + T: SignedData, +{ + fn default() -> Self { + Self(HashMap::default()) + } +} + +impl ParSignedDataSet +where + T: SignedData, +{ + /// Create a new partially signed data set. + pub fn new() -> Self { + Self::default() + } + + /// Get a partially signed data by public key. + pub fn get(&self, pub_key: &PubKey) -> Option<&ParSignedData> { + self.inner().get(pub_key) + } + + /// Insert a partially signed data. + pub fn insert(&mut self, pub_key: PubKey, partially_signed_data: ParSignedData) { + self.inner_mut().insert(pub_key, partially_signed_data); + } + + /// Remove a partially signed data by public key. + pub fn remove(&mut self, pub_key: &PubKey) -> Option> { + self.inner_mut().remove(pub_key) + } + + /// Inner partially signed data set. + pub fn inner(&self) -> &HashMap> { + &self.0 + } + + /// Inner partially signed data set. + pub fn inner_mut(&mut self) -> &mut HashMap> { + &mut self.0 + } +} + +/// SignedDataSet is a set of signed duty data. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SignedDataSet(HashMap); + +impl Default for SignedDataSet +where + T: SignedData, +{ + fn default() -> Self { + Self(HashMap::default()) + } +} + +impl SignedDataSet +where + T: SignedData, +{ + /// Create a new signed data set. + pub fn new() -> Self { + Self::default() + } + + /// Get a signed data by public key. + pub fn get(&self, pub_key: &PubKey) -> Option<&T> { + self.0.get(pub_key) + } + + /// Insert a signed data. + pub fn insert(&mut self, pub_key: PubKey, signed_data: T) { + self.0.insert(pub_key, signed_data); + } + + /// Remove a signed data by public key. + pub fn remove(&mut self, pub_key: &PubKey) -> Option { + self.0.remove(pub_key) + } + + /// Inner signed data set. + pub fn inner(&self) -> &HashMap { + &self.0 + } + + /// Inner signed data set. + pub fn inner_mut(&mut self) -> &mut HashMap { + &mut self.0 + } +} + +/// Slot struct +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Slot { + /// The slot number. + pub slot: SlotNumber, + + /// The time. + pub time: DateTime, + + /// The slot duration. + pub slot_duration: Duration, + + /// Slots per epoch. + pub slots_per_epoch: u64, +} + +impl Slot { + /// Get the epoch of the slot + pub fn epoch(&self) -> u64 { + #[allow(clippy::arithmetic_side_effects)] + self.slot.inner().saturating_div(self.slots_per_epoch) + } + + /// Returns true if this is the last slot in the epoch. + #[allow(clippy::arithmetic_side_effects)] + pub fn last_in_epoch(&self) -> bool { + self.slot.inner().wrapping_rem(self.slots_per_epoch) + == self.slots_per_epoch.saturating_sub(1) + } + + /// Returns true if this is the first slot in the epoch. + #[allow(clippy::arithmetic_side_effects)] + pub fn first_in_epoch(&self) -> bool { + self.slot.inner().wrapping_rem(self.slots_per_epoch) == 0 + } + + /// Returns the next slot + #[allow(clippy::arithmetic_side_effects)] + pub fn next_slot(&self) -> Slot { + Slot { + slot: self.slot.next(), + time: self.time + self.slot_duration, + slot_duration: self.slot_duration, + slots_per_epoch: self.slots_per_epoch, + } + } + + /// Returns an iterator over slots starting from this one + pub fn iter(&self) -> impl Iterator { + iter::successors(Some(self.clone()), |slot| Some(slot.next_slot())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pub_key_to_string() { + const ORIGINAL_PK_LEN: usize = 98; + + let key = PubKey::new([0; PK_LEN]); + + // Check whether the string representation is the same as the go's public key + // length + assert_eq!(key.to_string().len(), ORIGINAL_PK_LEN); + assert_eq!( + key.to_string(), + "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ); + } + + #[test] + fn test_new_builder_registration_duty() { + let duty = Duty::new_builder_registration_duty(SlotNumber(1)); + assert_eq!(duty.duty_type, DutyType::BuilderRegistration); + assert_eq!(duty.to_string(), "1/builder_registration"); + assert_eq!(u64::from(duty.slot), 1); + } + + #[test] + fn test_new_signature_duty() { + let duty = Duty::new_signature_duty(SlotNumber(1)); + assert_eq!(duty.duty_type, DutyType::Signature); + assert_eq!(duty.to_string(), "1/signature"); + assert_eq!(u64::from(duty.slot), 1); + } + + #[test] + fn test_new_prepare_aggregator_duty() { + let duty = Duty::new_prepare_aggregator_duty(SlotNumber(1)); + assert_eq!(duty.duty_type, DutyType::PrepareAggregator); + assert_eq!(duty.to_string(), "1/prepare_aggregator"); + assert_eq!(u64::from(duty.slot), 1); + } + + #[test] + fn test_new_aggregator_duty() { + let duty = Duty::new_aggregator_duty(SlotNumber(1)); + assert_eq!(duty.duty_type, DutyType::Aggregator); + assert_eq!(duty.to_string(), "1/aggregator"); + assert_eq!(u64::from(duty.slot), 1); + } + + #[test] + fn test_new_sync_contribution_duty() { + let duty = Duty::new_sync_contribution_duty(SlotNumber(1)); + assert_eq!(duty.duty_type, DutyType::SyncContribution); + assert_eq!(duty.to_string(), "1/sync_contribution"); + assert_eq!(u64::from(duty.slot), 1); + } + + #[test] + fn test_new_sync_message_duty() { + let duty = Duty::new_sync_message_duty(SlotNumber(1)); + assert_eq!(duty.duty_type, DutyType::SyncMessage); + assert_eq!(duty.to_string(), "1/sync_message"); + assert_eq!(u64::from(duty.slot), 1); + } + + #[test] + fn test_new_prepare_sync_contribution_duty() { + let duty = Duty::new_prepare_sync_contribution_duty(SlotNumber(1)); + assert_eq!(duty.duty_type, DutyType::PrepareSyncContribution); + assert_eq!(duty.to_string(), "1/prepare_sync_contribution"); + assert_eq!(u64::from(duty.slot), 1); + } + + #[test] + fn test_new_info_sync_duty() { + let duty = Duty::new_info_sync_duty(SlotNumber(1)); + assert_eq!(duty.duty_type, DutyType::InfoSync); + assert_eq!(duty.to_string(), "1/info_sync"); + assert_eq!(u64::from(duty.slot), 1); + } + + #[test] + fn test_slot() { + let slot = Slot { + slot: SlotNumber(123), + time: DateTime::from_timestamp(100, 100).unwrap(), + slot_duration: Duration::seconds(4), + slots_per_epoch: 32, + }; + + assert_eq!(u64::from(slot.slot), 0x7b); + assert_eq!(slot.epoch(), 3); + assert!(!slot.last_in_epoch()); + assert!(!slot.first_in_epoch()); + + let next = slot.next_slot(); + assert_eq!(next.slot, SlotNumber(124)); + assert_eq!(next.time, DateTime::from_timestamp(104, 100).unwrap()); + assert_eq!(next.slot_duration, Duration::seconds(4)); + assert_eq!(next.slots_per_epoch, 32); + } + + #[test] + fn test_serialize_pubkey() { + let pk = PubKey::new([42u8; PK_LEN]); + let serialized = serde_json::to_string(&pk).unwrap(); + assert_eq!(serialized, format!("\"0x{}\"", hex::encode([42u8; PK_LEN]))); + } + + #[test] + fn test_deserialize_pubkey() { + let serialized = format!("\"0x{}\"", hex::encode([42u8; PK_LEN])); + let deserialized: PubKey = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, PubKey::new([42u8; PK_LEN])); + } + + #[test] + fn test_slot_iter() { + let slot = Slot { + slot: SlotNumber(123), + time: DateTime::from_timestamp(100, 100).unwrap(), + slot_duration: Duration::seconds(4), + slots_per_epoch: 32, + }; + + assert_eq!(slot.iter().nth(10).unwrap().slot, SlotNumber(133)); + assert_eq!(slot.iter().nth(31).unwrap().slot, SlotNumber(154)); + assert_eq!(slot.iter().nth(32).unwrap().slot, SlotNumber(155)); + assert_eq!(slot.iter().nth(33).unwrap().slot, SlotNumber(156)); + } + + #[test] + fn test_display_duty_type() { + assert_eq!(DutyType::Unknown.to_string(), "unknown"); + assert_eq!(DutyType::Proposer.to_string(), "proposer"); + assert_eq!(DutyType::Attester.to_string(), "attester"); + assert_eq!(DutyType::Signature.to_string(), "signature"); + assert_eq!(DutyType::Exit.to_string(), "exit"); + assert_eq!(DutyType::BuilderProposer.to_string(), "builder_proposer"); + assert_eq!( + DutyType::BuilderRegistration.to_string(), + "builder_registration" + ); + assert_eq!(DutyType::Randao.to_string(), "randao"); + assert_eq!( + DutyType::PrepareAggregator.to_string(), + "prepare_aggregator" + ); + assert_eq!(DutyType::Aggregator.to_string(), "aggregator"); + assert_eq!(DutyType::SyncMessage.to_string(), "sync_message"); + assert_eq!( + DutyType::PrepareSyncContribution.to_string(), + "prepare_sync_contribution" + ); + assert_eq!(DutyType::SyncContribution.to_string(), "sync_contribution"); + assert_eq!(DutyType::InfoSync.to_string(), "info_sync"); + } + + #[test] + fn test_duty_type_is_valid() { + assert!(!DutyType::Unknown.is_valid()); + assert!(DutyType::Proposer.is_valid()); + assert!(DutyType::Attester.is_valid()); + assert!(DutyType::Signature.is_valid()); + assert!(DutyType::Exit.is_valid()); + assert!(!DutyType::DutySentinel(Box::new(DutyType::Unknown)).is_valid()); + assert!(!DutyType::DutySentinel(Box::new(DutyType::Attester)).is_valid()); + } + + #[test] + fn test_pub_key_from_bytes() { + let bytes = [42u8; PK_LEN]; + let pk = PubKey::try_from(&bytes[..]).unwrap(); + assert_eq!(pk, PubKey::new(bytes)); + } + + #[test] + fn test_pub_key_from_bytes_invalid_length() { + let bytes = [42u8; PK_LEN + 1]; + let result = PubKey::try_from(&bytes[..]); + assert!(result.is_err()); + } + + #[test] + fn test_pub_key_abbreviated() { + let pk = PubKey::new([42u8; PK_LEN]); + assert_eq!(pk.abbreviated(), "2a2_a2a"); + } + + #[test] + fn test_duty_definition_set() { + let mut duty_definition_set = DutyDefinitionSet::new(); + duty_definition_set.insert(DutyType::Proposer, DutyDefinition::new(DutyType::Proposer)); + assert_eq!( + duty_definition_set.get(&DutyType::Proposer), + Some(&DutyDefinition::new(DutyType::Proposer)) + ); + } + + #[test] + fn test_unsigned_data_set() { + let mut unsigned_data_set = UnsignedDataSet::new(); + unsigned_data_set.insert(DutyType::Proposer, UnsignedData::new(DutyType::Proposer)); + assert_eq!( + unsigned_data_set.get(&DutyType::Proposer), + Some(&UnsignedData::new(DutyType::Proposer)) + ); + } + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + struct MockSignedData; + + impl SignedData for MockSignedData { + type Error = std::io::Error; + + fn signature(&self) -> Signature { + Signature::new([42u8; SIG_LEN]) + } + + fn set_signature(&mut self, _signature: Signature) -> Result<(), std::io::Error> { + Ok(()) + } + + fn message_root(&self) -> [u8; 32] { + [42u8; 32] + } + } + + #[test] + fn test_partially_signed_data_set() { + let mut partially_signed_data_set = ParSignedDataSet::new(); + partially_signed_data_set.insert( + PubKey::new([42u8; PK_LEN]), + ParSignedData::new(MockSignedData, 0), + ); + assert_eq!( + partially_signed_data_set.get(&PubKey::new([42u8; PK_LEN])), + Some(&ParSignedData::new(MockSignedData, 0)) + ); + } + + #[test] + fn test_signed_data_set() { + let mut signed_data_set = SignedDataSet::new(); + signed_data_set.insert(PubKey::new([42u8; PK_LEN]), MockSignedData); + assert_eq!( + signed_data_set.get(&PubKey::new([42u8; PK_LEN])), + Some(&MockSignedData) + ); + } + + #[test] + fn test_pub_key_from_string() { + let pk_str = "0x7f790ba343adf8891fac21a94b02d6ca93d0bc2199a5ec083ff6676e8c2f9f78b08bb122f1093675f9d24c8b5e7af241".to_string(); + let pk = PubKey::try_from(pk_str).unwrap(); + assert_eq!( + pk, + PubKey::new([ + 127, 121, 11, 163, 67, 173, 248, 137, 31, 172, 33, 169, 75, 2, 214, 202, 147, 208, + 188, 33, 153, 165, 236, 8, 63, 246, 103, 110, 140, 47, 159, 120, 176, 139, 177, 34, + 241, 9, 54, 117, 249, 210, 76, 139, 94, 122, 242, 65 + ]) + ); + } + + #[test] + fn test_pub_key_from_string_invalid_length() { + let pk_str = "0x7f790ba343adf8891fac21a94b02d6ca93d0bc2199a5ec083ff6676e8c2f9f78b08bb121093675f9d24c8b5e7af241".to_string(); + let result = PubKey::try_from(pk_str); + assert!(result.is_err()); + } +}