Address review feedback for ZIP 317 fees in wallet

This commit is contained in:
Greg Pfeil 2023-04-13 19:16:35 -06:00
parent c54c4ee987
commit 0b9b5c8dc1
No known key found for this signature in database
GPG Key ID: 1193ACD196ED61F2
6 changed files with 45 additions and 49 deletions

View File

@ -12,7 +12,7 @@
from test_framework.mininode import COIN from test_framework.mininode import COIN
from decimal import Decimal from decimal import Decimal
# The fee per logical action, in ZAT. See https://zips.z.cash/zip-0317#fee-calculation. # The fee per logical action, in zatoshis. See https://zips.z.cash/zip-0317#fee-calculation.
MARGINAL_FEE = 5000 MARGINAL_FEE = 5000
# The lower bound on the number of logical actions in a tx, for purposes of fee calculation. See # The lower bound on the number of logical actions in a tx, for purposes of fee calculation. See
@ -24,7 +24,7 @@ GRACE_ACTIONS = 2
# https://zips.z.cash/zip-0317#recommended-algorithm-for-block-template-construction # https://zips.z.cash/zip-0317#recommended-algorithm-for-block-template-construction
WEIGHT_RATIO_CAP = 4 WEIGHT_RATIO_CAP = 4
# The Zcashd RPC sentinel value to indicate the conventional_fee when a positional argument is # The zcashd RPC sentinel value to indicate the conventional_fee when a positional argument is
# required. # required.
ZIP_317_FEE = -1 ZIP_317_FEE = -1

View File

@ -124,7 +124,7 @@ void ThrowInputSelectionError(
"Insufficient funds: have %s, %s", "Insufficient funds: have %s, %s",
FormatMoney(err.available), FormatMoney(err.available),
examine(err.reason, match { examine(err.reason, match {
[](const QuasiChangeError& qce) { [](const PhantomChangeError& qce) {
return strprintf( return strprintf(
"need %s more to surpass the dust threshold and avoid being " "need %s more to surpass the dust threshold and avoid being "
"forced to over-pay the fee. Alternatively, you could specify " "forced to over-pay the fee. Alternatively, you could specify "
@ -137,7 +137,7 @@ void ThrowInputSelectionError(
return strprintf("need %s", FormatMoney(ife.required)); return strprintf("need %s", FormatMoney(ife.required));
}, },
// TODO: Add the fee here, so we can suggest specifying an explicit fee (see // TODO: Add the fee here, so we can suggest specifying an explicit fee (see
// `QuasiChangeError`). // `PhantomChangeError`).
[](const DustThresholdError& dte) { [](const DustThresholdError& dte) {
return strprintf( return strprintf(
"need %s more to avoid creating invalid change output %s (dust threshold is %s)", "need %s more to avoid creating invalid change output %s (dust threshold is %s)",

View File

@ -4813,7 +4813,7 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
" the output is being sent to a transparent address, its an error to include this field.\n" " the output is being sent to a transparent address, its an error to include this field.\n"
" }, ... ]\n" " }, ... ]\n"
"3. minconf (numeric, optional, default=" + strprintf("%u", DEFAULT_NOTE_CONFIRMATIONS) + ") Only use funds confirmed at least this many times.\n" "3. minconf (numeric, optional, default=" + strprintf("%u", DEFAULT_NOTE_CONFIRMATIONS) + ") Only use funds confirmed at least this many times.\n"
"4. fee (numeric, optional, default=-1) The fee amount to attach to this transaction. The default behavior\n" "4. fee (numeric, optional, default=-1) The fee amount in " + CURRENCY_UNIT + " to attach to this transaction. The default behavior\n"
" is to use a fee calculated according to ZIP 317.\n" " is to use a fee calculated according to ZIP 317.\n"
"5. privacyPolicy (string, optional, default=\"LegacyCompat\") Policy for what information leakage is acceptable.\n" "5. privacyPolicy (string, optional, default=\"LegacyCompat\") Policy for what information leakage is acceptable.\n"
" One of the following strings:\n" " One of the following strings:\n"

View File

@ -200,31 +200,26 @@ SpendableInputs WalletTxBuilder::FindAllSpendableInputs(
return wallet.FindSpendableInputs(selector, minDepth, std::nullopt); return wallet.FindSpendableInputs(selector, minDepth, std::nullopt);
} }
static size_t NotOne(size_t n) static size_t PadCount(size_t n)
{ {
return n == 1 ? 2 : n; return n == 1 ? 2 : n;
} }
/// ISSUES:
/// • z_shieldcoinbase currently checks the fee in advance of calling PrepareTransaction in order to
/// determine the payment amount. But that fee possibly isnt correct. We could perhaps have _no_
/// payments in z_shieldcoinbase, but provide an explicit change address? (Dont expose this via
/// RPC, and give it a different name, e.g. `NetFundsRecipient` in the WalletTxBuilder interface)
static CAmount static CAmount
CalcZIP317Fee( CalcZIP317Fee(
const std::optional<SpendableInputs>& inputs, const std::optional<SpendableInputs>& inputs,
const std::vector<ResolvedPayment>& payments, const std::vector<ResolvedPayment>& payments,
const std::optional<ChangeAddress>& changeAddr) const std::optional<ChangeAddress>& changeAddr)
{ {
std::vector<CTxOut> vouts{}; std::vector<CTxOut> vout{};
size_t sproutOutputCount{}, saplingOutputCount{}, orchardOutputCount{}; size_t sproutOutputCount{}, saplingOutputCount{}, orchardOutputCount{};
for (const auto& payment : payments) { for (const auto& payment : payments) {
std::visit(match { std::visit(match {
[&](const CKeyID& addr) { [&](const CKeyID& addr) {
vouts.emplace_back(payment.amount, GetScriptForDestination(addr)); vout.emplace_back(payment.amount, GetScriptForDestination(addr));
}, },
[&](const CScriptID& addr) { [&](const CScriptID& addr) {
vouts.emplace_back(payment.amount, GetScriptForDestination(addr)); vout.emplace_back(payment.amount, GetScriptForDestination(addr));
}, },
[&](const libzcash::SaplingPaymentAddress&) { [&](const libzcash::SaplingPaymentAddress&) {
++saplingOutputCount; ++saplingOutputCount;
@ -241,10 +236,10 @@ CalcZIP317Fee(
[&](const RecipientAddress& addr) { [&](const RecipientAddress& addr) {
examine(addr, match { examine(addr, match {
[&](const CKeyID& taddr) { [&](const CKeyID& taddr) {
vouts.emplace_back(0, GetScriptForDestination(taddr)); vout.emplace_back(0, GetScriptForDestination(taddr));
}, },
[&](const CScriptID taddr) { [&](const CScriptID taddr) {
vouts.emplace_back(0, GetScriptForDestination(taddr)); vout.emplace_back(0, GetScriptForDestination(taddr));
}, },
[&](const libzcash::SaplingPaymentAddress&) { ++saplingOutputCount; }, [&](const libzcash::SaplingPaymentAddress&) { ++saplingOutputCount; },
[&](const libzcash::OrchardRawAddress&) { ++orchardOutputCount; } [&](const libzcash::OrchardRawAddress&) { ++orchardOutputCount; }
@ -253,38 +248,34 @@ CalcZIP317Fee(
}); });
} }
size_t logicalActionCount; std::vector<CTxIn> vin{};
size_t sproutInputCount = 0;
size_t saplingInputCount = 0;
size_t orchardInputCount = 0;
if (inputs.has_value()) { if (inputs.has_value()) {
std::vector<CTxIn> vins{}; for (const auto& utxo : inputs->utxos) {
for (const auto& utxo : inputs.value().utxos) { vin.emplace_back(
vins.emplace_back(
COutPoint(utxo.tx->GetHash(), utxo.i), COutPoint(utxo.tx->GetHash(), utxo.i),
utxo.tx->vout[utxo.i].scriptPubKey); utxo.tx->vout[utxo.i].scriptPubKey);
} }
logicalActionCount = CalculateLogicalActionCount( sproutInputCount = inputs->sproutNoteEntries.size();
vins, saplingInputCount = inputs->saplingNoteEntries.size();
vouts, orchardInputCount = inputs->orchardNoteMetadata.size();
std::max(inputs.value().sproutNoteEntries.size(), sproutOutputCount),
inputs.value().saplingNoteEntries.size(),
NotOne(saplingOutputCount),
NotOne(std::max(inputs.value().orchardNoteMetadata.size(), orchardOutputCount)));
} else {
logicalActionCount = CalculateLogicalActionCount(
{},
vouts,
sproutOutputCount,
0,
NotOne(saplingOutputCount),
NotOne(orchardOutputCount));
} }
size_t logicalActionCount = CalculateLogicalActionCount(
vin,
vout,
std::max(sproutInputCount, sproutOutputCount),
saplingInputCount,
PadCount(saplingOutputCount),
PadCount(std::max(orchardInputCount, orchardOutputCount)));
return CalculateConventionalFee(logicalActionCount); return CalculateConventionalFee(logicalActionCount);
} }
InvalidFundsError ReportInvalidFunds( InvalidFundsError ReportInvalidFunds(
const SpendableInputs& spendable, const SpendableInputs& spendable,
bool hasQuasiChange, bool hasPhantomChange,
CAmount fee, CAmount fee,
CAmount dustThreshold, CAmount dustThreshold,
CAmount targetAmount, CAmount targetAmount,
@ -292,9 +283,9 @@ InvalidFundsError ReportInvalidFunds(
{ {
return InvalidFundsError( return InvalidFundsError(
spendable.Total(), spendable.Total(),
hasQuasiChange hasPhantomChange
// TODO: NEED TESTS TO EXERCISE THIS // TODO: NEED TESTS TO EXERCISE THIS
? InvalidFundsReason(QuasiChangeError(fee, dustThreshold)) ? InvalidFundsReason(PhantomChangeError(fee, dustThreshold))
: (changeAmount > 0 && changeAmount < dustThreshold : (changeAmount > 0 && changeAmount < dustThreshold
// TODO: we should provide the option for the caller to explicitly forego change // 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 // (definitionally an amount below the dust amount) and send the extra to the
@ -367,6 +358,7 @@ WalletTxBuilder::IterateLimit(
CAmount targetAmount{0}; CAmount targetAmount{0};
do { do {
// NB: This makes a fresh copy so that we start from the full set of notes when we re-limit.
spendableMut = spendable; spendableMut = spendable;
targetAmount = sendAmount + updatedFee; targetAmount = sendAmount + updatedFee;
@ -459,8 +451,8 @@ WalletTxBuilder::ResolveInputsAndPayments(
CAmount maxOrchardAvailable = spendableMut.GetOrchardTotal(); CAmount maxOrchardAvailable = spendableMut.GetOrchardTotal();
uint32_t orchardOutputs{0}; uint32_t orchardOutputs{0};
// we can only select Orchard addresses if there are sufficient non-Sprout // we can only select Orchard addresses if were not sending from Sprout, since there is no tx
// funds to cover the total payments + fee. // version where both Sprout and Orchard are valid.
bool canResolveOrchard = afterNU5 && !selector.SelectsSprout(); bool canResolveOrchard = afterNU5 && !selector.SelectsSprout();
std::vector<ResolvedPayment> resolvedPayments; std::vector<ResolvedPayment> resolvedPayments;
std::optional<AddressResolutionError> resolutionError; std::optional<AddressResolutionError> resolutionError;

View File

@ -241,12 +241,16 @@ enum class AddressResolutionError {
RevealingReceiverAmountsNotAllowed, RevealingReceiverAmountsNotAllowed,
}; };
class QuasiChangeError { /// Phantom change is change that appears to exist until we add the output for it, at which point it
/// is consumed by the increase to the conventional fee. When we are at the limit of selectable
/// notes, this makes it impossible to create the transaction without either creating a 0-valued
/// output or overpaying the fee.
class PhantomChangeError {
public: public:
CAmount finalFee; CAmount finalFee;
CAmount dustThreshold; CAmount dustThreshold;
QuasiChangeError(CAmount finalFee, CAmount dustThreshold): PhantomChangeError(CAmount finalFee, CAmount dustThreshold):
finalFee(finalFee), dustThreshold(dustThreshold) { } finalFee(finalFee), dustThreshold(dustThreshold) { }
}; };
@ -268,7 +272,7 @@ public:
}; };
typedef std::variant< typedef std::variant<
QuasiChangeError, PhantomChangeError,
InsufficientFundsError, InsufficientFundsError,
DustThresholdError> InvalidFundsReason; DustThresholdError> InvalidFundsReason;

View File

@ -15,9 +15,9 @@ CAmount CalculateConventionalFee(size_t logicalActionCount) {
} }
template<typename T> template<typename T>
size_t GetUTXOFieldSize(const std::vector<T>& utxos) { static size_t GetTxIOFieldSize(const std::vector<T>& txIOs) {
auto size = GetSerializeSize(utxos, SER_NETWORK, PROTOCOL_VERSION); auto size = GetSerializeSize(txIOs, SER_NETWORK, PROTOCOL_VERSION);
auto countSize = GetSizeOfCompactSize(utxos.size()); auto countSize = GetSizeOfCompactSize(txIOs.size());
return size - countSize; return size - countSize;
} }
@ -28,8 +28,8 @@ size_t CalculateLogicalActionCount(
unsigned int saplingSpendCount, unsigned int saplingSpendCount,
unsigned int saplingOutputCount, unsigned int saplingOutputCount,
unsigned int orchardActionCount) { unsigned int orchardActionCount) {
const size_t tx_in_total_size = GetUTXOFieldSize(vin); const size_t tx_in_total_size = GetTxIOFieldSize(vin);
const size_t tx_out_total_size = GetUTXOFieldSize(vout); const size_t tx_out_total_size = GetTxIOFieldSize(vout);
return std::max(ceil_div(tx_in_total_size, P2PKH_STANDARD_INPUT_SIZE), return std::max(ceil_div(tx_in_total_size, P2PKH_STANDARD_INPUT_SIZE),
ceil_div(tx_out_total_size, P2PKH_STANDARD_OUTPUT_SIZE)) + ceil_div(tx_out_total_size, P2PKH_STANDARD_OUTPUT_SIZE)) +