Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

staking: add a min_validator_stake first delegation requirement #3877

Merged
merged 6 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
use std::sync::Arc;

use anyhow::Result;
use anyhow::{ensure, Result};
use async_trait::async_trait;
use cnidarium::{StateRead, StateWrite};
use cnidarium_component::ActionHandler;
use penumbra_num::Amount;

use crate::{
component::{
validator_handler::{ValidatorDataRead, ValidatorManager},
StateWriteExt as _,
},
event, validator, Delegate, StateReadExt as _,
component::{validator_handler::ValidatorDataRead, StateWriteExt as _},
event,
validator::State::*,
Delegate, StateReadExt as _,
};

#[async_trait]
Expand Down Expand Up @@ -52,7 +52,6 @@ impl ActionHandler for Delegate {
.await?
.ok_or_else(|| anyhow::anyhow!("missing state for validator"))?;

use validator::State::*;
if !validator.enabled {
anyhow::bail!(
"delegations are only allowed to enabled validators, but {} is disabled",
Expand Down Expand Up @@ -96,36 +95,45 @@ impl ActionHandler for Delegate {
}

async fn execute<S: StateWrite>(&self, mut state: S) -> Result<()> {
use crate::validator;
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.

tracing::debug!(?self, "queuing delegation for next epoch");
state.push_delegation(self.clone());

// When a validator definition is published, it starts in a `Defined` state
// until it gathers enough stake to become `Inactive` and get indexed in the
// validator list.
//
// Unlike other validator state transitions, this one is executed with the
// delegation transaction and not at the end of the epoch. This is because we
// want to avoid having to iterate over all defined validators at all.
// See #2921 for more details.
let validator_state = state
.get_validator_state(&self.validator_identity)
.await?
.ok_or_else(|| anyhow::anyhow!("missing state for validator"))?;

// TODO(erwan): The next PR (#3853) in this sprint changes this logic to require
// an initial delegation that is at least the minimum stake param.
if matches!(validator_state, validator::State::Defined)
&& self.delegation_amount.value()
>= state.get_stake_params().await?.min_validator_stake.value()
{
tracing::debug!(validator_identity = %self.validator_identity, delegation_amount = %self.delegation_amount, "validator has enough stake to transition out of defined state");
state
.set_validator_state(&self.validator_identity, validator::State::Inactive)
.await?;
// 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_validator_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(event::delegate(self));
Ok(())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,12 @@ impl ActionHandler for validator::Definition {
.is_some();

if validator_exists {
state.update_validator(v.validator.clone()).await.context(
"should be able to update validator during validator definition execution",
)?;
state
.update_validator_definition(v.validator.clone())
.await
.context(
"should be able to update validator during validator definition execution",
)?;
} else {
// This is a new validator definition. We prime the validator's
// rate data with an initial exchange rate of 1:1.
Expand Down
37 changes: 10 additions & 27 deletions crates/core/component/stake/src/component/epoch_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,6 @@ pub trait EpochHandler: StateWriteExt + ConsensusIndexRead {
total_delegations: Amount,
total_undelegations: Amount,
) -> Result<Option<(IdentityKey, FundingStreams, Amount)>> {
let min_validator_stake = self.get_stake_params().await?.min_validator_stake;

let validator = self.get_validator_definition(&validator_identity).await?.ok_or_else(|| {
anyhow::anyhow!("validator (identity={}) is in consensus index but its definition was not found in the JMT", &validator_identity)
})?;
Expand Down Expand Up @@ -329,36 +327,21 @@ pub trait EpochHandler: StateWriteExt + ConsensusIndexRead {
None
};

// We want to know if the validator has enough stake to remain in the consensus set.
// In order to do this, we need to know what is the size of the validator's delegation
// pool in terms of staking tokens (i.e. the unbonded amount).
let delegation_token_denom = DelegationToken::from(&validator.identity_key).denom();
let validator_unbonded_amount =
next_validator_rate.unbonded_amount(delegation_token_supply);

tracing::debug!(
validator_identity = %validator.identity_key,
validator_delegation_pool = ?delegation_token_supply,
validator_unbonded_amount = ?validator_unbonded_amount,
"calculated validator's unbonded amount for the upcoming epoch"
);

if validator_unbonded_amount < min_validator_stake {
tracing::debug!(
validator_identity = %validator.identity_key,
validator_unbonded_amount = ?validator_unbonded_amount,
min_validator_stake = ?min_validator_stake,
"validator's unbonded amount is below the minimum stake threshold, transitioning to defined"
);
self.set_validator_state(&validator.identity_key, validator::State::Defined)
.await?;
}
let final_state = self
.try_precursor_transition(
validator_identity,
validator_state,
&next_validator_rate,
delegation_token_supply,
)
.await;

tracing::debug!(validator_identity = %validator.identity_key,
previous_epoch_validator_rate= ?prev_validator_rate,
next_epoch_validator_rate = ?next_validator_rate,
delegation_denom = ?delegation_token_denom,
?delegation_token_supply,
voting_power = ?voting_power,
final_state = ?final_state,
"validator's end-epoch has been processed");

self.process_validator_pool_state(&validator.identity_key, epoch_to_end)
Expand Down
3 changes: 2 additions & 1 deletion crates/core/component/stake/src/component/stake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,8 @@ pub trait StateReadExt: StateRead {
/// Gets the stake parameters from the JMT.
async fn get_stake_params(&self) -> Result<StakeParameters> {
self.get(state_key::parameters::key())
.await?
.await
.expect("no deserialization error should happen")
.ok_or_else(|| anyhow!("Missing StakeParameters"))
}

Expand Down
Loading
Loading