diff --git a/Cargo.lock b/Cargo.lock index 8ad99d983..fa0841d3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2667,6 +2667,7 @@ dependencies = [ "easy-parallel", "lazy_static", "log", + "num-bigint", "prettytable-rs", "rand 0.8.5", "rodio", diff --git a/bin/darkfid/src/rpc.rs b/bin/darkfid/src/rpc.rs index 5fce9728e..130df9efc 100644 --- a/bin/darkfid/src/rpc.rs +++ b/bin/darkfid/src/rpc.rs @@ -60,6 +60,7 @@ impl RequestHandler for Darkfid { "blockchain.get_block" => self.blockchain_get_block(req.id, req.params).await, "blockchain.get_tx" => self.blockchain_get_tx(req.id, req.params).await, "blockchain.last_known_block" => self.blockchain_last_known_block(req.id, req.params).await, + "blockchain.best_fork_next_block_height" => self.blockchain_best_fork_next_block_height(req.id, req.params).await, "blockchain.lookup_zkas" => self.blockchain_lookup_zkas(req.id, req.params).await, "blockchain.subscribe_blocks" => self.blockchain_subscribe_blocks(req.id, req.params).await, "blockchain.subscribe_txs" => self.blockchain_subscribe_txs(req.id, req.params).await, diff --git a/bin/darkfid/src/rpc_blockchain.rs b/bin/darkfid/src/rpc_blockchain.rs index 87148e3d8..258d897f3 100644 --- a/bin/darkfid/src/rpc_blockchain.rs +++ b/bin/darkfid/src/rpc_blockchain.rs @@ -117,16 +117,16 @@ impl Darkfid { } // RPCAPI: - // Queries the blockchain database to find the last known block + // Queries the blockchain database to find the last known block. // // **Params:** // * `None` // // **Returns:** - // * `u64` Height of the last known block, as string + // * `f64` Height of the last known block // // --> {"jsonrpc": "2.0", "method": "blockchain.last_known_block", "params": [], "id": 1} - // <-- {"jsonrpc": "2.0", "result": "1234", "id": 1} + // <-- {"jsonrpc": "2.0", "result": 1234, "id": 1} pub async fn blockchain_last_known_block(&self, id: u16, params: JsonValue) -> JsonResult { let params = params.get::>().unwrap(); if !params.is_empty() { @@ -141,6 +141,34 @@ impl Darkfid { JsonResponse::new(JsonValue::Number(last_block_height.0 as f64), id).into() } + // RPCAPI: + // Queries the validator to find the current best fork next block height. + // + // **Params:** + // * `None` + // + // **Returns:** + // * `f64` Height of the last known block + // + // --> {"jsonrpc": "2.0", "method": "blockchain.best_fork_next_block_height", "params": [], "id": 1} + // <-- {"jsonrpc": "2.0", "result": 1234, "id": 1} + pub async fn blockchain_best_fork_next_block_height( + &self, + id: u16, + params: JsonValue, + ) -> JsonResult { + let params = params.get::>().unwrap(); + if !params.is_empty() { + return JsonError::new(InvalidParams, None, id).into() + } + + let Ok(next_block_height) = self.validator.best_fork_next_block_height().await else { + return JsonError::new(InternalError, None, id).into() + }; + + JsonResponse::new(JsonValue::Number(next_block_height as f64), id).into() + } + // RPCAPI: // Initializes a subscription to new incoming blocks. // Once a subscription is established, `darkfid` will send JSON-RPC notifications of diff --git a/bin/darkfid/src/task/garbage_collect.rs b/bin/darkfid/src/task/garbage_collect.rs index 6b036f1b3..b7ad08b6a 100644 --- a/bin/darkfid/src/task/garbage_collect.rs +++ b/bin/darkfid/src/task/garbage_collect.rs @@ -24,7 +24,7 @@ use darkfi::{ Error, Result, }; use darkfi_sdk::crypto::MerkleTree; -use log::{error, info}; +use log::{debug, error, info}; use crate::Darkfid; @@ -50,6 +50,7 @@ pub async fn garbage_collect_task(node: Arc) -> Result<()> { for tx in txs { let tx_hash = tx.hash(); let tx_vec = [tx.clone()]; + let mut valid = false; // Grab a lock over current consensus forks state let mut forks = node.validator.consensus.forks.write().await; @@ -108,15 +109,32 @@ pub async fn garbage_collect_task(node: Arc) -> Result<()> { ) .await { - Ok(_) => {} + Ok(_) => valid = true, Err(Error::TxVerifyFailed(TxVerifyFailed::ErroneousTxs(_))) => { // Remove transaction from fork's mempool fork.mempool.retain(|tx| *tx != tx_hash); } - Err(e) => return Err(e), + Err(e) => { + error!( + target: "darkfid::task::garbage_collect_task", + "Verifying transaction {tx_hash} failed: {e}" + ); + return Err(e) + } } } + // Remove transaction if its invalid for all the forks + if !valid { + debug!(target: "darkfid::task::garbage_collect_task", "Removing invalid transaction: {tx_hash}"); + if let Err(e) = node.validator.blockchain.remove_pending_txs_hashes(&[tx_hash]) { + error!( + target: "darkfid::task::garbage_collect_task", + "Removing invalid transaction {tx_hash} failed: {e}" + ); + }; + } + // Drop forks lock drop(forks); } diff --git a/bin/drk/Cargo.toml b/bin/drk/Cargo.toml index 459fce026..164a64847 100644 --- a/bin/drk/Cargo.toml +++ b/bin/drk/Cargo.toml @@ -22,6 +22,7 @@ blake3 = "1.5.0" bs58 = "0.5.0" lazy_static = "1.4.0" log = "0.4.21" +num-bigint = "0.4.4" prettytable-rs = "0.10.0" rand = "0.8.5" rodio = {version = "0.17.3", default-features = false, features = ["minimp3"]} diff --git a/bin/drk/money.sql b/bin/drk/money.sql index 2cbc4835e..dd669b1cb 100644 --- a/bin/drk/money.sql +++ b/bin/drk/money.sql @@ -11,6 +11,12 @@ CREATE TABLE IF NOT EXISTS BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o_money_tr tree BLOB NOT NULL ); +-- The Sparse Merkle tree containing coins nullifiers +CREATE TABLE IF NOT EXISTS BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o_money_smt ( + smt_key BLOB INTEGER PRIMARY KEY NOT NULL, + smt_value BLOB NOT NULL +); + -- The keypairs in our wallet CREATE TABLE IF NOT EXISTS BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o_money_keys ( key_id INTEGER PRIMARY KEY NOT NULL, diff --git a/bin/drk/src/cli_util.rs b/bin/drk/src/cli_util.rs index b2c2f9cf3..1c2d8ae3d 100644 --- a/bin/drk/src/cli_util.rs +++ b/bin/drk/src/cli_util.rs @@ -192,9 +192,14 @@ pub fn generate_completions(shell: &str) -> Result<()> { let user_data = Arg::with_name("user-data").help("Optional user data to use"); - let transfer = SubCommand::with_name("transfer") - .about("Create a payment transaction") - .args(&vec![amount, token, recipient, spend_hook.clone(), user_data.clone()]); + let transfer = + SubCommand::with_name("transfer").about("Create a payment transaction").args(&vec![ + amount.clone(), + token.clone(), + recipient.clone(), + spend_hook.clone(), + user_data.clone(), + ]); // Otc let value_pair = Arg::with_name("value-pair") @@ -286,16 +291,19 @@ pub fn generate_completions(shell: &str) -> Result<()> { .about("Mint an imported DAO on-chain") .args(&vec![name.clone()]); - let recipient = - Arg::with_name("recipient").help("Pubkey to send tokens to with proposal success"); + let duration = Arg::with_name("duration").help("Duration of the proposal, in days"); - let amount = Arg::with_name("amount").help("Amount to send from DAO with proposal success"); - - let token = Arg::with_name("token").help("Token ID to send from DAO with proposal success"); - - let propose = SubCommand::with_name("propose") - .about("Create a proposal for a DAO") - .args(&vec![name.clone(), recipient, amount, token]); + let propose_transfer = SubCommand::with_name("propose-transfer") + .about("Create a transfer proposal for a DAO") + .args(&vec![ + name.clone(), + duration, + amount, + token, + recipient, + spend_hook.clone(), + user_data.clone(), + ]); let proposals = SubCommand::with_name("proposals").about("List DAO proposals").args(&vec![name.clone()]); @@ -332,7 +340,7 @@ pub fn generate_completions(shell: &str) -> Result<()> { list, balance, mint, - propose, + propose_transfer, proposals, proposal, vote, diff --git a/bin/drk/src/dao.rs b/bin/drk/src/dao.rs index 8dfeec9fc..3c5f0ad13 100644 --- a/bin/drk/src/dao.rs +++ b/bin/drk/src/dao.rs @@ -24,12 +24,13 @@ use rusqlite::types::Value; use darkfi::{ tx::{ContractCallLeaf, Transaction, TransactionBuilder}, - util::parse::encode_base10, + util::parse::{decode_base10, encode_base10}, zk::{empty_witnesses, halo2::Field, ProvingKey, ZkCircuit}, zkas::ZkBinary, Error, Result, }; use darkfi_dao_contract::{ + blockwindow, client::{make_mint_call, DaoProposeCall, DaoProposeStakeInput, DaoVoteCall, DaoVoteInput}, model::{Dao, DaoAuthCall, DaoBulla, DaoMintParams, DaoProposeParams, DaoVoteParams}, DaoFunction, DAO_CONTRACT_ZKAS_DAO_MINT_NS, DAO_CONTRACT_ZKAS_DAO_PROPOSE_INPUT_NS, @@ -37,7 +38,9 @@ use darkfi_dao_contract::{ DAO_CONTRACT_ZKAS_DAO_VOTE_MAIN_NS, }; use darkfi_money_contract::{ - client::OwnCoin, model::TokenId, MoneyFunction, MONEY_CONTRACT_ZKAS_FEE_NS_V1, + client::OwnCoin, + model::{CoinAttributes, TokenId}, + MoneyFunction, MONEY_CONTRACT_ZKAS_FEE_NS_V1, }; use darkfi_sdk::{ bridgetree, @@ -60,7 +63,8 @@ use darkfi_serial::{ use crate::{ convert_named_params, error::{WalletDbError, WalletDbResult}, - money::BALANCE_BASE10_DECIMALS, + money::{BALANCE_BASE10_DECIMALS, MONEY_SMT_COL_KEY, MONEY_SMT_COL_VALUE, MONEY_SMT_TABLE}, + walletdb::{WalletSmt, WalletStorage}, Drk, }; @@ -372,19 +376,14 @@ impl Drk { pub async fn initialize_dao(&self) -> WalletDbResult<()> { // Initialize DAO wallet schema let wallet_schema = include_str!("../dao.sql"); - self.wallet.exec_batch_sql(wallet_schema).await?; + self.wallet.exec_batch_sql(wallet_schema)?; // Check if we have to initialize the Merkle trees. // We check if one exists, but we actually create two. This should be written // a bit better and safer. // For now, on success, we don't care what's returned, but in the future // we should actually check it. - if self - .wallet - .query_single(&DAO_TREES_TABLE, &[DAO_TREES_COL_DAOS_TREE], &[]) - .await - .is_err() - { + if self.wallet.query_single(&DAO_TREES_TABLE, &[DAO_TREES_COL_DAOS_TREE], &[]).is_err() { println!("Initializing DAO Merkle trees"); let tree = MerkleTree::new(1); self.put_dao_trees(&tree, &tree).await?; @@ -402,27 +401,25 @@ impl Drk { ) -> WalletDbResult<()> { // First we remove old records let query = format!("DELETE FROM {};", *DAO_TREES_TABLE); - self.wallet.exec_sql(&query, &[]).await?; + self.wallet.exec_sql(&query, &[])?; // then we insert the new one let query = format!( "INSERT INTO {} ({}, {}) VALUES (?1, ?2);", *DAO_TREES_TABLE, DAO_TREES_COL_DAOS_TREE, DAO_TREES_COL_PROPOSALS_TREE, ); - self.wallet - .exec_sql( - &query, - rusqlite::params![ - serialize_async(daos_tree).await, - serialize_async(proposals_tree).await - ], - ) - .await + self.wallet.exec_sql( + &query, + rusqlite::params![ + serialize_async(daos_tree).await, + serialize_async(proposals_tree).await + ], + ) } /// Fetch DAO Merkle trees from the wallet. pub async fn get_dao_trees(&self) -> Result<(MerkleTree, MerkleTree)> { - let row = match self.wallet.query_single(&DAO_TREES_TABLE, &[], &[]).await { + let row = match self.wallet.query_single(&DAO_TREES_TABLE, &[], &[]) { Ok(r) => r, Err(e) => { return Err(Error::RusqliteError(format!( @@ -507,7 +504,7 @@ impl Drk { /// Fetch all known DAOs from the wallet. pub async fn get_daos(&self) -> Result> { - let rows = match self.wallet.query_multiple(&DAO_DAOS_TABLE, &[], &[]).await { + let rows = match self.wallet.query_multiple(&DAO_DAOS_TABLE, &[], &[]) { Ok(r) => r, Err(e) => { return Err(Error::RusqliteError(format!("[get_daos] DAOs retrieval failed: {e:?}"))) @@ -630,15 +627,11 @@ impl Drk { ))) }; - let rows = match self - .wallet - .query_multiple( - &DAO_PROPOSALS_TABLE, - &[], - convert_named_params! {(DAO_PROPOSALS_COL_DAO_NAME, name)}, - ) - .await - { + let rows = match self.wallet.query_multiple( + &DAO_PROPOSALS_TABLE, + &[], + convert_named_params! {(DAO_PROPOSALS_COL_DAO_NAME, name)}, + ) { Ok(r) => r, Err(e) => { return Err(Error::RusqliteError(format!( @@ -854,23 +847,21 @@ impl Drk { DAO_DAOS_COL_NAME, dao.name, ); - self.wallet - .exec_sql( - &query, - rusqlite::params![ - serialize_async(&dao.leaf_position.unwrap()).await, - serialize_async(&dao.tx_hash.unwrap()).await, - dao.call_index.unwrap() - ], - ) - .await?; + self.wallet.exec_sql( + &query, + rusqlite::params![ + serialize_async(&dao.leaf_position.unwrap()).await, + serialize_async(&dao.tx_hash.unwrap()).await, + dao.call_index.unwrap() + ], + )?; } Ok(()) } /// Unconfirm imported DAOs by removing the leaf position, txid, and call index. - pub async fn unconfirm_daos(&self, daos: &[DaoRecord]) -> WalletDbResult<()> { + pub fn unconfirm_daos(&self, daos: &[DaoRecord]) -> WalletDbResult<()> { for dao in daos { let query = format!( "UPDATE {} SET {} = ?1, {} = ?2, {} = ?3 WHERE {} = \'{}\';", @@ -881,9 +872,10 @@ impl Drk { DAO_DAOS_COL_NAME, dao.name, ); - self.wallet - .exec_sql(&query, rusqlite::params![None::>, None::>, None::,]) - .await?; + self.wallet.exec_sql( + &query, + rusqlite::params![None::>, None::>, None::,], + )?; } Ok(()) @@ -914,24 +906,20 @@ impl Drk { DAO_PROPOSALS_COL_CALL_INDEX, ); - if let Err(e) = self - .wallet - .exec_sql( - &query, - rusqlite::params![ - dao.name, - serialize_async(&proposal.recipient).await, - serialize_async(&proposal.amount).await, - serialize_async(&proposal.token_id).await, - serialize_async(&proposal.bulla_blind).await, - serialize_async(&proposal.leaf_position.unwrap()).await, - serialize_async(&proposal.money_snapshot_tree.clone().unwrap()).await, - serialize_async(&proposal.tx_hash.unwrap()).await, - proposal.call_index, - ], - ) - .await - { + if let Err(e) = self.wallet.exec_sql( + &query, + rusqlite::params![ + dao.name, + serialize_async(&proposal.recipient).await, + serialize_async(&proposal.amount).await, + serialize_async(&proposal.token_id).await, + serialize_async(&proposal.bulla_blind).await, + serialize_async(&proposal.leaf_position.unwrap()).await, + serialize_async(&proposal.money_snapshot_tree.clone().unwrap()).await, + serialize_async(&proposal.tx_hash.unwrap()).await, + proposal.call_index, + ], + ) { return Err(Error::RusqliteError(format!( "[put_dao_proposals] Proposal insert failed: {e:?}" ))) @@ -958,20 +946,18 @@ impl Drk { DAO_VOTES_COL_CALL_INDEX, ); - self.wallet - .exec_sql( - &query, - rusqlite::params![ - vote.proposal_id, - vote.vote_option as u64, - serialize_async(&vote.yes_vote_blind).await, - serialize_async(&vote.all_vote_value).await, - serialize_async(&vote.all_vote_blind).await, - serialize_async(&vote.tx_hash.unwrap()).await, - vote.call_index.unwrap(), - ], - ) - .await?; + self.wallet.exec_sql( + &query, + rusqlite::params![ + vote.proposal_id, + vote.vote_option as u64, + serialize_async(&vote.yes_vote_blind).await, + serialize_async(&vote.all_vote_value).await, + serialize_async(&vote.all_vote_blind).await, + serialize_async(&vote.tx_hash.unwrap()).await, + vote.call_index.unwrap(), + ], + )?; println!("DAO vote added to wallet"); } @@ -999,24 +985,24 @@ impl Drk { return Err(WalletDbError::GenericError); } }; - self.unconfirm_daos(&daos).await?; + self.unconfirm_daos(&daos)?; println!("Successfully unconfirmed DAOs"); Ok(()) } /// Reset all DAO proposals in the wallet. - pub async fn reset_dao_proposals(&self) -> WalletDbResult<()> { + pub fn reset_dao_proposals(&self) -> WalletDbResult<()> { println!("Resetting DAO proposals"); let query = format!("DELETE FROM {};", *DAO_PROPOSALS_TABLE); - self.wallet.exec_sql(&query, &[]).await + self.wallet.exec_sql(&query, &[]) } /// Reset all DAO votes in the wallet. - pub async fn reset_dao_votes(&self) -> WalletDbResult<()> { + pub fn reset_dao_votes(&self) -> WalletDbResult<()> { println!("Resetting DAO votes"); let query = format!("DELETE FROM {};", *DAO_VOTES_TABLE); - self.wallet.exec_sql(&query, &[]).await + self.wallet.exec_sql(&query, &[]) } /// Import given DAO params into the wallet with a given name. @@ -1034,10 +1020,8 @@ impl Drk { "INSERT INTO {} ({}, {}) VALUES (?1, ?2);", *DAO_DAOS_TABLE, DAO_DAOS_COL_NAME, DAO_DAOS_COL_PARAMS, ); - if let Err(e) = self - .wallet - .exec_sql(&query, rusqlite::params![name, serialize_async(¶ms).await,]) - .await + if let Err(e) = + self.wallet.exec_sql(&query, rusqlite::params![name, serialize_async(¶ms).await,]) { return Err(Error::RusqliteError(format!("[import_dao] DAO insert failed: {e:?}"))) }; @@ -1047,11 +1031,11 @@ impl Drk { /// Fetch a DAO given its name. pub async fn get_dao_by_name(&self, name: &str) -> Result { - let row = match self - .wallet - .query_single(&DAO_DAOS_TABLE, &[], convert_named_params! {(DAO_DAOS_COL_NAME, name)}) - .await - { + let row = match self.wallet.query_single( + &DAO_DAOS_TABLE, + &[], + convert_named_params! {(DAO_DAOS_COL_NAME, name)}, + ) { Ok(r) => r, Err(e) => { return Err(Error::RusqliteError(format!( @@ -1111,15 +1095,11 @@ impl Drk { /// Fetch a DAO proposal by its ID pub async fn get_dao_proposal_by_id(&self, proposal_id: u64) -> Result { // Grab the proposal record - let row = match self - .wallet - .query_single( - &DAO_PROPOSALS_TABLE, - &[], - convert_named_params! {(DAO_PROPOSALS_COL_PROPOSAL_ID, proposal_id)}, - ) - .await - { + let row = match self.wallet.query_single( + &DAO_PROPOSALS_TABLE, + &[], + convert_named_params! {(DAO_PROPOSALS_COL_PROPOSAL_ID, proposal_id)}, + ) { Ok(r) => r, Err(e) => { return Err(Error::RusqliteError(format!( @@ -1140,15 +1120,11 @@ impl Drk { // Fetch all known DAO proposal votes from the wallet given a proposal ID pub async fn get_dao_proposal_votes(&self, proposal_id: u64) -> Result> { - let rows = match self - .wallet - .query_multiple( - &DAO_VOTES_TABLE, - &[], - convert_named_params! {(DAO_VOTES_COL_PROPOSAL_ID, proposal_id)}, - ) - .await - { + let rows = match self.wallet.query_multiple( + &DAO_VOTES_TABLE, + &[], + convert_named_params! {(DAO_VOTES_COL_PROPOSAL_ID, proposal_id)}, + ) { Ok(r) => r, Err(e) => { return Err(Error::RusqliteError(format!( @@ -1323,89 +1299,120 @@ impl Drk { Ok(tx) } - /// Create a DAO proposal - pub async fn dao_propose( + /// Create a DAO transfer proposal. + #[allow(clippy::too_many_arguments)] + pub async fn dao_propose_transfer( &self, name: &str, - _recipient: PublicKey, - amount: u64, + duration_days: u64, + amount: &str, token_id: TokenId, + recipient: PublicKey, + spend_hook: Option, + user_data: Option, ) -> Result { + // 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() { return Err(Error::Custom( - "[dao_propose] DAO seems to not have been deployed yet".to_string(), + "[dao_propose_transfer] DAO seems to not have been deployed yet".to_string(), )) } - let bulla = dao.bulla(); - let owncoins = self.get_coins(false).await?; - + // Fetch DAO unspent OwnCoins to see what its balance is let dao_spend_hook = FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 } .to_func_id(); - - let mut dao_owncoins: Vec = owncoins.iter().map(|x| x.0.clone()).collect(); - dao_owncoins.retain(|x| { - x.note.token_id == token_id && - x.note.spend_hook == dao_spend_hook && - x.note.user_data == bulla.inner() - }); - - let mut gov_owncoins: Vec = owncoins.iter().map(|x| x.0.clone()).collect(); - gov_owncoins.retain(|x| x.note.token_id == dao.params.dao.gov_token_id); - + let dao_bulla = dao.bulla(); + let dao_owncoins = + self.get_contract_token_coins(&token_id, &dao_spend_hook, &dao_bulla.inner()).await?; if dao_owncoins.is_empty() { return Err(Error::Custom(format!( - "[dao_propose] Did not find any {token_id} coins owned by this DAO" - ))) - } - - if gov_owncoins.is_empty() { - return Err(Error::Custom(format!( - "[dao_propose] Did not find any governance {} coins in wallet", - dao.params.dao.gov_token_id + "[dao_propose_transfer] Did not find any {token_id} unspent coins owned by this DAO" ))) } + // Check DAO balance is sufficient + let amount = decode_base10(amount, BALANCE_BASE10_DECIMALS, false)?; if dao_owncoins.iter().map(|x| x.note.value).sum::() < amount { return Err(Error::Custom(format!( - "[dao_propose] Not enough DAO balance for token ID: {}", - token_id + "[dao_propose_transfer] Not enough DAO balance for token ID: {token_id}", ))) } - if gov_owncoins.iter().map(|x| x.note.value).sum::() < dao.params.dao.proposer_limit { + // 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_propose] Not enough gov token {} balance to propose", + "[dao_propose_transfer] Did not find any governance {} coins in wallet", dao.params.dao.gov_token_id ))) } - // FIXME: Here we're looking for a coin == proposer_limit but this shouldn't have to - // be the case { - let Some(gov_coin) = - gov_owncoins.iter().find(|x| x.note.value == dao.params.dao.proposer_limit) - else { - return Err(Error::Custom(format!( - "[dao_propose] Did not find a single gov coin of value {}", - dao.params.dao.proposer_limit - ))) - }; - // } + // 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 >= amount { + break + } - // Lookup the zkas bins + total_value += gov_owncoin.note.value; + gov_owncoins_to_use.push(gov_owncoin); + } + + // Check our governance coins balance is sufficient + if total_value < amount { + return Err(Error::Custom(format!( + "[dao_propose_transfer] Not enough gov token {} balance to propose", + dao.params.dao.gov_token_id + ))) + } + + // Generate proposal coin attributes + let proposal_coinattrs = vec![CoinAttributes { + public_key: recipient, + value: amount, + token_id, + spend_hook: spend_hook.unwrap_or(FuncId::none()), + user_data: user_data.unwrap_or(pallas::Base::ZERO), + blind: Blind::random(&mut OsRng), + }]; + + // 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_propose_transfer] 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_propose] Propose Burn circuit not found".to_string())) + return Err(Error::Custom( + "[dao_propose_transfer] 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_propose] Propose Main circuit not found".to_string())) + return Err(Error::Custom( + "[dao_propose_transfer] Propose Main circuit not found".to_string(), + )) }; let propose_burn_zkbin = ZkBinary::decode(&propose_burn_zkbin.1)?; @@ -1416,55 +1423,36 @@ impl Drk { let propose_main_circuit = ZkCircuit::new(empty_witnesses(&propose_main_zkbin)?, &propose_main_zkbin); - println!("Creating Propose Burn circuit proving key"); + // Creating DAO ProposeBurn and ProposeMain circuits proving keys let propose_burn_pk = ProvingKey::build(propose_burn_zkbin.k, &propose_burn_circuit); - println!("Creating Propose Main circuit proving key"); let propose_main_pk = ProvingKey::build(propose_main_zkbin.k, &propose_main_circuit); - // Now create the parameters for the proposal tx - let signature_secret = SecretKey::random(&mut OsRng); - - // Get the Merkle path for the gov coin in the money tree + // Fetch our money Merkle tree let money_merkle_tree = self.get_money_tree().await?; - let gov_coin_merkle_path = money_merkle_tree.witness(gov_coin.leaf_position, 0).unwrap(); - // Fetch the daos Merkle tree - let (daos_tree, _) = self.get_dao_trees().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); + } - // This is complete wrong and needs to be fixed later - // Non-trivial to fix, so we just make a workaround for now - - let hasher = PoseidonFp::new(); - let store = MemoryStorageFp::new(); - let money_null_smt = SmtMemoryFp::new(store, hasher.clone(), &EMPTY_NODES_FP); - - let input = DaoProposeStakeInput { - secret: gov_coin.secret, // <-- TODO: Is this correct? - note: gov_coin.note.clone(), - leaf_position: gov_coin.leaf_position, - merkle_path: gov_coin_merkle_path, - money_null_smt: &money_null_smt, - signature_secret, - }; - - 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) - }; - - // TODO: - /* // Convert coin_params to actual coins let mut proposal_coins = vec![]; for coin_params in proposal_coinattrs { proposal_coins.push(coin_params.to_coin()); } - */ - let proposal_data = vec![]; - //proposal_coins.encode_async(&mut proposal_data).await.unwrap(); + let mut proposal_data = vec![]; + proposal_coins.encode_async(&mut proposal_data).await?; + // Create Auth calls let auth_calls = vec![ DaoAuthCall { contract_id: *DAO_CONTRACT_ID, @@ -1478,31 +1466,55 @@ impl Drk { }, ]; - // TODO: get current height to calculate day - // Also contract must check we don't mint a proposal that its creation day is - // less than current height + // Retrieve current block height and compute current day + let current_block_height = self.get_next_block_height().await?; + let creation_day = blockwindow(current_block_height); + // Create the actual proposal // TODO: Simplify this model struct import once // we use the structs from contract everwhere let proposal = darkfi_dao_contract::model::DaoProposal { auth_calls, - creation_day: 0, - duration_days: 30, - user_data: pallas::Base::ZERO, - dao_bulla: dao.bulla(), + creation_day, + duration_days, + user_data: user_data.unwrap_or(pallas::Base::ZERO), + dao_bulla, blind: Blind::random(&mut OsRng), }; + // 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 { - inputs: vec![input], + money_null_smt: &money_null_smt, + inputs, proposal, dao: dao.params.dao, dao_leaf_position: dao.leaf_position.unwrap(), dao_merkle_path, dao_merkle_root, + signature_secret, }; - println!("Creating ZK proofs..."); let (params, proofs) = call.make( &propose_burn_zkbin, &propose_burn_pk, @@ -1510,14 +1522,34 @@ impl Drk { &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) } diff --git a/bin/drk/src/deploy.rs b/bin/drk/src/deploy.rs index e581a530e..e57a24255 100644 --- a/bin/drk/src/deploy.rs +++ b/bin/drk/src/deploy.rs @@ -50,10 +50,10 @@ pub const DEPLOY_AUTH_COL_IS_FROZEN: &str = "is_frozen"; impl Drk { /// Initialize wallet with tables for the Deployooor contract. - pub async fn initialize_deployooor(&self) -> WalletDbResult<()> { + pub fn initialize_deployooor(&self) -> WalletDbResult<()> { // Initialize Deployooor wallet schema let wallet_schema = include_str!("../deploy.sql"); - self.wallet.exec_batch_sql(wallet_schema).await?; + self.wallet.exec_batch_sql(wallet_schema)?; Ok(()) } @@ -68,7 +68,7 @@ impl Drk { "INSERT INTO {} ({}, {}) VALUES (?1, ?2);", *DEPLOY_AUTH_TABLE, DEPLOY_AUTH_COL_DEPLOY_AUTHORITY, DEPLOY_AUTH_COL_IS_FROZEN, ); - self.wallet.exec_sql(&query, rusqlite::params![serialize_async(&keypair).await, 0]).await?; + self.wallet.exec_sql(&query, rusqlite::params![serialize_async(&keypair).await, 0])?; eprintln!("Created new contract deploy authority"); println!("Contract ID: {}", ContractId::derive_public(keypair.public)); @@ -78,7 +78,7 @@ impl Drk { /// List contract deploy authorities from the wallet pub async fn list_deploy_auth(&self) -> Result> { - let rows = match self.wallet.query_multiple(&DEPLOY_AUTH_TABLE, &[], &[]).await { + let rows = match self.wallet.query_multiple(&DEPLOY_AUTH_TABLE, &[], &[]) { Ok(r) => r, Err(e) => { return Err(Error::RusqliteError(format!( @@ -111,15 +111,11 @@ impl Drk { /// Retrieve a deploy authority keypair given an index async fn get_deploy_auth(&self, idx: u64) -> Result { // Find the deploy authority keypair - let row = match self - .wallet - .query_single( - &DEPLOY_AUTH_TABLE, - &[DEPLOY_AUTH_COL_DEPLOY_AUTHORITY], - convert_named_params! {(DEPLOY_AUTH_COL_ID, idx)}, - ) - .await - { + let row = match self.wallet.query_single( + &DEPLOY_AUTH_TABLE, + &[DEPLOY_AUTH_COL_DEPLOY_AUTHORITY], + convert_named_params! {(DEPLOY_AUTH_COL_ID, idx)}, + ) { Ok(v) => v, Err(e) => { return Err(Error::RusqliteError(format!( diff --git a/bin/drk/src/error.rs b/bin/drk/src/error.rs index bf9877621..7f69f985e 100644 --- a/bin/drk/src/error.rs +++ b/bin/drk/src/error.rs @@ -25,6 +25,7 @@ pub type WalletDbResult = std::result::Result; pub enum WalletDbError { // Connection related errors ConnectionFailed = -32100, + FailedToAquireLock = -32101, // Configuration related errors PragmaUpdateError = -32110, diff --git a/bin/drk/src/main.rs b/bin/drk/src/main.rs index 7b4859326..d96f9d7f6 100644 --- a/bin/drk/src/main.rs +++ b/bin/drk/src/main.rs @@ -352,19 +352,28 @@ enum DaoSubcmd { name: String, }, - /// Create a proposal for a DAO - Propose { + /// Create a transfer proposal for a DAO + ProposeTransfer { /// Name identifier for the DAO name: String, - /// Pubkey to send tokens to with proposal success - recipient: String, + /// Duration of the proposal, in days + duration: u64, - /// Amount to send from DAO with proposal success + /// Amount to send amount: String, - /// Token ID to send from DAO with proposal success + /// Token ID to send token: String, + + /// Recipient address + recipient: String, + + /// Optional contract spend hook to use + spend_hook: Option, + + /// Optional user data to use + user_data: Option, }, /// List DAO proposals @@ -586,9 +595,9 @@ impl Drk { } /// Initialize wallet with tables for drk - async fn initialize_wallet(&self) -> Result<()> { + fn initialize_wallet(&self) -> Result<()> { let wallet_schema = include_str!("../wallet.sql"); - if let Err(e) = self.wallet.exec_batch_sql(wallet_schema).await { + if let Err(e) = self.wallet.exec_batch_sql(wallet_schema) { eprintln!("Error initializing wallet: {e:?}"); exit(2); } @@ -655,7 +664,7 @@ async fn realmain(args: Args, ex: Arc>) -> Result<()> { let drk = Drk::new(args.wallet_path, args.wallet_pass, None, ex).await?; if initialize { - drk.initialize_wallet().await?; + drk.initialize_wallet()?; if let Err(e) = drk.initialize_money().await { eprintln!("Failed to initialize Money: {e:?}"); exit(2); @@ -664,7 +673,7 @@ async fn realmain(args: Args, ex: Arc>) -> Result<()> { eprintln!("Failed to initialize DAO: {e:?}"); exit(2); } - if let Err(e) = drk.initialize_deployooor().await { + if let Err(e) = drk.initialize_deployooor() { eprintln!("Failed to initialize Deployooor: {e:?}"); exit(2); } @@ -749,7 +758,7 @@ async fn realmain(args: Args, ex: Arc>) -> Result<()> { } if let Some(idx) = default_address { - if let Err(e) = drk.set_default_address(idx).await { + if let Err(e) = drk.set_default_address(idx) { eprintln!("Failed to set default address: {e:?}"); exit(2); } @@ -1197,12 +1206,23 @@ async fn realmain(args: Args, ex: Arc>) -> Result<()> { Ok(()) } - DaoSubcmd::Propose { name, recipient, amount, token } => { + DaoSubcmd::ProposeTransfer { + name, + duration, + amount, + token, + recipient, + spend_hook, + user_data, + } => { + let drk = + Drk::new(args.wallet_path, args.wallet_pass, Some(args.endpoint), ex).await?; + if let Err(e) = f64::from_str(&amount) { eprintln!("Invalid amount: {e:?}"); exit(2); } - let amount = decode_base10(&amount, BALANCE_BASE10_DECIMALS, true)?; + let rcpt = match PublicKey::from_str(&recipient) { Ok(r) => r, Err(e) => { @@ -1211,8 +1231,6 @@ async fn realmain(args: Args, ex: Arc>) -> Result<()> { } }; - let drk = - Drk::new(args.wallet_path, args.wallet_pass, Some(args.endpoint), ex).await?; let token_id = match drk.get_token(token).await { Ok(t) => t, Err(e) => { @@ -1221,13 +1239,51 @@ async fn realmain(args: Args, ex: Arc>) -> Result<()> { } }; - let tx = match drk.dao_propose(&name, rcpt, amount, token_id).await { + let spend_hook = match spend_hook { + Some(s) => match FuncId::from_str(&s) { + Ok(s) => Some(s), + Err(e) => { + eprintln!("Invalid spend hook: {e:?}"); + exit(2); + } + }, + None => None, + }; + + 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 tx = match drk + .dao_propose_transfer( + &name, duration, &amount, token_id, rcpt, spend_hook, user_data, + ) + .await + { Ok(tx) => tx, Err(e) => { - eprintln!("Failed to create DAO proposal: {e:?}"); + eprintln!("Failed to create DAO transfer proposal: {e:?}"); exit(2); } }; + println!("{}", base64::encode(&serialize_async(&tx).await)); Ok(()) } @@ -1498,7 +1554,7 @@ async fn realmain(args: Args, ex: Arc>) -> Result<()> { return Ok(()) } - let map = match drk.get_txs_history().await { + let map = match drk.get_txs_history() { Ok(m) => m, Err(e) => { eprintln!("Failed to retrieve transactions history records: {e:?}"); diff --git a/bin/drk/src/money.rs b/bin/drk/src/money.rs index 88550d523..81ade82cd 100644 --- a/bin/drk/src/money.rs +++ b/bin/drk/src/money.rs @@ -44,8 +44,10 @@ use darkfi_money_contract::{ use darkfi_sdk::{ bridgetree, crypto::{ - note::AeadEncryptedNote, BaseBlind, FuncId, Keypair, MerkleNode, MerkleTree, PublicKey, - ScalarBlind, SecretKey, MONEY_CONTRACT_ID, + note::AeadEncryptedNote, + smt::{PoseidonFp, EMPTY_NODES_FP}, + BaseBlind, FuncId, Keypair, MerkleNode, MerkleTree, PublicKey, ScalarBlind, SecretKey, + MONEY_CONTRACT_ID, }, dark_tree::DarkLeaf, pasta::pallas, @@ -56,7 +58,9 @@ use darkfi_serial::{deserialize_async, serialize_async, AsyncEncodable}; use crate::{ convert_named_params, error::{WalletDbError, WalletDbResult}, - kaching, Drk, + kaching, + walletdb::{WalletSmt, WalletStorage}, + Drk, }; // Wallet SQL table constant names. These have to represent the `wallet.sql` @@ -66,6 +70,7 @@ lazy_static! { format!("{}_money_info", MONEY_CONTRACT_ID.to_string()); pub static ref MONEY_TREE_TABLE: String = format!("{}_money_tree", MONEY_CONTRACT_ID.to_string()); + pub static ref MONEY_SMT_TABLE: String = format!("{}_money_smt", MONEY_CONTRACT_ID.to_string()); pub static ref MONEY_KEYS_TABLE: String = format!("{}_money_keys", MONEY_CONTRACT_ID.to_string()); pub static ref MONEY_COINS_TABLE: String = @@ -82,6 +87,10 @@ pub const MONEY_INFO_COL_LAST_SCANNED_BLOCK: &str = "last_scanned_block"; // MONEY_TREE_TABLE pub const MONEY_TREE_COL_TREE: &str = "tree"; +// MONEY_SMT_TABLE +pub const MONEY_SMT_COL_KEY: &str = "smt_key"; +pub const MONEY_SMT_COL_VALUE: &str = "smt_value"; + // MONEY_KEYS_TABLE pub const MONEY_KEYS_COL_KEY_ID: &str = "key_id"; pub const MONEY_KEYS_COL_IS_DEFAULT: &str = "is_default"; @@ -120,7 +129,7 @@ impl Drk { pub async fn initialize_money(&self) -> WalletDbResult<()> { // Initialize Money wallet schema let wallet_schema = include_str!("../money.sql"); - self.wallet.exec_batch_sql(wallet_schema).await?; + self.wallet.exec_batch_sql(wallet_schema)?; // Check if we have to initialize the Merkle tree. // We check if we find a row in the tree table, and if not, we create a @@ -138,12 +147,12 @@ impl Drk { // We maintain the last scanned block as part of the Money contract, // but at this moment it is also somewhat applicable to DAO scans. - if self.last_scanned_block().await.is_err() { + if self.last_scanned_block().is_err() { let query = format!( "INSERT INTO {} ({}) VALUES (?1);", *MONEY_INFO_TABLE, MONEY_INFO_COL_LAST_SCANNED_BLOCK ); - self.wallet.exec_sql(&query, rusqlite::params![0]).await?; + self.wallet.exec_sql(&query, rusqlite::params![0])?; } // Insert DRK alias @@ -167,16 +176,14 @@ impl Drk { MONEY_KEYS_COL_PUBLIC, MONEY_KEYS_COL_SECRET ); - self.wallet - .exec_sql( - &query, - rusqlite::params![ - is_default, - serialize_async(&keypair.public).await, - serialize_async(&keypair.secret).await - ], - ) - .await?; + self.wallet.exec_sql( + &query, + rusqlite::params![ + is_default, + serialize_async(&keypair.public).await, + serialize_async(&keypair.secret).await + ], + )?; println!("New address:"); println!("{}", keypair.public); @@ -186,15 +193,11 @@ impl Drk { /// Fetch default secret key from the wallet. pub async fn default_secret(&self) -> Result { - let row = match self - .wallet - .query_single( - &MONEY_KEYS_TABLE, - &[MONEY_KEYS_COL_SECRET], - convert_named_params! {(MONEY_KEYS_COL_IS_DEFAULT, 1)}, - ) - .await - { + let row = match self.wallet.query_single( + &MONEY_KEYS_TABLE, + &[MONEY_KEYS_COL_SECRET], + convert_named_params! {(MONEY_KEYS_COL_IS_DEFAULT, 1)}, + ) { Ok(r) => r, Err(e) => { return Err(Error::RusqliteError(format!( @@ -213,15 +216,11 @@ impl Drk { /// Fetch default pubkey from the wallet. pub async fn default_address(&self) -> Result { - let row = match self - .wallet - .query_single( - &MONEY_KEYS_TABLE, - &[MONEY_KEYS_COL_PUBLIC], - convert_named_params! {(MONEY_KEYS_COL_IS_DEFAULT, 1)}, - ) - .await - { + let row = match self.wallet.query_single( + &MONEY_KEYS_TABLE, + &[MONEY_KEYS_COL_PUBLIC], + convert_named_params! {(MONEY_KEYS_COL_IS_DEFAULT, 1)}, + ) { Ok(r) => r, Err(e) => { return Err(Error::RusqliteError(format!( @@ -239,11 +238,11 @@ impl Drk { } /// Set provided index address as default in the wallet. - pub async fn set_default_address(&self, idx: usize) -> WalletDbResult<()> { + pub fn set_default_address(&self, idx: usize) -> WalletDbResult<()> { // First we update previous default record let is_default = 0; let query = format!("UPDATE {} SET {} = ?1", *MONEY_KEYS_TABLE, MONEY_KEYS_COL_IS_DEFAULT,); - self.wallet.exec_sql(&query, rusqlite::params![is_default]).await?; + self.wallet.exec_sql(&query, rusqlite::params![is_default])?; // and then we set the new one let is_default = 1; @@ -251,12 +250,12 @@ impl Drk { "UPDATE {} SET {} = ?1 WHERE {} = ?2", *MONEY_KEYS_TABLE, MONEY_KEYS_COL_IS_DEFAULT, MONEY_KEYS_COL_KEY_ID, ); - self.wallet.exec_sql(&query, rusqlite::params![is_default, idx]).await + self.wallet.exec_sql(&query, rusqlite::params![is_default, idx]) } /// Fetch all pukeys from the wallet. pub async fn addresses(&self) -> Result> { - let rows = match self.wallet.query_multiple(&MONEY_KEYS_TABLE, &[], &[]).await { + let rows = match self.wallet.query_multiple(&MONEY_KEYS_TABLE, &[], &[]) { Ok(r) => r, Err(e) => { return Err(Error::RusqliteError(format!( @@ -299,18 +298,15 @@ impl Drk { /// Fetch all secret keys from the wallet. pub async fn get_money_secrets(&self) -> Result> { - let rows = match self - .wallet - .query_multiple(&MONEY_KEYS_TABLE, &[MONEY_KEYS_COL_SECRET], &[]) - .await - { - Ok(r) => r, - Err(e) => { - return Err(Error::RusqliteError(format!( - "[get_money_secrets] Secret keys retrieval failed: {e:?}" - ))) - } - }; + let rows = + match self.wallet.query_multiple(&MONEY_KEYS_TABLE, &[MONEY_KEYS_COL_SECRET], &[]) { + Ok(r) => r, + Err(e) => { + return Err(Error::RusqliteError(format!( + "[get_money_secrets] Secret keys retrieval failed: {e:?}" + ))) + } + }; let mut secrets = Vec::with_capacity(rows.len()); @@ -356,7 +352,7 @@ impl Drk { MONEY_KEYS_COL_SECRET ); if let Err(e) = - self.wallet.exec_sql(&query, rusqlite::params![is_default, public, secret]).await + self.wallet.exec_sql(&query, rusqlite::params![is_default, public, secret]) { return Err(Error::RusqliteError(format!( "[import_money_secrets] Inserting new address failed: {e:?}" @@ -393,15 +389,13 @@ impl Drk { /// The boolean in the returned tuple notes if the coin was marked as spent. pub async fn get_coins(&self, fetch_spent: bool) -> Result> { let query = if fetch_spent { - self.wallet.query_multiple(&MONEY_COINS_TABLE, &[], &[]).await + self.wallet.query_multiple(&MONEY_COINS_TABLE, &[], &[]) } else { - self.wallet - .query_multiple( - &MONEY_COINS_TABLE, - &[], - convert_named_params! {(MONEY_COINS_COL_IS_SPENT, false)}, - ) - .await + self.wallet.query_multiple( + &MONEY_COINS_TABLE, + &[], + convert_named_params! {(MONEY_COINS_COL_IS_SPENT, false)}, + ) }; let rows = match query { @@ -427,8 +421,7 @@ impl Drk { &MONEY_COINS_TABLE, &[], convert_named_params! {(MONEY_COINS_COL_IS_SPENT, false), (MONEY_COINS_COL_TOKEN_ID, serialize_async(token_id).await), (MONEY_COINS_COL_SPEND_HOOK, serialize_async(&FuncId::none()).await)}, - ) - .await; + ); let rows = match query { Ok(r) => r, @@ -447,6 +440,36 @@ impl Drk { Ok(owncoins) } + /// Fetch provided contract specified token unspend balances from the wallet. + pub async fn get_contract_token_coins( + &self, + token_id: &TokenId, + spend_hook: &FuncId, + user_data: &pallas::Base, + ) -> Result> { + let query = self.wallet.query_multiple( + &MONEY_COINS_TABLE, + &[], + convert_named_params! {(MONEY_COINS_COL_IS_SPENT, false), (MONEY_COINS_COL_TOKEN_ID, serialize_async(token_id).await), (MONEY_COINS_COL_SPEND_HOOK, serialize_async(spend_hook).await), (MONEY_COINS_COL_USER_DATA, serialize_async(user_data).await)}, + ); + + let rows = match query { + Ok(r) => r, + Err(e) => { + return Err(Error::RusqliteError(format!( + "[get_contract_token_coins] Coins retrieval failed: {e:?}" + ))) + } + }; + + let mut owncoins = Vec::with_capacity(rows.len()); + for row in rows { + owncoins.push(self.parse_coin_record(&row).await?.0) + } + + Ok(owncoins) + } + /// Auxiliary function to parse a `MONEY_COINS_TABLE` record. /// The boolean in the returned tuple notes if the coin was marked as spent. async fn parse_coin_record(&self, row: &[Value]) -> Result<(OwnCoin, bool, String)> { @@ -539,12 +562,10 @@ impl Drk { "INSERT OR REPLACE INTO {} ({}, {}) VALUES (?1, ?2);", *MONEY_ALIASES_TABLE, MONEY_ALIASES_COL_ALIAS, MONEY_ALIASES_COL_TOKEN_ID, ); - self.wallet - .exec_sql( - &query, - rusqlite::params![serialize_async(&alias).await, serialize_async(&token_id).await], - ) - .await + self.wallet.exec_sql( + &query, + rusqlite::params![serialize_async(&alias).await, serialize_async(&token_id).await], + ) } /// Fetch all aliases from the wallet. @@ -554,7 +575,7 @@ impl Drk { alias_filter: Option, token_id_filter: Option, ) -> Result> { - let rows = match self.wallet.query_multiple(&MONEY_ALIASES_TABLE, &[], &[]).await { + let rows = match self.wallet.query_multiple(&MONEY_ALIASES_TABLE, &[], &[]) { Ok(r) => r, Err(e) => { return Err(Error::RusqliteError(format!( @@ -612,7 +633,7 @@ impl Drk { "DELETE FROM {} WHERE {} = ?1;", *MONEY_ALIASES_TABLE, MONEY_ALIASES_COL_ALIAS, ); - self.wallet.exec_sql(&query, rusqlite::params![serialize_async(&alias).await]).await + self.wallet.exec_sql(&query, rusqlite::params![serialize_async(&alias).await]) } /// Mark a given coin in the wallet as unspent. @@ -625,37 +646,34 @@ impl Drk { MONEY_COINS_COL_SPENT_TX_HASH, MONEY_COINS_COL_COIN ); - self.wallet - .exec_sql( - &query, - rusqlite::params![is_spend, "-", serialize_async(&coin.inner()).await], - ) - .await + self.wallet.exec_sql( + &query, + rusqlite::params![is_spend, "-", serialize_async(&coin.inner()).await], + ) } /// Replace the Money Merkle tree in the wallet. pub async fn put_money_tree(&self, tree: &MerkleTree) -> WalletDbResult<()> { // First we remove old record let query = format!("DELETE FROM {};", *MONEY_TREE_TABLE); - self.wallet.exec_sql(&query, &[]).await?; + self.wallet.exec_sql(&query, &[])?; // then we insert the new one let query = format!("INSERT INTO {} ({}) VALUES (?1);", *MONEY_TREE_TABLE, MONEY_TREE_COL_TREE,); - self.wallet.exec_sql(&query, rusqlite::params![serialize_async(tree).await]).await + self.wallet.exec_sql(&query, rusqlite::params![serialize_async(tree).await]) } /// Fetch the Money Merkle tree from the wallet. pub async fn get_money_tree(&self) -> Result { - let row = - match self.wallet.query_single(&MONEY_TREE_TABLE, &[MONEY_TREE_COL_TREE], &[]).await { - Ok(r) => r, - Err(e) => { - return Err(Error::RusqliteError(format!( - "[get_money_tree] Tree retrieval failed: {e:?}" - ))) - } - }; + let row = match self.wallet.query_single(&MONEY_TREE_TABLE, &[MONEY_TREE_COL_TREE], &[]) { + Ok(r) => r, + Err(e) => { + return Err(Error::RusqliteError(format!( + "[get_money_tree] Tree retrieval failed: {e:?}" + ))) + } + }; let Value::Blob(ref tree_bytes) = row[0] else { return Err(Error::ParseFailed("[get_money_tree] Tree bytes parsing failed")) @@ -665,11 +683,12 @@ impl Drk { } /// Get the last scanned block height from the wallet. - pub async fn last_scanned_block(&self) -> WalletDbResult { - let ret = self - .wallet - .query_single(&MONEY_INFO_TABLE, &[MONEY_INFO_COL_LAST_SCANNED_BLOCK], &[]) - .await?; + pub fn last_scanned_block(&self) -> WalletDbResult { + let ret = self.wallet.query_single( + &MONEY_INFO_TABLE, + &[MONEY_INFO_COL_LAST_SCANNED_BLOCK], + &[], + )?; let Value::Integer(height) = ret[0] else { return Err(WalletDbError::ParseColumnValueError); }; @@ -803,6 +822,7 @@ impl Drk { "[apply_tx_money_data] Put Money tree failed: {e:?}" ))) } + self.smt_insert(&nullifiers)?; self.mark_spent_coins(&nullifiers, tx_hash).await?; // This is the SQL query we'll be executing to insert new coins @@ -842,7 +862,7 @@ impl Drk { serialize_async(&owncoin.note.memo).await, ]; - if let Err(e) = self.wallet.exec_sql(&query, params).await { + if let Err(e) = self.wallet.exec_sql(&query, params) { return Err(Error::RusqliteError(format!( "[apply_tx_money_data] Inserting Money coin failed: {e:?}" ))) @@ -855,10 +875,8 @@ impl Drk { *MONEY_TOKENS_TABLE, MONEY_TOKENS_COL_IS_FROZEN, MONEY_TOKENS_COL_TOKEN_ID, ); - if let Err(e) = self - .wallet - .exec_sql(&query, rusqlite::params![serialize_async(&token_id).await]) - .await + if let Err(e) = + self.wallet.exec_sql(&query, rusqlite::params![serialize_async(&token_id).await]) { return Err(Error::RusqliteError(format!( "[apply_tx_money_data] Inserting Money coin failed: {e:?}" @@ -920,7 +938,7 @@ impl Drk { Ok(()) } - /// Mark a coin in the wallet as spent + /// Mark a coin in the wallet as spent. pub async fn mark_spent_coin(&self, coin: &Coin, spent_tx_hash: &String) -> WalletDbResult<()> { let query = format!( "UPDATE {} SET {} = ?1, {} = ?2 WHERE {} = ?3;", @@ -930,15 +948,13 @@ impl Drk { MONEY_COINS_COL_COIN ); let is_spent = 1; - self.wallet - .exec_sql( - &query, - rusqlite::params![is_spent, spent_tx_hash, serialize_async(&coin.inner()).await], - ) - .await + self.wallet.exec_sql( + &query, + rusqlite::params![is_spent, spent_tx_hash, serialize_async(&coin.inner()).await], + ) } - /// Marks all coins in the wallet as spent, if their nullifier is in the given set + /// Marks all coins in the wallet as spent, if their nullifier is in the given set. pub async fn mark_spent_coins( &self, nullifiers: &[Nullifier], @@ -961,7 +977,23 @@ impl Drk { Ok(()) } - /// Reset the Money Merkle tree in the wallet + /// Inserts given slice to the wallets nullifiers Sparse Merkle Tree. + pub fn smt_insert(&self, nullifiers: &[Nullifier]) -> Result<()> { + let store = WalletStorage::new( + &self.wallet, + &MONEY_SMT_TABLE, + MONEY_SMT_COL_KEY, + MONEY_SMT_COL_VALUE, + ); + let mut smt = WalletSmt::new(store, PoseidonFp::new(), &EMPTY_NODES_FP); + + let leaves: Vec<_> = nullifiers.iter().map(|x| (x.inner(), x.inner())).collect(); + smt.insert_batch(leaves)?; + + Ok(()) + } + + /// Reset the Money Merkle tree in the wallet. pub async fn reset_money_tree(&self) -> WalletDbResult<()> { println!("Resetting Money Merkle tree"); let mut tree = MerkleTree::new(1); @@ -973,11 +1005,21 @@ impl Drk { Ok(()) } - /// Reset the Money coins in the wallet - pub async fn reset_money_coins(&self) -> WalletDbResult<()> { + /// Reset the Money nullifiers Sparse Merkle Tree in the wallet. + pub fn reset_money_smt(&self) -> WalletDbResult<()> { + println!("Resetting Money Sparse Merkle tree"); + let query = format!("DELETE FROM {};", *MONEY_SMT_TABLE); + self.wallet.exec_sql(&query, &[])?; + println!("Successfully reset Money Sparse Merkle tree"); + + Ok(()) + } + + /// Reset the Money coins in the wallet. + pub fn reset_money_coins(&self) -> WalletDbResult<()> { println!("Resetting coins"); let query = format!("DELETE FROM {};", *MONEY_COINS_TABLE); - self.wallet.exec_sql(&query, &[]).await?; + self.wallet.exec_sql(&query, &[])?; println!("Successfully reset coins"); Ok(()) diff --git a/bin/drk/src/rpc.rs b/bin/drk/src/rpc.rs index e8a4b58e5..007c950bd 100644 --- a/bin/drk/src/rpc.rs +++ b/bin/drk/src/rpc.rs @@ -58,7 +58,7 @@ impl Drk { let req = JsonRequest::new("blockchain.last_known_block", JsonValue::Array(vec![])); let rep = self.rpc_client.as_ref().unwrap().request(req).await?; let last_known = *rep.get::().unwrap() as u32; - let last_scanned = match self.last_scanned_block().await { + let last_scanned = match self.last_scanned_block() { Ok(l) => l, Err(e) => { return Err(Error::RusqliteError(format!( @@ -149,7 +149,7 @@ impl Drk { }, }; if let Err(e) = - self.update_tx_history_records_status(&txs_hashes, "Finalized").await + self.update_tx_history_records_status(&txs_hashes, "Finalized") { return Err(Error::RusqliteError(format!( "[subscribe_blocks] Update transaction history record status failed: {e:?}" @@ -216,7 +216,7 @@ impl Drk { // Write this block height into `last_scanned_block` let query = format!("UPDATE {} SET {} = ?1;", *MONEY_INFO_TABLE, MONEY_INFO_COL_LAST_SCANNED_BLOCK); - if let Err(e) = self.wallet.exec_sql(&query, rusqlite::params![block.header.height]).await { + if let Err(e) = self.wallet.exec_sql(&query, rusqlite::params![block.header.height]) { return Err(Error::RusqliteError(format!( "[scan_block] Update last scanned block failed: {e:?}" ))) @@ -231,18 +231,19 @@ impl Drk { /// it looks for a checkpoint in the wallet to reset and start scanning from. pub async fn scan_blocks(&self, reset: bool) -> WalletDbResult<()> { // Grab last scanned block height - let mut height = self.last_scanned_block().await?; + let mut height = self.last_scanned_block()?; // If last scanned block is genesis (0) or reset flag // has been provided we reset, otherwise continue with // the next block height if height == 0 || reset { self.reset_money_tree().await?; - self.reset_money_coins().await?; + self.reset_money_smt()?; + self.reset_money_coins()?; self.reset_dao_trees().await?; self.reset_daos().await?; - self.reset_dao_proposals().await?; - self.reset_dao_votes().await?; - self.update_all_tx_history_records_status("Rejected").await?; + self.reset_dao_proposals()?; + self.reset_dao_votes()?; + self.update_all_tx_history_records_status("Rejected")?; height = 0; } else { height += 1; @@ -281,7 +282,7 @@ impl Drk { return Err(WalletDbError::GenericError) }; let txs_hashes = self.insert_tx_history_records(&block.txs).await?; - self.update_tx_history_records_status(&txs_hashes, "Finalized").await?; + self.update_tx_history_records_status(&txs_hashes, "Finalized")?; height += 1; } } @@ -384,4 +385,15 @@ impl Drk { Ok(gas) } + + /// Queries darkfid for current best fork next height. + pub async fn get_next_block_height(&self) -> Result { + let req = + JsonRequest::new("blockchain.best_fork_next_block_height", JsonValue::Array(vec![])); + let rep = self.rpc_client.as_ref().unwrap().request(req).await?; + + let next_height = *rep.get::().unwrap() as u32; + + Ok(next_height) + } } diff --git a/bin/drk/src/token.rs b/bin/drk/src/token.rs index fa41ed794..b2b5bee86 100644 --- a/bin/drk/src/token.rs +++ b/bin/drk/src/token.rs @@ -98,19 +98,15 @@ impl Drk { MONEY_TOKENS_COL_IS_FROZEN, ); - if let Err(e) = self - .wallet - .exec_sql( - &query, - rusqlite::params![ - serialize_async(&token_id).await, - serialize_async(&mint_authority).await, - serialize_async(&token_blind).await, - is_frozen, - ], - ) - .await - { + if let Err(e) = self.wallet.exec_sql( + &query, + rusqlite::params![ + serialize_async(&token_id).await, + serialize_async(&mint_authority).await, + serialize_async(&token_blind).await, + is_frozen, + ], + ) { return Err(Error::RusqliteError(format!( "[import_mint_authority] Inserting mint authority failed: {e:?}" ))) @@ -158,7 +154,7 @@ impl Drk { /// Fetch all token mint authorities from the wallet. pub async fn get_mint_authorities(&self) -> Result> { - let rows = match self.wallet.query_multiple(&MONEY_TOKENS_TABLE, &[], &[]).await { + let rows = match self.wallet.query_multiple(&MONEY_TOKENS_TABLE, &[], &[]) { Ok(r) => r, Err(e) => { return Err(Error::RusqliteError(format!( @@ -180,15 +176,18 @@ impl Drk { &self, token_id: &TokenId, ) -> Result<(TokenId, SecretKey, BaseBlind, bool)> { - let row = - match self.wallet.query_single(&MONEY_TOKENS_TABLE, &[], convert_named_params! {(MONEY_TOKENS_COL_TOKEN_ID, serialize_async(token_id).await)}).await { - Ok(r) => r, - Err(e) => { - return Err(Error::RusqliteError(format!( - "[get_token_mint_authority] Token mint autority retrieval failed: {e:?}" - ))) - } - }; + let row = match self.wallet.query_single( + &MONEY_TOKENS_TABLE, + &[], + convert_named_params! {(MONEY_TOKENS_COL_TOKEN_ID, serialize_async(token_id).await)}, + ) { + Ok(r) => r, + Err(e) => { + return Err(Error::RusqliteError(format!( + "[get_token_mint_authority] Token mint autority retrieval failed: {e:?}" + ))) + } + }; let token = self.parse_mint_authority_record(&row).await?; diff --git a/bin/drk/src/txs_history.rs b/bin/drk/src/txs_history.rs index 9bb032150..ea2156656 100644 --- a/bin/drk/src/txs_history.rs +++ b/bin/drk/src/txs_history.rs @@ -45,12 +45,10 @@ impl Drk { WALLET_TXS_HISTORY_COL_TX, ); let tx_hash = tx.hash().to_string(); - self.wallet - .exec_sql( - &query, - rusqlite::params![tx_hash, "Broadcasted", &serialize_async(tx).await,], - ) - .await?; + self.wallet.exec_sql( + &query, + rusqlite::params![tx_hash, "Broadcasted", &serialize_async(tx).await,], + )?; Ok(tx_hash) } @@ -72,15 +70,11 @@ impl Drk { &self, tx_hash: &str, ) -> Result<(String, String, Transaction)> { - let row = match self - .wallet - .query_single( - WALLET_TXS_HISTORY_TABLE, - &[], - convert_named_params! {(WALLET_TXS_HISTORY_COL_TX_HASH, tx_hash)}, - ) - .await - { + let row = match self.wallet.query_single( + WALLET_TXS_HISTORY_TABLE, + &[], + convert_named_params! {(WALLET_TXS_HISTORY_COL_TX_HASH, tx_hash)}, + ) { Ok(r) => r, Err(e) => { return Err(Error::RusqliteError(format!( @@ -110,15 +104,12 @@ impl Drk { } /// Fetch all transactions history records, excluding bytes column. - pub async fn get_txs_history(&self) -> WalletDbResult> { - let rows = self - .wallet - .query_multiple( - WALLET_TXS_HISTORY_TABLE, - &[WALLET_TXS_HISTORY_COL_TX_HASH, WALLET_TXS_HISTORY_COL_STATUS], - &[], - ) - .await?; + pub fn get_txs_history(&self) -> WalletDbResult> { + let rows = self.wallet.query_multiple( + WALLET_TXS_HISTORY_TABLE, + &[WALLET_TXS_HISTORY_COL_TX_HASH, WALLET_TXS_HISTORY_COL_STATUS], + &[], + )?; let mut ret = Vec::with_capacity(rows.len()); for row in rows { @@ -137,7 +128,7 @@ impl Drk { } /// Update given transactions history record statuses to the given one. - pub async fn update_tx_history_records_status( + pub fn update_tx_history_records_status( &self, txs_hashes: &[String], status: &str, @@ -155,15 +146,15 @@ impl Drk { txs_hashes_string ); - self.wallet.exec_sql(&query, rusqlite::params![status]).await + self.wallet.exec_sql(&query, rusqlite::params![status]) } /// Update all transaction history records statuses to the given one. - pub async fn update_all_tx_history_records_status(&self, status: &str) -> WalletDbResult<()> { + pub fn update_all_tx_history_records_status(&self, status: &str) -> WalletDbResult<()> { let query = format!( "UPDATE {} SET {} = ?1", WALLET_TXS_HISTORY_TABLE, WALLET_TXS_HISTORY_COL_STATUS, ); - self.wallet.exec_sql(&query, rusqlite::params![status]).await + self.wallet.exec_sql(&query, rusqlite::params![status]) } } diff --git a/bin/drk/src/walletdb.rs b/bin/drk/src/walletdb.rs index 124605b8c..f0f93dce2 100644 --- a/bin/drk/src/walletdb.rs +++ b/bin/drk/src/walletdb.rs @@ -16,14 +16,25 @@ * along with this program. If not, see . */ -use std::{path::PathBuf, sync::Arc}; +use std::{ + path::PathBuf, + sync::{Arc, Mutex}, +}; +use darkfi_sdk::{ + crypto::{ + pasta_prelude::PrimeField, + smt::{PoseidonFp, SparseMerkleTree, StorageAdapter, SMT_FP_DEPTH}, + }, + error::{ContractError, ContractResult}, + pasta::pallas, +}; use log::{debug, error}; +use num_bigint::BigUint; use rusqlite::{ types::{ToSql, Value}, Connection, }; -use smol::lock::Mutex; use crate::error::{WalletDbError, WalletDbResult}; @@ -62,9 +73,10 @@ impl WalletDb { /// This function executes a given SQL query that contains multiple SQL statements, /// that don't contain any parameters. - pub async fn exec_batch_sql(&self, query: &str) -> WalletDbResult<()> { + pub fn exec_batch_sql(&self, query: &str) -> WalletDbResult<()> { debug!(target: "walletdb::exec_batch_sql", "[WalletDb] Executing batch SQL query:\n{query}"); - if let Err(e) = self.conn.lock().await.execute_batch(query) { + let Ok(conn) = self.conn.lock() else { return Err(WalletDbError::FailedToAquireLock) }; + if let Err(e) = conn.execute_batch(query) { error!(target: "walletdb::exec_batch_sql", "[WalletDb] Query failed: {e}"); return Err(WalletDbError::QueryExecutionFailed) }; @@ -74,11 +86,13 @@ impl WalletDb { /// This function executes a given SQL query, but isn't able to return anything. /// Therefore it's best to use it for initializing a table or similar things. - pub async fn exec_sql(&self, query: &str, params: &[&dyn ToSql]) -> WalletDbResult<()> { + pub fn exec_sql(&self, query: &str, params: &[&dyn ToSql]) -> WalletDbResult<()> { debug!(target: "walletdb::exec_sql", "[WalletDb] Executing SQL query:\n{query}"); + let Ok(conn) = self.conn.lock() else { return Err(WalletDbError::FailedToAquireLock) }; + // If no params are provided, execute directly if params.is_empty() { - if let Err(e) = self.conn.lock().await.execute(query, ()) { + if let Err(e) = conn.execute(query, ()) { error!(target: "walletdb::exec_sql", "[WalletDb] Query failed: {e}"); return Err(WalletDbError::QueryExecutionFailed) }; @@ -86,7 +100,6 @@ impl WalletDb { } // First we prepare the query - let conn = self.conn.lock().await; let Ok(mut stmt) = conn.prepare(query) else { return Err(WalletDbError::QueryPreparationFailed) }; @@ -137,7 +150,7 @@ impl WalletDb { /// Query provided table from selected column names and provided `WHERE` clauses, /// for a single row. - pub async fn query_single( + pub fn query_single( &self, table: &str, col_names: &[&str], @@ -148,7 +161,8 @@ impl WalletDb { debug!(target: "walletdb::query_single", "[WalletDb] Executing SQL query:\n{query}"); // First we prepare the query - let conn = self.conn.lock().await; + let Ok(conn) = self.conn.lock() else { return Err(WalletDbError::FailedToAquireLock) }; + let Ok(mut stmt) = conn.prepare(&query) else { return Err(WalletDbError::QueryPreparationFailed) }; @@ -188,7 +202,7 @@ impl WalletDb { /// Query provided table from selected column names and provided `WHERE` clauses, /// for multiple rows. - pub async fn query_multiple( + pub fn query_multiple( &self, table: &str, col_names: &[&str], @@ -199,16 +213,13 @@ impl WalletDb { debug!(target: "walletdb::multiple", "[WalletDb] Executing SQL query:\n{query}"); // First we prepare the query - let conn = self.conn.lock().await; + let Ok(conn) = self.conn.lock() else { return Err(WalletDbError::FailedToAquireLock) }; let Ok(mut stmt) = conn.prepare(&query) else { return Err(WalletDbError::QueryPreparationFailed) }; // Execute the query using provided converted params let Ok(mut rows) = stmt.query(params) else { - if let Err(e) = stmt.query(params) { - println!("eeer: {e:?}"); - } return Err(WalletDbError::QueryExecutionFailed) }; @@ -263,127 +274,251 @@ macro_rules! convert_named_params { }; } +/// Wallet SMT definition +pub type WalletSmt<'a> = SparseMerkleTree< + 'static, + SMT_FP_DEPTH, + { SMT_FP_DEPTH + 1 }, + pallas::Base, + PoseidonFp, + WalletStorage<'a>, +>; + +/// An SMT adapter for wallet SQLite database storage. +pub struct WalletStorage<'a> { + wallet: &'a WalletPtr, + table: &'a str, + key_col: &'a str, + value_col: &'a str, +} + +impl<'a> WalletStorage<'a> { + pub fn new( + wallet: &'a WalletPtr, + table: &'a str, + key_col: &'a str, + value_col: &'a str, + ) -> Self { + Self { wallet, table, key_col, value_col } + } +} + +impl<'a> StorageAdapter for WalletStorage<'a> { + type Value = pallas::Base; + + fn put(&mut self, key: BigUint, value: pallas::Base) -> ContractResult { + let query = format!( + "INSERT OR REPLACE INTO {} ({}, {}) VALUES (?1, ?2);", + self.table, self.key_col, self.value_col + ); + if let Err(e) = + self.wallet.exec_sql(&query, rusqlite::params![key.to_bytes_le(), value.to_repr()]) + { + error!(target: "walletdb::StorageAdapter::put", "Inserting key {key:?}, value {value:?} into DB failed: {e:?}"); + return Err(ContractError::SmtPutFailed) + } + + Ok(()) + } + + fn get(&self, key: &BigUint) -> Option { + let row = match self.wallet.query_single( + self.table, + &[self.value_col], + convert_named_params! {(self.key_col, key.to_bytes_le())}, + ) { + Ok(r) => r, + Err(WalletDbError::RowNotFound) => return None, + Err(e) => { + error!(target: "walletdb::StorageAdapter::get", "Fetching key {key:?} from DB failed: {e:?}"); + return None + } + }; + + let Value::Blob(ref value_bytes) = row[0] else { + error!(target: "walletdb::StorageAdapter::get", "Parsing key {key:?} value bytes"); + return None + }; + + let mut repr = [0; 32]; + repr.copy_from_slice(value_bytes); + + pallas::Base::from_repr(repr).into() + } + + fn del(&mut self, key: &BigUint) -> ContractResult { + let query = format!("DELETE FROM {} WHERE {} = ?1;", self.table, self.key_col); + if let Err(e) = self.wallet.exec_sql(&query, rusqlite::params![key.to_bytes_le()]) { + error!(target: "walletdb::StorageAdapter::del", "Removing key {key:?} from DB failed: {e:?}"); + return Err(ContractError::SmtDelFailed) + } + + Ok(()) + } +} + #[cfg(test)] mod tests { + use darkfi::zk::halo2::Field; + use darkfi_sdk::{ + crypto::smt::{gen_empty_nodes, util::FieldHasher, PoseidonFp, SparseMerkleTree}, + pasta::pallas, + }; + use rand::rngs::OsRng; use rusqlite::types::Value; - use crate::walletdb::WalletDb; + use crate::walletdb::{WalletDb, WalletStorage}; #[test] fn test_mem_wallet() { - smol::block_on(async { - let wallet = WalletDb::new(None, Some("foobar")).unwrap(); - wallet.exec_sql("CREATE TABLE mista ( numba INTEGER );", &[]).await.unwrap(); - wallet.exec_sql("INSERT INTO mista ( numba ) VALUES ( 42 );", &[]).await.unwrap(); + let wallet = WalletDb::new(None, Some("foobar")).unwrap(); + wallet.exec_sql("CREATE TABLE mista ( numba INTEGER );", &[]).unwrap(); + wallet.exec_sql("INSERT INTO mista ( numba ) VALUES ( 42 );", &[]).unwrap(); - let ret = wallet.query_single("mista", &["numba"], &[]).await.unwrap(); - assert_eq!(ret.len(), 1); - let numba: i64 = if let Value::Integer(numba) = ret[0] { numba } else { -1 }; - assert_eq!(numba, 42); - }); + let ret = wallet.query_single("mista", &["numba"], &[]).unwrap(); + assert_eq!(ret.len(), 1); + let numba: i64 = if let Value::Integer(numba) = ret[0] { numba } else { -1 }; + assert_eq!(numba, 42); } #[test] fn test_query_single() { - smol::block_on(async { - let wallet = WalletDb::new(None, None).unwrap(); - wallet - .exec_sql( - "CREATE TABLE mista ( why INTEGER, are TEXT, you INTEGER, gae BLOB );", - &[], - ) - .await - .unwrap(); + let wallet = WalletDb::new(None, None).unwrap(); + wallet + .exec_sql("CREATE TABLE mista ( why INTEGER, are TEXT, you INTEGER, gae BLOB );", &[]) + .unwrap(); - let why = 42; - let are = "are".to_string(); - let you = 69; - let gae = vec![42u8; 32]; + let why = 42; + let are = "are".to_string(); + let you = 69; + let gae = vec![42u8; 32]; - wallet - .exec_sql( - "INSERT INTO mista ( why, are, you, gae ) VALUES (?1, ?2, ?3, ?4);", - rusqlite::params![why, are, you, gae], - ) - .await - .unwrap(); + wallet + .exec_sql( + "INSERT INTO mista ( why, are, you, gae ) VALUES (?1, ?2, ?3, ?4);", + rusqlite::params![why, are, you, gae], + ) + .unwrap(); - let ret = - wallet.query_single("mista", &["why", "are", "you", "gae"], &[]).await.unwrap(); - assert_eq!(ret.len(), 4); - assert_eq!(ret[0], Value::Integer(why)); - assert_eq!(ret[1], Value::Text(are.clone())); - assert_eq!(ret[2], Value::Integer(you)); - assert_eq!(ret[3], Value::Blob(gae.clone())); + let ret = wallet.query_single("mista", &["why", "are", "you", "gae"], &[]).unwrap(); + assert_eq!(ret.len(), 4); + assert_eq!(ret[0], Value::Integer(why)); + assert_eq!(ret[1], Value::Text(are.clone())); + assert_eq!(ret[2], Value::Integer(you)); + assert_eq!(ret[3], Value::Blob(gae.clone())); - let ret = wallet - .query_single( - "mista", - &["gae"], - rusqlite::named_params! {":why": why, ":are": are, ":you": you}, - ) - .await - .unwrap(); - assert_eq!(ret.len(), 1); - assert_eq!(ret[0], Value::Blob(gae)); - }); + let ret = wallet + .query_single( + "mista", + &["gae"], + rusqlite::named_params! {":why": why, ":are": are, ":you": you}, + ) + .unwrap(); + assert_eq!(ret.len(), 1); + assert_eq!(ret[0], Value::Blob(gae)); } #[test] fn test_query_multi() { - smol::block_on(async { - let wallet = WalletDb::new(None, None).unwrap(); - wallet - .exec_sql( - "CREATE TABLE mista ( why INTEGER, are TEXT, you INTEGER, gae BLOB );", - &[], - ) - .await - .unwrap(); + let wallet = WalletDb::new(None, None).unwrap(); + wallet + .exec_sql("CREATE TABLE mista ( why INTEGER, are TEXT, you INTEGER, gae BLOB );", &[]) + .unwrap(); - let why = 42; - let are = "are".to_string(); - let you = 69; - let gae = vec![42u8; 32]; + let why = 42; + let are = "are".to_string(); + let you = 69; + let gae = vec![42u8; 32]; - wallet - .exec_sql( - "INSERT INTO mista ( why, are, you, gae ) VALUES (?1, ?2, ?3, ?4);", - rusqlite::params![why, are, you, gae], - ) - .await - .unwrap(); - wallet - .exec_sql( - "INSERT INTO mista ( why, are, you, gae ) VALUES (?1, ?2, ?3, ?4);", - rusqlite::params![why, are, you, gae], - ) - .await - .unwrap(); + wallet + .exec_sql( + "INSERT INTO mista ( why, are, you, gae ) VALUES (?1, ?2, ?3, ?4);", + rusqlite::params![why, are, you, gae], + ) + .unwrap(); + wallet + .exec_sql( + "INSERT INTO mista ( why, are, you, gae ) VALUES (?1, ?2, ?3, ?4);", + rusqlite::params![why, are, you, gae], + ) + .unwrap(); - let ret = wallet.query_multiple("mista", &[], &[]).await.unwrap(); - assert_eq!(ret.len(), 2); - for row in ret { - assert_eq!(row.len(), 4); - assert_eq!(row[0], Value::Integer(why)); - assert_eq!(row[1], Value::Text(are.clone())); - assert_eq!(row[2], Value::Integer(you)); - assert_eq!(row[3], Value::Blob(gae.clone())); - } + let ret = wallet.query_multiple("mista", &[], &[]).unwrap(); + assert_eq!(ret.len(), 2); + for row in ret { + assert_eq!(row.len(), 4); + assert_eq!(row[0], Value::Integer(why)); + assert_eq!(row[1], Value::Text(are.clone())); + assert_eq!(row[2], Value::Integer(you)); + assert_eq!(row[3], Value::Blob(gae.clone())); + } - let ret = wallet - .query_multiple( - "mista", - &["gae"], - convert_named_params! {("why", why), ("are", are), ("you", you)}, - ) - .await - .unwrap(); - assert_eq!(ret.len(), 2); - for row in ret { - assert_eq!(row.len(), 1); - assert_eq!(row[0], Value::Blob(gae.clone())); - } - }); + let ret = wallet + .query_multiple( + "mista", + &["gae"], + convert_named_params! {("why", why), ("are", are), ("you", you)}, + ) + .unwrap(); + assert_eq!(ret.len(), 2); + for row in ret { + assert_eq!(row.len(), 1); + assert_eq!(row[0], Value::Blob(gae.clone())); + } + } + + #[test] + fn test_sqlite_smt() { + // Setup SQLite database + let table = &"smt"; + let key_col = &"smt_key"; + let value_col = &"smt_value"; + let wallet = WalletDb::new(None, None).unwrap(); + wallet.exec_sql(&format!("CREATE TABLE {table} ( {key_col} BLOB INTEGER PRIMARY KEY NOT NULL, {value_col} BLOB NOT NULL);"), &[]).unwrap(); + + // Setup SMT + const HEIGHT: usize = 3; + let hasher = PoseidonFp::new(); + let empty_leaf = pallas::Base::ZERO; + let empty_nodes = gen_empty_nodes::<{ HEIGHT + 1 }, _, _>(&hasher, empty_leaf); + let store = WalletStorage::new(&wallet, table, key_col, value_col); + let mut smt = SparseMerkleTree::::new( + store, + hasher.clone(), + &empty_nodes, + ); + + let leaves = vec![ + (pallas::Base::from(1), pallas::Base::random(&mut OsRng)), + (pallas::Base::from(2), pallas::Base::random(&mut OsRng)), + (pallas::Base::from(3), pallas::Base::random(&mut OsRng)), + ]; + smt.insert_batch(leaves.clone()).unwrap(); + + let hash1 = leaves[0].1; + let hash2 = leaves[1].1; + let hash3 = leaves[2].1; + + let hash = |l, r| hasher.hash([l, r]); + + let hash01 = hash(empty_nodes[3], hash1); + let hash23 = hash(hash2, hash3); + + let hash0123 = hash(hash01, hash23); + let root = hash(hash0123, empty_nodes[1]); + assert_eq!(root, smt.root()); + + // Now try to construct a membership proof for leaf 3 + let pos = leaves[2].0; + let path = smt.prove_membership(&pos); + assert_eq!(path.path[0], empty_nodes[1]); + assert_eq!(path.path[1], hash01); + assert_eq!(path.path[2], hash2); + + assert_eq!(hash23, hash(path.path[2], hash3)); + assert_eq!(hash0123, hash(path.path[1], hash(path.path[2], hash3))); + assert_eq!(root, hash(hash(path.path[1], hash(path.path[2], hash3)), path.path[0])); + + assert!(path.verify(&root, &hash3, &pos)); } } diff --git a/src/contract/dao/src/client/propose.rs b/src/contract/dao/src/client/propose.rs index 03bd2f800..dfd9a1d74 100644 --- a/src/contract/dao/src/client/propose.rs +++ b/src/contract/dao/src/client/propose.rs @@ -21,9 +21,12 @@ use darkfi_sdk::{ bridgetree, bridgetree::Hashable, crypto::{ - note::AeadEncryptedNote, pasta_prelude::*, pedersen::pedersen_commitment_u64, - poseidon_hash, smt::SmtMemoryFp, Blind, FuncId, MerkleNode, PublicKey, ScalarBlind, - SecretKey, + note::AeadEncryptedNote, + pasta_prelude::*, + pedersen::pedersen_commitment_u64, + poseidon_hash, + smt::{PoseidonFp, SparseMerkleTree, StorageAdapter, SMT_FP_DEPTH}, + Blind, FuncId, MerkleNode, PublicKey, ScalarBlind, SecretKey, }, pasta::pallas, }; @@ -40,25 +43,26 @@ use crate::{ model::{Dao, DaoProposal, DaoProposeParams, DaoProposeParamsInput, VecAuthCallCommit}, }; -pub struct DaoProposeStakeInput<'a> { +pub struct DaoProposeStakeInput { pub secret: SecretKey, pub note: darkfi_money_contract::client::MoneyNote, pub leaf_position: bridgetree::Position, pub merkle_path: Vec, - pub money_null_smt: &'a SmtMemoryFp, - pub signature_secret: SecretKey, } -pub struct DaoProposeCall<'a> { - pub inputs: Vec>, +pub struct DaoProposeCall<'a, T: StorageAdapter> { + pub money_null_smt: + &'a SparseMerkleTree<'a, SMT_FP_DEPTH, { SMT_FP_DEPTH + 1 }, pallas::Base, PoseidonFp, T>, + pub inputs: Vec, pub proposal: DaoProposal, pub dao: Dao, pub dao_leaf_position: bridgetree::Position, pub dao_merkle_path: Vec, pub dao_merkle_root: MerkleNode, + pub signature_secret: SecretKey, } -impl<'a> DaoProposeCall<'a> { +impl<'a, T: StorageAdapter> DaoProposeCall<'a, T> { pub fn make( self, burn_zkbin: &ZkBinary, @@ -70,6 +74,10 @@ impl<'a> DaoProposeCall<'a> { let gov_token_blind = Blind::random(&mut OsRng); + let smt_null_root = self.money_null_smt.root(); + let signature_public = PublicKey::from_secret(self.signature_secret); + let (sig_x, sig_y) = signature_public.xy(); + let mut inputs = vec![]; let mut total_funds = 0; let mut total_funds_blinds = ScalarBlind::ZERO; @@ -79,8 +87,6 @@ impl<'a> DaoProposeCall<'a> { total_funds += input.note.value; total_funds_blinds += funds_blind; - let signature_public = PublicKey::from_secret(input.signature_secret); - // Note from the previous output let note = input.note; let leaf_pos: u64 = input.leaf_position.into(); @@ -97,8 +103,7 @@ impl<'a> DaoProposeCall<'a> { .to_coin(); let nullifier = poseidon_hash([input.secret.inner(), coin.inner()]); - let smt_null_root = input.money_null_smt.root(); - let smt_null_path = input.money_null_smt.prove_membership(&nullifier); + let smt_null_path = self.money_null_smt.prove_membership(&nullifier); if !smt_null_path.verify(&smt_null_root, &pallas::Base::ZERO, &nullifier) { return Err( ClientFailed::VerifyError(DaoError::InvalidInputMerkleRoot.to_string()).into() @@ -117,7 +122,7 @@ impl<'a> DaoProposeCall<'a> { Witness::Uint32(Value::known(leaf_pos.try_into().unwrap())), Witness::MerklePath(Value::known(input.merkle_path.clone().try_into().unwrap())), Witness::SparseMerklePath(Value::known(smt_null_path.path)), - Witness::Base(Value::known(input.signature_secret.inner())), + Witness::Base(Value::known(self.signature_secret.inner())), ]; // TODO: We need a generic ZkSet widget to avoid doing this all the time @@ -144,8 +149,6 @@ impl<'a> DaoProposeCall<'a> { let value_commit = pedersen_commitment_u64(note.value, funds_blind); let value_coords = value_commit.to_affine().coordinates().unwrap(); - let (sig_x, sig_y) = signature_public.xy(); - let public_inputs = vec![ smt_null_root, *value_coords.x(), diff --git a/src/contract/test-harness/src/dao_propose.rs b/src/contract/test-harness/src/dao_propose.rs index 75f4c699d..b1cb837e0 100644 --- a/src/contract/test-harness/src/dao_propose.rs +++ b/src/contract/test-harness/src/dao_propose.rs @@ -71,8 +71,6 @@ impl TestHarness { .unwrap() .clone(); - let signature_secret = SecretKey::random(&mut OsRng); - // Useful code snippet to dump a sled contract DB /*{ let blockchain = &wallet.validator.blockchain; @@ -95,8 +93,6 @@ impl TestHarness { .money_merkle_tree .witness(propose_owncoin.leaf_position, 0) .unwrap(), - money_null_smt: &wallet.money_null_smt, - signature_secret, }; // Convert coin_params to actual coins @@ -131,7 +127,10 @@ impl TestHarness { blind: Blind::random(&mut OsRng), }; + let signature_secret = SecretKey::random(&mut OsRng); + let call = DaoProposeCall { + money_null_smt: &wallet.money_null_smt, inputs: vec![input], proposal: proposal.clone(), dao: dao.clone(), @@ -141,6 +140,7 @@ impl TestHarness { .witness(*wallet.dao_leafs.get(dao_bulla).unwrap(), 0) .unwrap(), dao_merkle_root: wallet.dao_merkle_tree.root(0).unwrap(), + signature_secret, }; let (params, proofs) = call.make( diff --git a/src/contract/test-harness/src/lib.rs b/src/contract/test-harness/src/lib.rs index e3b200523..cfe75af63 100644 --- a/src/contract/test-harness/src/lib.rs +++ b/src/contract/test-harness/src/lib.rs @@ -179,7 +179,7 @@ impl Wallet { let hasher = PoseidonFp::new(); let store = MemoryStorageFp::new(); - let money_null_smt = SmtMemoryFp::new(store, hasher.clone(), &EMPTY_NODES_FP); + let money_null_smt = SmtMemoryFp::new(store, hasher, &EMPTY_NODES_FP); Ok(Self { keypair, diff --git a/src/sdk/src/crypto/smt/mod.rs b/src/sdk/src/crypto/smt/mod.rs index 9d415af9f..48924b562 100644 --- a/src/sdk/src/crypto/smt/mod.rs +++ b/src/sdk/src/crypto/smt/mod.rs @@ -72,7 +72,7 @@ pub use empty::EMPTY_NODES_FP; #[cfg(test)] mod test; -mod util; +pub mod util; pub use util::Poseidon; pub mod wasmdb; diff --git a/src/validator/mod.rs b/src/validator/mod.rs index 1b409c8bc..f0bd85615 100644 --- a/src/validator/mod.rs +++ b/src/validator/mod.rs @@ -721,4 +721,14 @@ impl Validator { Ok(()) } + + /// Auxiliary function to retrieve current best fork next block height. + pub async fn best_fork_next_block_height(&self) -> Result { + let forks = self.consensus.forks.read().await; + let fork = &forks[best_fork_index(&forks)?]; + let next_block_height = fork.get_next_block_height()?; + drop(forks); + + Ok(next_block_height) + } }