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()
|