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