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 {}