use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::entities::{ProtocolParameters, ProtocolVersion, SignerWithStake, StakeDistribution};
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)]
pub struct CertificateMetadata {
pub network: String,
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<T: Into<String>, U: Into<ProtocolVersion>>(
network: T,
protocol_version: U,
protocol_parameters: ProtocolParameters,
initiated_at: DateTime<Utc>,
sealed_at: DateTime<Utc>,
signers: Vec<StakeDistributionParty>,
) -> CertificateMetadata {
CertificateMetadata {
network: network.into(),
protocol_version: protocol_version.into(),
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.network.as_bytes());
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 chrono::{Duration, TimeZone, Timelike};
use super::*;
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 = "f16631f048b33746aa0141cf607ee53ddb76308725e6912530cc41cc54834206";
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::try_seconds(100).unwrap();
let metadata = CertificateMetadata::new(
"devnet",
"0.1.0",
ProtocolParameters::new(1000, 100, 0.123),
initiated_at,
sealed_at,
get_parties(),
);
assert_eq!(hash_expected, metadata.compute_hash());
assert_ne!(
hash_expected,
CertificateMetadata {
network: "modified".into(),
..metadata.clone()
}
.compute_hash(),
);
assert_ne!(
hash_expected,
CertificateMetadata {
protocol_version: "0.1.0-modified".to_string(),
..metadata.clone()
}
.compute_hash(),
);
assert_ne!(
hash_expected,
CertificateMetadata {
protocol_parameters: ProtocolParameters::new(2000, 100, 0.123),
..metadata.clone()
}
.compute_hash(),
);
assert_ne!(
hash_expected,
CertificateMetadata {
initiated_at: metadata.initiated_at - Duration::try_seconds(78).unwrap(),
..metadata.clone()
}
.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 {
sealed_at: metadata.sealed_at - Duration::try_seconds(78).unwrap(),
..metadata.clone()
}
.compute_hash(),
);
let mut signers = get_parties();
signers.truncate(1);
assert_ne!(
hash_expected,
CertificateMetadata {
signers,
..metadata.clone()
}
.compute_hash(),
);
}
}