penumbra_sdk_stake/component/action_handler/
undelegate_claim.rs

1use anyhow::{ensure, Result};
2use async_trait::async_trait;
3use cnidarium::StateWrite;
4use penumbra_sdk_proof_params::CONVERT_PROOF_VERIFICATION_KEY;
5use penumbra_sdk_sct::component::clock::EpochRead;
6
7use crate::component::validator_handler::ValidatorDataRead;
8use crate::component::SlashingData;
9use crate::undelegate_claim::UndelegateClaimProofPublic;
10use crate::UndelegateClaim;
11use crate::{component::action_handler::ActionHandler, UnbondingToken};
12
13#[async_trait]
14impl ActionHandler for UndelegateClaim {
15    type CheckStatelessContext = ();
16    async fn check_stateless(&self, _context: ()) -> Result<()> {
17        let unbonding_id = UnbondingToken::new(
18            self.body.validator_identity,
19            self.body.unbonding_start_height,
20        )
21        .id();
22
23        self.proof.verify(
24            &CONVERT_PROOF_VERIFICATION_KEY,
25            UndelegateClaimProofPublic {
26                balance_commitment: self.body.balance_commitment,
27                unbonding_id,
28                penalty: self.body.penalty,
29            },
30        )?;
31
32        Ok(())
33    }
34
35    async fn check_and_execute<S: StateWrite>(&self, state: S) -> Result<()> {
36        // These checks all formerly happened in the `check_historical` method,
37        // if profiling shows that they cause a bottleneck we could (CAREFULLY)
38        // move some of them back.
39
40        let current_height = state.get_block_height().await?;
41        let unbonding_start_height = self.body.unbonding_start_height;
42        ensure!(
43            current_height >= unbonding_start_height,
44            "the unbonding start height must be less than or equal to the current height"
45        );
46
47        // Compute the unbonding height for the claim, and check that it is less than or equal to the current height.
48        // If the pool is `Unbonded` or unbonding at an already elapsed height, we default to the current height.
49        let allowed_unbonding_height = state
50            .compute_unbonding_height(&self.body.validator_identity, unbonding_start_height)
51            .await?
52            .unwrap_or(current_height);
53
54        let wait_blocks = allowed_unbonding_height.saturating_sub(current_height);
55
56        ensure!(
57            current_height >= allowed_unbonding_height,
58            "cannot claim unbonding tokens before height {} (currently at {}, wait {} blocks)",
59            allowed_unbonding_height,
60            current_height,
61            wait_blocks
62        );
63
64        let unbonding_epoch_start = state
65            .get_epoch_by_height(self.body.unbonding_start_height)
66            .await?;
67        let unbonding_epoch_end = state.get_epoch_by_height(allowed_unbonding_height).await?;
68
69        // This should never happen, but if it did we want to make sure that it wouldn't
70        // crash the mempool.
71        ensure!(
72            unbonding_epoch_end.index >= unbonding_epoch_start.index,
73            "unbonding epoch end must be greater than or equal to unbonding epoch start"
74        );
75
76        // Compute the penalty for the epoch range [unbonding_epoch_start, unbonding_epoch_end], and check
77        // that it matches the penalty in the claim.
78        let expected_penalty = state
79            .compounded_penalty_over_range(
80                &self.body.validator_identity,
81                unbonding_epoch_start.index,
82                unbonding_epoch_end.index,
83            )
84            .await?;
85
86        ensure!(
87            self.body.penalty == expected_penalty,
88            "penalty (kept_rate: {}) does not match expected penalty (kept_rate: {})",
89            self.body.penalty.kept_rate(),
90            expected_penalty.kept_rate(),
91        );
92
93        /* ---------- execution ----------- */
94        // No state changes here - this action just converts one token to another
95
96        Ok(())
97    }
98}