// Copyright (c) 2016-2020 The Hush developers // Distributed under the GPLv3 software license, see the accompanying // file COPYING or https://www.gnu.org/licenses/gpl-3.0.en.html #include #include #include #include "base58.h" #include "chainparams.h" #include "key_io.h" #include "main.h" #include "primitives/block.h" #include "random.h" #include "transaction_builder.h" #include "utiltest.h" #include "wallet/wallet.h" #include "zcash/JoinSplit.hpp" #include "zcash/Note.hpp" #include "zcash/NoteEncryption.hpp" #include using ::testing::Return; extern ZCJoinSplit* params; ACTION(ThrowLogicError) { throw std::logic_error("Boom"); } static const std::string tSecretRegtest = "cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN"; class MockWalletDB { public: MOCK_METHOD0(TxnBegin, bool()); MOCK_METHOD0(TxnCommit, bool()); MOCK_METHOD0(TxnAbort, bool()); MOCK_METHOD2(WriteTx, bool(uint256 hash, const CWalletTx& wtx)); MOCK_METHOD1(WriteWitnessCacheSize, bool(int64_t nWitnessCacheSize)); MOCK_METHOD1(WriteBestBlock, bool(const CBlockLocator& loc)); }; template void CWallet::SetBestChainINTERNAL( MockWalletDB& walletdb, const CBlockLocator& loc); class TestWallet : public CWallet { public: TestWallet() : CWallet() { } bool EncryptKeys(CKeyingMaterial& vMasterKeyIn) { return CCryptoKeyStore::EncryptKeys(vMasterKeyIn); } bool Unlock(const CKeyingMaterial& vMasterKeyIn) { return CCryptoKeyStore::Unlock(vMasterKeyIn); } void BuildWitnessCache(const CBlockIndex* pindex, bool witnessOnly) { CWallet::BuildWitnessCache(pindex, witnessOnly); } void DecrementNoteWitnesses(const CBlockIndex* pindex) { CWallet::DecrementNoteWitnesses(pindex); } void SetBestChain(MockWalletDB& walletdb, const CBlockLocator& loc) { CWallet::SetBestChainINTERNAL(walletdb, loc); } bool UpdatedNoteData(const CWalletTx& wtxIn, CWalletTx& wtx) { return CWallet::UpdatedNoteData(wtxIn, wtx); } void MarkAffectedTransactionsDirty(const CTransaction& tx) { CWallet::MarkAffectedTransactionsDirty(tx); } }; CWalletTx GetValidReceive(const libzcash::SproutSpendingKey& sk, CAmount value, bool randomInputs, int32_t version = 2) { return GetValidReceive(*params, sk, value, randomInputs, version); } CWalletTx GetValidSpend(const libzcash::SproutSpendingKey& sk, const libzcash::SproutNote& note, CAmount value) { return GetValidSpend(*params, sk, note, value); } std::vector SetSaplingNoteData(CWalletTx& wtx) { mapSaplingNoteData_t saplingNoteData; SaplingOutPoint saplingOutPoint = {wtx.GetHash(), 0}; SaplingNoteData saplingNd; saplingNoteData[saplingOutPoint] = saplingNd; wtx.SetSaplingNoteData(saplingNoteData); std::vector saplingNotes {saplingOutPoint}; return saplingNotes; } std::pair CreateValidBlock(TestWallet& wallet, const libzcash::SproutSpendingKey& sk, const CBlockIndex& index, CBlock& block, SproutMerkleTree& sproutTree, SaplingMerkleTree& saplingTree) { auto wtx = GetValidReceive(sk, 50, true, 4); auto note = GetNote(sk, wtx, 0, 1); auto nullifier = note.nullifier(sk); mapSproutNoteData_t noteData; JSOutPoint jsoutpt {wtx.GetHash(), 0, 1}; SproutNoteData nd {sk.address(), nullifier}; noteData[jsoutpt] = nd; wtx.SetSproutNoteData(noteData); auto saplingNotes = SetSaplingNoteData(wtx); wallet.AddToWallet(wtx, true, NULL); block.vtx.push_back(wtx); wallet.BuildWitnessCache(&index, false); return std::make_pair(jsoutpt, saplingNotes[0]); } std::pair GetWitnessesAndAnchors(TestWallet& wallet, std::vector& sproutNotes, std::vector& saplingNotes, std::vector>& sproutWitnesses, std::vector>& saplingWitnesses) { sproutWitnesses.clear(); saplingWitnesses.clear(); uint256 sproutAnchor; uint256 saplingAnchor; wallet.GetSproutNoteWitnesses(sproutNotes, sproutWitnesses, sproutAnchor); wallet.GetSaplingNoteWitnesses(saplingNotes, saplingWitnesses, saplingAnchor); return std::make_pair(sproutAnchor, saplingAnchor); } TEST(WalletTests, SetupDatadirLocationRunAsFirstTest) { // Get temporary and unique path for file. boost::filesystem::path pathTemp = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); boost::filesystem::create_directories(pathTemp); mapArgs["-datadir"] = pathTemp.string(); } /* TEST(WalletTests, FindMySaplingNotes) { SelectParams(CBaseChainParams::REGTEST); UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); auto consensusParams = Params().GetConsensus(); TestWallet wallet; // Generate dummy Sapling address std::vector> rawSeed(32); HDSeed seed(rawSeed); auto sk = libzcash::SaplingExtendedSpendingKey::Master(seed); auto expsk = sk.expsk; auto fvk = expsk.full_viewing_key(); auto pk = sk.DefaultAddress(); // Generate dummy Sapling note libzcash::SaplingNote note(pk, 50000); auto cm = note.cm().get(); SaplingMerkleTree tree; tree.append(cm); auto anchor = tree.root(); auto witness = tree.witness(); // Generate transaction auto builder = TransactionBuilder(consensusParams, 1); ASSERT_TRUE(builder.AddSaplingSpend(expsk, note, anchor, witness)); builder.AddSaplingOutput(fvk.ovk, pk, 25000, {}); auto maybe_tx = builder.Build(); ASSERT_EQ(static_cast(maybe_tx), true); auto tx = maybe_tx.get(); // No Sapling notes can be found in tx which does not belong to the wallet CWalletTx wtx {&wallet, tx}; ASSERT_FALSE(wallet.HaveSaplingSpendingKey(fvk)); auto noteMap = wallet.FindMySaplingNotes(wtx).first; EXPECT_EQ(0, noteMap.size()); // Add spending key to wallet, so Sapling notes can be found ASSERT_TRUE(wallet.AddSaplingZKey(sk, pk)); ASSERT_TRUE(wallet.HaveSaplingSpendingKey(fvk)); noteMap = wallet.FindMySaplingNotes(wtx).first; EXPECT_EQ(2, noteMap.size()); // Revert to default UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); } TEST(WalletTests, FindMySaplingNotesWithIvkOnly) { SelectParams(CBaseChainParams::REGTEST); UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); auto consensusParams = Params().GetConsensus(); TestWallet wallet; // Generate dummy Sapling address std::vector> rawSeed(32); HDSeed seed(rawSeed); auto sk = libzcash::SaplingExtendedSpendingKey::Master(seed); auto addr = sk.DefaultAddress(); auto expsk = sk.expsk; auto fvk = expsk.full_viewing_key(); auto pk = sk.DefaultAddress(); auto ivk = fvk.in_viewing_key(); // Generate dummy Sapling note libzcash::SaplingNote note(pk, 50000); auto cm = note.cm().get(); SaplingMerkleTree tree; tree.append(cm); auto anchor = tree.root(); auto witness = tree.witness(); // Generate transaction auto builder = TransactionBuilder(consensusParams, 1); ASSERT_TRUE(builder.AddSaplingSpend(expsk, note, anchor, witness)); builder.AddSaplingOutput(fvk.ovk, pk, 25000, {}); auto maybe_tx = builder.Build(); ASSERT_EQ(static_cast(maybe_tx), true); auto tx = maybe_tx.get(); // No Sapling notes can be found in tx which does not belong to the wallet CWalletTx wtx {&wallet, tx}; ASSERT_FALSE(wallet.HaveSaplingSpendingKey(fvk)); ASSERT_FALSE(wallet.HaveSaplingIncomingViewingKey(addr)); auto noteMap = wallet.FindMySaplingNotes(wtx).first; EXPECT_EQ(0, noteMap.size()); // Add ivk to wallet, so Sapling notes can be found ASSERT_TRUE(wallet.AddSaplingIncomingViewingKey(ivk, addr)); ASSERT_FALSE(wallet.HaveSaplingSpendingKey(fvk)); ASSERT_TRUE(wallet.HaveSaplingIncomingViewingKey(addr)); noteMap = wallet.FindMySaplingNotes(wtx).first; EXPECT_EQ(2, noteMap.size()); // Revert to default UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); } // Generate note A and spend to create note B, from which we spend to create two conflicting transactions TEST(WalletTests, GetConflictedSaplingNotes) { SelectParams(CBaseChainParams::REGTEST); UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); auto consensusParams = Params().GetConsensus(); TestWallet wallet; // Generate Sapling address std::vector> rawSeed(32); HDSeed seed(rawSeed); auto sk = libzcash::SaplingExtendedSpendingKey::Master(seed); auto expsk = sk.expsk; auto fvk = expsk.full_viewing_key(); auto ivk = fvk.in_viewing_key(); auto pk = sk.DefaultAddress(); ASSERT_TRUE(wallet.AddSaplingZKey(sk, pk)); ASSERT_TRUE(wallet.HaveSaplingSpendingKey(fvk)); // Generate note A libzcash::SaplingNote note(pk, 50000); auto cm = note.cm().get(); SaplingMerkleTree saplingTree; saplingTree.append(cm); auto anchor = saplingTree.root(); auto witness = saplingTree.witness(); // Generate tx to create output note B auto builder = TransactionBuilder(consensusParams, 1); ASSERT_TRUE(builder.AddSaplingSpend(expsk, note, anchor, witness)); builder.AddSaplingOutput(fvk.ovk, pk, 35000, {}); auto maybe_tx = builder.Build(); ASSERT_EQ(static_cast(maybe_tx), true); auto tx = maybe_tx.get(); CWalletTx wtx {&wallet, tx}; // Fake-mine the transaction EXPECT_EQ(-1, chainActive.Height()); SproutMerkleTree sproutTree; CBlock block; block.vtx.push_back(wtx); block.hashMerkleRoot = block.BuildMerkleTree(); auto blockHash = block.GetHash(); CBlockIndex fakeIndex {block}; mapBlockIndex.insert(std::make_pair(blockHash, &fakeIndex)); chainActive.SetTip(&fakeIndex); EXPECT_TRUE(chainActive.Contains(&fakeIndex)); EXPECT_EQ(0, chainActive.Height()); // Simulate SyncTransaction which calls AddToWalletIfInvolvingMe auto saplingNoteData = wallet.FindMySaplingNotes(wtx).first; ASSERT_TRUE(saplingNoteData.size() > 0); wtx.SetSaplingNoteData(saplingNoteData); wtx.SetMerkleBranch(block); wallet.AddToWallet(wtx, true, NULL); // Simulate receiving new block and ChainTip signal wallet.BuildWitnessCache(&fakeIndex, false); wallet.UpdateNullifierNoteMapForBlock(&block); // Retrieve the updated wtx from wallet uint256 hash = wtx.GetHash(); wtx = wallet.mapWallet[hash]; // Decrypt output note B auto maybe_pt = libzcash::SaplingNotePlaintext::decrypt( wtx.vShieldedOutput[0].encCiphertext, ivk, wtx.vShieldedOutput[0].ephemeralKey, wtx.vShieldedOutput[0].cm); ASSERT_EQ(static_cast(maybe_pt), true); auto maybe_note = maybe_pt.get().note(ivk); ASSERT_EQ(static_cast(maybe_note), true); auto note2 = maybe_note.get(); SaplingOutPoint sop0(wtx.GetHash(), 0); auto spend_note_witness = wtx.mapSaplingNoteData[sop0].witnesses.front(); auto maybe_nf = note2.nullifier(fvk, spend_note_witness.position()); ASSERT_EQ(static_cast(maybe_nf), true); auto nullifier2 = maybe_nf.get(); anchor = saplingTree.root(); // Create transaction to spend note B auto builder2 = TransactionBuilder(consensusParams, 2); ASSERT_TRUE(builder2.AddSaplingSpend(expsk, note2, anchor, spend_note_witness)); builder2.AddSaplingOutput(fvk.ovk, pk, 20000, {}); auto maybe_tx2 = builder2.Build(); ASSERT_EQ(static_cast(maybe_tx2), true); auto tx2 = maybe_tx2.get(); // Create conflicting transaction which also spends note B auto builder3 = TransactionBuilder(consensusParams, 2); ASSERT_TRUE(builder3.AddSaplingSpend(expsk, note2, anchor, spend_note_witness)); builder3.AddSaplingOutput(fvk.ovk, pk, 19999, {}); auto maybe_tx3 = builder3.Build(); ASSERT_EQ(static_cast(maybe_tx3), true); auto tx3 = maybe_tx3.get(); CWalletTx wtx2 {&wallet, tx2}; CWalletTx wtx3 {&wallet, tx3}; auto hash2 = wtx2.GetHash(); auto hash3 = wtx3.GetHash(); // No conflicts for no spends (wtx is currently the only transaction in the wallet) EXPECT_EQ(0, wallet.GetConflicts(hash2).size()); EXPECT_EQ(0, wallet.GetConflicts(hash3).size()); // No conflicts for one spend wallet.AddToWallet(wtx2, true, NULL); EXPECT_EQ(0, wallet.GetConflicts(hash2).size()); // Conflicts for two spends wallet.AddToWallet(wtx3, true, NULL); auto c3 = wallet.GetConflicts(hash2); EXPECT_EQ(2, c3.size()); EXPECT_EQ(std::set({hash2, hash3}), c3); // Tear down chainActive.SetTip(NULL); mapBlockIndex.erase(blockHash); // Revert to default UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); } TEST(WalletTests, SproutNullifierIsSpent) { CWallet wallet; auto sk = libzcash::SproutSpendingKey::random(); wallet.AddSproutSpendingKey(sk); auto wtx = GetValidReceive(sk, 10, true); auto note = GetNote(sk, wtx, 0, 1); auto nullifier = note.nullifier(sk); EXPECT_FALSE(wallet.IsSproutSpent(nullifier)); wallet.AddToWallet(wtx, true, NULL); EXPECT_FALSE(wallet.IsSproutSpent(nullifier)); auto wtx2 = GetValidSpend(sk, note, 5); wallet.AddToWallet(wtx2, true, NULL); EXPECT_FALSE(wallet.IsSproutSpent(nullifier)); // Fake-mine the transaction EXPECT_EQ(-1, chainActive.Height()); CBlock block; block.vtx.push_back(wtx2); block.hashMerkleRoot = block.BuildMerkleTree(); auto blockHash = block.GetHash(); CBlockIndex fakeIndex {block}; mapBlockIndex.insert(std::make_pair(blockHash, &fakeIndex)); chainActive.SetTip(&fakeIndex); EXPECT_TRUE(chainActive.Contains(&fakeIndex)); EXPECT_EQ(0, chainActive.Height()); wtx2.SetMerkleBranch(block); wallet.AddToWallet(wtx2, true, NULL); EXPECT_TRUE(wallet.IsSproutSpent(nullifier)); // Tear down chainActive.SetTip(NULL); mapBlockIndex.erase(blockHash); } TEST(WalletTests, SaplingNullifierIsSpent) { SelectParams(CBaseChainParams::REGTEST); UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); auto consensusParams = Params().GetConsensus(); TestWallet wallet; // Generate dummy Sapling address std::vector> rawSeed(32); HDSeed seed(rawSeed); auto sk = libzcash::SaplingExtendedSpendingKey::Master(seed); auto expsk = sk.expsk; auto fvk = expsk.full_viewing_key(); auto pk = sk.DefaultAddress(); // Generate dummy Sapling note libzcash::SaplingNote note(pk, 50000); auto cm = note.cm().get(); SaplingMerkleTree tree; tree.append(cm); auto anchor = tree.root(); auto witness = tree.witness(); // Generate transaction auto builder = TransactionBuilder(consensusParams, 1); ASSERT_TRUE(builder.AddSaplingSpend(expsk, note, anchor, witness)); builder.AddSaplingOutput(fvk.ovk, pk, 25000, {}); auto maybe_tx = builder.Build(); ASSERT_EQ(static_cast(maybe_tx), true); auto tx = maybe_tx.get(); CWalletTx wtx {&wallet, tx}; ASSERT_TRUE(wallet.AddSaplingZKey(sk, pk)); ASSERT_TRUE(wallet.HaveSaplingSpendingKey(fvk)); // Manually compute the nullifier based on the known position auto nf = note.nullifier(fvk, witness.position()); ASSERT_TRUE(nf); uint256 nullifier = nf.get(); // Verify note has not been spent EXPECT_FALSE(wallet.IsSaplingSpent(nullifier)); // Fake-mine the transaction EXPECT_EQ(-1, chainActive.Height()); CBlock block; block.vtx.push_back(wtx); block.hashMerkleRoot = block.BuildMerkleTree(); auto blockHash = block.GetHash(); CBlockIndex fakeIndex {block}; mapBlockIndex.insert(std::make_pair(blockHash, &fakeIndex)); chainActive.SetTip(&fakeIndex); EXPECT_TRUE(chainActive.Contains(&fakeIndex)); EXPECT_EQ(0, chainActive.Height()); wtx.SetMerkleBranch(block); wallet.AddToWallet(wtx, true, NULL); // Verify note has been spent EXPECT_TRUE(wallet.IsSaplingSpent(nullifier)); // Tear down chainActive.SetTip(NULL); mapBlockIndex.erase(blockHash); // Revert to default UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); } TEST(WalletTests, NavigateFromSproutNullifierToNote) { CWallet wallet; auto sk = libzcash::SproutSpendingKey::random(); wallet.AddSproutSpendingKey(sk); auto wtx = GetValidReceive(sk, 10, true); auto note = GetNote(sk, wtx, 0, 1); auto nullifier = note.nullifier(sk); mapSproutNoteData_t noteData; JSOutPoint jsoutpt {wtx.GetHash(), 0, 1}; SproutNoteData nd {sk.address(), nullifier}; noteData[jsoutpt] = nd; wtx.SetSproutNoteData(noteData); EXPECT_EQ(0, wallet.mapSproutNullifiersToNotes.count(nullifier)); wallet.AddToWallet(wtx, true, NULL); EXPECT_EQ(1, wallet.mapSproutNullifiersToNotes.count(nullifier)); EXPECT_EQ(wtx.GetHash(), wallet.mapSproutNullifiersToNotes[nullifier].hash); EXPECT_EQ(0, wallet.mapSproutNullifiersToNotes[nullifier].js); EXPECT_EQ(1, wallet.mapSproutNullifiersToNotes[nullifier].n); } TEST(WalletTests, NavigateFromSaplingNullifierToNote) { SelectParams(CBaseChainParams::REGTEST); UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); auto consensusParams = Params().GetConsensus(); TestWallet wallet; // Generate dummy Sapling address std::vector> rawSeed(32); HDSeed seed(rawSeed); auto sk = libzcash::SaplingExtendedSpendingKey::Master(seed); auto expsk = sk.expsk; auto fvk = expsk.full_viewing_key(); auto pk = sk.DefaultAddress(); // Generate dummy Sapling note libzcash::SaplingNote note(pk, 50000); auto cm = note.cm().get(); SaplingMerkleTree saplingTree; saplingTree.append(cm); auto anchor = saplingTree.root(); auto witness = saplingTree.witness(); // Generate transaction auto builder = TransactionBuilder(consensusParams, 1); ASSERT_TRUE(builder.AddSaplingSpend(expsk, note, anchor, witness)); builder.AddSaplingOutput(fvk.ovk, pk, 25000, {}); auto maybe_tx = builder.Build(); ASSERT_EQ(static_cast(maybe_tx), true); auto tx = maybe_tx.get(); CWalletTx wtx {&wallet, tx}; ASSERT_TRUE(wallet.AddSaplingZKey(sk, pk)); ASSERT_TRUE(wallet.HaveSaplingSpendingKey(fvk)); // Manually compute the nullifier based on the expected position auto nf = note.nullifier(fvk, witness.position()); ASSERT_TRUE(nf); uint256 nullifier = nf.get(); // Verify dummy note is unspent EXPECT_FALSE(wallet.IsSaplingSpent(nullifier)); // Fake-mine the transaction EXPECT_EQ(-1, chainActive.Height()); SproutMerkleTree sproutTree; CBlock block; block.vtx.push_back(wtx); block.hashMerkleRoot = block.BuildMerkleTree(); auto blockHash = block.GetHash(); CBlockIndex fakeIndex {block}; mapBlockIndex.insert(std::make_pair(blockHash, &fakeIndex)); chainActive.SetTip(&fakeIndex); EXPECT_TRUE(chainActive.Contains(&fakeIndex)); EXPECT_EQ(0, chainActive.Height()); // Simulate SyncTransaction which calls AddToWalletIfInvolvingMe wtx.SetMerkleBranch(block); auto saplingNoteData = wallet.FindMySaplingNotes(wtx).first; ASSERT_TRUE(saplingNoteData.size() > 0); wtx.SetSaplingNoteData(saplingNoteData); wallet.AddToWallet(wtx, true, NULL); // Verify dummy note is now spent, as AddToWallet invokes AddToSpends() EXPECT_TRUE(wallet.IsSaplingSpent(nullifier)); // Test invariant: no witnesses means no nullifier. EXPECT_EQ(0, wallet.mapSaplingNullifiersToNotes.size()); for (mapSaplingNoteData_t::value_type &item : wtx.mapSaplingNoteData) { SaplingNoteData nd = item.second; ASSERT_TRUE(nd.witnesses.empty()); ASSERT_FALSE(nd.nullifier); } // Simulate receiving new block and ChainTip signal wallet.BuildWitnessCache(&fakeIndex, false); wallet.UpdateNullifierNoteMapForBlock(&block); // Retrieve the updated wtx from wallet uint256 hash = wtx.GetHash(); wtx = wallet.mapWallet[hash]; // Verify Sapling nullifiers map to SaplingOutPoints EXPECT_EQ(2, wallet.mapSaplingNullifiersToNotes.size()); for (mapSaplingNoteData_t::value_type &item : wtx.mapSaplingNoteData) { SaplingOutPoint op = item.first; SaplingNoteData nd = item.second; EXPECT_EQ(hash, op.hash); EXPECT_EQ(1, nd.witnesses.size()); ASSERT_TRUE(nd.nullifier); auto nf = nd.nullifier.get(); EXPECT_EQ(1, wallet.mapSaplingNullifiersToNotes.count(nf)); EXPECT_EQ(op.hash, wallet.mapSaplingNullifiersToNotes[nf].hash); EXPECT_EQ(op.n, wallet.mapSaplingNullifiersToNotes[nf].n); } // Tear down chainActive.SetTip(NULL); mapBlockIndex.erase(blockHash); // Revert to default UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); } TEST(WalletTests, SpentSproutNoteIsFromMe) { CWallet wallet; auto sk = libzcash::SproutSpendingKey::random(); wallet.AddSproutSpendingKey(sk); auto wtx = GetValidReceive(sk, 10, true); auto note = GetNote(sk, wtx, 0, 1); auto nullifier = note.nullifier(sk); auto wtx2 = GetValidSpend(sk, note, 5); EXPECT_FALSE(wallet.IsFromMe(wtx)); EXPECT_FALSE(wallet.IsFromMe(wtx2)); mapSproutNoteData_t noteData; JSOutPoint jsoutpt {wtx.GetHash(), 0, 1}; SproutNoteData nd {sk.address(), nullifier}; noteData[jsoutpt] = nd; wtx.SetSproutNoteData(noteData); EXPECT_FALSE(wallet.IsFromMe(wtx)); EXPECT_FALSE(wallet.IsFromMe(wtx2)); wallet.AddToWallet(wtx, true, NULL); EXPECT_FALSE(wallet.IsFromMe(wtx)); EXPECT_TRUE(wallet.IsFromMe(wtx2)); } // Create note A, spend A to create note B, spend and verify note B is from me. TEST(WalletTests, SpentSaplingNoteIsFromMe) { SelectParams(CBaseChainParams::REGTEST); UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); auto consensusParams = Params().GetConsensus(); TestWallet wallet; // Generate Sapling address std::vector> rawSeed(32); HDSeed seed(rawSeed); auto sk = libzcash::SaplingExtendedSpendingKey::Master(seed); auto expsk = sk.expsk; auto fvk = expsk.full_viewing_key(); auto ivk = fvk.in_viewing_key(); auto pk = sk.DefaultAddress(); // Generate Sapling note A libzcash::SaplingNote note(pk, 50000); auto cm = note.cm().get(); SaplingMerkleTree saplingTree; saplingTree.append(cm); auto anchor = saplingTree.root(); auto witness = saplingTree.witness(); // Generate transaction, which sends funds to note B auto builder = TransactionBuilder(consensusParams, 1); ASSERT_TRUE(builder.AddSaplingSpend(expsk, note, anchor, witness)); builder.AddSaplingOutput(fvk.ovk, pk, 25000, {}); auto maybe_tx = builder.Build(); ASSERT_EQ(static_cast(maybe_tx), true); auto tx = maybe_tx.get(); CWalletTx wtx {&wallet, tx}; ASSERT_TRUE(wallet.AddSaplingZKey(sk, pk)); ASSERT_TRUE(wallet.HaveSaplingSpendingKey(fvk)); // Fake-mine the transaction EXPECT_EQ(-1, chainActive.Height()); SproutMerkleTree sproutTree; CBlock block; block.vtx.push_back(wtx); block.hashMerkleRoot = block.BuildMerkleTree(); auto blockHash = block.GetHash(); CBlockIndex fakeIndex {block}; mapBlockIndex.insert(std::make_pair(blockHash, &fakeIndex)); chainActive.SetTip(&fakeIndex); EXPECT_TRUE(chainActive.Contains(&fakeIndex)); EXPECT_EQ(0, chainActive.Height()); auto saplingNoteData = wallet.FindMySaplingNotes(wtx).first; ASSERT_TRUE(saplingNoteData.size() > 0); wtx.SetSaplingNoteData(saplingNoteData); wtx.SetMerkleBranch(block); wallet.AddToWallet(wtx, true, NULL); // Simulate receiving new block and ChainTip signal. // This triggers calculation of nullifiers for notes belonging to this wallet // in the output descriptions of wtx. wallet.BuildWitnessCache(&fakeIndex, false); wallet.UpdateNullifierNoteMapForBlock(&block); // Retrieve the updated wtx from wallet wtx = wallet.mapWallet[wtx.GetHash()]; // The test wallet never received the fake note which is being spent, so there // is no mapping from nullifier to notedata stored in mapSaplingNullifiersToNotes. // Therefore the wallet does not know the tx belongs to the wallet. EXPECT_FALSE(wallet.IsFromMe(wtx)); // Manually compute the nullifier and check map entry does not exist auto nf = note.nullifier(fvk, witness.position()); ASSERT_TRUE(nf); ASSERT_FALSE(wallet.mapSaplingNullifiersToNotes.count(nf.get())); // Decrypt note B auto maybe_pt = libzcash::SaplingNotePlaintext::decrypt( wtx.vShieldedOutput[0].encCiphertext, ivk, wtx.vShieldedOutput[0].ephemeralKey, wtx.vShieldedOutput[0].cm); ASSERT_EQ(static_cast(maybe_pt), true); auto maybe_note = maybe_pt.get().note(ivk); ASSERT_EQ(static_cast(maybe_note), true); auto note2 = maybe_note.get(); // Get witness to retrieve position of note B we want to spend SaplingOutPoint sop0(wtx.GetHash(), 0); auto spend_note_witness = wtx.mapSaplingNoteData[sop0].witnesses.front(); auto maybe_nf = note2.nullifier(fvk, spend_note_witness.position()); ASSERT_EQ(static_cast(maybe_nf), true); auto nullifier2 = maybe_nf.get(); // NOTE: Not updating the anchor results in a core dump. Shouldn't builder just return error? // *** Error in `./zcash-gtest': double free or corruption (out): 0x00007ffd8755d990 *** anchor = saplingTree.root(); // Create transaction to spend note B auto builder2 = TransactionBuilder(consensusParams, 2); ASSERT_TRUE(builder2.AddSaplingSpend(expsk, note2, anchor, spend_note_witness)); builder2.AddSaplingOutput(fvk.ovk, pk, 12500, {}); auto maybe_tx2 = builder2.Build(); ASSERT_EQ(static_cast(maybe_tx2), true); auto tx2 = maybe_tx2.get(); EXPECT_EQ(tx2.vin.size(), 0); EXPECT_EQ(tx2.vout.size(), 0); EXPECT_EQ(tx2.vjoinsplit.size(), 0); EXPECT_EQ(tx2.vShieldedSpend.size(), 1); EXPECT_EQ(tx2.vShieldedOutput.size(), 2); EXPECT_EQ(tx2.valueBalance, 10000); CWalletTx wtx2 {&wallet, tx2}; // Fake-mine this tx into the next block EXPECT_EQ(0, chainActive.Height()); CBlock block2; block2.vtx.push_back(wtx2); block2.hashMerkleRoot = block2.BuildMerkleTree(); block2.hashPrevBlock = blockHash; auto blockHash2 = block2.GetHash(); CBlockIndex fakeIndex2 {block2}; mapBlockIndex.insert(std::make_pair(blockHash2, &fakeIndex2)); fakeIndex2.SetHeight(1); chainActive.SetTip(&fakeIndex2); EXPECT_TRUE(chainActive.Contains(&fakeIndex2)); EXPECT_EQ(1, chainActive.Height()); auto saplingNoteData2 = wallet.FindMySaplingNotes(wtx2).first; ASSERT_TRUE(saplingNoteData2.size() > 0); wtx2.SetSaplingNoteData(saplingNoteData2); wtx2.SetMerkleBranch(block2); wallet.AddToWallet(wtx2, true, NULL); // Verify note B is spent. AddToWallet invokes AddToSpends which updates mapTxSaplingNullifiers EXPECT_TRUE(wallet.IsSaplingSpent(nullifier2)); // Verify note B belongs to wallet. EXPECT_TRUE(wallet.IsFromMe(wtx2)); ASSERT_TRUE(wallet.mapSaplingNullifiersToNotes.count(nullifier2)); // Tear down chainActive.SetTip(NULL); mapBlockIndex.erase(blockHash); mapBlockIndex.erase(blockHash2); // Revert to default UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); } TEST(WalletTests, CachedWitnessesEmptyChain) { TestWallet wallet; auto sk = libzcash::SproutSpendingKey::random(); wallet.AddSproutSpendingKey(sk); auto wtx = GetValidReceive(sk, 10, true, 4); auto note = GetNote(sk, wtx, 0, 0); auto note2 = GetNote(sk, wtx, 0, 1); auto nullifier = note.nullifier(sk); auto nullifier2 = note2.nullifier(sk); mapSproutNoteData_t sproutNoteData; JSOutPoint jsoutpt {wtx.GetHash(), 0, 0}; JSOutPoint jsoutpt2 {wtx.GetHash(), 0, 1}; SproutNoteData nd {sk.address(), nullifier}; SproutNoteData nd2 {sk.address(), nullifier2}; sproutNoteData[jsoutpt] = nd; sproutNoteData[jsoutpt2] = nd2; wtx.SetSproutNoteData(sproutNoteData); std::vector sproutNotes {jsoutpt, jsoutpt2}; std::vector saplingNotes = SetSaplingNoteData(wtx); std::vector> sproutWitnesses; std::vector> saplingWitnesses; ::GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses); EXPECT_FALSE((bool) sproutWitnesses[0]); EXPECT_FALSE((bool) sproutWitnesses[1]); EXPECT_FALSE((bool) saplingWitnesses[0]); wallet.AddToWallet(wtx, true, NULL); ::GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses); EXPECT_FALSE((bool) sproutWitnesses[0]); EXPECT_FALSE((bool) sproutWitnesses[1]); EXPECT_FALSE((bool) saplingWitnesses[0]); CBlock block; block.vtx.push_back(wtx); CBlockIndex index(block); SproutMerkleTree sproutTree; SaplingMerkleTree saplingTree; wallet.BuildWitnessCache(&index, false); ::GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses); EXPECT_TRUE((bool) sproutWitnesses[0]); EXPECT_TRUE((bool) sproutWitnesses[1]); EXPECT_TRUE((bool) saplingWitnesses[0]); // Until #1302 is implemented, this should triggger an assertion EXPECT_DEATH(wallet.DecrementNoteWitnesses(&index), ".*nWitnessCacheSize > 0.*"); } TEST(WalletTests, CachedWitnessesChainTip) { TestWallet wallet; std::pair anchors1; CBlock block1; SproutMerkleTree sproutTree; SaplingMerkleTree saplingTree; auto sk = libzcash::SproutSpendingKey::random(); wallet.AddSproutSpendingKey(sk); { // First block (case tested in _empty_chain) CBlockIndex index1(block1); index1.SetHeight(1); auto outpts = CreateValidBlock(wallet, sk, index1, block1, sproutTree, saplingTree); // Called to fetch anchor std::vector sproutNotes {outpts.first}; std::vector saplingNotes {outpts.second}; std::vector> sproutWitnesses; std::vector> saplingWitnesses; anchors1 = GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses); EXPECT_NE(anchors1.first, anchors1.second); } { // Second transaction auto wtx = GetValidReceive(sk, 50, true, 4); auto note = GetNote(sk, wtx, 0, 1); auto nullifier = note.nullifier(sk); mapSproutNoteData_t sproutNoteData; JSOutPoint jsoutpt {wtx.GetHash(), 0, 1}; SproutNoteData nd {sk.address(), nullifier}; sproutNoteData[jsoutpt] = nd; wtx.SetSproutNoteData(sproutNoteData); std::vector saplingNotes = SetSaplingNoteData(wtx); wallet.AddToWallet(wtx, true, NULL); std::vector sproutNotes {jsoutpt}; std::vector> sproutWitnesses; std::vector> saplingWitnesses; GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses); EXPECT_FALSE((bool) sproutWitnesses[0]); EXPECT_FALSE((bool) saplingWitnesses[0]); // Second block CBlock block2; block2.hashPrevBlock = block1.GetHash(); block2.vtx.push_back(wtx); CBlockIndex index2(block2); index2.SetHeight(2); SproutMerkleTree sproutTree2 {sproutTree}; SaplingMerkleTree saplingTree2 {saplingTree}; wallet.IncrementNoteWitnesses(&index2, &block2, sproutTree2, saplingTree2); auto anchors2 = GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses); EXPECT_NE(anchors2.first, anchors2.second); EXPECT_TRUE((bool) sproutWitnesses[0]); EXPECT_TRUE((bool) saplingWitnesses[0]); EXPECT_NE(anchors1.first, anchors2.first); EXPECT_NE(anchors1.second, anchors2.second); // Decrementing should give us the previous anchor wallet.DecrementNoteWitnesses(&index2); auto anchors3 = GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses); EXPECT_FALSE((bool) sproutWitnesses[0]); EXPECT_FALSE((bool) saplingWitnesses[0]); // Should not equal first anchor because none of these notes had witnesses EXPECT_NE(anchors1.first, anchors3.first); EXPECT_NE(anchors1.second, anchors3.second); // Re-incrementing with the same block should give the same result wallet.BuildWitnessCache(&index2, false); auto anchors4 = GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses); EXPECT_NE(anchors4.first, anchors4.second); EXPECT_TRUE((bool) sproutWitnesses[0]); EXPECT_TRUE((bool) saplingWitnesses[0]); EXPECT_EQ(anchors2.first, anchors4.first); EXPECT_EQ(anchors2.second, anchors4.second); // Incrementing with the same block again should not change the cache wallet.BuildWitnessCache(&index2, false); std::vector> sproutWitnesses5; std::vector> saplingWitnesses5; auto anchors5 = GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses5, saplingWitnesses5); EXPECT_NE(anchors5.first, anchors5.second); EXPECT_EQ(sproutWitnesses, sproutWitnesses5); EXPECT_EQ(saplingWitnesses, saplingWitnesses5); EXPECT_EQ(anchors4.first, anchors5.first); EXPECT_EQ(anchors4.second, anchors5.second); } } TEST(WalletTests, CachedWitnessesDecrementFirst) { TestWallet wallet; SproutMerkleTree sproutTree; SaplingMerkleTree saplingTree; auto sk = libzcash::SproutSpendingKey::random(); wallet.AddSproutSpendingKey(sk); { // First block (case tested in _empty_chain) CBlock block1; CBlockIndex index1(block1); index1.SetHeight(1); CreateValidBlock(wallet, sk, index1, block1, sproutTree, saplingTree); } std::pair anchors2; CBlock block2; CBlockIndex index2(block2); { // Second block (case tested in _chain_tip) index2.SetHeight(2); auto outpts = CreateValidBlock(wallet, sk, index2, block2, sproutTree, saplingTree); // Called to fetch anchor std::vector sproutNotes {outpts.first}; std::vector saplingNotes {outpts.second}; std::vector> sproutWitnesses; std::vector> saplingWitnesses; anchors2 = GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses); } { // Third transaction - never mined auto wtx = GetValidReceive(sk, 20, true, 4); auto note = GetNote(sk, wtx, 0, 1); auto nullifier = note.nullifier(sk); mapSproutNoteData_t noteData; JSOutPoint jsoutpt {wtx.GetHash(), 0, 1}; SproutNoteData nd {sk.address(), nullifier}; noteData[jsoutpt] = nd; wtx.SetSproutNoteData(noteData); std::vector saplingNotes = SetSaplingNoteData(wtx); wallet.AddToWallet(wtx, true, NULL); std::vector sproutNotes {jsoutpt}; std::vector> sproutWitnesses; std::vector> saplingWitnesses; auto anchors3 = GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses); EXPECT_FALSE((bool) sproutWitnesses[0]); EXPECT_FALSE((bool) saplingWitnesses[0]); // Decrementing (before the transaction has ever seen an increment) // should give us the previous anchor wallet.DecrementNoteWitnesses(&index2); auto anchors4 = GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses); EXPECT_FALSE((bool) sproutWitnesses[0]); EXPECT_FALSE((bool) saplingWitnesses[0]); // Should not equal second anchor because none of these notes had witnesses EXPECT_NE(anchors2.first, anchors4.first); EXPECT_NE(anchors2.second, anchors4.second); // Re-incrementing with the same block should give the same result wallet.BuildWitnessCache(&index2, false); auto anchors5 = GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses); EXPECT_FALSE((bool) sproutWitnesses[0]); EXPECT_FALSE((bool) saplingWitnesses[0]); EXPECT_EQ(anchors3.first, anchors5.first); EXPECT_EQ(anchors3.second, anchors5.second); } } TEST(WalletTests, CachedWitnessesCleanIndex) { TestWallet wallet; std::vector blocks; std::vector indices; std::vector sproutNotes; std::vector saplingNotes; std::vector sproutAnchors; std::vector saplingAnchors; SproutMerkleTree sproutTree; SproutMerkleTree sproutRiTree = sproutTree; SaplingMerkleTree saplingTree; SaplingMerkleTree saplingRiTree = saplingTree; std::vector> sproutWitnesses; std::vector> saplingWitnesses; auto sk = libzcash::SproutSpendingKey::random(); wallet.AddSproutSpendingKey(sk); // Generate a chain size_t numBlocks = WITNESS_CACHE_SIZE + 10; blocks.resize(numBlocks); indices.resize(numBlocks); for (size_t i = 0; i < numBlocks; i++) { indices[i].SetHeight(i); auto oldSproutRoot = sproutTree.root(); auto oldSaplingRoot = saplingTree.root(); auto outpts = CreateValidBlock(wallet, sk, indices[i], blocks[i], sproutTree, saplingTree); EXPECT_NE(oldSproutRoot, sproutTree.root()); EXPECT_NE(oldSaplingRoot, saplingTree.root()); sproutNotes.push_back(outpts.first); saplingNotes.push_back(outpts.second); auto anchors = GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses); for (size_t j = 0; j <= i; j++) { EXPECT_TRUE((bool) sproutWitnesses[j]); EXPECT_TRUE((bool) saplingWitnesses[j]); } sproutAnchors.push_back(anchors.first); saplingAnchors.push_back(anchors.second); } // Now pretend we are reindexing: the chain is cleared, and each block is // used to increment witnesses again. for (size_t i = 0; i < numBlocks; i++) { SproutMerkleTree sproutRiPrevTree {sproutRiTree}; SaplingMerkleTree saplingRiPrevTree {saplingRiTree}; wallet.BuildWitnessCache(&indices[i], false); auto anchors = GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses); for (size_t j = 0; j < numBlocks; j++) { EXPECT_TRUE((bool) sproutWitnesses[j]); EXPECT_TRUE((bool) saplingWitnesses[j]); } // Should equal final anchor because witness cache unaffected EXPECT_EQ(sproutAnchors.back(), anchors.first); EXPECT_EQ(saplingAnchors.back(), anchors.second); if ((i == 5) || (i == 50)) { // Pretend a reorg happened that was recorded in the block files { wallet.DecrementNoteWitnesses(&(indices[i])); auto anchors = GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses); for (size_t j = 0; j < numBlocks; j++) { EXPECT_TRUE((bool) sproutWitnesses[j]); EXPECT_TRUE((bool) saplingWitnesses[j]); } // Should equal final anchor because witness cache unaffected EXPECT_EQ(sproutAnchors.back(), anchors.first); EXPECT_EQ(saplingAnchors.back(), anchors.second); } { wallet.BuildWitnessCache(&indices[i], false); auto anchors = GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses); for (size_t j = 0; j < numBlocks; j++) { EXPECT_TRUE((bool) sproutWitnesses[j]); EXPECT_TRUE((bool) saplingWitnesses[j]); } // Should equal final anchor because witness cache unaffected EXPECT_EQ(sproutAnchors.back(), anchors.first); EXPECT_EQ(saplingAnchors.back(), anchors.second); } } } } TEST(WalletTests, ClearNoteWitnessCache) { TestWallet wallet; auto sk = libzcash::SproutSpendingKey::random(); wallet.AddSproutSpendingKey(sk); auto wtx = GetValidReceive(sk, 10, true, 4); auto hash = wtx.GetHash(); auto note = GetNote(sk, wtx, 0, 0); auto nullifier = note.nullifier(sk); mapSproutNoteData_t noteData; JSOutPoint jsoutpt {wtx.GetHash(), 0, 0}; JSOutPoint jsoutpt2 {wtx.GetHash(), 0, 1}; SproutNoteData nd {sk.address(), nullifier}; noteData[jsoutpt] = nd; wtx.SetSproutNoteData(noteData); auto saplingNotes = SetSaplingNoteData(wtx); // Pretend we mined the tx by adding a fake witness SproutMerkleTree sproutTree; wtx.mapSproutNoteData[jsoutpt].witnesses.push_front(sproutTree.witness()); wtx.mapSproutNoteData[jsoutpt].witnessHeight = 1; wallet.nWitnessCacheSize = 1; SaplingMerkleTree saplingTree; wtx.mapSaplingNoteData[saplingNotes[0]].witnesses.push_front(saplingTree.witness()); wtx.mapSaplingNoteData[saplingNotes[0]].witnessHeight = 1; wallet.nWitnessCacheSize = 2; wallet.AddToWallet(wtx, true, NULL); std::vector sproutNotes {jsoutpt, jsoutpt2}; std::vector> sproutWitnesses; std::vector> saplingWitnesses; // Before clearing, we should have a witness for one note GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses); EXPECT_TRUE((bool) sproutWitnesses[0]); EXPECT_FALSE((bool) sproutWitnesses[1]); EXPECT_TRUE((bool) saplingWitnesses[0]); EXPECT_FALSE((bool) saplingWitnesses[1]); EXPECT_EQ(1, wallet.mapWallet[hash].mapSproutNoteData[jsoutpt].witnessHeight); EXPECT_EQ(1, wallet.mapWallet[hash].mapSaplingNoteData[saplingNotes[0]].witnessHeight); EXPECT_EQ(2, wallet.nWitnessCacheSize); // After clearing, we should not have a witness for either note wallet.ClearNoteWitnessCache(); auto anchors2 = GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses); EXPECT_FALSE((bool) sproutWitnesses[0]); EXPECT_FALSE((bool) sproutWitnesses[1]); EXPECT_FALSE((bool) saplingWitnesses[0]); EXPECT_FALSE((bool) saplingWitnesses[1]); EXPECT_EQ(-1, wallet.mapWallet[hash].mapSproutNoteData[jsoutpt].witnessHeight); EXPECT_EQ(-1, wallet.mapWallet[hash].mapSaplingNoteData[saplingNotes[0]].witnessHeight); EXPECT_EQ(0, wallet.nWitnessCacheSize); } TEST(WalletTests, WriteWitnessCache) { TestWallet wallet; MockWalletDB walletdb; CBlockLocator loc; auto sk = libzcash::SproutSpendingKey::random(); wallet.AddSproutSpendingKey(sk); auto wtx = GetValidReceive(sk, 10, true); wallet.AddToWallet(wtx, true, NULL); // TxnBegin fails EXPECT_CALL(walletdb, TxnBegin()) .WillOnce(Return(false)); wallet.SetBestChain(walletdb, loc); EXPECT_CALL(walletdb, TxnBegin()) .WillRepeatedly(Return(true)); // WriteTx fails EXPECT_CALL(walletdb, WriteTx(wtx.GetHash(), wtx)) .WillOnce(Return(false)); EXPECT_CALL(walletdb, TxnAbort()) .Times(1); wallet.SetBestChain(walletdb, loc); // WriteTx throws EXPECT_CALL(walletdb, WriteTx(wtx.GetHash(), wtx)) .WillOnce(ThrowLogicError()); EXPECT_CALL(walletdb, TxnAbort()) .Times(1); wallet.SetBestChain(walletdb, loc); EXPECT_CALL(walletdb, WriteTx(wtx.GetHash(), wtx)) .WillRepeatedly(Return(true)); // WriteWitnessCacheSize fails EXPECT_CALL(walletdb, WriteWitnessCacheSize(0)) .WillOnce(Return(false)); EXPECT_CALL(walletdb, TxnAbort()) .Times(1); wallet.SetBestChain(walletdb, loc); // WriteWitnessCacheSize throws EXPECT_CALL(walletdb, WriteWitnessCacheSize(0)) .WillOnce(ThrowLogicError()); EXPECT_CALL(walletdb, TxnAbort()) .Times(1); wallet.SetBestChain(walletdb, loc); EXPECT_CALL(walletdb, WriteWitnessCacheSize(0)) .WillRepeatedly(Return(true)); // WriteBestBlock fails EXPECT_CALL(walletdb, WriteBestBlock(loc)) .WillOnce(Return(false)); EXPECT_CALL(walletdb, TxnAbort()) .Times(1); wallet.SetBestChain(walletdb, loc); // WriteBestBlock throws EXPECT_CALL(walletdb, WriteBestBlock(loc)) .WillOnce(ThrowLogicError()); EXPECT_CALL(walletdb, TxnAbort()) .Times(1); wallet.SetBestChain(walletdb, loc); EXPECT_CALL(walletdb, WriteBestBlock(loc)) .WillRepeatedly(Return(true)); // TxCommit fails EXPECT_CALL(walletdb, TxnCommit()) .WillOnce(Return(false)); wallet.SetBestChain(walletdb, loc); EXPECT_CALL(walletdb, TxnCommit()) .WillRepeatedly(Return(true)); // Everything succeeds wallet.SetBestChain(walletdb, loc); } TEST(WalletTests, SetBestChainIgnoresTxsWithoutShieldedData) { SelectParams(CBaseChainParams::REGTEST); TestWallet wallet; MockWalletDB walletdb; CBlockLocator loc; // Set up transparent address CKey tsk = DecodeSecret(tSecretRegtest); wallet.AddKey(tsk); auto scriptPubKey = GetScriptForDestination(tsk.GetPubKey().GetID()); // Set up a Sprout address auto sk = libzcash::SproutSpendingKey::random(); wallet.AddSproutSpendingKey(sk); // Generate a transparent transaction that is ours CMutableTransaction t; t.vout.resize(1); t.vout[0].nValue = 90*CENT; t.vout[0].scriptPubKey = scriptPubKey; CWalletTx wtxTransparent {nullptr, t}; wallet.AddToWallet(wtxTransparent, true, NULL); // Generate a Sprout transaction that is ours auto wtxSprout = GetValidReceive(sk, 10, true); auto noteMap = wallet.FindMySproutNotes(wtxSprout); wtxSprout.SetSproutNoteData(noteMap); wallet.AddToWallet(wtxSprout, true, NULL); // Generate a Sprout transaction that only involves our transparent address auto sk2 = libzcash::SproutSpendingKey::random(); auto wtxInput = GetValidReceive(sk2, 10, true); auto note = GetNote(sk2, wtxInput, 0, 0); auto wtxTmp = GetValidSpend(sk2, note, 5); CMutableTransaction mtx {wtxTmp}; mtx.vout[0].scriptPubKey = scriptPubKey; CWalletTx wtxSproutTransparent {NULL, mtx}; wallet.AddToWallet(wtxSproutTransparent, true, NULL); EXPECT_CALL(walletdb, TxnBegin()) .WillOnce(Return(true)); EXPECT_CALL(walletdb, WriteTx(wtxTransparent.GetHash(), wtxTransparent)) .Times(0); EXPECT_CALL(walletdb, WriteTx(wtxSprout.GetHash(), wtxSprout)) .Times(1).WillOnce(Return(true)); EXPECT_CALL(walletdb, WriteTx(wtxSproutTransparent.GetHash(), wtxSproutTransparent)) .Times(0); EXPECT_CALL(walletdb, WriteWitnessCacheSize(0)) .WillOnce(Return(true)); EXPECT_CALL(walletdb, WriteBestBlock(loc)) .WillOnce(Return(true)); EXPECT_CALL(walletdb, TxnCommit()) .WillOnce(Return(true)); wallet.SetBestChain(walletdb, loc); } TEST(WalletTests, UpdatedSaplingNoteData) { SelectParams(CBaseChainParams::REGTEST); UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); auto consensusParams = Params().GetConsensus(); TestWallet wallet; std::vector> rawSeed(32); HDSeed seed(rawSeed); auto m = libzcash::SaplingExtendedSpendingKey::Master(seed); // Generate dummy Sapling address auto sk = m.Derive(0); auto expsk = sk.expsk; auto fvk = expsk.full_viewing_key(); auto pk = sk.DefaultAddress(); // Generate dummy recipient Sapling address auto sk2 = m.Derive(1); auto expsk2 = sk2.expsk; auto fvk2 = expsk2.full_viewing_key(); auto pk2 = sk2.DefaultAddress(); // Generate dummy Sapling note libzcash::SaplingNote note(pk, 50000); auto cm = note.cm().get(); SaplingMerkleTree saplingTree; saplingTree.append(cm); auto anchor = saplingTree.root(); auto witness = saplingTree.witness(); // Generate transaction auto builder = TransactionBuilder(consensusParams, 1); ASSERT_TRUE(builder.AddSaplingSpend(expsk, note, anchor, witness)); builder.AddSaplingOutput(fvk.ovk, pk2, 25000, {}); auto maybe_tx = builder.Build(); ASSERT_EQ(static_cast(maybe_tx), true); auto tx = maybe_tx.get(); // Wallet contains fvk1 but not fvk2 CWalletTx wtx {&wallet, tx}; ASSERT_TRUE(wallet.AddSaplingZKey(sk, pk)); ASSERT_TRUE(wallet.HaveSaplingSpendingKey(fvk)); ASSERT_FALSE(wallet.HaveSaplingSpendingKey(fvk2)); // Fake-mine the transaction EXPECT_EQ(-1, chainActive.Height()); SproutMerkleTree sproutTree; CBlock block; block.vtx.push_back(wtx); block.hashMerkleRoot = block.BuildMerkleTree(); auto blockHash = block.GetHash(); CBlockIndex fakeIndex {block}; mapBlockIndex.insert(std::make_pair(blockHash, &fakeIndex)); chainActive.SetTip(&fakeIndex); EXPECT_TRUE(chainActive.Contains(&fakeIndex)); EXPECT_EQ(0, chainActive.Height()); // Simulate SyncTransaction which calls AddToWalletIfInvolvingMe auto saplingNoteData = wallet.FindMySaplingNotes(wtx).first; ASSERT_TRUE(saplingNoteData.size() == 1); // wallet only has key for change output wtx.SetSaplingNoteData(saplingNoteData); wtx.SetMerkleBranch(block); wallet.AddToWallet(wtx, true, NULL); // Simulate receiving new block and ChainTip signal wallet.BuildWitnessCache(&fakeIndex, false); wallet.UpdateNullifierNoteMapForBlock(&block); // Retrieve the updated wtx from wallet uint256 hash = wtx.GetHash(); wtx = wallet.mapWallet[hash]; // Now lets add key fvk2 so wallet can find the payment note sent to pk2 ASSERT_TRUE(wallet.AddSaplingZKey(sk2, pk2)); ASSERT_TRUE(wallet.HaveSaplingSpendingKey(fvk2)); CWalletTx wtx2 = wtx; auto saplingNoteData2 = wallet.FindMySaplingNotes(wtx2).first; ASSERT_TRUE(saplingNoteData2.size() == 2); wtx2.SetSaplingNoteData(saplingNoteData2); // The payment note has not been witnessed yet, so let's fake the witness. SaplingOutPoint sop0(wtx2.GetHash(), 0); SaplingOutPoint sop1(wtx2.GetHash(), 1); wtx2.mapSaplingNoteData[sop0].witnesses.push_front(saplingTree.witness()); wtx2.mapSaplingNoteData[sop0].witnessHeight = 0; // The txs are different as wtx is aware of just the change output, // whereas wtx2 is aware of both payment and change outputs. EXPECT_NE(wtx.mapSaplingNoteData, wtx2.mapSaplingNoteData); EXPECT_EQ(1, wtx.mapSaplingNoteData.size()); EXPECT_EQ(1, wtx.mapSaplingNoteData[sop1].witnesses.size()); // wtx has witness for change EXPECT_EQ(2, wtx2.mapSaplingNoteData.size()); EXPECT_EQ(1, wtx2.mapSaplingNoteData[sop0].witnesses.size()); // wtx2 has fake witness for payment output EXPECT_EQ(0, wtx2.mapSaplingNoteData[sop1].witnesses.size()); // wtx2 never had BuildWitnessCache called // After updating, they should be the same EXPECT_TRUE(wallet.UpdatedNoteData(wtx2, wtx)); // We can't do this: // EXPECT_EQ(wtx.mapSaplingNoteData, wtx2.mapSaplingNoteData); // because nullifiers (if part of == comparator) have not all been computed // Also note that mapwallet[hash] is not updated with the updated wtx. // wtx = wallet.mapWallet[hash]; EXPECT_EQ(2, wtx.mapSaplingNoteData.size()); EXPECT_EQ(2, wtx2.mapSaplingNoteData.size()); // wtx copied over the fake witness from wtx2 for the payment output EXPECT_EQ(wtx.mapSaplingNoteData[sop0].witnesses.front(), wtx2.mapSaplingNoteData[sop0].witnesses.front()); // wtx2 never had its change output witnessed even though it has been in wtx EXPECT_EQ(0, wtx2.mapSaplingNoteData[sop1].witnesses.size()); EXPECT_EQ(wtx.mapSaplingNoteData[sop1].witnesses.front(), saplingTree.witness()); // Tear down chainActive.SetTip(NULL); mapBlockIndex.erase(blockHash); // Revert to default UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); } TEST(WalletTests, MarkAffectedSaplingTransactionsDirty) { SelectParams(CBaseChainParams::REGTEST); UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::ALWAYS_ACTIVE); auto consensusParams = Params().GetConsensus(); TestWallet wallet; // Generate Sapling address std::vector> rawSeed(32); HDSeed seed(rawSeed); auto sk = libzcash::SaplingExtendedSpendingKey::Master(seed); auto expsk = sk.expsk; auto fvk = expsk.full_viewing_key(); auto ivk = fvk.in_viewing_key(); auto pk = sk.DefaultAddress(); ASSERT_TRUE(wallet.AddSaplingZKey(sk, pk)); ASSERT_TRUE(wallet.HaveSaplingSpendingKey(fvk)); // Set up transparent address CBasicKeyStore keystore; CKey tsk = DecodeSecret(tSecretRegtest); keystore.AddKey(tsk); auto scriptPubKey = GetScriptForDestination(tsk.GetPubKey().GetID()); // Generate shielding tx from transparent to Sapling // 0.0005 t-HUSH in, 0.0004 z-HUSH out, 0.0001 t-HUSH fee auto builder = TransactionBuilder(consensusParams, 1, &keystore); builder.AddTransparentInput(COutPoint(), scriptPubKey, 50000); builder.AddSaplingOutput(fvk.ovk, pk, 40000, {}); auto maybe_tx = builder.Build(); ASSERT_EQ(static_cast(maybe_tx), true); auto tx1 = maybe_tx.get(); EXPECT_EQ(tx1.vin.size(), 1); EXPECT_EQ(tx1.vout.size(), 0); EXPECT_EQ(tx1.vjoinsplit.size(), 0); EXPECT_EQ(tx1.vShieldedSpend.size(), 0); EXPECT_EQ(tx1.vShieldedOutput.size(), 1); EXPECT_EQ(tx1.valueBalance, -40000); CWalletTx wtx {&wallet, tx1}; // Fake-mine the transaction EXPECT_EQ(-1, chainActive.Height()); SaplingMerkleTree saplingTree; SproutMerkleTree sproutTree; CBlock block; block.vtx.push_back(wtx); block.hashMerkleRoot = block.BuildMerkleTree(); auto blockHash = block.GetHash(); CBlockIndex fakeIndex {block}; mapBlockIndex.insert(std::make_pair(blockHash, &fakeIndex)); chainActive.SetTip(&fakeIndex); EXPECT_TRUE(chainActive.Contains(&fakeIndex)); EXPECT_EQ(0, chainActive.Height()); // Simulate SyncTransaction which calls AddToWalletIfInvolvingMe auto saplingNoteData = wallet.FindMySaplingNotes(wtx).first; ASSERT_TRUE(saplingNoteData.size() > 0); wtx.SetSaplingNoteData(saplingNoteData); wtx.SetMerkleBranch(block); wallet.AddToWallet(wtx, true, NULL); // Simulate receiving new block and ChainTip signal wallet.BuildWitnessCache(&fakeIndex, false); wallet.UpdateNullifierNoteMapForBlock(&block); // Retrieve the updated wtx from wallet uint256 hash = wtx.GetHash(); wtx = wallet.mapWallet[hash]; // Prepare to spend the note that was just created auto maybe_pt = libzcash::SaplingNotePlaintext::decrypt( tx1.vShieldedOutput[0].encCiphertext, ivk, tx1.vShieldedOutput[0].ephemeralKey, tx1.vShieldedOutput[0].cm); ASSERT_EQ(static_cast(maybe_pt), true); auto maybe_note = maybe_pt.get().note(ivk); ASSERT_EQ(static_cast(maybe_note), true); auto note = maybe_note.get(); auto anchor = saplingTree.root(); auto witness = saplingTree.witness(); // Create a Sapling-only transaction // 0.0004 z-ZEC in, 0.00025 z-ZEC out, 0.0001 t-ZEC fee, 0.00005 z-ZEC change auto builder2 = TransactionBuilder(consensusParams, 2); ASSERT_TRUE(builder2.AddSaplingSpend(expsk, note, anchor, witness)); builder2.AddSaplingOutput(fvk.ovk, pk, 25000, {}); auto maybe_tx2 = builder2.Build(); ASSERT_EQ(static_cast(maybe_tx2), true); auto tx2 = maybe_tx2.get(); EXPECT_EQ(tx2.vin.size(), 0); EXPECT_EQ(tx2.vout.size(), 0); EXPECT_EQ(tx2.vjoinsplit.size(), 0); EXPECT_EQ(tx2.vShieldedSpend.size(), 1); EXPECT_EQ(tx2.vShieldedOutput.size(), 2); EXPECT_EQ(tx2.valueBalance, 10000); CWalletTx wtx2 {&wallet, tx2}; auto hash2 = wtx2.GetHash(); wallet.MarkAffectedTransactionsDirty(wtx); // After getting a cached value, the first tx should be clean wallet.mapWallet[hash].GetDebit(ISMINE_ALL); EXPECT_TRUE(wallet.mapWallet[hash].fDebitCached); // After adding the note spend, the first tx should be dirty wallet.AddToWallet(wtx2, true, NULL); wallet.MarkAffectedTransactionsDirty(wtx2); EXPECT_FALSE(wallet.mapWallet[hash].fDebitCached); // Tear down chainActive.SetTip(NULL); mapBlockIndex.erase(blockHash); // Revert to default UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); } */ TEST(WalletTests, SaplingNoteLocking) { TestWallet wallet; SaplingOutPoint sop1 {uint256(), 1}; SaplingOutPoint sop2 {uint256(), 2}; // Test selective locking wallet.LockNote(sop1); EXPECT_TRUE(wallet.IsLockedNote(sop1)); EXPECT_FALSE(wallet.IsLockedNote(sop2)); // Test selective unlocking wallet.UnlockNote(sop1); EXPECT_FALSE(wallet.IsLockedNote(sop1)); // Test multiple locking wallet.LockNote(sop1); wallet.LockNote(sop2); EXPECT_TRUE(wallet.IsLockedNote(sop1)); EXPECT_TRUE(wallet.IsLockedNote(sop2)); // Test list auto v = wallet.ListLockedSaplingNotes(); EXPECT_EQ(v.size(), 2); EXPECT_TRUE(std::find(v.begin(), v.end(), sop1) != v.end()); EXPECT_TRUE(std::find(v.begin(), v.end(), sop2) != v.end()); // Test unlock all wallet.UnlockAllSaplingNotes(); EXPECT_FALSE(wallet.IsLockedNote(sop1)); EXPECT_FALSE(wallet.IsLockedNote(sop2)); }