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::new_unique(migration_slot),
256 signature: CertSignature {
257 signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]),
258 bitmap: vec![],
259 },
260 };
261
262 for reward_slot in [migration_slot - 1, migration_slot, migration_slot + 1] {
263 let bank = Bank::new_from_parent(
264 root_bank.clone(),
265 SlotLeader::default(),
266 reward_slot + NUM_SLOTS_FOR_REWARD,
267 );
268 bank.set_alpenglow_genesis_certificate(&genesis_cert);
269 let skip = Some(
270 SkipRewardCertificate::try_new(
271 reward_slot,
272 BlsSignatureCompressed([0; BLS_SIGNATURE_COMPRESSED_SIZE]),
273 vec![],
274 )
275 .unwrap(),
276 );
277
278 let result = extract_slot(&bank, &skip, &None);
279 if reward_slot <= migration_slot {
280 assert_eq!(
281 result,
282 Err(Error::InvalidSlotNumbers {
283 current_slot: bank.slot(),
284 notar_slot: None,
285 skip_slot: Some(reward_slot),
286 })
287 );
288 } else {
289 assert_eq!(result, Ok(Some(reward_slot)));
290 }
291 }
292 }
293
294 #[test]
295 fn validate_try_new() {
296 let reward_slot = 1;
297 let bank_slot = reward_slot + NUM_SLOTS_FOR_REWARD;
298 let num_skip_validators = 3;
299 let num_notar_validators = 5;
300 let num_validators = num_skip_validators + num_notar_validators;
301
302 let validator_keypairs = (0..num_validators)
303 .map(|_| ValidatorVoteKeypairs::new_rand())
304 .collect::<Vec<_>>();
305 let shred_version = rand::rng().random();
306 let keypair_map = validator_keypairs
307 .iter()
308 .map(|k| {
309 (
310 BLSPubkeyCompressed::from(*k.bls_keypair.public),
311 k.bls_keypair.clone(),
312 )
313 })
314 .collect::<HashMap<_, _>>();
315 let genesis = create_genesis_config_with_alpenglow_vote_accounts(
316 1_000_000_000,
317 &validator_keypairs,
318 vec![100; validator_keypairs.len()],
319 );
320 let (bank, _bank_forks) =
321 Bank::new_for_tests(&genesis.genesis_config).wrap_with_bank_forks_for_tests();
322 let bank = Bank::new_from_parent(bank, SlotLeader::default(), bank_slot);
323
324 let rank_map = bank
325 .epoch_stakes_from_slot(reward_slot)
326 .unwrap()
327 .bls_pubkey_to_rank_map();
328 let signing_keys = (0..num_validators)
329 .map(|index| {
330 let pubkey_affine = rank_map.get_pubkey_stake_entry(index).unwrap().bls_pubkey;
331 keypair_map
332 .get(&BLSPubkeyCompressed::from(*pubkey_affine))
333 .unwrap()
334 })
335 .collect::<Vec<_>>();
336
337 let block_id = Hash::new_unique();
338 let notar_vote = Vote::new_notarization_vote(Block {
339 slot: reward_slot,
340 block_id,
341 });
342 let notar_votes = (0..num_notar_validators)
343 .map(|rank| new_vote_msg(notar_vote, rank, signing_keys[rank], shred_version))
344 .collect::<Vec<_>>();
345 let (signature, bitmap) = build_sig_bitmap(¬ar_votes);
346 let notar_reward_cert =
347 NotarRewardCertificate::try_new(reward_slot, block_id, signature, bitmap).unwrap();
348
349 let skip_vote = Vote::new_skip_vote(reward_slot);
350 let skip_votes = (num_notar_validators..num_validators)
351 .map(|rank| new_vote_msg(skip_vote, rank, signing_keys[rank], shred_version))
352 .collect::<Vec<_>>();
353 let (signature, bitmap) = build_sig_bitmap(&skip_votes);
354 let skip_reward_cert =
355 SkipRewardCertificate::try_new(reward_slot, signature, bitmap).unwrap();
356
357 let validated_reward_cert = ValidatedRewardCert::try_new(
358 &bank,
359 shred_version,
360 &Some(skip_reward_cert),
361 &Some(notar_reward_cert),
362 )
363 .unwrap()
364 .unwrap();
365 assert_eq!(validated_reward_cert.validators.len(), num_validators);
366 }
367}