diff --git a/src/wallet/asyncrpcoperation_sendmany.cpp b/src/wallet/asyncrpcoperation_sendmany.cpp index dba999e7e..405cde231 100644 --- a/src/wallet/asyncrpcoperation_sendmany.cpp +++ b/src/wallet/asyncrpcoperation_sendmany.cpp @@ -888,13 +888,14 @@ bool AsyncRPCOperation_sendmany::find_utxos(bool fAcceptCoinbase=false) { bool AsyncRPCOperation_sendmany::find_unspent_notes() { - std::vector entries; + std::vector sproutEntries; + std::vector saplingEntries; { LOCK2(cs_main, pwalletMain->cs_wallet); - pwalletMain->GetFilteredNotes(entries, fromaddress_, mindepth_); + pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, fromaddress_, mindepth_); } - for (CSproutNotePlaintextEntry & entry : entries) { + for (CSproutNotePlaintextEntry & entry : sproutEntries) { z_inputs_.push_back(SendManyInputJSOP(entry.jsop, entry.plaintext.note(boost::get(frompaymentaddress_)), CAmount(entry.plaintext.value()))); std::string data(entry.plaintext.memo().begin(), entry.plaintext.memo().end()); LogPrint("zrpcunsafe", "%s: found unspent note (txid=%s, vjoinsplit=%d, ciphertext=%d, amount=%s, memo=%s)\n", @@ -906,6 +907,7 @@ bool AsyncRPCOperation_sendmany::find_unspent_notes() { HexStr(data).substr(0, 10) ); } + // TODO: Do something with Sapling notes if (z_inputs_.size() == 0) { return false; diff --git a/src/wallet/gtest/test_wallet.cpp b/src/wallet/gtest/test_wallet.cpp index d79b69ea0..86e3e2a2b 100644 --- a/src/wallet/gtest/test_wallet.cpp +++ b/src/wallet/gtest/test_wallet.cpp @@ -186,13 +186,16 @@ TEST(WalletTests, FindUnspentSproutNotes) { EXPECT_FALSE(wallet.IsSproutSpent(nullifier)); // We currently have an unspent and unconfirmed note in the wallet (depth of -1) - std::vector entries; - wallet.GetFilteredNotes(entries, "", 0); - EXPECT_EQ(0, entries.size()); - entries.clear(); - wallet.GetFilteredNotes(entries, "", -1); - EXPECT_EQ(1, entries.size()); - entries.clear(); + std::vector sproutEntries; + std::vector saplingEntries; + wallet.GetFilteredNotes(sproutEntries, saplingEntries, "", 0); + EXPECT_EQ(0, sproutEntries.size()); + sproutEntries.clear(); + saplingEntries.clear(); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, "", -1); + EXPECT_EQ(1, sproutEntries.size()); + sproutEntries.clear(); + saplingEntries.clear(); // Fake-mine the transaction EXPECT_EQ(-1, chainActive.Height()); @@ -212,15 +215,18 @@ TEST(WalletTests, FindUnspentSproutNotes) { // We now have an unspent and confirmed note in the wallet (depth of 1) - wallet.GetFilteredNotes(entries, "", 0); - EXPECT_EQ(1, entries.size()); - entries.clear(); - wallet.GetFilteredNotes(entries, "", 1); - EXPECT_EQ(1, entries.size()); - entries.clear(); - wallet.GetFilteredNotes(entries, "", 2); - EXPECT_EQ(0, entries.size()); - entries.clear(); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, "", 0); + EXPECT_EQ(1, sproutEntries.size()); + sproutEntries.clear(); + saplingEntries.clear(); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, "", 1); + EXPECT_EQ(1, sproutEntries.size()); + sproutEntries.clear(); + saplingEntries.clear(); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, "", 2); + EXPECT_EQ(0, sproutEntries.size()); + sproutEntries.clear(); + saplingEntries.clear(); // Let's spend the note. @@ -247,21 +253,25 @@ TEST(WalletTests, FindUnspentSproutNotes) { EXPECT_TRUE(wallet.IsSproutSpent(nullifier)); // The note has been spent. By default, GetFilteredNotes() ignores spent notes. - wallet.GetFilteredNotes(entries, "", 0); - EXPECT_EQ(0, entries.size()); - entries.clear(); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, "", 0); + EXPECT_EQ(0, sproutEntries.size()); + sproutEntries.clear(); + saplingEntries.clear(); // Let's include spent notes to retrieve it. - wallet.GetFilteredNotes(entries, "", 0, false); - EXPECT_EQ(1, entries.size()); - entries.clear(); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, "", 0, false); + EXPECT_EQ(1, sproutEntries.size()); + sproutEntries.clear(); + saplingEntries.clear(); // The spent note has two confirmations. - wallet.GetFilteredNotes(entries, "", 2, false); - EXPECT_EQ(1, entries.size()); - entries.clear(); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, "", 2, false); + EXPECT_EQ(1, sproutEntries.size()); + sproutEntries.clear(); + saplingEntries.clear(); // It does not have 3 confirmations. - wallet.GetFilteredNotes(entries, "", 3, false); - EXPECT_EQ(0, entries.size()); - entries.clear(); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, "", 3, false); + EXPECT_EQ(0, sproutEntries.size()); + sproutEntries.clear(); + saplingEntries.clear(); // Let's receive a new note @@ -301,21 +311,25 @@ TEST(WalletTests, FindUnspentSproutNotes) { wallet.AddToWallet(wtx3, true, NULL); // We now have an unspent note which has one confirmation, in addition to our spent note. - wallet.GetFilteredNotes(entries, "", 1); - EXPECT_EQ(1, entries.size()); - entries.clear(); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, "", 1); + EXPECT_EQ(1, sproutEntries.size()); + sproutEntries.clear(); + saplingEntries.clear(); // Let's return the spent note too. - wallet.GetFilteredNotes(entries, "", 1, false); - EXPECT_EQ(2, entries.size()); - entries.clear(); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, "", 1, false); + EXPECT_EQ(2, sproutEntries.size()); + sproutEntries.clear(); + saplingEntries.clear(); // Increasing number of confirmations will exclude our new unspent note. - wallet.GetFilteredNotes(entries, "", 2, false); - EXPECT_EQ(1, entries.size()); - entries.clear(); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, "", 2, false); + EXPECT_EQ(1, sproutEntries.size()); + sproutEntries.clear(); + saplingEntries.clear(); // If we also ignore spent notes at this depth, we won't find any notes. - wallet.GetFilteredNotes(entries, "", 2, true); - EXPECT_EQ(0, entries.size()); - entries.clear(); + wallet.GetFilteredNotes(sproutEntries, saplingEntries, "", 2, true); + EXPECT_EQ(0, sproutEntries.size()); + sproutEntries.clear(); + saplingEntries.clear(); // Tear down chainActive.SetTip(NULL); diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 18986b0aa..66f99724b 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -2556,10 +2556,11 @@ UniValue z_listunspent(const UniValue& params, bool fHelp) UniValue results(UniValue::VARR); if (zaddrs.size() > 0) { - std::vector entries; - pwalletMain->GetUnspentFilteredNotes(entries, zaddrs, nMinDepth, nMaxDepth, !fIncludeWatchonly); + std::vector sproutEntries; + std::vector saplingEntries; + pwalletMain->GetUnspentFilteredNotes(sproutEntries, saplingEntries, zaddrs, nMinDepth, nMaxDepth, !fIncludeWatchonly); std::set> nullifierSet = pwalletMain->GetNullifiersForAddresses(zaddrs); - for (CUnspentSproutNotePlaintextEntry & entry : entries) { + for (CUnspentSproutNotePlaintextEntry & entry : sproutEntries) { UniValue obj(UniValue::VOBJ); obj.push_back(Pair("txid", entry.jsop.hash.ToString())); obj.push_back(Pair("jsindex", (int)entry.jsop.js )); @@ -2576,6 +2577,7 @@ UniValue z_listunspent(const UniValue& params, bool fHelp) } results.push_back(obj); } + // TODO: Sapling } return results; @@ -3248,12 +3250,14 @@ CAmount getBalanceTaddr(std::string transparentAddress, int minDepth=1, bool ign CAmount getBalanceZaddr(std::string address, int minDepth = 1, bool ignoreUnspendable=true) { CAmount balance = 0; - std::vector entries; + std::vector sproutEntries; + std::vector saplingEntries; LOCK2(cs_main, pwalletMain->cs_wallet); - pwalletMain->GetFilteredNotes(entries, address, minDepth, true, ignoreUnspendable); - for (auto & entry : entries) { + pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, address, minDepth, true, ignoreUnspendable); + for (auto & entry : sproutEntries) { balance += CAmount(entry.plaintext.value()); } + // TODO: Sapling return balance; } @@ -3309,10 +3313,11 @@ UniValue z_listreceivedbyaddress(const UniValue& params, bool fHelp) } UniValue result(UniValue::VARR); - std::vector entries; - pwalletMain->GetFilteredNotes(entries, fromaddress, nMinDepth, false, false); + std::vector sproutEntries; + std::vector saplingEntries; + pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, fromaddress, nMinDepth, false, false); auto nullifierSet = hasSproutSpendingKey ? pwalletMain->GetNullifiersForAddresses({zaddr}) : std::set>(); - for (CSproutNotePlaintextEntry & entry : entries) { + for (CSproutNotePlaintextEntry & entry : sproutEntries) { UniValue obj(UniValue::VOBJ); obj.push_back(Pair("txid", entry.jsop.hash.ToString())); obj.push_back(Pair("amount", ValueFromAmount(CAmount(entry.plaintext.value())))); @@ -3326,6 +3331,7 @@ UniValue z_listreceivedbyaddress(const UniValue& params, bool fHelp) } result.push_back(obj); } + // TODO: Sapling return result; } @@ -4247,11 +4253,12 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp) if (useAny || useAnyNote || zaddrs.size() > 0) { // Get available notes - std::vector entries; - pwalletMain->GetFilteredNotes(entries, zaddrs); + std::vector sproutEntries; + std::vector saplingEntries; + pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, zaddrs); // Find unspent notes and update estimated size - for (CSproutNotePlaintextEntry& entry : entries) { + for (CSproutNotePlaintextEntry& entry : sproutEntries) { noteCounter++; CAmount nValue = entry.plaintext.value(); @@ -4265,8 +4272,7 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp) maxedOutNotesFlag = true; } else { estimatedTxSize += increase; - // TODO: Add Sapling support - auto zaddr = boost::get(entry.address); + auto zaddr = entry.address; SproutSpendingKey zkey; pwalletMain->GetSproutSpendingKey(zaddr, zkey); noteInputs.emplace_back(entry.jsop, entry.plaintext.note(zaddr), nValue, zkey); @@ -4278,6 +4284,7 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp) remainingNoteValue += nValue; } } + // TODO: Add Sapling support } size_t numUtxos = utxoInputs.size(); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 90d6a72e0..f227eb527 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -4091,7 +4091,13 @@ bool CMerkleTx::AcceptToMemoryPool(bool fLimitFree, bool fRejectAbsurdFee) * Find notes in the wallet filtered by payment address, min depth and ability to spend. * These notes are decrypted and added to the output parameter vector, outEntries. */ -void CWallet::GetFilteredNotes(std::vector & outEntries, std::string address, int minDepth, bool ignoreSpent, bool ignoreUnspendable) +void CWallet::GetFilteredNotes( + std::vector& sproutEntries, + std::vector& saplingEntries, + std::string address, + int minDepth, + bool ignoreSpent, + bool ignoreUnspendable) { std::set filterAddresses; @@ -4099,7 +4105,7 @@ void CWallet::GetFilteredNotes(std::vector & outEntri filterAddresses.insert(DecodePaymentAddress(address)); } - GetFilteredNotes(outEntries, filterAddresses, minDepth, ignoreSpent, ignoreUnspendable); + GetFilteredNotes(sproutEntries, saplingEntries, filterAddresses, minDepth, ignoreSpent, ignoreUnspendable); } /** @@ -4107,7 +4113,8 @@ void CWallet::GetFilteredNotes(std::vector & outEntri * These notes are decrypted and added to the output parameter vector, outEntries. */ void CWallet::GetFilteredNotes( - std::vector& outEntries, + std::vector& sproutEntries, + std::vector& saplingEntries, std::set& filterAddresses, int minDepth, bool ignoreSpent, @@ -4123,10 +4130,6 @@ void CWallet::GetFilteredNotes( continue; } - if (wtx.mapSproutNoteData.size() == 0) { - continue; - } - for (auto & pair : wtx.mapSproutNoteData) { JSOutPoint jsop = pair.first; SproutNoteData nd = pair.second; @@ -4172,7 +4175,7 @@ void CWallet::GetFilteredNotes( hSig, (unsigned char) j); - outEntries.push_back(CSproutNotePlaintextEntry{jsop, pa, plaintext}); + sproutEntries.push_back(CSproutNotePlaintextEntry{jsop, pa, plaintext}); } catch (const note_decryption_failed &err) { // Couldn't decrypt with this spending key @@ -4182,13 +4185,61 @@ void CWallet::GetFilteredNotes( throw std::runtime_error(strprintf("Error while decrypting note for payment address %s: %s", EncodePaymentAddress(pa), exc.what())); } } + + for (auto & pair : wtx.mapSaplingNoteData) { + SaplingOutPoint op = pair.first; + SaplingNoteData nd = pair.second; + + auto maybe_pt = SaplingNotePlaintext::decrypt( + wtx.vShieldedOutput[op.n].encCiphertext, + nd.ivk, + wtx.vShieldedOutput[op.n].ephemeralKey, + wtx.vShieldedOutput[op.n].cm); + assert(static_cast(maybe_pt)); + auto notePt = maybe_pt.get(); + + auto maybe_pa = nd.ivk.address(notePt.d); + assert(static_cast(maybe_pa)); + auto pa = maybe_pa.get(); + + // skip notes which belong to a different payment address in the wallet + if (!(filterAddresses.empty() || filterAddresses.count(pa))) { + continue; + } + + if (ignoreSpent && nd.nullifier && IsSaplingSpent(*nd.nullifier)) { + continue; + } + + // skip notes which cannot be spent + if (ignoreUnspendable) { + libzcash::SaplingIncomingViewingKey ivk; + libzcash::SaplingFullViewingKey fvk; + if (!(GetSaplingIncomingViewingKey(pa, ivk) && + GetSaplingFullViewingKey(ivk, fvk) && + HaveSaplingSpendingKey(fvk))) { + continue; + } + } + + // skip locked notes + // TODO: Add locking for Sapling notes + // if (IsLockedNote(jsop)) { + // continue; + // } + + auto note = notePt.note(nd.ivk).get(); + saplingEntries.push_back(SaplingNoteEntry { + op, pa, note, notePt.memo() }); + } } } /* Find unspent notes filtered by payment address, min depth and max depth */ void CWallet::GetUnspentFilteredNotes( - std::vector& outEntries, + std::vector& sproutEntries, + std::vector& saplingEntries, std::set& filterAddresses, int minDepth, int maxDepth, @@ -4204,10 +4255,6 @@ void CWallet::GetUnspentFilteredNotes( continue; } - if (wtx.mapSproutNoteData.size() == 0) { - continue; - } - for (auto & pair : wtx.mapSproutNoteData) { JSOutPoint jsop = pair.first; SproutNoteData nd = pair.second; @@ -4248,7 +4295,7 @@ void CWallet::GetUnspentFilteredNotes( hSig, (unsigned char) j); - outEntries.push_back(CUnspentSproutNotePlaintextEntry{jsop, pa, plaintext, wtx.GetDepthInMainChain()}); + sproutEntries.push_back(CUnspentSproutNotePlaintextEntry{jsop, pa, plaintext, wtx.GetDepthInMainChain()}); } catch (const note_decryption_failed &err) { // Couldn't decrypt with this spending key @@ -4258,6 +4305,48 @@ void CWallet::GetUnspentFilteredNotes( throw std::runtime_error(strprintf("Error while decrypting note for payment address %s: %s", EncodePaymentAddress(pa), exc.what())); } } + + for (auto & pair : wtx.mapSaplingNoteData) { + SaplingOutPoint op = pair.first; + SaplingNoteData nd = pair.second; + + auto maybe_pt = SaplingNotePlaintext::decrypt( + wtx.vShieldedOutput[op.n].encCiphertext, + nd.ivk, + wtx.vShieldedOutput[op.n].ephemeralKey, + wtx.vShieldedOutput[op.n].cm); + assert(static_cast(maybe_pt)); + auto notePt = maybe_pt.get(); + + auto maybe_pa = nd.ivk.address(notePt.d); + assert(static_cast(maybe_pa)); + auto pa = maybe_pa.get(); + + // skip notes which belong to a different payment address in the wallet + if (!(filterAddresses.empty() || filterAddresses.count(pa))) { + continue; + } + + // skip note which has been spent + if (nd.nullifier && IsSaplingSpent(*nd.nullifier)) { + continue; + } + + // skip notes where the spending key is not available + if (requireSpendingKey) { + libzcash::SaplingIncomingViewingKey ivk; + libzcash::SaplingFullViewingKey fvk; + if (!(GetSaplingIncomingViewingKey(pa, ivk) && + GetSaplingFullViewingKey(ivk, fvk) && + HaveSaplingSpendingKey(fvk))) { + continue; + } + } + + auto note = notePt.note(nd.ivk).get(); + saplingEntries.push_back(UnspentSaplingNoteEntry { + op, pa, note, notePt.memo(), wtx.GetDepthInMainChain() }); + } } } diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index b1655eebd..74cbb3a0e 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -307,6 +307,24 @@ struct CUnspentSproutNotePlaintextEntry { int nHeight; }; +/** Sapling note and its location in a transaction. */ +struct SaplingNoteEntry +{ + SaplingOutPoint op; + libzcash::SaplingPaymentAddress address; + libzcash::SaplingNote note; + std::array memo; +}; + +/** Sapling note, location in a transaction, and confirmation height. */ +struct UnspentSaplingNoteEntry { + SaplingOutPoint op; + libzcash::SaplingPaymentAddress address; + libzcash::SaplingNote note; + std::array memo; + int nHeight; +}; + /** A transaction with a merkle branch linking it to the block chain. */ class CMerkleTx : public CTransaction { @@ -1206,21 +1224,24 @@ public: void SetBroadcastTransactions(bool broadcast) { fBroadcastTransactions = broadcast; } /* Find notes filtered by payment address, min depth, ability to spend */ - void GetFilteredNotes(std::vector & outEntries, + void GetFilteredNotes(std::vector& sproutEntries, + std::vector& saplingEntries, std::string address, int minDepth=1, bool ignoreSpent=true, bool ignoreUnspendable=true); /* Find notes filtered by payment addresses, min depth, ability to spend */ - void GetFilteredNotes(std::vector& outEntries, + void GetFilteredNotes(std::vector& sproutEntries, + std::vector& saplingEntries, std::set& filterAddresses, int minDepth=1, bool ignoreSpent=true, bool ignoreUnspendable=true); /* Find unspent notes filtered by payment address, min depth and max depth */ - void GetUnspentFilteredNotes(std::vector& outEntries, + void GetUnspentFilteredNotes(std::vector& sproutEntries, + std::vector& saplingEntries, std::set& filterAddresses, int minDepth=1, int maxDepth=INT_MAX,