From 9d54fa2a83d1b79863cb86935d94347bc86045d6 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Mon, 26 Feb 2024 08:39:55 -0500 Subject: [PATCH] staking: add a `min_validator_stake` first delegation requirement (#3877) Close #3853 This PR: - removes the `Defined -> Inactive` transition from the delegate action execution - ensure that the first delegation to a validator pool must be at least `min_validator_stake` - add a `try_precursor_transition` method to the validator state machine to safely drive transitions in/out of the `Defined` state --- .../src/component/action_handler/delegate.rs | 66 +++-- .../action_handler/validator_definition.rs | 9 +- .../stake/src/component/epoch_handler.rs | 37 +-- .../component/stake/src/component/stake.rs | 3 +- .../validator_handler/validator_manager.rs | 268 ++++++++++++------ .../validator_handler/validator_store.rs | 9 + 6 files changed, 244 insertions(+), 148 deletions(-) diff --git a/crates/core/component/stake/src/component/action_handler/delegate.rs b/crates/core/component/stake/src/component/action_handler/delegate.rs index 1e8f4f4635..c7a6f44d74 100644 --- a/crates/core/component/stake/src/component/action_handler/delegate.rs +++ b/crates/core/component/stake/src/component/action_handler/delegate.rs @@ -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] @@ -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", @@ -96,36 +95,45 @@ impl ActionHandler for Delegate { } async fn execute(&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_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(()) } diff --git a/crates/core/component/stake/src/component/action_handler/validator_definition.rs b/crates/core/component/stake/src/component/action_handler/validator_definition.rs index bf75400b28..b8573217b6 100644 --- a/crates/core/component/stake/src/component/action_handler/validator_definition.rs +++ b/crates/core/component/stake/src/component/action_handler/validator_definition.rs @@ -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. diff --git a/crates/core/component/stake/src/component/epoch_handler.rs b/crates/core/component/stake/src/component/epoch_handler.rs index 7073a2b6dc..8e79d1c922 100644 --- a/crates/core/component/stake/src/component/epoch_handler.rs +++ b/crates/core/component/stake/src/component/epoch_handler.rs @@ -175,8 +175,6 @@ pub trait EpochHandler: StateWriteExt + ConsensusIndexRead { total_delegations: Amount, total_undelegations: Amount, ) -> Result> { - 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) })?; @@ -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) diff --git a/crates/core/component/stake/src/component/stake.rs b/crates/core/component/stake/src/component/stake.rs index aaaa84216f..7106754d27 100644 --- a/crates/core/component/stake/src/component/stake.rs +++ b/crates/core/component/stake/src/component/stake.rs @@ -191,7 +191,8 @@ pub trait StateReadExt: StateRead { /// Gets the stake parameters from the JMT. async fn get_stake_params(&self) -> Result { self.get(state_key::parameters::key()) - .await? + .await + .expect("no deserialization error should happen") .ok_or_else(|| anyhow!("Missing StakeParameters")) } diff --git a/crates/core/component/stake/src/component/validator_handler/validator_manager.rs b/crates/core/component/stake/src/component/validator_handler/validator_manager.rs index 124f77403f..bd9cd1009b 100644 --- a/crates/core/component/stake/src/component/validator_handler/validator_manager.rs +++ b/crates/core/component/stake/src/component/validator_handler/validator_manager.rs @@ -22,6 +22,7 @@ use sha2::{Digest as _, Sha256}; use tendermint::abci::types::{CommitInfo, Misbehavior}; use tokio::task::JoinSet; use validator::BondingState::*; +use validator::State::*; use cnidarium::StateWrite; use penumbra_proto::StateWriteProto; @@ -38,45 +39,74 @@ use crate::{ use penumbra_asset::asset; #[async_trait] +/// Defines the validator state machine of the staking component. +/// +/// # Overview +/// This trait offers an interface to the validator staking state machine. +/// +/// ## Validator management +/// - Add validator definition via [`add_validator`]. +/// - Update validator definitions via [`update_validator_definition`]. +/// +/// ## State machine interface +/// - A fallible state transition function via [`set_validator_state`]. +/// - A safer handle to tentatively explore state transitions via [`try_precursor_transition`]. +/// +/// ## Validator-specific logic +/// - Tracking a validator's uptime via [`track_uptime`]. +/// - Process byzantine behavior evidence via [`process_evidence`]. +/// +/// # State machine diagram: +/// ```plaintext +/// ┌───────────────────────────────────────────────────────┐ +/// │ ┌──────────────────────────────┐ │ +/// ▼ ▼ │ │ +/// ╔═══════════════╗ ┌────────────┐ │ │ +/// ┌─▶║ Defined ║◀─────▶│ Disabled │ │ │ +/// │ ╚═══════════════╝ └────────────┘ │ │ +/// │ │ │ │ │ +/// │ │ ▼ │ │ +/// │ │ ┏━━━━━━━━━━━━┓ │ │ +/// │ └──────────────▶┃ ┃ │ │ +/// │ ┃ Tombstoned ┃◀────────────┐ │ │ +/// │ ┌──────────────▶┃ ┃ │ │ │ +/// │ │ ┗━━━━━━━━━━━━┛ │ │ │ +/// │ │ ▲ │ │ │ +/// │ │ │ ┌──────────────┐ │ │ +/// │ ┌─────────────┐ ┌────────────┐ │ │◀──┘ │ +/// └──▶│ Jailed │◀───────│ Active │◀───▶│ Inactive │ │ +/// └─────────────┘ └────────────┘ │ │◀────┘ +/// │ └──────────────┘ +/// │ ▲ +/// └──────────────────────────────────────────┘ +/// +/// +/// +/// ╔═════════════════╗ +/// ║ starting state ║ +/// ╚═════════════════╝ +/// ┏━━━━━━━━━━━━━━━━━┓ +/// ┃ terminal state ┃ +/// ┗━━━━━━━━━━━━━━━━━┛ +/// ``` +/// +/// [`add_validator`]: Self::add_validator +/// [`update_validator_definition`]: Self::update_validator_definition +/// [`set_validator_state`]: Self::set_validator_state +/// [`try_precursor_transition`]: Self::try_precursor_transition +/// [`track_uptime`]: Self::track_uptime +/// [`process_evidence`]: Self::process_evidence pub trait ValidatorManager: StateWrite { - /// Perform a state transition for the specified validator and new state. - /// Initial validator state is defined using [`add_validator`] - /// ┌───────────────────────────────────────────────────────┐ - /// │ ┌──────────────────────────────┐ │ - /// ▼ ▼ │ │ - /// ╔══════*════════╗ ┌────────────┐ │ │ - /// ┌─▶║ Defined ║◀─────▶│ Disabled │ │ │ - /// │ ╚═══════════════╝ └────────────┘ │ │ - /// │ │ │ │ │ - /// │ │ ▼ │ │ - /// │ │ ┏━━━━━━━━━━━━┓ │ │ - /// │ └──────────────▶┃ ┃ │ │ - /// │ ┃ Tombstoned ┃◀────────────┐ │ │ - /// │ ┌──────────────▶┃ ┃ │ │ │ - /// │ │ ┗━━━━━━━━━━━━┛ │ │ │ - /// │ │ ▲ │ │ │ - /// │ │ │ ┌──────────────┐ │ │ - /// │ ┌─────────────┐ ┌────────────┐ │ │◀──┘ │ - /// └──▶│ Jailed │◀───────│ Active │◀───▶│ Inactive │ │ - /// └─────────────┘ └────────────┘ │ │◀────┘ - /// │ └──────────────┘ - /// │ ▲ - /// └──────────────────────────────────────────┘ - /// - /// - /// - /// ╔═════════════════╗ - /// ║ starting state ║ - /// ╚═════════════════╝ - /// ┏━━━━━━━━━━━━━━━━━┓ - /// ┃ terminal state ┃ - /// ┗━━━━━━━━━━━━━━━━━┛ + /// Execute a legal state transition, updating the validator records and + /// implementing the necessary side effects. + /// /// # Errors - /// This method errors on illegal state transitions; since execution must be infallible, - /// it's the caller's responsibility to ensure that the state transitions are legal. + /// This method errors on illegal state transitions, but will otherwise try to do what + /// you ask it to do. It is the caller's responsibility to ensure that the state transitions + /// are legal and pertinent. /// - /// It can also error if the validator is not found in the state, though this should - /// never happen. + /// An error can also happen if the state is corrupted or pushed into an incoherent mode + /// in this case, we return an error but there is no way to recover from those. async fn set_validator_state( &mut self, identity_key: &IdentityKey, @@ -106,17 +136,19 @@ pub trait ValidatorManager: StateWrite { old_state: validator::State, new_state: validator::State, ) -> Result<()> { - use validator::State::*; let validator_state_path = state_key::validators::state::by_id(identity_key); // We use the current epoch index to compute the unbonding epoch for the validator, // when necessary. let current_epoch = self.get_current_epoch().await?; + tracing::info!("executing state transition"); + // Validator state transitions are usually triggered by an epoch transition. The exception // to this rule is when a validator exits the active set. In this case, we want to end the // current epoch early in order to hold that validator transitions happen at epoch boundaries. if let (Active, Defined | Disabled | Jailed | Tombstoned) = (old_state, new_state) { + tracing::info!("signaling early epoch end as a result of validator state transition"); self.set_end_epoch_flag(); } @@ -124,20 +156,17 @@ pub trait ValidatorManager: StateWrite { (Defined, Inactive) => { // The validator has reached the minimum threshold to be indexed by // the staking component. - tracing::debug!(identity_key = ?identity_key, "validator has reached minimum stake threshold to be considered inactive"); self.add_consensus_set_index(identity_key); self.put(validator_state_path, Inactive); } (Inactive, Defined) => { // The validator has fallen below the minimum threshold to be // part of the "greater" consensus set. - tracing::debug!(identity_key = ?identity_key, "validator has fallen below minimum stake threshold to be considered inactive"); self.remove_consensus_set_index(identity_key); self.put(validator_state_path, Defined); } (Inactive, Active) => { let power = self.get_validator_power(identity_key).await; - tracing::debug!(validator_identity = %identity_key, voting_power = ?power, "validator has become active"); // The validators with the most voting power are selected to be part of the // chain's "Active set". @@ -152,6 +181,8 @@ pub trait ValidatorManager: StateWrite { ), ); + tracing::debug!(voting_power = ?power, "validator pool is bonded, uptime is tracked, setting validator to active"); + // Finally, set the validator to be active. self.put(validator_state_path, Active); } @@ -160,9 +191,6 @@ pub trait ValidatorManager: StateWrite { // When an active validator becomes inactive, or is disabled by its operator, // we need to start the unbonding process for its delegation pool. We keep it // in the consensus set, but it is no longer part of the "active set". - tracing::debug!(validator_identity = %identity_key, ?new_state, "validator has become inactive or disabled"); - - // The validator's delegation pool begins unbonding. self.set_validator_bonding_state( identity_key, Unbonding { @@ -178,26 +206,28 @@ pub trait ValidatorManager: StateWrite { // A jailed validator has been released from jail by its operator. // Its delegation pool has falled below the minimum threshold, so it is // considered `Defined`. - tracing::debug!(validator_identity = %identity_key, ?new_state, "releasing validator from jail"); - // TODO(erwan): deindex + // + // End-epoch handler is responsible for choosing when to deindex the validator. + tracing::debug!("releasing validator from jail"); self.put(validator_state_path, Defined); } (Jailed, Inactive) => { // Here, we don't have anything to do, only allow the validator to return to society. - tracing::debug!(validator_identity = %identity_key, ?new_state, "releasing validator from jail"); + // + // End-epoch handler is responsible for choosing when to deindex the validator. + tracing::debug!("releasing validator from jail"); self.put(validator_state_path, Inactive); } (Disabled, Inactive) => { // The validator was disabled by its operator, and was re-enabled. Since its // delegation pool was sufficiently large, it is considered inactive. - tracing::debug!(validator_identity = %identity_key, "disabled validator has become inactive"); + tracing::debug!("disabled validator has become inactive"); self.put(validator_state_path, Inactive); } (Inactive | Jailed, Disabled) => { // The validator was disabled by its operator. - // We record that the validator was disabled, so delegations to it are not processed. - tracing::debug!(validator_identity = %identity_key, validator_state = ?old_state, "validator has been disabled"); + tracing::debug!("validator has been disabled"); self.put(validator_state_path, Disabled); } (Active, Jailed) => { @@ -215,32 +245,26 @@ pub trait ValidatorManager: StateWrite { // validators are not unbonded immediately, because they need to // be held accountable for byzantine behavior for the entire // unbonding period. - self.set_validator_bonding_state( - identity_key, - Unbonding { - unbonds_at_epoch: self - .compute_unbonding_epoch(identity_key, current_epoch.index) - .await?, - }, - ); + let unbonds_at_epoch = self + .compute_unbonding_epoch(identity_key, current_epoch.index) + .await?; + + self.set_validator_bonding_state(identity_key, Unbonding { unbonds_at_epoch }); + + tracing::debug!(penalty, unbonds_at_epoch, "jailed validator"); // Finally, set the validator to be jailed. self.put(validator_state_path, Jailed); } (Active, Defined) => { + let unbonds_at_epoch = self + .compute_unbonding_epoch(identity_key, current_epoch.index) + .await?; + // The validator's delegation pool begins unbonding. + self.set_validator_bonding_state(identity_key, Unbonding { unbonds_at_epoch }); // The validator was part of the active set, but its delegation pool fell below // the minimum threshold. We remove it from the active set and the consensus set. - tracing::debug!(validator_identity = %identity_key, "validator has fallen below minimum stake threshold to be considered active"); - - // The validator's delegation pool begins unbonding. - self.set_validator_bonding_state( - identity_key, - Unbonding { - unbonds_at_epoch: self - .compute_unbonding_epoch(identity_key, current_epoch.index) - .await?, - }, - ); + tracing::debug!(unbonds_at_epoch, "ejecting from active set"); self.remove_consensus_set_index(identity_key); self.put(validator_state_path, Defined); } @@ -265,6 +289,11 @@ pub trait ValidatorManager: StateWrite { // applied. self.set_validator_bonding_state(identity_key, Unbonded); + tracing::info!( + misbehavior_penalty, + "tombstoning validator and unbond its pool" + ); + // Remove the validator from the consensus set. self.remove_consensus_set_index(identity_key); @@ -272,7 +301,7 @@ pub trait ValidatorManager: StateWrite { self.put(validator_state_path, Tombstoned); } (Tombstoned, Tombstoned) => { - tracing::debug!(validator_identity = %identity_key, "validator is already tombstoned"); + tracing::debug!("validator is already tombstoned"); // See discussion in https://github.com/penumbra-zone/penumbra/pull/3761 for context. // The abridged summary is that applying a misbehavior penalty once and immediately // unbonding the validator's delegation pool should be enough to deter misbehavior. @@ -315,27 +344,70 @@ pub trait ValidatorManager: StateWrite { (Disabled, Disabled) => { /* no-op */ } } - // Update the validator metrics once the state transition has been applied. - match old_state { - Defined => metrics::gauge!(metrics::DEFINED_VALIDATORS).decrement(1.0), - Inactive => metrics::gauge!(metrics::INACTIVE_VALIDATORS).decrement(1.0), - Active => metrics::gauge!(metrics::ACTIVE_VALIDATORS).decrement(1.0), - Disabled => metrics::gauge!(metrics::DISABLED_VALIDATORS).decrement(1.0), - Jailed => metrics::gauge!(metrics::JAILED_VALIDATORS).decrement(1.0), - Tombstoned => metrics::gauge!(metrics::TOMBSTONED_VALIDATORS).decrement(1.0), - }; - match new_state { - Defined => metrics::gauge!(metrics::DEFINED_VALIDATORS).increment(1.0), - Inactive => metrics::gauge!(metrics::INACTIVE_VALIDATORS).increment(1.0), - Active => metrics::gauge!(metrics::ACTIVE_VALIDATORS).increment(1.0), - Disabled => metrics::gauge!(metrics::DISABLED_VALIDATORS).increment(1.0), - Jailed => metrics::gauge!(metrics::JAILED_VALIDATORS).increment(1.0), - Tombstoned => metrics::gauge!(metrics::TOMBSTONED_VALIDATORS).increment(1.0), - }; + Self::state_machine_metrics(old_state, new_state); Ok(()) } + #[instrument(skip(self))] + /// Try to implement a state transition in/out of the `Defined` precursor state. + /// Returns the new state if sucessful, and `None` otherwise. + async fn try_precursor_transition( + &mut self, + validator_id: &IdentityKey, + previous_state: validator::State, + next_rate: &RateData, + delegation_token_supply: Amount, + ) -> Option { + // Conspicuously missing from this list are `Jailed | Disabled` validators. + // This is because their transition MUST be triggered by a manual validator upload. + if !matches!(previous_state, Defined | Inactive | Active) { + return None; + } + + let min_stake = self + .get_stake_params() + .await + .expect("staking parameters are always set") + .min_validator_stake; + + // We convert the delegation pool into staking tokens so that we can decide whether + // the validator meets the minimum stake threshold. + let unbonded_pool = next_rate.unbonded_amount(delegation_token_supply); + + tracing::debug!( + %validator_id, + ?delegation_token_supply, + ?unbonded_pool, + next_validator_exchange_rate = ?next_rate.validator_exchange_rate, + ?previous_state, + ?min_stake, + "computed validator unbonded pool to explore precursor transition" + ); + + let has_minimum_stake = unbonded_pool >= min_stake; + + // Refer yourself to the state machine diagram for the logic behind these transitions. + let new_state = match previous_state { + Defined if has_minimum_stake => Inactive, + Defined if !has_minimum_stake => Defined, + Active if has_minimum_stake => Active, + Active if !has_minimum_stake => Defined, + Inactive if has_minimum_stake => Inactive, + Inactive if !has_minimum_stake => Defined, + _ => unreachable!("the previous state was validated by the guard condition"), + }; + + if new_state != previous_state { + let _ = self + .set_validator_state(validator_id, new_state) + .await + .expect("we guard the state transition"); + } + + Some(new_state) + } + /// Add a validator during genesis, which will start in Active /// state with power assigned. async fn add_genesis_validator( @@ -357,7 +429,7 @@ pub trait ValidatorManager: StateWrite { let total_delegation_tokens = genesis_allocations .get(&delegation_id) .copied() - .unwrap_or(0u64.into()); + .unwrap_or_else(Amount::zero); let power = initial_rate_data.voting_power(total_delegation_tokens); self.add_validator_inner( @@ -475,7 +547,7 @@ pub trait ValidatorManager: StateWrite { /// Update a validator definition #[tracing::instrument(skip(self, validator), fields(id = ?validator.identity_key))] - async fn update_validator(&mut self, validator: Validator) -> Result<()> { + async fn update_validator_definition(&mut self, validator: Validator) -> Result<()> { use validator::State::*; tracing::debug!(definition = ?validator, "updating validator definition"); @@ -701,6 +773,26 @@ pub trait ValidatorManager: StateWrite { self.set_validator_state(&validator.identity_key, validator::State::Tombstoned) .await } + + fn state_machine_metrics(old_state: validator::State, new_state: validator::State) { + // Update the validator metrics once the state transition has been applied. + match old_state { + Defined => metrics::gauge!(metrics::DEFINED_VALIDATORS).decrement(1.0), + Inactive => metrics::gauge!(metrics::INACTIVE_VALIDATORS).decrement(1.0), + Active => metrics::gauge!(metrics::ACTIVE_VALIDATORS).decrement(1.0), + Disabled => metrics::gauge!(metrics::DISABLED_VALIDATORS).decrement(1.0), + Jailed => metrics::gauge!(metrics::JAILED_VALIDATORS).decrement(1.0), + Tombstoned => metrics::gauge!(metrics::TOMBSTONED_VALIDATORS).decrement(1.0), + }; + match new_state { + Defined => metrics::gauge!(metrics::DEFINED_VALIDATORS).increment(1.0), + Inactive => metrics::gauge!(metrics::INACTIVE_VALIDATORS).increment(1.0), + Active => metrics::gauge!(metrics::ACTIVE_VALIDATORS).increment(1.0), + Disabled => metrics::gauge!(metrics::DISABLED_VALIDATORS).increment(1.0), + Jailed => metrics::gauge!(metrics::JAILED_VALIDATORS).increment(1.0), + Tombstoned => metrics::gauge!(metrics::TOMBSTONED_VALIDATORS).increment(1.0), + }; + } } impl ValidatorManager for T {} diff --git a/crates/core/component/stake/src/component/validator_handler/validator_store.rs b/crates/core/component/stake/src/component/validator_handler/validator_store.rs index 7704c7d8a3..a03846109f 100644 --- a/crates/core/component/stake/src/component/validator_handler/validator_store.rs +++ b/crates/core/component/stake/src/component/validator_handler/validator_store.rs @@ -3,6 +3,7 @@ use std::pin::Pin; use crate::{ rate::RateData, validator::{State, Validator}, + DelegationToken, }; use anyhow::Result; use async_trait::async_trait; @@ -114,6 +115,14 @@ pub trait ValidatorDataRead: StateRead { self.get(&state_key::validators::uptime::by_id(identity_key)) } + async fn get_validator_pool_size(&self, identity_key: &IdentityKey) -> Option { + use penumbra_shielded_pool::component::SupplyRead; + + self.token_supply(&DelegationToken::from(identity_key).id()) + .await + .expect("no deserialization error expected") + } + // Tendermint validators are referenced to us by their Tendermint consensus key, // but we reference them by their Penumbra identity key. async fn get_validator_by_consensus_key(&self, ck: &PublicKey) -> Result> {