penumbra_sdk_governance/
component.rs

1use std::sync::Arc;
2
3use crate::{event, genesis};
4use anyhow::{Context, Result};
5use async_trait::async_trait;
6use cnidarium::StateWrite;
7use penumbra_sdk_proto::StateWriteProto as _;
8use tendermint::v0_37::abci;
9use tracing::instrument;
10
11use cnidarium_component::Component;
12
13use crate::{
14    proposal_state::{
15        Outcome as ProposalOutcome, State as ProposalState, Withdrawn as ProposalWithdrawn,
16    },
17    tally,
18};
19
20mod view;
21
22pub mod rpc;
23
24pub use view::StateReadExt;
25pub use view::StateWriteExt;
26
27use penumbra_sdk_sct::component::clock::EpochRead;
28
29pub struct Governance {}
30
31#[async_trait]
32impl Component for Governance {
33    type AppState = genesis::Content;
34
35    #[instrument(name = "governance", skip(state, app_state))]
36    async fn init_chain<S: StateWrite>(mut state: S, app_state: Option<&Self::AppState>) {
37        match app_state {
38            Some(genesis) => {
39                state.put_governance_params(genesis.governance_params.clone());
40                // Clients need to be able to read the next proposal number, even when no proposals have
41                // been submitted yet
42                state.init_proposal_counter();
43            }
44            None => {}
45        }
46    }
47
48    #[instrument(name = "governance", skip(_state, _begin_block))]
49    async fn begin_block<S: StateWrite + 'static>(
50        _state: &mut Arc<S>,
51        _begin_block: &abci::request::BeginBlock,
52    ) {
53    }
54
55    #[instrument(name = "governance", skip(state, _end_block))]
56    async fn end_block<S: StateWrite + 'static>(
57        state: &mut Arc<S>,
58        _end_block: &abci::request::EndBlock,
59    ) {
60        let mut state = Arc::get_mut(state).expect("state should be unique");
61        // Then, enact any proposals that have passed, after considering the tallies to determine what
62        // proposals have passed. Note that this occurs regardless of whether it's the end of an
63        // epoch, because proposals can finish at any time.
64        enact_all_passed_proposals(&mut state)
65            .await
66            .expect("enacting proposals should never fail");
67    }
68
69    #[instrument(name = "governance", skip(state))]
70    async fn end_epoch<S: StateWrite + 'static>(state: &mut Arc<S>) -> Result<()> {
71        let state = Arc::get_mut(state).expect("state should be unique");
72        state.tally_delegator_votes(None).await?;
73        Ok(())
74    }
75}
76
77#[instrument(skip(state))]
78pub async fn enact_all_passed_proposals<S: StateWrite>(mut state: S) -> Result<()> {
79    // For every unfinished proposal, conclude those that finish in this block
80    for proposal_id in state
81        .unfinished_proposals()
82        .await
83        .context("can get unfinished proposals")?
84    {
85        // TODO: this check will need to be altered when proposals have clock-time end times
86        let proposal_ready = state
87            .get_block_height()
88            .await
89            .expect("block height must be set")
90            >= state
91                .proposal_voting_end(proposal_id)
92                .await?
93                .context("proposal has voting end")?;
94
95        if !proposal_ready {
96            continue;
97        }
98
99        // Do a final tally of any pending delegator votes for the proposal
100        state.tally_delegator_votes(Some(proposal_id)).await?;
101
102        let current_state = state
103            .proposal_state(proposal_id)
104            .await?
105            .context("proposal has id")?;
106
107        let outcome = match current_state {
108            ProposalState::Voting => {
109                // If the proposal is still in the voting state, tally and conclude it (this will
110                // automatically remove it from the list of unfinished proposals)
111                let outcome = state.current_tally(proposal_id).await?.outcome(
112                    state
113                        .total_voting_power_at_proposal_start(proposal_id)
114                        .await?,
115                    &state.get_governance_params().await?,
116                );
117
118                // If the proposal passes, enact it now (or try to: if the proposal can't be
119                // enacted, continue onto the next one without throwing an error, just trace the
120                // error, since proposals are allowed to fail to be enacted)
121                match outcome {
122                    tally::Outcome::Pass => {
123                        // IMPORTANT: We **ONLY** enact proposals that have concluded, and whose
124                        // tally is `Pass`, and whose state is not `Withdrawn`. This is the sole
125                        // place in the codebase where we prevent withdrawn proposals from being
126                        // passed!
127                        let payload = state
128                            .proposal_payload(proposal_id)
129                            .await?
130                            .context("proposal has payload")?;
131                        match state.enact_proposal(proposal_id, &payload).await? {
132                            Ok(()) => {
133                                tracing::info!(proposal = %proposal_id, "proposal passed and enacted successfully");
134                            }
135                            Err(error) => {
136                                tracing::warn!(proposal = %proposal_id, %error, "proposal passed but failed to enact");
137                            }
138                        };
139
140                        let proposal =
141                            state
142                                .proposal_definition(proposal_id)
143                                .await?
144                                .ok_or_else(|| {
145                                    anyhow::anyhow!("proposal {} does not exist", proposal_id)
146                                })?;
147                        state.record_proto(event::proposal_passed(&proposal));
148                    }
149                    tally::Outcome::Fail => {
150                        tracing::info!(proposal = %proposal_id, "proposal failed");
151
152                        let proposal =
153                            state
154                                .proposal_definition(proposal_id)
155                                .await?
156                                .ok_or_else(|| {
157                                    anyhow::anyhow!("proposal {} does not exist", proposal_id)
158                                })?;
159                        state.record_proto(event::proposal_failed(&proposal));
160                    }
161                    tally::Outcome::Slash => {
162                        tracing::info!(proposal = %proposal_id, "proposal slashed");
163
164                        let proposal =
165                            state
166                                .proposal_definition(proposal_id)
167                                .await?
168                                .ok_or_else(|| {
169                                    anyhow::anyhow!("proposal {} does not exist", proposal_id)
170                                })?;
171                        state.record_proto(event::proposal_slashed(&proposal));
172                    }
173                }
174
175                outcome.into()
176            }
177            ProposalState::Withdrawn { reason } => {
178                tracing::info!(proposal = %proposal_id, reason = ?reason, "proposal concluded after being withdrawn");
179                ProposalOutcome::Failed {
180                    withdrawn: ProposalWithdrawn::WithReason { reason },
181                }
182            }
183            ProposalState::Finished { outcome: _ } => {
184                anyhow::bail!("proposal {proposal_id} is already finished, and should have been removed from the active set");
185            }
186            ProposalState::Claimed { outcome: _ } => {
187                anyhow::bail!("proposal {proposal_id} is already claimed, and should have been removed from the active set");
188            }
189        };
190
191        // Update the proposal state to reflect the outcome
192        state.put_proposal_state(proposal_id, ProposalState::Finished { outcome });
193    }
194
195    Ok(())
196}