Select spendable inputs based on recipient and change pool types

This information enables us to select notes and coins in a way that
minimizes information leakage while moving funds into the shielded pool
where possible.
This commit is contained in:
Jack Grigg 2022-03-12 16:14:05 +00:00
parent 959b068468
commit 920de99c85
5 changed files with 326 additions and 197 deletions

View File

@ -62,26 +62,22 @@ AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany(
sendFromAccount_ = pwalletMain->FindAccountForSelector(ztxoSelector_).value_or(ZCASH_LEGACY_ACCOUNT);
// we always allow shielded change when not sending from the legacy account
if (sendFromAccount_ != ZCASH_LEGACY_ACCOUNT) {
allowedChangeTypes_.insert(OutputPool::Sapling);
}
// calculate the target totals
// Determine the target totals and recipient pools
for (const SendManyRecipient& recipient : recipients_) {
std::visit(match {
[&](const CKeyID& addr) {
transparentRecipients_ += 1;
txOutputAmounts_.t_outputs_total += recipient.amount;
allowedChangeTypes_.insert(OutputPool::Transparent);
recipientPools_.insert(OutputPool::Transparent);
},
[&](const CScriptID& addr) {
transparentRecipients_ += 1;
txOutputAmounts_.t_outputs_total += recipient.amount;
allowedChangeTypes_.insert(OutputPool::Transparent);
recipientPools_.insert(OutputPool::Transparent);
},
[&](const libzcash::SaplingPaymentAddress& addr) {
txOutputAmounts_.sapling_outputs_total += recipient.amount;
recipientPools_.insert(OutputPool::Sapling);
if (ztxoSelector_.SelectsSprout() && !allowRevealedAmounts_) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
@ -93,6 +89,7 @@ AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany(
},
[&](const libzcash::OrchardRawAddress& addr) {
txOutputAmounts_.orchard_outputs_total += recipient.amount;
// TODO ORCHARD: Add to recipientPools_
if ((ztxoSelector_.SelectsSprout() || ztxoSelector_.SelectsSapling()) && !allowRevealedAmounts_) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
@ -210,7 +207,7 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
LOCK2(cs_main, pwalletMain->cs_wallet);
spendable = pwalletMain->FindSpendableInputs(ztxoSelector_, allowTransparentCoinbase, mindepth_);
}
if (!spendable.LimitToAmount(targetAmount, dustThreshold)) {
if (!spendable.LimitToAmount(targetAmount, dustThreshold, recipientPools_)) {
CAmount changeAmount{spendable.Total() - targetAmount};
if (changeAmount > 0 && changeAmount < dustThreshold) {
// TODO: we should provide the option for the caller to explicitly
@ -289,19 +286,27 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
LogPrint("zrpcunsafe", "%s: total shielded Orchard output: %s\n", getId(), FormatMoney(txOutputAmounts_.orchard_outputs_total));
LogPrint("zrpc", "%s: fee: %s\n", getId(), FormatMoney(fee_));
// Allow change to go to any pool for which we have recipients.
std::set<OutputPool> allowedChangeTypes = recipientPools_;
// We always allow shielded change when not sending from the legacy account.
if (sendFromAccount_ != ZCASH_LEGACY_ACCOUNT) {
allowedChangeTypes.insert(OutputPool::Sapling);
}
auto ovks = this->SelectOVKs(spendable);
std::visit(match {
[&](const CKeyID& keyId) {
allowedChangeTypes_.insert(OutputPool::Transparent);
allowedChangeTypes.insert(OutputPool::Transparent);
auto changeAddr = pwalletMain->GenerateChangeAddressForAccount(
sendFromAccount_, allowedChangeTypes_);
sendFromAccount_, allowedChangeTypes);
assert(changeAddr.has_value());
builder_.SendChangeTo(changeAddr.value(), ovks.first);
},
[&](const CScriptID& scriptId) {
allowedChangeTypes_.insert(OutputPool::Transparent);
allowedChangeTypes.insert(OutputPool::Transparent);
auto changeAddr = pwalletMain->GenerateChangeAddressForAccount(
sendFromAccount_, allowedChangeTypes_);
sendFromAccount_, allowedChangeTypes);
assert(changeAddr.has_value());
builder_.SendChangeTo(changeAddr.value(), ovks.first);
},
@ -321,7 +326,7 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
builder_.SendChangeTo(addr, ovks.first);
} else {
auto changeAddr = pwalletMain->GenerateChangeAddressForAccount(
sendFromAccount_, allowedChangeTypes_);
sendFromAccount_, allowedChangeTypes);
assert(changeAddr.has_value());
builder_.SendChangeTo(changeAddr.value(), ovks.first);
}
@ -334,7 +339,7 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
builder_.SendChangeTo(fvk.DefaultAddress(), ovks.first);
} else {
auto changeAddr = pwalletMain->GenerateChangeAddressForAccount(
sendFromAccount_, allowedChangeTypes_);
sendFromAccount_, allowedChangeTypes);
assert(changeAddr.has_value());
builder_.SendChangeTo(changeAddr.value(), ovks.first);
}
@ -354,10 +359,10 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
switch (rtype) {
case ReceiverType::P2PKH:
case ReceiverType::P2SH:
allowedChangeTypes_.insert(OutputPool::Transparent);
allowedChangeTypes.insert(OutputPool::Transparent);
break;
case ReceiverType::Sapling:
allowedChangeTypes_.insert(OutputPool::Sapling);
allowedChangeTypes.insert(OutputPool::Sapling);
break;
case ReceiverType::Orchard:
// TODO
@ -367,7 +372,7 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
auto changeAddr = pwalletMain->GenerateChangeAddressForAccount(
acct.GetAccountId(),
allowedChangeTypes_);
allowedChangeTypes);
assert(changeAddr.has_value());
builder_.SendChangeTo(changeAddr.value(), ovks.first);

View File

@ -80,7 +80,7 @@ private:
bool allowRevealedAmounts_{false};
uint32_t transparentRecipients_{0};
AccountId sendFromAccount_;
std::set<OutputPool> allowedChangeTypes_;
std::set<OutputPool> recipientPools_;
TxOutputAmounts txOutputAmounts_;
/**

View File

@ -6,6 +6,13 @@
#include "zcash/address/sapling.hpp"
#include "zcash/address/sprout.hpp"
void PrintTo(const OutputPool& pool, std::ostream* os) {
switch (pool) {
case OutputPool::Sapling: *os << "Sapling"; break;
case OutputPool::Transparent: *os << "Transparent"; break;
}
}
CWalletTx FakeWalletTx()
{
CMutableTransaction mtx;
@ -15,232 +22,273 @@ CWalletTx FakeWalletTx()
}
SpendableInputs FakeSpendableInputs(
CAmount saplingValue,
CAmount sproutValue,
CAmount transparentValue = 0,
const CWalletTx* wtx = nullptr)
std::set<OutputPool> available,
bool includeSprout,
const CWalletTx* wtx)
{
SpendableInputs inputs;
while (saplingValue > 0) {
SaplingOutPoint op;
libzcash::SaplingPaymentAddress address;
libzcash::SaplingNote note(address, 1, libzcash::Zip212Enabled::AfterZip212);
inputs.saplingNoteEntries.push_back(SaplingNoteEntry{
op, address, note, {}, 100});
saplingValue -= 1;
if (available.count(OutputPool::Sapling)) {
for (int i = 0; i < 10; i++) {
SaplingOutPoint op;
libzcash::SaplingPaymentAddress address;
libzcash::SaplingNote note(address, 1, libzcash::Zip212Enabled::AfterZip212);
inputs.saplingNoteEntries.push_back(SaplingNoteEntry{
op, address, note, {}, 100});
}
}
while (sproutValue > 0) {
JSOutPoint jsop;
libzcash::SproutPaymentAddress address;
libzcash::SproutNote note(uint256(), 1, uint256(), uint256());
inputs.sproutNoteEntries.push_back(SproutNoteEntry{
jsop, address, note, {}, 100});
sproutValue -= 1;
if (available.count(OutputPool::Transparent)) {
for (int i = 0; i < 10; i++) {
COutput utxo(wtx, 0, 100, true);
inputs.utxos.push_back(utxo);
}
}
while (transparentValue > 0) {
COutput utxo(wtx, 0, 100, true);
inputs.utxos.push_back(utxo);
transparentValue -= wtx->vout[0].nValue;
if (includeSprout) {
for (int i = 0; i < 10; i++) {
JSOutPoint jsop;
libzcash::SproutPaymentAddress address;
libzcash::SproutNote note(uint256(), 1, uint256(), uint256());
inputs.sproutNoteEntries.push_back(SproutNoteEntry{
jsop, address, note, {}, 100});
}
}
return inputs;
}
TEST(NoteSelectionTest, SelectsSproutBeforeTransparent)
class SpendableInputsTest :
public ::testing::TestWithParam<std::tuple<
// Pools with available inputs
std::set<OutputPool>,
// Recipient pools
std::set<OutputPool>,
// Expected pool selection order
std::vector<OutputPool>>> {
};
TEST_P(SpendableInputsTest, SelectsSproutBeforeFirst)
{
auto available = std::get<0>(GetParam());
auto recipientPools = std::get<1>(GetParam());
auto order = std::get<2>(GetParam());
auto wtx = FakeWalletTx();
// Create a set of inputs from Sprout and transparent pools.
auto inputs = FakeSpendableInputs(0, 10, 10, &wtx);
EXPECT_EQ(inputs.Total(), 20);
// Create a set of inputs from Sprout and the available pools.
auto inputs = FakeSpendableInputs(available, true, &wtx);
EXPECT_EQ(inputs.Total(), 10 * (available.size() + 1));
// We have Sprout notes and UTXOs.
// We have Sprout notes along with the expected notes.
EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0);
EXPECT_EQ(inputs.saplingNoteEntries.size(), 0);
EXPECT_EQ(inputs.sproutNoteEntries.size(), 10);
EXPECT_EQ(inputs.utxos.size(), 10);
for (auto pool : available) {
switch (pool) {
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), 10); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 10); break;
}
}
// Limit to 5 zatoshis.
inputs.LimitToAmount(5, 1);
EXPECT_TRUE(inputs.LimitToAmount(5, 1, recipientPools));
EXPECT_EQ(inputs.Total(), 5);
// We only have Sprout notes.
EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0);
EXPECT_EQ(inputs.saplingNoteEntries.size(), 0);
EXPECT_EQ(inputs.sproutNoteEntries.size(), 5);
EXPECT_EQ(inputs.utxos.size(), 0);
for (auto pool : order) {
switch (pool) {
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), 0); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 0); break;
}
}
}
TEST(NoteSelectionTest, SelectsSproutThenTransparent)
TEST_P(SpendableInputsTest, SelectsSproutThenFirst)
{
auto available = std::get<0>(GetParam());
auto recipientPools = std::get<1>(GetParam());
auto order = std::get<2>(GetParam());
auto wtx = FakeWalletTx();
// Create a set of inputs from Sprout and transparent pools.
auto inputs = FakeSpendableInputs(0, 10, 10, &wtx);
EXPECT_EQ(inputs.Total(), 20);
// Create a set of inputs from Sprout and the available pools.
auto inputs = FakeSpendableInputs(available, true, &wtx);
EXPECT_EQ(inputs.Total(), 10 * (available.size() + 1));
// We have Sprout notes and UTXOs.
// We have Sprout notes along with the expected notes.
EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0);
EXPECT_EQ(inputs.saplingNoteEntries.size(), 0);
EXPECT_EQ(inputs.sproutNoteEntries.size(), 10);
EXPECT_EQ(inputs.utxos.size(), 10);
for (auto pool : available) {
switch (pool) {
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), 10); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 10); break;
}
}
// Limit to 14 zatoshis.
inputs.LimitToAmount(14, 1);
EXPECT_TRUE(inputs.LimitToAmount(14, 1, std::get<1>(GetParam())));
EXPECT_EQ(inputs.Total(), 14);
// We have all Sprout notes and some transparent notes.
// We have all Sprout notes and some from the first pool.
EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0);
EXPECT_EQ(inputs.saplingNoteEntries.size(), 0);
EXPECT_EQ(inputs.sproutNoteEntries.size(), 10);
EXPECT_EQ(inputs.utxos.size(), 4);
for (int i = 0; i < order.size(); i++) {
auto expected = i == 0 ? 4 : 0;
switch (order[i]) {
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), expected); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), expected); break;
}
}
}
TEST(NoteSelectionTest, SelectsSproutBeforeSapling)
{
// Create a set of inputs from Sapling and Sprout.
auto inputs = FakeSpendableInputs(10, 10);
EXPECT_EQ(inputs.Total(), 20);
// We have Sapling and Sprout notes.
EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0);
EXPECT_EQ(inputs.saplingNoteEntries.size(), 10);
EXPECT_EQ(inputs.sproutNoteEntries.size(), 10);
EXPECT_EQ(inputs.utxos.size(), 0);
// Limit to 7 zatoshis.
inputs.LimitToAmount(7, 1);
EXPECT_EQ(inputs.Total(), 7);
// We only have Sprout notes.
EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0);
EXPECT_EQ(inputs.saplingNoteEntries.size(), 0);
EXPECT_EQ(inputs.sproutNoteEntries.size(), 7);
EXPECT_EQ(inputs.utxos.size(), 0);
}
TEST(NoteSelectionTest, SelectsSproutThenSapling)
{
// Create a set of inputs from Sapling and Sprout.
auto inputs = FakeSpendableInputs(10, 10);
EXPECT_EQ(inputs.Total(), 20);
// We have Sapling and Sprout notes.
EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0);
EXPECT_EQ(inputs.saplingNoteEntries.size(), 10);
EXPECT_EQ(inputs.sproutNoteEntries.size(), 10);
EXPECT_EQ(inputs.utxos.size(), 0);
// Limit to 16 zatoshis.
inputs.LimitToAmount(16, 1);
EXPECT_EQ(inputs.Total(), 16);
// We have all Sprout notes and some Sapling notes.
EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0);
EXPECT_EQ(inputs.saplingNoteEntries.size(), 6);
EXPECT_EQ(inputs.sproutNoteEntries.size(), 10);
EXPECT_EQ(inputs.utxos.size(), 0);
}
TEST(NoteSelectionTest, SelectsTransparentBeforeSapling)
TEST_P(SpendableInputsTest, SelectsFirstBeforeSecond)
{
auto available = std::get<0>(GetParam());
auto recipientPools = std::get<1>(GetParam());
auto order = std::get<2>(GetParam());
auto wtx = FakeWalletTx();
// Create a set of inputs from Sapling and transparent pools.
auto inputs = FakeSpendableInputs(10, 0, 10, &wtx);
EXPECT_EQ(inputs.Total(), 20);
// Create a set of inputs from the available pools.
auto inputs = FakeSpendableInputs(available, false, &wtx);
EXPECT_EQ(inputs.Total(), 10 * available.size());
// We have Sapling notes and UTXOs.
// We have the expected notes.
EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0);
EXPECT_EQ(inputs.saplingNoteEntries.size(), 10);
EXPECT_EQ(inputs.sproutNoteEntries.size(), 0);
EXPECT_EQ(inputs.utxos.size(), 10);
for (auto pool : available) {
switch (pool) {
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), 10); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 10); break;
}
}
// Limit to 8 zatoshis.
inputs.LimitToAmount(8, 1);
EXPECT_TRUE(inputs.LimitToAmount(8, 1, std::get<1>(GetParam())));
EXPECT_EQ(inputs.Total(), 8);
// We only have UTXOs.
// We only have the first pool selected.
EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0);
EXPECT_EQ(inputs.saplingNoteEntries.size(), 0);
EXPECT_EQ(inputs.sproutNoteEntries.size(), 0);
EXPECT_EQ(inputs.utxos.size(), 8);
for (int i = 0; i < order.size(); i++) {
auto expected = i == 0 ? 8 : 0;
switch (order[i]) {
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), expected); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), expected); break;
}
}
}
TEST(NoteSelectionTest, SelectsTransparentThenSapling)
TEST_P(SpendableInputsTest, SelectsFirstThenSecond)
{
auto available = std::get<0>(GetParam());
auto recipientPools = std::get<1>(GetParam());
auto order = std::get<2>(GetParam());
auto wtx = FakeWalletTx();
// Create a set of inputs from Sapling and transparent pools.
auto inputs = FakeSpendableInputs(10, 0, 10, &wtx);
EXPECT_EQ(inputs.Total(), 20);
// Create a set of inputs from the available pools.
auto inputs = FakeSpendableInputs(available, false, &wtx);
EXPECT_EQ(inputs.Total(), 10 * available.size());
// We have Sapling notes and UTXOs.
// We have the expected notes.
EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0);
EXPECT_EQ(inputs.saplingNoteEntries.size(), 10);
EXPECT_EQ(inputs.sproutNoteEntries.size(), 0);
EXPECT_EQ(inputs.utxos.size(), 10);
for (auto pool : available) {
switch (pool) {
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), 10); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 10); break;
}
}
// Limit to 13 zatoshis.
inputs.LimitToAmount(13, 1);
EXPECT_EQ(inputs.Total(), 13);
// If we only have one pool available, we won't have sufficient funds.
auto sufficientFunds = inputs.LimitToAmount(13, 1, std::get<1>(GetParam()));
if (available.size() == 1) {
EXPECT_FALSE(sufficientFunds);
EXPECT_EQ(inputs.Total(), 10);
} else {
EXPECT_TRUE(sufficientFunds);
EXPECT_EQ(inputs.Total(), 13);
}
// We have all UTXOs and some Sapling notes.
// We have all of the first pool and (if present) some of the second.
EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0);
EXPECT_EQ(inputs.saplingNoteEntries.size(), 3);
EXPECT_EQ(inputs.sproutNoteEntries.size(), 0);
EXPECT_EQ(inputs.utxos.size(), 10);
for (int i = 0; i < order.size(); i++) {
auto expected = i == 0 ? 10 : i == 1 ? 3 : 0;
switch (order[i]) {
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), expected); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), expected); break;
}
}
}
TEST(NoteSelectionTest, SelectsSproutAndTransparentBeforeSapling)
TEST_P(SpendableInputsTest, SelectsSproutAndFirstThenSecond)
{
auto available = std::get<0>(GetParam());
auto recipientPools = std::get<1>(GetParam());
auto order = std::get<2>(GetParam());
auto wtx = FakeWalletTx();
// Create a set of inputs from Sapling and transparent pools.
auto inputs = FakeSpendableInputs(10, 10, 10, &wtx);
EXPECT_EQ(inputs.Total(), 30);
// Create a set of inputs from Sprout and the available pools.
auto inputs = FakeSpendableInputs(available, true, &wtx);
EXPECT_EQ(inputs.Total(), 10 * (available.size() + 1));
// We have Sapling notes and UTXOs.
// We have Sprout notes along with the expected notes.
EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0);
EXPECT_EQ(inputs.saplingNoteEntries.size(), 10);
EXPECT_EQ(inputs.sproutNoteEntries.size(), 10);
EXPECT_EQ(inputs.utxos.size(), 10);
// Limit to 12 zatoshis.
inputs.LimitToAmount(12, 1);
EXPECT_EQ(inputs.Total(), 12);
// We have all UTXOs and some Sapling notes.
EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0);
EXPECT_EQ(inputs.saplingNoteEntries.size(), 0);
EXPECT_EQ(inputs.sproutNoteEntries.size(), 10);
EXPECT_EQ(inputs.utxos.size(), 2);
}
TEST(NoteSelectionTest, SelectsSproutAndTransparentThenSapling)
{
auto wtx = FakeWalletTx();
// Create a set of inputs from Sapling and transparent pools.
auto inputs = FakeSpendableInputs(10, 10, 10, &wtx);
EXPECT_EQ(inputs.Total(), 30);
// We have Sapling notes and UTXOs.
EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0);
EXPECT_EQ(inputs.saplingNoteEntries.size(), 10);
EXPECT_EQ(inputs.sproutNoteEntries.size(), 10);
EXPECT_EQ(inputs.utxos.size(), 10);
for (auto pool : available) {
switch (pool) {
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), 10); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), 10); break;
}
}
// Limit to 24 zatoshis.
inputs.LimitToAmount(24, 1);
EXPECT_EQ(inputs.Total(), 24);
// If we only have one pool available, we won't have sufficient funds.
auto sufficientFunds = inputs.LimitToAmount(24, 1, std::get<1>(GetParam()));
if (available.size() == 1) {
EXPECT_FALSE(sufficientFunds);
EXPECT_EQ(inputs.Total(), 20);
} else {
EXPECT_TRUE(sufficientFunds);
EXPECT_EQ(inputs.Total(), 24);
}
// We have all UTXOs and Sprout notes, and some Sapling notes.
// We have all of Sprout and the first pool, and (if present) some of the second.
EXPECT_EQ(inputs.orchardNoteMetadata.size(), 0);
EXPECT_EQ(inputs.saplingNoteEntries.size(), 4);
EXPECT_EQ(inputs.sproutNoteEntries.size(), 10);
EXPECT_EQ(inputs.utxos.size(), 10);
for (int i = 0; i < order.size(); i++) {
auto expected = i == 0 ? 10 : i == 1 ? 4 : 0;
switch (order[i]) {
case OutputPool::Sapling: EXPECT_EQ(inputs.saplingNoteEntries.size(), expected); break;
case OutputPool::Transparent: EXPECT_EQ(inputs.utxos.size(), expected); break;
}
}
}
const std::set<OutputPool> SET_T({OutputPool::Transparent});
const std::set<OutputPool> SET_S({OutputPool::Sapling});
const std::set<OutputPool> SET_TS({OutputPool::Transparent, OutputPool::Sapling});
const std::vector<OutputPool> VEC_T({OutputPool::Transparent});
const std::vector<OutputPool> VEC_S({OutputPool::Sapling});
const std::vector<OutputPool> VEC_TS({OutputPool::Transparent, OutputPool::Sapling});
const std::vector<OutputPool> VEC_ST({OutputPool::Sapling, OutputPool::Transparent});
INSTANTIATE_TEST_CASE_P(
ExhaustiveCases,
SpendableInputsTest,
::testing::Values(
// Available | Recipients | Order // Rationale
// ----------|------------|----------//----------
std::make_tuple(SET_T, SET_T, VEC_T), // N/A
std::make_tuple(SET_T, SET_S, VEC_T), // N/A
std::make_tuple(SET_T, SET_TS, VEC_T), // N/A
std::make_tuple(SET_S, SET_T, VEC_S), // N/A
std::make_tuple(SET_S, SET_S, VEC_S), // N/A
std::make_tuple(SET_S, SET_TS, VEC_S), // N/A
std::make_tuple(SET_TS, SET_T, VEC_ST), // Hide sender if possible.
std::make_tuple(SET_TS, SET_S, VEC_TS), // Opportunistic shielding.
std::make_tuple(SET_TS, SET_TS, VEC_TS) // Opportunistic shielding.
)
);

View File

@ -7065,8 +7065,14 @@ bool ZTXOSelector::SelectsSapling() const {
}, this->pattern);
}
bool SpendableInputs::LimitToAmount(const CAmount amountRequired, const CAmount dustThreshold) {
bool SpendableInputs::LimitToAmount(
const CAmount amountRequired,
const CAmount dustThreshold,
std::set<OutputPool> recipientPools)
{
assert(amountRequired >= 0 && dustThreshold > 0);
// Calling this method twice is a programming error.
assert(!limited);
CAmount totalSelected{0};
auto haveSufficientFunds = [&]() {
@ -7088,36 +7094,94 @@ bool SpendableInputs::LimitToAmount(const CAmount amountRequired, const CAmount
}
sproutNoteEntries.erase(sproutIt, sproutNoteEntries.end());
// Next select transparent utxos. We preferentially spend transparent funds,
// with the intent that we'd like to opportunistically shield whatever is
// possible, and we will always shield change after the introduction of
// unified addresses.
std::sort(utxos.begin(), utxos.end(),
[](COutput i, COutput j) -> bool {
return i.Value() > j.Value();
});
auto utxoIt = utxos.begin();
while (utxoIt != utxos.end() && !haveSufficientFunds()) {
totalSelected += utxoIt->Value();
++utxoIt;
// Check what input pools we have available.
bool haveTransparent = !utxos.empty();
bool haveSapling = !saplingNoteEntries.empty();
std::set<OutputPool> available;
if (haveTransparent) {
available.insert(OutputPool::Transparent);
}
utxos.erase(utxoIt, utxos.end());
// Finally select Sapling outputs. After the introduction of Orchard to the
// wallet, the selection of Sapling and Orchard notes, and the
// determination of change amounts, should be done in a fashion that
// minimizes information leakage whenever possible.
std::sort(saplingNoteEntries.begin(), saplingNoteEntries.end(),
[](SaplingNoteEntry i, SaplingNoteEntry j) -> bool {
return i.note.value() > j.note.value();
});
auto saplingIt = saplingNoteEntries.begin();
while (saplingIt != saplingNoteEntries.end() && !haveSufficientFunds()) {
totalSelected += saplingIt->note.value();
++saplingIt;
if (haveSapling) {
available.insert(OutputPool::Sapling);
}
saplingNoteEntries.erase(saplingIt, saplingNoteEntries.end());
// Now determine the order in which to select the remaining notes and coins.
// We do this in a way that minimizes information leakage while moving funds
// into the shielded pool where possible.
//
// In the following table:
// - T: transparent pool
// - S: Sapling pool
//
// Available | Recipients | Order | Rationale
// ----------|------------|-------|----------
// T | T | T | N/A
// T | S | T | N/A
// T | TS | T | N/A
// S | T | S | N/A
// S | S | S | N/A
// S | TS | S | N/A
// TS | T | ST | Hide sender if possible.
// TS | S | TS | Opportunistic shielding.
// TS | TS | TS | Opportunistic shielding.
std::vector<OutputPool> selectionOrder;
if (available.size() <= 1) {
// We have at most one input pool, so we don't need selection logic.
selectionOrder.assign(available.begin(), available.end());
} else if (recipientPools.size() == 1 && recipientPools.count(OutputPool::Transparent)) {
// Hide sender if possible.
selectionOrder = {
OutputPool::Sapling,
OutputPool::Transparent,
};
} else {
// Opportunistic shielding.
selectionOrder = {
OutputPool::Transparent,
OutputPool::Sapling,
};
}
// Ensure we provided a total selection order (so that all unselected notes
// and coins are erased).
assert(selectionOrder.size() == available.size());
// Finally, select the remaining notes and coins based on this order.
for (auto pool : selectionOrder) {
switch (pool) {
case OutputPool::Transparent:
{
std::sort(utxos.begin(), utxos.end(),
[](COutput i, COutput j) -> bool {
return i.Value() > j.Value();
});
auto utxoIt = utxos.begin();
while (utxoIt != utxos.end() && !haveSufficientFunds()) {
totalSelected += utxoIt->Value();
++utxoIt;
}
utxos.erase(utxoIt, utxos.end());
break;
}
case OutputPool::Sapling:
{
std::sort(saplingNoteEntries.begin(), saplingNoteEntries.end(),
[](SaplingNoteEntry i, SaplingNoteEntry j) -> bool {
return i.note.value() > j.note.value();
});
auto saplingIt = saplingNoteEntries.begin();
while (saplingIt != saplingNoteEntries.end() && !haveSufficientFunds()) {
totalSelected += saplingIt->note.value();
++saplingIt;
}
saplingNoteEntries.erase(saplingIt, saplingNoteEntries.end());
break;
}
}
}
limited = true;
return haveSufficientFunds();
}

View File

@ -811,6 +811,9 @@ enum class OutputPool {
};
class SpendableInputs {
private:
bool limited = false;
public:
std::vector<COutput> utxos;
std::vector<SproutNoteEntry> sproutNoteEntries;
@ -821,8 +824,17 @@ public:
* Selectively discard notes that are not required to obtain the desired
* amount. Returns `false` if the available inputs do not add up to the
* desired amount.
*
* `recipientPools` is the set of `OutputPool`s to which the caller intends
* to send funds. This is used during note selection to minimise information
* leakage. The empty set is short-hand for "all pools".
*
* This method must only be called once.
*/
bool LimitToAmount(CAmount amount, CAmount dustThreshold);
bool LimitToAmount(
CAmount amount,
CAmount dustThreshold,
std::set<OutputPool> recipientPools);
/**
* Compute the total ZEC amount of spendable inputs.