1#[cfg(feature = "dev-context-only-utils")]
2use qualifier_attr::qualifiers;
3use {
4 crate::{stake_history::StakeHistory, stakes::SerdeStakesToStakeFormat},
5 solana_bls_signatures::pubkey::{
6 PopVerified, PubkeyAffine as BLSPubkeyAffine, PubkeyCompressed as BLSPubkeyCompressed,
7 },
8 solana_clock::Epoch,
9 solana_pubkey::Pubkey,
10 solana_stake_interface::state::Stake,
11 solana_vote::vote_account::{VoteAccounts, VoteAccountsHashMap},
12 solana_vote_interface::state::BLS_PUBLIC_KEY_COMPRESSED_SIZE,
13 std::{
14 collections::HashMap,
15 num::NonZero,
16 sync::{Arc, OnceLock},
17 },
18 wincode::{SchemaRead, SchemaWrite, WriteResult, adapter::DiscardSeq, len::BincodeLen},
19};
20
21pub type NodeIdToVoteAccounts = HashMap<Pubkey, NodeVoteAccounts>;
22pub type EpochAuthorizedVoters = HashMap<Pubkey, Pubkey>;
23
24#[derive(Clone, Debug)]
27#[cfg_attr(feature = "dev-context-only-utils", derive(PartialEq))]
28pub struct BLSPubkeyStakeEntry {
29 pub vote_account_pubkey: Pubkey,
31 pub node_pubkey: Pubkey,
33 pub bls_pubkey: PopVerified<BLSPubkeyAffine>,
35 pub stake: NonZero<u64>,
37}
38
39#[derive(Clone, Debug)]
44#[cfg_attr(feature = "dev-context-only-utils", derive(PartialEq))]
45pub struct BLSPubkeyToRankMap {
46 vote_pubkey_to_rank: HashMap<Pubkey, u16>,
48 sorted_pubkeys: Vec<BLSPubkeyStakeEntry>,
50 node_pubkey_to_rank: HashMap<Pubkey, u16>,
52 total_stake: NonZero<u64>,
54}
55
56pub(crate) fn bls_pubkey_compressed_bytes_to_bls_pubkey(
57 bls_pubkey_compressed_bytes: [u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE],
58) -> Option<(BLSPubkeyCompressed, PopVerified<BLSPubkeyAffine>)> {
59 let bls_pubkey_compressed: BLSPubkeyCompressed =
60 wincode::deserialize(&bls_pubkey_compressed_bytes).ok()?;
61 let bls_pubkey_affine = BLSPubkeyAffine::try_from(bls_pubkey_compressed).ok()?;
62 let bls_pubkey_pop_verified = unsafe { PopVerified::new_unchecked(bls_pubkey_affine) };
65 Some((bls_pubkey_compressed, bls_pubkey_pop_verified))
66}
67
68impl BLSPubkeyToRankMap {
69 pub fn new(epoch_vote_accounts_hash_map: &VoteAccountsHashMap) -> Self {
70 let mut candidates = Vec::with_capacity(epoch_vote_accounts_hash_map.len());
71 let mut bls_pubkey_counts = HashMap::new();
72 let mut node_pubkey_counts = HashMap::new();
73 for (&vote_account_pubkey, (stake, account)) in epoch_vote_accounts_hash_map {
74 let Some(stake) = NonZero::new(*stake) else {
75 continue;
76 };
77 let node_pubkey = *account.vote_state_view().node_pubkey();
78 let Some((bls_pubkey_compressed, bls_pubkey)) = account
79 .vote_state_view()
80 .bls_pubkey_compressed()
81 .and_then(bls_pubkey_compressed_bytes_to_bls_pubkey)
82 else {
83 continue;
84 };
85 let entry = BLSPubkeyStakeEntry {
86 vote_account_pubkey,
87 node_pubkey,
88 bls_pubkey,
89 stake,
90 };
91 *bls_pubkey_counts.entry(bls_pubkey_compressed).or_insert(0) += 1;
92 *node_pubkey_counts.entry(node_pubkey).or_insert(0) += 1;
93 candidates.push((entry, bls_pubkey_compressed));
94 }
95 let mut keys_stake_entry_with_compressed: Vec<(BLSPubkeyStakeEntry, BLSPubkeyCompressed)> =
96 candidates
97 .into_iter()
98 .filter_map(|(entry, bls_pubkey_compressed)| {
99 (bls_pubkey_counts[&bls_pubkey_compressed] == 1
100 && node_pubkey_counts[&entry.node_pubkey] == 1)
101 .then_some((entry, bls_pubkey_compressed))
102 })
103 .collect();
104 let total_stake = keys_stake_entry_with_compressed
105 .iter()
106 .fold(0u64, |stake, (entry, _)| {
107 stake.saturating_add(entry.stake.get())
108 });
109 let total_stake = NonZero::new(total_stake).expect("total stakes should not be 0");
110 keys_stake_entry_with_compressed.sort_by(
111 |(a_entry, a_pubkey_compressed), (b_entry, b_pubkey_compressed)| {
112 b_entry
113 .stake
114 .cmp(&a_entry.stake)
115 .then(a_pubkey_compressed.cmp(b_pubkey_compressed))
116 },
117 );
118 let mut sorted_pubkeys = Vec::with_capacity(keys_stake_entry_with_compressed.len());
119 let mut vote_pubkey_to_rank_map =
120 HashMap::with_capacity(keys_stake_entry_with_compressed.len());
121 let mut node_pubkey_to_rank =
122 HashMap::with_capacity(keys_stake_entry_with_compressed.len());
123 for (rank, (entry, _bls_pubkey_compressed)) in
124 keys_stake_entry_with_compressed.into_iter().enumerate()
125 {
126 let rank = u16::try_from(rank).expect("BLS validator rank must fit in u16");
127 vote_pubkey_to_rank_map.insert(entry.vote_account_pubkey, rank);
128 node_pubkey_to_rank.insert(entry.node_pubkey, rank);
129 sorted_pubkeys.push(entry);
130 }
131 Self {
132 vote_pubkey_to_rank: vote_pubkey_to_rank_map,
133 sorted_pubkeys,
134 total_stake,
135 node_pubkey_to_rank,
136 }
137 }
138
139 pub fn is_empty(&self) -> bool {
140 self.sorted_pubkeys.is_empty()
141 }
142
143 pub fn len(&self) -> usize {
144 self.sorted_pubkeys.len()
145 }
146
147 pub fn total_stake(&self) -> NonZero<u64> {
148 self.total_stake
149 }
150
151 pub fn get_rank_for_vote_pubkey(&self, vote_pubkey: &Pubkey) -> Option<&u16> {
152 self.vote_pubkey_to_rank.get(vote_pubkey)
153 }
154
155 pub fn get_pubkey_stake_entry(&self, index: usize) -> Option<&BLSPubkeyStakeEntry> {
156 self.sorted_pubkeys.get(index)
157 }
158
159 #[inline]
161 pub fn get_ranked_entry_for_node(
162 &self,
163 node_pubkey: &Pubkey,
164 ) -> Option<(u16, &BLSPubkeyStakeEntry)> {
165 let rank = *self.node_pubkey_to_rank.get(node_pubkey)?;
166 let entry = self.sorted_pubkeys.get(usize::from(rank))?;
167 Some((rank, entry))
168 }
169}
170
171#[cfg_attr(feature = "frozen-abi", derive(StableAbi, StableAbiSample))]
172#[derive(Clone, Debug, Default, PartialEq, Eq, SchemaRead, SchemaWrite)]
173pub struct NodeVoteAccounts {
174 pub vote_accounts: Vec<Pubkey>,
175 pub total_stake: u64,
176}
177
178#[cfg_attr(
183 feature = "frozen-abi",
184 derive(SchemaWrite, StableAbi, StableAbiSample)
185)]
186#[derive(Clone, Debug, SchemaRead)]
187#[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))]
188pub(crate) enum DeserializableVersionedEpochStakes {
189 Current {
190 #[cfg_attr(
191 feature = "frozen-abi",
192 stable_abi_sample(with = "stable_abi_sample_deserializable_epoch_stakes(rng)")
193 )]
194 stakes: DeserializableEpochStakes,
195 total_stake: u64,
196 node_id_to_vote_accounts: NodeIdToVoteAccounts,
197 epoch_authorized_voters: EpochAuthorizedVoters,
198 },
199}
200
201#[cfg(feature = "frozen-abi")]
204fn stable_abi_sample_deserializable_epoch_stakes(
205 rng: &mut (impl solana_frozen_abi::rand::RngCore + ?Sized),
206) -> DeserializableEpochStakes {
207 use solana_frozen_abi::stable_abi::StableAbi;
208 let epoch = Epoch::random(rng);
209 let vote_accounts = VoteAccounts::random(rng);
210 let stake_history = StakeHistory::random(rng);
211 DeserializableEpochStakes {
212 vote_accounts,
213 _stake_delegations: Vec::new(),
214 _unused: 0,
215 epoch,
216 stake_history,
217 }
218}
219
220#[derive(Clone, Debug, SchemaWrite)]
221#[cfg_attr(feature = "frozen-abi", derive(StableAbi, StableAbiSample))]
222#[cfg_attr(feature = "dev-context-only-utils", derive(PartialEq))]
223pub enum VersionedEpochStakes {
224 Current {
225 stakes: EpochStakes,
226 total_stake: u64,
228 node_id_to_vote_accounts: Arc<NodeIdToVoteAccounts>,
229 epoch_authorized_voters: Arc<EpochAuthorizedVoters>,
230 #[cfg_attr(feature = "frozen-abi", stable_abi_sample(with = "Default::default()"))]
231 #[wincode(skip)]
232 bls_pubkey_to_rank_map: OnceLock<Arc<BLSPubkeyToRankMap>>,
233 },
234}
235
236impl From<DeserializableVersionedEpochStakes> for VersionedEpochStakes {
237 fn from(epoch_stakes: DeserializableVersionedEpochStakes) -> Self {
238 let DeserializableVersionedEpochStakes::Current {
239 stakes,
240 total_stake,
241 node_id_to_vote_accounts,
242 epoch_authorized_voters,
243 } = epoch_stakes;
244 Self::Current {
245 stakes: stakes.into(),
246 total_stake,
247 node_id_to_vote_accounts: Arc::new(node_id_to_vote_accounts),
248 epoch_authorized_voters: Arc::new(epoch_authorized_voters),
249 bls_pubkey_to_rank_map: OnceLock::new(),
250 }
251 }
252}
253
254impl VersionedEpochStakes {
255 #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))]
256 pub(crate) fn new(stakes: SerdeStakesToStakeFormat, leader_schedule_epoch: Epoch) -> Self {
257 let stakes = EpochStakes::from(stakes);
258 let epoch_vote_accounts = stakes.vote_accounts();
259 let (total_stake, node_id_to_vote_accounts, epoch_authorized_voters) =
260 Self::parse_epoch_vote_accounts(epoch_vote_accounts.as_ref(), leader_schedule_epoch);
261 Self::Current {
262 stakes,
263 total_stake,
264 node_id_to_vote_accounts: Arc::new(node_id_to_vote_accounts),
265 epoch_authorized_voters: Arc::new(epoch_authorized_voters),
266 bls_pubkey_to_rank_map: OnceLock::new(),
267 }
268 }
269
270 #[cfg(feature = "dev-context-only-utils")]
271 pub fn new_for_tests(
272 vote_accounts_hash_map: VoteAccountsHashMap,
273 leader_schedule_epoch: Epoch,
274 ) -> Self {
275 Self::new(
276 SerdeStakesToStakeFormat::Account(crate::stakes::Stakes::new_for_tests(
277 0,
278 solana_vote::vote_account::VoteAccounts::from(Arc::new(vote_accounts_hash_map)),
279 imbl::HashMap::default(),
280 )),
281 leader_schedule_epoch,
282 )
283 }
284
285 pub fn stakes(&self) -> &EpochStakes {
286 match self {
287 Self::Current { stakes, .. } => stakes,
288 }
289 }
290
291 pub fn total_stake(&self) -> u64 {
293 match self {
294 Self::Current { total_stake, .. } => *total_stake,
295 }
296 }
297
298 #[cfg(feature = "dev-context-only-utils")]
299 pub fn set_total_stake(&mut self, total_stake: u64) {
300 match self {
301 Self::Current {
302 total_stake: total_stake_field,
303 ..
304 } => {
305 *total_stake_field = total_stake;
306 }
307 }
308 }
309
310 pub fn node_id_to_vote_accounts(&self) -> &Arc<NodeIdToVoteAccounts> {
311 match self {
312 Self::Current {
313 node_id_to_vote_accounts,
314 ..
315 } => node_id_to_vote_accounts,
316 }
317 }
318
319 pub fn node_id_to_stake(&self, node_id: &Pubkey) -> Option<u64> {
320 self.node_id_to_vote_accounts()
321 .get(node_id)
322 .map(|x| x.total_stake)
323 }
324
325 pub fn epoch_authorized_voters(&self) -> &Arc<EpochAuthorizedVoters> {
326 match self {
327 Self::Current {
328 epoch_authorized_voters,
329 ..
330 } => epoch_authorized_voters,
331 }
332 }
333
334 pub fn bls_pubkey_to_rank_map(&self) -> &Arc<BLSPubkeyToRankMap> {
335 match self {
336 Self::Current {
337 bls_pubkey_to_rank_map,
338 ..
339 } => bls_pubkey_to_rank_map.get_or_init(|| {
340 Arc::new(BLSPubkeyToRankMap::new(
341 self.stakes().vote_accounts().as_ref(),
342 ))
343 }),
344 }
345 }
346
347 pub fn vote_account_stake(&self, vote_account: &Pubkey) -> u64 {
349 self.stakes()
350 .vote_accounts()
351 .get_delegated_stake(vote_account)
352 }
353
354 fn parse_epoch_vote_accounts(
355 epoch_vote_accounts: &VoteAccountsHashMap,
356 leader_schedule_epoch: Epoch,
357 ) -> (u64, NodeIdToVoteAccounts, EpochAuthorizedVoters) {
358 let mut node_id_to_vote_accounts: NodeIdToVoteAccounts = HashMap::new();
359 let mut epoch_authorized_voters: EpochAuthorizedVoters = HashMap::new();
360 let mut total_stake: u64 = 0;
361
362 for (key, (stake, account)) in epoch_vote_accounts.iter() {
363 total_stake += *stake;
364
365 if *stake == 0 {
366 continue;
367 }
368
369 let vote_state = account.vote_state_view();
370
371 if let Some(authorized_voter) = vote_state.get_authorized_voter(leader_schedule_epoch) {
372 let node_vote_accounts = node_id_to_vote_accounts
373 .entry(*vote_state.node_pubkey())
374 .or_default();
375
376 node_vote_accounts.total_stake += stake;
377 node_vote_accounts.vote_accounts.push(*key);
378
379 epoch_authorized_voters.insert(*key, *authorized_voter);
380 }
381 }
382
383 (
384 total_stake,
385 node_id_to_vote_accounts,
386 epoch_authorized_voters,
387 )
388 }
389}
390
391#[derive(Clone, Debug, Default)]
393#[cfg_attr(feature = "frozen-abi", derive(StableAbi, StableAbiSample))]
394#[cfg_attr(feature = "dev-context-only-utils", derive(PartialEq))]
395pub struct EpochStakes {
396 epoch: Epoch,
397 vote_accounts: VoteAccounts,
398 stake_history: StakeHistory,
399}
400
401impl EpochStakes {
402 pub fn vote_accounts(&self) -> &VoteAccounts {
403 &self.vote_accounts
404 }
405 pub fn staked_nodes(&self) -> Arc<HashMap<Pubkey, u64>> {
406 self.vote_accounts.staked_nodes()
407 }
408}
409
410#[derive(SchemaWrite)]
416struct SerializableEpochStakes<'a> {
417 vote_accounts: &'a VoteAccounts,
418 stake_delegations: Vec<(Pubkey, Stake)>,
419 unused: u64,
420 epoch: Epoch,
421 stake_history: &'a StakeHistory,
422}
423
424impl<'a> From<&'a EpochStakes> for SerializableEpochStakes<'a> {
425 fn from(epoch_stakes: &'a EpochStakes) -> Self {
426 Self {
427 vote_accounts: &epoch_stakes.vote_accounts,
428 stake_delegations: Vec::new(), unused: 0,
430 epoch: epoch_stakes.epoch,
431 stake_history: &epoch_stakes.stake_history,
432 }
433 }
434}
435
436unsafe impl<C: wincode::config::Config> SchemaWrite<C> for EpochStakes {
438 type Src = Self;
439
440 fn size_of(src: &Self::Src) -> WriteResult<usize> {
441 <SerializableEpochStakes<'_> as SchemaWrite<C>>::size_of(&src.into())
442 }
443
444 fn write(writer: impl wincode::io::Writer, src: &Self::Src) -> WriteResult<()> {
445 <SerializableEpochStakes<'_> as SchemaWrite<C>>::write(writer, &src.into())
446 }
447}
448
449impl From<SerdeStakesToStakeFormat> for EpochStakes {
450 fn from(stakes: SerdeStakesToStakeFormat) -> Self {
451 let (epoch, vote_accounts, stake_history) = match stakes {
452 SerdeStakesToStakeFormat::Stake(stakes) => stakes.into_epoch_stakes_fields(),
453 SerdeStakesToStakeFormat::Account(stakes) => stakes.into_epoch_stakes_fields(),
454 };
455 Self {
456 epoch,
457 vote_accounts,
458 stake_history,
459 }
460 }
461}
462
463#[cfg_attr(feature = "frozen-abi", derive(SchemaWrite))]
469#[derive(Clone, Debug, SchemaRead)]
470#[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))]
471pub(crate) struct DeserializableEpochStakes {
472 vote_accounts: VoteAccounts,
473 #[wincode(with = "DiscardSeq<(Pubkey, Stake), BincodeLen>")]
475 _stake_delegations: Vec<(Pubkey, Stake)>,
476 _unused: u64,
477 epoch: Epoch,
478 stake_history: StakeHistory,
479}
480
481impl From<DeserializableEpochStakes> for EpochStakes {
482 fn from(stakes: DeserializableEpochStakes) -> Self {
483 let DeserializableEpochStakes {
484 vote_accounts,
485 _stake_delegations: _,
486 _unused: _,
487 epoch,
488 stake_history,
489 } = stakes;
490 Self {
491 epoch,
492 vote_accounts,
493 stake_history,
494 }
495 }
496}
497
498#[cfg(test)]
499pub(crate) mod tests {
500 use {
501 super::*,
502 crate::{
503 serde_snapshot::deserialize_wincode_from, stake_account::StakeAccount, stakes::Stakes,
504 },
505 solana_account::AccountSharedData,
506 solana_bls_signatures::keypair::Keypair as BLSKeypair,
507 solana_rent::Rent,
508 solana_vote::vote_account::VoteAccount,
509 solana_vote_program::vote_state::create_v4_account_with_authorized,
510 std::iter,
511 test_case::test_case,
512 };
513
514 struct VoteAccountInfo {
515 vote_account: Pubkey,
516 account: AccountSharedData,
517 authorized_voter: Pubkey,
518 }
519
520 fn new_vote_accounts(
521 num_nodes: usize,
522 num_vote_accounts_per_node: usize,
523 is_alpenglow: bool,
524 ) -> HashMap<Pubkey, Vec<VoteAccountInfo>> {
525 (0..num_nodes)
527 .map(|_| {
528 let node_id = solana_pubkey::new_rand();
529 (
530 node_id,
531 iter::repeat_with(|| {
532 let authorized_voter = solana_pubkey::new_rand();
533 let bls_pubkey_compressed: BLSPubkeyCompressed =
534 (*BLSKeypair::new().public).into();
535 let bls_pubkey_compressed_serialized =
536 wincode::serialize(&bls_pubkey_compressed)
537 .unwrap()
538 .try_into()
539 .unwrap();
540
541 let bls_pubkey = if is_alpenglow {
542 bls_pubkey_compressed_serialized
543 } else {
544 [0u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE]
545 };
546 let account = create_v4_account_with_authorized(
547 &node_id,
548 &authorized_voter,
549 bls_pubkey,
550 &node_id,
551 0,
552 &node_id,
553 0,
554 &node_id,
555 100,
556 );
557 VoteAccountInfo {
558 vote_account: solana_pubkey::new_rand(),
559 account,
560 authorized_voter,
561 }
562 })
563 .take(num_vote_accounts_per_node)
564 .collect(),
565 )
566 })
567 .collect()
568 }
569
570 fn new_epoch_vote_accounts(
571 vote_accounts_map: &HashMap<Pubkey, Vec<VoteAccountInfo>>,
572 node_id_to_stake_fn: impl Fn(&Pubkey) -> u64,
573 ) -> VoteAccountsHashMap {
574 vote_accounts_map
576 .iter()
577 .flat_map(|(node_id, vote_accounts)| {
578 vote_accounts.iter().map(|v| {
579 let vote_account = VoteAccount::try_from(v.account.clone()).unwrap();
580 (v.vote_account, (node_id_to_stake_fn(node_id), vote_account))
581 })
582 })
583 .collect()
584 }
585
586 #[test_case(true; "alpenglow")]
587 #[test_case(false; "towerbft")]
588 fn test_parse_epoch_vote_accounts(is_alpenglow: bool) {
589 let stake_per_account = 100;
590 let num_vote_accounts_per_node = 2;
591 let num_nodes = 10;
592
593 let vote_accounts_map =
594 new_vote_accounts(num_nodes, num_vote_accounts_per_node, is_alpenglow);
595
596 let expected_authorized_voters: HashMap<_, _> = vote_accounts_map
597 .values()
598 .flat_map(|vote_accounts| {
599 vote_accounts
600 .iter()
601 .map(|v| (v.vote_account, v.authorized_voter))
602 })
603 .collect();
604
605 let expected_node_id_to_vote_accounts: HashMap<_, _> = vote_accounts_map
606 .iter()
607 .map(|(node_pubkey, vote_accounts)| {
608 let mut vote_accounts = vote_accounts
609 .iter()
610 .map(|v| v.vote_account)
611 .collect::<Vec<_>>();
612 vote_accounts.sort();
613 let node_vote_accounts = NodeVoteAccounts {
614 vote_accounts,
615 total_stake: stake_per_account * num_vote_accounts_per_node as u64,
616 };
617 (*node_pubkey, node_vote_accounts)
618 })
619 .collect();
620
621 let epoch_vote_accounts =
622 new_epoch_vote_accounts(&vote_accounts_map, |_| stake_per_account);
623
624 let (total_stake, mut node_id_to_vote_accounts, epoch_authorized_voters) =
625 VersionedEpochStakes::parse_epoch_vote_accounts(&epoch_vote_accounts, 0);
626
627 node_id_to_vote_accounts
629 .iter_mut()
630 .for_each(|(_, node_vote_accounts)| node_vote_accounts.vote_accounts.sort());
631
632 assert!(
633 node_id_to_vote_accounts.len() == expected_node_id_to_vote_accounts.len()
634 && node_id_to_vote_accounts
635 .iter()
636 .all(|(k, v)| expected_node_id_to_vote_accounts.get(k).unwrap() == v)
637 );
638 assert!(
639 epoch_authorized_voters.len() == expected_authorized_voters.len()
640 && epoch_authorized_voters
641 .iter()
642 .all(|(k, v)| expected_authorized_voters.get(k).unwrap() == v)
643 );
644 assert_eq!(
645 total_stake,
646 num_nodes as u64 * num_vote_accounts_per_node as u64 * 100
647 );
648 }
649
650 #[test_case(true; "alpenglow")]
651 #[test_case(false; "towerbft")]
652 fn test_node_id_to_stake(is_alpenglow: bool) {
653 let num_nodes = 10;
654 let num_vote_accounts_per_node = 2;
655
656 let vote_accounts_map =
657 new_vote_accounts(num_nodes, num_vote_accounts_per_node, is_alpenglow);
658 let node_id_to_stake_map = vote_accounts_map
659 .keys()
660 .enumerate()
661 .map(|(index, node_id)| (*node_id, ((index + 1) * 100) as u64))
662 .collect::<HashMap<_, _>>();
663 let epoch_vote_accounts = new_epoch_vote_accounts(&vote_accounts_map, |node_id| {
664 *node_id_to_stake_map.get(node_id).unwrap()
665 });
666 let epoch_stakes = VersionedEpochStakes::new_for_tests(epoch_vote_accounts, 0);
667
668 assert_eq!(epoch_stakes.total_stake(), 11000);
669 for (node_id, stake) in node_id_to_stake_map.iter() {
670 assert_eq!(
671 epoch_stakes.node_id_to_stake(node_id),
672 Some(*stake * num_vote_accounts_per_node as u64)
673 );
674 }
675 }
676
677 #[test]
678 fn test_bls_pubkey_rank_map() {
679 agave_logger::setup();
680 let num_nodes = 10;
681 let num_vote_accounts = num_nodes;
682
683 let vote_accounts_map = new_vote_accounts(num_nodes, 1, true);
684 let node_id_to_stake_map = vote_accounts_map
685 .keys()
686 .enumerate()
687 .map(|(index, node_id)| (*node_id, ((index + 1) * 100) as u64))
688 .collect::<HashMap<_, _>>();
689 let epoch_vote_accounts = new_epoch_vote_accounts(&vote_accounts_map, |node_id| {
690 *node_id_to_stake_map.get(node_id).unwrap()
691 });
692 let epoch_stakes = VersionedEpochStakes::new_for_tests(epoch_vote_accounts.clone(), 0);
693 let bls_pubkey_to_rank_map = epoch_stakes.bls_pubkey_to_rank_map();
694 let expected_num_vote_accounts = num_vote_accounts;
695 assert_eq!(bls_pubkey_to_rank_map.len(), expected_num_vote_accounts);
696 let expected_total_stake = epoch_stakes.total_stake();
697 assert_eq!(
698 bls_pubkey_to_rank_map.total_stake().get(),
699 expected_total_stake
700 );
701 for expected_rank in 0..bls_pubkey_to_rank_map.len() {
702 let expected_entry = bls_pubkey_to_rank_map
703 .get_pubkey_stake_entry(expected_rank)
704 .unwrap();
705 let (rank, entry) = bls_pubkey_to_rank_map
706 .get_ranked_entry_for_node(&expected_entry.node_pubkey)
707 .unwrap();
708 assert_eq!(usize::from(rank), expected_rank);
709 assert!(std::ptr::eq(entry, expected_entry));
710 assert_eq!(
711 bls_pubkey_to_rank_map
712 .get_rank_for_vote_pubkey(&entry.vote_account_pubkey)
713 .copied(),
714 Some(rank)
715 );
716 }
717 assert!(
718 bls_pubkey_to_rank_map
719 .get_ranked_entry_for_node(&Pubkey::new_unique())
720 .is_none()
721 );
722
723 let mut bank_epoch_stakes = HashMap::new();
725 bank_epoch_stakes.insert(0, epoch_stakes.clone());
726 let epoch_stakes = bank_epoch_stakes
727 .get(&0)
728 .expect("Epoch stakes should exist");
729 let bls_pubkey_to_rank_map2 = epoch_stakes.bls_pubkey_to_rank_map();
730 assert_eq!(bls_pubkey_to_rank_map2, bls_pubkey_to_rank_map);
731 }
732
733 #[test]
734 #[should_panic(expected = "total stakes should not be 0")]
735 fn test_multiple_vote_accounts_panics() {
736 agave_logger::setup();
737 let num_nodes = 10;
738
739 let vote_accounts_map = new_vote_accounts(num_nodes, 2, true);
740 let node_id_to_stake_map = vote_accounts_map
741 .keys()
742 .enumerate()
743 .map(|(index, node_id)| (*node_id, ((index + 1) * 100) as u64))
744 .collect::<HashMap<_, _>>();
745 let epoch_vote_accounts = new_epoch_vote_accounts(&vote_accounts_map, |node_id| {
746 *node_id_to_stake_map.get(node_id).unwrap()
747 });
748 let epoch_stakes = VersionedEpochStakes::new_for_tests(epoch_vote_accounts.clone(), 0);
749 epoch_stakes.bls_pubkey_to_rank_map();
750 }
751
752 #[test]
753 fn test_bls_pubkey_rank_map_excludes_duplicate_bls_and_identity() {
754 let new_bls_pubkey = || {
755 let compressed: BLSPubkeyCompressed = (*BLSKeypair::new().public).into();
756 wincode::serialize(&compressed).unwrap().try_into().unwrap()
757 };
758
759 let duplicate_bls_pubkey_serialized = new_bls_pubkey();
760 let duplicate_node_bls_pubkey_serialized = new_bls_pubkey();
761 let duplicate_node_bls_pubkey_serialized_2 = new_bls_pubkey();
762 let shared_voter_bls_pubkey_serialized = new_bls_pubkey();
763 let shared_voter_bls_pubkey_serialized_2 = new_bls_pubkey();
764 let unique_bls_pubkey_serialized = new_bls_pubkey();
765
766 let duplicate_bls_vote_pubkey = Pubkey::new_unique();
767 let duplicate_bls_vote_pubkey_2 = Pubkey::new_unique();
768 let duplicate_node_vote_pubkey = Pubkey::new_unique();
769 let duplicate_node_vote_pubkey_2 = Pubkey::new_unique();
770 let shared_voter_vote_pubkey = Pubkey::new_unique();
771 let shared_voter_vote_pubkey_2 = Pubkey::new_unique();
772 let unique_vote_pubkey = Pubkey::new_unique();
773
774 let duplicate_node_pubkey = Pubkey::new_unique();
775 let shared_authorized_voter = Pubkey::new_unique();
776 let shared_voter_node_pubkey = Pubkey::new_unique();
777 let shared_voter_node_pubkey_2 = Pubkey::new_unique();
778 let unique_node_pubkey = Pubkey::new_unique();
779 let unique_voter = Pubkey::new_unique();
780
781 let account = |node_pubkey, authorized_voter, bls_pubkey| {
782 VoteAccount::try_from(create_v4_account_with_authorized(
783 &node_pubkey,
784 &authorized_voter,
785 bls_pubkey,
786 &node_pubkey,
787 0,
788 &node_pubkey,
789 0,
790 &node_pubkey,
791 100,
792 ))
793 .unwrap()
794 };
795 let epoch_vote_accounts = VoteAccountsHashMap::from([
796 (
797 duplicate_bls_vote_pubkey,
798 (
799 100,
800 account(
801 Pubkey::new_unique(),
802 Pubkey::new_unique(),
803 duplicate_bls_pubkey_serialized,
804 ),
805 ),
806 ),
807 (
808 duplicate_bls_vote_pubkey_2,
809 (
810 100,
811 account(
812 Pubkey::new_unique(),
813 Pubkey::new_unique(),
814 duplicate_bls_pubkey_serialized,
815 ),
816 ),
817 ),
818 (
819 duplicate_node_vote_pubkey,
820 (
821 100,
822 account(
823 duplicate_node_pubkey,
824 Pubkey::new_unique(),
825 duplicate_node_bls_pubkey_serialized,
826 ),
827 ),
828 ),
829 (
830 duplicate_node_vote_pubkey_2,
831 (
832 100,
833 account(
834 duplicate_node_pubkey,
835 Pubkey::new_unique(),
836 duplicate_node_bls_pubkey_serialized_2,
837 ),
838 ),
839 ),
840 (
841 shared_voter_vote_pubkey,
842 (
843 100,
844 account(
845 shared_voter_node_pubkey,
846 shared_authorized_voter,
847 shared_voter_bls_pubkey_serialized,
848 ),
849 ),
850 ),
851 (
852 shared_voter_vote_pubkey_2,
853 (
854 100,
855 account(
856 shared_voter_node_pubkey_2,
857 shared_authorized_voter,
858 shared_voter_bls_pubkey_serialized_2,
859 ),
860 ),
861 ),
862 (
863 unique_vote_pubkey,
864 (
865 50,
866 account(
867 unique_node_pubkey,
868 unique_voter,
869 unique_bls_pubkey_serialized,
870 ),
871 ),
872 ),
873 ]);
874
875 let rank_map = BLSPubkeyToRankMap::new(&epoch_vote_accounts);
876
877 assert_eq!(rank_map.len(), 3);
878 assert_eq!(rank_map.total_stake().get(), 250);
879 for vote_pubkey in [
880 duplicate_bls_vote_pubkey,
881 duplicate_bls_vote_pubkey_2,
882 duplicate_node_vote_pubkey,
883 duplicate_node_vote_pubkey_2,
884 ] {
885 assert!(rank_map.get_rank_for_vote_pubkey(&vote_pubkey).is_none());
886 }
887 assert!(
888 rank_map
889 .get_ranked_entry_for_node(&duplicate_node_pubkey)
890 .is_none()
891 );
892 for node_pubkey in [
893 shared_voter_node_pubkey,
894 shared_voter_node_pubkey_2,
895 unique_node_pubkey,
896 ] {
897 let (rank, entry) = rank_map.get_ranked_entry_for_node(&node_pubkey).unwrap();
898 assert_eq!(entry.node_pubkey, node_pubkey);
899 assert_eq!(
900 rank_map
901 .get_rank_for_vote_pubkey(&entry.vote_account_pubkey)
902 .copied(),
903 Some(rank)
904 );
905 }
906 assert!(rank_map.get_pubkey_stake_entry(rank_map.len()).is_none());
907 }
908
909 #[test]
910 fn test_versioned_epoch_stakes_does_not_serialize_delegations() {
911 #[derive(SchemaRead)]
913 enum SerializedVersionedEpochStakes {
914 Current {
915 stakes: SerializedEpochStakes,
916 total_stake: u64,
917 node_id_to_vote_accounts: NodeIdToVoteAccounts,
918 epoch_authorized_voters: EpochAuthorizedVoters,
919 },
920 }
921 #[derive(SchemaRead)]
922 struct SerializedEpochStakes {
923 vote_accounts: VoteAccounts,
924 stake_delegations: Vec<(Pubkey, Stake)>,
925 unused: u64,
926 epoch: Epoch,
927 stake_history: StakeHistory,
928 }
929
930 let epoch = 42;
931 let delegated_amount = 456_789;
932 let rent = Rent::default();
933 let ((vote_pubkey, vote_account), (stake_pubkey, stake_account)) =
934 crate::stakes::tests::create_staked_node_accounts(123, &rent);
935 let vote_account = VoteAccount::try_from(vote_account).unwrap();
936 let vote_accounts = VoteAccounts::from(Arc::new(HashMap::from([(
937 vote_pubkey,
938 (delegated_amount, vote_account),
939 )])));
940 let stake_account = StakeAccount::try_from(stake_account).unwrap();
941 let stakes = Stakes::new_for_tests(
942 epoch,
943 vote_accounts,
944 imbl::HashMap::from_iter([(stake_pubkey, stake_account)]),
945 );
946
947 assert!(!stakes.stake_delegations().is_empty());
949
950 let epoch_stakes = VersionedEpochStakes::new(SerdeStakesToStakeFormat::Account(stakes), 0);
951
952 assert_eq!(
953 epoch_stakes
954 .stakes()
955 .vote_accounts()
956 .get_delegated_stake(&vote_pubkey),
957 delegated_amount,
958 );
959
960 let serialized_bytes = wincode::serialize(&epoch_stakes).unwrap();
961 let serialized_epoch_stakes: SerializedVersionedEpochStakes =
962 wincode::deserialize(&serialized_bytes).unwrap();
963 match serialized_epoch_stakes {
964 SerializedVersionedEpochStakes::Current {
965 stakes,
966 total_stake,
967 node_id_to_vote_accounts,
968 epoch_authorized_voters,
969 } => {
970 assert_eq!(
971 stakes.vote_accounts.get_delegated_stake(&vote_pubkey),
972 delegated_amount,
973 );
974 assert!(stakes.stake_delegations.is_empty()); assert_eq!(stakes.unused, 0);
976 assert_eq!(stakes.epoch, epoch);
977 assert_eq!(stakes.stake_history, StakeHistory::default());
978 assert_eq!(total_stake, delegated_amount);
979 assert_eq!(node_id_to_vote_accounts.len(), 1);
980 assert_eq!(epoch_authorized_voters.len(), 1);
981 }
982 }
983
984 let deserialized_epoch_stakes: VersionedEpochStakes =
985 deserialize_wincode_from::<_, DeserializableVersionedEpochStakes>(
986 std::io::Cursor::new(&serialized_bytes),
987 )
988 .unwrap()
989 .into();
990 assert_eq!(
991 deserialized_epoch_stakes
992 .stakes()
993 .vote_accounts()
994 .get_delegated_stake(&vote_pubkey),
995 delegated_amount,
996 );
997 }
998}