Add Sapling support to GetFilteredNotes() and GetUnspentFilteredNotes()

This could in future be refactored to be generic over PaymentAddress and
NotePlaintext in the return type, but for now let's be explicit about which
returned notes are for Sprout vs Sapling, and handle them separately.

Co-authored-by: Sean Bowe <ewillbefull@gmail.com>
This commit is contained in:
Jack Grigg 2018-08-01 01:24:49 +01:00
parent 20f87bc226
commit 94e99acda6
No known key found for this signature in database
GPG Key ID: 1B8D649257DB0829
5 changed files with 207 additions and 74 deletions

View File

@ -888,13 +888,14 @@ bool AsyncRPCOperation_sendmany::find_utxos(bool fAcceptCoinbase=false) {
bool AsyncRPCOperation_sendmany::find_unspent_notes() {
std::vector<CSproutNotePlaintextEntry> entries;
std::vector<CSproutNotePlaintextEntry> sproutEntries;
std::vector<SaplingNoteEntry> 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<libzcash::SproutPaymentAddress>(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;

View File

@ -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<CSproutNotePlaintextEntry> 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<CSproutNotePlaintextEntry> sproutEntries;
std::vector<SaplingNoteEntry> 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);

View File

@ -2556,10 +2556,11 @@ UniValue z_listunspent(const UniValue& params, bool fHelp)
UniValue results(UniValue::VARR);
if (zaddrs.size() > 0) {
std::vector<CUnspentSproutNotePlaintextEntry> entries;
pwalletMain->GetUnspentFilteredNotes(entries, zaddrs, nMinDepth, nMaxDepth, !fIncludeWatchonly);
std::vector<CUnspentSproutNotePlaintextEntry> sproutEntries;
std::vector<UnspentSaplingNoteEntry> saplingEntries;
pwalletMain->GetUnspentFilteredNotes(sproutEntries, saplingEntries, zaddrs, nMinDepth, nMaxDepth, !fIncludeWatchonly);
std::set<std::pair<PaymentAddress, uint256>> 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<CSproutNotePlaintextEntry> entries;
std::vector<CSproutNotePlaintextEntry> sproutEntries;
std::vector<SaplingNoteEntry> 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<CSproutNotePlaintextEntry> entries;
pwalletMain->GetFilteredNotes(entries, fromaddress, nMinDepth, false, false);
std::vector<CSproutNotePlaintextEntry> sproutEntries;
std::vector<SaplingNoteEntry> saplingEntries;
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, fromaddress, nMinDepth, false, false);
auto nullifierSet = hasSproutSpendingKey ? pwalletMain->GetNullifiersForAddresses({zaddr}) : std::set<std::pair<PaymentAddress, uint256>>();
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<CSproutNotePlaintextEntry> entries;
pwalletMain->GetFilteredNotes(entries, zaddrs);
std::vector<CSproutNotePlaintextEntry> sproutEntries;
std::vector<SaplingNoteEntry> 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<SproutPaymentAddress>(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();

View File

@ -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<CSproutNotePlaintextEntry> & outEntries, std::string address, int minDepth, bool ignoreSpent, bool ignoreUnspendable)
void CWallet::GetFilteredNotes(
std::vector<CSproutNotePlaintextEntry>& sproutEntries,
std::vector<SaplingNoteEntry>& saplingEntries,
std::string address,
int minDepth,
bool ignoreSpent,
bool ignoreUnspendable)
{
std::set<PaymentAddress> filterAddresses;
@ -4099,7 +4105,7 @@ void CWallet::GetFilteredNotes(std::vector<CSproutNotePlaintextEntry> & 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<CSproutNotePlaintextEntry> & outEntri
* These notes are decrypted and added to the output parameter vector, outEntries.
*/
void CWallet::GetFilteredNotes(
std::vector<CSproutNotePlaintextEntry>& outEntries,
std::vector<CSproutNotePlaintextEntry>& sproutEntries,
std::vector<SaplingNoteEntry>& saplingEntries,
std::set<PaymentAddress>& 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<bool>(maybe_pt));
auto notePt = maybe_pt.get();
auto maybe_pa = nd.ivk.address(notePt.d);
assert(static_cast<bool>(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<CUnspentSproutNotePlaintextEntry>& outEntries,
std::vector<CUnspentSproutNotePlaintextEntry>& sproutEntries,
std::vector<UnspentSaplingNoteEntry>& saplingEntries,
std::set<PaymentAddress>& 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<bool>(maybe_pt));
auto notePt = maybe_pt.get();
auto maybe_pa = nd.ivk.address(notePt.d);
assert(static_cast<bool>(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() });
}
}
}

View File

@ -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<unsigned char, ZC_MEMO_SIZE> memo;
};
/** Sapling note, location in a transaction, and confirmation height. */
struct UnspentSaplingNoteEntry {
SaplingOutPoint op;
libzcash::SaplingPaymentAddress address;
libzcash::SaplingNote note;
std::array<unsigned char, ZC_MEMO_SIZE> 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<CSproutNotePlaintextEntry> & outEntries,
void GetFilteredNotes(std::vector<CSproutNotePlaintextEntry>& sproutEntries,
std::vector<SaplingNoteEntry>& 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<CSproutNotePlaintextEntry>& outEntries,
void GetFilteredNotes(std::vector<CSproutNotePlaintextEntry>& sproutEntries,
std::vector<SaplingNoteEntry>& saplingEntries,
std::set<libzcash::PaymentAddress>& 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<CUnspentSproutNotePlaintextEntry>& outEntries,
void GetUnspentFilteredNotes(std::vector<CUnspentSproutNotePlaintextEntry>& sproutEntries,
std::vector<UnspentSaplingNoteEntry>& saplingEntries,
std::set<libzcash::PaymentAddress>& filterAddresses,
int minDepth=1,
int maxDepth=INT_MAX,