Skip to main content

mithril_common/entities/
signer.rs

1#[cfg(feature = "future_snark")]
2use crate::crypto_helper::{
3    ProtocolSignerVerificationKeyForSnark, ProtocolSignerVerificationKeySignatureForSnark,
4};
5use crate::{
6    crypto_helper::{
7        KesEvolutions, ProtocolOpCert, ProtocolSignerVerificationKeyForConcatenation,
8        ProtocolSignerVerificationKeySignatureForConcatenation,
9    },
10    entities::{PartyId, Stake},
11};
12use std::fmt::{Debug, Formatter};
13
14use serde::{Deserialize, Serialize};
15use sha2::{Digest, Sha256};
16
17/// Signer represents a signing participant in the network
18#[derive(Clone, Eq, Serialize, Deserialize)]
19pub struct Signer {
20    /// The unique identifier of the signer
21    ///
22    /// Used only for testing when SPO pool id is not certified
23    pub party_id: PartyId,
24
25    /// The verification key for the Concatenation proof system
26    #[serde(rename = "verification_key")]
27    pub verification_key_for_concatenation: ProtocolSignerVerificationKeyForConcatenation,
28
29    /// The KES signature over the verification key for Concatenation
30    ///
31    /// None is used only for testing when SPO pool id is not certified
32    #[serde(
33        skip_serializing_if = "Option::is_none",
34        rename = "verification_key_signature"
35    )]
36    pub verification_key_signature_for_concatenation:
37        Option<ProtocolSignerVerificationKeySignatureForConcatenation>,
38
39    /// The operational certificate of stake pool operator attached to the signer node
40    ///
41    /// None is used only for testing when SPO pool id is not certified
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub operational_certificate: Option<ProtocolOpCert>,
44
45    /// The number of evolutions of the KES key since the start KES period of the operational certificate at the time of signature.
46    #[serde(rename = "kes_period", skip_serializing_if = "Option::is_none")]
47    pub kes_evolutions: Option<KesEvolutions>,
48
49    /// The verification key for the SNARK proof system
50    #[cfg(feature = "future_snark")]
51    #[serde(skip_serializing_if = "Option::is_none", default)]
52    pub verification_key_for_snark: Option<ProtocolSignerVerificationKeyForSnark>,
53
54    /// The KES signature over the verification key for SNARK
55    #[cfg(feature = "future_snark")]
56    #[serde(skip_serializing_if = "Option::is_none", default)]
57    pub verification_key_signature_for_snark:
58        Option<ProtocolSignerVerificationKeySignatureForSnark>,
59}
60
61impl PartialEq for Signer {
62    fn eq(&self, other: &Self) -> bool {
63        self.party_id.eq(&other.party_id)
64    }
65}
66
67impl PartialOrd for Signer {
68    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
69        Some(self.cmp(other))
70    }
71}
72
73impl Ord for Signer {
74    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
75        self.party_id.cmp(&other.party_id)
76    }
77}
78
79impl Signer {
80    /// Convert the given values to a vec of signers.
81    pub fn vec_from<T: Into<Signer>>(from: Vec<T>) -> Vec<Self> {
82        from.into_iter().map(|f| f.into()).collect()
83    }
84
85    /// Computes the hash of Signer
86    pub fn compute_hash(&self) -> String {
87        let mut hasher = Sha256::new();
88        hasher.update(self.party_id.as_bytes());
89        hasher.update(
90            self.verification_key_for_concatenation
91                .to_json_hex()
92                .unwrap()
93                .as_bytes(),
94        );
95
96        if let Some(verification_key_signature) = &self.verification_key_signature_for_concatenation
97        {
98            hasher.update(verification_key_signature.to_json_hex().unwrap().as_bytes());
99        }
100        if let Some(operational_certificate) = &self.operational_certificate {
101            hasher.update(operational_certificate.to_json_hex().unwrap().as_bytes());
102        }
103
104        #[cfg(feature = "future_snark")]
105        if let Some(verification_key_for_snark) = &self.verification_key_for_snark {
106            hasher.update(verification_key_for_snark.to_json_hex().unwrap().as_bytes());
107        }
108        #[cfg(feature = "future_snark")]
109        if let Some(verification_key_signature_for_snark) =
110            &self.verification_key_signature_for_snark
111        {
112            hasher.update(verification_key_signature_for_snark.to_json_hex().unwrap().as_bytes());
113        }
114
115        hex::encode(hasher.finalize())
116    }
117}
118
119impl Debug for Signer {
120    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
121        let should_be_exhaustive = f.alternate();
122        let mut debug = f.debug_struct("Signer");
123        debug.field("party_id", &self.party_id);
124
125        match should_be_exhaustive {
126            true => {
127                debug
128                    .field(
129                        "verification_key_for_concatenation",
130                        &format_args!("{:?}", self.verification_key_for_concatenation),
131                    )
132                    .field(
133                        "verification_key_signature_for_concatenation",
134                        &format_args!("{:?}", self.verification_key_signature_for_concatenation),
135                    )
136                    .field(
137                        "operational_certificate",
138                        &format_args!("{:?}", self.operational_certificate),
139                    )
140                    .field("kes_evolutions", &format_args!("{:?}", self.kes_evolutions));
141
142                #[cfg(feature = "future_snark")]
143                {
144                    debug
145                        .field(
146                            "verification_key_for_snark",
147                            &format_args!("{:?}", self.verification_key_for_snark),
148                        )
149                        .field(
150                            "verification_key_signature_for_snark",
151                            &format_args!("{:?}", self.verification_key_signature_for_snark),
152                        );
153                }
154
155                debug.finish()
156            }
157            false => debug.finish_non_exhaustive(),
158        }
159    }
160}
161
162impl From<SignerWithStake> for Signer {
163    fn from(other: SignerWithStake) -> Self {
164        Self {
165            party_id: other.party_id,
166            verification_key_for_concatenation: other.verification_key_for_concatenation,
167            verification_key_signature_for_concatenation: other
168                .verification_key_signature_for_concatenation,
169            operational_certificate: other.operational_certificate,
170            kes_evolutions: other.kes_evolutions,
171            #[cfg(feature = "future_snark")]
172            verification_key_for_snark: other.verification_key_for_snark,
173            #[cfg(feature = "future_snark")]
174            verification_key_signature_for_snark: other.verification_key_signature_for_snark,
175        }
176    }
177}
178
179/// Signer represents a signing party in the network (including its stakes)
180#[derive(Clone, Eq, Serialize, Deserialize)]
181pub struct SignerWithStake {
182    /// The unique identifier of the signer
183    ///
184    /// Used only for testing when SPO pool id is not certified
185    pub party_id: PartyId,
186
187    /// The verification key for the Concatenation proof system
188    #[serde(rename = "verification_key")]
189    pub verification_key_for_concatenation: ProtocolSignerVerificationKeyForConcatenation,
190
191    /// The KES signature over the verification key for Concatenation
192    ///
193    /// None is used only for testing when SPO pool id is not certified
194    #[serde(
195        skip_serializing_if = "Option::is_none",
196        rename = "verification_key_signature"
197    )]
198    pub verification_key_signature_for_concatenation:
199        Option<ProtocolSignerVerificationKeySignatureForConcatenation>,
200
201    /// The operational certificate of stake pool operator attached to the signer node
202    ///
203    /// None is used only for testing when SPO pool id is not certified
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub operational_certificate: Option<ProtocolOpCert>,
206
207    /// The number of evolutions of the KES key since the start KES period of the operational certificate at the time of signature.
208    #[serde(rename = "kes_period", skip_serializing_if = "Option::is_none")]
209    pub kes_evolutions: Option<KesEvolutions>,
210
211    /// The signer stake
212    pub stake: Stake,
213
214    /// The verification key for the SNARK proof system
215    #[cfg(feature = "future_snark")]
216    #[serde(skip_serializing_if = "Option::is_none", default)]
217    pub verification_key_for_snark: Option<ProtocolSignerVerificationKeyForSnark>,
218
219    /// The KES signature over the verification key for SNARK (hex encoded)
220    #[cfg(feature = "future_snark")]
221    #[serde(skip_serializing_if = "Option::is_none", default)]
222    pub verification_key_signature_for_snark:
223        Option<ProtocolSignerVerificationKeySignatureForSnark>,
224}
225
226impl PartialEq for SignerWithStake {
227    fn eq(&self, other: &Self) -> bool {
228        self.party_id.eq(&other.party_id)
229    }
230}
231
232impl PartialOrd for SignerWithStake {
233    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
234        Some(self.cmp(other))
235    }
236}
237
238impl Ord for SignerWithStake {
239    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
240        self.party_id.cmp(&other.party_id)
241    }
242}
243
244impl SignerWithStake {
245    /// Turn a [Signer] into a [SignerWithStake].
246    pub fn from_signer(signer: Signer, stake: Stake) -> Self {
247        Self {
248            party_id: signer.party_id,
249            verification_key_for_concatenation: signer.verification_key_for_concatenation,
250            verification_key_signature_for_concatenation: signer
251                .verification_key_signature_for_concatenation,
252            operational_certificate: signer.operational_certificate,
253            kes_evolutions: signer.kes_evolutions,
254            stake,
255            #[cfg(feature = "future_snark")]
256            verification_key_for_snark: signer.verification_key_for_snark,
257            #[cfg(feature = "future_snark")]
258            verification_key_signature_for_snark: signer.verification_key_signature_for_snark,
259        }
260    }
261
262    /// Remove SNARK-related fields for backward compatibility with older eras.
263    ///
264    /// This clears the SNARK verification key and its KES signature, which is needed
265    /// during eras that do not support SNARK proofs (e.g. Pythagoras) to ensure
266    /// consistency between the signer's initializer and the key registration entries.
267    #[cfg(feature = "future_snark")]
268    pub fn without_snark_fields(mut self) -> Self {
269        self.verification_key_for_snark = None;
270        self.verification_key_signature_for_snark = None;
271        self
272    }
273
274    /// Remove SNARK-related fields from a list of signers with stake for backward compatibility.
275    #[cfg(feature = "future_snark")]
276    pub fn strip_snark_fields(signers: Vec<Self>) -> Vec<Self> {
277        signers.into_iter().map(Self::without_snark_fields).collect()
278    }
279
280    /// Computes the hash of SignerWithStake
281    pub fn compute_hash(&self) -> String {
282        let mut hasher = Sha256::new();
283        hasher.update(self.party_id.as_bytes());
284        hasher.update(
285            self.verification_key_for_concatenation
286                .to_json_hex()
287                .unwrap()
288                .as_bytes(),
289        );
290
291        if let Some(verification_key_signature) = &self.verification_key_signature_for_concatenation
292        {
293            hasher.update(verification_key_signature.to_json_hex().unwrap().as_bytes());
294        }
295        if let Some(operational_certificate) = &self.operational_certificate {
296            hasher.update(operational_certificate.to_json_hex().unwrap().as_bytes());
297        }
298        hasher.update(self.stake.to_be_bytes());
299
300        #[cfg(feature = "future_snark")]
301        if let Some(verification_key_for_snark) = &self.verification_key_for_snark {
302            hasher.update(verification_key_for_snark.to_json_hex().unwrap().as_bytes());
303        }
304        #[cfg(feature = "future_snark")]
305        if let Some(verification_key_signature_for_snark) =
306            &self.verification_key_signature_for_snark
307        {
308            hasher.update(verification_key_signature_for_snark.to_json_hex().unwrap().as_bytes());
309        }
310
311        hex::encode(hasher.finalize())
312    }
313}
314
315impl Debug for SignerWithStake {
316    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
317        let should_be_exhaustive = f.alternate();
318        let mut debug = f.debug_struct("SignerWithStake");
319        debug.field("party_id", &self.party_id).field("stake", &self.stake);
320
321        match should_be_exhaustive {
322            true => {
323                debug
324                    .field(
325                        "verification_key_for_concatenation",
326                        &format_args!("{:?}", self.verification_key_for_concatenation),
327                    )
328                    .field(
329                        "verification_key_signature_for_concatenation",
330                        &format_args!("{:?}", self.verification_key_signature_for_concatenation),
331                    )
332                    .field(
333                        "operational_certificate",
334                        &format_args!("{:?}", self.operational_certificate),
335                    )
336                    .field("kes_evolutions", &format_args!("{:?}", self.kes_evolutions));
337
338                #[cfg(feature = "future_snark")]
339                {
340                    debug
341                        .field(
342                            "verification_key_for_snark",
343                            &format_args!("{:?}", self.verification_key_for_snark),
344                        )
345                        .field(
346                            "verification_key_signature_for_snark",
347                            &format_args!("{:?}", self.verification_key_signature_for_snark),
348                        );
349                }
350
351                debug.finish()
352            }
353            false => debug.finish_non_exhaustive(),
354        }
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use crate::test::{builder::MithrilFixtureBuilder, double::fake_keys};
361
362    use super::*;
363
364    #[test]
365    fn test_stake_signers_from_into() {
366        let verification_key = MithrilFixtureBuilder::default()
367            .with_signers(1)
368            .build()
369            .signers_with_stake()[0]
370            .verification_key_for_concatenation;
371        let signer_expected = Signer {
372            party_id: "1".to_string(),
373            verification_key_for_concatenation: verification_key,
374            verification_key_signature_for_concatenation: None,
375            operational_certificate: None,
376            kes_evolutions: None,
377            #[cfg(feature = "future_snark")]
378            verification_key_for_snark: None,
379            #[cfg(feature = "future_snark")]
380            verification_key_signature_for_snark: None,
381        };
382        let signer_with_stake = SignerWithStake {
383            party_id: "1".to_string(),
384            verification_key_for_concatenation: verification_key,
385            verification_key_signature_for_concatenation: None,
386            operational_certificate: None,
387            kes_evolutions: None,
388            stake: 100,
389            #[cfg(feature = "future_snark")]
390            verification_key_for_snark: None,
391            #[cfg(feature = "future_snark")]
392            verification_key_signature_for_snark: None,
393        };
394
395        let signer_into: Signer = signer_with_stake.into();
396        assert_eq!(signer_expected, signer_into);
397    }
398
399    #[test]
400    fn test_signer_compute_hash() {
401        const HASH_EXPECTED: &str =
402            "02778791113dcd8647b019366e223bfe3aa8a054fa6d9d1918b6b669de485f1c";
403
404        let build_signer = |party_id: &str, key_index: usize| Signer {
405            party_id: party_id.to_string(),
406            verification_key_for_concatenation: fake_keys::signer_verification_key()[key_index]
407                .try_into()
408                .unwrap(),
409            verification_key_signature_for_concatenation: None,
410            operational_certificate: None,
411            kes_evolutions: None,
412            #[cfg(feature = "future_snark")]
413            verification_key_for_snark: None,
414            #[cfg(feature = "future_snark")]
415            verification_key_signature_for_snark: None,
416        };
417
418        assert_eq!(HASH_EXPECTED, build_signer("1", 3).compute_hash());
419        assert_ne!(HASH_EXPECTED, build_signer("0", 3).compute_hash());
420        assert_ne!(HASH_EXPECTED, build_signer("1", 0).compute_hash());
421    }
422
423    #[test]
424    fn test_signer_with_stake_compute_hash() {
425        #[cfg(not(feature = "future_snark"))]
426        const EXPECTED_HASH: &str =
427            "9a832baccd04aabfc419f57319e3831a1655a95bf3bf5ed96a1167d1e81b5085";
428        #[cfg(feature = "future_snark")]
429        const EXPECTED_HASH: &str =
430            "6158c4f514b1e15dc745845dac9014e710ee6b2f0c5b2b1023d5207cf6b75db9";
431        let signers = MithrilFixtureBuilder::default()
432            .with_signers(2)
433            .build()
434            .signers_with_stake();
435        let signer = signers[0].clone();
436
437        assert_eq!(EXPECTED_HASH, signer.compute_hash());
438
439        {
440            let mut signer_different_party_id = signer.clone();
441            signer_different_party_id.party_id = "whatever".to_string();
442
443            assert_ne!(EXPECTED_HASH, signer_different_party_id.compute_hash());
444        }
445        {
446            let mut signer_different_verification_key = signer.clone();
447            signer_different_verification_key.verification_key_for_concatenation =
448                signers[1].verification_key_for_concatenation;
449
450            assert_ne!(
451                EXPECTED_HASH,
452                signer_different_verification_key.compute_hash()
453            );
454        }
455        {
456            let mut signer_different_stake = signer.clone();
457            signer_different_stake.stake += 20;
458
459            assert_ne!(EXPECTED_HASH, signer_different_stake.compute_hash());
460        }
461
462        #[cfg(feature = "future_snark")]
463        {
464            let mut signer_different_verification_key_for_snark = signer.clone();
465            signer_different_verification_key_for_snark.verification_key_for_snark =
466                signers[1].verification_key_for_snark;
467
468            assert_ne!(
469                EXPECTED_HASH,
470                signer_different_verification_key_for_snark.compute_hash()
471            );
472        }
473
474        #[cfg(feature = "future_snark")]
475        {
476            let mut signer_different_verification_key_signature_for_snark = signer.clone();
477            signer_different_verification_key_signature_for_snark
478                .verification_key_signature_for_snark =
479                signers[1].verification_key_signature_for_snark;
480
481            assert_ne!(
482                EXPECTED_HASH,
483                signer_different_verification_key_signature_for_snark.compute_hash()
484            );
485        }
486    }
487
488    #[cfg(feature = "future_snark")]
489    mod strip_snark_fields {
490        use super::*;
491
492        #[test]
493        fn snark_fields_are_cleared_by_without_snark_fields() {
494            let signers = MithrilFixtureBuilder::default()
495                .with_signers(1)
496                .build()
497                .signers_with_stake();
498            let signer = signers[0].clone();
499            assert!(signer.verification_key_for_snark.is_some());
500            assert!(signer.verification_key_signature_for_snark.is_some());
501
502            let stripped = signer.without_snark_fields();
503
504            assert!(stripped.verification_key_for_snark.is_none());
505            assert!(stripped.verification_key_signature_for_snark.is_none());
506        }
507
508        #[test]
509        fn without_snark_fields_preserves_non_snark_data() {
510            let signers = MithrilFixtureBuilder::default()
511                .with_signers(1)
512                .build()
513                .signers_with_stake();
514            let signer = signers[0].clone();
515
516            let stripped = signer.clone().without_snark_fields();
517
518            assert_eq!(signer.party_id, stripped.party_id);
519            assert_eq!(
520                signer.verification_key_for_concatenation,
521                stripped.verification_key_for_concatenation
522            );
523            assert_eq!(signer.stake, stripped.stake);
524        }
525
526        #[test]
527        fn without_snark_fields_preserves_none_values() {
528            let signers = MithrilFixtureBuilder::default()
529                .with_signers(1)
530                .build()
531                .signers_with_stake();
532            let mut signer = signers[0].clone();
533            signer.verification_key_for_snark = None;
534            signer.verification_key_signature_for_snark = None;
535
536            let stripped = signer.without_snark_fields();
537
538            assert!(stripped.verification_key_for_snark.is_none());
539            assert!(stripped.verification_key_signature_for_snark.is_none());
540        }
541
542        #[test]
543        fn strip_snark_fields_clears_all_entries() {
544            let signers = MithrilFixtureBuilder::default()
545                .with_signers(3)
546                .build()
547                .signers_with_stake();
548            assert!(signers.iter().all(|s| s.verification_key_for_snark.is_some()));
549
550            let stripped = SignerWithStake::strip_snark_fields(signers);
551
552            assert!(stripped.iter().all(|s| s.verification_key_for_snark.is_none()));
553            assert!(
554                stripped
555                    .iter()
556                    .all(|s| s.verification_key_signature_for_snark.is_none())
557            );
558        }
559
560        #[test]
561        fn strip_snark_fields_handles_empty_list() {
562            let stripped = SignerWithStake::strip_snark_fields(vec![]);
563            assert!(stripped.is_empty());
564        }
565    }
566}