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    current_slot: Slot,
43    skip: &Option<SkipRewardCertificate>,
44    notar: &Option<NotarRewardCertificate>,
45) -> Result<Option<Slot>, Error> {
46    let slot = match (skip, notar) {
47        (None, None) => return Ok(None),
48        (Some(s), None) => s.slot,
49        (None, Some(n)) => n.slot,
50        (Some(s), Some(n)) => {
51            if s.slot != n.slot {
52                return Err(Error::InvalidSlotNumbers {
53                    current_slot,
54                    notar_slot: Some(n.slot),
55                    skip_slot: Some(s.slot),
56                });
57            }
58            s.slot
59        }
60    };
61    if slot.saturating_add(NUM_SLOTS_FOR_REWARD) != current_slot {
62        return Err(Error::InvalidSlotNumbers {
63            current_slot,
64            notar_slot: notar.as_ref().map(|c| c.slot),
65            skip_slot: skip.as_ref().map(|c| c.slot),
66        });
67    }
68    Ok(Some(slot))
69}
70
71/// Struct built by validating incoming reward certs.
72#[derive(Debug, Clone)]
73pub struct ValidatedRewardCert {
74    /// List of validators that were present in the reward certs.
75    validators: HashSet<Pubkey>,
76    /// The slot the reward certs refer to
77    reward_slot: Slot,
78}
79
80impl ValidatedRewardCert {
81    /// If validation of the provided reward certs succeeds, returns an instance of [`ValidatedRewardCert`].
82    pub fn try_new(
83        bank: &Bank,
84        shred_version: u16,
85        skip: &Option<SkipRewardCertificate>,
86        notar: &Option<NotarRewardCertificate>,
87    ) -> Result<Option<Self>, Error> {
88        let Some(reward_slot) = extract_slot(bank.slot(), skip, notar)? else {
89            return Ok(None);
90        };
91        let rank_map = bank
92            .epoch_stakes_from_slot(reward_slot)
93            .ok_or(Error::NoRankMap)?
94            .bls_pubkey_to_rank_map();
95        let max_validators = rank_map.len();
96        let mut validators = HashSet::with_capacity(max_validators);
97
98        let mut rank_map = |ind: usize| {
99            rank_map.get_pubkey_stake_entry(ind).map(|entry| {
100                validators.insert(entry.vote_account_pubkey);
101                entry.bls_pubkey
102            })
103        };
104
105        if let Some(skip) = skip {
106            let vote = Vote::new_skip_vote(skip.slot);
107            let payload = get_vote_payload_to_sign(vote, shred_version);
108            verify_base2(
109                &payload,
110                &skip.signature,
111                skip.to_bitmap(),
112                max_validators,
113                &mut rank_map,
114            )?
115        }
116        if let Some(notar) = notar {
117            let vote = Vote::new_notarization_vote(Block {
118                slot: notar.slot,
119                block_id: notar.block_id,
120            });
121            let payload = get_vote_payload_to_sign(vote, shred_version);
122            verify_base2(
123                &payload,
124                &notar.signature,
125                notar.bitmap(),
126                max_validators,
127                rank_map,
128            )?
129        }
130        if validators.is_empty() {
131            return Ok(None);
132        }
133        Ok(Some(Self {
134            validators,
135            reward_slot,
136        }))
137    }
138
139    /// Constructs a [`ValidatedRewardCert`] for a block produced locally.
140    ///
141    /// The leader-side reward certificate builder receives verified votes and
142    /// tracks the validator set while aggregating them, so block production
143    /// only needs the reward slot and validator set for bank reward
144    /// calculation.
145    pub fn try_new_for_leader(
146        current_slot: Slot,
147        skip: &Option<SkipRewardCertificate>,
148        notar: &Option<NotarRewardCertificate>,
149        validators: impl IntoIterator<Item = Pubkey>,
150    ) -> Result<Option<Self>, Error> {
151        let Some(reward_slot) = extract_slot(current_slot, skip, notar)? else {
152            return Ok(None);
153        };
154        let validators: HashSet<_> = validators.into_iter().collect();
155        if validators.is_empty() {
156            return Ok(None);
157        }
158        Ok(Some(Self {
159            validators,
160            reward_slot,
161        }))
162    }
163
164    pub(crate) fn slot(&self) -> Slot {
165        self.reward_slot
166    }
167
168    pub(crate) fn validators(&self) -> &HashSet<Pubkey> {
169        &self.validators
170    }
171
172    #[cfg(test)]
173    pub(crate) fn new_for_tests(reward_slot: Slot, validators: Vec<Pubkey>) -> Self {
174        let validators = validators.into_iter().collect();
175        Self {
176            reward_slot,
177            validators,
178        }
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use {
185        super::*,
186        crate::genesis_utils::{
187            ValidatorVoteKeypairs, create_genesis_config_with_alpenglow_vote_accounts,
188        },
189        agave_votor_messages::consensus_message::VoteMessage,
190        bitvec::vec::BitVec,
191        rand::Rng,
192        solana_bls_signatures::{
193            Keypair as BlsKeypair, Signature as BLSSignature,
194            SignatureCompressed as BlsSignatureCompressed, SignatureProjective,
195            pubkey::PubkeyCompressed as BLSPubkeyCompressed,
196        },
197        solana_hash::Hash,
198        solana_leader_schedule::SlotLeader,
199        solana_signer_store::encode_base2,
200        std::collections::HashMap,
201    };
202
203    fn new_vote(vote: Vote, rank: usize, keypair: &BlsKeypair, shred_version: u16) -> VoteMessage {
204        let payload = get_vote_payload_to_sign(vote, shred_version);
205        let signature = keypair.sign(&payload).into();
206        VoteMessage {
207            vote,
208            signature,
209            rank: rank.try_into().unwrap(),
210        }
211    }
212
213    fn build_sig_bitmap(votes: &[VoteMessage]) -> (BlsSignatureCompressed, Vec<u8>) {
214        let max_rank = votes.last().unwrap().rank;
215        let mut signature = SignatureProjective::identity();
216        let mut bitvec = BitVec::repeat(false, (max_rank + 1) as usize);
217        for vote in votes {
218            signature
219                .aggregate_with(std::iter::once(&vote.signature))
220                .unwrap();
221            bitvec.set(vote.rank as usize, true);
222        }
223        (
224            BLSSignature::from(signature).try_into().unwrap(),
225            encode_base2(&bitvec).unwrap(),
226        )
227    }
228
229    #[test]
230    fn validate_try_new() {
231        let reward_slot = 1;
232        let bank_slot = reward_slot + NUM_SLOTS_FOR_REWARD;
233        let num_skip_validators = 3;
234        let num_notar_validators = 5;
235        let num_validators = num_skip_validators + num_notar_validators;
236
237        let validator_keypairs = (0..num_validators)
238            .map(|_| ValidatorVoteKeypairs::new_rand())
239            .collect::<Vec<_>>();
240        let shred_version = rand::rng().random();
241        let keypair_map = validator_keypairs
242            .iter()
243            .map(|k| {
244                (
245                    BLSPubkeyCompressed::from(*k.bls_keypair.public),
246                    k.bls_keypair.clone(),
247                )
248            })
249            .collect::<HashMap<_, _>>();
250        let genesis = create_genesis_config_with_alpenglow_vote_accounts(
251            1_000_000_000,
252            &validator_keypairs,
253            vec![100; validator_keypairs.len()],
254        );
255        let (bank, _bank_forks) =
256            Bank::new_for_tests(&genesis.genesis_config).wrap_with_bank_forks_for_tests();
257        let bank = Bank::new_from_parent(bank, SlotLeader::default(), bank_slot);
258
259        let rank_map = bank
260            .epoch_stakes_from_slot(reward_slot)
261            .unwrap()
262            .bls_pubkey_to_rank_map();
263        let signing_keys = (0..num_validators)
264            .map(|index| {
265                let pubkey_affine = rank_map.get_pubkey_stake_entry(index).unwrap().bls_pubkey;
266                keypair_map
267                    .get(&BLSPubkeyCompressed::from(*pubkey_affine))
268                    .unwrap()
269            })
270            .collect::<Vec<_>>();
271
272        let block_id = Hash::new_unique();
273        let notar_vote = Vote::new_notarization_vote(Block {
274            slot: reward_slot,
275            block_id,
276        });
277        let notar_votes = (0..num_notar_validators)
278            .map(|rank| new_vote(notar_vote, rank, signing_keys[rank], shred_version))
279            .collect::<Vec<_>>();
280        let (signature, bitmap) = build_sig_bitmap(&notar_votes);
281        let notar_reward_cert =
282            NotarRewardCertificate::try_new(reward_slot, block_id, signature, bitmap).unwrap();
283
284        let skip_vote = Vote::new_skip_vote(reward_slot);
285        let skip_votes = (num_notar_validators..num_validators)
286            .map(|rank| new_vote(skip_vote, rank, signing_keys[rank], shred_version))
287            .collect::<Vec<_>>();
288        let (signature, bitmap) = build_sig_bitmap(&skip_votes);
289        let skip_reward_cert =
290            SkipRewardCertificate::try_new(reward_slot, signature, bitmap).unwrap();
291
292        let validated_reward_cert = ValidatedRewardCert::try_new(
293            &bank,
294            shred_version,
295            &Some(skip_reward_cert),
296            &Some(notar_reward_cert),
297        )
298        .unwrap()
299        .unwrap();
300        assert_eq!(validated_reward_cert.validators.len(), num_validators);
301    }
302}