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