diff --git a/src/contract/dao/tests/integration.rs b/src/contract/dao/tests/integration.rs
index 900a95198..28fe259ab 100644
--- a/src/contract/dao/tests/integration.rs
+++ b/src/contract/dao/tests/integration.rs
@@ -16,6 +16,22 @@
* along with this program. If not, see .
*/
+//! DAO integration test.
+//!
+//! Tests the full DAO lifecycle:
+//! 1. Airdrop treasury tokens (DRK) to the DAO wallet
+//! 2. Create the DAO on-chain (Dao::Mint)
+//! 3. Distribute governance tokens to three members
+//! 4. Transfer proposal: propose, vote (2 yes / 1 no), exec
+//! 5. Transfer proposal with early execution
+//! 6. Generic proposal: propose, vote, exec
+//! 7. Generic proposal with early execution
+//!
+//! Holders:
+//! * Alice, Bob, Charlie — DAO members (governance token holders)
+//! * Dao — the DAO's treasury wallet
+//! * Rachel — recipient of transfer proposals
+
use darkfi::{util::pcg::Pcg32, Result};
use darkfi_contract_test_harness::{init_logger, Holder, TestHarness};
use darkfi_dao_contract::{
@@ -23,376 +39,215 @@ use darkfi_dao_contract::{
model::{Dao, DaoBlindAggregateVote, DaoVoteParams},
DaoFunction,
};
-use darkfi_money_contract::{
- model::{CoinAttributes, TokenAttributes, DARK_TOKEN_ID},
- MoneyFunction,
-};
+use darkfi_money_contract::model::{CoinAttributes, DARK_TOKEN_ID};
use darkfi_sdk::{
crypto::{
pasta_prelude::*,
- pedersen_commitment_u64, poseidon_hash,
+ pedersen_commitment_u64,
util::{fp_mod_fv, fp_to_u64},
BaseBlind, Blind, FuncId, FuncRef, Keypair, ScalarBlind, SecretKey, DAO_CONTRACT_ID,
- MONEY_CONTRACT_ID,
},
pasta::pallas,
};
use rand::rngs::OsRng;
use tracing::info;
-// Integration test configuration
-// Holders this test will use:
-// * Alice, Bob, and Charlie are members of the DAO.
-// * Dao is the DAO wallet
-// * Rachel is the transfer proposal recipient.
-const HOLDERS: [Holder; 5] =
- [Holder::Alice, Holder::Bob, Holder::Charlie, Holder::Dao, Holder::Rachel];
-// DAO gov tokens distribution
+// DAO governance token distribution
const ALICE_GOV_SUPPLY: u64 = 100_000_000;
const BOB_GOV_SUPPLY: u64 = 100_000_000;
const CHARLIE_GOV_SUPPLY: u64 = 100_000_000;
-// DRK token, the treasury token, supply
+
+// DRK treasury supply
const DRK_TOKEN_SUPPLY: [u64; 1] = [1_000_000_000];
-// DAO parameters configuration
+
+// DAO parameters
const PROPOSER_LIMIT: u64 = 100_000_000;
const QUORUM: u64 = 200_000_000;
const EARLY_EXEC_QUORUM: u64 = 200_000_000;
const APPROVAL_RATIO_BASE: u64 = 2;
const APPROVAL_RATIO_QUOT: u64 = 1;
const PROPOSAL_DURATION_BLOCKWINDOW: u64 = 1;
-// The tokens we want to send via the transfer proposal
+
+// Transfer proposal amount
const TRANSFER_PROPOSAL_AMOUNT: u64 = 250_000_000;
+/// Collects the DAO keypairs so they can be passed around without
+/// repeating 6 secret key arguments everywhere.
+struct DaoKeys {
+ notes: Keypair,
+ proposer: Keypair,
+ proposals: Keypair,
+ votes: Keypair,
+ exec: Keypair,
+ early_exec: Keypair,
+}
+
#[test]
fn integration_test() -> Result<()> {
smol::block_on(async {
init_logger();
- // Initialize harness
- let mut th = TestHarness::new(&HOLDERS, false).await?;
+ use Holder::{Alice, Bob, Charlie, Dao, Rachel};
- // We'll use the ALICE token as the DAO governance token
- let wallet = th.holders.get_mut(&Holder::Alice).unwrap();
- //wallet.bench_wasm = true;
- let mint_authority = wallet.token_mint_authority;
+ let mut th = TestHarness::new(&[Alice, Bob, Charlie, Dao, Rachel], false).await?;
+ let mut height: u32 = 0;
+
+ // Derive DAO governance token ID from Alice's mint authority
let gov_token_blind = BaseBlind::random(&mut OsRng);
+ let gov_token_id = th.derive_token_id(&Alice, gov_token_blind);
- let auth_func_id = FuncRef {
- contract_id: *MONEY_CONTRACT_ID,
- func_code: MoneyFunction::AuthTokenMintV1 as u8,
- }
- .to_func_id();
- let token_attrs = TokenAttributes {
- auth_parent: auth_func_id,
- user_data: poseidon_hash([mint_authority.public.x(), mint_authority.public.y()]),
- blind: gov_token_blind,
- };
- let gov_token_id = token_attrs.to_token_id();
-
- // Block height to verify against
- let mut current_block_height = 0;
-
- // DAO parameters
- let dao_notes_keypair = th.holders.get(&Holder::Dao).unwrap().keypair;
+ // DAO keypairs
let mut rng = Pcg32::new(42);
- let dao_proposer_keypair = Keypair::random(&mut rng);
- let dao_proposals_keypair = Keypair::random(&mut rng);
- let dao_votes_keypair = Keypair::random(&mut rng);
- let dao_exec_keypair = Keypair::random(&mut rng);
- let dao_early_exec_keypair = Keypair::random(&mut rng);
- let dao = Dao {
+ let dao_keys = DaoKeys {
+ notes: th.wallet(&Dao).keypair,
+ proposer: Keypair::random(&mut rng),
+ proposals: Keypair::random(&mut rng),
+ votes: Keypair::random(&mut rng),
+ exec: Keypair::random(&mut rng),
+ early_exec: Keypair::random(&mut rng),
+ };
+
+ let dao = darkfi_dao_contract::model::Dao {
proposer_limit: PROPOSER_LIMIT,
quorum: QUORUM,
early_exec_quorum: EARLY_EXEC_QUORUM,
approval_ratio_base: APPROVAL_RATIO_BASE,
approval_ratio_quot: APPROVAL_RATIO_QUOT,
gov_token_id,
- notes_public_key: dao_notes_keypair.public,
- proposer_public_key: dao_proposer_keypair.public,
- proposals_public_key: dao_proposals_keypair.public,
- votes_public_key: dao_votes_keypair.public,
- exec_public_key: dao_exec_keypair.public,
- early_exec_public_key: dao_early_exec_keypair.public,
+ notes_public_key: dao_keys.notes.public,
+ proposer_public_key: dao_keys.proposer.public,
+ proposals_public_key: dao_keys.proposals.public,
+ votes_public_key: dao_keys.votes.public,
+ exec_public_key: dao_keys.exec.public,
+ early_exec_public_key: dao_keys.early_exec.public,
bulla_blind: Blind::random(&mut OsRng),
};
- // =======================================
- // Airdrop some treasury tokens to the DAO
- // =======================================
- info!("[Dao] Building DAO airdrop tx");
-
+ // ===========================================
+ // 1. Airdrop DRK treasury tokens to the DAO
+ // ===========================================
+ info!("Airdropping DRK treasury tokens to DAO");
let spend_hook =
FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 }
.to_func_id();
- let (genesis_mint_tx, genesis_mint_params) = th
- .genesis_mint(
- &Holder::Dao,
- &DRK_TOKEN_SUPPLY,
- Some(spend_hook),
- Some(dao.to_bulla().inner()),
- )
+ let (genesis_tx, genesis_params) = th
+ .genesis_mint(&Dao, &DRK_TOKEN_SUPPLY, Some(spend_hook), Some(dao.to_bulla().inner()))
+ .await?;
+ th.genesis_mint_to_all_with(genesis_tx, &genesis_params, height).await?;
+
+ assert_eq!(th.coins(&Dao).len(), 1);
+ assert_eq!(th.coins(&Dao)[0].note.token_id, *DARK_TOKEN_ID);
+ assert_eq!(th.coins(&Dao)[0].note.value, DRK_TOKEN_SUPPLY[0]);
+ height += 1;
+
+ // ===========================
+ // 2. Create the DAO on-chain
+ // ===========================
+ info!("Creating DAO bulla on-chain");
+ th.dao_mint_to_all(
+ &Alice,
+ &dao,
+ &dao_keys.notes.secret,
+ &dao_keys.proposer.secret,
+ &dao_keys.proposals.secret,
+ &dao_keys.votes.secret,
+ &dao_keys.exec.secret,
+ &dao_keys.early_exec.secret,
+ height,
+ )
+ .await?;
+ height += 1;
+
+ // =============================================
+ // 3. Mint governance tokens to the three members
+ // =============================================
+ info!("Minting governance tokens to Alice, Bob, Charlie");
+ th.token_mint_with_blind_to_all(ALICE_GOV_SUPPLY, &Alice, &Alice, gov_token_blind, height)
.await?;
- for holder in &HOLDERS {
- th.execute_genesis_mint_tx(
- holder,
- genesis_mint_tx.clone(),
- &genesis_mint_params,
- current_block_height,
- true,
- )
- .await?;
- }
+ assert_eq!(th.coins(&Alice).len(), 1);
+ assert_eq!(th.balance(&Alice, gov_token_id), ALICE_GOV_SUPPLY);
- th.assert_trees(&HOLDERS);
-
- let _dao_tokens = &th.holders.get(&Holder::Dao).unwrap().unspent_money_coins;
- assert!(_dao_tokens.len() == 1);
- assert!(_dao_tokens[0].note.token_id == *DARK_TOKEN_ID);
- assert!(_dao_tokens[0].note.value == DRK_TOKEN_SUPPLY[0]);
-
- current_block_height += 1;
-
- // ====================
- // Dao::Mint
- // Create the DAO bulla
- // ====================
- info!("[Dao] Building DAO mint tx");
- let (dao_mint_tx, dao_mint_params, fee_params) = th
- .dao_mint(
- &Holder::Alice,
- &dao,
- &dao_notes_keypair.secret,
- &dao_proposer_keypair.secret,
- &dao_proposals_keypair.secret,
- &dao_votes_keypair.secret,
- &dao_exec_keypair.secret,
- &dao_early_exec_keypair.secret,
- current_block_height,
- )
+ th.token_mint_with_blind_to_all(BOB_GOV_SUPPLY, &Alice, &Bob, gov_token_blind, height)
.await?;
- for holder in &HOLDERS {
- info!("[{holder:?}] Executing DAO Mint tx");
- th.execute_dao_mint_tx(
- holder,
- dao_mint_tx.clone(),
- &dao_mint_params,
- &fee_params,
- current_block_height,
- true,
- )
- .await?;
- }
+ assert_eq!(th.coins(&Bob).len(), 1);
+ assert_eq!(th.balance(&Bob, gov_token_id), BOB_GOV_SUPPLY);
- th.assert_trees(&HOLDERS);
+ th.token_mint_with_blind_to_all(
+ CHARLIE_GOV_SUPPLY,
+ &Alice,
+ &Charlie,
+ gov_token_blind,
+ height,
+ )
+ .await?;
- current_block_height += 1;
+ assert_eq!(th.coins(&Charlie).len(), 1);
+ assert_eq!(th.balance(&Charlie, gov_token_id), CHARLIE_GOV_SUPPLY);
+ height += 1;
- // ======================================
- // Mint the governance token to 3 holders
- // ======================================
- info!("[Dao] Building governance token mint tx for Alice");
- let (a_token_mint_tx, a_token_mint_params, a_auth_token_mint_params, a_fee_params) = th
- .token_mint(
- ALICE_GOV_SUPPLY,
- &Holder::Alice,
- &Holder::Alice,
- gov_token_blind,
- None,
- None,
- current_block_height,
- )
- .await?;
-
- for holder in &HOLDERS {
- info!("[{holder:?}] Executing governance token mint tx for Alice");
- th.execute_token_mint_tx(
- holder,
- a_token_mint_tx.clone(),
- &a_token_mint_params,
- &a_auth_token_mint_params,
- &a_fee_params,
- current_block_height,
- true,
- )
- .await?;
- }
-
- th.assert_trees(&HOLDERS);
-
- let _alice_tokens = &th.holders.get(&Holder::Alice).unwrap().unspent_money_coins;
- assert!(_alice_tokens.len() == 1);
- assert!(_alice_tokens[0].note.token_id == gov_token_id);
- assert!(_alice_tokens[0].note.value == ALICE_GOV_SUPPLY);
-
- info!("[Dao] Building governance token mint tx for Bob");
- let (b_token_mint_tx, b_token_mint_params, b_auth_token_mint_params, b_fee_params) = th
- .token_mint(
- BOB_GOV_SUPPLY,
- &Holder::Alice,
- &Holder::Bob,
- gov_token_blind,
- None,
- None,
- current_block_height,
- )
- .await?;
-
- for holder in &HOLDERS {
- info!("[{holder:?}] Executing governance token mint tx for Bob");
- th.execute_token_mint_tx(
- holder,
- b_token_mint_tx.clone(),
- &b_token_mint_params,
- &b_auth_token_mint_params,
- &b_fee_params,
- current_block_height,
- true,
- )
- .await?;
- }
-
- th.assert_trees(&HOLDERS);
-
- let _bob_tokens = &th.holders.get(&Holder::Bob).unwrap().unspent_money_coins;
- assert!(_bob_tokens.len() == 1);
- assert!(_bob_tokens[0].note.token_id == gov_token_id);
- assert!(_bob_tokens[0].note.value == BOB_GOV_SUPPLY);
-
- info!("[Dao] Building governance token mint tx for Charlie");
- let (c_token_mint_tx, c_token_mint_params, c_auth_token_mint_params, c_fee_params) = th
- .token_mint(
- CHARLIE_GOV_SUPPLY,
- &Holder::Alice,
- &Holder::Charlie,
- gov_token_blind,
- None,
- None,
- current_block_height,
- )
- .await?;
-
- for holder in &HOLDERS {
- info!("[{holder:?}] Executing governance token mint tx for Charlie");
- th.execute_token_mint_tx(
- holder,
- c_token_mint_tx.clone(),
- &c_token_mint_params,
- &c_auth_token_mint_params,
- &c_fee_params,
- current_block_height,
- true,
- )
- .await?;
- }
-
- th.assert_trees(&HOLDERS);
-
- let _charlie_tokens = &th.holders.get(&Holder::Charlie).unwrap().unspent_money_coins;
- assert!(_charlie_tokens.len() == 1);
- assert!(_charlie_tokens[0].note.token_id == gov_token_id);
- assert!(_charlie_tokens[0].note.value == CHARLIE_GOV_SUPPLY);
-
- current_block_height += 1;
-
- // We can add whatever we want in here, even arbitrary text
- // It's up to the auth module to decide what to do with it.
let user_data = pallas::Base::ZERO;
- // ============================
- // Execute proposals test cases
- // ============================
- info!("[Dao] DAO transfer proposal tx test case");
+ // ============================================
+ // 4. Transfer proposal (normal execution)
+ // ============================================
+ info!("DAO transfer proposal — normal execution");
execute_transfer_proposal(
&mut th,
&dao,
- &dao_proposer_keypair.secret,
- &dao_votes_keypair.secret,
- &dao_exec_keypair.secret,
+ &dao_keys,
&None,
user_data,
- &mut current_block_height,
+ &mut height,
0,
TRANSFER_PROPOSAL_AMOUNT,
TRANSFER_PROPOSAL_AMOUNT,
)
.await?;
- info!("[Dao] DAO early execution transfer proposal tx test case");
+ // ============================================
+ // 5. Transfer proposal (early execution)
+ // ============================================
+ info!("DAO transfer proposal — early execution");
execute_transfer_proposal(
&mut th,
&dao,
- &dao_proposer_keypair.secret,
- &dao_votes_keypair.secret,
- &dao_exec_keypair.secret,
- &Some(dao_early_exec_keypair.secret),
+ &dao_keys,
+ &Some(dao_keys.early_exec.secret),
user_data,
- &mut current_block_height,
+ &mut height,
1,
TRANSFER_PROPOSAL_AMOUNT,
TRANSFER_PROPOSAL_AMOUNT * 2,
)
.await?;
- info!("[Dao] DAO generic proposal tx test case");
+ // ============================================
+ // 6. Generic proposal (normal execution)
+ // ============================================
+ info!("DAO generic proposal — normal execution");
+ execute_generic_proposal(&mut th, &dao, &dao_keys, &None, user_data, &mut height).await?;
+
+ // Mint more governance tokens to refresh the merkle tree snapshot
+ // before the next proposal round.
+ info!("Minting additional governance tokens (snapshot refresh)");
+ th.token_mint_with_blind_to_all(ALICE_GOV_SUPPLY, &Alice, &Alice, gov_token_blind, height)
+ .await?;
+ height += 1;
+
+ // ============================================
+ // 7. Generic proposal (early execution)
+ // ============================================
+ info!("DAO generic proposal — early execution");
execute_generic_proposal(
&mut th,
&dao,
- &dao_proposer_keypair.secret,
- &dao_votes_keypair.secret,
- &dao_exec_keypair.secret,
- &None,
+ &dao_keys,
+ &Some(dao_keys.early_exec.secret),
user_data,
- &mut current_block_height,
- )
- .await?;
-
- // Now we will execute a random money transaction,
- // to update our merkle tree so our snapshot is fresh.
- info!("[Dao] Building governance token mint tx for Alice");
- let (a_token_mint_tx, a_token_mint_params, a_auth_token_mint_params, a_fee_params) = th
- .token_mint(
- ALICE_GOV_SUPPLY,
- &Holder::Alice,
- &Holder::Alice,
- gov_token_blind,
- None,
- None,
- current_block_height,
- )
- .await?;
-
- for holder in &HOLDERS {
- info!("[{holder:?}] Executing governance token mint tx for Alice");
- th.execute_token_mint_tx(
- holder,
- a_token_mint_tx.clone(),
- &a_token_mint_params,
- &a_auth_token_mint_params,
- &a_fee_params,
- current_block_height,
- true,
- )
- .await?;
- }
-
- th.assert_trees(&HOLDERS);
-
- current_block_height += 1;
-
- // Now we can continue our test cases
- info!("[Dao] DAO early execution generic proposal tx test case");
- execute_generic_proposal(
- &mut th,
- &dao,
- &dao_proposer_keypair.secret,
- &dao_votes_keypair.secret,
- &dao_exec_keypair.secret,
- &Some(dao_early_exec_keypair.secret),
- user_data,
- &mut current_block_height,
+ &mut height,
)
.await?;
@@ -401,32 +256,28 @@ fn integration_test() -> Result<()> {
})
}
-/// Test case:
-/// Generate a transfer proposal and execute it after voting passes.
+// =========================================================================
+// Proposal execution helpers
+// =========================================================================
+
+/// Execute a transfer proposal: propose → vote → (wait if not early) → exec.
+///
+/// Asserts that Rachel received the transfer and the DAO treasury decreased
+/// by `dao_treasury_decrease`.
#[allow(clippy::too_many_arguments)]
async fn execute_transfer_proposal(
th: &mut TestHarness,
dao: &Dao,
- dao_proposer_secret_key: &SecretKey,
- dao_votes_secret_key: &SecretKey,
- dao_exec_secret_key: &SecretKey,
- dao_early_exec_secret_key: &Option,
+ keys: &DaoKeys,
+ early_exec_secret: &Option,
user_data: pallas::Base,
- current_block_height: &mut u32,
- transfer_token_index: usize,
+ height: &mut u32,
+ rachel_coin_index: usize,
transfer_amount: u64,
dao_treasury_decrease: u64,
) -> Result<()> {
- // ================
- // Dao::Propose
- // Propose the vote
- // ================
- info!("[Dao] Building DAO transfer proposal tx");
-
- // These coins are passed around to all DAO members who verify its validity
- // They also check hashing them equals the proposal_commit
let proposal_coinattrs = vec![CoinAttributes {
- public_key: th.holders.get(&Holder::Rachel).unwrap().keypair.public,
+ public_key: th.wallet(&Holder::Rachel).keypair.public,
value: transfer_amount,
token_id: *DARK_TOKEN_ID,
spend_hook: FuncId::none(),
@@ -434,320 +285,159 @@ async fn execute_transfer_proposal(
blind: Blind::random(&mut OsRng),
}];
- // Grab creation blockwindow
- let block_target =
- th.holders.get_mut(&Holder::Dao).unwrap().validator.read().await.consensus.module.target;
- let creation_blockwindow = blockwindow(*current_block_height, block_target);
+ // Grab creation blockwindow for expiry calculation
+ let block_target = th.wallet(&Holder::Dao).validator.read().await.consensus.module.target;
+ let creation_blockwindow = blockwindow(*height, block_target);
- let (tx, params, fee_params, proposal_info) = th
- .dao_propose_transfer(
+ // Propose
+ info!("Building transfer proposal");
+ let proposal_info = th
+ .dao_propose_transfer_to_all(
&Holder::Alice,
&proposal_coinattrs,
user_data,
dao,
- dao_proposer_secret_key,
- *current_block_height,
+ &keys.proposer.secret,
+ *height,
PROPOSAL_DURATION_BLOCKWINDOW,
)
.await?;
+ *height += 1;
- for holder in &HOLDERS {
- info!("[{holder:?}] Executing DAO transfer proposal tx");
- th.execute_dao_propose_tx(
- holder,
- tx.clone(),
- ¶ms,
- &fee_params,
- *current_block_height,
- true,
- )
- .await?;
- }
- th.assert_trees(&HOLDERS);
- *current_block_height += 1;
+ // Vote: Alice=yes, Bob=no, Charlie=yes → 2/3 majority
+ let (total_yes, total_all, yes_blind, all_blind) =
+ run_vote_round(th, dao, keys, &proposal_info, *height).await?;
- // =====================================
- // Dao::Vote
- // Proposal is accepted. Start the vote.
- // =====================================
- info!("[Alice] Building transfer vote tx (yes)");
- let (alice_vote_tx, alice_vote_params, alice_vote_fee_params) =
- th.dao_vote(&Holder::Alice, true, dao, &proposal_info, *current_block_height).await?;
-
- info!("[Bob] Building transfer vote tx (no)");
- let (bob_vote_tx, bob_vote_params, bob_vote_fee_params) =
- th.dao_vote(&Holder::Bob, false, dao, &proposal_info, *current_block_height).await?;
-
- info!("[Charlie] Building transfer vote tx (yes)");
- let (charlie_vote_tx, charlie_vote_params, charlie_vote_fee_params) =
- th.dao_vote(&Holder::Charlie, true, dao, &proposal_info, *current_block_height).await?;
-
- for holder in &HOLDERS {
- info!("[{holder:?}] Executing Alice transfer vote tx");
- th.execute_dao_vote_tx(
- holder,
- alice_vote_tx.clone(),
- &alice_vote_fee_params,
- *current_block_height,
- true,
- )
- .await?;
-
- info!("[{holder:?}] Executing Bob transfer vote tx");
- th.execute_dao_vote_tx(
- holder,
- bob_vote_tx.clone(),
- &bob_vote_fee_params,
- *current_block_height,
- true,
- )
- .await?;
-
- info!("[{holder:?}] Executing Charlie transfer vote tx");
- th.execute_dao_vote_tx(
- holder,
- charlie_vote_tx.clone(),
- &charlie_vote_fee_params,
- *current_block_height,
- true,
- )
- .await?;
- }
- th.assert_trees(&HOLDERS);
-
- // Gather and decrypt all generic vote notes
- let vote_note_1 = alice_vote_params.note.decrypt_unsafe(dao_votes_secret_key).unwrap();
- let vote_note_2 = bob_vote_params.note.decrypt_unsafe(dao_votes_secret_key).unwrap();
- let vote_note_3 = charlie_vote_params.note.decrypt_unsafe(dao_votes_secret_key).unwrap();
-
- // Count the votes
- let (total_yes_vote_value, total_all_vote_value, total_yes_vote_blind, total_all_vote_blind) =
- count_votes(&[
- (vote_note_1, alice_vote_params),
- (vote_note_2, bob_vote_params),
- (vote_note_3, charlie_vote_params),
- ]);
-
- // Wait until proposal has expired
- if dao_early_exec_secret_key.is_none() {
- let mut current_blockwindow = creation_blockwindow;
- while current_blockwindow <= creation_blockwindow + PROPOSAL_DURATION_BLOCKWINDOW {
- *current_block_height += 1;
- current_blockwindow = blockwindow(*current_block_height, block_target);
- }
+ // Wait for proposal expiry (unless early exec)
+ if early_exec_secret.is_none() {
+ wait_for_proposal_expiry(height, creation_blockwindow, block_target);
}
- // ================
- // Dao::Exec
- // Execute the vote
- // ================
- info!("[Dao] Building transfer Dao::Exec tx");
- let (exec_tx, xfer_params, exec_fee_params) = th
- .dao_exec_transfer(
- &Holder::Alice,
- dao,
- dao_exec_secret_key,
- dao_early_exec_secret_key,
- &proposal_info,
- proposal_coinattrs,
- total_yes_vote_value,
- total_all_vote_value,
- total_yes_vote_blind,
- total_all_vote_blind,
- *current_block_height,
- )
- .await?;
+ // Execute
+ info!("Executing transfer proposal");
+ th.dao_exec_transfer_to_all(
+ &Holder::Alice,
+ dao,
+ &keys.exec.secret,
+ early_exec_secret,
+ &proposal_info,
+ proposal_coinattrs,
+ total_yes,
+ total_all,
+ yes_blind,
+ all_blind,
+ *height,
+ )
+ .await?;
+ *height += 1;
- for holder in &HOLDERS {
- info!("[{holder:?}] Executing transfer Dao::Exec tx");
- th.execute_dao_exec_tx(
- holder,
- exec_tx.clone(),
- Some(&xfer_params),
- &exec_fee_params,
- *current_block_height,
- true,
- )
- .await?;
- }
- th.assert_trees(&HOLDERS);
- *current_block_height += 1;
+ // Assert Rachel received the transfer
+ assert_eq!(th.coins(&Holder::Rachel)[rachel_coin_index].note.value, transfer_amount);
+ assert_eq!(th.coins(&Holder::Rachel)[rachel_coin_index].note.token_id, *DARK_TOKEN_ID);
- let rachel_wallet = th.holders.get(&Holder::Rachel).unwrap();
- assert!(rachel_wallet.unspent_money_coins[transfer_token_index].note.value == transfer_amount);
- assert!(
- rachel_wallet.unspent_money_coins[transfer_token_index].note.token_id == *DARK_TOKEN_ID
- );
-
- let dao_wallet = th.holders.get(&Holder::Dao).unwrap();
- assert!(
- dao_wallet.unspent_money_coins[0].note.value == DRK_TOKEN_SUPPLY[0] - dao_treasury_decrease
- );
- assert!(dao_wallet.unspent_money_coins[0].note.token_id == *DARK_TOKEN_ID);
+ // Assert DAO treasury decreased correctly
+ assert_eq!(th.coins(&Holder::Dao)[0].note.value, DRK_TOKEN_SUPPLY[0] - dao_treasury_decrease);
+ assert_eq!(th.coins(&Holder::Dao)[0].note.token_id, *DARK_TOKEN_ID);
Ok(())
}
-/// Test case:
-/// Generate a generic proposal and execute it after voting passes.
-#[allow(clippy::too_many_arguments)]
+/// Execute a generic proposal: propose → vote → (wait if not early) → exec.
async fn execute_generic_proposal(
th: &mut TestHarness,
dao: &Dao,
- dao_proposer_secret_key: &SecretKey,
- dao_votes_secret_key: &SecretKey,
- dao_exec_secret_key: &SecretKey,
- dao_early_exec_secret_key: &Option,
+ keys: &DaoKeys,
+ early_exec_secret: &Option,
user_data: pallas::Base,
- current_block_height: &mut u32,
+ height: &mut u32,
) -> Result<()> {
- // ================
- // Dao::Propose
- // Propose the vote
- // ================
- info!("[Dao] Building DAO generic proposal tx");
+ // Grab creation blockwindow for expiry calculation
+ let block_target = th.wallet(&Holder::Dao).validator.read().await.consensus.module.target;
+ let creation_blockwindow = blockwindow(*height, block_target);
- // Grab creation blockwindow
- let block_target =
- th.holders.get_mut(&Holder::Dao).unwrap().validator.read().await.consensus.module.target;
- let creation_blockwindow = blockwindow(*current_block_height, block_target);
-
- let (tx, params, fee_params, proposal_info) = th
- .dao_propose_generic(
+ // Propose
+ info!("Building generic proposal");
+ let proposal_info = th
+ .dao_propose_generic_to_all(
&Holder::Alice,
user_data,
dao,
- dao_proposer_secret_key,
- *current_block_height,
+ &keys.proposer.secret,
+ *height,
PROPOSAL_DURATION_BLOCKWINDOW,
)
.await?;
+ *height += 1;
- for holder in &HOLDERS {
- info!("[{holder:?}] Executing DAO generic proposal tx");
- th.execute_dao_propose_tx(
- holder,
- tx.clone(),
- ¶ms,
- &fee_params,
- *current_block_height,
- true,
- )
- .await?;
- }
- th.assert_trees(&HOLDERS);
- *current_block_height += 1;
+ // Vote: Alice=yes, Bob=no, Charlie=yes → 2/3 majority
+ let (total_yes, total_all, yes_blind, all_blind) =
+ run_vote_round(th, dao, keys, &proposal_info, *height).await?;
- // =====================================
- // Dao::Vote
- // Proposal is accepted. Start the vote.
- // =====================================
- info!("[Alice] Building generic vote tx (yes)");
- let (alice_vote_tx, alice_vote_params, alice_vote_fee_params) =
- th.dao_vote(&Holder::Alice, true, dao, &proposal_info, *current_block_height).await?;
-
- info!("[Bob] Building generic vote tx (no)");
- let (bob_vote_tx, bob_vote_params, bob_vote_fee_params) =
- th.dao_vote(&Holder::Bob, false, dao, &proposal_info, *current_block_height).await?;
-
- info!("[Charlie] Building generic vote tx (no)");
- let (charlie_vote_tx, charlie_vote_params, charlie_vote_fee_params) =
- th.dao_vote(&Holder::Charlie, true, dao, &proposal_info, *current_block_height).await?;
-
- for holder in &HOLDERS {
- info!("[{holder:?}] Executing Alice generic vote tx");
- th.execute_dao_vote_tx(
- holder,
- alice_vote_tx.clone(),
- &alice_vote_fee_params,
- *current_block_height,
- true,
- )
- .await?;
-
- info!("[{holder:?}] Executing Bob generic vote tx");
- th.execute_dao_vote_tx(
- holder,
- bob_vote_tx.clone(),
- &bob_vote_fee_params,
- *current_block_height,
- true,
- )
- .await?;
-
- info!("[{holder:?}] Executing Charlie generic vote tx");
- th.execute_dao_vote_tx(
- holder,
- charlie_vote_tx.clone(),
- &charlie_vote_fee_params,
- *current_block_height,
- true,
- )
- .await?;
- }
- th.assert_trees(&HOLDERS);
-
- // Gather and decrypt all generic vote notes
- let vote_note_1 = alice_vote_params.note.decrypt_unsafe(dao_votes_secret_key).unwrap();
- let vote_note_2 = bob_vote_params.note.decrypt_unsafe(dao_votes_secret_key).unwrap();
- let vote_note_3 = charlie_vote_params.note.decrypt_unsafe(dao_votes_secret_key).unwrap();
-
- // Count the votes
- let (total_yes_vote_value, total_all_vote_value, total_yes_vote_blind, total_all_vote_blind) =
- count_votes(&[
- (vote_note_1, alice_vote_params),
- (vote_note_2, bob_vote_params),
- (vote_note_3, charlie_vote_params),
- ]);
-
- // Wait until proposal has expired
- if dao_early_exec_secret_key.is_none() {
- let mut current_blockwindow = creation_blockwindow;
- while current_blockwindow <= creation_blockwindow + PROPOSAL_DURATION_BLOCKWINDOW {
- *current_block_height += 1;
- current_blockwindow = blockwindow(*current_block_height, block_target);
- }
+ // Wait for proposal expiry (unless early exec)
+ if early_exec_secret.is_none() {
+ wait_for_proposal_expiry(height, creation_blockwindow, block_target);
}
- // ================
- // Dao::Exec
- // Execute the vote
- // ================
- info!("[Dao] Building generic Dao::Exec tx");
- let (exec_tx, exec_fee_params) = th
- .dao_exec_generic(
- &Holder::Alice,
- dao,
- dao_exec_secret_key,
- dao_early_exec_secret_key,
- &proposal_info,
- total_yes_vote_value,
- total_all_vote_value,
- total_yes_vote_blind,
- total_all_vote_blind,
- *current_block_height,
- )
- .await?;
-
- for holder in &HOLDERS {
- info!("[{holder:?}] Executing generic Dao::Exec tx");
- th.execute_dao_exec_tx(
- holder,
- exec_tx.clone(),
- None,
- &exec_fee_params,
- *current_block_height,
- true,
- )
- .await?;
- }
- th.assert_trees(&HOLDERS);
- *current_block_height += 1;
+ // Execute
+ info!("Executing generic proposal");
+ th.dao_exec_generic_to_all(
+ &Holder::Alice,
+ dao,
+ &keys.exec.secret,
+ early_exec_secret,
+ &proposal_info,
+ total_yes,
+ total_all,
+ yes_blind,
+ all_blind,
+ *height,
+ )
+ .await?;
+ *height += 1;
Ok(())
}
-/// Auxiliary function to count proposal votes.
+// =========================================================================
+// Shared vote/count/wait utilities
+// =========================================================================
+
+/// Run a complete vote round: Alice=yes, Bob=no, Charlie=yes.
+/// Returns (total_yes_value, total_all_value, yes_blind, all_blind).
+async fn run_vote_round(
+ th: &mut TestHarness,
+ dao: &Dao,
+ keys: &DaoKeys,
+ proposal: &darkfi_dao_contract::model::DaoProposal,
+ height: u32,
+) -> Result<(u64, u64, ScalarBlind, ScalarBlind)> {
+ info!("Vote round: Alice=yes, Bob=no, Charlie=yes");
+
+ let alice_vote = th.dao_vote_to_all(&Holder::Alice, true, dao, proposal, height).await?;
+ let bob_vote = th.dao_vote_to_all(&Holder::Bob, false, dao, proposal, height).await?;
+ let charlie_vote = th.dao_vote_to_all(&Holder::Charlie, true, dao, proposal, height).await?;
+
+ // Decrypt vote notes and tally
+ let note_a = alice_vote.note.decrypt_unsafe(&keys.votes.secret).unwrap();
+ let note_b = bob_vote.note.decrypt_unsafe(&keys.votes.secret).unwrap();
+ let note_c = charlie_vote.note.decrypt_unsafe(&keys.votes.secret).unwrap();
+
+ Ok(count_votes(&[(note_a, alice_vote), (note_b, bob_vote), (note_c, charlie_vote)]))
+}
+
+/// Advance `height` past the proposal expiry blockwindow.
+fn wait_for_proposal_expiry(height: &mut u32, creation_blockwindow: u64, block_target: u32) {
+ let mut current_bw = creation_blockwindow;
+ while current_bw <= creation_blockwindow + PROPOSAL_DURATION_BLOCKWINDOW {
+ *height += 1;
+ current_bw = blockwindow(*height, block_target);
+ }
+}
+
+/// Tally votes from decrypted vote notes.
+///
+/// Verifies Pedersen commitment consistency between private tallies
+/// and public aggregate commitments.
fn count_votes(
votes: &[([pallas::Base; 4], DaoVoteParams)],
) -> (u64, u64, ScalarBlind, ScalarBlind) {
@@ -758,12 +448,7 @@ fn count_votes(
let mut total_all_vote_blind = Blind::ZERO;
for (i, (note, params)) in votes.iter().enumerate() {
- // Note format: [
- // vote_option,
- // yes_vote_blind,
- // all_vote_value_fp,
- // all_vote_blind,
- // ]
+ // Note layout: [vote_option, yes_vote_blind, all_vote_value, all_vote_blind]
let vote_option = fp_to_u64(note[0]).unwrap();
let yes_vote_blind = Blind(fp_mod_fv(note[1]));
let all_vote_value = fp_to_u64(note[2]).unwrap();
@@ -773,36 +458,28 @@ fn count_votes(
total_yes_vote_blind += yes_vote_blind;
total_all_vote_blind += all_vote_blind;
- // Update private values
- // vote_option is either 0 or 1
let yes_vote_value = vote_option * all_vote_value;
total_yes_vote_value += yes_vote_value;
total_all_vote_value += all_vote_value;
- // Update public values
let yes_vote_commit = params.yes_vote_commit;
let all_vote_commit = params.inputs.iter().map(|i| i.vote_commit).sum();
let blind_vote = DaoBlindAggregateVote { yes_vote_commit, all_vote_commit };
blind_total_vote.aggregate(blind_vote);
- // Just for the debug
- let vote_result = match vote_option != 0 {
- true => "yes",
- false => "no",
- };
- info!("Voter {i} voted {vote_result} with {all_vote_value} tokens in vote");
+ let label = if vote_option != 0 { "yes" } else { "no" };
+ info!("Voter {i} voted {label} with {all_vote_value} tokens");
}
info!("Vote outcome = {total_yes_vote_value} / {total_all_vote_value}");
- assert!(
- blind_total_vote.all_vote_commit ==
- pedersen_commitment_u64(total_all_vote_value, total_all_vote_blind)
+ assert_eq!(
+ blind_total_vote.all_vote_commit,
+ pedersen_commitment_u64(total_all_vote_value, total_all_vote_blind)
);
-
- assert!(
- blind_total_vote.yes_vote_commit ==
- pedersen_commitment_u64(total_yes_vote_value, total_yes_vote_blind)
+ assert_eq!(
+ blind_total_vote.yes_vote_commit,
+ pedersen_commitment_u64(total_yes_vote_value, total_yes_vote_blind)
);
(total_yes_vote_value, total_all_vote_value, total_yes_vote_blind, total_all_vote_blind)