From 6e9c7629af1153d436bc7fabf145e2b669419833 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Mon, 11 Dec 2017 16:43:09 +0000 Subject: [PATCH] Implement z_mergetoaddress for combining UTXOs and notes Closes #2493. --- qa/pull-tester/rpc-tests.sh | 1 + qa/rpc-tests/wallet_mergetoaddress.py | 348 +++++++ src/Makefile.am | 2 + src/rpcclient.cpp | 4 + src/rpcserver.cpp | 1 + src/rpcserver.h | 1 + src/test/rpc_wallet_tests.cpp | 309 ++++++ .../asyncrpcoperation_mergetoaddress.cpp | 923 ++++++++++++++++++ src/wallet/asyncrpcoperation_mergetoaddress.h | 189 ++++ src/wallet/rpcwallet.cpp | 328 +++++++ 10 files changed, 2106 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/pull-tester/rpc-tests.sh b/qa/pull-tester/rpc-tests.sh index 270c77de7..603185c90 100755 --- a/qa/pull-tester/rpc-tests.sh +++ b/qa/pull-tester/rpc-tests.sh @@ -16,6 +16,7 @@ testScripts=( 'wallet_treestate.py' 'wallet_protectcoinbase.py' 'wallet_shieldcoinbase.py' + 'wallet_mergetoaddress.py' 'wallet.py' 'wallet_overwintertx.py' 'wallet_nullifiers.py' diff --git a/qa/rpc-tests/wallet_mergetoaddress.py b/qa/rpc-tests/wallet_mergetoaddress.py new file mode 100755 index 000000000..e573d78bd --- /dev/null +++ b/qa/rpc-tests/wallet_mergetoaddress.py @@ -0,0 +1,348 @@ +#!/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 + +import time +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'] + 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', '-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']) + 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 + result = self.nodes[0].z_mergetoaddress([myzaddr], myzaddr, 0, 50, 2) + assert_equal(result["mergingUTXOs"], Decimal('0')) + # Remaining UTXOs are only counted if we are trying to merge any UTXOs + assert_equal(result["remainingUTXOs"], Decimal('0')) + assert_equal(result["mergingNotes"], Decimal('2')) + assert_equal(result["remainingNotes"], Decimal('3')) + wait_and_assert_operationid_status(self.nodes[0], result['opid']) + 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('2')) + wait_and_assert_operationid_status(self.nodes[0], result['opid']) + self.nodes[1].generate(1) + self.sync_all() + +if __name__ == '__main__': + WalletMergeToAddressTest().main() diff --git a/src/Makefile.am b/src/Makefile.am index d8ab34ea0..2db4e362a 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -204,6 +204,7 @@ BITCOIN_CORE_H = \ utiltime.h \ validationinterface.h \ version.h \ + wallet/asyncrpcoperation_mergetoaddress.h \ wallet/asyncrpcoperation_sendmany.h \ wallet/asyncrpcoperation_shieldcoinbase.h \ wallet/crypter.h \ @@ -297,6 +298,7 @@ libbitcoin_wallet_a_SOURCES = \ utiltest.h \ zcbenchmarks.cpp \ zcbenchmarks.h \ + wallet/asyncrpcoperation_mergetoaddress.cpp \ wallet/asyncrpcoperation_sendmany.cpp \ wallet/asyncrpcoperation_shieldcoinbase.cpp \ wallet/crypter.cpp \ diff --git a/src/rpcclient.cpp b/src/rpcclient.cpp index def32500d..45809cdb0 100644 --- a/src/rpcclient.cpp +++ b/src/rpcclient.cpp @@ -109,6 +109,10 @@ static const CRPCConvertParam vRPCConvertParams[] = { "z_gettotalbalance", 0}, { "z_gettotalbalance", 1}, { "z_gettotalbalance", 2}, + { "z_mergetoaddress", 0}, + { "z_mergetoaddress", 2}, + { "z_mergetoaddress", 3}, + { "z_mergetoaddress", 4}, { "z_sendmany", 1}, { "z_sendmany", 2}, { "z_sendmany", 3}, diff --git a/src/rpcserver.cpp b/src/rpcserver.cpp index f241bea86..568ba7926 100644 --- a/src/rpcserver.cpp +++ b/src/rpcserver.cpp @@ -387,6 +387,7 @@ static const CRPCCommand vRPCCommands[] = { "wallet", "z_listreceivedbyaddress",&z_listreceivedbyaddress,false }, { "wallet", "z_getbalance", &z_getbalance, false }, { "wallet", "z_gettotalbalance", &z_gettotalbalance, false }, + { "wallet", "z_mergetoaddress", &z_mergetoaddress, false }, { "wallet", "z_sendmany", &z_sendmany, false }, { "wallet", "z_shieldcoinbase", &z_shieldcoinbase, false }, { "wallet", "z_getoperationstatus", &z_getoperationstatus, true }, diff --git a/src/rpcserver.h b/src/rpcserver.h index d93efb4fc..8ce108cb4 100644 --- a/src/rpcserver.h +++ b/src/rpcserver.h @@ -289,6 +289,7 @@ extern UniValue z_importwallet(const UniValue& params, bool fHelp); // in rpcdum extern UniValue z_listreceivedbyaddress(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 extern UniValue z_sendmany(const UniValue& params, bool fHelp); // in rpcwallet.cpp extern UniValue z_shieldcoinbase(const UniValue& params, bool fHelp); // in rpcwallet.cpp extern UniValue z_getoperationstatus(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 3b563de54..18b687dbf 100644 --- a/src/test/rpc_wallet_tests.cpp +++ b/src/test/rpc_wallet_tests.cpp @@ -16,6 +16,7 @@ #include "rpcserver.h" #include "asyncrpcqueue.h" #include "asyncrpcoperation.h" +#include "wallet/asyncrpcoperation_mergetoaddress.h" #include "wallet/asyncrpcoperation_sendmany.h" #include "wallet/asyncrpcoperation_shieldcoinbase.h" @@ -1409,4 +1410,312 @@ BOOST_AUTO_TEST_CASE(rpc_z_shieldcoinbase_internals) } +BOOST_AUTO_TEST_CASE(rpc_z_mergetoaddress_parameters) +{ + SelectParams(CBaseChainParams::TESTNET); + + LOCK(pwalletMain->cs_wallet); + + BOOST_CHECK_THROW(CallRPC("z_mergetoaddress"), runtime_error); + BOOST_CHECK_THROW(CallRPC("z_mergetoaddress toofewargs"), runtime_error); + BOOST_CHECK_THROW(CallRPC("z_mergetoaddress just too many args present for this method"), runtime_error); + + // bad from address + BOOST_CHECK_THROW(CallRPC("z_mergetoaddress " + "[\"INVALIDtmRr6yJonqGK23UVhrKuyvTpF8qxQQjKigJ\"] tnpoQJVnYBZZqkFadj2bJJLThNCxbADGB5gSGeYTAGGrT5tejsxY9Zc1BtY8nnHmZkB"), runtime_error); + + // bad from address + BOOST_CHECK_THROW(CallRPC("z_mergetoaddress " + "** tnpoQJVnYBZZqkFadj2bJJLThNCxbADGB5gSGeYTAGGrT5tejsxY9Zc1BtY8nnHmZkB"), runtime_error); + + // bad from address + BOOST_CHECK_THROW(CallRPC("z_mergetoaddress " + "[\"**\"] tnpoQJVnYBZZqkFadj2bJJLThNCxbADGB5gSGeYTAGGrT5tejsxY9Zc1BtY8nnHmZkB"), runtime_error); + + // bad from address + BOOST_CHECK_THROW(CallRPC("z_mergetoaddress " + "tmRr6yJonqGK23UVhrKuyvTpF8qxQQjKigJ tnpoQJVnYBZZqkFadj2bJJLThNCxbADGB5gSGeYTAGGrT5tejsxY9Zc1BtY8nnHmZkB"), runtime_error); + + // bad from address + BOOST_CHECK_THROW(CallRPC("z_mergetoaddress " + "[tmRr6yJonqGK23UVhrKuyvTpF8qxQQjKigJ] tnpoQJVnYBZZqkFadj2bJJLThNCxbADGB5gSGeYTAGGrT5tejsxY9Zc1BtY8nnHmZkB"), runtime_error); + + // bad to address + BOOST_CHECK_THROW(CallRPC("z_mergetoaddress " + "[\"tmRr6yJonqGK23UVhrKuyvTpF8qxQQjKigJ\"] INVALIDtnpoQJVnYBZZqkFadj2bJJLThNCxbADGB5gSGeYTAGGrT5tejsxY9Zc1BtY8nnHmZkB"), runtime_error); + + // duplicate address + BOOST_CHECK_THROW(CallRPC("z_mergetoaddress " + "[\"tmRr6yJonqGK23UVhrKuyvTpF8qxQQjKigJ\", \"tmRr6yJonqGK23UVhrKuyvTpF8qxQQjKigJ\"] " + "tmQP9L3s31cLsghVYf2Jb5MhKj1jRBPoeQn" + ), runtime_error); + + // invalid fee amount, cannot be negative + BOOST_CHECK_THROW(CallRPC("z_mergetoaddress " + "[\"tmRr6yJonqGK23UVhrKuyvTpF8qxQQjKigJ\"] " + "tnpoQJVnYBZZqkFadj2bJJLThNCxbADGB5gSGeYTAGGrT5tejsxY9Zc1BtY8nnHmZkB " + "-0.0001" + ), runtime_error); + + // invalid fee amount, bigger than MAX_MONEY + BOOST_CHECK_THROW(CallRPC("z_mergetoaddress " + "[\"tmRr6yJonqGK23UVhrKuyvTpF8qxQQjKigJ\"] " + "tnpoQJVnYBZZqkFadj2bJJLThNCxbADGB5gSGeYTAGGrT5tejsxY9Zc1BtY8nnHmZkB " + "21000001" + ), runtime_error); + + // invalid transparent limit, must be at least 0 + BOOST_CHECK_THROW(CallRPC("z_mergetoaddress " + "[\"tmRr6yJonqGK23UVhrKuyvTpF8qxQQjKigJ\"] " + "tnpoQJVnYBZZqkFadj2bJJLThNCxbADGB5gSGeYTAGGrT5tejsxY9Zc1BtY8nnHmZkB " + "0.0001 -1" + ), runtime_error); + + // invalid shielded limit, must be at least 0 + BOOST_CHECK_THROW(CallRPC("z_mergetoaddress " + "[\"tmRr6yJonqGK23UVhrKuyvTpF8qxQQjKigJ\"] " + "tnpoQJVnYBZZqkFadj2bJJLThNCxbADGB5gSGeYTAGGrT5tejsxY9Zc1BtY8nnHmZkB " + "0.0001 100 -1" + ), runtime_error); + + // memo bigger than allowed length of ZC_MEMO_SIZE + std::vector v (2 * (ZC_MEMO_SIZE+1)); // x2 for hexadecimal string format + std::fill(v.begin(),v.end(), 'A'); + std::string badmemo(v.begin(), v.end()); + CZCPaymentAddress pa = pwalletMain->GenerateNewZKey(); + std::string zaddr1 = pa.ToString(); + BOOST_CHECK_THROW(CallRPC(string("z_mergetoaddress [\"tmRr6yJonqGK23UVhrKuyvTpF8qxQQjKigJ\"] ") + + zaddr1 + " 0.0001 100 100 " + badmemo), runtime_error); + + // Mutable tx containing contextual information we need to build tx + UniValue retValue = CallRPC("getblockcount"); + int nHeight = retValue.get_int(); + CMutableTransaction mtx = CreateNewContextualCMutableTransaction(Params().GetConsensus(), nHeight + 1); + + // Test constructor of AsyncRPCOperation_mergetoaddress + MergeToAddressRecipient testnetzaddr( + "ztjiDe569DPNbyTE6TSdJTaSDhoXEHLGvYoUnBU1wfVNU52TEyT6berYtySkd21njAeEoh8fFJUT42kua9r8EnhBaEKqCpP", + "testnet memo"); + MergeToAddressRecipient mainnetzaddr( + "zcMuhvq8sEkHALuSU2i4NbNQxshSAYrpCExec45ZjtivYPbuiFPwk6WHy4SvsbeZ4siy1WheuRGjtaJmoD1J8bFqNXhsG6U", + "mainnet memo"); + + try { + std::shared_ptr operation(new AsyncRPCOperation_mergetoaddress(mtx, {}, {}, testnetzaddr, -1 )); + BOOST_FAIL("Should have caused an error"); + } catch (const UniValue& objError) { + BOOST_CHECK( find_error(objError, "Fee is out of range")); + } + + try { + std::shared_ptr operation(new AsyncRPCOperation_mergetoaddress(mtx, {}, {}, testnetzaddr, 1)); + BOOST_FAIL("Should have caused an error"); + } catch (const UniValue& objError) { + BOOST_CHECK( find_error(objError, "No inputs")); + } + + std::vector inputs = { MergeToAddressInputUTXO{ COutPoint{uint256(), 0}, 0} }; + + try { + MergeToAddressRecipient badaddr("", "memo"); + std::shared_ptr operation(new AsyncRPCOperation_mergetoaddress(mtx, inputs, {}, badaddr, 1)); + BOOST_FAIL("Should have caused an error"); + } catch (const UniValue& objError) { + BOOST_CHECK( find_error(objError, "Recipient parameter missing")); + } + + // Testnet payment addresses begin with 'zt'. This test detects an incorrect prefix. + try { + std::vector inputs = { MergeToAddressInputUTXO{ COutPoint{uint256(), 0}, 0} }; + std::shared_ptr operation( new AsyncRPCOperation_mergetoaddress(mtx, inputs, {}, mainnetzaddr, 1) ); + BOOST_FAIL("Should have caused an error"); + } catch (const UniValue& objError) { + BOOST_CHECK( find_error(objError, "payment address is for wrong network type")); + } +} + + +// TODO: test private methods +BOOST_AUTO_TEST_CASE(rpc_z_mergetoaddress_internals) +{ + SelectParams(CBaseChainParams::TESTNET); + + LOCK(pwalletMain->cs_wallet); + + // Mutable tx containing contextual information we need to build tx + UniValue retValue = CallRPC("getblockcount"); + int nHeight = retValue.get_int(); + CMutableTransaction mtx = CreateNewContextualCMutableTransaction(Params().GetConsensus(), nHeight + 1); + + // Test that option -mempooltxinputlimit is respected. + mapArgs["-mempooltxinputlimit"] = "1"; + + // Add keys manually + BOOST_CHECK_NO_THROW(retValue = CallRPC("getnewaddress")); + MergeToAddressRecipient taddr1(retValue.get_str(), ""); + CZCPaymentAddress pa = pwalletMain->GenerateNewZKey(); + MergeToAddressRecipient zaddr1(pa.ToString(), "DEADBEEF"); + + // Supply 2 inputs when mempool limit is 1 + { + std::vector inputs = { + MergeToAddressInputUTXO{COutPoint{uint256(),0},0}, + MergeToAddressInputUTXO{COutPoint{uint256(),0},0} + }; + std::shared_ptr operation( new AsyncRPCOperation_mergetoaddress(mtx, inputs, {}, zaddr1) ); + operation->main(); + BOOST_CHECK(operation->isFailed()); + std::string msg = operation->getErrorMessage(); + BOOST_CHECK( msg.find("Number of transparent inputs 2 is greater than mempooltxinputlimit of 1") != string::npos); + } + + // Insufficient funds + { + std::vector inputs = { MergeToAddressInputUTXO{COutPoint{uint256(),0},0} }; + std::shared_ptr operation( new AsyncRPCOperation_mergetoaddress(mtx, inputs, {}, zaddr1) ); + operation->main(); + BOOST_CHECK(operation->isFailed()); + std::string msg = operation->getErrorMessage(); + BOOST_CHECK( msg.find("Insufficient funds, have 0.00 and miners fee is 0.0001") != string::npos); + } + + // get_memo_from_hex_string()) + { + std::vector inputs = { MergeToAddressInputUTXO{COutPoint{uint256(),0},100000} }; + std::shared_ptr operation( new AsyncRPCOperation_mergetoaddress(mtx, inputs, {}, zaddr1) ); + std::shared_ptr ptr = std::dynamic_pointer_cast (operation); + TEST_FRIEND_AsyncRPCOperation_mergetoaddress proxy(ptr); + + std::string memo = "DEADBEEF"; + boost::array array = proxy.get_memo_from_hex_string(memo); + BOOST_CHECK_EQUAL(array[0], 0xDE); + BOOST_CHECK_EQUAL(array[1], 0xAD); + BOOST_CHECK_EQUAL(array[2], 0xBE); + BOOST_CHECK_EQUAL(array[3], 0xEF); + for (int i=4; i v (2 * (ZC_MEMO_SIZE+1)); + std::fill(v.begin(),v.end(), 'A'); + std::string bigmemo(v.begin(), v.end()); + + try { + proxy.get_memo_from_hex_string(bigmemo); + BOOST_FAIL("Should have caused an error"); + } catch (const UniValue& objError) { + BOOST_CHECK( find_error(objError, "too big")); + } + + // invalid hexadecimal string + std::fill(v.begin(),v.end(), '@'); // not a hex character + std::string badmemo(v.begin(), v.end()); + + try { + proxy.get_memo_from_hex_string(badmemo); + BOOST_FAIL("Should have caused an error"); + } catch (const UniValue& objError) { + BOOST_CHECK( find_error(objError, "hexadecimal format")); + } + + // odd length hexadecimal string + std::fill(v.begin(),v.end(), 'A'); + v.resize(v.size() - 1); + assert(v.size() %2 == 1); // odd length + std::string oddmemo(v.begin(), v.end()); + try { + proxy.get_memo_from_hex_string(oddmemo); + BOOST_FAIL("Should have caused an error"); + } catch (const UniValue& objError) { + BOOST_CHECK( find_error(objError, "hexadecimal format")); + } + } + + // Test the perform_joinsplit methods. + { + // Dummy input so the operation object can be instantiated. + std::vector inputs = { MergeToAddressInputUTXO{COutPoint{uint256(),0},100000} }; + std::shared_ptr operation( new AsyncRPCOperation_mergetoaddress(mtx, inputs, {}, zaddr1) ); + std::shared_ptr ptr = std::dynamic_pointer_cast (operation); + TEST_FRIEND_AsyncRPCOperation_mergetoaddress proxy(ptr); + + // Enable test mode so tx is not sent and proofs are not generated + static_cast(operation.get())->testmode = true; + + MergeToAddressJSInfo info; + std::vector> witnesses; + uint256 anchor; + try { + proxy.perform_joinsplit(info, witnesses, anchor); + BOOST_FAIL("Should have caused an error"); + } catch (const std::runtime_error & e) { + BOOST_CHECK( string(e.what()).find("anchor is null")!= string::npos); + } + + try { + std::vector v; + proxy.perform_joinsplit(info, v); + BOOST_FAIL("Should have caused an error"); + } catch (const std::runtime_error & e) { + BOOST_CHECK( string(e.what()).find("anchor is null")!= string::npos); + } + + info.notes.push_back(Note()); + try { + proxy.perform_joinsplit(info); + BOOST_FAIL("Should have caused an error"); + } catch (const std::runtime_error & e) { + BOOST_CHECK( string(e.what()).find("number of notes")!= string::npos); + } + + info.notes.clear(); + info.vjsin.push_back(JSInput()); + info.vjsin.push_back(JSInput()); + info.vjsin.push_back(JSInput()); + try { + proxy.perform_joinsplit(info); + BOOST_FAIL("Should have caused an error"); + } catch (const std::runtime_error & e) { + BOOST_CHECK( string(e.what()).find("unsupported joinsplit input")!= string::npos); + } + + info.vjsin.clear(); + try { + proxy.perform_joinsplit(info); + BOOST_FAIL("Should have caused an error"); + } catch (const std::runtime_error & e) { + BOOST_CHECK( string(e.what()).find("error verifying joinsplit")!= string::npos); + } + } + + // Raw joinsplit is a zaddr->zaddr + { + std::string raw = "020000000000000000000100000000000000001027000000000000183a0d4c46c369078705e39bcfebee59a978dbd210ce8de3efc9555a03fbabfd3cea16693d730c63850d7e48ccde79854c19adcb7e9dcd7b7d18805ee09083f6b16e1860729d2d4a90e2f2acd009cf78b5eb0f4a6ee4bdb64b1262d7ce9eb910c460b02022991e968d0c50ee44908e4ccccbc591d0053bcca154dd6d6fc400a29fa686af4682339832ccea362a62aeb9df0d5aa74f86a1e75ac0f48a8ccc41e0a940643c6c33e1d09223b0a46eaf47a1bb4407cfc12b1dcf83a29c0cef51e45c7876ca5b9e5bae86d92976eb3ef68f29cd29386a8be8451b50f82bf9da10c04651868655194da8f6ed3d241bb5b5ff93a3e2bbe44644544d88bcde5cc35978032ee92699c7a61fcbb395e7583f47e698c4d53ede54f956629400bf510fb5e22d03158cc10bdcaaf29e418ef18eb6480dd9c8b9e2a377809f9f32a556ef872febd0021d4ad013aa9f0b7255e98e408d302abefd33a71180b720271835b487ab309e160b06dfe51932120fb84a7ede16b20c53599a11071592109e10260f265ee60d48c62bfe24074020e9b586ce9e9356e68f2ad1a9538258234afe4b83a209f178f45202270eaeaeecaf2ce3100b2c5a714f75f35777a9ebff5ebf47059d2bbf6f3726190216468f2b152673b766225b093f3a2f827c86d6b48b42117fec1d0ac38dd7af700308dcfb02eba821612b16a2c164c47715b9b0c93900893b1aba2ea03765c94d87022db5be06ab338d1912e0936dfe87586d0a8ee49144a6cd2e306abdcb652faa3e0222739deb23154d778b50de75069a4a2cce1208cd1ced3cb4744c9888ce1c2fcd2e66dc31e62d3aa9e423d7275882525e9981f92e84ac85975b8660739407efbe1e34c2249420fde7e17db3096d5b22e83d051d01f0e6e7690dca7d168db338aadf0897fedac10de310db2b1bff762d322935dddbb60c2efb8b15d231fa17b84630371cb275c209f0c4c7d0c68b150ea5cd514122215e3f7fcfb351d69514788d67c2f3c8922581946e3a04bdf1f07f15696ca76eb95b10698bf1188fd882945c57657515889d042a6fc45d38cbc943540c4f0f6d1c45a1574c81f3e42d1eb8702328b729909adee8a5cfed7c79d54627d1fd389af941d878376f7927b9830ca659bf9ab18c5ca5192d52d02723008728d03701b8ab3e1c4a3109409ec0b13df334c7deec3523eeef4c97b5603e643de3a647b873f4c1b47fbfc6586ba66724f112e51fc93839648005043620aa3ce458e246d77977b19c53d98e3e812de006afc1a79744df236582943631d04cc02941ac4be500e4ed9fb9e3e7cc187b1c4050fad1d9d09d5fd70d5d01d615b439d8c0015d2eb10398bcdbf8c4b2bd559dbe4c288a186aed3f86f608da4d582e120c4a896e015e2241900d1daeccd05db968852677c71d752bec46de9962174b46f980e8cc603654daf8b98a3ee92dac066033954164a89568b70b1780c2ce2410b2f816dbeddb2cd463e0c8f21a52cf6427d9647a6fd4bafa8fb4cd4d47ac057b0160bee86c6b2fb8adce214c2bcdda277512200adf0eaa5d2114a2c077b009836a68ec254bfe56f51d147b9afe2ddd9cb917c0c2de19d81b7b8fd9f4574f51fa1207630dc13976f4d7587c962f761af267de71f3909a576e6bedaf6311633910d291ac292c467cc8331ef577aef7646a5d949322fa0367a49f20597a13def53136ee31610395e3e48d291fd8f58504374031fe9dcfba5e06086ebcf01a9106f6a4d6e16e19e4c5bb893f7da79419c94eca31a384be6fa1747284dee0fc3bbc8b1b860172c10b29c1594bb8c747d7fe05827358ff2160f49050001625ffe2e880bd7fc26cd0ffd89750745379a8e862816e08a5a2008043921ab6a4976064ac18f7ee37b6628bc0127d8d5ebd3548e41d8881a082d86f20b32e33094f15a0e6ea6074b08c6cd28142de94713451640a55985051f5577eb54572699d838cb34a79c8939e981c0c277d06a6e2ce69ccb74f8a691ff08f81d8b99e6a86223d29a2b7c8e7b041aba44ea678ae654277f7e91cbfa79158b989164a3d549d9f4feb0cc43169699c13e321fe3f4b94258c75d198ff9184269cd6986c55409e07528c93f64942c6c283ce3917b4bf4c3be2fe3173c8c38cccb35f1fbda0ca88b35a599c0678cb22aa8eabea8249dbd2e4f849fffe69803d299e435ebcd7df95854003d8eda17a74d98b4be0e62d45d7fe48c06a6f464a14f8e0570077cc631279092802a89823f031eef5e1028a6d6fdbd502869a731ee7d28b4d6c71b419462a30d31442d3ee444ffbcbd16d558c9000c97e949c2b1f9d6f6d8db7b9131ebd963620d3fc8595278d6f8fdf49084325373196d53e64142fa5a23eccd6ef908c4d80b8b3e6cc334b7f7012103a3682e4678e9b518163d262a39a2c1a69bf88514c52b7ccd7cc8dc80e71f7c2ec0701cff982573eb0c2c4daeb47fa0b586f4451c10d1da2e5d182b03dd067a5e971b3a6138ca6667aaf853d2ac03b80a1d5870905f2cfb6c78ec3c3719c02f973d638a0f973424a2b0f2b0023f136d60092fe15fba4bc180b9176bd0ff576e053f1af6939fe9ca256203ffaeb3e569f09774d2a6cbf91873e56651f4d6ff77e0b5374b0a1a201d7e523604e0247644544cc571d48c458a4f96f45580b"; + UniValue obj(UniValue::VOBJ); + obj.push_back(Pair("rawtxn", raw)); + + // we have the spending key for the dummy recipient zaddr1 + std::vector inputs = { MergeToAddressInputUTXO{COutPoint{uint256(),0},100000} }; + std::shared_ptr operation( new AsyncRPCOperation_mergetoaddress(mtx, inputs, {}, zaddr1) ); + std::shared_ptr ptr = std::dynamic_pointer_cast (operation); + TEST_FRIEND_AsyncRPCOperation_mergetoaddress proxy(ptr); + + // Enable test mode so tx is not sent + static_cast(operation.get())->testmode = true; + + // Pretend that the operation completed successfully + proxy.set_state(OperationStatus::SUCCESS); + + // Verify test mode is returning output (since no input taddrs, signed and unsigned are the same). + BOOST_CHECK_NO_THROW( proxy.sign_send_raw_transaction(obj) ); + UniValue result = operation->getResult(); + BOOST_CHECK(!result.isNull()); + UniValue resultObj = result.get_obj(); + std::string hex = find_value(resultObj, "hex").get_str(); + BOOST_CHECK_EQUAL(hex, raw); + } +} + + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/wallet/asyncrpcoperation_mergetoaddress.cpp b/src/wallet/asyncrpcoperation_mergetoaddress.cpp new file mode 100644 index 000000000..5257cbc88 --- /dev/null +++ b/src/wallet/asyncrpcoperation_mergetoaddress.cpp @@ -0,0 +1,923 @@ +// 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(); + + // Enable payment disclosure if requested + paymentDisclosureMode = fExperimentalMode && GetBoolArg("-paymentdisclosure", false); +} + +AsyncRPCOperation_mergetoaddress::~AsyncRPCOperation_mergetoaddress() +{ +} + +void AsyncRPCOperation_mergetoaddress::main() +{ + if (isCancelled()) { + unlock_utxos(); // clean up + 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 + + // !!! 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. #1159 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)); + } +} diff --git a/src/wallet/asyncrpcoperation_mergetoaddress.h b/src/wallet/asyncrpcoperation_mergetoaddress.h new file mode 100644 index 000000000..1619b5c97 --- /dev/null +++ b/src/wallet/asyncrpcoperation_mergetoaddress.h @@ -0,0 +1,189 @@ +// 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(); + + // 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/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 3e63c3689..4478d5307 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -24,6 +24,7 @@ #include "utiltime.h" #include "asyncrpcoperation.h" #include "asyncrpcqueue.h" +#include "wallet/asyncrpcoperation_mergetoaddress.h" #include "wallet/asyncrpcoperation_sendmany.h" #include "wallet/asyncrpcoperation_shieldcoinbase.h" @@ -3748,6 +3749,333 @@ UniValue z_shieldcoinbase(const UniValue& params, bool fHelp) } +#define MERGE_TO_ADDRESS_DEFAULT_TRANSPARENT_LIMIT 50 +#define MERGE_TO_ADDRESS_DEFAULT_SHIELDED_LIMIT 10 + +#define JOINSPLIT_SIZE JSDescription().GetSerializeSize(SER_NETWORK, PROTOCOL_VERSION) + +UniValue z_mergetoaddress(const UniValue& params, bool fHelp) +{ + if (!EnsureWalletIsAvailable(fHelp)) + return NullUniValue; + + if (fHelp || params.size() < 2 || params.size() > 6) + throw runtime_error( + "z_mergetoaddress [\"fromaddress\", ... ] \"toaddress\" ( fee ) ( transparent_limit ) ( shielded_limit ) ( memo )\n" + "\nMerge multiple UTXOs and notes into a single UTXO or note. Coinbase UTXOs are ignored; use `z_shieldcoinbase`" + "\nto combine those into a single note." + "\n\nThis is an asynchronous operation, and UTXOs selected for merging will be locked. If there is an error, they" + "\nare unlocked. The RPC call `listlockunspent` can be used to return a list of locked UTXOs." + "\n\nThe number of UTXOs and notes selected for merging can be limited by the caller. If the transparent limit" + "\nparameter is set to zero, the -mempooltxinputlimit option will determine the number of UTXOs. Any limit is" + "\nconstrained by the consensus rule defining a maximum transaction size of " + + strprintf("%d bytes.", MAX_TX_SIZE) + + HelpRequiringPassphrase() + "\n" + "\nArguments:\n" + "1. fromaddresses (string, required) A JSON array with addresses.\n" + " The following special strings are accepted inside the array:\n" + " - \"*\": Merge both UTXOs and notes from all addresses belonging to the wallet.\n" + " - \"ANY_TADDR\": Merge UTXOs from all t-addrs belonging to the wallet.\n" + " - \"ANY_ZADDR\": Merge notes from all z-addrs belonging to the wallet.\n" + " If a special string is given, any given addresses of that type will be ignored.\n" + " [\n" + " \"address\" (string) Can be a t-addr or a z-addr\n" + " ,...\n" + " ]\n" + "2. \"toaddress\" (string, required) The t-addr or z-addr to send the funds to.\n" + "3. fee (numeric, optional, default=" + + strprintf("%s", FormatMoney(MERGE_TO_ADDRESS_OPERATION_DEFAULT_MINERS_FEE)) + ") The fee amount to attach to this transaction.\n" + "4. transparent_limit (numeric, optional, default=" + + strprintf("%d", MERGE_TO_ADDRESS_DEFAULT_TRANSPARENT_LIMIT) + ") Limit on the maximum number of UTXOs to merge. Set to 0 to use node option -mempooltxinputlimit.\n" + "4. shielded_limit (numeric, optional, default=" + + strprintf("%d", MERGE_TO_ADDRESS_DEFAULT_SHIELDED_LIMIT) + ") Limit on the maximum number of notes to merge. Set to 0 to merge as many as will fit in the transaction.\n" + "5. \"memo\" (string, optional) Encoded as hex. When toaddress is a z-addr, this will be stored in the memo field of the new note.\n" + "\nResult:\n" + "{\n" + " \"remainingUTXOs\": xxx (numeric) Number of UTXOs still available for merging.\n" + " \"remainingTransparentValue\": xxx (numeric) Value of UTXOs still available for merging.\n" + " \"remainingNotes\": xxx (numeric) Number of notes still available for merging.\n" + " \"remainingShieldedValue\": xxx (numeric) Value of notes still available for merging.\n" + " \"mergingUTXOs\": xxx (numeric) Number of UTXOs being merged.\n" + " \"mergingTransparentValue\": xxx (numeric) Value of UTXOs being merged.\n" + " \"mergingNotes\": xxx (numeric) Number of notes being merged.\n" + " \"mergingShieldedValue\": xxx (numeric) Value of notes being merged.\n" + " \"opid\": xxx (string) An operationid to pass to z_getoperationstatus to get the result of the operation.\n" + "}\n" + "\nExamples:\n" + + HelpExampleCli("z_mergetoaddress", "'[\"t1M72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd\"]' ztfaW34Gj9FrnGUEf833ywDVL62NWXBM81u6EQnM6VR45eYnXhwztecW1SjxA7JrmAXKJhxhj3vDNEpVCQoSvVoSpmbhtjf") + + HelpExampleRpc("z_mergetoaddress", "[\"t1M72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd\"], \"ztfaW34Gj9FrnGUEf833ywDVL62NWXBM81u6EQnM6VR45eYnXhwztecW1SjxA7JrmAXKJhxhj3vDNEpVCQoSvVoSpmbhtjf\"") + ); + + LOCK2(cs_main, pwalletMain->cs_wallet); + + bool useAny = false; + bool useAnyUTXO = false; + bool useAnyNote = false; + std::set taddrs = {}; + std::set zaddrs = {}; + + UniValue addresses = params[0].get_array(); + if (addresses.size()==0) + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, fromaddresses array is empty."); + + // Keep track of addresses to spot duplicates + std::set setAddress; + + // Sources + for (const UniValue& o : addresses.getValues()) { + if (!o.isStr()) + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected string"); + + std::string address = o.get_str(); + if (address == "*") { + useAny = true; + } else if (address == "ANY_TADDR") { + useAnyUTXO = true; + } else if (address == "ANY_ZADDR") { + useAnyNote = true; + } else { + CBitcoinAddress taddr(address); + if (taddr.IsValid()) { + // Ignore any listed t-addrs if we are using all of them + if (!(useAny || useAnyUTXO)) { + taddrs.insert(taddr); + } + } else { + try { + CZCPaymentAddress zaddr(address); + // Ignore listed z-addrs if we are using all of them + if (!(useAny || useAnyNote)) { + zaddrs.insert(zaddr.Get()); + } + } catch (const std::runtime_error&) { + throw JSONRPCError( + RPC_INVALID_PARAMETER, + string("Invalid parameter, unknown address format: ") + address); + } + } + } + + if (setAddress.count(address)) + throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, duplicated address: ") + address); + setAddress.insert(address); + } + + // Validate the destination address + auto destaddress = params[1].get_str(); + bool isToZaddr = false; + CBitcoinAddress taddr(destaddress); + if (!taddr.IsValid()) { + try { + CZCPaymentAddress zaddr(destaddress); + zaddr.Get(); + isToZaddr = true; + } catch (const std::runtime_error&) { + throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, unknown address format: ") + destaddress ); + } + } + + // Convert fee from currency format to zatoshis + CAmount nFee = SHIELD_COINBASE_DEFAULT_MINERS_FEE; + if (params.size() > 2) { + if (params[2].get_real() == 0.0) { + nFee = 0; + } else { + nFee = AmountFromValue( params[2] ); + } + } + + int nUTXOLimit = MERGE_TO_ADDRESS_DEFAULT_TRANSPARENT_LIMIT; + if (params.size() > 3) { + nUTXOLimit = params[3].get_int(); + if (nUTXOLimit < 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Limit on maximum number of UTXOs cannot be negative"); + } + } + + int nNoteLimit = MERGE_TO_ADDRESS_DEFAULT_SHIELDED_LIMIT; + if (params.size() > 4) { + nNoteLimit = params[4].get_int(); + if (nNoteLimit < 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Limit on maximum number of notes cannot be negative"); + } + } + + std::string memo; + if (params.size() > 5) { + memo = params[5].get_str(); + if (!isToZaddr) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Memo can not be used with a taddr. It can only be used with a zaddr."); + } else if (!IsHex(memo)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected memo data in hexadecimal format."); + } + if (memo.length() > ZC_MEMO_SIZE*2) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, size of memo is larger than maximum allowed %d", ZC_MEMO_SIZE )); + } + } + + MergeToAddressRecipient recipient(destaddress, memo); + + // Prepare to get UTXOs and notes + std::vector utxoInputs; + std::vector noteInputs; + CAmount mergedUTXOValue = 0; + CAmount mergedNoteValue = 0; + CAmount remainingUTXOValue = 0; + CAmount remainingNoteValue = 0; + size_t utxoCounter = 0; + size_t noteCounter = 0; + bool maxedOutUTXOsFlag = false; + bool maxedOutNotesFlag = false; + size_t mempoolLimit = (nUTXOLimit != 0) ? nUTXOLimit : (size_t)GetArg("-mempooltxinputlimit", 0); + + size_t estimatedTxSize = 200; // tx overhead + wiggle room + if (isToZaddr) { + estimatedTxSize += JOINSPLIT_SIZE; + } + + if (useAny || useAnyUTXO || taddrs.size() > 0) { + // Get available utxos + vector vecOutputs; + pwalletMain->AvailableCoins(vecOutputs, true, NULL, false, false); + + // Find unspent utxos and update estimated size + for (const COutput& out : vecOutputs) { + if (!out.fSpendable) { + continue; + } + + CTxDestination address; + if (!ExtractDestination(out.tx->vout[out.i].scriptPubKey, address)) { + continue; + } + // If taddr is not wildcard "*", filter utxos + if (taddrs.size() > 0 && !taddrs.count(address)) { + continue; + } + + utxoCounter++; + CAmount nValue = out.tx->vout[out.i].nValue; + + if (!maxedOutUTXOsFlag) { + CBitcoinAddress ba(address); + size_t increase = (ba.IsScript()) ? CTXIN_SPEND_P2SH_SIZE : CTXIN_SPEND_DUST_SIZE; + if (estimatedTxSize + increase >= MAX_TX_SIZE || + (mempoolLimit > 0 && utxoCounter > mempoolLimit)) + { + maxedOutUTXOsFlag = true; + } else { + estimatedTxSize += increase; + COutPoint utxo(out.tx->GetHash(), out.i); + utxoInputs.emplace_back(utxo, nValue); + mergedUTXOValue += nValue; + } + } + + if (maxedOutUTXOsFlag) { + remainingUTXOValue += nValue; + } + } + } + + if (useAny || useAnyNote || zaddrs.size() > 0) { + // Get available notes + std::vector entries; + pwalletMain->GetFilteredNotes(entries, zaddrs); + + // Find unspent notes and update estimated size + for (CNotePlaintextEntry& entry : entries) { + noteCounter++; + CAmount nValue = entry.plaintext.value; + + if (!maxedOutNotesFlag) { + // If we haven't added any notes yet and the merge is to a + // z-address, we have already accounted for the first JoinSplit. + size_t increase = (noteInputs.empty() && !isToZaddr) || (noteInputs.size() % 2 == 0) ? JOINSPLIT_SIZE : 0; + if (estimatedTxSize + increase >= MAX_TX_SIZE || + (nNoteLimit > 0 && noteCounter > nNoteLimit)) + { + maxedOutNotesFlag = true; + } else { + estimatedTxSize += increase; + SpendingKey zkey; + pwalletMain->GetSpendingKey(entry.address, zkey); + noteInputs.emplace_back(entry.jsop, entry.plaintext.note(entry.address), nValue, zkey); + mergedNoteValue += nValue; + } + } + + if (maxedOutNotesFlag) { + remainingNoteValue += nValue; + } + } + } + + size_t numUtxos = utxoInputs.size(); + size_t numNotes = noteInputs.size(); + + if (numUtxos == 0 && numNotes == 0) { + throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Could not find any funds to merge."); + } + + // Sanity check: Don't do anything if: + // - We only have one from address + // - It's equal to toaddress + // - The address only contains a single UTXO or note + if (setAddress.size() == 1 && setAddress.count(destaddress) && (numUtxos + numNotes) == 1) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Destination address is also the only source address, and all its funds are already merged."); + } + + CAmount mergedValue = mergedUTXOValue + mergedNoteValue; + if (mergedValue < nFee) { + throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, + strprintf("Insufficient funds, have %s, which is less than miners fee %s", + FormatMoney(mergedValue), FormatMoney(nFee))); + } + + // Check that the user specified fee is sane (if too high, it can result in error -25 absurd fee) + CAmount netAmount = mergedValue - nFee; + if (nFee > netAmount) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Fee %s is greater than the net amount to be shielded %s", FormatMoney(nFee), FormatMoney(netAmount))); + } + + // Keep record of parameters in context object + UniValue contextInfo(UniValue::VOBJ); + contextInfo.push_back(Pair("fromaddresses", params[0])); + contextInfo.push_back(Pair("toaddress", params[1])); + contextInfo.push_back(Pair("fee", ValueFromAmount(nFee))); + + // Contextual transaction we will build on + CMutableTransaction contextualTx = CreateNewContextualCMutableTransaction( + Params().GetConsensus(), + chainActive.Height() + 1); + bool isShielded = numNotes > 0 || isToZaddr; + if (contextualTx.nVersion == 1 && isShielded) { + contextualTx.nVersion = 2; // Tx format should support vjoinsplit + } + + // Create operation and add to global queue + std::shared_ptr q = getAsyncRPCQueue(); + std::shared_ptr operation( + new AsyncRPCOperation_mergetoaddress(contextualTx, utxoInputs, noteInputs, recipient, nFee, contextInfo) ); + q->addOperation(operation); + AsyncRPCOperationId operationId = operation->getId(); + + // Return continuation information + UniValue o(UniValue::VOBJ); + o.push_back(Pair("remainingUTXOs", utxoCounter - numUtxos)); + o.push_back(Pair("remainingTransparentValue", ValueFromAmount(remainingUTXOValue))); + o.push_back(Pair("remainingNotes", noteCounter - numNotes)); + o.push_back(Pair("remainingShieldedValue", ValueFromAmount(remainingNoteValue))); + o.push_back(Pair("mergingUTXOs", numUtxos)); + o.push_back(Pair("mergingTransparentValue", ValueFromAmount(mergedUTXOValue))); + o.push_back(Pair("mergingNotes", numNotes)); + o.push_back(Pair("mergingShieldedValue", ValueFromAmount(mergedNoteValue))); + o.push_back(Pair("opid", operationId)); + return o; +} + + UniValue z_listoperationids(const UniValue& params, bool fHelp) { if (!EnsureWalletIsAvailable(fHelp))