From a0b0126ae9aa99e9b0ca72542d2c46ce86887feb Mon Sep 17 00:00:00 2001 From: redshiftzero Date: Mon, 18 Sep 2023 13:22:25 -0400 Subject: [PATCH] wip: move governance out of the app crate to its component crate --- Cargo.lock | 4 + crates/core/app/src/lib.rs | 1 - crates/core/component/governance/Cargo.toml | 7 +- .../governance/src/action_handler.rs | 1 + .../governance}/src/action_handler/actions.rs | 0 .../action_handler/actions/delegator_vote.rs | 0 .../actions/proposal/deposit_claim.rs | 0 .../action_handler/actions/proposal/mod.rs | 0 .../action_handler/actions/proposal/submit.rs | 0 .../actions/proposal/withdraw.rs | 0 .../action_handler/actions/validator_vote.rs | 0 .../src/action_handler/transaction.rs | 0 .../action_handler/transaction/stateful.rs | 0 .../action_handler/transaction/stateless.rs | 0 .../governance/src}/component.rs | 24 +- .../governance/src}/event.rs | 0 crates/core/component/governance/src/lib.rs | 21 + .../governance/src}/metrics.rs | 0 .../governance/src}/mod.rs | 0 .../governance}/src/proposal.rs | 386 +----------------- .../governance/src/proposal_state.rs | 386 ++++++++++++++++++ .../governance/src}/state_key.rs | 0 .../governance/src}/tally.rs | 9 +- .../governance/src}/view.rs | 5 +- .../governance}/src/vote.rs | 0 crates/core/transaction/src/lib.rs | 2 - 26 files changed, 444 insertions(+), 402 deletions(-) create mode 100644 crates/core/component/governance/src/action_handler.rs rename crates/core/{app => component/governance}/src/action_handler/actions.rs (100%) rename crates/core/{app => component/governance}/src/action_handler/actions/delegator_vote.rs (100%) rename crates/core/{app => component/governance}/src/action_handler/actions/proposal/deposit_claim.rs (100%) rename crates/core/{app => component/governance}/src/action_handler/actions/proposal/mod.rs (100%) rename crates/core/{app => component/governance}/src/action_handler/actions/proposal/submit.rs (100%) rename crates/core/{app => component/governance}/src/action_handler/actions/proposal/withdraw.rs (100%) rename crates/core/{app => component/governance}/src/action_handler/actions/validator_vote.rs (100%) rename crates/core/{app => component/governance}/src/action_handler/transaction.rs (100%) rename crates/core/{app => component/governance}/src/action_handler/transaction/stateful.rs (100%) rename crates/core/{app => component/governance}/src/action_handler/transaction/stateless.rs (100%) rename crates/core/{app/src/governance => component/governance/src}/component.rs (90%) rename crates/core/{app/src/governance => component/governance/src}/event.rs (100%) rename crates/core/{app/src/governance => component/governance/src}/metrics.rs (100%) rename crates/core/{app/src/governance => component/governance/src}/mod.rs (100%) rename crates/core/{transaction => component/governance}/src/proposal.rs (50%) create mode 100644 crates/core/component/governance/src/proposal_state.rs rename crates/core/{app/src/governance => component/governance/src}/state_key.rs (100%) rename crates/core/{app/src/governance => component/governance/src}/tally.rs (97%) rename crates/core/{app/src/governance => component/governance/src}/view.rs (99%) rename crates/core/{transaction => component/governance}/src/vote.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index f453a26419..400635741d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4915,19 +4915,23 @@ dependencies = [ "bytes", "decaf377 0.5.0", "decaf377-rdsa", + "futures", "im", "metrics", "penumbra-asset", "penumbra-chain", "penumbra-component", "penumbra-keys", + "penumbra-num", "penumbra-proof-params", "penumbra-proto", "penumbra-sct", "penumbra-shielded-pool", + "penumbra-stake", "penumbra-storage", "penumbra-tct", "proptest", + "proptest-derive", "rand 0.8.5", "rand_core 0.6.4", "regex", diff --git a/crates/core/app/src/lib.rs b/crates/core/app/src/lib.rs index e5ab205aa9..a128501396 100644 --- a/crates/core/app/src/lib.rs +++ b/crates/core/app/src/lib.rs @@ -8,7 +8,6 @@ pub use mock_client::MockClient; pub use temp_storage_ext::TempStorageExt; pub mod app; -pub mod governance; #[cfg(test)] mod tests; diff --git a/crates/core/component/governance/Cargo.toml b/crates/core/component/governance/Cargo.toml index e3acf3df61..222ddfa5d5 100644 --- a/crates/core/component/governance/Cargo.toml +++ b/crates/core/component/governance/Cargo.toml @@ -11,7 +11,8 @@ component = [ "penumbra-storage", "penumbra-proto/penumbra-storage", "penumbra-chain/component", - "penumbra-sct/component" + "penumbra-sct/component", + "penumbra-stake/component", ] proving-keys = ["penumbra-proof-params/proving-keys"] default = ["std", "component", "proving-keys"] @@ -28,9 +29,11 @@ penumbra-proof-params = { path = "../../../crypto/proof-params", default-feature penumbra-sct = { path = "../sct", default-features = false } penumbra-component = { path = "../component", optional = true } penumbra-shielded-pool = { path = "../shielded-pool", default-features = false } +penumbra-stake = { path = "../stake", default-features = false } penumbra-chain = { path = "../chain", default-features = false } penumbra-asset = { path = "../../../core/asset", default-features = false } penumbra-keys = { path = "../../../core/keys", default-features = false } +penumbra-num = { path = "../../../core/num", default-features = false } # Penumbra dependencies decaf377-rdsa = { version = "0.7" } @@ -56,6 +59,8 @@ rand_core = { version = "0.6.3", features = ["getrandom"] } rand = "0.8" im = "15.1" regex = "1.5" +futures = "0.3" [dev-dependencies] proptest = "1" +proptest-derive = "0.3" diff --git a/crates/core/component/governance/src/action_handler.rs b/crates/core/component/governance/src/action_handler.rs new file mode 100644 index 0000000000..16a4773348 --- /dev/null +++ b/crates/core/component/governance/src/action_handler.rs @@ -0,0 +1 @@ +use penumbra_component::ActionHandler; diff --git a/crates/core/app/src/action_handler/actions.rs b/crates/core/component/governance/src/action_handler/actions.rs similarity index 100% rename from crates/core/app/src/action_handler/actions.rs rename to crates/core/component/governance/src/action_handler/actions.rs diff --git a/crates/core/app/src/action_handler/actions/delegator_vote.rs b/crates/core/component/governance/src/action_handler/actions/delegator_vote.rs similarity index 100% rename from crates/core/app/src/action_handler/actions/delegator_vote.rs rename to crates/core/component/governance/src/action_handler/actions/delegator_vote.rs diff --git a/crates/core/app/src/action_handler/actions/proposal/deposit_claim.rs b/crates/core/component/governance/src/action_handler/actions/proposal/deposit_claim.rs similarity index 100% rename from crates/core/app/src/action_handler/actions/proposal/deposit_claim.rs rename to crates/core/component/governance/src/action_handler/actions/proposal/deposit_claim.rs diff --git a/crates/core/app/src/action_handler/actions/proposal/mod.rs b/crates/core/component/governance/src/action_handler/actions/proposal/mod.rs similarity index 100% rename from crates/core/app/src/action_handler/actions/proposal/mod.rs rename to crates/core/component/governance/src/action_handler/actions/proposal/mod.rs diff --git a/crates/core/app/src/action_handler/actions/proposal/submit.rs b/crates/core/component/governance/src/action_handler/actions/proposal/submit.rs similarity index 100% rename from crates/core/app/src/action_handler/actions/proposal/submit.rs rename to crates/core/component/governance/src/action_handler/actions/proposal/submit.rs diff --git a/crates/core/app/src/action_handler/actions/proposal/withdraw.rs b/crates/core/component/governance/src/action_handler/actions/proposal/withdraw.rs similarity index 100% rename from crates/core/app/src/action_handler/actions/proposal/withdraw.rs rename to crates/core/component/governance/src/action_handler/actions/proposal/withdraw.rs diff --git a/crates/core/app/src/action_handler/actions/validator_vote.rs b/crates/core/component/governance/src/action_handler/actions/validator_vote.rs similarity index 100% rename from crates/core/app/src/action_handler/actions/validator_vote.rs rename to crates/core/component/governance/src/action_handler/actions/validator_vote.rs diff --git a/crates/core/app/src/action_handler/transaction.rs b/crates/core/component/governance/src/action_handler/transaction.rs similarity index 100% rename from crates/core/app/src/action_handler/transaction.rs rename to crates/core/component/governance/src/action_handler/transaction.rs diff --git a/crates/core/app/src/action_handler/transaction/stateful.rs b/crates/core/component/governance/src/action_handler/transaction/stateful.rs similarity index 100% rename from crates/core/app/src/action_handler/transaction/stateful.rs rename to crates/core/component/governance/src/action_handler/transaction/stateful.rs diff --git a/crates/core/app/src/action_handler/transaction/stateless.rs b/crates/core/component/governance/src/action_handler/transaction/stateless.rs similarity index 100% rename from crates/core/app/src/action_handler/transaction/stateless.rs rename to crates/core/component/governance/src/action_handler/transaction/stateless.rs diff --git a/crates/core/app/src/governance/component.rs b/crates/core/component/governance/src/component.rs similarity index 90% rename from crates/core/app/src/governance/component.rs rename to crates/core/component/governance/src/component.rs index 061d2d9ae5..bab50ec0e3 100644 --- a/crates/core/app/src/governance/component.rs +++ b/crates/core/component/governance/src/component.rs @@ -4,13 +4,19 @@ use anyhow::{Context, Result}; use async_trait::async_trait; use penumbra_chain::component::StateReadExt; use penumbra_storage::StateWrite; -use penumbra_transaction::proposal; use tendermint::v0_34::abci; use tracing::instrument; -use super::{tally, StateReadExt as _, StateWriteExt as _}; use penumbra_component::Component; +use crate::{ + proposal_state::{ + Outcome as ProposalOutcome, State as ProposalState, Withdrawn as ProposalWithdrawn, + }, + tally, + view::{StateReadExt as _, StateWriteExt}, +}; + pub struct Governance {} #[async_trait] @@ -84,7 +90,7 @@ pub async fn enact_all_passed_proposals(mut state: S) -> Result<( .context("proposal has id")?; let outcome = match current_state { - proposal::State::Voting => { + 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( @@ -126,22 +132,22 @@ pub async fn enact_all_passed_proposals(mut state: S) -> Result<( outcome.into() } - proposal::State::Withdrawn { reason } => { + ProposalState::Withdrawn { reason } => { tracing::info!(proposal = %proposal_id, reason = ?reason, "proposal concluded after being withdrawn"); - proposal::Outcome::Failed { - withdrawn: proposal::Withdrawn::WithReason { reason }, + ProposalOutcome::Failed { + withdrawn: ProposalWithdrawn::WithReason { reason }, } } - proposal::State::Finished { outcome: _ } => { + ProposalState::Finished { outcome: _ } => { anyhow::bail!("proposal {proposal_id} is already finished, and should have been removed from the active set"); } - proposal::State::Claimed { outcome: _ } => { + 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, proposal::State::Finished { outcome }); + state.put_proposal_state(proposal_id, ProposalState::Finished { outcome }); } Ok(()) diff --git a/crates/core/app/src/governance/event.rs b/crates/core/component/governance/src/event.rs similarity index 100% rename from crates/core/app/src/governance/event.rs rename to crates/core/component/governance/src/event.rs diff --git a/crates/core/component/governance/src/lib.rs b/crates/core/component/governance/src/lib.rs index acba2d7138..e0ee8d3190 100644 --- a/crates/core/component/governance/src/lib.rs +++ b/crates/core/component/governance/src/lib.rs @@ -3,7 +3,28 @@ mod delegator_vote; pub use delegator_vote::proof::{DelegatorVoteCircuit, DelegatorVoteProof}; pub mod proposal_nft; +pub mod proposal_state; pub mod voting_receipt_token; pub use proposal_nft::ProposalNft; pub use voting_receipt_token::VotingReceiptToken; + +mod metrics; +mod state_key; +mod tally; +mod view; + +#[cfg_attr(docsrs, doc(cfg(feature = "component")))] +#[cfg(feature = "component")] +pub mod component; + +#[cfg_attr(docsrs, doc(cfg(feature = "component")))] +#[cfg(feature = "component")] +mod action_handler; + +#[cfg_attr(docsrs, doc(cfg(feature = "component")))] +#[cfg(feature = "component")] +pub use component::StateReadExt; + +pub mod proposal; +pub mod vote; diff --git a/crates/core/app/src/governance/metrics.rs b/crates/core/component/governance/src/metrics.rs similarity index 100% rename from crates/core/app/src/governance/metrics.rs rename to crates/core/component/governance/src/metrics.rs diff --git a/crates/core/app/src/governance/mod.rs b/crates/core/component/governance/src/mod.rs similarity index 100% rename from crates/core/app/src/governance/mod.rs rename to crates/core/component/governance/src/mod.rs diff --git a/crates/core/transaction/src/proposal.rs b/crates/core/component/governance/src/proposal.rs similarity index 50% rename from crates/core/transaction/src/proposal.rs rename to crates/core/component/governance/src/proposal.rs index a1ebf69127..1599bb07cb 100644 --- a/crates/core/transaction/src/proposal.rs +++ b/crates/core/component/governance/src/proposal.rs @@ -1,4 +1,5 @@ use anyhow::Context; +use base64::engine::Engine; use bytes::Bytes; use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -7,6 +8,7 @@ use penumbra_chain::params::ChainParameters; use penumbra_proto::{core::governance::v1alpha1 as pb, DomainType, TypeUrl}; use crate::plan::TransactionPlan; +use crate::proposal_state::State; /// The protobuf type URL for a transaction plan. pub const TRANSACTION_PLAN_TYPE_URL: &str = "/penumbra.core.transaction.v1alpha1.TransactionPlan"; @@ -62,6 +64,7 @@ impl TryFrom for Proposal { } } +// todo: needs to go to penumbra-transaction crate due to use of transaction plan impl From for pb::Proposal { fn from(inner: Proposal) -> pb::Proposal { let mut proposal = pb::Proposal { @@ -353,386 +356,3 @@ impl ProposalPayload { matches!(self, ProposalPayload::DaoSpend { .. }) } } - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(try_from = "pb::ProposalState", into = "pb::ProposalState")] -pub enum State { - Voting, - Withdrawn { reason: String }, - Finished { outcome: Outcome }, - Claimed { outcome: Outcome }, -} - -impl State { - pub fn is_voting(&self) -> bool { - matches!(self, State::Voting) - } - - pub fn is_withdrawn(&self) -> bool { - matches!(self, State::Withdrawn { .. }) - } - - pub fn is_finished(&self) -> bool { - matches!(self, State::Finished { .. }) - } - - pub fn is_claimed(&self) -> bool { - matches!(self, State::Claimed { .. }) - } - - pub fn is_passed(&self) -> bool { - match self { - State::Finished { outcome } => outcome.is_passed(), - State::Claimed { outcome } => outcome.is_passed(), - _ => false, - } - } - - pub fn is_failed(&self) -> bool { - match self { - State::Finished { outcome } => outcome.is_failed(), - State::Claimed { outcome } => outcome.is_failed(), - _ => false, - } - } - - pub fn is_slashed(&self) -> bool { - match self { - State::Finished { outcome } => outcome.is_slashed(), - State::Claimed { outcome } => outcome.is_slashed(), - _ => false, - } - } -} - -impl State { - pub fn withdrawn(self) -> Withdrawn { - match self { - State::Voting => Withdrawn::No, - State::Withdrawn { reason } => Withdrawn::WithReason { reason }, - State::Finished { outcome } => match outcome { - Outcome::Passed => Withdrawn::No, - Outcome::Failed { withdrawn } | Outcome::Slashed { withdrawn } => withdrawn, - }, - State::Claimed { outcome } => match outcome { - Outcome::Passed => Withdrawn::No, - Outcome::Failed { withdrawn } | Outcome::Slashed { withdrawn } => withdrawn, - }, - } - } -} - -impl TypeUrl for State { - const TYPE_URL: &'static str = "/penumbra.core.governance.v1alpha1.ProposalState"; -} - -impl DomainType for State { - type Proto = pb::ProposalState; -} - -impl From for pb::ProposalState { - fn from(s: State) -> Self { - let state = match s { - State::Voting => pb::proposal_state::State::Voting(pb::proposal_state::Voting {}), - State::Withdrawn { reason } => { - pb::proposal_state::State::Withdrawn(pb::proposal_state::Withdrawn { reason }) - } - State::Finished { outcome } => { - pb::proposal_state::State::Finished(pb::proposal_state::Finished { - outcome: Some(outcome.into()), - }) - } - State::Claimed { outcome } => { - pb::proposal_state::State::Finished(pb::proposal_state::Finished { - outcome: Some(outcome.into()), - }) - } - }; - pb::ProposalState { state: Some(state) } - } -} - -impl TryFrom for State { - type Error = anyhow::Error; - - fn try_from(msg: pb::ProposalState) -> Result { - Ok( - match msg - .state - .ok_or_else(|| anyhow::anyhow!("missing proposal state"))? - { - pb::proposal_state::State::Voting(pb::proposal_state::Voting {}) => State::Voting, - pb::proposal_state::State::Withdrawn(pb::proposal_state::Withdrawn { reason }) => { - State::Withdrawn { reason } - } - pb::proposal_state::State::Finished(pb::proposal_state::Finished { outcome }) => { - State::Finished { - outcome: outcome - .ok_or_else(|| anyhow::anyhow!("missing proposal outcome"))? - .try_into()?, - } - } - pb::proposal_state::State::Claimed(pb::proposal_state::Claimed { outcome }) => { - State::Claimed { - outcome: outcome - .ok_or_else(|| anyhow::anyhow!("missing proposal outcome"))? - .try_into()?, - } - } - }, - ) - } -} - -// This is parameterized by `W`, the withdrawal reason, so that we can use `()` where a reason -// doesn't need to be specified. When this is the case, the serialized format in protobufs uses an -// empty string. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde( - try_from = "pb::ProposalOutcome", - into = "pb::ProposalOutcome", - bound = "W: Clone, pb::ProposalOutcome: From>, Outcome: TryFrom" -)] -pub enum Outcome { - Passed, - Failed { withdrawn: Withdrawn }, - Slashed { withdrawn: Withdrawn }, -} - -impl Outcome { - /// Determines if the outcome should be refunded (i.e. it was not slashed). - pub fn should_be_refunded(&self) -> bool { - !self.is_slashed() - } - - pub fn is_slashed(&self) -> bool { - matches!(self, Outcome::Slashed { .. }) - } - - pub fn is_failed(&self) -> bool { - matches!(self, Outcome::Failed { .. } | Outcome::Slashed { .. }) - } - - pub fn is_passed(&self) -> bool { - matches!(self, Outcome::Passed) - } - - pub fn as_ref(&self) -> Outcome<&W> { - match self { - Outcome::Passed => Outcome::Passed, - Outcome::Failed { withdrawn } => Outcome::Failed { - withdrawn: withdrawn.as_ref(), - }, - Outcome::Slashed { withdrawn } => Outcome::Slashed { - withdrawn: withdrawn.as_ref(), - }, - } - } - - pub fn map(self, f: impl FnOnce(W) -> X) -> Outcome { - match self { - Outcome::Passed => Outcome::Passed, - Outcome::Failed { withdrawn } => Outcome::Failed { - withdrawn: Option::from(withdrawn).map(f).into(), - }, - Outcome::Slashed { withdrawn } => Outcome::Slashed { - withdrawn: Option::from(withdrawn).map(f).into(), - }, - } - } -} - -// This is parameterized by `W`, the withdrawal reason, so that we can use `()` where a reason -// doesn't need to be specified. When this is the case, the serialized format in protobufs uses an -// empty string. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Withdrawn { - No, - WithReason { reason: W }, -} - -impl Withdrawn { - pub fn as_ref(&self) -> Withdrawn<&W> { - match self { - Withdrawn::No => Withdrawn::No, - Withdrawn::WithReason { reason } => Withdrawn::WithReason { reason }, - } - } -} - -impl From> for Withdrawn { - fn from(reason: Option) -> Self { - match reason { - Some(reason) => Withdrawn::WithReason { reason }, - None => Withdrawn::No, - } - } -} - -impl From> for Option { - fn from(withdrawn: Withdrawn) -> Self { - match withdrawn { - Withdrawn::No => None, - Withdrawn::WithReason { reason } => Some(reason), - } - } -} - -impl TryFrom> for Withdrawn<()> { - type Error = anyhow::Error; - - fn try_from(withdrawn: Withdrawn) -> Result { - Ok(match withdrawn { - Withdrawn::No => Withdrawn::No, - Withdrawn::WithReason { reason } => { - if reason.is_empty() { - Withdrawn::WithReason { reason: () } - } else { - anyhow::bail!("withdrawn reason is not empty") - } - } - }) - } -} - -impl TypeUrl for Outcome { - const TYPE_URL: &'static str = "/penumbra.core.governance.v1alpha1.ProposalOutcome"; -} - -impl DomainType for Outcome { - type Proto = pb::ProposalOutcome; -} - -impl From> for pb::ProposalOutcome { - fn from(o: Outcome) -> Self { - let outcome = match o { - Outcome::Passed => { - pb::proposal_outcome::Outcome::Passed(pb::proposal_outcome::Passed {}) - } - Outcome::Failed { withdrawn } => { - pb::proposal_outcome::Outcome::Failed(pb::proposal_outcome::Failed { - withdrawn: match withdrawn { - Withdrawn::No => None, - Withdrawn::WithReason { reason } => { - Some(pb::proposal_outcome::Withdrawn { reason }) - } - }, - }) - } - Outcome::Slashed { withdrawn } => { - pb::proposal_outcome::Outcome::Slashed(pb::proposal_outcome::Slashed { - withdrawn: match withdrawn { - Withdrawn::No => None, - Withdrawn::WithReason { reason } => { - Some(pb::proposal_outcome::Withdrawn { reason }) - } - }, - }) - } - }; - pb::ProposalOutcome { - outcome: Some(outcome), - } - } -} - -impl TryFrom for Outcome { - type Error = anyhow::Error; - - fn try_from(msg: pb::ProposalOutcome) -> Result { - Ok( - match msg - .outcome - .ok_or_else(|| anyhow::anyhow!("missing proposal outcome"))? - { - pb::proposal_outcome::Outcome::Passed(pb::proposal_outcome::Passed {}) => { - Outcome::Passed - } - pb::proposal_outcome::Outcome::Failed(pb::proposal_outcome::Failed { - withdrawn, - }) => Outcome::Failed { - withdrawn: if let Some(pb::proposal_outcome::Withdrawn { reason }) = withdrawn { - Withdrawn::WithReason { reason } - } else { - Withdrawn::No - }, - }, - pb::proposal_outcome::Outcome::Slashed(pb::proposal_outcome::Slashed { - withdrawn, - }) => Outcome::Slashed { - withdrawn: if let Some(pb::proposal_outcome::Withdrawn { reason }) = withdrawn { - Withdrawn::WithReason { reason } - } else { - Withdrawn::No - }, - }, - }, - ) - } -} - -impl TypeUrl for Outcome<()> { - const TYPE_URL: &'static str = "/penumbra.core.governance.v1alpha1.ProposalOutcome"; -} - -impl DomainType for Outcome<()> { - type Proto = pb::ProposalOutcome; -} - -impl From> for pb::ProposalOutcome { - fn from(o: Outcome<()>) -> Self { - let outcome = match o { - Outcome::Passed => { - pb::proposal_outcome::Outcome::Passed(pb::proposal_outcome::Passed {}) - } - Outcome::Failed { withdrawn } => { - pb::proposal_outcome::Outcome::Failed(pb::proposal_outcome::Failed { - withdrawn: >::from(withdrawn).map(|()| { - pb::proposal_outcome::Withdrawn { - reason: "".to_string(), - } - }), - }) - } - Outcome::Slashed { withdrawn } => { - pb::proposal_outcome::Outcome::Slashed(pb::proposal_outcome::Slashed { - withdrawn: >::from(withdrawn).map(|()| { - pb::proposal_outcome::Withdrawn { - reason: "".to_string(), - } - }), - }) - } - }; - pb::ProposalOutcome { - outcome: Some(outcome), - } - } -} - -impl TryFrom for Outcome<()> { - type Error = anyhow::Error; - - fn try_from(msg: pb::ProposalOutcome) -> Result { - Ok( - match msg - .outcome - .ok_or_else(|| anyhow::anyhow!("missing proposal outcome"))? - { - pb::proposal_outcome::Outcome::Passed(pb::proposal_outcome::Passed {}) => { - Outcome::Passed - } - pb::proposal_outcome::Outcome::Failed(pb::proposal_outcome::Failed { - withdrawn, - }) => Outcome::Failed { - withdrawn: >::from(withdrawn.map(|w| w.reason)).try_into()?, - }, - pb::proposal_outcome::Outcome::Slashed(pb::proposal_outcome::Slashed { - withdrawn, - }) => Outcome::Slashed { - withdrawn: >::from(withdrawn.map(|w| w.reason)).try_into()?, - }, - }, - ) - } -} diff --git a/crates/core/component/governance/src/proposal_state.rs b/crates/core/component/governance/src/proposal_state.rs new file mode 100644 index 0000000000..1ec6e498f4 --- /dev/null +++ b/crates/core/component/governance/src/proposal_state.rs @@ -0,0 +1,386 @@ +use serde::{Deserialize, Serialize}; + +use penumbra_proto::{core::governance::v1alpha1 as pb, DomainType, TypeUrl}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "pb::ProposalState", into = "pb::ProposalState")] +pub enum State { + Voting, + Withdrawn { reason: String }, + Finished { outcome: Outcome }, + Claimed { outcome: Outcome }, +} + +impl State { + pub fn is_voting(&self) -> bool { + matches!(self, State::Voting) + } + + pub fn is_withdrawn(&self) -> bool { + matches!(self, State::Withdrawn { .. }) + } + + pub fn is_finished(&self) -> bool { + matches!(self, State::Finished { .. }) + } + + pub fn is_claimed(&self) -> bool { + matches!(self, State::Claimed { .. }) + } + + pub fn is_passed(&self) -> bool { + match self { + State::Finished { outcome } => outcome.is_passed(), + State::Claimed { outcome } => outcome.is_passed(), + _ => false, + } + } + + pub fn is_failed(&self) -> bool { + match self { + State::Finished { outcome } => outcome.is_failed(), + State::Claimed { outcome } => outcome.is_failed(), + _ => false, + } + } + + pub fn is_slashed(&self) -> bool { + match self { + State::Finished { outcome } => outcome.is_slashed(), + State::Claimed { outcome } => outcome.is_slashed(), + _ => false, + } + } +} + +impl State { + pub fn withdrawn(self) -> Withdrawn { + match self { + State::Voting => Withdrawn::No, + State::Withdrawn { reason } => Withdrawn::WithReason { reason }, + State::Finished { outcome } => match outcome { + Outcome::Passed => Withdrawn::No, + Outcome::Failed { withdrawn } | Outcome::Slashed { withdrawn } => withdrawn, + }, + State::Claimed { outcome } => match outcome { + Outcome::Passed => Withdrawn::No, + Outcome::Failed { withdrawn } | Outcome::Slashed { withdrawn } => withdrawn, + }, + } + } +} + +impl TypeUrl for State { + const TYPE_URL: &'static str = "/penumbra.core.governance.v1alpha1.ProposalState"; +} + +impl DomainType for State { + type Proto = pb::ProposalState; +} + +impl From for pb::ProposalState { + fn from(s: State) -> Self { + let state = match s { + State::Voting => pb::proposal_state::State::Voting(pb::proposal_state::Voting {}), + State::Withdrawn { reason } => { + pb::proposal_state::State::Withdrawn(pb::proposal_state::Withdrawn { reason }) + } + State::Finished { outcome } => { + pb::proposal_state::State::Finished(pb::proposal_state::Finished { + outcome: Some(outcome.into()), + }) + } + State::Claimed { outcome } => { + pb::proposal_state::State::Finished(pb::proposal_state::Finished { + outcome: Some(outcome.into()), + }) + } + }; + pb::ProposalState { state: Some(state) } + } +} + +impl TryFrom for State { + type Error = anyhow::Error; + + fn try_from(msg: pb::ProposalState) -> Result { + Ok( + match msg + .state + .ok_or_else(|| anyhow::anyhow!("missing proposal state"))? + { + pb::proposal_state::State::Voting(pb::proposal_state::Voting {}) => State::Voting, + pb::proposal_state::State::Withdrawn(pb::proposal_state::Withdrawn { reason }) => { + State::Withdrawn { reason } + } + pb::proposal_state::State::Finished(pb::proposal_state::Finished { outcome }) => { + State::Finished { + outcome: outcome + .ok_or_else(|| anyhow::anyhow!("missing proposal outcome"))? + .try_into()?, + } + } + pb::proposal_state::State::Claimed(pb::proposal_state::Claimed { outcome }) => { + State::Claimed { + outcome: outcome + .ok_or_else(|| anyhow::anyhow!("missing proposal outcome"))? + .try_into()?, + } + } + }, + ) + } +} + +// This is parameterized by `W`, the withdrawal reason, so that we can use `()` where a reason +// doesn't need to be specified. When this is the case, the serialized format in protobufs uses an +// empty string. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde( + try_from = "pb::ProposalOutcome", + into = "pb::ProposalOutcome", + bound = "W: Clone, pb::ProposalOutcome: From>, Outcome: TryFrom" +)] +pub enum Outcome { + Passed, + Failed { withdrawn: Withdrawn }, + Slashed { withdrawn: Withdrawn }, +} + +impl Outcome { + /// Determines if the outcome should be refunded (i.e. it was not slashed). + pub fn should_be_refunded(&self) -> bool { + !self.is_slashed() + } + + pub fn is_slashed(&self) -> bool { + matches!(self, Outcome::Slashed { .. }) + } + + pub fn is_failed(&self) -> bool { + matches!(self, Outcome::Failed { .. } | Outcome::Slashed { .. }) + } + + pub fn is_passed(&self) -> bool { + matches!(self, Outcome::Passed) + } + + pub fn as_ref(&self) -> Outcome<&W> { + match self { + Outcome::Passed => Outcome::Passed, + Outcome::Failed { withdrawn } => Outcome::Failed { + withdrawn: withdrawn.as_ref(), + }, + Outcome::Slashed { withdrawn } => Outcome::Slashed { + withdrawn: withdrawn.as_ref(), + }, + } + } + + pub fn map(self, f: impl FnOnce(W) -> X) -> Outcome { + match self { + Outcome::Passed => Outcome::Passed, + Outcome::Failed { withdrawn } => Outcome::Failed { + withdrawn: Option::from(withdrawn).map(f).into(), + }, + Outcome::Slashed { withdrawn } => Outcome::Slashed { + withdrawn: Option::from(withdrawn).map(f).into(), + }, + } + } +} + +// This is parameterized by `W`, the withdrawal reason, so that we can use `()` where a reason +// doesn't need to be specified. When this is the case, the serialized format in protobufs uses an +// empty string. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Withdrawn { + No, + WithReason { reason: W }, +} + +impl Withdrawn { + pub fn as_ref(&self) -> Withdrawn<&W> { + match self { + Withdrawn::No => Withdrawn::No, + Withdrawn::WithReason { reason } => Withdrawn::WithReason { reason }, + } + } +} + +impl From> for Withdrawn { + fn from(reason: Option) -> Self { + match reason { + Some(reason) => Withdrawn::WithReason { reason }, + None => Withdrawn::No, + } + } +} + +impl From> for Option { + fn from(withdrawn: Withdrawn) -> Self { + match withdrawn { + Withdrawn::No => None, + Withdrawn::WithReason { reason } => Some(reason), + } + } +} + +impl TryFrom> for Withdrawn<()> { + type Error = anyhow::Error; + + fn try_from(withdrawn: Withdrawn) -> Result { + Ok(match withdrawn { + Withdrawn::No => Withdrawn::No, + Withdrawn::WithReason { reason } => { + if reason.is_empty() { + Withdrawn::WithReason { reason: () } + } else { + anyhow::bail!("withdrawn reason is not empty") + } + } + }) + } +} + +impl TypeUrl for Outcome { + const TYPE_URL: &'static str = "/penumbra.core.governance.v1alpha1.ProposalOutcome"; +} + +impl DomainType for Outcome { + type Proto = pb::ProposalOutcome; +} + +impl From> for pb::ProposalOutcome { + fn from(o: Outcome) -> Self { + let outcome = match o { + Outcome::Passed => { + pb::proposal_outcome::Outcome::Passed(pb::proposal_outcome::Passed {}) + } + Outcome::Failed { withdrawn } => { + pb::proposal_outcome::Outcome::Failed(pb::proposal_outcome::Failed { + withdrawn: match withdrawn { + Withdrawn::No => None, + Withdrawn::WithReason { reason } => { + Some(pb::proposal_outcome::Withdrawn { reason }) + } + }, + }) + } + Outcome::Slashed { withdrawn } => { + pb::proposal_outcome::Outcome::Slashed(pb::proposal_outcome::Slashed { + withdrawn: match withdrawn { + Withdrawn::No => None, + Withdrawn::WithReason { reason } => { + Some(pb::proposal_outcome::Withdrawn { reason }) + } + }, + }) + } + }; + pb::ProposalOutcome { + outcome: Some(outcome), + } + } +} + +impl TryFrom for Outcome { + type Error = anyhow::Error; + + fn try_from(msg: pb::ProposalOutcome) -> Result { + Ok( + match msg + .outcome + .ok_or_else(|| anyhow::anyhow!("missing proposal outcome"))? + { + pb::proposal_outcome::Outcome::Passed(pb::proposal_outcome::Passed {}) => { + Outcome::Passed + } + pb::proposal_outcome::Outcome::Failed(pb::proposal_outcome::Failed { + withdrawn, + }) => Outcome::Failed { + withdrawn: if let Some(pb::proposal_outcome::Withdrawn { reason }) = withdrawn { + Withdrawn::WithReason { reason } + } else { + Withdrawn::No + }, + }, + pb::proposal_outcome::Outcome::Slashed(pb::proposal_outcome::Slashed { + withdrawn, + }) => Outcome::Slashed { + withdrawn: if let Some(pb::proposal_outcome::Withdrawn { reason }) = withdrawn { + Withdrawn::WithReason { reason } + } else { + Withdrawn::No + }, + }, + }, + ) + } +} + +impl TypeUrl for Outcome<()> { + const TYPE_URL: &'static str = "/penumbra.core.governance.v1alpha1.ProposalOutcome"; +} + +impl DomainType for Outcome<()> { + type Proto = pb::ProposalOutcome; +} + +impl From> for pb::ProposalOutcome { + fn from(o: Outcome<()>) -> Self { + let outcome = match o { + Outcome::Passed => { + pb::proposal_outcome::Outcome::Passed(pb::proposal_outcome::Passed {}) + } + Outcome::Failed { withdrawn } => { + pb::proposal_outcome::Outcome::Failed(pb::proposal_outcome::Failed { + withdrawn: >::from(withdrawn).map(|()| { + pb::proposal_outcome::Withdrawn { + reason: "".to_string(), + } + }), + }) + } + Outcome::Slashed { withdrawn } => { + pb::proposal_outcome::Outcome::Slashed(pb::proposal_outcome::Slashed { + withdrawn: >::from(withdrawn).map(|()| { + pb::proposal_outcome::Withdrawn { + reason: "".to_string(), + } + }), + }) + } + }; + pb::ProposalOutcome { + outcome: Some(outcome), + } + } +} + +impl TryFrom for Outcome<()> { + type Error = anyhow::Error; + + fn try_from(msg: pb::ProposalOutcome) -> Result { + Ok( + match msg + .outcome + .ok_or_else(|| anyhow::anyhow!("missing proposal outcome"))? + { + pb::proposal_outcome::Outcome::Passed(pb::proposal_outcome::Passed {}) => { + Outcome::Passed + } + pb::proposal_outcome::Outcome::Failed(pb::proposal_outcome::Failed { + withdrawn, + }) => Outcome::Failed { + withdrawn: >::from(withdrawn.map(|w| w.reason)).try_into()?, + }, + pb::proposal_outcome::Outcome::Slashed(pb::proposal_outcome::Slashed { + withdrawn, + }) => Outcome::Slashed { + withdrawn: >::from(withdrawn.map(|w| w.reason)).try_into()?, + }, + }, + ) + } +} diff --git a/crates/core/app/src/governance/state_key.rs b/crates/core/component/governance/src/state_key.rs similarity index 100% rename from crates/core/app/src/governance/state_key.rs rename to crates/core/component/governance/src/state_key.rs diff --git a/crates/core/app/src/governance/tally.rs b/crates/core/component/governance/src/tally.rs similarity index 97% rename from crates/core/app/src/governance/tally.rs rename to crates/core/component/governance/src/tally.rs index beee71eaeb..dc3ed5efa7 100644 --- a/crates/core/app/src/governance/tally.rs +++ b/crates/core/component/governance/src/tally.rs @@ -4,9 +4,10 @@ use serde::{Deserialize, Serialize}; use penumbra_chain::params::{ChainParameters, Ratio}; use penumbra_proto::{core::governance::v1alpha1 as pb, DomainType, TypeUrl}; -use penumbra_transaction::{ - action::Vote, - proposal::{self, Withdrawn}, + +use crate::{ + proposal_state::{Outcome as StateOutcome, Withdrawn}, + vote::Vote, }; #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)] @@ -122,7 +123,7 @@ impl Outcome { } } -impl From for proposal::Outcome { +impl From for StateOutcome { fn from(outcome: Outcome) -> Self { match outcome { Outcome::Pass => Self::Passed, diff --git a/crates/core/app/src/governance/view.rs b/crates/core/component/governance/src/view.rs similarity index 99% rename from crates/core/app/src/governance/view.rs rename to crates/core/component/governance/src/view.rs index cbd2ea904d..b5e019a5c2 100644 --- a/crates/core/app/src/governance/view.rs +++ b/crates/core/component/governance/src/view.rs @@ -28,7 +28,8 @@ use tracing::instrument; use penumbra_stake::{rate::RateData, validator, StateReadExt as _}; -use super::{state_key, tally::Tally}; +use crate::proposal_state::State::{Claimed, Finished, Withdrawn}; +use crate::{state_key, tally::Tally}; #[async_trait] pub trait StateReadExt: StateRead + penumbra_stake::StateReadExt { @@ -161,7 +162,7 @@ pub trait StateReadExt: StateRead + penumbra_stake::StateReadExt { /// Throw an error if the proposal is not votable. async fn check_proposal_votable(&self, proposal_id: u64) -> Result<()> { if let Some(proposal_state) = self.proposal_state(proposal_id).await? { - use proposal::State::*; + use crate::proposal_state::State::*; match proposal_state { Voting => { // This is when you can vote on a proposal diff --git a/crates/core/transaction/src/vote.rs b/crates/core/component/governance/src/vote.rs similarity index 100% rename from crates/core/transaction/src/vote.rs rename to crates/core/component/governance/src/vote.rs diff --git a/crates/core/transaction/src/lib.rs b/crates/core/transaction/src/lib.rs index d2417d562f..4c73db3628 100644 --- a/crates/core/transaction/src/lib.rs +++ b/crates/core/transaction/src/lib.rs @@ -27,9 +27,7 @@ mod witness_data; pub mod action; pub mod memo; pub mod plan; -pub mod proposal; pub mod view; -pub mod vote; pub use action::Action; pub use auth_data::AuthorizationData;