From 6d3cd3a2f9d00004778fd3adcfd16e413aa161a6 Mon Sep 17 00:00:00 2001 From: Brad Miller Date: Thu, 15 Mar 2018 15:58:31 -0600 Subject: [PATCH] Implement note locking for z_mergetoaddress Co-authored-by: Eirik Ogilvie-Wigley --- qa/rpc-tests/wallet_mergetoaddress.py | 366 +++++++ .../asyncrpcoperation_mergetoaddress.cpp | 947 ++++++++++++++++++ src/wallet/asyncrpcoperation_mergetoaddress.h | 193 ++++ src/wallet/gtest/test_wallet.cpp | 33 + src/wallet/wallet.cpp | 41 + src/wallet/wallet.h | 9 + 6 files changed, 1589 insertions(+) create mode 100755 qa/rpc-tests/wallet_mergetoaddress.py create mode 100644 src/wallet/asyncrpcoperation_mergetoaddress.cpp create mode 100644 src/wallet/asyncrpcoperation_mergetoaddress.h diff --git a/qa/rpc-tests/wallet_mergetoaddress.py b/qa/rpc-tests/wallet_mergetoaddress.py new file mode 100755 index 000000000..e5d5089a4 --- /dev/null +++ b/qa/rpc-tests/wallet_mergetoaddress.py @@ -0,0 +1,366 @@ +#!/usr/bin/env python2 +# Copyright (c) 2017 The Zcash developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.authproxy import JSONRPCException +from test_framework.util import assert_equal, initialize_chain_clean, \ + start_node, connect_nodes_bi, sync_blocks, sync_mempools, \ + wait_and_assert_operationid_status + +from decimal import Decimal + +class WalletMergeToAddressTest (BitcoinTestFramework): + + def setup_chain(self): + print("Initializing test directory "+self.options.tmpdir) + initialize_chain_clean(self.options.tmpdir, 4) + + def setup_network(self, split=False): + args = ['-debug=zrpcunsafe', '-experimentalfeatures', '-zmergetoaddress'] + self.nodes = [] + self.nodes.append(start_node(0, self.options.tmpdir, args)) + self.nodes.append(start_node(1, self.options.tmpdir, args)) + args2 = ['-debug=zrpcunsafe', '-experimentalfeatures', '-zmergetoaddress', '-mempooltxinputlimit=7'] + self.nodes.append(start_node(2, self.options.tmpdir, args2)) + connect_nodes_bi(self.nodes,0,1) + connect_nodes_bi(self.nodes,1,2) + connect_nodes_bi(self.nodes,0,2) + self.is_network_split=False + self.sync_all() + + def run_test (self): + print "Mining blocks..." + + self.nodes[0].generate(1) + do_not_shield_taddr = self.nodes[0].getnewaddress() + + self.nodes[0].generate(4) + walletinfo = self.nodes[0].getwalletinfo() + assert_equal(walletinfo['immature_balance'], 50) + assert_equal(walletinfo['balance'], 0) + self.sync_all() + self.nodes[2].generate(1) + self.nodes[2].getnewaddress() + self.nodes[2].generate(1) + self.nodes[2].getnewaddress() + self.nodes[2].generate(1) + self.sync_all() + self.nodes[1].generate(101) + self.sync_all() + assert_equal(self.nodes[0].getbalance(), 50) + assert_equal(self.nodes[1].getbalance(), 10) + assert_equal(self.nodes[2].getbalance(), 30) + + # Shield the coinbase + myzaddr = self.nodes[0].z_getnewaddress() + result = self.nodes[0].z_shieldcoinbase("*", myzaddr, 0) + wait_and_assert_operationid_status(self.nodes[0], result['opid']) + self.sync_all() + self.nodes[1].generate(1) + self.sync_all() + + # Prepare some UTXOs and notes for merging + mytaddr = self.nodes[0].getnewaddress() + mytaddr2 = self.nodes[0].getnewaddress() + mytaddr3 = self.nodes[0].getnewaddress() + result = self.nodes[0].z_sendmany(myzaddr, [ + {'address': do_not_shield_taddr, 'amount': 10}, + {'address': mytaddr, 'amount': 10}, + {'address': mytaddr2, 'amount': 10}, + {'address': mytaddr3, 'amount': 10}, + ], 1, 0) + wait_and_assert_operationid_status(self.nodes[0], result) + self.sync_all() + self.nodes[1].generate(1) + self.sync_all() + + # Merging will fail because from arguments need to be in an array + try: + self.nodes[0].z_mergetoaddress("*", myzaddr) + assert(False) + except JSONRPCException,e: + errorString = e.error['message'] + assert_equal("JSON value is not an array as expected" in errorString, True) + + # Merging will fail when trying to spend from watch-only address + self.nodes[2].importaddress(mytaddr) + try: + self.nodes[2].z_mergetoaddress([mytaddr], myzaddr) + assert(False) + except JSONRPCException,e: + errorString = e.error['message'] + assert_equal("Could not find any funds to merge" in errorString, True) + + # Merging will fail because fee is negative + try: + self.nodes[0].z_mergetoaddress(["*"], myzaddr, -1) + assert(False) + except JSONRPCException,e: + errorString = e.error['message'] + assert_equal("Amount out of range" in errorString, True) + + # Merging will fail because fee is larger than MAX_MONEY + try: + self.nodes[0].z_mergetoaddress(["*"], myzaddr, Decimal('21000000.00000001')) + assert(False) + except JSONRPCException,e: + errorString = e.error['message'] + assert_equal("Amount out of range" in errorString, True) + + # Merging will fail because fee is larger than sum of UTXOs + try: + self.nodes[0].z_mergetoaddress(["*"], myzaddr, 999) + assert(False) + except JSONRPCException,e: + errorString = e.error['message'] + assert_equal("Insufficient funds" in errorString, True) + + # Merging will fail because transparent limit parameter must be at least 0 + try: + self.nodes[0].z_mergetoaddress(["*"], myzaddr, Decimal('0.001'), -1) + assert(False) + except JSONRPCException,e: + errorString = e.error['message'] + assert_equal("Limit on maximum number of UTXOs cannot be negative" in errorString, True) + + # Merging will fail because transparent limit parameter is absurdly large + try: + self.nodes[0].z_mergetoaddress(["*"], myzaddr, Decimal('0.001'), 99999999999999) + assert(False) + except JSONRPCException,e: + errorString = e.error['message'] + assert_equal("JSON integer out of range" in errorString, True) + + # Merging will fail because shielded limit parameter must be at least 0 + try: + self.nodes[0].z_mergetoaddress(["*"], myzaddr, Decimal('0.001'), 50, -1) + assert(False) + except JSONRPCException,e: + errorString = e.error['message'] + assert_equal("Limit on maximum number of notes cannot be negative" in errorString, True) + + # Merging will fail because shielded limit parameter is absurdly large + try: + self.nodes[0].z_mergetoaddress(["*"], myzaddr, Decimal('0.001'), 50, 99999999999999) + assert(False) + except JSONRPCException,e: + errorString = e.error['message'] + assert_equal("JSON integer out of range" in errorString, True) + + # Merging will fail for this specific case where it would spend a fee and do nothing + try: + self.nodes[0].z_mergetoaddress([mytaddr], mytaddr) + assert(False) + except JSONRPCException,e: + errorString = e.error['message'] + assert_equal("Destination address is also the only source address, and all its funds are already merged" in errorString, True) + + # Merge UTXOs from node 0 of value 30, standard fee of 0.00010000 + result = self.nodes[0].z_mergetoaddress([mytaddr, mytaddr2, mytaddr3], myzaddr) + wait_and_assert_operationid_status(self.nodes[0], result['opid']) + self.sync_all() + self.nodes[1].generate(1) + self.sync_all() + + # Confirm balances and that do_not_shield_taddr containing funds of 10 was left alone + assert_equal(self.nodes[0].getbalance(), 10) + assert_equal(self.nodes[0].z_getbalance(do_not_shield_taddr), Decimal('10.0')) + assert_equal(self.nodes[0].z_getbalance(myzaddr), Decimal('39.99990000')) + assert_equal(self.nodes[1].getbalance(), 40) + assert_equal(self.nodes[2].getbalance(), 30) + + # Shield all notes to another z-addr + myzaddr2 = self.nodes[0].z_getnewaddress() + result = self.nodes[0].z_mergetoaddress(["ANY_ZADDR"], myzaddr2, 0) + assert_equal(result["mergingUTXOs"], Decimal('0')) + assert_equal(result["remainingUTXOs"], Decimal('0')) + assert_equal(result["mergingNotes"], Decimal('2')) + assert_equal(result["remainingNotes"], Decimal('0')) + wait_and_assert_operationid_status(self.nodes[0], result['opid']) + self.sync_all() + blockhash = self.nodes[1].generate(1) + self.sync_all() + + assert_equal(len(self.nodes[0].getblock(blockhash[0])['tx']), 2) + assert_equal(self.nodes[0].z_getbalance(myzaddr), 0) + assert_equal(self.nodes[0].z_getbalance(myzaddr2), Decimal('39.99990000')) + + # Shield coinbase UTXOs from any node 2 taddr, and set fee to 0 + result = self.nodes[2].z_shieldcoinbase("*", myzaddr, 0) + wait_and_assert_operationid_status(self.nodes[2], result['opid']) + self.sync_all() + self.nodes[1].generate(1) + self.sync_all() + + assert_equal(self.nodes[0].getbalance(), 10) + assert_equal(self.nodes[0].z_getbalance(myzaddr), Decimal('30')) + assert_equal(self.nodes[0].z_getbalance(myzaddr2), Decimal('39.99990000')) + assert_equal(self.nodes[1].getbalance(), 60) + assert_equal(self.nodes[2].getbalance(), 0) + + # Merge all notes from node 0 into a node 0 taddr, and set fee to 0 + result = self.nodes[0].z_mergetoaddress(["ANY_ZADDR"], mytaddr, 0) + wait_and_assert_operationid_status(self.nodes[0], result['opid']) + self.sync_all() + self.nodes[1].generate(1) + self.sync_all() + + assert_equal(self.nodes[0].getbalance(), Decimal('79.99990000')) + assert_equal(self.nodes[0].z_getbalance(do_not_shield_taddr), Decimal('10.0')) + assert_equal(self.nodes[0].z_getbalance(mytaddr), Decimal('69.99990000')) + assert_equal(self.nodes[0].z_getbalance(myzaddr), 0) + assert_equal(self.nodes[0].z_getbalance(myzaddr2), 0) + assert_equal(self.nodes[1].getbalance(), 70) + assert_equal(self.nodes[2].getbalance(), 0) + + # Merge all node 0 UTXOs together into a node 1 taddr, and set fee to 0 + self.nodes[1].getnewaddress() # Ensure we have an empty address + n1taddr = self.nodes[1].getnewaddress() + result = self.nodes[0].z_mergetoaddress(["ANY_TADDR"], n1taddr, 0) + wait_and_assert_operationid_status(self.nodes[0], result['opid']) + self.sync_all() + self.nodes[1].generate(1) + self.sync_all() + + assert_equal(self.nodes[0].getbalance(), 0) + assert_equal(self.nodes[0].z_getbalance(do_not_shield_taddr), 0) + assert_equal(self.nodes[0].z_getbalance(mytaddr), 0) + assert_equal(self.nodes[0].z_getbalance(myzaddr), 0) + assert_equal(self.nodes[1].getbalance(), Decimal('159.99990000')) + assert_equal(self.nodes[1].z_getbalance(n1taddr), Decimal('79.99990000')) + assert_equal(self.nodes[2].getbalance(), 0) + + # Generate 800 regular UTXOs on node 0, and 20 regular UTXOs on node 2 + mytaddr = self.nodes[0].getnewaddress() + n2taddr = self.nodes[2].getnewaddress() + self.nodes[1].generate(1000) + self.sync_all() + for i in range(800): + self.nodes[1].sendtoaddress(mytaddr, 1) + for i in range(20): + self.nodes[1].sendtoaddress(n2taddr, 1) + self.nodes[1].generate(1) + self.sync_all() + + # Merging the 800 UTXOs will occur over two transactions, since max tx size is 100,000 bytes. + # We don't verify mergingTransparentValue as UTXOs are not selected in any specific order, so value can change on each test run. + # We set an unrealistically high limit parameter of 99999, to verify that max tx size will constrain the number of UTXOs. + result = self.nodes[0].z_mergetoaddress([mytaddr], myzaddr, 0, 99999) + assert_equal(result["mergingUTXOs"], Decimal('662')) + assert_equal(result["remainingUTXOs"], Decimal('138')) + assert_equal(result["mergingNotes"], Decimal('0')) + assert_equal(result["mergingShieldedValue"], Decimal('0')) + assert_equal(result["remainingNotes"], Decimal('0')) + assert_equal(result["remainingShieldedValue"], Decimal('0')) + remainingTransparentValue = result["remainingTransparentValue"] + opid1 = result['opid'] + + # Verify that UTXOs are locked (not available for selection) by queuing up another merging operation + result = self.nodes[0].z_mergetoaddress([mytaddr], myzaddr, 0, 0) + assert_equal(result["mergingUTXOs"], Decimal('138')) + assert_equal(result["mergingTransparentValue"], Decimal(remainingTransparentValue)) + assert_equal(result["remainingUTXOs"], Decimal('0')) + assert_equal(result["remainingTransparentValue"], Decimal('0')) + assert_equal(result["mergingNotes"], Decimal('0')) + assert_equal(result["mergingShieldedValue"], Decimal('0')) + assert_equal(result["remainingNotes"], Decimal('0')) + assert_equal(result["remainingShieldedValue"], Decimal('0')) + opid2 = result['opid'] + + # wait for both aysnc operations to complete + wait_and_assert_operationid_status(self.nodes[0], opid1) + wait_and_assert_operationid_status(self.nodes[0], opid2) + + # sync_all() invokes sync_mempool() but node 2's mempool limit will cause tx1 and tx2 to be rejected. + # So instead, we sync on blocks and mempool for node 0 and node 1, and after a new block is generated + # which mines tx1 and tx2, all nodes will have an empty mempool which can then be synced. + sync_blocks(self.nodes[:2]) + sync_mempools(self.nodes[:2]) + # Generate enough blocks to ensure all transactions are mined + while self.nodes[1].getmempoolinfo()['size'] > 0: + self.nodes[1].generate(1) + self.sync_all() + + # Verify maximum number of UTXOs which node 2 can shield is limited by option -mempooltxinputlimit + # This option is used when the limit parameter is set to 0. + result = self.nodes[2].z_mergetoaddress([n2taddr], myzaddr, Decimal('0.0001'), 0) + assert_equal(result["mergingUTXOs"], Decimal('7')) + assert_equal(result["remainingUTXOs"], Decimal('13')) + assert_equal(result["mergingNotes"], Decimal('0')) + assert_equal(result["remainingNotes"], Decimal('0')) + wait_and_assert_operationid_status(self.nodes[2], result['opid']) + self.sync_all() + self.nodes[1].generate(1) + self.sync_all() + + # Verify maximum number of UTXOs which node 0 can shield is set by default limit parameter of 50 + mytaddr = self.nodes[0].getnewaddress() + for i in range(100): + self.nodes[1].sendtoaddress(mytaddr, 1) + self.nodes[1].generate(1) + self.sync_all() + result = self.nodes[0].z_mergetoaddress([mytaddr], myzaddr, Decimal('0.0001')) + assert_equal(result["mergingUTXOs"], Decimal('50')) + assert_equal(result["remainingUTXOs"], Decimal('50')) + assert_equal(result["mergingNotes"], Decimal('0')) + # Remaining notes are only counted if we are trying to merge any notes + assert_equal(result["remainingNotes"], Decimal('0')) + wait_and_assert_operationid_status(self.nodes[0], result['opid']) + + # Verify maximum number of UTXOs which node 0 can shield can be set by the limit parameter + result = self.nodes[0].z_mergetoaddress([mytaddr], myzaddr, Decimal('0.0001'), 33) + assert_equal(result["mergingUTXOs"], Decimal('33')) + assert_equal(result["remainingUTXOs"], Decimal('17')) + assert_equal(result["mergingNotes"], Decimal('0')) + # Remaining notes are only counted if we are trying to merge any notes + assert_equal(result["remainingNotes"], Decimal('0')) + wait_and_assert_operationid_status(self.nodes[0], result['opid']) + # Don't sync node 2 which rejects the tx due to its mempooltxinputlimit + sync_blocks(self.nodes[:2]) + sync_mempools(self.nodes[:2]) + self.nodes[1].generate(1) + self.sync_all() + + # Verify maximum number of notes which node 0 can shield can be set by the limit parameter + # Also check that we can set off a second merge before the first one is complete + + # myzaddr has 5 notes at this point + result1 = self.nodes[0].z_mergetoaddress([myzaddr], myzaddr, 0.0001, 50, 2) + result2 = self.nodes[0].z_mergetoaddress([myzaddr], myzaddr, 0.0001, 50, 2) + + # First merge should select from all notes + assert_equal(result1["mergingUTXOs"], Decimal('0')) + # Remaining UTXOs are only counted if we are trying to merge any UTXOs + assert_equal(result1["remainingUTXOs"], Decimal('0')) + assert_equal(result1["mergingNotes"], Decimal('2')) + assert_equal(result1["remainingNotes"], Decimal('3')) + + # Second merge should ignore locked notes + assert_equal(result2["mergingUTXOs"], Decimal('0')) + assert_equal(result2["remainingUTXOs"], Decimal('0')) + assert_equal(result2["mergingNotes"], Decimal('2')) + assert_equal(result2["remainingNotes"], Decimal('1')) + wait_and_assert_operationid_status(self.nodes[0], result1['opid']) + wait_and_assert_operationid_status(self.nodes[0], result2['opid']) + + self.sync_all() + self.nodes[1].generate(1) + self.sync_all() + + # Shield both UTXOs and notes to a z-addr + result = self.nodes[0].z_mergetoaddress(["*"], myzaddr, 0, 10, 2) + assert_equal(result["mergingUTXOs"], Decimal('10')) + assert_equal(result["remainingUTXOs"], Decimal('7')) + assert_equal(result["mergingNotes"], Decimal('2')) + assert_equal(result["remainingNotes"], Decimal('1')) + wait_and_assert_operationid_status(self.nodes[0], result['opid']) + # Don't sync node 2 which rejects the tx due to its mempooltxinputlimit + sync_blocks(self.nodes[:2]) + sync_mempools(self.nodes[:2]) + self.nodes[1].generate(1) + self.sync_all() + +if __name__ == '__main__': + WalletMergeToAddressTest().main() diff --git a/src/wallet/asyncrpcoperation_mergetoaddress.cpp b/src/wallet/asyncrpcoperation_mergetoaddress.cpp new file mode 100644 index 000000000..fa823f50a --- /dev/null +++ b/src/wallet/asyncrpcoperation_mergetoaddress.cpp @@ -0,0 +1,947 @@ +// Copyright (c) 2017 The Zcash developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "asyncrpcoperation_mergetoaddress.h" + +#include "amount.h" +#include "asyncrpcqueue.h" +#include "core_io.h" +#include "init.h" +#include "main.h" +#include "miner.h" +#include "net.h" +#include "netbase.h" +#include "rpcprotocol.h" +#include "rpcserver.h" +#include "script/interpreter.h" +#include "sodium.h" +#include "timedata.h" +#include "util.h" +#include "utilmoneystr.h" +#include "utiltime.h" +#include "wallet.h" +#include "walletdb.h" +#include "zcash/IncrementalMerkleTree.hpp" + +#include +#include +#include +#include + +#include "paymentdisclosuredb.h" + +using namespace libzcash; + +int mta_find_output(UniValue obj, int n) +{ + UniValue outputMapValue = find_value(obj, "outputmap"); + if (!outputMapValue.isArray()) { + throw JSONRPCError(RPC_WALLET_ERROR, "Missing outputmap for JoinSplit operation"); + } + + UniValue outputMap = outputMapValue.get_array(); + assert(outputMap.size() == ZC_NUM_JS_OUTPUTS); + for (size_t i = 0; i < outputMap.size(); i++) { + if (outputMap[i].get_int() == n) { + return i; + } + } + + throw std::logic_error("n is not present in outputmap"); +} + +AsyncRPCOperation_mergetoaddress::AsyncRPCOperation_mergetoaddress( + CMutableTransaction contextualTx, + std::vector utxoInputs, + std::vector noteInputs, + MergeToAddressRecipient recipient, + CAmount fee, + UniValue contextInfo) : + tx_(contextualTx), utxoInputs_(utxoInputs), noteInputs_(noteInputs), + recipient_(recipient), fee_(fee), contextinfo_(contextInfo) +{ + if (fee < 0 || fee > MAX_MONEY) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Fee is out of range"); + } + + if (utxoInputs.empty() && noteInputs.empty()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "No inputs"); + } + + if (std::get<0>(recipient).size() == 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Recipient parameter missing"); + } + + toTaddr_ = CBitcoinAddress(std::get<0>(recipient)); + isToTaddr_ = toTaddr_.IsValid(); + isToZaddr_ = false; + + if (!isToTaddr_) { + CZCPaymentAddress address(std::get<0>(recipient)); + try { + PaymentAddress addr = address.Get(); + + isToZaddr_ = true; + toPaymentAddress_ = addr; + } catch (const std::runtime_error& e) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, string("runtime error: ") + e.what()); + } + } + + // Log the context info i.e. the call parameters to z_mergetoaddress + if (LogAcceptCategory("zrpcunsafe")) { + LogPrint("zrpcunsafe", "%s: z_mergetoaddress initialized (params=%s)\n", getId(), contextInfo.write()); + } else { + LogPrint("zrpc", "%s: z_mergetoaddress initialized\n", getId()); + } + + // Lock UTXOs + lock_utxos(); + lock_notes(); + + // Enable payment disclosure if requested + paymentDisclosureMode = fExperimentalMode && GetBoolArg("-paymentdisclosure", false); +} + +AsyncRPCOperation_mergetoaddress::~AsyncRPCOperation_mergetoaddress() +{ +} + +void AsyncRPCOperation_mergetoaddress::main() +{ + if (isCancelled()) { + unlock_utxos(); // clean up + unlock_notes(); + return; + } + + set_state(OperationStatus::EXECUTING); + start_execution_clock(); + + bool success = false; + +#ifdef ENABLE_MINING +#ifdef ENABLE_WALLET + GenerateBitcoins(false, NULL, 0); +#else + GenerateBitcoins(false, 0); +#endif +#endif + + try { + success = main_impl(); + } catch (const UniValue& objError) { + int code = find_value(objError, "code").get_int(); + std::string message = find_value(objError, "message").get_str(); + set_error_code(code); + set_error_message(message); + } catch (const runtime_error& e) { + set_error_code(-1); + set_error_message("runtime error: " + string(e.what())); + } catch (const logic_error& e) { + set_error_code(-1); + set_error_message("logic error: " + string(e.what())); + } catch (const exception& e) { + set_error_code(-1); + set_error_message("general exception: " + string(e.what())); + } catch (...) { + set_error_code(-2); + set_error_message("unknown error"); + } + +#ifdef ENABLE_MINING +#ifdef ENABLE_WALLET + GenerateBitcoins(GetBoolArg("-gen", false), pwalletMain, GetArg("-genproclimit", 1)); +#else + GenerateBitcoins(GetBoolArg("-gen", false), GetArg("-genproclimit", 1)); +#endif +#endif + + stop_execution_clock(); + + if (success) { + set_state(OperationStatus::SUCCESS); + } else { + set_state(OperationStatus::FAILED); + } + + std::string s = strprintf("%s: z_mergetoaddress finished (status=%s", getId(), getStateAsString()); + if (success) { + s += strprintf(", txid=%s)\n", tx_.GetHash().ToString()); + } else { + s += strprintf(", error=%s)\n", getErrorMessage()); + } + LogPrintf("%s", s); + + unlock_utxos(); // clean up + unlock_notes(); // clean up + + // !!! Payment disclosure START + if (success && paymentDisclosureMode && paymentDisclosureData_.size() > 0) { + uint256 txidhash = tx_.GetHash(); + std::shared_ptr db = PaymentDisclosureDB::sharedInstance(); + for (PaymentDisclosureKeyInfo p : paymentDisclosureData_) { + p.first.hash = txidhash; + if (!db->Put(p.first, p.second)) { + LogPrint("paymentdisclosure", "%s: Payment Disclosure: Error writing entry to database for key %s\n", getId(), p.first.ToString()); + } else { + LogPrint("paymentdisclosure", "%s: Payment Disclosure: Successfully added entry to database for key %s\n", getId(), p.first.ToString()); + } + } + } + // !!! Payment disclosure END +} + +// Notes: +// 1. #1359 Currently there is no limit set on the number of joinsplits, so size of tx could be invalid. +// 2. #1277 Spendable notes are not locked, so an operation running in parallel could also try to use them. +bool AsyncRPCOperation_mergetoaddress::main_impl() +{ + assert(isToTaddr_ != isToZaddr_); + + bool isPureTaddrOnlyTx = (noteInputs_.empty() && isToTaddr_); + CAmount minersFee = fee_; + + size_t numInputs = utxoInputs_.size(); + + // Check mempooltxinputlimit to avoid creating a transaction which the local mempool rejects + size_t limit = (size_t)GetArg("-mempooltxinputlimit", 0); + if (limit > 0 && numInputs > limit) { + throw JSONRPCError(RPC_WALLET_ERROR, + strprintf("Number of transparent inputs %d is greater than mempooltxinputlimit of %d", + numInputs, limit)); + } + + CAmount t_inputs_total = 0; + for (MergeToAddressInputUTXO& t : utxoInputs_) { + t_inputs_total += std::get<1>(t); + } + + CAmount z_inputs_total = 0; + for (MergeToAddressInputNote& t : noteInputs_) { + z_inputs_total += std::get<2>(t); + } + + CAmount targetAmount = z_inputs_total + t_inputs_total; + + if (targetAmount <= minersFee) { + throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, + strprintf("Insufficient funds, have %s and miners fee is %s", + FormatMoney(targetAmount), FormatMoney(minersFee))); + } + + CAmount sendAmount = targetAmount - minersFee; + + // update the transaction with the UTXO inputs and output (if any) + CMutableTransaction rawTx(tx_); + for (MergeToAddressInputUTXO& t : utxoInputs_) { + CTxIn in(std::get<0>(t)); + rawTx.vin.push_back(in); + } + if (isToTaddr_) { + CScript scriptPubKey = GetScriptForDestination(toTaddr_.Get()); + CTxOut out(sendAmount, scriptPubKey); + rawTx.vout.push_back(out); + } + tx_ = CTransaction(rawTx); + + LogPrint(isPureTaddrOnlyTx ? "zrpc" : "zrpcunsafe", "%s: spending %s to send %s with fee %s\n", + getId(), FormatMoney(targetAmount), FormatMoney(sendAmount), FormatMoney(minersFee)); + LogPrint("zrpc", "%s: transparent input: %s\n", getId(), FormatMoney(t_inputs_total)); + LogPrint("zrpcunsafe", "%s: private input: %s\n", getId(), FormatMoney(z_inputs_total)); + if (isToTaddr_) { + LogPrint("zrpc", "%s: transparent output: %s\n", getId(), FormatMoney(sendAmount)); + } else { + LogPrint("zrpcunsafe", "%s: private output: %s\n", getId(), FormatMoney(sendAmount)); + } + LogPrint("zrpc", "%s: fee: %s\n", getId(), FormatMoney(minersFee)); + + // Grab the current consensus branch ID + { + LOCK(cs_main); + consensusBranchId_ = CurrentEpochBranchId(chainActive.Height() + 1, Params().GetConsensus()); + } + + /** + * SCENARIO #1 + * + * taddrs -> taddr + * + * There are no zaddrs or joinsplits involved. + */ + if (isPureTaddrOnlyTx) { + UniValue obj(UniValue::VOBJ); + obj.push_back(Pair("rawtxn", EncodeHexTx(tx_))); + sign_send_raw_transaction(obj); + return true; + } + /** + * END SCENARIO #1 + */ + + + // Prepare raw transaction to handle JoinSplits + CMutableTransaction mtx(tx_); + crypto_sign_keypair(joinSplitPubKey_.begin(), joinSplitPrivKey_); + mtx.joinSplitPubKey = joinSplitPubKey_; + tx_ = CTransaction(mtx); + std::string hexMemo = std::get<1>(recipient_); + + + /** + * SCENARIO #2 + * + * taddrs -> zaddr + * + * We only need a single JoinSplit. + */ + if (noteInputs_.empty() && isToZaddr_) { + // Create JoinSplit to target z-addr. + MergeToAddressJSInfo info; + info.vpub_old = sendAmount; + info.vpub_new = 0; + + JSOutput jso = JSOutput(toPaymentAddress_, sendAmount); + if (hexMemo.size() > 0) { + jso.memo = get_memo_from_hex_string(hexMemo); + } + info.vjsout.push_back(jso); + + UniValue obj(UniValue::VOBJ); + obj = perform_joinsplit(info); + sign_send_raw_transaction(obj); + return true; + } + /** + * END SCENARIO #2 + */ + + + // Copy zinputs to more flexible containers + std::deque zInputsDeque; + for (auto o : noteInputs_) { + zInputsDeque.push_back(o); + } + + // When spending notes, take a snapshot of note witnesses and anchors as the treestate will + // change upon arrival of new blocks which contain joinsplit transactions. This is likely + // to happen as creating a chained joinsplit transaction can take longer than the block interval. + { + LOCK2(cs_main, pwalletMain->cs_wallet); + for (auto t : noteInputs_) { + JSOutPoint jso = std::get<0>(t); + std::vector vOutPoints = {jso}; + uint256 inputAnchor; + std::vector> vInputWitnesses; + pwalletMain->GetNoteWitnesses(vOutPoints, vInputWitnesses, inputAnchor); + jsopWitnessAnchorMap[jso.ToString()] = MergeToAddressWitnessAnchorData{vInputWitnesses[0], inputAnchor}; + } + } + + /** + * SCENARIO #3 + * + * zaddrs -> zaddr + * taddrs -> + * + * zaddrs -> + * taddrs -> taddr + * + * Send to zaddr by chaining JoinSplits together and immediately consuming any change + * Send to taddr by creating dummy z outputs and accumulating value in a change note + * which is used to set vpub_new in the last chained joinsplit. + */ + UniValue obj(UniValue::VOBJ); + CAmount jsChange = 0; // this is updated after each joinsplit + int changeOutputIndex = -1; // this is updated after each joinsplit if jsChange > 0 + bool vpubOldProcessed = false; // updated when vpub_old for taddr inputs is set in first joinsplit + bool vpubNewProcessed = false; // updated when vpub_new for miner fee and taddr outputs is set in last joinsplit + + // At this point, we are guaranteed to have at least one input note. + // Use address of first input note as the temporary change address. + SpendingKey changeKey = std::get<3>(zInputsDeque.front()); + PaymentAddress changeAddress = changeKey.address(); + + CAmount vpubOldTarget = 0; + CAmount vpubNewTarget = 0; + if (isToTaddr_) { + vpubNewTarget = z_inputs_total; + } else { + if (utxoInputs_.empty()) { + vpubNewTarget = minersFee; + } else { + vpubOldTarget = t_inputs_total - minersFee; + } + } + + // Keep track of treestate within this transaction + boost::unordered_map intermediates; + std::vector previousCommitments; + + while (!vpubNewProcessed) { + MergeToAddressJSInfo info; + info.vpub_old = 0; + info.vpub_new = 0; + + // Set vpub_old in the first joinsplit + if (!vpubOldProcessed) { + if (t_inputs_total < vpubOldTarget) { + throw JSONRPCError(RPC_WALLET_ERROR, + strprintf("Insufficient transparent funds for vpub_old %s (miners fee %s, taddr inputs %s)", + FormatMoney(vpubOldTarget), FormatMoney(minersFee), FormatMoney(t_inputs_total))); + } + info.vpub_old += vpubOldTarget; // funds flowing from public pool + vpubOldProcessed = true; + } + + CAmount jsInputValue = 0; + uint256 jsAnchor; + std::vector> witnesses; + + JSDescription prevJoinSplit; + + // Keep track of previous JoinSplit and its commitments + if (tx_.vjoinsplit.size() > 0) { + prevJoinSplit = tx_.vjoinsplit.back(); + } + + // If there is no change, the chain has terminated so we can reset the tracked treestate. + if (jsChange == 0 && tx_.vjoinsplit.size() > 0) { + intermediates.clear(); + previousCommitments.clear(); + } + + // + // Consume change as the first input of the JoinSplit. + // + if (jsChange > 0) { + LOCK2(cs_main, pwalletMain->cs_wallet); + + // Update tree state with previous joinsplit + ZCIncrementalMerkleTree tree; + auto it = intermediates.find(prevJoinSplit.anchor); + if (it != intermediates.end()) { + tree = it->second; + } else if (!pcoinsTip->GetAnchorAt(prevJoinSplit.anchor, tree)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Could not find previous JoinSplit anchor"); + } + + assert(changeOutputIndex != -1); + boost::optional changeWitness; + int n = 0; + for (const uint256& commitment : prevJoinSplit.commitments) { + tree.append(commitment); + previousCommitments.push_back(commitment); + if (!changeWitness && changeOutputIndex == n++) { + changeWitness = tree.witness(); + } else if (changeWitness) { + changeWitness.get().append(commitment); + } + } + if (changeWitness) { + witnesses.push_back(changeWitness); + } + jsAnchor = tree.root(); + intermediates.insert(std::make_pair(tree.root(), tree)); // chained js are interstitial (found in between block boundaries) + + // Decrypt the change note's ciphertext to retrieve some data we need + ZCNoteDecryption decryptor(changeKey.receiving_key()); + auto hSig = prevJoinSplit.h_sig(*pzcashParams, tx_.joinSplitPubKey); + try { + NotePlaintext plaintext = NotePlaintext::decrypt( + decryptor, + prevJoinSplit.ciphertexts[changeOutputIndex], + prevJoinSplit.ephemeralKey, + hSig, + (unsigned char)changeOutputIndex); + + Note note = plaintext.note(changeAddress); + info.notes.push_back(note); + info.zkeys.push_back(changeKey); + + jsInputValue += plaintext.value; + + LogPrint("zrpcunsafe", "%s: spending change (amount=%s)\n", + getId(), + FormatMoney(plaintext.value)); + + } catch (const std::exception& e) { + throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Error decrypting output note of previous JoinSplit: %s", e.what())); + } + } + + + // + // Consume spendable non-change notes + // + std::vector vInputNotes; + std::vector vInputZKeys; + std::vector vOutPoints; + std::vector> vInputWitnesses; + uint256 inputAnchor; + int numInputsNeeded = (jsChange > 0) ? 1 : 0; + while (numInputsNeeded++ < ZC_NUM_JS_INPUTS && zInputsDeque.size() > 0) { + MergeToAddressInputNote t = zInputsDeque.front(); + JSOutPoint jso = std::get<0>(t); + Note note = std::get<1>(t); + CAmount noteFunds = std::get<2>(t); + SpendingKey zkey = std::get<3>(t); + zInputsDeque.pop_front(); + + MergeToAddressWitnessAnchorData wad = jsopWitnessAnchorMap[jso.ToString()]; + vInputWitnesses.push_back(wad.witness); + if (inputAnchor.IsNull()) { + inputAnchor = wad.anchor; + } else if (inputAnchor != wad.anchor) { + throw JSONRPCError(RPC_WALLET_ERROR, "Selected input notes do not share the same anchor"); + } + + vOutPoints.push_back(jso); + vInputNotes.push_back(note); + vInputZKeys.push_back(zkey); + + jsInputValue += noteFunds; + + int wtxHeight = -1; + int wtxDepth = -1; + { + LOCK2(cs_main, pwalletMain->cs_wallet); + const CWalletTx& wtx = pwalletMain->mapWallet[jso.hash]; + // Zero confirmation notes belong to transactions which have not yet been mined + if (mapBlockIndex.find(wtx.hashBlock) == mapBlockIndex.end()) { + throw JSONRPCError(RPC_WALLET_ERROR, strprintf("mapBlockIndex does not contain block hash %s", wtx.hashBlock.ToString())); + } + wtxHeight = mapBlockIndex[wtx.hashBlock]->nHeight; + wtxDepth = wtx.GetDepthInMainChain(); + } + LogPrint("zrpcunsafe", "%s: spending note (txid=%s, vjoinsplit=%d, ciphertext=%d, amount=%s, height=%d, confirmations=%d)\n", + getId(), + jso.hash.ToString().substr(0, 10), + jso.js, + int(jso.n), // uint8_t + FormatMoney(noteFunds), + wtxHeight, + wtxDepth); + } + + // Add history of previous commitments to witness + if (vInputNotes.size() > 0) { + if (vInputWitnesses.size() == 0) { + throw JSONRPCError(RPC_WALLET_ERROR, "Could not find witness for note commitment"); + } + + for (auto& optionalWitness : vInputWitnesses) { + if (!optionalWitness) { + throw JSONRPCError(RPC_WALLET_ERROR, "Witness for note commitment is null"); + } + ZCIncrementalWitness w = *optionalWitness; // could use .get(); + if (jsChange > 0) { + for (const uint256& commitment : previousCommitments) { + w.append(commitment); + } + if (jsAnchor != w.root()) { + throw JSONRPCError(RPC_WALLET_ERROR, "Witness for spendable note does not have same anchor as change input"); + } + } + witnesses.push_back(w); + } + + // The jsAnchor is null if this JoinSplit is at the start of a new chain + if (jsAnchor.IsNull()) { + jsAnchor = inputAnchor; + } + + // Add spendable notes as inputs + std::copy(vInputNotes.begin(), vInputNotes.end(), std::back_inserter(info.notes)); + std::copy(vInputZKeys.begin(), vInputZKeys.end(), std::back_inserter(info.zkeys)); + } + + // Accumulate change + jsChange = jsInputValue + info.vpub_old; + + // Set vpub_new in the last joinsplit (when there are no more notes to spend) + if (zInputsDeque.empty()) { + assert(!vpubNewProcessed); + if (jsInputValue < vpubNewTarget) { + throw JSONRPCError(RPC_WALLET_ERROR, + strprintf("Insufficient funds for vpub_new %s (miners fee %s, taddr inputs %s)", + FormatMoney(vpubNewTarget), FormatMoney(minersFee), FormatMoney(t_inputs_total))); + } + info.vpub_new += vpubNewTarget; // funds flowing back to public pool + vpubNewProcessed = true; + jsChange -= vpubNewTarget; + // If we are merging to a t-addr, there should be no change + if (isToTaddr_) assert(jsChange == 0); + } + + // create dummy output + info.vjsout.push_back(JSOutput()); // dummy output while we accumulate funds into a change note for vpub_new + + // create output for any change + if (jsChange > 0) { + std::string outputType = "change"; + auto jso = JSOutput(changeAddress, jsChange); + // If this is the final output, set the target and memo + if (isToZaddr_ && vpubNewProcessed) { + outputType = "target"; + jso.addr = toPaymentAddress_; + if (!hexMemo.empty()) { + jso.memo = get_memo_from_hex_string(hexMemo); + } + } + info.vjsout.push_back(jso); + + LogPrint("zrpcunsafe", "%s: generating note for %s (amount=%s)\n", + getId(), + outputType, + FormatMoney(jsChange)); + } + + obj = perform_joinsplit(info, witnesses, jsAnchor); + + if (jsChange > 0) { + changeOutputIndex = mta_find_output(obj, 1); + } + } + + // Sanity check in case changes to code block above exits loop by invoking 'break' + assert(zInputsDeque.size() == 0); + assert(vpubNewProcessed); + + sign_send_raw_transaction(obj); + return true; +} + + +/** + * Sign and send a raw transaction. + * Raw transaction as hex string should be in object field "rawtxn" + */ +void AsyncRPCOperation_mergetoaddress::sign_send_raw_transaction(UniValue obj) +{ + // Sign the raw transaction + UniValue rawtxnValue = find_value(obj, "rawtxn"); + if (rawtxnValue.isNull()) { + throw JSONRPCError(RPC_WALLET_ERROR, "Missing hex data for raw transaction"); + } + std::string rawtxn = rawtxnValue.get_str(); + + UniValue params = UniValue(UniValue::VARR); + params.push_back(rawtxn); + UniValue signResultValue = signrawtransaction(params, false); + UniValue signResultObject = signResultValue.get_obj(); + UniValue completeValue = find_value(signResultObject, "complete"); + bool complete = completeValue.get_bool(); + if (!complete) { + // TODO: #1366 Maybe get "errors" and print array vErrors into a string + throw JSONRPCError(RPC_WALLET_ENCRYPTION_FAILED, "Failed to sign transaction"); + } + + UniValue hexValue = find_value(signResultObject, "hex"); + if (hexValue.isNull()) { + throw JSONRPCError(RPC_WALLET_ERROR, "Missing hex data for signed transaction"); + } + std::string signedtxn = hexValue.get_str(); + + // Send the signed transaction + if (!testmode) { + params.clear(); + params.setArray(); + params.push_back(signedtxn); + UniValue sendResultValue = sendrawtransaction(params, false); + if (sendResultValue.isNull()) { + throw JSONRPCError(RPC_WALLET_ERROR, "Send raw transaction did not return an error or a txid."); + } + + std::string txid = sendResultValue.get_str(); + + UniValue o(UniValue::VOBJ); + o.push_back(Pair("txid", txid)); + set_result(o); + } else { + // Test mode does not send the transaction to the network. + + CDataStream stream(ParseHex(signedtxn), SER_NETWORK, PROTOCOL_VERSION); + CTransaction tx; + stream >> tx; + + UniValue o(UniValue::VOBJ); + o.push_back(Pair("test", 1)); + o.push_back(Pair("txid", tx.GetHash().ToString())); + o.push_back(Pair("hex", signedtxn)); + set_result(o); + } + + // Keep the signed transaction so we can hash to the same txid + CDataStream stream(ParseHex(signedtxn), SER_NETWORK, PROTOCOL_VERSION); + CTransaction tx; + stream >> tx; + tx_ = tx; +} + + +UniValue AsyncRPCOperation_mergetoaddress::perform_joinsplit(MergeToAddressJSInfo& info) +{ + std::vector> witnesses; + uint256 anchor; + { + LOCK(cs_main); + anchor = pcoinsTip->GetBestAnchor(); // As there are no inputs, ask the wallet for the best anchor + } + return perform_joinsplit(info, witnesses, anchor); +} + + +UniValue AsyncRPCOperation_mergetoaddress::perform_joinsplit(MergeToAddressJSInfo& info, std::vector& outPoints) +{ + std::vector> witnesses; + uint256 anchor; + { + LOCK(cs_main); + pwalletMain->GetNoteWitnesses(outPoints, witnesses, anchor); + } + return perform_joinsplit(info, witnesses, anchor); +} + +UniValue AsyncRPCOperation_mergetoaddress::perform_joinsplit( + MergeToAddressJSInfo& info, + std::vector> witnesses, + uint256 anchor) +{ + if (anchor.IsNull()) { + throw std::runtime_error("anchor is null"); + } + + if (witnesses.size() != info.notes.size()) { + throw runtime_error("number of notes and witnesses do not match"); + } + + if (info.notes.size() != info.zkeys.size()) { + throw runtime_error("number of notes and spending keys do not match"); + } + + for (size_t i = 0; i < witnesses.size(); i++) { + if (!witnesses[i]) { + throw runtime_error("joinsplit input could not be found in tree"); + } + info.vjsin.push_back(JSInput(*witnesses[i], info.notes[i], info.zkeys[i])); + } + + // Make sure there are two inputs and two outputs + while (info.vjsin.size() < ZC_NUM_JS_INPUTS) { + info.vjsin.push_back(JSInput()); + } + + while (info.vjsout.size() < ZC_NUM_JS_OUTPUTS) { + info.vjsout.push_back(JSOutput()); + } + + if (info.vjsout.size() != ZC_NUM_JS_INPUTS || info.vjsin.size() != ZC_NUM_JS_OUTPUTS) { + throw runtime_error("unsupported joinsplit input/output counts"); + } + + CMutableTransaction mtx(tx_); + + LogPrint("zrpcunsafe", "%s: creating joinsplit at index %d (vpub_old=%s, vpub_new=%s, in[0]=%s, in[1]=%s, out[0]=%s, out[1]=%s)\n", + getId(), + tx_.vjoinsplit.size(), + FormatMoney(info.vpub_old), FormatMoney(info.vpub_new), + FormatMoney(info.vjsin[0].note.value), FormatMoney(info.vjsin[1].note.value), + FormatMoney(info.vjsout[0].value), FormatMoney(info.vjsout[1].value)); + + // Generate the proof, this can take over a minute. + boost::array inputs{info.vjsin[0], info.vjsin[1]}; + boost::array outputs{info.vjsout[0], info.vjsout[1]}; + boost::array inputMap; + boost::array outputMap; + + uint256 esk; // payment disclosure - secret + + JSDescription jsdesc = JSDescription::Randomized( + *pzcashParams, + joinSplitPubKey_, + anchor, + inputs, + outputs, + inputMap, + outputMap, + info.vpub_old, + info.vpub_new, + !this->testmode, + &esk); // parameter expects pointer to esk, so pass in address + { + auto verifier = libzcash::ProofVerifier::Strict(); + if (!(jsdesc.Verify(*pzcashParams, verifier, joinSplitPubKey_))) { + throw std::runtime_error("error verifying joinsplit"); + } + } + + mtx.vjoinsplit.push_back(jsdesc); + + // Empty output script. + CScript scriptCode; + CTransaction signTx(mtx); + uint256 dataToBeSigned = SignatureHash(scriptCode, signTx, NOT_AN_INPUT, SIGHASH_ALL, 0, consensusBranchId_); + + // Add the signature + if (!(crypto_sign_detached(&mtx.joinSplitSig[0], NULL, + dataToBeSigned.begin(), 32, + joinSplitPrivKey_) == 0)) { + throw std::runtime_error("crypto_sign_detached failed"); + } + + // Sanity check + if (!(crypto_sign_verify_detached(&mtx.joinSplitSig[0], + dataToBeSigned.begin(), 32, + mtx.joinSplitPubKey.begin()) == 0)) { + throw std::runtime_error("crypto_sign_verify_detached failed"); + } + + CTransaction rawTx(mtx); + tx_ = rawTx; + + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss << rawTx; + + std::string encryptedNote1; + std::string encryptedNote2; + { + CDataStream ss2(SER_NETWORK, PROTOCOL_VERSION); + ss2 << ((unsigned char)0x00); + ss2 << jsdesc.ephemeralKey; + ss2 << jsdesc.ciphertexts[0]; + ss2 << jsdesc.h_sig(*pzcashParams, joinSplitPubKey_); + + encryptedNote1 = HexStr(ss2.begin(), ss2.end()); + } + { + CDataStream ss2(SER_NETWORK, PROTOCOL_VERSION); + ss2 << ((unsigned char)0x01); + ss2 << jsdesc.ephemeralKey; + ss2 << jsdesc.ciphertexts[1]; + ss2 << jsdesc.h_sig(*pzcashParams, joinSplitPubKey_); + + encryptedNote2 = HexStr(ss2.begin(), ss2.end()); + } + + UniValue arrInputMap(UniValue::VARR); + UniValue arrOutputMap(UniValue::VARR); + for (size_t i = 0; i < ZC_NUM_JS_INPUTS; i++) { + arrInputMap.push_back(inputMap[i]); + } + for (size_t i = 0; i < ZC_NUM_JS_OUTPUTS; i++) { + arrOutputMap.push_back(outputMap[i]); + } + + + // !!! Payment disclosure START + unsigned char buffer[32] = {0}; + memcpy(&buffer[0], &joinSplitPrivKey_[0], 32); // private key in first half of 64 byte buffer + std::vector vch(&buffer[0], &buffer[0] + 32); + uint256 joinSplitPrivKey = uint256(vch); + size_t js_index = tx_.vjoinsplit.size() - 1; + uint256 placeholder; + for (int i = 0; i < ZC_NUM_JS_OUTPUTS; i++) { + uint8_t mapped_index = outputMap[i]; + // placeholder for txid will be filled in later when tx has been finalized and signed. + PaymentDisclosureKey pdKey = {placeholder, js_index, mapped_index}; + JSOutput output = outputs[mapped_index]; + libzcash::PaymentAddress zaddr = output.addr; // randomized output + PaymentDisclosureInfo pdInfo = {PAYMENT_DISCLOSURE_VERSION_EXPERIMENTAL, esk, joinSplitPrivKey, zaddr}; + paymentDisclosureData_.push_back(PaymentDisclosureKeyInfo(pdKey, pdInfo)); + + CZCPaymentAddress address(zaddr); + LogPrint("paymentdisclosure", "%s: Payment Disclosure: js=%d, n=%d, zaddr=%s\n", getId(), js_index, int(mapped_index), address.ToString()); + } + // !!! Payment disclosure END + + UniValue obj(UniValue::VOBJ); + obj.push_back(Pair("encryptednote1", encryptedNote1)); + obj.push_back(Pair("encryptednote2", encryptedNote2)); + obj.push_back(Pair("rawtxn", HexStr(ss.begin(), ss.end()))); + obj.push_back(Pair("inputmap", arrInputMap)); + obj.push_back(Pair("outputmap", arrOutputMap)); + return obj; +} + +boost::array AsyncRPCOperation_mergetoaddress::get_memo_from_hex_string(std::string s) +{ + boost::array memo = {{0x00}}; + + std::vector rawMemo = ParseHex(s.c_str()); + + // If ParseHex comes across a non-hex char, it will stop but still return results so far. + size_t slen = s.length(); + if (slen % 2 != 0 || (slen > 0 && rawMemo.size() != slen / 2)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Memo must be in hexadecimal format"); + } + + if (rawMemo.size() > ZC_MEMO_SIZE) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Memo size of %d is too big, maximum allowed is %d", rawMemo.size(), ZC_MEMO_SIZE)); + } + + // copy vector into boost array + int lenMemo = rawMemo.size(); + for (int i = 0; i < ZC_MEMO_SIZE && i < lenMemo; i++) { + memo[i] = rawMemo[i]; + } + return memo; +} + +/** + * Override getStatus() to append the operation's input parameters to the default status object. + */ +UniValue AsyncRPCOperation_mergetoaddress::getStatus() const +{ + UniValue v = AsyncRPCOperation::getStatus(); + if (contextinfo_.isNull()) { + return v; + } + + UniValue obj = v.get_obj(); + obj.push_back(Pair("method", "z_mergetoaddress")); + obj.push_back(Pair("params", contextinfo_)); + return obj; +} + +/** + * Lock input utxos + */ + void AsyncRPCOperation_mergetoaddress::lock_utxos() { + LOCK2(cs_main, pwalletMain->cs_wallet); + for (auto utxo : utxoInputs_) { + pwalletMain->LockCoin(std::get<0>(utxo)); + } +} + +/** + * Unlock input utxos + */ +void AsyncRPCOperation_mergetoaddress::unlock_utxos() { + LOCK2(cs_main, pwalletMain->cs_wallet); + for (auto utxo : utxoInputs_) { + pwalletMain->UnlockCoin(std::get<0>(utxo)); + } +} + + +/** + * Lock input notes + */ + void AsyncRPCOperation_mergetoaddress::lock_notes() { + LOCK2(cs_main, pwalletMain->cs_wallet); + for (auto note : noteInputs_) { + pwalletMain->LockNote(std::get<0>(note)); + } +} + +/** + * Unlock input notes + */ +void AsyncRPCOperation_mergetoaddress::unlock_notes() { + LOCK2(cs_main, pwalletMain->cs_wallet); + for (auto note : noteInputs_) { + pwalletMain->UnlockNote(std::get<0>(note)); + } +} diff --git a/src/wallet/asyncrpcoperation_mergetoaddress.h b/src/wallet/asyncrpcoperation_mergetoaddress.h new file mode 100644 index 000000000..34548a5ba --- /dev/null +++ b/src/wallet/asyncrpcoperation_mergetoaddress.h @@ -0,0 +1,193 @@ +// Copyright (c) 2017 The Zcash developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef ASYNCRPCOPERATION_MERGETOADDRESS_H +#define ASYNCRPCOPERATION_MERGETOADDRESS_H + +#include "amount.h" +#include "asyncrpcoperation.h" +#include "base58.h" +#include "paymentdisclosure.h" +#include "primitives/transaction.h" +#include "wallet.h" +#include "zcash/Address.hpp" +#include "zcash/JoinSplit.hpp" + +#include +#include + +#include + +// Default transaction fee if caller does not specify one. +#define MERGE_TO_ADDRESS_OPERATION_DEFAULT_MINERS_FEE 10000 + +using namespace libzcash; + +// Input UTXO is a tuple of txid, vout, amount +typedef std::tuple MergeToAddressInputUTXO; + +// Input JSOP is a tuple of JSOutpoint, note, amount, spending key +typedef std::tuple MergeToAddressInputNote; + +// A recipient is a tuple of address, memo (optional if zaddr) +typedef std::tuple MergeToAddressRecipient; + +// Package of info which is passed to perform_joinsplit methods. +struct MergeToAddressJSInfo { + std::vector vjsin; + std::vector vjsout; + std::vector notes; + std::vector zkeys; + CAmount vpub_old = 0; + CAmount vpub_new = 0; +}; + +// A struct to help us track the witness and anchor for a given JSOutPoint +struct MergeToAddressWitnessAnchorData { + boost::optional witness; + uint256 anchor; +}; + +class AsyncRPCOperation_mergetoaddress : public AsyncRPCOperation +{ +public: + AsyncRPCOperation_mergetoaddress( + CMutableTransaction contextualTx, + std::vector utxoInputs, + std::vector noteInputs, + MergeToAddressRecipient recipient, + CAmount fee = MERGE_TO_ADDRESS_OPERATION_DEFAULT_MINERS_FEE, + UniValue contextInfo = NullUniValue); + virtual ~AsyncRPCOperation_mergetoaddress(); + + // We don't want to be copied or moved around + AsyncRPCOperation_mergetoaddress(AsyncRPCOperation_mergetoaddress const&) = delete; // Copy construct + AsyncRPCOperation_mergetoaddress(AsyncRPCOperation_mergetoaddress&&) = delete; // Move construct + AsyncRPCOperation_mergetoaddress& operator=(AsyncRPCOperation_mergetoaddress const&) = delete; // Copy assign + AsyncRPCOperation_mergetoaddress& operator=(AsyncRPCOperation_mergetoaddress&&) = delete; // Move assign + + virtual void main(); + + virtual UniValue getStatus() const; + + bool testmode = false; // Set to true to disable sending txs and generating proofs + + bool paymentDisclosureMode = false; // Set to true to save esk for encrypted notes in payment disclosure database. + +private: + friend class TEST_FRIEND_AsyncRPCOperation_mergetoaddress; // class for unit testing + + UniValue contextinfo_; // optional data to include in return value from getStatus() + + uint32_t consensusBranchId_; + CAmount fee_; + int mindepth_; + MergeToAddressRecipient recipient_; + bool isToTaddr_; + bool isToZaddr_; + CBitcoinAddress toTaddr_; + PaymentAddress toPaymentAddress_; + + uint256 joinSplitPubKey_; + unsigned char joinSplitPrivKey_[crypto_sign_SECRETKEYBYTES]; + + // The key is the result string from calling JSOutPoint::ToString() + std::unordered_map jsopWitnessAnchorMap; + + std::vector utxoInputs_; + std::vector noteInputs_; + + CTransaction tx_; + + boost::array get_memo_from_hex_string(std::string s); + bool main_impl(); + + // JoinSplit without any input notes to spend + UniValue perform_joinsplit(MergeToAddressJSInfo&); + + // JoinSplit with input notes to spend (JSOutPoints)) + UniValue perform_joinsplit(MergeToAddressJSInfo&, std::vector&); + + // JoinSplit where you have the witnesses and anchor + UniValue perform_joinsplit( + MergeToAddressJSInfo& info, + std::vector> witnesses, + uint256 anchor); + + void sign_send_raw_transaction(UniValue obj); // throws exception if there was an error + + void lock_utxos(); + + void unlock_utxos(); + + void lock_notes(); + + void unlock_notes(); + + // payment disclosure! + std::vector paymentDisclosureData_; +}; + + +// To test private methods, a friend class can act as a proxy +class TEST_FRIEND_AsyncRPCOperation_mergetoaddress +{ +public: + std::shared_ptr delegate; + + TEST_FRIEND_AsyncRPCOperation_mergetoaddress(std::shared_ptr ptr) : delegate(ptr) {} + + CTransaction getTx() + { + return delegate->tx_; + } + + void setTx(CTransaction tx) + { + delegate->tx_ = tx; + } + + // Delegated methods + + boost::array get_memo_from_hex_string(std::string s) + { + return delegate->get_memo_from_hex_string(s); + } + + bool main_impl() + { + return delegate->main_impl(); + } + + UniValue perform_joinsplit(MergeToAddressJSInfo& info) + { + return delegate->perform_joinsplit(info); + } + + UniValue perform_joinsplit(MergeToAddressJSInfo& info, std::vector& v) + { + return delegate->perform_joinsplit(info, v); + } + + UniValue perform_joinsplit( + MergeToAddressJSInfo& info, + std::vector> witnesses, + uint256 anchor) + { + return delegate->perform_joinsplit(info, witnesses, anchor); + } + + void sign_send_raw_transaction(UniValue obj) + { + delegate->sign_send_raw_transaction(obj); + } + + void set_state(OperationStatus state) + { + delegate->state_.store(state); + } +}; + + +#endif /* ASYNCRPCOPERATION_MERGETOADDRESS_H */ diff --git a/src/wallet/gtest/test_wallet.cpp b/src/wallet/gtest/test_wallet.cpp index b39275f67..f73cc1a9a 100644 --- a/src/wallet/gtest/test_wallet.cpp +++ b/src/wallet/gtest/test_wallet.cpp @@ -1046,3 +1046,36 @@ TEST(wallet_tests, MarkAffectedTransactionsDirty) { wallet.MarkAffectedTransactionsDirty(wtx2); EXPECT_FALSE(wallet.mapWallet[hash].fDebitCached); } + +TEST(wallet_tests, NoteLocking) { + TestWallet wallet; + + auto sk = libzcash::SpendingKey::random(); + wallet.AddSpendingKey(sk); + + auto wtx = GetValidReceive(sk, 10, true); + auto wtx2 = GetValidReceive(sk, 10, true); + + JSOutPoint jsoutpt {wtx.GetHash(), 0, 0}; + JSOutPoint jsoutpt2 {wtx2.GetHash(),0, 0}; + + // Test selective locking + wallet.LockNote(jsoutpt); + EXPECT_TRUE(wallet.IsLockedNote(jsoutpt.hash, jsoutpt.js, jsoutpt.n)); + EXPECT_FALSE(wallet.IsLockedNote(jsoutpt2.hash, jsoutpt2.js, jsoutpt2.n)); + + // Test selective unlocking + wallet.UnlockNote(jsoutpt); + EXPECT_FALSE(wallet.IsLockedNote(jsoutpt.hash, jsoutpt.js, jsoutpt.n)); + + // Test multiple locking + wallet.LockNote(jsoutpt); + wallet.LockNote(jsoutpt2); + EXPECT_TRUE(wallet.IsLockedNote(jsoutpt.hash, jsoutpt.js, jsoutpt.n)); + EXPECT_TRUE(wallet.IsLockedNote(jsoutpt2.hash, jsoutpt2.js, jsoutpt2.n)); + + // Test unlock all + wallet.UnlockAllNotes(); + EXPECT_FALSE(wallet.IsLockedNote(jsoutpt.hash, jsoutpt.js, jsoutpt.n)); + EXPECT_FALSE(wallet.IsLockedNote(jsoutpt2.hash, jsoutpt2.js, jsoutpt2.n)); +} diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index a2354b5c2..1d1ef114b 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -3415,6 +3415,42 @@ void CWallet::ListLockedCoins(std::vector& vOutpts) } } + +// Note Locking Operations + +void CWallet::LockNote(JSOutPoint& output) +{ + AssertLockHeld(cs_wallet); // setLockedNotes + setLockedNotes.insert(output); +} + +void CWallet::UnlockNote(JSOutPoint& output) +{ + AssertLockHeld(cs_wallet); // setLockedNotes + setLockedNotes.erase(output); +} + +void CWallet::UnlockAllNotes() +{ + AssertLockHeld(cs_wallet); // setLockedNotes + setLockedNotes.clear(); +} + +bool CWallet::IsLockedNote(uint256 hash, size_t js, uint8_t n) const +{ + AssertLockHeld(cs_wallet); // setLockedNotes + JSOutPoint outpt(hash, js, n); + + return (setLockedNotes.count(outpt) > 0); +} + +std::vector CWallet::ListLockedNotes() +{ + AssertLockHeld(cs_wallet); // setLockedNotes + std::vector vOutpts(setLockedNotes.begin(), setLockedNotes.end()); + return vOutpts; +} + /** @} */ // end of Actions class CAffectedKeysVisitor : public boost::static_visitor { @@ -3690,6 +3726,11 @@ void CWallet::GetFilteredNotes(std::vector & outEntries, st if (ignoreUnspendable && !HaveSpendingKey(pa)) { continue; } + + // skip locked notes + if (IsLockedNote(jsop.hash, jsop.js, jsop.n)) { + continue; + } int i = jsop.js; // Index into CTransaction.vjoinsplit int j = jsop.n; // Index into JSDescription.ciphertexts diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 53157ec9f..a117057e0 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -882,6 +882,7 @@ public: CPubKey vchDefaultKey; std::set setLockedCoins; + std::set setLockedNotes; int64_t nTimeFirstKey; @@ -902,6 +903,14 @@ public: void UnlockAllCoins(); void ListLockedCoins(std::vector& vOutpts); + + bool IsLockedNote(uint256 hash, size_t js, uint8_t n) const; + void LockNote(JSOutPoint& output); + void UnlockNote(JSOutPoint& output); + void UnlockAllNotes(); + std::vector ListLockedNotes(); + + /** * keystore implementation * Generate a new key