From e24962601f0099270cd8d84b9207e6b78e0a18f8 Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Thu, 18 Apr 2024 15:07:19 -0400 Subject: [PATCH] =?UTF-8?q?tests(app):=20=F0=9F=94=92=20test=20disabling?= =?UTF-8?q?=20community=20spend=20proposals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app_can_disable_community_pool_spends.rs | 370 ++++++++++++++++++ ...posit_into_community_pool_when_disabled.rs | 114 ------ 2 files changed, 370 insertions(+), 114 deletions(-) create mode 100644 crates/core/app/tests/app_can_disable_community_pool_spends.rs delete mode 100644 crates/core/app/tests/app_cannot_deposit_into_community_pool_when_disabled.rs diff --git a/crates/core/app/tests/app_can_disable_community_pool_spends.rs b/crates/core/app/tests/app_can_disable_community_pool_spends.rs new file mode 100644 index 0000000000..bac8e9a281 --- /dev/null +++ b/crates/core/app/tests/app_can_disable_community_pool_spends.rs @@ -0,0 +1,370 @@ +use { + anyhow::anyhow, + cnidarium::TempStorage, + decaf377_rdsa::VerificationKey, + penumbra_app::{ + genesis::{AppState, Content}, + server::consensus::Consensus, + CommunityPoolStateReadExt as _, + }, + penumbra_community_pool::{ + CommunityPoolDeposit, CommunityPoolOutput, CommunityPoolSpend, StateReadExt as _, + }, + penumbra_governance::{ + Proposal, ProposalSubmit, StateReadExt as _, ValidatorVote, ValidatorVoteBody, + ValidatorVoteReason, + }, + penumbra_keys::{ + keys::{SpendKey, SpendKeyBytes}, + test_keys::{self}, + }, + penumbra_mock_client::MockClient, + penumbra_mock_consensus::TestNode, + penumbra_proto::{ + core::keys::v1::{GovernanceKey, IdentityKey}, + penumbra::core::component::stake::v1::Validator as PenumbraValidator, + DomainType, + }, + penumbra_shielded_pool::{genesis::Allocation, OutputPlan, SpendPlan}, + penumbra_stake::{component::validator_handler::ValidatorDataRead, DelegationToken}, + penumbra_transaction::{ + memo::MemoPlaintext, plan::MemoPlan, ActionPlan, TransactionParameters, TransactionPlan, + }, + rand::Rng, + rand_core::OsRng, + std::collections::BTreeMap, + tap::{Tap, TapFallible}, + tracing::{error_span, info, Instrument}, +}; + +mod common; + +const PROPOSAL_VOTING_BLOCKS: u64 = 3; + +/// Exercises that the app can disable proposals to spend community pool funds. +#[tokio::test] +async fn app_can_disable_community_pool_spends() -> anyhow::Result<()> { + // Install a test logger, and acquire some temporary storage. + let guard = common::set_tracing_subscriber(); + let storage = TempStorage::new().await?; + + // Define a helper to get the current community pool balance. + let pool_balance = || async { storage.latest_snapshot().community_pool_balance().await }; + let pending_pool_txs = || async { + storage + .latest_snapshot() + .pending_community_pool_transactions() + .await + }; + + // Generate a set of consensus keys. + let consensus_sk = ed25519_consensus::SigningKey::new(OsRng); + let consensus_vk = consensus_sk.verification_key(); + + // Generate a set of identity keys. + let spend_key: SpendKey = SpendKeyBytes(OsRng.gen()).into(); + let (identity_sk, identity_vk) = { + let sk = spend_key.spend_auth_key(); + let vk = VerificationKey::from(sk); + (sk, vk) + }; + let (governance_sk, governance_vk) = (identity_sk, identity_vk); + + // Define a validator and an associated genesis allocation. + let (validator, allocation) = { + let v = PenumbraValidator { + identity_key: Some(IdentityKey { + ik: identity_vk.to_bytes().to_vec(), + }), + // NB: for now, we will use the same key for governance. See the documentation of + // `GovernanceKey` for more information about cold storage of validator keys. + governance_key: Some(GovernanceKey { + gk: identity_vk.to_bytes().to_vec(), + }), + consensus_key: consensus_vk.as_bytes().to_vec(), + enabled: true, + sequence_number: 0, + name: String::default(), + website: String::default(), + description: String::default(), + funding_streams: Vec::default(), + }; + + let (address, _) = spend_key + .full_viewing_key() + .incoming() + .payment_address(0u32.into()); + + let ik = penumbra_stake::IdentityKey(identity_vk.into()); + let delegation_denom = DelegationToken::from(ik).denom(); + + let allocation = Allocation { + raw_amount: 1000u128.into(), + raw_denom: delegation_denom.to_string(), + address, + }; + + (v, allocation) + }; + + // Define our application state, and start the test node. + let mut test_node = { + let mut content = Content { + governance_content: penumbra_governance::genesis::Content { + governance_params: penumbra_governance::params::GovernanceParameters { + proposal_deposit_amount: 0_u32.into(), + proposal_voting_blocks: PROPOSAL_VOTING_BLOCKS, + ..Default::default() + }, + }, + community_pool_content: penumbra_community_pool::genesis::Content { + community_pool_params: penumbra_community_pool::params::CommunityPoolParameters { + // Disable community spend proposals. + community_pool_spend_proposals_enabled: false, + }, + }, + ..Default::default() + }; + content.stake_content.validators.push(validator); + content.shielded_pool_content.allocations.push(allocation); + let app_state = AppState::Content(content); + let app_state = serde_json::to_vec(&app_state).unwrap(); + let consensus = Consensus::new(storage.as_ref().clone()); + TestNode::builder() + .single_validator() + .app_state(app_state) + .init_chain(consensus) + .await + .tap_ok(|e| tracing::info!(hash = %e.last_app_hash_hex(), "finished init chain"))? + }; + let original_pool_balance = pool_balance().await?; + let [_validator] = storage + .latest_snapshot() + .validator_definitions() + .await? + .try_into() + .map_err(|validator| anyhow::anyhow!("expected one validator, got: {validator:?}"))?; + + // Sync the mock client, using the test wallet's spend key, to the latest snapshot. + let client = MockClient::new(test_keys::SPEND_KEY.clone()) + .with_sync_to_storage(&storage) + .await? + .tap(|c| info!(client.notes = %c.notes.len(), "mock client synced to test storage")); + + // Take one of the test wallet's notes, and prepare to deposit it in the community pool. + let note = client + .notes + .values() + .cloned() + .next() + .ok_or_else(|| anyhow!("mock client had no note"))?; + + // Create a community pool transaction. + let mut plan = { + let value = note.value(); + let spend = SpendPlan::new( + &mut OsRng, + note.clone(), + client + .position(note.commit()) + .ok_or_else(|| anyhow!("input note commitment was unknown to mock client"))?, + ) + .into(); + let deposit = CommunityPoolDeposit { value }.into(); + TransactionPlan { + actions: vec![spend, deposit], + // Now fill out the remaining parts of the transaction needed for verification: + memo: None, + detection_data: None, // We'll set this automatically below + transaction_parameters: TransactionParameters { + chain_id: TestNode::<()>::CHAIN_ID.to_string(), + ..Default::default() + }, + } + }; + plan.populate_detection_data(OsRng, 0); + let tx = client.witness_auth_build(&plan).await?; + + // Execute the transaction, applying it to the chain state. + test_node + .block() + .with_data(vec![tx.encode_to_vec()]) + .execute() + .instrument(error_span!("executing block with community pool deposit")) + .await?; + let post_deposit_pool_balance = pool_balance().await?; + + // Now, make a governance proposal that we should spend community pool funds, to return + // the note back to the test wallet. + let mut plan = { + let value = note.value(); + let proposed_tx_plan = TransactionPlan { + actions: vec![ + CommunityPoolSpend { value }.into(), + CommunityPoolOutput { + value, + address: *test_keys::ADDRESS_0, + } + .into(), + ], + memo: None, + detection_data: None, + transaction_parameters: TransactionParameters { + chain_id: TestNode::<()>::CHAIN_ID.to_string(), + ..Default::default() + }, + }; + let proposal_submit = ProposalSubmit { + proposal: Proposal { + id: 0_u64, + title: "return test deposit".to_owned(), + description: "a proposal to return the community pool deposit".to_owned(), + payload: penumbra_governance::ProposalPayload::CommunityPoolSpend { + transaction_plan: proposed_tx_plan.encode_to_vec(), + // transaction_plan: TransactionPlan::default().encode_to_vec(), + }, + }, + deposit_amount: 0_u32.into(), + }; + let proposal_nft_value = proposal_submit.proposal_nft_value(); + let proposal = ActionPlan::ProposalSubmit(proposal_submit); + TransactionPlan { + actions: vec![ + proposal, + // Next, create a new output of the exact same amount. + OutputPlan::new(&mut OsRng, proposal_nft_value, *test_keys::ADDRESS_0).into(), + ], + // Now fill out the remaining parts of the transaction needed for verification: + memo: Some(MemoPlan::new( + &mut OsRng, + MemoPlaintext::blank_memo(*test_keys::ADDRESS_0), + )?), + detection_data: None, + transaction_parameters: TransactionParameters { + chain_id: TestNode::<()>::CHAIN_ID.to_string(), + ..Default::default() + }, + } + }; + plan.populate_detection_data(OsRng, 0); + let tx = client.witness_auth_build(&plan).await?; + + // Execute the transaction, applying it to the chain state. + test_node + .block() + .with_data(vec![tx.encode_to_vec()]) + .execute() + .instrument(error_span!("executing block with governance proposal")) + .await?; + let post_proposal_pool_balance = pool_balance().await?; + let post_proposal_pending_txs = pending_pool_txs().await?; + let post_proposal_state = storage.latest_snapshot().proposal_state(0).await?; + + // Now make another transaction that will contain a validator vote upon our transaction. + let mut plan = { + let body = ValidatorVoteBody { + proposal: 0_u64, + vote: penumbra_governance::Vote::Yes, + identity_key: penumbra_stake::IdentityKey(identity_vk.to_bytes().into()), + governance_key: penumbra_stake::GovernanceKey(governance_vk), + reason: ValidatorVoteReason("test reason".to_owned()), + }; + let auth_sig = governance_sk.sign(OsRng, body.encode_to_vec().as_slice()); + let vote = ValidatorVote { body, auth_sig }.into(); + TransactionPlan { + actions: vec![vote], + memo: None, + detection_data: None, + transaction_parameters: TransactionParameters { + chain_id: TestNode::<()>::CHAIN_ID.to_string(), + ..Default::default() + }, + } + }; + plan.populate_detection_data(OsRng, 0); + let tx = client.witness_auth_build(&plan).await?; + + // Execute the transaction, applying it to the chain state. + test_node + .block() + .with_data(vec![tx.encode_to_vec()]) + .execute() + .instrument(error_span!("executing block with validator vote")) + .await?; + let post_vote_pool_balance = pool_balance().await?; + let post_vote_pending_txs = pending_pool_txs().await?; + let post_vote_state = storage.latest_snapshot().proposal_state(0).await?; + + test_node.fast_forward(PROPOSAL_VOTING_BLOCKS).await?; + let post_voting_period_pool_balance = pool_balance().await?; + let post_voting_period_pending_txs = pending_pool_txs().await?; + let post_voting_period_state = storage.latest_snapshot().proposal_state(0).await?; + + // At the outset, the pool should be empty. + assert_eq!( + original_pool_balance, + BTreeMap::default(), + "the community pool should be empty at the beginning of the chain" + ); + + // After we deposit a note into the community pool, we should see the original pool contents, + // plus the amount that we deposited. + assert_eq!( + [(note.asset_id(), note.amount())] + .into_iter() + .collect::>(), + post_deposit_pool_balance, + "a community pool deposit should be reflected in the visible balance, even if spends are disabled" + ); + + // A proposal should not itself affect the balance of the community pool. + assert_eq!( + post_deposit_pool_balance, post_proposal_pool_balance, + "the community pool balance should not be affected by a proposal" + ); + assert_eq!(post_proposal_state, None, "the proposal should be rejected"); + assert_eq!( + post_proposal_pending_txs.len(), + 0, + "no transaction(s) should be pending" + ); + + // ...nor should a vote by itself. + assert_eq!( + post_proposal_pool_balance, post_vote_pool_balance, + "the community pool balance should not be affected by a vote, even with quorum" + ); + assert_eq!( + post_vote_state, None, + "a vote for a rejected proposal should not cause it to enter the voting state" + ); + assert_eq!( + post_vote_pending_txs.len(), + 0, + "no transaction(s) should be pending" + ); + + // After any possible voting period, we should see the same pool balance. + assert_eq!( + post_voting_period_pool_balance, + [(note.asset_id(), note.amount())] + .into_iter() + .collect::>(), + "a rejected proposal should not decrease the funds of the community pool" + ); + assert_eq!( + post_voting_period_state, None, + "a proposal should be finished after the voting period completes" + ); + assert_eq!( + post_voting_period_pending_txs.len(), + 0, + "a proposal has been rejected, no transaction(s) are pending" + ); + + // Free our temporary storage. + Ok(()) + .tap(|_| drop(test_node)) + .tap(|_| drop(storage)) + .tap(|_| drop(guard)) +} diff --git a/crates/core/app/tests/app_cannot_deposit_into_community_pool_when_disabled.rs b/crates/core/app/tests/app_cannot_deposit_into_community_pool_when_disabled.rs deleted file mode 100644 index 63dbe2fe08..0000000000 --- a/crates/core/app/tests/app_cannot_deposit_into_community_pool_when_disabled.rs +++ /dev/null @@ -1,114 +0,0 @@ -use { - self::common::BuilderExt, - anyhow::anyhow, - cnidarium::TempStorage, - penumbra_app::{ - genesis::{AppState, Content}, - server::consensus::Consensus, - }, - penumbra_community_pool::{CommunityPoolDeposit, StateReadExt}, - penumbra_keys::test_keys, - penumbra_mock_client::MockClient, - penumbra_mock_consensus::TestNode, - penumbra_proto::DomainType, - penumbra_shielded_pool::SpendPlan, - penumbra_transaction::{TransactionParameters, TransactionPlan}, - rand_core::OsRng, - tap::{Tap, TapFallible}, - tracing::info, -}; - -mod common; - -/// Exercises that the app can *NOT* deposit a note into the community pool when disabled. -#[tokio::test] -async fn app_cannot_deposit_into_community_pool_when_disabled() -> anyhow::Result<()> { - // Install a test logger, and acquire some temporary storage. - let guard = common::set_tracing_subscriber(); - let storage = TempStorage::new().await?; - - // Define our application state, and start the test node. - let mut test_node = { - let app_state = AppState::Content(Content { - governance_content: penumbra_governance::genesis::Content { - governance_params: penumbra_governance::params::GovernanceParameters { - community_pool_is_frozen: true, - ..Default::default() - }, - }, - ..Default::default() - }); - let consensus = Consensus::new(storage.as_ref().clone()); - TestNode::builder() - .single_validator() - .with_penumbra_auto_app_state(app_state)? - .init_chain(consensus) - .await - .tap_ok(|e| tracing::info!(hash = %e.last_app_hash_hex(), "finished init chain"))? - }; - - // Sync the mock client, using the test wallet's spend key, to the latest snapshot. - let client = MockClient::new(test_keys::SPEND_KEY.clone()) - .with_sync_to_storage(&storage) - .await? - .tap(|c| info!(client.notes = %c.notes.len(), "mock client synced to test storage")); - - // Take one of the test wallet's notes, and prepare to deposit it in the community pool. - let note = client - .notes - .values() - .cloned() - .next() - .ok_or_else(|| anyhow!("mock client had no note"))?; - - // Create a community pool transaction. - let mut plan = { - let value = note.value(); - let spend = SpendPlan::new( - &mut OsRng, - note.clone(), - client - .position(note.commit()) - .ok_or_else(|| anyhow!("input note commitment was unknown to mock client"))?, - ) - .into(); - let deposit = CommunityPoolDeposit { value }.into(); - TransactionPlan { - actions: vec![spend, deposit], - // Now fill out the remaining parts of the transaction needed for verification: - memo: None, - detection_data: None, // We'll set this automatically below - transaction_parameters: TransactionParameters { - chain_id: TestNode::<()>::CHAIN_ID.to_string(), - ..Default::default() - }, - } - }; - plan.populate_detection_data(OsRng, 0); - let tx = client.witness_auth_build(&plan).await?; - - // Execute the transaction, applying it to the chain state. - let pre_tx_snapshot = storage.latest_snapshot(); - test_node - .block() - .with_data(vec![tx.encode_to_vec()]) - .execute() - .await?; - let post_tx_snapshot = storage.latest_snapshot(); - - // Show that nothing happened. - { - let pre_tx_balance = pre_tx_snapshot.community_pool_balance().await?; - let post_tx_balance = post_tx_snapshot.community_pool_balance().await?; - assert_eq!( - pre_tx_balance, post_tx_balance, - "community pool should not have been updated" - ); - } - - // Free our temporary storage. - Ok(()) - .tap(|_| drop(test_node)) - .tap(|_| drop(storage)) - .tap(|_| drop(guard)) -}