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(
116            self.sealed_at
117                .timestamp_nanos_opt()
118                .unwrap_or_default()
119                .to_be_bytes(),
120        );
121
122        for party in &self.signers {
123            hasher.update(party.compute_hash().as_bytes());
124        }
125
126        hex::encode(hasher.finalize())
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use chrono::{Duration, TimeZone, Timelike};
133
134    use super::*;
135
136    fn get_parties() -> Vec<StakeDistributionParty> {
137        vec![
138            StakeDistributionParty {
139                party_id: "1".to_string(),
140                stake: 10,
141            },
142            StakeDistributionParty {
143                party_id: "2".to_string(),
144                stake: 20,
145            },
146        ]
147    }
148
149    #[test]
150    fn test_certificate_metadata_compute_hash() {
151        let hash_expected = "f16631f048b33746aa0141cf607ee53ddb76308725e6912530cc41cc54834206";
152
153        let initiated_at = Utc
154            .with_ymd_and_hms(2024, 2, 12, 13, 11, 47)
155            .unwrap()
156            .with_nanosecond(123043)
157            .unwrap();
158        let sealed_at = initiated_at + Duration::try_seconds(100).unwrap();
159        let metadata = CertificateMetadata::new(
160            "devnet",
161            "0.1.0",
162            ProtocolParameters::new(1000, 100, 0.123),
163            initiated_at,
164            sealed_at,
165            get_parties(),
166        );
167
168        assert_eq!(hash_expected, metadata.compute_hash());
169
170        assert_ne!(
171            hash_expected,
172            CertificateMetadata {
173                network: "modified".into(),
174                ..metadata.clone()
175            }
176            .compute_hash(),
177        );
178
179        assert_ne!(
180            hash_expected,
181            CertificateMetadata {
182                protocol_version: "0.1.0-modified".to_string(),
183                ..metadata.clone()
184            }
185            .compute_hash(),
186        );
187
188        assert_ne!(
189            hash_expected,
190            CertificateMetadata {
191                protocol_parameters: ProtocolParameters::new(2000, 100, 0.123),
192                ..metadata.clone()
193            }
194            .compute_hash(),
195        );
196
197        assert_ne!(
198            hash_expected,
199            CertificateMetadata {
200                initiated_at: metadata.initiated_at - Duration::try_seconds(78).unwrap(),
201                ..metadata.clone()
202            }
203            .compute_hash()
204        );
205
206        let mut signers_with_different_party_id = get_parties();
207        signers_with_different_party_id[0].party_id = "1-modified".to_string();
208
209        assert_ne!(
210            hash_expected,
211            CertificateMetadata {
212                sealed_at: metadata.sealed_at - Duration::try_seconds(78).unwrap(),
213                ..metadata.clone()
214            }
215            .compute_hash(),
216        );
217
218        let mut signers = get_parties();
219        signers.truncate(1);
220
221        assert_ne!(
222            hash_expected,
223            CertificateMetadata {
224                signers,
225                ..metadata.clone()
226            }
227            .compute_hash(),
228        );
229    }
230}