diff --git a/src/gtest/test_noteencryption.cpp b/src/gtest/test_noteencryption.cpp index a674daf65..0ed6999f8 100644 --- a/src/gtest/test_noteencryption.cpp +++ b/src/gtest/test_noteencryption.cpp @@ -35,6 +35,11 @@ TEST(noteencryption, NotePlaintext) } SaplingNote note(addr, 39393); + auto cmu_opt = note.cm(); + if (!cmu_opt) { + FAIL(); + } + uint256 cmu = cmu_opt.get(); SaplingNotePlaintext pt(note, memo); auto res = pt.encrypt(addr.pk_d); @@ -48,11 +53,20 @@ TEST(noteencryption, NotePlaintext) auto encryptor = enc.second; auto epk = encryptor.get_epk(); - // Try to decrypt + // Try to decrypt with incorrect commitment + ASSERT_FALSE(SaplingNotePlaintext::decrypt( + ct, + ivk, + epk, + uint256() + )); + + // Try to decrypt with correct commitment auto foo = SaplingNotePlaintext::decrypt( ct, ivk, - epk + epk, + cmu ); if (!foo) { @@ -112,12 +126,24 @@ TEST(noteencryption, NotePlaintext) ASSERT_TRUE(decrypted_out_ct_unwrapped.pk_d == out_pt.pk_d); ASSERT_TRUE(decrypted_out_ct_unwrapped.esk == out_pt.esk); + // Test sender won't accept invalid commitments + ASSERT_FALSE( + SaplingNotePlaintext::decrypt( + ct, + epk, + decrypted_out_ct_unwrapped.esk, + decrypted_out_ct_unwrapped.pk_d, + uint256() + ) + ); + // Test sender can decrypt the note ciphertext. foo = SaplingNotePlaintext::decrypt( ct, epk, decrypted_out_ct_unwrapped.esk, - decrypted_out_ct_unwrapped.pk_d + decrypted_out_ct_unwrapped.pk_d, + cmu ); if (!foo) { diff --git a/src/gtest/test_transaction_builder.cpp b/src/gtest/test_transaction_builder.cpp index e3f3655d3..b8fe54466 100644 --- a/src/gtest/test_transaction_builder.cpp +++ b/src/gtest/test_transaction_builder.cpp @@ -56,7 +56,7 @@ TEST(TransactionBuilder, Invoke) // 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].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); diff --git a/src/wallet/gtest/test_wallet.cpp b/src/wallet/gtest/test_wallet.cpp index 4624a8579..d79b69ea0 100644 --- a/src/wallet/gtest/test_wallet.cpp +++ b/src/wallet/gtest/test_wallet.cpp @@ -4,9 +4,11 @@ #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" @@ -23,6 +25,8 @@ ACTION(ThrowLogicError) { throw std::logic_error("Boom"); } +static const std::string tSecretRegtest = "cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN"; + class MockWalletDB { public: MOCK_METHOD0(TxnBegin, bool()); @@ -131,14 +135,14 @@ std::pair GetWitnessesAndAnchors(TestWallet& wallet, return std::make_pair(sproutAnchor, saplingAnchor); } -TEST(wallet_tests, setup_datadir_location_run_as_first_test) { +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(wallet_tests, note_data_serialisation) { +TEST(WalletTests, SproutNoteDataSerialisation) { auto sk = libzcash::SproutSpendingKey::random(); auto wtx = GetValidReceive(sk, 10, true); auto note = GetNote(sk, wtx, 0, 1); @@ -162,7 +166,7 @@ TEST(wallet_tests, note_data_serialisation) { } -TEST(wallet_tests, find_unspent_notes) { +TEST(WalletTests, FindUnspentSproutNotes) { SelectParams(CBaseChainParams::TESTNET); CWallet wallet; auto sk = libzcash::SproutSpendingKey::random(); @@ -179,7 +183,7 @@ TEST(wallet_tests, find_unspent_notes) { wtx.SetSproutNoteData(noteData); wallet.AddToWallet(wtx, true, NULL); - EXPECT_FALSE(wallet.IsSpent(nullifier)); + EXPECT_FALSE(wallet.IsSproutSpent(nullifier)); // We currently have an unspent and unconfirmed note in the wallet (depth of -1) std::vector entries; @@ -204,7 +208,7 @@ TEST(wallet_tests, find_unspent_notes) { wtx.SetMerkleBranch(block); wallet.AddToWallet(wtx, true, NULL); - EXPECT_FALSE(wallet.IsSpent(nullifier)); + EXPECT_FALSE(wallet.IsSproutSpent(nullifier)); // We now have an unspent and confirmed note in the wallet (depth of 1) @@ -222,7 +226,7 @@ TEST(wallet_tests, find_unspent_notes) { // Let's spend the note. auto wtx2 = GetValidSpend(sk, note, 5); wallet.AddToWallet(wtx2, true, NULL); - EXPECT_FALSE(wallet.IsSpent(nullifier)); + EXPECT_FALSE(wallet.IsSproutSpent(nullifier)); // Fake-mine a spend transaction EXPECT_EQ(0, chainActive.Height()); @@ -240,7 +244,7 @@ TEST(wallet_tests, find_unspent_notes) { wtx2.SetMerkleBranch(block2); wallet.AddToWallet(wtx2, true, NULL); - EXPECT_TRUE(wallet.IsSpent(nullifier)); + EXPECT_TRUE(wallet.IsSproutSpent(nullifier)); // The note has been spent. By default, GetFilteredNotes() ignores spent notes. wallet.GetFilteredNotes(entries, "", 0); @@ -274,7 +278,7 @@ TEST(wallet_tests, find_unspent_notes) { wtx.SetSproutNoteData(noteData); wallet.AddToWallet(wtx, true, NULL); - EXPECT_FALSE(wallet.IsSpent(nullifier)); + EXPECT_FALSE(wallet.IsSproutSpent(nullifier)); wtx3 = wtx; } @@ -321,7 +325,7 @@ TEST(wallet_tests, find_unspent_notes) { } -TEST(wallet_tests, set_note_addrs_in_cwallettx) { +TEST(WalletTests, SetSproutNoteAddrsInCWalletTx) { auto sk = libzcash::SproutSpendingKey::random(); auto wtx = GetValidReceive(sk, 10, true); auto note = GetNote(sk, wtx, 0, 1); @@ -337,7 +341,67 @@ TEST(wallet_tests, set_note_addrs_in_cwallettx) { EXPECT_EQ(noteData, wtx.mapSproutNoteData); } -TEST(wallet_tests, set_invalid_note_addrs_in_cwallettx) { +TEST(WalletTests, SetSaplingNoteAddrsInCWalletTx) { + 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; + + auto sk = libzcash::SaplingSpendingKey::random(); + auto expsk = sk.expanded_spending_key(); + auto fvk = sk.full_viewing_key(); + auto pk = sk.default_address(); + auto ivk = fvk.in_viewing_key(); + + libzcash::SaplingNote note(pk, 50000); + auto cm = note.cm().get(); + SaplingMerkleTree tree; + tree.append(cm); + auto anchor = tree.root(); + auto witness = tree.witness(); + + auto nf = note.nullifier(fvk, witness.position()); + ASSERT_TRUE(nf); + uint256 nullifier = nf.get(); + + auto builder = TransactionBuilder(consensusParams, 1); + ASSERT_TRUE(builder.AddSaplingSpend(expsk, note, anchor, witness)); + builder.AddSaplingOutput(fvk, pk, 50000, {}); + builder.SetFee(0); + auto maybe_tx = builder.Build(); + ASSERT_EQ(static_cast(maybe_tx), true); + auto tx = maybe_tx.get(); + + CWalletTx wtx {&wallet, tx}; + + EXPECT_EQ(0, wtx.mapSaplingNoteData.size()); + mapSaplingNoteData_t noteData; + + SaplingOutPoint op {wtx.GetHash(), 0}; + SaplingNoteData nd; + nd.nullifier = nullifier; + nd.ivk = ivk; + nd.witnesses.push_front(witness); + nd.witnessHeight = 123; + noteData.insert(std::make_pair(op, nd)); + + wtx.SetSaplingNoteData(noteData); + EXPECT_EQ(noteData, wtx.mapSaplingNoteData); + + // Test individual fields in case equality operator is defined/changed. + EXPECT_EQ(ivk, wtx.mapSaplingNoteData[op].ivk); + EXPECT_EQ(nullifier, wtx.mapSaplingNoteData[op].nullifier); + EXPECT_EQ(nd.witnessHeight, wtx.mapSaplingNoteData[op].witnessHeight); + EXPECT_TRUE(witness == wtx.mapSaplingNoteData[op].witnesses.front()); + + // Revert to default + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_SAPLING, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); + UpdateNetworkUpgradeParameters(Consensus::UPGRADE_OVERWINTER, Consensus::NetworkUpgrade::NO_ACTIVATION_HEIGHT); +} + +TEST(WalletTests, SetSproutInvalidNoteAddrsInCWalletTx) { CWalletTx wtx; EXPECT_EQ(0, wtx.mapSproutNoteData.size()); @@ -350,7 +414,23 @@ TEST(wallet_tests, set_invalid_note_addrs_in_cwallettx) { EXPECT_THROW(wtx.SetSproutNoteData(noteData), std::logic_error); } -TEST(wallet_tests, GetNoteNullifier) { +// The following test is the same as SetInvalidSaplingNoteDataInCWalletTx +// TEST(WalletTests, SetSaplingInvalidNoteAddrsInCWalletTx) + +// Cannot add note data for an index which does not exist in tx.vShieldedOutput +TEST(WalletTests, SetInvalidSaplingNoteDataInCWalletTx) { + CWalletTx wtx; + EXPECT_EQ(0, wtx.mapSaplingNoteData.size()); + + mapSaplingNoteData_t noteData; + SaplingOutPoint op {uint256(), 1}; + SaplingNoteData nd; + noteData.insert(std::make_pair(op, nd)); + + EXPECT_THROW(wtx.SetSaplingNoteData(noteData), std::logic_error); +} + +TEST(WalletTests, GetSproutNoteNullifier) { CWallet wallet; auto sk = libzcash::SproutSpendingKey::random(); @@ -364,7 +444,7 @@ TEST(wallet_tests, GetNoteNullifier) { auto hSig = wtx.vjoinsplit[0].h_sig( *params, wtx.joinSplitPubKey); - auto ret = wallet.GetNoteNullifier( + auto ret = wallet.GetSproutNoteNullifier( wtx.vjoinsplit[0], address, dec, @@ -373,7 +453,7 @@ TEST(wallet_tests, GetNoteNullifier) { wallet.AddSproutSpendingKey(sk); - ret = wallet.GetNoteNullifier( + ret = wallet.GetSproutNoteNullifier( wtx.vjoinsplit[0], address, dec, @@ -381,7 +461,54 @@ TEST(wallet_tests, GetNoteNullifier) { EXPECT_EQ(nullifier, ret); } -TEST(wallet_tests, FindMyNotes) { +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 + auto sk = libzcash::SaplingSpendingKey::random(); + auto expsk = sk.expanded_spending_key(); + auto fvk = sk.full_viewing_key(); + auto pk = sk.default_address(); + + // 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, 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); + EXPECT_EQ(0, noteMap.size()); + + // Add spending key to wallet, so Sapling notes can be found + ASSERT_TRUE(wallet.AddSaplingZKey(sk)); + ASSERT_TRUE(wallet.HaveSaplingSpendingKey(fvk)); + noteMap = wallet.FindMySaplingNotes(wtx); + 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, FindMySproutNotes) { CWallet wallet; auto sk = libzcash::SproutSpendingKey::random(); @@ -392,12 +519,12 @@ TEST(wallet_tests, FindMyNotes) { auto note = GetNote(sk, wtx, 0, 1); auto nullifier = note.nullifier(sk); - auto noteMap = wallet.FindMyNotes(wtx); + auto noteMap = wallet.FindMySproutNotes(wtx); EXPECT_EQ(0, noteMap.size()); wallet.AddSproutSpendingKey(sk); - noteMap = wallet.FindMyNotes(wtx); + noteMap = wallet.FindMySproutNotes(wtx); EXPECT_EQ(2, noteMap.size()); JSOutPoint jsoutpt {wtx.GetHash(), 0, 1}; @@ -406,7 +533,7 @@ TEST(wallet_tests, FindMyNotes) { EXPECT_EQ(nd, noteMap[jsoutpt]); } -TEST(wallet_tests, FindMyNotesInEncryptedWallet) { +TEST(WalletTests, FindMySproutNotesInEncryptedWallet) { TestWallet wallet; uint256 r {GetRandHash()}; CKeyingMaterial vMasterKey (r.begin(), r.end()); @@ -420,7 +547,7 @@ TEST(wallet_tests, FindMyNotesInEncryptedWallet) { auto note = GetNote(sk, wtx, 0, 1); auto nullifier = note.nullifier(sk); - auto noteMap = wallet.FindMyNotes(wtx); + auto noteMap = wallet.FindMySproutNotes(wtx); EXPECT_EQ(2, noteMap.size()); JSOutPoint jsoutpt {wtx.GetHash(), 0, 1}; @@ -430,13 +557,13 @@ TEST(wallet_tests, FindMyNotesInEncryptedWallet) { ASSERT_TRUE(wallet.Unlock(vMasterKey)); - noteMap = wallet.FindMyNotes(wtx); + noteMap = wallet.FindMySproutNotes(wtx); EXPECT_EQ(2, noteMap.size()); EXPECT_EQ(1, noteMap.count(jsoutpt)); EXPECT_EQ(nd, noteMap[jsoutpt]); } -TEST(wallet_tests, get_conflicted_notes) { +TEST(WalletTests, GetConflictedSproutNotes) { CWallet wallet; auto sk = libzcash::SproutSpendingKey::random(); @@ -467,7 +594,135 @@ TEST(wallet_tests, get_conflicted_notes) { EXPECT_EQ(std::set({hash2, hash3}), c3); } -TEST(wallet_tests, nullifier_is_spent) { +// 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 + auto sk = libzcash::SaplingSpendingKey::random(); + auto expsk = sk.expanded_spending_key(); + auto fvk = sk.full_viewing_key(); + auto ivk = fvk.in_viewing_key(); + auto pk = sk.default_address(); + + ASSERT_TRUE(wallet.AddSaplingZKey(sk)); + 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, 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); + ASSERT_TRUE(saplingNoteData.size() > 0); + wtx.SetSaplingNoteData(saplingNoteData); + wtx.SetMerkleBranch(block); + wallet.AddToWallet(wtx, true, NULL); + + // Simulate receiving new block and ChainTip signal + wallet.IncrementNoteWitnesses(&fakeIndex, &block, sproutTree, saplingTree); + wallet.UpdateSaplingNullifierNoteMapForBlock(&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, 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, 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(); @@ -477,14 +732,14 @@ TEST(wallet_tests, nullifier_is_spent) { auto note = GetNote(sk, wtx, 0, 1); auto nullifier = note.nullifier(sk); - EXPECT_FALSE(wallet.IsSpent(nullifier)); + EXPECT_FALSE(wallet.IsSproutSpent(nullifier)); wallet.AddToWallet(wtx, true, NULL); - EXPECT_FALSE(wallet.IsSpent(nullifier)); + EXPECT_FALSE(wallet.IsSproutSpent(nullifier)); auto wtx2 = GetValidSpend(sk, note, 5); wallet.AddToWallet(wtx2, true, NULL); - EXPECT_FALSE(wallet.IsSpent(nullifier)); + EXPECT_FALSE(wallet.IsSproutSpent(nullifier)); // Fake-mine the transaction EXPECT_EQ(-1, chainActive.Height()); @@ -500,14 +755,83 @@ TEST(wallet_tests, nullifier_is_spent) { wtx2.SetMerkleBranch(block); wallet.AddToWallet(wtx2, true, NULL); - EXPECT_TRUE(wallet.IsSpent(nullifier)); + EXPECT_TRUE(wallet.IsSproutSpent(nullifier)); // Tear down chainActive.SetTip(NULL); mapBlockIndex.erase(blockHash); } -TEST(wallet_tests, navigate_from_nullifier_to_note) { +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 + auto sk = libzcash::SaplingSpendingKey::random(); + auto expsk = sk.expanded_spending_key(); + auto fvk = sk.full_viewing_key(); + auto pk = sk.default_address(); + + // 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, 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)); + 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(); @@ -524,16 +848,120 @@ TEST(wallet_tests, navigate_from_nullifier_to_note) { wtx.SetSproutNoteData(noteData); - EXPECT_EQ(0, wallet.mapNullifiersToNotes.count(nullifier)); + EXPECT_EQ(0, wallet.mapSproutNullifiersToNotes.count(nullifier)); wallet.AddToWallet(wtx, true, NULL); - EXPECT_EQ(1, wallet.mapNullifiersToNotes.count(nullifier)); - EXPECT_EQ(wtx.GetHash(), wallet.mapNullifiersToNotes[nullifier].hash); - EXPECT_EQ(0, wallet.mapNullifiersToNotes[nullifier].js); - EXPECT_EQ(1, wallet.mapNullifiersToNotes[nullifier].n); + 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(wallet_tests, spent_note_is_from_me) { +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 + auto sk = libzcash::SaplingSpendingKey::random(); + auto expsk = sk.expanded_spending_key(); + auto fvk = sk.full_viewing_key(); + auto pk = sk.default_address(); + + // 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, 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)); + 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); + 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.IncrementNoteWitnesses(&fakeIndex, &block, sproutTree, saplingTree); + wallet.UpdateSaplingNullifierNoteMapForBlock(&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(); @@ -561,7 +989,156 @@ TEST(wallet_tests, spent_note_is_from_me) { EXPECT_TRUE(wallet.IsFromMe(wtx2)); } -TEST(wallet_tests, cached_witnesses_empty_chain) { +// 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 + auto sk = libzcash::SaplingSpendingKey::random(); + auto expsk = sk.expanded_spending_key(); + auto fvk = sk.full_viewing_key(); + auto ivk = fvk.in_viewing_key(); + auto pk = sk.default_address(); + + // 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, 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)); + 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); + 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.IncrementNoteWitnesses(&fakeIndex, &block, sproutTree, saplingTree); + wallet.UpdateSaplingNullifierNoteMapForBlock(&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, 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.nHeight = 1; + chainActive.SetTip(&fakeIndex2); + EXPECT_TRUE(chainActive.Contains(&fakeIndex2)); + EXPECT_EQ(1, chainActive.Height()); + + auto saplingNoteData2 = wallet.FindMySaplingNotes(wtx2); + 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(); @@ -620,7 +1197,7 @@ TEST(wallet_tests, cached_witnesses_empty_chain) { ".*nWitnessCacheSize > 0.*"); } -TEST(wallet_tests, cached_witnesses_chain_tip) { +TEST(WalletTests, CachedWitnessesChainTip) { TestWallet wallet; std::pair anchors1; CBlock block1; @@ -722,7 +1299,7 @@ TEST(wallet_tests, cached_witnesses_chain_tip) { } } -TEST(wallet_tests, CachedWitnessesDecrementFirst) { +TEST(WalletTests, CachedWitnessesDecrementFirst) { TestWallet wallet; SproutMerkleTree sproutTree; SaplingMerkleTree saplingTree; @@ -802,7 +1379,7 @@ TEST(wallet_tests, CachedWitnessesDecrementFirst) { } } -TEST(wallet_tests, CachedWitnessesCleanIndex) { +TEST(WalletTests, CachedWitnessesCleanIndex) { TestWallet wallet; std::vector blocks; std::vector indices; @@ -889,7 +1466,7 @@ TEST(wallet_tests, CachedWitnessesCleanIndex) { } } -TEST(wallet_tests, ClearNoteWitnessCache) { +TEST(WalletTests, ClearNoteWitnessCache) { TestWallet wallet; auto sk = libzcash::SproutSpendingKey::random(); @@ -937,7 +1514,7 @@ TEST(wallet_tests, ClearNoteWitnessCache) { // After clearing, we should not have a witness for either note wallet.ClearNoteWitnessCache(); - auto anchros2 = GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses); + auto anchors2 = GetWitnessesAndAnchors(wallet, sproutNotes, saplingNotes, sproutWitnesses, saplingWitnesses); EXPECT_FALSE((bool) sproutWitnesses[0]); EXPECT_FALSE((bool) sproutWitnesses[1]); EXPECT_FALSE((bool) saplingWitnesses[0]); @@ -947,7 +1524,7 @@ TEST(wallet_tests, ClearNoteWitnessCache) { EXPECT_EQ(0, wallet.nWitnessCacheSize); } -TEST(wallet_tests, WriteWitnessCache) { +TEST(WalletTests, WriteWitnessCache) { TestWallet wallet; MockWalletDB walletdb; CBlockLocator loc; @@ -1024,7 +1601,7 @@ TEST(wallet_tests, WriteWitnessCache) { wallet.SetBestChain(walletdb, loc); } -TEST(wallet_tests, UpdateNullifierNoteMap) { +TEST(WalletTests, UpdateSproutNullifierNoteMap) { TestWallet wallet; uint256 r {GetRandHash()}; CKeyingMaterial vMasterKey (r.begin(), r.end()); @@ -1038,7 +1615,7 @@ TEST(wallet_tests, UpdateNullifierNoteMap) { auto note = GetNote(sk, wtx, 0, 1); auto nullifier = note.nullifier(sk); - // Pretend that we called FindMyNotes while the wallet was locked + // Pretend that we called FindMySproutNotes while the wallet was locked mapSproutNoteData_t noteData; JSOutPoint jsoutpt {wtx.GetHash(), 0, 1}; SproutNoteData nd {sk.address()}; @@ -1046,20 +1623,20 @@ TEST(wallet_tests, UpdateNullifierNoteMap) { wtx.SetSproutNoteData(noteData); wallet.AddToWallet(wtx, true, NULL); - EXPECT_EQ(0, wallet.mapNullifiersToNotes.count(nullifier)); + EXPECT_EQ(0, wallet.mapSproutNullifiersToNotes.count(nullifier)); EXPECT_FALSE(wallet.UpdateNullifierNoteMap()); ASSERT_TRUE(wallet.Unlock(vMasterKey)); EXPECT_TRUE(wallet.UpdateNullifierNoteMap()); - EXPECT_EQ(1, wallet.mapNullifiersToNotes.count(nullifier)); - EXPECT_EQ(wtx.GetHash(), wallet.mapNullifiersToNotes[nullifier].hash); - EXPECT_EQ(0, wallet.mapNullifiersToNotes[nullifier].js); - EXPECT_EQ(1, wallet.mapNullifiersToNotes[nullifier].n); + 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(wallet_tests, UpdatedNoteData) { +TEST(WalletTests, UpdatedSproutNoteData) { TestWallet wallet; auto sk = libzcash::SproutSpendingKey::random(); @@ -1106,7 +1683,126 @@ TEST(wallet_tests, UpdatedNoteData) { // TODO: The new note should get witnessed (but maybe not here) (#1350) } -TEST(wallet_tests, MarkAffectedTransactionsDirty) { +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; + + // Generate dummy Sapling address + auto sk = libzcash::SaplingSpendingKey::random(); + auto expsk = sk.expanded_spending_key(); + auto fvk = sk.full_viewing_key(); + auto pk = sk.default_address(); + + // Generate dummy recipient Sapling address + auto sk2 = libzcash::SaplingSpendingKey::random(); + auto fvk2 = sk2.full_viewing_key(); + auto pk2 = sk2.default_address(); + + // 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, 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)); + 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); + 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.IncrementNoteWitnesses(&fakeIndex, &block, sproutTree, saplingTree); + wallet.UpdateSaplingNullifierNoteMapForBlock(&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)); + ASSERT_TRUE(wallet.HaveSaplingSpendingKey(fvk2)); + CWalletTx wtx2 = wtx; + auto saplingNoteData2 = wallet.FindMySaplingNotes(wtx2); + 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 incrementnotewitness 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, MarkAffectedSproutTransactionsDirty) { TestWallet wallet; auto sk = libzcash::SproutSpendingKey::random(); @@ -1137,7 +1833,127 @@ TEST(wallet_tests, MarkAffectedTransactionsDirty) { EXPECT_FALSE(wallet.mapWallet[hash].fDebitCached); } -TEST(wallet_tests, NoteLocking) { +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 + auto sk = libzcash::SaplingSpendingKey::random(); + auto expsk = sk.expanded_spending_key(); + auto fvk = sk.full_viewing_key(); + auto ivk = fvk.in_viewing_key(); + auto pk = sk.default_address(); + + ASSERT_TRUE(wallet.AddSaplingZKey(sk)); + 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-ZEC in, 0.0004 z-ZEC out, 0.0001 t-ZEC fee + auto builder = TransactionBuilder(consensusParams, 1, &keystore); + builder.AddTransparentInput(COutPoint(), scriptPubKey, 50000); + builder.AddSaplingOutput(fvk, 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); + ASSERT_TRUE(saplingNoteData.size() > 0); + wtx.SetSaplingNoteData(saplingNoteData); + wtx.SetMerkleBranch(block); + wallet.AddToWallet(wtx, true, NULL); + + // Simulate receiving new block and ChainTip signal + wallet.IncrementNoteWitnesses(&fakeIndex, &block, sproutTree, saplingTree); + wallet.UpdateSaplingNullifierNoteMapForBlock(&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, 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, SproutNoteLocking) { TestWallet wallet; auto sk = libzcash::SproutSpendingKey::random(); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 675185af2..90d6a72e0 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -477,6 +477,7 @@ void CWallet::ChainTip(const CBlockIndex *pindex, } else { DecrementNoteWitnesses(pindex); } + UpdateSaplingNullifierNoteMapForBlock(pblock); } void CWallet::SetBestChain(const CBlockLocator& loc) @@ -583,15 +584,28 @@ set CWallet::GetConflicts(const uint256& txid) const for (const JSDescription& jsdesc : wtx.vjoinsplit) { for (const uint256& nullifier : jsdesc.nullifiers) { - if (mapTxNullifiers.count(nullifier) <= 1) { + if (mapTxSproutNullifiers.count(nullifier) <= 1) { continue; // No conflict if zero or one spends } - range_n = mapTxNullifiers.equal_range(nullifier); + range_n = mapTxSproutNullifiers.equal_range(nullifier); for (TxNullifiers::const_iterator it = range_n.first; it != range_n.second; ++it) { result.insert(it->second); } } } + + std::pair range_o; + + for (const SpendDescription &spend : wtx.vShieldedSpend) { + uint256 nullifier = spend.nullifier; + if (mapTxSaplingNullifiers.count(nullifier) <= 1) { + continue; // No conflict if zero or one spends + } + range_o = mapTxSaplingNullifiers.equal_range(nullifier); + for (TxNullifiers::const_iterator it = range_o.first; it != range_o.second; ++it) { + result.insert(it->second); + } + } return result; } @@ -673,7 +687,7 @@ void CWallet::SyncMetaData(pair::iterator, typename TxSpe CWalletTx* copyTo = &mapWallet[hash]; if (copyFrom == copyTo) continue; copyTo->mapValue = copyFrom->mapValue; - // mapSproutNoteData not copied on purpose + // mapSproutNoteData and mapSaplingNoteData not copied on purpose // (it is always set correctly for each CWalletTx) copyTo->vOrderForm = copyFrom->vOrderForm; // fTimeReceivedIsTxTime not copied on purpose @@ -710,10 +724,9 @@ bool CWallet::IsSpent(const uint256& hash, unsigned int n) const * Note is spent if any non-conflicted transaction * spends it: */ -bool CWallet::IsSpent(const uint256& nullifier) const -{ +bool CWallet::IsSproutSpent(const uint256& nullifier) const { pair range; - range = mapTxNullifiers.equal_range(nullifier); + range = mapTxSproutNullifiers.equal_range(nullifier); for (TxNullifiers::const_iterator it = range.first; it != range.second; ++it) { const uint256& wtxid = it->second; @@ -725,7 +738,21 @@ bool CWallet::IsSpent(const uint256& nullifier) const return false; } -void CWallet::AddToSpends(const COutPoint& outpoint, const uint256& wtxid) +bool CWallet::IsSaplingSpent(const uint256& nullifier) const { + pair range; + range = mapTxSaplingNullifiers.equal_range(nullifier); + + for (TxNullifiers::const_iterator it = range.first; it != range.second; ++it) { + const uint256& wtxid = it->second; + std::map::const_iterator mit = mapWallet.find(wtxid); + if (mit != mapWallet.end() && mit->second.GetDepthInMainChain() >= 0) { + return true; // Spent + } + } + return false; +} + +void CWallet::AddToTransparentSpends(const COutPoint& outpoint, const uint256& wtxid) { mapTxSpends.insert(make_pair(outpoint, wtxid)); @@ -734,12 +761,21 @@ void CWallet::AddToSpends(const COutPoint& outpoint, const uint256& wtxid) SyncMetaData(range); } -void CWallet::AddToSpends(const uint256& nullifier, const uint256& wtxid) +void CWallet::AddToSproutSpends(const uint256& nullifier, const uint256& wtxid) { - mapTxNullifiers.insert(make_pair(nullifier, wtxid)); + mapTxSproutNullifiers.insert(make_pair(nullifier, wtxid)); pair range; - range = mapTxNullifiers.equal_range(nullifier); + range = mapTxSproutNullifiers.equal_range(nullifier); + SyncMetaData(range); +} + +void CWallet::AddToSaplingSpends(const uint256& nullifier, const uint256& wtxid) +{ + mapTxSaplingNullifiers.insert(make_pair(nullifier, wtxid)); + + pair range; + range = mapTxSaplingNullifiers.equal_range(nullifier); SyncMetaData(range); } @@ -751,13 +787,16 @@ void CWallet::AddToSpends(const uint256& wtxid) return; for (const CTxIn& txin : thisTx.vin) { - AddToSpends(txin.prevout, wtxid); + AddToTransparentSpends(txin.prevout, wtxid); } for (const JSDescription& jsdesc : thisTx.vjoinsplit) { for (const uint256& nullifier : jsdesc.nullifiers) { - AddToSpends(nullifier, wtxid); + AddToSproutSpends(nullifier, wtxid); } } + for (const SpendDescription &spend : thisTx.vShieldedSpend) { + AddToSaplingSpends(spend.nullifier, wtxid); + } } void CWallet::ClearNoteWitnessCache() @@ -1148,7 +1187,7 @@ bool CWallet::UpdateNullifierNoteMap() auto i = item.first.js; auto hSig = wtxItem.second.vjoinsplit[i].h_sig( *pzcashParams, wtxItem.second.joinSplitPubKey); - item.second.nullifier = GetNoteNullifier( + item.second.nullifier = GetSproutNoteNullifier( wtxItem.second.vjoinsplit[i], item.second.address, dec, @@ -1157,6 +1196,10 @@ bool CWallet::UpdateNullifierNoteMap() } } } + + // TODO: Sapling. This method is only called from RPC walletpassphrase, which is currently unsupported + // as RPC encryptwallet is hidden behind two flags: -developerencryptwallet -experimentalfeatures + UpdateNullifierNoteMapWithTx(wtxItem.second); } } @@ -1164,7 +1207,8 @@ bool CWallet::UpdateNullifierNoteMap() } /** - * Update mapNullifiersToNotes with the cached nullifiers in this tx. + * Update mapSproutNullifiersToNotes and mapSaplingNullifiersToNotes + * with the cached nullifiers in this tx. */ void CWallet::UpdateNullifierNoteMapWithTx(const CWalletTx& wtx) { @@ -1172,9 +1216,74 @@ void CWallet::UpdateNullifierNoteMapWithTx(const CWalletTx& wtx) LOCK(cs_wallet); for (const mapSproutNoteData_t::value_type& item : wtx.mapSproutNoteData) { if (item.second.nullifier) { - mapNullifiersToNotes[*item.second.nullifier] = item.first; + mapSproutNullifiersToNotes[*item.second.nullifier] = item.first; } } + + for (const mapSaplingNoteData_t::value_type& item : wtx.mapSaplingNoteData) { + if (item.second.nullifier) { + mapSaplingNullifiersToNotes[*item.second.nullifier] = item.first; + } + } + } +} + +/** + * Update mapSaplingNullifiersToNotes, computing the nullifier from a cached witness if necessary. + */ +void CWallet::UpdateSaplingNullifierNoteMapWithTx(CWalletTx& wtx) { + LOCK(cs_wallet); + + for (mapSaplingNoteData_t::value_type &item : wtx.mapSaplingNoteData) { + SaplingOutPoint op = item.first; + SaplingNoteData nd = item.second; + + if (nd.witnesses.empty()) { + // If there are no witnesses, erase the nullifier and associated mapping. + if (item.second.nullifier) { + mapSaplingNullifiersToNotes.erase(item.second.nullifier.get()); + } + item.second.nullifier = boost::none; + } + else { + uint64_t position = nd.witnesses.front().position(); + SaplingFullViewingKey fvk = mapSaplingFullViewingKeys.at(nd.ivk); + OutputDescription output = wtx.vShieldedOutput[op.n]; + auto optPlaintext = SaplingNotePlaintext::decrypt(output.encCiphertext, nd.ivk, output.ephemeralKey, output.cm); + if (!optPlaintext) { + // An item in mapSaplingNoteData must have already been successfully decrypted, + // otherwise the item would not exist in the first place. + assert(false); + } + auto optNote = optPlaintext.get().note(nd.ivk); + if (!optNote) { + assert(false); + } + auto optNullifier = optNote.get().nullifier(fvk, position); + if (!optNullifier) { + // This should not happen. If it does, maybe the position has been corrupted or miscalculated? + assert(false); + } + uint256 nullifier = optNullifier.get(); + mapSaplingNullifiersToNotes[nullifier] = op; + item.second.nullifier = nullifier; + } + } +} + +/** + * Iterate over transactions in a block and update the cached Sapling nullifiers + * for transactions which belong to the wallet. + */ +void CWallet::UpdateSaplingNullifierNoteMapForBlock(const CBlock *pblock) { + LOCK(cs_wallet); + + for (const CTransaction& tx : pblock->vtx) { + auto hash = tx.GetHash(); + bool txIsOurs = mapWallet.count(hash); + if (txIsOurs) { + UpdateSaplingNullifierNoteMapWithTx(mapWallet[hash]); + } } } @@ -1305,21 +1414,39 @@ bool CWallet::AddToWallet(const CWalletTx& wtxIn, bool fFromLoadWallet, CWalletD bool CWallet::UpdatedNoteData(const CWalletTx& wtxIn, CWalletTx& wtx) { - if (wtxIn.mapSproutNoteData.empty() || wtxIn.mapSproutNoteData == wtx.mapSproutNoteData) { - return false; - } - auto tmp = wtxIn.mapSproutNoteData; - // Ensure we keep any cached witnesses we may already have - for (const std::pair nd : wtx.mapSproutNoteData) { - if (tmp.count(nd.first) && nd.second.witnesses.size() > 0) { - tmp.at(nd.first).witnesses.assign( - nd.second.witnesses.cbegin(), nd.second.witnesses.cend()); + bool unchangedSproutFlag = (wtxIn.mapSproutNoteData.empty() || wtxIn.mapSproutNoteData == wtx.mapSproutNoteData); + if (!unchangedSproutFlag) { + auto tmp = wtxIn.mapSproutNoteData; + // Ensure we keep any cached witnesses we may already have + for (const std::pair nd : wtx.mapSproutNoteData) { + if (tmp.count(nd.first) && nd.second.witnesses.size() > 0) { + tmp.at(nd.first).witnesses.assign( + nd.second.witnesses.cbegin(), nd.second.witnesses.cend()); + } + tmp.at(nd.first).witnessHeight = nd.second.witnessHeight; } - tmp.at(nd.first).witnessHeight = nd.second.witnessHeight; + // Now copy over the updated note data + wtx.mapSproutNoteData = tmp; } - // Now copy over the updated note data - wtx.mapSproutNoteData = tmp; - return true; + + bool unchangedSaplingFlag = (wtxIn.mapSaplingNoteData.empty() || wtxIn.mapSaplingNoteData == wtx.mapSaplingNoteData); + if (!unchangedSaplingFlag) { + auto tmp = wtxIn.mapSaplingNoteData; + // Ensure we keep any cached witnesses we may already have + + for (const std::pair nd : wtx.mapSaplingNoteData) { + if (tmp.count(nd.first) && nd.second.witnesses.size() > 0) { + tmp.at(nd.first).witnesses.assign( + nd.second.witnesses.cbegin(), nd.second.witnesses.cend()); + } + tmp.at(nd.first).witnessHeight = nd.second.witnessHeight; + } + + // Now copy over the updated note data + wtx.mapSaplingNoteData = tmp; + } + + return !unchangedSproutFlag || !unchangedSaplingFlag; } /** @@ -1333,15 +1460,19 @@ bool CWallet::AddToWalletIfInvolvingMe(const CTransaction& tx, const CBlock* pbl AssertLockHeld(cs_wallet); bool fExisted = mapWallet.count(tx.GetHash()) != 0; if (fExisted && !fUpdate) return false; - auto noteData = FindMyNotes(tx); - if (fExisted || IsMine(tx) || IsFromMe(tx) || noteData.size() > 0) + auto sproutNoteData = FindMySproutNotes(tx); + auto saplingNoteData = FindMySaplingNotes(tx); + if (fExisted || IsMine(tx) || IsFromMe(tx) || sproutNoteData.size() > 0 || saplingNoteData.size() > 0) { CWalletTx wtx(this,tx); - if (noteData.size() > 0) { - wtx.SetSproutNoteData(noteData); + if (sproutNoteData.size() > 0) { + wtx.SetSproutNoteData(sproutNoteData); + } + + if (saplingNoteData.size() > 0) { + wtx.SetSaplingNoteData(saplingNoteData); } - // TODO: Sapling note data // Get merkle branch if transaction was found in a block if (pblock) @@ -1378,12 +1509,20 @@ void CWallet::MarkAffectedTransactionsDirty(const CTransaction& tx) } for (const JSDescription& jsdesc : tx.vjoinsplit) { for (const uint256& nullifier : jsdesc.nullifiers) { - if (mapNullifiersToNotes.count(nullifier) && - mapWallet.count(mapNullifiersToNotes[nullifier].hash)) { - mapWallet[mapNullifiersToNotes[nullifier].hash].MarkDirty(); + if (mapSproutNullifiersToNotes.count(nullifier) && + mapWallet.count(mapSproutNullifiersToNotes[nullifier].hash)) { + mapWallet[mapSproutNullifiersToNotes[nullifier].hash].MarkDirty(); } } } + + for (const SpendDescription &spend : tx.vShieldedSpend) { + uint256 nullifier = spend.nullifier; + if (mapSaplingNullifiersToNotes.count(nullifier) && + mapWallet.count(mapSaplingNullifiersToNotes[nullifier].hash)) { + mapWallet[mapSaplingNullifiersToNotes[nullifier].hash].MarkDirty(); + } + } } void CWallet::EraseFromWallet(const uint256 &hash) @@ -1403,11 +1542,11 @@ void CWallet::EraseFromWallet(const uint256 &hash) * Returns a nullifier if the SpendingKey is available * Throws std::runtime_error if the decryptor doesn't match this note */ -boost::optional CWallet::GetNoteNullifier(const JSDescription& jsdesc, - const libzcash::SproutPaymentAddress& address, - const ZCNoteDecryption& dec, - const uint256& hSig, - uint8_t n) const +boost::optional CWallet::GetSproutNoteNullifier(const JSDescription &jsdesc, + const libzcash::SproutPaymentAddress &address, + const ZCNoteDecryption &dec, + const uint256 &hSig, + uint8_t n) const { boost::optional ret; auto note_pt = libzcash::SproutNotePlaintext::decrypt( @@ -1432,10 +1571,10 @@ boost::optional CWallet::GetNoteNullifier(const JSDescription& jsdesc, * PaymentAddresses in this wallet. * * It should never be necessary to call this method with a CWalletTx, because - * the result of FindMyNotes (for the addresses available at the time) will + * the result of FindMySproutNotes (for the addresses available at the time) will * already have been cached in CWalletTx.mapSproutNoteData. */ -mapSproutNoteData_t CWallet::FindMyNotes(const CTransaction& tx) const +mapSproutNoteData_t CWallet::FindMySproutNotes(const CTransaction &tx) const { LOCK(cs_SpendingKeyStore); uint256 hash = tx.GetHash(); @@ -1448,7 +1587,7 @@ mapSproutNoteData_t CWallet::FindMyNotes(const CTransaction& tx) const try { auto address = item.first; JSOutPoint jsoutpt {hash, i, j}; - auto nullifier = GetNoteNullifier( + auto nullifier = GetSproutNoteNullifier( tx.vjoinsplit[i], address, item.second, @@ -1465,7 +1604,7 @@ mapSproutNoteData_t CWallet::FindMyNotes(const CTransaction& tx) const // Couldn't decrypt with this decryptor } catch (const std::exception &exc) { // Unexpected failure - LogPrintf("FindMyNotes(): Unexpected error while testing decrypt:\n"); + LogPrintf("FindMySproutNotes(): Unexpected error while testing decrypt:\n"); LogPrintf("%s\n", exc.what()); } } @@ -1474,12 +1613,62 @@ mapSproutNoteData_t CWallet::FindMyNotes(const CTransaction& tx) const return noteData; } -bool CWallet::IsFromMe(const uint256& nullifier) const + +/** + * Finds all output notes in the given transaction that have been sent to + * SaplingPaymentAddresses in this wallet. + * + * It should never be necessary to call this method with a CWalletTx, because + * the result of FindMySaplingNotes (for the addresses available at the time) will + * already have been cached in CWalletTx.mapSaplingNoteData. + */ +mapSaplingNoteData_t CWallet::FindMySaplingNotes(const CTransaction &tx) const +{ + LOCK(cs_SpendingKeyStore); + uint256 hash = tx.GetHash(); + + mapSaplingNoteData_t noteData; + + // Protocol Spec: 4.19 Block Chain Scanning (Sapling) + for (uint32_t i = 0; i < tx.vShieldedOutput.size(); ++i) { + const OutputDescription output = tx.vShieldedOutput[i]; + for (auto it = mapSaplingFullViewingKeys.begin(); it != mapSaplingFullViewingKeys.end(); ++it) { + SaplingIncomingViewingKey ivk = it->first; + auto result = SaplingNotePlaintext::decrypt(output.encCiphertext, ivk, output.ephemeralKey, output.cm); + if (!result) { + continue; + } + // We don't cache the nullifier here as computing it requires knowledge of the note position + // in the commitment tree, which can only be determined when the transaction has been mined. + SaplingOutPoint op {hash, i}; + SaplingNoteData nd; + nd.ivk = ivk; + noteData.insert(std::make_pair(op, nd)); + break; + } + } + + return noteData; +} + +bool CWallet::IsSproutNullifierFromMe(const uint256& nullifier) const { { LOCK(cs_wallet); - if (mapNullifiersToNotes.count(nullifier) && - mapWallet.count(mapNullifiersToNotes.at(nullifier).hash)) { + if (mapSproutNullifiersToNotes.count(nullifier) && + mapWallet.count(mapSproutNullifiersToNotes.at(nullifier).hash)) { + return true; + } + } + return false; +} + +bool CWallet::IsSaplingNullifierFromMe(const uint256& nullifier) const +{ + { + LOCK(cs_wallet); + if (mapSaplingNullifiersToNotes.count(nullifier) && + mapWallet.count(mapSaplingNullifiersToNotes.at(nullifier).hash)) { return true; } } @@ -1627,11 +1816,16 @@ bool CWallet::IsFromMe(const CTransaction& tx) const } for (const JSDescription& jsdesc : tx.vjoinsplit) { for (const uint256& nullifier : jsdesc.nullifiers) { - if (IsFromMe(nullifier)) { + if (IsSproutNullifierFromMe(nullifier)) { return true; } } } + for (const SpendDescription &spend : tx.vShieldedSpend) { + if (IsSaplingNullifierFromMe(spend.nullifier)) { + return true; + } + } return false; } @@ -1680,7 +1874,7 @@ void CWalletTx::SetSproutNoteData(mapSproutNoteData_t ¬eData) // Store the address and nullifier for the Note mapSproutNoteData[nd.first] = nd.second; } else { - // If FindMyNotes() was used to obtain noteData, + // If FindMySproutNotes() was used to obtain noteData, // this should never happen throw std::logic_error("CWalletTx::SetSproutNoteData(): Invalid note"); } @@ -1757,25 +1951,10 @@ void CWalletTx::GetAmounts(list& listReceived, CAmount nDebit = GetDebit(filter); bool isFromMyTaddr = nDebit > 0; // debit>0 means we signed/sent this transaction - // Does this tx spend my notes? - bool isFromMyZaddr = false; - for (const JSDescription& js : vjoinsplit) { - for (const uint256& nullifier : js.nullifiers) { - if (pwallet->IsFromMe(nullifier)) { - isFromMyZaddr = true; - break; - } - } - if (isFromMyZaddr) { - break; - } - } - // Compute fee if we sent this transaction. if (isFromMyTaddr) { - CAmount nValueOut = GetValueOut(); // transparent outputs plus all vpub_old - CAmount nValueIn = 0; - nValueIn += GetShieldedValueIn(); + CAmount nValueOut = GetValueOut(); // transparent outputs plus all Sprout vpub_old and negative Sapling valueBalance + CAmount nValueIn = GetShieldedValueIn(); nFee = nDebit - nValueOut + nValueIn; } @@ -1788,7 +1967,7 @@ void CWalletTx::GetAmounts(list& listReceived, // Check input side for (const uint256& nullifier : js.nullifiers) { - if (pwallet->IsFromMe(nullifier)) { + if (pwallet->IsSproutNullifierFromMe(nullifier)) { fMyJSDesc = true; break; } @@ -1824,6 +2003,18 @@ void CWalletTx::GetAmounts(list& listReceived, } } + // If we sent utxos from this transaction, create output for value taken from (negative valueBalance) + // or added (positive valueBalance) to the transparent value pool by Sapling shielding and unshielding. + if (isFromMyTaddr) { + if (valueBalance < 0) { + COutputEntry output = {CNoDestination(), -valueBalance, (int) vout.size()}; + listSent.push_back(output); + } else if (valueBalance > 0) { + COutputEntry output = {CNoDestination(), valueBalance, (int) vout.size()}; + listReceived.push_back(output); + } + } + // Sent/received. for (unsigned int i = 0; i < vout.size(); ++i) { @@ -3947,7 +4138,7 @@ void CWallet::GetFilteredNotes( } // skip note which has been spent - if (ignoreSpent && nd.nullifier && IsSpent(*nd.nullifier)) { + if (ignoreSpent && nd.nullifier && IsSproutSpent(*nd.nullifier)) { continue; } @@ -4028,7 +4219,7 @@ void CWallet::GetUnspentFilteredNotes( } // skip note which has been spent - if (nd.nullifier && IsSpent(*nd.nullifier)) { + if (nd.nullifier && IsSproutSpent(*nd.nullifier)) { continue; } diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 9c2aa863f..b1655eebd 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -225,7 +225,7 @@ public: /** * Block height corresponding to the most current witness. * - * When we first create a SproutNoteData in CWallet::FindMyNotes, this is set to + * When we first create a SproutNoteData in CWallet::FindMySproutNotes, this is set to * -1 as a placeholder. The next time CWallet::ChainTip is called, we can * determine what height the witness cache for this note is valid for (even * if no witnesses were cached), and so can set the correct value in @@ -267,13 +267,25 @@ class SaplingNoteData { public: /** - * We initialize the hight to -1 for the same reason as we do in SproutNoteData. + * We initialize the height to -1 for the same reason as we do in SproutNoteData. * See the comment in that class for a full description. */ - SaplingNoteData() : witnessHeight {-1} { } + SaplingNoteData() : witnessHeight {-1}, nullifier() { } + SaplingNoteData(libzcash::SaplingIncomingViewingKey ivk) : ivk {ivk}, witnessHeight {-1}, nullifier() { } + SaplingNoteData(libzcash::SaplingIncomingViewingKey ivk, uint256 n) : ivk {ivk}, witnessHeight {-1}, nullifier(n) { } std::list witnesses; int witnessHeight; + libzcash::SaplingIncomingViewingKey ivk; + boost::optional nullifier; + + friend bool operator==(const SaplingNoteData& a, const SaplingNoteData& b) { + return (a.ivk == b.ivk && a.nullifier == b.nullifier && a.witnessHeight == b.witnessHeight); + } + + friend bool operator!=(const SaplingNoteData& a, const SaplingNoteData& b) { + return !(a == b); + } }; typedef std::map mapSproutNoteData_t; @@ -715,10 +727,12 @@ private: * detect and report conflicts (double-spends). */ typedef TxSpendMap TxNullifiers; - TxNullifiers mapTxNullifiers; + TxNullifiers mapTxSproutNullifiers; + TxNullifiers mapTxSaplingNullifiers; - void AddToSpends(const COutPoint& outpoint, const uint256& wtxid); - void AddToSpends(const uint256& nullifier, const uint256& wtxid); + void AddToTransparentSpends(const COutPoint& outpoint, const uint256& wtxid); + void AddToSproutSpends(const uint256& nullifier, const uint256& wtxid); + void AddToSaplingSpends(const uint256& nullifier, const uint256& wtxid); void AddToSpends(const uint256& wtxid); public: @@ -895,7 +909,9 @@ public: * - Restarting the node with -reindex (which operates on a locked wallet * but with the now-cached nullifiers). */ - std::map mapNullifiersToNotes; + std::map mapSproutNullifiersToNotes; + + std::map mapSaplingNullifiersToNotes; std::map mapWallet; @@ -920,7 +936,8 @@ public: bool SelectCoinsMinConf(const CAmount& nTargetValue, int nConfMine, int nConfTheirs, std::vector vCoins, std::set >& setCoinsRet, CAmount& nValueRet) const; bool IsSpent(const uint256& hash, unsigned int n) const; - bool IsSpent(const uint256& nullifier) const; + bool IsSproutSpent(const uint256& nullifier) const; + bool IsSaplingSpent(const uint256& nullifier) const; bool IsLockedCoin(uint256 hash, unsigned int n) const; void LockCoin(COutPoint& output); @@ -1036,6 +1053,8 @@ public: void MarkDirty(); bool UpdateNullifierNoteMap(); void UpdateNullifierNoteMapWithTx(const CWalletTx& wtx); + void UpdateSaplingNullifierNoteMapWithTx(CWalletTx& wtx); + void UpdateSaplingNullifierNoteMapForBlock(const CBlock* pblock); bool AddToWallet(const CWalletTx& wtxIn, bool fFromLoadWallet, CWalletDB* pwalletdb); void SyncTransaction(const CTransaction& tx, const CBlock* pblock); bool AddToWalletIfInvolvingMe(const CTransaction& tx, const CBlock* pblock, bool fUpdate); @@ -1076,14 +1095,17 @@ public: std::set GetAccountAddresses(const std::string& strAccount) const; - boost::optional GetNoteNullifier( + boost::optional GetSproutNoteNullifier( const JSDescription& jsdesc, const libzcash::SproutPaymentAddress& address, const ZCNoteDecryption& dec, const uint256& hSig, uint8_t n) const; - mapSproutNoteData_t FindMyNotes(const CTransaction& tx) const; - bool IsFromMe(const uint256& nullifier) const; + mapSproutNoteData_t FindMySproutNotes(const CTransaction& tx) const; + mapSaplingNoteData_t FindMySaplingNotes(const CTransaction& tx) const; + bool IsSproutNullifierFromMe(const uint256& nullifier) const; + bool IsSaplingNullifierFromMe(const uint256& nullifier) const; + void GetSproutNoteWitnesses( std::vector notes, std::vector>& witnesses, diff --git a/src/zcash/Note.cpp b/src/zcash/Note.cpp index c6c72e297..ee8f7b641 100644 --- a/src/zcash/Note.cpp +++ b/src/zcash/Note.cpp @@ -187,7 +187,8 @@ boost::optional SaplingOutgoingPlaintext::decrypt( boost::optional SaplingNotePlaintext::decrypt( const SaplingEncCiphertext &ciphertext, const uint256 &ivk, - const uint256 &epk + const uint256 &epk, + const uint256 &cmu ) { auto pt = AttemptSaplingEncDecryption(ciphertext, ivk, epk); @@ -204,6 +205,27 @@ boost::optional SaplingNotePlaintext::decrypt( assert(ss.size() == 0); + uint256 pk_d; + if (!librustzcash_ivk_to_pkd(ivk.begin(), ret.d.data(), pk_d.begin())) { + return boost::none; + } + + uint256 cmu_expected; + if (!librustzcash_sapling_compute_cm( + ret.d.data(), + pk_d.begin(), + ret.value(), + ret.rcm.begin(), + cmu_expected.begin() + )) + { + return boost::none; + } + + if (cmu_expected != cmu) { + return boost::none; + } + return ret; } @@ -211,7 +233,8 @@ boost::optional SaplingNotePlaintext::decrypt( const SaplingEncCiphertext &ciphertext, const uint256 &epk, const uint256 &esk, - const uint256 &pk_d + const uint256 &pk_d, + const uint256 &cmu ) { auto pt = AttemptSaplingEncDecryption(ciphertext, epk, esk, pk_d); @@ -226,6 +249,22 @@ boost::optional SaplingNotePlaintext::decrypt( SaplingNotePlaintext ret; ss >> ret; + uint256 cmu_expected; + if (!librustzcash_sapling_compute_cm( + ret.d.data(), + pk_d.begin(), + ret.value(), + ret.rcm.begin(), + cmu_expected.begin() + )) + { + return boost::none; + } + + if (cmu_expected != cmu) { + return boost::none; + } + assert(ss.size() == 0); return ret; diff --git a/src/zcash/Note.hpp b/src/zcash/Note.hpp index f1b8e4323..7d3377306 100644 --- a/src/zcash/Note.hpp +++ b/src/zcash/Note.hpp @@ -130,14 +130,16 @@ public: static boost::optional decrypt( const SaplingEncCiphertext &ciphertext, const uint256 &ivk, - const uint256 &epk + const uint256 &epk, + const uint256 &cmu ); static boost::optional decrypt( const SaplingEncCiphertext &ciphertext, const uint256 &epk, const uint256 &esk, - const uint256 &pk_d + const uint256 &pk_d, + const uint256 &cmu ); boost::optional note(const SaplingIncomingViewingKey& ivk) const; diff --git a/src/zcbenchmarks.cpp b/src/zcbenchmarks.cpp index 314de11ab..bad4ce605 100644 --- a/src/zcbenchmarks.cpp +++ b/src/zcbenchmarks.cpp @@ -291,7 +291,7 @@ double benchmark_try_decrypt_notes(size_t nAddrs) struct timeval tv_start; timer_start(tv_start); - auto nd = wallet.FindMyNotes(tx); + auto nd = wallet.FindMySproutNotes(tx); return timer_stop(tv_start); }