Bläddra i källkod

DAO::propose(): goodbye nullifiers, hello SMT

zero 2 år sedan
förälder
incheckning
fbe13ed480

+ 18 - 7
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:

+ 35 - 24
src/contract/dao/src/client/propose.rs

@@ -16,13 +16,14 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-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<MerkleNode>,
+    pub money_null_smt: &'a SmtMemoryFp,
     pub signature_secret: SecretKey,
 }
 
-pub struct DaoProposeCall {
-    pub inputs: Vec<DaoProposeStakeInput>,
+pub struct DaoProposeCall<'a> {
+    pub inputs: Vec<DaoProposeStakeInput<'a>>,
     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);
         }
 

+ 2 - 1
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,
 };

+ 33 - 17
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 &params.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())
+        };
 
-        // 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())
+        // 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())
         }
 
-        propose_nullifiers.push(input.nullifier);
+        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())
+        }
     }
 
     // Is the DAO bulla generated in the ZK proof valid

+ 26 - 18
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<DaoError> 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),
         }
     }
 }

+ 3 - 0
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;

+ 4 - 3
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,
 }

+ 17 - 4
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

+ 16 - 2
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), &[])?;

+ 16 - 2
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), &[])?;

+ 16 - 2
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), &[])?;

+ 2 - 1
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,

+ 6 - 0
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()

+ 3 - 0
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

+ 6 - 0
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()

+ 33 - 0
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()

+ 6 - 0
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()

+ 13 - 3
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![],

+ 6 - 0
src/contract/test-harness/src/money_fee.rs

@@ -163,6 +163,12 @@ impl TestHarness {
     ) -> Result<Vec<OwnCoin>> {
         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()));
 

+ 3 - 0
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

+ 6 - 0
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()

+ 5 - 2
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
             }

+ 1 - 1
src/runtime/import/db.rs

@@ -192,7 +192,7 @@ pub(crate) fn db_init(mut ctx: FunctionEnvMut<Env>, ptr: WasmPtr<u8>, 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.

+ 1 - 10
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<Env>, ptr: WasmPtr<u8>, 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!(

+ 1 - 10
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!(

+ 4 - 0
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 {

+ 0 - 1
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;

+ 1 - 1
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)
                     }
                 }