use {
crate::bank::Bank,
agave_votor_messages::{
certificate::{Certificate, CertificateType},
consensus_message::Block,
finalized_slot::FinalizedSlot,
unverified_vote_message::UnverifiedCertificate,
},
solana_bls_signatures::BlsError,
solana_clock::Slot,
solana_entry::block_component::{BlockFinalizationCert, VotesAggregate},
solana_pubkey::Pubkey,
solana_signer_store::{DecodeError, Decoded, decode},
std::collections::HashSet,
thiserror::Error,
};
#[derive(Debug, Error, PartialEq, Eq)]
pub enum BlockFinalizationCertError {
#[error("BLS signature conversion for {0:?} failed: {1}")]
BlsError(CertificateType, BlsError),
#[error("Certificate verification for {0:?} failed")]
CertificateVerificationFailed(CertificateType),
#[error("Missing epoch stakes for {0:?}")]
MissingEpochStakes(CertificateType),
#[error("Finalization certificate was base 3 for {0:?}")]
Base3Finalization(CertificateType),
#[error("Unable to decode certificate {0:?}: {1:?}")]
DecodeError(CertificateType, DecodeError),
}
#[derive(Clone, Debug)]
#[allow(clippy::large_enum_variant)]
enum ValidatedBlockFinalizationCertKind {
Finalize {
finalize_cert: Certificate,
notarize_cert: Certificate,
},
FastFinalize(Certificate),
}
#[derive(Clone, Debug)]
pub struct ValidatedBlockFinalizationCert {
kind: ValidatedBlockFinalizationCertKind,
signers: HashSet<Pubkey>,
}
impl ValidatedBlockFinalizationCert {
pub fn try_from_footer(
block_final_cert: BlockFinalizationCert,
bank: &Bank,
shred_version: u16,
) -> Result<Self, BlockFinalizationCertError> {
let block = Block {
slot: block_final_cert.slot,
block_id: block_final_cert.block_id,
};
if let Some(notar_aggregate) = block_final_cert.notar_aggregate {
let notarize_cert_type = CertificateType::Notarize(block);
let finalize_cert_type = CertificateType::Finalize(block.slot);
let notarize_cert = UnverifiedCertificate {
cert_type: notarize_cert_type,
signature: notar_aggregate
.uncompress_signature()
.map_err(|e| BlockFinalizationCertError::BlsError(notarize_cert_type, e))?,
bitmap: notar_aggregate.into_bitmap(),
shred_version,
};
let finalize_cert = UnverifiedCertificate {
cert_type: finalize_cert_type,
signature: block_final_cert
.final_aggregate
.uncompress_signature()
.map_err(|e| BlockFinalizationCertError::BlsError(finalize_cert_type, e))?,
bitmap: block_final_cert.final_aggregate.into_bitmap(),
shred_version,
};
let notarize_cert = Self::verify_certificate(bank, notarize_cert)?;
let finalize_cert = Self::verify_certificate(bank, finalize_cert)?;
let signers = Self::extract_signers(bank, &finalize_cert)?;
Ok(Self {
kind: ValidatedBlockFinalizationCertKind::Finalize {
finalize_cert,
notarize_cert,
},
signers,
})
} else {
let fast_finalize_cert_type = CertificateType::FinalizeFast(block);
let fast_finalize_cert = UnverifiedCertificate {
cert_type: fast_finalize_cert_type,
signature: block_final_cert
.final_aggregate
.uncompress_signature()
.map_err(|e| {
BlockFinalizationCertError::BlsError(fast_finalize_cert_type, e)
})?,
bitmap: block_final_cert.final_aggregate.into_bitmap(),
shred_version,
};
let fast_finalize_cert = Self::verify_certificate(bank, fast_finalize_cert)?;
let signers = Self::extract_signers(bank, &fast_finalize_cert)?;
Ok(Self {
kind: ValidatedBlockFinalizationCertKind::FastFinalize(fast_finalize_cert),
signers,
})
}
}
pub fn from_validated_slow(
finalize_cert: Certificate,
notarize_cert: Certificate,
bank: &Bank,
) -> Self {
debug_assert!(finalize_cert.cert_type.is_slow_finalization());
debug_assert!(notarize_cert.cert_type.is_notarize());
debug_assert_eq!(
notarize_cert.cert_type.slot(),
finalize_cert.cert_type.slot()
);
let signers = Self::extract_signers(bank, &finalize_cert)
.expect("Certificate should have been validated");
Self {
kind: ValidatedBlockFinalizationCertKind::Finalize {
finalize_cert,
notarize_cert,
},
signers,
}
}
pub fn from_validated_fast(cert: Certificate, bank: &Bank) -> Self {
debug_assert!(cert.cert_type.is_fast_finalization());
let signers =
Self::extract_signers(bank, &cert).expect("Certificate should have been validated");
Self {
kind: ValidatedBlockFinalizationCertKind::FastFinalize(cert),
signers,
}
}
pub fn slot(&self) -> FinalizedSlot {
match &self.kind {
ValidatedBlockFinalizationCertKind::Finalize {
finalize_cert,
notarize_cert,
} => {
debug_assert_eq!(
finalize_cert.cert_type.slot(),
notarize_cert.cert_type.slot()
);
FinalizedSlot::Slow(finalize_cert.cert_type.slot())
}
ValidatedBlockFinalizationCertKind::FastFinalize(certificate) => {
FinalizedSlot::Fast(certificate.cert_type.slot())
}
}
}
pub fn block(&self) -> Block {
match &self.kind {
ValidatedBlockFinalizationCertKind::Finalize { notarize_cert, .. } => notarize_cert
.cert_type
.to_block()
.expect("notarize certificate has block"),
ValidatedBlockFinalizationCertKind::FastFinalize(cert) => cert
.cert_type
.to_block()
.expect("fast finalize certificate has block"),
}
}
pub fn is_fast(&self) -> bool {
matches!(
self.kind,
ValidatedBlockFinalizationCertKind::FastFinalize(_)
)
}
pub fn clone_certificates(&self) -> Vec<Certificate> {
match &self.kind {
ValidatedBlockFinalizationCertKind::Finalize {
finalize_cert,
notarize_cert,
} => vec![finalize_cert.clone(), notarize_cert.clone()],
ValidatedBlockFinalizationCertKind::FastFinalize(cert) => vec![cert.clone()],
}
}
pub fn vote_rewards_input(&self) -> (&HashSet<Pubkey>, Slot) {
(&self.signers, self.slot().slot())
}
pub(crate) fn into_parts(self) -> (HashSet<Pubkey>, Certificate, Option<Certificate>) {
let signers = self.signers;
let (final_cert, notar_cert) = match self.kind {
ValidatedBlockFinalizationCertKind::Finalize {
finalize_cert,
notarize_cert,
} => (finalize_cert, Some(notarize_cert)),
ValidatedBlockFinalizationCertKind::FastFinalize(cert) => (cert, None),
};
(signers, final_cert, notar_cert)
}
pub fn to_block_final_cert(&self) -> BlockFinalizationCert {
match &self.kind {
ValidatedBlockFinalizationCertKind::Finalize {
finalize_cert,
notarize_cert,
} => {
let slot = finalize_cert.cert_type.slot();
let block_id = notarize_cert
.cert_type
.to_block()
.expect("notarize certificates correspond to blocks")
.block_id;
BlockFinalizationCert {
slot,
block_id,
final_aggregate: VotesAggregate::from_certificate(finalize_cert),
notar_aggregate: Some(VotesAggregate::from_certificate(notarize_cert)),
}
}
ValidatedBlockFinalizationCertKind::FastFinalize(cert) => {
let block = cert
.cert_type
.to_block()
.expect("fast finalizations correspond to blocks");
BlockFinalizationCert {
slot: block.slot,
block_id: block.block_id,
final_aggregate: VotesAggregate::from_certificate(cert),
notar_aggregate: None,
}
}
}
}
fn verify_certificate(
bank: &Bank,
cert: UnverifiedCertificate,
) -> Result<Certificate, BlockFinalizationCertError> {
let cert_type = cert.cert_type;
let cert = bank
.verify_certificate(cert)
.map_err(|_| BlockFinalizationCertError::CertificateVerificationFailed(cert_type))?;
Ok(cert)
}
fn extract_signers(
bank: &Bank,
cert: &Certificate,
) -> Result<HashSet<Pubkey>, BlockFinalizationCertError> {
let Some(epoch_stakes) = bank.epoch_stakes_from_slot(cert.cert_type.slot()) else {
return Err(BlockFinalizationCertError::MissingEpochStakes(
cert.cert_type,
));
};
let rank_map = epoch_stakes.bls_pubkey_to_rank_map();
let decoded = decode(&cert.bitmap, rank_map.len());
let ranks = match decoded {
Ok(Decoded::Base2(ranks)) => ranks,
Ok(Decoded::Base3(_, _)) => {
return Err(BlockFinalizationCertError::Base3Finalization(
cert.cert_type,
));
}
Err(err) => return Err(BlockFinalizationCertError::DecodeError(cert.cert_type, err)),
};
Ok(ranks
.iter_ones()
.filter_map(|rank| {
rank_map
.get_pubkey_stake_entry(rank)
.map(|entry| entry.vote_account_pubkey)
})
.collect())
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::genesis_utils::{
ValidatorVoteKeypairs, create_genesis_config_with_alpenglow_vote_accounts,
},
agave_votor_messages::{vote::Vote, wire::get_vote_payload_to_sign},
bitvec::prelude::*,
rand::Rng,
solana_bls_signatures::SignatureProjective,
solana_entry::block_component::VotesAggregate,
solana_hash::Hash,
solana_signer_store::encode_base2,
std::sync::Arc,
};
fn create_bank_with_bls_validators(
num_validators: usize,
stakes: Vec<u64>,
) -> (Arc<Bank>, Vec<ValidatorVoteKeypairs>) {
assert_eq!(num_validators, stakes.len());
let validator_keypairs: Vec<ValidatorVoteKeypairs> = (0..num_validators)
.map(|_| ValidatorVoteKeypairs::new_rand())
.collect();
let genesis_config_info = create_genesis_config_with_alpenglow_vote_accounts(
10_000_000,
&validator_keypairs,
stakes,
);
let bank = Arc::new(Bank::new_for_tests(&genesis_config_info.genesis_config));
(bank, validator_keypairs)
}
fn build_certificate_manual(
cert_type: CertificateType,
vote: Vote,
shred_version: u16,
signing_ranks: &[usize],
validator_keypairs: &[ValidatorVoteKeypairs],
) -> Certificate {
let serialized_vote = get_vote_payload_to_sign(vote, shred_version);
let mut signature = SignatureProjective::identity();
for &rank in signing_ranks {
let sig = validator_keypairs[rank].bls_keypair.sign(&serialized_vote);
signature.aggregate_with(std::iter::once(&sig)).unwrap();
}
let max_rank = signing_ranks.iter().copied().max().unwrap_or(0);
let mut bitvec = BitVec::<u8, Lsb0>::repeat(false, max_rank.saturating_add(1));
for &rank in signing_ranks {
bitvec.set(rank, true);
}
let bitmap = encode_base2(&bitvec).expect("Failed to encode bitmap");
Certificate {
cert_type,
signature: signature.into(),
bitmap,
}
}
#[test]
fn test_verify_final_cert_valid() {
let num_validators = 10;
let stakes: Vec<u64> = (0..num_validators)
.map(|i| (1000u64).saturating_sub((i as u64).saturating_mul(100)))
.collect();
let (bank, validator_keypairs) = create_bank_with_bls_validators(num_validators, stakes);
let shred_version = rand::rng().random();
let block = Block {
slot: bank.slot(),
block_id: Hash::new_unique(),
};
{
let cert_type = CertificateType::FinalizeFast(block);
let vote = Vote::new_notarization_vote(block);
let signing_ranks: Vec<usize> = (0..6).collect();
let fast_finalize_cert = build_certificate_manual(
cert_type,
vote,
shred_version,
&signing_ranks,
&validator_keypairs,
);
let block_final_cert = BlockFinalizationCert {
slot: block.slot,
block_id: block.block_id,
final_aggregate: VotesAggregate::from_certificate(&fast_finalize_cert),
notar_aggregate: None,
};
let validated = ValidatedBlockFinalizationCert::try_from_footer(
block_final_cert,
&bank,
shred_version,
)
.expect("Valid fast finalize certificate should pass verification");
assert_eq!(validated.clone_certificates(), vec![fast_finalize_cert]);
}
{
let notarize_cert_type = CertificateType::Notarize(block);
let notarize_vote = Vote::new_notarization_vote(block);
let notarize_signing_ranks: Vec<usize> = (0..4).collect();
let notarize_cert = build_certificate_manual(
notarize_cert_type,
notarize_vote,
shred_version,
¬arize_signing_ranks,
&validator_keypairs,
);
let finalize_cert_type = CertificateType::Finalize(block.slot);
let finalize_vote = Vote::new_finalization_vote(block.slot);
let finalize_signing_ranks: Vec<usize> = (0..4).collect();
let finalize_cert = build_certificate_manual(
finalize_cert_type,
finalize_vote,
shred_version,
&finalize_signing_ranks,
&validator_keypairs,
);
let block_final_cert = BlockFinalizationCert {
slot: block.slot,
block_id: block.block_id,
final_aggregate: VotesAggregate::from_certificate(&finalize_cert),
notar_aggregate: Some(VotesAggregate::from_certificate(¬arize_cert)),
};
let validated = ValidatedBlockFinalizationCert::try_from_footer(
block_final_cert,
&bank,
shred_version,
)
.expect("Valid slow finalize certificate should pass verification");
assert_eq!(
validated.clone_certificates(),
vec![finalize_cert, notarize_cert]
);
}
}
#[test]
fn test_verify_final_cert_insufficient_stake() {
let num_validators = 10;
let stakes: Vec<u64> = (0..num_validators)
.map(|i| (1000u64).saturating_sub((i as u64).saturating_mul(100)))
.collect();
let (bank, validator_keypairs) = create_bank_with_bls_validators(num_validators, stakes);
let shred_version = rand::rng().random();
let block = Block {
slot: bank.slot(),
block_id: Hash::new_unique(),
};
{
let cert_type = CertificateType::FinalizeFast(block);
let vote = Vote::new_notarization_vote(block);
let signing_ranks: Vec<usize> = (0..5).collect();
let fast_finalize_cert = build_certificate_manual(
cert_type,
vote,
shred_version,
&signing_ranks,
&validator_keypairs,
);
let block_final_cert = BlockFinalizationCert {
slot: block.slot,
block_id: block.block_id,
final_aggregate: VotesAggregate::from_certificate(&fast_finalize_cert),
notar_aggregate: None,
};
let err = ValidatedBlockFinalizationCert::try_from_footer(
block_final_cert,
&bank,
shred_version,
)
.unwrap_err();
assert!(
matches!(
err,
BlockFinalizationCertError::CertificateVerificationFailed { .. }
),
"Fast finalize with insufficient stake should fail verification. {err:?}"
);
}
{
let notarize_cert_type = CertificateType::Notarize(block);
let notarize_vote = Vote::new_notarization_vote(block);
let notarize_signing_ranks: Vec<usize> = (0..3).collect();
let notarize_cert = build_certificate_manual(
notarize_cert_type,
notarize_vote,
shred_version,
¬arize_signing_ranks,
&validator_keypairs,
);
let finalize_cert_type = CertificateType::Finalize(block.slot);
let finalize_vote = Vote::new_finalization_vote(block.slot);
let finalize_signing_ranks: Vec<usize> = (0..4).collect();
let finalize_cert = build_certificate_manual(
finalize_cert_type,
finalize_vote,
shred_version,
&finalize_signing_ranks,
&validator_keypairs,
);
let block_final_cert = BlockFinalizationCert {
slot: block.slot,
block_id: block.block_id,
final_aggregate: VotesAggregate::from_certificate(&finalize_cert),
notar_aggregate: Some(VotesAggregate::from_certificate(¬arize_cert)),
};
let err = ValidatedBlockFinalizationCert::try_from_footer(
block_final_cert,
&bank,
shred_version,
)
.unwrap_err();
assert!(
matches!(
err,
BlockFinalizationCertError::CertificateVerificationFailed { .. }
),
"Slow finalize with insufficient notarize stake should fail verification"
);
}
{
let notarize_cert_type = CertificateType::Notarize(block);
let notarize_vote = Vote::new_notarization_vote(block);
let notarize_signing_ranks: Vec<usize> = (0..4).collect();
let notarize_cert = build_certificate_manual(
notarize_cert_type,
notarize_vote,
shred_version,
¬arize_signing_ranks,
&validator_keypairs,
);
let finalize_cert_type = CertificateType::Finalize(block.slot);
let finalize_vote = Vote::new_finalization_vote(block.slot);
let finalize_signing_ranks: Vec<usize> = (0..3).collect();
let finalize_cert = build_certificate_manual(
finalize_cert_type,
finalize_vote,
shred_version,
&finalize_signing_ranks,
&validator_keypairs,
);
let block_final_cert = BlockFinalizationCert {
slot: block.slot,
block_id: block.block_id,
final_aggregate: VotesAggregate::from_certificate(&finalize_cert),
notar_aggregate: Some(VotesAggregate::from_certificate(¬arize_cert)),
};
let err = ValidatedBlockFinalizationCert::try_from_footer(
block_final_cert,
&bank,
shred_version,
)
.unwrap_err();
assert!(
matches!(
err,
BlockFinalizationCertError::CertificateVerificationFailed { .. }
),
"Slow finalize with insufficient finalize stake should fail verification"
);
}
}
}