// Copyright (c) 2014 The Bitcoin Core developers // Copyright (c) 2022-2023 The Zcash developers // Distributed under the MIT software license, see the accompanying // file COPYING or https://www.opensource.org/licenses/mit-license.php . #include "coins.h" #include "gtest/utils.h" #include "script/standard.h" #include "uint256.h" #include "util/strencodings.h" #include "test/test_bitcoin.h" #include "consensus/validation.h" #include "main.h" #include "undo.h" #include "primitives/transaction.h" #include "pubkey.h" #include "transaction_builder.h" #include "zcash/Note.hpp" #include "zcash/address/mnemonic.h" #include #include #include #include #include "zcash/IncrementalMerkleTree.hpp" namespace { class CCoinsViewTest : public CCoinsView { uint256 hashBestBlock_; uint256 hashBestSproutAnchor_; uint256 hashBestSaplingAnchor_; uint256 hashBestOrchardAnchor_; std::map map_; std::map mapSproutAnchors_; std::map mapSaplingAnchors_; std::map mapOrchardAnchors_; std::map mapSproutNullifiers_; std::map mapSaplingNullifiers_; std::map mapOrchardNullifiers_; public: CCoinsViewTest() { hashBestSproutAnchor_ = SproutMerkleTree::empty_root(); hashBestSaplingAnchor_ = SaplingMerkleTree::empty_root(); hashBestOrchardAnchor_ = OrchardMerkleFrontier::empty_root(); } bool GetSproutAnchorAt(const uint256& rt, SproutMerkleTree &tree) const { if (rt == SproutMerkleTree::empty_root()) { SproutMerkleTree new_tree; tree = new_tree; return true; } std::map::const_iterator it = mapSproutAnchors_.find(rt); if (it == mapSproutAnchors_.end()) { return false; } else { tree = it->second; return true; } } bool GetSaplingAnchorAt(const uint256& rt, SaplingMerkleTree &tree) const { if (rt == SaplingMerkleTree::empty_root()) { SaplingMerkleTree new_tree; tree = new_tree; return true; } std::map::const_iterator it = mapSaplingAnchors_.find(rt); if (it == mapSaplingAnchors_.end()) { return false; } else { tree = it->second; return true; } } bool GetOrchardAnchorAt(const uint256& rt, OrchardMerkleFrontier &tree) const { if (rt == OrchardMerkleFrontier::empty_root()) { OrchardMerkleFrontier new_tree; tree = new_tree; return true; } std::map::const_iterator it = mapOrchardAnchors_.find(rt); if (it == mapOrchardAnchors_.end()) { return false; } else { tree = it->second; return true; } } bool GetNullifier(const uint256 &nf, ShieldedType type) const { const std::map* mapToUse; switch (type) { case SPROUT: mapToUse = &mapSproutNullifiers_; break; case SAPLING: mapToUse = &mapSaplingNullifiers_; break; case ORCHARD: mapToUse = &mapOrchardNullifiers_; break; default: throw std::runtime_error("Unknown shielded type"); } std::map::const_iterator it = mapToUse->find(nf); if (it == mapToUse->end()) { return false; } else { // The map shouldn't contain any false entries. assert(it->second); return true; } } uint256 GetBestAnchor(ShieldedType type) const { switch (type) { case SPROUT: return hashBestSproutAnchor_; break; case SAPLING: return hashBestSaplingAnchor_; break; case ORCHARD: return hashBestOrchardAnchor_; break; default: throw std::runtime_error("Unknown shielded type"); } } bool GetCoins(const uint256& txid, CCoins& coins) const { std::map::const_iterator it = map_.find(txid); if (it == map_.end()) { return false; } coins = it->second; if (coins.IsPruned() && InsecureRandBool() == 0) { // Randomly return false in case of an empty entry. return false; } return true; } bool HaveCoins(const uint256& txid) const { CCoins coins; return GetCoins(txid, coins); } uint256 GetBestBlock() const { return hashBestBlock_; } void BatchWriteNullifiers(CNullifiersMap& mapNullifiers, std::map& cacheNullifiers) { for (CNullifiersMap::iterator it = mapNullifiers.begin(); it != mapNullifiers.end(); ) { if (it->second.flags & CNullifiersCacheEntry::DIRTY) { // Same optimization used in CCoinsViewDB is to only write dirty entries. if (it->second.entered) { cacheNullifiers[it->first] = true; } else { cacheNullifiers.erase(it->first); } } it = mapNullifiers.erase(it); } } template void BatchWriteAnchors(Map& mapAnchors, std::map& cacheAnchors) { for (auto it = mapAnchors.begin(); it != mapAnchors.end(); ) { if (it->second.flags & MapEntry::DIRTY) { // Same optimization used in CCoinsViewDB is to only write dirty entries. if (it->second.entered) { if (it->first != Tree::empty_root()) { auto ret = cacheAnchors.insert(std::make_pair(it->first, Tree())).first; ret->second = it->second.tree; } } else { cacheAnchors.erase(it->first); } } it = mapAnchors.erase(it); } } bool BatchWrite(CCoinsMap& mapCoins, const uint256& hashBlock, const uint256& hashSproutAnchor, const uint256& hashSaplingAnchor, const uint256& hashOrchardAnchor, CAnchorsSproutMap& mapSproutAnchors, CAnchorsSaplingMap& mapSaplingAnchors, CAnchorsOrchardMap& mapOrchardAnchors, CNullifiersMap& mapSproutNullifiers, CNullifiersMap& mapSaplingNullifiers, CNullifiersMap& mapOrchardNullifiers, CHistoryCacheMap &historyCacheMap) { for (CCoinsMap::iterator it = mapCoins.begin(); it != mapCoins.end(); ) { if (it->second.flags & CCoinsCacheEntry::DIRTY) { // Same optimization used in CCoinsViewDB is to only write dirty entries. map_[it->first] = it->second.coins; if (it->second.coins.IsPruned() && InsecureRandRange(3) == 0) { // Randomly delete empty entries on write. map_.erase(it->first); } } it = mapCoins.erase(it); } BatchWriteAnchors(mapSproutAnchors, mapSproutAnchors_); BatchWriteAnchors(mapSaplingAnchors, mapSaplingAnchors_); BatchWriteAnchors(mapOrchardAnchors, mapOrchardAnchors_); BatchWriteNullifiers(mapSproutNullifiers, mapSproutNullifiers_); BatchWriteNullifiers(mapSaplingNullifiers, mapSaplingNullifiers_); BatchWriteNullifiers(mapOrchardNullifiers, mapOrchardNullifiers_); if (!hashBlock.IsNull()) hashBestBlock_ = hashBlock; if (!hashSproutAnchor.IsNull()) hashBestSproutAnchor_ = hashSproutAnchor; if (!hashSaplingAnchor.IsNull()) hashBestSaplingAnchor_ = hashSaplingAnchor; if (!hashOrchardAnchor.IsNull()) hashBestOrchardAnchor_ = hashOrchardAnchor; return true; } bool GetStats(CCoinsStats& stats) const { return false; } }; class CCoinsViewCacheTest : public CCoinsViewCache { public: CCoinsViewCacheTest(CCoinsView* base) : CCoinsViewCache(base) {} void SelfTest() const { // Manually recompute the dynamic usage of the whole data, and compare it. size_t ret = memusage::DynamicUsage(cacheCoins) + memusage::DynamicUsage(cacheSproutAnchors) + memusage::DynamicUsage(cacheSaplingAnchors) + memusage::DynamicUsage(cacheOrchardAnchors) + memusage::DynamicUsage(cacheSproutNullifiers) + memusage::DynamicUsage(cacheSaplingNullifiers) + memusage::DynamicUsage(cacheOrchardNullifiers) + memusage::DynamicUsage(historyCacheMap); for (CCoinsMap::iterator it = cacheCoins.begin(); it != cacheCoins.end(); it++) { ret += it->second.coins.DynamicMemoryUsage(); } EXPECT_EQ(DynamicMemoryUsage(), ret); } }; class TxWithNullifiers { public: CTransaction tx; uint256 sproutNullifier; uint256 saplingNullifier; uint256 orchardNullifier; TxWithNullifiers() { CMutableTransaction mutableTx; sproutNullifier = GetRandHash(); JSDescription jsd; jsd.nullifiers[0] = sproutNullifier; mutableTx.vJoinSplit.emplace_back(jsd); saplingNullifier = GetRandHash(); SpendDescription sd; sd.nullifier = saplingNullifier; mutableTx.vShieldedSpend.push_back(sd); // The Orchard bundle builder always pads to two Actions, so we can just // use an empty builder to create a dummy Orchard bundle. uint256 orchardAnchor; uint256 dataToBeSigned; auto builder = orchard::Builder(true, true, orchardAnchor); mutableTx.orchardBundle = builder.Build().value().ProveAndSign({}, dataToBeSigned).value(); orchardNullifier = mutableTx.orchardBundle.GetNullifiers()[0]; tx = CTransaction(mutableTx); } }; } uint256 appendRandomSproutCommitment(SproutMerkleTree &tree) { libzcash::SproutSpendingKey k = libzcash::SproutSpendingKey::random(); libzcash::SproutPaymentAddress addr = k.address(); libzcash::SproutNote note(addr.a_pk, 0, uint256(), uint256()); auto cm = note.cm(); tree.append(cm); return cm; } template void AppendRandomLeaf(Tree &tree); template<> void AppendRandomLeaf(SproutMerkleTree &tree) { tree.append(GetRandHash()); } template<> void AppendRandomLeaf(SaplingMerkleTree &tree) { tree.append(GetRandHash()); } template<> void AppendRandomLeaf(OrchardMerkleFrontier &tree) { // OrchardMerkleFrontier only has APIs to append entire bundles, but // fortunately the tests only require that the tree root change. // TODO: Remove the need to create proofs by having a testing-only way to // append a random leaf to OrchardMerkleFrontier. uint256 orchardAnchor; uint256 dataToBeSigned; auto builder = orchard::Builder(true, true, orchardAnchor); auto bundle = builder.Build().value().ProveAndSign({}, dataToBeSigned).value(); tree.AppendBundle(bundle); } template bool GetAnchorAt(const CCoinsViewCacheTest &cache, const uint256 &rt, Tree &tree); template<> bool GetAnchorAt(const CCoinsViewCacheTest &cache, const uint256 &rt, SproutMerkleTree &tree) { return cache.GetSproutAnchorAt(rt, tree); } template<> bool GetAnchorAt(const CCoinsViewCacheTest &cache, const uint256 &rt, SaplingMerkleTree &tree) { return cache.GetSaplingAnchorAt(rt, tree); } template<> bool GetAnchorAt(const CCoinsViewCacheTest &cache, const uint256 &rt, OrchardMerkleFrontier &tree) { return cache.GetOrchardAnchorAt(rt, tree); } void checkNullifierCache(const CCoinsViewCacheTest &cache, const TxWithNullifiers &txWithNullifiers, bool shouldBeInCache) { // Make sure the nullifiers have not gotten mixed up EXPECT_TRUE(!cache.GetNullifier(txWithNullifiers.sproutNullifier, SAPLING)); EXPECT_TRUE(!cache.GetNullifier(txWithNullifiers.sproutNullifier, ORCHARD)); EXPECT_TRUE(!cache.GetNullifier(txWithNullifiers.saplingNullifier, SPROUT)); EXPECT_TRUE(!cache.GetNullifier(txWithNullifiers.saplingNullifier, ORCHARD)); EXPECT_TRUE(!cache.GetNullifier(txWithNullifiers.orchardNullifier, SPROUT)); EXPECT_TRUE(!cache.GetNullifier(txWithNullifiers.orchardNullifier, SAPLING)); // Check if the nullifiers either are or are not in the cache bool containsSproutNullifier = cache.GetNullifier(txWithNullifiers.sproutNullifier, SPROUT); bool containsSaplingNullifier = cache.GetNullifier(txWithNullifiers.saplingNullifier, SAPLING); bool containsOrchardNullifier = cache.GetNullifier(txWithNullifiers.orchardNullifier, ORCHARD); EXPECT_TRUE(containsSproutNullifier == shouldBeInCache); EXPECT_TRUE(containsSaplingNullifier == shouldBeInCache); EXPECT_TRUE(containsOrchardNullifier == shouldBeInCache); } TEST(CoinsTests, NullifierRegression) { LoadProofParameters(); // Correct behavior: { SCOPED_TRACE("add-remove without flush"); CCoinsViewTest base; CCoinsViewCacheTest cache1(&base); TxWithNullifiers txWithNullifiers; // Insert a nullifier into the base. cache1.SetNullifiers(txWithNullifiers.tx, true); checkNullifierCache(cache1, txWithNullifiers, true); cache1.Flush(); // Flush to base. // Remove the nullifier from cache cache1.SetNullifiers(txWithNullifiers.tx, false); // The nullifier now should be `false`. checkNullifierCache(cache1, txWithNullifiers, false); } // Also correct behavior: { SCOPED_TRACE("add-remove with flush"); CCoinsViewTest base; CCoinsViewCacheTest cache1(&base); TxWithNullifiers txWithNullifiers; // Insert a nullifier into the base. cache1.SetNullifiers(txWithNullifiers.tx, true); checkNullifierCache(cache1, txWithNullifiers, true); cache1.Flush(); // Flush to base. // Remove the nullifier from cache cache1.SetNullifiers(txWithNullifiers.tx, false); cache1.Flush(); // Flush to base. // The nullifier now should be `false`. checkNullifierCache(cache1, txWithNullifiers, false); } // Works because we bring it from the parent cache: { SCOPED_TRACE("remove from parent"); CCoinsViewTest base; CCoinsViewCacheTest cache1(&base); // Insert a nullifier into the base. TxWithNullifiers txWithNullifiers; cache1.SetNullifiers(txWithNullifiers.tx, true); checkNullifierCache(cache1, txWithNullifiers, true); cache1.Flush(); // Empties cache. // Create cache on top. { // Remove the nullifier. CCoinsViewCacheTest cache2(&cache1); checkNullifierCache(cache2, txWithNullifiers, true); cache1.SetNullifiers(txWithNullifiers.tx, false); cache2.Flush(); // Empties cache, flushes to cache1. } // The nullifier now should be `false`. checkNullifierCache(cache1, txWithNullifiers, false); } // Was broken: { SCOPED_TRACE("remove from child"); CCoinsViewTest base; CCoinsViewCacheTest cache1(&base); // Insert a nullifier into the base. TxWithNullifiers txWithNullifiers; cache1.SetNullifiers(txWithNullifiers.tx, true); cache1.Flush(); // Empties cache. // Create cache on top. { // Remove the nullifier. CCoinsViewCacheTest cache2(&cache1); cache2.SetNullifiers(txWithNullifiers.tx, false); cache2.Flush(); // Empties cache, flushes to cache1. } // The nullifier now should be `false`. checkNullifierCache(cache1, txWithNullifiers, false); } } template void anchorPopRegressionTestImpl(ShieldedType type) { // Correct behavior: { CCoinsViewTest base; CCoinsViewCacheTest cache1(&base); // Create dummy anchor/commitment Tree tree; AppendRandomLeaf(tree); // Add the anchor cache1.PushAnchor(tree); cache1.Flush(); // Remove the anchor cache1.PopAnchor(Tree::empty_root(), type); cache1.Flush(); // Add the anchor back cache1.PushAnchor(tree); cache1.Flush(); // The base contains the anchor, of course! { Tree checkTree; EXPECT_TRUE(GetAnchorAt(cache1, tree.root(), checkTree)); EXPECT_TRUE(checkTree.root() == tree.root()); } } // Previously incorrect behavior { CCoinsViewTest base; CCoinsViewCacheTest cache1(&base); // Create dummy anchor/commitment Tree tree; AppendRandomLeaf(tree); // Add the anchor and flush to disk cache1.PushAnchor(tree); cache1.Flush(); // Remove the anchor, but don't flush yet! cache1.PopAnchor(Tree::empty_root(), type); { CCoinsViewCacheTest cache2(&cache1); // Build cache on top cache2.PushAnchor(tree); // Put the same anchor back! cache2.Flush(); // Flush to cache1 } // cache2's flush kinda worked, i.e. cache1 thinks the // tree is there, but it didn't bring down the correct // treestate... { Tree checktree; EXPECT_TRUE(GetAnchorAt(cache1, tree.root(), checktree)); EXPECT_TRUE(checktree.root() == tree.root()); // Oh, shucks. } // Flushing cache won't help either, just makes the inconsistency // permanent. cache1.Flush(); { Tree checktree; EXPECT_TRUE(GetAnchorAt(cache1, tree.root(), checktree)); EXPECT_TRUE(checktree.root() == tree.root()); // Oh, shucks. } } } TEST(CoinsTests, AnchorPopRegression) { LoadProofParameters(); { SCOPED_TRACE("Sprout"); anchorPopRegressionTestImpl(SPROUT); } { SCOPED_TRACE("Sapling"); anchorPopRegressionTestImpl(SAPLING); } { SCOPED_TRACE("Orchard"); anchorPopRegressionTestImpl(ORCHARD); } } template void anchorRegressionTestImpl(ShieldedType type) { // Correct behavior: { CCoinsViewTest base; CCoinsViewCacheTest cache1(&base); // Insert anchor into base. Tree tree; AppendRandomLeaf(tree); cache1.PushAnchor(tree); cache1.Flush(); cache1.PopAnchor(Tree::empty_root(), type); EXPECT_TRUE(cache1.GetBestAnchor(type) == Tree::empty_root()); EXPECT_TRUE(!GetAnchorAt(cache1, tree.root(), tree)); } // Also correct behavior: { CCoinsViewTest base; CCoinsViewCacheTest cache1(&base); // Insert anchor into base. Tree tree; AppendRandomLeaf(tree); cache1.PushAnchor(tree); cache1.Flush(); cache1.PopAnchor(Tree::empty_root(), type); cache1.Flush(); EXPECT_TRUE(cache1.GetBestAnchor(type) == Tree::empty_root()); EXPECT_TRUE(!GetAnchorAt(cache1, tree.root(), tree)); } // Works because we bring the anchor in from parent cache. { CCoinsViewTest base; CCoinsViewCacheTest cache1(&base); // Insert anchor into base. Tree tree; AppendRandomLeaf(tree); cache1.PushAnchor(tree); cache1.Flush(); { // Pop anchor. CCoinsViewCacheTest cache2(&cache1); EXPECT_TRUE(GetAnchorAt(cache2, tree.root(), tree)); cache2.PopAnchor(Tree::empty_root(), type); cache2.Flush(); } EXPECT_TRUE(cache1.GetBestAnchor(type) == Tree::empty_root()); EXPECT_TRUE(!GetAnchorAt(cache1, tree.root(), tree)); } // Was broken: { CCoinsViewTest base; CCoinsViewCacheTest cache1(&base); // Insert anchor into base. Tree tree; AppendRandomLeaf(tree); cache1.PushAnchor(tree); cache1.Flush(); { // Pop anchor. CCoinsViewCacheTest cache2(&cache1); cache2.PopAnchor(Tree::empty_root(), type); cache2.Flush(); } EXPECT_TRUE(cache1.GetBestAnchor(type) == Tree::empty_root()); EXPECT_TRUE(!GetAnchorAt(cache1, tree.root(), tree)); } } TEST(CoinsTests, AnchorRegression) { LoadProofParameters(); { SCOPED_TRACE("Sprout"); anchorRegressionTestImpl(SPROUT); } { SCOPED_TRACE("Sapling"); anchorRegressionTestImpl(SAPLING); } { SCOPED_TRACE("Orchard"); anchorRegressionTestImpl(ORCHARD); } } TEST(CoinsTests, NullifiersTest) { CCoinsViewTest base; CCoinsViewCacheTest cache(&base); TxWithNullifiers txWithNullifiers; checkNullifierCache(cache, txWithNullifiers, false); cache.SetNullifiers(txWithNullifiers.tx, true); checkNullifierCache(cache, txWithNullifiers, true); cache.Flush(); CCoinsViewCacheTest cache2(&base); checkNullifierCache(cache2, txWithNullifiers, true); cache2.SetNullifiers(txWithNullifiers.tx, false); checkNullifierCache(cache2, txWithNullifiers, false); cache2.Flush(); CCoinsViewCacheTest cache3(&base); checkNullifierCache(cache3, txWithNullifiers, false); } template void anchorsFlushImpl(ShieldedType type) { CCoinsViewTest base; uint256 newrt; { CCoinsViewCacheTest cache(&base); Tree tree; EXPECT_TRUE(GetAnchorAt(cache, cache.GetBestAnchor(type), tree)); AppendRandomLeaf(tree); newrt = tree.root(); cache.PushAnchor(tree); cache.Flush(); } { CCoinsViewCacheTest cache(&base); Tree tree; EXPECT_TRUE(GetAnchorAt(cache, cache.GetBestAnchor(type), tree)); // Get the cached entry. EXPECT_TRUE(GetAnchorAt(cache, cache.GetBestAnchor(type), tree)); uint256 check_rt = tree.root(); EXPECT_TRUE(check_rt == newrt); } } TEST(CoinsTests,AnchorsFlushTest) { LoadProofParameters(); { SCOPED_TRACE("Sprout"); anchorsFlushImpl(SPROUT); } { SCOPED_TRACE("Sapling"); anchorsFlushImpl(SAPLING); } { SCOPED_TRACE("Orchard"); anchorsFlushImpl(ORCHARD); } } template void anchorsTestImpl(ShieldedType type) { // TODO: These tests should be more methodical. CCoinsViewTest base; CCoinsViewCacheTest cache(&base); EXPECT_TRUE(cache.GetBestAnchor(type) == Tree::empty_root()); { Tree tree; EXPECT_TRUE(GetAnchorAt(cache, cache.GetBestAnchor(type), tree)); EXPECT_TRUE(cache.GetBestAnchor(type) == tree.root()); AppendRandomLeaf(tree); AppendRandomLeaf(tree); AppendRandomLeaf(tree); AppendRandomLeaf(tree); AppendRandomLeaf(tree); AppendRandomLeaf(tree); AppendRandomLeaf(tree); Tree save_tree_for_later; save_tree_for_later = tree; uint256 newrt = tree.root(); uint256 newrt2; cache.PushAnchor(tree); EXPECT_TRUE(cache.GetBestAnchor(type) == newrt); { Tree confirm_same; EXPECT_TRUE(GetAnchorAt(cache, cache.GetBestAnchor(type), confirm_same)); EXPECT_TRUE(confirm_same.root() == newrt); } AppendRandomLeaf(tree); AppendRandomLeaf(tree); newrt2 = tree.root(); cache.PushAnchor(tree); EXPECT_TRUE(cache.GetBestAnchor(type) == newrt2); Tree test_tree; EXPECT_TRUE(GetAnchorAt(cache, cache.GetBestAnchor(type), test_tree)); EXPECT_TRUE(tree.root() == test_tree.root()); { Tree test_tree2; GetAnchorAt(cache, newrt, test_tree2); EXPECT_TRUE(test_tree2.root() == newrt); } { cache.PopAnchor(newrt, type); Tree obtain_tree; assert(!GetAnchorAt(cache, newrt2, obtain_tree)); // should have been popped off assert(GetAnchorAt(cache, newrt, obtain_tree)); assert(obtain_tree.root() == newrt); } } } TEST(CoinsTests, AnchorsTest) { { SCOPED_TRACE("Sprout"); anchorsTestImpl(SPROUT); } { SCOPED_TRACE("Sapling"); anchorsTestImpl(SAPLING); } { SCOPED_TRACE("Orchard"); anchorsTestImpl(ORCHARD); } }