Просмотр исходного кода

drk/dao: generic proposal flow added

skoupidi 1 год назад
Родитель
Сommit
6563461028
3 измененных файлов с 492 добавлено и 2 удалено
  1. 6 1
      bin/drk/src/cli_util.rs
  2. 397 0
      bin/drk/src/dao.rs
  3. 89 1
      bin/drk/src/main.rs

+ 6 - 1
bin/drk/src/cli_util.rs

@@ -305,7 +305,7 @@ pub fn generate_completions(shell: &str) -> Result<()> {
         .about("Create a transfer proposal for a DAO")
         .args(&vec![
             name.clone(),
-            duration,
+            duration.clone(),
             amount,
             token,
             recipient,
@@ -313,6 +313,10 @@ pub fn generate_completions(shell: &str) -> Result<()> {
             user_data.clone(),
         ]);
 
+    let propose_generic = SubCommand::with_name("propose-generic")
+        .about("Create a generic proposal for a DAO")
+        .args(&vec![name.clone(), duration, user_data.clone()]);
+
     let proposals =
         SubCommand::with_name("proposals").about("List DAO proposals").args(&vec![name]);
 
@@ -355,6 +359,7 @@ pub fn generate_completions(shell: &str) -> Result<()> {
         balance,
         mint,
         propose_transfer,
+        propose_generic,
         proposals,
         proposal,
         proposal_import,

+ 397 - 0
bin/drk/src/dao.rs

@@ -1862,6 +1862,64 @@ impl Drk {
         Ok(proposal_record)
     }
 
+    /// Create a DAO generic proposal.
+    pub async fn dao_propose_generic(
+        &self,
+        name: &str,
+        duration_blockwindows: u64,
+        user_data: Option<pallas::Base>,
+    ) -> Result<ProposalRecord> {
+        // Fetch DAO and check its deployed
+        let dao = self.get_dao_by_name(name).await?;
+        if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
+            return Err(Error::Custom(
+                "[dao_propose_transfer] DAO seems to not have been deployed yet".to_string(),
+            ))
+        }
+
+        // Check that we have the proposer key
+        if dao.params.proposer_secret_key.is_none() {
+            return Err(Error::Custom(
+                "[dao_propose_transfer] We need the proposer secret key to create proposals for this DAO".to_string(),
+            ))
+        }
+
+        // Retrieve next block height and current block time target,
+        // to compute their window.
+        let next_block_height = self.get_next_block_height().await?;
+        let block_target = self.get_block_target().await?;
+        let creation_blockwindow = blockwindow(next_block_height, block_target);
+
+        // Create the actual proposal
+        let proposal = DaoProposal {
+            auth_calls: vec![],
+            creation_blockwindow,
+            duration_blockwindows,
+            user_data: user_data.unwrap_or(pallas::Base::ZERO),
+            dao_bulla: dao.bulla(),
+            blind: Blind::random(&mut OsRng),
+        };
+
+        let proposal_record = ProposalRecord {
+            proposal,
+            data: None,
+            leaf_position: None,
+            money_snapshot_tree: None,
+            nullifiers_smt_snapshot: None,
+            tx_hash: None,
+            call_index: None,
+            exec_tx_hash: None,
+        };
+
+        if let Err(e) = self.put_dao_proposal(&proposal_record).await {
+            return Err(Error::DatabaseError(format!(
+                "[dao_propose_transfer] Put DAO proposal failed: {e:?}"
+            )))
+        }
+
+        Ok(proposal_record)
+    }
+
     /// Create a DAO transfer proposal transaction.
     pub async fn dao_transfer_proposal_tx(&self, proposal: &ProposalRecord) -> Result<Transaction> {
         // Check we know the plaintext data
@@ -2087,6 +2145,196 @@ impl Drk {
         Ok(tx)
     }
 
+    /// Create a DAO generic proposal transaction.
+    pub async fn dao_generic_proposal_tx(&self, proposal: &ProposalRecord) -> Result<Transaction> {
+        // Fetch DAO and check its deployed
+        let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else {
+            return Err(Error::Custom(format!(
+                "[dao_transfer_proposal_tx] DAO {} was not found",
+                proposal.proposal.dao_bulla
+            )))
+        };
+        if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
+            return Err(Error::Custom(
+                "[dao_transfer_proposal_tx] DAO seems to not have been deployed yet".to_string(),
+            ))
+        }
+
+        // Check that we have the proposer key
+        if dao.params.proposer_secret_key.is_none() {
+            return Err(Error::Custom(
+                "[dao_transfer_proposal_tx] We need the proposer secret key to create proposals for this DAO".to_string(),
+            ))
+        }
+
+        // Fetch our own governance OwnCoins to see what our balance is
+        let gov_owncoins = self.get_token_coins(&dao.params.dao.gov_token_id).await?;
+        if gov_owncoins.is_empty() {
+            return Err(Error::Custom(format!(
+                "[dao_transfer_proposal_tx] Did not find any governance {} coins in wallet",
+                dao.params.dao.gov_token_id
+            )))
+        }
+
+        // Find which governance coins we can use
+        let mut total_value = 0;
+        let mut gov_owncoins_to_use = vec![];
+        for gov_owncoin in gov_owncoins {
+            if total_value >= dao.params.dao.proposer_limit {
+                break
+            }
+
+            total_value += gov_owncoin.note.value;
+            gov_owncoins_to_use.push(gov_owncoin);
+        }
+
+        // Check our governance coins balance is sufficient
+        if total_value < dao.params.dao.proposer_limit {
+            return Err(Error::Custom(format!(
+                "[dao_transfer_proposal_tx] Not enough gov token {} balance to propose",
+                dao.params.dao.gov_token_id
+            )))
+        }
+
+        // Now we need to do a lookup for the zkas proof bincodes, and create
+        // the circuit objects and proving keys so we can build the transaction.
+        // We also do this through the RPC. First we grab the fee call from money.
+        let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
+
+        let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
+        else {
+            return Err(Error::Custom(
+                "[dao_transfer_proposal_tx] Fee circuit not found".to_string(),
+            ))
+        };
+
+        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
+
+        let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
+
+        // Creating Fee circuit proving key
+        let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
+
+        // Now we grab the DAO bins
+        let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
+
+        let Some(propose_burn_zkbin) =
+            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_PROPOSE_INPUT_NS)
+        else {
+            return Err(Error::Custom(
+                "[dao_transfer_proposal_tx] Propose Burn circuit not found".to_string(),
+            ))
+        };
+
+        let Some(propose_main_zkbin) =
+            zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_PROPOSE_MAIN_NS)
+        else {
+            return Err(Error::Custom(
+                "[dao_transfer_proposal_tx] Propose Main circuit not found".to_string(),
+            ))
+        };
+
+        let propose_burn_zkbin = ZkBinary::decode(&propose_burn_zkbin.1)?;
+        let propose_main_zkbin = ZkBinary::decode(&propose_main_zkbin.1)?;
+
+        let propose_burn_circuit =
+            ZkCircuit::new(empty_witnesses(&propose_burn_zkbin)?, &propose_burn_zkbin);
+        let propose_main_circuit =
+            ZkCircuit::new(empty_witnesses(&propose_main_zkbin)?, &propose_main_zkbin);
+
+        // Creating DAO ProposeBurn and ProposeMain circuits proving keys
+        let propose_burn_pk = ProvingKey::build(propose_burn_zkbin.k, &propose_burn_circuit);
+        let propose_main_pk = ProvingKey::build(propose_main_zkbin.k, &propose_main_circuit);
+
+        // Fetch our money Merkle tree
+        let money_merkle_tree = self.get_money_tree().await?;
+
+        // Now we can create the proposal transaction parameters.
+        // We first generate the `DaoProposeStakeInput` inputs,
+        // using our governance OwnCoins.
+        let mut inputs = Vec::with_capacity(gov_owncoins_to_use.len());
+        for gov_owncoin in gov_owncoins_to_use {
+            let input = DaoProposeStakeInput {
+                secret: gov_owncoin.secret,
+                note: gov_owncoin.note.clone(),
+                leaf_position: gov_owncoin.leaf_position,
+                merkle_path: money_merkle_tree.witness(gov_owncoin.leaf_position, 0).unwrap(),
+            };
+            inputs.push(input);
+        }
+
+        // Now create the parameters for the proposal tx
+        let signature_secret = SecretKey::random(&mut OsRng);
+
+        // Fetch the daos Merkle tree to compute the DAO Merkle path and root
+        let (daos_tree, _) = self.get_dao_trees().await?;
+        let (dao_merkle_path, dao_merkle_root) = {
+            let root = daos_tree.root(0).unwrap();
+            let leaf_pos = dao.leaf_position.unwrap();
+            let dao_merkle_path = daos_tree.witness(leaf_pos, 0).unwrap();
+            (dao_merkle_path, root)
+        };
+
+        // Generate the Money nullifiers Sparse Merkle Tree
+        let store = WalletStorage::new(
+            &self.wallet,
+            &MONEY_SMT_TABLE,
+            MONEY_SMT_COL_KEY,
+            MONEY_SMT_COL_VALUE,
+        );
+        let money_null_smt = WalletSmt::new(store, PoseidonFp::new(), &EMPTY_NODES_FP);
+
+        // Create the proposal call
+        let call = DaoProposeCall {
+            money_null_smt: &money_null_smt,
+            inputs,
+            proposal: proposal.proposal.clone(),
+            dao: dao.params.dao,
+            dao_leaf_position: dao.leaf_position.unwrap(),
+            dao_merkle_path,
+            dao_merkle_root,
+            signature_secret,
+        };
+
+        let (params, proofs) = call.make(
+            &dao.params.proposer_secret_key.unwrap(),
+            &propose_burn_zkbin,
+            &propose_burn_pk,
+            &propose_main_zkbin,
+            &propose_main_pk,
+        )?;
+
+        // Encode the call
+        let mut data = vec![DaoFunction::Propose as u8];
+        params.encode_async(&mut data).await?;
+        let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
+
+        // Create the TransactionBuilder containing above call
+        let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?;
+
+        // We first have to execute the fee-less tx to gather its used gas, and then we feed
+        // it into the fee-creating function.
+        let mut tx = tx_builder.build()?;
+        let sigs = tx.create_sigs(&[signature_secret])?;
+        tx.signatures = vec![sigs];
+
+        let tree = self.get_money_tree().await?;
+        let (fee_call, fee_proofs, fee_secrets) =
+            self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
+
+        // Append the fee call to the transaction
+        tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
+
+        // Now build the actual transaction and sign it with all necessary keys.
+        let mut tx = tx_builder.build()?;
+        let sigs = tx.create_sigs(&[signature_secret])?;
+        tx.signatures.push(sigs);
+        let sigs = tx.create_sigs(&fee_secrets)?;
+        tx.signatures.push(sigs);
+
+        Ok(tx)
+    }
+
     /// Vote on a DAO proposal
     pub async fn dao_vote(
         &self,
@@ -2619,4 +2867,153 @@ impl Drk {
 
         Ok(tx)
     }
+
+    /// Execute a DAO generic proposal.
+    pub async fn dao_exec_generic(&self, proposal: &ProposalRecord) -> Result<Transaction> {
+        if proposal.leaf_position.is_none() ||
+            proposal.money_snapshot_tree.is_none() ||
+            proposal.nullifiers_smt_snapshot.is_none() ||
+            proposal.tx_hash.is_none() ||
+            proposal.call_index.is_none()
+        {
+            return Err(Error::Custom(
+                "[dao_exec_transfer] Proposal seems to not have been deployed yet".to_string(),
+            ))
+        }
+
+        // Check proposal is not executed
+        if let Some(exec_tx_hash) = proposal.exec_tx_hash {
+            return Err(Error::Custom(format!(
+                "[dao_exec_transfer] Proposal was executed on transaction: {exec_tx_hash}"
+            )))
+        }
+
+        // Fetch DAO and check its deployed
+        let Ok(dao) = self.get_dao_by_bulla(&proposal.proposal.dao_bulla).await else {
+            return Err(Error::Custom(format!(
+                "[dao_exec_transfer] DAO {} was not found",
+                proposal.proposal.dao_bulla
+            )))
+        };
+        if dao.leaf_position.is_none() || dao.tx_hash.is_none() || dao.call_index.is_none() {
+            return Err(Error::Custom(
+                "[dao_exec_transfer] DAO seems to not have been deployed yet".to_string(),
+            ))
+        }
+
+        // Check that we have the exec key
+        if dao.params.exec_secret_key.is_none() {
+            return Err(Error::Custom(
+                "[dao_exec_transfer] We need the exec secret key to execute proposals for this DAO"
+                    .to_string(),
+            ))
+        }
+
+        // Check proposal is approved
+        let votes = self.get_dao_proposal_votes(&proposal.bulla()).await?;
+        let mut yes_vote_value = 0;
+        let mut yes_vote_blind = Blind::ZERO;
+        let mut all_vote_value = 0;
+        let mut all_vote_blind = Blind::ZERO;
+        for vote in votes {
+            if vote.vote_option {
+                yes_vote_value += vote.all_vote_value;
+            };
+            yes_vote_blind += vote.yes_vote_blind;
+            all_vote_value += vote.all_vote_value;
+            all_vote_blind += vote.all_vote_blind;
+        }
+        let approval_ratio = (yes_vote_value as f64 * 100.0) / all_vote_value as f64;
+        if all_vote_value < dao.params.dao.quorum ||
+            approval_ratio <
+                (dao.params.dao.approval_ratio_quot / dao.params.dao.approval_ratio_base)
+                    as f64
+        {
+            return Err(Error::Custom(
+                "[dao_exec_transfer] Proposal is not approved yet".to_string(),
+            ))
+        };
+
+        // Now we need to do a lookup for the zkas proof bincodes, and create
+        // the circuit objects and proving keys so we can build the transaction.
+        // We also do this through the RPC. First we grab the calls from money.
+        let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
+        let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
+        else {
+            return Err(Error::Custom("Fee circuit not found".to_string()))
+        };
+        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
+        let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
+        let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
+
+        // Now we grab the DAO bins
+        let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?;
+        let Some(dao_exec_zkbin) = zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_EXEC_NS)
+        else {
+            return Err(Error::Custom("[dao_exec_transfer] DAO Exec circuit not found".to_string()))
+        };
+        let dao_exec_zkbin = ZkBinary::decode(&dao_exec_zkbin.1)?;
+        let dao_exec_circuit = ZkCircuit::new(empty_witnesses(&dao_exec_zkbin)?, &dao_exec_zkbin);
+        let dao_exec_pk = ProvingKey::build(dao_exec_zkbin.k, &dao_exec_circuit);
+
+        // Fetch our money Merkle tree
+        let tree = self.get_money_tree().await?;
+
+        // Retrieve next block height and current block time target,
+        // to compute their window.
+        let next_block_height = self.get_next_block_height().await?;
+        let block_target = self.get_block_target().await?;
+        let current_blockwindow = blockwindow(next_block_height, block_target);
+
+        // Create the exec call
+        let exec_signature_secret = SecretKey::random(&mut OsRng);
+        let exec_builder = DaoExecCall {
+            proposal: proposal.proposal.clone(),
+            dao: dao.params.dao.clone(),
+            yes_vote_value,
+            all_vote_value,
+            yes_vote_blind,
+            all_vote_blind,
+            signature_secret: exec_signature_secret,
+            current_blockwindow,
+        };
+        let (exec_params, exec_proofs) = exec_builder.make(
+            &dao.params.exec_secret_key.unwrap(),
+            &None,
+            &dao_exec_zkbin,
+            &dao_exec_pk,
+        )?;
+
+        // Encode the call
+        let mut data = vec![DaoFunction::Exec as u8];
+        exec_params.encode_async(&mut data).await?;
+        let exec_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data };
+
+        // Create the TransactionBuilder containing above calls
+        let mut tx_builder = TransactionBuilder::new(
+            ContractCallLeaf { call: exec_call, proofs: exec_proofs },
+            vec![],
+        )?;
+
+        // We first have to execute the fee-less tx to gather its used gas, and then we feed
+        // it into the fee-creating function.
+        let mut tx = tx_builder.build()?;
+        let exec_sigs = tx.create_sigs(&[exec_signature_secret])?;
+        tx.signatures = vec![exec_sigs];
+
+        let (fee_call, fee_proofs, fee_secrets) =
+            self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?;
+
+        // Append the fee call to the transaction
+        tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
+
+        // Now build the actual transaction and sign it with all necessary keys.
+        let mut tx = tx_builder.build()?;
+        let sigs = tx.create_sigs(&[exec_signature_secret])?;
+        tx.signatures.push(sigs);
+        let sigs = tx.create_sigs(&fee_secrets)?;
+        tx.signatures.push(sigs);
+
+        Ok(tx)
+    }
 }

+ 89 - 1
bin/drk/src/main.rs

@@ -342,6 +342,18 @@ enum DaoSubcmd {
         user_data: Option<String>,
     },
 
+    /// Create a generic proposal for a DAO
+    ProposeGeneric {
+        /// Name identifier for the DAO
+        name: String,
+
+        /// Duration of the proposal, in block windows
+        duration: u64,
+
+        /// Optional user data to use
+        user_data: Option<String>,
+    },
+
     /// List DAO proposals
     Proposals {
         /// Name identifier for the DAO
@@ -1428,6 +1440,50 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
                 drk.stop_rpc_client().await
             }
 
+            DaoSubcmd::ProposeGeneric { name, duration, user_data } => {
+                let drk = new_wallet(
+                    blockchain_config.wallet_path,
+                    blockchain_config.wallet_pass,
+                    Some(blockchain_config.endpoint),
+                    ex,
+                    args.fun,
+                )
+                .await;
+
+                let user_data = match user_data {
+                    Some(u) => {
+                        let bytes: [u8; 32] = match bs58::decode(&u).into_vec()?.try_into() {
+                            Ok(b) => b,
+                            Err(e) => {
+                                eprintln!("Invalid user data: {e:?}");
+                                exit(2);
+                            }
+                        };
+
+                        match pallas::Base::from_repr(bytes).into() {
+                            Some(v) => Some(v),
+                            None => {
+                                eprintln!("Invalid user data");
+                                exit(2);
+                            }
+                        }
+                    }
+                    None => None,
+                };
+
+                let proposal = match drk.dao_propose_generic(&name, duration, user_data).await {
+                    Ok(p) => p,
+                    Err(e) => {
+                        eprintln!("Failed to create DAO transfer proposal: {e:?}");
+                        exit(2);
+                    }
+                };
+
+                println!("Generated proposal: {}", proposal.bulla());
+
+                drk.stop_rpc_client().await
+            }
+
             DaoSubcmd::Proposals { name } => {
                 let drk = new_wallet(
                     blockchain_config.wallet_path,
@@ -1483,7 +1539,9 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
                 }
 
                 if mint_proposal {
+                    // Identify proposal type by its auth calls
                     for call in &proposal.proposal.auth_calls {
+                        // We only support transfer right now
                         if call.function_code == DaoFunction::AuthMoneyTransfer as u8 {
                             let tx = match drk.dao_transfer_proposal_tx(&proposal).await {
                                 Ok(tx) => tx,
@@ -1498,6 +1556,20 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
                         }
                     }
 
+                    // If proposal has no auth calls, we consider it a generic one
+                    if proposal.proposal.auth_calls.is_empty() {
+                        let tx = match drk.dao_generic_proposal_tx(&proposal).await {
+                            Ok(tx) => tx,
+                            Err(e) => {
+                                eprintln!("Failed to create DAO generic proposal: {e:?}");
+                                exit(2);
+                            }
+                        };
+
+                        println!("{}", base64::encode(&serialize_async(&tx).await));
+                        return drk.stop_rpc_client().await
+                    }
+
                     eprintln!("Unsuported DAO proposal");
                     exit(2);
                 }
@@ -1581,7 +1653,7 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
 
                 let outcome = if table.is_empty() {
                     println!("Votes: No votes found");
-                    "Rejected"
+                    "Unknown"
                 } else {
                     println!("Votes:");
                     println!("{table}");
@@ -1751,7 +1823,9 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
                 .await;
                 let proposal = drk.get_dao_proposal_by_bulla(&bulla).await?;
 
+                // Identify proposal type by its auth calls
                 for call in &proposal.proposal.auth_calls {
+                    // We only support transfer right now
                     if call.function_code == DaoFunction::AuthMoneyTransfer as u8 {
                         let tx = match drk.dao_exec_transfer(&proposal).await {
                             Ok(tx) => tx,
@@ -1766,6 +1840,20 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
                     }
                 }
 
+                // If proposal has no auth calls, we consider it a generic one
+                if proposal.proposal.auth_calls.is_empty() {
+                    let tx = match drk.dao_exec_generic(&proposal).await {
+                        Ok(tx) => tx,
+                        Err(e) => {
+                            eprintln!("Failed to execute DAO generic proposal: {e:?}");
+                            exit(2);
+                        }
+                    };
+
+                    println!("{}", base64::encode(&serialize_async(&tx).await));
+                    return drk.stop_rpc_client().await
+                }
+
                 eprintln!("Unsuported DAO proposal");
                 exit(2);
             }