rpcwallet: Add Sapling support to z_sendmany
This commit is contained in:
parent
af4057b904
commit
e54c4d2ca1
|
@ -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'
|
||||
|
|
|
@ -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()
|
|
@ -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()) {
|
||||
|
@ -90,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");
|
||||
}
|
||||
|
@ -251,6 +249,9 @@ bool AsyncRPCOperation_sendmany::main_impl() {
|
|||
for (SendManyInputJSOP & t : z_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_) {
|
||||
|
@ -335,6 +336,15 @@ bool AsyncRPCOperation_sendmany::main_impl() {
|
|||
}
|
||||
|
||||
// update the transaction with these inputs
|
||||
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);
|
||||
|
@ -345,6 +355,7 @@ bool AsyncRPCOperation_sendmany::main_impl() {
|
|||
}
|
||||
tx_ = CTransaction(rawTx);
|
||||
}
|
||||
}
|
||||
|
||||
LogPrint((isfromtaddr_) ? "zrpc" : "zrpcunsafe", "%s: spending %s to send %s with fee %s\n",
|
||||
getId(), FormatMoney(targetAmount), FormatMoney(sendAmount), FormatMoney(minersFee));
|
||||
|
@ -354,6 +365,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);
|
||||
|
@ -411,9 +557,6 @@ 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);
|
||||
}
|
||||
|
||||
|
@ -741,6 +884,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);
|
||||
|
@ -775,9 +919,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"
|
||||
|
@ -902,10 +1043,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())));
|
||||
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,
|
||||
|
@ -914,9 +1064,19 @@ 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_inputs_.empty() && z_sapling_inputs_.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -924,6 +1084,10 @@ bool AsyncRPCOperation_sendmany::find_unspent_notes() {
|
|||
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_sapling_inputs_.begin(), z_sapling_inputs_.end(),
|
||||
[](SaplingNoteEntry i, SaplingNoteEntry j) -> bool {
|
||||
return ( i.note.value() > j.note.value());
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -103,6 +103,7 @@ private:
|
|||
std::vector<SendManyRecipient> z_outputs_;
|
||||
std::vector<SendManyInputUTXO> t_inputs_;
|
||||
std::vector<SendManyInputJSOP> z_inputs_;
|
||||
std::vector<SaplingNoteEntry> z_sapling_inputs_;
|
||||
|
||||
TransactionBuilder builder_;
|
||||
CTransaction tx_;
|
||||
|
|
|
@ -3823,9 +3823,6 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
|
|||
contextualTx.nVersion = 2; // Tx format should support vjoinsplits
|
||||
}
|
||||
|
||||
// TODO: Add Sapling support to AsyncRPCOperation_sendmany()
|
||||
assert(!noSproutAddrs);
|
||||
|
||||
// Create operation and add to global queue
|
||||
std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue();
|
||||
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(builder, contextualTx, fromaddress, taddrRecipients, zaddrRecipients, nMinDepth, nFee, contextInfo) );
|
||||
|
|
Loading…
Reference in New Issue