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                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                &notar.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    /// Constructs a [`ValidatedRewardCert`] for a block produced locally.
145    ///
146    /// The leader-side reward certificate builder receives verified votes and
147    /// tracks the validator set while aggregating them, so block production
148    /// only needs the reward slot and validator set for bank reward
149    /// calculation.
150    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(&notar_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}