Fix edge case revealed by #6409
Without `AllowRevealedRecipients`, we can’t send transparent change, but previously we asserted if we couldn’t get a transparent change address. Now it returns a new `TransparentChangeNotAllowed` failure, which is just a more specific `TransparentRecipientNotAllowed` to avoid confusion when there are no explicit unshielded recipients.
This commit is contained in:
parent
1c00591699
commit
2596b4920b
|
@ -105,7 +105,7 @@ class WalletShieldingCoinbaseTest (BitcoinTestFramework):
|
||||||
recipients = []
|
recipients = []
|
||||||
recipients.append({"address":myzaddr, "amount":Decimal('1.23456789')})
|
recipients.append({"address":myzaddr, "amount":Decimal('1.23456789')})
|
||||||
|
|
||||||
myopid = self.nodes[0].z_sendmany(mytaddr, recipients, 10, DEFAULT_FEE, 'AllowRevealedSenders')
|
myopid = self.nodes[0].z_sendmany(mytaddr, recipients, 10, DEFAULT_FEE, 'AllowFullyTransparent')
|
||||||
error_result = wait_and_assert_operationid_status_result(
|
error_result = wait_and_assert_operationid_status_result(
|
||||||
self.nodes[0],
|
self.nodes[0],
|
||||||
myopid, "failed",
|
myopid, "failed",
|
||||||
|
|
|
@ -494,5 +494,24 @@ class WalletZSendmanyTest(BitcoinTestFramework):
|
||||||
{'pools': {'orchard': {'valueZat': 200000000}}, 'minimum_confirmations': 1},
|
{'pools': {'orchard': {'valueZat': 200000000}}, 'minimum_confirmations': 1},
|
||||||
self.nodes[0].z_getbalanceforaccount(n0account0))
|
self.nodes[0].z_getbalanceforaccount(n0account0))
|
||||||
|
|
||||||
|
|
||||||
|
self.sync_all()
|
||||||
|
self.nodes[1].generate(1)
|
||||||
|
self.sync_all()
|
||||||
|
|
||||||
|
#
|
||||||
|
# Test transparent change
|
||||||
|
#
|
||||||
|
|
||||||
|
recipients = [{"address":n0ua1, "amount": 4}]
|
||||||
|
# Should fail because this generates transparent change, but we don’t have
|
||||||
|
# `AllowRevealedRecipients`
|
||||||
|
opid = self.nodes[2].z_sendmany(mytaddr, recipients, 1, 0, 'AllowRevealedSenders')
|
||||||
|
wait_and_assert_operationid_status(self.nodes[2], opid, 'failed', "This transaction would have transparent change, which is not enabled by default because it will publicly reveal the change address and amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowRevealedRecipients` or weaker if you wish to allow this transaction to proceed anyway.")
|
||||||
|
|
||||||
|
# Should succeed once we include `AllowRevealedRecipients`
|
||||||
|
opid = self.nodes[2].z_sendmany(mytaddr, recipients, 1, 0, 'AllowFullyTransparent')
|
||||||
|
wait_and_assert_operationid_status(self.nodes[2], opid)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
WalletZSendmanyTest().main()
|
WalletZSendmanyTest().main()
|
||||||
|
|
|
@ -62,6 +62,14 @@ void ThrowInputSelectionError(
|
||||||
"recipients and amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit "
|
"recipients and amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit "
|
||||||
"with the `privacyPolicy` parameter set to `AllowRevealedRecipients` "
|
"with the `privacyPolicy` parameter set to `AllowRevealedRecipients` "
|
||||||
"or weaker if you wish to allow this transaction to proceed anyway.");
|
"or weaker if you wish to allow this transaction to proceed anyway.");
|
||||||
|
case AddressResolutionError::TransparentChangeNotAllowed:
|
||||||
|
throw JSONRPCError(
|
||||||
|
RPC_INVALID_PARAMETER,
|
||||||
|
"This transaction would have transparent change, which is not "
|
||||||
|
"enabled by default because it will publicly reveal the change "
|
||||||
|
"address and amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit "
|
||||||
|
"with the `privacyPolicy` parameter set to `AllowRevealedRecipients` "
|
||||||
|
"or weaker if you wish to allow this transaction to proceed anyway.");
|
||||||
case AddressResolutionError::RevealingSaplingAmountNotAllowed:
|
case AddressResolutionError::RevealingSaplingAmountNotAllowed:
|
||||||
throw JSONRPCError(
|
throw JSONRPCError(
|
||||||
RPC_INVALID_PARAMETER,
|
RPC_INVALID_PARAMETER,
|
||||||
|
|
|
@ -13,7 +13,7 @@ int GetAnchorHeight(const CChain& chain, uint32_t anchorConfirmations)
|
||||||
return nextBlockHeight - anchorConfirmations;
|
return nextBlockHeight - anchorConfirmations;
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangeAddress
|
tl::expected<ChangeAddress, AddressResolutionError>
|
||||||
WalletTxBuilder::GetChangeAddress(
|
WalletTxBuilder::GetChangeAddress(
|
||||||
CWallet& wallet,
|
CWallet& wallet,
|
||||||
const ZTXOSelector& selector,
|
const ZTXOSelector& selector,
|
||||||
|
@ -55,15 +55,20 @@ WalletTxBuilder::GetChangeAddress(
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
auto changeAddressForTransparentSelector = [&](const std::set<ReceiverType>& receiverTypes) {
|
auto changeAddressForTransparentSelector = [&](const std::set<ReceiverType>& receiverTypes)
|
||||||
|
-> tl::expected<ChangeAddress, AddressResolutionError> {
|
||||||
auto addr = wallet.GenerateChangeAddressForAccount(
|
auto addr = wallet.GenerateChangeAddressForAccount(
|
||||||
sendFromAccount,
|
sendFromAccount,
|
||||||
getAllowedChangePools(receiverTypes));
|
getAllowedChangePools(receiverTypes));
|
||||||
assert(addr.has_value());
|
if (addr.has_value()) {
|
||||||
return addr.value();
|
return {addr.value()};
|
||||||
|
} else {
|
||||||
|
return tl::make_unexpected(AddressResolutionError::TransparentChangeNotAllowed);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
auto changeAddressForSaplingAddress = [&](const libzcash::SaplingPaymentAddress& addr) -> RecipientAddress {
|
auto changeAddressForSaplingAddress = [&](const libzcash::SaplingPaymentAddress& addr)
|
||||||
|
-> tl::expected<ChangeAddress, AddressResolutionError> {
|
||||||
// for Sapling, if using a legacy address, return change to the
|
// for Sapling, if using a legacy address, return change to the
|
||||||
// originating address; otherwise return it to the Sapling internal
|
// originating address; otherwise return it to the Sapling internal
|
||||||
// address corresponding to the UFVK.
|
// address corresponding to the UFVK.
|
||||||
|
@ -80,45 +85,48 @@ WalletTxBuilder::GetChangeAddress(
|
||||||
|
|
||||||
auto changeAddressForZUFVK = [&](
|
auto changeAddressForZUFVK = [&](
|
||||||
const ZcashdUnifiedFullViewingKey& zufvk,
|
const ZcashdUnifiedFullViewingKey& zufvk,
|
||||||
const std::set<ReceiverType>& receiverTypes) {
|
const std::set<ReceiverType>& receiverTypes)
|
||||||
|
-> tl::expected<ChangeAddress, AddressResolutionError> {
|
||||||
auto addr = zufvk.GetChangeAddress(getAllowedChangePools(receiverTypes));
|
auto addr = zufvk.GetChangeAddress(getAllowedChangePools(receiverTypes));
|
||||||
assert(addr.has_value());
|
assert(addr.has_value());
|
||||||
return addr.value();
|
return addr.value();
|
||||||
};
|
};
|
||||||
|
|
||||||
return examine(selector.GetPattern(), match {
|
return examine(selector.GetPattern(), match {
|
||||||
[&](const CKeyID&) -> ChangeAddress {
|
[&](const CKeyID&) {
|
||||||
return changeAddressForTransparentSelector({ReceiverType::P2PKH});
|
return changeAddressForTransparentSelector({ReceiverType::P2PKH});
|
||||||
},
|
},
|
||||||
[&](const CScriptID&) -> ChangeAddress {
|
[&](const CScriptID&) {
|
||||||
return changeAddressForTransparentSelector({ReceiverType::P2SH});
|
return changeAddressForTransparentSelector({ReceiverType::P2SH});
|
||||||
},
|
},
|
||||||
[](const libzcash::SproutPaymentAddress& addr) -> ChangeAddress {
|
[](const libzcash::SproutPaymentAddress& addr)
|
||||||
|
-> tl::expected<ChangeAddress, AddressResolutionError> {
|
||||||
// for Sprout, we return change to the originating address using the tx builder.
|
// for Sprout, we return change to the originating address using the tx builder.
|
||||||
return addr;
|
return addr;
|
||||||
},
|
},
|
||||||
[](const libzcash::SproutViewingKey& vk) -> ChangeAddress {
|
[](const libzcash::SproutViewingKey& vk)
|
||||||
|
-> tl::expected<ChangeAddress, AddressResolutionError> {
|
||||||
// for Sprout, we return change to the originating address using the tx builder.
|
// for Sprout, we return change to the originating address using the tx builder.
|
||||||
return vk.address();
|
return vk.address();
|
||||||
},
|
},
|
||||||
[&](const libzcash::SaplingPaymentAddress& addr) -> ChangeAddress {
|
[&](const libzcash::SaplingPaymentAddress& addr) {
|
||||||
return changeAddressForSaplingAddress(addr);
|
return changeAddressForSaplingAddress(addr);
|
||||||
},
|
},
|
||||||
[&](const libzcash::SaplingExtendedFullViewingKey& fvk) -> ChangeAddress {
|
[&](const libzcash::SaplingExtendedFullViewingKey& fvk) {
|
||||||
return changeAddressForSaplingAddress(fvk.DefaultAddress());
|
return changeAddressForSaplingAddress(fvk.DefaultAddress());
|
||||||
},
|
},
|
||||||
[&](const libzcash::UnifiedAddress& ua) -> ChangeAddress {
|
[&](const libzcash::UnifiedAddress& ua) {
|
||||||
auto zufvk = wallet.GetUFVKForAddress(ua);
|
auto zufvk = wallet.GetUFVKForAddress(ua);
|
||||||
assert(zufvk.has_value());
|
assert(zufvk.has_value());
|
||||||
return changeAddressForZUFVK(zufvk.value(), ua.GetKnownReceiverTypes());
|
return changeAddressForZUFVK(zufvk.value(), ua.GetKnownReceiverTypes());
|
||||||
},
|
},
|
||||||
[&](const libzcash::UnifiedFullViewingKey& fvk) -> ChangeAddress {
|
[&](const libzcash::UnifiedFullViewingKey& fvk) {
|
||||||
return changeAddressForZUFVK(
|
return changeAddressForZUFVK(
|
||||||
ZcashdUnifiedFullViewingKey::FromUnifiedFullViewingKey(params, fvk),
|
ZcashdUnifiedFullViewingKey::FromUnifiedFullViewingKey(params, fvk),
|
||||||
fvk.GetKnownReceiverTypes());
|
fvk.GetKnownReceiverTypes());
|
||||||
},
|
},
|
||||||
[&](const AccountZTXOPattern& acct) -> ChangeAddress {
|
[&](const AccountZTXOPattern& acct) -> tl::expected<ChangeAddress, AddressResolutionError> {
|
||||||
auto addr = wallet.GenerateChangeAddressForAccount(
|
auto addr = wallet.GenerateChangeAddressForAccount(
|
||||||
acct.GetAccountId(),
|
acct.GetAccountId(),
|
||||||
getAllowedChangePools(acct.GetReceiverTypes()));
|
getAllowedChangePools(acct.GetReceiverTypes()));
|
||||||
assert(addr.has_value());
|
assert(addr.has_value());
|
||||||
|
@ -297,11 +305,21 @@ InvalidFundsError ReportInvalidFunds(
|
||||||
: InvalidFundsReason(InsufficientFundsError(targetAmount))));
|
: InvalidFundsReason(InsufficientFundsError(targetAmount))));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static tl::expected<void, InputSelectionError>
|
||||||
AddChangePayment(Payments& resolvedPayments, const ChangeAddress& changeAddr, CAmount changeAmount)
|
AddChangePayment(
|
||||||
|
const SpendableInputs& spendable,
|
||||||
|
Payments& resolvedPayments,
|
||||||
|
const ChangeAddress& changeAddr,
|
||||||
|
CAmount changeAmount,
|
||||||
|
CAmount targetAmount)
|
||||||
{
|
{
|
||||||
assert(changeAmount > 0);
|
assert(changeAmount > 0);
|
||||||
|
|
||||||
|
// When spending transparent coinbase outputs, all inputs must be fully consumed.
|
||||||
|
if (spendable.HasTransparentCoinbase()) {
|
||||||
|
return tl::make_unexpected(ChangeNotAllowedError(spendable.Total(), targetAmount));
|
||||||
|
}
|
||||||
|
|
||||||
examine(changeAddr, match {
|
examine(changeAddr, match {
|
||||||
// TODO: Once we can add Sprout change to `resolvedPayments`, we don’t need to pass
|
// TODO: Once we can add Sprout change to `resolvedPayments`, we don’t need to pass
|
||||||
// `changeAddr` around the rest of these functions.
|
// `changeAddr` around the rest of these functions.
|
||||||
|
@ -312,6 +330,8 @@ AddChangePayment(Payments& resolvedPayments, const ChangeAddress& changeAddr, CA
|
||||||
ResolvedPayment(std::nullopt, sendTo, changeAmount, std::nullopt, true));
|
ResolvedPayment(std::nullopt, sendTo, changeAmount, std::nullopt, true));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// On the initial call, we haven’t yet selected inputs, so we assume the outputs dominate the
|
/// On the initial call, we haven’t yet selected inputs, so we assume the outputs dominate the
|
||||||
|
@ -344,11 +364,12 @@ WalletTxBuilder::IterateLimit(
|
||||||
CAmount bumpTargetAmount{0};
|
CAmount bumpTargetAmount{0};
|
||||||
std::optional<ChangeAddress> changeAddr;
|
std::optional<ChangeAddress> changeAddr;
|
||||||
CAmount changeAmount{0};
|
CAmount changeAmount{0};
|
||||||
|
CAmount targetAmount{0};
|
||||||
|
|
||||||
do {
|
do {
|
||||||
spendableMut = spendable;
|
spendableMut = spendable;
|
||||||
|
|
||||||
auto targetAmount{sendAmount + updatedFee};
|
targetAmount = sendAmount + updatedFee;
|
||||||
|
|
||||||
// TODO: the set of recipient pools is not quite sufficient information here; we should
|
// TODO: the set of recipient pools is not quite sufficient information here; we should
|
||||||
// probably perform note selection at the same time as we're performing resolved payment
|
// probably perform note selection at the same time as we're performing resolved payment
|
||||||
|
@ -364,13 +385,19 @@ WalletTxBuilder::IterateLimit(
|
||||||
// fresh) and once we generate it, hold onto it. But we still don’t have a guarantee
|
// fresh) and once we generate it, hold onto it. But we still don’t have a guarantee
|
||||||
// that we won’t end up discarding it.
|
// that we won’t end up discarding it.
|
||||||
if (changeAmount > 0 && !changeAddr.has_value()) {
|
if (changeAmount > 0 && !changeAddr.has_value()) {
|
||||||
changeAddr = GetChangeAddress(
|
auto possibleChangeAddr = GetChangeAddress(
|
||||||
wallet,
|
wallet,
|
||||||
selector,
|
selector,
|
||||||
spendableMut,
|
spendableMut,
|
||||||
resolved,
|
resolved,
|
||||||
strategy,
|
strategy,
|
||||||
afterNU5);
|
afterNU5);
|
||||||
|
|
||||||
|
if (possibleChangeAddr.has_value()) {
|
||||||
|
changeAddr = possibleChangeAddr.value();
|
||||||
|
} else {
|
||||||
|
return tl::make_unexpected(possibleChangeAddr.error());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
previousFee = updatedFee;
|
previousFee = updatedFee;
|
||||||
updatedFee = CalcZIP317Fee(
|
updatedFee = CalcZIP317Fee(
|
||||||
|
@ -399,7 +426,12 @@ WalletTxBuilder::IterateLimit(
|
||||||
|
|
||||||
if (changeAmount > 0) {
|
if (changeAmount > 0) {
|
||||||
assert(changeAddr.has_value());
|
assert(changeAddr.has_value());
|
||||||
AddChangePayment(resolved, changeAddr.value(), changeAmount);
|
|
||||||
|
auto changeRes =
|
||||||
|
AddChangePayment(spendableMut, resolved, changeAddr.value(), changeAmount, targetAmount);
|
||||||
|
if (!changeRes.has_value()) {
|
||||||
|
return tl::make_unexpected(changeRes.error());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return std::make_tuple(spendableMut, updatedFee, changeAddr);
|
return std::make_tuple(spendableMut, updatedFee, changeAddr);
|
||||||
|
@ -557,14 +589,24 @@ WalletTxBuilder::ResolveInputsAndPayments(
|
||||||
changeAmount));
|
changeAmount));
|
||||||
}
|
}
|
||||||
if (changeAmount > 0) {
|
if (changeAmount > 0) {
|
||||||
changeAddr = GetChangeAddress(
|
auto possibleChangeAddr = GetChangeAddress(
|
||||||
wallet,
|
wallet,
|
||||||
selector,
|
selector,
|
||||||
spendableMut,
|
spendableMut,
|
||||||
resolved,
|
resolved,
|
||||||
strategy,
|
strategy,
|
||||||
afterNU5);
|
afterNU5);
|
||||||
AddChangePayment(resolved, changeAddr.value(), changeAmount);
|
|
||||||
|
if (possibleChangeAddr.has_value()) {
|
||||||
|
changeAddr = possibleChangeAddr.value();
|
||||||
|
} else {
|
||||||
|
return tl::make_unexpected(possibleChangeAddr.error());
|
||||||
|
}
|
||||||
|
auto changeRes =
|
||||||
|
AddChangePayment(spendableMut, resolved, changeAddr.value(), changeAmount, targetAmount);
|
||||||
|
if (!changeRes.has_value()) {
|
||||||
|
return tl::make_unexpected(changeRes.error());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
auto limitResult = IterateLimit(wallet, selector, strategy, sendAmount, dustThreshold, spendableMut, resolved, afterNU5);
|
auto limitResult = IterateLimit(wallet, selector, strategy, sendAmount, dustThreshold, spendableMut, resolved, afterNU5);
|
||||||
|
@ -576,14 +618,9 @@ WalletTxBuilder::ResolveInputsAndPayments(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// When spending transparent coinbase outputs, all inputs must be fully
|
// When spending transparent coinbase outputs they may only be sent to shielded recipients.
|
||||||
// consumed, and they may only be sent to shielded recipients.
|
if (spendableMut.HasTransparentCoinbase() && resolved.HasTransparentRecipient()) {
|
||||||
if (spendableMut.HasTransparentCoinbase()) {
|
return tl::make_unexpected(AddressResolutionError::TransparentRecipientNotAllowed);
|
||||||
if (spendableMut.Total() != targetAmount) {
|
|
||||||
return tl::make_unexpected(ChangeNotAllowedError(spendableMut.Total(), targetAmount));
|
|
||||||
} else if (resolved.HasTransparentRecipient()) {
|
|
||||||
return tl::make_unexpected(AddressResolutionError::TransparentRecipientNotAllowed);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (spendableMut.orchardNoteMetadata.size() > this->maxOrchardActions) {
|
if (spendableMut.orchardNoteMetadata.size() > this->maxOrchardActions) {
|
||||||
|
|
|
@ -225,6 +225,8 @@ enum class AddressResolutionError {
|
||||||
SproutRecipientsNotSupported,
|
SproutRecipientsNotSupported,
|
||||||
//! Requested `PrivacyPolicy` doesn’t include `AllowRevealedRecipients`
|
//! Requested `PrivacyPolicy` doesn’t include `AllowRevealedRecipients`
|
||||||
TransparentRecipientNotAllowed,
|
TransparentRecipientNotAllowed,
|
||||||
|
//! Requested `PrivacyPolicy` doesn’t include `AllowRevealedRecipients`
|
||||||
|
TransparentChangeNotAllowed,
|
||||||
//! Requested `PrivacyPolicy` doesn’t include `AllowRevealedAmounts`, but we don’t have enough
|
//! Requested `PrivacyPolicy` doesn’t include `AllowRevealedAmounts`, but we don’t have enough
|
||||||
//! Sapling funds to avoid revealing amounts
|
//! Sapling funds to avoid revealing amounts
|
||||||
RevealingSaplingAmountNotAllowed,
|
RevealingSaplingAmountNotAllowed,
|
||||||
|
@ -338,7 +340,7 @@ private:
|
||||||
*/
|
*/
|
||||||
CAmount DefaultDustThreshold() const;
|
CAmount DefaultDustThreshold() const;
|
||||||
|
|
||||||
ChangeAddress
|
tl::expected<ChangeAddress, AddressResolutionError>
|
||||||
GetChangeAddress(
|
GetChangeAddress(
|
||||||
CWallet& wallet,
|
CWallet& wallet,
|
||||||
const ZTXOSelector& selector,
|
const ZTXOSelector& selector,
|
||||||
|
|
Loading…
Reference in New Issue