Replace the badly-named `PaymentSource` with `ZTXOSelector`

The ZTXOSelector type allows selection of previous Zcash
transaction outputs (both transparent outputs and shielded notes)
on the basis of either a (legacy) bare address, or for a
BIP-44 account.
This commit is contained in:
Kris Nuttycombe 2022-01-12 13:07:10 -07:00
parent c4a7f5c95c
commit f850e89449
7 changed files with 156 additions and 106 deletions

View File

@ -86,7 +86,7 @@ class WalletShieldingCoinbaseTest (BitcoinTestFramework):
myopid = self.nodes[3].z_sendmany(mytaddr, recipients)
except JSONRPCException as e:
errorString = e.error['message']
assert_equal("Invalid from address, no spending key found for address", errorString);
assert_equal("Invalid from address, no payment source found for address.", errorString);
# This send will fail because our consensus does not allow transparent change when
# shielding a coinbase utxo.

View File

@ -45,13 +45,13 @@ using namespace libzcash;
AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany(
TransactionBuilder builder,
PaymentSource paymentSource,
ZTXOSelector ztxoSelector,
std::vector<SendManyRecipient> recipients,
int minDepth,
CAmount fee,
bool allowRevealedAmounts,
UniValue contextInfo) :
builder_(builder), paymentSource_(paymentSource), recipients_(recipients),
builder_(builder), ztxoSelector_(ztxoSelector), recipients_(recipients),
mindepth_(minDepth), fee_(fee), allowRevealedAmounts_(allowRevealedAmounts),
contextinfo_(contextInfo)
{
@ -60,8 +60,8 @@ AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany(
assert(!recipients_.empty());
std::visit(match {
[&](const FromAnyTaddr& any) {
isfromtaddr_ = true;
[&](const AccountZTXOSelector& acct) {
isfromtaddr_ = (acct.GetAccountId() == ZCASH_LEGACY_ACCOUNT);
},
[&](const PaymentAddress& addr) {
// We don't need to lock on the wallet as spending key related methods are thread-safe
@ -91,7 +91,7 @@ AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany(
}
}, addr);
}
}, paymentSource);
}, ztxoSelector);
if ((isfromsprout_ || isfromsapling_) && minDepth == 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Minconf cannot be zero when sending from a shielded address");
@ -197,15 +197,15 @@ void AsyncRPCOperation_sendmany::main() {
LogPrintf("%s",s);
}
bool IsFromAnyTaddr(const PaymentSource& paymentSource) {
bool IsFromAnyTaddr(const ZTXOSelector& ztxoSelector) {
return std::visit(match {
[&](const FromAnyTaddr& fromAny) {
return true;
[&](const AccountZTXOSelector& fa) {
return fa.GetAccountId() == ZCASH_LEGACY_ACCOUNT;
},
[&](const PaymentAddress& addr) {
[&](const auto& other) {
return false;
}
}, paymentSource);
}, ztxoSelector);
}
// Construct and send the transaction, returning the resulting txid.
@ -234,7 +234,7 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
// Only select coinbase if we are spending from at most a single t-address.
bool allowTransparentCoinbase =
!IsFromAnyTaddr(paymentSource_) && // allow coinbase inputs from at most a single t-addr
!IsFromAnyTaddr(ztxoSelector_) && // allow coinbase inputs from at most a single t-addr
transparentRecipients_ == 0; // cannot send transparent coinbase to transparent recipients
// Set the dust threshold so that we can select enough inputs to avoid
@ -243,7 +243,7 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
// Find spendable inputs, and select a minimal set of them that
// can supply the required target amount.
auto spendable = pwalletMain->FindSpendableInputs(paymentSource_, allowTransparentCoinbase, mindepth_);
auto spendable = pwalletMain->FindSpendableInputs(ztxoSelector_, allowTransparentCoinbase, mindepth_);
if (!spendable.LimitToAmount(targetAmount, dustThreshold)) {
CAmount changeAmount{spendable.Total() - targetAmount};
if (changeAmount > 0 && changeAmount < dustThreshold) {
@ -368,34 +368,26 @@ uint256 AsyncRPCOperation_sendmany::main_impl() {
builder_.SendChangeTo(changeAddr);
};
// FIXME: it would be better to use the most recent shielded pool change
// address for the wallet's default unified address account, and the
// FIXME: use the appropriate shielded pool change address for the
// source unified address account (or the legacy account), and the
// associated OVK
std::visit(match {
[&](const FromAnyTaddr& fromAny) {
[&](const libzcash::SproutPaymentAddress& addr) {
ovk = getDefaultOVK();
builder_.SendChangeTo(addr);
},
[&](const libzcash::SaplingPaymentAddress& addr) {
libzcash::SaplingExtendedSpendingKey saplingKey;
assert(pwalletMain->GetSaplingExtendedSpendingKey(addr, saplingKey));
ovk = saplingKey.expsk.full_viewing_key().ovk;
builder_.SendChangeTo(addr, ovk);
},
[&](const auto& other) {
ovk = getDefaultOVK();
setTransparentChangeRecipient();
},
[&](const PaymentAddress& addr) {
std::visit(match {
[&](const libzcash::SproutPaymentAddress& addr) {
ovk = getDefaultOVK();
builder_.SendChangeTo(addr);
},
[&](const libzcash::SaplingPaymentAddress& addr) {
libzcash::SaplingExtendedSpendingKey saplingKey;
assert(pwalletMain->GetSaplingExtendedSpendingKey(addr, saplingKey));
ovk = saplingKey.expsk.full_viewing_key().ovk;
builder_.SendChangeTo(addr, ovk);
},
[&](const auto& other) {
ovk = getDefaultOVK();
setTransparentChangeRecipient();
}
}, addr);
}
}, paymentSource_);
}, ztxoSelector_);
// Track the total of notes that we've added to the builder
CAmount sum = 0;

View File

@ -45,7 +45,7 @@ class AsyncRPCOperation_sendmany : public AsyncRPCOperation {
public:
AsyncRPCOperation_sendmany(
TransactionBuilder builder,
PaymentSource paymentSource,
ZTXOSelector ztxoSelector,
std::vector<SendManyRecipient> recipients,
int minDepth,
CAmount fee = DEFAULT_FEE,
@ -69,7 +69,7 @@ private:
friend class TEST_FRIEND_AsyncRPCOperation_sendmany; // class for unit testing
TransactionBuilder builder_;
PaymentSource paymentSource_;
ZTXOSelector ztxoSelector_;
std::vector<SendManyRecipient> recipients_;
int mindepth_{1};
CAmount fee_;

View File

@ -4156,7 +4156,7 @@ UniValue z_getoperationstatus_IMPL(const UniValue& params, bool fRemoveFinishedO
#define CTXOUT_REGULAR_SIZE 34
size_t EstimateTxSize(
const PaymentSource& paymentSource,
const ZTXOSelector& ztxoSelector,
const std::vector<SendManyRecipient>& recipients,
int nextBlockHeight) {
CMutableTransaction mtx;
@ -4165,31 +4165,23 @@ size_t EstimateTxSize(
mtx.nVersion = SAPLING_TX_VERSION;
bool fromTaddr = std::visit(match {
[&](const FromAnyTaddr& any) {
[&](const AccountZTXOSelector& acct) {
return acct.GetReceiverTypes().count(ReceiverType::P2PKH) > 0 ||
acct.GetReceiverTypes().count(ReceiverType::P2SH) > 0;
},
[&](const CKeyID& keyId) {
return true;
},
[&](const PaymentAddress& addr) {
return std::visit(match {
[&](const CKeyID& keyId) {
return true;
},
[&](const CScriptID& scriptId) {
return true;
},
[&](const libzcash::SproutPaymentAddress& addr) {
return false;
},
[&](const libzcash::SaplingPaymentAddress& addr) {
return false;
},
[&](const libzcash::UnifiedAddress& addr) {
// TODO UA
throw JSONRPCError(RPC_INVALID_PARAMETER, "Unified addresses not yet supported.");
return false; // compiler is dumb
}
}, addr);
[&](const CScriptID& scriptId) {
return true;
},
[&](const libzcash::SproutPaymentAddress& addr) {
return false;
},
[&](const libzcash::SaplingPaymentAddress& addr) {
return false;
}
}, paymentSource);
}, ztxoSelector);
// As a sanity check, estimate and verify that the size of the transaction will be valid.
// Depending on the input notes, the actual tx size may turn out to be larger and perhaps invalid.
@ -4283,25 +4275,25 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
// Check that the from address is valid.
// Unified address (UA) allowed here (#5185)
auto fromaddress = params[0].get_str();
PaymentSource paymentSource;
ZTXOSelector ztxoSelector;
if (fromaddress == "ANY_TADDR") {
paymentSource = FromAnyTaddr();
ztxoSelector = AccountZTXOSelector(ZCASH_LEGACY_ACCOUNT, {ReceiverType::P2PKH, ReceiverType::P2SH});
} else {
auto addr = keyIO.DecodePaymentAddress(fromaddress);
if (!addr.has_value()) {
auto decoded = keyIO.DecodePaymentAddress(fromaddress);
if (!decoded.has_value()) {
throw JSONRPCError(
RPC_INVALID_ADDRESS_OR_KEY,
"Invalid from address: should be a taddr, a zaddr, or the string 'ANY_TADDR'.");
}
// Unified addresses are not yet supported.
if (std::holds_alternative<libzcash::UnifiedAddress>(addr.value())) {
auto ztxoSelectorOpt = pwalletMain->ToZTXOSelector(decoded.value(), true);
if (!ztxoSelectorOpt.has_value()) {
throw JSONRPCError(
RPC_INVALID_ADDRESS_OR_KEY,
"Invalid from address: unified addresses are not yet supported.");
"Invalid from address, no payment source found for address.");
}
paymentSource = addr.value();
ztxoSelector = ztxoSelectorOpt.value();
}
UniValue outputs = params[1].get_array();
@ -4374,7 +4366,7 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
// Sanity check for transaction size
// TODO: move this to the builder?
auto txsize = EstimateTxSize(paymentSource, recipients, nextBlockHeight);
auto txsize = EstimateTxSize(ztxoSelector, recipients, nextBlockHeight);
if (txsize > MAX_TX_SIZE_AFTER_SAPLING) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
@ -4436,7 +4428,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(builder, paymentSource, recipients, nMinDepth, nFee, allowRevealedAmounts, contextInfo)
new AsyncRPCOperation_sendmany(builder, ztxoSelector, recipients, nMinDepth, nFee, allowRevealedAmounts, contextInfo)
);
q->addOperation(operation);
AsyncRPCOperationId operationId = operation->getId();

View File

@ -1191,19 +1191,11 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_parameters)
int nHeight = retValue.get_int();
TransactionBuilder builder(Params().GetConsensus(), nHeight + 1, pwalletMain);
try {
libzcash::UnifiedAddress ua; //dummy
std::vector<SendManyRecipient> recipients = { SendManyRecipient(ua, 1*COIN, std::nullopt) };
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(builder, ua, recipients, 1));
} catch (const UniValue& objError) {
BOOST_CHECK( find_error(objError, "Invalid from address"));
}
// Note: The following will crash as a google test because AsyncRPCOperation_sendmany
// invokes a method on pwalletMain, which is undefined in the google test environment.
try {
KeyIO keyIO(Params());
auto sender = keyIO.DecodePaymentAddress("ztjiDe569DPNbyTE6TSdJTaSDhoXEHLGvYoUnBU1wfVNU52TEyT6berYtySkd21njAeEoh8fFJUT42kua9r8EnhBaEKqCpP").value();
auto sender = std::get<libzcash::SproutPaymentAddress>(keyIO.DecodePaymentAddress("ztjiDe569DPNbyTE6TSdJTaSDhoXEHLGvYoUnBU1wfVNU52TEyT6berYtySkd21njAeEoh8fFJUT42kua9r8EnhBaEKqCpP").value());
libzcash::UnifiedAddress ua; //dummy
std::vector<SendManyRecipient> recipients = { SendManyRecipient(ua, 1*COIN, std::nullopt) };
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(builder, sender, recipients, 1));
@ -1242,7 +1234,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals)
// add keys manually
BOOST_CHECK_NO_THROW(retValue = CallRPC("getnewaddress"));
auto taddr1 = keyIO.DecodePaymentAddress(retValue.get_str()).value();
auto taddr1 = std::get<CKeyID>(keyIO.DecodePaymentAddress(retValue.get_str()).value());
if (!pwalletMain->HaveMnemonicSeed()) {
pwalletMain->GenerateNewSeed();

View File

@ -1359,34 +1359,65 @@ void CWallet::SyncMetaData(pair<typename TxSpendMap<T>::iterator, typename TxSpe
}
}
std::optional<ZTXOSelector> CWallet::ToZTXOSelector(const libzcash::PaymentAddress& addr, bool requireSpendingKey) const {
auto self = this;
std::optional<ZTXOSelector> result;
std::visit(match {
[&](const CKeyID& addr) {
if (!requireSpendingKey || self->HaveKey(addr)) {
result = addr;
}
},
[&](const CScriptID& addr) {
if (!requireSpendingKey || self->HaveCScript(addr)) {
result = addr;
}
},
[&](const libzcash::SaplingPaymentAddress& addr) {
if (!requireSpendingKey || self->HaveSaplingSpendingKeyForAddress(addr)) {
result = addr;
}
},
[&](const libzcash::SproutPaymentAddress& addr) {
if (!requireSpendingKey || self->HaveSproutSpendingKey(addr)) {
result = addr;
}
},
[&](const libzcash::UnifiedAddress& ua) {
// TODO: Find the unified account corresponding to this UA
}
}, addr);
return result;
}
SpendableInputs CWallet::FindSpendableInputs(
PaymentSource paymentSource,
ZTXOSelector selector,
bool allowTransparentCoinbase,
uint32_t minDepth) {
uint32_t minDepth) const {
SpendableInputs unspent;
auto filters = std::visit(match {
[&](const PaymentAddress& addr) {
return std::visit(match {
[&](const CKeyID& keyId) {
std::optional<std::set<CTxDestination>> t_filter = std::set<CTxDestination>({keyId});
return std::make_pair(t_filter, AddrSet::Empty());
},
[&](const CScriptID& scriptId) {
std::optional<std::set<CTxDestination>> t_filter = std::set<CTxDestination>({scriptId});
return std::make_pair(t_filter, AddrSet::Empty());
},
[&](const auto& other) {
std::optional<std::set<CTxDestination>> t_filter = std::nullopt;
return std::make_pair(t_filter, AddrSet::ForPaymentAddresses({addr}));
}
}, addr);
[&](const CKeyID& keyId) {
std::optional<std::set<CTxDestination>> t_filter = std::set<CTxDestination>({keyId});
return std::make_pair(t_filter, AddrSet::Empty());
},
[&](const FromAnyTaddr& taddr) {
[&](const CScriptID& scriptId) {
std::optional<std::set<CTxDestination>> t_filter = std::set<CTxDestination>({scriptId});
return std::make_pair(t_filter, AddrSet::Empty());
},
[&](const libzcash::SproutPaymentAddress& addr) {
std::optional<std::set<CTxDestination>> t_filter = std::nullopt;
return std::make_pair(t_filter, AddrSet::ForPaymentAddresses({addr}));
},
[&](const libzcash::SaplingPaymentAddress& addr) {
std::optional<std::set<CTxDestination>> t_filter = std::nullopt;
return std::make_pair(t_filter, AddrSet::ForPaymentAddresses({addr}));
},
[&](const AccountZTXOSelector& acct) {
std::optional<std::set<CTxDestination>> t_filter = std::set<CTxDestination>({});
return std::make_pair(t_filter, AddrSet::Empty());
}
}, paymentSource);
}, selector);
if (filters.first.has_value()) {
this->AvailableCoins(

View File

@ -686,13 +686,47 @@ public:
std::string ToString() const;
};
class FromAnyTaddr {
/**
* A class representing a BIP-44 account's spend authority, which can be used
* to choose prior Zcash transaction outputs, including both transparent UTXOs
* and shielded notes.
*
* If the set of receiver types provided is non-empty, only outputs for
* protocols corresponding to the provided set of receiver types may be used.
* If the set of receiver types is empty, no restrictions are placed upon what
* protocols outputs are selected for.
*/
class AccountZTXOSelector {
libzcash::AccountId accountId;
std::set<libzcash::ReceiverType> receiverTypes;
public:
friend bool operator==(const FromAnyTaddr &a, const FromAnyTaddr &b) { return true; }
AccountZTXOSelector(libzcash::AccountId accountIdIn, std::set<libzcash::ReceiverType> receiverTypesIn):
accountId(accountIdIn), receiverTypes(receiverTypesIn) {}
libzcash::AccountId GetAccountId() const {
return accountId;
}
const std::set<libzcash::ReceiverType>& GetReceiverTypes() const {
return receiverTypes;
}
friend bool operator==(const AccountZTXOSelector &a, const AccountZTXOSelector &b) {
return a.accountId == b.accountId && a.receiverTypes == b.receiverTypes;
}
};
/** A source from which the zcashd wallet can spend funds */
typedef std::variant<FromAnyTaddr, libzcash::PaymentAddress> PaymentSource;
/**
* A selector which can be used to choose prior Zcash transaction outputs,
* including both transparent UTXOs and shielded notes.
*/
typedef std::variant<
CKeyID,
CScriptID,
libzcash::SproutPaymentAddress,
libzcash::SaplingPaymentAddress,
AccountZTXOSelector> ZTXOSelector;
class SpendableInputs {
public:
@ -1144,10 +1178,19 @@ public:
*/
static bool SelectCoinsMinConf(const CAmount& nTargetValue, int nConfMine, int nConfTheirs, std::vector<COutput> vCoins, std::set<std::pair<const CWalletTx*,unsigned int> >& setCoinsRet, CAmount& nValueRet);
/**
* Obtain the ZTXO selector for the specified payment address. If the
* `requireSpendingKey` flag is set, this will only return a selector
* that will choose outputs for which this wallet holds the spending keys.
*/
std::optional<ZTXOSelector> ToZTXOSelector(
const libzcash::PaymentAddress& addr,
bool requireSpendingKey) const;
SpendableInputs FindSpendableInputs(
PaymentSource paymentSource,
ZTXOSelector paymentSource,
bool allowTransparentCoinbase,
uint32_t minDepth);
uint32_t minDepth) const;
bool IsSpent(const uint256& hash, unsigned int n) const;
bool IsSproutSpent(const uint256& nullifier) const;