From d72c19a6624e9587ef93428f9e3f258a001f5edc Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 28 Mar 2018 10:38:57 -0700 Subject: [PATCH] Closes #2910. Add z_listunspent RPC call. --- qa/rpc-tests/wallet_protectcoinbase.py | 53 +++++++++- src/rpcclient.cpp | 4 + src/rpcserver.cpp | 1 + src/rpcserver.h | 1 + src/test/rpc_wallet_tests.cpp | 48 +++++++++ src/wallet/rpcwallet.cpp | 132 +++++++++++++++++++++++++ src/wallet/wallet.cpp | 77 +++++++++++++++ src/wallet/wallet.h | 14 ++- 8 files changed, 325 insertions(+), 5 deletions(-) diff --git a/qa/rpc-tests/wallet_protectcoinbase.py b/qa/rpc-tests/wallet_protectcoinbase.py index afe851a16..5854449ab 100755 --- a/qa/rpc-tests/wallet_protectcoinbase.py +++ b/qa/rpc-tests/wallet_protectcoinbase.py @@ -8,7 +8,7 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.authproxy import JSONRPCException from test_framework.mininode import COIN from test_framework.util import assert_equal, initialize_chain_clean, \ - start_nodes, connect_nodes_bi, stop_node, wait_and_assert_operationid_status + start_nodes, connect_nodes_bi, wait_and_assert_operationid_status import sys import time @@ -96,8 +96,6 @@ class WalletProtectCoinbaseTest (BitcoinTestFramework): break assert_equal("failed", status) assert_equal("no UTXOs found for taddr from address" in errorString, True) - stop_node(self.nodes[3], 3) - self.nodes.pop() # This send will fail because our wallet does not allow any change when protecting a coinbase utxo, # as it's currently not possible to specify a change address in z_sendmany. @@ -129,6 +127,10 @@ class WalletProtectCoinbaseTest (BitcoinTestFramework): assert_equal("failed", status) assert_equal("wallet does not allow any change" in errorString, True) + # Add viewing key for myzaddr to Node 3 + myviewingkey = self.nodes[0].z_exportviewingkey(myzaddr) + self.nodes[3].z_importviewingkey(myviewingkey, "no") + # This send will succeed. We send two coinbase utxos totalling 20.0 less a fee of 0.00010000, with no change. shieldvalue = Decimal('20.0') - Decimal('0.0001') recipients = [] @@ -136,9 +138,43 @@ class WalletProtectCoinbaseTest (BitcoinTestFramework): myopid = self.nodes[0].z_sendmany(mytaddr, recipients) mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid) self.sync_all() + + # Verify that z_listunspent can return a note that has zero confirmations + results = self.nodes[0].z_listunspent() + assert(len(results) == 0) + results = self.nodes[0].z_listunspent(0) # set minconf to zero + assert(len(results) == 1) + assert_equal(results[0]["address"], myzaddr) + assert_equal(results[0]["amount"], shieldvalue) + assert_equal(results[0]["confirmations"], 0) + + # Mine the tx self.nodes[1].generate(1) self.sync_all() + # Verify that z_listunspent returns one note which has been confirmed + results = self.nodes[0].z_listunspent() + assert(len(results) == 1) + assert_equal(results[0]["address"], myzaddr) + assert_equal(results[0]["amount"], shieldvalue) + assert_equal(results[0]["confirmations"], 1) + assert_equal(results[0]["spendable"], True) + + # Verify that z_listunspent returns note for watchonly address on node 3. + results = self.nodes[3].z_listunspent(1, 999, True) + assert(len(results) == 1) + assert_equal(results[0]["address"], myzaddr) + assert_equal(results[0]["amount"], shieldvalue) + assert_equal(results[0]["confirmations"], 1) + assert_equal(results[0]["spendable"], False) + + # Verify that z_listunspent returns error when address spending key from node 0 is not available in wallet of node 1. + try: + results = self.nodes[1].z_listunspent(1, 999, False, [myzaddr]) + except JSONRPCException as e: + errorString = e.error['message'] + assert_equal("Invalid parameter, spending key for address does not belong to wallet" in errorString, True) + # Verify that debug=zrpcunsafe logs params, and that full txid is associated with opid logpath = self.options.tmpdir+"/node0/regtest/debug.log" logcounter = 0 @@ -333,13 +369,22 @@ class WalletProtectCoinbaseTest (BitcoinTestFramework): self.nodes[1].generate(1) self.sync_all() - # check balances + # check balances and unspent notes resp = self.nodes[2].z_gettotalbalance() assert_equal(Decimal(resp["private"]), send_amount) + + notes = self.nodes[2].z_listunspent() + sum_of_notes = sum([note["amount"] for note in notes]) + assert_equal(Decimal(resp["private"]), sum_of_notes) + resp = self.nodes[0].z_getbalance(myzaddr) assert_equal(Decimal(resp), zbalance - custom_fee - send_amount) sproutvalue -= custom_fee check_value_pool(self.nodes[0], 'sprout', sproutvalue) + notes = self.nodes[0].z_listunspent(1, 99999, False, [myzaddr]) + sum_of_notes = sum([note["amount"] for note in notes]) + assert_equal(Decimal(resp), sum_of_notes) + if __name__ == '__main__': WalletProtectCoinbaseTest().main() diff --git a/src/rpcclient.cpp b/src/rpcclient.cpp index 45809cdb0..65134b430 100644 --- a/src/rpcclient.cpp +++ b/src/rpcclient.cpp @@ -105,6 +105,10 @@ static const CRPCConvertParam vRPCConvertParams[] = { "getblocksubsidy", 0}, { "z_listaddresses", 0}, { "z_listreceivedbyaddress", 1}, + { "z_listunspent", 0 }, + { "z_listunspent", 1 }, + { "z_listunspent", 2 }, + { "z_listunspent", 3 }, { "z_getbalance", 1}, { "z_gettotalbalance", 0}, { "z_gettotalbalance", 1}, diff --git a/src/rpcserver.cpp b/src/rpcserver.cpp index 568ba7926..86e27867e 100644 --- a/src/rpcserver.cpp +++ b/src/rpcserver.cpp @@ -385,6 +385,7 @@ static const CRPCCommand vRPCCommands[] = { "wallet", "zcrawreceive", &zc_raw_receive, true }, { "wallet", "zcsamplejoinsplit", &zc_sample_joinsplit, true }, { "wallet", "z_listreceivedbyaddress",&z_listreceivedbyaddress,false }, + { "wallet", "z_listunspent", &z_listunspent, false }, { "wallet", "z_getbalance", &z_getbalance, false }, { "wallet", "z_gettotalbalance", &z_gettotalbalance, false }, { "wallet", "z_mergetoaddress", &z_mergetoaddress, false }, diff --git a/src/rpcserver.h b/src/rpcserver.h index 8ce108cb4..fe1b2bdeb 100644 --- a/src/rpcserver.h +++ b/src/rpcserver.h @@ -287,6 +287,7 @@ extern UniValue z_listaddresses(const UniValue& params, bool fHelp); // in rpcwa extern UniValue z_exportwallet(const UniValue& params, bool fHelp); // in rpcdump.cpp extern UniValue z_importwallet(const UniValue& params, bool fHelp); // in rpcdump.cpp extern UniValue z_listreceivedbyaddress(const UniValue& params, bool fHelp); // in rpcwallet.cpp +extern UniValue z_listunspent(const UniValue& params, bool fHelp); // in rpcwallet.cpp extern UniValue z_getbalance(const UniValue& params, bool fHelp); // in rpcwallet.cpp extern UniValue z_gettotalbalance(const UniValue& params, bool fHelp); // in rpcwallet.cpp extern UniValue z_mergetoaddress(const UniValue& params, bool fHelp); // in rpcwallet.cpp diff --git a/src/test/rpc_wallet_tests.cpp b/src/test/rpc_wallet_tests.cpp index 04fccfe81..ce2da60c1 100644 --- a/src/test/rpc_wallet_tests.cpp +++ b/src/test/rpc_wallet_tests.cpp @@ -1266,6 +1266,54 @@ BOOST_AUTO_TEST_CASE(rpc_wallet_encrypted_wallet_zkeys) } +BOOST_AUTO_TEST_CASE(rpc_z_listunspent_parameters) +{ + SelectParams(CBaseChainParams::TESTNET); + + LOCK(pwalletMain->cs_wallet); + + UniValue retValue; + + // too many args + BOOST_CHECK_THROW(CallRPC("z_listunspent 1 2 3 4 5"), runtime_error); + + // minconf must be >= 0 + BOOST_CHECK_THROW(CallRPC("z_listunspent -1"), runtime_error); + + // maxconf must be > minconf + BOOST_CHECK_THROW(CallRPC("z_listunspent 2 1"), runtime_error); + + // maxconf must not be out of range + BOOST_CHECK_THROW(CallRPC("z_listunspent 1 9999999999"), runtime_error); + + // must be an array of addresses + BOOST_CHECK_THROW(CallRPC("z_listunspent 1 999 false ztjiDe569DPNbyTE6TSdJTaSDhoXEHLGvYoUnBU1wfVNU52TEyT6berYtySkd21njAeEoh8fFJUT42kua9r8EnhBaEKqCpP"), runtime_error); + + // address must be string + BOOST_CHECK_THROW(CallRPC("z_listunspent 1 999 false [123456]"), runtime_error); + + // no spending key + BOOST_CHECK_THROW(CallRPC("z_listunspent 1 999 false [\"ztjiDe569DPNbyTE6TSdJTaSDhoXEHLGvYoUnBU1wfVNU52TEyT6berYtySkd21njAeEoh8fFJUT42kua9r8EnhBaEKqCpP\"]"), runtime_error); + + // allow watch only + BOOST_CHECK_NO_THROW(CallRPC("z_listunspent 1 999 true [\"ztjiDe569DPNbyTE6TSdJTaSDhoXEHLGvYoUnBU1wfVNU52TEyT6berYtySkd21njAeEoh8fFJUT42kua9r8EnhBaEKqCpP\"]")); + + // wrong network, mainnet instead of testnet + BOOST_CHECK_THROW(CallRPC("z_listunspent 1 999 true [\"zcMuhvq8sEkHALuSU2i4NbNQxshSAYrpCExec45ZjtivYPbuiFPwk6WHy4SvsbeZ4siy1WheuRGjtaJmoD1J8bFqNXhsG6U\"]"), runtime_error); + + // create shielded address so we have the spending key + BOOST_CHECK_NO_THROW(retValue = CallRPC("z_getnewaddress")); + std::string myzaddr = retValue.get_str(); + + // return empty array for this address + BOOST_CHECK_NO_THROW(retValue = CallRPC("z_listunspent 1 999 false [\"" + myzaddr + "\"]")); + UniValue arr = retValue.get_array(); + BOOST_CHECK_EQUAL(0, arr.size()); + + // duplicate address error + BOOST_CHECK_THROW(CallRPC("z_listunspent 1 999 false [\"" + myzaddr + "\", \"" + myzaddr + "\"]"), runtime_error); +} + BOOST_AUTO_TEST_CASE(rpc_z_shieldcoinbase_parameters) { diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index b242580b1..59640b4d6 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -2428,6 +2428,138 @@ UniValue listunspent(const UniValue& params, bool fHelp) return results; } + +UniValue z_listunspent(const UniValue& params, bool fHelp) +{ + if (!EnsureWalletIsAvailable(fHelp)) + return NullUniValue; + + if (fHelp || params.size() > 4) + throw runtime_error( + "z_listunspent ( minconf maxconf includeWatchonly [\"zaddr\",...] )\n" + "\nReturns array of unspent shielded notes with between minconf and maxconf (inclusive) confirmations.\n" + "Optionally filter to only include notes sent to specified addresses.\n" + "When minconf is 0, unspent notes with zero confirmations are returned, even though they are not immediately spendable.\n" + "Results are an array of Objects, each of which has:\n" + "{txid, jsindex, jsoutindex, confirmations, address, amount, memo}\n" + "\nArguments:\n" + "1. minconf (numeric, optional, default=1) The minimum confirmations to filter\n" + "2. maxconf (numeric, optional, default=9999999) The maximum confirmations to filter\n" + "3. includeWatchonly (bool, optional, default=false) Also include watchonly addresses (see 'z_importviewingkey')\n" + "4. \"addresses\" (string) A json array of zaddrs to filter on. Duplicate addresses not allowed.\n" + " [\n" + " \"address\" (string) zaddr\n" + " ,...\n" + " ]\n" + "\nResult\n" + "[ (array of json object)\n" + " {\n" + " \"txid\" : \"txid\", (string) the transaction id \n" + " \"jsindex\" : n (numeric) the joinsplit index\n" + " \"jsoutindex\" : n (numeric) the output index of the joinsplit\n" + " \"confirmations\" : n (numeric) the number of confirmations\n" + " \"spendable\" : true|false (boolean) true if note can be spent by wallet, false if note has zero confirmations, false if address is watchonly\n" + " \"address\" : \"address\", (string) the shielded address\n" + " \"amount\": xxxxx, (numeric) the amount of value in the note\n" + " \"memo\": xxxxx, (string) hexademical string representation of memo field\n" + " }\n" + " ,...\n" + "]\n" + + "\nExamples\n" + + HelpExampleCli("z_listunspent", "") + + HelpExampleCli("z_listunspent", "6 9999999 false \"[\\\"ztbx5DLDxa5ZLFTchHhoPNkKs57QzSyib6UqXpEdy76T1aUdFxJt1w9318Z8DJ73XzbnWHKEZP9Yjg712N5kMmP4QzS9iC9\\\",\\\"ztfaW34Gj9FrnGUEf833ywDVL62NWXBM81u6EQnM6VR45eYnXhwztecW1SjxA7JrmAXKJhxhj3vDNEpVCQoSvVoSpmbhtjf\\\"]\"") + + HelpExampleRpc("z_listunspent", "6 9999999 false \"[\\\"ztbx5DLDxa5ZLFTchHhoPNkKs57QzSyib6UqXpEdy76T1aUdFxJt1w9318Z8DJ73XzbnWHKEZP9Yjg712N5kMmP4QzS9iC9\\\",\\\"ztfaW34Gj9FrnGUEf833ywDVL62NWXBM81u6EQnM6VR45eYnXhwztecW1SjxA7JrmAXKJhxhj3vDNEpVCQoSvVoSpmbhtjf\\\"]\"") + ); + + RPCTypeCheck(params, boost::assign::list_of(UniValue::VNUM)(UniValue::VNUM)(UniValue::VBOOL)(UniValue::VARR)); + + int nMinDepth = 1; + if (params.size() > 0) { + nMinDepth = params[0].get_int(); + } + if (nMinDepth < 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Minimum number of confirmations cannot be less than 0"); + } + + int nMaxDepth = 9999999; + if (params.size() > 1) { + nMaxDepth = params[1].get_int(); + } + if (nMaxDepth < nMinDepth) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Maximum number of confirmations must be greater or equal to the minimum number of confirmations"); + } + + std::set zaddrs = {}; + + bool fIncludeWatchonly = false; + if (params.size() > 2) { + fIncludeWatchonly = params[2].get_bool(); + } + + LOCK2(cs_main, pwalletMain->cs_wallet); + + // User has supplied zaddrs to filter on + if (params.size() > 3) { + UniValue addresses = params[3].get_array(); + if (addresses.size()==0) + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, addresses array is empty."); + + // Keep track of addresses to spot duplicates + set setAddress; + + // Sources + for (const UniValue& o : addresses.getValues()) { + if (!o.isStr()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected string"); + } + string address = o.get_str(); + try { + CZCPaymentAddress zaddr(address); + libzcash::PaymentAddress addr = zaddr.Get(); + if (!fIncludeWatchonly && !pwalletMain->HaveSpendingKey(addr)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, spending key for address does not belong to wallet: ") + address); + } + zaddrs.insert(addr); + } catch (const std::runtime_error&) { + throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, address is not a valid zaddr: ") + address); + } + + if (setAddress.count(address)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, duplicated address: ") + address); + } + setAddress.insert(address); + } + } + else { + // User did not provide zaddrs, so use default i.e. all addresses + pwalletMain->GetPaymentAddresses(zaddrs); + } + + UniValue results(UniValue::VARR); + + if (zaddrs.size() > 0) { + std::vector entries; + pwalletMain->GetUnspentFilteredNotes(entries, zaddrs, nMinDepth, nMaxDepth, !fIncludeWatchonly); + for (CUnspentNotePlaintextEntry & entry : entries) { + UniValue obj(UniValue::VOBJ); + obj.push_back(Pair("txid",entry.jsop.hash.ToString())); + obj.push_back(Pair("jsindex", (int)entry.jsop.js )); + obj.push_back(Pair("jsoutindex", (int)entry.jsop.n)); + obj.push_back(Pair("confirmations", entry.nHeight)); + obj.push_back(Pair("spendable", pwalletMain->HaveSpendingKey(entry.address))); + obj.push_back(Pair("address", CZCPaymentAddress(entry.address).ToString())); + obj.push_back(Pair("amount", ValueFromAmount(CAmount(entry.plaintext.value)))); + std::string data(entry.plaintext.memo.begin(), entry.plaintext.memo.end()); + obj.push_back(Pair("memo", HexStr(data))); + results.push_back(obj); + } + } + + return results; +} + + UniValue fundrawtransaction(const UniValue& params, bool fHelp) { if (!EnsureWalletIsAvailable(fHelp)) diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index e2cd01637..841232597 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -3758,3 +3758,80 @@ void CWallet::GetFilteredNotes( } } } + + +/* Find unspent notes filtered by payment address, min depth and max depth */ +void CWallet::GetUnspentFilteredNotes( + std::vector& outEntries, + std::set& filterAddresses, + int minDepth, + int maxDepth, + bool requireSpendingKey) +{ + LOCK2(cs_main, cs_wallet); + + for (auto & p : mapWallet) { + CWalletTx wtx = p.second; + + // Filter the transactions before checking for notes + if (!CheckFinalTx(wtx) || wtx.GetBlocksToMaturity() > 0 || wtx.GetDepthInMainChain() < minDepth || wtx.GetDepthInMainChain() > maxDepth) { + continue; + } + + if (wtx.mapNoteData.size() == 0) { + continue; + } + + for (auto & pair : wtx.mapNoteData) { + JSOutPoint jsop = pair.first; + CNoteData nd = pair.second; + PaymentAddress pa = nd.address; + + // 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 && IsSpent(*nd.nullifier)) { + continue; + } + + // skip notes where the spending key is not available + if (requireSpendingKey && !HaveSpendingKey(pa)) { + continue; + } + + int i = jsop.js; // Index into CTransaction.vjoinsplit + int j = jsop.n; // Index into JSDescription.ciphertexts + + // Get cached decryptor + ZCNoteDecryption decryptor; + if (!GetNoteDecryptor(pa, decryptor)) { + // Note decryptors are created when the wallet is loaded, so it should always exist + throw std::runtime_error(strprintf("Could not find note decryptor for payment address %s", CZCPaymentAddress(pa).ToString())); + } + + // determine amount of funds in the note + auto hSig = wtx.vjoinsplit[i].h_sig(*pzcashParams, wtx.joinSplitPubKey); + try { + NotePlaintext plaintext = NotePlaintext::decrypt( + decryptor, + wtx.vjoinsplit[i].ciphertexts[j], + wtx.vjoinsplit[i].ephemeralKey, + hSig, + (unsigned char) j); + + outEntries.push_back(CUnspentNotePlaintextEntry{jsop, pa, plaintext, wtx.GetDepthInMainChain()}); + + } catch (const note_decryption_failed &err) { + // Couldn't decrypt with this spending key + throw std::runtime_error(strprintf("Could not decrypt note for payment address %s", CZCPaymentAddress(pa).ToString())); + } catch (const std::exception &exc) { + // Unexpected failure + throw std::runtime_error(strprintf("Error while decrypting note for payment address %s: %s", CZCPaymentAddress(pa).ToString(), exc.what())); + } + } + } +} + diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 82fb1dca0..b43099b4b 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -271,7 +271,13 @@ struct CNotePlaintextEntry libzcash::NotePlaintext plaintext; }; - +/** Decrypted note, location in a transaction, and confirmation height. */ +struct CUnspentNotePlaintextEntry { + JSOutPoint jsop; + libzcash::PaymentAddress address; + libzcash::NotePlaintext plaintext; + int nHeight; +}; /** A transaction with a merkle branch linking it to the block chain. */ class CMerkleTx : public CTransaction @@ -1135,6 +1141,12 @@ public: bool ignoreSpent=true, bool ignoreUnspendable=true); + /* Find unspent notes filtered by payment address, min depth and max depth */ + void GetUnspentFilteredNotes(std::vector& outEntries, + std::set& filterAddresses, + int minDepth=1, + int maxDepth=INT_MAX, + bool requireSpendingKey=true); }; /** A key allocated from the key pool. */