mithril_common/entities/
certificate_metadata.rs1use 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub struct StakeDistributionParty {
12 pub party_id: PartyId,
14
15 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 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 pub fn from_signers(signers: Vec<SignerWithStake>) -> Vec<Self> {
40 signers.into_iter().map(|s| s.into()).collect()
41 }
42}
43
44#[derive(Clone, Debug, PartialEq)]
46pub struct CertificateMetadata {
47 pub network: String,
49
50 pub protocol_version: ProtocolVersion,
54
55 pub protocol_parameters: ProtocolParameters,
58
59 pub initiated_at: DateTime<Utc>,
63
64 pub sealed_at: DateTime<Utc>,
68
69 pub signers: Vec<StakeDistributionParty>,
72}
73
74impl CertificateMetadata {
75 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 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 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}