Add a wallet-aware transaction builder.
This factors several pieces out from asyncrpcoperation_sendmany to form the foundation of a new two-stage transaction construction process.
This commit is contained in:
parent
90e347905e
commit
78e76f1332
|
@ -327,6 +327,7 @@ BITCOIN_CORE_H = \
|
|||
wallet/asyncrpcoperation_saplingmigration.h \
|
||||
wallet/asyncrpcoperation_sendmany.h \
|
||||
wallet/asyncrpcoperation_shieldcoinbase.h \
|
||||
wallet/wallet_tx_builder.h \
|
||||
wallet/crypter.h \
|
||||
wallet/db.h \
|
||||
wallet/memo.h \
|
||||
|
@ -417,6 +418,7 @@ libbitcoin_wallet_a_SOURCES = \
|
|||
wallet/asyncrpcoperation_saplingmigration.cpp \
|
||||
wallet/asyncrpcoperation_sendmany.cpp \
|
||||
wallet/asyncrpcoperation_shieldcoinbase.cpp \
|
||||
wallet/wallet_tx_builder.cpp \
|
||||
wallet/crypter.cpp \
|
||||
wallet/db.cpp \
|
||||
wallet/orchard.cpp \
|
||||
|
|
|
@ -0,0 +1,682 @@
|
|||
// Copyright (c) 2022 The Zcash developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or https://www.opensource.org/licenses/mit-license.php .
|
||||
|
||||
#include "wallet/wallet_tx_builder.h"
|
||||
|
||||
using namespace libzcash;
|
||||
|
||||
PrepareTransactionResult WalletTxBuilder::PrepareTransaction(
|
||||
const ZTXOSelector& selector,
|
||||
SpendableInputs& spendable,
|
||||
const std::vector<Payment>& payments,
|
||||
TransactionStrategy strategy,
|
||||
CAmount fee,
|
||||
uint32_t anchorConfirmations) const
|
||||
{
|
||||
assert(!payments.empty());
|
||||
assert(selector.RequireSpendingKeys());
|
||||
assert(fee >= 0);
|
||||
|
||||
auto selected = ResolveInputsAndPayments(spendable, payments, strategy, fee);
|
||||
if (std::holds_alternative<InputSelectionError>(selected)) {
|
||||
return std::get<InputSelectionError>(selected);
|
||||
}
|
||||
|
||||
auto resolvedPayments = std::get<std::pair<SpendableInputs, Payments>>(selected).second;
|
||||
|
||||
auto sendFromAccount = wallet.FindAccountForSelector(selector).value_or(ZCASH_LEGACY_ACCOUNT);
|
||||
auto allowedChangeTypes = [&](const std::set<ReceiverType>& receiverTypes) -> std::set<OutputPool> {
|
||||
std::set<OutputPool> result{resolvedPayments.GetRecipientPools()};
|
||||
if (sendFromAccount != ZCASH_LEGACY_ACCOUNT) {
|
||||
result.insert(OutputPool::Sapling);
|
||||
}
|
||||
for (ReceiverType rtype : receiverTypes) {
|
||||
switch (rtype) {
|
||||
case ReceiverType::P2PKH:
|
||||
case ReceiverType::P2SH:
|
||||
// TODO: This is the correct policy, but it’s a breaking change from previous
|
||||
// behavior, so enable it separately.
|
||||
// if ((spendable.utxos.empty() && strategy.AllowRevealedRecipients())
|
||||
// || strategy.AllowFullyTransparent()) {
|
||||
if (!spendable.utxos.empty()) {
|
||||
result.insert(OutputPool::Transparent);
|
||||
}
|
||||
break;
|
||||
case ReceiverType::Sapling:
|
||||
if (!spendable.saplingNoteEntries.empty() || strategy.AllowRevealedAmounts()) {
|
||||
result.insert(OutputPool::Sapling);
|
||||
}
|
||||
break;
|
||||
case ReceiverType::Orchard:
|
||||
if (!spendable.orchardNoteMetadata.empty() || strategy.AllowRevealedAmounts()) {
|
||||
result.insert(OutputPool::Orchard);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
std::optional<ChangeAddress> changeAddr;
|
||||
std::visit(match {
|
||||
[&](const CKeyID& keyId) {
|
||||
changeAddr = pwalletMain->GenerateChangeAddressForAccount(
|
||||
sendFromAccount,
|
||||
allowedChangeTypes({ReceiverType::P2PKH}));
|
||||
},
|
||||
[&](const CScriptID& scriptId) {
|
||||
changeAddr = pwalletMain->GenerateChangeAddressForAccount(
|
||||
sendFromAccount,
|
||||
allowedChangeTypes({ReceiverType::P2SH}));
|
||||
},
|
||||
[&](const libzcash::SproutPaymentAddress& addr) {
|
||||
// for Sprout, we return change to the originating address.
|
||||
changeAddr = addr;
|
||||
},
|
||||
[&](const libzcash::SproutViewingKey& vk) {
|
||||
// for Sprout, we return change to the originating address.
|
||||
changeAddr = vk.address();
|
||||
},
|
||||
[&](const libzcash::SaplingPaymentAddress& addr) {
|
||||
// for Sapling, if using a legacy address, return change to the
|
||||
// originating address; otherwise return it to the Sapling internal
|
||||
// address corresponding to the UFVK.
|
||||
if (sendFromAccount == ZCASH_LEGACY_ACCOUNT) {
|
||||
changeAddr = addr;
|
||||
} else {
|
||||
changeAddr = pwalletMain->GenerateChangeAddressForAccount(
|
||||
sendFromAccount,
|
||||
allowedChangeTypes({ReceiverType::Sapling}));
|
||||
}
|
||||
},
|
||||
[&](const libzcash::SaplingExtendedFullViewingKey& fvk) {
|
||||
// for Sapling, if using a legacy address, return change to the
|
||||
// originating address; otherwise return it to the Sapling internal
|
||||
// address corresponding to the UFVK.
|
||||
if (sendFromAccount == ZCASH_LEGACY_ACCOUNT) {
|
||||
changeAddr = fvk.DefaultAddress();
|
||||
} else {
|
||||
changeAddr = pwalletMain->GenerateChangeAddressForAccount(
|
||||
sendFromAccount,
|
||||
allowedChangeTypes({ReceiverType::Sapling}));
|
||||
}
|
||||
},
|
||||
[&](const libzcash::UnifiedAddress& ua) {
|
||||
auto zufvk = pwalletMain->GetUFVKForAddress(ua);
|
||||
if (zufvk.has_value()) {
|
||||
changeAddr = zufvk.value().GetChangeAddress(
|
||||
allowedChangeTypes(ua.GetKnownReceiverTypes()));
|
||||
}
|
||||
},
|
||||
[&](const libzcash::UnifiedFullViewingKey& fvk) {
|
||||
auto zufvk = ZcashdUnifiedFullViewingKey::FromUnifiedFullViewingKey(Params(), fvk);
|
||||
changeAddr = zufvk.GetChangeAddress(
|
||||
allowedChangeTypes(fvk.GetKnownReceiverTypes()));
|
||||
},
|
||||
[&](const AccountZTXOPattern& acct) {
|
||||
changeAddr = pwalletMain->GenerateChangeAddressForAccount(
|
||||
acct.GetAccountId(),
|
||||
allowedChangeTypes(acct.GetReceiverTypes()));
|
||||
}
|
||||
}, selector.GetPattern());
|
||||
if (!changeAddr.has_value()) {
|
||||
return AddressResolutionError::ChangeAddressSelectionError;
|
||||
}
|
||||
|
||||
auto ovks = SelectOVKs(selector, spendable);
|
||||
|
||||
return TransactionEffects(
|
||||
sendFromAccount,
|
||||
anchorConfirmations,
|
||||
spendable,
|
||||
resolvedPayments,
|
||||
changeAddr.value(),
|
||||
fee,
|
||||
ovks.first,
|
||||
ovks.second);
|
||||
}
|
||||
|
||||
CAmount WalletTxBuilder::DefaultDustThreshold() const {
|
||||
CKey secret{CKey::TestOnlyRandomKey(true)};
|
||||
CScript scriptPubKey = GetScriptForDestination(secret.GetPubKey().GetID());
|
||||
CTxOut txout(CAmount(1), scriptPubKey);
|
||||
return txout.GetDustThreshold(minRelayFee);
|
||||
}
|
||||
|
||||
SpendableInputs WalletTxBuilder::FindAllSpendableInputs(
|
||||
const ZTXOSelector& selector,
|
||||
bool allowTransparentCoinbase,
|
||||
int32_t minDepth) const
|
||||
{
|
||||
return wallet.FindSpendableInputs(selector, allowTransparentCoinbase, minDepth, std::nullopt);
|
||||
}
|
||||
|
||||
bool WalletTxBuilder::AllowTransparentCoinbase(
|
||||
const std::vector<Payment>& payments,
|
||||
TransactionStrategy strategy)
|
||||
{
|
||||
bool allowed = strategy.AllowRevealedSenders();
|
||||
for (const auto& payment : payments) {
|
||||
if (!allowed) break;
|
||||
allowed &= std::visit(match {
|
||||
[](const CKeyID& p2pkh) { return false; },
|
||||
[](const CScriptID& p2sh) { return false; },
|
||||
[](const SproutPaymentAddress& addr) { return false; },
|
||||
[](const SaplingPaymentAddress& addr) { return true; },
|
||||
[](const UnifiedAddress& ua) {
|
||||
return ua.GetSaplingReceiver().has_value() || ua.GetOrchardReceiver().has_value();
|
||||
}
|
||||
}, payment.GetAddress());
|
||||
}
|
||||
return allowed;
|
||||
}
|
||||
|
||||
InputSelectionResult WalletTxBuilder::ResolveInputsAndPayments(
|
||||
SpendableInputs& spendableMut,
|
||||
const std::vector<Payment>& payments,
|
||||
TransactionStrategy strategy,
|
||||
CAmount fee) const
|
||||
{
|
||||
LOCK2(cs_main, wallet.cs_wallet);
|
||||
|
||||
bool allowTransparentCoinbase{true};
|
||||
|
||||
// Determine the target totals and recipient pools
|
||||
CAmount sendAmount{0};
|
||||
for (const auto& payment : payments) {
|
||||
std::visit(match {
|
||||
[&](const CKeyID& p2pkh) { allowTransparentCoinbase = strategy.AllowRevealedSenders(); },
|
||||
[&](const CScriptID& p2sh) { allowTransparentCoinbase = strategy.AllowRevealedSenders(); },
|
||||
[&](const SproutPaymentAddress& addr) { },
|
||||
[&](const SaplingPaymentAddress& addr) { },
|
||||
[&](const UnifiedAddress& ua) { }
|
||||
}, payment.GetAddress());
|
||||
sendAmount += payment.GetAmount();
|
||||
}
|
||||
CAmount targetAmount = sendAmount + fee;
|
||||
|
||||
// This is a simple greedy algorithm to attempt to preserve requested
|
||||
// transactional privacy while moving as much value to the most recent pool
|
||||
// as possible. This will also perform opportunistic shielding if the
|
||||
// transaction strategy permits.
|
||||
|
||||
CAmount maxSaplingAvailable = spendableMut.GetSaplingBalance();
|
||||
CAmount maxOrchardAvailable = spendableMut.GetOrchardBalance();
|
||||
uint32_t orchardOutputs{0};
|
||||
|
||||
// we can only select Orchard addresses if there are sufficient non-Sprout
|
||||
// funds to cover the total payments + fee.
|
||||
bool canResolveOrchard = spendableMut.Total() - spendableMut.GetSproutBalance() >= targetAmount;
|
||||
std::vector<ResolvedPayment> resolvedPayments;
|
||||
std::optional<AddressResolutionError> resolutionError;
|
||||
for (const auto& payment : payments) {
|
||||
std::visit(match {
|
||||
[&](const CKeyID& p2pkh) {
|
||||
if (strategy.AllowRevealedRecipients()) {
|
||||
resolvedPayments.emplace_back(std::nullopt, p2pkh, payment.GetAmount(), payment.GetMemo());
|
||||
} else {
|
||||
resolutionError = AddressResolutionError::TransparentRecipientNotPermitted;
|
||||
}
|
||||
},
|
||||
[&](const CScriptID& p2sh) {
|
||||
if (strategy.AllowRevealedRecipients()) {
|
||||
resolvedPayments.emplace_back(std::nullopt, p2sh, payment.GetAmount(), payment.GetMemo());
|
||||
} else {
|
||||
resolutionError = AddressResolutionError::TransparentRecipientNotPermitted;
|
||||
}
|
||||
},
|
||||
[&](const SproutPaymentAddress& addr) {
|
||||
resolutionError = AddressResolutionError::SproutRecipientNotPermitted;
|
||||
},
|
||||
[&](const SaplingPaymentAddress& addr) {
|
||||
if (strategy.AllowRevealedAmounts() || payment.GetAmount() < maxSaplingAvailable) {
|
||||
resolvedPayments.emplace_back(std::nullopt, addr, payment.GetAmount(), payment.GetMemo());
|
||||
if (!strategy.AllowRevealedAmounts()) {
|
||||
maxSaplingAvailable -= payment.GetAmount();
|
||||
}
|
||||
} else {
|
||||
resolutionError = AddressResolutionError::InsufficientSaplingFunds;
|
||||
}
|
||||
},
|
||||
[&](const UnifiedAddress& ua) {
|
||||
bool resolved{false};
|
||||
if (canResolveOrchard && ua.GetOrchardReceiver().has_value()) {
|
||||
if (strategy.AllowRevealedAmounts() || payment.GetAmount() < maxOrchardAvailable) {
|
||||
resolvedPayments.emplace_back(
|
||||
ua, ua.GetOrchardReceiver().value(), payment.GetAmount(), payment.GetMemo());
|
||||
if (!strategy.AllowRevealedAmounts()) {
|
||||
maxOrchardAvailable -= payment.GetAmount();
|
||||
}
|
||||
orchardOutputs += 1;
|
||||
resolved = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolved && ua.GetSaplingReceiver().has_value()) {
|
||||
if (strategy.AllowRevealedAmounts() || payment.GetAmount() < maxSaplingAvailable) {
|
||||
resolvedPayments.emplace_back(
|
||||
ua, ua.GetSaplingReceiver().value(), payment.GetAmount(), payment.GetMemo());
|
||||
if (!strategy.AllowRevealedAmounts()) {
|
||||
maxSaplingAvailable -= payment.GetAmount();
|
||||
}
|
||||
resolved = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolved && ua.GetP2SHReceiver().has_value() && strategy.AllowRevealedRecipients()) {
|
||||
resolvedPayments.emplace_back(
|
||||
ua, ua.GetP2SHReceiver().value(), payment.GetAmount(), std::nullopt);
|
||||
resolved = true;
|
||||
}
|
||||
|
||||
if (!resolved && ua.GetP2PKHReceiver().has_value() && strategy.AllowRevealedRecipients()) {
|
||||
resolvedPayments.emplace_back(
|
||||
ua, ua.GetP2PKHReceiver().value(), payment.GetAmount(), std::nullopt);
|
||||
resolved = true;
|
||||
}
|
||||
|
||||
if (!resolved) {
|
||||
resolutionError = AddressResolutionError::UnifiedAddressResolutionError;
|
||||
}
|
||||
}
|
||||
}, payment.GetAddress());
|
||||
|
||||
if (resolutionError.has_value()) {
|
||||
return resolutionError.value();
|
||||
}
|
||||
}
|
||||
auto resolved = Payments(resolvedPayments);
|
||||
|
||||
if (spendableMut.HasTransparentCoinbase() && resolved.HasTransparentRecipient()) {
|
||||
return AddressResolutionError::TransparentRecipientNotPermitted;
|
||||
}
|
||||
|
||||
if (orchardOutputs > this->maxOrchardActions) {
|
||||
return ExcessOrchardActionsError(spendableMut.orchardNoteMetadata.size());
|
||||
}
|
||||
|
||||
// Set the dust threshold so that we can select enough inputs to avoid
|
||||
// creating dust change amounts.
|
||||
CAmount dustThreshold{this->DefaultDustThreshold()};
|
||||
|
||||
// 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
|
||||
// construction above.
|
||||
if (!spendableMut.LimitToAmount(targetAmount, dustThreshold, resolved.GetRecipientPools())) {
|
||||
CAmount changeAmount{spendableMut.Total() - targetAmount};
|
||||
if (changeAmount > 0 && changeAmount < dustThreshold) {
|
||||
// TODO: we should provide the option for the caller to explicitly
|
||||
// forego change (definitionally an amount below the dust amount)
|
||||
// and send the extra to the recipient or the miner fee to avoid
|
||||
// creating dust change, rather than prohibit them from sending
|
||||
// entirely in this circumstance.
|
||||
// (Daira disagrees, as this could leak information to the recipient)
|
||||
return DustThresholdError(dustThreshold, spendableMut.Total(), changeAmount);
|
||||
} else {
|
||||
return InsufficientFundsError(spendableMut.Total(), targetAmount, allowTransparentCoinbase);
|
||||
}
|
||||
}
|
||||
|
||||
// When spending transparent coinbase outputs, all inputs must be fully
|
||||
// consumed, and they may only be sent to shielded recipients.
|
||||
if (spendableMut.HasTransparentCoinbase() && spendableMut.Total() != targetAmount) {
|
||||
return ChangeNotAllowedError(spendableMut.Total(), targetAmount);
|
||||
}
|
||||
|
||||
if (spendableMut.orchardNoteMetadata.size() > this->maxOrchardActions) {
|
||||
return ExcessOrchardActionsError(spendableMut.orchardNoteMetadata.size());
|
||||
}
|
||||
|
||||
return std::make_pair(spendableMut, resolved);
|
||||
}
|
||||
|
||||
std::pair<uint256, uint256> WalletTxBuilder::SelectOVKs(
|
||||
const ZTXOSelector& selector,
|
||||
const SpendableInputs& spendable) const
|
||||
{
|
||||
uint256 internalOVK;
|
||||
uint256 externalOVK;
|
||||
if (!spendable.orchardNoteMetadata.empty()) {
|
||||
std::optional<OrchardFullViewingKey> fvk;
|
||||
std::visit(match {
|
||||
[&](const UnifiedAddress& ua) {
|
||||
auto ufvk = wallet.GetUFVKForAddress(ua);
|
||||
// This is safe because spending key checks will have ensured that we
|
||||
// have a UFVK corresponding to this address, and Orchard notes will
|
||||
// not have been selected if the UFVK does not contain an Orchard key.
|
||||
fvk = ufvk.value().GetOrchardKey().value();
|
||||
},
|
||||
[&](const UnifiedFullViewingKey& ufvk) {
|
||||
// Orchard notes will not have been selected if the UFVK does not contain
|
||||
// an Orchard key.
|
||||
fvk = ufvk.GetOrchardKey().value();
|
||||
},
|
||||
[&](const AccountZTXOPattern& acct) {
|
||||
// By definition, we have a UFVK for every known account.
|
||||
auto ufvk = wallet.GetUnifiedFullViewingKeyByAccount(acct.GetAccountId());
|
||||
// Orchard notes will not have been selected if the UFVK does not contain
|
||||
// an Orchard key.
|
||||
fvk = ufvk.value().GetOrchardKey().value();
|
||||
},
|
||||
[&](const auto& other) {
|
||||
throw std::runtime_error("SelectOVKs: Selector cannot select Orchard notes.");
|
||||
}
|
||||
}, selector.GetPattern());
|
||||
assert(fvk.has_value());
|
||||
|
||||
internalOVK = fvk.value().ToInternalOutgoingViewingKey();
|
||||
externalOVK = fvk.value().ToExternalOutgoingViewingKey();
|
||||
} else if (!spendable.saplingNoteEntries.empty()) {
|
||||
std::optional<SaplingDiversifiableFullViewingKey> dfvk;
|
||||
std::visit(match {
|
||||
[&](const libzcash::SaplingPaymentAddress& addr) {
|
||||
libzcash::SaplingExtendedSpendingKey extsk;
|
||||
assert(pwalletMain->GetSaplingExtendedSpendingKey(addr, extsk));
|
||||
dfvk = extsk.ToXFVK();
|
||||
},
|
||||
[&](const UnifiedAddress& ua) {
|
||||
auto ufvk = pwalletMain->GetUFVKForAddress(ua);
|
||||
// This is safe because spending key checks will have ensured that we
|
||||
// have a UFVK corresponding to this address, and Sapling notes will
|
||||
// not have been selected if the UFVK does not contain a Sapling key.
|
||||
dfvk = ufvk.value().GetSaplingKey().value();
|
||||
},
|
||||
[&](const UnifiedFullViewingKey& ufvk) {
|
||||
// Sapling notes will not have been selected if the UFVK does not contain
|
||||
// a Sapling key.
|
||||
dfvk = ufvk.GetSaplingKey().value();
|
||||
},
|
||||
[&](const AccountZTXOPattern& acct) {
|
||||
// By definition, we have a UFVK for every known account.
|
||||
auto ufvk = pwalletMain->GetUnifiedFullViewingKeyByAccount(acct.GetAccountId());
|
||||
// Sapling notes will not have been selected if the UFVK does not contain
|
||||
// a Sapling key.
|
||||
dfvk = ufvk.value().GetSaplingKey().value();
|
||||
},
|
||||
[&](const auto& other) {
|
||||
throw std::runtime_error("SelectOVKs: Selector cannot select Sapling notes.");
|
||||
}
|
||||
}, selector.GetPattern());
|
||||
assert(dfvk.has_value());
|
||||
|
||||
auto ovks = dfvk.value().GetOVKs();
|
||||
internalOVK = ovks.first;
|
||||
externalOVK = ovks.second;
|
||||
} else if (!spendable.utxos.empty()) {
|
||||
std::optional<transparent::AccountPubKey> tfvk;
|
||||
std::visit(match {
|
||||
[&](const CKeyID& keyId) {
|
||||
tfvk = pwalletMain->GetLegacyAccountKey().ToAccountPubKey();
|
||||
},
|
||||
[&](const CScriptID& keyId) {
|
||||
tfvk = pwalletMain->GetLegacyAccountKey().ToAccountPubKey();
|
||||
},
|
||||
[&](const UnifiedAddress& ua) {
|
||||
// This is safe because spending key checks will have ensured that we
|
||||
// have a UFVK corresponding to this address, and transparent UTXOs will
|
||||
// not have been selected if the UFVK does not contain a transparent key.
|
||||
auto ufvk = pwalletMain->GetUFVKForAddress(ua);
|
||||
tfvk = ufvk.value().GetTransparentKey().value();
|
||||
},
|
||||
[&](const UnifiedFullViewingKey& ufvk) {
|
||||
// Transparent UTXOs will not have been selected if the UFVK does not contain
|
||||
// a transparent key.
|
||||
tfvk = ufvk.GetTransparentKey().value();
|
||||
},
|
||||
[&](const AccountZTXOPattern& acct) {
|
||||
if (acct.GetAccountId() == ZCASH_LEGACY_ACCOUNT) {
|
||||
tfvk = pwalletMain->GetLegacyAccountKey().ToAccountPubKey();
|
||||
} else {
|
||||
// By definition, we have a UFVK for every known account.
|
||||
auto ufvk = pwalletMain->GetUnifiedFullViewingKeyByAccount(acct.GetAccountId()).value();
|
||||
// Transparent UTXOs will not have been selected if the UFVK does not contain
|
||||
// a transparent key.
|
||||
tfvk = ufvk.GetTransparentKey().value();
|
||||
}
|
||||
},
|
||||
[&](const auto& other) {
|
||||
throw std::runtime_error("SelectOVKs: Selector cannot select transparent UTXOs.");
|
||||
}
|
||||
}, selector.GetPattern());
|
||||
assert(tfvk.has_value());
|
||||
|
||||
auto ovks = tfvk.value().GetOVKsForShielding();
|
||||
internalOVK = ovks.first;
|
||||
externalOVK = ovks.second;
|
||||
} else if (!spendable.sproutNoteEntries.empty()) {
|
||||
// use the legacy transparent account OVKs when sending from Sprout
|
||||
auto tfvk = pwalletMain->GetLegacyAccountKey().ToAccountPubKey();
|
||||
auto ovks = tfvk.GetOVKsForShielding();
|
||||
internalOVK = ovks.first;
|
||||
externalOVK = ovks.second;
|
||||
} else {
|
||||
// This should be unreachable; it is left in place as a guard to ensure
|
||||
// that when new input types are added to SpendableInputs in the future
|
||||
// that we do not accidentally return the all-zeros OVK.
|
||||
throw std::runtime_error("No spendable inputs.");
|
||||
}
|
||||
|
||||
return std::make_pair(internalOVK, externalOVK);
|
||||
}
|
||||
|
||||
PrivacyPolicy TransactionEffects::GetRequiredPrivacyPolicy() const
|
||||
{
|
||||
PrivacyPolicy maxPrivacy = PrivacyPolicy::FullPrivacy;
|
||||
|
||||
if (!spendable.orchardNoteMetadata.empty() && payments.HasSaplingRecipient()) {
|
||||
maxPrivacy = PrivacyPolicy::AllowRevealedAmounts;
|
||||
}
|
||||
|
||||
if (!spendable.saplingNoteEntries.empty() && payments.HasOrchardRecipient()) {
|
||||
maxPrivacy = PrivacyPolicy::AllowRevealedAmounts;
|
||||
}
|
||||
|
||||
if (!spendable.sproutNoteEntries.empty() && payments.HasSaplingRecipient()) {
|
||||
maxPrivacy = PrivacyPolicy::AllowRevealedAmounts;
|
||||
}
|
||||
|
||||
bool hasTransparentSource = !spendable.utxos.empty();
|
||||
if (payments.HasTransparentRecipient()) {
|
||||
if (hasTransparentSource) {
|
||||
// TODO: This is the correct policy, but it’s a breaking change from previous behavior,
|
||||
// so enable it separately.
|
||||
// maxPrivacy = PrivacyPolicy::AllowFullyTransparent;
|
||||
} else {
|
||||
maxPrivacy = PrivacyPolicy::AllowRevealedRecipients;
|
||||
}
|
||||
} else if (hasTransparentSource) {
|
||||
maxPrivacy = PrivacyPolicy::AllowRevealedSenders;
|
||||
}
|
||||
|
||||
// TODO: Check for conditions where PrivacyPolicy::AllowLinkingAccountAccesses
|
||||
// or PrivacyPolicy::NoPrivacy are required
|
||||
|
||||
return maxPrivacy;
|
||||
}
|
||||
|
||||
|
||||
TransactionBuilderResult TransactionEffects::ApproveAndBuild(
|
||||
const Consensus::Params& consensus,
|
||||
const CWallet& wallet,
|
||||
const CChain& chain,
|
||||
const TransactionStrategy& strategy) const
|
||||
{
|
||||
auto requiredPrivacy = this->GetRequiredPrivacyPolicy();
|
||||
if (!strategy.IsCompatibleWith(requiredPrivacy)) {
|
||||
return TransactionBuilderResult(strprintf(
|
||||
"The specified privacy policy, %s, does not permit the creation of "
|
||||
"the requested transaction. Select %s or weaker to allow this transaction "
|
||||
"to be constructed.",
|
||||
strategy.PolicyName(),
|
||||
TransactionStrategy::ToString(requiredPrivacy)
|
||||
));
|
||||
}
|
||||
|
||||
int nextBlockHeight = chain.Height() + 1;
|
||||
|
||||
// Allow Orchard recipients by setting an Orchard anchor.
|
||||
std::optional<uint256> orchardAnchor;
|
||||
if (spendable.sproutNoteEntries.empty() && nPreferredTxVersion > ZIP225_MIN_TX_VERSION && this->anchorConfirmations > 0) {
|
||||
auto orchardAnchorHeight = nextBlockHeight - this->anchorConfirmations;
|
||||
if (consensus.NetworkUpgradeActive(orchardAnchorHeight, Consensus::UPGRADE_NU5)) {
|
||||
LOCK(cs_main);
|
||||
auto anchorBlockIndex = chain[orchardAnchorHeight];
|
||||
assert(anchorBlockIndex != nullptr);
|
||||
orchardAnchor = anchorBlockIndex->hashFinalOrchardRoot;
|
||||
}
|
||||
}
|
||||
|
||||
auto builder = TransactionBuilder(consensus, nextBlockHeight, orchardAnchor, &wallet);
|
||||
builder.SetFee(fee);
|
||||
|
||||
// Track the total of notes that we've added to the builder. This
|
||||
// shouldn't strictly be necessary, given `spendable.LimitToAmount`
|
||||
CAmount sum = 0;
|
||||
CAmount targetAmount = payments.Total() + fee;
|
||||
|
||||
// Create Sapling outpoints
|
||||
std::vector<SaplingOutPoint> saplingOutPoints;
|
||||
std::vector<SaplingNote> saplingNotes;
|
||||
std::vector<SaplingExtendedSpendingKey> saplingKeys;
|
||||
|
||||
for (const auto& t : spendable.saplingNoteEntries) {
|
||||
saplingOutPoints.push_back(t.op);
|
||||
saplingNotes.push_back(t.note);
|
||||
|
||||
libzcash::SaplingExtendedSpendingKey saplingKey;
|
||||
assert(wallet.GetSaplingExtendedSpendingKey(t.address, saplingKey));
|
||||
saplingKeys.push_back(saplingKey);
|
||||
|
||||
sum += t.note.value();
|
||||
if (sum >= targetAmount) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch Sapling anchor and witnesses, and Orchard Merkle paths.
|
||||
uint256 anchor;
|
||||
std::vector<std::optional<SaplingWitness>> witnesses;
|
||||
std::vector<std::pair<libzcash::OrchardSpendingKey, orchard::SpendInfo>> orchardSpendInfo;
|
||||
{
|
||||
LOCK(wallet.cs_wallet);
|
||||
if (!wallet.GetSaplingNoteWitnesses(saplingOutPoints, anchorConfirmations, witnesses, anchor)) {
|
||||
// This error should not appear once we're nAnchorConfirmations blocks past
|
||||
// Sapling activation.
|
||||
return TransactionBuilderResult("Insufficient Sapling witnesses.");
|
||||
}
|
||||
if (builder.GetOrchardAnchor().has_value()) {
|
||||
orchardSpendInfo = wallet.GetOrchardSpendInfo(spendable.orchardNoteMetadata, builder.GetOrchardAnchor().value());
|
||||
}
|
||||
}
|
||||
|
||||
// Add Orchard spends
|
||||
for (size_t i = 0; i < orchardSpendInfo.size(); i++) {
|
||||
auto spendInfo = std::move(orchardSpendInfo[i]);
|
||||
if (!builder.AddOrchardSpend(
|
||||
std::move(spendInfo.first),
|
||||
std::move(spendInfo.second)))
|
||||
{
|
||||
return TransactionBuilderResult(
|
||||
strprintf("Failed to add Orchard note to transaction (check %s for details)", GetDebugLogPath())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add Sapling spends
|
||||
for (size_t i = 0; i < saplingNotes.size(); i++) {
|
||||
if (!witnesses[i]) {
|
||||
return TransactionBuilderResult(strprintf(
|
||||
"Missing witness for Sapling note at outpoint %s",
|
||||
spendable.saplingNoteEntries[i].op.ToString()
|
||||
));
|
||||
}
|
||||
|
||||
builder.AddSaplingSpend(saplingKeys[i].expsk, saplingNotes[i], anchor, witnesses[i].value());
|
||||
}
|
||||
|
||||
// Add outputs
|
||||
for (const auto& r : payments.GetResolvedPayments()) {
|
||||
std::visit(match {
|
||||
[&](const CKeyID& keyId) {
|
||||
builder.AddTransparentOutput(keyId, r.amount);
|
||||
},
|
||||
[&](const CScriptID& scriptId) {
|
||||
builder.AddTransparentOutput(scriptId, r.amount);
|
||||
},
|
||||
[&](const libzcash::SaplingPaymentAddress& addr) {
|
||||
builder.AddSaplingOutput(
|
||||
externalOVK, addr, r.amount,
|
||||
r.memo.has_value() ? r.memo.value().ToBytes() : Memo::NoMemo().ToBytes());
|
||||
},
|
||||
[&](const libzcash::OrchardRawAddress& addr) {
|
||||
builder.AddOrchardOutput(
|
||||
externalOVK, addr, r.amount,
|
||||
r.memo.has_value() ? std::optional(r.memo.value().ToBytes()) : std::nullopt);
|
||||
}
|
||||
}, r.address);
|
||||
}
|
||||
|
||||
// Add transparent utxos
|
||||
for (const auto& out : spendable.utxos) {
|
||||
const CTxOut& txOut = out.tx->vout[out.i];
|
||||
builder.AddTransparentInput(COutPoint(out.tx->GetHash(), out.i), txOut.scriptPubKey, txOut.nValue);
|
||||
|
||||
sum += txOut.nValue;
|
||||
if (sum >= targetAmount) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Find Sprout witnesses
|
||||
// When spending notes, take a snapshot of note witnesses and anchors as the treestate will
|
||||
// change upon arrival of new blocks which contain joinsplit transactions. This is likely
|
||||
// to happen as creating a chained joinsplit transaction can take longer than the block interval.
|
||||
// So, we need to take locks on cs_main and wallet.cs_wallet so that the witnesses aren't
|
||||
// updated.
|
||||
//
|
||||
// TODO: these locks would ideally be shared for selection of Sapling anchors and witnesses
|
||||
// as well.
|
||||
std::vector<std::optional<SproutWitness>> vSproutWitnesses;
|
||||
{
|
||||
LOCK2(cs_main, wallet.cs_wallet);
|
||||
std::vector<JSOutPoint> vOutPoints;
|
||||
for (const auto& t : spendable.sproutNoteEntries) {
|
||||
vOutPoints.push_back(t.jsop);
|
||||
}
|
||||
|
||||
// inputAnchor is not needed by builder.AddSproutInput as it is for Sapling.
|
||||
uint256 inputAnchor;
|
||||
if (!wallet.GetSproutNoteWitnesses(vOutPoints, anchorConfirmations, vSproutWitnesses, inputAnchor)) {
|
||||
// This error should not appear once we're nAnchorConfirmations blocks past
|
||||
// Sprout activation.
|
||||
return TransactionBuilderResult("Insufficient Sprout witnesses.");
|
||||
}
|
||||
}
|
||||
|
||||
// Add Sprout spends
|
||||
for (int i = 0; i < spendable.sproutNoteEntries.size(); i++) {
|
||||
const auto& t = spendable.sproutNoteEntries[i];
|
||||
libzcash::SproutSpendingKey sk;
|
||||
assert(wallet.GetSproutSpendingKey(t.address, sk));
|
||||
|
||||
builder.AddSproutInput(sk, t.note, vSproutWitnesses[i].value());
|
||||
|
||||
sum += t.note.value();
|
||||
if (sum >= targetAmount) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::visit(match {
|
||||
[&](const SproutPaymentAddress& addr) {
|
||||
builder.SendChangeToSprout(addr);
|
||||
},
|
||||
[&](const RecipientAddress& addr) {
|
||||
builder.SendChangeTo(addr, internalOVK);
|
||||
}
|
||||
}, changeAddr);
|
||||
|
||||
// Build the transaction
|
||||
return builder.Build();
|
||||
}
|
|
@ -5,7 +5,10 @@
|
|||
#ifndef ZCASH_WALLET_WALLET_TX_BUILDER_H
|
||||
#define ZCASH_WALLET_WALLET_TX_BUILDER_H
|
||||
|
||||
#include "consensus/params.h"
|
||||
#include "transaction_builder.h"
|
||||
#include "wallet/memo.h"
|
||||
#include "wallet/wallet.h"
|
||||
|
||||
using namespace libzcash;
|
||||
|
||||
|
@ -25,4 +28,272 @@ public:
|
|||
std::optional<Memo> memo) :
|
||||
RecipientMapping(ua, address), amount(amount), memo(memo) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* A requested payment that has not yet been resolved to a
|
||||
* specific recipient address.
|
||||
*/
|
||||
class Payment {
|
||||
private:
|
||||
PaymentAddress address;
|
||||
CAmount amount;
|
||||
std::optional<Memo> memo;
|
||||
public:
|
||||
Payment(
|
||||
PaymentAddress address,
|
||||
CAmount amount,
|
||||
std::optional<Memo> memo) :
|
||||
address(address), amount(amount), memo(memo) {}
|
||||
|
||||
const PaymentAddress& GetAddress() const {
|
||||
return address;
|
||||
}
|
||||
|
||||
CAmount GetAmount() const {
|
||||
return amount;
|
||||
}
|
||||
|
||||
const std::optional<Memo>& GetMemo() const {
|
||||
return memo;
|
||||
}
|
||||
};
|
||||
|
||||
class Payments {
|
||||
private:
|
||||
std::vector<ResolvedPayment> payments;
|
||||
std::set<OutputPool> recipientPools;
|
||||
CAmount t_outputs_total{0};
|
||||
CAmount sapling_outputs_total{0};
|
||||
CAmount orchard_outputs_total{0};
|
||||
public:
|
||||
Payments(std::vector<ResolvedPayment> payments): payments(payments) {
|
||||
for (const ResolvedPayment& payment : payments) {
|
||||
std::visit(match {
|
||||
[&](const CKeyID& addr) {
|
||||
t_outputs_total += payment.amount;
|
||||
recipientPools.insert(OutputPool::Transparent);
|
||||
},
|
||||
[&](const CScriptID& addr) {
|
||||
t_outputs_total += payment.amount;
|
||||
recipientPools.insert(OutputPool::Transparent);
|
||||
},
|
||||
[&](const libzcash::SaplingPaymentAddress& addr) {
|
||||
sapling_outputs_total += payment.amount;
|
||||
recipientPools.insert(OutputPool::Sapling);
|
||||
},
|
||||
[&](const libzcash::OrchardRawAddress& addr) {
|
||||
orchard_outputs_total += payment.amount;
|
||||
recipientPools.insert(OutputPool::Orchard);
|
||||
}
|
||||
}, payment.address);
|
||||
}
|
||||
}
|
||||
|
||||
const std::set<OutputPool>& GetRecipientPools() const {
|
||||
return recipientPools;
|
||||
}
|
||||
|
||||
bool HasTransparentRecipient() const {
|
||||
return recipientPools.count(OutputPool::Transparent) > 0;
|
||||
}
|
||||
|
||||
bool HasSaplingRecipient() const {
|
||||
return recipientPools.count(OutputPool::Sapling) > 0;
|
||||
}
|
||||
|
||||
bool HasOrchardRecipient() const {
|
||||
return recipientPools.count(OutputPool::Orchard) > 0;
|
||||
}
|
||||
|
||||
const std::vector<ResolvedPayment>& GetResolvedPayments() const {
|
||||
return payments;
|
||||
}
|
||||
|
||||
CAmount GetTransparentBalance() const {
|
||||
return t_outputs_total;
|
||||
}
|
||||
|
||||
CAmount GetSaplingBalance() const {
|
||||
return sapling_outputs_total;
|
||||
}
|
||||
|
||||
CAmount GetOrchardBalance() const {
|
||||
return orchard_outputs_total;
|
||||
}
|
||||
|
||||
CAmount Total() const {
|
||||
return
|
||||
t_outputs_total +
|
||||
sapling_outputs_total +
|
||||
orchard_outputs_total;
|
||||
}
|
||||
};
|
||||
|
||||
typedef std::variant<
|
||||
RecipientAddress,
|
||||
SproutPaymentAddress> ChangeAddress;
|
||||
|
||||
class TransactionEffects {
|
||||
private:
|
||||
AccountId sendFromAccount;
|
||||
uint32_t anchorConfirmations{1};
|
||||
SpendableInputs spendable;
|
||||
Payments payments;
|
||||
ChangeAddress changeAddr;
|
||||
CAmount fee{0};
|
||||
uint256 internalOVK;
|
||||
uint256 externalOVK;
|
||||
|
||||
public:
|
||||
TransactionEffects(
|
||||
AccountId sendFromAccount,
|
||||
uint32_t anchorConfirmations,
|
||||
SpendableInputs spendable,
|
||||
Payments payments,
|
||||
ChangeAddress changeAddr,
|
||||
CAmount fee,
|
||||
uint256 internalOVK,
|
||||
uint256 externalOVK) :
|
||||
sendFromAccount(sendFromAccount),
|
||||
anchorConfirmations(anchorConfirmations),
|
||||
spendable(spendable),
|
||||
payments(payments),
|
||||
changeAddr(changeAddr),
|
||||
fee(fee),
|
||||
internalOVK(internalOVK),
|
||||
externalOVK(externalOVK) {}
|
||||
|
||||
PrivacyPolicy GetRequiredPrivacyPolicy() const;
|
||||
|
||||
const SpendableInputs& GetSpendable() const {
|
||||
return spendable;
|
||||
}
|
||||
|
||||
const Payments& GetPayments() const {
|
||||
return payments;
|
||||
}
|
||||
|
||||
CAmount GetFee() const {
|
||||
return fee;
|
||||
}
|
||||
|
||||
TransactionBuilderResult ApproveAndBuild(
|
||||
const Consensus::Params& consensus,
|
||||
const CWallet& wallet,
|
||||
const CChain& chain,
|
||||
const TransactionStrategy& strategy) const;
|
||||
};
|
||||
|
||||
enum class AddressResolutionError {
|
||||
SproutSpendNotPermitted,
|
||||
SproutRecipientNotPermitted,
|
||||
TransparentRecipientNotPermitted,
|
||||
InsufficientSaplingFunds,
|
||||
UnifiedAddressResolutionError,
|
||||
ChangeAddressSelectionError
|
||||
};
|
||||
|
||||
class DustThresholdError {
|
||||
public:
|
||||
CAmount dustThreshold;
|
||||
CAmount available;
|
||||
CAmount changeAmount;
|
||||
|
||||
DustThresholdError(CAmount dustThreshold, CAmount available, CAmount changeAmount):
|
||||
dustThreshold(dustThreshold), available(available), changeAmount(changeAmount) { }
|
||||
};
|
||||
|
||||
class ChangeNotAllowedError {
|
||||
public:
|
||||
CAmount available;
|
||||
CAmount required;
|
||||
|
||||
ChangeNotAllowedError(CAmount available, CAmount required):
|
||||
available(available), required(required) { }
|
||||
};
|
||||
|
||||
class InsufficientFundsError {
|
||||
public:
|
||||
CAmount available;
|
||||
CAmount required;
|
||||
bool transparentCoinbasePermitted;
|
||||
|
||||
InsufficientFundsError(CAmount available, CAmount required, bool transparentCoinbasePermitted):
|
||||
available(available), required(required), transparentCoinbasePermitted(transparentCoinbasePermitted) { }
|
||||
};
|
||||
|
||||
class ExcessOrchardActionsError {
|
||||
public:
|
||||
uint32_t orchardNotes;
|
||||
|
||||
ExcessOrchardActionsError(uint32_t orchardNotes): orchardNotes(orchardNotes) { }
|
||||
};
|
||||
|
||||
typedef std::variant<
|
||||
AddressResolutionError,
|
||||
InsufficientFundsError,
|
||||
DustThresholdError,
|
||||
ChangeNotAllowedError,
|
||||
ExcessOrchardActionsError> InputSelectionError;
|
||||
|
||||
typedef std::variant<
|
||||
InputSelectionError,
|
||||
std::pair<SpendableInputs, Payments>> InputSelectionResult;
|
||||
|
||||
typedef std::variant<
|
||||
InputSelectionError,
|
||||
TransactionEffects> PrepareTransactionResult;
|
||||
|
||||
class WalletTxBuilder {
|
||||
private:
|
||||
const CWallet& wallet;
|
||||
CFeeRate minRelayFee;
|
||||
uint32_t maxOrchardActions;
|
||||
|
||||
/**
|
||||
* Compute the default dust threshold
|
||||
*/
|
||||
CAmount DefaultDustThreshold() const;
|
||||
|
||||
/**
|
||||
* Select inputs sufficient to fulfill the specified requested payments,
|
||||
* and choose unified address receivers based upon the available inputs
|
||||
* and the requested transaction strategy.
|
||||
*/
|
||||
InputSelectionResult ResolveInputsAndPayments(
|
||||
SpendableInputs& spendable,
|
||||
const std::vector<Payment>& payments,
|
||||
TransactionStrategy strategy,
|
||||
CAmount fee) const;
|
||||
|
||||
/**
|
||||
* Compute the internal and external OVKs to use in transaction construction, given
|
||||
* the spendable inputs.
|
||||
*/
|
||||
std::pair<uint256, uint256> SelectOVKs(
|
||||
const ZTXOSelector& selector,
|
||||
const SpendableInputs& spendable) const;
|
||||
|
||||
public:
|
||||
WalletTxBuilder(const CWallet& wallet, CFeeRate minRelayFee):
|
||||
wallet(wallet), minRelayFee(minRelayFee) {}
|
||||
|
||||
static bool AllowTransparentCoinbase(
|
||||
const std::vector<Payment>& payments,
|
||||
TransactionStrategy strategy);
|
||||
|
||||
SpendableInputs FindAllSpendableInputs(
|
||||
const ZTXOSelector& selector,
|
||||
bool allowTransparentCoinbase,
|
||||
int32_t minDepth) const;
|
||||
|
||||
PrepareTransactionResult PrepareTransaction(
|
||||
const ZTXOSelector& selector,
|
||||
SpendableInputs& spendable,
|
||||
const std::vector<Payment>& payments,
|
||||
TransactionStrategy strategy,
|
||||
CAmount fee,
|
||||
uint32_t anchorConfirmations) const;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
Loading…
Reference in New Issue