Sfoglia il codice sorgente

contract/dao/propose: enforce all input proofs to be verified against the same snapshot

skoupidi 2 mesi fa
parent
commit
3e7ba87da2

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

@@ -2545,6 +2545,7 @@ impl Drk {
 
         // Create the proposal call
         let call = DaoProposeCall {
+            money_merkle_coin_root: money_merkle_tree.root(0).unwrap(),
             money_null_smt: &money_null_smt,
             inputs,
             proposal: proposal.proposal.clone(),
@@ -2725,6 +2726,7 @@ impl Drk {
 
         // Create the proposal call
         let call = DaoProposeCall {
+            money_merkle_coin_root: money_merkle_tree.root(0).unwrap(),
             money_null_smt: &money_null_smt,
             inputs,
             proposal: proposal.proposal.clone(),

+ 4 - 20
src/contract/dao/src/client/propose.rs

@@ -19,7 +19,6 @@
 use darkfi_money_contract::model::CoinAttributes;
 use darkfi_sdk::{
     bridgetree,
-    bridgetree::Hashable,
     crypto::{
         note::AeadEncryptedNote,
         pasta_prelude::*,
@@ -51,6 +50,7 @@ pub struct DaoProposeStakeInput {
 }
 
 pub struct DaoProposeCall<'a, T: StorageAdapter<Value = pallas::Base>> {
+    pub money_merkle_coin_root: MerkleNode,
     pub money_null_smt:
         &'a SparseMerkleTree<'a, SMT_FP_DEPTH, { SMT_FP_DEPTH + 1 }, pallas::Base, PoseidonFp, T>,
     pub inputs: Vec<DaoProposeStakeInput>,
@@ -133,22 +133,6 @@ impl<T: StorageAdapter<Value = pallas::Base>> DaoProposeCall<'_, T> {
                 Witness::Base(Value::known(signature_secret.inner())),
             ];
 
-            // TODO: We need a generic ZkSet widget to avoid doing this all the time
-
-            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() {
-                    let level = level as u8;
-                    current = if position & (1 << level) == 0 {
-                        MerkleNode::combine(level.into(), &current, sibling)
-                    } else {
-                        MerkleNode::combine(level.into(), sibling, &current)
-                    };
-                }
-                current
-            };
-
             let token_commit = poseidon_hash([note.token_id.inner(), gov_token_blind.inner()]);
             if note.token_id != self.dao.gov_token_id {
                 return Err(ClientFailed::InvalidTokenId(note.token_id.to_string()).into())
@@ -166,7 +150,7 @@ impl<T: StorageAdapter<Value = pallas::Base>> DaoProposeCall<'_, T> {
                 *value_coords.x(),
                 *value_coords.y(),
                 token_commit,
-                merkle_coin_root.inner(),
+                self.money_merkle_coin_root.inner(),
                 sig_x,
                 sig_y,
             ];
@@ -180,8 +164,6 @@ impl<T: StorageAdapter<Value = pallas::Base>> DaoProposeCall<'_, T> {
 
             let input = DaoProposeParamsInput {
                 value_commit,
-                merkle_coin_root,
-                smt_null_root,
                 input_nullifier: input_nullifier.into(),
                 signature_public,
             };
@@ -260,6 +242,8 @@ impl<T: StorageAdapter<Value = pallas::Base>> DaoProposeCall<'_, T> {
                 .unwrap();
         let params = DaoProposeParams {
             dao_merkle_root: self.dao_merkle_root,
+            merkle_coin_root: self.money_merkle_coin_root,
+            smt_null_root,
             proposal_bulla,
             token_commit,
             note: enc_note,

+ 65 - 62
src/contract/dao/src/entrypoint/propose.rs

@@ -74,13 +74,13 @@ pub(crate) fn dao_propose_get_metadata(
         zk_public_inputs.push((
             DAO_CONTRACT_ZKAS_PROPOSE_INPUT_NS.to_string(),
             vec![
-                input.smt_null_root,
+                params.smt_null_root,
                 params.proposal_bulla.inner(),
                 input.input_nullifier.inner(),
                 *value_coords.x(),
                 *value_coords.y(),
                 params.token_commit,
-                input.merkle_coin_root.inner(),
+                params.merkle_coin_root.inner(),
                 sig_x,
                 sig_y,
             ],
@@ -122,76 +122,79 @@ pub(crate) fn dao_propose_process_instruction(
     let self_ = &calls[call_idx].data;
     let params: DaoProposeParams = deserialize(&self_.data[1..])?;
 
-    let coin_roots_db = wasm::db::db_lookup(*MONEY_CONTRACT_ID, MONEY_CONTRACT_COIN_ROOTS_TREE)?;
-    let null_roots_db =
-        wasm::db::db_lookup(*MONEY_CONTRACT_ID, MONEY_CONTRACT_NULLIFIER_ROOTS_TREE)?;
+    // Verify inputs are unique
     let mut input_nullifiers = vec![];
-
     for input in &params.inputs {
-        // Check input has not been reused
         if input_nullifiers.contains(&input.input_nullifier) {
             msg!("[Dao::Propose] Error: Attempted to reuse input");
             return Err(DaoError::ProposalInputsReuse.into())
         }
+        input_nullifiers.push(input.input_nullifier);
+    }
 
-        // Check the Merkle roots for the input coins are valid
-        let Some(coin_root_data) =
-            wasm::db::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())
-        };
-        if coin_root_data.len() != 32 + 1 {
-            msg!(
-                "[Dao::Propose] Error: Coin roots data length is not expected(32 + 1): {}",
-                coin_root_data.len()
-            );
-            return Err(MoneyError::RootsValueDataMismatch.into())
-        }
+    // Grab all db handles we want to work on
+    let coin_roots_db = wasm::db::db_lookup(*MONEY_CONTRACT_ID, MONEY_CONTRACT_COIN_ROOTS_TREE)?;
+    let null_roots_db =
+        wasm::db::db_lookup(*MONEY_CONTRACT_ID, MONEY_CONTRACT_NULLIFIER_ROOTS_TREE)?;
 
-        // Check the SMT roots for the input nullifiers are valid
-        let Some(null_root_data) =
-            wasm::db::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())
-        };
-
-        // Deserialize the SMT roots set
-        let null_root_data: Vec<Vec<u8>> = match deserialize(&null_root_data) {
-            Ok(set) => set,
-            Err(e) => {
-                msg!("[Dao::Propose] Error: Failed to deserialize nulls root snapshot: {}", e);
-                return Err(DaoError::SnapshotDeserializationError.into())
-            }
-        };
-
-        // Nullifiers roots snapshot must include the Merkle root data
-        if !null_root_data.contains(&coin_root_data) {
-            msg!("[Dao::Propose] Error: coin roots snapshot for {:?} does not exist in the nulls root snapshot {:?}",
-                 input.merkle_coin_root.inner(), input.smt_null_root);
-            return Err(DaoError::NonMatchingSnapshotRoots.into())
-        }
+    // Check the Merkle root for the input coins is valid
+    let Some(coin_root_data) =
+        wasm::db::db_get(coin_roots_db, &serialize(&params.merkle_coin_root))?
+    else {
+        msg!(
+            "[Dao::Propose] Error: Invalid input Merkle root: {:?}",
+            params.merkle_coin_root.inner()
+        );
+        return Err(DaoError::InvalidInputMerkleRoot.into())
+    };
+    if coin_root_data.len() != 32 + 1 {
+        msg!(
+            "[Dao::Propose] Error: Coins root data length is not expected(32 + 1): {}",
+            coin_root_data.len()
+        );
+        return Err(MoneyError::RootsValueDataMismatch.into())
+    }
+
+    // Check the SMT root for the input nullifiers is valid
+    let Some(null_root_data) = wasm::db::db_get(null_roots_db, &serialize(&params.smt_null_root))?
+    else {
+        msg!("[Dao::Propose] Error: Invalid inputs SMT root: {:?}", params.smt_null_root);
+        return Err(DaoError::InvalidInputMerkleRoot.into())
+    };
 
-        // Get block_height where tx_hash was confirmed
-        let tx_hash_data: [u8; 32] = coin_root_data[0..32].try_into().unwrap();
-        let tx_hash = TransactionHash(tx_hash_data);
-        let (tx_height, _) = wasm::util::get_tx_location(&tx_hash)?;
-
-        // Check snapshot age againts current height
-        let current_height = wasm::util::get_verifying_block_height()?;
-        // We assert here to prevent underflow and catch catastrophic
-        // internal failure.
-        assert!(current_height >= tx_height);
-        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())
+    // Deserialize the SMT roots set
+    let null_root_data: Vec<Vec<u8>> = match deserialize(&null_root_data) {
+        Ok(set) => set,
+        Err(e) => {
+            msg!("[Dao::Propose] Error: Failed to deserialize nulls root snapshot: {}", e);
+            return Err(DaoError::SnapshotDeserializationError.into())
         }
-        input_nullifiers.push(input.input_nullifier);
+    };
+
+    // Nullifiers roots snapshot must include the Merkle root data
+    if !null_root_data.contains(&coin_root_data) {
+        msg!("[Dao::Propose] Error: coin roots snapshot for {:?} does not exist in the nulls root snapshot {:?}",
+             params.merkle_coin_root.inner(), params.smt_null_root);
+        return Err(DaoError::NonMatchingSnapshotRoots.into())
+    }
+
+    // Get block_height where tx_hash was confirmed
+    let tx_hash_data: [u8; 32] = coin_root_data[0..32].try_into().unwrap();
+    let tx_hash = TransactionHash(tx_hash_data);
+    let (tx_height, _) = wasm::util::get_tx_location(&tx_hash)?;
+
+    // Check snapshot age againts current height
+    let current_height = wasm::util::get_verifying_block_height()?;
+    // We assert here to prevent underflow and catch catastrophic
+    // internal failure.
+    assert!(current_height >= tx_height);
+    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

+ 4 - 4
src/contract/dao/src/model.rs

@@ -264,6 +264,10 @@ pub struct DaoMintUpdate {
 pub struct DaoProposeParams {
     /// Merkle root of the DAO in the DAO state
     pub dao_merkle_root: MerkleNode,
+    /// Merkle root for the input coins inclusion proofs
+    pub merkle_coin_root: MerkleNode,
+    /// SMT root for the input nullifiers exclusion proofs
+    pub smt_null_root: pallas::Base,
     /// Token ID commitment for the proposal
     pub token_commit: pallas::Base,
     /// Bulla of the DAO proposal
@@ -281,10 +285,6 @@ pub struct DaoProposeParams {
 pub struct DaoProposeParamsInput {
     /// Value commitment for the input
     pub value_commit: pallas::Point,
-    /// 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,
     /// Input proposal specific nullifier
     pub input_nullifier: Nullifier,
     /// Public key used for signing

+ 2 - 0
src/contract/test-harness/src/dao_propose.rs

@@ -132,6 +132,7 @@ impl TestHarness {
         let dao_bulla = dao.to_bulla();
 
         let call = DaoProposeCall {
+            money_merkle_coin_root: wallet.money_merkle_tree.root(0).unwrap(),
             money_null_smt: &wallet.money_null_smt,
             inputs: vec![input],
             proposal: proposal.clone(),
@@ -236,6 +237,7 @@ impl TestHarness {
         let dao_bulla = dao.to_bulla();
 
         let call = DaoProposeCall {
+            money_merkle_coin_root: wallet.money_merkle_tree.root(0).unwrap(),
             money_null_smt: &wallet.money_null_smt,
             inputs: vec![input],
             proposal: proposal.clone(),

+ 2 - 2
src/contract/test-harness/src/vks.rs

@@ -49,8 +49,8 @@ use tracing::debug;
 
 /// Update these if any circuits are changed.
 /// Delete the existing cachefiles, and enable debug logging, you will see the new hashes.
-const PKS_HASH: &str = "ba2b7cbf8c306830b9881cb589dff276a7f1814ff84c694fe57ea55c668d52b8";
-const VKS_HASH: &str = "c90628c3b8437db940166f6d5bcf75bbe2ffd0841f831229437d05560cbfd103";
+const PKS_HASH: &str = "5466646bca6d6662dfb4689bb16cae2bb34e8ac88c17781f3b9a7eca63b866fb";
+const VKS_HASH: &str = "5e047421505c9d90b4d432c182737269776ebaf7f3f87dce44c1402f5bb4622c";
 
 /// Build a `PathBuf` to a cachefile
 fn cache_path(typ: &str) -> Result<PathBuf> {

+ 4 - 4
src/sdk/python/src/contract/dao/propose.rs

@@ -33,6 +33,8 @@ impl FunctionParams for dao_model::DaoProposeParams {
     fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
         let dict = PyDict::new(py);
         dict.set_item("dao_merkle_root", self.dao_merkle_root.to_string())?;
+        dict.set_item("merkle_coin_root", self.merkle_coin_root.to_string())?;
+        dict.set_item("smt_null_root", self.smt_null_root.to_string())?;
         dict.set_item("token_commit", self.token_commit.to_string())?;
         dict.set_item("proposal_bulla", self.proposal_bulla.to_string())?;
         dict.set_item("note", self.note.to_pydict(py)?)?;
@@ -49,6 +51,8 @@ impl FunctionParams for dao_model::DaoProposeParams {
     fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
         let prefix = format!("{}├─ ", "   ".repeat(depth));
         writeln!(out, "{prefix}dao_merkle_root: {}", self.dao_merkle_root).unwrap();
+        writeln!(out, "{prefix}merkle_coin_root: {}", self.merkle_coin_root).unwrap();
+        writeln!(out, "{prefix}smt_null_root: {:?}", self.smt_null_root).unwrap();
         writeln!(out, "{prefix}token_commit: {}", self.dao_merkle_root).unwrap();
         writeln!(out, "{prefix}proposal_bulla: {}", self.dao_merkle_root).unwrap();
         writeln!(out, "{prefix}note:").unwrap();
@@ -73,8 +77,6 @@ impl FunctionParams for dao_model::DaoProposeParamsInput {
     fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
         let dict = PyDict::new(py);
         dict.set_item("value_commit", format!("{:?}", self.value_commit))?;
-        dict.set_item("merkle_coin_root", self.merkle_coin_root.to_string())?;
-        dict.set_item("smt_null_root", self.smt_null_root.to_string())?;
         dict.set_item("signature_public", self.signature_public.to_string())?;
         Ok(dict.unbind())
     }
@@ -82,8 +84,6 @@ impl FunctionParams for dao_model::DaoProposeParamsInput {
     fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
         let prefix = format!("{}├─ ", "   ".repeat(depth));
         writeln!(out, "{prefix}value_commit: {:?}", self.value_commit).unwrap();
-        writeln!(out, "{prefix}merkle_coin_root: {}", self.merkle_coin_root).unwrap();
-        writeln!(out, "{prefix}smt_null_root: {:?}", self.smt_null_root).unwrap();
         writeln!(out, "{prefix}signature_public: {:?}", self.signature_public).unwrap();
         Ok(())
     }

+ 1 - 3
src/zk/gadget/smt.rs

@@ -754,9 +754,7 @@ mod tests {
             smt.store.get(&BigUint::from(1u32)).expect("left subtree root must be stored");
         let mut forged_path = [Fp::ZERO; SMT_FP_DEPTH];
         forged_path[0] = left_subtree_root;
-        for i in 1..SMT_FP_DEPTH {
-            forged_path[i] = EMPTY_NODES_FP[i + 1];
-        }
+        forged_path[1..SMT_FP_DEPTH].copy_from_slice(&EMPTY_NODES_FP[2..(SMT_FP_DEPTH + 1)]);
 
         // NON-VACUOUSNESS ANCHOR: outside the circuit, the forged witness really
         // does authenticate an EMPTY leaf against the genuine root. So *without*