penumbra_sdk_stake/component/validator_handler/
validator_store.rs

1use crate::{
2    component::{StateReadExt as _, MAX_VOTING_POWER},
3    event,
4    rate::RateData,
5    state_key,
6    validator::{self, BondingState::*, State, Validator},
7    IdentityKey, Uptime,
8};
9use anyhow::Result;
10use async_trait::async_trait;
11use cnidarium::{StateRead, StateWrite};
12use futures::{Future, FutureExt};
13use penumbra_sdk_num::Amount;
14use penumbra_sdk_proto::{
15    state::future::DomainFuture, DomainType, StateReadProto, StateWriteProto,
16};
17use std::pin::Pin;
18use tendermint::PublicKey;
19use tracing::instrument;
20
21#[async_trait]
22pub trait ValidatorDataRead: StateRead {
23    async fn get_validator_info(
24        &self,
25        identity_key: &IdentityKey,
26    ) -> Result<Option<validator::Info>> {
27        let validator = self.get_validator_definition(identity_key).await?;
28        let status = self.get_validator_status(identity_key).await?;
29        let rate_data = self.get_validator_rate(identity_key).await?;
30
31        match (validator, status, rate_data) {
32            (Some(validator), Some(status), Some(rate_data)) => Ok(Some(validator::Info {
33                validator,
34                status,
35                rate_data,
36            })),
37            _ => Ok(None),
38        }
39    }
40
41    fn get_validator_state(
42        &self,
43        identity_key: &IdentityKey,
44    ) -> DomainFuture<validator::State, Self::GetRawFut> {
45        self.get(&state_key::validators::state::by_id(identity_key))
46    }
47
48    async fn get_validator_bonding_state(
49        &self,
50        identity_key: &IdentityKey,
51    ) -> Option<validator::BondingState> {
52        self.get(&state_key::validators::pool::bonding_state::by_id(
53            identity_key,
54        ))
55        .await
56        .expect("no deserialization error expected")
57    }
58
59    /// Returns the amount of delegation tokens in the specified validator's pool.
60    async fn get_validator_pool_size(&self, identity_key: &IdentityKey) -> Option<Amount> {
61        self.get(&state_key::validators::pool::balance::by_id(identity_key))
62            .await
63            .expect("no deserialization error expected")
64    }
65
66    /// Convenience method to assemble a [`ValidatorStatus`](crate::validator::Status).
67    async fn get_validator_status(
68        &self,
69        identity_key: &IdentityKey,
70    ) -> Result<Option<validator::Status>> {
71        let bonding_state = self.get_validator_bonding_state(identity_key).await;
72        let state = self.get_validator_state(identity_key).await?;
73        let power = self.get_validator_power(identity_key).await?;
74        let identity_key = identity_key.clone();
75        match (state, power, bonding_state) {
76            (Some(state), Some(voting_power), Some(bonding_state)) => Ok(Some(validator::Status {
77                identity_key,
78                state,
79                voting_power,
80                bonding_state,
81            })),
82            _ => Ok(None),
83        }
84    }
85
86    fn get_validator_rate(
87        &self,
88        identity_key: &IdentityKey,
89    ) -> Pin<Box<dyn Future<Output = Result<Option<RateData>>> + Send + 'static>> {
90        self.get(&state_key::validators::rate::current_by_id(identity_key))
91            .boxed()
92    }
93
94    async fn get_prev_validator_rate(&self, identity_key: &IdentityKey) -> Option<RateData> {
95        self.get(&state_key::validators::rate::previous_by_id(identity_key))
96            .await
97            .expect("no deserialization error expected")
98    }
99
100    fn get_validator_power(
101        &self,
102        validator: &IdentityKey,
103    ) -> DomainFuture<Amount, Self::GetRawFut> {
104        self.get(&state_key::validators::power::by_id(validator))
105    }
106
107    /// Returns the block height at which the validator was last disabled.
108    /// If the validator was never disabled, returns `None`.
109    async fn get_last_disabled_height(&self, identity_key: &IdentityKey) -> Option<u64> {
110        self.nonverifiable_get_raw(
111            state_key::validators::last_disabled::by_id(identity_key).as_bytes(),
112        )
113        .await
114        .expect("no deserialization error expected")
115        .map(|bytes| u64::from_be_bytes(bytes.try_into().expect("we only write 8 bytes")))
116    }
117
118    async fn get_validator_definition(
119        &self,
120        identity_key: &IdentityKey,
121    ) -> Result<Option<Validator>> {
122        self.get(&state_key::validators::definitions::by_id(identity_key))
123            .await
124    }
125
126    fn get_validator_uptime(
127        &self,
128        identity_key: &IdentityKey,
129    ) -> DomainFuture<Uptime, Self::GetRawFut> {
130        let key = state_key::validators::uptime::by_id(identity_key);
131        self.nonverifiable_get(key.as_bytes())
132    }
133
134    async fn lookup_identity_key_by_consensus_key(&self, ck: &PublicKey) -> Option<IdentityKey> {
135        self.get(&state_key::validators::lookup_by::consensus_key(ck))
136            .await
137            .expect("no deserialization error")
138    }
139
140    async fn lookup_consensus_key_by_comet_address(&self, address: &[u8; 20]) -> Option<PublicKey> {
141        self.get(&state_key::validators::lookup_by::cometbft_address(address))
142            .await
143            .expect("no deserialization error")
144    }
145
146    // Tendermint validators are referenced to us by their Tendermint consensus key,
147    // but we reference them by their Penumbra identity key.
148    async fn get_validator_definition_by_consensus_key(
149        &self,
150        ck: &PublicKey,
151    ) -> Result<Option<Validator>> {
152        if let Some(identity_key) = self.lookup_identity_key_by_consensus_key(ck).await {
153            self.get_validator_definition(&identity_key).await
154        } else {
155            return Ok(None);
156        }
157    }
158
159    async fn get_validator_definition_by_cometbft_address(
160        &self,
161        address: &[u8; 20],
162    ) -> Result<Option<Validator>> {
163        if let Some(consensus_key) = self.lookup_consensus_key_by_comet_address(address).await {
164            self.get_validator_definition_by_consensus_key(&consensus_key)
165                .await
166        } else {
167            return Ok(None);
168        }
169    }
170
171    /// Compute the unbonding height for an undelegation initiated at `start_height`,
172    /// relative to the **current** state of the validator pool.
173    ///
174    /// Returns `None` if the pool is [`Unbonded`](crate::validator::State).
175    ///
176    /// This can be used to check if the undelegation is allowed, or compute a penalty range,
177    /// or to compute the epoch at which a delegation pool will be unbonded.
178    async fn compute_unbonding_height(
179        &self,
180        id: &IdentityKey,
181        start_height: u64,
182    ) -> Result<Option<u64>> {
183        let Some(val_bonding_state) = self.get_validator_bonding_state(id).await else {
184            anyhow::bail!(
185                "validator bonding state not tracked (validator_identity={})",
186                id
187            )
188        };
189
190        let min_block_delay = self.get_stake_params().await?.unbonding_delay;
191        let upper_bound_height = start_height.saturating_add(min_block_delay);
192
193        let unbonding_height = match val_bonding_state {
194            // The pool is bonded, so the unbonding height is the start height plus the delay.
195            Bonded => Some(upper_bound_height),
196            // The pool is unbonding at a specific height, so we can use that.
197            Unbonding { unbonds_at_height } => {
198                if unbonds_at_height > start_height {
199                    // The unbonding height is the minimum of the unbonding height and the upper bound.
200                    // There are a couple reasons:
201                    // - The unbonding delay parameter can change, and in particular, it can decrease.
202                    // - We might be processing an undelegation that was initiated before the validator
203                    //   began unbonding, and the unbonding height is in the past.
204                    Some(unbonds_at_height.min(upper_bound_height))
205                } else {
206                    // In some cases, the allowed unbonding height can be smaller than
207                    // undelgation start height, for example if the unbonding delay has
208                    // changed in a parameter update, or if the unbonding has finished
209                    // and the validator is not indexed by the staking module anymore.
210                    // This is functionally equivalent to dealing with an `Unbonded` pool.
211                    None
212                }
213            }
214            // The pool is unbonded, so the unbonding height can be decided by the caller.
215            Unbonded => None,
216        };
217
218        Ok(unbonding_height)
219    }
220
221    // TODO(erwan): we pull the entire validator definition instead of tracking
222    // the consensus key separately.  If we did, not only could we save on deserialization
223    // but we could also make this a clean [`DomainFuture`].
224    fn fetch_validator_consensus_key(
225        &self,
226        identity_key: &IdentityKey,
227    ) -> Pin<Box<dyn Future<Output = Result<Option<PublicKey>>> + Send + 'static>> {
228        use futures::TryFutureExt;
229        self.get(&state_key::validators::definitions::by_id(identity_key))
230            .map_ok(|opt: Option<Validator>| opt.map(|v: Validator| v.consensus_key))
231            .boxed()
232    }
233}
234
235impl<T: StateRead + ?Sized> ValidatorDataRead for T {}
236
237#[async_trait]
238pub(crate) trait ValidatorDataWrite: StateWrite {
239    fn set_validator_uptime(&mut self, identity_key: &IdentityKey, uptime: Uptime) {
240        self.nonverifiable_put_raw(
241            state_key::validators::uptime::by_id(identity_key)
242                .as_bytes()
243                .to_vec(),
244            uptime.encode_to_vec(),
245        );
246    }
247
248    fn set_validator_bonding_state(
249        &mut self,
250        identity_key: &IdentityKey,
251        state: validator::BondingState,
252    ) {
253        tracing::debug!(?state, validator_identity = %identity_key, "set bonding state for validator");
254        self.put(
255            state_key::validators::pool::bonding_state::by_id(identity_key),
256            state.clone(),
257        );
258        self.record_proto(
259            event::EventValidatorBondingStateChange {
260                identity_key: *identity_key,
261                bonding_state: state,
262            }
263            .to_proto(),
264        );
265    }
266
267    #[instrument(skip(self))]
268    fn set_validator_power(
269        &mut self,
270        identity_key: &IdentityKey,
271        voting_power: Amount,
272    ) -> Result<()> {
273        tracing::debug!(validator_identity = ?identity_key, ?voting_power, "setting validator power");
274        if voting_power.value() > MAX_VOTING_POWER {
275            anyhow::bail!("voting power exceeds maximum")
276        }
277        self.put(
278            state_key::validators::power::by_id(identity_key),
279            voting_power,
280        );
281        self.record_proto(
282            event::EventValidatorVotingPowerChange {
283                identity_key: *identity_key,
284                voting_power,
285            }
286            .to_proto(),
287        );
288
289        Ok(())
290    }
291
292    #[instrument(skip(self))]
293    fn set_initial_validator_state(
294        &mut self,
295        id: &IdentityKey,
296        initial_state: State,
297    ) -> Result<()> {
298        tracing::debug!(validator_identity = %id, ?initial_state, "setting initial validator state");
299        if !matches!(initial_state, State::Active | State::Defined) {
300            anyhow::bail!("invalid initial validator state");
301        }
302
303        self.put(state_key::validators::state::by_id(id), initial_state);
304        self.record_proto(
305            event::EventValidatorStateChange {
306                identity_key: *id,
307                state: initial_state,
308            }
309            .to_proto(),
310        );
311        Ok(())
312    }
313
314    #[instrument(skip(self))]
315    fn set_validator_rate_data(&mut self, identity_key: &IdentityKey, rate_data: RateData) {
316        tracing::debug!("setting validator rate data");
317        self.put(
318            state_key::validators::rate::current_by_id(identity_key),
319            rate_data.clone(),
320        );
321        self.record_proto(
322            event::EventRateDataChange {
323                identity_key: *identity_key,
324                rate_data,
325            }
326            .to_proto(),
327        );
328    }
329
330    #[instrument(skip(self))]
331    /// Persist the previous validator rate data, inclusive of accumulated penalties.
332    fn set_prev_validator_rate(&mut self, identity_key: &IdentityKey, rate_data: RateData) {
333        let path = state_key::validators::rate::previous_by_id(identity_key);
334        self.put(path, rate_data)
335    }
336
337    #[instrument(skip(self))]
338    /// Set the block height at which the validator was last disabled.
339    /// This is useful to make sure that the validator is not re-enabled too soon.
340    /// See #4067 for details about epoch-grinding.
341    fn set_last_disabled_height(&mut self, identity_key: &IdentityKey, height: u64) {
342        self.nonverifiable_put_raw(
343            state_key::validators::last_disabled::by_id(identity_key)
344                .as_bytes()
345                .to_vec(),
346            height.to_be_bytes().to_vec(),
347        );
348    }
349}
350
351impl<T: StateWrite + ?Sized> ValidatorDataWrite for T {}
352
353#[async_trait]
354pub(crate) trait ValidatorPoolTracker: StateWrite {
355    /// Set the validator pool size, overwriting any existing value.
356    fn set_validator_pool_size(&mut self, identity_key: &IdentityKey, amount: Amount) {
357        self.put(
358            state_key::validators::pool::balance::by_id(identity_key),
359            amount,
360        );
361    }
362
363    /// Checked increase of the validator pool size by the given amount.
364    /// Returns the new pool size, or `None` if the update failed.
365    async fn increase_validator_pool_size(
366        &mut self,
367        identity_key: &IdentityKey,
368        add: Amount,
369    ) -> Option<Amount> {
370        let state_path = state_key::validators::pool::balance::by_id(identity_key);
371        let old_supply = self
372            .get(&state_path)
373            .await
374            .expect("no deserialization error expected")
375            .unwrap_or(Amount::zero());
376
377        tracing::debug!(validator_identity = %identity_key, ?add, ?old_supply, "expanding validator pool size");
378
379        if let Some(new_supply) = old_supply.checked_add(&add) {
380            self.put(state_path, new_supply);
381            Some(new_supply)
382        } else {
383            None
384        }
385    }
386
387    /// Checked decrease of the validator pool size by the given amount.
388    /// Returns the new pool size, or `None` if the update failed.
389    async fn decrease_validator_pool_size(
390        &mut self,
391        identity_key: &IdentityKey,
392        sub: Amount,
393    ) -> Option<Amount> {
394        let state_path = state_key::validators::pool::balance::by_id(identity_key);
395        let old_supply = self
396            .get(&state_path)
397            .await
398            .expect("no deserialization error expected")
399            .unwrap_or(Amount::zero());
400
401        tracing::debug!(validator_identity = %identity_key, ?sub, ?old_supply, "contracting validator pool size");
402
403        if let Some(new_supply) = old_supply.checked_sub(&sub) {
404            self.put(state_path, new_supply);
405            Some(new_supply)
406        } else {
407            None
408        }
409    }
410}
411
412impl<T: StateWrite + ?Sized> ValidatorPoolTracker for T {}