Skip to main content

solana_runtime/
validated_reward_certificate.rs

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/// Different types of errors that can happen when trying to construct a [`ValidatedRewardCert`].
18#[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
37/// Extracts the slot corresponding to the provided reward certs.
38///
39/// Returns Ok(None) if no certs were provided.
40/// Returns Error if the reward slot is invalid.
41fn 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/// Struct built by validating incoming reward certs.
75#[derive(Debug, Clone)]
76pub struct ValidatedRewardCert {
77    /// List of validators that were present in the reward certs.
78    validators: HashSet<Pubkey>,
79    /// The slot the reward certs refer to
80    reward_slot: Slot,
81}
82
83impl ValidatedRewardCert {
84    /// If validation of the provided reward certs succeeds, returns an instance of [`ValidatedRewardCert`].
85    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                &notar.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    /// Constructs a [`ValidatedRewardCert`] for a block produced locally.
143    ///
144    /// The leader-side reward certificate builder receives verified votes and
145    /// tracks the validator set while aggregating them, so block production
146    /// only needs the reward slot and validator set for bank reward
147    /// calculation.
148    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(&notar_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}