Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Makefile.gtest.include
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ zcash_gtest_SOURCES += \
if ENABLE_WALLET
zcash_gtest_SOURCES += \
wallet/gtest/test_paymentdisclosure.cpp \
wallet/gtest/test_coincontrol.cpp \
wallet/gtest/test_wallet.cpp
endif

Expand Down
113 changes: 102 additions & 11 deletions src/wallet/asyncrpcoperation_sendmany.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,13 @@ AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany(
std::vector<SendManyRecipient> zOutputs,
int minDepth,
CAmount fee,
UniValue contextInfo) :
tx_(contextualTx), fromaddress_(fromAddress), t_outputs_(tOutputs), z_outputs_(zOutputs), mindepth_(minDepth), fee_(fee), contextinfo_(contextInfo)
UniValue contextInfo,
bool useInputSelection,
std::set<COutPoint> pinnedTransparent,
std::set<SaplingOutPoint> pinnedSapling,
std::set<JSOutPoint> pinnedSprout) :
tx_(contextualTx), fromaddress_(fromAddress), t_outputs_(tOutputs), z_outputs_(zOutputs), mindepth_(minDepth), fee_(fee), contextinfo_(contextInfo),
useInputSelection_(useInputSelection), pinnedTransparent_(pinnedTransparent), pinnedSapling_(pinnedSapling), pinnedSprout_(pinnedSprout)
{
assert(fee_ >= 0);

Expand Down Expand Up @@ -296,10 +301,19 @@ bool AsyncRPCOperation_sendmany::main_impl() {
}
selectedUTXOAmount += std::get<2>(t);
selectedTInputs.push_back(t);
LogPrint("zrpcunsafe", "%s: spending utxo (txid=%s, vout=%d, amount=%s, coinbase=%d)%s\n",
getId(),
std::get<0>(t).ToString().substr(0, 10),
std::get<1>(t),
FormatMoney(std::get<2>(t)),
(int)std::get<3>(t),
(useInputSelection_ && pinnedTransparent_.count(COutPoint(std::get<0>(t), std::get<1>(t)))) ? " [PINNED]" : "");
if (selectedUTXOAmount >= targetAmount) {
// Select another utxo if there is change less than the dust threshold.
dustChange = selectedUTXOAmount - targetAmount;
if (dustChange == 0 || dustChange >= dustThreshold) {
// When pinning inputs (coin-control), consume ALL pinned UTXOs
// and never early-break; the dust-change guard below still applies.
if (!useInputSelection_ && (dustChange == 0 || dustChange >= dustThreshold)) {
break;
}
}
Expand Down Expand Up @@ -419,7 +433,9 @@ bool AsyncRPCOperation_sendmany::main_impl() {
ops.push_back(t.op);
notes.push_back(t.note);
sum += t.note.value();
if (sum >= targetAmount) {
// Coin control: when the caller pinned exact notes, consume ALL of them
// (any surplus becomes shielded change); only auto-selection early-breaks.
if (!useInputSelection_ && sum >= targetAmount) {
break;
}
}
Expand Down Expand Up @@ -550,7 +566,9 @@ bool AsyncRPCOperation_sendmany::main_impl() {
for (auto o : z_sprout_inputs_) {
zInputsDeque.push_back(o);
tmp += std::get<2>(o);
if (tmp >= targetAmount) {
// Coin control: when the caller pinned exact notes, consume ALL of them
// (any surplus becomes shielded change); only auto-selection early-breaks.
if (!useInputSelection_ && tmp >= targetAmount) {
break;
}
}
Expand Down Expand Up @@ -790,14 +808,15 @@ bool AsyncRPCOperation_sendmany::main_impl() {
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",
LogPrint("zrpcunsafe", "%s: spending note (txid=%s, vjoinsplit=%d, ciphertext=%d, amount=%s, height=%d, confirmations=%d)%s\n",
getId(),
jso.hash.ToString().substr(0, 10),
jso.js,
int(jso.n), // uint8_t
FormatMoney(noteFunds),
wtxHeight,
wtxDepth
wtxDepth,
(useInputSelection_ && pinnedSprout_.count(jso)) ? " [PINNED]" : ""
);
}

Expand Down Expand Up @@ -1025,6 +1044,32 @@ bool AsyncRPCOperation_sendmany::find_utxos(bool fAcceptCoinbase=false) {
t_inputs_.push_back(utxo);
}

// Coin-control: restrict to exactly the pinned transparent UTXOs.
// NON-CONSENSUS: only narrows which already-valid inputs we may select.
if (useInputSelection_) {
// Build the set of available outpoints for membership / presence checks.
std::set<COutPoint> available;
for (const SendManyInputUTXO & t : t_inputs_) {
available.insert(COutPoint(std::get<0>(t), std::get<1>(t)));
}
// Every pinned transparent input must be present in the spendable set.
for (const COutPoint & op : pinnedTransparent_) {
if (!available.count(op)) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
strprintf("Pinned transparent input not available (spendable, confirmed, owned by from-address): %s:%d",
op.hash.ToString(), op.n));
}
}
// Drop any UTXO that was not pinned.
std::vector<SendManyInputUTXO> filtered;
for (const SendManyInputUTXO & t : t_inputs_) {
if (pinnedTransparent_.count(COutPoint(std::get<0>(t), std::get<1>(t)))) {
filtered.push_back(t);
}
}
t_inputs_ = filtered;
}

// sort in ascending order, so smaller utxos appear first
std::sort(t_inputs_.begin(), t_inputs_.end(), [](SendManyInputUTXO i, SendManyInputUTXO j) -> bool {
return ( std::get<2>(i) < std::get<2>(j));
Expand Down Expand Up @@ -1054,25 +1099,71 @@ bool AsyncRPCOperation_sendmany::find_unspent_notes() {
for (CSproutNotePlaintextEntry & entry : sproutEntries) {
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 Sprout 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)%s\n",
getId(),
entry.jsop.hash.ToString().substr(0, 10),
entry.jsop.js,
int(entry.jsop.n), // uint8_t
FormatMoney(entry.plaintext.value()),
HexStr(data).substr(0, 10)
HexStr(data).substr(0, 10),
(useInputSelection_ && pinnedSprout_.count(entry.jsop)) ? " [PINNED]" : ""
);
}

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",
LogPrint("zrpcunsafe", "%s: found unspent Sapling note (txid=%s, vShieldedSpend=%d, amount=%s, memo=%s)%s\n",
getId(),
entry.op.hash.ToString().substr(0, 10),
entry.op.n,
FormatMoney(entry.note.value()),
HexStr(data).substr(0, 10));
HexStr(data).substr(0, 10),
(useInputSelection_ && pinnedSapling_.count(entry.op)) ? " [PINNED]" : "");
}

// Coin-control: restrict to exactly the pinned shielded notes.
// NON-CONSENSUS: only narrows which already-valid notes we may select.
if (useInputSelection_) {
// Sapling: every pinned note must be present in the spendable set.
std::set<SaplingOutPoint> availableSapling;
for (const SaplingNoteEntry & e : z_sapling_inputs_) {
availableSapling.insert(e.op);
}
for (const SaplingOutPoint & op : pinnedSapling_) {
if (!availableSapling.count(op)) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
strprintf("Pinned Sapling note not available (spendable, confirmed, owned by from-address): %s:%d",
op.hash.ToString(), op.n));
}
}
std::vector<SaplingNoteEntry> filteredSapling;
for (const SaplingNoteEntry & e : z_sapling_inputs_) {
if (pinnedSapling_.count(e.op)) {
filteredSapling.push_back(e);
}
}
z_sapling_inputs_ = filteredSapling;

// Sprout: every pinned note must be present in the spendable set.
std::set<JSOutPoint> availableSprout;
for (const SendManyInputJSOP & t : z_sprout_inputs_) {
availableSprout.insert(std::get<0>(t));
}
for (const JSOutPoint & op : pinnedSprout_) {
if (!availableSprout.count(op)) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
strprintf("Pinned Sprout note not available (spendable, confirmed, owned by from-address): %s:%d:%d",
op.hash.ToString(), (int)op.js, (int)op.n));
}
}
std::vector<SendManyInputJSOP> filteredSprout;
for (const SendManyInputJSOP & t : z_sprout_inputs_) {
if (pinnedSprout_.count(std::get<0>(t))) {
filteredSprout.push_back(t);
}
}
z_sprout_inputs_ = filteredSprout;
}

if (z_sprout_inputs_.empty() && z_sapling_inputs_.empty()) {
Expand Down
35 changes: 34 additions & 1 deletion src/wallet/asyncrpcoperation_sendmany.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "wallet/paymentdisclosure.h"

#include <array>
#include <set>
#include <unordered_map>
#include <tuple>

Expand Down Expand Up @@ -60,7 +61,14 @@ class AsyncRPCOperation_sendmany : public AsyncRPCOperation {
std::vector<SendManyRecipient> zOutputs,
int minDepth,
CAmount fee = ASYNC_RPC_OPERATION_DEFAULT_MINERS_FEE,
UniValue contextInfo = NullUniValue);
UniValue contextInfo = NullUniValue,
// Optional coin-control: when useInputSelection is true, the spend is
// restricted to exactly the pinned UTXOs/notes below. NON-CONSENSUS:
// this only narrows which already-valid inputs the wallet may select.
bool useInputSelection = false,
std::set<COutPoint> pinnedTransparent = std::set<COutPoint>(),
std::set<SaplingOutPoint> pinnedSapling = std::set<SaplingOutPoint>(),
std::set<JSOutPoint> pinnedSprout = std::set<JSOutPoint>());
virtual ~AsyncRPCOperation_sendmany();

// We don't want to be copied or moved around
Expand Down Expand Up @@ -105,6 +113,14 @@ class AsyncRPCOperation_sendmany : public AsyncRPCOperation {
std::vector<SendManyInputJSOP> z_sprout_inputs_;
std::vector<SaplingNoteEntry> z_sapling_inputs_;

// Coin-control (optional). When useInputSelection_ is true, find_utxos()
// and find_unspent_notes() restrict the spend to exactly these inputs.
// NON-CONSENSUS: this only narrows selection of already-valid inputs.
bool useInputSelection_ = false;
std::set<COutPoint> pinnedTransparent_;
std::set<SaplingOutPoint> pinnedSapling_;
std::set<JSOutPoint> pinnedSprout_;

TransactionBuilder builder_;
CTransaction tx_;

Expand Down Expand Up @@ -198,6 +214,23 @@ class TEST_FRIEND_AsyncRPCOperation_sendmany {
void set_state(OperationStatus state) {
delegate->state_.store(state);
}

// Coin-control accessors (for unit testing input selection wiring).
bool useInputSelection() {
return delegate->useInputSelection_;
}

const std::set<COutPoint>& pinnedTransparent() {
return delegate->pinnedTransparent_;
}

const std::set<SaplingOutPoint>& pinnedSapling() {
return delegate->pinnedSapling_;
}

const std::set<JSOutPoint>& pinnedSprout() {
return delegate->pinnedSprout_;
}
};


Expand Down
Loading
Loading