From 456877baba642b51d8f4aff9b195c6b465ab61b2 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Mon, 16 Jan 2023 15:00:20 +0100 Subject: [PATCH 01/49] Adds `ProposalType` and `VoteType` --- apps/src/lib/cli.rs | 19 ++++++-- apps/src/lib/client/tx.rs | 41 +++++++++++++---- core/src/ledger/storage_api/governance.rs | 4 +- core/src/types/governance.rs | 54 ++++++++++++++--------- core/src/types/transaction/governance.rs | 53 ++++++++++++++++------ 5 files changed, 124 insertions(+), 47 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 9c23cadb46..bd928f1b2f 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1569,7 +1569,6 @@ pub mod args { use namada::ibc::core::ics24_host::identifier::{ChannelId, PortId}; use namada::types::address::Address; use namada::types::chain::{ChainId, ChainIdPrefix}; - use namada::types::governance::ProposalVote; use namada::types::key::*; use namada::types::masp::MaspValue; use namada::types::storage::{self, Epoch}; @@ -1663,7 +1662,8 @@ pub mod args { const PUBLIC_KEY: Arg = arg("public-key"); const PROPOSAL_ID: Arg = arg("proposal-id"); const PROPOSAL_ID_OPT: ArgOpt = arg_opt("proposal-id"); - const PROPOSAL_VOTE: Arg = arg("vote"); + const PROPOSAL_VOTE: Arg = arg("vote"); + const PROPOSAL_VOTE_MEMO_OPT: ArgOpt = arg_opt("memo"); const RAW_ADDRESS: Arg
= arg("address"); const RAW_ADDRESS_OPT: ArgOpt
= RAW_ADDRESS.opt(); const RAW_PUBLIC_KEY_OPT: ArgOpt = arg_opt("public-key"); @@ -2292,7 +2292,9 @@ pub mod args { /// Proposal id pub proposal_id: Option, /// The vote - pub vote: ProposalVote, + pub vote: String, + /// The optional vote memo path + pub memo: Option, /// Flag if proposal vote should be run offline pub offline: bool, /// The proposal file path @@ -2304,6 +2306,7 @@ pub mod args { let tx = Tx::parse(matches); let proposal_id = PROPOSAL_ID_OPT.parse(matches); let vote = PROPOSAL_VOTE.parse(matches); + let memo = PROPOSAL_VOTE_MEMO_OPT.parse(matches); let offline = PROPOSAL_OFFLINE.parse(matches); let proposal_data = DATA_PATH_OPT.parse(matches); @@ -2311,6 +2314,7 @@ pub mod args { tx, proposal_id, vote, + memo, offline, proposal_data, } @@ -2332,6 +2336,15 @@ pub mod args { .def() .about("The vote for the proposal. Either yay or nay."), ) + .arg( + PROPOSAL_VOTE_MEMO_OPT + .def() + .about("The optional vote memo.") + .conflicts_with_all(&[ + PROPOSAL_OFFLINE.name, + DATA_PATH_OPT.name, + ]), + ) .arg( PROPOSAL_OFFLINE .def() diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 0014833ca8..678ed91e4c 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -42,7 +42,7 @@ use namada::ledger::pos::{CommissionPair, PosParams}; use namada::proto::Tx; use namada::types::address::{masp, masp_tx_key, Address}; use namada::types::governance::{ - OfflineProposal, OfflineVote, Proposal, ProposalVote, + OfflineProposal, OfflineVote, Proposal, ProposalVote, VoteType, }; use namada::types::key::*; use namada::types::masp::{PaymentAddress, TransferTarget}; @@ -1989,12 +1989,18 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { args.tx.ledger_address.clone(), ) .await; - let offline_vote = OfflineVote::new( - &proposal, - args.vote, - signer.clone(), - &signing_key, - ); + + let vote = match args.vote.as_str() { + "yay" => ProposalVote::Yay(VoteType::Default), + "nay" => ProposalVote::Nay, + _ => { + eprintln!("Vote must be either yay or nay"); + safe_exit(1); + } + }; + + let offline_vote = + OfflineVote::new(&proposal, vote, signer.clone(), &signing_key); let proposal_vote_filename = proposal_file_path .parent() @@ -2047,6 +2053,23 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { rpc::get_delegators_delegation(&client, &voter_address) .await; + let vote = match args.vote.as_str() { + "yay" => match args.memo { + Some(path) => { + let memo_file = std::fs::File::open(path) + .expect("Error while opening vote memo file"); + let vote = serde_json::from_reader(memo_file) + .expect("Could not deserialize vote memo"); + ProposalVote::Yay(vote) + } + None => ProposalVote::Yay(VoteType::Default), + }, + "nay" => ProposalVote::Nay, + _ => { + eprintln!("Vote must be either yay or nay"); + safe_exit(1); + } + }; // Optimize by quering if a vote from a validator // is equal to ours. If so, we can avoid voting, but ONLY if we // are voting in the last third of the voting @@ -2066,14 +2089,14 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { &client, delegations, proposal_id, - &args.vote, + &vote, ) .await; } let tx_data = VoteProposalData { id: proposal_id, - vote: args.vote, + vote: vote, voter: voter_address, delegations: delegations.into_iter().collect(), }; diff --git a/core/src/ledger/storage_api/governance.rs b/core/src/ledger/storage_api/governance.rs index c6197ebbb3..76e15ad512 100644 --- a/core/src/ledger/storage_api/governance.rs +++ b/core/src/ledger/storage_api/governance.rs @@ -4,7 +4,7 @@ use super::token; use crate::ledger::governance::{storage, ADDRESS as governance_address}; use crate::ledger::storage_api::{self, StorageRead, StorageWrite}; use crate::types::transaction::governance::{ - InitProposalData, VoteProposalData, + InitProposalData, ProposalType, VoteProposalData, }; /// A proposal creation transaction. @@ -38,7 +38,7 @@ where let grace_epoch_key = storage::get_grace_epoch_key(proposal_id); storage.write(&grace_epoch_key, data.grace_epoch)?; - if let Some(proposal_code) = data.proposal_code { + if let ProposalType::Default(Some(proposal_code)) = data.proposal_type { let proposal_code_key = storage::get_proposal_code_key(proposal_id); storage.write_bytes(&proposal_code_key, proposal_code)?; } diff --git a/core/src/types/governance.rs b/core/src/types/governance.rs index 438017a370..dc21dbde14 100644 --- a/core/src/types/governance.rs +++ b/core/src/types/governance.rs @@ -2,7 +2,6 @@ use std::collections::BTreeMap; use std::fmt::{self, Display}; -use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; use rust_decimal::Decimal; @@ -19,6 +18,24 @@ use crate::types::token::SCALE; /// Type alias for vote power pub type VotePower = u128; +/// The type of a governance vote with the optional associated Memo +#[derive( + Debug, + Clone, + PartialEq, + BorshSerialize, + BorshDeserialize, + Serialize, + Deserialize, + Eq, +)] +pub enum VoteType { + /// A default vote without Memo + Default, + /// A vote for the PGF council encoding for the proposed addresses and the budget cap + PGFCouncil(Vec
, u64), +} + #[derive( Debug, Clone, @@ -32,7 +49,7 @@ pub type VotePower = u128; /// The vote for a proposal pub enum ProposalVote { /// Yes - Yay, + Yay(VoteType), /// No Nay, } @@ -41,7 +58,7 @@ impl ProposalVote { /// Check if a vote is yay pub fn is_yay(&self) -> bool { match self { - ProposalVote::Yay => true, + ProposalVote::Yay(_) => true, ProposalVote::Nay => false, } } @@ -50,7 +67,7 @@ impl ProposalVote { impl Display for ProposalVote { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - ProposalVote::Yay => write!(f, "yay"), + ProposalVote::Yay(_) => write!(f, "yay"), ProposalVote::Nay => write!(f, "nay"), } } @@ -63,20 +80,6 @@ pub enum ProposalVoteParseError { InvalidVote, } -impl FromStr for ProposalVote { - type Err = ProposalVoteParseError; - - fn from_str(s: &str) -> Result { - if s.eq("yay") { - Ok(ProposalVote::Yay) - } else if s.eq("nay") { - Ok(ProposalVote::Nay) - } else { - Err(ProposalVoteParseError::InvalidVote) - } - } -} - /// The result of a proposal pub enum TallyResult { /// Proposal was accepted @@ -128,6 +131,17 @@ impl Display for TallyResult { } } +/// The type of a governance proposal +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +pub enum ProposalType { + /// A default proposal with the optional path to wasm code + Default(Option), + /// A PGF council proposal + PGFCouncil, +} + #[derive( Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, )] @@ -145,8 +159,8 @@ pub struct Proposal { pub voting_end_epoch: Epoch, /// The epoch from which this changes are executed pub grace_epoch: Epoch, - /// The code containing the storage changes - pub proposal_code_path: Option, + /// The proposal type + pub proposal_type: ProposalType, } impl Display for Proposal { diff --git a/core/src/types/transaction/governance.rs b/core/src/types/transaction/governance.rs index ba2bd5f933..7a517dbf71 100644 --- a/core/src/types/transaction/governance.rs +++ b/core/src/types/transaction/governance.rs @@ -2,9 +2,45 @@ use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; use crate::types::address::Address; -use crate::types::governance::{Proposal, ProposalError, ProposalVote}; +use crate::types::governance::{self, Proposal, ProposalError, ProposalVote}; use crate::types::storage::Epoch; +/// The type of a [`InitProposal`] +#[derive( + Debug, + Clone, + PartialEq, + BorshSerialize, + BorshDeserialize, + Serialize, + Deserialize, +)] +pub enum ProposalType { + /// Default governance proposal with the optional wasm code + Default(Option>), + /// PGF council proposal + PGFCouncil, +} + +impl TryFrom for ProposalType { + type Error = ProposalError; + fn try_from(value: governance::ProposalType) -> Result { + match value { + governance::ProposalType::Default(path) => { + if let Some(p) = path { + match std::fs::read(p) { + Ok(code) => Ok(Self::Default(Some(code))), + Err(_) => Err(Self::Error::InvalidProposalData), + } + } else { + Ok(Self::Default(None)) + } + } + governance::ProposalType::PGFCouncil => Ok(Self::PGFCouncil), + } + } +} + /// A tx data type to hold proposal data #[derive( Debug, @@ -28,8 +64,8 @@ pub struct InitProposalData { pub voting_end_epoch: Epoch, /// The epoch from which this changes are executed pub grace_epoch: Epoch, - /// The code containing the storage changes - pub proposal_code: Option>, + /// The proposal type + pub proposal_type: ProposalType, } /// A tx data type to hold vote proposal data @@ -57,15 +93,6 @@ impl TryFrom for InitProposalData { type Error = ProposalError; fn try_from(proposal: Proposal) -> Result { - let proposal_code = if let Some(path) = proposal.proposal_code_path { - match std::fs::read(path) { - Ok(bytes) => Some(bytes), - Err(_) => return Err(Self::Error::InvalidProposalData), - } - } else { - None - }; - Ok(InitProposalData { id: proposal.id, content: proposal.content.try_to_vec().unwrap(), @@ -73,7 +100,7 @@ impl TryFrom for InitProposalData { voting_start_epoch: proposal.voting_start_epoch, voting_end_epoch: proposal.voting_end_epoch, grace_epoch: proposal.grace_epoch, - proposal_code, + proposal_type: proposal.proposal_type.try_into()?, }) } } From ce294e02e3747b9ef10ece9971bc68724904f251 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Mon, 16 Jan 2023 15:16:25 +0100 Subject: [PATCH 02/49] Renames `proposal_type` --- core/src/ledger/storage_api/governance.rs | 2 +- core/src/types/governance.rs | 4 ++-- core/src/types/transaction/governance.rs | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/ledger/storage_api/governance.rs b/core/src/ledger/storage_api/governance.rs index 76e15ad512..afbd33f89d 100644 --- a/core/src/ledger/storage_api/governance.rs +++ b/core/src/ledger/storage_api/governance.rs @@ -38,7 +38,7 @@ where let grace_epoch_key = storage::get_grace_epoch_key(proposal_id); storage.write(&grace_epoch_key, data.grace_epoch)?; - if let ProposalType::Default(Some(proposal_code)) = data.proposal_type { + if let ProposalType::Default(Some(proposal_code)) = data.r#type { let proposal_code_key = storage::get_proposal_code_key(proposal_id); storage.write_bytes(&proposal_code_key, proposal_code)?; } diff --git a/core/src/types/governance.rs b/core/src/types/governance.rs index dc21dbde14..a03cef9140 100644 --- a/core/src/types/governance.rs +++ b/core/src/types/governance.rs @@ -153,14 +153,14 @@ pub struct Proposal { pub content: BTreeMap, /// The proposal author address pub author: Address, + /// The proposal type + pub r#type: ProposalType, /// The epoch from which voting is allowed pub voting_start_epoch: Epoch, /// The epoch from which voting is stopped pub voting_end_epoch: Epoch, /// The epoch from which this changes are executed pub grace_epoch: Epoch, - /// The proposal type - pub proposal_type: ProposalType, } impl Display for Proposal { diff --git a/core/src/types/transaction/governance.rs b/core/src/types/transaction/governance.rs index 7a517dbf71..594e81fabb 100644 --- a/core/src/types/transaction/governance.rs +++ b/core/src/types/transaction/governance.rs @@ -58,14 +58,14 @@ pub struct InitProposalData { pub content: Vec, /// The proposal author address pub author: Address, + /// The proposal type + pub r#type: ProposalType, /// The epoch from which voting is allowed pub voting_start_epoch: Epoch, /// The epoch from which voting is stopped pub voting_end_epoch: Epoch, /// The epoch from which this changes are executed pub grace_epoch: Epoch, - /// The proposal type - pub proposal_type: ProposalType, } /// A tx data type to hold vote proposal data @@ -97,10 +97,10 @@ impl TryFrom for InitProposalData { id: proposal.id, content: proposal.content.try_to_vec().unwrap(), author: proposal.author, + r#type: proposal.r#type.try_into()?, voting_start_epoch: proposal.voting_start_epoch, voting_end_epoch: proposal.voting_end_epoch, grace_epoch: proposal.grace_epoch, - proposal_type: proposal.proposal_type.try_into()?, }) } } From e18d3425b629604a153b42db18d9af90bff85d8f Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Tue, 17 Jan 2023 14:41:57 +0100 Subject: [PATCH 03/49] Updates init proposal tx and governance VP --- core/src/ledger/governance/storage.rs | 140 ++++++++---------- core/src/ledger/storage_api/governance.rs | 11 ++ core/src/types/transaction/governance.rs | 25 +++- .../user-guide/ledger/on-chain-governance.md | 5 +- shared/src/ledger/native_vp/governance/mod.rs | 53 ++++++- 5 files changed, 152 insertions(+), 82 deletions(-) diff --git a/core/src/ledger/governance/storage.rs b/core/src/ledger/governance/storage.rs index fb4ecaf76b..54f9ca6d88 100644 --- a/core/src/ledger/governance/storage.rs +++ b/core/src/ledger/governance/storage.rs @@ -5,6 +5,7 @@ use crate::types::storage::{DbKeySeg, Key, KeySeg}; const PROPOSAL_PREFIX: &str = "proposal"; const PROPOSAL_VOTE: &str = "vote"; const PROPOSAL_AUTHOR: &str = "author"; +const PROPOSAL_TYPE: &str = "type"; const PROPOSAL_CONTENT: &str = "content"; const PROPOSAL_START_EPOCH: &str = "start_epoch"; const PROPOSAL_END_EPOCH: &str = "end_epoch"; @@ -30,16 +31,10 @@ pub fn is_governance_key(key: &Key) -> bool { /// Check if a key is a vote key pub fn is_vote_key(key: &Key) -> bool { match &key.segments[..] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::StringSeg(id), - DbKeySeg::StringSeg(vote), - DbKeySeg::AddressSeg(_validator_address), - DbKeySeg::AddressSeg(_address), - ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && vote == PROPOSAL_VOTE => + [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix), DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(vote), DbKeySeg::AddressSeg(_validator_address), DbKeySeg::AddressSeg(_address)] + if addr == &ADDRESS + && prefix == PROPOSAL_PREFIX + && vote == PROPOSAL_VOTE => { id.parse::().is_ok() } @@ -50,14 +45,10 @@ pub fn is_vote_key(key: &Key) -> bool { /// Check if key is author key pub fn is_author_key(key: &Key) -> bool { match &key.segments[..] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::StringSeg(id), - DbKeySeg::StringSeg(author), - ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && author == PROPOSAL_AUTHOR => + [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix), DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(author)] + if addr == &ADDRESS + && prefix == PROPOSAL_PREFIX + && author == PROPOSAL_AUTHOR => { id.parse::().is_ok() } @@ -65,17 +56,13 @@ pub fn is_author_key(key: &Key) -> bool { } } -/// Check if key is proposal key +/// Check if key is proposal code key pub fn is_proposal_code_key(key: &Key) -> bool { match &key.segments[..] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::StringSeg(id), - DbKeySeg::StringSeg(proposal_code), - ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && proposal_code == PROPOSAL_CODE => + [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix), DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(proposal_code)] + if addr == &ADDRESS + && prefix == PROPOSAL_PREFIX + && proposal_code == PROPOSAL_CODE => { id.parse::().is_ok() } @@ -86,14 +73,10 @@ pub fn is_proposal_code_key(key: &Key) -> bool { /// Check if key is grace epoch key pub fn is_grace_epoch_key(key: &Key) -> bool { match &key.segments[..] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::StringSeg(id), - DbKeySeg::StringSeg(grace_epoch), - ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && grace_epoch == PROPOSAL_GRACE_EPOCH => + [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix), DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(grace_epoch)] + if addr == &ADDRESS + && prefix == PROPOSAL_PREFIX + && grace_epoch == PROPOSAL_GRACE_EPOCH => { id.parse::().is_ok() } @@ -104,14 +87,10 @@ pub fn is_grace_epoch_key(key: &Key) -> bool { /// Check if key is content key pub fn is_content_key(key: &Key) -> bool { match &key.segments[..] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::StringSeg(id), - DbKeySeg::StringSeg(content), - ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && content == PROPOSAL_CONTENT => + [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix), DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(content)] + if addr == &ADDRESS + && prefix == PROPOSAL_PREFIX + && content == PROPOSAL_CONTENT => { id.parse::().is_ok() } @@ -122,14 +101,10 @@ pub fn is_content_key(key: &Key) -> bool { /// Check if key is balance key pub fn is_balance_key(key: &Key) -> bool { match &key.segments[..] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::StringSeg(id), - DbKeySeg::StringSeg(funds), - ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && funds == PROPOSAL_FUNDS => + [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix), DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(funds)] + if addr == &ADDRESS + && prefix == PROPOSAL_PREFIX + && funds == PROPOSAL_FUNDS => { id.parse::().is_ok() } @@ -140,14 +115,10 @@ pub fn is_balance_key(key: &Key) -> bool { /// Check if key is start epoch key pub fn is_start_epoch_key(key: &Key) -> bool { match &key.segments[..] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::StringSeg(id), - DbKeySeg::StringSeg(start_epoch), - ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && start_epoch == PROPOSAL_START_EPOCH => + [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix), DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(start_epoch)] + if addr == &ADDRESS + && prefix == PROPOSAL_PREFIX + && start_epoch == PROPOSAL_START_EPOCH => { id.parse::().is_ok() } @@ -158,14 +129,24 @@ pub fn is_start_epoch_key(key: &Key) -> bool { /// Check if key is epoch key pub fn is_end_epoch_key(key: &Key) -> bool { match &key.segments[..] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::StringSeg(id), - DbKeySeg::StringSeg(end_epoch), - ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && end_epoch == PROPOSAL_END_EPOCH => + [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix), DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(end_epoch)] + if addr == &ADDRESS + && prefix == PROPOSAL_PREFIX + && end_epoch == PROPOSAL_END_EPOCH => + { + id.parse::().is_ok() + } + _ => false, + } +} + +/// Check if key is proposal type key +pub fn is_proposal_type_key(key: &Key) -> bool { + match &key.segments[..] { + [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix), DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(proposal_type)] + if addr == &ADDRESS + && prefix == PROPOSAL_PREFIX + && proposal_type == PROPOSAL_TYPE => { id.parse::().is_ok() } @@ -334,6 +315,15 @@ pub fn get_author_key(id: u64) -> Key { .expect("Cannot obtain a storage key") } +/// Get key of a proposal type +pub fn get_proposal_type_key(id: u64) -> Key { + proposal_prefix() + .push(&id.to_string()) + .expect("Cannot obtain a storage key") + .push(&PROPOSAL_TYPE.to_owned()) + .expect("Cannot obtain a storage key") +} + /// Get key of proposal voting start epoch pub fn get_voting_start_epoch_key(id: u64) -> Key { proposal_prefix() @@ -370,21 +360,21 @@ pub fn get_grace_epoch_key(id: u64) -> Key { .expect("Cannot obtain a storage key") } -/// Get proposal code key -pub fn get_proposal_code_key(id: u64) -> Key { +/// Get the proposal committing key prefix +pub fn get_commiting_proposals_prefix(epoch: u64) -> Key { proposal_prefix() - .push(&id.to_string()) + .push(&PROPOSAL_COMMITTING_EPOCH.to_owned()) .expect("Cannot obtain a storage key") - .push(&PROPOSAL_CODE.to_owned()) + .push(&epoch.to_string()) .expect("Cannot obtain a storage key") } -/// Get the proposal committing key prefix -pub fn get_commiting_proposals_prefix(epoch: u64) -> Key { +/// Get proposal code key +pub fn get_proposal_code_key(id: u64) -> Key { proposal_prefix() - .push(&PROPOSAL_COMMITTING_EPOCH.to_owned()) + .push(&id.to_string()) .expect("Cannot obtain a storage key") - .push(&epoch.to_string()) + .push(&PROPOSAL_CODE.to_owned()) .expect("Cannot obtain a storage key") } diff --git a/core/src/ledger/storage_api/governance.rs b/core/src/ledger/storage_api/governance.rs index afbd33f89d..0560547002 100644 --- a/core/src/ledger/storage_api/governance.rs +++ b/core/src/ledger/storage_api/governance.rs @@ -28,6 +28,17 @@ where let author_key = storage::get_author_key(proposal_id); storage.write(&author_key, data.author.clone())?; + let proposal_type_key = storage::get_proposal_type_key(proposal_id); + match data.r#type { + ProposalType::Default(Some(code)) => { + // Remove wasm code and write it under a different subkey + storage.write(&proposal_type_key, ProposalType::Default(None))?; + let proposal_code_key = storage::get_proposal_code_key(proposal_id); + storage.write_bytes(&proposal_code_key, code)? + } + _ => storage.write(&proposal_type_key, data.r#type.clone())?, + } + let voting_start_epoch_key = storage::get_voting_start_epoch_key(proposal_id); storage.write(&voting_start_epoch_key, data.voting_start_epoch)?; diff --git a/core/src/types/transaction/governance.rs b/core/src/types/transaction/governance.rs index 594e81fabb..ec893634de 100644 --- a/core/src/types/transaction/governance.rs +++ b/core/src/types/transaction/governance.rs @@ -2,7 +2,9 @@ use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; use crate::types::address::Address; -use crate::types::governance::{self, Proposal, ProposalError, ProposalVote}; +use crate::types::governance::{ + self, Proposal, ProposalError, ProposalVote, VoteType, +}; use crate::types::storage::Epoch; /// The type of a [`InitProposal`] @@ -22,6 +24,27 @@ pub enum ProposalType { PGFCouncil, } +impl PartialEq for ProposalType { + fn eq(&self, other: &VoteType) -> bool { + match self { + Self::Default(_) => { + if let VoteType::Default = other { + true + } else { + false + } + } + Self::PGFCouncil => { + if let VoteType::PGFCouncil(..) = other { + true + } else { + false + } + } + } + } +} + impl TryFrom for ProposalType { type Error = ProposalError; fn try_from(value: governance::ProposalType) -> Result { diff --git a/documentation/docs/src/user-guide/ledger/on-chain-governance.md b/documentation/docs/src/user-guide/ledger/on-chain-governance.md index 9223bf19ea..048879b71d 100644 --- a/documentation/docs/src/user-guide/ledger/on-chain-governance.md +++ b/documentation/docs/src/user-guide/ledger/on-chain-governance.md @@ -27,7 +27,7 @@ Now, we need to create a json file `proposal.json` holding the content of our pr "voting_start_epoch": 3, "voting_end_epoch": 6, "grace_epoch": 12, - "proposal_code_path": "./wasm_for_tests/tx_no_op.wasm" + "type": "Default" } ``` @@ -67,10 +67,11 @@ Only validators and delegators can vote. Assuming you have a validator or a dele namada client vote-proposal \ --proposal-id 0 \ --vote yay \ + --memo path \ --signer validator ``` -where `--vote` can be either `yay` or `nay`. +where `--vote` can be either `yay` or `nay`. The optional `memo` field represents the path to a json file econding the data to attach to the vote. ## Check the result diff --git a/shared/src/ledger/native_vp/governance/mod.rs b/shared/src/ledger/native_vp/governance/mod.rs index 5cde3c5468..bbab531e3c 100644 --- a/shared/src/ledger/native_vp/governance/mod.rs +++ b/shared/src/ledger/native_vp/governance/mod.rs @@ -7,6 +7,8 @@ use std::collections::BTreeSet; use namada_core::ledger::governance::storage as gov_storage; use namada_core::ledger::storage; use namada_core::ledger::vp_env::VpEnv; +use namada_core::types::governance::VoteType; +use namada_core::types::transaction::governance::ProposalType; use thiserror::Error; use utils::is_valid_validator_voting_period; @@ -73,6 +75,9 @@ where (KeyType::CONTENT, Some(proposal_id)) => { self.is_valid_content_key(proposal_id) } + (KeyType::TYPE, Some(proposal_id)) => { + self.is_valid_proposal_type(proposal_id) + } (KeyType::PROPOSAL_CODE, Some(proposal_id)) => { self.is_valid_proposal_code(proposal_id) } @@ -133,6 +138,7 @@ where counter_key.clone(), gov_storage::get_content_key(counter), gov_storage::get_author_key(counter), + gov_storage::get_proposal_type_key(counter), gov_storage::get_funds_key(counter), gov_storage::get_voting_start_epoch_key(counter), gov_storage::get_voting_end_epoch_key(counter), @@ -170,9 +176,15 @@ where let voter = gov_storage::get_voter_address(key); let delegation_address = gov_storage::get_vote_delegation_address(key); + let vote_type: Option = self.ctx.read_post(key)?; + let proposal_type_key = gov_storage::get_proposal_type_key(proposal_id); + let proposal_type: Option = + self.ctx.read_pre(&proposal_type_key)?; match ( pre_counter, + vote_type, + proposal_type, voter, delegation_address, current_epoch, @@ -181,12 +193,16 @@ where ) { ( Some(pre_counter), + Some(vote_type), + Some(proposal_type), Some(voter_address), Some(delegation_address), Some(current_epoch), Some(pre_voting_start_epoch), Some(pre_voting_end_epoch), ) => { + let is_valid_vote_type = proposal_type.eq(&vote_type); + let is_delegator = self .is_delegator( pre_voting_start_epoch, @@ -212,7 +228,8 @@ where pre_voting_end_epoch, ); - let is_valid = pre_counter > proposal_id + let is_valid = is_valid_vote_type + && pre_counter > proposal_id && current_epoch >= pre_voting_start_epoch && current_epoch <= pre_voting_end_epoch && (is_delegator @@ -248,9 +265,33 @@ where } } - /// Validate a proposal_code key + /// Validate the proposal type + pub fn is_valid_proposal_type(&self, proposal_id: u64) -> Result { + let proposal_type_key = gov_storage::get_proposal_type_key(proposal_id); + Ok(self + .ctx + .read_post::(&proposal_type_key)? + .is_some()) + } + + /// Validate a proposal code pub fn is_valid_proposal_code(&self, proposal_id: u64) -> Result { - let code_key: Key = gov_storage::get_proposal_code_key(proposal_id); + let proposal_type_key: Key = + gov_storage::get_proposal_type_key(proposal_id); + let proposal_type: Option = + self.ctx.read_post(&proposal_type_key)?; + + // Check that the proposal type admits wasm code + match proposal_type { + Some(proposal_type) => { + if let ProposalType::PGFCouncil = proposal_type { + return Ok(false); + } + } + None => return Ok(false), + } + + let code_key = gov_storage::get_proposal_code_key(proposal_id); let max_code_size_parameter_key = gov_storage::get_max_proposal_code_size_key(); @@ -608,6 +649,8 @@ enum KeyType { #[allow(non_camel_case_types)] PROPOSAL_CODE, #[allow(non_camel_case_types)] + TYPE, + #[allow(non_camel_case_types)] PROPOSAL_COMMIT, #[allow(non_camel_case_types)] GRACE_EPOCH, @@ -635,8 +678,10 @@ impl KeyType { Self::VOTE } else if gov_storage::is_content_key(key) { KeyType::CONTENT + } else if gov_storage::is_proposal_type_key(key) { + Self::TYPE } else if gov_storage::is_proposal_code_key(key) { - KeyType::PROPOSAL_CODE + Self::PROPOSAL_CODE } else if gov_storage::is_grace_epoch_key(key) { KeyType::GRACE_EPOCH } else if gov_storage::is_start_epoch_key(key) { From 19c4730ddf0b36fbd528e0cfaa86e057fbc8ef46 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Tue, 17 Jan 2023 16:51:05 +0100 Subject: [PATCH 04/49] Updates governance specs with regard to PGF --- documentation/specs/src/base-ledger/governance.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/specs/src/base-ledger/governance.md b/documentation/specs/src/base-ledger/governance.md index bc9dc3c4f0..6aa971fa11 100644 --- a/documentation/specs/src/base-ledger/governance.md +++ b/documentation/specs/src/base-ledger/governance.md @@ -122,8 +122,8 @@ pub enum ProposalType { - Doesn't carry any wasm code - Allows both validators and delegators to vote -- Requires 1/3 of the total voting power to vote for the same council -- Expect every vote to carry a memo in the form of a tuple `Set<(Set
, BudgetCap)>` +- Requires 1/3 of the total voting power to vote +- Expect every vote to carry a memo in the form of a tuple `(Set
, BudgetCap)` `ETHBridge` is aimed at regulating actions on the bridge like the update of the Ethereum smart contracts or the withdrawing of all the funds from the `Vault` : From fedee0482cc94c8b4c03b261dabfba1ef8f037f9 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Wed, 18 Jan 2023 18:58:13 +0100 Subject: [PATCH 05/49] Updates proposal tally for proposal types --- apps/src/lib/client/rpc.rs | 379 +++++++++++++++--- apps/src/lib/node/ledger/shell/governance.rs | 267 +++++++----- core/src/ledger/storage_api/governance.rs | 2 +- core/src/types/governance.rs | 9 +- core/src/types/transaction/governance.rs | 11 + .../src/ledger/native_vp/governance/utils.rs | 233 +++++++++-- 6 files changed, 708 insertions(+), 193 deletions(-) diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 4dd36ea2eb..1fde0906a2 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -23,6 +23,7 @@ use masp_primitives::transaction::components::Amount; use masp_primitives::zip32::ExtendedFullViewingKey; #[cfg(not(feature = "mainnet"))] use namada::core::ledger::testnet_pow; +use namada::core::types::transaction::governance::ProposalType; use namada::ledger::events::Event; use namada::ledger::governance::parameters::GovParams; use namada::ledger::governance::storage as gov_storage; @@ -37,7 +38,7 @@ use namada::proto::{SignedTxData, Tx}; use namada::types::address::{masp, tokens, Address}; use namada::types::governance::{ OfflineProposal, OfflineVote, ProposalResult, ProposalVote, TallyResult, - VotePower, + VotePower, VoteType, }; use namada::types::hash::Hash; use namada::types::key::*; @@ -755,6 +756,7 @@ pub async fn query_proposal(_ctx: Context, args: args::QueryProposal) { let author_key = gov_storage::get_author_key(id); let start_epoch_key = gov_storage::get_voting_start_epoch_key(id); let end_epoch_key = gov_storage::get_voting_end_epoch_key(id); + let proposal_type_key = gov_storage::get_proposal_type_key(id); let author = query_storage_value::
(client, &author_key).await?; @@ -762,6 +764,9 @@ pub async fn query_proposal(_ctx: Context, args: args::QueryProposal) { query_storage_value::(client, &start_epoch_key).await?; let end_epoch = query_storage_value::(client, &end_epoch_key).await?; + let proposal_type = + query_storage_value::(client, &proposal_type_key) + .await?; if details { let content_key = gov_storage::get_content_key(id); @@ -775,6 +780,7 @@ pub async fn query_proposal(_ctx: Context, args: args::QueryProposal) { query_storage_value::(client, &grace_epoch_key).await?; println!("Proposal: {}", id); + println!("{:4}Type: {}", "", proposal_type); //FIXME: need Offline type? println!("{:4}Author: {}", "", author); println!("{:4}Content:", ""); for (key, value) in &content { @@ -789,7 +795,8 @@ pub async fn query_proposal(_ctx: Context, args: args::QueryProposal) { { let votes = get_proposal_votes(client, start_epoch, id).await; let partial_proposal_result = - compute_tally(client, start_epoch, votes).await; + compute_tally(client, start_epoch, votes, &proposal_type) + .await; println!( "{:4}Yay votes: {}", "", partial_proposal_result.total_yay_power @@ -802,12 +809,14 @@ pub async fn query_proposal(_ctx: Context, args: args::QueryProposal) { } else { let votes = get_proposal_votes(client, start_epoch, id).await; let proposal_result = - compute_tally(client, start_epoch, votes).await; + compute_tally(client, start_epoch, votes, &proposal_type) + .await; println!("{:4}Status: done", ""); println!("{:4}Result: {}", "", proposal_result); } } else { println!("Proposal: {}", id); + println!("{:4}Type: {}", "", proposal_type); println!("{:4}Author: {}", "", author); println!("{:4}Start Epoch: {}", "", start_epoch); println!("{:4}End Epoch: {}", "", end_epoch); @@ -1182,8 +1191,24 @@ pub async fn query_proposal_result( if current_epoch > end_epoch { let votes = get_proposal_votes(&client, end_epoch, id).await; - let proposal_result = - compute_tally(&client, end_epoch, votes).await; + let proposal_type_key = + gov_storage::get_proposal_type_key(id); + let proposal_type = + query_storage_value::( + &client, + &proposal_type_key, + ) + .await + .expect( + "Could not read proposal type from storage", + ); + let proposal_result = compute_tally( + &client, + end_epoch, + votes, + &proposal_type, + ) + .await; println!("Proposal: {}", id); println!("{:4}Result: {}", "", proposal_result); } else { @@ -1275,9 +1300,13 @@ pub async fn query_proposal_result( files, ) .await; - let proposal_result = - compute_tally(&client, proposal.tally_epoch, votes) - .await; + let proposal_result = compute_tally( + &client, + proposal.tally_epoch, + votes, + &ProposalType::Default(None), + ) + .await; println!("{:4}Result: {}", "", proposal_result); } @@ -2202,11 +2231,16 @@ pub async fn get_proposal_votes( let vote_iter = query_storage_prefix::(client, &vote_prefix_key).await; - let mut yay_validators: HashMap = HashMap::new(); - let mut yay_delegators: HashMap> = - HashMap::new(); - let mut nay_delegators: HashMap> = + let mut yay_validators: HashMap = HashMap::new(); + let mut yay_delegators: HashMap< + Address, + HashMap, + > = HashMap::new(); + let mut nay_delegators: HashMap< + Address, + HashMap, + > = HashMap::new(); if let Some(vote_iter) = vote_iter { for (key, vote) in vote_iter { @@ -2219,7 +2253,7 @@ pub async fn get_proposal_votes( .await .unwrap_or_default() .into(); - yay_validators.insert(voter_address, amount); + yay_validators.insert(voter_address, (amount, vote)); } else if !validators.contains(&voter_address) { let validator_address = gov_storage::get_vote_delegation_address(&key) @@ -2238,13 +2272,17 @@ pub async fn get_proposal_votes( if vote.is_yay() { let entry = yay_delegators.entry(voter_address).or_default(); - entry - .insert(validator_address, VotePower::from(amount)); + entry.insert( + validator_address, + (VotePower::from(amount), vote), + ); } else { let entry = nay_delegators.entry(voter_address).or_default(); - entry - .insert(validator_address, VotePower::from(amount)); + entry.insert( + validator_address, + (VotePower::from(amount), vote), + ); } } } @@ -2267,11 +2305,16 @@ pub async fn get_proposal_offline_votes( let proposal_hash = proposal.compute_hash(); - let mut yay_validators: HashMap = HashMap::new(); - let mut yay_delegators: HashMap> = - HashMap::new(); - let mut nay_delegators: HashMap> = + let mut yay_validators: HashMap = HashMap::new(); + let mut yay_delegators: HashMap< + Address, + HashMap, + > = HashMap::new(); + let mut nay_delegators: HashMap< + Address, + HashMap, + > = HashMap::new(); for path in files { let file = File::open(&path).expect("Proposal file must exist."); @@ -2303,7 +2346,10 @@ pub async fn get_proposal_offline_votes( .await .unwrap_or_default() .into(); - yay_validators.insert(proposal_vote.address, amount); + yay_validators.insert( + proposal_vote.address, + (amount, ProposalVote::Yay(VoteType::Default)), + ); } else if is_delegator_at( client, &proposal_vote.address, @@ -2347,12 +2393,21 @@ pub async fn get_proposal_offline_votes( let entry = yay_delegators .entry(proposal_vote.address.clone()) .or_default(); - entry.insert(validator, VotePower::from(delegated_amount)); + entry.insert( + validator, + ( + VotePower::from(delegated_amount), + ProposalVote::Yay(VoteType::Default), + ), + ); } else { let entry = nay_delegators .entry(proposal_vote.address.clone()) .or_default(); - entry.insert(validator, VotePower::from(delegated_amount)); + entry.insert( + validator, + (VotePower::from(delegated_amount), ProposalVote::Nay), + ); } } @@ -2442,6 +2497,7 @@ pub async fn compute_tally( client: &HttpClient, epoch: Epoch, votes: Votes, + proposal_type: &ProposalType, ) -> ProposalResult { let total_staked_tokens: VotePower = get_total_staked_tokens(client, epoch).await.into(); @@ -2452,42 +2508,257 @@ pub async fn compute_tally( nay_delegators, } = votes; - let mut total_yay_staked_tokens = VotePower::from(0_u64); - for (_, amount) in yay_validators.clone().into_iter() { - total_yay_staked_tokens += amount; - } + //FIXME: share some code with ledger + match proposal_type { + ProposalType::Default(_) => { + let mut total_yay_staked_tokens = VotePower::from(0u64); + for (_, (amount, validator_vote)) in yay_validators.iter() { + if let ProposalVote::Yay(VoteType::Default) = validator_vote { + total_yay_staked_tokens += amount; + } else { + tracing::error!( + "Unexpected vote type while computing tally" + ); + return ProposalResult { + result: TallyResult::Failed, + total_voting_power: total_staked_tokens, + total_yay_power: 0, + total_nay_power: 0, + }; + } + } - // YAY: Add delegator amount whose validator didn't vote / voted nay - for (_, vote_map) in yay_delegators.iter() { - for (validator_address, vote_power) in vote_map.iter() { - if !yay_validators.contains_key(validator_address) { - total_yay_staked_tokens += vote_power; + // YAY: Add delegator amount whose validator didn't vote / voted nay + for (_, vote_map) in yay_delegators.iter() { + for (validator_address, (vote_power, delegator_vote)) in + vote_map.iter() + { + if let ProposalVote::Yay(VoteType::Default) = delegator_vote + { + if !yay_validators.contains_key(validator_address) { + total_yay_staked_tokens += vote_power; + } + } else { + tracing::error!( + "Unexpected vote type while computing tally" + ); + return ProposalResult { + result: TallyResult::Failed, + total_voting_power: total_staked_tokens, + total_yay_power: 0, + total_nay_power: 0, + }; + } + } } - } - } - // NAY: Remove delegator amount whose validator validator vote yay - for (_, vote_map) in nay_delegators.iter() { - for (validator_address, vote_power) in vote_map.iter() { - if yay_validators.contains_key(validator_address) { - total_yay_staked_tokens -= vote_power; + // NAY: Remove delegator amount whose validator validator vote yay + for (_, vote_map) in nay_delegators.iter() { + for (validator_address, (vote_power, delegator_vote)) in + vote_map.iter() + { + if let ProposalVote::Nay = delegator_vote { + if yay_validators.contains_key(validator_address) { + total_yay_staked_tokens -= vote_power; + } + } else { + tracing::error!( + "Unexpected vote type while computing tally" + ); + return ProposalResult { + result: TallyResult::Failed, + total_voting_power: total_staked_tokens, + total_yay_power: 0, + total_nay_power: 0, + }; + } + } } - } - } - if total_yay_staked_tokens >= (total_staked_tokens / 3) * 2 { - ProposalResult { - result: TallyResult::Passed, - total_voting_power: total_staked_tokens, - total_yay_power: total_yay_staked_tokens, - total_nay_power: 0, + if total_yay_staked_tokens >= 2 / 3 * total_staked_tokens { + ProposalResult { + result: TallyResult::Passed, + total_voting_power: total_staked_tokens, + total_yay_power: total_yay_staked_tokens, + total_nay_power: 0, //FIXME: need this field? + } + } else { + ProposalResult { + result: TallyResult::Rejected, + total_voting_power: total_staked_tokens, + total_yay_power: total_yay_staked_tokens, + total_nay_power: 0, + } + } } - } else { - ProposalResult { - result: TallyResult::Rejected, - total_voting_power: total_staked_tokens, - total_yay_power: total_yay_staked_tokens, - total_nay_power: 0, + ProposalType::PGFCouncil => { + let mut total_yay_staked_tokens = HashMap::new(); + for (_, (amount, vote)) in yay_validators.iter() { + if let ProposalVote::Yay(VoteType::PGFCouncil(votes)) = vote { + for v in votes { + *total_yay_staked_tokens.entry(v).or_insert(0) += + amount; + } + } else { + tracing::error!( + "Unexpected vote type while computing tally" + ); + return ProposalResult { + result: TallyResult::Failed, + total_voting_power: total_staked_tokens, + total_yay_power: 0, + total_nay_power: 0, + }; + } + } + + // YAY: Add delegator amount whose validator didn't vote / voted nay or adjust voting power + // if delegator voted yay with a different memo + for (_, vote_map) in yay_delegators.iter() { + for (validator_address, (vote_power, delegator_vote)) in + vote_map.iter() + { + if let ProposalVote::Yay(VoteType::PGFCouncil( + delegator_votes, + )) = delegator_vote + { + match yay_validators.get(validator_address) { + Some((_, validator_vote)) => { + if let ProposalVote::Yay( + VoteType::PGFCouncil(validator_votes), + ) = validator_vote + { + for vote in validator_votes + .symmetric_difference(delegator_votes) + { + if validator_votes.contains(vote) { + // Delegator didn't vote for this, reduce voting power + if let Some(power) = + total_yay_staked_tokens + .get_mut(vote) + { + *power -= vote_power; + } else { + tracing::error!( +"Expected PGF vote was not in tally" ); + return ProposalResult { + result: TallyResult::Failed, + total_voting_power: + total_staked_tokens, + total_yay_power: 0, + total_nay_power: 0, + }; + } + } else { + // Validator didn't vote for this, add voting power + *total_yay_staked_tokens + .entry(vote) + .or_insert(0) += vote_power; + } + } + } else { + tracing::error!( + "Unexpected vote type while computing tally" + ); + return ProposalResult { + result: TallyResult::Failed, + total_voting_power: total_staked_tokens, + total_yay_power: 0, + total_nay_power: 0, + }; + } + } + None => { + // Validator didn't vote or voted nay, add delegator vote + + for vote in delegator_votes { + *total_yay_staked_tokens + .entry(vote) + .or_insert(0) += vote_power; + } + } + } + } else { + tracing::error!( + "Unexpected vote type while computing tally" + ); + return ProposalResult { + result: TallyResult::Failed, + total_voting_power: total_staked_tokens, + total_yay_power: 0, + total_nay_power: 0, + }; + } + } + } + + // NAY: Remove delegator amount whose validator voted yay + for (_, vote_map) in nay_delegators.iter() { + for (validator_address, (vote_power, _delegator_vote)) in + vote_map.iter() + { + if yay_validators.contains_key(validator_address) { + for (_, validator_vote) in + yay_validators.get(validator_address) + { + if let ProposalVote::Yay(VoteType::PGFCouncil( + votes, + )) = validator_vote + { + for vote in votes { + if let Some(power) = + total_yay_staked_tokens.get_mut(vote) + { + *power -= vote_power; + } else { + tracing::error!( +"Expected PGF vote was not in tally" ); + return ProposalResult { + result: TallyResult::Failed, + total_voting_power: + total_staked_tokens, + total_yay_power: 0, + total_nay_power: 0, + }; + } + } + } else { + tracing::error!( + "Unexpected vote type while computing tally" + ); + return ProposalResult { + result: TallyResult::Failed, + total_voting_power: total_staked_tokens, + total_yay_power: 0, + total_nay_power: 0, + }; + } + } + } + } + } + + // At least 1/3 of the total voting power must vote Yay + let total_voted_power = total_yay_staked_tokens + .iter() + .fold(0, |acc, (_, vote_power)| acc + vote_power); + + if total_voted_power >= 1 / 3 * total_staked_tokens { + //FIXME: add the winning council to the result + ProposalResult { + result: TallyResult::Passed, + total_voting_power: total_staked_tokens, + total_yay_power: 0, //FIXME: + total_nay_power: 0, + } + } else { + ProposalResult { + result: TallyResult::Rejected, + total_voting_power: total_staked_tokens, + total_yay_power: 0, //FIXME: + total_nay_power: 0, + } + } } } } diff --git a/apps/src/lib/node/ledger/shell/governance.rs b/apps/src/lib/node/ledger/shell/governance.rs index a27814029d..b0cb269574 100644 --- a/apps/src/lib/node/ledger/shell/governance.rs +++ b/apps/src/lib/node/ledger/shell/governance.rs @@ -1,10 +1,11 @@ use namada::core::ledger::slash_fund::ADDRESS as slash_fund_address; +use namada::core::types::transaction::governance::ProposalType; use namada::ledger::events::EventType; use namada::ledger::governance::{ storage as gov_storage, ADDRESS as gov_address, }; use namada::ledger::native_vp::governance::utils::{ - compute_tally, get_proposal_votes, ProposalEvent, + compute_tally, get_proposal_votes, ProposalEvent, Tally, }; use namada::ledger::protocol; use namada::ledger::storage::types::encode; @@ -35,6 +36,7 @@ where for id in std::mem::take(&mut shell.proposal_data) { let proposal_funds_key = gov_storage::get_funds_key(id); let proposal_end_epoch_key = gov_storage::get_voting_end_epoch_key(id); + let proposal_type_key = gov_storage::get_proposal_type_key(id); let funds = shell .read_storage_key::(&proposal_funds_key) @@ -50,139 +52,194 @@ where ) })?; + let proposal_type = shell + .read_storage_key::(&proposal_type_key) + .ok_or_else(|| { + Error::BadProposal(id, "Invalid proposal type".to_string()) + })?; + let votes = get_proposal_votes(&shell.wl_storage, proposal_end_epoch, id); - let is_accepted = votes.and_then(|votes| { - compute_tally(&shell.wl_storage, proposal_end_epoch, votes) + let tally_result = votes.and_then(|votes| { + compute_tally( + &shell.wl_storage, + proposal_end_epoch, + votes, + &proposal_type, + ) }); - let transfer_address = match is_accepted { - Ok(true) => { - let proposal_author_key = gov_storage::get_author_key(id); - let proposal_author = shell - .read_storage_key::
(&proposal_author_key) - .ok_or_else(|| { - Error::BadProposal( - id, - "Invalid proposal author.".to_string(), - ) - })?; - - let proposal_code_key = gov_storage::get_proposal_code_key(id); - let proposal_code = - shell.read_storage_key_bytes(&proposal_code_key); - match proposal_code { - Some(proposal_code) => { - let tx = Tx::new(proposal_code, Some(encode(&id))); - let tx_type = - TxType::Decrypted(DecryptedTx::Decrypted { - tx, - #[cfg(not(feature = "mainnet"))] - has_valid_pow: false, - }); - let pending_execution_key = - gov_storage::get_proposal_execution_key(id); - shell + // Execute proposal if succesful + let transfer_address = match tally_result { + Ok(result) => { + match result { + Tally::Default(success) => { + if success { + let proposal_author_key = + gov_storage::get_author_key(id); + let proposal_author = shell + .read_storage_key::
( + &proposal_author_key, + ) + .ok_or_else(|| { + Error::BadProposal( + id, + "Invalid proposal author.".to_string(), + ) + })?; + + let proposal_code_key = + gov_storage::get_proposal_code_key(id); + let proposal_code = shell + .read_storage_key_bytes(&proposal_code_key); + match proposal_code { + Some(proposal_code) => { + let tx = Tx::new( + proposal_code, + Some(encode(&id)), + ); + let tx_type = TxType::Decrypted( + DecryptedTx::Decrypted { + tx, + #[cfg(not(feature = "mainnet"))] + has_valid_pow: false, + }, + ); + let pending_execution_key = + gov_storage::get_proposal_execution_key( + id, + ); + shell .wl_storage .write(&pending_execution_key, ()) .expect("Should be able to write to storage."); - let tx_result = protocol::apply_tx( - tx_type, - 0, /* this is used to compute the fee - * based on the code size. We dont - * need it here. */ - TxIndex::default(), - &mut BlockGasMeter::default(), - &mut shell.wl_storage.write_log, - &shell.wl_storage.storage, - &mut shell.vp_wasm_cache, - &mut shell.tx_wasm_cache, - ); - shell + let tx_result = protocol::apply_tx( + tx_type, + 0, /* this is used to compute the fee + * based on the code size. We dont + * need it here. */ + TxIndex::default(), + &mut BlockGasMeter::default(), + &mut shell.wl_storage.write_log, + &shell.wl_storage.storage, + &mut shell.vp_wasm_cache, + &mut shell.tx_wasm_cache, + ); + shell .wl_storage .delete(&pending_execution_key) .expect("Should be able to delete the storage."); - match tx_result { - Ok(tx_result) => { - if tx_result.is_accepted() { - shell.wl_storage.write_log.commit_tx(); - let proposal_event: Event = - ProposalEvent::new( - EventType::Proposal.to_string(), - TallyResult::Passed, - id, - true, - true, - ) - .into(); - response.events.push(proposal_event); - proposals_result.passed.push(id); + match tx_result { + Ok(tx_result) => { + if tx_result.is_accepted() { + shell + .wl_storage + .write_log + .commit_tx(); + let proposal_event: Event = + ProposalEvent::new( + EventType::Proposal + .to_string(), + TallyResult::Passed, + id, + true, + true, + ) + .into(); + response + .events + .push(proposal_event); + proposals_result + .passed + .push(id); - proposal_author - } else { - shell.wl_storage.write_log.drop_tx(); + proposal_author + } else { + shell + .wl_storage + .write_log + .drop_tx(); + let proposal_event: Event = + ProposalEvent::new( + EventType::Proposal + .to_string(), + TallyResult::Passed, + id, + true, + false, + ) + .into(); + response + .events + .push(proposal_event); + proposals_result + .rejected + .push(id); + + slash_fund_address + } + } + Err(_e) => { + shell + .wl_storage + .write_log + .drop_tx(); + let proposal_event: Event = + ProposalEvent::new( + EventType::Proposal + .to_string(), + TallyResult::Passed, + id, + true, + false, + ) + .into(); + response + .events + .push(proposal_event); + proposals_result.rejected.push(id); + + slash_fund_address + } + } + } + None => { let proposal_event: Event = ProposalEvent::new( EventType::Proposal.to_string(), TallyResult::Passed, id, - true, + false, false, ) .into(); response.events.push(proposal_event); - proposals_result.rejected.push(id); + proposals_result.passed.push(id); - slash_fund_address + proposal_author } } - Err(_e) => { - shell.wl_storage.write_log.drop_tx(); - let proposal_event: Event = ProposalEvent::new( - EventType::Proposal.to_string(), - TallyResult::Passed, - id, - true, - false, - ) - .into(); - response.events.push(proposal_event); - proposals_result.rejected.push(id); + } else { + let proposal_event: Event = ProposalEvent::new( + EventType::Proposal.to_string(), + TallyResult::Rejected, + id, + false, + false, + ) + .into(); + response.events.push(proposal_event); + proposals_result.rejected.push(id); - slash_fund_address - } + slash_fund_address } } - None => { - let proposal_event: Event = ProposalEvent::new( - EventType::Proposal.to_string(), - TallyResult::Passed, - id, - false, - false, - ) - .into(); - response.events.push(proposal_event); - proposals_result.passed.push(id); - - proposal_author + Tally::PGFCouncil(_council) => { + //TODO: implement when PGF is in place + todo!(); } } } - Ok(false) => { - let proposal_event: Event = ProposalEvent::new( - EventType::Proposal.to_string(), - TallyResult::Rejected, - id, - false, - false, - ) - .into(); - response.events.push(proposal_event); - proposals_result.rejected.push(id); - - slash_fund_address - } Err(err) => { tracing::error!( "Unexpectedly failed to tally proposal ID {id} with error \ diff --git a/core/src/ledger/storage_api/governance.rs b/core/src/ledger/storage_api/governance.rs index 0560547002..b71f4a6e40 100644 --- a/core/src/ledger/storage_api/governance.rs +++ b/core/src/ledger/storage_api/governance.rs @@ -30,7 +30,7 @@ where let proposal_type_key = storage::get_proposal_type_key(proposal_id); match data.r#type { - ProposalType::Default(Some(code)) => { + ProposalType::Default(Some(ref code)) => { // Remove wasm code and write it under a different subkey storage.write(&proposal_type_key, ProposalType::Default(None))?; let proposal_code_key = storage::get_proposal_code_key(proposal_id); diff --git a/core/src/types/governance.rs b/core/src/types/governance.rs index a03cef9140..e0c8d97c1a 100644 --- a/core/src/types/governance.rs +++ b/core/src/types/governance.rs @@ -1,6 +1,6 @@ //! Files defyining the types used in governance. -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::fmt::{self, Display}; use borsh::{BorshDeserialize, BorshSerialize}; @@ -22,6 +22,7 @@ pub type VotePower = u128; #[derive( Debug, Clone, + Hash, PartialEq, BorshSerialize, BorshDeserialize, @@ -32,13 +33,14 @@ pub type VotePower = u128; pub enum VoteType { /// A default vote without Memo Default, - /// A vote for the PGF council encoding for the proposed addresses and the budget cap - PGFCouncil(Vec
, u64), + /// A vote for the PGF council encoding for the proposed multisig addresses and the budget cap + PGFCouncil(BTreeSet<(Address, u64)>), } #[derive( Debug, Clone, + Hash, PartialEq, BorshSerialize, BorshDeserialize, @@ -82,6 +84,7 @@ pub enum ProposalVoteParseError { /// The result of a proposal pub enum TallyResult { + //FIXME: add payload to passed to specify the memo that passed /// Proposal was accepted Passed, /// Proposal was rejected diff --git a/core/src/types/transaction/governance.rs b/core/src/types/transaction/governance.rs index ec893634de..c838cbfb9b 100644 --- a/core/src/types/transaction/governance.rs +++ b/core/src/types/transaction/governance.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; @@ -24,6 +26,15 @@ pub enum ProposalType { PGFCouncil, } +impl Display for ProposalType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + ProposalType::Default(_) => write!(f, "Default"), + ProposalType::PGFCouncil => write!(f, "PGF Council"), + } + } +} + impl PartialEq for ProposalType { fn eq(&self, other: &VoteType) -> bool { match self { diff --git a/shared/src/ledger/native_vp/governance/utils.rs b/shared/src/ledger/native_vp/governance/utils.rs index a0337938ff..e94df4c2d7 100644 --- a/shared/src/ledger/native_vp/governance/utils.rs +++ b/shared/src/ledger/native_vp/governance/utils.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use borsh::BorshDeserialize; +use namada_core::types::transaction::governance::ProposalType; use namada_proof_of_stake::{ bond_amount, read_all_validator_addresses, read_pos_params, read_total_stake, read_validator_stake, @@ -13,7 +14,9 @@ use crate::ledger::governance::storage as gov_storage; use crate::ledger::pos::BondId; use crate::ledger::storage_api; use crate::types::address::Address; -use crate::types::governance::{ProposalVote, TallyResult, VotePower}; +use crate::types::governance::{ + ProposalVote, TallyResult, VotePower, VoteType, +}; use crate::types::storage::Epoch; use crate::types::token; @@ -21,12 +24,15 @@ use crate::types::token; /// outcome pub struct Votes { /// Map from validators who votes yay to their total stake amount - pub yay_validators: HashMap, + pub yay_validators: HashMap, /// Map from delegation who votes yay to their bond amount - pub yay_delegators: HashMap>, + pub yay_delegators: + HashMap>, /// Map from delegation who votes nay to their bond amount - pub nay_delegators: HashMap>, + pub nay_delegators: + HashMap>, } +//FIXME: since I attach the vote, can I use only two field, one for the validators and one for the delegators? /// Proposal errors #[derive(Error, Debug)] @@ -75,12 +81,19 @@ impl ProposalEvent { } } -/// Return a proposal result - accepted only when the result is `Ok(true)`. +pub enum Tally { + //FIXME: can join this with TallyResult? + Default(bool), + PGFCouncil(Option<(Address, u64)>), +} + +/// Return a proposal result pub fn compute_tally( storage: &S, epoch: Epoch, votes: Votes, -) -> storage_api::Result + proposal_type: &ProposalType, +) -> storage_api::Result where S: storage_api::StorageRead, { @@ -94,30 +107,185 @@ where nay_delegators, } = votes; - let mut total_yay_staked_tokens = VotePower::from(0_u64); - for (_, amount) in yay_validators.clone().into_iter() { - total_yay_staked_tokens += amount; - } + match proposal_type { + ProposalType::Default(_) => { + let mut total_yay_staked_tokens = VotePower::from(0u64); + for (_, (amount, validator_vote)) in yay_validators.iter() { + if let ProposalVote::Yay(VoteType::Default) = validator_vote { + total_yay_staked_tokens += amount; + } else { + return Err(storage_api::Error::SimpleMessage( + "Unexpected vote type", + )); + } + } - // YAY: Add delegator amount whose validator didn't vote / voted nay - for (_, vote_map) in yay_delegators.iter() { - for (validator_address, vote_power) in vote_map.iter() { - if !yay_validators.contains_key(validator_address) { - total_yay_staked_tokens += vote_power; + // YAY: Add delegator amount whose validator didn't vote / voted nay + for (_, vote_map) in yay_delegators.iter() { + for (validator_address, (vote_power, delegator_vote)) in + vote_map.iter() + { + if let ProposalVote::Yay(VoteType::Default) = delegator_vote + { + if !yay_validators.contains_key(validator_address) { + total_yay_staked_tokens += vote_power; + } + } else { + return Err(storage_api::Error::SimpleMessage( + "Unexpected vote type", + )); + } + } } + + // NAY: Remove delegator amount whose validator validator vote yay + for (_, vote_map) in nay_delegators.iter() { + for (validator_address, (vote_power, delegator_vote)) in + vote_map.iter() + { + if let ProposalVote::Nay = delegator_vote { + if yay_validators.contains_key(validator_address) { + total_yay_staked_tokens -= vote_power; + } + } else { + return Err(storage_api::Error::SimpleMessage( + "Unexpected vote type", + )); + } + } + } + + Ok(Tally::Default( + total_yay_staked_tokens >= 2 / 3 * total_stake, + )) } - } + ProposalType::PGFCouncil => { + let mut total_yay_staked_tokens = HashMap::new(); + for (_, (amount, vote)) in yay_validators.iter() { + if let ProposalVote::Yay(VoteType::PGFCouncil(votes)) = vote { + for v in votes { + *total_yay_staked_tokens.entry(v).or_insert(0) += + amount; + } + } else { + return Err(storage_api::Error::SimpleMessage( + "Unexpected vote type", + )); + } + } - // NAY: Remove delegator amount whose validator validator vote yay - for (_, vote_map) in nay_delegators.iter() { - for (validator_address, vote_power) in vote_map.iter() { - if yay_validators.contains_key(validator_address) { - total_yay_staked_tokens -= vote_power; + // YAY: Add delegator amount whose validator didn't vote / voted nay or adjust voting power + // if delegator voted yay with a different memo + for (_, vote_map) in yay_delegators.iter() { + for (validator_address, (vote_power, delegator_vote)) in + vote_map.iter() + { + if let ProposalVote::Yay(VoteType::PGFCouncil( + delegator_votes, + )) = delegator_vote + { + match yay_validators.get(validator_address) { + Some((_, validator_vote)) => { + if let ProposalVote::Yay( + VoteType::PGFCouncil(validator_votes), + ) = validator_vote + { + for vote in validator_votes + .symmetric_difference(delegator_votes) + { + if validator_votes.contains(vote) { + // Delegator didn't vote for this, reduce voting power + if let Some(power) = + total_yay_staked_tokens + .get_mut(vote) + { + *power -= vote_power; + } else { + return Err(storage_api::Error::SimpleMessage("Expected PGF vote was not in tally")); + } + } else { + // Validator didn't vote for this, add voting power + *total_yay_staked_tokens + .entry(vote) + .or_insert(0) += vote_power; + } + } + } else { + return Err( + storage_api::Error::SimpleMessage( + "Unexpected vote type", + ), + ); + } + } + None => { + // Validator didn't vote or voted nay, add delegator vote + + for vote in delegator_votes { + *total_yay_staked_tokens + .entry(vote) + .or_insert(0) += vote_power; + } + } + } + } else { + return Err(storage_api::Error::SimpleMessage( + "Unexpected vote type", + )); + } + } + } + + // NAY: Remove delegator amount whose validator voted yay + for (_, vote_map) in nay_delegators.iter() { + for (validator_address, (vote_power, _delegator_vote)) in + vote_map.iter() + { + if yay_validators.contains_key(validator_address) { + for (_, validator_vote) in + yay_validators.get(validator_address) + { + if let ProposalVote::Yay(VoteType::PGFCouncil( + votes, + )) = validator_vote + { + for vote in votes { + if let Some(power) = + total_yay_staked_tokens.get_mut(vote) + { + *power -= vote_power; + } else { + return Err(storage_api::Error::SimpleMessage("Expected PGF vote was not in tally")); + } + } + } else { + return Err(storage_api::Error::SimpleMessage( + "Unexpected vote type", + )); + } + } + } + } + } + + // At least 1/3 of the total voting power must vote Yay + let total_voted_power = total_yay_staked_tokens + .iter() + .fold(0, |acc, (_, vote_power)| acc + vote_power); + + if total_voted_power >= 1 / 3 * total_stake { + // Select the winner council based on simple majority + Ok(Tally::PGFCouncil( + total_yay_staked_tokens + .into_iter() + .max_by(|a, b| a.1.cmp(&b.1)) + .map_or(None, |(vote, _)| Some(vote.to_owned())), + )) + } else { + Ok(Tally::PGFCouncil(None)) } } } - - Ok(3 * total_yay_staked_tokens >= 2 * total_stake) } /// Prepare Votes structure to compute proposal tally @@ -138,10 +306,14 @@ where storage_api::iter_prefix::(storage, &vote_prefix_key)?; let mut yay_validators = HashMap::new(); - let mut yay_delegators: HashMap> = - HashMap::new(); - let mut nay_delegators: HashMap> = - HashMap::new(); + let mut yay_delegators: HashMap< + Address, + HashMap, + > = HashMap::new(); + let mut nay_delegators: HashMap< + Address, + HashMap, + > = HashMap::new(); for next_vote in vote_iter { let (vote_key, vote) = next_vote?; @@ -158,7 +330,8 @@ where .unwrap_or_default() .into(); - yay_validators.insert(voter_address.clone(), amount); + yay_validators + .insert(voter_address.clone(), (amount, vote)); } else if !validators.contains(voter_address) { let validator_address = gov_storage::get_vote_delegation_address(&vote_key); @@ -179,7 +352,7 @@ where .or_default(); entry.insert( validator.to_owned(), - VotePower::from(amount), + (VotePower::from(amount), vote), ); } else { let entry = nay_delegators @@ -187,7 +360,7 @@ where .or_default(); entry.insert( validator.to_owned(), - VotePower::from(amount), + (VotePower::from(amount), vote), ); } } From 1cec4b1619e13fa84f11ab1738d9d799d6752187 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Wed, 18 Jan 2023 18:58:45 +0100 Subject: [PATCH 06/49] Fixes governance specs --- documentation/specs/src/base-ledger/governance.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/documentation/specs/src/base-ledger/governance.md b/documentation/specs/src/base-ledger/governance.md index 6aa971fa11..c089603740 100644 --- a/documentation/specs/src/base-ledger/governance.md +++ b/documentation/specs/src/base-ledger/governance.md @@ -105,13 +105,13 @@ At the moment, Namada supports 3 types of governance proposals: ```rust pub enum ProposalType { /// Carries the optional proposal code path - Custom(Option), + Default(Option), PGFCouncil, ETHBridge, } ``` -`Custom` represents a generic proposal with the following properties: +`Default` represents a generic proposal with the following properties: - Can carry a wasm code to be executed in case the proposal passes - Allows both validators and delegators to vote @@ -122,7 +122,7 @@ pub enum ProposalType { - Doesn't carry any wasm code - Allows both validators and delegators to vote -- Requires 1/3 of the total voting power to vote +- Requires 1/3 of the total voting power to vote `Yay` - Expect every vote to carry a memo in the form of a tuple `(Set
, BudgetCap)` `ETHBridge` is aimed at regulating actions on the bridge like the update of the Ethereum smart contracts or the withdrawing of all the funds from the `Vault` : From 2b120d39edb89b6f19a69e29916cdf54a19dce15 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Thu, 19 Jan 2023 12:20:13 +0100 Subject: [PATCH 07/49] Updates tally result --- apps/src/lib/client/rpc.rs | 25 +++++++++------- apps/src/lib/node/ledger/shell/governance.rs | 24 +++++++++++---- core/src/types/governance.rs | 29 ++++++++++++++++--- .../src/ledger/native_vp/governance/utils.rs | 12 ++------ 4 files changed, 61 insertions(+), 29 deletions(-) diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 1fde0906a2..89e63161e8 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -37,8 +37,8 @@ use namada::ledger::storage::ConversionState; use namada::proto::{SignedTxData, Tx}; use namada::types::address::{masp, tokens, Address}; use namada::types::governance::{ - OfflineProposal, OfflineVote, ProposalResult, ProposalVote, TallyResult, - VotePower, VoteType, + OfflineProposal, OfflineVote, ProposalResult, ProposalVote, Tally, + TallyResult, VotePower, VoteType, }; use namada::types::hash::Hash; use namada::types::key::*; @@ -2508,7 +2508,6 @@ pub async fn compute_tally( nay_delegators, } = votes; - //FIXME: share some code with ledger match proposal_type { ProposalType::Default(_) => { let mut total_yay_staked_tokens = VotePower::from(0u64); @@ -2577,10 +2576,10 @@ pub async fn compute_tally( if total_yay_staked_tokens >= 2 / 3 * total_staked_tokens { ProposalResult { - result: TallyResult::Passed, + result: TallyResult::Passed(Tally::Default(true)), total_voting_power: total_staked_tokens, total_yay_power: total_yay_staked_tokens, - total_nay_power: 0, //FIXME: need this field? + total_nay_power: 0, } } else { ProposalResult { @@ -2739,23 +2738,29 @@ pub async fn compute_tally( } // At least 1/3 of the total voting power must vote Yay - let total_voted_power = total_yay_staked_tokens + let total_yay_voted_power = total_yay_staked_tokens .iter() .fold(0, |acc, (_, vote_power)| acc + vote_power); - if total_voted_power >= 1 / 3 * total_staked_tokens { + if total_yay_voted_power >= 1 / 3 * total_staked_tokens { //FIXME: add the winning council to the result + let (vote, yay_power) = total_yay_staked_tokens + .into_iter() + .max_by(|a, b| a.1.cmp(&b.1)) + .map_or((None, 0), |(vote, power)| { + (Some(vote.to_owned()), power) + }); ProposalResult { - result: TallyResult::Passed, + result: TallyResult::Passed(Tally::PGFCouncil(vote)), total_voting_power: total_staked_tokens, - total_yay_power: 0, //FIXME: + total_yay_power: yay_power, total_nay_power: 0, } } else { ProposalResult { result: TallyResult::Rejected, total_voting_power: total_staked_tokens, - total_yay_power: 0, //FIXME: + total_yay_power: 0, total_nay_power: 0, } } diff --git a/apps/src/lib/node/ledger/shell/governance.rs b/apps/src/lib/node/ledger/shell/governance.rs index b0cb269574..a5bc5b189e 100644 --- a/apps/src/lib/node/ledger/shell/governance.rs +++ b/apps/src/lib/node/ledger/shell/governance.rs @@ -5,14 +5,14 @@ use namada::ledger::governance::{ storage as gov_storage, ADDRESS as gov_address, }; use namada::ledger::native_vp::governance::utils::{ - compute_tally, get_proposal_votes, ProposalEvent, Tally, + compute_tally, get_proposal_votes, ProposalEvent, }; use namada::ledger::protocol; use namada::ledger::storage::types::encode; use namada::ledger::storage::{DBIter, StorageHasher, DB}; use namada::ledger::storage_api::{token, StorageWrite}; use namada::types::address::Address; -use namada::types::governance::TallyResult; +use namada::types::governance::{Tally, TallyResult}; use namada::types::storage::Epoch; use super::*; @@ -140,7 +140,11 @@ where ProposalEvent::new( EventType::Proposal .to_string(), - TallyResult::Passed, + TallyResult::Passed( + Tally::Default( + true, + ), + ), id, true, true, @@ -163,7 +167,11 @@ where ProposalEvent::new( EventType::Proposal .to_string(), - TallyResult::Passed, + TallyResult::Passed( + Tally::Default( + true, + ), + ), id, true, false, @@ -188,7 +196,9 @@ where ProposalEvent::new( EventType::Proposal .to_string(), - TallyResult::Passed, + TallyResult::Passed( + Tally::Default(true), + ), id, true, false, @@ -207,7 +217,9 @@ where let proposal_event: Event = ProposalEvent::new( EventType::Proposal.to_string(), - TallyResult::Passed, + TallyResult::Passed( + Tally::Default(true), + ), id, false, false, diff --git a/core/src/types/governance.rs b/core/src/types/governance.rs index e0c8d97c1a..55eec46727 100644 --- a/core/src/types/governance.rs +++ b/core/src/types/governance.rs @@ -34,7 +34,8 @@ pub enum VoteType { /// A default vote without Memo Default, /// A vote for the PGF council encoding for the proposed multisig addresses and the budget cap - PGFCouncil(BTreeSet<(Address, u64)>), + PGFCouncil(BTreeSet<(Address, u64)>), //FIXME: use Amount instead of u64? + //FIXME: create a type for (Address, u64) ? } #[derive( @@ -82,11 +83,30 @@ pub enum ProposalVoteParseError { InvalidVote, } +pub enum Tally { + Default(bool), + PGFCouncil(Option<(Address, u64)>), +} + +impl Display for Tally { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Tally::Default(_) => Ok(()), + Tally::PGFCouncil(v) => match v { + Some((address, cap)) => { + write!(f, "PGF address: {}, Spending cap: {}", address, cap) + } + None => Ok(()), + }, + } + } +} + /// The result of a proposal pub enum TallyResult { - //FIXME: add payload to passed to specify the memo that passed + //FIXME: use this type as return of compute_tally? /// Proposal was accepted - Passed, + Passed(Tally), //FIXME: use an optional String here? /// Proposal was rejected Rejected, /// A critical error in tally computation @@ -127,7 +147,8 @@ impl Display for ProposalResult { impl Display for TallyResult { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - TallyResult::Passed => write!(f, "passed"), + TallyResult::Passed(vote) => write!(f, "passed {}", vote), + TallyResult::Rejected => write!(f, "rejected"), TallyResult::Failed => write!(f, "failed"), } diff --git a/shared/src/ledger/native_vp/governance/utils.rs b/shared/src/ledger/native_vp/governance/utils.rs index e94df4c2d7..ed6d83813d 100644 --- a/shared/src/ledger/native_vp/governance/utils.rs +++ b/shared/src/ledger/native_vp/governance/utils.rs @@ -15,7 +15,7 @@ use crate::ledger::pos::BondId; use crate::ledger::storage_api; use crate::types::address::Address; use crate::types::governance::{ - ProposalVote, TallyResult, VotePower, VoteType, + ProposalVote, Tally, TallyResult, VotePower, VoteType, }; use crate::types::storage::Epoch; use crate::types::token; @@ -81,12 +81,6 @@ impl ProposalEvent { } } -pub enum Tally { - //FIXME: can join this with TallyResult? - Default(bool), - PGFCouncil(Option<(Address, u64)>), -} - /// Return a proposal result pub fn compute_tally( storage: &S, @@ -269,11 +263,11 @@ where } // At least 1/3 of the total voting power must vote Yay - let total_voted_power = total_yay_staked_tokens + let total_yay_voted_power = total_yay_staked_tokens .iter() .fold(0, |acc, (_, vote_power)| acc + vote_power); - if total_voted_power >= 1 / 3 * total_stake { + if total_yay_voted_power >= 1 / 3 * total_stake { // Select the winner council based on simple majority Ok(Tally::PGFCouncil( total_yay_staked_tokens From 82892b6ddc75619badca1bf3c681c5db1969778f Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Thu, 19 Jan 2023 17:18:35 +0100 Subject: [PATCH 08/49] Refactors `compute_tally` and removes duplicate --- apps/src/lib/client/rpc.rs | 316 ++---------------- apps/src/lib/node/ledger/shell/governance.rs | 292 ++++++++-------- core/src/types/governance.rs | 49 ++- .../src/ledger/native_vp/governance/utils.rs | 148 +++++--- 4 files changed, 276 insertions(+), 529 deletions(-) diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 89e63161e8..8cc7a4c5fe 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -27,7 +27,7 @@ use namada::core::types::transaction::governance::ProposalType; use namada::ledger::events::Event; use namada::ledger::governance::parameters::GovParams; use namada::ledger::governance::storage as gov_storage; -use namada::ledger::native_vp::governance::utils::Votes; +use namada::ledger::native_vp::governance::utils::{self, Votes}; use namada::ledger::parameters::{storage as param_storage, EpochDuration}; use namada::ledger::pos::{ self, BondId, BondsAndUnbondsDetail, CommissionPair, PosParams, Slash, @@ -37,8 +37,7 @@ use namada::ledger::storage::ConversionState; use namada::proto::{SignedTxData, Tx}; use namada::types::address::{masp, tokens, Address}; use namada::types::governance::{ - OfflineProposal, OfflineVote, ProposalResult, ProposalVote, Tally, - TallyResult, VotePower, VoteType, + OfflineProposal, OfflineVote, ProposalVote, VotePower, VoteType, }; use namada::types::hash::Hash; use namada::types::key::*; @@ -780,7 +779,7 @@ pub async fn query_proposal(_ctx: Context, args: args::QueryProposal) { query_storage_value::(client, &grace_epoch_key).await?; println!("Proposal: {}", id); - println!("{:4}Type: {}", "", proposal_type); //FIXME: need Offline type? + println!("{:4}Type: {}", "", proposal_type); println!("{:4}Author: {}", "", author); println!("{:4}Content:", ""); for (key, value) in &content { @@ -789,14 +788,15 @@ pub async fn query_proposal(_ctx: Context, args: args::QueryProposal) { println!("{:4}Start Epoch: {}", "", start_epoch); println!("{:4}End Epoch: {}", "", end_epoch); println!("{:4}Grace Epoch: {}", "", grace_epoch); + let votes = get_proposal_votes(client, start_epoch, id).await; + let total_stake = + get_total_staked_tokens(client, start_epoch).await.into(); if start_epoch > current_epoch { println!("{:4}Status: pending", ""); } else if start_epoch <= current_epoch && current_epoch <= end_epoch { - let votes = get_proposal_votes(client, start_epoch, id).await; let partial_proposal_result = - compute_tally(client, start_epoch, votes, &proposal_type) - .await; + utils::compute_tally(votes, total_stake, &proposal_type); println!( "{:4}Yay votes: {}", "", partial_proposal_result.total_yay_power @@ -807,10 +807,8 @@ pub async fn query_proposal(_ctx: Context, args: args::QueryProposal) { ); println!("{:4}Status: on-going", ""); } else { - let votes = get_proposal_votes(client, start_epoch, id).await; let proposal_result = - compute_tally(client, start_epoch, votes, &proposal_type) - .await; + utils::compute_tally(votes, total_stake, &proposal_type); println!("{:4}Status: done", ""); println!("{:4}Result: {}", "", proposal_result); } @@ -1202,13 +1200,15 @@ pub async fn query_proposal_result( .expect( "Could not read proposal type from storage", ); - let proposal_result = compute_tally( - &client, - end_epoch, + let total_stake = + get_total_staked_tokens(&client, end_epoch) + .await + .into(); + let proposal_result = utils::compute_tally( votes, + total_stake, &proposal_type, - ) - .await; + ); println!("Proposal: {}", id); println!("{:4}Result: {}", "", proposal_result); } else { @@ -1300,13 +1300,17 @@ pub async fn query_proposal_result( files, ) .await; - let proposal_result = compute_tally( + let total_stake = get_total_staked_tokens( &client, proposal.tally_epoch, + ) + .await + .into(); + let proposal_result = utils::compute_tally( votes, + total_stake, &ProposalType::Default(None), - ) - .await; + ); println!("{:4}Result: {}", "", proposal_result); } @@ -2492,282 +2496,6 @@ pub async fn get_proposal_offline_votes( } } -// Compute the result of a proposal -pub async fn compute_tally( - client: &HttpClient, - epoch: Epoch, - votes: Votes, - proposal_type: &ProposalType, -) -> ProposalResult { - let total_staked_tokens: VotePower = - get_total_staked_tokens(client, epoch).await.into(); - - let Votes { - yay_validators, - yay_delegators, - nay_delegators, - } = votes; - - match proposal_type { - ProposalType::Default(_) => { - let mut total_yay_staked_tokens = VotePower::from(0u64); - for (_, (amount, validator_vote)) in yay_validators.iter() { - if let ProposalVote::Yay(VoteType::Default) = validator_vote { - total_yay_staked_tokens += amount; - } else { - tracing::error!( - "Unexpected vote type while computing tally" - ); - return ProposalResult { - result: TallyResult::Failed, - total_voting_power: total_staked_tokens, - total_yay_power: 0, - total_nay_power: 0, - }; - } - } - - // YAY: Add delegator amount whose validator didn't vote / voted nay - for (_, vote_map) in yay_delegators.iter() { - for (validator_address, (vote_power, delegator_vote)) in - vote_map.iter() - { - if let ProposalVote::Yay(VoteType::Default) = delegator_vote - { - if !yay_validators.contains_key(validator_address) { - total_yay_staked_tokens += vote_power; - } - } else { - tracing::error!( - "Unexpected vote type while computing tally" - ); - return ProposalResult { - result: TallyResult::Failed, - total_voting_power: total_staked_tokens, - total_yay_power: 0, - total_nay_power: 0, - }; - } - } - } - - // NAY: Remove delegator amount whose validator validator vote yay - for (_, vote_map) in nay_delegators.iter() { - for (validator_address, (vote_power, delegator_vote)) in - vote_map.iter() - { - if let ProposalVote::Nay = delegator_vote { - if yay_validators.contains_key(validator_address) { - total_yay_staked_tokens -= vote_power; - } - } else { - tracing::error!( - "Unexpected vote type while computing tally" - ); - return ProposalResult { - result: TallyResult::Failed, - total_voting_power: total_staked_tokens, - total_yay_power: 0, - total_nay_power: 0, - }; - } - } - } - - if total_yay_staked_tokens >= 2 / 3 * total_staked_tokens { - ProposalResult { - result: TallyResult::Passed(Tally::Default(true)), - total_voting_power: total_staked_tokens, - total_yay_power: total_yay_staked_tokens, - total_nay_power: 0, - } - } else { - ProposalResult { - result: TallyResult::Rejected, - total_voting_power: total_staked_tokens, - total_yay_power: total_yay_staked_tokens, - total_nay_power: 0, - } - } - } - ProposalType::PGFCouncil => { - let mut total_yay_staked_tokens = HashMap::new(); - for (_, (amount, vote)) in yay_validators.iter() { - if let ProposalVote::Yay(VoteType::PGFCouncil(votes)) = vote { - for v in votes { - *total_yay_staked_tokens.entry(v).or_insert(0) += - amount; - } - } else { - tracing::error!( - "Unexpected vote type while computing tally" - ); - return ProposalResult { - result: TallyResult::Failed, - total_voting_power: total_staked_tokens, - total_yay_power: 0, - total_nay_power: 0, - }; - } - } - - // YAY: Add delegator amount whose validator didn't vote / voted nay or adjust voting power - // if delegator voted yay with a different memo - for (_, vote_map) in yay_delegators.iter() { - for (validator_address, (vote_power, delegator_vote)) in - vote_map.iter() - { - if let ProposalVote::Yay(VoteType::PGFCouncil( - delegator_votes, - )) = delegator_vote - { - match yay_validators.get(validator_address) { - Some((_, validator_vote)) => { - if let ProposalVote::Yay( - VoteType::PGFCouncil(validator_votes), - ) = validator_vote - { - for vote in validator_votes - .symmetric_difference(delegator_votes) - { - if validator_votes.contains(vote) { - // Delegator didn't vote for this, reduce voting power - if let Some(power) = - total_yay_staked_tokens - .get_mut(vote) - { - *power -= vote_power; - } else { - tracing::error!( -"Expected PGF vote was not in tally" ); - return ProposalResult { - result: TallyResult::Failed, - total_voting_power: - total_staked_tokens, - total_yay_power: 0, - total_nay_power: 0, - }; - } - } else { - // Validator didn't vote for this, add voting power - *total_yay_staked_tokens - .entry(vote) - .or_insert(0) += vote_power; - } - } - } else { - tracing::error!( - "Unexpected vote type while computing tally" - ); - return ProposalResult { - result: TallyResult::Failed, - total_voting_power: total_staked_tokens, - total_yay_power: 0, - total_nay_power: 0, - }; - } - } - None => { - // Validator didn't vote or voted nay, add delegator vote - - for vote in delegator_votes { - *total_yay_staked_tokens - .entry(vote) - .or_insert(0) += vote_power; - } - } - } - } else { - tracing::error!( - "Unexpected vote type while computing tally" - ); - return ProposalResult { - result: TallyResult::Failed, - total_voting_power: total_staked_tokens, - total_yay_power: 0, - total_nay_power: 0, - }; - } - } - } - - // NAY: Remove delegator amount whose validator voted yay - for (_, vote_map) in nay_delegators.iter() { - for (validator_address, (vote_power, _delegator_vote)) in - vote_map.iter() - { - if yay_validators.contains_key(validator_address) { - for (_, validator_vote) in - yay_validators.get(validator_address) - { - if let ProposalVote::Yay(VoteType::PGFCouncil( - votes, - )) = validator_vote - { - for vote in votes { - if let Some(power) = - total_yay_staked_tokens.get_mut(vote) - { - *power -= vote_power; - } else { - tracing::error!( -"Expected PGF vote was not in tally" ); - return ProposalResult { - result: TallyResult::Failed, - total_voting_power: - total_staked_tokens, - total_yay_power: 0, - total_nay_power: 0, - }; - } - } - } else { - tracing::error!( - "Unexpected vote type while computing tally" - ); - return ProposalResult { - result: TallyResult::Failed, - total_voting_power: total_staked_tokens, - total_yay_power: 0, - total_nay_power: 0, - }; - } - } - } - } - } - - // At least 1/3 of the total voting power must vote Yay - let total_yay_voted_power = total_yay_staked_tokens - .iter() - .fold(0, |acc, (_, vote_power)| acc + vote_power); - - if total_yay_voted_power >= 1 / 3 * total_staked_tokens { - //FIXME: add the winning council to the result - let (vote, yay_power) = total_yay_staked_tokens - .into_iter() - .max_by(|a, b| a.1.cmp(&b.1)) - .map_or((None, 0), |(vote, power)| { - (Some(vote.to_owned()), power) - }); - ProposalResult { - result: TallyResult::Passed(Tally::PGFCouncil(vote)), - total_voting_power: total_staked_tokens, - total_yay_power: yay_power, - total_nay_power: 0, - } - } else { - ProposalResult { - result: TallyResult::Rejected, - total_voting_power: total_staked_tokens, - total_yay_power: 0, - total_nay_power: 0, - } - } - } - } -} - pub async fn get_bond_amount_at( client: &HttpClient, delegator: &Address, diff --git a/apps/src/lib/node/ledger/shell/governance.rs b/apps/src/lib/node/ledger/shell/governance.rs index a5bc5b189e..10c24bff7b 100644 --- a/apps/src/lib/node/ledger/shell/governance.rs +++ b/apps/src/lib/node/ledger/shell/governance.rs @@ -11,8 +11,9 @@ use namada::ledger::protocol; use namada::ledger::storage::types::encode; use namada::ledger::storage::{DBIter, StorageHasher, DB}; use namada::ledger::storage_api::{token, StorageWrite}; +use namada::proof_of_stake::read_total_stake; use namada::types::address::Address; -use namada::types::governance::{Tally, TallyResult}; +use namada::types::governance::{Tally, TallyResult, VotePower}; use namada::types::storage::Epoch; use super::*; @@ -59,145 +60,104 @@ where })?; let votes = - get_proposal_votes(&shell.wl_storage, proposal_end_epoch, id); - let tally_result = votes.and_then(|votes| { - compute_tally( - &shell.wl_storage, - proposal_end_epoch, - votes, - &proposal_type, - ) - }); + get_proposal_votes(&shell.wl_storage, proposal_end_epoch, id) + .map_err(|msg| Error::BadProposal(id, msg.to_string()))?; + let params = read_pos_params(&shell.wl_storage) + .map_err(|msg| Error::BadProposal(id, msg.to_string()))?; + let total_stake = + read_total_stake(&shell.wl_storage, ¶ms, proposal_end_epoch) + .map_err(|msg| Error::BadProposal(id, msg.to_string()))?; + let total_stake = VotePower::from(u64::from(total_stake)); + let tally_result = + compute_tally(votes, total_stake, &proposal_type).result; // Execute proposal if succesful let transfer_address = match tally_result { - Ok(result) => { - match result { - Tally::Default(success) => { - if success { - let proposal_author_key = - gov_storage::get_author_key(id); - let proposal_author = shell - .read_storage_key::
( - &proposal_author_key, + TallyResult::Passed(tally) => { + match tally { + Tally::Default => { + let proposal_author_key = + gov_storage::get_author_key(id); + let proposal_author = shell + .read_storage_key::
(&proposal_author_key) + .ok_or_else(|| { + Error::BadProposal( + id, + "Invalid proposal author.".to_string(), ) - .ok_or_else(|| { - Error::BadProposal( - id, - "Invalid proposal author.".to_string(), - ) - })?; + })?; - let proposal_code_key = - gov_storage::get_proposal_code_key(id); - let proposal_code = shell - .read_storage_key_bytes(&proposal_code_key); - match proposal_code { - Some(proposal_code) => { - let tx = Tx::new( - proposal_code, - Some(encode(&id)), + let proposal_code_key = + gov_storage::get_proposal_code_key(id); + let proposal_code = + shell.read_storage_key_bytes(&proposal_code_key); + match proposal_code { + Some(proposal_code) => { + let tx = + Tx::new(proposal_code, Some(encode(&id))); + let tx_type = + TxType::Decrypted(DecryptedTx::Decrypted { + tx, + #[cfg(not(feature = "mainnet"))] + has_valid_pow: false, + }); + let pending_execution_key = + gov_storage::get_proposal_execution_key(id); + shell + .wl_storage + .write(&pending_execution_key, ()) + .expect( + "Should be able to write to storage.", ); - let tx_type = TxType::Decrypted( - DecryptedTx::Decrypted { - tx, - #[cfg(not(feature = "mainnet"))] - has_valid_pow: false, - }, + let tx_result = protocol::apply_tx( + tx_type, + 0, /* this is used to compute the fee + * based on the code size. We dont + * need it here. */ + TxIndex::default(), + &mut BlockGasMeter::default(), + &mut shell.wl_storage.write_log, + &shell.wl_storage.storage, + &mut shell.vp_wasm_cache, + &mut shell.tx_wasm_cache, + ); + shell + .wl_storage + .storage + .delete(&pending_execution_key) + .expect( + "Should be able to delete the storage.", ); - let pending_execution_key = - gov_storage::get_proposal_execution_key( - id, - ); - shell - .wl_storage - .write(&pending_execution_key, ()) - .expect("Should be able to write to storage."); - let tx_result = protocol::apply_tx( - tx_type, - 0, /* this is used to compute the fee - * based on the code size. We dont - * need it here. */ - TxIndex::default(), - &mut BlockGasMeter::default(), - &mut shell.wl_storage.write_log, - &shell.wl_storage.storage, - &mut shell.vp_wasm_cache, - &mut shell.tx_wasm_cache, - ); - shell - .wl_storage - .delete(&pending_execution_key) - .expect("Should be able to delete the storage."); - match tx_result { - Ok(tx_result) => { - if tx_result.is_accepted() { - shell - .wl_storage - .write_log - .commit_tx(); - let proposal_event: Event = - ProposalEvent::new( - EventType::Proposal - .to_string(), - TallyResult::Passed( - Tally::Default( - true, - ), - ), - id, - true, - true, - ) - .into(); - response - .events - .push(proposal_event); - proposals_result - .passed - .push(id); - - proposal_author - } else { - shell - .wl_storage - .write_log - .drop_tx(); - let proposal_event: Event = - ProposalEvent::new( - EventType::Proposal - .to_string(), - TallyResult::Passed( - Tally::Default( - true, - ), - ), - id, - true, - false, - ) - .into(); - response - .events - .push(proposal_event); - proposals_result - .rejected - .push(id); + match tx_result { + Ok(tx_result) => { + if tx_result.is_accepted() { + shell.wl_storage.commit_tx(); + let proposal_event: Event = + ProposalEvent::new( + EventType::Proposal + .to_string(), + TallyResult::Passed( + Tally::Default, + ), + id, + true, + true, + ) + .into(); + response + .events + .push(proposal_event); + proposals_result.passed.push(id); - slash_fund_address - } - } - Err(_e) => { - shell - .wl_storage - .write_log - .drop_tx(); + proposal_author + } else { + shell.wl_storage.drop_tx(); let proposal_event: Event = ProposalEvent::new( EventType::Proposal .to_string(), TallyResult::Passed( - Tally::Default(true), + Tally::Default, ), id, true, @@ -212,38 +172,40 @@ where slash_fund_address } } - } - None => { - let proposal_event: Event = - ProposalEvent::new( - EventType::Proposal.to_string(), - TallyResult::Passed( - Tally::Default(true), - ), - id, - false, - false, - ) - .into(); - response.events.push(proposal_event); - proposals_result.passed.push(id); + Err(_e) => { + shell.wl_storage.drop_tx(); + let proposal_event: Event = + ProposalEvent::new( + EventType::Proposal.to_string(), + TallyResult::Passed( + Tally::Default, + ), + id, + true, + false, + ) + .into(); + response.events.push(proposal_event); + proposals_result.rejected.push(id); - proposal_author + slash_fund_address + } } } - } else { - let proposal_event: Event = ProposalEvent::new( - EventType::Proposal.to_string(), - TallyResult::Rejected, - id, - false, - false, - ) - .into(); - response.events.push(proposal_event); - proposals_result.rejected.push(id); + None => { + let proposal_event: Event = ProposalEvent::new( + EventType::Proposal.to_string(), + TallyResult::Passed(Tally::Default), + id, + false, + false, + ) + .into(); + response.events.push(proposal_event); + proposals_result.passed.push(id); - slash_fund_address + proposal_author + } } } Tally::PGFCouncil(_council) => { @@ -252,14 +214,28 @@ where } } } - Err(err) => { + TallyResult::Rejected => { + let proposal_event: Event = ProposalEvent::new( + EventType::Proposal.to_string(), + TallyResult::Rejected, + id, + false, + false, + ) + .into(); + response.events.push(proposal_event); + proposals_result.rejected.push(id); + + slash_fund_address + } + TallyResult::Failed(msg) => { tracing::error!( "Unexpectedly failed to tally proposal ID {id} with error \ - {err}" + {msg}" ); let proposal_event: Event = ProposalEvent::new( EventType::Proposal.to_string(), - TallyResult::Failed, + TallyResult::Failed(msg), id, false, false, diff --git a/core/src/types/governance.rs b/core/src/types/governance.rs index 55eec46727..7c9a003f1d 100644 --- a/core/src/types/governance.rs +++ b/core/src/types/governance.rs @@ -13,11 +13,15 @@ use crate::types::hash::Hash; use crate::types::key::common::{self, Signature}; use crate::types::key::SigScheme; use crate::types::storage::Epoch; +use crate::types::token::Amount; use crate::types::token::SCALE; /// Type alias for vote power pub type VotePower = u128; +/// A PGF cocuncil composed of the address and spending cap +pub type Council = (Address, Amount); + /// The type of a governance vote with the optional associated Memo #[derive( Debug, @@ -34,8 +38,7 @@ pub enum VoteType { /// A default vote without Memo Default, /// A vote for the PGF council encoding for the proposed multisig addresses and the budget cap - PGFCouncil(BTreeSet<(Address, u64)>), //FIXME: use Amount instead of u64? - //FIXME: create a type for (Address, u64) ? + PGFCouncil(BTreeSet), } #[derive( @@ -83,34 +86,22 @@ pub enum ProposalVoteParseError { InvalidVote, } +/// The type of the tally pub enum Tally { - Default(bool), - PGFCouncil(Option<(Address, u64)>), -} - -impl Display for Tally { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Tally::Default(_) => Ok(()), - Tally::PGFCouncil(v) => match v { - Some((address, cap)) => { - write!(f, "PGF address: {}, Spending cap: {}", address, cap) - } - None => Ok(()), - }, - } - } + /// Tally a default proposal + Default, + /// Tally a PGF proposal + PGFCouncil(Council), } /// The result of a proposal pub enum TallyResult { - //FIXME: use this type as return of compute_tally? - /// Proposal was accepted - Passed(Tally), //FIXME: use an optional String here? + /// Proposal was accepted with the associated value + Passed(Tally), /// Proposal was rejected Rejected, - /// A critical error in tally computation - Failed, + /// A critical error in tally computation with an error message + Failed(String), } /// The result with votes of a proposal @@ -147,10 +138,16 @@ impl Display for ProposalResult { impl Display for TallyResult { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - TallyResult::Passed(vote) => write!(f, "passed {}", vote), - + TallyResult::Passed(vote) => match vote { + Tally::Default => write!(f, "passed"), + Tally::PGFCouncil((council, cap)) => write!( + f, + "passed with PGF council address: {}, spending cap: {}", + council, cap + ), + }, TallyResult::Rejected => write!(f, "rejected"), - TallyResult::Failed => write!(f, "failed"), + TallyResult::Failed(msg) => write!(f, "failed with: {}", msg), } } } diff --git a/shared/src/ledger/native_vp/governance/utils.rs b/shared/src/ledger/native_vp/governance/utils.rs index ed6d83813d..3ca7251d05 100644 --- a/shared/src/ledger/native_vp/governance/utils.rs +++ b/shared/src/ledger/native_vp/governance/utils.rs @@ -3,10 +3,11 @@ use std::collections::HashMap; use borsh::BorshDeserialize; +use namada_core::types::governance::ProposalResult; use namada_core::types::transaction::governance::ProposalType; use namada_proof_of_stake::{ bond_amount, read_all_validator_addresses, read_pos_params, - read_total_stake, read_validator_stake, + read_validator_stake, }; use thiserror::Error; @@ -82,19 +83,11 @@ impl ProposalEvent { } /// Return a proposal result -pub fn compute_tally( - storage: &S, - epoch: Epoch, +pub fn compute_tally( votes: Votes, + total_stake: VotePower, proposal_type: &ProposalType, -) -> storage_api::Result -where - S: storage_api::StorageRead, -{ - let params = read_pos_params(storage)?; - let total_stake = read_total_stake(storage, ¶ms, epoch)?; - let total_stake = VotePower::from(u64::from(total_stake)); - +) -> ProposalResult { let Votes { yay_validators, yay_delegators, @@ -103,14 +96,20 @@ where match proposal_type { ProposalType::Default(_) => { - let mut total_yay_staked_tokens = VotePower::from(0u64); + let mut total_yay_staked_tokens = VotePower::default(); for (_, (amount, validator_vote)) in yay_validators.iter() { if let ProposalVote::Yay(VoteType::Default) = validator_vote { total_yay_staked_tokens += amount; } else { - return Err(storage_api::Error::SimpleMessage( - "Unexpected vote type", - )); + return ProposalResult { + result: TallyResult::Failed(format!( + "Unexpected vote type. Expected: Default, Found: {}", + validator_vote + )), + total_voting_power: total_stake, + total_yay_power: 0, + total_nay_power: 0, + }; } } @@ -125,9 +124,11 @@ where total_yay_staked_tokens += vote_power; } } else { - return Err(storage_api::Error::SimpleMessage( - "Unexpected vote type", - )); + return ProposalResult { + result: TallyResult::Failed(format!("Unexpected vote type. Expected: Default, Found: {}", delegator_vote)), + total_voting_power: total_stake, + total_yay_power: 0, + total_nay_power: 0} } } } @@ -142,29 +143,50 @@ where total_yay_staked_tokens -= vote_power; } } else { - return Err(storage_api::Error::SimpleMessage( - "Unexpected vote type", - )); + return ProposalResult { + result: TallyResult::Failed(format!("Unexpected vote type. Expected: Default, Found: {}", delegator_vote)), + total_voting_power: total_stake, + total_yay_power: 0, + total_nay_power: 0} } } } - Ok(Tally::Default( - total_yay_staked_tokens >= 2 / 3 * total_stake, - )) + // Proposal passes if 2/3 of total voting power voted Yay + if total_yay_staked_tokens >= 2 / 3 * total_stake { + ProposalResult { + result: TallyResult::Passed(Tally::Default), + total_voting_power: total_stake, + total_yay_power: total_yay_staked_tokens, + total_nay_power: 0} + } else { + ProposalResult{ + result: TallyResult::Rejected, + total_voting_power: total_stake, + total_yay_power: total_yay_staked_tokens, + total_nay_power: 0 + } + } } ProposalType::PGFCouncil => { let mut total_yay_staked_tokens = HashMap::new(); - for (_, (amount, vote)) in yay_validators.iter() { - if let ProposalVote::Yay(VoteType::PGFCouncil(votes)) = vote { + for (_, (amount, validator_vote)) in yay_validators.iter() { + if let ProposalVote::Yay(VoteType::PGFCouncil(votes)) = + validator_vote + { for v in votes { *total_yay_staked_tokens.entry(v).or_insert(0) += amount; } } else { - return Err(storage_api::Error::SimpleMessage( - "Unexpected vote type", - )); + return ProposalResult { + result: TallyResult::Failed(format!( + "Unexpected vote type. Expected: PGFCouncil, Found: {}", + validator_vote + )), + total_voting_power: total_stake, + total_yay_power: 0, + total_nay_power: 0} } } @@ -195,7 +217,12 @@ where { *power -= vote_power; } else { - return Err(storage_api::Error::SimpleMessage("Expected PGF vote was not in tally")); + return ProposalResult { + result: TallyResult::Failed(format!("Expected PGF vote {:?} was not in tally", vote)), + total_voting_power: total_stake, + total_yay_power: 0, + total_nay_power: 0} + } } else { // Validator didn't vote for this, add voting power @@ -205,11 +232,12 @@ where } } } else { - return Err( - storage_api::Error::SimpleMessage( - "Unexpected vote type", - ), - ); + return ProposalResult { + + result: TallyResult::Failed(format!("Unexpected vote type. Expected: PGFCouncil, Found: {}", validator_vote)), + total_voting_power: total_stake, + total_yay_power: 0, + total_nay_power: 0} } } None => { @@ -223,9 +251,11 @@ where } } } else { - return Err(storage_api::Error::SimpleMessage( - "Unexpected vote type", - )); + return ProposalResult{ + result: TallyResult::Failed(format!("Unexpected vote type. Expected: PGFCouncil, Found: {}", delegator_vote)), + total_voting_power: total_stake, + total_yay_power: 0, + total_nay_power: 0} } } } @@ -249,13 +279,19 @@ where { *power -= vote_power; } else { - return Err(storage_api::Error::SimpleMessage("Expected PGF vote was not in tally")); + return ProposalResult{ + result: TallyResult::Failed(format!("Expected PGF vote {:?} was not in tally", vote)), + total_voting_power: total_stake, + total_yay_power: 0, + total_nay_power: 0} } } } else { - return Err(storage_api::Error::SimpleMessage( - "Unexpected vote type", - )); + return ProposalResult{ + result: TallyResult::Failed(format!("Unexpected vote type. Expected: PGFCouncil, Found: {}", validator_vote)), + total_voting_power: total_stake, + total_yay_power: 0, + total_nay_power: 0} } } } @@ -267,16 +303,26 @@ where .iter() .fold(0, |acc, (_, vote_power)| acc + vote_power); - if total_yay_voted_power >= 1 / 3 * total_stake { - // Select the winner council based on simple majority - Ok(Tally::PGFCouncil( - total_yay_staked_tokens + match total_yay_voted_power.checked_mul(3) { + Some(v) if v < total_stake => ProposalResult{ + result: TallyResult::Rejected, + total_voting_power: total_stake, + total_yay_power: total_yay_voted_power, + total_nay_power: 0}, + _ => { + // Select the winner council based on simple majority + let council = total_yay_staked_tokens .into_iter() .max_by(|a, b| a.1.cmp(&b.1)) - .map_or(None, |(vote, _)| Some(vote.to_owned())), - )) - } else { - Ok(Tally::PGFCouncil(None)) + .map(|(vote, _)| vote.to_owned()) + .unwrap(); // Cannot be None at this point + + ProposalResult{ + result: TallyResult::Passed(Tally::PGFCouncil(council)), + total_voting_power: total_stake, + total_yay_power: total_yay_voted_power, + total_nay_power: 0} + } } } } From 801b40c262390deafae91ab78564d66474d6e9cd Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Thu, 19 Jan 2023 18:16:57 +0100 Subject: [PATCH 09/49] Refactors governance `Votes` --- apps/src/lib/client/rpc.rs | 69 +++------- .../src/ledger/native_vp/governance/utils.rs | 130 +++++++----------- 2 files changed, 73 insertions(+), 126 deletions(-) diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 8cc7a4c5fe..24b53bf641 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -2237,11 +2237,7 @@ pub async fn get_proposal_votes( let mut yay_validators: HashMap = HashMap::new(); - let mut yay_delegators: HashMap< - Address, - HashMap, - > = HashMap::new(); - let mut nay_delegators: HashMap< + let mut delegators: HashMap< Address, HashMap, > = HashMap::new(); @@ -2273,21 +2269,11 @@ pub async fn get_proposal_votes( ) .await; if let Some(amount) = delegator_token_amount { - if vote.is_yay() { - let entry = - yay_delegators.entry(voter_address).or_default(); - entry.insert( - validator_address, - (VotePower::from(amount), vote), - ); - } else { - let entry = - nay_delegators.entry(voter_address).or_default(); - entry.insert( - validator_address, - (VotePower::from(amount), vote), - ); - } + let entry = delegators.entry(voter_address).or_default(); + entry.insert( + validator_address, + (VotePower::from(amount), vote), + ); } } } @@ -2295,8 +2281,7 @@ pub async fn get_proposal_votes( Votes { yay_validators, - yay_delegators, - nay_delegators, + delegators, } } @@ -2311,11 +2296,7 @@ pub async fn get_proposal_offline_votes( let mut yay_validators: HashMap = HashMap::new(); - let mut yay_delegators: HashMap< - Address, - HashMap, - > = HashMap::new(); - let mut nay_delegators: HashMap< + let mut delegators: HashMap< Address, HashMap, > = HashMap::new(); @@ -2393,26 +2374,17 @@ pub async fn get_proposal_offline_votes( - delta.slashed_amount.unwrap_or_default(); } } - if proposal_vote.vote.is_yay() { - let entry = yay_delegators - .entry(proposal_vote.address.clone()) - .or_default(); - entry.insert( - validator, - ( - VotePower::from(delegated_amount), - ProposalVote::Yay(VoteType::Default), - ), - ); - } else { - let entry = nay_delegators - .entry(proposal_vote.address.clone()) - .or_default(); - entry.insert( - validator, - (VotePower::from(delegated_amount), ProposalVote::Nay), - ); - } + + let entry = delegators + .entry(proposal_vote.address.clone()) + .or_default(); + entry.insert( + validator, + ( + VotePower::from(delegated_amount), + proposal_vote.vote.clone(), + ), + ); } // let key = pos::bonds_for_source_prefix(&proposal_vote.address); @@ -2491,8 +2463,7 @@ pub async fn get_proposal_offline_votes( Votes { yay_validators, - yay_delegators, - nay_delegators, + delegators, } } diff --git a/shared/src/ledger/native_vp/governance/utils.rs b/shared/src/ledger/native_vp/governance/utils.rs index 3ca7251d05..823ccf9d4e 100644 --- a/shared/src/ledger/native_vp/governance/utils.rs +++ b/shared/src/ledger/native_vp/governance/utils.rs @@ -26,14 +26,10 @@ use crate::types::token; pub struct Votes { /// Map from validators who votes yay to their total stake amount pub yay_validators: HashMap, - /// Map from delegation who votes yay to their bond amount - pub yay_delegators: - HashMap>, - /// Map from delegation who votes nay to their bond amount - pub nay_delegators: + /// Map from delegation votes to their bond amount + pub delegators: HashMap>, } -//FIXME: since I attach the vote, can I use only two field, one for the validators and one for the delegators? /// Proposal errors #[derive(Error, Debug)] @@ -90,8 +86,8 @@ pub fn compute_tally( ) -> ProposalResult { let Votes { yay_validators, - yay_delegators, - nay_delegators, + delegators, + } = votes; match proposal_type { @@ -113,45 +109,39 @@ pub fn compute_tally( } } - // YAY: Add delegator amount whose validator didn't vote / voted nay - for (_, vote_map) in yay_delegators.iter() { + for (_, vote_map) in delegators.iter() { for (validator_address, (vote_power, delegator_vote)) in vote_map.iter() { - if let ProposalVote::Yay(VoteType::Default) = delegator_vote - { - if !yay_validators.contains_key(validator_address) { + match delegator_vote { + + ProposalVote::Yay(VoteType::Default) => { + + if !yay_validators.contains_key(validator_address) { + // YAY: Add delegator amount whose validator didn't vote / voted nay total_yay_staked_tokens += vote_power; } - } else { - return ProposalResult { - result: TallyResult::Failed(format!("Unexpected vote type. Expected: Default, Found: {}", delegator_vote)), - total_voting_power: total_stake, - total_yay_power: 0, - total_nay_power: 0} - } - } - } - - // NAY: Remove delegator amount whose validator validator vote yay - for (_, vote_map) in nay_delegators.iter() { - for (validator_address, (vote_power, delegator_vote)) in - vote_map.iter() - { - if let ProposalVote::Nay = delegator_vote { + } + ProposalVote::Nay => { + + // NAY: Remove delegator amount whose validator validator vote yay + if yay_validators.contains_key(validator_address) { total_yay_staked_tokens -= vote_power; } - } else { - return ProposalResult { + } + + _ => + return ProposalResult { result: TallyResult::Failed(format!("Unexpected vote type. Expected: Default, Found: {}", delegator_vote)), total_voting_power: total_stake, total_yay_power: 0, - total_nay_power: 0} - } - } + total_nay_power: 0}} + + } } + // Proposal passes if 2/3 of total voting power voted Yay if total_yay_staked_tokens >= 2 / 3 * total_stake { ProposalResult { @@ -192,15 +182,14 @@ pub fn compute_tally( // YAY: Add delegator amount whose validator didn't vote / voted nay or adjust voting power // if delegator voted yay with a different memo - for (_, vote_map) in yay_delegators.iter() { + for (_, vote_map) in delegators.iter() { for (validator_address, (vote_power, delegator_vote)) in vote_map.iter() { - if let ProposalVote::Yay(VoteType::PGFCouncil( - delegator_votes, - )) = delegator_vote - { - match yay_validators.get(validator_address) { + match delegator_vote { + ProposalVote::Yay(VoteType::PGFCouncil(delegator_votes)) => { + + match yay_validators.get(validator_address) { Some((_, validator_vote)) => { if let ProposalVote::Yay( VoteType::PGFCouncil(validator_votes), @@ -250,19 +239,13 @@ pub fn compute_tally( } } } - } else { - return ProposalResult{ - result: TallyResult::Failed(format!("Unexpected vote type. Expected: PGFCouncil, Found: {}", delegator_vote)), - total_voting_power: total_stake, - total_yay_power: 0, - total_nay_power: 0} - } - } - } + }, + ProposalVote::Nay => { + + - // NAY: Remove delegator amount whose validator voted yay - for (_, vote_map) in nay_delegators.iter() { - for (validator_address, (vote_power, _delegator_vote)) in + +for (validator_address, (vote_power, _delegator_vote)) in vote_map.iter() { if yay_validators.contains_key(validator_address) { @@ -296,8 +279,21 @@ pub fn compute_tally( } } } - } + }, + _ => +return ProposalResult { + + result: TallyResult::Failed(format!("Unexpected vote type. Expected: PGFCouncil, Found: {}", delegator_vote)), + total_voting_power: total_stake, + total_yay_power: 0, + total_nay_power: 0} + } + }} + + + + // At least 1/3 of the total voting power must vote Yay let total_yay_voted_power = total_yay_staked_tokens .iter() @@ -346,11 +342,7 @@ where storage_api::iter_prefix::(storage, &vote_prefix_key)?; let mut yay_validators = HashMap::new(); - let mut yay_delegators: HashMap< - Address, - HashMap, - > = HashMap::new(); - let mut nay_delegators: HashMap< + let mut delegators: HashMap< Address, HashMap, > = HashMap::new(); @@ -386,23 +378,8 @@ where .1; if amount != token::Amount::default() { - if vote.is_yay() { - let entry = yay_delegators - .entry(voter_address.to_owned()) - .or_default(); - entry.insert( - validator.to_owned(), - (VotePower::from(amount), vote), - ); - } else { - let entry = nay_delegators - .entry(voter_address.to_owned()) - .or_default(); - entry.insert( - validator.to_owned(), - (VotePower::from(amount), vote), - ); - } + let entry = delegators.entry(voter_address.to_owned()).or_default(); + entry.insert(validator.to_owned(), (VotePower::from(amount), vote)); } } None => continue, @@ -415,8 +392,7 @@ where Ok(Votes { yay_validators, - yay_delegators, - nay_delegators, + delegators, }) } From 3818805c14f9cfbdbd16df216af100b055098ae1 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Fri, 20 Jan 2023 16:29:02 +0100 Subject: [PATCH 10/49] Clippy + fmt --- apps/src/lib/client/tx.rs | 2 +- apps/src/lib/node/ledger/shell/governance.rs | 2 +- core/src/ledger/governance/storage.rs | 110 ++++-- core/src/types/governance.rs | 6 +- core/src/types/transaction/governance.rs | 15 +- .../src/ledger/native_vp/governance/utils.rs | 323 ++++++++++-------- 6 files changed, 266 insertions(+), 192 deletions(-) diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 678ed91e4c..60c00f62df 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -2096,7 +2096,7 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { let tx_data = VoteProposalData { id: proposal_id, - vote: vote, + vote, voter: voter_address, delegations: delegations.into_iter().collect(), }; diff --git a/apps/src/lib/node/ledger/shell/governance.rs b/apps/src/lib/node/ledger/shell/governance.rs index 10c24bff7b..5a16a6cfad 100644 --- a/apps/src/lib/node/ledger/shell/governance.rs +++ b/apps/src/lib/node/ledger/shell/governance.rs @@ -209,7 +209,7 @@ where } } Tally::PGFCouncil(_council) => { - //TODO: implement when PGF is in place + // TODO: implement when PGF is in place todo!(); } } diff --git a/core/src/ledger/governance/storage.rs b/core/src/ledger/governance/storage.rs index 54f9ca6d88..e00c4be678 100644 --- a/core/src/ledger/governance/storage.rs +++ b/core/src/ledger/governance/storage.rs @@ -31,10 +31,16 @@ pub fn is_governance_key(key: &Key) -> bool { /// Check if a key is a vote key pub fn is_vote_key(key: &Key) -> bool { match &key.segments[..] { - [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix), DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(vote), DbKeySeg::AddressSeg(_validator_address), DbKeySeg::AddressSeg(_address)] - if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && vote == PROPOSAL_VOTE => + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::StringSeg(id), + DbKeySeg::StringSeg(vote), + DbKeySeg::AddressSeg(_validator_address), + DbKeySeg::AddressSeg(_address), + ] if addr == &ADDRESS + && prefix == PROPOSAL_PREFIX + && vote == PROPOSAL_VOTE => { id.parse::().is_ok() } @@ -45,10 +51,14 @@ pub fn is_vote_key(key: &Key) -> bool { /// Check if key is author key pub fn is_author_key(key: &Key) -> bool { match &key.segments[..] { - [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix), DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(author)] - if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && author == PROPOSAL_AUTHOR => + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::StringSeg(id), + DbKeySeg::StringSeg(author), + ] if addr == &ADDRESS + && prefix == PROPOSAL_PREFIX + && author == PROPOSAL_AUTHOR => { id.parse::().is_ok() } @@ -59,10 +69,14 @@ pub fn is_author_key(key: &Key) -> bool { /// Check if key is proposal code key pub fn is_proposal_code_key(key: &Key) -> bool { match &key.segments[..] { - [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix), DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(proposal_code)] - if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && proposal_code == PROPOSAL_CODE => + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::StringSeg(id), + DbKeySeg::StringSeg(proposal_code), + ] if addr == &ADDRESS + && prefix == PROPOSAL_PREFIX + && proposal_code == PROPOSAL_CODE => { id.parse::().is_ok() } @@ -73,10 +87,14 @@ pub fn is_proposal_code_key(key: &Key) -> bool { /// Check if key is grace epoch key pub fn is_grace_epoch_key(key: &Key) -> bool { match &key.segments[..] { - [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix), DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(grace_epoch)] - if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && grace_epoch == PROPOSAL_GRACE_EPOCH => + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::StringSeg(id), + DbKeySeg::StringSeg(grace_epoch), + ] if addr == &ADDRESS + && prefix == PROPOSAL_PREFIX + && grace_epoch == PROPOSAL_GRACE_EPOCH => { id.parse::().is_ok() } @@ -87,10 +105,14 @@ pub fn is_grace_epoch_key(key: &Key) -> bool { /// Check if key is content key pub fn is_content_key(key: &Key) -> bool { match &key.segments[..] { - [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix), DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(content)] - if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && content == PROPOSAL_CONTENT => + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::StringSeg(id), + DbKeySeg::StringSeg(content), + ] if addr == &ADDRESS + && prefix == PROPOSAL_PREFIX + && content == PROPOSAL_CONTENT => { id.parse::().is_ok() } @@ -101,10 +123,14 @@ pub fn is_content_key(key: &Key) -> bool { /// Check if key is balance key pub fn is_balance_key(key: &Key) -> bool { match &key.segments[..] { - [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix), DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(funds)] - if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && funds == PROPOSAL_FUNDS => + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::StringSeg(id), + DbKeySeg::StringSeg(funds), + ] if addr == &ADDRESS + && prefix == PROPOSAL_PREFIX + && funds == PROPOSAL_FUNDS => { id.parse::().is_ok() } @@ -115,10 +141,14 @@ pub fn is_balance_key(key: &Key) -> bool { /// Check if key is start epoch key pub fn is_start_epoch_key(key: &Key) -> bool { match &key.segments[..] { - [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix), DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(start_epoch)] - if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && start_epoch == PROPOSAL_START_EPOCH => + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::StringSeg(id), + DbKeySeg::StringSeg(start_epoch), + ] if addr == &ADDRESS + && prefix == PROPOSAL_PREFIX + && start_epoch == PROPOSAL_START_EPOCH => { id.parse::().is_ok() } @@ -129,10 +159,14 @@ pub fn is_start_epoch_key(key: &Key) -> bool { /// Check if key is epoch key pub fn is_end_epoch_key(key: &Key) -> bool { match &key.segments[..] { - [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix), DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(end_epoch)] - if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && end_epoch == PROPOSAL_END_EPOCH => + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::StringSeg(id), + DbKeySeg::StringSeg(end_epoch), + ] if addr == &ADDRESS + && prefix == PROPOSAL_PREFIX + && end_epoch == PROPOSAL_END_EPOCH => { id.parse::().is_ok() } @@ -143,10 +177,14 @@ pub fn is_end_epoch_key(key: &Key) -> bool { /// Check if key is proposal type key pub fn is_proposal_type_key(key: &Key) -> bool { match &key.segments[..] { - [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix), DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(proposal_type)] - if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && proposal_type == PROPOSAL_TYPE => + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::StringSeg(id), + DbKeySeg::StringSeg(proposal_type), + ] if addr == &ADDRESS + && prefix == PROPOSAL_PREFIX + && proposal_type == PROPOSAL_TYPE => { id.parse::().is_ok() } diff --git a/core/src/types/governance.rs b/core/src/types/governance.rs index 7c9a003f1d..8aa2e31758 100644 --- a/core/src/types/governance.rs +++ b/core/src/types/governance.rs @@ -13,8 +13,7 @@ use crate::types::hash::Hash; use crate::types::key::common::{self, Signature}; use crate::types::key::SigScheme; use crate::types::storage::Epoch; -use crate::types::token::Amount; -use crate::types::token::SCALE; +use crate::types::token::{Amount, SCALE}; /// Type alias for vote power pub type VotePower = u128; @@ -37,7 +36,8 @@ pub type Council = (Address, Amount); pub enum VoteType { /// A default vote without Memo Default, - /// A vote for the PGF council encoding for the proposed multisig addresses and the budget cap + /// A vote for the PGF council encoding for the proposed multisig addresses + /// and the budget cap PGFCouncil(BTreeSet), } diff --git a/core/src/types/transaction/governance.rs b/core/src/types/transaction/governance.rs index c838cbfb9b..c0c2efb560 100644 --- a/core/src/types/transaction/governance.rs +++ b/core/src/types/transaction/governance.rs @@ -9,7 +9,7 @@ use crate::types::governance::{ }; use crate::types::storage::Epoch; -/// The type of a [`InitProposal`] +/// The type of a Proposal #[derive( Debug, Clone, @@ -39,18 +39,10 @@ impl PartialEq for ProposalType { fn eq(&self, other: &VoteType) -> bool { match self { Self::Default(_) => { - if let VoteType::Default = other { - true - } else { - false - } + matches!(other, VoteType::Default) } Self::PGFCouncil => { - if let VoteType::PGFCouncil(..) = other { - true - } else { - false - } + matches!(other, VoteType::PGFCouncil(..)) } } } @@ -58,6 +50,7 @@ impl PartialEq for ProposalType { impl TryFrom for ProposalType { type Error = ProposalError; + fn try_from(value: governance::ProposalType) -> Result { match value { governance::ProposalType::Default(path) => { diff --git a/shared/src/ledger/native_vp/governance/utils.rs b/shared/src/ledger/native_vp/governance/utils.rs index 823ccf9d4e..72f00323ad 100644 --- a/shared/src/ledger/native_vp/governance/utils.rs +++ b/shared/src/ledger/native_vp/governance/utils.rs @@ -7,7 +7,7 @@ use namada_core::types::governance::ProposalResult; use namada_core::types::transaction::governance::ProposalType; use namada_proof_of_stake::{ bond_amount, read_all_validator_addresses, read_pos_params, - read_validator_stake, + read_validator_stake, }; use thiserror::Error; @@ -87,7 +87,6 @@ pub fn compute_tally( let Votes { yay_validators, delegators, - } = votes; match proposal_type { @@ -99,9 +98,10 @@ pub fn compute_tally( } else { return ProposalResult { result: TallyResult::Failed(format!( - "Unexpected vote type. Expected: Default, Found: {}", - validator_vote - )), + "Unexpected vote type. Expected: Default, Found: \ + {}", + validator_vote + )), total_voting_power: total_stake, total_yay_power: 0, total_nay_power: 0, @@ -114,48 +114,53 @@ pub fn compute_tally( vote_map.iter() { match delegator_vote { - - ProposalVote::Yay(VoteType::Default) => { - - if !yay_validators.contains_key(validator_address) { - // YAY: Add delegator amount whose validator didn't vote / voted nay - total_yay_staked_tokens += vote_power; + ProposalVote::Yay(VoteType::Default) => { + if !yay_validators.contains_key(validator_address) { + // YAY: Add delegator amount whose validator + // didn't vote / voted nay + total_yay_staked_tokens += vote_power; + } } - } ProposalVote::Nay => { - - // NAY: Remove delegator amount whose validator validator vote yay - - if yay_validators.contains_key(validator_address) { - total_yay_staked_tokens -= vote_power; - } + // NAY: Remove delegator amount whose validator + // validator vote yay + + if yay_validators.contains_key(validator_address) { + total_yay_staked_tokens -= vote_power; + } } - - _ => - return ProposalResult { - result: TallyResult::Failed(format!("Unexpected vote type. Expected: Default, Found: {}", delegator_vote)), - total_voting_power: total_stake, - total_yay_power: 0, - total_nay_power: 0}} - } + _ => { + return ProposalResult { + result: TallyResult::Failed(format!( + "Unexpected vote type. Expected: Default, \ + Found: {}", + delegator_vote + )), + total_voting_power: total_stake, + total_yay_power: 0, + total_nay_power: 0, + }; + } + } + } } - // Proposal passes if 2/3 of total voting power voted Yay - if total_yay_staked_tokens >= 2 / 3 * total_stake { - ProposalResult { - result: TallyResult::Passed(Tally::Default), - total_voting_power: total_stake, - total_yay_power: total_yay_staked_tokens, - total_nay_power: 0} + if total_yay_staked_tokens >= (total_stake / 3) * 2 { + ProposalResult { + result: TallyResult::Passed(Tally::Default), + total_voting_power: total_stake, + total_yay_power: total_yay_staked_tokens, + total_nay_power: 0, + } } else { - ProposalResult{ - result: TallyResult::Rejected, - total_voting_power: total_stake, - total_yay_power: total_yay_staked_tokens, - total_nay_power: 0 - } + ProposalResult { + result: TallyResult::Rejected, + total_voting_power: total_stake, + total_yay_power: total_yay_staked_tokens, + total_nay_power: 0, + } } } ProposalType::PGFCouncil => { @@ -170,141 +175,173 @@ pub fn compute_tally( } } else { return ProposalResult { - result: TallyResult::Failed(format!( - "Unexpected vote type. Expected: PGFCouncil, Found: {}", - validator_vote - )), - total_voting_power: total_stake, - total_yay_power: 0, - total_nay_power: 0} + result: TallyResult::Failed(format!( + "Unexpected vote type. Expected: PGFCouncil, \ + Found: {}", + validator_vote + )), + total_voting_power: total_stake, + total_yay_power: 0, + total_nay_power: 0, + }; } } - // YAY: Add delegator amount whose validator didn't vote / voted nay or adjust voting power - // if delegator voted yay with a different memo + // YAY: Add delegator amount whose validator didn't vote / voted nay + // or adjust voting power if delegator voted yay with a + // different memo for (_, vote_map) in delegators.iter() { for (validator_address, (vote_power, delegator_vote)) in vote_map.iter() { match delegator_vote { - ProposalVote::Yay(VoteType::PGFCouncil(delegator_votes)) => { - - match yay_validators.get(validator_address) { - Some((_, validator_vote)) => { - if let ProposalVote::Yay( - VoteType::PGFCouncil(validator_votes), - ) = validator_vote - { - for vote in validator_votes - .symmetric_difference(delegator_votes) + ProposalVote::Yay(VoteType::PGFCouncil( + delegator_votes, + )) => { + match yay_validators.get(validator_address) { + Some((_, validator_vote)) => { + if let ProposalVote::Yay( + VoteType::PGFCouncil(validator_votes), + ) = validator_vote { - if validator_votes.contains(vote) { - // Delegator didn't vote for this, reduce voting power - if let Some(power) = - total_yay_staked_tokens - .get_mut(vote) - { - *power -= vote_power; - } else { - return ProposalResult { + for vote in validator_votes + .symmetric_difference( + delegator_votes, + ) + { + if validator_votes.contains(vote) { + // Delegator didn't vote for + // this, reduce voting power + if let Some(power) = + total_yay_staked_tokens + .get_mut(vote) + { + *power -= vote_power; + } else { + return ProposalResult { result: TallyResult::Failed(format!("Expected PGF vote {:?} was not in tally", vote)), total_voting_power: total_stake, total_yay_power: 0, - total_nay_power: 0} - + total_nay_power: 0}; + } + } else { + // Validator didn't vote for + // this, add voting power + *total_yay_staked_tokens + .entry(vote) + .or_insert(0) += vote_power; } - } else { - // Validator didn't vote for this, add voting power - *total_yay_staked_tokens - .entry(vote) - .or_insert(0) += vote_power; } - } - } else { - return ProposalResult { - - result: TallyResult::Failed(format!("Unexpected vote type. Expected: PGFCouncil, Found: {}", validator_vote)), + } else { + return ProposalResult { + result: TallyResult::Failed( + format!( + "Unexpected vote type. \ + Expected: PGFCouncil, \ + Found: {}", + validator_vote + ), + ), total_voting_power: total_stake, total_yay_power: 0, - total_nay_power: 0} + total_nay_power: 0, + }; + } } - } - None => { - // Validator didn't vote or voted nay, add delegator vote + None => { + // Validator didn't vote or voted nay, add + // delegator vote - for vote in delegator_votes { - *total_yay_staked_tokens - .entry(vote) - .or_insert(0) += vote_power; + for vote in delegator_votes { + *total_yay_staked_tokens + .entry(vote) + .or_insert(0) += vote_power; + } } } } - }, ProposalVote::Nay => { - - - - -for (validator_address, (vote_power, _delegator_vote)) in - vote_map.iter() - { - if yay_validators.contains_key(validator_address) { - for (_, validator_vote) in - yay_validators.get(validator_address) - { - if let ProposalVote::Yay(VoteType::PGFCouncil( - votes, - )) = validator_vote + for ( + validator_address, + (vote_power, _delegator_vote), + ) in vote_map.iter() { - for vote in votes { - if let Some(power) = - total_yay_staked_tokens.get_mut(vote) + if let Some((_, validator_vote)) = + yay_validators.get(validator_address) + { + if let ProposalVote::Yay( + VoteType::PGFCouncil(votes), + ) = validator_vote { - *power -= vote_power; + for vote in votes { + if let Some(power) = + total_yay_staked_tokens + .get_mut(vote) + { + *power -= vote_power; + } else { + return ProposalResult { + result: TallyResult::Failed( + format!( + "Expected PGF \ + vote {:?} was \ + not in tally", + vote + ), + ), + total_voting_power: + total_stake, + total_yay_power: 0, + total_nay_power: 0, + }; + } + } } else { - return ProposalResult{ - result: TallyResult::Failed(format!("Expected PGF vote {:?} was not in tally", vote)), + return ProposalResult { + result: TallyResult::Failed( + format!( + "Unexpected vote type. \ + Expected: PGFCouncil, \ + Found: {}", + validator_vote + ), + ), total_voting_power: total_stake, total_yay_power: 0, - total_nay_power: 0} + total_nay_power: 0, + }; } } - } else { - return ProposalResult{ - result: TallyResult::Failed(format!("Unexpected vote type. Expected: PGFCouncil, Found: {}", validator_vote)), + } + } + _ => { + return ProposalResult { + result: TallyResult::Failed(format!( + "Unexpected vote type. Expected: \ + PGFCouncil, Found: {}", + delegator_vote + )), total_voting_power: total_stake, total_yay_power: 0, - total_nay_power: 0} - } + total_nay_power: 0, + }; } } } - }, - _ => -return ProposalResult { - - result: TallyResult::Failed(format!("Unexpected vote type. Expected: PGFCouncil, Found: {}", delegator_vote)), - total_voting_power: total_stake, - total_yay_power: 0, - total_nay_power: 0} - } - }} - - + } - - // At least 1/3 of the total voting power must vote Yay let total_yay_voted_power = total_yay_staked_tokens .iter() .fold(0, |acc, (_, vote_power)| acc + vote_power); match total_yay_voted_power.checked_mul(3) { - Some(v) if v < total_stake => ProposalResult{ - result: TallyResult::Rejected, - total_voting_power: total_stake, - total_yay_power: total_yay_voted_power, - total_nay_power: 0}, + Some(v) if v < total_stake => ProposalResult { + result: TallyResult::Rejected, + total_voting_power: total_stake, + total_yay_power: total_yay_voted_power, + total_nay_power: 0, + }, _ => { // Select the winner council based on simple majority let council = total_yay_staked_tokens @@ -313,11 +350,12 @@ return ProposalResult { .map(|(vote, _)| vote.to_owned()) .unwrap(); // Cannot be None at this point - ProposalResult{ - result: TallyResult::Passed(Tally::PGFCouncil(council)), - total_voting_power: total_stake, - total_yay_power: total_yay_voted_power, - total_nay_power: 0} + ProposalResult { + result: TallyResult::Passed(Tally::PGFCouncil(council)), + total_voting_power: total_stake, + total_yay_power: total_yay_voted_power, + total_nay_power: 0, + } } } } @@ -378,8 +416,13 @@ where .1; if amount != token::Amount::default() { - let entry = delegators.entry(voter_address.to_owned()).or_default(); - entry.insert(validator.to_owned(), (VotePower::from(amount), vote)); + let entry = delegators + .entry(voter_address.to_owned()) + .or_default(); + entry.insert( + validator.to_owned(), + (VotePower::from(amount), vote), + ); } } None => continue, From f7fa9d87f6c4613d54627544d443ec124143dab5 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Tue, 24 Jan 2023 18:34:06 +0100 Subject: [PATCH 11/49] Refactors cli governance vote. Improves custom proposal vote validation --- apps/src/lib/cli.rs | 21 +---- apps/src/lib/client/tx.rs | 88 ++++++++++++------- core/src/types/governance.rs | 61 ++++++++++++- shared/src/ledger/native_vp/governance/mod.rs | 24 ++++- 4 files changed, 141 insertions(+), 53 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index bd928f1b2f..3d703356cc 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1569,6 +1569,7 @@ pub mod args { use namada::ibc::core::ics24_host::identifier::{ChannelId, PortId}; use namada::types::address::Address; use namada::types::chain::{ChainId, ChainIdPrefix}; + use namada::types::governance::ProposalVote; use namada::types::key::*; use namada::types::masp::MaspValue; use namada::types::storage::{self, Epoch}; @@ -1662,8 +1663,7 @@ pub mod args { const PUBLIC_KEY: Arg = arg("public-key"); const PROPOSAL_ID: Arg = arg("proposal-id"); const PROPOSAL_ID_OPT: ArgOpt = arg_opt("proposal-id"); - const PROPOSAL_VOTE: Arg = arg("vote"); - const PROPOSAL_VOTE_MEMO_OPT: ArgOpt = arg_opt("memo"); + const PROPOSAL_VOTE: Arg = arg("vote"); const RAW_ADDRESS: Arg
= arg("address"); const RAW_ADDRESS_OPT: ArgOpt
= RAW_ADDRESS.opt(); const RAW_PUBLIC_KEY_OPT: ArgOpt = arg_opt("public-key"); @@ -2292,9 +2292,7 @@ pub mod args { /// Proposal id pub proposal_id: Option, /// The vote - pub vote: String, - /// The optional vote memo path - pub memo: Option, + pub vote: ProposalVote, /// Flag if proposal vote should be run offline pub offline: bool, /// The proposal file path @@ -2306,7 +2304,6 @@ pub mod args { let tx = Tx::parse(matches); let proposal_id = PROPOSAL_ID_OPT.parse(matches); let vote = PROPOSAL_VOTE.parse(matches); - let memo = PROPOSAL_VOTE_MEMO_OPT.parse(matches); let offline = PROPOSAL_OFFLINE.parse(matches); let proposal_data = DATA_PATH_OPT.parse(matches); @@ -2314,7 +2311,6 @@ pub mod args { tx, proposal_id, vote, - memo, offline, proposal_data, } @@ -2334,16 +2330,7 @@ pub mod args { .arg( PROPOSAL_VOTE .def() - .about("The vote for the proposal. Either yay or nay."), - ) - .arg( - PROPOSAL_VOTE_MEMO_OPT - .def() - .about("The optional vote memo.") - .conflicts_with_all(&[ - PROPOSAL_OFFLINE.name, - DATA_PATH_OPT.name, - ]), + .about("The vote for the proposal. Either yay or nay (with optional memo). For PGF vote: yay $council1 $cap1 $council2 $cap2 ..."), ) .arg( PROPOSAL_OFFLINE diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 60c00f62df..be85519063 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -54,7 +54,7 @@ use namada::types::token::{ Transfer, HEAD_TX_KEY, PIN_KEY_PREFIX, TX_KEY_PREFIX, }; use namada::types::transaction::governance::{ - InitProposalData, VoteProposalData, + InitProposalData, ProposalType, VoteProposalData, }; use namada::types::transaction::{pos, InitAccount, InitValidator, UpdateVp}; use namada::types::{storage, token}; @@ -1965,6 +1965,12 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { }; if args.offline { + if !args.vote.is_default_vote() { + eprintln!( + "Wrong vote type for offline proposal. Just vote yay or nay!" + ); + safe_exit(1); + } let signer = ctx.get(signer); let proposal_file_path = args.proposal_data.expect("Proposal file should exist."); @@ -1990,17 +1996,12 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { ) .await; - let vote = match args.vote.as_str() { - "yay" => ProposalVote::Yay(VoteType::Default), - "nay" => ProposalVote::Nay, - _ => { - eprintln!("Vote must be either yay or nay"); - safe_exit(1); - } - }; - - let offline_vote = - OfflineVote::new(&proposal, vote, signer.clone(), &signing_key); + let offline_vote = OfflineVote::new( + &proposal, + args.vote, + signer.clone(), + &signing_key, + ); let proposal_vote_filename = proposal_file_path .parent() @@ -2036,6 +2037,48 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { ) .await; + // Check vote type and memo + let proposal_type_key = gov_storage::get_proposal_type_key(proposal_id); + let proposal_type: ProposalType = + rpc::query_storage_value(&client, &proposal_type_key) + .await + .expect(&format!( + "Didn't find type of proposal id {} in storage", + proposal_id + )); + + if let ProposalVote::Yay(ref vote_type) = args.vote { + if &proposal_type != vote_type { + eprintln!( + "Expected vote of type {}, found {}", + proposal_type, args.vote + ); + safe_exit(1); + } else if let VoteType::PGFCouncil(set) = vote_type { + // Check that addresses proposed as council are established and are present in storage + for (address, _) in set { + match address { + Address::Established(_) => { + let vp_key = Key::validity_predicate(&address); + if !rpc::query_has_storage_key(&client, &vp_key) + .await + { + eprintln!("Proposed PGF council {} cannot be found in storage", address); + safe_exit(1); + } + } + _ => { + eprintln!( + "PGF council vote contains a non-established address: {}", + address + ); + safe_exit(1); + } + } + } + } + } + match proposal_start_epoch { Some(epoch) => { if current_epoch < epoch { @@ -2053,23 +2096,6 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { rpc::get_delegators_delegation(&client, &voter_address) .await; - let vote = match args.vote.as_str() { - "yay" => match args.memo { - Some(path) => { - let memo_file = std::fs::File::open(path) - .expect("Error while opening vote memo file"); - let vote = serde_json::from_reader(memo_file) - .expect("Could not deserialize vote memo"); - ProposalVote::Yay(vote) - } - None => ProposalVote::Yay(VoteType::Default), - }, - "nay" => ProposalVote::Nay, - _ => { - eprintln!("Vote must be either yay or nay"); - safe_exit(1); - } - }; // Optimize by quering if a vote from a validator // is equal to ours. If so, we can avoid voting, but ONLY if we // are voting in the last third of the voting @@ -2089,14 +2115,14 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { &client, delegations, proposal_id, - &vote, + &args.vote, ) .await; } let tx_data = VoteProposalData { id: proposal_id, - vote, + vote: args.vote, voter: voter_address, delegations: delegations.into_iter().collect(), }; diff --git a/core/src/types/governance.rs b/core/src/types/governance.rs index 8aa2e31758..58b66fd480 100644 --- a/core/src/types/governance.rs +++ b/core/src/types/governance.rs @@ -2,6 +2,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::fmt::{self, Display}; +use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; use rust_decimal::Decimal; @@ -36,8 +37,7 @@ pub type Council = (Address, Amount); pub enum VoteType { /// A default vote without Memo Default, - /// A vote for the PGF council encoding for the proposed multisig addresses - /// and the budget cap + /// A vote for the PGF council PGFCouncil(BTreeSet), } @@ -60,6 +60,54 @@ pub enum ProposalVote { Nay, } +impl FromStr for ProposalVote { + type Err = String; + + fn from_str(s: &str) -> Result { + let splits = s.trim().split_ascii_whitespace(); + let mut iter = splits.clone().into_iter(); + + match iter.next() { + Some(t) => match t { + "yay" => { + let mut set = BTreeSet::new(); + let address_iter = + splits.clone().into_iter().skip(1).step_by(2); + let cap_iter = splits.into_iter().skip(2).step_by(2); + for (address, cap) in + address_iter.zip(cap_iter).map(|(addr, cap)| { + ( + addr.to_owned().parse().map_err( + |e: crate::types::address::DecodeError| { + e.to_string() + }, + ), + cap.parse::().map_err(|e| e.to_string()), + ) + }) + { + set.insert((address?, cap?.into())); + } + + if set.is_empty() { + Ok(Self::Yay(VoteType::Default)) + } else { + Ok(Self::Yay(VoteType::PGFCouncil(set))) + } + } + "nay" => match iter.next() { + Some(t) => { + Err(format!("Unexpected {} argument for Nay vote", t)) + } + None => Ok(Self::Nay), + }, + _ => Err("Expected either yay or nay".to_string()), + }, + None => Err("Expected either yay or nay".to_string()), + } + } +} + impl ProposalVote { /// Check if a vote is yay pub fn is_yay(&self) -> bool { @@ -68,6 +116,15 @@ impl ProposalVote { ProposalVote::Nay => false, } } + + /// Check if vote is of type default + pub fn is_default_vote(&self) -> bool { + match self { + ProposalVote::Yay(VoteType::Default) => true, + ProposalVote::Nay => true, + _ => false, + } + } } impl Display for ProposalVote { diff --git a/shared/src/ledger/native_vp/governance/mod.rs b/shared/src/ledger/native_vp/governance/mod.rs index bbab531e3c..f4cf171ec9 100644 --- a/shared/src/ledger/native_vp/governance/mod.rs +++ b/shared/src/ledger/native_vp/governance/mod.rs @@ -201,7 +201,26 @@ where Some(pre_voting_start_epoch), Some(pre_voting_end_epoch), ) => { - let is_valid_vote_type = proposal_type.eq(&vote_type); + if proposal_type != vote_type { + return Ok(false); + } + + // Vote type specific checks + if let VoteType::PGFCouncil(set) = vote_type { + // Check that all the addresses are established + for (address, _) in set { + match address { + Address::Established(_) => { + // Check that established address exists in storage + let vp_key = Key::validity_predicate(&address); + if !self.ctx.has_key_pre(&vp_key)? { + return Ok(false); + } + } + _ => return Ok(false), + } + } + } let is_delegator = self .is_delegator( @@ -228,8 +247,7 @@ where pre_voting_end_epoch, ); - let is_valid = is_valid_vote_type - && pre_counter > proposal_id + let is_valid = pre_counter > proposal_id && current_epoch >= pre_voting_start_epoch && current_epoch <= pre_voting_end_epoch && (is_delegator From 25dcfbb5333c6c36b554551faaab8c23da859ed5 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Wed, 25 Jan 2023 17:33:56 +0100 Subject: [PATCH 12/49] Improves governance vote command help --- apps/src/lib/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 3d703356cc..c0704eb492 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2330,7 +2330,7 @@ pub mod args { .arg( PROPOSAL_VOTE .def() - .about("The vote for the proposal. Either yay or nay (with optional memo). For PGF vote: yay $council1 $cap1 $council2 $cap2 ..."), + .about("The vote for the proposal. Either yay or nay (with optional memo).\nDefault vote: yay | nay\nPGF vote: yay $council1 $cap1 $council2 $cap2 ... | nay"), ) .arg( PROPOSAL_OFFLINE From e40e44cc5f79857a721f7ef6e1a5e33014ef7018 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Wed, 25 Jan 2023 18:16:00 +0100 Subject: [PATCH 13/49] Fixes governance VP --- shared/src/ledger/native_vp/governance/mod.rs | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/shared/src/ledger/native_vp/governance/mod.rs b/shared/src/ledger/native_vp/governance/mod.rs index f4cf171ec9..6c6712ef3c 100644 --- a/shared/src/ledger/native_vp/governance/mod.rs +++ b/shared/src/ledger/native_vp/governance/mod.rs @@ -7,7 +7,7 @@ use std::collections::BTreeSet; use namada_core::ledger::governance::storage as gov_storage; use namada_core::ledger::storage; use namada_core::ledger::vp_env::VpEnv; -use namada_core::types::governance::VoteType; +use namada_core::types::governance::{ProposalVote, VoteType}; use namada_core::types::transaction::governance::ProposalType; use thiserror::Error; use utils::is_valid_validator_voting_period; @@ -176,14 +176,14 @@ where let voter = gov_storage::get_voter_address(key); let delegation_address = gov_storage::get_vote_delegation_address(key); - let vote_type: Option = self.ctx.read_post(key)?; + let vote: Option = self.ctx.read_post(key)?; let proposal_type_key = gov_storage::get_proposal_type_key(proposal_id); let proposal_type: Option = self.ctx.read_pre(&proposal_type_key)?; match ( pre_counter, - vote_type, + vote, proposal_type, voter, delegation_address, @@ -193,7 +193,7 @@ where ) { ( Some(pre_counter), - Some(vote_type), + Some(vote), Some(proposal_type), Some(voter_address), Some(delegation_address), @@ -201,23 +201,27 @@ where Some(pre_voting_start_epoch), Some(pre_voting_end_epoch), ) => { - if proposal_type != vote_type { - return Ok(false); - } + if let ProposalVote::Yay(vote_type) = vote { + if proposal_type != vote_type { + //FIXME: technically this is needed only for Yay votes + return Ok(false); + } - // Vote type specific checks - if let VoteType::PGFCouncil(set) = vote_type { - // Check that all the addresses are established - for (address, _) in set { - match address { - Address::Established(_) => { - // Check that established address exists in storage - let vp_key = Key::validity_predicate(&address); - if !self.ctx.has_key_pre(&vp_key)? { - return Ok(false); + // Vote type specific checks + if let VoteType::PGFCouncil(set) = vote_type { + // Check that all the addresses are established + for (address, _) in set { + match address { + Address::Established(_) => { + // Check that established address exists in storage + let vp_key = + Key::validity_predicate(&address); + if !self.ctx.has_key_pre(&vp_key)? { + return Ok(false); + } } + _ => return Ok(false), } - _ => return Ok(false), } } } From c65dc2def799c93ed2af63cababc9b10c3996b0f Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Wed, 25 Jan 2023 18:16:28 +0100 Subject: [PATCH 14/49] Pgf proposal e2e test --- tests/src/e2e/ledger_tests.rs | 262 +++++++++++++++++++++++++++++++++- 1 file changed, 256 insertions(+), 6 deletions(-) diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 9437103546..9f83e3c8ef 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -20,6 +20,7 @@ use borsh::BorshSerialize; use color_eyre::eyre::Result; use data_encoding::HEXLOWER; use namada::types::address::{btc, eth, masp_rewards, Address}; +use namada::types::governance::ProposalType; use namada::types::storage::Epoch; use namada::types::token; use namada_apps::client::tx::ShieldedContext; @@ -2329,7 +2330,13 @@ fn proposal_submission() -> Result<()> { // 2. Submit valid proposal let albert = find_address(&test, ALBERT)?; - let valid_proposal_json_path = prepare_proposal_data(&test, albert); + let valid_proposal_json_path = prepare_proposal_data( + &test, + albert, + ProposalType::Default(Some( + wasm_abs_path(TX_PROPOSAL_CODE).to_str().unwrap().to_owned(), + )), + ); let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); let submit_proposal_args = vec![ @@ -2429,6 +2436,7 @@ fn proposal_submission() -> Result<()> { "voting_start_epoch": 9999_u64, "voting_end_epoch": 10000_u64, "grace_epoch": 10009_u64, + "type": "Default" } ); let invalid_proposal_json_path = @@ -2634,6 +2642,240 @@ fn proposal_submission() -> Result<()> { Ok(()) } +/// Test submission and vote of a PGF proposal +/// +/// 1 - Sumbit a proposal +/// 2 - Check balance +/// 3 - Vote for the accepted proposal +/// 4 - Check proposal passed +/// 5 - Check funds +#[test] +fn pgf_governance_proposal() -> Result<()> { + let test = setup::network( + |genesis| { + let parameters = ParametersConfig { + epochs_per_year: epochs_per_year_from_min_duration(1), + max_proposal_bytes: Default::default(), + min_num_of_blocks: 1, + max_expected_time_per_block: 1, + ..genesis.parameters + }; + + GenesisConfig { + parameters, + ..genesis + } + }, + None, + )?; + + let namadac_help = vec!["--help"]; + + let mut client = run!(test, Bin::Client, namadac_help, Some(40))?; + client.exp_string("Namada client command line interface.")?; + client.assert_success(); + + // Run the ledger node + let mut ledger = + run_as!(test, Who::Validator(0), Bin::Node, &["ledger"], Some(40))?; + + ledger.exp_string("Namada ledger node started")?; + let _bg_ledger = ledger.background(); + + let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); + + // Delegate some token + let tx_args = vec![ + "bond", + "--validator", + "validator-0", + "--source", + BERTHA, + "--amount", + "900", + "--gas-amount", + "0", + "--gas-limit", + "0", + "--gas-token", + NAM, + "--ledger-address", + &validator_one_rpc, + ]; + let mut client = run!(test, Bin::Client, tx_args, Some(40))?; + client.exp_string("Transaction is valid.")?; + client.assert_success(); + + // 1 - Submit proposal + let albert = find_address(&test, ALBERT)?; + let valid_proposal_json_path = + prepare_proposal_data(&test, albert, ProposalType::PGFCouncil); + let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); + + let submit_proposal_args = vec![ + "init-proposal", + "--data-path", + valid_proposal_json_path.to_str().unwrap(), + "--ledger-address", + &validator_one_rpc, + ]; + let mut client = run!(test, Bin::Client, submit_proposal_args, Some(40))?; + client.exp_string("Transaction is valid.")?; + client.assert_success(); + + // 2 - Query the proposal + let proposal_query_args = vec![ + "query-proposal", + "--proposal-id", + "0", + "--ledger-address", + &validator_one_rpc, + ]; + + let mut client = run!(test, Bin::Client, proposal_query_args, Some(40))?; + client.exp_string("Proposal: 0")?; + client.assert_success(); + + // Query token balance proposal author (submitted funds) + let query_balance_args = vec![ + "balance", + "--owner", + ALBERT, + "--token", + NAM, + "--ledger-address", + &validator_one_rpc, + ]; + + let mut client = run!(test, Bin::Client, query_balance_args, Some(40))?; + client.exp_string("NAM: 999500")?; + client.assert_success(); + + // Query token balance governance + let query_balance_args = vec![ + "balance", + "--owner", + GOVERNANCE_ADDRESS, + "--token", + NAM, + "--ledger-address", + &validator_one_rpc, + ]; + + let mut client = run!(test, Bin::Client, query_balance_args, Some(40))?; + client.exp_string("NAM: 500")?; + client.assert_success(); + + // 3 - Send a yay vote from a validator + let mut epoch = get_epoch(&test, &validator_one_rpc).unwrap(); + while epoch.0 <= 13 { + sleep(1); + epoch = get_epoch(&test, &validator_one_rpc).unwrap(); + } + + let vote = format!("yay {} 1000", find_address(&test, ALBERT)?); + + let submit_proposal_vote = vec![ + "vote-proposal", + "--proposal-id", + "0", + "--vote", + &vote, + "--signer", + "validator-0", + "--ledger-address", + &validator_one_rpc, + ]; + + let mut client = run_as!( + test, + Who::Validator(0), + Bin::Client, + submit_proposal_vote, + Some(15) + )?; + client.exp_string("Transaction is valid.")?; + client.assert_success(); + + // Send vote from delegator + let submit_proposal_vote_delagator = vec![ + "vote-proposal", + "--proposal-id", + "0", + "--vote", + "nay", + "--signer", + BERTHA, + "--ledger-address", + &validator_one_rpc, + ]; + + let mut client = + run!(test, Bin::Client, submit_proposal_vote_delagator, Some(40))?; + client.exp_string("Transaction is valid.")?; + client.assert_success(); + + // 4 - Query the proposal and check the result + let mut epoch = get_epoch(&test, &validator_one_rpc).unwrap(); + while epoch.0 <= 25 { + sleep(1); + epoch = get_epoch(&test, &validator_one_rpc).unwrap(); + } + + let query_proposal = vec![ + "query-proposal-result", + "--proposal-id", + "0", + "--ledger-address", + &validator_one_rpc, + ]; + + let mut client = run!(test, Bin::Client, query_proposal, Some(15))?; + client.exp_string("Result: passed")?; //FIXME: here result is 0 yay votes, tally problem? No both the votes have been rejected + client.assert_success(); + + // FIXME: test not 1/3 of total voting power (vote with just the delegator) + // FIXME: test majority when 1/3 of votes (vote with both BERTHA and validator 0 anche check that validtor 0 won) + + // 12. Wait proposal grace and check proposal author funds + let mut epoch = get_epoch(&test, &validator_one_rpc).unwrap(); + while epoch.0 < 31 { + sleep(1); + epoch = get_epoch(&test, &validator_one_rpc).unwrap(); + } + + let query_balance_args = vec![ + "balance", + "--owner", + ALBERT, + "--token", + NAM, + "--ledger-address", + &validator_one_rpc, + ]; + + let mut client = run!(test, Bin::Client, query_balance_args, Some(30))?; + client.exp_string("NAM: 1000000")?; + client.assert_success(); + + // Check if governance funds are 0 + let query_balance_args = vec![ + "balance", + "--owner", + GOVERNANCE_ADDRESS, + "--token", + NAM, + "--ledger-address", + &validator_one_rpc, + ]; + + let mut client = run!(test, Bin::Client, query_balance_args, Some(30))?; + client.exp_string("NAM: 0")?; + client.assert_success(); + + Ok(()) +} + /// In this test we: /// 1. Run the ledger node /// 2. Create an offline proposal @@ -2692,7 +2934,8 @@ fn proposal_offline() -> Result<()> { "author": albert, "voting_start_epoch": 3_u64, "voting_end_epoch": 9_u64, - "grace_epoch": 18_u64 + "grace_epoch": 18_u64, + "type": "Default" } ); let valid_proposal_json_path = @@ -3372,7 +3615,11 @@ fn implicit_account_reveal_pk() -> Result<()> { Box::new(|source| { // Gen data for proposal tx let source = find_address(&test, source).unwrap(); - let valid_proposal_json_path = prepare_proposal_data(&test, source); + let valid_proposal_json_path = prepare_proposal_data( + &test, + source, + ProposalType::Default(None), + ); vec![ "init-proposal", "--data-path", @@ -3436,8 +3683,11 @@ fn implicit_account_reveal_pk() -> Result<()> { /// Prepare proposal data in the test's temp dir from the given source address. /// This can be submitted with "init-proposal" command. -fn prepare_proposal_data(test: &setup::Test, source: Address) -> PathBuf { - let proposal_code = wasm_abs_path(TX_PROPOSAL_CODE); +fn prepare_proposal_data( + test: &setup::Test, + source: Address, + proposal_type: ProposalType, +) -> PathBuf { let valid_proposal_json = json!( { "content": { @@ -3455,7 +3705,7 @@ fn prepare_proposal_data(test: &setup::Test, source: Address) -> PathBuf { "voting_start_epoch": 12_u64, "voting_end_epoch": 24_u64, "grace_epoch": 30_u64, - "proposal_code_path": proposal_code.to_str().unwrap() + "type": proposal_type } ); let valid_proposal_json_path = From 6506d0d95ae35b93a250648694dea5c08f951a30 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Wed, 25 Jan 2023 19:05:16 +0100 Subject: [PATCH 15/49] Adds pgf e2e test to github workflow --- .github/workflows/scripts/e2e.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/scripts/e2e.json b/.github/workflows/scripts/e2e.json index 8ee62bb236..75671da802 100644 --- a/.github/workflows/scripts/e2e.json +++ b/.github/workflows/scripts/e2e.json @@ -11,6 +11,7 @@ "e2e::ledger_tests::pos_bonds": 19, "e2e::ledger_tests::pos_init_validator": 15, "e2e::ledger_tests::proposal_offline": 15, + "e2e::ledger_tests::pgf_governance_proposal": 35, "e2e::ledger_tests::proposal_submission": 35, "e2e::ledger_tests::run_ledger": 5, "e2e::ledger_tests::run_ledger_load_state_and_reset": 5, From 1fcae4c39116ab5369ed420d2d571a76ffba6f19 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Wed, 25 Jan 2023 19:47:52 +0100 Subject: [PATCH 16/49] Clippy + fmt --- apps/src/lib/cli.rs | 10 +++---- apps/src/lib/client/tx.rs | 28 ++++++++++++------- core/src/types/governance.rs | 11 ++++---- shared/src/ledger/native_vp/governance/mod.rs | 5 ++-- tests/src/e2e/ledger_tests.rs | 3 +- 5 files changed, 33 insertions(+), 24 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index c0704eb492..40005aeaf7 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2327,11 +2327,11 @@ pub mod args { DATA_PATH_OPT.name, ]), ) - .arg( - PROPOSAL_VOTE - .def() - .about("The vote for the proposal. Either yay or nay (with optional memo).\nDefault vote: yay | nay\nPGF vote: yay $council1 $cap1 $council2 $cap2 ... | nay"), - ) + .arg(PROPOSAL_VOTE.def().about( + "The vote for the proposal. Either yay or nay (with \ + optional memo).\nDefault vote: yay | nay\nPGF vote: yay \ + $council1 $cap1 $council2 $cap2 ... | nay", + )) .arg( PROPOSAL_OFFLINE .def() diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index be85519063..daa32bb4d7 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -2042,10 +2042,12 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { let proposal_type: ProposalType = rpc::query_storage_value(&client, &proposal_type_key) .await - .expect(&format!( - "Didn't find type of proposal id {} in storage", - proposal_id - )); + .unwrap_or_else(|| { + panic!( + "Didn't find type of proposal id {} in storage", + proposal_id + ) + }); if let ProposalVote::Yay(ref vote_type) = args.vote { if &proposal_type != vote_type { @@ -2055,23 +2057,29 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { ); safe_exit(1); } else if let VoteType::PGFCouncil(set) = vote_type { - // Check that addresses proposed as council are established and are present in storage + // Check that addresses proposed as council are established and + // are present in storage for (address, _) in set { match address { Address::Established(_) => { - let vp_key = Key::validity_predicate(&address); + let vp_key = Key::validity_predicate(address); if !rpc::query_has_storage_key(&client, &vp_key) .await { - eprintln!("Proposed PGF council {} cannot be found in storage", address); + eprintln!( + "Proposed PGF council {} cannot be found \ + in storage", + address + ); safe_exit(1); } } _ => { eprintln!( - "PGF council vote contains a non-established address: {}", - address - ); + "PGF council vote contains a non-established \ + address: {}", + address + ); safe_exit(1); } } diff --git a/core/src/types/governance.rs b/core/src/types/governance.rs index 58b66fd480..0333ae1202 100644 --- a/core/src/types/governance.rs +++ b/core/src/types/governance.rs @@ -65,7 +65,7 @@ impl FromStr for ProposalVote { fn from_str(s: &str) -> Result { let splits = s.trim().split_ascii_whitespace(); - let mut iter = splits.clone().into_iter(); + let mut iter = splits.clone(); match iter.next() { Some(t) => match t { @@ -119,11 +119,10 @@ impl ProposalVote { /// Check if vote is of type default pub fn is_default_vote(&self) -> bool { - match self { - ProposalVote::Yay(VoteType::Default) => true, - ProposalVote::Nay => true, - _ => false, - } + matches!( + self, + ProposalVote::Yay(VoteType::Default) | ProposalVote::Nay + ) } } diff --git a/shared/src/ledger/native_vp/governance/mod.rs b/shared/src/ledger/native_vp/governance/mod.rs index 6c6712ef3c..c6413f68db 100644 --- a/shared/src/ledger/native_vp/governance/mod.rs +++ b/shared/src/ledger/native_vp/governance/mod.rs @@ -203,7 +203,7 @@ where ) => { if let ProposalVote::Yay(vote_type) = vote { if proposal_type != vote_type { - //FIXME: technically this is needed only for Yay votes + // FIXME: technically this is needed only for Yay votes return Ok(false); } @@ -213,7 +213,8 @@ where for (address, _) in set { match address { Address::Established(_) => { - // Check that established address exists in storage + // Check that established address exists in + // storage let vp_key = Key::validity_predicate(&address); if !self.ctx.has_key_pre(&vp_key)? { diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 9f83e3c8ef..8302711cfc 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -2835,7 +2835,8 @@ fn pgf_governance_proposal() -> Result<()> { client.assert_success(); // FIXME: test not 1/3 of total voting power (vote with just the delegator) - // FIXME: test majority when 1/3 of votes (vote with both BERTHA and validator 0 anche check that validtor 0 won) + // FIXME: test majority when 1/3 of votes (vote with both BERTHA and + // validator 0 anche check that validtor 0 won) // 12. Wait proposal grace and check proposal author funds let mut epoch = get_epoch(&test, &validator_one_rpc).unwrap(); From ec26f2e92e6be7c4309431c39fcd42a45d0fde17 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Thu, 26 Jan 2023 13:31:43 +0100 Subject: [PATCH 17/49] Fixes `ProposalType` serialization --- .../docs/src/user-guide/ledger/on-chain-governance.md | 4 +++- tests/src/e2e/ledger_tests.rs | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/documentation/docs/src/user-guide/ledger/on-chain-governance.md b/documentation/docs/src/user-guide/ledger/on-chain-governance.md index 048879b71d..325a6ae4bd 100644 --- a/documentation/docs/src/user-guide/ledger/on-chain-governance.md +++ b/documentation/docs/src/user-guide/ledger/on-chain-governance.md @@ -27,7 +27,9 @@ Now, we need to create a json file `proposal.json` holding the content of our pr "voting_start_epoch": 3, "voting_end_epoch": 6, "grace_epoch": 12, - "type": "Default" + "type": { + "Default":null + } } ``` diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 8302711cfc..128c3d115c 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -2436,7 +2436,9 @@ fn proposal_submission() -> Result<()> { "voting_start_epoch": 9999_u64, "voting_end_epoch": 10000_u64, "grace_epoch": 10009_u64, - "type": "Default" + "type": { + "Default":null + } } ); let invalid_proposal_json_path = @@ -2936,7 +2938,9 @@ fn proposal_offline() -> Result<()> { "voting_start_epoch": 3_u64, "voting_end_epoch": 9_u64, "grace_epoch": 18_u64, - "type": "Default" + "type": { + "Default": null + } } ); let valid_proposal_json_path = From 41f282c5a9783de36af16b601f595dccdd3db528 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Thu, 26 Jan 2023 14:58:10 +0100 Subject: [PATCH 18/49] Improves pgf e2e test --- tests/src/e2e/ledger_tests.rs | 141 +++++++++++++++++++++++++--------- 1 file changed, 106 insertions(+), 35 deletions(-) diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 128c3d115c..b182dca31c 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -2710,6 +2710,22 @@ fn pgf_governance_proposal() -> Result<()> { // 1 - Submit proposal let albert = find_address(&test, ALBERT)?; + let valid_proposal_json_path = + prepare_proposal_data(&test, albert.clone(), ProposalType::PGFCouncil); + let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); + + let submit_proposal_args = vec![ + "init-proposal", + "--data-path", + valid_proposal_json_path.to_str().unwrap(), + "--ledger-address", + &validator_one_rpc, + ]; + let mut client = run!(test, Bin::Client, submit_proposal_args, Some(40))?; + client.exp_string("Transaction is valid.")?; + client.assert_success(); + + // Sumbit another proposal let valid_proposal_json_path = prepare_proposal_data(&test, albert, ProposalType::PGFCouncil); let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); @@ -2738,6 +2754,18 @@ fn pgf_governance_proposal() -> Result<()> { client.exp_string("Proposal: 0")?; client.assert_success(); + let proposal_query_args = vec![ + "query-proposal", + "--proposal-id", + "1", + "--ledger-address", + &validator_one_rpc, + ]; + + let mut client = run!(test, Bin::Client, proposal_query_args, Some(40))?; + client.exp_string("Proposal: 1")?; + client.assert_success(); + // Query token balance proposal author (submitted funds) let query_balance_args = vec![ "balance", @@ -2750,7 +2778,7 @@ fn pgf_governance_proposal() -> Result<()> { ]; let mut client = run!(test, Bin::Client, query_balance_args, Some(40))?; - client.exp_string("NAM: 999500")?; + client.exp_string("NAM: 999000")?; client.assert_success(); // Query token balance governance @@ -2765,7 +2793,7 @@ fn pgf_governance_proposal() -> Result<()> { ]; let mut client = run!(test, Bin::Client, query_balance_args, Some(40))?; - client.exp_string("NAM: 500")?; + client.exp_string("NAM: 1000")?; client.assert_success(); // 3 - Send a yay vote from a validator @@ -2775,7 +2803,8 @@ fn pgf_governance_proposal() -> Result<()> { epoch = get_epoch(&test, &validator_one_rpc).unwrap(); } - let vote = format!("yay {} 1000", find_address(&test, ALBERT)?); + let albert_address = find_address(&test, ALBERT)?; + let vote = format!("yay {} 1000", albert_address); let submit_proposal_vote = vec![ "vote-proposal", @@ -2799,13 +2828,32 @@ fn pgf_governance_proposal() -> Result<()> { client.exp_string("Transaction is valid.")?; client.assert_success(); - // Send vote from delegator + // Send different yay vote from delegator to check majority on 1/3 + let different_vote = format!("yay {} 900", albert_address); let submit_proposal_vote_delagator = vec![ "vote-proposal", "--proposal-id", "0", "--vote", - "nay", + &different_vote, + "--signer", + BERTHA, + "--ledger-address", + &validator_one_rpc, + ]; + + let mut client = + run!(test, Bin::Client, submit_proposal_vote_delagator, Some(40))?; + client.exp_string("Transaction is valid.")?; + client.assert_success(); + + // Send vote to the second proposal from delegator + let submit_proposal_vote_delagator = vec![ + "vote-proposal", + "--proposal-id", + "1", + "--vote", + &different_vote, "--signer", BERTHA, "--ledger-address", @@ -2817,7 +2865,7 @@ fn pgf_governance_proposal() -> Result<()> { client.exp_string("Transaction is valid.")?; client.assert_success(); - // 4 - Query the proposal and check the result + // 4 - Query the proposal and check the result is the one voted by the validator (majority) let mut epoch = get_epoch(&test, &validator_one_rpc).unwrap(); while epoch.0 <= 25 { sleep(1); @@ -2832,49 +2880,72 @@ fn pgf_governance_proposal() -> Result<()> { &validator_one_rpc, ]; + // FIXME: document spending cap is NAMNAM let mut client = run!(test, Bin::Client, query_proposal, Some(15))?; - client.exp_string("Result: passed")?; //FIXME: here result is 0 yay votes, tally problem? No both the votes have been rejected + client.exp_string(&format!( + "Result: passed with PGF council address: {}, spending cap: 0.001", + albert_address + ))?; client.assert_success(); - // FIXME: test not 1/3 of total voting power (vote with just the delegator) - // FIXME: test majority when 1/3 of votes (vote with both BERTHA and - // validator 0 anche check that validtor 0 won) - - // 12. Wait proposal grace and check proposal author funds + // Query the second proposal and check the it didn't pass let mut epoch = get_epoch(&test, &validator_one_rpc).unwrap(); - while epoch.0 < 31 { + while epoch.0 <= 25 { sleep(1); epoch = get_epoch(&test, &validator_one_rpc).unwrap(); } - let query_balance_args = vec![ - "balance", - "--owner", - ALBERT, - "--token", - NAM, + let query_proposal = vec![ + "query-proposal-result", + "--proposal-id", + "1", "--ledger-address", &validator_one_rpc, ]; - let mut client = run!(test, Bin::Client, query_balance_args, Some(30))?; - client.exp_string("NAM: 1000000")?; + let mut client = run!(test, Bin::Client, query_proposal, Some(15))?; + client.exp_string(&format!( + "Result: passed with PGF council address: {}, spending cap: 0.001", + albert_address + ))?; client.assert_success(); - // Check if governance funds are 0 - let query_balance_args = vec![ - "balance", - "--owner", - GOVERNANCE_ADDRESS, - "--token", - NAM, - "--ledger-address", - &validator_one_rpc, - ]; - - let mut client = run!(test, Bin::Client, query_balance_args, Some(30))?; - client.exp_string("NAM: 0")?; - client.assert_success(); + //FIXME: uncomment + // // 12. Wait proposal grace and check proposal author funds + // let mut epoch = get_epoch(&test, &validator_one_rpc).unwrap(); + // while epoch.0 < 31 { + // sleep(1); + // epoch = get_epoch(&test, &validator_one_rpc).unwrap(); + // } + + // let query_balance_args = vec![ + // "balance", + // "--owner", + // ALBERT, + // "--token", + // NAM, + // "--ledger-address", + // &validator_one_rpc, + // ]; + + // let mut client = run!(test, Bin::Client, query_balance_args, Some(30))?; + // client.exp_string("NAM: 1000000")?; + // client.assert_success(); + + // // Check if governance funds are 0 + // let query_balance_args = vec![ + // "balance", + // "--owner", + // GOVERNANCE_ADDRESS, + // "--token", + // NAM, + // "--ledger-address", + // &validator_one_rpc, + // ]; + + // let mut client = run!(test, Bin::Client, query_balance_args, Some(30))?; + // client.exp_string("NAM: 0")?; + // client.assert_success(); Ok(()) } From 070aaea25cfe3bace04320a7de42f3f9df11e36a Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Thu, 26 Jan 2023 15:32:51 +0100 Subject: [PATCH 19/49] Improves governance VP vote check --- apps/src/lib/cli.rs | 2 +- shared/src/ledger/native_vp/governance/mod.rs | 13 +++++++------ tests/src/e2e/ledger_tests.rs | 13 +------------ 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 40005aeaf7..1901b4c4c0 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2330,7 +2330,7 @@ pub mod args { .arg(PROPOSAL_VOTE.def().about( "The vote for the proposal. Either yay or nay (with \ optional memo).\nDefault vote: yay | nay\nPGF vote: yay \ - $council1 $cap1 $council2 $cap2 ... | nay", + $council1 $cap1 $council2 $cap2 ... | nay (cap is expressed in microNAM)", )) .arg( PROPOSAL_OFFLINE diff --git a/shared/src/ledger/native_vp/governance/mod.rs b/shared/src/ledger/native_vp/governance/mod.rs index c6413f68db..a8974dc036 100644 --- a/shared/src/ledger/native_vp/governance/mod.rs +++ b/shared/src/ledger/native_vp/governance/mod.rs @@ -6,6 +6,7 @@ use std::collections::BTreeSet; use namada_core::ledger::governance::storage as gov_storage; use namada_core::ledger::storage; +use namada_core::ledger::storage_api::OptionExt; use namada_core::ledger::vp_env::VpEnv; use namada_core::types::governance::{ProposalVote, VoteType}; use namada_core::types::transaction::governance::ProposalType; @@ -177,14 +178,10 @@ where let voter = gov_storage::get_voter_address(key); let delegation_address = gov_storage::get_vote_delegation_address(key); let vote: Option = self.ctx.read_post(key)?; - let proposal_type_key = gov_storage::get_proposal_type_key(proposal_id); - let proposal_type: Option = - self.ctx.read_pre(&proposal_type_key)?; match ( pre_counter, vote, - proposal_type, voter, delegation_address, current_epoch, @@ -194,7 +191,6 @@ where ( Some(pre_counter), Some(vote), - Some(proposal_type), Some(voter_address), Some(delegation_address), Some(current_epoch), @@ -202,8 +198,13 @@ where Some(pre_voting_end_epoch), ) => { if let ProposalVote::Yay(vote_type) = vote { + let proposal_type_key = + gov_storage::get_proposal_type_key(proposal_id); + let proposal_type: ProposalType = self + .ctx + .read_pre(&proposal_type_key)? + .ok_or_err_msg("Missing proposal type in storage")?; if proposal_type != vote_type { - // FIXME: technically this is needed only for Yay votes return Ok(false); } diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index b182dca31c..60e2a0e16d 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -2880,7 +2880,6 @@ fn pgf_governance_proposal() -> Result<()> { &validator_one_rpc, ]; - // FIXME: document spending cap is NAMNAM let mut client = run!(test, Bin::Client, query_proposal, Some(15))?; client.exp_string(&format!( "Result: passed with PGF council address: {}, spending cap: 0.001", @@ -2889,12 +2888,6 @@ fn pgf_governance_proposal() -> Result<()> { client.assert_success(); // Query the second proposal and check the it didn't pass - let mut epoch = get_epoch(&test, &validator_one_rpc).unwrap(); - while epoch.0 <= 25 { - sleep(1); - epoch = get_epoch(&test, &validator_one_rpc).unwrap(); - } - let query_proposal = vec![ "query-proposal-result", "--proposal-id", @@ -2904,15 +2897,11 @@ fn pgf_governance_proposal() -> Result<()> { ]; let mut client = run!(test, Bin::Client, query_proposal, Some(15))?; - client.exp_string(&format!( - "Result: passed with PGF council address: {}, spending cap: 0.001", - albert_address - ))?; + client.exp_string("Result: rejected")?; client.assert_success(); //FIXME: uncomment // // 12. Wait proposal grace and check proposal author funds - // let mut epoch = get_epoch(&test, &validator_one_rpc).unwrap(); // while epoch.0 < 31 { // sleep(1); // epoch = get_epoch(&test, &validator_one_rpc).unwrap(); From e68719c497bd7ec6210a52b47e88c923efd13630 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Thu, 26 Jan 2023 16:07:54 +0100 Subject: [PATCH 20/49] Finalizes pgf e2e test --- tests/src/e2e/ledger_tests.rs | 75 +++++++++++++++++------------------ 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 60e2a0e16d..940ef53d3c 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -2646,10 +2646,10 @@ fn proposal_submission() -> Result<()> { /// Test submission and vote of a PGF proposal /// -/// 1 - Sumbit a proposal +/// 1 - Sumbit two proposals /// 2 - Check balance -/// 3 - Vote for the accepted proposal -/// 4 - Check proposal passed +/// 3 - Vote for the accepted proposals +/// 4 - Check one proposal passed and the other one didn't /// 5 - Check funds #[test] fn pgf_governance_proposal() -> Result<()> { @@ -2900,41 +2900,40 @@ fn pgf_governance_proposal() -> Result<()> { client.exp_string("Result: rejected")?; client.assert_success(); - //FIXME: uncomment - // // 12. Wait proposal grace and check proposal author funds - // while epoch.0 < 31 { - // sleep(1); - // epoch = get_epoch(&test, &validator_one_rpc).unwrap(); - // } - - // let query_balance_args = vec![ - // "balance", - // "--owner", - // ALBERT, - // "--token", - // NAM, - // "--ledger-address", - // &validator_one_rpc, - // ]; - - // let mut client = run!(test, Bin::Client, query_balance_args, Some(30))?; - // client.exp_string("NAM: 1000000")?; - // client.assert_success(); - - // // Check if governance funds are 0 - // let query_balance_args = vec![ - // "balance", - // "--owner", - // GOVERNANCE_ADDRESS, - // "--token", - // NAM, - // "--ledger-address", - // &validator_one_rpc, - // ]; - - // let mut client = run!(test, Bin::Client, query_balance_args, Some(30))?; - // client.exp_string("NAM: 0")?; - // client.assert_success(); + // 12. Wait proposals grace and check proposal author funds + while epoch.0 < 31 { + sleep(1); + epoch = get_epoch(&test, &validator_one_rpc).unwrap(); + } + + let query_balance_args = vec![ + "balance", + "--owner", + ALBERT, + "--token", + NAM, + "--ledger-address", + &validator_one_rpc, + ]; + + let mut client = run!(test, Bin::Client, query_balance_args, Some(30))?; + client.exp_string("NAM: 999500")?; + client.assert_success(); + + // Check if governance funds are 500 + let query_balance_args = vec![ + "balance", + "--owner", + GOVERNANCE_ADDRESS, + "--token", + NAM, + "--ledger-address", + &validator_one_rpc, + ]; + + let mut client = run!(test, Bin::Client, query_balance_args, Some(30))?; + client.exp_string("NAM: 500")?; + client.assert_success(); Ok(()) } From ad08b8f8f1d1e09ac4a9c9b8316d6c30ee503a91 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Thu, 26 Jan 2023 17:21:24 +0100 Subject: [PATCH 21/49] Refunds for pgf proposal --- apps/src/lib/node/ledger/shell/governance.rs | 25 ++++++++++++++-- tests/src/e2e/ledger_tests.rs | 30 +++++++++----------- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/governance.rs b/apps/src/lib/node/ledger/shell/governance.rs index 5a16a6cfad..c4c97c88fd 100644 --- a/apps/src/lib/node/ledger/shell/governance.rs +++ b/apps/src/lib/node/ledger/shell/governance.rs @@ -208,9 +208,30 @@ where } } } - Tally::PGFCouncil(_council) => { + Tally::PGFCouncil(council) => { // TODO: implement when PGF is in place - todo!(); + let proposal_event: Event = ProposalEvent::new( + EventType::Proposal.to_string(), + TallyResult::Passed(Tally::PGFCouncil(council)), + id, + false, + false, + ) + .into(); + response.events.push(proposal_event); + proposals_result.passed.push(id); + + let proposal_author_key = + gov_storage::get_author_key(id); + let proposal_author = shell + .read_storage_key::
(&proposal_author_key) + .ok_or_else(|| { + Error::BadProposal( + id, + "Invalid proposal author.".to_string(), + ) + })?; + proposal_author } } } diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 940ef53d3c..785c4709a5 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -2704,7 +2704,7 @@ fn pgf_governance_proposal() -> Result<()> { "--ledger-address", &validator_one_rpc, ]; - let mut client = run!(test, Bin::Client, tx_args, Some(40))?; + client = run!(test, Bin::Client, tx_args, Some(40))?; client.exp_string("Transaction is valid.")?; client.assert_success(); @@ -2721,7 +2721,7 @@ fn pgf_governance_proposal() -> Result<()> { "--ledger-address", &validator_one_rpc, ]; - let mut client = run!(test, Bin::Client, submit_proposal_args, Some(40))?; + client = run!(test, Bin::Client, submit_proposal_args, Some(40))?; client.exp_string("Transaction is valid.")?; client.assert_success(); @@ -2737,7 +2737,7 @@ fn pgf_governance_proposal() -> Result<()> { "--ledger-address", &validator_one_rpc, ]; - let mut client = run!(test, Bin::Client, submit_proposal_args, Some(40))?; + client = run!(test, Bin::Client, submit_proposal_args, Some(40))?; client.exp_string("Transaction is valid.")?; client.assert_success(); @@ -2750,7 +2750,7 @@ fn pgf_governance_proposal() -> Result<()> { &validator_one_rpc, ]; - let mut client = run!(test, Bin::Client, proposal_query_args, Some(40))?; + client = run!(test, Bin::Client, proposal_query_args, Some(40))?; client.exp_string("Proposal: 0")?; client.assert_success(); @@ -2762,7 +2762,7 @@ fn pgf_governance_proposal() -> Result<()> { &validator_one_rpc, ]; - let mut client = run!(test, Bin::Client, proposal_query_args, Some(40))?; + client = run!(test, Bin::Client, proposal_query_args, Some(40))?; client.exp_string("Proposal: 1")?; client.assert_success(); @@ -2777,7 +2777,7 @@ fn pgf_governance_proposal() -> Result<()> { &validator_one_rpc, ]; - let mut client = run!(test, Bin::Client, query_balance_args, Some(40))?; + client = run!(test, Bin::Client, query_balance_args, Some(40))?; client.exp_string("NAM: 999000")?; client.assert_success(); @@ -2792,7 +2792,7 @@ fn pgf_governance_proposal() -> Result<()> { &validator_one_rpc, ]; - let mut client = run!(test, Bin::Client, query_balance_args, Some(40))?; + client = run!(test, Bin::Client, query_balance_args, Some(40))?; client.exp_string("NAM: 1000")?; client.assert_success(); @@ -2818,7 +2818,7 @@ fn pgf_governance_proposal() -> Result<()> { &validator_one_rpc, ]; - let mut client = run_as!( + client = run_as!( test, Who::Validator(0), Bin::Client, @@ -2842,8 +2842,7 @@ fn pgf_governance_proposal() -> Result<()> { &validator_one_rpc, ]; - let mut client = - run!(test, Bin::Client, submit_proposal_vote_delagator, Some(40))?; + client = run!(test, Bin::Client, submit_proposal_vote_delagator, Some(40))?; client.exp_string("Transaction is valid.")?; client.assert_success(); @@ -2860,13 +2859,12 @@ fn pgf_governance_proposal() -> Result<()> { &validator_one_rpc, ]; - let mut client = - run!(test, Bin::Client, submit_proposal_vote_delagator, Some(40))?; + client = run!(test, Bin::Client, submit_proposal_vote_delagator, Some(40))?; client.exp_string("Transaction is valid.")?; client.assert_success(); // 4 - Query the proposal and check the result is the one voted by the validator (majority) - let mut epoch = get_epoch(&test, &validator_one_rpc).unwrap(); + epoch = get_epoch(&test, &validator_one_rpc).unwrap(); while epoch.0 <= 25 { sleep(1); epoch = get_epoch(&test, &validator_one_rpc).unwrap(); @@ -2880,7 +2878,7 @@ fn pgf_governance_proposal() -> Result<()> { &validator_one_rpc, ]; - let mut client = run!(test, Bin::Client, query_proposal, Some(15))?; + client = run!(test, Bin::Client, query_proposal, Some(15))?; client.exp_string(&format!( "Result: passed with PGF council address: {}, spending cap: 0.001", albert_address @@ -2916,7 +2914,7 @@ fn pgf_governance_proposal() -> Result<()> { &validator_one_rpc, ]; - let mut client = run!(test, Bin::Client, query_balance_args, Some(30))?; + client = run!(test, Bin::Client, query_balance_args, Some(30))?; client.exp_string("NAM: 999500")?; client.assert_success(); @@ -2931,7 +2929,7 @@ fn pgf_governance_proposal() -> Result<()> { &validator_one_rpc, ]; - let mut client = run!(test, Bin::Client, query_balance_args, Some(30))?; + client = run!(test, Bin::Client, query_balance_args, Some(30))?; client.exp_string("NAM: 500")?; client.assert_success(); From 1ee08a7f5913f9d534a1c97de4a967ad81600ea9 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Thu, 26 Jan 2023 18:07:51 +0100 Subject: [PATCH 22/49] Fixes pgf e2e error --- tests/src/e2e/ledger_tests.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 785c4709a5..be77836585 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -2894,7 +2894,7 @@ fn pgf_governance_proposal() -> Result<()> { &validator_one_rpc, ]; - let mut client = run!(test, Bin::Client, query_proposal, Some(15))?; + client = run!(test, Bin::Client, query_proposal, Some(15))?; client.exp_string("Result: rejected")?; client.assert_success(); @@ -2918,7 +2918,7 @@ fn pgf_governance_proposal() -> Result<()> { client.exp_string("NAM: 999500")?; client.assert_success(); - // Check if governance funds are 500 + // Check if governance funds are 0 let query_balance_args = vec![ "balance", "--owner", @@ -2930,7 +2930,7 @@ fn pgf_governance_proposal() -> Result<()> { ]; client = run!(test, Bin::Client, query_balance_args, Some(30))?; - client.exp_string("NAM: 500")?; + client.exp_string("NAM: 0")?; client.assert_success(); Ok(()) From f5b3bfaca1e320ac05e5b4a39a5e41477cc5dd88 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Thu, 26 Jan 2023 18:43:41 +0100 Subject: [PATCH 23/49] Clippy + fmt --- apps/src/lib/cli.rs | 3 ++- apps/src/lib/node/ledger/shell/governance.rs | 6 +++--- tests/src/e2e/ledger_tests.rs | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 1901b4c4c0..14e96de8f9 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2330,7 +2330,8 @@ pub mod args { .arg(PROPOSAL_VOTE.def().about( "The vote for the proposal. Either yay or nay (with \ optional memo).\nDefault vote: yay | nay\nPGF vote: yay \ - $council1 $cap1 $council2 $cap2 ... | nay (cap is expressed in microNAM)", + $council1 $cap1 $council2 $cap2 ... | nay (cap is \ + expressed in microNAM)", )) .arg( PROPOSAL_OFFLINE diff --git a/apps/src/lib/node/ledger/shell/governance.rs b/apps/src/lib/node/ledger/shell/governance.rs index c4c97c88fd..716da6e569 100644 --- a/apps/src/lib/node/ledger/shell/governance.rs +++ b/apps/src/lib/node/ledger/shell/governance.rs @@ -223,15 +223,15 @@ where let proposal_author_key = gov_storage::get_author_key(id); - let proposal_author = shell + + shell .read_storage_key::
(&proposal_author_key) .ok_or_else(|| { Error::BadProposal( id, "Invalid proposal author.".to_string(), ) - })?; - proposal_author + })? } } } diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index be77836585..0e745d4353 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -2863,7 +2863,8 @@ fn pgf_governance_proposal() -> Result<()> { client.exp_string("Transaction is valid.")?; client.assert_success(); - // 4 - Query the proposal and check the result is the one voted by the validator (majority) + // 4 - Query the proposal and check the result is the one voted by the + // validator (majority) epoch = get_epoch(&test, &validator_one_rpc).unwrap(); while epoch.0 <= 25 { sleep(1); From 3fe49b9fec9d7c0a2df20c9a1ac820362af9d116 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Fri, 27 Jan 2023 18:26:14 +0100 Subject: [PATCH 24/49] Updates eth bridge vote in gov specs --- documentation/specs/src/base-ledger/governance.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/documentation/specs/src/base-ledger/governance.md b/documentation/specs/src/base-ledger/governance.md index c089603740..1ee52d6714 100644 --- a/documentation/specs/src/base-ledger/governance.md +++ b/documentation/specs/src/base-ledger/governance.md @@ -129,8 +129,8 @@ pub enum ProposalType { - Doesn't carry any wasm code - Allows only validators to vote -- Requires 2/3 of the validators' total voting power to succeed -- Expect every vote to carry a memo in the form of a tuple `(Action, Signature)` +- Requires 2/3 of the total voting power to succeed +- Expect every vote to carry a memo in the form of a `Signature` over some bytes provided in the proposal ### GovernanceAddress VP @@ -207,7 +207,8 @@ where `ProposalVote` is an enum representing a `Yay` or `Nay` vote: the yay vari The storage key will only be created if the transaction is signed either by a validator or a delegator. In case a vote misses a required memo or carries a memo with an invalid format, the vote will be discarded at validation time (VP) and it won't be written to storage. -If delegators are allowed to vote, validators will be able to vote only for 2/3 of the total voting period, while delegators can vote until the end of the voting period. +If delegators are allowed to vote, validators will be able to vote only for 2/3 of the total voting period, while delegators can vote until the end of the voting period. If only validators are allowed to vote +for the `ProposalType` in exam, they are allowed to vote for the entire voting window. If a delegator votes differently than its validator, this will *override* the corresponding vote of this validator (e.g. if a delegator has a voting power of 200 and votes opposite to the delegator holding these tokens, than 200 will be subtracted from the voting power of the involved validator). From 80dd27cd905806ddd538cde2c1fc76e8e8756ad0 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Fri, 27 Jan 2023 18:28:34 +0100 Subject: [PATCH 25/49] Adds ETH bridge cutom proposal --- core/src/types/governance.rs | 88 +++++++++++++++++------- core/src/types/transaction/governance.rs | 7 ++ 2 files changed, 71 insertions(+), 24 deletions(-) diff --git a/core/src/types/governance.rs b/core/src/types/governance.rs index 0333ae1202..58685e69ed 100644 --- a/core/src/types/governance.rs +++ b/core/src/types/governance.rs @@ -39,6 +39,8 @@ pub enum VoteType { Default, /// A vote for the PGF council PGFCouncil(BTreeSet), + /// A vote for ETH bridge carrying the signature over the proposed message + ETHBridge(Signature), } #[derive( @@ -70,13 +72,23 @@ impl FromStr for ProposalVote { match iter.next() { Some(t) => match t { "yay" => { - let mut set = BTreeSet::new(); - let address_iter = - splits.clone().into_iter().skip(1).step_by(2); - let cap_iter = splits.into_iter().skip(2).step_by(2); - for (address, cap) in - address_iter.zip(cap_iter).map(|(addr, cap)| { - ( + // Check next token to detect vote type + match iter.next() { + Some(token) => { + if Address::decode(token).is_ok() { + // PGF vote + let mut set = BTreeSet::new(); + let address_iter = splits + .clone() + .into_iter() + .skip(1) + .step_by(2); + let cap_iter = + splits.into_iter().skip(2).step_by(2); + for (address, cap) in address_iter + .zip(cap_iter) + .map(|(addr, cap)| { + ( addr.to_owned().parse().map_err( |e: crate::types::address::DecodeError| { e.to_string() @@ -84,15 +96,24 @@ impl FromStr for ProposalVote { ), cap.parse::().map_err(|e| e.to_string()), ) - }) - { - set.insert((address?, cap?.into())); - } - - if set.is_empty() { - Ok(Self::Yay(VoteType::Default)) - } else { - Ok(Self::Yay(VoteType::PGFCouncil(set))) + }) + { + set.insert((address?, cap?.into())); + } + + Ok(Self::Yay(VoteType::PGFCouncil(set))) + } else { + // ETH Bridge vote + let signature = serde_json::from_str(token) + .map_err(|e| e.to_string())?; + + Ok(Self::Yay(VoteType::ETHBridge(signature))) + } + } + None => { + // Default vote + Ok(Self::Yay(VoteType::Default)) + } } } "nay" => match iter.next() { @@ -111,10 +132,7 @@ impl FromStr for ProposalVote { impl ProposalVote { /// Check if a vote is yay pub fn is_yay(&self) -> bool { - match self { - ProposalVote::Yay(_) => true, - ProposalVote::Nay => false, - } + matches!(self, ProposalVote::Yay(_)) } /// Check if vote is of type default @@ -129,7 +147,25 @@ impl ProposalVote { impl Display for ProposalVote { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - ProposalVote::Yay(_) => write!(f, "yay"), + ProposalVote::Yay(vote_type) => match vote_type { + VoteType::Default => write!(f, "yay"), + VoteType::PGFCouncil(councils) => { + writeln!(f, "yay with councils:")?; + for (address, spending_cap) in councils { + writeln!( + f, + "Council: {}, spending cap: {}", + address, spending_cap + )? + } + + Ok(()) + } + VoteType::ETHBridge(sig) => { + write!(f, "yay with signature: {:#?}", sig) + } + }, + ProposalVote::Nay => write!(f, "nay"), } } @@ -144,10 +180,12 @@ pub enum ProposalVoteParseError { /// The type of the tally pub enum Tally { - /// Tally a default proposal + /// Default proposal Default, - /// Tally a PGF proposal + /// PGF proposal PGFCouncil(Council), + /// ETH Bridge proposal + ETHBridge, } /// The result of a proposal @@ -195,7 +233,7 @@ impl Display for TallyResult { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { TallyResult::Passed(vote) => match vote { - Tally::Default => write!(f, "passed"), + Tally::Default | Tally::ETHBridge => write!(f, "passed"), Tally::PGFCouncil((council, cap)) => write!( f, "passed with PGF council address: {}, spending cap: {}", @@ -217,6 +255,8 @@ pub enum ProposalType { Default(Option), /// A PGF council proposal PGFCouncil, + /// An ETH bridge proposal + ETHBridge, } #[derive( diff --git a/core/src/types/transaction/governance.rs b/core/src/types/transaction/governance.rs index c0c2efb560..3b1f183eaa 100644 --- a/core/src/types/transaction/governance.rs +++ b/core/src/types/transaction/governance.rs @@ -24,6 +24,8 @@ pub enum ProposalType { Default(Option>), /// PGF council proposal PGFCouncil, + /// ETH proposal + ETHBridge, } impl Display for ProposalType { @@ -31,6 +33,7 @@ impl Display for ProposalType { match self { ProposalType::Default(_) => write!(f, "Default"), ProposalType::PGFCouncil => write!(f, "PGF Council"), + ProposalType::ETHBridge => write!(f, "ETH Bridge"), } } } @@ -44,6 +47,9 @@ impl PartialEq for ProposalType { Self::PGFCouncil => { matches!(other, VoteType::PGFCouncil(..)) } + Self::ETHBridge => { + matches!(other, VoteType::ETHBridge(_)) + } } } } @@ -64,6 +70,7 @@ impl TryFrom for ProposalType { } } governance::ProposalType::PGFCouncil => Ok(Self::PGFCouncil), + governance::ProposalType::ETHBridge => Ok(Self::ETHBridge), } } } From 62c040e5c930afeae0c23920babdae6958c82431 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Fri, 27 Jan 2023 18:29:52 +0100 Subject: [PATCH 26/49] Updates governance vp for eth votes --- shared/src/ledger/native_vp/governance/mod.rs | 93 +++++++++++-------- 1 file changed, 55 insertions(+), 38 deletions(-) diff --git a/shared/src/ledger/native_vp/governance/mod.rs b/shared/src/ledger/native_vp/governance/mod.rs index a8974dc036..f3b44433ce 100644 --- a/shared/src/ledger/native_vp/governance/mod.rs +++ b/shared/src/ledger/native_vp/governance/mod.rs @@ -179,8 +179,13 @@ where let delegation_address = gov_storage::get_vote_delegation_address(key); let vote: Option = self.ctx.read_post(key)?; + let proposal_type_key = gov_storage::get_proposal_type_key(proposal_id); + let proposal_type: Option = + self.ctx.read_pre(&proposal_type_key)?; + match ( pre_counter, + proposal_type, vote, voter, delegation_address, @@ -190,6 +195,7 @@ where ) { ( Some(pre_counter), + Some(proposal_type), Some(vote), Some(voter_address), Some(delegation_address), @@ -197,13 +203,18 @@ where Some(pre_voting_start_epoch), Some(pre_voting_end_epoch), ) => { + if pre_counter <= proposal_id { + // Invalid proposal id + return Ok(false); + } + if current_epoch < pre_voting_start_epoch + || current_epoch > pre_voting_end_epoch + { + // Voted outside of voting window + return Ok(false); + } + if let ProposalVote::Yay(vote_type) = vote { - let proposal_type_key = - gov_storage::get_proposal_type_key(proposal_id); - let proposal_type: ProposalType = self - .ctx - .read_pre(&proposal_type_key)? - .ok_or_err_msg("Missing proposal type in storage")?; if proposal_type != vote_type { return Ok(false); } @@ -225,41 +236,47 @@ where _ => return Ok(false), } } + } else if let VoteType::ETHBridge(_sig) = vote_type { + // TODO: Check the validity of the signature with the governance ETH key in storage for the given validator } } - let is_delegator = self - .is_delegator( - pre_voting_start_epoch, - verifiers, - voter_address, - delegation_address, - ) - .unwrap_or(false); - - let is_validator = self - .is_validator( - pre_voting_start_epoch, - verifiers, - voter_address, - delegation_address, - ) - .unwrap_or(false); - - let is_valid_validator_voting_period = - is_valid_validator_voting_period( - current_epoch, - pre_voting_start_epoch, - pre_voting_end_epoch, - ); - - let is_valid = pre_counter > proposal_id - && current_epoch >= pre_voting_start_epoch - && current_epoch <= pre_voting_end_epoch - && (is_delegator - || (is_validator && is_valid_validator_voting_period)); - - Ok(is_valid) + match proposal_type { + ProposalType::Default(_) | ProposalType::PGFCouncil => { + if self + .is_validator( + pre_voting_start_epoch, + verifiers, + voter_address, + delegation_address, + ) + .unwrap_or(false) + { + Ok(is_valid_validator_voting_period( + current_epoch, + pre_voting_start_epoch, + pre_voting_end_epoch, + )) + } else { + Ok(self + .is_delegator( + pre_voting_start_epoch, + verifiers, + voter_address, + delegation_address, + ) + .unwrap_or(false)) + } + } + ProposalType::ETHBridge => Ok(self + .is_validator( + pre_voting_start_epoch, + verifiers, + voter_address, + delegation_address, + ) + .unwrap_or(false)), + } } _ => Ok(false), } From 539e6b8b60be1f7a27e9c1247c6715962fbf5ea1 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Fri, 27 Jan 2023 18:30:51 +0100 Subject: [PATCH 27/49] Updates tally with eth proposals --- .../src/ledger/native_vp/governance/utils.rs | 46 +++++++++++++++---- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/shared/src/ledger/native_vp/governance/utils.rs b/shared/src/ledger/native_vp/governance/utils.rs index 72f00323ad..ef3db6561d 100644 --- a/shared/src/ledger/native_vp/governance/utils.rs +++ b/shared/src/ledger/native_vp/governance/utils.rs @@ -90,17 +90,31 @@ pub fn compute_tally( } = votes; match proposal_type { - ProposalType::Default(_) => { + ProposalType::Default(_) | ProposalType::ETHBridge => { let mut total_yay_staked_tokens = VotePower::default(); + for (_, (amount, validator_vote)) in yay_validators.iter() { - if let ProposalVote::Yay(VoteType::Default) = validator_vote { - total_yay_staked_tokens += amount; + if let ProposalVote::Yay(vote_type) = validator_vote { + if proposal_type == vote_type { + total_yay_staked_tokens += amount; + } else { + return ProposalResult { + result: TallyResult::Failed(format!( + "Unexpected vote type. Expected: {}, Found: \ + {}", + proposal_type, validator_vote + )), + total_voting_power: total_stake, + total_yay_power: 0, + total_nay_power: 0, + }; + } } else { return ProposalResult { result: TallyResult::Failed(format!( - "Unexpected vote type. Expected: Default, Found: \ + "Unexpected vote type. Expected: {}, Found: \ {}", - validator_vote + proposal_type, validator_vote )), total_voting_power: total_stake, total_yay_power: 0, @@ -109,6 +123,7 @@ pub fn compute_tally( } } + // This loop is taken only for Default proposals for (_, vote_map) in delegators.iter() { for (validator_address, (vote_power, delegator_vote)) in vote_map.iter() @@ -133,9 +148,9 @@ pub fn compute_tally( _ => { return ProposalResult { result: TallyResult::Failed(format!( - "Unexpected vote type. Expected: Default, \ + "Unexpected vote type. Expected: {}, \ Found: {}", - delegator_vote + proposal_type, delegator_vote )), total_voting_power: total_stake, total_yay_power: 0, @@ -148,8 +163,21 @@ pub fn compute_tally( // Proposal passes if 2/3 of total voting power voted Yay if total_yay_staked_tokens >= (total_stake / 3) * 2 { + let tally_result = match proposal_type { + ProposalType::Default(_) => { + TallyResult::Passed(Tally::Default) + } + ProposalType::ETHBridge => { + TallyResult::Passed(Tally::ETHBridge) + } + _ => TallyResult::Failed(format!( + "Unexpected proposal type {}", + proposal_type + )), + }; + ProposalResult { - result: TallyResult::Passed(Tally::Default), + result: tally_result, total_voting_power: total_stake, total_yay_power: total_yay_staked_tokens, total_nay_power: 0, @@ -343,7 +371,7 @@ pub fn compute_tally( total_nay_power: 0, }, _ => { - // Select the winner council based on simple majority + // Select the winner council based on approval voting (majority) let council = total_yay_staked_tokens .into_iter() .max_by(|a, b| a.1.cmp(&b.1)) From f6e3683cde450f1f4609d6017c6ad6afdea9ca31 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Fri, 27 Jan 2023 18:31:33 +0100 Subject: [PATCH 28/49] Manages eth proposals in `finalize_block` --- apps/src/lib/node/ledger/shell/governance.rs | 38 ++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/governance.rs b/apps/src/lib/node/ledger/shell/governance.rs index 716da6e569..a1974c667a 100644 --- a/apps/src/lib/node/ledger/shell/governance.rs +++ b/apps/src/lib/node/ledger/shell/governance.rs @@ -209,7 +209,7 @@ where } } Tally::PGFCouncil(council) => { - // TODO: implement when PGF is in place + // TODO: implement when PGF is in place, update the PGF council in storage let proposal_event: Event = ProposalEvent::new( EventType::Proposal.to_string(), TallyResult::Passed(Tally::PGFCouncil(council)), @@ -221,6 +221,31 @@ where response.events.push(proposal_event); proposals_result.passed.push(id); + let proposal_author_key = + gov_storage::get_author_key(id); + + shell + .read_storage_key::
(&proposal_author_key) + .ok_or_else(|| { + Error::BadProposal( + id, + "Invalid proposal author.".to_string(), + ) + })? + } + Tally::ETHBridge => { + //TODO: implement when ETH Bridge. Apply the modification requested by the proposal + let proposal_event: Event = ProposalEvent::new( + EventType::Proposal.to_string(), + TallyResult::Passed(Tally::ETHBridge), + id, + false, + false, + ) + .into(); + response.events.push(proposal_event); + proposals_result.passed.push(id); + let proposal_author_key = gov_storage::get_author_key(id); @@ -264,7 +289,16 @@ where .into(); response.events.push(proposal_event); - slash_fund_address + //FIXME: panic here? Remove TallyResult::Failed? -> Yes remove failed + let proposal_author_key = gov_storage::get_author_key(id); + shell + .read_storage_key::
(&proposal_author_key) + .ok_or_else(|| { + Error::BadProposal( + id, + "Invalid proposal author.".to_string(), + ) + })? } }; From 953bbb5b64aa1be9ecd909cdb3c3e434a729f95e Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Fri, 27 Jan 2023 18:33:05 +0100 Subject: [PATCH 29/49] Updates cli proposal vote help --- apps/src/lib/cli.rs | 4 ++-- apps/src/lib/client/tx.rs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 14e96de8f9..e7720f9abf 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2330,8 +2330,8 @@ pub mod args { .arg(PROPOSAL_VOTE.def().about( "The vote for the proposal. Either yay or nay (with \ optional memo).\nDefault vote: yay | nay\nPGF vote: yay \ - $council1 $cap1 $council2 $cap2 ... | nay (cap is \ - expressed in microNAM)", + $council1 $cap1 $council2 $cap2 ... | nay (council is bech32m encoded, cap is \ + expressed in microNAM)\nETH Bridge vote: yay $signature | nay (signature is json encoded)", )) .arg( PROPOSAL_OFFLINE diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index daa32bb4d7..baabbc08ee 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -44,6 +44,7 @@ use namada::types::address::{masp, masp_tx_key, Address}; use namada::types::governance::{ OfflineProposal, OfflineVote, Proposal, ProposalVote, VoteType, }; +use namada::types::key::common::Signature; use namada::types::key::*; use namada::types::masp::{PaymentAddress, TransferTarget}; use namada::types::storage::{ From 9cdf6cc931557d0ec040cf9660c2fc92149e9b61 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Mon, 30 Jan 2023 15:04:23 +0100 Subject: [PATCH 30/49] Refactors custom proposal vote in client --- apps/src/lib/cli.rs | 32 +++++++-- apps/src/lib/client/tx.rs | 69 +++++++++++++++++-- apps/src/lib/node/ledger/shell/governance.rs | 1 - core/src/types/governance.rs | 68 ------------------ shared/src/ledger/native_vp/governance/mod.rs | 1 - 5 files changed, 87 insertions(+), 84 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index e7720f9abf..1a03e86003 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1569,7 +1569,6 @@ pub mod args { use namada::ibc::core::ics24_host::identifier::{ChannelId, PortId}; use namada::types::address::Address; use namada::types::chain::{ChainId, ChainIdPrefix}; - use namada::types::governance::ProposalVote; use namada::types::key::*; use namada::types::masp::MaspValue; use namada::types::storage::{self, Epoch}; @@ -1663,7 +1662,9 @@ pub mod args { const PUBLIC_KEY: Arg = arg("public-key"); const PROPOSAL_ID: Arg = arg("proposal-id"); const PROPOSAL_ID_OPT: ArgOpt = arg_opt("proposal-id"); - const PROPOSAL_VOTE: Arg = arg("vote"); + const PROPOSAL_VOTE_PGF_OPT: ArgOpt = arg_opt("pgf"); + const PROPOSAL_VOTE_ETH_OPT: ArgOpt = arg_opt("eth"); + const PROPOSAL_VOTE: Arg = arg("vote"); const RAW_ADDRESS: Arg
= arg("address"); const RAW_ADDRESS_OPT: ArgOpt
= RAW_ADDRESS.opt(); const RAW_PUBLIC_KEY_OPT: ArgOpt = arg_opt("public-key"); @@ -2292,7 +2293,11 @@ pub mod args { /// Proposal id pub proposal_id: Option, /// The vote - pub vote: ProposalVote, + pub vote: String, + /// PGF proposal + pub proposal_pgf: Option, + /// ETH proposal + pub proposal_eth: Option, /// Flag if proposal vote should be run offline pub offline: bool, /// The proposal file path @@ -2303,6 +2308,8 @@ pub mod args { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); let proposal_id = PROPOSAL_ID_OPT.parse(matches); + let proposal_pgf = PROPOSAL_VOTE_PGF_OPT.parse(matches); + let proposal_eth = PROPOSAL_VOTE_ETH_OPT.parse(matches); let vote = PROPOSAL_VOTE.parse(matches); let offline = PROPOSAL_OFFLINE.parse(matches); let proposal_data = DATA_PATH_OPT.parse(matches); @@ -2311,6 +2318,8 @@ pub mod args { tx, proposal_id, vote, + proposal_pgf, + proposal_eth, offline, proposal_data, } @@ -2328,10 +2337,19 @@ pub mod args { ]), ) .arg(PROPOSAL_VOTE.def().about( - "The vote for the proposal. Either yay or nay (with \ - optional memo).\nDefault vote: yay | nay\nPGF vote: yay \ - $council1 $cap1 $council2 $cap2 ... | nay (council is bech32m encoded, cap is \ - expressed in microNAM)\nETH Bridge vote: yay $signature | nay (signature is json encoded)", + "The vote for the proposal. Either yay or nay" + )) + .arg(PROPOSAL_VOTE_PGF_OPT.def().about("The list of proposed councils and spending caps:\n$council1 $cap1 $council2 $cap2 ... (council is bech32m encoded, cap is expressed in microNAM") + .requires(PROPOSAL_ID.name) + .conflicts_with( + + PROPOSAL_VOTE_ETH_OPT.name + ) + ) + .arg(PROPOSAL_VOTE_ETH_OPT.def().about("The signing key and message bytes (hex encoded) to be signed: $signing_key $message") + .requires(PROPOSAL_ID.name) + .conflicts_with( + PROPOSAL_VOTE_PGF_OPT.name )) .arg( PROPOSAL_OFFLINE diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index baabbc08ee..ad2e0035af 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; use std::collections::hash_map::Entry; -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::env; use std::fmt::Debug; use std::fs::{File, OpenOptions}; @@ -11,6 +11,7 @@ use std::path::PathBuf; use async_std::io::prelude::WriteExt; use async_std::io::{self}; use borsh::{BorshDeserialize, BorshSerialize}; +use data_encoding::HEXLOWER_PERMISSIVE; use itertools::Either::*; use masp_primitives::asset_type::AssetType; use masp_primitives::consensus::{BranchId, TestNetwork}; @@ -44,7 +45,6 @@ use namada::types::address::{masp, masp_tx_key, Address}; use namada::types::governance::{ OfflineProposal, OfflineVote, Proposal, ProposalVote, VoteType, }; -use namada::types::key::common::Signature; use namada::types::key::*; use namada::types::masp::{PaymentAddress, TransferTarget}; use namada::types::storage::{ @@ -1965,8 +1965,63 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { safe_exit(1) }; + // Construct vote + let proposal_vote = match args.vote.as_str() { + "yay" => { + if let Some(pgf) = args.proposal_pgf { + let splits = pgf.trim().split_ascii_whitespace(); + let address_iter = splits.clone().into_iter().step_by(2); + let cap_iter = splits.into_iter().skip(1).step_by(2); + let mut set = BTreeSet::new(); + for (address, cap) in + address_iter.zip(cap_iter).map(|(addr, cap)| { + ( + addr.to_owned() + .parse() + .expect("Failed to parse pgf council address"), + cap.parse::() + .expect("Failed to parse pgf spending cap"), + ) + }) + { + set.insert((address, cap.into())); + } + + ProposalVote::Yay(VoteType::PGFCouncil(set)) + } else if let Some(eth) = args.proposal_eth { + let mut splits = eth.trim().split_ascii_whitespace(); + //Sign the message + let sigkey = splits + .next() + .expect("Expected signing key") + .parse::() + .expect("Signing key parsing failed."); + + let msg = splits.next().expect("Missing message to sign"); + if splits.next().is_some() { + eprintln!("Unexpected argument after message"); + safe_exit(1); + } + + ProposalVote::Yay(VoteType::ETHBridge(common::SigScheme::sign( + &sigkey, + HEXLOWER_PERMISSIVE + .decode(msg.as_bytes()) + .expect("Error while decoding message"), + ))) + } else { + ProposalVote::Yay(VoteType::Default) + } + } + "nay" => ProposalVote::Nay, + _ => { + eprintln!("Vote must be either yay or nay"); + safe_exit(1); + } + }; + if args.offline { - if !args.vote.is_default_vote() { + if !proposal_vote.is_default_vote() { eprintln!( "Wrong vote type for offline proposal. Just vote yay or nay!" ); @@ -1999,7 +2054,7 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { let offline_vote = OfflineVote::new( &proposal, - args.vote, + proposal_vote, signer.clone(), &signing_key, ); @@ -2050,7 +2105,7 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { ) }); - if let ProposalVote::Yay(ref vote_type) = args.vote { + if let ProposalVote::Yay(ref vote_type) = proposal_vote { if &proposal_type != vote_type { eprintln!( "Expected vote of type {}, found {}", @@ -2124,14 +2179,14 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { &client, delegations, proposal_id, - &args.vote, + &proposal_vote, ) .await; } let tx_data = VoteProposalData { id: proposal_id, - vote: args.vote, + vote: proposal_vote, voter: voter_address, delegations: delegations.into_iter().collect(), }; diff --git a/apps/src/lib/node/ledger/shell/governance.rs b/apps/src/lib/node/ledger/shell/governance.rs index a1974c667a..f1493be938 100644 --- a/apps/src/lib/node/ledger/shell/governance.rs +++ b/apps/src/lib/node/ledger/shell/governance.rs @@ -289,7 +289,6 @@ where .into(); response.events.push(proposal_event); - //FIXME: panic here? Remove TallyResult::Failed? -> Yes remove failed let proposal_author_key = gov_storage::get_author_key(id); shell .read_storage_key::
(&proposal_author_key) diff --git a/core/src/types/governance.rs b/core/src/types/governance.rs index 58685e69ed..ea433a6b7e 100644 --- a/core/src/types/governance.rs +++ b/core/src/types/governance.rs @@ -2,7 +2,6 @@ use std::collections::{BTreeMap, BTreeSet}; use std::fmt::{self, Display}; -use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; use rust_decimal::Decimal; @@ -62,73 +61,6 @@ pub enum ProposalVote { Nay, } -impl FromStr for ProposalVote { - type Err = String; - - fn from_str(s: &str) -> Result { - let splits = s.trim().split_ascii_whitespace(); - let mut iter = splits.clone(); - - match iter.next() { - Some(t) => match t { - "yay" => { - // Check next token to detect vote type - match iter.next() { - Some(token) => { - if Address::decode(token).is_ok() { - // PGF vote - let mut set = BTreeSet::new(); - let address_iter = splits - .clone() - .into_iter() - .skip(1) - .step_by(2); - let cap_iter = - splits.into_iter().skip(2).step_by(2); - for (address, cap) in address_iter - .zip(cap_iter) - .map(|(addr, cap)| { - ( - addr.to_owned().parse().map_err( - |e: crate::types::address::DecodeError| { - e.to_string() - }, - ), - cap.parse::().map_err(|e| e.to_string()), - ) - }) - { - set.insert((address?, cap?.into())); - } - - Ok(Self::Yay(VoteType::PGFCouncil(set))) - } else { - // ETH Bridge vote - let signature = serde_json::from_str(token) - .map_err(|e| e.to_string())?; - - Ok(Self::Yay(VoteType::ETHBridge(signature))) - } - } - None => { - // Default vote - Ok(Self::Yay(VoteType::Default)) - } - } - } - "nay" => match iter.next() { - Some(t) => { - Err(format!("Unexpected {} argument for Nay vote", t)) - } - None => Ok(Self::Nay), - }, - _ => Err("Expected either yay or nay".to_string()), - }, - None => Err("Expected either yay or nay".to_string()), - } - } -} - impl ProposalVote { /// Check if a vote is yay pub fn is_yay(&self) -> bool { diff --git a/shared/src/ledger/native_vp/governance/mod.rs b/shared/src/ledger/native_vp/governance/mod.rs index f3b44433ce..0b9f365c94 100644 --- a/shared/src/ledger/native_vp/governance/mod.rs +++ b/shared/src/ledger/native_vp/governance/mod.rs @@ -6,7 +6,6 @@ use std::collections::BTreeSet; use namada_core::ledger::governance::storage as gov_storage; use namada_core::ledger::storage; -use namada_core::ledger::storage_api::OptionExt; use namada_core::ledger::vp_env::VpEnv; use namada_core::types::governance::{ProposalVote, VoteType}; use namada_core::types::transaction::governance::ProposalType; From a4de695e78ca59bfc30447373f75f1244095c198 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Mon, 30 Jan 2023 15:25:07 +0100 Subject: [PATCH 31/49] Refactors pgf council vote to HashSet --- apps/src/lib/client/tx.rs | 4 ++-- core/src/types/governance.rs | 6 ++---- documentation/specs/src/base-ledger/governance.md | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index ad2e0035af..0bbbd20e75 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; use std::collections::hash_map::Entry; -use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::env; use std::fmt::Debug; use std::fs::{File, OpenOptions}; @@ -1972,7 +1972,7 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { let splits = pgf.trim().split_ascii_whitespace(); let address_iter = splits.clone().into_iter().step_by(2); let cap_iter = splits.into_iter().skip(1).step_by(2); - let mut set = BTreeSet::new(); + let mut set = HashSet::new(); for (address, cap) in address_iter.zip(cap_iter).map(|(addr, cap)| { ( diff --git a/core/src/types/governance.rs b/core/src/types/governance.rs index ea433a6b7e..fb6f36bfda 100644 --- a/core/src/types/governance.rs +++ b/core/src/types/governance.rs @@ -1,6 +1,6 @@ //! Files defyining the types used in governance. -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::{BTreeMap, HashSet}; use std::fmt::{self, Display}; use borsh::{BorshDeserialize, BorshSerialize}; @@ -25,7 +25,6 @@ pub type Council = (Address, Amount); #[derive( Debug, Clone, - Hash, PartialEq, BorshSerialize, BorshDeserialize, @@ -37,7 +36,7 @@ pub enum VoteType { /// A default vote without Memo Default, /// A vote for the PGF council - PGFCouncil(BTreeSet), + PGFCouncil(HashSet), /// A vote for ETH bridge carrying the signature over the proposed message ETHBridge(Signature), } @@ -45,7 +44,6 @@ pub enum VoteType { #[derive( Debug, Clone, - Hash, PartialEq, BorshSerialize, BorshDeserialize, diff --git a/documentation/specs/src/base-ledger/governance.md b/documentation/specs/src/base-ledger/governance.md index 1ee52d6714..4ce75ccbe1 100644 --- a/documentation/specs/src/base-ledger/governance.md +++ b/documentation/specs/src/base-ledger/governance.md @@ -123,7 +123,7 @@ pub enum ProposalType { - Doesn't carry any wasm code - Allows both validators and delegators to vote - Requires 1/3 of the total voting power to vote `Yay` -- Expect every vote to carry a memo in the form of a tuple `(Set
, BudgetCap)` +- Expect every vote to carry a memo in the form of `Set<(Address, BudgetCap)>` `ETHBridge` is aimed at regulating actions on the bridge like the update of the Ethereum smart contracts or the withdrawing of all the funds from the `Vault` : From e1e31304a9eb3bd4ba9271818dc9aeec4e3c8f21 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Mon, 30 Jan 2023 16:21:09 +0100 Subject: [PATCH 32/49] Adds e2e test for eth proposal --- tests/src/e2e/ledger_tests.rs | 233 +++++++++++++++++++++++++++++++++- 1 file changed, 232 insertions(+), 1 deletion(-) diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 0e745d4353..86695dfa8a 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -30,7 +30,9 @@ use namada_apps::config::genesis::genesis_config::{ use serde_json::json; use setup::constants::*; -use super::helpers::{get_height, is_debug_mode, wait_for_block_height}; +use super::helpers::{ + find_keypair, get_height, is_debug_mode, wait_for_block_height, +}; use super::setup::get_all_wasms_hashes; use crate::e2e::helpers::{ epoch_sleep, find_address, find_bonded_stake, get_actor_rpc, get_epoch, @@ -2644,6 +2646,235 @@ fn proposal_submission() -> Result<()> { Ok(()) } +/// Test submission and vote of an ETH proposal. +/// +/// 1 - Submit proposal +/// 2 - Vote with delegator and check failure +/// 3 - Vote with validator and check success +/// 4 - Check that proposal passed and funds +#[test] +fn eth_governance_proposal() -> Result<()> { + let test = setup::network( + |genesis| { + let parameters = ParametersConfig { + epochs_per_year: epochs_per_year_from_min_duration(1), + max_proposal_bytes: Default::default(), + min_num_of_blocks: 1, + max_expected_time_per_block: 1, + ..genesis.parameters + }; + + GenesisConfig { + parameters, + ..genesis + } + }, + None, + )?; + + let namadac_help = vec!["--help"]; + + let mut client = run!(test, Bin::Client, namadac_help, Some(40))?; + client.exp_string("Namada client command line interface.")?; + client.assert_success(); + + // Run the ledger node + let mut ledger = + run_as!(test, Who::Validator(0), Bin::Node, &["ledger"], Some(40))?; + + ledger.exp_string("Namada ledger node started")?; + let _bg_ledger = ledger.background(); + + let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); + + // Delegate some token + let tx_args = vec![ + "bond", + "--validator", + "validator-0", + "--source", + BERTHA, + "--amount", + "900", + "--gas-amount", + "0", + "--gas-limit", + "0", + "--gas-token", + NAM, + "--ledger-address", + &validator_one_rpc, + ]; + client = run!(test, Bin::Client, tx_args, Some(40))?; + client.exp_string("Transaction is valid.")?; + client.assert_success(); + + // 1 - Submit proposal + let albert = find_address(&test, ALBERT)?; + let valid_proposal_json_path = + prepare_proposal_data(&test, albert.clone(), ProposalType::ETHBridge); + let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); + + let submit_proposal_args = vec![ + "init-proposal", + "--data-path", + valid_proposal_json_path.to_str().unwrap(), + "--ledger-address", + &validator_one_rpc, + ]; + client = run!(test, Bin::Client, submit_proposal_args, Some(40))?; + client.exp_string("Transaction is valid.")?; + client.assert_success(); + + // Query the proposal + let proposal_query_args = vec![ + "query-proposal", + "--proposal-id", + "0", + "--ledger-address", + &validator_one_rpc, + ]; + + client = run!(test, Bin::Client, proposal_query_args, Some(40))?; + client.exp_string("Proposal: 0")?; + client.assert_success(); + + let proposal_query_args = vec![ + "query-proposal", + "--proposal-id", + "1", + "--ledger-address", + &validator_one_rpc, + ]; + + client = run!(test, Bin::Client, proposal_query_args, Some(40))?; + client.exp_string("Proposal: 1")?; + client.assert_success(); + + // Query token balance proposal author (submitted funds) + let query_balance_args = vec![ + "balance", + "--owner", + ALBERT, + "--token", + NAM, + "--ledger-address", + &validator_one_rpc, + ]; + + client = run!(test, Bin::Client, query_balance_args, Some(40))?; + client.exp_string("NAM: 999500")?; + client.assert_success(); + + // Query token balance governance + let query_balance_args = vec![ + "balance", + "--owner", + GOVERNANCE_ADDRESS, + "--token", + NAM, + "--ledger-address", + &validator_one_rpc, + ]; + + client = run!(test, Bin::Client, query_balance_args, Some(40))?; + client.exp_string("NAM: 500")?; + client.assert_success(); + + //FIXME: try wrong type of vote also in pgf test + // 2 - Vote with delegator and check failure + let mut epoch = get_epoch(&test, &validator_one_rpc).unwrap(); + while epoch.0 <= 13 { + sleep(1); + epoch = get_epoch(&test, &validator_one_rpc).unwrap(); + } + let signing_key = find_keypair(&test, BERTHA)?; + let msg = "0xfd34672ab"; + let vote_arg = format!("{} {}", signing_key, msg); + let submit_proposal_vote_delagator = vec![ + "vote-proposal", + "--proposal-id", + "0", + "--vote", + "yay", + "--eth", + &vote_arg, + "--signer", + BERTHA, + "--ledger-address", + &validator_one_rpc, + ]; + + client = run!(test, Bin::Client, submit_proposal_vote_delagator, Some(40))?; + client.exp_string("Transaction is invalid.")?; + client.assert_success(); + + // 3 - Send a yay vote from a validator + let signing_key = find_keypair(&test, "validator-0")?; + let vote_arg = format!("{} {}", signing_key, msg); + + let submit_proposal_vote = vec![ + "vote-proposal", + "--proposal-id", + "0", + "--vote", + "yay", + "--eth", + &vote_arg, + "--signer", + "validator-0", + "--ledger-address", + &validator_one_rpc, + ]; + + client = run_as!( + test, + Who::Validator(0), + Bin::Client, + submit_proposal_vote, + Some(15) + )?; + client.exp_string("Transaction is valid.")?; + client.assert_success(); + + // 4 - Wait proposals grace and check proposal author funds + while epoch.0 < 31 { + sleep(1); + epoch = get_epoch(&test, &validator_one_rpc).unwrap(); + } + + let query_balance_args = vec![ + "balance", + "--owner", + ALBERT, + "--token", + NAM, + "--ledger-address", + &validator_one_rpc, + ]; + + client = run!(test, Bin::Client, query_balance_args, Some(30))?; + client.exp_string("NAM: 999500")?; + client.assert_success(); + + // Check if governance funds are 0 + let query_balance_args = vec![ + "balance", + "--owner", + GOVERNANCE_ADDRESS, + "--token", + NAM, + "--ledger-address", + &validator_one_rpc, + ]; + + client = run!(test, Bin::Client, query_balance_args, Some(30))?; + client.exp_string("NAM: 0")?; + client.assert_success(); + + Ok(()) +} + /// Test submission and vote of a PGF proposal /// /// 1 - Sumbit two proposals From 7749a649076a8fe52665de31537e3bca222e594f Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Mon, 30 Jan 2023 17:02:02 +0100 Subject: [PATCH 33/49] Fixes pgf e2e test and governance vp --- .github/workflows/scripts/e2e.json | 1 + shared/src/ledger/native_vp/governance/mod.rs | 8 ++---- tests/src/e2e/ledger_tests.rs | 26 ++++++++----------- 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/.github/workflows/scripts/e2e.json b/.github/workflows/scripts/e2e.json index 75671da802..a6900183dd 100644 --- a/.github/workflows/scripts/e2e.json +++ b/.github/workflows/scripts/e2e.json @@ -12,6 +12,7 @@ "e2e::ledger_tests::pos_init_validator": 15, "e2e::ledger_tests::proposal_offline": 15, "e2e::ledger_tests::pgf_governance_proposal": 35, + "e2e::ledger_tests::eth_governance_proposal": 35, "e2e::ledger_tests::proposal_submission": 35, "e2e::ledger_tests::run_ledger": 5, "e2e::ledger_tests::run_ledger_load_state_and_reset": 5, diff --git a/shared/src/ledger/native_vp/governance/mod.rs b/shared/src/ledger/native_vp/governance/mod.rs index 0b9f365c94..1f6434dfef 100644 --- a/shared/src/ledger/native_vp/governance/mod.rs +++ b/shared/src/ledger/native_vp/governance/mod.rs @@ -323,12 +323,8 @@ where // Check that the proposal type admits wasm code match proposal_type { - Some(proposal_type) => { - if let ProposalType::PGFCouncil = proposal_type { - return Ok(false); - } - } - None => return Ok(false), + Some(ProposalType::Default(_)) => (), + _ => return Ok(false), } let code_key = gov_storage::get_proposal_code_key(proposal_id); diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 86695dfa8a..a210d6bb78 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -2739,18 +2739,6 @@ fn eth_governance_proposal() -> Result<()> { client.exp_string("Proposal: 0")?; client.assert_success(); - let proposal_query_args = vec![ - "query-proposal", - "--proposal-id", - "1", - "--ledger-address", - &validator_one_rpc, - ]; - - client = run!(test, Bin::Client, proposal_query_args, Some(40))?; - client.exp_string("Proposal: 1")?; - client.assert_success(); - // Query token balance proposal author (submitted funds) let query_balance_args = vec![ "balance", @@ -3035,14 +3023,16 @@ fn pgf_governance_proposal() -> Result<()> { } let albert_address = find_address(&test, ALBERT)?; - let vote = format!("yay {} 1000", albert_address); + let arg_vote = format!("{} 1000", albert_address); let submit_proposal_vote = vec![ "vote-proposal", "--proposal-id", "0", "--vote", - &vote, + "yay", + "--pgf", + &arg_vote, "--signer", "validator-0", "--ledger-address", @@ -3060,12 +3050,14 @@ fn pgf_governance_proposal() -> Result<()> { client.assert_success(); // Send different yay vote from delegator to check majority on 1/3 - let different_vote = format!("yay {} 900", albert_address); + let different_vote = format!("{} 900", albert_address); let submit_proposal_vote_delagator = vec![ "vote-proposal", "--proposal-id", "0", "--vote", + "yay", + "--pgf", &different_vote, "--signer", BERTHA, @@ -3083,6 +3075,10 @@ fn pgf_governance_proposal() -> Result<()> { "--proposal-id", "1", "--vote", + "yay", + "--pgf", + "yay", + "--pgf", &different_vote, "--signer", BERTHA, From 15289315843b74a9b93ff1ee139ac99805b7f856 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Mon, 30 Jan 2023 17:44:22 +0100 Subject: [PATCH 34/49] Fixes typos in e2e tests --- tests/src/e2e/ledger_tests.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index a210d6bb78..3fd452fb56 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -3077,8 +3077,6 @@ fn pgf_governance_proposal() -> Result<()> { "--vote", "yay", "--pgf", - "yay", - "--pgf", &different_vote, "--signer", BERTHA, From 6c112dedbd78dfaca110e8c16e1023264f02a9d0 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Mon, 30 Jan 2023 18:50:20 +0100 Subject: [PATCH 35/49] Generates keypair for eth proposal e2e test --- tests/src/e2e/ledger_tests.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 3fd452fb56..f4b4374d30 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -2776,7 +2776,15 @@ fn eth_governance_proposal() -> Result<()> { sleep(1); epoch = get_epoch(&test, &validator_one_rpc).unwrap(); } - let signing_key = find_keypair(&test, BERTHA)?; + + use namada::types::key::{self, secp256k1, SigScheme}; + use rand::prelude::ThreadRng; + use rand::thread_rng; + + // Generate a signing key to sign the eth message to sign the eth message to sign the eth message + let mut rng: ThreadRng = thread_rng(); + let node_sk = secp256k1::SigScheme::generate(&mut rng); + let signing_key = key::common::SecretKey::Secp256k1(node_sk); let msg = "0xfd34672ab"; let vote_arg = format!("{} {}", signing_key, msg); let submit_proposal_vote_delagator = vec![ @@ -2798,7 +2806,6 @@ fn eth_governance_proposal() -> Result<()> { client.assert_success(); // 3 - Send a yay vote from a validator - let signing_key = find_keypair(&test, "validator-0")?; let vote_arg = format!("{} {}", signing_key, msg); let submit_proposal_vote = vec![ From 5344964f469b2f63f87e70ddff4609f28ce7e0d0 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Mon, 30 Jan 2023 19:42:16 +0100 Subject: [PATCH 36/49] Fixes length of hex message in e2e tests --- tests/src/e2e/ledger_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index f4b4374d30..93dab04059 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -2785,7 +2785,7 @@ fn eth_governance_proposal() -> Result<()> { let mut rng: ThreadRng = thread_rng(); let node_sk = secp256k1::SigScheme::generate(&mut rng); let signing_key = key::common::SecretKey::Secp256k1(node_sk); - let msg = "0xfd34672ab"; + let msg = "fd34672ab5"; let vote_arg = format!("{} {}", signing_key, msg); let submit_proposal_vote_delagator = vec![ "vote-proposal", From 2317df4db6cb26e156bcdc3de4b40a15e62a2df4 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Mon, 30 Jan 2023 23:03:02 +0100 Subject: [PATCH 37/49] Fixes nam value in governance eth test --- tests/src/e2e/ledger_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 93dab04059..9db74239a9 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -2849,7 +2849,7 @@ fn eth_governance_proposal() -> Result<()> { ]; client = run!(test, Bin::Client, query_balance_args, Some(30))?; - client.exp_string("NAM: 999500")?; + client.exp_string("NAM: 1000000")?; client.assert_success(); // Check if governance funds are 0 From 418a854add4377bb2a0f760001f84bc7263a5fb0 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Tue, 31 Jan 2023 10:58:44 +0100 Subject: [PATCH 38/49] Removes comment --- tests/src/e2e/ledger_tests.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 9db74239a9..23a8214697 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -2769,7 +2769,6 @@ fn eth_governance_proposal() -> Result<()> { client.exp_string("NAM: 500")?; client.assert_success(); - //FIXME: try wrong type of vote also in pgf test // 2 - Vote with delegator and check failure let mut epoch = get_epoch(&test, &validator_one_rpc).unwrap(); while epoch.0 <= 13 { From 24ad2340492633a5b44f78436d9ba440920d30ab Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Tue, 31 Jan 2023 11:36:53 +0100 Subject: [PATCH 39/49] Clippy + fmt --- apps/src/lib/cli.rs | 42 ++++++++++++------- apps/src/lib/client/tx.rs | 2 +- apps/src/lib/node/ledger/shell/governance.rs | 6 ++- shared/src/ledger/native_vp/governance/mod.rs | 3 +- .../src/ledger/native_vp/governance/utils.rs | 9 ++-- tests/src/e2e/ledger_tests.rs | 9 ++-- 6 files changed, 42 insertions(+), 29 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 1a03e86003..e851120240 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2308,7 +2308,7 @@ pub mod args { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); let proposal_id = PROPOSAL_ID_OPT.parse(matches); - let proposal_pgf = PROPOSAL_VOTE_PGF_OPT.parse(matches); + let proposal_pgf = PROPOSAL_VOTE_PGF_OPT.parse(matches); let proposal_eth = PROPOSAL_VOTE_ETH_OPT.parse(matches); let vote = PROPOSAL_VOTE.parse(matches); let offline = PROPOSAL_OFFLINE.parse(matches); @@ -2336,21 +2336,33 @@ pub mod args { DATA_PATH_OPT.name, ]), ) - .arg(PROPOSAL_VOTE.def().about( - "The vote for the proposal. Either yay or nay" - )) - .arg(PROPOSAL_VOTE_PGF_OPT.def().about("The list of proposed councils and spending caps:\n$council1 $cap1 $council2 $cap2 ... (council is bech32m encoded, cap is expressed in microNAM") - .requires(PROPOSAL_ID.name) - .conflicts_with( - - PROPOSAL_VOTE_ETH_OPT.name - ) + .arg( + PROPOSAL_VOTE + .def() + .about("The vote for the proposal. Either yay or nay"), + ) + .arg( + PROPOSAL_VOTE_PGF_OPT + .def() + .about( + "The list of proposed councils and spending \ + caps:\n$council1 $cap1 $council2 $cap2 ... \ + (council is bech32m encoded, cap is expressed in \ + microNAM", + ) + .requires(PROPOSAL_ID.name) + .conflicts_with(PROPOSAL_VOTE_ETH_OPT.name), + ) + .arg( + PROPOSAL_VOTE_ETH_OPT + .def() + .about( + "The signing key and message bytes (hex encoded) \ + to be signed: $signing_key $message", + ) + .requires(PROPOSAL_ID.name) + .conflicts_with(PROPOSAL_VOTE_PGF_OPT.name), ) - .arg(PROPOSAL_VOTE_ETH_OPT.def().about("The signing key and message bytes (hex encoded) to be signed: $signing_key $message") - .requires(PROPOSAL_ID.name) - .conflicts_with( - PROPOSAL_VOTE_PGF_OPT.name - )) .arg( PROPOSAL_OFFLINE .def() diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 0bbbd20e75..96998ebc18 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1990,7 +1990,7 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { ProposalVote::Yay(VoteType::PGFCouncil(set)) } else if let Some(eth) = args.proposal_eth { let mut splits = eth.trim().split_ascii_whitespace(); - //Sign the message + // Sign the message let sigkey = splits .next() .expect("Expected signing key") diff --git a/apps/src/lib/node/ledger/shell/governance.rs b/apps/src/lib/node/ledger/shell/governance.rs index f1493be938..b9e2bcc19a 100644 --- a/apps/src/lib/node/ledger/shell/governance.rs +++ b/apps/src/lib/node/ledger/shell/governance.rs @@ -209,7 +209,8 @@ where } } Tally::PGFCouncil(council) => { - // TODO: implement when PGF is in place, update the PGF council in storage + // TODO: implement when PGF is in place, update the PGF + // council in storage let proposal_event: Event = ProposalEvent::new( EventType::Proposal.to_string(), TallyResult::Passed(Tally::PGFCouncil(council)), @@ -234,7 +235,8 @@ where })? } Tally::ETHBridge => { - //TODO: implement when ETH Bridge. Apply the modification requested by the proposal + // TODO: implement when ETH Bridge. Apply the + // modification requested by the proposal let proposal_event: Event = ProposalEvent::new( EventType::Proposal.to_string(), TallyResult::Passed(Tally::ETHBridge), diff --git a/shared/src/ledger/native_vp/governance/mod.rs b/shared/src/ledger/native_vp/governance/mod.rs index 1f6434dfef..022103d499 100644 --- a/shared/src/ledger/native_vp/governance/mod.rs +++ b/shared/src/ledger/native_vp/governance/mod.rs @@ -236,7 +236,8 @@ where } } } else if let VoteType::ETHBridge(_sig) = vote_type { - // TODO: Check the validity of the signature with the governance ETH key in storage for the given validator + // TODO: Check the validity of the signature with the + // governance ETH key in storage for the given validator } } diff --git a/shared/src/ledger/native_vp/governance/utils.rs b/shared/src/ledger/native_vp/governance/utils.rs index ef3db6561d..0bb2088165 100644 --- a/shared/src/ledger/native_vp/governance/utils.rs +++ b/shared/src/ledger/native_vp/governance/utils.rs @@ -100,8 +100,7 @@ pub fn compute_tally( } else { return ProposalResult { result: TallyResult::Failed(format!( - "Unexpected vote type. Expected: {}, Found: \ - {}", + "Unexpected vote type. Expected: {}, Found: {}", proposal_type, validator_vote )), total_voting_power: total_stake, @@ -112,8 +111,7 @@ pub fn compute_tally( } else { return ProposalResult { result: TallyResult::Failed(format!( - "Unexpected vote type. Expected: {}, Found: \ - {}", + "Unexpected vote type. Expected: {}, Found: {}", proposal_type, validator_vote )), total_voting_power: total_stake, @@ -371,7 +369,8 @@ pub fn compute_tally( total_nay_power: 0, }, _ => { - // Select the winner council based on approval voting (majority) + // Select the winner council based on approval voting + // (majority) let council = total_yay_staked_tokens .into_iter() .max_by(|a, b| a.1.cmp(&b.1)) diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 23a8214697..7e62ed53ac 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -30,9 +30,7 @@ use namada_apps::config::genesis::genesis_config::{ use serde_json::json; use setup::constants::*; -use super::helpers::{ - find_keypair, get_height, is_debug_mode, wait_for_block_height, -}; +use super::helpers::{get_height, is_debug_mode, wait_for_block_height}; use super::setup::get_all_wasms_hashes; use crate::e2e::helpers::{ epoch_sleep, find_address, find_bonded_stake, get_actor_rpc, get_epoch, @@ -2712,7 +2710,7 @@ fn eth_governance_proposal() -> Result<()> { // 1 - Submit proposal let albert = find_address(&test, ALBERT)?; let valid_proposal_json_path = - prepare_proposal_data(&test, albert.clone(), ProposalType::ETHBridge); + prepare_proposal_data(&test, albert, ProposalType::ETHBridge); let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); let submit_proposal_args = vec![ @@ -2780,7 +2778,8 @@ fn eth_governance_proposal() -> Result<()> { use rand::prelude::ThreadRng; use rand::thread_rng; - // Generate a signing key to sign the eth message to sign the eth message to sign the eth message + // Generate a signing key to sign the eth message to sign the eth message to + // sign the eth message let mut rng: ThreadRng = thread_rng(); let node_sk = secp256k1::SigScheme::generate(&mut rng); let signing_key = key::common::SecretKey::Secp256k1(node_sk); From 77a8c298080d694dfcd7a3540245e98a76fd7ff2 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Tue, 31 Jan 2023 12:07:12 +0100 Subject: [PATCH 40/49] Fixes vote memo in governance docs --- .../docs/src/user-guide/ledger/on-chain-governance.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/documentation/docs/src/user-guide/ledger/on-chain-governance.md b/documentation/docs/src/user-guide/ledger/on-chain-governance.md index 325a6ae4bd..499e746d7e 100644 --- a/documentation/docs/src/user-guide/ledger/on-chain-governance.md +++ b/documentation/docs/src/user-guide/ledger/on-chain-governance.md @@ -69,11 +69,10 @@ Only validators and delegators can vote. Assuming you have a validator or a dele namada client vote-proposal \ --proposal-id 0 \ --vote yay \ - --memo path \ --signer validator ``` -where `--vote` can be either `yay` or `nay`. The optional `memo` field represents the path to a json file econding the data to attach to the vote. +where `--vote` can be either `yay` or `nay`. An optional `memo` field can be attached to the vote for pgf and eth bridge proposals. ## Check the result From 2d69bf3ae809447c2d3f870a0725c94d6cee9b81 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Wed, 8 Feb 2023 15:46:11 +0100 Subject: [PATCH 41/49] Misc fixes --- apps/src/lib/cli.rs | 2 +- apps/src/lib/client/tx.rs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index e851120240..9666486983 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2347,7 +2347,7 @@ pub mod args { .about( "The list of proposed councils and spending \ caps:\n$council1 $cap1 $council2 $cap2 ... \ - (council is bech32m encoded, cap is expressed in \ + (council is bech32m encoded address, cap is expressed in \ microNAM", ) .requires(PROPOSAL_ID.name) diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 96998ebc18..3de4fe332e 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1966,7 +1966,7 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { }; // Construct vote - let proposal_vote = match args.vote.as_str() { + let proposal_vote = match args.vote.to_ascii_lowercase().as_str() { "yay" => { if let Some(pgf) = args.proposal_pgf { let splits = pgf.trim().split_ascii_whitespace(); @@ -1976,8 +1976,7 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { for (address, cap) in address_iter.zip(cap_iter).map(|(addr, cap)| { ( - addr.to_owned() - .parse() + addr.parse() .expect("Failed to parse pgf council address"), cap.parse::() .expect("Failed to parse pgf spending cap"), From b45deb2478caa12e73a11f9797a749ecb5ff57a3 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Wed, 8 Feb 2023 16:07:35 +0100 Subject: [PATCH 42/49] Continues governance tally if wrong vote type --- .../src/ledger/native_vp/governance/utils.rs | 119 +++++++----------- 1 file changed, 45 insertions(+), 74 deletions(-) diff --git a/shared/src/ledger/native_vp/governance/utils.rs b/shared/src/ledger/native_vp/governance/utils.rs index 0bb2088165..8e9af95d47 100644 --- a/shared/src/ledger/native_vp/governance/utils.rs +++ b/shared/src/ledger/native_vp/governance/utils.rs @@ -98,26 +98,22 @@ pub fn compute_tally( if proposal_type == vote_type { total_yay_staked_tokens += amount; } else { - return ProposalResult { - result: TallyResult::Failed(format!( - "Unexpected vote type. Expected: {}, Found: {}", - proposal_type, validator_vote - )), - total_voting_power: total_stake, - total_yay_power: 0, - total_nay_power: 0, - }; + // Log the error and continue + tracing::error!( + "Unexpected vote type. Expected: {}, Found: {}", + proposal_type, + validator_vote + ); + continue; } } else { - return ProposalResult { - result: TallyResult::Failed(format!( - "Unexpected vote type. Expected: {}, Found: {}", - proposal_type, validator_vote - )), - total_voting_power: total_stake, - total_yay_power: 0, - total_nay_power: 0, - }; + // Log the error and continue + tracing::error!( + "Unexpected vote type. Expected: {}, Found: {}", + proposal_type, + validator_vote + ); + continue; } } @@ -144,16 +140,13 @@ pub fn compute_tally( } _ => { - return ProposalResult { - result: TallyResult::Failed(format!( - "Unexpected vote type. Expected: {}, \ - Found: {}", - proposal_type, delegator_vote - )), - total_voting_power: total_stake, - total_yay_power: 0, - total_nay_power: 0, - }; + // Log the error and continue + tracing::error!( + "Unexpected vote type. Expected: {}, Found: {}", + proposal_type, + delegator_vote + ); + continue; } } } @@ -200,16 +193,12 @@ pub fn compute_tally( amount; } } else { - return ProposalResult { - result: TallyResult::Failed(format!( - "Unexpected vote type. Expected: PGFCouncil, \ - Found: {}", - validator_vote - )), - total_voting_power: total_stake, - total_yay_power: 0, - total_nay_power: 0, - }; + // Log the error and continue + tracing::error!( + "Unexpected vote type. Expected: PGFCouncil, Found: {}", + validator_vote + ); + continue; } } @@ -259,19 +248,12 @@ pub fn compute_tally( } } } else { - return ProposalResult { - result: TallyResult::Failed( - format!( - "Unexpected vote type. \ - Expected: PGFCouncil, \ - Found: {}", - validator_vote - ), - ), - total_voting_power: total_stake, - total_yay_power: 0, - total_nay_power: 0, - }; + // Log the error and continue + tracing::error!( + "Unexpected vote type. Expected: PGFCouncil, Found: {}", + validator_vote + ); + continue; } } None => { @@ -323,34 +305,23 @@ pub fn compute_tally( } } } else { - return ProposalResult { - result: TallyResult::Failed( - format!( - "Unexpected vote type. \ - Expected: PGFCouncil, \ - Found: {}", - validator_vote - ), - ), - total_voting_power: total_stake, - total_yay_power: 0, - total_nay_power: 0, - }; + // Log the error and continue + tracing::error!( + "Unexpected vote type. Expected: PGFCouncil, Found: {}", + validator_vote + ); + continue; } } } } _ => { - return ProposalResult { - result: TallyResult::Failed(format!( - "Unexpected vote type. Expected: \ - PGFCouncil, Found: {}", - delegator_vote - )), - total_voting_power: total_stake, - total_yay_power: 0, - total_nay_power: 0, - }; + // Log the error and continue + tracing::error!( + "Unexpected vote type. Expected: PGFCouncil, Found: {}", + delegator_vote + ); + continue; } } } From d2e7cd6361c42411716d71c3f29a352a1055a14d Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Wed, 8 Feb 2023 16:50:04 +0100 Subject: [PATCH 43/49] Replaces `TallyResult::Failed` with `Error` --- apps/src/lib/client/rpc.rs | 67 ++++++++++++------- apps/src/lib/node/ledger/shell/governance.rs | 30 +-------- core/src/types/governance.rs | 3 - .../src/ledger/native_vp/governance/utils.rs | 53 +++++++-------- 4 files changed, 71 insertions(+), 82 deletions(-) diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 24b53bf641..82326c51ac 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -795,22 +795,32 @@ pub async fn query_proposal(_ctx: Context, args: args::QueryProposal) { println!("{:4}Status: pending", ""); } else if start_epoch <= current_epoch && current_epoch <= end_epoch { - let partial_proposal_result = - utils::compute_tally(votes, total_stake, &proposal_type); - println!( - "{:4}Yay votes: {}", - "", partial_proposal_result.total_yay_power - ); - println!( - "{:4}Nay votes: {}", - "", partial_proposal_result.total_nay_power - ); - println!("{:4}Status: on-going", ""); + match utils::compute_tally(votes, total_stake, &proposal_type) { + Ok(partial_proposal_result) => { + println!( + "{:4}Yay votes: {}", + "", partial_proposal_result.total_yay_power + ); + println!( + "{:4}Nay votes: {}", + "", partial_proposal_result.total_nay_power + ); + println!("{:4}Status: on-going", ""); + } + Err(msg) => { + eprintln!("Error in tally computation: {}", msg) + } + } } else { - let proposal_result = - utils::compute_tally(votes, total_stake, &proposal_type); - println!("{:4}Status: done", ""); - println!("{:4}Result: {}", "", proposal_result); + match utils::compute_tally(votes, total_stake, &proposal_type) { + Ok(proposal_result) => { + println!("{:4}Status: done", ""); + println!("{:4}Result: {}", "", proposal_result); + } + Err(msg) => { + eprintln!("Error in tally computation: {}", msg) + } + } } } else { println!("Proposal: {}", id); @@ -1204,13 +1214,19 @@ pub async fn query_proposal_result( get_total_staked_tokens(&client, end_epoch) .await .into(); - let proposal_result = utils::compute_tally( + println!("Proposal: {}", id); + match utils::compute_tally( votes, total_stake, &proposal_type, - ); - println!("Proposal: {}", id); - println!("{:4}Result: {}", "", proposal_result); + ) { + Ok(proposal_result) => { + println!("{:4}Result: {}", "", proposal_result) + } + Err(msg) => { + eprintln!("Error in tally computation: {}", msg) + } + } } else { eprintln!("Proposal is still in progress."); cli::safe_exit(1) @@ -1306,13 +1322,18 @@ pub async fn query_proposal_result( ) .await .into(); - let proposal_result = utils::compute_tally( + match utils::compute_tally( votes, total_stake, &ProposalType::Default(None), - ); - - println!("{:4}Result: {}", "", proposal_result); + ) { + Ok(proposal_result) => { + println!("{:4}Result: {}", "", proposal_result) + } + Err(msg) => { + eprintln!("Error in tally computation: {}", msg) + } + } } None => { eprintln!( diff --git a/apps/src/lib/node/ledger/shell/governance.rs b/apps/src/lib/node/ledger/shell/governance.rs index b9e2bcc19a..9efc7d1697 100644 --- a/apps/src/lib/node/ledger/shell/governance.rs +++ b/apps/src/lib/node/ledger/shell/governance.rs @@ -68,8 +68,9 @@ where read_total_stake(&shell.wl_storage, ¶ms, proposal_end_epoch) .map_err(|msg| Error::BadProposal(id, msg.to_string()))?; let total_stake = VotePower::from(u64::from(total_stake)); - let tally_result = - compute_tally(votes, total_stake, &proposal_type).result; + let tally_result = compute_tally(votes, total_stake, &proposal_type) + .map_err(|msg| Error::BadProposal(id, msg.to_string()))? + .result; // Execute proposal if succesful let transfer_address = match tally_result { @@ -276,31 +277,6 @@ where slash_fund_address } - TallyResult::Failed(msg) => { - tracing::error!( - "Unexpectedly failed to tally proposal ID {id} with error \ - {msg}" - ); - let proposal_event: Event = ProposalEvent::new( - EventType::Proposal.to_string(), - TallyResult::Failed(msg), - id, - false, - false, - ) - .into(); - response.events.push(proposal_event); - - let proposal_author_key = gov_storage::get_author_key(id); - shell - .read_storage_key::
(&proposal_author_key) - .ok_or_else(|| { - Error::BadProposal( - id, - "Invalid proposal author.".to_string(), - ) - })? - } }; let native_token = shell.wl_storage.storage.native_token.clone(); diff --git a/core/src/types/governance.rs b/core/src/types/governance.rs index fb6f36bfda..dc17d07e22 100644 --- a/core/src/types/governance.rs +++ b/core/src/types/governance.rs @@ -124,8 +124,6 @@ pub enum TallyResult { Passed(Tally), /// Proposal was rejected Rejected, - /// A critical error in tally computation with an error message - Failed(String), } /// The result with votes of a proposal @@ -171,7 +169,6 @@ impl Display for TallyResult { ), }, TallyResult::Rejected => write!(f, "rejected"), - TallyResult::Failed(msg) => write!(f, "failed with: {}", msg), } } } diff --git a/shared/src/ledger/native_vp/governance/utils.rs b/shared/src/ledger/native_vp/governance/utils.rs index 8e9af95d47..050e762009 100644 --- a/shared/src/ledger/native_vp/governance/utils.rs +++ b/shared/src/ledger/native_vp/governance/utils.rs @@ -40,6 +40,9 @@ pub enum Error { /// Invalid proposal field deserialization #[error("Invalid proposal {0}")] InvalidProposal(u64), + /// Error during tally + #[error("Error while tallying proposal: {0}")] + Tally(String), } /// Proposal event definition @@ -83,7 +86,7 @@ pub fn compute_tally( votes: Votes, total_stake: VotePower, proposal_type: &ProposalType, -) -> ProposalResult { +) -> Result { let Votes { yay_validators, delegators, @@ -161,25 +164,27 @@ pub fn compute_tally( ProposalType::ETHBridge => { TallyResult::Passed(Tally::ETHBridge) } - _ => TallyResult::Failed(format!( - "Unexpected proposal type {}", - proposal_type - )), + _ => { + return Err(Error::Tally(format!( + "Unexpected proposal type: {}", + proposal_type + ))) + } }; - ProposalResult { + Ok(ProposalResult { result: tally_result, total_voting_power: total_stake, total_yay_power: total_yay_staked_tokens, total_nay_power: 0, - } + }) } else { - ProposalResult { + Ok(ProposalResult { result: TallyResult::Rejected, total_voting_power: total_stake, total_yay_power: total_yay_staked_tokens, total_nay_power: 0, - } + }) } } ProposalType::PGFCouncil => { @@ -233,11 +238,7 @@ pub fn compute_tally( { *power -= vote_power; } else { - return ProposalResult { - result: TallyResult::Failed(format!("Expected PGF vote {:?} was not in tally", vote)), - total_voting_power: total_stake, - total_yay_power: 0, - total_nay_power: 0}; + return Err(Error::Tally(format!("Expected PGF vote {:?} was not in tally", vote))); } } else { // Validator didn't vote for @@ -288,20 +289,14 @@ pub fn compute_tally( { *power -= vote_power; } else { - return ProposalResult { - result: TallyResult::Failed( - format!( - "Expected PGF \ + return Err(Error::Tally( + format!( + "Expected PGF \ vote {:?} was \ not in tally", - vote - ), + vote ), - total_voting_power: - total_stake, - total_yay_power: 0, - total_nay_power: 0, - }; + )); } } } else { @@ -333,12 +328,12 @@ pub fn compute_tally( .fold(0, |acc, (_, vote_power)| acc + vote_power); match total_yay_voted_power.checked_mul(3) { - Some(v) if v < total_stake => ProposalResult { + Some(v) if v < total_stake => Ok(ProposalResult { result: TallyResult::Rejected, total_voting_power: total_stake, total_yay_power: total_yay_voted_power, total_nay_power: 0, - }, + }), _ => { // Select the winner council based on approval voting // (majority) @@ -348,12 +343,12 @@ pub fn compute_tally( .map(|(vote, _)| vote.to_owned()) .unwrap(); // Cannot be None at this point - ProposalResult { + Ok(ProposalResult { result: TallyResult::Passed(Tally::PGFCouncil(council)), total_voting_power: total_stake, total_yay_power: total_yay_voted_power, total_nay_power: 0, - } + }) } } } From b13f715bcfcf588b78d23dd19f1575a8d13c002d Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Wed, 8 Feb 2023 16:57:09 +0100 Subject: [PATCH 44/49] Removes unwrap from `compute_tally` --- shared/src/ledger/native_vp/governance/utils.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/shared/src/ledger/native_vp/governance/utils.rs b/shared/src/ledger/native_vp/governance/utils.rs index 050e762009..18debd1c5a 100644 --- a/shared/src/ledger/native_vp/governance/utils.rs +++ b/shared/src/ledger/native_vp/governance/utils.rs @@ -341,7 +341,11 @@ pub fn compute_tally( .into_iter() .max_by(|a, b| a.1.cmp(&b.1)) .map(|(vote, _)| vote.to_owned()) - .unwrap(); // Cannot be None at this point + .ok_or_else(|| { + Error::Tally( + "Missing expected elected council".to_string(), + ) + })?; Ok(ProposalResult { result: TallyResult::Passed(Tally::PGFCouncil(council)), From f7fe4284d1c492bc294626e509aa279915340720 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Wed, 8 Feb 2023 17:27:58 +0100 Subject: [PATCH 45/49] Fmt --- apps/src/lib/cli.rs | 4 +- .../lib/node/ledger/shell/finalize_block.rs | 9 +++-- .../src/ledger/native_vp/governance/utils.rs | 37 ++++++++++++------- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 9666486983..a04bc8df4f 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2347,8 +2347,8 @@ pub mod args { .about( "The list of proposed councils and spending \ caps:\n$council1 $cap1 $council2 $cap2 ... \ - (council is bech32m encoded address, cap is expressed in \ - microNAM", + (council is bech32m encoded address, cap is \ + expressed in microNAM", ) .requires(PROPOSAL_ID.name) .conflicts_with(PROPOSAL_VOTE_ETH_OPT.name), diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index e37f7bb27b..02dbc4648e 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -469,7 +469,7 @@ mod test_finalize_block { use namada::types::storage::Epoch; use namada::types::time::DurationSecs; use namada::types::transaction::governance::{ - InitProposalData, VoteProposalData, + InitProposalData, ProposalType, VoteProposalData, }; use namada::types::transaction::{EncryptionKey, Fee, WrapperTx, MIN_FEE}; @@ -854,7 +854,7 @@ mod test_finalize_block { voting_start_epoch: Epoch::default(), voting_end_epoch: Epoch::default().next(), grace_epoch: Epoch::default().next(), - proposal_code: None, + r#type: ProposalType::Default(None), }; storage_api::governance::init_proposal( &mut shell.wl_storage, @@ -873,7 +873,10 @@ mod test_finalize_block { .unwrap(); }; // Add a proposal to be accepted and one to be rejected. - add_proposal(0, ProposalVote::Yay); + add_proposal( + 0, + ProposalVote::Yay(namada::types::governance::VoteType::Default), + ); add_proposal(1, ProposalVote::Nay); // Commit the genesis state diff --git a/shared/src/ledger/native_vp/governance/utils.rs b/shared/src/ledger/native_vp/governance/utils.rs index 18debd1c5a..2511db46c9 100644 --- a/shared/src/ledger/native_vp/governance/utils.rs +++ b/shared/src/ledger/native_vp/governance/utils.rs @@ -168,7 +168,7 @@ pub fn compute_tally( return Err(Error::Tally(format!( "Unexpected proposal type: {}", proposal_type - ))) + ))); } }; @@ -238,7 +238,14 @@ pub fn compute_tally( { *power -= vote_power; } else { - return Err(Error::Tally(format!("Expected PGF vote {:?} was not in tally", vote))); + return Err(Error::Tally( + format!( + "Expected PGF \ + vote {:?} was \ + not in tally", + vote + ), + )); } } else { // Validator didn't vote for @@ -251,9 +258,10 @@ pub fn compute_tally( } else { // Log the error and continue tracing::error!( - "Unexpected vote type. Expected: PGFCouncil, Found: {}", - validator_vote - ); + "Unexpected vote type. Expected: \ + PGFCouncil, Found: {}", + validator_vote + ); continue; } } @@ -291,9 +299,8 @@ pub fn compute_tally( } else { return Err(Error::Tally( format!( - "Expected PGF \ - vote {:?} was \ - not in tally", + "Expected PGF vote \ + {:?} was not in tally", vote ), )); @@ -302,9 +309,10 @@ pub fn compute_tally( } else { // Log the error and continue tracing::error!( - "Unexpected vote type. Expected: PGFCouncil, Found: {}", - validator_vote - ); + "Unexpected vote type. Expected: \ + PGFCouncil, Found: {}", + validator_vote + ); continue; } } @@ -313,9 +321,10 @@ pub fn compute_tally( _ => { // Log the error and continue tracing::error!( - "Unexpected vote type. Expected: PGFCouncil, Found: {}", - delegator_vote - ); + "Unexpected vote type. Expected: PGFCouncil, \ + Found: {}", + delegator_vote + ); continue; } } From 8e76e68a173f1f1755b8da96dd6c05b0902577c2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 11 Feb 2023 13:39:45 +0000 Subject: [PATCH 46/49] [ci] wasm checksums update --- wasm/checksums.json | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/wasm/checksums.json b/wasm/checksums.json index 7c2b824921..27a65383d6 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,20 +1,20 @@ { - "tx_bond.wasm": "tx_bond.f0094b887c57565472bede01d98fb77f6faac2f72597e2efb2ebfe9b1bf7c234.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.02dca468021b1ec811d0f35cc4b55a24f7c3f7b5e51f16399709257421f4a1f4.wasm", - "tx_ibc.wasm": "tx_ibc.a1735e3221f1ae055c74bb52327765dd37e8676e15fab496f9ab0ed4d0628f51.wasm", - "tx_init_account.wasm": "tx_init_account.7b6eafeceb81b679c382279a5d9c40dfd81fcf37e5a1940340355c9f55af1543.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.f2ed71fe70fc564e1d67e4e7d2ea25466327b62ba2eee18ece0021abff9e2c82.wasm", - "tx_init_validator.wasm": "tx_init_validator.fedcfaecaf37e3e7d050c76a4512baa399fc528710a27038573df53596613a2c.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.3e5417561e8108d4045775bf6d095cbaad22c73ff17a5ba2ad11a1821665a58a.wasm", - "tx_transfer.wasm": "tx_transfer.833a3849ca2c417f4e907c95c6eb15e6b52827458cf603e1c4f5511ab3e4fe76.wasm", - "tx_unbond.wasm": "tx_unbond.d4fd6c94abb947533a2728940b43fb021a008ad593c7df7a3886c4260cac39b5.wasm", - "tx_update_vp.wasm": "tx_update_vp.6d1eabab15dc6d04eec5b25ad687f026a4d6c3f118a1d7aca9232394496bd845.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.54b594f686a72869b0d7f15492591854f26e287a1cf3b6e543b0246d5ac61003.wasm", - "tx_withdraw.wasm": "tx_withdraw.342c222d0707eb5b5a44b89fc1245f527be3fdf841af64152a9ab35a4766e1b5.wasm", - "vp_implicit.wasm": "vp_implicit.73678ac01aa009ac4e0d4a49eecaa19b49cdd3d95f6862a9558c9b175ae68260.wasm", - "vp_masp.wasm": "vp_masp.85446251f8e1defed81549dab37edfe8e640339c7230e678b65340cf71ce1369.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.573b882a806266d6cdfa635fe803e46d6ce89c99321196c231c61d05193a086d.wasm", - "vp_token.wasm": "vp_token.8c6e5a86f047e7b1f1004f0d8a4e91fad1b1c0226a6e42d7fe350f98dc84359b.wasm", - "vp_user.wasm": "vp_user.75c68f018f163d18d398cb4082b261323d115aae43ec021c868d1128e4b0ee29.wasm", - "vp_validator.wasm": "vp_validator.2dc9f1c8f106deeef5ee988955733955444d16b400ebb16a25e7d71e4b1be874.wasm" + "tx_bond.wasm": "tx_bond.6e07c8d128c0edb32c141950d54f4213b7c8b0bb041b9a9b5a87f869a6e180f4.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.73f057510828b379b4a99527da4cba913222f55557a8e1cca026c0d806f84c4e.wasm", + "tx_ibc.wasm": "tx_ibc.5c8bad91e5642dc50a44942e0a6db4d2f856a51c3c34bca17941c3764eee7688.wasm", + "tx_init_account.wasm": "tx_init_account.416f8c25435798fb98198f2a50159126cd790de21b7cf0d18625f82b699c961a.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.b215f7d55d7484230af7d31b1a884e1f97073502aeeeeafd5f5d857984a20725.wasm", + "tx_init_validator.wasm": "tx_init_validator.a5b0afdb2f4d90d86c91332f28d4304e59ab9f569874e5edf2c1fc6106e83ba6.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.8a876a55eac05d1cc173d4fb4b6b94f24bb72088fda0fdab444d1e9eabbd901d.wasm", + "tx_transfer.wasm": "tx_transfer.edc2b79c63b4a36a148ca93cfa4398f42b4a5c0d9bcc7a1b6f9375b6f61ba8cb.wasm", + "tx_unbond.wasm": "tx_unbond.f322a8c58081b081cc788b0c8fd4229909aa46092871e530bce59eb3dbbc1ec7.wasm", + "tx_update_vp.wasm": "tx_update_vp.9373e973ea84ba200c03e2a3e6bd69999e88828e2cc6f5effa6c564d83202272.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.7f16c727ce465e46c7db1b59e1a290450bb61d63d150602a1504f4731c532481.wasm", + "tx_withdraw.wasm": "tx_withdraw.b9886a6d90f39a499d7e45256caea6fda7edf29fe95055835783623b149917ee.wasm", + "vp_implicit.wasm": "vp_implicit.54c801a9642d2268331993c1054b68b40915b926f5b4cbefdfa97b66fe3e994a.wasm", + "vp_masp.wasm": "vp_masp.36190de9d8d459fe3761f0ef2c3a571e2f158e0dc21f575bcfe1e9fa35123ad3.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.dee8fa07f15893b1f5c692ccedf5536a797f87fc5090d67c2eb04d355cc7d67d.wasm", + "vp_token.wasm": "vp_token.9d43763c1308d2628a4f6b820b73eb3c98542e574aa9a230e2d83282a2b1153b.wasm", + "vp_user.wasm": "vp_user.1b949e01e6210071437762c52a98e9a4d350b62a7575ef4ea869598c08d642c8.wasm", + "vp_validator.wasm": "vp_validator.10179404232726984631037cd469fa08c16f2428ba3083a1234fc8bc8731f0c0.wasm" } \ No newline at end of file From 444ebb82f1244475dbd476dc27598342594de1e2 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Thu, 16 Feb 2023 19:31:33 +0100 Subject: [PATCH 47/49] Updates governance user guide --- .../docs/src/user-guide/ledger/on-chain-governance.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/documentation/docs/src/user-guide/ledger/on-chain-governance.md b/documentation/docs/src/user-guide/ledger/on-chain-governance.md index 499e746d7e..7cae5acd58 100644 --- a/documentation/docs/src/user-guide/ledger/on-chain-governance.md +++ b/documentation/docs/src/user-guide/ledger/on-chain-governance.md @@ -39,7 +39,11 @@ You should change the value of: - `voting_start_epoch` with a future epoch (must be a multiple of 3) for which you want the voting to begin - `voting_end_epoch` with an epoch greater than `voting_start_epoch`, a multiple of 3, and by which no further votes will be accepted - `grace_epoch` with an epoch greater than `voting_end_epoch` + 6, in which the proposal, if passed, will come into effect -- `proposal_code_path` with the absolute path of the wasm file to execute (or remove the field completely) +- `type` with the correct type for your proposal, which can be one of the followings: + - `"type": {"Default":null}` for a default proposal without wasm code + - `"type": {"Default":"$PATH_TO_WASM_CODE"}` for a default proposal with an associated wasm code + - `"type": "PGFCouncil"` to initiate a proposal for a new council + - `"type": "ETHBridge"` for an ethereum bridge related proposal As soon as your `proposal.json` file is ready, you can submit the proposal with (making sure to be in the same directory as the `proposal.json` file): From e1a62f30b2c00116f8f78caa5e9a667d51420997 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Fri, 17 Feb 2023 14:16:22 +0100 Subject: [PATCH 48/49] Refactors proposal execution --- apps/src/lib/node/ledger/shell/governance.rs | 325 ++++++++---------- shared/src/ledger/native_vp/governance/mod.rs | 1 + 2 files changed, 142 insertions(+), 184 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/governance.rs b/apps/src/lib/node/ledger/shell/governance.rs index 9efc7d1697..f508622d56 100644 --- a/apps/src/lib/node/ledger/shell/governance.rs +++ b/apps/src/lib/node/ledger/shell/governance.rs @@ -13,7 +13,7 @@ use namada::ledger::storage::{DBIter, StorageHasher, DB}; use namada::ledger::storage_api::{token, StorageWrite}; use namada::proof_of_stake::read_total_stake; use namada::types::address::Address; -use namada::types::governance::{Tally, TallyResult, VotePower}; +use namada::types::governance::{Council, Tally, TallyResult, VotePower}; use namada::types::storage::Epoch; use super::*; @@ -75,192 +75,30 @@ where // Execute proposal if succesful let transfer_address = match tally_result { TallyResult::Passed(tally) => { - match tally { - Tally::Default => { - let proposal_author_key = - gov_storage::get_author_key(id); - let proposal_author = shell - .read_storage_key::
(&proposal_author_key) - .ok_or_else(|| { - Error::BadProposal( - id, - "Invalid proposal author.".to_string(), - ) - })?; - - let proposal_code_key = - gov_storage::get_proposal_code_key(id); - let proposal_code = - shell.read_storage_key_bytes(&proposal_code_key); - match proposal_code { - Some(proposal_code) => { - let tx = - Tx::new(proposal_code, Some(encode(&id))); - let tx_type = - TxType::Decrypted(DecryptedTx::Decrypted { - tx, - #[cfg(not(feature = "mainnet"))] - has_valid_pow: false, - }); - let pending_execution_key = - gov_storage::get_proposal_execution_key(id); - shell - .wl_storage - .write(&pending_execution_key, ()) - .expect( - "Should be able to write to storage.", - ); - let tx_result = protocol::apply_tx( - tx_type, - 0, /* this is used to compute the fee - * based on the code size. We dont - * need it here. */ - TxIndex::default(), - &mut BlockGasMeter::default(), - &mut shell.wl_storage.write_log, - &shell.wl_storage.storage, - &mut shell.vp_wasm_cache, - &mut shell.tx_wasm_cache, - ); - shell - .wl_storage - .storage - .delete(&pending_execution_key) - .expect( - "Should be able to delete the storage.", - ); - match tx_result { - Ok(tx_result) => { - if tx_result.is_accepted() { - shell.wl_storage.commit_tx(); - let proposal_event: Event = - ProposalEvent::new( - EventType::Proposal - .to_string(), - TallyResult::Passed( - Tally::Default, - ), - id, - true, - true, - ) - .into(); - response - .events - .push(proposal_event); - proposals_result.passed.push(id); - - proposal_author - } else { - shell.wl_storage.drop_tx(); - let proposal_event: Event = - ProposalEvent::new( - EventType::Proposal - .to_string(), - TallyResult::Passed( - Tally::Default, - ), - id, - true, - false, - ) - .into(); - response - .events - .push(proposal_event); - proposals_result.rejected.push(id); - - slash_fund_address - } - } - Err(_e) => { - shell.wl_storage.drop_tx(); - let proposal_event: Event = - ProposalEvent::new( - EventType::Proposal.to_string(), - TallyResult::Passed( - Tally::Default, - ), - id, - true, - false, - ) - .into(); - response.events.push(proposal_event); - proposals_result.rejected.push(id); - - slash_fund_address - } - } - } - None => { - let proposal_event: Event = ProposalEvent::new( - EventType::Proposal.to_string(), - TallyResult::Passed(Tally::Default), - id, - false, - false, - ) - .into(); - response.events.push(proposal_event); - proposals_result.passed.push(id); - - proposal_author - } - } - } + let (successful_execution, proposal_event) = match tally { + Tally::Default => execute_default_proposal(shell, id), Tally::PGFCouncil(council) => { - // TODO: implement when PGF is in place, update the PGF - // council in storage - let proposal_event: Event = ProposalEvent::new( - EventType::Proposal.to_string(), - TallyResult::Passed(Tally::PGFCouncil(council)), - id, - false, - false, - ) - .into(); - response.events.push(proposal_event); - proposals_result.passed.push(id); - - let proposal_author_key = - gov_storage::get_author_key(id); - - shell - .read_storage_key::
(&proposal_author_key) - .ok_or_else(|| { - Error::BadProposal( - id, - "Invalid proposal author.".to_string(), - ) - })? + execute_pgf_proposal(id, council) } - Tally::ETHBridge => { - // TODO: implement when ETH Bridge. Apply the - // modification requested by the proposal - let proposal_event: Event = ProposalEvent::new( - EventType::Proposal.to_string(), - TallyResult::Passed(Tally::ETHBridge), - id, - false, - false, - ) - .into(); - response.events.push(proposal_event); - proposals_result.passed.push(id); + Tally::ETHBridge => execute_eth_proposal(id), + }; - let proposal_author_key = - gov_storage::get_author_key(id); - - shell - .read_storage_key::
(&proposal_author_key) - .ok_or_else(|| { - Error::BadProposal( - id, - "Invalid proposal author.".to_string(), - ) - })? - } + response.events.push(proposal_event); + if successful_execution { + proposals_result.passed.push(id); + shell + .read_storage_key::
( + &gov_storage::get_author_key(id), + ) + .ok_or_else(|| { + Error::BadProposal( + id, + "Invalid proposal author.".to_string(), + ) + })? + } else { + proposals_result.rejected.push(id); + slash_fund_address } } TallyResult::Rejected => { @@ -296,3 +134,122 @@ where Ok(proposals_result) } + +fn execute_default_proposal( + shell: &mut Shell, + id: u64, +) -> (bool, Event) +where + D: DB + for<'iter> DBIter<'iter> + Sync + 'static, + H: StorageHasher + Sync + 'static, +{ + let proposal_code_key = gov_storage::get_proposal_code_key(id); + let proposal_code = shell.read_storage_key_bytes(&proposal_code_key); + match proposal_code { + Some(proposal_code) => { + let tx = Tx::new(proposal_code, Some(encode(&id))); + let tx_type = TxType::Decrypted(DecryptedTx::Decrypted { + tx, + #[cfg(not(feature = "mainnet"))] + has_valid_pow: false, + }); + let pending_execution_key = + gov_storage::get_proposal_execution_key(id); + shell + .wl_storage + .write(&pending_execution_key, ()) + .expect("Should be able to write to storage."); + let tx_result = protocol::apply_tx( + tx_type, + 0, /* this is used to compute the fee + * based on the code size. We dont + * need it here. */ + TxIndex::default(), + &mut BlockGasMeter::default(), + &mut shell.wl_storage.write_log, + &shell.wl_storage.storage, + &mut shell.vp_wasm_cache, + &mut shell.tx_wasm_cache, + ); + shell + .wl_storage + .storage + .delete(&pending_execution_key) + .expect("Should be able to delete the storage."); + match tx_result { + Ok(tx_result) if tx_result.is_accepted() => { + shell.wl_storage.commit_tx(); + ( + tx_result.is_accepted(), + ProposalEvent::new( + EventType::Proposal.to_string(), + TallyResult::Passed(Tally::Default), + id, + true, + tx_result.is_accepted(), + ) + .into(), + ) + } + _ => { + shell.wl_storage.drop_tx(); + ( + false, + ProposalEvent::new( + EventType::Proposal.to_string(), + TallyResult::Passed(Tally::Default), + id, + true, + false, + ) + .into(), + ) + } + } + } + None => ( + true, + ProposalEvent::new( + EventType::Proposal.to_string(), + TallyResult::Passed(Tally::Default), + id, + false, + false, + ) + .into(), + ), + } +} + +fn execute_pgf_proposal(id: u64, council: Council) -> (bool, Event) { + // TODO: implement when PGF is in place, update the PGF + // council in storage + ( + true, + ProposalEvent::new( + EventType::Proposal.to_string(), + TallyResult::Passed(Tally::PGFCouncil(council)), + id, + false, + false, + ) + .into(), + ) +} + +fn execute_eth_proposal(id: u64) -> (bool, Event) { + // TODO: implement when ETH Bridge. Apply the + // modification requested by the proposal + // + ( + true, + ProposalEvent::new( + EventType::Proposal.to_string(), + TallyResult::Passed(Tally::ETHBridge), + id, + false, + false, + ) + .into(), + ) +} diff --git a/shared/src/ledger/native_vp/governance/mod.rs b/shared/src/ledger/native_vp/governance/mod.rs index 022103d499..2e0de7a18f 100644 --- a/shared/src/ledger/native_vp/governance/mod.rs +++ b/shared/src/ledger/native_vp/governance/mod.rs @@ -238,6 +238,7 @@ where } else if let VoteType::ETHBridge(_sig) = vote_type { // TODO: Check the validity of the signature with the // governance ETH key in storage for the given validator + // } } From b5c3c5b45f1b3931415ac3a0f2176aa8406ad191 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Fri, 20 Jan 2023 16:36:09 +0100 Subject: [PATCH 49/49] changelog: add #1056 --- .../unreleased/features/1056-governance-custom-proposals.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/features/1056-governance-custom-proposals.md diff --git a/.changelog/unreleased/features/1056-governance-custom-proposals.md b/.changelog/unreleased/features/1056-governance-custom-proposals.md new file mode 100644 index 0000000000..d8395c16ff --- /dev/null +++ b/.changelog/unreleased/features/1056-governance-custom-proposals.md @@ -0,0 +1,2 @@ +- Implements governance custom proposals + ([#1056](https://github.com/anoma/namada/pull/1056)) \ No newline at end of file