diff --git a/.changelog/unreleased/features/2803-remove-offline-governance.md b/.changelog/unreleased/features/2803-remove-offline-governance.md new file mode 100644 index 0000000000..f49fef71ec --- /dev/null +++ b/.changelog/unreleased/features/2803-remove-offline-governance.md @@ -0,0 +1,2 @@ +- Removes offline governance as a proposal option. + ([\#2803](https://github.com/anoma/namada/pull/2803)) \ No newline at end of file diff --git a/.github/workflows/scripts/e2e.json b/.github/workflows/scripts/e2e.json index c151f03b02..56bca7fc25 100644 --- a/.github/workflows/scripts/e2e.json +++ b/.github/workflows/scripts/e2e.json @@ -14,7 +14,6 @@ "e2e::ledger_tests::pos_bonds": 77, "e2e::ledger_tests::implicit_account_reveal_pk": 30, "e2e::ledger_tests::pos_init_validator": 40, - "e2e::ledger_tests::proposal_offline": 21, "e2e::ledger_tests::rollback": 21, "e2e::ledger_tests::pgf_governance_proposal": 320, "e2e::ledger_tests::proposal_submission": 200, diff --git a/crates/apps/src/lib/bench_utils.rs b/crates/apps/src/lib/bench_utils.rs index 4179548411..59cebef384 100644 --- a/crates/apps/src/lib/bench_utils.rs +++ b/crates/apps/src/lib/bench_utils.rs @@ -250,10 +250,9 @@ impl Default for BenchShell { let signed_tx = bench_shell.generate_tx( TX_INIT_PROPOSAL_WASM, InitProposalData { - id: 0, content: content_section.get_hash(), author: defaults::albert_address(), - r#type: ProposalType::Default(None), + r#type: ProposalType::Default, voting_start_epoch, voting_end_epoch: voting_start_epoch + 3_u64, grace_epoch: voting_start_epoch + 9_u64, diff --git a/crates/apps/src/lib/cli.rs b/crates/apps/src/lib/cli.rs index 33e37f4dc2..4c2876f4d5 100644 --- a/crates/apps/src/lib/cli.rs +++ b/crates/apps/src/lib/cli.rs @@ -3189,7 +3189,6 @@ pub mod args { 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"); pub const PUBLIC_KEY: Arg = arg("public-key"); @@ -4888,7 +4887,6 @@ pub mod args { InitProposal:: { tx: self.tx.to_sdk(ctx), 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, tx_code_path: self.tx_code_path, @@ -4900,7 +4898,6 @@ pub mod args { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); let proposal_data = DATA_PATH.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); @@ -4909,7 +4906,6 @@ pub mod args { tx, proposal_data, tx_code_path, - is_offline, is_pgf_stewards, is_pgf_funding, } @@ -4920,19 +4916,6 @@ pub mod args { .arg(DATA_PATH.def().help( "The data path file (json) that describes the proposal.", )) - .arg( - PROPOSAL_OFFLINE - .def() - .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() @@ -4975,12 +4958,9 @@ pub mod args { tx: self.tx.to_sdk(ctx), proposal_id: self.proposal_id, vote: self.vote, - voter: ctx.borrow_chain_or_exit().get(&self.voter), - is_offline: self.is_offline, - proposal_data: self.proposal_data.map(|path| { - std::fs::read(path) - .expect("Should be able to read the file.") - }), + voter_address: ctx + .borrow_chain_or_exit() + .get(&self.voter_address), tx_code_path: self.tx_code_path.to_path_buf(), } } @@ -4989,54 +4969,26 @@ pub mod args { impl Args for VoteProposal { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); - let proposal_id = PROPOSAL_ID_OPT.parse(matches); + let proposal_id = PROPOSAL_ID.parse(matches); let vote = PROPOSAL_VOTE.parse(matches); - let voter = ADDRESS.parse(matches); - let is_offline = PROPOSAL_OFFLINE.parse(matches); - let proposal_data = DATA_PATH_OPT.parse(matches); + let voter_address = ADDRESS.parse(matches); let tx_code_path = PathBuf::from(TX_VOTE_PROPOSAL); Self { tx, proposal_id, vote, - is_offline, - voter, - proposal_data, + voter_address, tx_code_path, } } fn def(app: App) -> App { app.add_args::>() - .arg( - PROPOSAL_ID_OPT - .def() - .help("The proposal identifier.") - .conflicts_with_all([ - PROPOSAL_OFFLINE.name, - DATA_PATH_OPT.name, - ]), - ) + .arg(PROPOSAL_ID_OPT.def().help("The proposal identifier.")) .arg(PROPOSAL_VOTE.def().help( "The vote for the proposal. Either yay, nay, or abstain.", )) - .arg( - PROPOSAL_OFFLINE - .def() - .help("Flag if the proposal vote should run offline.") - .conflicts_with(PROPOSAL_ID.name), - ) - .arg( - DATA_PATH_OPT - .def() - .help( - "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.")) } } @@ -5124,11 +5076,7 @@ pub mod args { /// Common query args pub query: Query, /// Proposal id - pub proposal_id: Option, - /// Flag if proposal result should be run on offline data - pub offline: bool, - /// The folder containing the proposal and votes - pub proposal_folder: Option, + pub proposal_id: u64, } impl CliToSdk> for QueryProposalResult { @@ -5136,8 +5084,6 @@ pub mod args { QueryProposalResult:: { query: self.query.to_sdk(ctx), proposal_id: self.proposal_id, - offline: self.offline, - proposal_folder: self.proposal_folder, } } } @@ -5145,49 +5091,14 @@ pub mod args { impl Args for QueryProposalResult { fn parse(matches: &ArgMatches) -> Self { let query = Query::parse(matches); - let proposal_id = PROPOSAL_ID_OPT.parse(matches); - let offline = PROPOSAL_OFFLINE.parse(matches); - let proposal_folder = DATA_PATH_OPT.parse(matches); + let proposal_id = PROPOSAL_ID.parse(matches); - Self { - query, - proposal_id, - offline, - proposal_folder, - } + Self { query, proposal_id } } fn def(app: App) -> App { app.add_args::>() - .arg( - PROPOSAL_ID_OPT - .def() - .help("The proposal identifier.") - .conflicts_with_all([ - PROPOSAL_OFFLINE.name, - DATA_PATH_OPT.name, - ]), - ) - .arg( - PROPOSAL_OFFLINE - .def() - .help( - "Flag if the proposal result should run on \ - offline data.", - ) - .conflicts_with(PROPOSAL_ID.name) - .requires(DATA_PATH_OPT.name), - ) - .arg( - DATA_PATH_OPT - .def() - .help( - "The path to the folder containing the proposal \ - and votes files in json format.", - ) - .conflicts_with(PROPOSAL_ID.name) - .requires(PROPOSAL_OFFLINE.name), - ) + .arg(PROPOSAL_ID.def().help("The proposal identifier.")) } } diff --git a/crates/apps/src/lib/client/rpc.rs b/crates/apps/src/lib/client/rpc.rs index 613ad8cb90..e5a8255219 100644 --- a/crates/apps/src/lib/client/rpc.rs +++ b/crates/apps/src/lib/client/rpc.rs @@ -2,7 +2,6 @@ use std::cmp::Ordering; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; -use std::fs::{self, read_dir}; use std::io; use std::str::FromStr; @@ -23,10 +22,6 @@ use namada::core::storage::{ BlockHeight, BlockResults, Epoch, IndexedTx, Key, KeySeg, }; use namada::core::token::{Change, MaspDigitPos}; -use namada::governance::cli::offline::{ - find_offline_proposal, find_offline_votes, read_offline_files, - OfflineSignedProposal, OfflineVote, -}; use namada::governance::parameters::GovernanceParameters; use namada::governance::pgf::parameters::PgfParameters; use namada::governance::pgf::storage::steward::StewardDetail; @@ -34,9 +29,8 @@ use namada::governance::storage::keys as governance_storage; use namada::governance::storage::proposal::{ StoragePgfFunding, StorageProposal, }; -use namada::governance::utils::{ - compute_proposal_result, ProposalVotes, TallyType, TallyVote, VotePower, -}; +use namada::governance::utils::{ProposalVotes, VotePower}; +use namada::governance::ProposalVote; use namada::io::Io; use namada::ledger::events::Event; use namada::ledger::ibc::storage::{ @@ -1255,133 +1249,43 @@ pub async fn query_proposal_result( context: &impl Namada, args: args::QueryProposalResult, ) { - if args.proposal_id.is_some() { - let proposal_id = - args.proposal_id.expect("Proposal id should be defined."); + let proposal_id = args.proposal_id; - let current_epoch = query_epoch(context.client()).await.unwrap(); - let proposal_result = namada_sdk::rpc::query_proposal_result( - context.client(), - proposal_id, - ) - .await; - let proposal_query = namada_sdk::rpc::query_proposal_by_id( - context.client(), - proposal_id, - ) - .await; + let current_epoch = query_epoch(context.client()).await.unwrap(); + let proposal_result = + namada_sdk::rpc::query_proposal_result(context.client(), proposal_id) + .await; + let proposal_query = + namada_sdk::rpc::query_proposal_by_id(context.client(), proposal_id) + .await; - if let (Ok(Some(proposal_result)), Ok(Some(proposal_query))) = - (proposal_result, proposal_query) - { - display_line!(context.io(), "Proposal Id: {} ", proposal_id); - if current_epoch > proposal_query.voting_end_epoch { - display_line!(context.io(), "{:4}{}", "", proposal_result); + if let (Ok(Some(proposal_result)), Ok(Some(proposal_query))) = + (proposal_result, proposal_query) + { + display_line!(context.io(), "Proposal Id: {} ", proposal_id); + if current_epoch > proposal_query.voting_end_epoch { + display_line!(context.io(), "{:4}{}", "", proposal_result); + } else { + display_line!( + context.io(), + "{:4}Still voting until epoch {}", + "", + proposal_query.voting_end_epoch + ); + let res = format!("{}", proposal_result); + if let Some(idx) = res.find(' ') { + let slice = &res[idx..]; + display_line!(context.io(), "{:4}Currently{}", "", slice); } else { display_line!( context.io(), - "{:4}Still voting until epoch {}", + "{:4}Error parsing the result string", "", - proposal_query.voting_end_epoch ); - let res = format!("{}", proposal_result); - if let Some(idx) = res.find(' ') { - let slice = &res[idx..]; - display_line!(context.io(), "{:4}Currently{}", "", slice); - } else { - display_line!( - context.io(), - "{:4}Error parsing the result string", - "", - ); - } } - } else { - edisplay_line!(context.io(), "Proposal {} not found.", proposal_id); - }; + } } 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( - context.client(), - &proposal.proposal.author, - ) - .await - .unwrap() - .expect("Account should exist."); - - let proposal = proposal.validate( - &author_account.public_keys_map, - author_account.threshold, - false, - ); - - if let Ok(proposal) = proposal { - proposal - } else { - edisplay_line!( - context.io(), - "The offline proposal is not valid." - ); - return; - } - } else { - edisplay_line!( - context.io(), - "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(context, &proposal, votes.clone()) - .await; - let total_voting_power = get_total_staked_tokens( - context.client(), - proposal.proposal.tally_epoch, - ) - .await; - - let proposal_result = compute_proposal_result( - proposal_votes, - total_voting_power, - TallyType::TwoThirds, - ); - - display_line!( - context.io(), - "Proposal offline: {}", - proposal.proposal.hash() - ); - display_line!(context.io(), "Parsed {} votes.", votes.len()); - display_line!(context.io(), "{:4}{}", "", proposal_result); + edisplay_line!(context.io(), "Proposal {} not found.", proposal_id); } } @@ -2886,69 +2790,6 @@ fn unwrap_client_response( }) } -pub async fn compute_offline_proposal_votes( - context: &impl Namada, - 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(context.client(), &vote.address).await; - let is_delegator = is_delegator(context.client(), &vote.address).await; - if is_validator { - let validator_stake = get_validator_stake( - context.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( - context.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 { - display_line!( - context.io(), - "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, >( @@ -2960,10 +2801,12 @@ pub async fn compute_proposal_votes< .await .unwrap(); - let mut validators_vote: HashMap = HashMap::default(); + let mut validators_vote: HashMap = + HashMap::default(); let mut validator_voting_power: HashMap = HashMap::default(); - let mut delegators_vote: HashMap = HashMap::default(); + let mut delegators_vote: HashMap = + HashMap::default(); let mut delegator_voting_power: HashMap< Address, HashMap, @@ -2976,7 +2819,7 @@ pub async fn compute_proposal_votes< .await .unwrap_or_default(); - validators_vote.insert(vote.validator.clone(), vote.data.into()); + validators_vote.insert(vote.validator.clone(), vote.data); validator_voting_power.insert(vote.validator, validator_stake); } else { let delegator_stake = get_bond_amount_at( @@ -2988,8 +2831,7 @@ pub async fn compute_proposal_votes< .await; if let Some(stake) = delegator_stake { - delegators_vote - .insert(vote.delegator.clone(), vote.data.into()); + delegators_vote.insert(vote.delegator.clone(), vote.data); delegator_voting_power .entry(vote.delegator.clone()) .or_default() diff --git a/crates/apps/src/lib/client/tx.rs b/crates/apps/src/lib/client/tx.rs index 95051d8c8e..47fe96ee1b 100644 --- a/crates/apps/src/lib/client/tx.rs +++ b/crates/apps/src/lib/client/tx.rs @@ -9,13 +9,9 @@ use ledger_transport_hid::TransportNativeHID; use namada::core::address::{Address, ImplicitAddress}; use namada::core::dec::Dec; use namada::core::key::{self, *}; -use namada::governance::cli::offline::{ - OfflineProposal, OfflineSignedProposal, OfflineVote, -}; use namada::governance::cli::onchain::{ DefaultProposal, PgfFundingProposal, PgfStewardProposal, }; -use namada::governance::ProposalVote; use namada::io::Io; use namada::state::EPOCH_SWITCH_BLOCKS_DELAY; use namada::tx::data::pos::{BecomeValidator, ConsensusKeyChange}; @@ -1020,52 +1016,7 @@ where let current_epoch = rpc::query_and_print_epoch(namada).await; let governance_parameters = rpc::query_governance_parameters(namada.client()).await; - let (mut tx_builder, signing_data) = if args.is_offline { - let proposal = OfflineProposal::try_from(args.proposal_data.as_ref()) - .map_err(|e| { - error::TxSubmitError::FailedGovernaneProposalDeserialize( - e.to_string(), - ) - })? - .validate(current_epoch, args.tx.force) - .map_err(|e| { - error::TxSubmitError::InvalidProposal(e.to_string()) - })?; - - let default_signer = Some(proposal.author.clone()); - let signing_data = aux_signing_data( - namada, - &args.tx, - Some(proposal.author.clone()), - default_signer, - ) - .await?; - - let mut wallet = namada.wallet_mut().await; - let signed_offline_proposal = proposal.sign( - args.tx - .signing_keys - .iter() - .map(|pk| wallet.find_key_by_pk(pk, None)) - .collect::>() - .expect("secret keys corresponding to public keys not found"), - &signing_data.account_public_keys_map.unwrap(), - ); - let output_file_path = signed_offline_proposal - .serialize(args.tx.output_folder) - .map_err(|e| { - error::TxSubmitError::FailedGovernaneProposalDeserialize( - e.to_string(), - ) - })?; - - display_line!( - namada.io(), - "Proposal serialized to: {}", - output_file_path - ); - return Ok(()); - } else if args.is_pgf_funding { + let (mut tx_builder, signing_data) = if args.is_pgf_funding { let proposal = PgfFundingProposal::try_from(args.proposal_data.as_ref()) .map_err(|e| { @@ -1160,69 +1111,7 @@ pub async fn submit_vote_proposal( where ::Error: std::fmt::Display, { - let (mut tx_builder, signing_data) = if args.is_offline { - let default_signer = Some(args.voter.clone()); - let signing_data = aux_signing_data( - namada, - &args.tx, - Some(args.voter.clone()), - default_signer.clone(), - ) - .await?; - - let proposal_vote = ProposalVote::try_from(args.vote) - .map_err(|_| error::TxSubmitError::InvalidProposalVote)?; - - let proposal = OfflineSignedProposal::try_from( - args.proposal_data.clone().unwrap().as_ref(), - ) - .map_err(|e| error::TxSubmitError::InvalidProposal(e.to_string()))? - .validate( - &signing_data.account_public_keys_map.clone().unwrap(), - signing_data.threshold, - args.tx.force, - ) - .map_err(|e| error::TxSubmitError::InvalidProposal(e.to_string()))?; - let delegations = rpc::get_delegators_delegation_at( - namada.client(), - &args.voter, - proposal.proposal.tally_epoch, - ) - .await - .keys() - .cloned() - .collect::>(); - - let offline_vote = OfflineVote::new( - &proposal, - proposal_vote, - args.voter.clone(), - delegations, - ); - - let mut wallet = namada.wallet_mut().await; - let offline_signed_vote = offline_vote.sign( - args.tx - .signing_keys - .iter() - .map(|pk| wallet.find_key_by_pk(pk, None)) - .collect::>() - .expect("secret keys corresponding to public keys not found"), - &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"); - - display_line!( - namada.io(), - "Proposal vote serialized to: {}", - output_file_path - ); - return Ok(()); - } else { - args.build(namada).await? - }; + let (mut tx_builder, signing_data) = args.build(namada).await?; if args.tx.dump_tx { tx::dump_tx(namada.io(), &args.tx, tx_builder); diff --git a/crates/apps/src/lib/node/ledger/shell/finalize_block.rs b/crates/apps/src/lib/node/ledger/shell/finalize_block.rs index 920a2297d4..3d68b17344 100644 --- a/crates/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/crates/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -1224,13 +1224,12 @@ mod test_finalize_block { shell.proposal_data.insert(proposal_id); let proposal = InitProposalData { - id: proposal_id, content: Hash::default(), author: validator.clone(), voting_start_epoch: Epoch::default(), voting_end_epoch: Epoch::default().next(), grace_epoch: Epoch::default().next(), - r#type: ProposalType::Default(None), + r#type: ProposalType::Default, }; namada::governance::init_proposal( diff --git a/crates/apps/src/lib/node/ledger/shell/governance.rs b/crates/apps/src/lib/node/ledger/shell/governance.rs index 5a3a619295..722dc78f94 100644 --- a/crates/apps/src/lib/node/ledger/shell/governance.rs +++ b/crates/apps/src/lib/node/ledger/shell/governance.rs @@ -11,10 +11,11 @@ use namada::governance::storage::proposal::{ AddRemove, PGFAction, PGFTarget, ProposalType, StoragePgfFunding, }; use namada::governance::utils::{ - compute_proposal_result, ProposalVotes, TallyResult, TallyType, TallyVote, - VotePower, + compute_proposal_result, ProposalVotes, TallyResult, TallyType, VotePower, +}; +use namada::governance::{ + storage as gov_api, ProposalVote, ADDRESS as gov_address, }; -use namada::governance::{storage as gov_api, ADDRESS as gov_address}; use namada::ibc; use namada::ledger::governance::utils::ProposalEvent; use namada::ledger::pos::BondId; @@ -94,7 +95,7 @@ where let transfer_address = match proposal_result.result { TallyResult::Passed => { let proposal_event = match proposal_type { - ProposalType::Default(_) => { + ProposalType::Default => { let proposal_code = gov_api::get_proposal_code(&shell.state, id)?; let result = execute_default_proposal( @@ -103,15 +104,31 @@ where proposal_code.clone(), )?; tracing::info!( - "Governance proposal (default {} wasm) {} has \ - been executed ({}) and passed.", - if proposal_code.is_some() { - "with" - } else { - "without" - }, + "Default Governance proposal {} has been executed \ + and passed.", id, - result + ); + + ProposalEvent::default_proposal_event( + id, + proposal_code.is_some(), + result, + ) + .into() + } + ProposalType::DefaultWithWasm(_) => { + let proposal_code = + gov_api::get_proposal_code(&shell.state, id)?; + let result = execute_default_proposal( + shell, + id, + proposal_code.clone(), + )?; + tracing::info!( + "DefaultWithWasm Governance proposal {} has been \ + executed and passed, wasm executiong was {}.", + id, + if result { "successful" } else { "unsuccessful" } ); ProposalEvent::default_proposal_event( @@ -235,10 +252,12 @@ where { let votes = gov_api::get_proposal_votes(storage, proposal_id)?; - let mut validators_vote: HashMap = HashMap::default(); + let mut validators_vote: HashMap = + HashMap::default(); let mut validator_voting_power: HashMap = HashMap::default(); - let mut delegators_vote: HashMap = HashMap::default(); + let mut delegators_vote: HashMap = + HashMap::default(); let mut delegator_voting_power: HashMap< Address, HashMap, @@ -253,7 +272,7 @@ where read_validator_stake(storage, params, &validator, epoch) .unwrap_or_default(); - validators_vote.insert(validator.clone(), vote_data.into()); + validators_vote.insert(validator.clone(), vote_data); validator_voting_power.insert(validator, validator_stake); } else { let validator = vote.validator.clone(); @@ -267,7 +286,7 @@ where let delegator_stake = bond_amount(storage, &bond_id, epoch); if let Ok(stake) = delegator_stake { - delegators_vote.insert(delegator.clone(), vote_data.into()); + delegators_vote.insert(delegator.clone(), vote_data); delegator_voting_power .entry(delegator) .or_default() diff --git a/crates/benches/native_vps.rs b/crates/benches/native_vps.rs index 17027328b4..ae6b91746a 100644 --- a/crates/benches/native_vps.rs +++ b/crates/benches/native_vps.rs @@ -120,10 +120,9 @@ fn governance(c: &mut Criterion) { shell.generate_tx( TX_INIT_PROPOSAL_WASM, InitProposalData { - id: 0, content: content_section.get_hash(), author: defaults::albert_address(), - r#type: ProposalType::Default(None), + r#type: ProposalType::Default, voting_start_epoch, voting_end_epoch: voting_start_epoch + 3_u64, grace_epoch: voting_start_epoch + 9_u64, @@ -172,12 +171,11 @@ fn governance(c: &mut Criterion) { shell.generate_tx( TX_INIT_PROPOSAL_WASM, InitProposalData { - id: 1, content: content_section.get_hash(), author: defaults::albert_address(), - r#type: ProposalType::Default(Some( + r#type: ProposalType::DefaultWithWasm( wasm_code_section.get_hash(), - )), + ), voting_start_epoch, voting_end_epoch: voting_start_epoch + 3_u64, grace_epoch: voting_start_epoch + 9_u64, diff --git a/crates/benches/txs.rs b/crates/benches/txs.rs index fbd12b5b38..4e1febc345 100644 --- a/crates/benches/txs.rs +++ b/crates/benches/txs.rs @@ -460,10 +460,9 @@ fn init_proposal(c: &mut Criterion) { shell.generate_tx( TX_INIT_PROPOSAL_WASM, InitProposalData { - id: 0, content: content_section.get_hash(), author: defaults::albert_address(), - r#type: ProposalType::Default(None), + r#type: ProposalType::Default, voting_start_epoch: 12.into(), voting_end_epoch: 15.into(), grace_epoch: 18.into(), @@ -510,12 +509,11 @@ fn init_proposal(c: &mut Criterion) { shell.generate_tx( TX_INIT_PROPOSAL_WASM, InitProposalData { - id: 1, content: content_section.get_hash(), author: defaults::albert_address(), - r#type: ProposalType::Default(Some( + r#type: ProposalType::DefaultWithWasm( wasm_code_section.get_hash(), - )), + ), voting_start_epoch: 12.into(), voting_end_epoch: 15.into(), grace_epoch: 18.into(), diff --git a/crates/governance/src/cli/mod.rs b/crates/governance/src/cli/mod.rs index 45b839d1f4..365f44d087 100644 --- a/crates/governance/src/cli/mod.rs +++ b/crates/governance/src/cli/mod.rs @@ -1,5 +1,3 @@ -/// CLi governance offline structures -pub mod offline; /// CLi governance on chain structures pub mod onchain; /// CLi governance validation diff --git a/crates/governance/src/cli/offline.rs b/crates/governance/src/cli/offline.rs deleted file mode 100644 index f508fef4ca..0000000000 --- a/crates/governance/src/cli/offline.rs +++ /dev/null @@ -1,409 +0,0 @@ -use std::collections::{BTreeMap, BTreeSet}; -use std::fs::{File, ReadDir}; -use std::path::PathBuf; - -use namada_core::account::AccountPublicKeysMap; -use namada_core::address::Address; -use namada_core::borsh::{BorshDeserialize, BorshSerialize, BorshSerializeExt}; -use namada_core::hash::Hash; -use namada_core::key::{common, RefTo, SigScheme}; -use namada_core::sign::SignatureIndex; -use namada_core::storage::Epoch; -use namada_macros::BorshDeserializer; -#[cfg(feature = "migrations")] -use namada_migrations::*; -use serde::{Deserialize, Serialize}; - -use super::validation::{is_valid_tally_epoch, ProposalValidation}; -use crate::storage::vote::ProposalVote; - -#[derive( - Debug, - Clone, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - 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, - force: bool, - ) -> Result { - if force { - return Ok(self); - } - 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, - BorshDeserializer, - 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, - force: bool, - ) -> Result { - if force { - return Ok(self); - } - 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, - BorshDeserializer, - 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.serialize_to_vec(); - let delegations_hash = self.delegations.serialize_to_vec(); - - 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() - } - - /// Check if the vote is nay - pub fn is_nay(&self) -> bool { - self.vote.is_nay() - } - - /// Check if the vote is abstain - pub fn is_abstain(&self) -> bool { - self.vote.is_abstain() - } - - /// compute the hash of a proposal - pub fn compute_hash(&self) -> Hash { - let proposal_hash_data = self.proposal_hash.serialize_to_vec(); - let proposal_vote_data = self.vote.serialize_to_vec(); - let delegations_hash = self.delegations.serialize_to_vec(); - 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 { - account_public_keys_map - .index_secret_keys(keys.to_vec()) - .values() - .map(|signing_key| { - let public_key = signing_key.ref_to(); - let signature = common::SigScheme::sign(signing_key, hashed_data); - SignatureIndex::from_single_signature(public_key, signature) - }) - .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| { - if account_public_keys_map - .get_index_from_public_key(&signature_index.pubkey) - .is_some() - { - let sig_check = common::SigScheme::verify_signature( - &signature_index.pubkey, - 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/crates/governance/src/cli/onchain.rs b/crates/governance/src/cli/onchain.rs index b56169e3f0..f198264adb 100644 --- a/crates/governance/src/cli/onchain.rs +++ b/crates/governance/src/cli/onchain.rs @@ -30,8 +30,6 @@ use crate::storage::proposal::PGFTarget; )] /// The proposal structure pub struct OnChainProposal { - /// The proposal id - pub id: u64, /// The proposal content pub content: BTreeMap, /// The proposal author address diff --git a/crates/governance/src/cli/validation.rs b/crates/governance/src/cli/validation.rs index 07efc82e93..a20bf87297 100644 --- a/crates/governance/src/cli/validation.rs +++ b/crates/governance/src/cli/validation.rs @@ -51,12 +51,6 @@ pub enum ProposalValidation { 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 \ @@ -187,20 +181,6 @@ pub fn is_valid_content( } } -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, diff --git a/crates/governance/src/lib.rs b/crates/governance/src/lib.rs index 49d6695a99..f163ffc295 100644 --- a/crates/governance/src/lib.rs +++ b/crates/governance/src/lib.rs @@ -6,6 +6,7 @@ use namada_core::address::{self, Address}; pub mod cli; /// governance parameters pub mod parameters; +/// governance public good fundings pub mod pgf; /// governance storage pub mod storage; diff --git a/crates/governance/src/storage/mod.rs b/crates/governance/src/storage/mod.rs index a6cc8bc787..b92d0bddf4 100644 --- a/crates/governance/src/storage/mod.rs +++ b/crates/governance/src/storage/mod.rs @@ -48,15 +48,14 @@ where 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))?; + ProposalType::DefaultWithWasm(_) => { + storage.write(&proposal_type_key, data.r#type.clone())?; let proposal_code_key = governance_keys::get_proposal_code_key(proposal_id); - let proposal_code = code - .clone() - .ok_or(Error::new_const("Missing proposal code"))?; - storage.write_bytes(&proposal_code_key, proposal_code)? + + let proposal_code = + code.ok_or(Error::new_const("Missing proposal code"))?; + storage.write_bytes(&proposal_code_key, proposal_code)?; } _ => storage.write(&proposal_type_key, data.r#type.clone())?, } @@ -72,14 +71,6 @@ where 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 = - governance_keys::get_proposal_code_key(proposal_id); - let proposal_code = - code.ok_or(Error::new_const("Missing proposal code"))?; - storage.write_bytes(&proposal_code_key, proposal_code)?; - } - storage.write(&counter_key, proposal_id + 1)?; let min_proposal_funds_key = governance_keys::get_min_proposal_fund_key(); diff --git a/crates/governance/src/storage/proposal.rs b/crates/governance/src/storage/proposal.rs index ade0da2448..0f54ccdb91 100644 --- a/crates/governance/src/storage/proposal.rs +++ b/crates/governance/src/storage/proposal.rs @@ -40,8 +40,6 @@ pub enum ProposalError { Deserialize, )] pub struct InitProposalData { - /// The proposal id - pub id: u64, /// The proposal content pub content: Hash, /// The proposal author address @@ -60,7 +58,7 @@ 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, + ProposalType::DefaultWithWasm(hash) => Some(hash), _ => None, } } @@ -93,10 +91,12 @@ impl TryFrom for InitProposalData { fn try_from(value: DefaultProposal) -> Result { Ok(InitProposalData { - id: value.proposal.id, content: Hash::default(), author: value.proposal.author, - r#type: ProposalType::Default(None), + r#type: match value.data { + Some(_) => ProposalType::DefaultWithWasm(Hash::default()), + None => ProposalType::Default, + }, voting_start_epoch: value.proposal.voting_start_epoch, voting_end_epoch: value.proposal.voting_end_epoch, grace_epoch: value.proposal.grace_epoch, @@ -112,7 +112,6 @@ impl TryFrom for InitProposalData { BTreeSet::>::try_from(value.data).unwrap(); Ok(InitProposalData { - id: value.proposal.id, content: Hash::default(), author: value.proposal.author, r#type: ProposalType::PGFSteward(extra_data), @@ -152,7 +151,6 @@ impl TryFrom for InitProposalData { continuous_fundings.extend(retro_fundings); Ok(InitProposalData { - id: value.proposal.id, content: Hash::default(), author: value.proposal.author, r#type: ProposalType::PGFPayment(continuous_fundings), /* here continuous_fundings also contains the retro funding */ @@ -202,8 +200,10 @@ impl StoragePgfFunding { Deserialize, )] pub enum ProposalType { - /// Default governance proposal with the optional wasm code - Default(Option), + /// Default governance proposal + Default, + /// Governance proposal with wasm code + DefaultWithWasm(Hash), /// PGF stewards proposal PGFSteward(BTreeSet>), /// PGF funding proposal @@ -427,13 +427,18 @@ pub enum PGFAction { impl ProposalType { /// Check if the proposal type is default pub fn is_default(&self) -> bool { - matches!(self, ProposalType::Default(_)) + matches!(self, ProposalType::Default) + } + + /// Check if the proposal type is default + pub fn is_default_with_wasm(&self) -> bool { + matches!(self, ProposalType::DefaultWithWasm(_)) } fn format_data(&self) -> String { match self { - ProposalType::Default(Some(hash)) => format!("Hash: {}", &hash), - ProposalType::Default(None) => "".to_string(), + ProposalType::DefaultWithWasm(hash) => format!("Hash: {}", &hash), + ProposalType::Default => "".to_string(), ProposalType::PGFSteward(addresses) => format!( "Addresses:{}", addresses @@ -455,7 +460,8 @@ impl ProposalType { impl Display for ProposalType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - ProposalType::Default(_) => write!(f, "Default"), + ProposalType::Default => write!(f, "Default"), + ProposalType::DefaultWithWasm(_) => write!(f, "DefaultWithWasm"), ProposalType::PGFSteward(_) => write!(f, "PGF steward"), ProposalType::PGFPayment(_) => write!(f, "PGF funding"), } @@ -642,7 +648,7 @@ pub mod testing { use namada_core::storage::testing::arb_epoch; use namada_core::token::testing::arb_amount; use proptest::prelude::*; - use proptest::{collection, option, prop_compose}; + use proptest::{collection, prop_compose}; use super::*; use crate::storage::vote::testing::arb_proposal_vote; @@ -726,7 +732,7 @@ pub mod testing { /// Generate an arbitrary proposal type pub fn arb_proposal_type() -> impl Strategy { prop_oneof![ - option::of(arb_hash()).prop_map(ProposalType::Default), + arb_hash().prop_map(ProposalType::DefaultWithWasm), collection::btree_set( arb_add_remove(arb_non_internal_address()), 0..10, @@ -740,7 +746,6 @@ pub mod testing { prop_compose! { /// Generate a proposal initialization pub fn arb_init_proposal()( - id: u64, content in arb_hash(), author in arb_non_internal_address(), r#type in arb_proposal_type(), @@ -749,7 +754,6 @@ pub mod testing { grace_epoch in arb_epoch(), ) -> InitProposalData { InitProposalData { - id, content, author, r#type, diff --git a/crates/governance/src/storage/vote.rs b/crates/governance/src/storage/vote.rs index 2f5d6aa443..01327a3960 100644 --- a/crates/governance/src/storage/vote.rs +++ b/crates/governance/src/storage/vote.rs @@ -42,6 +42,18 @@ impl ProposalVote { pub fn is_abstain(&self) -> bool { matches!(self, ProposalVote::Abstain) } + + /// Check if two votes are equal, returns an error if the variants of the + /// two instances are different + #[allow(clippy::match_like_matches_macro)] + pub fn is_same_side(&self, other: &ProposalVote) -> bool { + match (self, other) { + (ProposalVote::Yay, ProposalVote::Yay) => true, + (ProposalVote::Nay, ProposalVote::Nay) => true, + (ProposalVote::Abstain, ProposalVote::Abstain) => true, + _ => false, + } + } } impl Display for ProposalVote { diff --git a/crates/governance/src/utils.rs b/crates/governance/src/utils.rs index bd4b086d8c..f9f4a6cb4a 100644 --- a/crates/governance/src/utils.rs +++ b/crates/governance/src/utils.rs @@ -10,7 +10,6 @@ use namada_macros::BorshDeserializer; #[cfg(feature = "migrations")] use namada_migrations::*; -use super::cli::offline::OfflineVote; use super::storage::proposal::ProposalType; use super::storage::vote::ProposalVote; @@ -82,7 +81,8 @@ impl TallyType { /// Compute the type of tally for a proposal pub fn from(proposal_type: ProposalType, is_steward: bool) -> Self { match (proposal_type, is_steward) { - (ProposalType::Default(_), _) => TallyType::TwoThirds, + (ProposalType::Default, _) => TallyType::TwoThirds, + (ProposalType::DefaultWithWasm(_), _) => TallyType::TwoThirds, (ProposalType::PGFSteward(_), _) => TallyType::OneHalfOverOneThird, (ProposalType::PGFPayment(_), true) => { TallyType::LessOneHalfOverOneThirdNay @@ -243,77 +243,16 @@ impl Display for ProposalResult { } } -/// General representation of a vote -#[derive(Debug, Clone)] -pub enum TallyVote { - /// Represent a vote for a proposal onchain - OnChain(ProposalVote), - /// Represent a vote for a proposal offline - Offline(OfflineVote), -} - -impl From for TallyVote { - fn from(vote: ProposalVote) -> 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 a vote is nay - pub fn is_nay(&self) -> bool { - match self { - TallyVote::OnChain(vote) => vote.is_nay(), - TallyVote::Offline(vote) => vote.is_nay(), - } - } - - /// Check if a vote is abstain - pub fn is_abstain(&self) -> bool { - match self { - TallyVote::OnChain(vote) => vote.is_abstain(), - TallyVote::Offline(vote) => vote.is_abstain(), - } - } - - /// Check if two votes are equal, returns an error if the variants of the - /// two instances are different - pub fn is_same_side(&self, other: &TallyVote) -> bool { - match (self, other) { - (TallyVote::OnChain(vote), TallyVote::OnChain(other_vote)) => { - vote == other_vote - } - (TallyVote::Offline(vote), TallyVote::Offline(other_vote)) => { - vote.vote == other_vote.vote - } - _ => false, - } - } -} - /// Proposal structure holding votes information necessary to compute the /// outcome #[derive(Default, Debug, Clone)] pub struct ProposalVotes { /// Map from validator address to vote - pub validators_vote: HashMap, + 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, + pub delegators_vote: HashMap, /// Map from delegator address to the corresponding validator voting power pub delegator_voting_power: HashMap>, } @@ -324,7 +263,7 @@ impl ProposalVotes { &mut self, address: &Address, voting_power: VotePower, - vote: TallyVote, + vote: ProposalVote, ) { match self.validators_vote.insert(address.clone(), vote) { None => { @@ -345,7 +284,7 @@ impl ProposalVotes { address: &Address, validator_address: &Address, voting_power: VotePower, - vote: TallyVote, + vote: ProposalVote, ) { self.delegator_voting_power .entry(address.clone()) @@ -506,7 +445,7 @@ mod test { proposal_votes.add_validator( &validator_address, validator_voting_power, - ProposalVote::Yay.into(), + ProposalVote::Yay, ); for tally_type in [ @@ -545,7 +484,7 @@ mod test { proposal_votes.add_validator( &validator_address, validator_voting_power, - ProposalVote::Yay.into(), + ProposalVote::Yay, ); let delegator_address = address::testing::established_address_2(); @@ -554,7 +493,7 @@ mod test { &delegator_address, &validator_address, delegator_voting_power, - ProposalVote::Yay.into(), + ProposalVote::Yay, ); for tally_type in [ @@ -593,7 +532,7 @@ mod test { proposal_votes.add_validator( &validator_address, validator_voting_power, - ProposalVote::Yay.into(), + ProposalVote::Yay, ); let delegator_address = address::testing::established_address_2(); @@ -602,7 +541,7 @@ mod test { &delegator_address, &validator_address, delegator_voting_power, - ProposalVote::Nay.into(), + ProposalVote::Nay, ); for tally_type in [ @@ -644,7 +583,7 @@ mod test { proposal_votes.add_validator( &validator_address, validator_voting_power, - ProposalVote::Yay.into(), + ProposalVote::Yay, ); let delegator_address = address::testing::established_address_2(); @@ -653,7 +592,7 @@ mod test { &delegator_address, &validator_address, delegator_voting_power, - ProposalVote::Nay.into(), + ProposalVote::Nay, ); let delegator_address_two = address::testing::established_address_3(); @@ -662,7 +601,7 @@ mod test { &delegator_address_two, &validator_address, delegator_voting_power_two, - ProposalVote::Abstain.into(), + ProposalVote::Abstain, ); for tally_type in [ @@ -711,7 +650,7 @@ mod test { proposal_votes.add_validator( &validator_address, validator_voting_power, - ProposalVote::Yay.into(), + ProposalVote::Yay, ); let delegator_address = address::testing::established_address_2(); @@ -720,7 +659,7 @@ mod test { &delegator_address, &validator_address, delegator_voting_power, - ProposalVote::Nay.into(), + ProposalVote::Nay, ); let delegator_address_two = address::testing::established_address_3(); @@ -729,7 +668,7 @@ mod test { &delegator_address_two, &validator_address, delegator_voting_power_two, - ProposalVote::Abstain.into(), + ProposalVote::Abstain, ); for tally_type in [ @@ -778,7 +717,7 @@ mod test { proposal_votes.add_validator( &validator_address, validator_voting_power, - ProposalVote::Yay.into(), + ProposalVote::Yay, ); let delegator_address_two = address::testing::established_address_3(); @@ -787,7 +726,7 @@ mod test { &delegator_address_two, &validator_address, delegator_voting_power_two, - ProposalVote::Abstain.into(), + ProposalVote::Abstain, ); for tally_type in [ @@ -835,7 +774,7 @@ mod test { proposal_votes.add_validator( &validator_address, validator_voting_power, - ProposalVote::Yay.into(), + ProposalVote::Yay, ); let validator_address_two = address::testing::established_address_2(); @@ -843,7 +782,7 @@ mod test { proposal_votes.add_validator( &validator_address_two, validator_voting_power_two, - ProposalVote::Nay.into(), + ProposalVote::Nay, ); for tally_type in [ @@ -898,7 +837,7 @@ mod test { proposal_votes.add_validator( &validator_address, validator_voting_power, - ProposalVote::Yay.into(), + ProposalVote::Yay, ); let validator_address_two = address::testing::established_address_2(); @@ -906,7 +845,7 @@ mod test { proposal_votes.add_validator( &validator_address_two, validator_voting_power_two, - ProposalVote::Nay.into(), + ProposalVote::Nay, ); let delegator_address_two = address::testing::established_address_3(); @@ -915,7 +854,7 @@ mod test { &delegator_address_two, &validator_address_two, delegator_voting_power_two, - ProposalVote::Abstain.into(), + ProposalVote::Abstain, ); for tally_type in [ @@ -970,7 +909,7 @@ mod test { proposal_votes.add_validator( &validator_address, validator_voting_power, - ProposalVote::Yay.into(), + ProposalVote::Yay, ); let validator_address_two = address::testing::established_address_2(); @@ -978,7 +917,7 @@ mod test { proposal_votes.add_validator( &validator_address_two, validator_voting_power_two, - ProposalVote::Yay.into(), + ProposalVote::Yay, ); let delegator_address_two = address::testing::established_address_3(); @@ -987,7 +926,7 @@ mod test { &delegator_address_two, &validator_address_two, delegator_voting_power_two, - ProposalVote::Abstain.into(), + ProposalVote::Abstain, ); let proposal_result = compute_proposal_result( @@ -1029,7 +968,7 @@ mod test { proposal_votes.add_validator( &validator_address, validator_voting_power, - ProposalVote::Yay.into(), + ProposalVote::Yay, ); let validator_address_two = address::testing::established_address_2(); @@ -1037,7 +976,7 @@ mod test { proposal_votes.add_validator( &validator_address_two, validator_voting_power_two, - ProposalVote::Yay.into(), + ProposalVote::Yay, ); let delegator_address_two = address::testing::established_address_3(); @@ -1046,7 +985,7 @@ mod test { &delegator_address_two, &validator_address_two, delegator_voting_power_two, - ProposalVote::Abstain.into(), + ProposalVote::Abstain, ); let delegator_address = address::testing::established_address_4(); @@ -1055,7 +994,7 @@ mod test { &delegator_address, &validator_address, delegator_voting_power, - ProposalVote::Nay.into(), + ProposalVote::Nay, ); let proposal_result = compute_proposal_result( @@ -1101,7 +1040,7 @@ mod test { &delegator_address_two, &validator_address_two, delegator_voting_power_two, - ProposalVote::Abstain.into(), + ProposalVote::Abstain, ); let delegator_address = address::testing::established_address_4(); @@ -1110,7 +1049,7 @@ mod test { &delegator_address, &validator_address, delegator_voting_power, - ProposalVote::Nay.into(), + ProposalVote::Nay, ); let proposal_result = compute_proposal_result( @@ -1153,7 +1092,7 @@ mod test { &delegator_address_two, &validator_address_two, delegator_voting_power_two, - ProposalVote::Yay.into(), + ProposalVote::Yay, ); let delegator_address = address::testing::established_address_4(); @@ -1162,7 +1101,7 @@ mod test { &delegator_address, &validator_address, delegator_voting_power, - ProposalVote::Yay.into(), + ProposalVote::Yay, ); let proposal_result = compute_proposal_result( @@ -1207,7 +1146,7 @@ mod test { &delegator_address_two, &validator_address_two, delegator_voting_power_two, - ProposalVote::Yay.into(), + ProposalVote::Yay, ); let delegator_address = address::testing::established_address_4(); @@ -1216,7 +1155,7 @@ mod test { &delegator_address, &validator_address, delegator_voting_power, - ProposalVote::Yay.into(), + ProposalVote::Yay, ); let proposal_result = compute_proposal_result( @@ -1261,7 +1200,7 @@ mod test { &delegator_address_two, &validator_address_two, delegator_voting_power_two, - ProposalVote::Yay.into(), + ProposalVote::Yay, ); let delegator_address = address::testing::established_address_4(); @@ -1270,7 +1209,7 @@ mod test { &delegator_address, &validator_address, delegator_voting_power, - ProposalVote::Yay.into(), + ProposalVote::Yay, ); let proposal_result = compute_proposal_result( @@ -1315,7 +1254,7 @@ mod test { &delegator_address_two, &validator_address_two, delegator_voting_power_two, - ProposalVote::Nay.into(), + ProposalVote::Nay, ); let delegator_address = address::testing::established_address_4(); @@ -1324,7 +1263,7 @@ mod test { &delegator_address, &validator_address, delegator_voting_power, - ProposalVote::Nay.into(), + ProposalVote::Nay, ); let proposal_result = compute_proposal_result( @@ -1371,7 +1310,7 @@ mod test { &delegator_address_two, &validator_address_two, delegator_voting_power_two, - ProposalVote::Nay.into(), + ProposalVote::Nay, ); let delegator_address = address::testing::established_address_4(); @@ -1380,7 +1319,7 @@ mod test { &delegator_address, &validator_address, delegator_voting_power, - ProposalVote::Nay.into(), + ProposalVote::Nay, ); let proposal_result = compute_proposal_result( diff --git a/crates/light_sdk/src/transaction/governance.rs b/crates/light_sdk/src/transaction/governance.rs index a0ac5e96cc..3f44093fdf 100644 --- a/crates/light_sdk/src/transaction/governance.rs +++ b/crates/light_sdk/src/transaction/governance.rs @@ -20,7 +20,6 @@ impl InitProposal { /// Build a raw InitProposal transaction from the given parameters #[allow(clippy::too_many_arguments)] pub fn new( - id: u64, content: Hash, author: Address, r#type: ProposalType, @@ -30,7 +29,6 @@ impl InitProposal { args: GlobalArgs, ) -> Self { let init_proposal = namada_sdk::governance::InitProposalData { - id, content, author, r#type, diff --git a/crates/namada/src/ledger/governance/mod.rs b/crates/namada/src/ledger/governance/mod.rs index b549cfb201..7d1c6e67a5 100644 --- a/crates/namada/src/ledger/governance/mod.rs +++ b/crates/namada/src/ledger/governance/mod.rs @@ -121,10 +121,10 @@ where _ => Ok(false), }; match &result { - Err(err) => tracing::info!( + Err(err) => tracing::debug!( "Key {key_type:?} rejected with error: {err:#?}." ), - Ok(false) => tracing::info!("Key {key_type:?} rejected"), + Ok(false) => tracing::debug!("Key {key_type:?} rejected"), Ok(true) => {} } result.unwrap_or(false) @@ -424,7 +424,10 @@ where && are_continuous_fundings_unique && are_targets_unique) } - _ => Ok(true), // default proposal + // Default proposal condition are checked already for all other + // proposals. + // default_with_wasm proposal needs to check only for valid code + _ => Ok(true), } } @@ -434,7 +437,7 @@ where let proposal_type: ProposalType = self.force_read(&proposal_type_key, ReadType::Post)?; - if !proposal_type.is_default() { + if !proposal_type.is_default_with_wasm() { return Ok(false); } @@ -488,7 +491,7 @@ where let has_post_committing_epoch = self.ctx.has_key_post(&committing_epoch_key)?; if !has_post_committing_epoch { - tracing::info!("Committing proposal key is missing present"); + return Ok(false); } let is_valid_grace_epoch = end_epoch < grace_epoch @@ -851,3 +854,1812 @@ impl KeyType { } } } + +#[cfg(test)] +mod test { + use std::cell::RefCell; + use std::collections::BTreeSet; + + use borsh_ext::BorshSerializeExt; + use namada_gas::{TxGasMeter, VpGasMeter}; + use namada_governance::storage::keys::{ + get_author_key, get_committing_proposals_key, get_content_key, + get_counter_key, get_funds_key, get_grace_epoch_key, + get_proposal_type_key, get_vote_proposal_key, get_voting_end_epoch_key, + get_voting_start_epoch_key, + }; + use namada_governance::{ProposalType, ProposalVote, ADDRESS}; + use namada_proof_of_stake::bond_tokens; + use namada_sdk::address::testing::{ + established_address_1, established_address_3, nam, + }; + use namada_sdk::key::testing::keypair_1; + use namada_sdk::key::RefTo; + use namada_sdk::time::DateTimeUtc; + use namada_sdk::token; + use namada_sdk::validity_predicate::VpSentinel; + use namada_state::mockdb::MockDB; + use namada_state::testing::TestState; + use namada_state::{ + BlockHash, BlockHeight, Epoch, FullAccessState, Key, Sha256Hasher, + State, StorageRead, TxIndex, + }; + use namada_token::storage_key::balance_key; + use namada_tx::data::TxType; + use namada_tx::{Code, Data, Section, Signature, Tx}; + + use crate::core::address::Address; + use crate::ledger::governance::GovernanceVp; + use crate::ledger::native_vp::ibc::{ + get_dummy_genesis_validator, get_dummy_header, + }; + use crate::ledger::native_vp::{Ctx, NativeVp}; + use crate::ledger::pos; + use crate::vm::wasm; + + const TX_GAS_LIMIT: u64 = 1_000_000; + + fn init_storage() -> TestState { + let mut state = TestState::default(); + + pos::test_utils::test_init_genesis( + &mut state, + namada_proof_of_stake::OwnedPosParams::default(), + vec![get_dummy_genesis_validator()].into_iter(), + Epoch(1), + ) + .unwrap(); + + state + .in_mem_mut() + .set_header(get_dummy_header()) + .expect("Setting a dummy header shouldn't fail"); + state + .in_mem_mut() + .begin_block(BlockHash::default(), BlockHeight(1)) + .unwrap(); + + state + } + + #[test] + fn test_noop() { + let state = init_storage(); + let keys_changed = BTreeSet::new(); + + let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + )); + let (vp_wasm_cache, _vp_cache_dir) = + wasm::compilation_cache::common::testing::cache(); + + let tx_index = TxIndex::default(); + let sentinel = RefCell::new(VpSentinel::default()); + + let signer = keypair_1(); + let signer_address = Address::from(&signer.clone().ref_to()); + let verifiers = BTreeSet::from([signer_address]); + + let tx_code = vec![]; + let tx_data = vec![]; + + let mut tx = Tx::from_type(TxType::Raw); + tx.header.chain_id = state.in_mem().chain_id.clone(); + tx.set_code(Code::new(tx_code, None)); + tx.set_data(Data::new(tx_data)); + tx.add_section(Section::Signature(Signature::new( + vec![tx.header_hash()], + [(0, keypair_1())].into_iter().collect(), + None, + ))); + + let ctx = Ctx::new( + &ADDRESS, + &state, + &tx, + &tx_index, + &gas_meter, + &sentinel, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); + + let governance_vp = GovernanceVp { ctx }; + // this should return true because state has been stored + assert!( + governance_vp + .validate_tx(&tx, &keys_changed, &verifiers) + .expect("validation failed") + ); + } + + fn initialize_account_balance( + state: &mut S, + address: &Address, + amount: token::Amount, + ) where + S: State, + { + let balance_key = balance_key(&nam(), address); + state + .write_log_mut() + .write(&balance_key, amount.serialize_to_vec()) + .expect("write failed"); + state.write_log_mut().commit_tx(); + } + + fn update_epoch_to( + state: &mut FullAccessState, + total_epochs: u64, + height: BlockHeight, + ) { + state.in_mem_mut().update_epoch_blocks_delay = Some(1); + for _ in 0..total_epochs { + state.in_mem_mut().update_epoch_blocks_delay = Some(1); + state + .update_epoch( + height, + DateTimeUtc::now() + .next_second() + .next_second() + .next_second() + .next_second() + .next_second(), + ) + .unwrap(); + } + } + + fn get_proposal_keys(proposal_id: u64, grace_epoch: u64) -> BTreeSet { + let counter_key = get_counter_key(); + let voting_end_epoch_key = get_voting_end_epoch_key(proposal_id); + let voting_start_epoch_key = get_voting_start_epoch_key(proposal_id); + let grace_epoch_key = get_grace_epoch_key(proposal_id); + let content_key = get_content_key(proposal_id); + let author_key = get_author_key(proposal_id); + let proposal_type_key = get_proposal_type_key(proposal_id); + let funds_key = get_funds_key(proposal_id); + let commiting_key = + get_committing_proposals_key(proposal_id, grace_epoch); + + BTreeSet::from([ + counter_key.clone(), + funds_key.clone(), + content_key.clone(), + author_key.clone(), + proposal_type_key.clone(), + voting_start_epoch_key.clone(), + voting_end_epoch_key.clone(), + grace_epoch_key.clone(), + commiting_key.clone(), + ]) + } + + fn transfer( + state: &mut S, + source: &Address, + target: &Address, + amount: u64, + ) where + S: State, + { + let source_balance_key = balance_key(&nam(), source); + let target_balance_key = balance_key(&nam(), target); + let amount = token::Amount::native_whole(amount); + + let mut current_source: token::Amount = + state.read(&source_balance_key).unwrap().unwrap(); + let mut current_target: token::Amount = + state.read(&target_balance_key).unwrap().unwrap(); + + current_source.spend(&amount).unwrap(); + current_target.receive(&amount).unwrap(); + + state + .write_log_mut() + .write(&source_balance_key, current_source.serialize_to_vec()) + .expect("write failed"); + + state + .write_log_mut() + .write(&target_balance_key, current_target.serialize_to_vec()) + .expect("write failed"); + } + + #[allow(clippy::too_many_arguments)] + fn init_proposal( + state: &mut S, + proposal_id: u64, + funds: u64, + start_epoch: u64, + end_epoch: u64, + grace_epoch: u64, + signer_address: &Address, + no_commiting_key: bool, + ) where + S: State, + { + let counter_key = get_counter_key(); + let voting_end_epoch_key = get_voting_end_epoch_key(proposal_id); + let voting_start_epoch_key = get_voting_start_epoch_key(proposal_id); + let grace_epoch_key = get_grace_epoch_key(proposal_id); + let content_key = get_content_key(proposal_id); + let author_key = get_author_key(proposal_id); + let proposal_type_key = get_proposal_type_key(proposal_id); + let funds_key = get_funds_key(proposal_id); + let commiting_key = + get_committing_proposals_key(proposal_id, grace_epoch); + // let governance_balance_key = balance_key(&nam(), &ADDRESS); + // let author_balance_key = balance_key(&nam(), signer_address); + + transfer(state, signer_address, &ADDRESS, funds); + + state + .write_log_mut() + .write(&counter_key, (proposal_id + 1).serialize_to_vec()) + .unwrap(); + state + .write_log_mut() + .write(&voting_end_epoch_key, Epoch(end_epoch).serialize_to_vec()) + .unwrap(); + state + .write_log_mut() + .write( + &voting_start_epoch_key, + Epoch(start_epoch).serialize_to_vec(), + ) + .unwrap(); + state + .write_log_mut() + .write(&grace_epoch_key, Epoch(grace_epoch).serialize_to_vec()) + .unwrap(); + state + .write_log_mut() + .write(&content_key, vec![1, 2, 3, 4]) + .unwrap(); + state + .write_log_mut() + .write(&author_key, signer_address.serialize_to_vec()) + .unwrap(); + state + .write_log_mut() + .write(&proposal_type_key, ProposalType::Default.serialize_to_vec()) + .unwrap(); + state + .write_log_mut() + .write( + &funds_key, + token::Amount::native_whole(funds).serialize_to_vec(), + ) + .unwrap(); + if !no_commiting_key { + state + .write_log_mut() + .write(&commiting_key, ().serialize_to_vec()) + .unwrap(); + } + } + + #[test] + fn test_goverance_proposal_accepted() { + let mut state = init_storage(); + + let proposal_id = 0; + let grace_epoch = 19; + + let keys_changed = get_proposal_keys(proposal_id, grace_epoch); + + let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + )); + let (vp_wasm_cache, _vp_cache_dir) = + wasm::compilation_cache::common::testing::cache(); + + let tx_index = TxIndex::default(); + let sentinel = RefCell::new(VpSentinel::default()); + + let signer = keypair_1(); + let signer_address = Address::from(&signer.clone().ref_to()); + let verifiers = BTreeSet::from([signer_address.clone()]); + + initialize_account_balance( + &mut state, + &signer_address.clone(), + token::Amount::native_whole(510), + ); + initialize_account_balance( + &mut state, + &ADDRESS, + token::Amount::native_whole(0), + ); + state.commit_block().unwrap(); + + let tx_code = vec![]; + let tx_data = vec![]; + + let mut tx = Tx::from_type(TxType::Raw); + tx.header.chain_id = state.in_mem().chain_id.clone(); + tx.set_code(Code::new(tx_code, None)); + tx.set_data(Data::new(tx_data)); + tx.add_section(Section::Signature(Signature::new( + vec![tx.header_hash()], + [(0, keypair_1())].into_iter().collect(), + None, + ))); + + init_proposal( + &mut state, + proposal_id, + 500, + 3, + 9, + 19, + &signer_address, + false, + ); + + let ctx = Ctx::new( + &ADDRESS, + &state, + &tx, + &tx_index, + &gas_meter, + &sentinel, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); + + let governance_vp = GovernanceVp { ctx }; + // this should return true because state has been stored + assert!( + governance_vp + .validate_tx(&tx, &keys_changed, &verifiers) + .expect("validation failed") + ); + + state.write_log_mut().commit_tx(); + state.commit_block().unwrap(); + + let governance_balance_key = balance_key(&nam(), &ADDRESS); + let amount: token::Amount = + state.read(&governance_balance_key).unwrap().unwrap(); + assert_eq!(amount, token::Amount::native_whole(500)); + + let author_balance_key = balance_key(&nam(), &signer_address); + let amount: token::Amount = + state.read(&author_balance_key).unwrap().unwrap(); + assert_eq!(amount, token::Amount::native_whole(10)); + + let governance_counter_key = get_counter_key(); + let counter: u64 = + state.read(&governance_counter_key).unwrap().unwrap(); + assert_eq!(counter, 1); + } + + #[test] + fn test_governance_proposal_not_enough_funds_failed() { + let mut state = init_storage(); + + let proposal_id = 0; + let grace_epoch = 19; + + let keys_changed = get_proposal_keys(proposal_id, grace_epoch); + + let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + )); + let (vp_wasm_cache, _vp_cache_dir) = + wasm::compilation_cache::common::testing::cache(); + + let tx_index = TxIndex::default(); + let sentinel = RefCell::new(VpSentinel::default()); + + let signer = keypair_1(); + let signer_address = Address::from(&signer.clone().ref_to()); + let verifiers = BTreeSet::from([signer_address.clone()]); + + initialize_account_balance( + &mut state, + &signer_address.clone(), + token::Amount::native_whole(500), + ); + initialize_account_balance( + &mut state, + &ADDRESS, + token::Amount::native_whole(0), + ); + state.commit_block().unwrap(); + + let tx_code = vec![]; + let tx_data = vec![]; + + let mut tx = Tx::from_type(TxType::Raw); + tx.header.chain_id = state.in_mem().chain_id.clone(); + tx.set_code(Code::new(tx_code, None)); + tx.set_data(Data::new(tx_data)); + tx.add_section(Section::Signature(Signature::new( + vec![tx.header_hash()], + [(0, keypair_1())].into_iter().collect(), + None, + ))); + + init_proposal( + &mut state, + proposal_id, + 499, + 3, + 9, + 19, + &signer_address, + false, + ); + + let ctx = Ctx::new( + &ADDRESS, + &state, + &tx, + &tx_index, + &gas_meter, + &sentinel, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); + + let governance_vp = GovernanceVp { ctx }; + // this should return true because state has been stored + let vp_result = governance_vp + .validate_tx(&tx, &keys_changed, &verifiers) + .expect("validation failed"); + assert!(!vp_result); + + if !vp_result { + state.write_log_mut().drop_tx(); + } else { + state.write_log_mut().commit_tx(); + } + state.commit_block().unwrap(); + + let governance_balance_key = balance_key(&nam(), &ADDRESS); + let amount: token::Amount = + state.read(&governance_balance_key).unwrap().unwrap(); + assert_eq!(amount, token::Amount::native_whole(0)); + + let author_balance_key = balance_key(&nam(), &signer_address); + let amount: token::Amount = + state.read(&author_balance_key).unwrap().unwrap(); + assert_eq!(amount, token::Amount::native_whole(500)); + + let governance_counter_key = get_counter_key(); + let counter: u64 = + state.read(&governance_counter_key).unwrap().unwrap(); + assert_eq!(counter, 0); + } + + #[test] + fn test_governance_proposal_more_funds_accepted() { + let mut state = init_storage(); + + let proposal_id = 0; + let grace_epoch = 19; + + let keys_changed = get_proposal_keys(proposal_id, grace_epoch); + + let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + )); + let (vp_wasm_cache, _vp_cache_dir) = + wasm::compilation_cache::common::testing::cache(); + + let tx_index = TxIndex::default(); + let sentinel = RefCell::new(VpSentinel::default()); + + let signer = keypair_1(); + let signer_address = Address::from(&signer.clone().ref_to()); + let verifiers = BTreeSet::from([signer_address.clone()]); + + initialize_account_balance( + &mut state, + &signer_address.clone(), + token::Amount::native_whole(510), + ); + initialize_account_balance( + &mut state, + &ADDRESS, + token::Amount::native_whole(0), + ); + state.commit_block().unwrap(); + + let tx_code = vec![]; + let tx_data = vec![]; + + let mut tx = Tx::from_type(TxType::Raw); + tx.header.chain_id = state.in_mem().chain_id.clone(); + tx.set_code(Code::new(tx_code, None)); + tx.set_data(Data::new(tx_data)); + tx.add_section(Section::Signature(Signature::new( + vec![tx.header_hash()], + [(0, keypair_1())].into_iter().collect(), + None, + ))); + + init_proposal( + &mut state, + proposal_id, + 509, + 3, + 9, + 19, + &signer_address, + false, + ); + + let ctx = Ctx::new( + &ADDRESS, + &state, + &tx, + &tx_index, + &gas_meter, + &sentinel, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); + + let governance_vp = GovernanceVp { ctx }; + let vp_result = governance_vp + .validate_tx(&tx, &keys_changed, &verifiers) + .expect("validation failed"); + assert!(vp_result); + + if !vp_result { + state.write_log_mut().drop_tx(); + } else { + state.write_log_mut().commit_tx(); + } + state.commit_block().unwrap(); + + let governance_balance_key = balance_key(&nam(), &ADDRESS); + let amount: token::Amount = + state.read(&governance_balance_key).unwrap().unwrap(); + assert_eq!(amount, token::Amount::native_whole(509)); + + let author_balance_key = balance_key(&nam(), &signer_address); + let amount: token::Amount = + state.read(&author_balance_key).unwrap().unwrap(); + assert_eq!(amount, token::Amount::native_whole(1)); + + let governance_counter_key = get_counter_key(); + let counter: u64 = + state.read(&governance_counter_key).unwrap().unwrap(); + assert_eq!(counter, 1); + } + + #[test] + fn test_governance_too_small_voting_period_failed() { + let mut state = init_storage(); + + let proposal_id = 0; + let grace_epoch = 19; + + let keys_changed = get_proposal_keys(proposal_id, grace_epoch); + + let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + )); + let (vp_wasm_cache, _vp_cache_dir) = + wasm::compilation_cache::common::testing::cache(); + + let tx_index = TxIndex::default(); + let sentinel = RefCell::new(VpSentinel::default()); + + let signer = keypair_1(); + let signer_address = Address::from(&signer.clone().ref_to()); + let verifiers = BTreeSet::from([signer_address.clone()]); + + initialize_account_balance( + &mut state, + &signer_address.clone(), + token::Amount::native_whole(510), + ); + initialize_account_balance( + &mut state, + &ADDRESS, + token::Amount::native_whole(0), + ); + state.commit_block().unwrap(); + + let tx_code = vec![]; + let tx_data = vec![]; + + let mut tx = Tx::from_type(TxType::Raw); + tx.header.chain_id = state.in_mem().chain_id.clone(); + tx.set_code(Code::new(tx_code, None)); + tx.set_data(Data::new(tx_data)); + tx.add_section(Section::Signature(Signature::new( + vec![tx.header_hash()], + [(0, keypair_1())].into_iter().collect(), + None, + ))); + + init_proposal( + &mut state, + proposal_id, + 509, + 3, + 8, + 19, + &signer_address, + false, + ); + + let ctx = Ctx::new( + &ADDRESS, + &state, + &tx, + &tx_index, + &gas_meter, + &sentinel, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); + + let governance_vp = GovernanceVp { ctx }; + // this should return true because state has been stored + assert!( + !governance_vp + .validate_tx(&tx, &keys_changed, &verifiers) + .expect("validation failed") + ); + } + + #[test] + fn test_governance_too_small_grace_period_failed() { + let mut state = init_storage(); + + let proposal_id = 0; + let grace_epoch = 12; + + let keys_changed = get_proposal_keys(proposal_id, grace_epoch); + + let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + )); + let (vp_wasm_cache, _vp_cache_dir) = + wasm::compilation_cache::common::testing::cache(); + + let tx_index = TxIndex::default(); + let sentinel = RefCell::new(VpSentinel::default()); + + let signer = keypair_1(); + let signer_address = Address::from(&signer.clone().ref_to()); + let verifiers = BTreeSet::from([signer_address.clone()]); + + initialize_account_balance( + &mut state, + &signer_address.clone(), + token::Amount::native_whole(510), + ); + initialize_account_balance( + &mut state, + &ADDRESS, + token::Amount::native_whole(0), + ); + state.commit_block().unwrap(); + + let tx_code = vec![]; + let tx_data = vec![]; + + let mut tx = Tx::from_type(TxType::Raw); + tx.header.chain_id = state.in_mem().chain_id.clone(); + tx.set_code(Code::new(tx_code, None)); + tx.set_data(Data::new(tx_data)); + tx.add_section(Section::Signature(Signature::new( + vec![tx.header_hash()], + [(0, keypair_1())].into_iter().collect(), + None, + ))); + + init_proposal( + &mut state, + proposal_id, + 509, + 3, + 9, + 12, + &signer_address, + false, + ); + + let ctx = Ctx::new( + &ADDRESS, + &state, + &tx, + &tx_index, + &gas_meter, + &sentinel, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); + + let governance_vp = GovernanceVp { ctx }; + // this should return true because state has been stored + assert!( + !governance_vp + .validate_tx(&tx, &keys_changed, &verifiers) + .expect("validation failed") + ); + } + + #[test] + fn test_governance_too_big_voting_window_failed() { + let mut state = init_storage(); + + let proposal_id = 0; + let grace_epoch = 40; + + let keys_changed = get_proposal_keys(proposal_id, grace_epoch); + + let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + )); + let (vp_wasm_cache, _vp_cache_dir) = + wasm::compilation_cache::common::testing::cache(); + + let tx_index = TxIndex::default(); + let sentinel = RefCell::new(VpSentinel::default()); + + let signer = keypair_1(); + let signer_address = Address::from(&signer.clone().ref_to()); + let verifiers = BTreeSet::from([signer_address.clone()]); + + initialize_account_balance( + &mut state, + &signer_address.clone(), + token::Amount::native_whole(510), + ); + initialize_account_balance( + &mut state, + &ADDRESS, + token::Amount::native_whole(0), + ); + state.commit_block().unwrap(); + + let tx_code = vec![]; + let tx_data = vec![]; + + let mut tx = Tx::from_type(TxType::Raw); + tx.header.chain_id = state.in_mem().chain_id.clone(); + tx.set_code(Code::new(tx_code, None)); + tx.set_data(Data::new(tx_data)); + tx.add_section(Section::Signature(Signature::new( + vec![tx.header_hash()], + [(0, keypair_1())].into_iter().collect(), + None, + ))); + + init_proposal( + &mut state, + proposal_id, + 509, + 3, + 9, + 40, + &signer_address, + false, + ); + + let ctx = Ctx::new( + &ADDRESS, + &state, + &tx, + &tx_index, + &gas_meter, + &sentinel, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); + + let governance_vp = GovernanceVp { ctx }; + // this should return true because state has been stored + assert!( + !governance_vp + .validate_tx(&tx, &keys_changed, &verifiers) + .expect("validation failed") + ); + } + + #[test] + fn test_governance_no_committing_key_failed() { + let mut state = init_storage(); + + let proposal_id = 0; + let grace_epoch = 19; + + let counter_key = get_counter_key(); + let voting_end_epoch_key = get_voting_end_epoch_key(proposal_id); + let voting_start_epoch_key = get_voting_start_epoch_key(proposal_id); + let grace_epoch_key = get_grace_epoch_key(proposal_id); + let content_key = get_content_key(proposal_id); + let author_key = get_author_key(proposal_id); + let proposal_type_key = get_proposal_type_key(proposal_id); + let funds_key = get_funds_key(proposal_id); + + let keys_changed = BTreeSet::from([ + counter_key.clone(), + funds_key.clone(), + content_key.clone(), + author_key.clone(), + proposal_type_key.clone(), + voting_start_epoch_key.clone(), + voting_end_epoch_key.clone(), + grace_epoch_key.clone(), + ]); + + let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + )); + let (vp_wasm_cache, _vp_cache_dir) = + wasm::compilation_cache::common::testing::cache(); + + let tx_index = TxIndex::default(); + let sentinel = RefCell::new(VpSentinel::default()); + + let signer = keypair_1(); + let signer_address = Address::from(&signer.clone().ref_to()); + let verifiers = BTreeSet::from([signer_address.clone()]); + + initialize_account_balance( + &mut state, + &signer_address.clone(), + token::Amount::native_whole(510), + ); + initialize_account_balance( + &mut state, + &ADDRESS, + token::Amount::native_whole(0), + ); + state.commit_block().unwrap(); + + let tx_code = vec![]; + let tx_data = vec![]; + + let mut tx = Tx::from_type(TxType::Raw); + tx.header.chain_id = state.in_mem().chain_id.clone(); + tx.set_code(Code::new(tx_code, None)); + tx.set_data(Data::new(tx_data)); + tx.add_section(Section::Signature(Signature::new( + vec![tx.header_hash()], + [(0, keypair_1())].into_iter().collect(), + None, + ))); + + init_proposal( + &mut state, + proposal_id, + 509, + 3, + 9, + grace_epoch, + &signer_address, + true, + ); + + let ctx = Ctx::new( + &ADDRESS, + &state, + &tx, + &tx_index, + &gas_meter, + &sentinel, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); + + let governance_vp = GovernanceVp { ctx }; + // this should return true because state has been stored + assert!( + !governance_vp + .validate_tx(&tx, &keys_changed, &verifiers) + .expect("validation failed") + ); + } + + #[test] + fn test_governance_invalid_start_epoch_failed() { + let mut state = init_storage(); + + let proposal_id = 0; + let grace_epoch = 19; + + let counter_key = get_counter_key(); + let voting_end_epoch_key = get_voting_end_epoch_key(proposal_id); + let voting_start_epoch_key = get_voting_start_epoch_key(proposal_id); + let grace_epoch_key = get_grace_epoch_key(proposal_id); + let content_key = get_content_key(proposal_id); + let author_key = get_author_key(proposal_id); + let proposal_type_key = get_proposal_type_key(proposal_id); + let funds_key = get_funds_key(proposal_id); + + let keys_changed = BTreeSet::from([ + counter_key.clone(), + funds_key.clone(), + content_key.clone(), + author_key.clone(), + proposal_type_key.clone(), + voting_start_epoch_key.clone(), + voting_end_epoch_key.clone(), + grace_epoch_key.clone(), + ]); + + let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + )); + let (vp_wasm_cache, _vp_cache_dir) = + wasm::compilation_cache::common::testing::cache(); + + let tx_index = TxIndex::default(); + let sentinel = RefCell::new(VpSentinel::default()); + + let signer = keypair_1(); + let signer_address = Address::from(&signer.clone().ref_to()); + let verifiers = BTreeSet::from([signer_address.clone()]); + + initialize_account_balance( + &mut state, + &signer_address.clone(), + token::Amount::native_whole(510), + ); + initialize_account_balance( + &mut state, + &ADDRESS, + token::Amount::native_whole(0), + ); + state.commit_block().unwrap(); + + let tx_code = vec![]; + let tx_data = vec![]; + + let mut tx = Tx::from_type(TxType::Raw); + tx.header.chain_id = state.in_mem().chain_id.clone(); + tx.set_code(Code::new(tx_code, None)); + tx.set_data(Data::new(tx_data)); + tx.add_section(Section::Signature(Signature::new( + vec![tx.header_hash()], + [(0, keypair_1())].into_iter().collect(), + None, + ))); + + init_proposal( + &mut state, + proposal_id, + 500, + 0, + 9, + grace_epoch, + &signer_address, + false, + ); + + let ctx = Ctx::new( + &ADDRESS, + &state, + &tx, + &tx_index, + &gas_meter, + &sentinel, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); + + let governance_vp = GovernanceVp { ctx }; + // this should return true because state has been stored + assert!( + !governance_vp + .validate_tx(&tx, &keys_changed, &verifiers) + .expect("validation failed") + ); + } + + #[test] + fn test_goverance_vote_validator_success() { + let mut state = init_storage(); + + let proposal_id = 0; + let grace_epoch = 19; + + let mut keys_changed = get_proposal_keys(proposal_id, grace_epoch); + + let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + )); + let (vp_wasm_cache, _vp_cache_dir) = + wasm::compilation_cache::common::testing::cache(); + + let tx_index = TxIndex::default(); + let sentinel = RefCell::new(VpSentinel::default()); + + let signer = keypair_1(); + let signer_address = Address::from(&signer.clone().ref_to()); + let mut verifiers = BTreeSet::from([signer_address.clone()]); + + initialize_account_balance( + &mut state, + &signer_address.clone(), + token::Amount::native_whole(510), + ); + initialize_account_balance( + &mut state, + &ADDRESS, + token::Amount::native_whole(0), + ); + state.commit_block().unwrap(); + + let tx_code = vec![]; + let tx_data = vec![]; + + let mut tx = Tx::from_type(TxType::Raw); + tx.header.chain_id = state.in_mem().chain_id.clone(); + tx.set_code(Code::new(tx_code, None)); + tx.set_data(Data::new(tx_data)); + tx.add_section(Section::Signature(Signature::new( + vec![tx.header_hash()], + [(0, keypair_1())].into_iter().collect(), + None, + ))); + + init_proposal( + &mut state, + proposal_id, + 500, + 3, + 9, + 19, + &signer_address, + false, + ); + + let ctx = Ctx::new( + &ADDRESS, + &state, + &tx, + &tx_index, + &gas_meter, + &sentinel, + &keys_changed, + &verifiers, + vp_wasm_cache.clone(), + ); + + let governance_vp = GovernanceVp { ctx }; + // this should return true because state has been stored + assert!( + governance_vp + .validate_tx(&tx, &keys_changed, &verifiers) + .expect("validation failed") + ); + + state.write_log_mut().commit_tx(); + state.commit_block().unwrap(); + + let height = state.in_mem().get_block_height().0 + (7 * 2); + + update_epoch_to(&mut state, 7, height); + + let validator_address = established_address_1(); + + let vote_key = get_vote_proposal_key( + 0, + validator_address.clone(), + validator_address.clone(), + ); + state + .write_log_mut() + .write(&vote_key, ProposalVote::Yay.serialize_to_vec()) + .unwrap(); + + keys_changed.clear(); + keys_changed.insert(vote_key); + + verifiers.clear(); + verifiers.insert(validator_address); + + let ctx = Ctx::new( + &ADDRESS, + &state, + &tx, + &tx_index, + &gas_meter, + &sentinel, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); + + let governance_vp = GovernanceVp { ctx }; + + assert!( + governance_vp + .validate_tx(&tx, &keys_changed, &verifiers) + .expect("validation failed") + ); + } + + #[test] + fn test_goverance_vote_validator_out_of_voting_window_fail() { + let mut state = init_storage(); + + let proposal_id = 0; + let grace_epoch = 19; + + let mut keys_changed = get_proposal_keys(proposal_id, grace_epoch); + + let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + )); + let (vp_wasm_cache, _vp_cache_dir) = + wasm::compilation_cache::common::testing::cache(); + + let tx_index = TxIndex::default(); + let sentinel = RefCell::new(VpSentinel::default()); + + let signer = keypair_1(); + let signer_address = Address::from(&signer.clone().ref_to()); + let mut verifiers = BTreeSet::from([signer_address.clone()]); + + initialize_account_balance( + &mut state, + &signer_address.clone(), + token::Amount::native_whole(510), + ); + initialize_account_balance( + &mut state, + &ADDRESS, + token::Amount::native_whole(0), + ); + state.commit_block().unwrap(); + + let tx_code = vec![]; + let tx_data = vec![]; + + let mut tx = Tx::from_type(TxType::Raw); + tx.header.chain_id = state.in_mem().chain_id.clone(); + tx.set_code(Code::new(tx_code, None)); + tx.set_data(Data::new(tx_data)); + tx.add_section(Section::Signature(Signature::new( + vec![tx.header_hash()], + [(0, keypair_1())].into_iter().collect(), + None, + ))); + + init_proposal( + &mut state, + proposal_id, + 500, + 3, + 9, + 19, + &signer_address, + false, + ); + + let ctx = Ctx::new( + &ADDRESS, + &state, + &tx, + &tx_index, + &gas_meter, + &sentinel, + &keys_changed, + &verifiers, + vp_wasm_cache.clone(), + ); + + let governance_vp = GovernanceVp { ctx }; + // this should return true because state has been stored + assert!( + governance_vp + .validate_tx(&tx, &keys_changed, &verifiers) + .expect("validation failed") + ); + + state.write_log_mut().commit_tx(); + state.commit_block().unwrap(); + + let height = state.in_mem().get_block_height().0 + (7 * 2); + + update_epoch_to(&mut state, 10, height); + + let validator_address = established_address_1(); + + let vote_key = get_vote_proposal_key( + 0, + validator_address.clone(), + validator_address.clone(), + ); + state + .write_log_mut() + .write(&vote_key, ProposalVote::Yay.serialize_to_vec()) + .unwrap(); + + keys_changed.clear(); + keys_changed.insert(vote_key); + + verifiers.clear(); + verifiers.insert(validator_address); + + let ctx = Ctx::new( + &ADDRESS, + &state, + &tx, + &tx_index, + &gas_meter, + &sentinel, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); + + let governance_vp = GovernanceVp { ctx }; + + assert!( + !governance_vp + .validate_tx(&tx, &keys_changed, &verifiers) + .expect("validation failed") + ); + } + + #[test] + fn test_goverance_vote_validator_fail() { + let mut state = init_storage(); + + let proposal_id = 0; + let grace_epoch = 19; + + let mut keys_changed = get_proposal_keys(proposal_id, grace_epoch); + + let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + )); + let (vp_wasm_cache, _vp_cache_dir) = + wasm::compilation_cache::common::testing::cache(); + + let tx_index = TxIndex::default(); + let sentinel = RefCell::new(VpSentinel::default()); + + let signer = keypair_1(); + let signer_address = Address::from(&signer.clone().ref_to()); + let mut verifiers = BTreeSet::from([signer_address.clone()]); + + initialize_account_balance( + &mut state, + &signer_address.clone(), + token::Amount::native_whole(510), + ); + initialize_account_balance( + &mut state, + &ADDRESS, + token::Amount::native_whole(0), + ); + state.commit_block().unwrap(); + + let tx_code = vec![]; + let tx_data = vec![]; + + let mut tx = Tx::from_type(TxType::Raw); + tx.header.chain_id = state.in_mem().chain_id.clone(); + tx.set_code(Code::new(tx_code, None)); + tx.set_data(Data::new(tx_data)); + tx.add_section(Section::Signature(Signature::new( + vec![tx.header_hash()], + [(0, keypair_1())].into_iter().collect(), + None, + ))); + + init_proposal( + &mut state, + proposal_id, + 500, + 3, + 9, + 19, + &signer_address, + false, + ); + + let ctx = Ctx::new( + &ADDRESS, + &state, + &tx, + &tx_index, + &gas_meter, + &sentinel, + &keys_changed, + &verifiers, + vp_wasm_cache.clone(), + ); + + let governance_vp = GovernanceVp { ctx }; + // this should return true because state has been stored + assert!( + governance_vp + .validate_tx(&tx, &keys_changed, &verifiers) + .expect("validation failed") + ); + + state.write_log_mut().commit_tx(); + state.commit_block().unwrap(); + + let height = state.in_mem().get_block_height().0 + (7 * 2); + + update_epoch_to(&mut state, 8, height); + + let validator_address = established_address_1(); + + let vote_key = get_vote_proposal_key( + 0, + validator_address.clone(), + validator_address.clone(), + ); + state + .write_log_mut() + .write(&vote_key, ProposalVote::Yay.serialize_to_vec()) + .unwrap(); + + keys_changed.clear(); + keys_changed.insert(vote_key); + + verifiers.clear(); + verifiers.insert(validator_address); + + let ctx = Ctx::new( + &ADDRESS, + &state, + &tx, + &tx_index, + &gas_meter, + &sentinel, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); + + let governance_vp = GovernanceVp { ctx }; + + assert!( + !governance_vp + .validate_tx(&tx, &keys_changed, &verifiers) + .expect("validation failed") + ); + } + + #[test] + fn test_goverance_vote_delegator_success() { + let mut state = init_storage(); + + let proposal_id = 0; + let grace_epoch = 19; + + let mut keys_changed = get_proposal_keys(proposal_id, grace_epoch); + + let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + )); + let (vp_wasm_cache, _vp_cache_dir) = + wasm::compilation_cache::common::testing::cache(); + + let tx_index = TxIndex::default(); + let sentinel = RefCell::new(VpSentinel::default()); + + let signer = keypair_1(); + let signer_address = Address::from(&signer.clone().ref_to()); + let mut verifiers = BTreeSet::from([signer_address.clone()]); + + initialize_account_balance( + &mut state, + &signer_address.clone(), + token::Amount::native_whole(510), + ); + initialize_account_balance( + &mut state, + &ADDRESS, + token::Amount::native_whole(0), + ); + state.commit_block().unwrap(); + + let tx_code = vec![]; + let tx_data = vec![]; + + let mut tx = Tx::from_type(TxType::Raw); + tx.header.chain_id = state.in_mem().chain_id.clone(); + tx.set_code(Code::new(tx_code, None)); + tx.set_data(Data::new(tx_data)); + tx.add_section(Section::Signature(Signature::new( + vec![tx.header_hash()], + [(0, keypair_1())].into_iter().collect(), + None, + ))); + + init_proposal( + &mut state, + proposal_id, + 500, + 3, + 9, + 19, + &signer_address, + false, + ); + + let ctx = Ctx::new( + &ADDRESS, + &state, + &tx, + &tx_index, + &gas_meter, + &sentinel, + &keys_changed, + &verifiers, + vp_wasm_cache.clone(), + ); + + let governance_vp = GovernanceVp { ctx }; + + assert!( + governance_vp + .validate_tx(&tx, &keys_changed, &verifiers) + .expect("validation failed") + ); + + state.write_log_mut().commit_tx(); + state.commit_block().unwrap(); + + let height = state.in_mem().get_block_height().0 + (9 * 2); + + let validator_address = established_address_1(); + let delegator_address = established_address_3(); + + initialize_account_balance( + &mut state, + &delegator_address, + token::Amount::native_whole(1000000), + ); + + bond_tokens( + &mut state, + Some(&delegator_address), + &validator_address, + token::Amount::from_u64(10000), + Epoch(1), + None, + ) + .unwrap(); + + update_epoch_to(&mut state, 9, height); + + let vote_key = get_vote_proposal_key( + 0, + delegator_address.clone(), + validator_address.clone(), + ); + state + .write_log_mut() + .write(&vote_key, ProposalVote::Yay.serialize_to_vec()) + .unwrap(); + + keys_changed.clear(); + keys_changed.insert(vote_key); + + verifiers.clear(); + verifiers.insert(delegator_address); + + let ctx = Ctx::new( + &ADDRESS, + &state, + &tx, + &tx_index, + &gas_meter, + &sentinel, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); + + let governance_vp = GovernanceVp { ctx }; + + assert!( + governance_vp + .validate_tx(&tx, &keys_changed, &verifiers) + .expect("validation failed") + ); + } + + #[test] + fn test_goverance_vote_delegator_fail() { + let mut state = init_storage(); + + let proposal_id = 0; + let grace_epoch = 19; + + let mut keys_changed = get_proposal_keys(proposal_id, grace_epoch); + + let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + )); + let (vp_wasm_cache, _vp_cache_dir) = + wasm::compilation_cache::common::testing::cache(); + + let tx_index = TxIndex::default(); + let sentinel = RefCell::new(VpSentinel::default()); + + let signer = keypair_1(); + let signer_address = Address::from(&signer.clone().ref_to()); + let mut verifiers = BTreeSet::from([signer_address.clone()]); + + initialize_account_balance( + &mut state, + &signer_address.clone(), + token::Amount::native_whole(510), + ); + initialize_account_balance( + &mut state, + &ADDRESS, + token::Amount::native_whole(0), + ); + state.commit_block().unwrap(); + + let tx_code = vec![]; + let tx_data = vec![]; + + let mut tx = Tx::from_type(TxType::Raw); + tx.header.chain_id = state.in_mem().chain_id.clone(); + tx.set_code(Code::new(tx_code, None)); + tx.set_data(Data::new(tx_data)); + tx.add_section(Section::Signature(Signature::new( + vec![tx.header_hash()], + [(0, keypair_1())].into_iter().collect(), + None, + ))); + + init_proposal( + &mut state, + proposal_id, + 500, + 3, + 9, + 19, + &signer_address, + false, + ); + + let ctx = Ctx::new( + &ADDRESS, + &state, + &tx, + &tx_index, + &gas_meter, + &sentinel, + &keys_changed, + &verifiers, + vp_wasm_cache.clone(), + ); + + let governance_vp = GovernanceVp { ctx }; + + assert!( + governance_vp + .validate_tx(&tx, &keys_changed, &verifiers) + .expect("validation failed") + ); + + state.write_log_mut().commit_tx(); + state.commit_block().unwrap(); + + let height = state.in_mem().get_block_height().0 + (10 * 2); + + let validator_address = established_address_1(); + let delegator_address = established_address_3(); + + initialize_account_balance( + &mut state, + &delegator_address, + token::Amount::native_whole(1000000), + ); + + bond_tokens( + &mut state, + Some(&delegator_address), + &validator_address, + token::Amount::from_u64(10000), + Epoch(1), + None, + ) + .unwrap(); + + update_epoch_to(&mut state, 10, height); + + let vote_key = get_vote_proposal_key( + 0, + delegator_address.clone(), + validator_address.clone(), + ); + state + .write_log_mut() + .write(&vote_key, ProposalVote::Yay.serialize_to_vec()) + .unwrap(); + + keys_changed.clear(); + keys_changed.insert(vote_key); + + verifiers.clear(); + verifiers.insert(delegator_address); + + let ctx = Ctx::new( + &ADDRESS, + &state, + &tx, + &tx_index, + &gas_meter, + &sentinel, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); + + let governance_vp = GovernanceVp { ctx }; + + assert!( + !governance_vp + .validate_tx(&tx, &keys_changed, &verifiers) + .expect("validation failed") + ); + } + + #[test] + fn test_goverance_vote_invalid_verifier_fail() { + let mut state = init_storage(); + + let proposal_id = 0; + let grace_epoch = 19; + + let mut keys_changed = get_proposal_keys(proposal_id, grace_epoch); + + let gas_meter = RefCell::new(VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + )); + let (vp_wasm_cache, _vp_cache_dir) = + wasm::compilation_cache::common::testing::cache(); + + let tx_index = TxIndex::default(); + let sentinel = RefCell::new(VpSentinel::default()); + + let signer = keypair_1(); + let signer_address = Address::from(&signer.clone().ref_to()); + let mut verifiers = BTreeSet::from([signer_address.clone()]); + + initialize_account_balance( + &mut state, + &signer_address.clone(), + token::Amount::native_whole(510), + ); + initialize_account_balance( + &mut state, + &ADDRESS, + token::Amount::native_whole(0), + ); + state.commit_block().unwrap(); + + let tx_code = vec![]; + let tx_data = vec![]; + + let mut tx = Tx::from_type(TxType::Raw); + tx.header.chain_id = state.in_mem().chain_id.clone(); + tx.set_code(Code::new(tx_code, None)); + tx.set_data(Data::new(tx_data)); + tx.add_section(Section::Signature(Signature::new( + vec![tx.header_hash()], + [(0, keypair_1())].into_iter().collect(), + None, + ))); + + init_proposal( + &mut state, + proposal_id, + 500, + 3, + 9, + 19, + &signer_address, + false, + ); + + let ctx = Ctx::new( + &ADDRESS, + &state, + &tx, + &tx_index, + &gas_meter, + &sentinel, + &keys_changed, + &verifiers, + vp_wasm_cache.clone(), + ); + + let governance_vp = GovernanceVp { ctx }; + + assert!( + governance_vp + .validate_tx(&tx, &keys_changed, &verifiers) + .expect("validation failed") + ); + + state.write_log_mut().commit_tx(); + state.commit_block().unwrap(); + + let height = state.in_mem().get_block_height().0 + (10 * 2); + + let validator_address = established_address_1(); + let delegator_address = established_address_3(); + + initialize_account_balance( + &mut state, + &delegator_address, + token::Amount::native_whole(1000000), + ); + + bond_tokens( + &mut state, + Some(&delegator_address), + &validator_address, + token::Amount::from_u64(10000), + Epoch(1), + None, + ) + .unwrap(); + + update_epoch_to(&mut state, 10, height); + + let vote_key = get_vote_proposal_key( + 0, + delegator_address.clone(), + validator_address.clone(), + ); + state + .write_log_mut() + .write(&vote_key, ProposalVote::Yay.serialize_to_vec()) + .unwrap(); + + keys_changed.clear(); + keys_changed.insert(vote_key); + + verifiers.clear(); + verifiers.insert(validator_address); + + let ctx = Ctx::new( + &ADDRESS, + &state, + &tx, + &tx_index, + &gas_meter, + &sentinel, + &keys_changed, + &verifiers, + vp_wasm_cache, + ); + + let governance_vp = GovernanceVp { ctx }; + + assert!( + !governance_vp + .validate_tx(&tx, &keys_changed, &verifiers) + .expect("validation failed") + ); + } +} diff --git a/crates/proof_of_stake/src/lib.rs b/crates/proof_of_stake/src/lib.rs index 02b9ada912..254bab6bcb 100644 --- a/crates/proof_of_stake/src/lib.rs +++ b/crates/proof_of_stake/src/lib.rs @@ -2485,6 +2485,10 @@ where #[cfg(any(test, feature = "testing"))] /// PoS related utility functions to help set up tests. pub mod test_utils { + use namada_core::chain::ProposalBytes; + use namada_core::hash::Hash; + use namada_core::time::DurationSecs; + use namada_parameters::{init_storage, EpochDuration}; use namada_trans_token::credit_tokens; use super::*; @@ -2568,6 +2572,27 @@ pub mod test_utils { namada_governance::parameters::GovernanceParameters::default(); gov_params.init_storage(storage)?; let params = read_non_pos_owned_params(storage, owned)?; + let chain_parameters = namada_parameters::Parameters { + max_tx_bytes: 123456789, + epoch_duration: EpochDuration { + min_num_of_blocks: 2, + min_duration: DurationSecs(4), + }, + max_expected_time_per_block: DurationSecs(2), + max_proposal_bytes: ProposalBytes::default(), + max_block_gas: 10000000, + vp_allowlist: vec![], + tx_allowlist: vec![], + implicit_vp_code_hash: Some(Hash::default()), + epochs_per_year: 10000000, + max_signatures_per_transaction: 15, + staked_ratio: Dec::new(2, 3).unwrap(), + pos_inflation_amount: token::Amount::from_u64(1000), + fee_unshielding_gas_limit: 10000, + fee_unshielding_descriptions_limit: 15, + minimum_gas_price: BTreeMap::new(), + }; + init_storage(&chain_parameters, storage).unwrap(); init_genesis_helper(storage, ¶ms, validators, current_epoch)?; Ok(params) } diff --git a/crates/sdk/src/args.rs b/crates/sdk/src/args.rs index edf940ace8..7e9defea5b 100644 --- a/crates/sdk/src/args.rs +++ b/crates/sdk/src/args.rs @@ -427,8 +427,6 @@ pub struct InitProposal { pub tx: Tx, /// The proposal data pub proposal_data: C::Data, - /// 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 @@ -458,11 +456,6 @@ impl InitProposal { } } - /// Flag if proposal should be run offline - pub fn is_offline(self, is_offline: bool) -> Self { - Self { is_offline, ..self } - } - /// Flag if proposal is of type Pgf stewards pub fn is_pgf_stewards(self, is_pgf_stewards: bool) -> Self { Self { @@ -578,15 +571,11 @@ pub struct VoteProposal { /// Common tx arguments pub tx: Tx, /// Proposal id - pub proposal_id: Option, + pub proposal_id: u64, /// 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, + pub voter_address: C::Address, /// Path to the TX WASM code file pub tx_code_path: PathBuf, } @@ -607,7 +596,7 @@ impl VoteProposal { /// Proposal id pub fn proposal_id(self, proposal_id: u64) -> Self { Self { - proposal_id: Some(proposal_id), + proposal_id, ..self } } @@ -618,19 +607,9 @@ impl VoteProposal { } /// The address of the voter - pub fn voter(self, voter: C::Address) -> Self { - Self { voter, ..self } - } - - /// Flag if proposal vote should be run offline - pub fn is_offline(self, is_offline: bool) -> Self { - Self { is_offline, ..self } - } - - /// The proposal file path - pub fn proposal_data(self, proposal_data: C::Data) -> Self { + pub fn voter(self, voter_address: C::Address) -> Self { Self { - proposal_data: Some(proposal_data), + voter_address, ..self } } diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index c79ffc0193..d5ce0c738b 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -274,7 +274,6 @@ pub trait Namada: Sized + MaybeSync + MaybeSend { fn new_init_proposal(&self, proposal_data: Vec) -> args::InitProposal { args::InitProposal { proposal_data, - is_offline: false, is_pgf_stewards: false, is_pgf_funding: false, tx_code_path: PathBuf::from(TX_INIT_PROPOSAL), @@ -295,17 +294,16 @@ pub trait Namada: Sized + MaybeSync + MaybeSend { } /// Make a VoteProposal builder from the given minimum set of arguments - fn new_vote_prposal( + fn new_proposal_vote( &self, + proposal_id: u64, vote: String, - voter: Address, + voter_address: Address, ) -> args::VoteProposal { args::VoteProposal { vote, - voter, - proposal_id: None, - is_offline: false, - proposal_data: None, + voter_address, + proposal_id, tx_code_path: PathBuf::from(TX_VOTE_PROPOSAL), tx: self.tx_builder(), } @@ -1175,7 +1173,7 @@ pub mod testing { let mut tx = Tx { header, sections: vec![] }; let content_hash = tx.add_section(Section::ExtraData(content_extra_data)).get_hash(); init_proposal.content = content_hash; - if let ProposalType::Default(Some(hash)) = &mut init_proposal.r#type { + if let ProposalType::DefaultWithWasm(hash) = &mut init_proposal.r#type { let type_hash = tx.add_section(Section::ExtraData(type_extra_data)).get_hash(); *hash = type_hash; } diff --git a/crates/sdk/src/rpc.rs b/crates/sdk/src/rpc.rs index 7bc8b1ae7d..2d034fb5d4 100644 --- a/crates/sdk/src/rpc.rs +++ b/crates/sdk/src/rpc.rs @@ -979,7 +979,7 @@ pub async fn query_proposal_result( proposal_votes.add_validator( &vote.validator, voting_power, - vote.data.into(), + vote.data, ); } false => { @@ -996,7 +996,7 @@ pub async fn query_proposal_result( &vote.delegator, &vote.validator, voting_power, - vote.data.into(), + vote.data, ); } } diff --git a/crates/sdk/src/signing.rs b/crates/sdk/src/signing.rs index 4d05007734..9df8ca9474 100644 --- a/crates/sdk/src/signing.rs +++ b/crates/sdk/src/signing.rs @@ -916,8 +916,8 @@ struct LedgerProposalType<'a>(&'a ProposalType, &'a Tx); impl<'a> Display for LedgerProposalType<'a> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self.0 { - ProposalType::Default(None) => write!(f, "Default"), - ProposalType::Default(Some(hash)) => { + ProposalType::Default => write!(f, "Default"), + ProposalType::DefaultWithWasm(hash) => { let extra = self .1 .get_section(hash) @@ -939,10 +939,10 @@ fn proposal_type_to_ledger_vector( output: &mut Vec, ) { match proposal_type { - ProposalType::Default(None) => { + ProposalType::Default => { output.push("Proposal type : Default".to_string()) } - ProposalType::Default(Some(hash)) => { + ProposalType::DefaultWithWasm(hash) => { output.push("Proposal type : Default".to_string()); let extra = tx .get_section(hash) @@ -1223,7 +1223,6 @@ pub async fn to_ledger_vector( .hash(); tv.output.push("Type : Init proposal".to_string()); - tv.output.push(format!("ID : {}", init_proposal_data.id)); proposal_type_to_ledger_vector( &init_proposal_data.r#type, tx, @@ -1243,8 +1242,6 @@ pub async fn to_ledger_vector( format!("Content : {}", HEXLOWER.encode(&extra.0)), ]); - tv.output_expert - .push(format!("ID : {}", init_proposal_data.id)); proposal_type_to_ledger_vector( &init_proposal_data.r#type, tx, diff --git a/crates/sdk/src/tx.rs b/crates/sdk/src/tx.rs index 7255a3f03a..100ac96b06 100644 --- a/crates/sdk/src/tx.rs +++ b/crates/sdk/src/tx.rs @@ -1861,7 +1861,6 @@ pub async fn build_default_proposal( args::InitProposal { tx, proposal_data: _, - is_offline: _, is_pgf_stewards: _, is_pgf_funding: _, tx_code_path, @@ -1893,7 +1892,7 @@ pub async fn build_default_proposal( let (_, extra_section_hash) = tx_builder.add_extra_section(init_proposal_code, None); init_proposal_data.r#type = - ProposalType::Default(Some(extra_section_hash)); + ProposalType::DefaultWithWasm(extra_section_hash); }; Ok(()) }; @@ -1919,18 +1918,16 @@ pub async fn build_vote_proposal( tx, proposal_id, vote, - voter, - is_offline: _, - proposal_data: _, + voter_address, tx_code_path, }: &args::VoteProposal, epoch: Epoch, ) -> Result<(Tx, SigningTxData)> { - let default_signer = Some(voter.clone()); + let default_signer = Some(voter_address.clone()); let signing_data = signing::aux_signing_data( context, tx, - Some(voter.clone()), + default_signer.clone(), default_signer.clone(), ) .await?; @@ -1941,20 +1938,18 @@ pub async fn build_vote_proposal( let proposal_vote = ProposalVote::try_from(vote.clone()) .map_err(|_| TxSubmitError::InvalidProposalVote)?; - let proposal_id = proposal_id.ok_or_else(|| { - Error::Other("Proposal id must be defined.".to_string()) - })?; let proposal = if let Some(proposal) = - rpc::query_proposal_by_id(context.client(), proposal_id).await? + rpc::query_proposal_by_id(context.client(), *proposal_id).await? { proposal } else { return Err(Error::from(TxSubmitError::ProposalDoesNotExist( - proposal_id, + *proposal_id, ))); }; - let is_validator = rpc::is_validator(context.client(), voter).await?; + let is_validator = + rpc::is_validator(context.client(), voter_address).await?; if !proposal.can_be_voted(epoch, is_validator) { eprintln!("Proposal {} cannot be voted on anymore.", proposal_id); @@ -1962,19 +1957,19 @@ pub async fn build_vote_proposal( eprintln!( "NB: voter address {} is a validator, and validators can only \ vote on proposals within the first 2/3 of the voting period.", - voter + voter_address ); } if !tx.force { return Err(Error::from( - TxSubmitError::InvalidProposalVotingPeriod(proposal_id), + TxSubmitError::InvalidProposalVotingPeriod(*proposal_id), )); } } let delegations = rpc::get_delegators_delegation_at( context.client(), - voter, + voter_address, proposal.voting_start_epoch, ) .await? @@ -1989,9 +1984,9 @@ pub async fn build_vote_proposal( } let data = VoteProposalData { - id: proposal_id, + id: *proposal_id, vote: proposal_vote, - voter: voter.clone(), + voter: voter_address.clone(), delegations, }; @@ -2015,7 +2010,6 @@ pub async fn build_pgf_funding_proposal( args::InitProposal { tx, proposal_data: _, - is_offline: _, is_pgf_stewards: _, is_pgf_funding: _, tx_code_path, @@ -2064,7 +2058,6 @@ pub async fn build_pgf_stewards_proposal( args::InitProposal { tx, proposal_data: _, - is_offline: _, is_pgf_stewards: _, is_pgf_funding: _, tx_code_path, diff --git a/crates/tests/src/e2e/ledger_tests.rs b/crates/tests/src/e2e/ledger_tests.rs index 7a012f4d88..cc0d4962eb 100644 --- a/crates/tests/src/e2e/ledger_tests.rs +++ b/crates/tests/src/e2e/ledger_tests.rs @@ -48,9 +48,7 @@ use super::helpers::{ get_height, get_pregenesis_wallet, wait_for_block_height, wait_for_wasm_pre_compile, }; -use super::setup::{ - get_all_wasms_hashes, set_ethereum_bridge_mode, working_dir, NamadaCmd, -}; +use super::setup::{set_ethereum_bridge_mode, working_dir, NamadaCmd}; use crate::e2e::helpers::{ epoch_sleep, find_address, find_bonded_stake, get_actor_rpc, get_epoch, is_debug_mode, parse_reached_epoch, @@ -2661,160 +2659,6 @@ fn pgf_steward_change_commissions() -> Result<()> { Ok(()) } -/// In this test we: -/// 1. Run the ledger node -/// 2. Create an offline proposal -/// 3. Create an offline vote -/// 4. Tally offline -#[test] -fn proposal_offline() -> Result<()> { - let working_dir = setup::working_dir(); - let test = setup::network( - |mut genesis, base_dir: &_| { - genesis.parameters.parameters.epochs_per_year = - epochs_per_year_from_min_duration(1); - genesis.parameters.parameters.max_proposal_bytes = - Default::default(); - genesis.parameters.parameters.min_num_of_blocks = 4; - genesis.parameters.parameters.max_expected_time_per_block = 1; - genesis.parameters.parameters.vp_allowlist = - Some(get_all_wasms_hashes(&working_dir, Some("vp_"))); - // Enable tx allowlist to test the execution of a - // non-allowed tx by governance - genesis.parameters.parameters.tx_allowlist = - Some(get_all_wasms_hashes(&working_dir, Some("tx_"))); - setup::set_validators(1, genesis, base_dir, |_| 0) - }, - None, - )?; - - set_ethereum_bridge_mode( - &test, - &test.net.chain_id, - Who::Validator(0), - ethereum_bridge::ledger::Mode::Off, - None, - ); - - // 1. 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)); - - // 1.1 Delegate some token - let tx_args = vec![ - "bond", - "--validator", - "validator-0", - "--source", - ALBERT, - "--amount", - "900", - "--node", - &validator_one_rpc, - ]; - let mut client = run!(test, Bin::Client, tx_args, Some(40))?; - client.exp_string(TX_APPLIED_SUCCESS)?; - client.assert_success(); - - // 2. Create an offline proposal - let albert = find_address(&test, ALBERT)?; - let valid_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, - "tally_epoch": 3_u64, - } - ); - let valid_proposal_json_path = - test.test_dir.path().join("valid_proposal.json"); - write_json_file(valid_proposal_json_path.as_path(), 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![ - "init-proposal", - "--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))?; - let (_, matched) = client.exp_regex("Proposal serialized to: .*")?; - client.assert_success(); - - 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, - "--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 serialized to: ")?; - client.assert_success(); - - // 4. Compute offline tally - let tally_offline = vec![ - "query-proposal-result", - "--data-path", - test.test_dir.path().to_str().unwrap(), - "--offline", - "--node", - &validator_one_rpc, - ]; - - let mut client = run!(test, Bin::Client, tally_offline, Some(15))?; - client.exp_string("Parsed 1 votes")?; - client.exp_string("rejected with 900.000000 yay votes")?; - client.assert_success(); - - Ok(()) -} - pub fn write_json_file(proposal_path: &std::path::Path, proposal_content: T) where T: Serialize, diff --git a/documentation/dev/src/explore/design/ledger/governance.md b/documentation/dev/src/explore/design/ledger/governance.md index a7215c79b2..78905a1caa 100644 --- a/documentation/dev/src/explore/design/ledger/governance.md +++ b/documentation/dev/src/explore/design/ledger/governance.md @@ -106,8 +106,3 @@ Vote is valid if it follows these rules: The outcome of a proposal is computed at the epoch specific in the `endEpoch` field and executed at `graceEpoch` field (if it contains a non-empty `proposalCode` field). A proposal is accepted only if enough `yay` votes (net of the voting power) to match the threshold set in `ProposalType` is reached. If a proposal gets accepted, the locked funds will be reimbursed to the author. In case it gets rejected, the locked funds will be moved to slash fund. - -## Off-chain proposal - -In cases where it's not possible to run a proposal online (for example, when the chain is halted), an offline mechanism can be used. -The ledger offers the possibility to create and sign proposals that are verified against a specific chain epoch.