Auto merge of #3489 - str4d:3215-z_sendmany, r=str4d

Add Sapling support to z_sendmany

Closes #3215.
This commit is contained in:
Homu 2018-09-03 00:29:56 -07:00
commit edd321609c
11 changed files with 471 additions and 103 deletions

View File

@ -24,6 +24,7 @@ testScripts=(
'wallet_nullifiers.py'
'wallet_1941.py'
'wallet_addresses.py'
'wallet_sapling.py'
'listtransactions.py'
'mempool_resurrect_test.py'
'txn_doublespend.py'

View File

@ -262,7 +262,7 @@ class WalletProtectCoinbaseTest (BitcoinTestFramework):
myopid = self.nodes[0].z_sendmany(mytaddr, recipients)
wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Insufficient transparent funds, have 10.00, need 10000.0001")
myopid = self.nodes[0].z_sendmany(myzaddr, recipients)
wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Insufficient protected funds, have 9.9998, need 10000.0001")
wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Insufficient shielded funds, have 9.9998, need 10000.0001")
# Send will fail because of insufficient funds unless sender uses coinbase utxos
try:

104
qa/rpc-tests/wallet_sapling.py Executable file
View File

@ -0,0 +1,104 @@
#!/usr/bin/env python2
# Copyright (c) 2018 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.util import (
assert_equal,
start_nodes,
wait_and_assert_operationid_status,
)
from decimal import Decimal
# Test wallet behaviour with Sapling addresses
class WalletSaplingTest(BitcoinTestFramework):
def setup_nodes(self):
return start_nodes(4, self.options.tmpdir, [[
'-nuparams=5ba81b19:201', # Overwinter
'-nuparams=76b809bb:201', # Sapling
]] * 4)
def run_test(self):
# Sanity-check the test harness
assert_equal(self.nodes[0].getblockcount(), 200)
# Activate Sapling
self.nodes[2].generate(1)
self.sync_all()
taddr0 = self.nodes[0].getnewaddress()
# Skip over the address containing node 1's coinbase
self.nodes[1].getnewaddress()
taddr1 = self.nodes[1].getnewaddress()
saplingAddr0 = self.nodes[0].z_getnewaddress('sapling')
saplingAddr1 = self.nodes[1].z_getnewaddress('sapling')
# Verify addresses
assert(saplingAddr0 in self.nodes[0].z_listaddresses())
assert(saplingAddr1 in self.nodes[1].z_listaddresses())
assert_equal(self.nodes[0].z_validateaddress(saplingAddr0)['type'], 'sapling')
assert_equal(self.nodes[0].z_validateaddress(saplingAddr1)['type'], 'sapling')
# Verify balance
assert_equal(self.nodes[0].z_getbalance(saplingAddr0), Decimal('0'))
assert_equal(self.nodes[1].z_getbalance(saplingAddr1), Decimal('0'))
assert_equal(self.nodes[1].z_getbalance(taddr1), Decimal('0'))
# Node 0 shields some funds
# taddr -> Sapling
# -> taddr (change)
recipients = []
recipients.append({"address": saplingAddr0, "amount": Decimal('20')})
myopid = self.nodes[0].z_sendmany(taddr0, recipients, 1, 0)
wait_and_assert_operationid_status(self.nodes[0], myopid)
self.sync_all()
self.nodes[2].generate(1)
self.sync_all()
# Verify balance
assert_equal(self.nodes[0].z_getbalance(saplingAddr0), Decimal('20'))
assert_equal(self.nodes[1].z_getbalance(saplingAddr1), Decimal('0'))
assert_equal(self.nodes[1].z_getbalance(taddr1), Decimal('0'))
# Node 0 sends some shielded funds to node 1
# Sapling -> Sapling
# -> Sapling (change)
recipients = []
recipients.append({"address": saplingAddr1, "amount": Decimal('15')})
myopid = self.nodes[0].z_sendmany(saplingAddr0, recipients, 1, 0)
wait_and_assert_operationid_status(self.nodes[0], myopid)
self.sync_all()
self.nodes[2].generate(1)
self.sync_all()
# Verify balance
assert_equal(self.nodes[0].z_getbalance(saplingAddr0), Decimal('5'))
assert_equal(self.nodes[1].z_getbalance(saplingAddr1), Decimal('15'))
assert_equal(self.nodes[1].z_getbalance(taddr1), Decimal('0'))
# Node 1 sends some shielded funds to node 0, as well as unshielding
# Sapling -> Sapling
# -> taddr
# -> Sapling (change)
recipients = []
recipients.append({"address": saplingAddr0, "amount": Decimal('5')})
recipients.append({"address": taddr1, "amount": Decimal('5')})
myopid = self.nodes[1].z_sendmany(saplingAddr1, recipients, 1, 0)
wait_and_assert_operationid_status(self.nodes[1], myopid)
self.sync_all()
self.nodes[2].generate(1)
self.sync_all()
# Verify balance
assert_equal(self.nodes[0].z_getbalance(saplingAddr0), Decimal('10'))
assert_equal(self.nodes[1].z_getbalance(saplingAddr1), Decimal('5'))
assert_equal(self.nodes[1].z_getbalance(taddr1), Decimal('5'))
if __name__ == '__main__':
WalletSaplingTest().main()

View File

@ -970,26 +970,26 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_parameters)
// Test constructor of AsyncRPCOperation_sendmany
try {
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(mtx, "",{}, {}, -1));
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(boost::none, mtx, "",{}, {}, -1));
} catch (const UniValue& objError) {
BOOST_CHECK( find_error(objError, "Minconf cannot be negative"));
}
try {
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(mtx, "",{}, {}, 1));
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(boost::none, mtx, "",{}, {}, 1));
} catch (const UniValue& objError) {
BOOST_CHECK( find_error(objError, "From address parameter missing"));
}
try {
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(mtx, "tmRr6yJonqGK23UVhrKuyvTpF8qxQQjKigJ", {}, {}, 1) );
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(boost::none, mtx, "tmRr6yJonqGK23UVhrKuyvTpF8qxQQjKigJ", {}, {}, 1) );
} catch (const UniValue& objError) {
BOOST_CHECK( find_error(objError, "No recipients"));
}
try {
std::vector<SendManyRecipient> recipients = { SendManyRecipient("dummy",1.0, "") };
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(mtx, "INVALID", recipients, {}, 1) );
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(boost::none, mtx, "INVALID", recipients, {}, 1) );
} catch (const UniValue& objError) {
BOOST_CHECK( find_error(objError, "Invalid from address"));
}
@ -997,7 +997,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_parameters)
// Testnet payment addresses begin with 'zt'. This test detects an incorrect prefix.
try {
std::vector<SendManyRecipient> recipients = { SendManyRecipient("dummy",1.0, "") };
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(mtx, "zcMuhvq8sEkHALuSU2i4NbNQxshSAYrpCExec45ZjtivYPbuiFPwk6WHy4SvsbeZ4siy1WheuRGjtaJmoD1J8bFqNXhsG6U", recipients, {}, 1) );
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(boost::none, mtx, "zcMuhvq8sEkHALuSU2i4NbNQxshSAYrpCExec45ZjtivYPbuiFPwk6WHy4SvsbeZ4siy1WheuRGjtaJmoD1J8bFqNXhsG6U", recipients, {}, 1) );
} catch (const UniValue& objError) {
BOOST_CHECK( find_error(objError, "Invalid from address"));
}
@ -1006,7 +1006,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_parameters)
// invokes a method on pwalletMain, which is undefined in the google test environment.
try {
std::vector<SendManyRecipient> recipients = { SendManyRecipient("dummy",1.0, "") };
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(mtx, "ztjiDe569DPNbyTE6TSdJTaSDhoXEHLGvYoUnBU1wfVNU52TEyT6berYtySkd21njAeEoh8fFJUT42kua9r8EnhBaEKqCpP", recipients, {}, 1) );
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(boost::none, mtx, "ztjiDe569DPNbyTE6TSdJTaSDhoXEHLGvYoUnBU1wfVNU52TEyT6berYtySkd21njAeEoh8fFJUT42kua9r8EnhBaEKqCpP", recipients, {}, 1) );
} catch (const UniValue& objError) {
BOOST_CHECK( find_error(objError, "no spending key found for zaddr"));
}
@ -1039,7 +1039,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals)
// there are no utxos to spend
{
std::vector<SendManyRecipient> recipients = { SendManyRecipient(zaddr1,100.0, "DEADBEEF") };
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(mtx, taddr1, {}, recipients, 1) );
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(boost::none, mtx, taddr1, {}, recipients, 1) );
operation->main();
BOOST_CHECK(operation->isFailed());
std::string msg = operation->getErrorMessage();
@ -1050,7 +1050,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals)
{
try {
std::vector<SendManyRecipient> recipients = {SendManyRecipient(taddr1, 100.0, "DEADBEEF")};
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(mtx, zaddr1, recipients, {}, 0));
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(boost::none, mtx, zaddr1, recipients, {}, 0));
BOOST_CHECK(false); // Fail test if an exception is not thrown
} catch (const UniValue& objError) {
BOOST_CHECK(find_error(objError, "Minconf cannot be zero when sending from zaddr"));
@ -1061,7 +1061,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals)
// there are no unspent notes to spend
{
std::vector<SendManyRecipient> recipients = { SendManyRecipient(taddr1,100.0, "DEADBEEF") };
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(mtx, zaddr1, recipients, {}, 1) );
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(boost::none, mtx, zaddr1, recipients, {}, 1) );
operation->main();
BOOST_CHECK(operation->isFailed());
std::string msg = operation->getErrorMessage();
@ -1071,7 +1071,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals)
// get_memo_from_hex_string())
{
std::vector<SendManyRecipient> recipients = { SendManyRecipient(zaddr1,100.0, "DEADBEEF") };
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(mtx, zaddr1, recipients, {}, 1) );
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(boost::none, mtx, zaddr1, recipients, {}, 1) );
std::shared_ptr<AsyncRPCOperation_sendmany> ptr = std::dynamic_pointer_cast<AsyncRPCOperation_sendmany> (operation);
TEST_FRIEND_AsyncRPCOperation_sendmany proxy(ptr);
@ -1122,7 +1122,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals)
// add_taddr_change_output_to_tx() will append a vout to a raw transaction
{
std::vector<SendManyRecipient> recipients = { SendManyRecipient(zaddr1,100.0, "DEADBEEF") };
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(mtx, zaddr1, recipients, {}, 1) );
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(boost::none, mtx, zaddr1, recipients, {}, 1) );
std::shared_ptr<AsyncRPCOperation_sendmany> ptr = std::dynamic_pointer_cast<AsyncRPCOperation_sendmany> (operation);
TEST_FRIEND_AsyncRPCOperation_sendmany proxy(ptr);
@ -1151,7 +1151,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals)
SendManyRecipient("tmUSbHz3vxnwLvRyNDXbwkZxjVyDodMJEhh",CAmount(4.56), ""),
SendManyRecipient("tmYZAXYPCP56Xa5JQWWPZuK7o7bfUQW6kkd",CAmount(7.89), ""),
};
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(mtx, zaddr1, recipients, {}, 1) );
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(boost::none, mtx, zaddr1, recipients, {}, 1) );
std::shared_ptr<AsyncRPCOperation_sendmany> ptr = std::dynamic_pointer_cast<AsyncRPCOperation_sendmany> (operation);
TEST_FRIEND_AsyncRPCOperation_sendmany proxy(ptr);
@ -1174,7 +1174,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals)
// we have the spending key for the dummy recipient zaddr1
std::vector<SendManyRecipient> recipients = { SendManyRecipient(zaddr1, 0.0005, "ABCD") };
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(mtx, zaddr1, {}, recipients, 1) );
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(boost::none, mtx, zaddr1, {}, recipients, 1) );
std::shared_ptr<AsyncRPCOperation_sendmany> ptr = std::dynamic_pointer_cast<AsyncRPCOperation_sendmany> (operation);
TEST_FRIEND_AsyncRPCOperation_sendmany proxy(ptr);
@ -1199,7 +1199,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals)
// Dummy input so the operation object can be instantiated.
std::vector<SendManyRecipient> recipients = { SendManyRecipient(zaddr1, 0.0005, "ABCD") };
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(mtx, zaddr1, {}, recipients, 1) );
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(boost::none, mtx, zaddr1, {}, recipients, 1) );
std::shared_ptr<AsyncRPCOperation_sendmany> ptr = std::dynamic_pointer_cast<AsyncRPCOperation_sendmany> (operation);
TEST_FRIEND_AsyncRPCOperation_sendmany proxy(ptr);

View File

@ -69,6 +69,7 @@ private:
boost::optional<CTxDestination> tChangeAddr;
public:
TransactionBuilder() {}
TransactionBuilder(const Consensus::Params& consensusParams, int nHeight, CKeyStore* keyStore = nullptr);
void SetFee(CAmount fee);

View File

@ -35,6 +35,9 @@
using namespace libzcash;
extern UniValue signrawtransaction(const UniValue& params, bool fHelp);
extern UniValue sendrawtransaction(const UniValue& params, bool fHelp);
int find_output(UniValue obj, int n) {
UniValue outputMapValue = find_value(obj, "outputmap");
if (!outputMapValue.isArray()) {
@ -53,6 +56,7 @@ int find_output(UniValue obj, int n) {
}
AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany(
boost::optional<TransactionBuilder> builder,
CMutableTransaction contextualTx,
std::string fromAddress,
std::vector<SendManyRecipient> tOutputs,
@ -75,7 +79,13 @@ AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany(
if (tOutputs.size() == 0 && zOutputs.size() == 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "No recipients");
}
isUsingBuilder_ = false;
if (builder) {
isUsingBuilder_ = true;
builder_ = builder.get();
}
fromtaddr_ = DecodeDestination(fromAddress);
isfromtaddr_ = IsValidDestination(fromtaddr_);
isfromzaddr_ = false;
@ -83,19 +93,14 @@ AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany(
if (!isfromtaddr_) {
auto address = DecodePaymentAddress(fromAddress);
if (IsValidPaymentAddress(address)) {
// TODO: Add Sapling support. For now, ensure we can freely convert.
assert(boost::get<libzcash::SproutPaymentAddress>(&address) != nullptr);
SproutPaymentAddress addr = boost::get<libzcash::SproutPaymentAddress>(address);
// We don't need to lock on the wallet as spending key related methods are thread-safe
SproutSpendingKey key;
if (!pwalletMain->GetSproutSpendingKey(addr, key)) {
if (!boost::apply_visitor(HaveSpendingKeyForPaymentAddress(pwalletMain), address)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid from address, no spending key found for zaddr");
}
isfromzaddr_ = true;
frompaymentaddress_ = addr;
spendingkey_ = key;
frompaymentaddress_ = address;
spendingkey_ = boost::apply_visitor(GetSpendingKeyForPaymentAddress(pwalletMain), address).get();
} else {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid from address");
}
@ -235,15 +240,21 @@ bool AsyncRPCOperation_sendmany::main_impl() {
throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Insufficient funds, no unspent notes found for zaddr from address.");
}
// At least one of z_sprout_inputs_ and z_sapling_inputs_ must be empty by design
assert(z_sprout_inputs_.empty() || z_sapling_inputs_.empty());
CAmount t_inputs_total = 0;
for (SendManyInputUTXO & t : t_inputs_) {
t_inputs_total += std::get<2>(t);
}
CAmount z_inputs_total = 0;
for (SendManyInputJSOP & t : z_inputs_) {
for (SendManyInputJSOP & t : z_sprout_inputs_) {
z_inputs_total += std::get<2>(t);
}
for (auto t : z_sapling_inputs_) {
z_inputs_total += t.note.value();
}
CAmount t_outputs_total = 0;
for (SendManyRecipient & t : t_outputs_) {
@ -269,7 +280,7 @@ bool AsyncRPCOperation_sendmany::main_impl() {
if (isfromzaddr_ && (z_inputs_total < targetAmount)) {
throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS,
strprintf("Insufficient protected funds, have %s, need %s",
strprintf("Insufficient shielded funds, have %s, need %s",
FormatMoney(z_inputs_total), FormatMoney(targetAmount)));
}
@ -328,15 +339,25 @@ bool AsyncRPCOperation_sendmany::main_impl() {
}
// update the transaction with these inputs
CMutableTransaction rawTx(tx_);
for (SendManyInputUTXO & t : t_inputs_) {
uint256 txid = std::get<0>(t);
int vout = std::get<1>(t);
CAmount amount = std::get<2>(t);
CTxIn in(COutPoint(txid, vout));
rawTx.vin.push_back(in);
if (isUsingBuilder_) {
CScript scriptPubKey = GetScriptForDestination(fromtaddr_);
for (auto t : t_inputs_) {
uint256 txid = std::get<0>(t);
int vout = std::get<1>(t);
CAmount amount = std::get<2>(t);
builder_.AddTransparentInput(COutPoint(txid, vout), scriptPubKey, amount);
}
} else {
CMutableTransaction rawTx(tx_);
for (SendManyInputUTXO & t : t_inputs_) {
uint256 txid = std::get<0>(t);
int vout = std::get<1>(t);
CAmount amount = std::get<2>(t);
CTxIn in(COutPoint(txid, vout));
rawTx.vin.push_back(in);
}
tx_ = CTransaction(rawTx);
}
tx_ = CTransaction(rawTx);
}
LogPrint((isfromtaddr_) ? "zrpc" : "zrpcunsafe", "%s: spending %s to send %s with fee %s\n",
@ -347,6 +368,141 @@ bool AsyncRPCOperation_sendmany::main_impl() {
LogPrint("zrpcunsafe", "%s: private output: %s\n", getId(), FormatMoney(z_outputs_total));
LogPrint("zrpc", "%s: fee: %s\n", getId(), FormatMoney(minersFee));
/**
* SCENARIO #0
*
* Sprout not involved, so we just use the TransactionBuilder and we're done.
* We added the transparent inputs to the builder earlier.
*/
if (isUsingBuilder_) {
builder_.SetFee(minersFee);
// Get various necessary keys
SaplingExpandedSpendingKey expsk;
SaplingFullViewingKey from;
if (isfromzaddr_) {
auto sk = boost::get<libzcash::SaplingSpendingKey>(spendingkey_);
expsk = sk.expanded_spending_key();
from = expsk.full_viewing_key();
} else {
// TODO: Set "from" to something!
}
// Set change address if we are using transparent funds
// TODO: Should we just use fromtaddr_ as the change address?
if (isfromtaddr_) {
LOCK2(cs_main, pwalletMain->cs_wallet);
EnsureWalletIsUnlocked();
CReserveKey keyChange(pwalletMain);
CPubKey vchPubKey;
bool ret = keyChange.GetReservedKey(vchPubKey);
if (!ret) {
// should never fail, as we just unlocked
throw JSONRPCError(
RPC_WALLET_KEYPOOL_RAN_OUT,
"Could not generate a taddr to use as a change address");
}
CTxDestination changeAddr = vchPubKey.GetID();
assert(builder_.SendChangeTo(changeAddr));
}
// Select Sapling notes
std::vector<SaplingOutPoint> ops;
std::vector<SaplingNote> notes;
CAmount sum = 0;
for (auto t : z_sapling_inputs_) {
ops.push_back(t.op);
notes.push_back(t.note);
sum += t.note.value();
if (sum >= targetAmount) {
break;
}
}
// Fetch Sapling anchor and witnesses
uint256 anchor;
std::vector<boost::optional<SaplingWitness>> witnesses;
{
LOCK2(cs_main, pwalletMain->cs_wallet);
pwalletMain->GetSaplingNoteWitnesses(ops, witnesses, anchor);
}
// Add Sapling spends
for (size_t i = 0; i < notes.size(); i++) {
if (!witnesses[i]) {
throw JSONRPCError(RPC_WALLET_ERROR, "Missing witness for Sapling note");
}
assert(builder_.AddSaplingSpend(expsk, notes[i], anchor, witnesses[i].get()));
}
// Add Sapling outputs
for (auto r : z_outputs_) {
auto address = std::get<0>(r);
auto value = std::get<1>(r);
auto hexMemo = std::get<2>(r);
auto addr = DecodePaymentAddress(address);
assert(boost::get<libzcash::SaplingPaymentAddress>(&addr) != nullptr);
auto to = boost::get<libzcash::SaplingPaymentAddress>(addr);
auto memo = get_memo_from_hex_string(hexMemo);
builder_.AddSaplingOutput(from, to, value, memo);
}
// Add transparent outputs
for (auto r : t_outputs_) {
auto outputAddress = std::get<0>(r);
auto amount = std::get<1>(r);
auto address = DecodeDestination(outputAddress);
if (!builder_.AddTransparentOutput(address, amount)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid output address, not a valid taddr.");
}
}
// Build the transaction
auto maybe_tx = builder_.Build();
if (!maybe_tx) {
throw JSONRPCError(RPC_WALLET_ERROR, "Failed to build transaction.");
}
tx_ = maybe_tx.get();
// Send the transaction
// TODO: Use CWallet::CommitTransaction instead of sendrawtransaction
auto signedtxn = EncodeHexTx(tx_);
if (!testmode) {
UniValue params = UniValue(UniValue::VARR);
params.push_back(signedtxn);
UniValue sendResultValue = sendrawtransaction(params, false);
if (sendResultValue.isNull()) {
throw JSONRPCError(RPC_WALLET_ERROR, "sendrawtransaction did not return an error or a txid.");
}
auto 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.
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);
}
return true;
}
/**
* END SCENARIO #0
*/
// Grab the current consensus branch ID
{
LOCK(cs_main);
@ -395,7 +551,7 @@ bool AsyncRPCOperation_sendmany::main_impl() {
// Copy zinputs and zoutputs to more flexible containers
std::deque<SendManyInputJSOP> zInputsDeque; // zInputsDeque stores minimum numbers of notes for target amount
CAmount tmp = 0;
for (auto o : z_inputs_) {
for (auto o : z_sprout_inputs_) {
zInputsDeque.push_back(o);
tmp += std::get<2>(o);
if (tmp >= targetAmount) {
@ -404,18 +560,15 @@ bool AsyncRPCOperation_sendmany::main_impl() {
}
std::deque<SendManyRecipient> zOutputsDeque;
for (auto o : z_outputs_) {
// TODO: Add Sapling support. For now, ensure we can later convert freely.
auto addr = DecodePaymentAddress(std::get<0>(o));
assert(boost::get<libzcash::SproutPaymentAddress>(&addr) != nullptr);
zOutputsDeque.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.
if (z_inputs_.size() > 0) {
if (z_sprout_inputs_.size() > 0) {
LOCK2(cs_main, pwalletMain->cs_wallet);
for (auto t : z_inputs_) {
for (auto t : z_sprout_inputs_) {
JSOutPoint jso = std::get<0>(t);
std::vector<JSOutPoint> vOutPoints = { jso };
uint256 inputAnchor;
@ -448,7 +601,7 @@ bool AsyncRPCOperation_sendmany::main_impl() {
if (selectedUTXOCoinbase) {
assert(isSingleZaddrOutput);
throw JSONRPCError(RPC_WALLET_ERROR, strprintf(
"Change %s not allowed. When protecting coinbase funds, the wallet does not "
"Change %s not allowed. When shielding coinbase funds, the wallet does not "
"allow any change as there is currently no way to specify a change address "
"in z_sendmany.", FormatMoney(change)));
} else {
@ -734,6 +887,7 @@ bool AsyncRPCOperation_sendmany::main_impl() {
info.vjsout.push_back(JSOutput()); // dummy output while we accumulate funds into a change note for vpub_new
} else {
PaymentAddress pa = DecodePaymentAddress(address);
// If we are here, we know we have no Sapling outputs.
JSOutput jso = JSOutput(boost::get<libzcash::SproutPaymentAddress>(pa), value);
if (hexMemo.size() > 0) {
jso.memo = get_memo_from_hex_string(hexMemo);
@ -768,9 +922,6 @@ bool AsyncRPCOperation_sendmany::main_impl() {
}
extern UniValue signrawtransaction(const UniValue& params, bool fHelp);
extern UniValue sendrawtransaction(const UniValue& params, bool fHelp);
/**
* Sign and send a raw transaction.
* Raw transaction as hex string should be in object field "rawtxn"
@ -872,7 +1023,7 @@ bool AsyncRPCOperation_sendmany::find_utxos(bool fAcceptCoinbase=false) {
if (isCoinbase && fAcceptCoinbase==false) {
continue;
}
CAmount nValue = out.tx->vout[out.i].nValue;
SendManyInputUTXO utxo(out.tx->GetHash(), out.i, nValue, isCoinbase);
t_inputs_.push_back(utxo);
@ -895,10 +1046,19 @@ bool AsyncRPCOperation_sendmany::find_unspent_notes() {
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, fromaddress_, mindepth_);
}
// If using the TransactionBuilder, we only want Sapling notes.
// If not using it, we only want Sprout notes.
// TODO: Refactor `GetFilteredNotes()` so we only fetch what we need.
if (isUsingBuilder_) {
sproutEntries.clear();
} else {
saplingEntries.clear();
}
for (CSproutNotePlaintextEntry & entry : sproutEntries) {
z_inputs_.push_back(SendManyInputJSOP(entry.jsop, entry.plaintext.note(boost::get<libzcash::SproutPaymentAddress>(frompaymentaddress_)), CAmount(entry.plaintext.value())));
z_sprout_inputs_.push_back(SendManyInputJSOP(entry.jsop, entry.plaintext.note(boost::get<libzcash::SproutPaymentAddress>(frompaymentaddress_)), CAmount(entry.plaintext.value())));
std::string data(entry.plaintext.memo().begin(), entry.plaintext.memo().end());
LogPrint("zrpcunsafe", "%s: found unspent note (txid=%s, vjoinsplit=%d, ciphertext=%d, amount=%s, memo=%s)\n",
LogPrint("zrpcunsafe", "%s: found unspent Sprout note (txid=%s, vjoinsplit=%d, ciphertext=%d, amount=%s, memo=%s)\n",
getId(),
entry.jsop.hash.ToString().substr(0, 10),
entry.jsop.js,
@ -907,16 +1067,31 @@ bool AsyncRPCOperation_sendmany::find_unspent_notes() {
HexStr(data).substr(0, 10)
);
}
// TODO: Do something with Sapling notes
if (z_inputs_.size() == 0) {
for (auto entry : saplingEntries) {
z_sapling_inputs_.push_back(entry);
std::string data(entry.memo.begin(), entry.memo.end());
LogPrint("zrpcunsafe", "%s: found unspent Sapling note (txid=%s, vShieldedSpend=%d, amount=%s, memo=%s)\n",
getId(),
entry.op.hash.ToString().substr(0, 10),
entry.op.n,
FormatMoney(entry.note.value()),
HexStr(data).substr(0, 10));
}
if (z_sprout_inputs_.empty() && z_sapling_inputs_.empty()) {
return false;
}
// sort in descending order, so big notes appear first
std::sort(z_inputs_.begin(), z_inputs_.end(), [](SendManyInputJSOP i, SendManyInputJSOP j) -> bool {
return ( std::get<2>(i) > std::get<2>(j));
});
std::sort(z_sprout_inputs_.begin(), z_sprout_inputs_.end(),
[](SendManyInputJSOP i, SendManyInputJSOP j) -> bool {
return std::get<2>(i) > std::get<2>(j);
});
std::sort(z_sapling_inputs_.begin(), z_sapling_inputs_.end(),
[](SaplingNoteEntry i, SaplingNoteEntry j) -> bool {
return i.note.value() > j.note.value();
});
return true;
}

View File

@ -8,6 +8,7 @@
#include "asyncrpcoperation.h"
#include "amount.h"
#include "primitives/transaction.h"
#include "transaction_builder.h"
#include "zcash/JoinSplit.hpp"
#include "zcash/Address.hpp"
#include "wallet.h"
@ -51,7 +52,15 @@ struct WitnessAnchorData {
class AsyncRPCOperation_sendmany : public AsyncRPCOperation {
public:
AsyncRPCOperation_sendmany(CMutableTransaction contextualTx, std::string fromAddress, std::vector<SendManyRecipient> tOutputs, std::vector<SendManyRecipient> zOutputs, int minDepth, CAmount fee = ASYNC_RPC_OPERATION_DEFAULT_MINERS_FEE, UniValue contextInfo = NullUniValue);
AsyncRPCOperation_sendmany(
boost::optional<TransactionBuilder> builder,
CMutableTransaction contextualTx,
std::string fromAddress,
std::vector<SendManyRecipient> tOutputs,
std::vector<SendManyRecipient> zOutputs,
int minDepth,
CAmount fee = ASYNC_RPC_OPERATION_DEFAULT_MINERS_FEE,
UniValue contextInfo = NullUniValue);
virtual ~AsyncRPCOperation_sendmany();
// We don't want to be copied or moved around
@ -73,6 +82,7 @@ private:
UniValue contextinfo_; // optional data to include in return value from getStatus()
bool isUsingBuilder_; // Indicates that no Sprout addresses are involved
uint32_t consensusBranchId_;
CAmount fee_;
int mindepth_;
@ -92,8 +102,10 @@ private:
std::vector<SendManyRecipient> t_outputs_;
std::vector<SendManyRecipient> z_outputs_;
std::vector<SendManyInputUTXO> t_inputs_;
std::vector<SendManyInputJSOP> z_inputs_;
std::vector<SendManyInputJSOP> z_sprout_inputs_;
std::vector<SaplingNoteEntry> z_sapling_inputs_;
TransactionBuilder builder_;
CTransaction tx_;
void add_taddr_change_output_to_tx(CAmount amount);

View File

@ -784,41 +784,6 @@ UniValue z_importviewingkey(const UniValue& params, bool fHelp)
return NullUniValue;
}
class GetSpendingKeyForPaymentAddress : public boost::static_visitor<libzcash::SpendingKey>
{
private:
CWallet *m_wallet;
public:
GetSpendingKeyForPaymentAddress(CWallet *wallet) : m_wallet(wallet) {}
libzcash::SpendingKey operator()(const libzcash::SproutPaymentAddress &zaddr) const
{
libzcash::SproutSpendingKey k;
if (!pwalletMain->GetSproutSpendingKey(zaddr, k)) {
throw JSONRPCError(RPC_WALLET_ERROR, "Wallet does not hold private zkey for this zaddr");
}
return k;
}
libzcash::SpendingKey operator()(const libzcash::SaplingPaymentAddress &zaddr) const
{
libzcash::SaplingIncomingViewingKey ivk;
libzcash::SaplingFullViewingKey fvk;
libzcash::SaplingSpendingKey sk;
if (!pwalletMain->GetSaplingIncomingViewingKey(zaddr, ivk) ||
!pwalletMain->GetSaplingFullViewingKey(ivk, fvk) ||
!pwalletMain->GetSaplingSpendingKey(fvk, sk)) {
throw JSONRPCError(RPC_WALLET_ERROR, "Wallet does not hold private zkey for this zaddr");
}
return sk;
}
libzcash::SpendingKey operator()(const libzcash::InvalidEncoding& no) const {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid zaddr");
}
};
UniValue z_exportkey(const UniValue& params, bool fHelp)
{
if (!EnsureWalletIsAvailable(fHelp))
@ -852,7 +817,10 @@ UniValue z_exportkey(const UniValue& params, bool fHelp)
// Sapling support
auto sk = boost::apply_visitor(GetSpendingKeyForPaymentAddress(pwalletMain), address);
return EncodeSpendingKey(sk);
if (!sk) {
throw JSONRPCError(RPC_WALLET_ERROR, "Wallet does not hold private zkey for this zaddr");
}
return EncodeSpendingKey(sk.get());
}
UniValue z_exportviewingkey(const UniValue& params, bool fHelp)

View File

@ -13,6 +13,7 @@
#include "netbase.h"
#include "rpc/server.h"
#include "timedata.h"
#include "transaction_builder.h"
#include "util.h"
#include "utilmoneystr.h"
#include "wallet.h"
@ -3609,26 +3610,26 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
// Check that the from address is valid.
auto fromaddress = params[0].get_str();
bool fromTaddr = false;
bool fromSapling = false;
CTxDestination taddr = DecodeDestination(fromaddress);
fromTaddr = IsValidDestination(taddr);
libzcash::SproutPaymentAddress zaddr;
if (!fromTaddr) {
auto res = DecodePaymentAddress(fromaddress);
if (!IsValidPaymentAddress(res)) {
// invalid
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid from address, should be a taddr or zaddr.");
}
// TODO: Add Sapling support. For now, ensure we can freely convert.
assert(boost::get<libzcash::SproutPaymentAddress>(&res) != nullptr);
zaddr = boost::get<libzcash::SproutPaymentAddress>(res);
}
// Check that we have the spending key
if (!fromTaddr) {
if (!pwalletMain->HaveSproutSpendingKey(zaddr)) {
// Check that we have the spending key
if (!boost::apply_visitor(HaveSpendingKeyForPaymentAddress(pwalletMain), res)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "From address does not belong to this node, zaddr spending key not found.");
}
// Remember whether this is a Sprout or Sapling address
fromSapling = boost::get<libzcash::SaplingPaymentAddress>(&res) != nullptr;
}
// This logic will need to be updated if we add a new shielded pool
bool fromSprout = !(fromTaddr || fromSapling);
UniValue outputs = params[1].get_array();
@ -3638,6 +3639,9 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
// Keep track of addresses to spot duplicates
set<std::string> setAddress;
// Track whether we see any Sprout addresses
bool noSproutAddrs = !fromSprout;
// Recipients
std::vector<SendManyRecipient> taddrRecipients;
std::vector<SendManyRecipient> zaddrRecipients;
@ -3658,8 +3662,26 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
bool isZaddr = false;
CTxDestination taddr = DecodeDestination(address);
if (!IsValidDestination(taddr)) {
if (IsValidPaymentAddressString(address)) {
auto res = DecodePaymentAddress(address);
if (IsValidPaymentAddress(res)) {
isZaddr = true;
bool toSapling = boost::get<libzcash::SaplingPaymentAddress>(&res) != nullptr;
bool toSprout = !toSapling;
noSproutAddrs = noSproutAddrs && toSapling;
// If we are sending from a shielded address, all recipient
// shielded addresses must be of the same type.
if (fromSprout && toSapling) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Cannot send from a Sprout address to a Sapling address using z_sendmany");
}
if (fromSapling && toSprout) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Cannot send from a Sapling address to a Sprout address using z_sendmany");
}
} else {
throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, unknown address format: ")+address );
}
@ -3787,7 +3809,14 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
o.push_back(Pair("fee", std::stod(FormatMoney(nFee))));
UniValue contextInfo = o;
// Builder (used if Sapling addresses are involved)
boost::optional<TransactionBuilder> builder;
if (noSproutAddrs) {
builder = TransactionBuilder(Params().GetConsensus(), nextBlockHeight, pwalletMain);
}
// Contextual transaction we will build on
// (used if no Sapling addresses are involved)
CMutableTransaction contextualTx = CreateNewContextualCMutableTransaction(Params().GetConsensus(), nextBlockHeight);
bool isShielded = !fromTaddr || zaddrRecipients.size() > 0;
if (contextualTx.nVersion == 1 && isShielded) {
@ -3796,7 +3825,7 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
// Create operation and add to global queue
std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue();
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(contextualTx, fromaddress, taddrRecipients, zaddrRecipients, nMinDepth, nFee, contextInfo) );
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(builder, contextualTx, fromaddress, taddrRecipients, zaddrRecipients, nMinDepth, nFee, contextInfo) );
q->addOperation(operation);
AsyncRPCOperationId operationId = operation->getId();
return operationId;

View File

@ -4373,3 +4373,57 @@ bool PaymentAddressBelongsToWallet::operator()(const libzcash::InvalidEncoding&
{
return false;
}
bool HaveSpendingKeyForPaymentAddress::operator()(const libzcash::SproutPaymentAddress &zaddr) const
{
return m_wallet->HaveSproutSpendingKey(zaddr);
}
bool HaveSpendingKeyForPaymentAddress::operator()(const libzcash::SaplingPaymentAddress &zaddr) const
{
libzcash::SaplingIncomingViewingKey ivk;
libzcash::SaplingFullViewingKey fvk;
return m_wallet->GetSaplingIncomingViewingKey(zaddr, ivk) &&
m_wallet->GetSaplingFullViewingKey(ivk, fvk) &&
m_wallet->HaveSaplingSpendingKey(fvk);
}
bool HaveSpendingKeyForPaymentAddress::operator()(const libzcash::InvalidEncoding& no) const
{
return false;
}
boost::optional<libzcash::SpendingKey> GetSpendingKeyForPaymentAddress::operator()(
const libzcash::SproutPaymentAddress &zaddr) const
{
libzcash::SproutSpendingKey k;
if (m_wallet->GetSproutSpendingKey(zaddr, k)) {
return libzcash::SpendingKey(k);
} else {
return boost::none;
}
}
boost::optional<libzcash::SpendingKey> GetSpendingKeyForPaymentAddress::operator()(
const libzcash::SaplingPaymentAddress &zaddr) const
{
libzcash::SaplingIncomingViewingKey ivk;
libzcash::SaplingFullViewingKey fvk;
libzcash::SaplingSpendingKey sk;
if (m_wallet->GetSaplingIncomingViewingKey(zaddr, ivk) &&
m_wallet->GetSaplingFullViewingKey(ivk, fvk) &&
m_wallet->GetSaplingSpendingKey(fvk, sk)) {
return libzcash::SpendingKey(sk);
} else {
return boost::none;
}
}
boost::optional<libzcash::SpendingKey> GetSpendingKeyForPaymentAddress::operator()(
const libzcash::InvalidEncoding& no) const
{
// Defaults to InvalidEncoding
return libzcash::SpendingKey();
}

View File

@ -1319,4 +1319,28 @@ public:
bool operator()(const libzcash::InvalidEncoding& no) const;
};
class HaveSpendingKeyForPaymentAddress : public boost::static_visitor<bool>
{
private:
CWallet *m_wallet;
public:
HaveSpendingKeyForPaymentAddress(CWallet *wallet) : m_wallet(wallet) {}
bool operator()(const libzcash::SproutPaymentAddress &zaddr) const;
bool operator()(const libzcash::SaplingPaymentAddress &zaddr) const;
bool operator()(const libzcash::InvalidEncoding& no) const;
};
class GetSpendingKeyForPaymentAddress : public boost::static_visitor<boost::optional<libzcash::SpendingKey>>
{
private:
CWallet *m_wallet;
public:
GetSpendingKeyForPaymentAddress(CWallet *wallet) : m_wallet(wallet) {}
boost::optional<libzcash::SpendingKey> operator()(const libzcash::SproutPaymentAddress &zaddr) const;
boost::optional<libzcash::SpendingKey> operator()(const libzcash::SaplingPaymentAddress &zaddr) const;
boost::optional<libzcash::SpendingKey> operator()(const libzcash::InvalidEncoding& no) const;
};
#endif // BITCOIN_WALLET_WALLET_H