LCOV - code coverage report
Current view: top level - src/wallet/test - wallet_shielded_balances_tests.cpp (source / functions) Hit Total Coverage
Test: total_coverage.info Lines: 173 174 99.4 %
Date: 2025-02-23 09:33:43 Functions: 15 15 100.0 %

          Line data    Source code
       1             : // Copyright (c) 2021 The PIVX Core developers
       2             : // Distributed under the MIT software license, see the accompanying
       3             : // file COPYING or https://www.opensource.org/licenses/mit-license.php.
       4             : 
       5             : #include "wallet/test/wallet_test_fixture.h"
       6             : 
       7             : #include "consensus/merkle.h"
       8             : #include "primitives/block.h"
       9             : #include "random.h"
      10             : #include "sapling/note.h"
      11             : #include "sapling/noteencryption.h"
      12             : #include "sapling/transaction_builder.h"
      13             : #include "test/librust/utiltest.h"
      14             : #include "wallet/wallet.h"
      15             : 
      16             : #include <boost/filesystem.hpp>
      17             : #include <boost/test/unit_test.hpp>
      18             : 
      19             : CAmount fee = COIN; // Hardcoded fee
      20             : 
      21             : BOOST_FIXTURE_TEST_SUITE(wallet_shielded_balances_tests, WalletTestingSetup)
      22             : 
      23           3 : void setupWallet(CWallet& wallet)
      24             : {
      25           3 :     wallet.SetMinVersion(FEATURE_SAPLING);
      26           3 :     wallet.SetupSPKM(false);
      27           3 : }
      28             : 
      29             : // Find and set notes data in the tx + add any missing ivk to the wallet's keystore.
      30           6 : CWalletTx& SetWalletNotesData(CWallet* wallet, CWalletTx& wtx)
      31             : {
      32           6 :     Optional<mapSaplingNoteData_t> saplingNoteData{nullopt};
      33           6 :     wallet->FindNotesDataAndAddMissingIVKToKeystore(*wtx.tx, saplingNoteData);
      34           6 :     assert(static_cast<bool>(saplingNoteData));
      35           6 :     wtx.SetSaplingNoteData(*saplingNoteData);
      36          12 :     BOOST_CHECK(wallet->AddToWallet(wtx));
      37             :     // Updated tx
      38          12 :     return wallet->mapWallet.at(wtx.GetHash());
      39             : }
      40             : 
      41             : /**
      42             :  * Creates and send a tx with an input of 'inputAmount' to 'vDest'.
      43             :  */
      44           3 : CWalletTx& AddShieldedBalanceToWallet(CAmount inputAmount,
      45             :                                       const std::vector<ShieldedDestination>& vDest,
      46             :                                       CWallet* wallet,
      47             :                                       const Consensus::Params& consensusParams)
      48             : {
      49             : 
      50             :     // Dummy wallet, used to generate the dummy transparent input key and sign it in the transaction builder
      51           6 :     CWallet dummyWallet("dummy", WalletDatabase::CreateDummy());
      52           3 :     dummyWallet.SetMinVersion(FEATURE_SAPLING);
      53           3 :     dummyWallet.SetupSPKM(false, true);
      54           6 :     LOCK(dummyWallet.cs_wallet);
      55             : 
      56             :     // Create a transaction shielding balance to 'vDest' and load it to the wallet.
      57           6 :     CWalletTx wtx = GetValidSaplingReceive(consensusParams, dummyWallet, inputAmount, vDest, wallet);
      58             : 
      59             :     // Updated tx after load it to the wallet
      60           3 :     CWalletTx& wtxUpdated = SetWalletNotesData(wallet, wtx);
      61             :     // Check tx credit now
      62           3 :     BOOST_CHECK_EQUAL(wtxUpdated.GetCredit(ISMINE_ALL), inputAmount);
      63           6 :     BOOST_CHECK(wtxUpdated.IsAmountCached(CWalletTx::CREDIT, ISMINE_SPENDABLE_SHIELDED));
      64           6 :     return wtxUpdated;
      65             : }
      66             : 
      67           2 : CWalletTx& AddShieldedBalanceToWallet(const libzcash::SaplingPaymentAddress& sendTo, CAmount amount,
      68             :                                       CWallet& wallet, const Consensus::Params& consensusParams,
      69             :                                       libzcash::SaplingExtendedSpendingKey& extskOut)
      70             : {
      71             :     // Create a transaction shielding balance to 'sendTo' and load it to the wallet.
      72           4 :     BOOST_CHECK(wallet.GetSaplingExtendedSpendingKey(sendTo, extskOut));
      73           2 :     std::vector<ShieldedDestination> vDest;
      74           2 :     vDest.push_back({extskOut, amount});
      75           4 :     return AddShieldedBalanceToWallet(amount, vDest, &wallet, consensusParams);
      76             : }
      77             : 
      78           3 : struct SaplingSpendValues {
      79             :     libzcash::SaplingNote note;
      80             :     const uint256 anchor;
      81             :     const SaplingWitness witness;
      82             : };
      83             : 
      84             : /**
      85             :  * Update the wallet internally as if the wallet would had received a valid block containing wtx.
      86             :  * Then return the note, anchor and witness for any subsequent spending process.
      87             :  */
      88           2 : SaplingSpendValues UpdateWalletInternalNotesData(CWalletTx& wtx, const SaplingOutPoint& sapPoint, CWallet& wallet)
      89             : {
      90             :     // Get note
      91           2 :     SaplingNoteData nd = wtx.mapSaplingNoteData.at(sapPoint);
      92           2 :     assert(nd.IsMyNote());
      93           2 :     const auto& ivk = *(nd.ivk);
      94           2 :     auto maybe_pt = libzcash::SaplingNotePlaintext::decrypt(
      95           2 :             wtx.tx->sapData->vShieldedOutput[sapPoint.n].encCiphertext,
      96             :             ivk,
      97           2 :             wtx.tx->sapData->vShieldedOutput[sapPoint.n].ephemeralKey,
      98           2 :             wtx.tx->sapData->vShieldedOutput[sapPoint.n].cmu);
      99           2 :     assert(static_cast<bool>(maybe_pt));
     100           4 :     Optional<libzcash::SaplingNotePlaintext> notePlainText = maybe_pt.get();
     101           6 :     libzcash::SaplingNote note = notePlainText->note(ivk).get();
     102             : 
     103             :     // Append note to the tree
     104           4 :     auto commitment = note.cmu().get();
     105           4 :     SaplingMerkleTree tree;
     106           2 :     tree.append(commitment);
     107           2 :     auto anchor = tree.root();
     108           4 :     auto witness = tree.witness();
     109             : 
     110             :     // Update wtx credit chain data
     111             :     // Pretend we mined the tx by adding a fake witness and nullifier to be able to spend it.
     112           4 :     wtx.mapSaplingNoteData[sapPoint].witnesses.push_front(tree.witness());
     113           2 :     wtx.mapSaplingNoteData[sapPoint].witnessHeight = 1;
     114           2 :     wallet.GetSaplingScriptPubKeyMan()->nWitnessCacheSize = 1;
     115           2 :     wallet.GetSaplingScriptPubKeyMan()->UpdateSaplingNullifierNoteMapWithTx(wtx);
     116           4 :     return {note, anchor, witness};
     117             : }
     118             : 
     119             : /**
     120             :  * Validates:
     121             :  * 1) CWalletTx getCredit for shielded credit.
     122             :  * Incoming spendable shielded balance must be cached in the cacheableAmounts.
     123             :  *
     124             :  * 2) CWalletTx getDebit & getCredit for shielded debit to transparent address.
     125             :  * Same wallet as point (1), spending half of the credit received in (1) to a transparent remote address.
     126             :  * The other half of the balance - minus fee - must appear as credit (shielded change).
     127             :  *
     128             :  */
     129           2 : BOOST_AUTO_TEST_CASE(GetShieldedSimpleCachedCreditAndDebit)
     130             : {
     131             : 
     132             :     ///////////////////////
     133             :     //////// Credit ////////
     134             :     ///////////////////////
     135             : 
     136           1 :     auto consensusParams = Params().GetConsensus();
     137             : 
     138             :     // Main wallet
     139           1 :     CWallet &wallet = m_wallet;
     140           3 :     LOCK2(cs_main, wallet.cs_wallet);
     141           1 :     setupWallet(wallet);
     142             : 
     143             :     // First generate a shielded address
     144           1 :     libzcash::SaplingPaymentAddress pa = wallet.GenerateNewSaplingZKey();
     145           1 :     CAmount firstCredit = COIN * 10;
     146             : 
     147             :     // Add shielded balance.
     148           1 :     libzcash::SaplingExtendedSpendingKey extskOut;
     149           1 :     CWalletTx& wtxUpdated = AddShieldedBalanceToWallet(pa, firstCredit, wallet, consensusParams, extskOut);
     150             : 
     151             :     ///////////////////////
     152             :     //////// Debit ////////
     153             :     ///////////////////////
     154             : 
     155             :     // Update transaction and wallet internal state to be able to spend it.
     156           1 :     SaplingOutPoint sapPoint {wtxUpdated.GetHash(), 0};
     157           2 :     SaplingSpendValues sapSpendValues = UpdateWalletInternalNotesData(wtxUpdated, sapPoint, wallet);
     158             : 
     159             :     // Debit value
     160           1 :     CAmount firstDebit = COIN * 5;
     161           1 :     CAmount firstDebitShieldedChange = firstDebit - fee;
     162             : 
     163             :     // Create the spending transaction
     164           2 :     auto builder = TransactionBuilder(consensusParams, &wallet);
     165           1 :     builder.SetFee(fee);
     166           1 :     builder.AddSaplingSpend(
     167             :             extskOut.expsk,
     168             :             sapSpendValues.note,
     169             :             sapSpendValues.anchor,
     170             :             sapSpendValues.witness);
     171             : 
     172             :     // Send to transparent address
     173           3 :     builder.AddTransparentOutput(CreateDummyDestinationScript(),
     174             :                                  firstDebit);
     175             : 
     176           2 :     CTransaction tx = builder.Build().GetTxOrThrow();
     177             :     // add tx to wallet and update it.
     178           1 :     wallet.AddToWallet({&wallet, MakeTransactionRef(tx)});
     179           1 :     CWalletTx& wtxDebit = wallet.mapWallet.at(tx.GetHash());
     180             :     // Update tx notes data (shielded change need it)
     181           1 :     CWalletTx& wtxDebitUpdated = SetWalletNotesData(&wallet, wtxDebit);
     182             : 
     183             :     // The debit need to be the entire first note value
     184           1 :     BOOST_CHECK_EQUAL(wtxDebitUpdated.GetDebit(ISMINE_ALL), firstCredit);
     185           2 :     BOOST_CHECK(wtxDebitUpdated.IsAmountCached(CWalletTx::DEBIT, ISMINE_SPENDABLE_SHIELDED));
     186             :     // The credit should be only the change.
     187           1 :     BOOST_CHECK_EQUAL(wtxDebitUpdated.GetCredit(ISMINE_ALL), firstDebitShieldedChange);
     188           2 :     BOOST_CHECK(wtxDebitUpdated.IsAmountCached(CWalletTx::CREDIT, ISMINE_SPENDABLE_SHIELDED));
     189             : 
     190             :     // Checks that the only shielded output of this tx is change.
     191           2 :     BOOST_CHECK(wallet.GetSaplingScriptPubKeyMan()->IsNoteSaplingChange(
     192             :             SaplingOutPoint(wtxDebitUpdated.GetHash(), 0), pa));
     193           1 : }
     194             : 
     195           2 : libzcash::SaplingPaymentAddress getNewDummyShieldedAddress()
     196             : {
     197           2 :     HDSeed seed;
     198           2 :     auto m = libzcash::SaplingExtendedSpendingKey::Master(seed);
     199           4 :     return m.DefaultAddress();
     200             : }
     201             : 
     202           2 : CWalletTx& buildTxAndLoadToWallet(CWallet& wallet, const libzcash::SaplingExtendedSpendingKey& extskOut,
     203             :                                   const SaplingSpendValues& sapSpendValues, libzcash::SaplingPaymentAddress dest,
     204             :                                   const CAmount& destAmount, const Consensus::Params& consensus)
     205             : {
     206             :     // Create the spending transaction
     207           4 :     auto builder = TransactionBuilder(consensus, &wallet);
     208           2 :     builder.SetFee(fee);
     209           2 :     builder.AddSaplingSpend(
     210           2 :             extskOut.expsk,
     211           2 :             sapSpendValues.note,
     212           2 :             sapSpendValues.anchor,
     213           2 :             sapSpendValues.witness);
     214             : 
     215             :     // Send to shielded address
     216           4 :     builder.AddSaplingOutput(
     217           2 :             extskOut.expsk.ovk,
     218             :             dest,
     219             :             destAmount,
     220             :             {}
     221             :     );
     222             : 
     223           4 :     CTransaction tx = builder.Build().GetTxOrThrow();
     224             :     // add tx to wallet and update it.
     225           2 :     wallet.AddToWallet({&wallet, MakeTransactionRef(tx)});
     226           2 :     CWalletTx& wtx = wallet.mapWallet.at(tx.GetHash());
     227             :     // Update tx notes data and return the updated wtx.
     228           4 :     return SetWalletNotesData(&wallet, wtx);
     229             : }
     230             : 
     231             : /**
     232             :  * Validates shielded to remote shielded + change cached balances.
     233             :  */
     234           2 : BOOST_AUTO_TEST_CASE(VerifyShieldedToRemoteShieldedCachedBalance)
     235             : {
     236           1 :     auto consensusParams = Params().GetConsensus();
     237             : 
     238             :     // Main wallet
     239           1 :     CWallet &wallet = m_wallet;
     240           3 :     LOCK2(cs_main, wallet.cs_wallet);
     241           1 :     setupWallet(wallet);
     242             : 
     243             :     // First generate a shielded address
     244           1 :     libzcash::SaplingPaymentAddress pa = wallet.GenerateNewSaplingZKey();
     245           1 :     CAmount firstCredit = COIN * 20;
     246             : 
     247             :     // Add shielded balance.
     248           1 :     libzcash::SaplingExtendedSpendingKey extskOut;
     249           1 :     CWalletTx& wtxUpdated = AddShieldedBalanceToWallet(pa, firstCredit, wallet, consensusParams, extskOut);
     250             : 
     251             :     // Update transaction and wallet internal state to be able to spend it.
     252           1 :     SaplingOutPoint sapPoint {wtxUpdated.GetHash(), 0};
     253           2 :     SaplingSpendValues sapSpendValues = UpdateWalletInternalNotesData(wtxUpdated, sapPoint, wallet);
     254             : 
     255             :     // Remote destination values
     256           1 :     libzcash::SaplingPaymentAddress destShieldedAddress = getNewDummyShieldedAddress();
     257           1 :     CAmount destAmount = COIN * 8;
     258             : 
     259             :     // Create the spending transaction and load it to the wallet
     260           1 :     CWalletTx& wtxDebitUpdated = buildTxAndLoadToWallet(wallet,
     261             :                            extskOut,
     262             :                            sapSpendValues,
     263             :                            destShieldedAddress,
     264             :                            destAmount,
     265           1 :                            consensusParams);
     266             : 
     267             :     // Validate results
     268           1 :     CAmount expectedShieldedChange = firstCredit - destAmount - fee;
     269             : 
     270             :     // The debit need to be the entire first note value
     271           1 :     BOOST_CHECK_EQUAL(wtxDebitUpdated.GetDebit(ISMINE_ALL), firstCredit);
     272           2 :     BOOST_CHECK(wtxDebitUpdated.IsAmountCached(CWalletTx::DEBIT, ISMINE_SPENDABLE_SHIELDED));
     273             :     // The credit should be only the change.
     274           1 :     BOOST_CHECK_EQUAL(wtxDebitUpdated.GetCredit(ISMINE_ALL), expectedShieldedChange);
     275           2 :     BOOST_CHECK(wtxDebitUpdated.IsAmountCached(CWalletTx::CREDIT, ISMINE_SPENDABLE_SHIELDED));
     276             :     // Plus, change should be same and be cached as well
     277           1 :     BOOST_CHECK_EQUAL(wtxDebitUpdated.GetShieldedChange(), expectedShieldedChange);
     278           2 :     BOOST_CHECK(wtxDebitUpdated.fShieldedChangeCached);
     279           1 : }
     280             : 
     281           0 : struct FakeBlock
     282             : {
     283             :     CBlock block;
     284             :     CBlockIndex* pindex;
     285             : };
     286             : 
     287           1 : FakeBlock SimpleFakeMine(CWalletTx& wtx, SaplingMerkleTree& currentTree, CWallet& wallet)
     288             : {
     289           1 :     FakeBlock fakeBlock;
     290           1 :     fakeBlock.block.nVersion = CBlock::CURRENT_VERSION;
     291           1 :     fakeBlock.block.vtx.emplace_back(wtx.tx);
     292           1 :     fakeBlock.block.hashMerkleRoot = BlockMerkleRoot(fakeBlock.block);
     293           3 :     for (const OutputDescription& out : wtx.tx->sapData->vShieldedOutput) {
     294           2 :         currentTree.append(out.cmu);
     295             :     }
     296           1 :     fakeBlock.block.hashFinalSaplingRoot = currentTree.root();
     297           1 :     fakeBlock.pindex = new CBlockIndex(fakeBlock.block);
     298           1 :     mapBlockIndex.insert(std::make_pair(fakeBlock.block.GetHash(), fakeBlock.pindex));
     299           2 :     fakeBlock.pindex->phashBlock = &mapBlockIndex.find(fakeBlock.block.GetHash())->first;
     300           1 :     chainActive.SetTip(fakeBlock.pindex);
     301           3 :     BOOST_CHECK(chainActive.Contains(fakeBlock.pindex));
     302           2 :     WITH_LOCK(wallet.cs_wallet, wallet.SetLastBlockProcessed(fakeBlock.pindex));
     303           1 :     wtx.m_confirm = CWalletTx::Confirmation(CWalletTx::Status::CONFIRMED, fakeBlock.pindex->nHeight, fakeBlock.pindex->GetBlockHash(), 0);
     304           1 :     return fakeBlock;
     305             : }
     306             : 
     307             : /**
     308             :  * Test:
     309             :  * 1) receive two shielded notes on the same tx.
     310             :  * 2) check available credit.
     311             :  * 3) spend one of them.
     312             :  * 4) force available credit cache recalculation and validate the updated amount.
     313             :  */
     314           2 : BOOST_AUTO_TEST_CASE(GetShieldedAvailableCredit)
     315             : {
     316           1 :     auto consensusParams = Params().GetConsensus();
     317             : 
     318             :     // Main wallet
     319           1 :     CWallet &wallet = m_wallet;
     320           3 :     LOCK2(cs_main, wallet.cs_wallet);
     321           1 :     setupWallet(wallet);
     322             : 
     323             :     // 1) generate a shielded address and send 20 PIV in two shielded outputs
     324           1 :     libzcash::SaplingPaymentAddress pa = wallet.GenerateNewSaplingZKey();
     325           1 :     CAmount credit = COIN * 20;
     326             : 
     327             :     // Add two equal shielded outputs.
     328           1 :     libzcash::SaplingExtendedSpendingKey extskOut;
     329           2 :     BOOST_CHECK(wallet.GetSaplingExtendedSpendingKey(pa, extskOut));
     330             : 
     331           2 :     std::vector<ShieldedDestination> vDest;
     332           1 :     vDest.push_back({extskOut, credit / 2});
     333           1 :     vDest.push_back({extskOut, credit / 2});
     334           1 :     CWalletTx& wtxUpdated = AddShieldedBalanceToWallet(credit, vDest, &wallet, consensusParams);
     335             : 
     336             :     // Available credit ISMINE_SPENDABLE must be 0
     337             :     // Available credit ISMINE_SHIELDED_SPENDABLE must be 'credit' and be cached.
     338           1 :     BOOST_CHECK_EQUAL(wtxUpdated.GetAvailableCredit(true, ISMINE_SPENDABLE), 0);
     339           1 :     BOOST_CHECK_EQUAL(wtxUpdated.GetShieldedAvailableCredit(), credit);
     340           2 :     BOOST_CHECK(wtxUpdated.IsAmountCached(CWalletTx::AVAILABLE_CREDIT, ISMINE_SPENDABLE_SHIELDED));
     341             : 
     342             :     // 2) Confirm the tx
     343           2 :     SaplingMerkleTree tree;
     344           2 :     FakeBlock fakeBlock = SimpleFakeMine(wtxUpdated, tree, wallet);
     345             :     // Simulate receiving a new block and updating the witnesses/nullifiers
     346           1 :     wallet.IncrementNoteWitnesses(fakeBlock.pindex, &fakeBlock.block, tree);
     347           1 :     wallet.GetSaplingScriptPubKeyMan()->UpdateSaplingNullifierNoteMapForBlock(&fakeBlock.block);
     348           1 :     wtxUpdated = wallet.mapWallet.at(wtxUpdated.GetHash());
     349             : 
     350             :     // 3) Now can spend one output and recalculate the shielded credit.
     351           2 :     std::vector<SaplingNoteEntry> saplingEntries;
     352           2 :     Optional<libzcash::SaplingPaymentAddress> opPa(pa);
     353           1 :     wallet.GetSaplingScriptPubKeyMan()->GetFilteredNotes(saplingEntries,
     354             :                                                          opPa,
     355             :                                                          0);
     356             : 
     357           2 :     std::vector<SaplingOutPoint> ops = {saplingEntries[0].op};
     358           1 :     uint256 anchor;
     359           2 :     std::vector<Optional<SaplingWitness>> witnesses;
     360           1 :     wallet.GetSaplingScriptPubKeyMan()->GetSaplingNoteWitnesses(ops, witnesses, anchor);
     361           2 :     SaplingSpendValues sapSpendValues{saplingEntries[0].note, anchor, *witnesses[0]};
     362             : 
     363             :     // Remote destination values
     364           1 :     libzcash::SaplingPaymentAddress destShieldedAddress = getNewDummyShieldedAddress();
     365           1 :     CAmount change = COIN * 1;
     366           1 :     CAmount destAmount = credit / 2 - fee - change; // one note - fee
     367             : 
     368             :     // Create the spending transaction and load it to the wallet
     369           1 :     CWalletTx& wtxDebitUpdated = buildTxAndLoadToWallet(wallet,
     370             :                                                         extskOut,
     371             :                                                         sapSpendValues,
     372             :                                                         destShieldedAddress,
     373             :                                                         destAmount,
     374           1 :                                                         consensusParams);
     375             : 
     376             :     // Check previous credit tx balance being the same and then force a recalculation
     377           1 :     BOOST_CHECK_EQUAL(wtxUpdated.GetShieldedAvailableCredit(), credit);
     378           1 :     BOOST_CHECK_EQUAL(wtxUpdated.GetShieldedAvailableCredit(false), credit / 2);
     379           1 :     BOOST_CHECK_EQUAL(wtxUpdated.GetShieldedChange(), 0);
     380             : 
     381             :     // Now check the debit tx
     382           1 :     BOOST_CHECK_EQUAL(wtxDebitUpdated.GetDebit(ISMINE_SPENDABLE_SHIELDED), credit / 2);
     383           1 :     BOOST_CHECK_EQUAL(wtxDebitUpdated.GetShieldedChange(), change);
     384           1 :     BOOST_CHECK_EQUAL(wtxDebitUpdated.GetCredit(ISMINE_SPENDABLE_SHIELDED), change);
     385           1 : }
     386             : 
     387             : BOOST_AUTO_TEST_SUITE_END()

Generated by: LCOV version 1.14