use crate::entities::{ProtocolParameters, ProtocolVersion, SignerWithStake, StakeDistribution};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use super::{PartyId, Stake};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StakeDistributionParty {
pub party_id: PartyId,
pub stake: Stake,
}
impl From<SignerWithStake> for StakeDistributionParty {
fn from(value: SignerWithStake) -> Self {
Self {
party_id: value.party_id,
stake: value.stake,
}
}
}
impl StakeDistributionParty {
pub fn compute_hash(&self) -> String {
let mut hasher = Sha256::new();
hasher.update(self.party_id.as_bytes());
hasher.update(self.stake.to_be_bytes());
hex::encode(hasher.finalize())
}
pub fn from_signers(signers: Vec<SignerWithStake>) -> Vec<Self> {
signers.into_iter().map(|s| s.into()).collect()
}
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct CertificateMetadata {
pub protocol_version: ProtocolVersion,
pub protocol_parameters: ProtocolParameters,
pub initiated_at: DateTime<Utc>,
pub sealed_at: DateTime<Utc>,
pub signers: Vec<StakeDistributionParty>,
}
impl CertificateMetadata {
pub fn new(
protocol_version: ProtocolVersion,
protocol_parameters: ProtocolParameters,
initiated_at: DateTime<Utc>,
sealed_at: DateTime<Utc>,
signers: Vec<StakeDistributionParty>,
) -> CertificateMetadata {
CertificateMetadata {
protocol_version,
protocol_parameters,
initiated_at,
sealed_at,
signers,
}
}
pub fn get_stake_distribution(&self) -> StakeDistribution {
self.signers
.clone()
.iter()
.map(|s| (s.party_id.clone(), s.stake))
.collect()
}
pub fn compute_hash(&self) -> String {
let mut hasher = Sha256::new();
hasher.update(self.protocol_version.as_bytes());
hasher.update(self.protocol_parameters.compute_hash().as_bytes());
hasher.update(
self.initiated_at
.timestamp_nanos_opt()
.unwrap_or_default()
.to_be_bytes(),
);
hasher.update(
self.sealed_at
.timestamp_nanos_opt()
.unwrap_or_default()
.to_be_bytes(),
);
for party in &self.signers {
hasher.update(party.compute_hash().as_bytes());
}
hex::encode(hasher.finalize())
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{Duration, TimeZone, Timelike};
fn get_parties() -> Vec<StakeDistributionParty> {
vec![
StakeDistributionParty {
party_id: "1".to_string(),
stake: 10,
},
StakeDistributionParty {
party_id: "2".to_string(),
stake: 20,
},
]
}
#[test]
fn test_certificate_metadata_compute_hash() {
let hash_expected = "11dd856403cc74ee6c307560b8b2393863299f79656bdabd0c4905aef621c688";
let initiated_at = Utc
.with_ymd_and_hms(2024, 2, 12, 13, 11, 47)
.unwrap()
.with_nanosecond(123043)
.unwrap();
let sealed_at = initiated_at + Duration::seconds(100);
assert_eq!(
hash_expected,
CertificateMetadata::new(
"0.1.0".to_string(),
ProtocolParameters::new(1000, 100, 0.123),
initiated_at,
sealed_at,
get_parties(),
)
.compute_hash()
);
assert_ne!(
hash_expected,
CertificateMetadata::new(
"0.1.0-modified".to_string(),
ProtocolParameters::new(1000, 100, 0.123),
initiated_at,
sealed_at,
get_parties(),
)
.compute_hash()
);
assert_ne!(
hash_expected,
CertificateMetadata::new(
"0.1.0".to_string(),
ProtocolParameters::new(2000, 100, 0.123),
initiated_at,
sealed_at,
get_parties(),
)
.compute_hash()
);
assert_ne!(
hash_expected,
CertificateMetadata::new(
"0.1.0".to_string(),
ProtocolParameters::new(1000, 100, 0.123),
initiated_at - Duration::seconds(78),
sealed_at,
get_parties(),
)
.compute_hash()
);
let mut signers_with_different_party_id = get_parties();
signers_with_different_party_id[0].party_id = "1-modified".to_string();
assert_ne!(
hash_expected,
CertificateMetadata::new(
"0.1.0".to_string(),
ProtocolParameters::new(1000, 100, 0.123),
initiated_at,
sealed_at + Duration::seconds(207),
signers_with_different_party_id,
)
.compute_hash()
);
let mut signers = get_parties();
signers.truncate(1);
assert_ne!(
hash_expected,
CertificateMetadata::new(
"0.1.0".to_string(),
ProtocolParameters::new(1000, 100, 0.123),
initiated_at,
sealed_at,
signers,
)
.compute_hash()
);
}
}