1use chrono::{DateTime, Utc};
2
3use mithril_common::entities::{
4 Certificate, CertificateMetadata, CertificateSignature, Epoch,
5 HexEncodedAggregateVerificationKey, HexEncodedKey, ProtocolMessage, ProtocolParameters,
6 ProtocolVersion, SignedEntityType, StakeDistributionParty,
7};
8use mithril_common::messages::{
9 CertificateListItemMessage, CertificateListItemMessageMetadata, CertificateMessage,
10 CertificateMetadataMessagePart,
11};
12#[cfg(test)]
13use mithril_common::{
14 entities::{CardanoDbBeacon, ImmutableFileNumber},
15 test_utils::{fake_data, fake_keys},
16};
17use mithril_persistence::{
18 database::Hydrator,
19 sqlite::{HydrationError, Projection, SqLiteEntity},
20};
21
22#[derive(Debug, PartialEq, Clone)]
24pub struct CertificateRecord {
25 pub certificate_id: String,
27
28 pub parent_certificate_id: Option<String>,
30
31 pub message: String,
33
34 pub signature: HexEncodedKey,
37
38 pub aggregate_verification_key: HexEncodedAggregateVerificationKey,
41
42 pub epoch: Epoch,
44
45 pub network: String,
47
48 pub signed_entity_type: SignedEntityType,
50
51 pub protocol_version: ProtocolVersion,
53
54 pub protocol_parameters: ProtocolParameters,
56
57 pub protocol_message: ProtocolMessage,
59
60 pub signers: Vec<StakeDistributionParty>,
62
63 pub initiated_at: DateTime<Utc>,
65
66 pub sealed_at: DateTime<Utc>,
68}
69
70#[cfg(test)]
71impl CertificateRecord {
72 pub(crate) fn dummy_genesis(id: &str, epoch: Epoch) -> Self {
73 Self {
74 parent_certificate_id: None,
75 signature: fake_keys::genesis_signature()[0].to_owned(),
76 ..Self::dummy(id, "", epoch, SignedEntityType::genesis(epoch))
77 }
78 }
79
80 pub(crate) fn dummy_db_snapshot(
81 id: &str,
82 parent_id: &str,
83 epoch: Epoch,
84 immutable_file_number: ImmutableFileNumber,
85 ) -> Self {
86 Self::dummy(
87 id,
88 parent_id,
89 epoch,
90 SignedEntityType::CardanoImmutableFilesFull(CardanoDbBeacon::new(
91 *epoch,
92 immutable_file_number,
93 )),
94 )
95 }
96
97 pub(crate) fn dummy(
98 id: &str,
99 parent_id: &str,
100 epoch: Epoch,
101 signed_entity_type: SignedEntityType,
102 ) -> Self {
103 Self {
104 certificate_id: id.to_string(),
105 parent_certificate_id: Some(parent_id.to_string()),
106 message: "message".to_string(),
107 signature: fake_keys::multi_signature()[0].to_owned(),
108 aggregate_verification_key: fake_keys::aggregate_verification_key()[0].to_owned(),
109 epoch,
110 network: fake_data::network().to_string(),
111 signed_entity_type,
112 protocol_version: "protocol_version".to_string(),
113 protocol_parameters: ProtocolParameters {
114 k: 0,
115 m: 0,
116 phi_f: 0.0,
117 },
118 protocol_message: Default::default(),
119 signers: vec![],
120 initiated_at: DateTime::parse_from_rfc3339("2024-02-12T13:11:47Z")
121 .unwrap()
122 .with_timezone(&Utc),
123 sealed_at: DateTime::parse_from_rfc3339("2024-02-12T13:12:57Z")
124 .unwrap()
125 .with_timezone(&Utc),
126 }
127 }
128}
129
130impl From<Certificate> for CertificateRecord {
131 fn from(other: Certificate) -> Self {
132 let signed_entity_type = other.signed_entity_type();
133 let (signature, parent_certificate_id) = match other.signature {
134 CertificateSignature::GenesisSignature(signature) => {
135 (signature.to_bytes_hex().unwrap(), None)
136 }
137 CertificateSignature::MultiSignature(_, signature) => {
138 (signature.to_json_hex().unwrap(), Some(other.previous_hash))
139 }
140 };
141
142 CertificateRecord {
143 certificate_id: other.hash,
144 parent_certificate_id,
145 message: other.signed_message,
146 signature,
147 aggregate_verification_key: other.aggregate_verification_key.to_json_hex().unwrap(),
148 epoch: other.epoch,
149 network: other.metadata.network,
150 signed_entity_type,
151 protocol_version: other.metadata.protocol_version,
152 protocol_parameters: other.metadata.protocol_parameters,
153 protocol_message: other.protocol_message,
154 signers: other.metadata.signers,
155 initiated_at: other.metadata.initiated_at,
156 sealed_at: other.metadata.sealed_at,
157 }
158 }
159}
160
161impl From<CertificateRecord> for Certificate {
162 fn from(other: CertificateRecord) -> Self {
163 let certificate_metadata = CertificateMetadata::new(
164 other.network,
165 other.protocol_version,
166 other.protocol_parameters,
167 other.initiated_at,
168 other.sealed_at,
169 other.signers,
170 );
171 let (previous_hash, signature) = match other.parent_certificate_id {
172 None => (
173 String::new(),
174 CertificateSignature::GenesisSignature(other.signature.try_into().unwrap()),
175 ),
176 Some(parent_certificate_id) => (
177 parent_certificate_id,
178 CertificateSignature::MultiSignature(
179 other.signed_entity_type,
180 other.signature.try_into().unwrap(),
181 ),
182 ),
183 };
184
185 Certificate {
186 hash: other.certificate_id,
187 previous_hash,
188 epoch: other.epoch,
189 metadata: certificate_metadata,
190 signed_message: other.protocol_message.compute_hash(),
191 protocol_message: other.protocol_message,
192 aggregate_verification_key: other.aggregate_verification_key.try_into().unwrap(),
193 signature,
194 }
195 }
196}
197
198impl From<CertificateRecord> for CertificateMessage {
199 fn from(value: CertificateRecord) -> Self {
200 let metadata = CertificateMetadataMessagePart {
201 network: value.network,
202 protocol_version: value.protocol_version,
203 protocol_parameters: value.protocol_parameters,
204 initiated_at: value.initiated_at,
205 sealed_at: value.sealed_at,
206 signers: value.signers,
207 };
208 let (multi_signature, genesis_signature) = if value.parent_certificate_id.is_none() {
209 (String::new(), value.signature)
210 } else {
211 (value.signature, String::new())
212 };
213
214 CertificateMessage {
215 hash: value.certificate_id,
216 previous_hash: value.parent_certificate_id.unwrap_or_default(),
217 epoch: value.epoch,
218 signed_entity_type: value.signed_entity_type,
219 metadata,
220 protocol_message: value.protocol_message,
221 signed_message: value.message,
222 aggregate_verification_key: value.aggregate_verification_key,
223 multi_signature,
224 genesis_signature,
225 }
226 }
227}
228
229impl From<CertificateRecord> for CertificateListItemMessage {
230 fn from(value: CertificateRecord) -> Self {
231 let metadata = CertificateListItemMessageMetadata {
232 network: value.network,
233 protocol_version: value.protocol_version,
234 protocol_parameters: value.protocol_parameters,
235 initiated_at: value.initiated_at,
236 sealed_at: value.sealed_at,
237 total_signers: value.signers.len(),
238 };
239
240 CertificateListItemMessage {
241 hash: value.certificate_id,
242 previous_hash: value.parent_certificate_id.unwrap_or_default(),
243 epoch: value.epoch,
244 signed_entity_type: value.signed_entity_type,
245 metadata,
246 protocol_message: value.protocol_message,
247 signed_message: value.message,
248 aggregate_verification_key: value.aggregate_verification_key,
249 }
250 }
251}
252
253impl SqLiteEntity for CertificateRecord {
254 fn hydrate(row: sqlite::Row) -> Result<Self, HydrationError>
255 where
256 Self: Sized,
257 {
258 let certificate_id = row.read::<&str, _>(0).to_string();
259 let parent_certificate_id = row.read::<Option<&str>, _>(1).map(|s| s.to_owned());
260 let message = row.read::<&str, _>(2).to_string();
261 let signature = row.read::<&str, _>(3).to_string();
262 let aggregate_verification_key = row.read::<&str, _>(4).to_string();
263 let epoch_int = row.read::<i64, _>(5);
264 let network = row.read::<&str, _>(6).to_string();
265 let signed_entity_type_id = row.read::<i64, _>(7);
266 let signed_entity_beacon_string = Hydrator::read_signed_entity_beacon_column(&row, 8);
267 let protocol_version = row.read::<&str, _>(9).to_string();
268 let protocol_parameters_string = row.read::<&str, _>(10);
269 let protocol_message_string = row.read::<&str, _>(11);
270 let signers_string = row.read::<&str, _>(12);
271 let initiated_at = row.read::<&str, _>(13);
272 let sealed_at = row.read::<&str, _>(14);
273
274 let certificate_record = Self {
275 certificate_id,
276 parent_certificate_id,
277 message,
278 signature,
279 aggregate_verification_key,
280 epoch: Epoch(epoch_int.try_into().map_err(|e| {
281 HydrationError::InvalidData(format!(
282 "Could not cast i64 ({epoch_int}) to u64. Error: '{e}'"
283 ))
284 })?),
285 network,
286 signed_entity_type: Hydrator::hydrate_signed_entity_type(
287 signed_entity_type_id.try_into().map_err(|e| {
288 HydrationError::InvalidData(format!(
289 "Could not cast i64 ({signed_entity_type_id}) to u64. Error: '{e}'"
290 ))
291 })?,
292 &signed_entity_beacon_string,
293 )?,
294 protocol_version,
295 protocol_parameters: serde_json::from_str(protocol_parameters_string).map_err(
296 |e| {
297 HydrationError::InvalidData(format!(
298 "Could not turn string '{protocol_parameters_string}' to ProtocolParameters. Error: {e}"
299 ))
300 },
301 )?,
302 protocol_message: serde_json::from_str(protocol_message_string).map_err(
303 |e| {
304 HydrationError::InvalidData(format!(
305 "Could not turn string '{protocol_message_string}' to ProtocolMessage. Error: {e}"
306 ))
307 },
308 )?,
309 signers: serde_json::from_str(signers_string).map_err(
310 |e| {
311 HydrationError::InvalidData(format!(
312 "Could not turn string '{signers_string}' to Vec<StakeDistributionParty>. Error: {e}"
313 ))
314 },
315 )?,
316 initiated_at: DateTime::parse_from_rfc3339(initiated_at).map_err(
317 |e| {
318 HydrationError::InvalidData(format!(
319 "Could not turn string '{initiated_at}' to rfc3339 Datetime. Error: {e}"
320 ))
321 },
322 )?.with_timezone(&Utc),
323 sealed_at: DateTime::parse_from_rfc3339(sealed_at).map_err(
324 |e| {
325 HydrationError::InvalidData(format!(
326 "Could not turn string '{sealed_at}' to rfc3339 Datetime. Error: {e}"
327 ))
328 },
329 )?.with_timezone(&Utc),
330 };
331
332 Ok(certificate_record)
333 }
334
335 fn get_projection() -> Projection {
336 let mut projection = Projection::default();
337 projection.add_field("certificate_id", "{:certificate:}.certificate_id", "text");
338 projection.add_field(
339 "parent_certificate_id",
340 "{:certificate:}.parent_certificate_id",
341 "text",
342 );
343 projection.add_field("message", "{:certificate:}.message", "text");
344 projection.add_field("signature", "{:certificate:}.signature", "text");
345 projection.add_field(
346 "aggregate_verification_key",
347 "{:certificate:}.aggregate_verification_key",
348 "text",
349 );
350 projection.add_field("epoch", "{:certificate:}.epoch", "integer");
351 projection.add_field("network", "{:certificate:}.network", "text");
352 projection.add_field(
353 "signed_entity_type_id",
354 "{:certificate:}.signed_entity_type_id",
355 "integer",
356 );
357 projection.add_field(
358 "signed_entity_beacon",
359 "{:certificate:}.signed_entity_beacon",
360 "text",
361 );
362 projection.add_field(
363 "protocol_version",
364 "{:certificate:}.protocol_version",
365 "text",
366 );
367 projection.add_field(
368 "protocol_parameters",
369 "{:certificate:}.protocol_parameters",
370 "text",
371 );
372 projection.add_field(
373 "protocol_message",
374 "{:certificate:}.protocol_message",
375 "text",
376 );
377 projection.add_field("signers", "{:certificate:}.signers", "text");
378 projection.add_field("initiated_at", "{:certificate:}.initiated_at", "text");
379 projection.add_field("sealed_at", "{:certificate:}.sealed_at", "text");
380
381 projection
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use mithril_common::crypto_helper::tests_setup::setup_certificate_chain;
388
389 use super::*;
390
391 #[test]
392 fn test_convert_certificates() {
393 let certificates = setup_certificate_chain(20, 3);
394 let mut certificate_records: Vec<CertificateRecord> = Vec::new();
395 for certificate in certificates.certificates_chained.clone() {
396 certificate_records.push(certificate.into());
397 }
398 let mut certificates_new: Vec<Certificate> = Vec::new();
399 for certificate_record in certificate_records {
400 certificates_new.push(certificate_record.into());
401 }
402 assert_eq!(certificates.certificates_chained, certificates_new);
403 }
404
405 #[test]
406 fn converting_certificate_record_to_certificate_should_not_recompute_hash() {
407 let expected_hash = "my_hash";
408 let record = CertificateRecord::dummy_genesis(expected_hash, Epoch(1));
409 let certificate: Certificate = record.into();
410
411 assert_eq!(expected_hash, &certificate.hash);
412 }
413}