Skip to main content

filecoin_f3_certs/
lib.rs

1// Copyright 2019-2024 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4mod error;
5
6pub use crate::error::CertsError;
7use crate::error::CertsError::UnsortedDiff;
8/// The `certs` package provides functionality for handling finality certificates in the GPBFT consensus protocol.
9///
10/// This package includes:
11/// - Structures and types for representing finality certificates and power table changes
12/// - Functions for creating and applying power table diffs
13/// - Methods for verifying finality certificates
14///
15/// Key components:
16/// - `FinalityCertificate`: Represents a single finalized GPBFT instance
17/// - `PowerTableDelta`: Represents a change in power for a single participant
18/// - `PowerTableDiff`: A collection of power table deltas
19///
20/// The package allows for:
21/// - Creating new finality certificates from justifications
22/// - Applying power table diffs to update the network's power distribution
23/// - Verifying the signatures of finality certificates
24///
25/// Note: The signature verification only checks that the certificate's value has been signed by a majority
26/// of the power. It does not validate the power delta or other parts of the certificate.
27use ahash::HashMap;
28use filecoin_f3_gpbft::api::Verifier;
29pub use filecoin_f3_gpbft::chain::Tipset;
30use filecoin_f3_gpbft::{
31    ActorId, BitField, ECChain, Justification, NetworkName, Payload, Phase, PowerEntries,
32    PowerEntry, PubKey, Sign, StoragePower, SupplementalData, Zero, cid_from_bytes,
33    is_strong_quorum,
34};
35use std::ops::Neg;
36
37pub type Result<T> = std::result::Result<T, error::CertsError>;
38
39/// `PowerTableDelta` represents a single power table change between GPBFT instances. If the resulting
40/// power is 0 after applying the delta, the participant is removed from the power table.
41#[derive(Debug, Clone)]
42pub struct PowerTableDelta {
43    /// Participant with changed power
44    pub participant_id: ActorId,
45    /// Change in power from base (signed)
46    pub power_delta: StoragePower,
47    /// New signing key if relevant (else empty)
48    pub signing_key: PubKey,
49}
50
51impl PowerTableDelta {
52    /// Checks if the power delta is zero (no change)
53    ///
54    /// Returns true if both the `power_delta` is zero and the `signing_key` is empty,
55    /// indicating no change for this participant.
56    pub fn is_zero(&self) -> bool {
57        self.power_delta.is_zero() && self.signing_key.is_empty()
58    }
59}
60
61/// Represents a set of changes to the power table
62///
63/// `PowerTableDiff` is a collection of individual power table deltas, each representing
64/// a change in power or signing key for a specific participant in the network.
65/// It is used to track and apply changes to the power table between GPBFT instances.
66pub type PowerTableDiff = Vec<PowerTableDelta>;
67
68/// Represents a single finalized GPBFT instance
69#[derive(Default, Debug, Clone)]
70pub struct FinalityCertificate {
71    /// The GPBFT instance to which this finality certificate corresponds
72    pub gpbft_instance: u64,
73    /// The ECChain finalized during this instance, starting with the last tipset finalized in
74    /// the previous instance
75    pub ec_chain: ECChain,
76    /// Additional data signed by the participants in this instance. Currently used to certify
77    /// the power table used in the next instance
78    pub supplemental_data: SupplementalData,
79    /// Indexes in the base power table of the `certifiers` (`bitset`)
80    pub signers: BitField,
81    /// Aggregated signature of the `certifiers`
82    pub signature: Vec<u8>,
83    /// Changes between the power table used to validate this finality certificate and the power
84    /// used to validate the next finality certificate. Sorted by `ParticipantID`, ascending
85    pub power_table_delta: PowerTableDiff,
86}
87
88impl FinalityCertificate {
89    /// Creates a new `FinalityCertificate` from a `PowerTableDiff` and a Justification
90    ///
91    /// # Arguments
92    /// * `power_delta` - The changes in the power table
93    /// * `justification` - The justification for the decision
94    ///
95    /// # Returns
96    /// A Result containing the new `FinalityCertificate` if successful
97    pub fn new(power_delta: PowerTableDiff, justification: &Justification) -> Result<Self> {
98        if justification.vote.phase != Phase::Decide {
99            return Err(CertsError::InvalidJustificationVotePhase {
100                expected: Phase::Decide,
101                actual: justification.vote.phase,
102            });
103        }
104
105        if justification.vote.round != 0 {
106            return Err(CertsError::InvalidRound {
107                expected: 0,
108                actual: justification.vote.round,
109            });
110        }
111
112        if justification.vote.value.is_empty() {
113            return Err(CertsError::BottomDecision(justification.vote.instance));
114        }
115
116        Ok(FinalityCertificate {
117            gpbft_instance: justification.vote.instance,
118            supplemental_data: justification.vote.supplemental_data.clone(),
119            ec_chain: justification.vote.value.clone(),
120            signers: justification.signers.clone(),
121            signature: justification.signature.clone(),
122            power_table_delta: power_delta,
123        })
124    }
125}
126
127/// Apply a set of power table diffs to the passed power table.
128///
129/// - The delta must be sorted by participant ID, ascending.
130/// - The returned power table is sorted by power, descending.
131pub fn apply_power_table_diffs(
132    prev_power_table: &PowerEntries,
133    diffs: &[&PowerTableDiff],
134) -> Result<PowerEntries> {
135    let mut power_table_map: HashMap<ActorId, PowerEntry> = prev_power_table
136        .iter()
137        .map(|pe| (pe.id, pe.clone()))
138        .collect();
139
140    for (j, diff) in diffs.iter().enumerate() {
141        let mut last_actor_id = 0;
142        for (i, d) in diff.iter().enumerate() {
143            // We assert this to make sure the finality certificate has a consistent power-table
144            // diff.
145            if i > 0 && d.participant_id <= last_actor_id {
146                return Err(UnsortedDiff(j));
147            }
148
149            // Empty power diffs aren't allowed.
150            if d.is_zero() {
151                return Err(CertsError::EmptyDelta(j, d.participant_id));
152            }
153
154            last_actor_id = d.participant_id;
155
156            if !power_table_map.contains_key(&d.participant_id) {
157                // New power entries must specify positive power.
158                if d.power_delta <= StoragePower::from(0) {
159                    return Err(CertsError::NonPositivePowerDeltaForNewEntry(
160                        j,
161                        d.participant_id,
162                    ));
163                }
164            }
165
166            let pe = power_table_map
167                .entry(d.participant_id)
168                .or_insert_with(|| PowerEntry {
169                    id: d.participant_id,
170                    power: StoragePower::from(0),
171                    pub_key: PubKey::default(),
172                });
173
174            // Power deltas can't replace a key with the same key.
175            // This also implicitly checks the key for emptiness on a new entry, because that is the
176            // default.
177            if pe.pub_key == d.signing_key {
178                return Err(CertsError::UnchangedKey(j, pe.id));
179            }
180
181            if !d.power_delta.is_zero() {
182                pe.power += &d.power_delta;
183            }
184
185            if !d.signing_key.is_empty() {
186                // If we end up with no power, we shouldn't replace the key.
187                // This condition will never be true for a new entry.
188                if pe.power.is_zero() {
189                    return Err(CertsError::RemovesAllPowerWithNewKey(j, pe.id));
190                }
191                pe.pub_key = d.signing_key.clone();
192            }
193
194            match pe.power.sign() {
195                Sign::Minus => return Err(CertsError::NegativePower(j, pe.id)),
196                Sign::NoSign => {
197                    power_table_map.remove(&d.participant_id);
198                }
199                Sign::Plus => {
200                    // Already inserted, nothing to do.
201                }
202            }
203        }
204    }
205
206    let mut new_power_table: PowerEntries = power_table_map.into_values().collect();
207    new_power_table.sort();
208    Ok(new_power_table)
209}
210
211/// Create a power table diff between the two given power tables. It makes no assumptions about
212/// order, but does assume that the power table entries are unique. The returned diff is sorted by
213/// participant ID ascending.
214pub fn make_power_table_diff(
215    old_power_table: &PowerEntries,
216    new_power_table: &PowerEntries,
217) -> PowerTableDiff {
218    let mut old_power_map: HashMap<ActorId, &PowerEntry> =
219        old_power_table.iter().map(|e| (e.id, e)).collect();
220
221    let mut diff = PowerTableDiff::new();
222
223    for new_entry in new_power_table.iter() {
224        let mut delta = PowerTableDelta {
225            participant_id: new_entry.id,
226            power_delta: StoragePower::from(0),
227            signing_key: PubKey::default(),
228        };
229
230        let delta = match old_power_map.remove(&new_entry.id) {
231            Some(old_entry) => {
232                delta.power_delta = &new_entry.power - &old_entry.power;
233                if new_entry.pub_key != old_entry.pub_key {
234                    delta.signing_key = new_entry.pub_key.clone();
235                }
236                delta
237            }
238            None => {
239                delta.power_delta = new_entry.power.clone();
240                delta.signing_key = new_entry.pub_key.clone();
241                delta
242            }
243        };
244
245        if !delta.is_zero() {
246            diff.push(delta);
247        }
248    }
249
250    for old_entry in old_power_map.values() {
251        diff.push(PowerTableDelta {
252            participant_id: old_entry.id,
253            power_delta: old_entry.power.clone().neg(),
254            signing_key: PubKey::default(),
255        });
256    }
257
258    diff.sort_by_key(|delta| delta.participant_id);
259    diff
260}
261
262/// Verifies the signature of a finality certificate
263fn verify_signature(
264    verifier: &impl Verifier,
265    network: NetworkName,
266    cert: &FinalityCertificate,
267    power_table: &PowerEntries,
268) -> Result<()> {
269    let (scaled_powers, scaled_total) = power_table
270        .scaled()
271        .map_err(|e| CertsError::SerializationError(e.to_string().into()))?;
272
273    let signers: Vec<u64> = cert.signers.iter().collect();
274    let signer_scaled_total =
275        signer_scaled_total(cert.gpbft_instance, power_table, &scaled_powers, &signers)?;
276    if !is_strong_quorum(signer_scaled_total, scaled_total) {
277        return Err(CertsError::InsufficientPower {
278            instance: cert.gpbft_instance,
279            signer_power: signer_scaled_total,
280            total_power: scaled_total,
281        });
282    }
283
284    // Construct the payload for signature verification
285    let payload = Payload {
286        instance: cert.gpbft_instance,
287        round: 0,
288        phase: Phase::Decide,
289        supplemental_data: cert.supplemental_data.clone(),
290        value: cert.ec_chain.clone(),
291    };
292
293    // Encode the payload for signing
294    let payload_bytes = payload.serialize_for_signing(&network.to_string());
295
296    // Extract public keys for signers
297    let signers_pk: Vec<PubKey> = signers
298        .iter()
299        .map(|&index| power_table[index as usize].pub_key.clone())
300        .collect();
301
302    // Verify the aggregate signature
303    let res = verifier
304        .verify_aggregate(&payload_bytes, &cert.signature, &signers_pk)
305        .map_err(|e| CertsError::SignatureVerificationFailed {
306            instance: cert.gpbft_instance,
307            error: e.to_string(),
308        });
309
310    // Temporarily silencing verification errors
311    // The current BDN implementation uses standard BLS aggregation, causing verification to fail.
312    // This logging allows development to continue.
313    // TODO: Remove this workaround once BDN aggregation scheme is implemented
314    if let Err(err) = res {
315        println!("WARN: {}", err);
316    }
317
318    Ok(())
319}
320
321/// Validates a sequence of finality certificates.
322///
323/// This function checks the validity of a series of finality certificates, ensuring they form a
324/// consistent chain and that the power table changes are correctly applied and verified.
325///
326/// # Arguments
327/// * `verifier` - The signature verifier implementation
328/// * `network` - The network name
329/// * `prev_power_table` - The initial power table
330/// * `next_instance` - The expected next instance number
331/// * `base` - The optional base tipset
332/// * `certs` - The sequence of finality certificates to validate
333///
334/// # Returns
335/// A Result containing a tuple of:
336/// * The next instance number
337/// * The validated ECChain
338/// * The final power table after applying all changes
339///
340/// # Errors
341/// Returns a `CertsError` if any validation step fails, including instance mismatches,
342/// invalid certificates, power table inconsistencies, or serialization errors.
343///
344pub fn validate_finality_certificates<'a>(
345    verifier: &impl Verifier,
346    network: NetworkName,
347    prev_power_table: PowerEntries,
348    mut next_instance: u64,
349    mut base: Option<&'a Tipset>,
350    certs: &'a [FinalityCertificate],
351) -> Result<(u64, ECChain, PowerEntries)> {
352    let mut chain: Option<ECChain> = None;
353    let mut current_power_table = prev_power_table;
354
355    for cert in certs {
356        if cert.gpbft_instance != next_instance {
357            return Err(CertsError::InstanceMismatch {
358                expected: next_instance,
359                actual: cert.gpbft_instance,
360            });
361        }
362
363        // Basic sanity checks
364        if let Err(e) = cert.ec_chain.validate() {
365            return Err(CertsError::InvalidFinalityCertificate(
366                cert.gpbft_instance,
367                e,
368            ));
369        }
370
371        if cert.ec_chain.is_empty() {
372            return Err(CertsError::EmptyFinalityCertificate(cert.gpbft_instance));
373        }
374
375        // Validate base tipset if specified
376        if base.is_some() && base != cert.ec_chain.base() {
377            return Err(CertsError::BaseTipsetMismatch(cert.gpbft_instance));
378        }
379
380        // Verify the certificate signature
381        verify_signature(verifier, network, cert, &current_power_table)?;
382
383        // Compute new power table and validate
384        let new_power_table =
385            apply_power_table_diffs(&current_power_table, &[&cert.power_table_delta])?;
386
387        let bytes = new_power_table.serialize_cbor();
388        let power_table_cid = cid_from_bytes(&bytes);
389
390        if cert.supplemental_data.power_table != power_table_cid {
391            return Err(CertsError::IncorrectPowerDiff {
392                instance: cert.gpbft_instance,
393                expected: power_table_cid.into(),
394                actual: cert.supplemental_data.power_table.into(),
395            });
396        }
397
398        next_instance += 1;
399        chain = match (chain, cert.ec_chain.has_suffix()) {
400            (None, _) => {
401                // First certificate - use its full chain as starting point
402                Some(ECChain::new_unvalidated(cert.ec_chain.to_vec()))
403            }
404            (Some(existing), false) => {
405                // No new tipsets, keep existing chain
406                Some(existing)
407            }
408            (Some(existing), true) => {
409                // Extend existing chain with new suffix tipsets
410                existing.extend(
411                    &cert
412                        .ec_chain
413                        .suffix()
414                        .iter()
415                        .map(|ts| ts.key.clone())
416                        .collect::<Vec<_>>(),
417                )
418            }
419        };
420
421        current_power_table = new_power_table;
422        base = cert.ec_chain.head();
423    }
424
425    Ok((
426        next_instance,
427        chain.ok_or(CertsError::EmptyChain)?,
428        current_power_table,
429    ))
430}
431
432/// Calculate total signers power.
433fn signer_scaled_total(
434    instance: u64,
435    power_table: &PowerEntries,
436    scaled_power: &[i64],
437    signer_indices: &[u64],
438) -> Result<i64> {
439    let mut total_signer_power = 0;
440
441    for &index in signer_indices {
442        let idx = index as usize;
443
444        // Validate signer index is in bounds
445        if idx >= power_table.len() {
446            return Err(CertsError::SignerIndexOutOfBounds {
447                instance,
448                signer_index: idx,
449                power_table_size: power_table.len(),
450            });
451        }
452
453        let scaled_power = scaled_power[idx];
454
455        // Check that signer has non-zero effective power
456        if scaled_power == 0 {
457            return Err(CertsError::ZeroEffectivePower {
458                instance,
459                signer: power_table[idx].id,
460            });
461        }
462
463        total_signer_power += scaled_power;
464    }
465
466    Ok(total_signer_power)
467}
468
469#[cfg(test)]
470mod tests {
471    use super::*;
472    use filecoin_f3_gpbft::chain::Tipset;
473    use filecoin_f3_gpbft::test_utils::powertable_cid;
474    use filecoin_f3_gpbft::{Cid, Payload};
475    use std::str::FromStr;
476
477    fn create_mock_justification(phase: Phase, cid: &str) -> Justification {
478        let base_tipset = Tipset {
479            epoch: 1,
480            key: vec![1, 2, 3],
481            power_table: powertable_cid(),
482            commitments: keccak_hash::H256::zero(),
483        };
484
485        let additional_tipset = Tipset {
486            epoch: 2,
487            key: vec![4, 5, 6],
488            power_table: powertable_cid(),
489            commitments: keccak_hash::H256::zero(),
490        };
491
492        let ec_chain = ECChain::new(base_tipset, vec![additional_tipset]).unwrap();
493
494        // Create signers bitfield with participants 1 and 2 (from our test power table)
495        let mut signers = BitField::new();
496        signers.set(0);
497        signers.set(1);
498
499        Justification {
500            vote: Payload {
501                instance: 1,
502                round: 0,
503                phase,
504                supplemental_data: SupplementalData {
505                    commitments: keccak_hash::H256::zero(),
506                    power_table: Cid::from_str(cid).unwrap(),
507                },
508                value: ec_chain,
509            },
510            signers,
511            signature: vec![7, 8, 9],
512        }
513    }
514
515    #[test]
516    fn test_power_table_delta_is_zero() {
517        let zero_delta = PowerTableDelta {
518            participant_id: 1,
519            power_delta: StoragePower::from(0),
520            signing_key: PubKey::default(),
521        };
522        assert!(zero_delta.is_zero());
523
524        let non_zero_power_delta = PowerTableDelta {
525            participant_id: 1,
526            power_delta: StoragePower::from(100),
527            signing_key: PubKey::default(),
528        };
529        assert!(!non_zero_power_delta.is_zero());
530
531        let non_zero_key_delta = PowerTableDelta {
532            participant_id: 1,
533            power_delta: StoragePower::from(0),
534            signing_key: PubKey::new(vec![1, 2, 3]),
535        };
536        assert!(!non_zero_key_delta.is_zero());
537    }
538
539    #[test]
540    fn test_finality_certificate_new_success() {
541        let power_delta = PowerTableDiff::new();
542        let justification = create_mock_justification(Phase::Decide, &powertable_cid().to_string());
543
544        let result = FinalityCertificate::new(power_delta, &justification);
545        assert!(result.is_ok());
546
547        let cert = result.unwrap();
548        assert_eq!(cert.gpbft_instance, justification.vote.instance);
549        assert_eq!(cert.ec_chain, justification.vote.value);
550        assert_eq!(cert.supplemental_data, justification.vote.supplemental_data);
551        assert_eq!(cert.signers, justification.signers);
552        assert_eq!(cert.signature, justification.signature);
553    }
554
555    #[test]
556    fn test_finality_certificate_new_wrong_phase() {
557        let power_delta = PowerTableDiff::new();
558        let justification = create_mock_justification(Phase::Commit, &powertable_cid().to_string());
559
560        let result = FinalityCertificate::new(power_delta, &justification);
561        assert!(result.is_err());
562        assert_eq!(
563            result.unwrap_err(),
564            CertsError::InvalidJustificationVotePhase {
565                expected: Phase::Decide,
566                actual: Phase::Commit
567            }
568        );
569    }
570
571    #[test]
572    fn test_finality_certificate_new_wrong_round() {
573        let power_delta = PowerTableDiff::new();
574        let mut justification =
575            create_mock_justification(Phase::Decide, &powertable_cid().to_string());
576        justification.vote.round = 1;
577
578        let result = FinalityCertificate::new(power_delta, &justification);
579        assert!(result.is_err());
580        assert_eq!(
581            result.unwrap_err(),
582            CertsError::InvalidRound {
583                expected: 0,
584                actual: 1
585            }
586        );
587    }
588
589    // It makes no sense that ECChain can be empty. Perhaps this warrants a discussion.
590    #[test]
591    fn test_finality_certificate_new_empty_value() {
592        let power_delta = PowerTableDiff::new();
593        let mut justification =
594            create_mock_justification(Phase::Decide, &powertable_cid().to_string());
595        justification.vote.value = ECChain::new_unvalidated(Vec::new());
596
597        let result = FinalityCertificate::new(power_delta, &justification);
598        assert!(result.is_err());
599        assert_eq!(result.unwrap_err(), CertsError::BottomDecision(1));
600    }
601
602    #[test]
603    fn test_make_power_table_diff() {
604        let old_power_table = vec![
605            PowerEntry {
606                id: 1,
607                power: StoragePower::from(100),
608                pub_key: PubKey::new(vec![1, 2, 3]),
609            },
610            PowerEntry {
611                id: 2,
612                power: StoragePower::from(200),
613                pub_key: PubKey::new(vec![4, 5, 6]),
614            },
615        ];
616        let new_power_table = vec![
617            PowerEntry {
618                id: 1,
619                power: StoragePower::from(150),
620                pub_key: PubKey::new(vec![1, 2, 3]),
621            },
622            PowerEntry {
623                id: 3,
624                power: StoragePower::from(300),
625                pub_key: PubKey::new(vec![7, 8, 9]),
626            },
627        ];
628
629        let diff = make_power_table_diff(
630            &PowerEntries(old_power_table),
631            &PowerEntries(new_power_table),
632        );
633
634        assert_eq!(diff.len(), 3);
635        assert_eq!(diff[0].participant_id, 1);
636        assert_eq!(diff[0].power_delta, StoragePower::from(50));
637        assert!(diff[0].signing_key.is_empty());
638        assert_eq!(diff[1].participant_id, 2);
639        assert_eq!(diff[1].power_delta, StoragePower::from(-200));
640        assert!(diff[1].signing_key.is_empty());
641        assert_eq!(diff[2].participant_id, 3);
642        assert_eq!(diff[2].power_delta, StoragePower::from(300));
643        assert_eq!(diff[2].signing_key, PubKey::new(vec![7, 8, 9]));
644    }
645
646    #[test]
647    fn test_apply_power_table_diffs() {
648        let prev_power_table = vec![
649            PowerEntry {
650                id: 1,
651                power: StoragePower::from(100),
652                pub_key: PubKey::new(vec![1, 2, 3]),
653            },
654            PowerEntry {
655                id: 2,
656                power: StoragePower::from(200),
657                pub_key: PubKey::new(vec![4, 5, 6]),
658            },
659        ];
660        let diff_one = vec![
661            PowerTableDelta {
662                participant_id: 1,
663                power_delta: StoragePower::from(50),
664                signing_key: PubKey::default(),
665            },
666            PowerTableDelta {
667                participant_id: 3,
668                power_delta: StoragePower::from(300),
669                signing_key: PubKey::new(vec![7, 8, 9]),
670            },
671        ];
672
673        let diff_two = vec![PowerTableDelta {
674            participant_id: 2,
675            power_delta: StoragePower::from(-200),
676            signing_key: PubKey::default(),
677        }];
678
679        let diffs = vec![&diff_one, &diff_two];
680
681        let result = apply_power_table_diffs(&PowerEntries(prev_power_table), &diffs).unwrap();
682
683        assert_eq!(result.len(), 2);
684        assert_eq!(result[0].id, 3);
685        assert_eq!(result[0].power, StoragePower::from(300));
686        assert_eq!(result[0].pub_key, PubKey::new(vec![7, 8, 9]));
687        assert_eq!(result[1].id, 1);
688        assert_eq!(result[1].power, StoragePower::from(150));
689        assert_eq!(result[1].pub_key, PubKey::new(vec![1, 2, 3]));
690    }
691
692    #[derive(Debug)]
693    struct MockVerifier;
694
695    #[derive(Debug, thiserror::Error)]
696    #[error("Mock verifier error")]
697    struct MockVerifierError;
698
699    #[allow(unused)]
700    impl Verifier for MockVerifier {
701        type Error = MockVerifierError;
702
703        fn verify(
704            &self,
705            pub_key: &PubKey,
706            msg: &[u8],
707            sig: &[u8],
708        ) -> std::result::Result<(), Self::Error> {
709            Ok(())
710        }
711
712        fn aggregate(
713            &self,
714            pub_keys: &[PubKey],
715            sigs: &[Vec<u8>],
716        ) -> std::result::Result<Vec<u8>, Self::Error> {
717            Ok(vec![])
718        }
719
720        fn verify_aggregate(
721            &self,
722            payload: &[u8],
723            agg_sig: &[u8],
724            signers: &[PubKey],
725        ) -> std::result::Result<(), Self::Error> {
726            Ok(())
727        }
728    }
729
730    #[test]
731    fn test_validate_finality_certificates_success() -> anyhow::Result<()> {
732        let initial_power_table = vec![
733            PowerEntry {
734                id: 1,
735                power: StoragePower::from(100),
736                pub_key: PubKey::new(vec![1, 2, 3]),
737            },
738            PowerEntry {
739                id: 2,
740                power: StoragePower::from(200),
741                pub_key: PubKey::new(vec![4, 5, 6]),
742            },
743        ];
744
745        let mut certs = Vec::new();
746        for i in 1..=1 {
747            let power_delta = PowerTableDiff::new();
748            let mut justification = create_mock_justification(
749                Phase::Decide,
750                "bafy2bzacearmjx2rxwdc6qkvowushj5nlkdkxmvop2ljojts2znfozxguafmg",
751            );
752            justification.vote.instance = i;
753            let cert = FinalityCertificate::new(power_delta, &justification)?;
754            certs.push(cert);
755        }
756
757        let result = validate_finality_certificates(
758            &MockVerifier,
759            NetworkName::TestnetCalibration,
760            PowerEntries(initial_power_table.clone()),
761            1,
762            None,
763            &certs,
764        )?;
765
766        let (next_instance, _, current_power_table) = result;
767
768        assert_eq!(next_instance, 2);
769        let mut initial_power_table = PowerEntries(initial_power_table);
770        initial_power_table.sort();
771        assert_eq!(current_power_table, initial_power_table);
772
773        Ok(())
774    }
775
776    #[test]
777    fn test_validate_finality_certificates_empty() -> anyhow::Result<()> {
778        let initial_power_table = vec![
779            PowerEntry {
780                id: 1,
781                power: StoragePower::from(100),
782                pub_key: PubKey::new(vec![1, 2, 3]),
783            },
784            PowerEntry {
785                id: 2,
786                power: StoragePower::from(200),
787                pub_key: PubKey::new(vec![4, 5, 6]),
788            },
789        ];
790        let base_tipset = Tipset {
791            epoch: 1,
792            key: vec![1, 2, 3],
793            power_table: powertable_cid(),
794            commitments: keccak_hash::H256::zero(),
795        };
796
797        let certs = Vec::new();
798
799        let result = validate_finality_certificates(
800            &MockVerifier,
801            NetworkName::TestnetCalibration,
802            PowerEntries(initial_power_table.clone()),
803            1,
804            Some(&base_tipset),
805            &certs,
806        );
807
808        assert!(matches!(result, Err(CertsError::EmptyChain)));
809
810        Ok(())
811    }
812
813    #[test]
814    fn test_validate_finality_certificates_non_consecutive() -> anyhow::Result<()> {
815        let initial_power_table = vec![
816            PowerEntry {
817                id: 1,
818                power: StoragePower::from(100),
819                pub_key: PubKey::new(vec![1, 2, 3]),
820            },
821            PowerEntry {
822                id: 2,
823                power: StoragePower::from(200),
824                pub_key: PubKey::new(vec![4, 5, 6]),
825            },
826        ];
827        let mut certs = Vec::new();
828        for i in [1, 3, 4] {
829            let power_delta = PowerTableDiff::new();
830            let mut justification = create_mock_justification(
831                Phase::Decide,
832                "bafy2bzacearmjx2rxwdc6qkvowushj5nlkdkxmvop2ljojts2znfozxguafmg",
833            );
834            justification.vote.instance = i;
835            let cert = FinalityCertificate::new(power_delta, &justification)?;
836            certs.push(cert);
837        }
838
839        let result = validate_finality_certificates(
840            &MockVerifier,
841            NetworkName::TestnetCalibration,
842            PowerEntries(initial_power_table),
843            1,
844            None,
845            &certs,
846        );
847
848        assert!(matches!(result, Err(CertsError::InstanceMismatch { .. })));
849
850        Ok(())
851    }
852
853    #[test]
854    fn test_validate_finality_certificates_base_mismatch() -> anyhow::Result<()> {
855        let power_delta = PowerTableDiff::new();
856        let justification = create_mock_justification(Phase::Decide, &powertable_cid().to_string());
857        let cert = FinalityCertificate::new(power_delta, &justification)?;
858
859        let mismatched_base = Tipset {
860            epoch: 2,
861            key: vec![4, 5, 6],
862            power_table: powertable_cid(),
863            commitments: keccak_hash::H256::zero(),
864        };
865
866        let result = validate_finality_certificates(
867            &MockVerifier,
868            NetworkName::TestnetCalibration,
869            PowerEntries(Vec::new()),
870            1,
871            Some(&mismatched_base),
872            &[cert],
873        );
874
875        assert!(matches!(result, Err(CertsError::BaseTipsetMismatch(_))));
876
877        Ok(())
878    }
879
880    #[test]
881    fn test_validate_finality_certificates_invalid_ec_chain() -> anyhow::Result<()> {
882        let initial_power_table = vec![
883            PowerEntry {
884                id: 1,
885                power: StoragePower::from(100),
886                pub_key: PubKey::new(vec![1, 2, 3]),
887            },
888            PowerEntry {
889                id: 2,
890                power: StoragePower::from(200),
891                pub_key: PubKey::new(vec![4, 5, 6]),
892            },
893        ];
894
895        let base_tipset = Tipset {
896            epoch: 1,
897            key: vec![1, 2, 3],
898            power_table: powertable_cid(),
899            commitments: keccak_hash::H256::zero(),
900        };
901
902        let power_delta = PowerTableDiff::new();
903        let mut justification =
904            create_mock_justification(Phase::Decide, &powertable_cid().to_string());
905
906        // Create an invalid ECChain (empty in this case)
907        justification.vote.value = ECChain::new_unvalidated(vec![Tipset::default()]);
908
909        let invalid_cert = FinalityCertificate::new(power_delta, &justification)?;
910
911        let result = validate_finality_certificates(
912            &MockVerifier,
913            NetworkName::TestnetCalibration,
914            PowerEntries(initial_power_table),
915            1,
916            Some(&base_tipset),
917            &[invalid_cert],
918        );
919
920        assert!(matches!(
921            result,
922            Err(CertsError::InvalidFinalityCertificate(_, _))
923        ));
924
925        Ok(())
926    }
927
928    #[test]
929    fn test_validate_finality_certificates_empty_ec_chain() -> anyhow::Result<()> {
930        let initial_power_table = vec![
931            PowerEntry {
932                id: 1,
933                power: StoragePower::from(100),
934                pub_key: PubKey::new(vec![1, 2, 3]),
935            },
936            PowerEntry {
937                id: 2,
938                power: StoragePower::from(200),
939                pub_key: PubKey::new(vec![4, 5, 6]),
940            },
941        ];
942        let base_tipset = Tipset {
943            epoch: 1,
944            key: vec![1, 2, 3],
945            power_table: powertable_cid(),
946            commitments: keccak_hash::H256::zero(),
947        };
948
949        let cert_with_empty_ec_chain = FinalityCertificate::default();
950
951        let result = validate_finality_certificates(
952            &MockVerifier,
953            NetworkName::TestnetCalibration,
954            PowerEntries(initial_power_table),
955            0,
956            Some(&base_tipset),
957            &[cert_with_empty_ec_chain],
958        );
959
960        // println!("Result: {}", result.unwrap_err());
961        assert!(matches!(
962            result,
963            Err(CertsError::EmptyFinalityCertificate(_))
964        ));
965
966        Ok(())
967    }
968
969    #[test]
970    fn test_validate_finality_certificates_incorrect_power_diff() -> anyhow::Result<()> {
971        let initial_power_table = vec![
972            PowerEntry {
973                id: 1,
974                power: StoragePower::from(100),
975                pub_key: PubKey::new(vec![1, 2, 3]),
976            },
977            PowerEntry {
978                id: 2,
979                power: StoragePower::from(200),
980                pub_key: PubKey::new(vec![4, 5, 6]),
981            },
982        ];
983        let base_tipset = Tipset {
984            epoch: 1,
985            key: vec![1, 2, 3],
986            power_table: powertable_cid(),
987            commitments: keccak_hash::H256::zero(),
988        };
989
990        let incorrect_power_delta = vec![PowerTableDelta {
991            participant_id: 3, // Non-existent participant
992            power_delta: StoragePower::from(50),
993            signing_key: PubKey::new(vec![7, 8, 9]),
994        }];
995
996        let justification = create_mock_justification(Phase::Decide, &powertable_cid().to_string());
997        let cert_with_incorrect_power_diff =
998            FinalityCertificate::new(incorrect_power_delta, &justification)?;
999
1000        let result = validate_finality_certificates(
1001            &MockVerifier,
1002            NetworkName::TestnetCalibration,
1003            PowerEntries(initial_power_table),
1004            1,
1005            Some(&base_tipset),
1006            &[cert_with_incorrect_power_diff],
1007        );
1008
1009        assert!(matches!(result, Err(CertsError::IncorrectPowerDiff { .. })));
1010
1011        Ok(())
1012    }
1013
1014    #[test]
1015    fn test_validate_finality_certificates_empty_vec() -> anyhow::Result<()> {
1016        let initial_power_table = vec![
1017            PowerEntry {
1018                id: 1,
1019                power: StoragePower::from(100),
1020                pub_key: PubKey::new(vec![1, 2, 3]),
1021            },
1022            PowerEntry {
1023                id: 2,
1024                power: StoragePower::from(200),
1025                pub_key: PubKey::new(vec![4, 5, 6]),
1026            },
1027        ];
1028        let base_tipset = Tipset {
1029            epoch: 1,
1030            key: vec![1, 2, 3],
1031            power_table: powertable_cid(),
1032            commitments: keccak_hash::H256::zero(),
1033        };
1034
1035        let certs = Vec::new();
1036
1037        let result = validate_finality_certificates(
1038            &MockVerifier,
1039            NetworkName::TestnetCalibration,
1040            PowerEntries(initial_power_table),
1041            1,
1042            Some(&base_tipset),
1043            &certs,
1044        );
1045
1046        assert!(matches!(result, Err(CertsError::EmptyChain)));
1047
1048        Ok(())
1049    }
1050}