Procházet zdrojové kódy

src/contract/money: delayed_tx test added

skoupidi před 2 roky
rodič
revize
cd4655bb62

+ 8 - 2
src/contract/money/Makefile

@@ -68,7 +68,13 @@ test-token-mint: all
 		--features=no-entrypoint,client \
 		--test token_mint
 
-test: test-integration test-mint-pay-swap test-genesis-mint test-token-mint
+test-delayed-tx: all
+	RUSTFLAGS="$(RUSTFLAGS)" $(CARGO) test --target=$(RUST_TARGET) \
+		--release --package $(PKGNAME) \
+		--features=no-entrypoint,client \
+		--test delayed_tx
+
+test: test-integration test-mint-pay-swap test-genesis-mint test-token-mint test-delayed-tx
 
 clippy: all
 	RUSTFLAGS="$(RUSTFLAGS)" $(CARGO) clippy --target=$(WASM_TARGET) \
@@ -84,4 +90,4 @@ clean:
 		--release --package $(PKGNAME)
 	rm -f $(PROOFS_BIN) $(WASM_BIN)
 
-.PHONY: all test-integration test-mint-pay-swap test-genesis-mint test clippy clean
+.PHONY: all test-integration test-mint-pay-swap test-genesis-mint test-delayed-tx test clippy clean

+ 286 - 0
src/contract/money/tests/delayed_tx.rs

@@ -0,0 +1,286 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use darkfi::{
+    tx::{ContractCallLeaf, TransactionBuilder},
+    zk::halo2::Field,
+    Result,
+};
+use darkfi_contract_test_harness::{init_logger, Holder, TestHarness};
+use darkfi_money_contract::{
+    client::{
+        compute_remainder_blind,
+        fee_v1::{create_fee_proof, FeeCallInput, FeeCallOutput, FEE_CALL_GAS},
+        transfer_v1::make_transfer_call,
+        MoneyNote, OwnCoin,
+    },
+    model::{Input, MoneyFeeParamsV1, Output},
+    MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_FEE_NS_V1,
+    MONEY_CONTRACT_ZKAS_MINT_NS_V1,
+};
+use darkfi_sdk::{
+    blockchain::expected_reward,
+    crypto::{
+        contract_id::MONEY_CONTRACT_ID, note::AeadEncryptedNote, BaseBlind, FuncId, MerkleNode,
+        ScalarBlind, SecretKey,
+    },
+    pasta::pallas,
+    ContractCall,
+};
+use darkfi_serial::AsyncEncodable;
+use rand::rngs::OsRng;
+
+#[test]
+#[ignore]
+fn delayed_tx() -> Result<()> {
+    smol::block_on(async {
+        init_logger();
+
+        // Holders this test will use
+        const HOLDERS: [Holder; 3] = [Holder::Alice, Holder::Bob, Holder::Charlie];
+
+        // Initialize harness
+        let mut th = TestHarness::new(&HOLDERS, true).await?;
+
+        // Generate one new block mined by Alice
+        th.generate_block(&Holder::Alice, &HOLDERS).await?;
+
+        // Generate two new blocks mined by Bob
+        th.generate_block(&Holder::Bob, &HOLDERS).await?;
+        th.generate_block(&Holder::Bob, &HOLDERS).await?;
+
+        // Assert correct rewards
+        let alice_coins = &th.holders.get(&Holder::Alice).unwrap().unspent_money_coins;
+        let bob_coins = th.holders.get(&Holder::Bob).unwrap().unspent_money_coins.clone();
+        assert!(alice_coins.len() == 1);
+        assert!(bob_coins.len() == 2);
+        assert!(alice_coins[0].note.value == expected_reward(1));
+        assert!(bob_coins[0].note.value == expected_reward(2));
+        assert!(bob_coins[1].note.value == expected_reward(3));
+
+        let current_block_height = 4;
+
+        // Manually create an Alice to Charlie transfer call,
+        // where the output is used to pay the fee
+        let wallet = th.holders.get(&Holder::Alice).unwrap();
+        let rcpt = th.holders.get(&Holder::Charlie).unwrap().keypair.public;
+        let mut money_merkle_tree = wallet.money_merkle_tree.clone();
+
+        let (mint_pk, mint_zkbin) = th.proving_keys.get(MONEY_CONTRACT_ZKAS_MINT_NS_V1).unwrap();
+        let (burn_pk, burn_zkbin) = th.proving_keys.get(MONEY_CONTRACT_ZKAS_BURN_NS_V1).unwrap();
+
+        // Create the transfer call
+        let (alice_xfer_params, secrets, _) = make_transfer_call(
+            wallet.keypair,
+            rcpt,
+            alice_coins[0].note.value / 2,
+            alice_coins[0].note.token_id,
+            alice_coins.to_owned(),
+            money_merkle_tree.clone(),
+            mint_zkbin.clone(),
+            mint_pk.clone(),
+            burn_zkbin.clone(),
+            burn_pk.clone(),
+        )?;
+
+        let mut output_coins = vec![];
+        for output in &alice_xfer_params.outputs {
+            money_merkle_tree.append(MerkleNode::from(output.coin.inner()));
+
+            // Attempt to decrypt the output note to see if this is a coin for the holder.
+            let Ok(note) = output.note.decrypt::<MoneyNote>(&wallet.keypair.secret) else {
+                continue
+            };
+
+            let owncoin = OwnCoin {
+                coin: output.coin,
+                note: note.clone(),
+                secret: wallet.keypair.secret,
+                leaf_position: money_merkle_tree.mark().unwrap(),
+            };
+
+            output_coins.push(owncoin);
+        }
+
+        // Encode the call
+        let mut data = vec![MoneyFunction::TransferV1 as u8];
+        alice_xfer_params.encode_async(&mut data).await?;
+        let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
+
+        // Create the TransactionBuilder containing the `Transfer` call
+        let mut tx_builder =
+            TransactionBuilder::new(ContractCallLeaf { call, proofs: secrets.proofs }, vec![])?;
+
+        let mut tx = tx_builder.build()?;
+        let sigs = tx.create_sigs(&secrets.signature_secrets)?;
+        tx.signatures = vec![sigs];
+
+        // First we verify the fee-less transaction to see how much gas it uses for execution
+        // and verification.
+        let mut gas_used = FEE_CALL_GAS;
+        gas_used += wallet
+            .validator
+            .add_test_transactions(&[tx], current_block_height, false, false)
+            .await?;
+
+        let coin = &output_coins[0];
+        let change_value = coin.note.value - gas_used;
+
+        // Input and output setup
+        let input = FeeCallInput {
+            coin: coin.clone(),
+            merkle_path: money_merkle_tree.witness(coin.leaf_position, 0).unwrap(),
+            user_data_blind: BaseBlind::random(&mut OsRng),
+        };
+
+        let output = FeeCallOutput {
+            public_key: wallet.keypair.public,
+            value: change_value,
+            token_id: coin.note.token_id,
+            blind: BaseBlind::random(&mut OsRng),
+            spend_hook: FuncId::none(),
+            user_data: pallas::Base::ZERO,
+        };
+
+        // Create blinding factors
+        let token_blind = BaseBlind::random(&mut OsRng);
+        let input_value_blind = ScalarBlind::random(&mut OsRng);
+        let fee_value_blind = ScalarBlind::random(&mut OsRng);
+        let output_value_blind = compute_remainder_blind(&[input_value_blind], &[fee_value_blind]);
+
+        // Create an ephemeral signing key
+        let signature_secret = SecretKey::random(&mut OsRng);
+
+        let (fee_pk, fee_zkbin) = th.proving_keys.get(MONEY_CONTRACT_ZKAS_FEE_NS_V1).unwrap();
+
+        let (proof, public_inputs) = create_fee_proof(
+            fee_zkbin,
+            fee_pk,
+            &input,
+            input_value_blind,
+            &output,
+            output_value_blind,
+            output.spend_hook,
+            output.user_data,
+            output.blind,
+            token_blind,
+            signature_secret,
+        )?;
+
+        // Encrypted note for the output
+        let note = MoneyNote {
+            coin_blind: output.blind,
+            value: output.value,
+            token_id: output.token_id,
+            spend_hook: output.spend_hook,
+            user_data: output.user_data,
+            value_blind: output_value_blind,
+            token_blind,
+            memo: vec![],
+        };
+
+        let encrypted_note = AeadEncryptedNote::encrypt(&note, &output.public_key, &mut OsRng)?;
+
+        let fee_call_params = MoneyFeeParamsV1 {
+            input: Input {
+                value_commit: public_inputs.input_value_commit,
+                token_commit: public_inputs.token_commit,
+                nullifier: public_inputs.nullifier,
+                merkle_root: public_inputs.merkle_root,
+                user_data_enc: public_inputs.input_user_data_enc,
+                signature_public: public_inputs.signature_public,
+            },
+            output: Output {
+                value_commit: public_inputs.output_value_commit,
+                token_commit: public_inputs.token_commit,
+                coin: public_inputs.output_coin,
+                note: encrypted_note,
+            },
+            fee_value_blind,
+            token_blind,
+        };
+
+        // Encode the contract call
+        let mut data = vec![MoneyFunction::FeeV1 as u8];
+        gas_used.encode_async(&mut data).await?;
+        fee_call_params.encode_async(&mut data).await?;
+        let fee_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
+
+        // Append the fee call to the transaction
+        tx_builder.append(ContractCallLeaf { call: fee_call, proofs: vec![proof] }, vec![])?;
+        let alice_fee_params = Some(fee_call_params);
+
+        // Now build the actual transaction and sign it with all necessary keys.
+        let mut alice_tx = tx_builder.build()?;
+        let sigs = alice_tx.create_sigs(&secrets.signature_secrets)?;
+        alice_tx.signatures = vec![sigs];
+        let sigs = alice_tx.create_sigs(&[signature_secret])?;
+        alice_tx.signatures.push(sigs);
+
+        // Bob transfers some tokens to Charlie
+        let (bob_tx, (bob_xfer_params, bob_fee_params), _spent_soins) = th
+            .transfer(
+                bob_coins[0].note.value,
+                &Holder::Bob,
+                &Holder::Charlie,
+                &[bob_coins[0].clone()],
+                bob_coins[0].note.token_id,
+                current_block_height,
+            )
+            .await?;
+
+        // Bob->Charlie transaction gets in first
+        for holder in &HOLDERS {
+            th.execute_transfer_tx(
+                holder,
+                bob_tx.clone(),
+                &bob_xfer_params,
+                &bob_fee_params,
+                current_block_height,
+                true,
+            )
+            .await?;
+        }
+
+        // Execute the Alice->Charlie transaction
+        for holder in &HOLDERS {
+            th.execute_transfer_tx(
+                holder,
+                alice_tx.clone(),
+                &alice_xfer_params,
+                &alice_fee_params,
+                current_block_height,
+                true,
+            )
+            .await?;
+        }
+
+        // Assert coins in wallets
+        let alice_coins = &th.holders.get(&Holder::Alice).unwrap().unspent_money_coins;
+        let bob_coins = &th.holders.get(&Holder::Bob).unwrap().unspent_money_coins;
+        let charlie_coins = &th.holders.get(&Holder::Charlie).unwrap().unspent_money_coins;
+        assert!(alice_coins.len() == 1);
+        assert!(bob_coins.len() == 1);
+        assert!(charlie_coins.len() == 2);
+        assert!(charlie_coins[0].note.value == expected_reward(2));
+        assert!(charlie_coins[1].note.value == expected_reward(1) / 2);
+
+        // Thanks for reading
+        Ok(())
+    })
+}

+ 66 - 35
src/contract/test-harness/src/money_transfer.rs

@@ -22,7 +22,7 @@ use darkfi::{
 };
 use darkfi_money_contract::{
     client::{transfer_v1::make_transfer_call, MoneyNote, OwnCoin},
-    model::{Input, MoneyFeeParamsV1, MoneyTransferParamsV1, Output, TokenId},
+    model::{MoneyFeeParamsV1, MoneyTransferParamsV1, TokenId},
     MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
 };
 use darkfi_sdk::{
@@ -128,17 +128,14 @@ impl TestHarness {
         // Execute the transaction
         wallet.add_transaction("money::transfer", tx, block_height).await?;
 
-        // Iterate over all inputs to mark any spent coins
-        let mut inputs: Vec<Input> = call_params.inputs.to_vec();
-        if let Some(ref fee_params) = fee_params {
-            inputs.push(fee_params.input.clone());
-        }
-
-        let nullifiers = inputs.iter().map(|i| i.nullifier.inner()).map(|l| (l, l)).collect();
+        // Iterate over call inputs to mark any spent coins
+        let nullifiers =
+            call_params.inputs.iter().map(|i| i.nullifier.inner()).map(|l| (l, l)).collect();
         wallet.money_null_smt.insert_batch(nullifiers).expect("smt.insert_batch()");
 
+        let mut found_owncoins = vec![];
         if append {
-            for input in &inputs {
+            for input in &call_params.inputs {
                 if let Some(spent_coin) = wallet
                     .unspent_money_coins
                     .iter()
@@ -150,37 +147,71 @@ impl TestHarness {
                     wallet.spent_money_coins.push(spent_coin.clone());
                 }
             }
-        }
 
-        // Iterate over all outputs to find any new OwnCoins
-        let mut found_owncoins = vec![];
-        let mut outputs: Vec<Output> = call_params.outputs.to_vec();
-        if let Some(ref fee_params) = fee_params {
-            outputs.push(fee_params.output.clone());
-        }
-
-        for output in &outputs {
-            if !append {
-                continue
+            // Iterate over call outputs to find any new OwnCoins
+            for output in &call_params.outputs {
+                wallet.money_merkle_tree.append(MerkleNode::from(output.coin.inner()));
+
+                // Attempt to decrypt the output note to see if this is a coin for the holder.
+                let Ok(note) = output.note.decrypt::<MoneyNote>(&wallet.keypair.secret) else {
+                    continue
+                };
+
+                let owncoin = OwnCoin {
+                    coin: output.coin,
+                    note: note.clone(),
+                    secret: wallet.keypair.secret,
+                    leaf_position: wallet.money_merkle_tree.mark().unwrap(),
+                };
+
+                debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
+                wallet.unspent_money_coins.push(owncoin.clone());
+                found_owncoins.push(owncoin);
             }
+        }
 
-            wallet.money_merkle_tree.append(MerkleNode::from(output.coin.inner()));
-
-            // Attempt to decrypt the output note to see if this is a coin for the holder.
-            let Ok(note) = output.note.decrypt::<MoneyNote>(&wallet.keypair.secret) else {
-                continue
-            };
+        // Handle fee call
+        if let Some(ref fee_params) = fee_params {
+            // Process call input to mark any spent coins
+            let nullifier = fee_params.input.nullifier.inner();
+            wallet
+                .money_null_smt
+                .insert_batch(vec![(nullifier, nullifier)])
+                .expect("smt.insert_batch()");
+
+            if append {
+                if let Some(spent_coin) = wallet
+                    .unspent_money_coins
+                    .iter()
+                    .find(|x| x.nullifier() == fee_params.input.nullifier)
+                    .cloned()
+                {
+                    debug!("Found spent OwnCoin({}) for {:?}", spent_coin.coin, holder);
+                    wallet
+                        .unspent_money_coins
+                        .retain(|x| x.nullifier() != fee_params.input.nullifier);
+                    wallet.spent_money_coins.push(spent_coin.clone());
+                }
 
-            let owncoin = OwnCoin {
-                coin: output.coin,
-                note: note.clone(),
-                secret: wallet.keypair.secret,
-                leaf_position: wallet.money_merkle_tree.mark().unwrap(),
-            };
+                // Process call output to find any new OwnCoins
+                wallet.money_merkle_tree.append(MerkleNode::from(fee_params.output.coin.inner()));
 
-            debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
-            wallet.unspent_money_coins.push(owncoin.clone());
-            found_owncoins.push(owncoin);
+                // Attempt to decrypt the output note to see if this is a coin for the holder.
+                if let Ok(note) =
+                    fee_params.output.note.decrypt::<MoneyNote>(&wallet.keypair.secret)
+                {
+                    let owncoin = OwnCoin {
+                        coin: fee_params.output.coin,
+                        note: note.clone(),
+                        secret: wallet.keypair.secret,
+                        leaf_position: wallet.money_merkle_tree.mark().unwrap(),
+                    };
+
+                    debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
+                    wallet.unspent_money_coins.push(owncoin.clone());
+                    found_owncoins.push(owncoin);
+                };
+            }
         }
 
         Ok(found_owncoins)