diff --git a/Cargo.lock b/Cargo.lock index 7cc7668fe..4aaadc98f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1641,6 +1641,7 @@ dependencies = [ "bs58", "chacha20poly1305", "darkfi", + "darkfi-contract-test-harness", "darkfi-money-contract", "darkfi-sdk", "darkfi-serial", diff --git a/src/contract/dao/Cargo.toml b/src/contract/dao/Cargo.toml index 3400ca495..19f816995 100644 --- a/src/contract/dao/Cargo.toml +++ b/src/contract/dao/Cargo.toml @@ -27,10 +27,10 @@ rand = { version = "0.8.5", optional = true } [dev-dependencies] async-std = {version = "1.12.0", features = ["attributes"]} darkfi = {path = "../../../", features = ["tx", "blockchain"]} -darkfi-money-contract = { path = "../money", features = ["client", "no-entrypoint"] } +darkfi-money-contract = {path = "../money", features = ["client", "no-entrypoint"]} simplelog = "0.12.1" sled = "0.34.7" -#sqlx = {version = "0.6.3", features = ["runtime-async-std-rustls", "sqlite"]} +darkfi-contract-test-harness = {path = "../test-harness"} # We need to disable random using "custom" which makes the crate a noop # so the wasm32-unknown-unknown target is enabled. diff --git a/src/contract/dao/src/client/exec.rs b/src/contract/dao/src/client/exec.rs index 39fb9e904..89da4ff44 100644 --- a/src/contract/dao/src/client/exec.rs +++ b/src/contract/dao/src/client/exec.rs @@ -32,7 +32,7 @@ use darkfi::{ }; use super::{DaoInfo, DaoProposalInfo}; -use crate::model::{DaoBlindAggregateVote, DaoExecParams}; +use crate::model::{DaoBlindAggregateVote, DaoExecParams, DaoProposalBulla}; pub struct DaoExecCall { pub proposal: DaoProposalInfo, @@ -85,14 +85,14 @@ impl DaoExecCall { self.dao.bulla_blind, ]); - let proposal_bulla = poseidon_hash::<6>([ + let proposal_bulla = DaoProposalBulla::from(poseidon_hash::<6>([ proposal_dest_x, proposal_dest_y, proposal_amount, self.proposal.token_id.inner(), dao_bulla, self.proposal.blind, - ]); + ])); let coin_0 = poseidon_hash::<7>([ proposal_dest_x, @@ -157,7 +157,7 @@ impl DaoExecCall { debug!(target: "dao", "proposal_bulla: {:?}", proposal_bulla); let public_inputs = vec![ - proposal_bulla, + proposal_bulla.inner(), coin_0, coin_1, *yes_vote_commit_coords.x(), diff --git a/src/contract/dao/src/client/propose.rs b/src/contract/dao/src/client/propose.rs index 18f4105d0..f48678526 100644 --- a/src/contract/dao/src/client/propose.rs +++ b/src/contract/dao/src/client/propose.rs @@ -34,7 +34,7 @@ use darkfi::{ Result, }; -use crate::model::{DaoProposeParams, DaoProposeParamsInput}; +use crate::model::{DaoProposalBulla, DaoProposeParams, DaoProposeParamsInput}; use super::DaoInfo; @@ -195,14 +195,14 @@ impl DaoProposeCall { let dao_leaf_position: u64 = self.dao_leaf_position.into(); - let proposal_bulla = poseidon_hash::<6>([ + let proposal_bulla = DaoProposalBulla::from(poseidon_hash::<6>([ proposal_dest_x, proposal_dest_y, proposal_amount, self.proposal.token_id.inner(), dao_bulla, self.proposal.blind, - ]); + ])); let prover_witnesses = vec![ // Proposers total number of gov tokens @@ -231,7 +231,7 @@ impl DaoProposeCall { let public_inputs = vec![ token_commit, self.dao_merkle_root.inner(), - proposal_bulla, + proposal_bulla.inner(), *total_funds_coords.x(), *total_funds_coords.y(), ]; diff --git a/src/contract/dao/src/client/vote.rs b/src/contract/dao/src/client/vote.rs index 5513b3188..cb4aa5aae 100644 --- a/src/contract/dao/src/client/vote.rs +++ b/src/contract/dao/src/client/vote.rs @@ -36,7 +36,7 @@ use darkfi::{ }; use super::{DaoInfo, DaoProposalInfo}; -use crate::model::{DaoVoteParams, DaoVoteParamsInput}; +use crate::model::{DaoProposalBulla, DaoVoteParams, DaoVoteParamsInput}; #[derive(SerialEncodable, SerialDecodable)] pub struct DaoVoteNote { @@ -196,14 +196,14 @@ impl DaoVoteCall { self.dao.bulla_blind, ]); - let proposal_bulla = poseidon_hash::<6>([ + let proposal_bulla = DaoProposalBulla::from(poseidon_hash::<6>([ proposal_dest_x, proposal_dest_y, proposal_amount, self.proposal.token_id.inner(), dao_bulla, self.proposal.blind, - ]); + ])); let vote_option = self.vote_option as u64; assert!(vote_option == 0 || vote_option == 1); @@ -243,7 +243,7 @@ impl DaoVoteCall { let public_inputs = vec![ token_commit, - proposal_bulla, + proposal_bulla.inner(), // this should be a value commit?? *yes_vote_commit_coords.x(), *yes_vote_commit_coords.y(), diff --git a/src/contract/dao/src/entrypoint/exec.rs b/src/contract/dao/src/entrypoint/exec.rs index e691b6587..21db13785 100644 --- a/src/contract/dao/src/entrypoint/exec.rs +++ b/src/contract/dao/src/entrypoint/exec.rs @@ -55,7 +55,7 @@ pub(crate) fn dao_exec_get_metadata( zk_public_inputs.push(( DAO_CONTRACT_ZKAS_DAO_EXEC_NS.to_string(), vec![ - params.proposal, + params.proposal.inner(), params.coin_0.inner(), params.coin_1.inner(), *yes_vote_coords.x(), @@ -110,8 +110,11 @@ pub(crate) fn dao_exec_process_instruction( // Checks // ====== // 1. Check coins in MoneyTransfer are the same as our coin 0 and coin 1 - if mt_params.outputs[0].coin != params.coin_0 || - mt_params.outputs[1].coin != params.coin_1 || + // * outputs[0] is the change returned to DAO + // * outputs[1] is the value being sent to the recipient + // (This is how it's done in the client API of Money::Transfer) + if mt_params.outputs[0].coin != params.coin_1 || + mt_params.outputs[1].coin != params.coin_0 || mt_params.outputs.len() != 2 { msg!("[Dao::Exec] Error: Coin commitments mismatch"); diff --git a/src/contract/dao/src/entrypoint/propose.rs b/src/contract/dao/src/entrypoint/propose.rs index 1530f79ab..65a25c276 100644 --- a/src/contract/dao/src/entrypoint/propose.rs +++ b/src/contract/dao/src/entrypoint/propose.rs @@ -85,7 +85,7 @@ pub(crate) fn dao_propose_get_metadata( vec![ params.token_commit, params.dao_merkle_root.inner(), - params.proposal_bulla, + params.proposal_bulla.inner(), *total_funds_coords.x(), *total_funds_coords.y(), ], diff --git a/src/contract/dao/src/entrypoint/vote.rs b/src/contract/dao/src/entrypoint/vote.rs index ee35cb02c..d89c9589e 100644 --- a/src/contract/dao/src/entrypoint/vote.rs +++ b/src/contract/dao/src/entrypoint/vote.rs @@ -90,7 +90,7 @@ pub(crate) fn dao_vote_get_metadata( DAO_CONTRACT_ZKAS_DAO_VOTE_MAIN_NS.to_string(), vec![ params.token_commit, - params.proposal_bulla, + params.proposal_bulla.inner(), *yes_vote_commit_coords.x(), *yes_vote_commit_coords.y(), *all_vote_commit_coords.x(), diff --git a/src/contract/dao/src/lib.rs b/src/contract/dao/src/lib.rs index 8a0f639dd..ef0483bb2 100644 --- a/src/contract/dao/src/lib.rs +++ b/src/contract/dao/src/lib.rs @@ -57,12 +57,6 @@ pub mod entrypoint; /// Client API for interaction with this smart contract pub mod client; -// TODO: Delete these and use the proper API -#[cfg(feature = "client")] -pub mod money_client; -#[cfg(feature = "client")] -pub mod wallet_cache; - // These are the different sled trees that will be created pub const DAO_CONTRACT_DB_INFO_TREE: &str = "dao_info"; pub const DAO_CONTRACT_DB_DAO_BULLAS: &str = "dao_bullas"; diff --git a/src/contract/dao/src/model.rs b/src/contract/dao/src/model.rs index 2d89ef503..57848ab93 100644 --- a/src/contract/dao/src/model.rs +++ b/src/contract/dao/src/model.rs @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +use core::str::FromStr; + use darkfi_money_contract::model::Coin; use darkfi_sdk::{ crypto::{note::AeadEncryptedNote, pasta_prelude::*, MerkleNode, Nullifier, PublicKey}, @@ -51,11 +53,53 @@ impl DaoBulla { } } -use core::str::FromStr; +impl std::hash::Hash for DaoBulla { + fn hash(&self, state: &mut H) { + state.write(&self.to_bytes()); + } +} + darkfi_sdk::fp_from_bs58!(DaoBulla); darkfi_sdk::fp_to_bs58!(DaoBulla); darkfi_sdk::ty_from_fp!(DaoBulla); +/// A `DaoProposalBulla` represented in the state +#[derive(Debug, Copy, Clone, Eq, PartialEq, SerialEncodable, SerialDecodable)] +pub struct DaoProposalBulla(pallas::Base); + +impl DaoProposalBulla { + /// Reference the raw inner base field element + pub fn inner(&self) -> pallas::Base { + self.0 + } + + /// Create a `DaoBulla` object from given bytes, erroring if the + /// input bytes are noncanonical. + pub fn from_bytes(x: [u8; 32]) -> Result { + match pallas::Base::from_repr(x).into() { + Some(v) => Ok(Self(v)), + None => Err(ContractError::IoError( + "Failed to instantiate DaoProposalBulla from bytes".to_string(), + )), + } + } + + /// Convert the `DaoBulla` type into 32 raw bytes + pub fn to_bytes(&self) -> [u8; 32] { + self.0.to_repr() + } +} + +impl std::hash::Hash for DaoProposalBulla { + fn hash(&self, state: &mut H) { + state.write(&self.to_bytes()); + } +} + +darkfi_sdk::fp_from_bs58!(DaoProposalBulla); +darkfi_sdk::fp_to_bs58!(DaoProposalBulla); +darkfi_sdk::ty_from_fp!(DaoProposalBulla); + /// Parameters for `Dao::Mint` #[derive(Debug, Copy, Clone, SerialEncodable, SerialDecodable)] pub struct DaoMintParams { @@ -80,7 +124,7 @@ pub struct DaoProposeParams { /// Token ID commitment for the proposal pub token_commit: pallas::Base, /// Bulla of the DAO proposal - pub proposal_bulla: pallas::Base, + pub proposal_bulla: DaoProposalBulla, /// Encrypted note pub note: AeadEncryptedNote, /// Inputs for the proposal @@ -102,7 +146,7 @@ pub struct DaoProposeParamsInput { #[derive(Debug, Copy, Clone, SerialEncodable, SerialDecodable)] pub struct DaoProposeUpdate { /// Minted proposal bulla - pub proposal_bulla: pallas::Base, + pub proposal_bulla: DaoProposalBulla, /// Snapshotted Merkle root in the Money state pub snapshot_root: MerkleNode, } @@ -124,7 +168,7 @@ pub struct DaoVoteParams { /// Token commitment for the vote inputs pub token_commit: pallas::Base, /// Proposal bulla being voted on - pub proposal_bulla: pallas::Base, + pub proposal_bulla: DaoProposalBulla, /// Commitment for yes votes pub yes_vote_commit: pallas::Point, /// Encrypted note @@ -150,7 +194,7 @@ pub struct DaoVoteParamsInput { #[derive(Debug, Clone, SerialEncodable, SerialDecodable)] pub struct DaoVoteUpdate { /// The proposal bulla being voted on - pub proposal_bulla: pallas::Base, + pub proposal_bulla: DaoProposalBulla, /// The updated proposal metadata pub proposal_metadata: DaoProposalMetadata, /// Vote nullifiers, @@ -188,7 +232,7 @@ impl Default for DaoBlindAggregateVote { #[derive(Debug, Copy, Clone, SerialEncodable, SerialDecodable)] pub struct DaoExecParams { /// The proposal bulla - pub proposal: pallas::Base, + pub proposal: DaoProposalBulla, /// The output coin for the proposal recipient pub coin_0: Coin, /// The output coin for the change returned to DAO @@ -203,5 +247,5 @@ pub struct DaoExecParams { #[derive(Debug, Copy, Clone, SerialEncodable, SerialDecodable)] pub struct DaoExecUpdate { /// The proposal bulla - pub proposal: pallas::Base, + pub proposal: DaoProposalBulla, } diff --git a/src/contract/dao/src/money_client.rs b/src/contract/dao/src/money_client.rs deleted file mode 100644 index f575ba8d3..000000000 --- a/src/contract/dao/src/money_client.rs +++ /dev/null @@ -1,227 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2023 Dyne.org foundation - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -//! TODO: This file should be deleted and the API from money::client -//! should be used directly. - -use darkfi::{ - zk::{Proof, ProvingKey}, - zkas::ZkBinary, - Result, -}; -use darkfi_sdk::{ - bridgetree, - crypto::{ - note::AeadEncryptedNote, pasta_prelude::*, MerkleNode, PublicKey, SecretKey, TokenId, - ValueBlind, - }, - pasta::pallas, -}; - -use rand::rngs::OsRng; - -use darkfi_money_contract::{ - client::{ - transfer_v1::{ - create_transfer_burn_proof, create_transfer_mint_proof, TransactionBuilderInputInfo, - TransactionBuilderOutputInfo, - }, - MoneyNote, - }, - model::{ClearInput, Input, MoneyTransferParamsV1, Output}, -}; - -pub struct TransferCall { - pub clear_inputs: Vec, - pub inputs: Vec, - pub outputs: Vec, -} - -pub struct TransferClearInput { - pub value: u64, - pub token_id: TokenId, - pub signature_secret: SecretKey, -} - -pub struct TransferInput { - pub leaf_position: bridgetree::Position, - pub merkle_path: Vec, - pub secret: SecretKey, - pub note: MoneyNote, - pub user_data_blind: pallas::Base, - pub value_blind: ValueBlind, - pub signature_secret: SecretKey, -} - -pub struct TransferOutput { - pub value: u64, - pub token_id: TokenId, - pub public: PublicKey, - pub serial: pallas::Base, - pub spend_hook: pallas::Base, - pub user_data: pallas::Base, -} - -impl TransferCall { - fn compute_remainder_blind( - clear_inputs: &[ClearInput], - input_blinds: &[ValueBlind], - output_blinds: &[ValueBlind], - ) -> ValueBlind { - let mut total = ValueBlind::zero(); - - for input in clear_inputs { - total += input.value_blind; - } - - for input_blind in input_blinds { - total += input_blind; - } - - for output_blind in output_blinds { - total -= output_blind; - } - - total - } - - pub fn make( - self, - mint_zkbin: &ZkBinary, - mint_pk: &ProvingKey, - burn_zkbin: &ZkBinary, - burn_pk: &ProvingKey, - ) -> Result<(MoneyTransferParamsV1, Vec)> { - assert!(self.clear_inputs.len() + self.inputs.len() > 0); - - let mut clear_inputs = vec![]; - let token_blind = ValueBlind::random(&mut OsRng); - for input in &self.clear_inputs { - let signature_public = PublicKey::from_secret(input.signature_secret); - let value_blind = ValueBlind::random(&mut OsRng); - - let clear_input = ClearInput { - value: input.value, - token_id: input.token_id, - value_blind, - token_blind, - signature_public, - }; - clear_inputs.push(clear_input); - } - - let mut proofs = vec![]; - let mut inputs = vec![]; - let mut input_blinds = vec![]; - - for input in self.inputs { - let value_blind = input.value_blind; - input_blinds.push(value_blind); - - // FIXME: Just an API hack - let _input = TransactionBuilderInputInfo { - leaf_position: input.leaf_position, - merkle_path: input.merkle_path, - secret: input.secret, - note: input.note, - }; - - let (proof, revealed) = create_transfer_burn_proof( - burn_zkbin, - burn_pk, - &_input, - value_blind, - token_blind, - input.user_data_blind, - input.signature_secret, - )?; - - proofs.push(proof); - - let input = Input { - value_commit: revealed.value_commit, - token_commit: revealed.token_commit, - nullifier: revealed.nullifier, - merkle_root: revealed.merkle_root, - spend_hook: revealed.spend_hook, - user_data_enc: revealed.user_data_enc, - signature_public: revealed.signature_public, - }; - inputs.push(input); - } - - let mut outputs = vec![]; - let mut output_blinds = vec![]; - // This value_blind calc assumes there will always be at least a single output - assert!(!self.outputs.is_empty()); - - for (i, output) in self.outputs.iter().enumerate() { - let value_blind = if i == self.outputs.len() - 1 { - Self::compute_remainder_blind(&clear_inputs, &input_blinds, &output_blinds) - } else { - ValueBlind::random(&mut OsRng) - }; - output_blinds.push(value_blind); - - let serial = output.serial; - - // FIXME: This is a hack between the two APIs - let _output = TransactionBuilderOutputInfo { - value: output.value, - token_id: output.token_id, - public_key: output.public, - }; - - let (proof, revealed) = create_transfer_mint_proof( - mint_zkbin, - mint_pk, - &_output, - value_blind, - token_blind, - serial, - output.spend_hook, - output.user_data, - )?; - - proofs.push(proof); - - let note = MoneyNote { - serial, - value: output.value, - token_id: output.token_id, - spend_hook: output.spend_hook, - user_data: output.user_data, - value_blind, - token_blind, - memo: Vec::new(), - }; - - let encrypted_note = AeadEncryptedNote::encrypt(¬e, &output.public, &mut OsRng)?; - - let output = Output { - value_commit: revealed.value_commit, - token_commit: revealed.token_commit, - coin: revealed.coin, - note: encrypted_note, - }; - outputs.push(output); - } - - Ok((MoneyTransferParamsV1 { clear_inputs, inputs, outputs }, proofs)) - } -} diff --git a/src/contract/dao/src/wallet_cache.rs b/src/contract/dao/src/wallet_cache.rs deleted file mode 100644 index 82582103d..000000000 --- a/src/contract/dao/src/wallet_cache.rs +++ /dev/null @@ -1,85 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2023 Dyne.org foundation - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -use darkfi_sdk::{ - bridgetree, - crypto::{note::AeadEncryptedNote, pasta_prelude::Field, MerkleNode, MerkleTree, SecretKey}, - pasta::pallas, -}; - -use darkfi_money_contract::{client::MoneyNote, model::Coin}; - -pub struct OwnCoin { - pub coin: Coin, - pub note: MoneyNote, - pub leaf_position: bridgetree::Position, -} - -pub struct WalletCache { - // Normally this would be a HashMap, but SecretKey is not Hash-able - // TODO: This can be HashableBase - cache: Vec<(SecretKey, Vec)>, - /// The entire Money Merkle tree state - pub tree: MerkleTree, -} - -impl Default for WalletCache { - fn default() -> Self { - Self::new() - } -} - -impl WalletCache { - pub fn new() -> Self { - let mut tree = MerkleTree::new(100); - tree.append(MerkleNode::from(pallas::Base::ZERO)); - let _ = tree.mark().unwrap(); - Self { cache: Vec::new(), tree } - } - - /// Must be called at the start to begin tracking received coins for this secret. - pub fn track(&mut self, secret: SecretKey) { - self.cache.push((secret, Vec::new())); - } - - /// Get all coins received by this secret key - /// track() must be called on this secret before calling this or the function will panic. - pub fn get_received(&mut self, secret: &SecretKey) -> Vec { - for (other_secret, own_coins) in self.cache.iter_mut() { - if *secret == *other_secret { - // clear own_coins vec, and return current contents - return std::mem::take(own_coins) - } - } - panic!("you forget to track() this secret!"); - } - - pub fn try_decrypt_note(&mut self, coin: Coin, ciphertext: &AeadEncryptedNote) { - // Add the new coins to the Merkle tree - self.tree.append(MerkleNode::from(coin.inner())); - - // Loop through all our secret keys... - for (secret, own_coins) in self.cache.iter_mut() { - // .. attempt to decrypt the note ... - if let Ok(note) = ciphertext.decrypt(secret) { - let leaf_position = self.tree.mark().expect("coin should be in tree"); - own_coins.push(OwnCoin { coin, note, leaf_position }); - } - } - } -} diff --git a/src/contract/dao/tests/integration.rs b/src/contract/dao/tests/integration.rs index ce98d7506..dcb770e28 100644 --- a/src/contract/dao/tests/integration.rs +++ b/src/contract/dao/tests/integration.rs @@ -16,702 +16,385 @@ * along with this program. If not, see . */ -use std::time::{Duration, Instant}; - -use darkfi::{tx::Transaction, Result}; +use darkfi::Result; +use darkfi_contract_test_harness::{init_logger, Holder, TestHarness}; +use darkfi_dao_contract::{ + client::{DaoInfo, DaoVoteNote}, + model::DaoBlindAggregateVote, +}; use darkfi_sdk::{ - crypto::{ - pasta_prelude::*, pedersen_commitment_u64, poseidon_hash, Keypair, MerkleNode, MerkleTree, - SecretKey, TokenId, DAO_CONTRACT_ID, DARK_TOKEN_ID, MONEY_CONTRACT_ID, - }, + crypto::{pasta_prelude::Field, pedersen_commitment_u64, DAO_CONTRACT_ID, DARK_TOKEN_ID}, pasta::pallas, - ContractCall, }; -use darkfi_serial::{Decodable, Encodable}; -use log::debug; +use log::info; use rand::rngs::OsRng; -use darkfi_dao_contract::{client, model, money_client, wallet_cache::WalletCache, DaoFunction}; - -use darkfi_money_contract::{ - client::token_mint_v1::TokenMintCallBuilder, - model::{Coin, MoneyTokenMintParamsV1, MoneyTransferParamsV1}, - MoneyFunction, -}; - -mod harness; -use harness::{init_logger, DaoTestHarness}; - -// TODO: Anonymity leaks in this proof of concept: -// -// * Vote updates are linked to the proposal_bulla -// * Nullifier of vote will link vote with the coin when it's spent - -// TODO: strategize and cleanup Result/Error usage -// TODO: fix up code doc -// TODO: db_* errors returned from runtime should be more specific. -// TODO: db_* functions should be consistently ordered -// TODO: migrate rest of func calls below to make() format and cleanup -// TODO: Migrate to test-harness - #[async_std::test] async fn integration_test() -> Result<()> { - init_logger()?; + init_logger(); - // Some benchmark averages - let mut mint_verify_times = vec![]; - let mut propose_verify_times = vec![]; - let mut vote_verify_times = vec![]; - let mut exec_verify_times = vec![]; + // Holders this test will use: + // * Faucet airdrops DRK + // * Alice, Bob, and Charlie are members of the DAO. + // * Rachel is the proposal recipient. + // * Dao is the DAO wallet + const HOLDERS: [Holder; 6] = + [Holder::Faucet, Holder::Alice, Holder::Bob, Holder::Charlie, Holder::Rachel, Holder::Dao]; + + // Initialize harness + let mut th = TestHarness::new(&["money".to_string(), "dao".to_string()]).await?; + + // We'll use the ALICE token as the DAO governance token + let gov_token_id = th.token_id(&Holder::Alice); + const ALICE_GOV_SUPPLY: u64 = 100_000_000; + const BOB_GOV_SUPPLY: u64 = 100_000_000; + const CHARLIE_GOV_SUPPLY: u64 = 100_000_000; + // And the DRK token as the treasury token + let drk_token_id = *DARK_TOKEN_ID; + const DRK_TOKEN_SUPPLY: u64 = 1_000_000_000; + // The tokens we want to send via the proposal + const PROPOSAL_AMOUNT: u64 = 250_000_000; // Slot to verify against let current_slot = 0; - let dao_th = DaoTestHarness::new().await?; - - // Money parameters - let xdrk_supply = 1_000_000; - let xdrk_token_id = *DARK_TOKEN_ID; - - // Governance token parameters - let gdrk_mint_auth = Keypair::random(&mut OsRng); - let gdrk_supply = 1_000_000; - let gdrk_token_id = TokenId::derive(gdrk_mint_auth.secret); - // DAO parameters - let dao = client::DaoInfo { - proposer_limit: 110, - quorum: 110, + let dao_keypair = th.holders.get(&Holder::Dao).unwrap().keypair; + let dao = DaoInfo { + proposer_limit: 100_000_000, + quorum: 199_999_999, approval_ratio_base: 2, approval_ratio_quot: 1, - gov_token_id: gdrk_token_id, - public_key: dao_th.dao_kp.public, + gov_token_id, + public_key: dao_keypair.public, bulla_blind: pallas::Base::random(&mut OsRng), }; - // We use this to receive coins - let mut cache = WalletCache::new(); - - // ======================================================= + // ==================== // Dao::Mint - // // Create the DAO bulla - // ======================================================= - debug!(target: "dao", "Stage 1. Creating DAO bulla"); + // ==================== + info!("Stage 1. Creating DAO bulla"); - let (params, proofs) = client::make_mint_call( - &dao, - &dao_th.dao_kp.secret, - &dao_th.dao_mint_zkbin, - &dao_th.dao_mint_pk, + info!("[Dao] Building DAO mint tx"); + let (dao_mint_tx, dao_mint_params) = th.dao_mint(&dao, &dao_keypair)?; + + info!("[Faucet] Executing DAO Mint tx"); + th.execute_dao_mint_tx(Holder::Faucet, &dao_mint_tx, &dao_mint_params, current_slot).await?; + + info!("[Alice] Executing DAO Mint tx"); + th.execute_dao_mint_tx(Holder::Alice, &dao_mint_tx, &dao_mint_params, current_slot).await?; + + info!("[Bob] Executing DAO Mint tx"); + th.execute_dao_mint_tx(Holder::Bob, &dao_mint_tx, &dao_mint_params, current_slot).await?; + + info!("[Charlie] Executing DAO Mint tx"); + th.execute_dao_mint_tx(Holder::Charlie, &dao_mint_tx, &dao_mint_params, current_slot).await?; + + info!("[Rachel] Executing DAO Mint tx"); + th.execute_dao_mint_tx(Holder::Rachel, &dao_mint_tx, &dao_mint_params, current_slot).await?; + + info!("[Dao] Executing DAO Mint tx"); + th.execute_dao_mint_tx(Holder::Dao, &dao_mint_tx, &dao_mint_params, current_slot).await?; + + // TODO: assert_trees + + // ======================================= + // Airdrop some treasury tokens to the DAO + // ======================================= + info!("Stage 2. Send Treasury token"); + + info!("[Faucet] Building DAO airdrop tx"); + let (airdrop_tx, airdrop_params) = th.airdrop_native( + DRK_TOKEN_SUPPLY, + Holder::Dao, + Some(DAO_CONTRACT_ID.inner()), // spend_hook + Some(dao_mint_params.dao_bulla.inner()), // user_data + None, + None, )?; - let mut data = vec![DaoFunction::Mint as u8]; - params.encode(&mut data)?; - let calls = vec![ContractCall { contract_id: dao_th.dao_contract_id, data }]; - let proofs = vec![proofs]; - let mut tx = Transaction { calls, proofs, signatures: vec![] }; - let sigs = tx.create_sigs(&mut OsRng, &[dao_th.dao_kp.secret])?; - tx.signatures = vec![sigs]; - - let timer = Instant::now(); - dao_th.alice_validator.read().await.add_transactions(&[tx.clone()], current_slot, true).await?; - mint_verify_times.push(timer.elapsed()); - // TODO: Witness and add to wallet merkle tree? - - let mut dao_tree = MerkleTree::new(100); - let dao_leaf_position = { - let node = MerkleNode::from(params.dao_bulla.inner()); - dao_tree.append(node); - dao_tree.mark().unwrap() - }; - let dao_bulla = params.dao_bulla; - debug!(target: "dao", "Created DAO bulla: {:?}", dao_bulla.inner()); - - // ======================================================= - // Money::Transfer - // - // Mint the initial supply of treasury token - // and send it all to the DAO directly - // ======================================================= - debug!(target: "dao", "Stage 2. Minting treasury token"); - - cache.track(dao_th.dao_kp.secret); - - // Address of deployed contract in our example is dao::exec::FUNC_ID - // This field is public, you can see it's being sent to a DAO - // but nothing else is visible. - // - // In the python code we wrote: - // - // spend_hook = b"0xdao_ruleset" - // - // TODO: this should be the contract/func ID - let spend_hook = DAO_CONTRACT_ID.inner(); - // The user_data can be a simple hash of the items passed into the ZK proof - // up to corresponding linked ZK proof to interpret however they need. - // In out case, it's the bulla for the DAO - let user_data = dao_bulla.inner(); - - let call = money_client::TransferCall { - clear_inputs: vec![money_client::TransferClearInput { - value: xdrk_supply, - token_id: xdrk_token_id, - signature_secret: dao_th.faucet_kp.secret, - }], - inputs: vec![], - outputs: vec![money_client::TransferOutput { - value: xdrk_supply, - token_id: xdrk_token_id, - public: dao_th.dao_kp.public, - serial: pallas::Base::random(&mut OsRng), - spend_hook, - user_data, - }], - }; - let (params, proofs) = call.make( - &dao_th.money_mint_zkbin, - &dao_th.money_mint_pk, - &dao_th.money_burn_zkbin, - &dao_th.money_burn_pk, - )?; - - let contract_id = *MONEY_CONTRACT_ID; - - let mut data = vec![MoneyFunction::TransferV1 as u8]; - params.encode(&mut data)?; - let calls = vec![ContractCall { contract_id, data }]; - let proofs = vec![proofs]; - let mut tx = Transaction { calls, proofs, signatures: vec![] }; - let sigs = tx.create_sigs(&mut OsRng, &vec![dao_th.faucet_kp.secret])?; - tx.signatures = vec![sigs]; - - dao_th.alice_validator.read().await.add_transactions(&[tx.clone()], current_slot, true).await?; - - // Wallet stuff - - // DAO reads the money received from the encrypted note - { - assert_eq!(tx.calls.len(), 1); - let calldata = &tx.calls[0].data; - let params_data = &calldata[1..]; - let params: MoneyTransferParamsV1 = Decodable::decode(params_data)?; - - for output in params.outputs { - let coin = Coin::from(output.coin); - cache.try_decrypt_note(coin, &output.note); - } - } - - let mut recv_coins = cache.get_received(&dao_th.dao_kp.secret); - assert_eq!(recv_coins.len(), 1); - let dao_recv_coin = recv_coins.pop().unwrap(); - let treasury_note = dao_recv_coin.note; - - // Check the actual coin received is valid before accepting it - - let coords = dao_th.dao_kp.public.inner().to_affine().coordinates().unwrap(); - let coin = poseidon_hash::<7>([ - *coords.x(), - *coords.y(), - pallas::Base::from(treasury_note.value), - treasury_note.token_id.inner(), - treasury_note.serial, - treasury_note.spend_hook, - treasury_note.user_data, - ]); - assert_eq!(coin, dao_recv_coin.coin.inner()); - - assert_eq!(treasury_note.spend_hook, spend_hook); - assert_eq!(treasury_note.user_data, dao_bulla.inner()); - - debug!(target: "dao", "DAO received a coin worth {} xDRK", treasury_note.value); - - // ======================================================= - // Money::Transfer - // - // Mint the governance token - // Send it to three hodlers - // ======================================================= - debug!(target: "dao", "Stage 3. Minting governance token"); - - cache.track(dao_th.alice_kp.secret); - cache.track(dao_th.bob_kp.secret); - cache.track(dao_th.charlie_kp.secret); - - // TODO: Clean this whole test up - let token_mint_zkbin = include_bytes!("../../money/proof/token_mint_v1.zk.bin"); - let token_mint_zkbin = darkfi::zkas::ZkBinary::decode(token_mint_zkbin)?; - let token_mint_empty_wit = darkfi::zk::empty_witnesses(&token_mint_zkbin); - let token_mint_circuit = - darkfi::zk::ZkCircuit::new(token_mint_empty_wit, token_mint_zkbin.clone()); - let token_mint_pk = darkfi::zk::ProvingKey::build(13, &token_mint_circuit); - - // Spend hook and user data disabled - let spend_hook = pallas::Base::from(0); - let user_data = pallas::Base::from(0); - - let mut builder = TokenMintCallBuilder { - mint_authority: gdrk_mint_auth, - recipient: dao_th.alice_kp.public, - amount: 400000, - spend_hook, - user_data, - token_mint_zkbin, - token_mint_pk, - }; - let debris1 = builder.build()?; - - builder.recipient = dao_th.bob_kp.public; - let debris2 = builder.build()?; - - builder.amount = 200000; - builder.recipient = dao_th.charlie_kp.public; - let debris3 = builder.build()?; - - assert!(2 * 400000 + 200000 == gdrk_supply); - - // This should actually be 3 calls in a single tx, but w/e. - let mut data = vec![MoneyFunction::TokenMintV1 as u8]; - debris1.params.encode(&mut data)?; - let calls = vec![ContractCall { contract_id: *MONEY_CONTRACT_ID, data }]; - let proofs = vec![debris1.proofs]; - let mut tx1 = Transaction { calls, proofs, signatures: vec![] }; - let sigs = tx1.create_sigs(&mut OsRng, &[gdrk_mint_auth.secret])?; - tx1.signatures = vec![sigs]; - - let mut data = vec![MoneyFunction::TokenMintV1 as u8]; - debris2.params.encode(&mut data)?; - let calls = vec![ContractCall { contract_id: *MONEY_CONTRACT_ID, data }]; - let proofs = vec![debris2.proofs]; - let mut tx2 = Transaction { calls, proofs, signatures: vec![] }; - let sigs = tx2.create_sigs(&mut OsRng, &[gdrk_mint_auth.secret])?; - tx2.signatures = vec![sigs]; - - let mut data = vec![MoneyFunction::TokenMintV1 as u8]; - debris3.params.encode(&mut data)?; - let calls = vec![ContractCall { contract_id: *MONEY_CONTRACT_ID, data }]; - let proofs = vec![debris3.proofs]; - let mut tx3 = Transaction { calls, proofs, signatures: vec![] }; - let sigs = tx3.create_sigs(&mut OsRng, &[gdrk_mint_auth.secret])?; - tx3.signatures = vec![sigs]; - - dao_th - .alice_validator - .read() - .await - .add_transactions(&[tx1.clone(), tx2.clone(), tx3.clone()], current_slot, true) + info!("[Faucet] Executing DAO airdrop tx"); + th.execute_airdrop_native_tx(Holder::Faucet, &airdrop_tx, &airdrop_params, current_slot) .await?; - // Wallet - { - for tx in [tx1, tx2, tx3] { - assert_eq!(tx.calls.len(), 1); - let calldata = &tx.calls[0].data; - let params_data = &calldata[1..]; - let params: MoneyTokenMintParamsV1 = Decodable::decode(params_data)?; - cache.try_decrypt_note(params.output.coin, ¶ms.output.note); - } - } + info!("[Alice] Executing DAO airdrop tx"); + th.execute_airdrop_native_tx(Holder::Alice, &airdrop_tx, &airdrop_params, current_slot).await?; - let gov_keypairs = vec![dao_th.alice_kp, dao_th.bob_kp, dao_th.charlie_kp]; - let mut gov_recv = vec![None, None, None]; - // Check that each person received one coin - for (i, key) in gov_keypairs.iter().enumerate() { - let gov_recv_coin = { - let mut recv_coins = cache.get_received(&key.secret); - assert_eq!(recv_coins.len(), 1); - let recv_coin = recv_coins.pop().unwrap(); - let note = &recv_coin.note; + info!("[Bob] Executing DAO airdrop tx"); + th.execute_airdrop_native_tx(Holder::Bob, &airdrop_tx, &airdrop_params, current_slot).await?; - assert_eq!(note.token_id, gdrk_token_id); - // Normal payment - assert_eq!(note.spend_hook, pallas::Base::from(0)); - assert_eq!(note.user_data, pallas::Base::from(0)); + info!("[Charlie] Executing DAO airdrop tx"); + th.execute_airdrop_native_tx(Holder::Charlie, &airdrop_tx, &airdrop_params, current_slot) + .await?; - let (pub_x, pub_y) = key.public.xy(); - let coin = poseidon_hash::<7>([ - pub_x, - pub_y, - pallas::Base::from(note.value), - note.token_id.inner(), - note.serial, - note.spend_hook, - note.user_data, - ]); - assert_eq!(coin, recv_coin.coin.inner()); + info!("[Rachel] Executing DAO airdrop tx"); + th.execute_airdrop_native_tx(Holder::Rachel, &airdrop_tx, &airdrop_params, current_slot) + .await?; - debug!(target: "dao", "Holder{} received a coin worth {} gDRK", i, note.value); + info!("[Dao] Executing DAO airdrop tx"); + th.execute_airdrop_native_tx(Holder::Dao, &airdrop_tx, &airdrop_params, current_slot).await?; - recv_coin - }; - gov_recv[i] = Some(gov_recv_coin); - } - // unwrap them for this demo - let gov_recv: Vec<_> = gov_recv.into_iter().map(|r| r.unwrap()).collect(); + th.assert_trees(&HOLDERS); - // ======================================================= + // Gather the DAO owncoin + th.gather_owncoin(Holder::Dao, airdrop_params.outputs[0].clone(), None)?; + + // ====================================== + // Mint the governance token to 3 holders + // ====================================== + info!("Stage 3. Minting governance token"); + + info!("[Alice] Building governance token mint tx for Alice"); + let (a_token_mint_tx, a_token_mint_params) = + th.token_mint(ALICE_GOV_SUPPLY, Holder::Alice, Holder::Alice, None, None)?; + + info!("[Faucet] Executing governance token mint tx for Alice"); + th.execute_token_mint_tx(Holder::Faucet, &a_token_mint_tx, &a_token_mint_params, current_slot) + .await?; + + info!("[Alice] Executing governance token mint tx for Alice"); + th.execute_token_mint_tx(Holder::Alice, &a_token_mint_tx, &a_token_mint_params, current_slot) + .await?; + + info!("[Bob] Executing governance token mint tx for Alice"); + th.execute_token_mint_tx(Holder::Bob, &a_token_mint_tx, &a_token_mint_params, current_slot) + .await?; + + info!("[Charlie] Executing governance token mint tx for Alice"); + th.execute_token_mint_tx(Holder::Charlie, &a_token_mint_tx, &a_token_mint_params, current_slot) + .await?; + + info!("[Rachel] Executing governance token mint tx for Alice"); + th.execute_token_mint_tx(Holder::Rachel, &a_token_mint_tx, &a_token_mint_params, current_slot) + .await?; + + info!("[Dao] Executing governance token mint tx for Alice"); + th.execute_token_mint_tx(Holder::Dao, &a_token_mint_tx, &a_token_mint_params, current_slot) + .await?; + + th.assert_trees(&HOLDERS); + + // Gather owncoin + th.gather_owncoin(Holder::Alice, a_token_mint_params.output, None)?; + + info!("[Alice] Building governance token mint tx for Bob"); + let (b_token_mint_tx, b_token_mint_params) = + th.token_mint(BOB_GOV_SUPPLY, Holder::Alice, Holder::Bob, None, None)?; + + info!("[Faucet] Executing governance token mint tx for Bob"); + th.execute_token_mint_tx(Holder::Faucet, &b_token_mint_tx, &b_token_mint_params, current_slot) + .await?; + + info!("[Alice] Executing governance token mint tx for Bob"); + th.execute_token_mint_tx(Holder::Alice, &b_token_mint_tx, &b_token_mint_params, current_slot) + .await?; + + info!("[Bob] Executing governance token mint tx for Bob"); + th.execute_token_mint_tx(Holder::Bob, &b_token_mint_tx, &b_token_mint_params, current_slot) + .await?; + + info!("[Charlie] Executing governance token mint tx for Bob"); + th.execute_token_mint_tx(Holder::Charlie, &b_token_mint_tx, &b_token_mint_params, current_slot) + .await?; + + info!("[Rachel] Executing governance token mint tx for Bob"); + th.execute_token_mint_tx(Holder::Rachel, &b_token_mint_tx, &b_token_mint_params, current_slot) + .await?; + + info!("[Dao] Executing governance token mint tx for Bob"); + th.execute_token_mint_tx(Holder::Dao, &b_token_mint_tx, &b_token_mint_params, current_slot) + .await?; + + th.assert_trees(&HOLDERS); + + // Gather owncoin + th.gather_owncoin(Holder::Bob, b_token_mint_params.output, None)?; + + info!("[Alice] Building governance token mint tx for Charlie"); + let (c_token_mint_tx, c_token_mint_params) = + th.token_mint(CHARLIE_GOV_SUPPLY, Holder::Alice, Holder::Charlie, None, None)?; + + info!("[Faucet] Executing governance token mint tx for Charlie"); + th.execute_token_mint_tx(Holder::Faucet, &c_token_mint_tx, &c_token_mint_params, current_slot) + .await?; + + info!("[Alice] Executing governance token mint tx for Charlie"); + th.execute_token_mint_tx(Holder::Alice, &c_token_mint_tx, &c_token_mint_params, current_slot) + .await?; + + info!("[Bob] Executing governance token mint tx for Charlie"); + th.execute_token_mint_tx(Holder::Bob, &c_token_mint_tx, &c_token_mint_params, current_slot) + .await?; + + info!("[Charlie] Executing governance token mint tx for Charlie"); + th.execute_token_mint_tx(Holder::Charlie, &c_token_mint_tx, &c_token_mint_params, current_slot) + .await?; + + info!("[Rachel] Executing governance token mint tx for Charlie"); + th.execute_token_mint_tx(Holder::Rachel, &c_token_mint_tx, &c_token_mint_params, current_slot) + .await?; + + info!("[Dao] Executing governance token mint tx for Charlie"); + th.execute_token_mint_tx(Holder::Dao, &c_token_mint_tx, &c_token_mint_params, current_slot) + .await?; + + th.assert_trees(&HOLDERS); + + // Gather owncoin + th.gather_owncoin(Holder::Charlie, c_token_mint_params.output, None)?; + + // ================ // Dao::Propose - // // Propose the vote - // In order to make a valid vote, first the proposer must - // meet a criteria for a minimum number of gov tokens - // - // DAO rules: - // 1. gov token IDs must match on all inputs - // 2. proposals must be submitted by minimum amount - // 3. all votes >= quorum - // 4. outcome > approval_ratio - // 5. structure of outputs - // output 0: value and address - // output 1: change address - // ======================================================= - debug!(target: "dao", "Stage 4. Propose the vote"); - + // ================ + info!("Stage 4. Propose the vote"); // TODO: look into proposal expiry once time for voting has finished - - let receiver_keypair = Keypair::random(&mut OsRng); - - let (money_leaf_position, money_merkle_path) = { - let tree = &cache.tree; - let leaf_position = gov_recv[0].leaf_position; - let merkle_path = tree.witness(leaf_position, 0).unwrap(); - (leaf_position, merkle_path) - }; - - // TODO: is it possible for an invalid transfer() to be constructed on exec()? - // need to look into this - let signature_secret = SecretKey::random(&mut OsRng); - let input = client::DaoProposeStakeInput { - secret: dao_th.alice_kp.secret, - note: gov_recv[0].note.clone(), - leaf_position: money_leaf_position, - merkle_path: money_merkle_path, - signature_secret, - }; - - let (dao_merkle_path, dao_merkle_root) = { - let tree = &dao_tree; - let root = tree.root(0).unwrap(); - let merkle_path = tree.witness(dao_leaf_position, 0).unwrap(); - (merkle_path, root) - }; - - let proposal = client::DaoProposalInfo { - dest: receiver_keypair.public, - amount: 1000, - token_id: xdrk_token_id, - blind: pallas::Base::random(&mut OsRng), - }; - - let call = client::DaoProposeCall { - inputs: vec![input], - proposal, - dao: dao.clone(), - dao_leaf_position, - dao_merkle_path, - dao_merkle_root, - }; - let (params, proofs) = call.make( - &dao_th.dao_propose_burn_zkbin, - &dao_th.dao_propose_burn_pk, - &dao_th.dao_propose_main_zkbin, - &dao_th.dao_propose_main_pk, + // TODO: Is it possible for an invalid transfer() to be constructed on exec()? + // Need to look into this. + info!("[Alice] Building DAO proposal tx"); + let (propose_tx, propose_params, propose_info) = th.dao_propose( + Holder::Alice, + Holder::Rachel, + PROPOSAL_AMOUNT, + drk_token_id, + dao.clone(), + dao_mint_params.dao_bulla, )?; - let contract_id = *DAO_CONTRACT_ID; + info!("[Faucet] Executing DAO proposal tx"); + th.execute_dao_propose_tx(Holder::Faucet, &propose_tx, &propose_params, current_slot).await?; - let mut data = vec![DaoFunction::Propose as u8]; - params.encode(&mut data)?; - let calls = vec![ContractCall { contract_id, data }]; - let proofs = vec![proofs]; - let mut tx = Transaction { calls, proofs, signatures: vec![] }; - let sigs = tx.create_sigs(&mut OsRng, &vec![signature_secret])?; - tx.signatures = vec![sigs]; + info!("[Alice] Executing DAO proposal tx"); + th.execute_dao_propose_tx(Holder::Alice, &propose_tx, &propose_params, current_slot).await?; - let timer = Instant::now(); - dao_th.alice_validator.read().await.add_transactions(&[tx.clone()], current_slot, true).await?; - propose_verify_times.push(timer.elapsed()); + info!("[Bob] Executing DAO proposal tx"); + th.execute_dao_propose_tx(Holder::Bob, &propose_tx, &propose_params, current_slot).await?; - //// Wallet + info!("[Charlie] Executing DAO proposal tx"); + th.execute_dao_propose_tx(Holder::Charlie, &propose_tx, &propose_params, current_slot).await?; - // HACK: Here we clone the tree so we can reproduce the root for voting. - // This should be done in a nicer way - let tree_at_proposal = cache.tree.clone(); + info!("[Rachel] Executing DAO proposal tx"); + th.execute_dao_propose_tx(Holder::Rachel, &propose_tx, &propose_params, current_slot).await?; - // Read received proposal - let (proposal, proposal_bulla) = { - let note: client::DaoProposeNote = params.note.decrypt(&dao_th.dao_kp.secret).unwrap(); + info!("[Dao] Executing DAO proposal tx"); + th.execute_dao_propose_tx(Holder::Dao, &propose_tx, &propose_params, current_slot).await?; - // TODO: check it belongs to DAO bulla + th.assert_trees(&HOLDERS); - // Return the proposal info - (note.proposal, params.proposal_bulla) - }; - debug!(target: "dao", "Proposal now active!"); - debug!(target: "dao", " destination: {:?}", proposal.dest); - debug!(target: "dao", " amount: {}", proposal.amount); - debug!(target: "dao", " token_id: {:?}", proposal.token_id); - debug!(target: "dao", " dao_bulla: {:?}", dao_bulla.inner()); - debug!(target: "dao", "Proposal bulla: {:?}", proposal_bulla); + // ===================================== + // Dao::Vote + // Proposal is accepted. Start the vote. + // ===================================== + info!("Stage 5. Start voting"); - // ======================================================= - // Proposal is accepted! - // Start the voting - // ======================================================= - - // Copying these schizo comments from python code: - // Lets the voting begin - // Voters have access to the proposal and dao data - // vote_state = VoteState() - // We don't need to copy nullifier set because it is checked from gov_state - // in vote_state_transition() anyway - // - // TODO: what happens if voters don't unblind their vote - // Answer: - // 1. there is a time limit - // 2. both the MPC or users can unblind - // - // TODO: bug if I vote then send money, then we can double vote - // TODO: all timestamps missing - // - timelock (future voting starts in 2 days) - // Fix: use nullifiers from money gov state only from - // beginning of gov period - // Cannot use nullifiers from before voting period - - debug!(target: "dao", "Stage 5. Start voting"); - - // We were previously saving updates here for testing - // let mut updates = vec![]; - - // User 1: YES - - let (money_leaf_position, money_merkle_path) = { - let tree = &tree_at_proposal; - let leaf_position = gov_recv[0].leaf_position; - let merkle_path = tree.witness(leaf_position, 0).unwrap(); - (leaf_position, merkle_path) - }; - - let signature_secret = SecretKey::random(&mut OsRng); - let input = client::DaoVoteInput { - secret: dao_th.alice_kp.secret, - note: gov_recv[0].note.clone(), - leaf_position: money_leaf_position, - merkle_path: money_merkle_path, - signature_secret, - }; - - let vote_option: bool = true; - // assert!(vote_option || !vote_option); // wtf - - // We create a new keypair to encrypt the vote. - // For the demo MVP, you can just use the dao_keypair secret - let vote_keypair_1 = Keypair::random(&mut OsRng); - - let call = client::DaoVoteCall { - inputs: vec![input], - vote_option, - yes_vote_blind: pallas::Scalar::random(&mut OsRng), - vote_keypair: vote_keypair_1, - proposal: proposal.clone(), - dao: dao.clone(), - }; - let (params, proofs) = call.make( - &dao_th.dao_vote_burn_zkbin, - &dao_th.dao_vote_burn_pk, - &dao_th.dao_vote_main_zkbin, - &dao_th.dao_vote_main_pk, + info!("[Alice] Building vote tx (yes)"); + let (alice_vote_tx, alice_vote_params) = th.dao_vote( + Holder::Alice, + &dao_keypair, + true, + dao.clone(), + propose_info.clone(), + propose_params.proposal_bulla, )?; - let contract_id = *DAO_CONTRACT_ID; - - let mut data = vec![DaoFunction::Vote as u8]; - params.encode(&mut data)?; - let calls = vec![ContractCall { contract_id, data }]; - let proofs = vec![proofs]; - let mut tx = Transaction { calls, proofs, signatures: vec![] }; - let sigs = tx.create_sigs(&mut OsRng, &vec![signature_secret])?; - tx.signatures = vec![sigs]; - - let timer = Instant::now(); - dao_th.alice_validator.read().await.add_transactions(&[tx.clone()], current_slot, true).await?; - vote_verify_times.push(timer.elapsed()); - - // Secret vote info. Needs to be revealed at some point. - // TODO: look into verifiable encryption for notes - // TODO: look into timelock puzzle as a possibility - let vote_note_1 = { - let note: client::DaoVoteNote = params.note.decrypt(&vote_keypair_1.secret).unwrap(); - note - }; - debug!(target: "dao", "User 1 voted!"); - debug!(target: "dao", " vote_option: {}", vote_note_1.vote_option); - debug!(target: "dao", " value: {}", vote_note_1.all_vote_value); - - // User 2: NO - - let (money_leaf_position, money_merkle_path) = { - let tree = &tree_at_proposal; - let leaf_position = gov_recv[1].leaf_position; - let merkle_path = tree.witness(leaf_position, 0).unwrap(); - (leaf_position, merkle_path) - }; - - let signature_secret = SecretKey::random(&mut OsRng); - let input = client::DaoVoteInput { - //secret: gov_keypair_2.secret, - secret: dao_th.bob_kp.secret, - note: gov_recv[1].note.clone(), - leaf_position: money_leaf_position, - merkle_path: money_merkle_path, - signature_secret, - }; - - let vote_option: bool = false; - // assert!(vote_option || !vote_option); // wtf - - // We create a new keypair to encrypt the vote. - let vote_keypair_2 = Keypair::random(&mut OsRng); - - let call = client::DaoVoteCall { - inputs: vec![input], - vote_option, - yes_vote_blind: pallas::Scalar::random(&mut OsRng), - vote_keypair: vote_keypair_2, - proposal: proposal.clone(), - dao: dao.clone(), - }; - let (params, proofs) = call.make( - &dao_th.dao_vote_burn_zkbin, - &dao_th.dao_vote_burn_pk, - &dao_th.dao_vote_main_zkbin, - &dao_th.dao_vote_main_pk, + info!("[Bob] Building vote tx (no)"); + let (bob_vote_tx, bob_vote_params) = th.dao_vote( + Holder::Bob, + &dao_keypair, + false, + dao.clone(), + propose_info.clone(), + propose_params.proposal_bulla, )?; - let contract_id = *DAO_CONTRACT_ID; - - let mut data = vec![DaoFunction::Vote as u8]; - params.encode(&mut data)?; - let calls = vec![ContractCall { contract_id, data }]; - let proofs = vec![proofs]; - let mut tx = Transaction { calls, proofs, signatures: vec![] }; - let sigs = tx.create_sigs(&mut OsRng, &vec![signature_secret])?; - tx.signatures = vec![sigs]; - - let timer = Instant::now(); - dao_th.alice_validator.read().await.add_transactions(&[tx.clone()], current_slot, true).await?; - vote_verify_times.push(timer.elapsed()); - - let vote_note_2 = { - let note: client::DaoVoteNote = params.note.decrypt(&vote_keypair_2.secret).unwrap(); - note - }; - debug!(target: "dao", "User 2 voted!"); - debug!(target: "dao", " vote_option: {}", vote_note_2.vote_option); - debug!(target: "dao", " value: {}", vote_note_2.all_vote_value); - - // User 3: YES - - let (money_leaf_position, money_merkle_path) = { - let tree = &tree_at_proposal; - let leaf_position = gov_recv[2].leaf_position; - let merkle_path = tree.witness(leaf_position, 0).unwrap(); - (leaf_position, merkle_path) - }; - - let signature_secret = SecretKey::random(&mut OsRng); - let input = client::DaoVoteInput { - //secret: gov_keypair_3.secret, - secret: dao_th.charlie_kp.secret, - note: gov_recv[2].note.clone(), - leaf_position: money_leaf_position, - merkle_path: money_merkle_path, - signature_secret, - }; - - let vote_option: bool = true; - // assert!(vote_option || !vote_option); // wtf - - // We create a new keypair to encrypt the vote. - let vote_keypair_3 = Keypair::random(&mut OsRng); - - let call = client::DaoVoteCall { - inputs: vec![input], - vote_option, - yes_vote_blind: pallas::Scalar::random(&mut OsRng), - vote_keypair: vote_keypair_3, - proposal: proposal.clone(), - dao: dao.clone(), - }; - let (params, proofs) = call.make( - &dao_th.dao_vote_burn_zkbin, - &dao_th.dao_vote_burn_pk, - &dao_th.dao_vote_main_zkbin, - &dao_th.dao_vote_main_pk, + info!("[Charlie] Building vote tx (yes)"); + let (charlie_vote_tx, charlie_vote_params) = th.dao_vote( + Holder::Charlie, + &dao_keypair, + true, + dao.clone(), + propose_info.clone(), + propose_params.proposal_bulla, )?; - let contract_id = *DAO_CONTRACT_ID; + info!("[Faucet] Executing Alice vote tx"); + th.execute_dao_vote_tx(Holder::Faucet, &alice_vote_tx, &alice_vote_params, current_slot) + .await?; + info!("[Faucet] Executing Bob vote tx"); + th.execute_dao_vote_tx(Holder::Faucet, &bob_vote_tx, &bob_vote_params, current_slot).await?; + info!("[Faucet] Executing Charlie vote tx"); + th.execute_dao_vote_tx(Holder::Faucet, &charlie_vote_tx, &charlie_vote_params, current_slot) + .await?; - let mut data = vec![DaoFunction::Vote as u8]; - params.encode(&mut data)?; - let calls = vec![ContractCall { contract_id, data }]; - let proofs = vec![proofs]; - let mut tx = Transaction { calls, proofs, signatures: vec![] }; - let sigs = tx.create_sigs(&mut OsRng, &vec![signature_secret])?; - tx.signatures = vec![sigs]; + info!("[Alice] Executing Alice vote tx"); + th.execute_dao_vote_tx(Holder::Alice, &alice_vote_tx, &alice_vote_params, current_slot).await?; + info!("[Alice] Executing Bob vote tx"); + th.execute_dao_vote_tx(Holder::Alice, &bob_vote_tx, &bob_vote_params, current_slot).await?; + info!("[Alice] Executing Charlie vote tx"); + th.execute_dao_vote_tx(Holder::Alice, &charlie_vote_tx, &charlie_vote_params, current_slot) + .await?; - let timer = Instant::now(); - dao_th.alice_validator.read().await.add_transactions(&[tx.clone()], current_slot, true).await?; - vote_verify_times.push(timer.elapsed()); + info!("[Bob] Executing Alice vote tx"); + th.execute_dao_vote_tx(Holder::Bob, &alice_vote_tx, &alice_vote_params, current_slot).await?; + info!("[Bob] Executing Bob vote tx"); + th.execute_dao_vote_tx(Holder::Bob, &bob_vote_tx, &bob_vote_params, current_slot).await?; + info!("[Bob] Executing Charlie vote tx"); + th.execute_dao_vote_tx(Holder::Bob, &charlie_vote_tx, &charlie_vote_params, current_slot) + .await?; - // Secret vote info. Needs to be revealed at some point. - // TODO: look into verifiable encryption for notes - // TODO: look into timelock puzzle as a possibility - let vote_note_3 = { - let note: client::DaoVoteNote = params.note.decrypt(&vote_keypair_3.secret).unwrap(); - note - }; - debug!(target: "dao", "User 3 voted!"); - debug!(target: "dao", " vote_option: {}", vote_note_3.vote_option); - debug!(target: "dao", " value: {}", vote_note_3.all_vote_value); + info!("[Charlie] Executing Alice vote tx"); + th.execute_dao_vote_tx(Holder::Charlie, &alice_vote_tx, &alice_vote_params, current_slot) + .await?; + info!("[Charlie] Executing Bob vote tx"); + th.execute_dao_vote_tx(Holder::Charlie, &bob_vote_tx, &bob_vote_params, current_slot).await?; + info!("[Charlie] Executing Charlie vote tx"); + th.execute_dao_vote_tx(Holder::Charlie, &charlie_vote_tx, &charlie_vote_params, current_slot) + .await?; - // Every votes produces a semi-homomorphic encryption of their vote. - // Which is either yes or no - // We copy the state tree for the governance token so coins can be used - // to vote on other proposals at the same time. - // With their vote, they produce a ZK proof + nullifier - // The votes are unblinded by MPC to a selected party at the end of the - // voting period. - // (that's if we want votes to be hidden during voting) + info!("[Rachel] Executing Alice vote tx"); + th.execute_dao_vote_tx(Holder::Rachel, &alice_vote_tx, &alice_vote_params, current_slot) + .await?; + info!("[Rachel] Executing Bob vote tx"); + th.execute_dao_vote_tx(Holder::Rachel, &bob_vote_tx, &bob_vote_params, current_slot).await?; + info!("[Rachel] Executing Charlie vote tx"); + th.execute_dao_vote_tx(Holder::Rachel, &charlie_vote_tx, &charlie_vote_params, current_slot) + .await?; + info!("[Dao] Executing Alice vote tx"); + th.execute_dao_vote_tx(Holder::Dao, &alice_vote_tx, &alice_vote_params, current_slot).await?; + info!("[Dao] Executing Bob vote tx"); + th.execute_dao_vote_tx(Holder::Dao, &bob_vote_tx, &bob_vote_params, current_slot).await?; + info!("[Dao] Executing Charlie vote tx"); + th.execute_dao_vote_tx(Holder::Dao, &charlie_vote_tx, &charlie_vote_params, current_slot) + .await?; + + // Gather and decrypt all vote notes + let vote_note_1: DaoVoteNote = alice_vote_params.note.decrypt(&dao_keypair.secret).unwrap(); + let vote_note_2: DaoVoteNote = bob_vote_params.note.decrypt(&dao_keypair.secret).unwrap(); + let vote_note_3: DaoVoteNote = charlie_vote_params.note.decrypt(&dao_keypair.secret).unwrap(); + + // Count the votes let mut total_yes_vote_value = 0; let mut total_all_vote_value = 0; - - let mut blind_total_vote = model::DaoBlindAggregateVote::default(); - - // Just keep track of these for the assert statements after the for loop - // but they aren't needed otherwise. - let mut total_yes_vote_blind = pallas::Scalar::from(0); - let mut total_all_vote_blind = pallas::Scalar::from(0); + let mut blind_total_vote = DaoBlindAggregateVote::default(); + let mut total_yes_vote_blind = pallas::Scalar::ZERO; + let mut total_all_vote_blind = pallas::Scalar::ZERO; for (i, note) in [vote_note_1, vote_note_2, vote_note_3].iter().enumerate() { total_yes_vote_blind += note.yes_vote_blind; total_all_vote_blind += note.all_vote_blind; // Update private values - // vote_option is either 0 or 1 let yes_vote_value = note.vote_option as u64 * note.all_vote_value; total_yes_vote_value += yes_vote_value; total_all_vote_value += note.all_vote_value; // Update public values - let yes_vote_commit = pedersen_commitment_u64(yes_vote_value, note.yes_vote_blind); let all_vote_commit = pedersen_commitment_u64(note.all_vote_value, note.all_vote_blind); - - let blind_vote = model::DaoBlindAggregateVote { yes_vote_commit, all_vote_commit }; + let blind_vote = DaoBlindAggregateVote { yes_vote_commit, all_vote_commit }; blind_total_vote.aggregate(blind_vote); // Just for the debug @@ -719,150 +402,79 @@ async fn integration_test() -> Result<()> { true => "yes", false => "no", }; - debug!( - target: "dao", - "Voter {} voted {} with {} gDRK", - i, - vote_result, - note.all_vote_value, - ); + + info!("Voter {} voted {} with {} tokens", i, vote_result, note.all_vote_value); } - debug!(target: "dao", "Outcome = {} / {}", total_yes_vote_value, total_all_vote_value); + info!("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), + 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), + pedersen_commitment_u64(total_yes_vote_value, total_yes_vote_blind) ); - // ======================================================= + // ================ + // Dao::Exec // Execute the vote - // ======================================================= + // ================ + info!("Stage 6. Execute the vote"); - debug!(target: "dao", "Stage 6. Execute vote"); - - // Used to export user_data from this coin so it can be accessed by DAO::exec() - let user_data_blind = pallas::Base::random(&mut OsRng); - - let user_serial = pallas::Base::random(&mut OsRng); - let dao_serial = pallas::Base::random(&mut OsRng); - let input_value = treasury_note.value; - let input_value_blind = pallas::Scalar::random(&mut OsRng); - let xfer_signature_secret = SecretKey::random(&mut OsRng); - let exec_signature_secret = SecretKey::random(&mut OsRng); - - let (treasury_leaf_position, treasury_merkle_path) = { - let tree = &cache.tree; - let leaf_position = dao_recv_coin.leaf_position; - let merkle_path = tree.witness(leaf_position, 0).unwrap(); - (leaf_position, merkle_path) - }; - - // TODO: this should be the contract/func ID - //let spend_hook = pallas::Base::from(110); - let spend_hook = DAO_CONTRACT_ID.inner(); - // The user_data can be a simple hash of the items passed into the ZK proof - // up to corresponding linked ZK proof to interpret however they need. - // In out case, it's the bulla for the DAO - let user_data = dao_bulla.inner(); - - let xfer_call = money_client::TransferCall { - clear_inputs: vec![], - inputs: vec![money_client::TransferInput { - leaf_position: treasury_leaf_position, - merkle_path: treasury_merkle_path, - secret: dao_th.dao_kp.secret, - note: treasury_note, - user_data_blind, - value_blind: input_value_blind, - signature_secret: xfer_signature_secret, - }], - outputs: vec![ - // Sending money - money_client::TransferOutput { - value: 1000, - token_id: xdrk_token_id, - //public: user_keypair.public, - public: receiver_keypair.public, - serial: user_serial, - spend_hook: pallas::Base::from(0), - user_data: pallas::Base::from(0), - }, - // Change back to DAO - money_client::TransferOutput { - value: xdrk_supply - 1000, - token_id: xdrk_token_id, - public: dao_th.dao_kp.public, - serial: dao_serial, - spend_hook, - user_data, - }, - ], - }; - let (xfer_params, xfer_proofs) = xfer_call.make( - &dao_th.money_mint_zkbin, - &dao_th.money_mint_pk, - &dao_th.money_burn_zkbin, - &dao_th.money_burn_pk, + info!("[Dao] Building Dao::Exec tx"); + let (exec_tx, xfer_params, exec_params) = th.dao_exec( + dao, + dao_mint_params.dao_bulla, + propose_info, + total_yes_vote_value, + total_all_vote_value, + total_yes_vote_blind, + total_all_vote_blind, )?; - let mut data = vec![MoneyFunction::TransferV1 as u8]; - xfer_params.encode(&mut data)?; - let xfer_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data }; + info!("[Faucet] Executing Dao::Exec tx"); + th.execute_dao_exec_tx(Holder::Faucet, &exec_tx, &xfer_params, &exec_params, current_slot) + .await?; - let call = client::DaoExecCall { - proposal, - dao, - yes_vote_value: total_yes_vote_value, - all_vote_value: total_all_vote_value, - yes_vote_blind: total_yes_vote_blind, - all_vote_blind: total_all_vote_blind, - user_serial, - dao_serial, - input_value, - input_value_blind, - hook_dao_exec: spend_hook, - signature_secret: exec_signature_secret, - }; - let (exec_params, exec_proofs) = call.make(&dao_th.dao_exec_zkbin, &dao_th.dao_exec_pk)?; + info!("[Alice] Executing Dao::Exec tx"); + th.execute_dao_exec_tx(Holder::Alice, &exec_tx, &xfer_params, &exec_params, current_slot) + .await?; - let mut data = vec![DaoFunction::Exec as u8]; - exec_params.encode(&mut data)?; - let exec_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data }; + info!("[Bob] Executing Dao::Exec tx"); + th.execute_dao_exec_tx(Holder::Bob, &exec_tx, &xfer_params, &exec_params, current_slot).await?; - let mut tx = Transaction { - calls: vec![xfer_call, exec_call], - proofs: vec![xfer_proofs, exec_proofs], - signatures: vec![], - }; - let xfer_sigs = tx.create_sigs(&mut OsRng, &vec![xfer_signature_secret])?; - let exec_sigs = tx.create_sigs(&mut OsRng, &vec![exec_signature_secret])?; - tx.signatures = vec![xfer_sigs, exec_sigs]; + info!("[Charlie] Executing Dao::Exec tx"); + th.execute_dao_exec_tx(Holder::Charlie, &exec_tx, &xfer_params, &exec_params, current_slot) + .await?; - let timer = Instant::now(); - dao_th.alice_validator.read().await.add_transactions(&[tx.clone()], current_slot, true).await?; - exec_verify_times.push(timer.elapsed()); + info!("[Rachel] Executing Dao::Exec tx"); + th.execute_dao_exec_tx(Holder::Rachel, &exec_tx, &xfer_params, &exec_params, current_slot) + .await?; - // Statistics - let mint_avg = mint_verify_times.iter().sum::(); - let mint_avg = mint_avg / mint_verify_times.len() as u32; - println!("Average Mint verification time: {:?}", mint_avg); + info!("[Dao] Executing Dao::Exec tx"); + th.execute_dao_exec_tx(Holder::Dao, &exec_tx, &xfer_params, &exec_params, current_slot).await?; - let propose_avg = propose_verify_times.iter().sum::(); - let propose_avg = propose_avg / propose_verify_times.len() as u32; - println!("Average Propose verification time: {:?}", propose_avg); + th.assert_trees(&HOLDERS); - let vote_avg = vote_verify_times.iter().sum::(); - let vote_avg = vote_avg / vote_verify_times.len() as u32; - println!("Average Vote verification time: {:?}", vote_avg); + // Gather the coins + th.gather_owncoin(Holder::Dao, xfer_params.outputs[0].clone(), None)?; + th.gather_owncoin(Holder::Rachel, xfer_params.outputs[1].clone(), None)?; - let exec_avg = exec_verify_times.iter().sum::(); - let exec_avg = exec_avg / exec_verify_times.len() as u32; - println!("Average Exec verification time: {:?}", exec_avg); + let rachel_wallet = th.holders.get(&Holder::Rachel).unwrap(); + assert!(rachel_wallet.unspent_money_coins[0].note.value == PROPOSAL_AMOUNT); + assert!(rachel_wallet.unspent_money_coins[0].note.token_id == drk_token_id); + // FIXME: The harness doesn't register that we spent the first coin on the proposal. + let dao_wallet = th.holders.get(&Holder::Dao).unwrap(); + assert!(dao_wallet.unspent_money_coins[1].note.value == DRK_TOKEN_SUPPLY - PROPOSAL_AMOUNT); + assert!(dao_wallet.unspent_money_coins[1].note.token_id == drk_token_id); + + // Stats + th.statistics(); + + // Thanks for reading Ok(()) } diff --git a/src/contract/money/tests/verification_bench.rs b/src/contract/money/tests/verification_bench.rs index 81be63c95..29fc6cb8a 100644 --- a/src/contract/money/tests/verification_bench.rs +++ b/src/contract/money/tests/verification_bench.rs @@ -53,7 +53,8 @@ async fn alice2alice_random_amounts() -> Result<()> { info!(target: "money", "[Faucet] ========================"); info!(target: "money", "[Faucet] Building Alice's airdrop"); info!(target: "money", "[Faucet] ========================"); - let (airdrop_tx, airdrop_params) = th.airdrop_native(ALICE_AIRDROP, Holder::Alice)?; + let (airdrop_tx, airdrop_params) = + th.airdrop_native(ALICE_AIRDROP, Holder::Alice, None, None, None, None)?; info!(target: "money", "[Faucet] =========================="); info!(target: "money", "[Faucet] Executing Alice airdrop tx"); @@ -151,7 +152,8 @@ async fn alice2alice_multiplecoins_random_amounts() -> Result<()> { info!(target: "money", "[Faucet] ==================================================="); info!(target: "money", "[Faucet] Building Money::Mint params for Alice's mint for token {} and amount {}", i, amount); info!(target: "money", "[Faucet] ==================================================="); - let (mint_tx, mint_params) = th.token_mint(amount, Holder::Alice, Holder::Alice)?; + let (mint_tx, mint_params) = + th.token_mint(amount, Holder::Alice, Holder::Alice, None, None)?; info!(target: "money", "[Faucet] ======================="); info!(target: "money", "[Faucet] Executing Alice mint tx");