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 bank: &Bank,
43 skip: &Option<SkipRewardCertificate>,
44 notar: &Option<NotarRewardCertificate>,
45) -> Result<Option<Slot>, Error> {
46 let current_slot = bank.slot();
47 let slot = match (skip, notar) {
48 (None, None) => return Ok(None),
49 (Some(s), None) => s.slot,
50 (None, Some(n)) => n.slot,
51 (Some(s), Some(n)) => {
52 if s.slot != n.slot {
53 return Err(Error::InvalidSlotNumbers {
54 current_slot,
55 notar_slot: Some(n.slot),
56 skip_slot: Some(s.slot),
57 });
58 }
59 s.slot
60 }
61 };
62 if slot.saturating_add(NUM_SLOTS_FOR_REWARD) != current_slot
63 || slot <= bank.get_alpenglow_migration_slot().unwrap_or(slot)
64 {
65 return Err(Error::InvalidSlotNumbers {
66 current_slot: bank.slot(),
67 notar_slot: notar.as_ref().map(|c| c.slot),
68 skip_slot: skip.as_ref().map(|c| c.slot),
69 });
70 }
71 Ok(Some(slot))
72}
73
74#[derive(Debug, Clone)]
76pub struct ValidatedRewardCert {
77 validators: HashSet<Pubkey>,
79 reward_slot: Slot,
81}
82
83impl ValidatedRewardCert {
84 pub fn try_new(
86 bank: &Bank,
87 shred_version: u16,
88 skip: &Option<SkipRewardCertificate>,
89 notar: &Option<NotarRewardCertificate>,
90 ) -> Result<Option<Self>, Error> {
91 let Some(reward_slot) = extract_slot(bank, skip, notar)? else {
92 return Ok(None);
93 };
94 let rank_map = bank
95 .epoch_stakes_from_slot(reward_slot)
96 .ok_or(Error::NoRankMap)?
97 .bls_pubkey_to_rank_map();
98 let max_validators = rank_map.len();
99 let mut validators = HashSet::with_capacity(max_validators);
100
101 let mut rank_map = |ind: usize| {
102 rank_map.get_pubkey_stake_entry(ind).map(|entry| {
103 validators.insert(entry.vote_account_pubkey);
104 entry.bls_pubkey
105 })
106 };
107
108 if let Some(skip) = skip {
109 let vote = Vote::new_skip_vote(skip.slot);
110 let payload = get_vote_payload_to_sign(vote, shred_version);
111 verify_base2(
112 &payload,
113 &skip.signature,
114 skip.to_bitmap(),
115 max_validators,
116 &mut rank_map,
117 None,
118 )?
119 }
120 if let Some(notar) = notar {
121 let vote = Vote::new_notarization_vote(Block {
122 slot: notar.slot,
123 block_id: notar.block_id,
124 });
125 let payload = get_vote_payload_to_sign(vote, shred_version);
126 verify_base2(
127 &payload,
128 ¬ar.signature,
129 notar.bitmap(),
130 max_validators,
131 rank_map,
132 None,
133 )?
134 }
135 if validators.is_empty() {
136 return Ok(None);
137 }
138 Ok(Some(Self {
139 validators,
140 reward_slot,
141 }))
142 }
143
144 pub fn try_new_for_leader(
151 bank: &Bank,
152 skip: &Option<SkipRewardCertificate>,
153 notar: &Option<NotarRewardCertificate>,
154 validators: impl IntoIterator<Item = Pubkey>,
155 ) -> Result<Option<Self>, Error> {
156 let Some(reward_slot) = extract_slot(bank, skip, notar)? else {
157 return Ok(None);
158 };
159 let validators: HashSet<_> = validators.into_iter().collect();
160 if validators.is_empty() {
161 return Ok(None);
162 }
163 Ok(Some(Self {
164 validators,
165 reward_slot,
166 }))
167 }
168
169 pub(crate) fn slot(&self) -> Slot {
170 self.reward_slot
171 }
172
173 pub(crate) fn validators(&self) -> &HashSet<Pubkey> {
174 &self.validators
175 }
176
177 #[cfg(test)]
178 pub(crate) fn new_for_tests(reward_slot: Slot, validators: Vec<Pubkey>) -> Self {
179 let validators = validators.into_iter().collect();
180 Self {
181 reward_slot,
182 validators,
183 }
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use {
190 super::*,
191 crate::genesis_utils::{
192 ValidatorVoteKeypairs, create_genesis_config_with_alpenglow_vote_accounts,
193 },
194 agave_votor_messages::{
195 certificate::{CertSignature, GenesisCert},
196 consensus_message::VoteMessage,
197 },
198 bitvec::vec::BitVec,
199 rand::Rng,
200 solana_bls_signatures::{
201 BLS_SIGNATURE_AFFINE_SIZE, BLS_SIGNATURE_COMPRESSED_SIZE, Keypair as BlsKeypair,
202 Signature as BLSSignature, SignatureCompressed as BlsSignatureCompressed,
203 SignatureProjective, pubkey::PubkeyCompressed as BLSPubkeyCompressed,
204 },
205 solana_hash::Hash,
206 solana_leader_schedule::SlotLeader,
207 solana_signer_store::encode_base2,
208 std::{collections::HashMap, num::NonZero},
209 };
210
211 fn new_vote_msg(
212 vote: Vote,
213 rank: usize,
214 keypair: &BlsKeypair,
215 shred_version: u16,
216 ) -> VoteMessage {
217 let payload = get_vote_payload_to_sign(vote, shred_version);
218 let signature = keypair.sign(&payload).into();
219 VoteMessage {
220 vote,
221 signature,
222 rank: rank.try_into().unwrap(),
223 stake: NonZero::new(123).unwrap(),
224 }
225 }
226
227 fn build_sig_bitmap(votes: &[VoteMessage]) -> (BlsSignatureCompressed, Vec<u8>) {
228 let max_rank = votes.last().unwrap().rank;
229 let mut signature = SignatureProjective::identity();
230 let mut bitvec = BitVec::repeat(false, (max_rank + 1) as usize);
231 for vote in votes {
232 signature
233 .aggregate_with(std::iter::once(&vote.signature))
234 .unwrap();
235 bitvec.set(vote.rank as usize, true);
236 }
237 (
238 BLSSignature::from(signature).try_into().unwrap(),
239 encode_base2(&bitvec).unwrap(),
240 )
241 }
242
243 #[test]
244 fn test_extract_slot_rejects_tower_slots() {
245 let migration_slot = 1;
246 let validator_keypairs = [ValidatorVoteKeypairs::new_rand()];
247 let genesis = create_genesis_config_with_alpenglow_vote_accounts(
248 1_000_000_000,
249 &validator_keypairs,
250 vec![100],
251 );
252 let (root_bank, _bank_forks) =
253 Bank::new_for_tests(&genesis.genesis_config).wrap_with_bank_forks_for_tests();
254 let genesis_cert = GenesisCert {
255 block: Block {
256 slot: migration_slot,
257 block_id: Hash::default(),
258 },
259 signature: CertSignature {
260 signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]),
261 bitmap: vec![],
262 },
263 };
264
265 for reward_slot in [migration_slot - 1, migration_slot, migration_slot + 1] {
266 let bank = Bank::new_from_parent(
267 root_bank.clone(),
268 SlotLeader::default(),
269 reward_slot + NUM_SLOTS_FOR_REWARD,
270 );
271 bank.set_alpenglow_genesis_certificate(&genesis_cert);
272 let skip = Some(
273 SkipRewardCertificate::try_new(
274 reward_slot,
275 BlsSignatureCompressed([0; BLS_SIGNATURE_COMPRESSED_SIZE]),
276 vec![],
277 )
278 .unwrap(),
279 );
280
281 let result = extract_slot(&bank, &skip, &None);
282 if reward_slot <= migration_slot {
283 assert_eq!(
284 result,
285 Err(Error::InvalidSlotNumbers {
286 current_slot: bank.slot(),
287 notar_slot: None,
288 skip_slot: Some(reward_slot),
289 })
290 );
291 } else {
292 assert_eq!(result, Ok(Some(reward_slot)));
293 }
294 }
295 }
296
297 #[test]
298 fn validate_try_new() {
299 let reward_slot = 1;
300 let bank_slot = reward_slot + NUM_SLOTS_FOR_REWARD;
301 let num_skip_validators = 3;
302 let num_notar_validators = 5;
303 let num_validators = num_skip_validators + num_notar_validators;
304
305 let validator_keypairs = (0..num_validators)
306 .map(|_| ValidatorVoteKeypairs::new_rand())
307 .collect::<Vec<_>>();
308 let shred_version = rand::rng().random();
309 let keypair_map = validator_keypairs
310 .iter()
311 .map(|k| {
312 (
313 BLSPubkeyCompressed::from(*k.bls_keypair.public),
314 k.bls_keypair.clone(),
315 )
316 })
317 .collect::<HashMap<_, _>>();
318 let genesis = create_genesis_config_with_alpenglow_vote_accounts(
319 1_000_000_000,
320 &validator_keypairs,
321 vec![100; validator_keypairs.len()],
322 );
323 let (bank, _bank_forks) =
324 Bank::new_for_tests(&genesis.genesis_config).wrap_with_bank_forks_for_tests();
325 let bank = Bank::new_from_parent(bank, SlotLeader::default(), bank_slot);
326
327 let rank_map = bank
328 .epoch_stakes_from_slot(reward_slot)
329 .unwrap()
330 .bls_pubkey_to_rank_map();
331 let signing_keys = (0..num_validators)
332 .map(|index| {
333 let pubkey_affine = rank_map.get_pubkey_stake_entry(index).unwrap().bls_pubkey;
334 keypair_map
335 .get(&BLSPubkeyCompressed::from(*pubkey_affine))
336 .unwrap()
337 })
338 .collect::<Vec<_>>();
339
340 let block_id = Hash::new_unique();
341 let notar_vote = Vote::new_notarization_vote(Block {
342 slot: reward_slot,
343 block_id,
344 });
345 let notar_votes = (0..num_notar_validators)
346 .map(|rank| new_vote_msg(notar_vote, rank, signing_keys[rank], shred_version))
347 .collect::<Vec<_>>();
348 let (signature, bitmap) = build_sig_bitmap(¬ar_votes);
349 let notar_reward_cert =
350 NotarRewardCertificate::try_new(reward_slot, block_id, signature, bitmap).unwrap();
351
352 let skip_vote = Vote::new_skip_vote(reward_slot);
353 let skip_votes = (num_notar_validators..num_validators)
354 .map(|rank| new_vote_msg(skip_vote, rank, signing_keys[rank], shred_version))
355 .collect::<Vec<_>>();
356 let (signature, bitmap) = build_sig_bitmap(&skip_votes);
357 let skip_reward_cert =
358 SkipRewardCertificate::try_new(reward_slot, signature, bitmap).unwrap();
359
360 let validated_reward_cert = ValidatedRewardCert::try_new(
361 &bank,
362 shred_version,
363 &Some(skip_reward_cert),
364 &Some(notar_reward_cert),
365 )
366 .unwrap()
367 .unwrap();
368 assert_eq!(validated_reward_cert.validators.len(), num_validators);
369 }
370}