1use {
2 crate::bank::Bank,
3 agave_bls_cert_verify::cert_verify::{Error as BlsCertVerifyError, verify_base2},
4 agave_votor_messages::{
5 consensus_message::Block,
6 reward_certificate::{NUM_SLOTS_FOR_REWARD, NotarRewardCertificate, SkipRewardCertificate},
7 vote::Vote,
8 wire::get_vote_payload_to_sign,
9 },
10 solana_bls_signatures::BlsError,
11 solana_clock::Slot,
12 solana_pubkey::Pubkey,
13 std::collections::HashSet,
14 thiserror::Error,
15};
16
17#[derive(Debug, PartialEq, Eq, Error)]
19pub enum Error {
20 #[error(
21 "skip or notar certs have invalid slot numbers: cur={current_slot}, notar={notar_slot:?}, \
22 skip={skip_slot:?}"
23 )]
24 InvalidSlotNumbers {
25 current_slot: Slot,
26 notar_slot: Option<Slot>,
27 skip_slot: Option<Slot>,
28 },
29 #[error("rank map unavailable")]
30 NoRankMap,
31 #[error("bls cert verification failed with {0}")]
32 BlsCertVerify(#[from] BlsCertVerifyError),
33 #[error("verify signature failed with {0:?}")]
34 VerifySig(#[from] BlsError),
35}
36
37fn extract_slot(
42 current_slot: Slot,
43 skip: &Option<SkipRewardCertificate>,
44 notar: &Option<NotarRewardCertificate>,
45) -> Result<Option<Slot>, Error> {
46 let slot = match (skip, notar) {
47 (None, None) => return Ok(None),
48 (Some(s), None) => s.slot,
49 (None, Some(n)) => n.slot,
50 (Some(s), Some(n)) => {
51 if s.slot != n.slot {
52 return Err(Error::InvalidSlotNumbers {
53 current_slot,
54 notar_slot: Some(n.slot),
55 skip_slot: Some(s.slot),
56 });
57 }
58 s.slot
59 }
60 };
61 if slot.saturating_add(NUM_SLOTS_FOR_REWARD) != current_slot {
62 return Err(Error::InvalidSlotNumbers {
63 current_slot,
64 notar_slot: notar.as_ref().map(|c| c.slot),
65 skip_slot: skip.as_ref().map(|c| c.slot),
66 });
67 }
68 Ok(Some(slot))
69}
70
71#[derive(Debug, Clone)]
73pub struct ValidatedRewardCert {
74 validators: HashSet<Pubkey>,
76 reward_slot: Slot,
78}
79
80impl ValidatedRewardCert {
81 pub fn try_new(
83 bank: &Bank,
84 shred_version: u16,
85 skip: &Option<SkipRewardCertificate>,
86 notar: &Option<NotarRewardCertificate>,
87 ) -> Result<Option<Self>, Error> {
88 let Some(reward_slot) = extract_slot(bank.slot(), skip, notar)? else {
89 return Ok(None);
90 };
91 let rank_map = bank
92 .epoch_stakes_from_slot(reward_slot)
93 .ok_or(Error::NoRankMap)?
94 .bls_pubkey_to_rank_map();
95 let max_validators = rank_map.len();
96 let mut validators = HashSet::with_capacity(max_validators);
97
98 let mut rank_map = |ind: usize| {
99 rank_map.get_pubkey_stake_entry(ind).map(|entry| {
100 validators.insert(entry.vote_account_pubkey);
101 entry.bls_pubkey
102 })
103 };
104
105 if let Some(skip) = skip {
106 let vote = Vote::new_skip_vote(skip.slot);
107 let payload = get_vote_payload_to_sign(vote, shred_version);
108 verify_base2(
109 &payload,
110 &skip.signature,
111 skip.to_bitmap(),
112 max_validators,
113 &mut rank_map,
114 )?
115 }
116 if let Some(notar) = notar {
117 let vote = Vote::new_notarization_vote(Block {
118 slot: notar.slot,
119 block_id: notar.block_id,
120 });
121 let payload = get_vote_payload_to_sign(vote, shred_version);
122 verify_base2(
123 &payload,
124 ¬ar.signature,
125 notar.bitmap(),
126 max_validators,
127 rank_map,
128 )?
129 }
130 if validators.is_empty() {
131 return Ok(None);
132 }
133 Ok(Some(Self {
134 validators,
135 reward_slot,
136 }))
137 }
138
139 pub fn try_new_for_leader(
146 current_slot: Slot,
147 skip: &Option<SkipRewardCertificate>,
148 notar: &Option<NotarRewardCertificate>,
149 validators: impl IntoIterator<Item = Pubkey>,
150 ) -> Result<Option<Self>, Error> {
151 let Some(reward_slot) = extract_slot(current_slot, skip, notar)? else {
152 return Ok(None);
153 };
154 let validators: HashSet<_> = validators.into_iter().collect();
155 if validators.is_empty() {
156 return Ok(None);
157 }
158 Ok(Some(Self {
159 validators,
160 reward_slot,
161 }))
162 }
163
164 pub(crate) fn slot(&self) -> Slot {
165 self.reward_slot
166 }
167
168 pub(crate) fn validators(&self) -> &HashSet<Pubkey> {
169 &self.validators
170 }
171
172 #[cfg(test)]
173 pub(crate) fn new_for_tests(reward_slot: Slot, validators: Vec<Pubkey>) -> Self {
174 let validators = validators.into_iter().collect();
175 Self {
176 reward_slot,
177 validators,
178 }
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use {
185 super::*,
186 crate::genesis_utils::{
187 ValidatorVoteKeypairs, create_genesis_config_with_alpenglow_vote_accounts,
188 },
189 agave_votor_messages::consensus_message::VoteMessage,
190 bitvec::vec::BitVec,
191 rand::Rng,
192 solana_bls_signatures::{
193 Keypair as BlsKeypair, Signature as BLSSignature,
194 SignatureCompressed as BlsSignatureCompressed, SignatureProjective,
195 pubkey::PubkeyCompressed as BLSPubkeyCompressed,
196 },
197 solana_hash::Hash,
198 solana_leader_schedule::SlotLeader,
199 solana_signer_store::encode_base2,
200 std::collections::HashMap,
201 };
202
203 fn new_vote(vote: Vote, rank: usize, keypair: &BlsKeypair, shred_version: u16) -> VoteMessage {
204 let payload = get_vote_payload_to_sign(vote, shred_version);
205 let signature = keypair.sign(&payload).into();
206 VoteMessage {
207 vote,
208 signature,
209 rank: rank.try_into().unwrap(),
210 }
211 }
212
213 fn build_sig_bitmap(votes: &[VoteMessage]) -> (BlsSignatureCompressed, Vec<u8>) {
214 let max_rank = votes.last().unwrap().rank;
215 let mut signature = SignatureProjective::identity();
216 let mut bitvec = BitVec::repeat(false, (max_rank + 1) as usize);
217 for vote in votes {
218 signature
219 .aggregate_with(std::iter::once(&vote.signature))
220 .unwrap();
221 bitvec.set(vote.rank as usize, true);
222 }
223 (
224 BLSSignature::from(signature).try_into().unwrap(),
225 encode_base2(&bitvec).unwrap(),
226 )
227 }
228
229 #[test]
230 fn validate_try_new() {
231 let reward_slot = 1;
232 let bank_slot = reward_slot + NUM_SLOTS_FOR_REWARD;
233 let num_skip_validators = 3;
234 let num_notar_validators = 5;
235 let num_validators = num_skip_validators + num_notar_validators;
236
237 let validator_keypairs = (0..num_validators)
238 .map(|_| ValidatorVoteKeypairs::new_rand())
239 .collect::<Vec<_>>();
240 let shred_version = rand::rng().random();
241 let keypair_map = validator_keypairs
242 .iter()
243 .map(|k| {
244 (
245 BLSPubkeyCompressed::from(*k.bls_keypair.public),
246 k.bls_keypair.clone(),
247 )
248 })
249 .collect::<HashMap<_, _>>();
250 let genesis = create_genesis_config_with_alpenglow_vote_accounts(
251 1_000_000_000,
252 &validator_keypairs,
253 vec![100; validator_keypairs.len()],
254 );
255 let (bank, _bank_forks) =
256 Bank::new_for_tests(&genesis.genesis_config).wrap_with_bank_forks_for_tests();
257 let bank = Bank::new_from_parent(bank, SlotLeader::default(), bank_slot);
258
259 let rank_map = bank
260 .epoch_stakes_from_slot(reward_slot)
261 .unwrap()
262 .bls_pubkey_to_rank_map();
263 let signing_keys = (0..num_validators)
264 .map(|index| {
265 let pubkey_affine = rank_map.get_pubkey_stake_entry(index).unwrap().bls_pubkey;
266 keypair_map
267 .get(&BLSPubkeyCompressed::from(*pubkey_affine))
268 .unwrap()
269 })
270 .collect::<Vec<_>>();
271
272 let block_id = Hash::new_unique();
273 let notar_vote = Vote::new_notarization_vote(Block {
274 slot: reward_slot,
275 block_id,
276 });
277 let notar_votes = (0..num_notar_validators)
278 .map(|rank| new_vote(notar_vote, rank, signing_keys[rank], shred_version))
279 .collect::<Vec<_>>();
280 let (signature, bitmap) = build_sig_bitmap(¬ar_votes);
281 let notar_reward_cert =
282 NotarRewardCertificate::try_new(reward_slot, block_id, signature, bitmap).unwrap();
283
284 let skip_vote = Vote::new_skip_vote(reward_slot);
285 let skip_votes = (num_notar_validators..num_validators)
286 .map(|rank| new_vote(skip_vote, rank, signing_keys[rank], shred_version))
287 .collect::<Vec<_>>();
288 let (signature, bitmap) = build_sig_bitmap(&skip_votes);
289 let skip_reward_cert =
290 SkipRewardCertificate::try_new(reward_slot, signature, bitmap).unwrap();
291
292 let validated_reward_cert = ValidatedRewardCert::try_new(
293 &bank,
294 shred_version,
295 &Some(skip_reward_cert),
296 &Some(notar_reward_cert),
297 )
298 .unwrap()
299 .unwrap();
300 assert_eq!(validated_reward_cert.validators.len(), num_validators);
301 }
302}