diff --git a/.github/workflows/scripts/e2e.json b/.github/workflows/scripts/e2e.json index c50969792c5..15b871e61b4 100644 --- a/.github/workflows/scripts/e2e.json +++ b/.github/workflows/scripts/e2e.json @@ -12,7 +12,6 @@ "e2e::ledger_tests::pos_init_validator": 40, "e2e::ledger_tests::proposal_offline": 21, "e2e::ledger_tests::pgf_governance_proposal": 100, - "e2e::ledger_tests::eth_governance_proposal": 100, "e2e::ledger_tests::proposal_submission": 200, "e2e::ledger_tests::run_ledger": 5, "e2e::ledger_tests::run_ledger_load_state_and_reset": 23, diff --git a/Cargo.lock b/Cargo.lock index 13b76fdc920..c18ef4cf753 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4561,6 +4561,7 @@ dependencies = [ "prost", "rand 0.8.5", "regex", + "serde 1.0.163", "serde_json", "sha2 0.9.9", "tempfile", diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 5e62e658c80..efd107082e2 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -246,6 +246,7 @@ pub mod cmds { .subcommand(QueryProposal::def().display_order(4)) .subcommand(QueryProposalResult::def().display_order(4)) .subcommand(QueryProtocolParameters::def().display_order(4)) + .subcommand(QueryPgf::def().display_order(4)) .subcommand(QueryValidatorState::def().display_order(4)) // Actions .subcommand(SignTx::def().display_order(5)) @@ -297,6 +298,7 @@ pub mod cmds { Self::parse_with_ctx(matches, QueryProposalResult); let query_protocol_parameters = Self::parse_with_ctx(matches, QueryProtocolParameters); + let query_pgf = Self::parse_with_ctx(matches, QueryPgf); let query_validator_state = Self::parse_with_ctx(matches, QueryValidatorState); let add_to_eth_bridge_pool = @@ -333,6 +335,7 @@ pub mod cmds { .or(query_proposal) .or(query_proposal_result) .or(query_protocol_parameters) + .or(query_pgf) .or(query_validator_state) .or(query_account) .or(sign_tx) @@ -405,6 +408,7 @@ pub mod cmds { QueryProposal(QueryProposal), QueryProposalResult(QueryProposalResult), QueryProtocolParameters(QueryProtocolParameters), + QueryPgf(QueryPgf), QueryValidatorState(QueryValidatorState), SignTx(SignTx), } @@ -1185,6 +1189,28 @@ pub mod cmds { } } + #[derive(Clone, Debug)] + pub struct QueryPgf(pub args::QueryPgf); + + impl SubCmd for QueryPgf { + const CMD: &'static str = "query-pgf"; + + fn parse(matches: &ArgMatches) -> Option + where + Self: Sized, + { + matches + .subcommand_matches(Self::CMD) + .map(|matches| QueryPgf(args::QueryPgf::parse(matches))) + } + + fn def() -> App { + App::new(Self::CMD) + .about("Query pgf stewards and continous funding.") + .add_args::>() + } + } + #[derive(Clone, Debug)] pub struct TxCustom(pub args::TxCustom); @@ -2496,6 +2522,9 @@ pub mod args { "port-id", DefaultFn(|| PortId::from_str("transfer").unwrap()), ); + pub const PROPOSAL_ETH: ArgFlag = flag("eth"); + pub const PROPOSAL_PGF_STEWARD: ArgFlag = flag("pgf-stewards"); + pub const PROPOSAL_PGF_FUNDING: ArgFlag = flag("pgf-funding"); pub const PROPOSAL_OFFLINE: ArgFlag = flag("offline"); pub const PROTOCOL_KEY: ArgOpt = arg_opt("protocol-key"); pub const PRE_GENESIS_PATH: ArgOpt = arg_opt("pre-genesis-path"); @@ -3677,26 +3706,15 @@ pub mod args { )) } } - #[derive(Clone, Debug)] - pub struct InitProposal { - /// Common tx arguments - pub tx: Tx, - /// The proposal file path - pub proposal_data: PathBuf, - /// Flag if proposal should be run offline - pub offline: bool, - /// Native token address - pub native_token: C::NativeAddress, - /// Path to the TX WASM code file - pub tx_code_path: PathBuf, - } impl CliToSdk> for InitProposal { fn to_sdk(self, ctx: &mut Context) -> InitProposal { InitProposal:: { tx: self.tx.to_sdk(ctx), - proposal_data: self.proposal_data, - offline: self.offline, + proposal_data: std::fs::read(self.proposal_data).expect(""), + is_offline: self.is_offline, + is_pgf_stewards: self.is_pgf_stewards, + is_pgf_funding: self.is_pgf_funding, native_token: ctx.native_token.clone(), tx_code_path: self.tx_code_path, } @@ -3707,15 +3725,19 @@ pub mod args { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); let proposal_data = DATA_PATH.parse(matches); - let offline = PROPOSAL_OFFLINE.parse(matches); + let is_offline = PROPOSAL_OFFLINE.parse(matches); + let is_pgf_stewards = PROPOSAL_PGF_STEWARD.parse(matches); + let is_pgf_funding = PROPOSAL_PGF_FUNDING.parse(matches); let tx_code_path = PathBuf::from(TX_INIT_PROPOSAL); Self { tx, proposal_data, - offline, native_token: (), tx_code_path, + is_offline, + is_pgf_stewards, + is_pgf_funding, } } @@ -3727,45 +3749,66 @@ pub mod args { .arg( PROPOSAL_OFFLINE .def() - .help("Flag if the proposal vote should run offline."), + .help( + "Flag if the proposal should be serialized \ + offline (only for default types).", + ) + .conflicts_with_all([ + PROPOSAL_PGF_FUNDING.name, + PROPOSAL_PGF_STEWARD.name, + PROPOSAL_ETH.name, + ]), + ) + .arg( + PROPOSAL_ETH + .def() + .help("Flag if the proposal is of type eth.") + .conflicts_with_all([ + PROPOSAL_PGF_FUNDING.name, + PROPOSAL_PGF_STEWARD.name, + ]), + ) + .arg( + PROPOSAL_PGF_STEWARD + .def() + .help( + "Flag if the proposal is of type pgf-stewards. \ + Used to elect/remove stewards.", + ) + .conflicts_with_all([ + PROPOSAL_ETH.name, + PROPOSAL_PGF_FUNDING.name, + ]), + ) + .arg( + PROPOSAL_PGF_FUNDING + .def() + .help( + "Flag if the proposal is of type pgf-funding. \ + Used to control continous/retro pgf fundings.", + ) + .conflicts_with_all([ + PROPOSAL_ETH.name, + PROPOSAL_PGF_STEWARD.name, + ]), ) } } - #[derive(Clone, Debug)] - pub struct VoteProposal { - /// Common tx arguments - pub tx: Tx, - /// Proposal id - pub proposal_id: Option, - /// The vote - pub vote: String, - /// The address of the voter - pub voter_address: C::Address, - /// 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 - pub proposal_data: Option, - /// Path to the TX WASM code file - pub tx_code_path: PathBuf, - } - impl CliToSdk> for VoteProposal { fn to_sdk(self, ctx: &mut Context) -> VoteProposal { VoteProposal:: { tx: self.tx.to_sdk(ctx), proposal_id: self.proposal_id, vote: self.vote, - voter_address: ctx.get(&self.voter_address), - offline: self.offline, - proposal_data: self.proposal_data, + voter: ctx.get(&self.voter), + is_offline: self.is_offline, + proposal_data: self.proposal_data.map(|path| { + println!("Not able to read {}.", path.to_string_lossy()); + std::fs::read(path) + .expect("Should be able to read the file.") + }), tx_code_path: self.tx_code_path.to_path_buf(), - proposal_pgf: self.proposal_pgf, - proposal_eth: self.proposal_eth, } } } @@ -3774,11 +3817,9 @@ 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 voter_address = ADDRESS.parse(matches); - let offline = PROPOSAL_OFFLINE.parse(matches); + let voter = ADDRESS.parse(matches); + let is_offline = PROPOSAL_OFFLINE.parse(matches); let proposal_data = DATA_PATH_OPT.parse(matches); let tx_code_path = PathBuf::from(TX_VOTE_PROPOSAL); @@ -3786,10 +3827,8 @@ pub mod args { tx, proposal_id, vote, - proposal_pgf, - proposal_eth, - offline, - voter_address, + is_offline, + voter, proposal_data, tx_code_path, } @@ -3809,29 +3848,7 @@ pub mod args { .arg( PROPOSAL_VOTE .def() - .help("The vote for the proposal. Either yay or nay"), - ) - .arg( - PROPOSAL_VOTE_PGF_OPT - .def() - .help( - "The list of proposed councils and spending \ - caps:\n$council1 $cap1 $council2 $cap2 ... \ - (council is bech32m encoded address, cap is \ - expressed in microNAM", - ) - .requires(PROPOSAL_ID.name) - .conflicts_with(PROPOSAL_VOTE_ETH_OPT.name), - ) - .arg( - PROPOSAL_VOTE_ETH_OPT - .def() - .help( - "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), + .help("The vote for the proposal. Either yay or nay."), ) .arg( PROPOSAL_OFFLINE @@ -3846,6 +3863,7 @@ pub mod args { "The data path file (json) that describes the \ proposal.", ) + .requires(PROPOSAL_OFFLINE.name) .conflicts_with(PROPOSAL_ID.name), ) .arg(ADDRESS.def().help("The address of the voter.")) @@ -3938,7 +3956,15 @@ pub mod args { fn def(app: App) -> App { app.add_args::>() - .arg(PROPOSAL_ID_OPT.def().help("The proposal identifier.")) + .arg( + PROPOSAL_ID_OPT + .def() + .help("The proposal identifier.") + .conflicts_with_all([ + PROPOSAL_OFFLINE.name, + DATA_PATH_OPT.name, + ]), + ) .arg( PROPOSAL_OFFLINE .def() @@ -3946,16 +3972,18 @@ pub mod args { "Flag if the proposal result should run on \ offline data.", ) - .conflicts_with(PROPOSAL_ID.name), + .conflicts_with(PROPOSAL_ID.name) + .requires(DATA_PATH_OPT.name), ) .arg( DATA_PATH_OPT .def() .help( "The path to the folder containing the proposal \ - json and votes", + and votes files in json format.", ) - .conflicts_with(PROPOSAL_ID.name), + .conflicts_with(PROPOSAL_ID.name) + .requires(PROPOSAL_OFFLINE.name), ) } } @@ -3985,6 +4013,26 @@ pub mod args { } } + impl CliToSdk> for QueryPgf { + fn to_sdk(self, ctx: &mut Context) -> QueryPgf { + QueryPgf:: { + query: self.query.to_sdk(ctx), + } + } + } + + impl Args for QueryPgf { + fn parse(matches: &ArgMatches) -> Self { + let query = Query::parse(matches); + + Self { query } + } + + fn def(app: App) -> App { + app.add_args::>() + } + } + impl CliToSdk> for Withdraw { fn to_sdk(self, ctx: &mut Context) -> Withdraw { Withdraw:: { @@ -4076,9 +4124,10 @@ pub mod args { fn def(app: App) -> App { app.add_args::>().arg( - BALANCE_OWNER + OWNER .def() - .help("The substorage space address to query."), + .help("The substorage space address to query.") + .required(true), ) } } diff --git a/apps/src/lib/cli/client.rs b/apps/src/lib/cli/client.rs index 43e8171d8a5..86490492699 100644 --- a/apps/src/lib/cli/client.rs +++ b/apps/src/lib/cli/client.rs @@ -549,6 +549,19 @@ impl CliApi { let args = args.to_sdk(&mut ctx); rpc::query_protocol_parameters(&client, args).await; } + Sub::QueryPgf(QueryPgf(mut args)) => { + let client = client.unwrap_or_else(|| { + C::from_tendermint_address( + &mut args.query.ledger_address, + ) + }); + client + .wait_until_node_is_synced() + .await + .proceed_or_else(error)?; + let args = args.to_sdk(&mut ctx); + rpc::query_pgf(&client, args).await; + } Sub::QueryAccount(QueryAccount(mut args)) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address( diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 855941e5458..cf802f5706c 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -2,10 +2,9 @@ use std::cmp::Ordering; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; -use std::fs::File; +use std::fs::{self, read_dir}; use std::io::{self, Write}; use std::iter::Iterator; -use std::path::PathBuf; use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; @@ -15,19 +14,25 @@ use masp_primitives::asset_type::AssetType; use masp_primitives::merkle_tree::MerklePath; use masp_primitives::sapling::{Node, ViewingKey}; use masp_primitives::zip32::ExtendedFullViewingKey; -use namada::core::types::transaction::governance::ProposalType; +use namada::core::ledger::governance::cli::offline::{ + find_offline_proposal, find_offline_votes, read_offline_files, + OfflineSignedProposal, OfflineVote, +}; +use namada::core::ledger::governance::parameters::GovernanceParameters; +use namada::core::ledger::governance::storage::keys as governance_storage; +use namada::core::ledger::governance::storage::proposal::{ + PGFTarget, StorageProposal, +}; +use namada::core::ledger::governance::utils::{ + compute_proposal_result, ProposalVotes, TallyType, TallyVote, VotePower, +}; use namada::ledger::events::Event; -use namada::ledger::governance::parameters::GovParams; -use namada::ledger::governance::storage as gov_storage; use namada::ledger::masp::{ Conversions, MaspAmount, MaspChange, PinnedBalanceError, ShieldedContext, ShieldedUtils, }; -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, -}; +use namada::ledger::pos::{CommissionPair, PosParams, Slash}; use namada::ledger::queries::RPC; use namada::ledger::rpc::{ self, enriched_bonds_and_unbonds, format_denominated_amount, query_epoch, @@ -38,9 +43,6 @@ use namada::ledger::wallet::{AddressVpType, Wallet}; use namada::proof_of_stake::types::{ValidatorState, WeightedValidator}; use namada::types::address::{masp, Address}; use namada::types::control_flow::ProceedOrElse; -use namada::types::governance::{ - OfflineProposal, OfflineVote, ProposalVote, VotePower, VoteType, -}; use namada::types::hash::Hash; use namada::types::key::*; use namada::types::masp::{BalanceOwner, ExtendedViewingKey, PaymentAddress}; @@ -571,133 +573,48 @@ pub async fn query_proposal( client: &C, args: args::QueryProposal, ) { - async fn print_proposal( - client: &C, - id: u64, - current_epoch: Epoch, - details: bool, - ) -> Option<()> { - 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?; - let start_epoch = - 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); - let grace_epoch_key = gov_storage::get_grace_epoch_key(id); - let content = query_storage_value::>( - client, - &content_key, - ) - .await?; - let grace_epoch = - query_storage_value::(client, &grace_epoch_key) - .await?; - - println!("Proposal: {}", id); - println!("{:4}Type: {}", "", proposal_type); - println!("{:4}Author: {}", "", author); - println!("{:4}Content:", ""); - for (key, value) in &content { - println!("{:8}{}: {}", "", key, value); - } - 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) + let current_epoch = query_and_print_epoch(client).await; + + if let Some(id) = args.proposal_id { + let proposal = query_proposal_by_id(client, id).await; + if let Some(proposal) = proposal { + println!("{}", proposal.to_string_with_status(current_epoch)); + } else { + eprintln!("No proposal found with id: {}", id); + } + } else { + println!("asd2"); + let last_proposal_id_key = governance_storage::get_counter_key(); + let last_proposal_id = + query_storage_value::(client, &last_proposal_id_key) .await - .try_into() .unwrap(); - if start_epoch > current_epoch { - println!("{:4}Status: pending", ""); - } else if start_epoch <= current_epoch && current_epoch <= end_epoch - { - 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 { - 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) - } - } - } + + let from_id = if last_proposal_id > 10 { + last_proposal_id - 10 } 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); - if start_epoch > current_epoch { - println!("{:4}Status: pending", ""); - } else if start_epoch <= current_epoch && current_epoch <= end_epoch - { - println!("{:4}Status: on-going", ""); - } else { - println!("{:4}Status: done", ""); - } - } + 0 + }; - Some(()) - } + println!("id: {}", last_proposal_id); - let current_epoch = query_and_print_epoch(client).await; - match args.proposal_id { - Some(id) => { - if print_proposal::(client, id, current_epoch, true) + for id in from_id..last_proposal_id { + let proposal = query_proposal_by_id(client, id) .await - .is_none() - { - eprintln!("No valid proposal was found with id {}", id) - } - } - None => { - let last_proposal_id_key = gov_storage::get_counter_key(); - let last_proposal_id = - query_storage_value::(client, &last_proposal_id_key) - .await - .unwrap(); - - for id in 0..last_proposal_id { - if print_proposal::(client, id, current_epoch, false) - .await - .is_none() - { - eprintln!("No valid proposal was found with id {}", id) - }; - } + .expect("Proposal should be written to storage."); + println!("{}", proposal); } } } +/// Query proposal by Id +pub async fn query_proposal_by_id( + client: &C, + proposal_id: u64, +) -> Option { + namada::ledger::rpc::query_proposal_by_id(client, proposal_id).await +} + /// Query token shielded balance(s) pub async fn query_shielded_balance< C: namada::ledger::queries::Client + Sync, @@ -976,174 +893,101 @@ pub async fn query_proposal_result< client: &C, args: args::QueryProposalResult, ) { - let current_epoch = query_epoch(client).await; + if args.proposal_id.is_some() { + let proposal_id = + args.proposal_id.expect("Proposal id should be defined."); + let proposal = if let Some(proposal) = + query_proposal_by_id(client, proposal_id).await + { + proposal + } else { + eprintln!("Proposal {} not found.", proposal_id); + return; + }; - match args.proposal_id { - Some(id) => { - let end_epoch_key = gov_storage::get_voting_end_epoch_key(id); - let end_epoch = - query_storage_value::(client, &end_epoch_key).await; - - match end_epoch { - Some(end_epoch) => { - if current_epoch > end_epoch { - let votes = - get_proposal_votes(client, end_epoch, id).await; - let proposal_type_key = - gov_storage::get_proposal_type_key(id); - let proposal_type = query_storage_value::< - C, - ProposalType, - >( - client, &proposal_type_key - ) - .await - .expect("Could not read proposal type from storage"); - let total_stake = - get_total_staked_tokens(client, end_epoch) - .await - .try_into() - .unwrap(); - println!("Proposal: {}", id); - match utils::compute_tally( - votes, - total_stake, - &proposal_type, - ) { - 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) - } - } - None => { - eprintln!("Error while retriving proposal."); - cli::safe_exit(1) - } - } - } - None => { - if args.offline { - match args.proposal_folder { - Some(path) => { - let mut dir = tokio::fs::read_dir(&path) - .await - .expect("Should be able to read the directory."); - let mut files = HashSet::new(); - let mut is_proposal_present = false; - - while let Some(entry) = - dir.next_entry().await.transpose() - { - match entry { - Ok(entry) => match entry.file_type().await { - Ok(entry_stat) => { - if entry_stat.is_file() { - if entry.file_name().eq(&"proposal") - { - is_proposal_present = true - } else if entry - .file_name() - .to_string_lossy() - .starts_with("proposal-vote-") - { - // Folder may contain other - // files than just the proposal - // and the votes - files.insert(entry.path()); - } - } - } - Err(e) => { - eprintln!( - "Can't read entry type: {}.", - e - ); - cli::safe_exit(1) - } - }, - Err(e) => { - eprintln!("Can't read entry: {}.", e); - cli::safe_exit(1) - } - } - } + let tally_type = proposal.get_tally_type(); + let total_voting_power = + get_total_staked_tokens(client, proposal.voting_end_epoch).await; - if !is_proposal_present { - eprintln!( - "The folder must contain the offline proposal \ - in a file named \"proposal\"" - ); - cli::safe_exit(1) - } + let votes = compute_proposal_votes( + client, + proposal_id, + proposal.voting_end_epoch, + ) + .await; - let file = File::open(path.join("proposal")) - .expect("Proposal file must exist."); - let proposal: OfflineProposal = - serde_json::from_reader(file).expect( - "JSON was not well-formatted for proposal.", - ); + let proposal_result = + compute_proposal_result(votes, total_voting_power, tally_type); - let public_key = rpc::get_public_key_at( - client, - &proposal.address, - 0, - ) - .await - .expect("Public key should exist."); + println!("Proposal Id: {} ", proposal_id); + println!("{:4}{}", "", proposal_result); + } else { + let proposal_folder = args.proposal_folder.expect( + "The argument --proposal-folder is required with --offline.", + ); + let data_directory = read_dir(&proposal_folder).unwrap_or_else(|_| { + panic!( + "Should be able to read {} directory.", + proposal_folder.to_string_lossy() + ) + }); + let files = read_offline_files(data_directory); + let proposal_path = find_offline_proposal(&files); + + let proposal = if let Some(path) = proposal_path { + let proposal_file = + fs::File::open(path).expect("file should open read only"); + let proposal: OfflineSignedProposal = + serde_json::from_reader(proposal_file) + .expect("file should be proper JSON"); + + let author_account = + rpc::get_account_info(client, &proposal.proposal.author) + .await + .expect("Account should exist."); - if !proposal.check_signature(&public_key) { - eprintln!("Bad proposal signature."); - cli::safe_exit(1) - } + let proposal = proposal.validate( + &author_account.public_keys_map, + author_account.threshold, + ); - let votes = get_proposal_offline_votes( - client, - proposal.clone(), - files, - ) - .await; - let total_stake = get_total_staked_tokens( - client, - proposal.tally_epoch, - ) - .await - .try_into() - .unwrap(); - match utils::compute_tally( - votes, - total_stake, - &ProposalType::Default(None), - ) { - Ok(proposal_result) => { - println!("{:4}Result: {}", "", proposal_result) - } - Err(msg) => { - eprintln!("Error in tally computation: {}", msg) - } - } - } - None => { - eprintln!( - "Offline flag must be followed by data-path." - ); - cli::safe_exit(1) - } - }; + if proposal.is_ok() { + proposal.unwrap() } else { - eprintln!( - "Either --proposal-id or --data-path should be provided \ - as arguments." - ); - cli::safe_exit(1) + eprintln!("The offline proposal is not valid."); + return; } - } + } else { + eprintln!("Couldn't find a file name offline_proposal_*.json."); + return; + }; + + let votes = find_offline_votes(&files) + .iter() + .map(|path| { + let vote_file = fs::File::open(path).expect(""); + let vote: OfflineVote = + serde_json::from_reader(vote_file).expect(""); + vote + }) + .collect::>(); + + let proposal_votes = + compute_offline_proposal_votes(client, &proposal, votes.clone()) + .await; + let total_voting_power = + get_total_staked_tokens(client, proposal.proposal.tally_epoch) + .await; + + let proposal_result = compute_proposal_result( + proposal_votes, + total_voting_power, + TallyType::TwoThird, + ); + + println!("Proposal offline: {}", proposal.proposal.hash()); + println!("Parsed {} votes.", votes.len()); + println!("{:4}{}", "", proposal_result); } } @@ -1164,14 +1008,43 @@ pub async fn query_account( } } +pub async fn query_pgf( + client: &C, + _args: args::QueryPgf, +) { + let stewards = query_pgf_stewards(client).await; + let fundings = query_pgf_fundings(client).await; + + match stewards.len() { + 0 => println!("Pgf stewards: no stewards are currectly set."), + _ => { + println!("Pgf stewards:"); + for steward in stewards { + println!("{:4}- {}", "", steward); + } + } + } + + match fundings.len() { + 0 => println!("Pgf fundings: no fundings are currently set."), + _ => { + println!("Pgf fundings:"); + for payment in fundings { + println!("{:4}- {}", "", payment.target); + println!("{:6}{}", "", payment.amount.to_string_native()); + } + } + } +} + pub async fn query_protocol_parameters< C: namada::ledger::queries::Client + Sync, >( client: &C, _args: args::QueryProtocolParameters, ) { - let gov_parameters = get_governance_parameters(client).await; - println!("Governance Parameters\n {:4}", gov_parameters); + let governance_parameters = query_governance_parameters(client).await; + println!("Governance Parameters\n {:4}", governance_parameters); println!("Protocol parameters"); let key = param_storage::get_epoch_duration_storage_key(); @@ -1206,10 +1079,7 @@ pub async fn query_protocol_parameters< println!("{:4}Transactions whitelist: {:?}", "", tx_whitelist); println!("PoS parameters"); - let key = pos::params_key(); - let pos_params = query_storage_value::(client, &key) - .await - .expect("Parameter should be defined."); + let pos_params = query_pos_parameters(client).await; println!( "{:4}Block proposer reward: {}", "", pos_params.block_proposer_reward @@ -1261,6 +1131,30 @@ pub async fn query_unbond_with_slashing< ) } +pub async fn query_pos_parameters( + client: &C, +) -> PosParams { + unwrap_client_response::( + RPC.vp().pos().pos_params(client).await, + ) +} + +pub async fn query_pgf_stewards( + client: &C, +) -> BTreeSet
{ + unwrap_client_response::>( + RPC.vp().pgf().stewards(client).await, + ) +} + +pub async fn query_pgf_fundings( + client: &C, +) -> BTreeSet { + unwrap_client_response::>( + RPC.vp().pgf().funding(client).await, + ) +} + pub async fn query_and_print_unbonds< C: namada::ledger::queries::Client + Sync, >( @@ -2051,199 +1945,6 @@ pub async fn epoch_sleep( } } -pub async fn get_proposal_votes( - client: &C, - epoch: Epoch, - proposal_id: u64, -) -> Votes { - namada::ledger::rpc::get_proposal_votes(client, epoch, proposal_id).await -} - -pub async fn get_proposal_offline_votes< - C: namada::ledger::queries::Client + Sync, ->( - client: &C, - proposal: OfflineProposal, - files: HashSet, -) -> Votes { - // let validators = get_all_validators(client, proposal.tally_epoch).await; - - let proposal_hash = proposal.compute_hash(); - - let mut yay_validators: HashMap = - HashMap::new(); - let mut delegators: HashMap< - Address, - HashMap, - > = HashMap::new(); - - for path in files { - let file = File::open(&path).expect("Proposal file must exist."); - let proposal_vote: OfflineVote = serde_json::from_reader(file) - .expect("JSON was not well-formatted for offline vote."); - - let public_key = - rpc::get_public_key_at(client, &proposal_vote.address, 0) - .await - .expect("Public key should exist."); - - if !proposal_vote.proposal_hash.eq(&proposal_hash) - || !proposal_vote.check_signature(&public_key) - { - continue; - } - - if proposal_vote.vote.is_yay() - // && validators.contains(&proposal_vote.address) - && unwrap_client_response::( - RPC.vp().pos().is_validator(client, &proposal_vote.address).await, - ) - { - let amount: VotePower = get_validator_stake( - client, - proposal.tally_epoch, - &proposal_vote.address, - ) - .await - .unwrap_or_default() - .try_into() - .expect("Amount out of bounds"); - yay_validators.insert( - proposal_vote.address, - (amount, ProposalVote::Yay(VoteType::Default)), - ); - } else if is_delegator_at( - client, - &proposal_vote.address, - proposal.tally_epoch, - ) - .await - { - // TODO: decide whether to do this with `bond_with_slashing` RPC - // endpoint or with `bonds_and_unbonds` - let bonds_and_unbonds: pos::types::BondsAndUnbondsDetails = - unwrap_client_response::( - RPC.vp() - .pos() - .bonds_and_unbonds( - client, - &Some(proposal_vote.address.clone()), - &None, - ) - .await, - ); - for ( - BondId { - source: _, - validator, - }, - BondsAndUnbondsDetail { - bonds, - unbonds: _, - slashes: _, - }, - ) in bonds_and_unbonds - { - let mut delegated_amount = token::Amount::zero(); - for delta in bonds { - if delta.start <= proposal.tally_epoch { - delegated_amount += delta.amount - - delta.slashed_amount.unwrap_or_default(); - } - } - - let entry = delegators - .entry(proposal_vote.address.clone()) - .or_default(); - entry.insert( - validator, - ( - VotePower::try_from(delegated_amount).unwrap(), - proposal_vote.vote.clone(), - ), - ); - } - - // let key = pos::bonds_for_source_prefix(&proposal_vote.address); - // let bonds_iter = - // query_storage_prefix::(client, &key).await; - // if let Some(bonds) = bonds_iter { - // for (key, epoched_bonds) in bonds { - // // Look-up slashes for the validator in this key and - // // apply them if any - // let validator = - // pos::get_validator_address_from_bond(&key) - // .expect( - // "Delegation key should contain validator - // address.", ); - // let slashes_key = pos::validator_slashes_key(&validator); - // let slashes = query_storage_value::( - // client, - // &slashes_key, - // ) - // .await - // .unwrap_or_default(); - // let mut delegated_amount: token::Amount = 0.into(); - // let bond = epoched_bonds - // .get(proposal.tally_epoch) - // .expect("Delegation bond should be defined."); - // let mut to_deduct = bond.neg_deltas; - // for (start_epoch, &(mut delta)) in - // bond.pos_deltas.iter().sorted() - // { - // // deduct bond's neg_deltas - // if to_deduct > delta { - // to_deduct -= delta; - // // If the whole bond was deducted, continue to - // // the next one - // continue; - // } else { - // delta -= to_deduct; - // to_deduct = token::Amount::zero(); - // } - - // delta = apply_slashes( - // &slashes, - // delta, - // *start_epoch, - // None, - // None, - // ); - // delegated_amount += delta; - // } - - // let validator_address = - // pos::get_validator_address_from_bond(&key).expect( - // "Delegation key should contain validator - // address.", ); - // if proposal_vote.vote.is_yay() { - // let entry = yay_delegators - // .entry(proposal_vote.address.clone()) - // .or_default(); - // entry.insert( - // validator_address, - // VotePower::from(delegated_amount), - // ); - // } else { - // let entry = nay_delegators - // .entry(proposal_vote.address.clone()) - // .or_default(); - // entry.insert( - // validator_address, - // VotePower::from(delegated_amount), - // ); - // } - // } - // } - } - } - - Votes { - yay_validators, - delegators, - } -} - pub async fn get_bond_amount_at( client: &C, delegator: &Address, @@ -2302,12 +2003,23 @@ pub async fn get_delegators_delegation< namada::ledger::rpc::get_delegators_delegation(client, address).await } -pub async fn get_governance_parameters< +pub async fn get_delegators_delegation_at< + C: namada::ledger::queries::Client + Sync, +>( + client: &C, + address: &Address, + epoch: Epoch, +) -> HashMap { + namada::ledger::rpc::get_delegators_delegation_at(client, address, epoch) + .await +} + +pub async fn query_governance_parameters< C: namada::ledger::queries::Client + Sync, >( client: &C, -) -> GovParams { - namada::ledger::rpc::get_governance_parameters(client).await +) -> GovernanceParameters { + namada::ledger::rpc::query_governance_parameters(client).await } /// A helper to unwrap client's response. Will shut down process on error. @@ -2319,3 +2031,121 @@ fn unwrap_client_response( cli::safe_exit(1) }) } + +pub async fn compute_offline_proposal_votes< + C: namada::ledger::queries::Client + Sync, +>( + client: &C, + proposal: &OfflineSignedProposal, + votes: Vec, +) -> ProposalVotes { + let mut validators_vote: HashMap = HashMap::default(); + let mut validator_voting_power: HashMap = + HashMap::default(); + let mut delegators_vote: HashMap = HashMap::default(); + let mut delegator_voting_power: HashMap< + Address, + HashMap, + > = HashMap::default(); + for vote in votes { + let is_validator = is_validator(client, &vote.address).await; + let is_delegator = is_delegator(client, &vote.address).await; + if is_validator { + let validator_stake = get_validator_stake( + client, + proposal.proposal.tally_epoch, + &vote.address, + ) + .await + .unwrap_or_default(); + validators_vote.insert(vote.address.clone(), vote.clone().into()); + validator_voting_power + .insert(vote.address.clone(), validator_stake); + } else if is_delegator { + let validators = get_delegators_delegation_at( + client, + &vote.address.clone(), + proposal.proposal.tally_epoch, + ) + .await; + + for validator in vote.delegations.clone() { + let delegator_stake = + validators.get(&validator).cloned().unwrap_or_default(); + + delegators_vote + .insert(vote.address.clone(), vote.clone().into()); + delegator_voting_power + .entry(vote.address.clone()) + .or_default() + .insert(validator, delegator_stake); + } + } else { + println!( + "Skipping vote, not a validator/delegator at epoch {}.", + proposal.proposal.tally_epoch + ); + } + } + + ProposalVotes { + validators_vote, + validator_voting_power, + delegators_vote, + delegator_voting_power, + } +} + +pub async fn compute_proposal_votes< + C: namada::ledger::queries::Client + Sync, +>( + client: &C, + proposal_id: u64, + epoch: Epoch, +) -> ProposalVotes { + let votes = + namada::ledger::rpc::query_proposal_votes(client, proposal_id).await; + + let mut validators_vote: HashMap = HashMap::default(); + let mut validator_voting_power: HashMap = + HashMap::default(); + let mut delegators_vote: HashMap = HashMap::default(); + let mut delegator_voting_power: HashMap< + Address, + HashMap, + > = HashMap::default(); + + for vote in votes { + if vote.is_validator() { + let validator_stake = + get_validator_stake(client, epoch, &vote.validator.clone()) + .await + .unwrap_or_default(); + + validators_vote.insert(vote.validator.clone(), vote.data.into()); + validator_voting_power.insert(vote.validator, validator_stake); + } else { + let delegator_stake = get_bond_amount_at( + client, + &vote.delegator, + &vote.validator, + epoch, + ) + .await + .unwrap_or_default(); + + delegators_vote.insert(vote.delegator.clone(), vote.data.into()); + delegator_voting_power + .entry(vote.delegator.clone()) + .or_default() + .insert(vote.validator, delegator_stake); + } + } + + ProposalVotes { + validators_vote, + validator_voting_power, + delegators_vote, + delegator_voting_power, + } +} diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 9aca90e40b7..125fe305888 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1,4 +1,3 @@ -use std::collections::HashSet; use std::env; use std::fmt::Debug; use std::fs::{File, OpenOptions}; @@ -7,29 +6,25 @@ use std::path::PathBuf; use async_trait::async_trait; use borsh::{BorshDeserialize, BorshSerialize}; -use data_encoding::HEXLOWER_PERMISSIVE; use masp_proofs::prover::LocalTxProver; -use namada::ledger::governance::storage as gov_storage; +use namada::core::ledger::governance::cli::offline::{ + OfflineSignedProposal, OfflineVote, +}; +use namada::core::ledger::governance::cli::onchain::{ + DefaultProposal, PgfFundingProposal, PgfStewardProposal, ProposalVote, +}; use namada::ledger::queries::Client; use namada::ledger::rpc::{TxBroadcastData, TxResponse}; -use namada::ledger::signing::find_pk; use namada::ledger::wallet::{Wallet, WalletUtils}; use namada::ledger::{masp, pos, signing, tx}; use namada::proof_of_stake::parameters::PosParams; use namada::proto::Tx; use namada::types::address::{Address, ImplicitAddress}; use namada::types::dec::Dec; -use namada::types::governance::{ - OfflineProposal, OfflineVote, Proposal, ProposalVote, VoteType, -}; use namada::types::key::{self, *}; -use namada::types::storage::{Epoch, Key}; -use namada::types::token; -use namada::types::transaction::governance::{ProposalType, VoteProposalData}; use namada::types::transaction::pos::InitValidator; use super::rpc; -use crate::cli::context::WalletAddress; use crate::cli::{args, safe_exit, Context}; use crate::client::rpc::query_wasm_code_hash; use crate::client::tx::tx::ProcessTxResponse; @@ -720,182 +715,175 @@ where C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, { - let file = File::open(&args.proposal_data).expect("File must exist."); - let proposal: Proposal = - serde_json::from_reader(file).expect("JSON was not well-formatted"); - - let signer = WalletAddress::new(proposal.clone().author.to_string()); let current_epoch = rpc::query_and_print_epoch(client).await; + let governance_parameters = rpc::query_governance_parameters(client).await; - let governance_parameters = rpc::get_governance_parameters(client).await; - if proposal.voting_start_epoch <= current_epoch - || proposal.voting_start_epoch.0 - % governance_parameters.min_proposal_period - != 0 - { - println!("{}", proposal.voting_start_epoch <= current_epoch); - println!( - "{}", - proposal.voting_start_epoch.0 - % governance_parameters.min_proposal_period - == 0 - ); - eprintln!( - "Invalid proposal start epoch: {} must be greater than current \ - epoch {} and a multiple of {}", - proposal.voting_start_epoch, - current_epoch, - governance_parameters.min_proposal_period - ); - if !args.tx.force { - safe_exit(1) - } - } else if proposal.voting_end_epoch <= proposal.voting_start_epoch - || proposal.voting_end_epoch.0 - proposal.voting_start_epoch.0 - < governance_parameters.min_proposal_period - || proposal.voting_end_epoch.0 - proposal.voting_start_epoch.0 - > governance_parameters.max_proposal_period - || proposal.voting_end_epoch.0 % 3 != 0 - { - eprintln!( - "Invalid proposal end epoch: difference between proposal start \ - and end epoch must be at least {} and at max {} and end epoch \ - must be a multiple of {}", - governance_parameters.min_proposal_period, - governance_parameters.max_proposal_period, - governance_parameters.min_proposal_period - ); - if !args.tx.force { - safe_exit(1) - } - } else if proposal.grace_epoch <= proposal.voting_end_epoch - || proposal.grace_epoch.0 - proposal.voting_end_epoch.0 - < governance_parameters.min_proposal_grace_epochs - { - eprintln!( - "Invalid proposal grace epoch: difference between proposal grace \ - and end epoch must be at least {}", - governance_parameters.min_proposal_grace_epochs - ); - if !args.tx.force { - safe_exit(1) - } - } + let (mut tx_builder, signing_data) = if args.is_offline { + let proposal = namada::core::ledger::governance::cli::offline::OfflineProposal::try_from(args.proposal_data.as_ref()).map_err(|e| tx::Error::FailedGovernaneProposalDeserialize(e.to_string()))?.validate(current_epoch) + .map_err(|e| tx::Error::InvalidProposal(e.to_string()))?; - if args.offline { - let signer = ctx.get(&signer); - let key = find_pk(client, &mut ctx.wallet, &signer, None).await?; - let signing_key = - signing::find_key_by_pk(&mut ctx.wallet, &args.tx, &key)?; - let offline_proposal = - OfflineProposal::new(proposal, signer, &signing_key); - let proposal_filename = args - .proposal_data - .parent() - .expect("No parent found") - .join("proposal"); - let out = File::create(&proposal_filename).unwrap(); - match serde_json::to_writer_pretty(out, &offline_proposal) { - Ok(_) => { - println!( - "Proposal created: {}.", - proposal_filename.to_string_lossy() - ); - } - Err(e) => { - eprintln!("Error while creating proposal file: {}.", e); - safe_exit(1) - } - } - Ok(()) - } else { - let signer = ctx.get(&signer); - let tx_data = proposal.clone().try_into(); - let (mut init_proposal_data, init_proposal_content, init_proposal_code) = - if let Ok(data) = tx_data { - data - } else { - eprintln!("Invalid data for init proposal transaction."); - safe_exit(1) - }; - - let balance = - rpc::get_token_balance(client, &ctx.native_token, &proposal.author) - .await; - if balance - < token::Amount::from_uint( - governance_parameters.min_proposal_fund, - 0, - ) - .unwrap() - { - eprintln!( - "Address {} doesn't have enough funds.", - &proposal.author - ); - safe_exit(1); - } - - if init_proposal_content.len() - > governance_parameters.max_proposal_content_size as usize - { - eprintln!("Proposal content size too big.",); - safe_exit(1); - } + let default_signer = Some(proposal.author.clone()); + let signing_data = signing::aux_signing_data( + client, + &mut ctx.wallet, + &args.tx, + &Some(proposal.author.clone()), + default_signer, + ) + .await?; - let tx_code_hash = query_wasm_code_hash(client, args::TX_INIT_PROPOSAL) - .await - .unwrap(); + let signed_offline_proposal = proposal.sign( + args.tx.signing_keys, + &signing_data.account_public_keys_map.unwrap(), + ); + let output_file_path = signed_offline_proposal + .serialize(args.tx.output_folder) + .map_err(|e| { + tx::Error::FailedGovernaneProposalDeserialize(e.to_string()) + })?; - let default_signer = Some(signer.clone()); + println!("Proposal serialized to: {}", output_file_path); + return Ok(()); + } else if args.is_pgf_funding { + let proposal = + PgfFundingProposal::try_from(args.proposal_data.as_ref()) + .map_err(|e| { + tx::Error::FailedGovernaneProposalDeserialize(e.to_string()) + })? + .validate(&governance_parameters, current_epoch) + .map_err(|e| tx::Error::InvalidProposal(e.to_string()))?; + + let default_signer = Some(proposal.proposal.author.clone()); let signing_data = signing::aux_signing_data( client, &mut ctx.wallet, &args.tx, - &Some(signer.clone()), + &Some(proposal.proposal.author.clone()), default_signer, ) .await?; - let chain_id = args.tx.chain_id.clone().unwrap(); - let mut tx = Tx::new(chain_id, args.tx.expiration); + submit_reveal_aux( + client, + &mut ctx, + args.tx.clone(), + &proposal.proposal.author, + ) + .await?; - // Put any proposal content into an extra section - let extra_section_hash = tx.add_extra_section(init_proposal_content).1; - init_proposal_data.content = extra_section_hash; + ( + tx::build_pgf_funding_proposal( + client, + args.clone(), + proposal, + &signing_data.gas_payer, + ) + .await?, + signing_data, + ) + } else if args.is_pgf_stewards { + let proposal = PgfStewardProposal::try_from( + args.proposal_data.as_ref(), + ) + .map_err(|e| { + tx::Error::FailedGovernaneProposalDeserialize(e.to_string()) + })?; + let author_balane = rpc::get_token_balance( + client, + &ctx.native_token, + &proposal.proposal.author, + ) + .await; + let proposal = proposal + .validate(&governance_parameters, current_epoch, author_balane) + .map_err(|e| tx::Error::InvalidProposal(e.to_string()))?; - // Put any proposal code into an extra section - if let Some(init_proposal_code) = init_proposal_code { - let extra_section_hash = tx.add_extra_section(init_proposal_code).1; - init_proposal_data.r#type = - ProposalType::Default(Some(extra_section_hash)); - } + let default_signer = Some(proposal.proposal.author.clone()); + let signing_data = signing::aux_signing_data( + client, + &mut ctx.wallet, + &args.tx, + &Some(proposal.proposal.author.clone()), + default_signer, + ) + .await?; - tx.add_code_from_hash(tx_code_hash) - .add_data(init_proposal_data); + submit_reveal_aux( + client, + &mut ctx, + args.tx.clone(), + &proposal.proposal.author, + ) + .await?; - submit_reveal_aux(client, &mut ctx, args.tx.clone(), &signer).await?; + ( + tx::build_pgf_stewards_proposal( + client, + args.clone(), + proposal, + &signing_data.gas_payer, + ) + .await?, + signing_data, + ) + } else { + let proposal = DefaultProposal::try_from(args.proposal_data.as_ref()) + .map_err(|e| { + tx::Error::FailedGovernaneProposalDeserialize(e.to_string()) + })?; + let author_balane = rpc::get_token_balance( + client, + &ctx.native_token, + &proposal.proposal.author, + ) + .await; + let proposal = proposal + .validate(&governance_parameters, current_epoch, author_balane) + .map_err(|e| tx::Error::InvalidProposal(e.to_string()))?; - tx::prepare_tx( + let default_signer = Some(proposal.proposal.author.clone()); + let signing_data = signing::aux_signing_data( client, + &mut ctx.wallet, &args.tx, - &mut tx, - signing_data.gas_payer.clone(), - #[cfg(not(feature = "mainnet"))] - false, + &Some(proposal.proposal.author.clone()), + default_signer, ) - .await; - signing::generate_test_vector(client, &mut ctx.wallet, &tx).await; + .await?; - if args.tx.dump_tx { - tx::dump_tx(&args.tx, tx); - } else { - signing::sign_tx(&mut ctx.wallet, &args.tx, &mut tx, signing_data); - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; - } + submit_reveal_aux( + client, + &mut ctx, + args.tx.clone(), + &proposal.proposal.author, + ) + .await?; - Ok(()) + ( + tx::build_default_proposal( + client, + args.clone(), + proposal, + &signing_data.gas_payer, + ) + .await?, + signing_data, + ) + }; + + if args.tx.dump_tx { + tx::dump_tx(&args.tx, tx_builder); + } else { + signing::sign_tx( + &mut ctx.wallet, + &args.tx, + &mut tx_builder, + signing_data, + ); + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx_builder).await?; } + + Ok(()) } pub async fn submit_vote_proposal( @@ -907,280 +895,81 @@ where C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, { - // Construct vote - 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(); - let address_iter = splits.clone().step_by(2); - let cap_iter = splits.into_iter().skip(1).step_by(2); - let mut set = HashSet::new(); - for (address, cap) in - address_iter.zip(cap_iter).map(|(addr, cap)| { - ( - addr.parse() - .expect("Failed to parse pgf council address"), - cap.parse::() - .expect("Failed to parse pgf spending cap"), - ) - }) - { - set.insert(( - address, - token::Amount::from_uint(cap, 0).unwrap(), - )); - } - - 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); - } + let current_epoch = rpc::query_and_print_epoch(client).await; - 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); - } - }; + let default_signer = Some(args.voter.clone()); + let signing_data = signing::aux_signing_data( + client, + &mut ctx.wallet, + &args.tx, + &Some(args.voter.clone()), + default_signer.clone(), + ) + .await?; - if args.offline { - if !proposal_vote.is_default_vote() { - eprintln!( - "Wrong vote type for offline proposal. Just vote yay or nay!" - ); - safe_exit(1); - } - let proposal_file_path = - args.proposal_data.expect("Proposal file should exist."); - let file = File::open(&proposal_file_path).expect("File must exist."); + let mut tx_builder = if args.is_offline { + let proposal_vote = ProposalVote::try_from(args.vote) + .map_err(|_| tx::Error::InvalidProposalVote)?; - let proposal: OfflineProposal = - serde_json::from_reader(file).expect("JSON was not well-formatted"); - let public_key = namada::ledger::rpc::get_public_key_at( + let proposal = OfflineSignedProposal::try_from( + args.proposal_data.clone().unwrap().as_ref(), + ) + .map_err(|e| tx::Error::InvalidProposal(e.to_string()))? + .validate( + &signing_data.account_public_keys_map.clone().unwrap(), + signing_data.threshold, + ) + .map_err(|e| tx::Error::InvalidProposal(e.to_string()))?; + let delegations = rpc::get_delegators_delegation_at( client, - &proposal.address, - 0, + &args.voter, + proposal.proposal.tally_epoch, ) .await - .expect("Public key should exist."); - if !proposal.check_signature(&public_key) { - eprintln!("Proposal signature mismatch!"); - safe_exit(1) - } + .keys() + .cloned() + .collect::>(); - let key = - find_pk(client, &mut ctx.wallet, &args.voter_address, None).await?; - let signing_key = - signing::find_key_by_pk(&mut ctx.wallet, &args.tx, &key)?; let offline_vote = OfflineVote::new( &proposal, proposal_vote, - args.voter_address.clone(), - &signing_key, + args.voter.clone(), + delegations, ); - let proposal_vote_filename = proposal_file_path - .parent() - .expect("No parent found") - .join(format!("proposal-vote-{}", &args.voter_address.to_string())); - let out = File::create(&proposal_vote_filename).unwrap(); - match serde_json::to_writer_pretty(out, &offline_vote) { - Ok(_) => { - println!( - "Proposal vote created: {}.", - proposal_vote_filename.to_string_lossy() - ); - Ok(()) - } - Err(e) => { - eprintln!("Error while creating proposal vote file: {}.", e); - safe_exit(1) - } - } - } else { - let current_epoch = rpc::query_and_print_epoch(client).await; + let offline_signed_vote = offline_vote.sign( + args.tx.signing_keys, + &signing_data.account_public_keys_map.unwrap(), + ); + let output_file_path = offline_signed_vote + .serialize(args.tx.output_folder) + .expect("Should be able to serialize the offline proposal"); - let voter_address = args.voter_address.clone(); - let proposal_id = args.proposal_id.unwrap(); - let proposal_start_epoch_key = - gov_storage::get_voting_start_epoch_key(proposal_id); - let proposal_start_epoch = rpc::query_storage_value::( + println!("Proposal vote serialized to: {}", output_file_path); + return Ok(()); + } else { + tx::build_vote_proposal( client, - &proposal_start_epoch_key, + args.clone(), + current_epoch, + &signing_data.gas_payer, ) - .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::< - C, - ProposalType, - >(client, &proposal_type_key) - .await - .unwrap_or_else(|| { - panic!("Didn't find type of proposal id {} in storage", proposal_id) - }); - - if let ProposalVote::Yay(ref vote_type) = proposal_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 { - eprintln!( - "Current epoch {} is not greater than proposal start \ - epoch {}", - current_epoch, epoch - ); - - if !args.tx.force { - safe_exit(1) - } - } - let mut delegations = - rpc::get_delegators_delegation(client, &voter_address) - .await; - - // 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 - // window, otherwise there's the risk of the - // validator changing his vote and, effectively, invalidating - // the delgator's vote - if !args.tx.force - && is_safe_voting_window(client, proposal_id, epoch).await? - { - delegations = filter_delegations( - client, - delegations, - proposal_id, - &proposal_vote, - ) - .await; - } - - let tx_data = VoteProposalData { - id: proposal_id, - vote: proposal_vote, - voter: voter_address, - delegations: delegations.into_iter().collect(), - }; - - let tx_code_hash = query_wasm_code_hash( - client, - args.tx_code_path.to_str().unwrap(), - ) - .await - .unwrap(); + .await? + }; - let default_signer = Some(args.voter_address.clone()); - let signing_data = signing::aux_signing_data( - client, - &mut ctx.wallet, - &args.tx, - &Some(args.voter_address.clone()), - default_signer, - ) - .await?; - - let chain_id = args.tx.chain_id.clone().unwrap(); - let mut tx = Tx::new(chain_id, args.tx.expiration); - tx.add_code_from_hash(tx_code_hash).add_data(tx_data); - - tx::prepare_tx( - client, - &args.tx, - &mut tx, - signing_data.gas_payer.clone(), - #[cfg(not(feature = "mainnet"))] - false, - ) - .await; - signing::generate_test_vector(client, &mut ctx.wallet, &tx) - .await; - - if args.tx.dump_tx { - tx::dump_tx(&args.tx, tx); - } else { - // no need to releal pk since people who an vote on - // governane proposal must be enstablished addresses - - signing::sign_tx( - &mut ctx.wallet, - &args.tx, - &mut tx, - signing_data, - ); - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx) - .await?; - } - Ok(()) - } - None => { - eprintln!( - "Proposal start epoch for proposal id {} is not definied.", - proposal_id - ); - if !args.tx.force { safe_exit(1) } else { Ok(()) } - } - } + if args.tx.dump_tx { + tx::dump_tx(&args.tx, tx_builder); + } else { + signing::sign_tx( + &mut ctx.wallet, + &args.tx, + &mut tx_builder, + signing_data, + ); + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx_builder).await?; } + + Ok(()) } pub async fn sign_tx( @@ -1282,64 +1071,6 @@ where Ok(()) } -/// Check if current epoch is in the last third of the voting period of the -/// proposal. This ensures that it is safe to optimize the vote writing to -/// storage. -async fn is_safe_voting_window( - client: &C, - proposal_id: u64, - proposal_start_epoch: Epoch, -) -> Result -where - C: namada::ledger::queries::Client + Sync, - C::Error: std::fmt::Display, -{ - tx::is_safe_voting_window(client, proposal_id, proposal_start_epoch).await -} - -/// Removes validators whose vote corresponds to that of the delegator (needless -/// vote) -async fn filter_delegations( - client: &C, - delegations: HashSet
, - proposal_id: u64, - delegator_vote: &ProposalVote, -) -> HashSet
-where - C: namada::ledger::queries::Client + Sync, - C::Error: std::fmt::Display, -{ - // Filter delegations by their validator's vote concurrently - let delegations = futures::future::join_all( - delegations - .into_iter() - // we cannot use `filter/filter_map` directly because we want to - // return a future - .map(|validator_address| async { - let vote_key = gov_storage::get_vote_proposal_key( - proposal_id, - validator_address.to_owned(), - validator_address.to_owned(), - ); - - if let Some(validator_vote) = - rpc::query_storage_value::( - client, &vote_key, - ) - .await - { - if &validator_vote == delegator_vote { - return None; - } - } - Some(validator_address) - }), - ) - .await; - // Take out the `None`s - delegations.into_iter().flatten().collect() -} - pub async fn submit_bond( client: &C, ctx: &mut Context, @@ -1580,12 +1311,11 @@ where #[cfg(test)] mod test_tx { use masp_primitives::transaction::components::Amount; + use namada::core::types::storage::Epoch; use namada::ledger::masp::{make_asset_type, MaspAmount}; use namada::types::address::testing::gen_established_address; use namada::types::token::MaspDenom; - use super::*; - #[test] fn test_masp_add_amount() { let address_1 = gen_established_address(); diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index 70fe241b3b3..320a153d643 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -4,10 +4,10 @@ use std::collections::HashMap; use borsh::{BorshDeserialize, BorshSerialize}; use derivative::Derivative; +use namada::core::ledger::governance::parameters::GovernanceParameters; #[cfg(not(feature = "mainnet"))] use namada::core::ledger::testnet_pow; use namada::ledger::eth_bridge::EthereumBridgeConfig; -use namada::ledger::governance::parameters::GovParams; use namada::ledger::parameters::EpochDuration; use namada::ledger::pos::{Dec, GenesisValidator, PosParams}; use namada::types::address::Address; @@ -29,9 +29,9 @@ pub mod genesis_config { use data_encoding::HEXLOWER; use eyre::Context; + use namada::core::ledger::governance::parameters::GovernanceParameters; #[cfg(not(feature = "mainnet"))] use namada::core::ledger::testnet_pow; - use namada::ledger::governance::parameters::GovParams; use namada::ledger::parameters::EpochDuration; use namada::ledger::pos::{Dec, GenesisValidator, PosParams}; use namada::types::address::Address; @@ -148,7 +148,7 @@ pub mod genesis_config { // Maximum size of proposal in kibibytes (KiB) pub max_proposal_code_size: u64, // Minimum proposal period length in epochs - pub min_proposal_period: u64, + pub min_proposal_voting_period: u64, // Maximum proposal period length in epochs pub max_proposal_period: u64, // Maximum number of characters in the proposal content @@ -616,15 +616,15 @@ pub mod genesis_config { let GovernanceParamsConfig { min_proposal_fund, max_proposal_code_size, - min_proposal_period, + min_proposal_voting_period, max_proposal_content_size, min_proposal_grace_epochs, max_proposal_period, } = gov_params; - let gov_params = GovParams { - min_proposal_fund, + let gov_params = GovernanceParameters { + min_proposal_fund: token::Amount::native_whole(min_proposal_fund), max_proposal_code_size, - min_proposal_period, + min_proposal_voting_period, max_proposal_content_size, min_proposal_grace_epochs, max_proposal_period, @@ -725,7 +725,7 @@ pub struct Genesis { pub implicit_accounts: Vec, pub parameters: Parameters, pub pos_params: PosParams, - pub gov_params: GovParams, + pub gov_params: GovernanceParameters, // Ethereum bridge config pub ethereum_bridge_params: Option, } @@ -1083,7 +1083,7 @@ pub fn genesis(num_validators: u64) -> Genesis { token_accounts, parameters, pos_params: PosParams::default(), - gov_params: GovParams::default(), + gov_params: GovernanceParameters::default(), ethereum_bridge_params: Some(EthereumBridgeConfig { eth_start_height: Default::default(), min_confirmations: Default::default(), diff --git a/apps/src/lib/node/ledger/mod.rs b/apps/src/lib/node/ledger/mod.rs index cab175953a4..e421fc6e6bb 100644 --- a/apps/src/lib/node/ledger/mod.rs +++ b/apps/src/lib/node/ledger/mod.rs @@ -14,8 +14,8 @@ use std::thread; use byte_unit::Byte; use futures::future::TryFutureExt; +use namada::core::ledger::governance::storage::keys as governance_storage; use namada::eth_bridge::ethers::providers::{Http, Provider}; -use namada::ledger::governance::storage as gov_storage; use namada::types::storage::Key; use once_cell::unsync::Lazy; use sysinfo::{RefreshKind, System, SystemExt}; @@ -64,7 +64,7 @@ const ENV_VAR_RAYON_THREADS: &str = "NAMADA_RAYON_THREADS"; //``` impl Shell { fn load_proposals(&mut self) { - let proposals_key = gov_storage::get_commiting_proposals_prefix( + let proposals_key = governance_storage::get_commiting_proposals_prefix( self.wl_storage.storage.last_epoch.0, ); @@ -73,7 +73,7 @@ impl Shell { for (key, _, _) in proposal_iter { let key = Key::from_str(key.as_str()).expect("Key should be parsable"); - if gov_storage::get_commit_proposal_epoch(&key).unwrap() + if governance_storage::get_commit_proposal_epoch(&key).unwrap() != self.wl_storage.storage.last_epoch.0 { // NOTE: `iter_prefix` iterate over the matching prefix. In this @@ -85,7 +85,7 @@ impl Shell { continue; } - let proposal_id = gov_storage::get_commit_proposal_id(&key); + let proposal_id = governance_storage::get_commit_proposal_id(&key); if let Some(id) = proposal_id { self.proposal_data.insert(id); } diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index db6c2a5c7ec..8af2d1a7461 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -3,6 +3,8 @@ use std::collections::HashMap; use data_encoding::HEXUPPER; +use namada::core::ledger::pgf::storage::keys as pgf_storage; +use namada::core::ledger::pgf::ADDRESS as pgf_address; use namada::ledger::parameters::storage as params_storage; use namada::ledger::pos::{namada_proof_of_stake, staking_token_address}; use namada::ledger::storage::EPOCH_SWITCH_BLOCKS_DELAY; @@ -92,8 +94,7 @@ where &mut self.wl_storage, )?; - let _proposals_result = - execute_governance_proposals(self, &mut response)?; + execute_governance_proposals(self, &mut response)?; // Copy the new_epoch + pipeline_len - 1 validator set into // new_epoch + pipeline_len @@ -817,6 +818,68 @@ where .remove(&mut self.wl_storage, &address)?; } + // Pgf inflation + let pgf_inflation_rate_key = pgf_storage::get_pgf_inflation_rate_key(); + let pgf_inflation_rate = self + .read_storage_key::(&pgf_inflation_rate_key) + .unwrap_or_default(); + + let pgf_pd_rate = pgf_inflation_rate / Dec::from(epochs_per_year); + let pgf_inflation = Dec::from(total_tokens) * pgf_pd_rate; + let pgf_inflation_amount = token::Amount::from(pgf_inflation); + + credit_tokens( + &mut self.wl_storage, + &staking_token, + &pgf_address, + pgf_inflation_amount, + )?; + + tracing::info!( + "Minting {} tokens for PGF rewards distribution into the PGF \ + account.", + pgf_inflation_amount.to_string_native() + ); + + // Pgf steward inflation + let pgf_stewards_inflation_rate_key = + pgf_storage::get_steward_inflation_rate_key(); + let pgf_stewards_inflation_rate = self + .read_storage_key::(&pgf_stewards_inflation_rate_key) + .unwrap_or_default(); + + let pgf_stewards_pd_rate = + pgf_stewards_inflation_rate / Dec::from(epochs_per_year); + let pgf_steward_inflation = + Dec::from(total_tokens) * pgf_stewards_pd_rate; + + let pgf_stewards_key = pgf_storage::get_stewards_key(); + let pgf_stewards: BTreeSet
= + self.read_storage_key(&pgf_stewards_key).unwrap_or_default(); + + let pgf_steward_reward = match pgf_stewards.len() { + 0 => Dec::zero(), + _ => pgf_steward_inflation + .trunc_div(&Dec::from(pgf_stewards.len())) + .unwrap_or_default(), + }; + let pgf_steward_reward = token::Amount::from(pgf_steward_reward); + + for steward in pgf_stewards { + credit_tokens( + &mut self.wl_storage, + &staking_token, + &steward, + pgf_steward_reward, + )?; + tracing::info!( + "Minting {} tokens for PGF Steward rewards distribution into \ + the steward address {}.", + pgf_steward_reward.to_string_native(), + steward, + ); + } + Ok(()) } @@ -941,6 +1004,11 @@ mod test_finalize_block { use data_encoding::HEXUPPER; use namada::core::ledger::eth_bridge::storage::wrapped_erc20s; + use namada::core::ledger::governance::storage::keys::get_proposal_execution_key; + use namada::core::ledger::governance::storage::proposal::ProposalType; + use namada::core::ledger::governance::storage::vote::{ + StorageProposalVote, VoteType, + }; use namada::eth_bridge::storage::bridge_pool::{ self, get_key_from_hash, get_nonce_key, get_signed_root_key, }; @@ -973,7 +1041,6 @@ mod test_finalize_block { use namada::types::ethereum_events::{ EthAddress, TransferToEthereum, Uint as ethUint, }; - use namada::types::governance::ProposalVote; use namada::types::hash::Hash; use namada::types::keccak::KeccakHash; use namada::types::key::tm_consensus_key_raw_hash; @@ -981,7 +1048,7 @@ mod test_finalize_block { use namada::types::time::{DateTimeUtc, DurationSecs}; use namada::types::token::{Amount, NATIVE_MAX_DECIMAL_PLACES}; use namada::types::transaction::governance::{ - InitProposalData, ProposalType, VoteProposalData, + InitProposalData, VoteProposalData, }; use namada::types::transaction::protocol::EthereumTxData; use namada::types::transaction::{Fee, WrapperTx, MIN_FEE_AMOUNT}; @@ -1756,11 +1823,8 @@ mod test_finalize_block { }; // Add a proposal to be accepted and one to be rejected. - add_proposal( - 0, - ProposalVote::Yay(namada::types::governance::VoteType::Default), - ); - add_proposal(1, ProposalVote::Nay); + add_proposal(0, StorageProposalVote::Yay(VoteType::Default)); + add_proposal(1, StorageProposalVote::Nay); // Commit the genesis state shell.wl_storage.commit_block().unwrap(); @@ -3507,10 +3571,9 @@ mod test_finalize_block { /// Test that updating the ethereum bridge params via governance works. #[tokio::test] async fn test_eth_bridge_param_updates() { - use namada::ledger::governance::storage as gov_storage; let (mut shell, _broadcaster, _, mut control_receiver) = setup_at_height(3u64); - let proposal_execution_key = gov_storage::get_proposal_execution_key(0); + let proposal_execution_key = get_proposal_execution_key(0); shell .wl_storage .write(&proposal_execution_key, 0u64) diff --git a/apps/src/lib/node/ledger/shell/governance.rs b/apps/src/lib/node/ledger/shell/governance.rs index c628e76bb13..09f0c9dac34 100644 --- a/apps/src/lib/node/ledger/shell/governance.rs +++ b/apps/src/lib/node/ledger/shell/governance.rs @@ -1,22 +1,30 @@ -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 std::collections::{BTreeSet, HashMap}; + +use namada::core::ledger::governance::storage::keys as gov_storage; +use namada::core::ledger::governance::storage::proposal::{ + AddRemove, PGFAction, PGFTarget, ProposalType, }; -use namada::ledger::native_vp::governance::utils::{ - compute_tally, get_proposal_votes, ProposalEvent, +use namada::core::ledger::governance::utils::{ + compute_proposal_result, ProposalVotes, TallyResult, TallyType, TallyVote, + VotePower, }; +use namada::core::ledger::governance::ADDRESS as gov_address; +use namada::core::ledger::pgf::storage::keys as pgf_storage; +use namada::core::ledger::pgf::ADDRESS; +use namada::core::ledger::storage_api::governance as gov_api; +use namada::ledger::governance::utils::ProposalEvent; +use namada::ledger::pos::BondId; 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::proof_of_stake::parameters::PosParams; +use namada::proof_of_stake::{bond_amount, read_total_stake}; use namada::proto::{Code, Data}; use namada::types::address::Address; -use namada::types::governance::{Council, Tally, TallyResult, VotePower}; use namada::types::storage::Epoch; +use super::utils::force_read; use super::*; #[derive(Default)] @@ -40,219 +48,333 @@ where 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) - .ok_or_else(|| { - Error::BadProposal(id, "Invalid proposal funds.".to_string()) - })?; - let proposal_end_epoch = shell - .read_storage_key::(&proposal_end_epoch_key) - .ok_or_else(|| { - Error::BadProposal( - id, - "Invalid proposal end_epoch.".to_string(), - ) - })?; - - 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) - .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::try_from(total_stake) - .expect("Voting power exceeds NAM supply"); - 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 { - TallyResult::Passed(tally) => { - let (successful_execution, proposal_event) = match tally { - Tally::Default => execute_default_proposal(shell, id), - Tally::PGFCouncil(council) => { - execute_pgf_proposal(id, council) + let funds: token::Amount = + force_read(&shell.wl_storage, &proposal_funds_key)?; + let proposal_end_epoch: Epoch = + force_read(&shell.wl_storage, &proposal_end_epoch_key)?; + let proposal_type: ProposalType = + force_read(&shell.wl_storage, &proposal_type_key)?; + + let params = read_pos_params(&shell.wl_storage)?; + let total_voting_power = + read_total_stake(&shell.wl_storage, ¶ms, proposal_end_epoch)?; + + let tally_type = TallyType::from(proposal_type.clone()); + let votes = compute_proposal_votes( + &shell.wl_storage, + ¶ms, + id, + proposal_end_epoch, + )?; + let proposal_result = + compute_proposal_result(votes, total_voting_power, tally_type); + + let transfer_address = match proposal_result.result { + TallyResult::Passed => { + let proposal_event = match proposal_type { + ProposalType::Default(_) => { + let proposal_code_key = + gov_storage::get_proposal_code_key(id); + let proposal_code = + shell.wl_storage.read_bytes(&proposal_code_key)?; + let result = execute_default_proposal( + shell, + id, + proposal_code.clone(), + )?; + tracing::info!( + "Governance proposal (default) {} has been \ + executed ({}) and passed.", + id, + result + ); + + ProposalEvent::default_proposal_event( + id, + proposal_code.is_some(), + result, + ) + .into() } - Tally::ETHBridge => execute_eth_proposal(id), - }; + ProposalType::PGFSteward(stewards) => { + let result = execute_pgf_steward_proposal( + &mut shell.wl_storage, + stewards, + )?; + tracing::info!( + "Governance proposal (pgf stewards){} has been \ + executed and passed.", + id + ); + + ProposalEvent::pgf_steward_proposal_event(id, result) + .into() + } + ProposalType::PGFPayment(payments) => { + let native_token = + &shell.wl_storage.get_native_token()?; + let result = execute_pgf_payment_proposal( + &mut shell.wl_storage, + native_token, + payments, + )?; + tracing::info!( + "Governance proposal (pgs payments) {} has been \ + executed and passed.", + id + ); + ProposalEvent::pgf_payments_proposal_event(id, result) + .into() + } + }; 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 - } + proposals_result.passed.push(id); + + let proposal_author_key = gov_storage::get_author_key(id); + shell.wl_storage.read::
(&proposal_author_key)? } TallyResult::Rejected => { - let proposal_event: Event = ProposalEvent::new( - EventType::Proposal.to_string(), - TallyResult::Rejected, - id, - false, - false, - ) - .into(); + if let ProposalType::PGFPayment(_) = proposal_type { + let two_third_nay = proposal_result.two_third_nay(); + if two_third_nay { + let pgf_stewards_key = pgf_storage::get_stewards_key(); + let proposal_author = gov_storage::get_author_key(id); + + let mut pgf_stewards = shell + .wl_storage + .read::>(&pgf_stewards_key)? + .unwrap_or_default(); + let proposal_author: Address = + force_read(&shell.wl_storage, &proposal_author)?; + + pgf_stewards.remove(&proposal_author); + shell + .wl_storage + .write(&pgf_stewards_key, pgf_stewards)?; + + tracing::info!( + "Governance proposal {} was rejected with 2/3 of \ + nay votes. Removing {} from stewards set.", + id, + proposal_author + ); + } + } + let proposal_event = + ProposalEvent::rejected_proposal_event(id).into(); response.events.push(proposal_event); proposals_result.rejected.push(id); - slash_fund_address + tracing::info!( + "Governance proposal {} has been executed and rejected.", + id + ); + + None } }; let native_token = shell.wl_storage.storage.native_token.clone(); - // transfer proposal locked funds - token::transfer( - &mut shell.wl_storage, - &native_token, - &gov_address, - &transfer_address, - funds, - ) - .expect( - "Must be able to transfer governance locked funds after proposal \ - has been tallied", - ); + if let Some(address) = transfer_address { + token::transfer( + &mut shell.wl_storage, + &native_token, + &gov_address, + &address, + funds, + )?; + } else { + token::burn( + &mut shell.wl_storage, + &native_token, + &gov_address, + funds, + )?; + } } Ok(proposals_result) } +fn compute_proposal_votes( + storage: &S, + params: &PosParams, + proposal_id: u64, + epoch: Epoch, +) -> storage_api::Result +where + S: StorageRead, +{ + let votes = gov_api::get_proposal_votes(storage, proposal_id)?; + + let mut validators_vote: HashMap = HashMap::default(); + let mut validator_voting_power: HashMap = + HashMap::default(); + let mut delegators_vote: HashMap = HashMap::default(); + let mut delegator_voting_power: HashMap< + Address, + HashMap, + > = HashMap::default(); + + for vote in votes { + if vote.is_validator() { + let validator = vote.validator.clone(); + let vote_data = vote.data.clone(); + + let validator_stake = + read_total_stake(storage, params, epoch).unwrap_or_default(); + + validators_vote.insert(validator.clone(), vote_data.into()); + validator_voting_power.insert(validator, validator_stake); + } else { + let validator = vote.validator.clone(); + let delegator = vote.delegator.clone(); + let vote_data = vote.data.clone(); + + let bond_id = BondId { + source: delegator.clone(), + validator: validator.clone(), + }; + let (_, delegator_stake) = + bond_amount(storage, &bond_id, epoch).unwrap_or_default(); + + delegators_vote.insert(delegator.clone(), vote_data.into()); + delegator_voting_power + .entry(delegator) + .or_default() + .insert(validator, delegator_stake); + } + } + + Ok(ProposalVotes { + validators_vote, + validator_voting_power, + delegators_vote, + delegator_voting_power, + }) +} + fn execute_default_proposal( shell: &mut Shell, id: u64, -) -> (bool, Event) + proposal_code: Option>, +) -> storage_api::Result 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 mut tx = - Tx::from_type(TxType::Decrypted(DecryptedTx::Decrypted { - #[cfg(not(feature = "mainnet"))] - has_valid_pow: false, - })); - tx.header.chain_id = shell.chain_id.clone(); - tx.set_data(Data::new(encode(&id))); - tx.set_code(Code::new(proposal_code)); - 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::dispatch_tx( - tx, - 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, - &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() => { + if let Some(code) = proposal_code { + let pending_execution_key = gov_storage::get_proposal_execution_key(id); + shell.wl_storage.write(&pending_execution_key, ())?; + + let mut tx = Tx::from_type(TxType::Decrypted(DecryptedTx::Decrypted { + #[cfg(not(feature = "mainnet"))] + has_valid_pow: false, + })); + tx.header.chain_id = shell.chain_id.clone(); + tx.set_data(Data::new(encode(&id))); + tx.set_code(Code::new(code)); + + // 0 parameter is used to compute the fee + // based on the code size. We dont + // need it here. + let tx_result = protocol::dispatch_tx( + tx, + 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, + &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(), - ) + Ok(true) + } else { + Ok(false) } } + Err(_) => { + shell.wl_storage.drop_tx(); + Ok(false) + } } - None => ( - true, - ProposalEvent::new( - EventType::Proposal.to_string(), - TallyResult::Passed(Tally::Default), - id, - false, - false, - ) - .into(), - ), + } else { + tracing::info!( + "Governance proposal {} doesn't have any associated proposal code.", + id + ); + Ok(true) } } -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_pgf_steward_proposal( + storage: &mut S, + stewards: HashSet>, +) -> Result +where + S: StorageRead + StorageWrite, +{ + let stewards_key = pgf_storage::get_stewards_key(); + let mut storage_stewards: BTreeSet
= + storage.read(&stewards_key)?.unwrap_or_default(); + + for action in stewards { + match action { + AddRemove::Add(steward) => storage_stewards.insert(steward), + AddRemove::Remove(steward) => storage_stewards.remove(&steward), + }; + } + + let write_result = storage.write(&stewards_key, storage_stewards); + Ok(write_result.is_ok()) } -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(), - ) +fn execute_pgf_payment_proposal( + storage: &mut S, + token: &Address, + payments: Vec, +) -> Result +where + S: StorageRead + StorageWrite, +{ + let continous_payments_key = pgf_storage::get_payments_key(); + let mut continous_payments: BTreeSet = + storage.read(&continous_payments_key)?.unwrap_or_default(); + + for payment in payments { + match payment { + PGFAction::Continuous(action) => match action { + AddRemove::Add(target) => { + continous_payments.insert(target); + } + AddRemove::Remove(target) => { + continous_payments.remove(&target); + } + }, + PGFAction::Retro(target) => { + token::transfer( + storage, + token, + &ADDRESS, + &target.target, + target.amount, + )?; + } + } + } + + let write_result = + storage.write(&continous_payments_key, continous_payments); + Ok(write_result.is_ok()) } diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index 32ba02e2e29..c65fbb3f99f 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -16,6 +16,7 @@ mod stats; #[cfg(any(test, feature = "testing"))] #[allow(dead_code)] pub mod testing; +pub mod utils; mod vote_extensions; use std::collections::{BTreeSet, HashSet}; diff --git a/apps/src/lib/node/ledger/shell/utils.rs b/apps/src/lib/node/ledger/shell/utils.rs new file mode 100644 index 00000000000..e55f54e8f82 --- /dev/null +++ b/apps/src/lib/node/ledger/shell/utils.rs @@ -0,0 +1,14 @@ +use borsh::BorshDeserialize; +use namada::ledger::storage_api::{self, StorageRead}; +use namada::types::storage::Key; + +pub(super) fn force_read(storage: &S, key: &Key) -> storage_api::Result +where + S: StorageRead, + T: BorshDeserialize, +{ + storage + .read::(key) + .transpose() + .expect("Storage key must be present.") +} diff --git a/apps/src/lib/wallet/defaults.rs b/apps/src/lib/wallet/defaults.rs index 81ca184f161..8ab728c96f7 100644 --- a/apps/src/lib/wallet/defaults.rs +++ b/apps/src/lib/wallet/defaults.rs @@ -8,7 +8,7 @@ pub use dev::{ validator_keys, }; use namada::ledger::wallet::alias::Alias; -use namada::ledger::{eth_bridge, governance, pos}; +use namada::ledger::{eth_bridge, governance, pgf, pos}; use namada::types::address::Address; use namada::types::key::*; @@ -22,6 +22,7 @@ pub fn addresses_from_genesis(genesis: GenesisConfig) -> Vec<(Alias, Address)> { ("pos_slash_pool".into(), pos::SLASH_POOL_ADDRESS), ("governance".into(), governance::ADDRESS), ("eth_bridge".into(), eth_bridge::ADDRESS), + ("pgf".into(), pgf::ADDRESS), ]; // Genesis validators let validator_addresses = @@ -75,7 +76,7 @@ mod dev { use borsh::BorshDeserialize; use namada::ledger::wallet::alias::Alias; - use namada::ledger::{governance, pos}; + use namada::ledger::{governance, pgf, pos}; use namada::types::address::{ apfel, btc, dot, eth, kartoffel, nam, schnitzel, Address, }; @@ -146,6 +147,7 @@ mod dev { ("pos".into(), pos::ADDRESS), ("pos_slash_pool".into(), pos::SLASH_POOL_ADDRESS), ("governance".into(), governance::ADDRESS), + ("governance".into(), pgf::ADDRESS), ("validator".into(), validator_address()), ("albert".into(), albert_address()), ("bertha".into(), bertha_address()), diff --git a/core/src/ledger/governance/cli/mod.rs b/core/src/ledger/governance/cli/mod.rs new file mode 100644 index 00000000000..45b839d1f44 --- /dev/null +++ b/core/src/ledger/governance/cli/mod.rs @@ -0,0 +1,6 @@ +/// CLi governance offline structures +pub mod offline; +/// CLi governance on chain structures +pub mod onchain; +/// CLi governance validation +mod validation; diff --git a/core/src/ledger/governance/cli/offline.rs b/core/src/ledger/governance/cli/offline.rs new file mode 100644 index 00000000000..e63ac2ff498 --- /dev/null +++ b/core/src/ledger/governance/cli/offline.rs @@ -0,0 +1,389 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fs::{File, ReadDir}; +use std::path::PathBuf; + +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; + +use super::onchain::ProposalVote; +use super::validation::{is_valid_tally_epoch, ProposalValidation}; +use crate::proto::SignatureIndex; +use crate::types::account::AccountPublicKeysMap; +use crate::types::address::Address; +use crate::types::hash::Hash; +use crate::types::key::{common, RefTo, SigScheme}; +use crate::types::storage::Epoch; + +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +/// The offline proposal structure +pub struct OfflineProposal { + /// The proposal content + pub content: BTreeMap, + /// The proposal author address + pub author: Address, + /// The epoch from which this changes are executed + pub tally_epoch: Epoch, +} + +impl OfflineProposal { + /// Validate the offline proposal + pub fn validate( + self, + current_epoch: Epoch, + ) -> Result { + is_valid_tally_epoch(self.tally_epoch, current_epoch)?; + + Ok(self) + } + + /// Hash an offline proposal + pub fn hash(&self) -> Hash { + let content_serialized = serde_json::to_vec(&self.content) + .expect("Conversion to bytes shouldn't fail."); + let author_serialized = serde_json::to_vec(&self.author) + .expect("Conversion to bytes shouldn't fail."); + let tally_epoch_serialized = serde_json::to_vec(&self.tally_epoch) + .expect("Conversion to bytes shouldn't fail."); + let proposal_serialized = &[ + content_serialized, + author_serialized, + tally_epoch_serialized, + ] + .concat(); + Hash::sha256(proposal_serialized) + } + + /// Sign an offline proposal + pub fn sign( + self, + signing_keys: Vec, + account_public_keys_map: &AccountPublicKeysMap, + ) -> OfflineSignedProposal { + let proposal_hash = self.hash(); + + let signatures_index = compute_signatures_index( + &signing_keys, + account_public_keys_map, + &proposal_hash, + ); + + OfflineSignedProposal { + proposal: self, + signatures: signatures_index, + } + } +} + +impl TryFrom<&[u8]> for OfflineProposal { + type Error = serde_json::Error; + + fn try_from(value: &[u8]) -> Result { + serde_json::from_slice(value) + } +} + +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +/// The signed offline proposal structure +pub struct OfflineSignedProposal { + /// The proposal content + pub proposal: OfflineProposal, + /// The signatures over proposal data + pub signatures: BTreeSet, +} + +impl TryFrom<&[u8]> for OfflineSignedProposal { + type Error = serde_json::Error; + + fn try_from(value: &[u8]) -> Result { + serde_json::from_slice(value) + } +} + +impl OfflineSignedProposal { + /// Serialize the proposal to file. Returns the filename if successful. + pub fn serialize( + &self, + output_folder: Option, + ) -> Result { + let proposal_filename = + format!("offline_proposal_{}.json", self.proposal.hash()); + + let filepath = match output_folder { + Some(base_path) => base_path + .join(proposal_filename) + .to_str() + .unwrap() + .to_owned(), + None => proposal_filename, + }; + + let out = + File::create(&filepath).expect("Should be able to create a file."); + serde_json::to_writer_pretty(out, self)?; + + Ok(filepath) + } + + /// Check whether the signature is valid or not + fn check_signature( + &self, + account_public_keys_map: &AccountPublicKeysMap, + threshold: u8, + ) -> bool { + let proposal_hash = self.proposal.hash(); + if self.signatures.len() < threshold as usize { + return false; + } + + let valid_signatures = compute_total_valid_signatures( + &self.signatures, + account_public_keys_map, + &proposal_hash, + ); + + valid_signatures >= threshold + } + + /// Validate an offline proposal + pub fn validate( + self, + account_public_keys_map: &AccountPublicKeysMap, + threshold: u8, + ) -> Result { + let valid_signature = + self.check_signature(account_public_keys_map, threshold); + if !valid_signature { + Err(ProposalValidation::OkNoSignature) + } else { + Ok(self) + } + } +} + +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +/// The offline proposal structure +pub struct OfflineVote { + /// The proposal data hash + pub proposal_hash: Hash, + /// The proposal vote + pub vote: ProposalVote, + /// The signature over proposal data + pub signatures: BTreeSet, + /// The address corresponding to the signature pk + pub address: Address, + /// The validators address to which this address delegated to + pub delegations: Vec
, +} + +impl OfflineVote { + /// Create an offline vote for a proposal + pub fn new( + proposal: &OfflineSignedProposal, + vote: ProposalVote, + address: Address, + delegations: Vec
, + ) -> Self { + let proposal_hash = proposal.proposal.hash(); + + Self { + proposal_hash, + vote, + delegations, + signatures: BTreeSet::default(), + address, + } + } + + /// Sign the offline vote + pub fn sign( + self, + keypairs: Vec, + account_public_keys_map: &AccountPublicKeysMap, + ) -> Self { + let proposal_vote_data = self + .vote + .try_to_vec() + .expect("Conversion to bytes shouldn't fail."); + let delegations_hash = self + .delegations + .try_to_vec() + .expect("Conversion to bytes shouldn't fail."); + + let vote_hash = Hash::sha256( + [ + self.proposal_hash.to_vec(), + proposal_vote_data, + delegations_hash, + ] + .concat(), + ); + + let signatures = compute_signatures_index( + &keypairs, + account_public_keys_map, + &vote_hash, + ); + + Self { signatures, ..self } + } + + /// Check if the vote is yay + pub fn is_yay(&self) -> bool { + self.vote.is_yay() + } + + /// compute the hash of a proposal + pub fn compute_hash(&self) -> Hash { + let proposal_hash_data = self + .proposal_hash + .try_to_vec() + .expect("Conversion to bytes shouldn't fail."); + let proposal_vote_data = self + .vote + .try_to_vec() + .expect("Conversion to bytes shouldn't fail."); + let delegations_hash = self + .delegations + .try_to_vec() + .expect("Conversion to bytes shouldn't fail."); + let vote_serialized = + &[proposal_hash_data, proposal_vote_data, delegations_hash] + .concat(); + + Hash::sha256(vote_serialized) + } + + /// Check whether the signature is valid or not + pub fn check_signature( + &self, + account_public_keys_map: &AccountPublicKeysMap, + threshold: u8, + ) -> bool { + if self.signatures.len() < threshold as usize { + return false; + } + let vote_data_hash = self.compute_hash(); + + let valid_signatures = compute_total_valid_signatures( + &self.signatures, + account_public_keys_map, + &vote_data_hash, + ); + + valid_signatures >= threshold + } + + /// Serialize the proposal to file. Returns the filename if successful. + pub fn serialize( + &self, + output_folder: Option, + ) -> Result { + let vote_filename = format!( + "offline_vote_{}_{}.json", + self.proposal_hash, self.address + ); + let filepath = match output_folder { + Some(base_path) => { + base_path.join(vote_filename).to_str().unwrap().to_owned() + } + None => vote_filename, + }; + let out = File::create(&filepath).unwrap(); + serde_json::to_writer_pretty(out, self)?; + + Ok(filepath) + } +} + +/// Compute the signatures index +fn compute_signatures_index( + keys: &[common::SecretKey], + account_public_keys_map: &AccountPublicKeysMap, + hashed_data: &Hash, +) -> BTreeSet { + keys.iter() + .filter_map(|signing_key| { + let public_key = signing_key.ref_to(); + let public_key_index = + account_public_keys_map.get_index_from_public_key(&public_key); + if public_key_index.is_some() { + let signature = + common::SigScheme::sign(signing_key, hashed_data); + Some(SignatureIndex::from_single_signature(signature)) + } else { + None + } + }) + .collect::>() +} + +/// Compute the total amount of signatures +fn compute_total_valid_signatures( + signatures: &BTreeSet, + account_public_keys_map: &AccountPublicKeysMap, + hashed_data: &Hash, +) -> u8 { + signatures.iter().fold(0_u8, |acc, signature_index| { + let public_key = account_public_keys_map + .get_public_key_from_index(signature_index.index); + if let Some(pk) = public_key { + let sig_check = common::SigScheme::verify_signature( + &pk, + hashed_data, + &signature_index.signature, + ); + if sig_check.is_ok() { acc + 1 } else { acc } + } else { + acc + } + }) +} + +/// Read all offline files from a folder +pub fn read_offline_files(path: ReadDir) -> Vec { + path.filter_map(|path| { + if let Ok(path) = path { + let file_type = path.file_type(); + if let Ok(file_type) = file_type { + if file_type.is_file() + && path.file_name().to_string_lossy().contains("offline_") + { + Some(path.path()) + } else { + None + } + } else { + None + } + } else { + None + } + }) + .collect::>() +} + +/// Find offline votes from a folder +pub fn find_offline_proposal(files: &[PathBuf]) -> Option { + files + .iter() + .filter(|path| path.to_string_lossy().contains("offline_proposal_")) + .cloned() + .collect::>() + .first() + .cloned() +} + +/// Find offline votes from a folder +pub fn find_offline_votes(files: &[PathBuf]) -> Vec { + files + .iter() + .filter(|path| path.to_string_lossy().contains("offline_vote_")) + .cloned() + .collect::>() +} diff --git a/core/src/ledger/governance/cli/onchain.rs b/core/src/ledger/governance/cli/onchain.rs new file mode 100644 index 00000000000..33cabc61566 --- /dev/null +++ b/core/src/ledger/governance/cli/onchain.rs @@ -0,0 +1,326 @@ +use std::collections::BTreeMap; + +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; + +use super::validation::{ + is_valid_author_balance, is_valid_content, is_valid_default_proposal_data, + is_valid_end_epoch, is_valid_grace_epoch, is_valid_pgf_funding_data, + is_valid_pgf_stewards_data, is_valid_proposal_period, is_valid_start_epoch, + ProposalValidation, +}; +use crate::ledger::governance::parameters::GovernanceParameters; +use crate::ledger::storage_api::token; +use crate::types::address::Address; +use crate::types::storage::Epoch; + +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +/// The proposal structure +pub struct OnChainProposal { + /// The proposal id + pub id: Option, + /// The proposal content + pub content: BTreeMap, + /// The proposal author address + pub author: Address, + /// 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, +} + +/// Pgf default proposal +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +pub struct DefaultProposal { + /// The proposal data + pub proposal: OnChainProposal, + /// The default proposal extra data + pub data: Option>, +} + +impl DefaultProposal { + /// Validate a default funding proposal + pub fn validate( + self, + governance_parameters: &GovernanceParameters, + current_epoch: Epoch, + balance: token::Amount, + ) -> Result { + is_valid_start_epoch( + self.proposal.voting_start_epoch, + current_epoch, + governance_parameters.min_proposal_voting_period, + )?; + is_valid_end_epoch( + self.proposal.voting_start_epoch, + self.proposal.voting_end_epoch, + current_epoch, + governance_parameters.min_proposal_voting_period, + governance_parameters.min_proposal_voting_period, + governance_parameters.max_proposal_period, + )?; + is_valid_grace_epoch( + self.proposal.grace_epoch, + self.proposal.voting_end_epoch, + governance_parameters.min_proposal_grace_epochs, + )?; + is_valid_proposal_period( + self.proposal.voting_start_epoch, + self.proposal.grace_epoch, + governance_parameters.max_proposal_period, + )?; + is_valid_author_balance( + balance, + governance_parameters.min_proposal_fund, + )?; + is_valid_content( + &self.proposal.content, + governance_parameters.max_proposal_content_size, + )?; + is_valid_default_proposal_data( + &self.data, + governance_parameters.max_proposal_code_size, + )?; + + Ok(self) + } +} + +impl TryFrom<&[u8]> for DefaultProposal { + type Error = serde_json::Error; + + fn try_from(value: &[u8]) -> Result { + serde_json::from_slice(value) + } +} + +/// Pgf stewards proposal +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +pub struct PgfStewardProposal { + /// The proposal data + pub proposal: OnChainProposal, + /// The Pgf steward proposal extra data + pub data: Vec, +} + +impl PgfStewardProposal { + /// Validate a Pgf stewards proposal + pub fn validate( + self, + governance_parameters: &GovernanceParameters, + current_epoch: Epoch, + balance: token::Amount, + ) -> Result { + is_valid_start_epoch( + self.proposal.voting_start_epoch, + current_epoch, + governance_parameters.min_proposal_voting_period, + )?; + is_valid_end_epoch( + self.proposal.voting_start_epoch, + self.proposal.voting_end_epoch, + current_epoch, + governance_parameters.min_proposal_voting_period, + governance_parameters.min_proposal_voting_period, + governance_parameters.max_proposal_period, + )?; + is_valid_grace_epoch( + self.proposal.grace_epoch, + self.proposal.voting_end_epoch, + governance_parameters.min_proposal_grace_epochs, + )?; + is_valid_proposal_period( + self.proposal.voting_start_epoch, + self.proposal.grace_epoch, + governance_parameters.max_proposal_period, + )?; + is_valid_author_balance( + balance, + governance_parameters.min_proposal_fund, + )?; + is_valid_content( + &self.proposal.content, + governance_parameters.max_proposal_content_size, + )?; + is_valid_pgf_stewards_data(&self.data)?; + + Ok(self) + } +} + +impl TryFrom<&[u8]> for PgfStewardProposal { + type Error = serde_json::Error; + + fn try_from(value: &[u8]) -> Result { + serde_json::from_slice(value) + } +} + +/// Pgf funding proposal +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +pub struct PgfFundingProposal { + /// The proposal data + pub proposal: OnChainProposal, + /// The Pgf funding proposal extra data + pub data: PgfFunding, +} + +impl PgfFundingProposal { + /// Validate a Pgf funding proposal + pub fn validate( + self, + governance_parameters: &GovernanceParameters, + current_epoch: Epoch, + ) -> Result { + is_valid_start_epoch( + self.proposal.voting_start_epoch, + current_epoch, + governance_parameters.min_proposal_voting_period, + )?; + is_valid_end_epoch( + self.proposal.voting_start_epoch, + self.proposal.voting_end_epoch, + current_epoch, + governance_parameters.min_proposal_voting_period, + governance_parameters.min_proposal_voting_period, + governance_parameters.max_proposal_period, + )?; + is_valid_grace_epoch( + self.proposal.grace_epoch, + self.proposal.voting_end_epoch, + governance_parameters.min_proposal_grace_epochs, + )?; + is_valid_proposal_period( + self.proposal.voting_start_epoch, + self.proposal.grace_epoch, + governance_parameters.max_proposal_period, + )?; + is_valid_content( + &self.proposal.content, + governance_parameters.max_proposal_content_size, + )?; + is_valid_pgf_funding_data(&self.data)?; + + Ok(self) + } +} + +impl TryFrom<&[u8]> for PgfFundingProposal { + type Error = serde_json::Error; + + fn try_from(value: &[u8]) -> Result { + serde_json::from_slice(value) + } +} + +/// Pgf stewards +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +pub struct PgfSteward { + /// Pgf action + pub action: PgfAction, + /// steward address + pub address: Address, +} + +/// Pgf action +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +pub enum PgfAction { + /// Add action + Add, + /// Remove action + Remove, +} + +/// Pgf fundings +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +pub struct PgfFunding { + /// Pgf continous funding + pub continous: Vec, + /// pgf retro fundings + pub retro: Vec, +} + +/// Pgf continous funding +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +pub struct PgfContinous { + /// Pgf target + pub target: PgfFundingTarget, + /// Pgf action + pub action: PgfAction, +} + +/// Pgf retro funding +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +pub struct PgfRetro { + /// Pgf retro target + pub target: PgfFundingTarget, +} + +/// Pgf Target +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +pub struct PgfFundingTarget { + /// Target amount + pub amount: token::Amount, + /// Target address + pub address: Address, +} + +/// Rappresent an proposal vote +#[derive( + Debug, + Clone, + BorshSerialize, + BorshDeserialize, + Serialize, + Deserialize, + PartialEq, +)] +pub enum ProposalVote { + /// Rappresent an yay proposal vote + Yay, + /// Rappresent an nay proposal vote + Nay, + /// Rappresent an invalid proposal vote + Invalid, +} + +impl TryFrom for ProposalVote { + type Error = String; + + fn try_from(value: String) -> Result { + match value.trim().to_lowercase().as_str() { + "yay" => Ok(ProposalVote::Yay), + "nay" => Ok(ProposalVote::Nay), + _ => Err("invalid vote".to_string()), + } + } +} + +impl ProposalVote { + /// Check if the proposal type is yay + pub fn is_yay(&self) -> bool { + matches!(self, ProposalVote::Yay) + } +} diff --git a/core/src/ledger/governance/cli/utils.rs b/core/src/ledger/governance/cli/utils.rs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/core/src/ledger/governance/cli/validation.rs b/core/src/ledger/governance/cli/validation.rs new file mode 100644 index 00000000000..585a3362ad3 --- /dev/null +++ b/core/src/ledger/governance/cli/validation.rs @@ -0,0 +1,241 @@ +use std::collections::BTreeMap; + +use thiserror::Error; + +use super::onchain::{PgfFunding, PgfSteward}; +use crate::types::storage::Epoch; +use crate::types::token; + +/// This enum raprresent a proposal data +#[derive(Clone, Debug, PartialEq, Error)] +pub enum ProposalValidation { + /// The proposal field are correct but there is no signature + #[error("The proposal is not signed. Can't vote on it")] + OkNoSignature, + /// The proposal start epoch is invalid + #[error( + "Invalid proposal start epoch: {0} must be greater than current epoch \ + {1} and a multiple of {2}" + )] + InvalidStartEpoch(Epoch, Epoch, u64), + /// The proposal difference between start and end epoch is invalid + #[error( + "Invalid proposal end epoch: difference between proposal start and \ + end epoch must be at least {0}, at max {1} and the end epoch must be \ + a multiple of {0}" + )] + InvalidStartEndDifference(u64, u64), + /// The proposal difference between end and grace epoch is invalid + #[error( + "Invalid proposal grace epoch: difference between proposal grace and \ + end epoch must be at least {0}, but found {1}" + )] + InvalidEndGraceDifference(u64, u64), + /// The proposal difference between end and grace epoch is invalid + #[error( + "Invalid proposal period: difference between proposal start and grace \ + epoch must be at most {1}, but found {0}" + )] + InvalidProposalPeriod(u64, u64), + /// The proposal author does not have enought balance to pay for proposal + /// fees + #[error( + "Invalid proposal minimum funds: the author address has {0} but \ + minimum is {1}" + )] + InvalidBalance(String, String), + /// The proposal content is too large + #[error( + "Invalid proposal content length: the proposal content length is {0} \ + but maximum is {1}" + )] + InvalidContentLength(u64, u64), + /// Invalid offline proposal tally epoch + #[error( + "Invalid proposal tally epoch: tally epoch ({0}) must be less than \ + current epoch ({1})" + )] + InvalidTallyEPoch(Epoch, Epoch), + /// The proposal wasm code is not valid + #[error( + "Invalid proposal extra data: file doesn't exist or content size \ + ({0}) is to big (max {1})" + )] + InvalidDefaultProposalExtraData(u64, u64), + /// The pgf stewards data is not valid + #[error("Invalid proposal extra data: cannot be empty.")] + InvalidPgfStewardsExtraData, + /// The pgf funding data is not valid + #[error("invalid proposal extra data: cannot be empty.")] + InvalidPgfFundingExtraData, +} + +pub fn is_valid_author_balance( + author_balance: token::Amount, + min_proposal_fund: token::Amount, +) -> Result<(), ProposalValidation> { + if author_balance.can_spend(&min_proposal_fund) { + Ok(()) + } else { + Err(ProposalValidation::InvalidBalance( + author_balance.to_string_native(), + min_proposal_fund.to_string_native(), + )) + } +} + +pub fn is_valid_start_epoch( + proposal_start_epoch: Epoch, + current_epoch: Epoch, + proposal_epoch_multiplier: u64, +) -> Result<(), ProposalValidation> { + let start_epoch_greater_than_current = proposal_start_epoch > current_epoch; + let start_epoch_is_multipler = + proposal_start_epoch.0 % proposal_epoch_multiplier == 0; + + if start_epoch_greater_than_current && start_epoch_is_multipler { + Ok(()) + } else { + Err(ProposalValidation::InvalidStartEpoch( + proposal_start_epoch, + current_epoch, + proposal_epoch_multiplier, + )) + } +} + +pub fn is_valid_end_epoch( + proposal_start_epoch: Epoch, + proposal_end_epoch: Epoch, + _current_epoch: Epoch, + proposal_epoch_multiplier: u64, + min_proposal_voting_period: u64, + max_proposal_period: u64, +) -> Result<(), ProposalValidation> { + let voting_period = proposal_end_epoch.0 - proposal_start_epoch.0; + let end_epoch_is_multipler = + proposal_end_epoch % proposal_epoch_multiplier == 0; + let is_valid_voting_period = voting_period > 0 + && voting_period >= min_proposal_voting_period + && min_proposal_voting_period <= max_proposal_period; + + if end_epoch_is_multipler && is_valid_voting_period { + Ok(()) + } else { + Err(ProposalValidation::InvalidStartEndDifference( + min_proposal_voting_period, + max_proposal_period, + )) + } +} + +pub fn is_valid_grace_epoch( + proposal_grace_epoch: Epoch, + proposal_end_epoch: Epoch, + min_proposal_grace_epoch: u64, +) -> Result<(), ProposalValidation> { + let grace_period = proposal_grace_epoch.0 - proposal_end_epoch.0; + + if grace_period > 0 && grace_period >= min_proposal_grace_epoch { + Ok(()) + } else { + Err(ProposalValidation::InvalidEndGraceDifference( + grace_period, + min_proposal_grace_epoch, + )) + } +} + +pub fn is_valid_proposal_period( + proposal_start_epoch: Epoch, + proposal_grace_epoch: Epoch, + max_proposal_period: u64, +) -> Result<(), ProposalValidation> { + let proposal_period = proposal_grace_epoch.0 - proposal_start_epoch.0; + + if proposal_period > 0 && proposal_period <= max_proposal_period { + Ok(()) + } else { + Err(ProposalValidation::InvalidProposalPeriod( + proposal_period, + max_proposal_period, + )) + } +} + +pub fn is_valid_content( + proposal_content: &BTreeMap, + max_content_length: u64, +) -> Result<(), ProposalValidation> { + let proposal_content_keys_length: u64 = + proposal_content.keys().map(|key| key.len() as u64).sum(); + let proposal_content_values_length: u64 = proposal_content + .values() + .map(|value| value.len() as u64) + .sum(); + let proposal_content_length = + proposal_content_values_length + proposal_content_keys_length; + + if proposal_content_length <= max_content_length { + Ok(()) + } else { + Err(ProposalValidation::InvalidContentLength( + proposal_content_length, + max_content_length, + )) + } +} + +pub fn is_valid_tally_epoch( + tally_epoch: Epoch, + current_epoch: Epoch, +) -> Result<(), ProposalValidation> { + if tally_epoch <= current_epoch { + Ok(()) + } else { + Err(ProposalValidation::InvalidTallyEPoch( + tally_epoch, + current_epoch, + )) + } +} + +pub fn is_valid_default_proposal_data( + data: &Option>, + max_extra_data_size: u64, +) -> Result<(), ProposalValidation> { + match data { + Some(content) => { + let extra_data_length = content.len() as u64; + if extra_data_length <= max_extra_data_size { + Ok(()) + } else { + Err(ProposalValidation::InvalidDefaultProposalExtraData( + extra_data_length, + max_extra_data_size, + )) + } + } + None => Ok(()), + } +} + +pub fn is_valid_pgf_stewards_data( + data: &Vec, +) -> Result<(), ProposalValidation> { + if !data.is_empty() { + Ok(()) + } else { + Err(ProposalValidation::InvalidPgfStewardsExtraData) + } +} + +pub fn is_valid_pgf_funding_data( + data: &PgfFunding, +) -> Result<(), ProposalValidation> { + if !data.continous.is_empty() || !data.retro.is_empty() { + Ok(()) + } else { + Err(ProposalValidation::InvalidPgfFundingExtraData) + } +} diff --git a/core/src/ledger/governance/mod.rs b/core/src/ledger/governance/mod.rs index ae488383bfc..00fcb3a990d 100644 --- a/core/src/ledger/governance/mod.rs +++ b/core/src/ledger/governance/mod.rs @@ -2,10 +2,14 @@ use crate::types::address::{self, Address}; +/// governance CLI structures +pub mod cli; /// governance parameters pub mod parameters; /// governance storage pub mod storage; +/// Governance utility functions/structs +pub mod utils; /// The governance internal address pub const ADDRESS: Address = address::GOV; diff --git a/core/src/ledger/governance/parameters.rs b/core/src/ledger/governance/parameters.rs index 2d247bc24f2..ebb28372af6 100644 --- a/core/src/ledger/governance/parameters.rs +++ b/core/src/ledger/governance/parameters.rs @@ -2,9 +2,9 @@ use std::fmt::Display; use borsh::{BorshDeserialize, BorshSerialize}; -use super::storage as gov_storage; +use super::storage::keys as goverance_storage; use crate::ledger::storage_api::{self, StorageRead, StorageWrite}; -use crate::types::token::Amount; +use crate::types::token; #[derive( Clone, @@ -18,13 +18,13 @@ use crate::types::token::Amount; BorshDeserialize, )] /// Governance parameter structure -pub struct GovParams { +pub struct GovernanceParameters { /// Minimum amount of locked funds - pub min_proposal_fund: u64, + pub min_proposal_fund: token::Amount, /// Maximum kibibyte length for proposal code pub max_proposal_code_size: u64, /// Minimum proposal voting period in epochs - pub min_proposal_period: u64, + pub min_proposal_voting_period: u64, /// Maximum proposal voting period in epochs pub max_proposal_period: u64, /// Maximum number of characters for proposal content @@ -33,16 +33,16 @@ pub struct GovParams { pub min_proposal_grace_epochs: u64, } -impl Display for GovParams { +impl Display for GovernanceParameters { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "Min. proposal fund: {}\nMax. proposal code size: {}\nMin. \ proposal period: {}\nMax. proposal period: {}\nMax. proposal \ content size: {}\nMin. proposal grace epochs: {}", - self.min_proposal_fund, + self.min_proposal_fund.to_string_native(), self.max_proposal_code_size, - self.min_proposal_period, + self.min_proposal_voting_period, self.max_proposal_period, self.max_proposal_content_size, self.min_proposal_grace_epochs @@ -50,12 +50,12 @@ impl Display for GovParams { } } -impl Default for GovParams { +impl Default for GovernanceParameters { fn default() -> Self { Self { - min_proposal_fund: 500, + min_proposal_fund: token::Amount::native_whole(500), max_proposal_code_size: 300_000, - min_proposal_period: 3, + min_proposal_voting_period: 3, max_proposal_period: 27, max_proposal_content_size: 10_000, min_proposal_grace_epochs: 6, @@ -63,7 +63,7 @@ impl Default for GovParams { } } -impl GovParams { +impl GovernanceParameters { /// Initialize governance parameters into storage pub fn init_storage(&self, storage: &mut S) -> storage_api::Result<()> where @@ -72,39 +72,42 @@ impl GovParams { let Self { min_proposal_fund, max_proposal_code_size, - min_proposal_period, + min_proposal_voting_period, max_proposal_period, max_proposal_content_size, min_proposal_grace_epochs, } = self; - let min_proposal_fund_key = gov_storage::get_min_proposal_fund_key(); - let amount = Amount::native_whole(*min_proposal_fund); - storage.write(&min_proposal_fund_key, amount)?; + let min_proposal_fund_key = + goverance_storage::get_min_proposal_fund_key(); + storage.write(&min_proposal_fund_key, min_proposal_fund)?; let max_proposal_code_size_key = - gov_storage::get_max_proposal_code_size_key(); + goverance_storage::get_max_proposal_code_size_key(); storage.write(&max_proposal_code_size_key, max_proposal_code_size)?; - let min_proposal_period_key = - gov_storage::get_min_proposal_period_key(); - storage.write(&min_proposal_period_key, min_proposal_period)?; + let min_proposal_voting_period_key = + goverance_storage::get_min_proposal_voting_period_key(); + storage.write( + &min_proposal_voting_period_key, + min_proposal_voting_period, + )?; let max_proposal_period_key = - gov_storage::get_max_proposal_period_key(); + goverance_storage::get_max_proposal_period_key(); storage.write(&max_proposal_period_key, max_proposal_period)?; let max_proposal_content_size_key = - gov_storage::get_max_proposal_content_key(); + goverance_storage::get_max_proposal_content_key(); storage .write(&max_proposal_content_size_key, max_proposal_content_size)?; let min_proposal_grace_epoch_key = - gov_storage::get_min_proposal_grace_epoch_key(); + goverance_storage::get_min_proposal_grace_epoch_key(); storage .write(&min_proposal_grace_epoch_key, min_proposal_grace_epochs)?; - let counter_key = gov_storage::get_counter_key(); + let counter_key = goverance_storage::get_counter_key(); storage.write(&counter_key, u64::MIN) } } diff --git a/core/src/ledger/governance/storage.rs b/core/src/ledger/governance/storage/keys.rs similarity index 78% rename from core/src/ledger/governance/storage.rs rename to core/src/ledger/governance/storage/keys.rs index e00c4be6787..a975b6541fd 100644 --- a/core/src/ledger/governance/storage.rs +++ b/core/src/ledger/governance/storage/keys.rs @@ -1,27 +1,32 @@ +use namada_macros::StorageKeys; + use crate::ledger::governance::ADDRESS; use crate::types::address::Address; 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"; -const PROPOSAL_GRACE_EPOCH: &str = "grace_epoch"; -const PROPOSAL_FUNDS: &str = "funds"; -const PROPOSAL_CODE: &str = "proposal_code"; -const PROPOSAL_COMMITTING_EPOCH: &str = "epoch"; - -const MIN_PROPOSAL_FUND_KEY: &str = "min_fund"; -const MAX_PROPOSAL_CODE_SIZE_KEY: &str = "max_code_size"; -const MIN_PROPOSAL_PERIOD_KEY: &str = "min_period"; -const MAX_PROPOSAL_PERIOD_KEY: &str = "max_period"; -const MAX_PROPOSAL_CONTENT_SIZE_KEY: &str = "max_content"; -const MIN_GRACE_EPOCH_KEY: &str = "min_grace_epoch"; -const COUNTER_KEY: &str = "counter"; -const PENDING_PROPOSAL: &str = "pending"; +/// Storage keys for governance internal address. +#[derive(StorageKeys)] +struct Keys { + proposal: &'static str, + vote: &'static str, + author: &'static str, + proposal_type: &'static str, + content: &'static str, + start_epoch: &'static str, + end_epoch: &'static str, + grace_epoch: &'static str, + funds: &'static str, + proposal_code: &'static str, + committing_epoch: &'static str, + min_fund: &'static str, + max_code_size: &'static str, + min_period: &'static str, + max_period: &'static str, + max_content: &'static str, + min_grace_epoch: &'static str, + counter: &'static str, + pending: &'static str, +} /// Check if key is inside governance address space pub fn is_governance_key(key: &Key) -> bool { @@ -39,8 +44,8 @@ pub fn is_vote_key(key: &Key) -> bool { DbKeySeg::AddressSeg(_validator_address), DbKeySeg::AddressSeg(_address), ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && vote == PROPOSAL_VOTE => + && prefix == Keys::VALUES.proposal + && vote == Keys::VALUES.vote => { id.parse::().is_ok() } @@ -57,8 +62,8 @@ pub fn is_author_key(key: &Key) -> bool { DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(author), ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && author == PROPOSAL_AUTHOR => + && prefix == Keys::VALUES.proposal + && author == Keys::VALUES.author => { id.parse::().is_ok() } @@ -75,8 +80,8 @@ pub fn is_proposal_code_key(key: &Key) -> bool { DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(proposal_code), ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && proposal_code == PROPOSAL_CODE => + && prefix == Keys::VALUES.proposal + && proposal_code == Keys::VALUES.proposal_code => { id.parse::().is_ok() } @@ -93,8 +98,8 @@ pub fn is_grace_epoch_key(key: &Key) -> bool { DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(grace_epoch), ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && grace_epoch == PROPOSAL_GRACE_EPOCH => + && prefix == Keys::VALUES.proposal + && grace_epoch == Keys::VALUES.grace_epoch => { id.parse::().is_ok() } @@ -111,8 +116,8 @@ pub fn is_content_key(key: &Key) -> bool { DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(content), ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && content == PROPOSAL_CONTENT => + && prefix == Keys::VALUES.proposal + && content == Keys::VALUES.content => { id.parse::().is_ok() } @@ -129,8 +134,8 @@ pub fn is_balance_key(key: &Key) -> bool { DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(funds), ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && funds == PROPOSAL_FUNDS => + && prefix == Keys::VALUES.proposal + && funds == Keys::VALUES.funds => { id.parse::().is_ok() } @@ -147,8 +152,8 @@ pub fn is_start_epoch_key(key: &Key) -> bool { DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(start_epoch), ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && start_epoch == PROPOSAL_START_EPOCH => + && prefix == Keys::VALUES.proposal + && start_epoch == Keys::VALUES.start_epoch => { id.parse::().is_ok() } @@ -165,8 +170,8 @@ pub fn is_end_epoch_key(key: &Key) -> bool { DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(end_epoch), ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && end_epoch == PROPOSAL_END_EPOCH => + && prefix == Keys::VALUES.proposal + && end_epoch == Keys::VALUES.end_epoch => { id.parse::().is_ok() } @@ -183,8 +188,8 @@ pub fn is_proposal_type_key(key: &Key) -> bool { DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(proposal_type), ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && proposal_type == PROPOSAL_TYPE => + && prefix == Keys::VALUES.proposal + && proposal_type == Keys::VALUES.proposal_type => { id.parse::().is_ok() } @@ -194,7 +199,7 @@ pub fn is_proposal_type_key(key: &Key) -> bool { /// Check if key is counter key pub fn is_counter_key(key: &Key) -> bool { - matches!(&key.segments[..], [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(counter)] if addr == &ADDRESS && counter == COUNTER_KEY) + matches!(&key.segments[..], [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(counter)] if addr == &ADDRESS && counter == Keys::VALUES.counter) } /// Check if key is a proposal fund parameter key @@ -202,7 +207,7 @@ pub fn is_min_proposal_fund_key(key: &Key) -> bool { matches!(&key.segments[..], [ DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(min_funds_param), - ] if addr == &ADDRESS && min_funds_param == MIN_PROPOSAL_FUND_KEY) + ] if addr == &ADDRESS && min_funds_param == Keys::VALUES.min_fund) } /// Check if key is a proposal max content parameter key @@ -211,7 +216,7 @@ pub fn is_max_content_size_key(key: &Key) -> bool { DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(max_content_size_param), ] if addr == &ADDRESS - && max_content_size_param == MAX_PROPOSAL_CONTENT_SIZE_KEY) + && max_content_size_param == Keys::VALUES.max_content) } /// Check if key is a max proposal size key @@ -220,16 +225,16 @@ pub fn is_max_proposal_code_size_key(key: &Key) -> bool { DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(max_content_size_param), ] if addr == &ADDRESS - && max_content_size_param == MAX_PROPOSAL_CONTENT_SIZE_KEY) + && max_content_size_param == Keys::VALUES.max_code_size) } /// Check if key is a min proposal period param key -pub fn is_min_proposal_period_key(key: &Key) -> bool { +pub fn is_min_proposal_voting_period_key(key: &Key) -> bool { matches!(&key.segments[..], [ DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(min_proposal_period_param), + DbKeySeg::StringSeg(min_proposal_voting_period_param), ] if addr == &ADDRESS - && min_proposal_period_param == MIN_PROPOSAL_PERIOD_KEY) + && min_proposal_voting_period_param == Keys::VALUES.min_period) } /// Check if key is a max proposal period param key @@ -238,7 +243,7 @@ pub fn is_max_proposal_period_key(key: &Key) -> bool { DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(max_proposal_period_param), ] if addr == &ADDRESS - && max_proposal_period_param == MAX_PROPOSAL_PERIOD_KEY) + && max_proposal_period_param == Keys::VALUES.max_period) } /// Check if key is a min grace epoch key @@ -250,8 +255,8 @@ pub fn is_commit_proposal_key(key: &Key) -> bool { DbKeySeg::StringSeg(_epoch), DbKeySeg::StringSeg(_id), ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && epoch_prefix == PROPOSAL_COMMITTING_EPOCH + && prefix == Keys::VALUES.proposal + && epoch_prefix == Keys::VALUES.committing_epoch ) } @@ -261,7 +266,7 @@ pub fn is_min_grace_epoch_key(key: &Key) -> bool { DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(min_grace_epoch_param), ] if addr == &ADDRESS - && min_grace_epoch_param == MIN_GRACE_EPOCH_KEY) + && min_grace_epoch_param == Keys::VALUES.min_grace_epoch) } /// Check if key is parameter key @@ -269,7 +274,7 @@ pub fn is_parameter_key(key: &Key) -> bool { is_min_proposal_fund_key(key) || is_max_content_size_key(key) || is_max_proposal_code_size_key(key) - || is_min_proposal_period_key(key) + || is_min_proposal_voting_period_key(key) || is_max_proposal_period_key(key) || is_min_grace_epoch_key(key) } @@ -282,56 +287,56 @@ pub fn is_start_or_end_epoch_key(key: &Key) -> bool { /// Get governance prefix key pub fn proposal_prefix() -> Key { Key::from(ADDRESS.to_db_key()) - .push(&PROPOSAL_PREFIX.to_owned()) + .push(&Keys::VALUES.proposal.to_owned()) .expect("Cannot obtain a storage key") } /// Get key for the minimum proposal fund pub fn get_min_proposal_fund_key() -> Key { Key::from(ADDRESS.to_db_key()) - .push(&MIN_PROPOSAL_FUND_KEY.to_owned()) + .push(&Keys::VALUES.min_fund.to_owned()) .expect("Cannot obtain a storage key") } /// Get maximum proposal code size key pub fn get_max_proposal_code_size_key() -> Key { Key::from(ADDRESS.to_db_key()) - .push(&MAX_PROPOSAL_CODE_SIZE_KEY.to_owned()) + .push(&Keys::VALUES.max_code_size.to_owned()) .expect("Cannot obtain a storage key") } /// Get minimum proposal period key -pub fn get_min_proposal_period_key() -> Key { +pub fn get_min_proposal_voting_period_key() -> Key { Key::from(ADDRESS.to_db_key()) - .push(&MIN_PROPOSAL_PERIOD_KEY.to_owned()) + .push(&Keys::VALUES.min_period.to_owned()) .expect("Cannot obtain a storage key") } /// Get maximum proposal period key pub fn get_max_proposal_period_key() -> Key { Key::from(ADDRESS.to_db_key()) - .push(&MAX_PROPOSAL_PERIOD_KEY.to_owned()) + .push(&Keys::VALUES.max_period.to_owned()) .expect("Cannot obtain a storage key") } /// Get maximum proposal content key pub fn get_max_proposal_content_key() -> Key { Key::from(ADDRESS.to_db_key()) - .push(&MAX_PROPOSAL_CONTENT_SIZE_KEY.to_owned()) + .push(&Keys::VALUES.max_content.to_owned()) .expect("Cannot obtain a storage key") } /// Get min grace epoch proposal key pub fn get_min_proposal_grace_epoch_key() -> Key { Key::from(ADDRESS.to_db_key()) - .push(&MIN_GRACE_EPOCH_KEY.to_owned()) + .push(&Keys::VALUES.min_grace_epoch.to_owned()) .expect("Cannot obtain a storage key") } /// Get key of proposal ids counter pub fn get_counter_key() -> Key { Key::from(ADDRESS.to_db_key()) - .push(&COUNTER_KEY.to_owned()) + .push(&Keys::VALUES.counter.to_owned()) .expect("Cannot obtain a storage key") } @@ -340,7 +345,7 @@ pub fn get_content_key(id: u64) -> Key { proposal_prefix() .push(&id.to_string()) .expect("Cannot obtain a storage key") - .push(&PROPOSAL_CONTENT.to_owned()) + .push(&Keys::VALUES.content.to_owned()) .expect("Cannot obtain a storage key") } @@ -349,7 +354,7 @@ pub fn get_author_key(id: u64) -> Key { proposal_prefix() .push(&id.to_string()) .expect("Cannot obtain a storage key") - .push(&PROPOSAL_AUTHOR.to_owned()) + .push(&Keys::VALUES.author.to_owned()) .expect("Cannot obtain a storage key") } @@ -358,7 +363,7 @@ 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()) + .push(&Keys::VALUES.proposal_type.to_owned()) .expect("Cannot obtain a storage key") } @@ -367,7 +372,7 @@ pub fn get_voting_start_epoch_key(id: u64) -> Key { proposal_prefix() .push(&id.to_string()) .expect("Cannot obtain a storage key") - .push(&PROPOSAL_START_EPOCH.to_owned()) + .push(&Keys::VALUES.start_epoch.to_owned()) .expect("Cannot obtain a storage key") } @@ -376,7 +381,7 @@ pub fn get_voting_end_epoch_key(id: u64) -> Key { proposal_prefix() .push(&id.to_string()) .expect("Cannot obtain a storage key") - .push(&PROPOSAL_END_EPOCH.to_owned()) + .push(&Keys::VALUES.end_epoch.to_owned()) .expect("Cannot obtain a storage key") } @@ -385,7 +390,7 @@ pub fn get_funds_key(id: u64) -> Key { proposal_prefix() .push(&id.to_string()) .expect("Cannot obtain a storage key") - .push(&PROPOSAL_FUNDS.to_owned()) + .push(&Keys::VALUES.funds.to_owned()) .expect("Cannot obtain a storage key") } @@ -394,14 +399,14 @@ pub fn get_grace_epoch_key(id: u64) -> Key { proposal_prefix() .push(&id.to_string()) .expect("Cannot obtain a storage key") - .push(&PROPOSAL_GRACE_EPOCH.to_owned()) + .push(&Keys::VALUES.grace_epoch.to_owned()) .expect("Cannot obtain a storage key") } /// Get the proposal committing key prefix pub fn get_commiting_proposals_prefix(epoch: u64) -> Key { proposal_prefix() - .push(&PROPOSAL_COMMITTING_EPOCH.to_owned()) + .push(&Keys::VALUES.committing_epoch.to_owned()) .expect("Cannot obtain a storage key") .push(&epoch.to_string()) .expect("Cannot obtain a storage key") @@ -412,7 +417,7 @@ pub fn get_proposal_code_key(id: u64) -> Key { proposal_prefix() .push(&id.to_string()) .expect("Cannot obtain a storage key") - .push(&PROPOSAL_CODE.to_owned()) + .push(&Keys::VALUES.proposal_code.to_owned()) .expect("Cannot obtain a storage key") } @@ -428,7 +433,7 @@ pub fn get_proposal_vote_prefix_key(id: u64) -> Key { proposal_prefix() .push(&id.to_string()) .expect("Cannot obtain a storage key") - .push(&PROPOSAL_VOTE.to_owned()) + .push(&Keys::VALUES.vote.to_owned()) .expect("Cannot obtain a storage key") } @@ -448,7 +453,7 @@ pub fn get_vote_proposal_key( /// Get the proposal execution key pub fn get_proposal_execution_key(id: u64) -> Key { Key::from(ADDRESS.to_db_key()) - .push(&PENDING_PROPOSAL.to_owned()) + .push(&Keys::VALUES.pending.to_owned()) .expect("Cannot obtain a storage key") .push(&id.to_string()) .expect("Cannot obtain a storage key") diff --git a/core/src/ledger/governance/storage/mod.rs b/core/src/ledger/governance/storage/mod.rs new file mode 100644 index 00000000000..e2de8e6ab89 --- /dev/null +++ b/core/src/ledger/governance/storage/mod.rs @@ -0,0 +1,6 @@ +/// Governance proposal keys +pub mod keys; +/// Proposal structures +pub mod proposal; +/// Vote structures +pub mod vote; diff --git a/core/src/ledger/governance/storage/proposal.rs b/core/src/ledger/governance/storage/proposal.rs new file mode 100644 index 00000000000..b5ac1284c47 --- /dev/null +++ b/core/src/ledger/governance/storage/proposal.rs @@ -0,0 +1,270 @@ +use std::collections::{BTreeMap, HashSet}; +use std::fmt::Display; + +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::ledger::governance::cli::onchain::{ + PgfAction, PgfContinous, PgfRetro, PgfSteward, +}; +use crate::ledger::governance::utils::{ProposalStatus, TallyType}; +use crate::ledger::storage_api::token::Amount; +use crate::types::address::Address; +use crate::types::hash::Hash; +use crate::types::storage::Epoch; + +#[allow(missing_docs)] +#[derive(Debug, Error)] +pub enum ProposalTypeError { + #[error("Invalid proposal type.")] + InvalidProposalType, +} + +/// An add or remove action for PGF +#[derive( + Debug, + Clone, + Hash, + PartialEq, + Eq, + PartialOrd, + BorshSerialize, + BorshDeserialize, + Serialize, + Deserialize, +)] +pub enum AddRemove { + /// Add + Add(T), + /// Remove + Remove(T), +} + +/// The target of a PGF payment +#[derive( + Debug, + Clone, + PartialEq, + BorshSerialize, + BorshDeserialize, + Serialize, + Deserialize, + Ord, + Eq, + PartialOrd, +)] +pub struct PGFTarget { + /// The target address + pub target: Address, + /// The amount of token to fund the target address + pub amount: Amount, +} + +/// The actions that a PGF Steward can propose to execute +#[derive( + Debug, + Clone, + PartialEq, + BorshSerialize, + BorshDeserialize, + Serialize, + Deserialize, +)] +pub enum PGFAction { + /// A continuous payment + Continuous(AddRemove), + /// A retro payment + Retro(PGFTarget), +} + +/// The type of a Proposal +#[derive( + Debug, + Clone, + PartialEq, + BorshSerialize, + BorshDeserialize, + Serialize, + Deserialize, +)] +pub enum ProposalType { + /// Default governance proposal with the optional wasm code + Default(Option), + /// PGF stewards proposal + PGFSteward(HashSet>), + /// PGF funding proposal + PGFPayment(Vec), +} + +impl ProposalType { + /// Check if the proposal type is default + pub fn is_default(&self) -> bool { + matches!(self, ProposalType::Default(_)) + } +} + +impl Display for ProposalType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ProposalType::Default(_) => write!(f, "Default"), + ProposalType::PGFSteward(_) => write!(f, "Pgf steward"), + ProposalType::PGFPayment(_) => write!(f, "Pgf funding"), + } + } +} + +impl TryFrom for AddRemove
{ + type Error = ProposalTypeError; + + fn try_from(value: PgfSteward) -> Result { + match value.action { + PgfAction::Add => Ok(AddRemove::Add(value.address)), + PgfAction::Remove => Ok(AddRemove::Remove(value.address)), + } + } +} + +impl TryFrom for PGFAction { + type Error = ProposalTypeError; + + fn try_from(value: PgfContinous) -> Result { + match value.action { + PgfAction::Add => { + Ok(PGFAction::Continuous(AddRemove::Add(PGFTarget { + target: value.target.address, + amount: value.target.amount, + }))) + } + PgfAction::Remove => { + Ok(PGFAction::Continuous(AddRemove::Remove(PGFTarget { + target: value.target.address, + amount: value.target.amount, + }))) + } + } + } +} + +impl TryFrom for PGFAction { + type Error = ProposalTypeError; + + fn try_from(value: PgfRetro) -> Result { + Ok(PGFAction::Retro(PGFTarget { + target: value.target.address, + amount: value.target.amount, + })) + } +} + +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] +/// Proposal rappresentation when fetched from the storage +pub struct StorageProposal { + /// The proposal id + pub id: u64, + /// The proposal content + 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, +} + +impl StorageProposal { + /// Check if the proposal can be voted + pub fn can_be_voted( + &self, + current_epoch: Epoch, + is_validator: bool, + ) -> bool { + if is_validator { + self.voting_start_epoch < self.voting_end_epoch + && current_epoch * 3 + <= self.voting_start_epoch + self.voting_end_epoch * 2 + } else { + let valid_start_epoch = current_epoch >= self.voting_start_epoch; + let valid_end_epoch = current_epoch <= self.voting_end_epoch; + valid_start_epoch && valid_end_epoch + } + } + + /// Return the type of tally for the proposal + pub fn get_tally_type(&self) -> TallyType { + TallyType::from(self.r#type.clone()) + } + + /// Return the status of a proposal + pub fn get_status(&self, current_epoch: Epoch) -> ProposalStatus { + if self.voting_start_epoch > self.voting_end_epoch { + ProposalStatus::Pending + } else if self.voting_start_epoch <= current_epoch + && current_epoch <= self.voting_end_epoch + { + ProposalStatus::OnGoing + } else { + ProposalStatus::Ended + } + } + + /// Serialize a proposal to string + pub fn to_string_with_status(&self, current_epoch: Epoch) -> String { + format!( + "Proposal Id: {} + {:2}Type: {} + {:2}Author: {} + {:2}Content: {:?} + {:2}Start Epoch: {} + {:2}End Epoch: {} + {:2}Grace Epoch: {} + {:2}Status: {} + ", + self.id, + "", + self.r#type, + "", + self.author, + "", + self.content, + "", + self.voting_start_epoch, + "", + self.voting_end_epoch, + "", + self.grace_epoch, + "", + self.get_status(current_epoch) + ) + } +} + +impl Display for StorageProposal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Proposal Id: {} + {:2}Type: {} + {:2}Author: {} + {:2}Start Epoch: {} + {:2}End Epoch: {} + {:2}Grace Epoch: {} + ", + self.id, + "", + self.r#type, + "", + self.author, + "", + self.voting_start_epoch, + "", + self.voting_end_epoch, + "", + self.grace_epoch + ) + } +} diff --git a/core/src/ledger/governance/storage/vote.rs b/core/src/ledger/governance/storage/vote.rs new file mode 100644 index 00000000000..3ba8ec2ae25 --- /dev/null +++ b/core/src/ledger/governance/storage/vote.rs @@ -0,0 +1,127 @@ +use std::fmt::Display; + +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; + +use super::super::cli::onchain::ProposalVote; +use super::proposal::ProposalType; + +/// The type of a governance vote with the optional associated Memo +#[derive( + Debug, + Clone, + PartialEq, + BorshSerialize, + BorshDeserialize, + Eq, + Serialize, + Deserialize, +)] +pub enum VoteType { + /// A default vote without Memo + Default, + /// A vote for the PGF stewards + PGFSteward, + /// A vote for a PGF payment proposal + PGFPayment, +} + +#[derive( + Debug, + Clone, + PartialEq, + BorshSerialize, + BorshDeserialize, + Eq, + Serialize, + Deserialize, +)] +/// The vote for a proposal +pub enum StorageProposalVote { + /// Yes + Yay(VoteType), + /// No + Nay, +} + +impl StorageProposalVote { + /// Check if a vote is yay + pub fn is_yay(&self) -> bool { + matches!(self, StorageProposalVote::Yay(_)) + } + + /// Check if vote is of type default + pub fn is_default_vote(&self) -> bool { + matches!( + self, + StorageProposalVote::Yay(VoteType::Default) + | StorageProposalVote::Nay + ) + } + + /// Check if a vote is compatible with a proposal + pub fn is_compatible(&self, proposal_type: &ProposalType) -> bool { + match self { + StorageProposalVote::Yay(vote_type) => proposal_type.eq(vote_type), + StorageProposalVote::Nay => true, + } + } + + /// Create a new vote + pub fn build( + proposal_vote: &ProposalVote, + proposal_type: &ProposalType, + ) -> Option { + match (proposal_vote, proposal_type) { + (ProposalVote::Yay, ProposalType::Default(_)) => { + Some(StorageProposalVote::Yay(VoteType::Default)) + } + (ProposalVote::Yay, ProposalType::PGFSteward(_)) => { + Some(StorageProposalVote::Yay(VoteType::PGFSteward)) + } + (ProposalVote::Yay, ProposalType::PGFPayment(_)) => { + Some(StorageProposalVote::Yay(VoteType::PGFPayment)) + } + (ProposalVote::Nay, ProposalType::Default(_)) => { + Some(StorageProposalVote::Nay) + } + (ProposalVote::Nay, ProposalType::PGFSteward(_)) => { + Some(StorageProposalVote::Nay) + } + (ProposalVote::Nay, ProposalType::PGFPayment(_)) => { + Some(StorageProposalVote::Nay) + } + _ => None, + } + } +} + +impl Display for StorageProposalVote { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + StorageProposalVote::Yay(vote_type) => match vote_type { + VoteType::Default + | VoteType::PGFSteward + | VoteType::PGFPayment => write!(f, "yay"), + }, + + StorageProposalVote::Nay => write!(f, "nay"), + } + } +} + +impl PartialEq for ProposalType { + fn eq(&self, other: &VoteType) -> bool { + match self { + Self::Default(_) => { + matches!(other, VoteType::Default) + } + Self::PGFSteward(_) => { + matches!(other, VoteType::PGFSteward) + } + Self::PGFPayment(_) => { + matches!(other, VoteType::PGFPayment) + } + } + } +} diff --git a/core/src/ledger/governance/utils.rs b/core/src/ledger/governance/utils.rs new file mode 100644 index 00000000000..30df25e3fb3 --- /dev/null +++ b/core/src/ledger/governance/utils.rs @@ -0,0 +1,291 @@ +use std::collections::HashMap; +use std::fmt::Display; + +use borsh::{BorshDeserialize, BorshSerialize}; + +use super::cli::offline::OfflineVote; +use super::storage::proposal::ProposalType; +use super::storage::vote::StorageProposalVote; +use crate::types::address::Address; +use crate::types::storage::Epoch; +use crate::types::token; + +/// Proposal status +pub enum ProposalStatus { + /// Pending proposal status + Pending, + /// Ongoing proposal status + OnGoing, + /// Ended proposal status + Ended, +} + +impl Display for ProposalStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ProposalStatus::Pending => write!(f, "pending"), + ProposalStatus::OnGoing => write!(f, "on-going"), + ProposalStatus::Ended => write!(f, "ended"), + } + } +} + +/// Alias to comulate voting power +pub type VotePower = token::Amount; + +/// Structure rappresenting a proposal vote +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] +pub struct Vote { + /// Field holding the address of the validator + pub validator: Address, + /// Field holding the address of the delegator + pub delegator: Address, + /// Field holding vote data + pub data: StorageProposalVote, +} + +impl Vote { + /// Check if a vote is from a validator + pub fn is_validator(&self) -> bool { + self.validator.eq(&self.delegator) + } +} + +/// Rappresent a tally type +pub enum TallyType { + /// Rappresent a tally type for proposal requiring 2/3 of the votes + TwoThird, + /// Rappresent a tally type for proposal requiring 1/3 of the votes + OneThird, + /// Rappresent a tally type for proposal requiring less than 1/3 of the + /// votes to be nay + LessOneThirdNay, +} + +impl From for TallyType { + fn from(proposal_type: ProposalType) -> Self { + match proposal_type { + ProposalType::Default(_) => TallyType::TwoThird, + ProposalType::PGFSteward(_) => TallyType::TwoThird, + ProposalType::PGFPayment(_) => TallyType::LessOneThirdNay, + } + } +} + +/// The result of a proposal +pub enum TallyResult { + /// Proposal was accepted with the associated value + Passed, + /// Proposal was rejected + Rejected, +} + +impl Display for TallyResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TallyResult::Passed => write!(f, "passed"), + TallyResult::Rejected => write!(f, "rejected"), + } + } +} + +impl TallyResult { + /// Create a new tally result + pub fn new( + tally_type: &TallyType, + yay_voting_power: VotePower, + nay_voting_power: VotePower, + total_voting_power: VotePower, + ) -> Self { + let passed = match tally_type { + TallyType::TwoThird => { + let at_least_two_third_voted = yay_voting_power + + nay_voting_power + >= total_voting_power / 3 * 2; + let at_last_half_voted_yay = + yay_voting_power > nay_voting_power; + at_least_two_third_voted && at_last_half_voted_yay + } + TallyType::OneThird => { + let at_least_two_third_voted = yay_voting_power + + nay_voting_power + >= total_voting_power / 3; + let at_last_half_voted_yay = + yay_voting_power > nay_voting_power; + at_least_two_third_voted && at_last_half_voted_yay + } + TallyType::LessOneThirdNay => { + nay_voting_power <= total_voting_power / 3 + } + }; + + if passed { Self::Passed } else { Self::Rejected } + } +} + +/// The result with votes of a proposal +pub struct ProposalResult { + /// The result of a proposal + pub result: TallyResult, + /// The total voting power during the proposal tally + pub total_voting_power: VotePower, + /// The total voting power from yay votes + pub total_yay_power: VotePower, + /// The total voting power from nay votes (unused at the moment) + pub total_nay_power: VotePower, +} + +impl Display for ProposalResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let percentage = self + .total_yay_power + .checked_div(self.total_voting_power) + .unwrap_or_default(); + + write!( + f, + "{} with {} yay votes and {} nay votes ({:.2}%)", + self.result, + self.total_yay_power.to_string_native(), + self.total_nay_power.to_string_native(), + percentage + .checked_mul(token::Amount::from_u64(100)) + .unwrap_or_default() + .to_string_native() + ) + } +} + +impl ProposalResult { + /// Return true if two third of total voting power voted nay + pub fn two_third_nay(&self) -> bool { + self.total_nay_power >= (self.total_voting_power / 3) * 2 + } +} + +/// /// General rappresentation of a vote +pub enum TallyVote { + /// Rappresent a vote for a proposal onchain + OnChain(StorageProposalVote), + /// Rappresent a vote for a proposal offline + Offline(OfflineVote), +} + +impl From for TallyVote { + fn from(vote: StorageProposalVote) -> Self { + Self::OnChain(vote) + } +} + +impl From for TallyVote { + fn from(vote: OfflineVote) -> Self { + Self::Offline(vote) + } +} + +impl TallyVote { + /// Check if a vote is yay + pub fn is_yay(&self) -> bool { + match self { + TallyVote::OnChain(vote) => vote.is_yay(), + TallyVote::Offline(vote) => vote.is_yay(), + } + } + + /// Check if two votes are equal + pub fn is_same_side(&self, other: &TallyVote) -> bool { + let both_yay = self.is_yay() && other.is_yay(); + let both_nay = !self.is_yay() && !other.is_yay(); + + both_yay || !both_nay + } +} + +/// Proposal structure holding votes information necessary to compute the +/// outcome +pub struct ProposalVotes { + /// Map from validator address to vote + pub validators_vote: HashMap, + /// Map from validator to their voting power + pub validator_voting_power: HashMap, + /// Map from delegation address to their vote + pub delegators_vote: HashMap, + /// Map from delegator address to the corresponding validator voting power + pub delegator_voting_power: HashMap>, +} + +/// Compute the result of a proposal +pub fn compute_proposal_result( + votes: ProposalVotes, + total_voting_power: VotePower, + tally_at: TallyType, +) -> ProposalResult { + let mut yay_voting_power = VotePower::default(); + let mut nay_voting_power = VotePower::default(); + + for (address, vote_power) in votes.validator_voting_power { + let vote_type = votes.validators_vote.get(&address); + if let Some(vote) = vote_type { + if vote.is_yay() { + yay_voting_power += vote_power; + } else { + nay_voting_power += vote_power; + } + } + } + + for (delegator, degalations) in votes.delegator_voting_power { + let delegator_vote = match votes.delegators_vote.get(&delegator) { + Some(vote) => vote, + None => continue, + }; + for (validator, voting_power) in degalations { + let validator_vote = votes.validators_vote.get(&validator); + if let Some(validator_vote) = validator_vote { + if !validator_vote.is_same_side(delegator_vote) { + if delegator_vote.is_yay() { + yay_voting_power += voting_power; + nay_voting_power -= voting_power; + } else { + nay_voting_power += voting_power; + yay_voting_power -= voting_power; + } + } + } else if delegator_vote.is_yay() { + yay_voting_power += voting_power; + } else { + nay_voting_power += voting_power; + } + } + } + + let tally_result = TallyResult::new( + &tally_at, + yay_voting_power, + nay_voting_power, + total_voting_power, + ); + + ProposalResult { + result: tally_result, + total_voting_power, + total_yay_power: yay_voting_power, + total_nay_power: nay_voting_power, + } +} + +/// Calculate the valid voting window for validator given a proposal epoch +/// details +pub fn is_valid_validator_voting_period( + current_epoch: Epoch, + voting_start_epoch: Epoch, + voting_end_epoch: Epoch, +) -> bool { + if voting_start_epoch >= voting_end_epoch { + false + } else { + let duration = voting_end_epoch - voting_start_epoch; + let two_third_duration = (duration / 3) * 2; + current_epoch <= voting_start_epoch + two_third_duration + } +} diff --git a/core/src/ledger/mod.rs b/core/src/ledger/mod.rs index 89b8105551c..9a84fbc1264 100644 --- a/core/src/ledger/mod.rs +++ b/core/src/ledger/mod.rs @@ -6,8 +6,8 @@ pub mod governance; #[cfg(any(feature = "abciplus", feature = "abcipp"))] pub mod ibc; pub mod parameters; +pub mod pgf; pub mod replay_protection; -pub mod slash_fund; pub mod storage; pub mod storage_api; pub mod testnet_pow; diff --git a/core/src/ledger/slash_fund/mod.rs b/core/src/ledger/pgf/mod.rs similarity index 55% rename from core/src/ledger/slash_fund/mod.rs rename to core/src/ledger/pgf/mod.rs index 7a7d53963b0..a36621e49e1 100644 --- a/core/src/ledger/slash_fund/mod.rs +++ b/core/src/ledger/pgf/mod.rs @@ -1,8 +1,11 @@ -//! SlashFund library code +//! Pgf library code use crate::types::address::{Address, InternalAddress}; -/// Internal SlashFund address -pub const ADDRESS: Address = Address::Internal(InternalAddress::SlashFund); - +/// Pgf parameters +pub mod parameters; +/// Pgf storage pub mod storage; + +/// The Pgf internal address +pub const ADDRESS: Address = Address::Internal(InternalAddress::Pgf); diff --git a/core/src/ledger/pgf/parameters.rs b/core/src/ledger/pgf/parameters.rs new file mode 100644 index 00000000000..335ec566079 --- /dev/null +++ b/core/src/ledger/pgf/parameters.rs @@ -0,0 +1,70 @@ +use std::collections::BTreeSet; + +use borsh::{BorshDeserialize, BorshSerialize}; + +use super::storage::keys as pgf_storage; +use crate::ledger::storage_api::{self, StorageRead, StorageWrite}; +use crate::types::address::Address; +use crate::types::dec::Dec; + +#[derive( + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + BorshSerialize, + BorshDeserialize, +)] +/// Pgf parameter structure +pub struct PgfParams { + /// The set of stewards + pub stewards: BTreeSet
, + /// The set of continous payments + pub payments: BTreeSet, + /// The pgf funding inflation rate + pub pgf_inflation_rate: Dec, + /// The pgf stewards inflation rate + pub stewards_inflation_rate: Dec, +} + +impl Default for PgfParams { + fn default() -> Self { + Self { + stewards: BTreeSet::default(), + payments: BTreeSet::default(), + pgf_inflation_rate: Dec::new(5, 2).unwrap(), + stewards_inflation_rate: Dec::new(1, 2).unwrap(), + } + } +} + +impl PgfParams { + /// Initialize governance parameters into storage + pub fn init_storage(&self, storage: &mut S) -> storage_api::Result<()> + where + S: StorageRead + StorageWrite, + { + let Self { + stewards, + payments, + pgf_inflation_rate, + stewards_inflation_rate, + } = self; + + let stewards_key = pgf_storage::get_stewards_key(); + storage.write(&stewards_key, stewards)?; + + let payments_key = pgf_storage::get_payments_key(); + storage.write(&payments_key, payments)?; + + let pgf_inflation_rate_key = pgf_storage::get_pgf_inflation_rate_key(); + storage.write(&pgf_inflation_rate_key, pgf_inflation_rate)?; + + let steward_inflation_rate_key = + pgf_storage::get_steward_inflation_rate_key(); + storage.write(&steward_inflation_rate_key, stewards_inflation_rate) + } +} diff --git a/core/src/ledger/pgf/storage/keys.rs b/core/src/ledger/pgf/storage/keys.rs new file mode 100644 index 00000000000..fa4875bb779 --- /dev/null +++ b/core/src/ledger/pgf/storage/keys.rs @@ -0,0 +1,67 @@ +use namada_macros::StorageKeys; + +use crate::ledger::pgf::ADDRESS; +use crate::types::address::Address; +use crate::types::storage::{DbKeySeg, Key, KeySeg}; + +/// Storage keys for pgf internal address. +#[derive(StorageKeys)] +struct Keys { + stewards: &'static str, + payments: &'static str, + pgf_inflation_rate: &'static str, + steward_inflation_rate: &'static str, +} + +/// Check if key is inside governance address space +pub fn is_pgf_key(key: &Key) -> bool { + matches!(&key.segments[0], DbKeySeg::AddressSeg(addr) if addr == &ADDRESS) +} + +/// Check if key is a steward key +pub fn is_stewards_key(key: &Key) -> bool { + matches!(&key.segments[..], [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix)] if addr == &ADDRESS && prefix == Keys::VALUES.stewards) +} + +/// Check if key is a payments key +pub fn is_payments_key(key: &Key) -> bool { + matches!(&key.segments[..], [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix)] if addr == &ADDRESS && prefix == Keys::VALUES.payments) +} + +/// Check if key is a pgf inflation rate key +pub fn is_pgf_inflation_rate_key(key: &Key) -> bool { + matches!(&key.segments[..], [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix)] if addr == &ADDRESS && prefix == Keys::VALUES.pgf_inflation_rate) +} + +/// Check if key is a steward inflation rate key +pub fn is_steward_inflation_rate_key(key: &Key) -> bool { + matches!(&key.segments[..], [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix)] if addr == &ADDRESS && prefix == Keys::VALUES.steward_inflation_rate) +} + +/// Get key for stewards key +pub fn get_stewards_key() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&Keys::VALUES.stewards.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Get key for payments key +pub fn get_payments_key() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&Keys::VALUES.payments.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Get key for inflation rate key +pub fn get_pgf_inflation_rate_key() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&Keys::VALUES.pgf_inflation_rate.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Get key for inflation rate key +pub fn get_steward_inflation_rate_key() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&Keys::VALUES.steward_inflation_rate.to_owned()) + .expect("Cannot obtain a storage key") +} diff --git a/core/src/ledger/pgf/storage/mod.rs b/core/src/ledger/pgf/storage/mod.rs new file mode 100644 index 00000000000..fb9d6f8896c --- /dev/null +++ b/core/src/ledger/pgf/storage/mod.rs @@ -0,0 +1,2 @@ +/// Pgf storage keys +pub mod keys; diff --git a/core/src/ledger/slash_fund/storage.rs b/core/src/ledger/slash_fund/storage.rs deleted file mode 100644 index 9c437da591c..00000000000 --- a/core/src/ledger/slash_fund/storage.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! Slash fund storage - -use crate::types::storage::{DbKeySeg, Key}; - -/// Check if a key is a slash fund key -pub fn is_slash_fund_key(key: &Key) -> bool { - matches!(&key.segments[0], DbKeySeg::AddressSeg(addr) if addr == &super::ADDRESS) -} diff --git a/core/src/ledger/storage_api/governance.rs b/core/src/ledger/storage_api/governance.rs index c2316bffa74..1d4d3f767ef 100644 --- a/core/src/ledger/storage_api/governance.rs +++ b/core/src/ledger/storage_api/governance.rs @@ -1,12 +1,25 @@ //! Governance +use std::collections::BTreeMap; + +use borsh::BorshDeserialize; + use super::token; -use crate::ledger::governance::{storage, ADDRESS as governance_address}; +use crate::ledger::governance::storage::keys as governance_keys; +use crate::ledger::governance::storage::proposal::{ + ProposalType, StorageProposal, +}; +use crate::ledger::governance::storage::vote::StorageProposalVote; +use crate::ledger::governance::utils::Vote; +use crate::ledger::governance::ADDRESS as governance_address; use crate::ledger::storage_api::{self, StorageRead, StorageWrite}; +use crate::types::address::Address; +use crate::types::storage::Epoch; use crate::types::transaction::governance::{ - InitProposalData, ProposalType, VoteProposalData, + InitProposalData, VoteProposalData, }; +/// A proposal creation transaction. /// A proposal creation transaction. pub fn init_proposal( storage: &mut S, @@ -17,25 +30,26 @@ pub fn init_proposal( where S: StorageRead + StorageWrite, { - let counter_key = storage::get_counter_key(); + let counter_key = governance_keys::get_counter_key(); let proposal_id = if let Some(id) = data.id { id } else { storage.read(&counter_key)?.unwrap() }; - let content_key = storage::get_content_key(proposal_id); + let content_key = governance_keys::get_content_key(proposal_id); storage.write_bytes(&content_key, content)?; - let author_key = storage::get_author_key(proposal_id); + let author_key = governance_keys::get_author_key(proposal_id); storage.write(&author_key, data.author.clone())?; - let proposal_type_key = storage::get_proposal_type_key(proposal_id); + let proposal_type_key = governance_keys::get_proposal_type_key(proposal_id); match data.r#type { ProposalType::Default(Some(_)) => { // 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); + let proposal_code_key = + governance_keys::get_proposal_code_key(proposal_id); let proposal_code = code.clone().ok_or( storage_api::Error::new_const("Missing proposal code"), )?; @@ -45,17 +59,19 @@ where } let voting_start_epoch_key = - storage::get_voting_start_epoch_key(proposal_id); + governance_keys::get_voting_start_epoch_key(proposal_id); storage.write(&voting_start_epoch_key, data.voting_start_epoch)?; - let voting_end_epoch_key = storage::get_voting_end_epoch_key(proposal_id); + let voting_end_epoch_key = + governance_keys::get_voting_end_epoch_key(proposal_id); storage.write(&voting_end_epoch_key, data.voting_end_epoch)?; - let grace_epoch_key = storage::get_grace_epoch_key(proposal_id); + let grace_epoch_key = governance_keys::get_grace_epoch_key(proposal_id); storage.write(&grace_epoch_key, data.grace_epoch)?; if let ProposalType::Default(Some(_)) = data.r#type { - let proposal_code_key = storage::get_proposal_code_key(proposal_id); + let proposal_code_key = + governance_keys::get_proposal_code_key(proposal_id); let proposal_code = code.ok_or(storage_api::Error::new_const("Missing proposal code"))?; storage.write_bytes(&proposal_code_key, proposal_code)?; @@ -63,16 +79,19 @@ where storage.write(&counter_key, proposal_id + 1)?; - let min_proposal_funds_key = storage::get_min_proposal_fund_key(); + let min_proposal_funds_key = governance_keys::get_min_proposal_fund_key(); let min_proposal_funds: token::Amount = storage.read(&min_proposal_funds_key)?.unwrap(); - let funds_key = storage::get_funds_key(proposal_id); + let funds_key = governance_keys::get_funds_key(proposal_id); storage.write(&funds_key, min_proposal_funds)?; // this key must always be written for each proposal let committing_proposals_key = - storage::get_committing_proposals_key(proposal_id, data.grace_epoch.0); + governance_keys::get_committing_proposals_key( + proposal_id, + data.grace_epoch.0, + ); storage.write(&committing_proposals_key, ())?; token::transfer( @@ -93,7 +112,7 @@ where S: StorageRead + StorageWrite, { for delegation in data.delegations { - let vote_key = storage::get_vote_proposal_key( + let vote_key = governance_keys::get_vote_proposal_key( data.id, data.voter.clone(), delegation, @@ -102,3 +121,99 @@ where } Ok(()) } + +/// Read a proposal by id from storage +pub fn get_proposal_by_id( + storage: &S, + id: u64, +) -> storage_api::Result> +where + S: StorageRead, +{ + let author_key = governance_keys::get_author_key(id); + let content = governance_keys::get_content_key(id); + let start_epoch_key = governance_keys::get_voting_start_epoch_key(id); + let end_epoch_key = governance_keys::get_voting_end_epoch_key(id); + let grace_epoch_key = governance_keys::get_grace_epoch_key(id); + let proposal_type_key = governance_keys::get_proposal_type_key(id); + + let author: Option
= storage.read(&author_key)?; + let content: Option> = storage.read(&content)?; + let voting_start_epoch: Option = storage.read(&start_epoch_key)?; + let voting_end_epoch: Option = storage.read(&end_epoch_key)?; + let grace_epoch: Option = storage.read(&grace_epoch_key)?; + let proposal_type: Option = + storage.read(&proposal_type_key)?; + + let proposal = proposal_type.map(|proposal_type| StorageProposal { + id, + content: content.unwrap(), + author: author.unwrap(), + r#type: proposal_type, + voting_start_epoch: voting_start_epoch.unwrap(), + voting_end_epoch: voting_end_epoch.unwrap(), + grace_epoch: grace_epoch.unwrap(), + }); + + Ok(proposal) +} + +/// Query all the votes for a proposal_id +pub fn get_proposal_votes( + storage: &S, + proposal_id: u64, +) -> storage_api::Result> +where + S: storage_api::StorageRead, +{ + let vote_prefix_key = + governance_keys::get_proposal_vote_prefix_key(proposal_id); + let vote_iter = storage_api::iter_prefix::( + storage, + &vote_prefix_key, + )?; + + let votes = vote_iter + .filter_map(|vote_result| { + if let Ok((vote_key, vote)) = vote_result { + let voter_address = + governance_keys::get_voter_address(&vote_key); + let delegator_address = + governance_keys::get_vote_delegation_address(&vote_key); + match (voter_address, delegator_address) { + (Some(delegator_address), Some(validator_address)) => { + Some(Vote { + validator: validator_address.to_owned(), + delegator: delegator_address.to_owned(), + data: vote, + }) + } + _ => None, + } + } else { + None + } + }) + .collect::>(); + + Ok(votes) +} + +/// Check if an accepted proposal is being executed +pub fn is_proposal_accepted( + storage: &S, + tx_data: &[u8], +) -> storage_api::Result +where + S: storage_api::StorageRead, +{ + let proposal_id = u64::try_from_slice(tx_data).ok(); + match proposal_id { + Some(id) => { + let proposal_execution_key = + governance_keys::get_proposal_execution_key(id); + storage.has_key(&proposal_execution_key) + } + None => Ok(false), + } +} diff --git a/core/src/ledger/storage_api/mod.rs b/core/src/ledger/storage_api/mod.rs index f97478e0d66..1108c44e3d0 100644 --- a/core/src/ledger/storage_api/mod.rs +++ b/core/src/ledger/storage_api/mod.rs @@ -6,6 +6,7 @@ pub mod collections; mod error; pub mod governance; pub mod key; +pub mod pgf; pub mod token; pub mod validation; diff --git a/core/src/ledger/storage_api/pgf.rs b/core/src/ledger/storage_api/pgf.rs new file mode 100644 index 00000000000..8dfafee0fb7 --- /dev/null +++ b/core/src/ledger/storage_api/pgf.rs @@ -0,0 +1,30 @@ +//! Pgf + +use std::collections::BTreeSet; + +use crate::ledger::governance::storage::proposal::PGFTarget; +use crate::ledger::pgf::storage::keys as pgf_keys; +use crate::ledger::storage_api::{self}; +use crate::types::address::Address; + +/// Query the current pgf steward set +pub fn get_stewards(storage: &S) -> storage_api::Result> +where + S: storage_api::StorageRead, +{ + let stewards_key = pgf_keys::get_stewards_key(); + let stewards: Option> = storage.read(&stewards_key)?; + + Ok(stewards.unwrap_or_default()) +} + +/// Query the current pgf continous payments +pub fn get_payments(storage: &S) -> storage_api::Result> +where + S: storage_api::StorageRead, +{ + let payment_key = pgf_keys::get_payments_key(); + let payments: Option> = storage.read(&payment_key)?; + + Ok(payments.unwrap_or_default()) +} diff --git a/core/src/ledger/storage_api/token.rs b/core/src/ledger/storage_api/token.rs index 1985d8325c7..95987d6a83f 100644 --- a/core/src/ledger/storage_api/token.rs +++ b/core/src/ledger/storage_api/token.rs @@ -134,3 +134,35 @@ where storage.write(&balance_key, new_balance)?; storage.write(&total_supply_key, new_supply) } + +/// Burn an amount of token for a specific address. +pub fn burn( + storage: &mut S, + token: &Address, + source: &Address, + amount: token::Amount, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let key = token::balance_key(token, source); + let balance = read_balance(storage, token, source)?; + + let amount_to_burn = match balance.checked_sub(amount) { + Some(new_balance) => { + storage.write(&key, new_balance)?; + amount + } + None => { + storage.write(&key, token::Amount::default())?; + balance + } + }; + + let total_supply = read_total_supply(&*storage, source)?; + let new_total_supply = + total_supply.checked_sub(amount_to_burn).unwrap_or_default(); + + let total_supply_key = token::minted_balance_key(token); + storage.write(&total_supply_key, new_total_supply) +} diff --git a/core/src/types/address.rs b/core/src/types/address.rs index 1a6611a2f5d..0576511cec4 100644 --- a/core/src/types/address.rs +++ b/core/src/types/address.rs @@ -75,8 +75,6 @@ mod internal { "ano::Protocol Parameters "; pub const GOVERNANCE: &str = "ano::Governance "; - pub const SLASH_FUND: &str = - "ano::Slash Fund "; pub const IBC: &str = "ibc::Inter-Blockchain Communication "; pub const ETH_BRIDGE: &str = @@ -87,6 +85,8 @@ mod internal { "ano::Replay Protection "; pub const MULTITOKEN: &str = "ano::Multitoken "; + pub const PGF: &str = + "ano::Pgf "; } /// Fixed-length address strings prefix for established addresses. @@ -219,9 +219,6 @@ impl Address { InternalAddress::Governance => { internal::GOVERNANCE.to_string() } - InternalAddress::SlashFund => { - internal::SLASH_FUND.to_string() - } InternalAddress::Ibc => internal::IBC.to_string(), InternalAddress::IbcToken(hash) => { format!("{}::{}", PREFIX_IBC, hash) @@ -243,6 +240,7 @@ impl Address { InternalAddress::Multitoken => { internal::MULTITOKEN.to_string() } + InternalAddress::Pgf => internal::PGF.to_string(), }; debug_assert_eq!(string.len(), FIXED_LEN_STRING_BYTES); string @@ -304,9 +302,6 @@ impl Address { internal::GOVERNANCE => { Ok(Address::Internal(InternalAddress::Governance)) } - internal::SLASH_FUND => { - Ok(Address::Internal(InternalAddress::SlashFund)) - } internal::ETH_BRIDGE => { Ok(Address::Internal(InternalAddress::EthBridge)) } @@ -319,6 +314,7 @@ impl Address { internal::MULTITOKEN => { Ok(Address::Internal(InternalAddress::Multitoken)) } + internal::PGF => Ok(Address::Internal(InternalAddress::Pgf)), _ => Err(Error::new( ErrorKind::InvalidData, "Invalid internal address", @@ -541,8 +537,6 @@ pub enum InternalAddress { IbcToken(String), /// Governance address Governance, - /// SlashFund address for governance - SlashFund, /// Bridge to Ethereum EthBridge, /// The pool of transactions to be relayed to Ethereum @@ -553,6 +547,8 @@ pub enum InternalAddress { ReplayProtection, /// Multitoken Multitoken, + /// Pgf + Pgf, } impl Display for InternalAddress { @@ -565,7 +561,6 @@ impl Display for InternalAddress { Self::PosSlashPool => "PosSlashPool".to_string(), Self::Parameters => "Parameters".to_string(), Self::Governance => "Governance".to_string(), - Self::SlashFund => "SlashFund".to_string(), Self::Ibc => "IBC".to_string(), Self::IbcToken(hash) => format!("IbcToken: {}", hash), Self::EthBridge => "EthBridge".to_string(), @@ -573,6 +568,7 @@ impl Display for InternalAddress { Self::Erc20(eth_addr) => format!("Erc20: {}", eth_addr), Self::ReplayProtection => "ReplayProtection".to_string(), Self::Multitoken => "Multitoken".to_string(), + Self::Pgf => "PublicGoodFundings".to_string(), } ) } @@ -859,7 +855,6 @@ pub mod testing { InternalAddress::PoS => {} InternalAddress::PosSlashPool => {} InternalAddress::Governance => {} - InternalAddress::SlashFund => {} InternalAddress::Parameters => {} InternalAddress::Ibc => {} InternalAddress::IbcToken(_) => {} @@ -867,6 +862,7 @@ pub mod testing { InternalAddress::EthBridgePool => {} InternalAddress::Erc20(_) => {} InternalAddress::ReplayProtection => {} + InternalAddress::Pgf => {} InternalAddress::Multitoken => {} /* Add new addresses in the * `prop_oneof` below. */ }; @@ -877,12 +873,12 @@ pub mod testing { Just(InternalAddress::Parameters), arb_ibc_token(), Just(InternalAddress::Governance), - Just(InternalAddress::SlashFund), Just(InternalAddress::EthBridge), Just(InternalAddress::EthBridgePool), Just(arb_erc20()), Just(InternalAddress::ReplayProtection), Just(InternalAddress::Multitoken), + Just(InternalAddress::Pgf), ] } diff --git a/core/src/types/governance.rs b/core/src/types/governance.rs deleted file mode 100644 index d3450dccd1c..00000000000 --- a/core/src/types/governance.rs +++ /dev/null @@ -1,384 +0,0 @@ -//! Files defyining the types used in governance. - -use std::collections::{BTreeMap, HashSet}; -use std::fmt::{self, Display}; - -use borsh::{BorshDeserialize, BorshSerialize}; -use serde::{Deserialize, Serialize}; -use thiserror::Error; - -use crate::types::address::Address; -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, DenominatedAmount, NATIVE_MAX_DECIMAL_PLACES, -}; -use crate::types::uint::Uint; - -/// Type alias for vote power -pub type VotePower = Uint; - -/// 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, - Clone, - PartialEq, - BorshSerialize, - BorshDeserialize, - Serialize, - Deserialize, - Eq, -)] -pub enum VoteType { - /// A default vote without Memo - Default, - /// A vote for the PGF council - PGFCouncil(HashSet), - /// A vote for ETH bridge carrying the signature over the proposed message - ETHBridge(Signature), -} - -#[derive( - Debug, - Clone, - PartialEq, - BorshSerialize, - BorshDeserialize, - Serialize, - Deserialize, - Eq, -)] -/// The vote for a proposal -pub enum ProposalVote { - /// Yes - Yay(VoteType), - /// No - Nay, -} - -impl ProposalVote { - /// Check if a vote is yay - pub fn is_yay(&self) -> bool { - matches!(self, ProposalVote::Yay(_)) - } - - /// Check if vote is of type default - pub fn is_default_vote(&self) -> bool { - matches!( - self, - ProposalVote::Yay(VoteType::Default) | ProposalVote::Nay - ) - } -} - -impl Display for ProposalVote { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - 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.to_string_native() - )? - } - - Ok(()) - } - VoteType::ETHBridge(sig) => { - write!(f, "yay with signature: {:#?}", sig) - } - }, - - ProposalVote::Nay => write!(f, "nay"), - } - } -} - -#[allow(missing_docs)] -#[derive(Debug, Error)] -pub enum ProposalVoteParseError { - #[error("Invalid vote. Vote shall be yay or nay.")] - InvalidVote, -} - -/// The type of the tally -#[derive(Clone, Debug)] -pub enum Tally { - /// Default proposal - Default, - /// PGF proposal - PGFCouncil(Council), - /// ETH Bridge proposal - ETHBridge, -} - -/// The result of a proposal -#[derive(Clone, Debug)] -pub enum TallyResult { - /// Proposal was accepted with the associated value - Passed(Tally), - /// Proposal was rejected - Rejected, -} - -/// The result with votes of a proposal -pub struct ProposalResult { - /// The result of a proposal - pub result: TallyResult, - /// The total voting power during the proposal tally - pub total_voting_power: VotePower, - /// The total voting power from yay votes - pub total_yay_power: VotePower, - /// The total voting power from nay votes (unused at the moment) - pub total_nay_power: VotePower, -} - -impl Display for ProposalResult { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let percentage = DenominatedAmount { - amount: Amount::from_uint( - self.total_yay_power - .fixed_precision_div(&self.total_voting_power, 4) - .unwrap_or_default(), - 0, - ) - .unwrap(), - denom: 2.into(), - }; - - write!( - f, - "{} with {} yay votes over {} ({}%)", - self.result, - DenominatedAmount { - amount: Amount::from_uint(self.total_yay_power, 0).unwrap(), - denom: NATIVE_MAX_DECIMAL_PLACES.into() - }, - DenominatedAmount { - amount: Amount::from_uint(self.total_voting_power, 0).unwrap(), - denom: NATIVE_MAX_DECIMAL_PLACES.into() - }, - percentage - ) - } -} - -impl Display for TallyResult { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - TallyResult::Passed(vote) => match vote { - Tally::Default | Tally::ETHBridge => write!(f, "passed"), - Tally::PGFCouncil((council, cap)) => write!( - f, - "passed with PGF council address: {}, spending cap: {}", - council, - cap.to_string_native() - ), - }, - TallyResult::Rejected => write!(f, "rejected"), - } - } -} - -/// 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, - /// An ETH bridge proposal - ETHBridge, -} - -#[derive( - Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, -)] -/// The proposal structure -pub struct Proposal { - /// The proposal id - pub id: Option, - /// The proposal content - 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, -} - -impl Display for Proposal { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "id: {:?}, author: {:?}", self.id, self.author) - } -} - -#[allow(missing_docs)] -#[derive(Debug, Error)] -pub enum ProposalError { - #[error("Invalid proposal data.")] - InvalidProposalData, -} - -#[derive( - Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, -)] -/// The offline proposal structure -pub struct OfflineProposal { - /// The proposal content - pub content: BTreeMap, - /// The proposal author address - pub author: Address, - /// The epoch from which this changes are executed - pub tally_epoch: Epoch, - /// The signature over proposal data - pub signature: Signature, - /// The address corresponding to the signature pk - pub address: Address, -} - -impl OfflineProposal { - /// Create an offline proposal with a signature - pub fn new( - proposal: Proposal, - address: Address, - signing_key: &common::SecretKey, - ) -> Self { - let content_serialized = serde_json::to_vec(&proposal.content) - .expect("Conversion to bytes shouldn't fail."); - let author_serialized = serde_json::to_vec(&proposal.author) - .expect("Conversion to bytes shouldn't fail."); - let tally_epoch_serialized = serde_json::to_vec(&proposal.grace_epoch) - .expect("Conversion to bytes shouldn't fail."); - let proposal_serialized = &[ - content_serialized, - author_serialized, - tally_epoch_serialized, - ] - .concat(); - let proposal_data_hash = Hash::sha256(proposal_serialized); - let signature = - common::SigScheme::sign(signing_key, proposal_data_hash); - Self { - content: proposal.content, - author: proposal.author, - tally_epoch: proposal.grace_epoch, - signature, - address, - } - } - - /// Check whether the signature is valid or not - pub fn check_signature(&self, public_key: &common::PublicKey) -> bool { - let proposal_data_hash = self.compute_hash(); - common::SigScheme::verify_signature( - public_key, - &proposal_data_hash, - &self.signature, - ) - .is_ok() - } - - /// Compute the hash of the proposal - pub fn compute_hash(&self) -> Hash { - let content_serialized = serde_json::to_vec(&self.content) - .expect("Conversion to bytes shouldn't fail."); - let author_serialized = serde_json::to_vec(&self.author) - .expect("Conversion to bytes shouldn't fail."); - let tally_epoch_serialized = serde_json::to_vec(&self.tally_epoch) - .expect("Conversion to bytes shouldn't fail."); - let proposal_serialized = &[ - content_serialized, - author_serialized, - tally_epoch_serialized, - ] - .concat(); - Hash::sha256(proposal_serialized) - } -} - -#[derive( - Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, -)] -/// The offline proposal structure -pub struct OfflineVote { - /// The proposal data hash - pub proposal_hash: Hash, - /// The proposal vote - pub vote: ProposalVote, - /// The signature over proposal data - pub signature: Signature, - /// The address corresponding to the signature pk - pub address: Address, -} - -impl OfflineVote { - /// Create an offline vote for a proposal - pub fn new( - proposal: &OfflineProposal, - vote: ProposalVote, - address: Address, - signing_key: &common::SecretKey, - ) -> Self { - let proposal_hash = proposal.compute_hash(); - let proposal_hash_data = proposal_hash - .try_to_vec() - .expect("Conversion to bytes shouldn't fail."); - let proposal_vote_data = vote - .try_to_vec() - .expect("Conversion to bytes shouldn't fail."); - let vote_serialized = - &[proposal_hash_data, proposal_vote_data].concat(); - let signature = common::SigScheme::sign(signing_key, vote_serialized); - Self { - proposal_hash, - vote, - signature, - address, - } - } - - /// compute the hash of a proposal - pub fn compute_hash(&self) -> Hash { - let proposal_hash_data = self - .proposal_hash - .try_to_vec() - .expect("Conversion to bytes shouldn't fail."); - let proposal_vote_data = self - .vote - .try_to_vec() - .expect("Conversion to bytes shouldn't fail."); - let vote_serialized = - &[proposal_hash_data, proposal_vote_data].concat(); - - Hash::sha256(vote_serialized) - } - - /// Check whether the signature is valid or not - pub fn check_signature(&self, public_key: &common::PublicKey) -> bool { - let vote_data_hash = self.compute_hash(); - common::SigScheme::verify_signature( - public_key, - &vote_data_hash, - &self.signature, - ) - .is_ok() - } -} diff --git a/core/src/types/mod.rs b/core/src/types/mod.rs index 8303e757424..8aee038d9b1 100644 --- a/core/src/types/mod.rs +++ b/core/src/types/mod.rs @@ -8,7 +8,6 @@ pub mod eth_abi; pub mod eth_bridge_pool; pub mod ethereum_events; pub mod ethereum_structs; -pub mod governance; pub mod hash; pub mod ibc; pub mod internal; diff --git a/core/src/types/token.rs b/core/src/types/token.rs index bf4e23cbe3c..6056495cd1c 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -76,6 +76,11 @@ impl Amount { self.raw = self.raw.checked_sub(amount.raw).unwrap(); } + /// Check if there are enough funds. + pub fn can_spend(&self, amount: &Amount) -> bool { + self.raw >= amount.raw + } + /// Receive a given amount. /// Panics on overflow and when [`uint::MAX_SIGNED_VALUE`] is exceeded. pub fn receive(&mut self, amount: &Amount) { @@ -154,6 +159,20 @@ impl Amount { Self { raw: change.abs() } } + /// Checked division. Returns `None` on underflow. + pub fn checked_div(&self, amount: Amount) -> Option { + self.raw + .checked_div(amount.raw) + .map(|result| Self { raw: result }) + } + + /// Checked division. Returns `None` on overflow. + pub fn checked_mul(&self, amount: Amount) -> Option { + self.raw + .checked_mul(amount.raw) + .map(|result| Self { raw: result }) + } + /// Given a string and a denomination, parse an amount from string. pub fn from_str( string: impl AsRef, diff --git a/core/src/types/transaction/governance.rs b/core/src/types/transaction/governance.rs index bfc17eeaefd..0d653c44cf0 100644 --- a/core/src/types/transaction/governance.rs +++ b/core/src/types/transaction/governance.rs @@ -1,86 +1,25 @@ -use std::fmt::Display; +use std::collections::HashSet; use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; +use thiserror::Error; -use crate::types::address::Address; -use crate::types::governance::{ - self, Proposal, ProposalError, ProposalVote, VoteType, +use crate::ledger::governance::cli::onchain::{ + DefaultProposal, PgfFundingProposal, PgfStewardProposal, +}; +use crate::ledger::governance::storage::proposal::{ + AddRemove, PGFAction, ProposalType, }; +use crate::ledger::governance::storage::vote::StorageProposalVote; +use crate::types::address::Address; use crate::types::hash::Hash; use crate::types::storage::Epoch; -/// The type of a Proposal -#[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, - /// ETH proposal - ETHBridge, -} - -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"), - ProposalType::ETHBridge => write!(f, "ETH Bridge"), - } - } -} - -impl PartialEq for ProposalType { - fn eq(&self, other: &VoteType) -> bool { - match self { - Self::Default(_) => { - matches!(other, VoteType::Default) - } - Self::PGFCouncil => { - matches!(other, VoteType::PGFCouncil(..)) - } - Self::ETHBridge => { - matches!(other, VoteType::ETHBridge(_)) - } - } - } -} - -impl TryFrom for (ProposalType, Option>) { - 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(( - ProposalType::Default(Some(Hash::default())), - Some(code), - )), - Err(_) => Err(Self::Error::InvalidProposalData), - } - } else { - Ok((ProposalType::Default(None), None)) - } - } - governance::ProposalType::PGFCouncil => { - Ok((ProposalType::PGFCouncil, None)) - } - governance::ProposalType::ETHBridge => { - Ok((ProposalType::ETHBridge, None)) - } - } - } +#[allow(missing_docs)] +#[derive(Debug, Error)] +pub enum ProposalError { + #[error("Invalid proposal data.")] + InvalidProposalData, } /// A tx data type to hold proposal data @@ -110,6 +49,16 @@ pub struct InitProposalData { pub grace_epoch: Epoch, } +impl InitProposalData { + /// Get the hash of the corresponding extra data section + pub fn get_section_code_hash(&self) -> Option { + match self.r#type { + ProposalType::Default(hash) => hash, + _ => None, + } + } +} + /// A tx data type to hold vote proposal data #[derive( Debug, @@ -124,30 +73,82 @@ pub struct VoteProposalData { /// The proposal id pub id: u64, /// The proposal vote - pub vote: ProposalVote, + pub vote: StorageProposalVote, /// The proposal author address pub voter: Address, /// Delegator addreses pub delegations: Vec
, } -impl TryFrom for (InitProposalData, Vec, Option>) { +impl TryFrom for InitProposalData { + type Error = ProposalError; + + fn try_from(value: DefaultProposal) -> Result { + Ok(InitProposalData { + id: value.proposal.id, + content: Hash::default(), + author: value.proposal.author, + r#type: ProposalType::Default(None), + voting_start_epoch: value.proposal.voting_start_epoch, + voting_end_epoch: value.proposal.voting_end_epoch, + grace_epoch: value.proposal.grace_epoch, + }) + } +} + +impl TryFrom for InitProposalData { type Error = ProposalError; - fn try_from(proposal: Proposal) -> Result { - let (r#type, code) = proposal.r#type.try_into()?; - Ok(( - InitProposalData { - id: proposal.id, - content: Hash::default(), - author: proposal.author, - r#type, - voting_start_epoch: proposal.voting_start_epoch, - voting_end_epoch: proposal.voting_end_epoch, - grace_epoch: proposal.grace_epoch, - }, - proposal.content.try_to_vec().unwrap(), - code, - )) + fn try_from(value: PgfStewardProposal) -> Result { + let extra_data = value + .data + .iter() + .cloned() + .map(|steward| AddRemove::
::try_from(steward).unwrap()) + .collect::>>(); + + Ok(InitProposalData { + id: value.proposal.id, + content: Hash::default(), + author: value.proposal.author, + r#type: ProposalType::PGFSteward(extra_data), + voting_start_epoch: value.proposal.voting_start_epoch, + voting_end_epoch: value.proposal.voting_end_epoch, + grace_epoch: value.proposal.grace_epoch, + }) + } +} + +impl TryFrom for InitProposalData { + type Error = ProposalError; + + fn try_from(value: PgfFundingProposal) -> Result { + let continous_fundings = value + .data + .continous + .iter() + .cloned() + .map(|funding| PGFAction::try_from(funding).unwrap()) + .collect::>(); + + let retro_fundings = value + .data + .retro + .iter() + .cloned() + .map(|funding| PGFAction::try_from(funding).unwrap()) + .collect::>(); + + let extra_data = [continous_fundings, retro_fundings].concat(); + + Ok(InitProposalData { + id: value.proposal.id, + content: Hash::default(), + author: value.proposal.author, + r#type: ProposalType::PGFPayment(extra_data), + voting_start_epoch: value.proposal.voting_start_epoch, + voting_end_epoch: value.proposal.voting_end_epoch, + grace_epoch: value.proposal.grace_epoch, + }) } } diff --git a/documentation/dev/src/explore/design/ledger/governance.md b/documentation/dev/src/explore/design/ledger/governance.md index d9e578eed5f..a7215c79b20 100644 --- a/documentation/dev/src/explore/design/ledger/governance.md +++ b/documentation/dev/src/explore/design/ledger/governance.md @@ -13,7 +13,7 @@ Also, it introduces some protocol parameters: - `min_proposal_fund` - `max_proposal_code_size` -- `min_proposal_period` +- `min_proposal_voting_period` - `max_proposal_period` - `max_proposal_content_size` - `min_proposal_grace_epochs` @@ -26,7 +26,7 @@ On-chain proposals are created under the `governance_address` storage space and, /$GovernanceAddress/counter: u64 /$GovernanceAddress/min_proposal_fund: u64 /$GovernanceAddress/max_proposal_code_size: u64 -/$GovernanceAddress/min_proposal_period: u64 +/$GovernanceAddress/min_proposal_voting_period: u64 /$GovernanceAddress/max_proposal_period: u64 /$GovernanceAddress/max_proposal_content_size: u64 /$GovernanceAddress/min_proposal_grace_epochs: u64 @@ -50,11 +50,11 @@ and follow these rules: - `$id` must be equal to `counter + 1`. - `startEpoch` must: - be greater than `currentEpoch`, where current epoch is the epoch in which the transaction is executed and included in a block - - be a multiple of `min_proposal_period`. + - be a multiple of `min_proposal_voting_period`. - `endEpoch` must: - - be at least `min_proposal_period` epochs greater than `startEpoch` + - be at least `min_proposal_voting_period` epochs greater than `startEpoch` - be at most `max_proposal_period` epochs greater than `startEpoch` - - be a multiple of `min_proposal_period` + - be a multiple of `min_proposal_voting_period` - `graceEpoch` must: - be at least `min_grace_epoch` epochs greater than `endEpoch` - `proposalCode` can be empty and must be a valid transaction with size less than `max_proposal_code_size` kibibytes. diff --git a/genesis/dev.toml b/genesis/dev.toml index ce6e992fa8b..a2320a1cf06 100644 --- a/genesis/dev.toml +++ b/genesis/dev.toml @@ -238,8 +238,8 @@ validator_stake_threshold = "1" min_proposal_fund = 500 # proposal code size in bytes max_proposal_code_size = 500000 -# min proposal period length in epochs -min_proposal_period = 3 +# min proposal voting period length in epochs +min_proposal_voting_period = 3 # max proposal period length in epochs max_proposal_period = 27 # maximum number of characters in the proposal content diff --git a/genesis/e2e-tests-single-node.toml b/genesis/e2e-tests-single-node.toml index 529c239bcc5..b2596c6767f 100644 --- a/genesis/e2e-tests-single-node.toml +++ b/genesis/e2e-tests-single-node.toml @@ -217,7 +217,7 @@ min_proposal_fund = 500 # proposal code size in bytes max_proposal_code_size = 1000000 # min proposal period length in epochs -min_proposal_period = 3 +min_proposal_voting_period = 3 # max proposal period length in epochs max_proposal_period = 27 # maximum number of characters in the proposal content diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index d84c538571f..c18a6262682 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -869,7 +869,7 @@ pub fn is_validator( address: &Address, ) -> storage_api::Result where - S: StorageRead + StorageWrite, + S: StorageRead, { let rate = read_validator_max_commission_rate_change(storage, address)?; Ok(rate.is_some()) diff --git a/shared/src/ledger/args.rs b/shared/src/ledger/args.rs index ed267420728..309ef97b945 100644 --- a/shared/src/ledger/args.rs +++ b/shared/src/ledger/args.rs @@ -163,6 +163,44 @@ pub struct TxIbcTransfer { pub tx_code_path: PathBuf, } +/// Transaction to initialize create a new proposal +#[derive(Clone, Debug)] +pub struct InitProposal { + /// Common tx arguments + pub tx: Tx, + /// The proposal data + pub proposal_data: C::Data, + /// Native token address + pub native_token: C::NativeAddress, + /// Flag if proposal should be run offline + pub is_offline: bool, + /// Flag if proposal is of type Pgf stewards + pub is_pgf_stewards: bool, + /// Flag if proposal is of type Pgf funding + pub is_pgf_funding: bool, + /// Path to the tx WASM file + pub tx_code_path: PathBuf, +} + +/// Transaction to vote on a proposal +#[derive(Clone, Debug)] +pub struct VoteProposal { + /// Common tx arguments + pub tx: Tx, + /// Proposal id + pub proposal_id: Option, + /// The vote + pub vote: String, + /// The address of the voter + pub voter: C::Address, + /// Flag if proposal vote should be run offline + pub is_offline: bool, + /// The proposal file path + pub proposal_data: Option, + /// Path to the TX WASM code file + pub tx_code_path: PathBuf, +} + /// Transaction to initialize a new account #[derive(Clone, Debug)] pub struct TxInitAccount { @@ -285,6 +323,13 @@ pub struct QueryProtocolParameters { pub query: Query, } +/// Query pgf data +#[derive(Clone, Debug)] +pub struct QueryPgf { + /// Common query args + pub query: Query, +} + /// Withdraw arguments #[derive(Clone, Debug)] pub struct Withdraw { @@ -465,7 +510,7 @@ pub struct Tx { pub dry_run: bool, /// Dump the transaction bytes to file pub dump_tx: bool, - /// The output directory path to where serialize the transaction + /// The output directory path to where serialize the data pub output_folder: Option, /// Submit the transaction even if it doesn't pass client checks pub force: bool, diff --git a/shared/src/ledger/events.rs b/shared/src/ledger/events.rs index 03e68417e28..0b5aa0253bc 100644 --- a/shared/src/ledger/events.rs +++ b/shared/src/ledger/events.rs @@ -10,7 +10,7 @@ use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; use thiserror::Error; -use crate::ledger::native_vp::governance::utils::ProposalEvent; +use crate::ledger::governance::utils::ProposalEvent; use crate::tendermint_proto::abci::EventAttribute; use crate::types::ibc::IbcEvent; #[cfg(feature = "ferveo-tpke")] @@ -50,6 +50,8 @@ pub enum EventType { Ibc(String), /// The proposal that has been executed Proposal, + /// The pgf payment + PgfPayment, } impl Display for EventType { @@ -59,6 +61,7 @@ impl Display for EventType { EventType::Applied => write!(f, "applied"), EventType::Ibc(t) => write!(f, "{}", t), EventType::Proposal => write!(f, "proposal"), + EventType::PgfPayment => write!(f, "pgf_payment"), }?; Ok(()) } @@ -72,6 +75,7 @@ impl FromStr for EventType { "accepted" => Ok(EventType::Accepted), "applied" => Ok(EventType::Applied), "proposal" => Ok(EventType::Proposal), + "pgf_payments" => Ok(EventType::PgfPayment), // IBC "update_client" => Ok(EventType::Ibc("update_client".to_string())), "send_packet" => Ok(EventType::Ibc("send_packet".to_string())), diff --git a/shared/src/ledger/native_vp/governance/mod.rs b/shared/src/ledger/governance/mod.rs similarity index 50% rename from shared/src/ledger/native_vp/governance/mod.rs rename to shared/src/ledger/governance/mod.rs index d41242a4776..14dd40ae223 100644 --- a/shared/src/ledger/native_vp/governance/mod.rs +++ b/shared/src/ledger/governance/mod.rs @@ -4,19 +4,23 @@ pub mod utils; use std::collections::BTreeSet; -use namada_core::ledger::governance::storage as gov_storage; -use namada_core::ledger::storage; +use borsh::BorshDeserialize; +use namada_core::ledger::governance::storage::keys as gov_storage; +use namada_core::ledger::governance::storage::proposal::ProposalType; +use namada_core::ledger::governance::storage::vote::StorageProposalVote; +use namada_core::ledger::governance::utils::is_valid_validator_voting_period; +use namada_core::ledger::storage_api::governance::is_proposal_accepted; use namada_core::ledger::vp_env::VpEnv; -use namada_core::types::governance::{ProposalVote, VoteType}; -use namada_core::types::transaction::governance::ProposalType; +use namada_core::ledger::{pgf, storage}; +use namada_core::proto::Tx; +use namada_proof_of_stake::is_validator; use thiserror::Error; -use utils::is_valid_validator_voting_period; +use self::utils::ReadType; use crate::ledger::native_vp::{Ctx, NativeVp}; use crate::ledger::storage_api::StorageRead; use crate::ledger::{native_vp, pos}; -use crate::proto::Tx; -use crate::types::address::Address; +use crate::types::address::{Address, InternalAddress}; use crate::types::storage::{Epoch, Key}; use crate::types::token; use crate::vm::WasmCacheAccess; @@ -24,11 +28,23 @@ use crate::vm::WasmCacheAccess; /// for handling Governance NativeVP errors pub type Result = std::result::Result; +/// The governance internal address +pub const ADDRESS: Address = Address::Internal(InternalAddress::Governance); + +/// The maximum number of item in a pgf proposal +pub const MAX_PGF_ACTIONS: usize = 20; + #[allow(missing_docs)] #[derive(Error, Debug)] pub enum Error { #[error("Native VP error: {0}")] NativeVpError(#[from] native_vp::Error), + #[error("Proposal field should not be empty: {0}")] + EmptyProposalField(String), + #[error("Vote key is not valid: {0}")] + InvalidVoteKey(String), + #[error("Vote type is not compatible with proposal type.")] + InvalidVoteType, } /// Governance VP @@ -99,13 +115,7 @@ where (KeyType::PROPOSAL_COMMIT, _) => { self.is_valid_proposal_commit() } - (KeyType::PARAMETER, _) => self.is_valid_parameter( - if let Some(data) = &tx_data.data() { - data - } else { - return false; - }, - ), + (KeyType::PARAMETER, _) => self.is_valid_parameter(tx_data), (KeyType::BALANCE, _) => self.is_valid_balance(&native_token), (KeyType::UNKNOWN_GOVERNANCE, _) => Ok(false), (KeyType::UNKNOWN, _) => Ok(true), @@ -126,10 +136,9 @@ where { fn is_valid_key_set(&self, keys: &BTreeSet) -> Result<(bool, u64)> { let counter_key = gov_storage::get_counter_key(); - let pre_counter: u64 = - self.ctx.pre().read(&counter_key)?.unwrap_or_default(); + let pre_counter: u64 = self.force_read(&counter_key, ReadType::Pre)?; let post_counter: u64 = - self.ctx.post().read(&counter_key)?.unwrap_or_default(); + self.force_read(&counter_key, ReadType::Post)?; if post_counter < pre_counter { return Ok((false, 0)); @@ -150,7 +159,7 @@ where gov_storage::get_grace_epoch_key(counter), ]); - // Check that expected set is a subset the actual one + // Check that expected set is a subset of the actual one if !keys.is_superset(&mandatory_keys) { return Ok((false, 0)); } @@ -170,122 +179,80 @@ where gov_storage::get_voting_start_epoch_key(proposal_id); let voting_end_epoch_key = gov_storage::get_voting_end_epoch_key(proposal_id); + let proposal_type_key = gov_storage::get_proposal_type_key(proposal_id); - let current_epoch = self.ctx.get_block_epoch().ok(); + let current_epoch = self.ctx.get_block_epoch()?; - let pre_counter: Option = self.ctx.pre().read(&counter_key)?; - let pre_voting_start_epoch: Option = - self.ctx.pre().read(&voting_start_epoch_key)?; - let pre_voting_end_epoch: Option = - self.ctx.pre().read(&voting_end_epoch_key)?; + let pre_counter: u64 = self.force_read(&counter_key, ReadType::Pre)?; + let pre_voting_start_epoch: Epoch = + self.force_read(&voting_start_epoch_key, ReadType::Pre)?; + let pre_voting_end_epoch: Epoch = + self.force_read(&voting_end_epoch_key, ReadType::Pre)?; + let proposal_type: ProposalType = + self.force_read(&proposal_type_key, ReadType::Pre)?; 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, - proposal_type, - vote, - voter, - delegation_address, + let vote: StorageProposalVote = self.force_read(key, ReadType::Post)?; + + let (voter_address, delegation_address) = + match (voter, delegation_address) { + (Some(voter_address), Some(delegator_address)) => { + (voter_address, delegator_address) + } + _ => return Err(Error::InvalidVoteKey(key.to_string())), + }; + + // Invalid proposal id + if pre_counter <= proposal_id { + return Ok(false); + } + + // Voted outside of voting window. We dont check for validator because + // if the proposal type is validator, we need to let + // them vote for the entire voting window. + if !self.is_valid_voting_window( current_epoch, pre_voting_start_epoch, pre_voting_end_epoch, + false, ) { - ( - Some(pre_counter), - Some(proposal_type), - Some(vote), - Some(voter_address), - Some(delegation_address), - Some(current_epoch), - 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); - } + return Ok(false); + } - if let ProposalVote::Yay(vote_type) = vote { - 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), - } - } - } 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 - // - } - } + if !vote.is_compatible(&proposal_type) { + return Err(Error::InvalidVoteType); + } - 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), + // first check if validator, then check if delegator + let is_validator = self + .is_validator( + pre_voting_start_epoch, + verifiers, + voter_address, + delegation_address, + ) + .unwrap_or(false); + + if is_validator { + let valid_voting_period = is_valid_validator_voting_period( + current_epoch, + pre_voting_start_epoch, + pre_voting_end_epoch, + ); + return Ok(valid_voting_period); } + + let is_delegator = self + .is_delegator( + pre_voting_start_epoch, + verifiers, + voter_address, + delegation_address, + ) + .unwrap_or(false); + Ok(is_delegator) } /// Validate a content key @@ -299,39 +266,53 @@ where return Ok(false); } - let max_content_length: Option = - self.ctx.pre().read(&max_content_length_parameter_key)?; - let post_content: Option> = - self.ctx.read_bytes_post(&content_key)?; + let max_content_length: usize = + self.force_read(&max_content_length_parameter_key, ReadType::Pre)?; + let post_content = + self.ctx.read_bytes_post(&content_key)?.unwrap_or_default(); - match (post_content, max_content_length) { - (Some(post_content), Some(max_content_length)) => { - Ok(post_content.len() < max_content_length) - } - _ => Ok(false), - } + Ok(post_content.len() < max_content_length) } /// 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()) + let proposal_type: ProposalType = + self.force_read(&proposal_type_key, ReadType::Post)?; + + match proposal_type { + ProposalType::PGFSteward(stewards) => { + Ok(stewards.len() < MAX_PGF_ACTIONS) + } + ProposalType::PGFPayment(payments) => { + if payments.len() < MAX_PGF_ACTIONS { + return Ok(true); + } + let stewards_key = pgf::storage::keys::get_stewards_key(); + let author_key = gov_storage::get_author_key(proposal_id); + + let author: Option
= + self.ctx.pre().read(&author_key)?; + let stewards: BTreeSet
= + self.force_read(&stewards_key, ReadType::Pre)?; + + match author { + Some(address) => Ok(stewards.contains(&address)), + None => Ok(false), + } + } + _ => Ok(true), // default proposal + } } /// Validate a proposal code pub fn is_valid_proposal_code(&self, proposal_id: u64) -> Result { - let proposal_type_key: Key = - gov_storage::get_proposal_type_key(proposal_id); - let proposal_type: Option = - self.ctx.read_post(&proposal_type_key)?; + let proposal_type_key = gov_storage::get_proposal_type_key(proposal_id); + let proposal_type: ProposalType = + self.force_read(&proposal_type_key, ReadType::Post)?; - // Check that the proposal type admits wasm code - match proposal_type { - Some(ProposalType::Default(_)) => (), - _ => return Ok(false), + if !proposal_type.is_default() { + return Ok(false); } let code_key = gov_storage::get_proposal_code_key(proposal_id); @@ -343,22 +324,21 @@ where return Ok(false); } - let max_proposal_length: Option = - self.ctx.pre().read(&max_code_size_parameter_key)?; - let post_code: Option> = self.ctx.read_bytes_post(&code_key)?; + let max_proposal_length: usize = + self.force_read(&max_code_size_parameter_key, ReadType::Pre)?; + let post_code: Vec = + self.ctx.read_bytes_post(&code_key)?.unwrap_or_default(); - match (post_code, max_proposal_length) { - (Some(post_code), Some(max_content_length)) => { - Ok(post_code.len() < max_content_length) - } - _ => Ok(false), - } + Ok(post_code.len() <= max_proposal_length) } /// Validate a grace_epoch key pub fn is_valid_grace_epoch(&self, proposal_id: u64) -> Result { + let start_epoch_key = + gov_storage::get_voting_start_epoch_key(proposal_id); let end_epoch_key = gov_storage::get_voting_end_epoch_key(proposal_id); let grace_epoch_key = gov_storage::get_grace_epoch_key(proposal_id); + let max_proposal_period = gov_storage::get_max_proposal_period_key(); let min_grace_epoch_key = gov_storage::get_min_proposal_grace_epoch_key(); @@ -367,27 +347,32 @@ where return Ok(false); } - let end_epoch: Option = self.ctx.post().read(&end_epoch_key)?; - let grace_epoch: Option = - self.ctx.post().read(&grace_epoch_key)?; - let min_grace_epoch: Option = - self.ctx.pre().read(&min_grace_epoch_key)?; - match (min_grace_epoch, grace_epoch, end_epoch) { - (Some(min_grace_epoch), Some(grace_epoch), Some(end_epoch)) => { - let committing_epoch_key = - gov_storage::get_committing_proposals_key( - proposal_id, - grace_epoch, - ); - let has_post_committing_epoch = - self.ctx.has_key_post(&committing_epoch_key)?; - - Ok(has_post_committing_epoch - && end_epoch < grace_epoch - && grace_epoch - end_epoch >= min_grace_epoch) - } - _ => Ok(false), - } + let start_epoch: Epoch = + self.force_read(&start_epoch_key, ReadType::Post)?; + let end_epoch: Epoch = + self.force_read(&end_epoch_key, ReadType::Post)?; + let grace_epoch: Epoch = + self.force_read(&grace_epoch_key, ReadType::Post)?; + let min_grace_epoch: u64 = + self.force_read(&min_grace_epoch_key, ReadType::Pre)?; + let max_proposal_period: u64 = + self.force_read(&max_proposal_period, ReadType::Pre)?; + + let committing_epoch_key = gov_storage::get_committing_proposals_key( + proposal_id, + grace_epoch.into(), + ); + let has_post_committing_epoch = + self.ctx.has_key_post(&committing_epoch_key)?; + + let is_valid_grace_epoch = end_epoch < grace_epoch + && (grace_epoch - end_epoch).0 >= min_grace_epoch; + let is_valid_max_proposal_perido = start_epoch < grace_epoch + && grace_epoch.0 - start_epoch.0 <= max_proposal_period; + + Ok(has_post_committing_epoch + && is_valid_grace_epoch + && is_valid_max_proposal_perido) } /// Validate a start_epoch key @@ -396,9 +381,9 @@ where gov_storage::get_voting_start_epoch_key(proposal_id); let end_epoch_key = gov_storage::get_voting_end_epoch_key(proposal_id); let min_period_parameter_key = - gov_storage::get_min_proposal_period_key(); + gov_storage::get_min_proposal_voting_period_key(); - let current_epoch = self.ctx.get_block_epoch().ok(); + let current_epoch = self.ctx.get_block_epoch()?; let has_pre_start_epoch = self.ctx.has_key_pre(&start_epoch_key)?; let has_pre_end_epoch = self.ctx.has_key_pre(&end_epoch_key)?; @@ -407,27 +392,19 @@ where return Ok(false); } - let start_epoch: Option = - self.ctx.post().read(&start_epoch_key)?; - let end_epoch: Option = self.ctx.post().read(&end_epoch_key)?; - let min_period: Option = - self.ctx.pre().read(&min_period_parameter_key)?; - - match (min_period, start_epoch, end_epoch, current_epoch) { - ( - Some(min_period), - Some(start_epoch), - Some(end_epoch), - Some(current_epoch), - ) => { - if end_epoch <= start_epoch || start_epoch <= current_epoch { - return Ok(false); - } - Ok((end_epoch - start_epoch) % min_period == 0 - && (end_epoch - start_epoch).0 >= min_period) - } - _ => Ok(false), + let start_epoch: Epoch = + self.force_read(&start_epoch_key, ReadType::Post)?; + let end_epoch: Epoch = + self.force_read(&end_epoch_key, ReadType::Post)?; + let min_period: u64 = + self.force_read(&min_period_parameter_key, ReadType::Pre)?; + + if end_epoch <= start_epoch || start_epoch <= current_epoch { + return Ok(false); } + + Ok((end_epoch - start_epoch) % min_period == 0 + && (end_epoch - start_epoch).0 >= min_period) } /// Validate a end_epoch key @@ -436,11 +413,11 @@ where gov_storage::get_voting_start_epoch_key(proposal_id); let end_epoch_key = gov_storage::get_voting_end_epoch_key(proposal_id); let min_period_parameter_key = - gov_storage::get_min_proposal_period_key(); + gov_storage::get_min_proposal_voting_period_key(); let max_period_parameter_key = gov_storage::get_max_proposal_period_key(); - let current_epoch = self.ctx.get_block_epoch().ok(); + let current_epoch = self.ctx.get_block_epoch()?; let has_pre_start_epoch = self.ctx.has_key_pre(&start_epoch_key)?; let has_pre_end_epoch = self.ctx.has_key_pre(&end_epoch_key)?; @@ -449,36 +426,21 @@ where return Ok(false); } - let start_epoch: Option = - self.ctx.post().read(&start_epoch_key)?; - let end_epoch: Option = self.ctx.post().read(&end_epoch_key)?; - let min_period: Option = - self.ctx.pre().read(&min_period_parameter_key)?; - let max_period: Option = - self.ctx.pre().read(&max_period_parameter_key)?; - match ( - min_period, - max_period, - start_epoch, - end_epoch, - current_epoch, - ) { - ( - Some(min_period), - Some(max_period), - Some(start_epoch), - Some(end_epoch), - Some(current_epoch), - ) => { - if end_epoch <= start_epoch || start_epoch <= current_epoch { - return Ok(false); - } - Ok((end_epoch - start_epoch) % min_period == 0 - && (end_epoch - start_epoch).0 >= min_period - && (end_epoch - start_epoch).0 <= max_period) - } - _ => Ok(false), + let start_epoch: Epoch = + self.force_read(&start_epoch_key, ReadType::Post)?; + let end_epoch: Epoch = + self.force_read(&end_epoch_key, ReadType::Post)?; + let min_period: u64 = + self.force_read(&min_period_parameter_key, ReadType::Pre)?; + let max_period: u64 = + self.force_read(&max_period_parameter_key, ReadType::Pre)?; + + if end_epoch <= start_epoch || start_epoch <= current_epoch { + return Ok(false); } + Ok((end_epoch - start_epoch) % min_period == 0 + && (end_epoch - start_epoch).0 >= min_period + && (end_epoch - start_epoch).0 <= max_period) } /// Validate a funds key @@ -492,33 +454,20 @@ where token::balance_key(native_token_address, self.ctx.address); let min_funds_parameter_key = gov_storage::get_min_proposal_fund_key(); - let min_funds_parameter: Option = - self.ctx.pre().read(&min_funds_parameter_key)?; + let min_funds_parameter: token::Amount = + self.force_read(&min_funds_parameter_key, ReadType::Pre)?; let pre_balance: Option = self.ctx.pre().read(&balance_key)?; - let post_balance: Option = - self.ctx.post().read(&balance_key)?; - let post_funds: Option = - self.ctx.post().read(&funds_key)?; - - match (min_funds_parameter, pre_balance, post_balance, post_funds) { - ( - Some(min_funds_parameter), - Some(pre_balance), - Some(post_balance), - Some(post_funds), - ) => Ok(post_funds >= min_funds_parameter - && post_balance - pre_balance == post_funds), - ( - Some(min_funds_parameter), - None, - Some(post_balance), - Some(post_funds), - ) => { - Ok(post_funds >= min_funds_parameter - && post_balance == post_funds) - } - _ => Ok(false), + let post_balance: token::Amount = + self.force_read(&balance_key, ReadType::Post)?; + let post_funds: token::Amount = + self.force_read(&funds_key, ReadType::Post)?; + + if let Some(pre_balance) = pre_balance { + Ok(post_funds >= min_funds_parameter + && post_balance - pre_balance == post_funds) + } else { + Ok(post_funds >= min_funds_parameter && post_balance == post_funds) } } @@ -528,24 +477,19 @@ where token::balance_key(native_token_address, self.ctx.address); let min_funds_parameter_key = gov_storage::get_min_proposal_fund_key(); - let min_funds_parameter: Option = - self.ctx.pre().read(&min_funds_parameter_key)?; let pre_balance: Option = self.ctx.pre().read(&balance_key)?; - let post_balance: Option = - self.ctx.post().read(&balance_key)?; - - match (min_funds_parameter, pre_balance, post_balance) { - ( - Some(min_funds_parameter), - Some(pre_balance), - Some(post_balance), - ) => Ok(post_balance > pre_balance - && post_balance - pre_balance >= min_funds_parameter), - (Some(min_funds_parameter), None, Some(post_balance)) => { - Ok(post_balance >= min_funds_parameter) - } - _ => Ok(false), + + let min_funds_parameter: token::Amount = + self.force_read(&min_funds_parameter_key, ReadType::Pre)?; + let post_balance: token::Amount = + self.force_read(&balance_key, ReadType::Post)?; + + if let Some(pre_balance) = pre_balance { + Ok(post_balance > pre_balance + && post_balance - pre_balance >= min_funds_parameter) + } else { + Ok(post_balance >= min_funds_parameter) } } @@ -558,70 +502,61 @@ where let author_key = gov_storage::get_author_key(proposal_id); let has_pre_author = self.ctx.has_key_pre(&author_key)?; - if has_pre_author { return Ok(false); } - let author = self.ctx.post().read(&author_key)?; + let author = self.force_read(&author_key, ReadType::Post)?; match author { - Some(author) => match author { - Address::Established(_) => { - let address_exist_key = Key::validity_predicate(&author); - let address_exist = - self.ctx.has_key_post(&address_exist_key)?; + Address::Established(_) => { + let address_exist_key = Key::validity_predicate(&author); + let address_exist = + self.ctx.has_key_post(&address_exist_key)?; - Ok(address_exist && verifiers.contains(&author)) - } - Address::Implicit(_) => Ok(verifiers.contains(&author)), - Address::Internal(_) => Ok(false), - }, - _ => Ok(false), + Ok(address_exist && verifiers.contains(&author)) + } + Address::Implicit(_) => Ok(verifiers.contains(&author)), + Address::Internal(_) => Ok(false), } } /// Validate a counter key pub fn is_valid_counter(&self, set_count: u64) -> Result { let counter_key = gov_storage::get_counter_key(); - let pre_counter: Option = self.ctx.pre().read(&counter_key)?; - let post_counter: Option = self.ctx.post().read(&counter_key)?; + let pre_counter: u64 = self.force_read(&counter_key, ReadType::Pre)?; + let post_counter: u64 = + self.force_read(&counter_key, ReadType::Post)?; - match (pre_counter, post_counter) { - (Some(pre_counter), Some(post_counter)) => { - Ok(pre_counter + set_count == post_counter) - } - _ => Ok(false), - } + Ok(pre_counter + set_count == post_counter) } /// Validate a commit key pub fn is_valid_proposal_commit(&self) -> Result { let counter_key = gov_storage::get_counter_key(); - let pre_counter: Option = self.ctx.pre().read(&counter_key)?; - let post_counter: Option = self.ctx.post().read(&counter_key)?; - - match (pre_counter, post_counter) { - (Some(pre_counter), Some(post_counter)) => { - // NOTE: can't do pre_counter + set_count == post_counter here - // because someone may update an empty proposal that just - // register a committing key causing a bug - Ok(pre_counter < post_counter) - } - _ => Ok(false), - } + let pre_counter: u64 = self.force_read(&counter_key, ReadType::Pre)?; + let post_counter: u64 = + self.force_read(&counter_key, ReadType::Post)?; + + // NOTE: can't do pre_counter + set_count == post_counter here + // because someone may update an empty proposal that just + // register a committing key causing a bug + Ok(pre_counter < post_counter) } /// Validate a governance parameter - pub fn is_valid_parameter(&self, tx_data: &[u8]) -> Result { - utils::is_proposal_accepted(&self.ctx.pre(), tx_data) - .map_err(Error::NativeVpError) + pub fn is_valid_parameter(&self, tx: &Tx) -> Result { + match tx.data() { + Some(data) => is_proposal_accepted(&self.ctx.pre(), data.as_ref()) + .map_err(Error::NativeVpError), + None => Ok(true), + } } /// Check if a vote is from a validator pub fn is_validator( &self, - epoch: Epoch, + _epoch: Epoch, verifiers: &BTreeSet
, address: &Address, delegation_address: &Address, @@ -631,23 +566,47 @@ where H: 'static + storage::StorageHasher, CA: 'static + WasmCacheAccess, { - let all_validators = - pos::namada_proof_of_stake::read_all_validator_addresses( - &self.ctx.pre(), - epoch, - )?; - if !all_validators.is_empty() { - let is_voter_validator = all_validators - .into_iter() - .any(|validator| validator.eq(address)); - let is_signer_validator = verifiers.contains(address); - let is_delegation_address = delegation_address.eq(address); - - Ok(is_voter_validator - && is_signer_validator - && is_delegation_address) + if !address.eq(delegation_address) { + return Ok(false); + } + + let is_validator = is_validator(&self.ctx.pre(), address)?; + + Ok(is_validator && verifiers.contains(address)) + } + + /// Private method to read from storage data that are 100% in storage. + fn force_read(&self, key: &Key, read_type: ReadType) -> Result + where + T: BorshDeserialize, + { + let res = match read_type { + ReadType::Pre => self.ctx.pre().read::(key), + ReadType::Post => self.ctx.post().read::(key), + }?; + + if let Some(data) = res { + Ok(data) } else { - Ok(false) + Err(Error::EmptyProposalField(key.to_string())) + } + } + + fn is_valid_voting_window( + &self, + current_epoch: Epoch, + start_epoch: Epoch, + end_epoch: Epoch, + is_validator: bool, + ) -> bool { + if is_validator { + is_valid_validator_voting_period( + current_epoch, + start_epoch, + end_epoch, + ) + } else { + current_epoch >= start_epoch && current_epoch <= end_epoch } } @@ -659,10 +618,6 @@ where address: &Address, delegation_address: &Address, ) -> Result { - // let bond_key = pos::bond_key(&BondId { - // source: address.clone(), - // validator: delegation_address.clone(), - // }); let bond_handle = pos::namada_proof_of_stake::bond_handle( address, delegation_address, @@ -670,7 +625,6 @@ where let params = pos::namada_proof_of_stake::read_pos_params(&self.ctx.pre())?; let bond = bond_handle.get_sum(&self.ctx.pre(), epoch, ¶ms)?; - // let bonds: Option = self.ctx.pre().read(&bond_key)?; if bond.is_some() && verifiers.contains(address) { Ok(true) @@ -681,7 +635,7 @@ where } #[allow(clippy::upper_case_acronyms)] -#[derive(Clone, Debug)] +#[derive(Debug)] enum KeyType { #[allow(non_camel_case_types)] COUNTER, diff --git a/shared/src/ledger/governance/utils.rs b/shared/src/ledger/governance/utils.rs new file mode 100644 index 00000000000..d4b4d1316c4 --- /dev/null +++ b/shared/src/ledger/governance/utils.rs @@ -0,0 +1,122 @@ +//! Governance utility functions + +use std::collections::HashMap; + +use namada_core::ledger::governance::utils::TallyResult; +use thiserror::Error; + +use crate::ledger::events::EventType; + +pub(super) enum ReadType { + Pre, + Post, +} + +/// Proposal errors +#[derive(Error, Debug)] +pub enum Error { + /// Invalid validator set deserialization + #[error("Invalid validator set")] + InvalidValidatorSet, + /// Invalid proposal field deserialization + #[error("Invalid proposal {0}")] + InvalidProposal(u64), + /// Error during tally + #[error("Error while tallying proposal: {0}")] + Tally(String), +} + +/// Proposal event definition +pub struct ProposalEvent { + /// Proposal event type + pub event_type: String, + /// Proposal event attributes + pub attributes: HashMap, +} + +impl ProposalEvent { + /// Create a proposal event + pub fn new( + event_type: String, + tally: TallyResult, + id: u64, + has_proposal_code: bool, + proposal_code_exit_status: bool, + ) -> Self { + let attributes = HashMap::from([ + ("tally_result".to_string(), tally.to_string()), + ("proposal_id".to_string(), id.to_string()), + ( + "has_proposal_code".to_string(), + (!has_proposal_code as u64).to_string(), + ), + ( + "proposal_code_exit_status".to_string(), + (!proposal_code_exit_status as u64).to_string(), + ), + ]); + Self { + event_type, + attributes, + } + } + + /// Create a new proposal event for rejected proposal + pub fn rejected_proposal_event(proposal_id: u64) -> Self { + ProposalEvent::new( + EventType::Proposal.to_string(), + TallyResult::Rejected, + proposal_id, + false, + false, + ) + } + + /// Create a new proposal event for default proposal + pub fn default_proposal_event( + proposal_id: u64, + has_code: bool, + execution_status: bool, + ) -> Self { + ProposalEvent::new( + EventType::Proposal.to_string(), + TallyResult::Passed, + proposal_id, + has_code, + execution_status, + ) + } + + /// Create a new proposal event for pgf stewards proposal + pub fn pgf_steward_proposal_event(proposal_id: u64, result: bool) -> Self { + ProposalEvent::new( + EventType::Proposal.to_string(), + TallyResult::Passed, + proposal_id, + false, + result, + ) + } + + /// Create a new proposal event for pgf payments proposal + pub fn pgf_payments_proposal_event(proposal_id: u64, result: bool) -> Self { + ProposalEvent::new( + EventType::Proposal.to_string(), + TallyResult::Passed, + proposal_id, + false, + result, + ) + } + + /// Create a new proposal event for eth proposal + pub fn eth_proposal_event(proposal_id: u64, result: bool) -> Self { + ProposalEvent::new( + EventType::Proposal.to_string(), + TallyResult::Passed, + proposal_id, + false, + result, + ) + } +} diff --git a/shared/src/ledger/mod.rs b/shared/src/ledger/mod.rs index 399f75f800b..a5b2a06819e 100644 --- a/shared/src/ledger/mod.rs +++ b/shared/src/ledger/mod.rs @@ -3,10 +3,12 @@ pub mod args; pub mod eth_bridge; pub mod events; +pub mod governance; pub mod ibc; pub mod inflation; pub mod masp; pub mod native_vp; +pub mod pgf; pub mod pos; #[cfg(all(feature = "wasm-runtime", feature = "ferveo-tpke"))] pub mod protocol; @@ -20,5 +22,5 @@ pub mod vp_host_fns; pub mod wallet; pub use namada_core::ledger::{ - gas, governance, parameters, replay_protection, storage_api, tx_env, vp_env, + gas, parameters, replay_protection, storage_api, tx_env, vp_env, }; diff --git a/shared/src/ledger/native_vp/governance/utils.rs b/shared/src/ledger/native_vp/governance/utils.rs deleted file mode 100644 index c00f57b5d9a..00000000000 --- a/shared/src/ledger/native_vp/governance/utils.rs +++ /dev/null @@ -1,484 +0,0 @@ -//! Governance utility functions - -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_validator_stake, -}; -use thiserror::Error; - -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, Tally, TallyResult, VotePower, VoteType, -}; -use crate::types::storage::Epoch; - -/// Proposal structure holding votes information necessary to compute the -/// outcome -pub struct Votes { - /// Map from validators who votes yay to their total stake amount - pub yay_validators: HashMap, - /// Map from delegation votes to their bond amount - pub delegators: - HashMap>, -} - -/// Proposal errors -#[derive(Error, Debug)] -pub enum Error { - /// Invalid validator set deserialization - #[error("Invalid validator set")] - InvalidValidatorSet, - /// Invalid proposal field deserialization - #[error("Invalid proposal {0}")] - InvalidProposal(u64), - /// Error during tally - #[error("Error while tallying proposal: {0}")] - Tally(String), -} - -/// Proposal event definition -pub struct ProposalEvent { - /// Proposal event type - pub event_type: String, - /// Proposal event attributes - pub attributes: HashMap, -} - -impl ProposalEvent { - /// Create a proposal event - pub fn new( - event_type: String, - tally: TallyResult, - id: u64, - has_proposal_code: bool, - proposal_code_exit_status: bool, - ) -> Self { - let attributes = HashMap::from([ - ("tally_result".to_string(), tally.to_string()), - ("proposal_id".to_string(), id.to_string()), - ( - "has_proposal_code".to_string(), - (!has_proposal_code as u64).to_string(), - ), - ( - "proposal_code_exit_status".to_string(), - (!proposal_code_exit_status as u64).to_string(), - ), - ]); - Self { - event_type, - attributes, - } - } -} - -/// Return a proposal result -pub fn compute_tally( - votes: Votes, - total_stake: VotePower, - proposal_type: &ProposalType, -) -> Result { - let Votes { - yay_validators, - delegators, - } = votes; - - match proposal_type { - ProposalType::Default(_) | ProposalType::ETHBridge => { - let mut total_yay_staked_tokens = VotePower::default(); - - for (_, (amount, validator_vote)) in yay_validators.iter() { - if let ProposalVote::Yay(vote_type) = validator_vote { - if proposal_type == vote_type { - total_yay_staked_tokens += *amount; - } else { - // Log the error and continue - tracing::error!( - "Unexpected vote type. Expected: {}, Found: {}", - proposal_type, - validator_vote - ); - continue; - } - } else { - // Log the error and continue - tracing::error!( - "Unexpected vote type. Expected: {}, Found: {}", - proposal_type, - validator_vote - ); - continue; - } - } - - // 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() - { - 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::Nay => { - // NAY: Remove delegator amount whose validator - // validator vote yay - - if yay_validators.contains_key(validator_address) { - total_yay_staked_tokens -= *vote_power; - } - } - - _ => { - // Log the error and continue - tracing::error!( - "Unexpected vote type. Expected: {}, Found: {}", - proposal_type, - delegator_vote - ); - continue; - } - } - } - } - - // 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) - } - _ => { - return Err(Error::Tally(format!( - "Unexpected proposal type: {}", - proposal_type - ))); - } - }; - - Ok(ProposalResult { - result: tally_result, - total_voting_power: total_stake, - total_yay_power: total_yay_staked_tokens, - total_nay_power: 0.into(), - }) - } else { - Ok(ProposalResult { - result: TallyResult::Rejected, - total_voting_power: total_stake, - total_yay_power: total_yay_staked_tokens, - total_nay_power: 0.into(), - }) - } - } - ProposalType::PGFCouncil => { - let mut total_yay_staked_tokens = HashMap::new(); - 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(VotePower::zero()) += *amount; - } - } else { - // Log the error and continue - tracing::error!( - "Unexpected vote type. Expected: PGFCouncil, Found: {}", - validator_vote - ); - continue; - } - } - - // 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, - ) - { - 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(Error::Tally( - format!( - "Expected PGF \ - vote {:?} was \ - not in tally", - vote - ), - )); - } - } else { - // Validator didn't vote for - // this, add voting power - *total_yay_staked_tokens - .entry(vote) - .or_insert( - VotePower::zero(), - ) += *vote_power; - } - } - } else { - // Log the error and continue - tracing::error!( - "Unexpected vote type. Expected: \ - PGFCouncil, Found: {}", - validator_vote - ); - continue; - } - } - None => { - // Validator didn't vote or voted nay, add - // delegator vote - - for vote in delegator_votes { - *total_yay_staked_tokens - .entry(vote) - .or_insert(VotePower::zero()) += - *vote_power; - } - } - } - } - ProposalVote::Nay => { - for ( - validator_address, - (vote_power, _delegator_vote), - ) in vote_map.iter() - { - if let Some((_, validator_vote)) = - 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(Error::Tally( - format!( - "Expected PGF vote \ - {:?} was not in tally", - vote - ), - )); - } - } - } else { - // Log the error and continue - tracing::error!( - "Unexpected vote type. Expected: \ - PGFCouncil, Found: {}", - validator_vote - ); - continue; - } - } - } - } - _ => { - // Log the error and continue - tracing::error!( - "Unexpected vote type. Expected: PGFCouncil, \ - Found: {}", - delegator_vote - ); - continue; - } - } - } - } - - // At least 1/3 of the total voting power must vote Yay - let total_yay_voted_power = total_yay_staked_tokens - .iter() - .fold(VotePower::zero(), |acc, (_, vote_power)| { - acc + *vote_power - }); - - match total_yay_voted_power.checked_mul(3.into()) { - 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: VotePower::zero(), - }), - _ => { - // 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)) - .map(|(vote, _)| vote.to_owned()) - .ok_or_else(|| { - Error::Tally( - "Missing expected elected council".to_string(), - ) - })?; - - Ok(ProposalResult { - result: TallyResult::Passed(Tally::PGFCouncil(council)), - total_voting_power: total_stake, - total_yay_power: total_yay_voted_power, - total_nay_power: VotePower::zero(), - }) - } - } - } - } -} - -/// Prepare Votes structure to compute proposal tally -pub fn get_proposal_votes( - storage: &S, - epoch: Epoch, - proposal_id: u64, -) -> storage_api::Result -where - S: storage_api::StorageRead, -{ - let params = read_pos_params(storage)?; - let validators = read_all_validator_addresses(storage, epoch)?; - - let vote_prefix_key = - gov_storage::get_proposal_vote_prefix_key(proposal_id); - let vote_iter = - storage_api::iter_prefix::(storage, &vote_prefix_key)?; - - let mut yay_validators = HashMap::new(); - let mut delegators: HashMap< - Address, - HashMap, - > = HashMap::new(); - - for next_vote in vote_iter { - let (vote_key, vote) = next_vote?; - let voter_address = gov_storage::get_voter_address(&vote_key); - match voter_address { - Some(voter_address) => { - if vote.is_yay() && validators.contains(voter_address) { - let amount: VotePower = read_validator_stake( - storage, - ¶ms, - voter_address, - epoch, - )? - .unwrap_or_default() - .try_into() - .expect("Amount out of bounds"); - - 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); - match validator_address { - Some(validator) => { - let bond_id = BondId { - source: voter_address.clone(), - validator: validator.clone(), - }; - let amount = - bond_amount(storage, &bond_id, epoch)?.1; - - if !amount.is_zero() { - let entry = delegators - .entry(voter_address.to_owned()) - .or_default(); - entry.insert( - validator.to_owned(), - ( - VotePower::try_from(amount).unwrap(), - vote, - ), - ); - } - } - None => continue, - } - } - } - None => continue, - } - } - - Ok(Votes { - yay_validators, - delegators, - }) -} - -/// Calculate the valid voting window for validator given a proposal epoch -/// details -pub fn is_valid_validator_voting_period( - current_epoch: Epoch, - voting_start_epoch: Epoch, - voting_end_epoch: Epoch, -) -> bool { - voting_start_epoch < voting_end_epoch - && current_epoch * 3 <= voting_start_epoch + voting_end_epoch * 2 -} - -/// Check if an accepted proposal is being executed -pub fn is_proposal_accepted( - storage: &S, - tx_data: &[u8], -) -> storage_api::Result -where - S: storage_api::StorageRead, -{ - let proposal_id = u64::try_from_slice(tx_data).ok(); - match proposal_id { - Some(id) => { - let proposal_execution_key = - gov_storage::get_proposal_execution_key(id); - storage.has_key(&proposal_execution_key) - } - None => Ok(false), - } -} diff --git a/shared/src/ledger/native_vp/mod.rs b/shared/src/ledger/native_vp/mod.rs index ed34545f167..02a3435397d 100644 --- a/shared/src/ledger/native_vp/mod.rs +++ b/shared/src/ledger/native_vp/mod.rs @@ -2,11 +2,9 @@ //! as the PoS and IBC modules. pub mod ethereum_bridge; -pub mod governance; pub mod multitoken; pub mod parameters; pub mod replay_protection; -pub mod slash_fund; use std::cell::RefCell; use std::collections::BTreeSet; diff --git a/shared/src/ledger/native_vp/parameters.rs b/shared/src/ledger/native_vp/parameters.rs index bb1db0ab307..a2559ad0ab7 100644 --- a/shared/src/ledger/native_vp/parameters.rs +++ b/shared/src/ledger/native_vp/parameters.rs @@ -8,7 +8,7 @@ use namada_core::types::address::Address; use namada_core::types::storage::Key; use thiserror::Error; -use super::governance; +use crate::core::ledger::storage_api::governance; use crate::ledger::native_vp::{self, Ctx, NativeVp}; use crate::vm::WasmCacheAccess; @@ -55,11 +55,10 @@ where return false; }; match key_type { - KeyType::PARAMETER => governance::utils::is_proposal_accepted( - &self.ctx.pre(), - &data, - ) - .unwrap_or(false), + KeyType::PARAMETER => { + governance::is_proposal_accepted(&self.ctx.pre(), &data) + .unwrap_or(false) + } KeyType::UNKNOWN_PARAMETER => false, KeyType::UNKNOWN => true, } diff --git a/shared/src/ledger/pgf/mod.rs b/shared/src/ledger/pgf/mod.rs new file mode 100644 index 00000000000..f402b0f97be --- /dev/null +++ b/shared/src/ledger/pgf/mod.rs @@ -0,0 +1,126 @@ +//! Pgf VP + +/// Pgf utility functions and structures +pub mod utils; + +use std::collections::BTreeSet; + +use namada_core::ledger::pgf::storage::keys as pgf_storage; +use namada_core::ledger::storage; +use namada_core::ledger::storage_api::governance::is_proposal_accepted; +use namada_core::proto::Tx; +use thiserror::Error; + +use crate::ledger::native_vp; +use crate::ledger::native_vp::{Ctx, NativeVp}; +use crate::types::address::{Address, InternalAddress}; +use crate::types::storage::Key; +use crate::vm::WasmCacheAccess; + +/// for handling Pgf NativeVP errors +pub type Result = std::result::Result; + +/// The PGF internal address +pub const ADDRESS: Address = Address::Internal(InternalAddress::Pgf); + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum Error { + #[error("Native VP error: {0}")] + NativeVpError(#[from] native_vp::Error), +} + +/// Pgf VP +pub struct PgfVp<'a, DB, H, CA> +where + DB: storage::DB + for<'iter> storage::DBIter<'iter>, + H: storage::StorageHasher, + CA: WasmCacheAccess, +{ + /// Context to interact with the host structures. + pub ctx: Ctx<'a, DB, H, CA>, +} + +impl<'a, DB, H, CA> NativeVp for PgfVp<'a, DB, H, CA> +where + DB: 'static + storage::DB + for<'iter> storage::DBIter<'iter>, + H: 'static + storage::StorageHasher, + CA: 'static + WasmCacheAccess, +{ + type Error = Error; + + fn validate_tx( + &self, + tx_data: &Tx, + keys_changed: &BTreeSet, + _verifiers: &BTreeSet
, + ) -> Result { + let result = keys_changed.iter().all(|key| { + let key_type = KeyType::from(key); + + let result = match key_type { + KeyType::STEWARDS => Ok(false), + KeyType::PAYMENTS => Ok(false), + KeyType::PGF_INFLATION_RATE + | KeyType::STEWARD_INFLATION_RATE => { + self.is_valid_parameter_change(tx_data) + } + KeyType::UNKNOWN_PGF => Ok(false), + KeyType::UNKNOWN => Ok(true), + }; + result.unwrap_or(false) + }); + Ok(result) + } +} + +impl<'a, DB, H, CA> PgfVp<'a, DB, H, CA> +where + DB: 'static + storage::DB + for<'iter> storage::DBIter<'iter>, + H: 'static + storage::StorageHasher, + CA: 'static + WasmCacheAccess, +{ + /// Validate a governance parameter + pub fn is_valid_parameter_change(&self, tx: &Tx) -> Result { + match tx.data() { + Some(data) => is_proposal_accepted(&self.ctx.pre(), data.as_ref()) + .map_err(Error::NativeVpError), + None => Ok(true), + } + } +} + +#[allow(clippy::upper_case_acronyms)] +#[derive(Debug)] +enum KeyType { + #[allow(non_camel_case_types)] + STEWARDS, + #[allow(non_camel_case_types)] + PAYMENTS, + #[allow(non_camel_case_types)] + PGF_INFLATION_RATE, + #[allow(non_camel_case_types)] + STEWARD_INFLATION_RATE, + #[allow(non_camel_case_types)] + UNKNOWN_PGF, + #[allow(non_camel_case_types)] + UNKNOWN, +} + +impl From<&Key> for KeyType { + fn from(key: &Key) -> Self { + if pgf_storage::is_stewards_key(key) { + Self::STEWARDS + } else if pgf_storage::is_payments_key(key) { + KeyType::PAYMENTS + } else if pgf_storage::is_pgf_inflation_rate_key(key) { + Self::PGF_INFLATION_RATE + } else if pgf_storage::is_steward_inflation_rate_key(key) { + Self::STEWARD_INFLATION_RATE + } else if pgf_storage::is_pgf_key(key) { + KeyType::UNKNOWN_PGF + } else { + KeyType::UNKNOWN + } + } +} diff --git a/shared/src/ledger/pgf/utils.rs b/shared/src/ledger/pgf/utils.rs new file mode 100644 index 00000000000..e1bec701baa --- /dev/null +++ b/shared/src/ledger/pgf/utils.rs @@ -0,0 +1,66 @@ +use std::collections::HashMap; + +use namada_core::types::address::Address; +use namada_core::types::token; + +use crate::ledger::events::EventType; + +/// Proposal event definition +pub struct ProposalEvent { + /// Proposal event type + pub event_type: String, + /// Proposal event attributes + pub attributes: HashMap, +} + +impl ProposalEvent { + /// Create a proposal event + pub fn new( + event_type: String, + target: Address, + amount: token::Amount, + is_steward: bool, + success: bool, + ) -> Self { + let attributes = HashMap::from([ + ("target".to_string(), target.to_string()), + ("amount".to_string(), amount.to_string_native()), + ("is_steward".to_string(), is_steward.to_string()), + ("successed".to_string(), success.to_string()), + ]); + Self { + event_type, + attributes, + } + } + + /// Create a new proposal event for pgf continous funding + pub fn pgf_funding_payment( + target: Address, + amount: token::Amount, + success: bool, + ) -> Self { + ProposalEvent::new( + EventType::PgfPayment.to_string(), + target, + amount, + false, + success, + ) + } + + /// Create a new proposal event for steward payments + pub fn pgf_steward_payment( + target: Address, + amount: token::Amount, + success: bool, + ) -> Self { + ProposalEvent::new( + EventType::PgfPayment.to_string(), + target, + amount, + true, + success, + ) + } +} diff --git a/shared/src/ledger/pos/vp.rs b/shared/src/ledger/pos/vp.rs index 18085c5c537..76607c3199c 100644 --- a/shared/src/ledger/pos/vp.rs +++ b/shared/src/ledger/pos/vp.rs @@ -3,6 +3,7 @@ use std::collections::BTreeSet; use std::panic::{RefUnwindSafe, UnwindSafe}; +use namada_core::ledger::storage_api::governance; // use borsh::BorshDeserialize; pub use namada_proof_of_stake; pub use namada_proof_of_stake::parameters::PosParams; @@ -12,7 +13,7 @@ pub use namada_proof_of_stake::types; use thiserror::Error; use super::is_params_key; -use crate::ledger::native_vp::{self, governance, Ctx, NativeVp}; +use crate::ledger::native_vp::{self, Ctx, NativeVp}; // use crate::ledger::pos::{ // is_validator_address_raw_hash_key, // is_validator_max_commission_rate_change_key, @@ -106,18 +107,14 @@ where tracing::debug!("\nValidating PoS Tx\n"); for key in keys_changed { - // println!("KEY: {}\n", key); if is_params_key(key) { let data = if let Some(data) = tx_data.data() { data } else { return Ok(false); }; - if !governance::utils::is_proposal_accepted( - &self.ctx.pre(), - &data, - ) - .map_err(Error::NativeVpError)? + if !governance::is_proposal_accepted(&self.ctx.pre(), &data) + .map_err(Error::NativeVpError)? { return Ok(false); } diff --git a/shared/src/ledger/protocol/mod.rs b/shared/src/ledger/protocol/mod.rs index 6cab156d7a8..7a0244932a0 100644 --- a/shared/src/ledger/protocol/mod.rs +++ b/shared/src/ledger/protocol/mod.rs @@ -8,15 +8,15 @@ use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use thiserror::Error; use crate::ledger::gas::{self, BlockGasMeter, VpGasMeter}; +use crate::ledger::governance::GovernanceVp; use crate::ledger::ibc::vp::Ibc; use crate::ledger::native_vp::ethereum_bridge::bridge_pool_vp::BridgePoolVp; use crate::ledger::native_vp::ethereum_bridge::vp::EthBridge; -use crate::ledger::native_vp::governance::GovernanceVp; use crate::ledger::native_vp::multitoken::MultitokenVp; use crate::ledger::native_vp::parameters::{self, ParametersVp}; use crate::ledger::native_vp::replay_protection::ReplayProtectionVp; -use crate::ledger::native_vp::slash_fund::SlashFundVp; use crate::ledger::native_vp::{self, NativeVp}; +use crate::ledger::pgf::PgfVp; use crate::ledger::pos::{self, PosVP}; use crate::ledger::storage::write_log::WriteLog; use crate::ledger::storage::{DBIter, Storage, StorageHasher, WlStorage, DB}; @@ -61,9 +61,9 @@ pub enum Error { #[error("IBC Token native VP: {0}")] MultitokenNativeVpError(crate::ledger::native_vp::multitoken::Error), #[error("Governance native VP error: {0}")] - GovernanceNativeVpError(crate::ledger::native_vp::governance::Error), - #[error("SlashFund native VP error: {0}")] - SlashFundNativeVpError(crate::ledger::native_vp::slash_fund::Error), + GovernanceNativeVpError(crate::ledger::governance::Error), + #[error("Pgf native VP error: {0}")] + PgfNativeVpError(crate::ledger::pgf::Error), #[error("Ethereum bridge native VP error: {0}")] EthBridgeNativeVpError(native_vp::ethereum_bridge::vp::Error), #[error("Ethereum bridge pool native VP error: {0}")] @@ -543,14 +543,6 @@ where gas_meter = governance.ctx.gas_meter.into_inner(); result } - InternalAddress::SlashFund => { - let slash_fund = SlashFundVp { ctx }; - let result = slash_fund - .validate_tx(tx, &keys_changed, &verifiers) - .map_err(Error::SlashFundNativeVpError); - gas_meter = slash_fund.ctx.gas_meter.into_inner(); - result - } InternalAddress::Multitoken => { let multitoken = MultitokenVp { ctx }; let result = multitoken @@ -585,6 +577,14 @@ where replay_protection_vp.ctx.gas_meter.into_inner(); result } + InternalAddress::Pgf => { + let pgf_vp = PgfVp { ctx }; + let result = pgf_vp + .validate_tx(tx, &keys_changed, &verifiers) + .map_err(Error::PgfNativeVpError); + gas_meter = pgf_vp.ctx.gas_meter.into_inner(); + result + } InternalAddress::IbcToken(_) | InternalAddress::Erc20(_) => { // The address should be a part of a multitoken key diff --git a/shared/src/ledger/queries/vp/governance.rs b/shared/src/ledger/queries/vp/governance.rs new file mode 100644 index 00000000000..230612cbb86 --- /dev/null +++ b/shared/src/ledger/queries/vp/governance.rs @@ -0,0 +1,38 @@ +// cd shared && cargo expand ledger::queries::vp::governance + +use namada_core::ledger::governance::storage::proposal::StorageProposal; +use namada_core::ledger::governance::utils::Vote; + +use crate::ledger::queries::types::RequestCtx; +use crate::ledger::storage::{DBIter, StorageHasher, DB}; +use crate::ledger::storage_api; + +// Governance queries +router! {GOV, + ( "proposal" / [id: u64 ] ) -> Option = proposal_id, + ( "proposal" / [id: u64 ] / "votes" ) -> Vec = proposal_id_votes, +} + +/// Find if the given address belongs to a validator account. +fn proposal_id( + ctx: RequestCtx<'_, D, H>, + id: u64, +) -> storage_api::Result> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + storage_api::governance::get_proposal_by_id(ctx.wl_storage, id) +} + +/// Find if the given address belongs to a validator account. +fn proposal_id_votes( + ctx: RequestCtx<'_, D, H>, + id: u64, +) -> storage_api::Result> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + storage_api::governance::get_proposal_votes(ctx.wl_storage, id) +} diff --git a/shared/src/ledger/queries/vp/mod.rs b/shared/src/ledger/queries/vp/mod.rs index ad05a2b88b1..c53386b2f8f 100644 --- a/shared/src/ledger/queries/vp/mod.rs +++ b/shared/src/ledger/queries/vp/mod.rs @@ -1,10 +1,16 @@ //! Queries router and handlers for validity predicates // Re-export to show in rustdoc! +pub use governance::Gov; +use governance::GOV; pub use pos::Pos; use pos::POS; pub use token::Token; use token::TOKEN; +mod governance; +pub use pgf::Pgf; +use pgf::PGF; +mod pgf; pub mod pos; mod token; @@ -13,6 +19,8 @@ mod token; router! {VP, ( "pos" ) = (sub POS), ( "token" ) = (sub TOKEN), + ( "governance" ) = (sub GOV), + ( "pgf" ) = (sub PGF), } /// Client-only methods for the router type are composed from router functions. diff --git a/shared/src/ledger/queries/vp/pgf.rs b/shared/src/ledger/queries/vp/pgf.rs new file mode 100644 index 00000000000..932d898068d --- /dev/null +++ b/shared/src/ledger/queries/vp/pgf.rs @@ -0,0 +1,36 @@ +use std::collections::BTreeSet; + +use namada_core::ledger::governance::storage::proposal::PGFTarget; +use namada_core::types::address::Address; + +use crate::ledger::queries::types::RequestCtx; +use crate::ledger::storage::{DBIter, StorageHasher, DB}; +use crate::ledger::storage_api; + +// PoS validity predicate queries +router! {PGF, + ( "stewards" ) -> BTreeSet
= stewards, + ( "fundings" ) -> BTreeSet = funding, +} + +/// Query the currect pgf steward set +fn stewards( + ctx: RequestCtx<'_, D, H>, +) -> storage_api::Result> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + storage_api::pgf::get_stewards(ctx.wl_storage) +} + +/// Query the continous pgf fundings +fn funding( + ctx: RequestCtx<'_, D, H>, +) -> storage_api::Result> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + storage_api::pgf::get_payments(ctx.wl_storage) +} diff --git a/shared/src/ledger/queries/vp/pos.rs b/shared/src/ledger/queries/vp/pos.rs index 075b936e252..aff5181a19b 100644 --- a/shared/src/ledger/queries/vp/pos.rs +++ b/shared/src/ledger/queries/vp/pos.rs @@ -69,6 +69,9 @@ router! {POS, ( "delegations" / [owner: Address] ) -> HashSet
= delegation_validators, + ( "delegations_at" / [owner: Address] / [epoch: opt Epoch] ) + -> HashMap = delegations, + ( "bond_deltas" / [source: Address] / [validator: Address] ) -> HashMap = bond_deltas, @@ -463,7 +466,6 @@ where /// Find all the validator addresses to whom the given `owner` address has /// some delegation in any epoch -#[allow(dead_code)] fn delegations( ctx: RequestCtx<'_, D, H>, owner: Address, diff --git a/shared/src/ledger/rpc.rs b/shared/src/ledger/rpc.rs index c9e458692ab..d0fad687be5 100644 --- a/shared/src/ledger/rpc.rs +++ b/shared/src/ledger/rpc.rs @@ -8,6 +8,9 @@ use borsh::BorshDeserialize; use masp_primitives::asset_type::AssetType; use masp_primitives::merkle_tree::MerklePath; use masp_primitives::sapling::Node; +use namada_core::ledger::governance::parameters::GovernanceParameters; +use namada_core::ledger::governance::storage::proposal::StorageProposal; +use namada_core::ledger::governance::utils::Vote; use namada_core::ledger::storage::LastBlock; #[cfg(not(feature = "mainnet"))] use namada_core::ledger::testnet_pow; @@ -23,11 +26,9 @@ use namada_proof_of_stake::types::{ }; use serde::Serialize; +use crate::core::ledger::governance::storage::keys as gov_storage; use crate::ledger::args::InputAmount; use crate::ledger::events::Event; -use crate::ledger::governance::parameters::GovParams; -use crate::ledger::governance::storage as gov_storage; -use crate::ledger::native_vp::governance::utils::Votes; use crate::ledger::queries::vp::pos::EnrichedBondsAndUnbondsDetails; use crate::ledger::queries::RPC; use crate::proto::Tx; @@ -37,7 +38,6 @@ use crate::tendermint_rpc::error::Error as TError; use crate::tendermint_rpc::query::Query; use crate::tendermint_rpc::Order; use crate::types::control_flow::{time, Halt, TryHalt}; -use crate::types::governance::{ProposalVote, VotePower}; use crate::types::hash::Hash; use crate::types::key::common; use crate::types::storage::{BlockHeight, BlockResults, Epoch, PrefixValue}; @@ -124,8 +124,8 @@ pub async fn query_block( fn unwrap_client_response( response: Result, ) -> T { - response.unwrap_or_else(|_err| { - panic!("Error in the query"); + response.unwrap_or_else(|err| { + panic!("Error in the query: {:?}", err.to_string()); }) } @@ -628,69 +628,6 @@ pub async fn query_tx_response( Ok(result) } -/// Get the votes for a given proposal id -pub async fn get_proposal_votes( - client: &C, - epoch: Epoch, - proposal_id: u64, -) -> Votes { - let validators = get_all_validators(client, epoch).await; - - let vote_prefix_key = - gov_storage::get_proposal_vote_prefix_key(proposal_id); - let vote_iter = - query_storage_prefix::(client, &vote_prefix_key).await; - - let mut yay_validators: HashMap = - HashMap::new(); - let mut delegators: HashMap< - Address, - HashMap, - > = HashMap::new(); - - if let Some(vote_iter) = vote_iter { - for (key, vote) in vote_iter { - let voter_address = gov_storage::get_voter_address(&key) - .expect("Vote key should contain the voting address.") - .clone(); - if vote.is_yay() && validators.contains(&voter_address) { - let amount: VotePower = - get_validator_stake(client, epoch, &voter_address) - .await - .try_into() - .expect("Amount of bonds"); - yay_validators.insert(voter_address, (amount, vote)); - } else if !validators.contains(&voter_address) { - let validator_address = - gov_storage::get_vote_delegation_address(&key) - .expect( - "Vote key should contain the delegation address.", - ) - .clone(); - let delegator_token_amount = get_bond_amount_at( - client, - &voter_address, - &validator_address, - epoch, - ) - .await; - if let Some(amount) = delegator_token_amount { - let entry = delegators.entry(voter_address).or_default(); - entry.insert( - validator_address, - (VotePower::from(amount), vote), - ); - } - } - } - } - - Votes { - yay_validators, - delegators, - } -} - /// Get the PoS parameters pub async fn get_pos_params( client: &C, @@ -764,6 +701,34 @@ pub async fn get_delegators_delegation< ) } +/// Get the delegator's delegation at some epoh +pub async fn get_delegators_delegation_at< + C: crate::ledger::queries::Client + Sync, +>( + client: &C, + address: &Address, + epoch: Epoch, +) -> HashMap { + unwrap_client_response::( + RPC.vp() + .pos() + .delegations(client, address, &Some(epoch)) + .await, + ) +} + +/// Query proposal by Id +pub async fn query_proposal_by_id( + client: &C, + proposal_id: u64, +) -> Option { + // let a = RPC.vp().gov().proposal_id(client, &proposal_id).await; + // println!("{:?}", a.err().unwrap()); + unwrap_client_response::( + RPC.vp().gov().proposal_id(client, &proposal_id).await, + ) +} + /// Query and return validator's commission rate and max commission rate change /// per epoch pub async fn query_commission_rate( @@ -900,11 +865,11 @@ pub async fn query_unbond_with_slashing< } /// Get the givernance parameters -pub async fn get_governance_parameters< +pub async fn query_governance_parameters< C: crate::ledger::queries::Client + Sync, >( client: &C, -) -> GovParams { +) -> GovernanceParameters { let key = gov_storage::get_max_proposal_code_size_key(); let max_proposal_code_size = query_storage_value::(client, &key) .await @@ -925,27 +890,37 @@ pub async fn get_governance_parameters< .await .expect("Parameter should be definied."); - let key = gov_storage::get_min_proposal_period_key(); - let min_proposal_period = query_storage_value::(client, &key) - .await - .expect("Parameter should be definied."); + let key = gov_storage::get_min_proposal_voting_period_key(); + let min_proposal_voting_period = + query_storage_value::(client, &key) + .await + .expect("Parameter should be definied."); let key = gov_storage::get_max_proposal_period_key(); let max_proposal_period = query_storage_value::(client, &key) .await .expect("Parameter should be definied."); - GovParams { - min_proposal_fund: u128::try_from(min_proposal_fund) - .expect("Amount out of bounds") as u64, + GovernanceParameters { + min_proposal_fund, max_proposal_code_size, - min_proposal_period, + min_proposal_voting_period, max_proposal_period, max_proposal_content_size, min_proposal_grace_epochs, } } +/// Get the givernance parameters +pub async fn query_proposal_votes( + client: &C, + proposal_id: u64, +) -> Vec { + unwrap_client_response::>( + RPC.vp().gov().proposal_id_votes(client, &proposal_id).await, + ) +} + /// Get the bond amount at the given epoch pub async fn get_bond_amount_at( client: &C, @@ -976,7 +951,6 @@ pub async fn bonds_and_unbonds( .await, ) } - /// Get bonds and unbonds with all details (slashes and rewards, if any) /// grouped by their bond IDs, enriched with extra information calculated from /// the data. diff --git a/shared/src/ledger/tx.rs b/shared/src/ledger/tx.rs index 20d6e186a6d..5f3a85566e4 100644 --- a/shared/src/ledger/tx.rs +++ b/shared/src/ledger/tx.rs @@ -16,9 +16,17 @@ use masp_primitives::transaction::components::transparent::fees::{ InputView as TransparentInputView, OutputView as TransparentOutputView, }; use masp_primitives::transaction::components::Amount; +use namada_core::ledger::governance::cli::onchain::{ + DefaultProposal, PgfFundingProposal, PgfStewardProposal, ProposalVote, +}; +use namada_core::ledger::governance::storage::proposal::ProposalType; +use namada_core::ledger::governance::storage::vote::StorageProposalVote; use namada_core::types::address::{masp, Address}; use namada_core::types::dec::Dec; use namada_core::types::token::MaspDenom; +use namada_core::types::transaction::governance::{ + InitProposalData, VoteProposalData, +}; use namada_proof_of_stake::parameters::PosParams; use namada_proof_of_stake::types::{CommissionPair, ValidatorState}; use prost::EncodeError; @@ -34,7 +42,6 @@ use crate::ibc::tx_msg::Msg; use crate::ibc::Height as IbcHeight; use crate::ibc_proto::cosmos::base::v1beta1::Coin; use crate::ledger::args::{self, InputAmount}; -use crate::ledger::governance::storage as gov_storage; use crate::ledger::masp::{ShieldedContext, ShieldedUtils}; use crate::ledger::rpc::{ self, format_denominated_amount, validate_amount, TxBroadcastData, @@ -199,6 +206,21 @@ pub enum Error { /// Like EncodeTxFailure but for the encode error type #[error("Encoding tx data, {0}, shouldn't fail")] EncodeFailure(EncodeError), + /// Failed to deserialize the proposal data from json + #[error("Failed to deserialize the proposal data: {0}")] + FailedGovernaneProposalDeserialize(String), + /// The proposal data are invalid + #[error("Proposal data are invalid: {0}")] + InvalidProposal(String), + /// The proposal vote is not valid + #[error("Proposal vote is invalid")] + InvalidProposalVote, + /// The proposal can't be voted + #[error("Proposal {0} can't be voted")] + InvalidProposalVotingPeriod(u64), + /// The proposal can't be found + #[error("Proposal {0} can't be found")] + ProposalDoesNotExist(u64), /// Encoding public key failure #[error("Encoding a public key, {0}, shouldn't fail")] EncodeKeyFailure(std::io::Error), @@ -1075,34 +1097,240 @@ pub async fn build_bond( Ok(tx) } -/// Check if current epoch is in the last third of the voting period of the -/// proposal. This ensures that it is safe to optimize the vote writing to -/// storage. -pub async fn is_safe_voting_window( +/// Build a default proposal governance +pub async fn build_default_proposal< + C: crate::ledger::queries::Client + Sync, +>( client: &C, - proposal_id: u64, - proposal_start_epoch: Epoch, -) -> Result { - let current_epoch = rpc::query_epoch(client).await; + args::InitProposal { + tx, + proposal_data: _, + native_token: _, + is_offline: _, + is_pgf_stewards: _, + is_pgf_funding: _, + tx_code_path, + }: args::InitProposal, + proposal: DefaultProposal, + gas_payer: &common::PublicKey, +) -> Result { + let mut init_proposal_data = + InitProposalData::try_from(proposal.clone()) + .map_err(|e| Error::InvalidProposal(e.to_string()))?; - let proposal_end_epoch_key = - gov_storage::get_voting_end_epoch_key(proposal_id); - let proposal_end_epoch = - rpc::query_storage_value::(client, &proposal_end_epoch_key) - .await; + let tx_code_hash = + query_wasm_code_hash(client, tx_code_path.to_str().unwrap()) + .await + .unwrap(); - match proposal_end_epoch { - Some(proposal_end_epoch) => { - Ok(!crate::ledger::native_vp::governance::utils::is_valid_validator_voting_period( - current_epoch, - proposal_start_epoch, - proposal_end_epoch, - )) - } - None => { - Err(Error::EpochNotInStorage) - } + let chain_id = tx.chain_id.clone().unwrap(); + + let mut tx_builder = Tx::new(chain_id, tx.expiration); + + let (_, extra_section_hash) = tx_builder + .add_extra_section(proposal.proposal.content.try_to_vec().unwrap()); + init_proposal_data.content = extra_section_hash; + + if let Some(init_proposal_code) = proposal.data { + tx_builder.add_extra_section(init_proposal_code); + init_proposal_data.r#type = + ProposalType::Default(Some(extra_section_hash)); + }; + + tx_builder + .add_code_from_hash(tx_code_hash) + .add_data(init_proposal_data); + + prepare_tx::( + client, + &tx, + &mut tx_builder, + gas_payer.clone(), + #[cfg(not(feature = "mainnet"))] + false, + ) + .await; + + Ok(tx_builder) +} + +/// Build a proposal vote +pub async fn build_vote_proposal( + client: &C, + args::VoteProposal { + tx, + proposal_id, + vote, + voter, + is_offline: _, + proposal_data: _, + tx_code_path, + }: args::VoteProposal, + epoch: Epoch, + gas_payer: &common::PublicKey, +) -> Result { + let proposal_vote = + ProposalVote::try_from(vote).map_err(|_| Error::InvalidProposalVote)?; + + let proposal_id = proposal_id.expect("Proposal id must be defined."); + let proposal = if let Some(proposal) = + rpc::query_proposal_by_id(client, proposal_id).await + { + proposal + } else { + return Err(Error::ProposalDoesNotExist(proposal_id)); + }; + + let storage_vote = + StorageProposalVote::build(&proposal_vote, &proposal.r#type) + .expect("Should be able to build the proposal vote"); + + let is_validator = rpc::is_validator(client, &voter).await; + + if !proposal.can_be_voted(epoch, is_validator) { + return Err(Error::InvalidProposalVotingPeriod(proposal_id)); } + + let delegations = rpc::get_delegators_delegation_at( + client, + &voter, + proposal.voting_start_epoch, + ) + .await + .keys() + .cloned() + .collect::>(); + + let data = VoteProposalData { + id: proposal_id, + vote: storage_vote, + voter: voter.clone(), + delegations, + }; + + let tx_code_hash = + query_wasm_code_hash(client, tx_code_path.to_str().unwrap()) + .await + .unwrap(); + + let chain_id = tx.chain_id.clone().unwrap(); + + let mut tx_builder = Tx::new(chain_id, tx.expiration); + tx_builder.add_code_from_hash(tx_code_hash).add_data(data); + + prepare_tx::( + client, + &tx, + &mut tx_builder, + gas_payer.clone(), + #[cfg(not(feature = "mainnet"))] + false, + ) + .await; + + Ok(tx_builder) +} + +/// Build a pgf funding proposal governance +pub async fn build_pgf_funding_proposal< + C: crate::ledger::queries::Client + Sync, +>( + client: &C, + args::InitProposal { + tx, + proposal_data: _, + native_token: _, + is_offline: _, + is_pgf_stewards: _, + is_pgf_funding: _, + tx_code_path, + }: args::InitProposal, + proposal: PgfFundingProposal, + gas_payer: &common::PublicKey, +) -> Result { + let mut init_proposal_data = + InitProposalData::try_from(proposal.clone()) + .map_err(|e| Error::InvalidProposal(e.to_string()))?; + + let tx_code_hash = + query_wasm_code_hash(client, tx_code_path.to_str().unwrap()) + .await + .unwrap(); + + let chain_id = tx.chain_id.clone().unwrap(); + + let mut tx_builder = Tx::new(chain_id, tx.expiration); + + let (_, extra_section_hash) = tx_builder + .add_extra_section(proposal.proposal.content.try_to_vec().unwrap()); + init_proposal_data.content = extra_section_hash; + + tx_builder + .add_code_from_hash(tx_code_hash) + .add_data(init_proposal_data); + + prepare_tx::( + client, + &tx, + &mut tx_builder, + gas_payer.clone(), + #[cfg(not(feature = "mainnet"))] + false, + ) + .await; + + Ok(tx_builder) +} + +/// Build a pgf funding proposal governance +pub async fn build_pgf_stewards_proposal< + C: crate::ledger::queries::Client + Sync, +>( + client: &C, + args::InitProposal { + tx, + proposal_data: _, + native_token: _, + is_offline: _, + is_pgf_stewards: _, + is_pgf_funding: _, + tx_code_path, + }: args::InitProposal, + proposal: PgfStewardProposal, + gas_payer: &common::PublicKey, +) -> Result { + let mut init_proposal_data = + InitProposalData::try_from(proposal.clone()) + .map_err(|e| Error::InvalidProposal(e.to_string()))?; + + let tx_code_hash = + query_wasm_code_hash(client, tx_code_path.to_str().unwrap()) + .await + .unwrap(); + + let chain_id = tx.chain_id.clone().unwrap(); + + let mut tx_builder = Tx::new(chain_id, tx.expiration); + + let (_, extra_section_hash) = tx_builder + .add_extra_section(proposal.proposal.content.try_to_vec().unwrap()); + init_proposal_data.content = extra_section_hash; + + tx_builder + .add_code_from_hash(tx_code_hash) + .add_data(init_proposal_data); + + prepare_tx::( + client, + &tx, + &mut tx_builder, + gas_payer.clone(), + #[cfg(not(feature = "mainnet"))] + false, + ) + .await; + + Ok(tx_builder) } /// Submit an IBC transfer diff --git a/shared/src/types/mod.rs b/shared/src/types/mod.rs index 4f1a795d409..25ffab20545 100644 --- a/shared/src/types/mod.rs +++ b/shared/src/types/mod.rs @@ -5,7 +5,7 @@ pub mod ibc; pub mod key; pub use namada_core::types::{ - address, chain, dec, eth_abi, eth_bridge_pool, ethereum_events, governance, - hash, internal, keccak, masp, storage, time, token, transaction, uint, + address, chain, dec, eth_abi, eth_bridge_pool, ethereum_events, hash, + internal, keccak, masp, storage, time, token, transaction, uint, validity_predicate, vote_extensions, voting_power, }; diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 65c0d306f35..b8c4d10c073 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -47,6 +47,7 @@ num-traits.workspace = true prost.workspace = true regex.workspace = true serde_json.workspace = true +serde.workspace = true sha2.workspace = true tempfile.workspace = true test-log.workspace = true diff --git a/tests/src/e2e/helpers.rs b/tests/src/e2e/helpers.rs index 1fde997ef69..e2d887b5a21 100644 --- a/tests/src/e2e/helpers.rs +++ b/tests/src/e2e/helpers.rs @@ -458,3 +458,9 @@ pub fn wait_for_wasm_pre_compile(ledger: &mut NamadaCmd) -> Result<()> { ledger.exp_string("Finished compiling all")?; Ok(()) } + +/// Convert epoch `min_duration` in seconds to `epochs_per_year` genesis +/// parameter. +pub fn epochs_per_year_from_min_duration(min_duration: u64) -> u64 { + 60 * 60 * 24 * 365 / min_duration +} diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 17a1ddccc14..2ea7d901f0c 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -20,7 +20,6 @@ use borsh::BorshSerialize; use color_eyre::eyre::Result; use data_encoding::HEXLOWER; use namada::types::address::Address; -use namada::types::governance::ProposalType; use namada::types::storage::Epoch; use namada::types::token; use namada_apps::client::tx::CLIShieldedUtils; @@ -30,6 +29,7 @@ use namada_apps::config::genesis::genesis_config::{ }; use namada_apps::config::utils::convert_tm_addr_to_socket_addr; use namada_apps::facade::tendermint_config::net::Address as TendermintAddress; +use namada_core::ledger::governance::cli::onchain::{PgfAction, PgfSteward}; use namada_test_utils::TestWasms; use namada_vp_prelude::testnet_pow; use serde_json::json; @@ -37,7 +37,8 @@ use setup::constants::*; use setup::Test; use super::helpers::{ - get_height, wait_for_block_height, wait_for_wasm_pre_compile, + epochs_per_year_from_min_duration, get_height, wait_for_block_height, + wait_for_wasm_pre_compile, }; use super::setup::{get_all_wasms_hashes, set_ethereum_bridge_mode, NamadaCmd}; use crate::e2e::helpers::{ @@ -1968,12 +1969,8 @@ fn proposal_submission() -> Result<()> { let valid_proposal_json_path = prepare_proposal_data( &test, albert, - ProposalType::Default(Some( - TestWasms::TxProposalCode - .path() - .to_string_lossy() - .to_string(), - )), + TestWasms::TxProposalCode.read_bytes(), + 12, ); let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); @@ -2003,7 +2000,7 @@ fn proposal_submission() -> Result<()> { ]; let mut client = run!(test, Bin::Client, proposal_query_args, Some(40))?; - client.exp_string("Proposal: 0")?; + client.exp_string("Proposal Id: 0")?; client.assert_success(); // 4. Query token balance proposal author (submitted funds) @@ -2039,66 +2036,28 @@ fn proposal_submission() -> Result<()> { // 6. Submit an invalid proposal // proposal is invalid due to voting_end_epoch - voting_start_epoch < 3 let albert = find_address(&test, ALBERT)?; - let invalid_proposal_json = json!( - { - "content": { - "title": "TheTitle", - "authors": "test@test.com", - "discussions-to": "www.github.com/anoma/aip/1", - "created": "2022-03-10T08:54:37Z", - "license": "MIT", - "abstract": "Ut convallis eleifend orci vel venenatis. Duis - vulputate metus in lacus sollicitudin vestibulum. Suspendisse vel velit - ac est consectetur feugiat nec ac urna. Ut faucibus ex nec dictum - fermentum. Morbi aliquet purus at sollicitudin ultrices. Quisque viverra - varius cursus. Praesent sed mauris gravida, pharetra turpis non, gravida - eros. Nullam sed ex justo. Ut at placerat ipsum, sit amet rhoncus libero. - Sed blandit non purus non suscipit. Phasellus sed quam nec augue bibendum - bibendum ut vitae urna. Sed odio diam, ornare nec sapien eget, congue - viverra enim.", - "motivation": "Ut convallis eleifend orci vel venenatis. Duis - vulputate metus in lacus sollicitudin vestibulum. Suspendisse vel velit - ac est consectetur feugiat nec ac urna. Ut faucibus ex nec dictum - fermentum. Morbi aliquet purus at sollicitudin ultrices.", - "details": "Ut convallis eleifend orci vel venenatis. Duis - vulputate metus in lacus sollicitudin vestibulum. Suspendisse vel velit - ac est consectetur feugiat nec ac urna. Ut faucibus ex nec dictum - fermentum. Morbi aliquet purus at sollicitudin ultrices. Quisque viverra - varius cursus. Praesent sed mauris gravida, pharetra turpis non, gravida - eros.", "requires": "2" - }, - "author": albert, - "voting_start_epoch": 9999_u64, - "voting_end_epoch": 10000_u64, - "grace_epoch": 10009_u64, - "type": { - "Default":null - } - } - ); - let invalid_proposal_json_path = - test.test_dir.path().join("invalid_proposal.json"); - generate_proposal_json_file( - invalid_proposal_json_path.as_path(), - &invalid_proposal_json, + let invalid_proposal_json = prepare_proposal_data( + &test, + albert, + TestWasms::TxProposalCode.read_bytes(), + 1, ); let submit_proposal_args = vec![ "init-proposal", "--data-path", - invalid_proposal_json_path.to_str().unwrap(), + invalid_proposal_json.to_str().unwrap(), "--node", &validator_one_rpc, ]; let mut client = run!(test, Bin::Client, submit_proposal_args, Some(40))?; - client.exp_string( - "Invalid proposal end epoch: difference between proposal start and \ - end epoch must be at least 3 and at max 27 and end epoch must be a \ - multiple of 3", + client.exp_regex( + "Proposal data are invalid: Invalid proposal start epoch: 1 must be \ + greater than current epoch .* and a multiple of 3", )?; client.assert_failure(); - // 7. Check invalid proposal was not accepted + // 7. Check invalid proposal was not submitted let proposal_query_args = vec![ "query-proposal", "--proposal-id", @@ -2108,7 +2067,7 @@ fn proposal_submission() -> Result<()> { ]; let mut client = run!(test, Bin::Client, proposal_query_args, Some(40))?; - client.exp_string("No valid proposal was found with id 1")?; + client.exp_string("No proposal found with id: 1")?; client.assert_success(); // 8. Query token balance (funds shall not be submitted) @@ -2207,7 +2166,10 @@ fn proposal_submission() -> Result<()> { ]; let mut client = run!(test, Bin::Client, query_proposal, Some(15))?; - client.exp_string("Result: passed")?; + client.exp_string("Proposal Id: 0")?; + client.exp_string( + "passed with 200900.000000 yay votes and 0.000000 nay votes (0.%)", + )?; client.assert_success(); // 12. Wait proposal grace and check proposal author funds @@ -2258,228 +2220,6 @@ 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 _bg_ledger = - start_namada_ledger_node_wait_wasm(&test, Some(0), Some(40))? - .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, 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(); - - // 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(); - - // 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(); - } - - 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 = "fd34672ab5"; - let vote_arg = format!("{} {}", signing_key, msg); - let submit_proposal_vote_delagator = vec![ - "vote-proposal", - "--proposal-id", - "0", - "--vote", - "yay", - "--eth", - &vote_arg, - "--address", - 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 vote_arg = format!("{} {}", signing_key, msg); - - let submit_proposal_vote = vec![ - "vote-proposal", - "--proposal-id", - "0", - "--vote", - "yay", - "--eth", - &vote_arg, - "--address", - "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: 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, - ]; - - 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 @@ -2553,12 +2293,18 @@ fn pgf_governance_proposal() -> Result<()> { // 1 - Submit proposal let albert = find_address(&test, ALBERT)?; + let pgf_stewards = PgfSteward { + action: PgfAction::Add, + address: albert.clone(), + }; + let valid_proposal_json_path = - prepare_proposal_data(&test, albert.clone(), ProposalType::PGFCouncil); + prepare_proposal_data(&test, albert, vec![pgf_stewards], 12); let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); let submit_proposal_args = vec![ "init-proposal", + "--pgf-stewards", "--data-path", valid_proposal_json_path.to_str().unwrap(), "--ledger-address", @@ -2569,22 +2315,6 @@ fn pgf_governance_proposal() -> Result<()> { 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)); - - 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(); - // 2 - Query the proposal let proposal_query_args = vec![ "query-proposal", @@ -2595,19 +2325,7 @@ fn pgf_governance_proposal() -> Result<()> { ]; 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.exp_string("Proposal Id: 0")?; client.assert_success(); // Query token balance proposal author (submitted funds) @@ -2622,7 +2340,7 @@ fn pgf_governance_proposal() -> Result<()> { ]; client = run!(test, Bin::Client, query_balance_args, Some(40))?; - client.exp_string("nam: 999000")?; + client.exp_string("nam: 999500")?; client.assert_success(); // Query token balance governance @@ -2637,7 +2355,7 @@ fn pgf_governance_proposal() -> Result<()> { ]; client = run!(test, Bin::Client, query_balance_args, Some(40))?; - client.exp_string("nam: 1000")?; + client.exp_string("nam: 500")?; client.assert_success(); // 3 - Send a yay vote from a validator @@ -2648,16 +2366,12 @@ fn pgf_governance_proposal() -> Result<()> { } let albert_address = find_address(&test, ALBERT)?; - let arg_vote = format!("{} 1000", albert_address); - let submit_proposal_vote = vec![ "vote-proposal", "--proposal-id", "0", "--vote", "yay", - "--pgf", - &arg_vote, "--address", "validator-0", "--ledger-address", @@ -2676,15 +2390,12 @@ fn pgf_governance_proposal() -> Result<()> { client.assert_success(); // Send different yay vote from delegator to check majority on 1/3 - let different_vote = format!("{} 900", albert_address); let submit_proposal_vote_delagator = vec![ "vote-proposal", "--proposal-id", "0", "--vote", "yay", - "--pgf", - &different_vote, "--address", BERTHA, "--ledger-address", @@ -2697,29 +2408,6 @@ fn pgf_governance_proposal() -> Result<()> { 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", - "yay", - "--pgf", - &different_vote, - "--address", - BERTHA, - "--ledger-address", - &validator_one_rpc, - ]; - - // this is valid because the client filter ALBERT delegation and there are - // none - let mut client = - run!(test, Bin::Client, submit_proposal_vote_delagator, Some(15))?; - client.exp_string("Transaction applied with 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) epoch = get_epoch(&test, &validator_one_rpc).unwrap(); @@ -2737,23 +2425,7 @@ fn pgf_governance_proposal() -> Result<()> { ]; 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(); - - // Query the second proposal and check the it didn't pass - let query_proposal = vec![ - "query-proposal-result", - "--proposal-id", - "1", - "--ledger-address", - &validator_one_rpc, - ]; - - client = run!(test, Bin::Client, query_proposal, Some(15))?; - client.exp_string("Result: rejected")?; + client.exp_string("passed")?; client.assert_success(); // 12. Wait proposals grace and check proposal author funds @@ -2773,7 +2445,7 @@ fn pgf_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 @@ -2791,6 +2463,15 @@ fn pgf_governance_proposal() -> Result<()> { client.exp_string("nam: 0")?; client.assert_success(); + // 14. Query pgf stewards + let query_pgf = vec!["query-pgf", "--node", &validator_one_rpc]; + + let mut client = run!(test, Bin::Client, query_pgf, Some(30))?; + client.exp_string("Pgf stewards:")?; + client.exp_string(&format!("- {}", albert_address))?; + client.exp_string("Pgf fundings: no fundings are currently set.")?; + client.assert_success(); + Ok(()) } @@ -2868,7 +2549,7 @@ fn proposal_offline() -> Result<()> { client.exp_string("Transaction is valid.")?; client.assert_success(); - // 2. Create an offline + // 2. Create an offline proposal let albert = find_address(&test, ALBERT)?; let valid_proposal_json = json!( { @@ -2884,12 +2565,7 @@ fn proposal_offline() -> Result<()> { "requires": "2" }, "author": albert, - "voting_start_epoch": 3_u64, - "voting_end_epoch": 9_u64, - "grace_epoch": 18_u64, - "type": { - "Default": null - } + "tally_epoch": 3_u64, } ); let valid_proposal_json_path = @@ -2899,6 +2575,12 @@ fn proposal_offline() -> Result<()> { &valid_proposal_json, ); + let mut epoch = get_epoch(&test, &validator_one_rpc).unwrap(); + while epoch.0 <= 3 { + sleep(1); + epoch = get_epoch(&test, &validator_one_rpc).unwrap(); + } + let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); let offline_proposal_args = vec![ @@ -2906,44 +2588,48 @@ fn proposal_offline() -> Result<()> { "--data-path", valid_proposal_json_path.to_str().unwrap(), "--offline", + "--signing-keys", + ALBERT_KEY, + "--output-folder-path", + test.test_dir.path().to_str().unwrap(), "--node", &validator_one_rpc, ]; let mut client = run!(test, Bin::Client, offline_proposal_args, Some(15))?; - client.exp_string("Proposal created: ")?; + let (_, matched) = client.exp_regex("Proposal serialized to: .*")?; client.assert_success(); - // 3. Generate an offline yay vote - let mut epoch = get_epoch(&test, &validator_one_rpc).unwrap(); - while epoch.0 <= 2 { - sleep(1); - epoch = get_epoch(&test, &validator_one_rpc).unwrap(); - } - - let proposal_path = test.test_dir.path().join("proposal"); + let proposal_path = matched + .split(':') + .collect::>() + .get(1) + .unwrap() + .trim() + .to_string(); + // 3. Generate an offline yay vote let submit_proposal_vote = vec![ "vote-proposal", "--data-path", - proposal_path.to_str().unwrap(), + &proposal_path, "--vote", "yay", "--address", ALBERT, "--offline", + "--signing-keys", + ALBERT_KEY, + "--output-folder-path", + test.test_dir.path().to_str().unwrap(), "--node", &validator_one_rpc, ]; let mut client = run!(test, Bin::Client, submit_proposal_vote, Some(15))?; - client.exp_string("Proposal vote created: ")?; + client.exp_string("Proposal vote serialized to: ")?; client.assert_success(); - let expected_file_name = format!("proposal-vote-{}", albert); - let expected_path_vote = test.test_dir.path().join(expected_file_name); - assert!(expected_path_vote.exists()); - // 4. Compute offline tally let tally_offline = vec![ "query-proposal-result", @@ -2955,7 +2641,8 @@ fn proposal_offline() -> Result<()> { ]; let mut client = run!(test, Bin::Client, tally_offline, Some(15))?; - client.exp_string("Result: rejected")?; + client.exp_string("Parsed 1 votes")?; + client.exp_string("rejected with 900.000000 yay votes")?; client.assert_success(); Ok(()) @@ -3773,7 +3460,8 @@ fn implicit_account_reveal_pk() -> Result<()> { let valid_proposal_json_path = prepare_proposal_data( &test, author, - ProposalType::Default(None), + TestWasms::TxProposalCode.read_bytes(), + 12, ); vec![ "init-proposal", @@ -3891,10 +3579,11 @@ fn test_epoch_sleep() -> Result<()> { fn prepare_proposal_data( test: &setup::Test, source: Address, - proposal_type: ProposalType, + data: impl serde::Serialize, + start_epoch: u64, ) -> PathBuf { - let valid_proposal_json = json!( - { + let valid_proposal_json = json!({ + "proposal": { "content": { "title": "TheTitle", "authors": "test@test.com", @@ -3907,12 +3596,13 @@ fn prepare_proposal_data( "requires": "2" }, "author": source, - "voting_start_epoch": 12_u64, + "voting_start_epoch": start_epoch, "voting_end_epoch": 24_u64, "grace_epoch": 30_u64, - "type": proposal_type - } - ); + }, + "data": data + }); + let valid_proposal_json_path = test.test_dir.path().join("valid_proposal.json"); generate_proposal_json_file( @@ -3921,9 +3611,3 @@ fn prepare_proposal_data( ); valid_proposal_json_path } - -/// Convert epoch `min_duration` in seconds to `epochs_per_year` genesis -/// parameter. -fn epochs_per_year_from_min_duration(min_duration: u64) -> u64 { - 60 * 60 * 24 * 365 / min_duration -} diff --git a/tx_prelude/src/lib.rs b/tx_prelude/src/lib.rs index 16103d6d349..cd2f5d5f92d 100644 --- a/tx_prelude/src/lib.rs +++ b/tx_prelude/src/lib.rs @@ -19,7 +19,6 @@ pub use borsh::{BorshDeserialize, BorshSerialize}; pub use namada_core::ledger::eth_bridge; pub use namada_core::ledger::governance::storage as gov_storage; pub use namada_core::ledger::parameters::storage as parameters_storage; -pub use namada_core::ledger::slash_fund::storage as slash_fund_storage; pub use namada_core::ledger::storage::types::encode; pub use namada_core::ledger::storage_api::{ self, governance, iter_prefix, iter_prefix_bytes, Error, OptionExt, diff --git a/vp_prelude/src/lib.rs b/vp_prelude/src/lib.rs index a7d19832e14..c8e555a87bc 100644 --- a/vp_prelude/src/lib.rs +++ b/vp_prelude/src/lib.rs @@ -72,7 +72,7 @@ pub fn log_string>(msg: T) { /// Checks if a proposal id is being executed pub fn is_proposal_accepted(ctx: &Ctx, proposal_id: u64) -> VpResult { let proposal_execution_key = - gov_storage::get_proposal_execution_key(proposal_id); + gov_storage::keys::get_proposal_execution_key(proposal_id); ctx.has_key_pre(&proposal_execution_key) } diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index a413f7b2fbf..594cb03a8aa 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -3890,6 +3890,7 @@ dependencies = [ "num-traits", "prost", "regex", + "serde", "serde_json", "sha2 0.9.9", "tempfile", diff --git a/wasm/checksums.json b/wasm/checksums.json index 29b6d2ea6f3..1dcc94d2479 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,21 +1,21 @@ { - "tx_bond.wasm": "tx_bond.08cedac1d825cd8ee9b04ef059bc68fee04f82fec57e0f6bc38123de8fb35b85.wasm", - "tx_bridge_pool.wasm": "tx_bridge_pool.c9ef2bdb19910e12eadabc7f0552b45c937bcf1f47ca09759e52112b68d94114.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.2be60082c6b891cb437b4392e1b2570f5d92fed35dd624cd079ea48c4bc7902c.wasm", - "tx_ibc.wasm": "tx_ibc.f8056f465d17043d2864309d1c50a897a028b6ebf9702e5c403e1bf8c670f6e6.wasm", - "tx_init_account.wasm": "tx_init_account.e91ab25cebd15af9a958eca4a34ba287804b1ff6ba4c82fc941f7f50816e9c4d.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.1683eb9a2b2616af21f4f55457f7e06e0ae0591f460ede412a548ea9b1ab694c.wasm", - "tx_init_validator.wasm": "tx_init_validator.67574f70c1f76c2c68498d17323793366c13d119480b6787c8c5ede312e51da0.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.e6375abba3f750700c06fbdeb2d5edec22b6a382116a67a7f9d7f7ba49f134e1.wasm", - "tx_transfer.wasm": "tx_transfer.e65b47bc94c5a09e3edc68298ad0e5e35041b91bc4d679bf5b7f433dffdce58e.wasm", - "tx_unbond.wasm": "tx_unbond.4fd70d297ccedb369bf88d8a8459a47ea733d329410387a6f80a0e026c6e480d.wasm", - "tx_unjail_validator.wasm": "tx_unjail_validator.28082f1002a97f06b52bc7a9267d1e5675a5241c5d37f7d0c58257f9fa2cc9b2.wasm", - "tx_update_vp.wasm": "tx_update_vp.65c5ca3e48fdef70e696460eca7540cf6196511d76fb2465133b145409329b3e.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.e0a003d922230d32b741b57ca18913cbc4d5d2290f02cb37dfdaa7f27cebb486.wasm", - "tx_withdraw.wasm": "tx_withdraw.40499cb0e268d3cc3d9bca5dacca05d8df35f5b90adf4087a5171505472d921a.wasm", - "vp_implicit.wasm": "vp_implicit.57af3b7d2ee9e2c9d7ef1e33a85646abeea7ea02dad19b7e0b20b289de5a7ae9.wasm", - "vp_masp.wasm": "vp_masp.4d656f775b7462095e7d9ff533e346829fad02568b1cf92fa2f99cc0677720ce.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.b0c855f0356a468db1d26baebc988508d4824836b86adb94ac432368feb03ac4.wasm", - "vp_user.wasm": "vp_user.31967a7411caf61729e46840eec90d2db386cf4745f49d59086a43cc3c3e2d39.wasm", - "vp_validator.wasm": "vp_validator.0bca8e34f5d51c74b2861d588e3dd6a8e61de7c63d356af63e2182e030ac42ee.wasm" + "tx_bond.wasm": "tx_bond.f737f9d4c536b627b4dd6c7a7f54de1d93ea7fa4648065f8861a74c5ab779d8c.wasm", + "tx_bridge_pool.wasm": "tx_bridge_pool.d3dcafbe987b7daf28b582f8f6c5ec784f2d7b9b2c5e25ad4e2c371a1e089156.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.ca16e544548224a07274a34d9fa4de0be2ca0317a5c0c75ade474c4f1a52921d.wasm", + "tx_ibc.wasm": "tx_ibc.37526a8aa8c8192afaf1d69190d26fb6f842ce8b812ed19fd4abe0c7b5f398b0.wasm", + "tx_init_account.wasm": "tx_init_account.f660b2d9be134bd33968e75a875b598dc0cd6d2c1b7a455df9cde66b77b0598a.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.bac6e829287e6af56268753db4db4488e9d1d6090e36af5e826337c0493de0d8.wasm", + "tx_init_validator.wasm": "tx_init_validator.2b88e1b4ecf0baee9b8fb61862a1dae91d12c13f65abea03179062485dc0965f.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.6e0e21d30b26f9736dc7df75dda806b311887d60d0767cf916a6adc22f3a970a.wasm", + "tx_transfer.wasm": "tx_transfer.c16423b1c59b9c02418895cf3e612ca9cda2260846f93188666919450f64fa6b.wasm", + "tx_unbond.wasm": "tx_unbond.b106bdcb2eab70b69a792eb64e2fe4c6fb146065569f2d234a27e0f6821aac08.wasm", + "tx_unjail_validator.wasm": "tx_unjail_validator.201c90d5ffbd371fe852b8ed65d5dd59049ee97adafb4da6963b4b150a6762a0.wasm", + "tx_update_account.wasm": "tx_update_account.9ee92e949b654aec3e6501534252b124ff9840c8060d0359bf8f0e3f226474be.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.aa98455871f48deffa0e52809cd37fa13bb2a32ff15efe314fc903265ba1337e.wasm", + "tx_withdraw.wasm": "tx_withdraw.c81ec78d17156d4c9905a3336da3189c03a8777ea8ee06854bf9cb7411dbce73.wasm", + "vp_implicit.wasm": "vp_implicit.a759a4677ae16f8ac48e9cced46723ae593fe9c981074078e70ec7fbe2be23f7.wasm", + "vp_masp.wasm": "vp_masp.a03d2f286f985301130eecff738f4c3c92958836f2030add8bfac4f0cc552b23.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.4eae2710b3a8f12e0a7ec5402e42965ac1c77803beb4e2f501faacac69330930.wasm", + "vp_user.wasm": "vp_user.93aa25048fe883e581f27f5b2fa782e17d0c0e422faa2f139b300ef31e435229.wasm", + "vp_validator.wasm": "vp_validator.72c78f3a8ef989f0d6939c59c1bc91d4ea9db45420f0242b3114cda3842a8bbd.wasm" } \ No newline at end of file diff --git a/wasm/wasm_source/src/tx_init_proposal.rs b/wasm/wasm_source/src/tx_init_proposal.rs index c0da1d9316b..e11ebb7a61f 100644 --- a/wasm/wasm_source/src/tx_init_proposal.rs +++ b/wasm/wasm_source/src/tx_init_proposal.rs @@ -8,22 +8,26 @@ fn apply_tx(ctx: &mut Ctx, tx: Tx) -> TxResult { let tx_data = transaction::governance::InitProposalData::try_from_slice(&data[..]) .wrap_err("failed to decode InitProposalData")?; + // Get the content from the referred to section let content = tx .get_section(&tx_data.content) .ok_or_err_msg("Missing proposal content")? .extra_data() .ok_or_err_msg("Missing full proposal content")?; + // Get the code from the referred to section - let code = match tx_data.r#type { - transaction::governance::ProposalType::Default(Some(hash)) => Some( + let code_hash = tx_data.get_section_code_hash(); + let code = match code_hash { + Some(hash) => Some( tx.get_section(&hash) .ok_or_err_msg("Missing proposal code")? .extra_data() .ok_or_err_msg("Missing full proposal code")?, ), - _ => None, + None => None, }; + log_string("apply_tx called to create a new governance proposal"); governance::init_proposal(ctx, tx_data, content, code) diff --git a/wasm/wasm_source/src/vp_implicit.rs b/wasm/wasm_source/src/vp_implicit.rs index 9e54225ec97..9da84d17767 100644 --- a/wasm/wasm_source/src/vp_implicit.rs +++ b/wasm/wasm_source/src/vp_implicit.rs @@ -34,8 +34,8 @@ impl<'a> From<&'a storage::Key> for KeyType<'a> { Self::Token { owner } } else if proof_of_stake::is_pos_key(key) { Self::PoS - } else if gov_storage::is_vote_key(key) { - let voter_address = gov_storage::get_voter_address(key); + } else if gov_storage::keys::is_vote_key(key) { + let voter_address = gov_storage::keys::get_voter_address(key); if let Some(address) = voter_address { Self::GovernanceVote(address) } else { diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index 301210f194f..d68801834a3 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -28,8 +28,8 @@ impl<'a> From<&'a storage::Key> for KeyType<'a> { Self::Token { owner } } else if proof_of_stake::is_pos_key(key) { Self::PoS - } else if gov_storage::is_vote_key(key) { - let voter_address = gov_storage::get_voter_address(key); + } else if gov_storage::keys::is_vote_key(key) { + let voter_address = gov_storage::keys::get_voter_address(key); if let Some(address) = voter_address { Self::GovernanceVote(address) } else { diff --git a/wasm/wasm_source/src/vp_validator.rs b/wasm/wasm_source/src/vp_validator.rs index 77afb1cb38c..9b0de80f3e9 100644 --- a/wasm/wasm_source/src/vp_validator.rs +++ b/wasm/wasm_source/src/vp_validator.rs @@ -30,8 +30,8 @@ impl<'a> From<&'a storage::Key> for KeyType<'a> { Self::Token { owner } } else if proof_of_stake::is_pos_key(key) { Self::PoS - } else if gov_storage::is_vote_key(key) { - let voter_address = gov_storage::get_voter_address(key); + } else if gov_storage::keys::is_vote_key(key) { + let voter_address = gov_storage::keys::get_voter_address(key); if let Some(address) = voter_address { Self::GovernanceVote(address) } else { diff --git a/wasm_for_tests/tx_memory_limit.wasm b/wasm_for_tests/tx_memory_limit.wasm index a1147ca70a4..a94693fb5bb 100755 Binary files a/wasm_for_tests/tx_memory_limit.wasm and b/wasm_for_tests/tx_memory_limit.wasm differ diff --git a/wasm_for_tests/tx_mint_tokens.wasm b/wasm_for_tests/tx_mint_tokens.wasm index eb897497a9d..922a6122c72 100755 Binary files a/wasm_for_tests/tx_mint_tokens.wasm and b/wasm_for_tests/tx_mint_tokens.wasm differ diff --git a/wasm_for_tests/tx_no_op.wasm b/wasm_for_tests/tx_no_op.wasm index 0c1caae3594..cfd0aeb26d1 100755 Binary files a/wasm_for_tests/tx_no_op.wasm and b/wasm_for_tests/tx_no_op.wasm differ diff --git a/wasm_for_tests/tx_proposal_code.wasm b/wasm_for_tests/tx_proposal_code.wasm index 0c2ff65fbb7..e6da887a4b6 100755 Binary files a/wasm_for_tests/tx_proposal_code.wasm and b/wasm_for_tests/tx_proposal_code.wasm differ diff --git a/wasm_for_tests/tx_read_storage_key.wasm b/wasm_for_tests/tx_read_storage_key.wasm index c54d5b0ba0f..c32c042dfe1 100755 Binary files a/wasm_for_tests/tx_read_storage_key.wasm and b/wasm_for_tests/tx_read_storage_key.wasm differ diff --git a/wasm_for_tests/tx_write.wasm b/wasm_for_tests/tx_write.wasm index 9207cc854fe..2ad523b70b9 100755 Binary files a/wasm_for_tests/tx_write.wasm and b/wasm_for_tests/tx_write.wasm differ diff --git a/wasm_for_tests/tx_write_storage_key.wasm b/wasm_for_tests/tx_write_storage_key.wasm index a0fb758ae96..5d600d185f1 100755 Binary files a/wasm_for_tests/tx_write_storage_key.wasm and b/wasm_for_tests/tx_write_storage_key.wasm differ diff --git a/wasm_for_tests/vp_always_false.wasm b/wasm_for_tests/vp_always_false.wasm index d8a31f53509..85064f4fce7 100755 Binary files a/wasm_for_tests/vp_always_false.wasm and b/wasm_for_tests/vp_always_false.wasm differ diff --git a/wasm_for_tests/vp_always_true.wasm b/wasm_for_tests/vp_always_true.wasm index 85a5a1db772..9f6878d28f3 100755 Binary files a/wasm_for_tests/vp_always_true.wasm and b/wasm_for_tests/vp_always_true.wasm differ diff --git a/wasm_for_tests/vp_eval.wasm b/wasm_for_tests/vp_eval.wasm index a383d642662..06920b9afe4 100755 Binary files a/wasm_for_tests/vp_eval.wasm and b/wasm_for_tests/vp_eval.wasm differ diff --git a/wasm_for_tests/vp_memory_limit.wasm b/wasm_for_tests/vp_memory_limit.wasm index 1b0fd306497..ed3e7c21956 100755 Binary files a/wasm_for_tests/vp_memory_limit.wasm and b/wasm_for_tests/vp_memory_limit.wasm differ diff --git a/wasm_for_tests/vp_read_storage_key.wasm b/wasm_for_tests/vp_read_storage_key.wasm index f984eead2d7..3858c2c5869 100755 Binary files a/wasm_for_tests/vp_read_storage_key.wasm and b/wasm_for_tests/vp_read_storage_key.wasm differ diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index 2cb67721afd..4e8cd9d41a3 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -3890,6 +3890,7 @@ dependencies = [ "num-traits", "prost", "regex", + "serde", "serde_json", "sha2 0.9.9", "tempfile", diff --git a/wasm_for_tests/wasm_source/src/lib.rs b/wasm_for_tests/wasm_source/src/lib.rs index bc99e36551c..790ad1a72c8 100644 --- a/wasm_for_tests/wasm_source/src/lib.rs +++ b/wasm_for_tests/wasm_source/src/lib.rs @@ -34,7 +34,7 @@ pub mod main { #[transaction] fn apply_tx(ctx: &mut Ctx, _tx_data: Tx) -> TxResult { // governance - let target_key = gov_storage::get_min_proposal_grace_epoch_key(); + let target_key = gov_storage::keys::get_min_proposal_grace_epoch_key(); ctx.write(&target_key, 9_u64)?; // parameters