mithril_common/entities/
certificate_metadata.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use sha2::{Digest, Sha256};
4
5use crate::entities::{ProtocolParameters, ProtocolVersion, SignerWithStake, StakeDistribution};
6
7use super::{PartyId, Stake};
8
9/// This represents a stakeholder.
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub struct StakeDistributionParty {
12    /// Party identifier as in the stake distribution
13    pub party_id: PartyId,
14
15    /// Amount of stake owned by the party.
16    pub stake: Stake,
17}
18
19impl From<SignerWithStake> for StakeDistributionParty {
20    fn from(value: SignerWithStake) -> Self {
21        Self {
22            party_id: value.party_id,
23            stake: value.stake,
24        }
25    }
26}
27
28impl StakeDistributionParty {
29    /// As a sub structure of certificate, Party must be hashable.
30    pub fn compute_hash(&self) -> String {
31        let mut hasher = Sha256::new();
32        hasher.update(self.party_id.as_bytes());
33        hasher.update(self.stake.to_be_bytes());
34
35        hex::encode(hasher.finalize())
36    }
37
38    /// Transform a list of signers into a list of `StakeDistributionParty``
39    pub fn from_signers(signers: Vec<SignerWithStake>) -> Vec<Self> {
40        signers.into_iter().map(|s| s.into()).collect()
41    }
42}
43
44/// CertificateMetadata represents the metadata associated to a Certificate
45#[derive(Clone, Debug, PartialEq)]
46pub struct CertificateMetadata {
47    /// Cardano network
48    pub network: String,
49
50    /// Protocol Version (semver)
51    /// Useful to achieve backward compatibility of the certificates (including of the multi signature)
52    /// part of METADATA(p,n)
53    pub protocol_version: ProtocolVersion,
54
55    /// Protocol parameters
56    /// part of METADATA(p,n)
57    pub protocol_parameters: ProtocolParameters,
58
59    /// Date and time when the certificate was initiated
60    /// Represents the time at which the single signatures registration is opened
61    /// part of METADATA(p,n)
62    pub initiated_at: DateTime<Utc>,
63
64    /// Date and time when the certificate was sealed
65    /// Represents the time at which the quorum of single signatures was reached so that they were aggregated into a multi signature
66    /// part of METADATA(p,n)
67    pub sealed_at: DateTime<Utc>,
68
69    /// The list of the active signers with their stakes and verification keys
70    /// part of METADATA(p,n)
71    pub signers: Vec<StakeDistributionParty>,
72}
73
74impl CertificateMetadata {
75    /// CertificateMetadata factory
76    pub fn new<T: Into<String>, U: Into<ProtocolVersion>>(
77        network: T,
78        protocol_version: U,
79        protocol_parameters: ProtocolParameters,
80        initiated_at: DateTime<Utc>,
81        sealed_at: DateTime<Utc>,
82        signers: Vec<StakeDistributionParty>,
83    ) -> CertificateMetadata {
84        CertificateMetadata {
85            network: network.into(),
86            protocol_version: protocol_version.into(),
87            protocol_parameters,
88            initiated_at,
89            sealed_at,
90            signers,
91        }
92    }
93
94    /// Deduce the stake distribution from the metadata [signers][CertificateMetadata::signers]
95    pub fn get_stake_distribution(&self) -> StakeDistribution {
96        self.signers
97            .clone()
98            .iter()
99            .map(|s| (s.party_id.clone(), s.stake))
100            .collect()
101    }
102
103    /// Computes the hash of the certificate metadata
104    pub fn compute_hash(&self) -> String {
105        let mut hasher = Sha256::new();
106        hasher.update(self.network.as_bytes());
107        hasher.update(self.protocol_version.as_bytes());
108        hasher.update(self.protocol_parameters.compute_hash().as_bytes());
109        hasher.update(
110            self.initiated_at
111                .timestamp_nanos_opt()
112                .unwrap_or_default()
113                .to_be_bytes(),
114        );
115        hasher.update(self.sealed_at.timestamp_nanos_opt().unwrap_or_default().to_be_bytes());
116
117        for party in &self.signers {
118            hasher.update(party.compute_hash().as_bytes());
119        }
120
121        hex::encode(hasher.finalize())
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use chrono::{Duration, TimeZone, Timelike};
128
129    use super::*;
130
131    fn get_parties() -> Vec<StakeDistributionParty> {
132        vec![
133            StakeDistributionParty {
134                party_id: "1".to_string(),
135                stake: 10,
136            },
137            StakeDistributionParty {
138                party_id: "2".to_string(),
139                stake: 20,
140            },
141        ]
142    }
143
144    #[test]
145    fn test_certificate_metadata_compute_hash() {
146        let hash_expected = "f16631f048b33746aa0141cf607ee53ddb76308725e6912530cc41cc54834206";
147
148        let initiated_at = Utc
149            .with_ymd_and_hms(2024, 2, 12, 13, 11, 47)
150            .unwrap()
151            .with_nanosecond(123043)
152            .unwrap();
153        let sealed_at = initiated_at + Duration::try_seconds(100).unwrap();
154        let metadata = CertificateMetadata::new(
155            "devnet",
156            "0.1.0",
157            ProtocolParameters::new(1000, 100, 0.123),
158            initiated_at,
159            sealed_at,
160            get_parties(),
161        );
162
163        assert_eq!(hash_expected, metadata.compute_hash());
164
165        assert_ne!(
166            hash_expected,
167            CertificateMetadata {
168                network: "modified".into(),
169                ..metadata.clone()
170            }
171            .compute_hash(),
172        );
173
174        assert_ne!(
175            hash_expected,
176            CertificateMetadata {
177                protocol_version: "0.1.0-modified".to_string(),
178                ..metadata.clone()
179            }
180            .compute_hash(),
181        );
182
183        assert_ne!(
184            hash_expected,
185            CertificateMetadata {
186                protocol_parameters: ProtocolParameters::new(2000, 100, 0.123),
187                ..metadata.clone()
188            }
189            .compute_hash(),
190        );
191
192        assert_ne!(
193            hash_expected,
194            CertificateMetadata {
195                initiated_at: metadata.initiated_at - Duration::try_seconds(78).unwrap(),
196                ..metadata.clone()
197            }
198            .compute_hash()
199        );
200
201        let mut signers_with_different_party_id = get_parties();
202        signers_with_different_party_id[0].party_id = "1-modified".to_string();
203
204        assert_ne!(
205            hash_expected,
206            CertificateMetadata {
207                sealed_at: metadata.sealed_at - Duration::try_seconds(78).unwrap(),
208                ..metadata.clone()
209            }
210            .compute_hash(),
211        );
212
213        let mut signers = get_parties();
214        signers.truncate(1);
215
216        assert_ne!(
217            hash_expected,
218            CertificateMetadata {
219                signers,
220                ..metadata.clone()
221            }
222            .compute_hash(),
223        );
224    }
225}