penumbra_sdk_governance/action_handler/
validator_vote.rs

1use anyhow::{Context, Result};
2use async_trait::async_trait;
3use cnidarium::StateWrite;
4use penumbra_sdk_proto::{DomainType, StateWriteProto as _};
5
6use crate::component::StateWriteExt;
7use crate::event;
8use crate::{action_handler::ActionHandler, StateReadExt};
9use crate::{
10    proposal_state::Outcome,
11    proposal_state::State as ProposalState,
12    {ValidatorVote, ValidatorVoteBody, MAX_VALIDATOR_VOTE_REASON_LENGTH},
13};
14
15#[async_trait]
16impl ActionHandler for ValidatorVote {
17    type CheckStatelessContext = ();
18    async fn check_stateless(&self, _context: ()) -> Result<()> {
19        let ValidatorVote { body, auth_sig } = self;
20
21        // Check the signature using the GOVERNANCE KEY:
22        let body_bytes = body.encode_to_vec();
23        body.governance_key
24            .0
25            .verify(&body_bytes, auth_sig)
26            .context("validator vote signature failed to verify")?;
27
28        // Check the length of the validator reason field.
29        if body.reason.0.len() > MAX_VALIDATOR_VOTE_REASON_LENGTH {
30            anyhow::bail!("validator vote reason is too long");
31        }
32
33        // This is stateless verification, so we still need to check that the proposal being voted
34        // on exists, and that this validator hasn't voted on it already.
35
36        Ok(())
37    }
38
39    async fn check_and_execute<S: StateWrite>(&self, mut state: S) -> Result<()> {
40        let ValidatorVote {
41            auth_sig: _,
42            body:
43                ValidatorVoteBody {
44                    proposal,
45                    vote,
46                    identity_key,
47                    governance_key,
48                    reason,
49                },
50        } = self;
51
52        state.check_proposal_votable(*proposal).await?;
53        state
54            .check_validator_active_at_proposal_start(*proposal, identity_key)
55            .await?;
56        state
57            .check_validator_has_not_voted(*proposal, identity_key)
58            .await?;
59        state
60            .check_governance_key_matches_validator(identity_key, governance_key)
61            .await?;
62
63        let proposal_state = state
64            .proposal_state(*proposal)
65            .await?
66            .expect("proposal missing state");
67
68        // TODO(erwan): Keeping this guard here, because there was previously a
69        // comment stressing that we want to avoid enacting withdrawn proposals.
70        // However, note that this is already checked in the stateful check and
71        // we execute against the same snapshotted state, so this seem redundant.
72        // I will remove it once in the PR review once this is confirmed.
73        if proposal_state.is_withdrawn() {
74            tracing::debug!(validator_identity = %identity_key, proposal = %proposal, "cannot cast a vote for a withdrawn proposal");
75            return Ok(());
76        }
77
78        tracing::debug!(validator_identity = %identity_key, proposal = %proposal, "cast validator vote");
79        state.cast_validator_vote(*proposal, *identity_key, *vote, reason.clone());
80
81        // Emergency proposals are passed immediately after receiving +1/3 of
82        // validator votes. These include the eponymous `Emergency` proposal but
83        // also `IbcFreeze` and `IbcUnfreeze`.
84        let proposal_payload = state
85            .proposal_payload(*proposal)
86            .await?
87            .expect("proposal missing payload");
88
89        if proposal_payload.is_emergency() || proposal_payload.is_ibc_freeze() {
90            tracing::debug!(proposal = %proposal, "detected an emergency-tier proposal, checking pass conditions");
91            let tally = state.current_tally(*proposal).await?;
92            let total_voting_power = state
93                .total_voting_power_at_proposal_start(*proposal)
94                .await?;
95            let governance_params = state.get_governance_params().await?;
96            if tally.emergency_pass(total_voting_power, &governance_params) {
97                // If the emergency pass condition is met, enact the proposal
98                tracing::debug!(proposal = %proposal, "emergency pass condition met, trying to enact proposal");
99                // Try to enact the proposal based on its payload
100                match state.enact_proposal(*proposal, &proposal_payload).await? {
101                    Ok(_) => tracing::debug!(proposal = %proposal, "emergency proposal enacted"),
102                    Err(error) => {
103                        tracing::error!(proposal = %proposal, %error, "error enacting emergency proposal")
104                    }
105                }
106                // Update the proposal state to reflect the outcome (it will always be passed,
107                // because we got to this point)
108                state.put_proposal_state(
109                    *proposal,
110                    ProposalState::Finished {
111                        outcome: Outcome::Passed,
112                    },
113                );
114            }
115        }
116
117        // Get the validator's voting power for the proposal
118        let voting_power = state
119            .specific_validator_voting_power_at_proposal_start(*proposal, *identity_key)
120            .await?;
121        state.record_proto(event::validator_vote(self, voting_power));
122
123        Ok(())
124    }
125}