Skip to main content

solana_runtime/
validated_block_finalization.rs

1//! Validated block finalization certificates.
2//!
3//! This module provides [`ValidatedBlockFinalizationCert`], a type that represents
4//! a validated proof that a block has been finalized. The type can only be constructed
5//! through validation, ensuring type-safe guarantees about certificate validity.
6
7use {
8    crate::bank::Bank,
9    agave_votor_messages::{
10        certificate::{
11            CertSignature, Certificate, CertificateType, FastFinalizeCert, FinalizeCert, NotarCert,
12        },
13        consensus_message::Block,
14        finalized_slot::FinalizedSlot,
15        unverified_vote_message::UnverifiedCertificate,
16    },
17    solana_bls_signatures::BlsError,
18    solana_clock::Slot,
19    solana_entry::block_component::{BlockFinalizationCert, VotesAggregate},
20    solana_pubkey::Pubkey,
21    solana_signer_store::{DecodeError, Decoded, decode},
22    std::collections::HashSet,
23    thiserror::Error,
24};
25
26/// Error type for block finalization certificate validation.
27#[derive(Debug, Error, PartialEq, Eq)]
28pub enum BlockFinalizationCertError {
29    /// BLS signature conversion failed
30    #[error("BLS signature conversion for {0:?} failed: {1}")]
31    BlsError(CertificateType, BlsError),
32
33    /// Certificate signature verification failed
34    #[error("Certificate verification for {0:?} failed")]
35    CertificateVerificationFailed(CertificateType),
36
37    /// Missing epoch stakes for slot
38    #[error("Missing epoch stakes for {0:?}")]
39    MissingEpochStakes(CertificateType),
40
41    /// Base 3 finalization certificate
42    #[error("Finalization certificate was base 3 for {0:?}")]
43    Base3Finalization(CertificateType),
44
45    /// Decoding error
46    #[error("Unable to decode certificate {0:?}: {1:?}")]
47    DecodeError(CertificateType, DecodeError),
48}
49
50/// There are two ways a block can be finalized:
51/// - `Finalize`: Slow finalization with both a Finalize certificate and a Notarize certificate
52/// - `FastFinalize`: Fast finalization with a single FinalizeFast certificate
53#[derive(Clone, Debug)]
54#[allow(clippy::large_enum_variant)]
55enum ValidatedBlockFinalizationCertKind {
56    /// Slow finalization: requires both a Finalize cert and a Notarize cert for the same slot
57    Finalize {
58        /// The finalize certificate
59        finalize_cert: FinalizeCert,
60        /// The notarize certificate
61        notarize_cert: NotarCert,
62    },
63    /// Fast finalization: a single FastFinalize certificate
64    FastFinalize(FastFinalizeCert),
65}
66
67/// A validated proof that a block has been finalized.
68///
69/// This type can only be constructed through validation methods, providing
70/// compile-time guarantees that the contained certificates are valid.
71#[derive(Clone, Debug)]
72pub struct ValidatedBlockFinalizationCert {
73    kind: ValidatedBlockFinalizationCertKind,
74    signers: HashSet<Pubkey>,
75}
76
77impl ValidatedBlockFinalizationCert {
78    /// Creates a validated block finalization certificate from a footer's final certificate.
79    ///
80    /// This method performs both BLS signature conversion and stake threshold verification
81    /// against the provided bank's epoch stakes.
82    ///
83    /// # Errors
84    /// Returns an error if:
85    /// - BLS signature conversion fails
86    /// - Certificate signature verification fails
87    /// - The certificates don't meet the required stake thresholds
88    pub fn try_from_footer(
89        block_final_cert: BlockFinalizationCert,
90        bank: &Bank,
91        shred_version: u16,
92    ) -> Result<Self, BlockFinalizationCertError> {
93        let block = Block {
94            slot: block_final_cert.slot,
95            block_id: block_final_cert.block_id,
96        };
97
98        if let Some(notar_aggregate) = block_final_cert.notar_aggregate {
99            // Slow finalization
100            let notarize_cert_type = CertificateType::Notarize(block);
101            let finalize_cert_type = CertificateType::Finalize(block.slot);
102
103            let unverified_notar_cert = UnverifiedCertificate {
104                cert_type: notarize_cert_type,
105                signature: notar_aggregate
106                    .uncompress_signature()
107                    .map_err(|e| BlockFinalizationCertError::BlsError(notarize_cert_type, e))?,
108                bitmap: notar_aggregate.into_bitmap(),
109                shred_version,
110            };
111            let unverified_finalize_cert = UnverifiedCertificate {
112                cert_type: finalize_cert_type,
113                signature: block_final_cert
114                    .final_aggregate
115                    .uncompress_signature()
116                    .map_err(|e| BlockFinalizationCertError::BlsError(finalize_cert_type, e))?,
117                bitmap: block_final_cert.final_aggregate.into_bitmap(),
118                shred_version,
119            };
120
121            // Verify both certificates
122            let notar_signature = Self::verify_certificate(bank, unverified_notar_cert)?;
123            let finalize_signature = Self::verify_certificate(bank, unverified_finalize_cert)?;
124
125            let finalize_signers =
126                Self::extract_signers(bank, finalize_cert_type, &finalize_signature.bitmap)?;
127            let mut signers =
128                Self::extract_signers(bank, notarize_cert_type, &notar_signature.bitmap)?;
129            signers.extend(finalize_signers);
130
131            let notarize_cert = NotarCert {
132                block,
133                signature: notar_signature,
134            };
135            let finalize_cert = FinalizeCert {
136                slot: block.slot,
137                signature: finalize_signature,
138            };
139
140            Ok(Self {
141                kind: ValidatedBlockFinalizationCertKind::Finalize {
142                    finalize_cert,
143                    notarize_cert,
144                },
145                signers,
146            })
147        } else {
148            // Fast finalization
149            let fast_finalize_cert_type = CertificateType::FinalizeFast(block);
150
151            let fast_finalize_cert = UnverifiedCertificate {
152                cert_type: fast_finalize_cert_type,
153                signature: block_final_cert
154                    .final_aggregate
155                    .uncompress_signature()
156                    .map_err(|e| {
157                        BlockFinalizationCertError::BlsError(fast_finalize_cert_type, e)
158                    })?,
159                bitmap: block_final_cert.final_aggregate.into_bitmap(),
160                shred_version,
161            };
162
163            let signature = Self::verify_certificate(bank, fast_finalize_cert)?;
164            let signers = Self::extract_signers(bank, fast_finalize_cert_type, &signature.bitmap)?;
165            let fast_finalize_cert = FastFinalizeCert { block, signature };
166            Ok(Self {
167                kind: ValidatedBlockFinalizationCertKind::FastFinalize(fast_finalize_cert),
168                signers,
169            })
170        }
171    }
172
173    /// Creates a validated block finalization certificate from already-validated certificates.
174    ///
175    /// This is intended for use by the consensus pool, where certificates have already been
176    /// validated by the bls sigverifier
177    ///
178    /// # Safety
179    /// The caller must ensure that the provided certificates have been properly validated.
180    /// Using unvalidated certificates will compromise the type's safety guarantees.
181    pub fn from_validated_slow(
182        finalize_cert: FinalizeCert,
183        notarize_cert: NotarCert,
184        bank: &Bank,
185    ) -> Self {
186        debug_assert_eq!(notarize_cert.block.slot, finalize_cert.slot);
187
188        let finalize_signers = Self::extract_signers(
189            bank,
190            CertificateType::Finalize(finalize_cert.slot),
191            &finalize_cert.signature.bitmap,
192        )
193        .expect("Certificate should have been validated");
194        let mut signers = Self::extract_signers(
195            bank,
196            CertificateType::Notarize(notarize_cert.block),
197            &notarize_cert.signature.bitmap,
198        )
199        .expect("Certificate should have been validated");
200        signers.extend(finalize_signers);
201        Self {
202            kind: ValidatedBlockFinalizationCertKind::Finalize {
203                finalize_cert,
204                notarize_cert,
205            },
206            signers,
207        }
208    }
209
210    /// Creates a validated fast finalization certificate from an already-validated certificate.
211    ///
212    /// This is intended for use by the consensus pool, where certificates have already been
213    /// validated by the bls sigverifier
214    ///
215    /// # Safety
216    /// The caller must ensure that the provided certificate has been properly validated.
217    /// Using an unvalidated certificate will compromise the type's safety guarantees.
218    pub fn from_validated_fast(cert: FastFinalizeCert, bank: &Bank) -> Self {
219        let cert_type = CertificateType::FinalizeFast(cert.block);
220        let signers = Self::extract_signers(bank, cert_type, &cert.signature.bitmap)
221            .expect("Certificate should have been validated");
222        Self {
223            kind: ValidatedBlockFinalizationCertKind::FastFinalize(cert),
224            signers,
225        }
226    }
227
228    /// Returns the slot that is finalized.
229    pub fn slot(&self) -> FinalizedSlot {
230        match &self.kind {
231            ValidatedBlockFinalizationCertKind::Finalize {
232                finalize_cert,
233                notarize_cert,
234            } => {
235                debug_assert_eq!(finalize_cert.slot, notarize_cert.block.slot);
236                FinalizedSlot::Slow(finalize_cert.slot)
237            }
238            ValidatedBlockFinalizationCertKind::FastFinalize(certificate) => {
239                FinalizedSlot::Fast(certificate.block.slot)
240            }
241        }
242    }
243
244    /// Returns the block that is finalized (slot, block_id).
245    pub fn block(&self) -> Block {
246        match &self.kind {
247            ValidatedBlockFinalizationCertKind::Finalize { notarize_cert, .. } => {
248                notarize_cert.block
249            }
250            ValidatedBlockFinalizationCertKind::FastFinalize(cert) => cert.block,
251        }
252    }
253
254    /// Returns true if this is a fast finalization.
255    pub fn is_fast(&self) -> bool {
256        matches!(
257            self.kind,
258            ValidatedBlockFinalizationCertKind::FastFinalize(_)
259        )
260    }
261
262    /// Returns cloned copies of the certificates that prove this finalization.
263    ///
264    /// For slow finalization this returns the finalize certificate followed by the notarize
265    /// certificate. For fast finalization this returns the single fast-finalize certificate.
266    pub fn clone_certificates(&self) -> Vec<Certificate> {
267        match &self.kind {
268            ValidatedBlockFinalizationCertKind::Finalize {
269                finalize_cert,
270                notarize_cert,
271            } => {
272                let notar = Certificate {
273                    cert_type: CertificateType::Notarize(notarize_cert.block),
274                    signature: notarize_cert.signature.signature,
275                    bitmap: notarize_cert.signature.bitmap.clone(),
276                };
277                let finalize = Certificate {
278                    cert_type: CertificateType::Finalize(finalize_cert.slot),
279                    signature: finalize_cert.signature.signature,
280                    bitmap: finalize_cert.signature.bitmap.clone(),
281                };
282                vec![finalize, notar]
283            }
284            ValidatedBlockFinalizationCertKind::FastFinalize(cert) => {
285                let cert = Certificate {
286                    cert_type: CertificateType::FinalizeFast(cert.block),
287                    signature: cert.signature.signature,
288                    bitmap: cert.signature.bitmap.clone(),
289                };
290                vec![cert]
291            }
292        }
293    }
294
295    /// Returns the data needed to calculating and paying vote rewards.
296    pub fn vote_rewards_input(&self) -> (&HashSet<Pubkey>, Slot) {
297        (&self.signers, self.slot().slot())
298    }
299
300    /// Consumes self and returns the contained certificates and the signers.
301    ///
302    /// For slow finalization, returns (signers, finalize_cert, Some(notarize_cert)).
303    /// For fast finalization, returns (signers, fast_finalize_cert, None).
304    pub(crate) fn into_parts(self) -> (HashSet<Pubkey>, Certificate, Option<Certificate>) {
305        let signers = self.signers;
306        let (final_cert, notar_cert) = match self.kind {
307            ValidatedBlockFinalizationCertKind::Finalize {
308                finalize_cert,
309                notarize_cert,
310            } => {
311                let notar = Certificate {
312                    cert_type: CertificateType::Notarize(notarize_cert.block),
313                    signature: notarize_cert.signature.signature,
314                    bitmap: notarize_cert.signature.bitmap,
315                };
316                let finalize = Certificate {
317                    cert_type: CertificateType::Finalize(finalize_cert.slot),
318                    signature: finalize_cert.signature.signature,
319                    bitmap: finalize_cert.signature.bitmap,
320                };
321                (finalize, Some(notar))
322            }
323            ValidatedBlockFinalizationCertKind::FastFinalize(cert) => {
324                let cert = Certificate {
325                    cert_type: CertificateType::FinalizeFast(cert.block),
326                    signature: cert.signature.signature,
327                    bitmap: cert.signature.bitmap,
328                };
329                (cert, None)
330            }
331        };
332        (signers, final_cert, notar_cert)
333    }
334
335    /// Converts this validated certificate into a [`BlockFinalizationCert`] for inclusion in a block footer.
336    pub fn to_block_final_cert(&self) -> BlockFinalizationCert {
337        match &self.kind {
338            ValidatedBlockFinalizationCertKind::Finalize {
339                finalize_cert,
340                notarize_cert,
341            } => {
342                let slot = finalize_cert.slot;
343                let block_id = notarize_cert.block.block_id;
344                BlockFinalizationCert {
345                    slot,
346                    block_id,
347                    final_aggregate: VotesAggregate::from_cert_signature(
348                        finalize_cert.signature.clone(),
349                    ),
350                    notar_aggregate: Some(VotesAggregate::from_cert_signature(
351                        notarize_cert.signature.clone(),
352                    )),
353                }
354            }
355            ValidatedBlockFinalizationCertKind::FastFinalize(cert) => {
356                let block = cert.block;
357                BlockFinalizationCert {
358                    slot: block.slot,
359                    block_id: block.block_id,
360                    final_aggregate: VotesAggregate::from_cert_signature(cert.signature.clone()),
361                    notar_aggregate: None,
362                }
363            }
364        }
365    }
366
367    /// Verifies the certificate and returns `CertSignature` on success.
368    fn verify_certificate(
369        bank: &Bank,
370        cert: UnverifiedCertificate,
371    ) -> Result<CertSignature, BlockFinalizationCertError> {
372        let cert_type = cert.cert_type;
373        let cert = bank
374            .verify_certificate(cert)
375            .map_err(|_| BlockFinalizationCertError::CertificateVerificationFailed(cert_type))?;
376        debug_assert_eq!(cert.cert_type, cert_type);
377        Ok(CertSignature {
378            signature: cert.signature,
379            bitmap: cert.bitmap,
380        })
381    }
382
383    fn extract_signers(
384        bank: &Bank,
385        cert_type: CertificateType,
386        bitmap: &[u8],
387    ) -> Result<HashSet<Pubkey>, BlockFinalizationCertError> {
388        let Some(epoch_stakes) = bank.epoch_stakes_from_slot(cert_type.slot()) else {
389            return Err(BlockFinalizationCertError::MissingEpochStakes(cert_type));
390        };
391        let rank_map = epoch_stakes.bls_pubkey_to_rank_map();
392        let decoded = decode(bitmap, rank_map.len());
393        let ranks = match decoded {
394            Ok(Decoded::Base2(ranks)) => ranks,
395            Ok(Decoded::Base3(_, _)) => {
396                return Err(BlockFinalizationCertError::Base3Finalization(cert_type));
397            }
398            Err(err) => return Err(BlockFinalizationCertError::DecodeError(cert_type, err)),
399        };
400
401        Ok(ranks
402            .iter_ones()
403            .filter_map(|rank| {
404                rank_map
405                    .get_pubkey_stake_entry(rank)
406                    .map(|entry| entry.vote_account_pubkey)
407            })
408            .collect())
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    use {
415        super::*,
416        crate::genesis_utils::{
417            ValidatorVoteKeypairs, create_genesis_config_with_alpenglow_vote_accounts,
418        },
419        agave_votor_messages::{vote::Vote, wire::get_vote_payload_to_sign},
420        bitvec::prelude::*,
421        rand::Rng,
422        solana_bls_signatures::SignatureProjective,
423        solana_entry::block_component::VotesAggregate,
424        solana_hash::Hash,
425        solana_signer_store::encode_base2,
426        std::sync::Arc,
427    };
428
429    /// Creates a bank with BLS-enabled validators for testing certificate verification.
430    /// Returns (bank, validator_keypairs) where bank has validators with BLS keys.
431    fn create_bank_with_bls_validators(
432        num_validators: usize,
433        stakes: Vec<u64>,
434    ) -> (Arc<Bank>, Vec<ValidatorVoteKeypairs>) {
435        assert_eq!(num_validators, stakes.len());
436        let validator_keypairs: Vec<ValidatorVoteKeypairs> = (0..num_validators)
437            .map(|_| ValidatorVoteKeypairs::new_rand())
438            .collect();
439
440        let genesis_config_info = create_genesis_config_with_alpenglow_vote_accounts(
441            10_000_000,
442            &validator_keypairs,
443            stakes,
444        );
445
446        let bank = Arc::new(Bank::new_for_tests(&genesis_config_info.genesis_config));
447        (bank, validator_keypairs)
448    }
449
450    /// Build a cert signature by manually aggregating BLS signatures and encoding bitmap.
451    /// `signing_ranks` specifies which validator ranks are signing.
452    fn build_cert_signature(
453        vote: Vote,
454        shred_version: u16,
455        signing_ranks: &[usize],
456        validator_keypairs: &[ValidatorVoteKeypairs],
457    ) -> CertSignature {
458        let serialized_vote = get_vote_payload_to_sign(vote, shred_version);
459
460        // Aggregate signatures
461        let mut signature = SignatureProjective::identity();
462        for &rank in signing_ranks {
463            let sig = validator_keypairs[rank].bls_keypair.sign(&serialized_vote);
464            signature.aggregate_with(std::iter::once(&sig)).unwrap();
465        }
466
467        // Build bitmap
468        let max_rank = signing_ranks.iter().copied().max().unwrap_or(0);
469        let mut bitvec = BitVec::<u8, Lsb0>::repeat(false, max_rank.saturating_add(1));
470        for &rank in signing_ranks {
471            bitvec.set(rank, true);
472        }
473        let bitmap = encode_base2(&bitvec).expect("Failed to encode bitmap");
474
475        CertSignature {
476            signature: signature.into(),
477            bitmap,
478        }
479    }
480
481    fn vote_pubkeys_for_ranks(bank: &Bank, ranks: &[usize]) -> HashSet<Pubkey> {
482        let rank_map = bank
483            .epoch_stakes_from_slot(bank.slot())
484            .unwrap()
485            .bls_pubkey_to_rank_map();
486        ranks
487            .iter()
488            .map(|rank| {
489                rank_map
490                    .get_pubkey_stake_entry(*rank)
491                    .unwrap()
492                    .vote_account_pubkey
493            })
494            .collect()
495    }
496
497    #[test]
498    fn test_verify_final_cert_valid() {
499        // Create 10 validators with descending stakes (1000, 900, 800, ...)
500        // Total stake = 5500
501        let num_validators = 10;
502        let stakes: Vec<u64> = (0..num_validators)
503            .map(|i| (1000u64).saturating_sub((i as u64).saturating_mul(100)))
504            .collect();
505        let (bank, validator_keypairs) = create_bank_with_bls_validators(num_validators, stakes);
506        let shred_version = rand::rng().random();
507
508        let block = Block {
509            slot: bank.slot(),
510            block_id: Hash::new_unique(),
511        };
512
513        // Test 1: Fast finalize (requires 80% stake = 4400)
514        // Top 6 validators = 1000+900+800+700+600+500 = 4500 (>= 80%)
515        {
516            let cert_type = CertificateType::FinalizeFast(block);
517            let vote = Vote::new_notarization_vote(block);
518            let signing_ranks: Vec<usize> = (0..6).collect();
519            let fast_finalize_cert_sig =
520                build_cert_signature(vote, shred_version, &signing_ranks, &validator_keypairs);
521            let fast_finalize_cert = Certificate {
522                cert_type,
523                signature: fast_finalize_cert_sig.signature,
524                bitmap: fast_finalize_cert_sig.bitmap.clone(),
525            };
526
527            let block_final_cert = BlockFinalizationCert {
528                slot: block.slot,
529                block_id: block.block_id,
530                final_aggregate: VotesAggregate::from_cert_signature(fast_finalize_cert_sig),
531                notar_aggregate: None,
532            };
533
534            let validated = ValidatedBlockFinalizationCert::try_from_footer(
535                block_final_cert,
536                &bank,
537                shred_version,
538            )
539            .expect("Valid fast finalize certificate should pass verification");
540            assert_eq!(validated.clone_certificates(), vec![fast_finalize_cert]);
541        }
542
543        // Test 2: Slow finalize (requires 60% stake = 3300 for both certs)
544        // Top 4 validators = 1000+900+800+700 = 3400 (>= 60%)
545        {
546            let notarize_cert_type = CertificateType::Notarize(block);
547            let notarize_vote = Vote::new_notarization_vote(block);
548            let notarize_signing_ranks: Vec<usize> = (0..4).collect();
549            let notarize_cert_sig = build_cert_signature(
550                notarize_vote,
551                shred_version,
552                &notarize_signing_ranks,
553                &validator_keypairs,
554            );
555            let notarize_cert = Certificate {
556                cert_type: notarize_cert_type,
557                signature: notarize_cert_sig.signature,
558                bitmap: notarize_cert_sig.bitmap.clone(),
559            };
560
561            let finalize_cert_type = CertificateType::Finalize(block.slot);
562            let finalize_vote = Vote::new_finalization_vote(block.slot);
563            let finalize_signing_ranks: Vec<usize> = (0..4).collect();
564            let finalize_cert_sig = build_cert_signature(
565                finalize_vote,
566                shred_version,
567                &finalize_signing_ranks,
568                &validator_keypairs,
569            );
570            let finalize_cert = Certificate {
571                cert_type: finalize_cert_type,
572                signature: finalize_cert_sig.signature,
573                bitmap: finalize_cert_sig.bitmap.clone(),
574            };
575
576            let block_final_cert = BlockFinalizationCert {
577                slot: block.slot,
578                block_id: block.block_id,
579                final_aggregate: VotesAggregate::from_cert_signature(finalize_cert_sig),
580                notar_aggregate: Some(VotesAggregate::from_cert_signature(notarize_cert_sig)),
581            };
582
583            let validated = ValidatedBlockFinalizationCert::try_from_footer(
584                block_final_cert,
585                &bank,
586                shred_version,
587            )
588            .expect("Valid slow finalize certificate should pass verification");
589            assert_eq!(
590                validated.clone_certificates(),
591                vec![finalize_cert, notarize_cert]
592            );
593        }
594    }
595
596    #[test]
597    fn test_slow_finalization_signers_include_notarize_and_finalize_signers() {
598        let stakes = vec![1000, 900, 800, 700, 100];
599        let (bank, validator_keypairs) = create_bank_with_bls_validators(stakes.len(), stakes);
600        let shred_version = rand::rng().random();
601
602        let block = Block {
603            slot: bank.slot(),
604            block_id: Hash::new_unique(),
605        };
606
607        let notarize_vote = Vote::new_notarization_vote(block);
608        let notarize_signing_ranks = vec![0, 1, 3];
609        let notarize_cert = NotarCert {
610            block,
611            signature: build_cert_signature(
612                notarize_vote,
613                shred_version,
614                &notarize_signing_ranks,
615                &validator_keypairs,
616            ),
617        };
618
619        let finalize_vote = Vote::new_finalization_vote(block.slot);
620        let finalize_signing_ranks = vec![0, 1, 2];
621        let finalize_cert = FinalizeCert {
622            slot: block.slot,
623            signature: build_cert_signature(
624                finalize_vote,
625                shred_version,
626                &finalize_signing_ranks,
627                &validator_keypairs,
628            ),
629        };
630
631        let mut expected_signers = vote_pubkeys_for_ranks(&bank, &notarize_signing_ranks);
632        expected_signers.extend(vote_pubkeys_for_ranks(&bank, &finalize_signing_ranks));
633
634        let block_final_cert = BlockFinalizationCert {
635            slot: block.slot,
636            block_id: block.block_id,
637            final_aggregate: VotesAggregate::from_cert_signature(finalize_cert.signature.clone()),
638            notar_aggregate: Some(VotesAggregate::from_cert_signature(
639                notarize_cert.signature.clone(),
640            )),
641        };
642        let validated =
643            ValidatedBlockFinalizationCert::try_from_footer(block_final_cert, &bank, shred_version)
644                .expect("Valid slow finalize certificate should pass verification");
645        let (signers, _, _) = validated.into_parts();
646        assert_eq!(signers, expected_signers);
647
648        let validated = ValidatedBlockFinalizationCert::from_validated_slow(
649            finalize_cert,
650            notarize_cert,
651            &bank,
652        );
653        let (signers, _, _) = validated.into_parts();
654        assert_eq!(signers, expected_signers);
655    }
656
657    #[test]
658    fn test_verify_final_cert_insufficient_stake() {
659        // Create 10 validators with descending stakes (1000, 900, 800, ...)
660        // Total stake = 5500
661        let num_validators = 10;
662        let stakes: Vec<u64> = (0..num_validators)
663            .map(|i| (1000u64).saturating_sub((i as u64).saturating_mul(100)))
664            .collect();
665        let (bank, validator_keypairs) = create_bank_with_bls_validators(num_validators, stakes);
666        let shred_version = rand::rng().random();
667
668        let block = Block {
669            slot: bank.slot(),
670            block_id: Hash::new_unique(),
671        };
672
673        // Fast finalize with insufficient stake (requires 80% = 4400)
674        // Top 5 validators = 1000+900+800+700+600 = 4000 (< 80%)
675        {
676            let vote = Vote::new_notarization_vote(block);
677            let signing_ranks: Vec<usize> = (0..5).collect();
678            let fast_finalize_cert_sig =
679                build_cert_signature(vote, shred_version, &signing_ranks, &validator_keypairs);
680
681            let block_final_cert = BlockFinalizationCert {
682                slot: block.slot,
683                block_id: block.block_id,
684                final_aggregate: VotesAggregate::from_cert_signature(fast_finalize_cert_sig),
685                notar_aggregate: None,
686            };
687
688            let err = ValidatedBlockFinalizationCert::try_from_footer(
689                block_final_cert,
690                &bank,
691                shred_version,
692            )
693            .unwrap_err();
694            assert!(
695                matches!(
696                    err,
697                    BlockFinalizationCertError::CertificateVerificationFailed { .. }
698                ),
699                "Fast finalize with insufficient stake should fail verification.  {err:?}"
700            );
701        }
702
703        // Slow finalize with insufficient notarize stake (requires 60% = 3300)
704        // Top 3 validators = 1000+900+800 = 2700 (< 60%)
705        {
706            let notarize_vote = Vote::new_notarization_vote(block);
707            let notarize_signing_ranks: Vec<usize> = (0..3).collect();
708            let notarize_cert_sig = build_cert_signature(
709                notarize_vote,
710                shred_version,
711                &notarize_signing_ranks,
712                &validator_keypairs,
713            );
714
715            // Finalize cert has enough stake
716            let finalize_vote = Vote::new_finalization_vote(block.slot);
717            let finalize_signing_ranks: Vec<usize> = (0..4).collect();
718            let finalize_cert_sig = build_cert_signature(
719                finalize_vote,
720                shred_version,
721                &finalize_signing_ranks,
722                &validator_keypairs,
723            );
724
725            let block_final_cert = BlockFinalizationCert {
726                slot: block.slot,
727                block_id: block.block_id,
728                final_aggregate: VotesAggregate::from_cert_signature(finalize_cert_sig),
729                notar_aggregate: Some(VotesAggregate::from_cert_signature(notarize_cert_sig)),
730            };
731
732            let err = ValidatedBlockFinalizationCert::try_from_footer(
733                block_final_cert,
734                &bank,
735                shred_version,
736            )
737            .unwrap_err();
738            assert!(
739                matches!(
740                    err,
741                    BlockFinalizationCertError::CertificateVerificationFailed { .. }
742                ),
743                "Slow finalize with insufficient notarize stake should fail verification"
744            );
745        }
746
747        // Slow finalize with insufficient finalize stake (requires 60% = 3300)
748        // Notarize has enough stake, but finalize doesn't
749        {
750            // Notarize cert has enough stake
751            let notarize_vote = Vote::new_notarization_vote(block);
752            let notarize_signing_ranks: Vec<usize> = (0..4).collect();
753            let notarize_cert_sig = build_cert_signature(
754                notarize_vote,
755                shred_version,
756                &notarize_signing_ranks,
757                &validator_keypairs,
758            );
759
760            // Finalize cert has insufficient stake
761            // Top 3 validators = 1000+900+800 = 2700 (< 60%)
762            let finalize_vote = Vote::new_finalization_vote(block.slot);
763            let finalize_signing_ranks: Vec<usize> = (0..3).collect();
764            let finalize_cert_sig = build_cert_signature(
765                finalize_vote,
766                shred_version,
767                &finalize_signing_ranks,
768                &validator_keypairs,
769            );
770
771            let block_final_cert = BlockFinalizationCert {
772                slot: block.slot,
773                block_id: block.block_id,
774                final_aggregate: VotesAggregate::from_cert_signature(finalize_cert_sig),
775                notar_aggregate: Some(VotesAggregate::from_cert_signature(notarize_cert_sig)),
776            };
777
778            let err = ValidatedBlockFinalizationCert::try_from_footer(
779                block_final_cert,
780                &bank,
781                shred_version,
782            )
783            .unwrap_err();
784            assert!(
785                matches!(
786                    err,
787                    BlockFinalizationCertError::CertificateVerificationFailed { .. }
788                ),
789                "Slow finalize with insufficient finalize stake should fail verification"
790            );
791        }
792    }
793}