penumbra_sdk_stake/component/validator_handler/
validator_manager.rs

1use {
2    crate::{
3        component::{
4            metrics,
5            stake::{ConsensusIndexWrite, RateDataWrite},
6            validator_handler::{
7                validator_store::ValidatorPoolTracker, ValidatorDataRead, ValidatorDataWrite,
8            },
9            StateReadExt as _, StateWriteExt as _,
10        },
11        event,
12        rate::{BaseRateData, RateData},
13        state_key,
14        validator::{
15            self,
16            BondingState::*,
17            State::{self, *},
18            Validator,
19        },
20        DelegationToken, IdentityKey, Penalty, Uptime,
21    },
22    anyhow::{ensure, Result},
23    async_trait::async_trait,
24    cnidarium::StateWrite,
25    penumbra_sdk_asset::asset,
26    penumbra_sdk_num::Amount,
27    penumbra_sdk_proto::{DomainType as _, StateWriteProto},
28    penumbra_sdk_sct::component::{
29        clock::{EpochManager, EpochRead},
30        StateReadExt as _,
31    },
32    penumbra_sdk_shielded_pool::component::AssetRegistry,
33    std::collections::BTreeMap,
34    tendermint::abci::types::Misbehavior,
35    tracing::{instrument, Instrument},
36};
37
38#[async_trait]
39/// Defines the validator state machine of the staking component.
40///
41/// # Overview
42/// An interface to the validator state machine.
43///
44/// ## Validator management
45/// - Add validator definition via [`add_validator`].
46/// - Update validator definitions via [`update_validator_definition`].
47/// - Process byzantine behavior evidence via [`process_evidence`].
48///
49/// ## State machine interface
50/// - A fallible state transition function via [`set_validator_state`].
51/// - A safer handle to tentatively explore state transitions via [`try_precursor_transition`].
52///
53/// # State machine diagram:
54/// ```plaintext
55///             ┌───────────────────────────────────────────────────────┐
56///             │                      ┌──────────────────────────────┐ │
57///             ▼                      ▼                              │ │
58///     ╔═══════════════╗       ┌────────────┐                        │ │
59///  ┌─▶║    Defined    ║◀─────▶│  Disabled  │                        │ │
60///  │  ╚═══════════════╝       └────────────┘                        │ │
61///  │          │                      │                              │ │
62///  │          │                      ▼                              │ │
63///  │          │               ┏━━━━━━━━━━━━┓                        │ │
64///  │          └──────────────▶┃            ┃                        │ │
65///  │                          ┃ Tombstoned ┃◀────────────┐          │ │
66///  │          ┌──────────────▶┃            ┃             │          │ │
67///  │          │               ┗━━━━━━━━━━━━┛             │          │ │
68///  │          │                      ▲                   │          │ │
69///  │          │                      │           ┌──────────────┐   │ │
70///  │   ┌─────────────┐        ┌────────────┐     │              │◀──┘ │
71///  └──▶│   Jailed    │◀───────│   Active   │◀───▶│   Inactive   │     │
72///      └─────────────┘        └────────────┘     │              │◀────┘
73///             │                                  └──────────────┘
74///             │                                          ▲
75///             └──────────────────────────────────────────┘
76///
77///     ╔═════════════════╗
78///     ║ starting state  ║
79///     ╚═════════════════╝
80///     ┏━━━━━━━━━━━━━━━━━┓
81///     ┃ terminal state  ┃
82///     ┗━━━━━━━━━━━━━━━━━┛
83/// ```
84///
85/// [`add_validator`]: Self::add_validator
86/// [`update_validator_definition`]: Self::update_validator_definition
87/// [`set_validator_state`]: Self::set_validator_state
88/// [`try_precursor_transition`]: Self::try_precursor_transition
89/// [`process_evidence`]: Self::process_evidence
90pub trait ValidatorManager: StateWrite {
91    /// Execute a legal state transition, updating the validator records and
92    /// implementing the necessary side effects.
93    ///
94    /// Returns a `(old_state, new_state)` tuple, corresponding to the executed transition.
95    ///
96    /// # Errors
97    /// This method errors on illegal state transitions, but will otherwise try to do what
98    /// you ask it to do. It is the caller's responsibility to ensure that the state transitions
99    /// are legal and pertinent.
100    ///
101    /// An error can also happen if the state is corrupted or pushed into an incoherent mode
102    /// in this case, we return an error but there is no way to recover from those.
103    async fn set_validator_state(
104        &mut self,
105        identity_key: &IdentityKey,
106        new_state: validator::State,
107    ) -> Result<(State, State)> {
108        let old_state = self
109            .get_validator_state(identity_key)
110            .await?
111            .ok_or_else(|| {
112                anyhow::anyhow!("validator state not found for validator {}", identity_key)
113            })?;
114
115        // Delegating to an inner method here lets us create a span that has both states,
116        // without having to manage span entry/exit in async code.
117        self.set_validator_state_inner(identity_key, old_state, new_state)
118            .await
119    }
120
121    // Inner function pretends to be the outer one, so we can include `old_state`
122    // in the tracing span.  This way, we don't need to include any information
123    // in tracing events inside the function about what the state transition is,
124    // because it's already attached to the span.
125    #[instrument(skip(self), name = "set_validator_state")]
126    async fn set_validator_state_inner(
127        &mut self,
128        identity_key: &IdentityKey,
129        old_state: validator::State,
130        new_state: validator::State,
131    ) -> Result<(State, State)> {
132        let validator_state_path = state_key::validators::state::by_id(identity_key);
133
134        let current_height = self.get_block_height().await?;
135
136        // Using the start height of the current epoch let us do block based unbonding delays without
137        // requiring to bind actions to a specific block height (instead they bind to a whole epoch).
138        let unbonding_start_height = self.get_epoch_by_height(current_height).await?.start_height;
139
140        tracing::debug!("trying to execute a state transition");
141
142        // Validator state transitions are usually triggered by an epoch transition. The exception
143        // to this rule is when a validator exits the active set. In this case, we want to end the
144        // current epoch early in order to hold that validator transitions happen at epoch boundaries.
145        if let (Active, Defined | Disabled | Jailed | Tombstoned) = (old_state, new_state) {
146            tracing::info!("signaling early epoch end as a result of validator state transition");
147            self.set_end_epoch_flag();
148        }
149
150        // Determine if the state transition is valid, returning an error otherwise.
151        match (old_state, new_state) {
152            (Defined | Disabled | Jailed, Inactive) => {
153                // The validator has enough stake to be considered for the consensus set.
154                self.add_consensus_set_index(identity_key);
155            }
156            (Inactive | Jailed | Disabled, Defined) => {
157                // The validator's delegation pool has fallen below the `min_validator_stake` threshold.
158                // If necessary, the epoch-handler will deindex this validator after processing it.
159            }
160            (Inactive | Jailed | Defined, Disabled) => {
161                // The validator was disabled by its operator.
162                // If necessary, the epoch-handler will deindex this validator after processing it.
163                // We record the height at which the validator was disabled outside of the `match`.
164            }
165            (Inactive, Active) => {
166                // The delegator has been promoted into the active set, we initialize its uptime tracker,
167                // and bond its delegation pool.
168                self.set_validator_bonding_state(identity_key, Bonded);
169
170                // Track the validator uptime, overwriting any prior tracking data.
171                self.set_validator_uptime(
172                    identity_key,
173                    Uptime::new(
174                        self.get_block_height().await?,
175                        self.signed_blocks_window_len().await? as usize,
176                    ),
177                );
178
179                let power = self.get_validator_power(identity_key).await;
180                tracing::debug!(voting_power = ?power, "validator pool: bonded, uptime: tracked, setting validator to active");
181            }
182
183            (Active, Inactive | Defined | Disabled) => {
184                // When a validator is honorably discharged from the active set, we begin unbonding
185                // its delegation pool. The epoch-handler will decide whether it wants to keep it in
186                // the consensus set index or not.
187                // In the special case of a validator being disabled, we record the height at which it was disabled.
188                self.set_validator_bonding_state(
189                    identity_key,
190                    Unbonding {
191                        unbonds_at_height: self
192                            .compute_unbonding_height(identity_key, unbonding_start_height)
193                            .await?
194                            .expect("active validators MUST be bonded"),
195                    },
196                );
197            }
198
199            (Active, Jailed) => {
200                // An active validator has missed too many blocks, we penalize it by
201                // slashing its delegation pool, forbid new delegations, and start
202                // unbonding its delegated stake.  The epoch-handler is responsible
203                // for removing this identity from the consensus set index.
204                let penalty = self.get_stake_params().await?.slashing_penalty_downtime;
205
206                // Record the slashing penalty on this validator.
207                self.record_slashing_penalty(identity_key, Penalty::from_bps_squared(penalty))
208                    .await;
209
210                // The validator's delegation pool begins unbonding.  Jailed
211                // validators are not unbonded immediately, because they need to
212                // be held accountable for byzantine behavior for the entire
213                // unbonding period.
214                let unbonds_at_height = self
215                    .compute_unbonding_height(identity_key, unbonding_start_height)
216                    .await?
217                    .expect("active validators MUST be bonded");
218
219                self.set_validator_bonding_state(identity_key, Unbonding { unbonds_at_height });
220
221                tracing::debug!(penalty, unbonds_at_height, "jailed validator");
222            }
223
224            (Defined | Disabled | Inactive | Active | Jailed, Tombstoned) => {
225                // When we detect byzantine misbehavior from a validator, we:
226                // 1. Record the maximum slashing penalty for the corresponding pool
227                // 2. Immediately unbond its delegation pool
228                // 3. Forbid new delegations
229                //
230                // Later, during end-epoch processing, we remove the validator from the
231                // consensus set index.
232                let misbehavior_penalty =
233                    self.get_stake_params().await?.slashing_penalty_misbehavior;
234
235                // Record the slashing penalty on this validator.
236                self.record_slashing_penalty(
237                    identity_key,
238                    Penalty::from_bps_squared(misbehavior_penalty),
239                )
240                .await;
241
242                // Regardless of its current bonding state, the validator's
243                // delegation pool is unbonded immediately, because the
244                // validator has already had the maximum slashing penalty
245                // applied.
246                self.set_validator_bonding_state(identity_key, Unbonded);
247
248                tracing::info!(
249                    misbehavior_penalty,
250                    "validator has been tombstoned and slashed"
251                );
252            }
253
254            /* Identities: no-ops */
255            (Tombstoned, Tombstoned) => {
256                tracing::debug!("validator is already tombstoned");
257                // See discussion in https://github.com/penumbra-zone/penumbra/pull/3761 for context.
258                // The abridged summary is that applying a misbehavior penalty once and immediately
259                // unbonding the validator's delegation pool should be enough to deter misbehavior.
260                // Considering every single misbehavior actions as "counts" that accumulate runs the
261                // risk of cratering misconfigured validator into oblivion.
262            }
263            (Defined, Defined) => { /* no-op */ }
264            (Inactive, Inactive) => { /* no-op */ }
265            (Active, Active) => { /* no-op */ }
266            (Jailed, Jailed) => { /* no-op */ }
267            (Disabled, Disabled) => { /* no-op */ }
268
269            /* Bad transitions */
270            (Disabled | Defined | Jailed, Active) => {
271                anyhow::bail!(
272                    "only inactive validators can become active (identity={}, old_state={:?}, new_state={:?})",
273                    identity_key,
274                    old_state,
275                    new_state,
276                )
277            }
278            (Disabled | Defined | Inactive, Jailed) => {
279                anyhow::bail!(
280                    "only active validators can get jailed (identity={}, old_state={:?}, new_state={:?})",
281                    identity_key,
282                    old_state,
283                    new_state
284                )
285            }
286            (Tombstoned, Defined | Disabled | Inactive | Active | Jailed) => {
287                anyhow::bail!(
288                    "tombstoning is permanent, identity_key={}, new_state={:?}",
289                    identity_key,
290                    new_state
291                )
292            }
293        }
294
295        // Now that we have validated the state transition, we can record the last disabled height.
296        // Doing this here lets us keep the match statement clean and focused on the critical transitions.
297        if new_state == Disabled {
298            self.set_last_disabled_height(identity_key, current_height)
299        }
300
301        // At this point, we are guaranteed that this state transition is valid.
302        tracing::info!("successful state transition");
303        self.put(validator_state_path, new_state);
304
305        self.record_proto(
306            event::EventValidatorStateChange {
307                identity_key: *identity_key,
308                state: new_state,
309            }
310            .to_proto(),
311        );
312
313        Ok((old_state, new_state))
314    }
315
316    #[instrument(skip(self))]
317    /// Try to perform a state transition in/out of the `Defined` precursor state.
318    /// If successful, returns the new state, otherwise returns `None`.
319    async fn try_precursor_transition(
320        &mut self,
321        validator_id: &IdentityKey,
322        previous_state: validator::State,
323        next_rate: &RateData,
324        delegation_token_supply: Amount,
325    ) -> Option<State> {
326        // Conspicuously missing from this list are `Jailed | Disabled` validators.
327        // This is because their transition MUST be triggered by a manual validator upload.
328        if !matches!(previous_state, Defined | Inactive | Active) {
329            return None;
330        }
331
332        let min_stake = self
333            .get_stake_params()
334            .await
335            .expect("staking parameters are always set")
336            .min_validator_stake;
337
338        // We convert the delegation pool into staking tokens so that we can decide whether
339        // the validator meets the minimum stake threshold.
340        let unbonded_pool = next_rate.unbonded_amount(delegation_token_supply);
341
342        tracing::debug!(
343            %validator_id,
344            ?delegation_token_supply,
345            ?unbonded_pool,
346            next_validator_exchange_rate = ?next_rate.validator_exchange_rate,
347            ?previous_state,
348            ?min_stake,
349            "computed validator unbonded pool to explore precursor transition"
350        );
351
352        let has_minimum_stake = unbonded_pool >= min_stake;
353
354        // Refer yourself to the state machine diagram for the logic behind these transitions.
355        // Note: this could be refactored, no doubt, but it's going to look ugly either way.
356        let new_state = match previous_state {
357            Defined if has_minimum_stake => Inactive,
358            Defined if !has_minimum_stake => Defined,
359            Active if has_minimum_stake => Active,
360            Active if !has_minimum_stake => Defined,
361            Inactive if has_minimum_stake => Inactive,
362            Inactive if !has_minimum_stake => Defined,
363            _ => unreachable!("the previous state was validated by the guard condition"),
364        };
365
366        if new_state != previous_state {
367            let _ = self
368                .set_validator_state(validator_id, new_state)
369                .await
370                .expect("this must be a valid transition because we guard the method");
371        }
372
373        Some(new_state)
374    }
375
376    /// Add a new genesis validator starting in the [`Active`] state with its
377    /// genesis allocation entirely bonded.
378    #[instrument(skip(self, genesis_allocations))]
379    async fn add_genesis_validator(
380        &mut self,
381        genesis_allocations: &BTreeMap<asset::Id, Amount>,
382        genesis_base_rate: &BaseRateData,
383        validator: Validator,
384    ) -> Result<()> {
385        let initial_validator_rate = RateData {
386            identity_key: validator.identity_key.clone(),
387            validator_reward_rate: genesis_base_rate.base_reward_rate.clone(),
388            validator_exchange_rate: genesis_base_rate.base_exchange_rate.clone(),
389        };
390        // The initial allocations to the validator are specified in `genesis_allocations`.
391        // In this case, the validator's delegation pool size is exactly its allocation
392        // because we hardcoded the exchange rate to 1.
393        let delegation_id = DelegationToken::from(validator.identity_key.clone()).id();
394        let total_delegation_tokens = genesis_allocations
395            .get(&delegation_id)
396            .copied()
397            .unwrap_or_else(Amount::zero);
398        let power = initial_validator_rate.voting_power(total_delegation_tokens);
399
400        tracing::debug!(?initial_validator_rate, ?power, "adding genesis validator");
401
402        self.add_validator_inner(
403            validator.clone(),
404            initial_validator_rate,
405            // All genesis validators start in the "Active" state:
406            validator::State::Active,
407            // All genesis validators start in the "Bonded" bonding state:
408            validator::BondingState::Bonded,
409            power,
410            total_delegation_tokens,
411        )
412        .await?;
413
414        // Here, we are in the special case of genesis validators. Since they start in
415        // the active state we need to bundle the effects of the `Inactive -> Active`
416        // state transition:
417        // - add them to the consensus set index
418        // - track their uptime
419        self.add_consensus_set_index(&validator.identity_key);
420        self.set_validator_uptime(
421            &validator.identity_key,
422            Uptime::new(0, self.signed_blocks_window_len().await? as usize),
423        );
424
425        Ok(())
426    }
427
428    /// Add a validator after genesis, which will start in a [`validator::State::Defined`]
429    /// state with zero voting power, and unbonded delegation tokens. This is the default
430    /// "initial" state for a validator.
431    async fn add_validator(&mut self, validator: Validator, rate_data: RateData) -> Result<()> {
432        // We don't immediately report the validator voting power to CometBFT
433        // until it becomes active.
434        self.add_validator_inner(
435            validator.clone(),
436            rate_data,
437            validator::State::Defined,
438            validator::BondingState::Unbonded,
439            Amount::zero(),
440            Amount::zero(),
441        )
442        .await
443    }
444
445    /// Record a new validator definition and prime its initial state.
446    /// This method is used for both genesis and post-genesis validators.
447    /// In the former case, the validator starts in `[validator::State::Active]`
448    /// state, while in the latter case, it starts in `[validator::State::Defined]`.
449    ///
450    /// # Errors
451    /// This method errors if the initial state is not one of the two valid
452    /// initial states. Or if the voting power is negative.
453    #[instrument(skip(self))]
454    async fn add_validator_inner(
455        &mut self,
456        validator: Validator,
457        initial_rate_data: RateData,
458        initial_state: validator::State,
459        initial_bonding_state: validator::BondingState,
460        initial_voting_power: Amount,
461        initial_delegation_pool_size: Amount,
462    ) -> Result<()> {
463        tracing::debug!("adding validator");
464        if !matches!(initial_state, State::Defined | State::Active) {
465            anyhow::bail!(
466                "validator (identity_key={}) cannot have initial_state={:?}",
467                validator.identity_key,
468                initial_state
469            )
470        }
471        let validator_identity = validator.identity_key.clone();
472
473        // First, we record the validator definition in the general validator index:
474        self.put(
475            state_key::validators::definitions::by_id(&validator_identity),
476            validator.clone(),
477        );
478        // Then, we create a mapping from the validator's consensus key to its
479        // identity key, so we can look up the validator by its consensus key, and
480        // vice-versa.
481        self.register_consensus_key(&validator_identity, &validator.consensus_key);
482        // We register the validator's delegation token in the token registry...
483        self.register_denom(&DelegationToken::from(&validator_identity).denom())
484            .await;
485        // ... and its reward rate data in the JMT.
486        self.set_validator_rate_data(&validator_identity, initial_rate_data);
487
488        // Track the validator's definition in an event (the rest of the attributes will be tracked
489        // in events emitted by the calls to set_* methods below).
490        self.record_proto(
491            event::EventValidatorDefinitionUpload {
492                validator: validator.clone(),
493            }
494            .to_proto(),
495        );
496
497        // We initialize the validator's state, power, and bonding state.
498        self.set_initial_validator_state(&validator_identity, initial_state)?;
499        self.set_validator_power(&validator_identity, initial_voting_power)?;
500        self.set_validator_bonding_state(&validator_identity, initial_bonding_state);
501        self.set_validator_pool_size(&validator_identity, initial_delegation_pool_size);
502
503        metrics::gauge!(metrics::MISSED_BLOCKS, "identity_key" => validator_identity.to_string())
504            .increment(0.0);
505
506        Ok(())
507    }
508
509    /// Create a new validator definition or update an existing one.
510    /// # Errors
511    /// This method errors if the validator state is not found in the state,
512    /// or if the validator definition has been disabled too recently.
513    #[tracing::instrument(skip(self, validator), fields(id = ?validator.identity_key))]
514    async fn update_validator_definition(&mut self, validator: Validator) -> Result<()> {
515        tracing::debug!(definition = ?validator, "updating validator definition");
516        let id = &validator.identity_key;
517        let current_state = self
518            .get_validator_state(id)
519            .await?
520            .ok_or_else(|| anyhow::anyhow!("updated validator has no recorded state"))?;
521
522        tracing::debug!(?current_state, ?validator.enabled, "updating validator state");
523
524        match (current_state, validator.enabled) {
525            (Active | Inactive | Jailed | Defined | Disabled, false) => {
526                // The operator has disabled their validator.
527                self.set_validator_state(id, Disabled).await?;
528            }
529            (Disabled, true) => {
530                let last_disabled_height = self.get_last_disabled_height(id).await;
531                if let Some(last_disabled) = last_disabled_height {
532                    let current_height = self.get_block_height().await?;
533                    let epoch_duration = self.get_sct_params().await?.epoch_duration;
534
535                    // The actual delay is not too load-bearing, what we want here is to make sure that
536                    // there is a buffer between the last disabled height and the current height.
537                    // See #4067 for details about epoch-grinding.
538                    let allowed_enabled_height = last_disabled.saturating_add(epoch_duration);
539                    let wait_duration = current_height.saturating_sub(allowed_enabled_height);
540                    ensure!(
541                        current_height >= allowed_enabled_height,
542                        "validator has been disabled too recently (last_disabled={}, current_height={}, epoch_duration={}), wait {} blocks",
543                        last_disabled,
544                        current_height,
545                        epoch_duration,
546                        wait_duration
547                    );
548                } else {
549                    tracing::warn!(validator_identity = %id, "validator has no recorded last_disabled_height but is disabled");
550                }
551                // The operator has re-enabled their validator, if it has enough stake it will become
552                // inactive, otherwise it will become defined.
553                let min_validator_stake = self.get_stake_params().await?.min_validator_stake;
554                let current_validator_rate =
555                    self.get_validator_rate(id).await?.ok_or_else(|| {
556                        anyhow::anyhow!("updated validator has no recorded rate data")
557                    })?;
558                let delegation_token_supply = self
559                    .get_validator_pool_size(id)
560                    .await
561                    .unwrap_or_else(Amount::zero);
562                let unbonded_amount =
563                    current_validator_rate.unbonded_amount(delegation_token_supply);
564
565                if unbonded_amount >= min_validator_stake {
566                    self.set_validator_state(id, Inactive).await?;
567                } else {
568                    self.set_validator_state(id, Defined).await?;
569                }
570            }
571            (Jailed, true) => {
572                // Treat updates to jailed validators as unjail requests.
573                // If the validator has enough stake, it will become inactive, otherwise it will become defined.
574                let min_validator_stake = self.get_stake_params().await?.min_validator_stake;
575                let validator_rate_data = self
576                    .get_validator_rate(id)
577                    .await?
578                    .ok_or_else(|| anyhow::anyhow!("updated validator has no recorded state"))?;
579                let delegation_pool_size = self
580                    .get_validator_pool_size(id)
581                    .await
582                    .unwrap_or_else(Amount::zero);
583
584                let unbonded_pool_size = validator_rate_data.unbonded_amount(delegation_pool_size);
585
586                if unbonded_pool_size >= min_validator_stake {
587                    self.set_validator_state(id, Inactive).await?;
588                } else {
589                    self.set_validator_state(id, Defined).await?;
590                }
591            }
592            (Active | Inactive, true) => {
593                // This validator update does not affect the validator's state.
594            }
595            (Defined, true) => {
596                // This validator update does not affect the validator's state.
597            }
598            (Tombstoned, _) => {
599                // Ignore updates to tombstoned validators.
600            }
601        }
602
603        // Update the consensus key lookup, in case the validator rotated their
604        // consensus key.
605        self.register_consensus_key(&validator.identity_key, &validator.consensus_key);
606
607        self.put(
608            state_key::validators::definitions::by_id(id),
609            validator.clone(),
610        );
611
612        // Track the validator's definition in an event.
613        self.record_proto(event::EventValidatorDefinitionUpload { validator }.to_proto());
614
615        Ok(())
616    }
617
618    /// Update the validator pool's bonding state.
619    #[instrument(skip(self))]
620    async fn process_validator_pool_state(
621        &mut self,
622        validator_identity: &IdentityKey,
623        from_height: u64,
624    ) -> Result<()> {
625        let pool_state = self.get_validator_bonding_state(validator_identity).await;
626
627        // If the pool is already unbonded, this will return the current epoch.
628        let allowed_unbonding_height = self
629            .compute_unbonding_height(validator_identity, from_height)
630            .await?
631            .unwrap_or(from_height); // If the pool is unbonded, the unbonding height is the current height.
632
633        tracing::debug!(
634            ?pool_state,
635            ?allowed_unbonding_height,
636            "processing validator pool state"
637        );
638
639        if from_height >= allowed_unbonding_height {
640            // The validator's delegation pool has finished unbonding, so we
641            // transition it to the Unbonded state.
642            let _ = self
643                .set_validator_bonding_state(validator_identity, Unbonded)
644                .instrument(tracing::debug_span!(
645                    "validator_pool_unbonded",
646                    ?validator_identity
647                ));
648        }
649
650        Ok(())
651    }
652
653    /// Process evidence of byzantine behavior from CometBFT.
654    ///
655    /// Evidence *MUST* be processed before `end_block` is called, because
656    /// the evidence may trigger a validator state transition requiring
657    /// an early epoch change.
658    ///
659    /// # Errors
660    /// Returns an error if the validator is not found in the JMT.
661    async fn process_evidence(&mut self, evidence: &Misbehavior) -> Result<()> {
662        let validator = self
663            .get_validator_definition_by_cometbft_address(&evidence.validator.address)
664            .await?
665            .ok_or_else(|| {
666                anyhow::anyhow!(
667                    "attempted to slash unknown validator with evidence={:?}",
668                    evidence
669                )
670            })?;
671
672        let (old_state, new_state) = self
673            .set_validator_state(&validator.identity_key, validator::State::Tombstoned)
674            .await?;
675
676        if let (Inactive | Jailed | Active, Tombstoned) = (old_state, new_state) {
677            let current_height = self.get_block_height().await?;
678            self.record_proto(
679                event::EventTombstoneValidator::from_evidence(
680                    current_height,
681                    validator.identity_key.clone(),
682                    evidence,
683                )
684                .to_proto(),
685            );
686        }
687
688        Ok(())
689    }
690}
691
692impl<T: StateWrite + ?Sized> ValidatorManager for T {}