use {
crate::bank::Bank,
agave_bls_cert_verify::cert_verify::{Error as BlsCertVerifyError, verify_base2},
agave_votor_messages::{
reward_certificate::{NUM_SLOTS_FOR_REWARD, NotarRewardCertificate, SkipRewardCertificate},
vote::Vote,
},
solana_bls_signatures::BlsError,
solana_clock::Slot,
solana_pubkey::Pubkey,
thiserror::Error,
};
#[derive(Debug, PartialEq, Eq, Error)]
pub enum Error {
#[error(
"skip or notar certs have invalid slot numbers: cur={current_slot}, notar={notar_slot:?}, \
skip={skip_slot:?}"
)]
InvalidSlotNumbers {
current_slot: Slot,
notar_slot: Option<Slot>,
skip_slot: Option<Slot>,
},
#[error("rank map unavailable")]
NoRankMap,
#[error("bls cert verification failed with {0}")]
BlsCertVerify(#[from] BlsCertVerifyError),
#[error("verify signature failed with {0:?}")]
VerifySig(#[from] BlsError),
#[error("empty certs were provided")]
Empty,
}
#[allow(dead_code)]
fn extract_slot(
current_slot: Slot,
skip: &Option<SkipRewardCertificate>,
notar: &Option<NotarRewardCertificate>,
) -> Result<Option<Slot>, Error> {
let slot = match (skip, notar) {
(None, None) => return Ok(None),
(Some(s), None) => s.slot,
(None, Some(n)) => n.slot,
(Some(s), Some(n)) => {
if s.slot != n.slot {
return Err(Error::InvalidSlotNumbers {
current_slot,
notar_slot: Some(n.slot),
skip_slot: Some(s.slot),
});
}
s.slot
}
};
if slot.saturating_add(NUM_SLOTS_FOR_REWARD) != current_slot {
return Err(Error::InvalidSlotNumbers {
current_slot,
notar_slot: notar.as_ref().map(|c| c.slot),
skip_slot: skip.as_ref().map(|c| c.slot),
});
}
Ok(Some(slot))
}
#[allow(dead_code)]
pub(crate) struct ValidatedRewardCert {
validators: Vec<Pubkey>,
reward_slot: Slot,
}
impl ValidatedRewardCert {
#[allow(dead_code)]
pub(crate) fn try_new(
bank: &Bank,
skip: &Option<SkipRewardCertificate>,
notar: &Option<NotarRewardCertificate>,
) -> Result<Self, Error> {
let Some(reward_slot) = extract_slot(bank.slot(), skip, notar)? else {
return Err(Error::Empty);
};
let rank_map = bank
.epoch_stakes_from_slot(reward_slot)
.ok_or(Error::NoRankMap)?
.bls_pubkey_to_rank_map();
let max_validators = rank_map.len();
let mut validators = Vec::with_capacity(max_validators);
let mut rank_map = |ind: usize| {
rank_map.get_pubkey_stake_entry(ind).map(|entry| {
validators.push(entry.pubkey);
entry.bls_pubkey
})
};
if let Some(skip) = skip {
let vote = Vote::new_skip_vote(skip.slot);
let payload = bincode::serialize(&vote).unwrap();
verify_base2(
&payload,
&skip.signature,
skip.to_bitmap(),
max_validators,
&mut rank_map,
)?
}
if let Some(notar) = notar {
let vote = Vote::new_notarization_vote(notar.slot, notar.block_id);
let payload = bincode::serialize(&vote).unwrap();
verify_base2(
&payload,
¬ar.signature,
notar.bitmap(),
max_validators,
rank_map,
)?
}
if validators.is_empty() {
return Err(Error::Empty);
}
Ok(Self {
validators,
reward_slot,
})
}
#[allow(dead_code)]
pub(crate) fn into_parts(self) -> (Slot, Vec<Pubkey>) {
(self.reward_slot, self.validators)
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::genesis_utils::{
ValidatorVoteKeypairs, create_genesis_config_with_alpenglow_vote_accounts,
},
agave_votor_messages::consensus_message::VoteMessage,
bitvec::vec::BitVec,
solana_bls_signatures::{
Keypair as BlsKeypair, Pubkey as BLSPubkey, Signature as BLSSignature,
SignatureCompressed as BlsSignatureCompressed, SignatureProjective,
},
solana_hash::Hash,
solana_signer_store::encode_base2,
std::{collections::HashMap, sync::Arc},
};
fn new_vote(vote: Vote, rank: usize, keypair: &BlsKeypair) -> VoteMessage {
let serialized = bincode::serialize(&vote).unwrap();
let signature = keypair.sign(&serialized).into();
VoteMessage {
vote,
signature,
rank: rank.try_into().unwrap(),
}
}
fn build_sig_bitmap(votes: &[VoteMessage]) -> (BlsSignatureCompressed, Vec<u8>) {
let max_rank = votes.last().unwrap().rank;
let mut signature = SignatureProjective::identity();
let mut bitvec = BitVec::repeat(false, (max_rank + 1) as usize);
for vote in votes {
signature
.aggregate_with(std::iter::once(&vote.signature))
.unwrap();
bitvec.set(vote.rank as usize, true);
}
(
BLSSignature::from(signature).try_into().unwrap(),
encode_base2(&bitvec).unwrap(),
)
}
#[test]
fn validate_try_new() {
let reward_slot = 1;
let bank_slot = reward_slot + NUM_SLOTS_FOR_REWARD;
let num_skip_validators = 3;
let num_notar_validators = 5;
let num_validators = num_skip_validators + num_notar_validators;
let validator_keypairs = (0..num_validators)
.map(|_| ValidatorVoteKeypairs::new_rand())
.collect::<Vec<_>>();
let keypair_map = validator_keypairs
.iter()
.map(|k| (BLSPubkey::from(k.bls_keypair.public), k.bls_keypair.clone()))
.collect::<HashMap<_, _>>();
let genesis = create_genesis_config_with_alpenglow_vote_accounts(
1_000_000_000,
&validator_keypairs,
vec![100; validator_keypairs.len()],
);
let bank = Arc::new(Bank::new_for_tests(&genesis.genesis_config));
let bank = Bank::new_from_parent(bank, &Pubkey::default(), bank_slot);
let rank_map = bank
.epoch_stakes_from_slot(reward_slot)
.unwrap()
.bls_pubkey_to_rank_map();
let signing_keys = (0..num_validators)
.map(|index| {
keypair_map
.get(&rank_map.get_pubkey_stake_entry(index).unwrap().bls_pubkey)
.unwrap()
})
.collect::<Vec<_>>();
let blockid = Hash::new_unique();
let notar_vote = Vote::new_notarization_vote(reward_slot, blockid);
let notar_votes = (0..num_notar_validators)
.map(|rank| new_vote(notar_vote, rank, signing_keys[rank]))
.collect::<Vec<_>>();
let (signature, bitmap) = build_sig_bitmap(¬ar_votes);
let notar_reward_cert =
NotarRewardCertificate::try_new(reward_slot, blockid, signature, bitmap).unwrap();
let skip_vote = Vote::new_skip_vote(reward_slot);
let skip_votes = (num_notar_validators..num_validators)
.map(|rank| new_vote(skip_vote, rank, signing_keys[rank]))
.collect::<Vec<_>>();
let (signature, bitmap) = build_sig_bitmap(&skip_votes);
let skip_reward_cert =
SkipRewardCertificate::try_new(reward_slot, signature, bitmap).unwrap();
let validated_reward_cert =
ValidatedRewardCert::try_new(&bank, &Some(skip_reward_cert), &Some(notar_reward_cert))
.unwrap();
assert_eq!(validated_reward_cert.validators.len(), num_validators);
}
}