diff --git a/src/contract/dao/proof/propose-input.zk b/src/contract/dao/proof/propose-input.zk index 93eefc002..76eef64ca 100644 --- a/src/contract/dao/proof/propose-input.zk +++ b/src/contract/dao/proof/propose-input.zk @@ -1,4 +1,4 @@ -k = 13; +k = 14; field = "pallas"; constant "ProposeInput" { @@ -19,7 +19,10 @@ witness "ProposeInput" { Base coin_token_blind, Uint32 leaf_pos, - MerklePath path, + MerklePath coin_path, + + Base null_tree_root, + SparseMerklePath null_path, Base signature_secret, } @@ -40,10 +43,18 @@ circuit "ProposeInput" { ); # We need this to detect whether the above coin was already spent. - # To avoid leaking timing & other info, we can just make a - # money::transfer() call within the same tx. + # Use a SMT, and show that at this position, the leaf is ZERO + ZERO = witness_base(0); + ONE = witness_base(1); nullifier = poseidon_hash(coin_secret, coin); - constrain_instance(nullifier); + is_member = sparse_tree_is_member( + null_tree_root, # Expected root + null_path, # Path to root + ZERO, # Leaf value + nullifier # Position + ); + constrain_equal_base(is_member, ONE); + constrain_instance(null_tree_root); # Pedersen commitment for coin's coin_value vcv = ec_mul_short(coin_value, VALUE_COMMIT_VALUE); @@ -57,8 +68,8 @@ circuit "ProposeInput" { constrain_instance(coin_token_commit); # Merkle root - root = merkle_root(leaf_pos, path, coin); - constrain_instance(root); + merkle_coin_root = merkle_root(leaf_pos, coin_path, coin); + constrain_instance(merkle_coin_root); # Finally we derive a public key for the signature and constrain # its coordinates: diff --git a/src/contract/dao/src/client/propose.rs b/src/contract/dao/src/client/propose.rs index 1845f88ba..74ab75824 100644 --- a/src/contract/dao/src/client/propose.rs +++ b/src/contract/dao/src/client/propose.rs @@ -16,13 +16,14 @@ * along with this program. If not, see . */ -use darkfi_money_contract::model::{CoinAttributes, Nullifier}; +use darkfi_money_contract::model::CoinAttributes; use darkfi_sdk::{ bridgetree, bridgetree::Hashable, crypto::{ note::AeadEncryptedNote, pasta_prelude::*, pedersen::pedersen_commitment_u64, - poseidon_hash, Blind, FuncId, MerkleNode, PublicKey, ScalarBlind, SecretKey, + poseidon_hash, smt::SmtMemoryFp, Blind, FuncId, MerkleNode, PublicKey, ScalarBlind, + SecretKey, }, pasta::pallas, }; @@ -36,16 +37,17 @@ use darkfi::{ use crate::model::{Dao, DaoProposal, DaoProposeParams, DaoProposeParamsInput, VecAuthCallCommit}; -pub struct DaoProposeStakeInput { +pub struct DaoProposeStakeInput<'a> { 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 { - pub inputs: Vec, +pub struct DaoProposeCall<'a> { + pub inputs: Vec>, pub proposal: DaoProposal, pub dao: Dao, pub dao_leaf_position: bridgetree::Position, @@ -53,7 +55,7 @@ pub struct DaoProposeCall { pub dao_merkle_root: MerkleNode, } -impl DaoProposeCall { +impl<'a> DaoProposeCall<'a> { pub fn make( self, burn_zkbin: &ZkBinary, @@ -80,6 +82,22 @@ impl DaoProposeCall { let note = input.note; let leaf_pos: u64 = input.leaf_position.into(); + let public_key = PublicKey::from_secret(input.secret); + let coin = CoinAttributes { + public_key, + value: note.value, + token_id: note.token_id, + spend_hook: FuncId::none(), + user_data: pallas::Base::ZERO, + blind: note.coin_blind, + } + .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); + assert!(smt_null_path.verify(&smt_null_root, &pallas::Base::ZERO, &nullifier)); + let prover_witnesses = vec![ Witness::Base(Value::known(input.secret.inner())), Witness::Base(Value::known(pallas::Base::from(note.value))), @@ -91,23 +109,14 @@ impl DaoProposeCall { Witness::Base(Value::known(gov_token_blind.inner())), Witness::Uint32(Value::known(leaf_pos.try_into().unwrap())), Witness::MerklePath(Value::known(input.merkle_path.clone().try_into().unwrap())), + Witness::Base(Value::known(smt_null_root)), + Witness::SparseMerklePath(Value::known(smt_null_path.path)), Witness::Base(Value::known(input.signature_secret.inner())), ]; - let public_key = PublicKey::from_secret(input.secret); - let coin = CoinAttributes { - public_key, - value: note.value, - token_id: note.token_id, - spend_hook: FuncId::none(), - user_data: pallas::Base::ZERO, - blind: note.coin_blind, - } - .to_coin(); - // TODO: We need a generic ZkSet widget to avoid doing this all the time - let merkle_root = { + let merkle_coin_root = { let position: u64 = input.leaf_position.into(); let mut current = MerkleNode::from(coin.inner()); for (level, sibling) in input.merkle_path.iter().enumerate() { @@ -121,8 +130,6 @@ impl DaoProposeCall { current }; - let nullifier: Nullifier = poseidon_hash([input.secret.inner(), coin.inner()]).into(); - let token_commit = poseidon_hash([note.token_id.inner(), gov_token_blind.inner()]); assert_eq!(self.dao.gov_token_id, note.token_id); @@ -132,11 +139,11 @@ impl DaoProposeCall { let (sig_x, sig_y) = signature_public.xy(); let public_inputs = vec![ - nullifier.inner(), + smt_null_root, *value_coords.x(), *value_coords.y(), token_commit, - merkle_root.inner(), + merkle_coin_root.inner(), sig_x, sig_y, ]; @@ -147,8 +154,12 @@ impl DaoProposeCall { let input_proof = Proof::create(proving_key, &[circuit], &public_inputs, &mut OsRng)?; proofs.push(input_proof); - let input = - DaoProposeParamsInput { nullifier, value_commit, merkle_root, signature_public }; + let input = DaoProposeParamsInput { + value_commit, + merkle_coin_root, + smt_null_root, + signature_public, + }; inputs.push(input); } diff --git a/src/contract/dao/src/entrypoint/mint.rs b/src/contract/dao/src/entrypoint/mint.rs index 19eebe629..76a87c6ac 100644 --- a/src/contract/dao/src/entrypoint/mint.rs +++ b/src/contract/dao/src/entrypoint/mint.rs @@ -21,7 +21,8 @@ use darkfi_sdk::{ dark_tree::DarkLeaf, db::{db_contains_key, db_lookup, db_set}, error::{ContractError, ContractResult}, - merkle_add, msg, + merkle::merkle_add, + msg, pasta::pallas, ContractCall, }; diff --git a/src/contract/dao/src/entrypoint/propose.rs b/src/contract/dao/src/entrypoint/propose.rs index 701dfbc78..aa82cd36c 100644 --- a/src/contract/dao/src/entrypoint/propose.rs +++ b/src/contract/dao/src/entrypoint/propose.rs @@ -18,8 +18,7 @@ use darkfi_money_contract::{ MONEY_CONTRACT_COIN_ROOTS_TREE, MONEY_CONTRACT_INFO_TREE, MONEY_CONTRACT_LATEST_COIN_ROOT, - MONEY_CONTRACT_LATEST_NULLIFIER_ROOT, MONEY_CONTRACT_NULLIFIERS_TREE, - MONEY_CONTRACT_NULLIFIER_ROOTS_TREE, + MONEY_CONTRACT_LATEST_NULLIFIER_ROOT, MONEY_CONTRACT_NULLIFIER_ROOTS_TREE, }; use darkfi_sdk::{ crypto::{contract_id::MONEY_CONTRACT_ID, pasta_prelude::*, ContractId, MerkleNode, PublicKey}, @@ -39,6 +38,7 @@ use crate::{ model::{DaoBlindAggregateVote, DaoProposalMetadata, DaoProposeParams, DaoProposeUpdate}, DaoFunction, DAO_CONTRACT_DB_DAO_MERKLE_ROOTS, DAO_CONTRACT_DB_PROPOSAL_BULLAS, DAO_CONTRACT_ZKAS_DAO_PROPOSE_INPUT_NS, DAO_CONTRACT_ZKAS_DAO_PROPOSE_MAIN_NS, + PROPOSAL_SNAPSHOT_CUTOFF_LIMIT, }; /// `get_metdata` function for `Dao::Propose` @@ -74,11 +74,11 @@ pub(crate) fn dao_propose_get_metadata( zk_public_inputs.push(( DAO_CONTRACT_ZKAS_DAO_PROPOSE_INPUT_NS.to_string(), vec![ - input.nullifier.inner(), + input.smt_null_root, *value_coords.x(), *value_coords.y(), params.token_commit, - input.merkle_root.inner(), + input.merkle_coin_root.inner(), sig_x, sig_y, ], @@ -120,26 +120,42 @@ pub(crate) fn dao_propose_process_instruction( let params: DaoProposeParams = deserialize(&self_.data[1..])?; let coin_roots_db = db_lookup(*MONEY_CONTRACT_ID, MONEY_CONTRACT_COIN_ROOTS_TREE)?; - let money_nullifier_db = db_lookup(*MONEY_CONTRACT_ID, MONEY_CONTRACT_NULLIFIERS_TREE)?; - let mut propose_nullifiers = Vec::with_capacity(params.inputs.len()); + let null_roots_db = db_lookup(*MONEY_CONTRACT_ID, MONEY_CONTRACT_NULLIFIER_ROOTS_TREE)?; for input in ¶ms.inputs { // Check the Merkle roots for the input coins are valid - if !db_contains_key(coin_roots_db, &serialize(&input.merkle_root))? { - msg!("[Dao::Propose] Error: Invalid input Merkle root: {}", input.merkle_root); + let Some(coin_root_data) = db_get(coin_roots_db, &serialize(&input.merkle_coin_root))? + else { + msg!( + "[Dao::Propose] Error: Invalid input Merkle root: {:?}", + input.merkle_coin_root.inner() + ); return Err(DaoError::InvalidInputMerkleRoot.into()) + }; + + // Check the SMT roots for the input nullifiers are valid + let Some(null_root_data) = db_get(null_roots_db, &serialize(&input.smt_null_root))? else { + msg!("[Dao::Propose] Error: Invalid input SMT root: {:?}", input.smt_null_root); + return Err(DaoError::InvalidInputMerkleRoot.into()) + }; + + // Both roots must snapshot the exact same state + if coin_root_data != null_root_data { + msg!("[Dao::Propose] Error: coin roots snapshot for {:?} does not match nulls root snapshot {:?}", + input.merkle_coin_root.inner(), input.smt_null_root); + return Err(DaoError::NonMatchingSnapshotRoots.into()) } - // Check the coins weren't already spent - // The nullifiers should not already exist. It is the double-spend protection. - if propose_nullifiers.contains(&input.nullifier) || - db_contains_key(money_nullifier_db, &serialize(&input.nullifier))? - { - msg!("[Dao::Vote] Error: Coin is already spent"); - return Err(DaoError::CoinAlreadySpent.into()) + assert_eq!(coin_root_data.len(), 32 + 2); + let tx_hash = &coin_root_data[0..32]; + // Get block_height where tx_hash was confirmed + let tx_height = get_verifying_block_height() as u32; + let current_height = get_verifying_block_height() as u32; + if current_height - tx_height > PROPOSAL_SNAPSHOT_CUTOFF_LIMIT { + msg!("[Dao::Propose] Error: Snapshot is too old. Current height: {}, snapshot height: {}", + current_height, tx_height); + return Err(DaoError::SnapshotTooOld.into()) } - - propose_nullifiers.push(input.nullifier); } // Is the DAO bulla generated in the ZK proof valid diff --git a/src/contract/dao/src/error.rs b/src/contract/dao/src/error.rs index fae297d19..a33a1df94 100644 --- a/src/contract/dao/src/error.rs +++ b/src/contract/dao/src/error.rs @@ -32,6 +32,12 @@ pub enum DaoError { #[error("Invalid input Merkle root")] InvalidInputMerkleRoot, + #[error("Snapshoot roots do not match")] + NonMatchingSnapshotRoots, + + #[error("Snapshoot is past the cutoff limit")] + SnapshotTooOld, + #[error("Invalid DAO Merkle root")] InvalidDaoMerkleRoot, @@ -94,24 +100,26 @@ impl From for ContractError { DaoError::DaoAlreadyExists => Self::Custom(2), DaoError::ProposalInputsEmpty => Self::Custom(3), DaoError::InvalidInputMerkleRoot => Self::Custom(4), - DaoError::InvalidDaoMerkleRoot => Self::Custom(5), - DaoError::ProposalAlreadyExists => Self::Custom(6), - DaoError::VoteInputsEmpty => Self::Custom(7), - DaoError::ProposalNonexistent => Self::Custom(8), - DaoError::ProposalEnded => Self::Custom(9), - DaoError::CoinAlreadySpent => Self::Custom(10), - DaoError::DoubleVote => Self::Custom(11), - DaoError::ExecCallWrongChildCallsLen => Self::Custom(12), - DaoError::ExecCallWrongChildCall => Self::Custom(13), - DaoError::ExecCallInvalidFormat => Self::Custom(14), - DaoError::ExecCallValueMismatch => Self::Custom(15), - DaoError::VoteCommitMismatch => Self::Custom(16), - DaoError::AuthXferSiblingWrongContractId => Self::Custom(17), - DaoError::AuthXferSiblingWrongFunctionCode => Self::Custom(18), - DaoError::AuthXferNonMatchingEncInputUserData => Self::Custom(19), - DaoError::AuthXferCallNotFoundInParent => Self::Custom(20), - DaoError::AuthXferWrongNumberOutputs => Self::Custom(21), - DaoError::AuthXferWrongOutputCoin => Self::Custom(22), + DaoError::NonMatchingSnapshotRoots => Self::Custom(5), + DaoError::SnapshotTooOld => Self::Custom(6), + DaoError::InvalidDaoMerkleRoot => Self::Custom(7), + DaoError::ProposalAlreadyExists => Self::Custom(8), + DaoError::VoteInputsEmpty => Self::Custom(9), + DaoError::ProposalNonexistent => Self::Custom(10), + DaoError::ProposalEnded => Self::Custom(11), + DaoError::CoinAlreadySpent => Self::Custom(12), + DaoError::DoubleVote => Self::Custom(13), + DaoError::ExecCallWrongChildCallsLen => Self::Custom(14), + DaoError::ExecCallWrongChildCall => Self::Custom(15), + DaoError::ExecCallInvalidFormat => Self::Custom(16), + DaoError::ExecCallValueMismatch => Self::Custom(17), + DaoError::VoteCommitMismatch => Self::Custom(18), + DaoError::AuthXferSiblingWrongContractId => Self::Custom(19), + DaoError::AuthXferSiblingWrongFunctionCode => Self::Custom(20), + DaoError::AuthXferNonMatchingEncInputUserData => Self::Custom(21), + DaoError::AuthXferCallNotFoundInParent => Self::Custom(22), + DaoError::AuthXferWrongNumberOutputs => Self::Custom(23), + DaoError::AuthXferWrongOutputCoin => Self::Custom(24), } } } diff --git a/src/contract/dao/src/lib.rs b/src/contract/dao/src/lib.rs index d6509b374..b34b557c2 100644 --- a/src/contract/dao/src/lib.rs +++ b/src/contract/dao/src/lib.rs @@ -89,6 +89,9 @@ pub const DAO_CONTRACT_ZKAS_DAO_AUTH_MONEY_TRANSFER_NS: &str = "AuthMoneyTransfe /// zkas dao auth money_transfer encrypted coin circuit namespace pub const DAO_CONTRACT_ZKAS_DAO_AUTH_MONEY_TRANSFER_ENC_COIN_NS: &str = "AuthMoneyTransferEncCoin"; +/// Not allowed to make proposals using snapshots with block heights older than this depth +pub const PROPOSAL_SNAPSHOT_CUTOFF_LIMIT: u32 = 100; + // ANCHOR: dao-blockwindow const BLOCK_TIME: u64 = 90; const SECS_IN_HOUR: u64 = 60 * 60; diff --git a/src/contract/dao/src/model.rs b/src/contract/dao/src/model.rs index c9bfc60fa..4970444f2 100644 --- a/src/contract/dao/src/model.rs +++ b/src/contract/dao/src/model.rs @@ -242,11 +242,12 @@ pub struct DaoProposeParams { // ANCHOR: dao-propose-params-input /// Input for a DAO proposal pub struct DaoProposeParamsInput { - pub nullifier: Nullifier, /// Value commitment for the input pub value_commit: pallas::Point, - /// Merkle root for the input's inclusion proof - pub merkle_root: MerkleNode, + /// Merkle root for the input's coin inclusion proof + pub merkle_coin_root: MerkleNode, + /// SMT root for the input's nullifier exclusion proof + pub smt_null_root: pallas::Base, /// Public key used for signing pub signature_public: PublicKey, } diff --git a/src/contract/money/src/entrypoint.rs b/src/contract/money/src/entrypoint.rs index c5c669988..8d88bf850 100644 --- a/src/contract/money/src/entrypoint.rs +++ b/src/contract/money/src/entrypoint.rs @@ -125,16 +125,29 @@ fn init_contract(cid: ContractId, _ix: &[u8]) -> ContractResult { zkas_db_set(&token_mint_v1_bincode[..])?; zkas_db_set(&token_frz_v1_bincode[..])?; + // FIXME: Get tx hash from env + let tx_hash = [0u8; 32]; + // No way to access call_idx here + //assert!(ix.len() > 4); + //let call_idx: u32 = deserialize(&ix[0..4])?; + let call_idx = 110u16; + let mut roots_value_data = Vec::with_capacity(32 + 2); + tx_hash.encode(&mut roots_value_data)?; + call_idx.encode(&mut roots_value_data)?; + assert_eq!(roots_value_data.len(), 32 + 2); + // Set up a database tree to hold Merkle roots of all coin trees - // k=root_hash:32, v=(block_height:3, tx_idx:2, call_idx: 2) + // k=root_hash:32, v=(tx_hash:32, call_idx: 2) if db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE).is_err() { - db_init(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?; + let db_coin_roots = db_init(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?; + db_set(db_coin_roots, &serialize(&EMPTY_COINS_TREE_ROOT), &roots_value_data)?; } // Set up a database tree to hold Merkle roots of all nullifier trees - // k=root_hash:32, v=(block_height:3, tx_idx:2, call_idx: 2) + // k=root_hash:32, v=(tx_hash:32, call_idx: 2) if db_lookup(cid, MONEY_CONTRACT_NULLIFIER_ROOTS_TREE).is_err() { - db_init(cid, MONEY_CONTRACT_NULLIFIER_ROOTS_TREE)?; + let db_null_roots = db_init(cid, MONEY_CONTRACT_NULLIFIER_ROOTS_TREE)?; + db_set(db_null_roots, &serialize(&EMPTY_NODES_FP[0]), &roots_value_data)?; } // Set up a database tree to hold all coins ever seen diff --git a/src/contract/money/src/entrypoint/genesis_mint_v1.rs b/src/contract/money/src/entrypoint/genesis_mint_v1.rs index 2a26abd4b..82045541f 100644 --- a/src/contract/money/src/entrypoint/genesis_mint_v1.rs +++ b/src/contract/money/src/entrypoint/genesis_mint_v1.rs @@ -21,7 +21,8 @@ use darkfi_sdk::{ dark_tree::DarkLeaf, db::{db_contains_key, db_lookup, db_set}, error::{ContractError, ContractResult}, - merkle_add, msg, + merkle::{merkle_add, sparse_merkle_insert_batch}, + msg, pasta::pallas, util::get_verifying_block_height, ContractCall, @@ -33,7 +34,8 @@ use crate::{ model::{MoneyGenesisMintParamsV1, MoneyGenesisMintUpdateV1, DARK_TOKEN_ID}, MoneyFunction, MONEY_CONTRACT_COINS_TREE, MONEY_CONTRACT_COIN_MERKLE_TREE, MONEY_CONTRACT_COIN_ROOTS_TREE, MONEY_CONTRACT_INFO_TREE, MONEY_CONTRACT_LATEST_COIN_ROOT, - MONEY_CONTRACT_ZKAS_MINT_NS_V1, + MONEY_CONTRACT_LATEST_NULLIFIER_ROOT, MONEY_CONTRACT_NULLIFIERS_TREE, + MONEY_CONTRACT_NULLIFIER_ROOTS_TREE, MONEY_CONTRACT_ZKAS_MINT_NS_V1, }; /// `get_metadata` function for `Money::GenesisMintV1` @@ -140,7 +142,19 @@ pub(crate) fn money_genesis_mint_process_update_v1( // Grab all db handles we want to work on let info_db = db_lookup(cid, MONEY_CONTRACT_INFO_TREE)?; let coins_db = db_lookup(cid, MONEY_CONTRACT_COINS_TREE)?; + let nullifiers_db = db_lookup(cid, MONEY_CONTRACT_NULLIFIERS_TREE)?; let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?; + let nullifier_roots_db = db_lookup(cid, MONEY_CONTRACT_NULLIFIER_ROOTS_TREE)?; + + // This will just make a snapshot to match the coins one + msg!("[GenesisMintV1] Updating nullifiers snapshot"); + sparse_merkle_insert_batch( + info_db, + nullifiers_db, + nullifier_roots_db, + MONEY_CONTRACT_LATEST_NULLIFIER_ROOT, + &vec![], + )?; msg!("[GenesisMintV1] Adding new coin to the set"); db_set(coins_db, &serialize(&update.coin), &[])?; diff --git a/src/contract/money/src/entrypoint/pow_reward_v1.rs b/src/contract/money/src/entrypoint/pow_reward_v1.rs index dccec8244..94f4dc794 100644 --- a/src/contract/money/src/entrypoint/pow_reward_v1.rs +++ b/src/contract/money/src/entrypoint/pow_reward_v1.rs @@ -22,7 +22,8 @@ use darkfi_sdk::{ dark_tree::DarkLeaf, db::{db_contains_key, db_lookup, db_set}, error::{ContractError, ContractResult}, - merkle_add, msg, + merkle::{merkle_add, sparse_merkle_insert_batch}, + msg, pasta::pallas, util::{get_last_block_height, get_verifying_block_height}, ContractCall, @@ -34,7 +35,8 @@ use crate::{ model::{MoneyPoWRewardParamsV1, MoneyPoWRewardUpdateV1, DARK_TOKEN_ID}, MoneyFunction, MONEY_CONTRACT_COINS_TREE, MONEY_CONTRACT_COIN_MERKLE_TREE, MONEY_CONTRACT_COIN_ROOTS_TREE, MONEY_CONTRACT_INFO_TREE, MONEY_CONTRACT_LATEST_COIN_ROOT, - MONEY_CONTRACT_ZKAS_MINT_NS_V1, + MONEY_CONTRACT_LATEST_NULLIFIER_ROOT, MONEY_CONTRACT_NULLIFIERS_TREE, + MONEY_CONTRACT_NULLIFIER_ROOTS_TREE, MONEY_CONTRACT_ZKAS_MINT_NS_V1, }; /// `get_metadata` function for `Money::PoWRewardV1` @@ -165,7 +167,19 @@ pub(crate) fn money_pow_reward_process_update_v1( // Grab all db handles we want to work on let info_db = db_lookup(cid, MONEY_CONTRACT_INFO_TREE)?; let coins_db = db_lookup(cid, MONEY_CONTRACT_COINS_TREE)?; + let nullifiers_db = db_lookup(cid, MONEY_CONTRACT_NULLIFIERS_TREE)?; let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?; + let nullifier_roots_db = db_lookup(cid, MONEY_CONTRACT_NULLIFIER_ROOTS_TREE)?; + + // This will just make a snapshot to match the coins one + msg!("[PowRewardV1] Updating nullifiers snapshot"); + sparse_merkle_insert_batch( + info_db, + nullifiers_db, + nullifier_roots_db, + MONEY_CONTRACT_LATEST_NULLIFIER_ROOT, + &vec![], + )?; msg!("[PoWRewardV1] Adding new coin to the set"); db_set(coins_db, &serialize(&update.coin), &[])?; diff --git a/src/contract/money/src/entrypoint/token_mint_v1.rs b/src/contract/money/src/entrypoint/token_mint_v1.rs index e6e2efc6b..39fab3823 100644 --- a/src/contract/money/src/entrypoint/token_mint_v1.rs +++ b/src/contract/money/src/entrypoint/token_mint_v1.rs @@ -21,7 +21,8 @@ use darkfi_sdk::{ dark_tree::DarkLeaf, db::{db_contains_key, db_lookup, db_set}, error::{ContractError, ContractResult}, - merkle_add, msg, + merkle::{merkle_add, sparse_merkle_insert_batch}, + msg, pasta::pallas, ContractCall, }; @@ -32,7 +33,8 @@ use crate::{ model::{MoneyTokenMintParamsV1, MoneyTokenMintUpdateV1}, MoneyFunction, MONEY_CONTRACT_COINS_TREE, MONEY_CONTRACT_COIN_MERKLE_TREE, MONEY_CONTRACT_COIN_ROOTS_TREE, MONEY_CONTRACT_INFO_TREE, MONEY_CONTRACT_LATEST_COIN_ROOT, - MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1, + MONEY_CONTRACT_LATEST_NULLIFIER_ROOT, MONEY_CONTRACT_NULLIFIERS_TREE, + MONEY_CONTRACT_NULLIFIER_ROOTS_TREE, MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1, }; /// `get_metadata` function for `Money::TokenMintV1` @@ -106,7 +108,19 @@ pub(crate) fn money_token_mint_process_update_v1( // Grab all db handles we want to work on let info_db = db_lookup(cid, MONEY_CONTRACT_INFO_TREE)?; let coins_db = db_lookup(cid, MONEY_CONTRACT_COINS_TREE)?; + let nullifiers_db = db_lookup(cid, MONEY_CONTRACT_NULLIFIERS_TREE)?; let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?; + let nullifier_roots_db = db_lookup(cid, MONEY_CONTRACT_NULLIFIER_ROOTS_TREE)?; + + // This will just make a snapshot to match the coins one + msg!("[MintV1] Updating nullifiers snapshot"); + sparse_merkle_insert_batch( + info_db, + nullifiers_db, + nullifier_roots_db, + MONEY_CONTRACT_LATEST_NULLIFIER_ROOT, + &vec![], + )?; msg!("[MintV1] Adding new coin to the set"); db_set(coins_db, &serialize(&update.coin), &[])?; diff --git a/src/contract/money/src/lib.rs b/src/contract/money/src/lib.rs index a0dbfabcb..170c94af0 100644 --- a/src/contract/money/src/lib.rs +++ b/src/contract/money/src/lib.rs @@ -83,7 +83,8 @@ pub const MONEY_CONTRACT_LATEST_COIN_ROOT: &[u8] = b"last_coins_root"; pub const MONEY_CONTRACT_LATEST_NULLIFIER_ROOT: &[u8] = b"last_nullifiers_root"; pub const MONEY_CONTRACT_TOTAL_FEES_PAID: &[u8] = b"total_fees_paid"; -/// Precalculated root hash for a tree containing Fp::ZERO to save gas +/// Precalculated root hash for a tree containing only a single Fp::ZERO coin. +/// Used to save gas. pub const EMPTY_COINS_TREE_ROOT: [u8; 32] = [ 0xb8, 0xc1, 0x07, 0x5a, 0x80, 0xa8, 0x09, 0x65, 0xc2, 0x39, 0x8f, 0x71, 0x1f, 0xe7, 0x3e, 0x05, 0xb4, 0xed, 0xae, 0xde, 0xf1, 0x62, 0xf2, 0x61, 0xd4, 0xee, 0xd7, 0xcd, 0x72, 0x74, 0x8d, 0x17, diff --git a/src/contract/test-harness/src/contract_deploy.rs b/src/contract/test-harness/src/contract_deploy.rs index c0edf0886..8b2a1f3da 100644 --- a/src/contract/test-harness/src/contract_deploy.rs +++ b/src/contract/test-harness/src/contract_deploy.rs @@ -110,6 +110,12 @@ impl TestHarness { } if let Some(ref fee_params) = fee_params { + let nullifier = fee_params.input.nullifier.inner(); + wallet + .money_null_smt + .insert_batch(vec![(nullifier, nullifier)]) + .expect("smt.insert_batch()"); + if let Some(spent_coin) = wallet .unspent_money_coins .iter() diff --git a/src/contract/test-harness/src/dao_exec.rs b/src/contract/test-harness/src/dao_exec.rs index 483b5b23f..731266726 100644 --- a/src/contract/test-harness/src/dao_exec.rs +++ b/src/contract/test-harness/src/dao_exec.rs @@ -274,6 +274,9 @@ impl TestHarness { outputs.push(fee_params.output.clone()); } + let nullifiers = inputs.iter().map(|i| i.nullifier.inner()).map(|l| (l, l)).collect(); + wallet.money_null_smt.insert_batch(nullifiers).expect("smt.insert_batch()"); + for input in inputs { if let Some(spent_coin) = wallet .unspent_money_coins diff --git a/src/contract/test-harness/src/dao_mint.rs b/src/contract/test-harness/src/dao_mint.rs index d47fc8a7d..35f795c3a 100644 --- a/src/contract/test-harness/src/dao_mint.rs +++ b/src/contract/test-harness/src/dao_mint.rs @@ -118,6 +118,12 @@ impl TestHarness { wallet.dao_leafs.insert(params.dao_bulla, leaf_pos); if let Some(ref fee_params) = fee_params { + let nullifier = fee_params.input.nullifier.inner(); + wallet + .money_null_smt + .insert_batch(vec![(nullifier, nullifier)]) + .expect("smt.insert_batch()"); + if let Some(spent_coin) = wallet .unspent_money_coins .iter() diff --git a/src/contract/test-harness/src/dao_propose.rs b/src/contract/test-harness/src/dao_propose.rs index 4a3703af4..1ff964646 100644 --- a/src/contract/test-harness/src/dao_propose.rs +++ b/src/contract/test-harness/src/dao_propose.rs @@ -73,6 +73,32 @@ impl TestHarness { let signature_secret = SecretKey::random(&mut OsRng); + debug!("ABOUT TO SHOW DB INTERNALZZZ!!!!!"); + { + let blockchain = &wallet.validator.blockchain; + let contracts = &blockchain.contracts; + let tree = contracts + .lookup(&blockchain.sled_db, &MONEY_CONTRACT_ID, "nullifier_roots") + .unwrap(); + for kv in tree.iter() { + let (key, value) = kv.unwrap(); + debug!("STATE {:?}", key); + debug!(" => {:?}", value); + } + } + debug!("[REDUX] ABOUT TO SHOW DB INTERNALZZZ!!!!!"); + { + let blockchain = &wallet.validator.blockchain; + let contracts = &blockchain.contracts; + let tree = + contracts.lookup(&blockchain.sled_db, &MONEY_CONTRACT_ID, "coin_roots").unwrap(); + for kv in tree.iter() { + let (key, value) = kv.unwrap(); + debug!("STATE {:?}", key); + debug!(" => {:?}", value); + } + } + let input = DaoProposeStakeInput { secret: wallet.keypair.secret, note: propose_owncoin.note.clone(), @@ -81,6 +107,7 @@ impl TestHarness { .money_merkle_tree .witness(propose_owncoin.leaf_position, 0) .unwrap(), + money_null_smt: &wallet.money_null_smt, signature_secret, }; @@ -197,6 +224,12 @@ impl TestHarness { wallet.dao_prop_leafs.insert(params.proposal_bulla, (prop_leaf_pos, prop_money_snapshot)); if let Some(ref fee_params) = fee_params { + let nullifier = fee_params.input.nullifier.inner(); + wallet + .money_null_smt + .insert_batch(vec![(nullifier, nullifier)]) + .expect("smt.insert_batch()"); + if let Some(spent_coin) = wallet .unspent_money_coins .iter() diff --git a/src/contract/test-harness/src/dao_vote.rs b/src/contract/test-harness/src/dao_vote.rs index 81fcb0eb2..ddd6e2fc2 100644 --- a/src/contract/test-harness/src/dao_vote.rs +++ b/src/contract/test-harness/src/dao_vote.rs @@ -155,6 +155,12 @@ impl TestHarness { } if let Some(ref fee_params) = fee_params { + let nullifier = fee_params.input.nullifier.inner(); + wallet + .money_null_smt + .insert_batch(vec![(nullifier, nullifier)]) + .expect("smt.insert_batch()"); + if let Some(spent_coin) = wallet .unspent_money_coins .iter() diff --git a/src/contract/test-harness/src/lib.rs b/src/contract/test-harness/src/lib.rs index 614e68bed..6a2394dfa 100644 --- a/src/contract/test-harness/src/lib.rs +++ b/src/contract/test-harness/src/lib.rs @@ -35,7 +35,10 @@ use darkfi_dao_contract::model::{DaoBulla, DaoProposalBulla}; use darkfi_money_contract::client::OwnCoin; use darkfi_sdk::{ bridgetree, - crypto::{Keypair, MerkleNode, MerkleTree}, + crypto::{ + smt::{MemoryStorageFp, PoseidonFp, SmtMemoryFp, EMPTY_NODES_FP}, + Keypair, MerkleNode, MerkleTree, + }, pasta::pallas, }; use darkfi_serial::{Encodable, WriteExt}; @@ -87,8 +90,8 @@ pub fn init_logger() { // We check this error so we can execute same file tests in parallel, // otherwise second one fails to init logger here. if simplelog::TermLogger::init( - simplelog::LevelFilter::Info, - //simplelog::LevelFilter::Debug, + //simplelog::LevelFilter::Info, + simplelog::LevelFilter::Debug, //simplelog::LevelFilter::Trace, cfg.build(), simplelog::TerminalMode::Mixed, @@ -122,6 +125,8 @@ pub struct Wallet { pub validator: ValidatorPtr, /// Holder's instance of the Merkle tree for the `Money` contract pub money_merkle_tree: MerkleTree, + /// Holder's instance of the Merkle tree for the `Money` contract + pub money_null_smt: SmtMemoryFp, /// Holder's instance of the Merkle tree for the `DAO` contract (holding DAO bullas) pub dao_merkle_tree: MerkleTree, /// Holder's instance of the Merkle tree for the `DAO` contract (holding DAO proposals) @@ -170,12 +175,17 @@ impl Wallet { money_merkle_tree.append(MerkleNode::from(pallas::Base::ZERO)); money_merkle_tree.mark().unwrap(); + let hasher = PoseidonFp::new(); + let store = MemoryStorageFp::new(); + let money_null_smt = SmtMemoryFp::new(store, hasher.clone(), &EMPTY_NODES_FP); + Ok(Self { keypair, token_mint_authority, contract_deploy_authority, validator, money_merkle_tree, + money_null_smt, dao_merkle_tree: MerkleTree::new(100), dao_proposals_tree: MerkleTree::new(100), unspent_money_coins: vec![], diff --git a/src/contract/test-harness/src/money_fee.rs b/src/contract/test-harness/src/money_fee.rs index 11ea84afc..319eddbec 100644 --- a/src/contract/test-harness/src/money_fee.rs +++ b/src/contract/test-harness/src/money_fee.rs @@ -163,6 +163,12 @@ impl TestHarness { ) -> Result> { let wallet = self.holders.get_mut(holder).unwrap(); + let nullifier = params.input.nullifier.inner(); + wallet + .money_null_smt + .insert_batch(vec![(nullifier, nullifier)]) + .expect("smt.insert_batch()"); + wallet.add_transaction("money::fee", tx, block_height, self.verify_fees).await?; wallet.money_merkle_tree.append(MerkleNode::from(params.output.coin.inner())); diff --git a/src/contract/test-harness/src/money_otc_swap.rs b/src/contract/test-harness/src/money_otc_swap.rs index a57502969..2bb48511e 100644 --- a/src/contract/test-harness/src/money_otc_swap.rs +++ b/src/contract/test-harness/src/money_otc_swap.rs @@ -201,6 +201,9 @@ impl TestHarness { outputs.push(fee_params.output.clone()); } + let nullifiers = inputs.iter().map(|i| i.nullifier.inner()).map(|l| (l, l)).collect(); + wallet.money_null_smt.insert_batch(nullifiers).expect("smt.insert_batch()"); + for input in inputs { if let Some(spent_coin) = wallet .unspent_money_coins diff --git a/src/contract/test-harness/src/money_token.rs b/src/contract/test-harness/src/money_token.rs index 938450d38..61c82120e 100644 --- a/src/contract/test-harness/src/money_token.rs +++ b/src/contract/test-harness/src/money_token.rs @@ -340,6 +340,12 @@ impl TestHarness { let mut found_owncoins = vec![]; if let Some(ref fee_params) = fee_params { if append { + let nullifier = fee_params.input.nullifier.inner(); + wallet + .money_null_smt + .insert_batch(vec![(nullifier, nullifier)]) + .expect("smt.insert_batch()"); + if let Some(spent_coin) = wallet .unspent_money_coins .iter() diff --git a/src/contract/test-harness/src/money_transfer.rs b/src/contract/test-harness/src/money_transfer.rs index 9a0729ec1..57befe38d 100644 --- a/src/contract/test-harness/src/money_transfer.rs +++ b/src/contract/test-harness/src/money_transfer.rs @@ -134,8 +134,11 @@ impl TestHarness { inputs.push(fee_params.input.clone()); } + let nullifiers = inputs.iter().map(|i| i.nullifier.inner()).map(|l| (l, l)).collect(); + wallet.money_null_smt.insert_batch(nullifiers).expect("smt.insert_batch()"); + if append { - for input in inputs.iter() { + for input in &inputs { if let Some(spent_coin) = wallet .unspent_money_coins .iter() @@ -156,7 +159,7 @@ impl TestHarness { outputs.push(fee_params.output.clone()); } - for output in outputs.iter() { + for output in &outputs { if !append { continue } diff --git a/src/runtime/import/db.rs b/src/runtime/import/db.rs index 18f20f0b8..b362dae24 100644 --- a/src/runtime/import/db.rs +++ b/src/runtime/import/db.rs @@ -192,7 +192,7 @@ pub(crate) fn db_init(mut ctx: FunctionEnvMut, ptr: WasmPtr, ptr_len: u } /// Lookup a database handle from its name. -/// If it does not exist, push it to the Vector of db_handles. +/// If it exists, push it to the Vector of db_handles. /// /// Returns the index of the DbHandle in the db_handles Vector on success. /// Otherwise, returns an error value. diff --git a/src/runtime/import/merkle.rs b/src/runtime/import/merkle.rs index bc5c7b4f5..de059abcd 100644 --- a/src/runtime/import/merkle.rs +++ b/src/runtime/import/merkle.rs @@ -20,7 +20,7 @@ use std::io::Cursor; use darkfi_sdk::crypto::{MerkleNode, MerkleTree}; use darkfi_serial::{serialize, Decodable, Encodable, WriteExt}; -use log::{debug, error, warn}; +use log::{debug, error}; use wasmer::{FunctionEnvMut, WasmPtr}; use super::acl::acl_allow; @@ -156,15 +156,6 @@ pub(crate) fn merkle_add(mut ctx: FunctionEnvMut, ptr: WasmPtr, len: u3 } }; - // Nothing to do so just return here - if coins.is_empty() { - warn!( - target: "runtime::merkle::merkle_add", - "[WASM] [{}] merkle_add(): Nothing to add! Returning.", cid, - ); - return darkfi_sdk::entrypoint::SUCCESS - } - // Make sure we've read the entire buffer if buf_reader.position() != (len as u64) { error!( diff --git a/src/runtime/import/smt.rs b/src/runtime/import/smt.rs index 828ceeb92..910687a89 100644 --- a/src/runtime/import/smt.rs +++ b/src/runtime/import/smt.rs @@ -24,7 +24,7 @@ use darkfi_sdk::crypto::{ }; use darkfi_serial::{serialize, Decodable, Encodable}; use halo2_proofs::pasta::pallas; -use log::{debug, error, warn}; +use log::{debug, error}; use num_bigint::BigUint; use wasmer::{FunctionEnvMut, WasmPtr}; @@ -201,15 +201,6 @@ pub(crate) fn sparse_merkle_insert_batch( } }; - // Nothing to do so just return here - if nullifiers.is_empty() { - warn!( - target: "runtime::smt::sparse_merkle_insert_batch", - "[WASM] [{}] sparse_merkle_insert_batch(): Nothing to add! Returning.", cid - ); - return darkfi_sdk::entrypoint::SUCCESS - } - // Make sure we've read the entire buffer if buf_reader.position() != (len as u64) { error!( diff --git a/src/sdk/src/crypto/smt/mod.rs b/src/sdk/src/crypto/smt/mod.rs index 3b84419d2..64e9d0045 100644 --- a/src/sdk/src/crypto/smt/mod.rs +++ b/src/sdk/src/crypto/smt/mod.rs @@ -167,6 +167,10 @@ impl< /// Takes a batch of field elements, inserts these hashes into the tree, /// and updates the Merkle root. pub fn insert_batch(&mut self, leaves: Vec<(F, F)>) -> ContractResult { + if leaves.is_empty() { + return Ok(()) + } + // Nodes that need recalculating let mut dirty_idxs = Vec::new(); for (pos, leaf) in leaves { diff --git a/src/sdk/src/lib.rs b/src/sdk/src/lib.rs index a13f772f6..811fe0c30 100644 --- a/src/sdk/src/lib.rs +++ b/src/sdk/src/lib.rs @@ -44,7 +44,6 @@ pub mod crypto; /// Merkle pub mod merkle; -pub use merkle::merkle_add; /// Transaction structure pub mod tx; diff --git a/src/zk/vm_heap.rs b/src/zk/vm_heap.rs index 85ce29874..958f3136d 100644 --- a/src/zk/vm_heap.rs +++ b/src/zk/vm_heap.rs @@ -119,7 +119,7 @@ macro_rules! impl_try_from { match value { HeapVar::$variant(v) => Ok(v), x => { - error!("Invalid TryFrom conversion {:?}", x); + error!("Expected {}, but instead got: {:?}", stringify!($variant), x); Err(plonk::Error::Synthesis) } }