penumbra_governance/
component.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
use std::sync::Arc;

use crate::{event, genesis};
use anyhow::{Context, Result};
use async_trait::async_trait;
use cnidarium::StateWrite;
use penumbra_proto::StateWriteProto as _;
use tendermint::v0_37::abci;
use tracing::instrument;

use cnidarium_component::Component;

use crate::{
    proposal_state::{
        Outcome as ProposalOutcome, State as ProposalState, Withdrawn as ProposalWithdrawn,
    },
    tally,
};

mod view;

pub mod rpc;

pub use view::StateReadExt;
pub use view::StateWriteExt;

use penumbra_sct::component::clock::EpochRead;

pub struct Governance {}

#[async_trait]
impl Component for Governance {
    type AppState = genesis::Content;

    #[instrument(name = "governance", skip(state, app_state))]
    async fn init_chain<S: StateWrite>(mut state: S, app_state: Option<&Self::AppState>) {
        match app_state {
            Some(genesis) => {
                state.put_governance_params(genesis.governance_params.clone());
                // Clients need to be able to read the next proposal number, even when no proposals have
                // been submitted yet
                state.init_proposal_counter();
            }
            None => {}
        }
    }

    #[instrument(name = "governance", skip(_state, _begin_block))]
    async fn begin_block<S: StateWrite + 'static>(
        _state: &mut Arc<S>,
        _begin_block: &abci::request::BeginBlock,
    ) {
    }

    #[instrument(name = "governance", skip(state, _end_block))]
    async fn end_block<S: StateWrite + 'static>(
        state: &mut Arc<S>,
        _end_block: &abci::request::EndBlock,
    ) {
        let mut state = Arc::get_mut(state).expect("state should be unique");
        // Then, enact any proposals that have passed, after considering the tallies to determine what
        // proposals have passed. Note that this occurs regardless of whether it's the end of an
        // epoch, because proposals can finish at any time.
        enact_all_passed_proposals(&mut state)
            .await
            .expect("enacting proposals should never fail");
    }

    #[instrument(name = "governance", skip(state))]
    async fn end_epoch<S: StateWrite + 'static>(state: &mut Arc<S>) -> Result<()> {
        let state = Arc::get_mut(state).expect("state should be unique");
        state.tally_delegator_votes(None).await?;
        Ok(())
    }
}

#[instrument(skip(state))]
pub async fn enact_all_passed_proposals<S: StateWrite>(mut state: S) -> Result<()> {
    // For every unfinished proposal, conclude those that finish in this block
    for proposal_id in state
        .unfinished_proposals()
        .await
        .context("can get unfinished proposals")?
    {
        // TODO: this check will need to be altered when proposals have clock-time end times
        let proposal_ready = state
            .get_block_height()
            .await
            .expect("block height must be set")
            >= state
                .proposal_voting_end(proposal_id)
                .await?
                .context("proposal has voting end")?;

        if !proposal_ready {
            continue;
        }

        // Do a final tally of any pending delegator votes for the proposal
        state.tally_delegator_votes(Some(proposal_id)).await?;

        let current_state = state
            .proposal_state(proposal_id)
            .await?
            .context("proposal has id")?;

        let outcome = match current_state {
            ProposalState::Voting => {
                // If the proposal is still in the voting state, tally and conclude it (this will
                // automatically remove it from the list of unfinished proposals)
                let outcome = state.current_tally(proposal_id).await?.outcome(
                    state
                        .total_voting_power_at_proposal_start(proposal_id)
                        .await?,
                    &state.get_governance_params().await?,
                );

                // If the proposal passes, enact it now (or try to: if the proposal can't be
                // enacted, continue onto the next one without throwing an error, just trace the
                // error, since proposals are allowed to fail to be enacted)
                match outcome {
                    tally::Outcome::Pass => {
                        // IMPORTANT: We **ONLY** enact proposals that have concluded, and whose
                        // tally is `Pass`, and whose state is not `Withdrawn`. This is the sole
                        // place in the codebase where we prevent withdrawn proposals from being
                        // passed!
                        let payload = state
                            .proposal_payload(proposal_id)
                            .await?
                            .context("proposal has payload")?;
                        match state.enact_proposal(proposal_id, &payload).await? {
                            Ok(()) => {
                                tracing::info!(proposal = %proposal_id, "proposal passed and enacted successfully");
                            }
                            Err(error) => {
                                tracing::warn!(proposal = %proposal_id, %error, "proposal passed but failed to enact");
                            }
                        };

                        let proposal =
                            state
                                .proposal_definition(proposal_id)
                                .await?
                                .ok_or_else(|| {
                                    anyhow::anyhow!("proposal {} does not exist", proposal_id)
                                })?;
                        state.record_proto(event::proposal_passed(&proposal));
                    }
                    tally::Outcome::Fail => {
                        tracing::info!(proposal = %proposal_id, "proposal failed");

                        let proposal =
                            state
                                .proposal_definition(proposal_id)
                                .await?
                                .ok_or_else(|| {
                                    anyhow::anyhow!("proposal {} does not exist", proposal_id)
                                })?;
                        state.record_proto(event::proposal_failed(&proposal));
                    }
                    tally::Outcome::Slash => {
                        tracing::info!(proposal = %proposal_id, "proposal slashed");

                        let proposal =
                            state
                                .proposal_definition(proposal_id)
                                .await?
                                .ok_or_else(|| {
                                    anyhow::anyhow!("proposal {} does not exist", proposal_id)
                                })?;
                        state.record_proto(event::proposal_slashed(&proposal));
                    }
                }

                outcome.into()
            }
            ProposalState::Withdrawn { reason } => {
                tracing::info!(proposal = %proposal_id, reason = ?reason, "proposal concluded after being withdrawn");
                ProposalOutcome::Failed {
                    withdrawn: ProposalWithdrawn::WithReason { reason },
                }
            }
            ProposalState::Finished { outcome: _ } => {
                anyhow::bail!("proposal {proposal_id} is already finished, and should have been removed from the active set");
            }
            ProposalState::Claimed { outcome: _ } => {
                anyhow::bail!("proposal {proposal_id} is already claimed, and should have been removed from the active set");
            }
        };

        // Update the proposal state to reflect the outcome
        state.put_proposal_state(proposal_id, ProposalState::Finished { outcome });
    }

    Ok(())
}