penumbra_stake/component/action_handler/delegate.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
use anyhow::{ensure, Result};
use async_trait::async_trait;
use cnidarium::StateWrite;
use cnidarium_component::ActionHandler;
use penumbra_num::Amount;
use penumbra_proto::{DomainType, StateWriteProto};
use penumbra_sct::component::clock::EpochRead;
use crate::{
component::validator_handler::ValidatorDataRead, event, validator::State::*, Delegate,
StateReadExt as _, StateWriteExt as _,
};
#[async_trait]
impl ActionHandler for Delegate {
type CheckStatelessContext = ();
async fn check_stateless(&self, _context: ()) -> Result<()> {
// There are no stateless checks specific to this action.
Ok(())
}
async fn check_and_execute<S: StateWrite>(&self, mut state: S) -> Result<()> {
// These checks all formerly happened in the `check_historical` method,
// if profiling shows that they cause a bottleneck we could (CAREFULLY)
// move some of them back.
let d = self;
// We check if the rate data is for the current epoch to provide a helpful
// error message if there is a mismatch.
let current_epoch = state.get_current_epoch().await?;
ensure!(
d.epoch_index == current_epoch.index,
"delegation was prepared for epoch {} but the current epoch is {}",
d.epoch_index,
current_epoch.index
);
// For delegations, we enforce correct computation (with rounding)
// of the *delegation amount based on the unbonded amount*, because
// users (should be) starting with the amount of unbonded stake they
// wish to delegate, and computing the amount of delegation tokens
// they receive.
//
// The direction of the computation matters because the computation
// involves rounding, so while both
//
// (unbonded amount, rates) -> delegation amount
// (delegation amount, rates) -> unbonded amount
//
// should give approximately the same results, they may not give
// exactly the same results.
let validator_rate = state
.get_validator_rate(&d.validator_identity)
.await?
.ok_or_else(|| anyhow::anyhow!("unknown validator identity {}", d.validator_identity))?
.clone();
let expected_delegation_amount = validator_rate.delegation_amount(d.unbonded_amount);
ensure!(
expected_delegation_amount == d.delegation_amount,
"given {} unbonded stake, expected {} delegation tokens but description produces {}",
d.unbonded_amount,
expected_delegation_amount,
d.delegation_amount,
);
// The delegation is only allowed if both conditions are met:
// - the validator definition is `enabled` by the operator
// - the validator is not jailed or tombstoned
let validator = state
.get_validator_definition(&d.validator_identity)
.await?
.ok_or_else(|| anyhow::anyhow!("missing definition for validator"))?;
let validator_state = state
.get_validator_state(&d.validator_identity)
.await?
.ok_or_else(|| anyhow::anyhow!("missing state for validator"))?;
ensure!(
validator.enabled,
"delegations are only allowed to enabled validators, but {} is disabled",
d.validator_identity,
);
ensure!(
matches!(validator_state, Defined | Inactive | Active),
"delegations are only allowed to active or inactive validators, but {} is in state {:?}",
d.validator_identity,
validator_state,
);
// (end of former check_historical checks)
let validator = self.validator_identity;
let unbonded_delegation = self.unbonded_amount;
// This action is executed in two phases:
// 1. We check if the self-delegation requirement is met.
// 2. We queue the delegation for the next epoch.
let validator_state = state
.get_validator_state(&self.validator_identity)
.await?
.ok_or_else(|| anyhow::anyhow!("missing state for validator"))?;
// When a validator definition is published, it starts in a `Defined` state
// where it is unindexed by the staking module. We transition validator with
// too little stake to the `Defined` state as well. See #2921 for more details.
if validator_state == Defined {
let min_stake = state.get_stake_params().await?.min_validator_stake;
// With #3853, we impose a minimum self-delegation requirement to simplify
// end-epoch handling. The first delegation" to a `Defined` validator must
// be at least `min_validator_stake`.
//
// Note: Validators can be demoted to `Defined` if they have too little stake,
// if we don't check that the pool is empty, we could trap delegations.
let validator_pool_size = state
.get_validator_pool_size(&validator)
.await
.unwrap_or_else(Amount::zero);
if validator_pool_size == Amount::zero() {
ensure!(
unbonded_delegation >= min_stake,
"first delegation to a `Defined` validator must be at least {min_stake}"
);
tracing::debug!(%validator, %unbonded_delegation, "first delegation to validator recorded");
}
}
// We queue the delegation so it can be processed at the epoch boundary.
tracing::debug!(?self, "queuing delegation for next epoch");
state.push_delegation(self.clone());
state.record_proto(event::EventDelegate::from(self).to_proto());
Ok(())
}
}